apple_kit

Apple-native services for Radroots iOS and macOS apps
git clone https://radroots.dev/git/apple_kit.git
Log | Files | Refs | README | LICENSE

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, &current, 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 }