commit 0a6d6dc44763372fe7da267537fa1f5236b89138
parent 5e91441795b5a838631af610a42d0ae367045bea
Author: triesap <tyson@radroots.org>
Date: Wed, 22 Jul 2026 01:03:00 +0000
blossom: add publication readiness evidence
- validate BUD-02 and BUD-01 observations against exact authored raster bytes
- bound complete GET bodies and single-frame JPEG, PNG, and still-WebP evidence
- freeze predecessor authority with executable vectors and source governance
- document transport-neutral adapter and release boundaries
Diffstat:
16 files changed, 3595 insertions(+), 24 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
@@ -171,6 +171,15 @@ publish policy both pass for the same source revision.
The executable raw-rebuild successor contract freezes the SourceMaintenance
predecessor and the `0001` through `0004` migration inventory; no schema
migration is added.
+<!-- release-change: blossom-publication-readiness-evidence -->
+- Blossom publication media now advances beyond local byte verification only
+ 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.
- 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/contracts/conformance/vectors/blossom/publication_readiness.v1.json b/contracts/conformance/vectors/blossom/publication_readiness.v1.json
@@ -0,0 +1,384 @@
+{
+ "suite": "blossom_publication_readiness",
+ "contract_version": "1.0.0",
+ "vectors": [
+ {
+ "id": "valid_created",
+ "kind": "blossom.verify_publication_readiness.valid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "none"
+ },
+ "expected": {
+ "url": "https://cdn.example/0d1c097e006a87476e84014ba5842f04c725ed2fc5a081743ab2b5bf13a538b9.png",
+ "sha256": "0d1c097e006a87476e84014ba5842f04c725ed2fc5a081743ab2b5bf13a538b9",
+ "size": 70,
+ "media_type": "image/png",
+ "format": "png",
+ "width": 1,
+ "height": 1,
+ "upload_status": 201,
+ "evidence_digest": "c52edeba688fa36c7963a478a35ff78504d7dd79a637c67f93d5acb635110660"
+ }
+ },
+ {
+ "id": "valid_ok_without_authored_dimensions",
+ "kind": "blossom.verify_publication_readiness.valid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "upload_status_200"
+ },
+ "expected": {
+ "url": "https://cdn.example/0d1c097e006a87476e84014ba5842f04c725ed2fc5a081743ab2b5bf13a538b9.png",
+ "sha256": "0d1c097e006a87476e84014ba5842f04c725ed2fc5a081743ab2b5bf13a538b9",
+ "size": 70,
+ "media_type": "image/png",
+ "format": "png",
+ "width": 1,
+ "height": 1,
+ "upload_status": 200
+ }
+ },
+ {
+ "id": "invalid_upload_status",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "upload_status_202"
+ },
+ "expected": {
+ "error": "invalid_bud02_upload_status"
+ }
+ },
+ {
+ "id": "invalid_head_status",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "head_status_204"
+ },
+ "expected": {
+ "error": "invalid_bud01_head_status"
+ }
+ },
+ {
+ "id": "invalid_get_status",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "get_status_206"
+ },
+ "expected": {
+ "error": "invalid_bud01_get_status"
+ }
+ },
+ {
+ "id": "declared_size_over_public_max",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "get_size_over_max"
+ },
+ "expected": {
+ "error": "publication_raster_byte_limit_exceeded"
+ }
+ },
+ {
+ "id": "missing_get_body",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "get_body_missing"
+ },
+ "expected": {
+ "error": "publication_get_body_missing"
+ }
+ },
+ {
+ "id": "short_get_body",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "get_body_short"
+ },
+ "expected": {
+ "error": "publication_get_body_short"
+ }
+ },
+ {
+ "id": "trailing_get_body",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "get_body_trailing"
+ },
+ "expected": {
+ "error": "publication_get_body_trailing"
+ }
+ },
+ {
+ "id": "authored_bytes_short",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "authored_bytes_short"
+ },
+ "expected": {
+ "error": "publication_authored_bytes_size_mismatch"
+ }
+ },
+ {
+ "id": "authored_bytes_wrong_hash",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "authored_bytes_wrong_hash"
+ },
+ "expected": {
+ "error": "publication_authored_bytes_hash_mismatch"
+ }
+ },
+ {
+ "id": "upload_url_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "upload_url_mismatch"
+ },
+ "expected": {
+ "error": "publication_upload_url_mismatch"
+ }
+ },
+ {
+ "id": "upload_hash_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "upload_hash_mismatch"
+ },
+ "expected": {
+ "error": "publication_upload_hash_mismatch"
+ }
+ },
+ {
+ "id": "upload_size_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "upload_size_mismatch"
+ },
+ "expected": {
+ "error": "publication_upload_size_mismatch"
+ }
+ },
+ {
+ "id": "upload_mime_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "upload_mime_mismatch"
+ },
+ "expected": {
+ "error": "publication_upload_media_type_mismatch"
+ }
+ },
+ {
+ "id": "head_url_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "head_url_mismatch"
+ },
+ "expected": {
+ "error": "publication_head_url_mismatch"
+ }
+ },
+ {
+ "id": "head_size_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "head_size_mismatch"
+ },
+ "expected": {
+ "error": "publication_head_size_mismatch"
+ }
+ },
+ {
+ "id": "head_mime_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "head_mime_mismatch"
+ },
+ "expected": {
+ "error": "publication_head_media_type_mismatch"
+ }
+ },
+ {
+ "id": "get_url_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "get_url_mismatch"
+ },
+ "expected": {
+ "error": "publication_get_url_mismatch"
+ }
+ },
+ {
+ "id": "get_declared_size_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "get_declared_size_mismatch"
+ },
+ "expected": {
+ "error": "publication_get_declared_size_mismatch"
+ }
+ },
+ {
+ "id": "get_complete_hash_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "get_bytes_wrong_hash"
+ },
+ "expected": {
+ "error": "publication_retrieved_bytes_hash_mismatch"
+ }
+ },
+ {
+ "id": "unsupported_raster_mime",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "unsupported_mime"
+ },
+ "expected": {
+ "error": "unsupported_publication_raster_media_type"
+ }
+ },
+ {
+ "id": "malformed_raster",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "malformed_container"
+ },
+ "expected": {
+ "error": "invalid_publication_raster"
+ }
+ },
+ {
+ "id": "animated_png",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "animated_png"
+ },
+ "expected": {
+ "error": "publication_raster_frame_count_mismatch"
+ }
+ },
+ {
+ "id": "decode_format_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "decode_format_mismatch"
+ },
+ "expected": {
+ "error": "publication_raster_decode_format_mismatch"
+ }
+ },
+ {
+ "id": "decode_length_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "decode_length_mismatch"
+ },
+ "expected": {
+ "error": "publication_raster_decode_length_mismatch"
+ }
+ },
+ {
+ "id": "decode_hash_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "decode_hash_mismatch"
+ },
+ "expected": {
+ "error": "publication_raster_decode_hash_mismatch"
+ }
+ },
+ {
+ "id": "decode_container_dimension_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "decode_container_dimension_mismatch"
+ },
+ "expected": {
+ "error": "publication_raster_container_dimension_mismatch"
+ }
+ },
+ {
+ "id": "authored_dimension_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "authored_dimension_mismatch"
+ },
+ "expected": {
+ "error": "publication_authored_raster_dimension_mismatch"
+ }
+ },
+ {
+ "id": "decode_zero_frames",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "decode_zero_frames"
+ },
+ "expected": {
+ "error": "publication_raster_frame_count_mismatch"
+ }
+ },
+ {
+ "id": "decode_zero_width",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "decode_zero_width"
+ },
+ "expected": {
+ "error": "publication_raster_dimensions_out_of_range"
+ }
+ },
+ {
+ "id": "decode_dimension_over_max",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "decode_dimension_over_max"
+ },
+ "expected": {
+ "error": "publication_raster_dimensions_out_of_range"
+ }
+ },
+ {
+ "id": "decode_pixel_limit",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "decode_pixel_limit"
+ },
+ "expected": {
+ "error": "publication_raster_pixel_limit_exceeded"
+ }
+ }
+ ]
+}
diff --git a/contracts/events/blossom-media.md b/contracts/events/blossom-media.md
@@ -217,6 +217,38 @@ The public typed API exposes these stable semantic identifiers:
- `blob_hash_mismatch`
- `blob_size_mismatch`
- `blob_media_type_mismatch`
+- `invalid_bud02_upload_status`
+- `invalid_bud01_head_status`
+- `invalid_bud01_get_status`
+- `publication_raster_byte_limit_exceeded`
+- `publication_get_body_allocation_failed`
+- `publication_get_body_length_overflow`
+- `publication_get_body_missing`
+- `publication_get_body_short`
+- `publication_get_body_trailing`
+- `publication_authored_bytes_size_mismatch`
+- `publication_authored_bytes_hash_mismatch`
+- `publication_upload_url_mismatch`
+- `publication_upload_hash_mismatch`
+- `publication_upload_size_mismatch`
+- `publication_upload_media_type_mismatch`
+- `publication_head_url_mismatch`
+- `publication_head_size_mismatch`
+- `publication_head_media_type_mismatch`
+- `publication_get_url_mismatch`
+- `publication_get_declared_size_mismatch`
+- `publication_retrieved_bytes_hash_mismatch`
+- `publication_retrieved_bytes_mismatch`
+- `unsupported_publication_raster_media_type`
+- `invalid_publication_raster`
+- `publication_raster_frame_count_mismatch`
+- `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_container_dimension_mismatch`
+- `publication_authored_raster_dimension_mismatch`
Malformed descriptor JSON can fail in serde before a `RadrootsBlossomError` exists. The executable
descriptor harness uses three additional wire-shape classifications for those cases:
@@ -235,3 +267,47 @@ that the packaged mirror is byte-for-byte current.
The public error enum is non-exhaustive so later BUD slices can add typed failures without breaking
consumers. Adding error detail is allowed, but it must not collapse protocol structure, reference
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:
+
+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;
+2. a BUD-01 `HEAD` has status `200` and matching approved URL, content length, and media type;
+3. a BUD-01 `GET` has status `200`, is collected through
+ `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.
+
+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.
+
+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.
+
+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
+HEAD/GET statuses, and the BUD-02 `uploaded` value. This per-URL digest is an adapter input, not the
+future artifact readiness-binding digest. The artifact adapter remains responsible for proving the
+exact URL-complete set, rejecting missing/duplicate/extra/reordered evidence, and binding that set
+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.
diff --git a/contracts/operations.toml b/contracts/operations.toml
@@ -29,6 +29,17 @@ public = [
"RadrootsBlossomApprovedDescriptor",
"RadrootsBlossomByteCommitment",
"RadrootsBlossomByteVerifiedDescriptor",
+ "RadrootsBlossomBud02UploadStatus",
+ "RadrootsBlossomBud02UploadObservation",
+ "RadrootsBlossomBud01HeadObservation",
+ "RadrootsBlossomBud01GetCollector",
+ "RadrootsBlossomBud01GetObservation",
+ "RadrootsBlossomRasterFormat",
+ "RadrootsBlossomRasterDimensions",
+ "RadrootsBlossomAuthoredRasterDimensions",
+ "RadrootsBlossomRasterDecodeObservation",
+ "RadrootsBlossomPublicationReadinessEvidenceDigest",
+ "RadrootsBlossomPublicationReadinessEvidence",
"RadrootsBlossomAuthorizationAction",
"RadrootsBlossomAuthorizationContent",
"RadrootsBlossomServerDomain",
@@ -351,6 +362,44 @@ rust_types = [
[operations.blossom_verify_descriptor_bytes.conformance]
vector = "contracts/conformance/vectors/blossom/hash_path_and_descriptor.v1.json"
+[operations.blossom_verify_publication_readiness]
+domain = "blossom"
+id = "blossom.verify_publication_readiness"
+stability = "beta"
+inputs = [
+ "RadrootsBlossomByteVerifiedDescriptor",
+ "Bytes",
+ "RadrootsBlossomAuthoredRasterDimensions",
+ "RadrootsBlossomBud02UploadObservation",
+ "RadrootsBlossomBud01HeadObservation",
+ "RadrootsBlossomBud01GetObservation",
+ "RadrootsBlossomRasterDecodeObservation",
+]
+outputs = ["RadrootsBlossomPublicationReadinessEvidence"]
+error_class = "validation_error"
+deterministic = true
+signing = "none"
+transport = "none"
+
+[operations.blossom_verify_publication_readiness.implementation]
+rust_modules = ["crates/blossom/src/publication_readiness.rs"]
+rust_types = [
+ "radroots_blossom::RadrootsBlossomAuthoredRasterDimensions",
+ "radroots_blossom::RadrootsBlossomBud01GetCollector",
+ "radroots_blossom::RadrootsBlossomBud01GetObservation",
+ "radroots_blossom::RadrootsBlossomBud01HeadObservation",
+ "radroots_blossom::RadrootsBlossomBud02UploadObservation",
+ "radroots_blossom::RadrootsBlossomPublicationReadinessEvidence",
+ "radroots_blossom::RadrootsBlossomRasterDecodeObservation",
+]
+
+[operations.blossom_verify_publication_readiness.conformance]
+vector = "contracts/conformance/vectors/blossom/publication_readiness.v1.json"
+case_kinds = [
+ "blossom.verify_publication_readiness.valid",
+ "blossom.verify_publication_readiness.invalid",
+]
+
[operations.blossom_build_upload_authorization_claim]
domain = "blossom"
id = "blossom.build_upload_authorization_claim"
diff --git a/contracts/releases/1.0.0-alpha.1.toml b/contracts/releases/1.0.0-alpha.1.toml
@@ -450,3 +450,15 @@ semver_impacts = [
"change_exported_algorithm_behavior",
]
summary = "Represent relay-event verification, contract admission, valid-stream eligibility, and current visibility as independent exhaustive outcomes; enforce hard raw-event, aggregate raw-JSON, and pre-parse per-event fetch bounds; and remove bare-envelope legacy replica ingestion from the default public feature surface."
+
+[[changes]]
+id = "blossom-publication-readiness-evidence"
+classification = "feature"
+semver_impacts = [
+ "add_exported_type",
+ "add_exported_function",
+ "add_exported_constant",
+ "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."
diff --git a/crates/blossom/README b/crates/blossom/README
@@ -2,7 +2,8 @@
`radroots_blossom` provides portable, runtime-independent primitives for
Blossom blob hashes, root hash paths, blob URLs, BUD-02 descriptors,
-Radroots-approved byte verification, and pure BUD-11 authorization claims.
+Radroots-approved byte verification, publication-readiness evidence, and pure
+BUD-11 authorization claims.
The crate is `no_std + alloc`, performs no HTTP requests, and does not depend on
Nostr event types. Structural Blossom validity is kept separate from the
@@ -15,6 +16,16 @@ claim construction and endpoint validation; signing and canonical
`Authorization: Nostr` encoding live behind the `radroots_nostr` `blossom`
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.
+
Protocol behavior is pinned to Blossom commit
`b5bd2801d1763aa635fc8fea7a76597e0eb18990`:
diff --git a/crates/blossom/src/error.rs b/crates/blossom/src/error.rs
@@ -40,6 +40,38 @@ pub enum RadrootsBlossomError {
AuthorizationServerMismatch,
AuthorizationHashRequired,
AuthorizationHashMismatch,
+ InvalidBud02UploadStatus { actual: u16 },
+ InvalidBud01HeadStatus { actual: u16 },
+ InvalidBud01GetStatus { actual: u16 },
+ PublicationRasterByteLimitExceeded { declared: u64, maximum: u64 },
+ PublicationGetBodyAllocationFailed,
+ PublicationGetBodyLengthOverflow,
+ PublicationGetBodyMissing,
+ PublicationGetBodyShort { declared: u64, actual: u64 },
+ PublicationGetBodyTrailing { declared: u64, actual: u64 },
+ PublicationAuthoredBytesSizeMismatch { expected: u64, actual: u64 },
+ PublicationAuthoredBytesHashMismatch,
+ PublicationUploadUrlMismatch,
+ PublicationUploadHashMismatch,
+ PublicationUploadSizeMismatch { expected: u64, actual: u64 },
+ PublicationUploadMediaTypeMismatch,
+ PublicationHeadUrlMismatch,
+ PublicationHeadSizeMismatch { expected: u64, actual: u64 },
+ PublicationHeadMediaTypeMismatch,
+ PublicationGetUrlMismatch,
+ PublicationGetDeclaredSizeMismatch { expected: u64, actual: u64 },
+ PublicationRetrievedBytesHashMismatch,
+ PublicationRetrievedBytesMismatch,
+ UnsupportedPublicationRasterMediaType,
+ InvalidPublicationRaster,
+ PublicationRasterFrameCountMismatch { actual: u32 },
+ PublicationRasterDimensionsOutOfRange { width: u32, height: u32 },
+ PublicationRasterPixelLimitExceeded { pixels: u64 },
+ PublicationRasterDecodeFormatMismatch,
+ PublicationRasterDecodeLengthMismatch { expected: u64, actual: u64 },
+ PublicationRasterDecodeHashMismatch,
+ PublicationRasterContainerDimensionMismatch,
+ PublicationAuthoredRasterDimensionMismatch,
}
impl RadrootsBlossomError {
@@ -82,6 +114,64 @@ impl RadrootsBlossomError {
Self::AuthorizationServerMismatch => "authorization_server_mismatch",
Self::AuthorizationHashRequired => "authorization_hash_required",
Self::AuthorizationHashMismatch => "authorization_hash_mismatch",
+ Self::InvalidBud02UploadStatus { .. } => "invalid_bud02_upload_status",
+ Self::InvalidBud01HeadStatus { .. } => "invalid_bud01_head_status",
+ Self::InvalidBud01GetStatus { .. } => "invalid_bud01_get_status",
+ Self::PublicationRasterByteLimitExceeded { .. } => {
+ "publication_raster_byte_limit_exceeded"
+ }
+ Self::PublicationGetBodyAllocationFailed => "publication_get_body_allocation_failed",
+ Self::PublicationGetBodyLengthOverflow => "publication_get_body_length_overflow",
+ Self::PublicationGetBodyMissing => "publication_get_body_missing",
+ Self::PublicationGetBodyShort { .. } => "publication_get_body_short",
+ Self::PublicationGetBodyTrailing { .. } => "publication_get_body_trailing",
+ Self::PublicationAuthoredBytesSizeMismatch { .. } => {
+ "publication_authored_bytes_size_mismatch"
+ }
+ Self::PublicationAuthoredBytesHashMismatch => {
+ "publication_authored_bytes_hash_mismatch"
+ }
+ Self::PublicationUploadUrlMismatch => "publication_upload_url_mismatch",
+ Self::PublicationUploadHashMismatch => "publication_upload_hash_mismatch",
+ Self::PublicationUploadSizeMismatch { .. } => "publication_upload_size_mismatch",
+ Self::PublicationUploadMediaTypeMismatch => "publication_upload_media_type_mismatch",
+ Self::PublicationHeadUrlMismatch => "publication_head_url_mismatch",
+ Self::PublicationHeadSizeMismatch { .. } => "publication_head_size_mismatch",
+ Self::PublicationHeadMediaTypeMismatch => "publication_head_media_type_mismatch",
+ Self::PublicationGetUrlMismatch => "publication_get_url_mismatch",
+ Self::PublicationGetDeclaredSizeMismatch { .. } => {
+ "publication_get_declared_size_mismatch"
+ }
+ Self::PublicationRetrievedBytesHashMismatch => {
+ "publication_retrieved_bytes_hash_mismatch"
+ }
+ Self::PublicationRetrievedBytesMismatch => "publication_retrieved_bytes_mismatch",
+ Self::UnsupportedPublicationRasterMediaType => {
+ "unsupported_publication_raster_media_type"
+ }
+ Self::InvalidPublicationRaster => "invalid_publication_raster",
+ Self::PublicationRasterFrameCountMismatch { .. } => {
+ "publication_raster_frame_count_mismatch"
+ }
+ Self::PublicationRasterDimensionsOutOfRange { .. } => {
+ "publication_raster_dimensions_out_of_range"
+ }
+ Self::PublicationRasterPixelLimitExceeded { .. } => {
+ "publication_raster_pixel_limit_exceeded"
+ }
+ Self::PublicationRasterDecodeFormatMismatch => {
+ "publication_raster_decode_format_mismatch"
+ }
+ Self::PublicationRasterDecodeLengthMismatch { .. } => {
+ "publication_raster_decode_length_mismatch"
+ }
+ Self::PublicationRasterDecodeHashMismatch => "publication_raster_decode_hash_mismatch",
+ Self::PublicationRasterContainerDimensionMismatch => {
+ "publication_raster_container_dimension_mismatch"
+ }
+ Self::PublicationAuthoredRasterDimensionMismatch => {
+ "publication_authored_raster_dimension_mismatch"
+ }
}
}
}
@@ -182,6 +272,111 @@ impl fmt::Display for RadrootsBlossomError {
Self::AuthorizationHashMismatch => {
f.write_str("Blossom authorization does not include the target blob hash")
}
+ Self::InvalidBud02UploadStatus { actual } => {
+ write!(f, "BUD-02 upload status must be 200 or 201, got {actual}")
+ }
+ Self::InvalidBud01HeadStatus { actual } => {
+ write!(f, "BUD-01 HEAD status must be 200, got {actual}")
+ }
+ Self::InvalidBud01GetStatus { actual } => {
+ write!(f, "BUD-01 GET status must be 200, got {actual}")
+ }
+ Self::PublicationRasterByteLimitExceeded { declared, maximum } => write!(
+ f,
+ "publication raster declares {declared} bytes, exceeding maximum {maximum}"
+ ),
+ Self::PublicationGetBodyAllocationFailed => {
+ f.write_str("publication GET body allocation failed")
+ }
+ Self::PublicationGetBodyLengthOverflow => {
+ f.write_str("publication GET body length overflowed")
+ }
+ Self::PublicationGetBodyMissing => f.write_str("publication GET body is missing"),
+ Self::PublicationGetBodyShort { declared, actual } => write!(
+ f,
+ "publication GET body is short: declared {declared}, got {actual}"
+ ),
+ Self::PublicationGetBodyTrailing { declared, actual } => write!(
+ f,
+ "publication GET body has trailing bytes: declared {declared}, got {actual}"
+ ),
+ Self::PublicationAuthoredBytesSizeMismatch { expected, actual } => write!(
+ f,
+ "authored raster byte size mismatch: expected {expected}, got {actual}"
+ ),
+ Self::PublicationAuthoredBytesHashMismatch => {
+ f.write_str("authored raster bytes do not match the sealed descriptor hash")
+ }
+ Self::PublicationUploadUrlMismatch => {
+ f.write_str("BUD-02 upload descriptor URL does not match the authored URL")
+ }
+ Self::PublicationUploadHashMismatch => {
+ f.write_str("BUD-02 upload descriptor hash does not match the authored hash")
+ }
+ Self::PublicationUploadSizeMismatch { expected, actual } => write!(
+ f,
+ "BUD-02 upload descriptor size mismatch: expected {expected}, got {actual}"
+ ),
+ Self::PublicationUploadMediaTypeMismatch => f.write_str(
+ "BUD-02 upload descriptor media type does not match the authored media type",
+ ),
+ Self::PublicationHeadUrlMismatch => {
+ f.write_str("BUD-01 HEAD URL does not match the authored URL")
+ }
+ Self::PublicationHeadSizeMismatch { expected, actual } => write!(
+ f,
+ "BUD-01 HEAD content length mismatch: expected {expected}, got {actual}"
+ ),
+ Self::PublicationHeadMediaTypeMismatch => {
+ f.write_str("BUD-01 HEAD media type does not match the authored media type")
+ }
+ Self::PublicationGetUrlMismatch => {
+ f.write_str("BUD-01 GET URL does not match the authored URL")
+ }
+ Self::PublicationGetDeclaredSizeMismatch { expected, actual } => write!(
+ f,
+ "BUD-01 GET declared size mismatch: expected {expected}, got {actual}"
+ ),
+ Self::PublicationRetrievedBytesHashMismatch => {
+ f.write_str("BUD-01 GET complete-byte hash does not match the authored hash")
+ }
+ Self::PublicationRetrievedBytesMismatch => {
+ f.write_str("BUD-01 GET bytes differ from the exact authored raster bytes")
+ }
+ Self::UnsupportedPublicationRasterMediaType => f.write_str(
+ "publication raster media type must be image/jpeg, image/png, or image/webp",
+ ),
+ 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::PublicationRasterDimensionsOutOfRange { width, height } => write!(
+ f,
+ "publication raster dimensions must be within 1..=16384, got {width}x{height}"
+ ),
+ Self::PublicationRasterPixelLimitExceeded { pixels } => write!(
+ 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!(
+ f,
+ "decoded raster byte length mismatch: expected {expected}, got {actual}"
+ ),
+ Self::PublicationRasterDecodeHashMismatch => {
+ f.write_str("decoded raster complete-byte hash does not match the authored hash")
+ }
+ Self::PublicationRasterContainerDimensionMismatch => f.write_str(
+ "decoded raster dimensions do not match the dimensions encoded by the container",
+ ),
+ Self::PublicationAuthoredRasterDimensionMismatch => {
+ f.write_str("decoded raster dimensions do not match the authored dimensions")
+ }
}
}
}
@@ -215,6 +410,65 @@ mod tests {
actual: 2,
},
RadrootsBlossomError::BlobMediaTypeMismatch,
+ RadrootsBlossomError::InvalidBud02UploadStatus { actual: 202 },
+ RadrootsBlossomError::InvalidBud01HeadStatus { actual: 204 },
+ RadrootsBlossomError::InvalidBud01GetStatus { actual: 206 },
+ RadrootsBlossomError::PublicationRasterByteLimitExceeded {
+ declared: 2,
+ maximum: 1,
+ },
+ RadrootsBlossomError::PublicationGetBodyAllocationFailed,
+ RadrootsBlossomError::PublicationGetBodyLengthOverflow,
+ RadrootsBlossomError::PublicationGetBodyMissing,
+ RadrootsBlossomError::PublicationGetBodyShort {
+ declared: 2,
+ actual: 1,
+ },
+ RadrootsBlossomError::PublicationGetBodyTrailing {
+ declared: 1,
+ actual: 2,
+ },
+ RadrootsBlossomError::PublicationAuthoredBytesSizeMismatch {
+ expected: 1,
+ actual: 2,
+ },
+ RadrootsBlossomError::PublicationAuthoredBytesHashMismatch,
+ RadrootsBlossomError::PublicationUploadUrlMismatch,
+ RadrootsBlossomError::PublicationUploadHashMismatch,
+ RadrootsBlossomError::PublicationUploadSizeMismatch {
+ expected: 1,
+ actual: 2,
+ },
+ RadrootsBlossomError::PublicationUploadMediaTypeMismatch,
+ RadrootsBlossomError::PublicationHeadUrlMismatch,
+ RadrootsBlossomError::PublicationHeadSizeMismatch {
+ expected: 1,
+ actual: 2,
+ },
+ RadrootsBlossomError::PublicationHeadMediaTypeMismatch,
+ RadrootsBlossomError::PublicationGetUrlMismatch,
+ RadrootsBlossomError::PublicationGetDeclaredSizeMismatch {
+ expected: 1,
+ actual: 2,
+ },
+ RadrootsBlossomError::PublicationRetrievedBytesHashMismatch,
+ RadrootsBlossomError::PublicationRetrievedBytesMismatch,
+ RadrootsBlossomError::UnsupportedPublicationRasterMediaType,
+ RadrootsBlossomError::InvalidPublicationRaster,
+ RadrootsBlossomError::PublicationRasterFrameCountMismatch { actual: 2 },
+ RadrootsBlossomError::PublicationRasterDimensionsOutOfRange {
+ width: 0,
+ height: 1,
+ },
+ RadrootsBlossomError::PublicationRasterPixelLimitExceeded { pixels: 20_000_001 },
+ RadrootsBlossomError::PublicationRasterDecodeFormatMismatch,
+ RadrootsBlossomError::PublicationRasterDecodeLengthMismatch {
+ expected: 1,
+ actual: 2,
+ },
+ RadrootsBlossomError::PublicationRasterDecodeHashMismatch,
+ RadrootsBlossomError::PublicationRasterContainerDimensionMismatch,
+ RadrootsBlossomError::PublicationAuthoredRasterDimensionMismatch,
RadrootsBlossomError::InvalidAuthorizationContent,
RadrootsBlossomError::InvalidAuthorizationAction,
RadrootsBlossomError::InvalidAuthorizationServerDomain,
diff --git a/crates/blossom/src/lib.rs b/crates/blossom/src/lib.rs
@@ -8,6 +8,7 @@ pub mod authorization;
pub mod descriptor;
pub mod error;
pub mod hash;
+pub mod publication_readiness;
pub mod url;
pub use authorization::{
@@ -25,6 +26,17 @@ pub use descriptor::{
};
pub use error::RadrootsBlossomError;
pub use hash::{RadrootsBlossomFileExtension, RadrootsBlossomHashPath, RadrootsBlossomSha256};
+pub use publication_readiness::{
+ RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_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,
+};
pub use url::{RadrootsBlossomApprovedBlobUrl, RadrootsBlossomBlobUrl};
pub const RADROOTS_BLOSSOM_PROTOCOL_COMMIT: &str = "b5bd2801d1763aa635fc8fea7a76597e0eb18990";
diff --git a/crates/blossom/src/publication_readiness.rs b/crates/blossom/src/publication_readiness.rs
@@ -0,0 +1,1189 @@
+use alloc::vec::Vec;
+use core::fmt;
+use sha2::{Digest, Sha256};
+
+use crate::{
+ RadrootsBlossomApprovedBlobUrl, RadrootsBlossomBlobDescriptor,
+ RadrootsBlossomByteVerifiedDescriptor, 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_DIMENSION: u32 = 16_384;
+pub const RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_PIXELS: u64 = 20_000_000;
+
+const READINESS_EVIDENCE_DIGEST_DOMAIN: &[u8] =
+ b"radroots.blossom.publication-readiness-evidence.v1\0";
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub enum RadrootsBlossomBud02UploadStatus {
+ Ok,
+ Created,
+}
+
+impl RadrootsBlossomBud02UploadStatus {
+ pub const fn as_u16(self) -> u16 {
+ match self {
+ Self::Ok => 200,
+ Self::Created => 201,
+ }
+ }
+
+ fn parse(status: u16) -> Result<Self, RadrootsBlossomError> {
+ match status {
+ 200 => Ok(Self::Ok),
+ 201 => Ok(Self::Created),
+ actual => Err(RadrootsBlossomError::InvalidBud02UploadStatus { actual }),
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub enum RadrootsBlossomRasterFormat {
+ Jpeg,
+ Png,
+ StillWebP,
+}
+
+impl RadrootsBlossomRasterFormat {
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::Jpeg => "jpeg",
+ Self::Png => "png",
+ Self::StillWebP => "still_webp",
+ }
+ }
+
+ pub fn from_media_type(
+ media_type: &RadrootsBlossomMediaType,
+ ) -> Result<Self, RadrootsBlossomError> {
+ match media_type.as_str() {
+ "image/jpeg" => Ok(Self::Jpeg),
+ "image/png" => Ok(Self::Png),
+ "image/webp" => Ok(Self::StillWebP),
+ _ => Err(RadrootsBlossomError::UnsupportedPublicationRasterMediaType),
+ }
+ }
+
+ const fn digest_code(self) -> u8 {
+ match self {
+ Self::Jpeg => 1,
+ Self::Png => 2,
+ Self::StillWebP => 3,
+ }
+ }
+}
+
+impl fmt::Display for RadrootsBlossomRasterFormat {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(self.as_str())
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub struct RadrootsBlossomRasterDimensions {
+ width: u32,
+ height: u32,
+}
+
+impl RadrootsBlossomRasterDimensions {
+ pub fn new(width: u32, height: u32) -> Result<Self, RadrootsBlossomError> {
+ if width == 0
+ || height == 0
+ || width > RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DIMENSION
+ || height > RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_DIMENSION
+ {
+ return Err(
+ RadrootsBlossomError::PublicationRasterDimensionsOutOfRange { width, height },
+ );
+ }
+ let pixels = u64::from(width) * u64::from(height);
+ if pixels > RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_PIXELS {
+ return Err(RadrootsBlossomError::PublicationRasterPixelLimitExceeded { pixels });
+ }
+ Ok(Self { width, height })
+ }
+
+ pub const fn width(self) -> u32 {
+ self.width
+ }
+
+ pub const fn height(self) -> u32 {
+ self.height
+ }
+
+ pub const fn pixels(self) -> u64 {
+ self.width as u64 * self.height as u64
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub enum RadrootsBlossomAuthoredRasterDimensions {
+ Unspecified,
+ Exact(RadrootsBlossomRasterDimensions),
+}
+
+impl RadrootsBlossomAuthoredRasterDimensions {
+ const fn exact(self) -> Option<RadrootsBlossomRasterDimensions> {
+ match self {
+ Self::Unspecified => None,
+ Self::Exact(dimensions) => Some(dimensions),
+ }
+ }
+}
+
+#[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
+/// approval policy. It does not represent BUD-11 authorization or entitlement.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsBlossomBud02UploadObservation {
+ status: RadrootsBlossomBud02UploadStatus,
+ descriptor: crate::RadrootsBlossomApprovedDescriptor,
+}
+
+impl RadrootsBlossomBud02UploadObservation {
+ pub fn new(
+ status: u16,
+ descriptor: RadrootsBlossomBlobDescriptor,
+ ) -> Result<Self, RadrootsBlossomError> {
+ Ok(Self {
+ status: RadrootsBlossomBud02UploadStatus::parse(status)?,
+ descriptor: descriptor.approve_reference()?,
+ })
+ }
+
+ pub const fn status(&self) -> RadrootsBlossomBud02UploadStatus {
+ self.status
+ }
+
+ pub fn descriptor(&self) -> &crate::RadrootsBlossomApprovedDescriptor {
+ &self.descriptor
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsBlossomBud01HeadObservation {
+ url: RadrootsBlossomApprovedBlobUrl,
+ content_length: u64,
+ media_type: RadrootsBlossomMediaType,
+}
+
+impl RadrootsBlossomBud01HeadObservation {
+ pub fn new(
+ status: u16,
+ url: RadrootsBlossomApprovedBlobUrl,
+ content_length: u64,
+ media_type: RadrootsBlossomMediaType,
+ ) -> Result<Self, RadrootsBlossomError> {
+ if status != 200 {
+ return Err(RadrootsBlossomError::InvalidBud01HeadStatus { actual: status });
+ }
+ Ok(Self {
+ url,
+ content_length,
+ media_type,
+ })
+ }
+
+ pub fn url(&self) -> &RadrootsBlossomApprovedBlobUrl {
+ &self.url
+ }
+
+ pub const fn content_length(&self) -> u64 {
+ self.content_length
+ }
+
+ pub fn media_type(&self) -> &RadrootsBlossomMediaType {
+ &self.media_type
+ }
+}
+
+pub struct RadrootsBlossomBud01GetCollector {
+ url: RadrootsBlossomApprovedBlobUrl,
+ declared_size: u64,
+ bytes: Vec<u8>,
+}
+
+impl RadrootsBlossomBud01GetCollector {
+ pub fn new(
+ status: u16,
+ url: RadrootsBlossomApprovedBlobUrl,
+ declared_size: u64,
+ ) -> Result<Self, RadrootsBlossomError> {
+ if status != 200 {
+ return Err(RadrootsBlossomError::InvalidBud01GetStatus { actual: status });
+ }
+ if declared_size > RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_BYTES {
+ return Err(RadrootsBlossomError::PublicationRasterByteLimitExceeded {
+ declared: declared_size,
+ maximum: RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_BYTES,
+ });
+ }
+ let capacity = usize::try_from(declared_size)
+ .map_err(|_| RadrootsBlossomError::PublicationGetBodyAllocationFailed)?;
+ let mut bytes = Vec::new();
+ bytes
+ .try_reserve_exact(capacity)
+ .map_err(|_| RadrootsBlossomError::PublicationGetBodyAllocationFailed)?;
+ Ok(Self {
+ url,
+ declared_size,
+ bytes,
+ })
+ }
+
+ pub fn push_chunk(&mut self, chunk: &[u8]) -> Result<(), RadrootsBlossomError> {
+ let actual = self
+ .bytes
+ .len()
+ .checked_add(chunk.len())
+ .and_then(|value| u64::try_from(value).ok())
+ .ok_or(RadrootsBlossomError::PublicationGetBodyLengthOverflow)?;
+ if actual > self.declared_size {
+ return Err(RadrootsBlossomError::PublicationGetBodyTrailing {
+ declared: self.declared_size,
+ actual,
+ });
+ }
+ self.bytes.extend_from_slice(chunk);
+ Ok(())
+ }
+
+ pub fn finish(self) -> Result<RadrootsBlossomBud01GetObservation, RadrootsBlossomError> {
+ let actual = self.bytes.len() as u64;
+ if actual == 0 {
+ return Err(RadrootsBlossomError::PublicationGetBodyMissing);
+ }
+ if actual < self.declared_size {
+ return Err(RadrootsBlossomError::PublicationGetBodyShort {
+ declared: self.declared_size,
+ actual,
+ });
+ }
+ Ok(RadrootsBlossomBud01GetObservation {
+ url: self.url,
+ declared_size: self.declared_size,
+ bytes: self.bytes,
+ })
+ }
+}
+
+pub struct RadrootsBlossomBud01GetObservation {
+ url: RadrootsBlossomApprovedBlobUrl,
+ declared_size: u64,
+ bytes: Vec<u8>,
+}
+
+impl RadrootsBlossomBud01GetObservation {
+ pub fn from_complete_body(
+ status: u16,
+ url: RadrootsBlossomApprovedBlobUrl,
+ declared_size: u64,
+ bytes: &[u8],
+ ) -> Result<Self, RadrootsBlossomError> {
+ let mut collector = RadrootsBlossomBud01GetCollector::new(status, url, declared_size)?;
+ collector.push_chunk(bytes)?;
+ collector.finish()
+ }
+
+ pub fn url(&self) -> &RadrootsBlossomApprovedBlobUrl {
+ &self.url
+ }
+
+ pub const fn declared_size(&self) -> u64 {
+ self.declared_size
+ }
+
+ pub fn bytes(&self) -> &[u8] {
+ &self.bytes
+ }
+}
+
+impl fmt::Debug for RadrootsBlossomBud01GetObservation {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("RadrootsBlossomBud01GetObservation")
+ .field("url", &self.url)
+ .field("declared_size", &self.declared_size)
+ .field("body_length", &self.bytes.len())
+ .finish()
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub struct RadrootsBlossomPublicationReadinessEvidenceDigest(RadrootsBlossomSha256);
+
+impl RadrootsBlossomPublicationReadinessEvidenceDigest {
+ pub const fn as_sha256(self) -> RadrootsBlossomSha256 {
+ self.0
+ }
+}
+
+impl fmt::Display for RadrootsBlossomPublicationReadinessEvidenceDigest {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.0.fmt(formatter)
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsBlossomPublicationReadinessEvidence {
+ url: RadrootsBlossomApprovedBlobUrl,
+ sha256: RadrootsBlossomSha256,
+ size: u64,
+ media_type: RadrootsBlossomMediaType,
+ raster_format: RadrootsBlossomRasterFormat,
+ dimensions: RadrootsBlossomRasterDimensions,
+ bud02_status: RadrootsBlossomBud02UploadStatus,
+ uploaded: u64,
+ evidence_digest: RadrootsBlossomPublicationReadinessEvidenceDigest,
+}
+
+impl RadrootsBlossomPublicationReadinessEvidence {
+ pub fn url(&self) -> &RadrootsBlossomApprovedBlobUrl {
+ &self.url
+ }
+
+ pub const fn sha256(&self) -> RadrootsBlossomSha256 {
+ self.sha256
+ }
+
+ pub const fn size(&self) -> u64 {
+ self.size
+ }
+
+ pub fn media_type(&self) -> &RadrootsBlossomMediaType {
+ &self.media_type
+ }
+
+ pub const fn raster_format(&self) -> RadrootsBlossomRasterFormat {
+ self.raster_format
+ }
+
+ pub const fn dimensions(&self) -> RadrootsBlossomRasterDimensions {
+ self.dimensions
+ }
+
+ pub const fn bud02_status(&self) -> RadrootsBlossomBud02UploadStatus {
+ self.bud02_status
+ }
+
+ pub const fn uploaded(&self) -> u64 {
+ self.uploaded
+ }
+
+ pub const fn evidence_digest(&self) -> RadrootsBlossomPublicationReadinessEvidenceDigest {
+ self.evidence_digest
+ }
+}
+
+pub fn verify_publication_readiness(
+ authored_descriptor: &RadrootsBlossomByteVerifiedDescriptor,
+ exact_authored_bytes: &[u8],
+ authored_dimensions: RadrootsBlossomAuthoredRasterDimensions,
+ upload: &RadrootsBlossomBud02UploadObservation,
+ head: &RadrootsBlossomBud01HeadObservation,
+ get: &RadrootsBlossomBud01GetObservation,
+ decode: &RadrootsBlossomRasterDecodeObservation,
+) -> Result<RadrootsBlossomPublicationReadinessEvidence, RadrootsBlossomError> {
+ let expected_url = authored_descriptor.url();
+ let expected_hash = authored_descriptor.sha256();
+ let expected_size = authored_descriptor.size();
+ let expected_media_type = authored_descriptor.media_type();
+
+ if expected_size > RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_BYTES {
+ return Err(RadrootsBlossomError::PublicationRasterByteLimitExceeded {
+ declared: expected_size,
+ maximum: RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_BYTES,
+ });
+ }
+ let authored_size = exact_authored_bytes.len() as u64;
+ if authored_size != expected_size {
+ return Err(RadrootsBlossomError::PublicationAuthoredBytesSizeMismatch {
+ expected: expected_size,
+ actual: authored_size,
+ });
+ }
+ if RadrootsBlossomSha256::digest(exact_authored_bytes) != expected_hash {
+ return Err(RadrootsBlossomError::PublicationAuthoredBytesHashMismatch);
+ }
+
+ let upload_descriptor = upload.descriptor().descriptor();
+ if upload_descriptor.sha256() != expected_hash {
+ return Err(RadrootsBlossomError::PublicationUploadHashMismatch);
+ }
+ if upload.descriptor().url() != expected_url {
+ return Err(RadrootsBlossomError::PublicationUploadUrlMismatch);
+ }
+ if upload_descriptor.size() != expected_size {
+ return Err(RadrootsBlossomError::PublicationUploadSizeMismatch {
+ expected: expected_size,
+ actual: upload_descriptor.size(),
+ });
+ }
+ if upload_descriptor.media_type() != expected_media_type {
+ return Err(RadrootsBlossomError::PublicationUploadMediaTypeMismatch);
+ }
+
+ if head.url() != expected_url {
+ return Err(RadrootsBlossomError::PublicationHeadUrlMismatch);
+ }
+ if head.content_length() != expected_size {
+ return Err(RadrootsBlossomError::PublicationHeadSizeMismatch {
+ expected: expected_size,
+ actual: head.content_length(),
+ });
+ }
+ if head.media_type() != expected_media_type {
+ return Err(RadrootsBlossomError::PublicationHeadMediaTypeMismatch);
+ }
+
+ if get.url() != expected_url {
+ return Err(RadrootsBlossomError::PublicationGetUrlMismatch);
+ }
+ if get.declared_size() != expected_size {
+ return Err(RadrootsBlossomError::PublicationGetDeclaredSizeMismatch {
+ expected: expected_size,
+ 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);
+ }
+
+ 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);
+ }
+ if authored_dimensions
+ .exact()
+ .is_some_and(|dimensions| dimensions != decode.dimensions())
+ {
+ return Err(RadrootsBlossomError::PublicationAuthoredRasterDimensionMismatch);
+ }
+
+ let evidence_digest = evidence_digest(
+ authored_descriptor,
+ expected_format,
+ decode.dimensions(),
+ upload,
+ );
+ Ok(RadrootsBlossomPublicationReadinessEvidence {
+ url: expected_url.clone(),
+ sha256: expected_hash,
+ size: expected_size,
+ media_type: expected_media_type.clone(),
+ raster_format: expected_format,
+ dimensions: decode.dimensions(),
+ bud02_status: upload.status(),
+ uploaded: upload_descriptor.uploaded(),
+ evidence_digest,
+ })
+}
+
+fn evidence_digest(
+ descriptor: &RadrootsBlossomByteVerifiedDescriptor,
+ format: RadrootsBlossomRasterFormat,
+ dimensions: RadrootsBlossomRasterDimensions,
+ upload: &RadrootsBlossomBud02UploadObservation,
+) -> RadrootsBlossomPublicationReadinessEvidenceDigest {
+ let mut hasher = Sha256::new();
+ hasher.update(READINESS_EVIDENCE_DIGEST_DOMAIN);
+ hasher.update(RADROOTS_BLOSSOM_PUBLICATION_READINESS_POLICY_VERSION.to_be_bytes());
+ update_length_prefixed(&mut hasher, descriptor.url().as_str().as_bytes());
+ hasher.update(descriptor.sha256().as_bytes());
+ hasher.update(descriptor.size().to_be_bytes());
+ update_length_prefixed(&mut hasher, descriptor.media_type().as_str().as_bytes());
+ hasher.update([format.digest_code()]);
+ hasher.update(dimensions.width().to_be_bytes());
+ hasher.update(dimensions.height().to_be_bytes());
+ hasher.update(upload.status().as_u16().to_be_bytes());
+ hasher.update(200_u16.to_be_bytes());
+ hasher.update(200_u16.to_be_bytes());
+ hasher.update(upload.descriptor().descriptor().uploaded().to_be_bytes());
+ let digest = hasher.finalize();
+ let mut bytes = [0_u8; 32];
+ bytes.copy_from_slice(&digest);
+ RadrootsBlossomPublicationReadinessEvidenceDigest(RadrootsBlossomSha256::from_bytes(bytes))
+}
+
+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),
+ }
+}
+
+fn invalid_raster<T>() -> Result<T, RadrootsBlossomError> {
+ Err(RadrootsBlossomError::InvalidPublicationRaster)
+}
+
+fn validate_png_container(
+ bytes: &[u8],
+) -> Result<RadrootsBlossomRasterDimensions, RadrootsBlossomError> {
+ const SIGNATURE: &[u8; 8] = b"\x89PNG\r\n\x1a\n";
+ if !bytes.starts_with(SIGNATURE) {
+ return invalid_raster();
+ }
+ let mut position = SIGNATURE.len();
+ let mut dimensions = None;
+ let mut has_image_data = false;
+ while position < bytes.len() {
+ let header_end = position
+ .checked_add(8)
+ .ok_or(RadrootsBlossomError::InvalidPublicationRaster)?;
+ let header = bytes
+ .get(position..header_end)
+ .ok_or(RadrootsBlossomError::InvalidPublicationRaster)?;
+ let length = u32::from_be_bytes(
+ header[..4]
+ .try_into()
+ .map_err(|_| RadrootsBlossomError::InvalidPublicationRaster)?,
+ ) as usize;
+ let kind: [u8; 4] = header[4..]
+ .try_into()
+ .map_err(|_| RadrootsBlossomError::InvalidPublicationRaster)?;
+ let data_start = header_end;
+ let data_end = data_start
+ .checked_add(length)
+ .ok_or(RadrootsBlossomError::InvalidPublicationRaster)?;
+ let chunk_end = data_end
+ .checked_add(4)
+ .ok_or(RadrootsBlossomError::InvalidPublicationRaster)?;
+ let data = bytes
+ .get(data_start..data_end)
+ .ok_or(RadrootsBlossomError::InvalidPublicationRaster)?;
+ if chunk_end > bytes.len() {
+ return invalid_raster();
+ }
+ position = chunk_end;
+
+ match &kind {
+ b"IHDR" if dimensions.is_none() && data.len() == 13 && data_start == 16 => {
+ let width = u32::from_be_bytes(
+ data[..4]
+ .try_into()
+ .map_err(|_| RadrootsBlossomError::InvalidPublicationRaster)?,
+ );
+ let height = u32::from_be_bytes(
+ data[4..8]
+ .try_into()
+ .map_err(|_| RadrootsBlossomError::InvalidPublicationRaster)?,
+ );
+ dimensions = Some(RadrootsBlossomRasterDimensions::new(width, height)?);
+ }
+ 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"IEND" if data.is_empty() && has_image_data && position == bytes.len() => {
+ return dimensions.ok_or(RadrootsBlossomError::InvalidPublicationRaster);
+ }
+ b"IEND" => return invalid_raster(),
+ _ if dimensions.is_none() => return invalid_raster(),
+ _ => {}
+ }
+ }
+ invalid_raster()
+}
+
+fn validate_webp_container(
+ bytes: &[u8],
+) -> Result<Option<RadrootsBlossomRasterDimensions>, RadrootsBlossomError> {
+ if bytes.len() < 20 || &bytes[..4] != b"RIFF" || &bytes[8..12] != b"WEBP" {
+ return invalid_raster();
+ }
+ let riff_size = u32::from_le_bytes(
+ bytes[4..8]
+ .try_into()
+ .map_err(|_| RadrootsBlossomError::InvalidPublicationRaster)?,
+ ) as usize;
+ if riff_size.checked_add(8) != Some(bytes.len()) {
+ return invalid_raster();
+ }
+
+ let mut position = 12_usize;
+ let mut dimensions = None;
+ let mut primary_chunks = 0_u8;
+ while position < bytes.len() {
+ let header_end = position
+ .checked_add(8)
+ .ok_or(RadrootsBlossomError::InvalidPublicationRaster)?;
+ let header = bytes
+ .get(position..header_end)
+ .ok_or(RadrootsBlossomError::InvalidPublicationRaster)?;
+ let kind: [u8; 4] = header[..4]
+ .try_into()
+ .map_err(|_| RadrootsBlossomError::InvalidPublicationRaster)?;
+ let length = u32::from_le_bytes(
+ header[4..]
+ .try_into()
+ .map_err(|_| RadrootsBlossomError::InvalidPublicationRaster)?,
+ ) as usize;
+ let data_end = header_end
+ .checked_add(length)
+ .ok_or(RadrootsBlossomError::InvalidPublicationRaster)?;
+ let padded_end = data_end
+ .checked_add(length & 1)
+ .ok_or(RadrootsBlossomError::InvalidPublicationRaster)?;
+ let data = bytes
+ .get(header_end..data_end)
+ .ok_or(RadrootsBlossomError::InvalidPublicationRaster)?;
+ if padded_end > bytes.len() {
+ return invalid_raster();
+ }
+ position = padded_end;
+
+ match &kind {
+ b"ANIM" | b"ANMF" => {
+ return Err(RadrootsBlossomError::PublicationRasterFrameCountMismatch {
+ actual: 2,
+ });
+ }
+ b"VP8X" if data.len() == 10 => {
+ if data[0] & 0b0000_0010 != 0 {
+ return Err(RadrootsBlossomError::PublicationRasterFrameCountMismatch {
+ actual: 2,
+ });
+ }
+ let width = 1 + read_u24_le(&data[4..7]);
+ let height = 1 + read_u24_le(&data[7..10]);
+ dimensions = Some(RadrootsBlossomRasterDimensions::new(width, height)?);
+ }
+ b"VP8X" => return invalid_raster(),
+ b"VP8L" => {
+ primary_chunks = primary_chunks.saturating_add(1);
+ let header = data
+ .get(..5)
+ .ok_or(RadrootsBlossomError::InvalidPublicationRaster)?;
+ if header[0] != 0x2f {
+ return invalid_raster();
+ }
+ let bits = u32::from_le_bytes(
+ header[1..5]
+ .try_into()
+ .map_err(|_| RadrootsBlossomError::InvalidPublicationRaster)?,
+ );
+ let parsed = RadrootsBlossomRasterDimensions::new(
+ (bits & 0x3fff) + 1,
+ ((bits >> 14) & 0x3fff) + 1,
+ )?;
+ if dimensions.is_some_and(|value| value != parsed) {
+ return Err(RadrootsBlossomError::PublicationRasterContainerDimensionMismatch);
+ }
+ dimensions = Some(parsed);
+ }
+ b"VP8 " => {
+ primary_chunks = primary_chunks.saturating_add(1);
+ let header = data
+ .get(..10)
+ .ok_or(RadrootsBlossomError::InvalidPublicationRaster)?;
+ if &header[3..6] != b"\x9d\x01\x2a" {
+ return invalid_raster();
+ }
+ let width = u16::from_le_bytes([header[6], header[7]]) & 0x3fff;
+ let height = u16::from_le_bytes([header[8], header[9]]) & 0x3fff;
+ let parsed =
+ RadrootsBlossomRasterDimensions::new(u32::from(width), u32::from(height))?;
+ if dimensions.is_some_and(|value| value != parsed) {
+ return Err(RadrootsBlossomError::PublicationRasterContainerDimensionMismatch);
+ }
+ dimensions = Some(parsed);
+ }
+ _ => {}
+ }
+ }
+ if position != bytes.len() || primary_chunks != 1 {
+ return invalid_raster();
+ }
+ Ok(dimensions)
+}
+
+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> {
+ if bytes.len() < 4 || !bytes.starts_with(b"\xff\xd8") {
+ return invalid_raster();
+ }
+ let mut position = 2_usize;
+ let mut dimensions = None;
+ loop {
+ let marker_start = position;
+ if bytes.get(position) != Some(&0xff) {
+ return invalid_raster();
+ }
+ while bytes.get(position) == Some(&0xff) {
+ position += 1;
+ }
+ let marker = *bytes
+ .get(position)
+ .ok_or(RadrootsBlossomError::InvalidPublicationRaster)?;
+ position += 1;
+ match marker {
+ 0xd9 if position == bytes.len() => {
+ return dimensions.ok_or(RadrootsBlossomError::InvalidPublicationRaster);
+ }
+ 0xd9 | 0x00 | 0xd8 | 0xd0..=0xd7 => return invalid_raster(),
+ 0x01 => continue,
+ _ => {}
+ }
+
+ let length_end = position
+ .checked_add(2)
+ .ok_or(RadrootsBlossomError::InvalidPublicationRaster)?;
+ let length_bytes = bytes
+ .get(position..length_end)
+ .ok_or(RadrootsBlossomError::InvalidPublicationRaster)?;
+ let length = usize::from(u16::from_be_bytes(
+ length_bytes
+ .try_into()
+ .map_err(|_| RadrootsBlossomError::InvalidPublicationRaster)?,
+ ));
+ if length < 2 {
+ return invalid_raster();
+ }
+ let data_start = length_end;
+ let data_end = position
+ .checked_add(length)
+ .ok_or(RadrootsBlossomError::InvalidPublicationRaster)?;
+ let data = bytes
+ .get(data_start..data_end)
+ .ok_or(RadrootsBlossomError::InvalidPublicationRaster)?;
+ position = data_end;
+
+ if is_jpeg_start_of_frame(marker) {
+ if dimensions.is_some() || data.len() < 6 {
+ 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)?);
+ }
+
+ if marker == 0xda {
+ position = jpeg_scan_end(bytes, position)?;
+ if position <= marker_start {
+ return invalid_raster();
+ }
+ }
+ }
+}
+
+fn is_jpeg_start_of_frame(marker: u8) -> bool {
+ matches!(
+ marker,
+ 0xc0..=0xc3 | 0xc5..=0xc7 | 0xc9..=0xcb | 0xcd..=0xcf
+ )
+}
+
+fn jpeg_scan_end(bytes: &[u8], mut position: usize) -> Result<usize, RadrootsBlossomError> {
+ while position < bytes.len() {
+ if bytes[position] != 0xff {
+ position += 1;
+ continue;
+ }
+ let marker_start = position;
+ while bytes.get(position) == Some(&0xff) {
+ position += 1;
+ }
+ let marker = *bytes
+ .get(position)
+ .ok_or(RadrootsBlossomError::InvalidPublicationRaster)?;
+ match marker {
+ 0x00 | 0xd0..=0xd7 => position += 1,
+ _ => return Ok(marker_start),
+ }
+ }
+ invalid_raster()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use alloc::{format, string::ToString};
+
+ 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,
+ 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
+ ];
+
+ const JPEG: &[u8] = &[
+ 0xff, 0xd8, 0xff, 0xc0, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
+ 0xff, 0xda, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, 0x00, 0xff, 0xd9,
+ ];
+
+ const STILL_WEBP: &[u8] = &[
+ b'R', b'I', b'F', b'F', 18, 0, 0, 0, b'W', b'E', b'B', b'P', b'V', b'P', b'8', b'L', 5, 0,
+ 0, 0, 0x2f, 0, 0, 0, 0, 0,
+ ];
+
+ fn descriptor(bytes: &[u8], media_type: &str, origin: &str) -> RadrootsBlossomBlobDescriptor {
+ let hash = RadrootsBlossomSha256::digest(bytes);
+ RadrootsBlossomBlobDescriptor::new(
+ crate::RadrootsBlossomBlobUrl::parse(&format!("{origin}/{hash}.png")).unwrap(),
+ hash,
+ bytes.len() as u64,
+ RadrootsBlossomMediaType::parse(media_type).unwrap(),
+ 1_800_000_000,
+ )
+ .unwrap()
+ }
+
+ fn verified(bytes: &[u8]) -> RadrootsBlossomByteVerifiedDescriptor {
+ let media_type = RadrootsBlossomMediaType::parse("image/png").unwrap();
+ descriptor(bytes, "image/png", "https://cdn.example")
+ .approve_reference()
+ .unwrap()
+ .verify_bytes(bytes, &media_type)
+ .unwrap()
+ }
+
+ fn observations(
+ bytes: &[u8],
+ ) -> (
+ RadrootsBlossomBud02UploadObservation,
+ RadrootsBlossomBud01HeadObservation,
+ RadrootsBlossomBud01GetObservation,
+ RadrootsBlossomRasterDecodeObservation,
+ ) {
+ let expected = verified(bytes);
+ let upload = RadrootsBlossomBud02UploadObservation::new(
+ 201,
+ descriptor(bytes, "image/png", "https://cdn.example"),
+ )
+ .unwrap();
+ let url = expected.url().clone();
+ let media_type = RadrootsBlossomMediaType::parse("image/png").unwrap();
+ let head = RadrootsBlossomBud01HeadObservation::new(
+ 200,
+ url.clone(),
+ bytes.len() as u64,
+ media_type,
+ )
+ .unwrap();
+ let get = RadrootsBlossomBud01GetObservation::from_complete_body(
+ 200,
+ url,
+ bytes.len() as u64,
+ bytes,
+ )
+ .unwrap();
+ let decode = RadrootsBlossomRasterDecodeObservation::new(
+ RadrootsBlossomRasterFormat::Png,
+ RadrootsBlossomSha256::digest(bytes),
+ bytes.len() as u64,
+ 1,
+ 1,
+ 1,
+ )
+ .unwrap();
+ (upload, head, get, decode)
+ }
+
+ #[test]
+ fn publication_readiness_accepts_exact_complete_observations() {
+ let expected = verified(PNG);
+ let (upload, head, get, decode) = observations(PNG);
+ let evidence = verify_publication_readiness(
+ &expected,
+ PNG,
+ RadrootsBlossomAuthoredRasterDimensions::Exact(
+ RadrootsBlossomRasterDimensions::new(1, 1).unwrap(),
+ ),
+ &upload,
+ &head,
+ &get,
+ &decode,
+ )
+ .unwrap();
+ assert_eq!(
+ evidence.url().as_str(),
+ "https://cdn.example/0d1c097e006a87476e84014ba5842f04c725ed2fc5a081743ab2b5bf13a538b9.png"
+ );
+ assert_eq!(evidence.sha256(), RadrootsBlossomSha256::digest(PNG));
+ assert_eq!(evidence.size(), PNG.len() as u64);
+ assert_eq!(evidence.media_type().as_str(), "image/png");
+ assert_eq!(evidence.raster_format(), RadrootsBlossomRasterFormat::Png);
+ assert_eq!(evidence.dimensions().pixels(), 1);
+ assert_eq!(evidence.bud02_status().as_u16(), 201);
+ assert_eq!(evidence.uploaded(), 1_800_000_000);
+ assert_eq!(
+ evidence.evidence_digest().to_string(),
+ "c52edeba688fa36c7963a478a35ff78504d7dd79a637c67f93d5acb635110660"
+ );
+ assert_eq!(
+ evidence.evidence_digest().as_sha256().to_string(),
+ evidence.evidence_digest().to_string()
+ );
+ }
+
+ #[test]
+ fn bounded_get_collector_rejects_status_bounds_and_body_shape() {
+ let url = verified(PNG).url().clone();
+ assert_eq!(
+ RadrootsBlossomBud01GetCollector::new(206, url.clone(), 1)
+ .err()
+ .unwrap()
+ .code(),
+ "invalid_bud01_get_status"
+ );
+ assert_eq!(
+ RadrootsBlossomBud01GetCollector::new(
+ 200,
+ url.clone(),
+ RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_BYTES + 1,
+ )
+ .err()
+ .unwrap()
+ .code(),
+ "publication_raster_byte_limit_exceeded"
+ );
+ assert_eq!(
+ RadrootsBlossomBud01GetCollector::new(200, url.clone(), 1)
+ .unwrap()
+ .finish()
+ .err()
+ .unwrap()
+ .code(),
+ "publication_get_body_missing"
+ );
+ let mut short = RadrootsBlossomBud01GetCollector::new(200, url.clone(), 2).unwrap();
+ short.push_chunk(b"a").unwrap();
+ assert_eq!(
+ short.finish().err().unwrap().code(),
+ "publication_get_body_short"
+ );
+ let mut trailing = RadrootsBlossomBud01GetCollector::new(200, url, 1).unwrap();
+ assert_eq!(
+ trailing.push_chunk(b"ab").unwrap_err().code(),
+ "publication_get_body_trailing"
+ );
+ }
+
+ #[test]
+ fn observation_constructors_reject_invalid_status_frames_and_dimensions() {
+ assert_eq!(
+ RadrootsBlossomBud02UploadObservation::new(
+ 204,
+ descriptor(PNG, "image/png", "https://cdn.example")
+ )
+ .unwrap_err()
+ .code(),
+ "invalid_bud02_upload_status"
+ );
+ let url = verified(PNG).url().clone();
+ assert_eq!(
+ RadrootsBlossomBud01HeadObservation::new(
+ 204,
+ url,
+ PNG.len() as u64,
+ RadrootsBlossomMediaType::parse("image/png").unwrap(),
+ )
+ .unwrap_err()
+ .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"),
+ ] {
+ assert_eq!(
+ RadrootsBlossomRasterDecodeObservation::new(
+ RadrootsBlossomRasterFormat::Png,
+ RadrootsBlossomSha256::digest(PNG),
+ PNG.len() as u64,
+ frames,
+ width,
+ height,
+ )
+ .unwrap_err()
+ .code(),
+ code
+ );
+ }
+ assert_eq!(
+ RadrootsBlossomRasterFormat::StillWebP.to_string(),
+ "still_webp"
+ );
+ assert_eq!(
+ RadrootsBlossomRasterFormat::from_media_type(
+ &RadrootsBlossomMediaType::parse("image/png;charset=utf-8").unwrap(),
+ )
+ .unwrap_err()
+ .code(),
+ "unsupported_publication_raster_media_type"
+ );
+ }
+
+ #[test]
+ fn closed_container_validation_accepts_each_format() {
+ assert_eq!(
+ validate_png_container(PNG).unwrap(),
+ RadrootsBlossomRasterDimensions::new(1, 1).unwrap()
+ );
+ assert_eq!(
+ validate_jpeg_container(JPEG).unwrap(),
+ RadrootsBlossomRasterDimensions::new(1, 1).unwrap()
+ );
+ assert_eq!(
+ validate_webp_container(STILL_WEBP).unwrap(),
+ Some(RadrootsBlossomRasterDimensions::new(1, 1).unwrap())
+ );
+ }
+
+ #[test]
+ fn closed_container_validation_rejects_animation_trailing_and_malformed_bytes() {
+ let mut apng = PNG.to_vec();
+ let iend = apng.len() - 12;
+ apng.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,
+ ],
+ );
+ assert_eq!(
+ validate_png_container(&apng).unwrap_err().code(),
+ "publication_raster_frame_count_mismatch"
+ );
+ assert_eq!(
+ validate_png_container(b"not png").unwrap_err().code(),
+ "invalid_publication_raster"
+ );
+ assert_eq!(
+ validate_webp_container(b"not webp").unwrap_err().code(),
+ "invalid_publication_raster"
+ );
+ assert_eq!(
+ validate_jpeg_container(b"not jpeg").unwrap_err().code(),
+ "invalid_publication_raster"
+ );
+
+ let mut animated_webp = [
+ b'R', b'I', b'F', b'F', 22, 0, 0, 0, b'W', b'E', b'B', b'P', b'V', b'P', b'8', b'X',
+ 10, 0, 0, 0, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ ];
+ assert_eq!(
+ validate_webp_container(&animated_webp).unwrap_err().code(),
+ "publication_raster_frame_count_mismatch"
+ );
+ animated_webp[4] = 21;
+ assert_eq!(
+ validate_webp_container(&animated_webp).unwrap_err().code(),
+ "invalid_publication_raster"
+ );
+
+ let mut trailing_jpeg = JPEG.to_vec();
+ trailing_jpeg.push(0);
+ assert_eq!(
+ validate_jpeg_container(&trailing_jpeg).unwrap_err().code(),
+ "invalid_publication_raster"
+ );
+
+ let mut duplicate_sof_jpeg = JPEG.to_vec();
+ duplicate_sof_jpeg.splice(15..15, JPEG[2..15].iter().copied());
+ assert_eq!(
+ validate_jpeg_container(&duplicate_sof_jpeg)
+ .unwrap_err()
+ .code(),
+ "invalid_publication_raster"
+ );
+ }
+
+ #[test]
+ fn get_debug_redacts_complete_body() {
+ let get = observations(PNG).2;
+ let debug = format!("{get:?}");
+ assert!(debug.contains("body_length"));
+ assert!(!debug.contains("89504e47"));
+ assert_eq!(get.bytes(), PNG);
+ }
+}
diff --git a/crates/blossom/tests/fixtures/publication_readiness.v1.json b/crates/blossom/tests/fixtures/publication_readiness.v1.json
@@ -0,0 +1,384 @@
+{
+ "suite": "blossom_publication_readiness",
+ "contract_version": "1.0.0",
+ "vectors": [
+ {
+ "id": "valid_created",
+ "kind": "blossom.verify_publication_readiness.valid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "none"
+ },
+ "expected": {
+ "url": "https://cdn.example/0d1c097e006a87476e84014ba5842f04c725ed2fc5a081743ab2b5bf13a538b9.png",
+ "sha256": "0d1c097e006a87476e84014ba5842f04c725ed2fc5a081743ab2b5bf13a538b9",
+ "size": 70,
+ "media_type": "image/png",
+ "format": "png",
+ "width": 1,
+ "height": 1,
+ "upload_status": 201,
+ "evidence_digest": "c52edeba688fa36c7963a478a35ff78504d7dd79a637c67f93d5acb635110660"
+ }
+ },
+ {
+ "id": "valid_ok_without_authored_dimensions",
+ "kind": "blossom.verify_publication_readiness.valid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "upload_status_200"
+ },
+ "expected": {
+ "url": "https://cdn.example/0d1c097e006a87476e84014ba5842f04c725ed2fc5a081743ab2b5bf13a538b9.png",
+ "sha256": "0d1c097e006a87476e84014ba5842f04c725ed2fc5a081743ab2b5bf13a538b9",
+ "size": 70,
+ "media_type": "image/png",
+ "format": "png",
+ "width": 1,
+ "height": 1,
+ "upload_status": 200
+ }
+ },
+ {
+ "id": "invalid_upload_status",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "upload_status_202"
+ },
+ "expected": {
+ "error": "invalid_bud02_upload_status"
+ }
+ },
+ {
+ "id": "invalid_head_status",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "head_status_204"
+ },
+ "expected": {
+ "error": "invalid_bud01_head_status"
+ }
+ },
+ {
+ "id": "invalid_get_status",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "get_status_206"
+ },
+ "expected": {
+ "error": "invalid_bud01_get_status"
+ }
+ },
+ {
+ "id": "declared_size_over_public_max",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "get_size_over_max"
+ },
+ "expected": {
+ "error": "publication_raster_byte_limit_exceeded"
+ }
+ },
+ {
+ "id": "missing_get_body",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "get_body_missing"
+ },
+ "expected": {
+ "error": "publication_get_body_missing"
+ }
+ },
+ {
+ "id": "short_get_body",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "get_body_short"
+ },
+ "expected": {
+ "error": "publication_get_body_short"
+ }
+ },
+ {
+ "id": "trailing_get_body",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "get_body_trailing"
+ },
+ "expected": {
+ "error": "publication_get_body_trailing"
+ }
+ },
+ {
+ "id": "authored_bytes_short",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "authored_bytes_short"
+ },
+ "expected": {
+ "error": "publication_authored_bytes_size_mismatch"
+ }
+ },
+ {
+ "id": "authored_bytes_wrong_hash",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "authored_bytes_wrong_hash"
+ },
+ "expected": {
+ "error": "publication_authored_bytes_hash_mismatch"
+ }
+ },
+ {
+ "id": "upload_url_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "upload_url_mismatch"
+ },
+ "expected": {
+ "error": "publication_upload_url_mismatch"
+ }
+ },
+ {
+ "id": "upload_hash_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "upload_hash_mismatch"
+ },
+ "expected": {
+ "error": "publication_upload_hash_mismatch"
+ }
+ },
+ {
+ "id": "upload_size_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "upload_size_mismatch"
+ },
+ "expected": {
+ "error": "publication_upload_size_mismatch"
+ }
+ },
+ {
+ "id": "upload_mime_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "upload_mime_mismatch"
+ },
+ "expected": {
+ "error": "publication_upload_media_type_mismatch"
+ }
+ },
+ {
+ "id": "head_url_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "head_url_mismatch"
+ },
+ "expected": {
+ "error": "publication_head_url_mismatch"
+ }
+ },
+ {
+ "id": "head_size_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "head_size_mismatch"
+ },
+ "expected": {
+ "error": "publication_head_size_mismatch"
+ }
+ },
+ {
+ "id": "head_mime_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "head_mime_mismatch"
+ },
+ "expected": {
+ "error": "publication_head_media_type_mismatch"
+ }
+ },
+ {
+ "id": "get_url_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "get_url_mismatch"
+ },
+ "expected": {
+ "error": "publication_get_url_mismatch"
+ }
+ },
+ {
+ "id": "get_declared_size_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "get_declared_size_mismatch"
+ },
+ "expected": {
+ "error": "publication_get_declared_size_mismatch"
+ }
+ },
+ {
+ "id": "get_complete_hash_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "get_bytes_wrong_hash"
+ },
+ "expected": {
+ "error": "publication_retrieved_bytes_hash_mismatch"
+ }
+ },
+ {
+ "id": "unsupported_raster_mime",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "unsupported_mime"
+ },
+ "expected": {
+ "error": "unsupported_publication_raster_media_type"
+ }
+ },
+ {
+ "id": "malformed_raster",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "malformed_container"
+ },
+ "expected": {
+ "error": "invalid_publication_raster"
+ }
+ },
+ {
+ "id": "animated_png",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "animated_png"
+ },
+ "expected": {
+ "error": "publication_raster_frame_count_mismatch"
+ }
+ },
+ {
+ "id": "decode_format_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "decode_format_mismatch"
+ },
+ "expected": {
+ "error": "publication_raster_decode_format_mismatch"
+ }
+ },
+ {
+ "id": "decode_length_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "decode_length_mismatch"
+ },
+ "expected": {
+ "error": "publication_raster_decode_length_mismatch"
+ }
+ },
+ {
+ "id": "decode_hash_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "decode_hash_mismatch"
+ },
+ "expected": {
+ "error": "publication_raster_decode_hash_mismatch"
+ }
+ },
+ {
+ "id": "decode_container_dimension_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "decode_container_dimension_mismatch"
+ },
+ "expected": {
+ "error": "publication_raster_container_dimension_mismatch"
+ }
+ },
+ {
+ "id": "authored_dimension_mismatch",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "authored_dimension_mismatch"
+ },
+ "expected": {
+ "error": "publication_authored_raster_dimension_mismatch"
+ }
+ },
+ {
+ "id": "decode_zero_frames",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "decode_zero_frames"
+ },
+ "expected": {
+ "error": "publication_raster_frame_count_mismatch"
+ }
+ },
+ {
+ "id": "decode_zero_width",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "decode_zero_width"
+ },
+ "expected": {
+ "error": "publication_raster_dimensions_out_of_range"
+ }
+ },
+ {
+ "id": "decode_dimension_over_max",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "decode_dimension_over_max"
+ },
+ "expected": {
+ "error": "publication_raster_dimensions_out_of_range"
+ }
+ },
+ {
+ "id": "decode_pixel_limit",
+ "kind": "blossom.verify_publication_readiness.invalid",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff000000401010018dd8db10000000049454e44ae426082",
+ "mutation": "decode_pixel_limit"
+ },
+ "expected": {
+ "error": "publication_raster_pixel_limit_exceeded"
+ }
+ }
+ ]
+}
diff --git a/crates/blossom/tests/publication_readiness.rs b/crates/blossom/tests/publication_readiness.rs
@@ -0,0 +1,347 @@
+use radroots_blossom::{
+ RadrootsBlossomApprovedBlobUrl, RadrootsBlossomAuthoredRasterDimensions,
+ RadrootsBlossomBlobDescriptor, RadrootsBlossomBlobUrl, RadrootsBlossomBud01GetObservation,
+ RadrootsBlossomBud01HeadObservation, RadrootsBlossomBud02UploadObservation,
+ RadrootsBlossomError, RadrootsBlossomMediaType, RadrootsBlossomRasterDecodeObservation,
+ RadrootsBlossomRasterDimensions, RadrootsBlossomRasterFormat, RadrootsBlossomSha256,
+ verify_publication_readiness,
+};
+use serde::Deserialize;
+use serde_json::Value;
+
+const PACKAGED_VECTORS: &[u8] = include_bytes!("fixtures/publication_readiness.v1.json");
+
+#[derive(Deserialize)]
+struct VectorFile {
+ vectors: Vec<Vector>,
+}
+
+#[derive(Deserialize)]
+struct Vector {
+ id: String,
+ kind: String,
+ input: Value,
+ expected: Value,
+}
+
+#[test]
+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);
+ for vector in &vector_file.vectors {
+ match vector.kind.as_str() {
+ "blossom.verify_publication_readiness.valid" => execute_valid(vector),
+ "blossom.verify_publication_readiness.invalid" => execute_invalid(vector),
+ kind => panic!("{} has unsupported kind {kind}", vector.id),
+ }
+ }
+}
+
+fn canonical_vectors() -> &'static [u8] {
+ let path = concat!(
+ env!("CARGO_MANIFEST_DIR"),
+ "/../../contracts/conformance/vectors/blossom/publication_readiness.v1.json"
+ );
+ std::fs::read(path)
+ .unwrap_or_else(|error| panic!("read canonical publication-readiness vectors: {error}"))
+ .leak()
+}
+
+fn execute_valid(vector: &Vector) {
+ let mutation = input_str(vector, "mutation");
+ let result = run_mutation(vector, mutation).unwrap_or_else(|error| {
+ panic!(
+ "{} unexpectedly failed: {} ({})",
+ vector.id,
+ error,
+ error.code()
+ )
+ });
+ assert_eq!(
+ result.url().as_str(),
+ expected_str(vector, "url"),
+ "{}",
+ vector.id
+ );
+ assert_eq!(
+ result.sha256().to_string(),
+ expected_str(vector, "sha256"),
+ "{}",
+ vector.id
+ );
+ assert_eq!(result.size(), expected_u64(vector, "size"), "{}", vector.id);
+ assert_eq!(
+ result.media_type().as_str(),
+ expected_str(vector, "media_type"),
+ "{}",
+ vector.id
+ );
+ assert_eq!(
+ result.raster_format().as_str(),
+ expected_str(vector, "format"),
+ "{}",
+ vector.id
+ );
+ assert_eq!(
+ u64::from(result.dimensions().width()),
+ expected_u64(vector, "width"),
+ "{}",
+ vector.id
+ );
+ assert_eq!(
+ u64::from(result.dimensions().height()),
+ expected_u64(vector, "height"),
+ "{}",
+ vector.id
+ );
+ assert_eq!(
+ u64::from(result.bud02_status().as_u16()),
+ expected_u64(vector, "upload_status"),
+ "{}",
+ vector.id
+ );
+ if let Some(expected_digest) = vector
+ .expected
+ .get("evidence_digest")
+ .and_then(Value::as_str)
+ {
+ assert_eq!(
+ result.evidence_digest().to_string(),
+ expected_digest,
+ "{}",
+ vector.id
+ );
+ }
+}
+
+fn execute_invalid(vector: &Vector) {
+ let mutation = input_str(vector, "mutation");
+ let error =
+ run_mutation(vector, mutation).expect_err("invalid publication-readiness vector must fail");
+ assert_eq!(error.code(), expected_str(vector, "error"), "{}", vector.id);
+}
+
+fn run_mutation(
+ vector: &Vector,
+ mutation: &str,
+) -> Result<radroots_blossom::RadrootsBlossomPublicationReadinessEvidence, RadrootsBlossomError> {
+ let canonical = hex::decode(input_str(vector, "bytes_hex")).unwrap();
+ let mut sealed_bytes = canonical.clone();
+ let mut exact_authored_bytes = canonical.clone();
+ let mut retrieved_bytes = canonical.clone();
+ let mut media_type = "image/png";
+ let mut upload_status = 201;
+ let mut head_status = 200;
+ let mut get_status = 200;
+ let mut upload_origin = "https://cdn.example";
+ let mut head_origin = "https://cdn.example";
+ let mut get_origin = "https://cdn.example";
+ let mut upload_hash_bytes = canonical.clone();
+ let mut upload_size_delta = 0_i64;
+ let mut upload_media_type = "image/png";
+ 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 {
+ "none" => {}
+ "upload_status_200" => {
+ upload_status = 200;
+ authored_dimensions = None;
+ }
+ "upload_status_202" => upload_status = 202,
+ "head_status_204" => head_status = 204,
+ "get_status_206" => get_status = 206,
+ "get_size_over_max" => get_declared_size_delta = 10_485_760,
+ "get_body_missing" => retrieved_bytes.clear(),
+ "get_body_short" => {
+ retrieved_bytes.pop();
+ }
+ "get_body_trailing" => retrieved_bytes.push(0),
+ "authored_bytes_short" => {
+ exact_authored_bytes.pop();
+ }
+ "authored_bytes_wrong_hash" => exact_authored_bytes[69] ^= 1,
+ "upload_url_mismatch" => upload_origin = "https://other.example",
+ "upload_hash_mismatch" => upload_hash_bytes[69] ^= 1,
+ "upload_size_mismatch" => upload_size_delta = 1,
+ "upload_mime_mismatch" => upload_media_type = "image/jpeg",
+ "head_url_mismatch" => head_origin = "https://other.example",
+ "head_size_mismatch" => head_size_delta = 1,
+ "head_mime_mismatch" => head_media_type = "image/jpeg",
+ "get_url_mismatch" => get_origin = "https://other.example",
+ "get_declared_size_mismatch" => {
+ retrieved_bytes.pop();
+ get_declared_size_delta = -1;
+ }
+ "get_bytes_wrong_hash" => retrieved_bytes[69] ^= 1,
+ "unsupported_mime" => {
+ media_type = "image/gif";
+ upload_media_type = "image/gif";
+ head_media_type = "image/gif";
+ }
+ "malformed_container" => {
+ sealed_bytes[0] = 0;
+ 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,
+ ],
+ );
+ 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;
+ }
+ "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;
+ }
+ other => panic!("{} has unknown mutation {other}", vector.id),
+ }
+
+ if mutation == "get_size_over_max" {
+ let url = approved_url(get_origin, &sealed_bytes);
+ return RadrootsBlossomBud01GetObservation::from_complete_body(
+ get_status,
+ url,
+ adjusted_size(sealed_bytes.len(), get_declared_size_delta),
+ &retrieved_bytes,
+ )
+ .and_then(|_| unreachable_result());
+ }
+
+ let expected_media_type = RadrootsBlossomMediaType::parse(media_type).unwrap();
+ let authored_descriptor = descriptor(
+ "https://cdn.example",
+ &sealed_bytes,
+ sealed_bytes.len() as u64,
+ media_type,
+ )
+ .approve_reference()?
+ .verify_bytes(&sealed_bytes, &expected_media_type)?;
+
+ let upload = RadrootsBlossomBud02UploadObservation::new(
+ upload_status,
+ descriptor(
+ upload_origin,
+ &upload_hash_bytes,
+ adjusted_size(sealed_bytes.len(), upload_size_delta),
+ upload_media_type,
+ ),
+ )?;
+ let head = RadrootsBlossomBud01HeadObservation::new(
+ head_status,
+ approved_url(head_origin, &sealed_bytes),
+ adjusted_size(sealed_bytes.len(), head_size_delta),
+ RadrootsBlossomMediaType::parse(head_media_type).unwrap(),
+ )?;
+ let get = RadrootsBlossomBud01GetObservation::from_complete_body(
+ get_status,
+ approved_url(get_origin, &sealed_bytes),
+ 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)?,
+ ),
+ None => RadrootsBlossomAuthoredRasterDimensions::Unspecified,
+ };
+ verify_publication_readiness(
+ &authored_descriptor,
+ &exact_authored_bytes,
+ authored_dimensions,
+ &upload,
+ &head,
+ &get,
+ &decode,
+ )
+}
+
+fn unreachable_result()
+-> Result<radroots_blossom::RadrootsBlossomPublicationReadinessEvidence, RadrootsBlossomError> {
+ unreachable!("oversized GET construction must fail")
+}
+
+fn descriptor(
+ origin: &str,
+ hash_bytes: &[u8],
+ size: u64,
+ media_type: &str,
+) -> RadrootsBlossomBlobDescriptor {
+ let hash = RadrootsBlossomSha256::digest(hash_bytes);
+ RadrootsBlossomBlobDescriptor::new(
+ RadrootsBlossomBlobUrl::parse(&format!("{origin}/{hash}.png")).unwrap(),
+ hash,
+ size,
+ RadrootsBlossomMediaType::parse(media_type).unwrap(),
+ 1_800_000_000,
+ )
+ .unwrap()
+}
+
+fn approved_url(origin: &str, hash_bytes: &[u8]) -> RadrootsBlossomApprovedBlobUrl {
+ let hash = RadrootsBlossomSha256::digest(hash_bytes);
+ RadrootsBlossomBlobUrl::parse(&format!("{origin}/{hash}.png"))
+ .unwrap()
+ .approve()
+ .unwrap()
+}
+
+fn adjusted_size(length: usize, delta: i64) -> u64 {
+ u64::try_from(i64::try_from(length).unwrap() + delta).unwrap()
+}
+
+fn input_str<'a>(vector: &'a Vector, field: &str) -> &'a str {
+ vector.input[field]
+ .as_str()
+ .unwrap_or_else(|| panic!("{} input.{field} must be a string", vector.id))
+}
+
+fn expected_str<'a>(vector: &'a Vector, field: &str) -> &'a str {
+ vector.expected[field]
+ .as_str()
+ .unwrap_or_else(|| panic!("{} expected.{field} must be a string", vector.id))
+}
+
+fn expected_u64(vector: &Vector, field: &str) -> u64 {
+ vector.expected[field]
+ .as_u64()
+ .unwrap_or_else(|| panic!("{} expected.{field} must be an unsigned integer", vector.id))
+}
diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs
@@ -2,6 +2,7 @@
mod admission_authority;
mod artifact_bundle;
+mod blossom_publication_readiness;
mod comment_authority;
mod deletion_authority;
mod food_availability_projection;
@@ -53,7 +54,7 @@ pub(crate) fn validate_artifact_contracts(workspace_root: &Path) -> Result<(), S
validate_nip09_reconciliation_manifest(workspace_root)?;
validate_food_availability_projection_manifest(workspace_root)?;
validate_source_maintenance_manifest(workspace_root)?;
- validate_raw_source_rebuild_manifest(workspace_root)?;
+ blossom_publication_readiness::validate_blossom_publication_readiness(workspace_root)?;
validate_knowledge_contract_manifest(workspace_root)
}
@@ -94,7 +95,7 @@ const REPLICA_CONTRACT_NAME: &str = "radroots_replica_contract";
const REPLICA_TRANSFER_CONSTANT: &str = "RADROOTS_REPLICA_TRANSFER_VERSION";
const REPLICA_TRANSFER_VERSION: u32 = 2;
const VENDORED_WORKSPACE_MEMBER_RELATIVE: &str = "crates/libsqlite3_sys_3_53_3";
-const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 23] = [
+const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 24] = [
(
"contracts/conformance/vectors/blossom/bud11_claims.v1.json",
"crates/blossom/tests/fixtures/bud11_claims.v1.json",
@@ -104,6 +105,10 @@ const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 23] = [
"crates/blossom/tests/fixtures/hash_path_and_descriptor.v1.json",
),
(
+ "contracts/conformance/vectors/blossom/publication_readiness.v1.json",
+ "crates/blossom/tests/fixtures/publication_readiness.v1.json",
+ ),
+ (
"contracts/conformance/vectors/blossom/bud11_nostr_adapter.v1.json",
"crates/nostr/tests/fixtures/bud11_nostr_adapter.v1.json",
),
diff --git a/tools/xtask/src/contract/blossom_publication_readiness.rs b/tools/xtask/src/contract/blossom_publication_readiness.rs
@@ -0,0 +1,776 @@
+use super::artifact_bundle::{read_regular_file, with_artifact_bundle_transaction};
+use super::raw_source_rebuild::validate_raw_source_rebuild_predecessor_production_sources_under_lock;
+use serde_json::Value;
+use sha2::{Digest, Sha256};
+use std::collections::BTreeSet;
+use std::fs;
+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 READINESS_SOURCE_RELATIVE: &str = "crates/blossom/src/publication_readiness.rs";
+const BLOSSOM_LIB_RELATIVE: &str = "crates/blossom/src/lib.rs";
+const BLOSSOM_MANIFEST_RELATIVE: &str = "crates/blossom/Cargo.toml";
+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";
+const RAW_PREDECESSOR_GOVERNANCE_RELATIVE: &str = "tools/xtask/src/contract/raw_source_rebuild.rs";
+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] = &[
+ CHANGELOG_RELATIVE,
+ RELEASE_RELATIVE,
+ "tools/xtask/src/contract.rs",
+ "tools/xtask/src/contract/food_availability_projection.rs",
+ "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 SOURCE_INVENTORY: &[&str] = &[
+ CHANGELOG_RELATIVE,
+ BLOSSOM_MANIFEST_RELATIVE,
+ "crates/blossom/README",
+ "crates/blossom/src/error.rs",
+ BLOSSOM_LIB_RELATIVE,
+ READINESS_SOURCE_RELATIVE,
+ "crates/blossom/tests/publication_readiness.rs",
+ VECTOR_MIRROR_RELATIVE,
+ "contracts/events/blossom-media.md",
+ VECTOR_CANONICAL_RELATIVE,
+ OPERATIONS_RELATIVE,
+ RELEASE_RELATIVE,
+ "tools/xtask/src/contract.rs",
+ "tools/xtask/src/contract/blossom_publication_readiness.rs",
+ "tools/xtask/src/contract/food_availability_projection.rs",
+ "tools/xtask/src/contract/nip09_reconciliation.rs",
+ RAW_PREDECESSOR_GOVERNANCE_RELATIVE,
+];
+
+const IMMUTABLE_RAW_PREDECESSOR_ARTIFACTS: &[(&str, usize, &str)] = &[
+ (
+ "crates/event_store/contracts/raw_source_rebuild_v1.manifest.json",
+ 45_449,
+ "b8737a9c5836517114e7df6c2194c46e3c200093e12c4e6297165d2b9dae56a1",
+ ),
+ (
+ "crates/event_store/contracts/raw_source_rebuild_v1.manifest.schema.json",
+ 17_896,
+ "f9d210967e54b66f39c8bb965d97b2001a0ebc0927e7c2c14edb8e474bfda695",
+ ),
+ (
+ "crates/event_store/contracts/raw_source_rebuild_v1.manifest.sha256",
+ 65,
+ "737ee2e4ecd400e1c647e80422c432cd2955d7c7cc04fdf3f9993551480e7957",
+ ),
+ (
+ "crates/event_store/src/generated/raw_source_rebuild_manifest.rs",
+ 50_735,
+ "20ad0d83304bb4ea3aeb0b37fc068891f1f2e5a0c3abc93d1a9932770330307c",
+ ),
+ (
+ "contracts/conformance/vectors/event_store/raw_source_rebuild.v1.json",
+ 26_833,
+ "c37a2bf3714f53ab04fae8c5c9dbe2ad4b3f5310efa51f46bd8b116660f1fe15",
+ ),
+ (
+ "crates/event_store/tests/fixtures/raw_source_rebuild.v1.json",
+ 26_833,
+ "c37a2bf3714f53ab04fae8c5c9dbe2ad4b3f5310efa51f46bd8b116660f1fe15",
+ ),
+];
+
+const CURRENT_BYTE_BOUND_BLOSSOM_SOURCES: &[(&str, usize, &str)] = &[
+ (
+ BLOSSOM_LIB_RELATIVE,
+ 2_088,
+ "ab0431ba43619431f4384a474c1e8b7e3e646a802443d66cf992df467fa9c36b",
+ ),
+ (
+ "crates/blossom/src/error.rs",
+ 30_667,
+ "2d30f4e21d71b6978cb3cc564d5ab542d238cf56c2eab89801fce8d1421bccf6",
+ ),
+];
+
+const REQUIRED_PUBLIC_TYPES: &[&str] = &[
+ "RadrootsBlossomBud02UploadStatus",
+ "RadrootsBlossomBud02UploadObservation",
+ "RadrootsBlossomBud01HeadObservation",
+ "RadrootsBlossomBud01GetCollector",
+ "RadrootsBlossomBud01GetObservation",
+ "RadrootsBlossomRasterFormat",
+ "RadrootsBlossomRasterDimensions",
+ "RadrootsBlossomAuthoredRasterDimensions",
+ "RadrootsBlossomRasterDecodeObservation",
+ "RadrootsBlossomPublicationReadinessEvidenceDigest",
+ "RadrootsBlossomPublicationReadinessEvidence",
+];
+
+const VECTOR_EXPECTATIONS: &[(&str, &str, &str, Option<&str>)] = &[
+ (
+ "valid_created",
+ "blossom.verify_publication_readiness.valid",
+ "none",
+ None,
+ ),
+ (
+ "valid_ok_without_authored_dimensions",
+ "blossom.verify_publication_readiness.valid",
+ "upload_status_200",
+ None,
+ ),
+ (
+ "invalid_upload_status",
+ "blossom.verify_publication_readiness.invalid",
+ "upload_status_202",
+ Some("invalid_bud02_upload_status"),
+ ),
+ (
+ "invalid_head_status",
+ "blossom.verify_publication_readiness.invalid",
+ "head_status_204",
+ Some("invalid_bud01_head_status"),
+ ),
+ (
+ "invalid_get_status",
+ "blossom.verify_publication_readiness.invalid",
+ "get_status_206",
+ Some("invalid_bud01_get_status"),
+ ),
+ (
+ "declared_size_over_public_max",
+ "blossom.verify_publication_readiness.invalid",
+ "get_size_over_max",
+ Some("publication_raster_byte_limit_exceeded"),
+ ),
+ (
+ "missing_get_body",
+ "blossom.verify_publication_readiness.invalid",
+ "get_body_missing",
+ Some("publication_get_body_missing"),
+ ),
+ (
+ "short_get_body",
+ "blossom.verify_publication_readiness.invalid",
+ "get_body_short",
+ Some("publication_get_body_short"),
+ ),
+ (
+ "trailing_get_body",
+ "blossom.verify_publication_readiness.invalid",
+ "get_body_trailing",
+ Some("publication_get_body_trailing"),
+ ),
+ (
+ "authored_bytes_short",
+ "blossom.verify_publication_readiness.invalid",
+ "authored_bytes_short",
+ Some("publication_authored_bytes_size_mismatch"),
+ ),
+ (
+ "authored_bytes_wrong_hash",
+ "blossom.verify_publication_readiness.invalid",
+ "authored_bytes_wrong_hash",
+ Some("publication_authored_bytes_hash_mismatch"),
+ ),
+ (
+ "upload_url_mismatch",
+ "blossom.verify_publication_readiness.invalid",
+ "upload_url_mismatch",
+ Some("publication_upload_url_mismatch"),
+ ),
+ (
+ "upload_hash_mismatch",
+ "blossom.verify_publication_readiness.invalid",
+ "upload_hash_mismatch",
+ Some("publication_upload_hash_mismatch"),
+ ),
+ (
+ "upload_size_mismatch",
+ "blossom.verify_publication_readiness.invalid",
+ "upload_size_mismatch",
+ Some("publication_upload_size_mismatch"),
+ ),
+ (
+ "upload_mime_mismatch",
+ "blossom.verify_publication_readiness.invalid",
+ "upload_mime_mismatch",
+ Some("publication_upload_media_type_mismatch"),
+ ),
+ (
+ "head_url_mismatch",
+ "blossom.verify_publication_readiness.invalid",
+ "head_url_mismatch",
+ Some("publication_head_url_mismatch"),
+ ),
+ (
+ "head_size_mismatch",
+ "blossom.verify_publication_readiness.invalid",
+ "head_size_mismatch",
+ Some("publication_head_size_mismatch"),
+ ),
+ (
+ "head_mime_mismatch",
+ "blossom.verify_publication_readiness.invalid",
+ "head_mime_mismatch",
+ Some("publication_head_media_type_mismatch"),
+ ),
+ (
+ "get_url_mismatch",
+ "blossom.verify_publication_readiness.invalid",
+ "get_url_mismatch",
+ Some("publication_get_url_mismatch"),
+ ),
+ (
+ "get_declared_size_mismatch",
+ "blossom.verify_publication_readiness.invalid",
+ "get_declared_size_mismatch",
+ Some("publication_get_declared_size_mismatch"),
+ ),
+ (
+ "get_complete_hash_mismatch",
+ "blossom.verify_publication_readiness.invalid",
+ "get_bytes_wrong_hash",
+ Some("publication_retrieved_bytes_hash_mismatch"),
+ ),
+ (
+ "unsupported_raster_mime",
+ "blossom.verify_publication_readiness.invalid",
+ "unsupported_mime",
+ Some("unsupported_publication_raster_media_type"),
+ ),
+ (
+ "malformed_raster",
+ "blossom.verify_publication_readiness.invalid",
+ "malformed_container",
+ Some("invalid_publication_raster"),
+ ),
+ (
+ "animated_png",
+ "blossom.verify_publication_readiness.invalid",
+ "animated_png",
+ Some("publication_raster_frame_count_mismatch"),
+ ),
+ (
+ "decode_format_mismatch",
+ "blossom.verify_publication_readiness.invalid",
+ "decode_format_mismatch",
+ Some("publication_raster_decode_format_mismatch"),
+ ),
+ (
+ "decode_length_mismatch",
+ "blossom.verify_publication_readiness.invalid",
+ "decode_length_mismatch",
+ Some("publication_raster_decode_length_mismatch"),
+ ),
+ (
+ "decode_hash_mismatch",
+ "blossom.verify_publication_readiness.invalid",
+ "decode_hash_mismatch",
+ Some("publication_raster_decode_hash_mismatch"),
+ ),
+ (
+ "decode_container_dimension_mismatch",
+ "blossom.verify_publication_readiness.invalid",
+ "decode_container_dimension_mismatch",
+ Some("publication_raster_container_dimension_mismatch"),
+ ),
+ (
+ "authored_dimension_mismatch",
+ "blossom.verify_publication_readiness.invalid",
+ "authored_dimension_mismatch",
+ Some("publication_authored_raster_dimension_mismatch"),
+ ),
+ (
+ "decode_zero_frames",
+ "blossom.verify_publication_readiness.invalid",
+ "decode_zero_frames",
+ Some("publication_raster_frame_count_mismatch"),
+ ),
+ (
+ "decode_zero_width",
+ "blossom.verify_publication_readiness.invalid",
+ "decode_zero_width",
+ Some("publication_raster_dimensions_out_of_range"),
+ ),
+ (
+ "decode_dimension_over_max",
+ "blossom.verify_publication_readiness.invalid",
+ "decode_dimension_over_max",
+ Some("publication_raster_dimensions_out_of_range"),
+ ),
+ (
+ "decode_pixel_limit",
+ "blossom.verify_publication_readiness.invalid",
+ "decode_pixel_limit",
+ Some("publication_raster_pixel_limit_exceeded"),
+ ),
+];
+
+pub(super) fn validate_blossom_publication_readiness(workspace_root: &Path) -> Result<(), String> {
+ with_artifact_bundle_transaction(workspace_root, |_| {
+ validate_blossom_publication_readiness_under_lock(workspace_root)
+ })
+}
+
+fn validate_blossom_publication_readiness_under_lock(workspace_root: &Path) -> Result<(), String> {
+ validate_immutable_predecessor(workspace_root)?;
+ validate_raw_source_rebuild_predecessor_production_sources_under_lock(
+ workspace_root,
+ RAW_PREDECESSOR_SUPERSEDED_PATHS,
+ TRANSITIVE_PREDECESSOR_SUPERSEDED_PATHS,
+ )?;
+ validate_source_inventory(workspace_root)?;
+ validate_source_boundary(workspace_root)?;
+ validate_vector(workspace_root)?;
+ validate_operation(workspace_root)?;
+ validate_release(workspace_root)
+}
+
+fn validate_immutable_predecessor(workspace_root: &Path) -> Result<(), String> {
+ for (relative, expected_length, expected_sha256) in IMMUTABLE_RAW_PREDECESSOR_ARTIFACTS {
+ let bytes = read_regular_file(workspace_root, relative)?;
+ if bytes.len() != *expected_length || sha256_hex(&bytes) != *expected_sha256 {
+ return Err(format!(
+ "immutable raw-source rebuild predecessor artifact `{relative}` drifted"
+ ));
+ }
+ }
+ Ok(())
+}
+
+fn validate_source_inventory(workspace_root: &Path) -> Result<(), String> {
+ let unique = SOURCE_INVENTORY.iter().copied().collect::<BTreeSet<_>>();
+ if unique.len() != SOURCE_INVENTORY.len() {
+ return Err("publication-readiness source inventory contains duplicates".to_owned());
+ }
+ for relative in SOURCE_INVENTORY {
+ let metadata = fs::symlink_metadata(workspace_root.join(relative)).map_err(|error| {
+ format!("inspect publication-readiness source `{relative}`: {error}")
+ })?;
+ if !metadata.file_type().is_file() {
+ return Err(format!(
+ "publication-readiness source `{relative}` must be a regular file"
+ ));
+ }
+ }
+ for superseded in RAW_PREDECESSOR_SUPERSEDED_PATHS
+ .iter()
+ .chain(TRANSITIVE_PREDECESSOR_SUPERSEDED_PATHS)
+ {
+ if !unique.contains(*superseded) {
+ return Err(format!(
+ "superseded predecessor source `{superseded}` is not current-byte governed"
+ ));
+ }
+ }
+ for (relative, expected_length, expected_sha256) in CURRENT_BYTE_BOUND_BLOSSOM_SOURCES {
+ validate_current_byte_bound_blossom_source(
+ relative,
+ &read_regular_file(workspace_root, relative)?,
+ *expected_length,
+ expected_sha256,
+ )?;
+ }
+ Ok(())
+}
+
+fn validate_current_byte_bound_blossom_source(
+ relative: &str,
+ bytes: &[u8],
+ expected_length: usize,
+ expected_sha256: &str,
+) -> Result<(), String> {
+ if bytes.len() != expected_length || sha256_hex(bytes) != expected_sha256 {
+ return Err(format!(
+ "publication-readiness current-byte source `{relative}` drifted"
+ ));
+ }
+ Ok(())
+}
+
+fn validate_source_boundary(workspace_root: &Path) -> Result<(), String> {
+ let source = String::from_utf8(read_regular_file(
+ workspace_root,
+ READINESS_SOURCE_RELATIVE,
+ )?)
+ .map_err(|error| format!("{READINESS_SOURCE_RELATIVE} must be UTF-8: {error}"))?;
+ validate_readiness_source_text(&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("RadrootsBlossomPublicationReadinessEvidence")
+ {
+ return Err(
+ "Blossom crate root must route the readiness module and public API exactly".to_owned(),
+ );
+ }
+
+ let manifest = parse_toml(workspace_root, BLOSSOM_MANIFEST_RELATIVE)?;
+ let dependencies = manifest
+ .get("dependencies")
+ .and_then(toml::Value::as_table)
+ .ok_or_else(|| format!("{BLOSSOM_MANIFEST_RELATIVE} must declare dependencies"))?;
+ let actual = dependencies
+ .keys()
+ .map(String::as_str)
+ .collect::<BTreeSet<_>>();
+ let expected = [
+ "mediatype",
+ "serde",
+ "sha2",
+ "unicode-general-category",
+ "url_nostd",
+ ]
+ .into_iter()
+ .collect::<BTreeSet<_>>();
+ if actual != expected {
+ return Err(format!(
+ "{BLOSSOM_MANIFEST_RELATIVE} dependency boundary drifted: expected {expected:?}, found {actual:?}"
+ ));
+ }
+ let raw_predecessor = String::from_utf8(read_regular_file(
+ workspace_root,
+ RAW_PREDECESSOR_GOVERNANCE_RELATIVE,
+ )?)
+ .map_err(|error| format!("{RAW_PREDECESSOR_GOVERNANCE_RELATIVE} must be UTF-8: {error}"))?;
+ validate_raw_predecessor_successor_routing(&raw_predecessor)?;
+ Ok(())
+}
+
+fn validate_raw_predecessor_successor_routing(source: &str) -> Result<(), String> {
+ let compact = source.split_whitespace().collect::<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\"];",
+ ] {
+ if !compact.contains(required) {
+ return Err(format!(
+ "{RAW_PREDECESSOR_GOVERNANCE_RELATIVE} lacks validation-only successor route `{required}`"
+ ));
+ }
+ }
+ Ok(())
+}
+
+fn validate_readiness_source_text(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_DIMENSION: u32 = 16_384",
+ "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_PIXELS: u64 = 20_000_000",
+ "pub fn verify_publication_readiness(",
+ "b\"radroots.blossom.publication-readiness-evidence.v1\\0\"",
+ ] {
+ if !source.contains(required) {
+ return Err(format!(
+ "{READINESS_SOURCE_RELATIVE} is missing governed fragment `{required}`"
+ ));
+ }
+ }
+ let lowercase = source.to_ascii_lowercase();
+ for forbidden in [
+ "reqwest",
+ "hyper::",
+ "tokio::",
+ "axum::",
+ "std::net",
+ "std::fs",
+ "authorization: nostr",
+ "bearer ",
+ "cookie",
+ ] {
+ if lowercase.contains(forbidden) {
+ return Err(format!(
+ "{READINESS_SOURCE_RELATIVE} crosses the transport-neutral boundary with `{forbidden}`"
+ ));
+ }
+ }
+ if source.contains("serde::Deserialize") || source.contains("derive(Deserialize") {
+ return Err(
+ "publication readiness typestates must not gain forgeable Deserialize implementations"
+ .to_owned(),
+ );
+ }
+ Ok(())
+}
+
+fn validate_vector(workspace_root: &Path) -> Result<(), String> {
+ let canonical = read_regular_file(workspace_root, VECTOR_CANONICAL_RELATIVE)?;
+ let mirror = read_regular_file(workspace_root, VECTOR_MIRROR_RELATIVE)?;
+ if canonical != mirror {
+ return Err(format!(
+ "{VECTOR_MIRROR_RELATIVE} must byte-match {VECTOR_CANONICAL_RELATIVE}"
+ ));
+ }
+ let vector: Value = serde_json::from_slice(&canonical)
+ .map_err(|error| format!("parse {VECTOR_CANONICAL_RELATIVE}: {error}"))?;
+ validate_vector_value(&vector)
+}
+
+fn validate_vector_value(vector: &Value) -> Result<(), String> {
+ if vector.get("suite").and_then(Value::as_str) != Some("blossom_publication_readiness")
+ || vector.get("contract_version").and_then(Value::as_str) != Some("1.0.0")
+ {
+ return Err("publication-readiness vector identity drifted".to_owned());
+ }
+ let cases = vector
+ .get("vectors")
+ .and_then(Value::as_array)
+ .ok_or_else(|| "publication-readiness vectors must be an array".to_owned())?;
+ if cases.len() != VECTOR_EXPECTATIONS.len() {
+ return Err(format!(
+ "publication-readiness vector count drifted: expected {}, found {}",
+ VECTOR_EXPECTATIONS.len(),
+ cases.len()
+ ));
+ }
+ for (case, (id, kind, mutation, expected_error)) in cases.iter().zip(VECTOR_EXPECTATIONS) {
+ let actual_id = case.get("id").and_then(Value::as_str);
+ let actual_kind = case.get("kind").and_then(Value::as_str);
+ let actual_mutation = case
+ .get("input")
+ .and_then(|input| input.get("mutation"))
+ .and_then(Value::as_str);
+ let actual_error = case
+ .get("expected")
+ .and_then(|expected| expected.get("error"))
+ .and_then(Value::as_str);
+ if actual_id != Some(*id)
+ || actual_kind != Some(*kind)
+ || actual_mutation != Some(*mutation)
+ || actual_error != *expected_error
+ {
+ return Err(format!(
+ "publication-readiness vector `{id}` identity or expected error drifted"
+ ));
+ }
+ let bytes_hex = case
+ .get("input")
+ .and_then(|input| input.get("bytes_hex"))
+ .and_then(Value::as_str)
+ .ok_or_else(|| format!("publication-readiness vector `{id}` lacks bytes_hex"))?;
+ if bytes_hex.len() != 140
+ || !bytes_hex
+ .bytes()
+ .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
+ {
+ return Err(format!(
+ "publication-readiness vector `{id}` must bind the exact 70-byte lowercase-hex raster"
+ ));
+ }
+ }
+ Ok(())
+}
+
+fn validate_operation(workspace_root: &Path) -> Result<(), String> {
+ let manifest = parse_toml(workspace_root, OPERATIONS_RELATIVE)?;
+ let operation = manifest
+ .get("operations")
+ .and_then(|value| value.get("blossom_verify_publication_readiness"))
+ .ok_or_else(|| "operations contract lacks Blossom publication readiness".to_owned())?;
+ if operation.get("domain").and_then(toml::Value::as_str) != Some("blossom")
+ || operation.get("id").and_then(toml::Value::as_str)
+ != Some("blossom.verify_publication_readiness")
+ || operation.get("transport").and_then(toml::Value::as_str) != Some("none")
+ || operation.get("signing").and_then(toml::Value::as_str) != Some("none")
+ || operation
+ .get("deterministic")
+ .and_then(toml::Value::as_bool)
+ != Some(true)
+ {
+ return Err("Blossom publication-readiness operation authority drifted".to_owned());
+ }
+ let shared = manifest
+ .get("shared_types")
+ .and_then(|value| value.get("public"))
+ .and_then(toml::Value::as_array)
+ .ok_or_else(|| "operations contract shared public types are missing".to_owned())?
+ .iter()
+ .filter_map(toml::Value::as_str)
+ .collect::<BTreeSet<_>>();
+ if let Some(missing) = REQUIRED_PUBLIC_TYPES
+ .iter()
+ .find(|required| !shared.contains(**required))
+ {
+ return Err(format!(
+ "operations contract lacks readiness public type `{missing}`"
+ ));
+ }
+ Ok(())
+}
+
+fn validate_release(workspace_root: &Path) -> Result<(), String> {
+ let release = parse_toml(workspace_root, RELEASE_RELATIVE)?;
+ let changes = release
+ .get("changes")
+ .and_then(toml::Value::as_array)
+ .ok_or_else(|| format!("{RELEASE_RELATIVE} must declare changes"))?;
+ let matches = changes
+ .iter()
+ .filter(|change| change.get("id").and_then(toml::Value::as_str) == Some(RELEASE_CHANGE_ID))
+ .collect::<Vec<_>>();
+ if matches.len() != 1
+ || matches[0]
+ .get("classification")
+ .and_then(toml::Value::as_str)
+ != Some("feature")
+ {
+ return Err(format!(
+ "{RELEASE_RELATIVE} must contain one feature change `{RELEASE_CHANGE_ID}`"
+ ));
+ }
+ let changelog = String::from_utf8(read_regular_file(workspace_root, CHANGELOG_RELATIVE)?)
+ .map_err(|error| format!("{CHANGELOG_RELATIVE} must be UTF-8: {error}"))?;
+ if changelog.matches(CHANGELOG_MARKER).count() != 1 {
+ return Err(format!(
+ "{CHANGELOG_RELATIVE} must contain exactly one readiness release marker"
+ ));
+ }
+ Ok(())
+}
+
+fn parse_toml(workspace_root: &Path, relative: &str) -> Result<toml::Value, String> {
+ let bytes = read_regular_file(workspace_root, relative)?;
+ let source = std::str::from_utf8(&bytes)
+ .map_err(|error| format!("{relative} must be UTF-8 TOML: {error}"))?;
+ toml::from_str(source).map_err(|error| format!("parse {relative}: {error}"))
+}
+
+fn sha256_hex(bytes: &[u8]) -> String {
+ let digest = Sha256::digest(bytes);
+ let mut output = String::with_capacity(64);
+ for byte in digest {
+ use std::fmt::Write;
+ write!(&mut output, "{byte:02x}").expect("String writes cannot fail");
+ }
+ output
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn workspace_root() -> std::path::PathBuf {
+ Path::new(env!("CARGO_MANIFEST_DIR"))
+ .parent()
+ .and_then(Path::parent)
+ .expect("xtask workspace root")
+ .to_path_buf()
+ }
+
+ #[test]
+ fn publication_readiness_vector_inventory_is_exact_and_mutation_sensitive() {
+ let bytes = read_regular_file(&workspace_root(), VECTOR_CANONICAL_RELATIVE).unwrap();
+ let mut vector: Value = serde_json::from_slice(&bytes).unwrap();
+ validate_vector_value(&vector).unwrap();
+ vector["vectors"][0]["input"]["mutation"] = Value::String("renamed".to_owned());
+ assert!(
+ validate_vector_value(&vector)
+ .unwrap_err()
+ .contains("valid_created")
+ );
+ }
+
+ #[test]
+ fn publication_readiness_source_boundary_rejects_transport_and_contract_drift() {
+ let source = String::from_utf8(
+ read_regular_file(&workspace_root(), READINESS_SOURCE_RELATIVE).unwrap(),
+ )
+ .unwrap();
+ validate_readiness_source_text(&source).unwrap();
+ let injected = format!("{source}\nfn injected() {{ let _ = reqwest::get; }}\n");
+ assert!(
+ validate_readiness_source_text(&injected)
+ .unwrap_err()
+ .contains("transport-neutral")
+ );
+ let removed = source.replace(
+ "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_PIXELS: u64 = 20_000_000",
+ "RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_PIXELS: u64 = 20_000_001",
+ );
+ assert!(
+ validate_readiness_source_text(&removed)
+ .unwrap_err()
+ .contains("missing governed fragment")
+ );
+
+ for (relative, expected_length, expected_sha256) in CURRENT_BYTE_BOUND_BLOSSOM_SOURCES {
+ let mut bytes = read_regular_file(&workspace_root(), relative).unwrap();
+ validate_current_byte_bound_blossom_source(
+ relative,
+ &bytes,
+ *expected_length,
+ expected_sha256,
+ )
+ .unwrap();
+ bytes.push(b' ');
+ assert!(
+ validate_current_byte_bound_blossom_source(
+ relative,
+ &bytes,
+ *expected_length,
+ expected_sha256,
+ )
+ .unwrap_err()
+ .contains("current-byte source")
+ );
+ }
+ }
+
+ #[test]
+ fn raw_predecessor_supersession_rejects_unknown_paths() {
+ let error = validate_raw_source_rebuild_predecessor_production_sources_under_lock(
+ &workspace_root(),
+ &["not/a/predecessor.rs"],
+ &[],
+ )
+ .unwrap_err();
+ assert!(error.contains("not predecessor-bound"), "{error}");
+ }
+
+ #[test]
+ fn retired_raw_predecessor_write_route_is_validation_only() {
+ let root = workspace_root();
+ let before = IMMUTABLE_RAW_PREDECESSOR_ARTIFACTS
+ .iter()
+ .map(|(relative, _, _)| {
+ (
+ *relative,
+ read_regular_file(&root, relative).expect("immutable raw predecessor artifact"),
+ )
+ })
+ .collect::<Vec<_>>();
+ super::super::raw_source_rebuild::write_raw_source_rebuild_manifest(&root)
+ .expect("retired raw predecessor write route validates its active successor");
+ for (relative, expected) in before {
+ assert_eq!(
+ read_regular_file(&root, relative).expect("raw predecessor after validation"),
+ expected,
+ "retired raw predecessor writer mutated {relative}"
+ );
+ }
+
+ let source = String::from_utf8(
+ read_regular_file(&root, RAW_PREDECESSOR_GOVERNANCE_RELATIVE).unwrap(),
+ )
+ .unwrap();
+ validate_raw_predecessor_successor_routing(&source).unwrap();
+ let bypass = source.replacen(
+ "validate_raw_source_rebuild_manifest(workspace_root)",
+ "Ok(())",
+ 1,
+ );
+ assert!(
+ validate_raw_predecessor_successor_routing(&bypass)
+ .unwrap_err()
+ .contains("validation-only successor route")
+ );
+ }
+}
diff --git a/tools/xtask/src/contract/food_availability_projection.rs b/tools/xtask/src/contract/food_availability_projection.rs
@@ -3993,6 +3993,8 @@ mod tests {
#[test]
fn downstream_nip09_only_supersession_is_transitively_validated() {
const CURRENT_SUCCESSOR_SUPERSEDED_PATHS: &[&str] = &[
+ "crates/blossom/src/error.rs",
+ "crates/blossom/src/lib.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
@@ -17213,7 +17213,9 @@ mod tests {
use super::*;
use std::fs;
- const RAW_SOURCE_REBUILD_PREDECESSOR_SUPERSEDED_PATHS: [&str; 11] = [
+ const RAW_SOURCE_REBUILD_PREDECESSOR_SUPERSEDED_PATHS: [&str; 13] = [
+ "crates/blossom/src/error.rs",
+ "crates/blossom/src/lib.rs",
"crates/event_store/Cargo.toml",
"crates/event_store/src/error.rs",
"crates/event_store/src/generated.rs",
@@ -21210,7 +21212,6 @@ version = "0.1.0"
for (relative, first_module) in [
("crates/event/src/lib.rs", "pub mod account;"),
("crates/event_codec/src/lib.rs", "pub mod d_tag;"),
- ("crates/blossom/src/lib.rs", "pub mod authorization;"),
] {
let spec = *SOURCE_ROUTE_WITNESS_SPECS
.iter()
diff --git a/tools/xtask/src/contract/raw_source_rebuild.rs b/tools/xtask/src/contract/raw_source_rebuild.rs
@@ -1,6 +1,4 @@
-use super::artifact_bundle::{
- GeneratedArtifact, read_regular_file, with_artifact_bundle_transaction,
-};
+use super::artifact_bundle::{GeneratedArtifact, read_regular_file};
use super::food_availability_projection::validate_food_availability_projection_predecessor_production_sources_under_lock;
use super::nip09_reconciliation::{
governed_regular_file_inventory, validate_current_event_store_successor_authority,
@@ -869,6 +867,8 @@ const EXPECTED_SOURCE_MAINTENANCE_DRIFT_PATHS: &[&str] = &[
];
const TRANSITIVE_PREDECESSOR_SUPERSEDED_PATHS: &[&str] = &[
+ "crates/blossom/src/error.rs",
+ "crates/blossom/src/lib.rs",
"crates/event_store/Cargo.toml",
"crates/event_store/src/error.rs",
"crates/event_store/src/generated.rs",
@@ -881,6 +881,8 @@ 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 GENERATED_ARTIFACT_PATHS: &[&str] = &[
MANIFEST_RELATIVE,
@@ -1084,16 +1086,71 @@ struct VectorCase {
}
pub(crate) fn write_raw_source_rebuild_manifest(workspace_root: &Path) -> Result<(), String> {
- with_artifact_bundle_transaction(workspace_root, |transaction| {
- transaction.write(expected_artifacts(workspace_root)?)?;
- validate_raw_source_rebuild_manifest_under_lock(workspace_root)
- })
+ validate_raw_source_rebuild_manifest(workspace_root)
}
pub(crate) fn validate_raw_source_rebuild_manifest(workspace_root: &Path) -> Result<(), String> {
- with_artifact_bundle_transaction(workspace_root, |_| {
- validate_raw_source_rebuild_manifest_under_lock(workspace_root)
- })
+ // Keep the frozen predecessor validator compiled for its governed mutation suite.
+ let _immutable_predecessor_validator: fn(&Path) -> Result<(), String> =
+ validate_raw_source_rebuild_manifest_under_lock;
+ super::blossom_publication_readiness::validate_blossom_publication_readiness(workspace_root)
+}
+
+pub(super) fn validate_raw_source_rebuild_predecessor_production_sources_under_lock(
+ workspace_root: &Path,
+ raw_superseded_paths: &[&str],
+ transitive_superseded_paths: &[&str],
+) -> Result<(), String> {
+ let manifest_bytes = read_regular_file(workspace_root, MANIFEST_RELATIVE)?;
+ let manifest: RawSourceRebuildManifest = serde_json::from_slice(&manifest_bytes)
+ .map_err(|error| format!("parse {MANIFEST_RELATIVE}: {error}"))?;
+ validate_manifest_shape(&manifest)?;
+
+ let superseded = raw_superseded_paths
+ .iter()
+ .copied()
+ .collect::<BTreeSet<_>>();
+ if superseded.len() != raw_superseded_paths.len() {
+ return Err("raw-source rebuild successor supersession paths must be unique".to_owned());
+ }
+ let predecessor_paths = manifest
+ .source_files
+ .iter()
+ .map(|source| source.path.as_str())
+ .collect::<BTreeSet<_>>();
+ if let Some(path) = superseded
+ .iter()
+ .find(|path| !predecessor_paths.contains(**path))
+ {
+ return Err(format!(
+ "raw-source rebuild successor supersession path `{path}` is not predecessor-bound"
+ ));
+ }
+
+ for source in &manifest.source_files {
+ if superseded.contains(source.path.as_str()) {
+ continue;
+ }
+ let current = read_regular_file(workspace_root, &source.path)?;
+ if current.len() as u64 != source.byte_length || sha256_hex(¤t) != source.sha256 {
+ return Err(format!(
+ "unchanged raw-source rebuild predecessor source `{}` drifted",
+ source.path
+ ));
+ }
+ }
+
+ let transitive = TRANSITIVE_PREDECESSOR_SUPERSEDED_PATHS
+ .iter()
+ .copied()
+ .chain(transitive_superseded_paths.iter().copied())
+ .collect::<BTreeSet<_>>()
+ .into_iter()
+ .collect::<Vec<_>>();
+ validate_food_availability_projection_predecessor_production_sources_under_lock(
+ workspace_root,
+ &transitive,
+ )
}
fn validate_raw_source_rebuild_manifest_under_lock(workspace_root: &Path) -> Result<(), String> {
@@ -1927,10 +1984,11 @@ fn validate_predecessor_source_supersession(
.to_owned(),
);
}
- if let Some(path) = transitive
- .iter()
- .find(|path| !successor_paths.contains(**path) && !predecessor_paths.contains(**path))
- {
+ if let Some(path) = transitive.iter().find(|path| {
+ !successor_paths.contains(**path)
+ && !predecessor_paths.contains(**path)
+ && !BLOSSOM_READINESS_SUCCESSOR_TRANSITIVE_PATHS.contains(path)
+ }) {
return Err(format!(
"raw-source rebuild transitive supersession path `{path}` is not bound by the current successor or immutable SourceMaintenance predecessor"
));
@@ -4447,7 +4505,7 @@ fn validate_command_reachability(workspace_root: &Path) -> Result<(), String> {
)?);
for ordered in [
"validate_source_maintenance_manifest(workspace_root)?",
- "validate_raw_source_rebuild_manifest(workspace_root)?",
+ "blossom_publication_readiness::validate_blossom_publication_readiness(workspace_root)?",
"validate_knowledge_contract_manifest(workspace_root)",
] {
if !aggregate.contains(ordered) {
@@ -4459,15 +4517,17 @@ fn validate_command_reachability(workspace_root: &Path) -> Result<(), String> {
let source_index = aggregate
.find("validate_source_maintenance_manifest(workspace_root)?")
.expect("checked above");
- let rebuild_index = aggregate
- .find("validate_raw_source_rebuild_manifest(workspace_root)?")
+ let readiness_index = aggregate
+ .find(
+ "blossom_publication_readiness::validate_blossom_publication_readiness(workspace_root)?",
+ )
.expect("checked above");
let knowledge_index = aggregate
.find("validate_knowledge_contract_manifest(workspace_root)")
.expect("checked above");
- if !(source_index < rebuild_index && rebuild_index < knowledge_index) {
+ if !(source_index < readiness_index && readiness_index < knowledge_index) {
return Err(
- "aggregate contract authority must validate the immutable predecessor before raw-source rebuild and knowledge contracts"
+ "aggregate contract authority must validate immutable predecessors before the Blossom readiness successor and knowledge contracts"
.to_owned(),
);
}