use serde::{Deserialize, Serialize};
use crate::PackHash;
pub const DEFAULT_CHUNK_SIZE: u32 = 1024 * 1024;
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ManifestError {
#[error("manifest decode: {0}")]
Decode(String),
#[error("manifest version {0} is not supported")]
UnsupportedVersion(u8),
#[error("manifest internal inconsistency: {0}")]
Inconsistent(&'static str),
#[error("manifest total_size {total} cannot fit {count} chunks of size {chunk}")]
SizeOutOfRange {
total: u64,
count: u32,
chunk: u32,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ChunkedPackManifestV1 {
pub version: u8,
pub chunk_size: u32,
pub total_size: u64,
pub chunk_count: u32,
#[serde(with = "serde_bytes_array_vec")]
pub chunk_hashes: Vec<PackHash>,
}
impl ChunkedPackManifestV1 {
pub fn from_chunks(chunk_size: u32, chunks: &[Vec<u8>]) -> Self {
let chunk_count = u32::try_from(chunks.len())
.expect("freenet-git ChunkedPack with >4G chunks is not supported");
let total_size: u64 = chunks.iter().map(|c| c.len() as u64).sum();
let chunk_hashes: Vec<PackHash> =
chunks.iter().map(|c| *blake3::hash(c).as_bytes()).collect();
Self {
version: 1,
chunk_size,
total_size,
chunk_count,
chunk_hashes,
}
}
pub fn to_bytes(&self) -> Vec<u8> {
bincode::serialize(self).expect("ChunkedPackManifestV1 serialization is infallible")
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ManifestError> {
let manifest: Self =
bincode::deserialize(bytes).map_err(|e| ManifestError::Decode(e.to_string()))?;
manifest.validate()?;
Ok(manifest)
}
pub fn validate(&self) -> Result<(), ManifestError> {
if self.version != 1 {
return Err(ManifestError::UnsupportedVersion(self.version));
}
if self.chunk_count == 0 {
return Err(ManifestError::Inconsistent("chunk_count must be > 0"));
}
if self.chunk_size == 0 {
return Err(ManifestError::Inconsistent("chunk_size must be > 0"));
}
if self.total_size == 0 {
return Err(ManifestError::Inconsistent("total_size must be > 0"));
}
if self.chunk_count as usize != self.chunk_hashes.len() {
return Err(ManifestError::Inconsistent(
"chunk_count does not match chunk_hashes length",
));
}
let chunk_size_u64 = self.chunk_size as u64;
let count_u64 = self.chunk_count as u64;
let upper = chunk_size_u64
.checked_mul(count_u64)
.ok_or(ManifestError::SizeOutOfRange {
total: self.total_size,
count: self.chunk_count,
chunk: self.chunk_size,
})?;
let lower =
chunk_size_u64
.checked_mul(count_u64 - 1)
.ok_or(ManifestError::SizeOutOfRange {
total: self.total_size,
count: self.chunk_count,
chunk: self.chunk_size,
})?;
if self.total_size > upper || self.total_size <= lower {
return Err(ManifestError::SizeOutOfRange {
total: self.total_size,
count: self.chunk_count,
chunk: self.chunk_size,
});
}
Ok(())
}
pub fn chunk_len(&self, i: u32) -> u64 {
debug_assert!(i < self.chunk_count, "chunk index out of range");
if i + 1 < self.chunk_count {
self.chunk_size as u64
} else {
self.total_size - (self.chunk_size as u64) * ((self.chunk_count - 1) as u64)
}
}
}
pub fn split_pack(pack: &[u8], chunk_size: u32) -> Vec<Vec<u8>> {
assert!(!pack.is_empty(), "split_pack: empty pack");
assert!(chunk_size > 0, "split_pack: zero chunk_size");
pack.chunks(chunk_size as usize)
.map(|c| c.to_vec())
.collect()
}
mod serde_bytes_array_vec {
use serde::de::{SeqAccess, Visitor};
use serde::ser::SerializeSeq;
use serde::{Deserializer, Serializer};
pub fn serialize<S: Serializer>(value: &[[u8; 32]], ser: S) -> Result<S::Ok, S::Error> {
let mut seq = ser.serialize_seq(Some(value.len()))?;
for item in value {
seq.serialize_element(serde_bytes::Bytes::new(item))?;
}
seq.end()
}
pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<Vec<[u8; 32]>, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = Vec<[u8; 32]>;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a sequence of 32-byte arrays")
}
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut out = Vec::with_capacity(seq.size_hint().unwrap_or(0));
while let Some(b) = seq.next_element::<serde_bytes::ByteBuf>()? {
let arr: [u8; 32] = b
.as_ref()
.try_into()
.map_err(|_| serde::de::Error::custom("expected 32-byte chunk hash"))?;
out.push(arr);
}
Ok(out)
}
}
de.deserialize_seq(V)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_small_manifest() {
let chunks: Vec<Vec<u8>> = vec![vec![0xAA; 100], vec![0xBB; 50]];
let m = ChunkedPackManifestV1::from_chunks(100, &chunks);
let bytes = m.to_bytes();
let decoded = ChunkedPackManifestV1::from_bytes(&bytes).expect("valid");
assert_eq!(decoded, m);
assert_eq!(decoded.total_size, 150);
assert_eq!(decoded.chunk_count, 2);
assert_eq!(decoded.chunk_len(0), 100);
assert_eq!(decoded.chunk_len(1), 50);
}
#[test]
fn rejects_zero_chunk_count() {
let m = ChunkedPackManifestV1 {
version: 1,
chunk_size: 1024,
total_size: 1024,
chunk_count: 0,
chunk_hashes: vec![],
};
let bytes = m.to_bytes();
let err = ChunkedPackManifestV1::from_bytes(&bytes).unwrap_err();
assert!(matches!(err, ManifestError::Inconsistent(_)));
}
#[test]
fn rejects_count_hashes_mismatch() {
let m = ChunkedPackManifestV1 {
version: 1,
chunk_size: 100,
total_size: 200,
chunk_count: 2,
chunk_hashes: vec![[0; 32]],
};
let bytes = m.to_bytes();
let err = ChunkedPackManifestV1::from_bytes(&bytes).unwrap_err();
assert!(matches!(err, ManifestError::Inconsistent(_)));
}
#[test]
fn rejects_total_too_large() {
let m = ChunkedPackManifestV1 {
version: 1,
chunk_size: 100,
total_size: 250, chunk_count: 2,
chunk_hashes: vec![[0; 32]; 2],
};
let bytes = m.to_bytes();
let err = ChunkedPackManifestV1::from_bytes(&bytes).unwrap_err();
assert!(matches!(err, ManifestError::SizeOutOfRange { .. }));
}
#[test]
fn rejects_total_too_small_for_count() {
let m = ChunkedPackManifestV1 {
version: 1,
chunk_size: 100,
total_size: 100,
chunk_count: 2,
chunk_hashes: vec![[0; 32]; 2],
};
let bytes = m.to_bytes();
let err = ChunkedPackManifestV1::from_bytes(&bytes).unwrap_err();
assert!(matches!(err, ManifestError::SizeOutOfRange { .. }));
}
#[test]
fn split_then_manifest_then_validate() {
let pack: Vec<u8> = (0..2500u32).map(|i| (i & 0xFF) as u8).collect();
let chunks = split_pack(&pack, 1000);
assert_eq!(chunks.len(), 3);
assert_eq!(chunks[0].len(), 1000);
assert_eq!(chunks[1].len(), 1000);
assert_eq!(chunks[2].len(), 500);
let m = ChunkedPackManifestV1::from_chunks(1000, &chunks);
assert!(m.validate().is_ok());
assert_eq!(m.total_size, 2500);
assert_eq!(m.chunk_len(0), 1000);
assert_eq!(m.chunk_len(1), 1000);
assert_eq!(m.chunk_len(2), 500);
let reassembled: Vec<u8> = chunks.into_iter().flatten().collect();
assert_eq!(reassembled, pack);
}
#[test]
fn manifest_wire_format_fixture() {
let m = ChunkedPackManifestV1 {
version: 1,
chunk_size: 4,
total_size: 7,
chunk_count: 2,
chunk_hashes: vec![[0xAA; 32], [0xBB; 32]],
};
let bytes = m.to_bytes();
let expected_hex = "010400000007000000000000000200000002000000000000002000000000000000\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
2000000000000000\
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
let mut actual_hex = String::with_capacity(bytes.len() * 2);
for b in &bytes {
use std::fmt::Write as _;
write!(actual_hex, "{b:02x}").unwrap();
}
let expected_clean: String = expected_hex
.chars()
.filter(|c| !c.is_whitespace())
.collect();
assert_eq!(
actual_hex, expected_clean,
"ChunkedPackManifestV1 wire format drift — bump version and update consumers"
);
assert_eq!(
blake3::hash(&bytes).to_hex().as_str(),
"7b792da2fc4b787ff10abdbc480596c118e88ad1209da7d0c4d10d0bc060264e",
"ChunkedPackManifestV1 BLAKE3 drift",
);
let decoded = ChunkedPackManifestV1::from_bytes(&bytes).unwrap();
assert_eq!(decoded, m);
}
}