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 68135576af9ac34126195178dabe82f02ff69497
parent 75a4a63149adb55b6a07f94686ef74b5446f1136
Author: triesap <tyson@radroots.org>
Date:   Sun, 20 Sep 2026 15:06:37 +0000

transfer: fence renewal behind inactive native execution

- Hold existing identifier admission across explicit caller work
- Reject active unknown and unreconciled native outcomes
- Preserve prior snapshots and late receipt persistence
- Verify competing owners and cancellation on macOS and iOS

Diffstat:
MSources/RadrootsKit/RadrootsAppleBackgroundTransfer.swift | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++----
MSources/RadrootsKit/RadrootsAppleBackgroundTransferStore.swift | 18++++++++++++------
MSources/RadrootsKit/RadrootsBackgroundTransfer.swift | 46++++++++++++++++++++++++++++++++++++----------
MSources/RadrootsKitTesting/RadrootsBackgroundTransferTesting.swift | 15+++++++++------
MTests/RadrootsKitTests/RadrootsAppleErrorSafetyTests.swift | 14+++++++-------
ATests/RadrootsKitTests/RadrootsInactiveExecutionTests.swift | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTests/RadrootsKitTests/RadrootsReceiptBarrierTests.swift | 10++++++----
Mcontracts/api_baselines/apple_kit.txt | 18++++++++++++------
8 files changed, 280 insertions(+), 43 deletions(-)

