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 eda7033bb978ad72e976860ce6f528afccc3bb37
parent 5cf21747f61a0a70d2da6ed994fb5246b59b8841
Author: triesap <tyson@radroots.org>
Date:   Fri,  7 Aug 2026 10:26:46 +0000

Align mobile store sources with capsule format

- apply the established SwiftFormat indentation and import ordering
- keep the protected-data and directory contracts behaviorally unchanged
- retain strict SwiftLint compatibility for the new source and tests
- preserve green package tests and iOS Simulator compilation

Diffstat:
MSources/RadrootsKit/RadrootsAppleMobileStore.swift | 228++++++++++++++++++++++++++++++++++++++++----------------------------------------
MTests/RadrootsKitTests/RadrootsAppleMobileStoreTests.swift | 215+++++++++++++++++++++++++++++++++++++++----------------------------------------
2 files changed, 221 insertions(+), 222 deletions(-)

diff --git a/Sources/RadrootsKit/RadrootsAppleMobileStore.swift b/Sources/RadrootsKit/RadrootsAppleMobileStore.swift @@ -1,140 +1,140 @@ import Foundation #if canImport(UIKit) - import UIKit + import UIKit #endif public enum RadrootsAppleProtectedDataAvailability: Sendable, Equatable { - case available - case unavailable + case available + case unavailable - @MainActor - public static var current: Self { - #if canImport(UIKit) - UIApplication.shared.isProtectedDataAvailable ? .available : .unavailable - #else - .available - #endif - } + @MainActor + public static var current: Self { + #if canImport(UIKit) + UIApplication.shared.isProtectedDataAvailable ? .available : .unavailable + #else + .available + #endif + } } public struct RadrootsAppleMobileStoreConfiguration: Sendable, Equatable { - public let applicationSupportDirectory: URL - public let ownerDirectory: URL - public let protectedDataAvailability: RadrootsAppleProtectedDataAvailability + public let applicationSupportDirectory: URL + public let ownerDirectory: URL + public let protectedDataAvailability: RadrootsAppleProtectedDataAvailability } public enum RadrootsAppleMobileStoreError: Error, Sendable, Equatable { - case invalidPublicKey - case protectedDataUnavailable - case invalidDirectoryLayout - case fileSystemFailure + case invalidPublicKey + case protectedDataUnavailable + case invalidDirectoryLayout + case fileSystemFailure } public enum RadrootsAppleMobileStore { - private static let productDirectory = "radroots" - private static let userDirectory = "users" + private static let productDirectory = "radroots" + private static let userDirectory = "users" - /// Prepares the Apple-owned directory consumed by the Rust mobile runtime. - /// - /// The returned Application Support directory is passed to Rust, which - /// independently derives and validates the same identity-scoped suffix. - public static func prepare( - roots: RadrootsAppleFileRoots, - publicKeyHex: String, - protectedDataAvailability: RadrootsAppleProtectedDataAvailability, - fileManager: FileManager = .default - ) throws -> RadrootsAppleMobileStoreConfiguration { - guard protectedDataAvailability == .available else { - throw RadrootsAppleMobileStoreError.protectedDataUnavailable - } - guard isCanonicalPublicKey(publicKeyHex) else { - throw RadrootsAppleMobileStoreError.invalidPublicKey - } + /// Prepares the Apple-owned directory consumed by the Rust mobile runtime. + /// + /// The returned Application Support directory is passed to Rust, which + /// independently derives and validates the same identity-scoped suffix. + public static func prepare( + roots: RadrootsAppleFileRoots, + publicKeyHex: String, + protectedDataAvailability: RadrootsAppleProtectedDataAvailability, + fileManager: FileManager = .default + ) throws -> RadrootsAppleMobileStoreConfiguration { + guard protectedDataAvailability == .available else { + throw RadrootsAppleMobileStoreError.protectedDataUnavailable + } + guard isCanonicalPublicKey(publicKeyHex) else { + throw RadrootsAppleMobileStoreError.invalidPublicKey + } - let applicationSupportDirectory = roots.dataRoot.standardizedFileURL - let productRoot = - applicationSupportDirectory - .appendingPathComponent(productDirectory, isDirectory: true) - let userRoot = - productRoot - .appendingPathComponent(userDirectory, isDirectory: true) - let ownerRoot = - userRoot - .appendingPathComponent(publicKeyHex, isDirectory: true) - - do { - // swiftlint:disable trailing_comma - for directory in [ - applicationSupportDirectory, - productRoot, - userRoot, - ownerRoot, - ] { - try createOrValidateDirectory(directory, fileManager: fileManager) - } - // swiftlint:enable trailing_comma - try excludeFromBackup(ownerRoot) - try applyFileProtection(ownerRoot, fileManager: fileManager) - } catch let error as RadrootsAppleMobileStoreError { - throw error - } catch { - throw RadrootsAppleMobileStoreError.fileSystemFailure - } + let applicationSupportDirectory = roots.dataRoot.standardizedFileURL + let productRoot = + applicationSupportDirectory + .appendingPathComponent(productDirectory, isDirectory: true) + let userRoot = + productRoot + .appendingPathComponent(userDirectory, isDirectory: true) + let ownerRoot = + userRoot + .appendingPathComponent(publicKeyHex, isDirectory: true) - return RadrootsAppleMobileStoreConfiguration( - applicationSupportDirectory: applicationSupportDirectory, - ownerDirectory: ownerRoot, - protectedDataAvailability: protectedDataAvailability - ) - } + do { + // swiftlint:disable trailing_comma + for directory in [ + applicationSupportDirectory, + productRoot, + userRoot, + ownerRoot, + ] { + try createOrValidateDirectory(directory, fileManager: fileManager) + } + // swiftlint:enable trailing_comma + try excludeFromBackup(ownerRoot) + try applyFileProtection(ownerRoot, fileManager: fileManager) + } catch let error as RadrootsAppleMobileStoreError { + throw error + } catch { + throw RadrootsAppleMobileStoreError.fileSystemFailure + } - private static func isCanonicalPublicKey(_ value: String) -> Bool { - value.utf8.count == 64 - && value.utf8.allSatisfy { - ($0 >= 48 && $0 <= 57) || ($0 >= 97 && $0 <= 102) - } - } + return RadrootsAppleMobileStoreConfiguration( + applicationSupportDirectory: applicationSupportDirectory, + ownerDirectory: ownerRoot, + protectedDataAvailability: protectedDataAvailability + ) + } - private static func createOrValidateDirectory( - _ directory: URL, - fileManager: FileManager - ) throws { - var isDirectory: ObjCBool = false - if fileManager.fileExists(atPath: directory.path, isDirectory: &isDirectory) { - let values = try directory.resourceValues(forKeys: [.isDirectoryKey, .isSymbolicLinkKey]) - guard isDirectory.boolValue, values.isDirectory == true, values.isSymbolicLink != true else { - throw RadrootsAppleMobileStoreError.invalidDirectoryLayout - } - return + private static func isCanonicalPublicKey(_ value: String) -> Bool { + value.utf8.count == 64 + && value.utf8.allSatisfy { + ($0 >= 48 && $0 <= 57) || ($0 >= 97 && $0 <= 102) + } } - try fileManager.createDirectory( - at: directory, - withIntermediateDirectories: false, - attributes: nil - ) - let values = try directory.resourceValues(forKeys: [.isDirectoryKey, .isSymbolicLinkKey]) - guard values.isDirectory == true, values.isSymbolicLink != true else { - throw RadrootsAppleMobileStoreError.invalidDirectoryLayout + + private static func createOrValidateDirectory( + _ directory: URL, + fileManager: FileManager + ) throws { + var isDirectory: ObjCBool = false + if fileManager.fileExists(atPath: directory.path, isDirectory: &isDirectory) { + let values = try directory.resourceValues(forKeys: [.isDirectoryKey, .isSymbolicLinkKey]) + guard isDirectory.boolValue, values.isDirectory == true, values.isSymbolicLink != true else { + throw RadrootsAppleMobileStoreError.invalidDirectoryLayout + } + return + } + try fileManager.createDirectory( + at: directory, + withIntermediateDirectories: false, + attributes: nil + ) + let values = try directory.resourceValues(forKeys: [.isDirectoryKey, .isSymbolicLinkKey]) + guard values.isDirectory == true, values.isSymbolicLink != true else { + throw RadrootsAppleMobileStoreError.invalidDirectoryLayout + } } - } - private static func excludeFromBackup(_ directory: URL) throws { - var directory = directory - var values = URLResourceValues() - values.isExcludedFromBackup = true - try directory.setResourceValues(values) - } + private static func excludeFromBackup(_ directory: URL) throws { + var directory = directory + var values = URLResourceValues() + values.isExcludedFromBackup = true + try directory.setResourceValues(values) + } - private static func applyFileProtection( - _ directory: URL, - fileManager: FileManager - ) throws { - #if os(iOS) - try fileManager.setAttributes( - [.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication], - ofItemAtPath: directory.path - ) - #endif - } + private static func applyFileProtection( + _ directory: URL, + fileManager: FileManager + ) throws { + #if os(iOS) + try fileManager.setAttributes( + [.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication], + ofItemAtPath: directory.path + ) + #endif + } } diff --git a/Tests/RadrootsKitTests/RadrootsAppleMobileStoreTests.swift b/Tests/RadrootsKitTests/RadrootsAppleMobileStoreTests.swift @@ -1,134 +1,133 @@ import Foundation -import Testing - @testable import RadrootsKit +import Testing private let publicKey = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" @Test func mobileStorePreparesTheExactIdentityScopedDirectory() throws { - let fixture = try MobileStoreFixture() - defer { fixture.remove() } - - let configuration = try RadrootsAppleMobileStore.prepare( - roots: fixture.roots, - publicKeyHex: publicKey, - protectedDataAvailability: .available - ) - - let expectedOwner = fixture.roots.dataRoot - .appendingPathComponent("radroots", isDirectory: true) - .appendingPathComponent("users", isDirectory: true) - .appendingPathComponent(publicKey, isDirectory: true) - .standardizedFileURL - #expect(configuration.applicationSupportDirectory == fixture.roots.dataRoot) - #expect(configuration.ownerDirectory == expectedOwner) - #expect(configuration.protectedDataAvailability == .available) - - // swiftlint:disable trailing_comma - let values = try expectedOwner.resourceValues(forKeys: [ - .isDirectoryKey, - .isSymbolicLinkKey, - .isExcludedFromBackupKey, - ]) - // swiftlint:enable trailing_comma - #expect(values.isDirectory == true) - #expect(values.isSymbolicLink != true) - #expect(values.isExcludedFromBackup == true) + let fixture = try MobileStoreFixture() + defer { fixture.remove() } + + let configuration = try RadrootsAppleMobileStore.prepare( + roots: fixture.roots, + publicKeyHex: publicKey, + protectedDataAvailability: .available + ) + + let expectedOwner = fixture.roots.dataRoot + .appendingPathComponent("radroots", isDirectory: true) + .appendingPathComponent("users", isDirectory: true) + .appendingPathComponent(publicKey, isDirectory: true) + .standardizedFileURL + #expect(configuration.applicationSupportDirectory == fixture.roots.dataRoot) + #expect(configuration.ownerDirectory == expectedOwner) + #expect(configuration.protectedDataAvailability == .available) + + // swiftlint:disable trailing_comma + let values = try expectedOwner.resourceValues(forKeys: [ + .isDirectoryKey, + .isSymbolicLinkKey, + .isExcludedFromBackupKey, + ]) + // swiftlint:enable trailing_comma + #expect(values.isDirectory == true) + #expect(values.isSymbolicLink != true) + #expect(values.isExcludedFromBackup == true) } @Test func mobileStoreDoesNotTouchDiskWhileProtectedDataIsUnavailable() throws { - let fixture = try MobileStoreFixture() - defer { fixture.remove() } - - #expect(throws: RadrootsAppleMobileStoreError.protectedDataUnavailable) { - _ = try RadrootsAppleMobileStore.prepare( - roots: fixture.roots, - publicKeyHex: publicKey, - protectedDataAvailability: .unavailable - ) - } - #expect(!FileManager.default.fileExists(atPath: fixture.roots.dataRoot.path)) + let fixture = try MobileStoreFixture() + defer { fixture.remove() } + + #expect(throws: RadrootsAppleMobileStoreError.protectedDataUnavailable) { + _ = try RadrootsAppleMobileStore.prepare( + roots: fixture.roots, + publicKeyHex: publicKey, + protectedDataAvailability: .unavailable + ) + } + #expect(!FileManager.default.fileExists(atPath: fixture.roots.dataRoot.path)) } @Test func mobileStoreRejectsNonCanonicalPublicKeysBeforeTouchingDisk() throws { - let fixture = try MobileStoreFixture() - defer { fixture.remove() } - - // swiftlint:disable trailing_comma - for invalidKey in [ - "", "79BE" + String(publicKey.dropFirst(4)), String(repeating: "g", count: 64), - ] { - #expect(throws: RadrootsAppleMobileStoreError.invalidPublicKey) { - _ = try RadrootsAppleMobileStore.prepare( - roots: fixture.roots, - publicKeyHex: invalidKey, - protectedDataAvailability: .available - ) + let fixture = try MobileStoreFixture() + defer { fixture.remove() } + + // swiftlint:disable trailing_comma + for invalidKey in [ + "", "79BE" + String(publicKey.dropFirst(4)), String(repeating: "g", count: 64), + ] { + #expect(throws: RadrootsAppleMobileStoreError.invalidPublicKey) { + _ = try RadrootsAppleMobileStore.prepare( + roots: fixture.roots, + publicKeyHex: invalidKey, + protectedDataAvailability: .available + ) + } } - } - // swiftlint:enable trailing_comma - #expect(!FileManager.default.fileExists(atPath: fixture.roots.dataRoot.path)) + // swiftlint:enable trailing_comma + #expect(!FileManager.default.fileExists(atPath: fixture.roots.dataRoot.path)) } @Test func mobileStoreRejectsAFileInTheDirectoryChain() throws { - let fixture = try MobileStoreFixture() - defer { fixture.remove() } - try FileManager.default.createDirectory( - at: fixture.roots.dataRoot, - withIntermediateDirectories: true - ) - let productPath = fixture.roots.dataRoot.appendingPathComponent("radroots") - try Data("not a directory".utf8).write(to: productPath) - - #expect(throws: RadrootsAppleMobileStoreError.invalidDirectoryLayout) { - _ = try RadrootsAppleMobileStore.prepare( - roots: fixture.roots, - publicKeyHex: publicKey, - protectedDataAvailability: .available + let fixture = try MobileStoreFixture() + defer { fixture.remove() } + try FileManager.default.createDirectory( + at: fixture.roots.dataRoot, + withIntermediateDirectories: true ) - } + let productPath = fixture.roots.dataRoot.appendingPathComponent("radroots") + try Data("not a directory".utf8).write(to: productPath) + + #expect(throws: RadrootsAppleMobileStoreError.invalidDirectoryLayout) { + _ = try RadrootsAppleMobileStore.prepare( + roots: fixture.roots, + publicKeyHex: publicKey, + protectedDataAvailability: .available + ) + } } @Test func mobileStoreRejectsASymlinkInTheDirectoryChain() throws { - let fixture = try MobileStoreFixture() - defer { fixture.remove() } - try FileManager.default.createDirectory( - at: fixture.roots.dataRoot, - withIntermediateDirectories: true - ) - let target = fixture.root.appendingPathComponent("target", isDirectory: true) - try FileManager.default.createDirectory(at: target, withIntermediateDirectories: false) - try FileManager.default.createSymbolicLink( - at: fixture.roots.dataRoot.appendingPathComponent("radroots"), - withDestinationURL: target - ) - - #expect(throws: RadrootsAppleMobileStoreError.invalidDirectoryLayout) { - _ = try RadrootsAppleMobileStore.prepare( - roots: fixture.roots, - publicKeyHex: publicKey, - protectedDataAvailability: .available + let fixture = try MobileStoreFixture() + defer { fixture.remove() } + try FileManager.default.createDirectory( + at: fixture.roots.dataRoot, + withIntermediateDirectories: true ) - } + let target = fixture.root.appendingPathComponent("target", isDirectory: true) + try FileManager.default.createDirectory(at: target, withIntermediateDirectories: false) + try FileManager.default.createSymbolicLink( + at: fixture.roots.dataRoot.appendingPathComponent("radroots"), + withDestinationURL: target + ) + + #expect(throws: RadrootsAppleMobileStoreError.invalidDirectoryLayout) { + _ = try RadrootsAppleMobileStore.prepare( + roots: fixture.roots, + publicKeyHex: publicKey, + protectedDataAvailability: .available + ) + } } private struct MobileStoreFixture { - let root: URL - let roots: RadrootsAppleFileRoots - - init() throws { - root = FileManager.default.temporaryDirectory - .appendingPathComponent("radroots-mobile-store-\(UUID().uuidString)", isDirectory: true) - try FileManager.default.createDirectory(at: root, withIntermediateDirectories: false) - roots = try RadrootsAppleFileRoots( - appIdentifier: "org.radroots.tests", - dataRoot: root.appendingPathComponent("data", isDirectory: true), - cacheRoot: root.appendingPathComponent("cache", isDirectory: true), - temporaryRoot: root.appendingPathComponent("tmp", isDirectory: true) - ) - } + let root: URL + let roots: RadrootsAppleFileRoots + + init() throws { + root = FileManager.default.temporaryDirectory + .appendingPathComponent("radroots-mobile-store-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: root, withIntermediateDirectories: false) + roots = try RadrootsAppleFileRoots( + appIdentifier: "org.radroots.tests", + dataRoot: root.appendingPathComponent("data", isDirectory: true), + cacheRoot: root.appendingPathComponent("cache", isDirectory: true), + temporaryRoot: root.appendingPathComponent("tmp", isDirectory: true) + ) + } - func remove() { - try? FileManager.default.removeItem(at: root) - } + func remove() { + try? FileManager.default.removeItem(at: root) + } }