fallow_cli/report/ci/
fingerprint.rs1pub const FINGERPRINT_KEY: &str = "tools.fallow.fingerprint/v1";
3
4pub const GHAS_FINGERPRINT_KEY: &str = "primaryLocationLineHash/v1";
6
7#[must_use]
8pub fn normalize_snippet(snippet: &str) -> String {
9 snippet
10 .lines()
11 .map(str::trim)
12 .filter(|line| !line.is_empty())
13 .collect::<Vec<_>>()
14 .join("\n")
15}
16
17#[must_use]
19pub fn fingerprint_hash(parts: &[&str]) -> String {
20 let mut hash: u64 = 0xcbf2_9ce4_8422_2325; for part in parts {
22 for byte in part.bytes() {
23 hash ^= u64::from(byte);
24 hash = hash.wrapping_mul(0x0100_0000_01b3); }
26 hash ^= 0xff;
27 hash = hash.wrapping_mul(0x0100_0000_01b3);
28 }
29 format!("{hash:016x}")
30}
31
32#[must_use]
33pub fn finding_fingerprint(rule_id: &str, path: &str, snippet: &str) -> String {
34 let normalized = normalize_snippet(snippet);
35 fingerprint_hash(&[rule_id, path, &normalized])
36}
37
38#[must_use]
40pub fn summary_fingerprint(body: &str) -> String {
41 fingerprint_hash(&[body])
42}
43
44#[must_use]
46pub fn composite_fingerprint(constituents: &[&str]) -> String {
47 let mut sorted: Vec<&str> = constituents.to_vec();
48 sorted.sort_unstable();
49 let joined = sorted.join(":");
50 format!("merged:{}", fingerprint_hash(&[joined.as_str()]))
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56
57 #[test]
58 fn fingerprint_is_stable_for_whitespace_only_snippet_changes() {
59 let a = finding_fingerprint(
60 "fallow/unused-export",
61 "src/a.ts",
62 " export const x = 1; ",
63 );
64 let b = finding_fingerprint(
65 "fallow/unused-export",
66 "src/a.ts",
67 "\nexport const x = 1;\n",
68 );
69 assert_eq!(a, b);
70 }
71
72 #[test]
73 fn fingerprint_parts_are_separated() {
74 assert_ne!(
75 fingerprint_hash(&["ab", "c"]),
76 fingerprint_hash(&["a", "bc"])
77 );
78 }
79
80 #[test]
81 fn composite_fingerprint_shifts_when_constituents_change() {
82 let three = composite_fingerprint(&["fp_a", "fp_b", "fp_c"]);
83 let drop_b = composite_fingerprint(&["fp_a", "fp_c"]);
84 let reordered = composite_fingerprint(&["fp_c", "fp_a", "fp_b"]);
85 assert_ne!(three, drop_b);
86 assert_eq!(three, reordered);
87 assert!(three.starts_with("merged:"));
88 assert_eq!(three.len(), 23);
89 }
90
91 #[test]
92 fn summary_fingerprint_shifts_when_body_changes() {
93 let a = summary_fingerprint("### Fallow check\n\n0 findings");
94 let b = summary_fingerprint("### Fallow check\n\n1 finding");
95 assert_ne!(a, b);
96 assert_eq!(a, summary_fingerprint("### Fallow check\n\n0 findings"));
97 assert_eq!(a.len(), 16);
98 }
99}