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    /// Visit canonical entries with a bounded trie frontier (52 levels, at
98    /// most 32 children each). The visitor can read resolution records through
99    /// the same store without retaining the complete map.
100    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    /// `None` deletes the key. Empty maps use a `None` root and store no node.
160    /// Equal values and absent deletions write nothing. The returned root is the
161    /// only publication point; keep the prior root when this returns an error.
162    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
308// Hash keys already provide a fixed 256-bit route. Like manifest routes, read
309// five bits most-significant first; the final group has one real bit.
310fn 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        // Still a valid canonical leaf for the same key; only its claimed blob
715        // address is wrong. A decoder or routing check cannot rescue this test.
716        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}