use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use std::io::{Cursor, Read, Write};
use super::error::{FafbError, FafbResult};
use super::flags::{Flags, FLAG_STRING_TABLE};
pub const MAGIC: [u8; 4] = *b"FAFB";
pub const MAGIC_U32: u32 = 0x4246_4146;
pub const VERSION_MAJOR: u8 = 1;
pub const VERSION_MINOR: u8 = 0;
pub const HEADER_SIZE: usize = 32;
pub const MAX_SECTIONS: u16 = 256;
pub const MAX_FILE_SIZE: u32 = 10 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FafbHeader {
pub version_major: u8,
pub version_minor: u8,
pub flags: Flags,
pub source_checksum: u32,
pub created_timestamp: u64,
pub section_count: u16,
pub section_table_offset: u32,
pub string_table_index: u16,
pub total_size: u32,
}
impl FafbHeader {
pub fn new() -> Self {
Self {
version_major: VERSION_MAJOR,
version_minor: VERSION_MINOR,
flags: Flags::from_raw(FLAG_STRING_TABLE),
source_checksum: 0,
created_timestamp: 0,
section_count: 0,
section_table_offset: HEADER_SIZE as u32,
string_table_index: 0,
total_size: HEADER_SIZE as u32,
}
}
pub fn with_timestamp() -> Self {
let mut header = Self::new();
header.created_timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
header
}
pub fn compute_checksum(yaml_source: &[u8]) -> u32 {
crc32fast::hash(yaml_source)
}
pub fn set_source_checksum(&mut self, yaml_source: &[u8]) {
self.source_checksum = Self::compute_checksum(yaml_source);
}
pub fn write<W: Write>(&self, writer: &mut W) -> FafbResult<()> {
writer.write_all(&MAGIC)?;
writer.write_u8(self.version_major)?;
writer.write_u8(self.version_minor)?;
writer.write_u16::<LittleEndian>(self.flags.raw())?;
writer.write_u32::<LittleEndian>(self.source_checksum)?;
writer.write_u64::<LittleEndian>(self.created_timestamp)?;
writer.write_u16::<LittleEndian>(self.section_count)?;
writer.write_u32::<LittleEndian>(self.section_table_offset)?;
writer.write_u16::<LittleEndian>(self.string_table_index)?;
writer.write_u32::<LittleEndian>(self.total_size)?;
Ok(())
}
pub fn to_bytes(&self) -> FafbResult<Vec<u8>> {
let mut buf = Vec::with_capacity(HEADER_SIZE);
self.write(&mut buf)?;
Ok(buf)
}
pub fn read<R: Read>(reader: &mut R) -> FafbResult<Self> {
let mut magic = [0u8; 4];
reader.read_exact(&mut magic)?;
let magic_u32 = u32::from_le_bytes(magic);
if magic_u32 != MAGIC_U32 {
return Err(FafbError::InvalidMagic(magic_u32));
}
let version_major = reader.read_u8()?;
let version_minor = reader.read_u8()?;
if version_major != VERSION_MAJOR {
return Err(FafbError::IncompatibleVersion {
expected: VERSION_MAJOR,
actual: version_major,
});
}
let flags = Flags::from_raw(reader.read_u16::<LittleEndian>()?);
let source_checksum = reader.read_u32::<LittleEndian>()?;
let created_timestamp = reader.read_u64::<LittleEndian>()?;
let section_count = reader.read_u16::<LittleEndian>()?;
if section_count > MAX_SECTIONS {
return Err(FafbError::TooManySections {
count: section_count,
max: MAX_SECTIONS,
});
}
let section_table_offset = reader.read_u32::<LittleEndian>()?;
let string_table_index = reader.read_u16::<LittleEndian>()?;
let total_size = reader.read_u32::<LittleEndian>()?;
if total_size > MAX_FILE_SIZE {
return Err(FafbError::SizeMismatch {
header_size: total_size,
actual_size: MAX_FILE_SIZE as usize,
});
}
Ok(Self {
version_major,
version_minor,
flags,
source_checksum,
created_timestamp,
section_count,
section_table_offset,
string_table_index,
total_size,
})
}
pub fn from_bytes(data: &[u8]) -> FafbResult<Self> {
if data.len() < HEADER_SIZE {
return Err(FafbError::FileTooSmall {
expected: HEADER_SIZE,
actual: data.len(),
});
}
let mut cursor = Cursor::new(data);
Self::read(&mut cursor)
}
pub fn validate(&self, file_data: &[u8]) -> FafbResult<()> {
if self.total_size as usize != file_data.len() {
return Err(FafbError::SizeMismatch {
header_size: self.total_size,
actual_size: file_data.len(),
});
}
if self.section_table_offset > self.total_size {
return Err(FafbError::InvalidSectionTableOffset {
offset: self.section_table_offset,
file_size: self.total_size,
});
}
Ok(())
}
pub fn is_compatible(&self) -> bool {
self.version_major == VERSION_MAJOR
}
pub fn version_string(&self) -> String {
format!("{}.{}", self.version_major, self.version_minor)
}
}
impl Default for FafbHeader {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_header_size() {
let header = FafbHeader::new();
let bytes = header.to_bytes().unwrap();
assert_eq!(bytes.len(), HEADER_SIZE);
assert_eq!(bytes.len(), 32);
}
#[test]
fn test_magic_bytes() {
let header = FafbHeader::new();
let bytes = header.to_bytes().unwrap();
assert_eq!(&bytes[0..4], b"FAFB");
}
#[test]
fn test_roundtrip() {
let mut original = FafbHeader::with_timestamp();
original.source_checksum = 0xDEADBEEF;
original.section_count = 5;
original.section_table_offset = 1024;
original.total_size = 2048;
original.string_table_index = 4;
original.flags.set_compressed(true);
original.flags.set_embeddings(true);
let bytes = original.to_bytes().unwrap();
let recovered = FafbHeader::from_bytes(&bytes).unwrap();
assert_eq!(original.version_major, recovered.version_major);
assert_eq!(original.version_minor, recovered.version_minor);
assert_eq!(original.flags, recovered.flags);
assert_eq!(original.source_checksum, recovered.source_checksum);
assert_eq!(original.created_timestamp, recovered.created_timestamp);
assert_eq!(original.section_count, recovered.section_count);
assert_eq!(
original.section_table_offset,
recovered.section_table_offset
);
assert_eq!(original.string_table_index, recovered.string_table_index);
assert_eq!(original.total_size, recovered.total_size);
}
#[test]
fn test_invalid_magic() {
let mut bytes = FafbHeader::new().to_bytes().unwrap();
bytes[0] = 0x00;
let result = FafbHeader::from_bytes(&bytes);
assert!(matches!(result, Err(FafbError::InvalidMagic(_))));
}
#[test]
fn test_incompatible_version() {
let mut bytes = FafbHeader::new().to_bytes().unwrap();
bytes[4] = 99;
let result = FafbHeader::from_bytes(&bytes);
assert!(matches!(
result,
Err(FafbError::IncompatibleVersion {
expected: 1,
actual: 99
})
));
}
#[test]
fn test_file_too_small() {
let bytes = vec![0u8; 16];
let result = FafbHeader::from_bytes(&bytes);
assert!(matches!(
result,
Err(FafbError::FileTooSmall {
expected: 32,
actual: 16
})
));
}
#[test]
fn test_too_many_sections() {
let mut header = FafbHeader::new();
header.section_count = 300;
let bytes = header.to_bytes().unwrap();
let result = FafbHeader::from_bytes(&bytes);
assert!(matches!(
result,
Err(FafbError::TooManySections {
count: 300,
max: 256
})
));
}
#[test]
fn test_checksum_computation() {
let yaml = b"faf_version: 2.5.0\nproject:\n name: test";
let checksum = FafbHeader::compute_checksum(yaml);
assert_eq!(checksum, FafbHeader::compute_checksum(yaml));
let yaml2 = b"faf_version: 2.5.0\nproject:\n name: different";
assert_ne!(checksum, FafbHeader::compute_checksum(yaml2));
}
#[test]
fn test_validate_size_mismatch() {
let mut header = FafbHeader::new();
header.total_size = 100;
let data = vec![0u8; 50];
let result = header.validate(&data);
assert!(matches!(
result,
Err(FafbError::SizeMismatch {
header_size: 100,
actual_size: 50
})
));
}
#[test]
fn test_validate_invalid_section_offset() {
let mut header = FafbHeader::new();
header.total_size = 100;
header.section_table_offset = 200;
let data = vec![0u8; 100];
let result = header.validate(&data);
assert!(matches!(
result,
Err(FafbError::InvalidSectionTableOffset {
offset: 200,
file_size: 100
})
));
}
#[test]
fn test_version_string() {
let header = FafbHeader::new();
assert_eq!(header.version_string(), "1.0");
}
#[test]
fn test_flags_preserved() {
let mut header = FafbHeader::new();
header.flags.set_compressed(true);
header.flags.set_signed(true);
let bytes = header.to_bytes().unwrap();
let recovered = FafbHeader::from_bytes(&bytes).unwrap();
assert!(recovered.flags.is_compressed());
assert!(recovered.flags.is_signed());
assert!(!recovered.flags.has_embeddings());
assert!(recovered.flags.has_string_table());
}
#[test]
fn test_unknown_flags_ignored() {
let mut header = FafbHeader::new();
header.flags = Flags::from_raw(0xFF00 | FLAG_STRING_TABLE);
let bytes = header.to_bytes().unwrap();
let recovered = FafbHeader::from_bytes(&bytes).unwrap();
assert_eq!(recovered.flags.raw(), 0xFF00 | FLAG_STRING_TABLE);
}
#[test]
fn test_string_table_flag_always_set() {
let header = FafbHeader::new();
assert!(header.flags.has_string_table());
}
#[test]
fn test_string_table_index_roundtrip() {
let mut header = FafbHeader::new();
header.string_table_index = 7;
header.total_size = 1000;
header.section_count = 8;
let bytes = header.to_bytes().unwrap();
let recovered = FafbHeader::from_bytes(&bytes).unwrap();
assert_eq!(recovered.string_table_index, 7);
}
}