use chrono::{DateTime, Utc};
use super::attributes::FileAttributes;
use super::reason::UsnReason;
use crate::error::{NtfsError, Result};
const USN_V2_MIN_SIZE: usize = 0x3C;
const USN_V3_MIN_SIZE: usize = 0x4C;
const USN_V4_MIN_SIZE: usize = 0x38;
const USN_MAX_RECORD_SIZE: usize = 65536;
#[derive(Debug, Clone)]
pub struct UsnRecord {
pub mft_entry: u64,
pub mft_sequence: u16,
pub parent_mft_entry: u64,
pub parent_mft_sequence: u16,
pub usn: i64,
pub timestamp: DateTime<Utc>,
pub reason: UsnReason,
pub filename: String,
pub file_attributes: FileAttributes,
pub source_info: u32,
pub security_id: u32,
pub major_version: u16,
}
fn read_u16_le(data: &[u8], offset: usize) -> u16 {
u16::from_le_bytes([data[offset], data[offset + 1]])
}
fn read_u32_le(data: &[u8], offset: usize) -> u32 {
u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
])
}
fn read_i64_le(data: &[u8], offset: usize) -> i64 {
i64::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
data[offset + 4],
data[offset + 5],
data[offset + 6],
data[offset + 7],
])
}
fn read_u64_le(data: &[u8], offset: usize) -> u64 {
u64::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
data[offset + 4],
data[offset + 5],
data[offset + 6],
data[offset + 7],
])
}
fn read_u128_le(data: &[u8], offset: usize) -> u128 {
let mut bytes = [0u8; 16];
bytes.copy_from_slice(&data[offset..offset + 16]);
u128::from_le_bytes(bytes)
}
fn filetime_to_datetime(filetime: i64) -> Option<DateTime<Utc>> {
if filetime <= 0 {
return None;
}
const EPOCH_DIFF: i64 = 116_444_736_000_000_000;
let unix_100ns = filetime - EPOCH_DIFF;
if unix_100ns < 0 {
return None;
}
let secs = unix_100ns / 10_000_000;
let nanos = ((unix_100ns % 10_000_000) * 100) as u32;
DateTime::from_timestamp(secs, nanos)
}
pub fn parse_usn_record_v2(data: &[u8]) -> Result<UsnRecord> {
if data.len() < USN_V2_MIN_SIZE {
return Err(NtfsError::Usn(format!(
"Data too short for USN_RECORD_V2: {} < {}",
data.len(),
USN_V2_MIN_SIZE
)));
}
let record_len = read_u32_le(data, 0x00) as usize;
if !(USN_V2_MIN_SIZE..=USN_MAX_RECORD_SIZE).contains(&record_len) {
return Err(NtfsError::Usn(format!(
"Invalid V2 record length: {record_len}"
)));
}
if record_len > data.len() {
return Err(NtfsError::Usn(format!(
"V2 record length {} exceeds available data {}",
record_len,
data.len()
)));
}
let file_ref = read_u64_le(data, 0x08);
let mft_entry = file_ref & 0x0000_FFFF_FFFF_FFFF;
let mft_sequence = ((file_ref >> 48) & 0xFFFF) as u16;
let parent_ref = read_u64_le(data, 0x10);
let parent_mft_entry = parent_ref & 0x0000_FFFF_FFFF_FFFF;
let parent_mft_sequence = ((parent_ref >> 48) & 0xFFFF) as u16;
let usn = read_i64_le(data, 0x18);
let timestamp_raw = read_i64_le(data, 0x20);
let reason_bits = read_u32_le(data, 0x28);
let source_info = read_u32_le(data, 0x2C);
let security_id = read_u32_le(data, 0x30);
let file_attr_bits = read_u32_le(data, 0x34);
let filename_length = read_u16_le(data, 0x38) as usize;
let filename_offset = read_u16_le(data, 0x3A) as usize;
let timestamp = filetime_to_datetime(timestamp_raw)
.unwrap_or_else(|| DateTime::from_timestamp(0, 0).unwrap_or_default());
let filename = if filename_offset + filename_length <= data.len() && filename_length >= 2 {
let name_bytes = &data[filename_offset..filename_offset + filename_length];
let u16_chars: Vec<u16> = name_bytes
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
String::from_utf16_lossy(&u16_chars)
} else {
String::new()
};
Ok(UsnRecord {
mft_entry,
mft_sequence,
parent_mft_entry,
parent_mft_sequence,
usn,
timestamp,
reason: UsnReason::from_bits_retain(reason_bits),
filename,
file_attributes: FileAttributes::from_bits_retain(file_attr_bits),
source_info,
security_id,
major_version: 2,
})
}
pub fn parse_usn_record_v3(data: &[u8]) -> Result<UsnRecord> {
if data.len() < USN_V3_MIN_SIZE {
return Err(NtfsError::Usn(format!(
"Data too short for USN_RECORD_V3: {} < {}",
data.len(),
USN_V3_MIN_SIZE
)));
}
let record_len = read_u32_le(data, 0x00) as usize;
if !(USN_V3_MIN_SIZE..=USN_MAX_RECORD_SIZE).contains(&record_len) {
return Err(NtfsError::Usn(format!(
"Invalid V3 record length: {record_len}"
)));
}
let file_ref_128 = read_u128_le(data, 0x08);
let mft_entry = (file_ref_128 & 0xFFFF_FFFF_FFFF) as u64;
let mft_sequence = 0u16;
let parent_ref_128 = read_u128_le(data, 0x18);
let parent_mft_entry = (parent_ref_128 & 0xFFFF_FFFF_FFFF) as u64;
let parent_mft_sequence = 0u16;
let usn = read_i64_le(data, 0x28);
let timestamp_raw = read_i64_le(data, 0x30);
let reason_bits = read_u32_le(data, 0x38);
let source_info = read_u32_le(data, 0x3C);
let security_id = read_u32_le(data, 0x40);
let file_attr_bits = read_u32_le(data, 0x44);
let filename_length = read_u16_le(data, 0x48) as usize;
let filename_offset = read_u16_le(data, 0x4A) as usize;
let timestamp = filetime_to_datetime(timestamp_raw)
.unwrap_or_else(|| DateTime::from_timestamp(0, 0).unwrap_or_default());
let filename = if filename_offset + filename_length <= data.len() && filename_length >= 2 {
let name_bytes = &data[filename_offset..filename_offset + filename_length];
let u16_chars: Vec<u16> = name_bytes
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
String::from_utf16_lossy(&u16_chars)
} else {
String::new()
};
Ok(UsnRecord {
mft_entry,
mft_sequence,
parent_mft_entry,
parent_mft_sequence,
usn,
timestamp,
reason: UsnReason::from_bits_retain(reason_bits),
filename,
file_attributes: FileAttributes::from_bits_retain(file_attr_bits),
source_info,
security_id,
major_version: 3,
})
}
pub fn parse_usn_journal(data: &[u8]) -> Result<Vec<UsnRecord>> {
let mut records = Vec::new();
let mut offset = 0;
let len = data.len();
while offset + 8 <= len {
if data[offset..offset + 4] == [0, 0, 0, 0] {
let mut found = false;
while offset + 8 <= len {
if data[offset..offset + 8] != [0, 0, 0, 0, 0, 0, 0, 0] {
found = true;
break;
}
offset += 8;
}
if !found {
break;
}
}
let record_len = read_u32_le(data, offset) as usize;
if !(USN_V4_MIN_SIZE..=USN_MAX_RECORD_SIZE).contains(&record_len) {
offset += 8;
continue;
}
if offset + record_len > len {
break;
}
let major_version = read_u16_le(data, offset + 4);
match major_version {
2 => {
if record_len < USN_V2_MIN_SIZE {
offset += 8;
continue;
}
if let Ok(record) = parse_usn_record_v2(&data[offset..offset + record_len]) {
records.push(record);
}
}
3 => {
if record_len < USN_V3_MIN_SIZE {
offset += 8;
continue;
}
if let Ok(record) = parse_usn_record_v3(&data[offset..offset + record_len]) {
records.push(record);
}
}
4 => {
}
_ => {
offset += 8;
continue;
}
}
let aligned = (record_len + 7) & !7;
offset += aligned;
}
Ok(records)
}
#[cfg(test)]
#[allow(clippy::unreadable_literal, clippy::cast_lossless)]
mod tests {
use super::*;
fn build_v2_record(
entry: u64,
seq: u16,
parent_entry: u64,
parent_seq: u16,
reason: u32,
filename: &str,
) -> 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 | ((seq as u64) << 48);
buf[0x08..0x10].copy_from_slice(&file_ref.to_le_bytes());
let parent_ref = parent_entry | ((parent_seq as u64) << 48);
buf[0x10..0x18].copy_from_slice(&parent_ref.to_le_bytes());
buf[0x18..0x20].copy_from_slice(&100i64.to_le_bytes());
let ts: i64 = 133500480000000000;
buf[0x20..0x28].copy_from_slice(&ts.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
}
#[test]
fn test_parse_v2_record() {
let data = build_v2_record(100, 3, 5, 5, 0x100, "test.txt");
let record = parse_usn_record_v2(&data).unwrap();
assert_eq!(record.mft_entry, 100);
assert_eq!(record.mft_sequence, 3);
assert_eq!(record.parent_mft_entry, 5);
assert_eq!(record.parent_mft_sequence, 5);
assert_eq!(record.filename, "test.txt");
assert!(record.reason.contains(UsnReason::FILE_CREATE));
assert_eq!(record.major_version, 2);
}
#[test]
fn test_parse_v2_unicode_filename() {
let data = build_v2_record(200, 1, 5, 5, 0x100, "日本語.txt");
let record = parse_usn_record_v2(&data).unwrap();
assert_eq!(record.filename, "日本語.txt");
}
#[test]
fn test_parse_journal_skips_zero_regions() {
let mut data = vec![0u8; 4096]; let record = build_v2_record(100, 1, 5, 5, 0x100, "after_gap.txt");
data.extend_from_slice(&record);
let records = parse_usn_journal(&data).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].filename, "after_gap.txt");
}
#[test]
fn test_parse_journal_multiple_records() {
let r1 = build_v2_record(100, 1, 5, 5, 0x100, "file1.txt");
let r2 = build_v2_record(200, 1, 100, 1, 0x200, "file2.txt");
let mut data = Vec::new();
data.extend_from_slice(&r1);
data.extend_from_slice(&r2);
let records = parse_usn_journal(&data).unwrap();
assert_eq!(records.len(), 2);
assert_eq!(records[0].filename, "file1.txt");
assert_eq!(records[1].filename, "file2.txt");
}
#[test]
fn test_parse_journal_includes_close_only() {
let r = build_v2_record(100, 1, 5, 5, 0x8000_0000, "closed.txt");
let records = parse_usn_journal(&r).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].reason, UsnReason::CLOSE);
}
#[test]
fn test_file_reference_extraction() {
let data = build_v2_record(12345, 7, 5, 5, 0x100, "x.txt");
let record = parse_usn_record_v2(&data).unwrap();
assert_eq!(record.mft_entry, 12345);
assert_eq!(record.mft_sequence, 7);
}
#[test]
fn test_parent_reference_extraction() {
let data = build_v2_record(100, 1, 983, 4, 0x100, "data.txt");
let record = parse_usn_record_v2(&data).unwrap();
assert_eq!(record.parent_mft_entry, 983);
assert_eq!(record.parent_mft_sequence, 4);
}
#[test]
fn test_reason_flags_preserved() {
let reason = 0x0000_0100 | 0x8000_0000; let data = build_v2_record(100, 1, 5, 5, reason, "x.txt");
let record = parse_usn_record_v2(&data).unwrap();
assert!(record.reason.contains(UsnReason::FILE_CREATE));
assert!(record.reason.contains(UsnReason::CLOSE));
}
#[test]
fn test_too_short_data_fails() {
let data = vec![0u8; 10];
assert!(parse_usn_record_v2(&data).is_err());
}
fn build_v3_record(entry: u64, parent_entry: u64, reason: u32, filename: &str) -> Vec<u8> {
let name_utf16: Vec<u16> = filename.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());
let file_ref_128: u128 = entry as u128;
buf[0x08..0x18].copy_from_slice(&file_ref_128.to_le_bytes());
let parent_ref_128: u128 = parent_entry as u128;
buf[0x18..0x28].copy_from_slice(&parent_ref_128.to_le_bytes());
buf[0x28..0x30].copy_from_slice(&200i64.to_le_bytes());
let ts: i64 = 133500480000000000;
buf[0x30..0x38].copy_from_slice(&ts.to_le_bytes());
buf[0x38..0x3C].copy_from_slice(&reason.to_le_bytes());
buf[0x3C..0x40].copy_from_slice(&0u32.to_le_bytes());
buf[0x40..0x44].copy_from_slice(&0u32.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());
}
buf
}
#[test]
fn test_parse_v3_record_basic() {
let data = build_v3_record(100, 5, 0x100, "v3_test.txt");
let record = parse_usn_record_v3(&data).unwrap();
assert_eq!(record.mft_entry, 100);
assert_eq!(record.mft_sequence, 0); assert_eq!(record.parent_mft_entry, 5);
assert_eq!(record.parent_mft_sequence, 0);
assert_eq!(record.filename, "v3_test.txt");
assert!(record.reason.contains(UsnReason::FILE_CREATE));
assert_eq!(record.major_version, 3);
}
#[test]
fn test_parse_v3_record_unicode() {
let data = build_v3_record(200, 5, 0x100, "日本語ファイル.txt");
let record = parse_usn_record_v3(&data).unwrap();
assert_eq!(record.filename, "日本語ファイル.txt");
assert_eq!(record.major_version, 3);
}
#[test]
fn test_parse_v3_too_short() {
let data = vec![0u8; 0x4B]; assert!(parse_usn_record_v3(&data).is_err());
}
#[test]
fn test_parse_v3_invalid_record_length_too_small() {
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());
assert!(parse_usn_record_v3(&data).is_err());
}
#[test]
fn test_parse_v3_invalid_record_length_too_large() {
let mut data = vec![0u8; 0x60];
data[0..4].copy_from_slice(&(70000u32).to_le_bytes());
data[4..6].copy_from_slice(&3u16.to_le_bytes());
assert!(parse_usn_record_v3(&data).is_err());
}
#[test]
fn test_parse_v3_zero_filename() {
let mut data = vec![0u8; 0x60];
let record_len = 0x4Cu32;
data[0..4].copy_from_slice(&record_len.to_le_bytes());
data[4..6].copy_from_slice(&3u16.to_le_bytes());
data[0x08..0x18].copy_from_slice(&100u128.to_le_bytes());
data[0x18..0x28].copy_from_slice(&5u128.to_le_bytes());
data[0x28..0x30].copy_from_slice(&100i64.to_le_bytes());
let ts: i64 = 133500480000000000;
data[0x30..0x38].copy_from_slice(&ts.to_le_bytes());
data[0x38..0x3C].copy_from_slice(&0x100u32.to_le_bytes());
data[0x48..0x4A].copy_from_slice(&0u16.to_le_bytes());
data[0x4A..0x4C].copy_from_slice(&0x4Cu16.to_le_bytes());
let record = parse_usn_record_v3(&data).unwrap();
assert_eq!(record.filename, "");
}
#[test]
fn test_parse_v2_invalid_record_length_too_small() {
let mut data = vec![0u8; 0x60];
data[0..4].copy_from_slice(&(0x30u32).to_le_bytes());
data[4..6].copy_from_slice(&2u16.to_le_bytes());
assert!(parse_usn_record_v2(&data).is_err());
}
#[test]
fn test_parse_v2_invalid_record_length_too_large() {
let mut data = vec![0u8; 0x60];
data[0..4].copy_from_slice(&(70000u32).to_le_bytes());
data[4..6].copy_from_slice(&2u16.to_le_bytes());
assert!(parse_usn_record_v2(&data).is_err());
}
#[test]
fn test_parse_v2_record_length_exceeds_data() {
let mut data = vec![0u8; 0x40];
data[0..4].copy_from_slice(&(0x100u32).to_le_bytes());
data[4..6].copy_from_slice(&2u16.to_le_bytes());
assert!(parse_usn_record_v2(&data).is_err());
}
#[test]
fn test_parse_v2_zero_filename() {
let mut data = vec![0u8; 0x40];
let record_len = 0x3Cu32;
data[0..4].copy_from_slice(&record_len.to_le_bytes());
data[4..6].copy_from_slice(&2u16.to_le_bytes());
let file_ref = 100u64 + (3u64 << 48);
data[0x08..0x10].copy_from_slice(&file_ref.to_le_bytes());
let parent_ref = 5u64 | (5u64 << 48);
data[0x10..0x18].copy_from_slice(&parent_ref.to_le_bytes());
data[0x18..0x20].copy_from_slice(&100i64.to_le_bytes());
let ts: i64 = 133500480000000000;
data[0x20..0x28].copy_from_slice(&ts.to_le_bytes());
data[0x28..0x2C].copy_from_slice(&0x100u32.to_le_bytes());
data[0x34..0x38].copy_from_slice(&0x20u32.to_le_bytes());
data[0x38..0x3A].copy_from_slice(&0u16.to_le_bytes());
data[0x3A..0x3C].copy_from_slice(&0x3Cu16.to_le_bytes());
let record = parse_usn_record_v2(&data).unwrap();
assert_eq!(record.filename, "");
}
#[test]
fn test_parse_v2_zero_timestamp() {
let mut data = build_v2_record(100, 1, 5, 5, 0x100, "t.txt");
data[0x20..0x28].copy_from_slice(&0i64.to_le_bytes());
let record = parse_usn_record_v2(&data).unwrap();
assert_eq!(record.timestamp.timestamp(), 0);
}
#[test]
fn test_parse_v2_negative_timestamp() {
let mut data = build_v2_record(100, 1, 5, 5, 0x100, "t.txt");
data[0x20..0x28].copy_from_slice(&(-1i64).to_le_bytes());
let record = parse_usn_record_v2(&data).unwrap();
assert_eq!(record.timestamp.timestamp(), 0);
}
#[test]
fn test_parse_v2_pre_epoch_timestamp() {
let mut data = build_v2_record(100, 1, 5, 5, 0x100, "old.txt");
let ts: i64 = 100_000_000_000_000_000; data[0x20..0x28].copy_from_slice(&ts.to_le_bytes());
let record = parse_usn_record_v2(&data).unwrap();
assert_eq!(record.timestamp.timestamp(), 0);
}
#[test]
fn test_parse_v2_filename_out_of_bounds() {
let mut data = build_v2_record(100, 1, 5, 5, 0x100, "ok.txt");
data[0x38..0x3A].copy_from_slice(&(5000u16).to_le_bytes());
let record = parse_usn_record_v2(&data).unwrap();
assert_eq!(record.filename, ""); }
#[test]
fn test_parse_v2_filename_length_one_byte() {
let mut data = build_v2_record(100, 1, 5, 5, 0x100, "x.txt");
data[0x38..0x3A].copy_from_slice(&1u16.to_le_bytes());
let record = parse_usn_record_v2(&data).unwrap();
assert_eq!(record.filename, "");
}
#[test]
fn test_parse_journal_v3_records() {
let r = build_v3_record(100, 5, 0x100, "v3file.txt");
let records = parse_usn_journal(&r).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].filename, "v3file.txt");
assert_eq!(records[0].major_version, 3);
}
#[test]
fn test_parse_journal_mixed_v2_v3() {
let mut data = Vec::new();
data.extend_from_slice(&build_v2_record(100, 1, 5, 5, 0x100, "v2.txt"));
data.extend_from_slice(&build_v3_record(200, 5, 0x200, "v3.txt"));
let records = parse_usn_journal(&data).unwrap();
assert_eq!(records.len(), 2);
assert_eq!(records[0].major_version, 2);
assert_eq!(records[1].major_version, 3);
}
#[test]
fn test_parse_journal_v4_record_skipped() {
let record_len = 0x38u32; let aligned_len = ((record_len as usize) + 7) & !7;
let mut data = vec![0u8; aligned_len];
data[0..4].copy_from_slice(&record_len.to_le_bytes());
data[4..6].copy_from_slice(&4u16.to_le_bytes());
let records = parse_usn_journal(&data).unwrap();
assert_eq!(records.len(), 0);
}
#[test]
fn test_parse_journal_unknown_version_skipped() {
let record_len = 0x40u32;
let aligned_len = ((record_len as usize) + 7) & !7;
let mut data = vec![0u8; aligned_len];
data[0..4].copy_from_slice(&record_len.to_le_bytes());
data[4..6].copy_from_slice(&99u16.to_le_bytes());
let records = parse_usn_journal(&data).unwrap();
assert_eq!(records.len(), 0);
}
#[test]
fn test_parse_journal_invalid_record_length_scan_forward() {
let mut data = vec![0u8; 16];
data[0..4].copy_from_slice(&3u32.to_le_bytes()); data[4..6].copy_from_slice(&2u16.to_le_bytes());
let records = parse_usn_journal(&data).unwrap();
assert_eq!(records.len(), 0);
}
#[test]
fn test_parse_journal_record_extends_past_end() {
let mut data = vec![0u8; 0x100];
data[0..4].copy_from_slice(&(0x1000u32).to_le_bytes());
data[4..6].copy_from_slice(&2u16.to_le_bytes());
let records = parse_usn_journal(&data).unwrap();
assert_eq!(records.len(), 0);
}
#[test]
fn test_parse_journal_empty_data() {
let records = parse_usn_journal(&[]).unwrap();
assert_eq!(records.len(), 0);
}
#[test]
fn test_parse_journal_all_zeros() {
let data = vec![0u8; 4096];
let records = parse_usn_journal(&data).unwrap();
assert_eq!(records.len(), 0);
}
#[test]
fn test_parse_journal_v2_too_small_for_v2_but_valid_len() {
let mut data = vec![0u8; 0x40];
data[0..4].copy_from_slice(&(0x3Au32).to_le_bytes()); data[4..6].copy_from_slice(&2u16.to_le_bytes());
let records = parse_usn_journal(&data).unwrap();
assert_eq!(records.len(), 0);
}
#[test]
fn test_parse_journal_v3_too_small_for_v3_but_valid_len() {
let mut data = vec![0u8; 0x50];
data[0..4].copy_from_slice(&(0x40u32).to_le_bytes()); data[4..6].copy_from_slice(&3u16.to_le_bytes());
let records = parse_usn_journal(&data).unwrap();
assert_eq!(records.len(), 0);
}
#[test]
fn test_parse_journal_v3_close_only_included() {
let r = build_v3_record(100, 5, 0x8000_0000, "closed.txt");
let records = parse_usn_journal(&r).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].reason, UsnReason::CLOSE);
}
#[test]
fn test_parse_journal_8byte_alignment() {
let r1 = build_v2_record(100, 1, 5, 5, 0x100, "a.txt");
assert_eq!(r1.len() % 8, 0); let r2 = build_v2_record(200, 1, 5, 5, 0x200, "b.txt");
let mut data = Vec::new();
data.extend_from_slice(&r1);
data.extend_from_slice(&r2);
let records = parse_usn_journal(&data).unwrap();
assert_eq!(records.len(), 2);
}
#[test]
fn test_filetime_to_datetime_zero() {
assert!(filetime_to_datetime(0).is_none());
}
#[test]
fn test_filetime_to_datetime_negative() {
assert!(filetime_to_datetime(-100).is_none());
}
#[test]
fn test_filetime_to_datetime_pre_unix_epoch() {
let pre_unix: i64 = 100_000_000_000_000_000;
assert!(filetime_to_datetime(pre_unix).is_none());
}
#[test]
fn test_filetime_to_datetime_valid() {
let ts: i64 = 133500480000000000;
let dt = filetime_to_datetime(ts);
assert!(dt.is_some());
let dt = dt.unwrap();
assert_eq!(dt.format("%Y").to_string(), "2024");
}
#[test]
fn test_parse_journal_v3_parse_error_skipped() {
let record_len = 0x4Cu32;
let aligned_len = ((record_len as usize) + 7) & !7;
let mut data = vec![0u8; aligned_len];
data[0..4].copy_from_slice(&record_len.to_le_bytes());
data[4..6].copy_from_slice(&3u16.to_le_bytes()); data[0..4].copy_from_slice(&(0x30u32).to_le_bytes());
let mut data2 = [0u8; 0x50]; data2[0..4].copy_from_slice(&(0x4Cu32).to_le_bytes()); data2[4..6].copy_from_slice(&3u16.to_le_bytes()); data2[0x48..0x4A].copy_from_slice(&0xFFu16.to_le_bytes()); data2[0x4A..0x4C].copy_from_slice(&0x4Cu16.to_le_bytes());
let mut data3 = [0u8; 0x60]; data3[0..4].copy_from_slice(&(0x50u32).to_le_bytes());
data3[4..6].copy_from_slice(&3u16.to_le_bytes());
let short_data = vec![1u8; 5]; let records = parse_usn_journal(&short_data).unwrap();
assert_eq!(records.len(), 0);
}
#[test]
fn test_parse_journal_partial_data_after_zeros() {
let mut data = vec![0u8; 64]; data.extend_from_slice(&[1, 2, 3, 4]);
let records = parse_usn_journal(&data).unwrap();
assert_eq!(records.len(), 0);
}
#[test]
fn test_read_u128_le() {
let mut data = [0u8; 16];
let val: u128 = 0x0102030405060708090A0B0C0D0E0F10;
data.copy_from_slice(&val.to_le_bytes());
assert_eq!(read_u128_le(&data, 0), val);
}
#[test]
fn test_read_u64_le() {
let mut data = [0u8; 8];
let val: u64 = 0x0102030405060708;
data.copy_from_slice(&val.to_le_bytes());
assert_eq!(read_u64_le(&data, 0), val);
}
#[test]
fn test_read_i64_le() {
let mut data = [0u8; 8];
let val: i64 = -12345;
data.copy_from_slice(&val.to_le_bytes());
assert_eq!(read_i64_le(&data, 0), val);
}
#[test]
fn test_read_u32_le() {
let mut data = [0u8; 4];
data.copy_from_slice(&42u32.to_le_bytes());
assert_eq!(read_u32_le(&data, 0), 42);
}
#[test]
fn test_read_u16_le() {
let mut data = [0u8; 2];
data.copy_from_slice(&1234u16.to_le_bytes());
assert_eq!(read_u16_le(&data, 0), 1234);
}
#[test]
fn test_parse_journal_boundary_after_zero_skip() {
let mut data = vec![0u8; 8];
data[0..4].copy_from_slice(&(0x3Au32).to_le_bytes()); data[4..6].copy_from_slice(&2u16.to_le_bytes());
let records = parse_usn_journal(&data).unwrap();
assert_eq!(records.len(), 0);
}
#[test]
fn test_parse_journal_zeros_then_exactly_8_non_zero_bytes() {
let mut data = vec![0u8; 64]; data.extend_from_slice(&[0x3A, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00]);
let records = parse_usn_journal(&data).unwrap();
assert_eq!(records.len(), 0);
}
#[test]
fn test_parse_journal_v2_parse_error_path() {
let mut data = vec![0u8; 0x3C]; data[0..4].copy_from_slice(&(0x100u32).to_le_bytes()); data[4..6].copy_from_slice(&2u16.to_le_bytes());
let result = parse_usn_record_v2(&data);
assert!(result.is_err());
let mut data2 = vec![0u8; 0x40]; data2[0..4].copy_from_slice(&(0x3Cu32).to_le_bytes()); data2[4..6].copy_from_slice(&2u16.to_le_bytes()); let ts: i64 = 133_500_480_000_000_000;
data2[0x20..0x28].copy_from_slice(&ts.to_le_bytes()); data2[0x3A..0x3C].copy_from_slice(&0x3Cu16.to_le_bytes());
let records = parse_usn_journal(&data2).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].filename, "");
}
#[test]
fn test_parse_journal_v3_parse_error_path() {
let mut data = vec![0u8; 0x4C]; data[0..4].copy_from_slice(&(0x30u32).to_le_bytes()); data[4..6].copy_from_slice(&3u16.to_le_bytes());
let result = parse_usn_record_v3(&data);
assert!(result.is_err());
let mut data1b = vec![0u8; 0x4C];
data1b[0..4].copy_from_slice(&(70000u32).to_le_bytes()); data1b[4..6].copy_from_slice(&3u16.to_le_bytes());
let result1b = parse_usn_record_v3(&data1b);
assert!(result1b.is_err());
let mut data2 = vec![0u8; 0x50]; data2[0..4].copy_from_slice(&(0x4Cu32).to_le_bytes()); data2[4..6].copy_from_slice(&3u16.to_le_bytes()); let ts: i64 = 133_500_480_000_000_000;
data2[0x30..0x38].copy_from_slice(&ts.to_le_bytes()); data2[0x4A..0x4C].copy_from_slice(&0x4Cu16.to_le_bytes());
let records = parse_usn_journal(&data2).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].filename, "");
}
#[test]
fn test_parse_journal_skips_invalid_unknown_and_v4_records() {
let mut data = Vec::new();
let mut invalid_len = vec![0u8; 8];
invalid_len[0..4].copy_from_slice(&3u32.to_le_bytes()); invalid_len[4] = 0xFF; data.extend_from_slice(&invalid_len);
let mut unknown_ver = vec![0u8; 0x40];
unknown_ver[0..4].copy_from_slice(&(0x40u32).to_le_bytes());
unknown_ver[4..6].copy_from_slice(&99u16.to_le_bytes());
data.extend_from_slice(&unknown_ver);
let mut v4_record = vec![0u8; 0x38];
v4_record[0..4].copy_from_slice(&(0x38u32).to_le_bytes());
v4_record[4..6].copy_from_slice(&4u16.to_le_bytes());
data.extend_from_slice(&v4_record);
let mut extending = vec![0u8; 16];
extending[0..4].copy_from_slice(&(0x1000u32).to_le_bytes());
extending[4..6].copy_from_slice(&2u16.to_le_bytes());
data.extend_from_slice(&extending);
let records = parse_usn_journal(&data).unwrap();
assert_eq!(records.len(), 0);
}
#[test]
fn test_parse_v2_record_len_exceeds_data_len() {
let mut data = vec![0u8; 0x3C]; data[0..4].copy_from_slice(&(0x3Cu32).to_le_bytes());
data[4..6].copy_from_slice(&2u16.to_le_bytes());
let result = parse_usn_record_v2(&data);
assert!(result.is_ok());
let mut data2 = vec![0u8; 0x3C];
data2[0..4].copy_from_slice(&(0x3Du32).to_le_bytes()); data2[4..6].copy_from_slice(&2u16.to_le_bytes());
let result2 = parse_usn_record_v2(&data2);
assert!(result2.is_err());
}
#[test]
fn test_parse_v3_record_len_out_of_range() {
let mut data = vec![0u8; 0x4C];
data[0..4].copy_from_slice(&(0x4Bu32).to_le_bytes()); data[4..6].copy_from_slice(&3u16.to_le_bytes());
let result = parse_usn_record_v3(&data);
assert!(result.is_err());
let mut data2 = vec![0u8; 0x4C];
data2[0..4].copy_from_slice(&(65537u32).to_le_bytes());
data2[4..6].copy_from_slice(&3u16.to_le_bytes());
let result2 = parse_usn_record_v3(&data2);
assert!(result2.is_err());
let mut data3 = vec![0u8; 0x4B]; data3[0..4].copy_from_slice(&(0x4Cu32).to_le_bytes());
data3[4..6].copy_from_slice(&3u16.to_le_bytes());
let result3 = parse_usn_record_v3(&data3);
assert!(result3.is_err());
}
}