diff --git a/CHANGELOG.md b/CHANGELOG.md index 121cc732..4143d9e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,9 @@ - Syntax highlighting for Python files using uv as script runner in shebang #3689 (@janlarres) ## Bugfixes +- Fix `--list-themes` unconditionally probing the terminal via OSC 10/11 even when `--theme` was set to an explicit value, see #3700 (regression introduced in bc42149a). (@optimistiCli) +- Fix inverted `$LESSCLOSE` warning so bat warns on nonzero exit, not on success. See #3654 (@cuiweixie) +- Sanitize control characters in filenames before displaying them in the file header, error messages, and the terminal title, preventing ANSI escape injection via crafted filenames. Closes #3054, see #3691 (@curious-rabbit) - Report initial input read errors instead of treating them as empty input. Closes #3002, see #3706 (@lawrence3699) - Treat ZIP archives as binary content based on their magic header, see #3686 (@officialasishkumar) - Fix i686 `.deb` package using incorrect architecture name (`i686` instead of `i386`), preventing installation on Debian. Closes #3611, see #3650 (@Sim-hu) diff --git a/README.md b/README.md index d6ef3381..5ae91cec 100644 --- a/README.md +++ b/README.md @@ -19,20 +19,6 @@ [Русский]

-### Sponsors - -A special *thank you* goes to our biggest sponsors:
- -

- - Warp -
- Warp, the intelligent terminal -
- Available on MacOS, Linux, Windows -
-

