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 = 0x01,
18 Block = 0x02,
20 RefState = 0x03,
22 RefUpdate = 0x04,
25 Tag = 0x05,
27 Attestation = 0x06,
29 Blob = 0x07,
31 BlockSummaryCache = 0x08,
34 RecoveryNote = 0x09,
37 ProjectGenesis = 0x0A,
39}
40
41impl ObjectType {
42 #[must_use]
44 pub const fn code(self) -> u16 {
45 self as u16
46 }
47
48 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 #[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#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
93pub struct ObjectId([u8; 32]);
94
95impl ObjectId {
96 #[must_use]
98 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
99 Self(bytes)
100 }
101
102 #[must_use]
104 pub const fn as_bytes(&self) -> &[u8; 32] {
105 &self.0
106 }
107
108 #[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 #[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}