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]