use crate::error::{NoxuLogError, Result};
use crate::file_reader::{FileReader, LogFileAccess};
use hashbrown::{HashMap, HashSet};
use noxu_util::lsn::{Lsn, NULL_LSN};
pub struct LastFileReader<F: LogFileAccess> {
reader: FileReader<F>,
trackable_entries: HashSet<u8>,
last_offset_seen: HashMap<u8, u64>,
next_unproven_offset: u64,
last_valid_offset: u64,
last_entry_type: u8,
file_num: u32,
}
impl<F: LogFileAccess> LastFileReader<F> {
pub fn new(file_access: F, read_buffer_size: usize) -> Result<Self> {
let (file_num, file_len) = Self::find_last_good_file(&file_access)?;
let start_lsn = Lsn::new(file_num, 0);
let end_of_file_lsn = Lsn::new(file_num, file_len as u32);
let reader = FileReader::new(
file_access,
true, start_lsn,
end_of_file_lsn,
NULL_LSN, read_buffer_size,
true, )?;
Ok(LastFileReader {
reader,
trackable_entries: HashSet::new(),
last_offset_seen: HashMap::new(),
next_unproven_offset: 0,
last_valid_offset: 0,
last_entry_type: 0,
file_num,
})
}
fn find_last_good_file(file_access: &F) -> Result<(u32, u64)> {
let first_file = file_access.get_first_file_num().unwrap_or(0);
let mut current_file = first_file;
let mut last_good_file = None;
#[expect(clippy::while_let_loop)]
loop {
match file_access.get_file_length(current_file) {
Ok(len) => {
if len > 0 {
last_good_file = Some((current_file, len));
}
if let Some(next) =
file_access.get_following_file_num(current_file, true)
{
current_file = next;
} else {
break;
}
}
Err(_) => {
break;
}
}
}
last_good_file.ok_or_else(|| NoxuLogError::UnexpectedEof {
lsn: NULL_LSN,
message: "No valid log files found".to_string(),
})
}
pub fn set_target_type(&mut self, entry_type: u8) {
self.trackable_entries.insert(entry_type);
}
pub fn get_last_seen(&self, entry_type: u8) -> Lsn {
self.last_offset_seen
.get(&entry_type)
.map(|&offset| Lsn::new(self.file_num, offset as u32))
.unwrap_or(NULL_LSN)
}
pub fn get_end_of_log(&self) -> Lsn {
Lsn::new(self.file_num, self.next_unproven_offset as u32)
}
pub fn get_last_valid_lsn(&self) -> Lsn {
Lsn::new(self.file_num, self.last_valid_offset as u32)
}
pub fn get_prev_offset(&self) -> u64 {
self.last_valid_offset
}
pub fn get_entry_type(&self) -> u8 {
self.last_entry_type
}
pub fn read_next_entry(&mut self) -> Result<bool> {
let current_offset =
self.reader.get_current_entry_lsn().file_offset() as u64;
let _next_offset = current_offset;
match self.reader.read_next_entry() {
Ok(found) => {
if found {
let lsn = self.reader.get_current_entry_lsn();
self.last_valid_offset = lsn.file_offset() as u64;
self.next_unproven_offset = self.last_valid_offset
+ self.reader.get_last_entry_size() as u64;
if let Some(header) = self.reader.get_current_entry_header()
{
self.last_entry_type = header.entry_type;
if self.trackable_entries.contains(&header.entry_type) {
self.last_offset_seen.insert(
header.entry_type,
self.last_valid_offset,
);
}
}
Ok(true)
} else {
Ok(false)
}
}
Err(NoxuLogError::Checksum { lsn: _, .. }) => {
Ok(false)
}
Err(e) => {
Err(e)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::file_reader::LogFileAccess;
use std::collections::HashMap;
use std::io;
struct MockFileAccess {
files: HashMap<u32, Vec<u8>>,
}
impl MockFileAccess {
fn new() -> Self {
MockFileAccess { files: HashMap::new() }
}
fn add_file(&mut self, file_num: u32, data: Vec<u8>) {
self.files.insert(file_num, data);
}
}
impl LogFileAccess for MockFileAccess {
fn read_from_file(
&self,
file_num: u32,
offset: u64,
buf: &mut [u8],
) -> Result<usize> {
if let Some(data) = self.files.get(&file_num) {
let start = offset as usize;
if start >= data.len() {
return Ok(0);
}
let end = (start + buf.len()).min(data.len());
let bytes_to_copy = end - start;
buf[..bytes_to_copy].copy_from_slice(&data[start..end]);
Ok(bytes_to_copy)
} else {
Err(io::Error::new(io::ErrorKind::NotFound, "File not found")
.into())
}
}
fn get_file_length(&self, file_num: u32) -> Result<u64> {
self.files.get(&file_num).map(|data| data.len() as u64).ok_or_else(
|| {
io::Error::new(io::ErrorKind::NotFound, "File not found")
.into()
},
)
}
fn get_first_file_num(&self) -> Option<u32> {
self.files.keys().min().copied()
}
fn get_following_file_num(
&self,
file_num: u32,
forward: bool,
) -> Option<u32> {
let mut file_nums: Vec<u32> = self.files.keys().copied().collect();
file_nums.sort();
if forward {
file_nums.iter().find(|&&n| n > file_num).copied()
} else {
file_nums.iter().rev().find(|&&n| n < file_num).copied()
}
}
fn get_file_header_prev_offset(&self, _file_num: u32) -> Result<u64> {
Ok(0)
}
}
#[test]
fn test_last_file_reader_creation() {
let mut mock = MockFileAccess::new();
mock.add_file(0, vec![0u8; 100]);
let result = LastFileReader::new(mock, 1024);
assert!(result.is_ok());
}
#[test]
fn test_find_last_good_file() {
let mut mock = MockFileAccess::new();
mock.add_file(0, vec![0u8; 100]);
mock.add_file(1, vec![0u8; 200]);
mock.add_file(2, vec![0u8; 50]);
let (file_num, len) =
LastFileReader::find_last_good_file(&mock).unwrap();
assert_eq!(file_num, 2);
assert_eq!(len, 50);
}
#[test]
fn test_last_file_reader_no_files() {
let mock = MockFileAccess::new();
let result = LastFileReader::new(mock, 1024);
assert!(result.is_err());
}
#[test]
fn test_last_file_reader_single_file() {
let mut mock = MockFileAccess::new();
mock.add_file(0, vec![0u8; 100]);
let result = LastFileReader::new(mock, 1024);
assert!(result.is_ok());
}
#[test]
fn test_find_last_good_file_single_file() {
let mut mock = MockFileAccess::new();
mock.add_file(0, vec![0u8; 42]);
let (file_num, len) =
LastFileReader::find_last_good_file(&mock).unwrap();
assert_eq!(file_num, 0);
assert_eq!(len, 42);
}
#[test]
fn test_find_last_good_file_empty_file_skipped() {
let mut mock = MockFileAccess::new();
mock.add_file(0, vec![0u8; 100]);
mock.add_file(1, vec![]);
let (file_num, len) =
LastFileReader::find_last_good_file(&mock).unwrap();
assert_eq!(file_num, 0);
assert_eq!(len, 100);
}
#[test]
fn test_last_file_reader_set_target_type() {
let mut mock = MockFileAccess::new();
mock.add_file(0, vec![0u8; 64]);
let mut reader = LastFileReader::new(mock, 64).unwrap();
reader.set_target_type(1);
reader.set_target_type(2);
assert!(reader.get_last_seen(1).is_null());
assert!(reader.get_last_seen(255).is_null());
}
#[test]
fn test_last_file_reader_initial_offsets() {
let mut mock = MockFileAccess::new();
mock.add_file(0, vec![0u8; 128]);
let reader = LastFileReader::new(mock, 64).unwrap();
assert_eq!(reader.get_prev_offset(), 0);
assert_eq!(reader.get_entry_type(), 0);
let eol = reader.get_end_of_log();
assert_eq!(eol.file_number(), 0);
}
#[test]
fn test_last_file_reader_read_entry() {
let mut mock = MockFileAccess::new();
mock.add_file(0, vec![0u8; 14]);
let mut reader = LastFileReader::new(mock, 64).unwrap();
let result = reader.read_next_entry();
assert!(matches!(result, Ok(true)));
assert_eq!(reader.get_entry_type(), 0);
}
#[test]
fn test_last_file_reader_read_entry_updates_valid_lsn() {
let mut mock = MockFileAccess::new();
mock.add_file(0, vec![0u8; 14]);
let mut reader = LastFileReader::new(mock, 64).unwrap();
reader.read_next_entry().unwrap();
let valid_lsn = reader.get_last_valid_lsn();
assert_eq!(valid_lsn.file_number(), 0);
}
#[test]
fn test_last_file_reader_read_entry_updates_end_of_log() {
let mut mock = MockFileAccess::new();
mock.add_file(0, vec![0u8; 14]);
let mut reader = LastFileReader::new(mock, 64).unwrap();
reader.read_next_entry().unwrap();
let eol = reader.get_end_of_log();
assert_eq!(eol.file_number(), 0);
let _ = eol.file_offset();
}
#[test]
fn test_last_file_reader_tracks_target_type() {
let mut mock = MockFileAccess::new();
let data = vec![0u8; 28]; mock.add_file(0, data);
let mut reader = LastFileReader::new(mock, 64).unwrap();
reader.set_target_type(0);
reader.read_next_entry().unwrap();
reader.read_next_entry().unwrap();
let lsn = reader.get_last_seen(0);
assert!(!lsn.is_null());
}
#[test]
fn test_last_file_reader_untracked_type_returns_null() {
let mut mock = MockFileAccess::new();
mock.add_file(0, vec![0u8; 14]); let mut reader = LastFileReader::new(mock, 64).unwrap();
reader.read_next_entry().unwrap();
assert!(reader.get_last_seen(5).is_null());
}
#[test]
fn test_last_file_reader_read_until_eof() {
let mut mock = MockFileAccess::new();
mock.add_file(0, vec![0u8; 14]);
let mut reader = LastFileReader::new(mock, 64).unwrap();
assert!(matches!(reader.read_next_entry(), Ok(true)));
let result = reader.read_next_entry();
assert!(matches!(result, Ok(false)) || result.is_err());
}
}