OXYGENHUB
# 61e9ad6ddc2d oxygen / crates / oxy-core / src / hash.rs
4.2 KiB · Rust · last changed Import the Oxygen specification and the first Rust implementation (root change) 1 month ago source blame history raw
ff0af1ee unknown 1
//! Object references: BLAKE3-256 over a domain-separating header and the body
2
//! (spec §5.2).
3
4
use std::fmt;
5
6
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
7
pub struct ObjectRef(pub [u8; 32]);
8
9
/// A change is referenced by the ObjectRef of its encoding.
10
pub type ChangeHash = ObjectRef;
11
/// An identity is referenced by the ObjectRef of its encoding.
12
pub type KeyId = ObjectRef;
13
14
impl ObjectRef {
15
    /// Reserved: means "this change" inside a change (spec §5.2, §7.4).
16
    pub const ZERO: ObjectRef = ObjectRef([0; 32]);
17
18
    pub fn is_zero(&self) -> bool {
19
        *self == Self::ZERO
20
    }
21
    pub fn to_hex(&self) -> String {
22
        hex::encode(self.0)
23
    }
24
    pub fn from_hex(s: &str) -> Option<ObjectRef> {
25
        let v = hex::decode(s).ok()?;
26
        let a: [u8; 32] = v.try_into().ok()?;
27
        Some(ObjectRef(a))
28
    }
29
    pub fn from_slice(b: &[u8]) -> Option<ObjectRef> {
30
        let a: [u8; 32] = b.try_into().ok()?;
31
        Some(ObjectRef(a))
32
    }
33
    /// First `n` hex characters.
34
    pub fn short(&self, n: usize) -> String {
35
        let h = self.to_hex();
36
        h[..n.min(h.len())].to_string()
37
    }
38
    pub fn starts_with_hex(&self, prefix: &str) -> bool {
39
        self.to_hex().starts_with(prefix)
40
    }
41
}
42
43
impl fmt::Display for ObjectRef {
44
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45
        f.write_str(&self.to_hex())
46
    }
47
}
48
49
impl fmt::Debug for ObjectRef {
50
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51
        write!(f, "#{}", self.short(12))
52
    }
53
}
54
55
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
56
pub enum ObjectType {
57
    Chunk,
58
    Blob,
59
    Change,
60
    Checkpoint,
61
    Op,
62
    Attest,
63
    Identity,
64
    Policy,
65
}
66
67
impl ObjectType {
68
    pub const ALL: [ObjectType; 8] = [
69
        ObjectType::Chunk,
70
        ObjectType::Blob,
71
        ObjectType::Change,
72
        ObjectType::Checkpoint,
73
        ObjectType::Op,
74
        ObjectType::Attest,
75
        ObjectType::Identity,
76
        ObjectType::Policy,
77
    ];
78
79
    pub fn name(self) -> &'static str {
80
        match self {
81
            ObjectType::Chunk => "chunk",
82
            ObjectType::Blob => "blob",
83
            ObjectType::Change => "change",
84
            ObjectType::Checkpoint => "checkpoint",
85
            ObjectType::Op => "op",
86
            ObjectType::Attest => "attest",
87
            ObjectType::Identity => "identity",
88
            ObjectType::Policy => "policy",
89
        }
90
    }
91
92
    pub fn from_name(s: &str) -> Option<ObjectType> {
93
        Self::ALL.iter().copied().find(|t| t.name() == s)
94
    }
95
}
96
97
/// `ObjectRef = BLAKE3-256("oxy1 " || type || " " || decimal(len) || 0x00 || body)`.
98
pub fn hash_object(ty: ObjectType, body: &[u8]) -> ObjectRef {
99
    let mut h = blake3::Hasher::new();
100
    h.update(&header(ty, body.len()));
101
    h.update(body);
102
    ObjectRef(*h.finalize().as_bytes())
103
}
104
105
pub fn header(ty: ObjectType, len: usize) -> Vec<u8> {
106
    let mut v = Vec::with_capacity(32);
107
    v.extend_from_slice(b"oxy1 ");
108
    v.extend_from_slice(ty.name().as_bytes());
109
    v.push(b' ');
110
    v.extend_from_slice(len.to_string().as_bytes());
111
    v.push(0);
112
    v
113
}
114
115
#[cfg(test)]
116
mod tests {
117
    use super::*;
118
119
    #[test]
120
    fn header_matches_spec() {
121
        assert_eq!(header(ObjectType::Chunk, 5), b"oxy1 chunk 5\0");
122
        assert_eq!(header(ObjectType::Change, 1234), b"oxy1 change 1234\0");
123
    }
124
125
    #[test]
126
    fn types_are_domain_separated() {
127
        let body = b"same bytes";
128
        let a = hash_object(ObjectType::Chunk, body);
129
        let b = hash_object(ObjectType::Blob, body);
130
        assert_ne!(a, b);
131
        assert_eq!(a, hash_object(ObjectType::Chunk, body));
132
        // Reference vector: pin so the format cannot drift silently.
133
        assert_eq!(
134
            hash_object(ObjectType::Chunk, b"").to_hex(),
135
            blake3::hash(b"oxy1 chunk 0\0").to_hex().as_str()
136
        );
137
    }
138
139
    #[test]
140
    fn hex_round_trip() {
141
        let r = hash_object(ObjectType::Chunk, b"x");
142
        assert_eq!(ObjectRef::from_hex(&r.to_hex()), Some(r));
143
        assert_eq!(ObjectRef::from_hex("zz"), None);
144
        assert!(r.starts_with_hex(&r.short(7)));
145
        assert!(ObjectRef::ZERO.is_zero());
146
        for t in ObjectType::ALL {
147
            assert_eq!(ObjectType::from_name(t.name()), Some(t));
148
        }
149
    }
150
}