use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ContentHash(String);
impl ContentHash {
pub fn of_text(text: &str) -> Self {
let normalised = normalise_newlines(text);
Self::of_bytes(normalised.as_bytes())
}
pub fn of_bytes(bytes: &[u8]) -> Self {
Self(format!("blake3:{}", blake3::hash(bytes).to_hex()))
}
pub fn parse(s: &str) -> Option<Self> {
let hex = s.strip_prefix("blake3:")?;
(hex.len() == 64 && hex.bytes().all(|b| b.is_ascii_hexdigit())).then(|| Self(s.to_ascii_lowercase()))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn short(&self, n: usize) -> &str {
let hex = &self.0["blake3:".len()..];
&hex[..n.min(hex.len())]
}
}
impl fmt::Display for ContentHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
pub(crate) fn normalise_newlines(text: &str) -> std::borrow::Cow<'_, str> {
if !text.contains('\r') {
return std::borrow::Cow::Borrowed(text);
}
std::borrow::Cow::Owned(text.replace("\r\n", "\n").replace('\r', "\n"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_round_trips() {
let h = ContentHash::of_text("x");
assert_eq!(ContentHash::parse(h.as_str()), Some(h.clone()));
assert_eq!(ContentHash::parse("md5:abc"), None);
assert_eq!(h.short(8).len(), 8);
}
}