use std::sync::Arc;
use super::{
attribute_list::{NtfsAttributeListEntry, parse_attribute_list},
reparse::{NtfsReparsePointInfo, parse_reparse_point},
};
use crate::{Error, Result};
const FILE_RECORD_SIGNATURE: &[u8; 4] = b"FILE";
const FILE_RECORD_FLAG_IN_USE: u16 = 0x0001;
const FILE_RECORD_FLAG_DIRECTORY: u16 = 0x0002;
const ATTRIBUTE_TYPE_ATTRIBUTE_LIST: u32 = 0x0000_0020;
const ATTRIBUTE_TYPE_FILE_NAME: u32 = 0x0000_0030;
const ATTRIBUTE_TYPE_DATA: u32 = 0x0000_0080;
const ATTRIBUTE_TYPE_INDEX_ROOT: u32 = 0x0000_0090;
const ATTRIBUTE_TYPE_INDEX_ALLOCATION: u32 = 0x0000_00A0;
const ATTRIBUTE_TYPE_REPARSE_POINT: u32 = 0x0000_00C0;
const ATTRIBUTE_TYPE_END: u32 = 0xFFFF_FFFF;
const ATTRIBUTE_FLAG_COMPRESSION_MASK: u16 = 0x00FF;
const ATTRIBUTE_FLAG_ENCRYPTED: u16 = 0x4000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct NtfsFileNameAttribute {
pub attribute_id: u16,
pub parent_record_number: u64,
pub name: String,
pub namespace: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct NtfsNonResidentAttribute {
pub first_vcn: u64,
pub last_vcn: u64,
pub compression_unit: u16,
pub data_size: u64,
pub valid_data_size: u64,
pub runlist: Arc<[u8]>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum NtfsDataAttributeValue {
Resident(Arc<[u8]>),
NonResident(NtfsNonResidentAttribute),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct NtfsDataAttribute {
pub attribute_id: u16,
pub name: Option<String>,
pub data_flags: u16,
pub value: NtfsDataAttributeValue,
}
#[derive(Debug, Clone)]
pub(crate) struct NtfsFileRecord {
pub flags: u16,
pub base_record_number: Option<u64>,
pub file_names: Vec<NtfsFileNameAttribute>,
pub data_attributes: Vec<NtfsDataAttribute>,
pub attribute_list_entries: Vec<NtfsAttributeListEntry>,
pub attribute_list_attributes: Vec<NtfsDataAttribute>,
pub index_root_attributes: Vec<NtfsDataAttribute>,
pub index_allocation_attributes: Vec<NtfsDataAttribute>,
pub reparse_point: Option<NtfsReparsePointInfo>,
pub has_reparse_point: bool,
}
impl NtfsFileRecord {
pub fn is_directory(&self) -> bool {
self.flags & FILE_RECORD_FLAG_DIRECTORY != 0
}
pub fn preferred_name(&self) -> Option<&NtfsFileNameAttribute> {
self
.file_names
.iter()
.min_by_key(|file_name| namespace_priority(file_name.namespace))
}
}
pub(crate) fn parse_file_record(raw: &[u8], record_number: u64) -> Result<Option<NtfsFileRecord>> {
if raw.iter().all(|byte| *byte == 0) {
return Ok(None);
}
let fixed = apply_update_sequence(raw)?;
if &fixed[0..4] != FILE_RECORD_SIGNATURE {
return Err(Error::invalid_format(format!(
"ntfs file record {record_number} has an invalid signature"
)));
}
let used_size = usize::try_from(le_u32(&fixed[24..28]))
.map_err(|_| Error::invalid_range("ntfs file record used size is too large"))?;
let flags = le_u16(&fixed[22..24]);
if flags & FILE_RECORD_FLAG_IN_USE == 0 || used_size == 0 {
return Ok(None);
}
if used_size > fixed.len() {
return Err(Error::invalid_format(format!(
"ntfs file record {record_number} extends past its allocated size"
)));
}
let base_reference = decode_file_reference(&fixed[32..40])?;
let base_record_number = if base_reference == 0 {
None
} else {
Some(base_reference)
};
let attributes_offset = usize::from(le_u16(&fixed[20..22]));
if attributes_offset > used_size {
return Err(Error::invalid_format(format!(
"ntfs file record {record_number} attributes start outside the used region"
)));
}
let mut file_names = Vec::new();
let mut data_attributes = Vec::new();
let mut attribute_list_entries = Vec::new();
let mut attribute_list_attributes = Vec::new();
let mut index_root_attributes = Vec::new();
let mut index_allocation_attributes = Vec::new();
let mut reparse_point = None;
let mut has_reparse_point = false;
let mut cursor = attributes_offset;
let used = &fixed[..used_size];
while cursor + 8 <= used.len() {
let attribute_type = le_u32(&used[cursor..cursor + 4]);
if attribute_type == ATTRIBUTE_TYPE_END {
break;
}
let attribute_size = usize::try_from(le_u32(&used[cursor + 4..cursor + 8])).map_err(|_| {
Error::invalid_range(format!(
"ntfs file record {record_number} attribute size is too large"
))
})?;
if attribute_size < 16 {
return Err(Error::invalid_format(format!(
"ntfs file record {record_number} contains a truncated attribute"
)));
}
let attribute_end = cursor
.checked_add(attribute_size)
.ok_or_else(|| Error::invalid_range("ntfs attribute end offset overflow"))?;
if attribute_end > used.len() {
return Err(Error::invalid_format(format!(
"ntfs file record {record_number} attribute exceeds the used region"
)));
}
let attribute = &used[cursor..attribute_end];
let non_resident = attribute[8] != 0;
let name_length = usize::from(attribute[9]);
let name_offset = usize::from(le_u16(&attribute[10..12]));
let data_flags = le_u16(&attribute[12..14]);
let attribute_id = le_u16(&attribute[14..16]);
let attribute_name = if name_length == 0 {
None
} else {
Some(read_utf16le(
attribute,
name_offset,
name_length,
"ntfs attribute name",
)?)
};
match attribute_type {
ATTRIBUTE_TYPE_ATTRIBUTE_LIST => {
if non_resident {
attribute_list_attributes.push(parse_stream_attribute(
attribute,
attribute_name,
attribute_id,
record_number,
"ntfs $ATTRIBUTE_LIST",
)?);
} else {
attribute_list_entries.extend(parse_attribute_list(resident_attribute_data(
attribute,
"ntfs $ATTRIBUTE_LIST",
)?)?);
}
}
ATTRIBUTE_TYPE_FILE_NAME => {
if non_resident {
return Err(Error::invalid_format(format!(
"ntfs file record {record_number} stores $FILE_NAME as non-resident"
)));
}
file_names.push(parse_file_name_attribute(attribute, attribute_id)?);
}
ATTRIBUTE_TYPE_DATA => {
if data_flags & ATTRIBUTE_FLAG_ENCRYPTED != 0 {
return Err(Error::invalid_format(format!(
"ntfs encrypted data attributes are not supported in record {record_number}"
)));
}
data_attributes.push(parse_stream_attribute(
attribute,
attribute_name,
attribute_id,
record_number,
"ntfs $DATA",
)?);
}
ATTRIBUTE_TYPE_INDEX_ROOT => {
index_root_attributes.push(parse_stream_attribute(
attribute,
attribute_name,
attribute_id,
record_number,
"ntfs $INDEX_ROOT",
)?);
}
ATTRIBUTE_TYPE_INDEX_ALLOCATION => {
index_allocation_attributes.push(parse_stream_attribute(
attribute,
attribute_name,
attribute_id,
record_number,
"ntfs $INDEX_ALLOCATION",
)?);
}
ATTRIBUTE_TYPE_REPARSE_POINT => {
if non_resident {
return Err(Error::invalid_format(format!(
"ntfs file record {record_number} stores $REPARSE_POINT as non-resident"
)));
}
reparse_point = Some(parse_reparse_point(resident_attribute_data(
attribute,
"ntfs $REPARSE_POINT",
)?)?);
has_reparse_point = true;
}
_ => {}
}
cursor = attribute_end;
}
Ok(Some(NtfsFileRecord {
flags,
base_record_number,
file_names,
data_attributes,
attribute_list_entries,
attribute_list_attributes,
index_root_attributes,
index_allocation_attributes,
reparse_point,
has_reparse_point,
}))
}
fn apply_update_sequence(raw: &[u8]) -> Result<Vec<u8>> {
if raw.len() < 48 {
return Err(Error::invalid_format(
"ntfs file record is too small".to_string(),
));
}
let mut fixed = raw.to_vec();
let update_sequence_offset = usize::from(le_u16(&fixed[4..6]));
let update_sequence_count = usize::from(le_u16(&fixed[6..8]));
if update_sequence_count == 0 {
return Err(Error::invalid_format(
"ntfs update-sequence array must contain at least one element".to_string(),
));
}
let array_size = update_sequence_count
.checked_mul(2)
.ok_or_else(|| Error::invalid_range("ntfs update-sequence array overflow"))?;
let update_sequence_end = update_sequence_offset
.checked_add(array_size)
.ok_or_else(|| Error::invalid_range("ntfs update-sequence array overflow"))?;
if update_sequence_end > fixed.len() {
return Err(Error::invalid_format(
"ntfs update-sequence array exceeds the file record".to_string(),
));
}
let sequence = [
fixed[update_sequence_offset],
fixed[update_sequence_offset + 1],
];
for index in 1..update_sequence_count {
let sector_tail = index
.checked_mul(512)
.and_then(|value| value.checked_sub(2))
.ok_or_else(|| Error::invalid_range("ntfs sector-tail offset overflow"))?;
if sector_tail + 2 > fixed.len() {
return Err(Error::invalid_format(
"ntfs update-sequence array references data past the file record".to_string(),
));
}
let replacement_offset = update_sequence_offset + index * 2;
let (prefix, suffix) = fixed.split_at_mut(sector_tail);
let sector_tail_bytes = &mut suffix[..2];
if *sector_tail_bytes != sequence {
return Err(Error::invalid_format(
"ntfs file record fixup verification failed".to_string(),
));
}
let replacement = [prefix[replacement_offset], prefix[replacement_offset + 1]];
sector_tail_bytes.copy_from_slice(&replacement);
}
Ok(fixed)
}
fn parse_file_name_attribute(attribute: &[u8], attribute_id: u16) -> Result<NtfsFileNameAttribute> {
let data = resident_attribute_data(attribute, "ntfs $FILE_NAME")?;
if data.len() < 66 {
return Err(Error::invalid_format(
"ntfs $FILE_NAME attribute is too small".to_string(),
));
}
let name_length = usize::from(data[64]);
let namespace = data[65];
Ok(NtfsFileNameAttribute {
attribute_id,
parent_record_number: decode_file_reference(&data[0..8])?,
name: read_utf16le(data, 66, name_length, "ntfs $FILE_NAME string")?,
namespace,
})
}
fn parse_stream_attribute(
attribute: &[u8], name: Option<String>, attribute_id: u16, record_number: u64, label: &str,
) -> Result<NtfsDataAttribute> {
let data_flags = le_u16(&attribute[12..14]);
let value = if attribute[8] == 0 {
NtfsDataAttributeValue::Resident(Arc::from(resident_attribute_data(attribute, label)?))
} else {
if attribute.len() < 64 {
return Err(Error::invalid_format(format!(
"{label} attribute in record {record_number} is truncated"
)));
}
let first_vcn = le_u64(&attribute[16..24]);
let last_vcn = le_u64(&attribute[24..32]);
if last_vcn < first_vcn {
return Err(Error::invalid_format(format!(
"{label} attribute in record {record_number} has an invalid VCN range"
)));
}
let runlist_offset = usize::from(le_u16(&attribute[32..34]));
if runlist_offset > attribute.len() {
return Err(Error::invalid_format(format!(
"{label} attribute in record {record_number} has an invalid runlist offset"
)));
}
let compression_unit = le_u16(&attribute[34..36]);
let compression_method = data_flags & ATTRIBUTE_FLAG_COMPRESSION_MASK;
if compression_method > 1 {
return Err(Error::invalid_format(format!(
"unsupported ntfs compression method 0x{compression_method:04x} in record {record_number}"
)));
}
NtfsDataAttributeValue::NonResident(NtfsNonResidentAttribute {
first_vcn,
last_vcn,
compression_unit,
data_size: le_u64(&attribute[48..56]),
valid_data_size: le_u64(&attribute[56..64]),
runlist: Arc::from(&attribute[runlist_offset..]),
})
};
Ok(NtfsDataAttribute {
attribute_id,
name,
data_flags,
value,
})
}
fn resident_attribute_data<'a>(attribute: &'a [u8], label: &str) -> Result<&'a [u8]> {
if attribute.len() < 24 {
return Err(Error::invalid_format(format!(
"{label} header is truncated"
)));
}
let data_length = usize::try_from(le_u32(&attribute[16..20]))
.map_err(|_| Error::invalid_range(format!("{label} data length is too large")))?;
let data_offset = usize::from(le_u16(&attribute[20..22]));
let data_end = data_offset
.checked_add(data_length)
.ok_or_else(|| Error::invalid_range(format!("{label} data offset overflow")))?;
if data_end > attribute.len() {
return Err(Error::invalid_format(format!(
"{label} data extends past the attribute boundary"
)));
}
Ok(&attribute[data_offset..data_end])
}
fn read_utf16le(bytes: &[u8], offset: usize, chars: usize, label: &str) -> Result<String> {
let byte_len = chars
.checked_mul(2)
.ok_or_else(|| Error::invalid_range(format!("{label} length overflow")))?;
let end = offset
.checked_add(byte_len)
.ok_or_else(|| Error::invalid_range(format!("{label} offset overflow")))?;
let slice = bytes
.get(offset..end)
.ok_or_else(|| Error::invalid_format(format!("{label} extends past the available bytes")))?;
let units = slice
.chunks_exact(2)
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
.collect::<Vec<_>>();
String::from_utf16(&units)
.map_err(|_| Error::invalid_format(format!("{label} is not valid UTF-16")))
}
fn decode_file_reference(bytes: &[u8]) -> Result<u64> {
let bytes = bytes
.get(..8)
.ok_or_else(|| Error::invalid_format("ntfs file reference is truncated"))?;
let mut raw = [0u8; 8];
raw[..6].copy_from_slice(&bytes[..6]);
Ok(u64::from_le_bytes(raw))
}
fn namespace_priority(namespace: u8) -> u8 {
match namespace {
1 | 3 => 0,
0 => 1,
2 => 2,
other => other.saturating_add(3),
}
}
fn le_u16(bytes: &[u8]) -> u16 {
let mut raw = [0u8; 2];
raw.copy_from_slice(bytes);
u16::from_le_bytes(raw)
}
fn le_u32(bytes: &[u8]) -> u32 {
let mut raw = [0u8; 4];
raw.copy_from_slice(bytes);
u32::from_le_bytes(raw)
}
fn le_u64(bytes: &[u8]) -> u64 {
let mut raw = [0u8; 8];
raw.copy_from_slice(bytes);
u64::from_le_bytes(raw)
}
#[cfg(test)]
mod tests {
use super::*;
fn non_resident_data_attribute_bytes(data_flags: u16, compression_unit: u16) -> Vec<u8> {
non_resident_attribute_bytes(ATTRIBUTE_TYPE_DATA, data_flags, compression_unit)
}
fn non_resident_attribute_bytes(
attribute_type: u32, data_flags: u16, compression_unit: u16,
) -> Vec<u8> {
let mut attribute = vec![0u8; 70];
let attribute_len = attribute.len() as u32;
attribute[0..4].copy_from_slice(&attribute_type.to_le_bytes());
attribute[4..8].copy_from_slice(&attribute_len.to_le_bytes());
attribute[8] = 1;
attribute[12..14].copy_from_slice(&data_flags.to_le_bytes());
attribute[14..16].copy_from_slice(&5u16.to_le_bytes());
attribute[24..32].copy_from_slice(&15u64.to_le_bytes());
attribute[32..34].copy_from_slice(&64u16.to_le_bytes());
attribute[34..36].copy_from_slice(&compression_unit.to_le_bytes());
attribute[48..56].copy_from_slice(&12u64.to_le_bytes());
attribute[56..64].copy_from_slice(&12u64.to_le_bytes());
attribute[64..70].copy_from_slice(&[0x11, 0x01, 0x01, 0x01, 0x0F, 0x00]);
attribute
}
fn synthetic_file_record(attribute: &[u8]) -> Vec<u8> {
let mut record = vec![0u8; 512];
let record_len = record.len() as u32;
record[0..4].copy_from_slice(FILE_RECORD_SIGNATURE);
record[4..6].copy_from_slice(&48u16.to_le_bytes());
record[6..8].copy_from_slice(&2u16.to_le_bytes());
record[16..18].copy_from_slice(&1u16.to_le_bytes());
record[20..22].copy_from_slice(&56u16.to_le_bytes());
record[22..24].copy_from_slice(&FILE_RECORD_FLAG_IN_USE.to_le_bytes());
let used_size = 56 + attribute.len() + 4;
record[24..28].copy_from_slice(&(used_size as u32).to_le_bytes());
record[28..32].copy_from_slice(&record_len.to_le_bytes());
record[40..42].copy_from_slice(&2u16.to_le_bytes());
record[48..50].copy_from_slice(&[0xAA, 0xBB]);
record[50..52].copy_from_slice(&[0x11, 0x22]);
record[56..56 + attribute.len()].copy_from_slice(attribute);
record[56 + attribute.len()..60 + attribute.len()]
.copy_from_slice(&ATTRIBUTE_TYPE_END.to_le_bytes());
record[510..512].copy_from_slice(&[0xAA, 0xBB]);
record
}
#[test]
fn decodes_file_reference_record_number() {
let reference = [0x34, 0x12, 0, 0, 0, 0, 0x78, 0x56];
assert_eq!(decode_file_reference(&reference).unwrap(), 0x1234);
}
#[test]
fn parses_compressed_nonresident_data_attributes() {
let attribute = non_resident_data_attribute_bytes(0x0001, 0);
let parsed = parse_stream_attribute(&attribute, None, 5, 50, "ntfs $DATA").unwrap();
let NtfsDataAttributeValue::NonResident(non_resident) = parsed.value else {
panic!("expected a non-resident attribute");
};
assert_eq!(non_resident.first_vcn, 0);
assert_eq!(non_resident.last_vcn, 15);
assert_eq!(non_resident.compression_unit, 0);
assert_eq!(non_resident.data_size, 12);
assert_eq!(non_resident.valid_data_size, 12);
}
#[test]
fn parses_nonresident_attribute_list_stream_attributes() {
let attribute = non_resident_data_attribute_bytes(0, 0);
let parsed = parse_stream_attribute(&attribute, None, 5, 50, "ntfs $ATTRIBUTE_LIST").unwrap();
let NtfsDataAttributeValue::NonResident(non_resident) = parsed.value else {
panic!("expected a non-resident attribute");
};
assert_eq!(non_resident.first_vcn, 0);
assert_eq!(non_resident.last_vcn, 15);
assert_eq!(non_resident.data_size, 12);
}
#[test]
fn parses_nonresident_attribute_list_file_record_attributes() {
let record = synthetic_file_record(&non_resident_attribute_bytes(
ATTRIBUTE_TYPE_ATTRIBUTE_LIST,
0,
0,
));
let parsed = parse_file_record(&record, 342).unwrap().unwrap();
assert!(parsed.attribute_list_entries.is_empty());
assert_eq!(parsed.attribute_list_attributes.len(), 1);
assert!(matches!(
parsed.attribute_list_attributes[0].value,
NtfsDataAttributeValue::NonResident(_)
));
}
}