1use crate::usn::{parse_usn_record_v2, UsnRecord};
11
12const RCRD_SIGNATURE: &[u8; 4] = b"RCRD";
16
17const LOG_PAGE_SIZE: usize = 0x1000; const RCRD_DATA_OFFSET: usize = 0x40;
22
23const LOG_RECORD_HEADER_MIN: usize = 0x40;
25
26const USN_V2_MIN_SIZE: usize = 0x3C;
28
29const USN_MAX_RECORD_SIZE: usize = 65536;
31
32#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum LogFileRecordSource {
37 RedoData,
39 UndoData,
41 PageSlack,
43}
44
45#[derive(Debug, Clone)]
47pub struct LogFileUsnRecord {
48 pub lsn: u64,
50 pub page_offset: usize,
52 pub source: LogFileRecordSource,
54 pub record: UsnRecord,
56}
57
58fn read_u16_le(data: &[u8], offset: usize) -> u16 {
61 u16::from_le_bytes([data[offset], data[offset + 1]])
62}
63
64fn read_u32_le(data: &[u8], offset: usize) -> u32 {
65 u32::from_le_bytes([
66 data[offset],
67 data[offset + 1],
68 data[offset + 2],
69 data[offset + 3],
70 ])
71}
72
73fn read_u64_le(data: &[u8], offset: usize) -> u64 {
74 u64::from_le_bytes([
75 data[offset],
76 data[offset + 1],
77 data[offset + 2],
78 data[offset + 3],
79 data[offset + 4],
80 data[offset + 5],
81 data[offset + 6],
82 data[offset + 7],
83 ])
84}
85
86fn try_parse_usn_at(data: &[u8], offset: usize) -> Option<UsnRecord> {
93 if offset + USN_V2_MIN_SIZE > data.len() {
94 return None;
95 }
96
97 let slice = &data[offset..];
98
99 if slice.len() < 8 {
101 return None; }
103
104 let record_len = u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]) as usize;
105
106 if !(USN_V2_MIN_SIZE..=USN_MAX_RECORD_SIZE).contains(&record_len) {
108 return None;
109 }
110 if record_len > slice.len() {
111 return None;
112 }
113
114 let major_version = u16::from_le_bytes([slice[4], slice[5]]);
116 if major_version != 2 {
117 return None;
118 }
119
120 parse_usn_record_v2(&slice[..record_len]).ok()
122}
123
124fn scan_for_usn_records(data: &[u8]) -> Vec<(usize, UsnRecord)> {
126 let mut results = Vec::new();
127 let mut offset = 0;
128
129 while offset + USN_V2_MIN_SIZE <= data.len() {
130 if let Some(record) = try_parse_usn_at(data, offset) {
131 let record_len = u32::from_le_bytes([
132 data[offset],
133 data[offset + 1],
134 data[offset + 2],
135 data[offset + 3],
136 ]) as usize;
137 results.push((offset, record));
138 let aligned = (record_len + 7) & !7;
140 offset += aligned;
141 } else {
142 offset += 8;
144 }
145 }
146
147 results
148}
149
150fn extract_from_rcrd_page(page_data: &[u8], page_offset: usize) -> Vec<LogFileUsnRecord> {
153 let mut results = Vec::new();
154
155 if page_data.len() < RCRD_DATA_OFFSET {
156 return results;
157 }
158
159 let page_lsn = if page_data.len() >= 0x20 {
162 read_u64_le(page_data, 0x18)
163 } else {
164 0 };
166
167 let data_area = &page_data[RCRD_DATA_OFFSET..];
169 let mut record_offset = 0;
170
171 while record_offset + LOG_RECORD_HEADER_MIN <= data_area.len() {
172 if record_offset + 8 <= data_area.len()
174 && data_area[record_offset..record_offset + 8] == [0, 0, 0, 0, 0, 0, 0, 0]
175 {
176 break;
178 }
179
180 let this_lsn = read_u64_le(data_area, record_offset);
182 let client_data_length = read_u32_le(data_area, record_offset + 0x18) as usize;
183 let _redo_op = read_u16_le(data_area, record_offset + 0x30);
184 let _undo_op = read_u16_le(data_area, record_offset + 0x32);
185 let redo_offset = read_u16_le(data_area, record_offset + 0x34) as usize;
186 let redo_length = read_u16_le(data_area, record_offset + 0x36) as usize;
187 let undo_offset = read_u16_le(data_area, record_offset + 0x38) as usize;
188 let undo_length = read_u16_le(data_area, record_offset + 0x3A) as usize;
189
190 let redo_base = record_offset + 0x30;
192 let undo_base = record_offset + 0x30;
193
194 let lsn = if this_lsn > 0 { this_lsn } else { page_lsn };
196
197 if redo_length >= USN_V2_MIN_SIZE && redo_offset > 0 {
199 let redo_start = redo_base + redo_offset;
200 if redo_start + redo_length <= data_area.len() {
201 let redo_data = &data_area[redo_start..redo_start + redo_length];
202 for (_off, record) in scan_for_usn_records(redo_data) {
203 results.push(LogFileUsnRecord {
204 lsn,
205 page_offset: page_offset + RCRD_DATA_OFFSET + redo_start,
206 source: LogFileRecordSource::RedoData,
207 record,
208 });
209 }
210 }
211 }
212
213 if undo_length >= USN_V2_MIN_SIZE && undo_offset > 0 {
215 let undo_start = undo_base + undo_offset;
216 let redo_start = redo_base + redo_offset;
218 let same_region = undo_start == redo_start && undo_length == redo_length;
219 if !same_region && undo_start + undo_length <= data_area.len() {
220 let undo_data = &data_area[undo_start..undo_start + undo_length];
221 for (_off, record) in scan_for_usn_records(undo_data) {
222 results.push(LogFileUsnRecord {
223 lsn,
224 page_offset: page_offset + RCRD_DATA_OFFSET + undo_start,
225 source: LogFileRecordSource::UndoData,
226 record,
227 });
228 }
229 }
230 }
231
232 let log_record_size = 0x30 + client_data_length;
236 if log_record_size == 0x30 && client_data_length == 0 {
237 record_offset += 8;
239 } else {
240 let aligned_size = (log_record_size + 7) & !7;
241 if aligned_size == 0 {
242 break; }
244 record_offset += aligned_size;
245 }
246
247 if record_offset > data_area.len() {
249 break;
250 }
251 }
252
253 let slack_start = RCRD_DATA_OFFSET + record_offset;
255 if slack_start < page_data.len() {
256 let slack_data = &page_data[slack_start..];
257 for (_off, record) in scan_for_usn_records(slack_data) {
258 results.push(LogFileUsnRecord {
259 lsn: page_lsn,
260 page_offset: page_offset + slack_start,
261 source: LogFileRecordSource::PageSlack,
262 record,
263 });
264 }
265 }
266
267 results
268}
269
270pub fn extract_usn_from_logfile(logfile_data: &[u8]) -> Vec<LogFileUsnRecord> {
283 let mut results = Vec::new();
284 let page_count = logfile_data.len() / LOG_PAGE_SIZE;
285
286 for page_idx in 0..page_count {
287 let page_offset = page_idx * LOG_PAGE_SIZE;
288
289 if page_offset + 4 > logfile_data.len() {
291 break; }
293 let sig = &logfile_data[page_offset..page_offset + 4];
294 if sig != RCRD_SIGNATURE {
295 continue;
296 }
297
298 let page_end = (page_offset + LOG_PAGE_SIZE).min(logfile_data.len());
299 let page_data = &logfile_data[page_offset..page_end];
300
301 let page_results = extract_from_rcrd_page(page_data, page_offset);
302 results.extend(page_results);
303 }
304
305 results
306}
307
308#[cfg(test)]
311#[allow(clippy::unreadable_literal, clippy::cast_lossless)]
312mod tests {
313 use super::*;
314
315 fn build_v2_record_bytes(
317 entry: u64,
318 seq: u16,
319 parent_entry: u64,
320 parent_seq: u16,
321 reason: u32,
322 filename: &str,
323 ) -> Vec<u8> {
324 let name_utf16: Vec<u16> = filename.encode_utf16().collect();
325 let name_bytes_len = name_utf16.len() * 2;
326 let record_len = 0x3C + name_bytes_len;
327 let aligned_len = (record_len + 7) & !7;
328 let mut buf = vec![0u8; aligned_len];
329
330 buf[0..4].copy_from_slice(&(record_len as u32).to_le_bytes());
332 buf[4..6].copy_from_slice(&2u16.to_le_bytes());
334 buf[6..8].copy_from_slice(&0u16.to_le_bytes());
336 let file_ref = entry | ((seq as u64) << 48);
338 buf[0x08..0x10].copy_from_slice(&file_ref.to_le_bytes());
339 let parent_ref = parent_entry | ((parent_seq as u64) << 48);
341 buf[0x10..0x18].copy_from_slice(&parent_ref.to_le_bytes());
342 buf[0x18..0x20].copy_from_slice(&100i64.to_le_bytes());
344 let ts: i64 = 133500480000000000;
346 buf[0x20..0x28].copy_from_slice(&ts.to_le_bytes());
347 buf[0x28..0x2C].copy_from_slice(&reason.to_le_bytes());
349 buf[0x2C..0x30].copy_from_slice(&0u32.to_le_bytes());
351 buf[0x30..0x34].copy_from_slice(&0u32.to_le_bytes());
353 buf[0x34..0x38].copy_from_slice(&0x20u32.to_le_bytes());
355 buf[0x38..0x3A].copy_from_slice(&(name_bytes_len as u16).to_le_bytes());
357 buf[0x3A..0x3C].copy_from_slice(&0x3Cu16.to_le_bytes());
359 for (i, &ch) in name_utf16.iter().enumerate() {
361 let off = 0x3C + i * 2;
362 buf[off..off + 2].copy_from_slice(&ch.to_le_bytes());
363 }
364
365 buf
366 }
367
368 fn build_rcrd_page_with_usn_in_redo(usn_data: &[u8], page_lsn: u64) -> Vec<u8> {
370 let mut page = vec![0u8; LOG_PAGE_SIZE];
371
372 page[0..4].copy_from_slice(RCRD_SIGNATURE);
374 page[0x18..0x20].copy_from_slice(&page_lsn.to_le_bytes());
376
377 let data_offset = RCRD_DATA_OFFSET;
379
380 let this_lsn: u64 = 42000;
382 page[data_offset..data_offset + 8].copy_from_slice(&this_lsn.to_le_bytes());
383
384 let client_data_length = usn_data.len() as u32;
386 page[data_offset + 0x18..data_offset + 0x1C]
387 .copy_from_slice(&client_data_length.to_le_bytes());
388
389 let redo_offset: u16 = 0x10; page[data_offset + 0x34..data_offset + 0x36].copy_from_slice(&redo_offset.to_le_bytes());
392
393 let redo_length = usn_data.len() as u16;
395 page[data_offset + 0x36..data_offset + 0x38].copy_from_slice(&redo_length.to_le_bytes());
396
397 let redo_start = data_offset + 0x30 + redo_offset as usize;
400 if redo_start + usn_data.len() <= page.len() {
401 page[redo_start..redo_start + usn_data.len()].copy_from_slice(usn_data);
402 }
403
404 page
405 }
406
407 fn build_rcrd_page_with_usn_in_slack(usn_data: &[u8], page_lsn: u64) -> Vec<u8> {
409 let mut page = vec![0u8; LOG_PAGE_SIZE];
410
411 page[0..4].copy_from_slice(RCRD_SIGNATURE);
413 page[0x18..0x20].copy_from_slice(&page_lsn.to_le_bytes());
415
416 let slack_pos = LOG_PAGE_SIZE - usn_data.len() - 8; let slack_pos = slack_pos & !7;
423 if slack_pos >= RCRD_DATA_OFFSET && slack_pos + usn_data.len() <= page.len() {
424 page[slack_pos..slack_pos + usn_data.len()].copy_from_slice(usn_data);
425 }
426
427 page
428 }
429
430 #[test]
431 fn test_extract_empty_logfile() {
432 let results = extract_usn_from_logfile(&[]);
433 assert!(results.is_empty());
434 }
435
436 #[test]
437 fn test_extract_non_rcrd_pages() {
438 let data = vec![0u8; LOG_PAGE_SIZE * 4];
440 let results = extract_usn_from_logfile(&data);
441 assert!(results.is_empty());
442 }
443
444 #[test]
445 fn test_extract_usn_from_redo_data() {
446 let usn_bytes = build_v2_record_bytes(100, 3, 5, 5, 0x100, "secret.txt");
447 let page = build_rcrd_page_with_usn_in_redo(&usn_bytes, 50000);
448
449 let results = extract_usn_from_logfile(&page);
450 assert!(!results.is_empty());
451
452 let found = &results[0];
453 assert_eq!(found.source, LogFileRecordSource::RedoData);
454 assert_eq!(found.record.mft_entry, 100);
455 assert_eq!(found.record.mft_sequence, 3);
456 assert_eq!(found.record.filename, "secret.txt");
457 assert_eq!(found.lsn, 42000); }
459
460 #[test]
461 fn test_extract_usn_from_page_slack() {
462 let usn_bytes = build_v2_record_bytes(200, 1, 50, 1, 0x200, "deleted.doc");
463 let page = build_rcrd_page_with_usn_in_slack(&usn_bytes, 60000);
464
465 let results = extract_usn_from_logfile(&page);
466 assert!(!results.is_empty());
467
468 let found = results
469 .iter()
470 .find(|r| r.source == LogFileRecordSource::PageSlack);
471 assert!(found.is_some());
472 let found = found.unwrap();
473 assert_eq!(found.record.mft_entry, 200);
474 assert_eq!(found.record.filename, "deleted.doc");
475 assert_eq!(found.lsn, 60000); }
477
478 #[test]
479 fn test_extract_multiple_pages() {
480 let usn1 = build_v2_record_bytes(100, 1, 5, 5, 0x100, "file1.txt");
481 let usn2 = build_v2_record_bytes(200, 1, 5, 5, 0x200, "file2.txt");
482
483 let page1 = build_rcrd_page_with_usn_in_redo(&usn1, 10000);
484 let page2 = build_rcrd_page_with_usn_in_redo(&usn2, 20000);
485
486 let mut logfile_data = Vec::new();
487 logfile_data.extend_from_slice(&page1);
488 logfile_data.extend_from_slice(&page2);
489
490 let results = extract_usn_from_logfile(&logfile_data);
491 assert!(results.len() >= 2);
492
493 let filenames: Vec<&str> = results.iter().map(|r| r.record.filename.as_str()).collect();
494 assert!(filenames.contains(&"file1.txt"));
495 assert!(filenames.contains(&"file2.txt"));
496 }
497
498 #[test]
499 fn test_extract_preserves_usn_record_fields() {
500 let usn_bytes = build_v2_record_bytes(42, 7, 30, 2, 0x0000_0800, "secure.pdf");
501 let page = build_rcrd_page_with_usn_in_redo(&usn_bytes, 99000);
502
503 let results = extract_usn_from_logfile(&page);
504 assert!(!results.is_empty());
505
506 let found = &results[0];
507 assert_eq!(found.record.mft_entry, 42);
508 assert_eq!(found.record.mft_sequence, 7);
509 assert_eq!(found.record.parent_mft_entry, 30);
510 assert_eq!(found.record.parent_mft_sequence, 2);
511 assert_eq!(found.record.filename, "secure.pdf");
512 assert_eq!(found.record.major_version, 2);
513 assert!(found
515 .record
516 .reason
517 .contains(crate::usn::UsnReason::SECURITY_CHANGE));
518 }
519
520 #[test]
521 fn test_extract_skips_rstr_pages() {
522 let mut logfile_data = vec![0u8; LOG_PAGE_SIZE * 3];
524
525 logfile_data[0..4].copy_from_slice(b"RSTR");
527
528 let usn_bytes = build_v2_record_bytes(300, 1, 5, 5, 0x100, "found.txt");
530 let rcrd_page = build_rcrd_page_with_usn_in_redo(&usn_bytes, 70000);
531 logfile_data[LOG_PAGE_SIZE..LOG_PAGE_SIZE * 2].copy_from_slice(&rcrd_page);
532
533 let results = extract_usn_from_logfile(&logfile_data);
534 assert!(!results.is_empty());
535 assert_eq!(results[0].record.filename, "found.txt");
536 assert!(results[0].page_offset >= LOG_PAGE_SIZE);
538 }
539
540 #[test]
541 fn test_extract_unicode_filename() {
542 let usn_bytes = build_v2_record_bytes(400, 2, 5, 5, 0x100, "\u{6d4b}\u{8bd5}.txt");
543 let page = build_rcrd_page_with_usn_in_redo(&usn_bytes, 80000);
544
545 let results = extract_usn_from_logfile(&page);
546 assert!(!results.is_empty());
547 assert_eq!(results[0].record.filename, "\u{6d4b}\u{8bd5}.txt");
548 }
549
550 #[test]
551 fn test_scan_for_usn_records_in_raw_data() {
552 let mut data = vec![0u8; 256];
554 let usn_bytes = build_v2_record_bytes(50, 1, 5, 5, 0x100, "hi.txt");
555 data[0..usn_bytes.len()].copy_from_slice(&usn_bytes);
556
557 let found = scan_for_usn_records(&data);
558 assert_eq!(found.len(), 1);
559 assert_eq!(found[0].1.filename, "hi.txt");
560 }
561
562 #[test]
563 fn test_scan_for_multiple_usn_records() {
564 let usn1 = build_v2_record_bytes(10, 1, 5, 5, 0x100, "a.txt");
565 let usn2 = build_v2_record_bytes(20, 1, 5, 5, 0x200, "b.txt");
566
567 let mut data = Vec::new();
568 data.extend_from_slice(&usn1);
569 data.extend_from_slice(&usn2);
570 data.extend_from_slice(&[0u8; 64]);
572
573 let found = scan_for_usn_records(&data);
574 assert_eq!(found.len(), 2);
575 assert_eq!(found[0].1.filename, "a.txt");
576 assert_eq!(found[1].1.filename, "b.txt");
577 }
578
579 #[test]
580 fn test_try_parse_usn_at_invalid_data() {
581 let data = vec![0xAA; 256];
583 assert!(try_parse_usn_at(&data, 0).is_none());
584 }
585
586 #[test]
587 fn test_try_parse_usn_at_too_short() {
588 let data = vec![0u8; 10];
589 assert!(try_parse_usn_at(&data, 0).is_none());
590 }
591
592 #[test]
593 fn test_extract_from_undersized_page() {
594 let mut page = vec![0u8; RCRD_DATA_OFFSET - 1];
596 page[0..4].copy_from_slice(RCRD_SIGNATURE);
597 let results = extract_from_rcrd_page(&page, 0);
598 assert!(results.is_empty());
599 }
600
601 #[test]
602 fn test_logfile_record_source_equality() {
603 assert_eq!(LogFileRecordSource::RedoData, LogFileRecordSource::RedoData);
604 assert_ne!(LogFileRecordSource::RedoData, LogFileRecordSource::UndoData);
605 assert_ne!(
606 LogFileRecordSource::UndoData,
607 LogFileRecordSource::PageSlack
608 );
609 }
610
611 fn build_rcrd_page_with_usn_in_undo(usn_data: &[u8], page_lsn: u64) -> Vec<u8> {
613 let mut page = vec![0u8; LOG_PAGE_SIZE];
614
615 page[0..4].copy_from_slice(RCRD_SIGNATURE);
616 page[0x18..0x20].copy_from_slice(&page_lsn.to_le_bytes());
617
618 let data_offset = RCRD_DATA_OFFSET;
619
620 let this_lsn: u64 = 42000;
622 page[data_offset..data_offset + 8].copy_from_slice(&this_lsn.to_le_bytes());
623
624 let client_data_length = usn_data.len() as u32;
625 page[data_offset + 0x18..data_offset + 0x1C]
626 .copy_from_slice(&client_data_length.to_le_bytes());
627
628 let undo_offset: u16 = 0x10;
631 page[data_offset + 0x38..data_offset + 0x3A].copy_from_slice(&undo_offset.to_le_bytes());
632
633 let undo_length = usn_data.len() as u16;
634 page[data_offset + 0x3A..data_offset + 0x3C].copy_from_slice(&undo_length.to_le_bytes());
635
636 let undo_start = data_offset + 0x30 + undo_offset as usize;
637 if undo_start + usn_data.len() <= page.len() {
638 page[undo_start..undo_start + usn_data.len()].copy_from_slice(usn_data);
639 }
640
641 page
642 }
643
644 #[test]
645 fn test_extract_usn_from_undo_data() {
646 let usn_bytes = build_v2_record_bytes(300, 2, 10, 1, 0x200, "undo_file.doc");
647 let page = build_rcrd_page_with_usn_in_undo(&usn_bytes, 75000);
648
649 let results = extract_usn_from_logfile(&page);
650 assert!(!results.is_empty());
651
652 let found = results
653 .iter()
654 .find(|r| r.source == LogFileRecordSource::UndoData);
655 assert!(found.is_some());
656 let found = found.unwrap();
657 assert_eq!(found.record.mft_entry, 300);
658 assert_eq!(found.record.filename, "undo_file.doc");
659 }
660
661 #[test]
662 fn test_extract_page_with_zero_lsn_uses_page_lsn() {
663 let usn_bytes = build_v2_record_bytes(100, 1, 5, 5, 0x100, "test.txt");
664 let mut page = build_rcrd_page_with_usn_in_redo(&usn_bytes, 99000);
665
666 let data_offset = RCRD_DATA_OFFSET;
668 page[data_offset..data_offset + 8].copy_from_slice(&0u64.to_le_bytes());
669
670 let results = extract_usn_from_logfile(&page);
671 assert!(!results.is_empty());
672 assert_eq!(results[0].lsn, 99000); }
674
675 #[test]
676 fn test_extract_zero_client_data_length() {
677 let mut page = vec![0u8; LOG_PAGE_SIZE];
679 page[0..4].copy_from_slice(RCRD_SIGNATURE);
680 page[0x18..0x20].copy_from_slice(&50000u64.to_le_bytes());
681
682 let data_offset = RCRD_DATA_OFFSET;
684 page[data_offset..data_offset + 8].copy_from_slice(&42000u64.to_le_bytes());
685 page[data_offset + 0x18..data_offset + 0x1C].copy_from_slice(&0u32.to_le_bytes());
687
688 let results = extract_usn_from_logfile(&page);
689 assert!(
691 results.is_empty()
692 || results .iter() .all(|r| r.source == LogFileRecordSource::PageSlack) );
696 }
697
698 #[test]
699 fn test_try_parse_usn_at_non_v2_version() {
700 let mut data = vec![0u8; 0x60];
702 let record_len = 0x4Cu32;
703 data[0..4].copy_from_slice(&record_len.to_le_bytes());
704 data[4..6].copy_from_slice(&3u16.to_le_bytes()); assert!(try_parse_usn_at(&data, 0).is_none());
706 }
707
708 #[test]
709 fn test_try_parse_usn_at_record_len_exceeds_slice() {
710 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());
714 assert!(try_parse_usn_at(&data, 0).is_none());
715 }
716
717 #[test]
718 fn test_scan_empty_data() {
719 let data: &[u8] = &[];
720 let found = scan_for_usn_records(data);
721 assert!(found.is_empty());
722 }
723
724 #[test]
725 fn test_scan_short_data() {
726 let data = vec![0u8; 10]; let found = scan_for_usn_records(&data);
728 assert!(found.is_empty());
729 }
730
731 #[test]
732 fn test_extract_logfile_data_not_page_aligned() {
733 let data = vec![0xAAu8; 100];
735 let results = extract_usn_from_logfile(&data);
736 assert!(results.is_empty());
737 }
738
739 #[test]
740 fn test_try_parse_usn_at_slice_shorter_than_8() {
741 let data = vec![0u8; USN_V2_MIN_SIZE];
748 let result = try_parse_usn_at(&data, 0);
751 assert!(result.is_none()); }
753
754 #[test]
755 fn test_extract_rcrd_page_huge_client_data_length() {
756 let mut page = vec![0u8; LOG_PAGE_SIZE];
760 page[0..4].copy_from_slice(RCRD_SIGNATURE);
761 page[0x18..0x20].copy_from_slice(&50000u64.to_le_bytes());
762
763 let data_offset = RCRD_DATA_OFFSET;
764 page[data_offset..data_offset + 8].copy_from_slice(&42000u64.to_le_bytes());
766 page[data_offset + 0x18..data_offset + 0x1C].copy_from_slice(&0xFFFFFFF0u32.to_le_bytes());
768
769 let results = extract_from_rcrd_page(&page, 0);
770 let _ = results;
772 }
773
774 #[test]
775 fn test_try_parse_usn_at_record_len_too_small() {
776 let mut data = vec![0u8; 0x60];
778 data[0..4].copy_from_slice(&(0x20u32).to_le_bytes());
780 data[4..6].copy_from_slice(&2u16.to_le_bytes());
781 assert!(try_parse_usn_at(&data, 0).is_none());
782 }
783
784 #[test]
785 fn test_try_parse_usn_at_record_len_too_large() {
786 let mut data = vec![0u8; 0x60];
788 data[0..4].copy_from_slice(&(70000u32).to_le_bytes());
789 data[4..6].copy_from_slice(&2u16.to_le_bytes());
790 assert!(try_parse_usn_at(&data, 0).is_none());
791 }
792
793 #[test]
794 fn test_extract_from_rcrd_page_short_for_page_lsn() {
795 let page = vec![0u8; 0x18]; let results = extract_from_rcrd_page(&page, 0);
808 assert!(results.is_empty());
809 }
810
811 #[test]
812 fn test_extract_aligned_size_zero_break() {
813 let mut page = vec![0u8; LOG_PAGE_SIZE];
821 page[0..4].copy_from_slice(RCRD_SIGNATURE);
822 page[0x18..0x20].copy_from_slice(&50000u64.to_le_bytes());
823
824 let data_offset = RCRD_DATA_OFFSET;
825 page[data_offset..data_offset + 8].copy_from_slice(&42000u64.to_le_bytes());
826 page[data_offset + 0x18..data_offset + 0x1C].copy_from_slice(&0u32.to_le_bytes());
828
829 let results = extract_from_rcrd_page(&page, 0);
830 let _ = results;
832 }
833
834 #[test]
835 fn test_extract_redo_start_exceeds_data_area() {
836 let mut page = vec![0u8; LOG_PAGE_SIZE];
839 page[0..4].copy_from_slice(RCRD_SIGNATURE);
840 page[0x18..0x20].copy_from_slice(&50000u64.to_le_bytes());
841
842 let data_offset = RCRD_DATA_OFFSET;
843 page[data_offset..data_offset + 8].copy_from_slice(&42000u64.to_le_bytes());
844
845 let client_data_length = 200u32;
846 page[data_offset + 0x18..data_offset + 0x1C]
847 .copy_from_slice(&client_data_length.to_le_bytes());
848
849 let redo_offset: u16 = 0x10;
851 page[data_offset + 0x34..data_offset + 0x36].copy_from_slice(&redo_offset.to_le_bytes());
852 let redo_length: u16 = 0xFFF0;
854 page[data_offset + 0x36..data_offset + 0x38].copy_from_slice(&redo_length.to_le_bytes());
855
856 let results = extract_from_rcrd_page(&page, 0);
857 let _ = results;
859 }
860
861 #[test]
862 fn test_extract_undo_start_exceeds_data_area() {
863 let mut page = vec![0u8; LOG_PAGE_SIZE];
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 = 200u32;
872 page[data_offset + 0x18..data_offset + 0x1C]
873 .copy_from_slice(&client_data_length.to_le_bytes());
874
875 let undo_offset: u16 = 0x10;
877 page[data_offset + 0x38..data_offset + 0x3A].copy_from_slice(&undo_offset.to_le_bytes());
878 let undo_length: u16 = 0xFFF0;
879 page[data_offset + 0x3A..data_offset + 0x3C].copy_from_slice(&undo_length.to_le_bytes());
880
881 let results = extract_from_rcrd_page(&page, 0);
882 let _ = results;
883 }
884
885 #[test]
886 fn test_extract_same_redo_undo_region_deduplicates() {
887 let usn_bytes = build_v2_record_bytes(100, 1, 5, 5, 0x100, "dedup.txt");
890 let mut page = vec![0u8; LOG_PAGE_SIZE];
891
892 page[0..4].copy_from_slice(RCRD_SIGNATURE);
893 page[0x18..0x20].copy_from_slice(&50000u64.to_le_bytes());
894
895 let data_offset = RCRD_DATA_OFFSET;
896 page[data_offset..data_offset + 8].copy_from_slice(&42000u64.to_le_bytes());
897
898 let client_data_length = usn_bytes.len() as u32;
899 page[data_offset + 0x18..data_offset + 0x1C]
900 .copy_from_slice(&client_data_length.to_le_bytes());
901
902 let shared_offset: u16 = 0x10;
904 let shared_length = usn_bytes.len() as u16;
905
906 page[data_offset + 0x34..data_offset + 0x36].copy_from_slice(&shared_offset.to_le_bytes());
908 page[data_offset + 0x36..data_offset + 0x38].copy_from_slice(&shared_length.to_le_bytes());
909
910 page[data_offset + 0x38..data_offset + 0x3A].copy_from_slice(&shared_offset.to_le_bytes());
912 page[data_offset + 0x3A..data_offset + 0x3C].copy_from_slice(&shared_length.to_le_bytes());
913
914 let redo_start = data_offset + 0x30 + shared_offset as usize;
916 if redo_start + usn_bytes.len() <= page.len() {
917 page[redo_start..redo_start + usn_bytes.len()].copy_from_slice(&usn_bytes);
918 }
919
920 let results = extract_usn_from_logfile(&page);
921 let redo_count = results
923 .iter()
924 .filter(|r| r.source == LogFileRecordSource::RedoData)
925 .count();
926 let undo_count = results
927 .iter()
928 .filter(|r| r.source == LogFileRecordSource::UndoData)
929 .count();
930 assert!(redo_count >= 1);
931 assert_eq!(undo_count, 0);
932 }
933
934 #[test]
935 fn test_extract_record_offset_overflow_safety() {
936 let mut page = vec![0u8; LOG_PAGE_SIZE];
940 page[0..4].copy_from_slice(RCRD_SIGNATURE);
941 page[0x18..0x20].copy_from_slice(&50000u64.to_le_bytes());
942
943 let data_offset = RCRD_DATA_OFFSET;
944 page[data_offset..data_offset + 8].copy_from_slice(&42000u64.to_le_bytes());
945 page[data_offset + 0x18..data_offset + 0x1C]
947 .copy_from_slice(&(LOG_PAGE_SIZE as u32).to_le_bytes());
948
949 let results = extract_from_rcrd_page(&page, 0);
950 let _ = results;
952 }
953
954 #[test]
955 fn test_extract_from_rcrd_page_short_page_for_lsn() {
956 let mut page = vec![0u8; RCRD_DATA_OFFSET + 10];
959 page[0..4].copy_from_slice(RCRD_SIGNATURE);
960 let results = extract_from_rcrd_page(&page, 0);
964 assert!(results.is_empty() || !results.is_empty());
966 }
967}