const FILE_SIGNATURE: [u8; 4] = [0x46, 0x49, 0x4C, 0x45];
const MFT_ENTRY_SIZE: usize = 1024;
const ATTR_FILE_NAME: u32 = 0x30;
const ATTR_END_MARKER: u32 = 0xFFFF_FFFF;
const MAX_ATTR_WALK: usize = 20;
const NS_WIN32: u8 = 1;
const NS_WIN32_AND_DOS: u8 = 3;
const NS_DOS: u8 = 2;
#[derive(Debug, Clone)]
pub struct CarvedMftEntry {
pub offset: usize,
pub entry_number: u64,
pub sequence_number: u16,
pub filename: String,
pub parent_entry: u64,
pub parent_sequence: u16,
pub is_directory: bool,
pub is_in_use: bool,
}
#[derive(Debug, Clone, Default)]
pub struct MftCarvingStats {
pub bytes_scanned: usize,
pub candidates_examined: u64,
pub entries_carved: usize,
pub rejected: u64,
}
pub fn carve_mft_entries(data: &[u8]) -> (Vec<CarvedMftEntry>, MftCarvingStats) {
let mut results = Vec::new();
let mut stats = MftCarvingStats {
bytes_scanned: data.len(),
..Default::default()
};
let len = data.len();
let mut offset = 0;
while offset + MFT_ENTRY_SIZE <= len {
if data[offset..offset + 4] == FILE_SIGNATURE {
stats.candidates_examined += 1;
if let Some(entry) = try_carve_entry(data, offset, &mut stats) {
results.push(entry);
}
}
offset += MFT_ENTRY_SIZE;
}
stats.entries_carved = results.len();
(results, stats)
}
fn try_carve_entry(
data: &[u8],
offset: usize,
stats: &mut MftCarvingStats,
) -> Option<CarvedMftEntry> {
let entry = &data[offset..offset + MFT_ENTRY_SIZE];
let sequence_number = read_u16_le(entry, 16);
if sequence_number == 0 {
stats.rejected += 1;
return None;
}
let first_attr_offset = read_u16_le(entry, 20) as usize;
if !(48..MFT_ENTRY_SIZE - 8).contains(&first_attr_offset) {
stats.rejected += 1;
return None;
}
let flags = read_u16_le(entry, 22);
let entry_number = u64::from(read_u32_le(entry, 44));
let mut best_filename: Option<(String, u64, u16, u8)> = None; let mut attr_offset = first_attr_offset;
let mut attrs_walked = 0;
while attr_offset + 8 <= MFT_ENTRY_SIZE && attrs_walked < MAX_ATTR_WALK {
let attr_type = read_u32_le(entry, attr_offset);
if attr_type == ATTR_END_MARKER {
break;
}
let attr_len = read_u32_le(entry, attr_offset + 4) as usize;
if attr_len < 8 || attr_offset + attr_len > MFT_ENTRY_SIZE {
break; }
let is_resident_filename =
attr_type == ATTR_FILE_NAME && entry.get(attr_offset + 8) == Some(&0);
if let Some((name, parent_e, parent_s, ns)) = is_resident_filename
.then(|| parse_filename_attr(entry, attr_offset))
.flatten()
{
let dominated = match &best_filename {
None => true,
Some((_, _, _, prev_ns)) => {
*prev_ns == NS_DOS && (ns == NS_WIN32 || ns == NS_WIN32_AND_DOS)
|| *prev_ns != NS_WIN32 && *prev_ns != NS_WIN32_AND_DOS && ns != NS_DOS
}
};
if dominated {
best_filename = Some((name, parent_e, parent_s, ns));
}
}
attr_offset += attr_len;
attrs_walked += 1;
}
if let Some((filename, parent_entry, parent_sequence, _)) = best_filename {
Some(CarvedMftEntry {
offset,
entry_number,
sequence_number,
filename,
parent_entry,
parent_sequence,
is_directory: flags & 0x02 != 0,
is_in_use: flags & 0x01 != 0,
})
} else {
stats.rejected += 1;
None
}
}
fn parse_filename_attr(entry: &[u8], attr_offset: usize) -> Option<(String, u64, u16, u8)> {
let content_offset = read_u16_le(entry, attr_offset + 20) as usize;
let content_size = read_u32_le(entry, attr_offset + 16) as usize;
let fn_start = attr_offset + content_offset;
if fn_start + 66 > entry.len() || content_size < 66 {
return None;
}
let parent_ref = read_u64_le(entry, fn_start);
let parent_entry = parent_ref & 0x0000_FFFF_FFFF_FFFF;
let parent_sequence = (parent_ref >> 48) as u16;
let name_len_chars = entry[fn_start + 64] as usize;
let namespace = entry[fn_start + 65];
if name_len_chars == 0 {
return None;
}
let name_bytes_start = fn_start + 66;
let name_bytes_end = name_bytes_start + name_len_chars * 2;
if name_bytes_end > entry.len()
|| name_bytes_end > attr_offset + read_u32_le(entry, attr_offset + 4) as usize
{
return None;
}
let name_u16: Vec<u16> = (0..name_len_chars)
.map(|i| read_u16_le(entry, name_bytes_start + i * 2))
.collect();
let filename = String::from_utf16_lossy(&name_u16);
Some((filename, parent_entry, parent_sequence, namespace))
}
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_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],
])
}
#[cfg(test)]
#[allow(clippy::cast_lossless)]
mod tests {
use super::*;
fn build_mft_entry(
entry_number: u32,
sequence: u16,
parent_entry: u64,
parent_sequence: u16,
filename: &str,
flags: u16, ) -> Vec<u8> {
let mut buf = vec![0u8; MFT_ENTRY_SIZE];
buf[0..4].copy_from_slice(&FILE_SIGNATURE);
buf[4..6].copy_from_slice(&48u16.to_le_bytes());
buf[6..8].copy_from_slice(&3u16.to_le_bytes());
buf[8..16].copy_from_slice(&1u64.to_le_bytes());
buf[16..18].copy_from_slice(&sequence.to_le_bytes());
buf[18..20].copy_from_slice(&1u16.to_le_bytes());
let first_attr_offset: u16 = 56;
buf[20..22].copy_from_slice(&first_attr_offset.to_le_bytes());
buf[22..24].copy_from_slice(&flags.to_le_bytes());
buf[24..28].copy_from_slice(&512u32.to_le_bytes());
buf[28..32].copy_from_slice(&1024u32.to_le_bytes());
buf[40..42].copy_from_slice(&2u16.to_le_bytes());
buf[44..48].copy_from_slice(&entry_number.to_le_bytes());
buf[48..50].copy_from_slice(&0x0001u16.to_le_bytes());
buf[50..52].copy_from_slice(&0x0000u16.to_le_bytes());
buf[52..54].copy_from_slice(&0x0000u16.to_le_bytes());
write_filename_attr(
&mut buf,
first_attr_offset as usize,
parent_entry,
parent_sequence,
filename,
NS_WIN32_AND_DOS,
);
buf
}
fn write_filename_attr(
buf: &mut [u8],
attr_start: usize,
parent_entry: u64,
parent_sequence: u16,
filename: &str,
namespace: u8,
) -> usize {
let name_utf16: Vec<u16> = filename.encode_utf16().collect();
let name_bytes_len = name_utf16.len() * 2;
let fn_content_size = 66 + name_bytes_len;
let content_offset: u16 = 24;
let attr_size = (content_offset as usize + fn_content_size + 7) & !7;
buf[attr_start..attr_start + 4].copy_from_slice(&ATTR_FILE_NAME.to_le_bytes());
buf[attr_start + 4..attr_start + 8].copy_from_slice(&(attr_size as u32).to_le_bytes());
buf[attr_start + 8] = 0; buf[attr_start + 9] = 0; buf[attr_start + 10..attr_start + 12].copy_from_slice(&0x18u16.to_le_bytes());
buf[attr_start + 16..attr_start + 20]
.copy_from_slice(&(fn_content_size as u32).to_le_bytes());
buf[attr_start + 20..attr_start + 22].copy_from_slice(&content_offset.to_le_bytes());
let fn_start = attr_start + content_offset as usize;
let parent_ref = parent_entry | ((parent_sequence as u64) << 48);
buf[fn_start..fn_start + 8].copy_from_slice(&parent_ref.to_le_bytes());
let ts: i64 = 133_500_480_000_000_000;
for i in 0..4 {
let off = fn_start + 8 + i * 8;
buf[off..off + 8].copy_from_slice(&ts.to_le_bytes());
}
buf[fn_start + 64] = name_utf16.len() as u8;
buf[fn_start + 65] = namespace;
for (i, &ch) in name_utf16.iter().enumerate() {
let off = fn_start + 66 + i * 2;
buf[off..off + 2].copy_from_slice(&ch.to_le_bytes());
}
let end_offset = attr_start + attr_size;
if end_offset + 4 <= buf.len() {
buf[end_offset..end_offset + 4].copy_from_slice(&ATTR_END_MARKER.to_le_bytes());
}
attr_size
}
#[test]
fn test_carve_empty_data() {
let (entries, stats) = carve_mft_entries(&[]);
assert_eq!(entries.len(), 0);
assert_eq!(stats.bytes_scanned, 0);
}
#[test]
fn test_carve_filename_dos_then_posix_domination() {
let mut buf = build_mft_entry(500, 1, 5, 5, "placeholder", 0x01);
let s0 = write_filename_attr(&mut buf, 56, 5, 5, "DOS~1.TXT", NS_DOS);
let s1 = write_filename_attr(&mut buf, 56 + s0, 5, 5, "longposix.txt", 0); let end = 56 + s0 + s1;
buf[end..end + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
let (entries, _) = carve_mft_entries(&buf);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].filename, "longposix.txt");
}
#[test]
fn test_carve_all_zeros() {
let data = vec![0u8; 8192];
let (entries, stats) = carve_mft_entries(&data);
assert_eq!(entries.len(), 0);
assert_eq!(stats.bytes_scanned, 8192);
}
#[test]
fn test_carve_random_garbage() {
let mut data = vec![0xDE; 4096];
for i in (0..data.len()).step_by(7) {
data[i] = (i % 256) as u8;
}
let (entries, _) = carve_mft_entries(&data);
assert_eq!(entries.len(), 0);
}
#[test]
fn test_carve_single_entry() {
let entry = build_mft_entry(42, 3, 5, 1, "malware.exe", 0x01);
let (entries, stats) = carve_mft_entries(&entry);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].entry_number, 42);
assert_eq!(entries[0].sequence_number, 3);
assert_eq!(entries[0].filename, "malware.exe");
assert_eq!(entries[0].parent_entry, 5);
assert_eq!(entries[0].parent_sequence, 1);
assert!(entries[0].is_in_use);
assert!(!entries[0].is_directory);
assert_eq!(entries[0].offset, 0);
assert_eq!(stats.entries_carved, 1);
assert_eq!(stats.candidates_examined, 1);
}
#[test]
fn test_carve_directory_entry() {
let entry = build_mft_entry(100, 1, 5, 1, "Documents", 0x03); let (entries, _) = carve_mft_entries(&entry);
assert_eq!(entries.len(), 1);
assert!(entries[0].is_directory);
assert!(entries[0].is_in_use);
assert_eq!(entries[0].filename, "Documents");
}
#[test]
fn test_carve_deleted_entry() {
let entry = build_mft_entry(200, 5, 100, 2, "deleted.tmp", 0x00);
let (entries, _) = carve_mft_entries(&entry);
assert_eq!(entries.len(), 1);
assert!(!entries[0].is_in_use);
assert_eq!(entries[0].filename, "deleted.tmp");
}
#[test]
fn test_carve_entry_embedded_in_garbage() {
let mut data = vec![0xAA; 2048];
let entry = build_mft_entry(77, 2, 5, 1, "evidence.docx", 0x01);
let entry_offset = data.len();
data.extend_from_slice(&entry);
data.extend_from_slice(&vec![0xBB; 2048]);
let (entries, stats) = carve_mft_entries(&data);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].offset, entry_offset);
assert_eq!(entries[0].entry_number, 77);
assert_eq!(entries[0].filename, "evidence.docx");
assert!(stats.candidates_examined >= 1);
}
#[test]
fn test_carve_multiple_entries_with_gaps() {
let mut data = Vec::new();
let e1 = build_mft_entry(10, 1, 5, 1, "first.txt", 0x01);
data.extend_from_slice(&e1);
data.extend_from_slice(&vec![0x00; 2048]);
let e2 = build_mft_entry(20, 2, 10, 1, "second.doc", 0x01);
let e2_offset = data.len();
data.extend_from_slice(&e2);
let e3 = build_mft_entry(30, 1, 5, 1, "third.pdf", 0x03);
let e3_offset = data.len();
data.extend_from_slice(&e3);
let (entries, stats) = carve_mft_entries(&data);
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].offset, 0);
assert_eq!(entries[0].filename, "first.txt");
assert_eq!(entries[1].offset, e2_offset);
assert_eq!(entries[1].filename, "second.doc");
assert_eq!(entries[2].offset, e3_offset);
assert_eq!(entries[2].filename, "third.pdf");
assert_eq!(stats.entries_carved, 3);
}
#[test]
fn test_carve_rejects_zero_sequence() {
let mut entry = build_mft_entry(42, 0, 5, 1, "test.txt", 0x01);
entry[16..18].copy_from_slice(&0u16.to_le_bytes());
let (entries, stats) = carve_mft_entries(&entry);
assert_eq!(entries.len(), 0);
assert!(stats.rejected > 0);
}
#[test]
fn test_carve_rejects_bad_first_attr_offset() {
let mut entry = build_mft_entry(42, 1, 5, 1, "test.txt", 0x01);
entry[20..22].copy_from_slice(&2000u16.to_le_bytes());
let (entries, stats) = carve_mft_entries(&entry);
assert_eq!(entries.len(), 0);
assert!(stats.rejected > 0);
}
#[test]
fn test_carve_rejects_no_filename_attr() {
let mut entry = vec![0u8; MFT_ENTRY_SIZE];
entry[0..4].copy_from_slice(&FILE_SIGNATURE);
entry[4..6].copy_from_slice(&48u16.to_le_bytes());
entry[6..8].copy_from_slice(&3u16.to_le_bytes());
entry[16..18].copy_from_slice(&1u16.to_le_bytes()); entry[20..22].copy_from_slice(&56u16.to_le_bytes()); entry[22..24].copy_from_slice(&0x01u16.to_le_bytes()); entry[24..28].copy_from_slice(&512u32.to_le_bytes());
entry[28..32].copy_from_slice(&1024u32.to_le_bytes());
entry[44..48].copy_from_slice(&42u32.to_le_bytes());
entry[56..60].copy_from_slice(&ATTR_END_MARKER.to_le_bytes());
let (entries, stats) = carve_mft_entries(&entry);
assert_eq!(entries.len(), 0);
assert!(stats.rejected > 0);
}
#[test]
fn test_carve_rejects_truncated_data() {
let mut data = vec![0u8; 512];
data[0..4].copy_from_slice(&FILE_SIGNATURE);
data[16..18].copy_from_slice(&1u16.to_le_bytes());
let (entries, _) = carve_mft_entries(&data);
assert_eq!(entries.len(), 0);
}
#[test]
fn test_carve_ignores_non_aligned_file_signature() {
let mut data = vec![0u8; 2048];
data[500..504].copy_from_slice(&FILE_SIGNATURE);
data[516..518].copy_from_slice(&1u16.to_le_bytes());
let (entries, _) = carve_mft_entries(&data);
assert_eq!(entries.len(), 0);
}
#[test]
fn test_carve_prefers_win32_over_dos_name() {
let mut buf = vec![0u8; MFT_ENTRY_SIZE];
buf[0..4].copy_from_slice(&FILE_SIGNATURE);
buf[4..6].copy_from_slice(&48u16.to_le_bytes());
buf[6..8].copy_from_slice(&3u16.to_le_bytes());
buf[8..16].copy_from_slice(&1u64.to_le_bytes());
buf[16..18].copy_from_slice(&1u16.to_le_bytes());
buf[18..20].copy_from_slice(&1u16.to_le_bytes());
let first_attr: u16 = 56;
buf[20..22].copy_from_slice(&first_attr.to_le_bytes());
buf[22..24].copy_from_slice(&0x01u16.to_le_bytes());
buf[24..28].copy_from_slice(&800u32.to_le_bytes());
buf[28..32].copy_from_slice(&1024u32.to_le_bytes());
buf[44..48].copy_from_slice(&99u32.to_le_bytes());
buf[48..50].copy_from_slice(&0x0001u16.to_le_bytes());
let attr1_size =
write_filename_attr(&mut buf, first_attr as usize, 5, 1, "IMPORT~1.XLS", NS_DOS);
let attr2_start = first_attr as usize + attr1_size;
write_filename_attr(
&mut buf,
attr2_start,
5,
1,
"Important Spreadsheet.xlsx",
NS_WIN32,
);
let (entries, _) = carve_mft_entries(&buf);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].filename, "Important Spreadsheet.xlsx");
}
#[test]
fn test_carving_stats() {
let mut data = Vec::new();
data.extend_from_slice(&build_mft_entry(10, 1, 5, 1, "valid.txt", 0x01));
let mut bad = build_mft_entry(20, 0, 5, 1, "bad.txt", 0x01);
bad[16..18].copy_from_slice(&0u16.to_le_bytes());
data.extend_from_slice(&bad);
let (_, stats) = carve_mft_entries(&data);
assert_eq!(stats.bytes_scanned, data.len());
assert_eq!(stats.candidates_examined, 2); assert_eq!(stats.entries_carved, 1);
assert_eq!(stats.rejected, 1);
}
#[test]
fn test_stats_default() {
let stats = MftCarvingStats::default();
assert_eq!(stats.bytes_scanned, 0);
assert_eq!(stats.candidates_examined, 0);
assert_eq!(stats.entries_carved, 0);
assert_eq!(stats.rejected, 0);
}
#[test]
fn test_carve_corrupt_attr_chain_short_attr_len() {
let mut buf = vec![0u8; MFT_ENTRY_SIZE];
buf[0..4].copy_from_slice(&FILE_SIGNATURE);
buf[4..6].copy_from_slice(&48u16.to_le_bytes());
buf[6..8].copy_from_slice(&3u16.to_le_bytes());
buf[8..16].copy_from_slice(&1u64.to_le_bytes());
buf[16..18].copy_from_slice(&1u16.to_le_bytes()); buf[18..20].copy_from_slice(&1u16.to_le_bytes());
let first_attr: u16 = 56;
buf[20..22].copy_from_slice(&first_attr.to_le_bytes());
buf[22..24].copy_from_slice(&0x01u16.to_le_bytes()); buf[24..28].copy_from_slice(&512u32.to_le_bytes());
buf[28..32].copy_from_slice(&1024u32.to_le_bytes());
buf[44..48].copy_from_slice(&42u32.to_le_bytes());
buf[48..50].copy_from_slice(&0x0001u16.to_le_bytes());
buf[56..60].copy_from_slice(&0x10u32.to_le_bytes()); buf[60..64].copy_from_slice(&4u32.to_le_bytes());
let (entries, stats) = carve_mft_entries(&buf);
assert_eq!(entries.len(), 0);
assert!(stats.rejected > 0);
}
#[test]
fn test_carve_attr_exceeds_entry_boundary() {
let mut buf = vec![0u8; MFT_ENTRY_SIZE];
buf[0..4].copy_from_slice(&FILE_SIGNATURE);
buf[4..6].copy_from_slice(&48u16.to_le_bytes());
buf[6..8].copy_from_slice(&3u16.to_le_bytes());
buf[8..16].copy_from_slice(&1u64.to_le_bytes());
buf[16..18].copy_from_slice(&1u16.to_le_bytes());
buf[18..20].copy_from_slice(&1u16.to_le_bytes());
let first_attr: u16 = 56;
buf[20..22].copy_from_slice(&first_attr.to_le_bytes());
buf[22..24].copy_from_slice(&0x01u16.to_le_bytes());
buf[24..28].copy_from_slice(&512u32.to_le_bytes());
buf[28..32].copy_from_slice(&1024u32.to_le_bytes());
buf[44..48].copy_from_slice(&42u32.to_le_bytes());
buf[48..50].copy_from_slice(&0x0001u16.to_le_bytes());
buf[56..60].copy_from_slice(&0x10u32.to_le_bytes()); buf[60..64].copy_from_slice(&2000u32.to_le_bytes());
let (entries, stats) = carve_mft_entries(&buf);
assert_eq!(entries.len(), 0);
assert!(stats.rejected > 0);
}
#[test]
fn test_carve_non_resident_filename_attr_skipped() {
let mut buf = vec![0u8; MFT_ENTRY_SIZE];
buf[0..4].copy_from_slice(&FILE_SIGNATURE);
buf[4..6].copy_from_slice(&48u16.to_le_bytes());
buf[6..8].copy_from_slice(&3u16.to_le_bytes());
buf[8..16].copy_from_slice(&1u64.to_le_bytes());
buf[16..18].copy_from_slice(&1u16.to_le_bytes());
buf[18..20].copy_from_slice(&1u16.to_le_bytes());
let first_attr: u16 = 56;
buf[20..22].copy_from_slice(&first_attr.to_le_bytes());
buf[22..24].copy_from_slice(&0x01u16.to_le_bytes());
buf[24..28].copy_from_slice(&512u32.to_le_bytes());
buf[28..32].copy_from_slice(&1024u32.to_le_bytes());
buf[44..48].copy_from_slice(&42u32.to_le_bytes());
buf[48..50].copy_from_slice(&0x0001u16.to_le_bytes());
let attr_size = 96u32;
buf[56..60].copy_from_slice(&ATTR_FILE_NAME.to_le_bytes()); buf[60..64].copy_from_slice(&attr_size.to_le_bytes());
buf[64] = 1;
let end_off = 56 + attr_size as usize;
buf[end_off..end_off + 4].copy_from_slice(&ATTR_END_MARKER.to_le_bytes());
let (entries, stats) = carve_mft_entries(&buf);
assert_eq!(entries.len(), 0);
assert!(stats.rejected > 0);
}
#[test]
fn test_carve_filename_attr_content_too_short() {
let mut buf = vec![0u8; MFT_ENTRY_SIZE];
buf[0..4].copy_from_slice(&FILE_SIGNATURE);
buf[4..6].copy_from_slice(&48u16.to_le_bytes());
buf[6..8].copy_from_slice(&3u16.to_le_bytes());
buf[8..16].copy_from_slice(&1u64.to_le_bytes());
buf[16..18].copy_from_slice(&1u16.to_le_bytes());
buf[18..20].copy_from_slice(&1u16.to_le_bytes());
let first_attr: u16 = 56;
buf[20..22].copy_from_slice(&first_attr.to_le_bytes());
buf[22..24].copy_from_slice(&0x01u16.to_le_bytes());
buf[24..28].copy_from_slice(&512u32.to_le_bytes());
buf[28..32].copy_from_slice(&1024u32.to_le_bytes());
buf[44..48].copy_from_slice(&42u32.to_le_bytes());
buf[48..50].copy_from_slice(&0x0001u16.to_le_bytes());
let attr_size = 48u32; buf[56..60].copy_from_slice(&ATTR_FILE_NAME.to_le_bytes());
buf[60..64].copy_from_slice(&attr_size.to_le_bytes());
buf[64] = 0; buf[72..76].copy_from_slice(&30u32.to_le_bytes()); buf[76..78].copy_from_slice(&24u16.to_le_bytes());
let end_off = 56 + attr_size as usize;
buf[end_off..end_off + 4].copy_from_slice(&ATTR_END_MARKER.to_le_bytes());
let (entries, stats) = carve_mft_entries(&buf);
assert_eq!(entries.len(), 0);
assert!(stats.rejected > 0);
}
#[test]
fn test_carve_filename_attr_zero_name_len() {
let mut buf = vec![0u8; MFT_ENTRY_SIZE];
buf[0..4].copy_from_slice(&FILE_SIGNATURE);
buf[4..6].copy_from_slice(&48u16.to_le_bytes());
buf[6..8].copy_from_slice(&3u16.to_le_bytes());
buf[8..16].copy_from_slice(&1u64.to_le_bytes());
buf[16..18].copy_from_slice(&1u16.to_le_bytes());
buf[18..20].copy_from_slice(&1u16.to_le_bytes());
let first_attr: u16 = 56;
buf[20..22].copy_from_slice(&first_attr.to_le_bytes());
buf[22..24].copy_from_slice(&0x01u16.to_le_bytes());
buf[24..28].copy_from_slice(&512u32.to_le_bytes());
buf[28..32].copy_from_slice(&1024u32.to_le_bytes());
buf[44..48].copy_from_slice(&42u32.to_le_bytes());
buf[48..50].copy_from_slice(&0x0001u16.to_le_bytes());
let content_offset: u16 = 24;
let content_size = 66u32; let attr_size = (content_offset as u32 + content_size + 7) & !7;
buf[56..60].copy_from_slice(&ATTR_FILE_NAME.to_le_bytes());
buf[60..64].copy_from_slice(&attr_size.to_le_bytes());
buf[64] = 0; buf[72..76].copy_from_slice(&content_size.to_le_bytes());
buf[76..78].copy_from_slice(&content_offset.to_le_bytes());
let fn_start = 56 + content_offset as usize;
let parent_ref = 5u64 | (1u64 << 48);
buf[fn_start..fn_start + 8].copy_from_slice(&parent_ref.to_le_bytes());
buf[fn_start + 64] = 0; buf[fn_start + 65] = NS_WIN32_AND_DOS;
let end_off = 56 + attr_size as usize;
if end_off + 4 <= buf.len() {
buf[end_off..end_off + 4].copy_from_slice(&ATTR_END_MARKER.to_le_bytes());
}
let (entries, stats) = carve_mft_entries(&buf);
assert_eq!(entries.len(), 0);
assert!(stats.rejected > 0);
}
#[test]
fn test_carve_filename_attr_name_exceeds_attr() {
let mut buf = vec![0u8; MFT_ENTRY_SIZE];
buf[0..4].copy_from_slice(&FILE_SIGNATURE);
buf[4..6].copy_from_slice(&48u16.to_le_bytes());
buf[6..8].copy_from_slice(&3u16.to_le_bytes());
buf[8..16].copy_from_slice(&1u64.to_le_bytes());
buf[16..18].copy_from_slice(&1u16.to_le_bytes());
buf[18..20].copy_from_slice(&1u16.to_le_bytes());
let first_attr: u16 = 56;
buf[20..22].copy_from_slice(&first_attr.to_le_bytes());
buf[22..24].copy_from_slice(&0x01u16.to_le_bytes());
buf[24..28].copy_from_slice(&512u32.to_le_bytes());
buf[28..32].copy_from_slice(&1024u32.to_le_bytes());
buf[44..48].copy_from_slice(&42u32.to_le_bytes());
buf[48..50].copy_from_slice(&0x0001u16.to_le_bytes());
let content_offset: u16 = 24;
let content_size = 70u32; let attr_size = (content_offset as u32 + content_size + 7) & !7;
buf[56..60].copy_from_slice(&ATTR_FILE_NAME.to_le_bytes());
buf[60..64].copy_from_slice(&attr_size.to_le_bytes());
buf[64] = 0; buf[72..76].copy_from_slice(&content_size.to_le_bytes());
buf[76..78].copy_from_slice(&content_offset.to_le_bytes());
let fn_start = 56 + content_offset as usize;
let parent_ref = 5u64 | (1u64 << 48);
buf[fn_start..fn_start + 8].copy_from_slice(&parent_ref.to_le_bytes());
buf[fn_start + 64] = 100;
buf[fn_start + 65] = NS_WIN32_AND_DOS;
let end_off = 56 + attr_size as usize;
if end_off + 4 <= buf.len() {
buf[end_off..end_off + 4].copy_from_slice(&ATTR_END_MARKER.to_le_bytes());
}
let (entries, stats) = carve_mft_entries(&buf);
assert_eq!(entries.len(), 0);
assert!(stats.rejected > 0);
}
#[test]
fn test_carve_dos_name_not_replaced_by_posix() {
let mut buf = vec![0u8; MFT_ENTRY_SIZE];
buf[0..4].copy_from_slice(&FILE_SIGNATURE);
buf[4..6].copy_from_slice(&48u16.to_le_bytes());
buf[6..8].copy_from_slice(&3u16.to_le_bytes());
buf[8..16].copy_from_slice(&1u64.to_le_bytes());
buf[16..18].copy_from_slice(&1u16.to_le_bytes());
buf[18..20].copy_from_slice(&1u16.to_le_bytes());
let first_attr: u16 = 56;
buf[20..22].copy_from_slice(&first_attr.to_le_bytes());
buf[22..24].copy_from_slice(&0x01u16.to_le_bytes());
buf[24..28].copy_from_slice(&800u32.to_le_bytes());
buf[28..32].copy_from_slice(&1024u32.to_le_bytes());
buf[44..48].copy_from_slice(&99u32.to_le_bytes());
buf[48..50].copy_from_slice(&0x0001u16.to_le_bytes());
let attr1_size = write_filename_attr(
&mut buf,
first_attr as usize,
5,
1,
"LongFileName.xlsx",
NS_WIN32,
);
let attr2_start = first_attr as usize + attr1_size;
write_filename_attr(&mut buf, attr2_start, 5, 1, "LONGFI~1.XLS", NS_DOS);
let (entries, _) = carve_mft_entries(&buf);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].filename, "LongFileName.xlsx");
}
#[test]
fn test_carve_resident_filename_parse_none_then_valid() {
let mut buf = vec![0u8; MFT_ENTRY_SIZE];
buf[0..4].copy_from_slice(&FILE_SIGNATURE);
buf[4..6].copy_from_slice(&48u16.to_le_bytes());
buf[6..8].copy_from_slice(&3u16.to_le_bytes());
buf[8..16].copy_from_slice(&1u64.to_le_bytes());
buf[16..18].copy_from_slice(&1u16.to_le_bytes());
buf[18..20].copy_from_slice(&1u16.to_le_bytes());
let first_attr: u16 = 56;
buf[20..22].copy_from_slice(&first_attr.to_le_bytes());
buf[22..24].copy_from_slice(&0x01u16.to_le_bytes());
buf[24..28].copy_from_slice(&800u32.to_le_bytes());
buf[28..32].copy_from_slice(&1024u32.to_le_bytes());
buf[44..48].copy_from_slice(&99u32.to_le_bytes());
buf[48..50].copy_from_slice(&0x0001u16.to_le_bytes());
let bad_attr_size = 48usize;
buf[56..60].copy_from_slice(&ATTR_FILE_NAME.to_le_bytes());
buf[60..64].copy_from_slice(&(bad_attr_size as u32).to_le_bytes());
buf[64] = 0; buf[72..76].copy_from_slice(&30u32.to_le_bytes()); buf[76..78].copy_from_slice(&24u16.to_le_bytes());
let good_start = 56 + bad_attr_size;
write_filename_attr(&mut buf, good_start, 5, 1, "recovered.txt", NS_WIN32);
let (entries, _) = carve_mft_entries(&buf);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].filename, "recovered.txt");
}
#[test]
fn test_carve_preserves_all_fields() {
let entry = build_mft_entry(12345, 7, 999, 3, "evidence.xlsx", 0x03);
let (entries, _) = carve_mft_entries(&entry);
assert_eq!(entries.len(), 1);
let e = &entries[0];
assert_eq!(e.entry_number, 12345);
assert_eq!(e.sequence_number, 7);
assert_eq!(e.parent_entry, 999);
assert_eq!(e.parent_sequence, 3);
assert_eq!(e.filename, "evidence.xlsx");
assert!(e.is_directory);
assert!(e.is_in_use);
assert_eq!(e.offset, 0);
}
#[test]
fn carve_filename_attr_at_entry_boundary_does_not_panic() {
let mut entry = vec![0u8; MFT_ENTRY_SIZE];
entry[0..4].copy_from_slice(&FILE_SIGNATURE);
entry[16..18].copy_from_slice(&1u16.to_le_bytes()); entry[20..22].copy_from_slice(&48u16.to_le_bytes()); entry[48..52].copy_from_slice(&0x10u32.to_le_bytes()); entry[52..56].copy_from_slice(&968u32.to_le_bytes());
entry[1016..1020].copy_from_slice(&ATTR_FILE_NAME.to_le_bytes());
entry[1020..1024].copy_from_slice(&8u32.to_le_bytes());
let (entries, _) = carve_mft_entries(&entry);
assert!(entries.is_empty());
}
}