pub const FINGERPRINT_KEY: &str = "tools.fallow.fingerprint/v1";
pub const GHAS_FINGERPRINT_KEY: &str = "primaryLocationLineHash/v1";
#[must_use]
pub fn normalize_snippet(snippet: &str) -> String {
snippet
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join("\n")
}
#[must_use]
pub fn fingerprint_hash(parts: &[&str]) -> String {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325; for part in parts {
for byte in part.bytes() {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x0100_0000_01b3); }
hash ^= 0xff;
hash = hash.wrapping_mul(0x0100_0000_01b3);
}
format!("{hash:016x}")
}
#[must_use]
pub fn finding_fingerprint(rule_id: &str, path: &str, snippet: &str) -> String {
let normalized = normalize_snippet(snippet);
fingerprint_hash(&[rule_id, path, &normalized])
}
#[must_use]
pub fn summary_fingerprint(body: &str) -> String {
fingerprint_hash(&[body])
}
#[must_use]
pub fn composite_fingerprint(constituents: &[&str]) -> String {
let mut sorted: Vec<&str> = constituents.to_vec();
sorted.sort_unstable();
let joined = sorted.join(":");
format!("merged:{}", fingerprint_hash(&[joined.as_str()]))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fingerprint_is_stable_for_whitespace_only_snippet_changes() {
let a = finding_fingerprint(
"fallow/unused-export",
"src/a.ts",
" export const x = 1; ",
);
let b = finding_fingerprint(
"fallow/unused-export",
"src/a.ts",
"\nexport const x = 1;\n",
);
assert_eq!(a, b);
}
#[test]
fn fingerprint_parts_are_separated() {
assert_ne!(
fingerprint_hash(&["ab", "c"]),
fingerprint_hash(&["a", "bc"])
);
}
#[test]
fn composite_fingerprint_shifts_when_constituents_change() {
let three = composite_fingerprint(&["fp_a", "fp_b", "fp_c"]);
let drop_b = composite_fingerprint(&["fp_a", "fp_c"]);
let reordered = composite_fingerprint(&["fp_c", "fp_a", "fp_b"]);
assert_ne!(three, drop_b);
assert_eq!(three, reordered);
assert!(three.starts_with("merged:"));
assert_eq!(three.len(), 23);
}
#[test]
fn summary_fingerprint_shifts_when_body_changes() {
let a = summary_fingerprint("### Fallow check\n\n0 findings");
let b = summary_fingerprint("### Fallow check\n\n1 finding");
assert_ne!(a, b);
assert_eq!(a, summary_fingerprint("### Fallow check\n\n0 findings"));
assert_eq!(a.len(), 16);
}
}