use xxhash_rust::xxh32::xxh32;
pub(crate) const HASHLINE_FILE_HASH_LENGTH: usize = 4;
pub(crate) fn compute_file_hash(text: &str) -> String {
let normalized = normalize_file_hash_text(text);
let low16 = xxh32(normalized.as_bytes(), 0) & 0xffff;
format!("{low16:0HASHLINE_FILE_HASH_LENGTH$X}")
}
pub(crate) fn format_hashline_header(path: &str, tag: &str) -> String {
format!("[{path}#{tag}]")
}
pub(crate) fn format_numbered_lines(lines: &[(usize, String)]) -> Vec<String> {
lines
.iter()
.map(|(line_number, line)| format!("{line_number}:{line}"))
.collect()
}
fn normalize_file_hash_text(text: &str) -> String {
let mut output = String::with_capacity(text.len());
let bytes = text.as_bytes();
let mut start = 0usize;
for (index, byte) in bytes.iter().enumerate() {
if *byte == b'\n' {
let mut end = index;
while end > start && matches!(bytes[end - 1], b' ' | b'\t' | b'\r') {
end -= 1;
}
output.push_str(&text[start..end]);
output.push('\n');
start = index + 1;
}
}
let mut end = text.len();
while end > start && matches!(bytes[end - 1], b' ' | b'\t' | b'\r') {
end -= 1;
}
output.push_str(&text[start..end]);
output
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compute_file_hash_matches_xxhash32_low16_vectors() {
assert_eq!(compute_file_hash(""), "5D05");
assert_eq!(compute_file_hash("hello"), "77F9");
assert_eq!(compute_file_hash("hello\n"), "5BF9");
assert_eq!(compute_file_hash("one\ntwo\n"), "0127");
}
#[test]
fn compute_file_hash_ignores_trailing_whitespace_and_crlf_shape() {
assert_eq!(
compute_file_hash("hello\n"),
compute_file_hash("hello \n")
);
assert_eq!(
compute_file_hash("one\ntwo\n"),
compute_file_hash("one\r\ntwo\t\r\n")
);
assert_ne!(compute_file_hash("hello\n"), compute_file_hash("hello!\n"));
}
#[test]
fn hashline_format_helpers_emit_header_and_numbered_rows() {
assert_eq!(
format_hashline_header("src/lib.rs", "ABCD"),
"[src/lib.rs#ABCD]"
);
assert_eq!(
format_numbered_lines(&[(2, "two".to_string()), (3, "three".to_string())]),
vec!["2:two".to_string(), "3:three".to_string()]
);
}
}