use forensicnomicon::ntfs::filename_namespace;
use crate::error::{NtfsError, Result};
use crate::time::Filetime;
const FN_MIN: usize = 0x42;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FileReference {
pub record_number: u64,
pub sequence: u16,
}
impl FileReference {
#[must_use]
pub fn from_u64(raw: u64) -> Self {
FileReference {
record_number: raw & 0x0000_FFFF_FFFF_FFFF,
sequence: (raw >> 48) as u16,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileName {
pub parent: FileReference,
pub created: Filetime,
pub modified: Filetime,
pub mft_modified: Filetime,
pub accessed: Filetime,
pub allocated_size: u64,
pub real_size: u64,
pub flags: u32,
pub namespace: u8,
pub name: String,
}
impl FileName {
#[must_use]
pub fn namespace_name(&self) -> Option<&'static str> {
filename_namespace::name(self.namespace)
}
#[must_use]
pub fn is_dos_namespace(&self) -> bool {
self.namespace == filename_namespace::DOS
}
pub fn parse(content: &[u8]) -> Result<FileName> {
if content.len() < FN_MIN {
return Err(NtfsError::TooShort {
what: "$FILE_NAME",
need: FN_MIN,
got: content.len(),
});
}
let parent =
FileReference::from_u64(u64::from_le_bytes(content[0x00..0x08].try_into().unwrap()));
let ft = |o: usize| Filetime::from_le(content[o..o + 8].try_into().unwrap());
let allocated_size = u64::from_le_bytes(content[0x28..0x30].try_into().unwrap());
let real_size = u64::from_le_bytes(content[0x30..0x38].try_into().unwrap());
let flags = u32::from_le_bytes(content[0x38..0x3C].try_into().unwrap());
let name_length = content[0x40] as usize;
let namespace = content[0x41];
let name_bytes = name_length.checked_mul(2).ok_or(NtfsError::BadAttribute {
offset: 0,
detail: "$FILE_NAME name length overflow",
})?;
let name_end = FN_MIN
.checked_add(name_bytes)
.ok_or(NtfsError::BadAttribute {
offset: 0,
detail: "$FILE_NAME name overflow",
})?;
if name_end > content.len() {
return Err(NtfsError::BadAttribute {
offset: 0,
detail: "$FILE_NAME name extends past content",
});
}
let units: Vec<u16> = content[FN_MIN..name_end]
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
let name = char::decode_utf16(units)
.map(|r| r.unwrap_or('\u{FFFD}'))
.collect();
Ok(FileName {
parent,
created: ft(0x08),
modified: ft(0x10),
mft_modified: ft(0x18),
accessed: ft(0x20),
allocated_size,
real_size,
flags,
namespace,
name,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(clippy::too_many_arguments)]
fn make_fn(
parent: u64,
created: u64,
modified: u64,
mft_modified: u64,
accessed: u64,
allocated: u64,
real: u64,
flags: u32,
namespace: u8,
name: &str,
) -> Vec<u8> {
let name_units: Vec<u16> = name.encode_utf16().collect();
let mut c = vec![0u8; FN_MIN + name_units.len() * 2];
c[0x00..0x08].copy_from_slice(&parent.to_le_bytes());
c[0x08..0x10].copy_from_slice(&created.to_le_bytes());
c[0x10..0x18].copy_from_slice(&modified.to_le_bytes());
c[0x18..0x20].copy_from_slice(&mft_modified.to_le_bytes());
c[0x20..0x28].copy_from_slice(&accessed.to_le_bytes());
c[0x28..0x30].copy_from_slice(&allocated.to_le_bytes());
c[0x30..0x38].copy_from_slice(&real.to_le_bytes());
c[0x38..0x3C].copy_from_slice(&flags.to_le_bytes());
c[0x40] = name_units.len() as u8;
c[0x41] = namespace;
for (i, u) in name_units.iter().enumerate() {
let p = 0x42 + i * 2;
c[p..p + 2].copy_from_slice(&u.to_le_bytes());
}
c
}
#[test]
fn file_reference_splits_record_and_sequence() {
let raw = (5u64 << 48) | 0x1234;
let r = FileReference::from_u64(raw);
assert_eq!(r.record_number, 0x1234);
assert_eq!(r.sequence, 5);
}
#[test]
fn parses_file_name() {
let c = make_fn(
(3u64 << 48) | 5, 0x10,
0x20,
0x30,
0x40,
0x1000,
0x0ABC,
super::super::standard_information::file_attr::ARCHIVE,
filename_namespace::WIN32,
"report.docx",
);
let f = FileName::parse(&c).unwrap();
assert_eq!(f.parent.record_number, 5);
assert_eq!(f.parent.sequence, 3);
assert_eq!(f.created, Filetime(0x10));
assert_eq!(f.accessed, Filetime(0x40));
assert_eq!(f.allocated_size, 0x1000);
assert_eq!(f.real_size, 0x0ABC);
assert_eq!(f.namespace, filename_namespace::WIN32);
assert_eq!(f.namespace_name(), Some("Win32"));
assert!(!f.is_dos_namespace());
assert_eq!(f.name, "report.docx");
}
#[test]
fn detects_dos_namespace() {
let c = make_fn(
0,
0,
0,
0,
0,
0,
0,
0,
filename_namespace::DOS,
"REPORT~1.DOC",
);
let f = FileName::parse(&c).unwrap();
assert!(f.is_dos_namespace());
}
#[test]
fn rejects_name_past_content() {
let mut c = make_fn(0, 0, 0, 0, 0, 0, 0, 0, filename_namespace::WIN32, "ab");
c[0x40] = 200; assert!(matches!(
FileName::parse(&c),
Err(NtfsError::BadAttribute { .. })
));
}
#[test]
fn rejects_too_short() {
let c = vec![0u8; 0x20];
assert!(matches!(
FileName::parse(&c),
Err(NtfsError::TooShort { .. })
));
}
}