Skip to main content

rete_core/v06/
open.rs

1//! Reading a `0x06` file: the header and the root come with the first read;
2//! everything else is built lazily from them — the dictionary and the index as
3//! two-level directories whose leaves fault on demand and stay for the session,
4//! the text index likewise, the label index and the quad index through their
5//! own lazy machinery.
6
7use std::sync::Arc;
8
9use super::*;
10use crate::dict::{ChunkLeaf, ChunkedSection, LeafedChunks, SectionMeta};
11use crate::dictionary::Dictionary;
12use crate::file::{decompress_bounded, ByteRange, FileError};
13use crate::index::{GraphIndex, LeafFetch, LeafedTiles, TileLeaf, NUM_PERMS};
14use crate::reader::RangeReader;
15
16/// Coalescing gaps for the batched faults (as for `0x05`).
17const TILE_GAP: u64 = 4096;
18const DICT_GAP: u64 = 64 * 1024;
19
20/// An open `0x06` file: its header and root, and the reader.
21pub struct V06File {
22    /// The 128-byte header.
23    pub h: Header06,
24    /// The decoded root block.
25    pub root: Root06,
26    /// Where everything else is read from.
27    pub reader: Arc<dyn RangeReader + Send + Sync>,
28}
29
30/// Read `(offset, len)` ranges as one coalesced batch.
31fn read_coalesced(
32    r: &(dyn RangeReader + Send + Sync),
33    ranges: &[(u64, u64)],
34    gap: u64,
35) -> Option<Vec<Vec<u8>>> {
36    let rs: Vec<ByteRange> = ranges
37        .iter()
38        .map(|&(offset, len)| ByteRange { offset, len })
39        .collect();
40    crate::file::read_coalesced(r, &rs, gap)
41}
42
43impl V06File {
44    /// Open from `first`, the file's leading bytes (at least the header; the
45    /// root is read from it when it fits, else fetched).
46    pub fn open(
47        reader: Arc<dyn RangeReader + Send + Sync>,
48        first: &[u8],
49    ) -> Result<Self, FileError> {
50        let h = Header06::from_bytes(first)?;
51        if h.file_len != reader.len() {
52            return Err(FileError::Container(
53                "0x06 file length does not match the header (truncated?)",
54            ));
55        }
56        let rs = HEADER_LEN;
57        let re = rs + h.root_len as usize;
58        let root_bytes = if re <= first.len() {
59            first[rs..re].to_vec()
60        } else {
61            reader.read_at(rs as u64, h.root_len as u64)?
62        };
63        if digest16(&root_bytes) != h.root_digest {
64            return Err(FileError::Container("0x06 root digest mismatch"));
65        }
66        let root = Root06::decode(&root_bytes, h.internal_codec)?;
67        for s in &root.sections {
68            let a = s.abs(&h)?;
69            let end = a.offset + a.len;
70            let ok = if kind::is_front(s.kind) {
71                a.offset >= h.front_base() && end <= h.data_offset
72            } else {
73                a.offset >= h.data_offset && end <= h.file_len - FOOTER.len() as u64
74            };
75            if !ok {
76                return Err(FileError::Container("0x06 section out of its region"));
77            }
78        }
79        Ok(V06File { h, root, reader })
80    }
81
82    /// The decompression bound of section `k`'s blocks: its recorded largest
83    /// block, or [`BLOCK_CEILING`] where none is recorded.
84    pub fn bound(&self, k: u8) -> u64 {
85        section_bound(&self.root, k)
86    }
87
88    /// The absolute range of section `k`, if present.
89    pub fn section(&self, k: u8) -> Option<ByteRange> {
90        self.root.section(k).and_then(|s| s.abs(&self.h).ok())
91    }
92
93    /// A batched leaf fetcher over this file's reader (leaves are stored with
94    /// the internal codec; decoding happens in the directory types).
95    pub fn fetch(&self) -> LeafFetch {
96        let r = self.reader.clone();
97        Arc::new(move |ranges: &[(u64, u64)]| read_coalesced(&*r, ranges, 0))
98    }
99
100    /// The Dataset Card (decompressed), if the file has one.
101    pub fn card(&self) -> Result<Option<Vec<u8>>, FileError> {
102        let r = self.h.card_range();
103        if r.len == 0 {
104            return Ok(None);
105        }
106        let stored = self.reader.read_at(r.offset, r.len)?;
107        if digest16(&stored) != self.h.card_digest {
108            return Err(FileError::Container("0x06 card digest mismatch"));
109        }
110        Ok(Some(decompress_bounded(
111            self.h.internal_codec,
112            &stored,
113            CARD_MAX,
114        )?))
115    }
116
117    /// The build record (raw), if the file has one.
118    pub fn build_record(&self) -> Result<Option<Vec<u8>>, FileError> {
119        let r = self.h.build_range();
120        if r.len == 0 {
121            return Ok(None);
122        }
123        Ok(Some(self.reader.read_at(r.offset, r.len)?))
124    }
125
126    /// The stats + schema section, as the `0x05` SCHEMA section bytes
127    /// (digest-prefixed) its decoder takes.
128    pub fn schema_section_bytes(&self) -> Option<Vec<u8>> {
129        let r = self.section(kind::SCHEMA)?;
130        let stored = self.reader.read_at(r.offset, r.len).ok()?;
131        let body =
132            decompress_bounded(self.h.internal_codec, &stored, self.bound(kind::SCHEMA)).ok()?;
133        let mut out = digest16(&body).to_vec();
134        out.extend_from_slice(&body);
135        Some(out)
136    }
137
138    /// The decoded stats + schema section.
139    pub fn schema(&self) -> Option<crate::schema_section::SchemaMeta> {
140        crate::schema_section::SchemaMeta::decode(&self.schema_section_bytes()?).ok()
141    }
142
143    /// The community pyramid's bytes.
144    pub fn pyramid_bytes(&self) -> Option<Vec<u8>> {
145        let r = self.section(kind::PYRAMID)?;
146        self.reader.read_at(r.offset, r.len).ok()
147    }
148
149    /// The four dictionary sections, two-level, chunks faulted on demand.
150    /// `want[si] == false` leaves that section empty (a search-only open).
151    pub fn dictionary(&self, want: [bool; 4]) -> Result<Dictionary, FileError> {
152        let unit = self
153            .root
154            .unit(tag::DICT)
155            .ok_or(FileError::Container("0x06 file has no dictionary"))?;
156        let roots = decode_dict_root(unit)?;
157        let leaves_base = self.section(kind::DICT_LEAVES).map_or(0, |r| r.offset);
158        let data_base = self.section(kind::DICT_DATA).map_or(0, |r| r.offset);
159        let cache = crate::chunk_cache::ChunkCache::unlimited_arc();
160        let codec = self.h.dict_codec;
161        let max = self.bound(kind::DICT_DATA);
162        let mut dirs: Vec<Option<Arc<LeafedChunks>>> = Vec::with_capacity(4);
163        let mut sections = Vec::with_capacity(4);
164        for (si, s) in roots.iter().enumerate() {
165            let meta = SectionMeta {
166                term_count: if want[si] { s.term_count } else { 0 },
167                restart_interval: s.restart_interval.max(1),
168                restart_offsets: Vec::new(),
169            };
170            if !want[si] {
171                sections.push(ChunkedSection::from_parts(
172                    meta,
173                    Vec::new(),
174                    None,
175                    cache.clone(),
176                    si as u8,
177                ));
178                dirs.push(None);
179                continue;
180            }
181            let leaves: Vec<ChunkLeaf> = s
182                .leaves
183                .iter()
184                .map(|(l, run, coff, key)| ChunkLeaf {
185                    first: l.first,
186                    n: l.n,
187                    first_run: *run,
188                    key: key.clone(),
189                    leaf: (leaves_base + l.off, l.len),
190                    chunk_base: data_base + s.data_off + coff,
191                })
192                .collect();
193            let dir = Arc::new(
194                LeafedChunks::new(leaves, s.n_chunks, self.fetch(), self.h.internal_codec)
195                    .with_max_block(self.bound(kind::DICT_LEAVES)),
196            );
197            let (r, d) = (self.reader.clone(), dir.clone());
198            let loader: crate::dict::ChunkLoader = Box::new(move |ci| {
199                let (off, len) = d.range(ci)?;
200                let b = r.read_at(off, len).ok()?;
201                decompress_bounded(codec, &b, max).ok()
202            });
203            let (r, d) = (self.reader.clone(), dir.clone());
204            let bulk: crate::dict::ChunkBulkLoader = Box::new(move |cis| {
205                d.ensure_leaves(&d.leaves_of(cis));
206                let ranges: Option<Vec<(u64, u64)>> = cis.iter().map(|&ci| d.range(ci)).collect();
207                let blobs = read_coalesced(&*r, &ranges?, DICT_GAP)?;
208                blobs
209                    .iter()
210                    .map(|b| decompress_bounded(codec, b, max).ok())
211                    .collect()
212            });
213            sections.push(
214                ChunkedSection::leafed(meta, dir.clone(), loader, cache.clone(), si as u8)
215                    .with_bulk_loader(bulk),
216            );
217            dirs.push(Some(dir));
218        }
219        let arr: [ChunkedSection; 4] = sections
220            .try_into()
221            .map_err(|_| FileError::Container("expected 4 dictionary sections"))?;
222        let r = self.reader.clone();
223        let multi: crate::dict::MultiChunkBulkLoader = Box::new(move |pairs| {
224            // Fault every section's leaves first (one batch), then the chunks.
225            let mut per: [Vec<usize>; 4] = Default::default();
226            for &(si, ci) in pairs {
227                if let Some(v) = per.get_mut(si as usize) {
228                    v.push(ci);
229                }
230            }
231            let mut leaf_ranges: Vec<(u64, u64)> = Vec::new();
232            let _ = &mut leaf_ranges;
233            for (si, cis) in per.iter().enumerate() {
234                if let Some(Some(d)) = dirs.get(si) {
235                    d.ensure_leaves(&d.leaves_of(cis));
236                }
237            }
238            let ranges: Option<Vec<(u64, u64)>> = pairs
239                .iter()
240                .map(|&(si, ci)| dirs.get(si as usize)?.as_ref()?.range(ci))
241                .collect();
242            let blobs = read_coalesced(&*r, &ranges?, DICT_GAP)?;
243            blobs
244                .iter()
245                .map(|b| decompress_bounded(codec, b, max).ok())
246                .collect()
247        });
248        let mut dict = Dictionary::from_chunked_sections(arr).with_multi_bulk_loader(multi);
249        dict.set_has_quoted_triples(self.h.flags & F_QUOTED != 0);
250        Ok(dict)
251    }
252
253    /// The default graph's index, two-level, tiles faulted on demand.
254    pub fn index(&self, read_concurrency: usize) -> Result<GraphIndex, FileError> {
255        let unit = self
256            .root
257            .unit(tag::INDEX)
258            .ok_or(FileError::Container("0x06 file has no index"))?;
259        let perms_root = decode_index_root(unit)?;
260        let leaves_base = self.section(kind::INDEX_LEAVES).map_or(0, |r| r.offset);
261        let data_base = self.section(kind::INDEX_DATA).map_or(0, |r| r.offset);
262        let perms = self.h.perms();
263        let mut lists: [Option<Arc<LeafedTiles>>; NUM_PERMS] = Default::default();
264        for p in &perms_root {
265            if p.si >= NUM_PERMS {
266                return Err(FileError::Container("0x06 index: bad permutation"));
267            }
268            let leaves: Vec<TileLeaf> = p
269                .leaves
270                .iter()
271                .map(|(l, mn, mx, toff)| TileLeaf {
272                    first: l.first,
273                    n: l.n,
274                    min_a: *mn,
275                    max_a: *mx,
276                    leaf: (leaves_base + l.off, l.len),
277                    tile_base: data_base + p.data_off + toff,
278                })
279                .collect();
280            lists[p.si] = Some(Arc::new(
281                LeafedTiles::new(
282                    leaves,
283                    p.n_tiles,
284                    p.data_len,
285                    self.fetch(),
286                    self.h.internal_codec,
287                )
288                .with_max_block(self.bound(kind::INDEX_LEAVES)),
289            ));
290        }
291        for perm in perms.iter() {
292            if lists[perm.section_index()].is_none() {
293                return Err(FileError::Container(
294                    "0x06 index lacks a stored permutation",
295                ));
296            }
297        }
298        let codec = self.h.block_codec;
299        let max = self.bound(kind::INDEX_DATA);
300        let (r, l) = (self.reader.clone(), lists.clone());
301        let loader: crate::index::TileLoader = Box::new(move |si, ti| {
302            let (off, len) = l.get(si)?.as_ref()?.range(ti)?;
303            let b = r.read_at(off, len).ok()?;
304            decompress_bounded(codec, &b, max).ok()
305        });
306        let (r, l) = (self.reader.clone(), lists.clone());
307        let bulk: crate::index::TileBulkLoader = Box::new(move |si, tis| {
308            let list = l.get(si)?.as_ref()?;
309            if let (Some(&a), Some(&b)) = (tis.first(), tis.last()) {
310                list.ensure_span(a, b + 1);
311            }
312            let ranges: Option<Vec<(u64, u64)>> = tis.iter().map(|&ti| list.range(ti)).collect();
313            let blobs = read_coalesced(&*r, &ranges?, TILE_GAP)?;
314            blobs
315                .iter()
316                .map(|b| decompress_bounded(codec, b, max).ok())
317                .collect()
318        });
319        let mut index = GraphIndex::from_leafed(lists, perms, loader).with_bulk_loader(bulk);
320        index.set_read_concurrency(read_concurrency);
321        Ok(index)
322    }
323
324    /// The text index (leafed token table), if the file has one.
325    pub fn text_index(&self) -> Option<Result<crate::text_index::TextIndex, FileError>> {
326        let unit = self.root.unit(tag::TEXT)?;
327        Some((|| {
328            let t = decode_text_root(unit)?;
329            let leaves_base = self.section(kind::TEXT_LEAVES).map_or(0, |r| r.offset);
330            let data = self
331                .section(kind::TEXT_DATA)
332                .unwrap_or(ByteRange { offset: 0, len: 0 });
333            let leaves = t
334                .leaves
335                .iter()
336                .map(|(l, tok)| crate::text_index::TokenLeaf {
337                    first: l.first,
338                    n: l.n,
339                    first_token: String::from_utf8_lossy(tok).into_owned(),
340                    leaf: (leaves_base + l.off, l.len),
341                })
342                .collect();
343            let tokens = Arc::new(
344                crate::text_index::LeafedTokens::new(
345                    leaves,
346                    t.n_tokens,
347                    self.fetch(),
348                    self.h.internal_codec,
349                )
350                .with_max_block(self.bound(kind::TEXT_LEAVES)),
351            );
352            let r = self.reader.clone();
353            let loader = Box::new(move |off: u64, len: u64| {
354                if off.checked_add(len)? > data.len {
355                    return None;
356                }
357                r.read_at(data.offset + off, len).ok()
358            });
359            crate::text_index::TextIndex::leafed(tokens, &t.scope, loader)
360        })())
361    }
362
363    /// Stored bytes of the token table (all leaves) — what a substring search
364    /// would read; a word lookup reads one leaf.
365    pub fn token_table_bytes(&self) -> Option<u64> {
366        self.root.unit(tag::TEXT)?;
367        Some(self.section(kind::TEXT_LEAVES).map_or(0, |r| r.len))
368    }
369
370    /// The label index, if the file has one.
371    pub fn label_index(&self) -> Option<Result<crate::label_index::LabelIndex, FileError>> {
372        let unit = self.root.unit(tag::LABELS)?;
373        Some((|| {
374            let l = decode_label_root(unit)?;
375            let data = self
376                .section(kind::LABEL_DATA)
377                .unwrap_or(ByteRange { offset: 0, len: 0 });
378            let nodes = self
379                .section(kind::LABEL_LEAVES)
380                .unwrap_or(ByteRange { offset: 0, len: 0 });
381            if data.len != l.data_len {
382                return Err(FileError::Container("0x06 label data length mismatch"));
383            }
384            let head_len = l.head.len() as u64;
385            let section_len = head_len + data.len + nodes.len;
386            let r = self.reader.clone();
387            let head = l.head.clone();
388            let loader = Box::new(move |off: u64, len: u64| -> Option<Vec<u8>> {
389                let end = off.checked_add(len)?;
390                if end <= head_len {
391                    return head.get(off as usize..end as usize).map(<[u8]>::to_vec);
392                }
393                let bo = off.checked_sub(head_len)?;
394                if bo + len <= data.len {
395                    r.read_at(data.offset + bo, len).ok()
396                } else if bo >= data.len && bo - data.len + len <= nodes.len {
397                    r.read_at(nodes.offset + (bo - data.len), len).ok()
398                } else {
399                    None
400                }
401            });
402            let max = self
403                .bound(kind::LABEL_LEAVES)
404                .max(self.bound(kind::LABEL_DATA));
405            crate::label_index::LabelIndex::from_head(
406                &l.head,
407                section_len,
408                self.h.block_codec,
409                loader,
410            )
411            .map(|li| li.with_max_block(max))
412        })())
413    }
414
415    /// The label index's entry count, from the root alone.
416    pub fn label_entry_count(&self) -> Option<u64> {
417        let l = decode_label_root(self.root.unit(tag::LABELS)?).ok()?;
418        crate::label_index::LabelIndexPreamble::parse(&l.head)
419            .ok()
420            .map(|p| p.entry_count)
421    }
422
423    /// The quad index's root, if the file has named graphs.
424    pub fn quad_root(&self) -> Option<Result<QuadRoot, FileError>> {
425        self.root.unit(tag::QUAD).map(decode_quad_root)
426    }
427
428    /// The named graphs (quad index), if any.
429    pub(crate) fn quad(
430        &self,
431        read_concurrency: usize,
432    ) -> Option<Result<crate::quad::QuadNamed, FileError>> {
433        let root = match self.quad_root()? {
434            Ok(r) => r,
435            Err(e) => return Some(Err(e)),
436        };
437        let leaves = self.section(kind::QUAD_LEAVES).map_or(0, |r| r.offset);
438        let data = self.section(kind::QUAD_DATA).map_or(0, |r| r.offset);
439        Some(
440            crate::quad::V06Quad::new(&root, leaves, data, self.h.internal_codec).map(|mut v| {
441                v.tile_max = self.bound(kind::QUAD_DATA);
442                v.leaf_max = self.bound(kind::QUAD_LEAVES);
443                crate::quad::QuadNamed::new_v06(
444                    self.reader.clone(),
445                    v,
446                    self.h.block_codec,
447                    self.h.perms(),
448                    read_concurrency,
449                )
450            }),
451        )
452    }
453
454    /// Verify every digest: the root, the card, each section (streamed), and
455    /// the content hash. `only` restricts the section checks to one kind.
456    /// `Ok(false)` names nothing; [`verify_report`](Self::verify_report) does.
457    pub fn verify(&self, only: Option<u8>) -> Result<bool, FileError> {
458        Ok(self.verify_report(only)?.is_empty())
459    }
460
461    /// Like [`verify`](Self::verify), returning what failed (empty = all good).
462    pub fn verify_report(&self, only: Option<u8>) -> Result<Vec<String>, FileError> {
463        const SLICE: u64 = 8 << 20;
464        let mut bad = Vec::new();
465        if content_hash(&self.h) != self.h.content_hash {
466            bad.push("content hash".to_string());
467        }
468        let card = self.h.card_range();
469        if only.is_none() && card.len > 0 {
470            let stored = self.reader.read_at(card.offset, card.len)?;
471            if digest16(&stored) != self.h.card_digest {
472                bad.push("card".to_string());
473            }
474        }
475        for s in &self.root.sections {
476            if only.is_some_and(|k| k != s.kind) {
477                continue;
478            }
479            let a = s.abs(&self.h)?;
480            let mut h = blake3::Hasher::new();
481            let mut at = a.offset;
482            while at < a.offset + a.len {
483                let n = (a.offset + a.len - at).min(SLICE);
484                h.update(&self.reader.read_at(at, n)?);
485                at += n;
486            }
487            if h.finalize().as_bytes()[..16] != s.digest {
488                bad.push(kind::name(s.kind).to_string());
489            }
490        }
491        if only.is_none() {
492            let foot = self
493                .reader
494                .read_at(self.h.file_len - FOOTER.len() as u64, FOOTER.len() as u64)?;
495            if foot != FOOTER {
496                bad.push("footer".to_string());
497            }
498        }
499        Ok(bad)
500    }
501}
502
503/// Is this file (by its first bytes) a `0x06` file?
504pub fn is_v06(first: &[u8]) -> bool {
505    first.len() > 4 && first[0..4] == crate::header::MAGIC && first[4] == VERSION
506}
507
508/// Read a `0x06` file's header and root through any reader (two reads at most;
509/// over a seeded or prefix-holding reader, none).
510pub fn read_front<R: RangeReader + ?Sized>(r: &R) -> Result<(Header06, Root06), FileError> {
511    let head = r.read_at(0, HEADER_LEN as u64)?;
512    let h = Header06::from_bytes(&head)?;
513    let root_bytes = r.read_at(HEADER_LEN as u64, h.root_len as u64)?;
514    if digest16(&root_bytes) != h.root_digest {
515        return Err(FileError::Container("0x06 root digest mismatch"));
516    }
517    let root = Root06::decode(&root_bytes, h.internal_codec)?;
518    Ok((h, root))
519}
520
521/// The stored bytes of section `k`, if present.
522pub fn read_section<R: RangeReader + ?Sized>(
523    r: &R,
524    h: &Header06,
525    root: &Root06,
526    k: u8,
527) -> Result<Option<Vec<u8>>, FileError> {
528    match root.section(k) {
529        Some(s) => {
530            let a = s.abs(h)?;
531            Ok(Some(r.read_at(a.offset, a.len)?))
532        }
533        None => Ok(None),
534    }
535}
536
537/// The card (decompressed) and the build record (raw): one read of the card
538/// region, which sits right after the root.
539#[allow(clippy::type_complexity)]
540pub fn read_card_build<R: RangeReader + ?Sized>(
541    r: &R,
542    h: &Header06,
543) -> Result<(Option<Vec<u8>>, Option<Vec<u8>>), FileError> {
544    let c = h.card_range();
545    let b = h.build_range();
546    if c.len == 0 && b.len == 0 {
547        return Ok((None, None));
548    }
549    let lo = if c.len == 0 {
550        b.offset
551    } else if b.len == 0 {
552        c.offset
553    } else {
554        c.offset.min(b.offset)
555    };
556    let hi = (c.offset + c.len).max(b.offset + b.len);
557    let span = r.read_at(lo, hi - lo)?;
558    let card = if c.len > 0 {
559        let stored = &span[(c.offset - lo) as usize..(c.offset - lo + c.len) as usize];
560        if digest16(stored) != h.card_digest {
561            return Err(FileError::Container("0x06 card digest mismatch"));
562        }
563        Some(decompress_bounded(h.internal_codec, stored, CARD_MAX)?)
564    } else {
565        None
566    };
567    let build = (b.len > 0)
568        .then(|| span[(b.offset - lo) as usize..(b.offset - lo + b.len) as usize].to_vec());
569    Ok((card, build))
570}
571
572/// Section `k`'s decompression bound in `root`: its recorded largest block,
573/// or [`BLOCK_CEILING`].
574pub fn section_bound(root: &Root06, k: u8) -> u64 {
575    root.section(k)
576        .map(|s| s.max_block as u64)
577        .filter(|&m| m > 0)
578        .unwrap_or(BLOCK_CEILING)
579}
580
581/// The stats + schema section as the digest-prefixed bytes its decoder takes.
582pub fn read_schema_bytes<R: RangeReader + ?Sized>(r: &R) -> Result<Option<Vec<u8>>, FileError> {
583    let (h, root) = read_front(r)?;
584    let Some(stored) = read_section(r, &h, &root, kind::SCHEMA)? else {
585        return Ok(None);
586    };
587    let body = decompress_bounded(
588        h.internal_codec,
589        &stored,
590        section_bound(&root, kind::SCHEMA),
591    )?;
592    let mut out = digest16(&body).to_vec();
593    out.extend_from_slice(&body);
594    Ok(Some(out))
595}
596
597/// The community pyramid's bytes, if any.
598pub fn read_pyramid<R: RangeReader + ?Sized>(r: &R) -> Result<Option<Vec<u8>>, FileError> {
599    let (h, root) = read_front(r)?;
600    read_section(r, &h, &root, kind::PYRAMID)
601}
602
603/// Stored bytes of the text index's token table (all leaves).
604pub fn read_token_table_bytes<R: RangeReader + ?Sized>(r: &R) -> Option<u64> {
605    let (_, root) = read_front(r).ok()?;
606    root.unit(tag::TEXT)?;
607    Some(root.section(kind::TEXT_LEAVES).map_or(0, |s| s.len))
608}
609
610/// A reader holding only some ranges of a file in memory (the borrowed-reader
611/// opens of a `0x06` file read what they need into one of these).
612pub struct SparseReader {
613    len: u64,
614    parts: Vec<(u64, Vec<u8>)>,
615}
616
617impl SparseReader {
618    /// Fetch `ranges` from `r` (plus the first bytes `first`) into memory —
619    /// the ones `first` does not already hold, in one batch.
620    pub fn fetch<R: RangeReader + ?Sized>(
621        r: &R,
622        first: Vec<u8>,
623        ranges: &[ByteRange],
624    ) -> Result<Self, FileError> {
625        let held = first.len() as u64;
626        let want: Vec<(u64, u64)> = ranges
627            .iter()
628            .filter(|g| g.len > 0 && g.offset.saturating_add(g.len) > held)
629            .map(|g| (g.offset, g.len))
630            .collect();
631        let mut parts = vec![(0u64, first)];
632        if !want.is_empty() {
633            for ((o, _), b) in want.iter().zip(r.read_many(&want)?) {
634                parts.push((*o, b));
635            }
636        }
637        Ok(SparseReader {
638            len: r.len(),
639            parts,
640        })
641    }
642}
643
644/// A borrowed reader whose first bytes are already in memory: reads inside
645/// them cost nothing, the rest go to `r`.
646pub struct Prefixed<'a, R: RangeReader + ?Sized> {
647    /// The file's first bytes.
648    pub first: &'a [u8],
649    /// The reader behind them.
650    pub r: &'a R,
651}
652
653impl<R: RangeReader + ?Sized> RangeReader for Prefixed<'_, R> {
654    fn len(&self) -> u64 {
655        self.r.len()
656    }
657
658    fn read_at(&self, offset: u64, len: u64) -> std::io::Result<Vec<u8>> {
659        match offset.checked_add(len) {
660            Some(end) if end <= self.first.len() as u64 => {
661                Ok(self.first[offset as usize..end as usize].to_vec())
662            }
663            _ => self.r.read_at(offset, len),
664        }
665    }
666}
667
668impl RangeReader for SparseReader {
669    fn len(&self) -> u64 {
670        self.len
671    }
672
673    fn read_at(&self, offset: u64, len: u64) -> std::io::Result<Vec<u8>> {
674        for (o, b) in &self.parts {
675            if offset >= *o && offset + len <= o + b.len() as u64 {
676                let s = (offset - o) as usize;
677                return Ok(b[s..s + len as usize].to_vec());
678            }
679        }
680        Err(std::io::Error::other("0x06: range not held by this reader"))
681    }
682}
683
684/// What the card's measured signals need from a `0x06` file's root: the text
685/// index's `(bytes, token-table bytes)` and the label index's `(bytes, entries)`.
686#[allow(clippy::type_complexity)]
687pub fn read_signals<R: RangeReader + ?Sized>(
688    r: &R,
689) -> Result<(Option<(u64, u64)>, Option<(u64, Option<u64>)>), FileError> {
690    let (_, root) = read_front(r)?;
691    let len = |k: u8| root.section(k).map_or(0, |s| s.len);
692    let text = root.unit(tag::TEXT).map(|_| {
693        (
694            len(kind::TEXT_LEAVES) + len(kind::TEXT_DATA),
695            len(kind::TEXT_LEAVES),
696        )
697    });
698    let labels = match root.unit(tag::LABELS) {
699        None => None,
700        Some(u) => {
701            let l = decode_label_root(u)?;
702            let entries = crate::label_index::LabelIndexPreamble::parse(&l.head)
703                .ok()
704                .map(|p| p.entry_count);
705            Some((
706                l.head.len() as u64 + len(kind::LABEL_DATA) + len(kind::LABEL_LEAVES),
707                entries,
708            ))
709        }
710    };
711    Ok((text, labels))
712}