fast_ntfs 1.0.2

Forked a low-level NTFS filesystem library
Documentation
mod common;

use fast_ntfs::indexes::NtfsFileNameIndex;
use fast_ntfs::structured_values::{NtfsFileName, NtfsFileNamespace, NtfsStandardInformation};
use fast_ntfs::{
    KnownNtfsFileRecordNumber, Ntfs, NtfsAttributeType, NtfsError, NtfsGuid, NtfsReadSeek,
};
use zerocopy::FromBytes;

const NT_TIMESTAMP_2021_01_01: u64 = 132_539_328_000_000_000;

fn testfs_with_root_record_split_across_mft_runs() -> std::io::Cursor<Vec<u8>> {
    const CLUSTER_SIZE: usize = 512;
    const MFT_LCN: usize = 32;
    const MFT_DATA_RUNS_OFFSET: usize = MFT_LCN * CLUSTER_SIZE + 0x140;
    const ROOT_SECOND_CLUSTER_LCN: usize = MFT_LCN + 11;
    const MOVED_CLUSTER_LCN: usize = 2000;
    const REMAINING_MFT_CLUSTERS: u16 = 1163;

    let mut fs = common::testfs1();
    let data = fs.get_mut();
    let source = ROOT_SECOND_CLUSTER_LCN * CLUSTER_SIZE;
    let destination = MOVED_CLUSTER_LCN * CLUSTER_SIZE;
    let cluster = data[source..source + CLUSTER_SIZE].to_vec();
    data[destination..destination + CLUSTER_SIZE].copy_from_slice(&cluster);
    data[source..source + CLUSTER_SIZE].fill(0);

    let mut data_runs = [0u8; 24];
    data_runs[..9].copy_from_slice(&[
        0x11,
        11,
        MFT_LCN as u8,
        0x22,
        REMAINING_MFT_CLUSTERS.to_le_bytes()[0],
        REMAINING_MFT_CLUSTERS.to_le_bytes()[1],
        0xb0,
        0x07,
        0,
    ]);
    data[MFT_DATA_RUNS_OFFSET..MFT_DATA_RUNS_OFFSET + data_runs.len()].copy_from_slice(&data_runs);

    fs
}

#[test]
fn reads_volume_metadata() {
    let mut fs = common::testfs1();
    let ntfs = Ntfs::new(&mut fs).unwrap();

    assert_eq!(ntfs.cluster_size(), 512);
    assert_eq!(ntfs.sector_size(), 512);
    assert_eq!(ntfs.size(), 2_096_640);

    let volume_info = ntfs.volume_info(&mut fs).unwrap();
    assert_eq!(
        (volume_info.major_version(), volume_info.minor_version()),
        (3, 1)
    );

    let volume_name = ntfs.volume_name(&mut fs).unwrap().unwrap();
    assert_eq!(volume_name.name_length(), 14);
    assert_eq!(volume_name.name(), "mylabel");
}

#[test]
fn cached_mft_runs_resolve_file_records() {
    let mut fs = common::testfs1();
    let ntfs = Ntfs::new(&mut fs).unwrap();
    let uncached_position = ntfs.root_directory(&mut fs).unwrap().position();

    ntfs.read_mft_runs(&mut fs).unwrap();
    // Building the cache is idempotent and subsequent file loads use it.
    ntfs.read_mft_runs(&mut fs).unwrap();
    let cached_root = ntfs.root_directory(&mut fs).unwrap();

    assert_eq!(cached_root.position(), uncached_position);
    assert_eq!(
        cached_root.file_record_number(),
        KnownNtfsFileRecordNumber::RootDirectory as u64
    );
}

#[test]
fn uncached_mft_reads_stitch_file_records_across_data_runs() {
    let mut fs = testfs_with_root_record_split_across_mft_runs();
    let ntfs = Ntfs::new(&mut fs).unwrap();
    let root = ntfs.root_directory(&mut fs).unwrap();

    assert_eq!(
        root.file_record_number(),
        KnownNtfsFileRecordNumber::RootDirectory as u64
    );
    assert!(root.is_directory());
}

#[test]
fn cached_mft_reads_stitch_file_records_across_data_runs() {
    let mut fs = testfs_with_root_record_split_across_mft_runs();
    let ntfs = Ntfs::new(&mut fs).unwrap();
    ntfs.read_mft_runs(&mut fs).unwrap();
    let root = ntfs.root_directory(&mut fs).unwrap();

    assert_eq!(
        root.file_record_number(),
        KnownNtfsFileRecordNumber::RootDirectory as u64
    );
    assert!(root.is_directory());
}

