use crate::hash::{Hash, Hasher, ZERO, hash as blake3_hash};
use crate::object::{EntryMode, Object, Tree, TreeEntry};
use crate::serialize::serialize;
use std::path::PathBuf;
use commonware_cryptography::{Sha256, sha256};
use commonware_parallel::Sequential;
use commonware_runtime::{Runner as _, Supervisor as _, deterministic};
use commonware_storage::{MerkleizedBitMap, merkle::Bagging, merkle::mmr};
const CHUNK_BYTES: usize = 32;
const SPARSE_BAGGING: Bagging = Bagging::ForwardFold;
pub const MAX_LEAVES: u64 = 1_000_000;
pub const MAX_FILTER_PATHS: usize = 100_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SparseManifest {
pub tree_hash: Hash,
pub bitmap_root: Hash,
pub filter_hash: Hash,
pub leaf_count: u64,
}
#[derive(Debug, Clone)]
pub struct SparseProof {
pub bitmap_bytes: Vec<u8>,
}
#[must_use]
pub fn hash_filter(filter: &[PathBuf]) -> Hash {
let mut canonical: Vec<&[u8]> = filter
.iter()
.filter_map(|p| p.to_str().map(str::as_bytes))
.collect();
canonical.sort_unstable();
canonical.dedup();
let mut h = Hasher::new();
for bytes in &canonical {
let len = u32::try_from(bytes.len()).unwrap_or(u32::MAX);
h.update(&len.to_le_bytes());
h.update(bytes);
}
h.finalize()
}
fn entry_matches_filter(entry: &TreeEntry, filter: &[PathBuf]) -> bool {
let name = entry.name.as_slice();
for path in filter {
let Some(bytes) = path.to_str().map(str::as_bytes) else {
continue;
};
if bytes.is_empty() {
continue;
}
if name == bytes {
return true;
}
if name.len() > bytes.len() && name.starts_with(bytes) && name[bytes.len()] == b'/' {
return true;
}
}
false
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum SparseError {
#[error("tree has {actual} entries, exceeds MAX_LEAVES = {}", MAX_LEAVES)]
TooManyLeaves { actual: u64 },
#[error(
"filter has {actual} paths, exceeds MAX_FILTER_PATHS = {}",
MAX_FILTER_PATHS
)]
TooManyFilterPaths { actual: usize },
#[error("source tree entries are not lex-sorted; refusing to build manifest")]
UnsortedTree,
}
pub fn build_sparse(
tree: &Tree,
filter: &[PathBuf],
) -> Result<(Vec<TreeEntry>, SparseManifest, SparseProof), SparseError> {
let leaf_count = u64::try_from(tree.entries.len()).unwrap_or(u64::MAX);
if leaf_count > MAX_LEAVES {
return Err(SparseError::TooManyLeaves { actual: leaf_count });
}
if filter.len() > MAX_FILTER_PATHS {
return Err(SparseError::TooManyFilterPaths {
actual: filter.len(),
});
}
if !tree.is_sorted() {
return Err(SparseError::UnsortedTree);
}
let mut bits: Vec<bool> = Vec::with_capacity(tree.entries.len());
let mut delivered: Vec<TreeEntry> = Vec::new();
for entry in &tree.entries {
let include = entry_matches_filter(entry, filter);
bits.push(include);
if include {
delivered.push(entry.clone());
}
}
let (bitmap_root, bitmap_bytes) = merkleize_bits(&bits);
let manifest = SparseManifest {
tree_hash: tree_hash(tree),
bitmap_root,
filter_hash: hash_filter(filter),
leaf_count,
};
let proof = SparseProof { bitmap_bytes };
Ok((delivered, manifest, proof))
}
fn merkleize_bits(bits: &[bool]) -> (Hash, Vec<u8>) {
let runner = deterministic::Runner::default();
let bits_owned = bits.to_vec();
runner.start(move |ctx| async move {
let hasher = mmr::StandardHasher::<Sha256>::new(SPARSE_BAGGING);
let bitmap: MerkleizedBitMap<_, sha256::Digest, CHUNK_BYTES, Sequential> =
MerkleizedBitMap::init(ctx.child("sparse"), "sparse", Sequential, &hasher)
.await
.expect("in-memory bitmap init cannot fail");
let mut dirty = bitmap.into_dirty();
for b in &bits_owned {
dirty.push(*b);
}
let merkleized = dirty.merkleize(&hasher).expect("merkleize is infallible");
let root = merkleized.root();
let mut bytes = Vec::with_capacity(bits_owned.len().div_ceil(8));
for (i, bit) in bits_owned.iter().enumerate() {
if i % 8 == 0 {
bytes.push(0u8);
}
if *bit {
let last = bytes.last_mut().expect("just pushed a byte");
*last |= 1 << (i % 8);
}
}
let mut root_bytes: Hash = [0u8; 32];
root_bytes.copy_from_slice(root.as_ref());
(root_bytes, bytes)
})
}
#[must_use]
pub fn verify_sparse(
manifest: &SparseManifest,
delivered_entries: &[TreeEntry],
filter: &[PathBuf],
proof: &SparseProof,
) -> bool {
if manifest.leaf_count > MAX_LEAVES {
return false;
}
if filter.len() > MAX_FILTER_PATHS {
return false;
}
if manifest.filter_hash != hash_filter(filter) {
return false;
}
let Ok(leaf_count) = usize::try_from(manifest.leaf_count) else {
return false;
};
let expected_bitmap_bytes = leaf_count.div_ceil(8);
if proof.bitmap_bytes.len() != expected_bitmap_bytes {
return false;
}
let mut set_bits = 0usize;
for i in 0..leaf_count {
let byte = proof.bitmap_bytes[i / 8];
if (byte >> (i % 8)) & 1 == 1 {
set_bits += 1;
}
}
if set_bits != delivered_entries.len() {
return false;
}
for entry in delivered_entries {
if !entry_matches_filter(entry, filter) {
return false;
}
}
let bits: Vec<bool> = (0..leaf_count)
.map(|i| (proof.bitmap_bytes[i / 8] >> (i % 8)) & 1 == 1)
.collect();
let (computed_root, _bytes) = merkleize_bits(&bits);
if computed_root != manifest.bitmap_root {
return false;
}
true
}
#[must_use]
pub fn tree_hash(tree: &Tree) -> Hash {
let Ok(bytes) = serialize(&Object::Tree(tree.clone())) else {
return ZERO;
};
blake3_hash(&bytes)
}
pub const SPARSE_WIRE_MAGIC: [u8; 4] = *b"MSP1";
pub const SPARSE_WIRE_VERSION: u8 = 0x01;
pub const SPARSE_WIRE_MAX_BYTES: usize = 16 * 1024 * 1024;
#[derive(Debug, Clone)]
pub struct SparseResponse {
pub manifest: SparseManifest,
pub entries: Vec<TreeEntry>,
pub proof: SparseProof,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum SparseWireError {
#[error("sparse wire: truncated buffer")]
Truncated,
#[error("sparse wire: bad magic")]
BadMagic,
#[error("sparse wire: unsupported version {0}")]
UnsupportedVersion(u8),
#[error("sparse wire: length out of bounds")]
LengthOutOfBounds,
#[error("sparse wire: leaf_count exceeds MAX_LEAVES")]
TooManyLeaves,
#[error("sparse wire: response exceeds maximum size")]
TooLarge,
#[error("sparse wire: invalid entry mode {0}")]
InvalidEntryMode(u8),
#[error("sparse wire: invalid entry name length")]
InvalidEntryName,
}
pub fn encode_sparse_response(resp: &SparseResponse) -> Result<Vec<u8>, SparseWireError> {
if resp.manifest.leaf_count > MAX_LEAVES {
return Err(SparseWireError::TooManyLeaves);
}
let mut entries_size: usize = 0;
for e in &resp.entries {
if e.name.is_empty() || e.name.len() > 255 {
return Err(SparseWireError::InvalidEntryName);
}
entries_size = entries_size
.checked_add(2 + e.name.len() + 1 + 32)
.ok_or(SparseWireError::TooLarge)?;
}
let total = 113usize
.checked_add(entries_size)
.and_then(|n| n.checked_add(4))
.and_then(|n| n.checked_add(resp.proof.bitmap_bytes.len()))
.ok_or(SparseWireError::TooLarge)?;
if total > SPARSE_WIRE_MAX_BYTES {
return Err(SparseWireError::TooLarge);
}
let mut out = Vec::with_capacity(total);
out.extend_from_slice(&SPARSE_WIRE_MAGIC);
out.push(SPARSE_WIRE_VERSION);
out.extend_from_slice(&resp.manifest.tree_hash);
out.extend_from_slice(&resp.manifest.bitmap_root);
out.extend_from_slice(&resp.manifest.filter_hash);
out.extend_from_slice(&resp.manifest.leaf_count.to_le_bytes());
let entries_len = u32::try_from(resp.entries.len()).map_err(|_| SparseWireError::TooLarge)?;
out.extend_from_slice(&entries_len.to_le_bytes());
for e in &resp.entries {
#[allow(clippy::cast_possible_truncation)]
let name_len = e.name.len() as u16;
out.extend_from_slice(&name_len.to_le_bytes());
out.extend_from_slice(&e.name);
out.push(e.mode as u8);
out.extend_from_slice(&e.object_hash);
}
let bitmap_len =
u32::try_from(resp.proof.bitmap_bytes.len()).map_err(|_| SparseWireError::TooLarge)?;
out.extend_from_slice(&bitmap_len.to_le_bytes());
out.extend_from_slice(&resp.proof.bitmap_bytes);
Ok(out)
}
pub fn decode_sparse_response(buf: &[u8]) -> Result<SparseResponse, SparseWireError> {
if buf.len() > SPARSE_WIRE_MAX_BYTES {
return Err(SparseWireError::TooLarge);
}
if buf.len() < 113 {
return Err(SparseWireError::Truncated);
}
if buf[0..4] != SPARSE_WIRE_MAGIC {
return Err(SparseWireError::BadMagic);
}
if buf[4] != SPARSE_WIRE_VERSION {
return Err(SparseWireError::UnsupportedVersion(buf[4]));
}
let mut tree_hash = [0u8; 32];
tree_hash.copy_from_slice(&buf[5..37]);
let mut bitmap_root = [0u8; 32];
bitmap_root.copy_from_slice(&buf[37..69]);
let mut filter_hash = [0u8; 32];
filter_hash.copy_from_slice(&buf[69..101]);
let mut leaf_count_bytes = [0u8; 8];
leaf_count_bytes.copy_from_slice(&buf[101..109]);
let leaf_count = u64::from_le_bytes(leaf_count_bytes);
if leaf_count > MAX_LEAVES {
return Err(SparseWireError::TooManyLeaves);
}
let mut entries_len_bytes = [0u8; 4];
entries_len_bytes.copy_from_slice(&buf[109..113]);
let entries_len = u32::from_le_bytes(entries_len_bytes) as usize;
if entries_len as u64 > MAX_LEAVES {
return Err(SparseWireError::TooManyLeaves);
}
let mut cursor: usize = 113;
let mut entries: Vec<TreeEntry> = Vec::with_capacity(entries_len.min(64));
for _ in 0..entries_len {
if cursor.checked_add(2).ok_or(SparseWireError::Truncated)? > buf.len() {
return Err(SparseWireError::Truncated);
}
let mut nlb = [0u8; 2];
nlb.copy_from_slice(&buf[cursor..cursor + 2]);
let name_len = u16::from_le_bytes(nlb) as usize;
cursor += 2;
if name_len == 0 || name_len > 255 {
return Err(SparseWireError::InvalidEntryName);
}
let needed = name_len
.checked_add(1)
.and_then(|n| n.checked_add(32))
.ok_or(SparseWireError::LengthOutOfBounds)?;
if cursor
.checked_add(needed)
.ok_or(SparseWireError::Truncated)?
> buf.len()
{
return Err(SparseWireError::Truncated);
}
let name = buf[cursor..cursor + name_len].to_vec();
cursor += name_len;
let mode_byte = buf[cursor];
cursor += 1;
let mode = EntryMode::from_u8(mode_byte)
.map_err(|_| SparseWireError::InvalidEntryMode(mode_byte))?;
let mut object_hash = [0u8; 32];
object_hash.copy_from_slice(&buf[cursor..cursor + 32]);
cursor += 32;
entries.push(TreeEntry {
name,
mode,
object_hash,
});
}
if cursor.checked_add(4).ok_or(SparseWireError::Truncated)? > buf.len() {
return Err(SparseWireError::Truncated);
}
let mut blb = [0u8; 4];
blb.copy_from_slice(&buf[cursor..cursor + 4]);
let bitmap_len = u32::from_le_bytes(blb) as usize;
cursor += 4;
if cursor
.checked_add(bitmap_len)
.ok_or(SparseWireError::LengthOutOfBounds)?
> buf.len()
{
return Err(SparseWireError::LengthOutOfBounds);
}
let bitmap_bytes = buf[cursor..cursor + bitmap_len].to_vec();
cursor += bitmap_len;
if cursor != buf.len() {
return Err(SparseWireError::LengthOutOfBounds);
}
Ok(SparseResponse {
manifest: SparseManifest {
tree_hash,
bitmap_root,
filter_hash,
leaf_count,
},
entries,
proof: SparseProof { bitmap_bytes },
})
}
pub const SPARSE_CACHE_MAGIC: [u8; 4] = *b"MSPC";
pub const SPARSE_CACHE_VERSION: u8 = 0x01;
pub const SPARSE_CACHE_DIR: &str = "sparse";
#[must_use]
pub fn encode_sparse_cache(manifest: &SparseManifest, proof: &SparseProof) -> Vec<u8> {
let mut out = Vec::with_capacity(81 + proof.bitmap_bytes.len());
out.extend_from_slice(&SPARSE_CACHE_MAGIC);
out.push(SPARSE_CACHE_VERSION);
out.extend_from_slice(&manifest.bitmap_root);
out.extend_from_slice(&manifest.filter_hash);
out.extend_from_slice(&manifest.leaf_count.to_le_bytes());
let len = u32::try_from(proof.bitmap_bytes.len()).unwrap_or(u32::MAX);
out.extend_from_slice(&len.to_le_bytes());
out.extend_from_slice(&proof.bitmap_bytes);
out
}
#[allow(clippy::type_complexity)]
pub fn decode_sparse_cache(buf: &[u8]) -> Result<(Hash, Hash, u64, Vec<u8>), SparseWireError> {
if buf.len() < 81 {
return Err(SparseWireError::Truncated);
}
if buf[0..4] != SPARSE_CACHE_MAGIC {
return Err(SparseWireError::BadMagic);
}
if buf[4] != SPARSE_CACHE_VERSION {
return Err(SparseWireError::UnsupportedVersion(buf[4]));
}
let mut bitmap_root = [0u8; 32];
bitmap_root.copy_from_slice(&buf[5..37]);
let mut filter_hash = [0u8; 32];
filter_hash.copy_from_slice(&buf[37..69]);
let mut lcb = [0u8; 8];
lcb.copy_from_slice(&buf[69..77]);
let leaf_count = u64::from_le_bytes(lcb);
if leaf_count > MAX_LEAVES {
return Err(SparseWireError::TooManyLeaves);
}
let mut blb = [0u8; 4];
blb.copy_from_slice(&buf[77..81]);
let bitmap_len = u32::from_le_bytes(blb) as usize;
let end = 81usize
.checked_add(bitmap_len)
.ok_or(SparseWireError::LengthOutOfBounds)?;
if end > buf.len() {
return Err(SparseWireError::LengthOutOfBounds);
}
if end != buf.len() {
return Err(SparseWireError::LengthOutOfBounds);
}
let bitmap_bytes = buf[81..end].to_vec();
Ok((bitmap_root, filter_hash, leaf_count, bitmap_bytes))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hash::ZERO;
use crate::object::{EntryMode, TreeEntry};
fn entry(name: &[u8]) -> TreeEntry {
TreeEntry {
name: name.to_vec(),
mode: EntryMode::Blob,
object_hash: ZERO,
}
}
fn make_tree(n: usize) -> Tree {
assert!(n <= 26 * 26, "test helper only supports n <= 676");
let mut entries = Vec::with_capacity(n);
for i in 0..n {
let a = b'a' + u8::try_from(i / 26).unwrap();
let b = b'a' + u8::try_from(i % 26).unwrap();
entries.push(entry(&[a, b]));
}
Tree { entries }
}
#[test]
fn build_and_verify_round_trip_simple() {
let tree = make_tree(10);
let filter = vec![
PathBuf::from("aa"),
PathBuf::from("ab"),
PathBuf::from("ac"),
];
let (delivered, manifest, proof) = build_sparse(&tree, &filter).unwrap();
assert_eq!(delivered.len(), 3);
assert_eq!(delivered[0].name, b"aa");
assert_eq!(delivered[1].name, b"ab");
assert_eq!(delivered[2].name, b"ac");
assert_eq!(manifest.leaf_count, 10);
assert!(verify_sparse(&manifest, &delivered, &filter, &proof));
}
#[test]
fn verify_rejects_extra_entry() {
let tree = make_tree(10);
let filter = vec![
PathBuf::from("aa"),
PathBuf::from("ab"),
PathBuf::from("ac"),
];
let (mut delivered, manifest, proof) = build_sparse(&tree, &filter).unwrap();
delivered.push(entry(b"ad"));
assert!(
!verify_sparse(&manifest, &delivered, &filter, &proof),
"verifier must reject delivered entries beyond the bitmap's set-bit count"
);
}
#[test]
fn verify_rejects_entry_outside_filter() {
let tree = make_tree(10);
let filter = vec![PathBuf::from("aa"), PathBuf::from("ab")];
let (mut delivered, manifest, proof) = build_sparse(&tree, &filter).unwrap();
assert_eq!(delivered.len(), 2);
delivered[1] = entry(b"az");
assert!(
!verify_sparse(&manifest, &delivered, &filter, &proof),
"verifier must reject any delivered entry not selected by the filter"
);
}
#[test]
fn verify_rejects_tampered_bitmap_bytes() {
let tree = make_tree(10);
let filter = vec![PathBuf::from("aa"), PathBuf::from("ab")];
let (delivered, manifest, mut proof) = build_sparse(&tree, &filter).unwrap();
proof.bitmap_bytes[0] ^= 0b1000_0000;
assert!(
!verify_sparse(&manifest, &delivered, &filter, &proof),
"verifier must reject when bitmap_bytes diverges from manifest.bitmap_root"
);
}
#[test]
fn verify_rejects_tampered_manifest_root() {
let tree = make_tree(10);
let filter = vec![PathBuf::from("aa"), PathBuf::from("ab")];
let (delivered, mut manifest, proof) = build_sparse(&tree, &filter).unwrap();
manifest.bitmap_root[0] ^= 1;
assert!(!verify_sparse(&manifest, &delivered, &filter, &proof));
}
#[test]
fn verify_rejects_wrong_filter() {
let tree = make_tree(10);
let filter_a = vec![PathBuf::from("aa")];
let filter_b = vec![PathBuf::from("ab")];
let (delivered, manifest, proof) = build_sparse(&tree, &filter_a).unwrap();
assert!(!verify_sparse(&manifest, &delivered, &filter_b, &proof));
}
#[test]
fn empty_filter_yields_empty_delivery() {
let tree = make_tree(10);
let filter: Vec<PathBuf> = vec![];
let (delivered, manifest, proof) = build_sparse(&tree, &filter).unwrap();
assert!(delivered.is_empty());
assert_eq!(manifest.leaf_count, 10);
assert!(verify_sparse(&manifest, &delivered, &filter, &proof));
}
#[test]
fn empty_tree_is_well_defined() {
let tree = Tree {
entries: Vec::new(),
};
let filter: Vec<PathBuf> = vec![];
let (delivered, manifest, proof) = build_sparse(&tree, &filter).unwrap();
assert!(delivered.is_empty());
assert_eq!(manifest.leaf_count, 0);
assert!(verify_sparse(&manifest, &delivered, &filter, &proof));
}
#[test]
fn prefix_filter_matches_subtree() {
let entries = vec![
entry(b"a"),
entry(b"src/bar"),
entry(b"src/foo"),
entry(b"srx"),
];
let tree = Tree { entries };
let filter = vec![PathBuf::from("src")];
let (delivered, manifest, proof) = build_sparse(&tree, &filter).unwrap();
assert_eq!(delivered.len(), 2);
assert_eq!(delivered[0].name, b"src/bar");
assert_eq!(delivered[1].name, b"src/foo");
assert!(verify_sparse(&manifest, &delivered, &filter, &proof));
}
#[test]
fn unsorted_tree_is_rejected() {
let tree = Tree {
entries: vec![entry(b"b"), entry(b"a")],
};
let err = build_sparse(&tree, &[]).unwrap_err();
assert_eq!(err, SparseError::UnsortedTree);
}
#[test]
fn filter_hash_is_order_independent() {
let a = hash_filter(&[PathBuf::from("y"), PathBuf::from("x")]);
let b = hash_filter(&[
PathBuf::from("x"),
PathBuf::from("y"),
PathBuf::from("x"), ]);
assert_eq!(a, b);
let c = hash_filter(&[PathBuf::from("x"), PathBuf::from("z")]);
assert_ne!(a, c);
}
#[test]
fn tree_hash_matches_canonical_serialize_then_hash() {
let tree = make_tree(5);
let canonical = crate::hash::hash(
&crate::serialize::serialize(&crate::object::Object::Tree(tree.clone())).unwrap(),
);
assert_eq!(tree_hash(&tree), canonical);
}
#[test]
fn tree_hash_differs_from_phase1_placeholder() {
let tree = make_tree(3);
let mut body = Hasher::new();
let count = u32::try_from(tree.entries.len()).unwrap();
body.update(&count.to_le_bytes());
for entry in &tree.entries {
let name_len = u32::try_from(entry.name.len()).unwrap();
body.update(&name_len.to_le_bytes());
body.update(&entry.name);
body.update(&[entry.mode as u8]);
body.update(&entry.object_hash);
}
let body_digest = body.finalize();
let phase1 = crate::hash::domain_digest(b"mkit-sparse-tree-v1", &body_digest);
assert_ne!(tree_hash(&tree), phase1);
}
#[test]
fn wire_round_trip_simple() {
let tree = make_tree(8);
let filter = vec![PathBuf::from("aa"), PathBuf::from("ab")];
let (entries, manifest, proof) = build_sparse(&tree, &filter).unwrap();
let resp = SparseResponse {
manifest,
entries,
proof,
};
let bytes = encode_sparse_response(&resp).unwrap();
let parsed = decode_sparse_response(&bytes).unwrap();
assert_eq!(parsed.manifest, resp.manifest);
assert_eq!(parsed.entries.len(), resp.entries.len());
for (a, b) in parsed.entries.iter().zip(resp.entries.iter()) {
assert_eq!(a.name, b.name);
assert_eq!(a.mode as u8, b.mode as u8);
assert_eq!(a.object_hash, b.object_hash);
}
assert_eq!(parsed.proof.bitmap_bytes, resp.proof.bitmap_bytes);
assert!(verify_sparse(
&parsed.manifest,
&parsed.entries,
&filter,
&parsed.proof
));
}
#[test]
fn wire_rejects_bad_magic() {
let tree = make_tree(2);
let (entries, manifest, proof) = build_sparse(&tree, &[PathBuf::from("aa")]).unwrap();
let mut bytes = encode_sparse_response(&SparseResponse {
manifest,
entries,
proof,
})
.unwrap();
bytes[0] = 0xFF;
assert_eq!(
decode_sparse_response(&bytes).unwrap_err(),
SparseWireError::BadMagic
);
}
#[test]
fn wire_rejects_unsupported_version() {
let tree = make_tree(2);
let (entries, manifest, proof) = build_sparse(&tree, &[PathBuf::from("aa")]).unwrap();
let mut bytes = encode_sparse_response(&SparseResponse {
manifest,
entries,
proof,
})
.unwrap();
bytes[4] = 0x99;
assert!(matches!(
decode_sparse_response(&bytes).unwrap_err(),
SparseWireError::UnsupportedVersion(0x99)
));
}
#[test]
fn wire_rejects_trailing_garbage() {
let tree = make_tree(2);
let (entries, manifest, proof) = build_sparse(&tree, &[PathBuf::from("aa")]).unwrap();
let mut bytes = encode_sparse_response(&SparseResponse {
manifest,
entries,
proof,
})
.unwrap();
bytes.push(0xAA);
assert_eq!(
decode_sparse_response(&bytes).unwrap_err(),
SparseWireError::LengthOutOfBounds
);
}
#[test]
fn wire_rejects_truncated_header() {
let tiny = vec![0u8; 50];
assert_eq!(
decode_sparse_response(&tiny).unwrap_err(),
SparseWireError::Truncated
);
}
#[test]
fn wire_rejects_overlong_leaf_count() {
let mut buf = Vec::with_capacity(113);
buf.extend_from_slice(&SPARSE_WIRE_MAGIC);
buf.push(SPARSE_WIRE_VERSION);
buf.extend_from_slice(&[0u8; 32]); buf.extend_from_slice(&[0u8; 32]); buf.extend_from_slice(&[0u8; 32]); buf.extend_from_slice(&(MAX_LEAVES + 1).to_le_bytes());
buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes());
assert_eq!(
decode_sparse_response(&buf).unwrap_err(),
SparseWireError::TooManyLeaves
);
}
#[test]
fn cache_round_trip_recovers_bitmap_root() {
let tree = make_tree(10);
let filter = vec![PathBuf::from("ab")];
let (_entries, manifest, proof) = build_sparse(&tree, &filter).unwrap();
let bytes = encode_sparse_cache(&manifest, &proof);
let (bitmap_root, filter_hash, leaf_count, bitmap_bytes) =
decode_sparse_cache(&bytes).unwrap();
assert_eq!(bitmap_root, manifest.bitmap_root);
assert_eq!(filter_hash, manifest.filter_hash);
assert_eq!(leaf_count, manifest.leaf_count);
assert_eq!(bitmap_bytes, proof.bitmap_bytes);
}
#[test]
fn cache_rejects_bad_magic() {
let tree = make_tree(1);
let (_entries, manifest, proof) = build_sparse(&tree, &[PathBuf::from("aa")]).unwrap();
let mut bytes = encode_sparse_cache(&manifest, &proof);
bytes[0] = 0x00;
assert_eq!(
decode_sparse_cache(&bytes).unwrap_err(),
SparseWireError::BadMagic
);
}
}