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:
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, ¤t, 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