dig_urn_protocol/grammar.rs
1//! The **single normative grammar** for the DIG URN — the contract every implementation's parser
2//! must conform to.
3//!
4//! Several parsers historically parsed `urn:dig:…` independently (this crate's [`DigUrn`], the
5//! `dig-sdk` regex, the extension JS parser, the browser C++ parser) with no shared conformance
6//! suite, so they drifted. This module fixes ONE grammar as the source of truth and pins it with a
7//! frozen vector file (`tests/fixtures/urn_conformance.json`, imported byte-identically from
8//! `digstore_core`) that [`crate::DigUrn`] is tested against; every other port is expected to run
9//! the same frozen vectors so all implementations conform to one definition.
10//!
11//! [`DigUrn`]: crate::DigUrn
12//!
13//! # Normative grammar (ABNF, RFC 5234)
14//!
15//! ```abnf
16//! dig-urn = "urn:dig:" chain ":" store-id [ ":" root-hash ] [ "/" resource ]
17//!
18//! chain = 1*chain-char ; non-empty; canonical value is "chia"
19//! chain-char = ALPHA / DIGIT / "-" ; (see "Chain segment" note — the parser is
20//! ; permissive; deployed content uses "chia")
21//!
22//! store-id = 64HEXDIG ; the CHIP-0035 singleton launcher id, 32 bytes
23//! root-hash = 64HEXDIG ; a capsule's on-chain root, 32 bytes (optional)
24//! resource = *pchar ; the resource path/key, verbatim after the
25//! ; FIRST "/", may itself contain "/" (optional)
26//!
27//! HEXDIG = DIGIT / "a" / "b" / "c" / "d" / "e" / "f" ; lowercase, canonical
28//! ```
29//!
30//! Notes that make the grammar *normative* (the parser's actual behaviour):
31//!
32//! * **Prefix** is the literal `urn:dig:`. Anything else is rejected.
33//! * **Resource split is at the FIRST `/`.** Everything before it is `chain:store-id[:root-hash]`;
34//! everything after is the resource (which may contain further `/`). A trailing-but-empty resource
35//! (`…/`) parses as `resource = ""` (an empty string), distinct from an absent resource.
36//! * **Colon arity in the head is exactly 2 or 3 segments** (`chain:store-id` or
37//! `chain:store-id:root-hash`). A 4th `:`-segment is rejected.
38//! * **`store-id` and `root-hash` are 32-byte lowercase hex.** A non-hex or wrong-length value is
39//! rejected.
40//! * **Canonical form** re-emits `urn:dig:<chain>:<store-id-hex>[:<root-hash-hex>][/<resource>]`,
41//! store-id and root-hash as lowercase hex, omitting absent fields. Parsing then re-canonicalising
42//! is idempotent for any canonical input.
43//! * **Retrieval key** is `SHA-256(canonical())` as raw 32 bytes (lowercase hex on the wire) — the
44//! URN-identity key the frozen corpus pins. The root-independent CONTENT key a resolver uses to
45//! fetch is `SHA-256(canonical_rootless())` (see `DigUrn::content_key`).
46//!
47//! # The `?salt` query — intentionally NOT part of the URN identity
48//!
49//! The secret salt is a private-store *decryption-key* input, never part of the canonical URN or the
50//! retrieval key: a private store derives its AES key from `canonical_urn + salt`, but the retrieval
51//! key (what the host sees) stays `SHA-256(canonical_urn)` — by design the host cannot tell a private
52//! store from a public one. So this grammar has **no `?salt` production**: the core parser leaves a
53//! `?salt=…` suffix inside the resource. Any edge parser that peels `?salt` MUST do so OUTSIDE the
54//! canonical-URN derivation (as [`crate::DigUrn::parse_with_salt`] does), and a surfaced salt MUST be
55//! exactly 32 bytes / 64 lowercase hex.
56//!
57//! # Back-compatibility (frozen corpus)
58//!
59//! The grammar MUST accept every historically-published URN: multi-chain labels (`chia`, `mainnet`,
60//! `testnet`), and the bare resourceless form `urn:dig:chia:<store>`. Tightening the parser (e.g.
61//! rejecting non-`chia` chains, or making `/resource` mandatory) is forbidden — it would break the
62//! frozen KAT corpus and existing on-chain-anchored artifacts.
63
64/// The normative URN grammar as ABNF (RFC 5234) text — the machine-/human-readable single source of
65/// truth, embedded so an agent can introspect it without leaving the crate. Kept byte-identical to
66/// the module-doc grammar block above and to `digstore_core::urn_grammar::URN_ABNF`.
67pub const URN_ABNF: &str = "\
68dig-urn = \"urn:dig:\" chain \":\" store-id [ \":\" root-hash ] [ \"/\" resource ]\n\
69chain = 1*chain-char ; non-empty; canonical value is \"chia\"\n\
70chain-char = ALPHA / DIGIT / \"-\"\n\
71store-id = 64HEXDIG ; CHIP-0035 singleton launcher id, 32 bytes\n\
72root-hash = 64HEXDIG ; a capsule's on-chain root, 32 bytes (optional)\n\
73resource = *pchar ; verbatim after the FIRST \"/\" (optional)\n\
74HEXDIG = DIGIT / \"a\" / \"b\" / \"c\" / \"d\" / \"e\" / \"f\"\n";
75
76/// The literal URN prefix. Anything not starting with this is rejected.
77pub const URN_PREFIX: &str = "urn:dig:";
78
79/// The canonical chain tag a conforming URN SHOULD carry. Other labels (`mainnet`, `testnet`) remain
80/// ACCEPTED for back-compat with the frozen corpus.
81pub const CANONICAL_CHAIN: &str = "chia";
82
83/// The default resource an empty/absent resource key resolves to (the store's landing view).
84pub const DEFAULT_RESOURCE_KEY: &str = "index.html";
85
86/// The `?salt=` query marker an edge parser peels off before delegating to the core parser.
87pub const SALT_QUERY_MARKER: &str = "?salt=";