use crate::usn::{parse_usn_record_v2, parse_usn_record_v3, UsnRecord};
const USN_V2_MIN_SIZE: usize = 0x3C;
const USN_V3_MIN_SIZE: usize = 0x4C;
const USN_MAX_RECORD_SIZE: usize = 65536;
const FILETIME_2000: i64 = 125_911_584_000_000_000;
const FILETIME_2030: i64 = 135_379_776_000_000_000;
#[derive(Debug, Clone)]
pub struct CarvedRecord {
pub offset: usize,
pub record: UsnRecord,
}
#[derive(Debug, Clone, Default)]
pub struct CarvingStats {
pub bytes_scanned: usize,
pub candidates_examined: u64,
pub records_carved: usize,
pub rejected_timestamp: u64,
pub rejected_structure: u64,
}
pub fn carve_usn_records(data: &[u8]) -> (Vec<CarvedRecord>, CarvingStats) {
let mut results = Vec::new();
let mut stats = CarvingStats {
bytes_scanned: data.len(),
..Default::default()
};
let len = data.len();
let mut offset = 0;
while offset + 8 <= len {
if data.get(offset..offset + 4) == Some(&[0, 0, 0, 0][..]) {
offset += 8;
continue;
}
let record_len = read_u32_le(data, offset) as usize;
let major_version = read_u16_le(data, offset + 4);
match major_version {
2 if (USN_V2_MIN_SIZE..=USN_MAX_RECORD_SIZE).contains(&record_len)
&& offset + record_len <= len =>
{
stats.candidates_examined += 1;
if let Some(carved) = try_carve_v2(data, offset, record_len, &mut stats) {
let aligned = (record_len + 7) & !7;
offset += aligned;
results.push(carved);
continue;
}
}
3 if (USN_V3_MIN_SIZE..=USN_MAX_RECORD_SIZE).contains(&record_len)
&& offset + record_len <= len =>
{
stats.candidates_examined += 1;
if let Some(carved) = try_carve_v3(data, offset, record_len, &mut stats) {
let aligned = (record_len + 7) & !7;
offset += aligned;
results.push(carved);
continue;
}
}
_ => {}
}
offset += 8;
}
stats.records_carved = results.len();
(results, stats)
}
fn try_carve_v2(
data: &[u8],
offset: usize,
record_len: usize,
stats: &mut CarvingStats,
) -> Option<CarvedRecord> {
if record_len < USN_V2_MIN_SIZE {
stats.rejected_structure += 1;
return None;
}
let record_data = data.get(offset..offset + record_len)?;
let filename_length = read_u16_le(record_data, 0x38) as usize;
let filename_offset = read_u16_le(record_data, 0x3A) as usize;
if filename_offset != 0x3C {
stats.rejected_structure += 1;
return None;
}
if filename_offset + filename_length > record_len {
stats.rejected_structure += 1;
return None;
}
if filename_length == 0 || filename_length % 2 != 0 {
stats.rejected_structure += 1;
return None;
}
let timestamp_raw = read_i64_le(record_data, 0x20);
if !is_valid_timestamp(timestamp_raw) {
stats.rejected_timestamp += 1;
return None;
}
if let Ok(record) = parse_usn_record_v2(record_data) {
Some(CarvedRecord { offset, record })
} else {
stats.rejected_structure += 1;
None
}
}
fn try_carve_v3(
data: &[u8],
offset: usize,
record_len: usize,
stats: &mut CarvingStats,
) -> Option<CarvedRecord> {
if record_len < USN_V3_MIN_SIZE {
stats.rejected_structure += 1;
return None;
}
let record_data = data.get(offset..offset + record_len)?;
let filename_length = read_u16_le(record_data, 0x48) as usize;
let filename_offset = read_u16_le(record_data, 0x4A) as usize;
if filename_offset != 0x4C {
stats.rejected_structure += 1;
return None;
}
if filename_offset + filename_length > record_len {
stats.rejected_structure += 1;
return None;
}
if filename_length == 0 || filename_length % 2 != 0 {
stats.rejected_structure += 1;
return None;
}
let timestamp_raw = read_i64_le(record_data, 0x30);
if !is_valid_timestamp(timestamp_raw) {
stats.rejected_timestamp += 1;
return None;
}
if let Ok(record) = parse_usn_record_v3(record_data) {
Some(CarvedRecord { offset, record })
} else {
stats.rejected_structure += 1;
None
}
}
fn is_valid_timestamp(filetime: i64) -> bool {
(FILETIME_2000..=FILETIME_2030).contains(&filetime)
}
fn read_u16_le(data: &[u8], offset: usize) -> u16 {
let mut b = [0u8; 2];
if let Some(s) = data.get(offset..offset + 2) {
b.copy_from_slice(s);
}
u16::from_le_bytes(b)
}
fn read_u32_le(data: &[u8], offset: usize) -> u32 {
let mut b = [0u8; 4];
if let Some(s) = data.get(offset..offset + 4) {
b.copy_from_slice(s);
}
u32::from_le_bytes(b)
}
fn read_i64_le(data: &[u8], offset: usize) -> i64 {
let mut b = [0u8; 8];
if let Some(s) = data.get(offset..offset + 8) {
b.copy_from_slice(s);
}
i64::from_le_bytes(b)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::usn::UsnReason;
fn build_v2_record_with_timestamp(
entry: u64,
seq: u16,
parent_entry: u64,
parent_seq: u16,
reason: u32,
filename: &str,
timestamp: i64,
) -> Vec<u8> {
let name_utf16: Vec<u16> = filename.encode_utf16().collect();
let name_bytes_len = name_utf16.len() * 2;
let record_len = 0x3C + name_bytes_len;
let aligned_len = (record_len + 7) & !7;
let mut buf = vec![0u8; aligned_len];
buf[0..4].copy_from_slice(&(record_len as u32).to_le_bytes());
buf[4..6].copy_from_slice(&2u16.to_le_bytes());
buf[6..8].copy_from_slice(&0u16.to_le_bytes());
let file_ref = entry | (u64::from(seq) << 48);
buf[0x08..0x10].copy_from_slice(&file_ref.to_le_bytes());
let parent_ref = parent_entry | (u64::from(parent_seq) << 48);
buf[0x10..0x18].copy_from_slice(&parent_ref.to_le_bytes());
buf[0x18..0x20].copy_from_slice(&100i64.to_le_bytes());
buf[0x20..0x28].copy_from_slice(×tamp.to_le_bytes());
buf[0x28..0x2C].copy_from_slice(&reason.to_le_bytes());
buf[0x2C..0x30].copy_from_slice(&0u32.to_le_bytes());
buf[0x30..0x34].copy_from_slice(&0u32.to_le_bytes());
buf[0x34..0x38].copy_from_slice(&0x20u32.to_le_bytes());
buf[0x38..0x3A].copy_from_slice(&(name_bytes_len as u16).to_le_bytes());
buf[0x3A..0x3C].copy_from_slice(&0x3Cu16.to_le_bytes());
for (i, &ch) in name_utf16.iter().enumerate() {
let off = 0x3C + i * 2;
buf[off..off + 2].copy_from_slice(&ch.to_le_bytes());
}
buf
}
fn build_v2_record(
entry: u64,
seq: u16,
parent_entry: u64,
parent_seq: u16,
reason: u32,
filename: &str,
) -> Vec<u8> {
let ts: i64 = 133_500_480_000_000_000;
build_v2_record_with_timestamp(entry, seq, parent_entry, parent_seq, reason, filename, ts)
}
#[test]
fn test_carve_from_random_data() {
let mut data = vec![0xDE; 8192];
for i in (0..data.len()).step_by(7) {
data[i] = (i % 256) as u8;
}
for i in (4..data.len() - 2).step_by(8) {
data[i] = 0xFF;
data[i + 1] = 0xFF;
}
let (records, stats) = carve_usn_records(&data);
assert_eq!(records.len(), 0);
assert_eq!(stats.bytes_scanned, 8192);
}
#[test]
fn test_carve_embedded_v2_record() {
let mut data = vec![0xAA; 512]; for i in (4..512).step_by(8) {
data[i] = 0xFF;
data[i + 1] = 0xFF;
}
let record = build_v2_record(42, 1, 5, 5, 0x100, "carved_file.txt");
let record_offset = data.len();
data.extend_from_slice(&record);
data.extend_from_slice(&vec![0xBB; 512]);
let (records, stats) = carve_usn_records(&data);
assert_eq!(records.len(), 1);
assert_eq!(records[0].offset, record_offset);
assert_eq!(records[0].record.filename, "carved_file.txt");
assert_eq!(records[0].record.mft_entry, 42);
assert_eq!(records[0].record.major_version, 2);
assert!(stats.candidates_examined >= 1);
assert_eq!(stats.records_carved, 1);
}
#[test]
fn test_carve_multiple_records_with_gaps() {
let mut data = Vec::new();
let mut garbage = vec![0xCC; 256];
for i in (4..garbage.len()).step_by(8) {
garbage[i] = 0xFF;
garbage[i + 1] = 0xFF;
}
data.extend_from_slice(&garbage);
let r1_offset = data.len();
let r1 = build_v2_record(100, 1, 5, 5, 0x100, "first.txt");
data.extend_from_slice(&r1);
let mut gap = vec![0xDD; 128];
for i in (4..gap.len()).step_by(8) {
gap[i] = 0xFF;
gap[i + 1] = 0xFF;
}
data.extend_from_slice(&gap);
let r2_offset = data.len();
let r2 = build_v2_record(200, 2, 100, 1, 0x200, "second.doc");
data.extend_from_slice(&r2);
let mut garbage2 = vec![0xEE; 64];
for i in (4..garbage2.len()).step_by(8) {
garbage2[i] = 0xFF;
garbage2[i + 1] = 0xFF;
}
data.extend_from_slice(&garbage2);
let r3_offset = data.len();
let r3 = build_v2_record(300, 1, 5, 5, 0x100, "third.pdf");
data.extend_from_slice(&r3);
data.extend_from_slice(&vec![0xFF; 256]);
let (records, stats) = carve_usn_records(&data);
assert_eq!(records.len(), 3);
assert_eq!(records[0].offset, r1_offset);
assert_eq!(records[0].record.filename, "first.txt");
assert_eq!(records[1].offset, r2_offset);
assert_eq!(records[1].record.filename, "second.doc");
assert_eq!(records[2].offset, r3_offset);
assert_eq!(records[2].record.filename, "third.pdf");
assert_eq!(stats.records_carved, 3);
}
#[test]
fn test_carve_rejects_invalid_timestamps() {
let ts_1990: i64 = 119_600_064_000_000_000; let r_old = build_v2_record_with_timestamp(100, 1, 5, 5, 0x100, "old.txt", ts_1990);
let ts_2035: i64 = 136_957_344_000_000_000; let r_future = build_v2_record_with_timestamp(200, 1, 5, 5, 0x100, "future.txt", ts_2035);
let r_valid = build_v2_record(300, 1, 5, 5, 0x100, "valid.txt");
let mut data = Vec::new();
data.extend_from_slice(&r_old);
data.extend_from_slice(&r_future);
data.extend_from_slice(&r_valid);
let (records, stats) = carve_usn_records(&data);
assert_eq!(records.len(), 1);
assert_eq!(records[0].record.filename, "valid.txt");
assert_eq!(stats.rejected_timestamp, 2);
}
#[test]
fn test_carve_handles_truncated_record() {
let record = build_v2_record(42, 1, 5, 5, 0x100, "truncated.txt");
let truncated = &record[..record.len() / 2];
let (records, _stats) = carve_usn_records(truncated);
assert_eq!(records.len(), 0);
}
#[test]
fn test_carve_empty_data() {
let (records, stats) = carve_usn_records(&[]);
assert_eq!(records.len(), 0);
assert_eq!(stats.bytes_scanned, 0);
}
#[test]
fn test_carve_all_zeros() {
let data = vec![0u8; 4096];
let (records, stats) = carve_usn_records(&data);
assert_eq!(records.len(), 0);
assert_eq!(stats.bytes_scanned, 4096);
assert_eq!(stats.candidates_examined, 0);
}
#[test]
fn test_carve_preserves_record_fields() {
let mut data = vec![0u8; 64]; let record = build_v2_record(12345, 7, 999, 3, 0x100 | 0x8000_0000, "important.xlsx");
data.extend_from_slice(&record);
let (records, _) = carve_usn_records(&data);
assert_eq!(records.len(), 1);
let r = &records[0].record;
assert_eq!(r.mft_entry, 12345);
assert_eq!(r.mft_sequence, 7);
assert_eq!(r.parent_mft_entry, 999);
assert_eq!(r.parent_mft_sequence, 3);
assert_eq!(r.filename, "important.xlsx");
assert!(r.reason.contains(UsnReason::FILE_CREATE));
assert!(r.reason.contains(UsnReason::CLOSE));
}
#[test]
fn test_carve_record_with_wrong_filename_offset() {
let mut data = build_v2_record(42, 1, 5, 5, 0x100, "test.txt");
data[0x3A..0x3C].copy_from_slice(&0x40u16.to_le_bytes());
let (records, stats) = carve_usn_records(&data);
assert_eq!(records.len(), 0);
assert!(stats.rejected_structure > 0);
}
#[test]
fn test_carve_record_with_zero_filename_length() {
let mut data = build_v2_record(42, 1, 5, 5, 0x100, "test.txt");
data[0x38..0x3A].copy_from_slice(&0u16.to_le_bytes());
let (records, stats) = carve_usn_records(&data);
assert_eq!(records.len(), 0);
assert!(stats.rejected_structure > 0);
}
#[test]
fn test_carve_record_with_odd_filename_length() {
let mut data = build_v2_record(42, 1, 5, 5, 0x100, "test.txt");
data[0x38..0x3A].copy_from_slice(&5u16.to_le_bytes());
let (records, stats) = carve_usn_records(&data);
assert_eq!(records.len(), 0);
assert!(stats.rejected_structure > 0);
}
#[test]
fn test_carve_record_filename_exceeds_record() {
let mut data = build_v2_record(42, 1, 5, 5, 0x100, "test.txt");
data[0x38..0x3A].copy_from_slice(&500u16.to_le_bytes());
let (records, stats) = carve_usn_records(&data);
assert_eq!(records.len(), 0);
assert!(stats.rejected_structure > 0);
}
#[test]
fn test_carve_v3_record() {
let name_utf16: Vec<u16> = "v3carved.txt".encode_utf16().collect();
let name_bytes_len = name_utf16.len() * 2;
let record_len = 0x4C + name_bytes_len;
let aligned_len = (record_len + 7) & !7;
let mut buf = vec![0u8; aligned_len];
buf[0..4].copy_from_slice(&(record_len as u32).to_le_bytes());
buf[4..6].copy_from_slice(&3u16.to_le_bytes()); buf[6..8].copy_from_slice(&0u16.to_le_bytes());
buf[0x08..0x18].copy_from_slice(&100u128.to_le_bytes());
buf[0x18..0x28].copy_from_slice(&5u128.to_le_bytes());
buf[0x28..0x30].copy_from_slice(&200i64.to_le_bytes());
let ts: i64 = 133_500_480_000_000_000; buf[0x30..0x38].copy_from_slice(&ts.to_le_bytes());
buf[0x38..0x3C].copy_from_slice(&0x100u32.to_le_bytes());
buf[0x44..0x48].copy_from_slice(&0x20u32.to_le_bytes());
buf[0x48..0x4A].copy_from_slice(&(name_bytes_len as u16).to_le_bytes());
buf[0x4A..0x4C].copy_from_slice(&0x4Cu16.to_le_bytes());
for (i, &ch) in name_utf16.iter().enumerate() {
let off = 0x4C + i * 2;
buf[off..off + 2].copy_from_slice(&ch.to_le_bytes());
}
let (records, stats) = carve_usn_records(&buf);
assert_eq!(records.len(), 1);
assert_eq!(records[0].record.filename, "v3carved.txt");
assert_eq!(records[0].record.major_version, 3);
assert_eq!(stats.records_carved, 1);
}
#[test]
fn test_carve_v3_wrong_filename_offset() {
let name_utf16: Vec<u16> = "test.txt".encode_utf16().collect();
let name_bytes_len = name_utf16.len() * 2;
let record_len = 0x4C + name_bytes_len;
let aligned_len = (record_len + 7) & !7;
let mut buf = vec![0u8; aligned_len];
buf[0..4].copy_from_slice(&(record_len as u32).to_le_bytes());
buf[4..6].copy_from_slice(&3u16.to_le_bytes());
let ts: i64 = 133_500_480_000_000_000;
buf[0x30..0x38].copy_from_slice(&ts.to_le_bytes());
buf[0x48..0x4A].copy_from_slice(&(name_bytes_len as u16).to_le_bytes());
buf[0x4A..0x4C].copy_from_slice(&0x50u16.to_le_bytes());
let (records, stats) = carve_usn_records(&buf);
assert_eq!(records.len(), 0);
assert!(stats.rejected_structure > 0);
}
#[test]
fn test_carve_v3_invalid_timestamp() {
let name_utf16: Vec<u16> = "old.txt".encode_utf16().collect();
let name_bytes_len = name_utf16.len() * 2;
let record_len = 0x4C + name_bytes_len;
let aligned_len = (record_len + 7) & !7;
let mut buf = vec![0u8; aligned_len];
buf[0..4].copy_from_slice(&(record_len as u32).to_le_bytes());
buf[4..6].copy_from_slice(&3u16.to_le_bytes());
let ts_old: i64 = 119_600_064_000_000_000; buf[0x30..0x38].copy_from_slice(&ts_old.to_le_bytes());
buf[0x48..0x4A].copy_from_slice(&(name_bytes_len as u16).to_le_bytes());
buf[0x4A..0x4C].copy_from_slice(&0x4Cu16.to_le_bytes());
let (records, stats) = carve_usn_records(&buf);
assert_eq!(records.len(), 0);
assert!(stats.rejected_timestamp > 0);
}
#[test]
fn test_carve_v3_zero_filename() {
let record_len = 0x4Cu32;
let aligned_len = ((record_len as usize) + 7) & !7;
let mut buf = vec![0u8; aligned_len];
buf[0..4].copy_from_slice(&record_len.to_le_bytes());
buf[4..6].copy_from_slice(&3u16.to_le_bytes());
let ts: i64 = 133_500_480_000_000_000;
buf[0x30..0x38].copy_from_slice(&ts.to_le_bytes());
buf[0x48..0x4A].copy_from_slice(&0u16.to_le_bytes()); buf[0x4A..0x4C].copy_from_slice(&0x4Cu16.to_le_bytes());
let (records, stats) = carve_usn_records(&buf);
assert_eq!(records.len(), 0);
assert!(stats.rejected_structure > 0);
}
#[test]
fn test_is_valid_timestamp() {
assert!(is_valid_timestamp(FILETIME_2000));
assert!(is_valid_timestamp(FILETIME_2030));
assert!(is_valid_timestamp(133_500_480_000_000_000)); assert!(!is_valid_timestamp(FILETIME_2000 - 1));
assert!(!is_valid_timestamp(FILETIME_2030 + 1));
assert!(!is_valid_timestamp(0));
assert!(!is_valid_timestamp(-1));
}
#[test]
fn test_carve_skips_version_0_and_1() {
let mut data = vec![0u8; 128];
data[0..4].copy_from_slice(&(0x40u32).to_le_bytes());
data[4..6].copy_from_slice(&1u16.to_le_bytes());
let (records, _) = carve_usn_records(&data);
assert_eq!(records.len(), 0);
}
#[test]
fn test_carving_stats_default() {
let stats = CarvingStats::default();
assert_eq!(stats.bytes_scanned, 0);
assert_eq!(stats.candidates_examined, 0);
assert_eq!(stats.records_carved, 0);
assert_eq!(stats.rejected_timestamp, 0);
assert_eq!(stats.rejected_structure, 0);
}
#[test]
fn test_try_carve_v2_parse_error() {
let valid_ts: i64 = 133_500_480_000_000_000;
let mut stats = CarvingStats::default();
let mut data = vec![0u8; 0x50]; data[0..4].copy_from_slice(&(0x20u32).to_le_bytes());
data[4..6].copy_from_slice(&2u16.to_le_bytes());
data[0x38..0x3A].copy_from_slice(&4u16.to_le_bytes()); data[0x3A..0x3C].copy_from_slice(&0x3Cu16.to_le_bytes()); data[0x20..0x28].copy_from_slice(&valid_ts.to_le_bytes());
let result = try_carve_v2(&data, 0, 0x50, &mut stats);
assert!(result.is_none());
assert!(stats.rejected_structure > 0);
}
#[test]
fn test_try_carve_v3_parse_error() {
let valid_ts: i64 = 133_500_480_000_000_000;
let mut stats = CarvingStats::default();
let mut data = vec![0u8; 0x60]; data[0..4].copy_from_slice(&(0x30u32).to_le_bytes()); data[4..6].copy_from_slice(&3u16.to_le_bytes());
data[0x48..0x4A].copy_from_slice(&4u16.to_le_bytes()); data[0x4A..0x4C].copy_from_slice(&0x4Cu16.to_le_bytes()); data[0x30..0x38].copy_from_slice(&valid_ts.to_le_bytes());
let result = try_carve_v3(&data, 0, 0x60, &mut stats);
assert!(result.is_none());
assert!(stats.rejected_structure > 0);
}
#[test]
fn test_try_carve_v2_record_len_below_min() {
let mut stats = CarvingStats::default();
let data = vec![0u8; 0x30]; let result = try_carve_v2(&data, 0, 0x30, &mut stats);
assert!(result.is_none());
assert_eq!(stats.rejected_structure, 1);
}
#[test]
fn test_try_carve_v3_record_len_below_min() {
let mut stats = CarvingStats::default();
let data = vec![0u8; 0x40]; let result = try_carve_v3(&data, 0, 0x40, &mut stats);
assert!(result.is_none());
assert_eq!(stats.rejected_structure, 1);
}
#[test]
fn test_try_carve_v3_filename_exceeds_record() {
let valid_ts: i64 = 133_500_480_000_000_000;
let mut stats = CarvingStats::default();
let record_len = 0x50usize; let mut data = vec![0u8; record_len];
data[0..4].copy_from_slice(&(record_len as u32).to_le_bytes());
data[4..6].copy_from_slice(&3u16.to_le_bytes());
data[0x30..0x38].copy_from_slice(&valid_ts.to_le_bytes());
data[0x48..0x4A].copy_from_slice(&100u16.to_le_bytes());
data[0x4A..0x4C].copy_from_slice(&0x4Cu16.to_le_bytes());
let result = try_carve_v3(&data, 0, record_len, &mut stats);
assert!(result.is_none());
assert!(stats.rejected_structure > 0);
}
#[test]
fn test_try_carve_v3_odd_filename_length() {
let valid_ts: i64 = 133_500_480_000_000_000;
let mut stats = CarvingStats::default();
let record_len = 0x60usize;
let mut data = vec![0u8; record_len];
data[0..4].copy_from_slice(&(record_len as u32).to_le_bytes());
data[4..6].copy_from_slice(&3u16.to_le_bytes());
data[0x30..0x38].copy_from_slice(&valid_ts.to_le_bytes());
data[0x48..0x4A].copy_from_slice(&3u16.to_le_bytes());
data[0x4A..0x4C].copy_from_slice(&0x4Cu16.to_le_bytes());
let result = try_carve_v3(&data, 0, record_len, &mut stats);
assert!(result.is_none());
assert!(stats.rejected_structure > 0);
}
#[test]
fn test_carve_v2_successful() {
let mut data = vec![0u8; 64]; let record = build_v2_record(42, 1, 5, 5, 0x100, "logged_v2.txt");
data.extend_from_slice(&record);
let (records, stats) = carve_usn_records(&data);
assert_eq!(records.len(), 1);
assert_eq!(records[0].record.filename, "logged_v2.txt");
assert_eq!(stats.records_carved, 1);
}
#[test]
fn test_carve_v3_successful() {
let name_utf16: Vec<u16> = "logged_v3.txt".encode_utf16().collect();
let name_bytes_len = name_utf16.len() * 2;
let record_len = 0x4C + name_bytes_len;
let aligned_len = (record_len + 7) & !7;
let mut buf = vec![0u8; aligned_len];
buf[0..4].copy_from_slice(&(record_len as u32).to_le_bytes());
buf[4..6].copy_from_slice(&3u16.to_le_bytes()); buf[6..8].copy_from_slice(&0u16.to_le_bytes());
buf[0x08..0x18].copy_from_slice(&100u128.to_le_bytes());
buf[0x18..0x28].copy_from_slice(&5u128.to_le_bytes());
buf[0x28..0x30].copy_from_slice(&200i64.to_le_bytes());
let ts: i64 = 133_500_480_000_000_000;
buf[0x30..0x38].copy_from_slice(&ts.to_le_bytes());
buf[0x38..0x3C].copy_from_slice(&0x100u32.to_le_bytes());
buf[0x44..0x48].copy_from_slice(&0x20u32.to_le_bytes());
buf[0x48..0x4A].copy_from_slice(&(name_bytes_len as u16).to_le_bytes());
buf[0x4A..0x4C].copy_from_slice(&0x4Cu16.to_le_bytes());
for (i, &ch) in name_utf16.iter().enumerate() {
let off = 0x4C + i * 2;
buf[off..off + 2].copy_from_slice(&ch.to_le_bytes());
}
let (records, stats) = carve_usn_records(&buf);
assert_eq!(records.len(), 1);
assert_eq!(records[0].record.filename, "logged_v3.txt");
assert_eq!(records[0].record.major_version, 3);
assert_eq!(stats.records_carved, 1);
}
#[test]
fn test_carve_v2_record_with_mismatched_internal_length() {
let valid_ts: i64 = 133_500_480_000_000_000;
let name_utf16: Vec<u16> = "ab".encode_utf16().collect();
let name_bytes_len = name_utf16.len() * 2; let outer_len = 0x3C + name_bytes_len; let aligned = (outer_len + 7) & !7;
let mut buf = vec![0u8; aligned];
buf[0..4].copy_from_slice(&(70000u32).to_le_bytes());
buf[4..6].copy_from_slice(&2u16.to_le_bytes());
buf[6..8].copy_from_slice(&0u16.to_le_bytes());
buf[0x20..0x28].copy_from_slice(&valid_ts.to_le_bytes());
buf[0x38..0x3A].copy_from_slice(&(name_bytes_len as u16).to_le_bytes());
buf[0x3A..0x3C].copy_from_slice(&0x3Cu16.to_le_bytes());
for (i, &ch) in name_utf16.iter().enumerate() {
buf[0x3C + i * 2..0x3C + i * 2 + 2].copy_from_slice(&ch.to_le_bytes());
}
let mut stats = CarvingStats::default();
let result = try_carve_v2(&buf, 0, aligned, &mut stats);
assert!(result.is_none());
assert!(stats.rejected_structure > 0);
}
}