use serde::{Deserialize, Serialize};
use super::error::BlobError;
pub const BLOB_REF_MAGIC: [u8; 4] = [0xB0, 0xB1, 0xB2, 0xB3];
pub const BLOB_REF_DISCRIMINATOR: u8 = BLOB_REF_MAGIC[0];
pub const BLOB_REF_VERSION_V1: u8 = 0x01;
pub const BLOB_REF_VERSION_V2_MANIFEST: u8 = 0x02;
pub const BLOB_REF_VERSION_V3_TREE: u8 = 0x03;
pub const BLOB_TREE_BODY_VERSION: u8 = 0x01;
pub const BLOB_REF_TREE_BODY_MAX_BYTES: usize = 1024;
pub const BLOB_TREE_MAX_TOTAL_SIZE: u64 = 128 * (1u64 << 50);
pub const BLOB_MANIFEST_BODY_VERSION: u8 = 0x01;
pub const BLOB_REF_SMALL_HEADER_LEN: usize = 4 + 1 + 32 + 8;
pub const BLOB_REF_MAX_SIZE: u64 = 16 * 1024 * 1024 * 1024;
pub const BLOB_CHUNK_SIZE_BYTES: u64 = 4 * 1024 * 1024;
pub const BLOB_MANIFEST_MAX_CHUNKS: usize = 8192;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Encoding {
Replicated,
ReedSolomon {
k: u8,
m: u8,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ChunkRef {
pub hash: [u8; 32],
pub size: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
struct TreeBody {
body_version: u8,
uri: String,
encoding: Encoding,
root_hash: [u8; 32],
total_size: u64,
depth: u8,
}
#[derive(Serialize)]
struct TreeBodyRef<'a> {
body_version: u8,
uri: &'a str,
encoding: Encoding,
root_hash: [u8; 32],
total_size: u64,
depth: u8,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
struct ManifestBody {
body_version: u8,
uri: String,
encoding: Encoding,
chunks: Vec<ChunkRef>,
total_size: u64,
}
#[derive(Serialize)]
struct ManifestBodyRef<'a> {
body_version: u8,
uri: &'a str,
encoding: Encoding,
chunks: &'a [ChunkRef],
total_size: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum BlobRef {
Small {
version: u8,
uri: String,
hash: [u8; 32],
size: u64,
},
Manifest {
version: u8,
uri: String,
encoding: Encoding,
chunks: Vec<ChunkRef>,
total_size: u64,
},
Tree {
version: u8,
uri: String,
encoding: Encoding,
root_hash: [u8; 32],
total_size: u64,
depth: u8,
},
}
impl BlobRef {
pub fn small(uri: impl Into<String>, hash: [u8; 32], size: u64) -> Self {
Self::Small {
version: BLOB_REF_VERSION_V1,
uri: uri.into(),
hash,
size,
}
}
#[deprecated(
since = "0.18.0",
note = "use `BlobRef::small` for explicit-variant construction"
)]
pub fn new(uri: impl Into<String>, hash: [u8; 32], size: u64) -> Self {
Self::small(uri, hash, size)
}
pub fn manifest(
uri: impl Into<String>,
encoding: Encoding,
chunks: Vec<ChunkRef>,
) -> Result<Self, BlobError> {
if chunks.is_empty() {
return Err(BlobError::Decode(
"manifest must carry at least one chunk".to_owned(),
));
}
if chunks.len() > BLOB_MANIFEST_MAX_CHUNKS {
return Err(BlobError::Decode(format!(
"manifest chunk count {} exceeds cap {}",
chunks.len(),
BLOB_MANIFEST_MAX_CHUNKS
)));
}
validate_chunk_sizes(&chunks)?;
let total_size: u64 = chunks.iter().map(|c| c.size as u64).sum();
if total_size > BLOB_REF_MAX_SIZE {
return Err(BlobError::Decode(format!(
"manifest total_size {} exceeds cap {}",
total_size, BLOB_REF_MAX_SIZE
)));
}
Ok(Self::Manifest {
version: BLOB_REF_VERSION_V2_MANIFEST,
uri: uri.into(),
encoding,
chunks,
total_size,
})
}
pub fn tree(
uri: impl Into<String>,
encoding: Encoding,
root_hash: [u8; 32],
total_size: u64,
depth: u8,
) -> Result<Self, BlobError> {
if total_size == 0 {
return Err(BlobError::Decode(
"tree total_size must be > 0; use BlobRef::Small for empty payloads".to_owned(),
));
}
if total_size > BLOB_TREE_MAX_TOTAL_SIZE {
return Err(BlobError::Decode(format!(
"tree total_size {} exceeds cap {}",
total_size, BLOB_TREE_MAX_TOTAL_SIZE
)));
}
if depth == 0 || depth > super::blob_tree::MAX_TREE_DEPTH {
return Err(BlobError::Decode(format!(
"tree depth {} out of range 1..={}",
depth,
super::blob_tree::MAX_TREE_DEPTH
)));
}
Ok(Self::Tree {
version: BLOB_REF_VERSION_V3_TREE,
uri: uri.into(),
encoding,
root_hash,
total_size,
depth,
})
}
pub fn version(&self) -> u8 {
match self {
Self::Small { version, .. }
| Self::Manifest { version, .. }
| Self::Tree { version, .. } => *version,
}
}
pub fn uri(&self) -> &str {
match self {
Self::Small { uri, .. } | Self::Manifest { uri, .. } | Self::Tree { uri, .. } => {
uri.as_str()
}
}
}
pub fn size(&self) -> u64 {
match self {
Self::Small { size, .. } => *size,
Self::Manifest { total_size, .. } | Self::Tree { total_size, .. } => *total_size,
}
}
pub fn is_chunked(&self) -> bool {
matches!(self, Self::Manifest { .. } | Self::Tree { .. })
}
pub fn is_tree(&self) -> bool {
matches!(self, Self::Tree { .. })
}
pub fn small_hash(&self) -> Option<&[u8; 32]> {
match self {
Self::Small { hash, .. } => Some(hash),
Self::Manifest { .. } | Self::Tree { .. } => None,
}
}
pub fn tree_root_hash(&self) -> Option<&[u8; 32]> {
match self {
Self::Tree { root_hash, .. } => Some(root_hash),
Self::Small { .. } | Self::Manifest { .. } => None,
}
}
pub fn tree_depth(&self) -> Option<u8> {
match self {
Self::Tree { depth, .. } => Some(*depth),
Self::Small { .. } | Self::Manifest { .. } => None,
}
}
pub fn chunks(&self) -> &[ChunkRef] {
match self {
Self::Small { .. } | Self::Tree { .. } => &[],
Self::Manifest { chunks, .. } => chunks,
}
}
pub fn encoding(&self) -> Option<Encoding> {
match self {
Self::Small { .. } => None,
Self::Manifest { encoding, .. } | Self::Tree { encoding, .. } => Some(*encoding),
}
}
#[expect(
clippy::expect_used,
reason = "ManifestBodyRef / TreeBodyRef are composed of sized Serialize types — `postcard::experimental::serialized_size` is infallible against them; mirrors the existing `#[expect]` on `encode()`"
)]
pub fn encoded_len(&self) -> usize {
match self {
Self::Small { uri, .. } => BLOB_REF_SMALL_HEADER_LEN + uri.len(),
Self::Manifest {
uri,
encoding,
chunks,
total_size,
..
} => {
let body = ManifestBodyRef {
body_version: BLOB_MANIFEST_BODY_VERSION,
uri: uri.as_str(),
encoding: *encoding,
chunks: chunks.as_slice(),
total_size: *total_size,
};
let body_len = postcard::experimental::serialized_size(&body)
.expect("manifest body postcard-encodes infallibly");
BLOB_REF_MAGIC.len() + 1 + body_len
}
Self::Tree {
uri,
encoding,
root_hash,
total_size,
depth,
..
} => {
let body = TreeBodyRef {
body_version: BLOB_TREE_BODY_VERSION,
uri: uri.as_str(),
encoding: *encoding,
root_hash: *root_hash,
total_size: *total_size,
depth: *depth,
};
let body_len = postcard::experimental::serialized_size(&body)
.expect("tree body postcard-encodes infallibly");
BLOB_REF_MAGIC.len() + 1 + body_len
}
}
}
#[expect(
clippy::expect_used,
reason = "ManifestBody / TreeBody are composed of sized Serialize types; postcard alloc-encoding is infallible against them"
)]
pub fn encode(&self) -> Vec<u8> {
match self {
Self::Small {
version,
uri,
hash,
size,
} => {
let mut buf = Vec::with_capacity(BLOB_REF_SMALL_HEADER_LEN + uri.len());
buf.extend_from_slice(&BLOB_REF_MAGIC);
buf.push(*version);
buf.extend_from_slice(hash);
buf.extend_from_slice(&size.to_le_bytes());
buf.extend_from_slice(uri.as_bytes());
buf
}
Self::Manifest {
version,
uri,
encoding,
chunks,
total_size,
} => {
let body = ManifestBody {
body_version: BLOB_MANIFEST_BODY_VERSION,
uri: uri.clone(),
encoding: *encoding,
chunks: chunks.clone(),
total_size: *total_size,
};
let body_bytes = postcard::to_allocvec(&body)
.expect("manifest body postcard-encodes infallibly");
let mut buf = Vec::with_capacity(5 + body_bytes.len());
buf.extend_from_slice(&BLOB_REF_MAGIC);
buf.push(*version);
buf.extend_from_slice(&body_bytes);
buf
}
Self::Tree {
version,
uri,
encoding,
root_hash,
total_size,
depth,
} => {
let body = TreeBody {
body_version: BLOB_TREE_BODY_VERSION,
uri: uri.clone(),
encoding: *encoding,
root_hash: *root_hash,
total_size: *total_size,
depth: *depth,
};
let body_bytes =
postcard::to_allocvec(&body).expect("tree body postcard-encodes infallibly");
let mut buf = Vec::with_capacity(5 + body_bytes.len());
buf.extend_from_slice(&BLOB_REF_MAGIC);
buf.push(*version);
buf.extend_from_slice(&body_bytes);
buf
}
}
}
pub fn decode(bytes: &[u8]) -> Result<Option<Self>, BlobError> {
if bytes.len() < BLOB_REF_MAGIC.len() || bytes[..BLOB_REF_MAGIC.len()] != BLOB_REF_MAGIC {
return Ok(None);
}
if bytes.len() < 5 {
return Err(BlobError::Decode(format!(
"frame too short for version byte: {} bytes",
bytes.len()
)));
}
let version = bytes[4];
match version {
BLOB_REF_VERSION_V1 => Self::decode_small(version, &bytes[5..]).map(Some),
BLOB_REF_VERSION_V2_MANIFEST => Self::decode_manifest(version, &bytes[5..]).map(Some),
BLOB_REF_VERSION_V3_TREE => Self::decode_tree(version, &bytes[5..]).map(Some),
other => Err(BlobError::UnsupportedVersion(other)),
}
}
fn decode_small(version: u8, rest: &[u8]) -> Result<Self, BlobError> {
if rest.len() < 40 {
return Err(BlobError::Decode(format!(
"small frame too short: {} bytes after version, need at least 40",
rest.len()
)));
}
let mut hash = [0u8; 32];
hash.copy_from_slice(&rest[0..32]);
let mut size_bytes = [0u8; 8];
size_bytes.copy_from_slice(&rest[32..40]);
let size = u64::from_le_bytes(size_bytes);
if size > BLOB_REF_MAX_SIZE {
return Err(BlobError::Decode(format!(
"blob size {} exceeds cap {}",
size, BLOB_REF_MAX_SIZE
)));
}
let uri = std::str::from_utf8(&rest[40..])
.map_err(|e| BlobError::Decode(format!("URI not UTF-8: {}", e)))?
.to_owned();
Ok(Self::Small {
version,
uri,
hash,
size,
})
}
fn decode_manifest(version: u8, rest: &[u8]) -> Result<Self, BlobError> {
const MAX_MANIFEST_WIRE_BYTES: usize = 8192 + 32 + BLOB_MANIFEST_MAX_CHUNKS * 50;
if rest.len() > MAX_MANIFEST_WIRE_BYTES {
return Err(BlobError::Decode(format!(
"manifest body {} bytes exceeds legitimate upper bound {}",
rest.len(),
MAX_MANIFEST_WIRE_BYTES
)));
}
let body: ManifestBody = postcard::from_bytes(rest)
.map_err(|e| BlobError::Decode(format!("manifest body decode failed: {}", e)))?;
if body.body_version != BLOB_MANIFEST_BODY_VERSION {
return Err(BlobError::UnsupportedVersion(body.body_version));
}
if body.chunks.is_empty() {
return Err(BlobError::Decode(
"manifest must carry at least one chunk".to_owned(),
));
}
if body.chunks.len() > BLOB_MANIFEST_MAX_CHUNKS {
return Err(BlobError::Decode(format!(
"manifest chunk count {} exceeds cap {}",
body.chunks.len(),
BLOB_MANIFEST_MAX_CHUNKS
)));
}
validate_chunk_sizes(&body.chunks)?;
let iterated_sum: u64 = body.chunks.iter().map(|c| c.size as u64).sum();
if iterated_sum != body.total_size {
return Err(BlobError::Decode(format!(
"manifest total_size mismatch: declared {}, iterated {}",
body.total_size, iterated_sum
)));
}
if body.total_size > BLOB_REF_MAX_SIZE {
return Err(BlobError::Decode(format!(
"manifest total_size {} exceeds cap {}",
body.total_size, BLOB_REF_MAX_SIZE
)));
}
Ok(Self::Manifest {
version,
uri: body.uri,
encoding: body.encoding,
chunks: body.chunks,
total_size: body.total_size,
})
}
fn decode_tree(version: u8, rest: &[u8]) -> Result<Self, BlobError> {
if rest.len() > BLOB_REF_TREE_BODY_MAX_BYTES {
return Err(BlobError::Decode(format!(
"tree body {} bytes exceeds cap {}",
rest.len(),
BLOB_REF_TREE_BODY_MAX_BYTES
)));
}
let body: TreeBody = postcard::from_bytes(rest)
.map_err(|e| BlobError::Decode(format!("tree body decode failed: {}", e)))?;
if body.body_version != BLOB_TREE_BODY_VERSION {
return Err(BlobError::UnsupportedVersion(body.body_version));
}
if body.total_size == 0 {
return Err(BlobError::Decode(
"tree total_size must be > 0; empty payloads use BlobRef::Small".to_owned(),
));
}
if body.total_size > BLOB_TREE_MAX_TOTAL_SIZE {
return Err(BlobError::Decode(format!(
"tree total_size {} exceeds cap {}",
body.total_size, BLOB_TREE_MAX_TOTAL_SIZE
)));
}
if body.depth == 0 || body.depth > super::blob_tree::MAX_TREE_DEPTH {
return Err(BlobError::Decode(format!(
"tree depth {} out of range 1..={}",
body.depth,
super::blob_tree::MAX_TREE_DEPTH
)));
}
if body.depth >= 2 {
let exp = body.depth as u32 - 1;
if let Some(min_size) = (super::blob_tree::TREE_FANOUT as u64).checked_pow(exp) {
if body.total_size < min_size {
return Err(BlobError::Decode(format!(
"tree depth {} requires total_size >= {} (TREE_FANOUT^(depth-1)); got {}",
body.depth, min_size, body.total_size
)));
}
}
}
Ok(Self::Tree {
version,
uri: body.uri,
encoding: body.encoding,
root_hash: body.root_hash,
total_size: body.total_size,
depth: body.depth,
})
}
pub fn verify(&self, bytes: &[u8]) -> Result<(), BlobError> {
match self {
Self::Small { hash, .. } => {
let actual: [u8; 32] = blake3::hash(bytes).into();
if actual == *hash {
Ok(())
} else {
Err(BlobError::HashMismatch {
expected: *hash,
actual,
})
}
}
Self::Manifest { .. } => Err(BlobError::Decode(
"verify is undefined on a Manifest variant; verify chunks individually".to_owned(),
)),
Self::Tree { .. } => Err(BlobError::Decode(
"verify is undefined on a Tree variant; verify chunks individually via tree walk"
.to_owned(),
)),
}
}
}
fn validate_chunk_sizes(chunks: &[ChunkRef]) -> Result<(), BlobError> {
let last = chunks.len() - 1;
for (i, chunk) in chunks.iter().enumerate() {
let size = chunk.size as u64;
if i < last {
if size != BLOB_CHUNK_SIZE_BYTES {
return Err(BlobError::Decode(format!(
"manifest non-last chunk {} has size {} (expected {})",
i, size, BLOB_CHUNK_SIZE_BYTES
)));
}
} else {
if size == 0 || size > BLOB_CHUNK_SIZE_BYTES {
return Err(BlobError::Decode(format!(
"manifest last chunk {} has size {} (expected 1..={})",
i, size, BLOB_CHUNK_SIZE_BYTES
)));
}
}
}
Ok(())
}
#[derive(Clone, Debug)]
pub enum ChunkedPayload<'a> {
Inline {
hash: [u8; 32],
payload: &'a [u8],
},
Chunked {
chunks: Vec<(ChunkRef, &'a [u8])>,
total_size: u64,
},
}
impl<'a> ChunkedPayload<'a> {
pub fn size(&self) -> u64 {
match self {
Self::Inline { payload, .. } => payload.len() as u64,
Self::Chunked { total_size, .. } => *total_size,
}
}
pub fn into_blob_ref(
self,
uri: impl Into<String>,
encoding: Encoding,
) -> Result<BlobRef, BlobError> {
match self {
Self::Inline { hash, payload } => Ok(BlobRef::small(uri, hash, payload.len() as u64)),
Self::Chunked { chunks, .. } => {
let chunk_refs: Vec<ChunkRef> = chunks.into_iter().map(|(r, _)| r).collect();
BlobRef::manifest(uri, encoding, chunk_refs)
}
}
}
}
pub fn chunk_payload(bytes: &[u8]) -> Result<ChunkedPayload<'_>, BlobError> {
let len = bytes.len() as u64;
if len > BLOB_REF_MAX_SIZE {
return Err(BlobError::Decode(format!(
"payload size {} exceeds cap {}",
len, BLOB_REF_MAX_SIZE
)));
}
if len <= BLOB_CHUNK_SIZE_BYTES {
let hash: [u8; 32] = blake3::hash(bytes).into();
return Ok(ChunkedPayload::Inline {
hash,
payload: bytes,
});
}
let chunk_size = BLOB_CHUNK_SIZE_BYTES as usize;
let chunk_count = bytes.len().div_ceil(chunk_size);
if chunk_count > BLOB_MANIFEST_MAX_CHUNKS {
return Err(BlobError::Decode(format!(
"payload requires {} chunks, exceeds cap {}",
chunk_count, BLOB_MANIFEST_MAX_CHUNKS
)));
}
let mut chunks = Vec::with_capacity(chunk_count);
for slice in bytes.chunks(chunk_size) {
let hash: [u8; 32] = blake3::hash(slice).into();
chunks.push((
ChunkRef {
hash,
size: slice.len() as u32,
},
slice,
));
}
Ok(ChunkedPayload::Chunked {
chunks,
total_size: len,
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ChunkRangeRequest {
pub chunk_index: usize,
pub start_in_chunk: u32,
pub end_in_chunk: u32,
}
impl ChunkRangeRequest {
pub fn len(&self) -> u32 {
self.end_in_chunk - self.start_in_chunk
}
pub fn is_empty(&self) -> bool {
self.start_in_chunk >= self.end_in_chunk
}
}
pub fn byte_range_to_chunks(
manifest: &BlobRef,
start: u64,
end: u64,
) -> Result<Vec<ChunkRangeRequest>, BlobError> {
let (chunks, total_size) = match manifest {
BlobRef::Manifest {
chunks, total_size, ..
} => (chunks.as_slice(), *total_size),
BlobRef::Small { .. } => {
return Err(BlobError::Decode(
"byte_range_to_chunks called on a Small BlobRef".to_owned(),
));
}
BlobRef::Tree { .. } => {
return Err(BlobError::Decode(
"byte_range_to_chunks called on a Tree BlobRef — \
use the tree-walker path instead"
.to_owned(),
));
}
};
if start > end {
return Err(BlobError::Decode(format!(
"range start {} > end {}",
start, end
)));
}
if end > total_size {
return Err(BlobError::Decode(format!(
"range end {} exceeds total_size {}",
end, total_size
)));
}
if start == end || start >= total_size {
return Ok(Vec::new());
}
let chunk_size = BLOB_CHUNK_SIZE_BYTES;
let first_chunk = (start / chunk_size) as usize;
let last_chunk_inclusive = ((end - 1) / chunk_size) as usize;
let mut out = Vec::with_capacity(last_chunk_inclusive - first_chunk + 1);
for (chunk_index, chunk) in chunks
.iter()
.enumerate()
.skip(first_chunk)
.take(last_chunk_inclusive - first_chunk + 1)
{
let chunk_start_in_blob = chunk_index as u64 * chunk_size;
let local_start = start.saturating_sub(chunk_start_in_blob);
let local_end = (end - chunk_start_in_blob).min(chunk.size as u64);
out.push(ChunkRangeRequest {
chunk_index,
start_in_chunk: local_start as u32,
end_in_chunk: local_end as u32,
});
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn small_fixture() -> BlobRef {
BlobRef::small("s3://bucket/key", [0xAB; 32], 12345)
}
#[test]
fn small_round_trip_encode_decode() {
let original = small_fixture();
let bytes = original.encode();
let decoded = BlobRef::decode(&bytes).unwrap().unwrap();
assert_eq!(decoded, original);
}
#[test]
fn decode_returns_none_when_magic_missing() {
let bytes = vec![0x00, 0x01, 0x02, 0x03, 0x04];
assert!(BlobRef::decode(&bytes).unwrap().is_none());
}
#[test]
fn decode_returns_none_for_payloads_starting_with_old_discriminator_only() {
let bytes = vec![0xB0, 0x00, 0x00, 0x00];
assert!(BlobRef::decode(&bytes).unwrap().is_none());
let bytes = vec![0xB0, 0xB1, 0x00, 0x00];
assert!(BlobRef::decode(&bytes).unwrap().is_none());
let bytes = vec![0xB0, 0xB1, 0xB2, 0x00];
assert!(BlobRef::decode(&bytes).unwrap().is_none());
}
#[test]
fn decode_rejects_short_small_frame() {
let mut bytes = BLOB_REF_MAGIC.to_vec();
bytes.push(BLOB_REF_VERSION_V1);
bytes.push(0x00); let err = BlobRef::decode(&bytes).unwrap_err();
assert!(matches!(err, BlobError::Decode(_)));
}
#[test]
fn decode_rejects_unknown_outer_version() {
let blob = small_fixture();
let mut bytes = blob.encode();
bytes[4] = 0xFE;
let err = BlobRef::decode(&bytes).unwrap_err();
assert!(matches!(err, BlobError::UnsupportedVersion(0xFE)));
}
#[test]
fn encoded_len_matches_real_encoding_small() {
let blob = small_fixture();
assert_eq!(blob.encoded_len(), blob.encode().len());
}
#[test]
fn small_verify_accepts_matching_bytes() {
let payload = b"the lazy dog";
let hash: [u8; 32] = blake3::hash(payload).into();
let blob = BlobRef::small("file:///x", hash, payload.len() as u64);
blob.verify(payload).unwrap();
}
#[test]
fn small_verify_rejects_mismatching_bytes() {
let blob = BlobRef::small("file:///x", [0xCC; 32], 0);
let err = blob.verify(b"different content").unwrap_err();
match err {
BlobError::HashMismatch { expected, actual } => {
assert_eq!(expected, [0xCC; 32]);
assert_ne!(actual, expected);
}
other => panic!("expected HashMismatch, got {:?}", other),
}
}
#[test]
fn small_decode_rejects_oversize_size_field() {
let mut bytes = BLOB_REF_MAGIC.to_vec();
bytes.push(BLOB_REF_VERSION_V1);
bytes.extend_from_slice(&[0u8; 32]);
bytes.extend_from_slice(&u64::MAX.to_le_bytes());
let err = BlobRef::decode(&bytes).unwrap_err();
assert!(matches!(err, BlobError::Decode(_)));
}
#[test]
fn empty_uri_round_trips_small() {
let blob = BlobRef::small("", [0x00; 32], 0);
let bytes = blob.encode();
let decoded = BlobRef::decode(&bytes).unwrap().unwrap();
assert_eq!(decoded.uri(), "");
assert_eq!(decoded.size(), 0);
}
fn manifest_fixture(chunk_count: usize) -> BlobRef {
let chunks: Vec<ChunkRef> = (0..chunk_count)
.map(|i| ChunkRef {
hash: [i as u8; 32],
size: BLOB_CHUNK_SIZE_BYTES as u32,
})
.collect();
BlobRef::manifest("mesh://abc", Encoding::Replicated, chunks).unwrap()
}
#[test]
fn manifest_round_trip_encode_decode() {
let original = manifest_fixture(8);
let bytes = original.encode();
let decoded = BlobRef::decode(&bytes).unwrap().unwrap();
assert_eq!(decoded, original);
}
#[test]
fn encoded_len_matches_encode_len_without_allocating() {
let small = small_fixture();
assert_eq!(small.encoded_len(), small.encode().len(), "Small parity");
for count in [1usize, 8, 128] {
let manifest = manifest_fixture(count);
assert_eq!(
manifest.encoded_len(),
manifest.encode().len(),
"Manifest({count} chunks) parity",
);
}
let tree = BlobRef::tree("mesh://tree", Encoding::Replicated, [0xCD; 32], 1024, 3)
.expect("tree ref");
assert_eq!(tree.encoded_len(), tree.encode().len(), "Tree parity");
let rs_manifest = BlobRef::manifest(
"mesh://rs",
Encoding::ReedSolomon { k: 4, m: 2 },
vec![ChunkRef {
hash: [0xAA; 32],
size: 1024,
}],
)
.unwrap();
assert_eq!(
rs_manifest.encoded_len(),
rs_manifest.encode().len(),
"RS Manifest parity",
);
}
#[test]
fn manifest_body_ref_serializes_identically_to_owned_form() {
let chunks: Vec<ChunkRef> = (0..32)
.map(|i| ChunkRef {
hash: [i as u8; 32],
size: 1024 + i as u32,
})
.collect();
let owned = ManifestBody {
body_version: BLOB_MANIFEST_BODY_VERSION,
uri: "mesh://parity-test".to_string(),
encoding: Encoding::ReedSolomon { k: 4, m: 2 },
chunks: chunks.clone(),
total_size: 99_999,
};
let borrowed = ManifestBodyRef {
body_version: BLOB_MANIFEST_BODY_VERSION,
uri: "mesh://parity-test",
encoding: Encoding::ReedSolomon { k: 4, m: 2 },
chunks: chunks.as_slice(),
total_size: 99_999,
};
let owned_bytes = postcard::to_allocvec(&owned).unwrap();
let borrowed_bytes = postcard::to_allocvec(&borrowed).unwrap();
assert_eq!(
owned_bytes, borrowed_bytes,
"ManifestBodyRef must serialize byte-for-byte identically to ManifestBody",
);
let measured_owned = postcard::experimental::serialized_size(&owned).unwrap();
let measured_borrowed = postcard::experimental::serialized_size(&borrowed).unwrap();
assert_eq!(measured_owned, measured_borrowed);
assert_eq!(measured_owned, owned_bytes.len());
}
#[test]
fn tree_body_ref_serializes_identically_to_owned_form() {
let owned = TreeBody {
body_version: BLOB_TREE_BODY_VERSION,
uri: "mesh://tree-parity".to_string(),
encoding: Encoding::Replicated,
root_hash: [0xCD; 32],
total_size: 1_234_567,
depth: 3,
};
let borrowed = TreeBodyRef {
body_version: BLOB_TREE_BODY_VERSION,
uri: "mesh://tree-parity",
encoding: Encoding::Replicated,
root_hash: [0xCD; 32],
total_size: 1_234_567,
depth: 3,
};
let owned_bytes = postcard::to_allocvec(&owned).unwrap();
let borrowed_bytes = postcard::to_allocvec(&borrowed).unwrap();
assert_eq!(
owned_bytes, borrowed_bytes,
"TreeBodyRef must serialize byte-for-byte identically to TreeBody",
);
}
#[test]
fn manifest_round_trip_with_reed_solomon_reserved() {
let chunks = vec![ChunkRef {
hash: [0xAA; 32],
size: 1024,
}];
let blob =
BlobRef::manifest("mesh://rs", Encoding::ReedSolomon { k: 4, m: 2 }, chunks).unwrap();
let bytes = blob.encode();
let decoded = BlobRef::decode(&bytes).unwrap().unwrap();
assert_eq!(
decoded.encoding(),
Some(Encoding::ReedSolomon { k: 4, m: 2 })
);
}
#[test]
fn manifest_rejects_empty_chunk_list() {
let err = BlobRef::manifest("mesh://", Encoding::Replicated, Vec::new()).unwrap_err();
assert!(matches!(err, BlobError::Decode(_)));
}
#[test]
fn manifest_rejects_too_many_chunks() {
let chunks: Vec<ChunkRef> = (0..BLOB_MANIFEST_MAX_CHUNKS + 1)
.map(|_| ChunkRef {
hash: [0; 32],
size: 1,
})
.collect();
let err = BlobRef::manifest("mesh://", Encoding::Replicated, chunks).unwrap_err();
assert!(matches!(err, BlobError::Decode(_)));
}
#[test]
fn manifest_rejects_total_size_over_cap() {
let chunks = vec![
ChunkRef {
hash: [0; 32],
size: u32::MAX,
};
5
];
let err = BlobRef::manifest("mesh://", Encoding::Replicated, chunks).unwrap_err();
assert!(matches!(err, BlobError::Decode(_)));
}
#[test]
fn manifest_rejects_non_last_chunk_smaller_than_stride() {
let chunks = vec![
ChunkRef {
hash: [1; 32],
size: 1, },
ChunkRef {
hash: [2; 32],
size: BLOB_CHUNK_SIZE_BYTES as u32,
},
];
let err = BlobRef::manifest("mesh://", Encoding::Replicated, chunks).unwrap_err();
assert!(matches!(err, BlobError::Decode(_)));
}
#[test]
fn manifest_rejects_non_last_chunk_larger_than_stride() {
let chunks = vec![
ChunkRef {
hash: [1; 32],
size: (BLOB_CHUNK_SIZE_BYTES as u32) + 1,
},
ChunkRef {
hash: [2; 32],
size: BLOB_CHUNK_SIZE_BYTES as u32,
},
];
let err = BlobRef::manifest("mesh://", Encoding::Replicated, chunks).unwrap_err();
assert!(matches!(err, BlobError::Decode(_)));
}
#[test]
fn manifest_rejects_last_chunk_above_stride() {
let chunks = vec![ChunkRef {
hash: [1; 32],
size: (BLOB_CHUNK_SIZE_BYTES as u32) + 1,
}];
let err = BlobRef::manifest("mesh://", Encoding::Replicated, chunks).unwrap_err();
assert!(matches!(err, BlobError::Decode(_)));
}
#[test]
fn manifest_rejects_zero_size_chunk() {
let chunks = vec![ChunkRef {
hash: [1; 32],
size: 0,
}];
let err = BlobRef::manifest("mesh://", Encoding::Replicated, chunks).unwrap_err();
assert!(matches!(err, BlobError::Decode(_)));
}
#[test]
fn manifest_accepts_single_short_chunk_as_last() {
let chunks = vec![ChunkRef {
hash: [1; 32],
size: 1024,
}];
let blob = BlobRef::manifest("mesh://", Encoding::Replicated, chunks).unwrap();
assert_eq!(blob.size(), 1024);
}
#[test]
fn manifest_accepts_multichunk_with_short_last() {
let chunks = vec![
ChunkRef {
hash: [1; 32],
size: BLOB_CHUNK_SIZE_BYTES as u32,
},
ChunkRef {
hash: [2; 32],
size: 1024,
},
];
let blob = BlobRef::manifest("mesh://", Encoding::Replicated, chunks).unwrap();
assert_eq!(blob.size(), BLOB_CHUNK_SIZE_BYTES + 1024);
}
#[test]
fn manifest_decode_detects_total_size_lie() {
use serde::Serialize;
#[derive(Serialize)]
struct LyingBody {
body_version: u8,
uri: String,
encoding: Encoding,
chunks: Vec<ChunkRef>,
total_size: u64,
}
let lying = LyingBody {
body_version: BLOB_MANIFEST_BODY_VERSION,
uri: "mesh://lie".to_owned(),
encoding: Encoding::Replicated,
chunks: vec![ChunkRef {
hash: [0; 32],
size: 100,
}],
total_size: 200, };
let body = postcard::to_allocvec(&lying).unwrap();
let mut bytes = BLOB_REF_MAGIC.to_vec();
bytes.push(BLOB_REF_VERSION_V2_MANIFEST);
bytes.extend_from_slice(&body);
let err = BlobRef::decode(&bytes).unwrap_err();
assert!(matches!(err, BlobError::Decode(_)));
}
#[test]
fn manifest_decode_rejects_unknown_body_version() {
use serde::Serialize;
#[derive(Serialize)]
struct FutureBody {
body_version: u8,
uri: String,
encoding: Encoding,
chunks: Vec<ChunkRef>,
total_size: u64,
}
let body = FutureBody {
body_version: 0xFE,
uri: "mesh://".to_owned(),
encoding: Encoding::Replicated,
chunks: vec![ChunkRef {
hash: [0; 32],
size: 1,
}],
total_size: 1,
};
let body_bytes = postcard::to_allocvec(&body).unwrap();
let mut bytes = BLOB_REF_MAGIC.to_vec();
bytes.push(BLOB_REF_VERSION_V2_MANIFEST);
bytes.extend_from_slice(&body_bytes);
let err = BlobRef::decode(&bytes).unwrap_err();
assert!(matches!(err, BlobError::UnsupportedVersion(0xFE)));
}
#[test]
fn manifest_size_matches_iterated_chunk_sum() {
let blob = manifest_fixture(10);
let iterated: u64 = blob.chunks().iter().map(|c| c.size as u64).sum();
assert_eq!(blob.size(), iterated);
}
#[test]
fn accessors_uniform_across_variants() {
let small = BlobRef::small("file:///s", [0; 32], 99);
assert_eq!(small.uri(), "file:///s");
assert_eq!(small.size(), 99);
assert!(!small.is_chunked());
assert!(small.small_hash().is_some());
assert!(small.chunks().is_empty());
assert_eq!(small.encoding(), None);
let m = manifest_fixture(3);
assert_eq!(m.uri(), "mesh://abc");
assert!(m.is_chunked());
assert!(m.small_hash().is_none());
assert_eq!(m.chunks().len(), 3);
assert_eq!(m.encoding(), Some(Encoding::Replicated));
}
#[test]
fn chunk_payload_inline_under_threshold() {
let payload = vec![0x42u8; 1024]; match chunk_payload(&payload).unwrap() {
ChunkedPayload::Inline { payload: p, hash } => {
assert_eq!(p.len(), 1024);
let expected_hash: [u8; 32] = blake3::hash(&payload).into();
assert_eq!(hash, expected_hash);
}
ChunkedPayload::Chunked { .. } => panic!("expected Inline for 1 KiB payload"),
}
}
#[test]
fn chunk_payload_inline_at_exact_threshold() {
let payload = vec![0x42u8; BLOB_CHUNK_SIZE_BYTES as usize]; assert!(matches!(
chunk_payload(&payload).unwrap(),
ChunkedPayload::Inline { .. }
));
}
#[test]
fn chunk_payload_chunks_above_threshold() {
let payload = vec![0x42u8; (BLOB_CHUNK_SIZE_BYTES as usize) + 1]; match chunk_payload(&payload).unwrap() {
ChunkedPayload::Chunked { chunks, total_size } => {
assert_eq!(chunks.len(), 2);
assert_eq!(chunks[0].0.size, BLOB_CHUNK_SIZE_BYTES as u32);
assert_eq!(chunks[1].0.size, 1);
assert_eq!(total_size, payload.len() as u64);
}
ChunkedPayload::Inline { .. } => panic!("expected Chunked for 4MiB+1 payload"),
}
}
#[test]
fn chunk_payload_idempotent_same_bytes_same_hashes() {
let payload: Vec<u8> = (0..(8 * 1024 * 1024 + 17))
.map(|i| (i % 251) as u8)
.collect();
let first = match chunk_payload(&payload).unwrap() {
ChunkedPayload::Chunked { chunks, .. } => {
chunks.iter().map(|(c, _)| *c).collect::<Vec<_>>()
}
_ => panic!("expected Chunked"),
};
let second = match chunk_payload(&payload).unwrap() {
ChunkedPayload::Chunked { chunks, .. } => {
chunks.iter().map(|(c, _)| *c).collect::<Vec<_>>()
}
_ => panic!("expected Chunked"),
};
assert_eq!(first, second);
}
#[test]
fn chunk_payload_empty_is_inline() {
let payload: Vec<u8> = Vec::new();
match chunk_payload(&payload).unwrap() {
ChunkedPayload::Inline { payload, hash } => {
assert!(payload.is_empty());
let expected: [u8; 32] = blake3::hash(b"").into();
assert_eq!(hash, expected);
}
_ => panic!("empty payload must be Inline"),
}
}
#[test]
fn chunk_payload_rejects_oversize() {
assert!(BLOB_MANIFEST_MAX_CHUNKS as u64 * BLOB_CHUNK_SIZE_BYTES > BLOB_REF_MAX_SIZE);
}
fn five_chunk_manifest() -> BlobRef {
let chunks: Vec<ChunkRef> = (0..5)
.map(|i| ChunkRef {
hash: [i as u8; 32],
size: BLOB_CHUNK_SIZE_BYTES as u32,
})
.collect();
BlobRef::manifest("mesh://x", Encoding::Replicated, chunks).unwrap()
}
#[test]
fn range_aligned_single_chunk() {
let m = five_chunk_manifest();
let req = byte_range_to_chunks(&m, 0, BLOB_CHUNK_SIZE_BYTES).unwrap();
assert_eq!(req.len(), 1);
assert_eq!(req[0].chunk_index, 0);
assert_eq!(req[0].start_in_chunk, 0);
assert_eq!(req[0].end_in_chunk, BLOB_CHUNK_SIZE_BYTES as u32);
}
#[test]
fn range_unaligned_within_one_chunk() {
let m = five_chunk_manifest();
let req = byte_range_to_chunks(&m, 100, 200).unwrap();
assert_eq!(req.len(), 1);
assert_eq!(req[0].chunk_index, 0);
assert_eq!(req[0].start_in_chunk, 100);
assert_eq!(req[0].end_in_chunk, 200);
assert_eq!(req[0].len(), 100);
}
#[test]
fn range_spans_two_chunks() {
let m = five_chunk_manifest();
let chunk = BLOB_CHUNK_SIZE_BYTES;
let req = byte_range_to_chunks(&m, chunk - 1024, chunk + 1024).unwrap();
assert_eq!(req.len(), 2);
assert_eq!(req[0].chunk_index, 0);
assert_eq!(req[0].start_in_chunk, (chunk - 1024) as u32);
assert_eq!(req[0].end_in_chunk, chunk as u32);
assert_eq!(req[1].chunk_index, 1);
assert_eq!(req[1].start_in_chunk, 0);
assert_eq!(req[1].end_in_chunk, 1024);
}
#[test]
fn range_spans_all_chunks() {
let m = five_chunk_manifest();
let req = byte_range_to_chunks(&m, 0, m.size()).unwrap();
assert_eq!(req.len(), 5);
for (i, r) in req.iter().enumerate() {
assert_eq!(r.chunk_index, i);
assert_eq!(r.start_in_chunk, 0);
assert_eq!(r.end_in_chunk, BLOB_CHUNK_SIZE_BYTES as u32);
}
}
#[test]
fn range_with_partial_last_chunk() {
let chunks = vec![
ChunkRef {
hash: [0; 32],
size: BLOB_CHUNK_SIZE_BYTES as u32,
},
ChunkRef {
hash: [1; 32],
size: 1024, },
];
let m = BlobRef::manifest("mesh://", Encoding::Replicated, chunks).unwrap();
let req = byte_range_to_chunks(&m, 0, BLOB_CHUNK_SIZE_BYTES + 100).unwrap();
assert_eq!(req.len(), 2);
assert_eq!(req[1].chunk_index, 1);
assert_eq!(req[1].start_in_chunk, 0);
assert_eq!(req[1].end_in_chunk, 100);
}
#[test]
fn range_empty_is_empty_request_list() {
let m = five_chunk_manifest();
assert!(byte_range_to_chunks(&m, 100, 100).unwrap().is_empty());
assert!(byte_range_to_chunks(&m, m.size(), m.size())
.unwrap()
.is_empty());
}
#[test]
fn range_rejects_end_past_total_size() {
let m = five_chunk_manifest();
let err = byte_range_to_chunks(&m, 0, m.size() + 1).unwrap_err();
assert!(matches!(err, BlobError::Decode(_)));
}
#[test]
fn range_rejects_start_after_end() {
let m = five_chunk_manifest();
let err = byte_range_to_chunks(&m, 200, 100).unwrap_err();
assert!(matches!(err, BlobError::Decode(_)));
}
#[test]
fn range_rejects_call_against_small() {
let s = BlobRef::small("file:///x", [0; 32], 100);
let err = byte_range_to_chunks(&s, 0, 50).unwrap_err();
assert!(matches!(err, BlobError::Decode(_)));
}
#[test]
fn range_math_reassembles_exact_payload() {
let payload: Vec<u8> = (0..(BLOB_CHUNK_SIZE_BYTES as usize * 3 + 1000))
.map(|i| (i % 251) as u8)
.collect();
let chunked = chunk_payload(&payload).unwrap();
let (chunks_owned, total_size) = match chunked {
ChunkedPayload::Chunked { chunks, total_size } => (chunks, total_size),
_ => panic!("expected Chunked"),
};
let chunk_refs: Vec<ChunkRef> = chunks_owned.iter().map(|(r, _)| *r).collect();
let chunk_bytes: Vec<&[u8]> = chunks_owned.iter().map(|(_, b)| *b).collect();
let m = BlobRef::manifest("mesh://x", Encoding::Replicated, chunk_refs).unwrap();
assert_eq!(m.size(), total_size);
let cases = [
(0u64, total_size),
(10, 5_000_000),
(BLOB_CHUNK_SIZE_BYTES, BLOB_CHUNK_SIZE_BYTES + 1),
(total_size - 100, total_size),
];
for (start, end) in cases {
let requests = byte_range_to_chunks(&m, start, end).unwrap();
let mut assembled = Vec::with_capacity((end - start) as usize);
for r in requests {
let chunk = chunk_bytes[r.chunk_index];
assembled
.extend_from_slice(&chunk[r.start_in_chunk as usize..r.end_in_chunk as usize]);
}
assert_eq!(
assembled,
payload[start as usize..end as usize],
"range [{}, {}) reassembly mismatch",
start,
end
);
}
}
fn tree_root() -> [u8; 32] {
[0xAB; 32]
}
#[test]
fn tree_constructor_sets_version_and_fields() {
let r = BlobRef::tree(
"mesh://ab".to_string(),
Encoding::Replicated,
tree_root(),
1024 * 1024 * 1024 * 64, 2,
)
.unwrap();
assert_eq!(r.version(), BLOB_REF_VERSION_V3_TREE);
assert_eq!(r.uri(), "mesh://ab");
assert_eq!(r.size(), 1024 * 1024 * 1024 * 64);
assert_eq!(r.tree_depth(), Some(2));
assert_eq!(r.tree_root_hash(), Some(&tree_root()));
assert_eq!(r.encoding(), Some(Encoding::Replicated));
assert!(r.is_chunked());
assert!(r.is_tree());
assert!(r.small_hash().is_none());
assert!(r.chunks().is_empty());
}
#[test]
fn tree_constructor_rejects_zero_total_size() {
let err = BlobRef::tree("mesh://aa", Encoding::Replicated, tree_root(), 0, 1).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("must be > 0"), "got: {msg}");
}
#[test]
fn tree_constructor_rejects_total_size_above_cap() {
let err = BlobRef::tree(
"mesh://aa",
Encoding::Replicated,
tree_root(),
BLOB_TREE_MAX_TOTAL_SIZE + 1,
4,
)
.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("exceeds cap"), "got: {msg}");
}
#[test]
fn tree_constructor_rejects_zero_depth() {
let err =
BlobRef::tree("mesh://aa", Encoding::Replicated, tree_root(), 1024, 0).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("depth"), "got: {msg}");
}
#[test]
fn tree_constructor_rejects_depth_above_cap() {
let err = BlobRef::tree(
"mesh://aa",
Encoding::Replicated,
tree_root(),
1024,
super::super::blob_tree::MAX_TREE_DEPTH + 1,
)
.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("depth"), "got: {msg}");
}
#[test]
fn tree_encode_decode_round_trips() {
let original = BlobRef::tree(
"mesh://cafe".to_string(),
Encoding::Replicated,
tree_root(),
1024 * 1024 * 1024, 1,
)
.unwrap();
let bytes = original.encode();
let decoded = BlobRef::decode(&bytes).unwrap().unwrap();
assert_eq!(original, decoded);
match decoded {
BlobRef::Tree {
version,
uri,
encoding,
root_hash,
total_size,
depth,
} => {
assert_eq!(version, BLOB_REF_VERSION_V3_TREE);
assert_eq!(uri, "mesh://cafe");
assert_eq!(encoding, Encoding::Replicated);
assert_eq!(root_hash, tree_root());
assert_eq!(total_size, 1024 * 1024 * 1024);
assert_eq!(depth, 1);
}
other => panic!("expected Tree, got {:?}", other),
}
}
#[test]
fn tree_decode_preserves_reedsolomon_encoding_tag() {
let original = BlobRef::tree(
"mesh://ff",
Encoding::ReedSolomon { k: 10, m: 4 },
tree_root(),
1u64 << 40, 3,
)
.unwrap();
let bytes = original.encode();
let decoded = BlobRef::decode(&bytes).unwrap().unwrap();
assert_eq!(
decoded.encoding(),
Some(Encoding::ReedSolomon { k: 10, m: 4 })
);
}
#[test]
fn tree_decode_rejects_unknown_outer_version() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&BLOB_REF_MAGIC);
bytes.push(0xFE); bytes.extend_from_slice(&[0u8; 64]);
let err = BlobRef::decode(&bytes).unwrap_err();
assert!(
matches!(err, BlobError::UnsupportedVersion(0xFE)),
"expected UnsupportedVersion(0xFE), got {err:?}"
);
}
#[test]
fn tree_decode_rejects_unknown_body_version() {
let original =
BlobRef::tree("mesh://aa", Encoding::Replicated, tree_root(), 1024, 1).unwrap();
let mut bytes = original.encode();
bytes[5] = 0xEF;
let err = BlobRef::decode(&bytes).unwrap_err();
assert!(
matches!(err, BlobError::UnsupportedVersion(0xEF)),
"expected UnsupportedVersion(0xEF), got {err:?}"
);
}
#[test]
fn tree_decode_rejects_oversize_body() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&BLOB_REF_MAGIC);
bytes.push(BLOB_REF_VERSION_V3_TREE);
bytes.extend(std::iter::repeat_n(0u8, BLOB_REF_TREE_BODY_MAX_BYTES + 1));
let err = BlobRef::decode(&bytes).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("exceeds cap"), "got: {msg}");
}
#[test]
fn tree_decode_rejects_total_size_above_cap() {
let body = TreeBody {
body_version: BLOB_TREE_BODY_VERSION,
uri: "mesh://x".to_string(),
encoding: Encoding::Replicated,
root_hash: tree_root(),
total_size: BLOB_TREE_MAX_TOTAL_SIZE + 1,
depth: 4,
};
let body_bytes = postcard::to_allocvec(&body).unwrap();
let mut bytes = Vec::new();
bytes.extend_from_slice(&BLOB_REF_MAGIC);
bytes.push(BLOB_REF_VERSION_V3_TREE);
bytes.extend_from_slice(&body_bytes);
let err = BlobRef::decode(&bytes).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("exceeds cap"), "got: {msg}");
}
#[test]
fn tree_decode_rejects_depth_inconsistent_with_total_size() {
let body = TreeBody {
body_version: BLOB_TREE_BODY_VERSION,
uri: "mesh://x".to_string(),
encoding: Encoding::Replicated,
root_hash: tree_root(),
total_size: 1,
depth: 4,
};
let body_bytes = postcard::to_allocvec(&body).unwrap();
let mut bytes = Vec::new();
bytes.extend_from_slice(&BLOB_REF_MAGIC);
bytes.push(BLOB_REF_VERSION_V3_TREE);
bytes.extend_from_slice(&body_bytes);
let err = BlobRef::decode(&bytes).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("requires total_size >="),
"expected depth-vs-size lower-bound error; got: {msg}",
);
}
#[test]
fn tree_decode_rejects_depth_above_cap() {
let body = TreeBody {
body_version: BLOB_TREE_BODY_VERSION,
uri: "mesh://x".to_string(),
encoding: Encoding::Replicated,
root_hash: tree_root(),
total_size: 1024,
depth: super::super::blob_tree::MAX_TREE_DEPTH + 1,
};
let body_bytes = postcard::to_allocvec(&body).unwrap();
let mut bytes = Vec::new();
bytes.extend_from_slice(&BLOB_REF_MAGIC);
bytes.push(BLOB_REF_VERSION_V3_TREE);
bytes.extend_from_slice(&body_bytes);
let err = BlobRef::decode(&bytes).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("depth"), "got: {msg}");
}
#[test]
fn verify_on_tree_returns_typed_error() {
let r = BlobRef::tree("mesh://aa", Encoding::Replicated, tree_root(), 1024, 1).unwrap();
let err = r.verify(b"any bytes").unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("Tree variant"),
"Tree verify should surface a typed Decode error pointing at tree-walk; got: {msg}",
);
}
#[test]
fn tree_does_not_alias_small_or_manifest_via_decode() {
let small = BlobRef::small("mesh://aa", [0xAA; 32], 100);
let manifest = BlobRef::manifest(
"mesh://bb",
Encoding::Replicated,
vec![ChunkRef {
hash: [0xBB; 32],
size: 1024,
}],
)
.unwrap();
let tree = BlobRef::tree(
"mesh://cc",
Encoding::Replicated,
[0xCC; 32],
1024 * 1024 * 1024,
1,
)
.unwrap();
let s_decoded = BlobRef::decode(&small.encode()).unwrap().unwrap();
let m_decoded = BlobRef::decode(&manifest.encode()).unwrap().unwrap();
let t_decoded = BlobRef::decode(&tree.encode()).unwrap().unwrap();
assert!(matches!(s_decoded, BlobRef::Small { .. }));
assert!(matches!(m_decoded, BlobRef::Manifest { .. }));
assert!(matches!(t_decoded, BlobRef::Tree { .. }));
assert_eq!(s_decoded.version(), BLOB_REF_VERSION_V1);
assert_eq!(m_decoded.version(), BLOB_REF_VERSION_V2_MANIFEST);
assert_eq!(t_decoded.version(), BLOB_REF_VERSION_V3_TREE);
}
}