commit d8f92ee33c21c104398c0d853f8826f2c613eeeb parent 6b88be1850529477e6192e10bd0666444d74bb1c Author: triesap <tyson@radroots.org> Date: Tue, 15 Sep 2026 01:31:24 +0000 transfer: enforce native destination authority - Expose supported native policy and preserve legacy background recovery - Reject unsafe request URLs and mismatched response authority - Prove redirect refusal through isolated simulator foreground uploads - Verify exact API and complete native suites while retaining receipt findings Diffstat:
11 files changed, 367 insertions(+), 31 deletions(-)
diff --git a/Sources/RadrootsKit/RadrootsAppleBackgroundTransferAdapters.swift b/Sources/RadrootsKit/RadrootsAppleBackgroundTransferAdapters.swift @@ -1,6 +1,17 @@ import Foundation public struct RadrootsAppleBackgroundTransferAdapters: Sendable { + /// System-managed background sessions cannot establish the required + /// redirect/connect guarantee. Keep their legacy recovery operational, + /// and route new public work through the host's shared foreground uploader. + public static func supportsNewEnqueue(for policy: RadrootsBackgroundTransferNetworkPolicy) -> Bool { + #if os(iOS) && targetEnvironment(simulator) + policy == .simulatorLoopbackHTTP + #else + false + #endif + } + public let now: @Sendable () -> Date public let enqueue: @Sendable (RadrootsBackgroundTransferRequest, UUID) async throws -> Void public let cancel: @Sendable (RadrootsBackgroundTransferIdentifier) async throws -> Void @@ -58,13 +69,14 @@ public struct RadrootsAppleBackgroundTransferAdapters: Sendable { return Self( now: now, enqueue: { request, executionID in + guard Self.supportsNewEnqueue(for: request.networkPolicy) else { + throw RadrootsBackgroundTransferError.unavailable + } #if targetEnvironment(simulator) - if request.networkPolicy == .simulatorLoopbackHTTP { - try await simulatorSession.enqueue(request, executionID: executionID) - return - } + try await simulatorSession.enqueue(request, executionID: executionID) + #else + throw RadrootsBackgroundTransferError.unavailable #endif - try await session.enqueue(request, executionID: executionID) }, cancel: { identifier in #if targetEnvironment(simulator) diff --git a/Sources/RadrootsKit/RadrootsAppleBackgroundTransferCoordinator.swift b/Sources/RadrootsKit/RadrootsAppleBackgroundTransferCoordinator.swift @@ -84,6 +84,9 @@ actor RadrootsTransferCoordinator { private static func completionFailure( request: RadrootsBackgroundTransferRequest, completion: RadrootsTransferCompletion ) -> RadrootsBackgroundTransferFailure? { + if completion.httpResult.destinationMismatch { + return .responseInvalid + } if UInt64(max(completion.bytesTransferred, 0)) > request.maximumTransferBytes || completion.totalBytesExpected.map({ UInt64(max($0, 0)) > request.maximumTransferBytes }) == true { return .transferTooLarge diff --git a/Sources/RadrootsKit/RadrootsAppleBackgroundURLSession.swift b/Sources/RadrootsKit/RadrootsAppleBackgroundURLSession.swift @@ -34,6 +34,10 @@ import Foundation } func enqueue(_ request: RadrootsBackgroundTransferRequest, executionID: UUID) async throws { + guard usesForegroundSession, + RadrootsAppleBackgroundTransferAdapters.supportsNewEnqueue(for: request.networkPolicy) + else { throw RadrootsBackgroundTransferError.unavailable } + try RadrootsNativeDestinationPolicy.validate(request.remoteURL, policy: request.networkPolicy) guard admissions.count < 256, admissions.insert(request.identifier).inserted else { throw RadrootsBackgroundTransferError.invalidRequest } diff --git a/Sources/RadrootsKit/RadrootsAppleBackgroundURLSessionDelegate.swift b/Sources/RadrootsKit/RadrootsAppleBackgroundURLSessionDelegate.swift @@ -34,6 +34,10 @@ import Foundation let result: RadrootsStagedBackgroundDownloadResult do { guard + RadrootsNativeDestinationPolicy.responseMatches( + downloadTask.response?.url, original: downloadTask.originalRequest, + current: downloadTask.currentRequest + ), let descriptor = RadrootsBackgroundURLTaskDescriptor( taskDescription: downloadTask.taskDescription ), @@ -94,6 +98,15 @@ import Foundation } func urlSession( + _: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, + completionHandler: @escaping (URLSession.ResponseDisposition) -> Void + ) { + completionHandler(RadrootsNativeDestinationPolicy.responseMatches( + response.url, original: dataTask.originalRequest, current: dataTask.currentRequest + ) ? .allow : .cancel) + } + + func urlSession( _: URLSession, task: URLSessionTask, didSendBodyData _: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64 ) { @@ -221,7 +234,10 @@ import Foundation return RadrootsBackgroundHTTPResult( statusCode: response.statusCode, mediaType: mediaType, body: body, contentEncoding: contentEncoding, bodyExceeded: exceeded, - mediaTypeWasMalformed: rawMediaType != nil && mediaType == nil + mediaTypeWasMalformed: rawMediaType != nil && mediaType == nil, + destinationMismatch: !RadrootsNativeDestinationPolicy.responseMatches( + response.url, original: task.originalRequest, current: task.currentRequest + ) ) } diff --git a/Sources/RadrootsKit/RadrootsBackgroundTransferValidation.swift b/Sources/RadrootsKit/RadrootsBackgroundTransferValidation.swift @@ -56,30 +56,7 @@ public enum RadrootsBackgroundTransferValidation { private static func validate( remoteURL: URL, networkPolicy: RadrootsBackgroundTransferNetworkPolicy ) throws { - guard let components = URLComponents(url: remoteURL, resolvingAgainstBaseURL: false), - components.host?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false, - components.user == nil, - components.password == nil, components.query == nil, components.fragment == nil - else { - throw RadrootsBackgroundTransferError.invalidRequest - } - let scheme = components.scheme?.lowercased() - switch networkPolicy { - case .publicHTTPS: - guard scheme == "https" else { - throw RadrootsBackgroundTransferError.invalidRequest - } - case .simulatorLoopbackHTTP: - #if os(iOS) && !targetEnvironment(simulator) - throw RadrootsBackgroundTransferError.invalidRequest - #else - guard scheme == "http", let host = components.host?.lowercased(), - host == "localhost" || host == "127.0.0.1" || host == "::1" - else { - throw RadrootsBackgroundTransferError.invalidRequest - } - #endif - } + try RadrootsNativeDestinationPolicy.validate(remoteURL, policy: networkPolicy) } private static func validate( diff --git a/Sources/RadrootsKit/RadrootsBackgroundURLTaskDescriptor.swift b/Sources/RadrootsKit/RadrootsBackgroundURLTaskDescriptor.swift @@ -12,11 +12,12 @@ struct RadrootsBackgroundHTTPResult: Sendable, Equatable { let contentEncoding: String? let bodyExceeded: Bool let mediaTypeWasMalformed: Bool + let destinationMismatch: Bool init( statusCode: Int?, mediaType: String?, body: Data?, contentEncoding: String? = nil, bodyExceeded: Bool, - mediaTypeWasMalformed: Bool = false + mediaTypeWasMalformed: Bool = false, destinationMismatch: Bool = false ) { self.statusCode = statusCode self.mediaType = mediaType @@ -24,6 +25,7 @@ struct RadrootsBackgroundHTTPResult: Sendable, Equatable { self.contentEncoding = contentEncoding self.bodyExceeded = bodyExceeded self.mediaTypeWasMalformed = mediaTypeWasMalformed + self.destinationMismatch = destinationMismatch } } diff --git a/Sources/RadrootsKit/RadrootsNativeDestinationPolicy.swift b/Sources/RadrootsKit/RadrootsNativeDestinationPolicy.swift @@ -0,0 +1,67 @@ +import Foundation + +/// URL authority checks are not DNS/IP-at-connect enforcement. New native +/// transfers are limited to the explicitly isolated simulator foreground path; +/// hosts requiring public endpoint guarantees use their shared foreground I/O. +enum RadrootsNativeDestinationPolicy { + static func validate(_ url: URL, policy: RadrootsBackgroundTransferNetworkPolicy) throws { + guard url.absoluteString.utf8.count <= 4096, + let value = URLComponents(url: url, resolvingAgainstBaseURL: false), + let host = value.host, !host.isEmpty, value.user == nil, value.password == nil, + value.query == nil, value.fragment == nil, + value.port.map({ (1 ... 65535).contains($0) }) ?? true, + !url.absoluteString.contains("\\") + else { throw RadrootsBackgroundTransferError.invalidRequest } + let path = value.percentEncodedPath + guard path.isEmpty || path.hasPrefix("/"), + path.split(separator: "/", omittingEmptySubsequences: false).allSatisfy(safeComponent) + else { throw RadrootsBackgroundTransferError.invalidRequest } + switch policy { + case .publicHTTPS: + guard value.scheme?.lowercased() == "https" else { throw RadrootsBackgroundTransferError.invalidRequest } + case .simulatorLoopbackHTTP: + #if os(iOS) && !targetEnvironment(simulator) + throw RadrootsBackgroundTransferError.invalidRequest + #else + guard value.scheme?.lowercased() == "http", + ["127.0.0.1", "localhost", "::1"].contains(normalizedHost(host)) + else { throw RadrootsBackgroundTransferError.invalidRequest } + #endif + } + } + + static func responseMatches(_ response: URL?, original: URLRequest?, current: URLRequest?) -> Bool { + guard let original, let current, let requested = original.url, let response, + let requestedParts = URLComponents(url: requested, resolvingAgainstBaseURL: false), + let responseParts = URLComponents(url: response, resolvingAgainstBaseURL: false), + current.httpMethod == original.httpMethod, current.url == requested + else { return false } + let policy: RadrootsBackgroundTransferNetworkPolicy = requestedParts.scheme?.lowercased() == "https" + ? .publicHTTPS : .simulatorLoopbackHTTP + guard (try? validate(requested, policy: policy)) != nil, + (try? validate(response, policy: policy)) != nil + else { return false } + return requestedParts.scheme?.lowercased() == responseParts.scheme?.lowercased() + && normalizedHost(requestedParts.host ?? "") == normalizedHost(responseParts.host ?? "") + && effectivePort(requestedParts) == effectivePort(responseParts) + && normalizedPath(requestedParts) == normalizedPath(responseParts) + } + + private static func safeComponent(_ component: Substring) -> Bool { + guard let decoded = String(component).removingPercentEncoding else { return false } + return decoded != "." && decoded != ".." && !decoded.contains("/") && !decoded.contains("\\") + && decoded.unicodeScalars.allSatisfy { !CharacterSet.controlCharacters.contains($0) } + } + + private static func normalizedHost(_ host: String) -> String { + host.lowercased().trimmingCharacters(in: CharacterSet(charactersIn: "[]")) + } + + private static func effectivePort(_ parts: URLComponents) -> Int { + parts.port ?? (parts.scheme?.lowercased() == "https" ? 443 : 80) + } + + private static func normalizedPath(_ parts: URLComponents) -> String { + parts.percentEncodedPath.isEmpty ? "/" : parts.percentEncodedPath + } +} diff --git a/Tests/RadrootsKitTests/RadrootsNativeDestinationCallbackTests.swift b/Tests/RadrootsKitTests/RadrootsNativeDestinationCallbackTests.swift @@ -0,0 +1,105 @@ +#if os(iOS) && targetEnvironment(simulator) + import Foundation + @testable import RadrootsKit + import RadrootsKitTesting + import Testing + + @Test func nativeDestinationRejectsNewPublicEnqueueBeforeTaskCreation() async throws { + let roots = try appleTransferRoots() + defer { try? FileManager.default.removeItem(at: roots.dataRoot.deletingLastPathComponent()) } + let store = RadrootsInMemoryBackgroundTransferStore() + let adapters = try RadrootsAppleBackgroundTransferAdapters.live( + sessionIdentifier: "org.radroots.public-refusal.\(UUID().uuidString.lowercased())", store: store, + fileResolver: RadrootsAppleBackgroundTransferFileResolver(roots: roots), + downloadStagingRoot: roots.temporaryRoot + ) + let request = try RadrootsBackgroundTransferRequest( + identifier: RadrootsBackgroundTransferIdentifier("public.refused"), + remoteURL: #require(URL(string: "https://example.org/upload")), method: .put, + operation: .upload(source: .file(RadrootsFileReference(scope: .cache, relativePath: "absent"))) + ) + await #expect(throws: RadrootsBackgroundTransferError.unavailable) { + try await adapters.enqueue(request, UUID()) + } + #expect(try await adapters.activeTransferIdentifiers().isEmpty) + #expect(try await store.loadSnapshots().isEmpty) + } + + @Test(arguments: ["same_host", "cross_host", "cross_port"]) + func nativeDestinationRefusesActualForegroundRedirect(_ kind: String) async throws { + let server = try NativeRedirectServer() + let target = try NativeRedirectServer() + defer { server.stop(); target.stop() } + let port = try await server.start() + let targetPort = try await target.start() + let destinations = ["same_host": "http://127.0.0.1:\(port)/other", + "cross_host": "http://localhost:\(targetPort)/other", + "cross_port": "http://127.0.0.1:\(targetPort)/other"] + try server.redirect(to: #require(destinations[kind])) + let roots = try appleTransferRoots() + defer { try? FileManager.default.removeItem(at: roots.dataRoot.deletingLastPathComponent()) } + let source = RadrootsFileReference(scope: .cache, relativePath: "redirect_body") + try RadrootsAppleFileAccess(roots: roots).write(.inline(Data("isolated fixture body".utf8)), to: source) + let store = RadrootsAppleBackgroundTransferStore(roots: roots) + let adapters = try RadrootsAppleBackgroundTransferAdapters.live( + sessionIdentifier: "org.radroots.redirect.\(UUID().uuidString.lowercased())", store: store, + fileResolver: RadrootsAppleBackgroundTransferFileResolver(roots: roots), + downloadStagingRoot: roots.temporaryRoot + ) + let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: adapters) + let request = try RadrootsBackgroundTransferRequest( + identifier: RadrootsBackgroundTransferIdentifier("redirect.\(kind)"), + remoteURL: #require(URL(string: "http://127.0.0.1:\(port)/upload")), method: .put, + operation: .upload(source: .file(source)), networkPolicy: .simulatorLoopbackHTTP + ) + _ = try await transfer.enqueue(request) + let deadline = Date().addingTimeInterval(15) + var snapshot = try await store.loadSnapshots().first + while snapshot?.state == .queued || snapshot?.state == .running { + guard Date() < deadline else { throw RadrootsBackgroundTransferError.transferFailure } + try await Task.sleep(for: .milliseconds(25)) + snapshot = try await store.loadSnapshots().first + } + #expect(snapshot?.state == .failed) + #expect(snapshot?.failure == .httpStatus) + #expect(server.requestCount == 1) + #expect(target.requestCount == 0) + } + + @Test func nativeDestinationDelegateRejectsDowngradeAndResponseMismatch() throws { + let roots = try appleTransferRoots() + defer { try? FileManager.default.removeItem(at: roots.dataRoot.deletingLastPathComponent()) } + let coordinator = RadrootsTransferCoordinator(sessionIdentifier: "callback.tests", + store: RadrootsInMemoryBackgroundTransferStore(), + fileResolver: RadrootsAppleBackgroundTransferFileResolver( + roots: roots + )) + let delegate = RadrootsTransferSessionDelegate(coordinator: coordinator, + downloadStagingRoot: roots.temporaryRoot, fileManager: .default) + let session = URLSession(configuration: .ephemeral) + defer { session.invalidateAndCancel() } + let url = try #require(URL(string: "https://example.org/upload")) + let task = session.dataTask(with: url) + let redirect = try #require(HTTPURLResponse(url: url, statusCode: 307, httpVersion: nil, headerFields: nil)) + var called = false + let downgrade = try #require(URL(string: "http://example.org/upload")) + delegate.urlSession(session, task: task, willPerformHTTPRedirection: redirect, + newRequest: URLRequest(url: downgrade)) { + called = true + #expect($0 == nil) + } + #expect(called) + for raw in ["https://other.org/upload", "https://example.org/other", "http://example.org/upload"] { + let responseURL = try #require(URL(string: raw)) + let response = try #require(HTTPURLResponse(url: responseURL, statusCode: 200, + httpVersion: nil, headerFields: nil)) + var received = false + delegate.urlSession(session, dataTask: task, didReceive: response) { + received = true + #expect($0 == .cancel) + } + #expect(received) + } + } + +#endif diff --git a/Tests/RadrootsKitTests/RadrootsNativeDestinationTests.swift b/Tests/RadrootsKitTests/RadrootsNativeDestinationTests.swift @@ -0,0 +1,65 @@ +import Foundation +@testable import RadrootsKit +import Testing + +@Test(arguments: [ + "https://user@example.org/upload", "https://user:secret@example.org/upload", + "https://example.org/upload?token=secret", "https://example.org/upload#fragment", + "https://example.org:0/upload", "https://example.org:65536/upload", + "http://example.org/upload", "https://example.org/a/../upload", + "https://example.org/%2e/upload", "https://example.org/%2Fupload", + "https://example.org/%5cupload", "https://example.org/%00upload" +]) func nativeDestinationRejectsAmbiguousAuthority(_ raw: String) throws { + let url = try #require(URL(string: raw)) + #expect(throws: RadrootsBackgroundTransferError.invalidRequest) { + try RadrootsNativeDestinationPolicy.validate(url, policy: .publicHTTPS) + } +} + +@Test(arguments: ["127.0.0.2", "127.1", "[::ffff:127.0.0.1]", "localhost.example.org", "example.org"]) +func nativeDestinationRejectsFixtureEscape(_ host: String) throws { + let url = try #require(URL(string: "http://\(host):8080/upload")) + #expect(throws: RadrootsBackgroundTransferError.invalidRequest) { + try RadrootsNativeDestinationPolicy.validate(url, policy: .simulatorLoopbackHTTP) + } +} + +@Test func nativeDestinationAcceptsExplicitAuthorityAndReportsPlatformCapability() throws { + try RadrootsNativeDestinationPolicy.validate(#require(URL(string: "https://example.org:443/upload")), + policy: .publicHTTPS) + #expect(!RadrootsAppleBackgroundTransferAdapters.supportsNewEnqueue(for: .publicHTTPS)) + #if os(iOS) && targetEnvironment(simulator) + #expect(RadrootsAppleBackgroundTransferAdapters.supportsNewEnqueue(for: .simulatorLoopbackHTTP)) + #else + #expect(!RadrootsAppleBackgroundTransferAdapters.supportsNewEnqueue(for: .simulatorLoopbackHTTP)) + #endif + #if !os(iOS) || targetEnvironment(simulator) + for host in ["127.0.0.1", "localhost", "[::1]"] { + try RadrootsNativeDestinationPolicy.validate(#require(URL(string: "http://\(host):8080/upload")), + policy: .simulatorLoopbackHTTP) + } + #endif +} + +@Test func nativeDestinationRequiresOriginalMethodOriginAndPath() throws { + var original = try URLRequest(url: #require(URL(string: "https://example.org/upload"))) + original.httpMethod = "PUT" + #expect(RadrootsNativeDestinationPolicy.responseMatches( + URL(string: "https://EXAMPLE.org:443/upload"), original: original, current: original + )) + for raw in ["http://example.org/upload", "https://other.org/upload", "https://example.org:444/upload", + "https://example.org/other", "https://example.org/upload?token=secret"] { + #expect(!RadrootsNativeDestinationPolicy.responseMatches( + URL(string: raw), + original: original, + current: original + )) + } + var redirected = original + redirected.httpMethod = "GET" + #expect(!RadrootsNativeDestinationPolicy.responseMatches(original.url, original: original, current: redirected)) + redirected = original + redirected.url = URL(string: "https://example.org/other") + #expect(!RadrootsNativeDestinationPolicy.responseMatches(original.url, original: original, current: redirected)) + #expect(!RadrootsNativeDestinationPolicy.responseMatches(nil, original: original, current: original)) +} diff --git a/Tests/RadrootsKitTests/RadrootsNativeRedirectServer.swift b/Tests/RadrootsKitTests/RadrootsNativeRedirectServer.swift @@ -0,0 +1,83 @@ +#if os(iOS) && targetEnvironment(simulator) + import Foundation + import Network + @testable import RadrootsKit + + /// Isolated loopback fixture. No external DNS, TLS trust changes, or credentials. + final class NativeRedirectServer: @unchecked Sendable { + private let listener: NWListener + private let queue = DispatchQueue(label: "org.radroots.tests.redirect-http") + private let lock = NSLock() + private var connections: [NWConnection] = [] + private var received = 0 + private var location: String? + + init() throws { + let parameters = NWParameters.tcp + parameters.requiredLocalEndpoint = .hostPort(host: "127.0.0.1", port: .any) + listener = try NWListener(using: parameters) + } + + var requestCount: Int { + lock.withLock { received } + } + + func redirect(to location: String) { + lock.withLock { self.location = location } + } + + func start() async throws -> UInt16 { + listener.newConnectionHandler = { [weak self] connection in + guard let self else { connection.cancel(); return } + connections.append(connection) + connection.start(queue: queue) + read(connection, accumulated: Data()) + } + listener.start(queue: queue) + for _ in 0 ..< 500 { + if let port = listener.port, port.rawValue > 0 { + return port.rawValue + } + try await Task.sleep(for: .milliseconds(10)) + } + throw RadrootsBackgroundTransferError.unavailable + } + + func stop() { + listener.cancel() + queue.async { for connection in self.connections { + connection.cancel() + } } + } + + private func read(_ connection: NWConnection, accumulated: Data) { + connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { data, _, complete, error in + var bytes = accumulated + if let data { + bytes.append(data) + } + guard error == nil, bytes.count <= 131_072 else { connection.cancel(); return } + if self.hasBody(bytes) { + let destination = self.lock.withLock { self.received += 1; return self.location } + let status = destination.map { "307 Temporary Redirect\r\nLocation: \($0)" } ?? "200 OK" + let response = Data("HTTP/1.1 \(status)\r\nContent-Length: 0\r\nConnection: close\r\n\r\n".utf8) + connection.send(content: response, completion: .contentProcessed { _ in connection.cancel() }) + } else if complete { + connection.cancel() + } else { + self.read(connection, accumulated: bytes) + } + } + } + + private func hasBody(_ bytes: Data) -> Bool { + guard let separator = bytes.range(of: Data("\r\n\r\n".utf8)), + let header = String(data: bytes[..<separator.lowerBound], encoding: .utf8) + else { return false } + let length = header.components(separatedBy: "\r\n").first { + $0.lowercased().hasPrefix("content-length:") + }.flatMap { Int($0.dropFirst("content-length:".count).trimmingCharacters(in: .whitespaces)) } ?? 0 + return (0 ... 65536).contains(length) && bytes.count - separator.upperBound == length + } + } +#endif diff --git a/contracts/api_baselines/apple_kit.txt b/contracts/api_baselines/apple_kit.txt @@ -788,6 +788,7 @@ radroots.apple-kit.public-api.v1 12:relationship 11:RadrootsKit 8:memberOf 103:s:11RadrootsKit0A29ApplePermissionStatusAdaptersV010permissionE03forAA0adE0OSo015CLAuthorizationE0V_tFZ 49:s:11RadrootsKit0A29ApplePermissionStatusAdaptersV 0: 12:relationship 11:RadrootsKit 8:memberOf 103:s:11RadrootsKit0A29ApplePermissionStatusAdaptersV010permissionE03forAA0adE0OSo015PHAuthorizationE0V_tFZ 49:s:11RadrootsKit0A29ApplePermissionStatusAdaptersV 0: 12:relationship 11:RadrootsKit 8:memberOf 103:s:11RadrootsKit0A29ApplePermissionStatusAdaptersV010permissionE03forAA0adE0OSo015UNAuthorizationE0V_tFZ 49:s:11RadrootsKit0A29ApplePermissionStatusAdaptersV 0: +12:relationship 11:RadrootsKit 8:memberOf 103:s:11RadrootsKit0A31AppleBackgroundTransferAdaptersV18supportsNewEnqueue3forSbAA0adE13NetworkPolicyO_tFZ 51:s:11RadrootsKit0A31AppleBackgroundTransferAdaptersV 0: 12:relationship 11:RadrootsKit 8:memberOf 104:s:11RadrootsKit0A23CaptureIntakeValidationO28normalizedScannerOutputKindsySayAA0a8DocumentgH4KindOGAGKFZ 43:s:11RadrootsKit0A23CaptureIntakeValidationO 0: 12:relationship 11:RadrootsKit 8:memberOf 104:s:11RadrootsKit0A26BackgroundTransferProgressV16bytesTransferred18totalBytesExpectedACs5Int64V_AGSgtKcfc 46:s:11RadrootsKit0A26BackgroundTransferProgressV 0: 12:relationship 11:RadrootsKit 8:memberOf 104:s:11RadrootsKit0A31AppleBackgroundTransferAdaptersV7enqueueyyAA0adE7RequestV_10Foundation4UUIDVtYaYbKcvp 51:s:11RadrootsKit0A31AppleBackgroundTransferAdaptersV 0: @@ -2875,6 +2876,7 @@ radroots.apple-kit.public-api.v1 6:symbol 11:RadrootsKit 17:swift.type.method 103:s:11RadrootsKit0A29ApplePermissionStatusAdaptersV010permissionE03forAA0adE0OSo015CLAuthorizationE0V_tFZ 22:permissionStatus(for:) 104:static func permissionStatus(for authorizationStatus: CLAuthorizationStatus) -> RadrootsPermissionStatus 6:symbol 11:RadrootsKit 17:swift.type.method 103:s:11RadrootsKit0A29ApplePermissionStatusAdaptersV010permissionE03forAA0adE0OSo015PHAuthorizationE0V_tFZ 22:permissionStatus(for:) 104:static func permissionStatus(for authorizationStatus: PHAuthorizationStatus) -> RadrootsPermissionStatus 6:symbol 11:RadrootsKit 17:swift.type.method 103:s:11RadrootsKit0A29ApplePermissionStatusAdaptersV010permissionE03forAA0adE0OSo015UNAuthorizationE0V_tFZ 22:permissionStatus(for:) 104:static func permissionStatus(for authorizationStatus: UNAuthorizationStatus) -> RadrootsPermissionStatus +6:symbol 11:RadrootsKit 17:swift.type.method 103:s:11RadrootsKit0A31AppleBackgroundTransferAdaptersV18supportsNewEnqueue3forSbAA0adE13NetworkPolicyO_tFZ 24:supportsNewEnqueue(for:) 91:static func supportsNewEnqueue(for policy: RadrootsBackgroundTransferNetworkPolicy) -> Bool 6:symbol 11:RadrootsKit 17:swift.type.method 104:s:11RadrootsKit0A23CaptureIntakeValidationO28normalizedScannerOutputKindsySayAA0a8DocumentgH4KindOGAGKFZ 32:normalizedScannerOutputKinds(_:) 138:static func normalizedScannerOutputKinds(_ outputKinds: [RadrootsDocumentScannerOutputKind]) throws -> [RadrootsDocumentScannerOutputKind] 6:symbol 11:RadrootsKit 17:swift.type.method 106:s:11RadrootsKit0A27DocumentPresentationAdapterO12transferItem3forAA0a13ShareTransferG0VAA0aI7RequestV_tKFZ 18:transferItem(for:) 95:static func transferItem(for request: RadrootsShareRequest) throws -> RadrootsShareTransferItem 6:symbol 11:RadrootsKit 17:swift.type.method 108:s:11RadrootsKit0A25ExternalActionDestinationV9appleMaps10coordinate5labelAcA0A18LocationCoordinateV_SSSgtKFZ 28:appleMaps(coordinate:label:) 127:static func appleMaps(coordinate: RadrootsLocationCoordinate, label: String? = nil) throws -> RadrootsExternalActionDestination