From cd02c09ad0d2d651772d01c383b42ba09c0e9e9b Mon Sep 17 00:00:00 2001 From: Jujuforce Date: Sun, 28 Jun 2026 14:03:30 +0200 Subject: [PATCH] fix: skip NTFS DOS (8.3) short-name entries when extracting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a file or directory has a separate 8.3 short name, it appears in the NTFS directory index twice: once under its Win32 long name and once under the Dos short name. The extractor iterated all index entries, so every such item was written a second time under its mangled NAME~1.EXT alias — and short-named directories had their entire subtree re-extracted. Skip index entries in the Dos namespace (the file is still extracted via its Win32 entry). Add unit tests for the skip decision. Co-Authored-By: Claude Opus 4.8 --- src/vhd.rs | 49 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) 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, "..")); + } +}