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
//! Tree manifests + missing-chunk diff (bead H006; plan §90; risk
//! R25's transfer arm).
//!
//! Large trees (sysroots, registry sets, snapshots) transfer as
//! HIERARCHICAL manifests: every directory node's digest summarizes
//! its subtree, so comparison short-circuits — an unchanged subtree is
//! one digest equality, not a walk. The missing-diff walks only into
//! subtrees whose digests differ and returns exactly the chunk set the
//! receiver lacks.
use rabs_protocol::result_identity::TypedDigest;
/// A tree node: leaf (file with chunk digests) or directory (children
/// with a summarizing digest).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TreeNode {
/// A file: the chunk digests that reassemble it.
File {
/// Entry name.
name: String,
/// Chunk digests in order.
chunks: Vec<TypedDigest>,
},
/// A directory: children plus the subtree digest that
/// short-circuits comparison.
Directory {
/// Entry name.
name: String,
/// The subtree summary digest (computed over children by the
/// H002 hasher; carried here as data).
subtree_digest: TypedDigest,
/// Child nodes, sorted by name.
children: Vec<TreeNode>,
},
}
impl TreeNode {
/// The node's name.
#[must_use]
pub fn name(&self) -> &str {
match self {
Self::File { name, .. } | Self::Directory { name, .. } => name,
}
}
}
/// Statistics from one diff run (proves the short-circuit happened).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct DiffStats {
/// Directory nodes whose digests matched and were SKIPPED whole.
pub subtrees_skipped: u64,
/// Nodes actually visited.
pub nodes_visited: u64,
}
/// Compute the chunks `have` is missing relative to `want`
/// (receiver-side diff: what must transfer). Returns the missing chunk
/// digests and the walk statistics.
#[must_use]
pub fn missing_chunks(want: &TreeNode, have: Option<&TreeNode>) -> (Vec<TypedDigest>, DiffStats) {
let mut missing = Vec::new();
let mut stats = DiffStats::default();
walk(want, have, &mut missing, &mut stats);
(missing, stats)
}
fn walk(
want: &TreeNode,
have: Option<&TreeNode>,
missing: &mut Vec<TypedDigest>,
stats: &mut DiffStats,
) {
stats.nodes_visited += 1;
match want {
TreeNode::File { chunks, .. } => {
let have_chunks: &[TypedDigest] = match have {
Some(TreeNode::File { chunks, .. }) => chunks,
_ => &[],
};
for chunk in chunks {
if !have_chunks.contains(chunk) && !missing.contains(chunk) {
missing.push(chunk.clone());
}
}
}
TreeNode::Directory {
subtree_digest,
children,
..
} => {
// SHORT-CIRCUIT: identical subtree digest ⇒ nothing below
// can be missing; skip the whole subtree.
if let Some(TreeNode::Directory {
subtree_digest: have_digest,
..
}) = have
&& have_digest == subtree_digest
{
stats.subtrees_skipped += 1;
return;
}
for child in children {
let have_child = match have {
Some(TreeNode::Directory {
children: have_children,
..
}) => have_children.iter().find(|c| c.name() == child.name()),
_ => None,
};
walk(child, have_child, missing, stats);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rabs_protocol::result_identity::DigestAlgorithm;
fn d(tag: u8) -> TypedDigest {
TypedDigest {
algorithm: DigestAlgorithm::Sha256V1,
domain: "rabs.chunk.v1",
bytes: [tag; 32],
}
}
fn subtree(tag: u8) -> TypedDigest {
TypedDigest {
algorithm: DigestAlgorithm::Sha256V1,
domain: "rabs.tree.v1",
bytes: [tag; 32],
}
}
fn file(name: &str, chunk_tags: &[u8]) -> TreeNode {
TreeNode::File {
name: name.into(),
chunks: chunk_tags.iter().map(|t| d(*t)).collect(),
}
}
fn dir(name: &str, digest_tag: u8, children: Vec<TreeNode>) -> TreeNode {
TreeNode::Directory {
name: name.into(),
subtree_digest: subtree(digest_tag),
children,
}
}
/// A tree with two subtrees: `deps` (large) and `src` (small).
fn want_tree(src_chunk: u8) -> TreeNode {
dir(
"root",
if src_chunk == 30 { 100 } else { 101 },
vec![
dir(
"deps",
50,
vec![file("liba.rlib", &[1, 2, 3]), file("libb.rlib", &[4, 5])],
),
dir("src", 60 + src_chunk, vec![file("main.o", &[src_chunk])]),
],
)
}
#[test]
fn empty_receiver_needs_everything() {
let (missing, stats) = missing_chunks(&want_tree(30), None);
assert_eq!(missing.len(), 6, "all chunks missing");
assert_eq!(stats.subtrees_skipped, 0);
}
#[test]
fn unchanged_subtrees_skip_transfer_and_traversal() {
// THE acceptance fixture: receiver HAS an identical `deps`
// subtree (same digest) but an older `src`. The diff must (a)
// return exactly the changed chunk, (b) SKIP the deps subtree
// without walking into it.
let have = want_tree(29); // src differs (chunk 29 vs 30)
let want = want_tree(30);
let (missing, stats) = missing_chunks(&want, Some(&have));
assert_eq!(missing, vec![d(30)], "exactly the changed chunk");
assert_eq!(stats.subtrees_skipped, 1, "deps skipped whole");
// Visited: root + deps(short-circuit visit) + src + main.o = 4
// — NOT the deps children.
assert_eq!(stats.nodes_visited, 4);
}
#[test]
fn identical_trees_diff_to_nothing_in_one_comparison() {
let (missing, stats) = missing_chunks(&want_tree(30), Some(&want_tree(30)));
assert!(missing.is_empty());
assert_eq!(stats.subtrees_skipped, 1, "root itself short-circuits");
assert_eq!(stats.nodes_visited, 1);
}
#[test]
fn missing_diff_is_correct_under_partial_overlap() {
// Receiver has liba but not libb; chunk 2 shared inside liba.
let have = dir(
"root",
200,
vec![dir("deps", 51, vec![file("liba.rlib", &[1, 2])])],
);
let want = dir(
"root",
100,
vec![dir(
"deps",
50,
vec![file("liba.rlib", &[1, 2, 3]), file("libb.rlib", &[4, 5])],
)],
);
let (missing, _) = missing_chunks(&want, Some(&have));
assert_eq!(missing, vec![d(3), d(4), d(5)], "exactly the gap");
// Duplicate chunks across files count once.
let want_dup = dir("root", 100, vec![file("x", &[7]), file("y", &[7])]);
let (missing_dup, _) = missing_chunks(&want_dup, None);
assert_eq!(missing_dup, vec![d(7)]);
}
}