From 28b733762b12d598d80f2d4df6631d818e4892a3 Mon Sep 17 00:00:00 2001 From: "Mark (dev)" Date: Wed, 22 Jul 2026 15:45:15 -0700 Subject: [PATCH] Add ImageType.jpeg read capability and magic-byte detection No writer emits .jpeg yet: senders without a JPEG XL encoder (the App Clip) keep labeling JPEG bytes .jpegXL, because deployed clients fail the entire signed CoreIdentity parse on an unknown discriminant. Consumers classify by bytes via ImageType.detect(from:), not the label. Co-Authored-By: Claude Opus 4.8 --- .changeset/imagetype-jpeg-read.md | 5 ++ Sources/CommProtocol/CoreIdentity.swift | 21 +++++ .../DescribedImageTests.swift | 79 +++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 .changeset/imagetype-jpeg-read.md create mode 100644 Tests/CommProtocolTests/DescribedImageTests.swift diff --git a/.changeset/imagetype-jpeg-read.md b/.changeset/imagetype-jpeg-read.md new file mode 100644 index 0000000..f1a03bd --- /dev/null +++ b/.changeset/imagetype-jpeg-read.md @@ -0,0 +1,5 @@ +--- +"@germ-network/autonomous-comm-protocol": minor +--- + +Add read capability for a `jpeg = 2` case on `ImageType`, plus `ImageType.detect(from:)` magic-byte classification (JXL codestream, JXL container, JPEG). No writer emits `.jpeg` yet: senders without a JPEG XL encoder (the App Clip) keep labeling JPEG bytes `.jpegXL` so that deployed clients — which fail the whole signed `CoreIdentity` parse on an unknown discriminant — are unaffected. Consumers should classify by bytes via `detect(from:)`, not the wire label. diff --git a/Sources/CommProtocol/CoreIdentity.swift b/Sources/CommProtocol/CoreIdentity.swift index 98792b2..d21c6a1 100644 --- a/Sources/CommProtocol/CoreIdentity.swift +++ b/Sources/CommProtocol/CoreIdentity.swift @@ -67,6 +67,27 @@ extension CoreIdentity: LinearEncodedQuintuple { public enum ImageType: UInt8, Sendable { case jpegXL = 1 + case jpeg = 2 + + ///Classify image bytes by magic number, independent of the declared wire label. + ///Senders without a JPEG XL encoder (the App Clip) label their JPEG images + ///`.jpegXL` for backward compatibility, so consumers should trust the bytes, + ///not the label. + public static func detect(from data: Data) -> ImageType? { + //JXL raw codestream: FF 0A + if data.starts(with: [0xFF, 0x0A]) { + return .jpegXL + } + //JXL ISOBMFF container: 00 00 00 0C 4A 58 4C 20 0D 0A 87 0A + if data.starts(with: [0, 0, 0, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A]) { + return .jpegXL + } + //JPEG: FF D8 FF + if data.starts(with: [0xFF, 0xD8, 0xFF]) { + return .jpeg + } + return nil + } } public struct DescribedImage: Equatable, Sendable { diff --git a/Tests/CommProtocolTests/DescribedImageTests.swift b/Tests/CommProtocolTests/DescribedImageTests.swift new file mode 100644 index 0000000..91194c0 --- /dev/null +++ b/Tests/CommProtocolTests/DescribedImageTests.swift @@ -0,0 +1,79 @@ +// +// DescribedImageTests.swift +// +// +// Created by Mark @ Germ on 7/22/26. +// + +import CryptoKit +import Foundation +import Testing + +@testable import CommProtocol + +struct DescribedImageTests { + @Test func testImageTypeRoundTrip() throws { + for imageType in [ImageType.jpegXL, .jpeg] { + let described = DescribedImage( + imageType: imageType, + imageData: SymmetricKey(size: .bits256).rawRepresentation, + altText: "description" + ) + let received = try DescribedImage.finalParse(described.wireFormat) + #expect(received == described) + #expect(received.imageType == imageType) + } + } + + @Test func testUnknownImageTypeRejected() throws { + let described = DescribedImage( + imageType: .jpeg, + imageData: SymmetricKey(size: .bits256).rawRepresentation, + altText: nil + ) + var encoded = try described.wireFormat + #expect(encoded.first == ImageType.jpeg.rawValue) + encoded[encoded.startIndex] = 3 + + #expect(throws: LinearEncodingError.unexpectedData) { + _ = try DescribedImage.finalParse(encoded) + } + } + + @Test func testDetectJXLCodestream() { + let data = Data([0xFF, 0x0A]) + Data(repeating: 0, count: 16) + #expect(ImageType.detect(from: data) == .jpegXL) + } + + @Test func testDetectJXLContainer() { + let box: [UInt8] = [0, 0, 0, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A] + let data = Data(box) + Data(repeating: 0, count: 16) + #expect(ImageType.detect(from: data) == .jpegXL) + } + + @Test func testDetectJPEG() { + let data = Data([0xFF, 0xD8, 0xFF, 0xE0]) + Data(repeating: 0, count: 16) + #expect(ImageType.detect(from: data) == .jpeg) + } + + @Test func testDetectRejectsOther() { + //PNG magic + #expect(ImageType.detect(from: Data([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])) == nil) + #expect(ImageType.detect(from: Data()) == nil) + #expect(ImageType.detect(from: Data([0xFF])) == nil) + } + + ///The compatibility posture: a sender without a JPEG XL encoder labels its + ///JPEG bytes `.jpegXL`; detection recovers the true format from the bytes. + @Test func testFalseLabelRecoveredByDetection() { + let jpegBytes = Data([0xFF, 0xD8, 0xFF, 0xE0]) + Data(repeating: 0, count: 16) + let described = DescribedImage( + imageData: jpegBytes, + altText: nil + ) + //wire label says jpegXL (the default) + #expect(described.imageType == .jpegXL) + //bytes say jpeg + #expect(ImageType.detect(from: jpegBytes) == .jpeg) + } +}