fallow_cli/report/ci/
fingerprint.rs1pub const FINGERPRINT_KEY: &str = "tools.fallow.fingerprint/v1";
3
4pub 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#[must_use]
24pub fn fingerprint_hash(parts: &[&str]) -> String {
25 let mut hash: u64 = 0xcbf2_9ce4_8422_2325; for part in parts {
27 for byte in part.bytes() {
28 hash ^= u64::from(byte);
29 hash = hash.wrapping_mul(0x0100_0000_01b3); }
31 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}