fast_ntfs 1.0.1

Forked a low-level NTFS filesystem library
Documentation
// Copyright 2021-2026 Colin Finck <colin@reactos.org>
// SPDX-License-Identifier: MIT OR Apache-2.0

use core::ops::Range;

use alloc::vec::Vec;
use core::mem;
use zerocopy::byteorder::LittleEndian;
use zerocopy::{FromBytes, I64, Immutable, KnownLayout, U32, Unaligned};

use crate::attribute_value::NtfsAttributeValue;
use crate::error::{NtfsError, Result};
use crate::helpers::pod_from_prefix;
use crate::index_entry::{IndexNodeEntryRanges, NtfsIndexNodeEntries};
use crate::indexes::NtfsIndexEntryType;
use crate::io::{Read, Seek};
use crate::record::Record;
use crate::record::RecordHeader;
use crate::traits::NtfsReadSeek;
use crate::types::{NtfsPosition, Vcn};

/// Size of all [`IndexRecordHeader`] fields.
const INDEX_RECORD_HEADER_SIZE: u32 = mem::size_of::<IndexRecordHeader>() as u32;

#[derive(Clone, Copy, Debug, FromBytes, Immutable, KnownLayout, Unaligned)]
#[repr(C, packed)]
struct IndexRecordHeader {
    record_header: RecordHeader,
    vcn: I64<LittleEndian>,
}

/// Size of all [`IndexNodeHeader`] fields plus some reserved bytes.
pub(crate) const INDEX_NODE_HEADER_SIZE: usize = mem::size_of::<IndexNodeHeader>();

#[derive(Clone, Copy, Debug, FromBytes, Immutable, KnownLayout, Unaligned)]
#[repr(C, packed)]
pub(crate) struct IndexNodeHeader {
    pub(crate) entries_offset: U32<LittleEndian>,
    pub(crate) index_size: U32<LittleEndian>,
    pub(crate) allocated_size: U32<LittleEndian>,
    pub(crate) flags: u8,
    reserved: [u8; 3],
}

/// A single NTFS Index Record.
///
/// These records are denoted via an `INDX` signature on the filesystem.
///
/// NTFS uses B-tree indexes to quickly look up files, Object IDs, Reparse Points, Security Descriptors, etc.
/// An Index Record is further comprised of Index Entries, which contain the actual key/data (see [`NtfsIndexEntry`],
/// iterated via [`NtfsIndexNodeEntries`]).
///
/// [`NtfsIndexEntry`]: crate::NtfsIndexEntry
///
/// Reference: <https://flatcap.github.io/linux-ntfs/ntfs/concepts/index_record.html>
#[derive(Debug)]
pub struct NtfsIndexRecord {
    record: Record,
    header: IndexRecordHeader,
    node_header: IndexNodeHeader,
}

const HAS_SUBNODES_FLAG: u8 = 0x01;

impl NtfsIndexRecord {
    pub(crate) fn new<T>(
        fs: &mut T,
        value: &mut NtfsAttributeValue,
        index_record_size: u32,
    ) -> Result<Self>
    where
        T: Read + Seek,
    {
        Self::new_with_buffer(fs, value, index_record_size, Vec::new())
    }

    pub(crate) fn new_with_buffer<T>(
        fs: &mut T,
        value: &mut NtfsAttributeValue,
        index_record_size: u32,
        mut data: Vec<u8>,
    ) -> Result<Self>
    where
        T: Read + Seek,
    {
        let data_position = value.data_position();

        data.resize(index_record_size as usize, 0);
        value.read_exact(fs, &mut data)?;

        Self::from_data(data, data_position)
    }

    pub(crate) fn from_data(data: Vec<u8>, data_position: NtfsPosition) -> Result<Self> {
        let mut record = Record::new(data, data_position)?;
        Self::validate_signature(&record)?;
        record.fixup()?;
        let header = pod_from_prefix::<IndexRecordHeader, { mem::size_of::<IndexRecordHeader>() }>(
            record.data(),
        )
        .ok_or(NtfsError::BufferTooSmall {
            expected: mem::size_of::<IndexRecordHeader>(),
            actual: record.data().len(),
        })?;
        let node_header_start = INDEX_RECORD_HEADER_SIZE as usize;
        let node_header = pod_from_prefix::<IndexNodeHeader, INDEX_NODE_HEADER_SIZE>(
            &record.data()[node_header_start..],
        )
        .ok_or(NtfsError::BufferTooSmall {
            expected: node_header_start + INDEX_NODE_HEADER_SIZE,
            actual: record.data().len(),
        })?;

        let index_record = Self {
            record,
            header,
            node_header,
        };
        index_record.validate_sizes()?;

        Ok(index_record)
    }

