use crate::cursor::ManifestCursor;
use crate::error::CoreError;
use limnifs_format::{MANIFEST_HEADER_LEN, MANIFEST_MAGIC};
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct ManifestHeader {
pub drop_store_version: u16,
pub metadata_version: u16,
pub manifest_version: u16,
}
impl ManifestHeader {
pub const CURRENT_DROP_STORE_VERSION: u16 = 1;
pub const CURRENT_METADATA_VERSION: u16 = 1;
pub const CURRENT_MANIFEST_VERSION: u16 = 1;
#[must_use]
pub const fn new(
drop_store_version: u16,
metadata_version: u16,
manifest_version: u16,
) -> Self {
Self {
drop_store_version,
metadata_version,
manifest_version,
}
}
#[must_use]
pub fn current() -> Self {
Self::new(
Self::CURRENT_DROP_STORE_VERSION,
Self::CURRENT_METADATA_VERSION,
Self::CURRENT_MANIFEST_VERSION,
)
}
#[must_use]
pub fn to_bytes(self) -> [u8; MANIFEST_HEADER_LEN] {
let mut out = [0u8; MANIFEST_HEADER_LEN];
out[..4].copy_from_slice(&MANIFEST_MAGIC);
out[4..6].copy_from_slice(&self.drop_store_version.to_le_bytes());
out[6..8].copy_from_slice(&self.metadata_version.to_le_bytes());
out[8..10].copy_from_slice(&self.manifest_version.to_le_bytes());
out
}
}
pub fn parse_manifest_header(cursor: &mut ManifestCursor<'_>) -> Result<ManifestHeader, CoreError> {
let magic = cursor.read_magic()?;
if magic != MANIFEST_MAGIC {
return Err(CoreError::BadMagic { found: magic });
}
let drop_store_version = cursor.read_u16_le()?;
let metadata_version = cursor.read_u16_le()?;
let manifest_version = cursor.read_u16_le()?;
let reserved = cursor.read_n(6)?;
if reserved.iter().any(|&b| b != 0) {
return Err(CoreError::Corrupt {
reason: format!(
"reserved bytes 10..{MANIFEST_HEADER_LEN} must be zero, found {reserved:?}"
),
});
}
Ok(ManifestHeader {
drop_store_version,
metadata_version,
manifest_version,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn current_header_bytes() -> [u8; MANIFEST_HEADER_LEN] {
let mut bytes = [0u8; MANIFEST_HEADER_LEN];
bytes[..4].copy_from_slice(b"LMFS");
bytes[4..6].copy_from_slice(&1u16.to_le_bytes());
bytes[6..8].copy_from_slice(&1u16.to_le_bytes());
bytes[8..10].copy_from_slice(&1u16.to_le_bytes());
bytes
}
#[test]
fn parses_current_header() {
let bytes = current_header_bytes();
let mut cursor = ManifestCursor::new(&bytes);
let header = parse_manifest_header(&mut cursor).expect("current header parses");
assert_eq!(header, ManifestHeader::current());
assert_eq!(cursor.position(), MANIFEST_HEADER_LEN);
}
#[test]
fn parses_zero_versions() {
let mut bytes = current_header_bytes();
bytes[4..10].fill(0);
let mut cursor = ManifestCursor::new(&bytes);
let header = parse_manifest_header(&mut cursor).expect("zero versions parse");
assert_eq!(header.drop_store_version, 0);
assert_eq!(header.metadata_version, 0);
assert_eq!(header.manifest_version, 0);
}
#[test]
fn rejects_short_buffer() {
let mut short = [0u8; 12];
short[..4].copy_from_slice(b"LMFS");
short[4..6].copy_from_slice(&1u16.to_le_bytes());
short[6..8].copy_from_slice(&1u16.to_le_bytes());
short[8..10].copy_from_slice(&1u16.to_le_bytes());
let mut cursor = ManifestCursor::new(&short);
match parse_manifest_header(&mut cursor) {
Err(CoreError::TooShort { have, need }) => {
assert_eq!(have, 2);
assert_eq!(need, 6);
}
other => panic!("expected TooShort, got {other:?}"),
}
}
#[test]
fn rejects_empty_buffer() {
let empty: [u8; 0] = [];
let mut cursor = ManifestCursor::new(&empty);
assert!(matches!(
parse_manifest_header(&mut cursor),
Err(CoreError::TooShort { .. })
));
}
#[test]
fn rejects_bad_magic() {
let mut bytes = current_header_bytes();
bytes[0] = b'X';
let mut cursor = ManifestCursor::new(&bytes);
match parse_manifest_header(&mut cursor) {
Err(CoreError::BadMagic { found }) => {
assert_eq!(found, *b"XMFS");
}
other => panic!("expected BadMagic, got {other:?}"),
}
}
#[test]
fn rejects_nonzero_reserved() {
let mut bytes = current_header_bytes();
bytes[13] = 0x01;
let mut cursor = ManifestCursor::new(&bytes);
match parse_manifest_header(&mut cursor) {
Err(CoreError::Corrupt { reason }) => {
assert!(reason.contains("reserved"), "reason was: {reason}");
}
other => panic!("expected Corrupt, got {other:?}"),
}
}
#[test]
fn accepts_extra_bytes_after_header() {
let mut bytes = vec![0u8; MANIFEST_HEADER_LEN + 32];
bytes[..MANIFEST_HEADER_LEN].copy_from_slice(¤t_header_bytes());
let mut cursor = ManifestCursor::new(&bytes);
let header = parse_manifest_header(&mut cursor).expect("extra bytes are ignored");
assert_eq!(header, ManifestHeader::current());
assert_eq!(cursor.position(), MANIFEST_HEADER_LEN);
assert_eq!(cursor.remaining_len(), 32);
}
#[test]
fn round_trip_to_bytes() {
let header = ManifestHeader::new(7, 11, 13);
let bytes = header.to_bytes();
assert_eq!(&bytes[..4], b"LMFS");
assert_eq!(u16::from_le_bytes([bytes[4], bytes[5]]), 7);
assert_eq!(u16::from_le_bytes([bytes[6], bytes[7]]), 11);
assert_eq!(u16::from_le_bytes([bytes[8], bytes[9]]), 13);
assert_eq!(&bytes[10..MANIFEST_HEADER_LEN], &[0, 0, 0, 0, 0, 0]);
let mut cursor = ManifestCursor::new(&bytes);
let reparsed = parse_manifest_header(&mut cursor).expect("roundtrip");
assert_eq!(reparsed, header);
}
}