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 368572e266628319a2697b35efd85ee449ab1704
parent fe3afbd679dbdcaae6baba2a95b256238fbb3f82
Author: triesap <tyson@radroots.org>
Date:   Tue,  1 Sep 2026 00:56:06 +0000

apple-kit: govern file-backed consumers

- Route file, media, transfer, and artifact reads through the sealed descriptor boundary.
- Require explicit byte limits and retain verified artifact bytes instead of path authority.
- Cover symlink, FIFO, replacement, oversize, and persistence failure vectors.
- Refresh the exact public API inventory after the intentional breaking migration.

Diffstat:
MSources/RadrootsKit/RadrootsAppleBackgroundTransfer.swift | 23+++++------------------
MSources/RadrootsKit/RadrootsAppleMediaPreparation.swift | 28+++++++++++++++++++---------
MSources/RadrootsKit/RadrootsAppleMobileStore.swift | 2+-
MSources/RadrootsKit/RadrootsBackgroundTransfer.swift | 53+++++++++++++++++++++++++++++++++++++++++++++++++----
MSources/RadrootsKit/RadrootsFileAccess.swift | 124++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
MSources/RadrootsKit/RadrootsVerifiedArtifactAccess.swift | 69+++++++++++++++++++--------------------------------------------------
MTests/RadrootsKitTests/RadrootsAppleBackgroundTransferTests.swift | 10+++++++++-
MTests/RadrootsKitTests/RadrootsAppleFileAccessTests.swift | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
MTests/RadrootsKitTests/RadrootsAppleMediaPreparationTests.swift | 7++++++-
MTests/RadrootsKitTests/RadrootsBackgroundTransferTests.swift | 41++++++++++++++++++++++++++++++++++++++++-
MTests/RadrootsKitTests/RadrootsDocumentPresentationTests.swift | 7++++++-
MTests/RadrootsKitTests/RadrootsVerifiedArtifactAccessTests.swift | 14++++++++++----
Mcontracts/api_baselines/apple_kit.txt | 12++++++++----
13 files changed, 338 insertions(+), 132 deletions(-)

