Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/imagetype-jpeg-read.md
Original file line number Diff line number Diff line change
@@ -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.
21 changes: 21 additions & 0 deletions Sources/CommProtocol/CoreIdentity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
79 changes: 79 additions & 0 deletions Tests/CommProtocolTests/DescribedImageTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading