forked from mirrors/jj
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:
parent
13a177f211
commit
e716168368
3 changed files with 70 additions and 3 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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(), @"[1mactually bold[0m");
|
||||
}
|
||||
|
||||
#[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.
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue