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

RadrootsAppleMediaPreparationTests.swift (7387B)


      1 import CoreGraphics
      2 import Darwin
      3 import Foundation
      4 import ImageIO
      5 @testable import RadrootsKit
      6 import Testing
      7 import UniformTypeIdentifiers
      8 
      9 @Test func appleMediaPreparationNormalizesAndCommitsStableFinalBytes() async throws {
     10     let roots = try mediaPreparationRoots()
     11     let sourceReference = RadrootsFileReference(scope: .cache, relativePath: "capture/source.jpg")
     12     let sourceURL = try roots.resolvedURL(for: sourceReference)
     13     try writeOrientedImageWithMetadata(to: sourceURL)
     14     let preparer = RadrootsAppleMediaPreparer(roots: roots)
     15     let request = try RadrootsAppleImagePreparationRequest(source: .file(sourceReference))
     16 
     17     let first = try await preparer.prepareImage(request)
     18     let second = try await preparer.prepareImage(request)
     19 
     20     #expect(first == second)
     21     #expect(first.width == 3)
     22     #expect(first.height == 2)
     23     #expect(first.file.mediaType == "image/png")
     24     #expect(first.file.sizeBytes > 0)
     25     #expect(first.sha256.count == 64)
     26     #expect(!first.debugDescription.contains(roots.temporaryRoot.path))
     27     let outputURL = try roots.stagedBlobURL(for: first.file)
     28     let outputSource = try #require(CGImageSourceCreateWithURL(outputURL as CFURL, nil))
     29     let outputProperties = try #require(
     30         CGImageSourceCopyPropertiesAtIndex(outputSource, 0, nil) as? [CFString: Any]
     31     )
     32     #expect(outputProperties[kCGImagePropertyGPSDictionary] == nil)
     33     let exif = outputProperties[kCGImagePropertyExifDictionary] as? [CFString: Any] ?? [:]
     34     #expect(Set(exif.keys).isSubset(of: [
     35         kCGImagePropertyExifColorSpace,
     36         kCGImagePropertyExifPixelXDimension,
     37         kCGImagePropertyExifPixelYDimension
     38     ]))
     39     #expect(outputProperties[kCGImagePropertyTIFFDictionary] == nil)
     40     #expect(outputProperties[kCGImagePropertyIPTCDictionary] == nil)
     41     let output = try Data(contentsOf: outputURL)
     42     #expect(RadrootsAppleFileDigest.sha256(output) == first.sha256)
     43     #expect(output.range(of: Data("SECRET_DEVICE".utf8)) == nil)
     44     #expect((outputProperties[kCGImagePropertyOrientation] as? NSNumber)?.intValue ?? 1 == 1)
     45 }
     46 
     47 @Test func appleMediaPreparationBuildsBoundedBlossomUploadRequest() async throws {
     48     let roots = try mediaPreparationRoots()
     49     let sourceReference = RadrootsFileReference(scope: .cache, relativePath: "capture/source.jpg")
     50     try writeOrientedImageWithMetadata(to: roots.resolvedURL(for: sourceReference))
     51     let preparer = RadrootsAppleMediaPreparer(roots: roots)
     52     let prepared = try await preparer.prepareImage(
     53         RadrootsAppleImagePreparationRequest(source: .file(sourceReference))
     54     )
     55 
     56     let request = try await preparer.blossomUploadRequest(
     57         preparedImage: prepared,
     58         remoteURL: #require(URL(string: "https://blossom.radroots.org/upload")),
     59         authorization: "Nostr signed-event",
     60         identifier: RadrootsBackgroundTransferIdentifier("field.media.upload")
     61     )
     62 
     63     #expect(request.method == .put)
     64     #expect(request.operation == .upload(source: .stagedBlob(prepared.file)))
     65     #expect(request.headers["Authorization"] == "Nostr signed-event")
     66     #expect(request.headers["Content-Type"] == "image/png")
     67     #expect(request.headers["X-SHA-256"] == prepared.sha256)
     68     #expect(request.headers["Accept"] == "application/json")
     69     #expect(request.headers["Accept-Encoding"] == "identity")
     70     #expect(request.metadata["sha256"] == prepared.sha256)
     71     #expect(try request.responsePolicy == .boundedJSON())
     72     #expect(request.expectedSourceSHA256 == prepared.sha256)
     73 
     74     let preparedURL = try roots.stagedBlobURL(for: prepared.file)
     75     var tampered = try Data(contentsOf: preparedURL)
     76     tampered[tampered.startIndex] ^= 0x01
     77     try tampered.write(to: preparedURL, options: .atomic)
     78     await #expect(throws: RadrootsAppleMediaPreparationError.invalidRequest) {
     79         _ = try await preparer.blossomUploadRequest(
     80             preparedImage: prepared,
     81             remoteURL: #require(URL(string: "https://blossom.radroots.org/upload")),
     82             authorization: "Nostr signed-event"
     83         )
     84     }
     85 }
     86 
     87 @Test func appleMediaPreparationEnforcesByteAndProtectedDataBounds() async throws {
     88     let roots = try mediaPreparationRoots()
     89     let sourceReference = RadrootsFileReference(scope: .cache, relativePath: "capture/source.jpg")
     90     try writeOrientedImageWithMetadata(to: roots.resolvedURL(for: sourceReference))
     91 
     92     let bounded = RadrootsAppleMediaPreparer(roots: roots)
     93     await #expect(throws: RadrootsAppleMediaPreparationError.invalidRequest) {
     94         _ = try await bounded.prepareImage(
     95             RadrootsAppleImagePreparationRequest(source: .file(sourceReference), maximumInputBytes: 1)
     96         )
     97     }
     98 
     99     let locked = RadrootsAppleMediaPreparer(
    100         roots: roots, protectedData: RadrootsProtectedDataProvider { .locked }
    101     )
    102     await #expect(throws: RadrootsAppleMediaPreparationError.unavailable) {
    103         _ = try await locked.prepareImage(
    104             RadrootsAppleImagePreparationRequest(source: .file(sourceReference))
    105         )
    106     }
    107 }
    108 
    109 private func writeOrientedImageWithMetadata(to url: URL) throws {
    110     let colorSpace = CGColorSpaceCreateDeviceRGB()
    111     let context = try #require(
    112         CGContext(
    113             data: nil, width: 2, height: 3, bitsPerComponent: 8, bytesPerRow: 8, space: colorSpace,
    114             bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
    115         )
    116     )
    117     context.setFillColor(CGColor(red: 0.2, green: 0.7, blue: 0.3, alpha: 1))
    118     context.fill(CGRect(x: 0, y: 0, width: 2, height: 3))
    119     let image = try #require(context.makeImage())
    120     try FileManager.default.createDirectory(
    121         at: url.deletingLastPathComponent(), withIntermediateDirectories: true
    122     )
    123     let destination = try #require(
    124         CGImageDestinationCreateWithURL(url as CFURL, UTType.jpeg.identifier as CFString, 1, nil)
    125     )
    126     let properties: [CFString: Any] = [
    127         kCGImagePropertyOrientation: 6,
    128         kCGImagePropertyGPSDictionary: [kCGImagePropertyGPSLatitude: 45.0],
    129         kCGImagePropertyTIFFDictionary: [
    130             kCGImagePropertyTIFFMake: "SECRET_DEVICE",
    131             kCGImagePropertyTIFFModel: "SECRET_DEVICE"
    132         ],
    133         kCGImagePropertyExifDictionary: [kCGImagePropertyExifUserComment: "SECRET_DEVICE"],
    134         kCGImagePropertyIPTCDictionary: [kCGImagePropertyIPTCCaptionAbstract: "SECRET_DEVICE"],
    135         kCGImageDestinationLossyCompressionQuality: 0.9
    136     ]
    137     CGImageDestinationAddImage(destination, image, properties as CFDictionary)
    138     try #require(CGImageDestinationFinalize(destination))
    139 }
    140 
    141 private func mediaPreparationRoots() throws -> RadrootsAppleFileRoots {
    142     let unresolvedRoot = FileManager.default.temporaryDirectory.appendingPathComponent(
    143         "radroots-media-preparation-\(UUID().uuidString)", isDirectory: true
    144     )
    145     try FileManager.default.createDirectory(at: unresolvedRoot, withIntermediateDirectories: true)
    146     let pointer = try #require(unresolvedRoot.path.withCString { Darwin.realpath($0, nil) })
    147     defer { Darwin.free(pointer) }
    148     let root = URL(fileURLWithPath: String(cString: pointer), isDirectory: true)
    149     return try RadrootsAppleFileRoots(
    150         appIdentifier: "org.radroots.tests",
    151         dataRoot: root.appendingPathComponent("data", isDirectory: true),
    152         cacheRoot: root.appendingPathComponent("cache", isDirectory: true),
    153         temporaryRoot: root.appendingPathComponent("tmp", isDirectory: true)
    154     )
    155 }