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
//! Dependency source snapshot manifests (bead K002; plan ยง95; risk
//! R84).
//!
//! Registry and git packages materialize from CHECKSUMMED immutable
//! source manifests: one manifest per (source, checksum) identity
//! (K001), listing every member file with its content digest. The
//! manifest serves two masters with one artifact โ materialization
//! (workers reconstruct the tree) and key inputs (the manifest digest
//! is the source identity's proof) โ and it is validated against
//! Cargo's OWN checksum for the package: a mismatch between what Cargo
//! resolved and what the manifest claims REFUSES materialization
//! outright (a poisoned mirror or torn unpack must never silently
//! substitute source bytes).
use rabs_protocol::result_identity::TypedDigest;
/// One member file of a dependency source tree.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SnapshotMember {
/// Path relative to the package root.
pub relative_path: String,
/// Content digest of the file.
pub content_digest: TypedDigest,
/// Executable bit (the H001 FileObject policy keeps it identity).
pub executable: bool,
}
/// The checksummed immutable source manifest.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DependencySourceManifest {
/// The K001 immutable-source checksum this manifest embodies
/// (registry unpack checksum or git revision).
pub source_checksum: String,
/// Cargo's own checksum for the package (lockfile `checksum` field
/// for registry packages; revision hash for git).
pub cargo_checksum: String,
/// Members sorted by relative path.
pub members: Vec<SnapshotMember>,
}
/// Validation failure.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SnapshotError {
/// The manifest's checksum does not match what Cargo resolved.
CargoChecksumMismatch {
/// What the manifest claims.
manifest: String,
/// What Cargo resolved.
resolved: String,
},
/// Members not sorted / duplicated (non-canonical manifest).
NonCanonicalMemberOrder(String),
}
impl DependencySourceManifest {
/// Validate against Cargo's resolved checksum and canonical form.
///
/// # Errors
/// [`SnapshotError`] on mismatch or malformation.
pub fn validate(&self, cargo_resolved_checksum: &str) -> Result<(), SnapshotError> {
if self.cargo_checksum != cargo_resolved_checksum {
return Err(SnapshotError::CargoChecksumMismatch {
manifest: self.cargo_checksum.clone(),
resolved: cargo_resolved_checksum.to_owned(),
});
}
for window in self.members.windows(2) {
if window[0].relative_path >= window[1].relative_path {
return Err(SnapshotError::NonCanonicalMemberOrder(
window[1].relative_path.clone(),
));
}
}
Ok(())
}
/// Serialize to the canonical line format (round-trip anchor).
#[must_use]
pub fn to_canonical_lines(&self) -> String {
let mut out = format!(
"source-checksum {}\ncargo-checksum {}\n",
self.source_checksum, self.cargo_checksum
);
for member in &self.members {
let mut hex = String::with_capacity(64);
for byte in &member.content_digest.bytes {
hex.push_str(&format!("{byte:02x}"));
}
out.push_str(&format!(
"member {} {} {} {}\n",
member.relative_path,
member.content_digest.domain,
hex,
u8::from(member.executable),
));
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use rabs_protocol::result_identity::DigestAlgorithm;
fn d(tag: u8) -> TypedDigest {
TypedDigest {
algorithm: DigestAlgorithm::Sha256V1,
domain: "rabs.object.v1",
bytes: [tag; 32],
}
}
fn manifest() -> DependencySourceManifest {
DependencySourceManifest {
source_checksum: "9f86d081884c7d65".into(),
cargo_checksum: "cargo-sum-abc123".into(),
members: vec![
SnapshotMember {
relative_path: "Cargo.toml".into(),
content_digest: d(1),
executable: false,
},
SnapshotMember {
relative_path: "src/lib.rs".into(),
content_digest: d(2),
executable: false,
},
SnapshotMember {
relative_path: "tools/gen.sh".into(),
content_digest: d(3),
executable: true,
},
],
}
}
#[test]
fn manifests_round_trip_deterministically() {
// THE acceptance: the canonical serialization is stable โ two
// renders of one manifest are byte-identical, and every field
// (paths, digests, exec bits, both checksums) appears.
let a = manifest().to_canonical_lines();
let b = manifest().to_canonical_lines();
assert_eq!(a, b);
assert!(a.contains("source-checksum 9f86d081884c7d65"));
assert!(a.contains("cargo-checksum cargo-sum-abc123"));
assert!(a.contains("member tools/gen.sh rabs.object.v1"));
assert!(a.lines().last().unwrap().ends_with(" 1"), "exec bit");
// A changed member changes the render (identity-bearing).
let mut changed = manifest();
changed.members[1].content_digest = d(9);
assert_ne!(a, changed.to_canonical_lines());
}
#[test]
fn cargo_checksum_mismatch_refuses() {
// THE acceptance: a manifest whose checksum differs from what
// Cargo resolved (poisoned mirror, torn unpack) refuses.
let m = manifest();
assert_eq!(m.validate("cargo-sum-abc123"), Ok(()));
assert_eq!(
m.validate("cargo-sum-EVIL"),
Err(SnapshotError::CargoChecksumMismatch {
manifest: "cargo-sum-abc123".into(),
resolved: "cargo-sum-EVIL".into(),
})
);
}
#[test]
fn non_canonical_member_order_refuses() {
let mut unsorted = manifest();
unsorted.members.swap(0, 2);
assert!(matches!(
unsorted.validate("cargo-sum-abc123"),
Err(SnapshotError::NonCanonicalMemberOrder(_))
));
// Duplicates are non-canonical too (>= comparison).
let mut dup = manifest();
dup.members[1].relative_path = "Cargo.toml".into();
dup.members.swap(0, 1); // keep "sorted" shape with equal keys
assert!(matches!(
dup.validate("cargo-sum-abc123"),
Err(SnapshotError::NonCanonicalMemberOrder(_))
));
}
}