apply strict filter

This commit is contained in:
curious-rabbit
2026-05-02 20:52:29 +02:00
parent 825f308931
commit aeecb5d8c0
3 changed files with 52 additions and 11 deletions
+3 -3
View File
@@ -55,14 +55,14 @@ impl DirEntry {
}
/// Returns the path as it should be presented to the user.
/// When stripping `./` would leave the path starting with `-`, keep the `./` so
/// downstream tools don't interpret the filename as an option.
/// When stripping `./` would leave the path starting with `-`, keep the original
/// (with `./`) so downstream tools don't interpret it as an option.
pub fn stripped_path(&self, config: &Config) -> Cow<'_, Path> {
let path = self.path();
if config.strip_cwd_prefix {
let stripped = strip_current_dir(path);
if starts_with_dash(stripped) {
Cow::Owned(Path::new(".").join(stripped))
Cow::Borrowed(path)
} else {
Cow::Borrowed(stripped)
}
+2
View File
@@ -242,6 +242,8 @@ impl CommandTemplate {
bail!("No executable provided for --exec or --exec-batch");
}
// Reject placeholder-as-executable for both --exec and --exec-batch
// (was previously checked only for --exec-batch).
if args[0].has_tokens() {
bail!(
"First argument of --exec/--exec-batch must be a fixed executable, not a placeholder"
+47 -8
View File
@@ -3,22 +3,40 @@
use std::borrow::Cow;
use std::fmt::Write;
/// True for any char that is neither printable nor permitted whitespace (only HT).
/// Covers C0/C1/DEL, bidi overrides, zero-width and format chars, and tag chars.
#[inline]
fn is_dangerous_control(c: char) -> bool {
// C0 (except HT), DEL, and C1 controls (U+0080..=U+009F can act as
// single-byte CSI/OSC initiators on 8-bit-control terminals).
matches!(c, '\x00'..='\x08' | '\x0A'..='\x1F' | '\x7F' | '\u{80}'..='\u{9F}')
fn needs_escape(c: char) -> bool {
if c == '\t' {
return false;
}
c.is_control()
|| matches!(c,
'\u{00AD}' // soft hyphen (invisible)
| '\u{180E}' // Mongolian vowel separator
| '\u{200B}'..='\u{200F}' // zero-width + LRM/RLM
| '\u{202A}'..='\u{202E}' // bidi embedding/override
| '\u{2060}'..='\u{206F}' // word joiner, invisibles, deprecated formats
| '\u{FEFF}' // BOM / zero-width no-break space
| '\u{FFF9}'..='\u{FFFB}' // interlinear annotation
| '\u{E0000}'..='\u{E007F}' // language tags
)
}
/// Replace control characters with `\xNN` so the original filename remains recoverable.
/// Replace dangerous chars with `\xNN` / `\u{NNNN}` so the original is recoverable.
pub fn sanitize_for_terminal(s: &str) -> Cow<'_, str> {
if !s.chars().any(is_dangerous_control) {
if !s.chars().any(needs_escape) {
return Cow::Borrowed(s);
}
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if is_dangerous_control(c) {
let _ = write!(out, "\\x{:02X}", c as u32);
if needs_escape(c) {
let v = c as u32;
if v <= 0xFF {
let _ = write!(out, "\\x{:02X}", v);
} else {
let _ = write!(out, "\\u{{{:04X}}}", v);
}
} else {
out.push(c);
}
@@ -98,4 +116,25 @@ mod tests {
"\\x9D0;pwned\\x9C"
);
}
#[test]
fn strips_bidi_overrides_and_zero_width() {
// Trojan-Source style RLO/LRO that flip rendered order of filename text.
assert_eq!(
sanitize_for_terminal("safe\u{202E}fil\u{202D}gnp.exe"),
"safe\\u{202E}fil\\u{202D}gnp.exe"
);
// Zero-width space and BOM are also format chars used to disguise filenames.
assert_eq!(sanitize_for_terminal("a\u{200B}b"), "a\\u{200B}b");
assert_eq!(sanitize_for_terminal("\u{FEFF}name"), "\\u{FEFF}name");
}
#[test]
fn keeps_legitimate_unicode_features() {
// Variation selectors (U+FE0F, U+E0100..) modify preceding glyphs in CJK/emoji
// and are legitimate in filenames. Private-use chars are used by icon fonts.
for s in ["heart\u{2764}\u{FE0F}.txt", "icon\u{E000}.cfg", "cjk\u{6F22}\u{E0101}.txt"] {
assert!(matches!(sanitize_for_terminal(s), Cow::Borrowed(_)), "{s:?}");
}
}
}