diff --git a/Sources/RadrootsKit/RadrootsAppleBackgroundTransfer.swift b/Sources/RadrootsKit/RadrootsAppleBackgroundTransfer.swift @@ -783,25 +783,12 @@ struct RadrootsBackgroundURLTaskDescriptor: Sendable, Equatable { case .download: task = session.downloadTask(with: urlRequest) case .upload(let source): let sourceURL = try fileResolver.resolve(source) - let values = try sourceURL.resourceValues(forKeys: [ - .fileSizeKey, .isRegularFileKey, .isSymbolicLinkKey, - ]) - guard values.isRegularFile == true, values.isSymbolicLink != true else { - throw RadrootsBackgroundTransferError.invalidRequest( - "background upload source must be a regular file") - } - guard let fileSize = values.fileSize, fileSize >= 0, - UInt64(fileSize) <= request.maximumTransferBytes - else { - throw RadrootsBackgroundTransferError.invalidRequest( - "background upload source exceeds its byte limit") - } - if case .stagedBlob(let blob) = source, values.fileSize != blob.sizeBytes { - throw RadrootsBackgroundTransferError.invalidRequest( - "background upload source size does not match its handle") - } + let sourceData = try fileResolver.read( + source, + maximumBytes: Int(request.maximumTransferBytes) + ) if let expectedDigest = request.expectedSourceSHA256, - try RadrootsAppleFileDigest.sha256(at: sourceURL) != expectedDigest + RadrootsAppleFileDigest.sha256(sourceData) != expectedDigest { throw RadrootsBackgroundTransferError.invalidRequest( "background upload source does not match its digest") diff --git a/Sources/RadrootsKit/RadrootsAppleMediaPreparation.swift b/Sources/RadrootsKit/RadrootsAppleMediaPreparation.swift @@ -87,12 +87,16 @@ public actor RadrootsAppleMediaPreparer { private func prepareValidatedImage(_ request: RadrootsAppleImagePreparationRequest) async throws -> RadrootsApplePreparedImage { try Task.checkCancellation() try requireProtectedData() - let sourceURL = try resolver.resolve(request.source) - let sourceValues = try sourceURL.resourceValues(forKeys: [.fileSizeKey, .isRegularFileKey, .isSymbolicLinkKey]) - guard sourceValues.isRegularFile == true, sourceValues.isSymbolicLink != true, let inputBytes = sourceValues.fileSize, - inputBytes > 0, inputBytes <= request.maximumInputBytes - else { throw RadrootsAppleMediaPreparationError.invalidRequest("image source is unavailable or exceeds its byte limit") } - guard let source = CGImageSourceCreateWithURL(sourceURL as CFURL, [kCGImageSourceShouldCache: false] as CFDictionary), + let sourceData: Data + do { + sourceData = try resolver.read(request.source, maximumBytes: request.maximumInputBytes) + } catch { + throw RadrootsAppleMediaPreparationError.invalidRequest("image source is unavailable or exceeds its byte limit") + } + guard !sourceData.isEmpty else { + throw RadrootsAppleMediaPreparationError.invalidRequest("image source is unavailable or exceeds its byte limit") + } + guard let source = CGImageSourceCreateWithData(sourceData as CFData, [kCGImageSourceShouldCache: false] as CFDictionary), CGImageSourceGetCount(source) == 1 else { throw RadrootsAppleMediaPreparationError.invalidRequest("image source must contain exactly one decodable image") } let dimensions = try Self.sourceDimensions(source) @@ -166,9 +170,11 @@ public actor RadrootsAppleMediaPreparer { identifier: RadrootsBackgroundTransferIdentifier = .generated() ) throws -> RadrootsBackgroundTransferRequest { do { - let fileURL = try roots.stagedBlobURL(for: preparedImage.file) - guard try Self.fileSize(at: fileURL) == preparedImage.file.sizeBytes, - try RadrootsAppleFileDigest.sha256(at: fileURL) == preparedImage.sha256 + let preparedData = try resolver.read( + .stagedBlob(preparedImage.file), maximumBytes: preparedImage.file.sizeBytes + ) + guard preparedData.count == preparedImage.file.sizeBytes, + RadrootsAppleFileDigest.sha256(preparedData) == preparedImage.sha256 else { throw RadrootsAppleMediaPreparationError.invalidRequest("prepared image no longer matches its commitment") } return try RadrootsBackgroundTransferRequest( identifier: identifier, remoteURL: remoteURL, method: .put, operation: .upload(source: .stagedBlob(preparedImage.file)), @@ -204,6 +210,10 @@ public actor RadrootsAppleMediaPreparer { } enum RadrootsAppleFileDigest { + static func sha256(_ data: Data) -> String { + CryptoKit.SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined() + } + static func sha256(at url: URL) throws -> String { let handle = try FileHandle(forReadingFrom: url) defer { try? handle.close() } diff --git a/Sources/RadrootsKit/RadrootsAppleMobileStore.swift b/Sources/RadrootsKit/RadrootsAppleMobileStore.swift @@ -52,7 +52,7 @@ public enum RadrootsAppleMobileStore { throw RadrootsAppleMobileStoreError.invalidPublicKey } - let applicationSupportDirectory = roots.dataRoot.standardizedFileURL + let applicationSupportDirectory = roots.dataRoot let productRoot = applicationSupportDirectory .appendingPathComponent(productDirectory, isDirectory: true) diff --git a/Sources/RadrootsKit/RadrootsBackgroundTransfer.swift b/Sources/RadrootsKit/RadrootsBackgroundTransfer.swift @@ -567,6 +567,7 @@ public protocol RadrootsBackgroundTransfer: Sendable { public protocol RadrootsBackgroundTransferFileResolver: Sendable { func resolve(_ file: RadrootsBackgroundTransferLocalFile) throws -> URL + func read(_ file: RadrootsBackgroundTransferLocalFile, maximumBytes: Int) throws -> Data } public struct RadrootsAppleBackgroundTransferFileResolver: RadrootsBackgroundTransferFileResolver, @@ -597,9 +598,48 @@ public struct RadrootsAppleBackgroundTransferFileResolver: RadrootsBackgroundTra } return candidate } + + public func read(_ file: RadrootsBackgroundTransferLocalFile, maximumBytes: Int) throws -> Data { + guard maximumBytes >= 0 else { + throw RadrootsBackgroundTransferError.invalidRequest( + "background transfer local file byte limit is invalid") + } + let root: URL + let relativePath: String + let expectedBytes: Int? + switch file { + case .file(let reference): + root = roots.root(for: reference.scope) + relativePath = reference.relativePath + expectedBytes = nil + case .stagedBlob(let blob): + root = roots.stagedBlobsRoot + relativePath = blob.blobID + expectedBytes = blob.sizeBytes + } + do { + let data = try RadrootsGovernedFileReader.read( + root: root, + relativePath: relativePath, + maximumBytes: maximumBytes + ) + guard expectedBytes == nil || expectedBytes == data.count else { + throw RadrootsBackgroundTransferError.invalidRequest( + "background transfer local file size does not match its handle") + } + return data + } catch let error as RadrootsBackgroundTransferError { + throw error + } catch { + throw RadrootsBackgroundTransferError.invalidRequest( + "background transfer local file failed governed admission") + } + } } public actor RadrootsAppleBackgroundTransferStore: RadrootsBackgroundTransferStore { + private static let maximumPersistenceBytes = 1024 * 1024 + private struct Envelope: Codable { let schemaVersion: Int let snapshots: [RadrootsBackgroundTransferSnapshot] @@ -633,18 +673,19 @@ public actor RadrootsAppleBackgroundTransferStore: RadrootsBackgroundTransferSto do { let url = try storeURL() let legacyURL = try legacyStoreURL() - let sourceURL: URL let isLegacy: Bool if fileManager.fileExists(atPath: url.path) { - sourceURL = url isLegacy = false } else if fileManager.fileExists(atPath: legacyURL.path) { - sourceURL = legacyURL isLegacy = true } else { return [] } - let data = try Data(contentsOf: sourceURL) + let data = try RadrootsGovernedFileReader.read( + root: roots.root(for: isLegacy ? .cache : .data), + relativePath: "background_transfers/transfers.json", + maximumBytes: Self.maximumPersistenceBytes + ) let decoded: [RadrootsBackgroundTransferSnapshot] let usedLegacyEncoding: Bool if let envelope = try? decoder.decode(Envelope.self, from: data) { @@ -716,6 +757,10 @@ public actor RadrootsAppleBackgroundTransferStore: RadrootsBackgroundTransferSto try fileManager.createDirectory( at: url.deletingLastPathComponent(), withIntermediateDirectories: true) let data = try encoder.encode(Envelope(snapshots: snapshots)) + guard data.count <= Self.maximumPersistenceBytes else { + throw RadrootsBackgroundTransferError.persistenceFailure( + "background transfer persistence exceeds its byte limit") + } try data.write(to: url, options: [.atomic]) #if os(iOS) try fileManager.setAttributes( diff --git a/Sources/RadrootsKit/RadrootsFileAccess.swift b/Sources/RadrootsKit/RadrootsFileAccess.swift @@ -1,3 +1,4 @@ +import Darwin import Foundation public enum RadrootsFileScope: Sendable, Equatable, CaseIterable, Codable { @@ -108,7 +109,7 @@ public enum RadrootsFilePayload: Sendable, Equatable { } public enum RadrootsFileReadMode: Sendable, Equatable { - case inline + case inline(maxBytes: Int) case preferInline(maxBytes: Int) case stagedBlob } @@ -143,6 +144,8 @@ public protocol RadrootsFileAccess { } public final class RadrootsAppleFileAccess: RadrootsFileAccess { + private static let maximumGovernedFileBytes = 512 * 1024 * 1024 + public let roots: RadrootsAppleFileRoots private let fileManager: FileManager @@ -158,29 +161,26 @@ public final class RadrootsAppleFileAccess: RadrootsFileAccess { case let .inline(inlineData): try inlineData.write(to: url, options: [.atomic]) case let .stagedBlob(stagedBlob): - try copyReplacingItem(from: stagedBlobURL(for: stagedBlob), to: url) + try readStagedBlob(stagedBlob).write(to: url, options: [.atomic]) } } public func read(_ file: RadrootsFileReference, mode: RadrootsFileReadMode) throws -> RadrootsFileReadResult { - let url = try roots.resolvedURL(for: file) - guard fileManager.fileExists(atPath: url.path) else { - throw RadrootsAppleFileError.notFound("file not found") - } switch mode { - case .inline: - return try .inline(Data(contentsOf: url)) + case let .inline(maxBytes): + return try .inline(readGovernedFile(file, maximumBytes: maxBytes)) case let .preferInline(maxBytes): - guard maxBytes >= 0 else { - throw RadrootsAppleFileError.invalidRequest("inline byte limit cannot be negative") + do { + return try .inline( + readGovernedFile(file, maximumBytes: maxBytes, preserveTooLarge: true) + ) + } catch RadrootsGovernedFileReadError.tooLarge { + let url = try roots.resolvedURL(for: file) + let staged = try stageFile(file, mediaType: nil, filenameHint: url.lastPathComponent) + return .stagedBlob(staged) } - let size = try fileSize(at: url) - if size <= maxBytes { - return try .inline(Data(contentsOf: url)) - } - let staged = try stageFile(file, mediaType: nil, filenameHint: url.lastPathComponent) - return .stagedBlob(staged) case .stagedBlob: + let url = try roots.resolvedURL(for: file) let staged = try stageFile(file, mediaType: nil, filenameHint: url.lastPathComponent) return .stagedBlob(staged) } @@ -239,6 +239,9 @@ public final class RadrootsAppleFileAccess: RadrootsFileAccess { mediaType: String? = nil, filenameHint: String? = nil ) throws -> RadrootsStagedBlobReference { + guard data.count <= Self.maximumGovernedFileBytes else { + throw RadrootsAppleFileError.invalidRequest("staged blob exceeds the governed byte limit") + } let blobID = UUID().uuidString.lowercased() let blob = try RadrootsStagedBlobReference( blobID: blobID, @@ -259,11 +262,8 @@ public final class RadrootsAppleFileAccess: RadrootsFileAccess { filenameHint: String? = nil ) throws -> RadrootsStagedBlobReference { let sourceURL = try roots.resolvedURL(for: file) - guard fileManager.fileExists(atPath: sourceURL.path) else { - throw RadrootsAppleFileError.notFound("file not found") - } - return try stageFileURL( - sourceURL, + return try stageBlob( + readGovernedFile(file, maximumBytes: Self.maximumGovernedFileBytes), mediaType: mediaType, filenameHint: filenameHint ?? sourceURL.lastPathComponent ) @@ -316,9 +316,10 @@ public final class RadrootsAppleFileAccess: RadrootsFileAccess { case let .inlineData(data): try data.write(to: fileURL, options: [.atomic]) case let .file(file): - try copyReplacingItem(from: roots.resolvedURL(for: file), to: fileURL) + try readGovernedFile(file, maximumBytes: Self.maximumGovernedFileBytes) + .write(to: fileURL, options: [.atomic]) case let .stagedBlob(stagedBlob): - try copyReplacingItem(from: stagedBlobURL(for: stagedBlob), to: fileURL) + try readStagedBlob(stagedBlob).write(to: fileURL, options: [.atomic]) } let sizeBytes: UInt64 = if let requestSizeBytes = request.sizeBytes { requestSizeBytes @@ -335,11 +336,19 @@ public final class RadrootsAppleFileAccess: RadrootsFileAccess { } public func readStagedBlob(_ blob: RadrootsStagedBlobReference) throws -> Data { - let url = try stagedBlobURL(for: blob) - guard fileManager.fileExists(atPath: url.path) else { - throw RadrootsAppleFileError.notFound("staged blob not found") + guard (0 ... Self.maximumGovernedFileBytes).contains(blob.sizeBytes) else { + throw RadrootsAppleFileError.invalidRequest("staged blob byte size is invalid") + } + let data: Data + do { + data = try RadrootsGovernedFileReader.read( + root: roots.stagedBlobsRoot, + relativePath: blob.blobID, + maximumBytes: blob.sizeBytes + ) + } catch let error as RadrootsGovernedFileReadError { + throw mappedGovernedReadError(error) } - let data = try Data(contentsOf: url) guard data.count == blob.sizeBytes else { throw RadrootsAppleFileError.permanentFailure("staged blob size does not match reference") } @@ -434,6 +443,9 @@ public final class RadrootsAppleFileAccess: RadrootsFileAccess { filenameHint: String? ) throws -> RadrootsStagedBlobReference { let sizeBytes = try fileSizeInt(at: sourceURL) + guard sizeBytes <= Self.maximumGovernedFileBytes else { + throw RadrootsAppleFileError.permanentFailure("file exceeds the governed byte limit") + } let blobID = UUID().uuidString.lowercased() let blob = try RadrootsStagedBlobReference( blobID: blobID, @@ -499,6 +511,44 @@ public final class RadrootsAppleFileAccess: RadrootsFileAccess { try UInt64(fileSizeInt(at: url)) } + private func readGovernedFile( + _ file: RadrootsFileReference, + maximumBytes: Int, + preserveTooLarge: Bool = false + ) throws -> Data { + guard (0 ... Self.maximumGovernedFileBytes).contains(maximumBytes) else { + throw RadrootsAppleFileError.invalidRequest("inline byte limit is invalid") + } + let root = roots.root(for: file.scope) + let resolved = try roots.resolvedURL(for: file) + let relative = try relativePath(for: resolved, under: root) + do { + return try RadrootsGovernedFileReader.read( + root: root, + relativePath: relative, + maximumBytes: maximumBytes + ) + } catch let error as RadrootsGovernedFileReadError { + if preserveTooLarge, error == .tooLarge { + throw error + } + throw mappedGovernedReadError(error) + } + } + + private func mappedGovernedReadError(_ error: RadrootsGovernedFileReadError) -> RadrootsAppleFileError { + switch error { + case .unavailable: + .notFound("file not found") + case .invalidRequest: + .invalidRequest("file request is invalid") + case .tooLarge: + .permanentFailure("file exceeds the governed byte limit") + case .invalidObject, .changedDuringRead, .ioFailure: + .permanentFailure("file failed governed admission") + } + } + private func relativePath(for url: URL, under rootURL: URL) throws -> String { let rootPath = rootURL.standardizedFileURL.path let filePath = url.standardizedFileURL.path @@ -548,21 +598,21 @@ public struct RadrootsAppleFileRoots: Sendable, Equatable { fileManager: FileManager = .default ) throws -> Self { let normalizedAppIdentifier = try normalizedAppIdentifier(appIdentifier) - let dataBaseURL = try fileManager.url( + let dataBaseURL = try canonicalExistingDirectory(fileManager.url( for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true - ) - let cacheBaseURL = try fileManager.url( + )) + let cacheBaseURL = try canonicalExistingDirectory(fileManager.url( for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true - ) + )) let dataRoot = dataBaseURL.appendingPathComponent(normalizedAppIdentifier, isDirectory: true) let cacheRoot = cacheBaseURL.appendingPathComponent(normalizedAppIdentifier, isDirectory: true) - let temporaryRoot = fileManager.temporaryDirectory + let temporaryRoot = try canonicalExistingDirectory(fileManager.temporaryDirectory) .appendingPathComponent(normalizedAppIdentifier, isDirectory: true) return try Self( appIdentifier: normalizedAppIdentifier, @@ -637,4 +687,14 @@ public struct RadrootsAppleFileRoots: Sendable, Equatable { } return standardized } + + private static func canonicalExistingDirectory(_ directory: URL) throws -> URL { + guard directory.isFileURL, + let pointer = directory.path.withCString({ Darwin.realpath($0, nil) }) + else { + throw RadrootsAppleFileError.permanentFailure("platform file root is unavailable") + } + defer { Darwin.free(pointer) } + return URL(fileURLWithPath: String(cString: pointer), isDirectory: true) + } } diff --git a/Sources/RadrootsKit/RadrootsVerifiedArtifactAccess.swift b/Sources/RadrootsKit/RadrootsVerifiedArtifactAccess.swift @@ -64,24 +64,24 @@ public struct RadrootsVerifiedArtifactDescriptor: Sendable, Equatable, Hashable public struct RadrootsVerifiedArtifactFile: Sendable, Equatable, CustomDebugStringConvertible { public let descriptor: RadrootsVerifiedArtifactDescriptor - public let fileURL: URL + public let data: Data - fileprivate init(descriptor: RadrootsVerifiedArtifactDescriptor, fileURL: URL) { + fileprivate init(descriptor: RadrootsVerifiedArtifactDescriptor, data: Data) { self.descriptor = descriptor - self.fileURL = fileURL + self.data = data } public var debugDescription: String { - "RadrootsVerifiedArtifactFile(artifactID: \(descriptor.artifactID), byteSize: \(descriptor.byteSize), mediaType: \(descriptor.mediaType), fileURL: <redacted>)" + "RadrootsVerifiedArtifactFile(artifactID: \(descriptor.artifactID), byteSize: \(descriptor.byteSize), mediaType: \(descriptor.mediaType), data: <redacted>)" } } /// Opens only Rust-approved, content-addressed inbound media artifacts. /// /// Rust remains the verification and cache authority. This Apple adapter -/// independently rechecks the immutable file immediately before handing its -/// local URL to a native renderer and refuses access while protected data is -/// unavailable. +/// independently rechecks the immutable file and returns only its retained, +/// verified bytes to a native renderer. It refuses access while protected data +/// is unavailable. public struct RadrootsAppleVerifiedArtifactAccess: Sendable { private let ownerDirectory: URL private let protectedData: RadrootsProtectedDataProvider @@ -90,7 +90,7 @@ public struct RadrootsAppleVerifiedArtifactAccess: Sendable { mobileStore: RadrootsAppleMobileStoreConfiguration, protectedData: RadrootsProtectedDataProvider = .available ) { - ownerDirectory = mobileStore.ownerDirectory.standardizedFileURL + ownerDirectory = mobileStore.ownerDirectory self.protectedData = protectedData } @@ -100,49 +100,25 @@ public struct RadrootsAppleVerifiedArtifactAccess: Sendable { guard protectedData.currentState() == .available else { throw RadrootsVerifiedArtifactAccessError.protectedDataUnavailable } - let cacheDirectory = - ownerDirectory - .appendingPathComponent("inbound_media.v1", isDirectory: true) - .standardizedFileURL - let candidate = - cacheDirectory - .appendingPathComponent(descriptor.filename, isDirectory: false) - .standardizedFileURL - guard candidate.deletingLastPathComponent() == cacheDirectory else { - throw RadrootsVerifiedArtifactAccessError.invalidDescriptor - } - do { - try Self.requireOrdinaryDirectory(ownerDirectory) - try Self.requireOrdinaryDirectory(cacheDirectory) - guard FileManager.default.fileExists(atPath: candidate.path) else { - throw RadrootsVerifiedArtifactAccessError.artifactUnavailable - } - let values = try candidate.resourceValues(forKeys: [ - .isRegularFileKey, - .isSymbolicLinkKey, - .fileSizeKey, - ]) - guard values.isRegularFile == true, - values.isSymbolicLink != true, - values.fileSize.flatMap(UInt64.init) == descriptor.byteSize - else { + let data = try RadrootsGovernedFileReader.read( + root: ownerDirectory, + relativePath: "inbound_media.v1/\(descriptor.filename)", + maximumBytes: Int(descriptor.byteSize) + ) + guard UInt64(data.count) == descriptor.byteSize else { throw RadrootsVerifiedArtifactAccessError.artifactCorrupt } - guard try RadrootsAppleFileDigest.sha256(at: candidate) == descriptor.artifactID else { + guard RadrootsAppleFileDigest.sha256(data) == descriptor.artifactID else { throw RadrootsVerifiedArtifactAccessError.artifactCorrupt } - #if os(iOS) - try FileManager.default.setAttributes( - [.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication], - ofItemAtPath: candidate.path - ) - #endif - return RadrootsVerifiedArtifactFile(descriptor: descriptor, fileURL: candidate) + return RadrootsVerifiedArtifactFile(descriptor: descriptor, data: data) } catch let error as RadrootsVerifiedArtifactAccessError { throw error - } catch let error as CocoaError where error.code == .fileReadNoSuchFile { + } catch RadrootsGovernedFileReadError.unavailable { throw RadrootsVerifiedArtifactAccessError.artifactUnavailable + } catch is RadrootsGovernedFileReadError { + throw RadrootsVerifiedArtifactAccessError.artifactCorrupt } catch { throw RadrootsVerifiedArtifactAccessError.fileSystemFailure } @@ -151,11 +127,4 @@ public struct RadrootsAppleVerifiedArtifactAccess: Sendable { public func revalidate(_ artifact: RadrootsVerifiedArtifactFile) throws -> Bool { try open(artifact.descriptor) == artifact } - - private static func requireOrdinaryDirectory(_ directory: URL) throws { - let values = try directory.resourceValues(forKeys: [.isDirectoryKey, .isSymbolicLinkKey]) - guard values.isDirectory == true, values.isSymbolicLink != true else { - throw RadrootsVerifiedArtifactAccessError.artifactCorrupt - } - } } diff --git a/Tests/RadrootsKitTests/RadrootsAppleBackgroundTransferTests.swift b/Tests/RadrootsKitTests/RadrootsAppleBackgroundTransferTests.swift @@ -1,3 +1,4 @@ +import Darwin import Foundation import RadrootsKitTesting import Testing @@ -816,9 +817,16 @@ private func successfulHTTPResult() -> RadrootsBackgroundHTTPResult { } private func appleTransferRoots() throws -> RadrootsAppleFileRoots { - let root = FileManager.default.temporaryDirectory.appendingPathComponent( + let unresolvedRoot = FileManager.default.temporaryDirectory.appendingPathComponent( "radroots-apple-background-transfer-\(UUID().uuidString)", isDirectory: true ) + try FileManager.default.createDirectory(at: unresolvedRoot, withIntermediateDirectories: false) + guard let resolvedPointer = unresolvedRoot.path.withCString({ Darwin.realpath($0, nil) }) else { + throw RadrootsBackgroundTransferError.persistenceFailure( + "background transfer test root is unavailable") + } + defer { Darwin.free(resolvedPointer) } + let root = URL(fileURLWithPath: String(cString: resolvedPointer), isDirectory: true) return try RadrootsAppleFileRoots( appIdentifier: "org.radroots.tests", dataRoot: root.appendingPathComponent("data", isDirectory: true), diff --git a/Tests/RadrootsKitTests/RadrootsAppleFileAccessTests.swift b/Tests/RadrootsKitTests/RadrootsAppleFileAccessTests.swift @@ -1,3 +1,4 @@ +import Darwin import Foundation @testable import RadrootsKit import Testing @@ -9,7 +10,7 @@ import Testing try access.write(.inline(data), to: file) - #expect(try access.read(file, mode: .inline) == .inline(data)) + #expect(try access.read(file, mode: .inline(maxBytes: data.count)) == .inline(data)) let entries = try access.list(RadrootsFileReference(scope: .data, relativePath: "identity")) #expect(entries.map(\.file.relativePath) == ["identity/public.json"]) #expect(entries.first?.name == "public.json") @@ -31,7 +32,7 @@ import Testing try access.write(.stagedBlob(staged), to: file) try access.releaseStagedBlob(staged) - #expect(try access.read(file, mode: .inline) == .inline(data)) + #expect(try access.read(file, mode: .inline(maxBytes: data.count)) == .inline(data)) #expect(throws: RadrootsAppleFileError.self) { _ = try access.readStagedBlob(staged) } @@ -97,7 +98,10 @@ import Testing #expect(imported.suggestedFilename == "relays.json") #expect(imported.mediaType == "application/json") #expect(imported.sizeBytes == UInt64(Data(#"{"relays":[]}"#.utf8).count)) - #expect(try access.read(destination, mode: .inline) == .inline(Data(#"{"relays":[]}"#.utf8))) + #expect( + try access.read(destination, mode: .inline(maxBytes: Data(#"{"relays":[]}"#.utf8).count)) + == .inline(Data(#"{"relays":[]}"#.utf8)) + ) } @Test func appleFileAccessPreparesAndReleasesExportDocuments() throws { @@ -168,7 +172,10 @@ import Testing _ = try access.stageBlob(Data("bad".utf8), mediaType: "text/plain", filenameHint: "../secret.txt") } #expect(throws: RadrootsAppleFileError.self) { - _ = try access.read(RadrootsFileReference(scope: .data, relativePath: "missing.json"), mode: .inline) + _ = try access.read( + RadrootsFileReference(scope: .data, relativePath: "missing.json"), + mode: .inline(maxBytes: 1) + ) } #expect(throws: RadrootsAppleFileError.self) { _ = try access.stageExternalFile(#require(URL(string: "https://radroots.org/file.json")), mediaType: nil, filenameHint: nil) @@ -218,7 +225,10 @@ import Testing try access.reset(scope: .data) #expect(try access.list(RadrootsFileReference(scope: .data, relativePath: "")).isEmpty) - #expect(try access.read(cacheFile, mode: .inline) == .inline(Data("cache".utf8))) + #expect( + try access.read(cacheFile, mode: .inline(maxBytes: Data("cache".utf8).count)) + == .inline(Data("cache".utf8)) + ) try access.resetStagedBlobs() @@ -227,9 +237,61 @@ import Testing } } +@Test func appleFileAccessRejectsUnboundedAndUnsafeGovernedReads() throws { + let access = try testFileAccess() + let file = RadrootsFileReference(scope: .data, relativePath: "governed/source.txt") + let data = Data("bounded".utf8) + try access.write(.inline(data), to: file) + + #expect(throws: RadrootsAppleFileError.self) { + _ = try access.read(file, mode: .inline(maxBytes: -1)) + } + #expect(throws: RadrootsAppleFileError.self) { + _ = try access.read(file, mode: .inline(maxBytes: data.count - 1)) + } + + let dataRoot = access.roots.dataRoot + let outside = dataRoot.deletingLastPathComponent().appendingPathComponent("outside", isDirectory: true) + try FileManager.default.createDirectory(at: outside, withIntermediateDirectories: true) + try data.write(to: outside.appendingPathComponent("source.txt")) + let link = dataRoot.appendingPathComponent("linked", isDirectory: true) + try FileManager.default.createSymbolicLink(at: link, withDestinationURL: outside) + #expect(throws: RadrootsAppleFileError.self) { + _ = try access.read( + RadrootsFileReference(scope: .data, relativePath: "linked/source.txt"), + mode: .inline(maxBytes: data.count) + ) + } + + let fifo = dataRoot.appendingPathComponent("governed/source.fifo") + #expect(Darwin.mkfifo(fifo.path, 0o600) == 0) + #expect(throws: RadrootsAppleFileError.self) { + _ = try access.read( + RadrootsFileReference(scope: .data, relativePath: "governed/source.fifo"), + mode: .inline(maxBytes: data.count) + ) + } +} + +@Test func appleFileAccessRejectsAReplacedStagedBlob() throws { + let access = try testFileAccess() + let data = Data("staged".utf8) + let staged = try access.stageBlob(data, mediaType: "text/plain", filenameHint: "staged.txt") + let stagedURL = access.roots.stagedBlobsRoot.appendingPathComponent(staged.blobID) + let replacement = stagedURL.appendingPathExtension("replacement") + try FileManager.default.moveItem(at: stagedURL, to: replacement) + try FileManager.default.createSymbolicLink(at: stagedURL, withDestinationURL: replacement) + + #expect(throws: RadrootsAppleFileError.self) { + _ = try access.readStagedBlob(staged) + } +} + private func testFileAccess() throws -> RadrootsAppleFileAccess { - let root = FileManager.default.temporaryDirectory + let unresolvedRoot = FileManager.default.temporaryDirectory .appendingPathComponent("radroots-file-access-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: unresolvedRoot, withIntermediateDirectories: true) + let root = try canonicalTestDirectory(unresolvedRoot) let roots = try RadrootsAppleFileRoots( appIdentifier: "org.radroots.tests", dataRoot: root.appendingPathComponent("data", isDirectory: true), @@ -239,6 +301,12 @@ private func testFileAccess() throws -> RadrootsAppleFileAccess { return RadrootsAppleFileAccess(roots: roots) } +private func canonicalTestDirectory(_ directory: URL) throws -> URL { + let pointer = try #require(directory.path.withCString { Darwin.realpath($0, nil) }) + defer { Darwin.free(pointer) } + return URL(fileURLWithPath: String(cString: pointer), isDirectory: true) +} + private func writeExternalTestFile(name: String, data: Data) throws -> URL { let directory = FileManager.default.temporaryDirectory .appendingPathComponent("radroots-file-access-external-\(UUID().uuidString)", isDirectory: true) diff --git a/Tests/RadrootsKitTests/RadrootsAppleMediaPreparationTests.swift b/Tests/RadrootsKitTests/RadrootsAppleMediaPreparationTests.swift @@ -1,4 +1,5 @@ import CoreGraphics +import Darwin import Foundation import ImageIO @testable import RadrootsKit @@ -99,9 +100,13 @@ private func writeOrientedImageWithMetadata(to url: URL) throws { } private func mediaPreparationRoots() throws -> RadrootsAppleFileRoots { - let root = FileManager.default.temporaryDirectory.appendingPathComponent( + let unresolvedRoot = FileManager.default.temporaryDirectory.appendingPathComponent( "radroots-media-preparation-\(UUID().uuidString)", isDirectory: true ) + try FileManager.default.createDirectory(at: unresolvedRoot, withIntermediateDirectories: true) + let pointer = try #require(unresolvedRoot.path.withCString { Darwin.realpath($0, nil) }) + defer { Darwin.free(pointer) } + let root = URL(fileURLWithPath: String(cString: pointer), isDirectory: true) return try RadrootsAppleFileRoots( appIdentifier: "org.radroots.tests", dataRoot: root.appendingPathComponent("data", isDirectory: true), cacheRoot: root.appendingPathComponent("cache", isDirectory: true), diff --git a/Tests/RadrootsKitTests/RadrootsBackgroundTransferTests.swift b/Tests/RadrootsKitTests/RadrootsBackgroundTransferTests.swift @@ -1,3 +1,4 @@ +import Darwin import Foundation import Testing @@ -250,6 +251,26 @@ import Testing atPath: roots.dataRoot.appendingPathComponent("background_transfers/transfers.json").path)) } +@Test func appleBackgroundTransferStoreRejectsOversizedAndNonRegularPersistence() async throws { + let roots = try testBackgroundTransferRoots() + let persistedURL = roots.dataRoot.appendingPathComponent( + "background_transfers/transfers.json") + try FileManager.default.createDirectory( + at: persistedURL.deletingLastPathComponent(), withIntermediateDirectories: true) + try Data(repeating: 0x41, count: 1024 * 1024 + 1).write(to: persistedURL) + let store = RadrootsAppleBackgroundTransferStore(roots: roots) + + await #expect(throws: RadrootsBackgroundTransferError.self) { + _ = try await store.loadSnapshots() + } + + try FileManager.default.removeItem(at: persistedURL) + #expect(persistedURL.path.withCString { Darwin.mkfifo($0, S_IRUSR | S_IWUSR) } == 0) + await #expect(throws: RadrootsBackgroundTransferError.self) { + _ = try await store.loadSnapshots() + } +} + @Test func appleBackgroundTransferStoreFailsClosedWhileProtectedDataIsLocked() async throws { let roots = try testBackgroundTransferRoots() let store = RadrootsAppleBackgroundTransferStore( @@ -278,6 +299,12 @@ import Testing == roots.stagedBlobsRoot.appendingPathComponent("blob-1").standardizedFileURL) try FileManager.default.createDirectory(at: roots.dataRoot, withIntermediateDirectories: true) + try FileManager.default.createDirectory( + at: roots.dataRoot.appendingPathComponent("exports", isDirectory: true), + withIntermediateDirectories: false) + try Data("governed".utf8).write( + to: roots.dataRoot.appendingPathComponent("exports/diagnostics.json")) + #expect(try resolver.read(file, maximumBytes: 64) == Data("governed".utf8)) let outside = roots.dataRoot.deletingLastPathComponent().appendingPathComponent("outside.bin") try Data("outside".utf8).write(to: outside) let symlink = roots.dataRoot.appendingPathComponent("escape.bin") @@ -288,6 +315,11 @@ import Testing ) { _ = try resolver.resolve(.file(RadrootsFileReference(scope: .data, relativePath: "escape.bin"))) } + #expect(throws: RadrootsBackgroundTransferError.self) { + _ = try resolver.read( + .file(RadrootsFileReference(scope: .data, relativePath: "escape.bin")), + maximumBytes: 64) + } #expect(throws: RadrootsAppleFileError.invalidRequest("file relative path must not be absolute")) { @@ -410,9 +442,16 @@ private func testDownloadRequest(identifier: String) throws -> RadrootsBackgroun } private func testBackgroundTransferRoots() throws -> RadrootsAppleFileRoots { - let root = FileManager.default.temporaryDirectory.appendingPathComponent( + let unresolvedRoot = FileManager.default.temporaryDirectory.appendingPathComponent( "radroots-background-transfer-\(UUID().uuidString)", isDirectory: true ) + try FileManager.default.createDirectory(at: unresolvedRoot, withIntermediateDirectories: false) + guard let resolvedPointer = unresolvedRoot.path.withCString({ Darwin.realpath($0, nil) }) else { + throw RadrootsBackgroundTransferError.persistenceFailure( + "background transfer test root is unavailable") + } + defer { Darwin.free(resolvedPointer) } + let root = URL(fileURLWithPath: String(cString: resolvedPointer), isDirectory: true) return try RadrootsAppleFileRoots( appIdentifier: "org.radroots.tests", dataRoot: root.appendingPathComponent("data", isDirectory: true), diff --git a/Tests/RadrootsKitTests/RadrootsDocumentPresentationTests.swift b/Tests/RadrootsKitTests/RadrootsDocumentPresentationTests.swift @@ -1,3 +1,4 @@ +import Darwin import Foundation @testable import RadrootsKit import Testing @@ -167,8 +168,12 @@ import UniformTypeIdentifiers } private func testDocumentPresentationFileAccess() throws -> RadrootsAppleFileAccess { - let root = FileManager.default.temporaryDirectory + let unresolvedRoot = FileManager.default.temporaryDirectory .appendingPathComponent("radroots-document-presentation-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: unresolvedRoot, withIntermediateDirectories: true) + let pointer = try #require(unresolvedRoot.path.withCString { Darwin.realpath($0, nil) }) + defer { Darwin.free(pointer) } + let root = URL(fileURLWithPath: String(cString: pointer), isDirectory: true) let roots = try RadrootsAppleFileRoots( appIdentifier: "org.radroots.document-presentation.tests", dataRoot: root.appendingPathComponent("data", isDirectory: true), diff --git a/Tests/RadrootsKitTests/RadrootsVerifiedArtifactAccessTests.swift b/Tests/RadrootsKitTests/RadrootsVerifiedArtifactAccessTests.swift @@ -1,3 +1,4 @@ +import Darwin import Foundation import Testing @@ -21,9 +22,9 @@ private let verifiedArtifactPublicKey = let artifact = try fixture.access.open(descriptor) - #expect(try Data(contentsOf: artifact.fileURL) == data) + #expect(artifact.data == data) #expect(try fixture.access.revalidate(artifact)) - #expect(!artifact.debugDescription.contains(artifact.fileURL.path)) + #expect(!artifact.debugDescription.contains(String(decoding: data, as: UTF8.self))) } @Test func verifiedArtifactAccessFailsClosedForLockMissingTamperAndSymlink() throws { @@ -109,9 +110,14 @@ private struct VerifiedArtifactFixture { let access: RadrootsAppleVerifiedArtifactAccess init() throws { - root = FileManager.default.temporaryDirectory + let unresolvedRoot = FileManager.default.temporaryDirectory .appendingPathComponent("radroots-verified-artifact-\(UUID().uuidString)", isDirectory: true) - try FileManager.default.createDirectory(at: root, withIntermediateDirectories: false) + try FileManager.default.createDirectory(at: unresolvedRoot, withIntermediateDirectories: false) + guard let resolvedPointer = unresolvedRoot.path.withCString({ Darwin.realpath($0, nil) }) else { + throw RadrootsVerifiedArtifactAccessError.fileSystemFailure + } + defer { Darwin.free(resolvedPointer) } + root = URL(fileURLWithPath: String(cString: resolvedPointer), isDirectory: true) let roots = try RadrootsAppleFileRoots( appIdentifier: "org.radroots.tests", dataRoot: root.appendingPathComponent("data", isDirectory: true), diff --git a/contracts/api_baselines/apple_kit.txt b/contracts/api_baselines/apple_kit.txt @@ -684,6 +684,7 @@ radroots.apple-kit.public-api.v1 12:relationship 11:RadrootsKit 13:requirementOf 106:s:11RadrootsKit0A11SecureStoreP3put_3for6policyy10Foundation4DataV_AA0acD3KeyVAA0A18SecretAccessPolicyVtKF 31:s:11RadrootsKit0A11SecureStoreP 0: 12:relationship 11:RadrootsKit 13:requirementOf 107:s:11RadrootsKit0A18BackgroundTransferP6settle_12verificationyAA0acD10IdentifierV_AA0acD12VerificationOtYaKF 38:s:11RadrootsKit0A18BackgroundTransferP 0: 12:relationship 11:RadrootsKit 13:requirementOf 108:s:11RadrootsKit0A10FileAccessP05stageC0_9mediaType12filenameHintAA0A19StagedBlobReferenceVAA0acL0V_SSSgAKtKF 30:s:11RadrootsKit0A10FileAccessP 0: +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 126:s:11RadrootsKit0A10FileAccessP013stageExternalC0_9mediaType12filenameHintAA0A19StagedBlobReferenceV10Foundation3URLV_SSSgALtKF 30:s:11RadrootsKit0A10FileAccessP 0: @@ -773,6 +774,7 @@ radroots.apple-kit.public-api.v1 12:relationship 11:RadrootsKit 8:memberOf 112:s:11RadrootsKit0A23AppleBackgroundTransferC6settle_12verificationyAA0adE10IdentifierV_AA0adE12VerificationOtYaKF 43:s:11RadrootsKit0A23AppleBackgroundTransferC 0: 12:relationship 11:RadrootsKit 8:memberOf 112:s:11RadrootsKit0A28LocationServicesAvailabilityV08locationD7Enabled13authorizationACSb_AA0aC13AuthorizationOtcfc 48:s:11RadrootsKit0A28LocationServicesAvailabilityV 0: 12:relationship 11:RadrootsKit 8:memberOf 113:s:11RadrootsKit0A15AppleFileAccessC05stageD0_9mediaType12filenameHintAA0A19StagedBlobReferenceVAA0adM0V_SSSgAKtKF 35:s:11RadrootsKit0A15AppleFileAccessC 0: +12:relationship 11:RadrootsKit 8:memberOf 114:s:11RadrootsKit0A35AppleBackgroundTransferFileResolverV4read_12maximumBytes10Foundation4DataVAA0ade5LocalF0O_SitKF 55:s:11RadrootsKit0A35AppleBackgroundTransferFileResolverV 0: 12:relationship 11:RadrootsKit 8:memberOf 115:s:11RadrootsKit0A21ExportDocumentRequestV19normalizedSizeBytes6source09requestedgH0s6UInt64VSgAA0acD6SourceO_AItKFZ 41:s:11RadrootsKit0A21ExportDocumentRequestV 0: 12:relationship 11:RadrootsKit 8:memberOf 117:s:11RadrootsKit0A15IdentityCustodyC014exportPortableC010passphraseAA0aC19PortabilityEnvelopeVAA0aC10PassphraseV_tYaKF 35:s:11RadrootsKit0A15IdentityCustodyC 0: 12:relationship 11:RadrootsKit 8:memberOf 118:s:11RadrootsKit0A15IdentityCustodyC06importC0_5label15replaceExistingAA0aC8SnapshotVAA0aC14SecretMaterialV_SSSgSbtYaKF 35:s:11RadrootsKit0A15IdentityCustodyC 0: @@ -861,7 +863,6 @@ radroots.apple-kit.public-api.v1 12:relationship 11:RadrootsKit 8:memberOf 44:s:11RadrootsKit0A9ShareItemO10normalizedACvp 28:s:11RadrootsKit0A9ShareItemO 0: 12:relationship 11:RadrootsKit 8:memberOf 45:s:11RadrootsKit0A11ShareResultV9completedSbvp 31:s:11RadrootsKit0A11ShareResultV 0: 12:relationship 11:RadrootsKit 8:memberOf 45:s:11RadrootsKit0A12BiometryKindO6faceIDyA2CmF 32:s:11RadrootsKit0A12BiometryKindO 0: -12:relationship 11:RadrootsKit 8:memberOf 45:s:11RadrootsKit0A12FileReadModeO6inlineyA2CmF 32:s:11RadrootsKit0A12FileReadModeO 0: 12:relationship 11:RadrootsKit 8:memberOf 45:s:11RadrootsKit0A14TelemetryLevelO4infoyA2CmF 34:s:11RadrootsKit0A14TelemetryLevelO 0: 12:relationship 11:RadrootsKit 8:memberOf 45:s:11RadrootsKit0A9FileEntryV11isDirectorySbvp 28:s:11RadrootsKit0A9FileEntryV 0: 12:relationship 11:RadrootsKit 8:memberOf 46:s:11RadrootsKit0A12BiometryKindO7opticIDyA2CmF 32:s:11RadrootsKit0A12BiometryKindO 0: @@ -908,6 +909,7 @@ radroots.apple-kit.public-api.v1 12:relationship 11:RadrootsKit 8:memberOf 50:s:11RadrootsKit0A21SharePresentationLinkV4bodyQrvp 41:s:11RadrootsKit0A21SharePresentationLinkV 0: 12:relationship 11:RadrootsKit 8:memberOf 51:s:11RadrootsKit0A10MediaAssetV9sizeBytess6UInt64Vvp 30:s:11RadrootsKit0A10MediaAssetV 0: 12:relationship 11:RadrootsKit 8:memberOf 51:s:11RadrootsKit0A11MediaSourceO8rawValueACSgSS_tcfc 31:s:11RadrootsKit0A11MediaSourceO 0: +12:relationship 11:RadrootsKit 8:memberOf 51:s:11RadrootsKit0A12FileReadModeO6inlineyACSi_tcACmF 32:s:11RadrootsKit0A12FileReadModeO 0: 12:relationship 11:RadrootsKit 8:memberOf 51:s:11RadrootsKit0A13FileReferenceV12relativePathSSvp 33:s:11RadrootsKit0A13FileReferenceV 0: 12:relationship 11:RadrootsKit 8:memberOf 51:s:11RadrootsKit0A14SecureStoreKeyV10normalizedACyKF 34:s:11RadrootsKit0A14SecureStoreKeyV 0: 12:relationship 11:RadrootsKit 8:memberOf 51:s:11RadrootsKit0A14TelemetryFieldV4boolyACSS_SbtKFZ 34:s:11RadrootsKit0A14TelemetryFieldV 0: @@ -1194,6 +1196,7 @@ radroots.apple-kit.public-api.v1 12:relationship 11:RadrootsKit 8:memberOf 65:s:11RadrootsKit0A19TelemetryFieldValueO10stringListyACSaySSGcACmF 39:s:11RadrootsKit0A19TelemetryFieldValueO 0: 12:relationship 11:RadrootsKit 8:memberOf 65:s:11RadrootsKit0A20IdentityCustodyErrorO17inconsistentStateyA2CmF 40:s:11RadrootsKit0A20IdentityCustodyErrorO 0: 12:relationship 11:RadrootsKit 8:memberOf 65:s:11RadrootsKit0A20IdentityCustodyErrorO17invalidPassphraseyA2CmF 40:s:11RadrootsKit0A20IdentityCustodyErrorO 0: +12:relationship 11:RadrootsKit 8:memberOf 65:s:11RadrootsKit0A20VerifiedArtifactFileV4data10Foundation4DataVvp 40:s:11RadrootsKit0A20VerifiedArtifactFileV 0: 12:relationship 11:RadrootsKit 8:memberOf 65:s:11RadrootsKit0A21AppleMobileStoreErrorO16invalidPublicKeyyA2CmF 41:s:11RadrootsKit0A21AppleMobileStoreErrorO 0: 12:relationship 11:RadrootsKit 8:memberOf 65:s:11RadrootsKit0A21CurrentLocationResultV7readingAA0aD7ReadingVvp 41:s:11RadrootsKit0A21CurrentLocationResultV 0: 12:relationship 11:RadrootsKit 8:memberOf 65:s:11RadrootsKit0A21LocationAuthorizationO16authorizedAlwaysyA2CmF 41:s:11RadrootsKit0A21LocationAuthorizationO 0: @@ -1228,7 +1231,6 @@ radroots.apple-kit.public-api.v1 12:relationship 11:RadrootsKit 8:memberOf 66:s:11RadrootsKit0A20IdentityCustodyErrorO18invalidSignRequestyA2CmF 40:s:11RadrootsKit0A20IdentityCustodyErrorO 0: 12:relationship 11:RadrootsKit 8:memberOf 66:s:11RadrootsKit0A20IdentityCustodyErrorO18storageUnavailableyA2CmF 40:s:11RadrootsKit0A20IdentityCustodyErrorO 0: 12:relationship 11:RadrootsKit 8:memberOf 66:s:11RadrootsKit0A20IdentityMetadataSlotO18transactionJournalyA2CmF 40:s:11RadrootsKit0A20IdentityMetadataSlotO 0: -12:relationship 11:RadrootsKit 8:memberOf 66:s:11RadrootsKit0A20VerifiedArtifactFileV7fileURL10Foundation0G0Vvp 40:s:11RadrootsKit0A20VerifiedArtifactFileV 0: 12:relationship 11:RadrootsKit 8:memberOf 66:s:11RadrootsKit0A21AppleMobileStoreErrorO17fileSystemFailureyA2CmF 41:s:11RadrootsKit0A21AppleMobileStoreErrorO 0: 12:relationship 11:RadrootsKit 8:memberOf 66:s:11RadrootsKit0A21ProtectedDataProviderV12currentStateAA0acdG0OyF 41:s:11RadrootsKit0A21ProtectedDataProviderV 0: 12:relationship 11:RadrootsKit 8:memberOf 66:s:11RadrootsKit0A22DocumentScannerSupportV18multiPageSupportedSbvp 42:s:11RadrootsKit0A22DocumentScannerSupportV 0: @@ -1996,11 +1998,13 @@ radroots.apple-kit.public-api.v1 6:symbol 11:RadrootsKit 12:swift.method 107:s:11RadrootsKit0A18BackgroundTransferP6settle_12verificationyAA0acD10IdentifierV_AA0acD12VerificationOtYaKF 23:settle(_:verification:) 130:func settle(_ identifier: RadrootsBackgroundTransferIdentifier, verification: RadrootsBackgroundTransferVerification) async throws 6:symbol 11:RadrootsKit 12:swift.method 108:s:11RadrootsKit0A10FileAccessP05stageC0_9mediaType12filenameHintAA0A19StagedBlobReferenceVAA0acL0V_SSSgAKtKF 36:stageFile(_:mediaType:filenameHint:) 145:@discardableResult func stageFile(_ file: RadrootsFileReference, mediaType: String?, filenameHint: String?) throws -> RadrootsStagedBlobReference 6:symbol 11:RadrootsKit 12:swift.method 109:s:11RadrootsKit0A14AppleFileRootsV11resolvedURL3for18allowRootDirectory10Foundation0G0VAA0aD9ReferenceV_SbtKF 36:resolvedURL(for:allowRootDirectory:) 97:func resolvedURL(for file: RadrootsFileReference, allowRootDirectory: Bool = false) throws -> URL +6:symbol 11:RadrootsKit 12:swift.method 109:s:11RadrootsKit0A30BackgroundTransferFileResolverP4read_12maximumBytes10Foundation4DataVAA0acd5LocalE0O_SitKF 21:read(_:maximumBytes:) 88:func read(_ file: RadrootsBackgroundTransferLocalFile, maximumBytes: Int) throws -> Data 6:symbol 11:RadrootsKit 12:swift.method 110:s:11RadrootsKit0A15AppleFileAccessC16sweepStagedBlobs9olderThanSayAA0aG13BlobReferenceVG10Foundation4DateV_tKF 28:sweepStagedBlobs(olderThan:) 104:@discardableResult func sweepStagedBlobs(olderThan cutoff: Date) throws -> [RadrootsStagedBlobReference] 6:symbol 11:RadrootsKit 12:swift.method 110:s:11RadrootsKit0A15IdentityCustodyC013migrateLegacyC04from5labelAA0aC8SnapshotVAA0A14SecureStoreKeyV_SSSgtYaKF 34:migrateLegacyIdentity(from:label:) 148:@discardableResult func migrateLegacyIdentity(from legacyKey: RadrootsSecureStoreKey, label: String? = nil) async throws -> RadrootsIdentitySnapshot 6:symbol 11:RadrootsKit 12:swift.method 112:s:11RadrootsKit0A23AppleBackgroundTransferC6settle_12verificationyAA0adE10IdentifierV_AA0adE12VerificationOtYaKF 23:settle(_:verification:) 130:func settle(_ identifier: RadrootsBackgroundTransferIdentifier, verification: RadrootsBackgroundTransferVerification) async throws 6:symbol 11:RadrootsKit 12:swift.method 113:s:11RadrootsKit0A15AppleFileAccessC05stageD0_9mediaType12filenameHintAA0A19StagedBlobReferenceVAA0adM0V_SSSgAKtKF 36:stageFile(_:mediaType:filenameHint:) 157:@discardableResult func stageFile(_ file: RadrootsFileReference, mediaType: String? = nil, filenameHint: String? = nil) throws -> RadrootsStagedBlobReference 6:symbol 11:RadrootsKit 12:swift.method 113:s:11RadrootsKit0A18BackgroundTransferP015handleEventsForC10URLSession10identifier17completionHandlerySS_yyYbctYaF 66:handleEventsForBackgroundURLSession(identifier:completionHandler:) 117:func handleEventsForBackgroundURLSession(identifier: String, completionHandler: @escaping @Sendable () -> Void) async +6:symbol 11:RadrootsKit 12:swift.method 114:s:11RadrootsKit0A35AppleBackgroundTransferFileResolverV4read_12maximumBytes10Foundation4DataVAA0ade5LocalF0O_SitKF 21:read(_:maximumBytes:) 88:func read(_ file: RadrootsBackgroundTransferLocalFile, maximumBytes: Int) throws -> Data 6:symbol 11:RadrootsKit 12:swift.method 116:s:11RadrootsKit0A10FileAccessP9stageBlob_9mediaType12filenameHintAA0a6StagedF9ReferenceV10Foundation4DataV_SSSgALtKF 36:stageBlob(_:mediaType:filenameHint:) 128:@discardableResult func stageBlob(_ data: Data, mediaType: String?, filenameHint: String?) throws -> RadrootsStagedBlobReference 6:symbol 11:RadrootsKit 12:swift.method 117:s:11RadrootsKit0A15IdentityCustodyC014exportPortableC010passphraseAA0aC19PortabilityEnvelopeVAA0aC10PassphraseV_tYaKF 35:exportPortableIdentity(passphrase:) 119:func exportPortableIdentity(passphrase: RadrootsIdentityPassphrase) async throws -> RadrootsIdentityPortabilityEnvelope 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 @@ -2393,6 +2397,7 @@ radroots.apple-kit.public-api.v1 6:symbol 11:RadrootsKit 14:swift.property 65:s:11RadrootsKit0A15LocationReadingV24horizontalAccuracyMetersSdvp 24:horizontalAccuracyMeters 36:let horizontalAccuracyMeters: Double 6:symbol 11:RadrootsKit 14:swift.property 65:s:11RadrootsKit0A15OpaqueSignatureV9signature10Foundation4DataVvp 9:signature 19:let signature: Data 6:symbol 11:RadrootsKit 14:swift.property 65:s:11RadrootsKit0A18UserPresenceStatusV21canEvaluateBiometricsSbvp 21:canEvaluateBiometrics 31:let canEvaluateBiometrics: Bool +6:symbol 11:RadrootsKit 14:swift.property 65:s:11RadrootsKit0A20VerifiedArtifactFileV4data10Foundation4DataVvp 4:data 14:let data: Data 6:symbol 11:RadrootsKit 14:swift.property 65:s:11RadrootsKit0A21CurrentLocationResultV7readingAA0aD7ReadingVvp 7:reading 36:let reading: RadrootsLocationReading 6:symbol 11:RadrootsKit 14:swift.property 65:s:11RadrootsKit0A21LocationServicesErrorO16errorDescriptionSSSgvp 16:errorDescription 37:var errorDescription: String? { get } 6:symbol 11:RadrootsKit 14:swift.property 65:s:11RadrootsKit0A22PreparedExportDocumentV17suggestedFilenameSSvp 17:suggestedFilename 29:let suggestedFilename: String @@ -2401,7 +2406,6 @@ radroots.apple-kit.public-api.v1 6:symbol 11:RadrootsKit 14:swift.property 65:s:11RadrootsKit0A26VerifiedArtifactDescriptorV13fileExtensionSSvp 13:fileExtension 25:let fileExtension: String 6:symbol 11:RadrootsKit 14:swift.property 66:s:11RadrootsKit0A14TelemetryEventV10occurredAt10Foundation4DateVvp 10:occurredAt 20:let occurredAt: Date 6:symbol 11:RadrootsKit 14:swift.property 66:s:11RadrootsKit0A18MediaPickerSupportV22cameraCaptureAvailableSbvp 22:cameraCaptureAvailable 32:let cameraCaptureAvailable: Bool -6:symbol 11:RadrootsKit 14:swift.property 66:s:11RadrootsKit0A20VerifiedArtifactFileV7fileURL10Foundation0G0Vvp 7:fileURL 16:let fileURL: URL 6:symbol 11:RadrootsKit 14:swift.property 66:s:11RadrootsKit0A22DocumentScannerSupportV18multiPageSupportedSbvp 18:multiPageSupported 28:let multiPageSupported: Bool 6:symbol 11:RadrootsKit 14:swift.property 66:s:11RadrootsKit0A24BackgroundTransferHandleV16debugDescriptionSSvp 16:debugDescription 36:var debugDescription: String { get } 6:symbol 11:RadrootsKit 14:swift.property 66:s:11RadrootsKit0A26BackgroundTransferSnapshotV12errorMessageSSSgvp 12:errorMessage 25:let errorMessage: String? @@ -2557,7 +2561,6 @@ radroots.apple-kit.public-api.v1 6:symbol 11:RadrootsKit 15:swift.enum.case 43:s:11RadrootsKit0A9ShareItemO4textyACSScACmF 26:RadrootsShareItem.text(_:) 17:case text(String) 6:symbol 11:RadrootsKit 15:swift.enum.case 44:s:11RadrootsKit0A9FileScopeO9temporaryyA2CmF 27:RadrootsFileScope.temporary 14:case temporary 6:symbol 11:RadrootsKit 15:swift.enum.case 45:s:11RadrootsKit0A12BiometryKindO6faceIDyA2CmF 27:RadrootsBiometryKind.faceID 11:case faceID -6:symbol 11:RadrootsKit 15:swift.enum.case 45:s:11RadrootsKit0A12FileReadModeO6inlineyA2CmF 27:RadrootsFileReadMode.inline 11:case inline 6:symbol 11:RadrootsKit 15:swift.enum.case 45:s:11RadrootsKit0A14TelemetryLevelO4infoyA2CmF 27:RadrootsTelemetryLevel.info 9:case info 6:symbol 11:RadrootsKit 15:swift.enum.case 46:s:11RadrootsKit0A12BiometryKindO7opticIDyA2CmF 28:RadrootsBiometryKind.opticID 12:case opticID 6:symbol 11:RadrootsKit 15:swift.enum.case 46:s:11RadrootsKit0A12BiometryKindO7touchIDyA2CmF 28:RadrootsBiometryKind.touchID 12:case touchID @@ -2582,6 +2585,7 @@ radroots.apple-kit.public-api.v1 6:symbol 11:RadrootsKit 15:swift.enum.case 50:s:11RadrootsKit0A19DocumentContentKindO4fileyA2CmF 32:RadrootsDocumentContentKind.file 9:case file 6:symbol 11:RadrootsKit 15:swift.enum.case 50:s:11RadrootsKit0A19DocumentContentKindO4jsonyA2CmF 32:RadrootsDocumentContentKind.json 9:case json 6:symbol 11:RadrootsKit 15:swift.enum.case 50:s:11RadrootsKit0A19UserPresenceSupportO4noneyA2CmF 32:RadrootsUserPresenceSupport.none 9:case none +6:symbol 11:RadrootsKit 15:swift.enum.case 51:s:11RadrootsKit0A12FileReadModeO6inlineyACSi_tcACmF 38:RadrootsFileReadMode.inline(maxBytes:) 26:case inline(maxBytes: Int) 6:symbol 11:RadrootsKit 15:swift.enum.case 51:s:11RadrootsKit0A18ProtectedDataStateO6lockedyA2CmF 33:RadrootsProtectedDataState.locked 11:case locked 6:symbol 11:RadrootsKit 15:swift.enum.case 52:s:11RadrootsKit0A11MediaSourceO13cameraCaptureyA2CmF 33:RadrootsMediaSource.cameraCapture 18:case cameraCapture 6:symbol 11:RadrootsKit 15:swift.enum.case 52:s:11RadrootsKit0A11MediaSourceO13libraryImportyA2CmF 33:RadrootsMediaSource.libraryImport 18:case libraryImport