// Copyright 2021 Colin Finck // SPDX-License-Identifier: GPL-2.0-or-later use crate::attribute::{NtfsAttribute, NtfsAttributeType, NtfsAttributes}; use crate::error::{NtfsError, Result}; use crate::index::NtfsIndex; use crate::indexes::NtfsFileNameIndex; use crate::ntfs::Ntfs; use crate::record::{Record, RecordHeader}; use crate::structured_values::{ NtfsFileName, NtfsIndexAllocation, NtfsIndexRoot, NtfsStandardInformation, }; use binread::io::{Read, Seek, SeekFrom}; use bitflags::bitflags; use byteorder::{ByteOrder, LittleEndian}; use memoffset::offset_of; #[repr(u64)] pub enum KnownNtfsFileRecordNumber { MFT = 0, MFTMirr = 1, LogFile = 2, Volume = 3, AttrDef = 4, RootDirectory = 5, Bitmap = 6, Boot = 7, BadClus = 8, Secure = 9, UpCase = 10, Extend = 11, } #[repr(C, packed)] struct FileRecordHeader { record_header: RecordHeader, sequence_number: u16, hard_link_count: u16, first_attribute_offset: u16, flags: u16, used_size: u32, allocated_size: u32, base_file_record: u64, next_attribute_number: u16, } bitflags! { pub struct NtfsFileFlags: u16 { /// Record is in use. const IN_USE = 0x0001; /// Record is a directory. const IS_DIRECTORY = 0x0002; } } #[derive(Debug)] pub struct NtfsFile<'n> { record: Record<'n>, } impl<'n> NtfsFile<'n> { pub(crate) fn new(ntfs: &'n Ntfs, fs: &mut T, position: u64) -> Result where T: Read + Seek, { let mut data = vec![0; ntfs.file_record_size() as usize]; fs.seek(SeekFrom::Start(position))?; fs.read_exact(&mut data)?; let mut record = Record::new(ntfs, data, position); record.fixup()?; let file = Self { record }; file.validate_signature()?; file.validate_sizes()?; Ok(file) } pub fn allocated_size(&self) -> u32 { let start = offset_of!(FileRecordHeader, allocated_size); LittleEndian::read_u32(&self.record.data()[start..]) } /// Returns the first attribute of the given type, or `NtfsError::AttributeNotFound`. pub(crate) fn attribute_by_ty<'f>( &'f self, ty: NtfsAttributeType, ) -> Result> { self.attributes() .find(|attribute| { // TODO: Replace by attribute.ty().contains() once https://github.com/rust-lang/rust/issues/62358 has landed. attribute.ty().map(|x| x == ty).unwrap_or(false) }) .ok_or(NtfsError::AttributeNotFound { position: self.position(), ty, }) } pub fn attributes<'f>(&'f self) -> NtfsAttributes<'n, 'f> { NtfsAttributes::new(self) } /// Convenience function to get a $DATA attribute of this file. /// /// As NTFS supports multiple data streams per file, you can optionally specify a data stream /// name and NTFS will look up the corresponding $DATA attribute. /// If you specify `None` for `data_stream_name`, the default unnamed $DATA attribute will be looked /// up (commonly known as the "file data"). /// /// If you need more control over which $DATA attribute is available and picked up, /// you can use [`NtfsFile::attributes`] to iterate over all attributes of this file. pub fn data<'f>( &'f self, data_stream_name: Option<&str>, ) -> Option>> { // Create an iterator that emits all $DATA attributes. let iter = self.attributes().filter(|attribute| { // TODO: Replace by attribute.ty().contains() once https://github.com/rust-lang/rust/issues/62358 has landed. attribute .ty() .map(|ty| ty == NtfsAttributeType::Data) .unwrap_or(false) }); for attribute in iter { match (attribute.name(), data_stream_name) { (None, None) => { // We found the unnamed $DATA attribute and are looking for the unnamed $DATA attribute. return Some(Ok(attribute)); } (Some(Ok(name)), Some(data_stream_name)) => { // We found a named $DATA attribute and are looking for a named $DATA attribute. if data_stream_name == name { return Some(Ok(attribute)); } } (Some(Err(e)), _) => { // We hit an error while fetching the $DATA attribute name. return Some(Err(e)); } _ => { // In any other case, we didn't find what we are looking for. continue; } } } None } /// Convenience function to return an [`NtfsIndex`] if this file is a directory. /// /// Apart from any propagated error, this function may return [`NtfsError::NotADirectory`] /// if this [`NtfsFile`] is not a directory. /// /// If you need more control over the picked up $INDEX_ROOT and $INDEX_ALLOCATION attributes /// you can use [`NtfsFile::attributes`] to iterate over all attributes of this file. pub fn directory_index<'f, T>( &'f self, fs: &mut T, ) -> Result> where T: Read + Seek, { if !self.flags().contains(NtfsFileFlags::IS_DIRECTORY) { return Err(NtfsError::NotADirectory { position: self.position(), }); } // Get the Index Root attribute that needs to exist. let index_root = self .attribute_by_ty(NtfsAttributeType::IndexRoot)? .resident_structured_value::()?; // Get the Index Allocation attribute that is only required for large indexes. let index_allocation_attribute = self.attribute_by_ty(NtfsAttributeType::IndexAllocation); let index_allocation = if let Ok(attribute) = index_allocation_attribute { Some(attribute.non_resident_structured_value::<_, NtfsIndexAllocation>(fs)?) } else { None }; NtfsIndex::::new(index_root, index_allocation) } pub(crate) fn first_attribute_offset(&self) -> u16 { let start = offset_of!(FileRecordHeader, first_attribute_offset); LittleEndian::read_u16(&self.record.data()[start..]) } /// Returns flags set for this NTFS file as specified by [`NtfsFileFlags`]. pub fn flags(&self) -> NtfsFileFlags { let start = offset_of!(FileRecordHeader, flags); NtfsFileFlags::from_bits_truncate(LittleEndian::read_u16(&self.record.data()[start..])) } pub fn hard_link_count(&self) -> u16 { let start = offset_of!(FileRecordHeader, hard_link_count); LittleEndian::read_u16(&self.record.data()[start..]) } /// Convenience function to get the $STANDARD_INFORMATION attribute of this file /// (see [`NtfsStandardInformation`]). /// /// This internally calls [`NtfsFile::attributes`] to iterate through the file's /// attributes and pick up the first $STANDARD_INFORMATION attribute. pub fn info(&self) -> Result { let attribute = self.attribute_by_ty(NtfsAttributeType::StandardInformation)?; attribute.resident_structured_value::() } /// Convenience function to get the $FILE_NAME attribute of this file (see [`NtfsFileName`]). /// /// This internally calls [`NtfsFile::attributes`] to iterate through the file's /// attributes and pick up the first $FILE_NAME attribute. pub fn name(&self) -> Result { let attribute = self.attribute_by_ty(NtfsAttributeType::FileName)?; attribute.resident_structured_value::() } pub(crate) fn ntfs(&self) -> &'n Ntfs { self.record.ntfs() } pub fn position(&self) -> u64 { self.record.position() } pub(crate) fn record_data(&self) -> &[u8] { self.record.data() } pub fn sequence_number(&self) -> u16 { let start = offset_of!(FileRecordHeader, sequence_number); LittleEndian::read_u16(&self.record.data()[start..]) } pub fn used_size(&self) -> u32 { let start = offset_of!(FileRecordHeader, used_size); LittleEndian::read_u32(&self.record.data()[start..]) } fn validate_signature(&self) -> Result<()> { let signature = &self.record.signature(); let expected = b"FILE"; if signature == expected { Ok(()) } else { Err(NtfsError::InvalidFileSignature { position: self.record.position(), expected, actual: *signature, }) } } fn validate_sizes(&self) -> Result<()> { if self.allocated_size() > self.record.len() { return Err(NtfsError::InvalidFileAllocatedSize { position: self.record.position(), expected: self.allocated_size(), actual: self.record.len(), }); } if self.used_size() > self.allocated_size() { return Err(NtfsError::InvalidFileUsedSize { position: self.record.position(), expected: self.used_size(), actual: self.allocated_size(), }); } Ok(()) } }