Skip to main content

heddle_object_model/object/
source_target_map.rs

1//! Immutable source-target bindings. Forks share a root; updates copy one trie
2//! route, never the map or its referencing annotations. Addresses are ordinary
3//! blob hashes. A failed update may leave unreachable immutable nodes, but
4//! never returns a replacement root for the caller to install.
5use super::ContentHash;
6
7const MAGIC: &[u8; 5] = b"HDTM\x01";
8const LEAF_ENTRIES: usize = 8;
9const LEVELS: usize = 52;
10/// Largest canonical node: header, depth, bitmap and 32 (hash, count) children.
11pub const MAX_NODE_BYTES: usize = 5 + 1 + 1 + 4 + 32 * 40;
12
13/// Storage must bound a read before allocating/fetching more than `max_bytes`.
14/// Writes install immutable bytes at their ordinary blob address.
15pub 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/// Remaining work allowance. Reads/writes are charged before the store call;
23/// read bytes are charged from the actual returned node, not its maximum size.
24#[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    /// Bounded canonical enumeration for transfer verification and rebuilding
66    /// indexes. Capture updates still use individual trie routes.
67    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    /// `None` deletes the key. Empty maps use a `None` root and store no node.
130    /// Equal values and absent deletions write nothing. The returned root is the
131    /// only publication point; keep the prior root when this returns an error.
132    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
278// Hash keys already provide a fixed 256-bit route. Like manifest routes, read
279// five bits most-significant first; the final group has one real bit.
280fn 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        // Still a valid canonical leaf for the same key; only its claimed blob
685        // address is wrong. A decoder or routing check cannot rescue this test.
686        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}