1use std::fs;
36use std::io::Write;
37use std::path::{Path, PathBuf};
38
39use crate::error::{Error, Result};
40use crate::store::{IoCounters, NodeId};
41
42pub const INDEX_MAGIC: u8 = 0x72;
45pub const INDEX_VERSION: u8 = 1;
47pub const MAX_FANOUT: usize = 256;
49pub const MAX_DEPTH: u8 = 3;
51pub const MAX_INDEX_NODE_BYTES: usize = 8 * 1024;
53
54pub const SEL_PAGE: u8 = 1;
56pub const SEL_OBJECT: u8 = 2;
58pub const SEL_STREAM: u8 = 3;
60pub const SEL_REVISION: u8 = 4;
62pub const SEL_RESOURCE: u8 = 5;
64pub const SEL_STREAM_DECODED: u8 = 6;
72pub const SEL_PACKAGE_MEMBER_RAW: u8 = 7;
78pub const SEL_PACKAGE_MEMBER_DECODED: u8 = 8;
84pub const SEL_OPC_MODEL: u8 = 9;
91pub const SEL_DOCX_MODEL: u8 = 10;
98pub const SEL_EPUB_MODEL: u8 = 11;
105
106const KIND_LEAF: u8 = 0;
108const KIND_INTERNAL: u8 = 1;
110
111const HEADER_LEN: usize = 8;
113const LEAF_ENTRY_LEN: usize = 1 + 2 + 4 + 8 + 8 + 32;
116const INTERNAL_ENTRY_LEN: usize = 1 + 4 + 1 + 4 + 32;
119
120const MAX_LEAF_ENTRIES: usize = {
122 let by_bytes = (MAX_INDEX_NODE_BYTES - HEADER_LEN) / LEAF_ENTRY_LEN;
123 if MAX_FANOUT < by_bytes {
124 MAX_FANOUT
125 } else {
126 by_bytes
127 }
128};
129
130const MAX_INTERNAL_CHILDREN: usize = {
132 let by_bytes = (MAX_INDEX_NODE_BYTES - HEADER_LEN) / INTERNAL_ENTRY_LEN;
133 if MAX_FANOUT < by_bytes {
134 MAX_FANOUT
135 } else {
136 by_bytes
137 }
138};
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
145pub struct SelectorKey {
146 pub kind: u8,
150 pub number: u32,
153 pub generation: u16,
155}
156
157impl SelectorKey {
158 pub const fn new(kind: u8, number: u32) -> Self {
160 SelectorKey {
161 kind,
162 number,
163 generation: 0,
164 }
165 }
166
167 pub const fn with_generation(kind: u8, number: u32, generation: u16) -> Self {
169 SelectorKey {
170 kind,
171 number,
172 generation,
173 }
174 }
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
179pub struct IndexEntry {
180 pub key: SelectorKey,
182 pub out_off: u64,
184 pub out_len: u64,
186 pub node_id: NodeId,
188}
189
190pub struct FsIndexStore {
196 root: PathBuf,
197 io: IoCounters,
198}
199
200impl std::fmt::Debug for FsIndexStore {
201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 f.debug_struct("FsIndexStore")
203 .field("root", &self.root)
204 .finish_non_exhaustive()
205 }
206}
207
208impl FsIndexStore {
209 pub fn open(root: impl AsRef<Path>) -> Result<Self> {
212 Self::open_with_io(root, IoCounters::new())
213 }
214
215 pub fn open_with_io(root: impl AsRef<Path>, io: IoCounters) -> Result<Self> {
217 let root = root.as_ref().to_path_buf();
218 fs::create_dir_all(root.join("index"))?;
219 Ok(FsIndexStore { root, io })
220 }
221
222 pub fn put(&mut self, canonical: &[u8]) -> Result<NodeId> {
224 let id = NodeId::of_node(canonical);
225 let path = self.node_path(&id);
226 if path.exists() {
227 return Ok(id);
228 }
229 let dir = path
230 .parent()
231 .ok_or_else(|| Error::internal_invariant("index node path has no parent"))?;
232 fs::create_dir_all(dir)?;
233 let tmp = dir.join(format!(".{}.tmp-{}", id.to_hex(), std::process::id()));
234 {
235 let mut f = fs::File::create(&tmp)?;
236 f.write_all(canonical)?;
237 f.sync_all()?;
238 }
239 fs::rename(&tmp, &path)?;
240 Ok(id)
241 }
242
243 pub fn get(&self, id: &NodeId) -> Result<Vec<u8>> {
245 let path = self.node_path(id);
246 let bytes = fs::read(&path).map_err(|e| {
247 if e.kind() == std::io::ErrorKind::NotFound {
248 Error::missing_external_object(format!("index node {id} is not present"))
249 } else {
250 Error::io(format!("reading index node {id}: {e}"))
251 }
252 })?;
253 let actual = NodeId::of_node(&bytes);
254 if actual != *id {
255 return Err(Error::integrity_mismatch(format!(
256 "index node {id} content hashes to {actual}"
257 )));
258 }
259 self.io.add_index(bytes.len() as u64);
260 Ok(bytes)
261 }
262
263 pub fn contains(&self, id: &NodeId) -> Result<bool> {
265 Ok(self.node_path(id).exists())
266 }
267
268 pub fn list_ids(&self) -> Result<Vec<NodeId>> {
274 let mut out: Vec<NodeId> = Vec::new();
275 let mut stack = vec![self.root.join("index")];
276 while let Some(dir) = stack.pop() {
277 let entries = match fs::read_dir(&dir) {
278 Ok(e) => e,
279 Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
280 Err(e) => return Err(e.into()),
281 };
282 for entry in entries.flatten() {
283 let path = entry.path();
284 if path.is_dir() {
285 stack.push(path);
286 continue;
287 }
288 if let Some(name) = path.file_name().and_then(|n| n.to_str())
289 && name.len() == 64
290 && let Ok(id) = NodeId::from_hex(name)
291 {
292 out.push(id);
293 }
294 }
295 }
296 out.sort_unstable();
297 out.dedup();
298 Ok(out)
299 }
300
301 pub fn count(&self) -> Result<u64> {
303 let mut n = 0u64;
304 let mut stack = vec![self.root.join("index")];
305 while let Some(dir) = stack.pop() {
306 let entries = match fs::read_dir(&dir) {
307 Ok(e) => e,
308 Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
309 Err(e) => return Err(e.into()),
310 };
311 for entry in entries.flatten() {
312 let path = entry.path();
313 if path.is_dir() {
314 stack.push(path);
315 continue;
316 }
317 if let Some(name) = path.file_name().and_then(|n| n.to_str())
318 && name.len() == 64
319 && NodeId::from_hex(name).is_ok()
320 {
321 n += 1;
322 }
323 }
324 }
325 Ok(n)
326 }
327
328 fn node_path(&self, id: &NodeId) -> PathBuf {
329 let hex = id.to_hex();
330 self.root
331 .join("index")
332 .join(&hex[0..2])
333 .join(&hex[2..4])
334 .join(&hex)
335 }
336}
337
338trait NodeReader {
341 fn read_node(&self, id: &NodeId) -> Result<Vec<u8>>;
342}
343
344impl NodeReader for FsIndexStore {
345 fn read_node(&self, id: &NodeId) -> Result<Vec<u8>> {
346 self.get(id)
347 }
348}
349
350#[derive(Debug, Clone, Copy, PartialEq, Eq)]
352struct KeyRange {
353 min_kind: u8,
354 min_number: u32,
355 max_kind: u8,
356 max_number: u32,
357}
358
359impl KeyRange {
360 fn contains(&self, key: &SelectorKey) -> bool {
362 self.contains_pair(key.kind, key.number)
363 }
364
365 fn contains_pair(&self, kind: u8, number: u32) -> bool {
367 (self.min_kind, self.min_number) <= (kind, number)
368 && (kind, number) <= (self.max_kind, self.max_number)
369 }
370
371 fn contains_range(&self, other: &KeyRange) -> bool {
373 (self.min_kind, self.min_number) <= (other.min_kind, other.min_number)
374 && (other.max_kind, other.max_number) <= (self.max_kind, self.max_number)
375 }
376}
377
378#[derive(Debug, Clone, Copy)]
380struct ChildRef {
381 range: KeyRange,
382 child_id: NodeId,
383}
384
385#[derive(Debug)]
387struct DecodedNode {
388 kind: u8,
389 depth: u8,
390 leaf: Vec<IndexEntry>,
391 internal: Vec<ChildRef>,
392}
393
394#[derive(Debug, Clone, Copy)]
396struct Descend {
397 id: NodeId,
398 expected_depth: Option<u8>,
399 expected_range: Option<KeyRange>,
400}
401
402fn entry_order(a: &IndexEntry, b: &IndexEntry) -> std::cmp::Ordering {
404 a.key
405 .cmp(&b.key)
406 .then(a.out_off.cmp(&b.out_off))
407 .then(a.out_len.cmp(&b.out_len))
408 .then(a.node_id.cmp(&b.node_id))
409}
410
411pub fn build(store: &mut FsIndexStore, entries: &[IndexEntry]) -> Result<NodeId> {
419 let mut sorted = entries.to_vec();
420 sorted.sort_by(entry_order);
421 sorted.dedup();
422
423 if sorted.is_empty() {
424 return store.put(&encode_leaf(&[], 0)?);
425 }
426
427 let mut level: Vec<ChildRef> = Vec::new();
428 for chunk in sorted.chunks(MAX_LEAF_ENTRIES) {
429 let bytes = encode_leaf(chunk, 0)?;
430 let id = store.put(&bytes)?;
431 level.push(child_from_leaf(chunk, id)?);
432 }
433
434 let mut depth = 0u8;
435 while level.len() > 1 {
436 if depth == MAX_DEPTH {
437 return Err(Error::resource_limit(format!(
438 "index would exceed MAX_DEPTH {MAX_DEPTH}"
439 )));
440 }
441 depth += 1;
442 let mut next: Vec<ChildRef> = Vec::new();
443 for chunk in level.chunks(MAX_INTERNAL_CHILDREN) {
444 let bytes = encode_internal(chunk, depth)?;
445 let id = store.put(&bytes)?;
446 next.push(child_from_internal(chunk, id)?);
447 }
448 level = next;
449 }
450
451 level
452 .pop()
453 .map(|root| root.child_id)
454 .ok_or_else(|| Error::internal_invariant("index build produced no node"))
455}
456
457pub fn lookup(store: &FsIndexStore, root: &NodeId, key: &SelectorKey) -> Result<Vec<IndexEntry>> {
464 lookup_impl(store, root, key)
465}
466
467pub fn validate(store: &FsIndexStore, root: &NodeId) -> Result<(u64, u8)> {
471 let (count, depth, _ids) = validate_impl_nodes(store, root)?;
472 Ok((count, depth))
473}
474
475pub fn validate_nodes(store: &FsIndexStore, root: &NodeId) -> Result<(u64, u8, Vec<NodeId>)> {
479 validate_impl_nodes(store, root)
480}
481
482pub struct TreeInspection {
489 pub entries: Vec<IndexEntry>,
491 pub nodes: Vec<NodeId>,
493}
494
495pub fn inspect(store: &FsIndexStore, root: &NodeId) -> Result<TreeInspection> {
498 let mut entries: Vec<IndexEntry> = Vec::new();
499 let mut nodes: Vec<NodeId> = Vec::new();
500 let mut stack: Vec<NodeId> = vec![*root];
501 while let Some(id) = stack.pop() {
502 if nodes.contains(&id) {
503 continue;
504 }
505 let bytes = store.get(&id)?;
506 let node = parse_node(&bytes)?;
507 nodes.push(id);
508 match node.kind {
509 KIND_LEAF => entries.extend(node.leaf),
510 _ => {
511 for c in node.internal {
512 stack.push(c.child_id);
513 }
514 }
515 }
516 }
517 entries.sort_by(entry_order);
518 entries.dedup();
519 Ok(TreeInspection { entries, nodes })
520}
521
522fn lookup_impl<R: NodeReader>(
523 store: &R,
524 root: &NodeId,
525 key: &SelectorKey,
526) -> Result<Vec<IndexEntry>> {
527 let mut out: Vec<IndexEntry> = Vec::new();
528 let mut stack: Vec<(NodeId, Option<u8>)> = vec![(*root, None)];
529 while let Some((id, expected)) = stack.pop() {
530 let bytes = store.read_node(&id)?;
531 let node = parse_node(&bytes)?;
532 if let Some(exp) = expected
533 && node.depth != exp
534 {
535 return Err(Error::integrity_mismatch(format!(
536 "index node {id} declares depth {} but was reached at depth {exp}",
537 node.depth
538 )));
539 }
540 match node.kind {
541 KIND_LEAF => {
542 for e in node.leaf {
543 if e.key == *key {
544 out.push(e);
545 }
546 }
547 }
548 _ => {
549 let child_depth = node
550 .depth
551 .checked_sub(1)
552 .ok_or_else(|| Error::integrity_mismatch("index internal node has depth 0"))?;
553 for child in node.internal {
554 if child.range.contains(key) {
555 stack.push((child.child_id, Some(child_depth)));
556 }
557 }
558 }
559 }
560 }
561 out.sort_by(entry_order);
562 out.dedup();
563 Ok(out)
564}
565
566fn validate_impl_nodes<R: NodeReader>(store: &R, root: &NodeId) -> Result<(u64, u8, Vec<NodeId>)> {
567 let mut seen: Vec<(NodeId, Vec<u8>, u8)> = Vec::new();
568 let mut count = 0u64;
569 let mut max_depth = 0u8;
570 let mut stack: Vec<Descend> = vec![Descend {
571 id: *root,
572 expected_depth: None,
573 expected_range: None,
574 }];
575 while let Some(step) = stack.pop() {
576 let bytes = store.read_node(&step.id)?;
577 let node = parse_node(&bytes)?;
578 if let Some(exp) = step.expected_depth
579 && node.depth != exp
580 {
581 return Err(Error::integrity_mismatch(format!(
582 "index node {} declares depth {} but was reached at depth {exp}",
583 step.id, node.depth
584 )));
585 }
586 if let Some(range) = step.expected_range {
587 for e in &node.leaf {
588 if !range.contains_pair(e.key.kind, e.key.number) {
589 return Err(Error::integrity_mismatch(format!(
590 "index leaf entry {:?} lies outside its parent range",
591 e.key
592 )));
593 }
594 }
595 for c in &node.internal {
596 if !range.contains_range(&c.range) {
597 return Err(Error::integrity_mismatch(format!(
598 "index child range {:?} exceeds its parent range",
599 c.range
600 )));
601 }
602 }
603 }
604 if !note_node(&mut seen, step.id, &bytes, node.depth)? {
605 continue;
606 }
607 count += 1;
608 if node.depth > max_depth {
609 max_depth = node.depth;
610 }
611 if node.kind == KIND_INTERNAL {
612 validate_child_order(&node.internal)?;
613 let child_depth = node
614 .depth
615 .checked_sub(1)
616 .ok_or_else(|| Error::integrity_mismatch("index internal node has depth 0"))?;
617 for c in node.internal {
618 stack.push(Descend {
619 id: c.child_id,
620 expected_depth: Some(child_depth),
621 expected_range: Some(c.range),
622 });
623 }
624 }
625 }
626 Ok((
627 count,
628 max_depth,
629 seen.into_iter().map(|(id, _, _)| id).collect(),
630 ))
631}
632
633fn note_node(
637 seen: &mut Vec<(NodeId, Vec<u8>, u8)>,
638 id: NodeId,
639 bytes: &[u8],
640 depth: u8,
641) -> Result<bool> {
642 if let Some((_, prev, prev_depth)) = seen.iter().find(|(seen_id, _, _)| *seen_id == id) {
643 if prev.as_slice() != bytes {
644 return Err(Error::integrity_mismatch(format!(
645 "index node {id} decoded twice with differing bytes"
646 )));
647 }
648 if *prev_depth != depth {
649 return Err(Error::integrity_mismatch(format!(
650 "index node {id} appears at inconsistent depths"
651 )));
652 }
653 return Ok(false);
654 }
655 seen.push((id, bytes.to_vec(), depth));
656 Ok(true)
657}
658
659fn validate_child_order(children: &[ChildRef]) -> Result<()> {
661 let mut prev_min: Option<(u8, u32)> = None;
662 for c in children {
663 let min = (c.range.min_kind, c.range.min_number);
664 let max = (c.range.max_kind, c.range.max_number);
665 if min > max {
666 return Err(Error::integrity_mismatch(
667 "index internal child has min greater than max",
668 ));
669 }
670 if let Some(prev) = prev_min
671 && min < prev
672 {
673 return Err(Error::integrity_mismatch(
674 "index internal children are not sorted",
675 ));
676 }
677 prev_min = Some(min);
678 }
679 Ok(())
680}
681
682fn child_from_leaf(chunk: &[IndexEntry], id: NodeId) -> Result<ChildRef> {
683 let first = chunk
684 .first()
685 .ok_or_else(|| Error::internal_invariant("empty leaf chunk"))?;
686 let last = chunk
687 .last()
688 .ok_or_else(|| Error::internal_invariant("empty leaf chunk"))?;
689 Ok(ChildRef {
690 range: KeyRange {
691 min_kind: first.key.kind,
692 min_number: first.key.number,
693 max_kind: last.key.kind,
694 max_number: last.key.number,
695 },
696 child_id: id,
697 })
698}
699
700fn child_from_internal(chunk: &[ChildRef], id: NodeId) -> Result<ChildRef> {
701 let first = chunk
702 .first()
703 .ok_or_else(|| Error::internal_invariant("empty internal chunk"))?;
704 let last = chunk
705 .last()
706 .ok_or_else(|| Error::internal_invariant("empty internal chunk"))?;
707 Ok(ChildRef {
708 range: KeyRange {
709 min_kind: first.range.min_kind,
710 min_number: first.range.min_number,
711 max_kind: last.range.max_kind,
712 max_number: last.range.max_number,
713 },
714 child_id: id,
715 })
716}
717
718fn push_header(out: &mut Vec<u8>, kind: u8, depth: u8, count: u32) {
719 out.push(INDEX_MAGIC);
720 out.push(INDEX_VERSION);
721 out.push(kind);
722 out.push(depth);
723 out.extend_from_slice(&count.to_le_bytes());
724}
725
726fn encode_leaf(entries: &[IndexEntry], depth: u8) -> Result<Vec<u8>> {
727 if entries.len() > MAX_FANOUT {
728 return Err(Error::resource_limit(format!(
729 "index leaf entry_count {} exceeds MAX_FANOUT {MAX_FANOUT}",
730 entries.len()
731 )));
732 }
733 let count = u32::try_from(entries.len())
734 .map_err(|_| Error::resource_limit("index leaf entry_count exceeds u32"))?;
735 let mut out = Vec::with_capacity(HEADER_LEN + entries.len() * LEAF_ENTRY_LEN);
736 push_header(&mut out, KIND_LEAF, depth, count);
737 for e in entries {
738 out.push(e.key.kind);
739 out.extend_from_slice(&e.key.generation.to_le_bytes());
740 out.extend_from_slice(&e.key.number.to_le_bytes());
741 out.extend_from_slice(&e.out_off.to_le_bytes());
742 out.extend_from_slice(&e.out_len.to_le_bytes());
743 out.extend_from_slice(e.node_id.as_bytes());
744 }
745 if out.len() > MAX_INDEX_NODE_BYTES {
746 return Err(Error::resource_limit(format!(
747 "index leaf node is {} bytes, exceeding MAX_INDEX_NODE_BYTES {MAX_INDEX_NODE_BYTES}",
748 out.len()
749 )));
750 }
751 Ok(out)
752}
753
754fn encode_internal(children: &[ChildRef], depth: u8) -> Result<Vec<u8>> {
755 if children.len() > MAX_FANOUT {
756 return Err(Error::resource_limit(format!(
757 "index internal entry_count {} exceeds MAX_FANOUT {MAX_FANOUT}",
758 children.len()
759 )));
760 }
761 let count = u32::try_from(children.len())
762 .map_err(|_| Error::resource_limit("index internal entry_count exceeds u32"))?;
763 let mut out = Vec::with_capacity(HEADER_LEN + children.len() * INTERNAL_ENTRY_LEN);
764 push_header(&mut out, KIND_INTERNAL, depth, count);
765 for c in children {
766 out.push(c.range.min_kind);
767 out.extend_from_slice(&c.range.min_number.to_le_bytes());
768 out.push(c.range.max_kind);
769 out.extend_from_slice(&c.range.max_number.to_le_bytes());
770 out.extend_from_slice(c.child_id.as_bytes());
771 }
772 if out.len() > MAX_INDEX_NODE_BYTES {
773 return Err(Error::resource_limit(format!(
774 "index internal node is {} bytes, exceeding MAX_INDEX_NODE_BYTES {MAX_INDEX_NODE_BYTES}",
775 out.len()
776 )));
777 }
778 Ok(out)
779}
780
781fn parse_node(bytes: &[u8]) -> Result<DecodedNode> {
782 if bytes.len() < HEADER_LEN {
783 return Err(Error::integrity_mismatch(
784 "index node is shorter than its header",
785 ));
786 }
787 let magic = bytes[0];
788 if magic != INDEX_MAGIC {
789 return Err(Error::integrity_mismatch(format!(
790 "index node has bad magic 0x{magic:02x}"
791 )));
792 }
793 let version = bytes[1];
794 if version != INDEX_VERSION {
795 return Err(Error::unsupported_version(format!(
796 "index node version {version} is not supported"
797 )));
798 }
799 let kind = bytes[2];
800 if kind != KIND_LEAF && kind != KIND_INTERNAL {
801 return Err(Error::integrity_mismatch(format!(
802 "index node has unknown kind {kind}"
803 )));
804 }
805 let depth = bytes[3];
806 if depth > MAX_DEPTH {
807 return Err(Error::resource_limit(format!(
808 "index node depth {depth} exceeds MAX_DEPTH {MAX_DEPTH}"
809 )));
810 }
811 if kind == KIND_LEAF && depth != 0 {
812 return Err(Error::integrity_mismatch(
813 "index leaf node has non-zero depth",
814 ));
815 }
816 if kind == KIND_INTERNAL && depth == 0 {
817 return Err(Error::integrity_mismatch("index internal node has depth 0"));
818 }
819 let count = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
820 if count as usize > MAX_FANOUT {
821 return Err(Error::resource_limit(format!(
822 "index node entry_count {count} exceeds MAX_FANOUT {MAX_FANOUT}"
823 )));
824 }
825 let entry_len = if kind == KIND_LEAF {
826 LEAF_ENTRY_LEN
827 } else {
828 INTERNAL_ENTRY_LEN
829 };
830 let count = count as usize;
831 let expected = HEADER_LEN
832 .checked_add(
833 count
834 .checked_mul(entry_len)
835 .ok_or_else(|| Error::resource_limit("index node size overflow"))?,
836 )
837 .ok_or_else(|| Error::resource_limit("index node size overflow"))?;
838 if bytes.len() < expected {
839 return Err(Error::integrity_mismatch(format!(
840 "index node is truncated: need {expected} bytes, have {}",
841 bytes.len()
842 )));
843 }
844 if bytes.len() > expected {
845 return Err(Error::integrity_mismatch(format!(
846 "index node has {} trailing bytes",
847 bytes.len() - expected
848 )));
849 }
850 if bytes.len() > MAX_INDEX_NODE_BYTES {
851 return Err(Error::resource_limit(format!(
852 "index node is {} bytes, exceeding MAX_INDEX_NODE_BYTES {MAX_INDEX_NODE_BYTES}",
853 bytes.len()
854 )));
855 }
856
857 let mut p = HEADER_LEN;
858 let mut leaf = Vec::new();
859 let mut internal = Vec::new();
860 if kind == KIND_LEAF {
861 leaf.reserve(count);
862 for _ in 0..count {
863 let kind = read_u8(bytes, &mut p)?;
864 let generation = read_u16(bytes, &mut p)?;
865 let number = read_u32(bytes, &mut p)?;
866 let out_off = read_u64(bytes, &mut p)?;
867 let out_len = read_u64(bytes, &mut p)?;
868 let node_id = read_node_id(bytes, &mut p)?;
869 leaf.push(IndexEntry {
870 key: SelectorKey {
871 kind,
872 number,
873 generation,
874 },
875 out_off,
876 out_len,
877 node_id,
878 });
879 }
880 } else {
881 internal.reserve(count);
882 for _ in 0..count {
883 let min_kind = read_u8(bytes, &mut p)?;
884 let min_number = read_u32(bytes, &mut p)?;
885 let max_kind = read_u8(bytes, &mut p)?;
886 let max_number = read_u32(bytes, &mut p)?;
887 let child_id = read_node_id(bytes, &mut p)?;
888 internal.push(ChildRef {
889 range: KeyRange {
890 min_kind,
891 min_number,
892 max_kind,
893 max_number,
894 },
895 child_id,
896 });
897 }
898 }
899 Ok(DecodedNode {
900 kind,
901 depth,
902 leaf,
903 internal,
904 })
905}
906
907fn read_u8(bytes: &[u8], p: &mut usize) -> Result<u8> {
908 let v = *bytes
909 .get(*p)
910 .ok_or_else(|| Error::integrity_mismatch("truncated index node"))?;
911 *p += 1;
912 Ok(v)
913}
914
915fn read_u16(bytes: &[u8], p: &mut usize) -> Result<u16> {
916 let end = p
917 .checked_add(2)
918 .ok_or_else(|| Error::integrity_mismatch("index node cursor overflow"))?;
919 let s = bytes
920 .get(*p..end)
921 .ok_or_else(|| Error::integrity_mismatch("truncated index node"))?;
922 *p = end;
923 Ok(u16::from_le_bytes([s[0], s[1]]))
924}
925
926fn read_u32(bytes: &[u8], p: &mut usize) -> Result<u32> {
927 let end = p
928 .checked_add(4)
929 .ok_or_else(|| Error::integrity_mismatch("index node cursor overflow"))?;
930 let s = bytes
931 .get(*p..end)
932 .ok_or_else(|| Error::integrity_mismatch("truncated index node"))?;
933 *p = end;
934 Ok(u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
935}
936
937fn read_u64(bytes: &[u8], p: &mut usize) -> Result<u64> {
938 let end = p
939 .checked_add(8)
940 .ok_or_else(|| Error::integrity_mismatch("index node cursor overflow"))?;
941 let s = bytes
942 .get(*p..end)
943 .ok_or_else(|| Error::integrity_mismatch("truncated index node"))?;
944 *p = end;
945 Ok(u64::from_le_bytes([
946 s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7],
947 ]))
948}
949
950fn read_node_id(bytes: &[u8], p: &mut usize) -> Result<NodeId> {
951 let end = p
952 .checked_add(32)
953 .ok_or_else(|| Error::integrity_mismatch("index node cursor overflow"))?;
954 let s = bytes
955 .get(*p..end)
956 .ok_or_else(|| Error::integrity_mismatch("truncated index node"))?;
957 let mut raw = [0u8; 32];
958 raw.copy_from_slice(s);
959 *p = end;
960 Ok(NodeId::from_bytes(raw))
961}
962
963#[cfg(test)]
964mod tests {
965 use super::*;
966 use std::cell::Cell;
967
968 struct CountingStore<'a> {
970 inner: &'a FsIndexStore,
971 gets: Cell<u64>,
972 }
973
974 impl<'a> CountingStore<'a> {
975 fn new(inner: &'a FsIndexStore) -> Self {
976 CountingStore {
977 inner,
978 gets: Cell::new(0),
979 }
980 }
981 }
982
983 impl NodeReader for CountingStore<'_> {
984 fn read_node(&self, id: &NodeId) -> Result<Vec<u8>> {
985 self.gets.set(self.gets.get() + 1);
986 self.inner.get(id)
987 }
988 }
989
990 fn temp_root(label: &str) -> PathBuf {
991 let mut p = std::env::temp_dir();
992 p.push(format!(
993 "vole-index-{label}-{}-{}",
994 std::process::id(),
995 std::time::SystemTime::now()
996 .duration_since(std::time::UNIX_EPOCH)
997 .unwrap()
998 .as_nanos()
999 ));
1000 fs::create_dir_all(&p).unwrap();
1001 p
1002 }
1003
1004 fn entry(kind: u8, number: u32, tag: &[u8]) -> IndexEntry {
1005 IndexEntry {
1006 key: SelectorKey::new(kind, number),
1007 out_off: u64::from(number) * 10,
1008 out_len: 5,
1009 node_id: NodeId::of_node(tag),
1010 }
1011 }
1012
1013 fn sample_entries(n: u32) -> Vec<IndexEntry> {
1014 let kinds = [SEL_PAGE, SEL_OBJECT, SEL_STREAM, SEL_REVISION, SEL_RESOURCE];
1015 (0..n)
1016 .map(|i| {
1017 let kind = kinds[(i as usize) % kinds.len()];
1018 entry(kind, i, &i.to_le_bytes())
1019 })
1020 .collect()
1021 }
1022
1023 fn node_files(index_root: &Path) -> Vec<PathBuf> {
1024 let mut out = Vec::new();
1025 let mut stack = vec![index_root.to_path_buf()];
1026 while let Some(dir) = stack.pop() {
1027 let Ok(entries) = fs::read_dir(&dir) else {
1028 continue;
1029 };
1030 for e in entries.flatten() {
1031 let path = e.path();
1032 if path.is_dir() {
1033 stack.push(path);
1034 } else if let Some(name) = path.file_name().and_then(|n| n.to_str())
1035 && name.len() == 64
1036 && NodeId::from_hex(name).is_ok()
1037 {
1038 out.push(path);
1039 }
1040 }
1041 }
1042 out.sort();
1043 out
1044 }
1045
1046 #[test]
1047 fn one_page_build_and_lookup() {
1048 let root = temp_root("one-page");
1049 let mut store = FsIndexStore::open(&root).unwrap();
1050 let e = entry(SEL_PAGE, 1, b"page-1");
1051 let root_id = build(&mut store, std::slice::from_ref(&e)).unwrap();
1052 assert_eq!(lookup(&store, &root_id, &e.key).unwrap(), vec![e.clone()]);
1053 assert!(
1054 lookup(&store, &root_id, &SelectorKey::new(SEL_OBJECT, 7))
1055 .unwrap()
1056 .is_empty()
1057 );
1058 assert_eq!(validate(&store, &root_id).unwrap(), (1, 0));
1059 assert_eq!(store.count().unwrap(), 1);
1060 fs::remove_dir_all(&root).ok();
1061 }
1062
1063 #[test]
1064 fn multi_level_build_finds_every_entry() {
1065 let root = temp_root("multi");
1066 let mut store = FsIndexStore::open(&root).unwrap();
1067 let entries = sample_entries(600);
1068 let root_id = build(&mut store, &entries).unwrap();
1069 let (nodes, depth) = validate(&store, &root_id).unwrap();
1070 assert!(nodes > 1, "expected internal nodes, got {nodes}");
1071 assert!(depth >= 1, "expected depth >= 1, got {depth}");
1072 for e in &entries {
1073 let got = lookup(&store, &root_id, &e.key).unwrap();
1074 assert_eq!(got, vec![e.clone()], "key {:?} not found", e.key);
1075 }
1076 fs::remove_dir_all(&root).ok();
1077 }
1078
1079 #[test]
1080 fn deep_tree_reaches_depth_two() {
1081 let root = temp_root("deep");
1082 let mut store = FsIndexStore::open(&root).unwrap();
1083 let entries = sample_entries(30_000);
1085 let root_id = build(&mut store, &entries).unwrap();
1086 let (nodes, depth) = validate(&store, &root_id).unwrap();
1087 assert!(nodes >= 203, "expected a deep tree, got {nodes} nodes");
1088 assert_eq!(depth, 2);
1089 for e in [&entries[0], &entries[12_345], &entries[29_999]] {
1090 assert_eq!(lookup(&store, &root_id, &e.key).unwrap(), vec![e.clone()]);
1091 }
1092 fs::remove_dir_all(&root).ok();
1093 }
1094
1095 #[test]
1096 fn missing_key_is_empty_not_error() {
1097 let root = temp_root("missing");
1098 let mut store = FsIndexStore::open(&root).unwrap();
1099 let root_id = build(&mut store, &sample_entries(50)).unwrap();
1100 let miss = SelectorKey::with_generation(SEL_PAGE, 999_999, 3);
1101 assert!(lookup(&store, &root_id, &miss).unwrap().is_empty());
1102 fs::remove_dir_all(&root).ok();
1103 }
1104
1105 #[test]
1106 fn empty_index_roundtrips() {
1107 let root = temp_root("empty");
1108 let mut store = FsIndexStore::open(&root).unwrap();
1109 let root_id = build(&mut store, &[]).unwrap();
1110 assert!(
1111 lookup(&store, &root_id, &SelectorKey::new(SEL_PAGE, 1))
1112 .unwrap()
1113 .is_empty()
1114 );
1115 assert_eq!(validate(&store, &root_id).unwrap(), (1, 0));
1116 fs::remove_dir_all(&root).ok();
1117 }
1118
1119 #[test]
1120 fn corrupt_node_is_rejected() {
1121 let root = temp_root("corrupt");
1122 let mut store = FsIndexStore::open(&root).unwrap();
1123 let root_id = build(&mut store, &sample_entries(600)).unwrap();
1124 let root_hex = root_id.to_hex();
1125 let target = node_files(&root.join("index"))
1126 .into_iter()
1127 .find(|p| p.file_name().and_then(|n| n.to_str()) != Some(root_hex.as_str()))
1128 .expect("expected a non-root node");
1129 fs::write(&target, b"corrupted bytes").unwrap();
1130 let err = validate(&store, &root_id).unwrap_err();
1131 assert_eq!(err.class(), crate::ErrorClass::IntegrityMismatch);
1132 fs::remove_dir_all(&root).ok();
1133 }
1134
1135 #[test]
1136 fn missing_child_node_is_missing_external_object() {
1137 let root = temp_root("dangling");
1138 let mut store = FsIndexStore::open(&root).unwrap();
1139 let root_id = build(&mut store, &sample_entries(600)).unwrap();
1140 let root_hex = root_id.to_hex();
1141 let target = node_files(&root.join("index"))
1142 .into_iter()
1143 .find(|p| p.file_name().and_then(|n| n.to_str()) != Some(root_hex.as_str()))
1144 .expect("expected a non-root node");
1145 fs::remove_file(&target).unwrap();
1146 let err = validate(&store, &root_id).unwrap_err();
1147 assert_eq!(err.class(), crate::ErrorClass::MissingExternalObject);
1148 fs::remove_dir_all(&root).ok();
1149 }
1150
1151 #[test]
1152 fn duplicate_id_with_differing_bytes_is_rejected() {
1153 let mut seen: Vec<(NodeId, Vec<u8>, u8)> = Vec::new();
1154 let id = NodeId::of_node(b"a");
1155 assert!(note_node(&mut seen, id, b"a", 0).unwrap());
1156 assert!(!note_node(&mut seen, id, b"a", 0).unwrap());
1158 let err = note_node(&mut seen, id, b"b", 0).unwrap_err();
1160 assert_eq!(err.class(), crate::ErrorClass::IntegrityMismatch);
1161 }
1162
1163 #[test]
1164 fn caps_are_enforced() {
1165 let too_many = sample_entries(MAX_LEAF_ENTRIES as u32 + 1);
1167 let err = encode_leaf(&too_many, 0).unwrap_err();
1168 assert_eq!(err.class(), crate::ErrorClass::ResourceLimit);
1169
1170 let mut node = vec![INDEX_MAGIC, INDEX_VERSION, KIND_LEAF, 0];
1172 node.extend_from_slice(&u32::try_from(MAX_FANOUT + 1).unwrap().to_le_bytes());
1173 node.resize(HEADER_LEN + (MAX_FANOUT + 1) * LEAF_ENTRY_LEN, 0);
1174 let err = parse_node(&node).unwrap_err();
1175 assert_eq!(err.class(), crate::ErrorClass::ResourceLimit);
1176
1177 let oversize = MAX_LEAF_ENTRIES + 1;
1179 let mut node = vec![INDEX_MAGIC, INDEX_VERSION, KIND_LEAF, 0];
1180 node.extend_from_slice(&u32::try_from(oversize).unwrap().to_le_bytes());
1181 node.resize(HEADER_LEN + oversize * LEAF_ENTRY_LEN, 0);
1182 let err = parse_node(&node).unwrap_err();
1183 assert_eq!(err.class(), crate::ErrorClass::ResourceLimit);
1184
1185 let node = vec![
1187 INDEX_MAGIC,
1188 INDEX_VERSION,
1189 KIND_INTERNAL,
1190 MAX_DEPTH + 1,
1191 0,
1192 0,
1193 0,
1194 0,
1195 ];
1196 let err = parse_node(&node).unwrap_err();
1197 assert_eq!(err.class(), crate::ErrorClass::ResourceLimit);
1198 }
1199
1200 #[test]
1201 fn framing_faults_are_rejected() {
1202 let mut node = vec![0x00, INDEX_VERSION, KIND_LEAF, 0, 0, 0, 0, 0];
1204 assert_eq!(
1205 parse_node(&node).unwrap_err().class(),
1206 crate::ErrorClass::IntegrityMismatch
1207 );
1208 node[0] = INDEX_MAGIC;
1210 node[1] = INDEX_VERSION + 1;
1211 assert_eq!(
1212 parse_node(&node).unwrap_err().class(),
1213 crate::ErrorClass::UnsupportedVersion
1214 );
1215 node[1] = INDEX_VERSION;
1217 node[2] = 9;
1218 assert_eq!(
1219 parse_node(&node).unwrap_err().class(),
1220 crate::ErrorClass::IntegrityMismatch
1221 );
1222 assert_eq!(
1224 parse_node(&[INDEX_MAGIC, INDEX_VERSION])
1225 .unwrap_err()
1226 .class(),
1227 crate::ErrorClass::IntegrityMismatch
1228 );
1229 let node = vec![INDEX_MAGIC, INDEX_VERSION, KIND_LEAF, 0, 0, 0, 0, 0, 0xff];
1231 assert_eq!(
1232 parse_node(&node).unwrap_err().class(),
1233 crate::ErrorClass::IntegrityMismatch
1234 );
1235 }
1236
1237 #[test]
1238 fn lookup_reads_bounded_nodes_regardless_of_entry_count() {
1239 let root = temp_root("bounded");
1240 let mut store = FsIndexStore::open(&root).unwrap();
1241 let entries = sample_entries(5_000);
1242 let root_id = build(&mut store, &entries).unwrap();
1243 let probe = entries[2_500].key;
1244 let counter = CountingStore::new(&store);
1245 let found = lookup_impl(&counter, &root_id, &probe).unwrap();
1246 assert_eq!(found.len(), 1);
1247 let reads = counter.gets.get();
1248 assert!(
1249 reads <= u64::from(MAX_DEPTH) + 1,
1250 "lookup read {reads} nodes, exceeding MAX_DEPTH+1"
1251 );
1252 fs::remove_dir_all(&root).ok();
1253 }
1254
1255 #[test]
1256 fn build_is_deterministic_under_shuffle() {
1257 let root = temp_root("determinism");
1258 let mut store = FsIndexStore::open(&root).unwrap();
1259 let entries = sample_entries(1_000);
1260 let id_a = build(&mut store, &entries).unwrap();
1261 let mut shuffled = entries.clone();
1262 shuffled.rotate_left(137);
1263 shuffled.reverse();
1264 let id_b = build(&mut store, &shuffled).unwrap();
1265 assert_eq!(id_a, id_b);
1266 fs::remove_dir_all(&root).ok();
1267 }
1268
1269 #[test]
1270 fn duplicate_entries_are_deduplicated() {
1271 let root = temp_root("dedup");
1272 let mut store = FsIndexStore::open(&root).unwrap();
1273 let entries = sample_entries(10);
1274 let id_a = build(&mut store, &entries).unwrap();
1275 let mut with_dupes = entries.clone();
1276 with_dupes.extend(entries.iter().cloned());
1277 let id_b = build(&mut store, &with_dupes).unwrap();
1278 assert_eq!(id_a, id_b);
1279 fs::remove_dir_all(&root).ok();
1280 }
1281}