1use 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
53pub 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
65pub 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
75pub 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
84pub const MAX_LABEL_BYTES: usize = 512;
86
87const VERSION: u8 = 1;
89pub(crate) const PREAMBLE_LEN: usize = 32;
91const DIGEST_LEN: usize = 16;
93const BLOCK_TARGET: usize = 16 * 1024;
95const NODE_FANOUT: usize = 1024;
97pub(crate) const HEAD_READ: u64 = 64 * 1024;
99const NODE_CACHE_CAP: usize = 256;
101const MAX_LEVELS: u8 = 5;
105const MAX_FETCHES_PER_SEARCH: usize = 1 << 16;
110
111pub fn label_key(label: &str) -> String {
113 label.to_lowercase()
114}
115
116pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
145pub(crate) enum Slot {
146 Direct = 0,
148 Family = 1,
150 Given = 2,
152}
153
154impl Slot {
155 #[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
167pub(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
180pub(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#[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 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
239pub(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#[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
347pub(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
364pub(crate) struct LabelIndexParts<W> {
368 pub head: Vec<u8>,
369 pub body: W,
370 #[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 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 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
484pub(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
498pub(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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
516pub struct LabelIndexPreamble {
517 pub levels: u8,
519 pub entry_count: u64,
521 pub root_len: u32,
523}
524
525impl LabelIndexPreamble {
526 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
545type SectionLoader = Box<dyn Fn(u64, u64) -> Option<Vec<u8>> + Send + Sync>;
547
548enum Source {
549 Resident(Arc<[u8]>),
550 Lazy(SectionLoader),
551}
552
553pub 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 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
575enum Walk {
577 Continue,
578 Stop,
579}
580
581impl LabelIndex {
582 pub(crate) fn with_max_block(mut self, max: u64) -> Self {
585 self.max_block = max;
586 self
587 }
588
589 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 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 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 pub fn entry_count(&self) -> u64 {
665 self.preamble.entry_count
666 }
667
668 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 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 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
792pub(crate) fn digest_ok(section: &[u8]) -> bool {
794 if section.len() < DIGEST_LEN {
795 return false;
796 }
797 blake3::hash(§ion[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
870pub 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); 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 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 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 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 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(); 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 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}