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}
563
564impl std::fmt::Debug for LabelIndex {
565    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
566        f.debug_struct("LabelIndex")
567            .field("preamble", &self.preamble)
568            .field("section_len", &self.section_len)
569            .finish()
570    }
571}
572
573/// Did a search reach its end, or must it go on?
574enum Walk {
575    Continue,
576    Stop,
577}
578
579impl LabelIndex {
580    /// Parse a whole section held in memory, checking its digest.
581    pub fn from_section(section: &[u8], codec: u8) -> Result<Self, FileError> {
582        if !digest_ok(section) {
583            return Err(FileError::Container("label index: digest mismatch"));
584        }
585        let bytes: Arc<[u8]> = Arc::from(section);
586        Self::open(
587            &bytes[..],
588            section.len() as u64,
589            codec,
590            Source::Resident(bytes.clone()),
591        )
592    }
593
594    /// Open from the section's leading bytes (`head`, at least the preamble;
595    /// normally `HEAD_READ` bytes) and a loader for any range of the section.
596    /// The root is read from `head` when it fits, else fetched.
597    pub fn from_head(
598        head: &[u8],
599        section_len: u64,
600        codec: u8,
601        loader: Box<dyn Fn(u64, u64) -> Option<Vec<u8>> + Send + Sync>,
602    ) -> Result<Self, FileError> {
603        Self::open(head, section_len, codec, Source::Lazy(loader))
604    }
605
606    fn open(head: &[u8], section_len: u64, codec: u8, source: Source) -> Result<Self, FileError> {
607        let preamble = LabelIndexPreamble::parse(head)?;
608        let root_end = PREAMBLE_LEN as u64 + preamble.root_len as u64;
609        if root_end > section_len {
610            return Err(FileError::Container("label index: root overruns section"));
611        }
612        let root_bytes: Vec<u8> = if root_end as usize <= head.len() {
613            head[PREAMBLE_LEN..root_end as usize].to_vec()
614        } else {
615            match &source {
616                Source::Resident(b) => b
617                    .get(PREAMBLE_LEN..root_end as usize)
618                    .ok_or(FileError::Container("label index: root overruns section"))?
619                    .to_vec(),
620                Source::Lazy(load) => load(PREAMBLE_LEN as u64, preamble.root_len as u64)
621                    .ok_or(FileError::Container("label index: root unreadable"))?,
622            }
623        };
624        let root = decode_node(&decompress(codec, &root_bytes)?)?;
625        Ok(LabelIndex {
626            preamble,
627            root,
628            body_base: root_end,
629            section_len,
630            codec,
631            source,
632            nodes: Mutex::new(HashMap::new()),
633        })
634    }
635
636    /// Number of entries in the index.
637    pub fn entry_count(&self) -> u64 {
638        self.preamble.entry_count
639    }
640
641    /// Byte length of the whole section.
642    pub fn section_len(&self) -> u64 {
643        self.section_len
644    }
645
646    fn fetch(&self, body_off: u64, len: u64) -> Option<Vec<u8>> {
647        let off = self.body_base.checked_add(body_off)?;
648        let end = off.checked_add(len)?;
649        if end > self.section_len {
650            return None;
651        }
652        match &self.source {
653            Source::Resident(b) => b.get(off as usize..end as usize).map(<[u8]>::to_vec),
654            Source::Lazy(load) => load(off, len),
655        }
656    }
657
658    fn node(&self, e: &DirEntry) -> Option<Arc<Vec<DirEntry>>> {
659        if let Some(n) = self.nodes.lock().ok()?.get(&e.off) {
660            return Some(n.clone());
661        }
662        let raw = decompress(self.codec, &self.fetch(e.off, e.len)?).ok()?;
663        let node = Arc::new(decode_node(&raw).ok()?);
664        if let Ok(mut cache) = self.nodes.lock() {
665            if cache.len() >= NODE_CACHE_CAP {
666                cache.clear();
667            }
668            cache.insert(e.off, node.clone());
669        }
670        Some(node)
671    }
672
673    /// Entries whose label starts with `prefix`, compared case-insensitively,
674    /// as `(label, subject term)` in key order, at most `limit`. An empty
675    /// `prefix` lists the index from the start.
676    pub fn prefix_search(&self, prefix: &str, limit: usize) -> Vec<(String, String)> {
677        let needle = label_key(prefix);
678        let mut out = Vec::new();
679        if limit == 0 {
680            return out;
681        }
682        let root = self.root.clone();
683        let mut budget = MAX_FETCHES_PER_SEARCH;
684        let _ = self.walk(
685            &root,
686            self.preamble.levels,
687            &needle,
688            limit,
689            &mut out,
690            &mut budget,
691        );
692        out
693    }
694
695    fn walk(
696        &self,
697        children: &[DirEntry],
698        depth: u8,
699        needle: &str,
700        limit: usize,
701        out: &mut Vec<(String, String)>,
702        budget: &mut usize,
703    ) -> Walk {
704        // The run of keys starting with `needle` begins in the last child whose
705        // first key is <= needle (or the first child).
706        let start = children
707            .partition_point(|c| c.first_key.as_str() <= needle)
708            .saturating_sub(1);
709        for child in &children[start..] {
710            if child.first_key.as_str() > needle && !child.first_key.starts_with(needle) {
711                return Walk::Stop;
712            }
713            if *budget == 0 {
714                return Walk::Stop;
715            }
716            *budget -= 1;
717            let step = if depth == 0 {
718                self.scan_block(child, needle, limit, out)
719            } else {
720                match self.node(child) {
721                    Some(node) => self.walk(&node, depth - 1, needle, limit, out, budget),
722                    None => Walk::Stop,
723                }
724            };
725            if let Walk::Stop = step {
726                return Walk::Stop;
727            }
728        }
729        Walk::Continue
730    }
731
732    fn scan_block(
733        &self,
734        e: &DirEntry,
735        needle: &str,
736        limit: usize,
737        out: &mut Vec<(String, String)>,
738    ) -> Walk {
739        let Some(raw) = self
740            .fetch(e.off, e.len)
741            .and_then(|b| decompress(self.codec, &b).ok())
742        else {
743            return Walk::Stop;
744        };
745        let Ok(entries) = decode_block(&raw) else {
746            return Walk::Stop;
747        };
748        for (label, subject) in entries {
749            let key = label_key(&label);
750            if key.starts_with(needle) {
751                out.push((label, subject));
752                if out.len() >= limit {
753                    return Walk::Stop;
754                }
755            } else if key.as_str() > needle {
756                return Walk::Stop;
757            }
758        }
759        Walk::Continue
760    }
761}
762
763/// Does a section's leading digest match the rest of it?
764pub(crate) fn digest_ok(section: &[u8]) -> bool {
765    if section.len() < DIGEST_LEN {
766        return false;
767    }
768    blake3::hash(&section[DIGEST_LEN..]).as_bytes()[..DIGEST_LEN] == section[..DIGEST_LEN]
769}
770
771struct Cursor<'a> {
772    b: &'a [u8],
773    pos: usize,
774}
775
776impl<'a> Cursor<'a> {
777    fn varint(&mut self) -> Result<u64, FileError> {
778        let (v, n) = read_uvarint(self.b.get(self.pos..).unwrap_or(&[]))
779            .ok_or(FileError::Container("label index: truncated varint"))?;
780        self.pos += n;
781        Ok(v)
782    }
783
784    fn front_coded(&mut self, prev: &str) -> Result<String, FileError> {
785        let shared = self.varint()? as usize;
786        let len = self.varint()? as usize;
787        let end = self
788            .pos
789            .checked_add(len)
790            .filter(|&e| e <= self.b.len())
791            .ok_or(FileError::Container("label index: string overruns"))?;
792        let base = prev
793            .get(..shared)
794            .ok_or(FileError::Container("label index: bad shared prefix"))?;
795        let suffix = std::str::from_utf8(&self.b[self.pos..end])
796            .map_err(|_| FileError::Container("label index: bad utf8"))?;
797        self.pos = end;
798        let mut s = String::with_capacity(base.len() + suffix.len());
799        s.push_str(base);
800        s.push_str(suffix);
801        Ok(s)
802    }
803}
804
805fn decode_node(raw: &[u8]) -> Result<Vec<DirEntry>, FileError> {
806    let mut c = Cursor { b: raw, pos: 0 };
807    let n = c.varint()? as usize;
808    let mut out = Vec::with_capacity(n.min(raw.len()));
809    let mut prev = String::new();
810    for _ in 0..n {
811        let first_key = c.front_coded(&prev)?;
812        let off = c.varint()?;
813        let len = c.varint()?;
814        let entries = c.varint()?;
815        prev.clone_from(&first_key);
816        out.push(DirEntry {
817            first_key,
818            off,
819            len,
820            entries,
821        });
822    }
823    Ok(out)
824}
825
826fn decode_block(raw: &[u8]) -> Result<Vec<(String, String)>, FileError> {
827    let mut c = Cursor { b: raw, pos: 0 };
828    let n = c.varint()? as usize;
829    let mut out = Vec::with_capacity(n.min(raw.len()));
830    let (mut pl, mut ps) = (String::new(), String::new());
831    for _ in 0..n {
832        let label = c.front_coded(&pl)?;
833        let subject = c.front_coded(&ps)?;
834        pl.clone_from(&label);
835        ps.clone_from(&subject);
836        out.push((label, subject));
837    }
838    Ok(out)
839}
840
841/// Entry count of a file's LABEL_INDEX, from its 32-byte preamble — one
842/// range read, never the index. `None` when the file has no such section or
843/// its preamble is unreadable. The card's `signals.label_index` uses this.
844pub fn read_label_index_entries_ranged<R: crate::RangeReader + ?Sized>(
845    reader: &R,
846    header: &crate::Header,
847) -> Option<u64> {
848    let s = header.label_index_section()?;
849    let head = reader
850        .read_at(s.offset, (PREAMBLE_LEN as u64).min(s.length))
851        .ok()?;
852    LabelIndexPreamble::parse(&head).ok().map(|p| p.entry_count)
853}
854
855#[cfg(test)]
856mod tests {
857    use super::*;
858    use crate::file::CODEC_NONE;
859
860    fn entries(pairs: &[(&str, &str)]) -> Vec<(String, String, String)> {
861        let mut v: Vec<_> = pairs
862            .iter()
863            .map(|(l, s)| (label_key(l), s.to_string(), l.to_string()))
864            .collect();
865        v.sort_by(|a, b| (&a.0, &a.1).cmp(&(&b.0, &b.1)));
866        v
867    }
868
869    #[test]
870    fn normalizes_whitespace_and_truncates() {
871        assert_eq!(normalize_label("  a \n\t b  ").as_deref(), Some("a b"));
872        assert_eq!(normalize_label(" \n "), None);
873        let long = "é".repeat(400); // 800 bytes
874        let n = normalize_label(&long).unwrap();
875        assert!(n.len() <= MAX_LABEL_BYTES && n.is_char_boundary(n.len()));
876    }
877
878    #[test]
879    fn composes_person_names_only_without_a_direct_label() {
880        let mut c = SubjectCandidates::default();
881        c.push(Slot::Family, "Svobodová".into());
882        c.push(Slot::Given, "Jana".into());
883        assert_eq!(
884            c.labels(),
885            vec![("svobodová, jana".to_string(), "Svobodová, Jana".to_string())]
886        );
887        c.push(Slot::Direct, "J. Svobodová".into());
888        assert_eq!(c.labels().len(), 1);
889        assert_eq!(c.labels()[0].1, "J. Svobodová");
890        // one entry per key, keeping the smallest label
891        let mut d = SubjectCandidates::default();
892        d.push(Slot::Direct, "acta".into());
893        d.push(Slot::Direct, "Acta".into());
894        assert_eq!(d.labels(), vec![("acta".to_string(), "Acta".to_string())]);
895        let mut f = SubjectCandidates::default();
896        f.push(Slot::Family, "Smith".into());
897        assert_eq!(f.labels()[0].1, "Smith");
898    }
899
900    #[test]
901    fn round_trip_small_and_multilevel() {
902        // Small: one block, root lists it.
903        let small = entries(&[
904            ("Acta Universitatis", "<a>"),
905            ("acta u2", "<b>"),
906            ("Beta", "<c>"),
907        ]);
908        let sec = encode_section(&small, CODEC_NONE);
909        let idx = LabelIndex::from_section(&sec, CODEC_NONE).unwrap();
910        assert_eq!(idx.entry_count(), 3);
911        let hits = idx.prefix_search("ACTA U", 10);
912        assert_eq!(hits.len(), 2);
913        assert_eq!(
914            hits[0].0, "acta u2",
915            "key order: 'acta u2' < 'acta universitatis'"
916        );
917        assert!(idx.prefix_search("zz", 10).is_empty());
918        assert_eq!(idx.prefix_search("", 2).len(), 2);
919        assert!(idx.prefix_search("a", 0).is_empty());
920
921        // Large: enough entries for more than NODE_FANOUT blocks → ≥ 1 level.
922        let n = 400_000usize;
923        let mut big: Vec<(String, String, String)> = (0..n)
924            .map(|i| {
925                let l = format!("Label {i:07} of a work with a reasonably long title");
926                (label_key(&l), format!("<http://ex/s{i}>"), l)
927            })
928            .collect();
929        big.sort_by(|a, b| (&a.0, &a.1).cmp(&(&b.0, &b.1)));
930        let sec = encode_section(&big, CODEC_NONE);
931        let idx = LabelIndex::from_section(&sec, CODEC_NONE).unwrap();
932        assert!(idx.preamble.levels >= 1, "multi-level: {:?}", idx.preamble);
933        assert_eq!(idx.entry_count(), n as u64);
934        let hits = idx.prefix_search("label 0123", 1000);
935        assert_eq!(hits.len(), 1000);
936        assert!(hits.iter().all(|(l, _)| l.starts_with("Label 0123")));
937        let exact = idx.prefix_search("Label 0399999", 5);
938        assert_eq!(exact.len(), 1);
939        assert_eq!(exact[0].1, "<http://ex/s399999>");
940        // the first and the last entries are reachable
941        assert_eq!(idx.prefix_search("label 0000000", 1).len(), 1);
942    }
943
944    #[test]
945    fn lazy_open_reads_a_bounded_head_then_one_node_per_level_and_a_block() {
946        use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
947        let n = 300_000usize;
948        let mut big: Vec<(String, String, String)> = (0..n)
949            .map(|i| {
950                let l = format!("Name {i:07} {}", "x".repeat(i % 40));
951                (label_key(&l), format!("<http://ex/p{i}>"), l)
952            })
953            .collect();
954        big.sort_by(|a, b| (&a.0, &a.1).cmp(&(&b.0, &b.1)));
955        let sec = Arc::new(encode_section(&big, CODEC_NONE));
956        let calls = Arc::new(AtomicUsize::new(0));
957        let bytes = Arc::new(AtomicUsize::new(0));
958        let (s2, c2, b2) = (sec.clone(), calls.clone(), bytes.clone());
959        let loader = Box::new(move |off: u64, len: u64| {
960            c2.fetch_add(1, SeqCst);
961            b2.fetch_add(len as usize, SeqCst);
962            s2.get(off as usize..(off + len) as usize)
963                .map(<[u8]>::to_vec)
964        });
965        let head_len = (HEAD_READ as usize).min(sec.len());
966        let idx =
967            LabelIndex::from_head(&sec[..head_len], sec.len() as u64, CODEC_NONE, loader).unwrap();
968        let levels = idx.preamble.levels as usize;
969        assert_eq!(calls.load(SeqCst), 0, "the root came with the head");
970        let hits = idx.prefix_search("name 0200000", 1);
971        assert_eq!(hits.len(), 1);
972        assert_eq!(
973            calls.load(SeqCst),
974            levels + 1,
975            "one node per level plus one block"
976        );
977        assert!(bytes.load(SeqCst) < 128 * 1024, "{}", bytes.load(SeqCst));
978    }
979
980    #[test]
981    fn digest_detects_a_flipped_byte() {
982        let sec = encode_section(&entries(&[("a", "<x>")]), CODEC_NONE);
983        assert!(digest_ok(&sec));
984        let mut bad = sec.clone();
985        let last = bad.len() - 1;
986        bad[last] ^= 1;
987        assert!(!digest_ok(&bad));
988        assert!(LabelIndex::from_section(&bad, CODEC_NONE).is_err());
989    }
990
991    #[test]
992    fn writer_rejects_out_of_order_and_drops_repeats() {
993        let mut w = LabelIndexWriter::new(CODEC_NONE, Vec::new());
994        w.push("b", "<s>", "b").unwrap();
995        w.push("b", "<s>", "B").unwrap(); // repeat of (key, subject): dropped
996        assert!(w.push("a", "<s>", "a").is_err());
997        let parts = w.finish().unwrap();
998        assert_eq!(parts.entry_count, 1);
999    }
1000
1001    #[test]
1002    fn malformed_sections_error_cleanly() {
1003        let sec = encode_section(&entries(&[("a", "<x>"), ("b", "<y>")]), CODEC_NONE);
1004        for cut in [0, 10, 20, 31, 33] {
1005            let _ = LabelIndex::from_head(
1006                &sec[..cut.min(sec.len())],
1007                sec.len() as u64,
1008                CODEC_NONE,
1009                Box::new(|_, _| None),
1010            );
1011        }
1012        // a root_len pointing past the section
1013        let mut bad = sec.clone();
1014        bad[28..32].copy_from_slice(&u32::MAX.to_le_bytes());
1015        assert!(
1016            LabelIndex::from_head(&bad, bad.len() as u64, CODEC_NONE, Box::new(|_, _| None))
1017                .is_err()
1018        );
1019    }
1020}