377 lines
14 KiB
Rust
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());
|
|
}
|
|
}
|