Skip to main content

somatize_store/
digest.rs

1//! What identifies some bytes: their content.
2
3use sha2::{Digest as _, Sha256};
4use std::fmt;
5
6/// The identity of some bytes, written as `sha256:` and hex.
7///
8/// The prefix is not decoration: it is what allows another algorithm the day one
9/// is needed without every stored name becoming ambiguous.
10#[derive(
11    Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
12)]
13pub struct Digest(String);
14
15impl Digest {
16    /// What identifies these bytes.
17    pub fn of(bytes: &[u8]) -> Self {
18        let mut hasher = Sha256::new();
19        hasher.update(bytes);
20        Self(format!("sha256:{:x}", hasher.finalize()))
21    }
22
23    /// A digest someone else computed — the id of an artifact, a key read back
24    /// from a record.
25    pub fn parse(text: impl Into<String>) -> Self {
26        Self(text.into())
27    }
28
29    /// As text.
30    pub fn as_str(&self) -> &str {
31        &self.0
32    }
33
34    /// How it is split into a directory and a file, so no directory ends up with
35    /// a million entries. The directory comes from the **hash** and not the whole
36    /// string — every digest starts `sha256:`, and they would all land in one
37    /// `sh/`. Public because a directory and a bucket lay bytes out the same way,
38    /// which is what lets one be copied into the other.
39    pub fn path(&self) -> (String, String) {
40        let hash = flatten(self.0.rsplit(':').next().unwrap_or(&self.0));
41        let head = hash.get(..2).unwrap_or(hash.as_str()).to_string();
42        (head, flatten(&self.0))
43    }
44}
45
46impl fmt::Display for Digest {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        f.write_str(&self.0)
49    }
50}
51
52/// Whatever a caller invented, as something a filesystem takes.
53fn flatten(text: &str) -> String {
54    text.chars()
55        .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
56        .collect()
57}