Skip to main content

prikk_object/
id.rs

1//! Object identifiers and object type codes.
2
3use core::fmt;
4use core::str::FromStr;
5
6use prikk_error::{PrikkError, Result};
7use prikk_hash::{sha256, to_hex};
8
9/// Single domain used for object identity preimages.
10pub const OBJECT_ID_DOMAIN: &[u8] = b"PRIKK-OBJECT-ID-v1";
11
12/// A Prikk object type code.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14#[repr(u16)]
15pub enum ObjectType {
16    /// Patch object.
17    Patch = 0x01,
18    /// Block object.
19    Block = 0x02,
20    /// RefState object.
21    RefState = 0x03,
22    /// RefUpdate event. Object-envelope type stored inline in `refs/logs/`
23    /// (journal then log), not a permanent object-store directory.
24    RefUpdate = 0x04,
25    /// Tag object.
26    Tag = 0x05,
27    /// Attestation object.
28    Attestation = 0x06,
29    /// Blob object.
30    Blob = 0x07,
31    /// Rebuildable block-summary cache. Uses the canonical codec for
32    /// reproducibility but is never a root of trust or part of block identity.
33    BlockSummaryCache = 0x08,
34    /// Signed doctor-repair note stored inline in `refs/recovery/`. Never a
35    /// `RefUpdate` substitute (FDD-02 §10.4).
36    RecoveryNote = 0x09,
37    /// Project identity anchor; its `ObjectId` is the `project_id` (FDD-03 §9.13).
38    ProjectGenesis = 0x0A,
39    /// RFC 115 Stage 2 (design-v1.md D3): a signed claim that named patches were sealed into a
40    /// named block, under the signer's key. Never trust-conferring and never existence-checked
41    /// against the block/patches it names — see `RecognitionClaimPayload`'s own doc.
42    RecognitionClaim = 0x0B,
43}
44
45impl ObjectType {
46    /// Return the stable u16 code used in object identity bytes.
47    #[must_use]
48    pub const fn code(self) -> u16 {
49        self as u16
50    }
51
52    /// Parse a stable u16 code.
53    pub fn from_code(code: u16) -> Result<Self> {
54        match code {
55            0x01 => Ok(Self::Patch),
56            0x02 => Ok(Self::Block),
57            0x03 => Ok(Self::RefState),
58            0x04 => Ok(Self::RefUpdate),
59            0x05 => Ok(Self::Tag),
60            0x06 => Ok(Self::Attestation),
61            0x07 => Ok(Self::Blob),
62            0x08 => Ok(Self::BlockSummaryCache),
63            0x09 => Ok(Self::RecoveryNote),
64            0x0A => Ok(Self::ProjectGenesis),
65            0x0B => Ok(Self::RecognitionClaim),
66            other => Err(PrikkError::MalformedData(format!(
67                "unknown object type code: {other}"
68            ))),
69        }
70    }
71
72    /// Return a stable human-readable name.
73    #[must_use]
74    pub const fn name(self) -> &'static str {
75        match self {
76            Self::Patch => "patch",
77            Self::Block => "block",
78            Self::RefState => "ref-state",
79            Self::RefUpdate => "ref-update",
80            Self::Tag => "tag",
81            Self::Attestation => "attestation",
82            Self::Blob => "blob",
83            Self::BlockSummaryCache => "block-summary-cache",
84            Self::RecoveryNote => "recovery-note",
85            Self::ProjectGenesis => "project-genesis",
86            Self::RecognitionClaim => "recognition-claim",
87        }
88    }
89}
90
91impl fmt::Display for ObjectType {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        f.write_str(self.name())
94    }
95}
96
97/// A 32-byte object identifier.
98#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
99pub struct ObjectId([u8; 32]);
100
101impl ObjectId {
102    /// Construct an object ID from raw bytes.
103    #[must_use]
104    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
105        Self(bytes)
106    }
107
108    /// Return raw ID bytes.
109    #[must_use]
110    pub const fn as_bytes(&self) -> &[u8; 32] {
111        &self.0
112    }
113
114    /// Compute an object ID from object type, schema version, and unsigned canonical payload.
115    #[must_use]
116    pub fn from_canonical_payload(
117        object_type: ObjectType,
118        schema_version: u32,
119        canonical_payload: &[u8],
120    ) -> Self {
121        let mut preimage =
122            Vec::with_capacity(OBJECT_ID_DOMAIN.len() + 2 + 4 + 8 + canonical_payload.len());
123        preimage.extend_from_slice(OBJECT_ID_DOMAIN);
124        preimage.extend_from_slice(&object_type.code().to_be_bytes());
125        preimage.extend_from_slice(&schema_version.to_be_bytes());
126        preimage.extend_from_slice(&(canonical_payload.len() as u64).to_be_bytes());
127        preimage.extend_from_slice(canonical_payload);
128        Self(sha256(&preimage))
129    }
130
131    /// Return lowercase hex.
132    #[must_use]
133    pub fn to_hex(&self) -> String {
134        to_hex(&self.0)
135    }
136}
137
138impl fmt::Debug for ObjectId {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        write!(f, "ObjectId({})", self.to_hex())
141    }
142}
143
144impl fmt::Display for ObjectId {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        f.write_str(&self.to_hex())
147    }
148}
149
150impl FromStr for ObjectId {
151    type Err = PrikkError;
152
153    fn from_str(s: &str) -> Result<Self> {
154        if s.len() != 64 {
155            return Err(PrikkError::InvalidObjectId(format!(
156                "expected 64 lowercase hex chars, got {}",
157                s.len()
158            )));
159        }
160        let mut out = [0_u8; 32];
161        for (slot, pair) in out.iter_mut().zip(s.as_bytes().chunks_exact(2)) {
162            let mut bytes = pair.iter().copied();
163            let high = bytes.next().ok_or_else(|| {
164                PrikkError::InvalidObjectId("hex pair is unexpectedly short".to_string())
165            })?;
166            let low = bytes.next().ok_or_else(|| {
167                PrikkError::InvalidObjectId("hex pair is unexpectedly short".to_string())
168            })?;
169            *slot = (hex_value(high)? << 4) | hex_value(low)?;
170        }
171        Ok(Self(out))
172    }
173}
174
175fn hex_value(byte: u8) -> Result<u8> {
176    match byte {
177        b'0'..=b'9' => Ok(byte - b'0'),
178        b'a'..=b'f' => Ok(byte - b'a' + 10),
179        _ => Err(PrikkError::InvalidObjectId(
180            "object IDs must use lowercase hex only".to_string(),
181        )),
182    }
183}
184
185#[cfg(test)]
186mod tests;