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}
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
573enum Walk {
575 Continue,
576 Stop,
577}
578
579impl LabelIndex {
580 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 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 pub fn entry_count(&self) -> u64 {
638 self.preamble.entry_count
639 }
640
641 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 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 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
763pub(crate) fn digest_ok(section: &[u8]) -> bool {
765 if section.len() < DIGEST_LEN {
766 return false;
767 }
768 blake3::hash(§ion[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
841pub 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); 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 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 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 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 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(); 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 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}