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

RadrootsAppleFileMaintenance.swift (4967B)


      1 import Darwin
      2 import Foundation
      3 
      4 /// Coordinates cooperating users of one governed root. Every operation that can
      5 /// create a reference, read, replace or write a collected file must hold a use
      6 /// reservation until its underlying work has drained, including after cancellation.
      7 /// This mechanism does not establish application reference absence or retention policy.
      8 public struct RadrootsAppleFileMaintenance: Sendable {
      9     private let root: URL
     10 
     11     public init(root: URL) {
     12         self.root = root
     13     }
     14 
     15     /// Contention returns nil. No waiting, background work or automatic retry.
     16     public func reserveUse() throws -> RadrootsFileUseReservation? {
     17         try RadrootsFileMaintenanceGate.open(root: root, exclusive: false).map(RadrootsFileUseReservation.init)
     18     }
     19 
     20     /// Acquire before inspecting references; retain through the last conditional unlink.
     21     public func reserveMaintenance() throws -> RadrootsFileMaintenanceReservation? {
     22         try RadrootsFileMaintenanceGate.open(root: root, exclusive: true).map(RadrootsFileMaintenanceReservation.init)
     23     }
     24 }
     25 
     26 /// Release by dropping the last owner only after every underlying user has drained.
     27 public final class RadrootsFileUseReservation: Sendable {
     28     private let gate: RadrootsFileMaintenanceGate
     29     fileprivate init(_ gate: RadrootsFileMaintenanceGate) {
     30         self.gate = gate
     31     }
     32 
     33     public func validate() throws {
     34         try gate.validate()
     35     }
     36 }
     37 
     38 /// Child scans retain this exclusive reservation. There is no early unlock API.
     39 public final class RadrootsFileMaintenanceReservation: Sendable {
     40     let gate: RadrootsFileMaintenanceGate
     41     fileprivate init(_ gate: RadrootsFileMaintenanceGate) {
     42         self.gate = gate
     43     }
     44 
     45     /// Empty path selects the governed root. Other paths must be canonical relative
     46     /// directories without symlinks. Streams are live and never persisted as cookies.
     47     public func openDirectory(relativePath: String = "") throws -> RadrootsFileMaintenanceScan {
     48         try RadrootsFileMaintenanceScan(reservation: self, relativePath: relativePath)
     49     }
     50 
     51     public func validate() throws {
     52         try gate.validate()
     53     }
     54 }
     55 
     56 /// Immutable descriptor ownership; descriptors close only at final deinitialization.
     57 /// Methods neither mutate descriptor state nor release the advisory reservation.
     58 final class RadrootsFileMaintenanceGate: @unchecked Sendable {
     59     static let name = ".radroots_file_maintenance.lock"
     60     let directory: RadrootsAtomicFile.Directory
     61     private let descriptor: Int32
     62 
     63     private init(directory: RadrootsAtomicFile.Directory, descriptor: Int32) {
     64         self.directory = directory
     65         self.descriptor = descriptor
     66     }
     67 
     68     deinit {
     69         Darwin.close(descriptor)
     70         Darwin.close(directory.descriptor)
     71     }
     72 
     73     static func open(root: URL, exclusive: Bool) throws -> RadrootsFileMaintenanceGate? {
     74         guard root.isFileURL, root.path.hasPrefix("/"), !root.path.utf8.contains(0) else {
     75             throw RadrootsAppleFileError.invalidRequest
     76         }
     77         let parts = root.path.split(separator: "/").map(String.init)
     78         guard !parts.isEmpty, parts.allSatisfy({ $0 != "." && $0 != ".." }) else {
     79             throw RadrootsAppleFileError.invalidRequest
     80         }
     81         let directory = try RadrootsAtomicFile.Directory.open(parts, create: true)
     82         var owned = false
     83         defer {
     84             if !owned {
     85                 Darwin.close(directory.descriptor)
     86             }
     87         }
     88         for attempt in 0 ..< 4 {
     89             try directory.validate()
     90             let descriptor = Darwin.openat(directory.descriptor, name,
     91                                            O_RDWR | O_CREAT | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK | (exclusive ? O_EXLOCK : O_SHLOCK), 0o600)
     92             if descriptor >= 0 {
     93                 let gate = RadrootsFileMaintenanceGate(directory: directory, descriptor: descriptor)
     94                 owned = true
     95                 try gate.validate()
     96                 return gate
     97             }
     98             let error = errno
     99             if error == EWOULDBLOCK {
    100                 return nil
    101             }
    102             guard error == ENOENT else { throw RadrootsAppleFileError.permanentFailure }
    103             if attempt == 3 {
    104                 throw RadrootsAppleFileError.transientFailure
    105             }
    106         }
    107         throw RadrootsAppleFileError.transientFailure
    108     }
    109 
    110     func validate() throws {
    111         try directory.validate()
    112         var held = stat()
    113         var current = stat()
    114         guard Darwin.fstat(descriptor, &held) == 0,
    115               Darwin.fstatat(directory.descriptor, Self.name, &current, AT_SYMLINK_NOFOLLOW) == 0,
    116               held.st_mode & S_IFMT == S_IFREG, current.st_mode & S_IFMT == S_IFREG,
    117               held.st_nlink == 1, held.st_size == 0, held.st_uid == geteuid(),
    118               held.st_dev == current.st_dev, held.st_ino == current.st_ino
    119         else {
    120             throw RadrootsAppleFileError.permanentFailure
    121         }
    122     }
    123 }