prolly-map 0.3.0

Content-addressed versioned map storage primitives.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
//! Store-to-store synchronization helpers for content-addressed tree nodes.

use super::cid::Cid;
use super::error::Error;
use super::node::Node;
use super::tree::Tree;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, VecDeque};

/// Current in-memory format version for portable tree snapshot bundles.
pub const SNAPSHOT_BUNDLE_FORMAT_VERSION: u32 = 1;

const SNAPSHOT_BUNDLE_BYTES_VERSION: u32 = SNAPSHOT_BUNDLE_FORMAT_VERSION;

/// Dry-run plan for making a destination store able to read one source tree.
///
/// `required_cids` are all node CIDs reachable from the tree root. `missing_cids`
/// are the subset not currently present in the destination store. All CIDs are
/// sorted by raw CID bytes for deterministic transport planning.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MissingNodePlan {
    /// All reachable node CIDs required by the tree.
    pub required_cids: Vec<Cid>,
    /// Number of reachable nodes required by the tree.
    pub required_nodes: usize,
    /// Serialized bytes for all reachable nodes in the source tree.
    pub required_bytes: usize,
    /// Required node CIDs not present in the destination store.
    pub missing_cids: Vec<Cid>,
    /// Number of missing destination nodes.
    pub missing_nodes: usize,
    /// Serialized bytes for missing nodes as read from the source store.
    pub missing_bytes: usize,
}

impl MissingNodePlan {
    /// Whether the destination already has every required node.
    pub fn is_empty(&self) -> bool {
        self.missing_cids.is_empty()
    }

    /// Return all required CIDs in deterministic byte order.
    pub fn required_cids(&self) -> &[Cid] {
        &self.required_cids
    }

    /// Return missing CIDs in deterministic byte order.
    pub fn missing_cids(&self) -> &[Cid] {
        &self.missing_cids
    }
}

/// Result of copying missing nodes from one store to another.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MissingNodeCopy {
    /// Dry-run plan used for this copy.
    pub plan: MissingNodePlan,
    /// Number of nodes written to the destination store.
    pub copied_nodes: usize,
    /// Serialized bytes written to the destination store.
    pub copied_bytes: usize,
}

/// One content-addressed node included in a portable tree snapshot bundle.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SnapshotBundleNode {
    /// CID bytes the node is stored under.
    pub cid: Cid,
    /// Serialized node bytes whose SHA-256 CID must equal `cid`.
    pub bytes: Vec<u8>,
}

/// Self-contained transport bundle for one tree and its reachable node bytes.
///
/// The bundle is intended for import/export between stores, processes, and
/// language bindings. `nodes` should contain exactly the node CIDs reachable
/// from `tree.root`, sorted by raw CID bytes for deterministic transport.
#[derive(Clone, Debug, PartialEq)]
pub struct SnapshotBundle {
    /// Bundle schema version. Currently always `1`.
    pub format_version: u32,
    /// Tree handle the imported store will be able to read.
    pub tree: Tree,
    /// Reachable serialized nodes for the tree.
    pub nodes: Vec<SnapshotBundleNode>,
}

/// Compact metadata for a validated snapshot bundle.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SnapshotBundleSummary {
    /// Bundle schema version.
    pub format_version: u32,
    /// Tree root CID, or `None` for an empty tree.
    pub root: Option<Cid>,
    /// Number of unique node CIDs included in the bundle.
    pub node_count: usize,
    /// Total serialized bytes across unique bundled nodes.
    pub byte_count: usize,
    /// Smallest serialized node payload in the bundle.
    pub min_node_bytes: usize,
    /// Largest serialized node payload in the bundle.
    pub max_node_bytes: usize,
}

/// Result of verifying a snapshot bundle as a self-contained tree.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SnapshotBundleVerification {
    /// True when the bundle has exactly the reachable node set for its tree.
    pub valid: bool,
    /// Validated bundle metadata.
    pub summary: SnapshotBundleSummary,
    /// Number of reachable CIDs discovered from the tree root.
    pub reachable_nodes: usize,
    /// Serialized bytes for reachable bundled nodes.
    pub reachable_bytes: usize,
    /// Reachable CIDs absent from the bundle.
    pub missing_cids: Vec<Cid>,
    /// Bundled CIDs not reachable from the tree root.
    pub extra_cids: Vec<Cid>,
}

#[derive(Serialize, Deserialize)]
struct SnapshotBundleWire {
    version: u32,
    tree: Tree,
    nodes: Vec<SnapshotBundleNodeWire>,
}

#[derive(Serialize, Deserialize)]
struct SnapshotBundleNodeWire {
    cid: Vec<u8>,
    bytes: Vec<u8>,
}

impl SnapshotBundle {
    /// Create a versioned snapshot bundle from a tree and reachable node bytes.
    pub fn new(tree: Tree, nodes: Vec<SnapshotBundleNode>) -> Self {
        Self {
            format_version: SNAPSHOT_BUNDLE_FORMAT_VERSION,
            tree,
            nodes,
        }
    }

