From 22d5588a63021599e6cb5c03e26d889f345d5a81 Mon Sep 17 00:00:00 2001 From: Mathjk Date: Wed, 30 Sep 2026 00:55:50 +0800 Subject: [PATCH] fix: don't panic on non-UTF-8 filenames when matching ignore patterns On Windows, NTFS filenames may contain unpaired surrogates that cannot be represented in UTF-8. `IgnorePatterns::is_excluded` converted each entry's relative path with the panicking `gix::path::into_bstr`, so a single such name anywhere in a scanned tree aborted the whole process when `--ignore-from` was used - in list, aggregate and stacks modes alike, as well as in `clean`'s entry filtering. Match on the OS string's encoded bytes instead - `OsStr::as_encoded_bytes` yields a self-synchronizing UTF-8 superset (WTF-8 on Windows, the raw bytes on Unix) which represents lone surrogates losslessly. The glob matcher then compares bytes, so wildcard patterns that don't spell out the invalid code unit, like `*` or `*.txt`, still match such names instead of silently letting them through. Fixes https://github.com/Byron/dua-cli/issues/408 --- src/common.rs | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/common.rs b/src/common.rs index c5655c60..6a0db752 100644 --- a/src/common.rs +++ b/src/common.rs @@ -213,8 +213,13 @@ impl IgnorePatterns { if relative_path.as_os_str().is_empty() { return false; } - let relative_path = - gix::path::to_unix_separators_on_windows(gix::path::into_bstr(relative_path)); + // Match on the OS string's encoded bytes (WTF-8 on Windows, the raw bytes on + // Unix), which can represent lone surrogates without panicking. That way, + // wildcard or suffix patterns that don't spell out an invalid code unit still + // match filenames that are not valid UTF-8, which NTFS permits on Windows. + let relative_path = gix::path::to_unix_separators_on_windows(bstr::BStr::new( + relative_path.as_os_str().as_encoded_bytes(), + )); self.search .pattern_matching_relative_path( relative_path.as_ref(), @@ -589,6 +594,29 @@ mod tests { } } + /// NTFS allows filenames that are not valid UTF-16, e.g. with unpaired surrogates. + /// Matching happens on the name's encoded bytes, so patterns that don't spell out + /// the invalid code unit still match, and none of this can panic. + #[cfg(windows)] + #[test] + fn ignore_patterns_tolerate_non_utf8_names() { + use std::os::windows::ffi::OsStringExt as _; + + let mut name: Vec = "lone".encode_utf16().collect(); + name.push(0xD800); + name.extend(".txt".encode_utf16()); + let name = std::ffi::OsString::from_wide(&name); + + assert!( + patterns_from("*.txt\n").is_excluded(Path::new(&name), false), + "a wildcard pattern matches the encoded bytes of a name that is not valid UTF-8" + ); + assert!( + !patterns_from("*.zzz\n").is_excluded(Path::new(&name), false), + "a pattern that doesn't match still doesn't exclude it" + ); + } + #[test] fn later_ignore_files_win_over_earlier_ones() { let dir = tempfile::tempdir().unwrap();