diff --git a/src/vhd.rs b/src/vhd.rs index 554ca2b..5020402 100644 --- a/src/vhd.rs +++ b/src/vhd.rs @@ -7,7 +7,10 @@ use std::{ use anyhow::{anyhow, Result}; use indicatif::{ProgressBar, ProgressStyle}; -use ntfs::{structured_values::NtfsStandardInformation, Ntfs, NtfsAttributeType, NtfsTime}; +use ntfs::{ + structured_values::{NtfsFileNamespace, NtfsStandardInformation}, + Ntfs, NtfsAttributeType, NtfsTime, +}; // --------------------------------------------------------------------------- // Constants @@ -563,6 +566,17 @@ fn is_ntfs_system_entry(name: &str) -> bool { name.starts_with('$') || name == "." || name == ".." || name == "System Volume Information" } +/// Whether a directory-index entry should be skipped when extracting. +/// +/// Besides NTFS system metadata, this skips DOS (8.3) short-name aliases: a file +/// that has a separate short name appears in the index *twice* — once with its +/// Win32 long name and once with the `Dos` short name. Without this, every such +/// entry is extracted a second time under its mangled `NAME~1.EXT` name (and +/// short-named directories get their whole subtree duplicated). +fn skip_index_entry(namespace: NtfsFileNamespace, name: &str) -> bool { + namespace == NtfsFileNamespace::Dos || is_ntfs_system_entry(name) +} + fn ntfs_time_to_system_time(t: NtfsTime) -> SystemTime { let nanos = (t.nt_timestamp() - WINDOWS_EPOCH_OFFSET) * 100; SystemTime::UNIX_EPOCH + Duration::from_nanos(nanos) @@ -602,7 +616,7 @@ fn extract_ntfs_dir( let entry = entry?; let key = entry.key().ok_or_else(|| anyhow!("missing key"))??; let name = key.name().to_string_lossy(); - if is_ntfs_system_entry(&name) { + if skip_index_entry(key.namespace(), &name) { continue; } @@ -649,7 +663,7 @@ fn calculate_ntfs_size( while let Some(entry) = iter.next(fs) { let entry = entry?; let key = entry.key().ok_or_else(|| anyhow!("missing key"))??; - if is_ntfs_system_entry(&key.name().to_string_lossy().as_ref()) { + if skip_index_entry(key.namespace(), key.name().to_string_lossy().as_ref()) { continue; } let file = entry.to_file(ntfs, fs)?; @@ -748,3 +762,32 @@ fn read_be_u32(buf: &[u8], offset: usize) -> u32 { fn read_be_u64(buf: &[u8], offset: usize) -> u64 { u64::from_be_bytes(buf[offset..offset + 8].try_into().unwrap()) } + +#[cfg(test)] +mod tests { + use super::skip_index_entry; + use ntfs::structured_values::NtfsFileNamespace; + + #[test] + fn skips_dos_short_name_aliases() { + // 8.3 aliases duplicate a Win32 entry and must not be extracted again. + assert!(skip_index_entry(NtfsFileNamespace::Dos, "OXGETH~1.EXE")); + assert!(skip_index_entry(NtfsFileNamespace::Dos, "PROGRA~1")); + } + + #[test] + fn keeps_long_and_native_names() { + assert!(!skip_index_entry(NtfsFileNamespace::Win32, "oxGetHwInfo.exe")); + // A name that is its own short name (no separate Dos entry) is kept. + assert!(!skip_index_entry(NtfsFileNamespace::Win32AndDos, "game.bat")); + assert!(!skip_index_entry(NtfsFileNamespace::Posix, "readme")); + } + + #[test] + fn skips_system_entries_regardless_of_namespace() { + assert!(skip_index_entry(NtfsFileNamespace::Win32, "$MFT")); + assert!(skip_index_entry(NtfsFileNamespace::Win32, "System Volume Information")); + assert!(skip_index_entry(NtfsFileNamespace::Win32, ".")); + assert!(skip_index_entry(NtfsFileNamespace::Win32, "..")); + } +}