use anyhow::{Result, bail};
use super::{FRAME_TYPE_FOOTER, encode_skippable_frame, identity::IDENTITY_MAGIC};
pub const FOOTER_VERSION_V1: u8 = 1;
pub const ARCHIVE_HASH_SEED: u64 = u64::from_le_bytes(*b"TRZN2HSH");
pub const FOOTER_FRAME_SIZE: u64 = 38;
const FOOTER_PAYLOAD_SIZE: usize = 30;
#[derive(Debug, Clone, Copy)]
pub struct Footer {
pub toc_offset: u64,
pub toc_frame_size: u64,
pub archive_xxhash64: u64,
}
pub fn encode_footer_frame(footer: &Footer) -> Vec<u8> {
let mut payload = Vec::with_capacity(FOOTER_PAYLOAD_SIZE);
payload.extend_from_slice(IDENTITY_MAGIC.as_slice());
payload.push(FRAME_TYPE_FOOTER);
payload.push(FOOTER_VERSION_V1);
payload.extend_from_slice(&footer.toc_offset.to_le_bytes());
payload.extend_from_slice(&footer.toc_frame_size.to_le_bytes());
payload.extend_from_slice(&footer.archive_xxhash64.to_le_bytes());
debug_assert_eq!(payload.len(), FOOTER_PAYLOAD_SIZE);
encode_skippable_frame(&payload)
}
pub fn decode_footer_payload(payload: &[u8]) -> Result<Footer> {
if payload.len() != FOOTER_PAYLOAD_SIZE {
bail!(
"footer payload has wrong size: {} bytes (expected {FOOTER_PAYLOAD_SIZE})",
payload.len()
);
}
if payload[0..4] != IDENTITY_MAGIC {
bail!("footer payload does not begin with TRZN");
}
if payload[4] != FRAME_TYPE_FOOTER {
bail!(
"unexpected frame type in footer payload: {:#04x}",
payload[4]
);
}
let version = payload[5];
if version != FOOTER_VERSION_V1 {
bail!("unsupported footer version: {version}");
}
let toc_offset = u64::from_le_bytes(payload[6..14].try_into().unwrap());
let toc_frame_size = u64::from_le_bytes(payload[14..22].try_into().unwrap());
let archive_xxhash64 = u64::from_le_bytes(payload[22..30].try_into().unwrap());
Ok(Footer {
toc_offset,
toc_frame_size,
archive_xxhash64,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::format::SKIPPABLE_FRAME_MAGIC;
#[test]
fn footer_frame_size_matches_layout() {
let f = Footer {
toc_offset: 0,
toc_frame_size: 0,
archive_xxhash64: 0,
};
let bytes = encode_footer_frame(&f);
assert_eq!(bytes.len() as u64, FOOTER_FRAME_SIZE);
assert_eq!(&bytes[0..4], SKIPPABLE_FRAME_MAGIC.to_le_bytes().as_slice());
assert_eq!(
&bytes[4..8],
(FOOTER_PAYLOAD_SIZE as u32).to_le_bytes().as_slice()
);
}
#[test]
fn encode_then_decode_roundtrips() {
let f = Footer {
toc_offset: 0x1234_5678_9abc_def0,
toc_frame_size: 0x0fed_cba9_8765_4321,
archive_xxhash64: 0xdead_beef_cafe_f00d,
};
let bytes = encode_footer_frame(&f);
let decoded = decode_footer_payload(&bytes[8..]).expect("decode");
assert_eq!(decoded.toc_offset, f.toc_offset);
assert_eq!(decoded.toc_frame_size, f.toc_frame_size);
assert_eq!(decoded.archive_xxhash64, f.archive_xxhash64);
}
#[test]
fn decode_rejects_wrong_frame_type() {
let mut payload = vec![0u8; FOOTER_PAYLOAD_SIZE];
payload[0..4].copy_from_slice(IDENTITY_MAGIC.as_slice());
payload[4] = 0x99; payload[5] = FOOTER_VERSION_V1;
let err = decode_footer_payload(&payload).expect_err("should reject");
assert!(format!("{err:#}").contains("unexpected frame type"));
}
#[test]
fn decode_rejects_wrong_size() {
let payload = vec![0u8; FOOTER_PAYLOAD_SIZE - 1];
let err = decode_footer_payload(&payload).expect_err("should reject");
assert!(format!("{err:#}").contains("wrong size"));
}
}