Skip to main content

fallow_cli/report/ci/
fingerprint.rs

1/// Fingerprint key used in SARIF partialFingerprints and other CI formats.
2pub const FINGERPRINT_KEY: &str = "tools.fallow.fingerprint/v1";
3
4/// Conventional SARIF key consumed by GitHub Code Scanning's alert-correlation
5/// engine. Emitted in addition to `FINGERPRINT_KEY` so GHAS deduplicates fallow
6/// alerts across pushes.
7pub const GHAS_FINGERPRINT_KEY: &str = "primaryLocationLineHash/v1";
8
9#[must_use]
10pub fn normalize_snippet(snippet: &str) -> String {
11    snippet
12        .lines()
13        .map(str::trim)
14        .filter(|line| !line.is_empty())
15        .collect::<Vec<_>>()
16        .join("\n")
17}
18
19/// Compute a deterministic fingerprint hash from key fields.
20///
21/// Uses FNV-1a (64-bit) for guaranteed cross-version stability.
22/// `DefaultHasher` is explicitly not specified across Rust versions.
23#[must_use]
24pub fn fingerprint_hash(parts: &[&str]) -> String {
25    let mut hash: u64 = 0xcbf2_9ce4_8422_2325; // FNV offset basis
26    for part in parts {
27        for byte in part.bytes() {
28            hash ^= u64::from(byte);
29            hash = hash.wrapping_mul(0x0100_0000_01b3); // FNV prime
30        }
31        // Separator between parts to avoid "ab"+"c" == "a"+"bc"
32        hash ^= 0xff;
33        hash = hash.wrapping_mul(0x0100_0000_01b3);
34    }
35    format!("{hash:016x}")
36}
37
38#[must_use]
39pub fn finding_fingerprint(rule_id: &str, path: &str, snippet: &str) -> String {
40    let normalized = normalize_snippet(snippet);
41    fingerprint_hash(&[rule_id, path, &normalized])
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47
48    #[test]
49    fn fingerprint_is_stable_for_whitespace_only_snippet_changes() {
50        let a = finding_fingerprint(
51            "fallow/unused-export",
52            "src/a.ts",
53            "  export const x = 1;  ",
54        );
55        let b = finding_fingerprint(
56            "fallow/unused-export",
57            "src/a.ts",
58            "\nexport const x = 1;\n",
59        );
60        assert_eq!(a, b);
61    }
62
63    #[test]
64    fn fingerprint_parts_are_separated() {
65        assert_ne!(
66            fingerprint_hash(&["ab", "c"]),
67            fingerprint_hash(&["a", "bc"])
68        );
69    }
70}