1use super::ContentHash;
6
7const MAGIC: &[u8; 5] = b"HDTM\x01";
8const LEAF_ENTRIES: usize = 8;
9const LEVELS: usize = 52;
10pub const MAX_NODE_BYTES: usize = 5 + 1 + 1 + 4 + 32 * 40;
12
13pub trait SourceTargetMapStore {
16 type Error;
17 fn read(&mut self, hash: ContentHash, max_bytes: usize)
18 -> Result<Option<Vec<u8>>, Self::Error>;
19 fn write(&mut self, hash: ContentHash, bytes: Vec<u8>) -> Result<(), Self::Error>;
20}
21
22#[derive(Clone, Copy, Debug)]
25pub struct MapBudget {
26 pub node_reads: usize,
27 pub read_bytes: usize,
28 pub node_writes: usize,
29 pub write_bytes: usize,
30}
31impl MapBudget {
32 pub const fn new(
33 node_reads: usize,
34 read_bytes: usize,
35 node_writes: usize,
36 write_bytes: usize,
37 ) -> Self {
38 Self {
39 node_reads,
40 read_bytes,
41 node_writes,
42 write_bytes,
43 }
44 }
45}
46
47#[derive(Debug, thiserror::Error)]
48pub enum MapError<E> {
49 #[error("source target map storage error")]
50 Storage(E),
51 #[error("source target map read budget exhausted")]
52 ReadBudget,
53 #[error("source target map write budget exhausted")]
54 WriteBudget,
55 #[error("source target map node is missing: {0}")]
56 MissingNode(ContentHash),
57 #[error("source target map node does not match its blob address: {0}")]
58 HashMismatch(ContentHash),
59 #[error("invalid source target map node: {0}")]
60 InvalidNode(&'static str),
61}
62
63pub struct SourceTargetMap;
64impl SourceTargetMap {
65 pub fn entries<S: SourceTargetMapStore>(
68 store: &mut S,
69 root: Option<ContentHash>,
70 limit: usize,
71 budget: &mut MapBudget,
72 ) -> Result<Vec<(ContentHash, ContentHash)>, MapError<S::Error>> {
73 let mut pending = root
74 .map(|root| vec![(root, None, Vec::new())])
75 .unwrap_or_default();
76 let mut result = Vec::new();
77 while let Some((hash, count, prefix)) = pending.pop() {
78 match load(store, hash, count, &prefix, budget)? {
79 Node::Leaf(entries) => {
80 if result.len().saturating_add(entries.len()) > limit {
81 return Err(MapError::ReadBudget);
82 }
83 result.extend(entries);
84 }
85 Node::Branch { children, .. } => {
86 for child in children.into_iter().rev() {
87 let mut next = prefix.clone();
88 next.push(child.slot);
89 pending.push((child.link.hash, Some(child.link.count), next));
90 }
91 }
92 }
93 }
94 result.sort_unstable_by_key(|entry| entry.0);
95 Ok(result)
96 }
97 pub fn visit_entries<S: SourceTargetMapStore>(
101 store: &mut S,
102 root: Option<ContentHash>,
103 budget: &mut MapBudget,
104 mut visitor: impl FnMut(&mut S, ContentHash, ContentHash) -> Result<(), S::Error>,
105 ) -> Result<(), MapError<S::Error>> {
106 let mut pending = root
107 .map(|root| vec![(root, None, Vec::new())])
108 .unwrap_or_default();
109 while let Some((hash, count, prefix)) = pending.pop() {
110 match load(store, hash, count, &prefix, budget)? {
111 Node::Leaf(entries) => {
112 for (key, value) in entries {
113 visitor(store, key, value).map_err(MapError::Storage)?;
114 }
115 }
116 Node::Branch { children, .. } => {
117 for child in children.into_iter().rev() {
118 let mut next = prefix.clone();
119 next.push(child.slot);
120 pending.push((child.link.hash, Some(child.link.count), next));
121 }
122 }
123 }
124 }
125 Ok(())
126 }
127 pub fn get<S: SourceTargetMapStore>(
128 store: &mut S,
129 root: Option<ContentHash>,
130 key: ContentHash,
131 budget: &mut MapBudget,
132 ) -> Result<Option<ContentHash>, MapError<S::Error>> {
133 let Some(mut hash) = root else {
134 return Ok(None);
135 };
136 let mut expected_count = None;
137 let mut prefix = Vec::new();
138 loop {
139 match load(store, hash, expected_count, &prefix, budget)? {
140 Node::Leaf(entries) => {
141 return Ok(entries
142 .binary_search_by_key(&key, |entry| entry.0)
143 .ok()
144 .map(|index| entries[index].1));
145 }
146 Node::Branch { children, .. } => {
147 let slot = group(key, prefix.len());
148 let Ok(index) = children.binary_search_by_key(&slot, |child| child.slot) else {
149 return Ok(None);
150 };
151 hash = children[index].link.hash;
152 expected_count = Some(children[index].link.count);
153 prefix.push(slot);
154 }
155 }
156 }
157 }
158
159 pub fn update<S: SourceTargetMapStore>(
163 store: &mut S,
164 root: Option<ContentHash>,
165 key: ContentHash,
166 value: Option<ContentHash>,
167 budget: &mut MapBudget,
168 ) -> Result<Option<ContentHash>, MapError<S::Error>> {
169 Ok(update_at(
170 store,
171 root.map(|hash| (hash, None)),
172 key,
173 value,
174 &mut Vec::new(),
175 budget,
176 )?
177 .map(|link| link.hash))
178 }
179}
180
181#[derive(Clone, Copy, PartialEq, Eq)]
182struct Link {
183 hash: ContentHash,
184 count: u64,
185}
186struct Child {
187 slot: u8,
188 link: Link,
189}
190enum Node {
191 Leaf(Vec<(ContentHash, ContentHash)>),
192 Branch { depth: u8, children: Vec<Child> },
193}
194impl Node {
195 fn count(&self) -> Option<u64> {
196 match self {
197 Self::Leaf(entries) => Some(entries.len() as u64),
198 Self::Branch { children, .. } => children
199 .iter()
200 .try_fold(0u64, |total, child| total.checked_add(child.link.count)),
201 }
202 }
203 fn encode(&self) -> Vec<u8> {
204 let mut bytes = Vec::new();
205 bytes.extend_from_slice(MAGIC);
206 match self {
207 Self::Leaf(entries) => {
208 bytes.extend_from_slice(&[0, entries.len() as u8]);
209 for (key, value) in entries {
210 bytes.extend_from_slice(key.as_bytes());
211 bytes.extend_from_slice(value.as_bytes());
212 }
213 }
214 Self::Branch { depth, children } => {
215 bytes.extend_from_slice(&[1, *depth]);
216 let bitmap = children
217 .iter()
218 .fold(0u32, |bits, child| bits | (1u32 << child.slot));
219 bytes.extend_from_slice(&bitmap.to_le_bytes());
220 for child in children {
221 bytes.extend_from_slice(child.link.hash.as_bytes());
222 bytes.extend_from_slice(&child.link.count.to_le_bytes());
223 }
224 }
225 }
226 bytes
227 }
228}
229
230fn take<const N: usize>(bytes: &mut &[u8]) -> Result<[u8; N], &'static str> {
231 let value = bytes
232 .get(..N)
233 .ok_or("truncated node")?
234 .try_into()
235 .map_err(|_| "invalid node field")?;
236 *bytes = &bytes[N..];
237 Ok(value)
238}
239fn decode(mut bytes: &[u8], prefix: &[u8]) -> Result<Node, &'static str> {
240 if &take::<5>(&mut bytes)? != MAGIC {
241 return Err("unsupported node format");
242 }
243 let kind = take::<1>(&mut bytes)?[0];
244 let node = match kind {
245 0 => {
246 let count = usize::from(take::<1>(&mut bytes)?[0]);
247 if !(1..=LEAF_ENTRIES).contains(&count) {
248 return Err("invalid leaf size");
249 }
250 let mut entries = Vec::with_capacity(count);
251 for _ in 0..count {
252 let key = ContentHash::from_bytes(take::<32>(&mut bytes)?);
253 let value = ContentHash::from_bytes(take::<32>(&mut bytes)?);
254 if entries
255 .last()
256 .is_some_and(|entry: &(ContentHash, ContentHash)| entry.0 >= key)
257 {
258 return Err("leaf keys are not strictly ordered");
259 }
260 if prefix
261 .iter()
262 .enumerate()
263 .any(|(depth, slot)| group(key, depth) != *slot)
264 {
265 return Err("leaf key is outside its route");
266 }
267 entries.push((key, value));
268 }
269 Node::Leaf(entries)
270 }
271 1 => {
272 let depth = take::<1>(&mut bytes)?[0];
273 if usize::from(depth) != prefix.len() || prefix.len() >= LEVELS {
274 return Err("invalid branch depth");
275 }
276 let bitmap = u32::from_le_bytes(take::<4>(&mut bytes)?);
277 if bitmap == 0 {
278 return Err("empty branch");
279 }
280 let mut children = Vec::with_capacity(bitmap.count_ones() as usize);
281 for slot in 0..32 {
282 if bitmap & (1 << slot) != 0 {
283 let hash = ContentHash::from_bytes(take::<32>(&mut bytes)?);
284 let count = u64::from_le_bytes(take::<8>(&mut bytes)?);
285 if count == 0 {
286 return Err("empty child");
287 }
288 children.push(Child {
289 slot,
290 link: Link { hash, count },
291 });
292 }
293 }
294 let node = Node::Branch { depth, children };
295 if node.count().ok_or("subtree count overflow")? <= LEAF_ENTRIES as u64 {
296 return Err("branch must collapse to a leaf");
297 }
298 node
299 }
300 _ => return Err("unknown node kind"),
301 };
302 if !bytes.is_empty() {
303 return Err("trailing node bytes");
304 }
305 Ok(node)
306}
307
308fn group(key: ContentHash, depth: usize) -> u8 {
311 let mut slot = 0;
312 for offset in 0..5 {
313 let bit = depth * 5 + offset;
314 slot = (slot << 1)
315 | if bit < 256 {
316 (key.as_bytes()[bit / 8] >> (7 - bit % 8)) & 1
317 } else {
318 0
319 };
320 }
321 slot
322}
323fn load<S: SourceTargetMapStore>(
324 store: &mut S,
325 hash: ContentHash,
326 expected_count: Option<u64>,
327 prefix: &[u8],
328 budget: &mut MapBudget,
329) -> Result<Node, MapError<S::Error>> {
330 if budget.node_reads == 0 || budget.read_bytes == 0 {
331 return Err(MapError::ReadBudget);
332 }
333 budget.node_reads -= 1;
334 let limit = budget.read_bytes.min(MAX_NODE_BYTES);
335 let bytes = store
336 .read(hash, limit)
337 .map_err(MapError::Storage)?
338 .ok_or(MapError::MissingNode(hash))?;
339 if bytes.len() > limit {
340 return Err(MapError::ReadBudget);
341 }
342 budget.read_bytes -= bytes.len();
343 if ContentHash::compute_typed("blob", &bytes) != hash {
344 return Err(MapError::HashMismatch(hash));
345 }
346 let node = decode(&bytes, prefix).map_err(MapError::InvalidNode)?;
347 if expected_count.is_some_and(|count| node.count() != Some(count)) {
348 return Err(MapError::InvalidNode("child count differs from parent"));
349 }
350 Ok(node)
351}
352fn persist<S: SourceTargetMapStore>(
353 store: &mut S,
354 node: Node,
355 budget: &mut MapBudget,
356) -> Result<Link, MapError<S::Error>> {
357 let count = node
358 .count()
359 .ok_or(MapError::InvalidNode("subtree count overflow"))?;
360 let bytes = node.encode();
361 if budget.node_writes == 0 || budget.write_bytes < bytes.len() {
362 return Err(MapError::WriteBudget);
363 }
364 budget.node_writes -= 1;
365 budget.write_bytes -= bytes.len();
366 let hash = ContentHash::compute_typed("blob", &bytes);
367 store.write(hash, bytes).map_err(MapError::Storage)?;
368 Ok(Link { hash, count })
369}
370fn build<S: SourceTargetMapStore>(
371 store: &mut S,
372 entries: Vec<(ContentHash, ContentHash)>,
373 depth: usize,
374 budget: &mut MapBudget,
375) -> Result<Link, MapError<S::Error>> {
376 if entries.len() <= LEAF_ENTRIES {
377 return persist(store, Node::Leaf(entries), budget);
378 }
379 if depth >= LEVELS {
380 return Err(MapError::InvalidNode("key route exhausted"));
381 }
382 let mut groups: [Vec<_>; 32] = std::array::from_fn(|_| Vec::new());
383 for entry in entries {
384 groups[usize::from(group(entry.0, depth))].push(entry);
385 }
386 let mut children = Vec::new();
387 for (slot, entries) in groups.into_iter().enumerate() {
388 if !entries.is_empty() {
389 children.push(Child {
390 slot: slot as u8,
391 link: build(store, entries, depth + 1, budget)?,
392 });
393 }
394 }
395 persist(
396 store,
397 Node::Branch {
398 depth: depth as u8,
399 children,
400 },
401 budget,
402 )
403}
404fn collect<S: SourceTargetMapStore>(
405 store: &mut S,
406 link: Link,
407 prefix: &[u8],
408 entries: &mut Vec<(ContentHash, ContentHash)>,
409 budget: &mut MapBudget,
410) -> Result<(), MapError<S::Error>> {
411 if link.count > LEAF_ENTRIES as u64 {
412 return Err(MapError::InvalidNode("collapse exceeds leaf bound"));
413 }
414 match load(store, link.hash, Some(link.count), prefix, budget)? {
415 Node::Leaf(mut leaf) => {
416 if entries.len() + leaf.len() > LEAF_ENTRIES {
417 return Err(MapError::InvalidNode("collapse exceeds leaf bound"));
418 }
419 entries.append(&mut leaf);
420 }
421 Node::Branch { .. } => return Err(MapError::InvalidNode("small subtree is not canonical")),
422 }
423 Ok(())
424}
425fn update_at<S: SourceTargetMapStore>(
426 store: &mut S,
427 old: Option<(ContentHash, Option<u64>)>,
428 key: ContentHash,
429 value: Option<ContentHash>,
430 prefix: &mut Vec<u8>,
431 budget: &mut MapBudget,
432) -> Result<Option<Link>, MapError<S::Error>> {
433 let Some((hash, expected_count)) = old else {
434 return value
435 .map(|value| persist(store, Node::Leaf(vec![(key, value)]), budget))
436 .transpose();
437 };
438 let node = load(store, hash, expected_count, prefix, budget)?;
439 let original = Link {
440 hash,
441 count: node
442 .count()
443 .ok_or(MapError::InvalidNode("subtree count overflow"))?,
444 };
445 match node {
446 Node::Leaf(mut entries) => {
447 match (entries.binary_search_by_key(&key, |entry| entry.0), value) {
448 (Ok(index), Some(value)) if entries[index].1 == value => return Ok(Some(original)),
449 (Err(_), None) => return Ok(Some(original)),
450 (Ok(index), Some(value)) => entries[index].1 = value,
451 (Ok(index), None) => {
452 entries.remove(index);
453 }
454 (Err(index), Some(value)) => entries.insert(index, (key, value)),
455 }
456 if entries.is_empty() {
457 return Ok(None);
458 }
459 Ok(Some(build(store, entries, prefix.len(), budget)?))
460 }
461 Node::Branch {
462 depth,
463 mut children,
464 } => {
465 let slot = group(key, prefix.len());
466 let position = children.binary_search_by_key(&slot, |child| child.slot);
467 let previous = position.ok().map(|index| children[index].link);
468 prefix.push(slot);
469 let result = update_at(
470 store,
471 previous.map(|link| (link.hash, Some(link.count))),
472 key,
473 value,
474 prefix,
475 budget,
476 );
477 prefix.pop();
478 let next = result?;
479 if previous == next {
480 return Ok(Some(original));
481 }
482 match (position, next) {
483 (Ok(index), Some(link)) => children[index].link = link,
484 (Ok(index), None) => {
485 children.remove(index);
486 }
487 (Err(index), Some(link)) => children.insert(index, Child { slot, link }),
488 (Err(_), None) => return Ok(Some(original)),
489 }
490 if children.is_empty() {
491 return Ok(None);
492 }
493 let node = Node::Branch { depth, children };
494 if node
495 .count()
496 .ok_or(MapError::InvalidNode("subtree count overflow"))?
497 <= LEAF_ENTRIES as u64
498 {
499 let Node::Branch { children, .. } = node else {
500 return Err(MapError::InvalidNode("expected branch"));
501 };
502 let mut entries = Vec::new();
503 for child in children {
504 prefix.push(child.slot);
505 let result = collect(store, child.link, prefix, &mut entries, budget);
506 prefix.pop();
507 result?;
508 }
509 entries.sort_unstable_by_key(|entry| entry.0);
510 return Ok(Some(persist(store, Node::Leaf(entries), budget)?));
511 }
512 Ok(Some(persist(store, node, budget)?))
513 }
514 }
515}
516
517#[cfg(test)]
518mod tests {
519 use std::collections::BTreeMap;
520
521 use super::*;
522
523 #[derive(Debug, thiserror::Error)]
524 #[error("{0}")]
525 struct StoreError(&'static str);
526 #[derive(Default)]
527 struct MemoryStore {
528 nodes: BTreeMap<ContentHash, Vec<u8>>,
529 reads: usize,
530 read_bytes: usize,
531 writes: usize,
532 write_bytes: usize,
533 }
534 impl MemoryStore {
535 fn reset_counts(&mut self) {
536 self.reads = 0;
537 self.read_bytes = 0;
538 self.writes = 0;
539 self.write_bytes = 0;
540 }
541 }
542 impl SourceTargetMapStore for MemoryStore {
543 type Error = StoreError;
544 fn read(
545 &mut self,
546 hash: ContentHash,
547 max_bytes: usize,
548 ) -> Result<Option<Vec<u8>>, Self::Error> {
549 self.reads += 1;
550 let Some(bytes) = self.nodes.get(&hash) else {
551 return Ok(None);
552 };
553 if bytes.len() > max_bytes {
554 return Err(StoreError("bounded read refused"));
555 }
556 self.read_bytes += bytes.len();
557 Ok(Some(bytes.clone()))
558 }
559 fn write(&mut self, hash: ContentHash, bytes: Vec<u8>) -> Result<(), Self::Error> {
560 assert_eq!(
561 hash,
562 ContentHash::compute_typed("blob", &bytes),
563 "ordinary blob storage key"
564 );
565 assert!(bytes.len() <= MAX_NODE_BYTES, "bounded canonical node");
566 self.writes += 1;
567 self.write_bytes += bytes.len();
568 if let Some(old) = self.nodes.insert(hash, bytes.clone()) {
569 assert_eq!(old, bytes, "writes cannot replace immutable content");
570 }
571 Ok(())
572 }
573 }
574 fn budget() -> MapBudget {
575 MapBudget::new(1024, 2_000_000, 1024, 2_000_000)
576 }
577 fn key(value: u32) -> ContentHash {
578 ContentHash::compute(&value.to_le_bytes())
579 }
580 fn put(
581 store: &mut MemoryStore,
582 root: Option<ContentHash>,
583 key: ContentHash,
584 value: Option<ContentHash>,
585 ) -> Option<ContentHash> {
586 SourceTargetMap::update(store, root, key, value, &mut budget()).expect("bounded map update")
587 }
588 fn get(
589 store: &mut MemoryStore,
590 root: Option<ContentHash>,
591 key: ContentHash,
592 ) -> Option<ContentHash> {
593 SourceTargetMap::get(store, root, key, &mut budget()).expect("bounded map lookup")
594 }
595
596 #[test]
597 fn one_and_ten_thousand_bindings_touch_only_one_bounded_route() {
598 for size in [1, 10_000] {
599 let mut store = MemoryStore::default();
600 let mut root = None;
601 for index in 0..size {
602 root = put(&mut store, root, key(index), Some(key(index + 100_000)));
603 }
604 store.reset_counts();
605 let before = budget();
606 let mut work = before;
607 let changed =
608 SourceTargetMap::update(&mut store, root, key(0), Some(key(900_000)), &mut work)
609 .expect("one replacement");
610 assert_ne!(changed, root);
611 assert!(
612 (1..=6).contains(&store.reads),
613 "replacement must not scan {size} bindings: {} reads",
614 store.reads
615 );
616 assert!(
617 (1..=6).contains(&store.writes),
618 "replacement must not rewrite {size} bindings: {} writes",
619 store.writes
620 );
621 eprintln!(
622 "{size} bindings: replacement {} reads/{} bytes, {} writes/{} bytes",
623 store.reads, store.read_bytes, store.writes, store.write_bytes
624 );
625 assert_eq!(before.node_reads - work.node_reads, store.reads);
626 assert_eq!(before.read_bytes - work.read_bytes, store.read_bytes);
627 assert_eq!(before.node_writes - work.node_writes, store.writes);
628 assert_eq!(before.write_bytes - work.write_bytes, store.write_bytes);
629 store.reset_counts();
630 assert_eq!(get(&mut store, changed, key(0)), Some(key(900_000)));
631 assert!(
632 (1..=6).contains(&store.reads),
633 "lookup follows only key's route"
634 );
635 assert_eq!(store.writes, 0);
636 assert_eq!(
637 get(&mut store, root, key(0)),
638 Some(key(100_000)),
639 "parent root is unchanged"
640 );
641 if size > 1 {
642 assert_eq!(
643 get(&mut store, changed, key(size - 1)),
644 Some(key(size - 1 + 100_000))
645 );
646 }
647 }
648 }
649
650 #[test]
651 fn unchanged_updates_and_absent_deletions_write_zero_nodes() {
652 let mut store = MemoryStore::default();
653 let mut root = None;
654 for index in 0..100 {
655 root = put(&mut store, root, key(index), Some(key(index + 100)));
656 }
657 store.reset_counts();
658 let mut no_writes = MapBudget::new(64, 100_000, 0, 0);
659 assert_eq!(
660 SourceTargetMap::update(&mut store, root, key(37), Some(key(137)), &mut no_writes)
661 .expect("equal value needs no writes"),
662 root
663 );
664 assert_eq!(
665 SourceTargetMap::update(&mut store, root, key(999), None, &mut no_writes)
666 .expect("absent key needs no writes"),
667 root
668 );
669 assert_eq!(store.writes, 0);
670 assert_eq!(store.write_bytes, 0);
671 store.reset_counts();
672 assert_eq!(put(&mut store, None, key(999), None), None);
673 assert_eq!((store.reads, store.writes), (0, 0));
674 }
675
676 #[test]
677 fn forks_share_roots_and_deletions_collapse_to_canonical_nodes() {
678 let mut store = MemoryStore::default();
679 let mut parent = None;
680 for index in 0..48 {
681 parent = put(&mut store, parent, key(index), Some(key(index + 100)));
682 }
683 store.reset_counts();
684 let mut fork = parent;
685 assert_eq!(
686 (store.reads, store.writes),
687 (0, 0),
688 "fork copies just the root"
689 );
690 for index in 0..41 {
691 fork = put(&mut store, fork, key(index), None);
692 }
693 let mut rebuilt = None;
694 for index in (41..48).rev() {
695 rebuilt = put(&mut store, rebuilt, key(index), Some(key(index + 100)));
696 }
697 assert_eq!(
698 fork, rebuilt,
699 "collapsed map has a history-independent canonical root"
700 );
701 assert_eq!(get(&mut store, fork, key(0)), None);
702 assert_eq!(get(&mut store, parent, key(0)), Some(key(100)));
703 for index in 41..48 {
704 fork = put(&mut store, fork, key(index), None);
705 }
706 assert_eq!(fork, None);
707 assert_eq!(get(&mut store, parent, key(47)), Some(key(147)));
708 }
709
710 #[test]
711 fn hash_integrity_missing_nodes_and_exact_routes_are_checked() {
712 let mut store = MemoryStore::default();
713 let root = put(&mut store, None, key(1), Some(key(2))).expect("root");
714 let changed = Node::Leaf(vec![(key(1), key(3))]).encode();
717 let original = store.nodes.insert(root, changed).expect("original bytes");
718 assert!(
719 matches!(SourceTargetMap::get(&mut store, Some(root), key(1), &mut budget()), Err(MapError::HashMismatch(hash)) if hash == root)
720 );
721 store.nodes.remove(&root);
722 assert!(
723 matches!(SourceTargetMap::get(&mut store, Some(root), key(1), &mut budget()), Err(MapError::MissingNode(hash)) if hash == root)
724 );
725 store.nodes.insert(root, original);
726 assert_eq!(get(&mut store, Some(root), key(1)), Some(key(2)));
727
728 let child = persist(
729 &mut store,
730 Node::Leaf(vec![(key(1), key(2))]),
731 &mut budget(),
732 )
733 .expect("valid leaf");
734 let other_slot = (group(key(1), 0) + 1) % 32;
735 let branch = Node::Branch {
736 depth: 0,
737 children: vec![Child {
738 slot: other_slot,
739 link: Link { count: 9, ..child },
740 }],
741 };
742 let branch =
743 persist(&mut store, branch, &mut budget()).expect("store malformed routing fixture");
744 let mut wrong_key = *key(1).as_bytes();
745 wrong_key[0] = (other_slot << 3) | (wrong_key[0] & 7);
746 assert!(matches!(
747 SourceTargetMap::get(
748 &mut store,
749 Some(branch.hash),
750 ContentHash::from_bytes(wrong_key),
751 &mut budget()
752 ),
753 Err(MapError::InvalidNode("leaf key is outside its route"))
754 ));
755 }
756
757 #[test]
758 fn reads_and_updates_stop_at_budget_without_installing_a_partial_root() {
759 let mut store = MemoryStore::default();
760 let mut root = None;
761 for index in 0..100 {
762 root = put(&mut store, root, key(index), Some(key(index + 100)));
763 }
764 store.reset_counts();
765 assert!(matches!(
766 SourceTargetMap::get(
767 &mut store,
768 root,
769 key(1),
770 &mut MapBudget::new(0, 100_000, 0, 0)
771 ),
772 Err(MapError::ReadBudget)
773 ));
774 assert_eq!(store.reads, 0, "reject before physical store read");
775 let mut bounded = MapBudget::new(64, 100_000, 1, 100_000);
776 assert!(matches!(
777 SourceTargetMap::update(&mut store, root, key(1), Some(key(900_000)), &mut bounded),
778 Err(MapError::WriteBudget)
779 ));
780 assert_eq!(
781 store.writes, 1,
782 "only permitted unreachable immutable write occurred"
783 );
784 assert_eq!(
785 get(&mut store, root, key(1)),
786 Some(key(101)),
787 "caller still has valid original root"
788 );
789 store.reset_counts();
790 assert!(matches!(
791 SourceTargetMap::get(&mut store, root, key(1), &mut MapBudget::new(64, 1, 0, 0)),
792 Err(MapError::Storage(StoreError("bounded read refused")))
793 ));
794 assert_eq!(
795 store.read_bytes, 0,
796 "backend refuses oversized body before fetching it"
797 );
798 store.reset_counts();
799 assert!(matches!(
800 SourceTargetMap::update(
801 &mut store,
802 root,
803 key(1),
804 Some(key(900_000)),
805 &mut MapBudget::new(64, 100_000, 64, 1)
806 ),
807 Err(MapError::WriteBudget)
808 ));
809 assert_eq!(
810 store.writes, 0,
811 "byte budget is checked before physical write"
812 );
813 }
814
815 #[test]
816 fn common_prefix_and_insertion_order_preserve_canonical_roots() {
817 let mut store = MemoryStore::default();
818 let mut forward = None;
819 let mut reverse = None;
820 let keys: Vec<_> = (0..16)
821 .map(|n| {
822 let mut bytes = [0; 32];
823 bytes[31] = n;
824 ContentHash::from_bytes(bytes)
825 })
826 .collect();
827 for key in &keys {
828 forward = put(&mut store, forward, *key, Some(*key));
829 }
830 for key in keys.iter().rev() {
831 reverse = put(&mut store, reverse, *key, Some(*key));
832 }
833 assert_eq!(
834 forward, reverse,
835 "routing remains deterministic through a very long common prefix"
836 );
837 for key in &keys {
838 assert_eq!(get(&mut store, forward, *key), Some(*key));
839 }
840 let mut visited = Vec::new();
841 SourceTargetMap::visit_entries(
842 &mut store,
843 forward,
844 &mut MapBudget::new(usize::MAX, 100_000, 0, 0),
845 |_, key, value| {
846 visited.push((key, value));
847 Ok(())
848 },
849 )
850 .expect("stream a deep canonical route");
851 assert_eq!(
852 visited,
853 keys.iter().map(|key| (*key, *key)).collect::<Vec<_>>()
854 );
855 let mut remaining = forward;
856 for key in &keys[..9] {
857 remaining = put(&mut store, remaining, *key, None);
858 }
859 for key in &keys[9..] {
860 assert_eq!(get(&mut store, remaining, *key), Some(*key));
861 }
862 }
863}