    /// Returns an iterator over all entries of this Index Record (cf. [`NtfsIndexEntry`]).
    ///
    /// [`NtfsIndexEntry`]: crate::NtfsIndexEntry
    pub fn entries<'s, E>(&'s self) -> Result<NtfsIndexNodeEntries<'s, E>>
    where
        E: NtfsIndexEntryType,
    {
        let (entries_range, position) = self.entries_range_and_position();
        let data = &self.record.data()[entries_range];

        Ok(NtfsIndexNodeEntries::new(data, position))
    }

    fn entries_range_and_position(&self) -> (Range<usize>, NtfsPosition) {
        let start = INDEX_RECORD_HEADER_SIZE as usize + self.index_entries_offset() as usize;
        let end = INDEX_RECORD_HEADER_SIZE as usize + self.index_data_size() as usize;
        let position = self.record.position() + start;

        (start..end, position)
    }

    /// Returns whether this index node has sub-nodes.
    /// Otherwise, this index node is a leaf node.
    pub fn has_subnodes(&self) -> bool {
        (self.node_header.flags & HAS_SUBNODES_FLAG) != 0
    }

    /// Returns the allocated size of this NTFS Index Record, in bytes.
    pub fn index_allocated_size(&self) -> u32 {
        self.node_header.allocated_size.get()
    }

    /// Returns the size actually used by index data within this NTFS Index Record, in bytes.
    pub fn index_data_size(&self) -> u32 {
        self.node_header.index_size.get()
    }

    pub(crate) fn index_entries_offset(&self) -> u32 {
        self.node_header.entries_offset.get()
    }

    pub(crate) fn into_entry_ranges<E>(self) -> IndexNodeEntryRanges<E>
    where
        E: NtfsIndexEntryType,
    {
        let (entries_range, position) = self.entries_range_and_position();
        IndexNodeEntryRanges::new(self.record.into_data(), entries_range, position)
    }

    fn validate_signature(record: &Record) -> Result<()> {
        let signature = &record.signature();
        let expected = b"INDX";

        if signature == expected {
            Ok(())
        } else {
            Err(NtfsError::InvalidIndexSignature {
                position: record.position(),
                expected,
                actual: *signature,
            })
        }
    }

    fn validate_sizes(&self) -> Result<()> {
        let index_record_size = self.record.len();

        // The total size allocated for this Index Record must not be larger than
        // the size defined for all index records of this index.
        let total_allocated_size = INDEX_RECORD_HEADER_SIZE + self.index_allocated_size();
        if total_allocated_size > index_record_size {
            return Err(NtfsError::InvalidIndexAllocatedSize {
                position: self.record.position(),
                expected: index_record_size,
                actual: total_allocated_size,
            });
        }

        // Furthermore, the total used size for this Index Record must not be
        // larger than the total allocated size.
        let total_data_size = INDEX_RECORD_HEADER_SIZE + self.index_data_size();
        if total_data_size > total_allocated_size {
            return Err(NtfsError::InvalidIndexUsedSize {
                position: self.record.position(),
                expected: total_allocated_size,
                actual: total_data_size,
            });
        }

        Ok(())
    }

    /// Returns the Virtual Cluster Number (VCN) of this Index Record, as reported by the header of this Index Record.
    ///
    /// This can be used to double-check that an Index Record is the actually requested one.
    /// [`NtfsIndexAllocation::record_from_vcn`] uses it for that purpose.
    ///
    /// [`NtfsIndexAllocation::record_from_vcn`]: crate::structured_values::NtfsIndexAllocation::record_from_vcn
    pub fn vcn(&self) -> Vcn {
        Vcn::from(self.header.vcn.get())
    }
}