ntfs-reader 0.4.6

Read MFT and USN journal
Documentation
// Copyright (c) 2022, Matteo Bernacchia <dev@kikijiki.com>. All rights reserved.
// This project is dual licensed under the Apache License 2.0 and the MIT license.
// See the LICENSE files in the project root for details.

use std::{
    collections::HashMap,
    path::{Path, PathBuf},
};

use time::OffsetDateTime;

use crate::{
    api::{ntfs_to_unix_time, NtfsAttributeType, ROOT_RECORD},
    file::NtfsFile,
    mft::Mft,
};

const MAX_PATH_DEPTH: usize = 1024;

pub trait FileInfoCache<'a> {
    fn get(&self, number: u64) -> Option<&Path>;
    fn insert(&mut self, number: u64, path: PathBuf);
}

#[derive(Default)]
pub struct HashMapCache(pub HashMap<u64, PathBuf>);
impl FileInfoCache<'_> for HashMapCache {
    fn get(&self, number: u64) -> Option<&Path> {
        if let Some(p) = self.0.get(&number) {
            Some(p)
        } else {
            None
        }
    }

    fn insert(&mut self, number: u64, path: PathBuf) {
        self.0.insert(number, path);
    }
}

#[derive(Default)]
pub struct VecCache(pub Vec<Option<PathBuf>>);
impl FileInfoCache<'_> for VecCache {
    fn get(&self, number: u64) -> Option<&Path> {
        if (self.0.len() as u64) > number {
            if let Some(p) = &self.0[number as usize] {
                return Some(p);
            }
        }
        None
    }

    fn insert(&mut self, number: u64, path: PathBuf) {
        if (self.0.len() as u64) <= number {
            self.0.resize(number as usize + 1, None);
        }
        self.0[number as usize] = Some(path);
    }
}

pub struct FileInfo {
    pub name: String,
    pub path: PathBuf,
    pub is_directory: bool,
    pub size: u64,
    /// Raw Windows `FILE_ATTRIBUTE_*` flags from `$STANDARD_INFORMATION`.
    pub file_attributes: u32,
    pub created: Option<OffsetDateTime>,
    pub accessed: Option<OffsetDateTime>,
    pub modified: Option<OffsetDateTime>,
}

impl FileInfo {
    pub fn new(mft: &Mft, file: &NtfsFile) -> Self {
        let mut info = Self::_new(mft, file);
        info._compute_path(mft, file);
        info
    }

    pub fn with_cache<C: for<'a> FileInfoCache<'a>>(
        mft: &Mft,
        file: &NtfsFile,
        cache: &mut C,
    ) -> Self {
        let mut info = Self::_new(mft, file);
        info._compute_path_with_cache(mft, file, cache);
        info
    }

    fn _new(mft: &Mft, file: &NtfsFile) -> Self {
        let mut accessed = None;
        let mut created = None;
        let mut modified = None;
        let mut size = 0u64;
        let mut file_attributes = 0u32;

        for record in mft.file_records(file) {
            record.attributes(|att| {
                if att.header.type_id == NtfsAttributeType::StandardInformation as u32 {
                    if let Some(stdinfo) = att.as_standard_info() {
                        accessed = Some(ntfs_to_unix_time(stdinfo.access_time));
                        created = Some(ntfs_to_unix_time(stdinfo.creation_time));
                        modified = Some(ntfs_to_unix_time(stdinfo.modification_time));
                        file_attributes = stdinfo.file_attributes;
                    }
                }

                // Ignore named alternate data streams. For a split non-resident
                // stream, only the first extent (lowest VCN 0) carries the file size.
                if att.header.type_id == NtfsAttributeType::Data as u32
                    && att.header.name_length == 0
                {
                    if att.header.is_non_resident == 0 {
                        if let Some(header) = att.resident_header() {
                            size = header.value_length as u64;
                        }
                    } else if let Some(header) = att.nonresident_header() {
                        if header.lowest_vcn == 0 {
                            size = header.data_size;
                        }
                    }
                }
            });
        }

        let base = file
            .base_record_number()
            .and_then(|number| mft.get_record(number));
        let logical_file = base.as_ref().unwrap_or(file);

        FileInfo {
            name: String::new(),
            path: PathBuf::new(),
            is_directory: logical_file.is_directory(),
            size,
            file_attributes,
            created,
            accessed,
            modified,
        }
    }

    fn _compute_path(&mut self, mft: &Mft, file: &NtfsFile) {
        let mut next_parent;

        if let Some(name) = file.get_best_file_name(mft) {
            self.name = name.to_string();
            next_parent = name.parent();
        } else {
            tracing::debug!("No name for file {}", file.number);
            return;
        }

        let mut components = Vec::new();
        for _ in 0..MAX_PATH_DEPTH {
            if next_parent == ROOT_RECORD {
                break;
            }

            let cur_file = match mft.get_record(next_parent) {
                Some(f) => f,
                None => return,
            };

            if let Some(cur_name_att) = cur_file.get_best_file_name(mft) {
                let cur_name = cur_name_att.to_string();
                components.push((cur_file.number(), PathBuf::from(cur_name)));
                next_parent = cur_name_att.parent();
            } else {
                return;
            }
        }

        let mut path = mft.volume.path.clone();
        for (_, comp) in components.iter().rev() {
            path.push(comp);
        }
        path.push(&self.name);

        self.path = path;
    }

    fn _compute_path_with_cache<C: for<'a> FileInfoCache<'a>>(
        &mut self,
        mft: &Mft,
        file: &NtfsFile,
        cache: &mut C,
    ) {
        let mut next_parent;

        if let Some(name) = file.get_best_file_name(mft) {
            self.name = name.to_string();
            next_parent = name.parent();
        } else {
            return;
        }

        let mut components = Vec::new();
        let mut cached_path = None;
        for _ in 0..MAX_PATH_DEPTH {
            if next_parent == ROOT_RECORD {
                break;
            }

            // Cache hit?
            if let Some(cur_path) = cache.get(next_parent) {
                cached_path = Some(cur_path);
                break;
            }

            let cur_file = match mft.get_record(next_parent) {
                Some(f) => f,
                None => return,
            };

            if let Some(cur_name_att) = cur_file.get_best_file_name(mft) {
                let cur_name = cur_name_att.to_string();
                components.push((cur_file.number(), PathBuf::from(cur_name)));
                next_parent = cur_name_att.parent();
            } else {
                return;
            }
        }

        let mut path = PathBuf::from(cached_path.unwrap_or(&mft.volume.path));

        for (number, comp) in components.iter().rev() {
            path.push(comp);
            cache.insert(*number, path.clone());
        }

        path.push(&self.name);
        cache.insert(file.number, path.clone());

        self.path = path;
    }
}