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}