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