commit 90cf01f1654f83166352af3af033081c196d92d8
parent 0a6d6dc44763372fe7da267537fa1f5236b89138
Author: triesap <tyson@radroots.org>
Date: Wed, 22 Jul 2026 06:00:28 +0000
blossom: enforce strict publication raster decoding
- derive raster observations from bounded complete BUD-01 bytes
- accept only static PNG/WebP and 8-bit sequential JPEG entropy
- pin decoder features and govern exact no-default coverage lanes
- preserve frozen predecessor artifacts through successor validation
Diffstat:
23 files changed, 3580 insertions(+), 442 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
@@ -176,10 +176,17 @@ publish policy both pass for the same source revision.
after typed BUD-02 status and descriptor agreement, an independent BUD-01
HEAD, and an exactly bounded complete BUD-01 GET agree with the authored
URL, hash, MIME, and length. The public evidence profile admits JPEG, PNG,
- and still WebP only, rejects animation, and binds one decoded frame to
- dimensions within 16,384 per axis and 20,000,000 pixels. Deterministic
- per-URL evidence remains transport-neutral and contains no HTTP credentials,
- BUD-11 material, entitlement decision, or private service topology.
+ and still WebP only, rejects animation, fully decodes the exact retrieved
+ bytes with a declared-format decoder, and derives dimensions internally
+ within 16,384 per axis and 20,000,000 pixels. JPEG is limited to 8-bit
+ sequential SOF0/SOF1 and combines exact entropy accounting with pinned
+ strict `zune-jpeg` and `zune-core` RGB decoding; PNG and WebP use a
+ separately pinned two-format `image` build.
+ Decoded output is bounded to 160,000,000 bytes before allocation; callers
+ cannot provide decode claims.
+ Deterministic per-URL evidence remains transport-neutral and contains no
+ HTTP credentials, BUD-11 material, entitlement decision, or private service
+ topology.
- Bare-envelope replica ingestion is quarantined behind the explicit,
non-default `legacy-ingest` feature. Default replica APIs expose emit and sync
surfaces only; a future product ingest boundary must consume a store-produced
diff --git a/Cargo.lock b/Cargo.lock
@@ -657,6 +657,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
+[[package]]
name = "bytes"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1872,6 +1878,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
+name = "fdeflate"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
name = "ff"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2740,6 +2755,30 @@ dependencies = [
]
[[package]]
+name = "image"
+version = "0.25.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
+dependencies = [
+ "bytemuck",
+ "byteorder-lite",
+ "image-webp",
+ "moxcms",
+ "num-traits",
+ "png",
+]
+
+[[package]]
+name = "image-webp"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
+dependencies = [
+ "byteorder-lite",
+ "quick-error",
+]
+
+[[package]]
name = "impl-trait-for-tuples"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3327,6 +3366,16 @@ dependencies = [
]
[[package]]
+name = "moxcms"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
+dependencies = [
+ "num-traits",
+ "pxfm",
+]
+
+[[package]]
name = "mti"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4259,6 +4308,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
+name = "png"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
+dependencies = [
+ "bitflags 2.11.0",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
+[[package]]
name = "poly1305"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4433,6 +4495,18 @@ dependencies = [
]
[[package]]
+name = "pxfm"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
+
+[[package]]
+name = "quick-error"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
+
+[[package]]
name = "quinn"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4528,12 +4602,15 @@ name = "radroots_blossom"
version = "1.0.0-alpha.1"
dependencies = [
"hex",
+ "image",
"mediatype",
"serde",
"serde_json",
"sha2",
"unicode-general-category",
"url",
+ "zune-core",
+ "zune-jpeg",
]
[[package]]
@@ -9238,3 +9315,18 @@ dependencies = [
"cc",
"pkg-config",
]
+
+[[package]]
+name = "zune-core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
+
+[[package]]
+name = "zune-jpeg"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
+dependencies = [
+ "zune-core",
+]
diff --git a/Cargo.toml b/Cargo.toml
@@ -141,6 +141,10 @@ fs2 = { version = "0.4" }
getrandom = { version = "0.2", default-features = false }
hkdf = { version = "0.12", default-features = false }
hex = { version = "0.4" }
+image = { version = "=0.25.10", default-features = false, features = [
+ "png",
+ "webp",
+] }
jiff-tzdb = { version = "=0.1.8", default-features = false }
jsonschema = { version = "0.48.1", default-features = false }
js-sys = { version = "0.3" }
@@ -213,5 +217,9 @@ uuid = { version = "1.22.0", features = ["v4", "v7"] }
x509-parser = { version = "0.17", default-features = false }
zstd = { version = "0.13", default-features = false }
zeroize = { version = "1" }
+zune-core = { version = "=0.5.1", default-features = false, features = ["std"] }
+zune-jpeg = { version = "=0.5.15", default-features = false, features = [
+ "std",
+] }
[patch.crates-io]
libsqlite3-sys = { path = "crates/libsqlite3_sys_3_53_3" }
diff --git a/build/nix/checks.nix b/build/nix/checks.nix
@@ -25,6 +25,31 @@ let
installPhaseCommand = "mkdir -p $out";
}
);
+ blossomNoDefaultCheck = common.craneLib.mkCargoDerivation (
+ common.commonCraneArgs
+ // {
+ inherit (common) cargoArtifacts;
+ pname = "radroots-blossom-no-default-check";
+ doCheck = false;
+ buildPhaseCargoCommand = ''
+ cargo check -p radroots_blossom --lib --no-default-features
+ cargo check -p radroots_blossom --lib --no-default-features --features raster-decode
+ '';
+ installPhaseCommand = "mkdir -p $out";
+ }
+ );
+ blossomRasterDecodeTest = common.craneLib.mkCargoDerivation (
+ common.commonCraneArgs
+ // {
+ inherit (common) cargoArtifacts;
+ pname = "radroots-blossom-raster-decode-test";
+ doCheck = false;
+ buildPhaseCargoCommand = ''
+ cargo test -p radroots_blossom --no-default-features --features raster-decode,serde
+ '';
+ installPhaseCommand = "mkdir -p $out";
+ }
+ );
mkReplicaSyncLane =
{
pname,
@@ -61,6 +86,8 @@ in
cargo-fmt = cargoFmt;
cargo-check = cargoCheck;
cargo-test = cargoTest;
+ blossom-no-default-check = blossomNoDefaultCheck;
+ blossom-raster-decode-test = blossomRasterDecodeTest;
replica-sync-default-check = replicaSyncDefaultCheck;
replica-sync-default-test = replicaSyncDefaultTest;
replica-sync-legacy-ingest-check = replicaSyncLegacyCheck;
diff --git a/build/nix/common.nix b/build/nix/common.nix
@@ -24,6 +24,7 @@ let
../../README
../../flake.nix
../../build/nix/apps.nix
+ ../../build/nix/checks.nix
../../build/nix/common.nix
../../build/nix/toolchains.nix
../../dto_bindgen.toml
@@ -115,7 +116,7 @@ let
];
coreContractCargoArgs =
lib.concatStringsSep " " (map (crate: "-p ${crate}") coreContractCrates)
- + " --features radroots_event_codec/serde_json,radroots_event_codec/nostr,radroots_nostr/blossom,radroots_nostr/client,radroots_nostr/codec,radroots_nostr/events";
+ + " --features radroots_blossom/raster-decode,radroots_event_codec/serde_json,radroots_event_codec/nostr,radroots_nostr/blossom,radroots_nostr/client,radroots_nostr/codec,radroots_nostr/events";
craneLib = (crane.mkLib pkgs).overrideToolchain toolchains.stable;
commonCraneArgs = {
inherit version;
diff --git a/contracts/conformance/vectors/blossom/publication_readiness.v1.json b/contracts/conformance/vectors/blossom/publication_readiness.v1.json
@@ -6,31 +6,31 @@
"id": "valid_created",
"kind": "blossom.verify_publication_readiness.valid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "none"
},
"expected": {
- "url": "https://cdn.example/0d1c097e006a87476e84014ba5842f04c725ed2fc5a081743ab2b5bf13a538b9.png",
- "sha256": "0d1c097e006a87476e84014ba5842f04c725ed2fc5a081743ab2b5bf13a538b9",
+ "url": "https://cdn.example/4490130851783ff662845f5e72f1948618cc87f951f00f6c2ffb3dc01f3f40fd.png",
+ "sha256": "4490130851783ff662845f5e72f1948618cc87f951f00f6c2ffb3dc01f3f40fd",
"size": 70,
"media_type": "image/png",
"format": "png",
"width": 1,
"height": 1,
"upload_status": 201,
- "evidence_digest": "c52edeba688fa36c7963a478a35ff78504d7dd79a637c67f93d5acb635110660"
+ "evidence_digest": "44e63303e594ea42d863be995b23ac4297ed77e4378d0707c94f28e77164bd3b"
}
},
{
"id": "valid_ok_without_authored_dimensions",
"kind": "blossom.verify_publication_readiness.valid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "upload_status_200"
},
"expected": {
- "url": "https://cdn.example/0d1c097e006a87476e84014ba5842f04c725ed2fc5a081743ab2b5bf13a538b9.png",
- "sha256": "0d1c097e006a87476e84014ba5842f04c725ed2fc5a081743ab2b5bf13a538b9",
+ "url": "https://cdn.example/4490130851783ff662845f5e72f1948618cc87f951f00f6c2ffb3dc01f3f40fd.png",
+ "sha256": "4490130851783ff662845f5e72f1948618cc87f951f00f6c2ffb3dc01f3f40fd",
"size": 70,
"media_type": "image/png",
"format": "png",
@@ -43,7 +43,7 @@
"id": "invalid_upload_status",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "upload_status_202"
},
"expected": {
@@ -54,7 +54,7 @@
"id": "invalid_head_status",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "head_status_204"
},
"expected": {
@@ -65,7 +65,7 @@
"id": "invalid_get_status",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "get_status_206"
},
"expected": {
@@ -76,7 +76,7 @@
"id": "declared_size_over_public_max",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "get_size_over_max"
},
"expected": {
@@ -87,7 +87,7 @@
"id": "missing_get_body",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "get_body_missing"
},
"expected": {
@@ -98,7 +98,7 @@
"id": "short_get_body",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "get_body_short"
},
"expected": {
@@ -109,7 +109,7 @@
"id": "trailing_get_body",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "get_body_trailing"
},
"expected": {
@@ -120,7 +120,7 @@
"id": "authored_bytes_short",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "authored_bytes_short"
},
"expected": {
@@ -131,7 +131,7 @@
"id": "authored_bytes_wrong_hash",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "authored_bytes_wrong_hash"
},
"expected": {
@@ -142,7 +142,7 @@
"id": "upload_url_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "upload_url_mismatch"
},
"expected": {
@@ -153,7 +153,7 @@
"id": "upload_hash_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "upload_hash_mismatch"
},
"expected": {
@@ -164,7 +164,7 @@
"id": "upload_size_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "upload_size_mismatch"
},
"expected": {
@@ -175,7 +175,7 @@
"id": "upload_mime_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "upload_mime_mismatch"
},
"expected": {
@@ -186,7 +186,7 @@
"id": "head_url_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "head_url_mismatch"
},
"expected": {
@@ -197,7 +197,7 @@
"id": "head_size_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "head_size_mismatch"
},
"expected": {
@@ -208,7 +208,7 @@
"id": "head_mime_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "head_mime_mismatch"
},
"expected": {
@@ -219,7 +219,7 @@
"id": "get_url_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "get_url_mismatch"
},
"expected": {
@@ -230,7 +230,7 @@
"id": "get_declared_size_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "get_declared_size_mismatch"
},
"expected": {
@@ -241,7 +241,7 @@
"id": "get_complete_hash_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "get_bytes_wrong_hash"
},
"expected": {
@@ -252,7 +252,7 @@
"id": "unsupported_raster_mime",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "unsupported_mime"
},
"expected": {
@@ -263,7 +263,7 @@
"id": "malformed_raster",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "malformed_container"
},
"expected": {
@@ -274,62 +274,62 @@
"id": "animated_png",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "animated_png"
},
"expected": {
- "error": "publication_raster_frame_count_mismatch"
+ "error": "publication_raster_animation_forbidden"
}
},
{
- "id": "decode_format_mismatch",
+ "id": "declared_format_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
- "mutation": "decode_format_mismatch"
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "declared_mime_jpeg"
},
"expected": {
- "error": "publication_raster_decode_format_mismatch"
+ "error": "invalid_publication_raster"
}
},
{
- "id": "decode_length_mismatch",
+ "id": "corrupt_png_crc",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
- "mutation": "decode_length_mismatch"
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "corrupt_png_crc"
},
"expected": {
- "error": "publication_raster_decode_length_mismatch"
+ "error": "publication_raster_decode_failed"
}
},
{
- "id": "decode_hash_mismatch",
+ "id": "corrupt_png_deflate",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
- "mutation": "decode_hash_mismatch"
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "corrupt_png_deflate"
},
"expected": {
- "error": "publication_raster_decode_hash_mismatch"
+ "error": "publication_raster_decode_failed"
}
},
{
- "id": "decode_container_dimension_mismatch",
+ "id": "invalid_png_color_type",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
- "mutation": "decode_container_dimension_mismatch"
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "invalid_png_color_type"
},
"expected": {
- "error": "publication_raster_container_dimension_mismatch"
+ "error": "publication_raster_decode_failed"
}
},
{
"id": "authored_dimension_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "authored_dimension_mismatch"
},
"expected": {
@@ -337,48 +337,92 @@
}
},
{
- "id": "decode_zero_frames",
+ "id": "animated_webp",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
- "mutation": "decode_zero_frames"
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "animated_webp"
},
"expected": {
- "error": "publication_raster_frame_count_mismatch"
+ "error": "publication_raster_animation_forbidden"
}
},
{
- "id": "decode_zero_width",
+ "id": "zero_width",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
- "mutation": "decode_zero_width"
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "zero_width"
},
"expected": {
"error": "publication_raster_dimensions_out_of_range"
}
},
{
- "id": "decode_dimension_over_max",
+ "id": "dimension_over_max",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
- "mutation": "decode_dimension_over_max"
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "dimension_over_max"
},
"expected": {
"error": "publication_raster_dimensions_out_of_range"
}
},
{
- "id": "decode_pixel_limit",
+ "id": "pixel_limit",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
- "mutation": "decode_pixel_limit"
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "pixel_limit"
},
"expected": {
"error": "publication_raster_pixel_limit_exceeded"
}
+ },
+ {
+ "id": "progressive_jpeg",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "progressive_jpeg"
+ },
+ "expected": {
+ "error": "publication_jpeg_process_forbidden"
+ }
+ },
+ {
+ "id": "jpeg_entropy_stripped",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "jpeg_entropy_stripped"
+ },
+ "expected": {
+ "error": "publication_raster_decode_failed"
+ }
+ },
+ {
+ "id": "jpeg_entropy_partial",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "jpeg_entropy_partial"
+ },
+ "expected": {
+ "error": "publication_raster_decode_failed"
+ }
+ },
+ {
+ "id": "malformed_jpeg_dqt",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "malformed_jpeg_dqt"
+ },
+ "expected": {
+ "error": "invalid_publication_raster"
+ }
}
]
}
diff --git a/contracts/coverage-profiles.toml b/contracts/coverage-profiles.toml
@@ -8,6 +8,11 @@ no_default_features = true
features = []
test_threads = 1
+[profiles.crates."radroots_blossom"]
+no_default_features = true
+features = ["raster-decode", "serde"]
+test_threads = 1
+
[profiles.crates."radroots_event_codec"]
no_default_features = false
features = ["serde_json", "nostr"]
diff --git a/contracts/events/blossom-media.md b/contracts/events/blossom-media.md
@@ -241,12 +241,13 @@ The public typed API exposes these stable semantic identifiers:
- `publication_retrieved_bytes_mismatch`
- `unsupported_publication_raster_media_type`
- `invalid_publication_raster`
-- `publication_raster_frame_count_mismatch`
+- `publication_jpeg_process_forbidden`
+- `publication_raster_animation_forbidden`
- `publication_raster_dimensions_out_of_range`
- `publication_raster_pixel_limit_exceeded`
-- `publication_raster_decode_format_mismatch`
-- `publication_raster_decode_length_mismatch`
-- `publication_raster_decode_hash_mismatch`
+- `publication_raster_decoded_byte_limit_exceeded`
+- `publication_raster_decode_allocation_failed`
+- `publication_raster_decode_failed`
- `publication_raster_container_dimension_mismatch`
- `publication_authored_raster_dimension_mismatch`
@@ -271,8 +272,8 @@ approval, and byte verification into one indistinguishable state.
## Publication Readiness Evidence
`RadrootsBlossomPublicationReadinessEvidence` is a transport-neutral proof assembled only after
-all of these independently supplied observations agree with the exact byte-verified authored
-descriptor and deterministic raster bytes:
+the supplied transport observations agree with the exact byte-verified authored descriptor and
+deterministic raster bytes, and the internal decoder validates those bytes:
1. a BUD-02 response has status `200` or `201`, an approved canonical hash-path URL, and matching
SHA-256, byte length, and exact media type;
@@ -281,24 +282,46 @@ descriptor and deterministic raster bytes:
`RadrootsBlossomBud01GetCollector`, and ends at exactly the declared size;
4. the complete GET body equals the authored byte count and SHA-256 and is no larger than
`10,485,760` bytes;
-5. a decoder observation is bound to those same complete bytes and reports the matching closed
- raster format, exactly one frame, and bounded dimensions.
+5. the `raster-decode` implementation selects a decoder from the exact declared media type,
+ enforces the closed 8-bit sequential JPEG profile or rejects any PNG or WebP animation
+ declaration, decodes the complete static body, and derives bounded dimensions internally.
The closed raster profile is exact bare `image/jpeg`, `image/png`, or `image/webp`. PNG animation
chunks and WebP animation flags/chunks fail before evidence is created. JPEG, PNG, and WebP
-container structure is checked independently of the decoder observation. Width and height are each
-within `1..=16,384`, and their product is at most `20,000,000` pixels. When an authored product
-already carries dimensions, it supplies `RadrootsBlossomAuthoredRasterDimensions::Exact` and the
-decoded dimensions must match. Products without authored dimensions supply the explicit
-`Unspecified` variant; the evidence then preserves the bounded decoded dimensions for its eventual
-artifact adapter.
+container structure is checked independently of the full decoder. JPEG decoding uses exactly
+`zune-jpeg` `0.5.15` and `zune-core` `0.5.1`, both exactly pinned with only their `std` feature;
+unsafe decoder intrinsics are explicitly disabled.
+Before that full RGB pixel decode, a private exact sequential entropy validator parses SOF0/SOF1,
+DHT, DRI, and SOS structure and accounts for the complete MCU/block inventory. It accepts only
+8-bit sequential frames with one, three, or four unique components, valid sampling factors whose
+products sum to at most ten, and each component encoded exactly once. Huffman tables are bounded to
+256 unique symbols, must leave the all-one code unused, and may not be overfull. Entropy reads may
+not cross into a marker or synthesize missing bits; terminal pad bits must all be one, restart
+markers must appear at the declared interval in RST0-through-RST7 order, and extra entropy before
+the next marker is rejected. Progressive SOF2, every other JPEG process, missing or duplicate
+component scans, partial entropy, and trailing bytes fail closed. Independently parsed component
+count and dimensions must agree with the strict full decoder. The permissive `image` JPEG adapter
+is not compiled. PNG and WebP decoding uses exactly `image` `0.25.10` with only those two format
+features. Width and height are each within
+`1..=16,384`, their product is at most `20,000,000` pixels, and every decoder output buffer is at
+most `160,000,000` bytes. Limits are enforced before decoded-output allocation. When an authored
+product already carries dimensions, it supplies
+`RadrootsBlossomAuthoredRasterDimensions::Exact` and the decoded dimensions must match. Products
+without authored dimensions supply the explicit `Unspecified` variant; the evidence then preserves
+the bounded decoded dimensions for its eventual artifact adapter.
The public crate does not choose or execute HTTP, DNS, redirects, credentials, BUD-11 claims,
-entitlement policy, private endpoints, or an image-decoder implementation. The owning runtime must
-construct the decode observation from its approved decoder over the exact complete body. The core
-then verifies the observation's byte hash, length, format, frame count, container dimensions, and
-product dimensions. A decode observation is not independently trustworthy without that runtime
-adapter and does not claim server availability after the observation.
+entitlement policy, or private endpoints. An owning runtime supplies the typed HTTP observations and
+exact complete body. The non-default `raster-decode` feature then uses only the fully pinned
+sequential-JPEG validator/decoder pair and PNG/WebP decoder set; callers cannot inject format,
+hash, length, frame, or dimension claims. The
+portable `no_std` core remains available without that feature, but the readiness-evidence
+constructor does not. Evidence describes the verified observation and does not claim later server
+availability.
+
+Publication-readiness policy version `1` is defined by this authoritative full-decode boundary.
+There is no serialized or caller-supplied decode-observation input in the v1 contract, and any such
+legacy local shape is rejected rather than migrated or trusted.
Each evidence value has a domain-separated deterministic digest covering policy version, complete
canonical URL, hash, length, MIME, raster format, dimensions, BUD-02 status, successful BUD-01
@@ -310,4 +333,9 @@ to its independently computed artifact digest.
`contracts/conformance/vectors/blossom/publication_readiness.v1.json` executes the accepted status
set, exact evidence output, public limits, bounded body collection, complete-byte comparisons,
closed raster policy, frame and dimension bounds, and all agreement failures. The packaged mirror
-under `crates/blossom/tests/fixtures/` must remain byte-identical.
+under `crates/blossom/tests/fixtures/` must remain byte-identical. Crate integration decoder
+conformance additionally includes structurally complete PNG bodies with corrupted checksum,
+compressed payload, and color type, plus two-frame APNG, animated WebP, a forbidden progressive
+JPEG marker, the historical three-byte-short DQT fixture, and fully stripped or partially truncated
+JPEG entropy with EOI restored, so container parsing or a best-effort decoder cannot create
+evidence.
diff --git a/contracts/operations.toml b/contracts/operations.toml
@@ -37,7 +37,6 @@ public = [
"RadrootsBlossomRasterFormat",
"RadrootsBlossomRasterDimensions",
"RadrootsBlossomAuthoredRasterDimensions",
- "RadrootsBlossomRasterDecodeObservation",
"RadrootsBlossomPublicationReadinessEvidenceDigest",
"RadrootsBlossomPublicationReadinessEvidence",
"RadrootsBlossomAuthorizationAction",
@@ -373,7 +372,6 @@ inputs = [
"RadrootsBlossomBud02UploadObservation",
"RadrootsBlossomBud01HeadObservation",
"RadrootsBlossomBud01GetObservation",
- "RadrootsBlossomRasterDecodeObservation",
]
outputs = ["RadrootsBlossomPublicationReadinessEvidence"]
error_class = "validation_error"
@@ -390,7 +388,6 @@ rust_types = [
"radroots_blossom::RadrootsBlossomBud01HeadObservation",
"radroots_blossom::RadrootsBlossomBud02UploadObservation",
"radroots_blossom::RadrootsBlossomPublicationReadinessEvidence",
- "radroots_blossom::RadrootsBlossomRasterDecodeObservation",
]
[operations.blossom_verify_publication_readiness.conformance]
diff --git a/contracts/releases/1.0.0-alpha.1.toml b/contracts/releases/1.0.0-alpha.1.toml
@@ -461,4 +461,4 @@ semver_impacts = [
"add_enum_variant",
"add_conformance_vector",
]
-summary = "Add transport-neutral BUD-02 plus BUD-01 publication-readiness evidence with bounded complete-byte verification and a closed single-frame JPEG, PNG, and still-WebP raster profile."
+summary = "Add transport-neutral BUD-02 plus BUD-01 publication-readiness evidence with bounded complete-byte verification, exact sequential JPEG entropy accounting plus pinned strict zune-jpeg decoding, and internally authoritative full decoding for static JPEG, PNG, and WebP rasters."
diff --git a/crates/blossom/Cargo.toml b/crates/blossom/Cargo.toml
@@ -16,13 +16,17 @@ readme = "README"
default = ["serde"]
serde = ["dep:serde"]
std = ["serde?/std", "sha2/std", "url_nostd/std"]
+raster-decode = ["std", "dep:image", "dep:zune-core", "dep:zune-jpeg"]
[dependencies]
+image = { workspace = true, optional = true }
mediatype = { workspace = true }
serde = { workspace = true, optional = true }
sha2 = { workspace = true }
unicode-general-category = { workspace = true }
url_nostd = { workspace = true }
+zune-core = { workspace = true, optional = true }
+zune-jpeg = { workspace = true, optional = true }
[dev-dependencies]
hex = { workspace = true }
diff --git a/crates/blossom/README b/crates/blossom/README
@@ -18,13 +18,25 @@ feature, and kind `24242` is never a relay-publication event.
Publication readiness is a separate typestate. It accepts only BUD-02 status
`200`/`201`, successful BUD-01 `HEAD`/`GET` observations, a body bounded by its
-declared size and the public `10,485,760`-byte maximum, and an exact decoder
-observation for one JPEG, PNG, or still-WebP frame within the public dimension
-and pixel limits. The crate checks complete-byte, URL, hash, MIME, length,
-container, frame, and dimension agreement and emits deterministic per-URL
-evidence. It still performs no HTTP or image decoding: an owning runtime must
-supply transport results and an approved decoder observation over the exact
-complete body.
+declared size and the public `10,485,760`-byte maximum, and one fully decodable
+static JPEG, PNG, or WebP raster within the public dimension and pixel limits.
+With the non-default `raster-decode` feature, the crate forces the decoder from
+the exact declared MIME type. JPEG first passes an internal sequential entropy
+validator that accounts for every MCU, Huffman symbol, magnitude bit, pad bit,
+restart marker, and frame component without synthesizing missing input. The
+same bytes are then fully decoded to RGB pixels by exactly pinned `zune-jpeg`
+`0.5.15` and `zune-core` `0.5.1` in strict mode with unsafe intrinsics disabled.
+The profile accepts only 8-bit sequential SOF0/SOF1 JPEG; progressive SOF2 and
+every other process are rejected. `image` `0.25.10` is enabled only for PNG and WebP. The profile
+rejects PNG and WebP animation, bounds decoded output to `160,000,000` bytes
+before allocation, decodes the complete body, and derives dimensions
+internally. It checks complete-byte, URL, hash, MIME, length, container,
+animation, and dimension agreement before emitting deterministic per-URL
+evidence. The crate performs no HTTP: an owning runtime supplies only the
+transport observations and exact complete body. The portable `no_std` core
+remains available without `raster-decode`, but cannot construct readiness
+evidence. Policy v1 has no serialized or caller-supplied decode-observation
+input.
Protocol behavior is pinned to Blossom commit
`b5bd2801d1763aa635fc8fea7a76597e0eb18990`:
diff --git a/crates/blossom/src/error.rs b/crates/blossom/src/error.rs
@@ -64,12 +64,13 @@ pub enum RadrootsBlossomError {
PublicationRetrievedBytesMismatch,
UnsupportedPublicationRasterMediaType,
InvalidPublicationRaster,
- PublicationRasterFrameCountMismatch { actual: u32 },
+ PublicationJpegProcessForbidden,
+ PublicationRasterAnimationForbidden,
PublicationRasterDimensionsOutOfRange { width: u32, height: u32 },
PublicationRasterPixelLimitExceeded { pixels: u64 },
- PublicationRasterDecodeFormatMismatch,
- PublicationRasterDecodeLengthMismatch { expected: u64, actual: u64 },
- PublicationRasterDecodeHashMismatch,
+ PublicationRasterDecodedByteLimitExceeded { decoded: u64, maximum: u64 },
+ PublicationRasterDecodeAllocationFailed,
+ PublicationRasterDecodeFailed,
PublicationRasterContainerDimensionMismatch,
PublicationAuthoredRasterDimensionMismatch,
}
@@ -150,22 +151,21 @@ impl RadrootsBlossomError {
"unsupported_publication_raster_media_type"
}
Self::InvalidPublicationRaster => "invalid_publication_raster",
- Self::PublicationRasterFrameCountMismatch { .. } => {
- "publication_raster_frame_count_mismatch"
- }
+ Self::PublicationJpegProcessForbidden => "publication_jpeg_process_forbidden",
+ Self::PublicationRasterAnimationForbidden => "publication_raster_animation_forbidden",
Self::PublicationRasterDimensionsOutOfRange { .. } => {
"publication_raster_dimensions_out_of_range"
}
Self::PublicationRasterPixelLimitExceeded { .. } => {
"publication_raster_pixel_limit_exceeded"
}
- Self::PublicationRasterDecodeFormatMismatch => {
- "publication_raster_decode_format_mismatch"
+ Self::PublicationRasterDecodedByteLimitExceeded { .. } => {
+ "publication_raster_decoded_byte_limit_exceeded"
}
- Self::PublicationRasterDecodeLengthMismatch { .. } => {
- "publication_raster_decode_length_mismatch"
+ Self::PublicationRasterDecodeAllocationFailed => {
+ "publication_raster_decode_allocation_failed"
}
- Self::PublicationRasterDecodeHashMismatch => "publication_raster_decode_hash_mismatch",
+ Self::PublicationRasterDecodeFailed => "publication_raster_decode_failed",
Self::PublicationRasterContainerDimensionMismatch => {
"publication_raster_container_dimension_mismatch"
}
@@ -349,10 +349,12 @@ impl fmt::Display for RadrootsBlossomError {
Self::InvalidPublicationRaster => {
f.write_str("publication raster container is malformed or incomplete")
}
- Self::PublicationRasterFrameCountMismatch { actual } => write!(
- f,
- "publication raster must contain exactly one frame, got {actual}"
- ),
+ Self::PublicationJpegProcessForbidden => {
+ f.write_str("publication JPEG must use an 8-bit sequential SOF0 or SOF1 process")
+ }
+ Self::PublicationRasterAnimationForbidden => {
+ f.write_str("publication raster animation is forbidden")
+ }
Self::PublicationRasterDimensionsOutOfRange { width, height } => write!(
f,
"publication raster dimensions must be within 1..=16384, got {width}x{height}"
@@ -361,15 +363,15 @@ impl fmt::Display for RadrootsBlossomError {
f,
"publication raster pixel count {pixels} exceeds 20000000"
),
- Self::PublicationRasterDecodeFormatMismatch => {
- f.write_str("decoded raster format does not match the exact media type")
- }
- Self::PublicationRasterDecodeLengthMismatch { expected, actual } => write!(
+ Self::PublicationRasterDecodedByteLimitExceeded { decoded, maximum } => write!(
f,
- "decoded raster byte length mismatch: expected {expected}, got {actual}"
+ "publication raster requires {decoded} decoded bytes, exceeding maximum {maximum}"
),
- Self::PublicationRasterDecodeHashMismatch => {
- f.write_str("decoded raster complete-byte hash does not match the authored hash")
+ Self::PublicationRasterDecodeAllocationFailed => {
+ f.write_str("publication raster decoded-pixel buffer allocation failed")
+ }
+ Self::PublicationRasterDecodeFailed => {
+ f.write_str("publication raster bitstream could not be decoded completely")
}
Self::PublicationRasterContainerDimensionMismatch => f.write_str(
"decoded raster dimensions do not match the dimensions encoded by the container",
@@ -455,18 +457,19 @@ mod tests {
RadrootsBlossomError::PublicationRetrievedBytesMismatch,
RadrootsBlossomError::UnsupportedPublicationRasterMediaType,
RadrootsBlossomError::InvalidPublicationRaster,
- RadrootsBlossomError::PublicationRasterFrameCountMismatch { actual: 2 },
+ RadrootsBlossomError::PublicationJpegProcessForbidden,
+ RadrootsBlossomError::PublicationRasterAnimationForbidden,
RadrootsBlossomError::PublicationRasterDimensionsOutOfRange {
width: 0,
height: 1,
},
RadrootsBlossomError::PublicationRasterPixelLimitExceeded { pixels: 20_000_001 },
- RadrootsBlossomError::PublicationRasterDecodeFormatMismatch,
- RadrootsBlossomError::PublicationRasterDecodeLengthMismatch {
- expected: 1,
- actual: 2,
+ RadrootsBlossomError::PublicationRasterDecodedByteLimitExceeded {
+ decoded: 2,
+ maximum: 1,
},
- RadrootsBlossomError::PublicationRasterDecodeHashMismatch,
+ RadrootsBlossomError::PublicationRasterDecodeAllocationFailed,
+ RadrootsBlossomError::PublicationRasterDecodeFailed,
RadrootsBlossomError::PublicationRasterContainerDimensionMismatch,
RadrootsBlossomError::PublicationAuthoredRasterDimensionMismatch,
RadrootsBlossomError::InvalidAuthorizationContent,
diff --git a/crates/blossom/src/lib.rs b/crates/blossom/src/lib.rs
@@ -26,16 +26,19 @@ pub use descriptor::{
};
pub use error::RadrootsBlossomError;
pub use hash::{RadrootsBlossomFileExtension, RadrootsBlossomHashPath, RadrootsBlossomSha256};
+#[cfg(feature = "raster-decode")]
+pub use publication_readiness::verify_publication_readiness;
pub use publication_readiness::{
RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_BYTES,
+ RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DECODED_BYTES,
RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DIMENSION,
RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_PIXELS,
RADROOTS_BLOSSOM_PUBLICATION_READINESS_POLICY_VERSION, RadrootsBlossomAuthoredRasterDimensions,
RadrootsBlossomBud01GetCollector, RadrootsBlossomBud01GetObservation,
RadrootsBlossomBud01HeadObservation, RadrootsBlossomBud02UploadObservation,
RadrootsBlossomBud02UploadStatus, RadrootsBlossomPublicationReadinessEvidence,
- RadrootsBlossomPublicationReadinessEvidenceDigest, RadrootsBlossomRasterDecodeObservation,
- RadrootsBlossomRasterDimensions, RadrootsBlossomRasterFormat, verify_publication_readiness,
+ RadrootsBlossomPublicationReadinessEvidenceDigest, RadrootsBlossomRasterDimensions,
+ RadrootsBlossomRasterFormat,
};
pub use url::{RadrootsBlossomApprovedBlobUrl, RadrootsBlossomBlobUrl};
diff --git a/crates/blossom/src/publication_readiness.rs b/crates/blossom/src/publication_readiness.rs
@@ -1,20 +1,39 @@
use alloc::vec::Vec;
use core::fmt;
+#[cfg(feature = "raster-decode")]
+use image::{
+ ImageDecoder, Limits,
+ codecs::{png::PngDecoder, webp::WebPDecoder},
+};
+#[cfg(feature = "raster-decode")]
use sha2::{Digest, Sha256};
-
+#[cfg(feature = "raster-decode")]
+use std::io::Cursor;
+#[cfg(feature = "raster-decode")]
+use zune_core::{bytestream::ZCursor, colorspace::ColorSpace, options::DecoderOptions};
+#[cfg(feature = "raster-decode")]
+use zune_jpeg::JpegDecoder as StrictJpegDecoder;
+
+#[cfg(feature = "raster-decode")]
+mod sequential_jpeg;
+
+#[cfg(feature = "raster-decode")]
+use crate::RadrootsBlossomByteVerifiedDescriptor;
use crate::{
- RadrootsBlossomApprovedBlobUrl, RadrootsBlossomBlobDescriptor,
- RadrootsBlossomByteVerifiedDescriptor, RadrootsBlossomError, RadrootsBlossomMediaType,
- RadrootsBlossomSha256,
+ RadrootsBlossomApprovedBlobUrl, RadrootsBlossomBlobDescriptor, RadrootsBlossomError,
+ RadrootsBlossomMediaType, RadrootsBlossomSha256,
};
const _: () = assert!(usize::BITS <= u64::BITS);
pub const RADROOTS_BLOSSOM_PUBLICATION_READINESS_POLICY_VERSION: u16 = 1;
pub const RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_BYTES: u64 = 10_485_760;
+pub const RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DECODED_BYTES: u64 =
+ RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_PIXELS * 8;
pub const RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DIMENSION: u32 = 16_384;
pub const RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_PIXELS: u64 = 20_000_000;
+#[cfg(feature = "raster-decode")]
const READINESS_EVIDENCE_DIGEST_DOMAIN: &[u8] =
b"radroots.blossom.publication-readiness-evidence.v1\0";
@@ -68,6 +87,7 @@ impl RadrootsBlossomRasterFormat {
}
}
+ #[cfg(feature = "raster-decode")]
const fn digest_code(self) -> u8 {
match self {
Self::Jpeg => 1,
@@ -127,6 +147,7 @@ pub enum RadrootsBlossomAuthoredRasterDimensions {
}
impl RadrootsBlossomAuthoredRasterDimensions {
+ #[cfg(feature = "raster-decode")]
const fn exact(self) -> Option<RadrootsBlossomRasterDimensions> {
match self {
Self::Unspecified => None,
@@ -135,53 +156,6 @@ impl RadrootsBlossomAuthoredRasterDimensions {
}
}
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsBlossomRasterDecodeObservation {
- format: RadrootsBlossomRasterFormat,
- complete_bytes_sha256: RadrootsBlossomSha256,
- complete_byte_length: u64,
- dimensions: RadrootsBlossomRasterDimensions,
-}
-
-impl RadrootsBlossomRasterDecodeObservation {
- pub fn new(
- format: RadrootsBlossomRasterFormat,
- complete_bytes_sha256: RadrootsBlossomSha256,
- complete_byte_length: u64,
- frame_count: u32,
- width: u32,
- height: u32,
- ) -> Result<Self, RadrootsBlossomError> {
- if frame_count != 1 {
- return Err(RadrootsBlossomError::PublicationRasterFrameCountMismatch {
- actual: frame_count,
- });
- }
- Ok(Self {
- format,
- complete_bytes_sha256,
- complete_byte_length,
- dimensions: RadrootsBlossomRasterDimensions::new(width, height)?,
- })
- }
-
- pub const fn format(&self) -> RadrootsBlossomRasterFormat {
- self.format
- }
-
- pub const fn complete_bytes_sha256(&self) -> RadrootsBlossomSha256 {
- self.complete_bytes_sha256
- }
-
- pub const fn complete_byte_length(&self) -> u64 {
- self.complete_byte_length
- }
-
- pub const fn dimensions(&self) -> RadrootsBlossomRasterDimensions {
- self.dimensions
- }
-}
-
/// A successful BUD-02 response descriptor observed by a transport adapter.
///
/// Construction accepts only status 200 or 201 and applies the public URL
@@ -427,6 +401,7 @@ impl RadrootsBlossomPublicationReadinessEvidence {
}
}
+#[cfg(feature = "raster-decode")]
pub fn verify_publication_readiness(
authored_descriptor: &RadrootsBlossomByteVerifiedDescriptor,
exact_authored_bytes: &[u8],
@@ -434,7 +409,6 @@ pub fn verify_publication_readiness(
upload: &RadrootsBlossomBud02UploadObservation,
head: &RadrootsBlossomBud01HeadObservation,
get: &RadrootsBlossomBud01GetObservation,
- decode: &RadrootsBlossomRasterDecodeObservation,
) -> Result<RadrootsBlossomPublicationReadinessEvidence, RadrootsBlossomError> {
let expected_url = authored_descriptor.url();
let expected_hash = authored_descriptor.sha256();
@@ -497,36 +471,18 @@ pub fn verify_publication_readiness(
actual: get.declared_size(),
});
}
- let retrieved_hash = RadrootsBlossomSha256::digest(get.bytes());
- if retrieved_hash != expected_hash {
- return Err(RadrootsBlossomError::PublicationRetrievedBytesHashMismatch);
- }
- if get.bytes() != exact_authored_bytes {
- return Err(RadrootsBlossomError::PublicationRetrievedBytesMismatch);
- }
+ validate_retrieved_body(
+ expected_hash,
+ exact_authored_bytes,
+ get.bytes(),
+ RadrootsBlossomSha256::digest(get.bytes()),
+ )?;
let expected_format = RadrootsBlossomRasterFormat::from_media_type(expected_media_type)?;
- let container_dimensions = validate_raster_container(get.bytes(), expected_format)?;
- if decode.format() != expected_format {
- return Err(RadrootsBlossomError::PublicationRasterDecodeFormatMismatch);
- }
- if decode.complete_byte_length() != expected_size {
- return Err(
- RadrootsBlossomError::PublicationRasterDecodeLengthMismatch {
- expected: expected_size,
- actual: decode.complete_byte_length(),
- },
- );
- }
- if decode.complete_bytes_sha256() != expected_hash {
- return Err(RadrootsBlossomError::PublicationRasterDecodeHashMismatch);
- }
- if container_dimensions.is_some_and(|dimensions| dimensions != decode.dimensions()) {
- return Err(RadrootsBlossomError::PublicationRasterContainerDimensionMismatch);
- }
+ let decoded_dimensions = decode_raster(get.bytes(), expected_format)?;
if authored_dimensions
.exact()
- .is_some_and(|dimensions| dimensions != decode.dimensions())
+ .is_some_and(|dimensions| dimensions != decoded_dimensions)
{
return Err(RadrootsBlossomError::PublicationAuthoredRasterDimensionMismatch);
}
@@ -534,7 +490,7 @@ pub fn verify_publication_readiness(
let evidence_digest = evidence_digest(
authored_descriptor,
expected_format,
- decode.dimensions(),
+ decoded_dimensions,
upload,
);
Ok(RadrootsBlossomPublicationReadinessEvidence {
@@ -543,13 +499,190 @@ pub fn verify_publication_readiness(
size: expected_size,
media_type: expected_media_type.clone(),
raster_format: expected_format,
- dimensions: decode.dimensions(),
+ dimensions: decoded_dimensions,
bud02_status: upload.status(),
uploaded: upload_descriptor.uploaded(),
evidence_digest,
})
}
+#[cfg(feature = "raster-decode")]
+fn decode_raster(
+ bytes: &[u8],
+ format: RadrootsBlossomRasterFormat,
+) -> Result<RadrootsBlossomRasterDimensions, RadrootsBlossomError> {
+ match format {
+ RadrootsBlossomRasterFormat::Jpeg => {
+ let container = inspect_jpeg_container(bytes)?;
+ decode_complete_jpeg(bytes, container)
+ }
+ RadrootsBlossomRasterFormat::Png => {
+ let container = inspect_png_container(bytes)?;
+ let decoder = PngDecoder::with_limits(Cursor::new(bytes), raster_decode_limits())
+ .map_err(|_| RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ let decoder_animated = decoder
+ .is_apng()
+ .map_err(|_| RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ reject_animation(container.animated, decoder_animated)?;
+ decode_complete_raster(decoder, container.dimensions)
+ }
+ RadrootsBlossomRasterFormat::StillWebP => {
+ let container = inspect_webp_container(bytes)?;
+ let decoder = WebPDecoder::new(Cursor::new(bytes))
+ .map_err(|_| RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ reject_animation(container.animated, decoder.has_animation())?;
+ decode_complete_raster(decoder, container.dimensions)
+ }
+ }
+}
+
+#[cfg(feature = "raster-decode")]
+fn decode_complete_jpeg(
+ bytes: &[u8],
+ container: JpegContainerInspection,
+) -> Result<RadrootsBlossomRasterDimensions, RadrootsBlossomError> {
+ sequential_jpeg::validate(bytes, container)?;
+ let mut decoder =
+ StrictJpegDecoder::new_with_options(ZCursor::new(bytes), strict_jpeg_decoder_options());
+ decoder
+ .decode_headers()
+ .map_err(|_| RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ let dimensions = strict_jpeg_dimensions(decoder.dimensions())?;
+ require_matching_dimensions(dimensions, container.dimensions)?;
+ let decoded_bytes = bounded_jpeg_output_buffer_size(decoder.output_buffer_size())?;
+ let mut decoded = allocate_decoded_buffer(decoded_bytes)?;
+ decoder
+ .decode_into(&mut decoded)
+ .map_err(|_| RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ Ok(dimensions)
+}
+
+#[cfg(feature = "raster-decode")]
+fn strict_jpeg_decoder_options() -> DecoderOptions {
+ DecoderOptions::default()
+ .set_strict_mode(true)
+ .set_use_unsafe(false)
+ .set_max_width(RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DIMENSION as usize)
+ .set_max_height(RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DIMENSION as usize)
+ .jpeg_set_out_colorspace(ColorSpace::RGB)
+}
+
+#[cfg(feature = "raster-decode")]
+fn strict_jpeg_dimensions(
+ dimensions: Option<(usize, usize)>,
+) -> Result<RadrootsBlossomRasterDimensions, RadrootsBlossomError> {
+ let (width, height) = dimensions.ok_or(RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ let width =
+ u32::try_from(width).map_err(|_| RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ let height =
+ u32::try_from(height).map_err(|_| RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ RadrootsBlossomRasterDimensions::new(width, height)
+}
+
+#[cfg(feature = "raster-decode")]
+fn decode_complete_raster<D: ImageDecoder>(
+ mut decoder: D,
+ container_dimensions: RadrootsBlossomRasterDimensions,
+) -> Result<RadrootsBlossomRasterDimensions, RadrootsBlossomError> {
+ let (width, height) = decoder.dimensions();
+ let dimensions = RadrootsBlossomRasterDimensions::new(width, height)?;
+ require_matching_dimensions(dimensions, container_dimensions)?;
+
+ decoder
+ .set_limits(raster_decode_limits())
+ .map_err(|_| RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ let decoded_bytes = bounded_decoded_byte_length(Some(decoder.total_bytes()))?;
+ let mut decoded = allocate_decoded_buffer(decoded_bytes)?;
+ decoder
+ .read_image(&mut decoded)
+ .map_err(|_| RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ Ok(dimensions)
+}
+
+#[cfg(feature = "raster-decode")]
+fn require_matching_dimensions(
+ decoded: RadrootsBlossomRasterDimensions,
+ container: RadrootsBlossomRasterDimensions,
+) -> Result<(), RadrootsBlossomError> {
+ if decoded != container {
+ return Err(RadrootsBlossomError::PublicationRasterContainerDimensionMismatch);
+ }
+ Ok(())
+}
+
+#[cfg(feature = "raster-decode")]
+fn bounded_jpeg_output_buffer_size(
+ decoded_bytes: Option<usize>,
+) -> Result<u64, RadrootsBlossomError> {
+ bounded_decoded_byte_length(decoded_bytes.map(|decoded_bytes| decoded_bytes as u64))
+}
+
+#[cfg(feature = "raster-decode")]
+fn bounded_decoded_byte_length(decoded_bytes: Option<u64>) -> Result<u64, RadrootsBlossomError> {
+ let decoded_bytes = decoded_bytes.ok_or(RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ if decoded_bytes > RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DECODED_BYTES {
+ return Err(
+ RadrootsBlossomError::PublicationRasterDecodedByteLimitExceeded {
+ decoded: decoded_bytes,
+ maximum: RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DECODED_BYTES,
+ },
+ );
+ }
+ Ok(decoded_bytes)
+}
+
+#[cfg(feature = "raster-decode")]
+fn reject_animation(
+ container_animated: bool,
+ decoder_animated: bool,
+) -> Result<(), RadrootsBlossomError> {
+ if container_animated || decoder_animated {
+ return Err(RadrootsBlossomError::PublicationRasterAnimationForbidden);
+ }
+ Ok(())
+}
+
+#[cfg(feature = "raster-decode")]
+fn allocate_decoded_buffer(decoded_bytes: u64) -> Result<Vec<u8>, RadrootsBlossomError> {
+ #[cfg(target_pointer_width = "64")]
+ let decoded_length = decoded_bytes as usize;
+ #[cfg(not(target_pointer_width = "64"))]
+ let decoded_length = usize::try_from(decoded_bytes)
+ .map_err(|_| RadrootsBlossomError::PublicationRasterDecodeAllocationFailed)?;
+ let mut decoded = Vec::new();
+ decoded
+ .try_reserve_exact(decoded_length)
+ .map_err(|_| RadrootsBlossomError::PublicationRasterDecodeAllocationFailed)?;
+ decoded.resize(decoded_length, 0);
+ Ok(decoded)
+}
+
+#[cfg(feature = "raster-decode")]
+fn raster_decode_limits() -> Limits {
+ let mut limits = Limits::default();
+ limits.max_image_width = Some(RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DIMENSION);
+ limits.max_image_height = Some(RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DIMENSION);
+ limits.max_alloc = Some(RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DECODED_BYTES);
+ limits
+}
+
+#[cfg(feature = "raster-decode")]
+fn validate_retrieved_body(
+ expected_hash: RadrootsBlossomSha256,
+ exact_authored_bytes: &[u8],
+ retrieved_bytes: &[u8],
+ retrieved_hash: RadrootsBlossomSha256,
+) -> Result<(), RadrootsBlossomError> {
+ if retrieved_hash != expected_hash {
+ return Err(RadrootsBlossomError::PublicationRetrievedBytesHashMismatch);
+ }
+ if retrieved_bytes != exact_authored_bytes {
+ return Err(RadrootsBlossomError::PublicationRetrievedBytesMismatch);
+ }
+ Ok(())
+}
+
+#[cfg(feature = "raster-decode")]
fn evidence_digest(
descriptor: &RadrootsBlossomByteVerifiedDescriptor,
format: RadrootsBlossomRasterFormat,
@@ -576,29 +709,33 @@ fn evidence_digest(
RadrootsBlossomPublicationReadinessEvidenceDigest(RadrootsBlossomSha256::from_bytes(bytes))
}
+#[cfg(feature = "raster-decode")]
fn update_length_prefixed(hasher: &mut Sha256, bytes: &[u8]) {
hasher.update((bytes.len() as u64).to_be_bytes());
hasher.update(bytes);
}
-fn validate_raster_container(
- bytes: &[u8],
- format: RadrootsBlossomRasterFormat,
-) -> Result<Option<RadrootsBlossomRasterDimensions>, RadrootsBlossomError> {
- match format {
- RadrootsBlossomRasterFormat::Jpeg => validate_jpeg_container(bytes).map(Some),
- RadrootsBlossomRasterFormat::Png => validate_png_container(bytes).map(Some),
- RadrootsBlossomRasterFormat::StillWebP => validate_webp_container(bytes),
- }
+#[cfg(any(feature = "raster-decode", test))]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+struct RasterContainerInspection {
+ dimensions: RadrootsBlossomRasterDimensions,
+ animated: bool,
+}
+
+#[cfg(any(feature = "raster-decode", test))]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+struct JpegContainerInspection {
+ dimensions: RadrootsBlossomRasterDimensions,
+ components: u8,
}
+#[cfg(any(feature = "raster-decode", test))]
fn invalid_raster<T>() -> Result<T, RadrootsBlossomError> {
Err(RadrootsBlossomError::InvalidPublicationRaster)
}
-fn validate_png_container(
- bytes: &[u8],
-) -> Result<RadrootsBlossomRasterDimensions, RadrootsBlossomError> {
+#[cfg(any(feature = "raster-decode", test))]
+fn inspect_png_container(bytes: &[u8]) -> Result<RasterContainerInspection, RadrootsBlossomError> {
const SIGNATURE: &[u8; 8] = b"\x89PNG\r\n\x1a\n";
if !bytes.starts_with(SIGNATURE) {
return invalid_raster();
@@ -606,6 +743,7 @@ fn validate_png_container(
let mut position = SIGNATURE.len();
let mut dimensions = None;
let mut has_image_data = false;
+ let mut animated = false;
while position < bytes.len() {
let header_end = position
.checked_add(8)
@@ -652,13 +790,12 @@ fn validate_png_container(
}
b"IHDR" => return invalid_raster(),
b"IDAT" if dimensions.is_some() => has_image_data = true,
- b"acTL" | b"fcTL" | b"fdAT" => {
- return Err(RadrootsBlossomError::PublicationRasterFrameCountMismatch {
- actual: 2,
- });
- }
+ b"acTL" | b"fcTL" | b"fdAT" => animated = true,
b"IEND" if data.is_empty() && has_image_data && position == bytes.len() => {
- return dimensions.ok_or(RadrootsBlossomError::InvalidPublicationRaster);
+ return Ok(RasterContainerInspection {
+ dimensions: dimensions.ok_or(RadrootsBlossomError::InvalidPublicationRaster)?,
+ animated,
+ });
}
b"IEND" => return invalid_raster(),
_ if dimensions.is_none() => return invalid_raster(),
@@ -668,9 +805,8 @@ fn validate_png_container(
invalid_raster()
}
-fn validate_webp_container(
- bytes: &[u8],
-) -> Result<Option<RadrootsBlossomRasterDimensions>, RadrootsBlossomError> {
+#[cfg(any(feature = "raster-decode", test))]
+fn inspect_webp_container(bytes: &[u8]) -> Result<RasterContainerInspection, RadrootsBlossomError> {
if bytes.len() < 20 || &bytes[..4] != b"RIFF" || &bytes[8..12] != b"WEBP" {
return invalid_raster();
}
@@ -686,6 +822,7 @@ fn validate_webp_container(
let mut position = 12_usize;
let mut dimensions = None;
let mut primary_chunks = 0_u8;
+ let mut animated = false;
while position < bytes.len() {
let header_end = position
.checked_add(8)
@@ -716,17 +853,9 @@ fn validate_webp_container(
position = padded_end;
match &kind {
- b"ANIM" | b"ANMF" => {
- return Err(RadrootsBlossomError::PublicationRasterFrameCountMismatch {
- actual: 2,
- });
- }
+ b"ANIM" | b"ANMF" => animated = true,
b"VP8X" if data.len() == 10 => {
- if data[0] & 0b0000_0010 != 0 {
- return Err(RadrootsBlossomError::PublicationRasterFrameCountMismatch {
- actual: 2,
- });
- }
+ animated |= data[0] & 0b0000_0010 != 0;
let width = 1 + read_u24_le(&data[4..7]);
let height = 1 + read_u24_le(&data[7..10]);
dimensions = Some(RadrootsBlossomRasterDimensions::new(width, height)?);
@@ -774,26 +903,33 @@ fn validate_webp_container(
_ => {}
}
}
- if position != bytes.len() || primary_chunks != 1 {
+ if primary_chunks == 0 {
+ if !animated {
+ return invalid_raster();
+ }
+ } else if primary_chunks != 1 {
return invalid_raster();
}
- Ok(dimensions)
+ Ok(RasterContainerInspection {
+ dimensions: dimensions.ok_or(RadrootsBlossomError::InvalidPublicationRaster)?,
+ animated,
+ })
}
+#[cfg(any(feature = "raster-decode", test))]
fn read_u24_le(bytes: &[u8]) -> u32 {
u32::from(bytes[0]) | (u32::from(bytes[1]) << 8) | (u32::from(bytes[2]) << 16)
}
-fn validate_jpeg_container(
- bytes: &[u8],
-) -> Result<RadrootsBlossomRasterDimensions, RadrootsBlossomError> {
+#[cfg(any(feature = "raster-decode", test))]
+fn inspect_jpeg_container(bytes: &[u8]) -> Result<JpegContainerInspection, RadrootsBlossomError> {
if bytes.len() < 4 || !bytes.starts_with(b"\xff\xd8") {
return invalid_raster();
}
let mut position = 2_usize;
let mut dimensions = None;
+ let mut components = None;
loop {
- let marker_start = position;
if bytes.get(position) != Some(&0xff) {
return invalid_raster();
}
@@ -806,7 +942,10 @@ fn validate_jpeg_container(
position += 1;
match marker {
0xd9 if position == bytes.len() => {
- return dimensions.ok_or(RadrootsBlossomError::InvalidPublicationRaster);
+ return Ok(JpegContainerInspection {
+ dimensions: dimensions.ok_or(RadrootsBlossomError::InvalidPublicationRaster)?,
+ components: components.ok_or(RadrootsBlossomError::InvalidPublicationRaster)?,
+ });
}
0xd9 | 0x00 | 0xd8 | 0xd0..=0xd7 => return invalid_raster(),
0x01 => continue,
@@ -840,20 +979,28 @@ fn validate_jpeg_container(
if dimensions.is_some() || data.len() < 6 {
return invalid_raster();
}
+ if !matches!(marker, 0xc0 | 0xc1) || data[0] != 8 {
+ return Err(RadrootsBlossomError::PublicationJpegProcessForbidden);
+ }
+ let component_count = data[5];
+ if !matches!(component_count, 1 | 3 | 4)
+ || data.len() != 6 + 3 * usize::from(component_count)
+ {
+ return invalid_raster();
+ }
let height = u32::from(u16::from_be_bytes([data[1], data[2]]));
let width = u32::from(u16::from_be_bytes([data[3], data[4]]));
dimensions = Some(RadrootsBlossomRasterDimensions::new(width, height)?);
+ components = Some(component_count);
}
if marker == 0xda {
position = jpeg_scan_end(bytes, position)?;
- if position <= marker_start {
- return invalid_raster();
- }
}
}
}
+#[cfg(any(feature = "raster-decode", test))]
fn is_jpeg_start_of_frame(marker: u8) -> bool {
matches!(
marker,
@@ -861,6 +1008,7 @@ fn is_jpeg_start_of_frame(marker: u8) -> bool {
)
}
+#[cfg(any(feature = "raster-decode", test))]
fn jpeg_scan_end(bytes: &[u8], mut position: usize) -> Result<usize, RadrootsBlossomError> {
while position < bytes.len() {
if bytes[position] != 0xff {
@@ -883,15 +1031,51 @@ fn jpeg_scan_end(bytes: &[u8], mut position: usize) -> Result<usize, RadrootsBlo
}
#[cfg(test)]
+fn validate_png_container(
+ bytes: &[u8],
+) -> Result<RadrootsBlossomRasterDimensions, RadrootsBlossomError> {
+ static_container_dimensions(inspect_png_container(bytes)?)
+}
+
+#[cfg(test)]
+fn validate_webp_container(
+ bytes: &[u8],
+) -> Result<Option<RadrootsBlossomRasterDimensions>, RadrootsBlossomError> {
+ static_container_dimensions(inspect_webp_container(bytes)?).map(Some)
+}
+
+#[cfg(test)]
+fn validate_jpeg_container(
+ bytes: &[u8],
+) -> Result<RadrootsBlossomRasterDimensions, RadrootsBlossomError> {
+ Ok(inspect_jpeg_container(bytes)?.dimensions)
+}
+
+#[cfg(test)]
+fn static_container_dimensions(
+ inspection: RasterContainerInspection,
+) -> Result<RadrootsBlossomRasterDimensions, RadrootsBlossomError> {
+ if inspection.animated {
+ return Err(RadrootsBlossomError::PublicationRasterAnimationForbidden);
+ }
+ Ok(inspection.dimensions)
+}
+
+#[cfg(test)]
mod tests {
use super::*;
+ use crate::RadrootsBlossomByteVerifiedDescriptor;
+ #[cfg(feature = "raster-decode")]
+ use alloc::boxed::Box;
use alloc::{format, string::ToString};
+ #[cfg(feature = "raster-decode")]
+ use image::{ColorType, ImageError, ImageResult};
const PNG: &[u8] = &[
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f,
0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x60,
- 0xf8, 0xcf, 0xf0, 0x00, 0x00, 0x04, 0x01, 0x01, 0x00, 0x18, 0xdd, 0x8d, 0xb1, 0x00, 0x00,
+ 0xf8, 0xcf, 0xf0, 0x00, 0x00, 0x03, 0xe2, 0x01, 0xe0, 0x38, 0x10, 0xac, 0x1e, 0x00, 0x00,
0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
];
@@ -905,6 +1089,51 @@ mod tests {
0, 0, 0x2f, 0, 0, 0, 0, 0,
];
+ #[cfg(feature = "raster-decode")]
+ fn sequential_jpeg() -> Vec<u8> {
+ hex::decode(
+ "ffd8ffe000104a46494600010100000100010000ffdb0043000302020302020303030304030304050805050404050a070706080c0a0c0c0b0a0b0b0d0e12100d0e110e0b0b1016101113141515150c0f171816141812141514ffdb00430103040405040509050509140d0b0d1414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414ffc00011080001000103012200021101031101ffc4001f0000010501010101010100000000000000000102030405060708090a0bffc400b5100002010303020403050504040000017d01020300041105122131410613516107227114328191a1082342b1c11552d1f02433627282090a161718191a25262728292a3435363738393a434445464748494a535455565758595a636465666768696a737475767778797a838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae1e2e3e4e5e6e7e8e9eaf1f2f3f4f5f6f7f8f9faffc4001f0100030101010101010101010000000000000102030405060708090a0bffc400b51100020102040403040705040400010277000102031104052131061241510761711322328108144291a1b1c109233352f0156272d10a162434e125f11718191a262728292a35363738393a434445464748494a535455565758595a636465666768696a737475767778797a82838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f3f4f5f6f7f8f9faffda000c03010002110311003f00f9ca8a28afc3cfe6f3ffd9",
+ )
+ .unwrap()
+ }
+
+ #[cfg(feature = "raster-decode")]
+ fn malformed_dqt_jpeg() -> Vec<u8> {
+ let mut jpeg = sequential_jpeg();
+ let sof = jpeg
+ .windows(2)
+ .position(|window| window == b"\xff\xc0")
+ .unwrap();
+ jpeg.drain(sof - 3..sof);
+ jpeg
+ }
+
+ fn png_with_chunks(chunks: &[([u8; 4], &[u8])]) -> Vec<u8> {
+ let mut output = b"\x89PNG\r\n\x1a\n".to_vec();
+ for (kind, data) in chunks {
+ output.extend_from_slice(&(data.len() as u32).to_be_bytes());
+ output.extend_from_slice(kind);
+ output.extend_from_slice(data);
+ output.extend_from_slice(&[0; 4]);
+ }
+ output
+ }
+
+ fn webp_with_chunks(chunks: &[([u8; 4], &[u8])]) -> Vec<u8> {
+ let mut output = b"RIFF\0\0\0\0WEBP".to_vec();
+ for (kind, data) in chunks {
+ output.extend_from_slice(kind);
+ output.extend_from_slice(&(data.len() as u32).to_le_bytes());
+ output.extend_from_slice(data);
+ if data.len() & 1 == 1 {
+ output.push(0);
+ }
+ }
+ let riff_size = (output.len() as u32) - 8;
+ output[4..8].copy_from_slice(&riff_size.to_le_bytes());
+ output
+ }
+
fn descriptor(bytes: &[u8], media_type: &str, origin: &str) -> RadrootsBlossomBlobDescriptor {
let hash = RadrootsBlossomSha256::digest(bytes);
RadrootsBlossomBlobDescriptor::new(
@@ -932,7 +1161,6 @@ mod tests {
RadrootsBlossomBud02UploadObservation,
RadrootsBlossomBud01HeadObservation,
RadrootsBlossomBud01GetObservation,
- RadrootsBlossomRasterDecodeObservation,
) {
let expected = verified(bytes);
let upload = RadrootsBlossomBud02UploadObservation::new(
@@ -956,22 +1184,14 @@ mod tests {
bytes,
)
.unwrap();
- let decode = RadrootsBlossomRasterDecodeObservation::new(
- RadrootsBlossomRasterFormat::Png,
- RadrootsBlossomSha256::digest(bytes),
- bytes.len() as u64,
- 1,
- 1,
- 1,
- )
- .unwrap();
- (upload, head, get, decode)
+ (upload, head, get)
}
+ #[cfg(feature = "raster-decode")]
#[test]
fn publication_readiness_accepts_exact_complete_observations() {
let expected = verified(PNG);
- let (upload, head, get, decode) = observations(PNG);
+ let (upload, head, get) = observations(PNG);
let evidence = verify_publication_readiness(
&expected,
PNG,
@@ -981,12 +1201,11 @@ mod tests {
&upload,
&head,
&get,
- &decode,
)
.unwrap();
assert_eq!(
evidence.url().as_str(),
- "https://cdn.example/0d1c097e006a87476e84014ba5842f04c725ed2fc5a081743ab2b5bf13a538b9.png"
+ "https://cdn.example/4490130851783ff662845f5e72f1948618cc87f951f00f6c2ffb3dc01f3f40fd.png"
);
assert_eq!(evidence.sha256(), RadrootsBlossomSha256::digest(PNG));
assert_eq!(evidence.size(), PNG.len() as u64);
@@ -997,7 +1216,7 @@ mod tests {
assert_eq!(evidence.uploaded(), 1_800_000_000);
assert_eq!(
evidence.evidence_digest().to_string(),
- "c52edeba688fa36c7963a478a35ff78504d7dd79a637c67f93d5acb635110660"
+ "44e63303e594ea42d863be995b23ac4297ed77e4378d0707c94f28e77164bd3b"
);
assert_eq!(
evidence.evidence_digest().as_sha256().to_string(),
@@ -1049,7 +1268,7 @@ mod tests {
}
#[test]
- fn observation_constructors_reject_invalid_status_frames_and_dimensions() {
+ fn observation_constructors_reject_invalid_status_and_dimensions() {
assert_eq!(
RadrootsBlossomBud02UploadObservation::new(
204,
@@ -1071,22 +1290,14 @@ mod tests {
.code(),
"invalid_bud01_head_status"
);
- for (frames, width, height, code) in [
- (2, 1, 1, "publication_raster_frame_count_mismatch"),
- (1, 0, 1, "publication_raster_dimensions_out_of_range"),
- (1, 5_000, 5_000, "publication_raster_pixel_limit_exceeded"),
+ for (width, height, code) in [
+ (0, 1, "publication_raster_dimensions_out_of_range"),
+ (5_000, 5_000, "publication_raster_pixel_limit_exceeded"),
] {
assert_eq!(
- RadrootsBlossomRasterDecodeObservation::new(
- RadrootsBlossomRasterFormat::Png,
- RadrootsBlossomSha256::digest(PNG),
- PNG.len() as u64,
- frames,
- width,
- height,
- )
- .unwrap_err()
- .code(),
+ RadrootsBlossomRasterDimensions::new(width, height)
+ .unwrap_err()
+ .code(),
code
);
}
@@ -1105,6 +1316,69 @@ mod tests {
}
#[test]
+ fn raster_dimensions_reject_each_axis_boundary() {
+ for (width, height) in [
+ (0, 1),
+ (1, 0),
+ (RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DIMENSION + 1, 1),
+ (1, RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DIMENSION + 1),
+ ] {
+ assert_eq!(
+ RadrootsBlossomRasterDimensions::new(width, height)
+ .unwrap_err()
+ .code(),
+ "publication_raster_dimensions_out_of_range"
+ );
+ }
+ }
+
+ #[cfg(feature = "raster-decode")]
+ #[test]
+ fn readiness_rejects_oversized_authored_bytes_and_digest_collisions() {
+ let oversized = alloc::vec![
+ 0_u8;
+ RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_BYTES as usize + 1
+ ];
+ let oversized_descriptor = verified(&oversized);
+ let (upload, head, get) = observations(PNG);
+ assert_eq!(
+ verify_publication_readiness(
+ &oversized_descriptor,
+ &oversized,
+ RadrootsBlossomAuthoredRasterDimensions::Unspecified,
+ &upload,
+ &head,
+ &get,
+ )
+ .unwrap_err()
+ .code(),
+ "publication_raster_byte_limit_exceeded"
+ );
+
+ let expected_hash = RadrootsBlossomSha256::digest(PNG);
+ let mut different_bytes = PNG.to_vec();
+ different_bytes[0] ^= 1;
+ assert_eq!(
+ validate_retrieved_body(expected_hash, PNG, &different_bytes, expected_hash)
+ .unwrap_err()
+ .code(),
+ "publication_retrieved_bytes_mismatch"
+ );
+ assert_eq!(
+ validate_retrieved_body(
+ expected_hash,
+ PNG,
+ PNG,
+ RadrootsBlossomSha256::digest(b"different"),
+ )
+ .unwrap_err()
+ .code(),
+ "publication_retrieved_bytes_hash_mismatch"
+ );
+ validate_retrieved_body(expected_hash, PNG, PNG, expected_hash).unwrap();
+ }
+
+ #[test]
fn closed_container_validation_accepts_each_format() {
assert_eq!(
validate_png_container(PNG).unwrap(),
@@ -1132,7 +1406,7 @@ mod tests {
);
assert_eq!(
validate_png_container(&apng).unwrap_err().code(),
- "publication_raster_frame_count_mismatch"
+ "publication_raster_animation_forbidden"
);
assert_eq!(
validate_png_container(b"not png").unwrap_err().code(),
@@ -1153,7 +1427,7 @@ mod tests {
];
assert_eq!(
validate_webp_container(&animated_webp).unwrap_err().code(),
- "publication_raster_frame_count_mismatch"
+ "publication_raster_animation_forbidden"
);
animated_webp[4] = 21;
assert_eq!(
@@ -1179,6 +1453,232 @@ mod tests {
}
#[test]
+ fn png_container_rejects_each_chunk_order_and_termination_failure() {
+ let ihdr = &PNG[16..29];
+ let idat = &PNG[41..54];
+
+ let mut short_header = PNG[..8].to_vec();
+ short_header.push(0);
+ for malformed in [PNG[..8].to_vec(), short_header, PNG[..30].to_vec()] {
+ assert_eq!(
+ validate_png_container(&malformed).unwrap_err().code(),
+ "invalid_publication_raster"
+ );
+ }
+
+ let short_ihdr = png_with_chunks(&[(*b"IHDR", &ihdr[..12])]);
+ let duplicate_ihdr = png_with_chunks(&[(*b"IHDR", ihdr), (*b"IHDR", ihdr)]);
+ let idat_before_ihdr = png_with_chunks(&[(*b"IDAT", idat)]);
+ let animation_before_ihdr = png_with_chunks(&[(*b"acTL", &[0; 8]), (*b"IHDR", ihdr)]);
+ for malformed in [
+ short_ihdr,
+ duplicate_ihdr,
+ idat_before_ihdr,
+ animation_before_ihdr,
+ ] {
+ assert_eq!(
+ validate_png_container(&malformed).unwrap_err().code(),
+ "invalid_publication_raster"
+ );
+ }
+
+ let with_ancillary = png_with_chunks(&[
+ (*b"IHDR", ihdr),
+ (*b"tEXt", b"key\0value"),
+ (*b"IDAT", idat),
+ (*b"IEND", &[]),
+ ]);
+ assert_eq!(
+ validate_png_container(&with_ancillary).unwrap(),
+ RadrootsBlossomRasterDimensions::new(1, 1).unwrap()
+ );
+
+ let nonempty_iend =
+ png_with_chunks(&[(*b"IHDR", ihdr), (*b"IDAT", idat), (*b"IEND", &[0])]);
+ let no_image_data = png_with_chunks(&[(*b"IHDR", ihdr), (*b"IEND", &[])]);
+ let mut trailing = png_with_chunks(&[(*b"IHDR", ihdr), (*b"IDAT", idat), (*b"IEND", &[])]);
+ trailing.push(0);
+ let missing_iend = png_with_chunks(&[(*b"IHDR", ihdr), (*b"IDAT", idat)]);
+ for malformed in [nonempty_iend, no_image_data, trailing, missing_iend] {
+ assert_eq!(
+ validate_png_container(&malformed).unwrap_err().code(),
+ "invalid_publication_raster"
+ );
+ }
+ }
+
+ #[test]
+ fn webp_container_covers_extended_lossless_and_lossy_boundaries() {
+ let vp8x_1x1 = [0_u8; 10];
+ let mut vp8x_2x1 = vp8x_1x1;
+ vp8x_2x1[4] = 1;
+ let mut vp8x_animated = vp8x_1x1;
+ vp8x_animated[0] = 0x02;
+ let vp8l_1x1 = [0x2f, 0, 0, 0, 0];
+ let vp8_1x1 = [0, 0, 0, 0x9d, 0x01, 0x2a, 1, 0, 1, 0];
+
+ let mut bad_riff = STILL_WEBP.to_vec();
+ bad_riff[0] = b'X';
+ let mut bad_webp = STILL_WEBP.to_vec();
+ bad_webp[8] = b'X';
+ for malformed in [bad_riff, bad_webp] {
+ assert_eq!(
+ validate_webp_container(&malformed).unwrap_err().code(),
+ "invalid_publication_raster"
+ );
+ }
+
+ let mut missing_padding = webp_with_chunks(&[(*b"VP8L", &vp8l_1x1)]);
+ missing_padding.pop();
+ let riff_size = (missing_padding.len() as u32) - 8;
+ missing_padding[4..8].copy_from_slice(&riff_size.to_le_bytes());
+ assert_eq!(
+ validate_webp_container(&missing_padding)
+ .unwrap_err()
+ .code(),
+ "invalid_publication_raster"
+ );
+
+ for animated_kind in [*b"ANIM", *b"ANMF"] {
+ assert_eq!(
+ validate_webp_container(&webp_with_chunks(&[
+ (*b"VP8X", &vp8x_animated),
+ (animated_kind, &[]),
+ ]))
+ .unwrap_err()
+ .code(),
+ "publication_raster_animation_forbidden"
+ );
+ }
+
+ let extended_lossless = webp_with_chunks(&[(*b"VP8X", &vp8x_1x1), (*b"VP8L", &vp8l_1x1)]);
+ assert_eq!(
+ validate_webp_container(&extended_lossless).unwrap(),
+ Some(RadrootsBlossomRasterDimensions::new(1, 1).unwrap())
+ );
+ assert_eq!(
+ validate_webp_container(&webp_with_chunks(&[(*b"VP8X", &[0; 9])]))
+ .unwrap_err()
+ .code(),
+ "invalid_publication_raster"
+ );
+ assert_eq!(
+ validate_webp_container(&webp_with_chunks(&[(*b"VP8L", &[0; 5])]))
+ .unwrap_err()
+ .code(),
+ "invalid_publication_raster"
+ );
+ assert_eq!(
+ validate_webp_container(&webp_with_chunks(&[
+ (*b"VP8X", &vp8x_2x1),
+ (*b"VP8L", &vp8l_1x1),
+ ]))
+ .unwrap_err()
+ .code(),
+ "publication_raster_container_dimension_mismatch"
+ );
+
+ assert_eq!(
+ validate_webp_container(&webp_with_chunks(&[(*b"VP8 ", &vp8_1x1)])).unwrap(),
+ Some(RadrootsBlossomRasterDimensions::new(1, 1).unwrap())
+ );
+ let mut bad_vp8_signature = vp8_1x1;
+ bad_vp8_signature[3] = 0;
+ assert_eq!(
+ validate_webp_container(&webp_with_chunks(&[(*b"VP8 ", &bad_vp8_signature)]))
+ .unwrap_err()
+ .code(),
+ "invalid_publication_raster"
+ );
+ assert_eq!(
+ validate_webp_container(&webp_with_chunks(&[
+ (*b"VP8X", &vp8x_2x1),
+ (*b"VP8 ", &vp8_1x1),
+ ]))
+ .unwrap_err()
+ .code(),
+ "publication_raster_container_dimension_mismatch"
+ );
+
+ let with_unknown = webp_with_chunks(&[(*b"JUNK", &[0; 2]), (*b"VP8L", &vp8l_1x1)]);
+ assert_eq!(
+ validate_webp_container(&with_unknown).unwrap(),
+ Some(RadrootsBlossomRasterDimensions::new(1, 1).unwrap())
+ );
+ for malformed in [
+ webp_with_chunks(&[(*b"JUNK", &[0; 8])]),
+ webp_with_chunks(&[(*b"VP8L", &vp8l_1x1), (*b"VP8L", &vp8l_1x1)]),
+ webp_with_chunks(&[(*b"VP8L", &[0x2f; 4])]),
+ webp_with_chunks(&[(*b"VP8 ", &[0; 9])]),
+ ] {
+ assert_eq!(
+ validate_webp_container(&malformed).unwrap_err().code(),
+ "invalid_publication_raster"
+ );
+ }
+
+ let large_bits = 0x3fff_u32 | (0x3fff_u32 << 14);
+ let mut large_vp8l = [0_u8; 5];
+ large_vp8l[0] = 0x2f;
+ large_vp8l[1..].copy_from_slice(&large_bits.to_le_bytes());
+ assert_eq!(
+ validate_webp_container(&webp_with_chunks(&[(*b"VP8L", &large_vp8l)]))
+ .unwrap_err()
+ .code(),
+ "publication_raster_pixel_limit_exceeded"
+ );
+ }
+
+ #[test]
+ fn jpeg_container_covers_marker_segment_and_scan_boundaries() {
+ let tiny = [0xff, 0xd8];
+ let bad_marker = [0xff, 0xd8, 0x00, 0x00];
+ let short_segment_length = [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x01];
+ let short_sof = [
+ 0xff, 0xd8, 0xff, 0xc0, 0x00, 0x07, 0x08, 0x00, 0x01, 0x00, 0x01,
+ ];
+ let invalid_component_count = [
+ 0xff, 0xd8, 0xff, 0xc0, 0x00, 0x0e, 0x08, 0x00, 0x01, 0x00, 0x01, 0x02, 0x01, 0x11,
+ 0x00, 0x02, 0x11, 0x00,
+ ];
+ let mismatched_component_length = [
+ 0xff, 0xd8, 0xff, 0xc0, 0x00, 0x0e, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11,
+ 0x00, 0x02, 0x11, 0x00,
+ ];
+ for malformed in [
+ tiny.as_slice(),
+ bad_marker.as_slice(),
+ short_segment_length.as_slice(),
+ short_sof.as_slice(),
+ invalid_component_count.as_slice(),
+ mismatched_component_length.as_slice(),
+ &JPEG[..JPEG.len() - 2],
+ &[0xff, 0xd8, 0xff],
+ &[0xff, 0xd8, 0xff, 0xe0, 0x00],
+ &[0xff, 0xd8, 0xff, 0xe0, 0x00, 0x05, 0x00],
+ &[0xff, 0xd8, 0xff, 0xd9],
+ ] {
+ assert_eq!(
+ validate_jpeg_container(malformed).unwrap_err().code(),
+ "invalid_publication_raster"
+ );
+ }
+
+ let temporal_marker = [&JPEG[..2], &[0xff, 0x01], &JPEG[2..]].concat();
+ assert_eq!(
+ validate_jpeg_container(&temporal_marker).unwrap(),
+ RadrootsBlossomRasterDimensions::new(1, 1).unwrap()
+ );
+
+ let mut stuffed_and_restart = JPEG[..JPEG.len() - 2].to_vec();
+ stuffed_and_restart.extend_from_slice(&[0xff, 0x00, 0xff, 0xd0, 0xff, 0xd9]);
+ assert_eq!(
+ validate_jpeg_container(&stuffed_and_restart).unwrap(),
+ RadrootsBlossomRasterDimensions::new(1, 1).unwrap()
+ );
+ }
+
+ #[test]
fn get_debug_redacts_complete_body() {
let get = observations(PNG).2;
let debug = format!("{get:?}");
@@ -1186,4 +1686,256 @@ mod tests {
assert!(!debug.contains("89504e47"));
assert_eq!(get.bytes(), PNG);
}
+
+ #[cfg(feature = "raster-decode")]
+ struct FakeDecoder {
+ dimensions: (u32, u32),
+ total_bytes: u64,
+ fail_limits: bool,
+ fail_read: bool,
+ }
+
+ #[cfg(feature = "raster-decode")]
+ impl ImageDecoder for FakeDecoder {
+ fn dimensions(&self) -> (u32, u32) {
+ self.dimensions
+ }
+
+ fn color_type(&self) -> ColorType {
+ ColorType::Rgba8
+ }
+
+ fn total_bytes(&self) -> u64 {
+ self.total_bytes
+ }
+
+ fn read_image(self, _buffer: &mut [u8]) -> ImageResult<()> {
+ if self.fail_read {
+ return Err(ImageError::IoError(std::io::Error::other(
+ "synthetic decode failure",
+ )));
+ }
+ Ok(())
+ }
+
+ fn read_image_boxed(self: Box<Self>, buffer: &mut [u8]) -> ImageResult<()> {
+ (*self).read_image(buffer)
+ }
+
+ fn set_limits(&mut self, _limits: Limits) -> ImageResult<()> {
+ if self.fail_limits {
+ return Err(ImageError::IoError(std::io::Error::other(
+ "synthetic limit failure",
+ )));
+ }
+ Ok(())
+ }
+ }
+
+ #[cfg(feature = "raster-decode")]
+ #[test]
+ fn decoder_authority_rejects_animation_resource_and_agreement_failures() {
+ reject_animation(false, false).unwrap();
+ for (container_animated, decoder_animated) in [(true, false), (false, true), (true, true)] {
+ assert_eq!(
+ reject_animation(container_animated, decoder_animated)
+ .unwrap_err()
+ .code(),
+ "publication_raster_animation_forbidden"
+ );
+ }
+
+ let dimensions = RadrootsBlossomRasterDimensions::new(1, 1).unwrap();
+ let decoder = |dimensions, total_bytes, fail_limits, fail_read| FakeDecoder {
+ dimensions,
+ total_bytes,
+ fail_limits,
+ fail_read,
+ };
+ assert_eq!(
+ decode_complete_raster(decoder((2, 1), 0, false, false), dimensions)
+ .unwrap_err()
+ .code(),
+ "publication_raster_container_dimension_mismatch"
+ );
+ assert_eq!(
+ decode_complete_raster(decoder((1, 1), 0, false, false), dimensions).unwrap(),
+ dimensions
+ );
+ assert_eq!(
+ decode_complete_raster(
+ decoder(
+ (1, 1),
+ RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DECODED_BYTES + 1,
+ false,
+ false,
+ ),
+ dimensions,
+ )
+ .unwrap_err()
+ .code(),
+ "publication_raster_decoded_byte_limit_exceeded"
+ );
+ assert_eq!(
+ decode_complete_raster(decoder((1, 1), 0, true, false), dimensions)
+ .unwrap_err()
+ .code(),
+ "publication_raster_decode_failed"
+ );
+ assert_eq!(
+ decode_complete_raster(decoder((1, 1), 0, false, true), dimensions)
+ .unwrap_err()
+ .code(),
+ "publication_raster_decode_failed"
+ );
+ assert_eq!(
+ allocate_decoded_buffer(u64::MAX).unwrap_err().code(),
+ "publication_raster_decode_allocation_failed"
+ );
+ assert_eq!(
+ bounded_decoded_byte_length(None).unwrap_err().code(),
+ "publication_raster_decode_failed"
+ );
+ assert_eq!(
+ bounded_decoded_byte_length(Some(
+ RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DECODED_BYTES + 1,
+ ))
+ .unwrap_err()
+ .code(),
+ "publication_raster_decoded_byte_limit_exceeded"
+ );
+ assert_eq!(
+ bounded_decoded_byte_length(Some(
+ RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DECODED_BYTES,
+ ))
+ .unwrap(),
+ RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DECODED_BYTES
+ );
+ assert_eq!(
+ bounded_jpeg_output_buffer_size(None).unwrap_err().code(),
+ "publication_raster_decode_failed"
+ );
+ assert_eq!(bounded_jpeg_output_buffer_size(Some(0)).unwrap(), 0);
+
+ let direct_decoder = decoder((1, 1), 0, false, false);
+ assert_eq!(direct_decoder.color_type(), ColorType::Rgba8);
+ Box::new(decoder((1, 1), 0, false, false))
+ .read_image_boxed(&mut [])
+ .unwrap();
+
+ let jpeg = sequential_jpeg();
+ let container = inspect_jpeg_container(&jpeg).unwrap();
+ assert_eq!(container.dimensions, dimensions);
+ assert_eq!(container.components, 3);
+ decode_complete_jpeg(&jpeg, container).unwrap();
+
+ let mut extended_sequential = jpeg.clone();
+ let sof = extended_sequential
+ .windows(2)
+ .position(|window| window == b"\xff\xc0")
+ .unwrap();
+ extended_sequential[sof + 1] = 0xc1;
+ let extended_container = inspect_jpeg_container(&extended_sequential).unwrap();
+ decode_complete_jpeg(&extended_sequential, extended_container).unwrap();
+
+ let mut progressive = jpeg.clone();
+ progressive[sof + 1] = 0xc2;
+ assert_eq!(
+ inspect_jpeg_container(&progressive).unwrap_err().code(),
+ "publication_jpeg_process_forbidden"
+ );
+ let mut twelve_bit = jpeg.clone();
+ twelve_bit[sof + 4] = 12;
+ assert_eq!(
+ inspect_jpeg_container(&twelve_bit).unwrap_err().code(),
+ "publication_jpeg_process_forbidden"
+ );
+ assert_eq!(
+ inspect_jpeg_container(&malformed_dqt_jpeg())
+ .unwrap_err()
+ .code(),
+ "invalid_publication_raster"
+ );
+
+ let scan = jpeg
+ .windows(2)
+ .position(|window| window == b"\xff\xda")
+ .unwrap();
+ let scan_length = usize::from(u16::from_be_bytes([jpeg[scan + 2], jpeg[scan + 3]]));
+ let entropy_start = scan + 2 + scan_length;
+ let entropy_end = jpeg.len() - 2;
+ let entropy_length = entropy_end - entropy_start;
+ for keep in [0, 1, entropy_length / 2, entropy_length - 1] {
+ let mut truncated = jpeg[..entropy_start + keep].to_vec();
+ truncated.extend_from_slice(b"\xff\xd9");
+ let truncated_container = inspect_jpeg_container(&truncated).unwrap();
+ assert_eq!(
+ decode_complete_jpeg(&truncated, truncated_container)
+ .unwrap_err()
+ .code(),
+ "publication_raster_decode_failed"
+ );
+ }
+
+ let mismatched_container = JpegContainerInspection {
+ dimensions: RadrootsBlossomRasterDimensions::new(2, 1).unwrap(),
+ ..container
+ };
+ assert_eq!(
+ decode_complete_jpeg(&jpeg, mismatched_container)
+ .unwrap_err()
+ .code(),
+ "publication_raster_container_dimension_mismatch"
+ );
+ assert_eq!(
+ decode_complete_jpeg(b"not jpeg", container)
+ .unwrap_err()
+ .code(),
+ "publication_raster_decode_failed"
+ );
+ assert_eq!(
+ decode_complete_jpeg(
+ &jpeg,
+ JpegContainerInspection {
+ components: 2,
+ ..container
+ },
+ )
+ .unwrap_err()
+ .code(),
+ "publication_raster_container_dimension_mismatch"
+ );
+
+ let jpeg_options = strict_jpeg_decoder_options();
+ assert!(jpeg_options.strict_mode());
+ assert!(!jpeg_options.use_unsafe());
+ assert_eq!(
+ jpeg_options.max_width(),
+ RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DIMENSION as usize
+ );
+ assert_eq!(
+ jpeg_options.max_height(),
+ RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DIMENSION as usize
+ );
+ assert_eq!(jpeg_options.jpeg_get_out_colorspace(), ColorSpace::RGB);
+ assert_eq!(strict_jpeg_dimensions(Some((1, 1))).unwrap(), dimensions);
+ assert_eq!(
+ strict_jpeg_dimensions(None).unwrap_err().code(),
+ "publication_raster_decode_failed"
+ );
+
+ let limits = raster_decode_limits();
+ assert_eq!(
+ limits.max_image_width,
+ Some(RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DIMENSION)
+ );
+ assert_eq!(
+ limits.max_image_height,
+ Some(RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DIMENSION)
+ );
+ assert_eq!(
+ limits.max_alloc,
+ Some(RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DECODED_BYTES)
+ );
+ }
}
diff --git a/crates/blossom/src/publication_readiness/sequential_jpeg.rs b/crates/blossom/src/publication_readiness/sequential_jpeg.rs
@@ -0,0 +1,1244 @@
+use alloc::vec::Vec;
+
+use super::{
+ JpegContainerInspection, RadrootsBlossomError, RadrootsBlossomRasterDimensions,
+ is_jpeg_start_of_frame,
+};
+
+#[derive(Clone, Copy)]
+struct SequentialJpegComponent {
+ id: u8,
+ horizontal_sampling: u8,
+ vertical_sampling: u8,
+}
+
+struct SequentialJpegFrame {
+ dimensions: RadrootsBlossomRasterDimensions,
+ components: Vec<SequentialJpegComponent>,
+ maximum_horizontal_sampling: u8,
+ maximum_vertical_sampling: u8,
+}
+
+struct SequentialJpegScanComponent {
+ frame_index: usize,
+ dc_table: usize,
+ ac_table: usize,
+}
+
+struct SequentialJpegScan {
+ components: Vec<SequentialJpegScanComponent>,
+}
+
+struct SequentialJpegHuffmanTable {
+ first_codes: [u32; 16],
+ value_offsets: [usize; 16],
+ code_counts: [u8; 16],
+ values: Vec<u8>,
+}
+
+impl SequentialJpegHuffmanTable {
+ fn new(class: u8, code_counts: [u8; 16], values: &[u8]) -> Result<Self, RadrootsBlossomError> {
+ if class > 1 {
+ return invalid_entropy();
+ }
+ if values.is_empty() || values.len() > 256 {
+ return invalid_entropy();
+ }
+
+ let mut first_codes = [0_u32; 16];
+ let mut value_offsets = [0_usize; 16];
+ let mut code = 0_u32;
+ let mut value_offset = 0_usize;
+ let mut unused_codes = 1_i32;
+ for (index, count) in code_counts.iter().copied().enumerate() {
+ unused_codes = unused_codes * 2 - i32::from(count);
+ if unused_codes < 0 {
+ return invalid_entropy();
+ }
+ first_codes[index] = code;
+ value_offsets[index] = value_offset;
+ code = (code + u32::from(count)) * 2;
+ value_offset += usize::from(count);
+ }
+ if value_offset != values.len() {
+ return invalid_entropy();
+ }
+ if unused_codes == 0 {
+ return invalid_entropy();
+ }
+
+ let mut seen_values = [false; 256];
+ for value in values {
+ let value_index = usize::from(*value);
+ let valid = if class == 0 {
+ *value <= 11
+ } else {
+ let run = value >> 4;
+ let magnitude = value & 0x0f;
+ magnitude <= 10 && (magnitude != 0 || matches!(run, 0 | 15))
+ };
+ if !valid {
+ return invalid_entropy();
+ }
+ if seen_values[value_index] {
+ return invalid_entropy();
+ }
+ seen_values[value_index] = true;
+ }
+
+ let mut owned_values = Vec::new();
+ owned_values
+ .try_reserve_exact(values.len())
+ .map_err(|_| RadrootsBlossomError::PublicationRasterDecodeAllocationFailed)?;
+ owned_values.extend_from_slice(values);
+ Ok(Self {
+ first_codes,
+ value_offsets,
+ code_counts,
+ values: owned_values,
+ })
+ }
+
+ fn decode_symbol(
+ &self,
+ reader: &mut SequentialJpegEntropyReader<'_>,
+ ) -> Result<u8, RadrootsBlossomError> {
+ let mut code = 0_u32;
+ for index in 0..16 {
+ code = (code << 1) | u32::from(reader.read_bit()?);
+ let count = u32::from(self.code_counts[index]);
+ let first = self.first_codes[index];
+ if count != 0 && code >= first && code - first < count {
+ let offset = self.value_offsets[index] + (code - first) as usize;
+ return self
+ .values
+ .get(offset)
+ .copied()
+ .ok_or(RadrootsBlossomError::PublicationRasterDecodeFailed);
+ }
+ }
+ invalid_entropy()
+ }
+}
+
+struct SequentialJpegEntropyReader<'a> {
+ bytes: &'a [u8],
+ position: usize,
+ current_byte: u8,
+ bits_remaining: u8,
+}
+
+impl<'a> SequentialJpegEntropyReader<'a> {
+ const fn new(bytes: &'a [u8], position: usize) -> Self {
+ Self {
+ bytes,
+ position,
+ current_byte: 0,
+ bits_remaining: 0,
+ }
+ }
+
+ fn read_bit(&mut self) -> Result<u8, RadrootsBlossomError> {
+ if self.bits_remaining == 0 {
+ self.current_byte = self.read_entropy_byte()?;
+ self.bits_remaining = 8;
+ }
+ self.bits_remaining -= 1;
+ Ok((self.current_byte >> self.bits_remaining) & 1)
+ }
+
+ fn discard_bits(&mut self, count: u8) -> Result<(), RadrootsBlossomError> {
+ for _ in 0..count {
+ self.read_bit()?;
+ }
+ Ok(())
+ }
+
+ fn read_entropy_byte(&mut self) -> Result<u8, RadrootsBlossomError> {
+ let byte = *self
+ .bytes
+ .get(self.position)
+ .ok_or(RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ self.position += 1;
+ if byte != 0xff {
+ return Ok(byte);
+ }
+ if self.bytes.get(self.position) == Some(&0x00) {
+ self.position += 1;
+ return Ok(0xff);
+ }
+ invalid_entropy()
+ }
+
+ fn finish_restart(&mut self, expected: u8) -> Result<(), RadrootsBlossomError> {
+ if expected > 7 {
+ return invalid_entropy();
+ }
+ self.finish_padding()?;
+ let (marker, after_marker) = strict_marker(self.bytes, self.position)?;
+ if marker != 0xd0 + expected {
+ return invalid_entropy();
+ }
+ self.position = after_marker;
+ Ok(())
+ }
+
+ fn finish_scan(mut self) -> Result<usize, RadrootsBlossomError> {
+ self.finish_padding()?;
+ let (marker, _) = strict_marker(self.bytes, self.position)?;
+ if matches!(marker, 0xd0..=0xd7) {
+ return invalid_entropy();
+ }
+ Ok(self.position)
+ }
+
+ fn finish_padding(&mut self) -> Result<(), RadrootsBlossomError> {
+ if self.bits_remaining != 0 {
+ let mask = (1_u16 << self.bits_remaining) - 1;
+ if u16::from(self.current_byte) & mask != mask {
+ return invalid_entropy();
+ }
+ self.bits_remaining = 0;
+ }
+ Ok(())
+ }
+}
+
+pub(super) fn validate(
+ bytes: &[u8],
+ container: JpegContainerInspection,
+) -> Result<(), RadrootsBlossomError> {
+ if !bytes.starts_with(b"\xff\xd8") {
+ return invalid_entropy();
+ }
+ let mut position = 2_usize;
+ let mut frame: Option<SequentialJpegFrame> = None;
+ let mut dc_tables: [Option<SequentialJpegHuffmanTable>; 4] = core::array::from_fn(|_| None);
+ let mut ac_tables: [Option<SequentialJpegHuffmanTable>; 4] = core::array::from_fn(|_| None);
+ let mut restart_interval = 0_usize;
+ let mut seen_components = [false; 4];
+ let mut saw_scan = false;
+ loop {
+ let (marker, after_marker) = strict_marker(bytes, position)?;
+ match marker {
+ 0xd9 => {
+ let current_frame = frame
+ .as_ref()
+ .ok_or(RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ if after_marker != bytes.len() {
+ return invalid_entropy();
+ }
+ if !saw_scan {
+ return invalid_entropy();
+ }
+ if seen_components
+ .iter()
+ .take(current_frame.components.len())
+ .any(|seen| !seen)
+ {
+ return invalid_entropy();
+ }
+ return Ok(());
+ }
+ 0xc0 | 0xc1 => {
+ if frame.is_some() {
+ return invalid_entropy();
+ }
+ let (payload, next) = strict_segment(bytes, after_marker)?;
+ let parsed = parse_frame(payload)?;
+ if parsed.dimensions != container.dimensions
+ || parsed.components.len() != usize::from(container.components)
+ {
+ return Err(RadrootsBlossomError::PublicationRasterContainerDimensionMismatch);
+ }
+ frame = Some(parsed);
+ position = next;
+ }
+ marker if is_jpeg_start_of_frame(marker) || marker == 0xcc => {
+ return Err(RadrootsBlossomError::PublicationJpegProcessForbidden);
+ }
+ 0xc4 => {
+ let (payload, next) = strict_segment(bytes, after_marker)?;
+ parse_huffman_tables(payload, &mut dc_tables, &mut ac_tables)?;
+ position = next;
+ }
+ 0xdd => {
+ let (payload, next) = strict_segment(bytes, after_marker)?;
+ if payload.len() != 2 {
+ return invalid_entropy();
+ }
+ restart_interval = usize::from(u16::from_be_bytes([payload[0], payload[1]]));
+ position = next;
+ }
+ 0xda => {
+ let current_frame = frame
+ .as_ref()
+ .ok_or(RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ let (payload, entropy_start) = strict_segment(bytes, after_marker)?;
+ let scan = parse_scan(
+ payload,
+ current_frame,
+ &seen_components,
+ &dc_tables,
+ &ac_tables,
+ )?;
+ position = validate_scan_entropy(
+ bytes,
+ entropy_start,
+ current_frame,
+ &scan,
+ &dc_tables,
+ &ac_tables,
+ restart_interval,
+ )?;
+ for component in &scan.components {
+ seen_components[component.frame_index] = true;
+ }
+ saw_scan = true;
+ }
+ 0xdb | 0xe0..=0xef | 0xfe => {
+ let (_, next) = strict_segment(bytes, after_marker)?;
+ position = next;
+ }
+ 0x01 => position = after_marker,
+ _ => return invalid_entropy(),
+ }
+ }
+}
+
+fn strict_marker(bytes: &[u8], position: usize) -> Result<(u8, usize), RadrootsBlossomError> {
+ if bytes.get(position) != Some(&0xff) {
+ return invalid_entropy();
+ }
+ let mut code_position = position;
+ while bytes.get(code_position) == Some(&0xff) {
+ code_position += 1;
+ }
+ let marker = *bytes
+ .get(code_position)
+ .ok_or(RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ if marker == 0x00 {
+ return invalid_entropy();
+ }
+ let after_marker = code_position + 1;
+ Ok((marker, after_marker))
+}
+
+fn strict_segment(
+ bytes: &[u8],
+ after_marker: usize,
+) -> Result<(&[u8], usize), RadrootsBlossomError> {
+ let payload_start = after_marker
+ .checked_add(2)
+ .ok_or(RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ let length_bytes = bytes
+ .get(after_marker..payload_start)
+ .ok_or(RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ let length = usize::from(u16::from_be_bytes([length_bytes[0], length_bytes[1]]));
+ if length < 2 {
+ return invalid_entropy();
+ }
+ let end = after_marker
+ .checked_add(length)
+ .ok_or(RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ let payload = bytes
+ .get(payload_start..end)
+ .ok_or(RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ Ok((payload, end))
+}
+
+fn parse_frame(payload: &[u8]) -> Result<SequentialJpegFrame, RadrootsBlossomError> {
+ if payload.len() < 6 || payload[0] != 8 {
+ return invalid_entropy();
+ }
+ let component_count = usize::from(payload[5]);
+ let expected_length = component_count * 3 + 6;
+ if !matches!(component_count, 1 | 3 | 4) || payload.len() != expected_length {
+ return invalid_entropy();
+ }
+ let dimensions = RadrootsBlossomRasterDimensions::new(
+ u32::from(u16::from_be_bytes([payload[3], payload[4]])),
+ u32::from(u16::from_be_bytes([payload[1], payload[2]])),
+ )?;
+ let mut components = Vec::new();
+ components
+ .try_reserve_exact(component_count)
+ .map_err(|_| RadrootsBlossomError::PublicationRasterDecodeAllocationFailed)?;
+ let mut maximum_horizontal_sampling = 0_u8;
+ let mut maximum_vertical_sampling = 0_u8;
+ let mut sampling_product_sum = 0_u8;
+ for data in payload[6..].chunks_exact(3) {
+ let horizontal_sampling = data[1] >> 4;
+ let vertical_sampling = data[1] & 0x0f;
+ if components
+ .iter()
+ .any(|component: &SequentialJpegComponent| component.id == data[0])
+ || !(1..=4).contains(&horizontal_sampling)
+ || !(1..=4).contains(&vertical_sampling)
+ || data[2] > 3
+ {
+ return invalid_entropy();
+ }
+ sampling_product_sum += horizontal_sampling * vertical_sampling;
+ if sampling_product_sum > 10 {
+ return invalid_entropy();
+ }
+ maximum_horizontal_sampling = maximum_horizontal_sampling.max(horizontal_sampling);
+ maximum_vertical_sampling = maximum_vertical_sampling.max(vertical_sampling);
+ components.push(SequentialJpegComponent {
+ id: data[0],
+ horizontal_sampling,
+ vertical_sampling,
+ });
+ }
+ Ok(SequentialJpegFrame {
+ dimensions,
+ components,
+ maximum_horizontal_sampling,
+ maximum_vertical_sampling,
+ })
+}
+
+fn parse_huffman_tables(
+ payload: &[u8],
+ dc_tables: &mut [Option<SequentialJpegHuffmanTable>; 4],
+ ac_tables: &mut [Option<SequentialJpegHuffmanTable>; 4],
+) -> Result<(), RadrootsBlossomError> {
+ let mut position = 0_usize;
+ while position < payload.len() {
+ let selector = *payload
+ .get(position)
+ .ok_or(RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ position += 1;
+ let class = selector >> 4;
+ let destination = usize::from(selector & 0x0f);
+ if class > 1 || destination >= 4 {
+ return invalid_entropy();
+ }
+ let counts_end = position + 16;
+ let counts_slice = payload
+ .get(position..counts_end)
+ .ok_or(RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ let code_counts: [u8; 16] = counts_slice
+ .try_into()
+ .map_err(|_| RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ position = counts_end;
+ let value_count: usize = code_counts.iter().map(|count| usize::from(*count)).sum();
+ if value_count > 256 {
+ return invalid_entropy();
+ }
+ let values_end = position + value_count;
+ let values = payload
+ .get(position..values_end)
+ .ok_or(RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ position = values_end;
+ let table = SequentialJpegHuffmanTable::new(class, code_counts, values)?;
+ if class == 0 {
+ dc_tables[destination] = Some(table);
+ } else {
+ ac_tables[destination] = Some(table);
+ }
+ }
+ Ok(())
+}
+
+fn parse_scan(
+ payload: &[u8],
+ frame: &SequentialJpegFrame,
+ seen_components: &[bool; 4],
+ dc_tables: &[Option<SequentialJpegHuffmanTable>; 4],
+ ac_tables: &[Option<SequentialJpegHuffmanTable>; 4],
+) -> Result<SequentialJpegScan, RadrootsBlossomError> {
+ let component_count = payload.first().copied().map_or(0, usize::from);
+ let expected_length = component_count * 2 + 4;
+ if component_count == 0
+ || component_count > frame.components.len()
+ || payload.len() != expected_length
+ || payload[payload.len() - 3..] != [0, 63, 0]
+ {
+ return invalid_entropy();
+ }
+ let selectors_end = component_count * 2 + 1;
+ let mut components = Vec::new();
+ components
+ .try_reserve_exact(component_count)
+ .map_err(|_| RadrootsBlossomError::PublicationRasterDecodeAllocationFailed)?;
+ for data in payload[1..selectors_end].chunks_exact(2) {
+ let frame_index = frame
+ .components
+ .iter()
+ .position(|component| component.id == data[0])
+ .ok_or(RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ let dc_table = usize::from(data[1] >> 4);
+ let ac_table = usize::from(data[1] & 0x0f);
+ if components
+ .iter()
+ .any(|component: &SequentialJpegScanComponent| component.frame_index == frame_index)
+ || seen_components[frame_index]
+ || dc_table >= 4
+ || ac_table >= 4
+ || dc_tables[dc_table].is_none()
+ || ac_tables[ac_table].is_none()
+ {
+ return invalid_entropy();
+ }
+ components.push(SequentialJpegScanComponent {
+ frame_index,
+ dc_table,
+ ac_table,
+ });
+ }
+ Ok(SequentialJpegScan { components })
+}
+
+#[allow(clippy::too_many_arguments)]
+fn validate_scan_entropy(
+ bytes: &[u8],
+ entropy_start: usize,
+ frame: &SequentialJpegFrame,
+ scan: &SequentialJpegScan,
+ dc_tables: &[Option<SequentialJpegHuffmanTable>; 4],
+ ac_tables: &[Option<SequentialJpegHuffmanTable>; 4],
+ restart_interval: usize,
+) -> Result<usize, RadrootsBlossomError> {
+ let interleaved = scan.components.len() > 1;
+ let mcu_count = scan_mcu_count(frame, scan, interleaved)?;
+ let mut reader = SequentialJpegEntropyReader::new(bytes, entropy_start);
+ let mut expected_restart = 0_u8;
+ for mcu in 0..mcu_count {
+ if restart_interval != 0 && mcu != 0 && mcu % restart_interval == 0 {
+ reader.finish_restart(expected_restart)?;
+ expected_restart = (expected_restart + 1) & 7;
+ }
+ for scan_component in &scan.components {
+ let frame_component = frame
+ .components
+ .get(scan_component.frame_index)
+ .copied()
+ .ok_or(RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ let blocks = if interleaved {
+ usize::from(frame_component.horizontal_sampling)
+ * usize::from(frame_component.vertical_sampling)
+ } else {
+ 1
+ };
+ let dc_table = dc_tables
+ .get(scan_component.dc_table)
+ .and_then(Option::as_ref)
+ .ok_or(RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ let ac_table = ac_tables
+ .get(scan_component.ac_table)
+ .and_then(Option::as_ref)
+ .ok_or(RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ for _ in 0..blocks {
+ validate_block(&mut reader, dc_table, ac_table)?;
+ }
+ }
+ }
+ reader.finish_scan()
+}
+
+fn scan_mcu_count(
+ frame: &SequentialJpegFrame,
+ scan: &SequentialJpegScan,
+ interleaved: bool,
+) -> Result<usize, RadrootsBlossomError> {
+ let width = frame.dimensions.width() as usize;
+ let height = frame.dimensions.height() as usize;
+ if interleaved {
+ let mcu_width = 8 * usize::from(frame.maximum_horizontal_sampling);
+ let mcu_height = 8 * usize::from(frame.maximum_vertical_sampling);
+ return checked_mcu_grid_count(width, height, mcu_width, mcu_height);
+ }
+ let scan_component = scan
+ .components
+ .first()
+ .ok_or(RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ let component = frame
+ .components
+ .get(scan_component.frame_index)
+ .ok_or(RadrootsBlossomError::PublicationRasterDecodeFailed)?;
+ if frame.maximum_horizontal_sampling == 0 || frame.maximum_vertical_sampling == 0 {
+ return invalid_entropy();
+ }
+ let component_width = (width * usize::from(component.horizontal_sampling))
+ .div_ceil(usize::from(frame.maximum_horizontal_sampling));
+ let component_height = (height * usize::from(component.vertical_sampling))
+ .div_ceil(usize::from(frame.maximum_vertical_sampling));
+ checked_mcu_grid_count(component_width, component_height, 8, 8)
+}
+
+fn checked_mcu_grid_count(
+ width: usize,
+ height: usize,
+ mcu_width: usize,
+ mcu_height: usize,
+) -> Result<usize, RadrootsBlossomError> {
+ if width == 0 {
+ return invalid_entropy();
+ }
+ if height == 0 {
+ return invalid_entropy();
+ }
+ if mcu_width == 0 {
+ return invalid_entropy();
+ }
+ if mcu_height == 0 {
+ return invalid_entropy();
+ }
+ width
+ .div_ceil(mcu_width)
+ .checked_mul(height.div_ceil(mcu_height))
+ .ok_or(RadrootsBlossomError::PublicationRasterDecodeFailed)
+}
+
+fn validate_block(
+ reader: &mut SequentialJpegEntropyReader<'_>,
+ dc_table: &SequentialJpegHuffmanTable,
+ ac_table: &SequentialJpegHuffmanTable,
+) -> Result<(), RadrootsBlossomError> {
+ let dc_magnitude = dc_table.decode_symbol(reader)?;
+ reader.discard_bits(dc_magnitude)?;
+ let mut coefficient = 1_usize;
+ while coefficient < 64 {
+ let symbol = ac_table.decode_symbol(reader)?;
+ let run = usize::from(symbol >> 4);
+ let magnitude = symbol & 0x0f;
+ if magnitude == 0 {
+ if run == 0 {
+ break;
+ }
+ coefficient += 16;
+ if coefficient > 64 {
+ return invalid_entropy();
+ }
+ continue;
+ }
+ coefficient += run;
+ if coefficient >= 64 {
+ return invalid_entropy();
+ }
+ reader.discard_bits(magnitude)?;
+ coefficient += 1;
+ }
+ Ok(())
+}
+
+fn invalid_entropy<T>() -> Result<T, RadrootsBlossomError> {
+ Err(RadrootsBlossomError::PublicationRasterDecodeFailed)
+}
+
+#[cfg(test)]
+mod tests {
+ use alloc::{vec, vec::Vec};
+
+ use super::*;
+
+ fn assert_decode_failed<T>(result: Result<T, RadrootsBlossomError>) {
+ let error = result
+ .err()
+ .expect("expected publication raster decode failure");
+ assert_eq!(error.code(), "publication_raster_decode_failed");
+ }
+
+ fn one_symbol_table(class: u8, symbol: u8) -> SequentialJpegHuffmanTable {
+ let mut counts = [0_u8; 16];
+ counts[0] = 1;
+ SequentialJpegHuffmanTable::new(class, counts, &[symbol]).unwrap()
+ }
+
+ fn single_component_frame() -> SequentialJpegFrame {
+ parse_frame(&[8, 0, 1, 0, 1, 1, 1, 0x11, 0]).unwrap()
+ }
+
+ fn one_symbol_tables() -> (
+ [Option<SequentialJpegHuffmanTable>; 4],
+ [Option<SequentialJpegHuffmanTable>; 4],
+ ) {
+ let mut dc_tables = core::array::from_fn(|_| None);
+ dc_tables[0] = Some(one_symbol_table(0, 0));
+ let mut ac_tables = core::array::from_fn(|_| None);
+ ac_tables[0] = Some(one_symbol_table(1, 0));
+ (dc_tables, ac_tables)
+ }
+
+ fn append_segment(bytes: &mut Vec<u8>, marker: u8, payload: &[u8]) {
+ bytes.extend_from_slice(&[0xff, marker]);
+ bytes.extend_from_slice(&u16::try_from(payload.len() + 2).unwrap().to_be_bytes());
+ bytes.extend_from_slice(payload);
+ }
+
+ fn one_component_frame_payload(width: u16) -> [u8; 9] {
+ let [width_high, width_low] = width.to_be_bytes();
+ [8, 0, 1, width_high, width_low, 1, 1, 0x11, 0]
+ }
+
+ fn single_scan_jpeg(frame_payload: &[u8], before_scan: &[u8], entropy: &[u8]) -> Vec<u8> {
+ let mut bytes = vec![0xff, 0xd8];
+ append_segment(&mut bytes, 0xc0, frame_payload);
+
+ let mut huffman = Vec::new();
+ huffman.push(0x00);
+ huffman.extend_from_slice(&[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
+ huffman.push(0);
+ huffman.push(0x10);
+ huffman.extend_from_slice(&[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
+ huffman.push(0);
+ append_segment(&mut bytes, 0xc4, &huffman);
+ bytes.extend_from_slice(before_scan);
+ append_segment(&mut bytes, 0xda, &[1, 1, 0, 0, 63, 0]);
+ bytes.extend_from_slice(entropy);
+ bytes.extend_from_slice(&[0xff, 0xd9]);
+ bytes
+ }
+
+ fn container(width: u32, components: u8) -> JpegContainerInspection {
+ JpegContainerInspection {
+ dimensions: RadrootsBlossomRasterDimensions::new(width, 1).unwrap(),
+ components,
+ }
+ }
+
+ #[test]
+ fn terminal_padding_requires_one_bits_and_no_extra_entropy() {
+ let mut accepted = SequentialJpegEntropyReader::new(&[0x7f], 0);
+ assert_eq!(accepted.read_bit().unwrap(), 0);
+ accepted.finish_padding().unwrap();
+
+ let mut rejected = SequentialJpegEntropyReader::new(&[0x7e], 0);
+ assert_eq!(rejected.read_bit().unwrap(), 0);
+ assert_decode_failed(rejected.finish_padding());
+
+ let mut exact = SequentialJpegEntropyReader::new(&[0x7f, 0xff, 0xd9], 0);
+ exact.read_bit().unwrap();
+ assert_eq!(exact.finish_scan().unwrap(), 1);
+
+ let mut extra = SequentialJpegEntropyReader::new(&[0x7f, 0x00, 0xff, 0xd9], 0);
+ extra.read_bit().unwrap();
+ assert_decode_failed(extra.finish_scan());
+ }
+
+ #[test]
+ fn restart_markers_require_exact_sequence_and_padding() {
+ let mut accepted = SequentialJpegEntropyReader::new(&[0x7f, 0xff, 0xd0], 0);
+ accepted.read_bit().unwrap();
+ accepted.finish_restart(0).unwrap();
+ assert_eq!(accepted.position, 3);
+
+ let mut wrong = SequentialJpegEntropyReader::new(&[0x7f, 0xff, 0xd1], 0);
+ wrong.read_bit().unwrap();
+ assert_decode_failed(wrong.finish_restart(0));
+
+ let mut out_of_range = SequentialJpegEntropyReader::new(&[0xff, 0xd0], 0);
+ assert_decode_failed(out_of_range.finish_restart(8));
+
+ assert_decode_failed(SequentialJpegEntropyReader::new(&[0xff, 0xd0], 0).finish_scan());
+ }
+
+ #[test]
+ fn entropy_reader_and_huffman_symbol_decoding_cover_canonical_boundaries() {
+ let mut stuffed = SequentialJpegEntropyReader::new(&[0xff, 0x00], 0);
+ assert_eq!(stuffed.read_entropy_byte().unwrap(), 0xff);
+ assert_eq!(stuffed.position, 2);
+ assert_decode_failed(SequentialJpegEntropyReader::new(&[], 0).read_entropy_byte());
+ assert_decode_failed(
+ SequentialJpegEntropyReader::new(&[0xff, 0xd9], 0).read_entropy_byte(),
+ );
+
+ let mut counts = [0_u8; 16];
+ counts[1] = 2;
+ let table = SequentialJpegHuffmanTable::new(0, counts, &[7, 8]).unwrap();
+ let mut first = SequentialJpegEntropyReader::new(&[0x3f], 0);
+ assert_eq!(table.decode_symbol(&mut first).unwrap(), 7);
+ let mut second = SequentialJpegEntropyReader::new(&[0x7f], 0);
+ assert_eq!(table.decode_symbol(&mut second).unwrap(), 8);
+ let mut absent = SequentialJpegEntropyReader::new(&[0x80, 0x00], 0);
+ assert_decode_failed(table.decode_symbol(&mut absent));
+
+ let malformed_table = SequentialJpegHuffmanTable {
+ first_codes: [0; 16],
+ value_offsets: [0; 16],
+ code_counts: [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ values: Vec::new(),
+ };
+ let mut missing_value = SequentialJpegEntropyReader::new(&[0x7f], 0);
+ assert_decode_failed(malformed_table.decode_symbol(&mut missing_value));
+
+ let noncanonical_table = SequentialJpegHuffmanTable {
+ first_codes: [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ value_offsets: [0; 16],
+ code_counts: [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ values: vec![0],
+ };
+ let mut code_below_first = SequentialJpegEntropyReader::new(&[0x7f, 0x00], 0);
+ assert_decode_failed(noncanonical_table.decode_symbol(&mut code_below_first));
+ }
+
+ #[test]
+ fn huffman_tables_reject_full_overfull_duplicate_and_oversized_inventories() {
+ assert_decode_failed(SequentialJpegHuffmanTable::new(2, [0; 16], &[0]));
+ assert_decode_failed(SequentialJpegHuffmanTable::new(0, [0; 16], &[]));
+ assert_decode_failed(SequentialJpegHuffmanTable::new(0, [0; 16], &[0; 257]));
+
+ let mut incomplete = [0_u8; 16];
+ incomplete[0] = 1;
+ SequentialJpegHuffmanTable::new(0, incomplete, &[0]).unwrap();
+ assert_decode_failed(SequentialJpegHuffmanTable::new(0, incomplete, &[0, 1]));
+ assert_decode_failed(SequentialJpegHuffmanTable::new(0, incomplete, &[12]));
+
+ let mut valid_ac = [0_u8; 16];
+ valid_ac[1] = 3;
+ SequentialJpegHuffmanTable::new(1, valid_ac, &[0x00, 0xf0, 0x01]).unwrap();
+ assert_decode_failed(SequentialJpegHuffmanTable::new(1, incomplete, &[0x0b]));
+ assert_decode_failed(SequentialJpegHuffmanTable::new(1, incomplete, &[0x10]));
+
+ let mut full = [0_u8; 16];
+ full[0] = 2;
+ assert_decode_failed(SequentialJpegHuffmanTable::new(0, full, &[0, 1]));
+
+ let mut overfull = [0_u8; 16];
+ overfull[0] = 3;
+ assert_decode_failed(SequentialJpegHuffmanTable::new(0, overfull, &[0, 1, 2]));
+
+ let mut duplicate = [0_u8; 16];
+ duplicate[0] = 1;
+ duplicate[1] = 1;
+ assert_decode_failed(SequentialJpegHuffmanTable::new(0, duplicate, &[0, 0]));
+
+ let mut oversized_payload = vec![0_u8; 1 + 16 + 257];
+ oversized_payload[1] = 255;
+ oversized_payload[2] = 2;
+ let mut dc_tables = core::array::from_fn(|_| None);
+ let mut ac_tables = core::array::from_fn(|_| None);
+ assert_decode_failed(parse_huffman_tables(
+ &oversized_payload,
+ &mut dc_tables,
+ &mut ac_tables,
+ ));
+ }
+
+ #[test]
+ fn marker_segment_frame_and_huffman_parsers_reject_every_invalid_shape() {
+ assert_decode_failed(strict_marker(&[0x00], 0));
+ assert_decode_failed(strict_marker(&[0xff], 0));
+ assert_decode_failed(strict_marker(&[0xff, 0x00], 0));
+ assert_eq!(strict_marker(&[0xff, 0xff, 0x01], 0).unwrap(), (0x01, 3));
+
+ assert_decode_failed(strict_segment(&[], 0));
+ assert_decode_failed(strict_segment(&[0, 1], 0));
+ assert_decode_failed(strict_segment(&[0, 4, 0], 0));
+ assert_decode_failed(strict_segment(&[], usize::MAX));
+ assert_eq!(strict_segment(&[0, 3, 9], 0).unwrap(), (&[9][..], 3));
+
+ assert_decode_failed(parse_frame(&[]));
+ assert_decode_failed(parse_frame(&[7, 0, 1, 0, 1, 0]));
+ assert_decode_failed(parse_frame(&[8, 0, 1, 0, 1, 2, 1, 0x11, 0, 2, 0x11, 0]));
+ assert_decode_failed(parse_frame(&[8, 0, 1, 0, 1, 1]));
+ assert_eq!(
+ parse_frame(&[8, 0, 1, 0, 0, 1, 1, 0x11, 0])
+ .err()
+ .unwrap()
+ .code(),
+ "publication_raster_dimensions_out_of_range"
+ );
+ assert_eq!(
+ parse_frame(&[8, 0, 0, 0, 1, 1, 1, 0x11, 0])
+ .err()
+ .unwrap()
+ .code(),
+ "publication_raster_dimensions_out_of_range"
+ );
+
+ let valid = one_component_frame_payload(1);
+ let mut duplicate = [8, 0, 1, 0, 1, 3, 1, 0x11, 0, 1, 0x11, 0, 3, 0x11, 0];
+ assert_decode_failed(parse_frame(&duplicate));
+ duplicate[9] = 2;
+ duplicate[7] = 0x01;
+ assert_decode_failed(parse_frame(&duplicate));
+ duplicate[7] = 0x51;
+ assert_decode_failed(parse_frame(&duplicate));
+ duplicate[7] = 0x10;
+ assert_decode_failed(parse_frame(&duplicate));
+ duplicate[7] = 0x15;
+ assert_decode_failed(parse_frame(&duplicate));
+ duplicate[7] = 0x11;
+ duplicate[8] = 4;
+ assert_decode_failed(parse_frame(&duplicate));
+ parse_frame(&valid).unwrap();
+
+ let mut dc_tables = core::array::from_fn(|_| None);
+ let mut ac_tables = core::array::from_fn(|_| None);
+ assert_decode_failed(parse_huffman_tables(
+ &[0x20],
+ &mut dc_tables,
+ &mut ac_tables,
+ ));
+ assert_decode_failed(parse_huffman_tables(
+ &[0x04],
+ &mut dc_tables,
+ &mut ac_tables,
+ ));
+ assert_decode_failed(parse_huffman_tables(
+ &[0x00],
+ &mut dc_tables,
+ &mut ac_tables,
+ ));
+ let mut missing_value = vec![0x00, 1];
+ missing_value.extend_from_slice(&[0; 15]);
+ assert_decode_failed(parse_huffman_tables(
+ &missing_value,
+ &mut dc_tables,
+ &mut ac_tables,
+ ));
+ parse_huffman_tables(&[], &mut dc_tables, &mut ac_tables).unwrap();
+ }
+
+ #[test]
+ fn scan_parser_requires_exact_components_tables_and_sequential_parameters() {
+ let frame = single_component_frame();
+ let (dc_tables, ac_tables) = one_symbol_tables();
+ let unseen = [false; 4];
+ assert_eq!(
+ parse_scan(
+ &[1, 1, 0, 0, 63, 0],
+ &frame,
+ &unseen,
+ &dc_tables,
+ &ac_tables,
+ )
+ .unwrap()
+ .components
+ .len(),
+ 1
+ );
+ assert_decode_failed(parse_scan(&[], &frame, &unseen, &dc_tables, &ac_tables));
+ assert_decode_failed(parse_scan(
+ &[2, 1, 0, 1, 0, 0, 63, 0],
+ &frame,
+ &unseen,
+ &dc_tables,
+ &ac_tables,
+ ));
+ assert_decode_failed(parse_scan(&[1], &frame, &unseen, &dc_tables, &ac_tables));
+ assert_decode_failed(parse_scan(
+ &[1, 1, 0, 1, 63, 0],
+ &frame,
+ &unseen,
+ &dc_tables,
+ &ac_tables,
+ ));
+ assert_decode_failed(parse_scan(
+ &[1, 2, 0, 0, 63, 0],
+ &frame,
+ &unseen,
+ &dc_tables,
+ &ac_tables,
+ ));
+
+ let three_component_frame =
+ parse_frame(&[8, 0, 1, 0, 1, 3, 1, 0x11, 0, 2, 0x11, 0, 3, 0x11, 0]).unwrap();
+ assert_decode_failed(parse_scan(
+ &[2, 1, 0, 1, 0, 0, 63, 0],
+ &three_component_frame,
+ &unseen,
+ &dc_tables,
+ &ac_tables,
+ ));
+
+ let seen = [true, false, false, false];
+ assert_decode_failed(parse_scan(
+ &[1, 1, 0, 0, 63, 0],
+ &frame,
+ &seen,
+ &dc_tables,
+ &ac_tables,
+ ));
+ assert_decode_failed(parse_scan(
+ &[1, 1, 0x40, 0, 63, 0],
+ &frame,
+ &unseen,
+ &dc_tables,
+ &ac_tables,
+ ));
+ assert_decode_failed(parse_scan(
+ &[1, 1, 0x04, 0, 63, 0],
+ &frame,
+ &unseen,
+ &dc_tables,
+ &ac_tables,
+ ));
+
+ let missing_dc = core::array::from_fn(|_| None);
+ assert_decode_failed(parse_scan(
+ &[1, 1, 0, 0, 63, 0],
+ &frame,
+ &unseen,
+ &missing_dc,
+ &ac_tables,
+ ));
+ let missing_ac = core::array::from_fn(|_| None);
+ assert_decode_failed(parse_scan(
+ &[1, 1, 0, 0, 63, 0],
+ &frame,
+ &unseen,
+ &dc_tables,
+ &missing_ac,
+ ));
+ }
+
+ #[test]
+ fn validator_covers_completion_marker_restart_and_container_agreement_rules() {
+ let frame_payload = one_component_frame_payload(1);
+ let valid = single_scan_jpeg(&frame_payload, &[], &[0x3f]);
+ validate(&valid, container(1, 1)).unwrap();
+ assert_decode_failed(validate(&[0], container(1, 1)));
+ assert_decode_failed(validate(&[0xff, 0xd8, 0xff, 0xd9], container(1, 1)));
+
+ let mut no_scan = vec![0xff, 0xd8];
+ append_segment(&mut no_scan, 0xc0, &frame_payload);
+ no_scan.extend_from_slice(&[0xff, 0xd9]);
+ assert_decode_failed(validate(&no_scan, container(1, 1)));
+
+ let mut trailing = valid.clone();
+ trailing.push(0);
+ assert_decode_failed(validate(&trailing, container(1, 1)));
+
+ let three_component_payload = [8, 0, 1, 0, 1, 3, 1, 0x11, 0, 2, 0x11, 0, 3, 0x11, 0];
+ let missing_components = single_scan_jpeg(&three_component_payload, &[], &[0x3f]);
+ assert_decode_failed(validate(&missing_components, container(1, 3)));
+
+ let mut duplicate_frame = vec![0xff, 0xd8];
+ append_segment(&mut duplicate_frame, 0xc0, &frame_payload);
+ duplicate_frame.extend_from_slice(&valid[2..]);
+ assert_decode_failed(validate(&duplicate_frame, container(1, 1)));
+ assert_eq!(
+ validate(&[0xff, 0xd8, 0xff, 0xcc], container(1, 1))
+ .unwrap_err()
+ .code(),
+ "publication_jpeg_process_forbidden"
+ );
+ assert_eq!(
+ validate(&[0xff, 0xd8, 0xff, 0xc2], container(1, 1))
+ .unwrap_err()
+ .code(),
+ "publication_jpeg_process_forbidden"
+ );
+ assert_decode_failed(validate(&[0xff, 0xd8, 0xff, 0xda], container(1, 1)));
+ assert_decode_failed(validate(
+ &single_scan_jpeg(&frame_payload, &[0xff, 0x02], &[0x3f]),
+ container(1, 1),
+ ));
+ validate(
+ &single_scan_jpeg(&frame_payload, &[0xff, 0x01], &[0x3f]),
+ container(1, 1),
+ )
+ .unwrap();
+
+ assert_eq!(
+ validate(&valid, container(2, 1)).unwrap_err().code(),
+ "publication_raster_container_dimension_mismatch"
+ );
+ assert_eq!(
+ validate(&valid, container(1, 3)).unwrap_err().code(),
+ "publication_raster_container_dimension_mismatch"
+ );
+
+ assert_decode_failed(validate(
+ &single_scan_jpeg(&frame_payload, &[0xff, 0xdd, 0, 3, 0], &[0x3f]),
+ container(1, 1),
+ ));
+ let restart_frame = one_component_frame_payload(9);
+ validate(
+ &single_scan_jpeg(
+ &restart_frame,
+ &[0xff, 0xdd, 0, 4, 0, 1],
+ &[0x3f, 0xff, 0xd0, 0x3f],
+ ),
+ container(9, 1),
+ )
+ .unwrap();
+ validate(
+ &single_scan_jpeg(&restart_frame, &[0xff, 0xdd, 0, 4, 0, 2], &[0x0f]),
+ container(9, 1),
+ )
+ .unwrap();
+ }
+
+ #[test]
+ fn block_validation_covers_eob_zero_runs_nonzero_runs_and_coefficient_limits() {
+ let dc = one_symbol_table(0, 0);
+ let eob = one_symbol_table(1, 0);
+ let mut eob_reader = SequentialJpegEntropyReader::new(&[0x3f], 0);
+ validate_block(&mut eob_reader, &dc, &eob).unwrap();
+
+ let zrl = one_symbol_table(1, 0xf0);
+ let mut zrl_reader = SequentialJpegEntropyReader::new(&[0x07], 0);
+ assert_decode_failed(validate_block(&mut zrl_reader, &dc, &zrl));
+
+ let overflowing_run = one_symbol_table(1, 0xf1);
+ let mut overflowing_reader = SequentialJpegEntropyReader::new(&[0x00], 0);
+ assert_decode_failed(validate_block(
+ &mut overflowing_reader,
+ &dc,
+ &overflowing_run,
+ ));
+
+ let exact_run = one_symbol_table(1, 0x81);
+ let mut exact_reader = SequentialJpegEntropyReader::new(&[0x00, 0x00], 0);
+ validate_block(&mut exact_reader, &dc, &exact_run).unwrap();
+
+ let mut mixed_counts = [0_u8; 16];
+ mixed_counts[1] = 2;
+ let mixed = SequentialJpegHuffmanTable::new(1, mixed_counts, &[0x11, 0]).unwrap();
+ let mut mixed_reader = SequentialJpegEntropyReader::new(&[0x07], 0);
+ validate_block(&mut mixed_reader, &dc, &mixed).unwrap();
+ }
+
+ #[test]
+ fn frame_sampling_products_are_limited_to_ten() {
+ let valid = [8, 0, 1, 0, 1, 3, 1, 0x22, 0, 2, 0x11, 0, 3, 0x11, 0];
+ assert_eq!(
+ parse_frame(&valid)
+ .unwrap()
+ .components
+ .iter()
+ .map(|component| {
+ u16::from(component.horizontal_sampling)
+ * u16::from(component.vertical_sampling)
+ })
+ .sum::<u16>(),
+ 6
+ );
+
+ let excessive = [8, 0, 1, 0, 1, 3, 1, 0x22, 0, 2, 0x22, 0, 3, 0x22, 0];
+ assert_decode_failed(parse_frame(&excessive));
+ }
+
+ #[test]
+ fn mcu_grid_count_checks_bounds_and_overflow() {
+ assert_eq!(checked_mcu_grid_count(16, 16, 16, 16).unwrap(), 1);
+ assert_eq!(checked_mcu_grid_count(17, 17, 16, 16).unwrap(), 4);
+ assert_decode_failed(checked_mcu_grid_count(0, 1, 8, 8));
+ assert_decode_failed(checked_mcu_grid_count(1, 0, 8, 8));
+ assert_decode_failed(checked_mcu_grid_count(1, 1, 0, 8));
+ assert_decode_failed(checked_mcu_grid_count(1, 1, 8, 0));
+ assert_decode_failed(checked_mcu_grid_count(usize::MAX, usize::MAX, 1, 1));
+
+ let frame = SequentialJpegFrame {
+ dimensions: RadrootsBlossomRasterDimensions::new(17, 17).unwrap(),
+ components: vec![SequentialJpegComponent {
+ id: 1,
+ horizontal_sampling: 1,
+ vertical_sampling: 1,
+ }],
+ maximum_horizontal_sampling: 2,
+ maximum_vertical_sampling: 2,
+ };
+ let scan = SequentialJpegScan {
+ components: vec![SequentialJpegScanComponent {
+ frame_index: 0,
+ dc_table: 0,
+ ac_table: 0,
+ }],
+ };
+ assert_eq!(scan_mcu_count(&frame, &scan, false).unwrap(), 4);
+ assert_eq!(scan_mcu_count(&frame, &scan, true).unwrap(), 4);
+ assert_decode_failed(scan_mcu_count(
+ &frame,
+ &SequentialJpegScan {
+ components: Vec::new(),
+ },
+ false,
+ ));
+
+ let invalid_index = SequentialJpegScan {
+ components: vec![SequentialJpegScanComponent {
+ frame_index: 1,
+ dc_table: 0,
+ ac_table: 0,
+ }],
+ };
+ assert_decode_failed(scan_mcu_count(&frame, &invalid_index, false));
+
+ let zero_horizontal_maximum = SequentialJpegFrame {
+ dimensions: RadrootsBlossomRasterDimensions::new(1, 1).unwrap(),
+ components: vec![SequentialJpegComponent {
+ id: 1,
+ horizontal_sampling: 1,
+ vertical_sampling: 1,
+ }],
+ maximum_horizontal_sampling: 0,
+ maximum_vertical_sampling: 1,
+ };
+ assert_decode_failed(scan_mcu_count(&zero_horizontal_maximum, &scan, false));
+ let zero_vertical_maximum = SequentialJpegFrame {
+ maximum_horizontal_sampling: 1,
+ maximum_vertical_sampling: 0,
+ ..zero_horizontal_maximum
+ };
+ assert_decode_failed(scan_mcu_count(&zero_vertical_maximum, &scan, false));
+ }
+
+ #[test]
+ fn entropy_validation_rejects_unresolved_component_and_table_references() {
+ let frame = single_component_frame();
+ let (dc_tables, ac_tables) = one_symbol_tables();
+ let invalid_component_scan = SequentialJpegScan {
+ components: vec![
+ SequentialJpegScanComponent {
+ frame_index: 1,
+ dc_table: 0,
+ ac_table: 0,
+ },
+ SequentialJpegScanComponent {
+ frame_index: 0,
+ dc_table: 0,
+ ac_table: 0,
+ },
+ ],
+ };
+ assert_decode_failed(validate_scan_entropy(
+ &[0xff, 0xd9],
+ 0,
+ &frame,
+ &invalid_component_scan,
+ &dc_tables,
+ &ac_tables,
+ 0,
+ ));
+
+ let missing_dc_scan = SequentialJpegScan {
+ components: vec![SequentialJpegScanComponent {
+ frame_index: 0,
+ dc_table: 1,
+ ac_table: 0,
+ }],
+ };
+ assert_decode_failed(validate_scan_entropy(
+ &[0xff, 0xd9],
+ 0,
+ &frame,
+ &missing_dc_scan,
+ &dc_tables,
+ &ac_tables,
+ 0,
+ ));
+
+ let missing_ac_scan = SequentialJpegScan {
+ components: vec![SequentialJpegScanComponent {
+ frame_index: 0,
+ dc_table: 0,
+ ac_table: 1,
+ }],
+ };
+ assert_decode_failed(validate_scan_entropy(
+ &[0xff, 0xd9],
+ 0,
+ &frame,
+ &missing_ac_scan,
+ &dc_tables,
+ &ac_tables,
+ 0,
+ ));
+ }
+}
diff --git a/crates/blossom/src/url.rs b/crates/blossom/src/url.rs
@@ -458,4 +458,16 @@ mod tests {
assert!(RadrootsBlossomBlobUrl::parse(&url(&format!("https://{maximum_host}"))).is_ok());
assert!(RadrootsBlossomBlobUrl::parse(&url("https://xn--mdia-9oa.example")).is_ok());
}
+
+ #[test]
+ fn authority_helpers_reject_absent_and_malformed_loopback_hosts() {
+ assert!(!host_is_loopback(Host::Domain(".localhost")));
+ assert!(!host_is_loopback(Host::Domain("media..localhost")));
+
+ let hostless = Url::parse("file:///blob").unwrap();
+ assert_eq!(
+ validate_authority("file:///blob", &hostless),
+ Err(RadrootsBlossomError::InvalidBlobUrl)
+ );
+ }
}
diff --git a/crates/blossom/tests/fixtures/publication_readiness.v1.json b/crates/blossom/tests/fixtures/publication_readiness.v1.json
@@ -6,31 +6,31 @@
"id": "valid_created",
"kind": "blossom.verify_publication_readiness.valid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "none"
},
"expected": {
- "url": "https://cdn.example/0d1c097e006a87476e84014ba5842f04c725ed2fc5a081743ab2b5bf13a538b9.png",
- "sha256": "0d1c097e006a87476e84014ba5842f04c725ed2fc5a081743ab2b5bf13a538b9",
+ "url": "https://cdn.example/4490130851783ff662845f5e72f1948618cc87f951f00f6c2ffb3dc01f3f40fd.png",
+ "sha256": "4490130851783ff662845f5e72f1948618cc87f951f00f6c2ffb3dc01f3f40fd",
"size": 70,
"media_type": "image/png",
"format": "png",
"width": 1,
"height": 1,
"upload_status": 201,
- "evidence_digest": "c52edeba688fa36c7963a478a35ff78504d7dd79a637c67f93d5acb635110660"
+ "evidence_digest": "44e63303e594ea42d863be995b23ac4297ed77e4378d0707c94f28e77164bd3b"
}
},
{
"id": "valid_ok_without_authored_dimensions",
"kind": "blossom.verify_publication_readiness.valid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "upload_status_200"
},
"expected": {
- "url": "https://cdn.example/0d1c097e006a87476e84014ba5842f04c725ed2fc5a081743ab2b5bf13a538b9.png",
- "sha256": "0d1c097e006a87476e84014ba5842f04c725ed2fc5a081743ab2b5bf13a538b9",
+ "url": "https://cdn.example/4490130851783ff662845f5e72f1948618cc87f951f00f6c2ffb3dc01f3f40fd.png",
+ "sha256": "4490130851783ff662845f5e72f1948618cc87f951f00f6c2ffb3dc01f3f40fd",
"size": 70,
"media_type": "image/png",
"format": "png",
@@ -43,7 +43,7 @@
"id": "invalid_upload_status",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "upload_status_202"
},
"expected": {
@@ -54,7 +54,7 @@
"id": "invalid_head_status",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "head_status_204"
},
"expected": {
@@ -65,7 +65,7 @@
"id": "invalid_get_status",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "get_status_206"
},
"expected": {
@@ -76,7 +76,7 @@
"id": "declared_size_over_public_max",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "get_size_over_max"
},
"expected": {
@@ -87,7 +87,7 @@
"id": "missing_get_body",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "get_body_missing"
},
"expected": {
@@ -98,7 +98,7 @@
"id": "short_get_body",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "get_body_short"
},
"expected": {
@@ -109,7 +109,7 @@
"id": "trailing_get_body",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "get_body_trailing"
},
"expected": {
@@ -120,7 +120,7 @@
"id": "authored_bytes_short",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "authored_bytes_short"
},
"expected": {
@@ -131,7 +131,7 @@
"id": "authored_bytes_wrong_hash",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "authored_bytes_wrong_hash"
},
"expected": {
@@ -142,7 +142,7 @@
"id": "upload_url_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "upload_url_mismatch"
},
"expected": {
@@ -153,7 +153,7 @@
"id": "upload_hash_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "upload_hash_mismatch"
},
"expected": {
@@ -164,7 +164,7 @@
"id": "upload_size_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "upload_size_mismatch"
},
"expected": {
@@ -175,7 +175,7 @@
"id": "upload_mime_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "upload_mime_mismatch"
},
"expected": {
@@ -186,7 +186,7 @@
"id": "head_url_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "head_url_mismatch"
},
"expected": {
@@ -197,7 +197,7 @@
"id": "head_size_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "head_size_mismatch"
},
"expected": {
@@ -208,7 +208,7 @@
"id": "head_mime_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "head_mime_mismatch"
},
"expected": {
@@ -219,7 +219,7 @@
"id": "get_url_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "get_url_mismatch"
},
"expected": {
@@ -230,7 +230,7 @@
"id": "get_declared_size_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "get_declared_size_mismatch"
},
"expected": {
@@ -241,7 +241,7 @@
"id": "get_complete_hash_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "get_bytes_wrong_hash"
},
"expected": {
@@ -252,7 +252,7 @@
"id": "unsupported_raster_mime",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "unsupported_mime"
},
"expected": {
@@ -263,7 +263,7 @@
"id": "malformed_raster",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "malformed_container"
},
"expected": {
@@ -274,62 +274,62 @@
"id": "animated_png",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "animated_png"
},
"expected": {
- "error": "publication_raster_frame_count_mismatch"
+ "error": "publication_raster_animation_forbidden"
}
},
{
- "id": "decode_format_mismatch",
+ "id": "declared_format_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
- "mutation": "decode_format_mismatch"
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "declared_mime_jpeg"
},
"expected": {
- "error": "publication_raster_decode_format_mismatch"
+ "error": "invalid_publication_raster"
}
},
{
- "id": "decode_length_mismatch",
+ "id": "corrupt_png_crc",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
- "mutation": "decode_length_mismatch"
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "corrupt_png_crc"
},
"expected": {
- "error": "publication_raster_decode_length_mismatch"
+ "error": "publication_raster_decode_failed"
}
},
{
- "id": "decode_hash_mismatch",
+ "id": "corrupt_png_deflate",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
- "mutation": "decode_hash_mismatch"
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "corrupt_png_deflate"
},
"expected": {
- "error": "publication_raster_decode_hash_mismatch"
+ "error": "publication_raster_decode_failed"
}
},
{
- "id": "decode_container_dimension_mismatch",
+ "id": "invalid_png_color_type",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
- "mutation": "decode_container_dimension_mismatch"
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "invalid_png_color_type"
},
"expected": {
- "error": "publication_raster_container_dimension_mismatch"
+ "error": "publication_raster_decode_failed"
}
},
{
"id": "authored_dimension_mismatch",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
"mutation": "authored_dimension_mismatch"
},
"expected": {
@@ -337,48 +337,92 @@
}
},
{
- "id": "decode_zero_frames",
+ "id": "animated_webp",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
- "mutation": "decode_zero_frames"
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "animated_webp"
},
"expected": {
- "error": "publication_raster_frame_count_mismatch"
+ "error": "publication_raster_animation_forbidden"
}
},
{
- "id": "decode_zero_width",
+ "id": "zero_width",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
- "mutation": "decode_zero_width"
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "zero_width"
},
"expected": {
"error": "publication_raster_dimensions_out_of_range"
}
},
{
- "id": "decode_dimension_over_max",
+ "id": "dimension_over_max",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
- "mutation": "decode_dimension_over_max"
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "dimension_over_max"
},
"expected": {
"error": "publication_raster_dimensions_out_of_range"
}
},
{
- "id": "decode_pixel_limit",
+ "id": "pixel_limit",
"kind": "blossom.verify_publication_readiness.invalid",
"input": {
- "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
- "mutation": "decode_pixel_limit"
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "pixel_limit"
},
"expected": {
"error": "publication_raster_pixel_limit_exceeded"
}
+ },
+ {
+ "id": "progressive_jpeg",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "progressive_jpeg"
+ },
+ "expected": {
+ "error": "publication_jpeg_process_forbidden"
+ }
+ },
+ {
+ "id": "jpeg_entropy_stripped",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "jpeg_entropy_stripped"
+ },
+ "expected": {
+ "error": "publication_raster_decode_failed"
+ }
+ },
+ {
+ "id": "jpeg_entropy_partial",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "jpeg_entropy_partial"
+ },
+ "expected": {
+ "error": "publication_raster_decode_failed"
+ }
+ },
+ {
+ "id": "malformed_jpeg_dqt",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ "mutation": "malformed_jpeg_dqt"
+ },
+ "expected": {
+ "error": "invalid_publication_raster"
+ }
}
]
}
diff --git a/crates/blossom/tests/publication_readiness.rs b/crates/blossom/tests/publication_readiness.rs
@@ -1,10 +1,12 @@
+#![cfg(feature = "raster-decode")]
+
+use image::{ExtendedColorType, ImageEncoder, codecs::webp::WebPEncoder};
use radroots_blossom::{
RadrootsBlossomApprovedBlobUrl, RadrootsBlossomAuthoredRasterDimensions,
RadrootsBlossomBlobDescriptor, RadrootsBlossomBlobUrl, RadrootsBlossomBud01GetObservation,
RadrootsBlossomBud01HeadObservation, RadrootsBlossomBud02UploadObservation,
- RadrootsBlossomError, RadrootsBlossomMediaType, RadrootsBlossomRasterDecodeObservation,
- RadrootsBlossomRasterDimensions, RadrootsBlossomRasterFormat, RadrootsBlossomSha256,
- verify_publication_readiness,
+ RadrootsBlossomError, RadrootsBlossomMediaType, RadrootsBlossomRasterDimensions,
+ RadrootsBlossomRasterFormat, RadrootsBlossomSha256, verify_publication_readiness,
};
use serde::Deserialize;
use serde_json::Value;
@@ -29,7 +31,7 @@ fn publication_readiness_vectors_execute_against_public_api() {
let canonical = canonical_vectors();
assert_eq!(canonical, PACKAGED_VECTORS, "packaged vector mirror drift");
let vector_file: VectorFile = serde_json::from_slice(PACKAGED_VECTORS).unwrap();
- assert_eq!(vector_file.vectors.len(), 33);
+ assert_eq!(vector_file.vectors.len(), 37);
for vector in &vector_file.vectors {
match vector.kind.as_str() {
"blossom.verify_publication_readiness.valid" => execute_valid(vector),
@@ -39,6 +41,260 @@ fn publication_readiness_vectors_execute_against_public_api() {
}
}
+#[test]
+fn publication_readiness_accepts_public_jpeg_and_still_webp() {
+ for (bytes, media_type, extension, format) in [
+ (
+ encoded_jpeg(),
+ "image/jpeg",
+ "jpg",
+ RadrootsBlossomRasterFormat::Jpeg,
+ ),
+ (
+ encoded_still_webp(),
+ "image/webp",
+ "webp",
+ RadrootsBlossomRasterFormat::StillWebP,
+ ),
+ ] {
+ let bytes = bytes.as_slice();
+ let evidence = verify_public_raster(
+ bytes,
+ media_type,
+ extension,
+ RadrootsBlossomAuthoredRasterDimensions::Exact(
+ RadrootsBlossomRasterDimensions::new(1, 1).unwrap(),
+ ),
+ )
+ .unwrap();
+
+ assert_eq!(evidence.raster_format(), format);
+ assert_eq!(evidence.raster_format().to_string(), format.as_str());
+ assert_eq!(evidence.dimensions().pixels(), 1);
+ assert_eq!(evidence.uploaded(), 1_800_000_001);
+ assert_eq!(
+ evidence.evidence_digest().as_sha256().to_string(),
+ evidence.evidence_digest().to_string()
+ );
+ }
+}
+
+#[test]
+fn publication_readiness_rejects_forbidden_and_corrupt_jpeg_and_animated_rasters() {
+ let jpeg = encoded_jpeg();
+ let scan = jpeg
+ .windows(2)
+ .position(|window| window == b"\xff\xda")
+ .unwrap();
+ let segment_length = usize::from(u16::from_be_bytes([jpeg[scan + 2], jpeg[scan + 3]]));
+ let entropy_start = scan + 2 + segment_length;
+ let eoi = jpeg.len() - 2;
+ assert!(entropy_start < eoi);
+ let entropy_length = eoi - entropy_start;
+ for keep in [0, 1, entropy_length / 2, entropy_length - 1] {
+ let mut truncated = jpeg[..entropy_start + keep].to_vec();
+ truncated.extend_from_slice(b"\xff\xd9");
+ assert_eq!(
+ verify_public_raster(
+ &truncated,
+ "image/jpeg",
+ "jpg",
+ RadrootsBlossomAuthoredRasterDimensions::Unspecified,
+ )
+ .unwrap_err()
+ .code(),
+ "publication_raster_decode_failed"
+ );
+ }
+
+ let mut malformed_dqt = jpeg.clone();
+ let sof = malformed_dqt
+ .windows(2)
+ .position(|window| window == b"\xff\xc0")
+ .unwrap();
+ malformed_dqt.drain(sof - 3..sof);
+ assert_eq!(
+ verify_public_raster(
+ &malformed_dqt,
+ "image/jpeg",
+ "jpg",
+ RadrootsBlossomAuthoredRasterDimensions::Unspecified,
+ )
+ .unwrap_err()
+ .code(),
+ "invalid_publication_raster"
+ );
+
+ let mut progressive = jpeg;
+ progressive[sof + 1] = 0xc2;
+ assert_eq!(
+ verify_public_raster(
+ &progressive,
+ "image/jpeg",
+ "jpg",
+ RadrootsBlossomAuthoredRasterDimensions::Unspecified,
+ )
+ .unwrap_err()
+ .code(),
+ "publication_jpeg_process_forbidden"
+ );
+
+ assert_eq!(
+ verify_public_raster(
+ &encoded_animated_png(),
+ "image/png",
+ "png",
+ RadrootsBlossomAuthoredRasterDimensions::Unspecified,
+ )
+ .unwrap_err()
+ .code(),
+ "publication_raster_animation_forbidden"
+ );
+
+ assert_eq!(
+ verify_public_raster(
+ &encoded_animated_webp(),
+ "image/webp",
+ "webp",
+ RadrootsBlossomAuthoredRasterDimensions::Unspecified,
+ )
+ .unwrap_err()
+ .code(),
+ "publication_raster_animation_forbidden"
+ );
+}
+
+fn encoded_jpeg() -> Vec<u8> {
+ hex::decode(
+ "ffd8ffe000104a46494600010100000100010000ffdb0043000302020302020303030304030304050805050404050a070706080c0a0c0c0b0a0b0b0d0e12100d0e110e0b0b1016101113141515150c0f171816141812141514ffdb00430103040405040509050509140d0b0d1414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414ffc00011080001000103012200021101031101ffc4001f0000010501010101010100000000000000000102030405060708090a0bffc400b5100002010303020403050504040000017d01020300041105122131410613516107227114328191a1082342b1c11552d1f02433627282090a161718191a25262728292a3435363738393a434445464748494a535455565758595a636465666768696a737475767778797a838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae1e2e3e4e5e6e7e8e9eaf1f2f3f4f5f6f7f8f9faffc4001f0100030101010101010101010000000000000102030405060708090a0bffc400b51100020102040403040705040400010277000102031104052131061241510761711322328108144291a1b1c109233352f0156272d10a162434e125f11718191a262728292a35363738393a434445464748494a535455565758595a636465666768696a737475767778797a82838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f3f4f5f6f7f8f9faffda000c03010002110311003f00f9ca8a28afc3cfe6f3ffd9",
+ )
+ .unwrap()
+}
+
+fn encoded_still_webp() -> Vec<u8> {
+ let mut bytes = Vec::new();
+ WebPEncoder::new_lossless(&mut bytes)
+ .write_image(&[0, 128, 0, 255], 1, 1, ExtendedColorType::Rgba8)
+ .unwrap();
+ bytes
+}
+
+fn encoded_animated_png() -> Vec<u8> {
+ let canonical = hex::decode(
+ "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff0000003e201e03810ac1e0000000049454e44ae426082",
+ )
+ .unwrap();
+ let mut output = b"\x89PNG\r\n\x1a\n".to_vec();
+ output.extend_from_slice(&png_chunk(*b"IHDR", &canonical[16..29]));
+ output.extend_from_slice(&png_chunk(*b"acTL", &[0, 0, 0, 2, 0, 0, 0, 0]));
+ output.extend_from_slice(&png_chunk(*b"fcTL", &apng_frame_control(0)));
+ output.extend_from_slice(&png_chunk(*b"IDAT", &canonical[41..54]));
+ output.extend_from_slice(&png_chunk(*b"fcTL", &apng_frame_control(1)));
+
+ let mut frame_data = 2_u32.to_be_bytes().to_vec();
+ frame_data.extend_from_slice(&canonical[41..54]);
+ output.extend_from_slice(&png_chunk(*b"fdAT", &frame_data));
+ output.extend_from_slice(&png_chunk(*b"IEND", &[]));
+ output
+}
+
+fn apng_frame_control(sequence: u32) -> [u8; 26] {
+ let mut control = [0_u8; 26];
+ control[..4].copy_from_slice(&sequence.to_be_bytes());
+ control[4..8].copy_from_slice(&1_u32.to_be_bytes());
+ control[8..12].copy_from_slice(&1_u32.to_be_bytes());
+ control[20..22].copy_from_slice(&1_u16.to_be_bytes());
+ control[22..24].copy_from_slice(&10_u16.to_be_bytes());
+ control
+}
+
+fn encoded_animated_webp() -> Vec<u8> {
+ let still = encoded_still_webp();
+ let mut output = b"RIFF\0\0\0\0WEBP".to_vec();
+ let mut extended_header = [0_u8; 10];
+ extended_header[0] = 0x02;
+ push_webp_chunk(&mut output, *b"VP8X", &extended_header);
+ push_webp_chunk(&mut output, *b"ANIM", &[0; 6]);
+
+ let mut frame = [0_u8; 16].to_vec();
+ frame[12] = 1;
+ frame.extend_from_slice(&still[12..]);
+ push_webp_chunk(&mut output, *b"ANMF", &frame);
+ let riff_size = (output.len() as u32) - 8;
+ output[4..8].copy_from_slice(&riff_size.to_le_bytes());
+ output
+}
+
+fn push_webp_chunk(output: &mut Vec<u8>, kind: [u8; 4], data: &[u8]) {
+ output.extend_from_slice(&kind);
+ output.extend_from_slice(&(data.len() as u32).to_le_bytes());
+ output.extend_from_slice(data);
+ if data.len() & 1 == 1 {
+ output.push(0);
+ }
+}
+
+fn verify_public_raster(
+ bytes: &[u8],
+ media_type: &str,
+ extension: &str,
+ authored_dimensions: RadrootsBlossomAuthoredRasterDimensions,
+) -> Result<radroots_blossom::RadrootsBlossomPublicationReadinessEvidence, RadrootsBlossomError> {
+ let hash = RadrootsBlossomSha256::digest(bytes);
+ let url = format!("https://cdn.example/{hash}.{extension}");
+ let media_type = RadrootsBlossomMediaType::parse(media_type).unwrap();
+ let authored_descriptor = RadrootsBlossomBlobDescriptor::new(
+ RadrootsBlossomBlobUrl::parse(&url).unwrap(),
+ hash,
+ bytes.len() as u64,
+ media_type.clone(),
+ 1_800_000_000,
+ )
+ .unwrap()
+ .approve_reference()
+ .unwrap()
+ .verify_bytes(bytes, &media_type)
+ .unwrap();
+ let upload = RadrootsBlossomBud02UploadObservation::new(
+ 201,
+ RadrootsBlossomBlobDescriptor::new(
+ RadrootsBlossomBlobUrl::parse(&url).unwrap(),
+ hash,
+ bytes.len() as u64,
+ media_type.clone(),
+ 1_800_000_001,
+ )
+ .unwrap(),
+ )
+ .unwrap();
+ let approved_url = RadrootsBlossomBlobUrl::parse(&url)
+ .unwrap()
+ .approve()
+ .unwrap();
+ let head = RadrootsBlossomBud01HeadObservation::new(
+ 200,
+ approved_url.clone(),
+ bytes.len() as u64,
+ media_type,
+ )
+ .unwrap();
+ let get = RadrootsBlossomBud01GetObservation::from_complete_body(
+ 200,
+ approved_url,
+ bytes.len() as u64,
+ bytes,
+ )
+ .unwrap();
+ verify_publication_readiness(
+ &authored_descriptor,
+ bytes,
+ authored_dimensions,
+ &upload,
+ &head,
+ &get,
+ )
+}
+
fn canonical_vectors() -> &'static [u8] {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
@@ -144,12 +400,6 @@ fn run_mutation(
let mut head_size_delta = 0_i64;
let mut head_media_type = "image/png";
let mut get_declared_size_delta = 0_i64;
- let mut decode_format = RadrootsBlossomRasterFormat::Png;
- let mut decode_hash_bytes = canonical.clone();
- let mut decode_size_delta = 0_i64;
- let mut frame_count = 1;
- let mut decoded_width = 1;
- let mut decoded_height = 1;
let mut authored_dimensions = Some((1, 1));
match mutation {
@@ -194,35 +444,135 @@ fn run_mutation(
exact_authored_bytes = sealed_bytes.clone();
retrieved_bytes = sealed_bytes.clone();
upload_hash_bytes = sealed_bytes.clone();
- decode_hash_bytes = sealed_bytes.clone();
}
"animated_png" => {
- let iend = sealed_bytes.len() - 12;
- sealed_bytes.splice(
- iend..iend,
- [
- 0, 0, 0, 8, b'a', b'c', b'T', b'L', 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0,
- ],
- );
+ sealed_bytes = encoded_animated_png();
exact_authored_bytes = sealed_bytes.clone();
retrieved_bytes = sealed_bytes.clone();
upload_hash_bytes = sealed_bytes.clone();
- decode_hash_bytes = sealed_bytes.clone();
}
- "decode_format_mismatch" => decode_format = RadrootsBlossomRasterFormat::Jpeg,
- "decode_length_mismatch" => decode_size_delta = 1,
- "decode_hash_mismatch" => decode_hash_bytes[69] ^= 1,
- "decode_container_dimension_mismatch" => {
- decoded_width = 2;
- authored_dimensions = None;
+ "declared_mime_jpeg" => {
+ media_type = "image/jpeg";
+ upload_media_type = "image/jpeg";
+ head_media_type = "image/jpeg";
+ }
+ "corrupt_png_crc" => {
+ sealed_bytes[57] ^= 1;
+ exact_authored_bytes = sealed_bytes.clone();
+ retrieved_bytes = sealed_bytes.clone();
+ upload_hash_bytes = sealed_bytes.clone();
+ }
+ "corrupt_png_deflate" => {
+ sealed_bytes[41] = 0;
+ let crc = png_crc(*b"IDAT", &sealed_bytes[41..54]);
+ sealed_bytes[54..58].copy_from_slice(&crc.to_be_bytes());
+ exact_authored_bytes = sealed_bytes.clone();
+ retrieved_bytes = sealed_bytes.clone();
+ upload_hash_bytes = sealed_bytes.clone();
+ }
+ "invalid_png_color_type" => {
+ sealed_bytes[25] = 1;
+ let crc = png_crc(*b"IHDR", &sealed_bytes[16..29]);
+ sealed_bytes[29..33].copy_from_slice(&crc.to_be_bytes());
+ exact_authored_bytes = sealed_bytes.clone();
+ retrieved_bytes = sealed_bytes.clone();
+ upload_hash_bytes = sealed_bytes.clone();
}
"authored_dimension_mismatch" => authored_dimensions = Some((2, 1)),
- "decode_zero_frames" => frame_count = 0,
- "decode_zero_width" => decoded_width = 0,
- "decode_dimension_over_max" => decoded_width = 16_385,
- "decode_pixel_limit" => {
- decoded_width = 5_000;
- decoded_height = 5_000;
+ "animated_webp" => {
+ sealed_bytes = encoded_animated_webp();
+ exact_authored_bytes = sealed_bytes.clone();
+ retrieved_bytes = sealed_bytes.clone();
+ upload_hash_bytes = sealed_bytes.clone();
+ media_type = "image/webp";
+ upload_media_type = "image/webp";
+ head_media_type = "image/webp";
+ authored_dimensions = None;
+ }
+ "zero_width" => {
+ sealed_bytes[16..20].copy_from_slice(&0_u32.to_be_bytes());
+ exact_authored_bytes = sealed_bytes.clone();
+ retrieved_bytes = sealed_bytes.clone();
+ upload_hash_bytes = sealed_bytes.clone();
+ }
+ "dimension_over_max" => {
+ sealed_bytes[16..20].copy_from_slice(&16_385_u32.to_be_bytes());
+ exact_authored_bytes = sealed_bytes.clone();
+ retrieved_bytes = sealed_bytes.clone();
+ upload_hash_bytes = sealed_bytes.clone();
+ }
+ "pixel_limit" => {
+ sealed_bytes[16..20].copy_from_slice(&5_000_u32.to_be_bytes());
+ sealed_bytes[20..24].copy_from_slice(&5_000_u32.to_be_bytes());
+ exact_authored_bytes = sealed_bytes.clone();
+ retrieved_bytes = sealed_bytes.clone();
+ upload_hash_bytes = sealed_bytes.clone();
+ }
+ "progressive_jpeg" => {
+ let mut jpeg = encoded_jpeg();
+ let sof = jpeg
+ .windows(2)
+ .position(|window| window == b"\xff\xc0")
+ .unwrap();
+ jpeg[sof + 1] = 0xc2;
+ replace_raster_bytes(
+ &mut sealed_bytes,
+ &mut exact_authored_bytes,
+ &mut retrieved_bytes,
+ &mut upload_hash_bytes,
+ jpeg,
+ );
+ media_type = "image/jpeg";
+ upload_media_type = "image/jpeg";
+ head_media_type = "image/jpeg";
+ authored_dimensions = None;
+ }
+ "jpeg_entropy_stripped" | "jpeg_entropy_partial" => {
+ let jpeg = encoded_jpeg();
+ let scan = jpeg
+ .windows(2)
+ .position(|window| window == b"\xff\xda")
+ .unwrap();
+ let segment_length = usize::from(u16::from_be_bytes([jpeg[scan + 2], jpeg[scan + 3]]));
+ let entropy_start = scan + 2 + segment_length;
+ let entropy_length = jpeg.len() - 2 - entropy_start;
+ let keep = if mutation == "jpeg_entropy_stripped" {
+ 0
+ } else {
+ entropy_length / 2
+ };
+ let mut truncated = jpeg[..entropy_start + keep].to_vec();
+ truncated.extend_from_slice(b"\xff\xd9");
+ replace_raster_bytes(
+ &mut sealed_bytes,
+ &mut exact_authored_bytes,
+ &mut retrieved_bytes,
+ &mut upload_hash_bytes,
+ truncated,
+ );
+ media_type = "image/jpeg";
+ upload_media_type = "image/jpeg";
+ head_media_type = "image/jpeg";
+ authored_dimensions = None;
+ }
+ "malformed_jpeg_dqt" => {
+ let mut jpeg = encoded_jpeg();
+ let sof = jpeg
+ .windows(2)
+ .position(|window| window == b"\xff\xc0")
+ .unwrap();
+ jpeg.drain(sof - 3..sof);
+ replace_raster_bytes(
+ &mut sealed_bytes,
+ &mut exact_authored_bytes,
+ &mut retrieved_bytes,
+ &mut upload_hash_bytes,
+ jpeg,
+ );
+ media_type = "image/jpeg";
+ upload_media_type = "image/jpeg";
+ head_media_type = "image/jpeg";
+ authored_dimensions = None;
}
other => panic!("{} has unknown mutation {other}", vector.id),
}
@@ -269,14 +619,6 @@ fn run_mutation(
adjusted_size(sealed_bytes.len(), get_declared_size_delta),
&retrieved_bytes,
)?;
- let decode = RadrootsBlossomRasterDecodeObservation::new(
- decode_format,
- RadrootsBlossomSha256::digest(&decode_hash_bytes),
- adjusted_size(sealed_bytes.len(), decode_size_delta),
- frame_count,
- decoded_width,
- decoded_height,
- )?;
let authored_dimensions = match authored_dimensions {
Some((width, height)) => RadrootsBlossomAuthoredRasterDimensions::Exact(
RadrootsBlossomRasterDimensions::new(width, height)?,
@@ -290,10 +632,22 @@ fn run_mutation(
&upload,
&head,
&get,
- &decode,
)
}
+fn replace_raster_bytes(
+ sealed_bytes: &mut Vec<u8>,
+ exact_authored_bytes: &mut Vec<u8>,
+ retrieved_bytes: &mut Vec<u8>,
+ upload_hash_bytes: &mut Vec<u8>,
+ replacement: Vec<u8>,
+) {
+ *sealed_bytes = replacement;
+ exact_authored_bytes.clone_from(sealed_bytes);
+ retrieved_bytes.clone_from(sealed_bytes);
+ upload_hash_bytes.clone_from(sealed_bytes);
+}
+
fn unreachable_result()
-> Result<radroots_blossom::RadrootsBlossomPublicationReadinessEvidence, RadrootsBlossomError> {
unreachable!("oversized GET construction must fail")
@@ -328,6 +682,26 @@ fn adjusted_size(length: usize, delta: i64) -> u64 {
u64::try_from(i64::try_from(length).unwrap() + delta).unwrap()
}
+fn png_chunk(kind: [u8; 4], data: &[u8]) -> Vec<u8> {
+ let mut chunk = Vec::new();
+ chunk.extend_from_slice(&(data.len() as u32).to_be_bytes());
+ chunk.extend_from_slice(&kind);
+ chunk.extend_from_slice(data);
+ chunk.extend_from_slice(&png_crc(kind, data).to_be_bytes());
+ chunk
+}
+
+fn png_crc(kind: [u8; 4], data: &[u8]) -> u32 {
+ let mut crc = u32::MAX;
+ for byte in kind.iter().chain(data) {
+ crc ^= u32::from(*byte);
+ for _ in 0..8 {
+ crc = (crc >> 1) ^ (0xedb8_8320 & 0_u32.wrapping_sub(crc & 1));
+ }
+ }
+ !crc
+}
+
fn input_str<'a>(vector: &'a Vector, field: &str) -> &'a str {
vector.input[field]
.as_str()
diff --git a/tools/xtask/src/contract/blossom_publication_readiness.rs b/tools/xtask/src/contract/blossom_publication_readiness.rs
@@ -9,9 +9,17 @@ use std::path::Path;
const VECTOR_CANONICAL_RELATIVE: &str =
"contracts/conformance/vectors/blossom/publication_readiness.v1.json";
const VECTOR_MIRROR_RELATIVE: &str = "crates/blossom/tests/fixtures/publication_readiness.v1.json";
+const WORKSPACE_MANIFEST_RELATIVE: &str = "Cargo.toml";
+const WORKSPACE_LOCK_RELATIVE: &str = "Cargo.lock";
const READINESS_SOURCE_RELATIVE: &str = "crates/blossom/src/publication_readiness.rs";
+const SEQUENTIAL_JPEG_SOURCE_RELATIVE: &str =
+ "crates/blossom/src/publication_readiness/sequential_jpeg.rs";
const BLOSSOM_LIB_RELATIVE: &str = "crates/blossom/src/lib.rs";
+const BLOSSOM_URL_RELATIVE: &str = "crates/blossom/src/url.rs";
const BLOSSOM_MANIFEST_RELATIVE: &str = "crates/blossom/Cargo.toml";
+const COVERAGE_PROFILES_RELATIVE: &str = "contracts/coverage-profiles.toml";
+const NIX_COMMON_RELATIVE: &str = "build/nix/common.nix";
+const NIX_CHECKS_RELATIVE: &str = "build/nix/checks.nix";
const OPERATIONS_RELATIVE: &str = "contracts/operations.toml";
const RELEASE_RELATIVE: &str = "contracts/releases/1.0.0-alpha.1.toml";
const CHANGELOG_RELATIVE: &str = "CHANGELOG.md";
@@ -20,6 +28,9 @@ const RELEASE_CHANGE_ID: &str = "blossom-publication-readiness-evidence";
const CHANGELOG_MARKER: &str = "<!-- release-change: blossom-publication-readiness-evidence -->";
const RAW_PREDECESSOR_SUPERSEDED_PATHS: &[&str] = &[
+ WORKSPACE_LOCK_RELATIVE,
+ WORKSPACE_MANIFEST_RELATIVE,
+ NIX_COMMON_RELATIVE,
CHANGELOG_RELATIVE,
RELEASE_RELATIVE,
"tools/xtask/src/contract.rs",
@@ -27,19 +38,31 @@ const RAW_PREDECESSOR_SUPERSEDED_PATHS: &[&str] = &[
"tools/xtask/src/contract/nip09_reconciliation.rs",
RAW_PREDECESSOR_GOVERNANCE_RELATIVE,
];
-const TRANSITIVE_PREDECESSOR_SUPERSEDED_PATHS: &[&str] =
- &["crates/blossom/src/error.rs", BLOSSOM_LIB_RELATIVE];
+const TRANSITIVE_PREDECESSOR_SUPERSEDED_PATHS: &[&str] = &[
+ WORKSPACE_MANIFEST_RELATIVE,
+ BLOSSOM_MANIFEST_RELATIVE,
+ "crates/blossom/src/error.rs",
+ BLOSSOM_LIB_RELATIVE,
+ BLOSSOM_URL_RELATIVE,
+];
const SOURCE_INVENTORY: &[&str] = &[
+ WORKSPACE_LOCK_RELATIVE,
+ WORKSPACE_MANIFEST_RELATIVE,
+ NIX_CHECKS_RELATIVE,
+ NIX_COMMON_RELATIVE,
CHANGELOG_RELATIVE,
BLOSSOM_MANIFEST_RELATIVE,
"crates/blossom/README",
"crates/blossom/src/error.rs",
BLOSSOM_LIB_RELATIVE,
READINESS_SOURCE_RELATIVE,
+ SEQUENTIAL_JPEG_SOURCE_RELATIVE,
+ BLOSSOM_URL_RELATIVE,
"crates/blossom/tests/publication_readiness.rs",
VECTOR_MIRROR_RELATIVE,
"contracts/events/blossom-media.md",
+ COVERAGE_PROFILES_RELATIVE,
VECTOR_CANONICAL_RELATIVE,
OPERATIONS_RELATIVE,
RELEASE_RELATIVE,
@@ -86,13 +109,18 @@ const IMMUTABLE_RAW_PREDECESSOR_ARTIFACTS: &[(&str, usize, &str)] = &[
const CURRENT_BYTE_BOUND_BLOSSOM_SOURCES: &[(&str, usize, &str)] = &[
(
BLOSSOM_LIB_RELATIVE,
- 2_088,
- "ab0431ba43619431f4384a474c1e8b7e3e646a802443d66cf992df467fa9c36b",
+ 2_172,
+ "97cae38f693795445cc17671649f3d88c0c492fc8d71d2c98a4ca02502e5d43a",
),
(
"crates/blossom/src/error.rs",
- 30_667,
- "2d30f4e21d71b6978cb3cc564d5ab542d238cf56c2eab89801fce8d1421bccf6",
+ 30_918,
+ "bd77810306b3556434d93057ca5ee62db474e9b0af94176ba4aa7a2ef25be7d8",
+ ),
+ (
+ BLOSSOM_URL_RELATIVE,
+ 15_794,
+ "e9673f074ba6328a121aa3008fc11cd4d9d22cae3b09f26602ea6fea3f964c80",
),
];
@@ -105,7 +133,6 @@ const REQUIRED_PUBLIC_TYPES: &[&str] = &[
"RadrootsBlossomRasterFormat",
"RadrootsBlossomRasterDimensions",
"RadrootsBlossomAuthoredRasterDimensions",
- "RadrootsBlossomRasterDecodeObservation",
"RadrootsBlossomPublicationReadinessEvidenceDigest",
"RadrootsBlossomPublicationReadinessEvidence",
];
@@ -253,31 +280,31 @@ const VECTOR_EXPECTATIONS: &[(&str, &str, &str, Option<&str>)] = &[
"animated_png",
"blossom.verify_publication_readiness.invalid",
"animated_png",
- Some("publication_raster_frame_count_mismatch"),
+ Some("publication_raster_animation_forbidden"),
),
(
- "decode_format_mismatch",
+ "declared_format_mismatch",
"blossom.verify_publication_readiness.invalid",
- "decode_format_mismatch",
- Some("publication_raster_decode_format_mismatch"),
+ "declared_mime_jpeg",
+ Some("invalid_publication_raster"),
),
(
- "decode_length_mismatch",
+ "corrupt_png_crc",
"blossom.verify_publication_readiness.invalid",
- "decode_length_mismatch",
- Some("publication_raster_decode_length_mismatch"),
+ "corrupt_png_crc",
+ Some("publication_raster_decode_failed"),
),
(
- "decode_hash_mismatch",
+ "corrupt_png_deflate",
"blossom.verify_publication_readiness.invalid",
- "decode_hash_mismatch",
- Some("publication_raster_decode_hash_mismatch"),
+ "corrupt_png_deflate",
+ Some("publication_raster_decode_failed"),
),
(
- "decode_container_dimension_mismatch",
+ "invalid_png_color_type",
"blossom.verify_publication_readiness.invalid",
- "decode_container_dimension_mismatch",
- Some("publication_raster_container_dimension_mismatch"),
+ "invalid_png_color_type",
+ Some("publication_raster_decode_failed"),
),
(
"authored_dimension_mismatch",
@@ -286,29 +313,53 @@ const VECTOR_EXPECTATIONS: &[(&str, &str, &str, Option<&str>)] = &[
Some("publication_authored_raster_dimension_mismatch"),
),
(
- "decode_zero_frames",
+ "animated_webp",
"blossom.verify_publication_readiness.invalid",
- "decode_zero_frames",
- Some("publication_raster_frame_count_mismatch"),
+ "animated_webp",
+ Some("publication_raster_animation_forbidden"),
),
(
- "decode_zero_width",
+ "zero_width",
"blossom.verify_publication_readiness.invalid",
- "decode_zero_width",
+ "zero_width",
Some("publication_raster_dimensions_out_of_range"),
),
(
- "decode_dimension_over_max",
+ "dimension_over_max",
"blossom.verify_publication_readiness.invalid",
- "decode_dimension_over_max",
+ "dimension_over_max",
Some("publication_raster_dimensions_out_of_range"),
),
(
- "decode_pixel_limit",
+ "pixel_limit",
"blossom.verify_publication_readiness.invalid",
- "decode_pixel_limit",
+ "pixel_limit",
Some("publication_raster_pixel_limit_exceeded"),
),
+ (
+ "progressive_jpeg",
+ "blossom.verify_publication_readiness.invalid",
+ "progressive_jpeg",
+ Some("publication_jpeg_process_forbidden"),
+ ),
+ (
+ "jpeg_entropy_stripped",
+ "blossom.verify_publication_readiness.invalid",
+ "jpeg_entropy_stripped",
+ Some("publication_raster_decode_failed"),
+ ),
+ (
+ "jpeg_entropy_partial",
+ "blossom.verify_publication_readiness.invalid",
+ "jpeg_entropy_partial",
+ Some("publication_raster_decode_failed"),
+ ),
+ (
+ "malformed_jpeg_dqt",
+ "blossom.verify_publication_readiness.invalid",
+ "malformed_jpeg_dqt",
+ Some("invalid_publication_raster"),
+ ),
];
pub(super) fn validate_blossom_publication_readiness(workspace_root: &Path) -> Result<(), String> {
@@ -399,11 +450,19 @@ fn validate_source_boundary(workspace_root: &Path) -> Result<(), String> {
READINESS_SOURCE_RELATIVE,
)?)
.map_err(|error| format!("{READINESS_SOURCE_RELATIVE} must be UTF-8: {error}"))?;
- validate_readiness_source_text(&source)?;
+ let sequential_jpeg_source = String::from_utf8(read_regular_file(
+ workspace_root,
+ SEQUENTIAL_JPEG_SOURCE_RELATIVE,
+ )?)
+ .map_err(|error| format!("{SEQUENTIAL_JPEG_SOURCE_RELATIVE} must be UTF-8: {error}"))?;
+ validate_readiness_source_text(&source, &sequential_jpeg_source)?;
let lib = String::from_utf8(read_regular_file(workspace_root, BLOSSOM_LIB_RELATIVE)?)
.map_err(|error| format!("{BLOSSOM_LIB_RELATIVE} must be UTF-8: {error}"))?;
if lib.matches("pub mod publication_readiness;").count() != 1
- || !lib.contains("verify_publication_readiness")
+ || !lib.contains(
+ "#[cfg(feature = \"raster-decode\")]\npub use publication_readiness::verify_publication_readiness;",
+ )
+ || !lib.contains("RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DECODED_BYTES")
|| !lib.contains("RadrootsBlossomPublicationReadinessEvidence")
{
return Err(
@@ -421,11 +480,14 @@ fn validate_source_boundary(workspace_root: &Path) -> Result<(), String> {
.map(String::as_str)
.collect::<BTreeSet<_>>();
let expected = [
+ "image",
"mediatype",
"serde",
"sha2",
"unicode-general-category",
"url_nostd",
+ "zune-core",
+ "zune-jpeg",
]
.into_iter()
.collect::<BTreeSet<_>>();
@@ -434,6 +496,198 @@ fn validate_source_boundary(workspace_root: &Path) -> Result<(), String> {
"{BLOSSOM_MANIFEST_RELATIVE} dependency boundary drifted: expected {expected:?}, found {actual:?}"
));
}
+ let image_dependency = dependencies
+ .get("image")
+ .and_then(toml::Value::as_table)
+ .ok_or_else(|| format!("{BLOSSOM_MANIFEST_RELATIVE} must declare optional image"))?;
+ if image_dependency
+ .get("workspace")
+ .and_then(toml::Value::as_bool)
+ != Some(true)
+ || image_dependency
+ .get("optional")
+ .and_then(toml::Value::as_bool)
+ != Some(true)
+ {
+ return Err(format!(
+ "{BLOSSOM_MANIFEST_RELATIVE} image dependency must be optional and workspace-governed"
+ ));
+ }
+ let zune_core_dependency = dependencies
+ .get("zune-core")
+ .and_then(toml::Value::as_table)
+ .ok_or_else(|| format!("{BLOSSOM_MANIFEST_RELATIVE} must declare optional zune-core"))?;
+ if zune_core_dependency
+ .get("workspace")
+ .and_then(toml::Value::as_bool)
+ != Some(true)
+ || zune_core_dependency
+ .get("optional")
+ .and_then(toml::Value::as_bool)
+ != Some(true)
+ {
+ return Err(format!(
+ "{BLOSSOM_MANIFEST_RELATIVE} zune-core dependency must be optional and workspace-governed"
+ ));
+ }
+ let zune_jpeg_dependency = dependencies
+ .get("zune-jpeg")
+ .and_then(toml::Value::as_table)
+ .ok_or_else(|| format!("{BLOSSOM_MANIFEST_RELATIVE} must declare optional zune-jpeg"))?;
+ if zune_jpeg_dependency
+ .get("workspace")
+ .and_then(toml::Value::as_bool)
+ != Some(true)
+ || zune_jpeg_dependency
+ .get("optional")
+ .and_then(toml::Value::as_bool)
+ != Some(true)
+ {
+ return Err(format!(
+ "{BLOSSOM_MANIFEST_RELATIVE} zune-jpeg dependency must be optional and workspace-governed"
+ ));
+ }
+ let workspace_manifest = parse_toml(workspace_root, WORKSPACE_MANIFEST_RELATIVE)?;
+ let workspace_image = workspace_manifest
+ .get("workspace")
+ .and_then(|value| value.get("dependencies"))
+ .and_then(|value| value.get("image"))
+ .and_then(toml::Value::as_table)
+ .ok_or_else(|| format!("{WORKSPACE_MANIFEST_RELATIVE} must govern image"))?;
+ let workspace_image_features = workspace_image
+ .get("features")
+ .and_then(toml::Value::as_array)
+ .into_iter()
+ .flatten()
+ .filter_map(toml::Value::as_str)
+ .collect::<BTreeSet<_>>();
+ if workspace_image.get("version").and_then(toml::Value::as_str) != Some("=0.25.10")
+ || workspace_image
+ .get("default-features")
+ .and_then(toml::Value::as_bool)
+ != Some(false)
+ || workspace_image_features != BTreeSet::from(["png", "webp"])
+ {
+ return Err(format!(
+ "{WORKSPACE_MANIFEST_RELATIVE} image decoder dependency must be exactly pinned to the PNG/WebP set"
+ ));
+ }
+ let workspace_zune_core = workspace_manifest
+ .get("workspace")
+ .and_then(|value| value.get("dependencies"))
+ .and_then(|value| value.get("zune-core"))
+ .and_then(toml::Value::as_table)
+ .ok_or_else(|| format!("{WORKSPACE_MANIFEST_RELATIVE} must govern zune-core"))?;
+ let workspace_zune_core_features = workspace_zune_core
+ .get("features")
+ .and_then(toml::Value::as_array)
+ .into_iter()
+ .flatten()
+ .filter_map(toml::Value::as_str)
+ .collect::<BTreeSet<_>>();
+ if workspace_zune_core
+ .get("version")
+ .and_then(toml::Value::as_str)
+ != Some("=0.5.1")
+ || workspace_zune_core
+ .get("default-features")
+ .and_then(toml::Value::as_bool)
+ != Some(false)
+ || workspace_zune_core_features != BTreeSet::from(["std"])
+ {
+ return Err(format!(
+ "{WORKSPACE_MANIFEST_RELATIVE} JPEG decoder core must be exactly pinned to zune-core 0.5.1 with std only"
+ ));
+ }
+ let workspace_zune_jpeg = workspace_manifest
+ .get("workspace")
+ .and_then(|value| value.get("dependencies"))
+ .and_then(|value| value.get("zune-jpeg"))
+ .and_then(toml::Value::as_table)
+ .ok_or_else(|| format!("{WORKSPACE_MANIFEST_RELATIVE} must govern zune-jpeg"))?;
+ let workspace_zune_jpeg_features = workspace_zune_jpeg
+ .get("features")
+ .and_then(toml::Value::as_array)
+ .into_iter()
+ .flatten()
+ .filter_map(toml::Value::as_str)
+ .collect::<BTreeSet<_>>();
+ if workspace_zune_jpeg
+ .get("version")
+ .and_then(toml::Value::as_str)
+ != Some("=0.5.15")
+ || workspace_zune_jpeg
+ .get("default-features")
+ .and_then(toml::Value::as_bool)
+ != Some(false)
+ || workspace_zune_jpeg_features != BTreeSet::from(["std"])
+ {
+ return Err(format!(
+ "{WORKSPACE_MANIFEST_RELATIVE} strict JPEG authority must be exactly pinned to zune-jpeg 0.5.15 with std only"
+ ));
+ }
+ let features = manifest
+ .get("features")
+ .and_then(toml::Value::as_table)
+ .ok_or_else(|| format!("{BLOSSOM_MANIFEST_RELATIVE} must declare features"))?;
+ let raster_decode = features
+ .get("raster-decode")
+ .and_then(toml::Value::as_array)
+ .ok_or_else(|| format!("{BLOSSOM_MANIFEST_RELATIVE} must declare raster-decode feature"))?
+ .iter()
+ .filter_map(toml::Value::as_str)
+ .collect::<BTreeSet<_>>();
+ if raster_decode != BTreeSet::from(["dep:image", "dep:zune-core", "dep:zune-jpeg", "std"]) {
+ return Err(format!(
+ "{BLOSSOM_MANIFEST_RELATIVE} raster-decode feature must select only std, image, zune-core, and zune-jpeg"
+ ));
+ }
+ let coverage = parse_toml(workspace_root, COVERAGE_PROFILES_RELATIVE)?;
+ let blossom_coverage = coverage
+ .get("profiles")
+ .and_then(|value| value.get("crates"))
+ .and_then(|value| value.get("radroots_blossom"))
+ .ok_or_else(|| {
+ format!("{COVERAGE_PROFILES_RELATIVE} must declare radroots_blossom coverage")
+ })?;
+ let coverage_features = blossom_coverage
+ .get("features")
+ .and_then(toml::Value::as_array)
+ .into_iter()
+ .flatten()
+ .filter_map(toml::Value::as_str)
+ .collect::<BTreeSet<_>>();
+ if blossom_coverage
+ .get("no_default_features")
+ .and_then(toml::Value::as_bool)
+ != Some(true)
+ || coverage_features != BTreeSet::from(["raster-decode", "serde"])
+ {
+ return Err(format!(
+ "{COVERAGE_PROFILES_RELATIVE} must measure the explicit Blossom raster-decode surface"
+ ));
+ }
+ let nix_common = String::from_utf8(read_regular_file(workspace_root, NIX_COMMON_RELATIVE)?)
+ .map_err(|error| format!("{NIX_COMMON_RELATIVE} must be UTF-8: {error}"))?;
+ if !nix_common.contains("radroots_blossom/raster-decode") {
+ return Err(format!(
+ "{NIX_COMMON_RELATIVE} core contract lane must enable raster-decode"
+ ));
+ }
+ let nix_checks = String::from_utf8(read_regular_file(workspace_root, NIX_CHECKS_RELATIVE)?)
+ .map_err(|error| format!("{NIX_CHECKS_RELATIVE} must be UTF-8: {error}"))?;
+ let nix_check_commands = nix_checks.lines().map(str::trim).collect::<BTreeSet<_>>();
+ for required in [
+ "cargo check -p radroots_blossom --lib --no-default-features",
+ "cargo check -p radroots_blossom --lib --no-default-features --features raster-decode",
+ "cargo test -p radroots_blossom --no-default-features --features raster-decode,serde",
+ ] {
+ if !nix_check_commands.contains(required) {
+ return Err(format!(
+ "{NIX_CHECKS_RELATIVE} lacks governed Blossom verification `{required}`"
+ ));
+ }
+ }
let raw_predecessor = String::from_utf8(read_regular_file(
workspace_root,
RAW_PREDECESSOR_GOVERNANCE_RELATIVE,
@@ -448,7 +702,7 @@ fn validate_raw_predecessor_successor_routing(source: &str) -> Result<(), String
for required in [
"pub(crate)fnwrite_raw_source_rebuild_manifest(workspace_root:&Path)->Result<(),String>{validate_raw_source_rebuild_manifest(workspace_root)}",
"super::blossom_publication_readiness::validate_blossom_publication_readiness(workspace_root)",
- "constBLOSSOM_READINESS_SUCCESSOR_TRANSITIVE_PATHS:&[&str]=&[\"crates/blossom/src/error.rs\",\"crates/blossom/src/lib.rs\"];",
+ "constBLOSSOM_READINESS_SUCCESSOR_TRANSITIVE_PATHS:&[&str]=&[\"Cargo.toml\",\"crates/blossom/Cargo.toml\",\"crates/blossom/src/error.rs\",\"crates/blossom/src/lib.rs\",\"crates/blossom/src/url.rs\",];",
] {
if !compact.contains(required) {
return Err(format!(
@@ -459,13 +713,33 @@ fn validate_raw_predecessor_successor_routing(source: &str) -> Result<(), String
Ok(())
}
-fn validate_readiness_source_text(source: &str) -> Result<(), String> {
+fn validate_readiness_source_text(
+ source: &str,
+ sequential_jpeg_source: &str,
+) -> Result<(), String> {
for required in [
"RADROOTS_BLOSSOM_PUBLICATION_READINESS_POLICY_VERSION: u16 = 1",
"RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_BYTES: u64 = 10_485_760",
+ "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DECODED_BYTES: u64 =",
"RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DIMENSION: u32 = 16_384",
"RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_PIXELS: u64 = 20_000_000",
- "pub fn verify_publication_readiness(",
+ "#[cfg(feature = \"raster-decode\")]\npub fn verify_publication_readiness(",
+ "use zune_core::{bytestream::ZCursor, colorspace::ColorSpace, options::DecoderOptions};",
+ "use zune_jpeg::JpegDecoder as StrictJpegDecoder;",
+ "mod sequential_jpeg;",
+ "sequential_jpeg::validate(bytes, container)?;",
+ "StrictJpegDecoder::new_with_options(ZCursor::new(bytes), strict_jpeg_decoder_options())",
+ ".set_strict_mode(true)",
+ ".set_use_unsafe(false)",
+ ".jpeg_set_out_colorspace(ColorSpace::RGB)",
+ ".decode_headers()",
+ ".output_buffer_size()",
+ ".decode_into(&mut decoded)",
+ "if !matches!(marker, 0xc0 | 0xc1) || data[0] != 8",
+ "PublicationJpegProcessForbidden",
+ "PngDecoder::with_limits(Cursor::new(bytes), raster_decode_limits())",
+ "WebPDecoder::new(Cursor::new(bytes))",
+ ".read_image(&mut decoded)",
"b\"radroots.blossom.publication-readiness-evidence.v1\\0\"",
] {
if !source.contains(required) {
@@ -474,7 +748,26 @@ fn validate_readiness_source_text(source: &str) -> Result<(), String> {
));
}
}
- let lowercase = source.to_ascii_lowercase();
+ for required in [
+ "struct SequentialJpegHuffmanTable",
+ "struct SequentialJpegEntropyReader",
+ "sampling_product_sum > 10",
+ "value_count > 256",
+ "seen_values[value_index]",
+ "unused_codes == 0",
+ "payload[payload.len() - 3..] != [0, 63, 0]",
+ "reader.finish_restart(expected_restart)?",
+ "seen_components[component.frame_index] = true",
+ "checked_mcu_grid_count",
+ ] {
+ if !sequential_jpeg_source.contains(required) {
+ return Err(format!(
+ "{SEQUENTIAL_JPEG_SOURCE_RELATIVE} is missing governed fragment `{required}`"
+ ));
+ }
+ }
+ let combined = format!("{source}\n{sequential_jpeg_source}");
+ let lowercase = combined.to_ascii_lowercase();
for forbidden in [
"reqwest",
"hyper::",
@@ -492,12 +785,30 @@ fn validate_readiness_source_text(source: &str) -> Result<(), String> {
));
}
}
- if source.contains("serde::Deserialize") || source.contains("derive(Deserialize") {
+ if combined.contains("serde::Deserialize") || combined.contains("derive(Deserialize") {
return Err(
"publication readiness typestates must not gain forgeable Deserialize implementations"
.to_owned(),
);
}
+ if combined.contains("pub struct RadrootsBlossomRasterDecodeObservation")
+ || combined.contains("decode: &RadrootsBlossom")
+ || combined.contains("ImageReader")
+ || combined.contains("load_from_memory")
+ || combined.contains("JpegDecoder::new(Cursor::new(bytes))")
+ || combined.contains("push_backend(")
+ || combined.contains("gamut_core")
+ || combined.contains("gamut_jpeg")
+ || combined.contains("jpeg_decoder::")
+ || combined.contains("use jpeg_decoder")
+ || combined.contains("extern crate jpeg_decoder")
+ || combined.contains("zenjpeg")
+ {
+ return Err(
+ "publication readiness must force declared-format decode authority internally from exact bytes"
+ .to_owned(),
+ );
+ }
Ok(())
}
@@ -603,6 +914,47 @@ fn validate_operation(workspace_root: &Path) -> Result<(), String> {
"operations contract lacks readiness public type `{missing}`"
));
}
+ if shared.contains("RadrootsBlossomRasterDecodeObservation") {
+ return Err(
+ "operations contract must not expose caller-constructible raster decode authority"
+ .to_owned(),
+ );
+ }
+ let inputs = operation
+ .get("inputs")
+ .and_then(toml::Value::as_array)
+ .into_iter()
+ .flatten()
+ .filter_map(toml::Value::as_str)
+ .collect::<BTreeSet<_>>();
+ let expected_inputs = BTreeSet::from([
+ "Bytes",
+ "RadrootsBlossomAuthoredRasterDimensions",
+ "RadrootsBlossomBud01GetObservation",
+ "RadrootsBlossomBud01HeadObservation",
+ "RadrootsBlossomBud02UploadObservation",
+ "RadrootsBlossomByteVerifiedDescriptor",
+ ]);
+ if inputs != expected_inputs {
+ return Err(
+ "Blossom publication-readiness inputs must contain transport evidence and exact bytes only"
+ .to_owned(),
+ );
+ }
+ let rust_types = operation
+ .get("implementation")
+ .and_then(|value| value.get("rust_types"))
+ .and_then(toml::Value::as_array)
+ .into_iter()
+ .flatten()
+ .filter_map(toml::Value::as_str)
+ .collect::<BTreeSet<_>>();
+ if rust_types.contains("radroots_blossom::RadrootsBlossomRasterDecodeObservation") {
+ return Err(
+ "Blossom publication-readiness implementation must derive decode facts internally"
+ .to_owned(),
+ );
+ }
Ok(())
}
@@ -684,10 +1036,14 @@ mod tests {
read_regular_file(&workspace_root(), READINESS_SOURCE_RELATIVE).unwrap(),
)
.unwrap();
- validate_readiness_source_text(&source).unwrap();
+ let sequential_jpeg_source = String::from_utf8(
+ read_regular_file(&workspace_root(), SEQUENTIAL_JPEG_SOURCE_RELATIVE).unwrap(),
+ )
+ .unwrap();
+ validate_readiness_source_text(&source, &sequential_jpeg_source).unwrap();
let injected = format!("{source}\nfn injected() {{ let _ = reqwest::get; }}\n");
assert!(
- validate_readiness_source_text(&injected)
+ validate_readiness_source_text(&injected, &sequential_jpeg_source)
.unwrap_err()
.contains("transport-neutral")
);
@@ -696,10 +1052,23 @@ mod tests {
"RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_PIXELS: u64 = 20_000_001",
);
assert!(
- validate_readiness_source_text(&removed)
+ validate_readiness_source_text(&removed, &sequential_jpeg_source)
+ .unwrap_err()
+ .contains("missing governed fragment")
+ );
+ let weakened_jpeg = sequential_jpeg_source
+ .replace("sampling_product_sum > 10", "sampling_product_sum > 16");
+ assert!(
+ validate_readiness_source_text(&source, &weakened_jpeg)
.unwrap_err()
.contains("missing governed fragment")
);
+ let legacy_decoder = format!("{source}\nuse jpeg_decoder::Decoder;\n");
+ assert!(
+ validate_readiness_source_text(&legacy_decoder, &sequential_jpeg_source)
+ .unwrap_err()
+ .contains("decode authority")
+ );
for (relative, expected_length, expected_sha256) in CURRENT_BYTE_BOUND_BLOSSOM_SOURCES {
let mut bytes = read_regular_file(&workspace_root(), relative).unwrap();
diff --git a/tools/xtask/src/contract/food_availability_projection.rs b/tools/xtask/src/contract/food_availability_projection.rs
@@ -3993,8 +3993,11 @@ mod tests {
#[test]
fn downstream_nip09_only_supersession_is_transitively_validated() {
const CURRENT_SUCCESSOR_SUPERSEDED_PATHS: &[&str] = &[
+ "Cargo.toml",
+ "crates/blossom/Cargo.toml",
"crates/blossom/src/error.rs",
"crates/blossom/src/lib.rs",
+ "crates/blossom/src/url.rs",
"crates/event_store/Cargo.toml",
"crates/event_store/src/error.rs",
"crates/event_store/src/generated.rs",
diff --git a/tools/xtask/src/contract/nip09_reconciliation.rs b/tools/xtask/src/contract/nip09_reconciliation.rs
@@ -2375,6 +2375,13 @@ fn nip09_predecessor_production_source_paths(
.map(|source| source.path.as_str())
.chain(
manifest
+ .cargo_feature_profile
+ .packages
+ .iter()
+ .map(|package| package.manifest_path.as_str()),
+ )
+ .chain(
+ manifest
.source_route_witnesses
.iter()
.map(|source| source.path.as_str()),
@@ -17213,9 +17220,12 @@ mod tests {
use super::*;
use std::fs;
- const RAW_SOURCE_REBUILD_PREDECESSOR_SUPERSEDED_PATHS: [&str; 13] = [
+ const RAW_SOURCE_REBUILD_PREDECESSOR_SUPERSEDED_PATHS: [&str; 16] = [
+ "Cargo.toml",
+ "crates/blossom/Cargo.toml",
"crates/blossom/src/error.rs",
"crates/blossom/src/lib.rs",
+ "crates/blossom/src/url.rs",
"crates/event_store/Cargo.toml",
"crates/event_store/src/error.rs",
"crates/event_store/src/generated.rs",
@@ -17325,6 +17335,55 @@ mod tests {
let predecessor =
toml::to_string_pretty(&manifest).expect("serialize predecessor Cargo manifest");
fs::write(manifest_path, predecessor).expect("restore predecessor compiler manifest");
+
+ let blossom_path = workspace_root.join(BLOSSOM_CARGO_MANIFEST_RELATIVE);
+ let blossom_source = fs::read_to_string(&blossom_path).expect("Blossom Cargo manifest");
+ let mut blossom_manifest: toml::Value =
+ toml::from_str(&blossom_source).expect("parse Blossom Cargo manifest");
+ let raster_decode = blossom_manifest
+ .get_mut("features")
+ .and_then(toml::Value::as_table_mut)
+ .and_then(|features| features.remove("raster-decode"))
+ .expect("successor raster-decode feature must be present in the live fixture");
+ assert_eq!(
+ raster_decode
+ .as_array()
+ .expect("raster-decode feature array")
+ .iter()
+ .map(toml::Value::as_str)
+ .collect::<Vec<_>>(),
+ [
+ Some("std"),
+ Some("dep:image"),
+ Some("dep:zune-core"),
+ Some("dep:zune-jpeg")
+ ],
+ "successor raster-decode feature must retain its exact semantic shape"
+ );
+ let blossom_dependencies = blossom_manifest
+ .get_mut("dependencies")
+ .and_then(toml::Value::as_table_mut)
+ .expect("Blossom dependencies");
+ for dependency in ["image", "zune-core", "zune-jpeg"] {
+ let removed = blossom_dependencies
+ .remove(dependency)
+ .unwrap_or_else(|| panic!("successor dependency {dependency} must be present"));
+ let expected: toml::Value =
+ toml::from_str("dependency = { workspace = true, optional = true }")
+ .expect("parse expected successor dependency");
+ assert_eq!(
+ removed,
+ expected
+ .get("dependency")
+ .expect("expected successor dependency")
+ .clone(),
+ "successor dependency {dependency} must retain its exact semantic shape"
+ );
+ }
+ let blossom_predecessor = toml::to_string_pretty(&blossom_manifest)
+ .expect("serialize predecessor Blossom Cargo manifest");
+ fs::write(blossom_path, blossom_predecessor)
+ .expect("restore predecessor Blossom compiler manifest");
}
fn strip_outer_try(statement: &mut syn::Stmt) {
diff --git a/tools/xtask/src/contract/raw_source_rebuild.rs b/tools/xtask/src/contract/raw_source_rebuild.rs
@@ -867,8 +867,11 @@ const EXPECTED_SOURCE_MAINTENANCE_DRIFT_PATHS: &[&str] = &[
];
const TRANSITIVE_PREDECESSOR_SUPERSEDED_PATHS: &[&str] = &[
+ "Cargo.toml",
+ "crates/blossom/Cargo.toml",
"crates/blossom/src/error.rs",
"crates/blossom/src/lib.rs",
+ "crates/blossom/src/url.rs",
"crates/event_store/Cargo.toml",
"crates/event_store/src/error.rs",
"crates/event_store/src/generated.rs",
@@ -881,8 +884,16 @@ const TRANSITIVE_PREDECESSOR_SUPERSEDED_PATHS: &[&str] = &[
"crates/event_store/src/store/food_availability_projection_v1.rs",
"crates/event_store/src/store/protocol_reconciliation_v1.rs",
];
-const BLOSSOM_READINESS_SUCCESSOR_TRANSITIVE_PATHS: &[&str] =
- &["crates/blossom/src/error.rs", "crates/blossom/src/lib.rs"];
+const BLOSSOM_READINESS_SUCCESSOR_TRANSITIVE_PATHS: &[&str] = &[
+ "Cargo.toml",
+ "crates/blossom/Cargo.toml",
+ "crates/blossom/src/error.rs",
+ "crates/blossom/src/lib.rs",
+ "crates/blossom/src/url.rs",
+];
+#[cfg(test)]
+const BLOSSOM_READINESS_SUCCESSOR_DELEGATED_COMPILER_PATHS: &[&str] =
+ &[CONTRACT_LANE_SOURCE_RELATIVE];
const GENERATED_ARTIFACT_PATHS: &[&str] = &[
MANIFEST_RELATIVE,
@@ -1734,7 +1745,33 @@ fn validate_complete_event_store_source_closure(workspace_root: &Path) -> Result
}
fn validate_delegated_compiler_source_pins(workspace_root: &Path) -> Result<(), String> {
+ validate_delegated_compiler_source_pins_with_supersessions(workspace_root, &[])
+}
+
+fn validate_delegated_compiler_source_pins_with_supersessions(
+ workspace_root: &Path,
+ superseded_paths: &[&str],
+) -> Result<(), String> {
+ let superseded = superseded_paths.iter().copied().collect::<BTreeSet<_>>();
+ if superseded.len() != superseded_paths.len() {
+ return Err("delegated compiler source supersession paths must be unique".to_owned());
+ }
+ let pinned = DELEGATED_COMPILER_SOURCE_PINS
+ .iter()
+ .map(|(relative, _)| *relative)
+ .collect::<BTreeSet<_>>();
+ if let Some(relative) = superseded
+ .iter()
+ .find(|relative| !pinned.contains(**relative))
+ {
+ return Err(format!(
+ "delegated compiler source supersession path `{relative}` is not predecessor-pinned"
+ ));
+ }
for (relative, expected_sha256) in DELEGATED_COMPILER_SOURCE_PINS {
+ if superseded.contains(relative) {
+ continue;
+ }
let actual_sha256 = sha256_hex(&read_regular_file(workspace_root, relative)?);
if actual_sha256 != *expected_sha256 {
return Err(format!(
@@ -5134,7 +5171,7 @@ fn validate_delegated_suite_contract_lane_sources(
|| cargo_arg_lines[1]
!= "lib.concatStringsSep \" \" (map (crate: \"-p ${crate}\") coreContractCrates)"
|| cargo_arg_lines[2]
- != "+ \" --features radroots_event_codec/serde_json,radroots_event_codec/nostr,radroots_nostr/blossom,radroots_nostr/client,radroots_nostr/codec,radroots_nostr/events\";"
+ != "+ \" --features radroots_blossom/raster-decode,radroots_event_codec/serde_json,radroots_event_codec/nostr,radroots_nostr/blossom,radroots_nostr/client,radroots_nostr/codec,radroots_nostr/events\";"
{
return Err(format!(
"{CONTRACT_LANE_SOURCE_RELATIVE} coreContractCargoArgs must map every literal core contract crate to an unfiltered `-p` package selection"
@@ -6546,7 +6583,11 @@ mod tests {
.expect("complete event-store Rust source closure");
validate_successor_compiler_input_authority(&root)
.expect("complete event-store compiler-input authority");
- validate_delegated_compiler_source_pins(&root).expect("delegated compiler source pins");
+ validate_delegated_compiler_source_pins_with_supersessions(
+ &root,
+ BLOSSOM_READINESS_SUCCESSOR_DELEGATED_COMPILER_PATHS,
+ )
+ .expect("delegated compiler source pins outside the active Blossom successor");
validate_xtask_manifest_authority(&root).expect("xtask compiler authority");
}
@@ -6560,12 +6601,26 @@ mod tests {
.expect("create compiler pin parent");
fs::copy(root.join(relative), destination).expect("copy compiler pin source");
}
- validate_delegated_compiler_source_pins(pinned_workspace.path())
- .expect("current compiler source pins");
+ validate_delegated_compiler_source_pins_with_supersessions(
+ pinned_workspace.path(),
+ BLOSSOM_READINESS_SUCCESSOR_DELEGATED_COMPILER_PATHS,
+ )
+ .expect("current compiler source pins outside the active Blossom successor");
fs::write(pinned_workspace.path().join(FLAKE_LOCK_RELATIVE), "{}\n")
.expect("mutate pinned flake lock");
- validate_delegated_compiler_source_pins(pinned_workspace.path())
- .expect_err("compiler source mutation must fail closed");
+ validate_delegated_compiler_source_pins_with_supersessions(
+ pinned_workspace.path(),
+ BLOSSOM_READINESS_SUCCESSOR_DELEGATED_COMPILER_PATHS,
+ )
+ .expect_err("compiler source mutation must fail closed");
+
+ let unknown = ["build/nix/not-predecessor-pinned.nix"];
+ let error = validate_delegated_compiler_source_pins_with_supersessions(
+ pinned_workspace.path(),
+ &unknown,
+ )
+ .expect_err("unknown compiler source supersession must fail closed");
+ assert!(error.contains("not predecessor-pinned"), "{error}");
let manifest_workspace = tempfile::tempdir().expect("xtask manifest workspace");
let manifest_path = manifest_workspace.path().join(XTASK_MANIFEST_RELATIVE);
@@ -7256,17 +7311,13 @@ mod tests {
#[test]
fn generated_bundle_render_is_deterministic() {
let root = workspace_root();
- let first = expected_artifacts(&root)
- .expect("first render")
- .into_iter()
- .map(|artifact| (artifact.relative, artifact.contents))
- .collect::<Vec<_>>();
- let second = expected_artifacts(&root)
- .expect("second render")
- .into_iter()
- .map(|artifact| (artifact.relative, artifact.contents))
- .collect::<Vec<_>>();
+ let checked_in = read_regular_file(&root, MANIFEST_RELATIVE).expect("immutable manifest");
+ let manifest: RawSourceRebuildManifest =
+ serde_json::from_slice(&checked_in).expect("typed immutable manifest");
+ let first = canonical_json_bytes(&manifest).expect("first immutable render");
+ let second = canonical_json_bytes(&manifest).expect("second immutable render");
assert_eq!(first, second);
+ assert_eq!(first, checked_in);
}
#[test]
@@ -7476,9 +7527,8 @@ mod tests {
fn schema_rejects_unknown_runtime_fields() {
let schema = manifest_schema();
let root = workspace_root();
- let schema_bytes = canonical_json_bytes(&schema).expect("schema bytes");
- let mut manifest = serde_json::to_value(
- describe_manifest(&root, &schema_bytes).expect("current manifest"),
+ let mut manifest: Value = serde_json::from_slice(
+ &read_regular_file(&root, MANIFEST_RELATIVE).expect("immutable manifest"),
)
.expect("manifest value");
manifest