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 }