Skip to main content

erigon_seg/
seg.rs

1//! Reader for a `seg`-compressed `.kv` file: a stream of *words* encoded with a
2//! Huffman-coded pattern dictionary plus literal runs.
3//!
4//! [`Seg`] owns the memory map and the pattern/position dictionaries; a [`Getter`] is a
5//! cheap cursor that decompresses words on demand. Getters are not thread-safe, so each
6//! thread makes its own from a shared `&Seg`.
7//!
8//! ## Header
9//!
10//! The file may begin with a small header before the seg body:
11//! * **v0** — no header; the body (the big-endian `words_count`) starts at offset 0.
12//! * **v1** — a `[version=1, feature_flags]` pair; if the `PAGE_COMPRESSION` flag is set,
13//!   one more byte (values-per-page) follows.
14//!
15//! An optional, out-of-band *metadata* blob (length-prefixed `u32`) may follow the
16//! header for some file kinds; pass `has_metadata = true` to [`Seg::open_with`] if so.
17//! Domain `.kv` files do not use it.
18
19use std::path::Path;
20use std::sync::Arc;
21
22use memmap2::Mmap;
23
24use crate::error::{Error, Result};
25use crate::util::{Advice, advise_mmap, mmap_file};
26use crate::varint::uvarint;
27
28const FORMAT_V1: u8 = 1;
29const FLAG_PAGE_COMPRESSION: u8 = 0b001;
30/// Minimum size of the seg *body* (words_count + empty_words_count + dict_size headers).
31const BODY_MIN: usize = 32;
32
33/// Options controlling how a `.kv` is opened.
34#[derive(Debug, Clone, Copy, Default)]
35pub struct OpenOptions {
36    /// Whether the file carries an out-of-band metadata blob after the header. This is
37    /// not auto-detectable from the file; it is a property of the file kind.
38    pub has_metadata: bool,
39}
40
41// ---------------------------------------------------------------- pattern dictionary
42
43/// One Huffman codeword in the pattern dictionary. `len == 0` marks an inner node whose
44/// `ptr` is the deeper table.
45struct Codeword {
46    pattern: Vec<u8>,
47    ptr: Option<Box<PatternTable>>,
48    len: u8,
49}
50
51/// A condensed pattern table indexed directly by `code` (erigon always condenses, since
52/// every table's `bit_len` is ≤ 9).
53struct PatternTable {
54    patterns: Vec<Option<Arc<Codeword>>>,
55    bit_len: i32,
56}
57
58impl PatternTable {
59    fn new(bit_len: i32) -> PatternTable {
60        PatternTable {
61            patterns: vec![None; 1usize << bit_len.max(0)],
62            bit_len,
63        }
64    }
65    fn insert(&mut self, cw: Arc<Codeword>, code: u16) {
66        let code_step: u16 = 1 << cw.len;
67        let code_from = code;
68        let mut code_to = code.wrapping_add(code_step);
69        if self.bit_len != cw.len as i32 && cw.len > 0 {
70            code_to = code_from | (1u16 << self.bit_len);
71        }
72        let mut c = code_from;
73        while c < code_to {
74            self.patterns[c as usize] = Some(cw.clone());
75            c = c.wrapping_add(code_step);
76        }
77    }
78}
79
80#[allow(clippy::too_many_arguments)]
81fn build_pattern_table(
82    table: &mut PatternTable,
83    depths: &[u64],
84    patterns: &[&[u8]],
85    code: u16,
86    bits: i32,
87    depth: u64,
88    max_depth: u64,
89) -> usize {
90    if depths.is_empty() {
91        return 0;
92    }
93    if depth == depths[0] {
94        let cw = Arc::new(Codeword {
95            pattern: patterns[0].to_vec(),
96            ptr: None,
97            len: bits as u8,
98        });
99        table.insert(cw, code);
100        return 1;
101    }
102    if bits == 9 {
103        let bl = if max_depth > 9 { 9 } else { max_depth as i32 };
104        let mut nested = PatternTable::new(bl);
105        let consumed = build_pattern_table(&mut nested, depths, patterns, 0, 0, depth, max_depth);
106        let cw = Arc::new(Codeword {
107            pattern: Vec::new(),
108            ptr: Some(Box::new(nested)),
109            len: 0,
110        });
111        table.insert(cw, code);
112        return consumed;
113    }
114    if max_depth == 0 {
115        return 0;
116    }
117    let b0 = build_pattern_table(
118        table,
119        depths,
120        patterns,
121        code,
122        bits + 1,
123        depth + 1,
124        max_depth - 1,
125    );
126    let b1 = build_pattern_table(
127        table,
128        &depths[b0..],
129        &patterns[b0..],
130        (1u16 << bits) | code,
131        bits + 1,
132        depth + 1,
133        max_depth - 1,
134    );
135    b0 + b1
136}
137
138// ---------------------------------------------------------------- position dictionary
139
140struct PosTable {
141    pos: Vec<u64>,
142    lens: Vec<u8>,
143    ptrs: Vec<Option<Box<PosTable>>>,
144    bit_len: i32,
145}
146
147impl PosTable {
148    fn new(bit_len: i32) -> PosTable {
149        let n = 1usize << bit_len.max(0);
150        PosTable {
151            pos: vec![0; n],
152            lens: vec![0; n],
153            ptrs: (0..n).map(|_| None).collect(),
154            bit_len,
155        }
156    }
157}
158
159#[allow(clippy::too_many_arguments)]
160fn build_pos_table(
161    depths: &[u64],
162    poss: &[u64],
163    table: &mut PosTable,
164    code: u16,
165    bits: i32,
166    depth: u64,
167    max_depth: u64,
168) -> usize {
169    if depths.is_empty() {
170        return 0;
171    }
172    if depth == depths[0] {
173        let p = poss[0];
174        if table.bit_len == bits {
175            table.pos[code as usize] = p;
176            table.lens[code as usize] = bits as u8;
177            table.ptrs[code as usize] = None;
178        } else {
179            let code_step = 1u16 << bits;
180            let code_to = code | (1u16 << table.bit_len);
181            let mut c = code;
182            while c < code_to {
183                table.pos[c as usize] = p;
184                table.lens[c as usize] = bits as u8;
185                table.ptrs[c as usize] = None;
186                c += code_step;
187            }
188        }
189        return 1;
190    }
191    if bits == 9 {
192        let bl = if max_depth > 9 { 9 } else { max_depth as i32 };
193        let mut nested = PosTable::new(bl);
194        let consumed = build_pos_table(depths, poss, &mut nested, 0, 0, depth, max_depth);
195        table.pos[code as usize] = 0;
196        table.lens[code as usize] = 0;
197        table.ptrs[code as usize] = Some(Box::new(nested));
198        return consumed;
199    }
200    if max_depth == 0 {
201        return 0;
202    }
203    let b0 = build_pos_table(
204        depths,
205        poss,
206        table,
207        code,
208        bits + 1,
209        depth + 1,
210        max_depth - 1,
211    );
212    let b1 = build_pos_table(
213        &depths[b0..],
214        &poss[b0..],
215        table,
216        (1u16 << bits) | code,
217        bits + 1,
218        depth + 1,
219        max_depth - 1,
220    );
221    b0 + b1
222}
223
224// ---------------------------------------------------------------- Seg
225
226/// A `seg`-compressed file (`.kv`). Owns its mmap and dictionaries; create a [`Getter`]
227/// to read words.
228pub struct Seg {
229    mmap: Mmap,
230    dict: Option<PatternTable>,
231    pos_dict: Option<PosTable>,
232    words_start: usize,
233    words_count: u64,
234    empty_words_count: u64,
235    version: u8,
236    page_values_count: u8,
237}
238
239impl Seg {
240    /// Open a `.kv` with default options (no out-of-band metadata).
241    pub fn open(path: impl AsRef<Path>) -> Result<Seg> {
242        Seg::open_with(path, OpenOptions::default())
243    }
244
245    /// Open a `.kv` with explicit [`OpenOptions`].
246    pub fn open_with(path: impl AsRef<Path>, opts: OpenOptions) -> Result<Seg> {
247        let mmap = mmap_file(path.as_ref())?;
248        Seg::from_mmap(mmap, opts)
249            .ok_or_else(|| Error::format(format!("{}: invalid .kv file", path.as_ref().display())))
250    }
251
252    fn from_mmap(mmap: Mmap, opts: OpenOptions) -> Option<Seg> {
253        let data: &[u8] = &mmap;
254
255        // ---- header: detect v0 vs v1, optional page byte, optional metadata ----
256        let mut off = 0usize;
257        let version = *data.first()?;
258        let mut page_values_count = 0u8;
259        if version == FORMAT_V1 {
260            // [version, feature_flags]
261            let flags = *data.get(1)?;
262            off = 2;
263            if flags & FLAG_PAGE_COMPRESSION != 0 {
264                page_values_count = *data.get(off)?;
265                off += 1;
266            }
267        }
268        if opts.has_metadata {
269            let lb = data.get(off..off + 4)?;
270            let metadata_len = u32::from_be_bytes(lb.try_into().unwrap()) as usize;
271            off += 4 + metadata_len;
272        }
273
274        let body = data.get(off..)?;
275        if body.len() < BODY_MIN {
276            return None;
277        }
278
279        let words_count = u64::from_be_bytes(body[0..8].try_into().unwrap());
280        let empty_words_count = u64::from_be_bytes(body[8..16].try_into().unwrap());
281        let dict_size = u64::from_be_bytes(body[16..24].try_into().unwrap()) as usize;
282        let mut pos = 24usize;
283        if pos + dict_size > body.len() {
284            return None;
285        }
286
287        // ---- pattern dictionary: (depth, pattern-bytes) pairs ----
288        let dd = &body[pos..pos + dict_size];
289        let mut depths: Vec<u64> = Vec::new();
290        let mut patterns: Vec<&[u8]> = Vec::new();
291        let mut max_depth = 0u64;
292        let mut dp = 0usize;
293        while dp < dict_size {
294            let (depth, ns) = uvarint(&dd[dp..]);
295            if ns == 0 || depth > 50 {
296                return None;
297            }
298            depths.push(depth);
299            max_depth = max_depth.max(depth);
300            dp += ns;
301            let (l, n) = uvarint(&dd[dp..]);
302            if n == 0 {
303                return None;
304            }
305            dp += n;
306            let l = l as usize;
307            if dp + l > dict_size {
308                return None;
309            }
310            patterns.push(&dd[dp..dp + l]);
311            dp += l;
312        }
313        let dict = if dict_size > 0 {
314            let bit_len = if max_depth > 9 { 9 } else { max_depth as i32 };
315            let mut t = PatternTable::new(bit_len);
316            build_pattern_table(&mut t, &depths, &patterns, 0, 0, 0, max_depth);
317            Some(t)
318        } else {
319            None
320        };
321
322        pos += dict_size;
323        if pos + 8 > body.len() {
324            return None;
325        }
326        let pos_dict_size = u64::from_be_bytes(body[pos..pos + 8].try_into().unwrap()) as usize;
327        pos += 8;
328        if pos + pos_dict_size > body.len() {
329            return None;
330        }
331
332        // ---- position dictionary: (depth, position) pairs ----
333        let pd = &body[pos..pos + pos_dict_size];
334        let mut pdepths: Vec<u64> = Vec::new();
335        let mut poss: Vec<u64> = Vec::new();
336        let mut pmax_depth = 0u64;
337        let mut dp = 0usize;
338        while dp < pos_dict_size {
339            let (depth, ns) = uvarint(&pd[dp..]);
340            if ns == 0 || depth > 50 {
341                return None;
342            }
343            pdepths.push(depth);
344            pmax_depth = pmax_depth.max(depth);
345            dp += ns;
346            let (p, n) = uvarint(&pd[dp..]);
347            if n == 0 {
348                return None;
349            }
350            dp += n;
351            poss.push(p);
352        }
353        let pos_dict = if pos_dict_size > 0 {
354            let bit_len = if pmax_depth > 9 { 9 } else { pmax_depth as i32 };
355            let mut t = PosTable::new(bit_len);
356            build_pos_table(&pdepths, &poss, &mut t, 0, 0, 0, pmax_depth);
357            Some(t)
358        } else {
359            None
360        };
361
362        // `words_start` is absolute (from file start), so getter offsets — which are
363        // relative to the words region — index `&mmap[words_start..]`.
364        let words_start = off + pos + pos_dict_size;
365        Some(Seg {
366            mmap,
367            dict,
368            pos_dict,
369            words_start,
370            words_count,
371            empty_words_count,
372            version,
373            page_values_count,
374        })
375    }
376
377    /// Number of words in the file (for a domain `.kv`, `2 × key_count`).
378    pub fn words_count(&self) -> u64 {
379        self.words_count
380    }
381
382    /// Number of empty (zero-length) words.
383    pub fn empty_words_count(&self) -> u64 {
384        self.empty_words_count
385    }
386
387    /// On-disk header version (`0` = legacy, `1` = versioned header).
388    pub fn version(&self) -> u8 {
389        self.version
390    }
391
392    /// Values-per-page if page-level compression is enabled, else `0`. Page reassembly
393    /// is not performed by this low-level reader; words are returned as encoded.
394    pub fn page_values_count(&self) -> u8 {
395        self.page_values_count
396    }
397
398    /// Advise the kernel that this `.kv` is read in random order (point lookups). See
399    /// [`KvReader::advise_random`](crate::KvReader::advise_random).
400    pub fn advise_random(&self) -> std::io::Result<()> {
401        advise_mmap(&self.mmap, Advice::Random)
402    }
403
404    /// Advise the kernel that this `.kv` is read front to back, so read-ahead helps.
405    /// Worth setting before an [`iter`](crate::KvReader::iter) or a merge.
406    pub fn advise_sequential(&self) -> std::io::Result<()> {
407        advise_mmap(&self.mmap, Advice::Sequential)
408    }
409
410    /// Total mapped file length in bytes — a safe over-estimate of the maximum word
411    /// offset, suitable as the `max_offset` bound when building a `.bt` index.
412    pub fn len(&self) -> usize {
413        self.mmap.len()
414    }
415
416    /// Whether the file maps to zero bytes.
417    pub fn is_empty(&self) -> bool {
418        self.mmap.is_empty()
419    }
420
421    /// Create a cursor positioned at the start of the words region.
422    pub fn getter(&self) -> Getter<'_> {
423        Getter {
424            pattern_dict: self.dict.as_ref(),
425            pos_dict: self.pos_dict.as_ref(),
426            data: &self.mmap[self.words_start..],
427            data_p: 0,
428            data_bit: 0,
429            spans: Vec::new(),
430        }
431    }
432}
433
434// ---------------------------------------------------------------- Getter
435
436/// A cursor over a [`Seg`]'s words. Cheap to create; not thread-safe, so each thread
437/// makes its own from a shared `&Seg`.
438pub struct Getter<'a> {
439    pattern_dict: Option<&'a PatternTable>,
440    pos_dict: Option<&'a PosTable>,
441    data: &'a [u8],
442    data_p: u64,
443    data_bit: i32,
444    /// Scratch reused across [`Getter::next`] calls: `(buf_pos, pattern_len)` for each
445    /// pattern laid down by the current word. Starts unallocated, so a getter that only
446    /// ever calls [`skip`](Getter::skip) — or decodes words that use no patterns — never
447    /// allocates.
448    spans: Vec<(usize, usize)>,
449}
450
451impl<'a> Getter<'a> {
452    /// Position the cursor at `offset` (a value from the `.bt` index, or 0 for the
453    /// first word).
454    #[inline]
455    pub fn reset(&mut self, offset: u64) {
456        self.data_p = offset;
457        self.data_bit = 0;
458    }
459
460    /// Whether another word is available at the current position.
461    #[inline]
462    pub fn has_next(&self) -> bool {
463        (self.data_p as usize) < self.data.len()
464    }
465
466    /// Byte offset of the cursor within the words region. Valid (byte-aligned) at word
467    /// boundaries — i.e. immediately after [`reset`](Self::reset), [`next`](Self::next),
468    /// or [`skip`](Self::skip). This is the value the `.bt` index stores per key.
469    #[inline]
470    pub fn offset(&self) -> u64 {
471        self.data_p
472    }
473
474    /// Advance past the word at the current offset without materializing it, returning
475    /// its length. Port of erigon `Getter.Skip`; far cheaper than [`next`](Self::next)
476    /// when only positions/offsets are needed (e.g. building an index).
477    pub fn skip(&mut self) -> u64 {
478        let word_len = self.next_pos(true).wrapping_sub(1); // -1: 0 is the terminator
479        if word_len == 0 {
480            if self.data_bit > 0 {
481                self.data_p += 1;
482                self.data_bit = 0;
483            }
484            return 0;
485        }
486        let mut add = 0u64;
487        let mut buf_pos: usize = 0;
488        let mut last_uncovered: usize = 0;
489        loop {
490            let pos = self.next_pos(false);
491            if pos == 0 {
492                break;
493            }
494            buf_pos += pos as usize - 1;
495            if buf_pos > last_uncovered {
496                add += (buf_pos - last_uncovered) as u64;
497            }
498            last_uncovered = buf_pos + self.next_pattern().len();
499        }
500        if self.data_bit > 0 {
501            self.data_p += 1;
502            self.data_bit = 0;
503        }
504        if word_len as usize > last_uncovered {
505            add += word_len - last_uncovered as u64;
506        }
507        self.data_p += add;
508        word_len
509    }
510
511    fn next_pos(&mut self, clean: bool) -> u64 {
512        if clean && self.data_bit > 0 {
513            self.data_p += 1;
514            self.data_bit = 0;
515        }
516        let mut table = self.pos_dict.expect("position dict missing");
517        if table.bit_len == 0 {
518            return table.pos[0];
519        }
520        let data = self.data;
521        let data_len = data.len();
522        let mut pos = 0u64;
523        loop {
524            let mut code = (data[self.data_p as usize] as u16) >> self.data_bit;
525            if 8 - self.data_bit < table.bit_len && (self.data_p as usize) + 1 < data_len {
526                code |= (data[self.data_p as usize + 1] as u16) << (8 - self.data_bit);
527            }
528            code &= (1u16 << table.bit_len) - 1;
529            let l = table.lens[code as usize];
530            if l == 0 {
531                table = table.ptrs[code as usize]
532                    .as_deref()
533                    .expect("pos inner node missing");
534                self.data_bit += 9;
535            } else {
536                self.data_bit += l as i32;
537                pos = table.pos[code as usize];
538            }
539            self.data_p += (self.data_bit / 8) as u64;
540            self.data_bit %= 8;
541            if l != 0 {
542                break;
543            }
544        }
545        pos
546    }
547
548    fn next_pattern(&mut self) -> &'a [u8] {
549        let mut table = self.pattern_dict.expect("pattern dict missing");
550        if table.bit_len == 0 {
551            return &table.patterns[0]
552                .as_ref()
553                .expect("empty pattern table")
554                .pattern;
555        }
556        let data = self.data;
557        let data_len = data.len();
558        loop {
559            let mut code = (data[self.data_p as usize] as u16) >> self.data_bit;
560            if 8 - self.data_bit < table.bit_len && (self.data_p as usize) + 1 < data_len {
561                code |= (data[self.data_p as usize + 1] as u16) << (8 - self.data_bit);
562            }
563            code &= (1u16 << table.bit_len) - 1;
564            let cw = table.patterns[code as usize]
565                .as_ref()
566                .expect("missing codeword");
567            let l = cw.len;
568            if l == 0 {
569                table = cw.ptr.as_deref().expect("pattern inner node missing");
570                self.data_bit += 9;
571                self.data_p += (self.data_bit / 8) as u64;
572                self.data_bit %= 8;
573            } else {
574                self.data_bit += l as i32;
575                self.data_p += (self.data_bit / 8) as u64;
576                self.data_bit %= 8;
577                return &cw.pattern;
578            }
579        }
580    }
581
582    /// Decompress the word at the current offset, advancing past it. Port of erigon
583    /// `Getter.Next(nil)`.
584    ///
585    /// Named `next` to mirror erigon; the cursor is deliberately not an `Iterator`,
586    /// since a domain `.kv` interleaves key and value words that callers consume in
587    /// pairs.
588    ///
589    /// Erigon decodes the Huffman position stream twice — once to lay down the patterns
590    /// and again, after rewinding, to find the gaps between them. The second walk exists
591    /// only to recover the pattern positions, so instead we record them the first time
592    /// through and replay them from `spans`; the literal pass becomes plain `memcpy`.
593    /// Output is byte-identical.
594    #[allow(clippy::should_implement_trait)]
595    pub fn next(&mut self) -> Vec<u8> {
596        let word_len = self.next_pos(true).wrapping_sub(1); // -1: 0 is the terminator
597        if word_len == 0 {
598            if self.data_bit > 0 {
599                self.data_p += 1;
600                self.data_bit = 0;
601            }
602            return Vec::new();
603        }
604        let word_len = word_len as usize;
605        let mut buf = vec![0u8; word_len];
606
607        // Single decode pass: lay down the patterns and record where each one landed.
608        self.spans.clear();
609        let mut buf_pos: usize = 0;
610        loop {
611            let pos = self.next_pos(false);
612            if pos == 0 {
613                break;
614            }
615            buf_pos += pos as usize - 1;
616            let pt = self.next_pattern();
617            // The untruncated length is what the literal pass needs, even if the pattern
618            // overruns the buffer and the copy below is clipped.
619            self.spans.push((buf_pos, pt.len()));
620            if buf_pos < buf.len() {
621                let n = pt.len().min(buf.len() - buf_pos);
622                buf[buf_pos..buf_pos + n].copy_from_slice(&pt[..n]);
623            }
624        }
625        if self.data_bit > 0 {
626            self.data_p += 1;
627            self.data_bit = 0;
628        }
629        // The literals begin right after the bit stream we just consumed.
630        let mut post_loop_pos = self.data_p;
631
632        // Fill the gaps between patterns with literal bytes, replaying `spans`.
633        let data = self.data;
634        let mut last_uncovered: usize = 0;
635        for &(bp, plen) in &self.spans {
636            if bp > last_uncovered {
637                let dif = bp - last_uncovered;
638                buf[last_uncovered..bp]
639                    .copy_from_slice(&data[post_loop_pos as usize..post_loop_pos as usize + dif]);
640                post_loop_pos += dif as u64;
641            }
642            last_uncovered = bp + plen;
643        }
644        if word_len > last_uncovered {
645            let dif = word_len - last_uncovered;
646            buf[last_uncovered..last_uncovered + dif]
647                .copy_from_slice(&data[post_loop_pos as usize..post_loop_pos as usize + dif]);
648            post_loop_pos += dif as u64;
649        }
650        self.data_p = post_loop_pos;
651        self.data_bit = 0;
652        buf
653    }
654}