OXYGENHUB

Coverage / crates/oxy-core/src/hash.rs 100.0%

86 of 86 lines run, 28 of 29 functions · run #417 on main · run missed

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
1836075×
    pub fn is_zero(&self) -> bool {
1936075×
        *self == Self::ZERO
2036075×
    }
21140077×
    pub fn to_hex(&self) -> String {
22140077×
        hex::encode(self.0)
23140077×
    }
2438468×
    pub fn from_hex(s: &str) -> Option<ObjectRef> {
2538468×
        let v = hex::decode(s).ok()?;
2638464×
        let a: [u8; 32] = v.try_into().ok()?;
2738464×
        Some(ObjectRef(a))
2838468×
    }
29184906×
    pub fn from_slice(b: &[u8]) -> Option<ObjectRef> {
30184906×
        let a: [u8; 32] = b.try_into().ok()?;
31184906×
        Some(ObjectRef(a))
32184906×
    }
33
    /// First `n` hex characters.
343714×
    pub fn short(&self, n: usize) -> String {
353714×
        let h = self.to_hex();
363714×
        h[..n.min(h.len())].to_string()
373714×
    }
38122×
    pub fn starts_with_hex(&self, prefix: &str) -> bool {
39122×
        self.to_hex().starts_with(prefix)
40122×
    }
41
}
42
43
impl fmt::Display for ObjectRef {
44123×
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45123×
        f.write_str(&self.to_hex())
46123×
    }
47
}
48
49
impl fmt::Debug for ObjectRef {
502926×
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
512926×
        write!(f, "#{}", self.short(12))
522926×
    }
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
    State,
66
    Receipt,
67
    /// A short-lived key's certificate (oxygenhub#241).
68
    Certificate,
69
    /// An author's grant to a certified subject (§14.7).
70
    Grant,
71
    /// A tree of files as one immutable thing: an image, a package, an
72
    /// archive (Cylinder, docs/cylinder.md §4.1).
73
    Artifact,
74
}
75
76
impl ObjectType {
77
    pub const ALL: [ObjectType; 13] = [
78
        ObjectType::Chunk,
79
        ObjectType::Blob,
80
        ObjectType::Change,
81
        ObjectType::Checkpoint,
82
        ObjectType::Op,
83
        ObjectType::Attest,
84
        ObjectType::Identity,
85
        ObjectType::Policy,
86
        ObjectType::State,
87
        ObjectType::Receipt,
88
        ObjectType::Certificate,
89
        ObjectType::Grant,
90
        ObjectType::Artifact,
91
    ];
92
93406038×
    pub fn name(self) -> &'static str {
94406038×
        match self {
9597920×
            ObjectType::Chunk => "chunk",
9658948×
            ObjectType::Blob => "blob",
9792858×
            ObjectType::Change => "change",
9831948×
            ObjectType::Checkpoint => "checkpoint",
9965717×
            ObjectType::Op => "op",
10013571×
            ObjectType::Attest => "attest",
10113690×
            ObjectType::Identity => "identity",
1029042×
            ObjectType::Policy => "policy",
10321392×
            ObjectType::State => "state",
104244×
            ObjectType::Receipt => "receipt",
105649×
            ObjectType::Certificate => "certificate",
10625×
            ObjectType::Grant => "grant",
10734×
            ObjectType::Artifact => "artifact",
108
        }
109406038×
    }
110
11158340×
    pub fn from_name(s: &str) -> Option<ObjectType> {
112262142×
        Self::ALL.iter().copied().find(|t| t.name() == s)
11358340×
    }
114
}
115
116
/// `ObjectRef = BLAKE3-256("oxy1 " || type || " " || decimal(len) || 0x00 || body)`.
117129488×
pub fn hash_object(ty: ObjectType, body: &[u8]) -> ObjectRef {
118129488×
    let mut h = blake3::Hasher::new();
119129488×
    h.update(&header(ty, body.len()));
120129488×
    h.update(body);
121129488×
    ObjectRef(*h.finalize().as_bytes())
122129488×
}
123
124143670×
pub fn header(ty: ObjectType, len: usize) -> Vec<u8> {
125143670×
    let mut v = Vec::with_capacity(32);
126143670×
    v.extend_from_slice(b"oxy1 ");
127143670×
    v.extend_from_slice(ty.name().as_bytes());
128143670×
    v.push(b' ');
129143670×
    v.extend_from_slice(len.to_string().as_bytes());
130143670×
    v.push(0);
131143670×
    v
132143670×
}
133
134
#[cfg(test)]
135
mod tests {
136
    use super::*;
137
138
    #[test]
1391×
    fn header_matches_spec() {
1401×
        assert_eq!(header(ObjectType::Chunk, 5), b"oxy1 chunk 5\0");
1411×
        assert_eq!(header(ObjectType::Change, 1234), b"oxy1 change 1234\0");
1421×
    }
143
144
    #[test]
1451×
    fn types_are_domain_separated() {
1461×
        let body = b"same bytes";
1471×
        let a = hash_object(ObjectType::Chunk, body);
1481×
        let b = hash_object(ObjectType::Blob, body);
1491×
        assert_ne!(a, b);
1501×
        assert_eq!(a, hash_object(ObjectType::Chunk, body));
151
        // Reference vector: pin so the format cannot drift silently.
1521×
        assert_eq!(
1531×
            hash_object(ObjectType::Chunk, b"").to_hex(),
1541×
            blake3::hash(b"oxy1 chunk 0\0").to_hex().as_str()
155
        );
1561×
    }
157
158
    #[test]
1591×
    fn hex_round_trip() {
1601×
        let r = hash_object(ObjectType::Chunk, b"x");
1611×
        assert_eq!(ObjectRef::from_hex(&r.to_hex()), Some(r));
1621×
        assert_eq!(ObjectRef::from_hex("zz"), None);
1631×
        assert!(r.starts_with_hex(&r.short(7)));
1641×
        assert!(ObjectRef::ZERO.is_zero());
16513×
        for t in ObjectType::ALL {
16613×
            assert_eq!(ObjectType::from_name(t.name()), Some(t));
167
        }
1681×
    }
169
}