use crate::io::{LittleEndian, ReadBytesExt, WriteBytesExt};
#[cfg(not(feature = "std"))]
use crate::io::{Read, Write};
use crate::manifest_blocks::{MANIFEST_LAYOUT_VERSION_V1, MAX_SECTION_NAME_BYTES};
#[cfg(not(feature = "std"))]
use alloc::{string::String, vec::Vec};
#[cfg(feature = "std")]
use std::io::{Read, Write};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TocEntry {
pub name: String,
pub block_offset: u64,
pub block_size: u32,
pub section_checksum: u128,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FooterPayload {
pub layout_version: u8,
pub flags: u8,
pub sections: Vec<TocEntry>,
}
impl FooterPayload {
#[must_use]
pub fn new(flags: u8, sections: Vec<TocEntry>) -> Self {
Self {
layout_version: MANIFEST_LAYOUT_VERSION_V1,
flags,
sections,
}
}
#[must_use]
pub fn section(&self, name: &str) -> Option<&TocEntry> {
self.sections.iter().find(|e| e.name == name)
}
pub fn encode<W: Write>(&self, mut writer: W) -> crate::Result<()> {
if self.sections.len() > usize::from(u16::MAX) {
return Err(crate::Error::ManifestFooterInvalid(
"section count exceeds u16::MAX",
));
}
let mut seen: crate::HashSet<&str> = crate::HashSet::default();
for entry in &self.sections {
if entry.name.is_empty() {
return Err(crate::Error::ManifestFooterInvalid(
"section name must be non-empty",
));
}
if entry.name.len() > MAX_SECTION_NAME_BYTES {
return Err(crate::Error::ManifestFooterInvalid(
"section name exceeds MAX_SECTION_NAME_BYTES",
));
}
if !seen.insert(entry.name.as_str()) {
return Err(crate::Error::ManifestFooterInvalid(
"duplicate section name",
));
}
}
writer.write_u8(self.layout_version)?;
writer.write_u8(self.flags)?;
#[expect(
clippy::cast_possible_truncation,
reason = "section count bounded by u16::MAX check above"
)]
writer.write_u16::<LittleEndian>(self.sections.len() as u16)?;
for entry in &self.sections {
#[expect(
clippy::cast_possible_truncation,
reason = "name length bounded by MAX_SECTION_NAME_BYTES check above"
)]
writer.write_u16::<LittleEndian>(entry.name.len() as u16)?;
writer.write_all(entry.name.as_bytes())?;
writer.write_u64::<LittleEndian>(entry.block_offset)?;
writer.write_u32::<LittleEndian>(entry.block_size)?;
writer.write_u128::<LittleEndian>(entry.section_checksum)?;
}
Ok(())
}
pub fn decode<R: Read>(mut reader: R) -> crate::Result<Self> {
let layout_version = reader.read_u8()?;
if layout_version != MANIFEST_LAYOUT_VERSION_V1 {
return Err(crate::Error::ManifestFooterInvalid(
"unknown manifest_layout_version",
));
}
let flags = reader.read_u8()?;
let section_count = usize::from(reader.read_u16::<LittleEndian>()?);
let mut sections: Vec<TocEntry> = Vec::new();
for _ in 0..section_count {
let name_len = usize::from(reader.read_u16::<LittleEndian>()?);
if name_len == 0 {
return Err(crate::Error::ManifestFooterInvalid(
"section name length must be non-zero",
));
}
if name_len > MAX_SECTION_NAME_BYTES {
return Err(crate::Error::ManifestFooterInvalid(
"section name length exceeds MAX_SECTION_NAME_BYTES",
));
}
let mut name_bytes = vec![0u8; name_len];
reader.read_exact(&mut name_bytes)?;
let name = String::from_utf8(name_bytes)
.map_err(|_| crate::Error::ManifestFooterInvalid("section name not UTF-8"))?;
let block_offset = reader.read_u64::<LittleEndian>()?;
let block_size = reader.read_u32::<LittleEndian>()?;
let section_checksum = reader.read_u128::<LittleEndian>()?;
if sections.iter().any(|e: &TocEntry| e.name == name) {
return Err(crate::Error::ManifestFooterInvalid(
"duplicate section name",
));
}
sections.push(TocEntry {
name,
block_offset,
block_size,
section_checksum,
});
}
Ok(Self {
layout_version,
flags,
sections,
})
}
}
#[cfg(test)]
#[expect(
clippy::unwrap_used,
clippy::expect_used,
clippy::cast_possible_truncation,
reason = "tests panic on the unhappy paths to surface failures loudly; \
the hand-rolled bad-byte fixtures need direct write_* calls \
that can't propagate via `?` cleanly; bounded test inputs \
never approach u16 truncation"
)]
mod tests;