From 8157be12436cd59910e3166486c24c5cddcdd6c3 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Thu, 27 Aug 2026 23:35:43 +0200 Subject: [PATCH] fix: render actual newline in terminal-sanitized error messages sanitize_for_terminal() escaped every control char it saw, including plain LF, so any multi-line diagnostic built from a hardcoded template (e.g. the path-separator error in main.rs) came out on a TTY as the literal 4-character sequence "\x0A" instead of a line break. LF cannot draw, move the cursor, or exfiltrate data the way ESC- and CSI/OSC-initiated sequences can, so it does not need the same defense as those. CR is still escaped since it can overwrite the current line and forge fake output. Closes #2104 --- CHANGELOG.md | 3 ++- src/sanitize.rs | 32 +++++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee98b5e..4e1d346 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ - ## Bugfixes -- +- Render actual newlines instead of the literal `\x0A` escape in multi-line error + messages printed to a terminal, see #2104 # 10.5.0 diff --git a/src/sanitize.rs b/src/sanitize.rs index 6ff8a94..3f8aed7 100644 --- a/src/sanitize.rs +++ b/src/sanitize.rs @@ -3,11 +3,19 @@ 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. +/// True for any char that is neither printable nor permitted whitespace (only HT +/// and LF). Covers C0/C1/DEL, bidi overrides, zero-width and format chars, and tag +/// chars. +/// +/// LF is deliberately let through even though it is a C0 control: it only ever +/// starts a new line and cannot draw, move the cursor or exfiltrate data the way +/// ESC- and CSI/OSC-initiated sequences can, so escaping it just mangles the +/// developer-authored multi-line diagnostics fd prints (see sharkdp/fd#2104). +/// CR is still escaped: unlike LF it can overwrite the current line and forge +/// fake output. #[inline] fn needs_escape(c: char) -> bool { - if c == '\t' { + if c == '\t' || c == '\n' { return false; } c.is_control() @@ -107,8 +115,22 @@ mod tests { } #[test] - fn strips_newline() { - assert_eq!(sanitize_for_terminal("a\nb"), "a\\x0Ab"); + fn preserves_newline() { + assert!(matches!(sanitize_for_terminal("a\nb"), Cow::Borrowed(_))); + assert_eq!(sanitize_for_terminal("a\nb"), "a\nb"); + } + + #[test] + fn preserves_real_newlines_in_diagnostics() { + // Multi-line diagnostics such as the path-separator error in main.rs use + // real newlines for readability; they carry no attacker-controlled bytes, + // so a bare '\n' must reach the terminal as an actual line break, not as + // the literal 4-character sequence "\x0A". See sharkdp/fd#2104. + let message = "The search pattern 'a/b' contains a path-separation character \ + and will not lead to any search results.\n\n\ + If you want to search for all files inside the 'a/b' directory, use a match-all pattern:\n\n \ + fd . 'a/b'"; + assert_eq!(sanitize_for_terminal(message), message); } #[test]