1use xxhash_rust::xxh3::xxh3_64;
4
5pub const HASH_BASE: u64 = 0x9e3779b97f4a7c15;
7
8pub fn token_hash(kind: u8, value: &str) -> u64 {
11 xxh3_64(value.as_bytes()) ^ (kind as u64)
12}
13
14#[inline]
18pub fn hash_token(kind_discriminant: u8, value: &str, ignore_case: bool) -> u64 {
19 if ignore_case {
20 xxh3_64(value.to_lowercase().as_bytes()) ^ (kind_discriminant as u64)
21 } else {
22 xxh3_64(value.as_bytes()) ^ (kind_discriminant as u64)
23 }
24}
25
26pub fn snippet_pair_hash(a: &str, b: &str) -> u64 {
36 let a = strip_cr(a);
37 let b = strip_cr(b);
38 let (first, second) = if a <= b { (&a, &b) } else { (&b, &a) };
39 let mut buf = Vec::with_capacity(first.len() + second.len() + 1);
40 buf.extend_from_slice(first.as_bytes());
41 buf.push(0); buf.extend_from_slice(second.as_bytes());
43 xxh3_64(&buf)
44}
45
46fn strip_cr(s: &str) -> std::borrow::Cow<'_, str> {
47 if s.contains('\r') {
48 std::borrow::Cow::Owned(s.replace('\r', ""))
49 } else {
50 std::borrow::Cow::Borrowed(s)
51 }
52}
53
54pub fn hash_window(hashes: &[u64]) -> u64 {
58 hashes
59 .iter()
60 .fold(0u64, |acc, &h| acc.wrapping_mul(HASH_BASE).wrapping_add(h))
61}
62
63pub fn roll(current: u64, outgoing: u64, incoming: u64, window_power: u64) -> u64 {
76 current
77 .wrapping_sub(outgoing.wrapping_mul(window_power))
78 .wrapping_mul(HASH_BASE)
79 .wrapping_add(incoming)
80}
81
82pub fn base_pow(n: usize) -> u64 {
85 let mut result = 1u64;
86 for _ in 0..n {
87 result = result.wrapping_mul(HASH_BASE);
88 }
89 result
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn token_hash_is_deterministic() {
98 let h1 = token_hash(1, "function");
99 let h2 = token_hash(1, "function");
100 assert_eq!(h1, h2);
101 }
102
103 #[test]
104 fn token_hash_differs_by_kind() {
105 let h1 = token_hash(1, "x");
106 let h2 = token_hash(2, "x");
107 assert_ne!(h1, h2);
108 }
109
110 #[test]
111 fn snippet_pair_hash_is_order_insensitive() {
112 let h1 = snippet_pair_hash("fn a() {}", "fn b() {}");
113 let h2 = snippet_pair_hash("fn b() {}", "fn a() {}");
114 assert_eq!(h1, h2, "swapping fragments must not change the hash");
115 }
116
117 #[test]
118 fn snippet_pair_hash_is_line_ending_agnostic() {
119 let lf = snippet_pair_hash("fn a() {\n}\n", "fn b() {\n}\n");
120 let crlf = snippet_pair_hash("fn a() {\r\n}\r\n", "fn b() {\r\n}\r\n");
121 let mixed = snippet_pair_hash("fn a() {\r\n}\r\n", "fn b() {\n}\n");
122 assert_eq!(lf, crlf, "CRLF and LF content must fingerprint identically");
123 assert_eq!(lf, mixed, "mixed line endings must fingerprint identically");
124 }
125
126 #[test]
127 fn snippet_pair_hash_separator_prevents_boundary_collisions() {
128 let h1 = snippet_pair_hash("ab", "c");
129 let h2 = snippet_pair_hash("a", "bc");
130 assert_ne!(h1, h2, "concatenation boundary must be unambiguous");
131 }
132
133 #[test]
134 fn hash_window_single_element() {
135 let h = token_hash(0, "a");
136 assert_eq!(hash_window(&[h]), h);
138 }
139
140 #[test]
141 fn roll_matches_naive_recompute() {
142 let a = token_hash(0, "a");
143 let b = token_hash(0, "b");
144 let c = token_hash(0, "c");
145 let d = token_hash(0, "d");
146
147 let initial = hash_window(&[a, b, c]);
148 let wp = base_pow(3 - 1);
149 let rolled = roll(initial, a, d, wp);
150 let naive = hash_window(&[b, c, d]);
151 assert_eq!(rolled, naive, "rolled hash must match naive recomputation");
152 }
153
154 #[test]
155 fn roll_window_of_one() {
156 let a = token_hash(0, "hello");
157 let b = token_hash(0, "world");
158 let initial = hash_window(&[a]);
159 let wp = base_pow(1 - 1); let rolled = roll(initial, a, b, wp);
161 let naive = hash_window(&[b]);
162 assert_eq!(rolled, naive);
163 }
164
165 #[test]
166 fn hash_window_empty_is_zero() {
167 assert_eq!(hash_window(&[]), 0u64);
168 }
169
170 #[test]
171 fn hash_token_case_insensitive_matches_different_case() {
172 let h1 = hash_token(1, "Function", true);
173 let h2 = hash_token(1, "function", true);
174 assert_eq!(h1, h2, "ignore_case=true must fold case before hashing");
175 }
176
177 #[test]
178 fn hash_token_case_sensitive_differs() {
179 let h1 = hash_token(1, "Function", false);
180 let h2 = hash_token(1, "function", false);
181 assert_ne!(h1, h2, "ignore_case=false must not fold case");
182 }
183
184 #[test]
185 fn hash_token_no_ignore_case_matches_token_hash() {
186 let h1 = hash_token(2, "hello", false);
187 let h2 = token_hash(2, "hello");
188 assert_eq!(
189 h1, h2,
190 "hash_token(ignore_case=false) must match token_hash"
191 );
192 }
193}