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