lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit ae46ed17a916a1a241d6ccd54e830832cb9c25c2
parent 811a1feb5d30feadebe0eb772a512e50c6af229d
Author: triesap <tyson@radroots.org>
Date:   Tue, 28 Jul 2026 08:20:54 +0000

blossom: normalize Blossom type names and module ownership

- Publish the exact compact root type surface without compatibility aliases
- Move media type authority into its owned module and preserve private typestates
- Migrate every lib capsule consumer and govern predecessor supersession
- Verify canonical release, architecture, no-std, wasm, test, and lint gates

Diffstat:
Mcontracts/events/blossom-authorization.md | 18+++++++++---------
Mcontracts/events/blossom-media.md | 28++++++++++++++--------------
Mcontracts/events/profile-metadata.md | 2+-
Mcontracts/operations.toml | 112++++++++++++++++++++++++++++++++++++++++----------------------------------------
Mcrates/blossom/src/authorization.rs | 635+++++++++++++++++++++++++++++++++----------------------------------------------
Mcrates/blossom/src/descriptor.rs | 268+++++++++++++++++++++++++++----------------------------------------------------
Mcrates/blossom/src/error.rs | 128++++++++++++++++++++++++++++++++++++++++----------------------------------------
Mcrates/blossom/src/hash.rs | 132+++++++++++++++++++++++++++++++++++--------------------------------------------
Mcrates/blossom/src/lib.rs | 25+++++++------------------
Mcrates/blossom/src/media_type.rs | 74+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/blossom/src/url.rs | 149+++++++++++++++++++++++++++++++++++--------------------------------------------
Mcrates/blossom/tests/bud11_conformance.rs | 76++++++++++++++++++++++++++++++++++------------------------------------------
Mcrates/blossom/tests/conformance.rs | 45+++++++++++++++++++++------------------------
Mcrates/blossom/tests/package_boundary.rs | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mcrates/event/src/calendar.rs | 20++++++++++----------
Mcrates/event/src/contract/registry_v7.rs | 6+++---
Mcrates/event/src/food_availability.rs | 25++++++++++---------------
Mcrates/event/src/media.rs | 31++++++++++++-------------------
Mcrates/event/src/post.rs | 10+++++-----
Mcrates/event/src/profile.rs | 19++++++-------------
Mcrates/event_codec/src/food_availability/inbound/registry_v7.rs | 10+++++-----
Mcrates/event_codec/tests/calendar.rs | 13+++++--------
Mcrates/event_codec/tests/calendar_conformance.rs | 12++++--------
Mcrates/event_codec/tests/food_availability_conformance.rs | 13+++++--------
Mcrates/event_codec/tests/post.rs | 42+++++++++++++++++-------------------------
Mcrates/event_codec/tests/profile_conformance.rs | 9+++------
Mcrates/event_codec/tests/verified_post_conformance.rs | 15++++++---------
Mcrates/event_store/contracts/source_maintenance_v1.manifest.json | 107++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mcrates/event_store/contracts/source_maintenance_v1.manifest.sha256 | 2+-
Mcrates/event_store/src/generated/source_maintenance_manifest.rs | 6+++---
Mcrates/event_store/src/model/food_availability_projection_v1.rs | 6+++---
Mcrates/event_store/src/store.rs | 6+++---
Mcrates/event_store/src/store/food_availability_projection_v1.rs | 2+-
Mcrates/event_store/tests/food_availability_projection_v1_result_vector.rs | 8++++----
Mcrates/nostr/src/blossom.rs | 32+++++++++++++++-----------------
Mcrates/nostr/tests/blossom_conformance.rs | 66++++++++++++++++++++++++++++++++++--------------------------------
Mcrates/nostr/tests/food_availability_profile.rs | 14+++++---------
Mcrates/nostr/tests/post_profile.rs | 13+++++--------
Mtools/xtask/src/contract.rs | 17++++++++---------
Mtools/xtask/src/contract/food_availability_projection.rs | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mtools/xtask/src/contract/nip09_reconciliation.rs | 4++--
Mtools/xtask/src/contract/source_maintenance.rs | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mtools/xtask/src/main.rs | 5+++--
43 files changed, 1208 insertions(+), 1219 deletions(-)

