1pub mod semantics;
8pub mod transactions;
9pub mod usn_extractor;
10
11pub use semantics::{classify, FileOperation};
12pub use transactions::{reconstruct_transactions, Transaction, TransactionState};
13pub use usn_extractor::{extract_usn_from_logfile, LogFileRecordSource, LogFileUsnRecord};
14
15use crate::error::Result;
16
17const RSTR_SIGNATURE: &[u8; 4] = b"RSTR";
21
22const RCRD_SIGNATURE: &[u8; 4] = b"RCRD";
24
25const LOG_PAGE_SIZE: usize = 0x1000; #[derive(Debug, Clone)]
32pub struct RestartArea {
33 pub offset: usize,
34 pub current_lsn: u64,
35 pub log_clients: u16,
36 pub system_page_size: u32,
37 pub log_page_size: u32,
38}
39
40#[derive(Debug, Clone)]
42pub struct LogFileSummary {
43 pub restart_areas: Vec<RestartArea>,
44 pub record_page_count: usize,
45 pub has_gaps: bool,
46 pub highest_lsn: u64,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum LogOp {
62 Noop,
63 CompensationLogRecord,
64 InitializeFileRecordSegment,
65 DeallocateFileRecordSegment,
66 WriteEndOfFileRecordSegment,
67 CreateAttribute,
68 DeleteAttribute,
69 UpdateResidentValue,
70 UpdateNonResidentValue,
71 UpdateMappingPairs,
72 DeleteDirtyClusters,
73 SetNewAttributeSizes,
74 AddIndexEntryRoot,
75 DeleteIndexEntryRoot,
76 AddIndexEntryAllocation,
77 DeleteIndexEntryAllocation,
78 WriteEndOfIndexBuffer,
79 SetIndexEntryVcnRoot,
80 SetIndexEntryVcnAllocation,
81 UpdateFileNameRoot,
82 UpdateFileNameAllocation,
83 SetBitsInNonResidentBitMap,
84 ClearBitsInNonResidentBitMap,
85 HotFix,
86 EndTopLevelAction,
87 PrepareTransaction,
88 CommitTransaction,
89 ForgetTransaction,
90 OpenNonResidentAttribute,
91 OpenAttributeTableDump,
92 AttributeNamesDump,
93 DirtyPageTableDump,
94 TransactionTableDump,
95 UpdateRecordDataRoot,
96 UpdateRecordDataAllocation,
97 Unknown(u16),
99}
100
101impl LogOp {
102 #[must_use]
104 pub fn from_u16(code: u16) -> Self {
105 use LogOp::{
106 AddIndexEntryAllocation, AddIndexEntryRoot, AttributeNamesDump,
107 ClearBitsInNonResidentBitMap, CommitTransaction, CompensationLogRecord,
108 CreateAttribute, DeallocateFileRecordSegment, DeleteAttribute, DeleteDirtyClusters,
109 DeleteIndexEntryAllocation, DeleteIndexEntryRoot, DirtyPageTableDump,
110 EndTopLevelAction, ForgetTransaction, HotFix, InitializeFileRecordSegment, Noop,
111 OpenAttributeTableDump, OpenNonResidentAttribute, PrepareTransaction,
112 SetBitsInNonResidentBitMap, SetIndexEntryVcnAllocation, SetIndexEntryVcnRoot,
113 SetNewAttributeSizes, TransactionTableDump, Unknown, UpdateFileNameAllocation,
114 UpdateFileNameRoot, UpdateMappingPairs, UpdateNonResidentValue,
115 UpdateRecordDataAllocation, UpdateRecordDataRoot, UpdateResidentValue,
116 WriteEndOfFileRecordSegment, WriteEndOfIndexBuffer,
117 };
118 match code {
119 0x00 => Noop,
120 0x01 => CompensationLogRecord,
121 0x02 => InitializeFileRecordSegment,
122 0x03 => DeallocateFileRecordSegment,
123 0x04 => WriteEndOfFileRecordSegment,
124 0x05 => CreateAttribute,
125 0x06 => DeleteAttribute,
126 0x07 => UpdateResidentValue,
127 0x08 => UpdateNonResidentValue,
128 0x09 => UpdateMappingPairs,
129 0x0A => DeleteDirtyClusters,
130 0x0B => SetNewAttributeSizes,
131 0x0C => AddIndexEntryRoot,
132 0x0D => DeleteIndexEntryRoot,
133 0x0E => AddIndexEntryAllocation,
134 0x0F => DeleteIndexEntryAllocation,
135 0x10 => WriteEndOfIndexBuffer,
136 0x11 => SetIndexEntryVcnRoot,
137 0x12 => SetIndexEntryVcnAllocation,
138 0x13 => UpdateFileNameRoot,
139 0x14 => UpdateFileNameAllocation,
140 0x15 => SetBitsInNonResidentBitMap,
141 0x16 => ClearBitsInNonResidentBitMap,
142 0x17 => HotFix,
143 0x18 => EndTopLevelAction,
144 0x19 => PrepareTransaction,
145 0x1A => CommitTransaction,
146 0x1B => ForgetTransaction,
147 0x1C => OpenNonResidentAttribute,
148 0x1D => OpenAttributeTableDump,
149 0x1E => AttributeNamesDump,
150 0x1F => DirtyPageTableDump,
151 0x20 => TransactionTableDump,
152 0x21 => UpdateRecordDataRoot,
153 0x22 => UpdateRecordDataAllocation,
154 other => Unknown(other),
155 }
156 }
157
158 #[must_use]
160 pub fn code(self) -> u16 {
161 use LogOp::{
162 AddIndexEntryAllocation, AddIndexEntryRoot, AttributeNamesDump,
163 ClearBitsInNonResidentBitMap, CommitTransaction, CompensationLogRecord,
164 CreateAttribute, DeallocateFileRecordSegment, DeleteAttribute, DeleteDirtyClusters,
165 DeleteIndexEntryAllocation, DeleteIndexEntryRoot, DirtyPageTableDump,
166 EndTopLevelAction, ForgetTransaction, HotFix, InitializeFileRecordSegment, Noop,
167 OpenAttributeTableDump, OpenNonResidentAttribute, PrepareTransaction,
168 SetBitsInNonResidentBitMap, SetIndexEntryVcnAllocation, SetIndexEntryVcnRoot,
169 SetNewAttributeSizes, TransactionTableDump, Unknown, UpdateFileNameAllocation,
170 UpdateFileNameRoot, UpdateMappingPairs, UpdateNonResidentValue,
171 UpdateRecordDataAllocation, UpdateRecordDataRoot, UpdateResidentValue,
172 WriteEndOfFileRecordSegment, WriteEndOfIndexBuffer,
173 };
174 match self {
175 Noop => 0x00,
176 CompensationLogRecord => 0x01,
177 InitializeFileRecordSegment => 0x02,
178 DeallocateFileRecordSegment => 0x03,
179 WriteEndOfFileRecordSegment => 0x04,
180 CreateAttribute => 0x05,
181 DeleteAttribute => 0x06,
182 UpdateResidentValue => 0x07,
183 UpdateNonResidentValue => 0x08,
184 UpdateMappingPairs => 0x09,
185 DeleteDirtyClusters => 0x0A,
186 SetNewAttributeSizes => 0x0B,
187 AddIndexEntryRoot => 0x0C,
188 DeleteIndexEntryRoot => 0x0D,
189 AddIndexEntryAllocation => 0x0E,
190 DeleteIndexEntryAllocation => 0x0F,
191 WriteEndOfIndexBuffer => 0x10,
192 SetIndexEntryVcnRoot => 0x11,
193 SetIndexEntryVcnAllocation => 0x12,
194 UpdateFileNameRoot => 0x13,
195 UpdateFileNameAllocation => 0x14,
196 SetBitsInNonResidentBitMap => 0x15,
197 ClearBitsInNonResidentBitMap => 0x16,
198 HotFix => 0x17,
199 EndTopLevelAction => 0x18,
200 PrepareTransaction => 0x19,
201 CommitTransaction => 0x1A,
202 ForgetTransaction => 0x1B,
203 OpenNonResidentAttribute => 0x1C,
204 OpenAttributeTableDump => 0x1D,
205 AttributeNamesDump => 0x1E,
206 DirtyPageTableDump => 0x1F,
207 TransactionTableDump => 0x20,
208 UpdateRecordDataRoot => 0x21,
209 UpdateRecordDataAllocation => 0x22,
210 Unknown(c) => c,
211 }
212 }
213}
214
215#[derive(Debug, Clone)]
223pub struct RecordPage {
224 pub offset: usize,
226 pub last_lsn: u64,
229 pub data: Vec<u8>,
231}
232
233pub fn read_record_pages(data: &[u8]) -> Vec<RecordPage> {
244 let mut pages = Vec::new();
245 let page_count = data.len() / LOG_PAGE_SIZE;
246
247 for page_idx in 0..page_count {
248 let offset = page_idx * LOG_PAGE_SIZE;
249
250 let page = &data[offset..offset + LOG_PAGE_SIZE];
252 if &page[0..4] != RCRD_SIGNATURE {
253 continue;
254 }
255
256 let mut buf = page.to_vec();
263 if crate::record::apply_fixup(&mut buf, 512).is_err() {
264 continue;
265 }
266
267 let last_lsn = u64::from_le_bytes(buf[0x08..0x10].try_into().unwrap_or([0; 8]));
268
269 pages.push(RecordPage {
270 offset,
271 last_lsn,
272 data: buf,
273 });
274 }
275
276 pages
277}
278
279#[derive(Debug, Clone)]
286pub struct LogRecord {
287 pub page_offset: usize,
289 pub this_lsn: u64,
291 pub client_previous_lsn: u64,
293 pub client_undo_next_lsn: u64,
295 pub record_type: u32,
297 pub transaction_id: u32,
299 pub redo_op: LogOp,
301 pub undo_op: LogOp,
303 pub target_attribute: u16,
305 pub mft_cluster_index: u16,
307 pub target_vcn: u64,
309}
310
311pub fn parse_log_records(page: &RecordPage) -> Vec<LogRecord> {
317 use crate::bytes::{le_u16, le_u32, le_u64};
318
319 const LFS_HEADER_LEN: usize = 0x30;
321
322 let data = &page.data;
323 let end = (le_u16(data, 0x18) as usize).min(data.len());
326 let page_last_lsn = le_u64(data, 0x08);
330
331 let mut records = Vec::new();
332 let mut off = LFS_HEADER_LEN.max(0x40); while off + LFS_HEADER_LEN <= end {
334 let this_lsn = le_u64(data, off);
335 let record_type = le_u32(data, off + 0x20);
336 let redo_raw = le_u16(data, off + 0x30);
337 let client_data_length = le_u32(data, off + 0x18) as usize;
338
339 let plausible = this_lsn != 0
342 && this_lsn <= page_last_lsn
343 && (record_type == 1 || record_type == 2)
344 && redo_raw <= 0x22
345 && off + LFS_HEADER_LEN + client_data_length <= data.len();
346 if !plausible {
347 off += 8; continue;
349 }
350
351 records.push(LogRecord {
352 page_offset: off,
353 this_lsn,
354 client_previous_lsn: le_u64(data, off + 0x08),
355 client_undo_next_lsn: le_u64(data, off + 0x10),
356 record_type,
357 transaction_id: le_u32(data, off + 0x24),
358 redo_op: LogOp::from_u16(redo_raw),
359 undo_op: LogOp::from_u16(le_u16(data, off + 0x32)),
360 target_attribute: le_u16(data, off + 0x3C),
361 mft_cluster_index: le_u16(data, off + 0x44),
362 target_vcn: le_u64(data, off + 0x48),
363 });
364
365 let advance = (LFS_HEADER_LEN + client_data_length + 7) & !7;
367 off += advance.max(8);
368 }
369
370 records
371}
372
373pub fn parse_logfile(data: &[u8]) -> Result<LogFileSummary> {
378 let mut restart_areas = Vec::new();
379 let mut record_page_count = 0;
380 let mut highest_lsn: u64 = 0;
381 let mut has_gaps = false;
382 let mut last_page_had_rcrd = false;
383
384 let page_count = data.len() / LOG_PAGE_SIZE;
385
386 for page_idx in 0..page_count {
387 let page_offset = page_idx * LOG_PAGE_SIZE;
388
389 let sig = &data[page_offset..page_offset + 4];
391
392 if sig == RSTR_SIGNATURE {
393 if page_offset + 0x28 <= data.len() {
394 let current_lsn = u64::from_le_bytes(
395 data[page_offset + 0x08..page_offset + 0x10]
396 .try_into()
397 .unwrap_or([0; 8]),
398 );
399 let log_clients = u16::from_le_bytes(
400 data[page_offset + 0x10..page_offset + 0x12]
401 .try_into()
402 .unwrap_or([0; 2]),
403 );
404 let system_page_size = u32::from_le_bytes(
405 data[page_offset + 0x20..page_offset + 0x24]
406 .try_into()
407 .unwrap_or([0; 4]),
408 );
409 let log_page_size = u32::from_le_bytes(
410 data[page_offset + 0x24..page_offset + 0x28]
411 .try_into()
412 .unwrap_or([0; 4]),
413 );
414
415 if current_lsn > highest_lsn {
416 highest_lsn = current_lsn;
417 }
418
419 restart_areas.push(RestartArea {
420 offset: page_offset,
421 current_lsn,
422 log_clients,
423 system_page_size,
424 log_page_size,
425 });
426 } last_page_had_rcrd = false;
428 } else if sig == RCRD_SIGNATURE {
429 record_page_count += 1;
430
431 if page_offset + 0x20 <= data.len() {
433 let page_lsn = u64::from_le_bytes(
434 data[page_offset + 0x18..page_offset + 0x20]
435 .try_into()
436 .unwrap_or([0; 8]),
437 );
438 if page_lsn > highest_lsn {
439 highest_lsn = page_lsn;
440 }
441 } last_page_had_rcrd = true;
444 } else {
445 if last_page_had_rcrd && page_idx > 2 {
447 let is_zeroed = data[page_offset..page_offset + 4] == [0, 0, 0, 0];
449 if !is_zeroed {
450 has_gaps = true;
451 }
452 }
453 last_page_had_rcrd = false;
454 }
455 }
456
457 Ok(LogFileSummary {
458 restart_areas,
459 record_page_count,
460 has_gaps,
461 highest_lsn,
462 })
463}
464
465pub fn detect_journal_clearing(logfile_summary: &LogFileSummary) -> bool {
471 if logfile_summary.has_gaps {
477 return true;
478 }
479
480 if logfile_summary.restart_areas.len() != 2 {
481 return logfile_summary.restart_areas.is_empty();
482 }
483
484 false
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490
491 const LFP_OPS: [LogOp; 35] = [
498 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, ];
534
535 #[test]
536 fn logop_from_u16_matches_logfileparser_table() {
537 for (code, &expected) in LFP_OPS.iter().enumerate() {
538 assert_eq!(
539 LogOp::from_u16(code as u16),
540 expected,
541 "opcode {code:#04x} must map to `LogFileParser`'s operation"
542 );
543 }
544 }
545
546 #[test]
547 fn logop_unknown_surfaces_the_raw_code() {
548 assert_eq!(LogOp::from_u16(0x23), LogOp::Unknown(0x23));
551 assert_eq!(LogOp::from_u16(0xFFFF), LogOp::Unknown(0xFFFF));
552 }
553
554 #[test]
555 fn logop_code_round_trips() {
556 for code in 0u16..=0x22 {
557 assert_eq!(LogOp::from_u16(code).code(), code, "round-trip {code:#04x}");
558 }
559 assert_eq!(LogOp::Unknown(0x99).code(), 0x99);
560 }
561
562 fn make_rstr_page(lsn: u64) -> Vec<u8> {
563 let mut page = vec![0u8; LOG_PAGE_SIZE];
564 page[0..4].copy_from_slice(RSTR_SIGNATURE);
565 page[0x08..0x10].copy_from_slice(&lsn.to_le_bytes());
566 page[0x10..0x12].copy_from_slice(&1u16.to_le_bytes()); page[0x20..0x24].copy_from_slice(&4096u32.to_le_bytes());
568 page[0x24..0x28].copy_from_slice(&4096u32.to_le_bytes());
569 page
570 }
571
572 fn make_rcrd_page(lsn: u64) -> Vec<u8> {
573 let mut page = vec![0u8; LOG_PAGE_SIZE];
574 page[0..4].copy_from_slice(RCRD_SIGNATURE);
575 page[0x18..0x20].copy_from_slice(&lsn.to_le_bytes());
576 page
577 }
578
579 fn make_rcrd_page_with_usa(last_lsn: u64) -> Vec<u8> {
586 let mut page = vec![0u8; LOG_PAGE_SIZE];
587 page[0..4].copy_from_slice(RCRD_SIGNATURE);
588 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;
592 page[0x28..0x2a].copy_from_slice(&usn.to_le_bytes()); for i in 0..8usize {
594 let original: u16 = 0xAA00 | i as u16;
595 let usa_slot = 0x2a + i * 2;
596 page[usa_slot..usa_slot + 2].copy_from_slice(&original.to_le_bytes());
597 let tail = (i + 1) * 512 - 2;
598 page[tail..tail + 2].copy_from_slice(&usn.to_le_bytes()); }
600 page
601 }
602
603 #[test]
604 fn read_record_pages_accepts_valid_usa_page() {
605 let pages = read_record_pages(&make_rcrd_page_with_usa(0x1234));
606 assert_eq!(pages.len(), 1);
607 assert_eq!(pages[0].offset, 0);
608 assert_eq!(pages[0].last_lsn, 0x1234);
609 assert_eq!(&pages[0].data[0x1fe..0x200], &0xAA00u16.to_le_bytes());
611 }
612
613 #[test]
614 fn read_record_pages_skips_non_rcrd_pages() {
615 let mut data = make_rstr_page(1000);
617 data.extend_from_slice(&vec![0u8; LOG_PAGE_SIZE]);
618 assert!(read_record_pages(&data).is_empty());
619 }
620
621 #[test]
622 fn read_record_pages_skips_page_with_invalid_usa() {
623 let pages = read_record_pages(&make_rcrd_page(5000));
625 assert!(pages.is_empty());
626 }
627
628 #[test]
629 fn read_record_pages_empty_input() {
630 assert!(read_record_pages(&[]).is_empty());
631 }
632
633 #[test]
634 fn read_record_pages_returns_only_valid_pages_in_mixed_stream() {
635 let mut data = make_rstr_page(1000);
637 data.extend_from_slice(&make_rcrd_page_with_usa(0xBEEF));
638 data.extend_from_slice(&make_rcrd_page(2000)); data.extend_from_slice(&vec![0u8; LOG_PAGE_SIZE]);
640 let pages = read_record_pages(&data);
641 assert_eq!(pages.len(), 1);
642 assert_eq!(pages[0].offset, LOG_PAGE_SIZE);
643 assert_eq!(pages[0].last_lsn, 0xBEEF);
644 }
645
646 fn page_with_records(
652 last_lsn: u64,
653 first_off: usize,
654 recs: &[(u64, u32, u32, u16, u16, u32)],
655 ) -> RecordPage {
656 let mut data = vec![0u8; LOG_PAGE_SIZE];
657 data[0..4].copy_from_slice(RCRD_SIGNATURE);
658 data[0x08..0x10].copy_from_slice(&last_lsn.to_le_bytes());
659 let mut off = first_off;
660 for &(lsn, rt, txid, redo, undo, cdl) in recs {
661 data[off..off + 8].copy_from_slice(&lsn.to_le_bytes());
662 data[off + 0x18..off + 0x1c].copy_from_slice(&cdl.to_le_bytes());
663 data[off + 0x20..off + 0x24].copy_from_slice(&rt.to_le_bytes());
664 data[off + 0x24..off + 0x28].copy_from_slice(&txid.to_le_bytes());
665 data[off + 0x30..off + 0x32].copy_from_slice(&redo.to_le_bytes());
666 data[off + 0x32..off + 0x34].copy_from_slice(&undo.to_le_bytes());
667 off += ((0x30 + cdl as usize + 7) & !7).max(8);
668 }
669 data[0x18..0x1a].copy_from_slice(&u16::try_from(off).unwrap_or(0).to_le_bytes());
670 RecordPage {
671 offset: 0,
672 last_lsn,
673 data,
674 }
675 }
676
677 #[test]
678 fn parse_log_records_decodes_record_fields() {
679 let page = page_with_records(5000, 0x40, &[(4200, 1, 7, 0x02, 0x00, 64)]);
680 let recs = parse_log_records(&page);
681 assert_eq!(recs.len(), 1);
682 let r = &recs[0];
683 assert_eq!(r.page_offset, 0x40);
684 assert_eq!(r.this_lsn, 4200);
685 assert_eq!(r.record_type, 1);
686 assert_eq!(r.transaction_id, 7);
687 assert_eq!(r.redo_op, LogOp::InitializeFileRecordSegment);
688 assert_eq!(r.undo_op, LogOp::Noop);
689 }
690
691 #[test]
692 fn parse_log_records_skips_leading_padding() {
693 let page = page_with_records(5000, 0x100, &[(4300, 2, 0, 0x01, 0x00, 80)]);
696 let recs = parse_log_records(&page);
697 assert_eq!(recs.len(), 1);
698 assert_eq!(recs[0].page_offset, 0x100);
699 assert_eq!(recs[0].redo_op, LogOp::CompensationLogRecord);
700 }
701
702 #[test]
703 fn parse_log_records_walks_multiple() {
704 let page = page_with_records(
705 5000,
706 0x40,
707 &[(4100, 1, 1, 0x02, 0x00, 32), (4150, 1, 1, 0x13, 0x00, 48)],
708 );
709 let recs = parse_log_records(&page);
710 assert_eq!(recs.len(), 2);
711 assert_eq!(recs[0].this_lsn, 4100);
712 assert_eq!(recs[1].this_lsn, 4150);
713 assert_eq!(recs[1].redo_op, LogOp::UpdateFileNameRoot);
714 }
715
716 #[test]
717 fn parse_log_records_empty_region() {
718 assert!(parse_log_records(&page_with_records(5000, 0x40, &[])).is_empty());
720 }
721
722 #[test]
723 fn test_parse_logfile_with_restart_areas() {
724 let mut data = Vec::new();
725 data.extend_from_slice(&make_rstr_page(1000));
726 data.extend_from_slice(&make_rstr_page(2000));
727 data.extend_from_slice(&make_rcrd_page(3000));
728
729 let summary = parse_logfile(&data).unwrap();
730 assert_eq!(summary.restart_areas.len(), 2);
731 assert_eq!(summary.record_page_count, 1);
732 assert_eq!(summary.highest_lsn, 3000);
733 assert!(!summary.has_gaps);
734 }
735
736 #[test]
737 fn test_detect_journal_clearing_with_gaps() {
738 let summary = LogFileSummary {
739 restart_areas: vec![],
740 record_page_count: 0,
741 has_gaps: true,
742 highest_lsn: 0,
743 };
744 assert!(detect_journal_clearing(&summary));
745 }
746
747 #[test]
748 fn test_normal_logfile_no_clearing() {
749 let summary = LogFileSummary {
750 restart_areas: vec![
751 RestartArea {
752 offset: 0,
753 current_lsn: 1000,
754 log_clients: 1,
755 system_page_size: 4096,
756 log_page_size: 4096,
757 },
758 RestartArea {
759 offset: 4096,
760 current_lsn: 2000,
761 log_clients: 1,
762 system_page_size: 4096,
763 log_page_size: 4096,
764 },
765 ],
766 record_page_count: 100,
767 has_gaps: false,
768 highest_lsn: 5000,
769 };
770 assert!(!detect_journal_clearing(&summary));
771 }
772
773 #[test]
774 fn test_detect_journal_clearing_empty_restart_areas() {
775 let summary = LogFileSummary {
776 restart_areas: vec![],
777 record_page_count: 0,
778 has_gaps: false,
779 highest_lsn: 0,
780 };
781 assert!(detect_journal_clearing(&summary));
782 }
783
784 #[test]
785 fn test_detect_journal_clearing_one_restart_area() {
786 let summary = LogFileSummary {
788 restart_areas: vec![RestartArea {
789 offset: 0,
790 current_lsn: 1000,
791 log_clients: 1,
792 system_page_size: 4096,
793 log_page_size: 4096,
794 }],
795 record_page_count: 50,
796 has_gaps: false,
797 highest_lsn: 5000,
798 };
799 assert!(!detect_journal_clearing(&summary));
800 }
801
802 #[test]
803 fn test_detect_journal_clearing_three_restart_areas() {
804 let summary = LogFileSummary {
806 restart_areas: vec![
807 RestartArea {
808 offset: 0,
809 current_lsn: 1000,
810 log_clients: 1,
811 system_page_size: 4096,
812 log_page_size: 4096,
813 },
814 RestartArea {
815 offset: 4096,
816 current_lsn: 2000,
817 log_clients: 1,
818 system_page_size: 4096,
819 log_page_size: 4096,
820 },
821 RestartArea {
822 offset: 8192,
823 current_lsn: 3000,
824 log_clients: 1,
825 system_page_size: 4096,
826 log_page_size: 4096,
827 },
828 ],
829 record_page_count: 50,
830 has_gaps: false,
831 highest_lsn: 5000,
832 };
833 assert!(!detect_journal_clearing(&summary));
834 }
835
836 #[test]
837 fn test_parse_logfile_empty() {
838 let summary = parse_logfile(&[]).unwrap();
839 assert_eq!(summary.restart_areas.len(), 0);
840 assert_eq!(summary.record_page_count, 0);
841 assert!(!summary.has_gaps);
842 assert_eq!(summary.highest_lsn, 0);
843 }
844
845 #[test]
846 fn test_parse_logfile_only_rcrd_pages() {
847 let mut data = Vec::new();
848 data.extend_from_slice(&make_rcrd_page(1000));
849 data.extend_from_slice(&make_rcrd_page(2000));
850 data.extend_from_slice(&make_rcrd_page(3000));
851
852 let summary = parse_logfile(&data).unwrap();
853 assert_eq!(summary.restart_areas.len(), 0);
854 assert_eq!(summary.record_page_count, 3);
855 assert_eq!(summary.highest_lsn, 3000);
856 }
857
858 #[test]
859 fn test_parse_logfile_gap_detection() {
860 let mut data = Vec::new();
863 data.extend_from_slice(&make_rstr_page(1000));
864 data.extend_from_slice(&make_rstr_page(2000));
865 data.extend_from_slice(&make_rcrd_page(3000));
866
867 let mut garbage_page = vec![0xDEu8; LOG_PAGE_SIZE];
869 garbage_page[0..4].copy_from_slice(b"JUNK");
870 data.extend_from_slice(&garbage_page);
871
872 data.extend_from_slice(&make_rcrd_page(5000));
873
874 let summary = parse_logfile(&data).unwrap();
875 assert!(summary.has_gaps);
876 }
877
878 #[test]
879 fn test_parse_logfile_no_gap_for_zeroed_page() {
880 let mut data = Vec::new();
882 data.extend_from_slice(&make_rstr_page(1000));
883 data.extend_from_slice(&make_rstr_page(2000));
884 data.extend_from_slice(&make_rcrd_page(3000));
885 data.extend_from_slice(&vec![0u8; LOG_PAGE_SIZE]); let summary = parse_logfile(&data).unwrap();
888 assert!(!summary.has_gaps);
889 }
890
891 #[test]
892 fn test_parse_logfile_restart_area_lsn_tracking() {
893 let mut data = Vec::new();
894 data.extend_from_slice(&make_rstr_page(5000));
895 data.extend_from_slice(&make_rstr_page(3000));
896 data.extend_from_slice(&make_rcrd_page(4000));
897
898 let summary = parse_logfile(&data).unwrap();
899 assert_eq!(summary.highest_lsn, 5000);
900 assert_eq!(summary.restart_areas.len(), 2);
901 assert_eq!(summary.restart_areas[0].current_lsn, 5000);
902 assert_eq!(summary.restart_areas[1].current_lsn, 3000);
903 }
904
905 #[test]
906 fn test_parse_logfile_short_rstr_page() {
907 let mut data = vec![0u8; LOG_PAGE_SIZE];
909 data[0..4].copy_from_slice(RSTR_SIGNATURE);
910 let summary = parse_logfile(&data).unwrap();
915 assert_eq!(summary.restart_areas.len(), 1);
916 assert_eq!(summary.restart_areas[0].current_lsn, 0);
917 }
918
919 #[test]
920 fn test_parse_logfile_page_offset_boundary() {
921 let data = make_rcrd_page(5000);
931 assert_eq!(data.len(), LOG_PAGE_SIZE);
932 let summary = parse_logfile(&data).unwrap();
933 assert_eq!(summary.record_page_count, 1);
934 assert_eq!(summary.highest_lsn, 5000);
935 }
936
937 #[test]
938 fn test_parse_logfile_data_smaller_than_page() {
939 let data = vec![0xAAu8; 100];
941 let summary = parse_logfile(&data).unwrap();
942 assert_eq!(summary.restart_areas.len(), 0);
943 assert_eq!(summary.record_page_count, 0);
944 }
945
946 #[test]
947 fn test_parse_logfile_boundary_check_line_61() {
948 let data = vec![0u8; LOG_PAGE_SIZE];
955 let summary = parse_logfile(&data).unwrap();
956 assert_eq!(summary.restart_areas.len(), 0);
958 assert_eq!(summary.record_page_count, 0);
959 assert!(!summary.has_gaps);
960 }
961
962 #[test]
963 fn test_parse_logfile_gap_not_flagged_early_pages() {
964 let mut data = Vec::new();
968 data.extend_from_slice(&make_rcrd_page(1000)); let mut garbage = vec![0xDEu8; LOG_PAGE_SIZE];
970 garbage[0..4].copy_from_slice(b"JUNK");
971 data.extend_from_slice(&garbage); let summary = parse_logfile(&data).unwrap();
974 assert!(!summary.has_gaps);
975 }
976
977 #[test]
978 fn test_parse_logfile_rstr_too_short_for_header() {
979 let mut data = make_rstr_page(0);
984 data[0x08..0x10].copy_from_slice(&0u64.to_le_bytes());
986
987 let summary = parse_logfile(&data).unwrap();
988 assert_eq!(summary.restart_areas.len(), 1);
989 assert_eq!(summary.restart_areas[0].current_lsn, 0);
990 assert_eq!(summary.highest_lsn, 0);
991 }
992}