1use crate::usn::{parse_usn_record_v2, UsnRecord};
11use safe_read::{le_u16, le_u32, le_u64};
12
13const RCRD_SIGNATURE: &[u8; 4] = b"RCRD";
17
18const LOG_PAGE_SIZE: usize = 0x1000; const RCRD_DATA_OFFSET: usize = 0x40;
23
24const LOG_RECORD_HEADER_MIN: usize = 0x40;
26
27const USN_V2_MIN_SIZE: usize = 0x3C;
29
30const USN_MAX_RECORD_SIZE: usize = 65536;
32
33#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum LogFileRecordSource {
38 RedoData,
40 UndoData,
42 PageSlack,
44}
45
46#[derive(Debug, Clone)]
48pub struct LogFileUsnRecord {
49 pub lsn: u64,
51 pub page_offset: usize,
53 pub source: LogFileRecordSource,
55 pub record: UsnRecord,
57}
58
59fn try_parse_usn_at(data: &[u8], offset: usize) -> Option<UsnRecord> {
66 if offset + USN_V2_MIN_SIZE > data.len() {
67 return None;
68 }
69
70 let slice = &data[offset..];
71
72 if slice.len() < 8 {
74 return None; }
76
77 let record_len = u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]) as usize;
78
79 if !(USN_V2_MIN_SIZE..=USN_MAX_RECORD_SIZE).contains(&record_len) {
81 return None;
82 }
83 if record_len > slice.len() {
84 return None;
85 }
86
87 let major_version = u16::from_le_bytes([slice[4], slice[5]]);
89 if major_version != 2 {
90 return None;
91 }
92
93 parse_usn_record_v2(&slice[..record_len]).ok()
95}
96
97fn scan_for_usn_records(data: &[u8]) -> Vec<(usize, UsnRecord)> {
99 let mut results = Vec::new();
100 let mut offset = 0;
101
102 while offset + USN_V2_MIN_SIZE <= data.len() {
103 if let Some(record) = try_parse_usn_at(data, offset) {
104 let record_len = u32::from_le_bytes([
105 data[offset],
106 data[offset + 1],
107 data[offset + 2],
108 data[offset + 3],
109 ]) as usize;
110 results.push((offset, record));
111 let aligned = (record_len + 7) & !7;
113 offset += aligned;
114 } else {
115 offset += 8;
117 }
118 }
119
120 results
121}
122
123fn extract_from_rcrd_page(page_data: &[u8], page_offset: usize) -> Vec<LogFileUsnRecord> {
126 let mut results = Vec::new();
127
128 if page_data.len() < RCRD_DATA_OFFSET {
129 return results;
130 }
131
132 let page_lsn = if page_data.len() >= 0x20 {
135 le_u64(page_data, 0x18)
136 } else {
137 0 };
139
140 let data_area = &page_data[RCRD_DATA_OFFSET..];
142 let mut record_offset = 0;
143
144 while record_offset + LOG_RECORD_HEADER_MIN <= data_area.len() {
145 if record_offset + 8 <= data_area.len()
147 && data_area[record_offset..record_offset + 8] == [0, 0, 0, 0, 0, 0, 0, 0]
148 {
149 break;
151 }
152
153 let this_lsn = le_u64(data_area, record_offset);
155 let client_data_length = le_u32(data_area, record_offset + 0x18) as usize;
156 let _redo_op = le_u16(data_area, record_offset + 0x30);
157 let _undo_op = le_u16(data_area, record_offset + 0x32);
158 let redo_offset = le_u16(data_area, record_offset + 0x34) as usize;
159 let redo_length = le_u16(data_area, record_offset + 0x36) as usize;
160 let undo_offset = le_u16(data_area, record_offset + 0x38) as usize;
161 let undo_length = le_u16(data_area, record_offset + 0x3A) as usize;
162
163 let redo_base = record_offset + 0x30;
165 let undo_base = record_offset + 0x30;
166
167 let lsn = if this_lsn > 0 { this_lsn } else { page_lsn };
169
170 if redo_length >= USN_V2_MIN_SIZE && redo_offset > 0 {
172 let redo_start = redo_base + redo_offset;
173 if redo_start + redo_length <= data_area.len() {
174 let redo_data = &data_area[redo_start..redo_start + redo_length];
175 for (_off, record) in scan_for_usn_records(redo_data) {
176 results.push(LogFileUsnRecord {
177 lsn,
178 page_offset: page_offset + RCRD_DATA_OFFSET + redo_start,
179 source: LogFileRecordSource::RedoData,
180 record,
181 });
182 }
183 }
184 }
185
186 if undo_length >= USN_V2_MIN_SIZE && undo_offset > 0 {
188 let undo_start = undo_base + undo_offset;
189 let redo_start = redo_base + redo_offset;
191 let same_region = undo_start == redo_start && undo_length == redo_length;
192 if !same_region && undo_start + undo_length <= data_area.len() {
193 let undo_data = &data_area[undo_start..undo_start + undo_length];
194 for (_off, record) in scan_for_usn_records(undo_data) {
195 results.push(LogFileUsnRecord {
196 lsn,
197 page_offset: page_offset + RCRD_DATA_OFFSET + undo_start,
198 source: LogFileRecordSource::UndoData,
199 record,
200 });
201 }
202 }
203 }
204
205 let log_record_size = 0x30 + client_data_length;
209 if log_record_size == 0x30 && client_data_length == 0 {
210 record_offset += 8;
212 } else {
213 let aligned_size = (log_record_size + 7) & !7;
214 if aligned_size == 0 {
215 break; }
217 record_offset += aligned_size;
218 }
219
220 if record_offset > data_area.len() {
222 break;
223 }
224 }
225
226 let slack_start = RCRD_DATA_OFFSET + record_offset;
228 if slack_start < page_data.len() {
229 let slack_data = &page_data[slack_start..];
230 for (_off, record) in scan_for_usn_records(slack_data) {
231 results.push(LogFileUsnRecord {
232 lsn: page_lsn,
233 page_offset: page_offset + slack_start,
234 source: LogFileRecordSource::PageSlack,
235 record,
236 });
237 }
238 }
239
240 results
241}
242
243pub fn extract_usn_from_logfile(logfile_data: &[u8]) -> Vec<LogFileUsnRecord> {
256 let mut results = Vec::new();
257 let page_count = logfile_data.len() / LOG_PAGE_SIZE;
258
259 for page_idx in 0..page_count {
260 let page_offset = page_idx * LOG_PAGE_SIZE;
261
262 if page_offset + 4 > logfile_data.len() {
264 break; }
266 let sig = &logfile_data[page_offset..page_offset + 4];
267 if sig != RCRD_SIGNATURE {
268 continue;
269 }
270
271 let page_end = (page_offset + LOG_PAGE_SIZE).min(logfile_data.len());
272 let page_data = &logfile_data[page_offset..page_end];
273
274 let page_results = extract_from_rcrd_page(page_data, page_offset);
275 results.extend(page_results);
276 }
277
278 results
279}
280
281#[cfg(test)]
284#[allow(clippy::unreadable_literal, clippy::cast_lossless)]
285mod tests {
286 use super::*;
287
288 fn build_v2_record_bytes(
290 entry: u64,
291 seq: u16,
292 parent_entry: u64,
293 parent_seq: u16,
294 reason: u32,
295 filename: &str,
296 ) -> Vec<u8> {
297 let name_utf16: Vec<u16> = filename.encode_utf16().collect();
298 let name_bytes_len = name_utf16.len() * 2;
299 let record_len = 0x3C + name_bytes_len;
300 let aligned_len = (record_len + 7) & !7;
301 let mut buf = vec![0u8; aligned_len];
302
303 buf[0..4].copy_from_slice(&(record_len as u32).to_le_bytes());
305 buf[4..6].copy_from_slice(&2u16.to_le_bytes());
307 buf[6..8].copy_from_slice(&0u16.to_le_bytes());
309 let file_ref = entry | ((seq as u64) << 48);
311 buf[0x08..0x10].copy_from_slice(&file_ref.to_le_bytes());
312 let parent_ref = parent_entry | ((parent_seq as u64) << 48);
314 buf[0x10..0x18].copy_from_slice(&parent_ref.to_le_bytes());
315 buf[0x18..0x20].copy_from_slice(&100i64.to_le_bytes());
317 let ts: i64 = 133500480000000000;
319 buf[0x20..0x28].copy_from_slice(&ts.to_le_bytes());
320 buf[0x28..0x2C].copy_from_slice(&reason.to_le_bytes());
322 buf[0x2C..0x30].copy_from_slice(&0u32.to_le_bytes());
324 buf[0x30..0x34].copy_from_slice(&0u32.to_le_bytes());
326 buf[0x34..0x38].copy_from_slice(&0x20u32.to_le_bytes());
328 buf[0x38..0x3A].copy_from_slice(&(name_bytes_len as u16).to_le_bytes());
330 buf[0x3A..0x3C].copy_from_slice(&0x3Cu16.to_le_bytes());
332 for (i, &ch) in name_utf16.iter().enumerate() {
334 let off = 0x3C + i * 2;
335 buf[off..off + 2].copy_from_slice(&ch.to_le_bytes());
336 }
337
338 buf
339 }
340
341 fn build_rcrd_page_with_usn_in_redo(usn_data: &[u8], page_lsn: u64) -> Vec<u8> {
343 let mut page = vec![0u8; LOG_PAGE_SIZE];
344
345 page[0..4].copy_from_slice(RCRD_SIGNATURE);
347 page[0x18..0x20].copy_from_slice(&page_lsn.to_le_bytes());
349
350 let data_offset = RCRD_DATA_OFFSET;
352
353 let this_lsn: u64 = 42000;
355 page[data_offset..data_offset + 8].copy_from_slice(&this_lsn.to_le_bytes());
356
357 let client_data_length = usn_data.len() as u32;
359 page[data_offset + 0x18..data_offset + 0x1C]
360 .copy_from_slice(&client_data_length.to_le_bytes());
361
362 let redo_offset: u16 = 0x10; page[data_offset + 0x34..data_offset + 0x36].copy_from_slice(&redo_offset.to_le_bytes());
365
366 let redo_length = usn_data.len() as u16;
368 page[data_offset + 0x36..data_offset + 0x38].copy_from_slice(&redo_length.to_le_bytes());
369
370 let redo_start = data_offset + 0x30 + redo_offset as usize;
373 if redo_start + usn_data.len() <= page.len() {
374 page[redo_start..redo_start + usn_data.len()].copy_from_slice(usn_data);
375 }
376
377 page
378 }
379
380 fn build_rcrd_page_with_usn_in_slack(usn_data: &[u8], page_lsn: u64) -> Vec<u8> {
382 let mut page = vec![0u8; LOG_PAGE_SIZE];
383
384 page[0..4].copy_from_slice(RCRD_SIGNATURE);
386 page[0x18..0x20].copy_from_slice(&page_lsn.to_le_bytes());
388
389 let slack_pos = LOG_PAGE_SIZE - usn_data.len() - 8; let slack_pos = slack_pos & !7;
396 if slack_pos >= RCRD_DATA_OFFSET && slack_pos + usn_data.len() <= page.len() {
397 page[slack_pos..slack_pos + usn_data.len()].copy_from_slice(usn_data);
398 }
399
400 page
401 }
402
403 #[test]
404 fn test_extract_empty_logfile() {
405 let results = extract_usn_from_logfile(&[]);
406 assert!(results.is_empty());
407 }
408
409 #[test]
410 fn test_extract_non_rcrd_pages() {
411 let data = vec![0u8; LOG_PAGE_SIZE * 4];
413 let results = extract_usn_from_logfile(&data);
414 assert!(results.is_empty());
415 }
416
417 #[test]
418 fn test_extract_usn_from_redo_data() {
419 let usn_bytes = build_v2_record_bytes(100, 3, 5, 5, 0x100, "secret.txt");
420 let page = build_rcrd_page_with_usn_in_redo(&usn_bytes, 50000);
421
422 let results = extract_usn_from_logfile(&page);
423 assert!(!results.is_empty());
424
425 let found = &results[0];
426 assert_eq!(found.source, LogFileRecordSource::RedoData);
427 assert_eq!(found.record.mft_entry, 100);
428 assert_eq!(found.record.mft_sequence, 3);
429 assert_eq!(found.record.filename, "secret.txt");
430 assert_eq!(found.lsn, 42000); }
432
433 #[test]
434 fn test_extract_usn_from_page_slack() {
435 let usn_bytes = build_v2_record_bytes(200, 1, 50, 1, 0x200, "deleted.doc");
436 let page = build_rcrd_page_with_usn_in_slack(&usn_bytes, 60000);
437
438 let results = extract_usn_from_logfile(&page);
439 assert!(!results.is_empty());
440
441 let found = results
442 .iter()
443 .find(|r| r.source == LogFileRecordSource::PageSlack);
444 assert!(found.is_some());
445 let found = found.unwrap();
446 assert_eq!(found.record.mft_entry, 200);
447 assert_eq!(found.record.filename, "deleted.doc");
448 assert_eq!(found.lsn, 60000); }
450
451 #[test]
452 fn test_extract_multiple_pages() {
453 let usn1 = build_v2_record_bytes(100, 1, 5, 5, 0x100, "file1.txt");
454 let usn2 = build_v2_record_bytes(200, 1, 5, 5, 0x200, "file2.txt");
455
456 let page1 = build_rcrd_page_with_usn_in_redo(&usn1, 10000);
457 let page2 = build_rcrd_page_with_usn_in_redo(&usn2, 20000);
458
459 let mut logfile_data = Vec::new();
460 logfile_data.extend_from_slice(&page1);
461 logfile_data.extend_from_slice(&page2);
462
463 let results = extract_usn_from_logfile(&logfile_data);
464 assert!(results.len() >= 2);
465
466 let filenames: Vec<&str> = results.iter().map(|r| r.record.filename.as_str()).collect();
467 assert!(filenames.contains(&"file1.txt"));
468 assert!(filenames.contains(&"file2.txt"));
469 }
470
471 #[test]
472 fn test_extract_preserves_usn_record_fields() {
473 let usn_bytes = build_v2_record_bytes(42, 7, 30, 2, 0x0000_0800, "secure.pdf");
474 let page = build_rcrd_page_with_usn_in_redo(&usn_bytes, 99000);
475
476 let results = extract_usn_from_logfile(&page);
477 assert!(!results.is_empty());
478
479 let found = &results[0];
480 assert_eq!(found.record.mft_entry, 42);
481 assert_eq!(found.record.mft_sequence, 7);
482 assert_eq!(found.record.parent_mft_entry, 30);
483 assert_eq!(found.record.parent_mft_sequence, 2);
484 assert_eq!(found.record.filename, "secure.pdf");
485 assert_eq!(found.record.major_version, 2);
486 assert!(found
488 .record
489 .reason
490 .contains(crate::usn::UsnReason::SECURITY_CHANGE));
491 }
492
493 #[test]
494 fn test_extract_skips_rstr_pages() {
495 let mut logfile_data = vec![0u8; LOG_PAGE_SIZE * 3];
497
498 logfile_data[0..4].copy_from_slice(b"RSTR");
500
501 let usn_bytes = build_v2_record_bytes(300, 1, 5, 5, 0x100, "found.txt");
503 let rcrd_page = build_rcrd_page_with_usn_in_redo(&usn_bytes, 70000);
504 logfile_data[LOG_PAGE_SIZE..LOG_PAGE_SIZE * 2].copy_from_slice(&rcrd_page);
505
506 let results = extract_usn_from_logfile(&logfile_data);
507 assert!(!results.is_empty());
508 assert_eq!(results[0].record.filename, "found.txt");
509 assert!(results[0].page_offset >= LOG_PAGE_SIZE);
511 }
512
513 #[test]
514 fn test_extract_unicode_filename() {
515 let usn_bytes = build_v2_record_bytes(400, 2, 5, 5, 0x100, "\u{6d4b}\u{8bd5}.txt");
516 let page = build_rcrd_page_with_usn_in_redo(&usn_bytes, 80000);
517
518 let results = extract_usn_from_logfile(&page);
519 assert!(!results.is_empty());
520 assert_eq!(results[0].record.filename, "\u{6d4b}\u{8bd5}.txt");
521 }
522
523 #[test]
524 fn test_scan_for_usn_records_in_raw_data() {
525 let mut data = vec![0u8; 256];
527 let usn_bytes = build_v2_record_bytes(50, 1, 5, 5, 0x100, "hi.txt");
528 data[0..usn_bytes.len()].copy_from_slice(&usn_bytes);
529
530 let found = scan_for_usn_records(&data);
531 assert_eq!(found.len(), 1);
532 assert_eq!(found[0].1.filename, "hi.txt");
533 }
534
535 #[test]
536 fn test_scan_for_multiple_usn_records() {
537 let usn1 = build_v2_record_bytes(10, 1, 5, 5, 0x100, "a.txt");
538 let usn2 = build_v2_record_bytes(20, 1, 5, 5, 0x200, "b.txt");
539
540 let mut data = Vec::new();
541 data.extend_from_slice(&usn1);
542 data.extend_from_slice(&usn2);
543 data.extend_from_slice(&[0u8; 64]);
545
546 let found = scan_for_usn_records(&data);
547 assert_eq!(found.len(), 2);
548 assert_eq!(found[0].1.filename, "a.txt");
549 assert_eq!(found[1].1.filename, "b.txt");
550 }
551
552 #[test]
553 fn test_try_parse_usn_at_invalid_data() {
554 let data = vec![0xAA; 256];
556 assert!(try_parse_usn_at(&data, 0).is_none());
557 }
558
559 #[test]
560 fn test_try_parse_usn_at_too_short() {
561 let data = vec![0u8; 10];
562 assert!(try_parse_usn_at(&data, 0).is_none());
563 }
564
565 #[test]
566 fn test_extract_from_undersized_page() {
567 let mut page = vec![0u8; RCRD_DATA_OFFSET - 1];
569 page[0..4].copy_from_slice(RCRD_SIGNATURE);
570 let results = extract_from_rcrd_page(&page, 0);
571 assert!(results.is_empty());
572 }
573
574 #[test]
575 fn test_logfile_record_source_equality() {
576 assert_eq!(LogFileRecordSource::RedoData, LogFileRecordSource::RedoData);
577 assert_ne!(LogFileRecordSource::RedoData, LogFileRecordSource::UndoData);
578 assert_ne!(
579 LogFileRecordSource::UndoData,
580 LogFileRecordSource::PageSlack
581 );
582 }
583
584 fn build_rcrd_page_with_usn_in_undo(usn_data: &[u8], page_lsn: u64) -> Vec<u8> {
586 let mut page = vec![0u8; LOG_PAGE_SIZE];
587
588 page[0..4].copy_from_slice(RCRD_SIGNATURE);
589 page[0x18..0x20].copy_from_slice(&page_lsn.to_le_bytes());
590
591 let data_offset = RCRD_DATA_OFFSET;
592
593 let this_lsn: u64 = 42000;
595 page[data_offset..data_offset + 8].copy_from_slice(&this_lsn.to_le_bytes());
596
597 let client_data_length = usn_data.len() as u32;
598 page[data_offset + 0x18..data_offset + 0x1C]
599 .copy_from_slice(&client_data_length.to_le_bytes());
600
601 let undo_offset: u16 = 0x10;
604 page[data_offset + 0x38..data_offset + 0x3A].copy_from_slice(&undo_offset.to_le_bytes());
605
606 let undo_length = usn_data.len() as u16;
607 page[data_offset + 0x3A..data_offset + 0x3C].copy_from_slice(&undo_length.to_le_bytes());
608
609 let undo_start = data_offset + 0x30 + undo_offset as usize;
610 if undo_start + usn_data.len() <= page.len() {
611 page[undo_start..undo_start + usn_data.len()].copy_from_slice(usn_data);
612 }
613
614 page
615 }
616
617 #[test]
618 fn test_extract_usn_from_undo_data() {
619 let usn_bytes = build_v2_record_bytes(300, 2, 10, 1, 0x200, "undo_file.doc");
620 let page = build_rcrd_page_with_usn_in_undo(&usn_bytes, 75000);
621
622 let results = extract_usn_from_logfile(&page);
623 assert!(!results.is_empty());
624
625 let found = results
626 .iter()
627 .find(|r| r.source == LogFileRecordSource::UndoData);
628 assert!(found.is_some());
629 let found = found.unwrap();
630 assert_eq!(found.record.mft_entry, 300);
631 assert_eq!(found.record.filename, "undo_file.doc");
632 }
633
634 #[test]
635 fn test_extract_page_with_zero_lsn_uses_page_lsn() {
636 let usn_bytes = build_v2_record_bytes(100, 1, 5, 5, 0x100, "test.txt");
637 let mut page = build_rcrd_page_with_usn_in_redo(&usn_bytes, 99000);
638
639 let data_offset = RCRD_DATA_OFFSET;
641 page[data_offset..data_offset + 8].copy_from_slice(&0u64.to_le_bytes());
642
643 let results = extract_usn_from_logfile(&page);
644 assert!(!results.is_empty());
645 assert_eq!(results[0].lsn, 99000); }
647
648 #[test]
649 fn test_extract_zero_client_data_length() {
650 let mut page = vec![0u8; LOG_PAGE_SIZE];
652 page[0..4].copy_from_slice(RCRD_SIGNATURE);
653 page[0x18..0x20].copy_from_slice(&50000u64.to_le_bytes());
654
655 let data_offset = RCRD_DATA_OFFSET;
657 page[data_offset..data_offset + 8].copy_from_slice(&42000u64.to_le_bytes());
658 page[data_offset + 0x18..data_offset + 0x1C].copy_from_slice(&0u32.to_le_bytes());
660
661 let results = extract_usn_from_logfile(&page);
662 assert!(
664 results.is_empty()
665 || results .iter() .all(|r| r.source == LogFileRecordSource::PageSlack) );
669 }
670
671 #[test]
672 fn test_try_parse_usn_at_non_v2_version() {
673 let mut data = vec![0u8; 0x60];
675 let record_len = 0x4Cu32;
676 data[0..4].copy_from_slice(&record_len.to_le_bytes());
677 data[4..6].copy_from_slice(&3u16.to_le_bytes()); assert!(try_parse_usn_at(&data, 0).is_none());
679 }
680
681 #[test]
682 fn test_try_parse_usn_at_record_len_exceeds_slice() {
683 let mut data = vec![0u8; 0x3C]; data[0..4].copy_from_slice(&(0x50u32).to_le_bytes()); data[4..6].copy_from_slice(&2u16.to_le_bytes());
687 assert!(try_parse_usn_at(&data, 0).is_none());
688 }
689
690 #[test]
691 fn test_scan_empty_data() {
692 let data: &[u8] = &[];
693 let found = scan_for_usn_records(data);
694 assert!(found.is_empty());
695 }
696
697 #[test]
698 fn test_scan_short_data() {
699 let data = vec![0u8; 10]; let found = scan_for_usn_records(&data);
701 assert!(found.is_empty());
702 }
703
704 #[test]
705 fn test_extract_logfile_data_not_page_aligned() {
706 let data = vec![0xAAu8; 100];
708 let results = extract_usn_from_logfile(&data);
709 assert!(results.is_empty());
710 }
711
712 #[test]
713 fn test_try_parse_usn_at_slice_shorter_than_8() {
714 let data = vec![0u8; USN_V2_MIN_SIZE];
721 let result = try_parse_usn_at(&data, 0);
724 assert!(result.is_none()); }
726
727 #[test]
728 fn test_extract_rcrd_page_huge_client_data_length() {
729 let mut page = vec![0u8; LOG_PAGE_SIZE];
733 page[0..4].copy_from_slice(RCRD_SIGNATURE);
734 page[0x18..0x20].copy_from_slice(&50000u64.to_le_bytes());
735
736 let data_offset = RCRD_DATA_OFFSET;
737 page[data_offset..data_offset + 8].copy_from_slice(&42000u64.to_le_bytes());
739 page[data_offset + 0x18..data_offset + 0x1C].copy_from_slice(&0xFFFFFFF0u32.to_le_bytes());
741
742 let results = extract_from_rcrd_page(&page, 0);
743 let _ = results;
745 }
746
747 #[test]
748 fn test_try_parse_usn_at_record_len_too_small() {
749 let mut data = vec![0u8; 0x60];
751 data[0..4].copy_from_slice(&(0x20u32).to_le_bytes());
753 data[4..6].copy_from_slice(&2u16.to_le_bytes());
754 assert!(try_parse_usn_at(&data, 0).is_none());
755 }
756
757 #[test]
758 fn test_try_parse_usn_at_record_len_too_large() {
759 let mut data = vec![0u8; 0x60];
761 data[0..4].copy_from_slice(&(70000u32).to_le_bytes());
762 data[4..6].copy_from_slice(&2u16.to_le_bytes());
763 assert!(try_parse_usn_at(&data, 0).is_none());
764 }
765
766 #[test]
767 fn test_extract_from_rcrd_page_short_for_page_lsn() {
768 let page = vec![0u8; 0x18]; let results = extract_from_rcrd_page(&page, 0);
781 assert!(results.is_empty());
782 }
783
784 #[test]
785 fn test_extract_aligned_size_zero_break() {
786 let mut page = vec![0u8; LOG_PAGE_SIZE];
794 page[0..4].copy_from_slice(RCRD_SIGNATURE);
795 page[0x18..0x20].copy_from_slice(&50000u64.to_le_bytes());
796
797 let data_offset = RCRD_DATA_OFFSET;
798 page[data_offset..data_offset + 8].copy_from_slice(&42000u64.to_le_bytes());
799 page[data_offset + 0x18..data_offset + 0x1C].copy_from_slice(&0u32.to_le_bytes());
801
802 let results = extract_from_rcrd_page(&page, 0);
803 let _ = results;
805 }
806
807 #[test]
808 fn test_extract_redo_start_exceeds_data_area() {
809 let mut page = vec![0u8; LOG_PAGE_SIZE];
812 page[0..4].copy_from_slice(RCRD_SIGNATURE);
813 page[0x18..0x20].copy_from_slice(&50000u64.to_le_bytes());
814
815 let data_offset = RCRD_DATA_OFFSET;
816 page[data_offset..data_offset + 8].copy_from_slice(&42000u64.to_le_bytes());
817
818 let client_data_length = 200u32;
819 page[data_offset + 0x18..data_offset + 0x1C]
820 .copy_from_slice(&client_data_length.to_le_bytes());
821
822 let redo_offset: u16 = 0x10;
824 page[data_offset + 0x34..data_offset + 0x36].copy_from_slice(&redo_offset.to_le_bytes());
825 let redo_length: u16 = 0xFFF0;
827 page[data_offset + 0x36..data_offset + 0x38].copy_from_slice(&redo_length.to_le_bytes());
828
829 let results = extract_from_rcrd_page(&page, 0);
830 let _ = results;
832 }
833
834 #[test]
835 fn test_extract_undo_start_exceeds_data_area() {
836 let mut page = vec![0u8; LOG_PAGE_SIZE];
838 page[0..4].copy_from_slice(RCRD_SIGNATURE);
839 page[0x18..0x20].copy_from_slice(&50000u64.to_le_bytes());
840
841 let data_offset = RCRD_DATA_OFFSET;
842 page[data_offset..data_offset + 8].copy_from_slice(&42000u64.to_le_bytes());
843
844 let client_data_length = 200u32;
845 page[data_offset + 0x18..data_offset + 0x1C]
846 .copy_from_slice(&client_data_length.to_le_bytes());
847
848 let undo_offset: u16 = 0x10;
850 page[data_offset + 0x38..data_offset + 0x3A].copy_from_slice(&undo_offset.to_le_bytes());
851 let undo_length: u16 = 0xFFF0;
852 page[data_offset + 0x3A..data_offset + 0x3C].copy_from_slice(&undo_length.to_le_bytes());
853
854 let results = extract_from_rcrd_page(&page, 0);
855 let _ = results;
856 }
857
858 #[test]
859 fn test_extract_same_redo_undo_region_deduplicates() {
860 let usn_bytes = build_v2_record_bytes(100, 1, 5, 5, 0x100, "dedup.txt");
863 let mut page = vec![0u8; LOG_PAGE_SIZE];
864
865 page[0..4].copy_from_slice(RCRD_SIGNATURE);
866 page[0x18..0x20].copy_from_slice(&50000u64.to_le_bytes());
867
868 let data_offset = RCRD_DATA_OFFSET;
869 page[data_offset..data_offset + 8].copy_from_slice(&42000u64.to_le_bytes());
870
871 let client_data_length = usn_bytes.len() as u32;
872 page[data_offset + 0x18..data_offset + 0x1C]
873 .copy_from_slice(&client_data_length.to_le_bytes());
874
875 let shared_offset: u16 = 0x10;
877 let shared_length = usn_bytes.len() as u16;
878
879 page[data_offset + 0x34..data_offset + 0x36].copy_from_slice(&shared_offset.to_le_bytes());
881 page[data_offset + 0x36..data_offset + 0x38].copy_from_slice(&shared_length.to_le_bytes());
882
883 page[data_offset + 0x38..data_offset + 0x3A].copy_from_slice(&shared_offset.to_le_bytes());
885 page[data_offset + 0x3A..data_offset + 0x3C].copy_from_slice(&shared_length.to_le_bytes());
886
887 let redo_start = data_offset + 0x30 + shared_offset as usize;
889 if redo_start + usn_bytes.len() <= page.len() {
890 page[redo_start..redo_start + usn_bytes.len()].copy_from_slice(&usn_bytes);
891 }
892
893 let results = extract_usn_from_logfile(&page);
894 let redo_count = results
896 .iter()
897 .filter(|r| r.source == LogFileRecordSource::RedoData)
898 .count();
899 let undo_count = results
900 .iter()
901 .filter(|r| r.source == LogFileRecordSource::UndoData)
902 .count();
903 assert!(redo_count >= 1);
904 assert_eq!(undo_count, 0);
905 }
906
907 #[test]
908 fn test_extract_record_offset_overflow_safety() {
909 let mut page = vec![0u8; LOG_PAGE_SIZE];
913 page[0..4].copy_from_slice(RCRD_SIGNATURE);
914 page[0x18..0x20].copy_from_slice(&50000u64.to_le_bytes());
915
916 let data_offset = RCRD_DATA_OFFSET;
917 page[data_offset..data_offset + 8].copy_from_slice(&42000u64.to_le_bytes());
918 page[data_offset + 0x18..data_offset + 0x1C]
920 .copy_from_slice(&(LOG_PAGE_SIZE as u32).to_le_bytes());
921
922 let results = extract_from_rcrd_page(&page, 0);
923 let _ = results;
925 }
926
927 #[test]
928 fn test_extract_from_rcrd_page_short_page_for_lsn() {
929 let mut page = vec![0u8; RCRD_DATA_OFFSET + 10];
932 page[0..4].copy_from_slice(RCRD_SIGNATURE);
933 let results = extract_from_rcrd_page(&page, 0);
937 assert!(results.is_empty() || !results.is_empty());
939 }
940}