1use crate::hash::{Hash, Hasher};
8use crate::object::{EntryMode, Object, Tree, TreeEntry};
9use crate::serialize::{deserialize, serialize};
10use std::path::PathBuf;
11
12pub const MAX_LEAVES: u64 = 1_000_000;
13pub const MAX_FILTER_PATHS: usize = 100_000;
14pub const MAX_FILTER_BYTES: usize = 1024 * 1024;
15pub const SPARSE_WIRE_MAGIC: [u8; 4] = *b"MSP1";
16pub const SPARSE_WIRE_VERSION: u8 = 2;
17pub const SPARSE_WIRE_MAX_BYTES: usize = 16 * 1024 * 1024;
18pub const SPARSE_CACHE_MAGIC: [u8; 4] = *b"MSPC";
19pub const SPARSE_CACHE_VERSION: u8 = 2;
20pub const SPARSE_CACHE_DIR: &str = "sparse";
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub struct SparseManifest {
24 pub tree_hash: Hash,
25 pub filter_hash: Hash,
26}
27#[derive(Debug, Clone)]
28pub struct SparseProof {
29 pub tree_bytes: Vec<u8>,
31}
32#[derive(Debug, Clone)]
33pub struct SparseResponse {
34 pub manifest: SparseManifest,
35 pub proof: SparseProof,
36}
37#[derive(Debug, Clone)]
39pub struct VerifiedSparseTree {
40 pub manifest: SparseManifest,
41 pub entries: Vec<TreeEntry>,
42}
43#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
44pub enum SparseError {
45 #[error("tree has {actual} entries, exceeds MAX_LEAVES")]
46 TooManyLeaves { actual: u64 },
47 #[error("filter has {actual} paths, exceeds MAX_FILTER_PATHS")]
48 TooManyFilterPaths { actual: usize },
49 #[error("source tree is not strictly sorted")]
50 UnsortedTree,
51 #[error("invalid canonical tree")]
52 InvalidTree,
53 #[error("unsupported filter; requires full authenticated metadata")]
54 UnsupportedFilter,
55 #[error("witness exceeds size limit; requires full authenticated metadata")]
56 TooLarge,
57}
58#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
59pub enum SparseWireError {
60 #[error("sparse wire: truncated")]
61 Truncated,
62 #[error("sparse wire: bad magic")]
63 BadMagic,
64 #[error("sparse wire: unsupported version {0}")]
65 UnsupportedVersion(u8),
66 #[error("sparse wire: length out of bounds")]
67 LengthOutOfBounds,
68 #[error("sparse wire: response exceeds maximum size")]
69 TooLarge,
70 #[error("sparse wire: invalid tree witness")]
71 InvalidTree,
72}
73
74pub fn validate_filter(filter: &[PathBuf]) -> Result<(), SparseError> {
78 if filter.len() > MAX_FILTER_PATHS {
79 return Err(SparseError::TooManyFilterPaths {
80 actual: filter.len(),
81 });
82 }
83 let mut total = 0usize;
84 for path in filter {
85 let value = path.to_str().ok_or(SparseError::UnsupportedFilter)?;
86 total = total
87 .checked_add(value.len())
88 .ok_or(SparseError::UnsupportedFilter)?;
89 if total > MAX_FILTER_BYTES
90 || value.is_empty()
91 || value.contains(['!', '*', '?', '[', ']', '\\'])
92 || (value != "."
93 && value
94 .split('/')
95 .any(|p| p.is_empty() || p == "." || p == ".."))
96 {
97 return Err(SparseError::UnsupportedFilter);
98 }
99 }
100 Ok(())
101}
102#[must_use]
103pub fn hash_filter(filter: &[PathBuf]) -> Hash {
104 let mut canonical: Vec<_> = filter
105 .iter()
106 .map(|p| p.as_os_str().as_encoded_bytes())
107 .collect();
108 canonical.sort_unstable();
109 canonical.dedup();
110 let mut h = Hasher::new();
111 h.update(b"mkit-sparse-filter-v2\0");
112 for bytes in canonical {
113 h.update(&(bytes.len() as u64).to_le_bytes());
114 h.update(bytes);
115 }
116 h.finalize()
117}
118fn matches(entry: &TreeEntry, filter: &[PathBuf]) -> bool {
119 filter.iter().any(|p| {
120 let prefix = p.as_os_str().as_encoded_bytes();
121 prefix == b"."
122 || prefix == entry.name
123 || (entry.mode == EntryMode::Tree
124 && prefix.starts_with(&entry.name)
125 && prefix.get(entry.name.len()) == Some(&b'/'))
126 })
127}
128fn validate_tree(tree: &Tree) -> Result<(), SparseError> {
129 if tree.entries.len() as u64 > MAX_LEAVES {
130 return Err(SparseError::TooManyLeaves {
131 actual: tree.entries.len() as u64,
132 });
133 }
134 if !tree.is_sorted() {
135 return Err(SparseError::UnsortedTree);
136 }
137 if tree
138 .entries
139 .iter()
140 .any(|e| !TreeEntry::validate_name(&e.name))
141 {
142 return Err(SparseError::InvalidTree);
143 }
144 Ok(())
145}
146#[must_use]
147pub fn tree_hash(tree: &Tree) -> Hash {
148 crate::merkle::compute_tree_id(tree)
149}
150
151pub fn build_sparse(tree: &Tree, filter: &[PathBuf]) -> Result<SparseResponse, SparseError> {
152 validate_filter(filter)?;
153 validate_tree(tree)?;
154 let size = tree
156 .entries
157 .iter()
158 .try_fold(10usize, |n, e| n.checked_add(37 + e.name.len()))
159 .ok_or(SparseError::TooLarge)?;
160 if size > SPARSE_WIRE_MAX_BYTES - 73 {
161 return Err(SparseError::TooLarge);
162 }
163 let tree_bytes =
164 serialize(&Object::Tree(tree.clone())).map_err(|_| SparseError::InvalidTree)?;
165 Ok(SparseResponse {
166 manifest: SparseManifest {
167 tree_hash: tree_hash(tree),
168 filter_hash: hash_filter(filter),
169 },
170 proof: SparseProof { tree_bytes },
171 })
172}
173pub fn verify_sparse(
176 expected_tree: &Hash,
177 filter: &[PathBuf],
178 response: &SparseResponse,
179) -> Result<VerifiedSparseTree, SparseWireError> {
180 validate_filter(filter).map_err(|_| SparseWireError::InvalidTree)?;
181 if response.manifest.tree_hash != *expected_tree
182 || response.manifest.filter_hash != hash_filter(filter)
183 {
184 return Err(SparseWireError::InvalidTree);
185 }
186 let tree = witness_tree(&response.proof)?;
187 if tree_hash(&tree) != *expected_tree {
188 return Err(SparseWireError::InvalidTree);
189 }
190 Ok(VerifiedSparseTree {
191 manifest: response.manifest,
192 entries: tree
193 .entries
194 .into_iter()
195 .filter(|entry| matches(entry, filter))
196 .collect(),
197 })
198}
199
200fn witness_tree(proof: &SparseProof) -> Result<Tree, SparseWireError> {
201 if proof.tree_bytes.len() > SPARSE_WIRE_MAX_BYTES - 73 {
202 return Err(SparseWireError::TooLarge);
203 }
204 let Object::Tree(tree) =
205 deserialize(&proof.tree_bytes).map_err(|_| SparseWireError::InvalidTree)?
206 else {
207 return Err(SparseWireError::InvalidTree);
208 };
209 validate_tree(&tree).map_err(|_| SparseWireError::InvalidTree)?;
210 if serialize(&Object::Tree(tree.clone())).map_err(|_| SparseWireError::InvalidTree)?
211 != proof.tree_bytes
212 {
213 return Err(SparseWireError::InvalidTree);
214 }
215 Ok(tree)
216}
217pub fn encode_sparse_response(resp: &SparseResponse) -> Result<Vec<u8>, SparseWireError> {
220 let mut out = encode_sparse_cache(&resp.manifest, &resp.proof)?;
221 out[..4].copy_from_slice(&SPARSE_WIRE_MAGIC);
222 Ok(out)
223}
224pub fn decode_sparse_response(buf: &[u8]) -> Result<SparseResponse, SparseWireError> {
225 let (manifest, proof) = decode_envelope(buf, SPARSE_WIRE_MAGIC)?;
226 Ok(SparseResponse { manifest, proof })
227}
228pub fn encode_sparse_cache(
229 manifest: &SparseManifest,
230 proof: &SparseProof,
231) -> Result<Vec<u8>, SparseWireError> {
232 witness_tree(proof)?;
233 let mut out = Vec::with_capacity(73 + proof.tree_bytes.len());
234 out.extend_from_slice(&SPARSE_CACHE_MAGIC);
235 out.push(SPARSE_CACHE_VERSION);
236 out.extend_from_slice(&manifest.tree_hash);
237 out.extend_from_slice(&manifest.filter_hash);
238 out.extend_from_slice(
239 &u32::try_from(proof.tree_bytes.len())
240 .map_err(|_| SparseWireError::TooLarge)?
241 .to_le_bytes(),
242 );
243 out.extend_from_slice(&proof.tree_bytes);
244 Ok(out)
245}
246pub fn decode_sparse_cache(buf: &[u8]) -> Result<(SparseManifest, SparseProof), SparseWireError> {
247 decode_envelope(buf, SPARSE_CACHE_MAGIC)
248}
249fn decode_envelope(
250 buf: &[u8],
251 magic: [u8; 4],
252) -> Result<(SparseManifest, SparseProof), SparseWireError> {
253 if buf.len() > SPARSE_WIRE_MAX_BYTES {
254 return Err(SparseWireError::TooLarge);
255 }
256 if buf.len() < 5 {
257 return Err(SparseWireError::Truncated);
258 }
259 if buf[..4] != magic {
260 return Err(SparseWireError::BadMagic);
261 }
262 if buf[4] != SPARSE_WIRE_VERSION {
263 return Err(SparseWireError::UnsupportedVersion(buf[4]));
264 }
265 if buf.len() < 73 {
266 return Err(SparseWireError::Truncated);
267 }
268 let len = u32::from_le_bytes(
269 buf[69..73]
270 .try_into()
271 .map_err(|_| SparseWireError::Truncated)?,
272 ) as usize;
273 if len != buf.len() - 73 {
274 return Err(SparseWireError::LengthOutOfBounds);
275 }
276 let manifest = SparseManifest {
277 tree_hash: buf[5..37]
278 .try_into()
279 .map_err(|_| SparseWireError::Truncated)?,
280 filter_hash: buf[37..69]
281 .try_into()
282 .map_err(|_| SparseWireError::Truncated)?,
283 };
284 let proof = SparseProof {
285 tree_bytes: buf[73..].to_vec(),
286 };
287 let tree = witness_tree(&proof)?;
288 if tree_hash(&tree) != manifest.tree_hash {
289 return Err(SparseWireError::InvalidTree);
290 }
291 Ok((manifest, proof))
292}
293
294pub fn verify_sparse_hierarchy(
298 root: Hash,
299 filter: &[PathBuf],
300 mut fetch: impl FnMut(&Hash, &[PathBuf]) -> Result<SparseResponse, SparseWireError>,
301) -> Result<Vec<(Vec<u8>, TreeEntry)>, SparseWireError> {
302 validate_filter(filter).map_err(|_| SparseWireError::InvalidTree)?;
303 let mut canonical_filter = filter.to_vec();
304 canonical_filter.sort();
305 canonical_filter.dedup();
306 let mut pending_bytes: usize = canonical_filter.iter().map(|p| p.as_os_str().len()).sum();
307 let mut pending = vec![(root, Vec::<u8>::new(), canonical_filter, 0usize)];
308 let mut result = Vec::new();
309 let mut total = 0usize;
310 let mut visited = 0usize;
311 while let Some((id, path, filter, depth)) = pending.pop() {
312 pending_bytes -= path.len() + filter.iter().map(|p| p.as_os_str().len()).sum::<usize>();
313 visited += 1;
314 if visited > 100_000 || depth > 256 {
315 return Err(SparseWireError::TooLarge);
316 }
317 let response = fetch(&id, &filter)?;
318 total = total
319 .checked_add(response.proof.tree_bytes.len())
320 .ok_or(SparseWireError::TooLarge)?;
321 if total > 64 * 1024 * 1024 {
322 return Err(SparseWireError::TooLarge);
323 }
324 let verified = verify_sparse(&id, &filter, &response)?;
325 for entry in verified.entries {
326 let mut full = path.clone();
327 if !full.is_empty() {
328 full.push(b'/');
329 }
330 full.extend_from_slice(&entry.name);
331 if full.len() > 4096 || result.len() as u64 >= MAX_LEAVES {
332 return Err(SparseWireError::TooLarge);
333 }
334 if entry.mode == EntryMode::Tree {
335 let mut child_filter = Vec::new();
336 for prefix in &filter {
337 let bytes = prefix.as_os_str().as_encoded_bytes();
338 if bytes == b"." || bytes == entry.name {
339 child_filter.push(PathBuf::from("."));
340 } else if bytes.starts_with(&entry.name)
341 && bytes.get(entry.name.len()) == Some(&b'/')
342 {
343 let suffix = std::str::from_utf8(&bytes[entry.name.len() + 1..])
344 .map_err(|_| SparseWireError::InvalidTree)?;
345 child_filter.push(PathBuf::from(suffix));
346 }
347 }
348 child_filter.sort();
349 child_filter.dedup();
350 pending_bytes = pending_bytes
351 .checked_add(
352 full.len()
353 + child_filter
354 .iter()
355 .map(|p| p.as_os_str().len())
356 .sum::<usize>(),
357 )
358 .ok_or(SparseWireError::TooLarge)?;
359 if pending.len() >= 100_000 || pending_bytes > 64 * 1024 * 1024 {
360 return Err(SparseWireError::TooLarge);
361 }
362 pending.push((entry.object_hash, full.clone(), child_filter, depth + 1));
363 }
364 result.push((full, entry));
365 }
366 }
367 result.sort_by(|a, b| a.0.cmp(&b.0));
368 Ok(result)
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374 fn tree() -> Tree {
375 Tree {
376 entries: vec![
377 entry(b"a", EntryMode::Blob, [1; 32]),
378 entry(b"b", EntryMode::Blob, [2; 32]),
379 ],
380 }
381 }
382 fn entry(name: &[u8], mode: EntryMode, object_hash: Hash) -> TreeEntry {
383 TreeEntry {
384 name: name.to_vec(),
385 mode,
386 object_hash,
387 }
388 }
389 fn response(tree: &Tree, filter: &[PathBuf]) -> SparseResponse {
390 build_sparse(tree, filter).unwrap()
391 }
392 #[test]
393 fn canonical_identity_and_content_substitution() {
394 let tree = tree();
395 let root = Object::Tree(tree.clone()).id().unwrap();
396 let filter = [PathBuf::from("a")];
397 let r = response(&tree, &filter);
398 assert_eq!(r.manifest.tree_hash, root);
399 let verified = verify_sparse(&root, &filter, &r).unwrap();
400 assert_eq!(verified.entries, vec![tree.entries[0].clone()]);
401 for altered in [
402 Tree {
403 entries: vec![
404 entry(b"a", EntryMode::Blob, [9; 32]),
405 tree.entries[1].clone(),
406 ],
407 },
408 Tree {
409 entries: vec![
410 entry(b"a", EntryMode::Tree, [1; 32]),
411 tree.entries[1].clone(),
412 ],
413 },
414 Tree {
415 entries: vec![tree.entries[1].clone()],
416 },
417 ] {
418 let forged = SparseResponse {
419 manifest: r.manifest,
420 proof: response(&altered, &filter).proof,
421 };
422 assert!(verify_sparse(&root, &filter, &forged).is_err());
423 }
424 assert!(verify_sparse(&[9; 32], &filter, &r).is_err());
425 }
426 #[test]
427 fn wire_cache_reject_wrong_version_trailing_and_substitution() {
428 let r = response(&tree(), &[PathBuf::from(".")]);
429 let bytes = encode_sparse_response(&r).unwrap();
430 let decoded = decode_sparse_response(&bytes).unwrap();
431 assert_eq!(
432 verify_sparse(&r.manifest.tree_hash, &[PathBuf::from(".")], &decoded)
433 .unwrap()
434 .entries,
435 tree().entries
436 );
437 let mut bad = bytes.clone();
438 bad[4] = 1;
439 assert!(matches!(
440 decode_sparse_response(&bad),
441 Err(SparseWireError::UnsupportedVersion(1))
442 ));
443 let mut bad = bytes.clone();
444 bad.push(0);
445 assert!(decode_sparse_response(&bad).is_err());
446 let mut bad = bytes;
447 bad[5] ^= 1;
448 assert!(decode_sparse_response(&bad).is_err());
449 let cache = encode_sparse_cache(&r.manifest, &r.proof).unwrap();
450 assert_eq!(decode_sparse_cache(&cache).unwrap().0, r.manifest);
451 }
452 #[test]
453 fn hierarchy_authenticates_children_and_requires_completeness() {
454 let child = tree();
455 let root = Tree {
456 entries: vec![entry(b"src", EntryMode::Tree, tree_hash(&child))],
457 };
458 let filter = [PathBuf::from("src/a")];
459 let result = verify_sparse_hierarchy(tree_hash(&root), &filter, |id, f| {
460 Ok(response(
461 if *id == tree_hash(&root) {
462 &root
463 } else {
464 &child
465 },
466 f,
467 ))
468 })
469 .unwrap();
470 assert_eq!(
471 result.iter().map(|x| x.0.as_slice()).collect::<Vec<_>>(),
472 vec![b"src".as_slice(), b"src/a".as_slice()]
473 );
474 assert!(
475 verify_sparse_hierarchy(tree_hash(&root), &filter, |id, f| {
476 if *id == tree_hash(&root) {
477 Ok(response(&root, f))
478 } else {
479 Err(SparseWireError::Truncated)
480 }
481 })
482 .is_err()
483 );
484 assert!(
485 verify_sparse_hierarchy(tree_hash(&root), &filter, |_, f| Ok(response(&root, f)))
486 .is_err()
487 );
488 }
489 #[test]
490 fn sparse_v2_golden_bytes() {
491 let r = response(&tree(), &[PathBuf::from("a")]);
492 let bytes = encode_sparse_response(&r).unwrap();
493 if std::env::var_os("MKIT_UPDATE_SPARSE_GOLDEN").is_some() {
494 std::fs::write(
495 concat!(
496 env!("CARGO_MANIFEST_DIR"),
497 "/../../tests/golden/sparse/response_v2.bin"
498 ),
499 &bytes,
500 )
501 .unwrap();
502 return;
503 }
504 assert_eq!(
505 bytes,
506 include_bytes!("../../../tests/golden/sparse/response_v2.bin")
507 );
508 assert_eq!(r.manifest.tree_hash, Object::Tree(tree()).id().unwrap());
509 }
510 #[test]
511 fn strict_filters_and_invalid_flattened_names() {
512 for filter in ["", "/a", "a/", "../a", "a/*", "!a", "a//b"] {
513 assert!(build_sparse(&tree(), &[PathBuf::from(filter)]).is_err());
514 }
515 let bad = Tree {
516 entries: vec![entry(b"src/a", EntryMode::Blob, [0; 32])],
517 };
518 assert!(build_sparse(&bad, &[]).is_err());
519 assert!(
520 verify_sparse(&tree_hash(&tree()), &[], &response(&tree(), &[]))
521 .unwrap()
522 .entries
523 .is_empty()
524 );
525 assert_eq!(
526 verify_sparse(
527 &tree_hash(&tree()),
528 &[PathBuf::from(".")],
529 &response(&tree(), &[PathBuf::from(".")])
530 )
531 .unwrap()
532 .entries
533 .len(),
534 2
535 );
536 }
537}