Skip to main content

hermes_core/structures/
sstable_index.rs

1//! Memory-efficient SSTable index structures
2//!
3//! This module provides two approaches for memory-efficient block indexing:
4//!
5//! ## Option 1: FST-based Index (native feature)
6//! Uses a Finite State Transducer to map keys to block ordinals. The FST can be
7//! mmap'd directly without parsing into heap-allocated structures.
8//!
9//! ## Option 2: Mmap'd Raw Index
10//! Keeps the prefix-compressed block index as raw bytes and decodes entries
11//! on-demand during binary search. No heap allocation for the index.
12//!
13//! Both approaches use a compact BlockAddrStore with bitpacked offsets/lengths.
14
15use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
16use std::io;
17use std::ops::Range;
18
19use crate::directories::OwnedBytes;
20
21use super::vint::{read_vint, write_vint};
22
23/// Block address - offset and length in the data section
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct BlockAddr {
26    pub offset: u64,
27    pub length: u32,
28}
29
30impl BlockAddr {
31    pub fn byte_range(&self) -> Range<u64> {
32        self.offset..self.offset + self.length as u64
33    }
34}
35
36/// Compact storage for block addresses using delta + bitpacking
37///
38/// Memory layout:
39/// - Header: num_blocks (u32) + offset_bits (u8) + length_bits (u8)
40/// - Bitpacked data: offsets and lengths interleaved
41///
42/// Uses delta encoding for offsets (blocks are sequential) and
43/// stores lengths directly (typically similar sizes).
44#[derive(Debug)]
45pub struct BlockAddrStore {
46    num_blocks: u32,
47    offset_bits: u8,
48    length_bits: u8,
49    /// Eagerly decoded addresses for O(1) random access
50    addrs: Vec<BlockAddr>,
51}
52
53impl BlockAddrStore {
54    /// Build from a list of block addresses
55    pub fn build(addrs: &[BlockAddr]) -> io::Result<Vec<u8>> {
56        if addrs.is_empty() {
57            let mut buf = Vec::with_capacity(6);
58            buf.write_u32::<LittleEndian>(0)?;
59            buf.write_u8(0)?;
60            buf.write_u8(0)?;
61            return Ok(buf);
62        }
63
64        // Compute delta offsets and find max values for bit width
65        let mut deltas = Vec::with_capacity(addrs.len());
66        let mut prev_end: u64 = 0;
67        let mut max_delta: u64 = 0;
68        let mut max_length: u32 = 0;
69
70        for addr in addrs {
71            // Delta from end of previous block (handles gaps)
72            let delta = addr.offset.saturating_sub(prev_end);
73            deltas.push(delta);
74            max_delta = max_delta.max(delta);
75            max_length = max_length.max(addr.length);
76            prev_end = addr.offset.checked_add(addr.length as u64).ok_or_else(|| {
77                io::Error::new(io::ErrorKind::InvalidInput, "block address overflow")
78            })?;
79        }
80
81        // Compute bit widths
82        let offset_bits = if max_delta == 0 {
83            1
84        } else {
85            (64 - max_delta.leading_zeros()) as u8
86        };
87        let length_bits = if max_length == 0 {
88            1
89        } else {
90            (32 - max_length.leading_zeros()) as u8
91        };
92
93        // Calculate packed size
94        let bits_per_entry = offset_bits as usize + length_bits as usize;
95        let total_bits = bits_per_entry.checked_mul(addrs.len()).ok_or_else(|| {
96            io::Error::new(io::ErrorKind::InvalidInput, "block address table too large")
97        })?;
98        let packed_bytes = total_bits.div_ceil(8);
99
100        let mut buf = Vec::with_capacity(6 + packed_bytes);
101        buf.write_u32::<LittleEndian>(addrs.len() as u32)?;
102        buf.write_u8(offset_bits)?;
103        buf.write_u8(length_bits)?;
104
105        // Bitpack the data
106        let mut bit_writer = BitWriter::new(&mut buf);
107        for (i, addr) in addrs.iter().enumerate() {
108            bit_writer.write(deltas[i], offset_bits)?;
109            bit_writer.write(addr.length as u64, length_bits)?;
110        }
111        bit_writer.flush()?;
112
113        Ok(buf)
114    }
115
116    /// Load from raw bytes — eagerly decodes all addresses for O(1) access
117    pub fn load(data: OwnedBytes) -> io::Result<Self> {
118        if data.len() < 6 {
119            return Err(io::Error::new(
120                io::ErrorKind::InvalidData,
121                "BlockAddrStore data too short",
122            ));
123        }
124
125        let mut reader = data.as_slice();
126        let num_blocks = reader.read_u32::<LittleEndian>()?;
127        let offset_bits = reader.read_u8()?;
128        let length_bits = reader.read_u8()?;
129
130        if offset_bits > 64 || length_bits > 32 {
131            return Err(io::Error::new(
132                io::ErrorKind::InvalidData,
133                "invalid block address bit width",
134            ));
135        }
136        if num_blocks > 0 && (offset_bits == 0 || length_bits == 0) {
137            return Err(io::Error::new(
138                io::ErrorKind::InvalidData,
139                "non-empty block address table has a zero bit width",
140            ));
141        }
142
143        let bits_per_entry = offset_bits as usize + length_bits as usize;
144        let total_bits = bits_per_entry
145            .checked_mul(num_blocks as usize)
146            .ok_or_else(|| {
147                io::Error::new(io::ErrorKind::InvalidData, "block address table overflow")
148            })?;
149        let packed_len = total_bits.checked_add(7).ok_or_else(|| {
150            io::Error::new(io::ErrorKind::InvalidData, "block address table overflow")
151        })? / 8;
152        if packed_len > data.len() - 6 {
153            return Err(io::Error::new(
154                io::ErrorKind::UnexpectedEof,
155                "block address table truncated",
156            ));
157        }
158
159        // Eagerly decode all block addresses once at load time
160        let packed_data = &data.as_slice()[6..6 + packed_len];
161        let mut bit_reader = BitReader::new(packed_data);
162        let mut addrs = Vec::new();
163        addrs.try_reserve_exact(num_blocks as usize).map_err(|_| {
164            io::Error::new(io::ErrorKind::InvalidData, "block address table too large")
165        })?;
166        let mut current_offset: u64 = 0;
167
168        for _ in 0..num_blocks {
169            let delta = bit_reader.read(offset_bits)?;
170            let length = bit_reader.read(length_bits)?;
171            let offset = current_offset.checked_add(delta).ok_or_else(|| {
172                io::Error::new(io::ErrorKind::InvalidData, "block address offset overflow")
173            })?;
174            let length = u32::try_from(length).map_err(|_| {
175                io::Error::new(io::ErrorKind::InvalidData, "block length exceeds u32")
176            })?;
177            current_offset = offset.checked_add(length as u64).ok_or_else(|| {
178                io::Error::new(io::ErrorKind::InvalidData, "block address end overflow")
179            })?;
180            addrs.push(BlockAddr { offset, length });
181        }
182
183        Ok(Self {
184            num_blocks,
185            offset_bits,
186            length_bits,
187            addrs,
188        })
189    }
190
191    /// Number of blocks
192    pub fn len(&self) -> usize {
193        self.num_blocks as usize
194    }
195
196    /// Check if empty
197    pub fn is_empty(&self) -> bool {
198        self.num_blocks == 0
199    }
200
201    /// Get block address by index — O(1) from eagerly decoded array
202    #[inline]
203    pub fn get(&self, idx: usize) -> Option<BlockAddr> {
204        self.addrs.get(idx).copied()
205    }
206
207    /// Get all block addresses
208    pub fn all(&self) -> Vec<BlockAddr> {
209        self.addrs.clone()
210    }
211}
212
213/// FST-based block index (Option 1)
214///
215/// Maps keys to block ordinals using an FST. The FST bytes can be mmap'd
216/// directly without any parsing or heap allocation.
217#[cfg(feature = "fst-index")]
218pub struct FstBlockIndex {
219    fst: fst::Map<OwnedBytes>,
220    block_addrs: BlockAddrStore,
221}
222
223#[cfg(feature = "fst-index")]
224impl FstBlockIndex {
225    /// Build FST index from keys and block addresses
226    pub fn build(entries: &[(Vec<u8>, BlockAddr)]) -> io::Result<Vec<u8>> {
227        use fst::MapBuilder;
228
229        // Build FST mapping keys to block ordinals
230        let mut fst_builder = MapBuilder::memory();
231        for (i, (key, _)) in entries.iter().enumerate() {
232            fst_builder
233                .insert(key, i as u64)
234                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
235        }
236        let fst_bytes = fst_builder
237            .into_inner()
238            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
239
240        // Build block address store
241        let addrs: Vec<BlockAddr> = entries.iter().map(|(_, addr)| *addr).collect();
242        let addr_bytes = BlockAddrStore::build(&addrs)?;
243
244        // Combine: fst_len (u32) + fst_bytes + addr_bytes
245        let mut result = Vec::with_capacity(4 + fst_bytes.len() + addr_bytes.len());
246        result.write_u32::<LittleEndian>(fst_bytes.len() as u32)?;
247        result.extend_from_slice(&fst_bytes);
248        result.extend_from_slice(&addr_bytes);
249
250        Ok(result)
251    }
252
253    /// Load from raw bytes
254    pub fn load(data: OwnedBytes) -> io::Result<Self> {
255        if data.len() < 4 {
256            return Err(io::Error::new(
257                io::ErrorKind::InvalidData,
258                "FstBlockIndex data too short",
259            ));
260        }
261
262        let fst_len = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
263
264        let fst_end = 4usize.checked_add(fst_len).ok_or_else(|| {
265            io::Error::new(io::ErrorKind::InvalidData, "FstBlockIndex length overflow")
266        })?;
267        if data.len() < fst_end {
268            return Err(io::Error::new(
269                io::ErrorKind::InvalidData,
270                "FstBlockIndex FST data truncated",
271            ));
272        }
273
274        let fst_data = data.slice(4..fst_end);
275        let addr_data = data.slice(fst_end..data.len());
276
277        let fst =
278            fst::Map::new(fst_data).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
279        let block_addrs = BlockAddrStore::load(addr_data)?;
280
281        if fst.len() != block_addrs.len() {
282            return Err(io::Error::new(
283                io::ErrorKind::InvalidData,
284                "FST key count does not match block address count",
285            ));
286        }
287        use fst::Streamer;
288        let mut entries = fst.stream();
289        let mut expected_ordinal = 0u64;
290        while let Some((_key, ordinal)) = entries.next() {
291            if ordinal != expected_ordinal {
292                return Err(io::Error::new(
293                    io::ErrorKind::InvalidData,
294                    "FST block ordinals are not contiguous",
295                ));
296            }
297            expected_ordinal += 1;
298        }
299
300        Ok(Self { fst, block_addrs })
301    }
302
303    /// Look up the block index for a key
304    /// Returns the block ordinal that could contain this key.
305    /// O(key_len) via FST exact lookup + single stream step.
306    pub fn locate(&self, key: &[u8]) -> Option<usize> {
307        // Fast exact match — O(key_len), no stream allocation
308        if let Some(ordinal) = self.fst.get(key) {
309            return Some(ordinal as usize);
310        }
311
312        // Find the first block whose first_key > target (single stream step)
313        use fst::{IntoStreamer, Streamer};
314        let mut stream = self.fst.range().gt(key).into_stream();
315        match stream.next() {
316            Some((_, ordinal)) if ordinal > 0 => Some(ordinal as usize - 1),
317            Some(_) => None, // key < first block's first key
318            None => {
319                // No key > target → target is after all keys; use last block
320                let len = self.fst.len();
321                if len > 0 { Some(len - 1) } else { None }
322            }
323        }
324    }
325
326    /// Get block address by ordinal
327    pub fn get_addr(&self, ordinal: usize) -> Option<BlockAddr> {
328        self.block_addrs.get(ordinal)
329    }
330
331    /// Number of blocks
332    pub fn len(&self) -> usize {
333        self.block_addrs.len()
334    }
335
336    /// Check if empty
337    pub fn is_empty(&self) -> bool {
338        self.block_addrs.is_empty()
339    }
340
341    /// Get all block addresses
342    pub fn all_addrs(&self) -> Vec<BlockAddr> {
343        self.block_addrs.all()
344    }
345}
346
347/// Mmap'd raw block index (Option 2)
348///
349/// Keeps the prefix-compressed block index as raw bytes and decodes
350/// entries on-demand. Uses restart points every R entries for O(log N)
351/// lookup via binary search instead of O(N) linear scan.
352pub struct MmapBlockIndex {
353    data: OwnedBytes,
354    num_blocks: u32,
355    block_addrs: BlockAddrStore,
356    /// Offset where the prefix-compressed keys start
357    keys_offset: usize,
358    /// Offset where the keys section ends (restart array begins)
359    keys_end: usize,
360    /// Byte offset in data where the restart offsets array starts
361    restart_array_offset: usize,
362    /// Number of restart points
363    restart_count: usize,
364    /// Restart interval (R) — a restart point every R entries
365    restart_interval: usize,
366}
367
368/// Restart interval: store full (uncompressed) key every R entries
369const RESTART_INTERVAL: usize = 16;
370
371impl MmapBlockIndex {
372    /// Build mmap-friendly index from entries.
373    ///
374    /// Format: `num_blocks (u32) | BlockAddrStore | prefix-compressed keys
375    /// (with restart points) | restart_offsets[..] | restart_count (u32) | restart_interval (u16)`
376    pub fn build(entries: &[(Vec<u8>, BlockAddr)]) -> io::Result<Vec<u8>> {
377        if entries.is_empty() {
378            let mut buf = Vec::with_capacity(16);
379            buf.write_u32::<LittleEndian>(0)?; // num_blocks
380            buf.extend_from_slice(&BlockAddrStore::build(&[])?);
381            // Empty restart array + footer
382            buf.write_u32::<LittleEndian>(0)?; // restart_count
383            buf.write_u16::<LittleEndian>(RESTART_INTERVAL as u16)?;
384            return Ok(buf);
385        }
386
387        // Build block address store
388        let addrs: Vec<BlockAddr> = entries.iter().map(|(_, addr)| *addr).collect();
389        let addr_bytes = BlockAddrStore::build(&addrs)?;
390
391        // Build prefix-compressed keys with restart points
392        let mut keys_buf = Vec::new();
393        let mut prev_key: Vec<u8> = Vec::new();
394        let mut restart_offsets: Vec<u32> = Vec::new();
395
396        for (i, (key, _)) in entries.iter().enumerate() {
397            let is_restart = i % RESTART_INTERVAL == 0;
398
399            if is_restart {
400                restart_offsets.push(keys_buf.len() as u32);
401                // Store full key (no prefix compression)
402                write_vint(&mut keys_buf, 0)?;
403                write_vint(&mut keys_buf, key.len() as u64)?;
404                keys_buf.extend_from_slice(key);
405            } else {
406                let prefix_len = common_prefix_len(&prev_key, key);
407                let suffix = &key[prefix_len..];
408                write_vint(&mut keys_buf, prefix_len as u64)?;
409                write_vint(&mut keys_buf, suffix.len() as u64)?;
410                keys_buf.extend_from_slice(suffix);
411            }
412
413            prev_key.clear();
414            prev_key.extend_from_slice(key);
415        }
416
417        // Combine: num_blocks + addr_bytes + keys + restart_offsets + footer
418        let restart_count = restart_offsets.len();
419        let mut result =
420            Vec::with_capacity(4 + addr_bytes.len() + keys_buf.len() + restart_count * 4 + 6);
421        result.write_u32::<LittleEndian>(entries.len() as u32)?;
422        result.extend_from_slice(&addr_bytes);
423        result.extend_from_slice(&keys_buf);
424
425        // Write restart offsets array
426        for &off in &restart_offsets {
427            result.write_u32::<LittleEndian>(off)?;
428        }
429
430        // Write footer
431        result.write_u32::<LittleEndian>(restart_count as u32)?;
432        result.write_u16::<LittleEndian>(RESTART_INTERVAL as u16)?;
433
434        Ok(result)
435    }
436
437    /// Load from raw bytes
438    pub fn load(data: OwnedBytes) -> io::Result<Self> {
439        if data.len() < 16 {
440            return Err(io::Error::new(
441                io::ErrorKind::InvalidData,
442                "MmapBlockIndex data too short",
443            ));
444        }
445
446        let num_blocks = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
447
448        // Load block addresses
449        let addr_data_start = 4;
450        let remaining = data.slice(addr_data_start..data.len());
451        let block_addrs = BlockAddrStore::load(remaining.clone())?;
452
453        if block_addrs.len() != num_blocks as usize {
454            return Err(io::Error::new(
455                io::ErrorKind::InvalidData,
456                "block address count does not match key count",
457            ));
458        }
459
460        // Calculate where keys start
461        let bits_per_entry = block_addrs.offset_bits as usize + block_addrs.length_bits as usize;
462        let total_bits = bits_per_entry
463            .checked_mul(num_blocks as usize)
464            .ok_or_else(|| {
465                io::Error::new(io::ErrorKind::InvalidData, "block index size overflow")
466            })?;
467        let addr_packed_size = total_bits.checked_add(7).ok_or_else(|| {
468            io::Error::new(io::ErrorKind::InvalidData, "block index size overflow")
469        })? / 8;
470        let keys_offset = addr_data_start
471            .checked_add(6)
472            .and_then(|v| v.checked_add(addr_packed_size))
473            .ok_or_else(|| {
474                io::Error::new(io::ErrorKind::InvalidData, "block index offset overflow")
475            })?; // 6 = header of BlockAddrStore
476
477        // Read footer (last 6 bytes: restart_count u32 + restart_interval u16)
478        if data.len() < keys_offset + 6 {
479            return Err(io::Error::new(
480                io::ErrorKind::InvalidData,
481                "MmapBlockIndex missing restart footer",
482            ));
483        }
484        let footer_start = data.len() - 6;
485        let restart_count = u32::from_le_bytes([
486            data[footer_start],
487            data[footer_start + 1],
488            data[footer_start + 2],
489            data[footer_start + 3],
490        ]) as usize;
491        let restart_interval =
492            u16::from_le_bytes([data[footer_start + 4], data[footer_start + 5]]) as usize;
493
494        if restart_interval == 0 {
495            return Err(io::Error::new(
496                io::ErrorKind::InvalidData,
497                "block index restart interval is zero",
498            ));
499        }
500
501        let expected_restart_count = (num_blocks as usize).div_ceil(restart_interval);
502        if restart_count != expected_restart_count {
503            return Err(io::Error::new(
504                io::ErrorKind::InvalidData,
505                "block index restart count is inconsistent",
506            ));
507        }
508
509        // Restart offsets array: restart_count × 4 bytes, just before footer
510        let restart_bytes = restart_count.checked_mul(4).ok_or_else(|| {
511            io::Error::new(io::ErrorKind::InvalidData, "restart table size overflow")
512        })?;
513        let restart_array_offset = footer_start.checked_sub(restart_bytes).ok_or_else(|| {
514            io::Error::new(io::ErrorKind::InvalidData, "restart table out of bounds")
515        })?;
516        if restart_array_offset < keys_offset {
517            return Err(io::Error::new(
518                io::ErrorKind::InvalidData,
519                "restart table overlaps block keys",
520            ));
521        }
522
523        // Keys section spans from keys_offset to restart_array_offset
524        let keys_end = restart_array_offset;
525
526        // Validate the complete prefix-compressed key stream and all restart
527        // offsets once so the hot lookup path can remain allocation-light and
528        // infallible without trusting corrupt on-disk lengths.
529        let keys_data = &data.as_slice()[keys_offset..keys_end];
530        let mut reader = keys_data;
531        let mut current_key = Vec::new();
532        let mut previous_key: Option<Vec<u8>> = None;
533        for ordinal in 0..num_blocks as usize {
534            let entry_offset = keys_data.len() - reader.len();
535            if ordinal % restart_interval == 0 {
536                let restart_idx = ordinal / restart_interval;
537                let pos = restart_array_offset + restart_idx * 4;
538                let recorded =
539                    u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
540                        as usize;
541                if recorded != entry_offset {
542                    return Err(io::Error::new(
543                        io::ErrorKind::InvalidData,
544                        "block index restart offset is inconsistent",
545                    ));
546                }
547            }
548
549            let prefix_len = usize::try_from(read_vint(&mut reader)?).map_err(|_| {
550                io::Error::new(io::ErrorKind::InvalidData, "block key prefix is too large")
551            })?;
552            let suffix_len = usize::try_from(read_vint(&mut reader)?).map_err(|_| {
553                io::Error::new(io::ErrorKind::InvalidData, "block key suffix is too large")
554            })?;
555            if ordinal % restart_interval == 0 && prefix_len != 0 {
556                return Err(io::Error::new(
557                    io::ErrorKind::InvalidData,
558                    "block index restart key uses a prefix",
559                ));
560            }
561            if prefix_len > current_key.len() || suffix_len > reader.len() {
562                return Err(io::Error::new(
563                    io::ErrorKind::UnexpectedEof,
564                    "block index key is truncated",
565                ));
566            }
567            current_key.truncate(prefix_len);
568            current_key.extend_from_slice(&reader[..suffix_len]);
569            reader = &reader[suffix_len..];
570
571            if previous_key
572                .as_ref()
573                .is_some_and(|previous| previous.as_slice() >= current_key.as_slice())
574            {
575                return Err(io::Error::new(
576                    io::ErrorKind::InvalidData,
577                    "block index keys are not strictly increasing",
578                ));
579            }
580            previous_key = Some(current_key.clone());
581        }
582        if !reader.is_empty() {
583            return Err(io::Error::new(
584                io::ErrorKind::InvalidData,
585                "block index contains trailing key data",
586            ));
587        }
588
589        Ok(Self {
590            data,
591            num_blocks,
592            block_addrs,
593            keys_offset,
594            keys_end,
595            restart_array_offset,
596            restart_count,
597            restart_interval,
598        })
599    }
600
601    /// Read restart offset at given index directly from mmap'd data
602    #[inline]
603    fn restart_offset(&self, idx: usize) -> u32 {
604        let pos = self.restart_array_offset + idx * 4;
605        u32::from_le_bytes([
606            self.data[pos],
607            self.data[pos + 1],
608            self.data[pos + 2],
609            self.data[pos + 3],
610        ])
611    }
612
613    /// Decode the full key at a restart point (prefix_len is always 0)
614    fn decode_restart_key<'a>(&self, keys_data: &'a [u8], restart_idx: usize) -> &'a [u8] {
615        let offset = self.restart_offset(restart_idx) as usize;
616        let mut reader = &keys_data[offset..];
617
618        let prefix_len = read_vint(&mut reader).unwrap_or(0) as usize;
619        debug_assert_eq!(prefix_len, 0, "restart point should have prefix_len=0");
620        let suffix_len = read_vint(&mut reader).unwrap_or(0) as usize;
621
622        // reader now points to the suffix bytes
623        &reader[..suffix_len]
624    }
625
626    /// O(log(N/R) + R) lookup using binary search on restart points, then
627    /// linear scan with prefix decompression within the interval.
628    pub fn locate(&self, target: &[u8]) -> Option<usize> {
629        if self.num_blocks == 0 {
630            return None;
631        }
632
633        let keys_data = &self.data.as_slice()[self.keys_offset..self.keys_end];
634
635        // Binary search on restart points to find the interval
636        let mut lo = 0usize;
637        let mut hi = self.restart_count;
638
639        while lo < hi {
640            let mid = lo + (hi - lo) / 2;
641            let key = self.decode_restart_key(keys_data, mid);
642            match key.cmp(target) {
643                std::cmp::Ordering::Equal => {
644                    return Some(mid * self.restart_interval);
645                }
646                std::cmp::Ordering::Less => lo = mid + 1,
647                std::cmp::Ordering::Greater => hi = mid,
648            }
649        }
650
651        // lo is the first restart point whose key > target (or restart_count)
652        // Search in the interval starting at restart (lo - 1), or 0 if lo == 0
653        if lo == 0 {
654            // target < first restart key — might be before all keys
655            // but we still need to scan from the beginning
656        }
657
658        let restart_idx = if lo > 0 { lo - 1 } else { 0 };
659        let start_ordinal = restart_idx * self.restart_interval;
660        let end_ordinal = if restart_idx + 1 < self.restart_count {
661            (restart_idx + 1) * self.restart_interval
662        } else {
663            self.num_blocks as usize
664        };
665
666        // Linear scan from restart point through at most R entries
667        let scan_offset = self.restart_offset(restart_idx) as usize;
668        let mut reader = &keys_data[scan_offset..];
669        let mut current_key = Vec::new();
670        let mut last_le_block: Option<usize> = None;
671
672        for i in start_ordinal..end_ordinal {
673            let prefix_len = match read_vint(&mut reader) {
674                Ok(v) => v as usize,
675                Err(_) => break,
676            };
677            let suffix_len = match read_vint(&mut reader) {
678                Ok(v) => v as usize,
679                Err(_) => break,
680            };
681
682            current_key.truncate(prefix_len);
683            if suffix_len > reader.len() {
684                break;
685            }
686            current_key.extend_from_slice(&reader[..suffix_len]);
687            reader = &reader[suffix_len..];
688
689            match current_key.as_slice().cmp(target) {
690                std::cmp::Ordering::Equal => return Some(i),
691                std::cmp::Ordering::Less => last_le_block = Some(i),
692                std::cmp::Ordering::Greater => return last_le_block,
693            }
694        }
695
696        last_le_block
697    }
698
699    /// Get block address by ordinal
700    pub fn get_addr(&self, ordinal: usize) -> Option<BlockAddr> {
701        self.block_addrs.get(ordinal)
702    }
703
704    /// Number of blocks
705    pub fn len(&self) -> usize {
706        self.num_blocks as usize
707    }
708
709    /// Check if empty
710    pub fn is_empty(&self) -> bool {
711        self.num_blocks == 0
712    }
713
714    /// Get all block addresses
715    pub fn all_addrs(&self) -> Vec<BlockAddr> {
716        self.block_addrs.all()
717    }
718
719    /// Decode all keys (for debugging/merging)
720    pub fn all_keys(&self) -> Vec<Vec<u8>> {
721        let mut result = Vec::with_capacity(self.num_blocks as usize);
722        let keys_data = &self.data.as_slice()[self.keys_offset..self.keys_end];
723        let mut reader = keys_data;
724        let mut current_key = Vec::new();
725
726        for _ in 0..self.num_blocks {
727            let prefix_len = match read_vint(&mut reader) {
728                Ok(v) => v as usize,
729                Err(_) => break,
730            };
731            let suffix_len = match read_vint(&mut reader) {
732                Ok(v) => v as usize,
733                Err(_) => break,
734            };
735
736            current_key.truncate(prefix_len);
737            if suffix_len > reader.len() {
738                break;
739            }
740            current_key.extend_from_slice(&reader[..suffix_len]);
741            reader = &reader[suffix_len..];
742
743            result.push(current_key.clone());
744        }
745
746        result
747    }
748}
749
750/// Unified block index that can use either FST or mmap'd raw index
751pub enum BlockIndex {
752    #[cfg(feature = "fst-index")]
753    Fst(FstBlockIndex),
754    Mmap(MmapBlockIndex),
755}
756
757impl BlockIndex {
758    /// Locate the block that could contain the key
759    pub fn locate(&self, key: &[u8]) -> Option<usize> {
760        match self {
761            #[cfg(feature = "fst-index")]
762            BlockIndex::Fst(idx) => idx.locate(key),
763            BlockIndex::Mmap(idx) => idx.locate(key),
764        }
765    }
766
767    /// Get block address by ordinal
768    pub fn get_addr(&self, ordinal: usize) -> Option<BlockAddr> {
769        match self {
770            #[cfg(feature = "fst-index")]
771            BlockIndex::Fst(idx) => idx.get_addr(ordinal),
772            BlockIndex::Mmap(idx) => idx.get_addr(ordinal),
773        }
774    }
775
776    /// Number of blocks
777    pub fn len(&self) -> usize {
778        match self {
779            #[cfg(feature = "fst-index")]
780            BlockIndex::Fst(idx) => idx.len(),
781            BlockIndex::Mmap(idx) => idx.len(),
782        }
783    }
784
785    /// Check if empty
786    pub fn is_empty(&self) -> bool {
787        self.len() == 0
788    }
789
790    /// Get all block addresses
791    pub fn all_addrs(&self) -> Vec<BlockAddr> {
792        match self {
793            #[cfg(feature = "fst-index")]
794            BlockIndex::Fst(idx) => idx.all_addrs(),
795            BlockIndex::Mmap(idx) => idx.all_addrs(),
796        }
797    }
798}
799
800// ============================================================================
801// Helper functions
802// ============================================================================
803
804fn common_prefix_len(a: &[u8], b: &[u8]) -> usize {
805    a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()
806}
807
808/// Simple bit writer for packing
809struct BitWriter<'a> {
810    output: &'a mut Vec<u8>,
811    buffer: u64,
812    bits_in_buffer: u8,
813}
814
815impl<'a> BitWriter<'a> {
816    fn new(output: &'a mut Vec<u8>) -> Self {
817        Self {
818            output,
819            buffer: 0,
820            bits_in_buffer: 0,
821        }
822    }
823
824    fn write(&mut self, value: u64, num_bits: u8) -> io::Result<()> {
825        debug_assert!(num_bits <= 64);
826
827        self.buffer |= value << self.bits_in_buffer;
828        self.bits_in_buffer += num_bits;
829
830        while self.bits_in_buffer >= 8 {
831            self.output.push(self.buffer as u8);
832            self.buffer >>= 8;
833            self.bits_in_buffer -= 8;
834        }
835
836        Ok(())
837    }
838
839    fn flush(&mut self) -> io::Result<()> {
840        if self.bits_in_buffer > 0 {
841            self.output.push(self.buffer as u8);
842            self.buffer = 0;
843            self.bits_in_buffer = 0;
844        }
845        Ok(())
846    }
847}
848
849/// Simple bit reader for unpacking
850struct BitReader<'a> {
851    data: &'a [u8],
852    byte_pos: usize,
853    bit_pos: u8,
854}
855
856impl<'a> BitReader<'a> {
857    fn new(data: &'a [u8]) -> Self {
858        Self {
859            data,
860            byte_pos: 0,
861            bit_pos: 0,
862        }
863    }
864
865    fn read(&mut self, num_bits: u8) -> io::Result<u64> {
866        if num_bits == 0 {
867            return Ok(0);
868        }
869
870        let mut result: u64 = 0;
871        let mut bits_read: u8 = 0;
872
873        while bits_read < num_bits {
874            if self.byte_pos >= self.data.len() {
875                return Err(io::Error::new(
876                    io::ErrorKind::UnexpectedEof,
877                    "Not enough bits",
878                ));
879            }
880
881            let bits_available = 8 - self.bit_pos;
882            let bits_to_read = (num_bits - bits_read).min(bits_available);
883            // Handle edge case where bits_to_read == 8 to avoid overflow
884            let mask = if bits_to_read >= 8 {
885                0xFF
886            } else {
887                (1u8 << bits_to_read) - 1
888            };
889            let bits = (self.data[self.byte_pos] >> self.bit_pos) & mask;
890
891            result |= (bits as u64) << bits_read;
892            bits_read += bits_to_read;
893            self.bit_pos += bits_to_read;
894
895            if self.bit_pos >= 8 {
896                self.byte_pos += 1;
897                self.bit_pos = 0;
898            }
899        }
900
901        Ok(result)
902    }
903}
904
905#[cfg(test)]
906mod tests {
907    use super::*;
908
909    #[test]
910    fn test_block_addr_store_roundtrip() {
911        let addrs = vec![
912            BlockAddr {
913                offset: 0,
914                length: 1000,
915            },
916            BlockAddr {
917                offset: 1000,
918                length: 1500,
919            },
920            BlockAddr {
921                offset: 2500,
922                length: 800,
923            },
924            BlockAddr {
925                offset: 3300,
926                length: 2000,
927            },
928        ];
929
930        let bytes = BlockAddrStore::build(&addrs).unwrap();
931        let store = BlockAddrStore::load(OwnedBytes::new(bytes)).unwrap();
932
933        assert_eq!(store.len(), 4);
934        for (i, expected) in addrs.iter().enumerate() {
935            let actual = store.get(i).unwrap();
936            assert_eq!(actual.offset, expected.offset, "offset mismatch at {}", i);
937            assert_eq!(actual.length, expected.length, "length mismatch at {}", i);
938        }
939    }
940
941    #[test]
942    fn test_block_addr_store_empty() {
943        let bytes = BlockAddrStore::build(&[]).unwrap();
944        let store = BlockAddrStore::load(OwnedBytes::new(bytes)).unwrap();
945        assert_eq!(store.len(), 0);
946        assert!(store.get(0).is_none());
947    }
948
949    #[test]
950    fn test_block_addr_store_rejects_truncated_packed_data() {
951        let bytes = vec![1, 0, 0, 0, 1, 1];
952        assert!(BlockAddrStore::load(OwnedBytes::new(bytes)).is_err());
953    }
954
955    #[test]
956    fn test_mmap_index_rejects_restart_table_underflow() {
957        let entries = vec![(
958            b"key".to_vec(),
959            BlockAddr {
960                offset: 0,
961                length: 1,
962            },
963        )];
964        let mut bytes = MmapBlockIndex::build(&entries).unwrap();
965        let footer = bytes.len() - 6;
966        bytes[footer..footer + 4].copy_from_slice(&u32::MAX.to_le_bytes());
967        assert!(MmapBlockIndex::load(OwnedBytes::new(bytes)).is_err());
968    }
969
970    #[test]
971    fn test_mmap_block_index_roundtrip() {
972        let entries = vec![
973            (
974                b"aaa".to_vec(),
975                BlockAddr {
976                    offset: 0,
977                    length: 100,
978                },
979            ),
980            (
981                b"bbb".to_vec(),
982                BlockAddr {
983                    offset: 100,
984                    length: 150,
985                },
986            ),
987            (
988                b"ccc".to_vec(),
989                BlockAddr {
990                    offset: 250,
991                    length: 200,
992                },
993            ),
994        ];
995
996        let bytes = MmapBlockIndex::build(&entries).unwrap();
997        let index = MmapBlockIndex::load(OwnedBytes::new(bytes)).unwrap();
998
999        assert_eq!(index.len(), 3);
1000
1001        // Test locate
1002        assert_eq!(index.locate(b"aaa"), Some(0));
1003        assert_eq!(index.locate(b"bbb"), Some(1));
1004        assert_eq!(index.locate(b"ccc"), Some(2));
1005        assert_eq!(index.locate(b"aab"), Some(0)); // Between aaa and bbb
1006        assert_eq!(index.locate(b"ddd"), Some(2)); // After all keys
1007        assert_eq!(index.locate(b"000"), None); // Before all keys
1008    }
1009
1010    #[cfg(feature = "fst-index")]
1011    #[test]
1012    fn test_fst_block_index_roundtrip() {
1013        let entries = vec![
1014            (
1015                b"aaa".to_vec(),
1016                BlockAddr {
1017                    offset: 0,
1018                    length: 100,
1019                },
1020            ),
1021            (
1022                b"bbb".to_vec(),
1023                BlockAddr {
1024                    offset: 100,
1025                    length: 150,
1026                },
1027            ),
1028            (
1029                b"ccc".to_vec(),
1030                BlockAddr {
1031                    offset: 250,
1032                    length: 200,
1033                },
1034            ),
1035        ];
1036
1037        let bytes = FstBlockIndex::build(&entries).unwrap();
1038        let index = FstBlockIndex::load(OwnedBytes::new(bytes)).unwrap();
1039
1040        assert_eq!(index.len(), 3);
1041
1042        // Test locate
1043        assert_eq!(index.locate(b"aaa"), Some(0));
1044        assert_eq!(index.locate(b"bbb"), Some(1));
1045        assert_eq!(index.locate(b"ccc"), Some(2));
1046        assert_eq!(index.locate(b"aab"), Some(0)); // Between aaa and bbb
1047        assert_eq!(index.locate(b"ddd"), Some(2)); // After all keys
1048    }
1049
1050    #[test]
1051    fn test_bit_writer_reader() {
1052        let mut buf = Vec::new();
1053        let mut writer = BitWriter::new(&mut buf);
1054
1055        writer.write(5, 3).unwrap(); // 101
1056        writer.write(3, 2).unwrap(); // 11
1057        writer.write(15, 4).unwrap(); // 1111
1058        writer.flush().unwrap();
1059
1060        let mut reader = BitReader::new(&buf);
1061        assert_eq!(reader.read(3).unwrap(), 5);
1062        assert_eq!(reader.read(2).unwrap(), 3);
1063        assert_eq!(reader.read(4).unwrap(), 15);
1064    }
1065}