    /// Number of serialized nodes in the bundle.
    pub fn node_count(&self) -> usize {
        self.nodes.len()
    }

    /// Total serialized node bytes in the bundle.
    pub fn byte_count(&self) -> usize {
        self.nodes.iter().map(|node| node.bytes.len()).sum()
    }

    /// Return the SHA-256 digest of this bundle's canonical byte encoding.
    ///
    /// The digest is stable for semantically equivalent bundles: node entries
    /// are canonicalized by CID before encoding, so caller-side ordering does
    /// not affect the result.
    pub fn digest(&self) -> Result<Cid, Error> {
        self.to_bytes().map(|bytes| Cid::from_bytes(&bytes))
    }

    /// Return validated metadata for this bundle without importing it.
    ///
    /// The summary canonicalizes node order, deduplicates identical repeated
    /// nodes, and verifies each node byte payload against its CID.
    pub fn summary(&self) -> Result<SnapshotBundleSummary, Error> {
        self.validate_format_version()?;
        let nodes = canonical_snapshot_nodes(&self.nodes)?;
        Ok(snapshot_bundle_summary(self, &nodes))
    }

    /// Verify that this bundle is complete and contains no unreachable nodes.
    ///
    /// This check is read-only: it validates version, canonicalizes nodes,
    /// verifies every node byte payload by CID, decodes reachable nodes from
    /// the supplied bytes, and compares the reachable CID set with the bundled
    /// CID set.
    pub fn verify(&self) -> Result<SnapshotBundleVerification, Error> {
        self.validate_format_version()?;
        let nodes = canonical_snapshot_nodes(&self.nodes)?;
        let summary = snapshot_bundle_summary(self, &nodes);
        let nodes_by_cid = nodes
            .iter()
            .map(|node| (node.cid.as_bytes().to_vec(), node.bytes.as_slice()))
            .collect::<BTreeMap<_, _>>();
        let reachability = reachable_snapshot_nodes(&self.tree, &nodes_by_cid)?;
        let provided_cids = nodes_by_cid.keys().cloned().collect::<Vec<_>>();

        let missing_cids = reachability
            .reachable_cids
            .iter()
            .filter(|cid| !nodes_by_cid.contains_key(*cid))
            .cloned()
            .map(cid_from_wire_bytes)
            .collect::<Result<Vec<_>, Error>>()?;
        let extra_cids = provided_cids
            .iter()
            .filter(|cid| !reachability.reachable_cids.contains(*cid))
            .cloned()
            .map(cid_from_wire_bytes)
            .collect::<Result<Vec<_>, Error>>()?;

        Ok(SnapshotBundleVerification {
            valid: missing_cids.is_empty() && extra_cids.is_empty(),
            summary,
            reachable_nodes: reachability.reachable_cids.len(),
            reachable_bytes: reachability.reachable_bytes,
            missing_cids,
            extra_cids,
        })
    }

    /// Validate that the bundle version is supported by this crate.
    pub fn validate_format_version(&self) -> Result<(), Error> {
        if self.format_version == SNAPSHOT_BUNDLE_FORMAT_VERSION {
            Ok(())
        } else {
            Err(Error::InvalidSnapshotBundle(format!(
                "unsupported format version {}",
                self.format_version
            )))
        }
    }

    /// Serialize this bundle as deterministic, versioned bytes.
    ///
    /// The encoded form canonicalizes node order by CID bytes and deduplicates
    /// repeated identical nodes. It rejects unsupported bundle versions,
    /// malformed CIDs, and node bytes whose content hash does not match the
    /// supplied CID.
    pub fn to_bytes(&self) -> Result<Vec<u8>, Error> {
        self.validate_format_version()?;
        let nodes = canonical_snapshot_nodes(&self.nodes)?;
        let wire = SnapshotBundleWire {
            version: SNAPSHOT_BUNDLE_BYTES_VERSION,
            tree: self.tree.clone(),
            nodes: nodes
                .into_iter()
                .map(|node| SnapshotBundleNodeWire {
                    cid: node.cid.as_bytes().to_vec(),
                    bytes: node.bytes,
                })
                .collect(),
        };
        serde_cbor::ser::to_vec_packed(&wire).map_err(|err| Error::Serialize(err.to_string()))
    }

    /// Decode a deterministic, versioned snapshot bundle byte payload.
    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
        let wire: SnapshotBundleWire =
            serde_cbor::from_slice(bytes).map_err(snapshot_bundle_deserialize)?;
        if wire.version != SNAPSHOT_BUNDLE_BYTES_VERSION {
            return Err(Error::InvalidSnapshotBundle(format!(
                "unsupported bytes version {}",
                wire.version
            )));
        }
        let nodes = wire
            .nodes
            .into_iter()
            .map(|node| {
                let cid = cid_from_wire_bytes(node.cid)?;
                verify_node_bytes(&cid, &node.bytes)?;
                Ok(SnapshotBundleNode {
                    cid,
                    bytes: node.bytes,
                })
            })
            .collect::<Result<Vec<_>, Error>>()?;
        Ok(Self {
            format_version: SNAPSHOT_BUNDLE_FORMAT_VERSION,
            tree: wire.tree,
            nodes: canonical_snapshot_nodes(&nodes)?,
        })
    }
}