#[test]
fn reuses_file_record_buffer() {
    let mut fs = common::testfs1();
    let ntfs = Ntfs::new(&mut fs).unwrap();
    ntfs.read_mft_runs(&mut fs).unwrap();

    let buffer = Vec::with_capacity(ntfs.file_record_size() as usize);
    let original_pointer = buffer.as_ptr();
    let root = ntfs
        .file_with_buffer(
            &mut fs,
            KnownNtfsFileRecordNumber::RootDirectory as u64,
            buffer,
        )
        .unwrap();
    let buffer = root.into_buffer();
    assert_eq!(buffer.as_ptr(), original_pointer);

    let volume = ntfs
        .file_with_buffer(&mut fs, KnownNtfsFileRecordNumber::Volume as u64, buffer)
        .unwrap();
    let buffer = volume.into_buffer();
    assert_eq!(buffer.as_ptr(), original_pointer);
}

#[test]
fn file_resolver_prepares_caches_and_reuses_its_buffer() {
    let mut fs = common::testfs1();
    let ntfs = Ntfs::new(&mut fs).unwrap();
    let mut resolver = ntfs.file_resolver(&mut fs).unwrap();

    // The resolver only shares `Ntfs`, so direct lookups remain available for its lifetime.
    assert_eq!(
        ntfs.root_directory(&mut fs).unwrap().file_record_number(),
        KnownNtfsFileRecordNumber::RootDirectory as u64
    );

    for file_record_number in [
        KnownNtfsFileRecordNumber::RootDirectory as u64,
        KnownNtfsFileRecordNumber::Volume as u64,
        KnownNtfsFileRecordNumber::RootDirectory as u64,
    ] {
        let resolved_number = resolver
            .with_file(&mut fs, file_record_number, |file, _fs| {
                file.file_record_number()
            })
            .unwrap();
        assert_eq!(resolved_number, file_record_number);
    }
}

#[test]
fn file_resolver_resolves_nested_paths_and_reuses_its_buffer() {
    let mut fs = common::testfs1();
    let ntfs = Ntfs::new(&mut fs).unwrap();
    let mut resolver = ntfs.file_resolver(&mut fs).unwrap();

    // Path lookup initializes the Upcase Table itself and accepts either separator.
    let file = resolver
        .resolve_path(&mut fs, "\\many_subdirs//./256\\")
        .unwrap();
    assert_eq!(
        file.name(&mut fs, None, None).unwrap().unwrap().name(),
        "256"
    );

    let root_record_number = resolver
        .with_path(&mut fs, "/", |root, _fs| root.file_record_number())
        .unwrap();
    assert_eq!(
        root_record_number,
        KnownNtfsFileRecordNumber::RootDirectory as u64
    );

    let nested_record_number = resolver
        .with_path(&mut fs, "many_subdirs/512", |file, _fs| {
            file.file_record_number()
        })
        .unwrap();
    assert_ne!(nested_record_number, root_record_number);
}

#[test]
fn file_resolver_reports_invalid_path_components() {
    let mut fs = common::testfs1();
    let ntfs = Ntfs::new(&mut fs).unwrap();
    let mut resolver = ntfs.file_resolver(&mut fs).unwrap();

    let missing = resolver
        .resolve_path(&mut fs, "many_subdirs/missing")
        .unwrap_err();
    assert!(matches!(missing, NtfsError::PathComponentNotFound { .. }));

    let not_a_directory = resolver
        .resolve_path(&mut fs, "empty-file/child")
        .unwrap_err();
    assert!(matches!(not_a_directory, NtfsError::NotADirectory { .. }));
}

#[test]
fn parses_mft_file_name_and_standard_information() {
    let mut fs = common::testfs1();
    let ntfs = Ntfs::new(&mut fs).unwrap();
    let mft = ntfs
        .file(&mut fs, KnownNtfsFileRecordNumber::MFT as u64)
        .unwrap();
    let mut attributes = mft.attributes_raw();

    let standard_attribute = attributes.next().unwrap().unwrap();
    assert_eq!(
        standard_attribute.ty().unwrap(),
        NtfsAttributeType::StandardInformation
    );
    assert_eq!(standard_attribute.attribute_length(), 96);
    assert_eq!(standard_attribute.value_length(), 72);
    standard_attribute
        .resident_structured_value::<NtfsStandardInformation>()
        .unwrap();

    let file_name_attribute = attributes.next().unwrap().unwrap();
    assert_eq!(
        file_name_attribute.ty().unwrap(),
        NtfsAttributeType::FileName
    );
    assert_eq!(file_name_attribute.attribute_length(), 104);
    assert_eq!(file_name_attribute.value_length(), 74);
    let file_name = file_name_attribute
        .structured_value::<_, NtfsFileName>(&mut fs)
        .unwrap();

    let creation_time = file_name.creation_time();
    assert!(creation_time.nt_timestamp() > NT_TIMESTAMP_2021_01_01);
    assert_eq!(creation_time, file_name.modification_time());
    assert_eq!(creation_time, file_name.mft_record_modification_time());
    assert_eq!(creation_time, file_name.access_time());
    assert_eq!(file_name.allocated_size(), file_name.data_size());
    assert_eq!(file_name.name_length(), 8);
    assert_eq!(file_name.name(), "$MFT");
}

