RadrootsAdmissionFileScan.swift (3984B)
1 import Darwin 2 import Foundation 3 4 /// One bounded pass over native reservation metadata, not a statement about 5 /// transfer completion or global absence. New entries may require another pass. 6 public struct RadrootsAdmissionCleanupResult: Sendable, Equatable { 7 public let scannedEntries: Int 8 public let removedFiles: Int 9 public let reachedEnd: Bool 10 } 11 12 /// Owns two descriptors. The mutex serializes stream position and validation; 13 /// deinitialization runs only after all method borrows finish. No callbacks or 14 /// asynchronous work execute under this lock. 15 final class RadrootsAdmissionFileScan: @unchecked Sendable { 16 private let lock = NSLock() 17 private let directory: RadrootsAtomicFile.Directory 18 private let stream: UnsafeMutablePointer<DIR> 19 20 init(url: URL) throws { 21 guard url.isFileURL, !url.path.utf8.contains(0) else { throw RadrootsAppleFileError.invalidRequest } 22 let parts = url.path.split(separator: "/").map(String.init) 23 guard parts.allSatisfy({ $0 != "." && $0 != ".." }) else { throw RadrootsAppleFileError.invalidRequest } 24 let directory = try RadrootsAtomicFile.Directory.open(parts, create: false) 25 let copy = Darwin.fcntl(directory.descriptor, F_DUPFD_CLOEXEC, 0) 26 guard copy >= 0 else { 27 Darwin.close(directory.descriptor) 28 throw RadrootsAppleFileError.permanentFailure 29 } 30 guard let stream = Darwin.fdopendir(copy) else { 31 Darwin.close(copy) 32 Darwin.close(directory.descriptor) 33 throw RadrootsAppleFileError.permanentFailure 34 } 35 self.directory = directory 36 self.stream = stream 37 } 38 39 deinit { 40 Darwin.closedir(stream) 41 Darwin.close(directory.descriptor) 42 } 43 44 func next(limit: Int) throws -> (names: [String], scanned: Int, reachedEnd: Bool) { 45 try lock.withLock { 46 try directory.validate() 47 var names: [String] = [] 48 var scanned = 0 49 var reachedEnd = false 50 for _ in 0 ..< limit { 51 errno = 0 52 guard let entry = Darwin.readdir(stream) else { 53 guard errno == 0 else { throw RadrootsAppleFileError.permanentFailure } 54 reachedEnd = true 55 break 56 } 57 scanned += 1 58 let count = Int(entry.pointee.d_namlen) 59 let name = withUnsafeBytes(of: entry.pointee.d_name) { bytes -> String? in 60 guard count <= bytes.count else { return nil } 61 return String(bytes: bytes.prefix(count), encoding: .utf8) 62 } 63 if let name { 64 names.append(name) 65 } 66 } 67 try directory.validate() 68 return (names, scanned, reachedEnd) 69 } 70 } 71 72 func removeInactive(name: String, coordination: Int32) throws -> Bool { 73 try lock.withLock { 74 try directory.validate() 75 var held = stat() 76 var gate = stat() 77 guard Darwin.fstat(coordination, &held) == 0, 78 Darwin.fstatat(directory.descriptor, ".coordination.lock", &gate, AT_SYMLINK_NOFOLLOW) == 0, 79 held.st_dev == gate.st_dev, held.st_ino == gate.st_ino 80 else { 81 throw RadrootsAppleFileError.permanentFailure 82 } 83 var value = stat() 84 guard name.withCString({ Darwin.fstatat(directory.descriptor, $0, &value, AT_SYMLINK_NOFOLLOW) }) == 0, 85 value.st_mode & S_IFMT == S_IFREG, value.st_size == 0 else { return false } 86 guard let descriptor = try RadrootsAtomicFile.acquireExclusiveLock(in: directory, name: name, create: false) else { 87 return false 88 } 89 defer { Darwin.close(descriptor) } 90 return try RadrootsAtomicFile.removeEmptyLockedFile(in: directory, name: name, descriptor: descriptor) 91 } 92 } 93 }