Skip to main content

rete_core/
label_index.rs

1//! The **LABEL_INDEX** section (kind 8, SPEC §6.4): every labelled subject's
2//! display label, sorted for prefix search, in a multi-level table that a
3//! remote reader walks with a handful of small range reads.
4//!
5//! It replaces the label block that used to sit inside the pyramid-meta
6//! (`meta.rs` `LABELIDX_V1`). That block was fetched whole with the pyramid, so
7//! it had to stay small — 8,192 entries, ranked by subject out-degree — and a
8//! file without a pyramid had none. This section has no cap and no ranking, is
9//! written by any build that asks for it (`rete build --label-index`, including
10//! `--no-pyramid` and the memory-bounded build), and costs a first search one
11//! section head, one directory node per level and one data block.
12//!
13//! ## Layout
14//!
15//! ```text
16//! off  size
17//! 0    16   digest: blake3(section[16..])[..16]  (the section is outside the header hash)
18//! 16   1    version (= 1)
19//! 17   1    levels: directory levels below the root (0 = the root lists data blocks)
20//! 18   2    reserved (0)
21//! 20   8    entry_count, u64 LE
22//! 28   4    root_len, u32 LE
23//! 32   root_len         root node (block codec)
24//! 32+root_len …         body: data blocks, then the directory nodes level by level;
25//!                       every offset in a node is relative to the body start
26//! node  (decompressed): varint n; per child: varint shared, varint suffix_len, suffix
27//!                       (the child's first key, front-coded), varint off, varint len, varint entries
28//! block (decompressed): varint n; per entry: varint shared, varint suffix_len, suffix  (label)
29//!                                            varint shared, varint suffix_len, suffix  (subject term)
30//! ```
31//!
32//! An entry's **key** is its label lowercased ([`label_key`]); entries are sorted
33//! by `(key, subject)`, one per `(subject, key)`. The subject is stored as its
34//! dictionary term, so a search never reads the dictionary.
35//!
36//! ## Which labels (index-only; the triples are never changed)
37//!
38//! - **L1**, every literal of [`DIRECT_LABEL_PREDICATES`].
39//! - **L2**, for a subject with no L1 label only: `"family, given"` from the
40//!   smallest value of [`FAMILY_NAME_PREDICATES`] and of [`GIVEN_NAME_PREDICATES`]
41//!   (or whichever of the two exists) — people who carry only `foaf:givenName`
42//!   / `foaf:familyName` get a label.
43//! - Normalized by [`normalize_label`]: escapes resolved, whitespace collapsed,
44//!   trimmed, truncated to [`MAX_LABEL_BYTES`].
45
46use std::collections::HashMap;
47use std::io::Write;
48use std::sync::{Arc, Mutex};
49
50use crate::file::{compress, decompress, FileError};
51use crate::varint::{read_uvarint, write_uvarint};
52
53/// The predicates whose literals are a subject's display label (rule L1). The
54/// same list the pyramid's label block used. Angle-bracketed, as stored.
55pub const DIRECT_LABEL_PREDICATES: &[&str] = &[
56    "<http://www.w3.org/2000/01/rdf-schema#label>",
57    "<http://www.w3.org/2004/02/skos/core#prefLabel>",
58    "<http://www.w3.org/2004/02/skos/core#altLabel>",
59    "<http://xmlns.com/foaf/0.1/name>",
60    "<http://purl.org/dc/terms/title>",
61    "<http://purl.org/dc/elements/1.1/title>",
62    "<http://schema.org/name>",
63];
64
65/// Family-name predicates for rule L2 (composed person names).
66pub const FAMILY_NAME_PREDICATES: &[&str] = &[
67    "<http://xmlns.com/foaf/0.1/familyName>",
68    "<http://xmlns.com/foaf/0.1/lastName>",
69    "<http://xmlns.com/foaf/0.1/surname>",
70    "<http://xmlns.com/foaf/0.1/family_name>",
71    "<http://schema.org/familyName>",
72    "<http://www.w3.org/2006/vcard/ns#family-name>",
73];
74
75/// Given-name predicates for rule L2 (composed person names).
76pub const GIVEN_NAME_PREDICATES: &[&str] = &[
77    "<http://xmlns.com/foaf/0.1/givenName>",
78    "<http://xmlns.com/foaf/0.1/firstName>",
79    "<http://xmlns.com/foaf/0.1/givenname>",
80    "<http://schema.org/givenName>",
81    "<http://www.w3.org/2006/vcard/ns#given-name>",
82];
83
84/// Longest label stored, in bytes; longer ones are cut at a char boundary.
85pub const MAX_LABEL_BYTES: usize = 512;
86
87/// Section format version written by this crate.
88const VERSION: u8 = 1;
89/// Bytes before the root node.
90pub(crate) const PREAMBLE_LEN: usize = 32;
91/// Length of the leading digest.
92const DIGEST_LEN: usize = 16;
93/// Uncompressed size a data block is closed at.
94const BLOCK_TARGET: usize = 16 * 1024;
95/// Most children in one directory node (and in the root).
96const NODE_FANOUT: usize = 1024;
97/// What a lazy open reads first: the preamble and, normally, the whole root.
98pub(crate) const HEAD_READ: u64 = 64 * 1024;
99/// Most decoded directory nodes a lazy index keeps.
100const NODE_CACHE_CAP: usize = 256;
101/// Most directory levels a reader accepts. Five levels of 1,024 children over
102/// 16 KiB blocks address far more than any file can hold; a larger value is a
103/// crafted or corrupt preamble.
104const MAX_LEVELS: u8 = 5;
105/// Most nodes and blocks one search may fetch. A well-formed index needs one
106/// node per level plus the blocks of the matching run; the bound stops a
107/// crafted directory (children pointing back at their parent) from turning a
108/// search into an unbounded walk.
109const MAX_FETCHES_PER_SEARCH: usize = 1 << 16;
110
111/// The search key of a label: its Unicode lowercase form.
112pub fn label_key(label: &str) -> String {
113    label.to_lowercase()
114}
115
116/// Normalize a literal's lexical value into a stored label: whitespace runs
117/// collapsed to one space, trimmed, cut to [`MAX_LABEL_BYTES`] at a char
118/// boundary. `None` for an empty result.
119pub fn normalize_label(lexical: &str) -> Option<String> {
120    let mut out = String::with_capacity(lexical.len().min(MAX_LABEL_BYTES));
121    for word in lexical.split_whitespace() {
122        if !out.is_empty() {
123            out.push(' ');
124        }
125        out.push_str(word);
126        if out.len() >= MAX_LABEL_BYTES {
127            break;
128        }
129    }
130    if out.len() > MAX_LABEL_BYTES {
131        let mut cut = MAX_LABEL_BYTES;
132        while !out.is_char_boundary(cut) {
133            cut -= 1;
134        }
135        out.truncate(cut);
136        while out.ends_with(' ') {
137            out.pop();
138        }
139    }
140    (!out.is_empty()).then_some(out)
141}
142
143/// What a label-candidate value is, by its predicate.
144#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
145pub(crate) enum Slot {
146    /// An L1 label.
147    Direct = 0,
148    /// An L2 family name.
149    Family = 1,
150    /// An L2 given name.
151    Given = 2,
152}
153
154impl Slot {
155    /// Read back from the memory-bounded build's spill (native only).
156    #[cfg(not(target_arch = "wasm32"))]
157    pub(crate) fn from_u8(v: u8) -> Option<Slot> {
158        match v {
159            0 => Some(Slot::Direct),
160            1 => Some(Slot::Family),
161            2 => Some(Slot::Given),
162            _ => None,
163        }
164    }
165}
166
167/// The [`Slot`] a predicate term feeds, if any.
168pub(crate) fn slot_of_predicate(pred: &str) -> Option<Slot> {
169    if DIRECT_LABEL_PREDICATES.contains(&pred) {
170        Some(Slot::Direct)
171    } else if FAMILY_NAME_PREDICATES.contains(&pred) {
172        Some(Slot::Family)
173    } else if GIVEN_NAME_PREDICATES.contains(&pred) {
174        Some(Slot::Given)
175    } else {
176        None
177    }
178}
179
180/// The normalized label value of an object term, when it is a literal.
181pub(crate) fn candidate_value(object_term: &str) -> Option<String> {
182    let lex = crate::terms::literal_lexical(object_term)?;
183    normalize_label(&lex)
184}
185
186/// One subject's candidate values, gathered before the rules run.
187#[derive(Debug, Default)]
188pub(crate) struct SubjectCandidates {
189    pub direct: Vec<String>,
190    pub family: Vec<String>,
191    pub given: Vec<String>,
192}
193
194impl SubjectCandidates {
195    pub(crate) fn push(&mut self, slot: Slot, value: String) {
196        match slot {
197            Slot::Direct => self.direct.push(value),
198            Slot::Family => self.family.push(value),
199            Slot::Given => self.given.push(value),
200        }
201    }
202
203    pub(crate) fn is_empty(&self) -> bool {
204        self.direct.is_empty() && self.family.is_empty() && self.given.is_empty()
205    }
206
207    /// Apply L1/L2: the `(key, label)` pairs this subject is indexed under, one
208    /// per key, keeping the smallest label for a key. Order-independent.
209    pub(crate) fn labels(&self) -> Vec<(String, String)> {
210        let mut by_key: std::collections::BTreeMap<String, String> = Default::default();
211        if !self.direct.is_empty() {
212            for v in &self.direct {
213                let k = label_key(v);
214                match by_key.get_mut(&k) {
215                    Some(cur) if v.as_str() < cur.as_str() => *cur = v.clone(),
216                    Some(_) => {}
217                    None => {
218                        by_key.insert(k, v.clone());
219                    }
220                }
221            }
222        } else {
223            let family = self.family.iter().min();
224            let given = self.given.iter().min();
225            let composed = match (family, given) {
226                (Some(f), Some(g)) => format!("{f}, {g}"),
227                (Some(f), None) => f.clone(),
228                (None, Some(g)) => g.clone(),
229                (None, None) => return Vec::new(),
230            };
231            if let Some(label) = normalize_label(&composed) {
232                by_key.insert(label_key(&label), label);
233            }
234        }
235        by_key.into_iter().collect()
236    }
237}
238
239/// The label entries of an in-memory graph: `(key, subject term, label)`,
240/// sorted by `(key, subject)`. `triples` must be the **deduplicated**
241/// default-graph id-triples **sorted by subject** (SPO order), which is what
242/// lets one pass group each subject's candidates.
243pub(crate) fn entries_from_sorted_triples(
244    dict: &crate::Dictionary,
245    triples: &[(u32, u32, u32)],
246) -> Vec<(String, String, String)> {
247    let mut slot_of: HashMap<u32, Slot> = HashMap::new();
248    for list in [
249        DIRECT_LABEL_PREDICATES,
250        FAMILY_NAME_PREDICATES,
251        GIVEN_NAME_PREDICATES,
252    ] {
253        for p in list {
254            if let (Some(pid), Some(slot)) = (dict.predicate_id(p), slot_of_predicate(p)) {
255                slot_of.insert(pid, slot);
256            }
257        }
258    }
259    let mut out: Vec<(String, String, String)> = Vec::new();
260    if slot_of.is_empty() {
261        return out;
262    }
263    let mut cur: Option<u32> = None;
264    let mut cands = SubjectCandidates::default();
265    let flush = |s: u32, cands: &mut SubjectCandidates, out: &mut Vec<_>| {
266        if cands.is_empty() {
267            return;
268        }
269        if let Some(subject) = dict.subject_term(s) {
270            for (key, label) in cands.labels() {
271                out.push((key, subject.clone(), label));
272            }
273        }
274        *cands = SubjectCandidates::default();
275    };
276    for &(s, p, o) in triples {
277        if cur != Some(s) {
278            if let Some(prev) = cur {
279                flush(prev, &mut cands, &mut out);
280            }
281            cur = Some(s);
282        }
283        let Some(&slot) = slot_of.get(&p) else {
284            continue;
285        };
286        if let Some(v) = dict.object_term(o).and_then(|t| candidate_value(&t)) {
287            cands.push(slot, v);
288        }
289    }
290    if let Some(prev) = cur {
291        flush(prev, &mut cands, &mut out);
292    }
293    out.sort_unstable_by(|a, b| (&a.0, &a.1).cmp(&(&b.0, &b.1)));
294    out.dedup_by(|b, a| a.0 == b.0 && a.1 == b.1);
295    out
296}
297
298// ---------------------------------------------------------------------------
299// Writer
300// ---------------------------------------------------------------------------
301
302/// One directory entry: a child's first key, its byte range in the body, and
303/// how many entries it covers.
304#[derive(Debug, Clone, PartialEq, Eq)]
305struct DirEntry {
306    first_key: String,
307    off: u64,
308    len: u64,
309    entries: u64,
310}
311
312fn shared_prefix(a: &str, b: &str) -> usize {
313    let mut n = a
314        .as_bytes()
315        .iter()
316        .zip(b.as_bytes())
317        .take_while(|(x, y)| x == y)
318        .count();
319    while n > 0 && !b.is_char_boundary(n) {
320        n -= 1;
321    }
322    n
323}
324
325fn push_front_coded(out: &mut Vec<u8>, prev: &str, cur: &str) {
326    let shared = shared_prefix(prev, cur);
327    write_uvarint(out, shared as u64);
328    let suffix = &cur.as_bytes()[shared..];
329    write_uvarint(out, suffix.len() as u64);
330    out.extend_from_slice(suffix);
331}
332
333fn encode_node(children: &[DirEntry]) -> Vec<u8> {
334    let mut raw = Vec::new();
335    write_uvarint(&mut raw, children.len() as u64);
336    let mut prev = "";
337    for c in children {
338        push_front_coded(&mut raw, prev, &c.first_key);
339        write_uvarint(&mut raw, c.off);
340        write_uvarint(&mut raw, c.len);
341        write_uvarint(&mut raw, c.entries);
342        prev = &c.first_key;
343    }
344    raw
345}
346
347/// Streams sorted entries into a LABEL_INDEX section. Data blocks go to the
348/// `body` sink as they fill; the directory (≈ 40 B per 16 KiB block) stays in
349/// memory until [`finish`](Self::finish).
350pub(crate) struct LabelIndexWriter<W: Write> {
351    codec: u8,
352    body: W,
353    body_len: u64,
354    block: Vec<u8>,
355    block_n: u64,
356    block_first_key: Option<String>,
357    prev_label: String,
358    prev_subject: String,
359    last: Option<(String, String)>,
360    leaves: Vec<DirEntry>,
361    entry_count: u64,
362}
363
364/// A finished section, split so the external build can stream its body from
365/// disk: `head` is bytes `16..32 + root_len` (everything but the digest and
366/// the body).
367pub(crate) struct LabelIndexParts<W> {
368    pub head: Vec<u8>,
369    pub body: W,
370    /// Read by the memory-bounded build (native only), to skip an empty section.
371    #[cfg(not(target_arch = "wasm32"))]
372    pub entry_count: u64,
373}
374
375impl<W: Write> LabelIndexWriter<W> {
376    pub(crate) fn new(codec: u8, body: W) -> Self {
377        LabelIndexWriter {
378            codec,
379            body,
380            body_len: 0,
381            block: Vec::new(),
382            block_n: 0,
383            block_first_key: None,
384            prev_label: String::new(),
385            prev_subject: String::new(),
386            last: None,
387            leaves: Vec::new(),
388            entry_count: 0,
389        }
390    }
391
392    /// Add one entry. Entries must arrive in strictly increasing
393    /// `(key, subject)` order; a repeat of the previous `(key, subject)` is
394    /// dropped.
395    pub(crate) fn push(&mut self, key: &str, subject: &str, label: &str) -> std::io::Result<()> {
396        if let Some((lk, ls)) = &self.last {
397            match (lk.as_str(), ls.as_str()).cmp(&(key, subject)) {
398                std::cmp::Ordering::Equal => return Ok(()),
399                std::cmp::Ordering::Greater => {
400                    return Err(std::io::Error::other("label entries out of order"))
401                }
402                std::cmp::Ordering::Less => {}
403            }
404        }
405        self.last = Some((key.to_string(), subject.to_string()));
406        if self.block_first_key.is_none() {
407            self.block_first_key = Some(key.to_string());
408            self.prev_label.clear();
409            self.prev_subject.clear();
410        }
411        push_front_coded(&mut self.block, &self.prev_label, label);
412        push_front_coded(&mut self.block, &self.prev_subject, subject);
413        self.prev_label.clear();
414        self.prev_label.push_str(label);
415        self.prev_subject.clear();
416        self.prev_subject.push_str(subject);
417        self.block_n += 1;
418        self.entry_count += 1;
419        if self.block.len() >= BLOCK_TARGET {
420            self.close_block()?;
421        }
422        Ok(())
423    }
424
425    fn close_block(&mut self) -> std::io::Result<()> {
426        let Some(first_key) = self.block_first_key.take() else {
427            return Ok(());
428        };
429        let mut raw = Vec::with_capacity(self.block.len() + 10);
430        write_uvarint(&mut raw, self.block_n);
431        raw.extend_from_slice(&self.block);
432        let bytes = compress(self.codec, &raw);
433        self.body.write_all(&bytes)?;
434        self.leaves.push(DirEntry {
435            first_key,
436            off: self.body_len,
437            len: bytes.len() as u64,
438            entries: self.block_n,
439        });
440        self.body_len += bytes.len() as u64;
441        self.block.clear();
442        self.block_n = 0;
443        Ok(())
444    }
445
446    /// Close the last block, write the directory levels, and return the parts.
447    pub(crate) fn finish(mut self) -> std::io::Result<LabelIndexParts<W>> {
448        self.close_block()?;
449        let mut level = std::mem::take(&mut self.leaves);
450        let mut levels = 0u8;
451        while level.len() > NODE_FANOUT {
452            let mut parents = Vec::with_capacity(level.len() / NODE_FANOUT + 1);
453            for group in level.chunks(NODE_FANOUT) {
454                let bytes = compress(self.codec, &encode_node(group));
455                self.body.write_all(&bytes)?;
456                parents.push(DirEntry {
457                    first_key: group[0].first_key.clone(),
458                    off: self.body_len,
459                    len: bytes.len() as u64,
460                    entries: group.iter().map(|c| c.entries).sum(),
461                });
462                self.body_len += bytes.len() as u64;
463            }
464            level = parents;
465            levels += 1;
466        }
467        let root = compress(self.codec, &encode_node(&level));
468        let mut head = Vec::with_capacity(PREAMBLE_LEN - DIGEST_LEN + root.len());
469        head.push(VERSION);
470        head.push(levels);
471        head.extend_from_slice(&[0, 0]);
472        head.extend_from_slice(&self.entry_count.to_le_bytes());
473        head.extend_from_slice(&(root.len() as u32).to_le_bytes());
474        head.extend_from_slice(&root);
475        Ok(LabelIndexParts {
476            head,
477            body: self.body,
478            #[cfg(not(target_arch = "wasm32"))]
479            entry_count: self.entry_count,
480        })
481    }
482}
483
484/// Encode a complete LABEL_INDEX section from sorted `(key, subject, label)`
485/// entries. Empty input yields an empty `Vec` (no section).
486pub(crate) fn encode_section(entries: &[(String, String, String)], codec: u8) -> Vec<u8> {
487    if entries.is_empty() {
488        return Vec::new();
489    }
490    let mut w = LabelIndexWriter::new(codec, Vec::new());
491    for (k, s, l) in entries {
492        w.push(k, s, l).expect("in-memory label index write");
493    }
494    let parts = w.finish().expect("in-memory label index write");
495    seal(&parts.head, &parts.body)
496}
497
498/// Prepend the digest to `head ++ body`.
499pub(crate) fn seal(head: &[u8], body: &[u8]) -> Vec<u8> {
500    let mut h = blake3::Hasher::new();
501    h.update(head);
502    h.update(body);
503    let mut out = Vec::with_capacity(DIGEST_LEN + head.len() + body.len());
504    out.extend_from_slice(&h.finalize().as_bytes()[..DIGEST_LEN]);
505    out.extend_from_slice(head);
506    out.extend_from_slice(body);
507    out
508}
509
510// ---------------------------------------------------------------------------
511// Reader
512// ---------------------------------------------------------------------------
513
514/// The fixed preamble of a LABEL_INDEX section.
515#[derive(Debug, Clone, Copy, PartialEq, Eq)]
516pub struct LabelIndexPreamble {
517    /// Directory levels below the root.
518    pub levels: u8,
519    /// Number of `(subject, label)` entries.
520    pub entry_count: u64,
521    /// Byte length of the root node.
522    pub root_len: u32,
523}
524
525impl LabelIndexPreamble {
526    /// Parse the first `PREAMBLE_LEN` bytes of a section.
527    pub fn parse(bytes: &[u8]) -> Result<Self, FileError> {
528        if bytes.len() < PREAMBLE_LEN {
529            return Err(FileError::Container("label index: truncated preamble"));
530        }
531        if bytes[16] != VERSION {
532            return Err(FileError::Container("label index: unknown version"));
533        }
534        if bytes[17] > MAX_LEVELS {
535            return Err(FileError::Container("label index: too many levels"));
536        }
537        Ok(LabelIndexPreamble {
538            levels: bytes[17],
539            entry_count: u64::from_le_bytes(bytes[20..28].try_into().unwrap()),
540            root_len: u32::from_le_bytes(bytes[28..32].try_into().unwrap()),
541        })
542    }
543}
544
545/// Fetches `(offset, len)` within the section.
546type SectionLoader = Box<dyn Fn(u64, u64) -> Option<Vec<u8>> + Send + Sync>;
547
548enum Source {
549    Resident(Arc<[u8]>),
550    Lazy(SectionLoader),
551}
552
553/// A parsed LABEL_INDEX: the root resident, inner nodes and blocks on demand.
554pub struct LabelIndex {
555    preamble: LabelIndexPreamble,
556    root: Vec<DirEntry>,
557    body_base: u64,
558    section_len: u64,
559    codec: u8,
560    source: Source,
561    nodes: Mutex<HashMap<u64, Arc<Vec<DirEntry>>>>,
562    /// Decompression bound on a node or block (`u64::MAX` = none: `0x05`).
563    max_block: u64,
564}
565
566impl std::fmt::Debug for LabelIndex {
567    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
568        f.debug_struct("LabelIndex")
569            .field("preamble", &self.preamble)
570            .field("section_len", &self.section_len)
571            .finish()
572    }
573}
574
575/// Did a search reach its end, or must it go on?
576enum Walk {
577    Continue,
578    Stop,
579}
580
581impl LabelIndex {
582    /// Bound every node and block decompression at `max` bytes (format
583    /// `0x06`, from its root).
584    pub(crate) fn with_max_block(mut self, max: u64) -> Self {
585        self.max_block = max;
586        self
587    }
588
589    /// Parse a whole section held in memory, checking its digest.
590    pub fn from_section(section: &[u8], codec: u8) -> Result<Self, FileError> {
591        if !digest_ok(section) {
592            return Err(FileError::Container("label index: digest mismatch"));
593        }
594        let bytes: Arc<[u8]> = Arc::from(section);
595        Self::open(
596            &bytes[..],
597            section.len() as u64,
598            codec,
599            Source::Resident(bytes.clone()),
600        )
601    }
602
603    /// Open from the section's leading bytes (`head`, at least the preamble;
604    /// normally `HEAD_READ` bytes) and a loader for any range of the section.
605    /// The root is read from `head` when it fits, else fetched.
606    pub fn from_head(
607        head: &[u8],
608        section_len: u64,
609        codec: u8,
610        loader: Box<dyn Fn(u64, u64) -> Option<Vec<u8>> + Send + Sync>,
611    ) -> Result<Self, FileError> {
612        Self::open(head, section_len, codec, Source::Lazy(loader))
613    }
614
615    fn open(head: &[u8], section_len: u64, codec: u8, source: Source) -> Result<Self, FileError> {
616        let preamble = LabelIndexPreamble::parse(head)?;
617        let root_end = PREAMBLE_LEN as u64 + preamble.root_len as u64;
618        if root_end > section_len {
619            return Err(FileError::Container("label index: root overruns section"));
620        }
621        let root_bytes: Vec<u8> = if root_end as usize <= head.len() {
622            head[PREAMBLE_LEN..root_end as usize].to_vec()
623        } else {
624            match &source {
625                Source::Resident(b) => b
626                    .get(PREAMBLE_LEN..root_end as usize)
627                    .ok_or(FileError::Container("label index: root overruns section"))?
628                    .to_vec(),
629                Source::Lazy(load) => load(PREAMBLE_LEN as u64, preamble.root_len as u64)
630                    .ok_or(FileError::Container("label index: root unreadable"))?,
631            }
632        };
633        let root = decode_node(&decompress(codec, &root_bytes)?)?;
634        Ok(LabelIndex {
635            preamble,
636            root,
637            body_base: root_end,
638            section_len,
639            codec,
640            source,
641            nodes: Mutex::new(HashMap::new()),
642            max_block: u64::MAX,
643        })
644    }
645
646    /// Where the data blocks end in the body (the directory nodes follow
647    /// them, lowest level first): the offset of the leftmost level-1 node, or
648    /// the whole body on a one-level index. `fetch` reads `(offset, len)`
649    /// relative to the **section** start. What the `0x06` writer splits on.
650    pub(crate) fn v05_data_len(&self, fetch: &dyn Fn(u64, u64) -> Option<Vec<u8>>) -> Option<u64> {
651        if self.preamble.levels == 0 {
652            return Some(self.section_len - self.body_base);
653        }
654        let mut e = self.root.first()?.clone();
655        for _ in 1..self.preamble.levels {
656            let raw = fetch(self.body_base + e.off, e.len)?;
657            let node = decode_node(&decompress(self.codec, &raw).ok()?).ok()?;
658            e = node.first()?.clone();
659        }
660        Some(e.off)
661    }
662
663    /// Number of entries in the index.
664    pub fn entry_count(&self) -> u64 {
665        self.preamble.entry_count
666    }
667
668    /// Byte length of the whole section.
669    pub fn section_len(&self) -> u64 {
670        self.section_len
671    }
672
673    fn fetch(&self, body_off: u64, len: u64) -> Option<Vec<u8>> {
674        let off = self.body_base.checked_add(body_off)?;
675        let end = off.checked_add(len)?;
676        if end > self.section_len {
677            return None;
678        }
679        match &self.source {
680            Source::Resident(b) => b.get(off as usize..end as usize).map(<[u8]>::to_vec),
681            Source::Lazy(load) => load(off, len),
682        }
683    }
684
685    fn node(&self, e: &DirEntry) -> Option<Arc<Vec<DirEntry>>> {
686        if let Some(n) = self.nodes.lock().ok()?.get(&e.off) {
687            return Some(n.clone());
688        }
689        let raw =
690            crate::file::decompress_bounded(self.codec, &self.fetch(e.off, e.len)?, self.max_block)
691                .ok()?;
692        let node = Arc::new(decode_node(&raw).ok()?);
693        if let Ok(mut cache) = self.nodes.lock() {
694            if cache.len() >= NODE_CACHE_CAP {
695                cache.clear();
696            }
697            cache.insert(e.off, node.clone());
698        }
699        Some(node)
700    }
701
702    /// Entries whose label starts with `prefix`, compared case-insensitively,
703    /// as `(label, subject term)` in key order, at most `limit`. An empty
704    /// `prefix` lists the index from the start.
705    pub fn prefix_search(&self, prefix: &str, limit: usize) -> Vec<(String, String)> {
706        let needle = label_key(prefix);
707        let mut out = Vec::new();
708        if limit == 0 {
709            return out;
710        }
711        let root = self.root.clone();
712        let mut budget = MAX_FETCHES_PER_SEARCH;
713        let _ = self.walk(
714            &root,
715            self.preamble.levels,
716            &needle,
717            limit,
718            &mut out,
719            &mut budget,
720        );
721        out
722    }
723
724    fn walk(
725        &self,
726        children: &[DirEntry],
727        depth: u8,
728        needle: &str,
729        limit: usize,
730        out: &mut Vec<(String, String)>,
731        budget: &mut usize,
732    ) -> Walk {
733        // The run of keys starting with `needle` begins in the last child whose
734        // first key is <= needle (or the first child).
735        let start = children
736            .partition_point(|c| c.first_key.as_str() <= needle)
737            .saturating_sub(1);
738        for child in &children[start..] {
739            if child.first_key.as_str() > needle && !child.first_key.starts_with(needle) {
740                return Walk::Stop;
741            }
742            if *budget == 0 {
743                return Walk::Stop;
744            }
745            *budget -= 1;
746            let step = if depth == 0 {
747                self.scan_block(child, needle, limit, out)
748            } else {
749                match self.node(child) {
750                    Some(node) => self.walk(&node, depth - 1, needle, limit, out, budget),
751                    None => Walk::Stop,
752                }
753            };
754            if let Walk::Stop = step {
755                return Walk::Stop;
756            }
757        }
758        Walk::Continue
759    }
760
761    fn scan_block(
762        &self,
763        e: &DirEntry,
764        needle: &str,
765        limit: usize,
766        out: &mut Vec<(String, String)>,
767    ) -> Walk {
768        let Some(raw) = self
769            .fetch(e.off, e.len)
770            .and_then(|b| crate::file::decompress_bounded(self.codec, &b, self.max_block).ok())
771        else {
772            return Walk::Stop;
773        };
774        let Ok(entries) = decode_block(&raw) else {
775            return Walk::Stop;
776        };
777        for (label, subject) in entries {
778            let key = label_key(&label);
779            if key.starts_with(needle) {
780                out.push((label, subject));
781                if out.len() >= limit {
782                    return Walk::Stop;
783                }
784            } else if key.as_str() > needle {
785                return Walk::Stop;
786            }
787        }
788        Walk::Continue
789    }
790}
791
792/// Does a section's leading digest match the rest of it?
793pub(crate) fn digest_ok(section: &[u8]) -> bool {
794    if section.len() < DIGEST_LEN {
795        return false;
796    }
797    blake3::hash(&section[DIGEST_LEN..]).as_bytes()[..DIGEST_LEN] == section[..DIGEST_LEN]
798}
799
800struct Cursor<'a> {
801    b: &'a [u8],
802    pos: usize,
803}
804
805impl<'a> Cursor<'a> {
806    fn varint(&mut self) -> Result<u64, FileError> {
807        let (v, n) = read_uvarint(self.b.get(self.pos..).unwrap_or(&[]))
808            .ok_or(FileError::Container("label index: truncated varint"))?;
809        self.pos += n;
810        Ok(v)
811    }
812
813    fn front_coded(&mut self, prev: &str) -> Result<String, FileError> {
814        let shared = self.varint()? as usize;
815        let len = self.varint()? as usize;
816        let end = self
817            .pos
818            .checked_add(len)
819            .filter(|&e| e <= self.b.len())
820            .ok_or(FileError::Container("label index: string overruns"))?;
821        let base = prev
822            .get(..shared)
823            .ok_or(FileError::Container("label index: bad shared prefix"))?;
824        let suffix = std::str::from_utf8(&self.b[self.pos..end])
825            .map_err(|_| FileError::Container("label index: bad utf8"))?;
826        self.pos = end;
827        let mut s = String::with_capacity(base.len() + suffix.len());
828        s.push_str(base);
829        s.push_str(suffix);
830        Ok(s)
831    }
832}
833
834fn decode_node(raw: &[u8]) -> Result<Vec<DirEntry>, FileError> {
835    let mut c = Cursor { b: raw, pos: 0 };
836    let n = c.varint()? as usize;
837    let mut out = Vec::with_capacity(n.min(raw.len()));
838    let mut prev = String::new();
839    for _ in 0..n {
840        let first_key = c.front_coded(&prev)?;
841        let off = c.varint()?;
842        let len = c.varint()?;
843        let entries = c.varint()?;
844        prev.clone_from(&first_key);
845        out.push(DirEntry {
846            first_key,
847            off,
848            len,
849            entries,
850        });
851    }
852    Ok(out)
853}
854
855fn decode_block(raw: &[u8]) -> Result<Vec<(String, String)>, FileError> {
856    let mut c = Cursor { b: raw, pos: 0 };
857    let n = c.varint()? as usize;
858    let mut out = Vec::with_capacity(n.min(raw.len()));
859    let (mut pl, mut ps) = (String::new(), String::new());
860    for _ in 0..n {
861        let label = c.front_coded(&pl)?;
862        let subject = c.front_coded(&ps)?;
863        pl.clone_from(&label);
864        ps.clone_from(&subject);
865        out.push((label, subject));
866    }
867    Ok(out)
868}
869
870/// Entry count of a file's LABEL_INDEX, from its 32-byte preamble — one
871/// range read, never the index. `None` when the file has no such section or
872/// its preamble is unreadable. The card's `signals.label_index` uses this.
873pub fn read_label_index_entries_ranged<R: crate::RangeReader + ?Sized>(
874    reader: &R,
875    header: &crate::Header,
876) -> Option<u64> {
877    let s = header.label_index_section()?;
878    if header.version == crate::v06::VERSION {
879        return crate::v06::open::read_signals(reader).ok()?.1?.1;
880    }
881    let head = reader
882        .read_at(s.offset, (PREAMBLE_LEN as u64).min(s.length))
883        .ok()?;
884    LabelIndexPreamble::parse(&head).ok().map(|p| p.entry_count)
885}
886
887#[cfg(test)]
888mod tests {
889    use super::*;
890    use crate::file::CODEC_NONE;
891
892    fn entries(pairs: &[(&str, &str)]) -> Vec<(String, String, String)> {
893        let mut v: Vec<_> = pairs
894            .iter()
895            .map(|(l, s)| (label_key(l), s.to_string(), l.to_string()))
896            .collect();
897        v.sort_by(|a, b| (&a.0, &a.1).cmp(&(&b.0, &b.1)));
898        v
899    }
900
901    #[test]
902    fn normalizes_whitespace_and_truncates() {
903        assert_eq!(normalize_label("  a \n\t b  ").as_deref(), Some("a b"));
904        assert_eq!(normalize_label(" \n "), None);
905        let long = "é".repeat(400); // 800 bytes
906        let n = normalize_label(&long).unwrap();
907        assert!(n.len() <= MAX_LABEL_BYTES && n.is_char_boundary(n.len()));
908    }
909
910    #[test]
911    fn composes_person_names_only_without_a_direct_label() {
912        let mut c = SubjectCandidates::default();
913        c.push(Slot::Family, "Svobodová".into());
914        c.push(Slot::Given, "Jana".into());
915        assert_eq!(
916            c.labels(),
917            vec![("svobodová, jana".to_string(), "Svobodová, Jana".to_string())]
918        );
919        c.push(Slot::Direct, "J. Svobodová".into());
920        assert_eq!(c.labels().len(), 1);
921        assert_eq!(c.labels()[0].1, "J. Svobodová");
922        // one entry per key, keeping the smallest label
923        let mut d = SubjectCandidates::default();
924        d.push(Slot::Direct, "acta".into());
925        d.push(Slot::Direct, "Acta".into());
926        assert_eq!(d.labels(), vec![("acta".to_string(), "Acta".to_string())]);
927        let mut f = SubjectCandidates::default();
928        f.push(Slot::Family, "Smith".into());
929        assert_eq!(f.labels()[0].1, "Smith");
930    }
931
932    #[test]
933    fn round_trip_small_and_multilevel() {
934        // Small: one block, root lists it.
935        let small = entries(&[
936            ("Acta Universitatis", "<a>"),
937            ("acta u2", "<b>"),
938            ("Beta", "<c>"),
939        ]);
940        let sec = encode_section(&small, CODEC_NONE);
941        let idx = LabelIndex::from_section(&sec, CODEC_NONE).unwrap();
942        assert_eq!(idx.entry_count(), 3);
943        let hits = idx.prefix_search("ACTA U", 10);
944        assert_eq!(hits.len(), 2);
945        assert_eq!(
946            hits[0].0, "acta u2",
947            "key order: 'acta u2' < 'acta universitatis'"
948        );
949        assert!(idx.prefix_search("zz", 10).is_empty());
950        assert_eq!(idx.prefix_search("", 2).len(), 2);
951        assert!(idx.prefix_search("a", 0).is_empty());
952
953        // Large: enough entries for more than NODE_FANOUT blocks → ≥ 1 level.
954        let n = 400_000usize;
955        let mut big: Vec<(String, String, String)> = (0..n)
956            .map(|i| {
957                let l = format!("Label {i:07} of a work with a reasonably long title");
958                (label_key(&l), format!("<http://ex/s{i}>"), l)
959            })
960            .collect();
961        big.sort_by(|a, b| (&a.0, &a.1).cmp(&(&b.0, &b.1)));
962        let sec = encode_section(&big, CODEC_NONE);
963        let idx = LabelIndex::from_section(&sec, CODEC_NONE).unwrap();
964        assert!(idx.preamble.levels >= 1, "multi-level: {:?}", idx.preamble);
965        assert_eq!(idx.entry_count(), n as u64);
966        let hits = idx.prefix_search("label 0123", 1000);
967        assert_eq!(hits.len(), 1000);
968        assert!(hits.iter().all(|(l, _)| l.starts_with("Label 0123")));
969        let exact = idx.prefix_search("Label 0399999", 5);
970        assert_eq!(exact.len(), 1);
971        assert_eq!(exact[0].1, "<http://ex/s399999>");
972        // the first and the last entries are reachable
973        assert_eq!(idx.prefix_search("label 0000000", 1).len(), 1);
974    }
975
976    #[test]
977    fn lazy_open_reads_a_bounded_head_then_one_node_per_level_and_a_block() {
978        use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
979        let n = 300_000usize;
980        let mut big: Vec<(String, String, String)> = (0..n)
981            .map(|i| {
982                let l = format!("Name {i:07} {}", "x".repeat(i % 40));
983                (label_key(&l), format!("<http://ex/p{i}>"), l)
984            })
985            .collect();
986        big.sort_by(|a, b| (&a.0, &a.1).cmp(&(&b.0, &b.1)));
987        let sec = Arc::new(encode_section(&big, CODEC_NONE));
988        let calls = Arc::new(AtomicUsize::new(0));
989        let bytes = Arc::new(AtomicUsize::new(0));
990        let (s2, c2, b2) = (sec.clone(), calls.clone(), bytes.clone());
991        let loader = Box::new(move |off: u64, len: u64| {
992            c2.fetch_add(1, SeqCst);
993            b2.fetch_add(len as usize, SeqCst);
994            s2.get(off as usize..(off + len) as usize)
995                .map(<[u8]>::to_vec)
996        });
997        let head_len = (HEAD_READ as usize).min(sec.len());
998        let idx =
999            LabelIndex::from_head(&sec[..head_len], sec.len() as u64, CODEC_NONE, loader).unwrap();
1000        let levels = idx.preamble.levels as usize;
1001        assert_eq!(calls.load(SeqCst), 0, "the root came with the head");
1002        let hits = idx.prefix_search("name 0200000", 1);
1003        assert_eq!(hits.len(), 1);
1004        assert_eq!(
1005            calls.load(SeqCst),
1006            levels + 1,
1007            "one node per level plus one block"
1008        );
1009        assert!(bytes.load(SeqCst) < 128 * 1024, "{}", bytes.load(SeqCst));
1010    }
1011
1012    #[test]
1013    fn digest_detects_a_flipped_byte() {
1014        let sec = encode_section(&entries(&[("a", "<x>")]), CODEC_NONE);
1015        assert!(digest_ok(&sec));
1016        let mut bad = sec.clone();
1017        let last = bad.len() - 1;
1018        bad[last] ^= 1;
1019        assert!(!digest_ok(&bad));
1020        assert!(LabelIndex::from_section(&bad, CODEC_NONE).is_err());
1021    }
1022
1023    #[test]
1024    fn writer_rejects_out_of_order_and_drops_repeats() {
1025        let mut w = LabelIndexWriter::new(CODEC_NONE, Vec::new());
1026        w.push("b", "<s>", "b").unwrap();
1027        w.push("b", "<s>", "B").unwrap(); // repeat of (key, subject): dropped
1028        assert!(w.push("a", "<s>", "a").is_err());
1029        let parts = w.finish().unwrap();
1030        assert_eq!(parts.entry_count, 1);
1031    }
1032
1033    #[test]
1034    fn malformed_sections_error_cleanly() {
1035        let sec = encode_section(&entries(&[("a", "<x>"), ("b", "<y>")]), CODEC_NONE);
1036        for cut in [0, 10, 20, 31, 33] {
1037            let _ = LabelIndex::from_head(
1038                &sec[..cut.min(sec.len())],
1039                sec.len() as u64,
1040                CODEC_NONE,
1041                Box::new(|_, _| None),
1042            );
1043        }
1044        // a root_len pointing past the section
1045        let mut bad = sec.clone();
1046        bad[28..32].copy_from_slice(&u32::MAX.to_le_bytes());
1047        assert!(
1048            LabelIndex::from_head(&bad, bad.len() as u64, CODEC_NONE, Box::new(|_, _| None))
1049                .is_err()
1050        );
1051    }
1052}