use crate::manifest_blocks::footer::{FooterPayload, TocEntry};
use byteorder::{LittleEndian, WriteBytesExt};
use std::io::Write;
pub fn compute(version_id: u64, footer: &FooterPayload) -> crate::Result<u128> {
let mut canonical: Vec<u8> = Vec::with_capacity(64 + footer.sections.len() * 64);
canonical.write_u64::<LittleEndian>(version_id)?;
canonical.write_u8(footer.layout_version)?;
canonical.write_u8(footer.flags)?;
let section_count_u32 = u32::try_from(footer.sections.len()).map_err(|_| {
crate::Error::ManifestFooterInvalid(
"section count exceeds u32 — manifest layout invariant violated",
)
})?;
canonical.write_u32::<LittleEndian>(section_count_u32)?;
let mut sorted: Vec<&TocEntry> = footer.sections.iter().collect();
sorted.sort_by(|a, b| a.name.cmp(&b.name));
for entry in sorted {
let name_len_u16 = u16::try_from(entry.name.len()).map_err(|_| {
crate::Error::ManifestFooterInvalid(
"section name length exceeds u16 — manifest layout invariant violated",
)
})?;
canonical.write_u16::<LittleEndian>(name_len_u16)?;
canonical.write_all(entry.name.as_bytes())?;
canonical.write_u64::<LittleEndian>(entry.block_offset)?;
canonical.write_u32::<LittleEndian>(entry.block_size)?;
canonical.write_u128::<LittleEndian>(entry.section_checksum)?;
}
Ok(xxhash_rust::xxh3::xxh3_128(&canonical))
}
#[cfg(test)]
#[expect(
clippy::unwrap_used,
reason = "tests panic on the unhappy paths to surface failures loudly"
)]
mod tests {
use super::*;
use crate::manifest_blocks::FLAG_FOOTER_MIRROR_ENABLED;
fn entry(name: &str, offset: u64, size: u32, checksum: u128) -> TocEntry {
TocEntry {
name: name.to_string(),
block_offset: offset,
block_size: size,
section_checksum: checksum,
}
}
#[test]
fn digest_is_deterministic() {
let payload = FooterPayload::new(
FLAG_FOOTER_MIRROR_ENABLED,
vec![entry("tables", 4096, 128, 0xAA)],
);
let h1 = compute(7, &payload).unwrap();
let h2 = compute(7, &payload).unwrap();
assert_eq!(h1, h2);
}
#[test]
fn digest_differs_for_different_version_id() {
let payload = FooterPayload::new(0, vec![entry("a", 4096, 64, 0xAA)]);
assert_ne!(compute(0, &payload).unwrap(), compute(1, &payload).unwrap());
}
#[test]
fn digest_differs_when_section_checksum_changes() {
let p1 = FooterPayload::new(0, vec![entry("tables", 4096, 64, 0xAA)]);
let p2 = FooterPayload::new(0, vec![entry("tables", 4096, 64, 0xBB)]);
assert_ne!(compute(0, &p1).unwrap(), compute(0, &p2).unwrap());
}
#[test]
fn digest_is_order_independent() {
let common = vec![entry("a", 4096, 64, 0xAA), entry("b", 4160, 64, 0xBB)];
let reordered = vec![entry("b", 4160, 64, 0xBB), entry("a", 4096, 64, 0xAA)];
let p1 = FooterPayload::new(0, common);
let p2 = FooterPayload::new(0, reordered);
assert_eq!(compute(0, &p1).unwrap(), compute(0, &p2).unwrap());
}
#[test]
fn digest_differs_when_section_offset_changes() {
let p1 = FooterPayload::new(0, vec![entry("tables", 4096, 64, 0xAA)]);
let p2 = FooterPayload::new(0, vec![entry("tables", 8192, 64, 0xAA)]);
assert_ne!(compute(0, &p1).unwrap(), compute(0, &p2).unwrap());
}
#[test]
fn digest_differs_when_layout_version_changes() {
let mut p1 = FooterPayload::new(0, vec![entry("a", 4096, 64, 0xAA)]);
let mut p2 = p1.clone();
p1.layout_version = 2;
p2.layout_version = 3;
assert_ne!(compute(0, &p1).unwrap(), compute(0, &p2).unwrap());
}
}