use derive_more::{Display, FromStr};
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
const BINARY_SNIFF: usize = 8000;
pub(crate) const BOM: &[u8] = b"\xEF\xBB\xBF";
#[derive(
Clone,
Copy,
PartialEq,
Eq,
Hash,
Display,
derive_more::Debug,
FromStr,
SerializeDisplay,
DeserializeFromStr,
)]
#[debug("{_0}")]
pub struct Digest(blake3::Hash);
impl Digest {
#[must_use]
pub fn of(bytes: &[u8]) -> Self {
Self(blake3::hash(bytes))
}
pub(crate) const fn new(hash: blake3::Hash) -> Self {
Self(hash)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
self.0.as_bytes()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Fingerprint {
pub exact: Digest,
pub canonical: Digest,
}
impl Fingerprint {
#[must_use]
pub fn of(bytes: &[u8]) -> Self {
let exact = Digest::of(bytes);
let canonical = if is_binary(bytes) {
exact
} else {
let mut hasher = blake3::Hasher::new();
canonical(bytes, |part| {
hasher.update(part);
});
Digest(hasher.finalize())
};
Self { exact, canonical }
}
#[must_use]
pub fn same_content(&self, other: &Self) -> bool {
self.canonical == other.canonical
}
}
pub(crate) fn is_binary(bytes: &[u8]) -> bool {
bytes.get(..BINARY_SNIFF).unwrap_or(bytes).contains(&0)
}
fn canonical<F: FnMut(&[u8])>(bytes: &[u8], mut emit: F) {
let text = bytes.strip_prefix(BOM).unwrap_or(bytes);
let mut blank = 0_usize;
let mut started = false;
for line in text.split(|&b| b == b'\n').map(<[u8]>::trim_ascii_end) {
if line.is_empty() {
blank = blank.saturating_add(1);
continue;
}
if started {
emit(b"\n");
}
for _ in 0..blank {
emit(b"\n");
}
emit(line);
started = true;
blank = 0;
}
if started {
emit(b"\n");
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::{Fingerprint, canonical};
fn canon(bytes: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
canonical(bytes, |part| out.extend_from_slice(part));
out
}
#[test]
fn canonical_form() {
for (input, want) in [
(&b""[..], &b""[..]),
(b" \n\t\n", b""),
(b"a", b"a\n"),
(b"a\n\n\n", b"a\n"),
(b"a \r\nb\t\r\n", b"a\nb\n"),
(b"\xEF\xBB\xBFa\n", b"a\n"),
(b"\n\na\n\n b\n", b"\n\na\n\n b\n"),
(b"a\rb\n", b"a\rb\n"),
] {
assert_eq!(canon(input), want, "input {:?}", String::from_utf8_lossy(input));
}
}
#[test]
fn binary_is_exact() {
let bytes = b"a \0 b \r\n";
let fp = Fingerprint::of(bytes);
assert_eq!(fp.exact, fp.canonical, "binary content must not be canonicalized");
}
proptest! {
#[test]
fn idempotent(text in "[a-z \t\r\n]{0,64}") {
let once = canon(text.as_bytes());
prop_assert_eq!(canon(&once), once);
}
#[test]
fn cosmetic_changes_keep_content(text in "[a-z]{1,8}(\n[a-z ]{0,8}){0,8}") {
let noisy = format!("\u{FEFF}{} \r\n\n\n", text.replace('\n', " \t\r\n"));
let (a, b) = (Fingerprint::of(text.as_bytes()), Fingerprint::of(noisy.as_bytes()));
prop_assert!(a.same_content(&b), "{text:?} vs {noisy:?}");
}
#[test]
fn real_changes_are_seen(
(text, at) in "[a-z]{1,16}".prop_flat_map(|t| { let n = t.len(); (Just(t), 0..n) })
) {
let mut edited = text.clone().into_bytes();
edited[at] = b'Z';
let (a, b) = (Fingerprint::of(text.as_bytes()), Fingerprint::of(&edited));
prop_assert!(!a.same_content(&b), "{text:?} vs {:?}", String::from_utf8_lossy(&edited));
}
}
}