use forensicnomicon::ntfs::{mft_flags, mft_offsets as off, SIGNATURE_BAAD, SIGNATURE_FILE};
use crate::error::{NtfsError, Result};
const HEADER_LEN: usize = 0x30;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MftRecordHeader {
pub signature: [u8; 4],
pub usa_offset: u16,
pub usa_count: u16,
pub lsn: u64,
pub sequence_number: u16,
pub hard_link_count: u16,
pub first_attribute_offset: u16,
pub flags: u16,
pub used_size: u32,
pub allocated_size: u32,
pub base_record: u64,
pub next_attr_id: u16,
pub record_number: u32,
}
impl MftRecordHeader {
#[must_use]
pub fn is_in_use(&self) -> bool {
self.flags & mft_flags::IN_USE != 0
}
#[must_use]
pub fn is_directory(&self) -> bool {
self.flags & mft_flags::DIRECTORY != 0
}
#[must_use]
pub fn is_base_record(&self) -> bool {
self.base_record == 0
}
#[must_use]
pub fn is_corrupt(&self) -> bool {
self.signature == SIGNATURE_BAAD
}
pub fn parse(buf: &[u8]) -> Result<MftRecordHeader> {
if buf.len() < HEADER_LEN {
return Err(NtfsError::TooShort {
what: "MFT record header",
need: HEADER_LEN,
got: buf.len(),
});
}
let signature: [u8; 4] = buf[off::SIGNATURE..off::SIGNATURE + 4].try_into().unwrap();
if signature != SIGNATURE_FILE && signature != SIGNATURE_BAAD {
return Err(NtfsError::BadRecordSignature(signature));
}
let u16at = |o: usize| u16::from_le_bytes(buf[o..o + 2].try_into().unwrap());
let u32at = |o: usize| u32::from_le_bytes(buf[o..o + 4].try_into().unwrap());
let u64at = |o: usize| u64::from_le_bytes(buf[o..o + 8].try_into().unwrap());
Ok(MftRecordHeader {
signature,
usa_offset: u16at(off::USA_OFFSET),
usa_count: u16at(off::USA_COUNT),
lsn: u64at(off::LSN),
sequence_number: u16at(off::SEQUENCE_NUMBER),
hard_link_count: u16at(off::HARD_LINK_COUNT),
first_attribute_offset: u16at(off::FIRST_ATTRIBUTE),
flags: u16at(off::FLAGS),
used_size: u32at(off::USED_SIZE),
allocated_size: u32at(off::ALLOCATED_SIZE),
base_record: u64at(off::BASE_RECORD),
next_attr_id: u16at(off::NEXT_ATTR_ID),
record_number: u32at(off::RECORD_NUMBER),
})
}
}
pub fn apply_fixup(buf: &mut [u8], sector_size: usize) -> Result<()> {
if buf.len() < HEADER_LEN {
return Err(NtfsError::TooShort {
what: "MFT record",
need: HEADER_LEN,
got: buf.len(),
});
}
if sector_size < 2 {
return Err(NtfsError::BadUpdateSequence(
"sector size smaller than 2 bytes",
));
}
let usa_offset = u16::from_le_bytes(
buf[off::USA_OFFSET..off::USA_OFFSET + 2]
.try_into()
.unwrap(),
) as usize;
let usa_count =
u16::from_le_bytes(buf[off::USA_COUNT..off::USA_COUNT + 2].try_into().unwrap()) as usize;
if usa_count == 0 {
return Err(NtfsError::BadUpdateSequence("usa_count is zero"));
}
let usa_end = usa_offset
.checked_add(usa_count * 2)
.ok_or(NtfsError::BadUpdateSequence("usa offset/count overflow"))?;
if usa_end > buf.len() {
return Err(NtfsError::BadUpdateSequence("usa extends past record"));
}
let fixup_sectors = usa_count - 1;
let span = fixup_sectors
.checked_mul(sector_size)
.ok_or(NtfsError::BadUpdateSequence("sector span overflow"))?;
if span > buf.len() {
return Err(NtfsError::BadUpdateSequence(
"fixup sectors exceed record size",
));
}
let usn = u16::from_le_bytes(buf[usa_offset..usa_offset + 2].try_into().unwrap());
for i in 0..fixup_sectors {
let tail = (i + 1) * sector_size - 2;
let found = u16::from_le_bytes([buf[tail], buf[tail + 1]]);
if found != usn {
return Err(NtfsError::FixupMismatch {
sector: i,
expected: usn,
found,
});
}
let original = usa_offset + 2 + i * 2;
buf[tail] = buf[original];
buf[tail + 1] = buf[original + 1];
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn make_record(size: usize, sector_size: usize, usn: u16, originals: &[u16]) -> Vec<u8> {
assert_eq!(size / sector_size, originals.len());
let mut b = vec![0u8; size];
b[0..4].copy_from_slice(b"FILE");
let usa_offset: u16 = 0x30;
let usa_count = (originals.len() + 1) as u16;
b[0x04..0x06].copy_from_slice(&usa_offset.to_le_bytes());
b[0x06..0x08].copy_from_slice(&usa_count.to_le_bytes());
let first_attr = usa_offset + usa_count * 2;
b[0x14..0x16].copy_from_slice(&first_attr.to_le_bytes());
b[0x16..0x18].copy_from_slice(&mft_flags::IN_USE.to_le_bytes());
let uo = usa_offset as usize;
b[uo..uo + 2].copy_from_slice(&usn.to_le_bytes());
for (i, orig) in originals.iter().enumerate() {
let p = uo + 2 + i * 2;
b[p..p + 2].copy_from_slice(&orig.to_le_bytes());
let tail = (i + 1) * sector_size - 2;
b[tail..tail + 2].copy_from_slice(&usn.to_le_bytes());
}
b
}
#[test]
fn parses_file_record_header() {
let mut b = make_record(1024, 512, 0xABCD, &[0x1111, 0x2222]);
b[0x08..0x10].copy_from_slice(&0x0000_0000_DEAD_BEEFu64.to_le_bytes()); b[0x10..0x12].copy_from_slice(&7u16.to_le_bytes()); b[0x12..0x14].copy_from_slice(&1u16.to_le_bytes()); b[0x18..0x1C].copy_from_slice(&0x0000_0188u32.to_le_bytes()); b[0x1C..0x20].copy_from_slice(&1024u32.to_le_bytes()); b[0x20..0x28].copy_from_slice(&0u64.to_le_bytes()); b[0x28..0x2A].copy_from_slice(&3u16.to_le_bytes()); b[0x2C..0x30].copy_from_slice(&42u32.to_le_bytes());
let h = MftRecordHeader::parse(&b).expect("valid FILE record");
assert_eq!(&h.signature, b"FILE");
assert_eq!(h.usa_offset, 0x30);
assert_eq!(h.usa_count, 3);
assert_eq!(h.lsn, 0xDEAD_BEEF);
assert_eq!(h.sequence_number, 7);
assert_eq!(h.hard_link_count, 1);
assert_eq!(h.first_attribute_offset, 0x30 + 3 * 2);
assert_eq!(h.used_size, 0x188);
assert_eq!(h.allocated_size, 1024);
assert_eq!(h.next_attr_id, 3);
assert_eq!(h.record_number, 42);
assert!(h.is_in_use());
assert!(!h.is_directory());
assert!(h.is_base_record());
assert!(!h.is_corrupt());
}
#[test]
fn directory_and_extension_flags_decode() {
let mut b = make_record(1024, 512, 1, &[0, 0]);
b[0x16..0x18].copy_from_slice(&(mft_flags::IN_USE | mft_flags::DIRECTORY).to_le_bytes());
b[0x20..0x28].copy_from_slice(&0x0001_0000_0000_0005u64.to_le_bytes()); let h = MftRecordHeader::parse(&b).unwrap();
assert!(h.is_in_use());
assert!(h.is_directory());
assert!(!h.is_base_record());
}
#[test]
fn baad_signature_parses_as_corrupt() {
let mut b = make_record(1024, 512, 1, &[0, 0]);
b[0..4].copy_from_slice(b"BAAD");
let h = MftRecordHeader::parse(&b).expect("BAAD is a valid (corrupt) record");
assert!(h.is_corrupt());
}
#[test]
fn rejects_unknown_signature() {
let mut b = make_record(1024, 512, 1, &[0, 0]);
b[0..4].copy_from_slice(b"XXXX");
assert!(matches!(
MftRecordHeader::parse(&b),
Err(NtfsError::BadRecordSignature(s)) if &s == b"XXXX"
));
}
#[test]
fn header_too_short_returns_error() {
let b = vec![b'F', b'I', b'L', b'E', 0, 0];
assert!(matches!(
MftRecordHeader::parse(&b),
Err(NtfsError::TooShort { .. })
));
}
#[test]
fn fixup_restores_sector_tails() {
let mut b = make_record(1024, 512, 0xABCD, &[0x1111, 0x2222]);
assert_eq!(&b[510..512], &0xABCDu16.to_le_bytes());
assert_eq!(&b[1022..1024], &0xABCDu16.to_le_bytes());
apply_fixup(&mut b, 512).expect("valid fixup");
assert_eq!(u16::from_le_bytes([b[510], b[511]]), 0x1111);
assert_eq!(u16::from_le_bytes([b[1022], b[1023]]), 0x2222);
}
#[test]
fn fixup_detects_torn_write() {
let mut b = make_record(1024, 512, 0xABCD, &[0x1111, 0x2222]);
b[1022..1024].copy_from_slice(&0xDEADu16.to_le_bytes());
assert!(matches!(
apply_fixup(&mut b, 512),
Err(NtfsError::FixupMismatch {
sector: 1,
expected: 0xABCD,
found: 0xDEAD
})
));
}
#[test]
fn fixup_rejects_zero_usa_count() {
let mut b = make_record(1024, 512, 1, &[0, 0]);
b[0x06..0x08].copy_from_slice(&0u16.to_le_bytes()); assert!(matches!(
apply_fixup(&mut b, 512),
Err(NtfsError::BadUpdateSequence(_))
));
}
#[test]
fn fixup_rejects_usa_out_of_bounds() {
let mut b = make_record(1024, 512, 1, &[0, 0]);
b[0x04..0x06].copy_from_slice(&0x0FFEu16.to_le_bytes()); b[0x06..0x08].copy_from_slice(&8u16.to_le_bytes()); assert!(matches!(
apply_fixup(&mut b, 512),
Err(NtfsError::BadUpdateSequence(_))
));
}
#[test]
fn fixup_rejects_buffer_shorter_than_header() {
let mut b = vec![0u8; HEADER_LEN - 1];
assert!(matches!(
apply_fixup(&mut b, 512),
Err(NtfsError::TooShort { .. })
));
}
#[test]
fn fixup_rejects_sector_size_below_two() {
let mut b = make_record(1024, 512, 1, &[0, 0]);
assert!(matches!(
apply_fixup(&mut b, 1),
Err(NtfsError::BadUpdateSequence(_))
));
}
#[test]
fn fixup_rejects_sectors_exceeding_record() {
let mut b = make_record(1024, 512, 1, &[0, 0]);
b[0x06..0x08].copy_from_slice(&10u16.to_le_bytes()); assert!(matches!(
apply_fixup(&mut b, 512),
Err(NtfsError::BadUpdateSequence(detail)) if detail.contains("exceed")
));
}
}