1use rustc_hash::FxHashSet;
29use vyre_primitives::hash::fnv1a::{fnv1a64_initial_state, fnv1a64_update_byte};
30const ALPHA: f64 = 1.23;
33
34const BUCKET_DIVISOR: usize = 4;
37
38const MAX_DISPLACEMENT_TRIES: u32 = 1_000_000;
42
43const MAX_SALT_RETRIES: u32 = 16;
47
48#[derive(Debug, Clone, Default)]
50pub struct PerfectHash {
51 seed1: u64,
52 seed2: u64,
53 displacement: Vec<u32>,
54 key_hashes: Vec<u64>,
55 values: Vec<u32>,
56 len: usize,
57}
58
59impl PerfectHash {
60 pub fn lookup(&self, key: &str) -> Option<u32> {
64 if self.displacement.is_empty() {
65 return None;
66 }
67 let bytes = key.as_bytes();
68 let h1 = hash_with_seed(bytes, self.seed1) as usize;
69 let bucket = h1 % self.displacement.len();
70 let disp = self.displacement[bucket];
71 let h2 = hash_with_seed(bytes, self.seed2.wrapping_add(disp as u64));
72 let slot = (h2 as usize) % self.key_hashes.len();
73 if self.key_hashes[slot] == hash_verify(bytes) {
74 Some(self.values[slot])
75 } else {
76 None
77 }
78 }
79
80 pub fn len(&self) -> usize {
82 self.len
83 }
84
85 pub fn is_empty(&self) -> bool {
87 self.len == 0
88 }
89
90 pub fn slots(&self) -> usize {
92 self.key_hashes.len()
93 }
94
95 pub fn displacement(&self) -> &[u32] {
97 &self.displacement
98 }
99
100 pub fn key_hashes(&self) -> &[u64] {
102 &self.key_hashes
103 }
104
105 pub fn values(&self) -> &[u32] {
107 &self.values
108 }
109
110 pub fn seeds(&self) -> (u64, u64) {
113 (self.seed1, self.seed2)
114 }
115}
116
117pub fn build_chd<I, S>(entries: I) -> PerfectHash
122where
123 I: IntoIterator<Item = (S, u32)>,
124 S: AsRef<str>,
125{
126 try_build_chd(entries).unwrap_or_default()
127}
128
129pub fn try_build_chd<I, S>(entries: I) -> Result<PerfectHash, BuildError>
131where
132 I: IntoIterator<Item = (S, u32)>,
133 S: AsRef<str>,
134{
135 let pairs: Vec<(String, u32)> = entries
136 .into_iter()
137 .map(|(k, v)| (k.as_ref().to_owned(), v))
138 .collect();
139
140 if pairs.is_empty() {
141 return Ok(PerfectHash::default());
142 }
143
144 let mut seen = FxHashSet::default();
146 seen.reserve(pairs.len());
147 for (k, _) in &pairs {
148 if !seen.insert(k.as_str()) {
149 return Err(BuildError::DuplicateKey(k.clone()));
150 }
151 }
152
153 for salt in 0..MAX_SALT_RETRIES {
154 if let Some(ph) = try_build_with_salt(&pairs, salt as u64) {
155 return Ok(ph);
156 }
157 }
158 Err(BuildError::ConstructionFailed(pairs.len()))
159}
160
161fn try_build_with_salt(pairs: &[(String, u32)], salt: u64) -> Option<PerfectHash> {
162 let n = pairs.len();
163 let table_size = (((n as f64) * ALPHA).ceil() as usize) | 1;
164 let n_buckets = ((n / BUCKET_DIVISOR).max(1)) | 1;
165
166 let seed1 = salt.wrapping_mul(0x9E37_79B9_7F4A_7C15).wrapping_add(1);
167 let seed2 = salt
168 .wrapping_mul(0xBF58_476D_1CE4_E5B9)
169 .wrapping_add(0xDEAD_BEEF_CAFE_BABE);
170
171 let mut bucket_offsets = vec![0usize; n_buckets + 1];
173 for (k, _) in pairs {
174 let h = hash_with_seed(k.as_bytes(), seed1) as usize;
175 bucket_offsets[h % n_buckets + 1] += 1;
176 }
177 for i in 1..bucket_offsets.len() {
178 bucket_offsets[i] += bucket_offsets[i - 1];
179 }
180 let mut bucket_cursor = bucket_offsets[..n_buckets].to_vec();
181 let mut bucket_items = vec![0usize; n];
182 for (i, (k, _)) in pairs.iter().enumerate() {
183 let h = hash_with_seed(k.as_bytes(), seed1) as usize;
184 let bucket = h % n_buckets;
185 let slot = bucket_cursor[bucket];
186 bucket_items[slot] = i;
187 bucket_cursor[bucket] += 1;
188 }
189
190 let mut bucket_order: Vec<usize> = (0..n_buckets).collect();
192 bucket_order.sort_by_key(|&b| std::cmp::Reverse(bucket_offsets[b + 1] - bucket_offsets[b]));
193
194 let mut displacement = vec![0_u32; n_buckets];
195 let mut key_hashes = vec![0_u64; table_size];
196 let mut values = vec![0_u32; table_size];
197 let mut occupied = vec![false; table_size];
198 let mut candidate_scratch = vec![false; table_size];
199 let mut candidate_slots = Vec::new();
200
201 'bucket: for b in bucket_order {
202 let bucket = &bucket_items[bucket_offsets[b]..bucket_offsets[b + 1]];
203 if bucket.is_empty() {
204 continue;
205 }
206 for disp in 0..MAX_DISPLACEMENT_TRIES {
215 candidate_slots.clear();
216 candidate_slots.reserve(bucket.len());
217 let mut ok = true;
218 for &ki in bucket {
219 let key = pairs[ki].0.as_bytes();
220 let h2 = hash_with_seed(key, seed2.wrapping_add(disp as u64));
221 let slot = (h2 as usize) % table_size;
222 if occupied[slot] || candidate_scratch[slot] {
223 ok = false;
224 break;
225 }
226 candidate_scratch[slot] = true;
227 candidate_slots.push(slot);
228 }
229 for slot in &candidate_slots {
232 candidate_scratch[*slot] = false;
233 }
234 if ok {
235 displacement[b] = disp;
236 for (ki, slot) in bucket.iter().zip(candidate_slots.iter()) {
237 let key = pairs[*ki].0.as_bytes();
238 key_hashes[*slot] = hash_verify(key);
239 values[*slot] = pairs[*ki].1;
240 occupied[*slot] = true;
241 }
242 continue 'bucket;
243 }
244 }
245 return None;
246 }
247
248 Some(PerfectHash {
249 seed1,
250 seed2,
251 displacement,
252 key_hashes,
253 values,
254 len: n,
255 })
256}
257
258#[derive(Debug, thiserror::Error)]
260pub enum BuildError {
261 #[error("duplicate key: {0:?}")]
263 DuplicateKey(String),
264 #[error("CHD construction failed for {0} keys after all salt retries")]
266 ConstructionFailed(usize),
267}
268
269#[inline]
272fn hash_with_seed(data: &[u8], seed: u64) -> u64 {
273 let mut h = seed ^ fnv1a64_initial_state();
274 for &b in data {
275 h = fnv1a64_update_byte(h, b);
276 }
277 h
278}
279
280#[inline]
285fn hash_verify(data: &[u8]) -> u64 {
286 let mut h: u64 = 0x517c_c1b7_2722_0a95;
287 for &b in data {
288 h = h.rotate_left(5) ^ (b as u64);
289 h = h.wrapping_mul(0x9e37_79b9_7f4a_7c15);
290 }
291 h ^= h >> 33;
293 h = h.wrapping_mul(0xff51_afd7_ed55_8ccd);
294 h ^= h >> 33;
295 h = h.wrapping_mul(0xc4ce_b9fe_1a85_ec53);
296 h ^= h >> 33;
297 h
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn empty_input_roundtrips() {
306 let ph = build_chd(Vec::<(&str, u32)>::new());
307 assert_eq!(ph.len(), 0);
308 assert!(ph.is_empty());
309 assert_eq!(ph.lookup("any"), None);
310 }
311
312 #[test]
313 fn single_entry() {
314 let ph = build_chd([("hello", 42_u32)]);
315 assert_eq!(ph.len(), 1);
316 assert_eq!(ph.lookup("hello"), Some(42));
317 assert_eq!(ph.lookup("world"), None);
318 }
319
320 #[test]
321 fn ten_keys_roundtrip() {
322 let entries: Vec<(String, u32)> = (0..10).map(|i| (format!("key_{i}"), i as u32)).collect();
323 let ph = build_chd(entries.clone());
324 assert_eq!(ph.len(), 10);
325 for (k, v) in &entries {
326 assert_eq!(ph.lookup(k), Some(*v), "key={k:?}");
327 }
328 assert_eq!(ph.lookup("unknown"), None);
329 }
330
331 #[test]
332 fn thousand_keys_roundtrip() {
333 let entries: Vec<(String, u32)> = (0..1000)
334 .map(|i| (format!("func_{i:04}"), i as u32))
335 .collect();
336 let ph = build_chd(entries.clone());
337 assert_eq!(ph.len(), 1000);
338 for (k, v) in &entries {
339 assert_eq!(ph.lookup(k), Some(*v), "key={k:?}");
340 }
341 for i in 1000..1100 {
342 assert_eq!(ph.lookup(&format!("func_{i:04}")), None);
343 }
344 }
345
346 #[test]
347 fn duplicate_keys_rejected() {
348 let err = try_build_chd([("dup", 1_u32), ("dup", 2_u32)]).unwrap_err();
349 assert!(matches!(err, BuildError::DuplicateKey(k) if k == "dup"));
350 }
351
352 #[test]
353 fn infallible_builder_returns_empty_on_duplicates() {
354 let ph = build_chd([("dup", 1_u32), ("dup", 2_u32)]);
357 assert_eq!(ph.len(), 0);
358 assert!(ph.is_empty());
359 assert_eq!(ph.lookup("dup"), None);
360 }
361
362 #[test]
363 fn value_preserved_bitwise() {
364 let entries: Vec<(String, u32)> = (0..100)
365 .map(|i| (format!("k_{i}"), (i as u32).wrapping_mul(0xDEAD_BEEF)))
366 .collect();
367 let ph = build_chd(entries.clone());
368 for (k, v) in entries {
369 assert_eq!(ph.lookup(&k), Some(v));
370 }
371 }
372
373 #[test]
374 fn unicode_keys_work() {
375 let entries = vec![
376 ("naïve".to_string(), 1_u32),
377 ("咖啡".to_string(), 2),
378 ("über".to_string(), 3),
379 ("🎉".to_string(), 4),
380 ("test".to_string(), 5),
381 ];
382 let ph = build_chd(entries.clone());
383 for (k, v) in entries {
384 assert_eq!(ph.lookup(&k), Some(v));
385 }
386 }
387
388 #[test]
389 fn space_overhead_under_30_percent() {
390 let entries: Vec<(String, u32)> = (0..500).map(|i| (format!("k_{i}"), i as u32)).collect();
391 let n = entries.len();
392 let ph = build_chd(entries);
393 let ratio = ph.slots() as f64 / n as f64;
394 assert!(ratio < 1.30, "slots/len ratio {ratio} > 1.30 budget");
395 }
396
397 #[test]
398 fn seeds_and_tables_are_non_trivial_after_build() {
399 let entries: Vec<(String, u32)> = (0..50).map(|i| (format!("k_{i}"), i as u32)).collect();
400 let ph = build_chd(entries);
401 let (s1, s2) = ph.seeds();
402 assert_ne!(s1, 0);
403 assert_ne!(s2, 0);
404 assert!(!ph.displacement().is_empty());
405 assert!(!ph.key_hashes().is_empty());
406 assert!(!ph.values().is_empty());
407 }
408
409 #[test]
410 fn negative_lookups_are_rejected_by_verify_hash() {
411 let entries: Vec<(String, u32)> = (0..200).map(|i| (format!("k_{i}"), i as u32)).collect();
412 let ph = build_chd(entries);
413 for i in 1000..1500 {
415 assert_eq!(ph.lookup(&format!("q_{i}")), None, "false hit on q_{i}");
416 }
417 }
418
419 #[test]
420 fn hash_with_seed_is_deterministic() {
421 assert_eq!(hash_with_seed(b"hello", 42), hash_with_seed(b"hello", 42));
422 assert_ne!(hash_with_seed(b"hello", 42), hash_with_seed(b"hello", 43));
423 assert_ne!(hash_with_seed(b"hello", 42), hash_with_seed(b"world", 42));
424 }
425
426 #[test]
427 fn hash_verify_differs_from_seeded_hash() {
428 let key = b"hello";
429 assert_ne!(hash_with_seed(key, 0), hash_verify(key));
430 }
431
432 #[test]
433 fn real_label_family_names_build_and_lookup() {
434 let funcs = [
437 "fopen",
438 "open",
439 "openat",
440 "CreateFileA",
441 "CreateFileW",
442 "std::fs::OpenOptions::open",
443 "std::fs::File::open",
444 "std::fs::File::create",
445 "tokio::fs::File::open",
446 "tokio::fs::File::create",
447 "rocket::response::NamedFile::open",
448 ];
449 let entries: Vec<(String, u32)> = funcs
450 .iter()
451 .enumerate()
452 .map(|(i, f)| (f.to_string(), i as u32))
453 .collect();
454
455 let ph = build_chd(entries.clone());
456 for (k, v) in entries {
457 assert_eq!(ph.lookup(&k), Some(v));
458 }
459 assert_eq!(ph.lookup("not_in_family"), None);
460 assert_eq!(ph.lookup("malloc"), None);
461 }
462
463 use proptest::prelude::*;
464
465 proptest! {
466 #[test]
467 fn proptest_roundtrip_random_keys(
468 entries in prop::collection::hash_map(
469 "[a-zA-Z0-9_]{1,32}",
470 0u32..10000u32,
471 1..256usize,
472 ),
473 ) {
474 let vec: Vec<(String, u32)> = entries.into_iter().collect();
475 let ph = build_chd(vec.clone());
476 for (k, v) in &vec {
477 prop_assert_eq!(ph.lookup(k), Some(*v), "key={}", k);
478 }
479 }
480
481 #[test]
482 fn proptest_negative_lookups_miss(
483 entries in prop::collection::vec(("[a-z]{1,16}", 0u32..100u32), 1..100usize),
484 queries in prop::collection::vec("[a-z]{1,16}", 1..50usize),
485 ) {
486 let deduped: std::collections::HashMap<String, u32> = entries.into_iter().collect();
487 prop_assume!(!deduped.is_empty());
488 let vec: Vec<(String, u32)> = deduped.clone().into_iter().collect();
489 let ph = build_chd(vec);
490 let key_set: std::collections::HashSet<&str> = deduped.keys().map(|k| k.as_str()).collect();
491 for q in &queries {
492 if key_set.contains(q.as_str()) {
493 continue;
494 }
495 prop_assert_eq!(ph.lookup(q), None, "false hit on {}", q);
496 }
497 }
498
499 #[test]
500 fn proptest_space_overhead_under_35_percent(
501 entries in prop::collection::vec(("[a-zA-Z0-9_]{1,32}", 0u32..10000u32), 10..500usize),
502 ) {
503 let deduped: std::collections::HashMap<String, u32> = entries.into_iter().collect();
504 prop_assume!(deduped.len() >= 10);
505 let vec: Vec<(String, u32)> = deduped.into_iter().collect();
506 let ph = build_chd(vec.clone());
507 let n = vec.len();
508 let ratio = ph.slots() as f64 / n as f64;
509 let budget = if n < 20 { 1.5 } else { 1.35 };
511 prop_assert!(ratio < budget, "slots/len ratio {ratio} > {budget} budget for n={n}");
512 }
513 }
514}