1
0
mirror of https://github.com/sharkdp/bat synced 2026-08-04 19:01:44 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 6510fc0c74 build(deps): bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-01 02:03:45 +00:00
21 changed files with 251 additions and 885 deletions
+9 -9
View File
@@ -35,7 +35,7 @@ jobs:
name: Extract crate metadata name: Extract crate metadata
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Extract crate information - name: Extract crate information
id: crate_metadata id: crate_metadata
run: | run: |
@@ -58,7 +58,7 @@ jobs:
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
components: rustfmt,clippy components: rustfmt,clippy
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- run: cargo fmt -- --check - run: cargo fmt -- --check
- run: cargo clippy --locked --all-targets --all-features -- -D warnings - run: cargo clippy --locked --all-targets --all-features -- -D warnings
@@ -68,7 +68,7 @@ jobs:
needs: crate_metadata needs: crate_metadata
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: Install rust toolchain (v${{ needs.crate_metadata.outputs.msrv }}) - name: Install rust toolchain (v${{ needs.crate_metadata.outputs.msrv }})
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
with: with:
@@ -80,7 +80,7 @@ jobs:
name: License checks name: License checks
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
with: with:
submodules: true # we especially want to perform license checks on submodules submodules: true # we especially want to perform license checks on submodules
- run: tests/scripts/license-checks.sh - run: tests/scripts/license-checks.sh
@@ -90,7 +90,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Git checkout - name: Git checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
with: with:
submodules: true # we need all syntax and theme submodules submodules: true # we need all syntax and theme submodules
- name: Install Rust toolchain - name: Install Rust toolchain
@@ -119,7 +119,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Git checkout - name: Git checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: Prepare environment variables - name: Prepare environment variables
run: | run: |
echo "BAT_SYSTEM_CONFIG_PREFIX=$GITHUB_WORKSPACE/tests/examples/system_config" >> $GITHUB_ENV echo "BAT_SYSTEM_CONFIG_PREFIX=$GITHUB_WORKSPACE/tests/examples/system_config" >> $GITHUB_ENV
@@ -135,7 +135,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Git checkout - name: Git checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: Install Rust toolchain - name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Check documentation - name: Check documentation
@@ -150,7 +150,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- run: cargo install cargo-audit --locked - run: cargo install cargo-audit --locked
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- run: cargo audit - run: cargo audit
build: build:
@@ -178,7 +178,7 @@ jobs:
BUILD_CMD: cargo BUILD_CMD: cargo
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: Install prerequisites - name: Install prerequisites
shell: bash shell: bash
@@ -13,7 +13,7 @@ jobs:
PR_NUMBER: ${{ github.event.number }} PR_NUMBER: ${{ github.event.number }}
PR_BASE: ${{ github.base_ref }} PR_BASE: ${{ github.base_ref }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Fetch PR base - name: Fetch PR base
run: git fetch --no-tags --prune --depth=1 origin run: git fetch --no-tags --prune --depth=1 origin
-5
View File
@@ -3,13 +3,11 @@
## Other ## Other
- Update Cargo dependencies to resolve current RustSec advisories, see #3861 (@TyceHerrman)
- Add instructions for removing fish help abbreviations to README, see #3655 (@claw-explorer). Closes #3536 - Add instructions for removing fish help abbreviations to README, see #3655 (@claw-explorer). Closes #3536
- Add .NET slnx extension, see #3682 (@ltrzesniewski) - Add .NET slnx extension, see #3682 (@ltrzesniewski)
## Features ## Features
- Add a `--sanitize=<auto|always|never>` flag for safe display of untrusted input. It implies `--strip-ansi` at the same value and additionally substitutes terminal-active control bytes (cursor moves, charset switches, beep, etc.) and Unicode bidi / zero-width formatting characters with the Unicode replacement character (U+FFFD). Mitigates Trojan-Source-style spoofing (CVE-2021-42574). See #3729 (@curious-rabbit)
- Map justfile, Justfile, .justfile, and *.justfile to Makefile syntax highlighting, see #3623 (@zachvalenta) - Map justfile, Justfile, .justfile, and *.justfile to Makefile syntax highlighting, see #3623 (@zachvalenta)
- Preserve `--diff` change markers and snip separators when `--plain` is set. Closes #3630, see #3643 (@mvanhorn) - Preserve `--diff` change markers and snip separators when `--plain` is set. Closes #3630, see #3643 (@mvanhorn)
- Added support for `hidden_file_extensions` from `.sublime-syntax` files, see #3613 (@Matei02355) - Added support for `hidden_file_extensions` from `.sublime-syntax` files, see #3613 (@Matei02355)
@@ -24,9 +22,6 @@
- Syntax highlighting for Python files using uv as script runner in shebang #3689 (@janlarres) - Syntax highlighting for Python files using uv as script runner in shebang #3689 (@janlarres)
## Bugfixes ## Bugfixes
- Fix `--list-languages` respecting `--paging=never`, see #3828 (@cyphercodes)
- Fix `--sanitize` passing through the bidi control characters U+200E, U+200F and U+061C, see #3862 (@lenamonj)
- `--strip-ansi`: also strip 8-bit C1 introducers (U+0090, U+0098, U+009B, U+009D, U+009E, U+009F) and DCS/SOS/PM/APC sequence bodies, which previously passed through. See #3729 (@curious-rabbit)
- Fix `--ignored-suffix` not falling back to first-line/shebang detection when the ignored suffix is also a registered extension (e.g. `--ignored-suffix .txt` on a shebang script), see #2745 and #3816 (@adnrivera) - Fix `--ignored-suffix` not falling back to first-line/shebang detection when the ignored suffix is also a registered extension (e.g. `--ignored-suffix .txt` on a shebang script), see #2745 and #3816 (@adnrivera)
- Fix `capacity overflow` panic when printing a snip separator at `--terminal-width=1` with multiple line ranges. Closes #3803, see #3804 (@leeewee) - Fix `capacity overflow` panic when printing a snip separator at `--terminal-width=1` with multiple line ranges. Closes #3803, see #3804 (@leeewee)
- Pass `--no-paging` to `bat` invocations inside the bash / zsh / fish / PowerShell shell completion scripts so that shell-level pager wiring (e.g. `LESSOPEN='|-bat -f -pp %s'`) cannot inject ANSI escape sequences into the completion candidates. Closes #3760 (@mvanhorn) - Pass `--no-paging` to `bat` invocations inside the bash / zsh / fish / PowerShell shell completion scripts so that shell-level pager wiring (e.g. `LESSOPEN='|-bat -f -pp %s'`) cannot inject ANSI escape sequences into the completion candidates. Closes #3760 (@mvanhorn)
Generated
+200 -186
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -70,13 +70,13 @@ regex = { version = "1.12.3", optional = true }
walkdir = { version = "2.5", optional = true } walkdir = { version = "2.5", optional = true }
bytesize = { version = "2.3.1" } bytesize = { version = "2.3.1" }
encoding_rs = "0.8.35" encoding_rs = "0.8.35"
execute = { version = "0.3.0", optional = true } execute = { version = "0.2.15", optional = true }
terminal-colorsaurus = "1.0" terminal-colorsaurus = "1.0"
unicode-segmentation = "1.13.2" unicode-segmentation = "1.13.2"
itertools = "0.14.0" itertools = "0.14.0"
[dependencies.gix] [dependencies.gix]
version = "0.86" version = "0.85"
optional = true optional = true
default-features = false default-features = false
features = ["sha1", "blob-diff"] features = ["sha1", "blob-diff"]
-4
View File
@@ -843,10 +843,6 @@ If your version of `bat` supports the `--strip-ansi=auto` option, it can be used
before syntax highlighting. Alternatively, you may disable both syntax highlighting and wrapping by before syntax highlighting. Alternatively, you may disable both syntax highlighting and wrapping by
passing the `--color=never --wrap=never` options to `bat`. passing the `--color=never --wrap=never` options to `bat`.
For untrusted input, the `--sanitize=auto|always|never` option additionally replaces terminal-active
control bytes and Unicode bidi / zero-width formatting characters with the Unicode replacement
character. It implies `--strip-ansi` at the same value.
> [!NOTE] > [!NOTE]
> The `auto` option of `--strip-ansi` avoids removing escape sequences when the syntax is plain text. > The `auto` option of `--strip-ansi` avoids removing escape sequences when the syntax is plain text.
-6
View File
@@ -100,11 +100,6 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script
ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_)} ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_)}
break break
} }
'*;--sanitize' {
$ArrayWhen |
ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_)}
break
}
'*;--strip-ansi' { '*;--strip-ansi' {
$ArrayWhen | $ArrayWhen |
ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_)} ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_)}
@@ -163,7 +158,6 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script
[CompletionResult]::new('--ignored-suffix' , 'ignored-suffix' , [CompletionResultType]::ParameterName, 'Ignore extension. For example: ''bat --ignored-suffix ".dev" my_file.json.dev'' will use JSON syntax, and ignore ''.dev''') [CompletionResult]::new('--ignored-suffix' , 'ignored-suffix' , [CompletionResultType]::ParameterName, 'Ignore extension. For example: ''bat --ignored-suffix ".dev" my_file.json.dev'' will use JSON syntax, and ignore ''.dev''')
[CompletionResult]::new('--squeeze-blank' , 'squeeze-blank' , [CompletionResultType]::ParameterName, 'Squeeze consecutive empty lines into a single empty line.') [CompletionResult]::new('--squeeze-blank' , 'squeeze-blank' , [CompletionResultType]::ParameterName, 'Squeeze consecutive empty lines into a single empty line.')
[CompletionResult]::new('--squeeze-limit' , 'squeeze-limit' , [CompletionResultType]::ParameterName, 'Set the maximum number of consecutive empty lines to be printed.') [CompletionResult]::new('--squeeze-limit' , 'squeeze-limit' , [CompletionResultType]::ParameterName, 'Set the maximum number of consecutive empty lines to be printed.')
[CompletionResult]::new('--sanitize' , 'sanitize' , [CompletionResultType]::ParameterName, 'Specify when to sanitize untrusted input for safe display. Implies --strip-ansi and also replaces terminal-active and bidi / zero-width bytes. (auto, always, *never*).')
[CompletionResult]::new('--strip-ansi' , 'strip-ansi' , [CompletionResultType]::ParameterName, 'Specify when to strip ANSI escape sequences from the input. The automatic mode will remove escape sequences unless the syntax highlighting language is plain text. (auto, always, *never*).') [CompletionResult]::new('--strip-ansi' , 'strip-ansi' , [CompletionResultType]::ParameterName, 'Specify when to strip ANSI escape sequences from the input. The automatic mode will remove escape sequences unless the syntax highlighting language is plain text. (auto, always, *never*).')
# [CompletionResult]::new('-p' , 'p' , [CompletionResultType]::ParameterName, 'Show plain style (alias for ''--style=plain'').') # [CompletionResult]::new('-p' , 'p' , [CompletionResultType]::ParameterName, 'Show plain style (alias for ''--style=plain'').')
[CompletionResult]::new('--plain' , 'plain' , [CompletionResultType]::ParameterName, 'Show plain style (alias for ''--style=plain'').') [CompletionResult]::new('--plain' , 'plain' , [CompletionResultType]::ParameterName, 'Show plain style (alias for ''--style=plain'').')
-5
View File
@@ -132,10 +132,6 @@ _bat() {
COMPREPLY=($(compgen -W "auto never always" -- "$cur")) COMPREPLY=($(compgen -W "auto never always" -- "$cur"))
return 0 return 0
;; ;;
--sanitize)
COMPREPLY=($(compgen -W "auto never always" -- "$cur"))
return 0
;;
--completion) --completion)
COMPREPLY=($(compgen -W "bash fish zsh ps1" -- "$cur")) COMPREPLY=($(compgen -W "bash fish zsh ps1" -- "$cur"))
return 0 return 0
@@ -225,7 +221,6 @@ _bat() {
--list-themes --list-themes
--squeeze-blank --squeeze-blank
--squeeze-limit --squeeze-limit
--sanitize
--strip-ansi --strip-ansi
--style --style
--line-range --line-range
-1
View File
@@ -221,7 +221,6 @@ complete -c $bat -s s -l squeeze-blank -d "Squeeze consecutive empty lines into
complete -c $bat -l squeeze-limit -x -d "Set the maximum number of consecutive empty lines to be printed" -n __bat_no_excl_args complete -c $bat -l squeeze-limit -x -d "Set the maximum number of consecutive empty lines to be printed" -n __bat_no_excl_args
complete -c $bat -l strip-ansi -x -a "auto never always" -d "Specify when to strip ANSI escape sequences from the input" -n __bat_no_excl_args complete -c $bat -l strip-ansi -x -a "auto never always" -d "Specify when to strip ANSI escape sequences from the input" -n __bat_no_excl_args
complete -c $bat -l sanitize -x -a "auto never always" -d "Specify when to sanitize untrusted input for safe display" -n __bat_no_excl_args
complete -c $bat -s p -l plain -d "Disable decorations" -n __bat_no_excl_args complete -c $bat -s p -l plain -d "Disable decorations" -n __bat_no_excl_args
-1
View File
@@ -54,7 +54,6 @@ _{{PROJECT_EXECUTABLE}}_main() {
--squeeze-blank'[squeeze consecutive empty lines into a single empty line]' --squeeze-blank'[squeeze consecutive empty lines into a single empty line]'
--squeeze-limit='[set the maximum number of consecutive empty lines]:limit:' --squeeze-limit='[set the maximum number of consecutive empty lines]:limit:'
--strip-ansi='[specify when to strip ANSI escape sequences]:when:(auto never always)' --strip-ansi='[specify when to strip ANSI escape sequences]:when:(auto never always)'
--sanitize='[specify when to sanitize untrusted input for safe display]:when:(auto never always)'
--style='[comma-separated list of style elements to display]: : _values "style [default]" --style='[comma-separated list of style elements to display]: : _values "style [default]"
default auto full plain changes header header-filename header-filesize grid rule numbers snip' default auto full plain changes header header-filename header-filesize grid rule numbers snip'
\*{-r+,--line-range=}'[only print the specified line range]:start\:end' \*{-r+,--line-range=}'[only print the specified line range]:start\:end'
-8
View File
@@ -164,14 +164,6 @@ Options:
escape sequences unless the syntax highlighting language is plain text. Possible values: escape sequences unless the syntax highlighting language is plain text. Possible values:
auto, always, *never*. auto, always, *never*.
--sanitize <when>
Specify when to sanitize input bytes for safe terminal display. Implies --strip-ansi to
the same value, and additionally substitutes terminal-active control bytes (cursor moves,
charset switches, beep, etc.) and Unicode bidi / zero-width formatting characters with the
Unicode replacement character (U+FFFD). Tab, LF, FF, and CRLF pass through. Useful for
displaying untrusted file content (e.g. file-manager preview panes). Possible values:
auto, always, *never*.
--style <components> --style <components>
Configure which elements (line numbers, file headers, grid borders, Git modifications, ..) Configure which elements (line numbers, file headers, grid borders, Git modifications, ..)
to display in addition to the file contents. The argument is a comma-separated list of to display in addition to the file contents. The argument is a comma-separated list of
+8 -33
View File
@@ -38,15 +38,6 @@ pub fn env_no_color() -> bool {
env::var_os("NO_COLOR").is_some_and(|x| !x.is_empty()) env::var_os("NO_COLOR").is_some_and(|x| !x.is_empty())
} }
fn parse_strip_ansi_value(raw: Option<&str>, flag_name: &str) -> StripAnsiMode {
match raw {
Some("never") | None => StripAnsiMode::Never,
Some("always") => StripAnsiMode::Always,
Some("auto") => StripAnsiMode::Auto,
_ => unreachable!("other values for {flag_name} are not allowed"),
}
}
enum HelpType { enum HelpType {
Short, Short,
Long, Long,
@@ -467,32 +458,16 @@ impl App {
4 4
}, },
), ),
strip_ansi: { strip_ansi: match self
let sanitize = parse_strip_ansi_value( .matches
self.matches
.get_one::<String>("sanitize")
.map(|s| s.as_str()),
"--sanitize",
);
let strip_ansi = parse_strip_ansi_value(
self.matches
.get_one::<String>("strip-ansi") .get_one::<String>("strip-ansi")
.map(|s| s.as_str()), .map(|s| s.as_str())
"--strip-ansi", {
); Some("never") => StripAnsiMode::Never,
// --sanitize implies --strip-ansi to the same value. Some("always") => StripAnsiMode::Always,
if sanitize != StripAnsiMode::Never { Some("auto") => StripAnsiMode::Auto,
sanitize _ => unreachable!("other values for --strip-ansi are not allowed"),
} else {
strip_ansi
}
}, },
sanitize: parse_strip_ansi_value(
self.matches
.get_one::<String>("sanitize")
.map(|s| s.as_str()),
"--sanitize",
),
quiet_empty: self.matches.get_flag("quiet-empty"), quiet_empty: self.matches.get_flag("quiet-empty"),
unbuffered: self.matches.get_flag("unbuffered"), unbuffered: self.matches.get_flag("unbuffered"),
theme: theme(self.theme_options()).to_string(), theme: theme(self.theme_options()).to_string(),
-18
View File
@@ -480,24 +480,6 @@ pub fn build_app(interactive_output: bool) -> Command {
language is plain text. Possible values: auto, always, *never*.") language is plain text. Possible values: auto, always, *never*.")
.hide_short_help(true) .hide_short_help(true)
) )
.arg(
Arg::new("sanitize")
.long("sanitize")
.overrides_with("sanitize")
.value_name("when")
.value_parser(["auto", "always", "never"])
.default_value("never")
.hide_default_value(true)
.help("Sanitize untrusted input for safe display (auto, always, *never*)")
.long_help("Specify when to sanitize input bytes for safe terminal display. \
Implies --strip-ansi to the same value, and additionally substitutes \
terminal-active control bytes (cursor moves, charset switches, beep, etc.) \
and Unicode bidi / zero-width formatting characters with the Unicode \
replacement character (U+FFFD). Tab, LF, FF, and CRLF pass through. Useful \
for displaying untrusted file content (e.g. file-manager preview panes). \
Possible values: auto, always, *never*.")
.hide_short_help(true)
)
.arg( .arg(
Arg::new("style") Arg::new("style")
.long("style") .long("style")
+5 -3
View File
@@ -427,9 +427,11 @@ fn run() -> Result<bool> {
if app.matches.get_flag("list-languages") { if app.matches.get_flag("list-languages") {
let languages: String = get_languages(&config, cache_dir)?; let languages: String = get_languages(&config, cache_dir)?;
let inputs: Vec<Input> = vec![Input::from_reader(Box::new(languages.as_bytes()))]; let inputs: Vec<Input> = vec![Input::from_reader(Box::new(languages.as_bytes()))];
let mut plain_config = config.clone(); let plain_config = Config {
plain_config.style_components = style_components: StyleComponents::new(StyleComponent::Plain.components(false)),
StyleComponents::new(StyleComponent::Plain.components(false)); paging_mode: PagingMode::QuitIfOneScreen,
..Default::default()
};
run_controller(inputs, &plain_config, cache_dir) run_controller(inputs, &plain_config, cache_dir)
} else if app.matches.get_flag("list-themes") { } else if app.matches.get_flag("list-themes") {
list_themes(&config, config_dir, cache_dir, app.theme_options())?; list_themes(&config, config_dir, cache_dir, app.theme_options())?;
-3
View File
@@ -111,9 +111,6 @@ pub struct Config<'a> {
// Whether or not to strip ANSI escape codes from the input // Whether or not to strip ANSI escape codes from the input
pub strip_ansi: StripAnsiMode, pub strip_ansi: StripAnsiMode,
// Substitute terminal-active and spoofing-relevant bytes; implies strip_ansi.
pub sanitize: StripAnsiMode,
/// Whether or not to produce no output when input is empty /// Whether or not to produce no output when input is empty
pub quiet_empty: bool, pub quiet_empty: bool,
+2 -204
View File
@@ -139,111 +139,16 @@ pub fn replace_nonprintable(
/// Strips ANSI escape sequences from the input. /// Strips ANSI escape sequences from the input.
pub fn strip_ansi(line: &str) -> String { pub fn strip_ansi(line: &str) -> String {
let mut buffer = String::with_capacity(line.len()); let mut buffer = String::with_capacity(line.len());
for seq in EscapeSequenceOffsetsIterator::new(line) { for seq in EscapeSequenceOffsetsIterator::new(line) {
if let EscapeSequenceOffsets::Text { .. } = seq { if let EscapeSequenceOffsets::Text { .. } = seq {
buffer.push_str(&line[seq.index_of_start()..seq.index_past_end()]); buffer.push_str(&line[seq.index_of_start()..seq.index_past_end()]);
} }
} }
buffer buffer
} }
/// Strips ANSI escape sequences and substitutes terminal-active control bytes
/// and visual-spoofing Unicode codepoints (bidi, zero-width) with U+FFFD.
pub fn sanitize(line: &str) -> String {
let stripped = strip_ansi(line);
let mut buffer = String::with_capacity(stripped.len());
let bytes = stripped.as_bytes();
let mut start = 0;
let mut i = 0;
// Skip directly to the next trigger byte instead of testing each one.
while let Some(off) = bytes[i..].iter().position(|&b| is_sanitize_trigger(b)) {
i += off;
let len = sanitize_at(bytes, i, &stripped, &mut buffer, &mut start);
i += len;
}
buffer.push_str(&stripped[start..]);
buffer
}
#[inline]
fn is_sanitize_trigger(b: u8) -> bool {
// C0 controls minus \t \n \f; DEL; UTF-8 leads with dangerous codepoints.
matches!(b, 0x00..=0x08 | 0x0B | 0x0D..=0x1F | 0x7F | 0xC2 | 0xD8 | 0xE2 | 0xEF)
}
/// Substitutes the byte/sequence at `bytes[i]` (or passes it through on
/// false-alarm trigger), flushing the prefix from `start`. Returns bytes consumed.
fn sanitize_at(
bytes: &[u8],
i: usize,
full: &str,
buffer: &mut String,
start: &mut usize,
) -> usize {
buffer.push_str(&full[*start..i]);
let consumed = match bytes[i] {
b'\r' if bytes.get(i + 1) == Some(&b'\n') => {
buffer.push_str("\r\n");
2
}
// 0xC2 leads U+0080..U+00FF; filter the C1 range.
0xC2 if matches!(bytes.get(i + 1), Some(0x80..=0x9F)) => {
buffer.push('\u{FFFD}');
2
}
0xE2 if is_dangerous_e2(bytes, i) => {
buffer.push('\u{FFFD}');
3
}
// 0xEF 0xBB 0xBF = U+FEFF (BOM / zero-width no-break space).
0xEF if bytes.get(i + 1) == Some(&0xBB) && bytes.get(i + 2) == Some(&0xBF) => {
buffer.push('\u{FFFD}');
3
}
// 0xD8 0x9C = U+061C (Arabic letter mark, a bidi control). The rest of
// the 0xD8 block is ordinary Arabic text.
0xD8 if bytes.get(i + 1) == Some(&0x9C) => {
buffer.push('\u{FFFD}');
2
}
// False-alarm trigger: pass the full UTF-8 sequence through.
lead @ (0xC2 | 0xD8 | 0xE2 | 0xEF) => {
let n = utf8_len_from_lead(lead);
buffer.push_str(&full[i..i + n]);
n
}
_ => {
buffer.push('\u{FFFD}');
1
}
};
*start = i + consumed;
consumed
}
#[inline]
fn is_dangerous_e2(bytes: &[u8], i: usize) -> bool {
// U+200B..D (zero-width), U+200E..F (LRM/RLM), U+202A..E (bidi embedding
// and override), U+2066..9 (bidi isolates).
matches!(
(bytes.get(i + 1), bytes.get(i + 2)),
(Some(0x80), Some(0x8B..=0x8F | 0xAA..=0xAE)) | (Some(0x81), Some(0xA6..=0xA9))
)
}
#[inline]
fn utf8_len_from_lead(lead: u8) -> usize {
if lead < 0x80 {
1
} else if lead < 0xE0 {
2
} else if lead < 0xF0 {
3
} else {
4
}
}
/// Escape C0, DEL, and C1 control characters so a string from an untrusted /// Escape C0, DEL, and C1 control characters so a string from an untrusted
/// filename or path can be safely written to the terminal. /// filename or path can be safely written to the terminal.
pub fn sanitize_for_terminal(input: &str) -> String { pub fn sanitize_for_terminal(input: &str) -> String {
@@ -365,113 +270,6 @@ fn test_strip_ansi() {
); );
} }
#[test]
fn test_strip_ansi_8bit_c1_introducers() {
assert_eq!(strip_ansi("a\u{9B}31mRED\u{9B}0mb"), "aREDb");
assert_eq!(strip_ansi("a\x1bP1;0|payload\x1b\\b"), "ab");
assert_eq!(strip_ansi("a\u{90}body\u{9C}b"), "ab");
}
#[test]
fn test_strip_ansi_single_char_esc() {
// strip_ansi must consume both bytes of single-byte ESC sequences (RIS, DECSC, keypad, VT52).
assert_eq!(strip_ansi("a\x1bcb"), "ab");
assert_eq!(strip_ansi("a\x1b7b\x1b8c"), "abc");
assert_eq!(strip_ansi("a\x1b=b\x1b>c"), "abc");
assert_eq!(strip_ansi("a\x1bZb"), "ab");
}
#[test]
fn test_strip_ansi_preserves_control_bytes() {
// strip_ansi removes only ANSI escape sequences; control bytes pass through.
assert_eq!(strip_ansi("safe\rEVIL"), "safe\rEVIL");
assert_eq!(strip_ansi("a\x08b\x07c\x0E\x0Fd"), "a\x08b\x07c\x0E\x0Fd");
}
#[test]
fn test_sanitize_substitutes_dangerous_bytes() {
let r = '\u{FFFD}';
assert_eq!(sanitize("safe\rEVIL"), format!("safe{r}EVIL"));
assert_eq!(sanitize("a\x08b"), format!("a{r}b"));
assert_eq!(sanitize("a\x07b"), format!("a{r}b"));
assert_eq!(sanitize("a\x0Bb"), format!("a{r}b"));
assert_eq!(sanitize("a\x0Eb\x0Fc"), format!("a{r}b{r}c"));
assert_eq!(sanitize("a\u{8D}b"), format!("a{r}b"));
assert_eq!(sanitize("a\u{85}b"), format!("a{r}b"));
assert_eq!(sanitize("trailing\r"), format!("trailing{r}"));
assert_eq!(sanitize("a\x7Fb"), format!("a{r}b"));
}
#[test]
fn test_sanitize_substitutes_bidi_and_zero_width() {
let r = '\u{FFFD}';
// Trojan-Source bidi formatting: U+202A..U+202E
assert_eq!(sanitize("a\u{202A}b"), format!("a{r}b"));
assert_eq!(sanitize("a\u{202E}b"), format!("a{r}b"));
// Bidi isolates: U+2066..U+2069
assert_eq!(sanitize("a\u{2066}b"), format!("a{r}b"));
assert_eq!(sanitize("a\u{2069}b"), format!("a{r}b"));
// Zero-width: U+200B..U+200D
assert_eq!(sanitize("a\u{200B}b"), format!("a{r}b"));
assert_eq!(sanitize("a\u{200D}b"), format!("a{r}b"));
// BOM in middle of file: U+FEFF
assert_eq!(sanitize("a\u{FEFF}b"), format!("a{r}b"));
}
#[test]
fn test_sanitize_substitutes_every_bidi_control() {
// Unicode Bidi_Control is exactly these 12 codepoints. Covering only some
// of them leaves a reordering attack available through the rest.
let r = '\u{FFFD}';
for c in [
'\u{061C}', '\u{200E}', '\u{200F}', '\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}',
'\u{202E}', '\u{2066}', '\u{2067}', '\u{2068}', '\u{2069}',
] {
assert_eq!(
sanitize(&format!("a{c}b")),
format!("a{r}b"),
"U+{:04X} was not substituted",
c as u32
);
}
}
#[test]
fn test_sanitize_preserves_arabic_sharing_the_alm_lead_byte() {
// U+061C is reached via lead byte 0xD8, which also leads ordinary Arabic.
assert_eq!(
sanitize("\u{0600}\u{061F}\u{06FF}"),
"\u{0600}\u{061F}\u{06FF}"
);
assert_eq!(sanitize("مرحبا بالعالم"), "مرحبا بالعالم");
}
#[test]
fn test_sanitize_preserves_legitimate_bytes() {
assert_eq!(sanitize("crlf\r\nline\r\n"), "crlf\r\nline\r\n");
assert_eq!(sanitize("a\tb\nc"), "a\tb\nc");
assert_eq!(sanitize("plain ascii"), "plain ascii");
assert_eq!(sanitize("üñíçödé"), "üñíçödé");
// FF (U+000C) passes through; section separator in C source / Emacs Lisp.
assert_eq!(sanitize("section1\x0Csection2"), "section1\x0Csection2");
// Common Unicode that shares a UTF-8 lead byte with dangerous codepoints
// must pass through unchanged.
assert_eq!(sanitize("snowman ☃ moon ☾"), "snowman ☃ moon ☾");
assert_eq!(sanitize("emoji 🎉 ☃"), "emoji 🎉 ☃");
assert_eq!(sanitize("0xC2 lead: ÿ ñ ç"), "0xC2 lead: ÿ ñ ç");
assert_eq!(sanitize("CJK 漢字 emoji 🦀"), "CJK 漢字 emoji 🦀");
}
#[test]
fn test_sanitize_strips_ansi() {
let r = '\u{FFFD}';
// sanitize is a strict superset of strip_ansi.
assert_eq!(sanitize("a\x1B[31mb\x1B[0mc"), "abc");
assert_eq!(sanitize("a\u{9B}31mb\u{9B}0mc"), "abc");
// ANSI then dangerous byte: ANSI gone, byte substituted.
assert_eq!(sanitize("\x1B[31mhello\rEVIL"), format!("hello{r}EVIL"));
}
#[test] #[test]
fn test_strip_overstrike() { fn test_strip_overstrike() {
// Bold: X\x08X (same char repeated) // Bold: X\x08X (same char repeated)
-9
View File
@@ -192,15 +192,6 @@ impl<'a> PrettyPrinter<'a> {
self self
} }
/// Whether to sanitize untrusted input for safe display (default: never)
///
/// Strips ANSI escape sequences and additionally substitutes terminal-active
/// control bytes and bidi / zero-width codepoints with U+FFFD.
pub fn sanitize(&mut self, mode: StripAnsiMode) -> &mut Self {
self.config.sanitize = mode;
self
}
/// Text wrapping mode (default: do not wrap) /// Text wrapping mode (default: do not wrap)
pub fn wrapping_mode(&mut self, mode: WrappingMode) -> &mut Self { pub fn wrapping_mode(&mut self, mode: WrappingMode) -> &mut Self {
self.config.wrapping_mode = mode; self.config.wrapping_mode = mode;
+4 -19
View File
@@ -30,8 +30,7 @@ use crate::input::OpenedInput;
use crate::line_range::{MaxBufferedLineNumber, RangeCheckResult}; use crate::line_range::{MaxBufferedLineNumber, RangeCheckResult};
use crate::output::OutputHandle; use crate::output::OutputHandle;
use crate::preprocessor::{ use crate::preprocessor::{
expand_tabs, replace_nonprintable, sanitize, sanitize_for_terminal, strip_ansi, expand_tabs, replace_nonprintable, sanitize_for_terminal, strip_ansi, strip_overstrike,
strip_overstrike,
}; };
use crate::style::StyleComponent; use crate::style::StyleComponent;
use crate::terminal::{as_terminal_escaped, to_ansi_color}; use crate::terminal::{as_terminal_escaped, to_ansi_color};
@@ -211,7 +210,6 @@ pub(crate) struct InteractivePrinter<'a> {
background_color_highlight: Option<Color>, background_color_highlight: Option<Color>,
consecutive_empty_lines: usize, consecutive_empty_lines: usize,
strip_ansi: bool, strip_ansi: bool,
sanitize: bool,
strip_overstrike: bool, strip_overstrike: bool,
} }
@@ -275,9 +273,7 @@ impl<'a> InteractivePrinter<'a> {
let needs_to_match_syntax = (!is_printing_binary let needs_to_match_syntax = (!is_printing_binary
|| matches!(config.binary, BinaryBehavior::AsText)) || matches!(config.binary, BinaryBehavior::AsText))
&& (config.colored_output && (config.colored_output || config.strip_ansi == StripAnsiMode::Auto);
|| config.strip_ansi == StripAnsiMode::Auto
|| config.sanitize == StripAnsiMode::Auto);
let (is_plain_text, strip_overstrike, highlighter_from_set) = if needs_to_match_syntax { let (is_plain_text, strip_overstrike, highlighter_from_set) = if needs_to_match_syntax {
// Determine the type of syntax for highlighting // Determine the type of syntax for highlighting
@@ -323,14 +319,6 @@ impl<'a> InteractivePrinter<'a> {
_ => false, _ => false,
}; };
let sanitize = match config.sanitize {
_ if config.show_nonprintable => false,
StripAnsiMode::Always => true,
StripAnsiMode::Auto if is_plain_text => false,
StripAnsiMode::Auto => true,
_ => false,
};
Ok(InteractivePrinter { Ok(InteractivePrinter {
panel_width, panel_width,
colors, colors,
@@ -344,7 +332,6 @@ impl<'a> InteractivePrinter<'a> {
background_color_highlight, background_color_highlight,
consecutive_empty_lines: 0, consecutive_empty_lines: 0,
strip_ansi, strip_ansi,
sanitize,
strip_overstrike, strip_overstrike,
}) })
} }
@@ -688,10 +675,8 @@ impl Printer for InteractivePrinter<'_> {
} }
} }
// Sanitize is the strict superset; otherwise strip-ansi alone. // If ANSI escape sequences are supposed to be stripped, do it before syntax highlighting.
if self.sanitize { if self.strip_ansi {
line = sanitize(&line).into()
} else if self.strip_ansi {
line = strip_ansi(&line).into() line = strip_ansi(&line).into()
} }
+12 -202
View File
@@ -386,23 +386,12 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
} }
fn next_text(&mut self) -> Option<EscapeSequenceOffsets> { fn next_text(&mut self) -> Option<EscapeSequenceOffsets> {
self.chars_take_while(|c| !is_sequence_introducer(c)) self.chars_take_while(|c| c != '\x1B')
.map(|(start, end)| EscapeSequenceOffsets::Text { start, end }) .map(|(start, end)| EscapeSequenceOffsets::Text { start, end })
} }
fn next_sequence(&mut self) -> Option<EscapeSequenceOffsets> { fn next_sequence(&mut self) -> Option<EscapeSequenceOffsets> {
let (start_sequence, c) = self.chars.next().expect("to not be finished"); let (start_sequence, c) = self.chars.next().expect("to not be finished");
// Handle 8-bit C1 introducers as their 7-bit `ESC <x>` equivalents.
match c {
'\u{9B}' => return self.next_csi_body(start_sequence),
'\u{9D}' => return self.next_osc_body(start_sequence),
'\u{90}' | '\u{98}' | '\u{9E}' | '\u{9F}' => {
return self.next_string_terminated_body(start_sequence)
}
_ => {}
}
match self.chars.peek() { match self.chars.peek() {
None => Some(EscapeSequenceOffsets::Unknown { None => Some(EscapeSequenceOffsets::Unknown {
start: start_sequence, start: start_sequence,
@@ -411,24 +400,12 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
Some((_, ']')) => self.next_osc(start_sequence), Some((_, ']')) => self.next_osc(start_sequence),
Some((_, '[')) => self.next_csi(start_sequence), Some((_, '[')) => self.next_csi(start_sequence),
// 7-bit DCS/SOS/PM/APC: `ESC P/X/^/_` introduces a string body. Some((i, c)) => match c {
Some((_, 'P' | 'X' | '^' | '_')) => {
self.chars.next();
self.next_string_terminated_body(start_sequence)
}
Some(&(i, c)) => match c {
'\x20'..='\x2F' => self.next_nf(start_sequence), '\x20'..='\x2F' => self.next_nf(start_sequence),
c => { c => Some(EscapeSequenceOffsets::Unknown {
// Single-byte ESC sequence (RIS, DECSC/DECRC, keypad, VT52 etc.).
let end = match self.chars.next() {
Some((j, fc)) => j + fc.len_utf8(),
None => i + c.len_utf8(),
};
Some(EscapeSequenceOffsets::Unknown {
start: start_sequence, start: start_sequence,
end, end: i + c.len_utf8(),
}) }),
}
}, },
} }
} }
@@ -436,27 +413,12 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
fn next_osc(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> { fn next_osc(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
let (osc_open_index, osc_open_char) = self.chars.next().expect("to not be finished"); let (osc_open_index, osc_open_char) = self.chars.next().expect("to not be finished");
debug_assert_eq!(osc_open_char, ']'); debug_assert_eq!(osc_open_char, ']');
let start_command = osc_open_index + osc_open_char.len_utf8();
Some(self.read_osc_body(start_sequence, start_command))
}
/// OSC body parser entered after the 8-bit introducer U+009D was consumed.
fn next_osc_body(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
let start_command = start_sequence + '\u{9D}'.len_utf8();
Some(self.read_osc_body(start_sequence, start_command))
}
fn read_osc_body(
&mut self,
start_sequence: usize,
start_command: usize,
) -> EscapeSequenceOffsets {
let mut start_terminator: usize; let mut start_terminator: usize;
let mut end_sequence: usize; let mut end_sequence: usize;
loop { loop {
// ST is BEL, ESC `\\`, or U+009C. match self.chars_take_while(|c| !matches!(c, '\x07' | '\x1B')) {
match self.chars_take_while(|c| !matches!(c, '\x07' | '\x1B' | '\u{9C}')) {
None => { None => {
start_terminator = self.text.len(); start_terminator = self.text.len();
end_sequence = start_terminator; end_sequence = start_terminator;
@@ -475,11 +437,6 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
break; break;
} }
Some((ti, '\u{9C}')) => {
end_sequence = ti + '\u{9C}'.len_utf8();
break;
}
Some((ti, '\x1B')) => { Some((ti, '\x1B')) => {
match self.chars.next() { match self.chars.next() {
Some((i, '\\')) => { Some((i, '\\')) => {
@@ -509,82 +466,20 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
} }
} }
EscapeSequenceOffsets::OSC { Some(EscapeSequenceOffsets::OSC {
start_sequence, start_sequence,
start_command, start_command: osc_open_index + osc_open_char.len_utf8(),
start_terminator, start_terminator,
end: end_sequence, end: end_sequence,
}
}
/// DCS/SOS/PM/APC body parser. Emitted as `Unknown` so the body is stripped.
fn next_string_terminated_body(
&mut self,
start_sequence: usize,
) -> Option<EscapeSequenceOffsets> {
let mut end_sequence: usize;
loop {
match self.chars_take_while(|c| !matches!(c, '\x07' | '\x1B' | '\u{9C}')) {
None => {
end_sequence = self.text.len();
break;
}
Some((_, end)) => {
end_sequence = end;
}
}
match self.chars.next() {
Some((ti, '\x07')) => {
end_sequence = ti + '\x07'.len_utf8();
break;
}
Some((ti, '\u{9C}')) => {
end_sequence = ti + '\u{9C}'.len_utf8();
break;
}
Some((ti, '\x1B')) => match self.chars.next() {
Some((i, '\\')) => {
end_sequence = i + '\\'.len_utf8();
break;
}
None => {
end_sequence = ti + '\x1B'.len_utf8();
break;
}
_ => {}
},
None => break,
Some((_, tc)) => {
panic!("this should not be reached: char {tc:?}")
}
}
}
Some(EscapeSequenceOffsets::Unknown {
start: start_sequence,
end: end_sequence,
}) })
} }
fn next_csi(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> { fn next_csi(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
let (csi_open_index, csi_open_char) = self.chars.next().expect("to not be finished"); let (csi_open_index, csi_open_char) = self.chars.next().expect("to not be finished");
debug_assert_eq!(csi_open_char, '['); debug_assert_eq!(csi_open_char, '[');
Some(self.read_csi_body(start_sequence, csi_open_index + csi_open_char.len_utf8()))
}
/// CSI body parser entered after the 8-bit introducer U+009B was consumed. let start_parameters: usize = csi_open_index + csi_open_char.len_utf8();
fn next_csi_body(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
let start_parameters = start_sequence + '\u{9B}'.len_utf8();
Some(self.read_csi_body(start_sequence, start_parameters))
}
fn read_csi_body(
&mut self,
start_sequence: usize,
start_parameters: usize,
) -> EscapeSequenceOffsets {
// Keep iterating while within the range of `0x30-0x3F`. // Keep iterating while within the range of `0x30-0x3F`.
let mut start_intermediates: usize = start_parameters; let mut start_intermediates: usize = start_parameters;
if let Some((_, end)) = self.chars_take_while(|c| matches!(c, '\x30'..='\x3F')) { if let Some((_, end)) = self.chars_take_while(|c| matches!(c, '\x30'..='\x3F')) {
@@ -603,13 +498,13 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
Some((i, c)) => i + c.len_utf8(), Some((i, c)) => i + c.len_utf8(),
}; };
EscapeSequenceOffsets::CSI { Some(EscapeSequenceOffsets::CSI {
start_sequence, start_sequence,
start_parameters, start_parameters,
start_intermediates, start_intermediates,
start_final_byte, start_final_byte,
end: end_of_sequence, end: end_of_sequence,
} })
} }
fn next_nf(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> { fn next_nf(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
@@ -648,25 +543,13 @@ impl Iterator for EscapeSequenceOffsetsIterator<'_> {
type Item = EscapeSequenceOffsets; type Item = EscapeSequenceOffsets;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
match self.chars.peek() { match self.chars.peek() {
Some((_, c)) if is_sequence_introducer(*c) => self.next_sequence(), Some((_, '\x1B')) => self.next_sequence(),
Some((_, _)) => self.next_text(), Some((_, _)) => self.next_text(),
None => None, None => None,
} }
} }
} }
/// True for ESC and the 8-bit C1 sequence introducers (DCS/SOS/CSI/OSC/PM/APC).
#[inline]
fn is_sequence_introducer(c: char) -> bool {
if (c as u32) < 0x80 {
return c == '\x1B';
}
matches!(
c,
'\u{90}' | '\u{98}' | '\u{9B}' | '\u{9D}' | '\u{9E}' | '\u{9F}'
)
}
/// An iterator over ANSI/VT escape sequences within a string. /// An iterator over ANSI/VT escape sequences within a string.
/// ///
/// ## Example /// ## Example
@@ -834,79 +717,6 @@ mod tests {
); );
} }
#[test]
fn test_escape_sequence_offsets_iterator_parses_8bit_csi() {
let mut iter = EscapeSequenceOffsetsIterator::new("\u{9B}31m");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::CSI {
start_sequence: 0,
start_parameters: 2,
start_intermediates: 4,
start_final_byte: 4,
end: 5,
})
);
assert_eq!(iter.next(), None);
}
#[test]
fn test_escape_sequence_offsets_iterator_parses_8bit_osc_with_8bit_st() {
let mut iter = EscapeSequenceOffsetsIterator::new("\u{9D}0;title\u{9C}");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::OSC {
start_sequence: 0,
start_command: 2,
start_terminator: 9,
end: 11,
})
);
assert_eq!(iter.next(), None);
}
#[test]
fn test_escape_sequence_offsets_iterator_parses_7bit_dcs_consumes_body() {
let mut iter = EscapeSequenceOffsetsIterator::new("\x1BP1;0;|payload\x1B\\rest");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::Unknown { start: 0, end: 16 })
);
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::Text { start: 16, end: 20 })
);
assert_eq!(iter.next(), None);
}
#[test]
fn test_escape_sequence_offsets_iterator_8bit_dcs_consumes_body() {
let mut iter = EscapeSequenceOffsetsIterator::new("\u{90}body\u{9C}rest");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::Unknown { start: 0, end: 8 })
);
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::Text { start: 8, end: 12 })
);
}
#[test]
fn test_escape_sequence_offsets_iterator_truncated_dcs_consumes_to_eof() {
// Unterminated DCS must still be consumed, not emitted as `Text`.
let input = "\x1BPno-terminator";
let mut iter = EscapeSequenceOffsetsIterator::new(input);
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::Unknown {
start: 0,
end: input.len(),
})
);
assert_eq!(iter.next(), None);
}
#[test] #[test]
fn test_escape_sequence_offsets_iterator_parses_csi() { fn test_escape_sequence_offsets_iterator_parses_csi() {
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[m"); let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[m");
+1 -159
View File
@@ -626,18 +626,12 @@ fn list_themes_to_piped_output() {
} }
#[test] #[test]
#[serial]
fn list_languages() { fn list_languages() {
mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| {
bat() bat()
.env("PAGER", mocked_pagers::from("echo pager-output"))
.arg("--list-languages") .arg("--list-languages")
.arg("--paging=never")
.assert() .assert()
.success() .success()
.stdout(predicate::str::contains("Rust").normalize()) .stdout(predicate::str::contains("Rust").normalize());
.stdout(predicate::str::contains("pager-output").not());
});
} }
#[test] #[test]
@@ -3850,158 +3844,6 @@ fn strip_ansi_auto_does_not_strip_ansi_when_plain_text_by_option() {
assert!(output.contains("\x1B[33mYellow")) assert!(output.contains("\x1B[33mYellow"))
} }
#[test]
fn sanitize_implies_strip_ansi() {
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--sanitize=always")
.write_stdin("\x1B[33mYellow\x1B[m")
.assert()
.success()
.stdout("Yellow");
}
#[test]
fn sanitize_strips_osc_clipboard_hijack() {
// OSC 52 sets the system clipboard. A file containing this would silently
// overwrite the user's clipboard if displayed unfiltered.
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--sanitize=always")
.write_stdin("safe\x1B]52;c;cm0=\x07payload")
.assert()
.success()
.stdout("safepayload");
}
#[test]
fn sanitize_strips_osc_8_hyperlink_spoof() {
// OSC 8 hyperlinks let displayed text point to an arbitrary URL.
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--sanitize=always")
.write_stdin("\x1B]8;;https://evil.example\x07click here\x1B]8;;\x07")
.assert()
.success()
.stdout("click here");
}
#[test]
fn sanitize_strips_window_title_injection() {
// OSC 0/1/2 set the terminal window title.
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--sanitize=always")
.write_stdin("hello\x1B]0;evil-title\x07world")
.assert()
.success()
.stdout("helloworld");
}
#[test]
fn sanitize_strips_8bit_csi() {
// 8-bit CSI introducer (U+009B) is the single-codepoint equivalent of ESC [.
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--sanitize=always")
.write_stdin("a\u{9B}31mRED\u{9B}0mb")
.assert()
.success()
.stdout("aREDb");
}
#[test]
fn sanitize_substitutes_bare_cr() {
// Bare CR (not part of CRLF) is the line-overwrite forgery vector.
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--sanitize=always")
.write_stdin("safe\rEVIL")
.assert()
.success()
.stdout("safe\u{FFFD}EVIL");
}
#[test]
fn sanitize_preserves_crlf() {
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--sanitize=always")
.write_stdin("line1\r\nline2\r\n")
.assert()
.success()
.stdout("line1\r\nline2\r\n");
}
#[test]
fn sanitize_substitutes_bidi_controls() {
// Trojan-Source attack (CVE-2021-42574): U+202E (RLO) reorders display.
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--sanitize=always")
.write_stdin("admin\u{202E}check")
.assert()
.success()
.stdout("admin\u{FFFD}check");
}
#[test]
fn sanitize_substitutes_zero_width() {
// Zero-width chars allow invisible content / identifier confusion.
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--sanitize=always")
.write_stdin("ad\u{200B}min")
.assert()
.success()
.stdout("ad\u{FFFD}min");
}
#[test]
fn sanitize_preserves_form_feed_in_source() {
// FF (U+000C) is used as a section separator in C source and Emacs Lisp.
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--sanitize=always")
.write_stdin("section1\x0Csection2")
.assert()
.success()
.stdout("section1\x0Csection2");
}
#[test]
fn sanitize_preserves_unicode_text() {
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--sanitize=always")
.write_stdin("snowman ☃ CJK 漢字 emoji 🦀")
.assert()
.success()
.stdout("snowman ☃ CJK 漢字 emoji 🦀");
}
// Tests that style components can be removed with `-component`. // Tests that style components can be removed with `-component`.
#[test] #[test]
fn style_components_can_be_removed() { fn style_components_can_be_removed() {