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

Compare commits

..

25 Commits

Author SHA1 Message Date
dependabot[bot] be1bd8b41b build(deps): bump assets/syntaxes/02_Extra/NSIS
Bumps [assets/syntaxes/02_Extra/NSIS](https://github.com/SublimeText/NSIS) from `619a65a` to `fe5aec8`.
- [Release notes](https://github.com/SublimeText/NSIS/releases)
- [Commits](https://github.com/SublimeText/NSIS/compare/619a65a04efbf343bdfcde875700675b9b273368...fe5aec8de320fd6c32c67cafd9911a3b521e7026)

---
updated-dependencies:
- dependency-name: assets/syntaxes/02_Extra/NSIS
  dependency-version: fe5aec8de320fd6c32c67cafd9911a3b521e7026
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-01 02:04:20 +00:00
Keith Hall 73dc3258be Merge pull request #3862 from lenamonj/fix/sanitize-bidi-control-set
Fix --sanitize passing through the bidi control characters U+200E, U+200F and U+061C
2026-07-30 00:27:20 +03:00
lenamonj df38225653 Add CHANGELOG entry for #3862 2026-07-29 17:02:33 -04:00
lenamonj c2cc79daa7 Fix --sanitize passing through the bidi control characters U+200E, U+200F and U+061C 2026-07-29 17:02:11 -04:00
Keith Hall 0230cba42d Merge pull request #3828 from cyphercodes/fix-list-languages-paging-never
Respect paging mode for --list-languages
2026-07-29 23:49:08 +03:00
Keith Hall 227e0fa00f Merge branch 'master' into fix-list-languages-paging-never 2026-07-29 23:33:51 +03:00
auto-merge-dependabot-prs[bot] 384f2f5d13 Merge pull request #3821 from sharkdp/dependabot/cargo/regex-1.12.4
build(deps): bump regex from 1.12.3 to 1.13.1
2026-07-29 20:23:23 +00:00
dependabot[bot] 060ccb1385 build(deps): bump regex from 1.12.3 to 1.13.1
Bumps [regex](https://github.com/rust-lang/regex) from 1.12.3 to 1.13.1.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.12.3...1.13.1)

---
updated-dependencies:
- dependency-name: regex
  dependency-version: 1.12.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 20:09:58 +00:00
auto-merge-dependabot-prs[bot] feabb25209 Merge pull request #3823 from sharkdp/dependabot/cargo/serde_with-3.21.0
build(deps): bump serde_with from 3.19.0 to 3.21.0
2026-07-29 20:06:40 +00:00
dependabot[bot] 839f2300b7 build(deps): bump serde_with from 3.19.0 to 3.21.0
Bumps [serde_with](https://github.com/jonasbb/serde_with) from 3.19.0 to 3.21.0.
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](https://github.com/jonasbb/serde_with/compare/v3.19.0...v3.21.0)

---
updated-dependencies:
- dependency-name: serde_with
  dependency-version: 3.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 19:46:45 +00:00
auto-merge-dependabot-prs[bot] ea0742babc Merge pull request #3820 from sharkdp/dependabot/cargo/quote-1.0.46
build(deps): bump quote from 1.0.45 to 1.0.47
2026-07-29 19:40:36 +00:00
dependabot[bot] bfa6e59978 build(deps): bump quote from 1.0.45 to 1.0.47
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.45 to 1.0.47.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.45...1.0.47)

---
updated-dependencies:
- dependency-name: quote
  dependency-version: 1.0.46
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 19:17:50 +00:00
auto-merge-dependabot-prs[bot] 9710a231d8 Merge pull request #3819 from sharkdp/dependabot/cargo/bytesize-2.4.0
build(deps): bump bytesize from 2.3.1 to 2.4.2
2026-07-29 19:14:35 +00:00
dependabot[bot] a9b67b2d99 build(deps): bump bytesize from 2.3.1 to 2.4.2
Bumps [bytesize](https://github.com/bytesize-rs/bytesize) from 2.3.1 to 2.4.2.
- [Release notes](https://github.com/bytesize-rs/bytesize/releases)
- [Changelog](https://github.com/bytesize-rs/bytesize/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bytesize-rs/bytesize/compare/bytesize-v2.3.1...bytesize-v2.4.2)

---
updated-dependencies:
- dependency-name: bytesize
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 18:54:55 +00:00
auto-merge-dependabot-prs[bot] b22be63fec Merge pull request #3822 from sharkdp/dependabot/cargo/unicode-segmentation-1.13.3
build(deps): bump unicode-segmentation from 1.13.2 to 1.13.3
2026-07-29 18:50:14 +00:00
Keith Hall 3a7a750969 Merge branch 'master' into fix-list-languages-paging-never 2026-07-29 21:25:10 +03:00
dependabot[bot] be5343c742 build(deps): bump unicode-segmentation from 1.13.2 to 1.13.3
Bumps [unicode-segmentation](https://github.com/unicode-rs/unicode-segmentation) from 1.13.2 to 1.13.3.
- [Commits](https://github.com/unicode-rs/unicode-segmentation/commits)

---
updated-dependencies:
- dependency-name: unicode-segmentation
  dependency-version: 1.13.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 18:21:51 +00:00
Keith Hall c371044276 Merge pull request #3861 from TyceHerrman/fix/cargo-audit-2026-07
chore(deps): resolve current cargo audit failures
2026-07-29 21:18:39 +03:00
Tyce Herrman d449ce5b7e chore(deps): resolve current cargo audit failures 2026-07-29 12:10:05 -04:00
cyphercodes 218afc30ac Respect paging mode for list languages 2026-07-04 06:31:26 +03:00
Keith Hall 78951393e2 Merge pull request #3729 from curious-rabbit/sanitize
add --sanitize option
2026-07-01 23:15:58 +03:00
curious-rabbit 0a52e4321f improve patch 2026-07-01 07:54:51 +02:00
curious-rabbit d72163160e add sanitize option 2026-07-01 07:09:49 +02:00
auto-merge-dependabot-prs[bot] 971f9679da Merge pull request #3817 from sharkdp/dependabot/submodules/assets/syntaxes/02_Extra/LESS-d076a4d
build(deps): bump assets/syntaxes/02_Extra/LESS from `836b47e` to `d076a4d`
2026-07-01 02:16:46 +00:00
dependabot[bot] 57d436861a build(deps): bump assets/syntaxes/02_Extra/LESS
Bumps [assets/syntaxes/02_Extra/LESS](https://github.com/danro/LESS-sublime) from `836b47e` to `d076a4d`.
- [Release notes](https://github.com/danro/LESS-sublime/releases)
- [Commits](https://github.com/danro/LESS-sublime/compare/836b47ec61a9c6a6445b4007e8353337fe63e2c9...d076a4dd416bcdb155bacf90cda72f7bf1c8abbb)

---
updated-dependencies:
- dependency-name: assets/syntaxes/02_Extra/LESS
  dependency-version: d076a4dd416bcdb155bacf90cda72f7bf1c8abbb
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-01 02:03:33 +00:00
21 changed files with 722 additions and 74 deletions
+9 -9
View File
@@ -35,7 +35,7 @@ jobs:
name: Extract crate metadata
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Extract crate information
id: crate_metadata
run: |
@@ -58,7 +58,7 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt,clippy
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- run: cargo fmt -- --check
- run: cargo clippy --locked --all-targets --all-features -- -D warnings
@@ -68,7 +68,7 @@ jobs:
needs: crate_metadata
steps:
- name: Checkout source code
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Install rust toolchain (v${{ needs.crate_metadata.outputs.msrv }})
uses: dtolnay/rust-toolchain@master
with:
@@ -80,7 +80,7 @@ jobs:
name: License checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
submodules: true # we especially want to perform license checks on submodules
- run: tests/scripts/license-checks.sh
@@ -90,7 +90,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Git checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
submodules: true # we need all syntax and theme submodules
- name: Install Rust toolchain
@@ -119,7 +119,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Git checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Prepare environment variables
run: |
echo "BAT_SYSTEM_CONFIG_PREFIX=$GITHUB_WORKSPACE/tests/examples/system_config" >> $GITHUB_ENV
@@ -135,7 +135,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Git checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Check documentation
@@ -150,7 +150,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- run: cargo install cargo-audit --locked
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- run: cargo audit
build:
@@ -178,7 +178,7 @@ jobs:
BUILD_CMD: cargo
steps:
- name: Checkout source code
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Install prerequisites
shell: bash
@@ -13,7 +13,7 @@ jobs:
PR_NUMBER: ${{ github.event.number }}
PR_BASE: ${{ github.base_ref }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Fetch PR base
run: git fetch --no-tags --prune --depth=1 origin
+5
View File
@@ -3,11 +3,13 @@
## 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 .NET slnx extension, see #3682 (@ltrzesniewski)
## 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)
- 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)
@@ -22,6 +24,9 @@
- Syntax highlighting for Python files using uv as script runner in shebang #3689 (@janlarres)
## 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 `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)
Generated
+24 -24
View File
@@ -84,9 +84,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.102"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
[[package]]
name = "arc-swap"
@@ -266,9 +266,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "bytesize"
version = "2.3.1"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3"
checksum = "3d7c8918969267b2932ffd5655509bbbea0833823058c378876953217f5fc50e"
[[package]]
name = "cc"
@@ -420,9 +420,9 @@ dependencies = [
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
version = "0.9.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
dependencies = [
"crossbeam-utils",
]
@@ -2016,9 +2016,9 @@ checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
name = "plist"
version = "1.9.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1"
checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85"
dependencies = [
"base64",
"indexmap",
@@ -2130,18 +2130,18 @@ dependencies = [
[[package]]
name = "quick-xml"
version = "0.39.4"
version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e"
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
dependencies = [
"memchr",
]
[[package]]
name = "quote"
version = "1.0.45"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
@@ -2183,9 +2183,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.12.3"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
dependencies = [
"aho-corasick",
"memchr",
@@ -2195,9 +2195,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.14"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
dependencies = [
"aho-corasick",
"memchr",
@@ -2206,9 +2206,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
version = "0.8.10"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "rgb"
@@ -2328,9 +2328,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.19.0"
version = "3.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05839ce67618e14a09b286535c0d9c94e85ef25469b0e13cb4f844e5593eb19"
checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c"
dependencies = [
"serde_core",
"serde_with_macros",
@@ -2338,9 +2338,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.19.0"
version = "3.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf2ebbe86054f9b45bc3881e865683ccfaccce97b9b4cb53f3039d67f355a334"
checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660"
dependencies = [
"darling",
"proc-macro2",
@@ -2744,9 +2744,9 @@ dependencies = [
[[package]]
name = "unicode-segmentation"
version = "1.13.2"
version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
name = "unicode-width"
+4
View File
@@ -843,6 +843,10 @@ 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
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]
> The `auto` option of `--strip-ansi` avoids removing escape sequences when the syntax is plain text.
+6
View File
@@ -100,6 +100,11 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script
ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_)}
break
}
'*;--sanitize' {
$ArrayWhen |
ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_)}
break
}
'*;--strip-ansi' {
$ArrayWhen |
ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_)}
@@ -158,6 +163,7 @@ 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('--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('--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('-p' , 'p' , [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,6 +132,10 @@ _bat() {
COMPREPLY=($(compgen -W "auto never always" -- "$cur"))
return 0
;;
--sanitize)
COMPREPLY=($(compgen -W "auto never always" -- "$cur"))
return 0
;;
--completion)
COMPREPLY=($(compgen -W "bash fish zsh ps1" -- "$cur"))
return 0
@@ -221,6 +225,7 @@ _bat() {
--list-themes
--squeeze-blank
--squeeze-limit
--sanitize
--strip-ansi
--style
--line-range
+1
View File
@@ -221,6 +221,7 @@ 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 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
+1
View File
@@ -54,6 +54,7 @@ _{{PROJECT_EXECUTABLE}}_main() {
--squeeze-blank'[squeeze consecutive empty lines into a single empty line]'
--squeeze-limit='[set the maximum number of consecutive empty lines]:limit:'
--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]"
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'
+8
View File
@@ -164,6 +164,14 @@ Options:
escape sequences unless the syntax highlighting language is plain text. Possible values:
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>
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
+34 -9
View File
@@ -38,6 +38,15 @@ pub fn env_no_color() -> bool {
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 {
Short,
Long,
@@ -458,16 +467,32 @@ impl App {
4
},
),
strip_ansi: match self
.matches
.get_one::<String>("strip-ansi")
.map(|s| s.as_str())
{
Some("never") => StripAnsiMode::Never,
Some("always") => StripAnsiMode::Always,
Some("auto") => StripAnsiMode::Auto,
_ => unreachable!("other values for --strip-ansi are not allowed"),
strip_ansi: {
let sanitize = parse_strip_ansi_value(
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")
.map(|s| s.as_str()),
"--strip-ansi",
);
// --sanitize implies --strip-ansi to the same value.
if sanitize != StripAnsiMode::Never {
sanitize
} 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"),
unbuffered: self.matches.get_flag("unbuffered"),
theme: theme(self.theme_options()).to_string(),
+18
View File
@@ -480,6 +480,24 @@ pub fn build_app(interactive_output: bool) -> Command {
language is plain text. Possible values: auto, always, *never*.")
.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::new("style")
.long("style")
+3 -5
View File
@@ -427,11 +427,9 @@ fn run() -> Result<bool> {
if app.matches.get_flag("list-languages") {
let languages: String = get_languages(&config, cache_dir)?;
let inputs: Vec<Input> = vec![Input::from_reader(Box::new(languages.as_bytes()))];
let plain_config = Config {
style_components: StyleComponents::new(StyleComponent::Plain.components(false)),
paging_mode: PagingMode::QuitIfOneScreen,
..Default::default()
};
let mut plain_config = config.clone();
plain_config.style_components =
StyleComponents::new(StyleComponent::Plain.components(false));
run_controller(inputs, &plain_config, cache_dir)
} else if app.matches.get_flag("list-themes") {
list_themes(&config, config_dir, cache_dir, app.theme_options())?;
+3
View File
@@ -111,6 +111,9 @@ pub struct Config<'a> {
// Whether or not to strip ANSI escape codes from the input
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
pub quiet_empty: bool,
+204 -2
View File
@@ -139,16 +139,111 @@ pub fn replace_nonprintable(
/// Strips ANSI escape sequences from the input.
pub fn strip_ansi(line: &str) -> String {
let mut buffer = String::with_capacity(line.len());
for seq in EscapeSequenceOffsetsIterator::new(line) {
if let EscapeSequenceOffsets::Text { .. } = seq {
buffer.push_str(&line[seq.index_of_start()..seq.index_past_end()]);
}
}
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
/// filename or path can be safely written to the terminal.
pub fn sanitize_for_terminal(input: &str) -> String {
@@ -270,6 +365,113 @@ 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]
fn test_strip_overstrike() {
// Bold: X\x08X (same char repeated)
+9
View File
@@ -192,6 +192,15 @@ impl<'a> PrettyPrinter<'a> {
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)
pub fn wrapping_mode(&mut self, mode: WrappingMode) -> &mut Self {
self.config.wrapping_mode = mode;
+19 -4
View File
@@ -30,7 +30,8 @@ use crate::input::OpenedInput;
use crate::line_range::{MaxBufferedLineNumber, RangeCheckResult};
use crate::output::OutputHandle;
use crate::preprocessor::{
expand_tabs, replace_nonprintable, sanitize_for_terminal, strip_ansi, strip_overstrike,
expand_tabs, replace_nonprintable, sanitize, sanitize_for_terminal, strip_ansi,
strip_overstrike,
};
use crate::style::StyleComponent;
use crate::terminal::{as_terminal_escaped, to_ansi_color};
@@ -210,6 +211,7 @@ pub(crate) struct InteractivePrinter<'a> {
background_color_highlight: Option<Color>,
consecutive_empty_lines: usize,
strip_ansi: bool,
sanitize: bool,
strip_overstrike: bool,
}
@@ -273,7 +275,9 @@ impl<'a> InteractivePrinter<'a> {
let needs_to_match_syntax = (!is_printing_binary
|| matches!(config.binary, BinaryBehavior::AsText))
&& (config.colored_output || config.strip_ansi == StripAnsiMode::Auto);
&& (config.colored_output
|| config.strip_ansi == StripAnsiMode::Auto
|| config.sanitize == StripAnsiMode::Auto);
let (is_plain_text, strip_overstrike, highlighter_from_set) = if needs_to_match_syntax {
// Determine the type of syntax for highlighting
@@ -319,6 +323,14 @@ impl<'a> InteractivePrinter<'a> {
_ => 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 {
panel_width,
colors,
@@ -332,6 +344,7 @@ impl<'a> InteractivePrinter<'a> {
background_color_highlight,
consecutive_empty_lines: 0,
strip_ansi,
sanitize,
strip_overstrike,
})
}
@@ -675,8 +688,10 @@ impl Printer for InteractivePrinter<'_> {
}
}
// If ANSI escape sequences are supposed to be stripped, do it before syntax highlighting.
if self.strip_ansi {
// Sanitize is the strict superset; otherwise strip-ansi alone.
if self.sanitize {
line = sanitize(&line).into()
} else if self.strip_ansi {
line = strip_ansi(&line).into()
}
+203 -13
View File
@@ -386,12 +386,23 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
}
fn next_text(&mut self) -> Option<EscapeSequenceOffsets> {
self.chars_take_while(|c| c != '\x1B')
self.chars_take_while(|c| !is_sequence_introducer(c))
.map(|(start, end)| EscapeSequenceOffsets::Text { start, end })
}
fn next_sequence(&mut self) -> Option<EscapeSequenceOffsets> {
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() {
None => Some(EscapeSequenceOffsets::Unknown {
start: start_sequence,
@@ -400,12 +411,24 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
Some((_, ']')) => self.next_osc(start_sequence),
Some((_, '[')) => self.next_csi(start_sequence),
Some((i, c)) => match c {
// 7-bit DCS/SOS/PM/APC: `ESC P/X/^/_` introduces a string body.
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),
c => Some(EscapeSequenceOffsets::Unknown {
start: start_sequence,
end: i + c.len_utf8(),
}),
c => {
// 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,
end,
})
}
},
}
}
@@ -413,12 +436,27 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
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");
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 end_sequence: usize;
loop {
match self.chars_take_while(|c| !matches!(c, '\x07' | '\x1B')) {
// ST is BEL, ESC `\\`, or U+009C.
match self.chars_take_while(|c| !matches!(c, '\x07' | '\x1B' | '\u{9C}')) {
None => {
start_terminator = self.text.len();
end_sequence = start_terminator;
@@ -437,6 +475,11 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
break;
}
Some((ti, '\u{9C}')) => {
end_sequence = ti + '\u{9C}'.len_utf8();
break;
}
Some((ti, '\x1B')) => {
match self.chars.next() {
Some((i, '\\')) => {
@@ -466,20 +509,82 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
}
}
Some(EscapeSequenceOffsets::OSC {
EscapeSequenceOffsets::OSC {
start_sequence,
start_command: osc_open_index + osc_open_char.len_utf8(),
start_command,
start_terminator,
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> {
let (csi_open_index, csi_open_char) = self.chars.next().expect("to not be finished");
debug_assert_eq!(csi_open_char, '[');
Some(self.read_csi_body(start_sequence, csi_open_index + csi_open_char.len_utf8()))
}
let start_parameters: usize = csi_open_index + csi_open_char.len_utf8();
/// CSI body parser entered after the 8-bit introducer U+009B was consumed.
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`.
let mut start_intermediates: usize = start_parameters;
if let Some((_, end)) = self.chars_take_while(|c| matches!(c, '\x30'..='\x3F')) {
@@ -498,13 +603,13 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
Some((i, c)) => i + c.len_utf8(),
};
Some(EscapeSequenceOffsets::CSI {
EscapeSequenceOffsets::CSI {
start_sequence,
start_parameters,
start_intermediates,
start_final_byte,
end: end_of_sequence,
})
}
}
fn next_nf(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
@@ -543,13 +648,25 @@ impl Iterator for EscapeSequenceOffsetsIterator<'_> {
type Item = EscapeSequenceOffsets;
fn next(&mut self) -> Option<Self::Item> {
match self.chars.peek() {
Some((_, '\x1B')) => self.next_sequence(),
Some((_, c)) if is_sequence_introducer(*c) => self.next_sequence(),
Some((_, _)) => self.next_text(),
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.
///
/// ## Example
@@ -717,6 +834,79 @@ 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]
fn test_escape_sequence_offsets_iterator_parses_csi() {
let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[m");
+163 -5
View File
@@ -626,12 +626,18 @@ fn list_themes_to_piped_output() {
}
#[test]
#[serial]
fn list_languages() {
bat()
.arg("--list-languages")
.assert()
.success()
.stdout(predicate::str::contains("Rust").normalize());
mocked_pagers::with_mocked_versions_of_more_and_most_in_path(|| {
bat()
.env("PAGER", mocked_pagers::from("echo pager-output"))
.arg("--list-languages")
.arg("--paging=never")
.assert()
.success()
.stdout(predicate::str::contains("Rust").normalize())
.stdout(predicate::str::contains("pager-output").not());
});
}
#[test]
@@ -3844,6 +3850,158 @@ fn strip_ansi_auto_does_not_strip_ansi_when_plain_text_by_option() {
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`.
#[test]
fn style_components_can_be_removed() {