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

RadrootsVerifiedArtifactAccess.swift (4844B)


      1 import Foundation
      2 
      3 public enum RadrootsVerifiedArtifactAccessError: Error, Equatable, Sendable {
      4   case invalidDescriptor
      5   case protectedDataUnavailable
      6   case artifactUnavailable
      7   case artifactCorrupt
      8   case fileSystemFailure
      9 }
     10 
     11 extension RadrootsVerifiedArtifactAccessError: LocalizedError {
     12     public var errorDescription: String? {
     13         switch self {
     14         case .invalidDescriptor: "The verified-artifact descriptor is invalid."
     15         case .protectedDataUnavailable: "Protected data is unavailable."
     16         case .artifactUnavailable: "The verified artifact is unavailable."
     17         case .artifactCorrupt: "The verified artifact is corrupt."
     18         case .fileSystemFailure: "The verified artifact could not be opened."
     19         }
     20     }
     21 }
     22 
     23 public struct RadrootsVerifiedArtifactDescriptor: Sendable, Equatable, Hashable {
     24   public let artifactID: String
     25   public let byteSize: UInt64
     26   public let mediaType: String
     27   public let fileExtension: String
     28   public let width: UInt32
     29   public let height: UInt32
     30 
     31   public init(
     32     artifactID: String,
     33     byteSize: UInt64,
     34     mediaType: String,
     35     width: UInt32,
     36     height: UInt32
     37   ) throws {
     38     let normalizedMediaType =
     39       mediaType
     40       .trimmingCharacters(in: .whitespacesAndNewlines)
     41       .lowercased()
     42     guard artifactID.range(of: "^[0-9a-f]{64}$", options: .regularExpression) != nil,
     43       let fileExtension = Self.fileExtension(for: normalizedMediaType),
     44       byteSize > 0,
     45       byteSize <= 512 * 1024 * 1024,
     46       width > 0,
     47       height > 0,
     48       width <= 16_384,
     49       height <= 16_384,
     50       UInt64(width) * UInt64(height) <= 100_000_000
     51     else {
     52       throw RadrootsVerifiedArtifactAccessError.invalidDescriptor
     53     }
     54     self.artifactID = artifactID
     55     self.byteSize = byteSize
     56     self.mediaType = normalizedMediaType
     57     self.fileExtension = fileExtension
     58     self.width = width
     59     self.height = height
     60   }
     61 
     62   fileprivate var filename: String {
     63     "\(artifactID).\(fileExtension)"
     64   }
     65 
     66   private static func fileExtension(for mediaType: String) -> String? {
     67     switch mediaType {
     68     case "image/gif": "gif"
     69     case "image/jpeg": "jpg"
     70     case "image/png": "png"
     71     case "image/webp": "webp"
     72     default: nil
     73     }
     74   }
     75 }
     76 
     77 public struct RadrootsVerifiedArtifactFile: Sendable, Equatable, CustomDebugStringConvertible {
     78   public let descriptor: RadrootsVerifiedArtifactDescriptor
     79   public let data: Data
     80 
     81   fileprivate init(descriptor: RadrootsVerifiedArtifactDescriptor, data: Data) {
     82     self.descriptor = descriptor
     83     self.data = data
     84   }
     85 
     86   public var debugDescription: String {
     87     "RadrootsVerifiedArtifactFile(artifactID: \(descriptor.artifactID), byteSize: \(descriptor.byteSize), mediaType: \(descriptor.mediaType), data: <redacted>)"
     88   }
     89 }
     90 
     91 /// Opens only Rust-approved, content-addressed inbound media artifacts.
     92 ///
     93 /// Rust remains the verification and cache authority. This Apple adapter
     94 /// independently rechecks the immutable file and returns only its retained,
     95 /// verified bytes to a native renderer. It refuses access while protected data
     96 /// is unavailable.
     97 public struct RadrootsAppleVerifiedArtifactAccess: Sendable {
     98   private let ownerDirectory: URL
     99   private let protectedData: RadrootsProtectedDataProvider
    100 
    101   public init(
    102     mobileStore: RadrootsAppleMobileStoreConfiguration,
    103     protectedData: RadrootsProtectedDataProvider = .available
    104   ) {
    105     ownerDirectory = mobileStore.ownerDirectory
    106     self.protectedData = protectedData
    107   }
    108 
    109   public func open(_ descriptor: RadrootsVerifiedArtifactDescriptor) throws
    110     -> RadrootsVerifiedArtifactFile
    111   {
    112     guard protectedData.currentState() == .available else {
    113       throw RadrootsVerifiedArtifactAccessError.protectedDataUnavailable
    114     }
    115     do {
    116       let data = try RadrootsGovernedFileReader.read(
    117         root: ownerDirectory,
    118         relativePath: "inbound_media.v1/\(descriptor.filename)",
    119         maximumBytes: Int(descriptor.byteSize)
    120       )
    121       guard UInt64(data.count) == descriptor.byteSize else {
    122         throw RadrootsVerifiedArtifactAccessError.artifactCorrupt
    123       }
    124       guard RadrootsAppleFileDigest.sha256(data) == descriptor.artifactID else {
    125         throw RadrootsVerifiedArtifactAccessError.artifactCorrupt
    126       }
    127       return RadrootsVerifiedArtifactFile(descriptor: descriptor, data: data)
    128     } catch let error as RadrootsVerifiedArtifactAccessError {
    129       throw error
    130     } catch RadrootsGovernedFileReadError.unavailable {
    131       throw RadrootsVerifiedArtifactAccessError.artifactUnavailable
    132     } catch is RadrootsGovernedFileReadError {
    133       throw RadrootsVerifiedArtifactAccessError.artifactCorrupt
    134     } catch {
    135       throw RadrootsVerifiedArtifactAccessError.fileSystemFailure
    136     }
    137   }
    138 
    139   public func revalidate(_ artifact: RadrootsVerifiedArtifactFile) throws -> Bool {
    140     try open(artifact.descriptor) == artifact
    141   }
    142 }