pub mod semantics;
pub mod transactions;
pub mod usn_extractor;
pub use semantics::{classify, FileOperation};
pub use transactions::{reconstruct_transactions, Transaction, TransactionState};
pub use usn_extractor::{extract_usn_from_logfile, LogFileRecordSource, LogFileUsnRecord};
use crate::error::Result;
const RSTR_SIGNATURE: &[u8; 4] = b"RSTR";
const RCRD_SIGNATURE: &[u8; 4] = b"RCRD";
const LOG_PAGE_SIZE: usize = 0x1000;
#[derive(Debug, Clone)]
pub struct RestartArea {
pub offset: usize,
pub current_lsn: u64,
pub log_clients: u16,
pub system_page_size: u32,
pub log_page_size: u32,
}
#[derive(Debug, Clone)]
pub struct LogFileSummary {
pub restart_areas: Vec<RestartArea>,
pub record_page_count: usize,
pub has_gaps: bool,
pub highest_lsn: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogOp {
Noop,
CompensationLogRecord,
InitializeFileRecordSegment,
DeallocateFileRecordSegment,
WriteEndOfFileRecordSegment,
CreateAttribute,
DeleteAttribute,
UpdateResidentValue,
UpdateNonResidentValue,
UpdateMappingPairs,
DeleteDirtyClusters,
SetNewAttributeSizes,
AddIndexEntryRoot,
DeleteIndexEntryRoot,
AddIndexEntryAllocation,
DeleteIndexEntryAllocation,
WriteEndOfIndexBuffer,
SetIndexEntryVcnRoot,
SetIndexEntryVcnAllocation,
UpdateFileNameRoot,
UpdateFileNameAllocation,
SetBitsInNonResidentBitMap,
ClearBitsInNonResidentBitMap,
HotFix,
EndTopLevelAction,
PrepareTransaction,
CommitTransaction,
ForgetTransaction,
OpenNonResidentAttribute,
OpenAttributeTableDump,
AttributeNamesDump,
DirtyPageTableDump,
TransactionTableDump,
UpdateRecordDataRoot,
UpdateRecordDataAllocation,
Unknown(u16),
}
impl LogOp {
#[must_use]
pub fn from_u16(code: u16) -> Self {
use LogOp::{
AddIndexEntryAllocation, AddIndexEntryRoot, AttributeNamesDump,
ClearBitsInNonResidentBitMap, CommitTransaction, CompensationLogRecord,
CreateAttribute, DeallocateFileRecordSegment, DeleteAttribute, DeleteDirtyClusters,
DeleteIndexEntryAllocation, DeleteIndexEntryRoot, DirtyPageTableDump,
EndTopLevelAction, ForgetTransaction, HotFix, InitializeFileRecordSegment, Noop,
OpenAttributeTableDump, OpenNonResidentAttribute, PrepareTransaction,
SetBitsInNonResidentBitMap, SetIndexEntryVcnAllocation, SetIndexEntryVcnRoot,
SetNewAttributeSizes, TransactionTableDump, Unknown, UpdateFileNameAllocation,
UpdateFileNameRoot, UpdateMappingPairs, UpdateNonResidentValue,
UpdateRecordDataAllocation, UpdateRecordDataRoot, UpdateResidentValue,
WriteEndOfFileRecordSegment, WriteEndOfIndexBuffer,
};
match code {
0x00 => Noop,
0x01 => CompensationLogRecord,
0x02 => InitializeFileRecordSegment,
0x03 => DeallocateFileRecordSegment,
0x04 => WriteEndOfFileRecordSegment,
0x05 => CreateAttribute,
0x06 => DeleteAttribute,
0x07 => UpdateResidentValue,
0x08 => UpdateNonResidentValue,
0x09 => UpdateMappingPairs,
0x0A => DeleteDirtyClusters,
0x0B => SetNewAttributeSizes,
0x0C => AddIndexEntryRoot,
0x0D => DeleteIndexEntryRoot,
0x0E => AddIndexEntryAllocation,
0x0F => DeleteIndexEntryAllocation,
0x10 => WriteEndOfIndexBuffer,
0x11 => SetIndexEntryVcnRoot,
0x12 => SetIndexEntryVcnAllocation,
0x13 => UpdateFileNameRoot,
0x14 => UpdateFileNameAllocation,
0x15 => SetBitsInNonResidentBitMap,
0x16 => ClearBitsInNonResidentBitMap,
0x17 => HotFix,
0x18 => EndTopLevelAction,
0x19 => PrepareTransaction,
0x1A => CommitTransaction,
0x1B => ForgetTransaction,
0x1C => OpenNonResidentAttribute,
0x1D => OpenAttributeTableDump,
0x1E => AttributeNamesDump,
0x1F => DirtyPageTableDump,
0x20 => TransactionTableDump,
0x21 => UpdateRecordDataRoot,
0x22 => UpdateRecordDataAllocation,
other => Unknown(other),
}
}
#[must_use]
pub fn code(self) -> u16 {
use LogOp::{
AddIndexEntryAllocation, AddIndexEntryRoot, AttributeNamesDump,
ClearBitsInNonResidentBitMap, CommitTransaction, CompensationLogRecord,
CreateAttribute, DeallocateFileRecordSegment, DeleteAttribute, DeleteDirtyClusters,
DeleteIndexEntryAllocation, DeleteIndexEntryRoot, DirtyPageTableDump,
EndTopLevelAction, ForgetTransaction, HotFix, InitializeFileRecordSegment, Noop,
OpenAttributeTableDump, OpenNonResidentAttribute, PrepareTransaction,
SetBitsInNonResidentBitMap, SetIndexEntryVcnAllocation, SetIndexEntryVcnRoot,
SetNewAttributeSizes, TransactionTableDump, Unknown, UpdateFileNameAllocation,
UpdateFileNameRoot, UpdateMappingPairs, UpdateNonResidentValue,
UpdateRecordDataAllocation, UpdateRecordDataRoot, UpdateResidentValue,
WriteEndOfFileRecordSegment, WriteEndOfIndexBuffer,
};
match self {
Noop => 0x00,
CompensationLogRecord => 0x01,
InitializeFileRecordSegment => 0x02,
DeallocateFileRecordSegment => 0x03,
WriteEndOfFileRecordSegment => 0x04,
CreateAttribute => 0x05,
DeleteAttribute => 0x06,
UpdateResidentValue => 0x07,
UpdateNonResidentValue => 0x08,
UpdateMappingPairs => 0x09,
DeleteDirtyClusters => 0x0A,
SetNewAttributeSizes => 0x0B,
AddIndexEntryRoot => 0x0C,
DeleteIndexEntryRoot => 0x0D,
AddIndexEntryAllocation => 0x0E,
DeleteIndexEntryAllocation => 0x0F,
WriteEndOfIndexBuffer => 0x10,
SetIndexEntryVcnRoot => 0x11,
SetIndexEntryVcnAllocation => 0x12,
UpdateFileNameRoot => 0x13,
UpdateFileNameAllocation => 0x14,
SetBitsInNonResidentBitMap => 0x15,
ClearBitsInNonResidentBitMap => 0x16,
HotFix => 0x17,
EndTopLevelAction => 0x18,
PrepareTransaction => 0x19,
CommitTransaction => 0x1A,
ForgetTransaction => 0x1B,
OpenNonResidentAttribute => 0x1C,
OpenAttributeTableDump => 0x1D,
AttributeNamesDump => 0x1E,
DirtyPageTableDump => 0x1F,
TransactionTableDump => 0x20,
UpdateRecordDataRoot => 0x21,
UpdateRecordDataAllocation => 0x22,
Unknown(c) => c,
}
}
}
#[derive(Debug, Clone)]
pub struct RecordPage {
pub offset: usize,
pub last_lsn: u64,
pub data: Vec<u8>,
}
pub fn read_record_pages(data: &[u8]) -> Vec<RecordPage> {
let mut pages = Vec::new();
let page_count = data.len() / LOG_PAGE_SIZE;
for page_idx in 0..page_count {
let offset = page_idx * LOG_PAGE_SIZE;
let page = &data[offset..offset + LOG_PAGE_SIZE];
if &page[0..4] != RCRD_SIGNATURE {
continue;
}
let mut buf = page.to_vec();
if crate::record::apply_fixup(&mut buf, 512).is_err() {
continue;
}
let last_lsn = u64::from_le_bytes(buf[0x08..0x10].try_into().unwrap_or([0; 8]));
pages.push(RecordPage {
offset,
last_lsn,
data: buf,
});
}
pages
}
#[derive(Debug, Clone)]
pub struct LogRecord {
pub page_offset: usize,
pub this_lsn: u64,
pub client_previous_lsn: u64,
pub client_undo_next_lsn: u64,
pub record_type: u32,
pub transaction_id: u32,
pub redo_op: LogOp,
pub undo_op: LogOp,
pub target_attribute: u16,
pub mft_cluster_index: u16,
pub target_vcn: u64,
}
pub fn parse_log_records(page: &RecordPage) -> Vec<LogRecord> {
use crate::bytes::{le_u16, le_u32, le_u64};
const LFS_HEADER_LEN: usize = 0x30;
let data = &page.data;
let end = (le_u16(data, 0x18) as usize).min(data.len());
let page_last_lsn = le_u64(data, 0x08);
let mut records = Vec::new();
let mut off = LFS_HEADER_LEN.max(0x40); while off + LFS_HEADER_LEN <= end {
let this_lsn = le_u64(data, off);
let record_type = le_u32(data, off + 0x20);
let redo_raw = le_u16(data, off + 0x30);
let client_data_length = le_u32(data, off + 0x18) as usize;
let plausible = this_lsn != 0
&& this_lsn <= page_last_lsn
&& (record_type == 1 || record_type == 2)
&& redo_raw <= 0x22
&& off + LFS_HEADER_LEN + client_data_length <= data.len();
if !plausible {
off += 8; continue;
}
records.push(LogRecord {
page_offset: off,
this_lsn,
client_previous_lsn: le_u64(data, off + 0x08),
client_undo_next_lsn: le_u64(data, off + 0x10),
record_type,
transaction_id: le_u32(data, off + 0x24),
redo_op: LogOp::from_u16(redo_raw),
undo_op: LogOp::from_u16(le_u16(data, off + 0x32)),
target_attribute: le_u16(data, off + 0x3C),
mft_cluster_index: le_u16(data, off + 0x44),
target_vcn: le_u64(data, off + 0x48),
});
let advance = (LFS_HEADER_LEN + client_data_length + 7) & !7;
off += advance.max(8);
}
records
}
pub fn parse_logfile(data: &[u8]) -> Result<LogFileSummary> {
let mut restart_areas = Vec::new();
let mut record_page_count = 0;
let mut highest_lsn: u64 = 0;
let mut has_gaps = false;
let mut last_page_had_rcrd = false;
let page_count = data.len() / LOG_PAGE_SIZE;
for page_idx in 0..page_count {
let page_offset = page_idx * LOG_PAGE_SIZE;
let sig = &data[page_offset..page_offset + 4];
if sig == RSTR_SIGNATURE {
if page_offset + 0x28 <= data.len() {
let current_lsn = u64::from_le_bytes(
data[page_offset + 0x08..page_offset + 0x10]
.try_into()
.unwrap_or([0; 8]),
);
let log_clients = u16::from_le_bytes(
data[page_offset + 0x10..page_offset + 0x12]
.try_into()
.unwrap_or([0; 2]),
);
let system_page_size = u32::from_le_bytes(
data[page_offset + 0x20..page_offset + 0x24]
.try_into()
.unwrap_or([0; 4]),
);
let log_page_size = u32::from_le_bytes(
data[page_offset + 0x24..page_offset + 0x28]
.try_into()
.unwrap_or([0; 4]),
);
if current_lsn > highest_lsn {
highest_lsn = current_lsn;
}
restart_areas.push(RestartArea {
offset: page_offset,
current_lsn,
log_clients,
system_page_size,
log_page_size,
});
} last_page_had_rcrd = false;
} else if sig == RCRD_SIGNATURE {
record_page_count += 1;
if page_offset + 0x20 <= data.len() {
let page_lsn = u64::from_le_bytes(
data[page_offset + 0x18..page_offset + 0x20]
.try_into()
.unwrap_or([0; 8]),
);
if page_lsn > highest_lsn {
highest_lsn = page_lsn;
}
}
last_page_had_rcrd = true;
} else {
if last_page_had_rcrd && page_idx > 2 {
let is_zeroed = data[page_offset..page_offset + 4] == [0, 0, 0, 0];
if !is_zeroed {
has_gaps = true;
}
}
last_page_had_rcrd = false;
}
}
Ok(LogFileSummary {
restart_areas,
record_page_count,
has_gaps,
highest_lsn,
})
}
pub fn detect_journal_clearing(logfile_summary: &LogFileSummary) -> bool {
if logfile_summary.has_gaps {
return true;
}
if logfile_summary.restart_areas.len() != 2 {
return logfile_summary.restart_areas.is_empty();
}
false
}
#[cfg(test)]
mod tests {
use super::*;
const LFP_OPS: [LogOp; 35] = [
LogOp::Noop, LogOp::CompensationLogRecord, LogOp::InitializeFileRecordSegment, LogOp::DeallocateFileRecordSegment, LogOp::WriteEndOfFileRecordSegment, LogOp::CreateAttribute, LogOp::DeleteAttribute, LogOp::UpdateResidentValue, LogOp::UpdateNonResidentValue, LogOp::UpdateMappingPairs, LogOp::DeleteDirtyClusters, LogOp::SetNewAttributeSizes, LogOp::AddIndexEntryRoot, LogOp::DeleteIndexEntryRoot, LogOp::AddIndexEntryAllocation, LogOp::DeleteIndexEntryAllocation, LogOp::WriteEndOfIndexBuffer, LogOp::SetIndexEntryVcnRoot, LogOp::SetIndexEntryVcnAllocation, LogOp::UpdateFileNameRoot, LogOp::UpdateFileNameAllocation, LogOp::SetBitsInNonResidentBitMap, LogOp::ClearBitsInNonResidentBitMap, LogOp::HotFix, LogOp::EndTopLevelAction, LogOp::PrepareTransaction, LogOp::CommitTransaction, LogOp::ForgetTransaction, LogOp::OpenNonResidentAttribute, LogOp::OpenAttributeTableDump, LogOp::AttributeNamesDump, LogOp::DirtyPageTableDump, LogOp::TransactionTableDump, LogOp::UpdateRecordDataRoot, LogOp::UpdateRecordDataAllocation, ];
#[test]
fn logop_from_u16_matches_logfileparser_table() {
for (code, &expected) in LFP_OPS.iter().enumerate() {
assert_eq!(
LogOp::from_u16(code as u16),
expected,
"opcode {code:#04x} must map to `LogFileParser`'s operation"
);
}
}
#[test]
fn logop_unknown_surfaces_the_raw_code() {
assert_eq!(LogOp::from_u16(0x23), LogOp::Unknown(0x23));
assert_eq!(LogOp::from_u16(0xFFFF), LogOp::Unknown(0xFFFF));
}
#[test]
fn logop_code_round_trips() {
for code in 0u16..=0x22 {
assert_eq!(LogOp::from_u16(code).code(), code, "round-trip {code:#04x}");
}
assert_eq!(LogOp::Unknown(0x99).code(), 0x99);
}
fn make_rstr_page(lsn: u64) -> Vec<u8> {
let mut page = vec![0u8; LOG_PAGE_SIZE];
page[0..4].copy_from_slice(RSTR_SIGNATURE);
page[0x08..0x10].copy_from_slice(&lsn.to_le_bytes());
page[0x10..0x12].copy_from_slice(&1u16.to_le_bytes()); page[0x20..0x24].copy_from_slice(&4096u32.to_le_bytes());
page[0x24..0x28].copy_from_slice(&4096u32.to_le_bytes());
page
}
fn make_rcrd_page(lsn: u64) -> Vec<u8> {
let mut page = vec![0u8; LOG_PAGE_SIZE];
page[0..4].copy_from_slice(RCRD_SIGNATURE);
page[0x18..0x20].copy_from_slice(&lsn.to_le_bytes());
page
}
fn make_rcrd_page_with_usa(last_lsn: u64) -> Vec<u8> {
let mut page = vec![0u8; LOG_PAGE_SIZE];
page[0..4].copy_from_slice(RCRD_SIGNATURE);
page[0x04..0x06].copy_from_slice(&0x28u16.to_le_bytes()); page[0x06..0x08].copy_from_slice(&9u16.to_le_bytes()); page[0x08..0x10].copy_from_slice(&last_lsn.to_le_bytes()); let usn: u16 = 0x0007;
page[0x28..0x2a].copy_from_slice(&usn.to_le_bytes()); for i in 0..8usize {
let original: u16 = 0xAA00 | i as u16;
let usa_slot = 0x2a + i * 2;
page[usa_slot..usa_slot + 2].copy_from_slice(&original.to_le_bytes());
let tail = (i + 1) * 512 - 2;
page[tail..tail + 2].copy_from_slice(&usn.to_le_bytes()); }
page
}
#[test]
fn read_record_pages_accepts_valid_usa_page() {
let pages = read_record_pages(&make_rcrd_page_with_usa(0x1234));
assert_eq!(pages.len(), 1);
assert_eq!(pages[0].offset, 0);
assert_eq!(pages[0].last_lsn, 0x1234);
assert_eq!(&pages[0].data[0x1fe..0x200], &0xAA00u16.to_le_bytes());
}
#[test]
fn read_record_pages_skips_non_rcrd_pages() {
let mut data = make_rstr_page(1000);
data.extend_from_slice(&vec![0u8; LOG_PAGE_SIZE]);
assert!(read_record_pages(&data).is_empty());
}
#[test]
fn read_record_pages_skips_page_with_invalid_usa() {
let pages = read_record_pages(&make_rcrd_page(5000));
assert!(pages.is_empty());
}
#[test]
fn read_record_pages_empty_input() {
assert!(read_record_pages(&[]).is_empty());
}
#[test]
fn read_record_pages_returns_only_valid_pages_in_mixed_stream() {
let mut data = make_rstr_page(1000);
data.extend_from_slice(&make_rcrd_page_with_usa(0xBEEF));
data.extend_from_slice(&make_rcrd_page(2000)); data.extend_from_slice(&vec![0u8; LOG_PAGE_SIZE]);
let pages = read_record_pages(&data);
assert_eq!(pages.len(), 1);
assert_eq!(pages[0].offset, LOG_PAGE_SIZE);
assert_eq!(pages[0].last_lsn, 0xBEEF);
}
fn page_with_records(
last_lsn: u64,
first_off: usize,
recs: &[(u64, u32, u32, u16, u16, u32)],
) -> RecordPage {
let mut data = vec![0u8; LOG_PAGE_SIZE];
data[0..4].copy_from_slice(RCRD_SIGNATURE);
data[0x08..0x10].copy_from_slice(&last_lsn.to_le_bytes());
let mut off = first_off;
for &(lsn, rt, txid, redo, undo, cdl) in recs {
data[off..off + 8].copy_from_slice(&lsn.to_le_bytes());
data[off + 0x18..off + 0x1c].copy_from_slice(&cdl.to_le_bytes());
data[off + 0x20..off + 0x24].copy_from_slice(&rt.to_le_bytes());
data[off + 0x24..off + 0x28].copy_from_slice(&txid.to_le_bytes());
data[off + 0x30..off + 0x32].copy_from_slice(&redo.to_le_bytes());
data[off + 0x32..off + 0x34].copy_from_slice(&undo.to_le_bytes());
off += ((0x30 + cdl as usize + 7) & !7).max(8);
}
data[0x18..0x1a].copy_from_slice(&u16::try_from(off).unwrap_or(0).to_le_bytes());
RecordPage {
offset: 0,
last_lsn,
data,
}
}
#[test]
fn parse_log_records_decodes_record_fields() {
let page = page_with_records(5000, 0x40, &[(4200, 1, 7, 0x02, 0x00, 64)]);
let recs = parse_log_records(&page);
assert_eq!(recs.len(), 1);
let r = &recs[0];
assert_eq!(r.page_offset, 0x40);
assert_eq!(r.this_lsn, 4200);
assert_eq!(r.record_type, 1);
assert_eq!(r.transaction_id, 7);
assert_eq!(r.redo_op, LogOp::InitializeFileRecordSegment);
assert_eq!(r.undo_op, LogOp::Noop);
}
#[test]
fn parse_log_records_skips_leading_padding() {
let page = page_with_records(5000, 0x100, &[(4300, 2, 0, 0x01, 0x00, 80)]);
let recs = parse_log_records(&page);
assert_eq!(recs.len(), 1);
assert_eq!(recs[0].page_offset, 0x100);
assert_eq!(recs[0].redo_op, LogOp::CompensationLogRecord);
}
#[test]
fn parse_log_records_walks_multiple() {
let page = page_with_records(
5000,
0x40,
&[(4100, 1, 1, 0x02, 0x00, 32), (4150, 1, 1, 0x13, 0x00, 48)],
);
let recs = parse_log_records(&page);
assert_eq!(recs.len(), 2);
assert_eq!(recs[0].this_lsn, 4100);
assert_eq!(recs[1].this_lsn, 4150);
assert_eq!(recs[1].redo_op, LogOp::UpdateFileNameRoot);
}
#[test]
fn parse_log_records_empty_region() {
assert!(parse_log_records(&page_with_records(5000, 0x40, &[])).is_empty());
}
#[test]
fn test_parse_logfile_with_restart_areas() {
let mut data = Vec::new();
data.extend_from_slice(&make_rstr_page(1000));
data.extend_from_slice(&make_rstr_page(2000));
data.extend_from_slice(&make_rcrd_page(3000));
let summary = parse_logfile(&data).unwrap();
assert_eq!(summary.restart_areas.len(), 2);
assert_eq!(summary.record_page_count, 1);
assert_eq!(summary.highest_lsn, 3000);
assert!(!summary.has_gaps);
}
#[test]
fn test_detect_journal_clearing_with_gaps() {
let summary = LogFileSummary {
restart_areas: vec![],
record_page_count: 0,
has_gaps: true,
highest_lsn: 0,
};
assert!(detect_journal_clearing(&summary));
}
#[test]
fn test_normal_logfile_no_clearing() {
let summary = LogFileSummary {
restart_areas: vec![
RestartArea {
offset: 0,
current_lsn: 1000,
log_clients: 1,
system_page_size: 4096,
log_page_size: 4096,
},
RestartArea {
offset: 4096,
current_lsn: 2000,
log_clients: 1,
system_page_size: 4096,
log_page_size: 4096,
},
],
record_page_count: 100,
has_gaps: false,
highest_lsn: 5000,
};
assert!(!detect_journal_clearing(&summary));
}
#[test]
fn test_detect_journal_clearing_empty_restart_areas() {
let summary = LogFileSummary {
restart_areas: vec![],
record_page_count: 0,
has_gaps: false,
highest_lsn: 0,
};
assert!(detect_journal_clearing(&summary));
}
#[test]
fn test_detect_journal_clearing_one_restart_area() {
let summary = LogFileSummary {
restart_areas: vec![RestartArea {
offset: 0,
current_lsn: 1000,
log_clients: 1,
system_page_size: 4096,
log_page_size: 4096,
}],
record_page_count: 50,
has_gaps: false,
highest_lsn: 5000,
};
assert!(!detect_journal_clearing(&summary));
}
#[test]
fn test_detect_journal_clearing_three_restart_areas() {
let summary = LogFileSummary {
restart_areas: vec![
RestartArea {
offset: 0,
current_lsn: 1000,
log_clients: 1,
system_page_size: 4096,
log_page_size: 4096,
},
RestartArea {
offset: 4096,
current_lsn: 2000,
log_clients: 1,
system_page_size: 4096,
log_page_size: 4096,
},
RestartArea {
offset: 8192,
current_lsn: 3000,
log_clients: 1,
system_page_size: 4096,
log_page_size: 4096,
},
],
record_page_count: 50,
has_gaps: false,
highest_lsn: 5000,
};
assert!(!detect_journal_clearing(&summary));
}
#[test]
fn test_parse_logfile_empty() {
let summary = parse_logfile(&[]).unwrap();
assert_eq!(summary.restart_areas.len(), 0);
assert_eq!(summary.record_page_count, 0);
assert!(!summary.has_gaps);
assert_eq!(summary.highest_lsn, 0);
}
#[test]
fn test_parse_logfile_only_rcrd_pages() {
let mut data = Vec::new();
data.extend_from_slice(&make_rcrd_page(1000));
data.extend_from_slice(&make_rcrd_page(2000));
data.extend_from_slice(&make_rcrd_page(3000));
let summary = parse_logfile(&data).unwrap();
assert_eq!(summary.restart_areas.len(), 0);
assert_eq!(summary.record_page_count, 3);
assert_eq!(summary.highest_lsn, 3000);
}
#[test]
fn test_parse_logfile_gap_detection() {
let mut data = Vec::new();
data.extend_from_slice(&make_rstr_page(1000));
data.extend_from_slice(&make_rstr_page(2000));
data.extend_from_slice(&make_rcrd_page(3000));
let mut garbage_page = vec![0xDEu8; LOG_PAGE_SIZE];
garbage_page[0..4].copy_from_slice(b"JUNK");
data.extend_from_slice(&garbage_page);
data.extend_from_slice(&make_rcrd_page(5000));
let summary = parse_logfile(&data).unwrap();
assert!(summary.has_gaps);
}
#[test]
fn test_parse_logfile_no_gap_for_zeroed_page() {
let mut data = Vec::new();
data.extend_from_slice(&make_rstr_page(1000));
data.extend_from_slice(&make_rstr_page(2000));
data.extend_from_slice(&make_rcrd_page(3000));
data.extend_from_slice(&vec![0u8; LOG_PAGE_SIZE]);
let summary = parse_logfile(&data).unwrap();
assert!(!summary.has_gaps);
}
#[test]
fn test_parse_logfile_restart_area_lsn_tracking() {
let mut data = Vec::new();
data.extend_from_slice(&make_rstr_page(5000));
data.extend_from_slice(&make_rstr_page(3000));
data.extend_from_slice(&make_rcrd_page(4000));
let summary = parse_logfile(&data).unwrap();
assert_eq!(summary.highest_lsn, 5000);
assert_eq!(summary.restart_areas.len(), 2);
assert_eq!(summary.restart_areas[0].current_lsn, 5000);
assert_eq!(summary.restart_areas[1].current_lsn, 3000);
}
#[test]
fn test_parse_logfile_short_rstr_page() {
let mut data = vec![0u8; LOG_PAGE_SIZE];
data[0..4].copy_from_slice(RSTR_SIGNATURE);
let summary = parse_logfile(&data).unwrap();
assert_eq!(summary.restart_areas.len(), 1);
assert_eq!(summary.restart_areas[0].current_lsn, 0);
}
#[test]
fn test_parse_logfile_page_offset_boundary() {
let data = make_rcrd_page(5000);
assert_eq!(data.len(), LOG_PAGE_SIZE);
let summary = parse_logfile(&data).unwrap();
assert_eq!(summary.record_page_count, 1);
assert_eq!(summary.highest_lsn, 5000);
}
#[test]
fn test_parse_logfile_data_smaller_than_page() {
let data = vec![0xAAu8; 100];
let summary = parse_logfile(&data).unwrap();
assert_eq!(summary.restart_areas.len(), 0);
assert_eq!(summary.record_page_count, 0);
}
#[test]
fn test_parse_logfile_boundary_check_line_61() {
let data = vec![0u8; LOG_PAGE_SIZE];
let summary = parse_logfile(&data).unwrap();
assert_eq!(summary.restart_areas.len(), 0);
assert_eq!(summary.record_page_count, 0);
assert!(!summary.has_gaps);
}
#[test]
fn test_parse_logfile_gap_not_flagged_early_pages() {
let mut data = Vec::new();
data.extend_from_slice(&make_rcrd_page(1000)); let mut garbage = vec![0xDEu8; LOG_PAGE_SIZE];
garbage[0..4].copy_from_slice(b"JUNK");
data.extend_from_slice(&garbage);
let summary = parse_logfile(&data).unwrap();
assert!(!summary.has_gaps);
}
#[test]
fn test_parse_logfile_rstr_too_short_for_header() {
let mut data = make_rstr_page(0);
data[0x08..0x10].copy_from_slice(&0u64.to_le_bytes());
let summary = parse_logfile(&data).unwrap();
assert_eq!(summary.restart_areas.len(), 1);
assert_eq!(summary.restart_areas[0].current_lsn, 0);
assert_eq!(summary.highest_lsn, 0);
}
}