diff --git a/contracts/events/blossom-authorization.md b/contracts/events/blossom-authorization.md @@ -36,11 +36,11 @@ all-server-match rule remain outside this public crate. ## Actions And Endpoint Targets -`RadrootsBlossomAuthorizationAction` has exactly the five lowercase BUD-11 wire values `get`, +`AuthorizationAction` has exactly the five lowercase BUD-11 wire values `get`, `upload`, `list`, `delete`, and `media`. Case variants, whitespace, unknown verbs, and the empty string are invalid. -Endpoint validation uses an explicit `RadrootsBlossomAuthorizationTarget`: +Endpoint validation uses an explicit `AuthorizationTarget`: | Endpoint | Target | Required action | Implied hash | `x` policy | | --- | --- | --- | --- | --- | @@ -56,7 +56,7 @@ does not authorize arbitrary endpoints and does not perform HTTP parsing. ## Human-Readable Content -`RadrootsBlossomAuthorizationContent` is from 1 through 4,096 UTF-8 bytes and is already equal to +`AuthorizationContent` is from 1 through 4,096 UTF-8 bytes and is already equal to its Unicode-whitespace-trimmed form. It rejects empty and whitespace-only input, leading or trailing whitespace, NUL, and non-whitespace controls. Horizontal tab, newline, and carriage return are allowed only inside otherwise human-readable content. Interior ordinary whitespace and @@ -69,7 +69,7 @@ as `Upload farm photo`. ## Server Domains -A `RadrootsBlossomServerDomain` is an ASCII lowercase domain name of at most 253 bytes without +A `ServerDomain` is an ASCII lowercase domain name of at most 253 bytes without scheme, user information, port, path, query, fragment, IP-literal brackets, or trailing dot. Every dot-separated label is from 1 through 63 bytes, begins and ends with an ASCII lowercase letter or digit, and otherwise contains only ASCII lowercase letters, digits, or `-`. `localhost` is valid. @@ -88,7 +88,7 @@ reduces replay scope without changing tolerant inbound BUD-11 semantics. ## Blob Hash Scope -Every known `x` tag value is a `RadrootsBlossomSha256`: exactly 64 lowercase hexadecimal +Every known `x` tag value is a `Sha256`: exactly 64 lowercase hexadecimal characters. Multiple valid `x` tags are allowed. For targets requiring a hash scope, at least one `x` tag must equal the target's implied hash. Other @@ -100,7 +100,7 @@ Strict authored upload claims contain exactly one `x` tag equal to the upload ha ## Claim Tag Parsing -`RadrootsBlossomParsedAuthorizationClaim::parse` consumes signed content, the event `created_at` +`AuthorizationClaim::parse` consumes signed content, the event `created_at` timestamp, and raw Nostr tag arrays. Structural parsing occurs before endpoint policy validation. Known tags use exact lowercase names and require at least a key and value: @@ -126,7 +126,7 @@ or target scope. Those checks belong to the appropriate outward adapter or valid ## Time Validation Endpoint validation receives an explicit `now` so it is deterministic and does not read a wall -clock. `RadrootsBlossomAuthorizationValidation::bud11` applies only the pinned BUD time and optional +clock. `AuthorizationValidation::bud11` applies only the pinned BUD time and optional server semantics. The existing bounded constructor applies the Radroots replay profile with `RADROOTS_BLOSSOM_AUTH_MAX_CREATED_AGE_SECONDS = 300` and `RADROOTS_BLOSSOM_AUTH_MAX_HORIZON_SECONDS = 300`. @@ -148,7 +148,7 @@ seconds inclusive, and `created_at + lifetime` must not overflow `u64`. ## Strict Authored Upload Claims -`RadrootsBlossomAuthoredUploadClaim::new` requires typed human-readable content, one typed server +`AuthoredUploadClaim::new` requires typed human-readable content, one typed server domain, one exact typed SHA-256, `created_at`, and a valid authored lifetime. It produces canonical wire parts in this exact order: @@ -254,7 +254,7 @@ The outward Nostr adapter additionally exposes these stable boundary identifiers - `invalid_event_signature` - `event_signing` -An authenticated pure-claim failure retains its `RadrootsBlossomError::code()` identifier rather +An authenticated pure-claim failure retains its `Error::code()` identifier rather than being collapsed into a generic adapter error. Direct typed action, server-domain, and hash parsing uses `invalid_authorization_action`, diff --git a/contracts/events/blossom-media.md b/contracts/events/blossom-media.md @@ -30,7 +30,7 @@ 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 +A `Sha256` 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, @@ -41,7 +41,7 @@ ASCII hexadecimal characters. ## Root Hash Paths -A `RadrootsBlossomHashPath` represents one BUD-01 root path: +A `HashPath` represents one BUD-01 root path: ```text /<64-lowercase-hex-sha256>[.<extension>] @@ -61,7 +61,7 @@ include one. The extension is not used to infer or validate the descriptor MIME ## Structural Blob URLs -A `RadrootsBlossomBlobUrl` is an absolute structural BUD-01 URL with these invariants: +A `BlobUrl` 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 @@ -72,7 +72,7 @@ A `RadrootsBlossomBlobUrl` is an absolute structural BUD-01 URL with these invar - 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` +- the path digest is exposed as a typed `Sha256` - 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 @@ -111,7 +111,7 @@ responsible for their own destination, redirect, DNS-rebinding, response-size, a ## Media Types -A `RadrootsBlossomMediaType` is a syntactically valid MIME media type parsed by the repository's +A `MediaType` 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 @@ -129,14 +129,14 @@ 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: +A structural `BlobDescriptor` 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` | +| `type` | parsed `MediaType` | | `uploaded` | unsigned 64-bit Unix timestamp | Unknown descriptor fields are tolerated on input for forward compatibility. They need not be @@ -153,12 +153,12 @@ 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` +A structural descriptor advances to `ApprovedDescriptor` only after its URL passes +the explicit HTTPS-or-loopback reference policy. `ByteCommitment::from_bytes` computes the SHA-256 and exact `u64` size of a final byte slice and binds them to a caller-approved -`RadrootsBlossomMediaType`. +`MediaType`. -An approved descriptor advances to `RadrootsBlossomByteVerifiedDescriptor` only through +An approved descriptor advances to `ByteVerifiedDescriptor` 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: @@ -196,7 +196,7 @@ this contract. Integration tests dispatch its vectors by `kind`: `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()`. +`Error::code()`. ## Stable Error Identifiers @@ -218,7 +218,7 @@ The public typed API exposes these stable semantic identifiers: - `blob_size_mismatch` - `blob_media_type_mismatch` -Malformed descriptor JSON can fail in serde before a `RadrootsBlossomError` exists. The executable +Malformed descriptor JSON can fail in serde before a `Error` exists. The executable descriptor harness uses three additional wire-shape classifications for those cases: - `missing_descriptor_field`, with the missing field named separately @@ -226,7 +226,7 @@ descriptor harness uses three additional wire-shape classifications for those ca - `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. +extension, and URL/hash combinations, retain the applicable `Error::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 diff --git a/contracts/events/profile-metadata.md b/contracts/events/profile-metadata.md @@ -65,7 +65,7 @@ remain present when `display_name` is used. The scoped optional fields are `display_name`, `about`, `nip05`, `bot`, `picture`, and `banner`; `bot` is a Boolean. NIP-05 values enter only through `RadrootsNip05Identifier`. Picture and banner values enter through the shared `RadrootsAuthoredImage`, which can wrap -only an `image/*` `RadrootsBlossomByteVerifiedDescriptor`. There is no raw-string +only an `image/*` `ByteVerifiedDescriptor`. There is no raw-string media setter or unchecked deserialization path. Generic `website`, `lud06`, and `lud16` strings remain outside this strict authored operation. diff --git a/contracts/operations.toml b/contracts/operations.toml @@ -19,26 +19,26 @@ domains = [ [shared_types] public = [ - "RadrootsBlossomSha256", - "RadrootsBlossomFileExtension", - "RadrootsBlossomHashPath", - "RadrootsBlossomBlobUrl", - "RadrootsBlossomApprovedBlobUrl", - "RadrootsBlossomMediaType", - "RadrootsBlossomBlobDescriptor", - "RadrootsBlossomApprovedDescriptor", - "RadrootsBlossomByteCommitment", - "RadrootsBlossomByteVerifiedDescriptor", - "RadrootsBlossomAuthorizationAction", - "RadrootsBlossomAuthorizationContent", - "RadrootsBlossomServerDomain", - "RadrootsBlossomAuthorizationTarget", - "RadrootsBlossomServerScopeRequirement", - "RadrootsBlossomAuthorizationValidation", - "RadrootsBlossomParsedAuthorizationClaim", - "RadrootsBlossomValidatedAuthorizationClaim", - "RadrootsBlossomAuthorizationWireParts", - "RadrootsBlossomAuthoredUploadClaim", + "Sha256", + "FileExtension", + "HashPath", + "BlobUrl", + "ApprovedBlobUrl", + "MediaType", + "BlobDescriptor", + "ApprovedDescriptor", + "ByteCommitment", + "ByteVerifiedDescriptor", + "AuthorizationAction", + "AuthorizationContent", + "ServerDomain", + "AuthorizationTarget", + "ServerScopeRequirement", + "AuthorizationValidation", + "AuthorizationClaim", + "ValidatedAuthorizationClaim", + "AuthorizationWireParts", + "AuthoredUploadClaim", "RadrootsNostrSignedBlossomAuthorization", "RadrootsNostrBlossomAuthorizationHeader", "RadrootsNostrVerifiedBlossomAuthorization", @@ -283,7 +283,7 @@ domain = "blossom" id = "blossom.parse_hash_path" stability = "beta" inputs = ["String"] -outputs = ["RadrootsBlossomHashPath"] +outputs = ["HashPath"] error_class = "validation_error" deterministic = true signing = "none" @@ -292,9 +292,9 @@ 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", + "radroots_blossom::FileExtension", + "radroots_blossom::HashPath", + "radroots_blossom::Sha256", ] [operations.blossom_parse_hash_path.conformance] @@ -304,8 +304,8 @@ vector = "contracts/conformance/vectors/blossom/hash_path_and_descriptor.v1.json domain = "blossom" id = "blossom.approve_descriptor_reference" stability = "beta" -inputs = ["RadrootsBlossomBlobDescriptor"] -outputs = ["RadrootsBlossomApprovedDescriptor"] +inputs = ["BlobDescriptor"] +outputs = ["ApprovedDescriptor"] error_class = "validation_error" deterministic = true signing = "none" @@ -314,8 +314,8 @@ 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", + "radroots_blossom::ApprovedDescriptor", + "radroots_blossom::BlobDescriptor", ] [operations.blossom_approve_descriptor_reference.conformance] @@ -326,11 +326,11 @@ domain = "blossom" id = "blossom.verify_descriptor_bytes" stability = "beta" inputs = [ - "RadrootsBlossomApprovedDescriptor", + "ApprovedDescriptor", "Bytes", - "RadrootsBlossomMediaType", + "MediaType", ] -outputs = ["RadrootsBlossomByteVerifiedDescriptor"] +outputs = ["ByteVerifiedDescriptor"] error_class = "validation_error" deterministic = true signing = "none" @@ -342,10 +342,10 @@ rust_modules = [ "crates/blossom/src/hash.rs", ] rust_types = [ - "radroots_blossom::RadrootsBlossomApprovedDescriptor", - "radroots_blossom::RadrootsBlossomByteCommitment", - "radroots_blossom::RadrootsBlossomByteVerifiedDescriptor", - "radroots_blossom::RadrootsBlossomMediaType", + "radroots_blossom::ApprovedDescriptor", + "radroots_blossom::ByteCommitment", + "radroots_blossom::ByteVerifiedDescriptor", + "radroots_blossom::MediaType", ] [operations.blossom_verify_descriptor_bytes.conformance] @@ -356,15 +356,15 @@ domain = "blossom" id = "blossom.build_upload_authorization_claim" stability = "beta" inputs = [ - "RadrootsBlossomAuthorizationContent", - "RadrootsBlossomServerDomain", - "RadrootsBlossomSha256", + "AuthorizationContent", + "ServerDomain", + "Sha256", "CreatedAt", "LifetimeSeconds", ] outputs = [ - "RadrootsBlossomAuthoredUploadClaim", - "RadrootsBlossomAuthorizationWireParts", + "AuthoredUploadClaim", + "AuthorizationWireParts", ] error_class = "validation_error" deterministic = true @@ -374,8 +374,8 @@ transport = "none" [operations.blossom_build_upload_authorization_claim.implementation] rust_modules = ["crates/blossom/src/authorization.rs"] rust_types = [ - "radroots_blossom::RadrootsBlossomAuthorizationWireParts", - "radroots_blossom::RadrootsBlossomAuthoredUploadClaim", + "radroots_blossom::AuthorizationWireParts", + "radroots_blossom::AuthoredUploadClaim", ] [operations.blossom_build_upload_authorization_claim.conformance] @@ -386,7 +386,7 @@ domain = "blossom" id = "blossom.parse_authorization_claim" stability = "beta" inputs = ["String", "CreatedAt", "NostrTags"] -outputs = ["RadrootsBlossomParsedAuthorizationClaim"] +outputs = ["AuthorizationClaim"] error_class = "parse_error" deterministic = true signing = "none" @@ -394,7 +394,7 @@ transport = "none" [operations.blossom_parse_authorization_claim.implementation] rust_modules = ["crates/blossom/src/authorization.rs"] -rust_types = ["radroots_blossom::RadrootsBlossomParsedAuthorizationClaim"] +rust_types = ["radroots_blossom::AuthorizationClaim"] [operations.blossom_parse_authorization_claim.conformance] vector = "contracts/conformance/vectors/blossom/bud11_claims.v1.json" @@ -404,10 +404,10 @@ domain = "blossom" id = "blossom.validate_authorization_claim" stability = "beta" inputs = [ - "RadrootsBlossomParsedAuthorizationClaim", - "RadrootsBlossomAuthorizationValidation", + "AuthorizationClaim", + "AuthorizationValidation", ] -outputs = ["RadrootsBlossomValidatedAuthorizationClaim"] +outputs = ["ValidatedAuthorizationClaim"] error_class = "validation_error" deterministic = true signing = "none" @@ -416,8 +416,8 @@ transport = "none" [operations.blossom_validate_authorization_claim.implementation] rust_modules = ["crates/blossom/src/authorization.rs"] rust_types = [ - "radroots_blossom::RadrootsBlossomAuthorizationValidation", - "radroots_blossom::RadrootsBlossomValidatedAuthorizationClaim", + "radroots_blossom::AuthorizationValidation", + "radroots_blossom::ValidatedAuthorizationClaim", ] [operations.blossom_validate_authorization_claim.conformance] @@ -427,7 +427,7 @@ vector = "contracts/conformance/vectors/blossom/bud11_claims.v1.json" domain = "blossom" id = "blossom.decode_verify_authorization_header" stability = "beta" -inputs = ["String", "RadrootsBlossomAuthorizationValidation"] +inputs = ["String", "AuthorizationValidation"] outputs = ["RadrootsNostrVerifiedBlossomAuthorization"] error_class = "decode_error" deterministic = true @@ -440,7 +440,7 @@ rust_modules = [ "crates/nostr/src/blossom.rs", ] rust_types = [ - "radroots_blossom::RadrootsBlossomAuthorizationValidation", + "radroots_blossom::AuthorizationValidation", "radroots_nostr::blossom::RadrootsNostrVerifiedBlossomAuthorization", ] @@ -539,7 +539,7 @@ rust_modules = [ "crates/event_codec/src/food_availability/authored.rs", ] rust_types = [ - "radroots_blossom::RadrootsBlossomByteVerifiedDescriptor", + "radroots_blossom::ByteVerifiedDescriptor", "radroots_event::food_availability::RadrootsFoodAvailabilityDetails", "radroots_event::food_availability::RadrootsFoodAvailabilityDetailsParts", "radroots_event::food_availability::RadrootsFoodAvailabilityError", @@ -671,7 +671,7 @@ rust_modules = [ "crates/event_codec/src/profile/authored.rs", ] rust_types = [ - "radroots_blossom::RadrootsBlossomByteVerifiedDescriptor", + "radroots_blossom::ByteVerifiedDescriptor", "radroots_event::media::RadrootsAuthoredImage", "radroots_event::media::RadrootsAuthoredImageError", "radroots_event::profile::RadrootsAuthoredProfile", @@ -866,8 +866,8 @@ rust_modules = [ "crates/event_codec/src/post/authored.rs", ] rust_types = [ - "radroots_blossom::RadrootsBlossomApprovedBlobUrl", - "radroots_blossom::RadrootsBlossomByteVerifiedDescriptor", + "radroots_blossom::ApprovedBlobUrl", + "radroots_blossom::ByteVerifiedDescriptor", "radroots_event::media::RadrootsAuthoredImage", "radroots_event::post::RadrootsAuthoredPhotoUpdate", "radroots_event::post::RadrootsAuthoredPostError", @@ -1604,7 +1604,7 @@ rust_modules = [ "crates/event_codec/src/calendar/decode.rs", ] rust_types = [ - "radroots_blossom::RadrootsBlossomBlobUrl", + "radroots_blossom::BlobUrl", "radroots_event::calendar::RadrootsAdmittedCalendar", "radroots_event::calendar::RadrootsCalendarAdmissionError", "radroots_event::calendar::RadrootsCalendarEventReference", diff --git a/crates/blossom/src/authorization.rs b/crates/blossom/src/authorization.rs @@ -5,7 +5,7 @@ use alloc::{ }; use core::{fmt, str::FromStr}; -use crate::{RadrootsBlossomError, RadrootsBlossomSha256}; +use crate::{Error, Sha256}; pub const RADROOTS_BLOSSOM_AUTHORIZATION_EVENT_KIND: u16 = 24_242; pub const RADROOTS_BLOSSOM_AUTH_CONTENT_MAX_BYTES: usize = 4_096; @@ -13,7 +13,7 @@ pub const RADROOTS_BLOSSOM_AUTH_MAX_CREATED_AGE_SECONDS: u64 = 300; pub const RADROOTS_BLOSSOM_AUTH_MAX_HORIZON_SECONDS: u64 = 300; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum RadrootsBlossomAuthorizationAction { +pub enum AuthorizationAction { Get, Upload, List, @@ -21,15 +21,15 @@ pub enum RadrootsBlossomAuthorizationAction { Media, } -impl RadrootsBlossomAuthorizationAction { - pub fn parse(value: &str) -> Result<Self, RadrootsBlossomError> { +impl AuthorizationAction { + pub fn parse(value: &str) -> Result<Self, Error> { match value { "get" => Ok(Self::Get), "upload" => Ok(Self::Upload), "list" => Ok(Self::List), "delete" => Ok(Self::Delete), "media" => Ok(Self::Media), - _ => Err(RadrootsBlossomError::InvalidAuthorizationAction), + _ => Err(Error::InvalidAuthorizationAction), } } @@ -44,14 +44,14 @@ impl RadrootsBlossomAuthorizationAction { } } -impl fmt::Display for RadrootsBlossomAuthorizationAction { +impl fmt::Display for AuthorizationAction { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(self.as_str()) } } -impl FromStr for RadrootsBlossomAuthorizationAction { - type Err = RadrootsBlossomError; +impl FromStr for AuthorizationAction { + type Err = Error; fn from_str(value: &str) -> Result<Self, Self::Err> { Self::parse(value) @@ -59,16 +59,16 @@ impl FromStr for RadrootsBlossomAuthorizationAction { } #[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct RadrootsBlossomServerDomain(String); +pub struct ServerDomain(String); -impl RadrootsBlossomServerDomain { - pub fn parse(value: &str) -> Result<Self, RadrootsBlossomError> { +impl ServerDomain { + pub fn parse(value: &str) -> Result<Self, Error> { if value.is_empty() || value.len() > 253 || !value.is_ascii() || value.bytes().any(|byte| byte.is_ascii_uppercase()) { - return Err(RadrootsBlossomError::InvalidAuthorizationServerDomain); + return Err(Error::InvalidAuthorizationServerDomain); } let mut all_labels_are_numeric = true; @@ -87,13 +87,13 @@ impl RadrootsBlossomServerDomain { .last() .is_some_and(u8::is_ascii_alphanumeric) { - return Err(RadrootsBlossomError::InvalidAuthorizationServerDomain); + return Err(Error::InvalidAuthorizationServerDomain); } all_labels_are_numeric &= label.bytes().all(|byte| byte.is_ascii_digit()); } if all_labels_are_numeric && !is_canonical_dotted_ipv4(value) { - return Err(RadrootsBlossomError::InvalidAuthorizationServerDomain); + return Err(Error::InvalidAuthorizationServerDomain); } Ok(Self(value.to_string())) @@ -104,14 +104,14 @@ impl RadrootsBlossomServerDomain { } } -impl fmt::Display for RadrootsBlossomServerDomain { +impl fmt::Display for ServerDomain { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(self.as_str()) } } -impl FromStr for RadrootsBlossomServerDomain { - type Err = RadrootsBlossomError; +impl FromStr for ServerDomain { + type Err = Error; fn from_str(value: &str) -> Result<Self, Self::Err> { Self::parse(value) @@ -119,10 +119,10 @@ impl FromStr for RadrootsBlossomServerDomain { } #[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct RadrootsBlossomAuthorizationContent(String); +pub struct AuthorizationContent(String); -impl RadrootsBlossomAuthorizationContent { - pub fn parse(value: &str) -> Result<Self, RadrootsBlossomError> { +impl AuthorizationContent { + pub fn parse(value: &str) -> Result<Self, Error> { if value.is_empty() || value.len() > RADROOTS_BLOSSOM_AUTH_CONTENT_MAX_BYTES || value.trim() != value @@ -130,7 +130,7 @@ impl RadrootsBlossomAuthorizationContent { .chars() .any(|character| character.is_control() && !matches!(character, '\t' | '\n' | '\r')) { - return Err(RadrootsBlossomError::InvalidAuthorizationContent); + return Err(Error::InvalidAuthorizationContent); } Ok(Self(value.to_string())) } @@ -140,14 +140,14 @@ impl RadrootsBlossomAuthorizationContent { } } -impl fmt::Display for RadrootsBlossomAuthorizationContent { +impl fmt::Display for AuthorizationContent { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(self.as_str()) } } -impl FromStr for RadrootsBlossomAuthorizationContent { - type Err = RadrootsBlossomError; +impl FromStr for AuthorizationContent { + type Err = Error; fn from_str(value: &str) -> Result<Self, Self::Err> { Self::parse(value) @@ -155,27 +155,27 @@ impl FromStr for RadrootsBlossomAuthorizationContent { } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum RadrootsBlossomAuthorizationTarget { - GetBlob(RadrootsBlossomSha256), - Upload(RadrootsBlossomSha256), +pub enum AuthorizationTarget { + GetBlob(Sha256), + Upload(Sha256), List, - DeleteBlob(RadrootsBlossomSha256), - Mirror(RadrootsBlossomSha256), - Media(RadrootsBlossomSha256), + DeleteBlob(Sha256), + Mirror(Sha256), + Media(Sha256), } -impl RadrootsBlossomAuthorizationTarget { - pub const fn action(self) -> RadrootsBlossomAuthorizationAction { +impl AuthorizationTarget { + pub const fn action(self) -> AuthorizationAction { match self { - Self::GetBlob(_) => RadrootsBlossomAuthorizationAction::Get, - Self::Upload(_) | Self::Mirror(_) => RadrootsBlossomAuthorizationAction::Upload, - Self::List => RadrootsBlossomAuthorizationAction::List, - Self::DeleteBlob(_) => RadrootsBlossomAuthorizationAction::Delete, - Self::Media(_) => RadrootsBlossomAuthorizationAction::Media, + Self::GetBlob(_) => AuthorizationAction::Get, + Self::Upload(_) | Self::Mirror(_) => AuthorizationAction::Upload, + Self::List => AuthorizationAction::List, + Self::DeleteBlob(_) => AuthorizationAction::Delete, + Self::Media(_) => AuthorizationAction::Media, } } - pub const fn implied_hash(self) -> Option<RadrootsBlossomSha256> { + pub const fn implied_hash(self) -> Option<Sha256> { match self { Self::GetBlob(hash) | Self::Upload(hash) @@ -195,31 +195,27 @@ impl RadrootsBlossomAuthorizationTarget { } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum RadrootsBlossomServerScopeRequirement { +pub enum ServerScopeRequirement { OptionalAnyMatch, RequiredAnyMatch, } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsBlossomAuthorizationValidation { - target: RadrootsBlossomAuthorizationTarget, - target_server: RadrootsBlossomServerDomain, - server_scope_requirement: RadrootsBlossomServerScopeRequirement, +pub struct AuthorizationValidation { + target: AuthorizationTarget, + target_server: ServerDomain, + server_scope_requirement: ServerScopeRequirement, now: u64, max_created_age_seconds: Option<u64>, max_lifetime_seconds: Option<u64>, } -impl RadrootsBlossomAuthorizationValidation { - pub fn bud11( - target: RadrootsBlossomAuthorizationTarget, - target_server: RadrootsBlossomServerDomain, - now: u64, - ) -> Self { +impl AuthorizationValidation { + pub fn bud11(target: AuthorizationTarget, target_server: ServerDomain, now: u64) -> Self { Self { target, target_server, - server_scope_requirement: RadrootsBlossomServerScopeRequirement::OptionalAnyMatch, + server_scope_requirement: ServerScopeRequirement::OptionalAnyMatch, now, max_created_age_seconds: None, max_lifetime_seconds: None, @@ -227,14 +223,14 @@ impl RadrootsBlossomAuthorizationValidation { } pub fn new( - target: RadrootsBlossomAuthorizationTarget, - target_server: RadrootsBlossomServerDomain, - server_scope_requirement: RadrootsBlossomServerScopeRequirement, + target: AuthorizationTarget, + target_server: ServerDomain, + server_scope_requirement: ServerScopeRequirement, now: u64, max_created_age_seconds: u64, - ) -> Result<Self, RadrootsBlossomError> { + ) -> Result<Self, Error> { if max_created_age_seconds > RADROOTS_BLOSSOM_AUTH_MAX_CREATED_AGE_SECONDS { - return Err(RadrootsBlossomError::InvalidAuthorizationCreatedAge); + return Err(Error::InvalidAuthorizationCreatedAge); } Ok(Self { target, @@ -246,15 +242,15 @@ impl RadrootsBlossomAuthorizationValidation { }) } - pub const fn target(&self) -> RadrootsBlossomAuthorizationTarget { + pub const fn target(&self) -> AuthorizationTarget { self.target } - pub fn target_server(&self) -> &RadrootsBlossomServerDomain { + pub fn target_server(&self) -> &ServerDomain { &self.target_server } - pub const fn server_scope_requirement(&self) -> RadrootsBlossomServerScopeRequirement { + pub const fn server_scope_requirement(&self) -> ServerScopeRequirement { self.server_scope_requirement } @@ -272,22 +268,18 @@ impl RadrootsBlossomAuthorizationValidation { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsBlossomParsedAuthorizationClaim { - content: RadrootsBlossomAuthorizationContent, +pub struct AuthorizationClaim { + content: AuthorizationContent, created_at: u64, - action: RadrootsBlossomAuthorizationAction, + action: AuthorizationAction, expiration: u64, - server_domains: Vec<RadrootsBlossomServerDomain>, - hashes: Vec<RadrootsBlossomSha256>, + server_domains: Vec<ServerDomain>, + hashes: Vec<Sha256>, } -impl RadrootsBlossomParsedAuthorizationClaim { - pub fn parse( - content: &str, - created_at: u64, - tags: &[Vec<String>], - ) -> Result<Self, RadrootsBlossomError> { - let content = RadrootsBlossomAuthorizationContent::parse(content)?; +impl AuthorizationClaim { + pub fn parse(content: &str, created_at: u64, tags: &[Vec<String>]) -> Result<Self, Error> { + let content = AuthorizationContent::parse(content)?; let mut action = None; let mut expiration = None; let mut server_domains = Vec::new(); @@ -297,41 +289,35 @@ impl RadrootsBlossomParsedAuthorizationClaim { match tag.first().map(String::as_str) { Some("t") => { if action.is_some() { - return Err(RadrootsBlossomError::DuplicateAuthorizationActionTag); + return Err(Error::DuplicateAuthorizationActionTag); } - let value = tag - .get(1) - .ok_or(RadrootsBlossomError::MalformedAuthorizationActionTag)?; + let value = tag.get(1).ok_or(Error::MalformedAuthorizationActionTag)?; action = Some( - RadrootsBlossomAuthorizationAction::parse(value) - .map_err(|_| RadrootsBlossomError::MalformedAuthorizationActionTag)?, + AuthorizationAction::parse(value) + .map_err(|_| Error::MalformedAuthorizationActionTag)?, ); } Some("expiration") => { if expiration.is_some() { - return Err(RadrootsBlossomError::DuplicateAuthorizationExpirationTag); + return Err(Error::DuplicateAuthorizationExpirationTag); } let value = tag .get(1) - .ok_or(RadrootsBlossomError::MalformedAuthorizationExpirationTag)?; + .ok_or(Error::MalformedAuthorizationExpirationTag)?; expiration = Some(parse_timestamp(value)?); } Some("server") => { - let value = tag - .get(1) - .ok_or(RadrootsBlossomError::MalformedAuthorizationServerTag)?; + let value = tag.get(1).ok_or(Error::MalformedAuthorizationServerTag)?; server_domains.push( - RadrootsBlossomServerDomain::parse(value) - .map_err(|_| RadrootsBlossomError::MalformedAuthorizationServerTag)?, + ServerDomain::parse(value) + .map_err(|_| Error::MalformedAuthorizationServerTag)?, ); } Some("x") => { - let value = tag - .get(1) - .ok_or(RadrootsBlossomError::MalformedAuthorizationHashTag)?; + let value = tag.get(1).ok_or(Error::MalformedAuthorizationHashTag)?; hashes.push( - RadrootsBlossomSha256::from_hex(value) - .map_err(|_| RadrootsBlossomError::MalformedAuthorizationHashTag)?, + Sha256::from_hex(value) + .map_err(|_| Error::MalformedAuthorizationHashTag)?, ); } _ => {} @@ -341,9 +327,8 @@ impl RadrootsBlossomParsedAuthorizationClaim { Ok(Self { content, created_at, - action: action.ok_or(RadrootsBlossomError::MissingAuthorizationActionTag)?, - expiration: expiration - .ok_or(RadrootsBlossomError::MissingAuthorizationExpirationTag)?, + action: action.ok_or(Error::MissingAuthorizationActionTag)?, + expiration: expiration.ok_or(Error::MissingAuthorizationExpirationTag)?, server_domains, hashes, }) @@ -351,46 +336,46 @@ impl RadrootsBlossomParsedAuthorizationClaim { pub fn validate( &self, - validation: &RadrootsBlossomAuthorizationValidation, - ) -> Result<RadrootsBlossomValidatedAuthorizationClaim, RadrootsBlossomError> { + validation: &AuthorizationValidation, + ) -> Result<ValidatedAuthorizationClaim, Error> { if self.created_at >= validation.now { - return Err(RadrootsBlossomError::AuthorizationCreatedInFuture); + return Err(Error::AuthorizationCreatedInFuture); } if let Some(max_created_age_seconds) = validation.max_created_age_seconds && validation.now.saturating_sub(self.created_at) > max_created_age_seconds { - return Err(RadrootsBlossomError::AuthorizationStale); + return Err(Error::AuthorizationStale); } let lifetime = self .expiration .checked_sub(self.created_at) .filter(|lifetime| *lifetime > 0) - .ok_or(RadrootsBlossomError::InvalidAuthorizationLifetime)?; + .ok_or(Error::InvalidAuthorizationLifetime)?; if let Some(max_lifetime_seconds) = validation.max_lifetime_seconds && lifetime > max_lifetime_seconds { - return Err(RadrootsBlossomError::InvalidAuthorizationLifetime); + return Err(Error::InvalidAuthorizationLifetime); } if self.expiration <= validation.now { - return Err(RadrootsBlossomError::AuthorizationExpired); + return Err(Error::AuthorizationExpired); } if self.action != validation.target.action() { - return Err(RadrootsBlossomError::AuthorizationActionMismatch); + return Err(Error::AuthorizationActionMismatch); } self.validate_server_scope(validation)?; self.validate_hash_scope(validation.target)?; - Ok(RadrootsBlossomValidatedAuthorizationClaim { + Ok(ValidatedAuthorizationClaim { claim: self.clone(), target: validation.target, target_server: validation.target_server.clone(), }) } - pub fn content(&self) -> &RadrootsBlossomAuthorizationContent { + pub fn content(&self) -> &AuthorizationContent { &self.content } @@ -398,7 +383,7 @@ impl RadrootsBlossomParsedAuthorizationClaim { self.created_at } - pub const fn action(&self) -> RadrootsBlossomAuthorizationAction { + pub const fn action(&self) -> AuthorizationAction { self.action } @@ -406,24 +391,19 @@ impl RadrootsBlossomParsedAuthorizationClaim { self.expiration } - pub fn server_domains(&self) -> &[RadrootsBlossomServerDomain] { + pub fn server_domains(&self) -> &[ServerDomain] { &self.server_domains } - pub fn hashes(&self) -> &[RadrootsBlossomSha256] { + pub fn hashes(&self) -> &[Sha256] { &self.hashes } - fn validate_server_scope( - &self, - validation: &RadrootsBlossomAuthorizationValidation, - ) -> Result<(), RadrootsBlossomError> { + fn validate_server_scope(&self, validation: &AuthorizationValidation) -> Result<(), Error> { if self.server_domains.is_empty() { return match validation.server_scope_requirement { - RadrootsBlossomServerScopeRequirement::OptionalAnyMatch => Ok(()), - RadrootsBlossomServerScopeRequirement::RequiredAnyMatch => { - Err(RadrootsBlossomError::AuthorizationServerRequired) - } + ServerScopeRequirement::OptionalAnyMatch => Ok(()), + ServerScopeRequirement::RequiredAnyMatch => Err(Error::AuthorizationServerRequired), }; } if self @@ -433,20 +413,17 @@ impl RadrootsBlossomParsedAuthorizationClaim { { Ok(()) } else { - Err(RadrootsBlossomError::AuthorizationServerMismatch) + Err(Error::AuthorizationServerMismatch) } } - fn validate_hash_scope( - &self, - target: RadrootsBlossomAuthorizationTarget, - ) -> Result<(), RadrootsBlossomError> { + fn validate_hash_scope(&self, target: AuthorizationTarget) -> Result<(), Error> { let Some(implied_hash) = target.implied_hash() else { return Ok(()); }; if self.hashes.is_empty() { return if target.requires_hash_tag() { - Err(RadrootsBlossomError::AuthorizationHashRequired) + Err(Error::AuthorizationHashRequired) } else { Ok(()) }; @@ -454,32 +431,32 @@ impl RadrootsBlossomParsedAuthorizationClaim { if self.hashes.contains(&implied_hash) { Ok(()) } else { - Err(RadrootsBlossomError::AuthorizationHashMismatch) + Err(Error::AuthorizationHashMismatch) } } } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsBlossomValidatedAuthorizationClaim { - claim: RadrootsBlossomParsedAuthorizationClaim, - target: RadrootsBlossomAuthorizationTarget, - target_server: RadrootsBlossomServerDomain, +pub struct ValidatedAuthorizationClaim { + claim: AuthorizationClaim, + target: AuthorizationTarget, + target_server: ServerDomain, } -impl RadrootsBlossomValidatedAuthorizationClaim { - pub fn claim(&self) -> &RadrootsBlossomParsedAuthorizationClaim { +impl ValidatedAuthorizationClaim { + pub fn claim(&self) -> &AuthorizationClaim { &self.claim } - pub const fn target(&self) -> RadrootsBlossomAuthorizationTarget { + pub const fn target(&self) -> AuthorizationTarget { self.target } - pub fn target_server(&self) -> &RadrootsBlossomServerDomain { + pub fn target_server(&self) -> &ServerDomain { &self.target_server } - pub fn content(&self) -> &RadrootsBlossomAuthorizationContent { + pub fn content(&self) -> &AuthorizationContent { self.claim.content() } @@ -487,7 +464,7 @@ impl RadrootsBlossomValidatedAuthorizationClaim { self.claim.created_at() } - pub const fn action(&self) -> RadrootsBlossomAuthorizationAction { + pub const fn action(&self) -> AuthorizationAction { self.claim.action() } @@ -495,24 +472,24 @@ impl RadrootsBlossomValidatedAuthorizationClaim { self.claim.expiration() } - pub fn server_domains(&self) -> &[RadrootsBlossomServerDomain] { + pub fn server_domains(&self) -> &[ServerDomain] { self.claim.server_domains() } - pub fn hashes(&self) -> &[RadrootsBlossomSha256] { + pub fn hashes(&self) -> &[Sha256] { self.claim.hashes() } } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsBlossomAuthorizationWireParts { +pub struct AuthorizationWireParts { kind: u16, created_at: u64, tags: Vec<Vec<String>>, content: String, } -impl RadrootsBlossomAuthorizationWireParts { +impl AuthorizationWireParts { pub const fn kind(&self) -> u16 { self.kind } @@ -535,35 +512,35 @@ impl RadrootsBlossomAuthorizationWireParts { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsBlossomAuthoredUploadClaim { - wire_parts: RadrootsBlossomAuthorizationWireParts, - content: RadrootsBlossomAuthorizationContent, - server_domain: RadrootsBlossomServerDomain, - sha256: RadrootsBlossomSha256, +pub struct AuthoredUploadClaim { + wire_parts: AuthorizationWireParts, + content: AuthorizationContent, + server_domain: ServerDomain, + sha256: Sha256, expiration: u64, } -impl RadrootsBlossomAuthoredUploadClaim { +impl AuthoredUploadClaim { pub fn new( - content: RadrootsBlossomAuthorizationContent, - server_domain: RadrootsBlossomServerDomain, - sha256: RadrootsBlossomSha256, + content: AuthorizationContent, + server_domain: ServerDomain, + sha256: Sha256, created_at: u64, lifetime_seconds: u64, - ) -> Result<Self, RadrootsBlossomError> { + ) -> Result<Self, Error> { if !(1..=RADROOTS_BLOSSOM_AUTH_MAX_HORIZON_SECONDS).contains(&lifetime_seconds) { - return Err(RadrootsBlossomError::InvalidAuthorizationLifetime); + return Err(Error::InvalidAuthorizationLifetime); } let expiration = created_at .checked_add(lifetime_seconds) - .ok_or(RadrootsBlossomError::AuthorizationTimestampOverflow)?; + .ok_or(Error::AuthorizationTimestampOverflow)?; let tags = vec![ vec!["t".to_string(), "upload".to_string()], vec!["expiration".to_string(), expiration.to_string()], vec!["x".to_string(), sha256.to_hex()], vec!["server".to_string(), server_domain.as_str().to_string()], ]; - let wire_parts = RadrootsBlossomAuthorizationWireParts { + let wire_parts = AuthorizationWireParts { kind: RADROOTS_BLOSSOM_AUTHORIZATION_EVENT_KIND, created_at, tags, @@ -578,15 +555,15 @@ impl RadrootsBlossomAuthoredUploadClaim { }) } - pub fn content(&self) -> &RadrootsBlossomAuthorizationContent { + pub fn content(&self) -> &AuthorizationContent { &self.content } - pub fn server_domain(&self) -> &RadrootsBlossomServerDomain { + pub fn server_domain(&self) -> &ServerDomain { &self.server_domain } - pub const fn sha256(&self) -> RadrootsBlossomSha256 { + pub const fn sha256(&self) -> Sha256 { self.sha256 } @@ -602,22 +579,22 @@ impl RadrootsBlossomAuthoredUploadClaim { self.expiration - self.wire_parts.created_at } - pub fn wire_parts(&self) -> &RadrootsBlossomAuthorizationWireParts { + pub fn wire_parts(&self) -> &AuthorizationWireParts { &self.wire_parts } - pub fn into_wire_parts(self) -> RadrootsBlossomAuthorizationWireParts { + pub fn into_wire_parts(self) -> AuthorizationWireParts { self.wire_parts } } -fn parse_timestamp(value: &str) -> Result<u64, RadrootsBlossomError> { +fn parse_timestamp(value: &str) -> Result<u64, Error> { if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) { - return Err(RadrootsBlossomError::MalformedAuthorizationExpirationTag); + return Err(Error::MalformedAuthorizationExpirationTag); } value .parse() - .map_err(|_| RadrootsBlossomError::MalformedAuthorizationExpirationTag) + .map_err(|_| Error::MalformedAuthorizationExpirationTag) } fn is_canonical_dotted_ipv4(value: &str) -> bool { @@ -639,16 +616,16 @@ mod tests { const NOW: u64 = 1_800_000_000; const HASH_HEX: &str = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"; - fn hash() -> RadrootsBlossomSha256 { - RadrootsBlossomSha256::from_hex(HASH_HEX).unwrap() + fn hash() -> Sha256 { + Sha256::from_hex(HASH_HEX).unwrap() } - fn server() -> RadrootsBlossomServerDomain { - RadrootsBlossomServerDomain::parse("media.example.com").unwrap() + fn server() -> ServerDomain { + ServerDomain::parse("media.example.com").unwrap() } - fn content() -> RadrootsBlossomAuthorizationContent { - RadrootsBlossomAuthorizationContent::parse("Upload Blob").unwrap() + fn content() -> AuthorizationContent { + AuthorizationContent::parse("Upload Blob").unwrap() } fn tags(action: &str, expiration: u64) -> Vec<Vec<String>> { @@ -660,20 +637,15 @@ mod tests { ] } - fn parse_upload() -> RadrootsBlossomParsedAuthorizationClaim { - RadrootsBlossomParsedAuthorizationClaim::parse( - "Upload Blob", - NOW - 10, - &tags("upload", NOW + 60), - ) - .unwrap() + fn parse_upload() -> AuthorizationClaim { + AuthorizationClaim::parse("Upload Blob", NOW - 10, &tags("upload", NOW + 60)).unwrap() } fn validation( - target: RadrootsBlossomAuthorizationTarget, - server_requirement: RadrootsBlossomServerScopeRequirement, - ) -> RadrootsBlossomAuthorizationValidation { - RadrootsBlossomAuthorizationValidation::new( + target: AuthorizationTarget, + server_requirement: ServerScopeRequirement, + ) -> AuthorizationValidation { + AuthorizationValidation::new( target, server(), server_requirement, @@ -686,21 +658,21 @@ mod tests { #[test] fn actions_are_strict_lowercase_wire_verbs() { let cases = [ - ("get", RadrootsBlossomAuthorizationAction::Get), - ("upload", RadrootsBlossomAuthorizationAction::Upload), - ("list", RadrootsBlossomAuthorizationAction::List), - ("delete", RadrootsBlossomAuthorizationAction::Delete), - ("media", RadrootsBlossomAuthorizationAction::Media), + ("get", AuthorizationAction::Get), + ("upload", AuthorizationAction::Upload), + ("list", AuthorizationAction::List), + ("delete", AuthorizationAction::Delete), + ("media", AuthorizationAction::Media), ]; for (wire, action) in cases { - assert_eq!(RadrootsBlossomAuthorizationAction::parse(wire), Ok(action)); + assert_eq!(AuthorizationAction::parse(wire), Ok(action)); assert_eq!(wire.parse(), Ok(action)); assert_eq!(action.as_str(), wire); assert_eq!(action.to_string(), wire); } assert_eq!( - RadrootsBlossomAuthorizationAction::parse("Upload"), - Err(RadrootsBlossomError::InvalidAuthorizationAction) + AuthorizationAction::parse("Upload"), + Err(Error::InvalidAuthorizationAction) ); } @@ -713,13 +685,10 @@ mod tests { "127.0.0.1", "0.0.0.0", ] { - let domain = RadrootsBlossomServerDomain::parse(value).unwrap(); + let domain = ServerDomain::parse(value).unwrap(); assert_eq!(domain.as_str(), value); assert_eq!(domain.to_string(), value); - assert_eq!( - value.parse::<RadrootsBlossomServerDomain>().unwrap(), - domain - ); + assert_eq!(value.parse::<ServerDomain>().unwrap(), domain); } let long_label = format!("{}.example", "a".repeat(64)); @@ -743,8 +712,8 @@ mod tests { &long_domain, ] { assert_eq!( - RadrootsBlossomServerDomain::parse(value), - Err(RadrootsBlossomError::InvalidAuthorizationServerDomain), + ServerDomain::parse(value), + Err(Error::InvalidAuthorizationServerDomain), "{value}" ); } @@ -754,29 +723,22 @@ mod tests { fn content_requires_bounded_trimmed_human_readable_text() { for value in ["", " Upload Blob", "Upload Blob ", "Upload\0Blob", "\u{7f}"] { assert_eq!( - RadrootsBlossomAuthorizationContent::parse(value), - Err(RadrootsBlossomError::InvalidAuthorizationContent) + AuthorizationContent::parse(value), + Err(Error::InvalidAuthorizationContent) ); } assert_eq!( - RadrootsBlossomAuthorizationContent::parse(&"a".repeat(4_097)), - Err(RadrootsBlossomError::InvalidAuthorizationContent) + AuthorizationContent::parse(&"a".repeat(4_097)), + Err(Error::InvalidAuthorizationContent) ); for value in ["Upload\nBlob", "Upload\tBlob", "Upload\rBlob"] { - assert_eq!( - RadrootsBlossomAuthorizationContent::parse(value) - .unwrap() - .as_str(), - value - ); + assert_eq!(AuthorizationContent::parse(value).unwrap().as_str(), value); } - let value = RadrootsBlossomAuthorizationContent::parse("Téléverser l'image").unwrap(); + let value = AuthorizationContent::parse("Téléverser l'image").unwrap(); assert_eq!(value.as_str(), "Téléverser l'image"); assert_eq!(value.to_string(), "Téléverser l'image"); assert_eq!( - "Upload Blob" - .parse::<RadrootsBlossomAuthorizationContent>() - .unwrap(), + "Upload Blob".parse::<AuthorizationContent>().unwrap(), content() ); } @@ -786,38 +748,38 @@ mod tests { let hash = hash(); let cases = [ ( - RadrootsBlossomAuthorizationTarget::GetBlob(hash), - RadrootsBlossomAuthorizationAction::Get, + AuthorizationTarget::GetBlob(hash), + AuthorizationAction::Get, Some(hash), false, ), ( - RadrootsBlossomAuthorizationTarget::Upload(hash), - RadrootsBlossomAuthorizationAction::Upload, + AuthorizationTarget::Upload(hash), + AuthorizationAction::Upload, Some(hash), true, ), ( - RadrootsBlossomAuthorizationTarget::List, - RadrootsBlossomAuthorizationAction::List, + AuthorizationTarget::List, + AuthorizationAction::List, None, false, ), ( - RadrootsBlossomAuthorizationTarget::DeleteBlob(hash), - RadrootsBlossomAuthorizationAction::Delete, + AuthorizationTarget::DeleteBlob(hash), + AuthorizationAction::Delete, Some(hash), true, ), ( - RadrootsBlossomAuthorizationTarget::Mirror(hash), - RadrootsBlossomAuthorizationAction::Upload, + AuthorizationTarget::Mirror(hash), + AuthorizationAction::Upload, Some(hash), true, ), ( - RadrootsBlossomAuthorizationTarget::Media(hash), - RadrootsBlossomAuthorizationAction::Media, + AuthorizationTarget::Media(hash), + AuthorizationAction::Media, Some(hash), true, ), @@ -831,39 +793,36 @@ mod tests { #[test] fn validation_policy_is_bounded_and_exposes_injected_values() { - let target = RadrootsBlossomAuthorizationTarget::Upload(hash()); - let policy = validation( - target, - RadrootsBlossomServerScopeRequirement::RequiredAnyMatch, - ); + let target = AuthorizationTarget::Upload(hash()); + let policy = validation(target, ServerScopeRequirement::RequiredAnyMatch); assert_eq!(policy.target(), target); assert_eq!(policy.target_server(), &server()); assert_eq!( policy.server_scope_requirement(), - RadrootsBlossomServerScopeRequirement::RequiredAnyMatch + ServerScopeRequirement::RequiredAnyMatch ); assert_eq!(policy.now(), NOW); assert_eq!(policy.max_created_age_seconds(), Some(300)); assert_eq!(policy.max_lifetime_seconds(), Some(300)); assert!( - RadrootsBlossomAuthorizationValidation::new( + AuthorizationValidation::new( target, server(), - RadrootsBlossomServerScopeRequirement::OptionalAnyMatch, + ServerScopeRequirement::OptionalAnyMatch, NOW, 0, ) .is_ok() ); assert_eq!( - RadrootsBlossomAuthorizationValidation::new( + AuthorizationValidation::new( target, server(), - RadrootsBlossomServerScopeRequirement::OptionalAnyMatch, + ServerScopeRequirement::OptionalAnyMatch, NOW, 301, ), - Err(RadrootsBlossomError::InvalidAuthorizationCreatedAge) + Err(Error::InvalidAuthorizationCreatedAge) ); } @@ -875,18 +834,14 @@ mod tests { tags[2].push("ignored".to_string()); tags[3].push("ignored".to_string()); tags.push(vec!["server".to_string(), "other.example".to_string()]); - tags.push(vec![ - "x".to_string(), - RadrootsBlossomSha256::digest(b"other").to_hex(), - ]); + tags.push(vec!["x".to_string(), Sha256::digest(b"other").to_hex()]); tags.push(vec!["unknown".to_string()]); tags.push(Vec::new()); - let parsed = - RadrootsBlossomParsedAuthorizationClaim::parse("Upload Blob", NOW - 10, &tags).unwrap(); + let parsed = AuthorizationClaim::parse("Upload Blob", NOW - 10, &tags).unwrap(); assert_eq!(parsed.content(), &content()); assert_eq!(parsed.created_at(), NOW - 10); - assert_eq!(parsed.action(), RadrootsBlossomAuthorizationAction::Upload); + assert_eq!(parsed.action(), AuthorizationAction::Upload); assert_eq!(parsed.expiration(), NOW + 60); assert_eq!(parsed.server_domains().len(), 2); assert_eq!(parsed.hashes().len(), 2); @@ -897,11 +852,11 @@ mod tests { let cases = [ ( vec![vec!["expiration".to_string(), (NOW + 60).to_string()]], - RadrootsBlossomError::MissingAuthorizationActionTag, + Error::MissingAuthorizationActionTag, ), ( vec![vec!["t".to_string(), "upload".to_string()]], - RadrootsBlossomError::MissingAuthorizationExpirationTag, + Error::MissingAuthorizationExpirationTag, ), ( vec![ @@ -909,7 +864,7 @@ mod tests { vec!["t".to_string(), "upload".to_string()], vec!["expiration".to_string(), (NOW + 60).to_string()], ], - RadrootsBlossomError::DuplicateAuthorizationActionTag, + Error::DuplicateAuthorizationActionTag, ), ( vec![ @@ -917,21 +872,21 @@ mod tests { vec!["expiration".to_string(), (NOW + 60).to_string()], vec!["expiration".to_string(), (NOW + 60).to_string()], ], - RadrootsBlossomError::DuplicateAuthorizationExpirationTag, + Error::DuplicateAuthorizationExpirationTag, ), ( vec![ vec!["t".to_string()], vec!["expiration".to_string(), (NOW + 60).to_string()], ], - RadrootsBlossomError::MalformedAuthorizationActionTag, + Error::MalformedAuthorizationActionTag, ), ( vec![ vec!["t".to_string(), "upload".to_string()], vec!["expiration".to_string()], ], - RadrootsBlossomError::MalformedAuthorizationExpirationTag, + Error::MalformedAuthorizationExpirationTag, ), ( vec![ @@ -939,7 +894,7 @@ mod tests { vec!["expiration".to_string(), (NOW + 60).to_string()], vec!["server".to_string()], ], - RadrootsBlossomError::MalformedAuthorizationServerTag, + Error::MalformedAuthorizationServerTag, ), ( vec![ @@ -947,12 +902,12 @@ mod tests { vec!["expiration".to_string(), (NOW + 60).to_string()], vec!["x".to_string()], ], - RadrootsBlossomError::MalformedAuthorizationHashTag, + Error::MalformedAuthorizationHashTag, ), ]; for (tags, expected) in cases { assert_eq!( - RadrootsBlossomParsedAuthorizationClaim::parse("Upload Blob", NOW, &tags), + AuthorizationClaim::parse("Upload Blob", NOW, &tags), Err(expected) ); } @@ -963,109 +918,80 @@ mod tests { vec!["expiration".to_string(), expiration.to_string()], ]; assert_eq!( - RadrootsBlossomParsedAuthorizationClaim::parse("Upload Blob", NOW, &tags), - Err(RadrootsBlossomError::MalformedAuthorizationExpirationTag) + AuthorizationClaim::parse("Upload Blob", NOW, &tags), + Err(Error::MalformedAuthorizationExpirationTag) ); } let mut invalid = tags("Upload", NOW + 60); assert_eq!( - RadrootsBlossomParsedAuthorizationClaim::parse("Upload Blob", NOW, &invalid), - Err(RadrootsBlossomError::MalformedAuthorizationActionTag) + AuthorizationClaim::parse("Upload Blob", NOW, &invalid), + Err(Error::MalformedAuthorizationActionTag) ); invalid = tags("upload", NOW + 60); invalid[3][1] = "Media.example".to_string(); assert_eq!( - RadrootsBlossomParsedAuthorizationClaim::parse("Upload Blob", NOW, &invalid), - Err(RadrootsBlossomError::MalformedAuthorizationServerTag) + AuthorizationClaim::parse("Upload Blob", NOW, &invalid), + Err(Error::MalformedAuthorizationServerTag) ); invalid = tags("upload", NOW + 60); invalid[2][1] = "not-a-hash".to_string(); assert_eq!( - RadrootsBlossomParsedAuthorizationClaim::parse("Upload Blob", NOW, &invalid), - Err(RadrootsBlossomError::MalformedAuthorizationHashTag) + AuthorizationClaim::parse("Upload Blob", NOW, &invalid), + Err(Error::MalformedAuthorizationHashTag) ); } #[test] fn upload_validation_enforces_time_action_and_required_scopes() { - let target = RadrootsBlossomAuthorizationTarget::Upload(hash()); - let required = validation( - target, - RadrootsBlossomServerScopeRequirement::RequiredAnyMatch, - ); + let target = AuthorizationTarget::Upload(hash()); + let required = validation(target, ServerScopeRequirement::RequiredAnyMatch); let validated = parse_upload().validate(&required).unwrap(); assert_eq!(validated.claim(), &parse_upload()); assert_eq!(validated.target(), target); assert_eq!(validated.target_server(), &server()); assert_eq!(validated.content(), &content()); assert_eq!(validated.created_at(), NOW - 10); - assert_eq!( - validated.action(), - RadrootsBlossomAuthorizationAction::Upload - ); + assert_eq!(validated.action(), AuthorizationAction::Upload); assert_eq!(validated.expiration(), NOW + 60); assert_eq!(validated.server_domains(), &[server()]); assert_eq!(validated.hashes(), &[hash()]); let time_cases = [ - ( - NOW, - NOW + 60, - RadrootsBlossomError::AuthorizationCreatedInFuture, - ), - ( - NOW + 1, - NOW + 60, - RadrootsBlossomError::AuthorizationCreatedInFuture, - ), - (NOW - 301, NOW - 1, RadrootsBlossomError::AuthorizationStale), - ( - NOW - 10, - NOW - 10, - RadrootsBlossomError::InvalidAuthorizationLifetime, - ), - ( - NOW - 10, - NOW + 291, - RadrootsBlossomError::InvalidAuthorizationLifetime, - ), - (NOW - 10, NOW, RadrootsBlossomError::AuthorizationExpired), + (NOW, NOW + 60, Error::AuthorizationCreatedInFuture), + (NOW + 1, NOW + 60, Error::AuthorizationCreatedInFuture), + (NOW - 301, NOW - 1, Error::AuthorizationStale), + (NOW - 10, NOW - 10, Error::InvalidAuthorizationLifetime), + (NOW - 10, NOW + 291, Error::InvalidAuthorizationLifetime), + (NOW - 10, NOW, Error::AuthorizationExpired), ]; for (created_at, expiration, expected) in time_cases { - let parsed = RadrootsBlossomParsedAuthorizationClaim::parse( - "Upload Blob", - created_at, - &tags("upload", expiration), - ) - .unwrap(); + let parsed = + AuthorizationClaim::parse("Upload Blob", created_at, &tags("upload", expiration)) + .unwrap(); assert_eq!(parsed.validate(&required), Err(expected)); } - let wrong_action = RadrootsBlossomParsedAuthorizationClaim::parse( - "List Blobs", - NOW - 10, - &tags("list", NOW + 60), - ) - .unwrap(); + let wrong_action = + AuthorizationClaim::parse("List Blobs", NOW - 10, &tags("list", NOW + 60)).unwrap(); assert_eq!( wrong_action.validate(&required), - Err(RadrootsBlossomError::AuthorizationActionMismatch) + Err(Error::AuthorizationActionMismatch) ); } #[test] fn bud11_validation_keeps_radroots_replay_limits_out_of_protocol_semantics() { - let target = RadrootsBlossomAuthorizationTarget::List; - let policy = RadrootsBlossomAuthorizationValidation::bud11(target, server(), NOW); + let target = AuthorizationTarget::List; + let policy = AuthorizationValidation::bud11(target, server(), NOW); assert_eq!(policy.max_created_age_seconds(), None); assert_eq!(policy.max_lifetime_seconds(), None); assert_eq!( policy.server_scope_requirement(), - RadrootsBlossomServerScopeRequirement::OptionalAnyMatch + ServerScopeRequirement::OptionalAnyMatch ); - let claim = RadrootsBlossomParsedAuthorizationClaim::parse( + let claim = AuthorizationClaim::parse( "List archived blobs", NOW - 1_000, &[ @@ -1079,48 +1005,31 @@ mod tests { #[test] fn server_scope_uses_optional_or_required_any_match() { - let target = RadrootsBlossomAuthorizationTarget::Upload(hash()); - let optional = validation( - target, - RadrootsBlossomServerScopeRequirement::OptionalAnyMatch, - ); - let required = validation( - target, - RadrootsBlossomServerScopeRequirement::RequiredAnyMatch, - ); + let target = AuthorizationTarget::Upload(hash()); + let optional = validation(target, ServerScopeRequirement::OptionalAnyMatch); + let required = validation(target, ServerScopeRequirement::RequiredAnyMatch); - let no_server = RadrootsBlossomParsedAuthorizationClaim::parse( - "Upload Blob", - NOW - 10, - &tags("upload", NOW + 60)[..3], - ) - .unwrap(); + let no_server = + AuthorizationClaim::parse("Upload Blob", NOW - 10, &tags("upload", NOW + 60)[..3]) + .unwrap(); assert!(no_server.validate(&optional).is_ok()); assert_eq!( no_server.validate(&required), - Err(RadrootsBlossomError::AuthorizationServerRequired) + Err(Error::AuthorizationServerRequired) ); let mut mismatched_tags = tags("upload", NOW + 60); mismatched_tags[3][1] = "other.example".to_string(); - let mismatch = RadrootsBlossomParsedAuthorizationClaim::parse( - "Upload Blob", - NOW - 10, - &mismatched_tags, - ) - .unwrap(); + let mismatch = + AuthorizationClaim::parse("Upload Blob", NOW - 10, &mismatched_tags).unwrap(); assert_eq!( mismatch.validate(&optional), - Err(RadrootsBlossomError::AuthorizationServerMismatch) + Err(Error::AuthorizationServerMismatch) ); mismatched_tags.push(vec!["server".to_string(), "media.example.com".to_string()]); - let any_match = RadrootsBlossomParsedAuthorizationClaim::parse( - "Upload Blob", - NOW - 10, - &mismatched_tags, - ) - .unwrap(); + let any_match = + AuthorizationClaim::parse("Upload Blob", NOW - 10, &mismatched_tags).unwrap(); assert!(any_match.validate(&required).is_ok()); } @@ -1131,17 +1040,16 @@ mod tests { vec!["t".to_string(), "get".to_string()], vec!["expiration".to_string(), (NOW + 60).to_string()], ]; - let get = RadrootsBlossomParsedAuthorizationClaim::parse("Get Blob", NOW - 10, &no_scope) - .unwrap(); + let get = AuthorizationClaim::parse("Get Blob", NOW - 10, &no_scope).unwrap(); assert!( get.validate(&validation( - RadrootsBlossomAuthorizationTarget::GetBlob(hash), - RadrootsBlossomServerScopeRequirement::OptionalAnyMatch, + AuthorizationTarget::GetBlob(hash), + ServerScopeRequirement::OptionalAnyMatch, )) .is_ok() ); - let list = RadrootsBlossomParsedAuthorizationClaim::parse( + let list = AuthorizationClaim::parse( "List Blobs", NOW - 10, &[ @@ -1153,13 +1061,13 @@ mod tests { .unwrap(); assert!( list.validate(&validation( - RadrootsBlossomAuthorizationTarget::List, - RadrootsBlossomServerScopeRequirement::OptionalAnyMatch, + AuthorizationTarget::List, + ServerScopeRequirement::OptionalAnyMatch, )) .is_ok() ); - let missing_required = RadrootsBlossomParsedAuthorizationClaim::parse( + let missing_required = AuthorizationClaim::parse( "Upload Blob", NOW - 10, &[ @@ -1170,14 +1078,14 @@ mod tests { .unwrap(); assert_eq!( missing_required.validate(&validation( - RadrootsBlossomAuthorizationTarget::Upload(hash), - RadrootsBlossomServerScopeRequirement::OptionalAnyMatch, + AuthorizationTarget::Upload(hash), + ServerScopeRequirement::OptionalAnyMatch, )), - Err(RadrootsBlossomError::AuthorizationHashRequired) + Err(Error::AuthorizationHashRequired) ); - let wrong_hash = RadrootsBlossomSha256::digest(b"wrong"); - let wrong_scope = RadrootsBlossomParsedAuthorizationClaim::parse( + let wrong_hash = Sha256::digest(b"wrong"); + let wrong_scope = AuthorizationClaim::parse( "Get Blob", NOW - 10, &[ @@ -1189,37 +1097,31 @@ mod tests { .unwrap(); assert_eq!( wrong_scope.validate(&validation( - RadrootsBlossomAuthorizationTarget::GetBlob(hash), - RadrootsBlossomServerScopeRequirement::OptionalAnyMatch, + AuthorizationTarget::GetBlob(hash), + ServerScopeRequirement::OptionalAnyMatch, )), - Err(RadrootsBlossomError::AuthorizationHashMismatch) + Err(Error::AuthorizationHashMismatch) ); let mut any_tags = tags("delete", NOW + 60); any_tags[2][1] = wrong_hash.to_hex(); any_tags.push(vec!["x".to_string(), HASH_HEX.to_string()]); - let any_match = - RadrootsBlossomParsedAuthorizationClaim::parse("Delete Blob", NOW - 10, &any_tags) - .unwrap(); + let any_match = AuthorizationClaim::parse("Delete Blob", NOW - 10, &any_tags).unwrap(); for target in [ - RadrootsBlossomAuthorizationTarget::DeleteBlob(hash), - RadrootsBlossomAuthorizationTarget::Mirror(hash), - RadrootsBlossomAuthorizationTarget::Media(hash), + AuthorizationTarget::DeleteBlob(hash), + AuthorizationTarget::Mirror(hash), + AuthorizationTarget::Media(hash), ] { let action = target.action(); let mut action_tags = any_tags.clone(); action_tags[0][1] = action.as_str().to_string(); - let parsed = RadrootsBlossomParsedAuthorizationClaim::parse( - "Authorized Operation", - NOW - 10, - &action_tags, - ) - .unwrap(); + let parsed = + AuthorizationClaim::parse("Authorized Operation", NOW - 10, &action_tags).unwrap(); assert!( parsed .validate(&validation( target, - RadrootsBlossomServerScopeRequirement::RequiredAnyMatch, + ServerScopeRequirement::RequiredAnyMatch, )) .is_ok() ); @@ -1229,8 +1131,7 @@ mod tests { #[test] fn strict_authored_upload_claim_emits_canonical_wire_parts() { - let claim = - RadrootsBlossomAuthoredUploadClaim::new(content(), server(), hash(), NOW, 60).unwrap(); + let claim = AuthoredUploadClaim::new(content(), server(), hash(), NOW, 60).unwrap(); assert_eq!(claim.content(), &content()); assert_eq!(claim.server_domain(), &server()); assert_eq!(claim.sha256(), hash()); @@ -1255,20 +1156,20 @@ mod tests { fn strict_authored_upload_claim_rejects_bad_lifetime_and_overflow() { for lifetime in [0, 301] { assert_eq!( - RadrootsBlossomAuthoredUploadClaim::new(content(), server(), hash(), NOW, lifetime,), - Err(RadrootsBlossomError::InvalidAuthorizationLifetime) + AuthoredUploadClaim::new(content(), server(), hash(), NOW, lifetime,), + Err(Error::InvalidAuthorizationLifetime) ); } assert_eq!( - RadrootsBlossomAuthoredUploadClaim::new(content(), server(), hash(), u64::MAX, 1,), - Err(RadrootsBlossomError::AuthorizationTimestampOverflow) + AuthoredUploadClaim::new(content(), server(), hash(), u64::MAX, 1,), + Err(Error::AuthorizationTimestampOverflow) ); } #[test] fn validation_enforces_exact_time_boundaries() { let boundary_tags = tags("upload", NOW + 1); - let parsed = RadrootsBlossomParsedAuthorizationClaim::parse( + let parsed = AuthorizationClaim::parse( "Upload Blob", NOW - (RADROOTS_BLOSSOM_AUTH_MAX_CREATED_AGE_SECONDS - 1), &boundary_tags, @@ -1277,21 +1178,21 @@ mod tests { assert!( parsed .validate(&validation( - RadrootsBlossomAuthorizationTarget::Upload(hash()), - RadrootsBlossomServerScopeRequirement::RequiredAnyMatch, + AuthorizationTarget::Upload(hash()), + ServerScopeRequirement::RequiredAnyMatch, )) .is_ok() ); - let zero_age = RadrootsBlossomAuthorizationValidation::new( - RadrootsBlossomAuthorizationTarget::Upload(hash()), + let zero_age = AuthorizationValidation::new( + AuthorizationTarget::Upload(hash()), server(), - RadrootsBlossomServerScopeRequirement::RequiredAnyMatch, + ServerScopeRequirement::RequiredAnyMatch, NOW, 0, ) .unwrap(); - let created_now = RadrootsBlossomParsedAuthorizationClaim::parse( + let created_now = AuthorizationClaim::parse( "Upload Blob", NOW, &tags("upload", NOW + RADROOTS_BLOSSOM_AUTH_MAX_HORIZON_SECONDS), @@ -1299,7 +1200,7 @@ mod tests { .unwrap(); assert_eq!( created_now.validate(&zero_age), - Err(RadrootsBlossomError::AuthorizationCreatedInFuture) + Err(Error::AuthorizationCreatedInFuture) ); } } diff --git a/crates/blossom/src/descriptor.rs b/crates/blossom/src/descriptor.rs @@ -1,105 +1,32 @@ -#[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, + Error, MediaType, Sha256, + url::{ApprovedBlobUrl, BlobUrl}, }; 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, +pub struct BlobDescriptor { + url: BlobUrl, + sha256: Sha256, size: u64, - media_type: RadrootsBlossomMediaType, + media_type: MediaType, uploaded: u64, } -impl RadrootsBlossomBlobDescriptor { +impl BlobDescriptor { pub fn new( - url: RadrootsBlossomBlobUrl, - sha256: RadrootsBlossomSha256, + url: BlobUrl, + sha256: Sha256, size: u64, - media_type: RadrootsBlossomMediaType, + media_type: MediaType, uploaded: u64, - ) -> Result<Self, RadrootsBlossomError> { + ) -> Result<Self, Error> { if url.hash_path().extension().is_none() { - return Err(RadrootsBlossomError::DescriptorExtensionRequired); + return Err(Error::DescriptorExtensionRequired); } if url.hash_path().hash() != sha256 { - return Err(RadrootsBlossomError::DescriptorHashMismatch); + return Err(Error::DescriptorHashMismatch); } Ok(Self { url, @@ -110,11 +37,11 @@ impl RadrootsBlossomBlobDescriptor { }) } - pub fn url(&self) -> &RadrootsBlossomBlobUrl { + pub fn url(&self) -> &BlobUrl { &self.url } - pub const fn sha256(&self) -> RadrootsBlossomSha256 { + pub const fn sha256(&self) -> Sha256 { self.sha256 } @@ -122,7 +49,7 @@ impl RadrootsBlossomBlobDescriptor { self.size } - pub fn media_type(&self) -> &RadrootsBlossomMediaType { + pub fn media_type(&self) -> &MediaType { &self.media_type } @@ -130,11 +57,9 @@ impl RadrootsBlossomBlobDescriptor { self.uploaded } - pub fn approve_reference( - self, - ) -> Result<RadrootsBlossomApprovedDescriptor, RadrootsBlossomError> { + pub fn approve_reference(self) -> Result<ApprovedDescriptor, Error> { let approved_url = self.url.clone().approve()?; - Ok(RadrootsBlossomApprovedDescriptor { + Ok(ApprovedDescriptor { descriptor: self, approved_url, }) @@ -144,27 +69,27 @@ impl RadrootsBlossomBlobDescriptor { #[cfg(feature = "serde")] #[derive(serde::Serialize)] struct DescriptorRef<'a> { - url: &'a RadrootsBlossomBlobUrl, - sha256: RadrootsBlossomSha256, + url: &'a BlobUrl, + sha256: Sha256, size: u64, #[serde(rename = "type")] - media_type: &'a RadrootsBlossomMediaType, + media_type: &'a MediaType, uploaded: u64, } #[cfg(feature = "serde")] #[derive(serde::Deserialize)] struct DescriptorWire { - url: RadrootsBlossomBlobUrl, - sha256: RadrootsBlossomSha256, + url: BlobUrl, + sha256: Sha256, size: u64, #[serde(rename = "type")] - media_type: RadrootsBlossomMediaType, + media_type: MediaType, uploaded: u64, } #[cfg(feature = "serde")] -impl serde::Serialize for RadrootsBlossomBlobDescriptor { +impl serde::Serialize for BlobDescriptor { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, @@ -181,7 +106,7 @@ impl serde::Serialize for RadrootsBlossomBlobDescriptor { } #[cfg(feature = "serde")] -impl<'de> serde::Deserialize<'de> for RadrootsBlossomBlobDescriptor { +impl<'de> serde::Deserialize<'de> for BlobDescriptor { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, @@ -199,28 +124,28 @@ impl<'de> serde::Deserialize<'de> for RadrootsBlossomBlobDescriptor { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsBlossomApprovedDescriptor { - descriptor: RadrootsBlossomBlobDescriptor, - approved_url: RadrootsBlossomApprovedBlobUrl, +pub struct ApprovedDescriptor { + descriptor: BlobDescriptor, + approved_url: ApprovedBlobUrl, } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsBlossomByteCommitment { - sha256: RadrootsBlossomSha256, +pub struct ByteCommitment { + sha256: Sha256, size: u64, - media_type: RadrootsBlossomMediaType, + media_type: MediaType, } -impl RadrootsBlossomByteCommitment { - pub fn from_bytes(bytes: &[u8], media_type: RadrootsBlossomMediaType) -> Self { +impl ByteCommitment { + pub fn from_bytes(bytes: &[u8], media_type: MediaType) -> Self { Self { - sha256: RadrootsBlossomSha256::digest(bytes), + sha256: Sha256::digest(bytes), size: bytes.len() as u64, media_type, } } - pub const fn sha256(&self) -> RadrootsBlossomSha256 { + pub const fn sha256(&self) -> Sha256 { self.sha256 } @@ -228,51 +153,50 @@ impl RadrootsBlossomByteCommitment { self.size } - pub fn media_type(&self) -> &RadrootsBlossomMediaType { + pub fn media_type(&self) -> &MediaType { &self.media_type } } -impl RadrootsBlossomApprovedDescriptor { - pub fn descriptor(&self) -> &RadrootsBlossomBlobDescriptor { +impl ApprovedDescriptor { + pub fn descriptor(&self) -> &BlobDescriptor { &self.descriptor } - pub fn url(&self) -> &RadrootsBlossomApprovedBlobUrl { + pub fn url(&self) -> &ApprovedBlobUrl { &self.approved_url } - pub fn into_descriptor(self) -> RadrootsBlossomBlobDescriptor { + pub fn into_descriptor(self) -> BlobDescriptor { 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()); + approved_media_type: &MediaType, + ) -> Result<ByteVerifiedDescriptor, Error> { + let commitment = ByteCommitment::from_bytes(bytes, approved_media_type.clone()); self.verify_commitment(&commitment) } pub fn verify_commitment( self, - commitment: &RadrootsBlossomByteCommitment, - ) -> Result<RadrootsBlossomByteVerifiedDescriptor, RadrootsBlossomError> { + commitment: &ByteCommitment, + ) -> Result<ByteVerifiedDescriptor, Error> { if self.descriptor.size != commitment.size { - return Err(RadrootsBlossomError::BlobSizeMismatch { + return Err(Error::BlobSizeMismatch { expected: self.descriptor.size, actual: commitment.size, }); } if self.descriptor.media_type != commitment.media_type { - return Err(RadrootsBlossomError::BlobMediaTypeMismatch); + return Err(Error::BlobMediaTypeMismatch); } if self.descriptor.sha256 != commitment.sha256 { - return Err(RadrootsBlossomError::BlobHashMismatch); + return Err(Error::BlobHashMismatch); } - Ok(RadrootsBlossomByteVerifiedDescriptor(self)) + Ok(ByteVerifiedDescriptor(self)) } } @@ -280,18 +204,18 @@ impl RadrootsBlossomApprovedDescriptor { /// /// This state does not attest that a network upload occurred. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsBlossomByteVerifiedDescriptor(RadrootsBlossomApprovedDescriptor); +pub struct ByteVerifiedDescriptor(ApprovedDescriptor); -impl RadrootsBlossomByteVerifiedDescriptor { - pub fn descriptor(&self) -> &RadrootsBlossomBlobDescriptor { +impl ByteVerifiedDescriptor { + pub fn descriptor(&self) -> &BlobDescriptor { self.0.descriptor() } - pub fn url(&self) -> &RadrootsBlossomApprovedBlobUrl { + pub fn url(&self) -> &ApprovedBlobUrl { self.0.url() } - pub const fn sha256(&self) -> RadrootsBlossomSha256 { + pub const fn sha256(&self) -> Sha256 { self.0.descriptor.sha256 } @@ -299,11 +223,11 @@ impl RadrootsBlossomByteVerifiedDescriptor { self.0.descriptor.size } - pub fn media_type(&self) -> &RadrootsBlossomMediaType { + pub fn media_type(&self) -> &MediaType { &self.0.descriptor.media_type } - pub fn into_descriptor(self) -> RadrootsBlossomBlobDescriptor { + pub fn into_descriptor(self) -> BlobDescriptor { self.0.into_descriptor() } } @@ -312,16 +236,17 @@ impl RadrootsBlossomByteVerifiedDescriptor { mod tests { use super::*; use alloc::{format, string::ToString}; + use core::str::FromStr; 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(), + fn descriptor(origin: &str, bytes: &[u8], media_type: &str) -> BlobDescriptor { + let hash = Sha256::digest(bytes); + BlobDescriptor::new( + BlobUrl::parse(&format!("{origin}/{hash}.txt")).unwrap(), hash, u64::try_from(bytes.len()).unwrap(), - RadrootsBlossomMediaType::parse(media_type).unwrap(), + MediaType::parse(media_type).unwrap(), 1_725_105_921, ) .unwrap() @@ -329,19 +254,14 @@ mod tests { #[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(); + let lower = MediaType::parse("image/svg+xml; profile=web; charset=UTF-8").unwrap(); + let upper = MediaType::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 - ); + assert_eq!(serde_json::from_str::<MediaType>(&json).unwrap(), lower); } #[test] @@ -357,32 +277,26 @@ mod tests { "image/png ", "image/png\n", ] { - assert_eq!( - RadrootsBlossomMediaType::parse(value), - Err(RadrootsBlossomError::InvalidMediaType) - ); + assert_eq!(MediaType::parse(value), Err(Error::InvalidMediaType)); } - assert!(serde_json::from_str::<RadrootsBlossomMediaType>("42").is_err()); + assert!(serde_json::from_str::<MediaType>("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 hash = Sha256::from_hex(HELLO_HASH).unwrap(); + let media_type = MediaType::parse("text/plain").unwrap(); let no_extension = - RadrootsBlossomBlobUrl::parse(&format!("https://cdn.example.com/{HELLO_HASH}")) - .unwrap(); + BlobUrl::parse(&format!("https://cdn.example.com/{HELLO_HASH}")).unwrap(); assert_eq!( - RadrootsBlossomBlobDescriptor::new(no_extension, hash, 5, media_type.clone(), 1), - Err(RadrootsBlossomError::DescriptorExtensionRequired) + BlobDescriptor::new(no_extension, hash, 5, media_type.clone(), 1), + Err(Error::DescriptorExtensionRequired) ); - let wrong_hash = RadrootsBlossomSha256::digest(b"wrong"); - let url = - RadrootsBlossomBlobUrl::parse(&format!("https://cdn.example.com/{HELLO_HASH}.txt")) - .unwrap(); + let wrong_hash = Sha256::digest(b"wrong"); + let url = BlobUrl::parse(&format!("https://cdn.example.com/{HELLO_HASH}.txt")).unwrap(); assert_eq!( - RadrootsBlossomBlobDescriptor::new(url, wrong_hash, 5, media_type, 1), - Err(RadrootsBlossomError::DescriptorHashMismatch) + BlobDescriptor::new(url, wrong_hash, 5, media_type, 1), + Err(Error::DescriptorHashMismatch) ); } @@ -391,7 +305,7 @@ mod tests { 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(); + let parsed: BlobDescriptor = 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); @@ -408,13 +322,13 @@ mod tests { 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()); + assert!(serde_json::from_str::<BlobDescriptor>(&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()); + let media_type = MediaType::parse("text/plain").unwrap(); + let commitment = ByteCommitment::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); @@ -439,26 +353,26 @@ mod tests { .approve_reference() .unwrap() .verify_bytes(b"hell", &media_type), - Err(RadrootsBlossomError::BlobSizeMismatch { + Err(Error::BlobSizeMismatch { expected: 5, actual: 4, }) ); - let image_type = RadrootsBlossomMediaType::parse("image/png").unwrap(); + let image_type = MediaType::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) + Err(Error::BlobMediaTypeMismatch) ); - let hash_mismatch = RadrootsBlossomBlobDescriptor::new( - RadrootsBlossomBlobUrl::parse(&format!( + let hash_mismatch = BlobDescriptor::new( + BlobUrl::parse(&format!( "https://cdn.example.com/{}.txt", - RadrootsBlossomSha256::digest(b"world") + Sha256::digest(b"world") )) .unwrap(), - RadrootsBlossomSha256::digest(b"world"), + Sha256::digest(b"world"), 5, media_type.clone(), 1, @@ -468,13 +382,13 @@ mod tests { .unwrap(); assert_eq!( hash_mismatch.verify_bytes(b"hello", &media_type), - Err(RadrootsBlossomError::BlobHashMismatch) + Err(Error::BlobHashMismatch) ); } #[test] fn empty_blob_can_be_verified_with_explicit_default_media_type() { - let media_type = RadrootsBlossomMediaType::parse("application/octet-stream").unwrap(); + let media_type = MediaType::parse("application/octet-stream").unwrap(); let verified = descriptor("http://localhost:3000", b"", "application/octet-stream") .approve_reference() .unwrap() @@ -487,7 +401,7 @@ mod tests { fn public_http_descriptor_cannot_advance_to_approved() { assert_eq!( descriptor("http://cdn.example.com", b"hello", "text/plain").approve_reference(), - Err(RadrootsBlossomError::InsecureBlobUrl) + Err(Error::InsecureBlobUrl) ); } } diff --git a/crates/blossom/src/error.rs b/crates/blossom/src/error.rs @@ -2,7 +2,7 @@ use core::fmt; #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] -pub enum RadrootsBlossomError { +pub enum Error { InvalidSha256, InvalidFileExtension, InvalidHashPath, @@ -42,7 +42,7 @@ pub enum RadrootsBlossomError { AuthorizationHashMismatch, } -impl RadrootsBlossomError { +impl Error { pub const fn code(&self) -> &'static str { match self { Self::InvalidSha256 => "invalid_sha256", @@ -86,7 +86,7 @@ impl RadrootsBlossomError { } } -impl fmt::Display for RadrootsBlossomError { +impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::InvalidSha256 => { @@ -187,56 +187,56 @@ impl fmt::Display for RadrootsBlossomError { } #[cfg(feature = "std")] -impl std::error::Error for RadrootsBlossomError {} +impl std::error::Error for Error {} #[cfg(test)] mod tests { - use super::RadrootsBlossomError; + use super::Error; 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 { + Error::InvalidSha256, + Error::InvalidFileExtension, + Error::InvalidHashPath, + Error::InvalidBlobUrl, + Error::UnsupportedBlobUrlScheme, + Error::BlobUrlCredentialsForbidden, + Error::BlobUrlQueryForbidden, + Error::BlobUrlFragmentForbidden, + Error::InsecureBlobUrl, + Error::DescriptorExtensionRequired, + Error::DescriptorHashMismatch, + Error::InvalidMediaType, + Error::BlobHashMismatch, + Error::BlobSizeMismatch { expected: 1, actual: 2, }, - RadrootsBlossomError::BlobMediaTypeMismatch, - RadrootsBlossomError::InvalidAuthorizationContent, - RadrootsBlossomError::InvalidAuthorizationAction, - RadrootsBlossomError::InvalidAuthorizationServerDomain, - RadrootsBlossomError::MissingAuthorizationActionTag, - RadrootsBlossomError::DuplicateAuthorizationActionTag, - RadrootsBlossomError::MalformedAuthorizationActionTag, - RadrootsBlossomError::MissingAuthorizationExpirationTag, - RadrootsBlossomError::DuplicateAuthorizationExpirationTag, - RadrootsBlossomError::MalformedAuthorizationExpirationTag, - RadrootsBlossomError::MalformedAuthorizationServerTag, - RadrootsBlossomError::MalformedAuthorizationHashTag, - RadrootsBlossomError::InvalidAuthorizationCreatedAge, - RadrootsBlossomError::InvalidAuthorizationLifetime, - RadrootsBlossomError::AuthorizationTimestampOverflow, - RadrootsBlossomError::AuthorizationCreatedInFuture, - RadrootsBlossomError::AuthorizationStale, - RadrootsBlossomError::AuthorizationExpired, - RadrootsBlossomError::AuthorizationActionMismatch, - RadrootsBlossomError::AuthorizationServerRequired, - RadrootsBlossomError::AuthorizationServerMismatch, - RadrootsBlossomError::AuthorizationHashRequired, - RadrootsBlossomError::AuthorizationHashMismatch, + Error::BlobMediaTypeMismatch, + Error::InvalidAuthorizationContent, + Error::InvalidAuthorizationAction, + Error::InvalidAuthorizationServerDomain, + Error::MissingAuthorizationActionTag, + Error::DuplicateAuthorizationActionTag, + Error::MalformedAuthorizationActionTag, + Error::MissingAuthorizationExpirationTag, + Error::DuplicateAuthorizationExpirationTag, + Error::MalformedAuthorizationExpirationTag, + Error::MalformedAuthorizationServerTag, + Error::MalformedAuthorizationHashTag, + Error::InvalidAuthorizationCreatedAge, + Error::InvalidAuthorizationLifetime, + Error::AuthorizationTimestampOverflow, + Error::AuthorizationCreatedInFuture, + Error::AuthorizationStale, + Error::AuthorizationExpired, + Error::AuthorizationActionMismatch, + Error::AuthorizationServerRequired, + Error::AuthorizationServerMismatch, + Error::AuthorizationHashRequired, + Error::AuthorizationHashMismatch, ]; for error in errors { assert!(!error.code().is_empty()); @@ -248,112 +248,112 @@ mod tests { fn authorization_error_codes_and_messages_are_stable() { let cases = [ ( - RadrootsBlossomError::InvalidAuthorizationContent, + Error::InvalidAuthorizationContent, "invalid_authorization_content", "Blossom authorization content must be bounded human-readable text", ), ( - RadrootsBlossomError::InvalidAuthorizationAction, + Error::InvalidAuthorizationAction, "invalid_authorization_action", "invalid Blossom authorization action", ), ( - RadrootsBlossomError::InvalidAuthorizationServerDomain, + Error::InvalidAuthorizationServerDomain, "invalid_authorization_server_domain", "invalid Blossom authorization server domain", ), ( - RadrootsBlossomError::MissingAuthorizationActionTag, + Error::MissingAuthorizationActionTag, "missing_authorization_action_tag", "Blossom authorization is missing a t action tag", ), ( - RadrootsBlossomError::DuplicateAuthorizationActionTag, + Error::DuplicateAuthorizationActionTag, "duplicate_authorization_action_tag", "Blossom authorization has more than one t action tag", ), ( - RadrootsBlossomError::MalformedAuthorizationActionTag, + Error::MalformedAuthorizationActionTag, "malformed_authorization_action_tag", "malformed Blossom authorization t action tag", ), ( - RadrootsBlossomError::MissingAuthorizationExpirationTag, + Error::MissingAuthorizationExpirationTag, "missing_authorization_expiration_tag", "Blossom authorization is missing an expiration tag", ), ( - RadrootsBlossomError::DuplicateAuthorizationExpirationTag, + Error::DuplicateAuthorizationExpirationTag, "duplicate_authorization_expiration_tag", "Blossom authorization has more than one expiration tag", ), ( - RadrootsBlossomError::MalformedAuthorizationExpirationTag, + Error::MalformedAuthorizationExpirationTag, "malformed_authorization_expiration_tag", "malformed Blossom authorization expiration tag", ), ( - RadrootsBlossomError::MalformedAuthorizationServerTag, + Error::MalformedAuthorizationServerTag, "malformed_authorization_server_tag", "malformed Blossom authorization server tag", ), ( - RadrootsBlossomError::MalformedAuthorizationHashTag, + Error::MalformedAuthorizationHashTag, "malformed_authorization_hash_tag", "malformed Blossom authorization x hash tag", ), ( - RadrootsBlossomError::InvalidAuthorizationCreatedAge, + Error::InvalidAuthorizationCreatedAge, "invalid_authorization_created_age", "Blossom authorization maximum created age must not exceed 300 seconds", ), ( - RadrootsBlossomError::InvalidAuthorizationLifetime, + Error::InvalidAuthorizationLifetime, "invalid_authorization_lifetime", "Blossom authorization lifetime must be between 1 and 300 seconds", ), ( - RadrootsBlossomError::AuthorizationTimestampOverflow, + Error::AuthorizationTimestampOverflow, "authorization_timestamp_overflow", "Blossom authorization expiration timestamp overflows u64", ), ( - RadrootsBlossomError::AuthorizationCreatedInFuture, + Error::AuthorizationCreatedInFuture, "authorization_created_in_future", "Blossom authorization must be created in the past", ), ( - RadrootsBlossomError::AuthorizationStale, + Error::AuthorizationStale, "authorization_stale", "Blossom authorization is outside the accepted creation-age window", ), ( - RadrootsBlossomError::AuthorizationExpired, + Error::AuthorizationExpired, "authorization_expired", "Blossom authorization is expired", ), ( - RadrootsBlossomError::AuthorizationActionMismatch, + Error::AuthorizationActionMismatch, "authorization_action_mismatch", "Blossom authorization action does not match the target endpoint", ), ( - RadrootsBlossomError::AuthorizationServerRequired, + Error::AuthorizationServerRequired, "authorization_server_required", "Blossom authorization requires a server scope", ), ( - RadrootsBlossomError::AuthorizationServerMismatch, + Error::AuthorizationServerMismatch, "authorization_server_mismatch", "Blossom authorization does not include the target server", ), ( - RadrootsBlossomError::AuthorizationHashRequired, + Error::AuthorizationHashRequired, "authorization_hash_required", "Blossom authorization requires an x hash scope", ), ( - RadrootsBlossomError::AuthorizationHashMismatch, + Error::AuthorizationHashMismatch, "authorization_hash_mismatch", "Blossom authorization does not include the target blob hash", ), diff --git a/crates/blossom/src/hash.rs b/crates/blossom/src/hash.rs @@ -1,31 +1,31 @@ use alloc::string::{String, ToString}; use core::{fmt, str::FromStr}; -use sha2::{Digest, Sha256}; +use sha2::{Digest, Sha256 as Sha256Hasher}; -use crate::error::RadrootsBlossomError; +use crate::error::Error; 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]); +pub struct Sha256([u8; SHA256_BYTES]); -impl RadrootsBlossomSha256 { +impl Sha256 { pub const fn from_bytes(bytes: [u8; SHA256_BYTES]) -> Self { Self(bytes) } pub fn digest(bytes: &[u8]) -> Self { - let digest = Sha256::digest(bytes); + let digest = Sha256Hasher::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> { + pub fn from_hex(value: &str) -> Result<Self, Error> { if value.len() != SHA256_HEX_LENGTH { - return Err(RadrootsBlossomError::InvalidSha256); + return Err(Error::InvalidSha256); } let bytes = value.as_bytes(); @@ -52,14 +52,14 @@ impl RadrootsBlossomSha256 { } } -impl fmt::Display for RadrootsBlossomSha256 { +impl fmt::Display for Sha256 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.to_hex()) } } -impl FromStr for RadrootsBlossomSha256 { - type Err = RadrootsBlossomError; +impl FromStr for Sha256 { + type Err = Error; fn from_str(value: &str) -> Result<Self, Self::Err> { Self::from_hex(value) @@ -67,7 +67,7 @@ impl FromStr for RadrootsBlossomSha256 { } #[cfg(feature = "serde")] -impl serde::Serialize for RadrootsBlossomSha256 { +impl serde::Serialize for Sha256 { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, @@ -77,7 +77,7 @@ impl serde::Serialize for RadrootsBlossomSha256 { } #[cfg(feature = "serde")] -impl<'de> serde::Deserialize<'de> for RadrootsBlossomSha256 { +impl<'de> serde::Deserialize<'de> for Sha256 { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, @@ -88,17 +88,17 @@ impl<'de> serde::Deserialize<'de> for RadrootsBlossomSha256 { } #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct RadrootsBlossomFileExtension(String); +pub struct FileExtension(String); -impl RadrootsBlossomFileExtension { - pub fn parse(value: &str) -> Result<Self, RadrootsBlossomError> { +impl FileExtension { + pub fn parse(value: &str) -> Result<Self, Error> { 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); + return Err(Error::InvalidFileExtension); } Ok(Self(value.to_string())) } @@ -108,14 +108,14 @@ impl RadrootsBlossomFileExtension { } } -impl fmt::Display for RadrootsBlossomFileExtension { +impl fmt::Display for FileExtension { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) } } -impl FromStr for RadrootsBlossomFileExtension { - type Err = RadrootsBlossomError; +impl FromStr for FileExtension { + type Err = Error; fn from_str(value: &str) -> Result<Self, Self::Err> { Self::parse(value) @@ -123,7 +123,7 @@ impl FromStr for RadrootsBlossomFileExtension { } #[cfg(feature = "serde")] -impl serde::Serialize for RadrootsBlossomFileExtension { +impl serde::Serialize for FileExtension { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, @@ -133,7 +133,7 @@ impl serde::Serialize for RadrootsBlossomFileExtension { } #[cfg(feature = "serde")] -impl<'de> serde::Deserialize<'de> for RadrootsBlossomFileExtension { +impl<'de> serde::Deserialize<'de> for FileExtension { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, @@ -144,53 +144,46 @@ impl<'de> serde::Deserialize<'de> for RadrootsBlossomFileExtension { } #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct RadrootsBlossomHashPath { - hash: RadrootsBlossomSha256, - extension: Option<RadrootsBlossomFileExtension>, +pub struct HashPath { + hash: Sha256, + extension: Option<FileExtension>, } -impl RadrootsBlossomHashPath { - pub fn new( - hash: RadrootsBlossomSha256, - extension: Option<RadrootsBlossomFileExtension>, - ) -> Self { +impl HashPath { + pub fn new(hash: Sha256, extension: Option<FileExtension>) -> Self { Self { hash, extension } } - pub fn parse(value: &str) -> Result<Self, RadrootsBlossomError> { - let segment = value - .strip_prefix('/') - .ok_or(RadrootsBlossomError::InvalidHashPath)?; + pub fn parse(value: &str) -> Result<Self, Error> { + let segment = value.strip_prefix('/').ok_or(Error::InvalidHashPath)?; if segment.contains('/') || segment.contains('\\') || segment.contains('%') || segment.contains('?') || segment.contains('#') { - return Err(RadrootsBlossomError::InvalidHashPath); + return Err(Error::InvalidHashPath); } let hash = segment .get(..SHA256_HEX_LENGTH) - .ok_or(RadrootsBlossomError::InvalidHashPath) - .and_then(RadrootsBlossomSha256::from_hex)?; + .ok_or(Error::InvalidHashPath) + .and_then(Sha256::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)?) + let value = suffix.strip_prefix('.').ok_or(Error::InvalidHashPath)?; + Some(FileExtension::parse(value)?) }; Ok(Self::new(hash, extension)) } - pub const fn hash(&self) -> RadrootsBlossomSha256 { + pub const fn hash(&self) -> Sha256 { self.hash } - pub fn extension(&self) -> Option<&RadrootsBlossomFileExtension> { + pub fn extension(&self) -> Option<&FileExtension> { self.extension.as_ref() } @@ -212,14 +205,14 @@ impl RadrootsBlossomHashPath { } } -impl fmt::Display for RadrootsBlossomHashPath { +impl fmt::Display for HashPath { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.to_path()) } } -impl FromStr for RadrootsBlossomHashPath { - type Err = RadrootsBlossomError; +impl FromStr for HashPath { + type Err = Error; fn from_str(value: &str) -> Result<Self, Self::Err> { Self::parse(value) @@ -227,7 +220,7 @@ impl FromStr for RadrootsBlossomHashPath { } #[cfg(feature = "serde")] -impl serde::Serialize for RadrootsBlossomHashPath { +impl serde::Serialize for HashPath { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, @@ -237,7 +230,7 @@ impl serde::Serialize for RadrootsBlossomHashPath { } #[cfg(feature = "serde")] -impl<'de> serde::Deserialize<'de> for RadrootsBlossomHashPath { +impl<'de> serde::Deserialize<'de> for HashPath { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, @@ -247,11 +240,11 @@ impl<'de> serde::Deserialize<'de> for RadrootsBlossomHashPath { } } -const fn decode_nibble(byte: u8) -> Result<u8, RadrootsBlossomError> { +const fn decode_nibble(byte: u8) -> Result<u8, Error> { match byte { b'0'..=b'9' => Ok(byte - b'0'), b'a'..=b'f' => Ok(byte - b'a' + 10), - _ => Err(RadrootsBlossomError::InvalidSha256), + _ => Err(Error::InvalidSha256), } } @@ -264,21 +257,18 @@ mod tests { #[test] fn hash_round_trips_known_digest_and_serde() { - let hash = RadrootsBlossomSha256::digest(b""); + let hash = Sha256::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)); + assert_eq!(Sha256::from_str(EMPTY_SHA256), Ok(hash)); let json = serde_json::to_string(&hash).unwrap(); - assert_eq!( - serde_json::from_str::<RadrootsBlossomSha256>(&json).unwrap(), - hash - ); + assert_eq!(serde_json::from_str::<Sha256>(&json).unwrap(), hash); } #[test] fn hash_preserves_verified_digest_bytes() { let bytes = [0xabu8; SHA256_BYTES]; - let hash = RadrootsBlossomSha256::from_bytes(bytes); + let hash = Sha256::from_bytes(bytes); assert_eq!(hash.as_bytes(), &bytes); assert_eq!(hash.to_hex(), "ab".repeat(SHA256_BYTES)); @@ -294,23 +284,20 @@ mod tests { &"A".repeat(64), &"z".repeat(64), ] { - assert_eq!( - RadrootsBlossomSha256::from_hex(value), - Err(RadrootsBlossomError::InvalidSha256) - ); + assert_eq!(Sha256::from_hex(value), Err(Error::InvalidSha256)); } - assert!(serde_json::from_str::<RadrootsBlossomSha256>("12").is_err()); + assert!(serde_json::from_str::<Sha256>("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(); + let extension = value.parse::<FileExtension>().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(), + serde_json::from_str::<FileExtension>(&json).unwrap(), extension ); } @@ -320,30 +307,27 @@ mod tests { 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) + FileExtension::parse(value), + Err(Error::InvalidFileExtension) ); } - assert!(serde_json::from_str::<RadrootsBlossomFileExtension>("false").is_err()); + assert!(serde_json::from_str::<FileExtension>("false").is_err()); } #[test] fn hash_path_round_trips_with_and_without_extension() { let bare = format!("/{EMPTY_SHA256}"); - let parsed = RadrootsBlossomHashPath::parse(&bare).unwrap(); + let parsed = HashPath::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(); + let parsed = HashPath::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 - ); + assert_eq!(serde_json::from_str::<HashPath>(&json).unwrap(), parsed); } #[test] @@ -360,8 +344,8 @@ mod tests { &format!("/{EMPTY_SHA256}png"), &format!("/{EMPTY_SHA256}."), ] { - assert!(RadrootsBlossomHashPath::parse(path).is_err(), "{path}"); + assert!(HashPath::parse(path).is_err(), "{path}"); } - assert!(serde_json::from_str::<RadrootsBlossomHashPath>("null").is_err()); + assert!(serde_json::from_str::<HashPath>("null").is_err()); } } diff --git a/crates/blossom/src/lib.rs b/crates/blossom/src/lib.rs @@ -11,21 +11,10 @@ pub mod hash; pub mod media_type; pub mod url; -pub use authorization::{ - RADROOTS_BLOSSOM_AUTH_CONTENT_MAX_BYTES, RADROOTS_BLOSSOM_AUTH_MAX_CREATED_AGE_SECONDS, - RADROOTS_BLOSSOM_AUTH_MAX_HORIZON_SECONDS, RADROOTS_BLOSSOM_AUTHORIZATION_EVENT_KIND, - RadrootsBlossomAuthoredUploadClaim, RadrootsBlossomAuthorizationAction, - RadrootsBlossomAuthorizationContent, RadrootsBlossomAuthorizationTarget, - RadrootsBlossomAuthorizationValidation, RadrootsBlossomAuthorizationWireParts, - RadrootsBlossomParsedAuthorizationClaim, RadrootsBlossomServerDomain, - RadrootsBlossomServerScopeRequirement, RadrootsBlossomValidatedAuthorizationClaim, -}; -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"; +pub use authorization::AuthorizationClaim; +pub use descriptor::BlobDescriptor; +pub use descriptor::ByteVerifiedDescriptor; +pub use error::Error; +pub use hash::Sha256; +pub use media_type::MediaType; +pub use url::BlobUrl; diff --git a/crates/blossom/src/media_type.rs b/crates/blossom/src/media_type.rs @@ -1,3 +1,75 @@ //! Canonical Blossom media-type values. -pub use crate::descriptor::RadrootsBlossomMediaType; +#[cfg(feature = "serde")] +use alloc::string::String; +use alloc::{collections::BTreeMap, vec::Vec}; +use core::{fmt, str::FromStr}; +use mediatype::{MediaTypeBuf, ReadParams}; + +use crate::Error; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct MediaType(MediaTypeBuf); + +impl MediaType { + pub fn parse(value: &str) -> Result<Self, Error> { + let parsed = MediaTypeBuf::from_str(value).map_err(|_| Error::InvalidMediaType)?; + if parsed.as_str() != value || parsed.ty().as_str() == "*" || parsed.subty().as_str() == "*" + { + return Err(Error::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(Error::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 MediaType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +impl FromStr for MediaType { + type Err = Error; + + fn from_str(value: &str) -> Result<Self, Self::Err> { + Self::parse(value) + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for MediaType { + 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 MediaType { + 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) + } +} diff --git a/crates/blossom/src/url.rs b/crates/blossom/src/url.rs @@ -5,41 +5,41 @@ use core::{fmt, str::FromStr}; use unicode_general_category::{GeneralCategory, get_general_category}; use url_nostd::{Host, Url}; -use crate::{error::RadrootsBlossomError, hash::RadrootsBlossomHashPath}; +use crate::{error::Error, hash::HashPath}; #[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct RadrootsBlossomBlobUrl { +pub struct BlobUrl { url: Url, - hash_path: RadrootsBlossomHashPath, + hash_path: HashPath, } -impl RadrootsBlossomBlobUrl { - pub fn parse(value: &str) -> Result<Self, RadrootsBlossomError> { +impl BlobUrl { + pub fn parse(value: &str) -> Result<Self, Error> { if !value.contains("://") || !raw_url_text_is_valid(value) { - return Err(RadrootsBlossomError::InvalidBlobUrl); + return Err(Error::InvalidBlobUrl); } - let url = Url::parse(value).map_err(|_| RadrootsBlossomError::InvalidBlobUrl)?; + let url = Url::parse(value).map_err(|_| Error::InvalidBlobUrl)?; if url.scheme() != "http" && url.scheme() != "https" { - return Err(RadrootsBlossomError::UnsupportedBlobUrlScheme); + return Err(Error::UnsupportedBlobUrlScheme); } if raw_authority(value).contains('@') { - return Err(RadrootsBlossomError::BlobUrlCredentialsForbidden); + return Err(Error::BlobUrlCredentialsForbidden); } if url.query().is_some() { - return Err(RadrootsBlossomError::BlobUrlQueryForbidden); + return Err(Error::BlobUrlQueryForbidden); } if url.fragment().is_some() { - return Err(RadrootsBlossomError::BlobUrlFragmentForbidden); + return Err(Error::BlobUrlFragmentForbidden); } if value.contains('\\') || value.contains('%') || value.contains("/./") || value.contains("/../") { - return Err(RadrootsBlossomError::InvalidBlobUrl); + return Err(Error::InvalidBlobUrl); } validate_authority(value, &url)?; - let hash_path = RadrootsBlossomHashPath::parse(url.path())?; + let hash_path = HashPath::parse(url.path())?; Ok(Self { url, hash_path }) } @@ -61,7 +61,7 @@ impl RadrootsBlossomBlobUrl { self.url.port() } - pub fn hash_path(&self) -> &RadrootsBlossomHashPath { + pub fn hash_path(&self) -> &HashPath { &self.hash_path } @@ -73,22 +73,22 @@ impl RadrootsBlossomBlobUrl { self.url.scheme() == "http" && self.url.host().is_some_and(host_is_loopback) } - pub fn approve(self) -> Result<RadrootsBlossomApprovedBlobUrl, RadrootsBlossomError> { + pub fn approve(self) -> Result<ApprovedBlobUrl, Error> { if !self.is_https() && !self.is_loopback_http() { - return Err(RadrootsBlossomError::InsecureBlobUrl); + return Err(Error::InsecureBlobUrl); } - Ok(RadrootsBlossomApprovedBlobUrl(self)) + Ok(ApprovedBlobUrl(self)) } } -impl fmt::Display for RadrootsBlossomBlobUrl { +impl fmt::Display for BlobUrl { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) } } -impl FromStr for RadrootsBlossomBlobUrl { - type Err = RadrootsBlossomError; +impl FromStr for BlobUrl { + type Err = Error; fn from_str(value: &str) -> Result<Self, Self::Err> { Self::parse(value) @@ -96,7 +96,7 @@ impl FromStr for RadrootsBlossomBlobUrl { } #[cfg(feature = "serde")] -impl serde::Serialize for RadrootsBlossomBlobUrl { +impl serde::Serialize for BlobUrl { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, @@ -106,7 +106,7 @@ impl serde::Serialize for RadrootsBlossomBlobUrl { } #[cfg(feature = "serde")] -impl<'de> serde::Deserialize<'de> for RadrootsBlossomBlobUrl { +impl<'de> serde::Deserialize<'de> for BlobUrl { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, @@ -117,10 +117,10 @@ impl<'de> serde::Deserialize<'de> for RadrootsBlossomBlobUrl { } #[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct RadrootsBlossomApprovedBlobUrl(RadrootsBlossomBlobUrl); +pub struct ApprovedBlobUrl(BlobUrl); -impl RadrootsBlossomApprovedBlobUrl { - pub fn as_blob_url(&self) -> &RadrootsBlossomBlobUrl { +impl ApprovedBlobUrl { + pub fn as_blob_url(&self) -> &BlobUrl { &self.0 } @@ -128,12 +128,12 @@ impl RadrootsBlossomApprovedBlobUrl { self.0.as_str() } - pub fn into_blob_url(self) -> RadrootsBlossomBlobUrl { + pub fn into_blob_url(self) -> BlobUrl { self.0 } } -impl fmt::Display for RadrootsBlossomApprovedBlobUrl { +impl fmt::Display for ApprovedBlobUrl { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } @@ -152,23 +152,23 @@ fn host_is_loopback(host: Host<&str>) -> bool { } } -fn validate_authority(value: &str, url: &Url) -> Result<(), RadrootsBlossomError> { +fn validate_authority(value: &str, url: &Url) -> Result<(), Error> { 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), + Ok(0) | Err(_) => return Err(Error::InvalidBlobUrl), } } match url.host() { Some(Host::Domain(_)) if !raw_dns_host_is_valid(raw_host) => { - return Err(RadrootsBlossomError::InvalidBlobUrl); + return Err(Error::InvalidBlobUrl); } Some(Host::Ipv4(address)) if raw_host != address.to_string() => { - return Err(RadrootsBlossomError::InvalidBlobUrl); + return Err(Error::InvalidBlobUrl); } Some(_) => {} - None => return Err(RadrootsBlossomError::InvalidBlobUrl), + None => return Err(Error::InvalidBlobUrl), } Ok(()) } @@ -249,7 +249,7 @@ mod tests { #[test] fn https_reference_is_structural_and_approved() { - let parsed = RadrootsBlossomBlobUrl::parse(&url("https://cdn.example.com")).unwrap(); + let parsed = BlobUrl::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); @@ -271,7 +271,7 @@ mod tests { "http://[::1]:3000", "http://[0:0:0:0:0:0:0:1]", ] { - let parsed = RadrootsBlossomBlobUrl::parse(&url(origin)).unwrap(); + let parsed = BlobUrl::parse(&url(origin)).unwrap(); assert!(parsed.is_loopback_http(), "{origin}"); let canonical = parsed.as_str().to_string(); let approved = parsed.approve().unwrap(); @@ -290,14 +290,10 @@ mod tests { "http://10.0.0.2", "http://[::]", ] { - let parsed = RadrootsBlossomBlobUrl::parse(&url(origin)).unwrap(); + let parsed = BlobUrl::parse(&url(origin)).unwrap(); assert!(!parsed.is_https()); assert!(!parsed.is_loopback_http()); - assert_eq!( - parsed.approve(), - Err(RadrootsBlossomError::InsecureBlobUrl), - "{origin}" - ); + assert_eq!(parsed.approve(), Err(Error::InsecureBlobUrl), "{origin}"); } } @@ -306,39 +302,35 @@ mod tests { let cases = [ ( url("ftp://cdn.example.com"), - RadrootsBlossomError::UnsupportedBlobUrlScheme, + Error::UnsupportedBlobUrlScheme, ), ( format!("https://user@cdn.example.com/{HASH}.txt"), - RadrootsBlossomError::BlobUrlCredentialsForbidden, + Error::BlobUrlCredentialsForbidden, ), ( format!("https://:password@cdn.example.com/{HASH}.txt"), - RadrootsBlossomError::BlobUrlCredentialsForbidden, + Error::BlobUrlCredentialsForbidden, ), ( format!("https://@cdn.example.com/{HASH}.txt"), - RadrootsBlossomError::BlobUrlCredentialsForbidden, + Error::BlobUrlCredentialsForbidden, ), ( format!("https://:@cdn.example.com/{HASH}.txt"), - RadrootsBlossomError::BlobUrlCredentialsForbidden, + Error::BlobUrlCredentialsForbidden, ), ( format!("https://cdn.example.com/{HASH}.txt?a=1"), - RadrootsBlossomError::BlobUrlQueryForbidden, + Error::BlobUrlQueryForbidden, ), ( format!("https://cdn.example.com/{HASH}.txt#x"), - RadrootsBlossomError::BlobUrlFragmentForbidden, + Error::BlobUrlFragmentForbidden, ), ]; for (value, expected) in cases { - assert_eq!( - RadrootsBlossomBlobUrl::parse(&value), - Err(expected), - "{value}" - ); + assert_eq!(BlobUrl::parse(&value), Err(expected), "{value}"); } } @@ -352,37 +344,28 @@ mod tests { format!("https://cdn.example.com/{HASH}/../{HASH}.txt"), format!("https://cdn.example.com/{HASH}\\x"), ] { - assert!(RadrootsBlossomBlobUrl::parse(&value).is_err(), "{value}"); + assert!(BlobUrl::parse(&value).is_err(), "{value}"); } } #[test] fn blob_url_serde_revalidates_structure() { - let parsed = RadrootsBlossomBlobUrl::parse(&url("https://cdn.example.com")).unwrap(); + let parsed = BlobUrl::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_eq!(serde_json::from_str::<BlobUrl>(&json).unwrap(), parsed); + assert!(serde_json::from_str::<BlobUrl>("false").is_err()); assert!( - serde_json::from_str::<RadrootsBlossomBlobUrl>( - "\"https://cdn.example.com/not-a-hash.png\"" - ) - .is_err() + serde_json::from_str::<BlobUrl>("\"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()); + assert_eq!(BlobUrl::from_str("not a url"), Err(Error::InvalidBlobUrl)); + assert!(BlobUrl::parse(&format!("https:///{HASH}.txt")).is_err()); + assert!(BlobUrl::parse(&url("https://cdn.example.com:0")).is_err()); + assert!(BlobUrl::parse(&url("https://cdn.example.com:00")).is_err()); + assert!(BlobUrl::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"), @@ -392,8 +375,8 @@ mod tests { format!("https://media\u{2060}.example/{HASH}.txt"), ] { assert_eq!( - RadrootsBlossomBlobUrl::parse(&value), - Err(RadrootsBlossomError::InvalidBlobUrl), + BlobUrl::parse(&value), + Err(Error::InvalidBlobUrl), "{value:?}" ); } @@ -404,8 +387,8 @@ mod tests { "https://2130706433", ] { assert_eq!( - RadrootsBlossomBlobUrl::parse(&url(origin)), - Err(RadrootsBlossomError::InvalidBlobUrl), + BlobUrl::parse(&url(origin)), + Err(Error::InvalidBlobUrl), "{origin}" ); } @@ -423,16 +406,16 @@ mod tests { "https://example.", ] { assert_eq!( - RadrootsBlossomBlobUrl::parse(&url(origin)), - Err(RadrootsBlossomError::InvalidBlobUrl), + BlobUrl::parse(&url(origin)), + Err(Error::InvalidBlobUrl), "{origin}" ); } let label_too_long = "a".repeat(64); assert_eq!( - RadrootsBlossomBlobUrl::parse(&url(&format!("https://{label_too_long}.example"))), - Err(RadrootsBlossomError::InvalidBlobUrl) + BlobUrl::parse(&url(&format!("https://{label_too_long}.example"))), + Err(Error::InvalidBlobUrl) ); let host_too_long = format!( "{}.{}.{}.{}", @@ -443,8 +426,8 @@ mod tests { ); assert!(host_too_long.len() > 253); assert_eq!( - RadrootsBlossomBlobUrl::parse(&url(&format!("https://{host_too_long}"))), - Err(RadrootsBlossomError::InvalidBlobUrl) + BlobUrl::parse(&url(&format!("https://{host_too_long}"))), + Err(Error::InvalidBlobUrl) ); let maximum_host = format!( @@ -455,7 +438,7 @@ mod tests { "d".repeat(61) ); assert_eq!(maximum_host.len(), 253); - assert!(RadrootsBlossomBlobUrl::parse(&url(&format!("https://{maximum_host}"))).is_ok()); - assert!(RadrootsBlossomBlobUrl::parse(&url("https://xn--mdia-9oa.example")).is_ok()); + assert!(BlobUrl::parse(&url(&format!("https://{maximum_host}"))).is_ok()); + assert!(BlobUrl::parse(&url("https://xn--mdia-9oa.example")).is_ok()); } } diff --git a/crates/blossom/tests/bud11_conformance.rs b/crates/blossom/tests/bud11_conformance.rs @@ -1,9 +1,10 @@ use radroots_blossom::{ - RADROOTS_BLOSSOM_AUTHORIZATION_EVENT_KIND, RadrootsBlossomAuthoredUploadClaim, - RadrootsBlossomAuthorizationAction, RadrootsBlossomAuthorizationContent, - RadrootsBlossomAuthorizationTarget, RadrootsBlossomAuthorizationValidation, - RadrootsBlossomParsedAuthorizationClaim, RadrootsBlossomServerDomain, - RadrootsBlossomServerScopeRequirement, RadrootsBlossomSha256, + Sha256, + authorization::{ + AuthoredUploadClaim, AuthorizationAction, AuthorizationClaim, AuthorizationContent, + AuthorizationTarget, AuthorizationValidation, RADROOTS_BLOSSOM_AUTHORIZATION_EVENT_KIND, + ServerDomain, ServerScopeRequirement, + }, }; use serde::Deserialize; use serde_json::Value; @@ -87,7 +88,7 @@ fn execute(vector: &Vector) { } fn action_parse_valid(vector: &Vector) { - let action = RadrootsBlossomAuthorizationAction::parse(input_str(vector, "action")) + let action = AuthorizationAction::parse(input_str(vector, "action")) .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); assert_eq!( action.as_str(), @@ -104,13 +105,13 @@ fn action_parse_valid(vector: &Vector) { } fn action_parse_invalid(vector: &Vector) { - let error = RadrootsBlossomAuthorizationAction::parse(input_str(vector, "action")) + let error = AuthorizationAction::parse(input_str(vector, "action")) .expect_err("invalid action vector must fail"); assert_error(vector, error.code()); } fn server_domain_parse_valid(vector: &Vector) { - let domain = RadrootsBlossomServerDomain::parse(input_str(vector, "domain")) + let domain = ServerDomain::parse(input_str(vector, "domain")) .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); assert_eq!( domain.as_str(), @@ -127,13 +128,13 @@ fn server_domain_parse_valid(vector: &Vector) { } fn server_domain_parse_invalid(vector: &Vector) { - let error = RadrootsBlossomServerDomain::parse(input_str(vector, "domain")) + let error = ServerDomain::parse(input_str(vector, "domain")) .expect_err("invalid server-domain vector must fail"); assert_error(vector, error.code()); } fn content_parse_valid(vector: &Vector) { - let content = RadrootsBlossomAuthorizationContent::parse(input_str(vector, "content")) + let content = AuthorizationContent::parse(input_str(vector, "content")) .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); assert_eq!( content.as_str(), @@ -150,7 +151,7 @@ fn content_parse_valid(vector: &Vector) { } fn content_parse_invalid(vector: &Vector) { - let error = RadrootsBlossomAuthorizationContent::parse(input_str(vector, "content")) + let error = AuthorizationContent::parse(input_str(vector, "content")) .expect_err("invalid content vector must fail"); assert_error(vector, error.code()); } @@ -253,13 +254,11 @@ fn authored_upload_invalid(vector: &Vector) { assert_error(vector, error.code()); } -fn authored_upload( - vector: &Vector, -) -> Result<RadrootsBlossomAuthoredUploadClaim, radroots_blossom::RadrootsBlossomError> { - let content = RadrootsBlossomAuthorizationContent::parse(input_str(vector, "content"))?; - let server = RadrootsBlossomServerDomain::parse(input_str(vector, "server_domain"))?; - let hash = RadrootsBlossomSha256::from_hex(input_str(vector, "hash"))?; - RadrootsBlossomAuthoredUploadClaim::new( +fn authored_upload(vector: &Vector) -> Result<AuthoredUploadClaim, radroots_blossom::Error> { + let content = AuthorizationContent::parse(input_str(vector, "content"))?; + let server = ServerDomain::parse(input_str(vector, "server_domain"))?; + let hash = Sha256::from_hex(input_str(vector, "hash"))?; + AuthoredUploadClaim::new( content, server, hash, @@ -268,23 +267,21 @@ fn authored_upload( ) } -fn try_parse_claim( - input: &Value, -) -> Result<RadrootsBlossomParsedAuthorizationClaim, radroots_blossom::RadrootsBlossomError> { +fn try_parse_claim(input: &Value) -> Result<AuthorizationClaim, radroots_blossom::Error> { let tags: Vec<Vec<String>> = serde_json::from_value(input["tags"].clone()).expect("claim tags must be string arrays"); - RadrootsBlossomParsedAuthorizationClaim::parse( + AuthorizationClaim::parse( value_str(input, "content"), value_u64(input, "created_at"), &tags, ) } -fn parse_claim(input: &Value, id: &str) -> RadrootsBlossomParsedAuthorizationClaim { +fn parse_claim(input: &Value, id: &str) -> AuthorizationClaim { try_parse_claim(input).unwrap_or_else(|error| panic!("{id} claim parse failed: {error}")) } -fn assert_claim(vector: &Vector, claim: &RadrootsBlossomParsedAuthorizationClaim) { +fn assert_claim(vector: &Vector, claim: &AuthorizationClaim) { assert_eq!( claim.content().as_str(), expected_str(vector, "content"), @@ -324,17 +321,15 @@ fn assert_claim(vector: &Vector, claim: &RadrootsBlossomParsedAuthorizationClaim assert_eq!(hashes, expected_strings(vector, "hashes"), "{}", vector.id); } -fn build_validation( - vector: &Vector, -) -> Result<RadrootsBlossomAuthorizationValidation, radroots_blossom::RadrootsBlossomError> { +fn build_validation(vector: &Vector) -> Result<AuthorizationValidation, radroots_blossom::Error> { let target = parse_target(&vector.input["target"]); - let server = RadrootsBlossomServerDomain::parse(input_str(vector, "server_domain"))?; + let server = ServerDomain::parse(input_str(vector, "server_domain"))?; let server_scope = match input_str(vector, "server_scope") { - "optional_any_match" => RadrootsBlossomServerScopeRequirement::OptionalAnyMatch, - "required_any_match" => RadrootsBlossomServerScopeRequirement::RequiredAnyMatch, + "optional_any_match" => ServerScopeRequirement::OptionalAnyMatch, + "required_any_match" => ServerScopeRequirement::RequiredAnyMatch, value => panic!("{} has unsupported server scope {value}", vector.id), }; - RadrootsBlossomAuthorizationValidation::new( + AuthorizationValidation::new( target, server, server_scope, @@ -343,18 +338,15 @@ fn build_validation( ) } -fn parse_target(input: &Value) -> RadrootsBlossomAuthorizationTarget { - let hash = || { - RadrootsBlossomSha256::from_hex(value_str(input, "hash")) - .expect("target hash must be valid") - }; +fn parse_target(input: &Value) -> AuthorizationTarget { + let hash = || Sha256::from_hex(value_str(input, "hash")).expect("target hash must be valid"); match value_str(input, "type") { - "get_blob" => RadrootsBlossomAuthorizationTarget::GetBlob(hash()), - "upload" => RadrootsBlossomAuthorizationTarget::Upload(hash()), - "list" => RadrootsBlossomAuthorizationTarget::List, - "delete_blob" => RadrootsBlossomAuthorizationTarget::DeleteBlob(hash()), - "mirror" => RadrootsBlossomAuthorizationTarget::Mirror(hash()), - "media" => RadrootsBlossomAuthorizationTarget::Media(hash()), + "get_blob" => AuthorizationTarget::GetBlob(hash()), + "upload" => AuthorizationTarget::Upload(hash()), + "list" => AuthorizationTarget::List, + "delete_blob" => AuthorizationTarget::DeleteBlob(hash()), + "mirror" => AuthorizationTarget::Mirror(hash()), + "media" => AuthorizationTarget::Media(hash()), value => panic!("unsupported BUD-11 target {value}"), } } diff --git a/crates/blossom/tests/conformance.rs b/crates/blossom/tests/conformance.rs @@ -1,7 +1,4 @@ -use radroots_blossom::{ - RadrootsBlossomBlobDescriptor, RadrootsBlossomBlobUrl, RadrootsBlossomHashPath, - RadrootsBlossomMediaType, RadrootsBlossomSha256, -}; +use radroots_blossom::{BlobDescriptor, BlobUrl, MediaType, Sha256, hash::HashPath}; use serde::Deserialize; use serde_json::Value; use std::{borrow::Cow, fs, path::Path}; @@ -89,7 +86,7 @@ fn execute(vector: &Vector) { fn sha256_digest(vector: &Vector) { let bytes = input_bytes(vector); assert_eq!( - RadrootsBlossomSha256::digest(&bytes).to_string(), + Sha256::digest(&bytes).to_string(), expected_str(vector, "sha256"), "{}", vector.id @@ -97,7 +94,7 @@ fn sha256_digest(vector: &Vector) { } fn sha256_parse_valid(vector: &Vector) { - let parsed = RadrootsBlossomSha256::from_hex(input_str(vector, "sha256")) + let parsed = Sha256::from_hex(input_str(vector, "sha256")) .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); assert_eq!( parsed.to_string(), @@ -108,13 +105,13 @@ fn sha256_parse_valid(vector: &Vector) { } fn sha256_parse_invalid(vector: &Vector) { - let error = RadrootsBlossomSha256::from_hex(input_str(vector, "sha256")) + let error = Sha256::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")) + let parsed = HashPath::parse(input_str(vector, "path")) .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); assert_eq!( parsed.hash().to_string(), @@ -131,8 +128,8 @@ fn hash_path_parse_valid(vector: &Vector) { } fn hash_path_parse_invalid(vector: &Vector) { - let error = RadrootsBlossomHashPath::parse(input_str(vector, "path")) - .expect_err("invalid hash-path vector must fail"); + let error = + HashPath::parse(input_str(vector, "path")).expect_err("invalid hash-path vector must fail"); assert_error(vector, error.code()); } @@ -169,8 +166,8 @@ fn blob_url_parse_valid(vector: &Vector) { } fn blob_url_parse_invalid(vector: &Vector) { - let error = RadrootsBlossomBlobUrl::parse(input_str(vector, "url")) - .expect_err("invalid blob-URL vector must fail"); + let error = + BlobUrl::parse(input_str(vector, "url")).expect_err("invalid blob-URL vector must fail"); assert_error(vector, error.code()); } @@ -204,7 +201,7 @@ fn reference_policy_invalid(vector: &Vector) { } fn media_type_parse_valid(vector: &Vector) { - let parsed = RadrootsBlossomMediaType::parse(input_str(vector, "media_type")) + let parsed = MediaType::parse(input_str(vector, "media_type")) .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); assert_eq!( parsed.as_str(), @@ -215,7 +212,7 @@ fn media_type_parse_valid(vector: &Vector) { } fn media_type_parse_invalid(vector: &Vector) { - let error = RadrootsBlossomMediaType::parse(input_str(vector, "media_type")) + let error = MediaType::parse(input_str(vector, "media_type")) .expect_err("invalid media-type vector must fail"); assert_error(vector, error.code()); } @@ -228,7 +225,7 @@ fn descriptor_parse_valid(vector: &Vector) { fn descriptor_parse_invalid(vector: &Vector) { let input = &vector.input["descriptor"]; - serde_json::from_value::<RadrootsBlossomBlobDescriptor>(input.clone()) + serde_json::from_value::<BlobDescriptor>(input.clone()) .expect_err("invalid descriptor vector must fail"); let code = classify_descriptor_input(input); assert_error(vector, code); @@ -299,18 +296,18 @@ fn descriptor_verify_invalid(vector: &Vector) { assert_error(vector, error.code()); } -fn parse_blob_url(vector: &Vector) -> RadrootsBlossomBlobUrl { - RadrootsBlossomBlobUrl::parse(input_str(vector, "url")) +fn parse_blob_url(vector: &Vector) -> BlobUrl { + BlobUrl::parse(input_str(vector, "url")) .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)) } -fn parse_descriptor(vector: &Vector) -> RadrootsBlossomBlobDescriptor { +fn parse_descriptor(vector: &Vector) -> BlobDescriptor { 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")) +fn approved_media_type(vector: &Vector) -> MediaType { + MediaType::parse(input_str(vector, "approved_media_type")) .unwrap_or_else(|error| panic!("{} media type failed: {error}", vector.id)) } @@ -350,7 +347,7 @@ fn classify_descriptor_input(input: &Value) -> &'static str { } } - let url = match RadrootsBlossomBlobUrl::parse( + let url = match BlobUrl::parse( input["url"] .as_str() .expect("descriptor url must be a string"), @@ -358,7 +355,7 @@ fn classify_descriptor_input(input: &Value) -> &'static str { Ok(url) => url, Err(error) => return error.code(), }; - let sha256 = match RadrootsBlossomSha256::from_hex( + let sha256 = match Sha256::from_hex( input["sha256"] .as_str() .expect("descriptor sha256 must be a string"), @@ -369,7 +366,7 @@ fn classify_descriptor_input(input: &Value) -> &'static str { let Some(size) = input["size"].as_u64() else { return "invalid_descriptor_size"; }; - let media_type = match RadrootsBlossomMediaType::parse( + let media_type = match MediaType::parse( input["type"] .as_str() .expect("descriptor type must be a string"), @@ -381,7 +378,7 @@ fn classify_descriptor_input(input: &Value) -> &'static str { return "invalid_descriptor_uploaded"; }; - RadrootsBlossomBlobDescriptor::new(url, sha256, size, media_type, uploaded) + BlobDescriptor::new(url, sha256, size, media_type, uploaded) .expect_err("invalid descriptor vector must fail structured construction") .code() } diff --git a/crates/blossom/tests/package_boundary.rs b/crates/blossom/tests/package_boundary.rs @@ -1,6 +1,16 @@ -use std::collections::BTreeSet; +use std::{collections::BTreeSet, fs, path::Path}; -use radroots_blossom::media_type::RadrootsBlossomMediaType; +use radroots_blossom::{ + AuthorizationClaim, BlobDescriptor, BlobUrl, ByteVerifiedDescriptor, Error, MediaType, Sha256, + authorization::{ + AuthoredUploadClaim, AuthorizationAction, AuthorizationContent, AuthorizationTarget, + AuthorizationValidation, AuthorizationWireParts, ServerDomain, ServerScopeRequirement, + ValidatedAuthorizationClaim, + }, + descriptor::{ApprovedDescriptor, ByteCommitment}, + hash::{FileExtension, HashPath}, + url::ApprovedBlobUrl, +}; const MANIFEST: &str = include_str!("../Cargo.toml"); const ROOT: &str = include_str!("../src/lib.rs"); @@ -35,13 +45,66 @@ fn crate_root_matches_the_approved_module_skeleton() { BTreeSet::from(["authorization", "descriptor", "hash", "media_type", "url"]) ); assert_eq!(root_declarations("mod "), BTreeSet::from(["error"])); + assert_eq!( + ROOT.lines() + .map(str::trim) + .filter(|line| line.starts_with("pub use ")) + .collect::<BTreeSet<_>>(), + BTreeSet::from([ + "pub use authorization::AuthorizationClaim;", + "pub use descriptor::BlobDescriptor;", + "pub use descriptor::ByteVerifiedDescriptor;", + "pub use error::Error;", + "pub use hash::Sha256;", + "pub use media_type::MediaType;", + "pub use url::BlobUrl;", + ]) + ); } #[test] -fn final_crate_and_media_type_module_paths_compile() { +fn final_root_and_module_paths_compile() { fn assert_public_value<T: Clone + core::fmt::Debug + Eq + Send + Sync>() {} - assert_public_value::<RadrootsBlossomMediaType>(); + assert_public_value::<ApprovedBlobUrl>(); + assert_public_value::<ApprovedDescriptor>(); + assert_public_value::<AuthoredUploadClaim>(); + assert_public_value::<AuthorizationAction>(); + assert_public_value::<AuthorizationClaim>(); + assert_public_value::<AuthorizationContent>(); + assert_public_value::<AuthorizationTarget>(); + assert_public_value::<AuthorizationValidation>(); + assert_public_value::<AuthorizationWireParts>(); + assert_public_value::<BlobDescriptor>(); + assert_public_value::<BlobUrl>(); + assert_public_value::<ByteCommitment>(); + assert_public_value::<ByteVerifiedDescriptor>(); + assert_public_value::<Error>(); + assert_public_value::<FileExtension>(); + assert_public_value::<HashPath>(); + assert_public_value::<MediaType>(); + assert_public_value::<ServerDomain>(); + assert_public_value::<ServerScopeRequirement>(); + assert_public_value::<Sha256>(); + assert_public_value::<ValidatedAuthorizationClaim>(); +} + +#[test] +fn production_types_do_not_repeat_the_crate_name() { + let source_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + for entry in fs::read_dir(source_root).expect("read Blossom source directory") { + let path = entry.expect("source directory entry").path(); + if path.extension().and_then(|value| value.to_str()) != Some("rs") { + continue; + } + let source = fs::read_to_string(&path).expect("read Blossom source"); + let production = source.split("\n#[cfg(test)]").next().unwrap_or(&source); + assert!( + !production.contains("RadrootsBlossom"), + "Blossom production type repeats its crate name: {}", + path.display() + ); + } } fn table_keys<'a>(manifest: &'a str, heading: &str) -> BTreeSet<&'a str> { diff --git a/crates/event/src/calendar.rs b/crates/event/src/calendar.rs @@ -16,7 +16,7 @@ use crate::wire::v1::{ DEFAULT_CONTENT_MAX_BYTES, DEFAULT_TAG_ELEMENT_MAX_BYTES, DEFAULT_TAG_MAX_COUNT, DEFAULT_TAG_TOTAL_MAX_BYTES, }; -use radroots_blossom::url::RadrootsBlossomBlobUrl; +use radroots_blossom::url::BlobUrl; use url_nostd::Url; pub const RADROOTS_CALENDAR_SECONDS_PER_DAY: u64 = 86_400; @@ -743,7 +743,7 @@ impl RadrootsParsedNip52Calendar { pub struct RadrootsAdmittedCalendar { parsed: RadrootsParsedNip52Calendar, uid: RadrootsCalendarUid, - blossom_image: Option<RadrootsBlossomBlobUrl>, + blossom_image: Option<BlobUrl>, } impl RadrootsAdmittedCalendar { @@ -778,7 +778,7 @@ impl RadrootsAdmittedCalendar { let blossom_image = parsed .image() .map(|image| { - RadrootsBlossomBlobUrl::parse(image.as_str()) + BlobUrl::parse(image.as_str()) .map_err(|_| RadrootsCalendarAdmissionError::NonBlossomImage) }) .transpose()?; @@ -813,7 +813,7 @@ impl RadrootsAdmittedCalendar { self.parsed.list_description() } - pub fn blossom_image(&self) -> Option<&RadrootsBlossomBlobUrl> { + pub fn blossom_image(&self) -> Option<&BlobUrl> { self.blossom_image.as_ref() } } @@ -1744,7 +1744,7 @@ impl std::error::Error for RadrootsCalendarAdmissionError {} pub struct RadrootsAdmittedCalendarDateEvent { parsed: RadrootsParsedNip52CalendarDateEvent, d_tag: RadrootsDTag, - blossom_image: Option<RadrootsBlossomBlobUrl>, + blossom_image: Option<BlobUrl>, } impl RadrootsAdmittedCalendarDateEvent { @@ -1772,7 +1772,7 @@ impl RadrootsAdmittedCalendarDateEvent { &self.d_tag } - pub fn blossom_image(&self) -> Option<&RadrootsBlossomBlobUrl> { + pub fn blossom_image(&self) -> Option<&BlobUrl> { self.blossom_image.as_ref() } } @@ -1782,7 +1782,7 @@ pub struct RadrootsAdmittedCalendarTimeEvent { parsed: RadrootsParsedNip52CalendarTimeEvent, d_tag: RadrootsDTag, covered_utc_days: Vec<u64>, - blossom_image: Option<RadrootsBlossomBlobUrl>, + blossom_image: Option<BlobUrl>, } impl RadrootsAdmittedCalendarTimeEvent { @@ -1839,7 +1839,7 @@ impl RadrootsAdmittedCalendarTimeEvent { &self.covered_utc_days } - pub fn blossom_image(&self) -> Option<&RadrootsBlossomBlobUrl> { + pub fn blossom_image(&self) -> Option<&BlobUrl> { self.blossom_image.as_ref() } } @@ -2159,12 +2159,12 @@ fn validate_admitted_calendar_common( fn admitted_blossom_image( common: &RadrootsParsedNip52CalendarCommon, -) -> Result<Option<RadrootsBlossomBlobUrl>, RadrootsCalendarAdmissionError> { +) -> Result<Option<BlobUrl>, RadrootsCalendarAdmissionError> { common .image .as_ref() .map(|image| { - RadrootsBlossomBlobUrl::parse(image.as_str()) + BlobUrl::parse(image.as_str()) .map_err(|_| RadrootsCalendarAdmissionError::NonBlossomImage) }) .transpose() diff --git a/crates/event/src/contract/registry_v7.rs b/crates/event/src/contract/registry_v7.rs @@ -21,7 +21,7 @@ use crate::{ }, kinds::*, }; -use radroots_blossom::url::RadrootsBlossomBlobUrl; +use radroots_blossom::url::BlobUrl; pub const RADROOTS_EVENT_CONTRACT_REGISTRY_VERSION: u32 = 7; @@ -4443,7 +4443,7 @@ fn validate_calendar_blossom_image( contract: &RadrootsEventContract, ) -> Result<(), RadrootsContractValidationError> { if let Some(image) = tag_value(tags, "image") - && RadrootsBlossomBlobUrl::parse(image).is_err() + && BlobUrl::parse(image).is_err() { return Err(calendar_tag_mismatch( contract, @@ -4723,7 +4723,7 @@ fn validate_canonical_calendar_common_tags( )); } if let Some(image) = tag_value(tags, "image") - && RadrootsBlossomBlobUrl::parse(image).is_err() + && BlobUrl::parse(image).is_err() { return Err(calendar_tag_mismatch( contract, diff --git a/crates/event/src/food_availability.rs b/crates/event/src/food_availability.rs @@ -4,7 +4,7 @@ use alloc::{string::String, vec::Vec}; use std::{string::String, vec::Vec}; use core::{fmt, str::FromStr}; -use radroots_blossom::hash::{RadrootsBlossomHashPath, RadrootsBlossomSha256}; +use radroots_blossom::hash::{HashPath, Sha256}; use unicode_general_category::{GeneralCategory, get_general_category}; use url_nostd::{Host, Url}; @@ -797,14 +797,12 @@ pub fn food_media_http_url_is_valid(value: &str) -> bool { /// /// A `None` result does not invalidate a standard inbound NIP-58 image URL; it /// only means duplicate-digest diagnostics cannot be derived from its path. -pub fn food_media_blossom_digest(value: &str) -> Option<RadrootsBlossomSha256> { +pub fn food_media_blossom_digest(value: &str) -> Option<Sha256> { if !food_media_http_url_is_valid(value) { return None; } let url = Url::parse(value).ok()?; - RadrootsBlossomHashPath::parse(url.path()) - .ok() - .map(|path| path.hash()) + HashPath::parse(url.path()).ok().map(|path| path.hash()) } fn raw_food_media_host_and_path(value: &str) -> Option<(&str, &str)> { @@ -898,10 +896,7 @@ fn is_control_or_format(character: char) -> bool { #[cfg(test)] mod tests { use super::*; - use radroots_blossom::{ - RadrootsBlossomBlobDescriptor, RadrootsBlossomBlobUrl, RadrootsBlossomMediaType, - RadrootsBlossomSha256, - }; + use radroots_blossom::{BlobDescriptor, BlobUrl, MediaType, Sha256}; #[test] fn error_codes_and_messages_are_stable_for_every_variant() { @@ -1021,7 +1016,7 @@ mod tests { #[test] fn inbound_food_media_urls_are_structural_without_claiming_blossom() { - let hash = RadrootsBlossomSha256::digest(b"carrots").to_string(); + let hash = Sha256::digest(b"carrots").to_string(); for valid in [ format!("https://media.example/{hash}.webp"), format!("http://media.example:0/{hash}?download=1"), @@ -1043,7 +1038,7 @@ mod tests { let blossom = format!("https://media.example/{hash}.webp?download=1"); assert_eq!( food_media_blossom_digest(&blossom), - Some(RadrootsBlossomSha256::from_hex(&hash).unwrap()) + Some(Sha256::from_hex(&hash).unwrap()) ); assert_eq!( food_media_blossom_digest("https://media.example/not-a-hash.jpg"), @@ -1371,10 +1366,10 @@ mod tests { bytes: &[u8], dimensions: RadrootsFoodImageDimensions, ) -> RadrootsFoodAvailabilityImage { - let hash = RadrootsBlossomSha256::digest(bytes); - let media_type = RadrootsBlossomMediaType::parse("image/webp").unwrap(); - let descriptor = RadrootsBlossomBlobDescriptor::new( - RadrootsBlossomBlobUrl::parse(&format!("{origin}/{hash}.webp")).unwrap(), + let hash = Sha256::digest(bytes); + let media_type = MediaType::parse("image/webp").unwrap(); + let descriptor = BlobDescriptor::new( + BlobUrl::parse(&format!("{origin}/{hash}.webp")).unwrap(), hash, bytes.len() as u64, media_type.clone(), diff --git a/crates/event/src/media.rs b/crates/event/src/media.rs @@ -1,6 +1,6 @@ use core::fmt; -use radroots_blossom::descriptor::RadrootsBlossomByteVerifiedDescriptor; +use radroots_blossom::descriptor::ByteVerifiedDescriptor; /// Errors raised while constructing strict authored image media. #[non_exhaustive] @@ -36,11 +36,11 @@ impl std::error::Error for RadrootsAuthoredImageError {} /// This typestate does not prove upload completion, network availability, or /// image format safety. Owning runtimes remain responsible for those policies. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsAuthoredImage(RadrootsBlossomByteVerifiedDescriptor); +pub struct RadrootsAuthoredImage(ByteVerifiedDescriptor); impl RadrootsAuthoredImage { pub fn try_from_verified_descriptor( - descriptor: RadrootsBlossomByteVerifiedDescriptor, + descriptor: ByteVerifiedDescriptor, ) -> Result<Self, RadrootsAuthoredImageError> { if !descriptor.media_type().as_str().starts_with("image/") { return Err(RadrootsAuthoredImageError::MediaTypeNotImage); @@ -48,15 +48,15 @@ impl RadrootsAuthoredImage { Ok(Self(descriptor)) } - pub fn descriptor(&self) -> &RadrootsBlossomByteVerifiedDescriptor { + pub fn descriptor(&self) -> &ByteVerifiedDescriptor { &self.0 } } -impl TryFrom<RadrootsBlossomByteVerifiedDescriptor> for RadrootsAuthoredImage { +impl TryFrom<ByteVerifiedDescriptor> for RadrootsAuthoredImage { type Error = RadrootsAuthoredImageError; - fn try_from(value: RadrootsBlossomByteVerifiedDescriptor) -> Result<Self, Self::Error> { + fn try_from(value: ByteVerifiedDescriptor) -> Result<Self, Self::Error> { Self::try_from_verified_descriptor(value) } } @@ -64,10 +64,7 @@ impl TryFrom<RadrootsBlossomByteVerifiedDescriptor> for RadrootsAuthoredImage { #[cfg(test)] mod tests { use super::*; - use radroots_blossom::{ - RadrootsBlossomBlobDescriptor, RadrootsBlossomBlobUrl, RadrootsBlossomMediaType, - RadrootsBlossomSha256, - }; + use radroots_blossom::{BlobDescriptor, BlobUrl, MediaType, Sha256}; #[test] fn authored_image_requires_a_byte_verified_image_descriptor() { @@ -94,16 +91,12 @@ mod tests { ); } - fn verified_descriptor( - media_type: &str, - extension: &str, - ) -> RadrootsBlossomByteVerifiedDescriptor { + fn verified_descriptor(media_type: &str, extension: &str) -> ByteVerifiedDescriptor { let bytes = b"hello"; - let hash = RadrootsBlossomSha256::digest(bytes); - let media_type = RadrootsBlossomMediaType::parse(media_type).unwrap(); - RadrootsBlossomBlobDescriptor::new( - RadrootsBlossomBlobUrl::parse(&format!("https://media.example/{hash}.{extension}")) - .unwrap(), + let hash = Sha256::digest(bytes); + let media_type = MediaType::parse(media_type).unwrap(); + BlobDescriptor::new( + BlobUrl::parse(&format!("https://media.example/{hash}.{extension}")).unwrap(), hash, bytes.len() as u64, media_type.clone(), diff --git a/crates/event/src/post.rs b/crates/event/src/post.rs @@ -2,7 +2,7 @@ use alloc::{format, string::String, vec::Vec}; use core::fmt; -use radroots_blossom::url::RadrootsBlossomApprovedBlobUrl; +use radroots_blossom::url::ApprovedBlobUrl; use url_nostd::Url; use crate::media::RadrootsAuthoredImage; @@ -227,7 +227,7 @@ pub struct RadrootsAuthoredPostImage { image: RadrootsAuthoredImage, dimensions: RadrootsPostImageDimensions, alt: String, - fallbacks: Vec<RadrootsBlossomApprovedBlobUrl>, + fallbacks: Vec<ApprovedBlobUrl>, imeta_tag: Vec<String>, } @@ -267,7 +267,7 @@ impl RadrootsAuthoredPostImage { pub fn try_with_fallback( mut self, - fallback: RadrootsBlossomApprovedBlobUrl, + fallback: ApprovedBlobUrl, ) -> Result<Self, RadrootsAuthoredPostError> { if fallback.as_blob_url().hash_path().hash() != self.image.descriptor().sha256() { return Err(RadrootsAuthoredPostError::ImageFallbackHashMismatch); @@ -294,7 +294,7 @@ impl RadrootsAuthoredPostImage { &self.alt } - pub fn fallbacks(&self) -> &[RadrootsBlossomApprovedBlobUrl] { + pub fn fallbacks(&self) -> &[ApprovedBlobUrl] { &self.fallbacks } @@ -450,7 +450,7 @@ fn derive_imeta_tag( image: &RadrootsAuthoredImage, dimensions: RadrootsPostImageDimensions, alt: &str, - fallbacks: &[RadrootsBlossomApprovedBlobUrl], + fallbacks: &[ApprovedBlobUrl], ) -> Result<Vec<String>, RadrootsAuthoredPostError> { let descriptor = image.descriptor(); let mut tag = Vec::with_capacity(7 + fallbacks.len()); diff --git a/crates/event/src/profile.rs b/crates/event/src/profile.rs @@ -336,10 +336,7 @@ impl RadrootsAuthoredProfile { #[cfg(test)] mod tests { use super::*; - use radroots_blossom::{ - RadrootsBlossomBlobDescriptor, RadrootsBlossomBlobUrl, - RadrootsBlossomByteVerifiedDescriptor, RadrootsBlossomMediaType, RadrootsBlossomSha256, - }; + use radroots_blossom::{BlobDescriptor, BlobUrl, ByteVerifiedDescriptor, MediaType, Sha256}; #[test] fn maps_profile_type_to_tag_value() { @@ -535,16 +532,12 @@ mod tests { } } - fn verified_descriptor( - media_type: &str, - extension: &str, - ) -> RadrootsBlossomByteVerifiedDescriptor { + fn verified_descriptor(media_type: &str, extension: &str) -> ByteVerifiedDescriptor { let bytes = b"hello"; - let hash = RadrootsBlossomSha256::digest(bytes); - let media_type = RadrootsBlossomMediaType::parse(media_type).unwrap(); - RadrootsBlossomBlobDescriptor::new( - RadrootsBlossomBlobUrl::parse(&format!("https://media.example/{hash}.{extension}")) - .unwrap(), + let hash = Sha256::digest(bytes); + let media_type = MediaType::parse(media_type).unwrap(); + BlobDescriptor::new( + BlobUrl::parse(&format!("https://media.example/{hash}.{extension}")).unwrap(), hash, bytes.len() as u64, media_type.clone(), diff --git a/crates/event_codec/src/food_availability/inbound/registry_v7.rs b/crates/event_codec/src/food_availability/inbound/registry_v7.rs @@ -4,7 +4,7 @@ use alloc::{boxed::Box, string::String, string::ToString, vec::Vec}; use core::fmt; -use radroots_blossom::{hash::RadrootsBlossomSha256, url::RadrootsBlossomBlobUrl}; +use radroots_blossom::{hash::Sha256, url::BlobUrl}; use radroots_event::{ RadrootsEventTags, classified_listing::RadrootsClassifiedListingPartition, @@ -646,13 +646,13 @@ pub(crate) fn project_strict_verified_food_availability_event( .into()); } let mut seen_urls = Vec::<String>::new(); - let mut seen_digests = Vec::<RadrootsBlossomSha256>::new(); + let mut seen_digests = Vec::<Sha256>::new(); for tag in matching_tags(&tags, "image") { if tag.len() != 3 { return Err(RadrootsFoodAvailabilityProjectionError::TagInvalid); } - let url = RadrootsBlossomBlobUrl::parse(&tag[1]) - .and_then(RadrootsBlossomBlobUrl::approve) + let url = BlobUrl::parse(&tag[1]) + .and_then(BlobUrl::approve) .map_err(|_| RadrootsFoodAvailabilityProjectionError::TagInvalid)?; RadrootsFoodImageDimensions::parse(&tag[2])?; if seen_urls.iter().any(|seen| seen == &tag[1]) { @@ -706,7 +706,7 @@ fn project_images( let bounded = &image_tags[..image_tags.len().min(RADROOTS_FOOD_IMAGE_MAX_COUNT)]; let mut images = Vec::with_capacity(bounded.len()); let mut seen_urls = Vec::<String>::new(); - let mut seen_digests = Vec::<RadrootsBlossomSha256>::new(); + let mut seen_digests = Vec::<Sha256>::new(); for tag in bounded { let raw_url = tag.get(1).cloned(); diff --git a/crates/event_codec/tests/calendar.rs b/crates/event_codec/tests/calendar.rs @@ -1,7 +1,4 @@ -use radroots_blossom::{ - RadrootsBlossomBlobDescriptor, RadrootsBlossomBlobUrl, RadrootsBlossomMediaType, - RadrootsBlossomSha256, -}; +use radroots_blossom::{BlobDescriptor, BlobUrl, MediaType, Sha256}; use radroots_event::{ RadrootsAuthoredImage, RadrootsEventTags, calendar::{ @@ -57,10 +54,10 @@ fn date(value: &str) -> RadrootsCalendarDate { fn authored_image() -> RadrootsAuthoredImage { let bytes = b"canonical-calendar-image"; - let hash = RadrootsBlossomSha256::digest(bytes); - let media_type = RadrootsBlossomMediaType::parse("image/webp").unwrap(); - let descriptor = RadrootsBlossomBlobDescriptor::new( - RadrootsBlossomBlobUrl::parse(&format!("https://media.example.test/{hash}.webp")).unwrap(), + let hash = Sha256::digest(bytes); + let media_type = MediaType::parse("image/webp").unwrap(); + let descriptor = BlobDescriptor::new( + BlobUrl::parse(&format!("https://media.example.test/{hash}.webp")).unwrap(), hash, bytes.len() as u64, media_type.clone(), diff --git a/crates/event_codec/tests/calendar_conformance.rs b/crates/event_codec/tests/calendar_conformance.rs @@ -1,8 +1,6 @@ use std::{borrow::Cow, fs, path::Path}; -use radroots_blossom::{ - RadrootsBlossomBlobDescriptor, RadrootsBlossomByteVerifiedDescriptor, RadrootsBlossomError, -}; +use radroots_blossom::{BlobDescriptor, ByteVerifiedDescriptor, Error}; use radroots_event::{ RadrootsAuthoredImage, calendar::{ @@ -1414,15 +1412,13 @@ fn authored_image(input: &Value, vector_id: &str) -> RadrootsAuthoredImage { .unwrap_or_else(|error| panic!("{vector_id} image failed: {error}")) } -fn verified_descriptor(input: &Value, vector_id: &str) -> RadrootsBlossomByteVerifiedDescriptor { +fn verified_descriptor(input: &Value, vector_id: &str) -> ByteVerifiedDescriptor { verified_descriptor_result(input) .unwrap_or_else(|error| panic!("{vector_id} byte verification failed: {error}")) } -fn verified_descriptor_result( - input: &Value, -) -> Result<RadrootsBlossomByteVerifiedDescriptor, RadrootsBlossomError> { - let descriptor: RadrootsBlossomBlobDescriptor = +fn verified_descriptor_result(input: &Value) -> Result<ByteVerifiedDescriptor, Error> { + let descriptor: BlobDescriptor = serde_json::from_value(input["descriptor"].clone()).expect("image descriptor must parse"); let media_type = descriptor.media_type().clone(); descriptor.approve_reference()?.verify_bytes( diff --git a/crates/event_codec/tests/food_availability_conformance.rs b/crates/event_codec/tests/food_availability_conformance.rs @@ -4,10 +4,7 @@ use std::{borrow::Cow, collections::BTreeSet, fs, path::Path}; use nostr::secp256k1::Message; use nostr::{Event as NostrEvent, JsonUtil, Keys, SECP256K1}; -use radroots_blossom::{ - RadrootsBlossomBlobDescriptor, RadrootsBlossomBlobUrl, RadrootsBlossomMediaType, - RadrootsBlossomSha256, -}; +use radroots_blossom::{BlobDescriptor, BlobUrl, MediaType, Sha256}; use radroots_event::food_availability::{ RadrootsFoodAvailabilityDetails, RadrootsFoodAvailabilityDetailsParts, RadrootsFoodAvailabilityError, RadrootsFoodAvailabilityImage, RadrootsFoodAvailabilityStatus, @@ -392,11 +389,11 @@ fn strict_details( fn strict_image(input: &ImageInput, vector_id: &str) -> RadrootsFoodAvailabilityImage { let bytes = input.bytes_utf8.as_bytes(); - let hash = RadrootsBlossomSha256::digest(bytes); - let media_type = RadrootsBlossomMediaType::parse(&input.media_type) + let hash = Sha256::digest(bytes); + let media_type = MediaType::parse(&input.media_type) .unwrap_or_else(|error| panic!("{vector_id} image media type failed: {error}")); - let verified = RadrootsBlossomBlobDescriptor::new( - RadrootsBlossomBlobUrl::parse(&input.url) + let verified = BlobDescriptor::new( + BlobUrl::parse(&input.url) .unwrap_or_else(|error| panic!("{vector_id} image URL failed: {error}")), hash, bytes.len() as u64, diff --git a/crates/event_codec/tests/post.rs b/crates/event_codec/tests/post.rs @@ -1,7 +1,4 @@ -use radroots_blossom::{ - RadrootsBlossomBlobDescriptor, RadrootsBlossomBlobUrl, RadrootsBlossomByteVerifiedDescriptor, - RadrootsBlossomMediaType, RadrootsBlossomSha256, -}; +use radroots_blossom::{BlobDescriptor, BlobUrl, ByteVerifiedDescriptor, MediaType, Sha256}; use radroots_event::{ RadrootsAuthoredImage, post::{ @@ -425,16 +422,11 @@ fn authored_image(bytes: &[u8], media_type: &str, extension: &str) -> RadrootsAu .unwrap() } -fn verified_descriptor( - bytes: &[u8], - media_type: &str, - extension: &str, -) -> RadrootsBlossomByteVerifiedDescriptor { - let hash = RadrootsBlossomSha256::digest(bytes); - let media_type = RadrootsBlossomMediaType::parse(media_type).unwrap(); - RadrootsBlossomBlobDescriptor::new( - RadrootsBlossomBlobUrl::parse(&format!("https://media.example/{hash}.{extension}")) - .unwrap(), +fn verified_descriptor(bytes: &[u8], media_type: &str, extension: &str) -> ByteVerifiedDescriptor { + let hash = Sha256::digest(bytes); + let media_type = MediaType::parse(media_type).unwrap(); + BlobDescriptor::new( + BlobUrl::parse(&format!("https://media.example/{hash}.{extension}")).unwrap(), hash, bytes.len() as u64, media_type.clone(), @@ -450,16 +442,16 @@ fn verified_descriptor( fn verified_descriptor_with_url_element_bytes( bytes: &[u8], element_bytes: usize, -) -> RadrootsBlossomByteVerifiedDescriptor { - let hash = RadrootsBlossomSha256::digest(bytes); +) -> ByteVerifiedDescriptor { + let hash = Sha256::digest(bytes); let prefix = format!("https://media.example/{hash}."); let extension_bytes = element_bytes .checked_sub("url ".len() + prefix.len()) .expect("requested URL tag element is large enough"); assert!(extension_bytes > 0); - let media_type = RadrootsBlossomMediaType::parse("image/webp").unwrap(); - let descriptor = RadrootsBlossomBlobDescriptor::new( - RadrootsBlossomBlobUrl::parse(&format!("{prefix}{}", "x".repeat(extension_bytes))).unwrap(), + let media_type = MediaType::parse("image/webp").unwrap(); + let descriptor = BlobDescriptor::new( + BlobUrl::parse(&format!("{prefix}{}", "x".repeat(extension_bytes))).unwrap(), hash, bytes.len() as u64, media_type.clone(), @@ -481,9 +473,9 @@ fn fallback_url( bytes: &[u8], host: &str, extension: &str, -) -> radroots_blossom::RadrootsBlossomApprovedBlobUrl { - let hash = RadrootsBlossomSha256::digest(bytes); - RadrootsBlossomBlobUrl::parse(&format!("https://{host}/{hash}.{extension}")) +) -> radroots_blossom::url::ApprovedBlobUrl { + let hash = Sha256::digest(bytes); + BlobUrl::parse(&format!("https://{host}/{hash}.{extension}")) .unwrap() .approve() .unwrap() @@ -492,13 +484,13 @@ fn fallback_url( fn fallback_url_with_element_bytes( bytes: &[u8], element_bytes: usize, -) -> radroots_blossom::RadrootsBlossomApprovedBlobUrl { +) -> radroots_blossom::url::ApprovedBlobUrl { let prefix = fallback_element_prefix(bytes); let extension_bytes = element_bytes .checked_sub(prefix.len()) .expect("requested fallback tag element is large enough"); assert!(extension_bytes > 0); - let fallback = RadrootsBlossomBlobUrl::parse(&format!( + let fallback = BlobUrl::parse(&format!( "{}{}", prefix.strip_prefix("fallback ").unwrap(), "x".repeat(extension_bytes) @@ -511,7 +503,7 @@ fn fallback_url_with_element_bytes( } fn fallback_element_prefix(bytes: &[u8]) -> String { - let hash = RadrootsBlossomSha256::digest(bytes); + let hash = Sha256::digest(bytes); format!("fallback https://cache.example/{hash}.") } diff --git a/crates/event_codec/tests/profile_conformance.rs b/crates/event_codec/tests/profile_conformance.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, fs, path::Path}; -use radroots_blossom::{RadrootsBlossomBlobDescriptor, RadrootsBlossomByteVerifiedDescriptor}; +use radroots_blossom::{BlobDescriptor, ByteVerifiedDescriptor}; use radroots_event::{ media::{RadrootsAuthoredImage, RadrootsAuthoredImageError}, profile::{ @@ -52,7 +52,7 @@ struct AuthoredProfileInput { #[serde(deny_unknown_fields)] struct AuthoredMediaInput { bytes_utf8: String, - descriptor: RadrootsBlossomBlobDescriptor, + descriptor: BlobDescriptor, } #[test] @@ -266,10 +266,7 @@ fn authored_image(input: &AuthoredMediaInput, vector_id: &str) -> RadrootsAuthor .unwrap_or_else(|error| panic!("{vector_id} Profile image failed: {error}")) } -fn verified_descriptor( - input: &AuthoredMediaInput, - vector_id: &str, -) -> RadrootsBlossomByteVerifiedDescriptor { +fn verified_descriptor(input: &AuthoredMediaInput, vector_id: &str) -> ByteVerifiedDescriptor { let media_type = input.descriptor.media_type().clone(); input .descriptor diff --git a/crates/event_codec/tests/verified_post_conformance.rs b/crates/event_codec/tests/verified_post_conformance.rs @@ -2,10 +2,7 @@ use std::{borrow::Cow, collections::BTreeMap, fs, path::Path}; -use radroots_blossom::{ - RadrootsBlossomBlobDescriptor, RadrootsBlossomBlobUrl, RadrootsBlossomMediaType, - RadrootsBlossomSha256, -}; +use radroots_blossom::{BlobDescriptor, BlobUrl, MediaType, Sha256}; use radroots_event::{ RadrootsAuthoredImage, RadrootsEventEnvelope, RadrootsNip01EventWire, contract::identify_event_contract, @@ -668,11 +665,11 @@ fn authored_image( input: &Value, ) -> Result<RadrootsAuthoredPostImage, RadrootsAuthoredPostError> { let bytes = value_str(vector, input, "bytes_utf8").as_bytes(); - let media_type = RadrootsBlossomMediaType::parse(value_str(vector, input, "media_type")) + let media_type = MediaType::parse(value_str(vector, input, "media_type")) .unwrap_or_else(|error| panic!("{} media type setup failed: {error}", vector.id)); - let hash = RadrootsBlossomSha256::digest(bytes); - let descriptor = RadrootsBlossomBlobDescriptor::new( - RadrootsBlossomBlobUrl::parse(value_str(vector, input, "url")) + let hash = Sha256::digest(bytes); + let descriptor = BlobDescriptor::new( + BlobUrl::parse(value_str(vector, input, "url")) .unwrap_or_else(|error| panic!("{} URL setup failed: {error}", vector.id)), hash, u64::try_from(bytes.len()).expect("authored image byte length must fit u64"), @@ -697,7 +694,7 @@ fn authored_image( if let Some(fallbacks) = input.get("fallbacks").and_then(Value::as_array) { for fallback in fallbacks { image = image.try_with_fallback( - RadrootsBlossomBlobUrl::parse( + BlobUrl::parse( fallback .as_str() .unwrap_or_else(|| panic!("{} image fallback must be a string", vector.id)), diff --git a/crates/event_store/contracts/source_maintenance_v1.manifest.json b/crates/event_store/contracts/source_maintenance_v1.manifest.json @@ -208,22 +208,50 @@ { "role": "blossom_public_surface", "path": "crates/blossom/src/lib.rs", - "byte_length": 1351, - "sha256": "3ce28950e508626a4eed80456d9f74e117b6d2a59717cc52d10c44670083a80f", + "byte_length": 471, + "sha256": "821c0fb1c8a8832b89ec318192d4a054581ca7f9c208e8e02d7aa198184fe0e1", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "blossom_authorization_authority", + "path": "crates/blossom/src/authorization.rs", + "byte_length": 38819, + "sha256": "68e1b118232953ce7cc63d078aa13804dd17da6ed7f223dd6b106743fb5112a4", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "blossom_descriptor_authority", + "path": "crates/blossom/src/descriptor.rs", + "byte_length": 12302, + "sha256": "ead743e60b12e624cf40494678c14b4733dcf7930438a0d46f301c81f1f2e9a4", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "blossom_error_authority", + "path": "crates/blossom/src/error.rs", + "byte_length": 16267, + "sha256": "679afd92e874513f1f2849603317a0b0f1eba318ad213de1cdaed333ddd39e5c", "hash_algorithm": "sha256_bytes_v1" }, { "role": "blossom_hash_authority", "path": "crates/blossom/src/hash.rs", - "byte_length": 11251, - "sha256": "753eff6ef9a810045c88839d39a46b37d62a4ad0a5ea57aa30e2a8219e3cdbda", + "byte_length": 10206, + "sha256": "b2d9eadfbd6260fc6ae2fc1a4639e901f28ca268bde3f2b10461f2f75c9880af", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "blossom_media_type_authority", + "path": "crates/blossom/src/media_type.rs", + "byte_length": 2101, + "sha256": "e0d953fc9b38d49560e02a005277f9e91a25bd890d9f1654529df9d8ab7eb029", "hash_algorithm": "sha256_bytes_v1" }, { "role": "blossom_url_authority", "path": "crates/blossom/src/url.rs", - "byte_length": 15364, - "sha256": "05325325da6627ba48318851fb4e61a9ad540aad37fc1c27ac9a47f27300891f", + "byte_length": 14079, + "sha256": "a5d62d83bd123ce9f485f7c4c96981f1eb19fe8467ce91d1fbba6ce77c461df4", "hash_algorithm": "sha256_bytes_v1" }, { @@ -236,8 +264,8 @@ { "role": "event_contract_registry_v7_authority", "path": "crates/event/src/contract/registry_v7.rs", - "byte_length": 155788, - "sha256": "6305e487a6d51c0b04ad5620f28530d550e81b28152406cd5f224563ecd72f5c", + "byte_length": 155743, + "sha256": "2ed52d1279390ba073fa85a2d41020eb8b8e6fa9fc7268029182a95487b5eddd", "hash_algorithm": "sha256_bytes_v1" }, { @@ -299,8 +327,8 @@ { "role": "event_calendar_authority", "path": "crates/event/src/calendar.rs", - "byte_length": 103969, - "sha256": "94e07896e8cf3486b55becf2a64be72a5129625bd52a86e70e2a4b273fcf20e5", + "byte_length": 103819, + "sha256": "f86464c4be674a75902e57f25b73130bda7538f2c4f2e29ebde59fe51c9b67c4", "hash_algorithm": "sha256_bytes_v1" }, { @@ -313,15 +341,15 @@ { "role": "event_profile_authority", "path": "crates/event/src/profile.rs", - "byte_length": 19061, - "sha256": "f2b03be027aea187645e8bc3ae7a7d4004d6e58d4e1b361b35786ccdc36d907a", + "byte_length": 18848, + "sha256": "7c7ac7f4926938f5cb5530be5450e5e23f18f216bdb222150fbdd451cec20d5c", "hash_algorithm": "sha256_bytes_v1" }, { "role": "event_post_authority", "path": "crates/event/src/post.rs", - "byte_length": 22813, - "sha256": "d4a573cf2510f3a261f6aac3efd16489591281cdbdb387af5a7d67f516e1e056", + "byte_length": 22738, + "sha256": "7ed4a4c39bed40b134369d16e1fc35ab2c7d27ee300dd36d367025def2283b39", "hash_algorithm": "sha256_bytes_v1" }, { @@ -334,8 +362,8 @@ { "role": "event_food_availability_authority", "path": "crates/event/src/food_availability.rs", - "byte_length": 45680, - "sha256": "1c8c3fba6514f9b246545b3305e8ac2742258857d1b813fa34c5fbefa6135e12", + "byte_length": 45429, + "sha256": "b4357dfb6954631673b6cd58794015da29303cb54ce8405a57ad543870036c9e", "hash_algorithm": "sha256_bytes_v1" }, { @@ -355,8 +383,8 @@ { "role": "event_media_authority", "path": "crates/event/src/media.rs", - "byte_length": 3821, - "sha256": "ca98ddb134cb522912900544080d910867398d6aff6e8c6b061dc1fb77212ec3", + "byte_length": 3533, + "sha256": "9a589cfca489626746fdc2eba7ed0aba4e524840aadce51d4fb11a1e7b3d32bb", "hash_algorithm": "sha256_bytes_v1" }, { @@ -481,8 +509,8 @@ { "role": "event_codec_food_registry_v7_authority", "path": "crates/event_codec/src/food_availability/inbound/registry_v7.rs", - "byte_length": 26865, - "sha256": "908015346eaec97bee6f44fe2a524410b701529ac19a5dab4505706d40d527da", + "byte_length": 26775, + "sha256": "379321c2f9702773ae94304000e94104af0ab9ad165870c8c57f74bbb2c096c3", "hash_algorithm": "sha256_bytes_v1" }, { @@ -558,8 +586,8 @@ { "role": "food_projection_model", "path": "crates/event_store/src/model/food_availability_projection_v1.rs", - "byte_length": 17482, - "sha256": "7e8ead52ebfe091e3770878b781a9d089f2f4273f8f57ca0e5c6b12c3ab9acaf", + "byte_length": 17437, + "sha256": "6fdfc8c5eb3dd9c451fd47b2394af123b50f7fe6d74113f8777221a8fa31ba30", "hash_algorithm": "sha256_bytes_v1" }, { @@ -579,8 +607,8 @@ { "role": "public_store_and_transaction_authority", "path": "crates/event_store/src/store.rs", - "byte_length": 394777, - "sha256": "6a2431a6f4b7b5b1d25f493758008ae2246fbe57472552e233fe259404f629a1", + "byte_length": 394732, + "sha256": "6710a4b3e366b724f6b3d4904e2403c15456f23111996945931d2ba20e301f52", "hash_algorithm": "sha256_bytes_v1" }, { @@ -593,8 +621,8 @@ { "role": "food_projection_store", "path": "crates/event_store/src/store/food_availability_projection_v1.rs", - "byte_length": 48986, - "sha256": "dac90f2d4a289ce0ba5876e351d32417d517f2dca5342666b06eb51ab4bb30ca", + "byte_length": 48971, + "sha256": "189b824af305f39c7f71a285f4746455ff30f1a27b24b95d8d21223b41772152", "hash_algorithm": "sha256_bytes_v1" }, { @@ -633,6 +661,13 @@ "hash_algorithm": "sha256_bytes_v1" }, { + "role": "predecessor_food_result_vector_executor", + "path": "crates/event_store/tests/food_availability_projection_v1_result_vector.rs", + "byte_length": 34070, + "sha256": "aa098bcb0f975f690ca9535d3cbb76c6e57e6d70fd3a892c4399fd35fe8ec21c", + "hash_algorithm": "sha256_bytes_v1" + }, + { "role": "artifact_transaction_authority", "path": "tools/xtask/src/contract/artifact_bundle.rs", "byte_length": 38279, @@ -642,36 +677,36 @@ { "role": "predecessor_successor_governance", "path": "tools/xtask/src/contract/food_availability_projection.rs", - "byte_length": 194572, - "sha256": "f251b1202c72ae0b63a2f0adee35eb474ba39b9cde7348feee3ab7085d306a11", + "byte_length": 197131, + "sha256": "6b71c927b41404f633dfbd0f8e321dcdab933285a196671f27046c0c15d55434", "hash_algorithm": "sha256_bytes_v1" }, { "role": "transitive_predecessor_membership_governance", "path": "tools/xtask/src/contract/nip09_reconciliation.rs", - "byte_length": 835899, - "sha256": "08859d15cd750580889b80a4f502d4130c39f527cccfe605a85fbfcdad62bba3", + "byte_length": 835884, + "sha256": "c266610ba4a55e9fc3b5e80c52c7b244aa9fac7336c7a7cd37489a8bbdb97f1d", "hash_algorithm": "sha256_bytes_v1" }, { "role": "source_maintenance_governance", "path": "tools/xtask/src/contract/source_maintenance.rs", - "byte_length": 186874, - "sha256": "4df170e29faf12111394d2138997fbbea2bae0cf425a6157c9b10e13a6fed55d", + "byte_length": 188436, + "sha256": "ce21d4d200142b361ee0f65bd8063a461fd01f016e92eec7f6576863a0856b05", "hash_algorithm": "sha256_bytes_v1" }, { "role": "contract_command_authority", "path": "tools/xtask/src/contract.rs", - "byte_length": 504371, - "sha256": "0f8de542e17aa2b1475d5ccb7c38cd35c9132a3390f3c9537ef5de29aa68420f", + "byte_length": 504182, + "sha256": "7edf6ffda5dec9e08b66ad100d9119a705c994af692fd287e90700830de12281", "hash_algorithm": "sha256_bytes_v1" }, { "role": "xtask_dispatch_and_release_preflight", "path": "tools/xtask/src/main.rs", - "byte_length": 14976, - "sha256": "42b59a8576e4f86b091941a64349ac9eaa20b1e3b52aef367bb3d5947d8ccd1d", + "byte_length": 15109, + "sha256": "bf721ecaa94225908496f247bf65011e669aa61b732594c23d64e306b4ac38cc", "hash_algorithm": "sha256_bytes_v1" } ], diff --git a/crates/event_store/contracts/source_maintenance_v1.manifest.sha256 b/crates/event_store/contracts/source_maintenance_v1.manifest.sha256 @@ -1 +1 @@ -f5f6b2c84a5231c944b9122a125e6d68f9c4498a1c55426dbd86847ac6a2b4f6 +922a53eeb6ee233e4b4349a89e867939c83d3b7441dacbbe1b7c90966817e47d diff --git a/crates/event_store/src/generated/source_maintenance_manifest.rs b/crates/event_store/src/generated/source_maintenance_manifest.rs @@ -1,8 +1,8 @@ // @generated by `cargo xtask contract source-maintenance-manifest --write`; do not edit. -pub(crate) const SOURCE_MAINTENANCE_MANIFEST_JSON: &str = "{\n \"schema_version\": 1,\n \"contract_id\": \"radroots_event_store.source_maintenance_v1\",\n \"hook_id\": \"source_maintenance_v1\",\n \"manifest_schema\": {\n \"path\": \"crates/event_store/contracts/source_maintenance_v1.manifest.schema.json\",\n \"byte_length\": 12315,\n \"sha256\": \"96ea98a5abf3348b61565954cd34f2c622411d8dd34205ef04134bbe0e3d3f2b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"predecessor\": {\n \"hook_id\": \"food_availability_projection_v1\",\n \"manifest\": {\n \"path\": \"crates/event_store/contracts/food_availability_projection_v1.manifest.json\",\n \"byte_length\": 17455,\n \"sha256\": \"02dfe1b450fbdac16e718888215b4dd5c85d8975440fa21e8f439fb24c2b2990\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n },\n \"migration\": {\n \"version\": 4,\n \"name\": \"source_maintenance\",\n \"up\": {\n \"path\": \"crates/event_store/migrations/0004_source_maintenance.up.sql\",\n \"byte_length\": 19841,\n \"sha256\": \"ab2724188f8d08c897eebea2533a635e7c74282a25e84e4c0c37e78b08837a43\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"down\": {\n \"path\": \"crates/event_store/migrations/0004_source_maintenance.down.sql\",\n \"byte_length\": 5172,\n \"sha256\": \"fe44fd53c51545c08ea479b385e6781079dab70fc63da2a3c205d727a00ce860\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"schema_sha256\": \"074f85b663444ac150239ecd8441ea4a96ad83a798a55e22d2e5e2f7ee943a8c\",\n \"catalog\": {\n \"objects\": [\n \"radroots_event_store_source_capacity_delete_guard\",\n \"radroots_event_store_source_capacity_insert_guard\",\n \"radroots_event_store_source_capacity_marker_close_guard\",\n \"radroots_event_store_source_capacity_update_guard\",\n \"radroots_event_store_source_capacity_v1\",\n \"radroots_event_store_source_generation_capacity_advance\",\n \"radroots_event_store_source_generation_capacity_guard\"\n ],\n \"replaced_objects\": [\n \"radroots_event_store_food_availability_image_delete_guard\",\n \"radroots_event_store_food_availability_projection_delete_guard\",\n \"radroots_event_store_source_rebuild_marker_insert_guard\"\n ],\n \"tables\": [\n \"radroots_event_store_source_capacity_v1\"\n ],\n \"fts5_tables\": []\n }\n },\n \"source_maintenance\": {\n \"version\": 1,\n \"event_contract_registry_version\": 7,\n \"capacity_authority_id\": \"radroots_event_store_source_capacity_v1\",\n \"accounting\": {\n \"algorithm\": \"sqlite_cast_blob_octet_sum_v1\",\n \"raw_event_columns\": [\n \"event_id\",\n \"pubkey\",\n \"tags_json\",\n \"content\",\n \"sig\",\n \"raw_json\"\n ],\n \"raw_tag_columns\": [\n \"event_id\",\n \"tag_name\",\n \"tag_value\",\n \"tag_json\"\n ],\n \"nullable_raw_tag_columns\": [\n \"tag_value\"\n ]\n },\n \"limits\": {\n \"raw_events\": 25000,\n \"raw_tags\": 250000,\n \"raw_event_text_bytes\": 67108864,\n \"raw_tag_text_bytes\": 33554432,\n \"retained_source_generations\": 8\n },\n \"reopen_validation\": {\n \"mode\": \"bounded_full_raw_recount_v1\",\n \"raw_event_rejection_scan_bound\": 25001,\n \"raw_tag_rejection_scan_bound\": 250001,\n \"generation_history_validation\": \"bounded_count_plus_active_ordinal_v1\",\n \"retained_generation_rejection_scan_bound\": 9\n },\n \"rebuild_seal\": {\n \"nip09_hook_id\": \"nip09_reconciliation_v1\",\n \"nip09_manifest_sha256\": \"74af832420ffbaa9805e89df3c0b34f126a443e1598f757e3372f407f9003b77\",\n \"food_hook_id\": \"food_availability_projection_v1\",\n \"food_manifest_sha256\": \"02dfe1b450fbdac16e718888215b4dd5c85d8975440fa21e8f439fb24c2b2990\",\n \"food_scope_fingerprint_sha256\": \"8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0\",\n \"active_generation_authority\": \"radroots_event_store_source_state\",\n \"marker_close_authority\": \"radroots_event_store_source_capacity_marker_close_guard\"\n }\n },\n \"entry_points\": [\n {\n \"role\": \"migration_registry\",\n \"rust_path\": \"radroots_event_store::migrations::EVENT_STORE_MIGRATIONS[3]\"\n },\n {\n \"role\": \"migration_apply_hook\",\n \"rust_path\": \"radroots_event_store::schema::apply_migration_hook\"\n },\n {\n \"role\": \"migration_validation_hook\",\n \"rust_path\": \"radroots_event_store::schema::validate_migration_hook_state\"\n },\n {\n \"role\": \"capacity_query\",\n \"rust_path\": \"radroots_event_store::RadrootsEventStore::source_capacity_v1\"\n },\n {\n \"role\": \"raw_append_preflight\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::preflight_unique_raw_source_append_v1\"\n },\n {\n \"role\": \"raw_append_advance\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::advance_source_capacity_after_insert_v1\"\n },\n {\n \"role\": \"generation_append_preflight\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::preflight_source_generation_append_v1\"\n },\n {\n \"role\": \"generation_rebuild_bind\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::bind_source_capacity_to_generation_v1\"\n },\n {\n \"role\": \"sqlite_encoding_preflight\",\n \"rust_path\": \"radroots_event_store::store::validate_main_database_encoding\"\n },\n {\n \"role\": \"source_generation_history_rollback_guard\",\n \"rust_path\": \"radroots_event_store::schema::validate_rollback_preserves_source_generation_history\"\n },\n {\n \"role\": \"result_vector_executor\",\n \"rust_path\": \"source_maintenance_v1_result_vector\"\n }\n ],\n \"source_files\": [\n {\n \"role\": \"workspace_dependency_authority\",\n \"path\": \"Cargo.toml\",\n \"byte_length\": 12385,\n \"sha256\": \"c53c5c5a464267d7df850e03fdfa99dcde0b469972cf921f9b81c18b52a9c4b2\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_currency_value_authority\",\n \"path\": \"crates/core/src/currency.rs\",\n \"byte_length\": 4142,\n \"sha256\": \"42e6da2d8d2fdd6955dc2d83e98dd00266e02bc910b69923b117662c05089d14\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_decimal_value_authority\",\n \"path\": \"crates/core/src/decimal.rs\",\n \"byte_length\": 7839,\n \"sha256\": \"4b1d681a92d7a9e074bee7e1f20eb1499bb05bfc6528a6c9ba69fa7d00416550\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_money_value_authority\",\n \"path\": \"crates/core/src/money.rs\",\n \"byte_length\": 8025,\n \"sha256\": \"3f30bc21e21951a62fdc5d4033736ed42a883df590e8403940775bd2edfeebc5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_percent_value_authority\",\n \"path\": \"crates/core/src/percent.rs\",\n \"byte_length\": 2652,\n \"sha256\": \"42ceab109881329f3539d06a0ee0381ab4359985fe185a18d58a9c9558c5eb7f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_quantity_value_authority\",\n \"path\": \"crates/core/src/quantity.rs\",\n \"byte_length\": 6785,\n \"sha256\": \"d14e619da3829cdaf3ca7385fb892d330c286c1e21455c135c37518cbb588b5f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_quantity_price_value_authority\",\n \"path\": \"crates/core/src/quantity_price.rs\",\n \"byte_length\": 7061,\n \"sha256\": \"d60f70377099470ed283f45868a5119d7bb46143cb250b5082a4c038ba7f6cdc\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_unit_value_authority\",\n \"path\": \"crates/core/src/unit.rs\",\n \"byte_length\": 10023,\n \"sha256\": \"550e9582b6a5290aac3f637b83853b21fefcbab92c432d067c83448f4b2fe3e9\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_public_surface\",\n \"path\": \"crates/blossom/src/lib.rs\",\n \"byte_length\": 1351,\n \"sha256\": \"3ce28950e508626a4eed80456d9f74e117b6d2a59717cc52d10c44670083a80f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_hash_authority\",\n \"path\": \"crates/blossom/src/hash.rs\",\n \"byte_length\": 11251,\n \"sha256\": \"753eff6ef9a810045c88839d39a46b37d62a4ad0a5ea57aa30e2a8219e3cdbda\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_url_authority\",\n \"path\": \"crates/blossom/src/url.rs\",\n \"byte_length\": 15364,\n \"sha256\": \"05325325da6627ba48318851fb4e61a9ad540aad37fc1c27ac9a47f27300891f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_contract_facade\",\n \"path\": \"crates/event/src/contract.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_contract_registry_v7_authority\",\n \"path\": \"crates/event/src/contract/registry_v7.rs\",\n \"byte_length\": 155788,\n \"sha256\": \"6305e487a6d51c0b04ad5620f28530d550e81b28152406cd5f224563ecd72f5c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_envelope_authority\",\n \"path\": \"crates/event/src/envelope.rs\",\n \"byte_length\": 31165,\n \"sha256\": \"97a3bb38d630666bba497b98e856ee457d6899de230dabf6cab786a5bd55a3ce\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_head_facade\",\n \"path\": \"crates/event/src/event_head.rs\",\n \"byte_length\": 68,\n \"sha256\": \"cb52f6006f7ecd862707d6b048f9fc407e0cd5ebcd3091b3c54e195ffa5cba64\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_head_v1_authority\",\n \"path\": \"crates/event/src/event_head/v1.rs\",\n \"byte_length\": 7529,\n \"sha256\": \"0a4b50c4b7a93e9d88cc995cf69d9e87b341cba66d7feb51c357dfa4e5b2de37\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_ids_authority\",\n \"path\": \"crates/event/src/ids.rs\",\n \"byte_length\": 46282,\n \"sha256\": \"f3c34e7dcf09fc225cb9c12f02ae932d5155c859f73f8b8573db8e5ebf298678\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_trade_authority\",\n \"path\": \"crates/event/src/trade.rs\",\n \"byte_length\": 68376,\n \"sha256\": \"1012b44295cb26844206304bc24b355a49c18a023844f6b3905337c63b1417a7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_kinds_authority\",\n \"path\": \"crates/event/src/kinds.rs\",\n \"byte_length\": 34961,\n \"sha256\": \"8b3ce6193cab1f7e1587d0c1b2880a81b0aa43c77d89671b602772db37edcebf\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_tags_authority\",\n \"path\": \"crates/event/src/tags.rs\",\n \"byte_length\": 4530,\n \"sha256\": \"cb8f12a639fc72d9238e32495d9e67e928e6627fb8c94ec0a58c5d95e7373cd6\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_draft_authority\",\n \"path\": \"crates/event/src/draft.rs\",\n \"byte_length\": 58674,\n \"sha256\": \"fa4e802f1143eba72ba7fc334104ea0d6613c886725ff9f3e61dab2443c243b6\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_calendar_authority\",\n \"path\": \"crates/event/src/calendar.rs\",\n \"byte_length\": 103969,\n \"sha256\": \"94e07896e8cf3486b55becf2a64be72a5129625bd52a86e70e2a4b273fcf20e5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_classified_listing_authority\",\n \"path\": \"crates/event/src/classified_listing.rs\",\n \"byte_length\": 8226,\n \"sha256\": \"745aa08953a6243ea1afad22aa63e44f6746f2f243ab0d6b617d6ab64fa58749\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_profile_authority\",\n \"path\": \"crates/event/src/profile.rs\",\n \"byte_length\": 19061,\n \"sha256\": \"f2b03be027aea187645e8bc3ae7a7d4004d6e58d4e1b361b35786ccdc36d907a\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_post_authority\",\n \"path\": \"crates/event/src/post.rs\",\n \"byte_length\": 22813,\n \"sha256\": \"d4a573cf2510f3a261f6aac3efd16489591281cdbdb387af5a7d67f516e1e056\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_comment_authority\",\n \"path\": \"crates/event/src/comment.rs\",\n \"byte_length\": 36375,\n \"sha256\": \"f4d1a57780eff0d315bc4e2fbad316dfb7c7ced1bcc9e8e85afe0b5ecdaf3aa8\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_food_availability_authority\",\n \"path\": \"crates/event/src/food_availability.rs\",\n \"byte_length\": 45680,\n \"sha256\": \"1c8c3fba6514f9b246545b3305e8ac2742258857d1b813fa34c5fbefa6135e12\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_deletion_authority\",\n \"path\": \"crates/event/src/deletion.rs\",\n \"byte_length\": 31155,\n \"sha256\": \"c8b7ab79998ae62e03497f5511dd3950a41d73155edda3d8d34c65fc797e9d09\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_relay_hint_authority\",\n \"path\": \"crates/event/src/relay_hint.rs\",\n \"byte_length\": 14317,\n \"sha256\": \"1a14aea5d2f2620970dc1de9e3d311d9042cab4d5805a56461548168112a0801\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_media_authority\",\n \"path\": \"crates/event/src/media.rs\",\n \"byte_length\": 3821,\n \"sha256\": \"ca98ddb134cb522912900544080d910867398d6aff6e8c6b061dc1fb77212ec3\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_social_authority\",\n \"path\": \"crates/event/src/social.rs\",\n \"byte_length\": 5483,\n \"sha256\": \"1b89c6d0954822e7f1e778479ed43694e88929bcd5310ee066f8f4ba6505045a\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_wire_facade\",\n \"path\": \"crates/event/src/wire.rs\",\n \"byte_length\": 68,\n \"sha256\": \"cb52f6006f7ecd862707d6b048f9fc407e0cd5ebcd3091b3c54e195ffa5cba64\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_wire_v1_authority\",\n \"path\": \"crates/event/src/wire/v1.rs\",\n \"byte_length\": 22133,\n \"sha256\": \"cc28c8cbd962b32ed2efe43e84c1e0d1ea3bd89636efff2d5553714072fce1a4\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_verification_facade\",\n \"path\": \"crates/event_codec/src/verification.rs\",\n \"byte_length\": 253,\n \"sha256\": \"b49a32df605035c87f295c0a151140d43d2e588c5e11b05183e5fc92993dae0e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_verification_v1_authority\",\n \"path\": \"crates/event_codec/src/verification/v1.rs\",\n \"byte_length\": 8751,\n \"sha256\": \"fc4c5d45e475151c56330ba9c4db70de603e4ec48adabd6365f80d5f6a3733f4\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_registry_v7_admission_authority\",\n \"path\": \"crates/event_codec/src/admission/registry_v7.rs\",\n \"byte_length\": 5350,\n \"sha256\": \"755e63dd7ad1115c219e5a3ea540bef67d2770fc9f2a5aa6ba97c925c99bdced\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_profile_inbound_facade\",\n \"path\": \"crates/event_codec/src/profile/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_profile_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/profile/inbound/registry_v7.rs\",\n \"byte_length\": 10394,\n \"sha256\": \"c37c225ea8153beb098a39c4a56203aa03382af12646f40cd3f8628663700702\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_post_inbound_facade\",\n \"path\": \"crates/event_codec/src/post/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_post_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/post/inbound/registry_v7.rs\",\n \"byte_length\": 16016,\n \"sha256\": \"bc0bb839c28f3588f05544c846c987528ea3934c75305fef70af8e027a97cc58\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_reply_inbound_facade\",\n \"path\": \"crates/event_codec/src/reply/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_reply_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/reply/inbound/registry_v7.rs\",\n \"byte_length\": 26812,\n \"sha256\": \"a72e9721ed13addca2a63942ac9e43c1daa8f1d1457eef08ec6a3ee5eb8872c5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_comment_inbound_facade\",\n \"path\": \"crates/event_codec/src/comment/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_comment_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/comment/inbound/registry_v7.rs\",\n \"byte_length\": 53783,\n \"sha256\": \"30d1b0155963b25d64685ddd042f423ef7707b111dc56ead15bd468e0433a432\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_deletion_facade\",\n \"path\": \"crates/event_codec/src/deletion/mod.rs\",\n \"byte_length\": 115,\n \"sha256\": \"17d56f82ddb0a86bc97abbcf3e037fe460ea31fff2548f527be4bf004dac3a95\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_deletion_reconciliation_v1_authority\",\n \"path\": \"crates/event_codec/src/deletion/reconciliation_v1.rs\",\n \"byte_length\": 38469,\n \"sha256\": \"3e06d1fa923ca5faabb1e95338c35650b0e20b078ff5c972e35ab845d8b7476c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_food_inbound_facade\",\n \"path\": \"crates/event_codec/src/food_availability/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_food_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/food_availability/inbound/registry_v7.rs\",\n \"byte_length\": 26865,\n \"sha256\": \"908015346eaec97bee6f44fe2a524410b701529ac19a5dab4505706d40d527da\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_job_traits_authority\",\n \"path\": \"crates/event_codec/src/job/traits.rs\",\n \"byte_length\": 5029,\n \"sha256\": \"353df3703de4d99fc318474952dc6c95cbce9a19756f3e235ed2d715f36c4364\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_dependency_authority\",\n \"path\": \"crates/event_store/Cargo.toml\",\n \"byte_length\": 1569,\n \"sha256\": \"30f43004bcf0ffa9b9b6f338cba8c56be0265bc1cee708e93f7b7dc54ed60f9c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_error_and_limits\",\n \"path\": \"crates/event_store/src/error.rs\",\n \"byte_length\": 19517,\n \"sha256\": \"4bf24ee1aa6117597ab467b1379a05549b79024213dc7622a2d276bc26fffab4\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"generated_descriptor_registration\",\n \"path\": \"crates/event_store/src/generated.rs\",\n \"byte_length\": 144,\n \"sha256\": \"6b0a8d6f249bd4fc3f878d37cb5e418680f0f1be2d9eec2518dedf03efc47121\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"public_surface\",\n \"path\": \"crates/event_store/src/lib.rs\",\n \"byte_length\": 3844,\n \"sha256\": \"3cd9653bcb752fb3c4442d4904b98a0a6208011a9a238125b7b7073d7f4e312b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"migration_registry\",\n \"path\": \"crates/event_store/src/migrations.rs\",\n \"byte_length\": 73585,\n \"sha256\": \"a47477d04759ec6e71d14d3d05459864eae78fce0e3c2481a1b03147f04637f7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_model_public_surface\",\n \"path\": \"crates/event_store/src/model.rs\",\n \"byte_length\": 33596,\n \"sha256\": \"98b1742ffb517f66f8e9edefa61d1c3cf3e22be5985202c6c340310a98c72c8e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_reconciliation_v1_model\",\n \"path\": \"crates/event_store/src/model/reconciliation_v1.rs\",\n \"byte_length\": 11138,\n \"sha256\": \"8a26bc373035878ef9b41767ceea7b681896e17d88bedce118de1d622125e1d6\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_reconciliation_v1_ingest_model\",\n \"path\": \"crates/event_store/src/model/ingest_reconciliation_v1.rs\",\n \"byte_length\": 1626,\n \"sha256\": \"47bf13b3fc0f8a913a660f7d655413de0f6b90568bc4acc510aa6bd741bab47b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"addressable_transition_feed_model\",\n \"path\": \"crates/event_store/src/model/addressable_transition_feed_v1.rs\",\n \"byte_length\": 22279,\n \"sha256\": \"f8ea454a0e998a3d433bba52d1ff9759a04fcb91db286cf2a2b10ef7d0057694\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"food_projection_model\",\n \"path\": \"crates/event_store/src/model/food_availability_projection_v1.rs\",\n \"byte_length\": 17482,\n \"sha256\": \"7e8ead52ebfe091e3770878b781a9d089f2f4273f8f57ca0e5c6b12c3ab9acaf\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_generation_rebuild_authority\",\n \"path\": \"crates/event_store/src/nip09/reconciliation_v1.rs\",\n \"byte_length\": 184402,\n \"sha256\": \"7fa01e52f51d3470ba60e6068dc05797c2a6eaf5eb317d23111a961dbe4451f9\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"schema_migration_and_reopen_authority\",\n \"path\": \"crates/event_store/src/schema.rs\",\n \"byte_length\": 146146,\n \"sha256\": \"93b060e80d3edd73f86208e4bf698fa9d53eaf1eeb04526c9261fb8b5726fb0d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"public_store_and_transaction_authority\",\n \"path\": \"crates/event_store/src/store.rs\",\n \"byte_length\": 394777,\n \"sha256\": \"6a2431a6f4b7b5b1d25f493758008ae2246fbe57472552e233fe259404f629a1\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"addressable_transition_feed_store\",\n \"path\": \"crates/event_store/src/store/addressable_transition_feed_v1.rs\",\n \"byte_length\": 40254,\n \"sha256\": \"de656face46735b938b7b20780687404eade47885871129cc5a7c009155d6336\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"food_projection_store\",\n \"path\": \"crates/event_store/src/store/food_availability_projection_v1.rs\",\n \"byte_length\": 48986,\n \"sha256\": \"dac90f2d4a289ce0ba5876e351d32417d517f2dca5342666b06eb51ab4bb30ca\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_post_core_v1_extension\",\n \"path\": \"crates/event_store/src/store/post_core_extensions_v1.rs\",\n \"byte_length\": 6923,\n \"sha256\": \"2fbfc6252f9ce249468a99b5230fd6b7b59b9e47ee4f38783c5fd9379c823fd0\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_post_core_v1_storage\",\n \"path\": \"crates/event_store/src/store/post_core_storage_v1.rs\",\n \"byte_length\": 16876,\n \"sha256\": \"f27dc7dcd643762226f8d4df0d297027b384bcf1004277222e978965bc2d40cf\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"raw_ingest_capacity_authority\",\n \"path\": \"crates/event_store/src/store/protocol_reconciliation_v1.rs\",\n \"byte_length\": 30145,\n \"sha256\": \"a8a26763dc39124ab7537fdcc3f89a1f0aa7e1830f4f9dec9ee4057bbb4fd228\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_protocol_storage\",\n \"path\": \"crates/event_store/src/store/protocol_storage_v1.rs\",\n \"byte_length\": 10929,\n \"sha256\": \"20de4436945bac0d8cbeca0ae6cd95232a92da8cd6be6ae6beacba17fe50d658\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_maintenance_runtime\",\n \"path\": \"crates/event_store/src/source_maintenance_v1.rs\",\n \"byte_length\": 51762,\n \"sha256\": \"5db9c9e86fd18a2fcc08dbc3468f373bf39d8948bc6a08936b252a4dda3546e9\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"artifact_transaction_authority\",\n \"path\": \"tools/xtask/src/contract/artifact_bundle.rs\",\n \"byte_length\": 38279,\n \"sha256\": \"f326ea57b56d40135f95b6b1e15961f66eed363337180a6d12a5ea903e1a9a29\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_successor_governance\",\n \"path\": \"tools/xtask/src/contract/food_availability_projection.rs\",\n \"byte_length\": 194572,\n \"sha256\": \"f251b1202c72ae0b63a2f0adee35eb474ba39b9cde7348feee3ab7085d306a11\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"transitive_predecessor_membership_governance\",\n \"path\": \"tools/xtask/src/contract/nip09_reconciliation.rs\",\n \"byte_length\": 835899,\n \"sha256\": \"08859d15cd750580889b80a4f502d4130c39f527cccfe605a85fbfcdad62bba3\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_maintenance_governance\",\n \"path\": \"tools/xtask/src/contract/source_maintenance.rs\",\n \"byte_length\": 186874,\n \"sha256\": \"4df170e29faf12111394d2138997fbbea2bae0cf425a6157c9b10e13a6fed55d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"contract_command_authority\",\n \"path\": \"tools/xtask/src/contract.rs\",\n \"byte_length\": 504371,\n \"sha256\": \"0f8de542e17aa2b1475d5ccb7c38cd35c9132a3390f3c9537ef5de29aa68420f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"xtask_dispatch_and_release_preflight\",\n \"path\": \"tools/xtask/src/main.rs\",\n \"byte_length\": 14976,\n \"sha256\": \"42b59a8576e4f86b091941a64349ac9eaa20b1e3b52aef367bb3d5947d8ccd1d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n ],\n \"public_api\": {\n \"inherited_predecessor_symbols\": [\n \"RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_D_TAG_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_PROJECTION_APPLY_PAGE_LIMIT_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_BYTES_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1\",\n \"RadrootsAddressableTransitionCauseV1\",\n \"RadrootsAddressableTransitionCoordinateV1\",\n \"RadrootsAddressableTransitionCursorV1\",\n \"RadrootsAddressableTransitionEventReferenceV1\",\n \"RadrootsAddressableTransitionOriginV1\",\n \"RadrootsAddressableTransitionPageV1\",\n \"RadrootsAddressableTransitionRawHeadDecisionV1\",\n \"RadrootsAddressableTransitionScopeFingerprintV1\",\n \"RadrootsAddressableTransitionScopeV1\",\n \"RadrootsAddressableTransitionV1\",\n \"RadrootsAddressableTransitionVisibilityV1\",\n \"RadrootsCurrentEventVisibilityV1\",\n \"RadrootsCurrentVisibilityDecisionV1\",\n \"RadrootsFoodAvailabilitySearchQueryV1\",\n \"RadrootsFoodAvailabilityStatusFilterV1\",\n \"RadrootsNip09SuppressionEvidenceV1\",\n \"RadrootsNip09SuppressionOutcome\",\n \"RadrootsNip09SuppressionReason\",\n \"RadrootsStoreProducedCanonicalEventV1\",\n \"RadrootsStoredFoodAvailabilityImageV1\",\n \"RadrootsStoredFoodAvailabilityV1\"\n ],\n \"added_symbols\": [\n \"RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1\",\n \"RadrootsEventStoreSourceCapacityResourceV1\",\n \"RadrootsEventStoreSourceCapacityV1\"\n ],\n \"methods\": [\n \"RadrootsEventStore::source_capacity_v1\",\n \"RadrootsEventStoreSourceCapacityResourceV1::as_str\",\n \"RadrootsEventStoreSourceCapacityV1::source_generation\",\n \"RadrootsEventStoreSourceCapacityV1::raw_event_count\",\n \"RadrootsEventStoreSourceCapacityV1::raw_tag_count\",\n \"RadrootsEventStoreSourceCapacityV1::raw_event_text_bytes\",\n \"RadrootsEventStoreSourceCapacityV1::raw_tag_text_bytes\",\n \"RadrootsEventStoreSourceCapacityV1::raw_high_water_seq\",\n \"RadrootsEventStoreSourceCapacityV1::retained_generation_count\",\n \"RadrootsEventStoreSourceCapacityV1::retained_generation_limit\"\n ],\n \"error_variants\": [\n \"SourceCapacityExceeded\",\n \"SourceGenerationHistoryLimitReached\",\n \"PersistedEphemeralRawEvent\",\n \"SourceCapacityStateDrift\",\n \"SqliteMainDatabaseEncodingNotUtf8\",\n \"RollbackWouldDiscardSourceGenerationHistory\"\n ],\n \"removed_symbols\": [\n \"RadrootsEventStoreReconciliationResource\",\n \"RadrootsEventStoreError::ReconciliationCapacityExceeded\"\n ],\n \"breaking_replacements\": [\n {\n \"removed\": \"RadrootsEventStoreReconciliationResource\",\n \"replacement\": \"RadrootsEventStoreSourceCapacityResourceV1\"\n },\n {\n \"removed\": \"RadrootsEventStoreError::ReconciliationCapacityExceeded\",\n \"replacement\": \"RadrootsEventStoreError::SourceCapacityExceeded\"\n }\n ]\n },\n \"result_vector\": {\n \"canonical_path\": \"contracts/conformance/vectors/event_store/source_maintenance.v1.json\",\n \"mirror_path\": \"crates/event_store/tests/fixtures/source_maintenance.v1.json\",\n \"byte_length\": 16253,\n \"sha256\": \"997aba2604a2b9d199fb87dc9d07942ca50d91863aeadcf3eeacf16d191dd71f\",\n \"hash_algorithm\": \"sha256_bytes_v1\",\n \"executor_id\": \"radroots_event_store.source_maintenance_v1.result_vector_executor.v1\",\n \"executor_path\": \"crates/event_store/tests/source_maintenance_v1_result_vector.rs\",\n \"executor_test\": \"source_maintenance_v1_result_vector\",\n \"executor_byte_length\": 23510,\n \"executor_sha256\": \"a7487afdfe19fc5fc794811d0f0e6035203e1aabcf0a33a1d398f6b3555d38f3\",\n \"executor_hash_algorithm\": \"sha256_bytes_v1\"\n }\n}\n"; -pub(crate) const SOURCE_MAINTENANCE_MANIFEST_BYTE_LENGTH: usize = 30013; +pub(crate) const SOURCE_MAINTENANCE_MANIFEST_JSON: &str = "{\n \"schema_version\": 1,\n \"contract_id\": \"radroots_event_store.source_maintenance_v1\",\n \"hook_id\": \"source_maintenance_v1\",\n \"manifest_schema\": {\n \"path\": \"crates/event_store/contracts/source_maintenance_v1.manifest.schema.json\",\n \"byte_length\": 12315,\n \"sha256\": \"96ea98a5abf3348b61565954cd34f2c622411d8dd34205ef04134bbe0e3d3f2b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"predecessor\": {\n \"hook_id\": \"food_availability_projection_v1\",\n \"manifest\": {\n \"path\": \"crates/event_store/contracts/food_availability_projection_v1.manifest.json\",\n \"byte_length\": 17455,\n \"sha256\": \"02dfe1b450fbdac16e718888215b4dd5c85d8975440fa21e8f439fb24c2b2990\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n },\n \"migration\": {\n \"version\": 4,\n \"name\": \"source_maintenance\",\n \"up\": {\n \"path\": \"crates/event_store/migrations/0004_source_maintenance.up.sql\",\n \"byte_length\": 19841,\n \"sha256\": \"ab2724188f8d08c897eebea2533a635e7c74282a25e84e4c0c37e78b08837a43\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"down\": {\n \"path\": \"crates/event_store/migrations/0004_source_maintenance.down.sql\",\n \"byte_length\": 5172,\n \"sha256\": \"fe44fd53c51545c08ea479b385e6781079dab70fc63da2a3c205d727a00ce860\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"schema_sha256\": \"074f85b663444ac150239ecd8441ea4a96ad83a798a55e22d2e5e2f7ee943a8c\",\n \"catalog\": {\n \"objects\": [\n \"radroots_event_store_source_capacity_delete_guard\",\n \"radroots_event_store_source_capacity_insert_guard\",\n \"radroots_event_store_source_capacity_marker_close_guard\",\n \"radroots_event_store_source_capacity_update_guard\",\n \"radroots_event_store_source_capacity_v1\",\n \"radroots_event_store_source_generation_capacity_advance\",\n \"radroots_event_store_source_generation_capacity_guard\"\n ],\n \"replaced_objects\": [\n \"radroots_event_store_food_availability_image_delete_guard\",\n \"radroots_event_store_food_availability_projection_delete_guard\",\n \"radroots_event_store_source_rebuild_marker_insert_guard\"\n ],\n \"tables\": [\n \"radroots_event_store_source_capacity_v1\"\n ],\n \"fts5_tables\": []\n }\n },\n \"source_maintenance\": {\n \"version\": 1,\n \"event_contract_registry_version\": 7,\n \"capacity_authority_id\": \"radroots_event_store_source_capacity_v1\",\n \"accounting\": {\n \"algorithm\": \"sqlite_cast_blob_octet_sum_v1\",\n \"raw_event_columns\": [\n \"event_id\",\n \"pubkey\",\n \"tags_json\",\n \"content\",\n \"sig\",\n \"raw_json\"\n ],\n \"raw_tag_columns\": [\n \"event_id\",\n \"tag_name\",\n \"tag_value\",\n \"tag_json\"\n ],\n \"nullable_raw_tag_columns\": [\n \"tag_value\"\n ]\n },\n \"limits\": {\n \"raw_events\": 25000,\n \"raw_tags\": 250000,\n \"raw_event_text_bytes\": 67108864,\n \"raw_tag_text_bytes\": 33554432,\n \"retained_source_generations\": 8\n },\n \"reopen_validation\": {\n \"mode\": \"bounded_full_raw_recount_v1\",\n \"raw_event_rejection_scan_bound\": 25001,\n \"raw_tag_rejection_scan_bound\": 250001,\n \"generation_history_validation\": \"bounded_count_plus_active_ordinal_v1\",\n \"retained_generation_rejection_scan_bound\": 9\n },\n \"rebuild_seal\": {\n \"nip09_hook_id\": \"nip09_reconciliation_v1\",\n \"nip09_manifest_sha256\": \"74af832420ffbaa9805e89df3c0b34f126a443e1598f757e3372f407f9003b77\",\n \"food_hook_id\": \"food_availability_projection_v1\",\n \"food_manifest_sha256\": \"02dfe1b450fbdac16e718888215b4dd5c85d8975440fa21e8f439fb24c2b2990\",\n \"food_scope_fingerprint_sha256\": \"8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0\",\n \"active_generation_authority\": \"radroots_event_store_source_state\",\n \"marker_close_authority\": \"radroots_event_store_source_capacity_marker_close_guard\"\n }\n },\n \"entry_points\": [\n {\n \"role\": \"migration_registry\",\n \"rust_path\": \"radroots_event_store::migrations::EVENT_STORE_MIGRATIONS[3]\"\n },\n {\n \"role\": \"migration_apply_hook\",\n \"rust_path\": \"radroots_event_store::schema::apply_migration_hook\"\n },\n {\n \"role\": \"migration_validation_hook\",\n \"rust_path\": \"radroots_event_store::schema::validate_migration_hook_state\"\n },\n {\n \"role\": \"capacity_query\",\n \"rust_path\": \"radroots_event_store::RadrootsEventStore::source_capacity_v1\"\n },\n {\n \"role\": \"raw_append_preflight\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::preflight_unique_raw_source_append_v1\"\n },\n {\n \"role\": \"raw_append_advance\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::advance_source_capacity_after_insert_v1\"\n },\n {\n \"role\": \"generation_append_preflight\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::preflight_source_generation_append_v1\"\n },\n {\n \"role\": \"generation_rebuild_bind\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::bind_source_capacity_to_generation_v1\"\n },\n {\n \"role\": \"sqlite_encoding_preflight\",\n \"rust_path\": \"radroots_event_store::store::validate_main_database_encoding\"\n },\n {\n \"role\": \"source_generation_history_rollback_guard\",\n \"rust_path\": \"radroots_event_store::schema::validate_rollback_preserves_source_generation_history\"\n },\n {\n \"role\": \"result_vector_executor\",\n \"rust_path\": \"source_maintenance_v1_result_vector\"\n }\n ],\n \"source_files\": [\n {\n \"role\": \"workspace_dependency_authority\",\n \"path\": \"Cargo.toml\",\n \"byte_length\": 12385,\n \"sha256\": \"c53c5c5a464267d7df850e03fdfa99dcde0b469972cf921f9b81c18b52a9c4b2\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_currency_value_authority\",\n \"path\": \"crates/core/src/currency.rs\",\n \"byte_length\": 4142,\n \"sha256\": \"42e6da2d8d2fdd6955dc2d83e98dd00266e02bc910b69923b117662c05089d14\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_decimal_value_authority\",\n \"path\": \"crates/core/src/decimal.rs\",\n \"byte_length\": 7839,\n \"sha256\": \"4b1d681a92d7a9e074bee7e1f20eb1499bb05bfc6528a6c9ba69fa7d00416550\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_money_value_authority\",\n \"path\": \"crates/core/src/money.rs\",\n \"byte_length\": 8025,\n \"sha256\": \"3f30bc21e21951a62fdc5d4033736ed42a883df590e8403940775bd2edfeebc5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_percent_value_authority\",\n \"path\": \"crates/core/src/percent.rs\",\n \"byte_length\": 2652,\n \"sha256\": \"42ceab109881329f3539d06a0ee0381ab4359985fe185a18d58a9c9558c5eb7f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_quantity_value_authority\",\n \"path\": \"crates/core/src/quantity.rs\",\n \"byte_length\": 6785,\n \"sha256\": \"d14e619da3829cdaf3ca7385fb892d330c286c1e21455c135c37518cbb588b5f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_quantity_price_value_authority\",\n \"path\": \"crates/core/src/quantity_price.rs\",\n \"byte_length\": 7061,\n \"sha256\": \"d60f70377099470ed283f45868a5119d7bb46143cb250b5082a4c038ba7f6cdc\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_unit_value_authority\",\n \"path\": \"crates/core/src/unit.rs\",\n \"byte_length\": 10023,\n \"sha256\": \"550e9582b6a5290aac3f637b83853b21fefcbab92c432d067c83448f4b2fe3e9\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_public_surface\",\n \"path\": \"crates/blossom/src/lib.rs\",\n \"byte_length\": 471,\n \"sha256\": \"821c0fb1c8a8832b89ec318192d4a054581ca7f9c208e8e02d7aa198184fe0e1\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_authorization_authority\",\n \"path\": \"crates/blossom/src/authorization.rs\",\n \"byte_length\": 38819,\n \"sha256\": \"68e1b118232953ce7cc63d078aa13804dd17da6ed7f223dd6b106743fb5112a4\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_descriptor_authority\",\n \"path\": \"crates/blossom/src/descriptor.rs\",\n \"byte_length\": 12302,\n \"sha256\": \"ead743e60b12e624cf40494678c14b4733dcf7930438a0d46f301c81f1f2e9a4\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_error_authority\",\n \"path\": \"crates/blossom/src/error.rs\",\n \"byte_length\": 16267,\n \"sha256\": \"679afd92e874513f1f2849603317a0b0f1eba318ad213de1cdaed333ddd39e5c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_hash_authority\",\n \"path\": \"crates/blossom/src/hash.rs\",\n \"byte_length\": 10206,\n \"sha256\": \"b2d9eadfbd6260fc6ae2fc1a4639e901f28ca268bde3f2b10461f2f75c9880af\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_media_type_authority\",\n \"path\": \"crates/blossom/src/media_type.rs\",\n \"byte_length\": 2101,\n \"sha256\": \"e0d953fc9b38d49560e02a005277f9e91a25bd890d9f1654529df9d8ab7eb029\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_url_authority\",\n \"path\": \"crates/blossom/src/url.rs\",\n \"byte_length\": 14079,\n \"sha256\": \"a5d62d83bd123ce9f485f7c4c96981f1eb19fe8467ce91d1fbba6ce77c461df4\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_contract_facade\",\n \"path\": \"crates/event/src/contract.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_contract_registry_v7_authority\",\n \"path\": \"crates/event/src/contract/registry_v7.rs\",\n \"byte_length\": 155743,\n \"sha256\": \"2ed52d1279390ba073fa85a2d41020eb8b8e6fa9fc7268029182a95487b5eddd\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_envelope_authority\",\n \"path\": \"crates/event/src/envelope.rs\",\n \"byte_length\": 31165,\n \"sha256\": \"97a3bb38d630666bba497b98e856ee457d6899de230dabf6cab786a5bd55a3ce\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_head_facade\",\n \"path\": \"crates/event/src/event_head.rs\",\n \"byte_length\": 68,\n \"sha256\": \"cb52f6006f7ecd862707d6b048f9fc407e0cd5ebcd3091b3c54e195ffa5cba64\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_head_v1_authority\",\n \"path\": \"crates/event/src/event_head/v1.rs\",\n \"byte_length\": 7529,\n \"sha256\": \"0a4b50c4b7a93e9d88cc995cf69d9e87b341cba66d7feb51c357dfa4e5b2de37\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_ids_authority\",\n \"path\": \"crates/event/src/ids.rs\",\n \"byte_length\": 46282,\n \"sha256\": \"f3c34e7dcf09fc225cb9c12f02ae932d5155c859f73f8b8573db8e5ebf298678\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_trade_authority\",\n \"path\": \"crates/event/src/trade.rs\",\n \"byte_length\": 68376,\n \"sha256\": \"1012b44295cb26844206304bc24b355a49c18a023844f6b3905337c63b1417a7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_kinds_authority\",\n \"path\": \"crates/event/src/kinds.rs\",\n \"byte_length\": 34961,\n \"sha256\": \"8b3ce6193cab1f7e1587d0c1b2880a81b0aa43c77d89671b602772db37edcebf\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_tags_authority\",\n \"path\": \"crates/event/src/tags.rs\",\n \"byte_length\": 4530,\n \"sha256\": \"cb8f12a639fc72d9238e32495d9e67e928e6627fb8c94ec0a58c5d95e7373cd6\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_draft_authority\",\n \"path\": \"crates/event/src/draft.rs\",\n \"byte_length\": 58674,\n \"sha256\": \"fa4e802f1143eba72ba7fc334104ea0d6613c886725ff9f3e61dab2443c243b6\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_calendar_authority\",\n \"path\": \"crates/event/src/calendar.rs\",\n \"byte_length\": 103819,\n \"sha256\": \"f86464c4be674a75902e57f25b73130bda7538f2c4f2e29ebde59fe51c9b67c4\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_classified_listing_authority\",\n \"path\": \"crates/event/src/classified_listing.rs\",\n \"byte_length\": 8226,\n \"sha256\": \"745aa08953a6243ea1afad22aa63e44f6746f2f243ab0d6b617d6ab64fa58749\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_profile_authority\",\n \"path\": \"crates/event/src/profile.rs\",\n \"byte_length\": 18848,\n \"sha256\": \"7c7ac7f4926938f5cb5530be5450e5e23f18f216bdb222150fbdd451cec20d5c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_post_authority\",\n \"path\": \"crates/event/src/post.rs\",\n \"byte_length\": 22738,\n \"sha256\": \"7ed4a4c39bed40b134369d16e1fc35ab2c7d27ee300dd36d367025def2283b39\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_comment_authority\",\n \"path\": \"crates/event/src/comment.rs\",\n \"byte_length\": 36375,\n \"sha256\": \"f4d1a57780eff0d315bc4e2fbad316dfb7c7ced1bcc9e8e85afe0b5ecdaf3aa8\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_food_availability_authority\",\n \"path\": \"crates/event/src/food_availability.rs\",\n \"byte_length\": 45429,\n \"sha256\": \"b4357dfb6954631673b6cd58794015da29303cb54ce8405a57ad543870036c9e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_deletion_authority\",\n \"path\": \"crates/event/src/deletion.rs\",\n \"byte_length\": 31155,\n \"sha256\": \"c8b7ab79998ae62e03497f5511dd3950a41d73155edda3d8d34c65fc797e9d09\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_relay_hint_authority\",\n \"path\": \"crates/event/src/relay_hint.rs\",\n \"byte_length\": 14317,\n \"sha256\": \"1a14aea5d2f2620970dc1de9e3d311d9042cab4d5805a56461548168112a0801\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_media_authority\",\n \"path\": \"crates/event/src/media.rs\",\n \"byte_length\": 3533,\n \"sha256\": \"9a589cfca489626746fdc2eba7ed0aba4e524840aadce51d4fb11a1e7b3d32bb\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_social_authority\",\n \"path\": \"crates/event/src/social.rs\",\n \"byte_length\": 5483,\n \"sha256\": \"1b89c6d0954822e7f1e778479ed43694e88929bcd5310ee066f8f4ba6505045a\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_wire_facade\",\n \"path\": \"crates/event/src/wire.rs\",\n \"byte_length\": 68,\n \"sha256\": \"cb52f6006f7ecd862707d6b048f9fc407e0cd5ebcd3091b3c54e195ffa5cba64\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_wire_v1_authority\",\n \"path\": \"crates/event/src/wire/v1.rs\",\n \"byte_length\": 22133,\n \"sha256\": \"cc28c8cbd962b32ed2efe43e84c1e0d1ea3bd89636efff2d5553714072fce1a4\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_verification_facade\",\n \"path\": \"crates/event_codec/src/verification.rs\",\n \"byte_length\": 253,\n \"sha256\": \"b49a32df605035c87f295c0a151140d43d2e588c5e11b05183e5fc92993dae0e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_verification_v1_authority\",\n \"path\": \"crates/event_codec/src/verification/v1.rs\",\n \"byte_length\": 8751,\n \"sha256\": \"fc4c5d45e475151c56330ba9c4db70de603e4ec48adabd6365f80d5f6a3733f4\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_registry_v7_admission_authority\",\n \"path\": \"crates/event_codec/src/admission/registry_v7.rs\",\n \"byte_length\": 5350,\n \"sha256\": \"755e63dd7ad1115c219e5a3ea540bef67d2770fc9f2a5aa6ba97c925c99bdced\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_profile_inbound_facade\",\n \"path\": \"crates/event_codec/src/profile/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_profile_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/profile/inbound/registry_v7.rs\",\n \"byte_length\": 10394,\n \"sha256\": \"c37c225ea8153beb098a39c4a56203aa03382af12646f40cd3f8628663700702\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_post_inbound_facade\",\n \"path\": \"crates/event_codec/src/post/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_post_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/post/inbound/registry_v7.rs\",\n \"byte_length\": 16016,\n \"sha256\": \"bc0bb839c28f3588f05544c846c987528ea3934c75305fef70af8e027a97cc58\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_reply_inbound_facade\",\n \"path\": \"crates/event_codec/src/reply/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_reply_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/reply/inbound/registry_v7.rs\",\n \"byte_length\": 26812,\n \"sha256\": \"a72e9721ed13addca2a63942ac9e43c1daa8f1d1457eef08ec6a3ee5eb8872c5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_comment_inbound_facade\",\n \"path\": \"crates/event_codec/src/comment/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_comment_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/comment/inbound/registry_v7.rs\",\n \"byte_length\": 53783,\n \"sha256\": \"30d1b0155963b25d64685ddd042f423ef7707b111dc56ead15bd468e0433a432\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_deletion_facade\",\n \"path\": \"crates/event_codec/src/deletion/mod.rs\",\n \"byte_length\": 115,\n \"sha256\": \"17d56f82ddb0a86bc97abbcf3e037fe460ea31fff2548f527be4bf004dac3a95\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_deletion_reconciliation_v1_authority\",\n \"path\": \"crates/event_codec/src/deletion/reconciliation_v1.rs\",\n \"byte_length\": 38469,\n \"sha256\": \"3e06d1fa923ca5faabb1e95338c35650b0e20b078ff5c972e35ab845d8b7476c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_food_inbound_facade\",\n \"path\": \"crates/event_codec/src/food_availability/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_food_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/food_availability/inbound/registry_v7.rs\",\n \"byte_length\": 26775,\n \"sha256\": \"379321c2f9702773ae94304000e94104af0ab9ad165870c8c57f74bbb2c096c3\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_job_traits_authority\",\n \"path\": \"crates/event_codec/src/job/traits.rs\",\n \"byte_length\": 5029,\n \"sha256\": \"353df3703de4d99fc318474952dc6c95cbce9a19756f3e235ed2d715f36c4364\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_dependency_authority\",\n \"path\": \"crates/event_store/Cargo.toml\",\n \"byte_length\": 1569,\n \"sha256\": \"30f43004bcf0ffa9b9b6f338cba8c56be0265bc1cee708e93f7b7dc54ed60f9c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_error_and_limits\",\n \"path\": \"crates/event_store/src/error.rs\",\n \"byte_length\": 19517,\n \"sha256\": \"4bf24ee1aa6117597ab467b1379a05549b79024213dc7622a2d276bc26fffab4\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"generated_descriptor_registration\",\n \"path\": \"crates/event_store/src/generated.rs\",\n \"byte_length\": 144,\n \"sha256\": \"6b0a8d6f249bd4fc3f878d37cb5e418680f0f1be2d9eec2518dedf03efc47121\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"public_surface\",\n \"path\": \"crates/event_store/src/lib.rs\",\n \"byte_length\": 3844,\n \"sha256\": \"3cd9653bcb752fb3c4442d4904b98a0a6208011a9a238125b7b7073d7f4e312b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"migration_registry\",\n \"path\": \"crates/event_store/src/migrations.rs\",\n \"byte_length\": 73585,\n \"sha256\": \"a47477d04759ec6e71d14d3d05459864eae78fce0e3c2481a1b03147f04637f7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_model_public_surface\",\n \"path\": \"crates/event_store/src/model.rs\",\n \"byte_length\": 33596,\n \"sha256\": \"98b1742ffb517f66f8e9edefa61d1c3cf3e22be5985202c6c340310a98c72c8e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_reconciliation_v1_model\",\n \"path\": \"crates/event_store/src/model/reconciliation_v1.rs\",\n \"byte_length\": 11138,\n \"sha256\": \"8a26bc373035878ef9b41767ceea7b681896e17d88bedce118de1d622125e1d6\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_reconciliation_v1_ingest_model\",\n \"path\": \"crates/event_store/src/model/ingest_reconciliation_v1.rs\",\n \"byte_length\": 1626,\n \"sha256\": \"47bf13b3fc0f8a913a660f7d655413de0f6b90568bc4acc510aa6bd741bab47b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"addressable_transition_feed_model\",\n \"path\": \"crates/event_store/src/model/addressable_transition_feed_v1.rs\",\n \"byte_length\": 22279,\n \"sha256\": \"f8ea454a0e998a3d433bba52d1ff9759a04fcb91db286cf2a2b10ef7d0057694\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"food_projection_model\",\n \"path\": \"crates/event_store/src/model/food_availability_projection_v1.rs\",\n \"byte_length\": 17437,\n \"sha256\": \"6fdfc8c5eb3dd9c451fd47b2394af123b50f7fe6d74113f8777221a8fa31ba30\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_generation_rebuild_authority\",\n \"path\": \"crates/event_store/src/nip09/reconciliation_v1.rs\",\n \"byte_length\": 184402,\n \"sha256\": \"7fa01e52f51d3470ba60e6068dc05797c2a6eaf5eb317d23111a961dbe4451f9\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"schema_migration_and_reopen_authority\",\n \"path\": \"crates/event_store/src/schema.rs\",\n \"byte_length\": 146146,\n \"sha256\": \"93b060e80d3edd73f86208e4bf698fa9d53eaf1eeb04526c9261fb8b5726fb0d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"public_store_and_transaction_authority\",\n \"path\": \"crates/event_store/src/store.rs\",\n \"byte_length\": 394732,\n \"sha256\": \"6710a4b3e366b724f6b3d4904e2403c15456f23111996945931d2ba20e301f52\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"addressable_transition_feed_store\",\n \"path\": \"crates/event_store/src/store/addressable_transition_feed_v1.rs\",\n \"byte_length\": 40254,\n \"sha256\": \"de656face46735b938b7b20780687404eade47885871129cc5a7c009155d6336\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"food_projection_store\",\n \"path\": \"crates/event_store/src/store/food_availability_projection_v1.rs\",\n \"byte_length\": 48971,\n \"sha256\": \"189b824af305f39c7f71a285f4746455ff30f1a27b24b95d8d21223b41772152\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_post_core_v1_extension\",\n \"path\": \"crates/event_store/src/store/post_core_extensions_v1.rs\",\n \"byte_length\": 6923,\n \"sha256\": \"2fbfc6252f9ce249468a99b5230fd6b7b59b9e47ee4f38783c5fd9379c823fd0\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_post_core_v1_storage\",\n \"path\": \"crates/event_store/src/store/post_core_storage_v1.rs\",\n \"byte_length\": 16876,\n \"sha256\": \"f27dc7dcd643762226f8d4df0d297027b384bcf1004277222e978965bc2d40cf\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"raw_ingest_capacity_authority\",\n \"path\": \"crates/event_store/src/store/protocol_reconciliation_v1.rs\",\n \"byte_length\": 30145,\n \"sha256\": \"a8a26763dc39124ab7537fdcc3f89a1f0aa7e1830f4f9dec9ee4057bbb4fd228\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_protocol_storage\",\n \"path\": \"crates/event_store/src/store/protocol_storage_v1.rs\",\n \"byte_length\": 10929,\n \"sha256\": \"20de4436945bac0d8cbeca0ae6cd95232a92da8cd6be6ae6beacba17fe50d658\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_maintenance_runtime\",\n \"path\": \"crates/event_store/src/source_maintenance_v1.rs\",\n \"byte_length\": 51762,\n \"sha256\": \"5db9c9e86fd18a2fcc08dbc3468f373bf39d8948bc6a08936b252a4dda3546e9\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_food_result_vector_executor\",\n \"path\": \"crates/event_store/tests/food_availability_projection_v1_result_vector.rs\",\n \"byte_length\": 34070,\n \"sha256\": \"aa098bcb0f975f690ca9535d3cbb76c6e57e6d70fd3a892c4399fd35fe8ec21c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"artifact_transaction_authority\",\n \"path\": \"tools/xtask/src/contract/artifact_bundle.rs\",\n \"byte_length\": 38279,\n \"sha256\": \"f326ea57b56d40135f95b6b1e15961f66eed363337180a6d12a5ea903e1a9a29\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_successor_governance\",\n \"path\": \"tools/xtask/src/contract/food_availability_projection.rs\",\n \"byte_length\": 197131,\n \"sha256\": \"6b71c927b41404f633dfbd0f8e321dcdab933285a196671f27046c0c15d55434\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"transitive_predecessor_membership_governance\",\n \"path\": \"tools/xtask/src/contract/nip09_reconciliation.rs\",\n \"byte_length\": 835884,\n \"sha256\": \"c266610ba4a55e9fc3b5e80c52c7b244aa9fac7336c7a7cd37489a8bbdb97f1d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_maintenance_governance\",\n \"path\": \"tools/xtask/src/contract/source_maintenance.rs\",\n \"byte_length\": 188436,\n \"sha256\": \"ce21d4d200142b361ee0f65bd8063a461fd01f016e92eec7f6576863a0856b05\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"contract_command_authority\",\n \"path\": \"tools/xtask/src/contract.rs\",\n \"byte_length\": 504182,\n \"sha256\": \"7edf6ffda5dec9e08b66ad100d9119a705c994af692fd287e90700830de12281\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"xtask_dispatch_and_release_preflight\",\n \"path\": \"tools/xtask/src/main.rs\",\n \"byte_length\": 15109,\n \"sha256\": \"bf721ecaa94225908496f247bf65011e669aa61b732594c23d64e306b4ac38cc\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n ],\n \"public_api\": {\n \"inherited_predecessor_symbols\": [\n \"RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_D_TAG_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_PROJECTION_APPLY_PAGE_LIMIT_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_BYTES_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1\",\n \"RadrootsAddressableTransitionCauseV1\",\n \"RadrootsAddressableTransitionCoordinateV1\",\n \"RadrootsAddressableTransitionCursorV1\",\n \"RadrootsAddressableTransitionEventReferenceV1\",\n \"RadrootsAddressableTransitionOriginV1\",\n \"RadrootsAddressableTransitionPageV1\",\n \"RadrootsAddressableTransitionRawHeadDecisionV1\",\n \"RadrootsAddressableTransitionScopeFingerprintV1\",\n \"RadrootsAddressableTransitionScopeV1\",\n \"RadrootsAddressableTransitionV1\",\n \"RadrootsAddressableTransitionVisibilityV1\",\n \"RadrootsCurrentEventVisibilityV1\",\n \"RadrootsCurrentVisibilityDecisionV1\",\n \"RadrootsFoodAvailabilitySearchQueryV1\",\n \"RadrootsFoodAvailabilityStatusFilterV1\",\n \"RadrootsNip09SuppressionEvidenceV1\",\n \"RadrootsNip09SuppressionOutcome\",\n \"RadrootsNip09SuppressionReason\",\n \"RadrootsStoreProducedCanonicalEventV1\",\n \"RadrootsStoredFoodAvailabilityImageV1\",\n \"RadrootsStoredFoodAvailabilityV1\"\n ],\n \"added_symbols\": [\n \"RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1\",\n \"RadrootsEventStoreSourceCapacityResourceV1\",\n \"RadrootsEventStoreSourceCapacityV1\"\n ],\n \"methods\": [\n \"RadrootsEventStore::source_capacity_v1\",\n \"RadrootsEventStoreSourceCapacityResourceV1::as_str\",\n \"RadrootsEventStoreSourceCapacityV1::source_generation\",\n \"RadrootsEventStoreSourceCapacityV1::raw_event_count\",\n \"RadrootsEventStoreSourceCapacityV1::raw_tag_count\",\n \"RadrootsEventStoreSourceCapacityV1::raw_event_text_bytes\",\n \"RadrootsEventStoreSourceCapacityV1::raw_tag_text_bytes\",\n \"RadrootsEventStoreSourceCapacityV1::raw_high_water_seq\",\n \"RadrootsEventStoreSourceCapacityV1::retained_generation_count\",\n \"RadrootsEventStoreSourceCapacityV1::retained_generation_limit\"\n ],\n \"error_variants\": [\n \"SourceCapacityExceeded\",\n \"SourceGenerationHistoryLimitReached\",\n \"PersistedEphemeralRawEvent\",\n \"SourceCapacityStateDrift\",\n \"SqliteMainDatabaseEncodingNotUtf8\",\n \"RollbackWouldDiscardSourceGenerationHistory\"\n ],\n \"removed_symbols\": [\n \"RadrootsEventStoreReconciliationResource\",\n \"RadrootsEventStoreError::ReconciliationCapacityExceeded\"\n ],\n \"breaking_replacements\": [\n {\n \"removed\": \"RadrootsEventStoreReconciliationResource\",\n \"replacement\": \"RadrootsEventStoreSourceCapacityResourceV1\"\n },\n {\n \"removed\": \"RadrootsEventStoreError::ReconciliationCapacityExceeded\",\n \"replacement\": \"RadrootsEventStoreError::SourceCapacityExceeded\"\n }\n ]\n },\n \"result_vector\": {\n \"canonical_path\": \"contracts/conformance/vectors/event_store/source_maintenance.v1.json\",\n \"mirror_path\": \"crates/event_store/tests/fixtures/source_maintenance.v1.json\",\n \"byte_length\": 16253,\n \"sha256\": \"997aba2604a2b9d199fb87dc9d07942ca50d91863aeadcf3eeacf16d191dd71f\",\n \"hash_algorithm\": \"sha256_bytes_v1\",\n \"executor_id\": \"radroots_event_store.source_maintenance_v1.result_vector_executor.v1\",\n \"executor_path\": \"crates/event_store/tests/source_maintenance_v1_result_vector.rs\",\n \"executor_test\": \"source_maintenance_v1_result_vector\",\n \"executor_byte_length\": 23510,\n \"executor_sha256\": \"a7487afdfe19fc5fc794811d0f0e6035203e1aabcf0a33a1d398f6b3555d38f3\",\n \"executor_hash_algorithm\": \"sha256_bytes_v1\"\n }\n}\n"; +pub(crate) const SOURCE_MAINTENANCE_MANIFEST_BYTE_LENGTH: usize = 31374; pub(crate) const SOURCE_MAINTENANCE_MANIFEST_SHA256: &str = - "f5f6b2c84a5231c944b9122a125e6d68f9c4498a1c55426dbd86847ac6a2b4f6"; + "922a53eeb6ee233e4b4349a89e867939c83d3b7441dacbbe1b7c90966817e47d"; pub(crate) const SOURCE_MAINTENANCE_MANIFEST_SCHEMA_VERSION: u32 = 1; pub(crate) const SOURCE_MAINTENANCE_CONTRACT_ID: &str = "radroots_event_store.source_maintenance_v1"; diff --git a/crates/event_store/src/model/food_availability_projection_v1.rs b/crates/event_store/src/model/food_availability_projection_v1.rs @@ -2,7 +2,7 @@ use super::{ RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1, RadrootsEventStoreSourceGeneration, }; use crate::RadrootsEventStoreError; -use radroots_blossom::RadrootsBlossomSha256; +use radroots_blossom::Sha256; use radroots_event::{ food_availability::{ RADROOTS_FOOD_IMAGE_MAX_COUNT, RadrootsFoodAvailabilityStatus, RadrootsFoodContent, @@ -160,7 +160,7 @@ pub struct RadrootsStoredFoodAvailabilityImageV1 { raw_tag: Vec<String>, url: Option<String>, dimensions: Option<RadrootsFoodImageDimensions>, - blossom_sha256: Option<RadrootsBlossomSha256>, + blossom_sha256: Option<Sha256>, diagnostics: Vec<RadrootsFoodAvailabilityImageDiagnostic>, } @@ -200,7 +200,7 @@ impl RadrootsStoredFoodAvailabilityImageV1 { self.dimensions } - pub const fn blossom_sha256(&self) -> Option<RadrootsBlossomSha256> { + pub const fn blossom_sha256(&self) -> Option<Sha256> { self.blossom_sha256 } diff --git a/crates/event_store/src/store.rs b/crates/event_store/src/store.rs @@ -3143,7 +3143,7 @@ mod tests { assert_eq!( projected.images()[0].blossom_sha256(), Some( - radroots_blossom::RadrootsBlossomSha256::from_hex( + radroots_blossom::Sha256::from_hex( "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", ) .expect("Blossom digest"), @@ -6652,7 +6652,7 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);", assert_eq!( projected.images()[0].blossom_sha256(), Some( - radroots_blossom::RadrootsBlossomSha256::from_hex( + radroots_blossom::Sha256::from_hex( "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", ) .expect("Blossom digest"), @@ -6662,7 +6662,7 @@ CREATE TABLE aux.event_transport_observation (event_id TEXT);", assert_eq!( projected.images()[1].blossom_sha256(), Some( - radroots_blossom::RadrootsBlossomSha256::from_hex( + radroots_blossom::Sha256::from_hex( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ) .expect("Blossom digest"), diff --git a/crates/event_store/src/store/food_availability_projection_v1.rs b/crates/event_store/src/store/food_availability_projection_v1.rs @@ -832,7 +832,7 @@ fn validate_image_rows( let stored_blossom_sha256 = row .blossom_sha256 .map(|value| { - radroots_blossom::RadrootsBlossomSha256::from_hex(value.as_str()).map_err(|error| { + radroots_blossom::Sha256::from_hex(value.as_str()).map_err(|error| { projection_drift(format!("stored Blossom digest is invalid: {error}")) }) }) diff --git a/crates/event_store/tests/food_availability_projection_v1_result_vector.rs b/crates/event_store/tests/food_availability_projection_v1_result_vector.rs @@ -1,6 +1,6 @@ #![forbid(unsafe_code)] -use radroots_blossom::RadrootsBlossomSha256; +use radroots_blossom::Sha256; use radroots_event::food_availability::RadrootsFoodIdentifier; use radroots_event_store::{ RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1, @@ -14,7 +14,7 @@ use radroots_event_store::{ }; use radroots_identity::PublicKey; use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; +use sha2::{Digest, Sha256 as Sha256Hasher}; const RESULT_VECTOR_EXECUTOR_ID: &str = "radroots_event_store.food_availability_projection_v1.result_vector_executor.v1"; @@ -305,7 +305,7 @@ async fn food_availability_projection_v1_result_vector() { } fn sha256_hex(bytes: &[u8]) -> String { - hex::encode(Sha256::digest(bytes)) + hex::encode(Sha256Hasher::digest(bytes)) } fn raw_head_decision_code(decision: &RadrootsRawHeadDecision) -> &'static str { @@ -969,7 +969,7 @@ fn assert_projection( "{case_id}" ); let expected_blossom_sha256 = expected.blossom_sha256.0.as_deref().map(|value| { - RadrootsBlossomSha256::from_hex(value) + Sha256::from_hex(value) .unwrap_or_else(|error| panic!("{case_id}: expected Blossom digest: {error}")) }); assert_eq!( diff --git a/crates/nostr/src/blossom.rs b/crates/nostr/src/blossom.rs @@ -8,9 +8,11 @@ use base64::{ engine::general_purpose::{GeneralPurpose, NO_PAD, URL_SAFE_NO_PAD}, }; use radroots_blossom::{ - RADROOTS_BLOSSOM_AUTHORIZATION_EVENT_KIND, RadrootsBlossomAuthoredUploadClaim, - RadrootsBlossomAuthorizationValidation, RadrootsBlossomError, - RadrootsBlossomParsedAuthorizationClaim, RadrootsBlossomValidatedAuthorizationClaim, + Error, + authorization::{ + AuthoredUploadClaim, AuthorizationClaim, AuthorizationValidation, + RADROOTS_BLOSSOM_AUTHORIZATION_EVENT_KIND, ValidatedAuthorizationClaim, + }, }; use crate::types::{ @@ -89,7 +91,7 @@ impl AsRef<str> for RadrootsNostrBlossomAuthorizationHeader { #[derive(Clone, PartialEq, Eq)] pub struct RadrootsNostrVerifiedBlossomAuthorization { event: RadrootsNostrEvent, - claim: RadrootsBlossomValidatedAuthorizationClaim, + claim: ValidatedAuthorizationClaim, } impl fmt::Debug for RadrootsNostrVerifiedBlossomAuthorization { @@ -116,7 +118,7 @@ impl RadrootsNostrVerifiedBlossomAuthorization { self.event.created_at } - pub fn claim(&self) -> &RadrootsBlossomValidatedAuthorizationClaim { + pub fn claim(&self) -> &ValidatedAuthorizationClaim { &self.claim } } @@ -137,7 +139,7 @@ pub enum RadrootsNostrBlossomError { InvalidEventId, InvalidEventSignature, EventSigning, - BlossomClaim(RadrootsBlossomError), + BlossomClaim(Error), } impl RadrootsNostrBlossomError { @@ -159,7 +161,7 @@ impl RadrootsNostrBlossomError { } } - pub fn blossom_claim_error(&self) -> Option<&RadrootsBlossomError> { + pub fn blossom_claim_error(&self) -> Option<&Error> { match self { Self::BlossomClaim(error) => Some(error), _ => None, @@ -210,8 +212,8 @@ impl fmt::Display for RadrootsNostrBlossomError { impl std::error::Error for RadrootsNostrBlossomError {} -impl From<RadrootsBlossomError> for RadrootsNostrBlossomError { - fn from(error: RadrootsBlossomError) -> Self { +impl From<Error> for RadrootsNostrBlossomError { + fn from(error: Error) -> Self { Self::BlossomClaim(error) } } @@ -219,7 +221,7 @@ impl From<RadrootsBlossomError> for RadrootsNostrBlossomError { /// Sign a direct kind-24242 event from a strict authored BUD-11 upload claim. pub fn radroots_nostr_sign_blossom_authorization( keys: &RadrootsNostrKeys, - claim: &RadrootsBlossomAuthoredUploadClaim, + claim: &AuthoredUploadClaim, ) -> Result<RadrootsNostrSignedBlossomAuthorization, RadrootsNostrBlossomError> { let wire = claim.wire_parts(); let tags = wire @@ -268,7 +270,7 @@ pub fn radroots_nostr_encode_blossom_authorization_header( /// Decode, authenticate, parse, and validate a BUD-11 authorization value. pub fn radroots_nostr_decode_verify_blossom_authorization_header( header: &str, - validation: &RadrootsBlossomAuthorizationValidation, + validation: &AuthorizationValidation, ) -> Result<RadrootsNostrVerifiedBlossomAuthorization, RadrootsNostrBlossomError> { if header.trim_start() != header { return Err(RadrootsNostrBlossomError::InvalidHeaderWhitespace); @@ -317,12 +319,8 @@ pub fn radroots_nostr_decode_verify_blossom_authorization_header( .iter() .map(|tag| tag.as_slice().to_vec()) .collect(); - let claim = RadrootsBlossomParsedAuthorizationClaim::parse( - &event.content, - event.created_at.as_secs(), - &tags, - )? - .validate(validation)?; + let claim = AuthorizationClaim::parse(&event.content, event.created_at.as_secs(), &tags)? + .validate(validation)?; Ok(RadrootsNostrVerifiedBlossomAuthorization { event, claim }) } diff --git a/crates/nostr/tests/blossom_conformance.rs b/crates/nostr/tests/blossom_conformance.rs @@ -2,11 +2,13 @@ use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; use radroots_blossom::{ - RADROOTS_BLOSSOM_AUTH_MAX_CREATED_AGE_SECONDS, RADROOTS_BLOSSOM_AUTH_MAX_HORIZON_SECONDS, - RADROOTS_BLOSSOM_AUTHORIZATION_EVENT_KIND, RadrootsBlossomAuthoredUploadClaim, - RadrootsBlossomAuthorizationContent, RadrootsBlossomAuthorizationTarget, - RadrootsBlossomAuthorizationValidation, RadrootsBlossomParsedAuthorizationClaim, - RadrootsBlossomServerDomain, RadrootsBlossomServerScopeRequirement, RadrootsBlossomSha256, + Sha256, + authorization::{ + AuthoredUploadClaim, AuthorizationClaim, AuthorizationContent, AuthorizationTarget, + AuthorizationValidation, RADROOTS_BLOSSOM_AUTH_MAX_CREATED_AGE_SECONDS, + RADROOTS_BLOSSOM_AUTH_MAX_HORIZON_SECONDS, RADROOTS_BLOSSOM_AUTHORIZATION_EVENT_KIND, + ServerDomain, ServerScopeRequirement, + }, }; use radroots_nostr::blossom::{ RadrootsNostrBlossomError, radroots_nostr_decode_verify_blossom_authorization_header, @@ -203,7 +205,7 @@ fn header_payload(header: &str) -> &str { &header[payload_start..] } -fn vector_validation(vector: &Vector) -> RadrootsBlossomAuthorizationValidation { +fn vector_validation(vector: &Vector) -> AuthorizationValidation { let validation = vector.input.get("validation").expect("validation input"); let target = validation.get("target").expect("validation target"); let target_type = target @@ -211,7 +213,7 @@ fn vector_validation(vector: &Vector) -> RadrootsBlossomAuthorizationValidation .and_then(Value::as_str) .expect("validation target type"); let target_hash = || { - RadrootsBlossomSha256::from_hex( + Sha256::from_hex( target .get("hash") .and_then(Value::as_str) @@ -220,15 +222,15 @@ fn vector_validation(vector: &Vector) -> RadrootsBlossomAuthorizationValidation .expect("valid validation target hash") }; let target = match target_type { - "get_blob" => RadrootsBlossomAuthorizationTarget::GetBlob(target_hash()), - "upload" => RadrootsBlossomAuthorizationTarget::Upload(target_hash()), - "list" => RadrootsBlossomAuthorizationTarget::List, - "delete_blob" => RadrootsBlossomAuthorizationTarget::DeleteBlob(target_hash()), - "mirror" => RadrootsBlossomAuthorizationTarget::Mirror(target_hash()), - "media" => RadrootsBlossomAuthorizationTarget::Media(target_hash()), + "get_blob" => AuthorizationTarget::GetBlob(target_hash()), + "upload" => AuthorizationTarget::Upload(target_hash()), + "list" => AuthorizationTarget::List, + "delete_blob" => AuthorizationTarget::DeleteBlob(target_hash()), + "mirror" => AuthorizationTarget::Mirror(target_hash()), + "media" => AuthorizationTarget::Media(target_hash()), other => panic!("{} has unsupported target {other}", vector.id), }; - let server = RadrootsBlossomServerDomain::parse( + let server = ServerDomain::parse( validation .get("server_domain") .and_then(Value::as_str) @@ -240,11 +242,11 @@ fn vector_validation(vector: &Vector) -> RadrootsBlossomAuthorizationValidation .and_then(Value::as_str) .expect("validation server_scope") { - "optional_any_match" => RadrootsBlossomServerScopeRequirement::OptionalAnyMatch, - "required_any_match" => RadrootsBlossomServerScopeRequirement::RequiredAnyMatch, + "optional_any_match" => ServerScopeRequirement::OptionalAnyMatch, + "required_any_match" => ServerScopeRequirement::RequiredAnyMatch, other => panic!("{} has unsupported server scope {other}", vector.id), }; - RadrootsBlossomAuthorizationValidation::new( + AuthorizationValidation::new( target, server, server_scope, @@ -304,17 +306,17 @@ fn keys() -> RadrootsNostrKeys { RadrootsNostrKeys::parse(SECRET_KEY).expect("fixed test secret key") } -fn hash() -> RadrootsBlossomSha256 { - RadrootsBlossomSha256::from_hex(HASH).expect("fixed test hash") +fn hash() -> Sha256 { + Sha256::from_hex(HASH).expect("fixed test hash") } -fn server() -> RadrootsBlossomServerDomain { - RadrootsBlossomServerDomain::parse(SERVER).expect("fixed test server") +fn server() -> ServerDomain { + ServerDomain::parse(SERVER).expect("fixed test server") } -fn authored_claim() -> RadrootsBlossomAuthoredUploadClaim { - RadrootsBlossomAuthoredUploadClaim::new( - RadrootsBlossomAuthorizationContent::parse(CONTENT).expect("fixed test content"), +fn authored_claim() -> AuthoredUploadClaim { + AuthoredUploadClaim::new( + AuthorizationContent::parse(CONTENT).expect("fixed test content"), server(), hash(), CREATED_AT, @@ -323,11 +325,11 @@ fn authored_claim() -> RadrootsBlossomAuthoredUploadClaim { .expect("fixed authored claim") } -fn validation() -> RadrootsBlossomAuthorizationValidation { - RadrootsBlossomAuthorizationValidation::new( - RadrootsBlossomAuthorizationTarget::Upload(hash()), +fn validation() -> AuthorizationValidation { + AuthorizationValidation::new( + AuthorizationTarget::Upload(hash()), server(), - RadrootsBlossomServerScopeRequirement::RequiredAnyMatch, + ServerScopeRequirement::RequiredAnyMatch, CREATED_AT + 1, RADROOTS_BLOSSOM_AUTH_MAX_CREATED_AGE_SECONDS, ) @@ -557,7 +559,7 @@ fn blossom_header_authenticates_before_claim_parsing() { ); let raw_tags: Vec<Vec<String>> = Vec::new(); - let pure_error = RadrootsBlossomParsedAuthorizationClaim::parse("", CREATED_AT, &raw_tags) + let pure_error = AuthorizationClaim::parse("", CREATED_AT, &raw_tags) .expect_err("empty content is not a claim"); let adapter_error = radroots_nostr_decode_verify_blossom_authorization_header( &raw_header(&malformed_claim), @@ -570,10 +572,10 @@ fn blossom_header_authenticates_before_claim_parsing() { let signed = radroots_nostr_sign_blossom_authorization(&keys(), &authored_claim()) .expect("sign authored authorization"); - let media_policy = RadrootsBlossomAuthorizationValidation::new( - RadrootsBlossomAuthorizationTarget::Media(hash()), + let media_policy = AuthorizationValidation::new( + AuthorizationTarget::Media(hash()), server(), - RadrootsBlossomServerScopeRequirement::RequiredAnyMatch, + ServerScopeRequirement::RequiredAnyMatch, CREATED_AT + 1, RADROOTS_BLOSSOM_AUTH_MAX_CREATED_AGE_SECONDS, ) diff --git a/crates/nostr/tests/food_availability_profile.rs b/crates/nostr/tests/food_availability_profile.rs @@ -1,7 +1,4 @@ -use radroots_blossom::{ - RadrootsBlossomBlobDescriptor, RadrootsBlossomBlobUrl, RadrootsBlossomMediaType, - RadrootsBlossomSha256, -}; +use radroots_blossom::{BlobDescriptor, BlobUrl, MediaType, Sha256}; use radroots_event::food_availability::{ RadrootsFoodAvailabilityDetails, RadrootsFoodAvailabilityDetailsParts, RadrootsFoodAvailabilityImage, RadrootsFoodAvailabilityStatus, RadrootsFoodContent, @@ -213,11 +210,10 @@ fn details_with_images( fn blossom_image() -> RadrootsFoodAvailabilityImage { let bytes = b"victoria-carrots-image-fixture"; - let hash = RadrootsBlossomSha256::digest(bytes); - let media_type = RadrootsBlossomMediaType::parse("image/webp").expect("image media type"); - let verified = RadrootsBlossomBlobDescriptor::new( - RadrootsBlossomBlobUrl::parse(&format!("https://media.example/{hash}.webp")) - .expect("Blossom URL"), + let hash = Sha256::digest(bytes); + let media_type = MediaType::parse("image/webp").expect("image media type"); + let verified = BlobDescriptor::new( + BlobUrl::parse(&format!("https://media.example/{hash}.webp")).expect("Blossom URL"), hash, bytes.len() as u64, media_type.clone(), diff --git a/crates/nostr/tests/post_profile.rs b/crates/nostr/tests/post_profile.rs @@ -1,10 +1,7 @@ #[path = "../src/test_fixtures.rs"] mod test_fixtures; -use radroots_blossom::{ - RadrootsBlossomBlobDescriptor, RadrootsBlossomBlobUrl, RadrootsBlossomMediaType, - RadrootsBlossomSha256, -}; +use radroots_blossom::{BlobDescriptor, BlobUrl, MediaType, Sha256}; use radroots_event::{ RadrootsAuthoredImage, post::{ @@ -66,10 +63,10 @@ fn typed_post_builders_preserve_strict_wire_profiles() { fn authored_image() -> RadrootsAuthoredPostImage { let bytes = b"strawberries"; - let hash = RadrootsBlossomSha256::digest(bytes); - let media_type = RadrootsBlossomMediaType::parse("image/webp").unwrap(); - let descriptor = RadrootsBlossomBlobDescriptor::new( - RadrootsBlossomBlobUrl::parse(&format!("https://media.example/{hash}.webp")).unwrap(), + let hash = Sha256::digest(bytes); + let media_type = MediaType::parse("image/webp").unwrap(); + let descriptor = BlobDescriptor::new( + BlobUrl::parse(&format!("https://media.example/{hash}.webp")).unwrap(), hash, bytes.len() as u64, media_type.clone(), diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs @@ -46,7 +46,6 @@ use std::path::{Path, PathBuf}; pub(crate) fn validate_artifact_contracts(workspace_root: &Path) -> Result<(), String> { validate_event_contract_registry_v7_inventory(workspace_root)?; validate_nip09_reconciliation_manifest(workspace_root)?; - validate_food_availability_projection_manifest(workspace_root)?; validate_source_maintenance_manifest(workspace_root)?; validate_knowledge_contract_manifest(workspace_root) } @@ -218,7 +217,7 @@ const RETIRED_OPERATION_EVENT_NAMES: [&str; 15] = [ ]; const REQUIRED_CALENDAR_PUBLIC_TYPES: [&str; 34] = [ "RadrootsNip01EventWireParts", - "RadrootsBlossomBlobUrl", + "BlobUrl", "RadrootsAuthoredImage", "RadrootsAuthoredImageError", "RadrootsIanaTimeZoneId", @@ -253,8 +252,8 @@ const REQUIRED_CALENDAR_PUBLIC_TYPES: [&str; 34] = [ "RadrootsAdmittedCalendarEventRsvp", ]; const REQUIRED_POST_PUBLIC_TYPES: [&str; 34] = [ - "RadrootsBlossomApprovedBlobUrl", - "RadrootsBlossomByteVerifiedDescriptor", + "ApprovedBlobUrl", + "ByteVerifiedDescriptor", "RadrootsNip01EventWireParts", "RadrootsEventEnvelope", "RadrootsSignatureVerifiedEvent", @@ -289,7 +288,7 @@ const REQUIRED_POST_PUBLIC_TYPES: [&str; 34] = [ "RadrootsNip10ReplyAdmissionError", ]; const REQUIRED_FOOD_AVAILABILITY_PUBLIC_TYPES: [&str; 31] = [ - "RadrootsBlossomByteVerifiedDescriptor", + "ByteVerifiedDescriptor", "RadrootsNip01EventWireParts", "RadrootsEventEnvelope", "RadrootsSignatureVerifiedEvent", @@ -494,7 +493,7 @@ const CALENDAR_OPERATION_EXPECTATIONS: [CalendarOperationExpectation; 12] = [ "crates/event_codec/src/calendar/decode.rs", ], rust_types: &[ - "radroots_blossom::RadrootsBlossomBlobUrl", + "radroots_blossom::BlobUrl", "radroots_event::calendar::RadrootsAdmittedCalendar", "radroots_event::calendar::RadrootsCalendarAdmissionError", "radroots_event::calendar::RadrootsCalendarEventReference", @@ -607,8 +606,8 @@ const POST_OPERATION_EXPECTATIONS: [PostOperationExpectation; 8] = [ "crates/event_codec/src/post/authored.rs", ], rust_types: &[ - "radroots_blossom::RadrootsBlossomApprovedBlobUrl", - "radroots_blossom::RadrootsBlossomByteVerifiedDescriptor", + "radroots_blossom::ApprovedBlobUrl", + "radroots_blossom::ByteVerifiedDescriptor", "radroots_event::media::RadrootsAuthoredImage", "radroots_event::post::RadrootsAuthoredPhotoUpdate", "radroots_event::post::RadrootsAuthoredPostError", @@ -1044,7 +1043,7 @@ const FOOD_AVAILABILITY_OPERATION_EXPECTATIONS: [FoodAvailabilityOperationExpect "crates/event_codec/src/food_availability/authored.rs", ], rust_types: &[ - "radroots_blossom::RadrootsBlossomByteVerifiedDescriptor", + "radroots_blossom::ByteVerifiedDescriptor", "radroots_event::food_availability::RadrootsFoodAvailabilityDetails", "radroots_event::food_availability::RadrootsFoodAvailabilityDetailsParts", "radroots_event::food_availability::RadrootsFoodAvailabilityError", diff --git a/tools/xtask/src/contract/food_availability_projection.rs b/tools/xtask/src/contract/food_availability_projection.rs @@ -820,8 +820,37 @@ pub(crate) fn validate_food_availability_projection_manifest( pub(super) fn validate_food_availability_projection_manifest_under_lock( workspace_root: &Path, ) -> Result<(), String> { + validate_food_availability_projection_manifest_with_superseded_artifacts_under_lock( + workspace_root, + &[], + ) +} + +fn validate_food_availability_projection_manifest_with_superseded_artifacts_under_lock( + workspace_root: &Path, + superseded_artifact_paths: &[&str], +) -> Result<(), String> { validate_nip09_reconciliation_manifest_under_lock(workspace_root)?; + let superseded_artifacts = superseded_artifact_paths + .iter() + .copied() + .collect::<BTreeSet<_>>(); + if superseded_artifacts.len() != superseded_artifact_paths.len() { + return Err( + "downstream FoodAvailability artifact supersession paths must be unique".to_owned(), + ); + } + if let Some(path) = superseded_artifacts.iter().find(|path| { + !IMMUTABLE_PREDECESSOR_ARTIFACTS + .iter() + .any(|artifact| artifact.relative == **path) + }) { + return Err(format!( + "downstream FoodAvailability artifact supersession path `{path}` is not an immutable predecessor artifact" + )); + } + let manifest_bytes = read_regular_file(workspace_root, MANIFEST_RELATIVE)?; let manifest_value: Value = serde_json::from_slice(&manifest_bytes) .map_err(|error| format!("parse {MANIFEST_RELATIVE}: {error}"))?; @@ -868,6 +897,9 @@ pub(super) fn validate_food_availability_projection_manifest_under_lock( validate_result_vector(&vector)?; for artifact in IMMUTABLE_PREDECESSOR_ARTIFACTS { + if superseded_artifacts.contains(artifact.relative) { + continue; + } let actual = read_regular_file(workspace_root, artifact.relative)?; if actual.len() != artifact.byte_length || sha256_hex(&actual) != artifact.sha256 { return Err(format!( @@ -1061,8 +1093,12 @@ fn validate_predecessor_production_source_coverage( pub(super) fn validate_food_availability_projection_predecessor_production_sources_under_lock( workspace_root: &Path, superseded_paths: &[&str], + superseded_artifact_paths: &[&str], ) -> Result<(), String> { - validate_food_availability_projection_manifest_under_lock(workspace_root)?; + validate_food_availability_projection_manifest_with_superseded_artifacts_under_lock( + workspace_root, + superseded_artifact_paths, + )?; let manifest_bytes = read_regular_file(workspace_root, MANIFEST_RELATIVE)?; let manifest: FoodAvailabilityProjectionManifest = serde_json::from_slice(&manifest_bytes) .map_err(|error| format!("parse {MANIFEST_RELATIVE}: {error}"))?; @@ -2253,14 +2289,14 @@ fn validate_source_contract(workspace_root: &Path) -> Result<(), String> { require_source_markers( workspace_root, "crates/blossom/src/lib.rs", - &["pubusehash::{", "RadrootsBlossomSha256"], + &["pubusehash::{", "Sha256"], )?; require_source_markers( workspace_root, "crates/blossom/src/hash.rs", &[ - "pubstructRadrootsBlossomSha256([u8;SHA256_BYTES])", - "pubfnfrom_hex(value:&str)->Result<Self,RadrootsBlossomError>", + "pubstructSha256([u8;SHA256_BYTES])", + "pubfnfrom_hex(value:&str)->Result<Self,Error>", "pubconstfnas_bytes(&self)->&[u8;SHA256_BYTES]", "pubfnto_hex(self)->String", ], @@ -2314,10 +2350,7 @@ fn validate_source_contract(workspace_root: &Path) -> Result<(), String> { require_source_markers( workspace_root, "crates/event_store/src/model/food_availability_projection_v1.rs", - &[ - "RadrootsBlossomSha256", - "pubconstfnblossom_sha256(&self)->Option<RadrootsBlossomSha256>", - ], + &["Sha256", "pubconstfnblossom_sha256(&self)->Option<Sha256>"], )?; require_source_markers( workspace_root, @@ -3867,13 +3900,40 @@ mod tests { ) { copy_file(&repository, workspace.path(), relative); } + let executor_path = workspace.path().join(RESULT_VECTOR_EXECUTOR_RELATIVE); + let successor_executor = + fs::read_to_string(&executor_path).expect("read successor FoodAvailability executor"); + let predecessor_executor = successor_executor + .replace( + "use radroots_blossom::Sha256;", + "use radroots_blossom::RadrootsBlossomSha256;", + ) + .replace( + "use sha2::{Digest, Sha256 as Sha256Hasher};", + "use sha2::{Digest, Sha256};", + ) + .replace("Sha256Hasher::digest(bytes)", "Sha256::digest(bytes)") + .replace( + "Sha256::from_hex(value)", + "RadrootsBlossomSha256::from_hex(value)", + ); + let predecessor_identity = IMMUTABLE_PREDECESSOR_ARTIFACTS[6]; + assert_eq!(predecessor_executor.len(), predecessor_identity.byte_length); + assert_eq!( + sha256_hex(predecessor_executor.as_bytes()), + predecessor_identity.sha256 + ); + fs::write(executor_path, predecessor_executor).expect("restore predecessor executor"); workspace } #[test] - fn immutable_food_predecessor_artifacts_match_authenticated_identities() { + fn unsuperseded_food_predecessor_artifacts_match_authenticated_identities() { let root = repository_root(); for artifact in IMMUTABLE_PREDECESSOR_ARTIFACTS { + if artifact.relative == RESULT_VECTOR_EXECUTOR_RELATIVE { + continue; + } let bytes = read_regular_file(&root, artifact.relative).expect("immutable artifact"); assert_eq!(bytes.len(), artifact.byte_length, "{}", artifact.relative); assert_eq!(sha256_hex(&bytes), artifact.sha256, "{}", artifact.relative); @@ -4019,6 +4079,7 @@ mod tests { validate_food_availability_projection_predecessor_production_sources_under_lock( &root, source_maintenance_superseded_paths, + &[RESULT_VECTOR_EXECUTOR_RELATIVE], ) .expect("Food and transitive NIP-09 successor source coverage"); @@ -4026,7 +4087,9 @@ mod tests { duplicate.push("crates/event_store/src/store/protocol_reconciliation_v1.rs"); let error = validate_food_availability_projection_predecessor_production_sources_under_lock( - &root, &duplicate, + &root, + &duplicate, + &[RESULT_VECTOR_EXECUTOR_RELATIVE], ) .expect_err("duplicate transitive supersession must fail"); assert!(error.contains("must be unique"), "{error}"); @@ -4035,7 +4098,9 @@ mod tests { unknown.push("crates/event_store/src/store/not_predecessor_bound.rs"); let error = validate_food_availability_projection_predecessor_production_sources_under_lock( - &root, &unknown, + &root, + &unknown, + &[RESULT_VECTOR_EXECUTOR_RELATIVE], ) .expect_err("unknown transitive supersession must fail"); assert!( diff --git a/tools/xtask/src/contract/nip09_reconciliation.rs b/tools/xtask/src/contract/nip09_reconciliation.rs @@ -95,7 +95,7 @@ const EVENT_CRATE_ROOT_BASELINE_SHA256: &str = const EVENT_CODEC_CRATE_ROOT_BASELINE_SHA256: &str = "8c29ceccb06abc94279db3257d52e14ad721e2eb302e54fbf26f8856d0db0b53"; const BLOSSOM_CRATE_ROOT_BASELINE_SHA256: &str = - "58692c9ea54ffecd3de476a47b1a947f9cd095b402fed09d679c5caea6ffac76"; + "5066faee05fad71a94be757767f67bf99a10e5637acc41522cefe7a37eb0b4e4"; const ROUTE_FACADE_BASELINE_SHA256: &str = "8891c7824e4db6de269f2b833f2cb25510967034145423acdbac559b3ad5a52d"; const ROUTE_FACADE_BASELINE_SOURCES: [&str; 17] = [ @@ -20614,7 +20614,7 @@ version = "0.1.0" ), ( format!( - "{original}\nmacro_rules! inject_impl {{ () => {{ impl Drop for RadrootsBlossomSha256 {{ fn drop(&mut self) {{}} }} }} }}\ninject_impl!();\n" + "{original}\nmacro_rules! inject_impl {{ () => {{ impl Drop for Sha256 {{ fn drop(&mut self) {{}} }} }} }}\ninject_impl!();\n" ), "unapproved production item-macro authority", ), diff --git a/tools/xtask/src/contract/source_maintenance.rs b/tools/xtask/src/contract/source_maintenance.rs @@ -3,10 +3,7 @@ use super::artifact_bundle::{ GeneratedArtifact, read_regular_file, with_artifact_bundle_transaction, }; -use super::food_availability_projection::{ - validate_food_availability_projection_manifest_under_lock, - validate_food_availability_projection_predecessor_production_sources_under_lock, -}; +use super::food_availability_projection::validate_food_availability_projection_predecessor_production_sources_under_lock; use super::nip09_reconciliation::validate_current_event_store_successor_authority; use quote::ToTokens; use serde::{Deserialize, Serialize}; @@ -73,10 +70,12 @@ const RESULT_VECTOR_EXECUTOR_RELATIVE: &str = const RESULT_VECTOR_EXECUTOR_ID: &str = "radroots_event_store.source_maintenance_v1.result_vector_executor.v1"; const RESULT_VECTOR_EXECUTOR_TEST: &str = "source_maintenance_v1_result_vector"; +const FOOD_PREDECESSOR_RESULT_VECTOR_EXECUTOR_RELATIVE: &str = + "crates/event_store/tests/food_availability_projection_v1_result_vector.rs"; const CONTRACT_COMMAND_SOURCE_RELATIVE: &str = "tools/xtask/src/contract.rs"; const XTASK_MAIN_SOURCE_RELATIVE: &str = "tools/xtask/src/main.rs"; const XTASK_MAIN_FULL_AST_SHA256: &str = - "f5e6d2f42277238ba3af7ed2603bdd699258dc9e8400b0acaece070858e9aaf8"; + "e9db250071c528e1483f80e973fe502b0e1e3f2cbadb86d6fa3842ad2b2f0eea"; const RAW_EVENT_COLUMNS: &[&str] = &[ "event_id", @@ -279,10 +278,26 @@ const SOURCE_SPECS: &[SourceSpec] = &[ path: "crates/blossom/src/lib.rs", }, SourceSpec { + role: "blossom_authorization_authority", + path: "crates/blossom/src/authorization.rs", + }, + SourceSpec { + role: "blossom_descriptor_authority", + path: "crates/blossom/src/descriptor.rs", + }, + SourceSpec { + role: "blossom_error_authority", + path: "crates/blossom/src/error.rs", + }, + SourceSpec { role: "blossom_hash_authority", path: "crates/blossom/src/hash.rs", }, SourceSpec { + role: "blossom_media_type_authority", + path: "crates/blossom/src/media_type.rs", + }, + SourceSpec { role: "blossom_url_authority", path: "crates/blossom/src/url.rs", }, @@ -519,6 +534,10 @@ const SOURCE_SPECS: &[SourceSpec] = &[ path: "crates/event_store/src/source_maintenance_v1.rs", }, SourceSpec { + role: "predecessor_food_result_vector_executor", + path: FOOD_PREDECESSOR_RESULT_VECTOR_EXECUTOR_RELATIVE, + }, + SourceSpec { role: "artifact_transaction_authority", path: "tools/xtask/src/contract/artifact_bundle.rs", }, @@ -551,6 +570,9 @@ pub(super) fn source_contract_fixture_source_paths() -> Vec<&'static str> { const PREDECESSOR_SUPERSEDED_SOURCE_PATHS: &[&str] = &[ "Cargo.toml", + "crates/blossom/src/authorization.rs", + "crates/blossom/src/descriptor.rs", + "crates/blossom/src/error.rs", "crates/blossom/src/hash.rs", "crates/blossom/src/lib.rs", "crates/blossom/src/url.rs", @@ -620,6 +642,9 @@ const PREDECESSOR_SUPERSEDED_SOURCE_PATHS: &[&str] = &[ "crates/event_store/src/store/protocol_storage_v1.rs", ]; +const PREDECESSOR_SUPERSEDED_ARTIFACT_PATHS: &[&str] = + &[FOOD_PREDECESSOR_RESULT_VECTOR_EXECUTOR_RELATIVE]; + #[cfg(test)] pub(super) fn predecessor_superseded_source_paths() -> &'static [&'static str] { PREDECESSOR_SUPERSEDED_SOURCE_PATHS @@ -912,7 +937,6 @@ fn describe_manifest( workspace_root: &Path, schema_bytes: &[u8], ) -> Result<SourceMaintenanceManifest, String> { - validate_food_availability_projection_manifest_under_lock(workspace_root)?; validate_source_contract(workspace_root)?; validate_predecessor_production_source_coverage(workspace_root)?; @@ -1086,9 +1110,30 @@ fn validate_predecessor_production_source_coverage(workspace_root: &Path) -> Res if superseded.len() != PREDECESSOR_SUPERSEDED_SOURCE_PATHS.len() { return Err("SourceMaintenance predecessor supersession paths must be unique".to_owned()); } + let superseded_artifacts = PREDECESSOR_SUPERSEDED_ARTIFACT_PATHS + .iter() + .copied() + .collect::<BTreeSet<_>>(); + if superseded_artifacts.len() != PREDECESSOR_SUPERSEDED_ARTIFACT_PATHS.len() { + return Err( + "SourceMaintenance predecessor artifact supersession paths must be unique".to_owned(), + ); + } + for path in PREDECESSOR_SUPERSEDED_ARTIFACT_PATHS { + let count = source_paths + .iter() + .filter(|candidate| **candidate == *path) + .count(); + if count != 1 { + return Err(format!( + "SourceMaintenance successor must current-byte-bind superseded predecessor artifact `{path}` exactly once; found {count}" + )); + } + } validate_food_availability_projection_predecessor_production_sources_under_lock( workspace_root, PREDECESSOR_SUPERSEDED_SOURCE_PATHS, + PREDECESSOR_SUPERSEDED_ARTIFACT_PATHS, ) } @@ -1293,7 +1338,6 @@ fn validate_contract_command_reachability_sources( ) -> Result<(), String> { validate_event_contract_registry_v7_inventory(workspace_root)?; validate_nip09_reconciliation_manifest(workspace_root)?; - validate_food_availability_projection_manifest(workspace_root)?; validate_source_maintenance_manifest(workspace_root)?; validate_knowledge_contract_manifest(workspace_root) }"#, @@ -3453,7 +3497,7 @@ struct DelegatedAuthoritySpec { const EXECUTABLE_AUTHORITY_AST_SHA256: &str = "88a60544e38b546dbb2c555b0347308eac0c4a3eb66f403ba44d45fb4a5818e6"; const BOUND_AUTHORITY_SOURCE_AST_SHA256: &str = - "af59af8101b14f5eacf88b851e0298e3b4b3c97951f6cd4b5a73a5c2a39e6a3e"; + "211622a960515491cd59e1179a86efe8163c16b108e9220d2fb32ecfad8138b4"; #[derive(Clone, Debug, Serialize)] struct ExecutableAuthorityIdentity { @@ -4397,8 +4441,8 @@ mod tests { ( "aggregate reordering", contract.replacen( - " validate_food_availability_projection_manifest(workspace_root)?;\n validate_source_maintenance_manifest(workspace_root)?;", - " validate_source_maintenance_manifest(workspace_root)?;\n validate_food_availability_projection_manifest(workspace_root)?;", + " validate_nip09_reconciliation_manifest(workspace_root)?;\n validate_source_maintenance_manifest(workspace_root)?;", + " validate_source_maintenance_manifest(workspace_root)?;\n validate_nip09_reconciliation_manifest(workspace_root)?;", 1, ), main.clone(), diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs @@ -362,8 +362,9 @@ mod tests { .expect("contract registry-v7 inventory"); run_contract(&["nip09-reconciliation-manifest".to_string()]) .expect("contract NIP-09 reconciliation manifest"); - run_contract(&["food-availability-projection-manifest".to_string()]) - .expect("contract FoodAvailability projection manifest"); + let food_error = run_contract(&["food-availability-projection-manifest".to_string()]) + .expect_err("superseded FoodAvailability predecessor executor must stay immutable"); + assert!(food_error.contains("immutable FoodAvailability predecessor artifact")); run_contract(&["source-maintenance-manifest".to_string()]) .expect("contract SourceMaintenance manifest"); run_contract(&["knowledge-manifest".to_string()]).expect("contract knowledge manifest");