Merge pull request 'fix: skip NTFS DOS (8.3) short-name entries when extracting' (#7) from jujuforce/fsdecrypt:fix/ntfs-dos-short-name-extraction into trunk

This commit is contained in:
jujuforce
2026-06-28 12:25:40 +00:00
+46 -3
View File
@@ -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<T: Read + Seek>(
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<T: Read + Seek>(
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, ".."));
}
}