struct SnapshotBundleReachability {
    reachable_cids: Vec<Vec<u8>>,
    reachable_bytes: usize,
}

fn snapshot_bundle_summary(
    bundle: &SnapshotBundle,
    nodes: &[SnapshotBundleNode],
) -> SnapshotBundleSummary {
    let byte_count = nodes.iter().map(|node| node.bytes.len()).sum();
    let min_node_bytes = nodes
        .iter()
        .map(|node| node.bytes.len())
        .min()
        .unwrap_or_default();
    let max_node_bytes = nodes
        .iter()
        .map(|node| node.bytes.len())
        .max()
        .unwrap_or_default();

    SnapshotBundleSummary {
        format_version: bundle.format_version,
        root: bundle.tree.root.clone(),
        node_count: nodes.len(),
        byte_count,
        min_node_bytes,
        max_node_bytes,
    }
}

fn reachable_snapshot_nodes(
    tree: &Tree,
    nodes_by_cid: &BTreeMap<Vec<u8>, &[u8]>,
) -> Result<SnapshotBundleReachability, Error> {
    let mut seen = BTreeMap::<Vec<u8>, ()>::new();
    let mut missing = BTreeMap::<Vec<u8>, ()>::new();
    let mut frontier = VecDeque::new();
    let mut reachable_bytes = 0usize;

    if let Some(root) = &tree.root {
        frontier.push_back(root.as_bytes().to_vec());
    }

    while let Some(cid) = frontier.pop_front() {
        if seen.contains_key(&cid) {
            continue;
        }
        seen.insert(cid.clone(), ());

        let Some(bytes) = nodes_by_cid.get(&cid) else {
            missing.insert(cid, ());
            continue;
        };

        let node = Node::from_bytes(bytes)?;
        if node.keys.len() != node.vals.len() {
            return Err(Error::InvalidNode);
        }
        reachable_bytes += bytes.len();

        if !node.leaf {
            for child in &node.vals {
                let cid = child
                    .as_slice()
                    .try_into()
                    .map(Cid)
                    .map_err(|_| Error::InvalidNode)?;
                if !seen.contains_key(cid.as_bytes()) {
                    frontier.push_back(cid.as_bytes().to_vec());
                }
            }
        }
    }

    let mut reachable_cids = seen.into_keys().collect::<Vec<_>>();
    for cid in missing.into_keys() {
        if !reachable_cids.contains(&cid) {
            reachable_cids.push(cid);
        }
    }
    reachable_cids.sort();

    Ok(SnapshotBundleReachability {
        reachable_cids,
        reachable_bytes,
    })
}

pub(crate) fn verify_node_bytes(expected: &Cid, bytes: &[u8]) -> Result<(), Error> {
    let actual = Cid::from_bytes(bytes);
    if &actual == expected {
        Ok(())
    } else {
        Err(Error::CidMismatch {
            expected: expected.clone(),
            actual,
        })
    }
}

fn canonical_snapshot_nodes(
    nodes: &[SnapshotBundleNode],
) -> Result<Vec<SnapshotBundleNode>, Error> {
    let mut by_cid: BTreeMap<Vec<u8>, Vec<u8>> = BTreeMap::new();
    for node in nodes {
        verify_node_bytes(&node.cid, &node.bytes)?;
        let cid = node.cid.as_bytes().to_vec();
        if let Some(existing) = by_cid.get(&cid) {
            if existing != &node.bytes {
                return Err(Error::InvalidSnapshotBundle(format!(
                    "bundle contains conflicting duplicate node CID {}",
                    hex_bytes(&cid)
                )));
            }
            continue;
        }
        by_cid.insert(cid, node.bytes.clone());
    }
    by_cid
        .into_iter()
        .map(|(cid, bytes)| {
            Ok(SnapshotBundleNode {
                cid: cid_from_wire_bytes(cid)?,
                bytes,
            })
        })
        .collect()
}

fn cid_from_wire_bytes(bytes: Vec<u8>) -> Result<Cid, Error> {
    let cid: [u8; 32] = bytes.try_into().map_err(|bytes: Vec<u8>| {
        Error::InvalidSnapshotBundle(format!("CID must be exactly 32 bytes, got {}", bytes.len()))
    })?;
    Ok(Cid(cid))
}

fn snapshot_bundle_deserialize(error: serde_cbor::Error) -> Error {
    Error::InvalidSnapshotBundle(format!("could not decode bundle bytes: {error}"))
}

fn hex_bytes(bytes: &[u8]) -> String {
    bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}