#[test]
fn upcase_table_supports_case_insensitive_lookup_and_empty_data() {
    let mut fs = common::testfs1();
    let ntfs = Ntfs::new(&mut fs).unwrap();
    ntfs.read_upcase_table(&mut fs).unwrap();
    let root = ntfs.root_directory(&mut fs).unwrap();
    let index = root.directory_index(&mut fs).unwrap();
    let mut finder = index.finder();
    let entry = NtfsFileNameIndex::find(&mut finder, &ntfs, &mut fs, "EMPTY-FILE")
        .unwrap()
        .unwrap();
    let file = entry.to_file(&ntfs, &mut fs).unwrap();
    let item = file.data(&mut fs, "").unwrap().unwrap();
    let attribute = item.to_attribute().unwrap();
    let mut value = attribute.value(&mut fs).unwrap();

    assert_eq!(attribute.value_length(), 0);
    assert!(value.is_empty());
    assert_eq!(value.read(&mut fs, &mut [0u8; 5]).unwrap(), 0);
}

#[test]
fn converts_public_value_types() {
    let known_attribute_types = [
        NtfsAttributeType::StandardInformation,
        NtfsAttributeType::AttributeList,
        NtfsAttributeType::FileName,
        NtfsAttributeType::ObjectId,
        NtfsAttributeType::SecurityDescriptor,
        NtfsAttributeType::VolumeName,
        NtfsAttributeType::VolumeInformation,
        NtfsAttributeType::Data,
        NtfsAttributeType::IndexRoot,
        NtfsAttributeType::IndexAllocation,
        NtfsAttributeType::Bitmap,
        NtfsAttributeType::ReparsePoint,
        NtfsAttributeType::EAInformation,
        NtfsAttributeType::EA,
        NtfsAttributeType::PropertySet,
        NtfsAttributeType::LoggedUtilityStream,
        NtfsAttributeType::End,
    ];
    for ty in known_attribute_types {
        assert_eq!(NtfsAttributeType::try_from(ty as u32), Ok(ty));
    }
    assert!(NtfsAttributeType::try_from(0).is_err());
    assert_eq!(NtfsAttributeType::Data.to_string(), "Data");

    for (raw, expected) in [
        (0, NtfsFileNamespace::Posix),
        (1, NtfsFileNamespace::Win32),
        (2, NtfsFileNamespace::Dos),
        (3, NtfsFileNamespace::Win32AndDos),
    ] {
        assert_eq!(NtfsFileNamespace::try_from(raw), Ok(expected));
    }
    assert!(NtfsFileNamespace::try_from(4).is_err());

    let guid = NtfsGuid::read_from_bytes(&[
        0x0b, 0x77, 0xc8, 0x67, 0xf1, 0x44, 0x0a, 0x41, 0xab, 0x9a, 0xf9, 0xb5, 0x44, 0x6f, 0x13,
        0xee,
    ])
    .unwrap();
    assert_eq!(guid.to_string(), "67C8770B-44F1-410A-AB9A-F9B5446F13EE");

    let mut fs = common::testfs1();
    let position = Ntfs::new(&mut fs).unwrap().mft_position();
    assert_eq!((position + 0u64).value(), position.value());
    assert!((position + u64::MAX).value().is_none());
}

#[test]
fn manual_parameters_report_actionable_errors() {
    let mut fs = common::testfs1();
    let (cluster_size, sector_size, size, mft_position, file_record_size, serial_number) = {
        let ntfs = Ntfs::new(&mut fs).unwrap();
        (
            ntfs.cluster_size(),
            ntfs.sector_size(),
            ntfs.size(),
            ntfs.mft_position(),
            ntfs.file_record_size(),
            ntfs.serial_number(),
        )
    };

    assert!(matches!(
        Ntfs::new_manual(
            &mut fs,
            cluster_size,
            sector_size,
            0,
            None,
            file_record_size,
            Some(serial_number),
        ),
        Err(NtfsError::InvalidFilesystemSize { size: 0 })
    ));

    let mft_offset = mft_position.value().unwrap().get();
    assert!(matches!(
        Ntfs::new_manual(
            &mut fs,
            cluster_size,
            sector_size,
            mft_offset,
            Some(mft_position),
            file_record_size,
            Some(serial_number),
        ),
        Err(NtfsError::MftPositionOutOfBounds { .. })
    ));

    let ntfs = Ntfs::new_manual(
        &mut fs,
        cluster_size,
        sector_size,
        size,
        None,
        file_record_size,
        Some(serial_number),
    )
    .unwrap();
    assert!(matches!(
        ntfs.file(&mut fs, 0),
        Err(NtfsError::MissingMftPosition)
    ));

    let ntfs = Ntfs::new_manual(
        &mut fs,
        cluster_size,
        sector_size,
        size,
        Some(mft_position),
        file_record_size,
        Some(serial_number),
    )
    .unwrap();
    assert_eq!(
        ntfs.root_directory(&mut fs).unwrap().file_record_number(),
        KnownNtfsFileRecordNumber::RootDirectory as u64
    );
}