use crate::hash::fnv32;
pub fn line_hash(prev: Option<&str>, line: &str) -> String {
let h = match prev {
None => fnv32(line.as_bytes()),
Some(p) => {
let mut buf = Vec::with_capacity(p.len() + 1 + line.len());
buf.extend_from_slice(p.as_bytes());
buf.push(b'\n');
buf.extend_from_slice(line.as_bytes());
fnv32(&buf)
}
};
let folded = (h ^ (h >> 12) ^ (h >> 24)) & 0xfff;
format!("{folded:03x}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hash_is_three_lowercase_hex_chars() {
for s in ["", "x", "fn main() {}", " let y = 1;", "🦀 unicode"] {
let h = line_hash(None, s);
assert_eq!(h.len(), 3, "hash {h:?} for {s:?} not 3 chars");
assert!(
h.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_uppercase()),
"hash {h:?} not lowercase hex"
);
}
}
#[test]
fn first_line_hash_output_is_locked() {
for (input, expected) in [
("", "c8d"),
("x", "0bf"),
("fn main() {}", "8a1"),
(" let y = 1;", "b4d"),
("🦀 unicode", "64b"),
("let total = a + b;", "e3b"),
] {
assert_eq!(line_hash(None, input), expected, "drift for {input:?}");
}
}
#[test]
fn hash_is_deterministic() {
assert_eq!(
line_hash(Some("a"), "let total = a + b;"),
line_hash(Some("a"), "let total = a + b;")
);
}
#[test]
fn distinct_lines_usually_differ() {
let a = line_hash(None, " return Ok(());");
let b = line_hash(None, " return Err(e);");
assert_ne!(a, b);
}
#[test]
fn whitespace_is_significant() {
assert_ne!(line_hash(None, "x = 1"), line_hash(None, " x = 1"));
}
#[test]
fn predecessor_is_part_of_the_hash() {
let line = " let y = 1;";
assert_ne!(
line_hash(Some("prev-a"), line),
line_hash(Some("prev-b"), line)
);
assert_ne!(line_hash(None, line), line_hash(Some(""), line));
}
}