diff --git a/Sources/RadrootsKit/RadrootsAppleBackgroundTransfer.swift b/Sources/RadrootsKit/RadrootsAppleBackgroundTransfer.swift @@ -5,6 +5,9 @@ public actor RadrootsAppleBackgroundTransfer: RadrootsBackgroundTransfer { private let adapters: RadrootsAppleBackgroundTransferAdapters private var admissions: Set<RadrootsBackgroundTransferIdentifier> = [] private var stoppedAdmissions: [RadrootsBackgroundTransferIdentifier: RadrootsBackgroundTransferState] = [:] + private struct InactiveExecutionBodyError: Error { + let underlying: any Error + } public init(store: any RadrootsBackgroundTransferStore, adapters: RadrootsAppleBackgroundTransferAdapters) { self.store = store @@ -35,6 +38,48 @@ public actor RadrootsAppleBackgroundTransfer: RadrootsBackgroundTransfer { return try await admission(request, retry: true) } + public func withInactiveExecution<Result: Sendable>( + for identifier: RadrootsBackgroundTransferIdentifier, + operation: @escaping @Sendable (RadrootsBackgroundTransferSnapshot?) async throws -> Result + ) async throws -> Result { + try Task.checkCancellation() + try reserve(identifier) + defer { release(identifier) } + do { + return try await store.withAdmission(for: identifier) { + let snapshot = try await self.inactiveSnapshot(identifier) + try Task.checkCancellation() + do { return try await operation(snapshot) } + catch { throw InactiveExecutionBodyError(underlying: error) } + } + } catch let error as InactiveExecutionBodyError { + throw error.underlying + } catch is CancellationError { + throw CancellationError() + } catch let error as RadrootsBackgroundTransferError { + throw error + } catch { + throw RadrootsBackgroundTransferError.persistenceFailure + } + } + + private func inactiveSnapshot( + _ identifier: RadrootsBackgroundTransferIdentifier + ) async throws -> RadrootsBackgroundTransferSnapshot? { + guard try await !activeIdentifiers().contains(identifier), stoppedAdmissions[identifier] == nil else { + throw RadrootsBackgroundTransferError.transferFailure + } + let snapshot = try await load(identifier) + if let snapshot { + guard [.failed, .interrupted, .cancelled, .expired].contains(snapshot.state), + snapshot.response == nil, snapshot.downloadedArtifact == nil + else { throw RadrootsBackgroundTransferError.transferFailure } + } + // Do not rewrite state, drop a receipt or cancel an OS task. Completion + // callbacks may still persist late evidence while admission is held. + return snapshot + } + private func admission( _ request: RadrootsBackgroundTransferRequest, retry: Bool ) async throws -> RadrootsBackgroundTransferHandle { @@ -104,7 +149,8 @@ public actor RadrootsAppleBackgroundTransfer: RadrootsBackgroundTransfer { try? await adapters.cancel(request.identifier) } if let current = try await load(request.identifier), current.executionID == queued.executionID, - current.state == .queued || current.state == .running { + current.state == .queued || current.state == .running + { _ = try await exchange(current, current.transitioned(to: .failed, at: adapters.now(), failure: .enqueueFailed, possibleRemoteOrphan: request.isUpload)) @@ -114,7 +160,8 @@ public actor RadrootsAppleBackgroundTransfer: RadrootsBackgroundTransfer { if stoppedAdmissions[request.identifier] != nil || Task.isCancelled { try await stop(request.identifier, state: stoppedAdmissions[request.identifier] ?? .cancelled) } else if let current = try await load(request.identifier), current.executionID == queued.executionID, - current.state == .queued { + current.state == .queued + { _ = try await exchange(current, current.transitioned(to: .running, at: adapters.now())) } return RadrootsBackgroundTransferHandle(request: request) @@ -177,7 +224,8 @@ public actor RadrootsAppleBackgroundTransfer: RadrootsBackgroundTransfer { } public func snapshot(for identifier: RadrootsBackgroundTransferIdentifier) async throws - -> RadrootsBackgroundTransferSnapshot? { + -> RadrootsBackgroundTransferSnapshot? + { try await snapshots().first { $0.identifier == identifier } } @@ -216,7 +264,8 @@ public actor RadrootsAppleBackgroundTransfer: RadrootsBackgroundTransfer { } private func load(_ identifier: RadrootsBackgroundTransferIdentifier) async throws - -> RadrootsBackgroundTransferSnapshot? { + -> RadrootsBackgroundTransferSnapshot? + { try await loadAll().first { $0.identifier == identifier } } diff --git a/Sources/RadrootsKit/RadrootsAppleBackgroundTransferStore.swift b/Sources/RadrootsKit/RadrootsAppleBackgroundTransferStore.swift @@ -42,10 +42,10 @@ public actor RadrootsAppleBackgroundTransferStore: RadrootsBackgroundTransferSto try withStoreLock { try loadSnapshotsSynchronously() } } - public func withAdmission( + public func withAdmission<Result: Sendable>( for identifier: RadrootsBackgroundTransferIdentifier, - operation: @escaping @Sendable () async throws -> RadrootsBackgroundTransferHandle - ) async throws -> RadrootsBackgroundTransferHandle { + operation: @escaping @Sendable () async throws -> Result + ) async throws -> Result { guard let admission = try await acquireAdmission(identifier) else { throw RadrootsBackgroundTransferError.invalidRequest } @@ -127,14 +127,20 @@ public actor RadrootsAppleBackgroundTransferStore: RadrootsBackgroundTransferSto defer { Darwin.close(coordination) } do { let directory = try admissionCoordinationURL().deletingLastPathComponent() - if admissionScan == nil { admissionScan = try RadrootsAdmissionFileScan(url: directory) } + 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 try admissionScan.removeInactive(name: name, coordination: coordination) { + removed += 1 + } + } + if batch.reachedEnd { + self.admissionScan = nil } - if batch.reachedEnd { self.admissionScan = nil } return RadrootsAdmissionCleanupResult(scannedEntries: batch.scanned, removedFiles: removed, reachedEnd: batch.reachedEnd) } catch { admissionScan = nil diff --git a/Sources/RadrootsKit/RadrootsBackgroundTransfer.swift b/Sources/RadrootsKit/RadrootsBackgroundTransfer.swift @@ -19,7 +19,8 @@ extension RadrootsBackgroundTransferError: LocalizedError { } public struct RadrootsBackgroundTransferIdentifier: Sendable, Equatable, Hashable, Comparable, - Codable { + Codable +{ public let rawValue: String public init(_ value: String) throws { @@ -52,7 +53,8 @@ public struct RadrootsBackgroundTransferIdentifier: Sendable, Equatable, Hashabl } public enum RadrootsBackgroundTransferMethod: String, Sendable, Equatable, Hashable, Codable, - CaseIterable { + CaseIterable +{ case get = "GET" case post = "POST" case put = "PUT" @@ -69,7 +71,8 @@ public enum RadrootsBackgroundTransferOperation: Sendable, Equatable, Hashable, } public enum RadrootsBackgroundTransferState: String, Sendable, Equatable, Hashable, Codable, - CaseIterable { + CaseIterable +{ case queued case running case awaitingVerification @@ -81,7 +84,8 @@ public enum RadrootsBackgroundTransferState: String, Sendable, Equatable, Hashab } public enum RadrootsBackgroundTransferFailure: String, Sendable, Equatable, Hashable, Codable, - CaseIterable { + CaseIterable +{ case enqueueFailed = "background_transfer_enqueue_failed" case expired = "background_transfer_expired" case interrupted = "background_transfer_interrupted" @@ -106,10 +110,10 @@ public enum RadrootsBackgroundTransferNetworkPolicy: String, Sendable, Equatable public protocol RadrootsBackgroundTransferStore: Sendable { /// Serializes admission across suspension and independent owners. This is a /// nonblocking reservation, separate from short persistence transactions. - func withAdmission( + func withAdmission<Result: Sendable>( for identifier: RadrootsBackgroundTransferIdentifier, - operation: @escaping @Sendable () async throws -> RadrootsBackgroundTransferHandle - ) async throws -> RadrootsBackgroundTransferHandle + operation: @escaping @Sendable () async throws -> Result + ) async throws -> Result func admissionIsActive(for identifier: RadrootsBackgroundTransferIdentifier) async throws -> Bool /// Atomically installs a redacted snapshot only if the exact expected value /// still owns the identifier. Nil reserves a previously absent identifier. @@ -123,6 +127,16 @@ public protocol RadrootsBackgroundTransferStore: Sendable { } public protocol RadrootsBackgroundTransfer: Sendable { + /// Explicitly reconcile the OS and retain exclusive admission for this prior + /// identifier during bounded caller work. Only absent or inactive failed + /// execution without a retained response is admitted. Unknown state throws. + /// This does not establish absence of remote effects or grant retry/signing + /// authority. Preserve prior lineage and use a new identifier for renewal; + /// enqueue/retry of the reserved identifier is rejected until the body exits. + func withInactiveExecution<Result: Sendable>( + for identifier: RadrootsBackgroundTransferIdentifier, + operation: @escaping @Sendable (RadrootsBackgroundTransferSnapshot?) async throws -> Result + ) async throws -> Result func enqueue(_ request: RadrootsBackgroundTransferRequest) async throws -> RadrootsBackgroundTransferHandle func retry(_ request: RadrootsBackgroundTransferRequest) async throws @@ -141,6 +155,15 @@ public protocol RadrootsBackgroundTransfer: Sendable { ) async } +public extension RadrootsBackgroundTransfer { + func withInactiveExecution<Result: Sendable>( + for _: RadrootsBackgroundTransferIdentifier, + operation _: @escaping @Sendable (RadrootsBackgroundTransferSnapshot?) async throws -> Result + ) async throws -> Result { + throw RadrootsBackgroundTransferError.unavailable + } +} + public protocol RadrootsBackgroundTransferFileResolver: Sendable { func resolve(_ file: RadrootsBackgroundTransferLocalFile) throws -> URL func read(_ file: RadrootsBackgroundTransferLocalFile, maximumBytes: Int) throws -> Data @@ -155,12 +178,14 @@ public struct RadrootsUnavailableBackgroundTransfer: RadrootsBackgroundTransfer, public init() {} public func enqueue(_: RadrootsBackgroundTransferRequest) async throws - -> RadrootsBackgroundTransferHandle { + -> RadrootsBackgroundTransferHandle + { throw RadrootsBackgroundTransferError.unavailable } public func retry(_: RadrootsBackgroundTransferRequest) async throws - -> RadrootsBackgroundTransferHandle { + -> RadrootsBackgroundTransferHandle + { throw RadrootsBackgroundTransferError.unavailable } @@ -180,7 +205,8 @@ public struct RadrootsUnavailableBackgroundTransfer: RadrootsBackgroundTransfer, } public func snapshot(for _: RadrootsBackgroundTransferIdentifier) async throws - -> RadrootsBackgroundTransferSnapshot? { + -> RadrootsBackgroundTransferSnapshot? + { throw RadrootsBackgroundTransferError.unavailable } diff --git a/Sources/RadrootsKitTesting/RadrootsBackgroundTransferTesting.swift b/Sources/RadrootsKitTesting/RadrootsBackgroundTransferTesting.swift @@ -4,10 +4,10 @@ import RadrootsKit public actor RadrootsInMemoryBackgroundTransferStore: RadrootsBackgroundTransferStore { private var admissions: Set<RadrootsBackgroundTransferIdentifier> = [] - public func withAdmission( + public func withAdmission<Result: Sendable>( for identifier: RadrootsBackgroundTransferIdentifier, - operation: @escaping @Sendable () async throws -> RadrootsBackgroundTransferHandle - ) async throws -> RadrootsBackgroundTransferHandle { + operation: @escaping @Sendable () async throws -> Result + ) async throws -> Result { guard admissions.insert(identifier).inserted else { throw RadrootsBackgroundTransferError.invalidRequest } defer { admissions.remove(identifier) } return try await operation() @@ -79,7 +79,8 @@ public actor RadrootsFakeBackgroundTransfer: RadrootsBackgroundTransfer { } public func enqueue(_ request: RadrootsBackgroundTransferRequest) async throws - -> RadrootsBackgroundTransferHandle { + -> RadrootsBackgroundTransferHandle + { enqueuedRequestsValue.append(request) switch enqueueOutcome { case .success: @@ -96,7 +97,8 @@ public actor RadrootsFakeBackgroundTransfer: RadrootsBackgroundTransfer { } public func retry(_ request: RadrootsBackgroundTransferRequest) async throws - -> RadrootsBackgroundTransferHandle { + -> RadrootsBackgroundTransferHandle + { try await enqueue(request) } @@ -197,7 +199,8 @@ public actor RadrootsFakeBackgroundTransfer: RadrootsBackgroundTransfer { } public func snapshot(for identifier: RadrootsBackgroundTransferIdentifier) async throws - -> RadrootsBackgroundTransferSnapshot? { + -> RadrootsBackgroundTransferSnapshot? + { try await store.loadSnapshots().first { $0.identifier == identifier } } diff --git a/Tests/RadrootsKitTests/RadrootsAppleErrorSafetyTests.swift b/Tests/RadrootsKitTests/RadrootsAppleErrorSafetyTests.swift @@ -8,14 +8,14 @@ import Testing let forbidden = [ "/Users/example/private.sqlite", "https://secret.example.invalid/token", - "nsec1secretcanary" + "nsec1secretcanary", ] for error in errors { let renderings = [ String(describing: error), String(reflecting: error), - (error as NSError).localizedDescription + (error as NSError).localizedDescription, ] #expect(renderings.allSatisfy { !$0.isEmpty }) #expect( @@ -32,7 +32,7 @@ import Testing code: 7, userInfo: [ NSLocalizedDescriptionKey: - "https://secret.example.invalid/token nsec1secretcanary" + "https://secret.example.invalid/token nsec1secretcanary", ] ) @@ -93,10 +93,10 @@ private func dependencyCanaryError() -> NSError { } private actor ThrowingBackgroundTransferStore: RadrootsBackgroundTransferStore { - func withAdmission( + func withAdmission<Result: Sendable>( for _: RadrootsBackgroundTransferIdentifier, - operation _: @escaping @Sendable () async throws -> RadrootsBackgroundTransferHandle - ) async throws -> RadrootsBackgroundTransferHandle { + operation _: @escaping @Sendable () async throws -> Result + ) async throws -> Result { throw dependencyCanaryError() } @@ -194,7 +194,7 @@ private let publicAppleErrorExamples: [any Error] = [ RadrootsVerifiedArtifactAccessError.protectedDataUnavailable, RadrootsVerifiedArtifactAccessError.artifactUnavailable, RadrootsVerifiedArtifactAccessError.artifactCorrupt, - RadrootsVerifiedArtifactAccessError.fileSystemFailure + RadrootsVerifiedArtifactAccessError.fileSystemFailure, ] private func verifyBackgroundTaskErrorMapping() async throws { diff --git a/Tests/RadrootsKitTests/RadrootsInactiveExecutionTests.swift b/Tests/RadrootsKitTests/RadrootsInactiveExecutionTests.swift @@ -0,0 +1,145 @@ +import Foundation +@testable import RadrootsKit +import RadrootsKitTesting +import Testing + +@Test func inactiveExecutionRefusesActiveTerminalSnapshots() async throws { + let request = try appleUploadRequest(identifier: "renewal.active") + for state: RadrootsBackgroundTransferState in [.failed, .expired, .cancelled, .interrupted] { + let snapshot = try RadrootsBackgroundTransferSnapshot(request: request, state: state, executionID: UUID()) + let store = RadrootsInMemoryBackgroundTransferStore(snapshots: [snapshot]) + let probe = RadrootsAppleBackgroundTransferProbe(activeIdentifiers: [request.identifier]) + let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: probe.adapters()) + let called = RadrootsCompletionProbe() + await #expect(throws: RadrootsBackgroundTransferError.transferFailure) { + try await transfer.withInactiveExecution(for: request.identifier) { _ in called.markCompleted() } + } + #expect(!called.completed) + #expect(try await store.loadSnapshots() == [snapshot]) + #expect(await probe.enqueuedRequests.isEmpty) + #expect(await probe.cancelledIdentifiers.isEmpty) + #expect(try await !store.admissionIsActive(for: request.identifier)) + } +} + +@Test func inactiveExecutionRefusesUnreconciledOrReceiptedSnapshots() async throws { + let request = try appleUploadRequest(identifier: "renewal.receipt") + for state: RadrootsBackgroundTransferState in [.queued, .running, .awaitingVerification, .completed] { + let snapshot = try RadrootsBackgroundTransferSnapshot(request: request, state: state) + let store = RadrootsInMemoryBackgroundTransferStore(snapshots: [snapshot]) + let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: RadrootsAppleBackgroundTransferProbe().adapters()) + let called = RadrootsCompletionProbe() + await #expect(throws: RadrootsBackgroundTransferError.transferFailure) { + try await transfer.withInactiveExecution(for: request.identifier) { _ in called.markCompleted() } + } + #expect(!called.completed) + #expect(try await store.loadSnapshots() == [snapshot]) + } +} + +@Test func inactiveExecutionUnknownInventoryPreservesEvidence() async throws { + let request = try appleUploadRequest(identifier: "renewal.unknown") + let snapshot = try RadrootsBackgroundTransferSnapshot(request: request, state: .expired) + let store = RadrootsInMemoryBackgroundTransferStore(snapshots: [snapshot]) + let called = RadrootsCompletionProbe() + let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: .unavailable) + await #expect(throws: RadrootsBackgroundTransferError.unavailable) { + try await transfer.withInactiveExecution(for: request.identifier) { _ in called.markCompleted() } + } + #expect(!called.completed) + #expect(try await store.loadSnapshots() == [snapshot]) + #expect(try await !store.admissionIsActive(for: request.identifier)) +} + +@Test func inactiveExecutionSuppliesExactRedactedSnapshotOrAbsenceThroughExistential() async throws { + let request = try appleUploadRequest(identifier: "renewal.inactive") + let snapshot = try RadrootsBackgroundTransferSnapshot(request: request, state: .expired, executionID: UUID()) + for snapshots in [[], [snapshot]] { + let store = RadrootsInMemoryBackgroundTransferStore(snapshots: snapshots) + let probe = RadrootsAppleBackgroundTransferProbe() + let transfer: any RadrootsBackgroundTransfer = RadrootsAppleBackgroundTransfer(store: store, adapters: probe.adapters()) + let result = try await transfer.withInactiveExecution(for: request.identifier) { observed in + #expect(observed == snapshots.first) + let reserved = try await store.admissionIsActive(for: request.identifier) + #expect(reserved) + return "caller-result" + } + #expect(result == "caller-result") + #expect(try await store.loadSnapshots() == snapshots) + #expect(await probe.enqueuedRequests.isEmpty) + #expect(await probe.cancelledIdentifiers.isEmpty) + #expect(try await !store.admissionIsActive(for: request.identifier)) + } +} + +@Test func inactiveExecutionFencesIndependentOwnersAndRetainsLateReceipt() async throws { + let roots = try appleTransferRoots() + defer { try? FileManager.default.removeItem(at: roots.dataRoot.deletingLastPathComponent()) } + let request = try appleUploadRequest(identifier: "renewal.exclusive") + let snapshot = try RadrootsBackgroundTransferSnapshot(request: request, state: .interrupted, executionID: UUID()) + let late = try snapshot.transitioned(to: .awaitingVerification, at: Date()) + let firstStore = RadrootsAppleBackgroundTransferStore(roots: roots) + let otherStore = RadrootsAppleBackgroundTransferStore(roots: roots) + try await firstStore.saveSnapshot(snapshot) + let probe = RadrootsAppleBackgroundTransferProbe() + let first = RadrootsAppleBackgroundTransfer(store: firstStore, adapters: probe.adapters()) + let other = RadrootsAppleBackgroundTransfer(store: otherStore, adapters: probe.adapters()) + let result = try await first.withInactiveExecution(for: request.identifier) { observed in + #expect(observed == snapshot) + await #expect(throws: RadrootsBackgroundTransferError.invalidRequest) { _ = try await other.retry(request) } + await #expect(throws: RadrootsBackgroundTransferError.invalidRequest) { + try await other.withInactiveExecution(for: request.identifier) { _ in Issue.record("Competing renewal admitted") } + } + await #expect(throws: RadrootsBackgroundTransferError.invalidRequest) { _ = try await first.retry(request) } + // Delegate persistence remains available during the long reservation. + let retained = try await otherStore.compareExchangeSnapshot(expected: snapshot, desired: late) + #expect(retained) + return 42 + } + #expect(result == 42) + #expect(try await otherStore.loadSnapshots() == [late]) + #expect(try await !otherStore.admissionIsActive(for: request.identifier)) + #expect(await probe.enqueuedRequests.isEmpty) + await #expect(throws: RadrootsBackgroundTransferError.transferFailure) { + try await other.withInactiveExecution(for: request.identifier) { _ in Issue.record("Receipt discarded") } + } +} + +@Test func inactiveExecutionCallerDenialReleasesAdmissionAndPreservesError() async throws { + let request = try appleUploadRequest(identifier: "renewal.denied") + let snapshot = try RadrootsBackgroundTransferSnapshot(request: request, state: .expired) + let store = RadrootsInMemoryBackgroundTransferStore(snapshots: [snapshot]) + let probe = RadrootsAppleBackgroundTransferProbe() + let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: probe.adapters()) + await #expect(throws: RenewalTestDenial.denied) { + try await transfer.withInactiveExecution(for: request.identifier) { _ in throw RenewalTestDenial.denied } + } + #expect(try await store.loadSnapshots() == [snapshot]) + #expect(try await !store.admissionIsActive(for: request.identifier)) + _ = try await transfer.retry(request) + #expect(await probe.enqueuedRequests == [request]) +} + +@Test func inactiveExecutionCancellationDuringQueryNeverCallsBody() async throws { + let request = try appleUploadRequest(identifier: "renewal.cancelled-query") + let store = RadrootsInMemoryBackgroundTransferStore() + let called = RadrootsCompletionProbe() + let (entered, signal) = AsyncStream<Void>.makeStream() + let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: RadrootsAppleBackgroundTransferAdapters( + enqueue: { _, _ in Issue.record("Unexpected enqueue") }, cancel: { _ in Issue.record("Unexpected cancel") }, + activeTransferIdentifiers: { + try await RadrootsBackgroundTaskQuery.read { _ in signal.yield(); signal.finish() } + }, handleBackgroundEvents: { _, completion in completion() } + )) + let task = Task { try await transfer.withInactiveExecution(for: request.identifier) { _ in called.markCompleted() } } + for await _ in entered { + break + } + task.cancel() + await #expect(throws: RadrootsBackgroundTransferError.transferFailure) { try await task.value } + #expect(!called.completed) + #expect(try await !store.admissionIsActive(for: request.identifier)) + #expect(try await store.loadSnapshots().isEmpty) +} + +private enum RenewalTestDenial: Error, Equatable { case denied } diff --git a/Tests/RadrootsKitTests/RadrootsReceiptBarrierTests.swift b/Tests/RadrootsKitTests/RadrootsReceiptBarrierTests.swift @@ -116,7 +116,8 @@ private actor ReceiptBarrierStore: RadrootsBackgroundTransferStore { } func compareExchangeSnapshot(expected: RadrootsBackgroundTransferSnapshot?, - desired: RadrootsBackgroundTransferSnapshot) async throws -> Bool { + desired: RadrootsBackgroundTransferSnapshot) async throws -> Bool + { if desired.state == .awaitingVerification { terminalAttempts += 1 if terminalAttempts == 1 { @@ -142,9 +143,10 @@ private actor ReceiptBarrierStore: RadrootsBackgroundTransferStore { return exchanged } - func withAdmission(for identifier: RadrootsBackgroundTransferIdentifier, - operation: @escaping @Sendable () async throws -> RadrootsBackgroundTransferHandle) - async throws -> RadrootsBackgroundTransferHandle { + func withAdmission<Result: Sendable>(for identifier: RadrootsBackgroundTransferIdentifier, + operation: @escaping @Sendable () async throws -> Result) + async throws -> Result + { try await storage.withAdmission(for: identifier, operation: operation) } diff --git a/contracts/api_baselines/apple_kit.txt b/contracts/api_baselines/apple_kit.txt @@ -742,9 +742,10 @@ radroots.apple-kit.public-api.v1 12:relationship 11:RadrootsKit 13:requirementOf 109:s:11RadrootsKit0A30BackgroundTransferFileResolverP4read_12maximumBytes10Foundation4DataVAA0acd5LocalE0O_SitKF 50:s:11RadrootsKit0A30BackgroundTransferFileResolverP 0: 12:relationship 11:RadrootsKit 13:requirementOf 113:s:11RadrootsKit0A18BackgroundTransferP015handleEventsForC10URLSession10identifier17completionHandlerySS_yyYbctYaF 38:s:11RadrootsKit0A18BackgroundTransferP 0: 12:relationship 11:RadrootsKit 13:requirementOf 116:s:11RadrootsKit0A10FileAccessP9stageBlob_9mediaType12filenameHintAA0a6StagedF9ReferenceV10Foundation4DataV_SSSgALtKF 30:s:11RadrootsKit0A10FileAccessP 0: -12:relationship 11:RadrootsKit 13:requirementOf 120:s:11RadrootsKit0A23BackgroundTransferStoreP13withAdmission3for9operationAA0acD6HandleVAA0acD10IdentifierV_AHyYaYbKctYaKF 43:s:11RadrootsKit0A23BackgroundTransferStoreP 0: 12:relationship 11:RadrootsKit 13:requirementOf 126:s:11RadrootsKit0A10FileAccessP013stageExternalC0_9mediaType12filenameHintAA0A19StagedBlobReferenceV10Foundation3URLV_SSSgALtKF 30:s:11RadrootsKit0A10FileAccessP 0: +12:relationship 11:RadrootsKit 13:requirementOf 127:s:11RadrootsKit0A23BackgroundTransferStoreP13withAdmission3for9operationqd__AA0acD10IdentifierV_qd__yYaYbKctYaKs8SendableRd__lF 43:s:11RadrootsKit0A23BackgroundTransferStoreP 0: 12:relationship 11:RadrootsKit 13:requirementOf 146:s:11RadrootsKit0A10FileAccessP012copyExternalC0_2to9mediaType17suggestedFilenameAA0A16ImportedDocumentV10Foundation3URLV_AA0aC9ReferenceVSSSgAOtKF 30:s:11RadrootsKit0A10FileAccessP 0: +12:relationship 11:RadrootsKit 13:requirementOf 147:s:11RadrootsKit0A18BackgroundTransferP21withInactiveExecution3for9operationqd__AA0acD10IdentifierV_qd__AA0acD8SnapshotVSgYaYbKctYaKs8SendableRd__lF 38:s:11RadrootsKit0A18BackgroundTransferP 0: 12:relationship 11:RadrootsKit 13:requirementOf 171:s:11RadrootsKit0A30BackgroundTransferFileResolverP18prepareUploadLease3for11executionID8existingAA0a10StagedBlobI0VAA0acD7RequestV_10Foundation4UUIDVAA0anO9ReferenceVSgtKF 50:s:11RadrootsKit0A30BackgroundTransferFileResolverP 0: 12:relationship 11:RadrootsKit 13:requirementOf 52:s:11RadrootsKit0A10FileAccessP16resetStagedBlobsyyKF 30:s:11RadrootsKit0A10FileAccessP 0: 12:relationship 11:RadrootsKit 13:requirementOf 52:s:11RadrootsKit0A9TelemetryP6recordyyAA0aC5EventVYaF 28:s:11RadrootsKit0A9TelemetryP 0: @@ -796,6 +797,7 @@ radroots.apple-kit.public-api.v1 12:relationship 11:RadrootsKit 13:requirementOf 94:s:11RadrootsKit0A18BackgroundTransferP8snapshot3forAA0acD8SnapshotVSgAA0acD10IdentifierV_tYaKF 38:s:11RadrootsKit0A18BackgroundTransferP 0: 12:relationship 11:RadrootsKit 13:requirementOf 98:s:11RadrootsKit0A24PermissionStatusProviderP9snapshots3forSayAA0aC8SnapshotVGSayAA0aC4KindOG_tYaKF 44:s:11RadrootsKit0A24PermissionStatusProviderP 0: 12:relationship 11:RadrootsKit 23:defaultImplementationOf 101:s:11RadrootsKit0A24PermissionStatusProviderPAAE9snapshots3forSayAA0aC8SnapshotVGSayAA0aC4KindOG_tYaKF 98:s:11RadrootsKit0A24PermissionStatusProviderP9snapshots3forSayAA0aC8SnapshotVGSayAA0aC4KindOG_tYaKF 0: +12:relationship 11:RadrootsKit 23:defaultImplementationOf 150:s:11RadrootsKit0A18BackgroundTransferPAAE21withInactiveExecution3for9operationqd__AA0acD10IdentifierV_qd__AA0acD8SnapshotVSgYaYbKctYaKs8SendableRd__lF 147:s:11RadrootsKit0A18BackgroundTransferP21withInactiveExecution3for9operationqd__AA0acD10IdentifierV_qd__AA0acD8SnapshotVSgYaYbKctYaKs8SendableRd__lF 0: 12:relationship 11:RadrootsKit 8:memberOf 100:s:11RadrootsKit0A15LocationReadingV7isFresh10relativeTo17maximumAgeSecondsSb10Foundation4DateV_SdtKF 35:s:11RadrootsKit0A15LocationReadingV 0: 12:relationship 11:RadrootsKit 8:memberOf 100:s:11RadrootsKit0A26PreparedExportFileDocumentV13configurationAC7SwiftUI0eF17ReadConfigurationV_tKcfc 46:s:11RadrootsKit0A26PreparedExportFileDocumentV 0: 12:relationship 11:RadrootsKit 8:memberOf 101:s:11RadrootsKit0A18AppleMediaPreparerC12prepareImageyAA0ac8PreparedG0VAA0acG18PreparationRequestVYaKF 38:s:11RadrootsKit0A18AppleMediaPreparerC 0: @@ -857,12 +859,12 @@ radroots.apple-kit.public-api.v1 12:relationship 11:RadrootsKit 8:memberOf 123:s:11RadrootsKit0A9FileEntryV4file4name11isDirectory9sizeBytes10modifiedAtAcA0aC9ReferenceV_SSSbSiSg10Foundation4DateVSgtcfc 28:s:11RadrootsKit0A9FileEntryV 0: 12:relationship 11:RadrootsKit 8:memberOf 124:s:11RadrootsKit0A21ExportDocumentRequestV6source17suggestedFilename9mediaType9sizeBytesAcA0acD6SourceO_S2SSgs6UInt64VSgtKcfc 41:s:11RadrootsKit0A21ExportDocumentRequestV 0: 12:relationship 11:RadrootsKit 8:memberOf 124:s:11RadrootsKit0A25AppleUserPresenceAdaptersV13currentStatus6verifyAcA0adeH0VyYaYbKc_AA0adE6ResultVAA0adE7RequestVYaYbKctcfc 45:s:11RadrootsKit0A25AppleUserPresenceAdaptersV 0: +12:relationship 11:RadrootsKit 8:memberOf 124:s:11RadrootsKit0A28AppleBackgroundTransferStoreC13withAdmission3for9operationxAA0adE10IdentifierV_xyYaYbKctYaKs8SendableRzlF 48:s:11RadrootsKit0A28AppleBackgroundTransferStoreC 0: 12:relationship 11:RadrootsKit 8:memberOf 124:s:11RadrootsKit0A29UnavailableBackgroundTransferV015handleEventsForD10URLSession10identifier17completionHandlerySS_yyYbctYaF 49:s:11RadrootsKit0A29UnavailableBackgroundTransferV 0: 12:relationship 11:RadrootsKit 8:memberOf 125:s:11RadrootsKit0A16IdentitySnapshotV5state8identity12signerHandle12recoveryCodeAcA0aC5StateO_AA0aC12PublicRecordVSgSSSgAMtcfc 36:s:11RadrootsKit0A16IdentitySnapshotV 0: 12:relationship 11:RadrootsKit 8:memberOf 125:s:11RadrootsKit0A18MediaImportRequestV07allowedC5Kinds14selectionLimit16destinationScopeACSayAA0aC4KindOG_SiAA0a4FileK0OtKcfc 38:s:11RadrootsKit0A18MediaImportRequestV 0: 12:relationship 11:RadrootsKit 8:memberOf 125:s:11RadrootsKit0A21CurrentLocationResultV7reading13authorization15servedFromCacheAcA0aD7ReadingV_AA0aD13AuthorizationOSbtKcfc 41:s:11RadrootsKit0A21CurrentLocationResultV 0: 12:relationship 11:RadrootsKit 8:memberOf 125:s:11RadrootsKit0A22CurrentLocationRequestV14timeoutSeconds21desiredAccuracyMeters023maximumCachedReadingAgeG0ACSd_SdSgAGtKcfc 42:s:11RadrootsKit0A22CurrentLocationRequestV 0: -12:relationship 11:RadrootsKit 8:memberOf 125:s:11RadrootsKit0A28AppleBackgroundTransferStoreC13withAdmission3for9operationAA0adE6HandleVAA0adE10IdentifierV_AHyYaYbKctYaKF 48:s:11RadrootsKit0A28AppleBackgroundTransferStoreC 0: 12:relationship 11:RadrootsKit 8:memberOf 126:s:11RadrootsKit0A24ExternalActionValidationO12appleMapsURL10coordinate5label10Foundation0H0VAA0A18LocationCoordinateV_SSSgtKFZ 44:s:11RadrootsKit0A24ExternalActionValidationO 0: 12:relationship 11:RadrootsKit 8:memberOf 126:s:11RadrootsKit0A26BackgroundTransferResponseV10statusCode9mediaType15contentEncoding4bodyACSi_SSSgAH10Foundation4DataVSgtKcfc 46:s:11RadrootsKit0A26BackgroundTransferResponseV 0: 12:relationship 11:RadrootsKit 8:memberOf 126:s:11RadrootsKit0A27DocumentPresentationAdapterO12contentTypes3forSay22UniformTypeIdentifiers6UTTypeVGAA0aC13ImportRequestV_tFZ 47:s:11RadrootsKit0A27DocumentPresentationAdapterO 0: @@ -882,6 +884,7 @@ radroots.apple-kit.public-api.v1 12:relationship 11:RadrootsKit 8:memberOf 143:s:11RadrootsKit0A21DocumentImportRequestV19allowedContentKinds23allowsMultipleSelection16destinationScopeACSayAA0acG4KindOG_SbAA0a4FileM0OtKcfc 41:s:11RadrootsKit0A21DocumentImportRequestV 0: 12:relationship 11:RadrootsKit 8:memberOf 143:s:11RadrootsKit0A22PreparedExportDocumentV10preparedID7fileURL17suggestedFilename9mediaType9sizeBytesACSS_10Foundation0I0VS2SSgs6UInt64VSgtKcfc 42:s:11RadrootsKit0A22PreparedExportDocumentV 0: 12:relationship 11:RadrootsKit 8:memberOf 144:s:11RadrootsKit0A18UserPresenceStatusV7support12biometryKind27canEvaluateDeviceCredential0iJ10BiometricsAcA0acD7SupportO_AA0a8BiometryH0OS2btcfc 38:s:11RadrootsKit0A18UserPresenceStatusV 0: +12:relationship 11:RadrootsKit 8:memberOf 144:s:11RadrootsKit0A23AppleBackgroundTransferC21withInactiveExecution3for9operationxAA0adE10IdentifierV_xAA0adE8SnapshotVSgYaYbKctYaKs8SendableRzlF 43:s:11RadrootsKit0A23AppleBackgroundTransferC 0: 12:relationship 11:RadrootsKit 8:memberOf 151:s:11RadrootsKit0A15AppleFileAccessC012copyExternalD0_2to9mediaType17suggestedFilenameAA0A16ImportedDocumentV10Foundation3URLV_AA0aD9ReferenceVSSSgAOtKF 35:s:11RadrootsKit0A15AppleFileAccessC 0: 12:relationship 11:RadrootsKit 8:memberOf 152:s:11RadrootsKit0A15IdentityCustodyC014importPortableC0_10passphrase15replaceExistingAA0aC8SnapshotVAA0aC19PortabilityEnvelopeV_AA0aC10PassphraseVSbtYaKF 35:s:11RadrootsKit0A15IdentityCustodyC 0: 12:relationship 11:RadrootsKit 8:memberOf 152:s:11RadrootsKit0A16ImportedDocumentV4file11originalURL17suggestedFilename9mediaType9sizeBytesAcA0A13FileReferenceV_10Foundation0G0VSgS2SSgs6UInt64VtKcfc 36:s:11RadrootsKit0A16ImportedDocumentV 0: @@ -1800,7 +1803,7 @@ radroots.apple-kit.public-api.v1 12:relationship 18:RadrootsKitTesting 8:memberOf 124:s:18RadrootsKitTesting0A22FakeBackgroundTransferC015handleEventsForE10URLSession10identifier17completionHandlerySS_yyYbctYaF 49:s:18RadrootsKitTesting0A22FakeBackgroundTransferC 0: 12:relationship 18:RadrootsKitTesting 8:memberOf 128:s:18RadrootsKitTesting0A25UITestLaunchConfigurationV13deterministic11environment9arguments8language6localeACSDyS2SG_SaySSGS2StFZ 52:s:18RadrootsKitTesting0A25UITestLaunchConfigurationV 0: 12:relationship 18:RadrootsKitTesting 8:memberOf 128:s:18RadrootsKitTesting0A32InMemoryPermissionStatusProviderC03setG0_3for10observedAty0aB00afG0O_AG0aF4KindO10Foundation4DateVSgtF 59:s:18RadrootsKitTesting0A32InMemoryPermissionStatusProviderC 0: -12:relationship 18:RadrootsKitTesting 8:memberOf 137:s:18RadrootsKitTesting0A31InMemoryBackgroundTransferStoreC13withAdmission3for9operation0aB00afG6HandleVAG0afG10IdentifierV_AIyYaYbKctYaKF 58:s:18RadrootsKitTesting0A31InMemoryBackgroundTransferStoreC 0: +12:relationship 18:RadrootsKitTesting 8:memberOf 136:s:18RadrootsKitTesting0A31InMemoryBackgroundTransferStoreC13withAdmission3for9operationx0aB00afG10IdentifierV_xyYaYbKctYaKs8SendableRzlF 58:s:18RadrootsKitTesting0A31InMemoryBackgroundTransferStoreC 0: 12:relationship 18:RadrootsKitTesting 8:memberOf 142:s:18RadrootsKitTesting0A19FakeDocumentScannerC7support11scanOutcomeAC0aB00aeF7SupportV_s6ResultOyAF0a7ScannedE0VAF0A18CaptureIntakeErrorOGtcfc 46:s:18RadrootsKitTesting0A19FakeDocumentScannerC 0: 12:relationship 18:RadrootsKitTesting 8:memberOf 143:s:18RadrootsKitTesting0A32InMemoryPermissionStatusProviderC8statuses07defaultG010observedAtACSDy0aB00aF4KindOAG0afG0OG_AK10Foundation4DateVtcfc 59:s:18RadrootsKitTesting0A32InMemoryPermissionStatusProviderC 0: 12:relationship 18:RadrootsKitTesting 8:memberOf 148:s:18RadrootsKitTesting0A22FakeBackgroundTransferC5store14enqueueOutcome9updatedAtAC0aB00aeF5Store_p_s6ResultOyytAG0aeF5ErrorOG10Foundation4DateVtcfc 49:s:18RadrootsKitTesting0A22FakeBackgroundTransferC 0: @@ -2144,14 +2147,17 @@ radroots.apple-kit.public-api.v1 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 6:symbol 11:RadrootsKit 12:swift.method 121:s:11RadrootsKit0A15AppleFileAccessC9stageBlob_9mediaType12filenameHintAA0a6StagedG9ReferenceV10Foundation4DataV_SSSgALtKF 36:stageBlob(_:mediaType:filenameHint:) 140:@discardableResult func stageBlob(_ data: Data, mediaType: String? = nil, filenameHint: String? = nil) throws -> RadrootsStagedBlobReference 6:symbol 11:RadrootsKit 12:swift.method 123:s:11RadrootsKit0A26PreparedExportFileDocumentV11fileWrapper13configurationSo06NSFileH0C7SwiftUI0eF18WriteConfigurationV_tKF 27:fileWrapper(configuration:) 110:func fileWrapper(configuration _: RadrootsPreparedExportFileDocument.WriteConfiguration) throws -> FileWrapper +6:symbol 11:RadrootsKit 12:swift.method 124:s:11RadrootsKit0A28AppleBackgroundTransferStoreC13withAdmission3for9operationxAA0adE10IdentifierV_xyYaYbKctYaKs8SendableRzlF 29:withAdmission(for:operation:) 185:func withAdmission<Result>(for identifier: RadrootsBackgroundTransferIdentifier, operation: @escaping @Sendable () async throws -> Result) async throws -> Result where Result : Sendable 6:symbol 11:RadrootsKit 12:swift.method 124:s:11RadrootsKit0A29UnavailableBackgroundTransferV015handleEventsForD10URLSession10identifier17completionHandlerySS_yyYbctYaF 66:handleEventsForBackgroundURLSession(identifier:completionHandler:) 119:func handleEventsForBackgroundURLSession(identifier _: String, completionHandler: @escaping @Sendable () -> Void) async -6:symbol 11:RadrootsKit 12:swift.method 125:s:11RadrootsKit0A28AppleBackgroundTransferStoreC13withAdmission3for9operationAA0adE6HandleVAA0adE10IdentifierV_AHyYaYbKctYaKF 29:withAdmission(for:operation:) 205:func withAdmission(for identifier: RadrootsBackgroundTransferIdentifier, operation: @escaping @Sendable () async throws -> RadrootsBackgroundTransferHandle) async throws -> RadrootsBackgroundTransferHandle 6:symbol 11:RadrootsKit 12:swift.method 126:s:11RadrootsKit0A10FileAccessP013stageExternalC0_9mediaType12filenameHintAA0A19StagedBlobReferenceV10Foundation3URLV_SSSgALtKF 44:stageExternalFile(_:mediaType:filenameHint:) 140:@discardableResult func stageExternalFile(_ sourceURL: URL, mediaType: String?, filenameHint: String?) throws -> RadrootsStagedBlobReference +6:symbol 11:RadrootsKit 12:swift.method 127:s:11RadrootsKit0A23BackgroundTransferStoreP13withAdmission3for9operationqd__AA0acD10IdentifierV_qd__yYaYbKctYaKs8SendableRd__lF 29:withAdmission(for:operation:) 185:func withAdmission<Result>(for identifier: RadrootsBackgroundTransferIdentifier, operation: @escaping @Sendable () async throws -> Result) async throws -> Result where Result : Sendable 6:symbol 11:RadrootsKit 12:swift.method 131:s:11RadrootsKit0A15AppleFileAccessC013stageExternalD0_9mediaType12filenameHintAA0A19StagedBlobReferenceV10Foundation3URLV_SSSgALtKF 44:stageExternalFile(_:mediaType:filenameHint:) 152:@discardableResult func stageExternalFile(_ sourceURL: URL, mediaType: String? = nil, filenameHint: String? = nil) throws -> RadrootsStagedBlobReference +6:symbol 11:RadrootsKit 12:swift.method 144:s:11RadrootsKit0A23AppleBackgroundTransferC21withInactiveExecution3for9operationxAA0adE10IdentifierV_xAA0adE8SnapshotVSgYaYbKctYaKs8SendableRzlF 37:withInactiveExecution(for:operation:) 228:func withInactiveExecution<Result>(for identifier: RadrootsBackgroundTransferIdentifier, operation: @escaping @Sendable (RadrootsBackgroundTransferSnapshot?) async throws -> Result) async throws -> Result where Result : Sendable 6:symbol 11:RadrootsKit 12:swift.method 146:s:11RadrootsKit0A10FileAccessP012copyExternalC0_2to9mediaType17suggestedFilenameAA0A16ImportedDocumentV10Foundation3URLV_AA0aC9ReferenceVSSSgAOtKF 51:copyExternalFile(_:to:mediaType:suggestedFilename:) 173:@discardableResult func copyExternalFile(_ sourceURL: URL, to file: RadrootsFileReference, mediaType: String?, suggestedFilename: String?) throws -> RadrootsImportedDocument +6:symbol 11:RadrootsKit 12:swift.method 147:s:11RadrootsKit0A18BackgroundTransferP21withInactiveExecution3for9operationqd__AA0acD10IdentifierV_qd__AA0acD8SnapshotVSgYaYbKctYaKs8SendableRd__lF 37:withInactiveExecution(for:operation:) 228:func withInactiveExecution<Result>(for identifier: RadrootsBackgroundTransferIdentifier, operation: @escaping @Sendable (RadrootsBackgroundTransferSnapshot?) async throws -> Result) async throws -> Result where Result : Sendable +6:symbol 11:RadrootsKit 12:swift.method 150:s:11RadrootsKit0A18BackgroundTransferPAAE21withInactiveExecution3for9operationqd__AA0acD10IdentifierV_qd__AA0acD8SnapshotVSgYaYbKctYaKs8SendableRd__lF 37:withInactiveExecution(for:operation:) 221:func withInactiveExecution<Result>(for _: RadrootsBackgroundTransferIdentifier, operation _: @escaping @Sendable (RadrootsBackgroundTransferSnapshot?) async throws -> Result) async throws -> Result where Result : Sendable 6:symbol 11:RadrootsKit 12:swift.method 151:s:11RadrootsKit0A15AppleFileAccessC012copyExternalD0_2to9mediaType17suggestedFilenameAA0A16ImportedDocumentV10Foundation3URLV_AA0aD9ReferenceVSSSgAOtKF 51:copyExternalFile(_:to:mediaType:suggestedFilename:) 185:@discardableResult func copyExternalFile(_ sourceURL: URL, to file: RadrootsFileReference, mediaType: String? = nil, suggestedFilename: String? = nil) throws -> RadrootsImportedDocument 6:symbol 11:RadrootsKit 12:swift.method 152:s:11RadrootsKit0A15IdentityCustodyC014importPortableC0_10passphrase15replaceExistingAA0aC8SnapshotVAA0aC19PortabilityEnvelopeV_AA0aC10PassphraseVSbtYaKF 53:importPortableIdentity(_:passphrase:replaceExisting:) 207:@discardableResult func importPortableIdentity(_ envelope: RadrootsIdentityPortabilityEnvelope, passphrase: RadrootsIdentityPassphrase, replaceExisting: Bool = false) async throws -> RadrootsIdentitySnapshot 6:symbol 11:RadrootsKit 12:swift.method 154:s:7SwiftUI4ViewP11RadrootsKitE24radrootsDocumentExporter14preparedExport12onCompletionQrAA7BindingVyAD0d8PreparedjG0VSgG_ys6ResultOyAD0djgO0Vs5Error_pGctF 54:radrootsDocumentExporter(preparedExport:onCompletion:) 195:@MainActor func radrootsDocumentExporter(preparedExport: Binding<RadrootsPreparedExportDocument?>, onCompletion: @escaping (Result<RadrootsExportDocumentResult, any Error>) -> Void) -> some View\n @@ -3105,7 +3111,7 @@ radroots.apple-kit.public-api.v1 6:symbol 18:RadrootsKitTesting 12:swift.method 123:s:18RadrootsKitTesting0A31InMemoryBackgroundTransferStoreC23compareExchangeSnapshot8expected7desiredSb0aB00afgK0VSg_AItYaKF 42:compareExchangeSnapshot(expected:desired:) 141:func compareExchangeSnapshot(expected: RadrootsBackgroundTransferSnapshot?, desired: RadrootsBackgroundTransferSnapshot) async throws -> Bool 6:symbol 18:RadrootsKitTesting 12:swift.method 124:s:18RadrootsKitTesting0A22FakeBackgroundTransferC015handleEventsForE10URLSession10identifier17completionHandlerySS_yyYbctYaF 66:handleEventsForBackgroundURLSession(identifier:completionHandler:) 117:func handleEventsForBackgroundURLSession(identifier: String, completionHandler: @escaping @Sendable () -> Void) async 6:symbol 18:RadrootsKitTesting 12:swift.method 128:s:18RadrootsKitTesting0A32InMemoryPermissionStatusProviderC03setG0_3for10observedAty0aB00afG0O_AG0aF4KindO10Foundation4DateVSgtF 28:setStatus(_:for:observedAt:) 109:func setStatus(_ status: RadrootsPermissionStatus, for kind: RadrootsPermissionKind, observedAt: Date? = nil) -6:symbol 18:RadrootsKitTesting 12:swift.method 137:s:18RadrootsKitTesting0A31InMemoryBackgroundTransferStoreC13withAdmission3for9operation0aB00afG6HandleVAG0afG10IdentifierV_AIyYaYbKctYaKF 29:withAdmission(for:operation:) 205:func withAdmission(for identifier: RadrootsBackgroundTransferIdentifier, operation: @escaping @Sendable () async throws -> RadrootsBackgroundTransferHandle) async throws -> RadrootsBackgroundTransferHandle +6:symbol 18:RadrootsKitTesting 12:swift.method 136:s:18RadrootsKitTesting0A31InMemoryBackgroundTransferStoreC13withAdmission3for9operationx0aB00afG10IdentifierV_xyYaYbKctYaKs8SendableRzlF 29:withAdmission(for:operation:) 185:func withAdmission<Result>(for identifier: RadrootsBackgroundTransferIdentifier, operation: @escaping @Sendable () async throws -> Result) async throws -> Result where Result : Sendable 6:symbol 18:RadrootsKitTesting 12:swift.method 54:s:18RadrootsKitTesting0A18RecordingTelemetryC5resetyyF 7:reset() 12:func reset() 6:symbol 18:RadrootsKitTesting 12:swift.method 67:s:18RadrootsKitTesting0A16FakeUserPresenceC9setStatusyy0aB00aefH0VF 13:setStatus(_:) 52:func setStatus(_ status: RadrootsUserPresenceStatus) 6:symbol 18:RadrootsKitTesting 12:swift.method 68:s:18RadrootsKitTesting0A15FakeMediaPickerC10setSupportyy0aB00aefH0VF 14:setSupport(_:) 54:func setSupport(_ support: RadrootsMediaPickerSupport)