//! Object references: BLAKE3-256 over a domain-separating header and the body //! (spec §5.2). use std::fmt; #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ObjectRef(pub [u8; 32]); /// A change is referenced by the ObjectRef of its encoding. pub type ChangeHash = ObjectRef; /// An identity is referenced by the ObjectRef of its encoding. pub type KeyId = ObjectRef; impl ObjectRef { /// Reserved: means "this change" inside a change (spec §5.2, §7.4). pub const ZERO: ObjectRef = ObjectRef([0; 32]); pub fn is_zero(&self) -> bool { *self == Self::ZERO } pub fn to_hex(&self) -> String { hex::encode(self.0) } pub fn from_hex(s: &str) -> Option { let v = hex::decode(s).ok()?; let a: [u8; 32] = v.try_into().ok()?; Some(ObjectRef(a)) } pub fn from_slice(b: &[u8]) -> Option { let a: [u8; 32] = b.try_into().ok()?; Some(ObjectRef(a)) } /// First `n` hex characters. pub fn short(&self, n: usize) -> String { let h = self.to_hex(); h[..n.min(h.len())].to_string() } pub fn starts_with_hex(&self, prefix: &str) -> bool { self.to_hex().starts_with(prefix) } } impl fmt::Display for ObjectRef { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.to_hex()) } } impl fmt::Debug for ObjectRef { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "#{}", self.short(12)) } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum ObjectType { Chunk, Blob, Change, Checkpoint, Op, Attest, Identity, Policy, State, } impl ObjectType { pub const ALL: [ObjectType; 9] = [ ObjectType::Chunk, ObjectType::Blob, ObjectType::Change, ObjectType::Checkpoint, ObjectType::Op, ObjectType::Attest, ObjectType::Identity, ObjectType::Policy, ObjectType::State, ]; pub fn name(self) -> &'static str { match self { ObjectType::Chunk => "chunk", ObjectType::Blob => "blob", ObjectType::Change => "change", ObjectType::Checkpoint => "checkpoint", ObjectType::Op => "op", ObjectType::Attest => "attest", ObjectType::Identity => "identity", ObjectType::Policy => "policy", ObjectType::State => "state", } } pub fn from_name(s: &str) -> Option { Self::ALL.iter().copied().find(|t| t.name() == s) } } /// `ObjectRef = BLAKE3-256("oxy1 " || type || " " || decimal(len) || 0x00 || body)`. pub fn hash_object(ty: ObjectType, body: &[u8]) -> ObjectRef { let mut h = blake3::Hasher::new(); h.update(&header(ty, body.len())); h.update(body); ObjectRef(*h.finalize().as_bytes()) } pub fn header(ty: ObjectType, len: usize) -> Vec { let mut v = Vec::with_capacity(32); v.extend_from_slice(b"oxy1 "); v.extend_from_slice(ty.name().as_bytes()); v.push(b' '); v.extend_from_slice(len.to_string().as_bytes()); v.push(0); v } #[cfg(test)] mod tests { use super::*; #[test] fn header_matches_spec() { assert_eq!(header(ObjectType::Chunk, 5), b"oxy1 chunk 5\0"); assert_eq!(header(ObjectType::Change, 1234), b"oxy1 change 1234\0"); } #[test] fn types_are_domain_separated() { let body = b"same bytes"; let a = hash_object(ObjectType::Chunk, body); let b = hash_object(ObjectType::Blob, body); assert_ne!(a, b); assert_eq!(a, hash_object(ObjectType::Chunk, body)); // Reference vector: pin so the format cannot drift silently. assert_eq!( hash_object(ObjectType::Chunk, b"").to_hex(), blake3::hash(b"oxy1 chunk 0\0").to_hex().as_str() ); } #[test] fn hex_round_trip() { let r = hash_object(ObjectType::Chunk, b"x"); assert_eq!(ObjectRef::from_hex(&r.to_hex()), Some(r)); assert_eq!(ObjectRef::from_hex("zz"), None); assert!(r.starts_with_hex(&r.short(7))); assert!(ObjectRef::ZERO.is_zero()); for t in ObjectType::ALL { assert_eq!(ObjectType::from_name(t.name()), Some(t)); } } }