use byteorder::{BigEndian, ReadBytesExt};
use bytes::{BufMut, BytesMut};
use noxu_util::lsn::Lsn;
use std::io::{self, Cursor};
use std::time::{SystemTime, UNIX_EPOCH};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum FileHeaderError {
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error("Invalid log version: expected <= {expected}, got {actual}")]
InvalidLogVersion { expected: u32, actual: u32 },
#[error("Wrong file number: expected {expected}, got {actual}")]
WrongFileNumber { expected: u64, actual: u64 },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileHeader {
pub file_num: u64,
pub last_entry_in_prev_file: Lsn,
pub timestamp: u64,
pub log_version: u32,
}
impl FileHeader {
pub fn new(
file_num: u64,
last_entry_in_prev_file: Lsn,
log_version: u32,
) -> Self {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
Self { file_num, last_entry_in_prev_file, timestamp, log_version }
}
pub const fn log_size() -> usize {
8 + 4 + 8 + 4 }
pub fn write_to_log(&self, buf: &mut BytesMut) {
buf.put_i64(self.timestamp as i64);
buf.put_u32(self.file_num as u32);
buf.put_u64(self.last_entry_in_prev_file.as_u64());
buf.put_u32(self.log_version);
}
pub fn read_from_log(buf: &[u8]) -> Result<Self, FileHeaderError> {
let mut cursor = Cursor::new(buf);
let timestamp = cursor.read_i64::<BigEndian>()? as u64;
let file_num = cursor.read_u32::<BigEndian>()? as u64;
let last_entry_lsn_raw = cursor.read_u64::<BigEndian>()?;
let last_entry_in_prev_file = Lsn::from_u64(last_entry_lsn_raw);
let log_version = cursor.read_u32::<BigEndian>()?;
Ok(Self { file_num, last_entry_in_prev_file, timestamp, log_version })
}
pub fn validate(
&self,
expected_file_num: u64,
current_log_version: u32,
) -> Result<(), FileHeaderError> {
if self.log_version > current_log_version {
return Err(FileHeaderError::InvalidLogVersion {
expected: current_log_version,
actual: self.log_version,
});
}
if self.file_num != expected_file_num {
return Err(FileHeaderError::WrongFileNumber {
expected: expected_file_num,
actual: self.file_num,
});
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct FileHeaderEntry {
pub header: FileHeader,
}
impl FileHeaderEntry {
pub fn new(
file_num: u64,
last_entry_in_prev_file: Lsn,
log_version: u32,
) -> Self {
Self {
header: FileHeader::new(
file_num,
last_entry_in_prev_file,
log_version,
),
}
}
pub fn log_size(&self) -> usize {
FileHeader::log_size()
}
pub fn write_to_log(&self, buf: &mut BytesMut) {
self.header.write_to_log(buf);
}
pub fn read_from_log(buf: &[u8]) -> Result<Self, FileHeaderError> {
Ok(Self { header: FileHeader::read_from_log(buf)? })
}
}
#[cfg(test)]
mod tests {
use super::*;
use noxu_util::NULL_LSN;
#[test]
fn test_file_header_roundtrip() {
let header = FileHeader::new(42, Lsn::new(10, 5000), 1);
let mut buf = BytesMut::new();
header.write_to_log(&mut buf);
let decoded = FileHeader::read_from_log(&buf).unwrap();
assert_eq!(header.file_num, decoded.file_num);
assert_eq!(
header.last_entry_in_prev_file,
decoded.last_entry_in_prev_file
);
assert_eq!(header.log_version, decoded.log_version);
assert_eq!(header.timestamp, decoded.timestamp);
}
#[test]
fn test_file_header_entry_roundtrip() {
let entry = FileHeaderEntry::new(0, NULL_LSN, 1);
let mut buf = BytesMut::new();
entry.write_to_log(&mut buf);
let decoded = FileHeaderEntry::read_from_log(&buf).unwrap();
assert_eq!(entry.header.file_num, decoded.header.file_num);
assert_eq!(entry.header.log_version, decoded.header.log_version);
}
#[test]
fn test_validate_success() {
let header = FileHeader::new(5, NULL_LSN, 1);
assert!(header.validate(5, 1).is_ok());
assert!(header.validate(5, 2).is_ok()); }
#[test]
fn test_validate_wrong_file_number() {
let header = FileHeader::new(5, NULL_LSN, 1);
let result = header.validate(6, 1);
assert!(result.is_err());
}
#[test]
fn test_validate_version_too_new() {
let header = FileHeader::new(5, NULL_LSN, 10);
let result = header.validate(5, 9);
assert!(result.is_err());
}
#[test]
fn test_log_size() {
assert_eq!(FileHeader::log_size(), 24);
}
}