334 lines
14 KiB
Rust
334 lines
14 KiB
Rust
//! Domain-separation tags.
|
|
//!
|
|
//! Epiphany hashes domain-separated preimages so that two semantically
|
|
//! different chunks with identical raw bytes never share a content address
|
|
//! (Chapter 8 §"Domain-Separated Preimages"). Every tag is a fixed 8-byte
|
|
//! ASCII string beginning with `MUSC`. Centralizing them here keeps the set
|
|
//! drift-free: there is exactly one definition of each tag in the workspace.
|
|
|
|
/// A fixed 8-byte domain-separation tag. Always the first bytes of a hash
|
|
/// preimage (see [`crate::Preimage`]).
|
|
///
|
|
/// The spec's domain-tag vocabulary is closed: the reserved built-ins
|
|
/// ([`DomainTag::BUILTINS`]) plus extension-introduced *system-derived* tags,
|
|
/// which "MUST begin with `MUSCS` and have length exactly 8 bytes" (Chapter 5).
|
|
/// Every tag is an 8-byte ASCII string. The field is private and the
|
|
/// constructors enforce that vocabulary, so a nonconforming tag — wrong prefix
|
|
/// (`b"BAD_TAG!"`), non-ASCII bytes, or an unregistered `MUSC....` domain —
|
|
/// cannot be minted and therefore cannot reach [`crate::derive_system_counter`]
|
|
/// or a hash preimage. To mint an extension's own tag use [`SystemDomainTag`].
|
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
|
pub struct DomainTag([u8; 8]);
|
|
|
|
impl DomainTag {
|
|
/// Length of every domain tag, in bytes.
|
|
pub const LEN: usize = 8;
|
|
|
|
/// The prefix marking a *system-derived* tag (Chapter 5:
|
|
/// "Additional domain tags introduced by registered extensions MUST begin
|
|
/// with `MUSCS` and have length exactly 8 bytes"). The three built-in
|
|
/// system tags ([`Self::SYSTEM_VOICE`], [`Self::SYSTEM_PITCH`],
|
|
/// [`Self::SYSTEM_ANOMALY`]) also carry it.
|
|
const SYSTEM_PREFIX: &'static [u8] = b"MUSCS";
|
|
|
|
/// The raw 8 ASCII bytes.
|
|
#[inline]
|
|
pub const fn as_bytes(&self) -> &[u8; 8] {
|
|
&self.0
|
|
}
|
|
|
|
// --- Built-in tags (Chapter 8 §"Domain-Separated Preimages", Ch. 5/6). ---
|
|
|
|
/// `.musc` chunk payloads.
|
|
pub const CHUNK: DomainTag = DomainTag(*b"MUSCCHNK");
|
|
/// Manifest chunk payloads.
|
|
pub const MANIFEST: DomainTag = DomainTag(*b"MUSCMANI");
|
|
/// Blob payloads; a `BlobId` is the [`crate::ContentHash`] under this tag.
|
|
pub const BLOB: DomainTag = DomainTag(*b"MUSCBLOB");
|
|
/// `ConflictId` derivation (Chapter 6 §"Conflict Identity").
|
|
pub const CONFLICT: DomainTag = DomainTag(*b"MUSCCONF");
|
|
/// Canonical operation-envelope hash, `EnvelopeHash` (Chapter 6 §6.5).
|
|
pub const ENVELOPE: DomainTag = DomainTag(*b"MUSCENVH");
|
|
/// Glyph-catalog metrics hash (Chapter 7 §"Glyph Catalog Identity").
|
|
pub const FONT_METRICS: DomainTag = DomainTag(*b"MUSCFNTM");
|
|
/// `ManifestId` derivation (Chapter 8 / deferred-types table).
|
|
pub const MANIFEST_ID: DomainTag = DomainTag(*b"MUSCMNIF");
|
|
/// System-promoted voice counter derivation (Chapter 5 §"System-Derived").
|
|
pub const SYSTEM_VOICE: DomainTag = DomainTag(*b"MUSCSVCE");
|
|
/// System-derived pitch counter derivation (Chapter 5 §"System-Derived").
|
|
pub const SYSTEM_PITCH: DomainTag = DomainTag(*b"MUSCSPCH");
|
|
/// `IntegrityAnomalyId` derivation (Chapter 5 §"System-Derived Counter
|
|
/// Collisions"). Reserved built-in: anomalies are core, not an extension
|
|
/// concern (Pass 11, item 1.4).
|
|
pub const SYSTEM_ANOMALY: DomainTag = DomainTag(*b"MUSCSANM");
|
|
/// `LayoutObjectId` derivation (Chapter 7 §"Provenance",
|
|
/// Requirement `req:layoutir:object-id-derivation`). Like
|
|
/// [`Self::FONT_METRICS`], this is a reserved built-in but **non-canonical**:
|
|
/// layout-object ids are not document state and enter no content hash, so the
|
|
/// tag lives in the layout namespace, not among the canonical system tags
|
|
/// (Chapter 8 §"Domain-tag registry").
|
|
pub const LAYOUT_OBJECT_ID: DomainTag = DomainTag(*b"MUSCLOID");
|
|
|
|
/// Every built-in tag, in declaration order. The closed core vocabulary
|
|
/// (the nine canonical tags plus the non-canonical layout tags
|
|
/// [`Self::FONT_METRICS`] and [`Self::LAYOUT_OBJECT_ID`]).
|
|
pub const BUILTINS: [DomainTag; 11] = [
|
|
Self::CHUNK,
|
|
Self::MANIFEST,
|
|
Self::BLOB,
|
|
Self::CONFLICT,
|
|
Self::ENVELOPE,
|
|
Self::FONT_METRICS,
|
|
Self::MANIFEST_ID,
|
|
Self::SYSTEM_VOICE,
|
|
Self::SYSTEM_PITCH,
|
|
Self::SYSTEM_ANOMALY,
|
|
Self::LAYOUT_OBJECT_ID,
|
|
];
|
|
|
|
/// Constructs a domain tag from raw bytes, accepting only the spec's closed
|
|
/// vocabulary: a reserved built-in, or a well-formed extension system tag
|
|
/// (begins `MUSCS`). Every byte must be printable ASCII. Returns `None`
|
|
/// otherwise — wrong prefix, non-ASCII bytes, or an unregistered
|
|
/// `MUSC....` domain that is neither built-in nor a `MUSCS` system tag.
|
|
/// This is the checked entry point for decoding a tag from storage/interop.
|
|
#[inline]
|
|
pub fn from_bytes(raw: [u8; 8]) -> Option<Self> {
|
|
if Self::is_valid_bytes(&raw) {
|
|
Some(DomainTag(raw))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Validity predicate for the closed vocabulary: printable-ASCII, not a
|
|
/// file-format magic byte string, and either a registered built-in or a
|
|
/// `MUSCS`-prefixed system tag.
|
|
#[inline]
|
|
fn is_valid_bytes(raw: &[u8; 8]) -> bool {
|
|
if !raw.iter().all(u8::is_ascii_graphic) {
|
|
return false;
|
|
}
|
|
if Self::is_file_magic(raw) {
|
|
return false;
|
|
}
|
|
DomainTag::BUILTINS.iter().any(|b| b.as_bytes() == raw)
|
|
|| raw.starts_with(Self::SYSTEM_PREFIX)
|
|
}
|
|
|
|
/// File-format magic byte strings are in the same 8-byte `MUSC*`
|
|
/// namespace, but they are not hash-domain tags and must not be reused by
|
|
/// extension system identifiers.
|
|
#[inline]
|
|
fn is_file_magic(raw: &[u8; 8]) -> bool {
|
|
raw == &BUNDLE_MAGIC || raw == &SUPERBLOCK_MAGIC
|
|
}
|
|
|
|
/// Whether this is one of the reserved built-in tags ([`Self::BUILTINS`]).
|
|
#[inline]
|
|
pub fn is_builtin(&self) -> bool {
|
|
Self::BUILTINS.contains(self)
|
|
}
|
|
|
|
/// Whether this is a *system-derived* tag (begins `MUSCS`): a built-in
|
|
/// [`Self::SYSTEM_VOICE`] / [`Self::SYSTEM_PITCH`] / [`Self::SYSTEM_ANOMALY`]
|
|
/// or an extension tag minted via [`SystemDomainTag::new_extension`].
|
|
#[inline]
|
|
pub fn is_system_derived(&self) -> bool {
|
|
self.0.starts_with(Self::SYSTEM_PREFIX)
|
|
}
|
|
|
|
/// Whether this is an *extension-introduced* system tag: system-derived and
|
|
/// not a reserved built-in. The three built-in system tags return `false`
|
|
/// here — they are reserved, not extension-introduced.
|
|
#[inline]
|
|
pub fn is_extension_system_tag(&self) -> bool {
|
|
self.is_system_derived() && !self.is_builtin()
|
|
}
|
|
}
|
|
|
|
/// A [`DomainTag`] proven to be *system-derived* (begins `MUSCS`): a built-in
|
|
/// [`DomainTag::SYSTEM_VOICE`] / [`DomainTag::SYSTEM_PITCH`] /
|
|
/// [`DomainTag::SYSTEM_ANOMALY`], or an
|
|
/// extension-introduced tag. Only these are admissible seeds for
|
|
/// [`crate::derive_system_counter`] (Chapter 5 §"System-Derived Identifiers").
|
|
///
|
|
/// Carrying the precondition in the type — rather than checking it at the call
|
|
/// site — makes `derive_system_counter` total: it is impossible to seed a
|
|
/// system identifier from, say, [`DomainTag::CHUNK`], because that value cannot
|
|
/// be turned into a `SystemDomainTag`.
|
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
|
pub struct SystemDomainTag(DomainTag);
|
|
|
|
impl SystemDomainTag {
|
|
/// Built-in: system-promoted voice counters (`MUSCSVCE`).
|
|
pub const VOICE: SystemDomainTag = SystemDomainTag(DomainTag::SYSTEM_VOICE);
|
|
/// Built-in: system-derived pitch counters (`MUSCSPCH`).
|
|
pub const PITCH: SystemDomainTag = SystemDomainTag(DomainTag::SYSTEM_PITCH);
|
|
/// Built-in: integrity-anomaly identifiers (`MUSCSANM`).
|
|
pub const ANOMALY: SystemDomainTag = SystemDomainTag(DomainTag::SYSTEM_ANOMALY);
|
|
|
|
/// Wraps a domain tag if it is system-derived; returns `None` otherwise.
|
|
#[inline]
|
|
pub fn new(tag: DomainTag) -> Option<Self> {
|
|
if tag.is_system_derived() {
|
|
Some(SystemDomainTag(tag))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Mints an *extension-introduced* system-derived tag from raw bytes,
|
|
/// enforcing the Chapter 5 rule: printable ASCII, begins `MUSCS`, and does
|
|
/// not collide with a reserved built-in. The only sanctioned way for an
|
|
/// extension to introduce its own system-derived domain tag.
|
|
#[inline]
|
|
pub fn new_extension(raw: [u8; 8]) -> Option<Self> {
|
|
let tag = DomainTag::from_bytes(raw)?;
|
|
if tag.is_extension_system_tag() {
|
|
Some(SystemDomainTag(tag))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// The underlying domain tag.
|
|
#[inline]
|
|
pub const fn tag(self) -> DomainTag {
|
|
self.0
|
|
}
|
|
|
|
/// The raw 8 ASCII bytes.
|
|
#[inline]
|
|
pub const fn as_bytes(&self) -> &[u8; 8] {
|
|
self.0.as_bytes()
|
|
}
|
|
}
|
|
|
|
// --- File-format magic byte strings (Chapter 8 §"The Bundle Layout"). ---
|
|
//
|
|
// These are not hashing domain tags; they are the literal magic bytes that
|
|
// open the fixed header and the superblock slots. They are centralized here
|
|
// alongside the domain tags so the full set of 8-byte `MUSC*` constants has a
|
|
// single home. `epiphany-bundle` (Agent D) consumes them.
|
|
|
|
/// Bundle fixed-header magic: ASCII `"MUSCBND\0"` (8 bytes, trailing NUL).
|
|
pub const BUNDLE_MAGIC: [u8; 8] = *b"MUSCBND\0";
|
|
|
|
/// Superblock-slot magic: ASCII `"MUSCSUPR"` (8 bytes).
|
|
pub const SUPERBLOCK_MAGIC: [u8; 8] = *b"MUSCSUPR";
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn every_tag_is_eight_ascii_bytes_starting_with_musc() {
|
|
for t in DomainTag::BUILTINS {
|
|
assert_eq!(t.as_bytes().len(), DomainTag::LEN);
|
|
assert!(t.as_bytes().starts_with(b"MUSC"), "{t:?}");
|
|
assert!(t.as_bytes().iter().all(|b| b.is_ascii()), "{t:?}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn tags_are_pairwise_distinct() {
|
|
let tags = DomainTag::BUILTINS;
|
|
for (i, a) in tags.iter().enumerate() {
|
|
for b in &tags[i + 1..] {
|
|
assert_ne!(a, b, "duplicate domain tag {a:?}");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn exact_tag_spellings_match_spec() {
|
|
// Locked literally against Chapter 8 / Chapter 5 / Chapter 6.
|
|
assert_eq!(DomainTag::CHUNK.as_bytes(), b"MUSCCHNK");
|
|
assert_eq!(DomainTag::MANIFEST.as_bytes(), b"MUSCMANI");
|
|
assert_eq!(DomainTag::BLOB.as_bytes(), b"MUSCBLOB");
|
|
assert_eq!(DomainTag::CONFLICT.as_bytes(), b"MUSCCONF");
|
|
assert_eq!(DomainTag::ENVELOPE.as_bytes(), b"MUSCENVH");
|
|
assert_eq!(DomainTag::FONT_METRICS.as_bytes(), b"MUSCFNTM");
|
|
assert_eq!(DomainTag::MANIFEST_ID.as_bytes(), b"MUSCMNIF");
|
|
assert_eq!(DomainTag::SYSTEM_VOICE.as_bytes(), b"MUSCSVCE");
|
|
assert_eq!(DomainTag::SYSTEM_PITCH.as_bytes(), b"MUSCSPCH");
|
|
assert_eq!(DomainTag::SYSTEM_ANOMALY.as_bytes(), b"MUSCSANM");
|
|
assert_eq!(DomainTag::LAYOUT_OBJECT_ID.as_bytes(), b"MUSCLOID");
|
|
assert_eq!(&BUNDLE_MAGIC, b"MUSCBND\0");
|
|
assert_eq!(&SUPERBLOCK_MAGIC, b"MUSCSUPR");
|
|
}
|
|
|
|
#[test]
|
|
fn builtin_system_tags_are_not_extension_tags() {
|
|
// They are system-derived (begin MUSCS)...
|
|
assert!(DomainTag::SYSTEM_VOICE.is_system_derived());
|
|
assert!(DomainTag::SYSTEM_PITCH.is_system_derived());
|
|
assert!(DomainTag::SYSTEM_ANOMALY.is_system_derived());
|
|
// ...but reserved built-ins, NOT extension-introduced.
|
|
assert!(DomainTag::SYSTEM_VOICE.is_builtin());
|
|
assert!(DomainTag::SYSTEM_ANOMALY.is_builtin());
|
|
assert!(!DomainTag::SYSTEM_VOICE.is_extension_system_tag());
|
|
assert!(!DomainTag::SYSTEM_PITCH.is_extension_system_tag());
|
|
assert!(!DomainTag::SYSTEM_ANOMALY.is_extension_system_tag());
|
|
// A non-system tag is neither.
|
|
assert!(!DomainTag::CHUNK.is_system_derived());
|
|
assert!(!DomainTag::CHUNK.is_extension_system_tag());
|
|
}
|
|
|
|
#[test]
|
|
fn from_bytes_accepts_only_the_closed_vocabulary() {
|
|
// Wrong prefix.
|
|
assert!(DomainTag::from_bytes(*b"BAD_TAG!").is_none());
|
|
assert!(DomainTag::from_bytes(*b"SHA2CHNK").is_none());
|
|
// Right format prefix but unregistered, non-system domain.
|
|
assert!(DomainTag::from_bytes(*b"MUSCWXYZ").is_none());
|
|
// Non-ASCII payload byte (0xFF) is rejected even with a MUSC prefix.
|
|
assert!(DomainTag::from_bytes([b'M', b'U', b'S', b'C', 0xFF, b'A', b'B', b'C']).is_none());
|
|
// Control byte (NUL) is not printable ASCII.
|
|
assert!(DomainTag::from_bytes(*b"MUSCS\0\0\0").is_none());
|
|
// Built-in: accepted.
|
|
assert_eq!(
|
|
DomainTag::from_bytes(*b"MUSCCHNK").unwrap(),
|
|
DomainTag::CHUNK
|
|
);
|
|
// The layout-object-id tag is a registered (non-system) built-in.
|
|
assert_eq!(
|
|
DomainTag::from_bytes(*b"MUSCLOID").unwrap(),
|
|
DomainTag::LAYOUT_OBJECT_ID
|
|
);
|
|
assert!(DomainTag::LAYOUT_OBJECT_ID.is_builtin());
|
|
assert!(!DomainTag::LAYOUT_OBJECT_ID.is_system_derived());
|
|
// Extension system tag: accepted.
|
|
assert!(DomainTag::from_bytes(*b"MUSCSEXT")
|
|
.unwrap()
|
|
.is_extension_system_tag());
|
|
}
|
|
|
|
#[test]
|
|
fn system_domain_tag_enforces_the_chapter5_rule() {
|
|
// Must begin MUSCS.
|
|
assert!(SystemDomainTag::new_extension(*b"MUSCXXXX").is_none());
|
|
// Must not collide with a reserved built-in.
|
|
assert!(SystemDomainTag::new_extension(*b"MUSCSVCE").is_none());
|
|
// MUSCSANM is now a reserved built-in too (Pass 11): not extension-mintable.
|
|
assert!(SystemDomainTag::new_extension(*b"MUSCSANM").is_none());
|
|
// Non-ASCII rejected.
|
|
assert!(
|
|
SystemDomainTag::new_extension([b'M', b'U', b'S', b'C', b'S', 0xFF, b'A', b'B'])
|
|
.is_none()
|
|
);
|
|
// File-format magic strings are reserved in the shared MUSC* namespace.
|
|
assert!(SystemDomainTag::new_extension(SUPERBLOCK_MAGIC).is_none());
|
|
// A genuine extension tag is accepted and classified correctly.
|
|
let ext = SystemDomainTag::new_extension(*b"MUSCSEXT").unwrap();
|
|
assert!(ext.tag().is_extension_system_tag());
|
|
// Built-in system tags wrap; non-system tags do not.
|
|
assert_eq!(SystemDomainTag::VOICE.tag(), DomainTag::SYSTEM_VOICE);
|
|
assert_eq!(SystemDomainTag::ANOMALY.tag(), DomainTag::SYSTEM_ANOMALY);
|
|
assert!(SystemDomainTag::new(DomainTag::SYSTEM_PITCH).is_some());
|
|
assert!(SystemDomainTag::new(DomainTag::SYSTEM_ANOMALY).is_some());
|
|
assert!(SystemDomainTag::new(DomainTag::CHUNK).is_none());
|
|
}
|
|
}
|