1use core::fmt;
4use core::str::FromStr;
5
6use prikk_error::{PrikkError, Result};
7use prikk_hash::{sha256, to_hex};
8
9pub const OBJECT_ID_DOMAIN: &[u8] = b"PRIKK-OBJECT-ID-v1";
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14#[repr(u16)]
15pub enum ObjectType {
16 Patch = 1,
18 Block = 2,
20 RefState = 3,
22 Tag = 4,
24 Attestation = 5,
26 Blob = 6,
28 RefUpdate = 7,
30}
31
32impl ObjectType {
33 #[must_use]
35 pub const fn code(self) -> u16 {
36 self as u16
37 }
38
39 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 #[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#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
78pub struct ObjectId([u8; 32]);
79
80impl ObjectId {
81 #[must_use]
83 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
84 Self(bytes)
85 }
86
87 #[must_use]
89 pub const fn as_bytes(&self) -> &[u8; 32] {
90 &self.0
91 }
92
93 #[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 #[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}