use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use std::io::{self, Read, Write};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::error::{LogError, Result};
pub const FILE_MAGIC: &[u8; 8] = b"NOXUDB\0\0";
pub const BYTE_ORDER_BIG_ENDIAN: u8 = 0x00;
pub const BYTE_ORDER_LITTLE_ENDIAN: u8 = 0x01;
pub const LOG_VERSION: u32 = 3;
pub const MIN_LOG_VERSION: u32 = 2;
pub const FILE_HEADER_SIZE_V2: usize = 8 + 4 + 1 + 3 + 8 + 4 + 4;
pub const FILE_HEADER_SIZE: usize = FILE_HEADER_SIZE_V2 + 4;
#[inline]
pub fn on_disk_size(version: u32) -> usize {
if version < LOG_VERSION { FILE_HEADER_SIZE_V2 } else { FILE_HEADER_SIZE }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileHeader {
pub timestamp: u64,
pub file_number: u32,
pub last_entry_in_prev_file: u32,
pub log_version: u32,
}
impl FileHeader {
pub fn new(file_number: u32, last_entry_in_prev_file: u32) -> Self {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("System time before UNIX epoch")
.as_millis() as u64;
FileHeader {
timestamp,
file_number,
last_entry_in_prev_file,
log_version: LOG_VERSION,
}
}
#[inline]
pub fn on_disk_size(version: u32) -> usize {
on_disk_size(version)
}
pub fn read_from<R: Read>(reader: &mut R) -> Result<Self> {
let mut magic = [0u8; 8];
reader.read_exact(&mut magic)?;
if &magic != FILE_MAGIC {
return Err(LogError::InvalidHeader {
file_num: u32::MAX,
message: format!(
"bad magic: expected {FILE_MAGIC:?}, found {magic:?}"
),
});
}
let log_version = reader.read_u32::<BigEndian>()?;
if log_version < MIN_LOG_VERSION {
return Err(LogError::VersionMismatch {
expected: LOG_VERSION,
found: log_version,
file_num: u32::MAX,
});
}
let byte_order = reader.read_u8()?;
if byte_order != BYTE_ORDER_BIG_ENDIAN {
return Err(LogError::InvalidHeader {
file_num: u32::MAX,
message: format!(
"unsupported byte order: 0x{:02X} \
(only big-endian 0x00 is supported)",
byte_order
),
});
}
let mut _reserved = [0u8; 3];
reader.read_exact(&mut _reserved)?;
let timestamp = reader.read_u64::<BigEndian>()?;
let file_number = reader.read_u32::<BigEndian>()?;
let last_entry_in_prev_file = reader.read_u32::<BigEndian>()?;
if log_version >= LOG_VERSION {
let mut covered = [0u8; FILE_HEADER_SIZE_V2];
covered[..8].copy_from_slice(FILE_MAGIC);
covered[8..12].copy_from_slice(&log_version.to_be_bytes());
covered[12] = BYTE_ORDER_BIG_ENDIAN;
covered[13..16].copy_from_slice(&[0u8; 3]);
covered[16..24].copy_from_slice(×tamp.to_be_bytes());
covered[24..28].copy_from_slice(&file_number.to_be_bytes());
covered[28..32]
.copy_from_slice(&last_entry_in_prev_file.to_be_bytes());
let expected_crc = crc32fast::hash(&covered);
let stored_crc = reader.read_u32::<BigEndian>()?;
if stored_crc != expected_crc {
return Err(LogError::HeaderChecksumMismatch {
file_num: file_number,
expected: expected_crc,
found: stored_crc,
});
}
}
Ok(FileHeader {
timestamp,
file_number,
last_entry_in_prev_file,
log_version,
})
}
pub fn write_to<W: Write>(&self, writer: &mut W) -> io::Result<()> {
let mut covered = [0u8; FILE_HEADER_SIZE_V2];
covered[..8].copy_from_slice(FILE_MAGIC);
covered[8..12].copy_from_slice(&self.log_version.to_be_bytes());
covered[12] = BYTE_ORDER_BIG_ENDIAN;
covered[13..16].copy_from_slice(&[0u8; 3]);
covered[16..24].copy_from_slice(&self.timestamp.to_be_bytes());
covered[24..28].copy_from_slice(&self.file_number.to_be_bytes());
covered[28..32]
.copy_from_slice(&self.last_entry_in_prev_file.to_be_bytes());
writer.write_all(&covered)?;
let crc = crc32fast::hash(&covered);
writer.write_u32::<BigEndian>(crc)?;
Ok(())
}
pub fn validate(&self, expected_file_num: u32) -> Result<u32> {
if self.log_version > LOG_VERSION {
return Err(LogError::VersionMismatch {
expected: LOG_VERSION,
found: self.log_version,
file_num: self.file_number,
});
}
if self.file_number != expected_file_num {
return Err(LogError::InvalidHeader {
file_num: self.file_number,
message: format!(
"Expected file number {expected_file_num:08x}, found {:08x}",
self.file_number
),
});
}
Ok(self.log_version)
}
pub fn last_entry_in_prev_file_offset(&self) -> u32 {
self.last_entry_in_prev_file
}
pub const fn size() -> usize {
FILE_HEADER_SIZE
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn test_log_version_is_3() {
assert_eq!(LOG_VERSION, 3);
}
#[test]
fn test_file_header_size_v2_is_32() {
assert_eq!(FILE_HEADER_SIZE_V2, 32);
}
#[test]
fn test_file_header_size_v3_is_36() {
assert_eq!(FILE_HEADER_SIZE, 36);
assert_eq!(FileHeader::size(), 36);
}
#[test]
fn test_on_disk_size_dispatch() {
assert_eq!(FileHeader::on_disk_size(2), 32, "v2 → 32 bytes");
assert_eq!(FileHeader::on_disk_size(3), 36, "v3 → 36 bytes");
assert_eq!(on_disk_size(2), 32);
assert_eq!(on_disk_size(3), 36);
}
#[test]
fn test_magic_bytes() {
assert_eq!(FILE_MAGIC, b"NOXUDB\0\0");
assert_eq!(FILE_MAGIC.len(), 8);
}
#[test]
fn test_v3_header_roundtrip_crc_verified() {
let header = FileHeader::new(42, 0x1000);
assert_eq!(header.log_version, LOG_VERSION, "new header is v3");
let mut buf = Vec::new();
header.write_to(&mut buf).unwrap();
assert_eq!(buf.len(), FILE_HEADER_SIZE, "v3 header is 36 bytes");
assert_eq!(&buf[..8], FILE_MAGIC);
assert_eq!(&buf[8..12], &LOG_VERSION.to_be_bytes());
assert_eq!(buf[12], BYTE_ORDER_BIG_ENDIAN);
assert_eq!(&buf[13..16], &[0u8; 3]);
let expected_crc = crc32fast::hash(&buf[..32]);
let stored_crc =
u32::from_be_bytes([buf[32], buf[33], buf[34], buf[35]]);
assert_eq!(stored_crc, expected_crc, "stored CRC matches computed CRC");
let mut cursor = Cursor::new(&buf);
let decoded = FileHeader::read_from(&mut cursor).unwrap();
assert_eq!(decoded.file_number, 42);
assert_eq!(decoded.last_entry_in_prev_file, 0x1000);
assert_eq!(decoded.log_version, LOG_VERSION);
assert!(decoded.timestamp > 0);
}
#[test]
fn test_corrupt_v3_header_byte_detected() {
let header = FileHeader::new(7, 0);
let mut buf = Vec::new();
header.write_to(&mut buf).unwrap();
buf[25] ^= 0xFF;
let mut cursor = Cursor::new(buf);
let result = FileHeader::read_from(&mut cursor);
assert!(
result.is_err(),
"corrupted v3 header must not parse successfully"
);
match result.unwrap_err() {
LogError::HeaderChecksumMismatch { .. } => {} other => panic!("expected HeaderChecksumMismatch, got {:?}", other),
}
}
#[test]
fn test_corrupt_v3_crc_field_detected() {
let header = FileHeader::new(3, 0);
let mut buf = Vec::new();
header.write_to(&mut buf).unwrap();
buf[32] ^= 0xFF;
buf[33] ^= 0xFF;
buf[34] ^= 0xFF;
buf[35] ^= 0xFF;
let mut cursor = Cursor::new(buf);
match FileHeader::read_from(&mut cursor).unwrap_err() {
LogError::HeaderChecksumMismatch { .. } => {}
other => panic!("expected HeaderChecksumMismatch, got {:?}", other),
}
}
#[test]
fn test_v2_header_backward_compat_no_crc_check() {
let mut buf = Vec::new();
buf.extend_from_slice(FILE_MAGIC); buf.extend_from_slice(&2u32.to_be_bytes()); buf.push(BYTE_ORDER_BIG_ENDIAN); buf.extend_from_slice(&[0u8; 3]); buf.extend_from_slice(&9999u64.to_be_bytes()); buf.extend_from_slice(&55u32.to_be_bytes()); buf.extend_from_slice(&0x400u32.to_be_bytes());
assert_eq!(buf.len(), FILE_HEADER_SIZE_V2, "v2 header is 32 bytes");
let mut cursor = Cursor::new(&buf);
let header = FileHeader::read_from(&mut cursor)
.expect("v2 header must parse without error");
assert_eq!(header.log_version, 2);
assert_eq!(header.file_number, 55);
assert_eq!(header.last_entry_in_prev_file, 0x400);
assert_eq!(header.timestamp, 9999);
assert_eq!(
FileHeader::on_disk_size(header.log_version),
32,
"v2 first-entry offset is 32"
);
}
#[test]
fn test_v2_header_consumes_exactly_32_bytes() {
let mut buf = Vec::new();
buf.extend_from_slice(FILE_MAGIC);
buf.extend_from_slice(&2u32.to_be_bytes());
buf.push(BYTE_ORDER_BIG_ENDIAN);
buf.extend_from_slice(&[0u8; 3]);
buf.extend_from_slice(&1u64.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let mut cursor = Cursor::new(&buf);
let _header = FileHeader::read_from(&mut cursor).unwrap();
assert_eq!(
cursor.position(),
32,
"v2 read must consume exactly 32 bytes"
);
}
#[test]
fn test_file_header_validate_ok() {
let header = FileHeader::new(10, 500);
assert!(header.validate(10).is_ok());
assert_eq!(header.validate(10).unwrap(), LOG_VERSION);
}
#[test]
fn test_file_header_validate_wrong_number() {
let header = FileHeader::new(5, 0);
assert!(header.validate(99).is_err());
}
#[test]
fn test_file_header_validate_future_version_rejected() {
let mut header = FileHeader::new(0, 0);
header.log_version = LOG_VERSION + 1;
assert!(header.validate(0).is_err());
}
#[test]
fn test_read_rejects_bad_magic() {
let mut buf = vec![0u8; FILE_HEADER_SIZE];
buf[..8].copy_from_slice(b"WRONGMAG");
let mut cursor = Cursor::new(buf);
assert!(FileHeader::read_from(&mut cursor).is_err());
}
#[test]
fn test_read_rejects_little_endian_byte_order() {
let header = FileHeader::new(0, 0);
let mut buf = Vec::new();
header.write_to(&mut buf).unwrap();
buf[12] = BYTE_ORDER_LITTLE_ENDIAN;
let new_crc = crc32fast::hash(&buf[..32]);
buf[32..36].copy_from_slice(&new_crc.to_be_bytes());
let mut cursor = Cursor::new(buf);
assert!(FileHeader::read_from(&mut cursor).is_err());
}
#[test]
fn test_read_rejects_old_version() {
let mut buf = Vec::new();
buf.extend_from_slice(FILE_MAGIC);
buf.extend_from_slice(&1u32.to_be_bytes()); buf.push(BYTE_ORDER_BIG_ENDIAN);
buf.extend_from_slice(&[0u8; 3]);
buf.extend_from_slice(&42u64.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
let mut cursor = Cursor::new(buf);
assert!(FileHeader::read_from(&mut cursor).is_err());
}
#[test]
fn test_last_entry_in_prev_file_offset() {
let header = FileHeader::new(3, 9999);
assert_eq!(header.last_entry_in_prev_file_offset(), 9999);
}
}