use std::collections::BTreeMap;
use rkyv::rancor::Error as RkyvError;
use rkyv::{Archive, Deserialize, Serialize};
use crate::constants::{FEC_K, FEC_M, Format};
use crate::directory::format_policy::validate_segment_format_for_catalog;
use crate::error::CarbonadoError;
use crate::filepack::{self, FilepackCborEntry, Packed};
use crate::utils::calc_padding_len;
pub const FILEPACK_MANIFEST_VERSION: u32 = 3;
pub const FILEPACK_MANIFEST_FORMAT_LEVEL_PUBLIC: u8 = 0x0E;
pub const FILEPACK_MANIFEST_FORMAT_LEVEL_ENCRYPTED: u8 = 0x0F;
pub const FILEPACK_MANIFEST_FORMAT_LEVEL: u8 = FILEPACK_MANIFEST_FORMAT_LEVEL_PUBLIC;
pub const MAX_FILEPACK_MANIFEST_ENTRIES: usize = 100_000;
pub const MAX_REL_PATH_LEN: usize = 4096;
pub const MAX_OTS_PROOF_LEN: usize = 65_536;
pub const MAX_RKYV_PAYLOAD_LEN: usize = 16 * 1024 * 1024;
pub const MAX_SEGMENTS_PER_ENTRY: usize = 10_000;
pub const MAX_TOTAL_SEGMENT_REFS: usize = 1_000_000;
pub const MAX_SEGMENT_MAIN_LEN: u64 = 256 * 1024 * 1024;
#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[rkyv(derive(Debug, PartialEq, Eq))]
pub struct SegmentRef {
pub segment_bao_root: [u8; 32],
pub chunk_index: u32,
pub main_len: u64,
pub verification_outboard_offset: u32,
pub verification_outboard_len: u32,
pub fec_parity_offset: u32,
pub fec_parity_len: u32,
pub dict_offset: u32,
pub dict_len: u32,
}
#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[rkyv(derive(Debug, PartialEq, Eq))]
pub struct FilepackEntry {
pub rel_path: String,
pub content_blake3: [u8; 32],
pub segment_format: u8,
pub segments: Vec<SegmentRef>,
pub ots_proof: Option<Vec<u8>>,
}
#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[rkyv(derive(Debug, PartialEq, Eq))]
struct FilepackManifestWire {
pub version: u32,
pub format_level: u8,
pub entries: Vec<FilepackEntry>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FilepackSegmentMap {
by_path: BTreeMap<String, Vec<SegmentRef>>,
}
impl FilepackSegmentMap {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, rel_path: impl Into<String>, segments: Vec<SegmentRef>) {
self.by_path.insert(rel_path.into(), segments);
}
pub fn segments_for(&self, rel_path: &str) -> Option<&[SegmentRef]> {
self.by_path.get(rel_path).map(Vec::as_slice)
}
pub fn from_manifest_entries(entries: &[FilepackEntry]) -> Self {
let mut map = Self::new();
for entry in entries {
map.insert(entry.rel_path.clone(), entry.segments.clone());
}
map
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FilepackManifest {
pub version: u32,
pub format_level: u8,
pub catalog_bao_root: [u8; 32],
pub catalog_ots_proof: Option<Vec<u8>>,
pub entries: Vec<FilepackEntry>,
}
impl FilepackManifest {
fn into_wire(self) -> FilepackManifestWire {
FilepackManifestWire {
version: self.version,
format_level: self.format_level,
entries: self.entries,
}
}
fn from_wire(catalog_bao_root: [u8; 32], wire: FilepackManifestWire) -> Self {
Self {
version: wire.version,
format_level: wire.format_level,
catalog_bao_root,
catalog_ots_proof: None,
entries: wire.entries,
}
}
}
impl FilepackManifest {
pub fn to_bytes(&self) -> Result<Vec<u8>, CarbonadoError> {
let wire = FilepackManifestWire {
version: self.version,
format_level: self.format_level,
entries: self.entries.clone(),
};
Self::wire_to_bytes(&wire)
}
pub fn into_bytes(self) -> Result<Vec<u8>, CarbonadoError> {
Self::wire_to_bytes(&self.into_wire())
}
fn wire_to_bytes(wire: &FilepackManifestWire) -> Result<Vec<u8>, CarbonadoError> {
let bytes = rkyv::to_bytes::<RkyvError>(wire)
.map_err(|e| CarbonadoError::InvalidFilepackManifest(e.to_string()))?;
Ok(bytes.into_vec())
}
pub fn from_bytes_with_root(
bytes: &[u8],
catalog_bao_root: [u8; 32],
) -> Result<Self, CarbonadoError> {
if bytes.len() > MAX_RKYV_PAYLOAD_LEN {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"rkyv payload exceeds {MAX_RKYV_PAYLOAD_LEN} bytes"
)));
}
Self::check_archived_wire_limits(bytes)?;
let wire: FilepackManifestWire = rkyv::from_bytes::<FilepackManifestWire, RkyvError>(bytes)
.map_err(|e| CarbonadoError::InvalidFilepackManifest(e.to_string()))?;
let index = Self::from_wire(catalog_bao_root, wire);
index.validate()?;
Ok(index)
}
pub(crate) fn from_bytes_unvalidated(
bytes: &[u8],
catalog_bao_root: [u8; 32],
) -> Result<Self, CarbonadoError> {
if bytes.len() > MAX_RKYV_PAYLOAD_LEN {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"rkyv payload exceeds {MAX_RKYV_PAYLOAD_LEN} bytes"
)));
}
Self::check_archived_wire_limits(bytes)?;
let wire: FilepackManifestWire = rkyv::from_bytes::<FilepackManifestWire, RkyvError>(bytes)
.map_err(|e| CarbonadoError::InvalidFilepackManifest(e.to_string()))?;
Ok(Self::from_wire(catalog_bao_root, wire))
}
fn check_archived_wire_limits(bytes: &[u8]) -> Result<(), CarbonadoError> {
let archived = rkyv::access::<ArchivedFilepackManifestWire, RkyvError>(bytes)
.map_err(|e| CarbonadoError::InvalidFilepackManifest(e.to_string()))?;
if archived.entries.len() > MAX_FILEPACK_MANIFEST_ENTRIES {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"entry count exceeds maximum {MAX_FILEPACK_MANIFEST_ENTRIES}"
)));
}
let catalog_encrypted = archived.format_level & 1 != 0;
let directory_catalog = archived.format_level == FILEPACK_MANIFEST_FORMAT_LEVEL_PUBLIC
|| archived.format_level == FILEPACK_MANIFEST_FORMAT_LEVEL_ENCRYPTED;
for entry in archived.entries.iter() {
if entry.rel_path.len() > MAX_REL_PATH_LEN {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"rel_path exceeds {MAX_REL_PATH_LEN} bytes"
)));
}
if entry.segments.len() > MAX_SEGMENTS_PER_ENTRY {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"segment count exceeds maximum {MAX_SEGMENTS_PER_ENTRY}"
)));
}
if let Some(proof) = entry.ots_proof.as_ref()
&& proof.len() > MAX_OTS_PROOF_LEN
{
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"ots_proof exceeds {MAX_OTS_PROOF_LEN} bytes"
)));
}
if directory_catalog {
validate_segment_format_for_catalog(entry.segment_format, catalog_encrypted)
.map_err(|e| match e {
CarbonadoError::SegmentFormatMismatch(msg) => {
CarbonadoError::InvalidFilepackManifest(msg)
}
other => other,
})?;
}
}
Ok(())
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, CarbonadoError> {
Self::from_bytes_with_root(bytes, [0u8; 32])
}
pub fn validate_rel_path(rel: &str) -> Result<(), CarbonadoError> {
if rel.is_empty() {
return Err(CarbonadoError::InvalidFilepackManifest(
"empty rel_path".into(),
));
}
if rel.len() > MAX_REL_PATH_LEN {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"rel_path exceeds {MAX_REL_PATH_LEN} bytes"
)));
}
if rel.contains('\\') {
return Err(CarbonadoError::InvalidFilepackManifest(
"rel_path must use forward slashes".into(),
));
}
if rel.starts_with('/') || rel.starts_with('\\') {
return Err(CarbonadoError::InvalidFilepackManifest(
"rel_path must be relative".into(),
));
}
if Path::new(rel).is_absolute() {
return Err(CarbonadoError::InvalidFilepackManifest(
"rel_path must be relative".into(),
));
}
for component in rel.split('/') {
if component == ".." {
return Err(CarbonadoError::InvalidFilepackManifest(
"rel_path must not contain '..' components".into(),
));
}
}
Ok(())
}
pub fn validate_segments(segments: &[SegmentRef]) -> Result<(), CarbonadoError> {
if segments.is_empty() {
return Err(CarbonadoError::InvalidFilepackManifest(
"entry must have at least one segment".into(),
));
}
if segments.len() > MAX_SEGMENTS_PER_ENTRY {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"segment count exceeds maximum {MAX_SEGMENTS_PER_ENTRY}"
)));
}
for window in segments.windows(2) {
if window[0].chunk_index >= window[1].chunk_index {
return Err(CarbonadoError::InvalidFilepackManifest(
"segments must be strictly sorted by chunk_index".into(),
));
}
}
for (expected, seg) in segments.iter().enumerate() {
if seg.chunk_index != expected as u32 {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"segments must be contiguous from 0; expected chunk_index {expected}, got {}",
seg.chunk_index
)));
}
}
Ok(())
}
pub fn validate(&self) -> Result<(), CarbonadoError> {
if self.version != FILEPACK_MANIFEST_VERSION {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"unsupported version {}",
self.version
)));
}
if self.format_level > 15 {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"format_level must be 0–15, got 0x{:02x}",
self.format_level
)));
}
let catalog_encrypted = self.format_level & 1 != 0;
let directory_catalog = self.format_level == FILEPACK_MANIFEST_FORMAT_LEVEL_PUBLIC
|| self.format_level == FILEPACK_MANIFEST_FORMAT_LEVEL_ENCRYPTED;
if let Some(proof) = &self.catalog_ots_proof
&& proof.len() > MAX_OTS_PROOF_LEN
{
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"catalog_ots_proof exceeds {MAX_OTS_PROOF_LEN} bytes"
)));
}
if self.entries.len() > MAX_FILEPACK_MANIFEST_ENTRIES {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"entry count exceeds maximum {MAX_FILEPACK_MANIFEST_ENTRIES}"
)));
}
let mut prev: Option<&str> = None;
for entry in &self.entries {
Self::validate_rel_path(&entry.rel_path)?;
if directory_catalog {
validate_segment_format_for_catalog(entry.segment_format, catalog_encrypted)
.map_err(|e| match e {
CarbonadoError::SegmentFormatMismatch(msg) => {
CarbonadoError::InvalidFilepackManifest(msg)
}
other => other,
})?;
} else if entry.segment_format != self.format_level {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"single-file segment_format 0x{:02x} must match format_level 0x{:02x}",
entry.segment_format, self.format_level
)));
}
Self::validate_segments(&entry.segments)?;
let seg_fmt = Format::from(entry.segment_format);
for seg in &entry.segments {
validate_segment_bundle_semantics(seg_fmt, seg, &entry.rel_path)?;
}
if let Some(proof) = &entry.ots_proof
&& proof.len() > MAX_OTS_PROOF_LEN
{
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"ots_proof exceeds {MAX_OTS_PROOF_LEN} bytes"
)));
}
if let Some(p) = prev
&& entry.rel_path.as_str() <= p
{
return Err(CarbonadoError::InvalidFilepackManifest(
"entries must be strictly sorted by rel_path".into(),
));
}
prev = Some(entry.rel_path.as_str());
}
Ok(())
}
pub fn validate_bao_bundle_refs(&self, bundle_len: usize) -> Result<(), CarbonadoError> {
let mut ranges: Vec<(u32, u32, String)> = Vec::new();
let mut segment_ref_count = 0usize;
for entry in &self.entries {
let seg_fmt = Format::from(entry.segment_format);
for seg in &entry.segments {
segment_ref_count = segment_ref_count.saturating_add(1);
if segment_ref_count > MAX_TOTAL_SEGMENT_REFS {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"segment ref count exceeds maximum {MAX_TOTAL_SEGMENT_REFS}"
)));
}
validate_segment_bundle_semantics(seg_fmt, seg, &entry.rel_path)?;
let ver_end = seg
.verification_outboard_offset
.checked_add(seg.verification_outboard_len)
.ok_or_else(|| {
CarbonadoError::InvalidFilepackManifest(
"verification_outboard offset overflow".into(),
)
})?;
if ver_end as usize > bundle_len {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"verification_outboard range for {} chunk {} exceeds bundle length {bundle_len}",
entry.rel_path, seg.chunk_index
)));
}
let fec_end = seg
.fec_parity_offset
.checked_add(seg.fec_parity_len)
.ok_or_else(|| {
CarbonadoError::InvalidFilepackManifest("fec_parity offset overflow".into())
})?;
if fec_end as usize > bundle_len {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"fec_parity range for {} chunk {} exceeds bundle length {bundle_len}",
entry.rel_path, seg.chunk_index
)));
}
if seg.verification_outboard_len > 0 {
ranges.push((
seg.verification_outboard_offset,
ver_end,
format!(
"{} chunk {} verification_outboard",
entry.rel_path, seg.chunk_index
),
));
}
if seg.fec_parity_len > 0 {
ranges.push((
seg.fec_parity_offset,
fec_end,
format!("{} chunk {} fec_parity", entry.rel_path, seg.chunk_index),
));
}
let dict_end = seg.dict_offset.checked_add(seg.dict_len).ok_or_else(|| {
CarbonadoError::InvalidFilepackManifest("dict offset overflow".into())
})?;
if dict_end as usize > bundle_len {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"dict range for {} chunk {} exceeds bundle length {bundle_len}",
entry.rel_path, seg.chunk_index
)));
}
if seg.dict_len > 0 {
ranges.push((
seg.dict_offset,
dict_end,
format!("{} chunk {} dict", entry.rel_path, seg.chunk_index),
));
}
}
}
ranges.sort_by_key(|(off, _, _)| *off);
let mut high_water: u32 = 0;
let mut prev_label: Option<&str> = None;
for (off, end, label) in &ranges {
if *off < high_water {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"bundle ranges overlap: {} vs {}",
prev_label.unwrap_or("(prior range)"),
label
)));
}
high_water = *end;
prev_label = Some(label.as_str());
}
Ok(())
}
pub fn from_filepack_cbor(
manifest: &[u8],
segment_map: &FilepackSegmentMap,
format_level: u8,
catalog_bao_root: [u8; 32],
) -> Result<Self, CarbonadoError> {
let flat = filepack::parse_filepack_cbor(manifest)?;
Self::from_filepack_cbor_entries(&flat, segment_map, format_level, catalog_bao_root)
}
pub fn from_packed(
packed: &Packed,
segment_map: &FilepackSegmentMap,
format_level: u8,
catalog_bao_root: [u8; 32],
) -> Result<Self, CarbonadoError> {
Self::from_filepack_cbor(
&packed.manifest,
segment_map,
format_level,
catalog_bao_root,
)
}
fn from_filepack_cbor_entries(
flat: &[FilepackCborEntry],
segment_map: &FilepackSegmentMap,
format_level: u8,
catalog_bao_root: [u8; 32],
) -> Result<Self, CarbonadoError> {
if flat.len() > MAX_FILEPACK_MANIFEST_ENTRIES {
return Err(CarbonadoError::InvalidFilepackCbor(format!(
"entry count exceeds maximum {MAX_FILEPACK_MANIFEST_ENTRIES}"
)));
}
let mut entries = Vec::with_capacity(flat.len());
for row in flat {
let segments = segment_map.segments_for(&row.rel_path).ok_or_else(|| {
CarbonadoError::InvalidFilepackCbor(format!(
"missing segment refs for rel_path {}",
row.rel_path
))
})?;
let segment_format = segments
.first()
.map(|_| {
if format_level & 1 != 0 {
crate::directory::format_policy::SEGMENT_FORMAT_ENCRYPTED_COMPRESSED
} else {
crate::directory::format_policy::SEGMENT_FORMAT_PUBLIC_COMPRESSED
}
})
.unwrap_or_else(|| {
if format_level & 1 != 0 {
crate::directory::format_policy::SEGMENT_FORMAT_ENCRYPTED_COMPRESSED
} else {
crate::directory::format_policy::SEGMENT_FORMAT_PUBLIC_COMPRESSED
}
});
entries.push(FilepackEntry {
rel_path: row.rel_path.clone(),
content_blake3: row.content_blake3,
segment_format,
segments: segments.to_vec(),
ots_proof: None,
});
}
let manifest = Self {
version: FILEPACK_MANIFEST_VERSION,
format_level,
catalog_bao_root,
catalog_ots_proof: None,
entries,
};
manifest.validate()?;
Ok(manifest)
}
pub fn to_filepack_cbor(&self) -> Result<Vec<u8>, CarbonadoError> {
self.validate()?;
let flat: Vec<(String, [u8; 32], u64)> = self
.entries
.iter()
.map(|e| (e.rel_path.clone(), e.content_blake3, 0))
.collect();
let package = filepack::entries_to_package_tree(&flat)?;
filepack::build_filepack_cbor_manifest(&package)
}
}
use std::path::Path;
pub fn expected_fec_parity_len(main_len: u64) -> u32 {
if main_len == 0 {
return 0;
}
let (_, chunk_len) = calc_padding_len(main_len as usize);
(FEC_M - FEC_K) as u32 * chunk_len
}
fn validate_segment_bundle_semantics(
seg_fmt: Format,
seg: &SegmentRef,
rel_path: &str,
) -> Result<(), CarbonadoError> {
if seg.main_len > MAX_SEGMENT_MAIN_LEN {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"main_len for {rel_path} chunk {} exceeds maximum {MAX_SEGMENT_MAIN_LEN}",
seg.chunk_index
)));
}
if seg_fmt.contains(Format::Fec) {
if seg.main_len > 0 && seg.fec_parity_len == 0 {
return Err(CarbonadoError::MissingFecParity);
}
if seg.main_len > 0 {
let expected = expected_fec_parity_len(seg.main_len);
if seg.fec_parity_len != expected {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"fec_parity_len for {rel_path} chunk {} must be {expected} (encode geometry), got {}",
seg.chunk_index, seg.fec_parity_len
)));
}
} else if seg.fec_parity_len != 0 {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"fec_parity present for {rel_path} chunk {} with zero main_len",
seg.chunk_index
)));
}
} else if seg.fec_parity_len != 0 {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"fec_parity present for {rel_path} chunk {} but segment format lacks FEC",
seg.chunk_index
)));
}
if seg.fec_parity_len > 0 {
let expected_fec_off = seg
.verification_outboard_offset
.checked_add(seg.verification_outboard_len)
.ok_or_else(|| {
CarbonadoError::InvalidFilepackManifest(
"verification_outboard offset overflow".into(),
)
})?;
if seg.fec_parity_offset != expected_fec_off {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"fec_parity_offset for {rel_path} chunk {} must follow verification_outboard contiguously",
seg.chunk_index
)));
}
}
if seg.dict_len > 0 {
let expected_dict_off = if seg.fec_parity_len > 0 {
seg.fec_parity_offset
.checked_add(seg.fec_parity_len)
.ok_or_else(|| {
CarbonadoError::InvalidFilepackManifest("fec_parity offset overflow".into())
})?
} else {
seg.verification_outboard_offset
.checked_add(seg.verification_outboard_len)
.ok_or_else(|| {
CarbonadoError::InvalidFilepackManifest(
"verification_outboard offset overflow".into(),
)
})?
};
if seg.dict_offset != expected_dict_off {
return Err(CarbonadoError::InvalidFilepackManifest(format!(
"dict_offset for {rel_path} chunk {} must follow bao/parity contiguously",
seg.chunk_index
)));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_segment_ref(main_len: u64, chunk_index: u32, root_byte: u8) -> SegmentRef {
let ver_len = if main_len > 0 { 64 } else { 0 };
let fec_len = expected_fec_parity_len(main_len);
SegmentRef {
segment_bao_root: [root_byte; 32],
chunk_index,
main_len,
verification_outboard_offset: 0,
verification_outboard_len: ver_len,
fec_parity_offset: ver_len,
fec_parity_len: fec_len,
dict_offset: 0,
dict_len: 0,
}
}
fn sample_manifest() -> FilepackManifest {
FilepackManifest {
version: FILEPACK_MANIFEST_VERSION,
format_level: FILEPACK_MANIFEST_FORMAT_LEVEL_PUBLIC,
catalog_bao_root: [1u8; 32],
catalog_ots_proof: None,
entries: vec![FilepackEntry {
rel_path: "a.txt".into(),
content_blake3: [2u8; 32],
segment_format: crate::directory::format_policy::SEGMENT_FORMAT_PUBLIC_COMPRESSED,
segments: vec![sample_segment_ref(42, 0, 3)],
ots_proof: None,
}],
}
}
#[test]
fn filepack_manifest_roundtrip() {
let manifest = sample_manifest();
let bytes = manifest.to_bytes().expect("to_bytes");
let decoded = FilepackManifest::from_bytes_with_root(&bytes, manifest.catalog_bao_root)
.expect("from_bytes");
assert_eq!(decoded, manifest);
}
#[test]
fn filepack_manifest_multi_segment_roundtrip() {
let mut manifest = sample_manifest();
manifest.entries[0]
.segments
.push(sample_segment_ref(99, 1, 4));
let bytes = manifest.to_bytes().expect("to_bytes");
let decoded = FilepackManifest::from_bytes_with_root(&bytes, manifest.catalog_bao_root)
.expect("from_bytes");
assert_eq!(decoded, manifest);
}
#[test]
fn malformed_rkyv_errors_no_panic() {
let garbage = b"not-valid-rkyv-bytes";
let err = FilepackManifest::from_bytes(garbage).unwrap_err();
assert!(matches!(err, CarbonadoError::InvalidFilepackManifest(_)));
}
#[test]
fn rejects_unsorted_entries() {
let mut manifest = sample_manifest();
manifest.entries.push(FilepackEntry {
rel_path: "0.txt".into(),
content_blake3: [0u8; 32],
segment_format: crate::directory::format_policy::SEGMENT_FORMAT_PUBLIC_COMPRESSED,
segments: vec![sample_segment_ref(1, 0, 0)],
ots_proof: None,
});
let err = manifest.validate().unwrap_err();
assert!(
matches!(err, CarbonadoError::InvalidFilepackManifest(ref msg) if msg.contains("rel_path")),
"got {err:?}"
);
}
#[test]
fn rejects_non_contiguous_segments() {
let mut manifest = sample_manifest();
manifest.entries[0].segments.push(SegmentRef {
segment_bao_root: [5u8; 32],
chunk_index: 2,
main_len: 10,
verification_outboard_offset: 0,
verification_outboard_len: 0,
fec_parity_offset: 0,
fec_parity_len: 0,
dict_offset: 0,
dict_len: 0,
});
let err = manifest.validate().unwrap_err();
assert!(
matches!(err, CarbonadoError::InvalidFilepackManifest(ref msg) if msg.contains("contiguous")),
"got {err:?}"
);
}
#[test]
fn rejects_path_traversal_rel_path() {
let err = FilepackManifest::validate_rel_path("../pwned").unwrap_err();
assert!(
matches!(err, CarbonadoError::InvalidFilepackManifest(ref msg) if msg.contains("..")),
"got {err:?}"
);
let err = FilepackManifest::validate_rel_path("foo/../../etc/passwd").unwrap_err();
assert!(
matches!(err, CarbonadoError::InvalidFilepackManifest(ref msg) if msg.contains("..")),
"got {err:?}"
);
let err = FilepackManifest::validate_rel_path("/etc/passwd").unwrap_err();
assert!(
matches!(err, CarbonadoError::InvalidFilepackManifest(ref msg) if msg.contains("relative")),
"got {err:?}"
);
}
#[test]
fn rejects_oversized_ots_proof() {
let mut manifest = sample_manifest();
manifest.entries[0].ots_proof = Some(vec![0u8; MAX_OTS_PROOF_LEN + 1]);
let err = manifest.validate().unwrap_err();
assert!(
matches!(err, CarbonadoError::InvalidFilepackManifest(ref msg) if msg.contains("ots_proof")),
"got {err:?}"
);
}
#[test]
fn rejects_unsupported_format_level() {
let mut manifest = sample_manifest();
manifest.format_level = 16;
let err = manifest.validate().unwrap_err();
assert!(
matches!(err, CarbonadoError::InvalidFilepackManifest(ref msg) if msg.contains("0–15")),
"got {err:?}"
);
}
#[test]
fn single_file_sidecar_allows_non_catalog_format_level() {
let mut manifest = sample_manifest();
manifest.format_level = 6;
manifest.entries[0].segment_format = 6;
for seg in &mut manifest.entries[0].segments {
seg.fec_parity_offset = 0;
seg.fec_parity_len = 0;
}
manifest
.validate()
.expect("one-entry sidecar may use any c0–c15 format_level");
}
#[test]
fn validate_bao_bundle_refs_missing_fec_parity() {
let manifest = FilepackManifest {
version: FILEPACK_MANIFEST_VERSION,
format_level: FILEPACK_MANIFEST_FORMAT_LEVEL_PUBLIC,
catalog_bao_root: [1u8; 32],
catalog_ots_proof: None,
entries: vec![FilepackEntry {
rel_path: "a.txt".into(),
content_blake3: [2u8; 32],
segment_format: crate::directory::format_policy::SEGMENT_FORMAT_PUBLIC_COMPRESSED,
segments: vec![SegmentRef {
segment_bao_root: [3u8; 32],
chunk_index: 0,
main_len: 42,
verification_outboard_offset: 0,
verification_outboard_len: 8,
fec_parity_offset: 0,
fec_parity_len: 0,
dict_offset: 0,
dict_len: 0,
}],
ots_proof: None,
}],
};
let err = manifest.validate_bao_bundle_refs(64).unwrap_err();
assert!(
matches!(err, CarbonadoError::MissingFecParity),
"got {err:?}"
);
}
#[test]
fn validate_bao_bundle_refs_rejects_cross_segment_overlap() {
let fec_len = expected_fec_parity_len(100);
let manifest = FilepackManifest {
version: FILEPACK_MANIFEST_VERSION,
format_level: FILEPACK_MANIFEST_FORMAT_LEVEL_PUBLIC,
catalog_bao_root: [1u8; 32],
catalog_ots_proof: None,
entries: vec![FilepackEntry {
rel_path: "a.txt".into(),
content_blake3: [2u8; 32],
segment_format: crate::directory::format_policy::SEGMENT_FORMAT_PUBLIC_COMPRESSED,
segments: vec![
SegmentRef {
segment_bao_root: [3u8; 32],
chunk_index: 0,
main_len: 100,
verification_outboard_offset: 0,
verification_outboard_len: 64,
fec_parity_offset: 64,
fec_parity_len: fec_len,
dict_offset: 0,
dict_len: 0,
},
SegmentRef {
segment_bao_root: [4u8; 32],
chunk_index: 1,
main_len: 100,
verification_outboard_offset: 32,
verification_outboard_len: 64,
fec_parity_offset: 96,
fec_parity_len: fec_len,
dict_offset: 0,
dict_len: 0,
},
],
ots_proof: None,
}],
};
let err = manifest.validate_bao_bundle_refs(1_000_000).unwrap_err();
assert!(
matches!(err, CarbonadoError::InvalidFilepackManifest(ref m) if m.contains("bundle ranges overlap")),
"got {err:?}"
);
}
#[test]
fn validate_bao_bundle_refs_scales_with_many_ranges() {
let fec_len = expected_fec_parity_len(64);
let ver_len = 32u32;
let range_stride = ver_len + fec_len;
let segment_count = 2000usize;
let mut segments = Vec::with_capacity(segment_count);
let mut bundle_cursor = 0u32;
for i in 0..segment_count {
let ver_off = bundle_cursor;
bundle_cursor = bundle_cursor.saturating_add(ver_len);
let fec_off = bundle_cursor;
bundle_cursor = bundle_cursor.saturating_add(fec_len);
segments.push(SegmentRef {
segment_bao_root: [i as u8; 32],
chunk_index: i as u32,
main_len: 64,
verification_outboard_offset: ver_off,
verification_outboard_len: ver_len,
fec_parity_offset: fec_off,
fec_parity_len: fec_len,
dict_offset: 0,
dict_len: 0,
});
}
let manifest = FilepackManifest {
version: FILEPACK_MANIFEST_VERSION,
format_level: FILEPACK_MANIFEST_FORMAT_LEVEL_PUBLIC,
catalog_bao_root: [1u8; 32],
catalog_ots_proof: None,
entries: vec![FilepackEntry {
rel_path: "many.txt".into(),
content_blake3: [2u8; 32],
segment_format: crate::directory::format_policy::SEGMENT_FORMAT_PUBLIC_COMPRESSED,
segments,
ots_proof: None,
}],
};
let bundle_len = (segment_count as u32 * range_stride) as usize;
manifest
.validate_bao_bundle_refs(bundle_len)
.unwrap_or_else(|e| panic!("many contiguous ranges must validate: {e:?}"));
}
#[test]
fn validate_bao_bundle_refs_rejects_fec_geometry_mismatch() {
let mut manifest = sample_manifest();
manifest.entries[0].segments[0].fec_parity_len = 1;
let err = manifest.validate_bao_bundle_refs(1_000_000).unwrap_err();
assert!(
matches!(err, CarbonadoError::InvalidFilepackManifest(ref m) if m.contains("fec_parity_len")),
"got {err:?}"
);
}
#[test]
fn validate_bao_bundle_refs_rejects_fec_on_non_fec_format() {
let manifest = FilepackManifest {
version: FILEPACK_MANIFEST_VERSION,
format_level: FILEPACK_MANIFEST_FORMAT_LEVEL_PUBLIC,
catalog_bao_root: [1u8; 32],
catalog_ots_proof: None,
entries: vec![FilepackEntry {
rel_path: "a.txt".into(),
content_blake3: [2u8; 32],
segment_format: 0x04,
segments: vec![SegmentRef {
segment_bao_root: [3u8; 32],
chunk_index: 0,
main_len: 10,
verification_outboard_offset: 0,
verification_outboard_len: 8,
fec_parity_offset: 8,
fec_parity_len: 16,
dict_offset: 0,
dict_len: 0,
}],
ots_proof: None,
}],
};
let err = manifest.validate().unwrap_err();
assert!(
matches!(err, CarbonadoError::InvalidFilepackManifest(ref m) if m.contains("c4–c7")),
"got {err:?}"
);
}
#[test]
fn validate_accepts_zero_length_verification_outboard_for_nonempty_main() {
let mut manifest = sample_manifest();
manifest.entries[0].segments[0].verification_outboard_len = 0;
manifest.entries[0].segments[0].fec_parity_offset = 0;
manifest
.validate()
.expect("zero-length outboard is valid for small Bao trees");
}
#[test]
fn decode_outboard_rejects_missing_verification_outboard() {
use crate::decoding::decode_outboard;
use crate::directory::format_policy::SEGMENT_FORMAT_PUBLIC_COMPRESSED;
use crate::encoding::encode_outboard_with_zstd;
let payload = b"payload";
let oenc = encode_outboard_with_zstd(
&[0u8; 32],
payload,
SEGMENT_FORMAT_PUBLIC_COMPRESSED,
None,
&crate::stream::ZstdEncode::level(20),
)
.expect("encode_outboard");
let err = decode_outboard(
&[0u8; 32],
oenc.hash.as_bytes(),
&oenc.main,
None,
oenc.fec_parity.as_deref(),
oenc.info.padding_len,
SEGMENT_FORMAT_PUBLIC_COMPRESSED,
)
.unwrap_err();
assert!(
matches!(err, CarbonadoError::MissingVerificationOutboard),
"got {err:?}"
);
}
#[test]
fn encode_outboard_empty_verification_outboard_still_roundtrips() {
use crate::decoding::decode_outboard;
use crate::directory::format_policy::SEGMENT_FORMAT_PUBLIC_COMPRESSED;
use crate::encoding::encode_outboard_with_zstd;
let payload = b"payload";
let oenc = encode_outboard_with_zstd(
&[0u8; 32],
payload,
SEGMENT_FORMAT_PUBLIC_COMPRESSED,
None,
&crate::stream::ZstdEncode::level(20),
)
.expect("encode_outboard");
let ver = oenc.verification_outboard.as_deref().expect("Some");
assert_eq!(ver.len(), 0, "small payloads may have zero-length outboard");
let decoded = decode_outboard(
&[0u8; 32],
oenc.hash.as_bytes(),
&oenc.main,
Some(ver),
oenc.fec_parity.as_deref(),
oenc.info.padding_len,
SEGMENT_FORMAT_PUBLIC_COMPRESSED,
)
.expect("roundtrip");
assert_eq!(decoded, payload);
}
#[test]
fn validate_accepts_zero_byte_segment_refs() {
let manifest = FilepackManifest {
version: FILEPACK_MANIFEST_VERSION,
format_level: FILEPACK_MANIFEST_FORMAT_LEVEL_PUBLIC,
catalog_bao_root: [1u8; 32],
catalog_ots_proof: None,
entries: vec![FilepackEntry {
rel_path: "empty.txt".into(),
content_blake3: *blake3::hash(b"").as_bytes(),
segment_format: crate::directory::format_policy::SEGMENT_FORMAT_PUBLIC_COMPRESSED,
segments: vec![sample_segment_ref(0, 0, 5)],
ots_proof: None,
}],
};
manifest.validate().expect("zero-byte segment semantics");
}
#[test]
fn rejects_oversized_rel_path() {
let long = "a".repeat(MAX_REL_PATH_LEN + 1);
let err = FilepackManifest::validate_rel_path(&long).unwrap_err();
assert!(matches!(err, CarbonadoError::InvalidFilepackManifest(_)));
}
}