- ### Syntax highlighting `bat` supports syntax highlighting for a large number of programming and markup @@ -208,12 +194,6 @@ Note that the [Manpage syntax](assets/syntaxes/02_Extra/Manpage.sublime-syntax) The [`prettybat`](https://github.com/eth-p/bat-extras/blob/master/doc/prettybat.md) script is a wrapper that will format code and print it with `bat`. -#### `Warp` - - - Warp - - #### Highlighting `--help` messages You can use `bat` to colorize help text: `$ cp --help | bat -plhelp` @@ -527,6 +507,30 @@ and line numbers but no grid and no file header. Set the `BAT_STYLE` environment variable to make these changes permanent or use `bat`'s [configuration file](#configuration-file). +By default, `bat` enables `changes`, `grid`, `header-filename`, `numbers`, and `snip`. + +The available pre-defined styles are: + +| Style | Description | +|-------|-------------| +| `default` | Enables the recommended style components listed above. | +| `full` | Enables all available components. | +| `auto` | Same as `default`, unless the output is piped. | +| `plain` | Disables all available components. | + +The available individual components are: + +| Component | Description | +|-----------|-------------| +| `changes` | Show Git modification markers. | +| `header` | Alias for `header-filename`. | +| `header-filename` | Show filenames before the content. | +| `header-filesize` | Show file sizes before the content. | +| `grid` | Vertical/horizontal lines to separate the side bar and header from the content. | +| `rule` | Horizontal lines to delimit files. | +| `numbers` | Show line numbers in the side bar. | +| `snip` | Draw separation lines between distinct line ranges. | + >[!tip] > If you specify a default style in `bat`'s config file, you can change which components > are displayed during a single run of `bat` using the `--style` command-line argument. diff --git a/assets/syntaxes/02_Extra/PureScript b/assets/syntaxes/02_Extra/PureScript index 5acebc18..1773f4fd 160000 --- a/assets/syntaxes/02_Extra/PureScript +++ b/assets/syntaxes/02_Extra/PureScript @@ -1 +1 @@ -Subproject commit 5acebc18503697be09df047591964e68e80fcf8e +Subproject commit 1773f4fddb08560d6bcb354901088e61e9ea0908 diff --git a/assets/syntaxes/02_Extra/typst-syntax-highlight b/assets/syntaxes/02_Extra/typst-syntax-highlight index 363f0e76..5f71d12f 160000 --- a/assets/syntaxes/02_Extra/typst-syntax-highlight +++ b/assets/syntaxes/02_Extra/typst-syntax-highlight @@ -1 +1 @@ -Subproject commit 363f0e767c938c615a14912c302db7936f025fc2 +Subproject commit 5f71d12fa129165bbe51aa867292555cdff6eb75 diff --git a/doc/sponsors.md b/doc/sponsors.md index 24509077..641e5205 100644 --- a/doc/sponsors.md +++ b/doc/sponsors.md @@ -10,5 +10,3 @@ No issue will have a different priority based on sponsorship status of the reporter. Contributions from anybody are most welcomed, please see our [`CONTRIBUTING.md`](../CONTRIBUTING.md) guide. - -If you want to see our biggest sponsors, check the top of [`README.md`](../README.md#sponsors). diff --git a/doc/sponsors/graphite-logo.jpeg b/doc/sponsors/graphite-logo.jpeg deleted file mode 100644 index 00443818..00000000 Binary files a/doc/sponsors/graphite-logo.jpeg and /dev/null differ diff --git a/doc/sponsors/warp-logo.png b/doc/sponsors/warp-logo.png deleted file mode 100644 index f99dd38c..00000000 Binary files a/doc/sponsors/warp-logo.png and /dev/null differ diff --git a/doc/sponsors/warp-pack-header.png b/doc/sponsors/warp-pack-header.png deleted file mode 100644 index ed7efe6a..00000000 Binary files a/doc/sponsors/warp-pack-header.png and /dev/null differ diff --git a/src/assets.rs b/src/assets.rs index 29247bd7..9483f032 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -158,7 +158,9 @@ impl HighlightingAssets { let syntax_match = mapping.get_syntax_for(path); if let Some(MappingTarget::MapToUnknown) = syntax_match { - return Err(Error::UndetectedSyntax(path.to_string_lossy().into())); + return Err(Error::UndetectedSyntax( + crate::preprocessor::sanitize_for_terminal(&path.to_string_lossy()), + )); } if let Some(MappingTarget::MapTo(syntax_name)) = syntax_match { @@ -175,13 +177,17 @@ impl HighlightingAssets { ) { (Some(syntax), _) => Ok(syntax), - (_, Some(MappingTarget::MapExtensionToUnknown)) => { - Err(Error::UndetectedSyntax(path.to_string_lossy().into())) - } + (_, Some(MappingTarget::MapExtensionToUnknown)) => Err(Error::UndetectedSyntax( + crate::preprocessor::sanitize_for_terminal(&path.to_string_lossy()), + )), _ => self .get_syntax_for_file_extension(file_name, &mapping.ignored_suffixes)? - .ok_or_else(|| Error::UndetectedSyntax(path.to_string_lossy().into())), + .ok_or_else(|| { + Error::UndetectedSyntax(crate::preprocessor::sanitize_for_terminal( + &path.to_string_lossy(), + )) + }), } } diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index f95bbb91..3124cdcd 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -645,7 +645,7 @@ impl App { Ok(styled_components) } - fn theme_options(&self) -> ThemeOptions { + pub(crate) fn theme_options(&self) -> ThemeOptions { Self::theme_options_from_matches(&self.matches) } diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs index f3fa01f4..1d89f8ec 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -17,7 +17,6 @@ use std::path::Path; use std::process; use bat::output::{OutputHandle, OutputType}; -use bat::theme::DetectColorScheme; use nu_ansi_term::Color::Green; use nu_ansi_term::Style; @@ -39,7 +38,7 @@ use bat::{ error::*, input::Input, style::{StyleComponent, StyleComponents}, - theme::{color_scheme, default_theme, ColorScheme}, + theme::{default_theme, theme, ColorScheme, ThemeOptions}, MappingTarget, PagingMode, }; @@ -197,7 +196,7 @@ pub fn list_themes( cfg: &Config, config_dir: &Path, cache_dir: &Path, - detect_color_scheme: DetectColorScheme, + theme_options: ThemeOptions, ) -> Result<()> { let assets = assets_from_cache_or_binary(cfg.use_custom_assets, cache_dir)?; let mut config = cfg.clone(); @@ -206,7 +205,7 @@ pub fn list_themes( config.language = Some("Rust"); config.style_components = StyleComponents(style); - let default_theme_name = default_theme(color_scheme(detect_color_scheme).unwrap_or_default()); + let default_theme_name = theme(theme_options).to_string(); let mut buf = String::new(); let mut handle = OutputHandle::FmtWrite(&mut buf); @@ -259,7 +258,9 @@ pub fn list_themes( fn set_terminal_title_to(new_terminal_title: String) { let osc_command_for_setting_terminal_title = "\x1b]0;"; let osc_end_command = "\x07"; - print!("{osc_command_for_setting_terminal_title}{new_terminal_title}{osc_end_command}"); + // Prevent BEL/ESC/C1 bytes in the title from terminating or nesting the OSC. + let safe_title = bat::sanitize_for_terminal(&new_terminal_title); + print!("{osc_command_for_setting_terminal_title}{safe_title}{osc_end_command}"); io::stdout().flush().unwrap(); } @@ -426,7 +427,7 @@ fn run() -> Result { }; run_controller(inputs, &plain_config, cache_dir) } else if app.matches.get_flag("list-themes") { - list_themes(&config, config_dir, cache_dir, DetectColorScheme::default())?; + list_themes(&config, config_dir, cache_dir, app.theme_options())?; Ok(true) } else if app.matches.get_flag("config-file") { println!("{}", config_file().to_string_lossy()); diff --git a/src/input.rs b/src/input.rs index a3420123..facd64bf 100644 --- a/src/input.rs +++ b/src/input.rs @@ -216,20 +216,20 @@ impl<'a> Input<'a> { description, metadata: self.metadata, reader: { - let mut file = File::open(&path) - .map_err(|e| format!("'{}': {e}", path.to_string_lossy()))?; + let path_display = + crate::preprocessor::sanitize_for_terminal(&path.to_string_lossy()); + let mut file = + File::open(&path).map_err(|e| format!("'{path_display}': {e}"))?; if file.metadata()?.is_dir() { - return Err(format!("'{}' is a directory.", path.to_string_lossy()).into()); + return Err(format!("'{path_display}' is a directory.").into()); } if let Some(stdout) = stdout_identifier { - let input_identifier = Identifier::try_from(file).map_err(|e| { - format!("{}: Error identifying file: {e}", path.to_string_lossy()) - })?; + let input_identifier = Identifier::try_from(file) + .map_err(|e| format!("{path_display}: Error identifying file: {e}"))?; if stdout.surely_conflicts_with(&input_identifier) { return Err(format!( - "IO circle detected. The input from '{}' is also an output. Aborting to avoid infinite loop.", - path.to_string_lossy() + "IO circle detected. The input from '{path_display}' is also an output. Aborting to avoid infinite loop.", ) .into()); } diff --git a/src/lessopen.rs b/src/lessopen.rs index 116206ba..c7593b53 100644 --- a/src/lessopen.rs +++ b/src/lessopen.rs @@ -261,7 +261,7 @@ impl Drop for Preprocessed { } }; - if lessclose_output.status.success() { + if !lessclose_output.status.success() { bat_warning!("$LESSCLOSE exited with nonzero exit code",) }; } diff --git a/src/lib.rs b/src/lib.rs index 4c60f10e..685214dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,6 +54,7 @@ mod vscreen; pub(crate) mod wrapping; pub use nonprintable_notation::{BinaryBehavior, NonprintableNotation}; +pub use preprocessor::sanitize_for_terminal; pub use preprocessor::StripAnsiMode; pub use pretty_printer::{Input, PrettyPrinter, Syntax}; pub use syntax_mapping::{MappingTarget, SyntaxMapping}; diff --git a/src/preprocessor.rs b/src/preprocessor.rs index 6b4e2935..a34f9f9e 100644 --- a/src/preprocessor.rs +++ b/src/preprocessor.rs @@ -149,6 +149,35 @@ pub fn strip_ansi(line: &str) -> String { buffer } +/// Escape C0, DEL, and C1 control characters so a string from an untrusted +/// filename or path can be safely written to the terminal. +pub fn sanitize_for_terminal(input: &str) -> String { + if !input + .chars() + .any(|c| matches!(c, '\x00'..='\x08' | '\x0A'..='\x1F' | '\x7F'..='\u{9F}')) + { + return input.to_owned(); + } + + let mut out = String::with_capacity(input.len() + 8); + for c in input.chars() { + match c { + '\t' => out.push('\t'), + '\x00'..='\x1F' => { + out.push('^'); + out.push(char::from_u32(0x40 + c as u32).unwrap_or('?')); + } + '\x7F' => out.push_str("^?"), + '\u{80}'..='\u{9F}' => { + use std::fmt::Write as _; + let _ = write!(out, "\\u{{{:x}}}", c as u32); + } + other => out.push(other), + } + } + out +} + /// Strips overstrike sequences (backspace formatting) from input. /// /// Overstrike formatting is used by man pages and some help output: @@ -261,3 +290,40 @@ fn test_strip_overstrike() { // Unicode with overstrike assert_eq!(strip_overstrike("ä\x08äöü", 2), "äöü"); } + +#[test] +fn test_sanitize_for_terminal_passthrough() { + assert_eq!(sanitize_for_terminal(""), ""); + assert_eq!(sanitize_for_terminal("hello.txt"), "hello.txt"); + assert_eq!(sanitize_for_terminal("résumé.pdf"), "résumé.pdf"); + assert_eq!(sanitize_for_terminal("日本語.md"), "日本語.md"); + assert_eq!( + sanitize_for_terminal("path/with spaces/file.log"), + "path/with spaces/file.log" + ); + assert_eq!(sanitize_for_terminal("a\tb"), "a\tb"); +} + +#[test] +fn test_sanitize_for_terminal_c0_controls() { + assert_eq!( + sanitize_for_terminal("\x1b[31mINJECTED\x1b[0m.txt"), + "^[[31mINJECTED^[[0m.txt" + ); + assert_eq!(sanitize_for_terminal("bad\x07rest"), "bad^Grest"); + assert_eq!(sanitize_for_terminal("\x00\x08\n\r\x7F"), "^@^H^J^M^?"); + assert_eq!(sanitize_for_terminal("\u{9b}31m"), "\\u{9b}31m"); + assert_eq!( + sanitize_for_terminal("\u{9d}0;pwned\x07"), + "\\u{9d}0;pwned^G" + ); +} + +#[test] +fn test_sanitize_for_terminal_idempotent_on_sanitized() { + let dirty = "\x1b]0;pwned\x07file.txt"; + let clean = sanitize_for_terminal(dirty); + assert_eq!(sanitize_for_terminal(&clean), clean); + assert!(!clean.contains('\x1b')); + assert!(!clean.contains('\x07')); +} diff --git a/src/printer.rs b/src/printer.rs index c58c914d..17f7aa30 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -29,7 +29,9 @@ use crate::error::*; use crate::input::OpenedInput; use crate::line_range::{MaxBufferedLineNumber, RangeCheckResult}; use crate::output::OutputHandle; -use crate::preprocessor::{expand_tabs, replace_nonprintable, strip_ansi, strip_overstrike}; +use crate::preprocessor::{ + expand_tabs, replace_nonprintable, sanitize_for_terminal, strip_ansi, strip_overstrike, +}; use crate::style::StyleComponent; use crate::terminal::{as_terminal_escaped, to_ansi_color}; use crate::vscreen::{AnsiStyle, EscapeSequence, EscapeSequenceIterator}; @@ -489,7 +491,7 @@ impl Printer for InteractivePrinter<'_> { (but will be present if the output of 'bat' is piped). You can use 'bat -A' \ to show the binary file contents.", Yellow.paint("[bat warning]"), - input.description.summary(), + sanitize_for_terminal(&input.description.summary()), )?; } else if self.config.style_components.grid() { self.print_horizontal_line(handle, '┬')?; @@ -543,9 +545,11 @@ impl Printer for InteractivePrinter<'_> { "{}{}{mode}", description .kind() - .map(|kind| format!("{kind}: ")) + .map(|kind| format!("{}: ", sanitize_for_terminal(kind))) .unwrap_or_else(|| "".into()), - self.colors.header_value.paint(description.title()), + self.colors + .header_value + .paint(sanitize_for_terminal(description.title())), ); self.print_header_multiline_component(handle, &header_filename) }