RadrootsFileMaintenanceScan.swift (6144B)
1 import Darwin 2 import Foundation 3 4 /// The mutex serializes the retained directory stream and synchronous unlink. 5 /// No callbacks or async work run under it. Final deinitialization closes both 6 /// scan descriptors, then releases its retained exclusive maintenance reservation. 7 public final class RadrootsFileMaintenanceScan: @unchecked Sendable { 8 private let reservation: RadrootsFileMaintenanceReservation 9 private let directory: RadrootsAtomicFile.Directory 10 private let stream: UnsafeMutablePointer<DIR> 11 private let lock = NSLock() 12 private let scanID = UUID() 13 private var invalidated = false 14 15 init(reservation: RadrootsFileMaintenanceReservation, relativePath: String) throws { 16 let parts = relativePath.isEmpty ? [] : relativePath.split(separator: "/", omittingEmptySubsequences: false).map(String.init) 17 guard parts.allSatisfy({ !$0.isEmpty && $0 != "." && $0 != ".." && !$0.utf8.contains(0) }) else { 18 throw RadrootsAppleFileError.invalidRequest 19 } 20 try reservation.validate() 21 let directory = try RadrootsAtomicFile.Directory.open(reservation.gate.directory.parts + parts, create: false) 22 var owned = false 23 defer { 24 if !owned { 25 Darwin.close(directory.descriptor) 26 } 27 } 28 let copy = Darwin.fcntl(directory.descriptor, F_DUPFD_CLOEXEC, 0) 29 guard copy >= 0 else { throw RadrootsAppleFileError.permanentFailure } 30 guard let stream = Darwin.fdopendir(copy) else { 31 Darwin.close(copy) 32 throw RadrootsAppleFileError.permanentFailure 33 } 34 do { 35 try reservation.validate() 36 try directory.validate() 37 } catch { 38 Darwin.closedir(stream) 39 throw error 40 } 41 self.reservation = reservation 42 self.directory = directory 43 self.stream = stream 44 owned = true 45 } 46 47 deinit { 48 Darwin.closedir(stream) 49 Darwin.close(directory.descriptor) 50 } 51 52 /// At most 64 directory entries per call, including entries not returned. 53 /// The caller owns total work budgets and must not infer absence from a prefix. 54 /// An I/O or identity error invalidates this scan; begin a fresh inventory. 55 public func next(limit: Int = 64) throws -> RadrootsFileMaintenancePage { 56 guard (1 ... 64).contains(limit) else { throw RadrootsAppleFileError.invalidRequest } 57 return try perform { 58 try validate() 59 var entries: [RadrootsFileMaintenanceEntry] = [] 60 var scanned = 0 61 var reachedEnd = false 62 for _ in 0 ..< limit { 63 errno = 0 64 guard let entry = Darwin.readdir(stream) else { 65 guard errno == 0 else { throw RadrootsAppleFileError.permanentFailure } 66 reachedEnd = true 67 break 68 } 69 scanned += 1 70 let count = Int(entry.pointee.d_namlen) 71 let name = withUnsafeBytes(of: entry.pointee.d_name) { bytes -> String? in 72 guard count <= bytes.count else { return nil } 73 return String(bytes: bytes.prefix(count), encoding: .utf8) 74 } 75 // Unrepresentable names are an incomplete inventory, not absence. 76 guard let name else { throw RadrootsAppleFileError.permanentFailure } 77 if name == "." || name == ".." || name == RadrootsFileMaintenanceGate.name { 78 continue 79 } 80 var value = stat() 81 let found = name.withCString { Darwin.fstatat(directory.descriptor, $0, &value, AT_SYMLINK_NOFOLLOW) } 82 if found != 0, errno == ENOENT { 83 continue 84 } 85 guard found == 0 else { throw RadrootsAppleFileError.permanentFailure } 86 entries.append(RadrootsFileMaintenanceEntry(name: name, value: value, scanID: scanID)) 87 } 88 try validate() 89 return RadrootsFileMaintenancePage(entries: entries, scannedEntries: scanned, reachedEnd: reachedEnd) 90 } 91 } 92 93 /// Only an unchanged, singly linked regular file from this scan is eligible. 94 /// The host must prove reference absence and grace under this reservation. 95 /// False means retained/absent. Errors after unlink are ambiguous: inspect 96 /// again; never assume an error means the file remains or reuse an old proof. 97 public func remove(_ entry: RadrootsFileMaintenanceEntry) throws -> Bool { 98 guard entry.scanID == scanID else { throw RadrootsAppleFileError.invalidRequest } 99 return try perform { 100 try validate() 101 guard entry.kind == .regularFile, entry.identity.links == 1, 102 entry.name != RadrootsFileMaintenanceGate.name else { return false } 103 var current = stat() 104 let found = entry.name.withCString { Darwin.fstatat(directory.descriptor, $0, ¤t, AT_SYMLINK_NOFOLLOW) } 105 if found != 0, errno == ENOENT { 106 return false 107 } 108 guard found == 0 else { throw RadrootsAppleFileError.permanentFailure } 109 guard RadrootsFileMaintenanceIdentity(current) == entry.identity else { return false } 110 guard entry.name.withCString({ Darwin.unlinkat(directory.descriptor, $0, 0) }) == 0 else { 111 throw RadrootsAppleFileError.permanentFailure 112 } 113 guard Darwin.fsync(directory.descriptor) == 0 else { throw RadrootsAppleFileError.permanentFailure } 114 try validate() 115 return true 116 } 117 } 118 119 private func validate() throws { 120 try reservation.validate() 121 try directory.validate() 122 } 123 124 private func perform<T>(_ operation: () throws -> T) throws -> T { 125 try lock.withLock { 126 guard !invalidated else { throw RadrootsAppleFileError.permanentFailure } 127 do { 128 return try operation() 129 } catch { 130 invalidated = true 131 throw error 132 } 133 } 134 } 135 }