epiphany/crates/epiphany-bundle/src/ids.rs

377 lines
14 KiB
Rust

//! Bundle-level identifiers and small value types.
//!
//! `epiphany-bundle` depends on `epiphany-determinism` (Agent A) and on nothing
//! else — in particular **not** on `epiphany-core` (Agent B) or `epiphany-ops`
//! (Agent C). The QUICKSTART fixes that boundary deliberately: *"bundles handle
//! bytes, ops handles semantics. A canonical-base snapshot from the bundle's
//! perspective is opaque bytes plus a frontier DVV; only `epiphany-ops`
//! interprets it."*
//!
//! So the identifiers the manifest carries that *belong* to the semantic layer
//! ([`DocumentId`], [`LineageId`], [`SnapshotId`], [`ExtensionId`], the causal
//! [`FrontierBytes`]) are modeled here as opaque fixed-width or length-prefixed
//! values. The bundle stores, orders, and integrity-checks them; it does not
//! interpret them. The identifiers the bundle *owns* ([`FileUuid`],
//! [`ManifestId`]) are defined and derived here in full.
use crate::codec::{DecodeError, Reader, Writer};
use epiphany_determinism::{ContentHash, DomainTag, Preimage};
/// Physical-bundle identity (Chapter 8 §"File, Document, and Lineage
/// Identity"): a 128-bit UUID set at file creation, persisting for the lifetime
/// of the physical file and changing on Save As. Distinct from [`DocumentId`],
/// which identifies the logical work. Opaque to the bundle: 16 raw bytes.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct FileUuid(pub [u8; 16]);
impl FileUuid {
/// The all-zero UUID. A valid sentinel only for an uninitialized prelude;
/// a created bundle carries a caller-supplied value. (`as_bytes`/`encode`/
/// `decode` are provided by the `opaque_id16!` macro below.)
pub const ZERO: FileUuid = FileUuid([0u8; 16]);
}
/// Logical-work identity (Chapter 8): stable across Save As copies of the same
/// work; a derivative-work fork mints a new one. Opaque to the bundle.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct DocumentId(pub [u8; 16]);
/// Shared-ancestor identity (Chapter 8): records that two documents share a
/// common ancestor, for version-control genealogy. Opaque to the bundle.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct LineageId(pub [u8; 16]);
/// A materialized-snapshot identity (Chapter 8 §"Snapshots and Canonical
/// Bases"). Opaque to the bundle; only the semantic layer materializes state.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct SnapshotId(pub [u8; 16]);
/// An extension's identity (Chapter 8 §"Extension Declarations"). Opaque to the
/// bundle, which preserves unknown extensions' chunks without interpreting them.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct ExtensionId(pub [u8; 16]);
/// Registry id for a [`crate::ProfileId::Custom`] profile (Chapter 8
/// §"Format Profiles"). Opaque 128-bit value.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct ProfileRegistryId(pub [u8; 16]);
macro_rules! opaque_id16 {
($name:ident) => {
impl $name {
/// The raw 16 bytes.
#[inline]
pub const fn as_bytes(&self) -> &[u8; 16] {
&self.0
}
#[inline]
pub(crate) fn encode(&self, w: &mut Writer) {
w.put_bytes(&self.0);
}
#[inline]
pub(crate) fn decode(r: &mut Reader) -> Result<Self, DecodeError> {
Ok($name(r.take_array::<16>()?))
}
}
};
}
opaque_id16!(FileUuid);
opaque_id16!(DocumentId);
opaque_id16!(LineageId);
opaque_id16!(SnapshotId);
opaque_id16!(ExtensionId);
opaque_id16!(ProfileRegistryId);
/// The identity of a manifest (Chapter 8 §"The Manifest"): *"Each commit
/// produces a new `ManifestId`."* The bundle owns this derivation. It is a
/// content-derived 128-bit value, `trunc128(BLAKE3("MUSCMNIF" || preimage))`,
/// using the determinism crate's reserved [`DomainTag::MANIFEST_ID`] tag and
/// the same big-endian truncation as every other content-derived id
/// (`ConflictId`, etc. — Chapter 6/8).
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct ManifestId(pub u128);
impl ManifestId {
/// Derives the manifest id from the manifest's identity-bearing preimage:
/// the document id, generation, and the canonical bytes of the manifest
/// body (with the `manifest_id` field itself excluded to avoid a circular
/// dependency). Deterministic — two writers encoding the same manifest
/// content derive the same id. RATIFIED by Pass 11 (item 1.6, P11-D5):
/// core_spec §"Manifest Encoding", Requirement `req:format:manifest-id`
/// (`trunc128(BLAKE3("MUSCMNIF" || document_id || generation || body))`,
/// body excluding `manifest_id`).
///
/// Note: `document_id` and `generation` are committed twice — explicitly
/// here and again inside `body_preimage` (the canonical manifest body opens
/// with them). This duplication is intentional and golden-locked, not an
/// oversight: the preimage shape above is the ratified format.
pub(crate) fn derive(document_id: DocumentId, generation: u64, body_preimage: &[u8]) -> Self {
let mut p = Preimage::new(DomainTag::MANIFEST_ID);
p.push_bytes(document_id.as_bytes());
p.push_u64_le(generation);
p.push_bytes(body_preimage);
ManifestId(p.finish_trunc128())
}
#[inline]
pub(crate) fn encode(&self, w: &mut Writer) {
w.put_u128(self.0);
}
#[inline]
pub(crate) fn decode(r: &mut Reader) -> Result<Self, DecodeError> {
Ok(ManifestId(r.get_u128()?))
}
}
impl core::fmt::Debug for ManifestId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "ManifestId({:032x})", self.0)
}
}
/// A blob identity (Chapter 8 §"Blobs"): the [`ContentHash`] of the blob's
/// uncompressed payload under the `MUSCBLOB` domain tag. A newtype over
/// `ContentHash`, mirroring the determinism crate's `ChunkId`.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct BlobId(pub ContentHash);
impl BlobId {
/// The blob id of `payload`: `BLAKE3("MUSCBLOB" || payload)`
/// ([`ContentHash::of_blob`]). Blobs are the one content hash that is a
/// bare `domain || payload` (Chapter 8 §"Blobs"; determinism crate
/// `ContentHash::of_blob`), not the structured chunk preimage.
#[inline]
pub fn of_payload(payload: &[u8]) -> Self {
BlobId(ContentHash::of_blob(payload))
}
#[inline]
pub(crate) fn encode(&self, w: &mut Writer) {
w.put_bytes(self.0.as_bytes());
}
#[inline]
pub(crate) fn decode(r: &mut Reader) -> Result<Self, DecodeError> {
Ok(BlobId(ContentHash(r.take_array::<32>()?)))
}
}
/// The schema version a chunk payload is encoded against (Chapter 8
/// §"Schema Versioning"). Major changes are non-backward-compatible; minor
/// changes only add optional fields/variants.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct SchemaVersion {
pub major: u16,
pub minor: u16,
}
impl SchemaVersion {
/// The current prototype schema version.
pub const V0: SchemaVersion = SchemaVersion { major: 0, minor: 1 };
/// Constructs a schema version.
#[inline]
pub const fn new(major: u16, minor: u16) -> Self {
SchemaVersion { major, minor }
}
/// Canonical 4 bytes for the hash preimage (Chapter 8
/// §"Domain-Separated Preimages"): major then minor, little-endian.
#[inline]
pub fn canonical_bytes(self) -> [u8; 4] {
let mut out = [0u8; 4];
out[0..2].copy_from_slice(&self.major.to_le_bytes());
out[2..4].copy_from_slice(&self.minor.to_le_bytes());
out
}
#[inline]
pub(crate) fn encode(&self, w: &mut Writer) {
w.put_u16(self.major).put_u16(self.minor);
}
#[inline]
pub(crate) fn decode(r: &mut Reader) -> Result<Self, DecodeError> {
Ok(SchemaVersion {
major: r.get_u16()?,
minor: r.get_u16()?,
})
}
}
/// The reduction-algorithm version that produced a canonical-base snapshot
/// (Chapter 8): a snapshot may serve as a canonical base only if this matches
/// the active superblock's value. Modeled as an opaque monotonically-versioned
/// `u32` (the algorithm catalog itself lives in `epiphany-ops`).
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct ReductionAlgorithmVersion(pub u32);
impl ReductionAlgorithmVersion {
#[inline]
pub(crate) fn encode(&self, w: &mut Writer) {
w.put_u32(self.0);
}
#[inline]
pub(crate) fn decode(r: &mut Reader) -> Result<Self, DecodeError> {
Ok(ReductionAlgorithmVersion(r.get_u32()?))
}
}
/// A semantic version (Chapter 8 §"Format Profiles" / §"Extension
/// Declarations"). Ordered major, then minor, then patch — the
/// "semantic version lexicographic" order Appendix D names for extension
/// declarations.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct SemVer {
pub major: u32,
pub minor: u32,
pub patch: u32,
}
impl SemVer {
/// Constructs a semantic version.
#[inline]
pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
SemVer {
major,
minor,
patch,
}
}
#[inline]
pub(crate) fn encode(&self, w: &mut Writer) {
w.put_u32(self.major)
.put_u32(self.minor)
.put_u32(self.patch);
}
#[inline]
pub(crate) fn decode(r: &mut Reader) -> Result<Self, DecodeError> {
Ok(SemVer {
major: r.get_u32()?,
minor: r.get_u32()?,
patch: r.get_u32()?,
})
}
}
/// A point in wall-clock time, nanoseconds from a region origin (mirrors
/// `epiphany-core`'s Chapter 3 `WallClockTime`, redefined locally to keep the
/// A-only dependency boundary). In the bundle it is the superblock's advisory
/// `commit_timestamp` only; **superblock selection is by generation, never by
/// timestamp** (Chapter 8 §"The Superblock Slots").
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct WallClockTime(pub i64);
impl WallClockTime {
#[inline]
pub(crate) fn encode(&self, w: &mut Writer) {
w.put_i64(self.0);
}
#[inline]
pub(crate) fn decode(r: &mut Reader) -> Result<Self, DecodeError> {
Ok(WallClockTime(r.get_i64()?))
}
}
/// A wall-clock duration, nanoseconds (Chapter 3). Used by [`crate::RetentionPolicy`].
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct WallClockDuration(pub i64);
impl WallClockDuration {
#[inline]
pub(crate) fn encode(&self, w: &mut Writer) {
w.put_i64(self.0);
}
#[inline]
pub(crate) fn decode(r: &mut Reader) -> Result<Self, DecodeError> {
Ok(WallClockDuration(r.get_i64()?))
}
}
/// A causal frontier (a dotted version vector) as seen by the bundle: an opaque,
/// length-prefixed byte string. The bundle stores and round-trips it but does
/// not interpret it — coverage and the DVV partial order are `epiphany-ops`'s
/// job (QUICKSTART: *"a frontier DVV; only `epiphany-ops` interprets it"*).
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct FrontierBytes(pub Vec<u8>);
impl FrontierBytes {
/// An empty frontier (the natural value for a bundle with no canonical base).
pub const fn empty() -> Self {
FrontierBytes(Vec::new())
}
/// Wraps opaque DVV bytes produced by the semantic layer.
#[inline]
pub fn from_bytes(bytes: Vec<u8>) -> Self {
FrontierBytes(bytes)
}
/// The opaque bytes.
#[inline]
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
#[inline]
pub(crate) fn encode(&self, w: &mut Writer) {
w.put_var_bytes(&self.0);
}
#[inline]
pub(crate) fn decode(r: &mut Reader) -> Result<Self, DecodeError> {
Ok(FrontierBytes(r.get_var_bytes()?))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn manifest_id_is_content_derived_and_deterministic() {
let doc = DocumentId([7u8; 16]);
let a = ManifestId::derive(doc, 3, b"body-bytes");
let b = ManifestId::derive(doc, 3, b"body-bytes");
let c = ManifestId::derive(doc, 4, b"body-bytes");
assert_eq!(a, b, "same inputs derive the same manifest id");
assert_ne!(a, c, "a different generation derives a different id");
}
#[test]
fn schema_version_canonical_bytes_are_major_then_minor_le() {
let v = SchemaVersion::new(0x0102, 0x0304);
assert_eq!(v.canonical_bytes(), [0x02, 0x01, 0x04, 0x03]);
}
#[test]
fn blob_id_matches_bare_domain_payload() {
assert_eq!(BlobId::of_payload(b"x").0, ContentHash::of_blob(b"x"));
}
#[test]
fn value_types_round_trip() {
let mut w = Writer::new();
SchemaVersion::new(2, 9).encode(&mut w);
SemVer::new(1, 4, 7).encode(&mut w);
WallClockTime(-123).encode(&mut w);
FrontierBytes::from_bytes(vec![1, 2, 3]).encode(&mut w);
let bytes = w.into_bytes();
let mut r = Reader::new(&bytes);
assert_eq!(
SchemaVersion::decode(&mut r).unwrap(),
SchemaVersion::new(2, 9)
);
assert_eq!(SemVer::decode(&mut r).unwrap(), SemVer::new(1, 4, 7));
assert_eq!(WallClockTime::decode(&mut r).unwrap(), WallClockTime(-123));
assert_eq!(
FrontierBytes::decode(&mut r).unwrap(),
FrontierBytes::from_bytes(vec![1, 2, 3])
);
assert!(r.finish().is_ok());
}
}