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

commit 8898507a58491950da93ec73ebd78888717618a6
parent ad5659709df6ca19a8e5517caf6c5f8396d1539e
Author: triesap <tyson@radroots.org>
Date:   Sun, 20 Sep 2026 04:49:23 +0000

transfer: retire inactive admission files safely

- Coordinate reservation acquisition and exact empty inode retirement
- Bound explicit inactive traversal while preserving active upload ownership
- Retry transient creating absence under the same validated parent
- Verify cancellation, contention and recovery on macOS and iOS

Diffstat:
ASources/RadrootsKit/RadrootsAdmissionFileScan.swift | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MSources/RadrootsKit/RadrootsAppleBackgroundTransferStore.swift | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
MSources/RadrootsKit/RadrootsAtomicFile.swift | 80++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
ATests/RadrootsKitTests/RadrootsAdmissionCleanupTests.swift | 346+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcontracts/api_baselines/apple_kit.txt | 12++++++++++++
5 files changed, 601 insertions(+), 21 deletions(-)

diff --git a/Sources/RadrootsKit/RadrootsAdmissionFileScan.swift b/Sources/RadrootsKit/RadrootsAdmissionFileScan.swift @@ -0,0 +1,93 @@ +import Darwin +import Foundation + +/// One bounded pass over native reservation metadata, not a statement about +/// transfer completion or global absence. New entries may require another pass. +public struct RadrootsAdmissionCleanupResult: Sendable, Equatable { + public let scannedEntries: Int + public let removedFiles: Int + public let reachedEnd: Bool +} + +/// Owns two descriptors. The mutex serializes stream position and validation; +/// deinitialization runs only after all method borrows finish. No callbacks or +/// asynchronous work execute under this lock. +final class RadrootsAdmissionFileScan: @unchecked Sendable { + private let lock = NSLock() + private let directory: RadrootsAtomicFile.Directory + private let stream: UnsafeMutablePointer<DIR> + + init(url: URL) throws { + guard url.isFileURL, !url.path.utf8.contains(0) else { throw RadrootsAppleFileError.invalidRequest } + let parts = url.path.split(separator: "/").map(String.init) + guard parts.allSatisfy({ $0 != "." && $0 != ".." }) else { throw RadrootsAppleFileError.invalidRequest } + let directory = try RadrootsAtomicFile.Directory.open(parts, create: false) + let copy = Darwin.fcntl(directory.descriptor, F_DUPFD_CLOEXEC, 0) + guard copy >= 0 else { + Darwin.close(directory.descriptor) + throw RadrootsAppleFileError.permanentFailure + } + guard let stream = Darwin.fdopendir(copy) else { + Darwin.close(copy) + Darwin.close(directory.descriptor) + throw RadrootsAppleFileError.permanentFailure + } + self.directory = directory + self.stream = stream + } + + deinit { + Darwin.closedir(stream) + Darwin.close(directory.descriptor) + } + + func next(limit: Int) throws -> (names: [String], scanned: Int, reachedEnd: Bool) { + try lock.withLock { + try directory.validate() + var names: [String] = [] + var scanned = 0 + var reachedEnd = false + for _ in 0 ..< limit { + errno = 0 + guard let entry = Darwin.readdir(stream) else { + guard errno == 0 else { throw RadrootsAppleFileError.permanentFailure } + reachedEnd = true + break + } + scanned += 1 + let count = Int(entry.pointee.d_namlen) + let name = withUnsafeBytes(of: entry.pointee.d_name) { bytes -> String? in + guard count <= bytes.count else { return nil } + return String(bytes: bytes.prefix(count), encoding: .utf8) + } + if let name { + names.append(name) + } + } + try directory.validate() + return (names, scanned, reachedEnd) + } + } + + func removeInactive(name: String, coordination: Int32) throws -> Bool { + try lock.withLock { + try directory.validate() + var held = stat() + var gate = stat() + guard Darwin.fstat(coordination, &held) == 0, + Darwin.fstatat(directory.descriptor, ".coordination.lock", &gate, AT_SYMLINK_NOFOLLOW) == 0, + held.st_dev == gate.st_dev, held.st_ino == gate.st_ino + else { + throw RadrootsAppleFileError.permanentFailure + } + var value = stat() + guard name.withCString({ Darwin.fstatat(directory.descriptor, $0, &value, AT_SYMLINK_NOFOLLOW) }) == 0, + value.st_mode & S_IFMT == S_IFREG, value.st_size == 0 else { return false } + guard let descriptor = try RadrootsAtomicFile.acquireExclusiveLock(in: directory, name: name, create: false) else { + return false + } + defer { Darwin.close(descriptor) } + return try RadrootsAtomicFile.removeEmptyLockedFile(in: directory, name: name, descriptor: descriptor) + } + } +} diff --git a/Sources/RadrootsKit/RadrootsAppleBackgroundTransferStore.swift b/Sources/RadrootsKit/RadrootsAppleBackgroundTransferStore.swift @@ -19,6 +19,12 @@ public actor RadrootsAppleBackgroundTransferStore: RadrootsBackgroundTransferSto private let encoder: JSONEncoder private let decoder: JSONDecoder private let protectedData: RadrootsProtectedDataProvider + private var admissionScan: RadrootsAdmissionFileScan? + + private struct Admission { + let descriptor: Int32 + let url: URL + } public init( roots: RadrootsAppleFileRoots, fileManager: FileManager = .default, @@ -40,34 +46,107 @@ public actor RadrootsAppleBackgroundTransferStore: RadrootsBackgroundTransferSto for identifier: RadrootsBackgroundTransferIdentifier, operation: @escaping @Sendable () async throws -> RadrootsBackgroundTransferHandle ) async throws -> RadrootsBackgroundTransferHandle { - guard let descriptor = try acquireAdmission(identifier) else { + guard let admission = try await acquireAdmission(identifier) else { throw RadrootsBackgroundTransferError.invalidRequest } // Only this attempt owns the descriptor across awaits. Persistence uses a // separate short lock; neither reservation waits for another owner. - defer { Darwin.close(descriptor) } + defer { retireAdmission(admission) } return try await operation() } public func admissionIsActive(for identifier: RadrootsBackgroundTransferIdentifier) async throws -> Bool { - guard let descriptor = try acquireAdmission(identifier) else { return true } - Darwin.close(descriptor) + guard let admission = try await acquireAdmission(identifier) else { return true } + retireAdmission(admission) return false } - private func acquireAdmission(_ identifier: RadrootsBackgroundTransferIdentifier) throws -> Int32? { + private func acquireAdmission(_ identifier: RadrootsBackgroundTransferIdentifier) async throws -> Admission? { try requireProtectedData() + let coordination = try await acquireAdmissionCoordination() + defer { Darwin.close(coordination) } do { let name = RadrootsAppleFileDigest.sha256(Data(identifier.rawValue.utf8)) let url = try roots.resolvedURL(for: RadrootsFileReference( scope: .data, relativePath: "background_transfers/admissions/\(name).lock" )) - return try RadrootsAtomicFile.acquireExclusiveLock(at: url) + guard let descriptor = try RadrootsAtomicFile.acquireExclusiveLock(at: url) else { return nil } + return Admission(descriptor: descriptor, url: url) + } catch RadrootsAppleFileError.transientFailure { + throw RadrootsBackgroundTransferError.unavailable } catch { throw RadrootsBackgroundTransferError.persistenceFailure } } + private func admissionCoordinationURL() throws -> URL { + try roots.resolvedURL(for: RadrootsFileReference( + scope: .data, relativePath: "background_transfers/admissions/.coordination.lock" + )) + } + + private func acquireAdmissionCoordination() async throws -> Int32 { + for _ in 0 ..< 16 { + try Task.checkCancellation() + try requireProtectedData() + do { + if let descriptor = try RadrootsAtomicFile.acquireExclusiveLock(at: admissionCoordinationURL()) { + return descriptor + } + } catch RadrootsAppleFileError.transientFailure { + // The leaf was not created within its bounded retry budget. + } catch { throw RadrootsBackgroundTransferError.persistenceFailure } + await Task.yield() + } + throw RadrootsBackgroundTransferError.unavailable + } + + private func retireAdmission(_ admission: Admission) { + do { + try requireProtectedData() + guard let coordination = try RadrootsAtomicFile.acquireExclusiveLock(at: admissionCoordinationURL()) else { + Darwin.close(admission.descriptor) + return + } + // Close the retired inode before releasing the acquisition gate. + defer { Darwin.close(admission.descriptor); Darwin.close(coordination) } + _ = try? RadrootsAtomicFile.removeEmptyLockedFile(at: admission.url, descriptor: admission.descriptor) + } catch { + // Cleanup failure never rewrites the operation result. A later + // explicit pass may reconcile the retained inactive inode. + Darwin.close(admission.descriptor) + } + } + + /// Explicit metadata housekeeping. Each call visits at most 64 directory + /// entries and continues this store's pass. A new pass starts after its end; + /// restarting the owner starts over. Snapshots and upload leases are untouched. + public func collectInactiveAdmissions(limit: Int = 64) async throws -> RadrootsAdmissionCleanupResult { + guard (1 ... 64).contains(limit) else { throw RadrootsBackgroundTransferError.invalidRequest } + let coordination = try await acquireAdmissionCoordination() + defer { Darwin.close(coordination) } + do { + let directory = try admissionCoordinationURL().deletingLastPathComponent() + if admissionScan == nil { admissionScan = try RadrootsAdmissionFileScan(url: directory) } + guard let admissionScan else { throw RadrootsBackgroundTransferError.persistenceFailure } + let batch = try admissionScan.next(limit: limit) + var removed = 0 + for name in batch.names where Self.isAdmissionFilename(name) { + if try admissionScan.removeInactive(name: name, coordination: coordination) { removed += 1 } + } + if batch.reachedEnd { self.admissionScan = nil } + return RadrootsAdmissionCleanupResult(scannedEntries: batch.scanned, removedFiles: removed, reachedEnd: batch.reachedEnd) + } catch { + admissionScan = nil + throw RadrootsBackgroundTransferError.persistenceFailure + } + } + + private static func isAdmissionFilename(_ name: String) -> Bool { + name.utf8.count == 69 && name.hasSuffix(".lock") + && name.utf8.prefix(64).allSatisfy { (48 ... 57).contains($0) || (97 ... 102).contains($0) } + } + public func compareExchangeSnapshot( expected: RadrootsBackgroundTransferSnapshot?, desired: RadrootsBackgroundTransferSnapshot ) async throws -> Bool { diff --git a/Sources/RadrootsKit/RadrootsAtomicFile.swift b/Sources/RadrootsKit/RadrootsAtomicFile.swift @@ -21,26 +21,44 @@ enum RadrootsAtomicFile { /// The caller owns and closes the returned descriptor. Nil denotes only /// active contention; invalid paths and I/O failures remain errors. - static func acquireExclusiveLock(at url: URL) throws -> Int32? { + static func acquireExclusiveLock( + at url: URL, create: Bool = true, injectedOpenError: ((Int) throws -> Int32?)? = nil + ) throws -> Int32? { let parts = url.path.split(separator: "/").map(String.init) guard url.isFileURL, !url.path.utf8.contains(0), let leaf = parts.last, parts.allSatisfy({ $0 != "." && $0 != ".." }) else { throw RadrootsAppleFileError.invalidRequest } - let directory = try Directory.open(Array(parts.dropLast()), create: true) + let directory = try Directory.open(Array(parts.dropLast()), create: create) defer { Darwin.close(directory.descriptor) } - let descriptor = leaf.withCString { - Darwin.openat( - directory.descriptor, - $0, - O_RDWR | O_CREAT | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK | O_EXLOCK, - 0o600 - ) + return try acquireExclusiveLock(in: directory, name: leaf, create: create, injectedOpenError: injectedOpenError) + } + + static func acquireExclusiveLock( + in directory: Directory, name: String, create: Bool, injectedOpenError: ((Int) throws -> Int32?)? = nil + ) throws -> Int32? { + guard !name.isEmpty, !name.contains("/"), !name.utf8.contains(0), name != ".", name != ".." else { + throw RadrootsAppleFileError.invalidRequest } - guard descriptor >= 0 else { - if errno == EWOULDBLOCK { - return nil + var descriptor: Int32 = -1 + // Concurrent first creation can return ENOENT even with O_CREAT. Retry + // only that absence, under the exact validated parent and a fixed bound. + // The injection is synchronous and used only by filesystem regressions. + for attempt in 0 ..< 4 { + try directory.validate() + let opened: (Int32, Int32) + if let error = try injectedOpenError?(attempt) { + opened = (-1, error) + } else { + opened = name.withCString { + let value = Darwin.openat(directory.descriptor, $0, + O_RDWR | (create ? O_CREAT : 0) | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK | O_EXLOCK, 0o600) + return (value, errno) + } } - throw RadrootsAppleFileError.permanentFailure + if opened.0 >= 0 { descriptor = opened.0; break } + if opened.1 == EWOULDBLOCK || !create && opened.1 == ENOENT { return nil } + guard create, opened.1 == ENOENT else { throw RadrootsAppleFileError.permanentFailure } + if attempt == 3 { throw RadrootsAppleFileError.transientFailure } } var value = stat() guard Darwin.fstat(descriptor, &value) == 0, value.st_mode & S_IFMT == S_IFREG @@ -56,6 +74,38 @@ enum RadrootsAtomicFile { return descriptor } + /// The caller holds this reservation and its acquisition/retirement gate. + /// Never unlink an inode substituted after the descriptor was acquired. + static func removeEmptyLockedFile(at url: URL, descriptor: Int32) throws -> Bool { + let parts = url.path.split(separator: "/").map(String.init) + guard url.isFileURL, !url.path.utf8.contains(0), let leaf = parts.last, + parts.allSatisfy({ $0 != "." && $0 != ".." }) + else { throw RadrootsAppleFileError.invalidRequest } + let directory = try Directory.open(Array(parts.dropLast()), create: false) + defer { Darwin.close(directory.descriptor) } + return try removeEmptyLockedFile(in: directory, name: leaf, descriptor: descriptor) + } + + static func removeEmptyLockedFile(in directory: Directory, name: String, descriptor: Int32) throws -> Bool { + guard !name.isEmpty, !name.contains("/"), !name.utf8.contains(0), name != ".", name != ".." else { + throw RadrootsAppleFileError.invalidRequest + } + var owned = stat() + guard Darwin.fstat(descriptor, &owned) == 0 else { throw RadrootsAppleFileError.permanentFailure } + guard owned.st_mode & S_IFMT == S_IFREG, owned.st_size == 0 else { return false } + try directory.validate() + var current = stat() + let found = name.withCString { Darwin.fstatat(directory.descriptor, $0, &current, AT_SYMLINK_NOFOLLOW) } + if found != 0, errno == ENOENT { return false } + guard found == 0 else { throw RadrootsAppleFileError.permanentFailure } + guard current.st_mode & S_IFMT == S_IFREG, current.st_size == 0, + current.st_dev == owned.st_dev, current.st_ino == owned.st_ino else { return false } + guard name.withCString({ Darwin.unlinkat(directory.descriptor, $0, 0) }) == 0, + Darwin.fsync(directory.descriptor) == 0 else { throw RadrootsAppleFileError.permanentFailure } + try directory.validate() + return true + } + static func install(_ data: Data, at url: URL, mode: Mode = .replace, readOnly: Bool = false) throws { try install(data, at: url, mode: mode, readOnly: readOnly, fault: nil) } @@ -190,10 +240,10 @@ enum RadrootsAtomicFile { } } - private struct Directory { + struct Directory { let descriptor: Int32 let parts: [String] - let identities: [Identity] + private let identities: [Identity] static func open(_ parts: [String], create: Bool) throws -> Self { var descriptor = Darwin.open("/", O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC) diff --git a/Tests/RadrootsKitTests/RadrootsAdmissionCleanupTests.swift b/Tests/RadrootsKitTests/RadrootsAdmissionCleanupTests.swift @@ -0,0 +1,346 @@ +import Darwin +import Foundation +@testable import RadrootsKit +import Testing + +@Test func nativeAdmissionCleanupRetriesOnlyBoundedCreationAbsence() throws { + let f = try AdmissionCleanupFixture() + defer { f.remove() } + var attempts = 0 + let path = f.path("creation") + let descriptor = try #require(try RadrootsAtomicFile.acquireExclusiveLock(at: path, injectedOpenError: { attempt in + attempts += 1 + return attempt < 2 ? ENOENT : nil + })) + #expect(attempts == 3) + #expect(try RadrootsAtomicFile.acquireExclusiveLock(at: path) == nil) + Darwin.close(descriptor) + attempts = 0 + #expect(throws: RadrootsAppleFileError.transientFailure) { + _ = try RadrootsAtomicFile.acquireExclusiveLock(at: f.path("exhausted"), injectedOpenError: { _ in + attempts += 1; return ENOENT + }) + } + #expect(attempts == 4 && !FileManager.default.fileExists(atPath: f.path("exhausted").path)) + attempts = 0 + #expect(throws: RadrootsAppleFileError.permanentFailure) { + _ = try RadrootsAtomicFile.acquireExclusiveLock(at: f.path("denied"), injectedOpenError: { _ in + attempts += 1; return EACCES + }) + } + #expect(attempts == 1) + #expect(try RadrootsAtomicFile.acquireExclusiveLock(at: f.path("missing"), create: false) == nil) + #expect(!FileManager.default.fileExists(atPath: f.path("missing").path)) +} + +@Test func nativeAdmissionCleanupCreationRetryRejectsChangedParent() throws { + let f = try AdmissionCleanupFixture() + defer { f.remove() } + var attempts = 0 + #expect(throws: RadrootsAppleFileError.permanentFailure) { + _ = try RadrootsAtomicFile.acquireExclusiveLock(at: f.path("changed"), injectedOpenError: { _ in + attempts += 1 + try FileManager.default.moveItem(at: f.directory, to: f.base.appendingPathComponent("moved")) + try FileManager.default.createDirectory(at: f.directory, withIntermediateDirectories: true) + return ENOENT + }) + } + #expect(attempts == 1) + #expect(!FileManager.default.fileExists(atPath: f.path("changed").path)) +} + +@Test func nativeAdmissionCleanupConcurrentOwnersNeverSplitOneReservation() async throws { + let f = try AdmissionCleanupFixture() + defer { f.remove() } + let request = try f.request("contended") + let probe = AdmissionCleanupProbe() + let collector = RadrootsAppleBackgroundTransferStore(roots: f.roots) + try await withThrowingTaskGroup(of: Void.self) { group in + for _ in 0 ..< 8 { + group.addTask { + let store = RadrootsAppleBackgroundTransferStore(roots: f.roots) + for _ in 0 ..< 20 { + do { + _ = try await store.withAdmission(for: request.identifier) { + await probe.enter() + await Task.yield() + await probe.leave() + return RadrootsBackgroundTransferHandle(request: request) + } + } catch RadrootsBackgroundTransferError.invalidRequest { + // Another owner has this exact reservation. + } catch RadrootsBackgroundTransferError.unavailable { + // Bounded coordination contention remains transient. + } + } + } + } + group.addTask { + for _ in 0 ..< 40 { + do { _ = try await collector.collectInactiveAdmissions(limit: 7) } + catch RadrootsBackgroundTransferError.unavailable {} + await Task.yield() + } + } + try await group.waitForAll() + } + #expect(await probe.maximum == 1) + #expect(await probe.active == 0) + #expect(await probe.completed > 0) + #expect(try await !collector.admissionIsActive(for: request.identifier)) +} + +@Test func nativeAdmissionCleanupPreservesNativeReceiptAndUploadLease() async throws { + let f = try AdmissionCleanupFixture() + defer { f.remove() } + let store = RadrootsAppleBackgroundTransferStore(roots: f.roots) + let request = try f.request("unverified") + let receipt = try RadrootsBackgroundTransferSnapshot(request: request, state: .awaitingVerification, + response: RadrootsBackgroundTransferResponse(statusCode: 200, mediaType: "application/json", body: Data("{}".utf8)), + executionID: UUID()) + try await store.saveSnapshot(receipt) + let bytes = Data("OS-consumed immutable upload bytes".utf8) + let lease = try RadrootsAppleFileAccess(roots: f.roots).leaseUploadBytes(bytes, identifier: UUID().uuidString.lowercased()) + try FileManager.default.createDirectory(at: f.directory, withIntermediateDirectories: true) + try Data().write(to: f.path(request.identifier.rawValue)) + #expect(try await store.collectInactiveAdmissions().removedFiles == 1) + #expect(try await store.loadSnapshots() == [receipt]) + #expect(try Data(contentsOf: lease.fileURL) == bytes) +} + +@Test func nativeAdmissionCleanupRetainsHeldReservationAndAllowsIndependentIDs() async throws { + let f = try AdmissionCleanupFixture() + defer { f.remove() } + let first = RadrootsAppleBackgroundTransferStore(roots: f.roots) + let second = RadrootsAppleBackgroundTransferStore(roots: f.roots) + let request = try f.request("held") + let gate = AdmissionCleanupGate() + let pending = Task { + try await first.withAdmission(for: request.identifier) { + await gate.hold() + return RadrootsBackgroundTransferHandle(request: request) + } + } + await gate.waitUntilEntered() + let path = f.path(request.identifier.rawValue) + #expect(FileManager.default.fileExists(atPath: path.path)) + #expect(try await second.collectInactiveAdmissions().removedFiles == 0) + #expect(try await second.admissionIsActive(for: request.identifier)) + await #expect(throws: RadrootsBackgroundTransferError.invalidRequest) { + _ = try await second.withAdmission(for: request.identifier) { RadrootsBackgroundTransferHandle(request: request) } + } + let other = try f.request("independent") + _ = try await second.withAdmission(for: other.identifier) { RadrootsBackgroundTransferHandle(request: other) } + #expect(!FileManager.default.fileExists(atPath: f.path(other.identifier.rawValue).path)) + #expect(FileManager.default.fileExists(atPath: path.path)) + await gate.release() + #expect(try await pending.value.identifier == request.identifier) + #expect(!FileManager.default.fileExists(atPath: path.path)) +} + +@Test func nativeAdmissionCleanupContinuesBeyondOneThousandEntriesAndRetainsUnknowns() async throws { + let f = try AdmissionCleanupFixture() + defer { f.remove() } + try FileManager.default.createDirectory(at: f.directory, withIntermediateDirectories: true) + for index in 0 ..< 1001 { + try Data().write(to: f.path("orphan-\(index)")) + } + let nonempty = f.path("nonempty") + let outside = f.base.appendingPathComponent("outside") + try Data("protected".utf8).write(to: nonempty) + try Data("external".utf8).write(to: outside) + try FileManager.default.createSymbolicLink(at: f.path("symlink"), withDestinationURL: outside) + try FileManager.default.createDirectory(at: f.path("directory"), withIntermediateDirectories: false) + let unknown = f.directory.appendingPathComponent("unknown.lock") + try Data().write(to: unknown) + let store = RadrootsAppleBackgroundTransferStore(roots: f.roots) + var removed = 0 + var calls = 0 + while true { + let result = try await store.collectInactiveAdmissions(limit: 37) + #expect(result.scannedEntries <= 37 && result.removedFiles <= result.scannedEntries) + removed += result.removedFiles + calls += 1 + if result.reachedEnd { + break + } + try #require(calls < 100) + } + #expect(calls > 27 && removed == 1001) + #expect(try Data(contentsOf: nonempty) == Data("protected".utf8)) + #expect(try Data(contentsOf: outside) == Data("external".utf8)) + #expect(FileManager.default.fileExists(atPath: unknown.path)) + #expect(try await store.collectInactiveAdmissions().removedFiles == 0) +} + +@Test func nativeAdmissionCleanupRetainsInterruptedRetirementThenRecovers() async throws { + let f = try AdmissionCleanupFixture() + defer { f.remove() } + let store = RadrootsAppleBackgroundTransferStore(roots: f.roots) + let request = try f.request("interrupted") + let gate = AdmissionCleanupGate() + let pending = Task { + try await store.withAdmission(for: request.identifier) { + await gate.hold() + return RadrootsBackgroundTransferHandle(request: request) + } + } + await gate.waitUntilEntered() + let coordination = try #require(try RadrootsAtomicFile.acquireExclusiveLock(at: f.coordination)) + await gate.release() + #expect(try await pending.value.identifier == request.identifier) + #expect(FileManager.default.fileExists(atPath: f.path(request.identifier.rawValue).path)) + Darwin.close(coordination) + #expect(try await store.collectInactiveAdmissions().removedFiles == 1) + #expect(try await store.collectInactiveAdmissions().removedFiles == 0) +} + +@Test func nativeAdmissionCleanupCancellationAndOperationErrorRetireOnlyTheirReservation() async throws { + let f = try AdmissionCleanupFixture() + defer { f.remove() } + let store = RadrootsAppleBackgroundTransferStore(roots: f.roots) + let request = try f.request("cancelled") + let gate = AdmissionCleanupGate() + let pending = Task { + try await store.withAdmission(for: request.identifier) { + await gate.hold() + try Task.checkCancellation() + return RadrootsBackgroundTransferHandle(request: request) + } + } + await gate.waitUntilEntered() + pending.cancel() + #expect(FileManager.default.fileExists(atPath: f.path(request.identifier.rawValue).path)) + await gate.release() + await #expect(throws: CancellationError.self) { _ = try await pending.value } + #expect(!FileManager.default.fileExists(atPath: f.path(request.identifier.rawValue).path)) + await #expect(throws: RadrootsBackgroundTransferError.transferFailure) { + _ = try await store.withAdmission(for: request.identifier) { throw RadrootsBackgroundTransferError.transferFailure } + } + #expect(!FileManager.default.fileExists(atPath: f.path(request.identifier.rawValue).path)) +} + +@Test func nativeAdmissionCleanupBoundsAndCoordinationContentionFailBeforeWork() async throws { + let f = try AdmissionCleanupFixture() + defer { f.remove() } + let store = RadrootsAppleBackgroundTransferStore(roots: f.roots) + for limit in [0, -1, 65, Int.max] { + await #expect(throws: RadrootsBackgroundTransferError.invalidRequest) { + _ = try await store.collectInactiveAdmissions(limit: limit) + } + } + #expect(!FileManager.default.fileExists(atPath: f.directory.path)) + let coordination = try #require(try RadrootsAtomicFile.acquireExclusiveLock(at: f.coordination)) + defer { Darwin.close(coordination) } + await #expect(throws: RadrootsBackgroundTransferError.unavailable) { + _ = try await store.collectInactiveAdmissions() + } +} + +@Test func nativeAdmissionCleanupRejectsReplacedDirectoryAndResumesFreshPass() async throws { + let f = try AdmissionCleanupFixture() + defer { f.remove() } + let store = RadrootsAppleBackgroundTransferStore(roots: f.roots) + #expect(try await !store.collectInactiveAdmissions(limit: 1).reachedEnd) + try FileManager.default.moveItem(at: f.directory, to: f.base.appendingPathComponent("retired")) + try FileManager.default.createDirectory(at: f.directory, withIntermediateDirectories: true) + let replacement = f.path("replacement") + try Data().write(to: replacement) + await #expect(throws: RadrootsBackgroundTransferError.persistenceFailure) { + _ = try await store.collectInactiveAdmissions() + } + #expect(FileManager.default.fileExists(atPath: replacement.path)) + #expect(try await store.collectInactiveAdmissions().removedFiles == 1) +} + +@Test func nativeAdmissionCleanupDoesNotUnlinkReplacedLeafOrFollowRootSymlink() async throws { + let f = try AdmissionCleanupFixture() + defer { f.remove() } + let path = f.path("replaced") + let descriptor = try #require(try RadrootsAtomicFile.acquireExclusiveLock(at: path)) + defer { Darwin.close(descriptor) } + try FileManager.default.moveItem(at: path, to: f.base.appendingPathComponent("held-inode")) + try Data().write(to: path) + #expect(try !RadrootsAtomicFile.removeEmptyLockedFile(at: path, descriptor: descriptor)) + #expect(FileManager.default.fileExists(atPath: path.path)) + try FileManager.default.moveItem(at: f.directory, to: f.base.appendingPathComponent("original")) + try FileManager.default.createSymbolicLink(at: f.directory, withDestinationURL: f.base.appendingPathComponent("original")) + let store = RadrootsAppleBackgroundTransferStore(roots: f.roots) + await #expect(throws: RadrootsBackgroundTransferError.persistenceFailure) { _ = try await store.collectInactiveAdmissions() } + #expect(FileManager.default.fileExists(atPath: path.path)) +} + +private actor AdmissionCleanupProbe { + var active = 0 + var maximum = 0 + var completed = 0 + func enter() { + active += 1; maximum = max(maximum, active) + } + + func leave() { + active -= 1; completed += 1 + } +} + +private actor AdmissionCleanupGate { + private var entered = false + private var waiters: [CheckedContinuation<Void, Never>] = [] + private var continuation: CheckedContinuation<Void, Never>? + + func hold() async { + entered = true + for waiter in waiters { + waiter.resume() + } + waiters.removeAll() + await withCheckedContinuation { continuation = $0 } + } + + func waitUntilEntered() async { + if !entered { + await withCheckedContinuation { waiters.append($0) } + } + } + + func release() { + continuation?.resume(); continuation = nil + } +} + +private struct AdmissionCleanupFixture: Sendable { + let base: URL + let roots: RadrootsAppleFileRoots + var directory: URL { + roots.dataRoot.appendingPathComponent("background_transfers/admissions", isDirectory: true) + } + + var coordination: URL { + directory.appendingPathComponent(".coordination.lock") + } + + init() throws { + let raw = FileManager.default.temporaryDirectory.appendingPathComponent("admission-cleanup-\(UUID().uuidString)") + try FileManager.default.createDirectory(at: raw, withIntermediateDirectories: true) + let pointer = try #require(raw.path.withCString { Darwin.realpath($0, nil) }) + defer { Darwin.free(pointer) } + base = URL(fileURLWithPath: String(cString: pointer), isDirectory: true) + roots = try RadrootsAppleFileRoots(appIdentifier: "org.radroots.tests", dataRoot: base.appendingPathComponent("data"), + cacheRoot: base.appendingPathComponent("cache"), temporaryRoot: base.appendingPathComponent("tmp")) + } + + func path(_ identifier: String) -> URL { + directory.appendingPathComponent(RadrootsAppleFileDigest.sha256(Data(identifier.utf8)) + ".lock") + } + + func request(_ id: String) throws -> RadrootsBackgroundTransferRequest { + try RadrootsBackgroundTransferRequest(identifier: RadrootsBackgroundTransferIdentifier(id), + remoteURL: #require(URL(string: "https://example.org/upload")), method: .put, + operation: .upload(source: .file(RadrootsFileReference(scope: .cache, relativePath: "body"))), + responsePolicy: .boundedJSON()) + } + + func remove() { + try? FileManager.default.removeItem(at: base) + } +} diff --git a/contracts/api_baselines/apple_kit.txt b/contracts/api_baselines/apple_kit.txt @@ -426,6 +426,9 @@ radroots.apple-kit.public-api.v1 12:relationship 11:RadrootsKit 10:conformsTo 41:s:11RadrootsKit0A21SharePresentationLinkV 13:s:s8SendableP 14:Swift.Sendable 12:relationship 11:RadrootsKit 10:conformsTo 41:s:11RadrootsKit0A21SharePresentationLinkV 16:s:7SwiftUI4ViewP 16:SwiftUICore.View 12:relationship 11:RadrootsKit 10:conformsTo 41:s:11RadrootsKit0A21SharePresentationLinkV 22:s:s16SendableMetatypeP 22:Swift.SendableMetatype +12:relationship 11:RadrootsKit 10:conformsTo 42:s:11RadrootsKit0A22AdmissionCleanupResultV 13:s:s8SendableP 14:Swift.Sendable +12:relationship 11:RadrootsKit 10:conformsTo 42:s:11RadrootsKit0A22AdmissionCleanupResultV 22:s:s16SendableMetatypeP 22:Swift.SendableMetatype +12:relationship 11:RadrootsKit 10:conformsTo 42:s:11RadrootsKit0A22AdmissionCleanupResultV 4:s:SQ 15:Swift.Equatable 12:relationship 11:RadrootsKit 10:conformsTo 42:s:11RadrootsKit0A22BackgroundTaskSnapshotV 13:s:s8SendableP 14:Swift.Sendable 12:relationship 11:RadrootsKit 10:conformsTo 42:s:11RadrootsKit0A22BackgroundTaskSnapshotV 22:s:s16SendableMetatypeP 22:Swift.SendableMetatype 12:relationship 11:RadrootsKit 10:conformsTo 42:s:11RadrootsKit0A22BackgroundTaskSnapshotV 4:s:SH 14:Swift.Hashable @@ -825,6 +828,7 @@ radroots.apple-kit.public-api.v1 12:relationship 11:RadrootsKit 8:memberOf 118:s:11RadrootsKit0A15IdentityCustodyC06importC0_5label15replaceExistingAA0aC8SnapshotVAA0aC14SecretMaterialV_SSSgSbtYaKF 35:s:11RadrootsKit0A15IdentityCustodyC 0: 12:relationship 11:RadrootsKit 8:memberOf 118:s:11RadrootsKit0A23AppleBackgroundTransferC015handleEventsForD10URLSession10identifier17completionHandlerySS_yyYbctYaF 43:s:11RadrootsKit0A23AppleBackgroundTransferC 0: 12:relationship 11:RadrootsKit 8:memberOf 118:s:11RadrootsKit0A27DocumentPresentationAdapterO11contentType3for07UniformG11Identifiers6UTTypeVAA0aC11ContentKindO_tFZ 47:s:11RadrootsKit0A27DocumentPresentationAdapterO 0: +12:relationship 11:RadrootsKit 8:memberOf 118:s:11RadrootsKit0A28AppleBackgroundTransferStoreC25collectInactiveAdmissions5limitAA0A22AdmissionCleanupResultVSi_tYaKF 48:s:11RadrootsKit0A28AppleBackgroundTransferStoreC 0: 12:relationship 11:RadrootsKit 8:memberOf 118:s:11RadrootsKit0A29UnavailableBackgroundTransferV6settle_12verificationyAA0adE10IdentifierV_AA0adE12VerificationOtYaKF 49:s:11RadrootsKit0A29UnavailableBackgroundTransferV 0: 12:relationship 11:RadrootsKit 8:memberOf 119:s:11RadrootsKit0A24AppleKeychainSecureStoreC3put_3for6policyy10Foundation4DataV_AA0aeF3KeyVAA0A18SecretAccessPolicyVtKF 44:s:11RadrootsKit0A24AppleKeychainSecureStoreC 0: 12:relationship 11:RadrootsKit 8:memberOf 121:s:11RadrootsKit0A15AppleFileAccessC9stageBlob_9mediaType12filenameHintAA0a6StagedG9ReferenceV10Foundation4DataV_SSSgALtKF 35:s:11RadrootsKit0A15AppleFileAccessC 0: @@ -1096,6 +1100,7 @@ radroots.apple-kit.public-api.v1 12:relationship 11:RadrootsKit 8:memberOf 58:s:11RadrootsKit0A19BackgroundTaskErrorO11unavailableyA2CmF 39:s:11RadrootsKit0A19BackgroundTaskErrorO 0: 12:relationship 11:RadrootsKit 8:memberOf 58:s:11RadrootsKit0A19ExternalActionErrorO11unavailableyA2CmF 39:s:11RadrootsKit0A19ExternalActionErrorO 0: 12:relationship 11:RadrootsKit 8:memberOf 58:s:11RadrootsKit0A20IdentityPublicRecordV12publicKeyHexSSvp 40:s:11RadrootsKit0A20IdentityPublicRecordV 0: +12:relationship 11:RadrootsKit 8:memberOf 58:s:11RadrootsKit0A22AdmissionCleanupResultV10reachedEndSbvp 42:s:11RadrootsKit0A22AdmissionCleanupResultV 0: 12:relationship 11:RadrootsKit 8:memberOf 58:s:11RadrootsKit0A22PreparedExportDocumentV10preparedIDSSvp 42:s:11RadrootsKit0A22PreparedExportDocumentV 0: 12:relationship 11:RadrootsKit 8:memberOf 58:s:11RadrootsKit0A22PreparedExportDocumentV9mediaTypeSSSgvp 42:s:11RadrootsKit0A22PreparedExportDocumentV 0: 12:relationship 11:RadrootsKit 8:memberOf 58:s:11RadrootsKit0A28AppleExternalActionsAdaptersV4liveACvpZ 48:s:11RadrootsKit0A28AppleExternalActionsAdaptersV 0: @@ -1141,6 +1146,7 @@ radroots.apple-kit.public-api.v1 12:relationship 11:RadrootsKit 8:memberOf 60:s:11RadrootsKit0A21LocationAuthorizationO11unavailableyA2CmF 41:s:11RadrootsKit0A21LocationAuthorizationO 0: 12:relationship 11:RadrootsKit 8:memberOf 60:s:11RadrootsKit0A21LocationAuthorizationO11unsupportedyA2CmF 41:s:11RadrootsKit0A21LocationAuthorizationO 0: 12:relationship 11:RadrootsKit 8:memberOf 60:s:11RadrootsKit0A21LocationServicesErrorO11unavailableyA2CmF 41:s:11RadrootsKit0A21LocationServicesErrorO 0: +12:relationship 11:RadrootsKit 8:memberOf 60:s:11RadrootsKit0A22AdmissionCleanupResultV12removedFilesSivp 42:s:11RadrootsKit0A22AdmissionCleanupResultV 0: 12:relationship 11:RadrootsKit 8:memberOf 60:s:11RadrootsKit0A26VerifiedArtifactDescriptorV9mediaTypeSSvp 46:s:11RadrootsKit0A26VerifiedArtifactDescriptorV 0: 12:relationship 11:RadrootsKit 8:memberOf 60:s:11RadrootsKit0A34UnavailableBackgroundTaskSchedulerVACycfc 54:s:11RadrootsKit0A34UnavailableBackgroundTaskSchedulerV 0: 12:relationship 11:RadrootsKit 8:memberOf 61:s:11RadrootsKit0A15IdentityCustodyC7recoverAA0aC8SnapshotVyKF 35:s:11RadrootsKit0A15IdentityCustodyC 0: @@ -1190,6 +1196,7 @@ radroots.apple-kit.public-api.v1 12:relationship 11:RadrootsKit 8:memberOf 62:s:11RadrootsKit0A20VerifiedArtifactFileV16debugDescriptionSSvp 40:s:11RadrootsKit0A20VerifiedArtifactFileV 0: 12:relationship 11:RadrootsKit 8:memberOf 62:s:11RadrootsKit0A21CurrentLocationResultV15servedFromCacheSbvp 41:s:11RadrootsKit0A21CurrentLocationResultV 0: 12:relationship 11:RadrootsKit 8:memberOf 62:s:11RadrootsKit0A21LocationAuthorizationO13notDeterminedyA2CmF 41:s:11RadrootsKit0A21LocationAuthorizationO 0: +12:relationship 11:RadrootsKit 8:memberOf 62:s:11RadrootsKit0A22AdmissionCleanupResultV14scannedEntriesSivp 42:s:11RadrootsKit0A22AdmissionCleanupResultV 0: 12:relationship 11:RadrootsKit 8:memberOf 62:s:11RadrootsKit0A22CurrentLocationRequestV14timeoutSecondsSdvp 42:s:11RadrootsKit0A22CurrentLocationRequestV 0: 12:relationship 11:RadrootsKit 8:memberOf 62:s:11RadrootsKit0A23BackgroundTransferErrorO11unavailableyA2CmF 43:s:11RadrootsKit0A23BackgroundTransferErrorO 0: 12:relationship 11:RadrootsKit 8:memberOf 62:s:11RadrootsKit0A23BackgroundTransferStateO11interruptedyA2CmF 43:s:11RadrootsKit0A23BackgroundTransferStateO 0: @@ -2094,6 +2101,7 @@ radroots.apple-kit.public-api.v1 6:symbol 11:RadrootsKit 12:swift.method 118:s:11RadrootsKit0A15AppleFileAccessC15leaseStagedBlob_14expectedSHA25610identifierAA0agH5LeaseVAA0agH9ReferenceV_S2StKF 45:leaseStagedBlob(_:expectedSHA256:identifier:) 168:func leaseStagedBlob(_ blob: RadrootsStagedBlobReference, expectedSHA256: String, identifier: String = UUID().uuidString.lowercased()) throws -> RadrootsStagedBlobLease 6:symbol 11:RadrootsKit 12:swift.method 118:s:11RadrootsKit0A15IdentityCustodyC06importC0_5label15replaceExistingAA0aC8SnapshotVAA0aC14SecretMaterialV_SSSgSbtYaKF 40:importIdentity(_:label:replaceExisting:) 176:@discardableResult func importIdentity(_ material: RadrootsIdentitySecretMaterial, label: String? = nil, replaceExisting: Bool = false) async throws -> RadrootsIdentitySnapshot 6:symbol 11:RadrootsKit 12:swift.method 118:s:11RadrootsKit0A23AppleBackgroundTransferC015handleEventsForD10URLSession10identifier17completionHandlerySS_yyYbctYaF 66:handleEventsForBackgroundURLSession(identifier:completionHandler:) 117:func handleEventsForBackgroundURLSession(identifier: String, completionHandler: @escaping @Sendable () -> Void) async +6:symbol 11:RadrootsKit 12:swift.method 118:s:11RadrootsKit0A28AppleBackgroundTransferStoreC25collectInactiveAdmissions5limitAA0A22AdmissionCleanupResultVSi_tYaKF 33:collectInactiveAdmissions(limit:) 94:func collectInactiveAdmissions(limit: Int = 64) async throws -> RadrootsAdmissionCleanupResult 6:symbol 11:RadrootsKit 12:swift.method 118:s:11RadrootsKit0A29UnavailableBackgroundTransferV6settle_12verificationyAA0adE10IdentifierV_AA0adE12VerificationOtYaKF 23:settle(_:verification:) 121:func settle(_: RadrootsBackgroundTransferIdentifier, verification _: RadrootsBackgroundTransferVerification) async throws 6:symbol 11:RadrootsKit 12:swift.method 119:s:11RadrootsKit0A24AppleKeychainSecureStoreC3put_3for6policyy10Foundation4DataV_AA0aeF3KeyVAA0A18SecretAccessPolicyVtKF 18:put(_:for:policy:) 120:func put(_ value: Data, for key: RadrootsSecureStoreKey, policy: RadrootsSecretAccessPolicy = .secureLocalSecret) throws 6:symbol 11:RadrootsKit 12:swift.method 120:s:11RadrootsKit0A23BackgroundTransferStoreP13withAdmission3for9operationAA0acD6HandleVAA0acD10IdentifierV_AHyYaYbKctYaKF 29:withAdmission(for:operation:) 205:func withAdmission(for identifier: RadrootsBackgroundTransferIdentifier, operation: @escaping @Sendable () async throws -> RadrootsBackgroundTransferHandle) async throws -> RadrootsBackgroundTransferHandle @@ -2301,6 +2309,7 @@ radroots.apple-kit.public-api.v1 6:symbol 11:RadrootsKit 12:swift.struct 41:s:11RadrootsKit0A21ExternalActionRequestV 29:RadrootsExternalActionRequest 36:struct RadrootsExternalActionRequest 6:symbol 11:RadrootsKit 12:swift.struct 41:s:11RadrootsKit0A21ProtectedDataProviderV 29:RadrootsProtectedDataProvider 36:struct RadrootsProtectedDataProvider 6:symbol 11:RadrootsKit 12:swift.struct 41:s:11RadrootsKit0A21SharePresentationLinkV 29:RadrootsSharePresentationLink 73:@MainActor struct RadrootsSharePresentationLink<Label> where Label : View +6:symbol 11:RadrootsKit 12:swift.struct 42:s:11RadrootsKit0A22AdmissionCleanupResultV 30:RadrootsAdmissionCleanupResult 37:struct RadrootsAdmissionCleanupResult 6:symbol 11:RadrootsKit 12:swift.struct 42:s:11RadrootsKit0A22BackgroundTaskSnapshotV 30:RadrootsBackgroundTaskSnapshot 37:struct RadrootsBackgroundTaskSnapshot 6:symbol 11:RadrootsKit 12:swift.struct 42:s:11RadrootsKit0A22CurrentLocationRequestV 30:RadrootsCurrentLocationRequest 37:struct RadrootsCurrentLocationRequest 6:symbol 11:RadrootsKit 12:swift.struct 42:s:11RadrootsKit0A22DocumentScannerSupportV 30:RadrootsDocumentScannerSupport 37:struct RadrootsDocumentScannerSupport @@ -2426,6 +2435,7 @@ radroots.apple-kit.public-api.v1 6:symbol 11:RadrootsKit 14:swift.property 58:s:11RadrootsKit0A17ShareTransferItemV7payloadAC7PayloadOvp 7:payload 46:let payload: RadrootsShareTransferItem.Payload 6:symbol 11:RadrootsKit 14:swift.property 58:s:11RadrootsKit0A18MediaImportRequestV14selectionLimitSivp 14:selectionLimit 23:let selectionLimit: Int 6:symbol 11:RadrootsKit 14:swift.property 58:s:11RadrootsKit0A20IdentityPublicRecordV12publicKeyHexSSvp 12:publicKeyHex 24:let publicKeyHex: String +6:symbol 11:RadrootsKit 14:swift.property 58:s:11RadrootsKit0A22AdmissionCleanupResultV10reachedEndSbvp 10:reachedEnd 20:let reachedEnd: Bool 6:symbol 11:RadrootsKit 14:swift.property 58:s:11RadrootsKit0A22PreparedExportDocumentV10preparedIDSSvp 10:preparedID 22:let preparedID: String 6:symbol 11:RadrootsKit 14:swift.property 58:s:11RadrootsKit0A22PreparedExportDocumentV9mediaTypeSSSgvp 9:mediaType 22:let mediaType: String? 6:symbol 11:RadrootsKit 14:swift.property 59:s:11RadrootsKit0A14TelemetryEventV6fieldsSayAA0aC5FieldVGvp 6:fields 36:let fields: [RadrootsTelemetryField] @@ -2442,6 +2452,7 @@ radroots.apple-kit.public-api.v1 6:symbol 11:RadrootsKit 14:swift.property 60:s:11RadrootsKit0A18PermissionSnapshotV6statusAA0aC6StatusOvp 6:status 36:let status: RadrootsPermissionStatus 6:symbol 11:RadrootsKit 14:swift.property 60:s:11RadrootsKit0A20IdentityPublicRecordV14identityHandleSSvp 14:identityHandle 26:let identityHandle: String 6:symbol 11:RadrootsKit 14:swift.property 60:s:11RadrootsKit0A21BackgroundTaskRequestV4kindAA0acD4KindOvp 4:kind 36:let kind: RadrootsBackgroundTaskKind +6:symbol 11:RadrootsKit 14:swift.property 60:s:11RadrootsKit0A22AdmissionCleanupResultV12removedFilesSivp 12:removedFiles 21:let removedFiles: Int 6:symbol 11:RadrootsKit 14:swift.property 60:s:11RadrootsKit0A26VerifiedArtifactDescriptorV9mediaTypeSSvp 9:mediaType 21:let mediaType: String 6:symbol 11:RadrootsKit 14:swift.property 61:s:11RadrootsKit0A15StagedBlobLeaseV7fileURL10Foundation0G0Vvp 7:fileURL 16:let fileURL: URL 6:symbol 11:RadrootsKit 14:swift.property 61:s:11RadrootsKit0A17MediaImportResultV5itemsSayAA0aC5AssetVGvp 5:items 31:let items: [RadrootsMediaAsset] @@ -2462,6 +2473,7 @@ radroots.apple-kit.public-api.v1 6:symbol 11:RadrootsKit 14:swift.property 62:s:11RadrootsKit0A20ExportDocumentResultV16exportedFilenameSSvp 16:exportedFilename 28:let exportedFilename: String 6:symbol 11:RadrootsKit 14:swift.property 62:s:11RadrootsKit0A20VerifiedArtifactFileV16debugDescriptionSSvp 16:debugDescription 36:var debugDescription: String { get } 6:symbol 11:RadrootsKit 14:swift.property 62:s:11RadrootsKit0A21CurrentLocationResultV15servedFromCacheSbvp 15:servedFromCache 25:let servedFromCache: Bool +6:symbol 11:RadrootsKit 14:swift.property 62:s:11RadrootsKit0A22AdmissionCleanupResultV14scannedEntriesSivp 14:scannedEntries 23:let scannedEntries: Int 6:symbol 11:RadrootsKit 14:swift.property 62:s:11RadrootsKit0A22CurrentLocationRequestV14timeoutSecondsSdvp 14:timeoutSeconds 32:let timeoutSeconds: TimeInterval 6:symbol 11:RadrootsKit 14:swift.property 62:s:11RadrootsKit0A25BackgroundTransferRequestV7headersSDyS2SGvp 7:headers 30:let headers: [String : String] 6:symbol 11:RadrootsKit 14:swift.property 62:s:11RadrootsKit0A26BackgroundTransferResponseV10statusCodeSivp 10:statusCode 19:let statusCode: Int