ntfs Rust library

repository·master·Indexed 20 days ago

https://github.com/colinfinck/ntfs

A low-level, read-only, no_std-compatible Rust library for reading and exploring NTFS 3.x filesystems (Windows 2000 through Windows 11). It provides a platform-independent way to access filesystem structures, attributes, and data without using unsafe code. Key features include a flattened view of NTFS attributes, support for Alternate Data Streams (ADS), efficient directory operations using the $Upcase Table, and a demonstration tool called ntfs-shell.

Tokens
9.2K
Snippets
24
Records
43
Agent score
67%

What's inside ntfs

  1. Overview of the ntfs Rust crate

    master

    The ntfs crate is a low-level NTFS filesystem library implemented in Rust. It is designed for NTFS 3.x versions (compatible with Windows 2000 through Windows 11).

    Key characteristics:

    • no_std compatible: Can be used in firmware-level code or user-mode applications (requires alloc for full functionality).
    • Safety: Contains no unsafe code and uses checked arithmetic.
    • Platform Independent: Works across different platforms and endianness.
    • Read-Only: The library is designed for reading filesystem structures, attributes, and data.
  2. Core Library Features

    master

    The ntfs library provides several high-level and low-level capabilities:

    • Filesystem Abstraction: Convenience functions allow treating NTFS like a standard filesystem using Read and Seek traits.
    • Attribute Access: Read arbitrary resident and non-resident attributes, including those in Attribute Lists and sparse attribute data. This enables reading file data and Alternate Data Streams (ADS) of any size.
    • Flattened View: Provides a "data-centric" view of NTFS Attributes, abstracting away nested Attribute Lists.
    • Efficient Directory Operations:
      • In-order iteration of directory contents at $O(1)$.
      • Efficient file finding using the filesystem's $Upcase Table for case-insensitive searches.
    • Type Safety: Uses Rust's type system to handle various NTFS index types safely.
    • Error Handling: Uses a custom NtfsError type that implements Display and provides location-specific details where applicable.
  3. Build and run the ntfs-shell example

    master

    The ntfs-shell is a demonstration tool included with the crate that allows you to explore the internal structures of an NTFS filesystem at any detail level. It opens the filesystem in read-only mode, making it safe to use on mounted partitions.

    Building

    To build the shell with all features enabled, run:

    cargo build --example ntfs-shell --all-features

    Running

    Pass the path to an NTFS image (works on all OSs) or a partition (Windows only) to the binary.

    • On Windows: You can pass a partition path like \\.\C:, but you must run with administrative privileges.
    • On all OSs: You can pass a path to an NTFS disk image.

    Using the Shell

    • Use help to see all supported commands.
    • Use help COMMAND to see the syntax for a specific command.
    • File Referencing: Most commands accepting a filename also accept an NTFS File Record Number. Prepend the number with / or 0x for hexadecimal.

    Examples of file referencing:

    fileinfo Windows
    fileinfo /146810
    fileinfo /0x23d7a
    cargo build --example ntfs-shell --all-features
  4. Manage NTFS indexes with NtfsIndex

    master

    The NtfsIndex struct is a helper for iterating over or finding specific entries within an NTFS index (such as a directory's file name index).

    To avoid manual attribute lookup, use NtfsFile::directory_index to obtain an NtfsIndex object directly for a directory.

    Key capabilities:

    • In-order traversal: Use .entries() to get an iterator that traverses all entries sorted by their index key.
    • Efficient searching: Use .finder() to get an NtfsIndexFinder for locating specific entries using a comparison function.
    // Example of obtaining an index from a directory file
    let root_dir_index = root_dir.directory_index(&mut testfs).unwrap();
    
    // 1. Iterate through entries
    let mut iter = root_dir_index.entries();
    while let Some(Ok(entry)) = iter.next(&mut testfs) {
        // process entry
    }
    
    // 2. Find a specific entry
    let mut finder = root_dir_index.finder();
    let entry = NtfsFileNameIndex::find(&mut finder, &ntfs, &mut testfs, "target_name").unwrap().unwrap();
  5. Use NtfsIndexEntry to access index data

    master

    An NtfsIndexEntry represents a single entry within an NTFS B-tree index (such as a directory). The type of data it contains is determined by the type parameter E, which must implement NtfsIndexEntryType.

    Commonly, E will be NtfsFileNameIndex for directory indexes. You can use NtfsFile::directory_index to obtain an NtfsIndex object for a directory easily.

    An index entry can contain either structured data or a file reference, but not both. Use data() to retrieve the entry's data or file_reference() to retrieve the referenced file's reference.

  6. Understand NtfsDataRun and sparse files

    master

    NtfsDataRun represents a continuous cluster range within a non-resident attribute.

    Key characteristics:

    • Allocated Size: A data run knows its allocated size (in bytes), but not necessarily the exact amount of 'used' data. When reading, you should respect the total attribute length to avoid reading uninitialized/allocated-but-unused data.
    • Sparse Data Runs: Some data runs may be 'sparse'. For these runs, data_position() returns None. When reading from a sparse run, the reader will return zeroed bytes instead of reading from the filesystem.
    • Data Position: data_position() returns the absolute position within the filesystem where the run's data starts.
  7. Iterate over NTFS file attributes

    master

    An NtfsFile can be queried for its attributes using two different approaches depending on whether you need a flattened view or a raw view:

    1. Flattened View (attributes()): Returns an iterator over NtfsAttributeItems. This is the recommended way to access data as it automatically traverses $ATTRIBUTE_LIST attributes and handles connected attributes, providing a single unified view of all data.
    2. Raw View (attributes_raw()): Returns an iterator over top-level NtfsAttributes. This does not traverse Attribute Lists; it only returns the attributes physically present in the primary File Record. Use this if you need to inspect the raw filesystem structure.
  8. Understand $INDEX_ALLOCATION and B-tree sub-nodes

    master

    In NTFS, the $INDEX_ALLOCATION attribute describes the sub-nodes of a B-tree. While top-level nodes are managed via NtfsIndexRoot, $INDEX_ALLOCATION handles the non-resident parts of the tree used for directories (indexing NtfsFileNames), Object IDs, Reparse Points, and Security Descriptors.

    An NtfsIndexAllocation can be resident or non-resident, but the NtfsStructuredValue implementation for this type specifically expects non-resident attributes. If an attempt is made to use a resident attribute, it will return NtfsError::UnexpectedResidentAttribute.

  9. Understand the $FILE_NAME attribute and its limitations

    master

    The $FILE_NAME attribute in NTFS is used for every hard link and contains the file name and directory information.

    Important Usage Note: The $FILE_NAME attribute duplicates several fields found in $STANDARD_INFORMATION (such as access_time, creation_time, modification_time, allocated_size, data_size, and file_attributes). However, NTFS only updates these fields when the file name changes.

    To ensure you are reading up-to-date metadata, you should use the corresponding fields from NtfsStandardInformation instead of NtfsFileName whenever possible. Use NtfsFileName specifically when you need the actual file name or the namespace information.

  10. Iterate over all Index Records in an $INDEX_ALLOCATION

    master

    To traverse all records within an $INDEX_ALLOCATION attribute, you can use the records method to create an iterator. There are two ways to consume these records depending on how you want to manage the filesystem reader (fs) borrow:

    1. Manual Iteration (NtfsIndexRecords)

    Use records() to get a NtfsIndexRecords object. This requires you to manually pass a mutable reference to the filesystem (&mut T) to the next() method on each iteration.

    2. Standard Iterator (NtfsIndexRecordsAttached)

    Use records().attach(fs) to get a NtfsIndexRecordsAttached object. This implements the standard Rust Iterator and FusedIterator traits by mutably borrowing the filesystem for the duration of the iteration. This allows you to use standard iterator methods like for loops or collect().

    // Option 1: Manual next()
    let mut iter = index_allocation.records(index_record_size);
    while let Some(result) = iter.next(fs) { 
        let record = result?; 
        // ...
    }
    
    // Option 2: Standard Iterator (Recommended)
    for result in index_allocation.records(index_record_size).attach(fs) {
        let record = result?;
        // ...
    }
  11. How to use the ntfs crate

    master

    To interact with an NTFS filesystem using this crate, follow these steps:

    1. Initialize an Ntfs structure by calling Ntfs::new(reader), where reader is a type implementing Read and Seek.
    2. Access the root directory using ntfs.root_directory(reader) to obtain an NtfsFile.
    3. Use NtfsFile methods to explore the filesystem:
      • attributes(reader): Retrieve file attributes.
      • attributes_raw(reader): Retrieve raw file attributes.
      • directory_index(reader): Access the directory index for a directory.
      • info(reader): Get file information.
      • name(): Get the file name.

    The crate is no_std-compatible, making it suitable for environments ranging from firmware to user-mode applications.

    # use ntfs::Ntfs;
    # let mut fs = std::io::Cursor::new(vec![]);
    // 1. Create an Ntfs structure from a reader
    let mut ntfs = Ntfs::new(&mut fs).unwrap();
    
    // 2. Retrieve the root directory
    let root_dir = ntfs.root_directory(&mut fs).unwrap();
    
    // 3. Dig into attributes or use convenience functions
    let index = root_dir.directory_index(&mut fs).unwrap();
    let mut iter = index.entries();
    
    while let Some(entry) = iter.next(&mut fs) {
        let entry = entry.unwrap();
        let file_name = entry.key().unwrap().unwrap();
        println!("{}", file_name.name());
    }
  12. Example: Iterate through root directory entries

    master

    This example demonstrates how to dump the names of all files and folders in the root directory of an NTFS filesystem. The list is retrieved directly from the NTFS index, meaning it is sorted according to NTFS's case-insensitive string comparison rules.

    let mut ntfs = Ntfs::new(&mut fs).unwrap();
    let root_dir = ntfs.root_directory(&mut fs).unwrap();
    let index = root_dir.directory_index(&mut fs).unwrap();
    let mut iter = index.entries();
    
    while let Some(entry) = iter.next(&mut fs) {
        let entry = entry.unwrap();
        let file_name = entry.key().unwrap().unwrap();
        println!("{}", file_name.name());
    }