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