commit cc80ef13c0749572901db232b551a2f14086f785
parent 4c74c57bb4aa1ac17daad9cf168d303030379a80
Author: triesap <tyson@radroots.org>
Date: Sat, 18 Jul 2026 03:09:21 +0000
blossom: add byte-verified blob descriptors
- add portable BUD-01 hash paths and structural URLs
- separate reference approval from descriptor byte verification
- execute canonical vectors in workspace and packaged crates
- register public contracts coverage and Nix test inclusion
Diffstat:
18 files changed, 4693 insertions(+), 2 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -3031,6 +3031,12 @@ dependencies = [
]
[[package]]
+name = "mediatype"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "120fa187be19d9962f0926633453784691731018a2bf936ddb4e29101b79c4a7"
+
+[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4275,6 +4281,18 @@ dependencies = [
]
[[package]]
+name = "radroots_blossom"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "hex",
+ "mediatype",
+ "serde",
+ "serde_json",
+ "sha2",
+ "url",
+]
+
+[[package]]
name = "radroots_core"
version = "0.1.0-alpha.2"
dependencies = [
diff --git a/Cargo.toml b/Cargo.toml
@@ -6,6 +6,7 @@ members = [
"crates/event_codec",
"crates/event_index",
"crates/authority",
+ "crates/blossom",
"crates/libsqlite3_sys_3_53_3",
"crates/geocoder",
"crates/identity",
@@ -72,6 +73,7 @@ radroots_event_store = { path = "crates/event_store", version = "0.1.0-alpha.2",
radroots_event_codec = { path = "crates/event_codec", version = "0.1.0-alpha.2", default-features = false }
radroots_event_index = { path = "crates/event_index", version = "0.1.0-alpha.2", default-features = false }
radroots_authority = { path = "crates/authority", version = "0.1.0-alpha.2", default-features = false }
+radroots_blossom = { path = "crates/blossom", version = "0.1.0-alpha.2", default-features = false }
radroots_geocoder = { path = "crates/geocoder", version = "0.1.0-alpha.2" }
radroots_identity = { path = "crates/identity", version = "0.1.0-alpha.2", default-features = false }
radroots_runtime_store = { path = "crates/runtime_store", version = "0.1.0-alpha.2", default-features = false }
@@ -139,6 +141,7 @@ getrandom = { version = "0.2", default-features = false }
hkdf = { version = "0.12", default-features = false }
hex = { version = "0.4" }
js-sys = { version = "0.3" }
+mediatype = { version = "0.21", default-features = false }
keyring = { version = "3.6.3", default-features = false, features = [
"apple-native",
"windows-native",
@@ -194,6 +197,7 @@ tracing-appender = { version = "0.2" }
tracing-log = { version = "0.2" }
tracing-subscriber = { version = "0.3" }
url = { version = "2" }
+url_nostd = { package = "url", version = "2", default-features = false }
uuid = { version = "1.22.0", features = ["v4", "v7"] }
x509-parser = { version = "0.17", default-features = false }
zstd = { version = "0.13", default-features = false }
diff --git a/build/nix/common.nix b/build/nix/common.nix
@@ -87,6 +87,7 @@ let
releaseRuntimeInputs = coverageRuntimeInputs;
coreContractCrates = [
"xtask"
+ "radroots_blossom"
"radroots_core"
"radroots_event"
"radroots_trade"
diff --git a/contracts/conformance/vectors/blossom/hash_path_and_descriptor.v1.json b/contracts/conformance/vectors/blossom/hash_path_and_descriptor.v1.json
@@ -0,0 +1,1273 @@
+{
+ "suite": "blossom_hash_path_and_descriptor",
+ "contract_version": "0.1.0",
+ "vectors": [
+ {
+ "id": "blossom_sha256_digest_empty_001",
+ "kind": "blossom.sha256.digest",
+ "input": {
+ "bytes_hex": ""
+ },
+ "expected": {
+ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ {
+ "id": "blossom_sha256_digest_abc_002",
+ "kind": "blossom.sha256.digest",
+ "input": {
+ "bytes_hex": "616263"
+ },
+ "expected": {
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
+ }
+ },
+ {
+ "id": "blossom_sha256_digest_binary_003",
+ "kind": "blossom.sha256.digest",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a726164726f6f7473"
+ },
+ "expected": {
+ "sha256": "a829dda89323e71a4575c1c45c52d65c3a0c87dd8ffcdab9ad12bbee57c00861"
+ }
+ },
+ {
+ "id": "blossom_sha256_parse_valid_004",
+ "kind": "blossom.sha256.parse.valid",
+ "input": {
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
+ },
+ "expected": {
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
+ }
+ },
+ {
+ "id": "blossom_sha256_parse_zero_valid_005",
+ "kind": "blossom.sha256.parse.valid",
+ "input": {
+ "sha256": "0000000000000000000000000000000000000000000000000000000000000000"
+ },
+ "expected": {
+ "sha256": "0000000000000000000000000000000000000000000000000000000000000000"
+ }
+ },
+ {
+ "id": "blossom_sha256_parse_short_invalid_006",
+ "kind": "blossom.sha256.parse.invalid",
+ "input": {
+ "sha256": "ba7816bf8f01cfea"
+ },
+ "expected": {
+ "error": "invalid_sha256"
+ }
+ },
+ {
+ "id": "blossom_sha256_parse_uppercase_invalid_007",
+ "kind": "blossom.sha256.parse.invalid",
+ "input": {
+ "sha256": "BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD"
+ },
+ "expected": {
+ "error": "invalid_sha256"
+ }
+ },
+ {
+ "id": "blossom_sha256_parse_non_hex_invalid_008",
+ "kind": "blossom.sha256.parse.invalid",
+ "input": {
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ag"
+ },
+ "expected": {
+ "error": "invalid_sha256"
+ }
+ },
+ {
+ "id": "blossom_hash_path_without_extension_009",
+ "kind": "blossom.hash_path.parse.valid",
+ "input": {
+ "path": "/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "expected": {
+ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "extension": null
+ }
+ },
+ {
+ "id": "blossom_hash_path_png_extension_010",
+ "kind": "blossom.hash_path.parse.valid",
+ "input": {
+ "path": "/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "extension": "png"
+ }
+ },
+ {
+ "id": "blossom_hash_path_multi_extension_011",
+ "kind": "blossom.hash_path.parse.valid",
+ "input": {
+ "path": "/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.tar.gz"
+ },
+ "expected": {
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "extension": "tar.gz"
+ }
+ },
+ {
+ "id": "blossom_hash_path_extension_case_preserved_012",
+ "kind": "blossom.hash_path.parse.valid",
+ "input": {
+ "path": "/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.Photo_JPG-2"
+ },
+ "expected": {
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "extension": "Photo_JPG-2"
+ }
+ },
+ {
+ "id": "blossom_hash_path_missing_root_slash_013",
+ "kind": "blossom.hash_path.parse.invalid",
+ "input": {
+ "path": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "invalid_hash_path"
+ }
+ },
+ {
+ "id": "blossom_hash_path_empty_014",
+ "kind": "blossom.hash_path.parse.invalid",
+ "input": {
+ "path": "/"
+ },
+ "expected": {
+ "error": "invalid_hash_path"
+ }
+ },
+ {
+ "id": "blossom_hash_path_extra_segment_015",
+ "kind": "blossom.hash_path.parse.invalid",
+ "input": {
+ "path": "/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad/png"
+ },
+ "expected": {
+ "error": "invalid_hash_path"
+ }
+ },
+ {
+ "id": "blossom_hash_path_uppercase_hash_016",
+ "kind": "blossom.hash_path.parse.invalid",
+ "input": {
+ "path": "/BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD.png"
+ },
+ "expected": {
+ "error": "invalid_sha256"
+ }
+ },
+ {
+ "id": "blossom_hash_path_empty_extension_017",
+ "kind": "blossom.hash_path.parse.invalid",
+ "input": {
+ "path": "/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad."
+ },
+ "expected": {
+ "error": "invalid_file_extension"
+ }
+ },
+ {
+ "id": "blossom_hash_path_empty_extension_component_018",
+ "kind": "blossom.hash_path.parse.invalid",
+ "input": {
+ "path": "/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.tar..gz"
+ },
+ "expected": {
+ "error": "invalid_file_extension"
+ }
+ },
+ {
+ "id": "blossom_hash_path_whitespace_extension_019",
+ "kind": "blossom.hash_path.parse.invalid",
+ "input": {
+ "path": "/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.p ng"
+ },
+ "expected": {
+ "error": "invalid_file_extension"
+ }
+ },
+ {
+ "id": "blossom_hash_path_percent_encoded_extension_020",
+ "kind": "blossom.hash_path.parse.invalid",
+ "input": {
+ "path": "/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.%70ng"
+ },
+ "expected": {
+ "error": "invalid_hash_path"
+ }
+ },
+ {
+ "id": "blossom_hash_path_query_forbidden_021",
+ "kind": "blossom.hash_path.parse.invalid",
+ "input": {
+ "path": "/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png?download=1"
+ },
+ "expected": {
+ "error": "invalid_hash_path"
+ }
+ },
+ {
+ "id": "blossom_blob_url_https_canonical_authority_022",
+ "kind": "blossom.blob_url.parse.valid",
+ "input": {
+ "url": "HTTPS://MEDIA.EXAMPLE/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.PNG"
+ },
+ "expected": {
+ "scheme": "https",
+ "host": "media.example",
+ "port": null,
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "extension": "PNG"
+ }
+ },
+ {
+ "id": "blossom_blob_url_public_http_structural_023",
+ "kind": "blossom.blob_url.parse.valid",
+ "input": {
+ "url": "http://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "scheme": "http",
+ "host": "media.example",
+ "port": null,
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "extension": "png"
+ }
+ },
+ {
+ "id": "blossom_blob_url_https_port_without_extension_024",
+ "kind": "blossom.blob_url.parse.valid",
+ "input": {
+ "url": "https://media.example:8443/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "expected": {
+ "scheme": "https",
+ "host": "media.example",
+ "port": 8443,
+ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "extension": null
+ }
+ },
+ {
+ "id": "blossom_blob_url_bracketed_ipv6_025",
+ "kind": "blossom.blob_url.parse.valid",
+ "input": {
+ "url": "http://[::1]:21100/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.bin"
+ },
+ "expected": {
+ "scheme": "http",
+ "host": "[::1]",
+ "port": 21100,
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "extension": "bin"
+ }
+ },
+ {
+ "id": "blossom_blob_url_relative_invalid_026",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "invalid_blob_url"
+ }
+ },
+ {
+ "id": "blossom_blob_url_unsupported_scheme_027",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "ftp://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "unsupported_blob_url_scheme"
+ }
+ },
+ {
+ "id": "blossom_blob_url_missing_authority_028",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://"
+ },
+ "expected": {
+ "error": "invalid_blob_url"
+ }
+ },
+ {
+ "id": "blossom_blob_url_credentials_forbidden_029",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://user:password@media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "blob_url_credentials_forbidden"
+ }
+ },
+ {
+ "id": "blossom_blob_url_empty_userinfo_forbidden_029b",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://@media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "blob_url_credentials_forbidden"
+ }
+ },
+ {
+ "id": "blossom_blob_url_empty_userinfo_separator_forbidden_029c",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://:@media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "blob_url_credentials_forbidden"
+ }
+ },
+ {
+ "id": "blossom_blob_url_query_forbidden_030",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png?download=1"
+ },
+ "expected": {
+ "error": "blob_url_query_forbidden"
+ }
+ },
+ {
+ "id": "blossom_blob_url_fragment_forbidden_031",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png#preview"
+ },
+ "expected": {
+ "error": "blob_url_fragment_forbidden"
+ }
+ },
+ {
+ "id": "blossom_blob_url_extra_path_segment_032",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://media.example/blobs/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "invalid_hash_path"
+ }
+ },
+ {
+ "id": "blossom_blob_url_port_out_of_range_033",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://media.example:65536/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "invalid_blob_url"
+ }
+ },
+ {
+ "id": "blossom_blob_url_uppercase_hash_034",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://media.example/BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD.png"
+ },
+ "expected": {
+ "error": "invalid_sha256"
+ }
+ },
+ {
+ "id": "blossom_blob_url_percent_encoded_extension_035",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.%70ng"
+ },
+ "expected": {
+ "error": "invalid_blob_url"
+ }
+ },
+ {
+ "id": "blossom_blob_url_unbracketed_ipv6_036",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "http://::1/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.bin"
+ },
+ "expected": {
+ "error": "invalid_blob_url"
+ }
+ },
+ {
+ "id": "blossom_blob_url_leading_whitespace_rejected_036b",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": " https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "invalid_blob_url"
+ }
+ },
+ {
+ "id": "blossom_blob_url_embedded_newline_rejected_036c",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://media.example/\nba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "invalid_blob_url"
+ }
+ },
+ {
+ "id": "blossom_blob_url_trailing_tab_rejected_036d",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png\t"
+ },
+ "expected": {
+ "error": "invalid_blob_url"
+ }
+ },
+ {
+ "id": "blossom_reference_https_public_approved_037",
+ "kind": "blossom.reference_policy.valid",
+ "input": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "approved": true,
+ "transport": "https"
+ }
+ },
+ {
+ "id": "blossom_reference_https_private_address_approved_038",
+ "kind": "blossom.reference_policy.valid",
+ "input": {
+ "url": "https://10.0.0.4/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "approved": true,
+ "transport": "https"
+ }
+ },
+ {
+ "id": "blossom_reference_http_localhost_approved_039",
+ "kind": "blossom.reference_policy.valid",
+ "input": {
+ "url": "http://LOCALHOST:21100/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "approved": true,
+ "transport": "loopback_http"
+ }
+ },
+ {
+ "id": "blossom_reference_http_localhost_subdomain_approved_040",
+ "kind": "blossom.reference_policy.valid",
+ "input": {
+ "url": "http://media.localhost/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "approved": true,
+ "transport": "loopback_http"
+ }
+ },
+ {
+ "id": "blossom_reference_http_nested_localhost_subdomain_approved_041",
+ "kind": "blossom.reference_policy.valid",
+ "input": {
+ "url": "http://blossom.dev.localhost/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "approved": true,
+ "transport": "loopback_http"
+ }
+ },
+ {
+ "id": "blossom_reference_http_ipv4_loopback_approved_042",
+ "kind": "blossom.reference_policy.valid",
+ "input": {
+ "url": "http://127.0.0.1:21100/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "approved": true,
+ "transport": "loopback_http"
+ }
+ },
+ {
+ "id": "blossom_reference_http_ipv4_loopback_range_approved_043",
+ "kind": "blossom.reference_policy.valid",
+ "input": {
+ "url": "http://127.255.255.254/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "approved": true,
+ "transport": "loopback_http"
+ }
+ },
+ {
+ "id": "blossom_reference_http_ipv6_loopback_approved_044",
+ "kind": "blossom.reference_policy.valid",
+ "input": {
+ "url": "http://[::1]:21100/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "approved": true,
+ "transport": "loopback_http"
+ }
+ },
+ {
+ "id": "blossom_reference_http_expanded_ipv6_loopback_approved_045",
+ "kind": "blossom.reference_policy.valid",
+ "input": {
+ "url": "http://[0:0:0:0:0:0:0:1]/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "approved": true,
+ "transport": "loopback_http"
+ }
+ },
+ {
+ "id": "blossom_reference_http_public_rejected_046",
+ "kind": "blossom.reference_policy.invalid",
+ "input": {
+ "url": "http://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "insecure_blob_url"
+ }
+ },
+ {
+ "id": "blossom_reference_http_false_localhost_suffix_rejected_047",
+ "kind": "blossom.reference_policy.invalid",
+ "input": {
+ "url": "http://localhost.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "insecure_blob_url"
+ }
+ },
+ {
+ "id": "blossom_reference_http_absolute_localhost_name_rejected_048",
+ "kind": "blossom.reference_policy.invalid",
+ "input": {
+ "url": "http://localhost./ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "insecure_blob_url"
+ }
+ },
+ {
+ "id": "blossom_reference_http_before_ipv4_loopback_range_rejected_049",
+ "kind": "blossom.reference_policy.invalid",
+ "input": {
+ "url": "http://126.255.255.255/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "insecure_blob_url"
+ }
+ },
+ {
+ "id": "blossom_reference_http_after_ipv4_loopback_range_rejected_050",
+ "kind": "blossom.reference_policy.invalid",
+ "input": {
+ "url": "http://128.0.0.0/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "insecure_blob_url"
+ }
+ },
+ {
+ "id": "blossom_reference_http_private_ipv4_rejected_051",
+ "kind": "blossom.reference_policy.invalid",
+ "input": {
+ "url": "http://10.0.0.4/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "insecure_blob_url"
+ }
+ },
+ {
+ "id": "blossom_reference_http_unspecified_ipv4_rejected_052",
+ "kind": "blossom.reference_policy.invalid",
+ "input": {
+ "url": "http://0.0.0.0/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "insecure_blob_url"
+ }
+ },
+ {
+ "id": "blossom_reference_http_ipv6_link_local_rejected_053",
+ "kind": "blossom.reference_policy.invalid",
+ "input": {
+ "url": "http://[fe80::1]/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "insecure_blob_url"
+ }
+ },
+ {
+ "id": "blossom_reference_http_ipv4_mapped_ipv6_rejected_054",
+ "kind": "blossom.reference_policy.invalid",
+ "input": {
+ "url": "http://[::ffff:127.0.0.1]/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "insecure_blob_url"
+ }
+ },
+ {
+ "id": "blossom_blob_url_short_ipv4_spelling_rejected_055",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "http://127.1/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "invalid_blob_url"
+ }
+ },
+ {
+ "id": "blossom_reference_http_private_ipv4_second_range_rejected_056",
+ "kind": "blossom.reference_policy.invalid",
+ "input": {
+ "url": "http://192.168.1.4/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "insecure_blob_url"
+ }
+ },
+ {
+ "id": "blossom_media_type_image_png_057",
+ "kind": "blossom.media_type.parse.valid",
+ "input": {
+ "media_type": "image/png"
+ },
+ "expected": {
+ "media_type": "image/png"
+ }
+ },
+ {
+ "id": "blossom_media_type_case_canonicalized_058",
+ "kind": "blossom.media_type.parse.valid",
+ "input": {
+ "media_type": "IMAGE/PNG"
+ },
+ "expected": {
+ "media_type": "image/png"
+ }
+ },
+ {
+ "id": "blossom_media_type_vendor_token_059",
+ "kind": "blossom.media_type.parse.valid",
+ "input": {
+ "media_type": "application/vnd.radroots.fixture+json"
+ },
+ "expected": {
+ "media_type": "application/vnd.radroots.fixture+json"
+ }
+ },
+ {
+ "id": "blossom_media_type_empty_060",
+ "kind": "blossom.media_type.parse.invalid",
+ "input": {
+ "media_type": ""
+ },
+ "expected": {
+ "error": "invalid_media_type"
+ }
+ },
+ {
+ "id": "blossom_media_type_parameters_sorted_and_canonicalized_061",
+ "kind": "blossom.media_type.parse.valid",
+ "input": {
+ "media_type": "TEXT/PLAIN; PROFILE=web; CHARSET=UTF-8"
+ },
+ "expected": {
+ "media_type": "text/plain; charset=UTF-8; profile=web"
+ }
+ },
+ {
+ "id": "blossom_media_type_duplicate_parameter_rejected_061b",
+ "kind": "blossom.media_type.parse.invalid",
+ "input": {
+ "media_type": "image/png; profile=a; PROFILE=b"
+ },
+ "expected": {
+ "error": "invalid_media_type"
+ }
+ },
+ {
+ "id": "blossom_media_type_missing_separator_062",
+ "kind": "blossom.media_type.parse.invalid",
+ "input": {
+ "media_type": "imagepng"
+ },
+ "expected": {
+ "error": "invalid_media_type"
+ }
+ },
+ {
+ "id": "blossom_media_type_empty_subtype_063",
+ "kind": "blossom.media_type.parse.invalid",
+ "input": {
+ "media_type": "image/"
+ },
+ "expected": {
+ "error": "invalid_media_type"
+ }
+ },
+ {
+ "id": "blossom_media_type_wildcard_forbidden_064",
+ "kind": "blossom.media_type.parse.invalid",
+ "input": {
+ "media_type": "image/*"
+ },
+ "expected": {
+ "error": "invalid_media_type"
+ }
+ },
+ {
+ "id": "blossom_media_type_parser_truncated_whitespace_forbidden_065",
+ "kind": "blossom.media_type.parse.invalid",
+ "input": {
+ "media_type": "image/png "
+ },
+ "expected": {
+ "error": "invalid_media_type"
+ }
+ },
+ {
+ "id": "blossom_media_type_non_ascii_forbidden_066",
+ "kind": "blossom.media_type.parse.invalid",
+ "input": {
+ "media_type": "image/p\u00f1g"
+ },
+ "expected": {
+ "error": "invalid_media_type"
+ }
+ },
+ {
+ "id": "blossom_descriptor_https_valid_067",
+ "kind": "blossom.descriptor.parse.valid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ }
+ },
+ {
+ "id": "blossom_descriptor_empty_blob_extra_fields_valid_068",
+ "kind": "blossom.descriptor.parse.valid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.bin",
+ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "size": 0,
+ "type": "application/octet-stream",
+ "uploaded": 0,
+ "blurhash": "fixture-only-extra-field",
+ "custom": {
+ "version": 1
+ }
+ }
+ },
+ "expected": {
+ "descriptor": {
+ "url": "https://media.example/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.bin",
+ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "size": 0,
+ "type": "application/octet-stream",
+ "uploaded": 0
+ }
+ }
+ },
+ {
+ "id": "blossom_descriptor_extension_does_not_define_mime_069",
+ "kind": "blossom.descriptor.parse.valid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.jpg",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "image/png",
+ "uploaded": 1700000001
+ }
+ },
+ "expected": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.jpg",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "image/png",
+ "uploaded": 1700000001
+ }
+ }
+ },
+ {
+ "id": "blossom_descriptor_public_http_structurally_valid_070",
+ "kind": "blossom.descriptor.parse.valid",
+ "input": {
+ "descriptor": {
+ "url": "http://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000002
+ }
+ },
+ "expected": {
+ "descriptor": {
+ "url": "http://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000002
+ }
+ }
+ },
+ {
+ "id": "blossom_descriptor_media_type_canonicalized_071",
+ "kind": "blossom.descriptor.parse.valid",
+ "input": {
+ "descriptor": {
+ "url": "http://127.0.0.1:21100/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.PNG",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "IMAGE/PNG",
+ "uploaded": 0
+ }
+ },
+ "expected": {
+ "descriptor": {
+ "url": "http://127.0.0.1:21100/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.PNG",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "image/png",
+ "uploaded": 0
+ }
+ }
+ },
+ {
+ "id": "blossom_descriptor_missing_url_072",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "missing_descriptor_field",
+ "field": "url"
+ }
+ },
+ {
+ "id": "blossom_descriptor_missing_sha256_073",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "missing_descriptor_field",
+ "field": "sha256"
+ }
+ },
+ {
+ "id": "blossom_descriptor_missing_size_074",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "missing_descriptor_field",
+ "field": "size"
+ }
+ },
+ {
+ "id": "blossom_descriptor_missing_type_075",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "missing_descriptor_field",
+ "field": "type"
+ }
+ },
+ {
+ "id": "blossom_descriptor_missing_uploaded_076",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain"
+ }
+ },
+ "expected": {
+ "error": "missing_descriptor_field",
+ "field": "uploaded"
+ }
+ },
+ {
+ "id": "blossom_descriptor_url_extension_required_077",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "descriptor_extension_required"
+ }
+ },
+ {
+ "id": "blossom_descriptor_url_hash_mismatch_078",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "0000000000000000000000000000000000000000000000000000000000000000",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "descriptor_hash_mismatch"
+ }
+ },
+ {
+ "id": "blossom_descriptor_negative_size_079",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": -1,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "invalid_descriptor_size"
+ }
+ },
+ {
+ "id": "blossom_descriptor_fractional_size_080",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3.5,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "invalid_descriptor_size"
+ }
+ },
+ {
+ "id": "blossom_descriptor_negative_uploaded_081",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": -1
+ }
+ },
+ "expected": {
+ "error": "invalid_descriptor_uploaded"
+ }
+ },
+ {
+ "id": "blossom_descriptor_parameterized_media_type_valid_082",
+ "kind": "blossom.descriptor.parse.valid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "TEXT/PLAIN; PROFILE=mobile; CHARSET=UTF-8",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain; charset=UTF-8; profile=mobile",
+ "uploaded": 1700000000
+ }
+ }
+ },
+ {
+ "id": "blossom_descriptor_invalid_media_type_082b",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "image/",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "invalid_media_type"
+ }
+ },
+ {
+ "id": "blossom_descriptor_uppercase_sha256_083",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "invalid_sha256"
+ }
+ },
+ {
+ "id": "blossom_descriptor_url_query_forbidden_084",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt?download=1",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "blob_url_query_forbidden"
+ }
+ },
+ {
+ "id": "blossom_descriptor_approved_https_reference_084b",
+ "kind": "blossom.descriptor.approve_reference.valid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "approved_url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt"
+ }
+ },
+ {
+ "id": "blossom_descriptor_public_http_reference_rejected_084c",
+ "kind": "blossom.descriptor.approve_reference.invalid",
+ "input": {
+ "descriptor": {
+ "url": "http://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "insecure_blob_url"
+ }
+ },
+ {
+ "id": "blossom_descriptor_verify_abc_valid_085",
+ "kind": "blossom.descriptor.verify_bytes.valid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ },
+ "approved_media_type": "text/plain",
+ "bytes_hex": "616263"
+ },
+ "expected": {
+ "verified": {
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "media_type": "text/plain"
+ }
+ }
+ },
+ {
+ "id": "blossom_descriptor_verify_empty_valid_086",
+ "kind": "blossom.descriptor.verify_bytes.valid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.bin",
+ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "size": 0,
+ "type": "application/octet-stream",
+ "uploaded": 1700000000
+ },
+ "approved_media_type": "application/octet-stream",
+ "bytes_hex": ""
+ },
+ "expected": {
+ "verified": {
+ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "size": 0,
+ "media_type": "application/octet-stream"
+ }
+ }
+ },
+ {
+ "id": "blossom_descriptor_verify_no_sniff_or_extension_inference_087",
+ "kind": "blossom.descriptor.verify_bytes.valid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.jpg",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "image/png",
+ "uploaded": 1700000000
+ },
+ "approved_media_type": "image/png",
+ "bytes_hex": "616263"
+ },
+ "expected": {
+ "verified": {
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "media_type": "image/png"
+ }
+ }
+ },
+ {
+ "id": "blossom_descriptor_verify_approved_media_type_canonicalized_088",
+ "kind": "blossom.descriptor.verify_bytes.valid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "image/png",
+ "uploaded": 1700000000
+ },
+ "approved_media_type": "IMAGE/PNG",
+ "bytes_hex": "616263"
+ },
+ "expected": {
+ "verified": {
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "media_type": "image/png"
+ }
+ }
+ },
+ {
+ "id": "blossom_descriptor_verify_hash_mismatch_089",
+ "kind": "blossom.descriptor.verify_bytes.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 7,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ },
+ "approved_media_type": "text/plain",
+ "bytes_hex": "6368616e676564"
+ },
+ "expected": {
+ "error": "blob_hash_mismatch"
+ }
+ },
+ {
+ "id": "blossom_descriptor_verify_size_mismatch_090",
+ "kind": "blossom.descriptor.verify_bytes.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 4,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ },
+ "approved_media_type": "text/plain",
+ "bytes_hex": "616263"
+ },
+ "expected": {
+ "error": "blob_size_mismatch"
+ }
+ },
+ {
+ "id": "blossom_descriptor_verify_media_type_mismatch_091",
+ "kind": "blossom.descriptor.verify_bytes.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ },
+ "approved_media_type": "application/octet-stream",
+ "bytes_hex": "616263"
+ },
+ "expected": {
+ "error": "blob_media_type_mismatch"
+ }
+ }
+ ]
+}
diff --git a/contracts/coverage.toml b/contracts/coverage.toml
@@ -32,6 +32,7 @@ reason = "branch coverage is not applicable while the crate has no measured bran
[required]
crates = [
"radroots_authority",
+ "radroots_blossom",
"radroots_core",
"radroots_event_store",
"radroots_event",
diff --git a/contracts/events/blossom-media.md b/contracts/events/blossom-media.md
@@ -0,0 +1,231 @@
+# Blossom Media Foundation
+
+Status: active implementation contract
+
+Scope: public, runtime-independent Radroots primitives for Blossom BUD-01 hash paths, BUD-02
+descriptors, and local descriptor-versus-byte verification.
+
+## Protocol Basis
+
+This contract is based on the Blossom repository at commit
+`b5bd2801d1763aa635fc8fea7a76597e0eb18990`:
+
+- [BUD-01: Server requirements and blob retrieval](https://github.com/hzrd149/blossom/blob/b5bd2801d1763aa635fc8fea7a76597e0eb18990/buds/01.md)
+- [BUD-02: Blob upload and descriptor](https://github.com/hzrd149/blossom/blob/b5bd2801d1763aa635fc8fea7a76597e0eb18990/buds/02.md)
+
+The upstream BUD rules and the Radroots application profile are distinct layers. A URL can be a
+structurally valid Blossom URL without being an approved Radroots reference. Callers must not treat
+structural parsing as approval, byte verification, network reachability, or proof that a server
+implements Blossom.
+
+## Ownership And Dependency Boundary
+
+`radroots_blossom` owns these primitives as a public `no_std + alloc` protocol leaf. It may use pure
+hashing and serialization dependencies, but it must not depend on Radroots event models, event
+codecs, Nostr, an HTTP client or server runtime, platform policy, or private code.
+
+This contract does not implement BUD-03 server discovery, BUD-11 authorization, BUD-12 listing,
+upload entitlement, object-storage policy, network I/O, redirects, retries, or availability checks.
+BUD-11 is added as a separate foundation slice.
+
+## SHA-256
+
+A `RadrootsBlossomSha256` is exactly 32 bytes. Its wire and display form is exactly 64 lowercase
+ASCII hexadecimal characters.
+
+- Parsing rejects uppercase hexadecimal, prefixes, separators, whitespace, non-hexadecimal bytes,
+ and every length other than 64 characters.
+- Serialization and display always emit lowercase hexadecimal.
+- Digest construction hashes the exact supplied bytes without text conversion or normalization.
+- The empty byte sequence is valid and has its ordinary SHA-256 digest.
+
+## Root Hash Paths
+
+A `RadrootsBlossomHashPath` represents one BUD-01 root path:
+
+```text
+/<64-lowercase-hex-sha256>[.<extension>]
+```
+
+The leading slash is required by the root-path parser. The path has exactly one segment. It has no
+query, fragment, percent decoding, dot segment, or trailing slash.
+
+An extension is returned without its leading period. It consists of one or more nonempty
+dot-separated components. Each component contains only ASCII letters, digits, `-`, or `_`.
+Extension case is preserved because the remote path may be case-sensitive. Examples include `png`,
+`JPG`, and `tar.gz`; empty components, whitespace, path separators, percent escapes, and `..` are
+invalid.
+
+BUD-01 retrieval paths may omit the extension. A URL carried by a BUD-02 blob descriptor must
+include one. The extension is not used to infer or validate the descriptor MIME type.
+
+## Structural Blob URLs
+
+A `RadrootsBlossomBlobUrl` is an absolute structural BUD-01 URL with these invariants:
+
+- the scheme is `http` or `https`, compared case-insensitively and exposed canonically in lowercase
+- the authority contains one nonempty DNS hostname, canonical dotted-decimal IPv4 address, or
+ bracketed IPv6 address and may contain a valid decimal port
+- user information is forbidden
+- the path is exactly one root hash path
+- query and fragment components are forbidden
+- the path digest is exposed as a typed `RadrootsBlossomSha256`
+- an optional safe extension is parsed independently from the digest
+
+Scheme and DNS host case are canonicalized to lowercase by the URL parser. Extension case is
+preserved. Noncanonical, shortened, octal, hexadecimal, or otherwise ambiguous IPv4 spellings are
+rejected before a typed URL is returned; this prevents approval behavior from changing after a
+serialization round trip. Raw user-information delimiters, whitespace, control characters,
+percent-encoded path data, malformed or zero ports, unbracketed IPv6 addresses, and additional path
+segments are also rejected before URL-parser normalization can discard or reinterpret them.
+
+Structural validity proves only that the URL has a BUD-01-compatible shape. It does not approve the
+transport scheme, perform DNS resolution, follow a redirect, issue `HEAD` or `GET`, or establish
+that bytes exist at the URL.
+
+## Approved Reference Policy
+
+Radroots reference approval is a second, explicit validation step over a structurally valid blob
+URL:
+
+- every structurally valid `https` URL is transport-approved
+- `http` is approved only for a syntactic loopback host
+- the loopback hostname set is case-insensitive exact `localhost`, or a hostname with at least one
+ nonempty label ending in `.localhost`
+- the loopback IPv4 set is canonical dotted-decimal `127.0.0.0/8`
+- the loopback IPv6 set is the address `::1`, including equivalent expanded IPv6 spelling
+
+The policy does not use DNS resolution. A public hostname that resolves to loopback is not accepted
+as loopback. `localhost.`, `localhost.example`, IPv4-mapped IPv6 addresses, private or link-local
+addresses, unspecified addresses, and arbitrary public hosts are not approved for `http`. Expanded
+IPv6 spellings that parse exactly to `::1` remain structurally valid and approved.
+
+HTTPS approval is a reference-transport rule, not an SSRF or egress policy. Network runtimes remain
+responsible for their own destination, redirect, DNS-rebinding, response-size, and timeout controls.
+
+## Media Types
+
+A `RadrootsBlossomMediaType` is a syntactically valid MIME media type parsed by the repository's
+`mediatype` dependency. MIME parameters are supported. The canonical representation lowercases the
+type, subtype, suffix, and parameter names, sorts parameters by name, and preserves parameter
+values. Duplicate parameter names are rejected. Semantic equality is case-insensitive for those
+names, while parameter values retain the media-type library's case-sensitive semantics.
+
+Empty values, missing type/subtype structure, wildcards, invalid parameter syntax, leading or
+parser-truncated trailing whitespace, control characters, and non-ASCII token characters are
+rejected.
+
+Parsing never substitutes a default for empty or malformed input. A caller that intends
+`application/octet-stream` must provide it explicitly.
+
+The media type used for byte verification is selected and approved by the caller. This foundation
+does not sniff or infer content type from file bytes or the URL extension.
+
+## BUD-02 Descriptors
+
+A structural `RadrootsBlossomBlobDescriptor` contains the five required BUD-02 fields:
+
+| Field | Contract |
+| --- | --- |
+| `url` | structurally valid absolute root blob URL with an extension |
+| `sha256` | typed lowercase SHA-256 equal to the digest in `url` |
+| `size` | unsigned 64-bit byte count |
+| `type` | parsed `RadrootsBlossomMediaType` |
+| `uploaded` | unsigned 64-bit Unix timestamp |
+
+Unknown descriptor fields are tolerated on input for forward compatibility. They need not be
+retained when the typed descriptor is serialized. Required fields, field types, URL structure, URL
+extension presence, URL/hash equality, and MIME syntax are validated during construction and
+deserialization; public fields must not permit bypassing those invariants.
+
+Descriptor parsing is structural. It does not apply the approved-reference scheme policy. An
+authored publication flow must explicitly require both a structurally valid descriptor and an
+approved descriptor URL.
+
+The `uploaded` value is parsed as protocol data. Structural parsing does not compare it to a wall
+clock or treat it as server attestation.
+
+## Byte Commitments And Byte-Verified Descriptors
+
+A structural descriptor advances to `RadrootsBlossomApprovedDescriptor` only after its URL passes
+the explicit HTTPS-or-loopback reference policy. `RadrootsBlossomByteCommitment::from_bytes`
+computes the SHA-256 and exact `u64` size of a final byte slice and binds them to a caller-approved
+`RadrootsBlossomMediaType`.
+
+An approved descriptor advances to `RadrootsBlossomByteVerifiedDescriptor` only through
+`verify_commitment`, or through the `verify_bytes` convenience path that constructs the same byte
+commitment. Verification succeeds only when all three values match:
+
+1. computed byte SHA-256 equals descriptor `sha256`
+2. exact byte length equals descriptor `size`
+3. caller-approved media type is semantically equal to descriptor `type`
+
+These comparisons are independent. URL extension does not participate in MIME verification, and
+no byte sniffing occurs.
+
+The resulting byte-verified state proves only that the reference policy approved the descriptor URL,
+the supplied descriptor and approved media type describe the supplied bytes, and the descriptor URL
+carries the same digest. It is deliberately not named or modeled as an upload receipt. It does not
+prove that an upload occurred, the descriptor was issued by the named server, the server is
+authorized or reachable, the blob is currently available, a redirect preserves the hash, or a later
+retrieval returns those bytes. An HTTP-capable runtime must separately require a successful BUD-02
+upload response before publication and must perform bounded retrieval checks at their owning layer.
+
+## Stable Conformance Operations
+
+`contracts/conformance/vectors/blossom/hash_path_and_descriptor.v1.json` is executable evidence for
+this contract. Integration tests dispatch its vectors by `kind`:
+
+- `blossom.sha256.digest`
+- `blossom.sha256.parse.valid` and `blossom.sha256.parse.invalid`
+- `blossom.hash_path.parse.valid` and `blossom.hash_path.parse.invalid`
+- `blossom.blob_url.parse.valid` and `blossom.blob_url.parse.invalid`
+- `blossom.reference_policy.valid` and `blossom.reference_policy.invalid`
+- `blossom.media_type.parse.valid` and `blossom.media_type.parse.invalid`
+- `blossom.descriptor.parse.valid` and `blossom.descriptor.parse.invalid`
+- `blossom.descriptor.approve_reference.valid` and
+ `blossom.descriptor.approve_reference.invalid`
+- `blossom.descriptor.verify_bytes.valid` and
+ `blossom.descriptor.verify_bytes.invalid`
+
+`bytes_hex` values are exact bytes encoded as lowercase hexadecimal. A `null` extension or port
+means absence. Invalid typed-operation vectors use the exact stable identifier returned by
+`RadrootsBlossomError::code()`.
+
+## Stable Error Identifiers
+
+The public typed API exposes these stable semantic identifiers:
+
+- `invalid_sha256`
+- `invalid_hash_path`
+- `invalid_file_extension`
+- `invalid_blob_url`
+- `unsupported_blob_url_scheme`
+- `blob_url_credentials_forbidden`
+- `blob_url_query_forbidden`
+- `blob_url_fragment_forbidden`
+- `insecure_blob_url`
+- `invalid_media_type`
+- `descriptor_extension_required`
+- `descriptor_hash_mismatch`
+- `blob_hash_mismatch`
+- `blob_size_mismatch`
+- `blob_media_type_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:
+
+- `missing_descriptor_field`, with the missing field named separately
+- `invalid_descriptor_size`
+- `invalid_descriptor_uploaded`
+
+Nested descriptor values that reach a Radroots parser, including invalid SHA-256, URL, MIME, URL
+extension, and URL/hash combinations, retain the applicable `RadrootsBlossomError::code()` value.
+
+The canonical vector file is mirrored under `crates/blossom/tests/fixtures/` so the published crate
+can execute the same conformance suite. Source-workspace tests require the canonical file and assert
+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.
diff --git a/contracts/manifest.toml b/contracts/manifest.toml
@@ -6,15 +6,17 @@ source = "rust"
[surface]
model_crates = [
"radroots_protocol_contract_v1",
+ "radroots_blossom",
"radroots_core",
"radroots_event",
"radroots_trade",
"radroots_identity",
]
-algorithm_crates = ["radroots_event_codec"]
+algorithm_crates = ["radroots_blossom", "radroots_event_codec"]
[surface.rust_crate_tiers]
advanced_substrate = [
+ "radroots_blossom",
"radroots_core",
"radroots_event",
"radroots_event_codec",
diff --git a/contracts/operations.toml b/contracts/operations.toml
@@ -5,6 +5,7 @@ source = "rust"
[public]
domains = [
+ "blossom",
"profile",
"farm",
"listing",
@@ -16,6 +17,16 @@ domains = [
[shared_types]
public = [
+ "RadrootsBlossomSha256",
+ "RadrootsBlossomFileExtension",
+ "RadrootsBlossomHashPath",
+ "RadrootsBlossomBlobUrl",
+ "RadrootsBlossomApprovedBlobUrl",
+ "RadrootsBlossomMediaType",
+ "RadrootsBlossomBlobDescriptor",
+ "RadrootsBlossomApprovedDescriptor",
+ "RadrootsBlossomByteCommitment",
+ "RadrootsBlossomByteVerifiedDescriptor",
"RadrootsNip01EventWireParts",
"RadrootsEventDraft",
"RadrootsSignedEvent",
@@ -94,12 +105,86 @@ classes = [
[implementation_provenance]
model_crates = [
"radroots_protocol_contract_v1",
+ "radroots_blossom",
"radroots_core",
"radroots_event",
"radroots_trade",
"radroots_identity",
]
-algorithm_crates = ["radroots_event_codec"]
+algorithm_crates = ["radroots_blossom", "radroots_event_codec"]
+
+[operations.blossom_parse_hash_path]
+domain = "blossom"
+id = "blossom.parse_hash_path"
+stability = "beta"
+inputs = ["String"]
+outputs = ["RadrootsBlossomHashPath"]
+error_class = "validation_error"
+deterministic = true
+signing = "none"
+transport = "none"
+
+[operations.blossom_parse_hash_path.implementation]
+rust_modules = ["crates/blossom/src/hash.rs"]
+rust_types = [
+ "radroots_blossom::RadrootsBlossomFileExtension",
+ "radroots_blossom::RadrootsBlossomHashPath",
+ "radroots_blossom::RadrootsBlossomSha256",
+]
+
+[operations.blossom_parse_hash_path.conformance]
+vector = "contracts/conformance/vectors/blossom/hash_path_and_descriptor.v1.json"
+
+[operations.blossom_approve_descriptor_reference]
+domain = "blossom"
+id = "blossom.approve_descriptor_reference"
+stability = "beta"
+inputs = ["RadrootsBlossomBlobDescriptor"]
+outputs = ["RadrootsBlossomApprovedDescriptor"]
+error_class = "validation_error"
+deterministic = true
+signing = "none"
+transport = "none"
+
+[operations.blossom_approve_descriptor_reference.implementation]
+rust_modules = ["crates/blossom/src/descriptor.rs", "crates/blossom/src/url.rs"]
+rust_types = [
+ "radroots_blossom::RadrootsBlossomApprovedDescriptor",
+ "radroots_blossom::RadrootsBlossomBlobDescriptor",
+]
+
+[operations.blossom_approve_descriptor_reference.conformance]
+vector = "contracts/conformance/vectors/blossom/hash_path_and_descriptor.v1.json"
+
+[operations.blossom_verify_descriptor_bytes]
+domain = "blossom"
+id = "blossom.verify_descriptor_bytes"
+stability = "beta"
+inputs = [
+ "RadrootsBlossomApprovedDescriptor",
+ "Bytes",
+ "RadrootsBlossomMediaType",
+]
+outputs = ["RadrootsBlossomByteVerifiedDescriptor"]
+error_class = "validation_error"
+deterministic = true
+signing = "none"
+transport = "none"
+
+[operations.blossom_verify_descriptor_bytes.implementation]
+rust_modules = [
+ "crates/blossom/src/descriptor.rs",
+ "crates/blossom/src/hash.rs",
+]
+rust_types = [
+ "radroots_blossom::RadrootsBlossomApprovedDescriptor",
+ "radroots_blossom::RadrootsBlossomByteCommitment",
+ "radroots_blossom::RadrootsBlossomByteVerifiedDescriptor",
+ "radroots_blossom::RadrootsBlossomMediaType",
+]
+
+[operations.blossom_verify_descriptor_bytes.conformance]
+vector = "contracts/conformance/vectors/blossom/hash_path_and_descriptor.v1.json"
[operations.profile_build_draft]
domain = "profile"
diff --git a/contracts/version.toml b/contracts/version.toml
@@ -13,6 +13,7 @@ major_on = [
]
minor_on = [
"add_exported_type",
+ "add_public_foundation_crate",
"add_optional_field",
"add_enum_variant",
"add_conformance_vector",
diff --git a/crates/blossom/Cargo.toml b/crates/blossom/Cargo.toml
@@ -0,0 +1,31 @@
+[package]
+name = "radroots_blossom"
+publish = ["crates-io"]
+version = "0.1.0-alpha.2"
+edition.workspace = true
+authors = ["Tyson Lupul <tyson@radroots.org>"]
+rust-version.workspace = true
+license.workspace = true
+description = "Portable Blossom protocol primitives for Radroots"
+repository.workspace = true
+homepage.workspace = true
+documentation = "https://docs.rs/radroots_blossom"
+readme = "README"
+
+[features]
+default = ["serde"]
+serde = ["dep:serde"]
+std = ["serde?/std", "sha2/std", "url_nostd/std"]
+
+[dependencies]
+mediatype = { workspace = true }
+serde = { workspace = true, optional = true }
+sha2 = { workspace = true }
+url_nostd = { workspace = true }
+
+[dev-dependencies]
+hex = { workspace = true }
+serde_json = { workspace = true, features = ["std"] }
+
+[lints.rust]
+unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] }
diff --git a/crates/blossom/README b/crates/blossom/README
@@ -0,0 +1,19 @@
+# radroots_blossom
+
+`radroots_blossom` provides portable, runtime-independent primitives for
+Blossom blob hashes, root hash paths, blob URLs, BUD-02 descriptors, and
+Radroots-approved byte verification.
+
+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
+stricter Radroots reference policy: public HTTP descriptors remain parseable,
+but only HTTPS and loopback HTTP references can advance to an approved state.
+The byte-verified descriptor state proves local descriptor-to-byte agreement;
+it is not an upload receipt, and HTTP-capable runtimes must gate successful
+BUD-02 upload completion separately.
+
+Protocol behavior is pinned to Blossom commit
+`b5bd2801d1763aa635fc8fea7a76597e0eb18990`:
+
+- BUD-01: <https://github.com/hzrd149/blossom/blob/b5bd2801d1763aa635fc8fea7a76597e0eb18990/buds/01.md>
+- BUD-02: <https://github.com/hzrd149/blossom/blob/b5bd2801d1763aa635fc8fea7a76597e0eb18990/buds/02.md>
diff --git a/crates/blossom/src/descriptor.rs b/crates/blossom/src/descriptor.rs
@@ -0,0 +1,493 @@
+#[cfg(feature = "serde")]
+use alloc::string::String;
+use alloc::{collections::BTreeMap, vec::Vec};
+use core::{fmt, str::FromStr};
+use mediatype::{MediaTypeBuf, ReadParams};
+
+use crate::{
+ RadrootsBlossomApprovedBlobUrl, RadrootsBlossomBlobUrl, RadrootsBlossomError,
+ RadrootsBlossomSha256,
+};
+
+const _: () = assert!(usize::BITS <= u64::BITS);
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub struct RadrootsBlossomMediaType(MediaTypeBuf);
+
+impl RadrootsBlossomMediaType {
+ pub fn parse(value: &str) -> Result<Self, RadrootsBlossomError> {
+ let parsed =
+ MediaTypeBuf::from_str(value).map_err(|_| RadrootsBlossomError::InvalidMediaType)?;
+ if parsed.as_str() != value || parsed.ty().as_str() == "*" || parsed.subty().as_str() == "*"
+ {
+ return Err(RadrootsBlossomError::InvalidMediaType);
+ }
+ let canonical = parsed.canonicalize();
+ let mut unique_params = BTreeMap::new();
+ for (name, value) in canonical.params() {
+ if unique_params.insert(name, value).is_some() {
+ return Err(RadrootsBlossomError::InvalidMediaType);
+ }
+ }
+ let ordered_params = unique_params.into_iter().collect::<Vec<_>>();
+ Ok(Self(MediaTypeBuf::from_parts(
+ canonical.ty(),
+ canonical.subty(),
+ canonical.suffix(),
+ &ordered_params,
+ )))
+ }
+
+ pub fn as_str(&self) -> &str {
+ self.0.as_str()
+ }
+}
+
+impl fmt::Display for RadrootsBlossomMediaType {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(self.as_str())
+ }
+}
+
+impl FromStr for RadrootsBlossomMediaType {
+ type Err = RadrootsBlossomError;
+
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ Self::parse(value)
+ }
+}
+
+#[cfg(feature = "serde")]
+impl serde::Serialize for RadrootsBlossomMediaType {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(self.as_str())
+ }
+}
+
+#[cfg(feature = "serde")]
+impl<'de> serde::Deserialize<'de> for RadrootsBlossomMediaType {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let value = String::deserialize(deserializer)?;
+ Self::parse(&value).map_err(serde::de::Error::custom)
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsBlossomBlobDescriptor {
+ url: RadrootsBlossomBlobUrl,
+ sha256: RadrootsBlossomSha256,
+ size: u64,
+ media_type: RadrootsBlossomMediaType,
+ uploaded: u64,
+}
+
+impl RadrootsBlossomBlobDescriptor {
+ pub fn new(
+ url: RadrootsBlossomBlobUrl,
+ sha256: RadrootsBlossomSha256,
+ size: u64,
+ media_type: RadrootsBlossomMediaType,
+ uploaded: u64,
+ ) -> Result<Self, RadrootsBlossomError> {
+ if url.hash_path().extension().is_none() {
+ return Err(RadrootsBlossomError::DescriptorExtensionRequired);
+ }
+ if url.hash_path().hash() != sha256 {
+ return Err(RadrootsBlossomError::DescriptorHashMismatch);
+ }
+ Ok(Self {
+ url,
+ sha256,
+ size,
+ media_type,
+ uploaded,
+ })
+ }
+
+ pub fn url(&self) -> &RadrootsBlossomBlobUrl {
+ &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 uploaded(&self) -> u64 {
+ self.uploaded
+ }
+
+ pub fn approve_reference(
+ self,
+ ) -> Result<RadrootsBlossomApprovedDescriptor, RadrootsBlossomError> {
+ let approved_url = self.url.clone().approve()?;
+ Ok(RadrootsBlossomApprovedDescriptor {
+ descriptor: self,
+ approved_url,
+ })
+ }
+}
+
+#[cfg(feature = "serde")]
+#[derive(serde::Serialize)]
+struct DescriptorRef<'a> {
+ url: &'a RadrootsBlossomBlobUrl,
+ sha256: RadrootsBlossomSha256,
+ size: u64,
+ #[serde(rename = "type")]
+ media_type: &'a RadrootsBlossomMediaType,
+ uploaded: u64,
+}
+
+#[cfg(feature = "serde")]
+#[derive(serde::Deserialize)]
+struct DescriptorWire {
+ url: RadrootsBlossomBlobUrl,
+ sha256: RadrootsBlossomSha256,
+ size: u64,
+ #[serde(rename = "type")]
+ media_type: RadrootsBlossomMediaType,
+ uploaded: u64,
+}
+
+#[cfg(feature = "serde")]
+impl serde::Serialize for RadrootsBlossomBlobDescriptor {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ DescriptorRef {
+ url: &self.url,
+ sha256: self.sha256,
+ size: self.size,
+ media_type: &self.media_type,
+ uploaded: self.uploaded,
+ }
+ .serialize(serializer)
+ }
+}
+
+#[cfg(feature = "serde")]
+impl<'de> serde::Deserialize<'de> for RadrootsBlossomBlobDescriptor {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let wire = DescriptorWire::deserialize(deserializer)?;
+ Self::new(
+ wire.url,
+ wire.sha256,
+ wire.size,
+ wire.media_type,
+ wire.uploaded,
+ )
+ .map_err(serde::de::Error::custom)
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsBlossomApprovedDescriptor {
+ descriptor: RadrootsBlossomBlobDescriptor,
+ approved_url: RadrootsBlossomApprovedBlobUrl,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsBlossomByteCommitment {
+ sha256: RadrootsBlossomSha256,
+ size: u64,
+ media_type: RadrootsBlossomMediaType,
+}
+
+impl RadrootsBlossomByteCommitment {
+ pub fn from_bytes(bytes: &[u8], media_type: RadrootsBlossomMediaType) -> Self {
+ Self {
+ sha256: RadrootsBlossomSha256::digest(bytes),
+ size: bytes.len() as u64,
+ media_type,
+ }
+ }
+
+ 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
+ }
+}
+
+impl RadrootsBlossomApprovedDescriptor {
+ pub fn descriptor(&self) -> &RadrootsBlossomBlobDescriptor {
+ &self.descriptor
+ }
+
+ pub fn url(&self) -> &RadrootsBlossomApprovedBlobUrl {
+ &self.approved_url
+ }
+
+ pub fn into_descriptor(self) -> RadrootsBlossomBlobDescriptor {
+ self.descriptor
+ }
+
+ pub fn verify_bytes(
+ self,
+ bytes: &[u8],
+ approved_media_type: &RadrootsBlossomMediaType,
+ ) -> Result<RadrootsBlossomByteVerifiedDescriptor, RadrootsBlossomError> {
+ let commitment =
+ RadrootsBlossomByteCommitment::from_bytes(bytes, approved_media_type.clone());
+ self.verify_commitment(&commitment)
+ }
+
+ pub fn verify_commitment(
+ self,
+ commitment: &RadrootsBlossomByteCommitment,
+ ) -> Result<RadrootsBlossomByteVerifiedDescriptor, RadrootsBlossomError> {
+ if self.descriptor.size != commitment.size {
+ return Err(RadrootsBlossomError::BlobSizeMismatch {
+ expected: self.descriptor.size,
+ actual: commitment.size,
+ });
+ }
+ if self.descriptor.media_type != commitment.media_type {
+ return Err(RadrootsBlossomError::BlobMediaTypeMismatch);
+ }
+ if self.descriptor.sha256 != commitment.sha256 {
+ return Err(RadrootsBlossomError::BlobHashMismatch);
+ }
+ Ok(RadrootsBlossomByteVerifiedDescriptor(self))
+ }
+}
+
+/// An approved descriptor whose hash, size, and media type match supplied bytes.
+///
+/// This state does not attest that a network upload occurred.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsBlossomByteVerifiedDescriptor(RadrootsBlossomApprovedDescriptor);
+
+impl RadrootsBlossomByteVerifiedDescriptor {
+ pub fn descriptor(&self) -> &RadrootsBlossomBlobDescriptor {
+ self.0.descriptor()
+ }
+
+ pub fn url(&self) -> &RadrootsBlossomApprovedBlobUrl {
+ self.0.url()
+ }
+
+ pub const fn sha256(&self) -> RadrootsBlossomSha256 {
+ self.0.descriptor.sha256
+ }
+
+ pub const fn size(&self) -> u64 {
+ self.0.descriptor.size
+ }
+
+ pub fn media_type(&self) -> &RadrootsBlossomMediaType {
+ &self.0.descriptor.media_type
+ }
+
+ pub fn into_descriptor(self) -> RadrootsBlossomBlobDescriptor {
+ self.0.into_descriptor()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use alloc::{format, string::ToString};
+
+ const HELLO_HASH: &str = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
+
+ fn descriptor(origin: &str, bytes: &[u8], media_type: &str) -> RadrootsBlossomBlobDescriptor {
+ let hash = RadrootsBlossomSha256::digest(bytes);
+ RadrootsBlossomBlobDescriptor::new(
+ RadrootsBlossomBlobUrl::parse(&format!("{origin}/{hash}.txt")).unwrap(),
+ hash,
+ u64::try_from(bytes.len()).unwrap(),
+ RadrootsBlossomMediaType::parse(media_type).unwrap(),
+ 1_725_105_921,
+ )
+ .unwrap()
+ }
+
+ #[test]
+ fn media_type_canonicalizes_parameters_and_compares_case_insensitively() {
+ let lower =
+ RadrootsBlossomMediaType::parse("image/svg+xml; profile=web; charset=UTF-8").unwrap();
+ let upper = RadrootsBlossomMediaType::from_str("IMAGE/SVG+XML; CHARSET=UTF-8; PROFILE=web")
+ .unwrap();
+ assert_eq!(lower, upper);
+ assert_eq!(lower.as_str(), "image/svg+xml; charset=UTF-8; profile=web");
+ assert_eq!(upper.as_str(), lower.as_str());
+ assert_eq!(lower.to_string(), lower.as_str());
+ let json = serde_json::to_string(&lower).unwrap();
+ assert_eq!(
+ serde_json::from_str::<RadrootsBlossomMediaType>(&json).unwrap(),
+ lower
+ );
+ }
+
+ #[test]
+ fn media_type_rejects_invalid_values_and_types() {
+ for value in [
+ "",
+ "image",
+ "image/",
+ "*/*",
+ "image/*",
+ "image/png; profile=a; PROFILE=b",
+ " image/png",
+ "image/png ",
+ "image/png\n",
+ ] {
+ assert_eq!(
+ RadrootsBlossomMediaType::parse(value),
+ Err(RadrootsBlossomError::InvalidMediaType)
+ );
+ }
+ assert!(serde_json::from_str::<RadrootsBlossomMediaType>("42").is_err());
+ }
+
+ #[test]
+ fn descriptor_requires_extension_and_matching_url_hash() {
+ let hash = RadrootsBlossomSha256::from_hex(HELLO_HASH).unwrap();
+ let media_type = RadrootsBlossomMediaType::parse("text/plain").unwrap();
+ let no_extension =
+ RadrootsBlossomBlobUrl::parse(&format!("https://cdn.example.com/{HELLO_HASH}"))
+ .unwrap();
+ assert_eq!(
+ RadrootsBlossomBlobDescriptor::new(no_extension, hash, 5, media_type.clone(), 1),
+ Err(RadrootsBlossomError::DescriptorExtensionRequired)
+ );
+ let wrong_hash = RadrootsBlossomSha256::digest(b"wrong");
+ let url =
+ RadrootsBlossomBlobUrl::parse(&format!("https://cdn.example.com/{HELLO_HASH}.txt"))
+ .unwrap();
+ assert_eq!(
+ RadrootsBlossomBlobDescriptor::new(url, wrong_hash, 5, media_type, 1),
+ Err(RadrootsBlossomError::DescriptorHashMismatch)
+ );
+ }
+
+ #[test]
+ fn descriptor_serde_roundtrip_tolerates_extension_fields() {
+ let raw = format!(
+ r#"{{"url":"https://cdn.example.com/{HELLO_HASH}.txt","sha256":"{HELLO_HASH}","size":5,"type":"text/plain","uploaded":1725105921,"magnet":"magnet:?xt=urn:test"}}"#
+ );
+ let parsed: RadrootsBlossomBlobDescriptor = serde_json::from_str(&raw).unwrap();
+ assert_eq!(parsed.url().hash_path().hash().to_string(), HELLO_HASH);
+ assert_eq!(parsed.sha256().to_string(), HELLO_HASH);
+ assert_eq!(parsed.size(), 5);
+ assert_eq!(parsed.media_type().as_str(), "text/plain");
+ assert_eq!(parsed.uploaded(), 1_725_105_921);
+ let encoded = serde_json::to_value(&parsed).unwrap();
+ assert_eq!(encoded["type"], "text/plain");
+ assert!(encoded.get("magnet").is_none());
+ }
+
+ #[test]
+ fn descriptor_deserialize_revalidates_invariants() {
+ let wrong_hash = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
+ let raw = format!(
+ r#"{{"url":"https://cdn.example.com/{HELLO_HASH}.txt","sha256":"{wrong_hash}","size":5,"type":"text/plain","uploaded":1}}"#
+ );
+ assert!(serde_json::from_str::<RadrootsBlossomBlobDescriptor>(&raw).is_err());
+ }
+
+ #[test]
+ fn byte_verified_descriptor_requires_approved_url_size_type_and_hash() {
+ let media_type = RadrootsBlossomMediaType::parse("text/plain").unwrap();
+ let commitment = RadrootsBlossomByteCommitment::from_bytes(b"hello", media_type.clone());
+ assert_eq!(commitment.sha256().to_string(), HELLO_HASH);
+ assert_eq!(commitment.size(), 5);
+ assert_eq!(commitment.media_type(), &media_type);
+ let approved = descriptor("https://cdn.example.com", b"hello", "text/plain")
+ .approve_reference()
+ .unwrap();
+ assert_eq!(
+ approved.url().as_str(),
+ approved.descriptor().url().as_str()
+ );
+ let verified = approved.clone().verify_commitment(&commitment).unwrap();
+ assert_eq!(verified.url().as_str(), approved.url().as_str());
+ assert_eq!(verified.descriptor(), approved.descriptor());
+ assert_eq!(verified.sha256().to_string(), HELLO_HASH);
+ assert_eq!(verified.size(), 5);
+ assert_eq!(verified.media_type(), &media_type);
+ assert_eq!(verified.clone().into_descriptor(), *approved.descriptor());
+ assert_eq!(approved.clone().into_descriptor(), *approved.descriptor());
+
+ assert_eq!(
+ descriptor("https://cdn.example.com", b"hello", "text/plain")
+ .approve_reference()
+ .unwrap()
+ .verify_bytes(b"hell", &media_type),
+ Err(RadrootsBlossomError::BlobSizeMismatch {
+ expected: 5,
+ actual: 4,
+ })
+ );
+ let image_type = RadrootsBlossomMediaType::parse("image/png").unwrap();
+ assert_eq!(
+ descriptor("https://cdn.example.com", b"hello", "text/plain")
+ .approve_reference()
+ .unwrap()
+ .verify_bytes(b"hello", &image_type),
+ Err(RadrootsBlossomError::BlobMediaTypeMismatch)
+ );
+ let hash_mismatch = RadrootsBlossomBlobDescriptor::new(
+ RadrootsBlossomBlobUrl::parse(&format!(
+ "https://cdn.example.com/{}.txt",
+ RadrootsBlossomSha256::digest(b"world")
+ ))
+ .unwrap(),
+ RadrootsBlossomSha256::digest(b"world"),
+ 5,
+ media_type.clone(),
+ 1,
+ )
+ .unwrap()
+ .approve_reference()
+ .unwrap();
+ assert_eq!(
+ hash_mismatch.verify_bytes(b"hello", &media_type),
+ Err(RadrootsBlossomError::BlobHashMismatch)
+ );
+ }
+
+ #[test]
+ fn empty_blob_can_be_verified_with_explicit_default_media_type() {
+ let media_type = RadrootsBlossomMediaType::parse("application/octet-stream").unwrap();
+ let verified = descriptor("http://localhost:3000", b"", "application/octet-stream")
+ .approve_reference()
+ .unwrap()
+ .verify_bytes(b"", &media_type)
+ .unwrap();
+ assert_eq!(verified.size(), 0);
+ }
+
+ #[test]
+ fn public_http_descriptor_cannot_advance_to_approved() {
+ assert_eq!(
+ descriptor("http://cdn.example.com", b"hello", "text/plain").approve_reference(),
+ Err(RadrootsBlossomError::InsecureBlobUrl)
+ );
+ }
+}
diff --git a/crates/blossom/src/error.rs b/crates/blossom/src/error.rs
@@ -0,0 +1,118 @@
+use core::fmt;
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum RadrootsBlossomError {
+ InvalidSha256,
+ InvalidFileExtension,
+ InvalidHashPath,
+ InvalidBlobUrl,
+ UnsupportedBlobUrlScheme,
+ BlobUrlCredentialsForbidden,
+ BlobUrlQueryForbidden,
+ BlobUrlFragmentForbidden,
+ InsecureBlobUrl,
+ DescriptorExtensionRequired,
+ DescriptorHashMismatch,
+ InvalidMediaType,
+ BlobHashMismatch,
+ BlobSizeMismatch { expected: u64, actual: u64 },
+ BlobMediaTypeMismatch,
+}
+
+impl RadrootsBlossomError {
+ pub const fn code(&self) -> &'static str {
+ match self {
+ Self::InvalidSha256 => "invalid_sha256",
+ Self::InvalidFileExtension => "invalid_file_extension",
+ Self::InvalidHashPath => "invalid_hash_path",
+ Self::InvalidBlobUrl => "invalid_blob_url",
+ Self::UnsupportedBlobUrlScheme => "unsupported_blob_url_scheme",
+ Self::BlobUrlCredentialsForbidden => "blob_url_credentials_forbidden",
+ Self::BlobUrlQueryForbidden => "blob_url_query_forbidden",
+ Self::BlobUrlFragmentForbidden => "blob_url_fragment_forbidden",
+ Self::InsecureBlobUrl => "insecure_blob_url",
+ Self::DescriptorExtensionRequired => "descriptor_extension_required",
+ Self::DescriptorHashMismatch => "descriptor_hash_mismatch",
+ Self::InvalidMediaType => "invalid_media_type",
+ Self::BlobHashMismatch => "blob_hash_mismatch",
+ Self::BlobSizeMismatch { .. } => "blob_size_mismatch",
+ Self::BlobMediaTypeMismatch => "blob_media_type_mismatch",
+ }
+ }
+}
+
+impl fmt::Display for RadrootsBlossomError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::InvalidSha256 => {
+ f.write_str("sha256 must be 64 lowercase hexadecimal characters")
+ }
+ Self::InvalidFileExtension => f.write_str("invalid Blossom file extension"),
+ Self::InvalidHashPath => f.write_str("invalid Blossom root hash path"),
+ Self::InvalidBlobUrl => f.write_str("invalid Blossom blob URL"),
+ Self::UnsupportedBlobUrlScheme => {
+ f.write_str("Blossom blob URL scheme must be http or https")
+ }
+ Self::BlobUrlCredentialsForbidden => {
+ f.write_str("Blossom blob URL credentials are forbidden")
+ }
+ Self::BlobUrlQueryForbidden => f.write_str("Blossom blob URL query is forbidden"),
+ Self::BlobUrlFragmentForbidden => f.write_str("Blossom blob URL fragment is forbidden"),
+ Self::InsecureBlobUrl => {
+ f.write_str("Radroots blob references require HTTPS or loopback HTTP")
+ }
+ Self::DescriptorExtensionRequired => {
+ f.write_str("BUD-02 descriptor URL requires a file extension")
+ }
+ Self::DescriptorHashMismatch => {
+ f.write_str("descriptor URL hash does not match descriptor sha256")
+ }
+ Self::InvalidMediaType => f.write_str("invalid media type"),
+ Self::BlobHashMismatch => f.write_str("blob bytes do not match descriptor sha256"),
+ Self::BlobSizeMismatch { expected, actual } => {
+ write!(f, "blob size mismatch: expected {expected}, got {actual}")
+ }
+ Self::BlobMediaTypeMismatch => {
+ f.write_str("descriptor media type does not match the approved media type")
+ }
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for RadrootsBlossomError {}
+
+#[cfg(test)]
+mod tests {
+ use super::RadrootsBlossomError;
+ use alloc::format;
+
+ #[test]
+ fn error_codes_and_messages_are_stable() {
+ let errors = [
+ RadrootsBlossomError::InvalidSha256,
+ RadrootsBlossomError::InvalidFileExtension,
+ RadrootsBlossomError::InvalidHashPath,
+ RadrootsBlossomError::InvalidBlobUrl,
+ RadrootsBlossomError::UnsupportedBlobUrlScheme,
+ RadrootsBlossomError::BlobUrlCredentialsForbidden,
+ RadrootsBlossomError::BlobUrlQueryForbidden,
+ RadrootsBlossomError::BlobUrlFragmentForbidden,
+ RadrootsBlossomError::InsecureBlobUrl,
+ RadrootsBlossomError::DescriptorExtensionRequired,
+ RadrootsBlossomError::DescriptorHashMismatch,
+ RadrootsBlossomError::InvalidMediaType,
+ RadrootsBlossomError::BlobHashMismatch,
+ RadrootsBlossomError::BlobSizeMismatch {
+ expected: 1,
+ actual: 2,
+ },
+ RadrootsBlossomError::BlobMediaTypeMismatch,
+ ];
+ for error in errors {
+ assert!(!error.code().is_empty());
+ assert!(!format!("{error}").is_empty());
+ }
+ }
+}
diff --git a/crates/blossom/src/hash.rs b/crates/blossom/src/hash.rs
@@ -0,0 +1,354 @@
+use alloc::string::{String, ToString};
+use core::{fmt, str::FromStr};
+use sha2::{Digest, Sha256};
+
+use crate::RadrootsBlossomError;
+
+const SHA256_BYTES: usize = 32;
+const SHA256_HEX_LENGTH: usize = SHA256_BYTES * 2;
+const LOWER_HEX: &[u8; 16] = b"0123456789abcdef";
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct RadrootsBlossomSha256([u8; SHA256_BYTES]);
+
+impl RadrootsBlossomSha256 {
+ pub fn digest(bytes: &[u8]) -> Self {
+ let digest = Sha256::digest(bytes);
+ let mut value = [0_u8; SHA256_BYTES];
+ value.copy_from_slice(&digest);
+ Self(value)
+ }
+
+ pub fn from_hex(value: &str) -> Result<Self, RadrootsBlossomError> {
+ if value.len() != SHA256_HEX_LENGTH {
+ return Err(RadrootsBlossomError::InvalidSha256);
+ }
+
+ let bytes = value.as_bytes();
+ let mut decoded = [0_u8; SHA256_BYTES];
+ for (index, output) in decoded.iter_mut().enumerate() {
+ let high = decode_nibble(bytes[index * 2])?;
+ let low = decode_nibble(bytes[index * 2 + 1])?;
+ *output = (high << 4) | low;
+ }
+ Ok(Self(decoded))
+ }
+
+ pub const fn as_bytes(&self) -> &[u8; SHA256_BYTES] {
+ &self.0
+ }
+
+ pub fn to_hex(self) -> String {
+ let mut output = String::with_capacity(SHA256_HEX_LENGTH);
+ for byte in self.0 {
+ output.push(char::from(LOWER_HEX[usize::from(byte >> 4)]));
+ output.push(char::from(LOWER_HEX[usize::from(byte & 0x0f)]));
+ }
+ output
+ }
+}
+
+impl fmt::Display for RadrootsBlossomSha256 {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(&self.to_hex())
+ }
+}
+
+impl FromStr for RadrootsBlossomSha256 {
+ type Err = RadrootsBlossomError;
+
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ Self::from_hex(value)
+ }
+}
+
+#[cfg(feature = "serde")]
+impl serde::Serialize for RadrootsBlossomSha256 {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(&self.to_hex())
+ }
+}
+
+#[cfg(feature = "serde")]
+impl<'de> serde::Deserialize<'de> for RadrootsBlossomSha256 {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let value = String::deserialize(deserializer)?;
+ Self::from_hex(&value).map_err(serde::de::Error::custom)
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct RadrootsBlossomFileExtension(String);
+
+impl RadrootsBlossomFileExtension {
+ pub fn parse(value: &str) -> Result<Self, RadrootsBlossomError> {
+ if value.is_empty()
+ || value.split('.').any(str::is_empty)
+ || !value
+ .bytes()
+ .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
+ {
+ return Err(RadrootsBlossomError::InvalidFileExtension);
+ }
+ Ok(Self(value.to_string()))
+ }
+
+ pub fn as_str(&self) -> &str {
+ &self.0
+ }
+}
+
+impl fmt::Display for RadrootsBlossomFileExtension {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(self.as_str())
+ }
+}
+
+impl FromStr for RadrootsBlossomFileExtension {
+ type Err = RadrootsBlossomError;
+
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ Self::parse(value)
+ }
+}
+
+#[cfg(feature = "serde")]
+impl serde::Serialize for RadrootsBlossomFileExtension {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(self.as_str())
+ }
+}
+
+#[cfg(feature = "serde")]
+impl<'de> serde::Deserialize<'de> for RadrootsBlossomFileExtension {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let value = String::deserialize(deserializer)?;
+ Self::parse(&value).map_err(serde::de::Error::custom)
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct RadrootsBlossomHashPath {
+ hash: RadrootsBlossomSha256,
+ extension: Option<RadrootsBlossomFileExtension>,
+}
+
+impl RadrootsBlossomHashPath {
+ pub fn new(
+ hash: RadrootsBlossomSha256,
+ extension: Option<RadrootsBlossomFileExtension>,
+ ) -> Self {
+ Self { hash, extension }
+ }
+
+ pub fn parse(value: &str) -> Result<Self, RadrootsBlossomError> {
+ let segment = value
+ .strip_prefix('/')
+ .ok_or(RadrootsBlossomError::InvalidHashPath)?;
+ if segment.contains('/')
+ || segment.contains('\\')
+ || segment.contains('%')
+ || segment.contains('?')
+ || segment.contains('#')
+ {
+ return Err(RadrootsBlossomError::InvalidHashPath);
+ }
+
+ let hash = segment
+ .get(..SHA256_HEX_LENGTH)
+ .ok_or(RadrootsBlossomError::InvalidHashPath)
+ .and_then(RadrootsBlossomSha256::from_hex)?;
+ let suffix = &segment[SHA256_HEX_LENGTH..];
+ let extension = if suffix.is_empty() {
+ None
+ } else {
+ let value = suffix
+ .strip_prefix('.')
+ .ok_or(RadrootsBlossomError::InvalidHashPath)?;
+ Some(RadrootsBlossomFileExtension::parse(value)?)
+ };
+ Ok(Self::new(hash, extension))
+ }
+
+ pub const fn hash(&self) -> RadrootsBlossomSha256 {
+ self.hash
+ }
+
+ pub fn extension(&self) -> Option<&RadrootsBlossomFileExtension> {
+ self.extension.as_ref()
+ }
+
+ pub fn to_path(&self) -> String {
+ let mut value = String::with_capacity(
+ 1 + SHA256_HEX_LENGTH
+ + self
+ .extension
+ .as_ref()
+ .map_or(0, |extension| 1 + extension.as_str().len()),
+ );
+ value.push('/');
+ value.push_str(&self.hash.to_hex());
+ if let Some(extension) = &self.extension {
+ value.push('.');
+ value.push_str(extension.as_str());
+ }
+ value
+ }
+}
+
+impl fmt::Display for RadrootsBlossomHashPath {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(&self.to_path())
+ }
+}
+
+impl FromStr for RadrootsBlossomHashPath {
+ type Err = RadrootsBlossomError;
+
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ Self::parse(value)
+ }
+}
+
+#[cfg(feature = "serde")]
+impl serde::Serialize for RadrootsBlossomHashPath {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(&self.to_path())
+ }
+}
+
+#[cfg(feature = "serde")]
+impl<'de> serde::Deserialize<'de> for RadrootsBlossomHashPath {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let value = String::deserialize(deserializer)?;
+ Self::parse(&value).map_err(serde::de::Error::custom)
+ }
+}
+
+const fn decode_nibble(byte: u8) -> Result<u8, RadrootsBlossomError> {
+ match byte {
+ b'0'..=b'9' => Ok(byte - b'0'),
+ b'a'..=b'f' => Ok(byte - b'a' + 10),
+ _ => Err(RadrootsBlossomError::InvalidSha256),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use alloc::format;
+
+ const EMPTY_SHA256: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
+
+ #[test]
+ fn hash_round_trips_known_digest_and_serde() {
+ let hash = RadrootsBlossomSha256::digest(b"");
+ assert_eq!(hash.to_string(), EMPTY_SHA256);
+ assert_eq!(hash.as_bytes().len(), SHA256_BYTES);
+ assert_eq!(RadrootsBlossomSha256::from_str(EMPTY_SHA256), Ok(hash));
+ let json = serde_json::to_string(&hash).unwrap();
+ assert_eq!(
+ serde_json::from_str::<RadrootsBlossomSha256>(&json).unwrap(),
+ hash
+ );
+ }
+
+ #[test]
+ fn hash_rejects_wrong_length_case_and_alphabet() {
+ for value in [
+ "",
+ "0",
+ &"a".repeat(63),
+ &"a".repeat(65),
+ &"A".repeat(64),
+ &"z".repeat(64),
+ ] {
+ assert_eq!(
+ RadrootsBlossomSha256::from_hex(value),
+ Err(RadrootsBlossomError::InvalidSha256)
+ );
+ }
+ assert!(serde_json::from_str::<RadrootsBlossomSha256>("12").is_err());
+ }
+
+ #[test]
+ fn extension_accepts_simple_and_compound_values() {
+ for value in ["png", "tar.gz", "x-custom_2", "PNG"] {
+ let extension = value.parse::<RadrootsBlossomFileExtension>().unwrap();
+ assert_eq!(extension.as_str(), value);
+ assert_eq!(extension.to_string(), value);
+ let json = serde_json::to_string(&extension).unwrap();
+ assert_eq!(
+ serde_json::from_str::<RadrootsBlossomFileExtension>(&json).unwrap(),
+ extension
+ );
+ }
+ }
+
+ #[test]
+ fn extension_rejects_invalid_values() {
+ for value in ["", ".png", "png.", "tar..gz", "a/b", "a b", "café"] {
+ assert_eq!(
+ RadrootsBlossomFileExtension::parse(value),
+ Err(RadrootsBlossomError::InvalidFileExtension)
+ );
+ }
+ assert!(serde_json::from_str::<RadrootsBlossomFileExtension>("false").is_err());
+ }
+
+ #[test]
+ fn hash_path_round_trips_with_and_without_extension() {
+ let bare = format!("/{EMPTY_SHA256}");
+ let parsed = RadrootsBlossomHashPath::parse(&bare).unwrap();
+ assert_eq!(parsed.hash().to_string(), EMPTY_SHA256);
+ assert_eq!(parsed.extension(), None);
+ assert_eq!(parsed.to_path(), bare);
+
+ let extended = format!("/{EMPTY_SHA256}.webp");
+ let parsed = RadrootsBlossomHashPath::from_str(&extended).unwrap();
+ assert_eq!(parsed.extension().unwrap().as_str(), "webp");
+ assert_eq!(parsed.to_string(), extended);
+ let json = serde_json::to_string(&parsed).unwrap();
+ assert_eq!(
+ serde_json::from_str::<RadrootsBlossomHashPath>(&json).unwrap(),
+ parsed
+ );
+ }
+
+ #[test]
+ fn hash_path_rejects_non_root_and_ambiguous_paths() {
+ for path in [
+ EMPTY_SHA256,
+ "/",
+ "/abc",
+ &format!("/{EMPTY_SHA256}/x"),
+ &format!("/{EMPTY_SHA256}%2epng"),
+ &format!("/{EMPTY_SHA256}.png?download=1"),
+ &format!("/{EMPTY_SHA256}.png#media"),
+ &format!("/{EMPTY_SHA256}\\x"),
+ &format!("/{EMPTY_SHA256}png"),
+ &format!("/{EMPTY_SHA256}."),
+ ] {
+ assert!(RadrootsBlossomHashPath::parse(path).is_err(), "{path}");
+ }
+ assert!(serde_json::from_str::<RadrootsBlossomHashPath>("null").is_err());
+ }
+}
diff --git a/crates/blossom/src/lib.rs b/crates/blossom/src/lib.rs
@@ -0,0 +1,20 @@
+#![cfg_attr(not(feature = "std"), no_std)]
+#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
+#![forbid(unsafe_code)]
+
+extern crate alloc;
+
+pub mod descriptor;
+pub mod error;
+pub mod hash;
+pub mod url;
+
+pub use descriptor::{
+ RadrootsBlossomApprovedDescriptor, RadrootsBlossomBlobDescriptor,
+ RadrootsBlossomByteCommitment, RadrootsBlossomByteVerifiedDescriptor, RadrootsBlossomMediaType,
+};
+pub use error::RadrootsBlossomError;
+pub use hash::{RadrootsBlossomFileExtension, RadrootsBlossomHashPath, RadrootsBlossomSha256};
+pub use url::{RadrootsBlossomApprovedBlobUrl, RadrootsBlossomBlobUrl};
+
+pub const RADROOTS_BLOSSOM_PROTOCOL_COMMIT: &str = "b5bd2801d1763aa635fc8fea7a76597e0eb18990";
diff --git a/crates/blossom/src/url.rs b/crates/blossom/src/url.rs
@@ -0,0 +1,380 @@
+#[cfg(feature = "serde")]
+use alloc::string::String;
+use alloc::string::ToString;
+use core::{fmt, str::FromStr};
+use url_nostd::{Host, Url};
+
+use crate::{RadrootsBlossomError, RadrootsBlossomHashPath};
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub struct RadrootsBlossomBlobUrl {
+ url: Url,
+ hash_path: RadrootsBlossomHashPath,
+}
+
+impl RadrootsBlossomBlobUrl {
+ pub fn parse(value: &str) -> Result<Self, RadrootsBlossomError> {
+ if !value.contains("://")
+ || value
+ .chars()
+ .any(|character| character.is_whitespace() || character.is_control())
+ {
+ return Err(RadrootsBlossomError::InvalidBlobUrl);
+ }
+ let url = Url::parse(value).map_err(|_| RadrootsBlossomError::InvalidBlobUrl)?;
+ if url.scheme() != "http" && url.scheme() != "https" {
+ return Err(RadrootsBlossomError::UnsupportedBlobUrlScheme);
+ }
+ if raw_authority(value).contains('@') {
+ return Err(RadrootsBlossomError::BlobUrlCredentialsForbidden);
+ }
+ if url.query().is_some() {
+ return Err(RadrootsBlossomError::BlobUrlQueryForbidden);
+ }
+ if url.fragment().is_some() {
+ return Err(RadrootsBlossomError::BlobUrlFragmentForbidden);
+ }
+ if value.contains('\\')
+ || value.contains('%')
+ || value.contains("/./")
+ || value.contains("/../")
+ {
+ return Err(RadrootsBlossomError::InvalidBlobUrl);
+ }
+ validate_authority(value, &url)?;
+ let hash_path = RadrootsBlossomHashPath::parse(url.path())?;
+ Ok(Self { url, hash_path })
+ }
+
+ pub fn as_str(&self) -> &str {
+ self.url.as_str()
+ }
+
+ pub fn scheme(&self) -> &str {
+ self.url.scheme()
+ }
+
+ pub fn host(&self) -> &str {
+ self.url
+ .host_str()
+ .expect("validated HTTP and HTTPS URLs always have a host")
+ }
+
+ pub fn port(&self) -> Option<u16> {
+ self.url.port()
+ }
+
+ pub fn hash_path(&self) -> &RadrootsBlossomHashPath {
+ &self.hash_path
+ }
+
+ pub fn is_https(&self) -> bool {
+ self.url.scheme() == "https"
+ }
+
+ pub fn is_loopback_http(&self) -> bool {
+ self.url.scheme() == "http" && self.url.host().is_some_and(host_is_loopback)
+ }
+
+ pub fn approve(self) -> Result<RadrootsBlossomApprovedBlobUrl, RadrootsBlossomError> {
+ if !self.is_https() && !self.is_loopback_http() {
+ return Err(RadrootsBlossomError::InsecureBlobUrl);
+ }
+ Ok(RadrootsBlossomApprovedBlobUrl(self))
+ }
+}
+
+impl fmt::Display for RadrootsBlossomBlobUrl {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(self.as_str())
+ }
+}
+
+impl FromStr for RadrootsBlossomBlobUrl {
+ type Err = RadrootsBlossomError;
+
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ Self::parse(value)
+ }
+}
+
+#[cfg(feature = "serde")]
+impl serde::Serialize for RadrootsBlossomBlobUrl {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(self.as_str())
+ }
+}
+
+#[cfg(feature = "serde")]
+impl<'de> serde::Deserialize<'de> for RadrootsBlossomBlobUrl {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let value = String::deserialize(deserializer)?;
+ Self::parse(&value).map_err(serde::de::Error::custom)
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub struct RadrootsBlossomApprovedBlobUrl(RadrootsBlossomBlobUrl);
+
+impl RadrootsBlossomApprovedBlobUrl {
+ pub fn as_blob_url(&self) -> &RadrootsBlossomBlobUrl {
+ &self.0
+ }
+
+ pub fn as_str(&self) -> &str {
+ self.0.as_str()
+ }
+
+ pub fn into_blob_url(self) -> RadrootsBlossomBlobUrl {
+ self.0
+ }
+}
+
+impl fmt::Display for RadrootsBlossomApprovedBlobUrl {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+fn host_is_loopback(host: Host<&str>) -> bool {
+ match host {
+ Host::Domain(domain) => {
+ domain == "localhost"
+ || domain.strip_suffix(".localhost").is_some_and(|prefix| {
+ !prefix.is_empty() && prefix.split('.').all(|label| !label.is_empty())
+ })
+ }
+ Host::Ipv4(address) => address.octets()[0] == 127,
+ Host::Ipv6(address) => address.segments() == [0, 0, 0, 0, 0, 0, 0, 1],
+ }
+}
+
+fn validate_authority(value: &str, url: &Url) -> Result<(), RadrootsBlossomError> {
+ let raw_host = raw_authority_host(value);
+ if let Some(port) = raw_authority_port(value) {
+ match port.parse::<u16>() {
+ Ok(1..) => {}
+ Ok(0) | Err(_) => return Err(RadrootsBlossomError::InvalidBlobUrl),
+ }
+ }
+ if let Some(Host::Ipv4(address)) = url.host()
+ && raw_host != address.to_string()
+ {
+ return Err(RadrootsBlossomError::InvalidBlobUrl);
+ }
+ Ok(())
+}
+
+fn raw_authority(value: &str) -> &str {
+ let (_, remainder) = value
+ .split_once("://")
+ .expect("blob URL parser requires an explicit scheme delimiter");
+ let authority_end = remainder.find(['/', '?', '#']).unwrap_or(remainder.len());
+ &remainder[..authority_end]
+}
+
+fn raw_authority_host(value: &str) -> &str {
+ let host_and_port = raw_authority(value);
+ if let Some(bracketed) = host_and_port.strip_prefix('[') {
+ return bracketed
+ .split_once(']')
+ .expect("validated bracketed URL host must close")
+ .0;
+ }
+ host_and_port
+ .rsplit_once(':')
+ .map_or(host_and_port, |(host, _)| host)
+}
+
+fn raw_authority_port(value: &str) -> Option<&str> {
+ let host_and_port = raw_authority(value);
+ if let Some(bracketed) = host_and_port.strip_prefix('[') {
+ let (_, suffix) = bracketed
+ .split_once(']')
+ .expect("validated bracketed URL host must close");
+ return suffix.strip_prefix(':');
+ }
+ host_and_port.rsplit_once(':').map(|(_, port)| port)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use alloc::{format, string::ToString};
+
+ const HASH: &str = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
+
+ fn url(origin: &str) -> String {
+ format!("{origin}/{HASH}.txt")
+ }
+
+ #[test]
+ fn https_reference_is_structural_and_approved() {
+ let parsed = RadrootsBlossomBlobUrl::parse(&url("https://cdn.example.com")).unwrap();
+ assert!(parsed.is_https());
+ assert!(!parsed.is_loopback_http());
+ assert_eq!(parsed.hash_path().hash().to_string(), HASH);
+ assert_eq!(parsed.hash_path().extension().unwrap().as_str(), "txt");
+ assert_eq!(parsed.scheme(), "https");
+ assert_eq!(parsed.host(), "cdn.example.com");
+ assert_eq!(parsed.port(), None);
+ assert_eq!(parsed.clone().approve().unwrap().as_str(), parsed.as_str());
+ assert_eq!(parsed.to_string(), url("https://cdn.example.com"));
+ }
+
+ #[test]
+ fn loopback_http_reference_set_is_approved() {
+ for origin in [
+ "http://localhost:3000",
+ "http://media.localhost",
+ "http://127.0.0.1",
+ "http://127.255.10.9:8080",
+ "http://[::1]:3000",
+ "http://[0:0:0:0:0:0:0:1]",
+ ] {
+ let parsed = RadrootsBlossomBlobUrl::parse(&url(origin)).unwrap();
+ assert!(parsed.is_loopback_http(), "{origin}");
+ let canonical = parsed.as_str().to_string();
+ let approved = parsed.approve().unwrap();
+ assert_eq!(approved.as_blob_url().as_str(), canonical);
+ assert_eq!(approved.clone().into_blob_url().as_str(), canonical);
+ assert_eq!(approved.to_string(), canonical);
+ }
+ }
+
+ #[test]
+ fn public_and_private_http_are_structural_but_not_approved() {
+ for origin in [
+ "http://cdn.example.com",
+ "http://localhost.example.com",
+ "http://192.168.1.2",
+ "http://10.0.0.2",
+ "http://[::]",
+ "http://.localhost",
+ "http://a..localhost",
+ ] {
+ let parsed = RadrootsBlossomBlobUrl::parse(&url(origin)).unwrap();
+ assert!(!parsed.is_https());
+ assert!(!parsed.is_loopback_http());
+ assert_eq!(
+ parsed.approve(),
+ Err(RadrootsBlossomError::InsecureBlobUrl),
+ "{origin}"
+ );
+ }
+ }
+
+ #[test]
+ fn blob_url_rejects_scheme_host_credentials_query_and_fragment() {
+ let cases = [
+ (
+ url("ftp://cdn.example.com"),
+ RadrootsBlossomError::UnsupportedBlobUrlScheme,
+ ),
+ (
+ format!("https://user@cdn.example.com/{HASH}.txt"),
+ RadrootsBlossomError::BlobUrlCredentialsForbidden,
+ ),
+ (
+ format!("https://:password@cdn.example.com/{HASH}.txt"),
+ RadrootsBlossomError::BlobUrlCredentialsForbidden,
+ ),
+ (
+ format!("https://@cdn.example.com/{HASH}.txt"),
+ RadrootsBlossomError::BlobUrlCredentialsForbidden,
+ ),
+ (
+ format!("https://:@cdn.example.com/{HASH}.txt"),
+ RadrootsBlossomError::BlobUrlCredentialsForbidden,
+ ),
+ (
+ format!("https://cdn.example.com/{HASH}.txt?a=1"),
+ RadrootsBlossomError::BlobUrlQueryForbidden,
+ ),
+ (
+ format!("https://cdn.example.com/{HASH}.txt#x"),
+ RadrootsBlossomError::BlobUrlFragmentForbidden,
+ ),
+ ];
+ for (value, expected) in cases {
+ assert_eq!(
+ RadrootsBlossomBlobUrl::parse(&value),
+ Err(expected),
+ "{value}"
+ );
+ }
+ }
+
+ #[test]
+ fn blob_url_rejects_non_root_encoded_and_traversal_paths() {
+ for value in [
+ format!("https://cdn.example.com/x/{HASH}.txt"),
+ format!("https://cdn.example.com/{HASH}.txt/x"),
+ format!("https://cdn.example.com/{HASH}%2etxt"),
+ format!("https://cdn.example.com/x/./../{HASH}.txt"),
+ format!("https://cdn.example.com/{HASH}/../{HASH}.txt"),
+ format!("https://cdn.example.com/{HASH}\\x"),
+ ] {
+ assert!(RadrootsBlossomBlobUrl::parse(&value).is_err(), "{value}");
+ }
+ }
+
+ #[test]
+ fn blob_url_serde_revalidates_structure() {
+ let parsed = RadrootsBlossomBlobUrl::parse(&url("https://cdn.example.com")).unwrap();
+ let json = serde_json::to_string(&parsed).unwrap();
+ assert_eq!(
+ serde_json::from_str::<RadrootsBlossomBlobUrl>(&json).unwrap(),
+ parsed
+ );
+ assert!(serde_json::from_str::<RadrootsBlossomBlobUrl>("false").is_err());
+ assert!(
+ serde_json::from_str::<RadrootsBlossomBlobUrl>(
+ "\"https://cdn.example.com/not-a-hash.png\""
+ )
+ .is_err()
+ );
+ }
+
+ #[test]
+ fn malformed_url_is_rejected() {
+ assert_eq!(
+ RadrootsBlossomBlobUrl::from_str("not a url"),
+ Err(RadrootsBlossomError::InvalidBlobUrl)
+ );
+ assert!(RadrootsBlossomBlobUrl::parse(&format!("https:///{HASH}.txt")).is_err());
+ assert!(RadrootsBlossomBlobUrl::parse(&url("https://cdn.example.com:0")).is_err());
+ assert!(RadrootsBlossomBlobUrl::parse(&url("https://cdn.example.com:00")).is_err());
+ assert!(RadrootsBlossomBlobUrl::parse(&url("https://cdn.example.com:")).is_err());
+ for value in [
+ format!(" https://cdn.example.com/{HASH}.txt"),
+ format!("https://cdn.example.com/{HASH}.txt\n"),
+ format!("https://cdn.example.com/{HASH}.txt\t"),
+ format!("https://cdn.example.com/{HASH}.txt\u{7f}"),
+ ] {
+ assert_eq!(
+ RadrootsBlossomBlobUrl::parse(&value),
+ Err(RadrootsBlossomError::InvalidBlobUrl),
+ "{value:?}"
+ );
+ }
+ for origin in [
+ "http://0177.0.0.1",
+ "http://0x7f.0.0.1",
+ "http://127.1",
+ "https://2130706433",
+ ] {
+ assert_eq!(
+ RadrootsBlossomBlobUrl::parse(&url(origin)),
+ Err(RadrootsBlossomError::InvalidBlobUrl),
+ "{origin}"
+ );
+ }
+ }
+}
diff --git a/crates/blossom/tests/conformance.rs b/crates/blossom/tests/conformance.rs
@@ -0,0 +1,387 @@
+use radroots_blossom::{
+ RadrootsBlossomBlobDescriptor, RadrootsBlossomBlobUrl, RadrootsBlossomHashPath,
+ RadrootsBlossomMediaType, RadrootsBlossomSha256,
+};
+use serde::Deserialize;
+use serde_json::Value;
+use std::{borrow::Cow, fs, path::Path};
+
+const PACKAGED_VECTORS: &str = include_str!("fixtures/hash_path_and_descriptor.v1.json");
+const WORKSPACE_VECTOR_PATH: &str =
+ "../../contracts/conformance/vectors/blossom/hash_path_and_descriptor.v1.json";
+const WORKSPACE_CONTRACT_MARKER_PATH: &str = "../../contracts/manifest.toml";
+
+#[derive(Debug, Deserialize)]
+struct Suite {
+ suite: String,
+ contract_version: String,
+ vectors: Vec<Vector>,
+}
+
+#[derive(Debug, Deserialize)]
+struct Vector {
+ id: String,
+ kind: String,
+ input: Value,
+ expected: Value,
+}
+
+#[test]
+fn checked_in_vectors_execute_against_public_api() {
+ let vectors = conformance_vectors();
+ let suite: Suite = serde_json::from_str(&vectors).expect("Blossom vectors must parse");
+ assert_eq!(suite.suite, "blossom_hash_path_and_descriptor");
+ assert_eq!(suite.contract_version, "0.1.0");
+ assert!(!suite.vectors.is_empty());
+
+ for vector in &suite.vectors {
+ execute(vector);
+ }
+}
+
+fn conformance_vectors() -> Cow<'static, str> {
+ let workspace_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(WORKSPACE_VECTOR_PATH);
+ match fs::read_to_string(&workspace_path) {
+ Ok(canonical) => {
+ assert_eq!(
+ canonical,
+ PACKAGED_VECTORS,
+ "packaged Blossom vectors must match {}",
+ workspace_path.display()
+ );
+ Cow::Owned(canonical)
+ }
+ Err(error)
+ if error.kind() == std::io::ErrorKind::NotFound
+ && !Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join(WORKSPACE_CONTRACT_MARKER_PATH)
+ .is_file() =>
+ {
+ Cow::Borrowed(PACKAGED_VECTORS)
+ }
+ Err(error) => panic!("failed to read {}: {error}", workspace_path.display()),
+ }
+}
+
+fn execute(vector: &Vector) {
+ match vector.kind.as_str() {
+ "blossom.sha256.digest" => sha256_digest(vector),
+ "blossom.sha256.parse.valid" => sha256_parse_valid(vector),
+ "blossom.sha256.parse.invalid" => sha256_parse_invalid(vector),
+ "blossom.hash_path.parse.valid" => hash_path_parse_valid(vector),
+ "blossom.hash_path.parse.invalid" => hash_path_parse_invalid(vector),
+ "blossom.blob_url.parse.valid" => blob_url_parse_valid(vector),
+ "blossom.blob_url.parse.invalid" => blob_url_parse_invalid(vector),
+ "blossom.reference_policy.valid" => reference_policy_valid(vector),
+ "blossom.reference_policy.invalid" => reference_policy_invalid(vector),
+ "blossom.media_type.parse.valid" => media_type_parse_valid(vector),
+ "blossom.media_type.parse.invalid" => media_type_parse_invalid(vector),
+ "blossom.descriptor.parse.valid" => descriptor_parse_valid(vector),
+ "blossom.descriptor.parse.invalid" => descriptor_parse_invalid(vector),
+ "blossom.descriptor.approve_reference.valid" => descriptor_approval_valid(vector),
+ "blossom.descriptor.approve_reference.invalid" => descriptor_approval_invalid(vector),
+ "blossom.descriptor.verify_bytes.valid" => descriptor_verify_valid(vector),
+ "blossom.descriptor.verify_bytes.invalid" => descriptor_verify_invalid(vector),
+ kind => panic!("{} uses unsupported vector kind {kind}", vector.id),
+ }
+}
+
+fn sha256_digest(vector: &Vector) {
+ let bytes = input_bytes(vector);
+ assert_eq!(
+ RadrootsBlossomSha256::digest(&bytes).to_string(),
+ expected_str(vector, "sha256"),
+ "{}",
+ vector.id
+ );
+}
+
+fn sha256_parse_valid(vector: &Vector) {
+ let parsed = RadrootsBlossomSha256::from_hex(input_str(vector, "sha256"))
+ .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
+ assert_eq!(
+ parsed.to_string(),
+ expected_str(vector, "sha256"),
+ "{}",
+ vector.id
+ );
+}
+
+fn sha256_parse_invalid(vector: &Vector) {
+ let error = RadrootsBlossomSha256::from_hex(input_str(vector, "sha256"))
+ .expect_err("invalid SHA-256 vector must fail");
+ assert_error(vector, error.code());
+}
+
+fn hash_path_parse_valid(vector: &Vector) {
+ let parsed = RadrootsBlossomHashPath::parse(input_str(vector, "path"))
+ .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
+ assert_eq!(
+ parsed.hash().to_string(),
+ expected_str(vector, "sha256"),
+ "{}",
+ vector.id
+ );
+ assert_eq!(
+ parsed.extension().map(|extension| extension.as_str()),
+ optional_expected_str(vector, "extension"),
+ "{}",
+ vector.id
+ );
+}
+
+fn hash_path_parse_invalid(vector: &Vector) {
+ let error = RadrootsBlossomHashPath::parse(input_str(vector, "path"))
+ .expect_err("invalid hash-path vector must fail");
+ assert_error(vector, error.code());
+}
+
+fn blob_url_parse_valid(vector: &Vector) {
+ let parsed = parse_blob_url(vector);
+ assert_eq!(
+ parsed.scheme(),
+ expected_str(vector, "scheme"),
+ "{}",
+ vector.id
+ );
+ assert_eq!(parsed.host(), expected_str(vector, "host"), "{}", vector.id);
+ assert_eq!(
+ parsed.port().map(u64::from),
+ vector.expected.get("port").and_then(Value::as_u64),
+ "{}",
+ vector.id
+ );
+ assert_eq!(
+ parsed.hash_path().hash().to_string(),
+ expected_str(vector, "sha256"),
+ "{}",
+ vector.id
+ );
+ assert_eq!(
+ parsed
+ .hash_path()
+ .extension()
+ .map(|extension| extension.as_str()),
+ optional_expected_str(vector, "extension"),
+ "{}",
+ vector.id
+ );
+}
+
+fn blob_url_parse_invalid(vector: &Vector) {
+ let error = RadrootsBlossomBlobUrl::parse(input_str(vector, "url"))
+ .expect_err("invalid blob-URL vector must fail");
+ assert_error(vector, error.code());
+}
+
+fn reference_policy_valid(vector: &Vector) {
+ let parsed = parse_blob_url(vector);
+ let transport = if parsed.is_https() {
+ "https"
+ } else if parsed.is_loopback_http() {
+ "loopback_http"
+ } else {
+ panic!("{} is not an approved reference", vector.id);
+ };
+ let approved = parsed
+ .approve()
+ .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
+ assert_eq!(vector.expected["approved"], true, "{}", vector.id);
+ assert_eq!(
+ transport,
+ expected_str(vector, "transport"),
+ "{}",
+ vector.id
+ );
+ assert!(!approved.as_str().is_empty());
+}
+
+fn reference_policy_invalid(vector: &Vector) {
+ let error = parse_blob_url(vector)
+ .approve()
+ .expect_err("insecure reference vector must fail");
+ assert_error(vector, error.code());
+}
+
+fn media_type_parse_valid(vector: &Vector) {
+ let parsed = RadrootsBlossomMediaType::parse(input_str(vector, "media_type"))
+ .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
+ assert_eq!(
+ parsed.as_str(),
+ expected_str(vector, "media_type"),
+ "{}",
+ vector.id
+ );
+}
+
+fn media_type_parse_invalid(vector: &Vector) {
+ let error = RadrootsBlossomMediaType::parse(input_str(vector, "media_type"))
+ .expect_err("invalid media-type vector must fail");
+ assert_error(vector, error.code());
+}
+
+fn descriptor_parse_valid(vector: &Vector) {
+ let descriptor = parse_descriptor(vector);
+ let actual = serde_json::to_value(descriptor).expect("descriptor must serialize");
+ assert_eq!(actual, vector.expected["descriptor"], "{}", vector.id);
+}
+
+fn descriptor_parse_invalid(vector: &Vector) {
+ let input = &vector.input["descriptor"];
+ serde_json::from_value::<RadrootsBlossomBlobDescriptor>(input.clone())
+ .expect_err("invalid descriptor vector must fail");
+ let code = classify_descriptor_input(input);
+ assert_error(vector, code);
+ if expected_error(vector) == "missing_descriptor_field" {
+ let field = expected_str(vector, "field");
+ assert!(input.get(field).is_none(), "{} retains {field}", vector.id);
+ }
+}
+
+fn descriptor_approval_valid(vector: &Vector) {
+ let approved = parse_descriptor(vector)
+ .approve_reference()
+ .unwrap_or_else(|error| panic!("{} approval failed: {error}", vector.id));
+ assert_eq!(
+ approved.url().as_str(),
+ expected_str(vector, "approved_url"),
+ "{}",
+ vector.id
+ );
+}
+
+fn descriptor_approval_invalid(vector: &Vector) {
+ let error = parse_descriptor(vector)
+ .approve_reference()
+ .expect_err("invalid descriptor approval vector must fail");
+ assert_error(vector, error.code());
+}
+
+fn descriptor_verify_valid(vector: &Vector) {
+ let bytes = input_bytes(vector);
+ let media_type = approved_media_type(vector);
+ let verified = parse_descriptor(vector)
+ .approve_reference()
+ .unwrap_or_else(|error| panic!("{} approval failed: {error}", vector.id))
+ .verify_bytes(&bytes, &media_type)
+ .unwrap_or_else(|error| panic!("{} verification failed: {error}", vector.id));
+ let expected = &vector.expected["verified"];
+ assert_eq!(
+ verified.sha256().to_string(),
+ expected["sha256"].as_str().expect("expected sha256"),
+ "{}",
+ vector.id
+ );
+ assert_eq!(
+ verified.size(),
+ expected["size"].as_u64().expect("expected size"),
+ "{}",
+ vector.id
+ );
+ assert_eq!(
+ verified.media_type().as_str(),
+ expected["media_type"]
+ .as_str()
+ .expect("expected media_type"),
+ "{}",
+ vector.id
+ );
+}
+
+fn descriptor_verify_invalid(vector: &Vector) {
+ let bytes = input_bytes(vector);
+ let media_type = approved_media_type(vector);
+ let error = parse_descriptor(vector)
+ .approve_reference()
+ .unwrap_or_else(|error| panic!("{} approval failed: {error}", vector.id))
+ .verify_bytes(&bytes, &media_type)
+ .expect_err("invalid descriptor verification vector must fail");
+ assert_error(vector, error.code());
+}
+
+fn parse_blob_url(vector: &Vector) -> RadrootsBlossomBlobUrl {
+ RadrootsBlossomBlobUrl::parse(input_str(vector, "url"))
+ .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id))
+}
+
+fn parse_descriptor(vector: &Vector) -> RadrootsBlossomBlobDescriptor {
+ serde_json::from_value(vector.input["descriptor"].clone())
+ .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id))
+}
+
+fn approved_media_type(vector: &Vector) -> RadrootsBlossomMediaType {
+ RadrootsBlossomMediaType::parse(input_str(vector, "approved_media_type"))
+ .unwrap_or_else(|error| panic!("{} media type failed: {error}", vector.id))
+}
+
+fn input_bytes(vector: &Vector) -> Vec<u8> {
+ hex::decode(input_str(vector, "bytes_hex"))
+ .unwrap_or_else(|error| panic!("{} bytes_hex failed: {error}", vector.id))
+}
+
+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 optional_expected_str<'a>(vector: &'a Vector, field: &str) -> Option<&'a str> {
+ vector.expected.get(field).and_then(Value::as_str)
+}
+
+fn expected_error(vector: &Vector) -> &str {
+ expected_str(vector, "error")
+}
+
+fn assert_error(vector: &Vector, actual: &str) {
+ assert_eq!(actual, expected_error(vector), "{}", vector.id);
+}
+
+fn classify_descriptor_input(input: &Value) -> &'static str {
+ for field in ["url", "sha256", "size", "type", "uploaded"] {
+ if input.get(field).is_none() {
+ return "missing_descriptor_field";
+ }
+ }
+
+ let url = match RadrootsBlossomBlobUrl::parse(
+ input["url"]
+ .as_str()
+ .expect("descriptor url must be a string"),
+ ) {
+ Ok(url) => url,
+ Err(error) => return error.code(),
+ };
+ let sha256 = match RadrootsBlossomSha256::from_hex(
+ input["sha256"]
+ .as_str()
+ .expect("descriptor sha256 must be a string"),
+ ) {
+ Ok(sha256) => sha256,
+ Err(error) => return error.code(),
+ };
+ let Some(size) = input["size"].as_u64() else {
+ return "invalid_descriptor_size";
+ };
+ let media_type = match RadrootsBlossomMediaType::parse(
+ input["type"]
+ .as_str()
+ .expect("descriptor type must be a string"),
+ ) {
+ Ok(media_type) => media_type,
+ Err(error) => return error.code(),
+ };
+ let Some(uploaded) = input["uploaded"].as_u64() else {
+ return "invalid_descriptor_uploaded";
+ };
+
+ RadrootsBlossomBlobDescriptor::new(url, sha256, size, media_type, uploaded)
+ .expect_err("invalid descriptor vector must fail structured construction")
+ .code()
+}
diff --git a/crates/blossom/tests/fixtures/hash_path_and_descriptor.v1.json b/crates/blossom/tests/fixtures/hash_path_and_descriptor.v1.json
@@ -0,0 +1,1273 @@
+{
+ "suite": "blossom_hash_path_and_descriptor",
+ "contract_version": "0.1.0",
+ "vectors": [
+ {
+ "id": "blossom_sha256_digest_empty_001",
+ "kind": "blossom.sha256.digest",
+ "input": {
+ "bytes_hex": ""
+ },
+ "expected": {
+ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ {
+ "id": "blossom_sha256_digest_abc_002",
+ "kind": "blossom.sha256.digest",
+ "input": {
+ "bytes_hex": "616263"
+ },
+ "expected": {
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
+ }
+ },
+ {
+ "id": "blossom_sha256_digest_binary_003",
+ "kind": "blossom.sha256.digest",
+ "input": {
+ "bytes_hex": "89504e470d0a1a0a726164726f6f7473"
+ },
+ "expected": {
+ "sha256": "a829dda89323e71a4575c1c45c52d65c3a0c87dd8ffcdab9ad12bbee57c00861"
+ }
+ },
+ {
+ "id": "blossom_sha256_parse_valid_004",
+ "kind": "blossom.sha256.parse.valid",
+ "input": {
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
+ },
+ "expected": {
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
+ }
+ },
+ {
+ "id": "blossom_sha256_parse_zero_valid_005",
+ "kind": "blossom.sha256.parse.valid",
+ "input": {
+ "sha256": "0000000000000000000000000000000000000000000000000000000000000000"
+ },
+ "expected": {
+ "sha256": "0000000000000000000000000000000000000000000000000000000000000000"
+ }
+ },
+ {
+ "id": "blossom_sha256_parse_short_invalid_006",
+ "kind": "blossom.sha256.parse.invalid",
+ "input": {
+ "sha256": "ba7816bf8f01cfea"
+ },
+ "expected": {
+ "error": "invalid_sha256"
+ }
+ },
+ {
+ "id": "blossom_sha256_parse_uppercase_invalid_007",
+ "kind": "blossom.sha256.parse.invalid",
+ "input": {
+ "sha256": "BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD"
+ },
+ "expected": {
+ "error": "invalid_sha256"
+ }
+ },
+ {
+ "id": "blossom_sha256_parse_non_hex_invalid_008",
+ "kind": "blossom.sha256.parse.invalid",
+ "input": {
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ag"
+ },
+ "expected": {
+ "error": "invalid_sha256"
+ }
+ },
+ {
+ "id": "blossom_hash_path_without_extension_009",
+ "kind": "blossom.hash_path.parse.valid",
+ "input": {
+ "path": "/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "expected": {
+ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "extension": null
+ }
+ },
+ {
+ "id": "blossom_hash_path_png_extension_010",
+ "kind": "blossom.hash_path.parse.valid",
+ "input": {
+ "path": "/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "extension": "png"
+ }
+ },
+ {
+ "id": "blossom_hash_path_multi_extension_011",
+ "kind": "blossom.hash_path.parse.valid",
+ "input": {
+ "path": "/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.tar.gz"
+ },
+ "expected": {
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "extension": "tar.gz"
+ }
+ },
+ {
+ "id": "blossom_hash_path_extension_case_preserved_012",
+ "kind": "blossom.hash_path.parse.valid",
+ "input": {
+ "path": "/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.Photo_JPG-2"
+ },
+ "expected": {
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "extension": "Photo_JPG-2"
+ }
+ },
+ {
+ "id": "blossom_hash_path_missing_root_slash_013",
+ "kind": "blossom.hash_path.parse.invalid",
+ "input": {
+ "path": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "invalid_hash_path"
+ }
+ },
+ {
+ "id": "blossom_hash_path_empty_014",
+ "kind": "blossom.hash_path.parse.invalid",
+ "input": {
+ "path": "/"
+ },
+ "expected": {
+ "error": "invalid_hash_path"
+ }
+ },
+ {
+ "id": "blossom_hash_path_extra_segment_015",
+ "kind": "blossom.hash_path.parse.invalid",
+ "input": {
+ "path": "/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad/png"
+ },
+ "expected": {
+ "error": "invalid_hash_path"
+ }
+ },
+ {
+ "id": "blossom_hash_path_uppercase_hash_016",
+ "kind": "blossom.hash_path.parse.invalid",
+ "input": {
+ "path": "/BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD.png"
+ },
+ "expected": {
+ "error": "invalid_sha256"
+ }
+ },
+ {
+ "id": "blossom_hash_path_empty_extension_017",
+ "kind": "blossom.hash_path.parse.invalid",
+ "input": {
+ "path": "/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad."
+ },
+ "expected": {
+ "error": "invalid_file_extension"
+ }
+ },
+ {
+ "id": "blossom_hash_path_empty_extension_component_018",
+ "kind": "blossom.hash_path.parse.invalid",
+ "input": {
+ "path": "/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.tar..gz"
+ },
+ "expected": {
+ "error": "invalid_file_extension"
+ }
+ },
+ {
+ "id": "blossom_hash_path_whitespace_extension_019",
+ "kind": "blossom.hash_path.parse.invalid",
+ "input": {
+ "path": "/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.p ng"
+ },
+ "expected": {
+ "error": "invalid_file_extension"
+ }
+ },
+ {
+ "id": "blossom_hash_path_percent_encoded_extension_020",
+ "kind": "blossom.hash_path.parse.invalid",
+ "input": {
+ "path": "/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.%70ng"
+ },
+ "expected": {
+ "error": "invalid_hash_path"
+ }
+ },
+ {
+ "id": "blossom_hash_path_query_forbidden_021",
+ "kind": "blossom.hash_path.parse.invalid",
+ "input": {
+ "path": "/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png?download=1"
+ },
+ "expected": {
+ "error": "invalid_hash_path"
+ }
+ },
+ {
+ "id": "blossom_blob_url_https_canonical_authority_022",
+ "kind": "blossom.blob_url.parse.valid",
+ "input": {
+ "url": "HTTPS://MEDIA.EXAMPLE/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.PNG"
+ },
+ "expected": {
+ "scheme": "https",
+ "host": "media.example",
+ "port": null,
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "extension": "PNG"
+ }
+ },
+ {
+ "id": "blossom_blob_url_public_http_structural_023",
+ "kind": "blossom.blob_url.parse.valid",
+ "input": {
+ "url": "http://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "scheme": "http",
+ "host": "media.example",
+ "port": null,
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "extension": "png"
+ }
+ },
+ {
+ "id": "blossom_blob_url_https_port_without_extension_024",
+ "kind": "blossom.blob_url.parse.valid",
+ "input": {
+ "url": "https://media.example:8443/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "expected": {
+ "scheme": "https",
+ "host": "media.example",
+ "port": 8443,
+ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "extension": null
+ }
+ },
+ {
+ "id": "blossom_blob_url_bracketed_ipv6_025",
+ "kind": "blossom.blob_url.parse.valid",
+ "input": {
+ "url": "http://[::1]:21100/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.bin"
+ },
+ "expected": {
+ "scheme": "http",
+ "host": "[::1]",
+ "port": 21100,
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "extension": "bin"
+ }
+ },
+ {
+ "id": "blossom_blob_url_relative_invalid_026",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "invalid_blob_url"
+ }
+ },
+ {
+ "id": "blossom_blob_url_unsupported_scheme_027",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "ftp://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "unsupported_blob_url_scheme"
+ }
+ },
+ {
+ "id": "blossom_blob_url_missing_authority_028",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://"
+ },
+ "expected": {
+ "error": "invalid_blob_url"
+ }
+ },
+ {
+ "id": "blossom_blob_url_credentials_forbidden_029",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://user:password@media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "blob_url_credentials_forbidden"
+ }
+ },
+ {
+ "id": "blossom_blob_url_empty_userinfo_forbidden_029b",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://@media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "blob_url_credentials_forbidden"
+ }
+ },
+ {
+ "id": "blossom_blob_url_empty_userinfo_separator_forbidden_029c",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://:@media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "blob_url_credentials_forbidden"
+ }
+ },
+ {
+ "id": "blossom_blob_url_query_forbidden_030",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png?download=1"
+ },
+ "expected": {
+ "error": "blob_url_query_forbidden"
+ }
+ },
+ {
+ "id": "blossom_blob_url_fragment_forbidden_031",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png#preview"
+ },
+ "expected": {
+ "error": "blob_url_fragment_forbidden"
+ }
+ },
+ {
+ "id": "blossom_blob_url_extra_path_segment_032",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://media.example/blobs/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "invalid_hash_path"
+ }
+ },
+ {
+ "id": "blossom_blob_url_port_out_of_range_033",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://media.example:65536/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "invalid_blob_url"
+ }
+ },
+ {
+ "id": "blossom_blob_url_uppercase_hash_034",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://media.example/BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD.png"
+ },
+ "expected": {
+ "error": "invalid_sha256"
+ }
+ },
+ {
+ "id": "blossom_blob_url_percent_encoded_extension_035",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.%70ng"
+ },
+ "expected": {
+ "error": "invalid_blob_url"
+ }
+ },
+ {
+ "id": "blossom_blob_url_unbracketed_ipv6_036",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "http://::1/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.bin"
+ },
+ "expected": {
+ "error": "invalid_blob_url"
+ }
+ },
+ {
+ "id": "blossom_blob_url_leading_whitespace_rejected_036b",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": " https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "invalid_blob_url"
+ }
+ },
+ {
+ "id": "blossom_blob_url_embedded_newline_rejected_036c",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://media.example/\nba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "invalid_blob_url"
+ }
+ },
+ {
+ "id": "blossom_blob_url_trailing_tab_rejected_036d",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png\t"
+ },
+ "expected": {
+ "error": "invalid_blob_url"
+ }
+ },
+ {
+ "id": "blossom_reference_https_public_approved_037",
+ "kind": "blossom.reference_policy.valid",
+ "input": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "approved": true,
+ "transport": "https"
+ }
+ },
+ {
+ "id": "blossom_reference_https_private_address_approved_038",
+ "kind": "blossom.reference_policy.valid",
+ "input": {
+ "url": "https://10.0.0.4/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "approved": true,
+ "transport": "https"
+ }
+ },
+ {
+ "id": "blossom_reference_http_localhost_approved_039",
+ "kind": "blossom.reference_policy.valid",
+ "input": {
+ "url": "http://LOCALHOST:21100/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "approved": true,
+ "transport": "loopback_http"
+ }
+ },
+ {
+ "id": "blossom_reference_http_localhost_subdomain_approved_040",
+ "kind": "blossom.reference_policy.valid",
+ "input": {
+ "url": "http://media.localhost/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "approved": true,
+ "transport": "loopback_http"
+ }
+ },
+ {
+ "id": "blossom_reference_http_nested_localhost_subdomain_approved_041",
+ "kind": "blossom.reference_policy.valid",
+ "input": {
+ "url": "http://blossom.dev.localhost/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "approved": true,
+ "transport": "loopback_http"
+ }
+ },
+ {
+ "id": "blossom_reference_http_ipv4_loopback_approved_042",
+ "kind": "blossom.reference_policy.valid",
+ "input": {
+ "url": "http://127.0.0.1:21100/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "approved": true,
+ "transport": "loopback_http"
+ }
+ },
+ {
+ "id": "blossom_reference_http_ipv4_loopback_range_approved_043",
+ "kind": "blossom.reference_policy.valid",
+ "input": {
+ "url": "http://127.255.255.254/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "approved": true,
+ "transport": "loopback_http"
+ }
+ },
+ {
+ "id": "blossom_reference_http_ipv6_loopback_approved_044",
+ "kind": "blossom.reference_policy.valid",
+ "input": {
+ "url": "http://[::1]:21100/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "approved": true,
+ "transport": "loopback_http"
+ }
+ },
+ {
+ "id": "blossom_reference_http_expanded_ipv6_loopback_approved_045",
+ "kind": "blossom.reference_policy.valid",
+ "input": {
+ "url": "http://[0:0:0:0:0:0:0:1]/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "approved": true,
+ "transport": "loopback_http"
+ }
+ },
+ {
+ "id": "blossom_reference_http_public_rejected_046",
+ "kind": "blossom.reference_policy.invalid",
+ "input": {
+ "url": "http://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "insecure_blob_url"
+ }
+ },
+ {
+ "id": "blossom_reference_http_false_localhost_suffix_rejected_047",
+ "kind": "blossom.reference_policy.invalid",
+ "input": {
+ "url": "http://localhost.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "insecure_blob_url"
+ }
+ },
+ {
+ "id": "blossom_reference_http_absolute_localhost_name_rejected_048",
+ "kind": "blossom.reference_policy.invalid",
+ "input": {
+ "url": "http://localhost./ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "insecure_blob_url"
+ }
+ },
+ {
+ "id": "blossom_reference_http_before_ipv4_loopback_range_rejected_049",
+ "kind": "blossom.reference_policy.invalid",
+ "input": {
+ "url": "http://126.255.255.255/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "insecure_blob_url"
+ }
+ },
+ {
+ "id": "blossom_reference_http_after_ipv4_loopback_range_rejected_050",
+ "kind": "blossom.reference_policy.invalid",
+ "input": {
+ "url": "http://128.0.0.0/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "insecure_blob_url"
+ }
+ },
+ {
+ "id": "blossom_reference_http_private_ipv4_rejected_051",
+ "kind": "blossom.reference_policy.invalid",
+ "input": {
+ "url": "http://10.0.0.4/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "insecure_blob_url"
+ }
+ },
+ {
+ "id": "blossom_reference_http_unspecified_ipv4_rejected_052",
+ "kind": "blossom.reference_policy.invalid",
+ "input": {
+ "url": "http://0.0.0.0/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "insecure_blob_url"
+ }
+ },
+ {
+ "id": "blossom_reference_http_ipv6_link_local_rejected_053",
+ "kind": "blossom.reference_policy.invalid",
+ "input": {
+ "url": "http://[fe80::1]/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "insecure_blob_url"
+ }
+ },
+ {
+ "id": "blossom_reference_http_ipv4_mapped_ipv6_rejected_054",
+ "kind": "blossom.reference_policy.invalid",
+ "input": {
+ "url": "http://[::ffff:127.0.0.1]/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "insecure_blob_url"
+ }
+ },
+ {
+ "id": "blossom_blob_url_short_ipv4_spelling_rejected_055",
+ "kind": "blossom.blob_url.parse.invalid",
+ "input": {
+ "url": "http://127.1/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "invalid_blob_url"
+ }
+ },
+ {
+ "id": "blossom_reference_http_private_ipv4_second_range_rejected_056",
+ "kind": "blossom.reference_policy.invalid",
+ "input": {
+ "url": "http://192.168.1.4/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
+ },
+ "expected": {
+ "error": "insecure_blob_url"
+ }
+ },
+ {
+ "id": "blossom_media_type_image_png_057",
+ "kind": "blossom.media_type.parse.valid",
+ "input": {
+ "media_type": "image/png"
+ },
+ "expected": {
+ "media_type": "image/png"
+ }
+ },
+ {
+ "id": "blossom_media_type_case_canonicalized_058",
+ "kind": "blossom.media_type.parse.valid",
+ "input": {
+ "media_type": "IMAGE/PNG"
+ },
+ "expected": {
+ "media_type": "image/png"
+ }
+ },
+ {
+ "id": "blossom_media_type_vendor_token_059",
+ "kind": "blossom.media_type.parse.valid",
+ "input": {
+ "media_type": "application/vnd.radroots.fixture+json"
+ },
+ "expected": {
+ "media_type": "application/vnd.radroots.fixture+json"
+ }
+ },
+ {
+ "id": "blossom_media_type_empty_060",
+ "kind": "blossom.media_type.parse.invalid",
+ "input": {
+ "media_type": ""
+ },
+ "expected": {
+ "error": "invalid_media_type"
+ }
+ },
+ {
+ "id": "blossom_media_type_parameters_sorted_and_canonicalized_061",
+ "kind": "blossom.media_type.parse.valid",
+ "input": {
+ "media_type": "TEXT/PLAIN; PROFILE=web; CHARSET=UTF-8"
+ },
+ "expected": {
+ "media_type": "text/plain; charset=UTF-8; profile=web"
+ }
+ },
+ {
+ "id": "blossom_media_type_duplicate_parameter_rejected_061b",
+ "kind": "blossom.media_type.parse.invalid",
+ "input": {
+ "media_type": "image/png; profile=a; PROFILE=b"
+ },
+ "expected": {
+ "error": "invalid_media_type"
+ }
+ },
+ {
+ "id": "blossom_media_type_missing_separator_062",
+ "kind": "blossom.media_type.parse.invalid",
+ "input": {
+ "media_type": "imagepng"
+ },
+ "expected": {
+ "error": "invalid_media_type"
+ }
+ },
+ {
+ "id": "blossom_media_type_empty_subtype_063",
+ "kind": "blossom.media_type.parse.invalid",
+ "input": {
+ "media_type": "image/"
+ },
+ "expected": {
+ "error": "invalid_media_type"
+ }
+ },
+ {
+ "id": "blossom_media_type_wildcard_forbidden_064",
+ "kind": "blossom.media_type.parse.invalid",
+ "input": {
+ "media_type": "image/*"
+ },
+ "expected": {
+ "error": "invalid_media_type"
+ }
+ },
+ {
+ "id": "blossom_media_type_parser_truncated_whitespace_forbidden_065",
+ "kind": "blossom.media_type.parse.invalid",
+ "input": {
+ "media_type": "image/png "
+ },
+ "expected": {
+ "error": "invalid_media_type"
+ }
+ },
+ {
+ "id": "blossom_media_type_non_ascii_forbidden_066",
+ "kind": "blossom.media_type.parse.invalid",
+ "input": {
+ "media_type": "image/p\u00f1g"
+ },
+ "expected": {
+ "error": "invalid_media_type"
+ }
+ },
+ {
+ "id": "blossom_descriptor_https_valid_067",
+ "kind": "blossom.descriptor.parse.valid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ }
+ },
+ {
+ "id": "blossom_descriptor_empty_blob_extra_fields_valid_068",
+ "kind": "blossom.descriptor.parse.valid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.bin",
+ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "size": 0,
+ "type": "application/octet-stream",
+ "uploaded": 0,
+ "blurhash": "fixture-only-extra-field",
+ "custom": {
+ "version": 1
+ }
+ }
+ },
+ "expected": {
+ "descriptor": {
+ "url": "https://media.example/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.bin",
+ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "size": 0,
+ "type": "application/octet-stream",
+ "uploaded": 0
+ }
+ }
+ },
+ {
+ "id": "blossom_descriptor_extension_does_not_define_mime_069",
+ "kind": "blossom.descriptor.parse.valid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.jpg",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "image/png",
+ "uploaded": 1700000001
+ }
+ },
+ "expected": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.jpg",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "image/png",
+ "uploaded": 1700000001
+ }
+ }
+ },
+ {
+ "id": "blossom_descriptor_public_http_structurally_valid_070",
+ "kind": "blossom.descriptor.parse.valid",
+ "input": {
+ "descriptor": {
+ "url": "http://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000002
+ }
+ },
+ "expected": {
+ "descriptor": {
+ "url": "http://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000002
+ }
+ }
+ },
+ {
+ "id": "blossom_descriptor_media_type_canonicalized_071",
+ "kind": "blossom.descriptor.parse.valid",
+ "input": {
+ "descriptor": {
+ "url": "http://127.0.0.1:21100/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.PNG",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "IMAGE/PNG",
+ "uploaded": 0
+ }
+ },
+ "expected": {
+ "descriptor": {
+ "url": "http://127.0.0.1:21100/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.PNG",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "image/png",
+ "uploaded": 0
+ }
+ }
+ },
+ {
+ "id": "blossom_descriptor_missing_url_072",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "missing_descriptor_field",
+ "field": "url"
+ }
+ },
+ {
+ "id": "blossom_descriptor_missing_sha256_073",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "missing_descriptor_field",
+ "field": "sha256"
+ }
+ },
+ {
+ "id": "blossom_descriptor_missing_size_074",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "missing_descriptor_field",
+ "field": "size"
+ }
+ },
+ {
+ "id": "blossom_descriptor_missing_type_075",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "missing_descriptor_field",
+ "field": "type"
+ }
+ },
+ {
+ "id": "blossom_descriptor_missing_uploaded_076",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain"
+ }
+ },
+ "expected": {
+ "error": "missing_descriptor_field",
+ "field": "uploaded"
+ }
+ },
+ {
+ "id": "blossom_descriptor_url_extension_required_077",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "descriptor_extension_required"
+ }
+ },
+ {
+ "id": "blossom_descriptor_url_hash_mismatch_078",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "0000000000000000000000000000000000000000000000000000000000000000",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "descriptor_hash_mismatch"
+ }
+ },
+ {
+ "id": "blossom_descriptor_negative_size_079",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": -1,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "invalid_descriptor_size"
+ }
+ },
+ {
+ "id": "blossom_descriptor_fractional_size_080",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3.5,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "invalid_descriptor_size"
+ }
+ },
+ {
+ "id": "blossom_descriptor_negative_uploaded_081",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": -1
+ }
+ },
+ "expected": {
+ "error": "invalid_descriptor_uploaded"
+ }
+ },
+ {
+ "id": "blossom_descriptor_parameterized_media_type_valid_082",
+ "kind": "blossom.descriptor.parse.valid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "TEXT/PLAIN; PROFILE=mobile; CHARSET=UTF-8",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain; charset=UTF-8; profile=mobile",
+ "uploaded": 1700000000
+ }
+ }
+ },
+ {
+ "id": "blossom_descriptor_invalid_media_type_082b",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "image/",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "invalid_media_type"
+ }
+ },
+ {
+ "id": "blossom_descriptor_uppercase_sha256_083",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "invalid_sha256"
+ }
+ },
+ {
+ "id": "blossom_descriptor_url_query_forbidden_084",
+ "kind": "blossom.descriptor.parse.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt?download=1",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "blob_url_query_forbidden"
+ }
+ },
+ {
+ "id": "blossom_descriptor_approved_https_reference_084b",
+ "kind": "blossom.descriptor.approve_reference.valid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "approved_url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt"
+ }
+ },
+ {
+ "id": "blossom_descriptor_public_http_reference_rejected_084c",
+ "kind": "blossom.descriptor.approve_reference.invalid",
+ "input": {
+ "descriptor": {
+ "url": "http://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ }
+ },
+ "expected": {
+ "error": "insecure_blob_url"
+ }
+ },
+ {
+ "id": "blossom_descriptor_verify_abc_valid_085",
+ "kind": "blossom.descriptor.verify_bytes.valid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ },
+ "approved_media_type": "text/plain",
+ "bytes_hex": "616263"
+ },
+ "expected": {
+ "verified": {
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "media_type": "text/plain"
+ }
+ }
+ },
+ {
+ "id": "blossom_descriptor_verify_empty_valid_086",
+ "kind": "blossom.descriptor.verify_bytes.valid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.bin",
+ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "size": 0,
+ "type": "application/octet-stream",
+ "uploaded": 1700000000
+ },
+ "approved_media_type": "application/octet-stream",
+ "bytes_hex": ""
+ },
+ "expected": {
+ "verified": {
+ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "size": 0,
+ "media_type": "application/octet-stream"
+ }
+ }
+ },
+ {
+ "id": "blossom_descriptor_verify_no_sniff_or_extension_inference_087",
+ "kind": "blossom.descriptor.verify_bytes.valid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.jpg",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "image/png",
+ "uploaded": 1700000000
+ },
+ "approved_media_type": "image/png",
+ "bytes_hex": "616263"
+ },
+ "expected": {
+ "verified": {
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "media_type": "image/png"
+ }
+ }
+ },
+ {
+ "id": "blossom_descriptor_verify_approved_media_type_canonicalized_088",
+ "kind": "blossom.descriptor.verify_bytes.valid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "image/png",
+ "uploaded": 1700000000
+ },
+ "approved_media_type": "IMAGE/PNG",
+ "bytes_hex": "616263"
+ },
+ "expected": {
+ "verified": {
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "media_type": "image/png"
+ }
+ }
+ },
+ {
+ "id": "blossom_descriptor_verify_hash_mismatch_089",
+ "kind": "blossom.descriptor.verify_bytes.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 7,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ },
+ "approved_media_type": "text/plain",
+ "bytes_hex": "6368616e676564"
+ },
+ "expected": {
+ "error": "blob_hash_mismatch"
+ }
+ },
+ {
+ "id": "blossom_descriptor_verify_size_mismatch_090",
+ "kind": "blossom.descriptor.verify_bytes.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 4,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ },
+ "approved_media_type": "text/plain",
+ "bytes_hex": "616263"
+ },
+ "expected": {
+ "error": "blob_size_mismatch"
+ }
+ },
+ {
+ "id": "blossom_descriptor_verify_media_type_mismatch_091",
+ "kind": "blossom.descriptor.verify_bytes.invalid",
+ "input": {
+ "descriptor": {
+ "url": "https://media.example/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.txt",
+ "sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+ "size": 3,
+ "type": "text/plain",
+ "uploaded": 1700000000
+ },
+ "approved_media_type": "application/octet-stream",
+ "bytes_hex": "616263"
+ },
+ "expected": {
+ "error": "blob_media_type_mismatch"
+ }
+ }
+ ]
+}