teamy-mft 0.7.0

TeamDman's Master File Table CLI and library for NTFS.
use crate::mft::mft_record_attribute_iter::MftRecordAttributeIter;
use crate::mft::mft_record_flags::MftRecordFlags;
use crate::mft::mft_record_location::MftRecordLocationOnDisk;
use crate::mft::mft_record_number::MftRecordNumber;
use crate::mft::mft_record_size::MftRecordSize;
use crate::windows_utils::storage::HandleReadExt;
use bytes::Bytes;
use eyre::bail;
use std::ops::Deref;
use tracing::instrument;
use uom::si::information::byte;

/// Zero-copy record view backed by `bytes::Bytes`.
/// Can be cloned cheaply and stored in ECS components.
pub struct MftRecord {
    data: Bytes,
}

impl Deref for MftRecord {
    type Target = Bytes;
    fn deref(&self) -> &Self::Target {
        &self.data
    }
}

impl std::fmt::Debug for MftRecord {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("MftRecord")
            .field("signature", &self.get_signature())
            .field("record_number", &self.get_record_number())
            .field("used_size", &self.get_used_size())
            .field("allocated_size", &self.get_allocated_size())
            .finish()
    }
}

impl MftRecord {
    /// Construct a record without validating the signature.
    ///
    /// Use this when the caller already ensured the slice is a single MFT record
    /// with fixups applied. This avoids redundant checks and copying.
    #[inline]
    pub fn from_bytes_unchecked(bytes: Bytes) -> Self {
        Self { data: bytes }
    }

    // ---- Raw field offset constants (for clarity & reuse) ----
    const OFFSET_FOR_SIGNATURE: usize = 0x00;
    const OFFSET_FOR_UPDATE_SEQUENCE_ARRAY_OFFSET: usize = 0x04; // u16
    const OFFSET_FOR_UPDATE_SEQUENCE_ARRAY_SIZE: usize = 0x06; // u16 (count of 2-byte words)
    const OFFSET_FOR_LOGFILE_SEQUENCE_NUMBER: usize = 0x08; // u64 ($LogFile sequence number / LSN)
    const OFFSET_FOR_SEQUENCE: usize = 0x10; // u16
    const OFFSET_FOR_HARDLINKS: usize = 0x12; // u16
    const OFFSET_FOR_FIRST_ATTR: usize = 0x14; // u16
    const OFFSET_FOR_FLAGS: usize = 0x16; // u16
    const OFFSET_FOR_USED_SIZE: usize = 0x18; // u32
    const OFFSET_FOR_ALLOC_SIZE: usize = 0x1C; // u32
    const OFFSET_FOR_BASE_REF: usize = 0x20; // u64
    const OFFSET_FOR_NEXT_ATTR_ID: usize = 0x28; // u16
    // 0x2A padding
    const OFFSET_FOR_RECORD_NUMBER: usize = 0x2C; // u32 on-disk

    /// Read a single MFT record from the given drive handle at the specified location.
    /// Validates the "FILE" signature.
    ///
    /// Useful for reading the $MFT record itself (record 0) or other known record numbers.
    ///
    /// # Errors
    ///
    /// Returns an error if the record cannot be read or parsed.
    #[instrument(skip_all)]
    pub fn try_from_handle(
        drive_handle: &impl HandleReadExt,
        mft_record_location: MftRecordLocationOnDisk,
        mft_record_size: MftRecordSize,
    ) -> eyre::Result<Self> {
        let mut data = vec![0u8; mft_record_size.get::<byte>()];
        // Convert the location to a signed offset for the read call. Use a checked conversion
        // so we fail with a clear error instead of silently wrapping on platforms where
        // `usize` may be larger than `i64`.
        let offset_usize = mft_record_location.get::<byte>();
        let offset_i64 = i64::try_from(offset_usize)
            .map_err(|e| eyre::eyre!("MFT record location doesn't fit in i64: {e}"))?;

        drive_handle.try_read_exact(offset_i64, data.as_mut_slice())?;
        if data.len() < 4 || &data[0..4] != b"FILE" {
            bail!(
                "Invalid MFT record signature: expected 'FILE', got {:?}",
                String::from_utf8_lossy(&data[0..4])
            );
        }
        Ok(Self {
            data: Bytes::from(data),
        })
    }

    /// Zero-copy access to the 4-byte signature.
    pub fn get_signature(&self) -> &[u8; 4] {
        // SAFETY: an MFT record is stored as a fixed-size buffer and the first 4 bytes
        // (signature) are always present when this object was created. We therefore
        // can safely obtain a reference to a [u8;4] at offset 0.
        unsafe {
            &*self
                .data
                .as_ptr()
                .add(Self::OFFSET_FOR_SIGNATURE)
                .cast::<[u8; 4]>()
        }
    }

