use std::io::Write;
use std::str;
use crate::error::{Result, SclsError};
use crate::hash::{Digest, HASH_SIZE};
use crate::records::RecordType;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Manifest {
pub slot_no: u64,
pub total_entries: u64,
pub total_chunks: u64,
pub root_hash: Digest,
pub namespace_info: Vec<NamespaceInfo>,
pub prev_manifest: u64,
pub summary: Summary,
}
impl Manifest {
pub fn write(&self, writer: &mut impl Write) -> Result<()> {
let mut buf = Vec::new();
let len = self.offset()?;
buf.write_all(&[RecordType::Manifest.to_byte()])?;
buf.write_all(&self.slot_no.to_be_bytes())?;
buf.write_all(&self.total_entries.to_be_bytes())?;
buf.write_all(&self.total_chunks.to_be_bytes())?;
buf.write_all(&self.summary.to_bytes()?)?;
for ns_info in &self.namespace_info {
buf.write_all(&ns_info.to_bytes()?)?;
}
buf.write_all(&0u32.to_be_bytes())?;
buf.write_all(&self.prev_manifest.to_be_bytes())?;
buf.write_all(self.root_hash.as_bytes())?;
buf.write_all(&len.to_be_bytes())?;
if buf.len() != len as usize {
return Err(SclsError::InconsistentManifestOffset {
expected: buf.len() as u32,
found: len,
});
}
writer.write_all(&len.to_be_bytes())?;
writer.write_all(&buf)?;
Ok(())
}
pub fn offset(&self) -> Result<u32> {
let created_at_len = 4 + self.summary.created_at.len();
let tool_len = 4 + self.summary.tool.len();
let comment_len = 4 + self.summary.comment.as_ref().map_or(0, String::len);
let ns_info_len = self.namespace_info.iter().fold(0usize, |acc, ns_info| {
acc + 4 + 8 + 8 + ns_info.name.len() + HASH_SIZE
});
u32::try_from(
[
1, 8, 8, 8, created_at_len, tool_len, comment_len, ns_info_len, 4, 8, HASH_SIZE, 4, ]
.iter()
.try_fold(0usize, |acc, &x| acc.checked_add(x))
.ok_or(SclsError::WireLengthOverflow("manifest offset".into()))?,
)
.map_err(|_| SclsError::WireLengthOverflow("manifest offset".into()))
}
}
impl TryFrom<&[u8]> for Manifest {
type Error = SclsError;
fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
let mut pos = 0;
if value.len() < 24 {
return Err(SclsError::MalformedRecord(
"manifest too short for header fields".into(),
));
}
let slot_no = u64::from_be_bytes(value[pos..pos + 8].try_into().unwrap());
pos += 8;
let total_entries = u64::from_be_bytes(value[pos..pos + 8].try_into().unwrap());
pos += 8;
let total_chunks = u64::from_be_bytes(value[pos..pos + 8].try_into().unwrap());
pos += 8;
let (summary, bytes_read) = Summary::parse(&value[pos..])?;
pos += bytes_read;
let (namespace_info, bytes_read) = NamespaceInfo::parse_list(&value[pos..])?;
pos += bytes_read;
let needed_len = pos
.checked_add(40)
.ok_or_else(|| SclsError::MalformedRecord("footer length overflow".into()))?;
if value.len() < needed_len {
return Err(SclsError::MalformedRecord(
"manifest too short for footer fields".into(),
));
}
let prev_manifest = u64::from_be_bytes(value[pos..pos + 8].try_into().unwrap());
pos += 8;
let root_hash_bytes: [u8; HASH_SIZE] = value[pos..pos + HASH_SIZE].try_into().unwrap();
let root_hash = root_hash_bytes.into();
pos += HASH_SIZE;
let offset = u32::from_be_bytes(value[pos..pos + 4].try_into().unwrap());
pos += 4;
if pos != value.len() {
return Err(SclsError::MalformedRecord(format!(
"manifest has {} trailing bytes",
value.len() - pos
)));
}
let manifest = Manifest {
slot_no,
total_entries,
total_chunks,
root_hash,
namespace_info,
prev_manifest,
summary,
};
if offset != manifest.offset()? {
return Err(SclsError::MalformedRecord(
"manifest offset does not match record length".into(),
));
}
Ok(manifest)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NamespaceInfo {
pub entries_count: u64,
pub chunks_count: u64,
pub name: String,
pub digest: Digest,
}
impl NamespaceInfo {
fn parse_list(data: &[u8]) -> Result<(Vec<Self>, usize)> {
let mut namespaces = Vec::new();
let mut pos: usize = 0;
loop {
let needed_len = pos.checked_add(4).ok_or_else(|| {
SclsError::MalformedRecord("namespace_info length overflow".into())
})?;
if data.len() < needed_len {
return Err(SclsError::MalformedRecord(
"incomplete namespace_info length".into(),
));
}
let len_ns = u32::from_be_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
pos += 4;
if len_ns == 0 {
break;
}
let required = 8 + 8 + len_ns + HASH_SIZE;
let min_len = pos
.checked_add(required)
.ok_or_else(|| SclsError::MalformedRecord("ns_info length overflow".into()))?;
if data.len() < min_len {
return Err(SclsError::MalformedRecord(
"incomplete namespace_info structure".into(),
));
}
let entries_count = u64::from_be_bytes(data[pos..pos + 8].try_into().unwrap());
pos += 8;
let chunks_count = u64::from_be_bytes(data[pos..pos + 8].try_into().unwrap());
pos += 8;
let name = str::from_utf8(&data[pos..pos + len_ns])
.map_err(|_| SclsError::MalformedRecord("invalid UTF-8 in namespace name".into()))?
.to_string();
pos += len_ns;
let digest_bytes: [u8; HASH_SIZE] = data[pos..pos + HASH_SIZE].try_into().unwrap();
let digest = digest_bytes.into();
pos += HASH_SIZE;
namespaces.push(Self {
entries_count,
chunks_count,
name,
digest,
});
}
Ok((namespaces, pos))
}
fn to_bytes(&self) -> Result<Vec<u8>> {
let mut buf = Vec::new();
let len_ns = u32::try_from(self.name.len())
.map_err(|_| SclsError::WireLengthOverflow("namespace length".into()))?;
buf.extend_from_slice(&len_ns.to_be_bytes());
buf.extend_from_slice(&self.entries_count.to_be_bytes());
buf.extend_from_slice(&self.chunks_count.to_be_bytes());
buf.extend_from_slice(self.name.as_bytes());
buf.extend_from_slice(self.digest.as_bytes());
Ok(buf)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Summary {
pub created_at: String,
pub tool: String,
pub comment: Option<String>,
}
impl Summary {
fn parse(data: &[u8]) -> Result<(Self, usize)> {
let mut pos = 0;
let (created_at, len) = parse_tstr(&data[pos..])?;
pos += len;
let (tool, len) = parse_tstr(&data[pos..])?;
pos += len;
let (comment_str, len) = parse_tstr(&data[pos..])?;
pos += len;
let comment = if comment_str.is_empty() {
None
} else {
Some(comment_str)
};
Ok((
Self {
created_at,
tool,
comment,
},
pos,
))
}
fn to_bytes(&self) -> Result<Vec<u8>> {
let mut buf = to_tstr_bytes(&self.created_at)?;
buf.extend(to_tstr_bytes(&self.tool)?);
buf.extend(to_tstr_bytes(self.comment.as_deref().unwrap_or(""))?);
Ok(buf)
}
}
fn parse_tstr(data: &[u8]) -> Result<(String, usize)> {
if data.len() < 4 {
return Err(SclsError::MalformedRecord(
"tstr too short for length".into(),
));
}
let len = u32::from_be_bytes(data[0..4].try_into().unwrap()) as usize;
let total_len = len
.checked_add(4)
.ok_or_else(|| SclsError::MalformedRecord("tstr length overflow".into()))?;
if data.len() < total_len {
return Err(SclsError::MalformedRecord(format!(
"tstr length {} bytes extends beyond remaining data",
len
)));
}
let s = str::from_utf8(&data[4..total_len])
.map_err(|_| SclsError::MalformedRecord("invalid UTF-8 in tstr".into()))?
.to_string();
Ok((s, total_len))
}
fn to_tstr_bytes<S: AsRef<str>>(input: S) -> Result<Vec<u8>> {
let len = u32::try_from(input.as_ref().len())
.map_err(|_| SclsError::WireLengthOverflow("tstr".into()))?;
let mut buf = len.to_be_bytes().to_vec();
buf.extend_from_slice(input.as_ref().as_bytes());
Ok(buf)
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use proptest::prelude::*;
use super::*;
fn tstr_bytes() -> impl Strategy<Value = Vec<u8>> {
prop::string::string_regex("[a-zA-Z0-9 ]{0,100}")
.unwrap()
.prop_map(|s| {
let len = s.len() as u32;
let mut bytes = len.to_be_bytes().to_vec();
bytes.extend_from_slice(s.as_bytes());
bytes
})
}
fn summary_bytes() -> impl Strategy<Value = Vec<u8>> {
(tstr_bytes(), tstr_bytes(), tstr_bytes()).prop_map(|(created, tool, comment)| {
let mut bytes = Vec::new();
bytes.extend(created);
bytes.extend(tool);
bytes.extend(comment);
bytes
})
}
fn namespace_info_bytes() -> impl Strategy<Value = Vec<u8>> {
(
any::<u64>(), any::<u64>(), prop::string::string_regex("[a-z/_]{1,20}").unwrap(), prop::array::uniform28(any::<u8>()), )
.prop_map(|(entries, chunks, name, digest)| {
let len_ns = name.len() as u32;
let mut bytes = len_ns.to_be_bytes().to_vec();
bytes.extend(entries.to_be_bytes());
bytes.extend(chunks.to_be_bytes());
bytes.extend(name.as_bytes());
bytes.extend(digest);
bytes
})
}
fn namespace_info_list_bytes(num_namespaces: usize) -> impl Strategy<Value = Vec<u8>> {
prop::collection::vec(namespace_info_bytes(), num_namespaces..=num_namespaces).prop_map(
|namespaces| {
let mut bytes = namespaces.concat();
bytes.extend(0u32.to_be_bytes()); bytes
},
)
}
fn manifest_bytes(num_namespaces: usize) -> impl Strategy<Value = Vec<u8>> {
(
any::<u64>(), any::<u64>(), any::<u64>(), summary_bytes(), namespace_info_list_bytes(num_namespaces), any::<u64>(), prop::array::uniform28(any::<u8>()), )
.prop_map(|(slot, entries, chunks, summary, ns_info, prev, root)| {
let mut bytes = Vec::new();
bytes.extend(slot.to_be_bytes());
bytes.extend(entries.to_be_bytes());
bytes.extend(chunks.to_be_bytes());
bytes.extend(summary);
bytes.extend(ns_info);
bytes.extend(prev.to_be_bytes());
bytes.extend(root);
let offset = (bytes.len() + 1 + 4) as u32;
bytes.extend(offset.to_be_bytes());
bytes
})
}
proptest! {
#[test]
fn parse_manifest_consumes_all_bytes(data in (0usize..=5).prop_flat_map(manifest_bytes)) {
let result = Manifest::try_from(data.as_slice());
prop_assert!(result.is_ok());
}
#[test]
fn parse_manifest_namespace_count_matches(
params in (0usize..=5)
.prop_flat_map(|num_ns| {
manifest_bytes(num_ns)
.prop_map(move |data| (num_ns, data))
})
) {
let (num_ns, data) = params;
let manifest = Manifest::try_from(data.as_slice())?;
prop_assert_eq!(manifest.namespace_info.len(), num_ns);
}
#[test]
fn parse_manifest_rejects_trailing_bytes(
data in (0usize..=5).prop_flat_map(manifest_bytes),
extra in prop::collection::vec(any::<u8>(), 1..10)
) {
let mut malformed = data;
malformed.extend(extra);
let result = Manifest::try_from(malformed.as_slice());
prop_assert!(result.is_err());
}
#[test]
fn parse_manifest_rejects_truncated(data in (0usize..=5).prop_flat_map(manifest_bytes)) {
prop_assume!(data.len() > 10);
let truncated = &data[..data.len() - 5];
let result = Manifest::try_from(truncated);
prop_assert!(result.is_err());
}
#[test]
fn ns_info_roundtrip(wire_in in namespace_info_list_bytes(1)) {
let (ns_info_list, _) = NamespaceInfo::parse_list(&wire_in)?;
let ns_info = ns_info_list.first().unwrap();
let mut wire_out = ns_info.to_bytes()?;
wire_out.extend_from_slice(&0u32.to_be_bytes());
prop_assert_eq!(wire_in, wire_out);
}
#[test]
fn summary_roundtrip(wire_in in summary_bytes()) {
let (summary, _) = Summary::parse(&wire_in)?;
let wire_out = summary.to_bytes()?;
prop_assert_eq!(wire_in, wire_out);
}
#[test]
fn manifest_invalid_offset(
mut wire_in in (0usize..5).prop_flat_map(manifest_bytes),
bad_offset in any::<u32>(),
) {
let len = wire_in.len();
prop_assume!((len + 1) as u32 != bad_offset);
let offset = &mut wire_in[len - 4..];
offset.copy_from_slice(&bad_offset.to_be_bytes());
prop_assert!(Manifest::try_from(wire_in.as_slice()).is_err());
}
#[test]
fn manifest_roundtrip(wire_in in (0usize..5).prop_flat_map(manifest_bytes)) {
let manifest = Manifest::try_from(wire_in.as_slice())?;
let mut wire_out: Vec<u8> = Vec::new();
let mut writer = Cursor::new(&mut wire_out);
manifest.write(&mut writer)?;
let len_prefix = {
let bytes: [u8; 4] = wire_out[0..4].try_into().unwrap();
u32::from_be_bytes(bytes)
};
let wire_out = &wire_out[5..];
prop_assert_eq!(len_prefix, manifest.offset()?);
prop_assert_eq!(len_prefix as usize, wire_in.len() + 1); prop_assert_eq!(wire_in, wire_out);
}
}
}