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