    #[inline]
    fn read_u16(&self, offset: usize) -> u16 {
        // SAFETY: caller must ensure the requested offset is within the underlying
        // record buffer. This function performs an unaligned read and converts from
        // little-endian byte order.
        unsafe {
            u16::from_le(std::ptr::read_unaligned(
                self.data.as_ptr().add(offset).cast::<u16>(),
            ))
        }
    }
    #[inline]
    fn read_u32(&self, offset: usize) -> u32 {
        // SAFETY: caller must ensure the offset is valid for a u32 read inside the
        // record buffer.
        unsafe {
            u32::from_le(std::ptr::read_unaligned(
                self.data.as_ptr().add(offset).cast::<u32>(),
            ))
        }
    }
    #[inline]
    fn read_u64(&self, offset: usize) -> u64 {
        // SAFETY: caller must ensure the offset is valid for a u64 read inside the
        // record buffer.
        unsafe {
            u64::from_le(std::ptr::read_unaligned(
                self.data.as_ptr().add(offset).cast::<u64>(),
            ))
        }
    }

    /// Offset (in bytes from record start) to the Update Sequence Array (USA).
    /// NOTE: This was previously (incorrectly) read from 0x18/0x19 (used size field).
    /// Correct field per NTFS layout is at 0x04.
    #[inline]
    pub fn get_update_sequence_array_offset(&self) -> u16 {
        self.read_u16(Self::OFFSET_FOR_UPDATE_SEQUENCE_ARRAY_OFFSET)
    }

    /// Number of 2-byte elements in the USA, including the first sentinel value.
    #[inline]
    pub fn get_update_sequence_array_size_words(&self) -> u16 {
        self.read_u16(Self::OFFSET_FOR_UPDATE_SEQUENCE_ARRAY_SIZE)
    }

    /// $`LogFile` sequence number (LSN) at offset 0x08 (8 bytes LE).
    #[inline]
    pub fn get_dollar_log_file(&self) -> u64 {
        self.read_u64(Self::OFFSET_FOR_LOGFILE_SEQUENCE_NUMBER)
    }

    #[inline]
    pub fn get_sequence_number(&self) -> u16 {
        self.read_u16(Self::OFFSET_FOR_SEQUENCE)
    }

    #[inline]
    pub fn get_hard_link_count(&self) -> u16 {
        self.read_u16(Self::OFFSET_FOR_HARDLINKS)
    }

    #[inline]
    pub fn get_first_attribute_offset(&self) -> u16 {
        self.read_u16(Self::OFFSET_FOR_FIRST_ATTR)
    }

    #[inline]
    pub fn flags(&self) -> MftRecordFlags {
        MftRecordFlags::from(self.read_u16(Self::OFFSET_FOR_FLAGS))
    }

    #[inline]
    #[must_use]
    pub fn flags_is_in_use(flags: MftRecordFlags) -> bool {
        flags.is_in_use()
    }

    #[inline]
    #[must_use]
    pub fn flags_is_deleted(flags: MftRecordFlags) -> bool {
        flags.is_deleted()
    }

    #[inline]
    #[must_use]
    pub fn is_in_use(&self) -> bool {
        Self::flags_is_in_use(self.flags())
    }

    #[inline]
    #[must_use]
    pub fn is_deleted(&self) -> bool {
        Self::flags_is_deleted(self.flags())
    }

    /// Bytes in use inside this record.
    #[inline]
    pub fn get_used_size(&self) -> u32 {
        self.read_u32(Self::OFFSET_FOR_USED_SIZE)
    }

    /// Allocated size (record size, typically 1024 or 4096).
    #[inline]
    pub fn get_allocated_size(&self) -> u32 {
        self.read_u32(Self::OFFSET_FOR_ALLOC_SIZE)
    }

    /// Base record reference (8 bytes) – if non-zero, this is an extension record.
    #[inline]
    pub fn get_base_reference_raw(&self) -> u64 {
        self.read_u64(Self::OFFSET_FOR_BASE_REF)
    }

    #[inline]
    pub fn get_next_attribute_id(&self) -> u16 {
        self.read_u16(Self::OFFSET_FOR_NEXT_ATTR_ID)
    }

    #[inline]
    pub fn get_record_number(&self) -> MftRecordNumber {
        self.read_u32(Self::OFFSET_FOR_RECORD_NUMBER).into()
    }

    /// Iterate raw attribute slices (header + body) in this record.
    pub fn iter_attributes(&self) -> MftRecordAttributeIter<'_> {
        MftRecordAttributeIter::new(self)
    }
}