ntfs-reader 0.4.5

Read MFT and USN journal
Documentation
#![cfg(target_os = "windows")]

use std::collections::HashSet;
use std::fs::{self, File};
use std::io::Write;
use std::os::windows::io::AsRawHandle;
use std::path::PathBuf;

use ntfs_reader::api::NtfsAttributeType;
use ntfs_reader::attribute::DataRun;
use ntfs_reader::errors::NtfsReaderResult;
use ntfs_reader::file_info::FileInfo;
use ntfs_reader::mft::Mft;
use ntfs_reader::test_utils::{test_volume_letter, TempDirGuard};
use ntfs_reader::volume::Volume;
use windows::Win32::Foundation::{
    ERROR_ACCESS_DENIED, ERROR_INVALID_FUNCTION, ERROR_NOT_SUPPORTED, HANDLE,
};
use windows::Win32::System::Ioctl::FSCTL_SET_SPARSE;
use windows::Win32::System::IO::DeviceIoControl;

#[test]
fn mft_creation() -> NtfsReaderResult<()> {
    let vol = Volume::new(format!("\\\\.\\{}:", test_volume_letter()))?;
    let mft = Mft::new(vol)?;

    assert!(mft.max_record > 0);
    assert!(!mft.data.is_empty());
    assert!(!mft.bitmap.is_empty());

    Ok(())
}

#[test]
fn record_exists_boundaries() -> NtfsReaderResult<()> {
    let vol = Volume::new(format!("\\\\.\\{}:", test_volume_letter()))?;
    let mft = Mft::new(vol)?;

    assert!(mft.record_exists(0));
    assert!(!mft.record_exists(u64::MAX));
    assert!(!mft.record_exists(mft.max_record + 1));

    Ok(())
}

#[test]
fn get_record_bounds() -> NtfsReaderResult<()> {
    let vol = Volume::new(format!("\\\\.\\{}:", test_volume_letter()))?;
    let mft = Mft::new(vol)?;

    assert!(mft.get_record(0).is_some());
    assert!(mft.get_record(mft.max_record + 1).is_none());

    Ok(())
}

#[test]
fn iterate_files_discovers_temp_artifacts() -> NtfsReaderResult<()> {
    let dir_name = format!("mft-iterate-files-{}", std::process::id());
    let dir = PathBuf::from(format!("\\\\?\\{}:\\{}", test_volume_letter(), dir_name));

    let _cleanup = TempDirGuard::new(&dir)?;

    let file_names = ["iter-a.txt", "iter-b.txt", "iter-c.txt"];
    let mut expected: HashSet<String> = HashSet::new();
    for name in &file_names {
        let path = dir.join(name);
        let mut f = File::create(&path)?;
        f.write_all(b"hello")?;
        expected.insert(format!("{}\\{}", dir_name, name));
    }

    let mut cur = dir.clone();
    let mut rel_parts = vec![dir_name.clone()];
    for i in 1..=10u32 {
        let comp = format!("deep_{i:02}");
        cur.push(&comp);
        rel_parts.push(comp);
    }
    fs::create_dir_all(&cur)?;
    let deep_file = cur.join("deep.txt");
    File::create(&deep_file)?.write_all(b"deep")?;
    rel_parts.push("deep.txt".to_string());
    expected.insert(rel_parts.join("\\"));

    let long_stem = "L".repeat(200);
    let long_name = format!("{}.txt", long_stem);
    let long_path = dir.join(&long_name);
    File::create(&long_path)?.write_all(b"long")?;
    expected.insert(format!("{}\\{}", dir_name, long_name));

    let big_path = dir.join("big-1G.bin");
    let big = File::create(&big_path)?;
    big.set_len(1_000_000_000)?;
    expected.insert(format!("{}\\{}", dir_name, "big-1G.bin"));

    let sparse_path = dir.join("sparse.bin");
    let sparse_file = File::create(&sparse_path)?;
    mark_sparse(&sparse_file)?;
    sparse_file.set_len(2_000_000_000)?;
    expected.insert(format!("{}\\{}", dir_name, "sparse.bin"));
    let sparse_key = format!("{}\\{}", dir_name, "sparse.bin");

    let vol = Volume::new(format!("\\\\.\\{}:", test_volume_letter()))?;
    let mft = Mft::new(vol)?;

    let to_rel_key = |p: &std::path::Path| -> Option<String> {
        let mut segs = Vec::new();
        let mut seen_root = false;
        for comp in p.components() {
            if let std::path::Component::Normal(os) = comp {
                if !seen_root {
                    if os.to_string_lossy().eq_ignore_ascii_case(&dir_name) {
                        segs.push(dir_name.clone());
                        seen_root = true;
                    }
                } else {
                    segs.push(os.to_string_lossy().to_string());
                }
            }
        }
        if seen_root {
            Some(segs.join("\\"))
        } else {
            None
        }
    };

    let mut found = HashSet::new();
    let mut sparse_checked = false;
    for file in mft.files() {
        if !file.is_directory() {
            let info = FileInfo::new(&mft, &file);
            if let Some(key) = to_rel_key(&info.path) {
                if key == sparse_key {
                    let data_att = file
                        .get_attribute(NtfsAttributeType::Data)
                        .expect("sparse file missing data attribute");
                    assert!(
                        data_att.header.is_non_resident != 0,
                        "sparse file data attribute should be non-resident"
                    );
                    let (total_size, runs) = data_att
                        .get_nonresident_data_runs(&mft.volume)
                        .expect("failed to parse sparse data runs");
                    assert_eq!(total_size, 2_000_000_000);
                    assert!(
                        matches!(runs.as_slice(), [DataRun::Sparse { length }] if *length >= total_size),
                        "expected a sparse run covering the file, got {:?}",
                        runs
                    );
                    sparse_checked = true;
                }
                found.insert(key);
            }
        }
    }

    for key in &expected {
        assert!(
            found.contains(key),
            "Did not find created path '{}' via iterate_files (found: {:?})",
            key,
            found
        );
    }

    assert!(
        sparse_checked,
        "Sparse file nonresident runs were not validated"
    );

    Ok(())
}

fn mark_sparse(file: &File) -> std::io::Result<()> {
    let handle = HANDLE(file.as_raw_handle());
    let mut bytes_returned = 0u32;
    unsafe {
        DeviceIoControl(
            handle,
            FSCTL_SET_SPARSE,
            None,
            0,
            None,
            0,
            Some(&mut bytes_returned as *mut u32),
            None,
        )
        .map_err(|err| device_io_error(err, "FSCTL_SET_SPARSE"))?;
    }
    Ok(())
}

fn device_io_error(err: windows::core::Error, op: &str) -> std::io::Error {
    let hresult = err.code();
    let kind = if hresult == ERROR_ACCESS_DENIED.to_hresult() {
        std::io::ErrorKind::PermissionDenied
    } else if hresult == ERROR_INVALID_FUNCTION.to_hresult()
        || hresult == ERROR_NOT_SUPPORTED.to_hresult()
    {
        std::io::ErrorKind::Unsupported
    } else {
        std::io::ErrorKind::Other
    };

    std::io::Error::new(kind, format!("{op} failed: {err}"))
}