Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,11 @@
//! - In the application, calling [`Builder::write_style(Never)`][Builder::write_style] to have all ANSI escape codes stripped
//! - In the application, [stripping ANSI escape codes](https://docs.rs/anstream/latest/anstream/adapter/fn.strip_str.html)
//! from user inputs
//! - Deactivating the build-time feature `color`, which escapes control characters as `\xNN`
//! rather than stripping them, and also covers the `\r`, VT and FF that stripping passes on
//!
//! Note: deactivating the build-time feature `color` is not a mitigation as that removes all ANSI escape code
//! stripping from `env_logger`.
//! Note: `\n` is never escaped, so untrusted input can still read as an extra record. The
//! default [indentation][Builder::format_indent] offsets such lines by four spaces.
//!
//! </div>
//!
Expand Down
40 changes: 40 additions & 0 deletions src/writer/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ impl BufferWriter {
use std::io::Write as _;

let buf = buf.as_bytes();
#[cfg(not(feature = "color"))]
let buf = &escape_controls(buf);
match &self.target {
WritableTarget::WriteStdout => {
let stream = io::stdout();
Expand Down Expand Up @@ -104,6 +106,44 @@ impl BufferWriter {
}
}

/// Escape control characters as `\xNN`, so that logging untrusted input can't
/// drive the terminal of whoever later reads the output. This build emits no
/// styling of its own, so unlike `anstream` it needs no VT parser to do it.
#[cfg(not(feature = "color"))]
fn escape_controls(buf: &[u8]) -> std::borrow::Cow<'_, [u8]> {
const HEX: &[u8; 16] = b"0123456789abcdef";

// Bitwise fold rather than `any`: short-circuiting would force a scalar loop.
let has_control = buf.iter().fold(false, |acc, &b| acc | is_control(b));
if !has_control {
return std::borrow::Cow::Borrowed(buf);
}

// Only records that actually carry controls pay for a copy, and the scan
// above found at least one, each of which grows the record by 3 bytes.
let mut escaped = Vec::with_capacity(buf.len() + 3);
for &byte in buf {
if is_control(byte) {
let (hi, lo) = (usize::from(byte >> 4), usize::from(byte & 0xf));
escaped.extend_from_slice(&[b'\\', b'x', HEX[hi], HEX[lo]]);
} else {
escaped.push(byte);
}
}
std::borrow::Cow::Owned(escaped)
}

/// Control is all of C0 plus DEL, minus `\n` and `\t`, which the format itself
/// relies on. Stricter than `anstream`, whose strip passes `\r`, VT and FF.
///
/// Multi-byte UTF-8 isn't flagged, having no byte in the tested ASCII range,
/// nor is raw C1; records come from `str`, so only a custom format emits that.
#[cfg(not(feature = "color"))]
#[inline]
fn is_control(byte: u8) -> bool {
((byte < 0x20) & (byte != b'\n') & (byte != b'\t')) | (byte == 0x7f)
}

#[cfg(feature = "color")]
fn adapt(buf: &[u8], write_style: WriteStyle) -> io::Result<Vec<u8>> {
use std::io::Write as _;
Expand Down