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;
102 }
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
165 };
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;
243 }
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;
292 }
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)]
311mod tests {
312 use super::*;
313
314 fn build_v2_record_bytes(
316 entry: u64,
317 seq: u16,
318 parent_entry: u64,
319 parent_seq: u16,
320 reason: u32,
321 filename: &str,
322 ) -> Vec<u8> {
323 let name_utf16: Vec<u16> = filename.encode_utf16().collect();
324 let name_bytes_len = name_utf16.len() * 2;
325 let record_len = 0x3C + name_bytes_len;
326 let aligned_len = (record_len + 7) & !7;
327 let mut buf = vec![0u8; aligned_len];
328
329 buf[0..4].copy_from_slice(&(record_len as u32).to_le_bytes());
331 buf[4..6].copy_from_slice(&2u16.to_le_bytes());
333 buf[6..8].copy_from_slice(&0u16.to_le_bytes());
335 let file_ref = entry | ((seq as u64) << 48);
337 buf[0x08..0x10].copy_from_slice(&file_ref.to_le_bytes());
338 let parent_ref = parent_entry | ((parent_seq as u64) << 48);
340 buf[0x10..0x18].copy_from_slice(&parent_ref.to_le_bytes());
341 buf[0x18..0x20].copy_from_slice(&100i64.to_le_bytes());
343 let ts: i64 = 133500480000000000;
345 buf[0x20..0x28].copy_from_slice(&ts.to_le_bytes());
346 buf[0x28..0x2C].copy_from_slice(&reason.to_le_bytes());
348 buf[0x2C..0x30].copy_from_slice(&0u32.to_le_bytes());
350 buf[0x30..0x34].copy_from_slice(&0u32.to_le_bytes());
352 buf[0x34..0x38].copy_from_slice(&0x20u32.to_le_bytes());
354 buf[0x38..0x3A].copy_from_slice(&(name_bytes_len as u16).to_le_bytes());
356 buf[0x3A..0x3C].copy_from_slice(&0x3Cu16.to_le_bytes());
358 for (i, &ch) in name_utf16.iter().enumerate() {
360 let off = 0x3C + i * 2;
361 buf[off..off + 2].copy_from_slice(&ch.to_le_bytes());
362 }
363
364 buf
365 }
366
367 fn build_rcrd_page_with_usn_in_redo(usn_data: &[u8], page_lsn: u64) -> Vec<u8> {
369 let mut page = vec![0u8; LOG_PAGE_SIZE];
370
371 page[0..4].copy_from_slice(RCRD_SIGNATURE);
373 page[0x18..0x20].copy_from_slice(&page_lsn.to_le_bytes());
375
376 let data_offset = RCRD_DATA_OFFSET;
378
379 let this_lsn: u64 = 42000;
381 page[data_offset..data_offset + 8].copy_from_slice(&this_lsn.to_le_bytes());
382
383 let client_data_length = usn_data.len() as u32;
385 page[data_offset + 0x18..data_offset + 0x1C]
386 .copy_from_slice(&client_data_length.to_le_bytes());
387
388 let redo_offset: u16 = 0x10; page[data_offset + 0x34..data_offset + 0x36].copy_from_slice(&redo_offset.to_le_bytes());
391
392 let redo_length = usn_data.len() as u16;
394 page[data_offset + 0x36..data_offset + 0x38].copy_from_slice(&redo_length.to_le_bytes());
395
396 let redo_start = data_offset + 0x30 + redo_offset as usize;
399 if redo_start + usn_data.len() <= page.len() {
400 page[redo_start..redo_start + usn_data.len()].copy_from_slice(usn_data);
401 }
402
403 page
404 }
405
406 fn build_rcrd_page_with_usn_in_slack(usn_data: &[u8], page_lsn: u64) -> Vec<u8> {
408 let mut page = vec![0u8; LOG_PAGE_SIZE];
409
410 page[0..4].copy_from_slice(RCRD_SIGNATURE);
412 page[0x18..0x20].copy_from_slice(&page_lsn.to_le_bytes());
414
415 let slack_pos = LOG_PAGE_SIZE - usn_data.len() - 8; let slack_pos = slack_pos & !7;
422 if slack_pos >= RCRD_DATA_OFFSET && slack_pos + usn_data.len() <= page.len() {
423 page[slack_pos..slack_pos + usn_data.len()].copy_from_slice(usn_data);
424 }
425
426 page
427 }
428
429 #[test]
430 fn test_extract_empty_logfile() {
431 let results = extract_usn_from_logfile(&[]);
432 assert!(results.is_empty());
433 }
434
435 #[test]
436 fn test_extract_non_rcrd_pages() {
437 let data = vec![0u8; LOG_PAGE_SIZE * 4];
439 let results = extract_usn_from_logfile(&data);
440 assert!(results.is_empty());
441 }
442
443 #[test]
444 fn test_extract_usn_from_redo_data() {
445 let usn_bytes = build_v2_record_bytes(100, 3, 5, 5, 0x100, "secret.txt");
446 let page = build_rcrd_page_with_usn_in_redo(&usn_bytes, 50000);
447
448 let results = extract_usn_from_logfile(&page);
449 assert!(!results.is_empty());
450
451 let found = &results[0];
452 assert_eq!(found.source, LogFileRecordSource::RedoData);
453 assert_eq!(found.record.mft_entry, 100);
454 assert_eq!(found.record.mft_sequence, 3);
455 assert_eq!(found.record.filename, "secret.txt");
456 assert_eq!(found.lsn, 42000); }
458
459 #[test]
460 fn test_extract_usn_from_page_slack() {
461 let usn_bytes = build_v2_record_bytes(200, 1, 50, 1, 0x200, "deleted.doc");
462 let page = build_rcrd_page_with_usn_in_slack(&usn_bytes, 60000);
463
464 let results = extract_usn_from_logfile(&page);
465 assert!(!results.is_empty());
466
467 let found = results
468 .iter()
469 .find(|r| r.source == LogFileRecordSource::PageSlack);
470 assert!(found.is_some());
471 let found = found.unwrap();
472 assert_eq!(found.record.mft_entry, 200);
473 assert_eq!(found.record.filename, "deleted.doc");
474 assert_eq!(found.lsn, 60000); }
476
477 #[test]
478 fn test_extract_multiple_pages() {
479 let usn1 = build_v2_record_bytes(100, 1, 5, 5, 0x100, "file1.txt");
480 let usn2 = build_v2_record_bytes(200, 1, 5, 5, 0x200, "file2.txt");
481
482 let page1 = build_rcrd_page_with_usn_in_redo(&usn1, 10000);
483 let page2 = build_rcrd_page_with_usn_in_redo(&usn2, 20000);
484
485 let mut logfile_data = Vec::new();
486 logfile_data.extend_from_slice(&page1);
487 logfile_data.extend_from_slice(&page2);
488
489 let results = extract_usn_from_logfile(&logfile_data);
490 assert!(results.len() >= 2);
491
492 let filenames: Vec<&str> = results.iter().map(|r| r.record.filename.as_str()).collect();
493 assert!(filenames.contains(&"file1.txt"));
494 assert!(filenames.contains(&"file2.txt"));
495 }
496
497 #[test]
498 fn test_extract_preserves_usn_record_fields() {
499 let usn_bytes = build_v2_record_bytes(42, 7, 30, 2, 0x0000_0800, "secure.pdf");
500 let page = build_rcrd_page_with_usn_in_redo(&usn_bytes, 99000);
501
502 let results = extract_usn_from_logfile(&page);
503 assert!(!results.is_empty());
504
505 let found = &results[0];
506 assert_eq!(found.record.mft_entry, 42);
507 assert_eq!(found.record.mft_sequence, 7);
508 assert_eq!(found.record.parent_mft_entry, 30);
509 assert_eq!(found.record.parent_mft_sequence, 2);
510 assert_eq!(found.record.filename, "secure.pdf");
511 assert_eq!(found.record.major_version, 2);
512 assert!(found
514 .record
515 .reason
516 .contains(crate::usn::UsnReason::SECURITY_CHANGE));
517 }
518
519 #[test]
520 fn test_extract_skips_rstr_pages() {
521 let mut logfile_data = vec![0u8; LOG_PAGE_SIZE * 3];
523
524 logfile_data[0..4].copy_from_slice(b"RSTR");
526
527 let usn_bytes = build_v2_record_bytes(300, 1, 5, 5, 0x100, "found.txt");
529 let rcrd_page = build_rcrd_page_with_usn_in_redo(&usn_bytes, 70000);
530 logfile_data[LOG_PAGE_SIZE..LOG_PAGE_SIZE * 2].copy_from_slice(&rcrd_page);
531
532 let results = extract_usn_from_logfile(&logfile_data);
533 assert!(!results.is_empty());
534 assert_eq!(results[0].record.filename, "found.txt");
535 assert!(results[0].page_offset >= LOG_PAGE_SIZE);
537 }
538
539 #[test]
540 fn test_extract_unicode_filename() {
541 let usn_bytes = build_v2_record_bytes(400, 2, 5, 5, 0x100, "\u{6d4b}\u{8bd5}.txt");
542 let page = build_rcrd_page_with_usn_in_redo(&usn_bytes, 80000);
543
544 let results = extract_usn_from_logfile(&page);
545 assert!(!results.is_empty());
546 assert_eq!(results[0].record.filename, "\u{6d4b}\u{8bd5}.txt");
547 }
548
549 #[test]
550 fn test_scan_for_usn_records_in_raw_data() {
551 let mut data = vec![0u8; 256];
553 let usn_bytes = build_v2_record_bytes(50, 1, 5, 5, 0x100, "hi.txt");
554 data[0..usn_bytes.len()].copy_from_slice(&usn_bytes);
555
556 let found = scan_for_usn_records(&data);
557 assert_eq!(found.len(), 1);
558 assert_eq!(found[0].1.filename, "hi.txt");
559 }
560
561 #[test]
562 fn test_scan_for_multiple_usn_records() {
563 let usn1 = build_v2_record_bytes(10, 1, 5, 5, 0x100, "a.txt");
564 let usn2 = build_v2_record_bytes(20, 1, 5, 5, 0x200, "b.txt");
565
566 let mut data = Vec::new();
567 data.extend_from_slice(&usn1);
568 data.extend_from_slice(&usn2);
569 data.extend_from_slice(&[0u8; 64]);
571
572 let found = scan_for_usn_records(&data);
573 assert_eq!(found.len(), 2);
574 assert_eq!(found[0].1.filename, "a.txt");
575 assert_eq!(found[1].1.filename, "b.txt");
576 }
577
578 #[test]
579 fn test_try_parse_usn_at_invalid_data() {
580 let data = vec![0xAA; 256];
582 assert!(try_parse_usn_at(&data, 0).is_none());
583 }
584
585 #[test]
586 fn test_try_parse_usn_at_too_short() {
587 let data = vec![0u8; 10];
588 assert!(try_parse_usn_at(&data, 0).is_none());
589 }
590
591 #[test]
592 fn test_extract_from_undersized_page() {
593 let mut page = vec![0u8; RCRD_DATA_OFFSET - 1];
595 page[0..4].copy_from_slice(RCRD_SIGNATURE);
596 let results = extract_from_rcrd_page(&page, 0);
597 assert!(results.is_empty());
598 }
599
600 #[test]
601 fn test_logfile_record_source_equality() {
602 assert_eq!(LogFileRecordSource::RedoData, LogFileRecordSource::RedoData);
603 assert_ne!(LogFileRecordSource::RedoData, LogFileRecordSource::UndoData);
604 assert_ne!(
605 LogFileRecordSource::UndoData,
606 LogFileRecordSource::PageSlack
607 );
608 }
609
610 fn build_rcrd_page_with_usn_in_undo(usn_data: &[u8], page_lsn: u64) -> Vec<u8> {
612 let mut page = vec![0u8; LOG_PAGE_SIZE];
613
614 page[0..4].copy_from_slice(RCRD_SIGNATURE);
615 page[0x18..0x20].copy_from_slice(&page_lsn.to_le_bytes());
616
617 let data_offset = RCRD_DATA_OFFSET;
618
619 let this_lsn: u64 = 42000;
621 page[data_offset..data_offset + 8].copy_from_slice(&this_lsn.to_le_bytes());
622
623 let client_data_length = usn_data.len() as u32;
624 page[data_offset + 0x18..data_offset + 0x1C]
625 .copy_from_slice(&client_data_length.to_le_bytes());
626
627 let undo_offset: u16 = 0x10;
630 page[data_offset + 0x38..data_offset + 0x3A].copy_from_slice(&undo_offset.to_le_bytes());
631
632 let undo_length = usn_data.len() as u16;
633 page[data_offset + 0x3A..data_offset + 0x3C].copy_from_slice(&undo_length.to_le_bytes());
634
635 let undo_start = data_offset + 0x30 + undo_offset as usize;
636 if undo_start + usn_data.len() <= page.len() {
637 page[undo_start..undo_start + usn_data.len()].copy_from_slice(usn_data);
638 }
639
640 page
641 }
642
643 #[test]
644 fn test_extract_usn_from_undo_data() {
645 let usn_bytes = build_v2_record_bytes(300, 2, 10, 1, 0x200, "undo_file.doc");
646 let page = build_rcrd_page_with_usn_in_undo(&usn_bytes, 75000);
647
648 let results = extract_usn_from_logfile(&page);
649 assert!(!results.is_empty());
650
651 let found = results
652 .iter()
653 .find(|r| r.source == LogFileRecordSource::UndoData);
654 assert!(found.is_some());
655 let found = found.unwrap();
656 assert_eq!(found.record.mft_entry, 300);
657 assert_eq!(found.record.filename, "undo_file.doc");
658 }
659
660 #[test]
661 fn test_extract_page_with_zero_lsn_uses_page_lsn() {
662 let usn_bytes = build_v2_record_bytes(100, 1, 5, 5, 0x100, "test.txt");
663 let mut page = build_rcrd_page_with_usn_in_redo(&usn_bytes, 99000);
664
665 let data_offset = RCRD_DATA_OFFSET;
667 page[data_offset..data_offset + 8].copy_from_slice(&0u64.to_le_bytes());
668
669 let results = extract_usn_from_logfile(&page);
670 assert!(!results.is_empty());
671 assert_eq!(results[0].lsn, 99000); }
673
674 #[test]
675 fn test_extract_zero_client_data_length() {
676 let mut page = vec![0u8; LOG_PAGE_SIZE];
678 page[0..4].copy_from_slice(RCRD_SIGNATURE);
679 page[0x18..0x20].copy_from_slice(&50000u64.to_le_bytes());
680
681 let data_offset = RCRD_DATA_OFFSET;
683 page[data_offset..data_offset + 8].copy_from_slice(&42000u64.to_le_bytes());
684 page[data_offset + 0x18..data_offset + 0x1C].copy_from_slice(&0u32.to_le_bytes());
686
687 let results = extract_usn_from_logfile(&page);
688 assert!(
690 results.is_empty()
691 || results
692 .iter()
693 .all(|r| r.source == LogFileRecordSource::PageSlack)
694 );
695 }
696
697 #[test]
698 fn test_try_parse_usn_at_non_v2_version() {
699 let mut data = vec![0u8; 0x60];
701 let record_len = 0x4Cu32;
702 data[0..4].copy_from_slice(&record_len.to_le_bytes());
703 data[4..6].copy_from_slice(&3u16.to_le_bytes()); assert!(try_parse_usn_at(&data, 0).is_none());
705 }
706
707 #[test]
708 fn test_try_parse_usn_at_record_len_exceeds_slice() {
709 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());
713 assert!(try_parse_usn_at(&data, 0).is_none());
714 }
715
716 #[test]
717 fn test_scan_empty_data() {
718 let data: &[u8] = &[];
719 let found = scan_for_usn_records(data);
720 assert!(found.is_empty());
721 }
722
723 #[test]
724 fn test_scan_short_data() {
725 let data = vec![0u8; 10]; let found = scan_for_usn_records(&data);
727 assert!(found.is_empty());
728 }
729
730 #[test]
731 fn test_extract_logfile_data_not_page_aligned() {
732 let data = vec![0xAAu8; 100];
734 let results = extract_usn_from_logfile(&data);
735 assert!(results.is_empty());
736 }
737
738 #[test]
739 fn test_try_parse_usn_at_slice_shorter_than_8() {
740 let data = vec![0u8; USN_V2_MIN_SIZE];
747 let result = try_parse_usn_at(&data, 0);
750 assert!(result.is_none()); }
752
753 #[test]
754 fn test_extract_rcrd_page_huge_client_data_length() {
755 let mut page = vec![0u8; LOG_PAGE_SIZE];
759 page[0..4].copy_from_slice(RCRD_SIGNATURE);
760 page[0x18..0x20].copy_from_slice(&50000u64.to_le_bytes());
761
762 let data_offset = RCRD_DATA_OFFSET;
763 page[data_offset..data_offset + 8].copy_from_slice(&42000u64.to_le_bytes());
765 page[data_offset + 0x18..data_offset + 0x1C].copy_from_slice(&0xFFFFFFF0u32.to_le_bytes());
767
768 let results = extract_from_rcrd_page(&page, 0);
769 let _ = results;
771 }
772
773 #[test]
774 fn test_try_parse_usn_at_record_len_too_small() {
775 let mut data = vec![0u8; 0x60];
777 data[0..4].copy_from_slice(&(0x20u32).to_le_bytes());
779 data[4..6].copy_from_slice(&2u16.to_le_bytes());
780 assert!(try_parse_usn_at(&data, 0).is_none());
781 }
782
783 #[test]
784 fn test_try_parse_usn_at_record_len_too_large() {
785 let mut data = vec![0u8; 0x60];
787 data[0..4].copy_from_slice(&(70000u32).to_le_bytes());
788 data[4..6].copy_from_slice(&2u16.to_le_bytes());
789 assert!(try_parse_usn_at(&data, 0).is_none());
790 }
791
792 #[test]
793 fn test_extract_from_rcrd_page_short_for_page_lsn() {
794 let page = vec![0u8; 0x18]; let results = extract_from_rcrd_page(&page, 0);
807 assert!(results.is_empty());
808 }
809
810 #[test]
811 fn test_extract_aligned_size_zero_break() {
812 let mut page = vec![0u8; LOG_PAGE_SIZE];
820 page[0..4].copy_from_slice(RCRD_SIGNATURE);
821 page[0x18..0x20].copy_from_slice(&50000u64.to_le_bytes());
822
823 let data_offset = RCRD_DATA_OFFSET;
824 page[data_offset..data_offset + 8].copy_from_slice(&42000u64.to_le_bytes());
825 page[data_offset + 0x18..data_offset + 0x1C].copy_from_slice(&0u32.to_le_bytes());
827
828 let results = extract_from_rcrd_page(&page, 0);
829 let _ = results;
831 }
832
833 #[test]
834 fn test_extract_redo_start_exceeds_data_area() {
835 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 redo_offset: u16 = 0x10;
850 page[data_offset + 0x34..data_offset + 0x36].copy_from_slice(&redo_offset.to_le_bytes());
851 let redo_length: u16 = 0xFFF0;
853 page[data_offset + 0x36..data_offset + 0x38].copy_from_slice(&redo_length.to_le_bytes());
854
855 let results = extract_from_rcrd_page(&page, 0);
856 let _ = results;
858 }
859
860 #[test]
861 fn test_extract_undo_start_exceeds_data_area() {
862 let mut page = vec![0u8; LOG_PAGE_SIZE];
864 page[0..4].copy_from_slice(RCRD_SIGNATURE);
865 page[0x18..0x20].copy_from_slice(&50000u64.to_le_bytes());
866
867 let data_offset = RCRD_DATA_OFFSET;
868 page[data_offset..data_offset + 8].copy_from_slice(&42000u64.to_le_bytes());
869
870 let client_data_length = 200u32;
871 page[data_offset + 0x18..data_offset + 0x1C]
872 .copy_from_slice(&client_data_length.to_le_bytes());
873
874 let undo_offset: u16 = 0x10;
876 page[data_offset + 0x38..data_offset + 0x3A].copy_from_slice(&undo_offset.to_le_bytes());
877 let undo_length: u16 = 0xFFF0;
878 page[data_offset + 0x3A..data_offset + 0x3C].copy_from_slice(&undo_length.to_le_bytes());
879
880 let results = extract_from_rcrd_page(&page, 0);
881 let _ = results;
882 }
883
884 #[test]
885 fn test_extract_same_redo_undo_region_deduplicates() {
886 let usn_bytes = build_v2_record_bytes(100, 1, 5, 5, 0x100, "dedup.txt");
889 let mut page = vec![0u8; LOG_PAGE_SIZE];
890
891 page[0..4].copy_from_slice(RCRD_SIGNATURE);
892 page[0x18..0x20].copy_from_slice(&50000u64.to_le_bytes());
893
894 let data_offset = RCRD_DATA_OFFSET;
895 page[data_offset..data_offset + 8].copy_from_slice(&42000u64.to_le_bytes());
896
897 let client_data_length = usn_bytes.len() as u32;
898 page[data_offset + 0x18..data_offset + 0x1C]
899 .copy_from_slice(&client_data_length.to_le_bytes());
900
901 let shared_offset: u16 = 0x10;
903 let shared_length = usn_bytes.len() as u16;
904
905 page[data_offset + 0x34..data_offset + 0x36].copy_from_slice(&shared_offset.to_le_bytes());
907 page[data_offset + 0x36..data_offset + 0x38].copy_from_slice(&shared_length.to_le_bytes());
908
909 page[data_offset + 0x38..data_offset + 0x3A].copy_from_slice(&shared_offset.to_le_bytes());
911 page[data_offset + 0x3A..data_offset + 0x3C].copy_from_slice(&shared_length.to_le_bytes());
912
913 let redo_start = data_offset + 0x30 + shared_offset as usize;
915 if redo_start + usn_bytes.len() <= page.len() {
916 page[redo_start..redo_start + usn_bytes.len()].copy_from_slice(&usn_bytes);
917 }
918
919 let results = extract_usn_from_logfile(&page);
920 let redo_count = results
922 .iter()
923 .filter(|r| r.source == LogFileRecordSource::RedoData)
924 .count();
925 let undo_count = results
926 .iter()
927 .filter(|r| r.source == LogFileRecordSource::UndoData)
928 .count();
929 assert!(redo_count >= 1);
930 assert_eq!(undo_count, 0);
931 }
932
933 #[test]
934 fn test_extract_record_offset_overflow_safety() {
935 let mut page = vec![0u8; LOG_PAGE_SIZE];
939 page[0..4].copy_from_slice(RCRD_SIGNATURE);
940 page[0x18..0x20].copy_from_slice(&50000u64.to_le_bytes());
941
942 let data_offset = RCRD_DATA_OFFSET;
943 page[data_offset..data_offset + 8].copy_from_slice(&42000u64.to_le_bytes());
944 page[data_offset + 0x18..data_offset + 0x1C]
946 .copy_from_slice(&(LOG_PAGE_SIZE as u32).to_le_bytes());
947
948 let results = extract_from_rcrd_page(&page, 0);
949 let _ = results;
951 }
952
953 #[test]
954 fn test_extract_from_rcrd_page_short_page_for_lsn() {
955 let mut page = vec![0u8; RCRD_DATA_OFFSET + 10];
958 page[0..4].copy_from_slice(RCRD_SIGNATURE);
959 let results = extract_from_rcrd_page(&page, 0);
963 assert!(results.is_empty() || !results.is_empty());
965 }
966}