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 = 2;
pub const MIN_LOG_VERSION: u32 = 2;
pub const FILE_HEADER_SIZE: usize = 8 + 4 + 1 + 3 + 8 + 4 + 4;
#[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,
}
}
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>()?;
Ok(FileHeader {
timestamp,
file_number,
last_entry_in_prev_file,
log_version,
})
}
pub fn write_to<W: Write>(&self, writer: &mut W) -> io::Result<()> {
writer.write_all(FILE_MAGIC)?;
writer.write_u32::<BigEndian>(self.log_version)?;
writer.write_u8(BYTE_ORDER_BIG_ENDIAN)?;
writer.write_all(&[0u8; 3])?;
writer.write_u64::<BigEndian>(self.timestamp)?;
writer.write_u32::<BigEndian>(self.file_number)?;
writer.write_u32::<BigEndian>(self.last_entry_in_prev_file)?;
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_file_header_size_is_32() {
assert_eq!(FILE_HEADER_SIZE, 32);
assert_eq!(FileHeader::size(), 32);
}
#[test]
fn test_log_version_is_2() {
assert_eq!(LOG_VERSION, 2);
}
#[test]
fn test_magic_bytes() {
assert_eq!(FILE_MAGIC, b"NOXUDB\0\0");
assert_eq!(FILE_MAGIC.len(), 8);
}
#[test]
fn test_file_header_roundtrip() {
let header = FileHeader::new(42, 0x1000);
let mut buf = Vec::new();
header.write_to(&mut buf).unwrap();
assert_eq!(buf.len(), FILE_HEADER_SIZE);
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, 0, 0]);
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_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 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);
}
}