Skip to main content

heddle_object_model/object/manifest/
build.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Deterministic construction and expansion of a canonical manifest trie.
3//!
4//! Construction is a pure function of the logical object set: the same set
5//! always yields the same node bytes and therefore the same root hash. Two
6//! sets that differ in one object share every subtree the change does not
7//! touch, so replacing one object rewrites only the old and new routes —
8//! O(path depth), never O(objects).
9
10use std::collections::{BTreeMap, BTreeSet};
11
12use super::node::{
13    MANIFEST_BRANCH_WIDTH, MANIFEST_LEAF_MAX_ENTRIES, MANIFEST_ROUTE_LEVELS, ManifestBranch,
14    ManifestChild, ManifestDecodeError, ManifestKey, ManifestLeaf, ManifestNode, ManifestNodeError,
15    ManifestObject,
16};
17use crate::object::ContentHash;
18
19/// Read access to canonical manifest node bytes, keyed by node address.
20///
21/// The store is content-addressed, so an implementation may be a map, a pack
22/// reader, or a network fetcher; nothing here assumes locality.
23pub trait ManifestNodeSource {
24    fn node_bytes(&self, hash: &ContentHash) -> Option<&[u8]>;
25}
26
27/// Optional whole-store enumeration, used only to report nodes that are
28/// present but unreachable from a root.
29pub trait ManifestNodeStore: ManifestNodeSource {
30    fn node_hashes(&self) -> Vec<ContentHash>;
31}
32
33impl ManifestNodeSource for BTreeMap<ContentHash, Vec<u8>> {
34    fn node_bytes(&self, hash: &ContentHash) -> Option<&[u8]> {
35        self.get(hash).map(Vec::as_slice)
36    }
37}
38
39impl ManifestNodeStore for BTreeMap<ContentHash, Vec<u8>> {
40    fn node_hashes(&self) -> Vec<ContentHash> {
41        self.keys().copied().collect()
42    }
43}
44
45impl ManifestNodeSource for std::collections::HashMap<ContentHash, Vec<u8>> {
46    fn node_bytes(&self, hash: &ContentHash) -> Option<&[u8]> {
47        self.get(hash).map(Vec::as_slice)
48    }
49}
50
51impl ManifestNodeStore for std::collections::HashMap<ContentHash, Vec<u8>> {
52    fn node_hashes(&self) -> Vec<ContentHash> {
53        self.keys().copied().collect()
54    }
55}
56
57/// The output of a build: a root address plus every node byte string it
58/// reaches, deduplicated by address.
59#[derive(Clone, Debug, PartialEq, Eq)]
60pub struct BuiltManifest {
61    pub root: ContentHash,
62    pub nodes: BTreeMap<ContentHash, Vec<u8>>,
63    pub object_count: u64,
64    pub decoded_bytes: u64,
65}
66
67impl BuiltManifest {
68    /// Addresses of every node in this manifest, in address order.
69    pub fn node_hashes(&self) -> Vec<ContentHash> {
70        self.nodes.keys().copied().collect()
71    }
72}
73
74#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
75pub enum ManifestBuildError {
76    #[error(
77        "object key {key:?} appears twice with different declared sizes ({first} and {second})"
78    )]
79    ConflictingDuplicate {
80        key: ManifestKey,
81        first: u64,
82        second: u64,
83    },
84    #[error("total decoded bytes overflow u64")]
85    DecodedBytesOverflow,
86    #[error(transparent)]
87    Node(#[from] ManifestNodeError),
88}
89
90/// Build the canonical manifest for `objects`.
91///
92/// Objects are deduplicated by `(kind, hash)`; an exact repeat is collapsed,
93/// while a repeat that disagrees about `decoded_size` is a caller bug and is
94/// rejected rather than silently resolved.
95pub fn build_manifest(
96    objects: impl IntoIterator<Item = ManifestObject>,
97) -> Result<BuiltManifest, ManifestBuildError> {
98    let mut unique: BTreeMap<ManifestKey, ManifestObject> = BTreeMap::new();
99    for object in objects {
100        let key = object.key();
101        if let Some(existing) = unique.get(&key)
102            && existing.decoded_size != object.decoded_size
103        {
104            return Err(ManifestBuildError::ConflictingDuplicate {
105                key,
106                first: existing.decoded_size,
107                second: object.decoded_size,
108            });
109        }
110        unique.insert(key, object);
111    }
112
113    let entries: Vec<ManifestObject> = unique.into_values().collect();
114    let mut nodes = BTreeMap::new();
115    let summary = build_level(&entries, 0, &mut nodes)?;
116    Ok(BuiltManifest {
117        root: summary.hash,
118        nodes,
119        object_count: summary.object_count,
120        decoded_bytes: summary.decoded_bytes,
121    })
122}
123
124struct Summary {
125    hash: ContentHash,
126    object_count: u64,
127    decoded_bytes: u64,
128}
129
130/// Emit the subtree for `entries` at `depth`.
131///
132/// A leaf is used when the entries fit, or when the fixed 256-bit route is
133/// exhausted and no bits remain to split on. Otherwise the entries are
134/// partitioned by their 5-bit route group at this depth.
135fn build_level(
136    entries: &[ManifestObject],
137    depth: u8,
138    nodes: &mut BTreeMap<ContentHash, Vec<u8>>,
139) -> Result<Summary, ManifestBuildError> {
140    if entries.len() <= MANIFEST_LEAF_MAX_ENTRIES || depth >= MANIFEST_ROUTE_LEVELS {
141        let leaf = ManifestLeaf::new(entries.to_vec())?;
142        let object_count = leaf.object_count();
143        let decoded_bytes = leaf
144            .decoded_bytes()
145            .ok_or(ManifestBuildError::DecodedBytesOverflow)?;
146        let node = ManifestNode::Leaf(leaf);
147        let hash = insert(nodes, &node);
148        return Ok(Summary {
149            hash,
150            object_count,
151            decoded_bytes,
152        });
153    }
154
155    let mut buckets: Vec<Vec<ManifestObject>> = vec![Vec::new(); MANIFEST_BRANCH_WIDTH];
156    for entry in entries {
157        let slot = entry.key().route().group(depth);
158        buckets[usize::from(slot)].push(*entry);
159    }
160
161    let mut children = Vec::new();
162    let mut object_count = 0u64;
163    let mut decoded_bytes = 0u64;
164    for (slot, bucket) in buckets.iter().enumerate() {
165        if bucket.is_empty() {
166            continue;
167        }
168        let child = build_level(bucket, depth + 1, nodes)?;
169        object_count += child.object_count;
170        decoded_bytes = decoded_bytes
171            .checked_add(child.decoded_bytes)
172            .ok_or(ManifestBuildError::DecodedBytesOverflow)?;
173        children.push(ManifestChild {
174            slot: slot as u8,
175            hash: child.hash,
176            object_count: child.object_count,
177            decoded_bytes: child.decoded_bytes,
178        });
179    }
180
181    let branch = ManifestNode::Branch(ManifestBranch::new(depth, children)?);
182    let hash = insert(nodes, &branch);
183    Ok(Summary {
184        hash,
185        object_count,
186        decoded_bytes,
187    })
188}
189
190fn insert(nodes: &mut BTreeMap<ContentHash, Vec<u8>>, node: &ManifestNode) -> ContentHash {
191    let bytes = node.encode();
192    let hash = ContentHash::compute(&bytes);
193    nodes.insert(hash, bytes);
194    hash
195}
196
197#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
198pub enum ManifestExpandError {
199    #[error("manifest node {0} is missing from the node source")]
200    MissingNode(ContentHash),
201    #[error("manifest node {hash}: {source}")]
202    Decode {
203        hash: ContentHash,
204        #[source]
205        source: ManifestDecodeError,
206    },
207    #[error("manifest traversal exceeded the fixed route depth at node {0}")]
208    DepthExceeded(ContentHash),
209}
210
211/// Expand a manifest root into its object set, in canonical plan-key order.
212///
213/// This is the differential-comparison surface: the downstream consumer
214/// compares this ordered expansion against its existing membership rows.
215pub fn expand_manifest<S: ManifestNodeSource + ?Sized>(
216    source: &S,
217    root: &ContentHash,
218) -> Result<Vec<ManifestObject>, ManifestExpandError> {
219    let mut objects = BTreeSet::new();
220    let mut visited = BTreeSet::new();
221    expand_node(source, root, 0, &mut visited, &mut objects)?;
222    Ok(objects.into_iter().collect())
223}
224
225fn expand_node<S: ManifestNodeSource + ?Sized>(
226    source: &S,
227    hash: &ContentHash,
228    depth: u8,
229    visited: &mut BTreeSet<ContentHash>,
230    objects: &mut BTreeSet<ManifestObject>,
231) -> Result<(), ManifestExpandError> {
232    if depth > MANIFEST_ROUTE_LEVELS {
233        return Err(ManifestExpandError::DepthExceeded(*hash));
234    }
235    if !visited.insert(*hash) {
236        return Ok(());
237    }
238    let bytes = source
239        .node_bytes(hash)
240        .ok_or(ManifestExpandError::MissingNode(*hash))?;
241    let node = ManifestNode::decode_addressed(bytes, hash).map_err(|source| {
242        ManifestExpandError::Decode {
243            hash: *hash,
244            source,
245        }
246    })?;
247    match node {
248        ManifestNode::Leaf(leaf) => {
249            objects.extend(leaf.entries().iter().copied());
250        }
251        ManifestNode::Branch(branch) => {
252            for child in branch.children() {
253                expand_node(source, &child.hash, depth + 1, visited, objects)?;
254            }
255        }
256    }
257    Ok(())
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263    use crate::object::manifest::node::ManifestObjectKind;
264
265    fn object(seed: u32) -> ManifestObject {
266        let mut bytes = [0u8; 32];
267        bytes[..4].copy_from_slice(&seed.to_be_bytes());
268        ManifestObject::new(
269            if seed.is_multiple_of(3) {
270                ManifestObjectKind::Tree
271            } else {
272                ManifestObjectKind::Blob
273            },
274            ContentHash::compute(&bytes),
275            u64::from(seed) * 7 + 1,
276        )
277    }
278
279    fn objects(count: u32) -> Vec<ManifestObject> {
280        (0..count).map(object).collect()
281    }
282
283    #[test]
284    fn empty_set_builds_the_canonical_empty_root() {
285        let built = build_manifest([]).unwrap();
286        assert_eq!(built.root, ManifestNode::empty().address());
287        assert_eq!(built.object_count, 0);
288        assert_eq!(built.decoded_bytes, 0);
289        assert!(
290            expand_manifest(&built.nodes, &built.root)
291                .unwrap()
292                .is_empty()
293        );
294    }
295
296    #[test]
297    fn small_sets_stay_a_single_leaf() {
298        let built = build_manifest(objects(MANIFEST_LEAF_MAX_ENTRIES as u32)).unwrap();
299        assert_eq!(built.nodes.len(), 1);
300        let node = ManifestNode::decode(&built.nodes[&built.root]).unwrap();
301        assert!(matches!(node, ManifestNode::Leaf(_)));
302    }
303
304    #[test]
305    fn exceeding_the_leaf_bound_splits_into_a_branch() {
306        let built = build_manifest(objects(MANIFEST_LEAF_MAX_ENTRIES as u32 + 1)).unwrap();
307        let node = ManifestNode::decode(&built.nodes[&built.root]).unwrap();
308        let ManifestNode::Branch(branch) = node else {
309            panic!("expected a branch root");
310        };
311        assert_eq!(branch.depth(), 0);
312        let total: u64 = branch.children().iter().map(|c| c.object_count).sum();
313        assert_eq!(total, MANIFEST_LEAF_MAX_ENTRIES as u64 + 1);
314    }
315
316    #[test]
317    fn build_is_order_independent_and_root_stable() {
318        let mut forward = objects(200);
319        let mut reversed = forward.clone();
320        reversed.reverse();
321        // A duplicate that agrees is collapsed rather than rejected.
322        forward.push(forward[7]);
323
324        let a = build_manifest(forward).unwrap();
325        let b = build_manifest(reversed).unwrap();
326        assert_eq!(a.root, b.root);
327        assert_eq!(a.nodes, b.nodes);
328        assert_eq!(a.object_count, 200);
329    }
330
331    #[test]
332    fn expansion_round_trips_the_object_set_in_key_order() {
333        let mut expected = objects(500);
334        let built = build_manifest(expected.clone()).unwrap();
335        let expanded = expand_manifest(&built.nodes, &built.root).unwrap();
336        expected.sort_by_key(ManifestObject::key);
337        assert_eq!(expanded, expected);
338        assert_eq!(built.object_count, expanded.len() as u64);
339        assert_eq!(
340            built.decoded_bytes,
341            expanded.iter().map(|o| o.decoded_size).sum::<u64>()
342        );
343    }
344
345    #[test]
346    fn replacing_one_object_rewrites_only_a_bounded_path() {
347        let base = objects(2_000);
348        let before = build_manifest(base.clone()).unwrap();
349
350        let mut after_objects = base.clone();
351        after_objects[1_234] = object(999_999);
352        let after = build_manifest(after_objects).unwrap();
353
354        assert_ne!(before.root, after.root);
355        let rewritten = after
356            .nodes
357            .keys()
358            .filter(|hash| !before.nodes.contains_key(*hash))
359            .count();
360        // O(path depth), not O(objects): a 2000-object trie is only a few
361        // levels deep, so a single replacement must not approach node count.
362        assert!(
363            rewritten <= usize::from(MANIFEST_ROUTE_LEVELS),
364            "rewrote {rewritten} nodes for a single object change"
365        );
366        assert!(
367            rewritten * 8 < before.nodes.len(),
368            "rewrote {rewritten} of {} nodes; structural sharing is not holding",
369            before.nodes.len()
370        );
371    }
372
373    #[test]
374    fn an_unchanged_object_set_reuses_the_root_byte_for_byte() {
375        // The context-only-state case: identical content membership must
376        // produce an identical root so nothing is re-published.
377        let set = objects(300);
378        let a = build_manifest(set.clone()).unwrap();
379        let b = build_manifest(set).unwrap();
380        assert_eq!(a.root, b.root);
381        assert_eq!(a.nodes[&a.root], b.nodes[&b.root]);
382    }
383
384    #[test]
385    fn conflicting_duplicate_sizes_are_rejected() {
386        let first = object(1);
387        let second = ManifestObject::new(first.kind, first.hash, first.decoded_size + 1);
388        let err = build_manifest([first, second]).unwrap_err();
389        assert!(matches!(
390            err,
391            ManifestBuildError::ConflictingDuplicate { .. }
392        ));
393    }
394
395    #[test]
396    fn expansion_reports_a_missing_node_rather_than_a_partial_answer() {
397        let built = build_manifest(objects(100)).unwrap();
398        let mut nodes = built.nodes.clone();
399        let victim = *nodes
400            .keys()
401            .find(|hash| **hash != built.root)
402            .expect("branch root has children");
403        nodes.remove(&victim);
404        assert_eq!(
405            expand_manifest(&nodes, &built.root).unwrap_err(),
406            ManifestExpandError::MissingNode(victim)
407        );
408    }
409}