cli: also sanitize non-colored output printed to a terminal

This makes us sanitize ANSI escape bytes in the output if it goes to
the terminal, even when it's not colored (by us), such as when using
`--color=never`. That means that e.g. `jj cat
tests/test_commit_template.rs` will not be colored, but `jj cat
tests/test_commit_template.rs | cat` will be. Sanitizing output sent
to the terminal might help reduce some security threats based on
hiding content by using ANSI escapes.

We could add a config option for sanitizing the output, but I'm not
sure it'll be useful.
This commit is contained in:
Martin von Zweigbergk 2023-01-21 13:55:34 -08:00 committed by Martin von Zweigbergk
parent 13a177f211
commit e716168368
3 changed files with 70 additions and 3 deletions

View file

@ -73,6 +73,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
with brackets. To disable, set the new `ui.unique-prefixes` option to `none`
* `jj print` was renamed to `jj cat`. `jj print` remains as an alias.
* In content that goes to the terminal, the ANSI escape byte (0x1b) is replaced
by a "␛" character. That prevents them from interfering with the ANSI escapes
jj itself writes.
### Fixed bugs

View file

@ -93,14 +93,17 @@ pub struct FormatterFactory {
#[derive(Clone, Debug)]
enum FormatterFactoryKind {
PlainText,
Sanitized,
Color { rules: Arc<Rules> },
}
impl FormatterFactory {
pub fn prepare(config: &config::Config, color: bool) -> Self {
pub fn prepare(config: &config::Config, color: bool, sanitized: bool) -> Self {
let kind = if color {
let rules = Arc::new(rules_from_config(config));
FormatterFactoryKind::Color { rules }
} else if sanitized {
FormatterFactoryKind::Sanitized
} else {
FormatterFactoryKind::PlainText
};
@ -113,6 +116,7 @@ impl FormatterFactory {
) -> Box<dyn Formatter + 'output> {
match &self.kind {
FormatterFactoryKind::PlainText => Box::new(PlainTextFormatter::new(output)),
FormatterFactoryKind::Sanitized => Box::new(SanitizingFormatter::new(output)),
FormatterFactoryKind::Color { rules } => {
Box::new(ColorFormatter::new(output, rules.clone()))
}
@ -154,6 +158,41 @@ impl<W: Write> Formatter for PlainTextFormatter<W> {
}
}
pub struct SanitizingFormatter<W> {
output: W,
}
impl<W> SanitizingFormatter<W> {
pub fn new(output: W) -> SanitizingFormatter<W> {
Self { output }
}
}
impl<W: Write> Write for SanitizingFormatter<W> {
fn write(&mut self, data: &[u8]) -> Result<usize, Error> {
write_sanitized(&mut self.output, data)?;
Ok(data.len())
}
fn flush(&mut self) -> Result<(), Error> {
self.output.flush()
}
}
impl<W: Write> Formatter for SanitizingFormatter<W> {
fn raw(&mut self) -> &mut dyn Write {
&mut self.output
}
fn push_label(&mut self, _label: &str) -> io::Result<()> {
Ok(())
}
fn pop_label(&mut self) -> io::Result<()> {
Ok(())
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Style {
pub fg_color: Option<Color>,
@ -451,6 +490,26 @@ mod tests {
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"hello");
}
#[test]
fn test_plaintext_formatter_ansi_codes_in_text() {
// Test that ANSI codes in the input text are NOT escaped.
let mut output: Vec<u8> = vec![];
let mut formatter = PlainTextFormatter::new(&mut output);
formatter.write_str("\x1b[1mactually bold\x1b[0m").unwrap();
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"actually bold");
}
#[test]
fn test_sanitizing_formatter_ansi_codes_in_text() {
// Test that ANSI codes in the input text are escaped.
let mut output: Vec<u8> = vec![];
let mut formatter = SanitizingFormatter::new(&mut output);
formatter
.write_str("\x1b[1mnot actually bold\x1b[0m")
.unwrap();
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"␛[1mnot actually bold␛[0m");
}
#[test]
fn test_color_formatter_color_codes() {
// Test the color code for each color.

View file

@ -109,8 +109,11 @@ fn pager_setting(config: &config::Config) -> FullCommandArgs {
impl Ui {
pub fn with_config(config: &config::Config) -> Ui {
let color = use_color(color_setting(config));
// Sanitize ANSI escape codes if we're printing to a terminal. Doesn't affect
// ANSI escape codes that originate from the formatter itself.
let sanitize = io::stdout().is_tty();
let formatter_factory = FormatterFactory::prepare(config, color, sanitize);
let progress_indicator = progress_indicator_setting(config);
let formatter_factory = FormatterFactory::prepare(config, color);
Ui {
color,
formatter_factory,
@ -125,7 +128,8 @@ impl Ui {
self.color = use_color(color_setting(config));
self.pager_cmd = pager_setting(config);
self.progress_indicator = progress_indicator_setting(config);
self.formatter_factory = FormatterFactory::prepare(config, self.color);
let sanitize = io::stdout().is_tty();
self.formatter_factory = FormatterFactory::prepare(config, self.color, sanitize);
}
/// Sets the pagination value.