Skip to main content

rete_core/v06/
mod.rs

1//! Format generation `0x06` (SPEC.md §4): everything a reader needs to plan a
2//! query sits at the **front** of the file — a 128-byte header, the root block,
3//! the Dataset Card, the planner statistics and every leaf directory — and the
4//! payload follows, clustered by section.
5//!
6//! ```text
7//! 0          header (128 B)
8//! 128        root block: section table + directory roots
9//!            card region: card | build record | slack
10//!            front sections: stats+schema, then the leaf directories
11//! data_off   data sections: dictionary chunks, index tiles, quad tiles,
12//!            label blocks, text postings, pyramid
13//! file_len-8 footer "RETE" 06 00 00 00
14//! ```
15//!
16//! Payload blocks (dictionary chunks, index tiles, quad tiles, label blocks,
17//! postings) are byte-identical to `0x05`; only their framing and their
18//! directories move. [`parts`] lifts a `0x05` file into [`parts::Parts`],
19//! [`write`](mod@write) assembles a `0x06` file from them, [`open`] reads one.
20
21pub mod open;
22pub mod parts;
23pub mod write;
24
25#[cfg(test)]
26mod roundtrip;
27
28use crate::file::FileError;
29use crate::varint::{read_uvarint, write_uvarint};
30
31/// The format byte of this generation.
32pub const VERSION: u8 = 0x06;
33/// Fixed header length.
34pub const HEADER_LEN: usize = 128;
35/// What a reader fetches first: header, root, card, statistics and, on a small
36/// file, every leaf directory (SPEC.md §9).
37pub const FIRST_READ: u64 = 64 * 1024;
38/// The 8-byte footer: the generation, then `RETE` — so every reader that
39/// confirms a length by the file's last four bytes finds the magic there.
40pub const FOOTER: [u8; 8] = *b"\x06\x00\x00\x00RETE";
41/// The writer's root budget: leaves grow until the stored root fits.
42pub const ROOT_BUDGET: usize = 40 * 1024;
43/// Starting (uncompressed) size of a leaf block.
44pub const LEAF_START: usize = 16 * 1024;
45/// Slack added to the card region beyond twice the card + record.
46pub const CARD_SLACK_MIN: u32 = 8 * 1024;
47
48/// Header flag: the file has named graphs (in the quad index).
49pub const F_NAMED: u8 = 0b0001;
50/// Header flag: some terms are RDF-star quoted triples.
51pub const F_QUOTED: u8 = 0b0010;
52/// Header flag: the text index also covers the named graphs' literals.
53pub const F_TEXT_NAMED: u8 = 0b0100;
54/// Header flag: quad tiles are clustered graph-major (all orders of a block of
55/// graphs adjacent) rather than order-major.
56pub const F_QUAD_GRAPH_MAJOR: u8 = 0b1000;
57
58/// Presence bit: a text index.
59pub const P_TEXT: u16 = 1;
60/// Presence bit: the text index leaves some predicates out (kind 11 in 0x05).
61pub const P_TEXT_PARTIAL: u16 = 2;
62/// Presence bit: a label index.
63pub const P_LABELS: u16 = 4;
64/// Presence bit: a stats + schema section.
65pub const P_SCHEMA: u16 = 8;
66/// Presence bit: the community pyramid.
67pub const P_PYRAMID: u16 = 16;
68/// Presence bit: the quad index.
69pub const P_QUAD: u16 = 32;
70
71/// Section kinds of the root's section table.
72#[allow(missing_docs)]
73pub mod kind {
74    pub const SCHEMA: u8 = 1;
75    pub const DICT_LEAVES: u8 = 2;
76    pub const INDEX_LEAVES: u8 = 3;
77    pub const QUAD_LEAVES: u8 = 4;
78    pub const LABEL_LEAVES: u8 = 5;
79    pub const TEXT_LEAVES: u8 = 6;
80    pub const DICT_DATA: u8 = 16;
81    pub const INDEX_DATA: u8 = 17;
82    pub const QUAD_DATA: u8 = 18;
83    pub const LABEL_DATA: u8 = 19;
84    pub const TEXT_DATA: u8 = 20;
85    pub const PYRAMID: u8 = 21;
86
87    /// Every kind, in file order (front kinds, then data kinds).
88    pub const ORDER: [u8; 12] = [
89        SCHEMA,
90        DICT_LEAVES,
91        INDEX_LEAVES,
92        QUAD_LEAVES,
93        LABEL_LEAVES,
94        TEXT_LEAVES,
95        DICT_DATA,
96        INDEX_DATA,
97        QUAD_DATA,
98        LABEL_DATA,
99        TEXT_DATA,
100        PYRAMID,
101    ];
102
103    /// A short name, for `verify --section` and `info`.
104    pub fn name(k: u8) -> &'static str {
105        match k {
106            SCHEMA => "schema",
107            DICT_LEAVES => "dict-leaves",
108            INDEX_LEAVES => "index-leaves",
109            QUAD_LEAVES => "quad-leaves",
110            LABEL_LEAVES => "label-leaves",
111            TEXT_LEAVES => "text-leaves",
112            DICT_DATA => "dict",
113            INDEX_DATA => "index",
114            QUAD_DATA => "quad",
115            LABEL_DATA => "labels",
116            TEXT_DATA => "text",
117            PYRAMID => "pyramid",
118            _ => "unknown",
119        }
120    }
121
122    /// Front sections live before the data region.
123    pub fn is_front(k: u8) -> bool {
124        k < DICT_DATA
125    }
126}
127
128/// Unit-root tags inside the root block.
129#[allow(missing_docs)]
130pub mod tag {
131    pub const DICT: u8 = 1;
132    pub const INDEX: u8 = 2;
133    pub const QUAD: u8 = 3;
134    pub const LABELS: u8 = 4;
135    pub const TEXT: u8 = 5;
136}
137
138/// The parsed 128-byte header.
139#[derive(Debug, Clone, Default, PartialEq, Eq)]
140#[allow(missing_docs)]
141pub struct Header06 {
142    pub flags: u8,
143    pub content_hash: [u8; 16],
144    pub file_len: u64,
145    pub root_len: u32,
146    pub card_region_len: u32,
147    pub data_offset: u64,
148    pub quad_count: u64,
149    pub term_count: u64,
150    pub graph_count: u64,
151    pub perm_mask: u8,
152    pub dict_codec: u8,
153    pub block_codec: u8,
154    pub internal_codec: u8,
155    pub pyramid_levels: u16,
156    /// Per-position term-id widths, 2 bits each (subjects, objects, predicates,
157    /// graphs from the low bits): 0 = u32, 1 = u64 (refused by this reader).
158    pub id_widths: u8,
159    pub presence: u16,
160    pub card_off: u32,
161    pub card_len: u32,
162    pub card_digest: [u8; 16],
163    pub build_off: u32,
164    pub build_len: u32,
165    pub root_digest: [u8; 16],
166}
167
168impl Header06 {
169    /// Serialize the fixed 128 bytes.
170    pub fn to_bytes(&self) -> [u8; HEADER_LEN] {
171        let mut b = [0u8; HEADER_LEN];
172        b[0..4].copy_from_slice(&crate::header::MAGIC);
173        b[4] = VERSION;
174        b[5] = self.flags;
175        b[6..8].copy_from_slice(&(HEADER_LEN as u16).to_le_bytes());
176        b[8..24].copy_from_slice(&self.content_hash);
177        b[24..32].copy_from_slice(&self.file_len.to_le_bytes());
178        b[32..36].copy_from_slice(&self.root_len.to_le_bytes());
179        b[36..40].copy_from_slice(&self.card_region_len.to_le_bytes());
180        b[40..48].copy_from_slice(&self.data_offset.to_le_bytes());
181        b[48..56].copy_from_slice(&self.quad_count.to_le_bytes());
182        b[56..64].copy_from_slice(&self.term_count.to_le_bytes());
183        b[64..72].copy_from_slice(&self.graph_count.to_le_bytes());
184        b[72] = self.perm_mask;
185        b[73] = self.dict_codec;
186        b[74] = self.block_codec;
187        b[75] = self.internal_codec;
188        b[76] = self.pyramid_levels as u8;
189        b[77] = self.id_widths;
190        b[78..80].copy_from_slice(&self.presence.to_le_bytes());
191        b[80..84].copy_from_slice(&self.card_off.to_le_bytes());
192        b[84..88].copy_from_slice(&self.card_len.to_le_bytes());
193        b[88..104].copy_from_slice(&self.card_digest);
194        b[104..108].copy_from_slice(&self.build_off.to_le_bytes());
195        b[108..112].copy_from_slice(&self.build_len.to_le_bytes());
196        b[112..128].copy_from_slice(&self.root_digest);
197        b
198    }
199
200    /// Parse and bounds-check the fixed 128 bytes. Every field is untrusted.
201    pub fn from_bytes(b: &[u8]) -> Result<Self, FileError> {
202        if b.len() < HEADER_LEN {
203            return Err(FileError::Container("0x06 header truncated"));
204        }
205        if b[0..4] != crate::header::MAGIC || b[4] != VERSION {
206            return Err(FileError::Container("not a 0x06 file"));
207        }
208        if u16::from_le_bytes([b[6], b[7]]) as usize != HEADER_LEN {
209            return Err(FileError::Container("0x06 header length is not 128"));
210        }
211        let u32_at = |o: usize| u32::from_le_bytes(b[o..o + 4].try_into().unwrap());
212        let u64_at = |o: usize| u64::from_le_bytes(b[o..o + 8].try_into().unwrap());
213        let h16 = |o: usize| {
214            let mut x = [0u8; 16];
215            x.copy_from_slice(&b[o..o + 16]);
216            x
217        };
218        let h = Header06 {
219            flags: b[5],
220            content_hash: h16(8),
221            file_len: u64_at(24),
222            root_len: u32_at(32),
223            card_region_len: u32_at(36),
224            data_offset: u64_at(40),
225            quad_count: u64_at(48),
226            term_count: u64_at(56),
227            graph_count: u64_at(64),
228            perm_mask: b[72],
229            dict_codec: b[73],
230            block_codec: b[74],
231            internal_codec: b[75],
232            pyramid_levels: b[76] as u16,
233            id_widths: b[77],
234            presence: u16::from_le_bytes([b[78], b[79]]),
235            card_off: u32_at(80),
236            card_len: u32_at(84),
237            card_digest: h16(88),
238            build_off: u32_at(104),
239            build_len: u32_at(108),
240            root_digest: h16(112),
241        };
242        // Structural bounds: the front must fit in the file, the card and the
243        // record in their region.
244        let front_min = (HEADER_LEN as u64)
245            .checked_add(h.root_len as u64)
246            .and_then(|x| x.checked_add(h.card_region_len as u64))
247            .ok_or(FileError::Container("0x06 front overflows"))?;
248        if front_min > h.data_offset || h.data_offset > h.file_len {
249            return Err(FileError::Container("0x06 front overruns the data region"));
250        }
251        let card_end = h.card_off as u64 + h.card_len as u64;
252        let build_end = h.build_off as u64 + h.build_len as u64;
253        if card_end > h.card_region_len as u64 || build_end > h.card_region_len as u64 {
254            return Err(FileError::Container("0x06 card region overrun"));
255        }
256        crate::index::PermSet::from_bits(h.perm_mask)
257            .map_err(|_| FileError::Container("0x06 permutation mask invalid"))?;
258        check_id_widths_byte(h.id_widths)?;
259        Ok(h)
260    }
261
262    /// Absolute offset of the root block.
263    pub fn root_offset(&self) -> u64 {
264        HEADER_LEN as u64
265    }
266    /// Absolute offset of the card region.
267    pub fn card_region_offset(&self) -> u64 {
268        HEADER_LEN as u64 + self.root_len as u64
269    }
270    /// Absolute offset where the front sections start (after the card region).
271    pub fn front_base(&self) -> u64 {
272        self.card_region_offset() + self.card_region_len as u64
273    }
274    /// The stored card's absolute range (`len == 0`: no card).
275    pub fn card_range(&self) -> crate::file::ByteRange {
276        crate::file::ByteRange {
277            offset: self.card_region_offset() + self.card_off as u64,
278            len: self.card_len as u64,
279        }
280    }
281    /// The build record's absolute range (`len == 0`: none).
282    pub fn build_range(&self) -> crate::file::ByteRange {
283        crate::file::ByteRange {
284            offset: self.card_region_offset() + self.build_off as u64,
285            len: self.build_len as u64,
286        }
287    }
288    /// The permutations the file carries.
289    pub fn perms(&self) -> crate::index::PermSet {
290        crate::index::PermSet::from_bits(self.perm_mask).unwrap_or(crate::index::PermSet::ALL)
291    }
292    /// Does the file carry this presence bit?
293    pub fn has(&self, bit: u16) -> bool {
294        self.presence & bit != 0
295    }
296}
297
298/// The first 16 bytes of a BLAKE3 digest.
299pub fn digest16(bytes: &[u8]) -> [u8; 16] {
300    let mut out = [0u8; 16];
301    out.copy_from_slice(&blake3::hash(bytes).as_bytes()[..16]);
302    out
303}
304
305/// Term-id position names, in the order of the 2-bit fields of header byte 77.
306const ID_POSITIONS: [&str; 4] = ["subjects", "objects", "predicates", "graphs"];
307
308/// Refuse a width byte this reader cannot honour: a reserved value (2, 3) is a
309/// corrupt header, a `u64` field is a file from a later writer.
310fn check_id_widths_byte(w: u8) -> Result<(), FileError> {
311    for i in 0..4 {
312        match (w >> (2 * i)) & 3 {
313            0 => {}
314            1 => {
315                return Err(FileError::Container(match i {
316                    0 => "u64 term ids (subjects): not supported by this reader",
317                    1 => "u64 term ids (objects): not supported by this reader",
318                    2 => "u64 term ids (predicates): not supported by this reader",
319                    _ => "u64 term ids (graphs): not supported by this reader",
320                }))
321            }
322            _ => return Err(FileError::Container("0x06 header: reserved term id width")),
323        }
324    }
325    Ok(())
326}
327
328/// The width byte a writer stamps: every position `u32` (0), after checking
329/// that the largest id of each position (`[subjects, objects, predicates,
330/// graphs]`, SPEC §4.1) fits. It
331/// never truncates: an id that does not fit is an error.
332pub fn id_widths_for(max_ids: [u64; 4]) -> Result<u8, FileError> {
333    for (i, m) in max_ids.iter().enumerate() {
334        if *m > u32::MAX as u64 {
335            return Err(FileError::Container(match i {
336                0 => "this dataset needs u64 term ids in subjects, not supported yet",
337                1 => "this dataset needs u64 term ids in objects, not supported yet",
338                2 => "this dataset needs u64 term ids in predicates, not supported yet",
339                _ => "this dataset needs u64 term ids in graphs, not supported yet",
340            }));
341        }
342    }
343    Ok(0)
344}
345
346/// Names of the four id positions, in field order, for display.
347pub fn id_position_names() -> [&'static str; 4] {
348    ID_POSITIONS
349}
350
351/// The content hash of a header whose card and root digests are set (SPEC.md
352/// §5): the logical header fields, the card and the root, never an offset and
353/// never the build record.
354pub fn content_hash(h: &Header06) -> [u8; 16] {
355    let b = h.to_bytes();
356    let mut x = blake3::Hasher::new();
357    x.update(b"rete/0x06");
358    x.update(&b[4..8]);
359    x.update(&b[48..80]);
360    x.update(&h.card_len.to_le_bytes());
361    x.update(&h.card_digest);
362    x.update(&h.root_len.to_le_bytes());
363    x.update(&h.root_digest);
364    let mut out = [0u8; 16];
365    out.copy_from_slice(&x.finalize().as_bytes()[..16]);
366    out
367}
368
369/// One entry of the root's section table.
370#[derive(Debug, Clone, Copy, PartialEq, Eq)]
371pub struct Sec06 {
372    /// [`kind`].
373    pub kind: u8,
374    /// The largest decompressed block of the section (a dictionary chunk, a
375    /// tile, a leaf, …), as the writer measured it; `0` = not recorded, read
376    /// under [`BLOCK_CEILING`]. Readers refuse to decode past it.
377    pub max_block: u32,
378    /// Offset relative to its region base (the front base, or `data_offset`).
379    pub offset: u64,
380    /// Stored length.
381    pub len: u64,
382    /// `BLAKE3(stored bytes)[..16]`.
383    pub digest: [u8; 16],
384}
385
386/// Size of one section-table entry.
387pub const SEC_ENTRY_LEN: usize = 38;
388
389/// The decompression bound of a section whose largest block is not recorded
390/// (label blocks and nodes; the writer cuts them at 16 KiB).
391pub const BLOCK_CEILING: u64 = 4 << 20;
392/// The bound on the root's unit roots, decompressed (Meta's are ~100 KiB).
393pub const UNIT_MAX: u64 = 16 << 20;
394/// The bound on the Dataset Card, decompressed.
395pub const CARD_MAX: u64 = 64 << 20;
396
397impl Sec06 {
398    /// The absolute byte range of this section in a file with header `h`;
399    /// an error when its offset or end overflows (a hostile root).
400    pub fn abs(&self, h: &Header06) -> Result<crate::file::ByteRange, crate::file::FileError> {
401        let base = if kind::is_front(self.kind) {
402            h.front_base()
403        } else {
404            h.data_offset
405        };
406        let overflow = || crate::file::FileError::Container("0x06 section overflows");
407        let offset = base.checked_add(self.offset).ok_or_else(overflow)?;
408        offset.checked_add(self.len).ok_or_else(overflow)?;
409        Ok(crate::file::ByteRange {
410            offset,
411            len: self.len,
412        })
413    }
414}
415
416/// The decoded root block.
417#[derive(Debug, Clone, Default)]
418pub struct Root06 {
419    /// The section table, in file order.
420    pub sections: Vec<Sec06>,
421    /// The unit roots, decompressed: `(tag, payload)`.
422    pub units: Vec<(u8, Vec<u8>)>,
423}
424
425impl Root06 {
426    /// The section of `kind`, if present.
427    pub fn section(&self, k: u8) -> Option<&Sec06> {
428        self.sections.iter().find(|s| s.kind == k)
429    }
430
431    /// The unit root payload with `tag`, if present.
432    pub fn unit(&self, t: u8) -> Option<&[u8]> {
433        self.units
434            .iter()
435            .find(|(x, _)| *x == t)
436            .map(|(_, p)| p.as_slice())
437    }
438
439    /// Serialize: the fixed section table, then the compressed unit roots.
440    pub fn encode(&self, codec: u8) -> Vec<u8> {
441        let mut units = Vec::new();
442        for (t, p) in &self.units {
443            units.push(*t);
444            write_uvarint(&mut units, p.len() as u64);
445            units.extend_from_slice(p);
446        }
447        let stored = crate::file::compress(codec, &units);
448        let mut out =
449            Vec::with_capacity(2 + SEC_ENTRY_LEN * self.sections.len() + 4 + stored.len());
450        out.push(1u8);
451        out.push(self.sections.len() as u8);
452        for s in &self.sections {
453            out.push(s.kind);
454            out.push(0);
455            out.extend_from_slice(&s.max_block.to_le_bytes());
456            out.extend_from_slice(&s.offset.to_le_bytes());
457            out.extend_from_slice(&s.len.to_le_bytes());
458            out.extend_from_slice(&s.digest);
459        }
460        out.extend_from_slice(&(stored.len() as u32).to_le_bytes());
461        out.extend_from_slice(&stored);
462        out
463    }
464
465    /// Parse a stored root block. Every count and length is untrusted.
466    pub fn decode(bytes: &[u8], codec: u8) -> Result<Self, FileError> {
467        let bad = |w| FileError::Container(w);
468        if bytes.len() < 2 || bytes[0] != 1 {
469            return Err(bad("0x06 root block: unknown version"));
470        }
471        let n = bytes[1] as usize;
472        let table_end = 2 + n * SEC_ENTRY_LEN;
473        if bytes.len() < table_end + 4 {
474            return Err(bad("0x06 root block truncated"));
475        }
476        let mut sections = Vec::with_capacity(n);
477        for i in 0..n {
478            let e = &bytes[2 + i * SEC_ENTRY_LEN..2 + (i + 1) * SEC_ENTRY_LEN];
479            let mut digest = [0u8; 16];
480            digest.copy_from_slice(&e[22..38]);
481            sections.push(Sec06 {
482                kind: e[0],
483                max_block: u32::from_le_bytes(e[2..6].try_into().unwrap()),
484                offset: u64::from_le_bytes(e[6..14].try_into().unwrap()),
485                len: u64::from_le_bytes(e[14..22].try_into().unwrap()),
486                digest,
487            });
488        }
489        let ulen = u32::from_le_bytes(bytes[table_end..table_end + 4].try_into().unwrap()) as usize;
490        let stored = bytes
491            .get(table_end + 4..table_end + 4 + ulen)
492            .ok_or(bad("0x06 root units overrun"))?;
493        let units_raw = crate::file::decompress_bounded(codec, stored, UNIT_MAX)?;
494        let mut units = Vec::new();
495        let mut c = Cur::new(&units_raw);
496        while !c.done() {
497            let t = c.u8()?;
498            let l = c.len()?;
499            units.push((t, c.bytes(l)?.to_vec()));
500        }
501        Ok(Root06 { sections, units })
502    }
503}
504
505/// A bounds-checked cursor over untrusted bytes.
506pub struct Cur<'a> {
507    b: &'a [u8],
508    /// Current position.
509    pub pos: usize,
510}
511
512// `len` reads a length varint; it is not the cursor's own length.
513#[allow(clippy::len_without_is_empty)]
514impl<'a> Cur<'a> {
515    /// A cursor at the start of `b`.
516    pub fn new(b: &'a [u8]) -> Self {
517        Cur { b, pos: 0 }
518    }
519    /// At the end?
520    pub fn done(&self) -> bool {
521        self.pos >= self.b.len()
522    }
523    /// One varint.
524    pub fn uv(&mut self) -> Result<u64, FileError> {
525        let (v, n) = read_uvarint(self.b.get(self.pos..).unwrap_or(&[]))
526            .ok_or(FileError::Container("0x06: truncated varint"))?;
527        self.pos += n;
528        Ok(v)
529    }
530    /// One varint as a `u32`.
531    pub fn u32v(&mut self) -> Result<u32, FileError> {
532        u32::try_from(self.uv()?).map_err(|_| FileError::Container("0x06: value exceeds u32"))
533    }
534    /// One varint as a length bounded by the remaining bytes.
535    pub fn len(&mut self) -> Result<usize, FileError> {
536        let v = self.uv()?;
537        let rest = self.b.len().saturating_sub(self.pos) as u64;
538        if v > rest {
539            return Err(FileError::Container("0x06: length overruns"));
540        }
541        Ok(v as usize)
542    }
543    /// One varint as a count, sanity-bounded by `max`.
544    pub fn count(&mut self, max: u64) -> Result<usize, FileError> {
545        let v = self.uv()?;
546        if v > max {
547            return Err(FileError::Container("0x06: count out of range"));
548        }
549        Ok(v as usize)
550    }
551    /// One byte.
552    pub fn u8(&mut self) -> Result<u8, FileError> {
553        let v = *self
554            .b
555            .get(self.pos)
556            .ok_or(FileError::Container("0x06: truncated"))?;
557        self.pos += 1;
558        Ok(v)
559    }
560    /// `n` raw bytes.
561    pub fn bytes(&mut self, n: usize) -> Result<&'a [u8], FileError> {
562        let s = self
563            .b
564            .get(
565                self.pos
566                    ..self
567                        .pos
568                        .checked_add(n)
569                        .ok_or(FileError::Container("0x06: overflow"))?,
570            )
571            .ok_or(FileError::Container("0x06: truncated bytes"))?;
572        self.pos += n;
573        Ok(s)
574    }
575    /// A length-prefixed byte string.
576    pub fn lp(&mut self) -> Result<&'a [u8], FileError> {
577        let n = self.len()?;
578        self.bytes(n)
579    }
580}
581
582/// Append a length-prefixed byte string.
583pub fn put_lp(out: &mut Vec<u8>, b: &[u8]) {
584    write_uvarint(out, b.len() as u64);
585    out.extend_from_slice(b);
586}
587
588// ---------------------------------------------------------------------------
589// Unit roots
590// ---------------------------------------------------------------------------
591
592/// One leaf of a two-level directory, as the root describes it.
593#[derive(Debug, Clone, PartialEq, Eq, Default)]
594pub struct LeafRef {
595    /// Index of the leaf's first entry in the whole unit.
596    pub first: usize,
597    /// Entries in the leaf.
598    pub n: usize,
599    /// Leaf offset within its leaves section.
600    pub off: u64,
601    /// Stored (compressed) leaf length.
602    pub len: u64,
603}
604
605/// The root of one dictionary section.
606#[derive(Debug, Clone, Default)]
607pub struct DictSecRoot {
608    /// Terms in the section.
609    pub term_count: u32,
610    /// Restart interval of its front-coded runs.
611    pub restart_interval: u32,
612    /// Chunks in the section.
613    pub n_chunks: usize,
614    /// Offset of the section's first chunk within DICT_DATA.
615    pub data_off: u64,
616    /// Bytes of chunk data.
617    pub data_len: u64,
618    /// Leaves: `(leaf, first run, chunk offset within the section's data, first key)`.
619    pub leaves: Vec<(LeafRef, usize, u64, Vec<u8>)>,
620}
621
622/// Encode the dictionary unit root.
623pub fn encode_dict_root(secs: &[DictSecRoot; 4]) -> Vec<u8> {
624    let mut o = Vec::new();
625    for s in secs {
626        write_uvarint(&mut o, s.term_count as u64);
627        write_uvarint(&mut o, s.restart_interval as u64);
628        write_uvarint(&mut o, s.n_chunks as u64);
629        write_uvarint(&mut o, s.data_off);
630        write_uvarint(&mut o, s.data_len);
631        write_uvarint(&mut o, s.leaves.len() as u64);
632        for (l, run, coff, key) in &s.leaves {
633            write_uvarint(&mut o, l.n as u64);
634            write_uvarint(&mut o, l.off);
635            write_uvarint(&mut o, l.len);
636            write_uvarint(&mut o, *run as u64);
637            write_uvarint(&mut o, *coff);
638            put_lp(&mut o, key);
639        }
640    }
641    o
642}
643
644/// Decode the dictionary unit root.
645pub fn decode_dict_root(p: &[u8]) -> Result<[DictSecRoot; 4], FileError> {
646    let mut c = Cur::new(p);
647    let mut out: [DictSecRoot; 4] = Default::default();
648    for s in out.iter_mut() {
649        s.term_count = c.u32v()?;
650        s.restart_interval = c.u32v()?;
651        s.n_chunks = c.count(u32::MAX as u64)?;
652        s.data_off = c.uv()?;
653        s.data_len = c.uv()?;
654        let nl = c.count(p.len() as u64)?;
655        let mut first = 0usize;
656        for _ in 0..nl {
657            let n = c.count(u32::MAX as u64)?;
658            let off = c.uv()?;
659            let len = c.uv()?;
660            let run = c.count(u64::MAX)?;
661            let coff = c.uv()?;
662            let key = c.lp()?.to_vec();
663            s.leaves
664                .push((LeafRef { first, n, off, len }, run, coff, key));
665            first = first
666                .checked_add(n)
667                .ok_or(FileError::Container("0x06 dict leaves overflow"))?;
668        }
669        if first != s.n_chunks {
670            return Err(FileError::Container(
671                "0x06 dict leaves do not cover the chunks",
672            ));
673        }
674        if s.restart_interval == 0 && s.term_count > 0 {
675            return Err(FileError::Container("0x06 dict zero restart interval"));
676        }
677    }
678    Ok(out)
679}
680
681/// The root of one permutation's tile directory.
682#[derive(Debug, Clone, Default)]
683pub struct PermRoot {
684    /// Section index (`IndexPermutation::section_index`).
685    pub si: usize,
686    /// Tiles.
687    pub n_tiles: usize,
688    /// Offset of the permutation's first tile within INDEX_DATA.
689    pub data_off: u64,
690    /// Bytes of tiles.
691    pub data_len: u64,
692    /// Leaves: `(leaf, first min_a, last max_a, tile offset within the permutation's data)`.
693    pub leaves: Vec<(LeafRef, u32, u32, u64)>,
694}
695
696/// Encode the index unit root (permutations in section-index order).
697pub fn encode_index_root(perms: &[PermRoot]) -> Vec<u8> {
698    let mut o = Vec::new();
699    write_uvarint(&mut o, perms.len() as u64);
700    for p in perms {
701        write_uvarint(&mut o, p.si as u64);
702        write_uvarint(&mut o, p.n_tiles as u64);
703        write_uvarint(&mut o, p.data_off);
704        write_uvarint(&mut o, p.data_len);
705        write_uvarint(&mut o, p.leaves.len() as u64);
706        for (l, mn, mx, toff) in &p.leaves {
707            write_uvarint(&mut o, l.n as u64);
708            write_uvarint(&mut o, l.off);
709            write_uvarint(&mut o, l.len);
710            write_uvarint(&mut o, *mn as u64);
711            write_uvarint(&mut o, *mx as u64);
712            write_uvarint(&mut o, *toff);
713        }
714    }
715    o
716}
717
718/// Decode the index unit root.
719pub fn decode_index_root(p: &[u8]) -> Result<Vec<PermRoot>, FileError> {
720    let mut c = Cur::new(p);
721    let n = c.count(6)?;
722    let mut out = Vec::with_capacity(n);
723    for _ in 0..n {
724        let si = c.count(5)?;
725        let n_tiles = c.count(u32::MAX as u64)?;
726        let data_off = c.uv()?;
727        let data_len = c.uv()?;
728        let nl = c.count(p.len() as u64)?;
729        let mut leaves = Vec::with_capacity(nl.min(p.len() / 5));
730        let mut first = 0usize;
731        for _ in 0..nl {
732            let ln = c.count(u32::MAX as u64)?;
733            let off = c.uv()?;
734            let len = c.uv()?;
735            let mn = c.u32v()?;
736            let mx = c.u32v()?;
737            let toff = c.uv()?;
738            leaves.push((
739                LeafRef {
740                    first,
741                    n: ln,
742                    off,
743                    len,
744                },
745                mn,
746                mx,
747                toff,
748            ));
749            first = first
750                .checked_add(ln)
751                .ok_or(FileError::Container("0x06 index leaves overflow"))?;
752        }
753        if first != n_tiles {
754            return Err(FileError::Container(
755                "0x06 index leaves do not cover the tiles",
756            ));
757        }
758        out.push(PermRoot {
759            si,
760            n_tiles,
761            data_off,
762            data_len,
763            leaves,
764        });
765    }
766    Ok(out)
767}
768
769/// The root of the text index.
770#[derive(Debug, Clone, Default)]
771pub struct TextRoot {
772    /// The scope block (empty for the default scope).
773    pub scope: Vec<u8>,
774    /// Distinct tokens.
775    pub n_tokens: usize,
776    /// Leaves: `(leaf, first token)`.
777    pub leaves: Vec<(LeafRef, Vec<u8>)>,
778}
779
780/// Encode the text unit root.
781pub fn encode_text_root(t: &TextRoot) -> Vec<u8> {
782    let mut o = Vec::new();
783    put_lp(&mut o, &t.scope);
784    write_uvarint(&mut o, t.n_tokens as u64);
785    write_uvarint(&mut o, t.leaves.len() as u64);
786    for (l, first) in &t.leaves {
787        write_uvarint(&mut o, l.n as u64);
788        write_uvarint(&mut o, l.off);
789        write_uvarint(&mut o, l.len);
790        put_lp(&mut o, first);
791    }
792    o
793}
794
795/// Decode the text unit root.
796pub fn decode_text_root(p: &[u8]) -> Result<TextRoot, FileError> {
797    let mut c = Cur::new(p);
798    let scope = c.lp()?.to_vec();
799    let n_tokens = c.count(u32::MAX as u64)?;
800    let nl = c.count(p.len() as u64)?;
801    let mut leaves = Vec::with_capacity(nl.min(p.len() / 5));
802    let mut first = 0usize;
803    for _ in 0..nl {
804        let n = c.count(u32::MAX as u64)?;
805        let off = c.uv()?;
806        let len = c.uv()?;
807        let tok = c.lp()?.to_vec();
808        leaves.push((LeafRef { first, n, off, len }, tok));
809        first = first
810            .checked_add(n)
811            .ok_or(FileError::Container("0x06 text leaves overflow"))?;
812    }
813    if first != n_tokens {
814        return Err(FileError::Container(
815            "0x06 text leaves do not cover the tokens",
816        ));
817    }
818    Ok(TextRoot {
819        scope,
820        n_tokens,
821        leaves,
822    })
823}
824
825/// The root of the label index: the 0x05 preamble and root node, and where the
826/// data blocks end in the 0x05 body coordinates (SPEC.md §6.4).
827#[derive(Debug, Clone, Default)]
828pub struct LabelRoot {
829    /// Bytes of data blocks (the 0x05 body offsets below this are blocks, the
830    /// rest are directory nodes).
831    pub data_len: u64,
832    /// The 32-byte 0x05 preamble (digest zeroed) followed by the root node.
833    pub head: Vec<u8>,
834}
835
836/// Encode the label unit root.
837pub fn encode_label_root(l: &LabelRoot) -> Vec<u8> {
838    let mut o = Vec::new();
839    write_uvarint(&mut o, l.data_len);
840    put_lp(&mut o, &l.head);
841    o
842}
843
844/// Decode the label unit root.
845pub fn decode_label_root(p: &[u8]) -> Result<LabelRoot, FileError> {
846    let mut c = Cur::new(p);
847    let data_len = c.uv()?;
848    let head = c.lp()?.to_vec();
849    if head.len() < 32 {
850        return Err(FileError::Container("0x06 label root truncated"));
851    }
852    Ok(LabelRoot { data_len, head })
853}
854
855/// The root of the quad index.
856#[derive(Debug, Clone, Default)]
857pub struct QuadRoot {
858    /// Named graphs.
859    pub graph_count: u64,
860    /// Named-graph quads.
861    pub quad_count: u64,
862    /// Graphs per graph-directory block.
863    pub block_graphs: u64,
864    /// The 0x05 `P0`: the graph-directory block index (offsets from the start of
865    /// QUAD_LEAVES, where the blocks are).
866    pub p0: Vec<u8>,
867    /// Bytes of graph-directory blocks at the start of QUAD_LEAVES.
868    pub gdir_len: u64,
869    /// Per order present (mask order): `(section index, tiles, leaves)`; a leaf
870    /// is `(leaf, first (g, a), last (g, a), offset of its first tile in QUAD_DATA)`.
871    pub orders: Vec<QuadOrderRoot>,
872}
873
874/// One order of the quad index.
875#[derive(Debug, Clone, Default)]
876pub struct QuadOrderRoot {
877    /// Section index of the order (`G+X` uses `X`'s index).
878    pub si: usize,
879    /// Tiles.
880    pub n_tiles: usize,
881    /// Leaves.
882    pub leaves: Vec<QuadLeafRoot>,
883}
884
885/// A quad order's leaf in the root: the leaf, the first tile's first `(g, a)`,
886/// the last tile's last `(g, a)`, and the first tile's offset in `quad`.
887pub type QuadLeafRoot = (LeafRef, (u32, u32), (u32, u32), u64);
888
889/// Encode the quad unit root.
890pub fn encode_quad_root(q: &QuadRoot) -> Vec<u8> {
891    let mut o = Vec::new();
892    write_uvarint(&mut o, q.graph_count);
893    write_uvarint(&mut o, q.quad_count);
894    write_uvarint(&mut o, q.block_graphs);
895    put_lp(&mut o, &q.p0);
896    write_uvarint(&mut o, q.gdir_len);
897    write_uvarint(&mut o, q.orders.len() as u64);
898    for ord in &q.orders {
899        write_uvarint(&mut o, ord.si as u64);
900        write_uvarint(&mut o, ord.n_tiles as u64);
901        write_uvarint(&mut o, ord.leaves.len() as u64);
902        for (l, f, la, toff) in &ord.leaves {
903            write_uvarint(&mut o, l.n as u64);
904            write_uvarint(&mut o, l.off);
905            write_uvarint(&mut o, l.len);
906            write_uvarint(&mut o, f.0 as u64);
907            write_uvarint(&mut o, f.1 as u64);
908            write_uvarint(&mut o, la.0 as u64);
909            write_uvarint(&mut o, la.1 as u64);
910            write_uvarint(&mut o, *toff);
911        }
912    }
913    o
914}
915
916/// Decode the quad unit root.
917pub fn decode_quad_root(p: &[u8]) -> Result<QuadRoot, FileError> {
918    let mut c = Cur::new(p);
919    let graph_count = c.uv()?;
920    let quad_count = c.uv()?;
921    let block_graphs = c.uv()?;
922    let p0 = c.lp()?.to_vec();
923    let gdir_len = c.uv()?;
924    let no = c.count(6)?;
925    let mut orders = Vec::with_capacity(no);
926    for _ in 0..no {
927        let si = c.count(5)?;
928        let n_tiles = c.count(u32::MAX as u64)?;
929        let nl = c.count(p.len() as u64)?;
930        let mut leaves = Vec::with_capacity(nl.min(p.len() / 5));
931        let mut first = 0usize;
932        for _ in 0..nl {
933            let n = c.count(u32::MAX as u64)?;
934            let off = c.uv()?;
935            let len = c.uv()?;
936            let f = (c.u32v()?, c.u32v()?);
937            let la = (c.u32v()?, c.u32v()?);
938            let toff = c.uv()?;
939            leaves.push((LeafRef { first, n, off, len }, f, la, toff));
940            first = first
941                .checked_add(n)
942                .ok_or(FileError::Container("0x06 quad leaves overflow"))?;
943        }
944        if first != n_tiles {
945            return Err(FileError::Container(
946                "0x06 quad leaves do not cover the tiles",
947            ));
948        }
949        orders.push(QuadOrderRoot {
950            si,
951            n_tiles,
952            leaves,
953        });
954    }
955    if block_graphs == 0 && graph_count > 0 {
956        return Err(FileError::Container("0x06 quad block size is zero"));
957    }
958    Ok(QuadRoot {
959        graph_count,
960        quad_count,
961        block_graphs,
962        p0,
963        gdir_len,
964        orders,
965    })
966}
967
968// ---------------------------------------------------------------------------
969// Leaf entries
970// ---------------------------------------------------------------------------
971
972/// One dictionary chunk as a leaf lists it.
973#[derive(Debug, Clone, PartialEq, Eq)]
974pub struct ChunkEnt {
975    /// Index of the chunk's first front-coded run.
976    pub first_run: usize,
977    /// Routing separator (SPEC.md §6.2).
978    pub key: Vec<u8>,
979    /// Compressed chunk length.
980    pub clen: u64,
981}
982
983/// Encode chunk entries (the first `first_run` absolute, then deltas).
984pub fn encode_chunk_leaf(ents: &[ChunkEnt]) -> Vec<u8> {
985    let mut o = Vec::new();
986    let mut prev = 0usize;
987    for (i, e) in ents.iter().enumerate() {
988        write_uvarint(
989            &mut o,
990            (if i == 0 {
991                e.first_run
992            } else {
993                e.first_run - prev
994            }) as u64,
995        );
996        put_lp(&mut o, &e.key);
997        write_uvarint(&mut o, e.clen);
998        prev = e.first_run;
999    }
1000    o
1001}
1002
1003/// Decode `n` chunk entries.
1004pub fn decode_chunk_leaf(b: &[u8], n: usize) -> Result<Vec<ChunkEnt>, FileError> {
1005    let mut c = Cur::new(b);
1006    let mut out = Vec::with_capacity(n.min(b.len()));
1007    let mut prev = 0usize;
1008    for i in 0..n {
1009        let d = c.count(u64::MAX)?;
1010        let first_run = if i == 0 {
1011            d
1012        } else {
1013            prev.checked_add(d)
1014                .ok_or(FileError::Container("0x06 chunk run overflow"))?
1015        };
1016        let key = c.lp()?.to_vec();
1017        let clen = c.uv()?;
1018        out.push(ChunkEnt {
1019            first_run,
1020            key,
1021            clen,
1022        });
1023        prev = first_run;
1024    }
1025    Ok(out)
1026}
1027
1028/// One index tile as a leaf lists it.
1029#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1030pub struct TileEnt {
1031    /// Leading-id range.
1032    pub min_a: u32,
1033    /// Leading-id range, inclusive.
1034    pub max_a: u32,
1035    /// Compressed length.
1036    pub clen: u64,
1037    /// `(min_b, max_b, min_c, max_c)`.
1038    pub syn: (u32, u32, u32, u32),
1039}
1040
1041/// Encode tile entries: the 0x05 directory rows, then the synopsis rows.
1042pub fn encode_tile_leaf(ents: &[TileEnt]) -> Vec<u8> {
1043    let mut o = Vec::new();
1044    let mut prev = 0u32;
1045    for (i, e) in ents.iter().enumerate() {
1046        write_uvarint(
1047            &mut o,
1048            (if i == 0 {
1049                e.min_a
1050            } else {
1051                e.min_a.wrapping_sub(prev)
1052            }) as u64,
1053        );
1054        write_uvarint(&mut o, e.max_a.wrapping_sub(e.min_a) as u64);
1055        write_uvarint(&mut o, e.clen);
1056        prev = e.min_a;
1057    }
1058    for e in ents {
1059        write_uvarint(&mut o, e.syn.0 as u64);
1060        write_uvarint(&mut o, e.syn.1.wrapping_sub(e.syn.0) as u64);
1061        write_uvarint(&mut o, e.syn.2 as u64);
1062        write_uvarint(&mut o, e.syn.3.wrapping_sub(e.syn.2) as u64);
1063    }
1064    o
1065}
1066
1067/// Decode `n` tile entries.
1068pub fn decode_tile_leaf(b: &[u8], n: usize) -> Result<Vec<TileEnt>, FileError> {
1069    let mut c = Cur::new(b);
1070    let mut out = Vec::with_capacity(n.min(b.len()));
1071    let mut prev = 0u32;
1072    for i in 0..n {
1073        let d = c.u32v()?;
1074        let min_a = if i == 0 { d } else { prev.wrapping_add(d) };
1075        let span = c.u32v()?;
1076        let clen = c.uv()?;
1077        out.push(TileEnt {
1078            min_a,
1079            max_a: min_a.wrapping_add(span),
1080            clen,
1081            syn: (0, 0, 0, 0),
1082        });
1083        prev = min_a;
1084    }
1085    for e in out.iter_mut() {
1086        let mb = c.u32v()?;
1087        let db = c.u32v()?;
1088        let mc = c.u32v()?;
1089        let dc = c.u32v()?;
1090        e.syn = (mb, mb.wrapping_add(db), mc, mc.wrapping_add(dc));
1091    }
1092    Ok(out)
1093}
1094
1095/// One text token as a leaf lists it.
1096#[derive(Debug, Clone, PartialEq, Eq)]
1097pub struct TokenEnt {
1098    /// The token.
1099    pub token: Vec<u8>,
1100    /// Posting offset within TEXT_DATA.
1101    pub off: u64,
1102    /// Posting length.
1103    pub len: u64,
1104}
1105
1106/// Encode token entries, front-coded within the leaf.
1107pub fn encode_token_leaf(ents: &[TokenEnt]) -> Vec<u8> {
1108    let mut o = Vec::new();
1109    let mut prev: &[u8] = &[];
1110    for e in ents {
1111        let shared = prev
1112            .iter()
1113            .zip(&e.token)
1114            .take_while(|(a, b)| a == b)
1115            .count();
1116        write_uvarint(&mut o, shared as u64);
1117        put_lp(&mut o, &e.token[shared..]);
1118        write_uvarint(&mut o, e.off);
1119        write_uvarint(&mut o, e.len);
1120        prev = &e.token;
1121    }
1122    o
1123}
1124
1125/// Decode `n` token entries.
1126pub fn decode_token_leaf(b: &[u8], n: usize) -> Result<Vec<TokenEnt>, FileError> {
1127    let mut c = Cur::new(b);
1128    let mut out: Vec<TokenEnt> = Vec::with_capacity(n.min(b.len()));
1129    for _ in 0..n {
1130        let shared = c.count(u32::MAX as u64)?;
1131        let suffix = c.lp()?;
1132        let mut token = match out.last() {
1133            Some(p) if shared <= p.token.len() => p.token[..shared].to_vec(),
1134            None if shared == 0 => Vec::new(),
1135            _ => return Err(FileError::Container("0x06 token front-coding out of range")),
1136        };
1137        token.extend_from_slice(suffix);
1138        let off = c.uv()?;
1139        let len = c.uv()?;
1140        out.push(TokenEnt { token, off, len });
1141    }
1142    Ok(out)
1143}
1144
1145/// One quad tile as a leaf lists it.
1146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1147pub struct QTileEnt {
1148    /// Leading key of the first row.
1149    pub first: (u32, u32),
1150    /// Leading key of the last row.
1151    pub last: (u32, u32),
1152    /// Stored length.
1153    pub clen: u64,
1154    /// Stored raw (not compressed).
1155    pub raw: bool,
1156    /// `(min_b, max_b, min_c, max_c)`.
1157    pub syn: (u32, u32, u32, u32),
1158    /// Absolute offset of the tile within QUAD_DATA.
1159    pub off: u64,
1160}
1161
1162/// Encode quad tile entries. A tile's offset is written as `0` when it follows
1163/// the previous tile of the leaf (or, for the first, sits at the leaf's root
1164/// offset), else as `offset + 1` (PMTiles' clustered encoding).
1165pub fn encode_qtile_leaf(ents: &[QTileEnt], leaf_off: u64) -> Vec<u8> {
1166    let mut o = Vec::new();
1167    let mut prev_g = 0u32;
1168    let mut next = leaf_off;
1169    for (i, e) in ents.iter().enumerate() {
1170        write_uvarint(
1171            &mut o,
1172            (if i == 0 {
1173                e.first.0
1174            } else {
1175                e.first.0 - prev_g
1176            }) as u64,
1177        );
1178        write_uvarint(&mut o, e.first.1 as u64);
1179        write_uvarint(&mut o, (e.last.0 - e.first.0) as u64);
1180        write_uvarint(&mut o, e.last.1 as u64);
1181        write_uvarint(&mut o, (e.clen << 1) | e.raw as u64);
1182        write_uvarint(&mut o, e.syn.0 as u64);
1183        write_uvarint(&mut o, e.syn.1.wrapping_sub(e.syn.0) as u64);
1184        write_uvarint(&mut o, e.syn.2 as u64);
1185        write_uvarint(&mut o, e.syn.3.wrapping_sub(e.syn.2) as u64);
1186        write_uvarint(&mut o, if e.off == next { 0 } else { e.off + 1 });
1187        next = e.off + e.clen;
1188        prev_g = e.first.0;
1189    }
1190    o
1191}
1192
1193/// Decode `n` quad tile entries of a leaf whose first tile sits at `leaf_off`.
1194pub fn decode_qtile_leaf(b: &[u8], n: usize, leaf_off: u64) -> Result<Vec<QTileEnt>, FileError> {
1195    let mut c = Cur::new(b);
1196    let mut out = Vec::with_capacity(n.min(b.len()));
1197    let mut prev_g = 0u32;
1198    let mut next = leaf_off;
1199    for i in 0..n {
1200        let d = c.u32v()?;
1201        let g = if i == 0 {
1202            d
1203        } else {
1204            prev_g
1205                .checked_add(d)
1206                .ok_or(FileError::Container("0x06 quad graph overflow"))?
1207        };
1208        let fa = c.u32v()?;
1209        let dg = c.u32v()?;
1210        let la = c.u32v()?;
1211        let packed = c.uv()?;
1212        let mb = c.u32v()?;
1213        let db = c.u32v()?;
1214        let mc = c.u32v()?;
1215        let dc = c.u32v()?;
1216        let oc = c.uv()?;
1217        let off = if oc == 0 { next } else { oc - 1 };
1218        let clen = packed >> 1;
1219        out.push(QTileEnt {
1220            first: (g, fa),
1221            last: (
1222                g.checked_add(dg)
1223                    .ok_or(FileError::Container("0x06 quad graph overflow"))?,
1224                la,
1225            ),
1226            clen,
1227            raw: packed & 1 == 1,
1228            syn: (mb, mb.wrapping_add(db), mc, mc.wrapping_add(dc)),
1229            off,
1230        });
1231        next = off
1232            .checked_add(clen)
1233            .ok_or(FileError::Container("0x06 quad offset overflow"))?;
1234        prev_g = g;
1235    }
1236    Ok(out)
1237}
1238
1239/// Cut `sizes` (each entry's encoded size) into leaves of about `target`
1240/// bytes: `(start, end)` index ranges, never empty, covering everything.
1241pub fn cut(sizes: &[usize], target: usize) -> Vec<(usize, usize)> {
1242    let mut out = Vec::new();
1243    let mut start = 0usize;
1244    let mut acc = 0usize;
1245    for (i, &s) in sizes.iter().enumerate() {
1246        acc += s;
1247        if acc >= target {
1248            out.push((start, i + 1));
1249            start = i + 1;
1250            acc = 0;
1251        }
1252    }
1253    if start < sizes.len() {
1254        out.push((start, sizes.len()));
1255    }
1256    out
1257}
1258
1259/// A logical, 0x05-shaped [`crate::header::Header`] for a 0x06 file, so code
1260/// that asks presence questions (`text_index_len > 0`, `label_index_section()`)
1261/// answers them the same for both formats. Its offsets all point at the end of
1262/// the file, so a physical read that was not ported to 0x06 fails loudly instead
1263/// of reading the wrong bytes.
1264pub fn logical_header(h: &Header06) -> crate::header::Header {
1265    use crate::header::{Section, SectionKind};
1266    let end = h.file_len;
1267    let mut flags = crate::header::FLAG_TILE_SYNOPSIS;
1268    if h.flags & F_NAMED != 0 {
1269        flags |= crate::header::FLAG_HAS_QUADS;
1270    }
1271    if h.flags & F_QUOTED != 0 {
1272        flags |= crate::header::FLAG_HAS_QUOTED_TRIPLES;
1273    }
1274    if h.has(P_QUAD) {
1275        flags |= crate::header::FLAG_QUAD_INDEX;
1276    }
1277    if h.flags & F_TEXT_NAMED != 0 && h.has(P_TEXT) {
1278        flags |= crate::header::FLAG_TEXT_INDEX_NAMED_GRAPHS;
1279    }
1280    let present = |bit: u16| if h.has(bit) { 1u64 } else { 0 };
1281    let mut extra = Vec::new();
1282    if h.has(P_LABELS) {
1283        extra.push(Section {
1284            kind: SectionKind::LabelIndex,
1285            flags: 0,
1286            offset: end,
1287            length: 1,
1288        });
1289    }
1290    if h.has(P_SCHEMA) {
1291        extra.push(Section {
1292            kind: SectionKind::Schema,
1293            flags: 0,
1294            offset: end,
1295            length: 1,
1296        });
1297    }
1298    if h.has(P_QUAD) {
1299        extra.push(Section {
1300            kind: SectionKind::QuadIndex,
1301            flags: 0,
1302            offset: end,
1303            length: 1,
1304        });
1305    }
1306    crate::header::Header {
1307        version: VERSION,
1308        flags,
1309        metadata_offset: end,
1310        metadata_len: h.card_len as u64,
1311        dictionary_offset: end,
1312        dictionary_len: 0,
1313        root_dir_offset: end,
1314        root_dir_len: 0,
1315        pyramid_meta_offset: end,
1316        pyramid_meta_len: present(P_PYRAMID),
1317        dict_codec: h.dict_codec,
1318        block_codec: h.block_codec,
1319        pyramid_levels: h.pyramid_levels,
1320        id_widths: h.id_widths,
1321        quad_count: h.quad_count,
1322        term_count: h.term_count,
1323        content_hash: h.content_hash,
1324        named_graphs_offset: 0,
1325        named_graphs_len: 0,
1326        schema_meta_len: 0,
1327        text_index_offset: end,
1328        text_index_len: present(P_TEXT),
1329        text_index_partial: h.has(P_TEXT_PARTIAL),
1330        build_info_offset: end,
1331        build_info_len: h.build_len as u64,
1332        perms: h.perms(),
1333        extra_sections: extra,
1334    }
1335}
1336
1337#[cfg(test)]
1338mod tests {
1339    use super::*;
1340
1341    #[test]
1342    fn header_round_trips() {
1343        let h = Header06 {
1344            flags: F_NAMED | F_QUAD_GRAPH_MAJOR,
1345            content_hash: [7; 16],
1346            file_len: 10_000,
1347            root_len: 300,
1348            card_region_len: 100,
1349            data_offset: 2_000,
1350            quad_count: 5,
1351            term_count: 9,
1352            graph_count: 2,
1353            perm_mask: 0b11_1111,
1354            dict_codec: 1,
1355            block_codec: 1,
1356            internal_codec: 1,
1357            pyramid_levels: 0,
1358            id_widths: 0,
1359            presence: P_TEXT | P_QUAD,
1360            card_off: 0,
1361            card_len: 40,
1362            card_digest: [3; 16],
1363            build_off: 40,
1364            build_len: 20,
1365            root_digest: [4; 16],
1366        };
1367        let b = h.to_bytes();
1368        assert_eq!(Header06::from_bytes(&b).unwrap(), h);
1369        // The hash ignores offsets and the build record.
1370        let mut h2 = h.clone();
1371        h2.data_offset = 3_000;
1372        h2.build_len = 0;
1373        h2.file_len = 11_000;
1374        assert_eq!(content_hash(&h), content_hash(&h2));
1375        h2.card_digest = [9; 16];
1376        assert_ne!(content_hash(&h), content_hash(&h2));
1377    }
1378
1379    #[test]
1380    fn leaves_round_trip() {
1381        let c = vec![
1382            ChunkEnt {
1383                first_run: 0,
1384                key: vec![],
1385                clen: 10,
1386            },
1387            ChunkEnt {
1388                first_run: 5,
1389                key: b"ab".to_vec(),
1390                clen: 20,
1391            },
1392        ];
1393        assert_eq!(decode_chunk_leaf(&encode_chunk_leaf(&c), 2).unwrap(), c);
1394        let t = vec![
1395            TileEnt {
1396                min_a: 3,
1397                max_a: 9,
1398                clen: 100,
1399                syn: (1, 4, 2, 8),
1400            },
1401            TileEnt {
1402                min_a: 9,
1403                max_a: 20,
1404                clen: 7,
1405                syn: (0, 0, 5, 5),
1406            },
1407        ];
1408        assert_eq!(decode_tile_leaf(&encode_tile_leaf(&t), 2).unwrap(), t);
1409        let k = vec![
1410            TokenEnt {
1411                token: b"apple".to_vec(),
1412                off: 0,
1413                len: 3,
1414            },
1415            TokenEnt {
1416                token: b"apply".to_vec(),
1417                off: 3,
1418                len: 4,
1419            },
1420        ];
1421        assert_eq!(decode_token_leaf(&encode_token_leaf(&k), 2).unwrap(), k);
1422        let q = vec![
1423            QTileEnt {
1424                first: (5, 1),
1425                last: (6, 2),
1426                clen: 30,
1427                raw: true,
1428                syn: (0, 1, 2, 3),
1429                off: 100,
1430            },
1431            QTileEnt {
1432                first: (6, 3),
1433                last: (9, 0),
1434                clen: 10,
1435                raw: false,
1436                syn: (4, 4, 4, 4),
1437                off: 500,
1438            },
1439        ];
1440        assert_eq!(
1441            decode_qtile_leaf(&encode_qtile_leaf(&q, 100), 2, 100).unwrap(),
1442            q
1443        );
1444    }
1445
1446    #[test]
1447    fn cut_covers_everything() {
1448        assert_eq!(cut(&[5, 5, 5, 5, 5], 10), vec![(0, 2), (2, 4), (4, 5)]);
1449        assert_eq!(cut(&[], 10), Vec::<(usize, usize)>::new());
1450        assert_eq!(cut(&[50], 10), vec![(0, 1)]);
1451    }
1452}
1453
1454#[cfg(test)]
1455mod id_width_tests {
1456    use super::*;
1457
1458    fn sample() -> Header06 {
1459        Header06 {
1460            file_len: 10_000,
1461            root_len: 300,
1462            card_region_len: 100,
1463            data_offset: 2_000,
1464            perm_mask: 0b11_1111,
1465            pyramid_levels: 7,
1466            ..Default::default()
1467        }
1468    }
1469
1470    #[test]
1471    fn width_byte_round_trips_and_is_hashed() {
1472        let h = sample();
1473        assert_eq!(Header06::from_bytes(&h.to_bytes()).unwrap(), h);
1474        assert_eq!(h.to_bytes()[76], 7);
1475        assert_eq!(h.to_bytes()[77], 0);
1476        // Hashing sees the byte: it is logical content.
1477        let mut other = h.clone();
1478        other.id_widths = 1;
1479        assert_ne!(content_hash(&h), content_hash(&other));
1480    }
1481
1482    #[test]
1483    fn a_reader_refuses_every_nonzero_width() {
1484        for (i, name) in id_position_names().iter().enumerate() {
1485            let mut b = sample().to_bytes();
1486            b[77] = 1 << (2 * i);
1487            let e = Header06::from_bytes(&b).unwrap_err().to_string();
1488            assert!(
1489                e.contains(&format!(
1490                    "u64 term ids ({name}): not supported by this reader"
1491                )),
1492                "{e}"
1493            );
1494            for reserved in [2u8, 3] {
1495                let mut b = sample().to_bytes();
1496                b[77] = reserved << (2 * i);
1497                let e = Header06::from_bytes(&b).unwrap_err().to_string();
1498                assert!(e.contains("reserved term id width"), "{e}");
1499            }
1500        }
1501    }
1502
1503    #[test]
1504    fn a_writer_refuses_an_id_that_does_not_fit_u32() {
1505        assert_eq!(id_widths_for([u32::MAX as u64; 4]).unwrap(), 0);
1506        for (i, name) in id_position_names().iter().enumerate() {
1507            let mut m = [1u64; 4];
1508            m[i] = u32::MAX as u64 + 1;
1509            let e = id_widths_for(m).unwrap_err().to_string();
1510            assert!(
1511                e.contains(&format!("needs u64 term ids in {name}, not supported yet")),
1512                "{e}"
1513            );
1514        }
1515    }
1516}
1517
1518#[cfg(test)]
1519mod hostile_root_tests {
1520    use super::*;
1521
1522    fn sec(kind: u8, offset: u64, len: u64) -> Sec06 {
1523        Sec06 {
1524            kind,
1525            max_block: 0,
1526            offset,
1527            len,
1528            digest: [0; 16],
1529        }
1530    }
1531
1532    #[test]
1533    fn section_range_overflow_is_an_error() {
1534        let h = Header06 {
1535            data_offset: 100,
1536            ..Header06::default()
1537        };
1538        // A data section, a front section, an offset that overflows the base,
1539        // and one whose end overflows.
1540        for (k, o, l) in [
1541            (kind::DICT_DATA, u64::MAX, 0),
1542            (kind::DICT_DATA, u64::MAX - 50, 10),
1543            (kind::DICT_DATA, 10, u64::MAX),
1544            (kind::DICT_LEAVES, u64::MAX, 1),
1545        ] {
1546            assert!(sec(k, o, l).abs(&h).is_err(), "kind {k} offset {o} len {l}");
1547        }
1548        let ok = sec(kind::DICT_DATA, 5, 7).abs(&h).unwrap();
1549        assert_eq!((ok.offset, ok.len), (105, 7));
1550    }
1551
1552    /// A root whose leaf count is as large as the payload allows but whose
1553    /// leaf entries are missing: refused, not reserved for.
1554    fn varints(vals: &[u64]) -> Vec<u8> {
1555        let mut v = Vec::new();
1556        for &x in vals {
1557            write_uvarint(&mut v, x);
1558        }
1559        v
1560    }
1561
1562    #[test]
1563    fn index_root_with_a_huge_leaf_count_is_refused() {
1564        // 1 permutation: si 0, n_tiles 1, data_off 0, data_len 0, nl = 60 ...
1565        let mut p = varints(&[1, 0, 1, 0, 0, 60]);
1566        p.resize(p.len() + 54, 0);
1567        assert!(decode_index_root(&p).is_err());
1568    }
1569
1570    #[test]
1571    fn text_root_with_a_huge_leaf_count_is_refused() {
1572        // empty scope, 1 token, nl = 60, then too few entries.
1573        let mut p = varints(&[0, 1, 60]);
1574        p.resize(p.len() + 50, 0);
1575        assert!(decode_text_root(&p).is_err());
1576    }
1577
1578    #[test]
1579    fn quad_root_with_a_huge_leaf_count_is_refused() {
1580        // graph_count, quad_count, block_graphs, empty p0, gdir_len, 1 order:
1581        // si 0, n_tiles 1, nl = 60, then too few entries.
1582        let mut p = varints(&[1, 1, 1, 0, 0, 1, 0, 1, 60]);
1583        p.resize(p.len() + 50, 0);
1584        assert!(decode_quad_root(&p).is_err());
1585    }
1586}