use std::collections::HashMap;
use std::io::{Read, Seek, SeekFrom};
use crate::tree::TreeNode;
const NTFS_OEM_ID: &[u8; 8] = b"NTFS ";
const ATTR_STANDARD_INFORMATION: u32 = 0x10;
const ATTR_ATTRIBUTE_LIST: u32 = 0x20;
const ATTR_FILE_NAME: u32 = 0x30;
const ATTR_DATA: u32 = 0x80;
const ATTR_END: u32 = 0xFFFF_FFFF;
const SYSTEM_RECORD_COUNT: u64 = 12;
const ROOT_MFT_RECORD: u64 = 5;
const MAX_DEPTH: usize = 32;
const NS_POSIX: u8 = 0;
const NS_WIN32: u8 = 1;
const NS_DOS: u8 = 2;
const NS_WIN32_DOS: u8 = 3;
#[derive(Debug)]
pub enum Error {
TooShort,
BadMagic,
BadClusterSize,
Io(std::io::Error),
TooDeep,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::TooShort => write!(f, "image too short to contain an NTFS boot sector"),
Error::BadMagic => write!(f, "NTFS OEM ID not found at offset 3"),
Error::BadClusterSize => write!(f, "NTFS cluster or MFT record size is invalid"),
Error::Io(e) => write!(f, "NTFS I/O error: {e}"),
Error::TooDeep => write!(f, "NTFS directory tree exceeded maximum recursion depth"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io(e) => Some(e),
_ => None,
}
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::Io(e)
}
}
#[derive(Debug, Clone)]
struct BootSector {
cluster_size: u64,
mft_record_size: u64,
mft_offset: u64,
}
fn parse_boot_sector(data: &[u8]) -> Result<BootSector, Error> {
if data.len() < 84 {
return Err(Error::TooShort);
}
if &data[3..11] != NTFS_OEM_ID {
return Err(Error::BadMagic);
}
let bytes_per_sector = u16::from_le_bytes([data[11], data[12]]) as u64;
let sectors_per_cluster = data[13] as u64;
if !(512..=4096).contains(&bytes_per_sector) {
return Err(Error::BadClusterSize);
}
if sectors_per_cluster == 0 {
return Err(Error::BadClusterSize);
}
let cluster_size = bytes_per_sector * sectors_per_cluster;
let mft_lcn = u64::from_le_bytes(data[48..56].try_into().unwrap());
let cpfrs = data[64] as i8;
let mft_record_size = if cpfrs > 0 {
(cpfrs as u64) * cluster_size
} else {
1u64 << ((-cpfrs) as u32)
};
if mft_record_size == 0 || mft_record_size > 65536 {
return Err(Error::BadClusterSize);
}
let mft_offset = mft_lcn * cluster_size;
Ok(BootSector {
cluster_size,
mft_record_size,
mft_offset,
})
}
fn apply_fixup(buf: &mut [u8]) -> bool {
if buf.len() < 8 {
return false;
}
let usa_offset = u16::from_le_bytes([buf[4], buf[5]]) as usize;
let usa_count = u16::from_le_bytes([buf[6], buf[7]]) as usize;
if usa_count < 2 || usa_offset + usa_count * 2 > buf.len() {
return false;
}
let usn_lo = buf[usa_offset];
let usn_hi = buf[usa_offset + 1];
for i in 1..usa_count {
let sector_end = i * 512 - 2;
if sector_end + 2 > buf.len() {
break;
}
if buf[sector_end] != usn_lo || buf[sector_end + 1] != usn_hi {
}
let fix_offset = usa_offset + i * 2;
buf[sector_end] = buf[fix_offset];
buf[sector_end + 1] = buf[fix_offset + 1];
}
true
}
fn read_mft_record<R: Read + Seek>(
file: &mut R,
offset: u64,
record_size: u64,
) -> Result<Option<Vec<u8>>, Error> {
file.seek(SeekFrom::Start(offset))?;
let mut buf = vec![0u8; record_size as usize];
match file.read_exact(&mut buf) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(Error::Io(e)),
}
if &buf[0..4] != b"FILE" {
return Ok(None);
}
if !apply_fixup(&mut buf) {
return Ok(None); }
let flags = u16::from_le_bytes([buf[22], buf[23]]);
if flags & 0x01 == 0 {
return Ok(None); }
Ok(Some(buf))
}
struct Attribute<'a> {
attr_type: u32,
non_resident: bool,
resident_data: Option<&'a [u8]>,
nonresident_slice: Option<&'a [u8]>,
}
fn parse_attributes(buf: &[u8]) -> Vec<Attribute<'_>> {
let mut attrs = Vec::new();
let attr_offset = match buf.get(20..22) {
Some(b) => u16::from_le_bytes([b[0], b[1]]) as usize,
None => return attrs,
};
let mut pos = attr_offset;
loop {
if pos + 8 > buf.len() {
break;
}
let attr_type = u32::from_le_bytes(buf[pos..pos + 4].try_into().unwrap());
if attr_type == ATTR_END {
break;
}
let length = u32::from_le_bytes(buf[pos + 4..pos + 8].try_into().unwrap()) as usize;
if length == 0 || pos + length > buf.len() {
break;
}
let non_resident = buf[pos + 8] != 0;
let resident_data = if !non_resident && pos + 16 + 4 <= pos + length {
if pos + 24 <= buf.len() {
let value_length =
u32::from_le_bytes(buf[pos + 16..pos + 20].try_into().unwrap()) as usize;
let value_offset = u16::from_le_bytes([buf[pos + 20], buf[pos + 21]]) as usize;
let data_start = pos + value_offset;
let data_end = data_start + value_length;
if data_end <= pos + length && data_end <= buf.len() {
Some(&buf[data_start..data_end])
} else {
None
}
} else {
None
}
} else {
None
};
let nonresident_slice = if non_resident && pos + length <= buf.len() {
Some(&buf[pos..pos + length])
} else {
None
};
attrs.push(Attribute {
attr_type,
non_resident,
resident_data,
nonresident_slice,
});
pos += length;
}
attrs
}
#[derive(Debug)]
struct FileNameAttr {
parent_ref: u64, name: String,
namespace: u8,
is_directory: bool, }
fn parse_filename_attr(data: &[u8]) -> Option<FileNameAttr> {
if data.len() < 66 {
return None;
}
let parent_ref_raw = u64::from_le_bytes(data[0..8].try_into().ok()?);
let parent_ref = parent_ref_raw & 0x0000_FFFF_FFFF_FFFF;
let file_attributes = u32::from_le_bytes(data[56..60].try_into().ok()?);
let is_directory = file_attributes & 0x10 != 0;
let filename_length = data[64] as usize; let namespace = data[65];
let name_bytes_len = filename_length * 2;
if 66 + name_bytes_len > data.len() {
return None;
}
let name_bytes = &data[66..66 + name_bytes_len];
let utf16_units: Vec<u16> = name_bytes
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
let name = String::from_utf16_lossy(&utf16_units);
Some(FileNameAttr {
parent_ref,
name,
namespace,
is_directory,
})
}
#[derive(Debug, Clone)]
struct Run {
start_lcn: u64,
#[allow(dead_code)]
length: u64,
}
fn decode_runlist(data: &[u8]) -> (Vec<Run>, bool) {
let mut runs = Vec::new();
let mut pos = 0usize;
let mut prev_lcn: i64 = 0;
let mut had_sparse = false;
while pos < data.len() {
let header = data[pos];
if header == 0 {
break;
}
pos += 1;
let len_size = (header >> 4) as usize; let off_size = (header & 0x0F) as usize;
if pos + len_size + off_size > data.len() {
break;
}
let mut length: u64 = 0;
for i in 0..len_size {
length |= (data[pos + i] as u64) << (i * 8);
}
pos += len_size;
let delta: i64 = if off_size == 0 {
0
} else {
let mut raw: u64 = 0;
for i in 0..off_size {
raw |= (data[pos + i] as u64) << (i * 8);
}
pos += off_size;
let sign_bit = 1u64 << (off_size * 8 - 1);
if raw & sign_bit != 0 {
let mask = !((sign_bit << 1) - 1);
(raw | mask) as i64
} else {
raw as i64
}
};
prev_lcn += delta;
if off_size == 0 {
had_sparse = true;
} else {
runs.push(Run {
start_lcn: prev_lcn as u64,
length,
});
}
}
(runs, had_sparse)
}
#[derive(Debug)]
struct RecordInfo {
mft_num: u64,
name: String,
parent_ref: u64,
is_directory: bool,
file_size: u64,
file_location: Option<u64>,
}
fn extract_record_info(
buf: &[u8],
mft_num: u64,
mft_record_abs_offset: u64,
cluster_size: u64,
volume_base: u64,
) -> Option<RecordInfo> {
let attrs = parse_attributes(buf);
let mut best_fn: Option<FileNameAttr> = None;
let mut file_size: u64 = 0;
let mut file_location: Option<u64> = None;
let first_attr_offset = u16::from_le_bytes([buf[20], buf[21]]) as usize;
let mut attr_pos = first_attr_offset;
for attr in &attrs {
if attr_pos + 8 > buf.len() {
break;
}
let attr_type_check = u32::from_le_bytes(buf[attr_pos..attr_pos + 4].try_into().unwrap());
if attr_type_check == ATTR_END {
break;
}
let attr_length =
u32::from_le_bytes(buf[attr_pos + 4..attr_pos + 8].try_into().unwrap()) as usize;
match attr.attr_type {
ATTR_FILE_NAME => {
if let Some(data) = attr.resident_data {
if let Some(fn_attr) = parse_filename_attr(data) {
if fn_attr.namespace == NS_DOS {
attr_pos += attr_length;
continue;
}
let take = match &best_fn {
None => true,
Some(existing) => {
namespace_priority(fn_attr.namespace)
> namespace_priority(existing.namespace)
}
};
if take {
best_fn = Some(fn_attr);
}
}
}
}
ATTR_DATA => {
if !attr.non_resident {
if let Some(data) = attr.resident_data {
file_size = data.len() as u64;
if attr_pos + 24 <= buf.len() {
let value_offset =
u16::from_le_bytes([buf[attr_pos + 20], buf[attr_pos + 21]]) as u64;
file_location =
Some(mft_record_abs_offset + attr_pos as u64 + value_offset);
}
}
} else if let Some(nr_slice) = attr.nonresident_slice {
if nr_slice.len() >= 64 {
let data_size = u64::from_le_bytes(nr_slice[48..56].try_into().unwrap());
file_size = data_size;
let runlist_offset =
u16::from_le_bytes([nr_slice[32], nr_slice[33]]) as usize;
if runlist_offset < nr_slice.len() {
let (runs, had_sparse) = decode_runlist(&nr_slice[runlist_offset..]);
if runs.len() == 1 && !had_sparse {
file_location =
Some(volume_base + runs[0].start_lcn * cluster_size);
}
}
}
}
}
ATTR_STANDARD_INFORMATION | ATTR_ATTRIBUTE_LIST => {
}
_ => {}
}
if attr_length == 0 {
break;
}
attr_pos += attr_length;
}
let fn_attr = best_fn?;
Some(RecordInfo {
mft_num,
name: fn_attr.name,
parent_ref: fn_attr.parent_ref,
is_directory: fn_attr.is_directory,
file_size,
file_location,
})
}
fn namespace_priority(ns: u8) -> u8 {
match ns {
NS_WIN32_DOS => 3,
NS_WIN32 => 2,
NS_POSIX => 1,
NS_DOS => 0,
_ => 0,
}
}
fn build_tree_recursive(
mft_num: u64,
name: String,
children_map: &HashMap<u64, Vec<RecordInfo>>,
depth: usize,
) -> Result<TreeNode, Error> {
if depth > MAX_DEPTH {
return Err(Error::TooDeep);
}
let is_dir = children_map.contains_key(&mft_num);
if is_dir || mft_num == ROOT_MFT_RECORD {
let mut node = TreeNode::new_directory(name);
if let Some(children) = children_map.get(&mft_num) {
for child in children {
let child_name = child.name.clone();
let child_num = child.mft_num;
let child_is_dir = child.is_directory;
if child_is_dir {
match build_tree_recursive(child_num, child_name, children_map, depth + 1) {
Ok(child_node) => node.add_child(child_node),
Err(Error::TooDeep) => {
}
Err(e) => return Err(e),
}
} else {
let file_node = match child.file_location {
Some(loc) => TreeNode::new_file_with_location(
child_name,
child.file_size,
loc,
child.file_size,
),
None => TreeNode::new_file(child_name, child.file_size),
};
node.add_child(file_node);
}
}
}
Ok(node)
} else {
Ok(TreeNode::new_directory(name))
}
}
pub fn detect<R: Read + Seek>(file: &mut R) -> bool {
let saved = match file.stream_position() {
Ok(p) => p,
Err(_) => return false,
};
let ok = detect_inner(file, saved);
let _ = file.seek(SeekFrom::Start(saved));
ok
}
fn detect_inner<R: Read + Seek>(file: &mut R, base: u64) -> bool {
if file.seek(SeekFrom::Start(base + 3)).is_err() {
return false;
}
let mut oem = [0u8; 8];
if file.read_exact(&mut oem).is_err() {
return false;
}
&oem == NTFS_OEM_ID
}
pub fn detect_and_parse<R: Read + Seek>(file: &mut R) -> Result<TreeNode, Error> {
let base = file.stream_position()?;
file.seek(SeekFrom::Start(base))?;
let mut boot_buf = [0u8; 512];
match file.read_exact(&mut boot_buf) {
Ok(()) => {}
Err(_) => return Err(Error::TooShort),
}
let boot = parse_boot_sector(&boot_buf)?;
let mut records: Vec<RecordInfo> = Vec::new();
let mut mft_num: u64 = 0;
loop {
let record_offset = base + boot.mft_offset + mft_num * boot.mft_record_size;
let record_abs = base + boot.mft_offset + mft_num * boot.mft_record_size;
match read_mft_record(file, record_offset, boot.mft_record_size)? {
None => {
}
Some(buf) if mft_num >= SYSTEM_RECORD_COUNT => {
if let Some(info) =
extract_record_info(&buf, mft_num, record_abs, boot.cluster_size, base)
{
if mft_num != ROOT_MFT_RECORD {
records.push(info);
}
}
}
Some(_) => {}
}
mft_num += 1;
if mft_num > 1_000_000 {
break;
}
}
let mut children_map: HashMap<u64, Vec<RecordInfo>> = HashMap::new();
for rec in records {
if rec.parent_ref == rec.mft_num {
continue;
}
children_map.entry(rec.parent_ref).or_default().push(rec);
}
let mut root = build_tree_recursive(ROOT_MFT_RECORD, "/".to_string(), &children_map, 0)?;
root.calculate_directory_size();
Ok(root)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Cursor, Seek, SeekFrom};
#[test]
fn error_display_too_short() {
let msg = format!("{}", Error::TooShort);
assert!(msg.contains("short") || msg.contains("NTFS"), "got: {msg}");
}
#[test]
fn error_display_bad_magic() {
let msg = format!("{}", Error::BadMagic);
assert!(msg.contains("OEM") || msg.contains("magic"), "got: {msg}");
}
#[test]
fn error_display_bad_cluster_size() {
let msg = format!("{}", Error::BadClusterSize);
assert!(
msg.contains("cluster") || msg.contains("invalid"),
"got: {msg}"
);
}
#[test]
fn error_display_too_deep() {
let msg = format!("{}", Error::TooDeep);
assert!(
msg.contains("depth") || msg.contains("recursion"),
"got: {msg}"
);
}
#[test]
fn error_display_io() {
let msg = format!("{}", Error::Io(std::io::Error::other("disk")));
assert!(msg.contains("disk"), "got: {msg}");
}
#[test]
fn error_source_io() {
use std::error::Error as StdError;
assert!(Error::Io(std::io::Error::other("s")).source().is_some());
}
#[test]
fn error_source_non_io() {
use std::error::Error as StdError;
assert!(Error::TooShort.source().is_none());
assert!(Error::BadMagic.source().is_none());
assert!(Error::BadClusterSize.source().is_none());
assert!(Error::TooDeep.source().is_none());
}
#[test]
fn parse_boot_sector_too_short() {
assert!(matches!(
parse_boot_sector(&[0u8; 10]),
Err(Error::TooShort)
));
}
#[test]
fn parse_boot_sector_bad_magic() {
let mut data = vec![0u8; 512];
data[3..11].copy_from_slice(b"NOTNTFS!");
assert!(matches!(parse_boot_sector(&data), Err(Error::BadMagic)));
}
#[test]
fn parse_boot_sector_bad_sector_size() {
let mut data = vec![0u8; 512];
data[3..11].copy_from_slice(NTFS_OEM_ID);
data[11..13].copy_from_slice(&0u16.to_le_bytes());
data[13] = 8; assert!(matches!(
parse_boot_sector(&data),
Err(Error::BadClusterSize)
));
}
#[test]
fn parse_boot_sector_zero_sectors_per_cluster() {
let mut data = vec![0u8; 512];
data[3..11].copy_from_slice(NTFS_OEM_ID);
data[11..13].copy_from_slice(&512u16.to_le_bytes()); data[13] = 0; assert!(matches!(
parse_boot_sector(&data),
Err(Error::BadClusterSize)
));
}
fn make_ntfs_boot_sector() -> Vec<u8> {
let mut boot = vec![0u8; 512];
boot[0] = 0xEB;
boot[1] = 0x52;
boot[2] = 0x90;
boot[3..11].copy_from_slice(NTFS_OEM_ID);
boot[11..13].copy_from_slice(&512u16.to_le_bytes());
boot[13] = 8;
boot[21] = 0xF8;
boot[48..56].copy_from_slice(&4u64.to_le_bytes());
boot[56..64].copy_from_slice(&2u64.to_le_bytes());
boot[64] = (-10i8) as u8;
boot[68] = (-10i8) as u8;
boot[72..80].copy_from_slice(&0x1234_5678_90AB_CDEFu64.to_le_bytes());
boot
}
fn make_minimal_ntfs_image() -> Vec<u8> {
const IMAGE_SIZE: usize = 32 * 1024;
const MFT_OFFSET: usize = 16384; const MFT_RECORD_SIZE: usize = 1024;
let mut img = vec![0u8; IMAGE_SIZE];
let boot = make_ntfs_boot_sector();
img[..512].copy_from_slice(&boot);
write_file_record(&mut img, MFT_OFFSET + 5 * MFT_RECORD_SIZE, 5, true);
write_file_record(&mut img, MFT_OFFSET + 12 * MFT_RECORD_SIZE, 12, false);
img
}
fn write_file_record(img: &mut [u8], offset: usize, mft_num: u64, is_dir: bool) {
const REC_SIZE: usize = 1024;
img[offset..offset + 4].copy_from_slice(b"FILE");
let usa_offset: u16 = 48;
let usa_count: u16 = 3;
img[offset + 4..offset + 6].copy_from_slice(&usa_offset.to_le_bytes());
img[offset + 6..offset + 8].copy_from_slice(&usa_count.to_le_bytes());
img[offset + usa_offset as usize..offset + usa_offset as usize + 2]
.copy_from_slice(&1u16.to_le_bytes());
img[offset + 510..offset + 512].copy_from_slice(&1u16.to_le_bytes());
img[offset + 1022..offset + 1024].copy_from_slice(&1u16.to_le_bytes());
img[offset + 16..offset + 18].copy_from_slice(&1u16.to_le_bytes());
img[offset + 18..offset + 20].copy_from_slice(&1u16.to_le_bytes());
let first_attr: u16 = 56;
img[offset + 20..offset + 22].copy_from_slice(&first_attr.to_le_bytes());
let flags: u16 = if is_dir { 0x03 } else { 0x01 };
img[offset + 22..offset + 24].copy_from_slice(&flags.to_le_bytes());
img[offset + 44..offset + 48].copy_from_slice(&(mft_num as u32).to_le_bytes());
let fn_name: Vec<u16> = if is_dir {
".".encode_utf16().collect()
} else {
"hello.txt".encode_utf16().collect()
};
let fn_name_bytes: Vec<u8> = fn_name.iter().flat_map(|&c| c.to_le_bytes()).collect();
let fn_value_len = 66 + fn_name_bytes.len();
let fn_attr_start = offset + first_attr as usize;
let fn_attr_value_offset: u16 = 24; let fn_attr_len = (fn_attr_value_offset as usize + fn_value_len + 7) & !7;
img[fn_attr_start..fn_attr_start + 4].copy_from_slice(&ATTR_FILE_NAME.to_le_bytes());
img[fn_attr_start + 4..fn_attr_start + 8]
.copy_from_slice(&(fn_attr_len as u32).to_le_bytes());
img[fn_attr_start + 8] = 0; img[fn_attr_start + 9] = 0; img[fn_attr_start + 16..fn_attr_start + 20]
.copy_from_slice(&(fn_value_len as u32).to_le_bytes());
img[fn_attr_start + 20..fn_attr_start + 22]
.copy_from_slice(&fn_attr_value_offset.to_le_bytes());
let fn_val_start = fn_attr_start + fn_attr_value_offset as usize;
let parent_ref: u64 = 5;
img[fn_val_start..fn_val_start + 8].copy_from_slice(&parent_ref.to_le_bytes());
let file_attrs: u32 = if is_dir { 0x10 } else { 0x20 };
img[fn_val_start + 56..fn_val_start + 60].copy_from_slice(&file_attrs.to_le_bytes());
img[fn_val_start + 64] = fn_name.len() as u8; img[fn_val_start + 65] = NS_WIN32_DOS; img[fn_val_start + 66..fn_val_start + 66 + fn_name_bytes.len()]
.copy_from_slice(&fn_name_bytes);
let mut next_attr = fn_attr_start + fn_attr_len;
if !is_dir {
const FILE_DATA: &[u8] = b"hello ntfs\n";
let data_val_offset: u16 = 24;
let data_attr_len = (data_val_offset as usize + FILE_DATA.len() + 7) & !7;
img[next_attr..next_attr + 4].copy_from_slice(&ATTR_DATA.to_le_bytes());
img[next_attr + 4..next_attr + 8]
.copy_from_slice(&(data_attr_len as u32).to_le_bytes());
img[next_attr + 8] = 0; img[next_attr + 16..next_attr + 20]
.copy_from_slice(&(FILE_DATA.len() as u32).to_le_bytes());
img[next_attr + 20..next_attr + 22].copy_from_slice(&data_val_offset.to_le_bytes());
img[next_attr + data_val_offset as usize
..next_attr + data_val_offset as usize + FILE_DATA.len()]
.copy_from_slice(FILE_DATA);
next_attr += data_attr_len;
}
img[next_attr..next_attr + 4].copy_from_slice(&ATTR_END.to_le_bytes());
let used: u32 = (next_attr - offset + 4) as u32;
img[offset + 24..offset + 28].copy_from_slice(&used.to_le_bytes());
img[offset + 28..offset + 32].copy_from_slice(&(REC_SIZE as u32).to_le_bytes());
}
fn cursor_of(img: &[u8]) -> Cursor<Vec<u8>> {
Cursor::new(img.to_vec())
}
#[test]
fn detect_valid_ntfs_boot() {
let boot = make_ntfs_boot_sector();
let mut img = vec![0u8; 1024];
img[..512].copy_from_slice(&boot);
let mut c = cursor_of(&img);
assert!(detect(&mut c), "should detect valid NTFS boot sector");
}
#[test]
fn detect_restores_position() {
let boot = make_ntfs_boot_sector();
let mut img = vec![0u8; 1024];
img[..512].copy_from_slice(&boot);
let mut c = cursor_of(&img);
c.seek(SeekFrom::Start(42)).unwrap();
let _ = detect(&mut c);
assert_eq!(
c.stream_position().unwrap(),
42,
"detect must restore stream position"
);
}
#[test]
fn detect_restores_position_on_failure() {
let img = vec![0u8; 512];
let mut c = Cursor::new(img);
c.seek(SeekFrom::Start(7)).unwrap();
let _ = detect(&mut c);
assert_eq!(c.stream_position().unwrap(), 7);
}
#[test]
fn detect_rejects_bad_magic() {
let mut boot = make_ntfs_boot_sector();
boot[3..11].copy_from_slice(b"FAT32 ");
let mut img = vec![0u8; 1024];
img[..512].copy_from_slice(&boot);
let mut c = cursor_of(&img);
assert!(
!detect(&mut c),
"corrupted OEM ID should not detect as NTFS"
);
}
#[test]
fn detect_rejects_too_short() {
let img = vec![0u8; 8];
let mut c = Cursor::new(img);
assert!(!detect(&mut c));
}
#[test]
fn detect_rejects_fat_image() {
let mut img = vec![0u8; 1024];
img[0] = 0xEB;
img[1] = 0x58;
img[2] = 0x90;
img[3..11].copy_from_slice(b"MSDOS5.0");
let mut c = Cursor::new(img);
assert!(!detect(&mut c), "FAT image should not be detected as NTFS");
}
#[test]
fn parse_boot_sector_valid() {
let boot = make_ntfs_boot_sector();
let bs = parse_boot_sector(&boot).expect("parse boot sector");
assert_eq!(bs.cluster_size, 4096);
assert_eq!(bs.mft_record_size, 1024);
assert_eq!(bs.mft_offset, 4 * 4096);
}
#[test]
fn parse_boot_sector_positive_cpfrs() {
let mut boot = make_ntfs_boot_sector();
boot[64] = 1u8;
let bs = parse_boot_sector(&boot).expect("positive clusters_per_FRS");
assert_eq!(bs.mft_record_size, 4096);
}
#[test]
fn apply_fixup_basic() {
let mut buf = vec![0u8; 1024];
buf[0..4].copy_from_slice(b"FILE");
let usa_off: u16 = 48;
let usa_cnt: u16 = 3;
buf[4..6].copy_from_slice(&usa_off.to_le_bytes());
buf[6..8].copy_from_slice(&usa_cnt.to_le_bytes());
buf[48..50].copy_from_slice(&0xABCDu16.to_le_bytes());
buf[50..52].copy_from_slice(&0x1234u16.to_le_bytes()); buf[52..54].copy_from_slice(&0x5678u16.to_le_bytes()); buf[510..512].copy_from_slice(&0xABCDu16.to_le_bytes());
buf[1022..1024].copy_from_slice(&0xABCDu16.to_le_bytes());
let ok = apply_fixup(&mut buf);
assert!(ok);
assert_eq!(&buf[510..512], &0x1234u16.to_le_bytes());
assert_eq!(&buf[1022..1024], &0x5678u16.to_le_bytes());
}
#[test]
fn decode_runlist_single_run() {
let data = [0x11u8, 8, 3, 0x00];
let (runs, had_sparse) = decode_runlist(&data);
assert!(!had_sparse);
assert_eq!(runs.len(), 1);
assert_eq!(runs[0].start_lcn, 3);
assert_eq!(runs[0].length, 8);
}
#[test]
fn decode_runlist_two_runs() {
let data = [0x11, 4, 10, 0x11, 2, 5, 0x00];
let (runs, had_sparse) = decode_runlist(&data);
assert!(!had_sparse);
assert_eq!(runs.len(), 2);
assert_eq!(runs[0].start_lcn, 10);
assert_eq!(runs[0].length, 4);
assert_eq!(runs[1].start_lcn, 15);
assert_eq!(runs[1].length, 2);
}
#[test]
fn decode_runlist_negative_delta() {
let data = [0x11, 8, 20, 0x11, 4, 0xFBu8, 0x00];
let (runs, had_sparse) = decode_runlist(&data);
assert!(!had_sparse);
assert_eq!(runs.len(), 2);
assert_eq!(runs[0].start_lcn, 20);
assert_eq!(runs[1].start_lcn, 15);
}
#[test]
fn parse_minimal_image_tree_shape() {
let img = make_minimal_ntfs_image();
let mut c = cursor_of(&img);
assert!(detect(&mut c), "should detect minimal NTFS image");
c.seek(SeekFrom::Start(0)).unwrap();
let root = detect_and_parse(&mut c).expect("parse minimal NTFS image");
assert_eq!(root.name, "/");
assert!(root.is_directory);
let hello = root.children.iter().find(|n| n.name == "hello.txt");
assert!(hello.is_some(), "hello.txt should be in root");
assert!(!hello.unwrap().is_directory);
}
#[test]
fn parse_minimal_image_file_size() {
let img = make_minimal_ntfs_image();
let mut c = cursor_of(&img);
c.seek(SeekFrom::Start(0)).unwrap();
let root = detect_and_parse(&mut c).expect("parse");
let hello = root
.children
.iter()
.find(|n| n.name == "hello.txt")
.unwrap();
assert_eq!(
hello.size,
b"hello ntfs\n".len() as u64,
"file size should match resident $DATA value length"
);
}
#[test]
fn error_from_io_error() {
let io_err = std::io::Error::other("disk error");
let err: Error = Error::from(io_err);
assert!(matches!(err, Error::Io(_)));
}
#[test]
fn parse_boot_sector_bad_mft_record_size() {
let mut data = make_ntfs_boot_sector();
data[64] = (-17i8) as u8; assert!(matches!(
parse_boot_sector(&data),
Err(Error::BadClusterSize)
));
}
#[test]
fn apply_fixup_buf_too_short() {
let mut buf = [0u8; 4]; assert!(!apply_fixup(&mut buf), "short buffer should return false");
}
#[test]
fn apply_fixup_usa_count_too_small() {
let mut buf = vec![0u8; 64];
buf[4] = 0; buf[5] = 0; buf[6] = 1; buf[7] = 0; assert!(!apply_fixup(&mut buf));
}
#[test]
fn apply_fixup_short_buf_breaks_sector_loop() {
let mut buf = vec![0u8; 12];
buf[4] = 0; buf[5] = 0;
buf[6] = 2; buf[7] = 0;
assert!(apply_fixup(&mut buf));
}
#[test]
fn parse_attributes_buf_too_short_for_offset() {
let buf = [0u8; 10];
let attrs = parse_attributes(&buf);
assert!(attrs.is_empty());
}
#[test]
fn parse_attributes_pos_exceeds_buf() {
let mut buf = vec![0u8; 30];
buf[20] = 25;
buf[21] = 0;
let attrs = parse_attributes(&buf);
assert!(attrs.is_empty());
}
#[test]
fn parse_attributes_zero_length_attr_breaks() {
let mut buf = vec![0u8; 40];
buf[20] = 22;
buf[21] = 0;
buf[22] = 1;
let attrs = parse_attributes(&buf);
assert!(attrs.is_empty());
}
#[test]
fn parse_filename_attr_too_short() {
let data = [0u8; 10]; assert!(parse_filename_attr(&data).is_none());
}
#[test]
fn parse_filename_attr_name_overflow() {
let mut data = vec![0u8; 70];
data[64] = 3;
assert!(parse_filename_attr(&data).is_none());
}
#[test]
fn parse_minimal_image_file_location_and_contents() {
let img = make_minimal_ntfs_image();
let mut c = cursor_of(&img);
c.seek(SeekFrom::Start(0)).unwrap();
let root = detect_and_parse(&mut c).expect("parse");
let hello = root
.children
.iter()
.find(|n| n.name == "hello.txt")
.unwrap();
assert!(
hello.file_location.is_some(),
"resident $DATA should have a file_location"
);
let loc = hello.file_location.unwrap();
let len = hello.size as usize;
c.seek(SeekFrom::Start(loc)).unwrap();
let mut buf = vec![0u8; len];
c.read_exact(&mut buf).unwrap();
assert_eq!(buf, b"hello ntfs\n");
}
#[test]
fn apply_fixup_usa_offset_overflow() {
let mut buf = vec![0u8; 64];
buf[4..6].copy_from_slice(&60u16.to_le_bytes()); buf[6..8].copy_from_slice(&5u16.to_le_bytes()); assert!(!apply_fixup(&mut buf));
}
#[test]
fn apply_fixup_usn_mismatch_continues() {
let mut buf = vec![0u8; 1024];
buf[4..6].copy_from_slice(&48u16.to_le_bytes()); buf[6..8].copy_from_slice(&3u16.to_le_bytes()); buf[48..50].copy_from_slice(&0xAAAAu16.to_le_bytes()); buf[50..52].copy_from_slice(&0x1111u16.to_le_bytes()); buf[52..54].copy_from_slice(&0x2222u16.to_le_bytes()); let ok = apply_fixup(&mut buf);
assert!(ok, "mismatch is non-fatal; fixup should still return true");
assert_eq!(&buf[510..512], &0x1111u16.to_le_bytes());
assert_eq!(&buf[1022..1024], &0x2222u16.to_le_bytes());
}
fn make_ntfs_image_with_slot(
slot: usize,
good_sig: bool,
good_fixup: bool,
in_use: bool,
) -> Vec<u8> {
const IMAGE_SIZE: usize = 32 * 1024;
const MFT_OFFSET: usize = 16384;
const MFT_RECORD_SIZE: usize = 1024;
let mut img = vec![0u8; IMAGE_SIZE];
let boot = make_ntfs_boot_sector();
img[..512].copy_from_slice(&boot);
write_file_record(&mut img, MFT_OFFSET + 5 * MFT_RECORD_SIZE, 5, true);
let off = MFT_OFFSET + slot * MFT_RECORD_SIZE;
if good_sig {
img[off..off + 4].copy_from_slice(b"FILE");
}
img[off + 4..off + 6].copy_from_slice(&48u16.to_le_bytes());
let usa_count: u16 = if good_fixup { 3 } else { 1 };
img[off + 6..off + 8].copy_from_slice(&usa_count.to_le_bytes());
if good_fixup {
img[off + 48..off + 50].copy_from_slice(&1u16.to_le_bytes());
img[off + 510..off + 512].copy_from_slice(&1u16.to_le_bytes());
img[off + 1022..off + 1024].copy_from_slice(&1u16.to_le_bytes());
}
let flags: u16 = if in_use { 0x01 } else { 0x00 };
img[off + 22..off + 24].copy_from_slice(&flags.to_le_bytes());
img
}
#[test]
fn read_mft_record_bad_fixup_returns_none() {
let img = make_ntfs_image_with_slot(6, true, false, true);
let mut c = cursor_of(&img);
let root = detect_and_parse(&mut c).expect("should succeed despite bad fixup slot");
assert_eq!(root.name, "/");
}
#[test]
fn read_mft_record_not_in_use_returns_none() {
let img = make_ntfs_image_with_slot(6, true, true, false);
let mut c = cursor_of(&img);
let root = detect_and_parse(&mut c).expect("should succeed despite not-in-use slot");
assert_eq!(root.name, "/");
}
#[test]
fn parse_attributes_resident_data_end_overflow() {
let mut buf = vec![0u8; 60];
buf[20] = 22;
buf[22 + 8] = 0;
let attr_type = ATTR_DATA;
buf[22..26].copy_from_slice(&attr_type.to_le_bytes());
buf[26..30].copy_from_slice(&38u32.to_le_bytes());
buf[38..42].copy_from_slice(&50u32.to_le_bytes()); buf[42..44].copy_from_slice(&24u16.to_le_bytes()); let attrs = parse_attributes(&buf);
assert_eq!(attrs.len(), 1);
assert!(attrs[0].resident_data.is_none());
}
#[test]
fn parse_attributes_resident_too_short_for_header() {
let mut buf = vec![0u8; 30];
buf[20] = 22; buf[22..26].copy_from_slice(&ATTR_DATA.to_le_bytes());
buf[26..30].copy_from_slice(&8u32.to_le_bytes()); drop(buf);
let mut buf2 = vec![0u8; 45];
buf2[20] = 22; buf2[22..26].copy_from_slice(&ATTR_DATA.to_le_bytes());
buf2[26..30].copy_from_slice(&23u32.to_le_bytes()); let attrs2 = parse_attributes(&buf2);
assert_eq!(attrs2.len(), 1);
assert!(attrs2[0].resident_data.is_none());
}
#[test]
fn parse_attributes_nonresident_slice_overflow() {
let mut buf = vec![0u8; 40];
buf[20] = 22; buf[22..26].copy_from_slice(&ATTR_DATA.to_le_bytes());
buf[26..30].copy_from_slice(&20u32.to_le_bytes()); buf[26..30].copy_from_slice(&18u32.to_le_bytes()); buf[22 + 8] = 1; drop(buf);
let mut buf3 = vec![0u8; 50];
buf3[20] = 22; buf3[22..26].copy_from_slice(&ATTR_DATA.to_le_bytes());
buf3[26..30].copy_from_slice(&28u32.to_le_bytes()); buf3[22 + 8] = 0; buf3[42..44].copy_from_slice(&24u16.to_le_bytes()); buf3[38..42].copy_from_slice(&0u32.to_le_bytes()); let attrs3 = parse_attributes(&buf3);
assert_eq!(attrs3.len(), 1);
assert!(attrs3[0].nonresident_slice.is_none());
assert!(attrs3[0].resident_data.is_some()); }
#[test]
fn decode_runlist_truncated_run_breaks() {
let data = [0x11u8, 8]; let (runs, _) = decode_runlist(&data);
assert!(runs.is_empty(), "truncated run should produce no runs");
}
#[test]
fn decode_runlist_sparse_run() {
let data = [0x10u8, 4, 0x00]; let (runs, had_sparse) = decode_runlist(&data);
assert!(had_sparse, "off_size=0 should set had_sparse");
assert!(runs.is_empty(), "sparse run has no physical location");
}
#[test]
fn namespace_priority_all_values() {
assert_eq!(namespace_priority(NS_WIN32_DOS), 3);
assert_eq!(namespace_priority(NS_WIN32), 2);
assert_eq!(namespace_priority(NS_POSIX), 1);
assert_eq!(namespace_priority(NS_DOS), 0);
assert_eq!(namespace_priority(99), 0); }
#[test]
fn build_tree_recursive_too_deep() {
let map: HashMap<u64, Vec<RecordInfo>> = HashMap::new();
let err = build_tree_recursive(ROOT_MFT_RECORD, "/".to_string(), &map, MAX_DEPTH + 1);
assert!(matches!(err, Err(Error::TooDeep)));
}
#[test]
fn build_tree_recursive_with_file_children() {
let mut map: HashMap<u64, Vec<RecordInfo>> = HashMap::new();
map.insert(
ROOT_MFT_RECORD,
vec![
RecordInfo {
mft_num: 20,
name: "with_loc.txt".to_string(),
parent_ref: ROOT_MFT_RECORD,
is_directory: false,
file_size: 100,
file_location: Some(4096),
},
RecordInfo {
mft_num: 21,
name: "no_loc.txt".to_string(),
parent_ref: ROOT_MFT_RECORD,
is_directory: false,
file_size: 200,
file_location: None, },
],
);
let root = build_tree_recursive(ROOT_MFT_RECORD, "/".to_string(), &map, 0).unwrap();
assert_eq!(root.children.len(), 2);
let f1 = root
.children
.iter()
.find(|n| n.name == "with_loc.txt")
.unwrap();
assert!(f1.file_location.is_some());
let f2 = root
.children
.iter()
.find(|n| n.name == "no_loc.txt")
.unwrap();
assert!(f2.file_location.is_none());
}
#[test]
fn build_tree_recursive_with_subdirectory() {
let mut map: HashMap<u64, Vec<RecordInfo>> = HashMap::new();
map.insert(
ROOT_MFT_RECORD,
vec![RecordInfo {
mft_num: 20,
name: "subdir".to_string(),
parent_ref: ROOT_MFT_RECORD,
is_directory: true,
file_size: 0,
file_location: None,
}],
);
map.insert(
20,
vec![RecordInfo {
mft_num: 21,
name: "file.txt".to_string(),
parent_ref: 20,
is_directory: false,
file_size: 42,
file_location: None,
}],
);
let root = build_tree_recursive(ROOT_MFT_RECORD, "/".to_string(), &map, 0).unwrap();
assert_eq!(root.children.len(), 1);
let sub = &root.children[0];
assert_eq!(sub.name, "subdir");
assert_eq!(sub.children.len(), 1);
assert_eq!(sub.children[0].name, "file.txt");
}
#[test]
fn build_tree_recursive_too_deep_child_skipped() {
let mut map: HashMap<u64, Vec<RecordInfo>> = HashMap::new();
map.insert(
ROOT_MFT_RECORD,
vec![RecordInfo {
mft_num: 20,
name: "deep".to_string(),
parent_ref: ROOT_MFT_RECORD,
is_directory: true,
file_size: 0,
file_location: None,
}],
);
let root = build_tree_recursive(ROOT_MFT_RECORD, "/".to_string(), &map, MAX_DEPTH).unwrap();
assert!(
root.children.is_empty(),
"TooDeep subdirectory should be skipped"
);
}
#[test]
fn build_tree_recursive_orphan_node() {
let map: HashMap<u64, Vec<RecordInfo>> = HashMap::new();
let node = build_tree_recursive(999, "orphan".to_string(), &map, 0).unwrap();
assert!(node.is_directory);
assert!(node.children.is_empty());
}
fn write_nonresident_file_record(img: &mut [u8], offset: usize, mft_num: u64) {
const REC_SIZE: usize = 1024;
img[offset..offset + 4].copy_from_slice(b"FILE");
img[offset + 4..offset + 6].copy_from_slice(&48u16.to_le_bytes()); img[offset + 6..offset + 8].copy_from_slice(&3u16.to_le_bytes()); img[offset + 48..offset + 50].copy_from_slice(&1u16.to_le_bytes()); img[offset + 510..offset + 512].copy_from_slice(&1u16.to_le_bytes());
img[offset + 1022..offset + 1024].copy_from_slice(&1u16.to_le_bytes());
img[offset + 16..offset + 18].copy_from_slice(&1u16.to_le_bytes()); img[offset + 18..offset + 20].copy_from_slice(&1u16.to_le_bytes()); img[offset + 22..offset + 24].copy_from_slice(&0x01u16.to_le_bytes()); img[offset + 44..offset + 48].copy_from_slice(&(mft_num as u32).to_le_bytes());
img[offset + 20..offset + 22].copy_from_slice(&56u16.to_le_bytes());
let fn_name: Vec<u16> = "big.bin".encode_utf16().collect();
let fn_name_bytes: Vec<u8> = fn_name.iter().flat_map(|&c| c.to_le_bytes()).collect();
let fn_value_len = 66 + fn_name_bytes.len();
let fn_val_off: u16 = 24;
let fn_attr_len = (fn_val_off as usize + fn_value_len + 7) & !7;
let fn_start = offset + 56;
img[fn_start..fn_start + 4].copy_from_slice(&ATTR_FILE_NAME.to_le_bytes());
img[fn_start + 4..fn_start + 8].copy_from_slice(&(fn_attr_len as u32).to_le_bytes());
img[fn_start + 8] = 0; img[fn_start + 16..fn_start + 20].copy_from_slice(&(fn_value_len as u32).to_le_bytes());
img[fn_start + 20..fn_start + 22].copy_from_slice(&fn_val_off.to_le_bytes());
let fn_val_start = fn_start + fn_val_off as usize;
img[fn_val_start..fn_val_start + 8].copy_from_slice(&5u64.to_le_bytes()); img[fn_val_start + 56..fn_val_start + 60].copy_from_slice(&0x20u32.to_le_bytes()); img[fn_val_start + 64] = fn_name.len() as u8;
img[fn_val_start + 65] = NS_WIN32_DOS;
img[fn_val_start + 66..fn_val_start + 66 + fn_name_bytes.len()]
.copy_from_slice(&fn_name_bytes);
let nr_start = fn_start + fn_attr_len;
let nr_len: u32 = 72; img[nr_start..nr_start + 4].copy_from_slice(&ATTR_DATA.to_le_bytes());
img[nr_start + 4..nr_start + 8].copy_from_slice(&nr_len.to_le_bytes());
img[nr_start + 8] = 1; img[nr_start + 32..nr_start + 34].copy_from_slice(&64u16.to_le_bytes());
img[nr_start + 48..nr_start + 56].copy_from_slice(&(8u64 * 4096u64).to_le_bytes());
img[nr_start + 64] = 0x11; img[nr_start + 65] = 8; img[nr_start + 66] = 4; img[nr_start + 67] = 0x00;
let end_start = nr_start + nr_len as usize;
img[end_start..end_start + 4].copy_from_slice(&ATTR_END.to_le_bytes());
let used: u32 = (end_start - offset + 4) as u32;
img[offset + 24..offset + 28].copy_from_slice(&used.to_le_bytes());
img[offset + 28..offset + 32].copy_from_slice(&(REC_SIZE as u32).to_le_bytes());
}
#[test]
fn nonresident_data_single_run_file_location() {
const IMAGE_SIZE: usize = 32 * 1024;
const MFT_OFFSET: usize = 16384;
const MFT_RECORD_SIZE: usize = 1024;
let mut img = vec![0u8; IMAGE_SIZE];
let boot = make_ntfs_boot_sector();
img[..512].copy_from_slice(&boot);
write_file_record(&mut img, MFT_OFFSET + 5 * MFT_RECORD_SIZE, 5, true);
write_nonresident_file_record(&mut img, MFT_OFFSET + 12 * MFT_RECORD_SIZE, 12);
let mut c = cursor_of(&img);
let root = detect_and_parse(&mut c).expect("parse");
let big = root.children.iter().find(|n| n.name == "big.bin");
assert!(big.is_some(), "big.bin should appear in root");
let big = big.unwrap();
assert_eq!(big.size, 8 * 4096);
assert_eq!(big.file_location, Some(16384));
}
#[test]
fn extract_record_info_standard_information_attr_ignored() {
let mut buf = vec![0u8; 100];
buf[20] = 22; buf[22..26].copy_from_slice(&ATTR_STANDARD_INFORMATION.to_le_bytes());
buf[26..30].copy_from_slice(&40u32.to_le_bytes()); buf[22 + 8] = 0; buf[38..42].copy_from_slice(&0u32.to_le_bytes());
buf[42..44].copy_from_slice(&24u16.to_le_bytes());
buf[62..66].copy_from_slice(&ATTR_END.to_le_bytes());
let attrs = parse_attributes(&buf);
assert_eq!(attrs.len(), 1);
assert_eq!(attrs[0].attr_type, ATTR_STANDARD_INFORMATION);
}
#[test]
fn extract_record_info_attr_attribute_list_ignored() {
let mut buf = vec![0u8; 100];
buf[20] = 22;
buf[22..26].copy_from_slice(&ATTR_ATTRIBUTE_LIST.to_le_bytes());
buf[26..30].copy_from_slice(&40u32.to_le_bytes());
buf[22 + 8] = 0; buf[38..42].copy_from_slice(&0u32.to_le_bytes());
buf[42..44].copy_from_slice(&24u16.to_le_bytes());
buf[62..66].copy_from_slice(&ATTR_END.to_le_bytes());
let attrs = parse_attributes(&buf);
assert_eq!(attrs.len(), 1);
assert_eq!(attrs[0].attr_type, ATTR_ATTRIBUTE_LIST);
}
#[test]
fn parse_attributes_unknown_type_catch_all() {
let mut buf = vec![0u8; 100];
buf[20] = 22;
buf[22..26].copy_from_slice(&0x60u32.to_le_bytes()); buf[26..30].copy_from_slice(&40u32.to_le_bytes());
buf[22 + 8] = 0;
buf[38..42].copy_from_slice(&0u32.to_le_bytes());
buf[42..44].copy_from_slice(&24u16.to_le_bytes());
buf[62..66].copy_from_slice(&ATTR_END.to_le_bytes());
let attrs = parse_attributes(&buf);
assert_eq!(attrs.len(), 1);
assert_eq!(attrs[0].attr_type, 0x60);
}
fn make_eri_buf(parent_ref: u64, namespace: u8, extra_attrs: &[u8]) -> Vec<u8> {
let mut buf = vec![0u8; 2048];
buf[20..22].copy_from_slice(&56u16.to_le_bytes());
let fn_value_len: u32 = 68;
let fn_attr_len: usize = ((24 + fn_value_len as usize + 7) & !7).max(24);
buf[56..60].copy_from_slice(&ATTR_FILE_NAME.to_le_bytes());
buf[60..64].copy_from_slice(&(fn_attr_len as u32).to_le_bytes());
buf[64] = 0; buf[72..76].copy_from_slice(&fn_value_len.to_le_bytes());
buf[76..78].copy_from_slice(&24u16.to_le_bytes()); let fv = 56 + 24; buf[fv..fv + 8].copy_from_slice(&parent_ref.to_le_bytes());
buf[fv + 56..fv + 60].copy_from_slice(&0x20u32.to_le_bytes()); buf[fv + 64] = 1; buf[fv + 65] = namespace;
buf[fv + 66..fv + 68].copy_from_slice(&(b'A' as u16).to_le_bytes());
let next = 56 + fn_attr_len;
buf[next..next + extra_attrs.len()].copy_from_slice(extra_attrs);
let end = next + extra_attrs.len();
buf[end..end + 4].copy_from_slice(&ATTR_END.to_le_bytes());
buf
}
fn make_resident_attr(attr_type: u32, len: usize) -> Vec<u8> {
let total = (24 + len + 7) & !7;
let mut a = vec![0u8; total];
a[0..4].copy_from_slice(&attr_type.to_le_bytes());
a[4..8].copy_from_slice(&(total as u32).to_le_bytes());
a[8] = 0; a[16..20].copy_from_slice(&(len as u32).to_le_bytes());
a[20..22].copy_from_slice(&24u16.to_le_bytes()); a
}
#[test]
fn extract_record_info_standard_information_in_record() {
let si_attr = make_resident_attr(ATTR_STANDARD_INFORMATION, 48);
let buf = make_eri_buf(ROOT_MFT_RECORD, NS_WIN32_DOS, &si_attr);
let info = extract_record_info(&buf, 12, 0, 4096, 0);
assert!(info.is_some());
assert_eq!(info.unwrap().name, "A");
}
#[test]
fn extract_record_info_attribute_list_in_record() {
let al_attr = make_resident_attr(ATTR_ATTRIBUTE_LIST, 24);
let buf = make_eri_buf(ROOT_MFT_RECORD, NS_WIN32_DOS, &al_attr);
let info = extract_record_info(&buf, 12, 0, 4096, 0);
assert!(info.is_some());
}
#[test]
fn extract_record_info_unknown_attr_type_catch_all() {
let unk_attr = make_resident_attr(0x60, 16);
let buf = make_eri_buf(ROOT_MFT_RECORD, NS_WIN32_DOS, &unk_attr);
let info = extract_record_info(&buf, 12, 0, 4096, 0);
assert!(info.is_some());
}
#[test]
fn extract_record_info_dos_namespace_skipped() {
let buf = make_eri_buf(ROOT_MFT_RECORD, NS_DOS, &[]);
let info = extract_record_info(&buf, 12, 0, 4096, 0);
assert!(info.is_none(), "NS_DOS-only record should produce no name");
}
#[test]
fn extract_record_info_two_filename_attrs_namespace_wins() {
let fn_value_len: u32 = 68;
let fn_attr_len = (24 + fn_value_len as usize + 7) & !7;
let mut second = vec![0u8; fn_attr_len];
second[0..4].copy_from_slice(&ATTR_FILE_NAME.to_le_bytes());
second[4..8].copy_from_slice(&(fn_attr_len as u32).to_le_bytes());
second[8] = 0; second[16..20].copy_from_slice(&fn_value_len.to_le_bytes());
second[20..22].copy_from_slice(&24u16.to_le_bytes());
let sv = 24; second[sv..sv + 8].copy_from_slice(&ROOT_MFT_RECORD.to_le_bytes());
second[sv + 56..sv + 60].copy_from_slice(&0x20u32.to_le_bytes());
second[sv + 64] = 1; second[sv + 65] = NS_WIN32_DOS; second[sv + 66..sv + 68].copy_from_slice(&(b'B' as u16).to_le_bytes());
let buf = make_eri_buf(ROOT_MFT_RECORD, NS_WIN32, &second);
let info = extract_record_info(&buf, 12, 0, 4096, 0).expect("should have name");
assert_eq!(info.name, "B");
}
#[test]
fn extract_record_info_attr_length_zero_breaks_inner_loop() {
let buf = make_eri_buf(ROOT_MFT_RECORD, NS_WIN32_DOS, &[]);
let info = extract_record_info(&buf, 12, 0, 4096, 0);
assert!(info.is_some()); }
fn write_nonresident_file_record_custom(
img: &mut [u8],
offset: usize,
mft_num: u64,
nr_len: u32,
runlist_offset: u16,
) {
const REC_SIZE: usize = 1024;
img[offset..offset + 4].copy_from_slice(b"FILE");
img[offset + 4..offset + 6].copy_from_slice(&48u16.to_le_bytes());
img[offset + 6..offset + 8].copy_from_slice(&3u16.to_le_bytes());
img[offset + 48..offset + 50].copy_from_slice(&1u16.to_le_bytes());
img[offset + 510..offset + 512].copy_from_slice(&1u16.to_le_bytes());
img[offset + 1022..offset + 1024].copy_from_slice(&1u16.to_le_bytes());
img[offset + 16..offset + 18].copy_from_slice(&1u16.to_le_bytes());
img[offset + 18..offset + 20].copy_from_slice(&1u16.to_le_bytes());
img[offset + 22..offset + 24].copy_from_slice(&0x01u16.to_le_bytes());
img[offset + 44..offset + 48].copy_from_slice(&(mft_num as u32).to_le_bytes());
img[offset + 20..offset + 22].copy_from_slice(&56u16.to_le_bytes());
let fn_name: Vec<u16> = "big.bin".encode_utf16().collect();
let fn_name_bytes: Vec<u8> = fn_name.iter().flat_map(|&c| c.to_le_bytes()).collect();
let fn_value_len = 66 + fn_name_bytes.len();
let fn_attr_len = (24 + fn_value_len + 7) & !7;
let fn_start = offset + 56;
img[fn_start..fn_start + 4].copy_from_slice(&ATTR_FILE_NAME.to_le_bytes());
img[fn_start + 4..fn_start + 8].copy_from_slice(&(fn_attr_len as u32).to_le_bytes());
img[fn_start + 8] = 0;
img[fn_start + 16..fn_start + 20].copy_from_slice(&(fn_value_len as u32).to_le_bytes());
img[fn_start + 20..fn_start + 22].copy_from_slice(&24u16.to_le_bytes());
let fv = fn_start + 24;
img[fv..fv + 8].copy_from_slice(&5u64.to_le_bytes());
img[fv + 56..fv + 60].copy_from_slice(&0x20u32.to_le_bytes());
img[fv + 64] = fn_name.len() as u8;
img[fv + 65] = NS_WIN32_DOS;
img[fv + 66..fv + 66 + fn_name_bytes.len()].copy_from_slice(&fn_name_bytes);
let nr_start = fn_start + fn_attr_len;
img[nr_start..nr_start + 4].copy_from_slice(&ATTR_DATA.to_le_bytes());
img[nr_start + 4..nr_start + 8].copy_from_slice(&nr_len.to_le_bytes());
img[nr_start + 8] = 1; img[nr_start + 32..nr_start + 34].copy_from_slice(&runlist_offset.to_le_bytes());
img[nr_start + 48..nr_start + 56].copy_from_slice(&(8u64 * 4096u64).to_le_bytes());
let end_start = nr_start + nr_len as usize;
img[end_start..end_start + 4].copy_from_slice(&ATTR_END.to_le_bytes());
let used: u32 = (end_start - offset + 4) as u32;
img[offset + 24..offset + 28].copy_from_slice(&used.to_le_bytes());
img[offset + 28..offset + 32].copy_from_slice(&(REC_SIZE as u32).to_le_bytes());
}
#[test]
fn nonresident_data_nr_slice_too_short_no_location() {
const IMAGE_SIZE: usize = 32 * 1024;
const MFT_OFFSET: usize = 16384;
const MFT_RECORD_SIZE: usize = 1024;
let mut img = vec![0u8; IMAGE_SIZE];
img[..512].copy_from_slice(&make_ntfs_boot_sector());
write_file_record(&mut img, MFT_OFFSET + 5 * MFT_RECORD_SIZE, 5, true);
write_nonresident_file_record_custom(
&mut img,
MFT_OFFSET + 12 * MFT_RECORD_SIZE,
12,
56,
64,
);
let mut c = cursor_of(&img);
let root = detect_and_parse(&mut c).expect("parse");
let big = root.children.iter().find(|n| n.name == "big.bin");
assert!(big.is_some());
assert!(big.unwrap().file_location.is_none());
}
#[test]
fn nonresident_data_runlist_offset_oob_no_location() {
const IMAGE_SIZE: usize = 32 * 1024;
const MFT_OFFSET: usize = 16384;
const MFT_RECORD_SIZE: usize = 1024;
let mut img = vec![0u8; IMAGE_SIZE];
img[..512].copy_from_slice(&make_ntfs_boot_sector());
write_file_record(&mut img, MFT_OFFSET + 5 * MFT_RECORD_SIZE, 5, true);
write_nonresident_file_record_custom(
&mut img,
MFT_OFFSET + 12 * MFT_RECORD_SIZE,
12,
72,
72,
);
let mut c = cursor_of(&img);
let root = detect_and_parse(&mut c).expect("parse");
let big = root.children.iter().find(|n| n.name == "big.bin");
assert!(big.is_some());
assert!(big.unwrap().file_location.is_none());
}
#[test]
fn extract_record_info_filename_attr_parse_returns_none() {
let mut buf = vec![0u8; 2048];
buf[20..22].copy_from_slice(&56u16.to_le_bytes()); let fn_attr_len: u32 = 40;
buf[56..60].copy_from_slice(&ATTR_FILE_NAME.to_le_bytes());
buf[60..64].copy_from_slice(&fn_attr_len.to_le_bytes());
buf[64] = 0; buf[72..76].copy_from_slice(&10u32.to_le_bytes()); buf[76..78].copy_from_slice(&24u16.to_le_bytes()); buf[96..100].copy_from_slice(&ATTR_END.to_le_bytes());
let info = extract_record_info(&buf, 12, 0, 4096, 0);
assert!(
info.is_none(),
"record with no valid FILE_NAME should return None"
);
}
#[test]
fn extract_record_info_filename_attr_resident_data_none() {
let mut buf = vec![0u8; 2048];
buf[20..22].copy_from_slice(&56u16.to_le_bytes());
let fn_attr_len: u32 = 40; buf[56..60].copy_from_slice(&ATTR_FILE_NAME.to_le_bytes());
buf[60..64].copy_from_slice(&fn_attr_len.to_le_bytes());
buf[64] = 0; buf[72..76].copy_from_slice(&200u32.to_le_bytes());
buf[76..78].copy_from_slice(&24u16.to_le_bytes());
buf[96..100].copy_from_slice(&ATTR_END.to_le_bytes());
let info = extract_record_info(&buf, 12, 0, 4096, 0);
assert!(info.is_none());
}
#[test]
fn extract_record_info_data_attr_resident_data_none_no_location() {
let si_attr = make_resident_attr(ATTR_DATA, 0); let mut data_attr = si_attr;
data_attr[16..20].copy_from_slice(&200u32.to_le_bytes()); data_attr[20..22].copy_from_slice(&24u16.to_le_bytes()); let buf = make_eri_buf(ROOT_MFT_RECORD, NS_WIN32_DOS, &data_attr);
let info = extract_record_info(&buf, 12, 0, 4096, 0);
assert!(info.is_some());
assert!(info.unwrap().file_location.is_none());
}
#[test]
fn detect_and_parse_skips_self_referencing_record() {
const IMAGE_SIZE: usize = 32 * 1024;
const MFT_OFFSET: usize = 16384;
const MFT_RECORD_SIZE: usize = 1024;
let mut img = vec![0u8; IMAGE_SIZE];
img[..512].copy_from_slice(&make_ntfs_boot_sector());
write_file_record(&mut img, MFT_OFFSET + 5 * MFT_RECORD_SIZE, 5, true);
let off = MFT_OFFSET + 12 * MFT_RECORD_SIZE;
img[off..off + 4].copy_from_slice(b"FILE");
img[off + 4..off + 6].copy_from_slice(&48u16.to_le_bytes()); img[off + 6..off + 8].copy_from_slice(&3u16.to_le_bytes()); img[off + 48..off + 50].copy_from_slice(&1u16.to_le_bytes()); img[off + 510..off + 512].copy_from_slice(&1u16.to_le_bytes());
img[off + 1022..off + 1024].copy_from_slice(&1u16.to_le_bytes());
img[off + 16..off + 18].copy_from_slice(&1u16.to_le_bytes());
img[off + 18..off + 20].copy_from_slice(&1u16.to_le_bytes());
img[off + 22..off + 24].copy_from_slice(&0x01u16.to_le_bytes()); img[off + 44..off + 48].copy_from_slice(&12u32.to_le_bytes()); img[off + 20..off + 22].copy_from_slice(&56u16.to_le_bytes());
let fn_name: Vec<u16> = "self.txt".encode_utf16().collect();
let fn_name_bytes: Vec<u8> = fn_name.iter().flat_map(|&c| c.to_le_bytes()).collect();
let fn_value_len = 66 + fn_name_bytes.len();
let fn_attr_len = (24 + fn_value_len + 7) & !7;
let fn_start = off + 56;
img[fn_start..fn_start + 4].copy_from_slice(&ATTR_FILE_NAME.to_le_bytes());
img[fn_start + 4..fn_start + 8].copy_from_slice(&(fn_attr_len as u32).to_le_bytes());
img[fn_start + 8] = 0;
img[fn_start + 16..fn_start + 20].copy_from_slice(&(fn_value_len as u32).to_le_bytes());
img[fn_start + 20..fn_start + 22].copy_from_slice(&24u16.to_le_bytes());
let fv = fn_start + 24;
img[fv..fv + 8].copy_from_slice(&12u64.to_le_bytes()); img[fv + 56..fv + 60].copy_from_slice(&0x20u32.to_le_bytes());
img[fv + 64] = fn_name.len() as u8;
img[fv + 65] = NS_WIN32_DOS;
img[fv + 66..fv + 66 + fn_name_bytes.len()].copy_from_slice(&fn_name_bytes);
img[fn_start + fn_attr_len..fn_start + fn_attr_len + 4]
.copy_from_slice(&ATTR_END.to_le_bytes());
let mut c = cursor_of(&img);
let root = detect_and_parse(&mut c).expect("should parse despite self-referencing record");
assert!(
root.children.iter().all(|n| n.name != "self.txt"),
"self-referencing record should be excluded from tree"
);
}
}