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}
40
41impl ObjectType {
42    /// Return the stable u16 code used in object identity bytes.
43    #[must_use]
44    pub const fn code(self) -> u16 {
45        self as u16
46    }
47
48    /// Parse a stable u16 code.
49    pub fn from_code(code: u16) -> Result<Self> {
50        match code {
51            0x01 => Ok(Self::Patch),
52            0x02 => Ok(Self::Block),
53            0x03 => Ok(Self::RefState),
54            0x04 => Ok(Self::RefUpdate),
55            0x05 => Ok(Self::Tag),
56            0x06 => Ok(Self::Attestation),
57            0x07 => Ok(Self::Blob),
58            0x08 => Ok(Self::BlockSummaryCache),
59            0x09 => Ok(Self::RecoveryNote),
60            0x0A => Ok(Self::ProjectGenesis),
61            other => Err(PrikkError::MalformedData(format!(
62                "unknown object type code: {other}"
63            ))),
64        }
65    }
66
67    /// Return a stable human-readable name.
68    #[must_use]
69    pub const fn name(self) -> &'static str {
70        match self {
71            Self::Patch => "patch",
72            Self::Block => "block",
73            Self::RefState => "ref-state",
74            Self::RefUpdate => "ref-update",
75            Self::Tag => "tag",
76            Self::Attestation => "attestation",
77            Self::Blob => "blob",
78            Self::BlockSummaryCache => "block-summary-cache",
79            Self::RecoveryNote => "recovery-note",
80            Self::ProjectGenesis => "project-genesis",
81        }
82    }
83}
84
85impl fmt::Display for ObjectType {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        f.write_str(self.name())
88    }
89}
90
91/// A 32-byte object identifier.
92#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
93pub struct ObjectId([u8; 32]);
94
95impl ObjectId {
96    /// Construct an object ID from raw bytes.
97    #[must_use]
98    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
99        Self(bytes)
100    }
101
102    /// Return raw ID bytes.
103    #[must_use]
104    pub const fn as_bytes(&self) -> &[u8; 32] {
105        &self.0
106    }
107
108    /// Compute an object ID from object type, schema version, and unsigned canonical payload.
109    #[must_use]
110    pub fn from_canonical_payload(
111        object_type: ObjectType,
112        schema_version: u32,
113        canonical_payload: &[u8],
114    ) -> Self {
115        let mut preimage =
116            Vec::with_capacity(OBJECT_ID_DOMAIN.len() + 2 + 4 + 8 + canonical_payload.len());
117        preimage.extend_from_slice(OBJECT_ID_DOMAIN);
118        preimage.extend_from_slice(&object_type.code().to_be_bytes());
119        preimage.extend_from_slice(&schema_version.to_be_bytes());
120        preimage.extend_from_slice(&(canonical_payload.len() as u64).to_be_bytes());
121        preimage.extend_from_slice(canonical_payload);
122        Self(sha256(&preimage))
123    }
124
125    /// Return lowercase hex.
126    #[must_use]
127    pub fn to_hex(&self) -> String {
128        to_hex(&self.0)
129    }
130}
131
132impl fmt::Debug for ObjectId {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        write!(f, "ObjectId({})", self.to_hex())
135    }
136}
137
138impl fmt::Display for ObjectId {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        f.write_str(&self.to_hex())
141    }
142}
143
144impl FromStr for ObjectId {
145    type Err = PrikkError;
146
147    fn from_str(s: &str) -> Result<Self> {
148        if s.len() != 64 {
149            return Err(PrikkError::InvalidObjectId(format!(
150                "expected 64 lowercase hex chars, got {}",
151                s.len()
152            )));
153        }
154        let mut out = [0_u8; 32];
155        for (slot, pair) in out.iter_mut().zip(s.as_bytes().chunks_exact(2)) {
156            let mut bytes = pair.iter().copied();
157            let high = bytes.next().ok_or_else(|| {
158                PrikkError::InvalidObjectId("hex pair is unexpectedly short".to_string())
159            })?;
160            let low = bytes.next().ok_or_else(|| {
161                PrikkError::InvalidObjectId("hex pair is unexpectedly short".to_string())
162            })?;
163            *slot = (hex_value(high)? << 4) | hex_value(low)?;
164        }
165        Ok(Self(out))
166    }
167}
168
169fn hex_value(byte: u8) -> Result<u8> {
170    match byte {
171        b'0'..=b'9' => Ok(byte - b'0'),
172        b'a'..=b'f' => Ok(byte - b'a' + 10),
173        _ => Err(PrikkError::InvalidObjectId(
174            "object IDs must use lowercase hex only".to_string(),
175        )),
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::{ObjectId, ObjectType};
182
183    #[test]
184    fn object_id_is_deterministic() {
185        let a = ObjectId::from_canonical_payload(ObjectType::Patch, 1, b"payload");
186        let b = ObjectId::from_canonical_payload(ObjectType::Patch, 1, b"payload");
187        let c = ObjectId::from_canonical_payload(ObjectType::Block, 1, b"payload");
188        assert_eq!(a, b);
189        assert_ne!(a, c);
190        assert_eq!(
191            a.to_hex(),
192            "5f8711b3f84991d60b65221d66ed5ec260d28cc19c5c4ed3c1fe44d334265fe6"
193        );
194    }
195
196    #[test]
197    fn hex_roundtrip() {
198        let id = ObjectId::from_canonical_payload(ObjectType::Patch, 1, b"payload");
199        let text = id.to_hex();
200        let parsed = text.parse::<ObjectId>();
201        assert_eq!(parsed, Ok(id));
202    }
203}