1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
20#[serde(transparent)]
21pub struct ContentHash(String);
22
23impl ContentHash {
24 pub fn of_text(text: &str) -> Self {
26 let normalised = normalise_newlines(text);
27 Self::of_bytes(normalised.as_bytes())
28 }
29
30 pub fn of_bytes(bytes: &[u8]) -> Self {
32 Self(format!("blake3:{}", blake3::hash(bytes).to_hex()))
33 }
34
35 pub fn parse(s: &str) -> Option<Self> {
39 let hex = s.strip_prefix("blake3:")?;
40 (hex.len() == 64 && hex.bytes().all(|b| b.is_ascii_hexdigit())).then(|| Self(s.to_ascii_lowercase()))
41 }
42
43 pub fn as_str(&self) -> &str {
45 &self.0
46 }
47
48 pub fn short(&self, n: usize) -> &str {
50 let hex = &self.0["blake3:".len()..];
51 &hex[..n.min(hex.len())]
52 }
53}
54
55impl fmt::Display for ContentHash {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 f.write_str(&self.0)
58 }
59}
60
61pub(crate) fn normalise_newlines(text: &str) -> std::borrow::Cow<'_, str> {
63 if !text.contains('\r') {
64 return std::borrow::Cow::Borrowed(text);
65 }
66 std::borrow::Cow::Owned(text.replace("\r\n", "\n").replace('\r', "\n"))
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn parse_round_trips() {
75 let h = ContentHash::of_text("x");
76 assert_eq!(ContentHash::parse(h.as_str()), Some(h.clone()));
77 assert_eq!(ContentHash::parse("md5:abc"), None);
78 assert_eq!(h.short(8).len(), 8);
79 }
80}