Skip to main content

protocache_core/
perfect_hash.rs

1//! Perfect-hash surface matching `perfect_hash.h`.
2
3pub use crate::serialize::{build_perfect_hash_index, build_perfect_hash_index_with_positions};
4
5use crate::hash::hash128;
6use crate::utils::{CorruptionKind, ReadError};
7
8#[derive(Clone, Copy, Debug)]
9/// Borrowed reader for a serialized ProtoCache perfect-hash index.
10pub struct PerfectHashView<'a> {
11    data: &'a [u8],
12    bitmap: &'a [u8],
13    table: &'a [u8],
14    section: usize,
15    section_magic: u64,
16    len: usize,
17    table_width: usize,
18}
19
20impl<'a> PerfectHashView<'a> {
21    /// Parses and bounds-checks an index from the beginning of `data`.
22    #[inline(always)]
23    pub fn new(data: &'a [u8]) -> Result<Self, ReadError> {
24        let header = read_u32_le(data).ok_or(ReadError::new(CorruptionKind::Truncated))?;
25        let len = (header & 0x0fff_ffff) as usize;
26        if len <= 1 {
27            let data = data
28                .get(..4)
29                .ok_or(ReadError::new(CorruptionKind::Truncated))?;
30            return Ok(Self {
31                data,
32                bitmap: &data[..0],
33                table: &data[..0],
34                section: 0,
35                section_magic: 0,
36                len,
37                table_width: 0,
38            });
39        }
40
41        let (section, bitmap_size, table_width, bytes) = perfect_hash_layout(len);
42        let data = data
43            .get(..bytes)
44            .ok_or(ReadError::new(CorruptionKind::Truncated))?;
45        let bitmap = data
46            .get(8..8 + bitmap_size)
47            .ok_or(ReadError::new(CorruptionKind::Truncated))?;
48        let table = data
49            .get(8 + bitmap_size..)
50            .ok_or(ReadError::new(CorruptionKind::Truncated))?;
51
52        Ok(Self {
53            data,
54            bitmap,
55            table,
56            section,
57            section_magic: fast_mod_magic(section as u32),
58            len,
59            table_width,
60        })
61    }
62
63    /// Returns the number of bytes occupied by the parsed index.
64    #[inline(always)]
65    pub fn data_size(&self) -> usize {
66        self.data.len()
67    }
68
69    /// Returns the number of indexed keys.
70    #[inline(always)]
71    pub fn len(&self) -> usize {
72        self.len
73    }
74
75    #[inline(always)]
76    pub fn is_empty(&self) -> bool {
77        self.len == 0
78    }
79
80    /// Returns the candidate storage position for `key`.
81    ///
82    /// The index stores no key bytes, so callers must compare the key stored at
83    /// the returned position before treating the lookup as a match.
84    #[inline(always)]
85    pub fn locate(&self, key: &[u8]) -> Option<usize> {
86        if self.len == 0 {
87            return None;
88        }
89        if self.len == 1 {
90            return Some(0);
91        }
92
93        let seed = read_u32_le(self.data.get(4..8)?)? as u64;
94        let code = hash128(key, seed);
95        let slots = [
96            fast_mod_u32(code[0], self.section as u32, self.section_magic) as usize,
97            fast_mod_u32(code[1], self.section as u32, self.section_magic) as usize + self.section,
98            fast_mod_u32(code[2], self.section as u32, self.section_magic) as usize
99                + self.section * 2,
100        ];
101        self.locate_slots(slots)
102    }
103
104    #[inline(always)]
105    fn locate_slots(&self, slots: [usize; 3]) -> Option<usize> {
106        let m = bit2(self.bitmap, slots[0])?
107            + bit2(self.bitmap, slots[1])?
108            + bit2(self.bitmap, slots[2])?;
109        let slot = slots[(m % 3) as usize];
110        let a = slot >> 5;
111        let b = slot & 31;
112
113        let off = read_offset(self.table, self.table_width, a)?;
114
115        let block = read_u64_le(self.bitmap.get(a * 8..a * 8 + 8)?)? | (u64::MAX << (b << 1));
116        Some(off + count_valid_slot(block))
117    }
118}
119
120#[inline(always)]
121fn perfect_hash_layout(len: usize) -> (usize, usize, usize, usize) {
122    let section = ((len * 105).saturating_add(255) / 256).max(10);
123    let bitmap_size = ((section * 3 + 31) & !31) / 4;
124    let table_width = if len > u16::MAX as usize {
125        4
126    } else if len > u8::MAX as usize {
127        2
128    } else if len > 24 {
129        1
130    } else {
131        0
132    };
133    let bytes = 8 + bitmap_size + bitmap_size / 8 * table_width;
134    (section, bitmap_size, table_width, bytes)
135}
136
137#[inline(always)]
138fn read_offset(table: &[u8], table_width: usize, index: usize) -> Option<usize> {
139    match table_width {
140        4 => {
141            let start = index * 4;
142            Some(read_u32_le(table.get(start..start + 4)?)? as usize)
143        }
144        2 => {
145            let start = index * 2;
146            Some(u16::from_le_bytes(table.get(start..start + 2)?.try_into().ok()?) as usize)
147        }
148        1 => Some(*table.get(index)? as usize),
149        0 => Some(0),
150        _ => None,
151    }
152}
153
154#[inline(always)]
155fn read_u32_le(bytes: &[u8]) -> Option<u32> {
156    Some(u32::from_le_bytes(bytes.get(..4)?.try_into().ok()?))
157}
158
159#[inline(always)]
160fn read_u64_le(bytes: &[u8]) -> Option<u64> {
161    Some(u64::from_le_bytes(bytes.get(..8)?.try_into().ok()?))
162}
163
164#[inline(always)]
165fn bit2(vec: &[u8], pos: usize) -> Option<u32> {
166    Some(((vec.get(pos >> 2)? >> ((pos & 3) << 1)) & 3) as u32)
167}
168
169#[inline(always)]
170fn fast_mod_magic(divisor: u32) -> u64 {
171    u64::MAX / divisor as u64 + 1
172}
173
174#[inline(always)]
175fn fast_mod_u32(value: u32, divisor: u32, magic: u64) -> u32 {
176    let low = magic.wrapping_mul(value as u64);
177    (((low as u128) * divisor as u128) >> 64) as u32
178}
179
180#[inline(always)]
181fn count_valid_slot(v: u64) -> usize {
182    let invalid = ((v & 0x5555_5555_5555_5555) & (v >> 1)).count_ones() as usize;
183    32 - invalid
184}
185
186#[cfg(test)]
187mod tests {
188    use std::collections::HashSet;
189
190    use super::{PerfectHashView, build_perfect_hash_index, fast_mod_magic, fast_mod_u32};
191
192    fn run_case(size: usize) {
193        let keys = (0..size).map(|i| i.to_string()).collect::<Vec<_>>();
194        let index = build_perfect_hash_index(&keys).expect("perfect hash index should build");
195        let view = PerfectHashView::new(&index).expect("perfect hash view should decode");
196
197        assert_eq!(view.len(), keys.len());
198        assert_eq!(view.data_size(), index.len());
199
200        let mut seen = HashSet::new();
201        for key in &keys {
202            let pos = view
203                .locate(key.as_bytes())
204                .expect("every inserted key should resolve");
205            assert!(pos < keys.len());
206            assert!(seen.insert(pos), "duplicate slot {pos} for key {key}");
207        }
208    }
209
210    #[test]
211    fn tiny_cases_match_cpp_coverage() {
212        for size in [0usize, 1, 2, 24] {
213            run_case(size);
214        }
215    }
216
217    #[test]
218    fn small_cases_match_cpp_coverage() {
219        for size in [200usize, 255, 1000] {
220            run_case(size);
221        }
222    }
223
224    #[test]
225    fn big_cases_match_cpp_coverage() {
226        for size in [65_535usize, 100_000] {
227            run_case(size);
228        }
229    }
230
231    #[test]
232    fn rejects_truncated_indices() {
233        assert!(PerfectHashView::new(&[]).is_err());
234        assert!(PerfectHashView::new(&[0, 0, 0]).is_err());
235
236        let index = build_perfect_hash_index(&["a", "b", "c"]).unwrap();
237        for len in 0..index.len() {
238            assert!(PerfectHashView::new(&index[..len]).is_err());
239        }
240    }
241
242    #[test]
243    fn fast_mod_matches_remainder() {
244        let divisors = [10u32, 11, 17, 31, 45, 255, 256, 1000, 65535, 100_000];
245        let values = [
246            0u32,
247            1,
248            2,
249            3,
250            7,
251            31,
252            32,
253            255,
254            256,
255            1024,
256            65_535,
257            1_000_000,
258            u32::MAX - 1,
259            u32::MAX,
260        ];
261
262        for divisor in divisors {
263            let magic = fast_mod_magic(divisor);
264            for value in values {
265                assert_eq!(fast_mod_u32(value, divisor, magic), value % divisor);
266            }
267        }
268    }
269}