diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..67a37e28 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,11 @@ +# On Windows MSVC, statically link the C runtime so that the resulting EXE does +# not depend on the vcruntime DLL. +# +# See: https://github.com/sharkdp/bat/issues/3634 + +[target.x86_64-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] +[target.i686-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] +[target.aarch64-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index f1bc3887..f881cada 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -161,12 +161,12 @@ jobs: fail-fast: false matrix: job: - - { target: aarch64-unknown-linux-musl , os: ubuntu-latest , dpkg_arch: arm64, use-cross: true } + - { target: aarch64-unknown-linux-musl , os: ubuntu-latest , dpkg_arch: musl-linux-arm64, use-cross: true } - { target: aarch64-unknown-linux-gnu , os: ubuntu-latest , dpkg_arch: arm64, use-cross: true } - { target: arm-unknown-linux-gnueabihf , os: ubuntu-latest , dpkg_arch: armhf, use-cross: true } - { target: arm-unknown-linux-musleabihf, os: ubuntu-latest , dpkg_arch: musl-linux-armhf, use-cross: true } - { target: i686-pc-windows-msvc , os: windows-2025 , } - - { target: i686-unknown-linux-gnu , os: ubuntu-latest , dpkg_arch: i686, use-cross: true } + - { target: i686-unknown-linux-gnu , os: ubuntu-latest , dpkg_arch: i386, use-cross: true } - { target: i686-unknown-linux-musl , os: ubuntu-latest , dpkg_arch: musl-linux-i686, use-cross: true } - { target: x86_64-apple-darwin , os: macos-15-intel, } - { target: aarch64-apple-darwin , os: macos-latest , } @@ -195,9 +195,7 @@ jobs: - name: Install cross if: matrix.job.use-cross - uses: taiki-e/install-action@v2 - with: - tool: cross + run: cargo install cross --git https://github.com/cross-rs/cross --rev 588b3c99db52b5a9c5906fab96cfadcf1bde7863 - name: Overwrite build command env variable if: matrix.job.use-cross @@ -335,7 +333,7 @@ jobs: DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }} DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }}-musl - case ${{ matrix.job.target }} in *-musl) DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }}-musl ; DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }} ;; esac; + case ${{ matrix.job.target }} in *-musl*) DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }}-musl ; DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }} ;; esac; DPKG_VERSION=${{ needs.crate_metadata.outputs.version }} DPKG_ARCH="${{ matrix.job.dpkg_arch }}" DPKG_NAME="${DPKG_BASENAME}_${DPKG_VERSION}_${DPKG_ARCH}.deb" diff --git a/.gitmodules b/.gitmodules index b64280c6..68c07c91 100644 --- a/.gitmodules +++ b/.gitmodules @@ -49,9 +49,6 @@ [submodule "assets/themes/zenburn"] path = assets/themes/zenburn url = https://github.com/colinta/zenburn.git -[submodule "assets/syntaxes/Kotlin"] - path = assets/syntaxes/02_Extra/Kotlin - url = https://github.com/vkostyukov/kotlin-sublime-package [submodule "assets/syntaxes/Elm"] path = assets/syntaxes/02_Extra/Elm url = https://github.com/elm-community/SublimeElmLanguageSupport @@ -281,3 +278,9 @@ [submodule "assets/syntaxes/02_Extra/COBOL"] path = assets/syntaxes/02_Extra/COBOL url = https://github.com/adukhan99/sublime_cobol.git +[submodule "assets/syntaxes/02_Extra/Kotlin"] + path = assets/syntaxes/02_Extra/Kotlin + url = https://github.com/guille/sublime-kotlin +[submodule "assets/syntaxes/02_Extra/Caddy"] + path = assets/syntaxes/02_Extra/Caddy + url = https://github.com/caddyserver/sublimetext.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 7de41716..7e0abc18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,44 @@ # unreleased -- Fixed bug caused by using `--plain` and `--terminal-width=N` flags simultaneously, see #3529 (@H4k1l) -- Fixed syntax tests path, see #3610 (@foxfromworld) + +## 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=` 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) - Add word wrapping mode via `--wrap=word`, see #3597 (@veeceey) +- Support configuring `--terminal-width` via `BAT_WIDTH`, see #3679 (@officialasishkumar) - Implement `--unbuffered` mode for streaming input, allowing partial lines to display immediately (e.g. `tail -f | bat -u`). Closes #3555, see #3583 (@mainnebula) - Added an initial `flake.nix` for a ready made development environment; see #3578 (@vorburger) - Add `--quiet-empty` (`-E`) flag to suppress output when input is empty. Closes #1936, see #3563 (@NORMAL-EX) - Improve native man pages and command help syntax highlighting by stripping overstriking, see #3517 (@akirk) - Add `--fallback-syntax`/`--fallback-language` to apply syntax highlighting only when auto-detection fails, see #1341 (@Xavrir) +- Map `BUILD` case sensitively to Python (Starlark) for Bazel, see #3576 (@vorburger) +- 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) +- Quote filenames before substituting them into `$LESSOPEN` / `$LESSCLOSE` templates, preventing shell injection when a filename contains shell metacharacters, see #3726 (@curious-rabbit) +- Fix `--list-themes` unconditionally probing the terminal via OSC 10/11 even when `--theme` was set to an explicit value, see #3700 (regression introduced in bc42149a). (@optimistiCli) +- Fix inverted `$LESSCLOSE` warning so bat warns on nonzero exit, not on success. See #3654 (@cuiweixie) +- Sanitize control characters in filenames before displaying them in the file header, error messages, and the terminal title, preventing ANSI escape injection via crafted filenames. Closes #3054, see #3691 (@curious-rabbit) +- Report initial input read errors instead of treating them as empty input. Closes #3002, see #3706 (@lawrence3699) +- Treat ZIP archives as binary content based on their magic header, see #3686 (@officialasishkumar) +- Fix i686 `.deb` package using incorrect architecture name (`i686` instead of `i386`), preventing installation on Debian. Closes #3611, see #3650 (@Sim-hu) +- Fix inconsistent `.deb` MUSL package names (aarch64-musl used `arm64` instead of `musl-linux-arm64`, and `musleabihf` target missed `bat-musl` prefix). Closes #3482, see #3642 (@mvanhorn) +- Fix incorrect text width computation when using `--binary=as-text` with non-printable characters in caret notation, see #3640 and #3631 (@eyupcanakman) - Fix `BAT_CONFIG_DIR` pointing at system config directory causing duplicate flag errors. Closes #3589, see #3620 (@Xavrir) - Fix syntax highlighting for symlinked files when the symlink name has no extension but the target does. Closes #1001, see #3621 (@Xavrir) - Report error when pager is missing instead of silently falling back, see #3588 (@IMaloney) @@ -23,19 +48,35 @@ - `--help` now correctly honors `--pager=builtin`. See #3516 (@keith-hall) - `--help` now correctly honors custom themes. See #3524 (@keith-hall) - Fixed test compatibility with future Cargo build directory changes, see #3550 (@nmacl) +- Fixed bug caused by using `--plain` and `--terminal-width=N` flags simultaneously, see #3529 (@H4k1l) +- Fixed syntax tests path, see #3610 (@foxfromworld) +- Fix zsh tab completion word-splitting language names containing spaces (e.g. `HTML (Jinja2)`, `Apache Conf`), see #3693 (@YoshKoz) +- Fix zsh tab completion offering invalid `-l` arguments (file globs, paths, hidden filenames) sourced from the second column of `--list-languages`. Closes #3735, see #3737 (@truffle-dev) +- Fix `usize` underflow in `--list-languages` when `--terminal-width` is smaller than the longest language name, see #3812 (@greymoth-jp) ## Other - +- Use git version of cross. See #3533 (@OctopusET) - Bump MSRV to 1.88, update `time` crate to 0.3.47 to fix RUSTSEC-2026-0009, see #3581 (@NORMAL-EX) +- Allow home and end keys to be used with builtin pager, see #3651 (@keith-hall) +- Builtin syntax mapping: cleanup matcher glob parsing logic #3652 (@cyqsimon) +- Statically link the CRT for MSVC builds via Cargo config to avoid runtime DLL dependencies. Closes #3634, see #3692 (@barry3406) +- Replace `libgit2` with a pure Rust implementation of git called `gitoxide`, see PR #3703 (@blinxen) ## Syntaxes +- Add shebang-based detection for Tcl (`tclsh`, `wish`) and Expect (`expect`) scripts, see #3647 (@mvanhorn) - Change the URL of Zig submodule from GitHub to Codeberg, see #3519 (@sorairolake) - Don't color strings inside CSV files, to make it easier to tell which column they belong to, see #3521 (@keith-hall) - Add syntax highlighting support for COBOL, see #3584 (@adukhan99) - Fixed manpage syntax so that ANSI escape codes don't get incorrectly highlighted and thus broken, see #3586 (@BlueElectivire) - Map several Google Cloud CLI config files to their appropriate syntax #3635 (@victor-gp) - Map all ignore dotfiles to Git Ignore syntax #3636 (@victor-gp) +- Improved Kotlin syntax, see #3699 (@guille) +- Include subdirectories in SSH Config syntax mapping, see #3758 (@injust) +- Add Ghostty syntax mapping, see #3759 (@injust) +- Add syntax highlighting for `Caddyfile` #3789 (@CosmicHorrorDev) +- Include `.code-workspace` as a JSON extension #3809 (@dhruvkb) +- Add syntax mapping for DNF repo configuration files, see #3814 (@injust) ## Themes diff --git a/Cargo.lock b/Cargo.lock index 2e777a2e..f04a37ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,19 +4,25 @@ version = 4 [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "ansi_colours" version = "1.2.3" @@ -28,9 +34,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -43,49 +49,59 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys", ] [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] [[package]] name = "assert_cmd" -version = "2.1.1" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85" +checksum = "39bae1d3fa576f7c6519514180a72559268dd7d1fe104070956cb687bc6673bd" dependencies = [ "anstyle", "bstr", @@ -98,9 +114,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64" @@ -127,7 +143,7 @@ dependencies = [ "execute", "expect-test", "flate2", - "git2", + "gix", "globset", "grep-cli", "indexmap", @@ -154,7 +170,7 @@ dependencies = [ "syntect", "tempfile", "terminal-colorsaurus", - "thiserror 2.0.16", + "thiserror", "toml", "unicode-segmentation", "unicode-width", @@ -172,6 +188,21 @@ dependencies = [ "serde", ] +[[package]] +name = "bisync" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5020822f6d6f23196ccaf55e228db36f9de1cf788052b37992e17cbc96ec41a7" +dependencies = [ + "bisync_macros", +] + +[[package]] +name = "bisync_macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d21f40d350a700f6aa107e45fb26448cf489d34794b2ba4522181dc9f1173af6" + [[package]] name = "bit-set" version = "0.8.0" @@ -189,15 +220,30 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.6.0" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] [[package]] name = "bstr" -version = "1.11.3" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "regex-automata", @@ -206,9 +252,9 @@ dependencies = [ [[package]] name = "bugreport" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f280f65ce85b880919349bbfcb204930291251eedcb2e5f84ce2f51df969c162" +checksum = "60c65ffd876f5b1dbfe5dde48856f146da8640989cbc11f157762ebfa3c2309e" dependencies = [ "git-version", "shell-escape", @@ -217,32 +263,43 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.21.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +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" -version = "1.2.7" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ - "jobserver", - "libc", + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -252,18 +309,18 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "clap" -version = "4.5.60" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -274,9 +331,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "clircle" @@ -289,22 +346,30 @@ dependencies = [ ] [[package]] -name = "colorchoice" -version = "1.0.3" +name = "clru" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "197fd99cb113a8d5d9b6376f3aa817f32c1078f2343b714fff7d2ca44fdf67d5" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "console" -version = "0.16.2" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" dependencies = [ "encode_unicode", "libc", - "once_cell", "unicode-width", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -316,6 +381,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -323,10 +397,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "crc32fast" -version = "1.4.2" +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -352,9 +435,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", ] @@ -367,15 +450,17 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossterm" -version = "0.27.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags", + "bitflags 2.11.1", "crossterm_winapi", - "libc", - "mio 0.8.11", + "derive_more", + "document-features", + "mio", "parking_lot", + "rustix", "signal-hook", "signal-hook-mio", "winapi", @@ -391,10 +476,20 @@ dependencies = [ ] [[package]] -name = "darling" -version = "0.21.3" +name = "crypto-common" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -402,11 +497,10 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", @@ -416,9 +510,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", @@ -439,14 +533,82 @@ dependencies = [ ] [[package]] -name = "deranged" -version = "0.5.5" +name = "dashmap" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "defmt" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + [[package]] name = "difflib" version = "0.4.0" @@ -454,27 +616,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] -name = "displaydoc" -version = "0.2.5" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "proc-macro2", - "quote", - "syn", + "block-buffer", + "crypto-common", ] [[package]] name = "dissimilar" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59f8e79d1fbf76bdfbde321e902714bf6c49df88a7dda6fc682fc2979226962d" +checksum = "aeda16ab4059c5fd2a83f2b9c9e9c981327b18aa8e3b313f7e6563799d4f093e" + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encode_unicode" @@ -493,18 +669,18 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys", ] [[package]] @@ -514,34 +690,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" dependencies = [ "cfg-if", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] name = "execute" -version = "0.2.15" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0be3cc61fe54b4cae4463cdbda0401978ffe19d4dcc7a5201a312cddf64726dd" +checksum = "a8bc8fa7331f8c9f97fb52c60c4e1d07fd424db18130cb252b1da7c6bbcfc2c9" dependencies = [ "execute-command-macro", "execute-command-tokens", - "generic-array", ] [[package]] name = "execute-command-macro" -version = "0.1.9" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90dec53d547564e911dc4ff3ecb726a64cf41a6fa01a2370ebc0d95175dd08bd" +checksum = "889365b82d31077b07ed81c1b37f49eed80266f57c99bc3706e8d8d4783388ac" dependencies = [ "execute-command-macro-impl", ] [[package]] name = "execute-command-macro-impl" -version = "0.1.10" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce8cd46a041ad005ab9c71263f9a0ff5b529eac0fe4cc9b4a20f4f0765d8cf4b" +checksum = "581bbece7790a10aa1c40586be3be5b6f466f0a608386885a81924df372fd530" dependencies = [ "execute-command-tokens", "quote", @@ -550,9 +725,9 @@ dependencies = [ [[package]] name = "execute-command-tokens" -version = "0.1.7" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69dc321eb6be977f44674620ca3aa21703cb20ffbe560e1ae97da08401ffbcad" +checksum = "9ef469ea74199a1c75f76f5176e18e4a8864caa6b7ca528c97a5f5fe67c94902" [[package]] name = "expect-test" @@ -576,16 +751,42 @@ dependencies = [ ] [[package]] -name = "fastrand" -version = "2.3.0" +name = "faster-hex" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" +dependencies = [ + "heapless", + "serde", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -607,33 +808,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "form_urlencoded" -version = "1.2.1" +name = "foldhash" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "generic-array" -version = "1.1.1" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cb8bc4c28d15ade99c7e90b219f30da4be5c88e586277e8cbe886beeb868ab2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", + "version_check", ] [[package]] name = "getrandom" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets 0.52.6", + "r-efi", + "wasip2", + "wasip3", ] [[package]] @@ -657,23 +863,744 @@ dependencies = [ ] [[package]] -name = "git2" -version = "0.20.4" +name = "gix" +version = "0.86.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" +checksum = "bb3790fd8981cba7949f1ba924ef865d902df731627bc5998d14164063892fce" dependencies = [ - "bitflags", + "gix-actor", + "gix-attributes", + "gix-command", + "gix-commitgraph", + "gix-config", + "gix-date", + "gix-diff", + "gix-discover", + "gix-error", + "gix-features", + "gix-filter", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-hashtable", + "gix-ignore", + "gix-index", + "gix-lock", + "gix-object", + "gix-odb", + "gix-pack", + "gix-path", + "gix-pathspec", + "gix-protocol", + "gix-ref", + "gix-refspec", + "gix-revision", + "gix-revwalk", + "gix-sec", + "gix-shallow", + "gix-submodule", + "gix-tempfile", + "gix-trace", + "gix-traverse", + "gix-url", + "gix-utils", + "gix-validate", + "gix-worktree", + "gix-worktree-stream", + "gix-zlib", + "nonempty", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-actor" +version = "0.41.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f9308ad6fd35b2a865cbe4117ac61b2be59e4a9ef1621c7a9794f7c8e52c5b" +dependencies = [ + "bstr", + "gix-date", + "gix-error", +] + +[[package]] +name = "gix-attributes" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31c593692ebdc1e38858d9a2b56f6a594c501e24a38971fe6685571f5a07be0" +dependencies = [ + "bstr", + "gix-features", + "gix-glob", + "gix-path", + "gix-quote", + "gix-trace", + "smallvec", + "thiserror", + "unicode-bom", +] + +[[package]] +name = "gix-bitmap" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd1d118d0f5d88b96e6f6e13b566475fef4797ead4a02c26fed36c1375066f7" +dependencies = [ + "gix-error", +] + +[[package]] +name = "gix-chunk" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a871e5cab12ba568845714473505deefffb3c04eb47f4708ce344cd459c1cc" +dependencies = [ + "gix-error", +] + +[[package]] +name = "gix-command" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00706d4fef135ef4b01680d5218c6ee40cda8baf697b864296cbc887d19118f6" +dependencies = [ + "bstr", + "gix-path", + "gix-quote", + "gix-trace", + "shell-words", +] + +[[package]] +name = "gix-commitgraph" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2cd7f054ae2727223fe46dd39c012f066b12f532962d336d29ee193261787da" +dependencies = [ + "bstr", + "gix-chunk", + "gix-error", + "gix-hash", + "memmap2", + "nonempty", +] + +[[package]] +name = "gix-config" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "103d11bef95c467577ecfa8b7b86a22e65af3507b2c9bfa3809a4afbae7df301" +dependencies = [ + "bstr", + "gix-config-value", + "gix-features", + "gix-glob", + "gix-path", + "gix-ref", + "gix-sec", + "gix-utils", + "smallvec", + "thiserror", + "unicode-bom", +] + +[[package]] +name = "gix-config-value" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f813e312a3f7f327187823cd4c754a3e4d948c98907d11e8f37554a2b6b8059" +dependencies = [ + "bitflags 2.11.1", + "bstr", + "gix-path", "libc", - "libgit2-sys", - "log", - "url", + "thiserror", +] + +[[package]] +name = "gix-date" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e47b9e8cdc688296609b706428de570f88b1e0eed7156dde7b4a89d26fa4567" +dependencies = [ + "bstr", + "gix-error", + "itoa", + "jiff", +] + +[[package]] +name = "gix-diff" +version = "0.66.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fee7d89a3c507491cdfc57a1d1e0e300214720b4f7709ebc253e422f99822bfc" +dependencies = [ + "bstr", + "gix-command", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-imara-diff", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-trace", + "gix-traverse", + "gix-worktree", + "thiserror", +] + +[[package]] +name = "gix-discover" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f517766fa1101dfe2606c1a19a8ffa699099030995a9194445446dfe261bdf" +dependencies = [ + "bstr", + "dunce", + "gix-fs", + "gix-path", + "gix-ref", + "gix-sec", + "thiserror", +] + +[[package]] +name = "gix-error" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9292309fd944e71b2a3c96d3c03a6feb8852db646febdde7cbb9f79cb5f329" +dependencies = [ + "bstr", +] + +[[package]] +name = "gix-features" +version = "0.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20aa09e83a48dc02c5f5f08578aa79d3ab1bab4618b8c362f88684645a02bdcc" +dependencies = [ + "bytes", + "crc32fast", + "crossbeam-channel", + "gix-path", + "gix-trace", + "gix-utils", + "libc", + "once_cell", + "parking_lot", + "prodash", + "walkdir", +] + +[[package]] +name = "gix-filter" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e7b5dbf524d97e839f642930c76d7f011c0791e7d11d8148989ac5af7c76aa8" +dependencies = [ + "bstr", + "encoding_rs", + "gix-attributes", + "gix-command", + "gix-hash", + "gix-object", + "gix-packetline", + "gix-path", + "gix-quote", + "gix-trace", + "gix-utils", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-fs" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865cf13fcaf5455220546cb9607c416bd1be9a6caafd143655a362fdeab64e80" +dependencies = [ + "bstr", + "gix-features", + "gix-path", + "gix-utils", + "thiserror", +] + +[[package]] +name = "gix-glob" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421e92a711554fa5827d1b0599d3389acdd0f6729e97a8c5a57d79af1e50bf36" +dependencies = [ + "bitflags 2.11.1", + "bstr", + "gix-features", + "gix-path", +] + +[[package]] +name = "gix-hash" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13adaa73415fd6c902310923f68d0b98e8cecf14b33ea58c02cc387cee56f54e" +dependencies = [ + "faster-hex", + "gix-features", + "sha1-checked", + "thiserror", +] + +[[package]] +name = "gix-hashtable" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78fccd6fea3bcf0b39c076bae60ae49b08daaf538b950202101a981f9d3c01d3" +dependencies = [ + "gix-hash", + "hashbrown 0.17.1", + "parking_lot", +] + +[[package]] +name = "gix-ignore" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cff8e8aa125e39377456073e63df3334d9e5741372ddcc226198015076dda2" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-trace", + "unicode-bom", +] + +[[package]] +name = "gix-imara-diff" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a791e6620676a875f362f3156ed213e73ca099a09bf992c18812abe65cc37b1" +dependencies = [ + "bstr", + "hashbrown 0.17.1", +] + +[[package]] +name = "gix-index" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5009c4e7e9f9b4cfaaab1153e49133eb04d79c015b5702d6c3d2ab94271a89c6" +dependencies = [ + "bitflags 2.11.1", + "bstr", + "filetime", + "fnv", + "gix-bitmap", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-traverse", + "gix-utils", + "gix-validate", + "hashbrown 0.17.1", + "itoa", + "libc", + "memmap2", + "rustix", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-lock" +version = "24.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c69157820343bf1c6e4b88b9808e920900de02e18aaf5862b30ada43814848" +dependencies = [ + "gix-tempfile", + "gix-utils", + "thiserror", +] + +[[package]] +name = "gix-object" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e48c235e7f886eb819fc878af75be889333dd3c38bee02ed7af48ae2cf596c4" +dependencies = [ + "bstr", + "gix-actor", + "gix-date", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-utils", + "gix-validate", + "itoa", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-odb" +version = "0.83.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd494ffb5037e62b8220109e894d2861ff2150a2cacbfccdba57ae1ebab2b96" +dependencies = [ + "arc-swap", + "gix-features", + "gix-fs", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-pack", + "gix-path", + "gix-quote", + "gix-zlib", + "memmap2", + "parking_lot", + "tempfile", + "thiserror", +] + +[[package]] +name = "gix-pack" +version = "0.73.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d5446127b269706e85998065267ddd2ccc3550179da6780b22fe496175ccb20" +dependencies = [ + "clru", + "gix-chunk", + "gix-error", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-path", + "gix-zlib", + "memmap2", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-packetline" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3766025c72319c4accdd854a18e6f0dd176c8eb0f3bc8a60a7765be2b50cabf2" +dependencies = [ + "bstr", + "faster-hex", + "gix-trace", + "thiserror", +] + +[[package]] +name = "gix-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ed3e8d7a82e886e17a72e03d4ba0c13db6f2219b6cd4e2900b4cae426ec20c9" +dependencies = [ + "bstr", + "gix-trace", + "gix-validate", + "thiserror", +] + +[[package]] +name = "gix-pathspec" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f6fa5f8007f008187c3f60b4373209ca83d1cc947f35ede03e16cd15a4d137" +dependencies = [ + "bitflags 2.11.1", + "bstr", + "gix-attributes", + "gix-config-value", + "gix-glob", + "gix-path", + "thiserror", +] + +[[package]] +name = "gix-protocol" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dede40e89c1e90f548415f50636bb051f6d9c60f68b8b710bc07825722d19588" +dependencies = [ + "bisync", + "bstr", + "gix-date", + "gix-features", + "gix-hash", + "gix-ref", + "gix-shallow", + "gix-transport", + "gix-utils", + "nonempty", + "thiserror", +] + +[[package]] +name = "gix-quote" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e541fc33cc2b783b7979040d445a0c86a2eca747c8faea4ca84230d06ae6ef" +dependencies = [ + "bstr", + "gix-error", + "gix-utils", +] + +[[package]] +name = "gix-ref" +version = "0.66.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeb0c90a8f6202ceaaa22996cbf837c943ccb2d8af9ff3490f0758305e6b7883" +dependencies = [ + "gix-actor", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-utils", + "gix-validate", + "memmap2", + "thiserror", +] + +[[package]] +name = "gix-refspec" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7406282cc0259b51f6aee299ca3d31279a020530363152a2e6c96e8a7f7bbc83" +dependencies = [ + "bstr", + "gix-error", + "gix-glob", + "gix-hash", + "gix-revision", + "gix-validate", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-revision" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e55e09d4a1ecf2beecc8c09cafcad37979e805b31f588b0e957e191df5783681" +dependencies = [ + "bstr", + "gix-commitgraph", + "gix-date", + "gix-error", + "gix-hash", + "gix-object", + "gix-revwalk", + "nonempty", +] + +[[package]] +name = "gix-revwalk" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c113c0a53294dc6280ffc06cbcc4f50f820397e97d6a00b429a44b8db26e29" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-error", + "gix-hash", + "gix-hashtable", + "gix-object", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-sec" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4fe6c152c1d50aea36f299825702cd37e303307832fec1d0fdd5844e47ce2f" +dependencies = [ + "bitflags 2.11.1", + "gix-path", + "libc", + "windows-sys", +] + +[[package]] +name = "gix-shallow" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecc9f4b40537043e4bbd7d3d1760e74fb8e7b07a546166b558acaa73ad97f4a" +dependencies = [ + "bstr", + "gix-hash", + "gix-lock", + "nonempty", + "thiserror", +] + +[[package]] +name = "gix-submodule" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd98077a56d08886112e6b08dc94076d03539f4bc0b9d7880e4be2b8a640d8c" +dependencies = [ + "bstr", + "gix-config", + "gix-path", + "gix-pathspec", + "gix-refspec", + "gix-url", + "thiserror", +] + +[[package]] +name = "gix-tempfile" +version = "24.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b675b920bd5a61d17ad542772f03ec34c60feb8ff683e1560c03ae967363731e" +dependencies = [ + "dashmap 6.2.1", + "gix-fs", + "libc", + "parking_lot", + "tempfile", +] + +[[package]] +name = "gix-trace" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be3eb81d9dc914335923e50d52829c551feefd6a72d176c4130c546b67a60814" + +[[package]] +name = "gix-transport" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c1bcf30081eb8ab04540a5795c67a2fcb22cb2e976e8b1a4ea657b1ed61469" +dependencies = [ + "bstr", + "gix-command", + "gix-features", + "gix-packetline", + "gix-path", + "gix-quote", + "gix-sec", + "gix-url", + "thiserror", +] + +[[package]] +name = "gix-traverse" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008c5cd879e46e86b5c2469e633611978b18775d53d05668d691bc13088bd409" +dependencies = [ + "bitflags 2.11.1", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-revwalk", + "smallvec", + "thiserror", +] + +[[package]] +name = "gix-url" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d10e53b8eae21ee601687f47bbbd6cb2ed7162cb4c1cafdd422fb7ec64cbee" +dependencies = [ + "bstr", + "gix-path", + "gix-utils", + "percent-encoding", + "thiserror", +] + +[[package]] +name = "gix-utils" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1795bd2a970ca8b2185318c2abb97d955c71992f1cf28de73ad3b593a9f3ce8" +dependencies = [ + "bstr", + "fastrand", + "getrandom", + "unicode-normalization", +] + +[[package]] +name = "gix-validate" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a034e84d1e04e1b1f20f51f12491da230b6ac8b925d0c8e1b89bcd87a7c5ccc" +dependencies = [ + "bstr", +] + +[[package]] +name = "gix-worktree" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31eb8e675122e83585e461fe28f68ff8c5ed55b49017b697e7e76423ff973424" +dependencies = [ + "bstr", + "gix-attributes", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-validate", +] + +[[package]] +name = "gix-worktree-stream" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b088c8724e7be120c4798dd86925cf05332c9d356a463542578600c50c7a549" +dependencies = [ + "gix-attributes", + "gix-error", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-object", + "gix-path", + "gix-traverse", + "parking_lot", +] + +[[package]] +name = "gix-zlib" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e8813f5579b3075ff9c90f7c59cd2b62b4ebb639361f0911648b22d7446cc7c" +dependencies = [ + "thiserror", + "zlib-rs", ] [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" @@ -702,135 +1629,73 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", + "allocator-api2", + "equivalent", + "foldhash 0.2.0", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "hashbrown" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", + "allocator-api2", + "equivalent", + "foldhash 0.2.0", ] [[package]] -name = "icu_locid_transform" -version = "1.5.0" +name = "heapless" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", + "hash32", "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", ] [[package]] -name = "icu_provider_macros" -version = "1.5.0" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "ident_case" @@ -838,44 +1703,23 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "idna" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -888,17 +1732,50 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] -name = "jobserver" -version = "0.1.32" +name = "jiff" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "34f877a98676d2fb664698d74cc6a51ce6c484ce8c770f05d0108ec9090aeb46" dependencies = [ - "libc", + "defmt", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-static" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0666b5ab5ecaca213fc2a85b8c0083d9004e84ee2d5f9a7e0017aaf50986f25f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", ] [[package]] @@ -907,35 +1784,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.182" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" - -[[package]] -name = "libgit2-sys" -version = "0.18.3+1.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" -dependencies = [ - "cc", - "libc", - "libz-sys", - "pkg-config", -] - -[[package]] -name = "libz-sys" -version = "1.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9b68e50e6e0b26f672573834882eb57759f6db9b3be2ea3c35c91188bb4eaa" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "linked-hash-map" @@ -945,43 +1804,45 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.15" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] -name = "linux-raw-sys" -version = "0.11.0" +name = "litrs" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "litemap" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.22" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.4" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] [[package]] name = "miniz_oxide" @@ -990,58 +1851,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "minus" -version = "5.6.1" +version = "5.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093bd0520d2a37943566a73750e6d44094dac75d66a978d1f0d97ffc78686832" +checksum = "1db1df1b8dd701aa57b41283b50b751b3ebc8fe1406955ec90c53b46b475fa56" dependencies = [ "crossbeam-channel", "crossterm", - "once_cell", "parking_lot", "regex", "textwrap", - "thiserror 1.0.69", + "thiserror", ] [[package]] name = "mio" -version = "0.8.11" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.48.0", -] - -[[package]] -name = "mio" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" -dependencies = [ - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.61.2", + "wasi", + "windows-sys", ] [[package]] name = "nix" -version = "0.31.2" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", ] +[[package]] +name = "nonempty" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6" + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -1050,9 +1906,9 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "ntapi" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" dependencies = [ "winapi", ] @@ -1063,14 +1919,14 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-traits" @@ -1083,20 +1939,23 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" -dependencies = [ - "parking_lot_core", -] +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "onig" -version = "6.5.1" +version = "6.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2" dependencies = [ - "bitflags", + "bitflags 2.11.1", "libc", "once_cell", "onig_sys", @@ -1104,9 +1963,9 @@ dependencies = [ [[package]] name = "onig_sys" -version = "69.9.1" +version = "69.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7" dependencies = [ "cc", "pkg-config", @@ -1114,9 +1973,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1124,15 +1983,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -1146,21 +2005,21 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plist" -version = "1.7.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" dependencies = [ "base64", "indexmap", @@ -1169,6 +2028,21 @@ dependencies = [ "time", ] +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1177,9 +2051,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "predicates" -version = "3.1.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" dependencies = [ "anstyle", "difflib", @@ -1191,15 +2065,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" [[package]] name = "predicates-tree" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" dependencies = [ "predicates-core", "termtree", @@ -1216,37 +2090,74 @@ dependencies = [ ] [[package]] -name = "proc-macro2" -version = "1.0.106" +name = "proc-macro-error-attr2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] -name = "quick-xml" -version = "0.32.0" +name = "prodash" +version = "31.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +checksum = "962200e2d7d551451297d9fdce85138374019ada198e30ea9ede38034e27604c" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] [[package]] -name = "rayon" -version = "1.10.0" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -1254,9 +2165,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -1264,18 +2175,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.8" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.1", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -1285,9 +2196,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -1296,50 +2207,52 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rgb" -version = "0.8.50" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" dependencies = [ "bytemuck", ] [[package]] -name = "rustix" -version = "0.38.43" +name = "rustc_version" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "semver", ] [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.11.1", "errno", "libc", - "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", + "linux-raw-sys", + "windows-sys", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -1358,9 +2271,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" -version = "1.0.25" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -1394,30 +2307,31 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.135" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] name = "serde_spanned" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] [[package]] name = "serde_with" -version = "3.17.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ "serde_core", "serde_with_macros", @@ -1425,9 +2339,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.17.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ "darling", "proc-macro2", @@ -1454,7 +2368,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" dependencies = [ - "dashmap", + "dashmap 5.5.3", "lazy_static", "parking_lot", "serial_test_derive", @@ -1471,6 +2385,27 @@ dependencies = [ "syn", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1-checked" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" +dependencies = [ + "digest", + "sha1", +] + [[package]] name = "shell-escape" version = "0.1.5" @@ -1501,35 +2436,42 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", - "mio 0.8.11", + "mio", "signal-hook", ] [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] [[package]] -name = "smallvec" -version = "1.13.2" +name = "simd-adler32" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "std_prelude" @@ -1545,26 +2487,15 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.108" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "synstructure" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "syntect" version = "5.3.0" @@ -1582,7 +2513,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "thiserror 2.0.16", + "thiserror", "walkdir", "yaml-rust", ] @@ -1603,15 +2534,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom", "once_cell", - "rustix 1.1.2", - "windows-sys 0.61.2", + "rustix", + "windows-sys", ] [[package]] @@ -1625,38 +2556,38 @@ dependencies = [ [[package]] name = "terminal-colorsaurus" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8909f33134da34b43f69145e748790de650a6abd84faf1f82e773444dd293ec8" +checksum = "7a46bb5364467da040298c573c8a95dbf9a512efc039630409a03126e3703e90" dependencies = [ "cfg-if", "libc", "memchr", - "mio 1.1.0", + "mio", "terminal-trx", - "windows-sys 0.61.2", + "windows-sys", "xterm-color", ] [[package]] name = "terminal-trx" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662a3cd5ca570df622e848ef18b50c151e65c9835257465417242243b0bce783" +checksum = "3b3f27d9a8a177e57545481faec87acb45c6e854ed1e5a3658ad186c106f38ed" dependencies = [ "cfg-if", "libc", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] name = "terminal_size" -version = "0.4.1" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ - "rustix 0.38.43", - "windows-sys 0.59.0", + "rustix", + "windows-sys", ] [[package]] @@ -1676,38 +2607,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.69" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" -dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.69" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -1746,20 +2657,25 @@ dependencies = [ ] [[package]] -name = "tinystr" -version = "0.7.6" +name = "tinyvec" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ - "displaydoc", - "zerovec", + "tinyvec_macros", ] [[package]] -name = "toml" -version = "0.9.8" +name = "tinyvec_macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "indexmap", "serde_core", @@ -1772,51 +2688,72 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.4" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "typenum" -version = "1.17.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicode-bom" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "unsafe-libyaml" @@ -1824,29 +2761,6 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" -[[package]] -name = "url" -version = "2.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" version = "0.2.2" @@ -1854,10 +2768,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "vcpkg" -version = "0.2.15" +name = "version_check" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wait-timeout" @@ -1880,17 +2794,60 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.13.3+wasi-0.2.2" +name = "wasip2" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", ] [[package]] @@ -1920,11 +2877,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys", ] [[package]] @@ -1940,7 +2897,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" dependencies = [ "windows-core 0.56.0", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -1950,7 +2907,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" dependencies = [ "windows-core 0.57.0", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -1962,7 +2919,7 @@ dependencies = [ "windows-implement 0.56.0", "windows-interface 0.56.0", "windows-result", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -1974,7 +2931,7 @@ dependencies = [ "windows-implement 0.57.0", "windows-interface 0.57.0", "windows-result", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -2033,25 +2990,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -2063,67 +3002,34 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2136,48 +3042,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -2186,36 +3068,109 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.14" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" [[package]] -name = "wit-bindgen-rt" -version = "0.33.0" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ - "bitflags", + "wit-bindgen-rust-macro", ] [[package]] -name = "write16" -version = "1.0.0" +name = "wit-bindgen" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] -name = "writeable" -version = "0.5.5" +name = "wit-bindgen-core" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "xterm-color" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de5f056fb9dc8b7908754867544e26145767187aaac5a98495e88ad7cb8a80f" +checksum = "7008a9d8ba97a7e47d9b2df63fcdb8dade303010c5a7cd5bf2469d4da6eba673" [[package]] name = "yaml-rust" @@ -2227,68 +3182,13 @@ dependencies = [ ] [[package]] -name = "yoke" -version = "0.7.5" +name = "zlib-rs" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" [[package]] -name = "yoke-derive" -version = "0.7.5" +name = "zmij" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerofrom" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerovec" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 6c783336..8d8a3be1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,8 +31,7 @@ minimal-application = [ "regex-onig", "wild", ] -git = ["git2"] # Support indicating git modifications -vendored-libgit2 = ["git2/vendored-libgit2"] +git = ["gix"] # Support indicating git modifications paging = [ "shell-words", "grep-cli", "minus"] # Support applying a pager on the output lessopen = ["execute"] # Support $LESSOPEN preprocessor build-assets = ["syntect/yaml-load", "syntect/plist-load", "regex", "walkdir"] @@ -52,11 +51,11 @@ thiserror = "2.0" wild = { version = "2.2", optional = true } content_inspector = "0.2.4" shell-words = { version = "1.1.1", optional = true } -minus = { version = "5.6", optional = true, features = [ +minus = { version = "5.7", optional = true, features = [ "dynamic_output", "search", ] } -unicode-width = "0.2.1" +unicode-width = "0.2.2" globset = "0.4" serde = "1.0" serde_derive = "1.0" @@ -64,22 +63,23 @@ serde_yaml = "0.9.28" semver = "1.0" path_abs = { version = "0.5", default-features = false } clircle = { version = "0.6.1", default-features = false } -bugreport = { version = "0.5.0", optional = true } +bugreport = { version = "0.6.0", optional = true } etcetera = { version = "0.11.0", optional = true } grep-cli = { version = "0.1.12", optional = true } -regex = { version = "1.12.2", optional = true } +regex = { version = "1.12.3", optional = true } walkdir = { version = "2.5", optional = true } bytesize = { version = "2.3.1" } encoding_rs = "0.8.35" -execute = { version = "0.2.15", optional = true } +execute = { version = "0.3.0", optional = true } terminal-colorsaurus = "1.0" -unicode-segmentation = "1.12.0" +unicode-segmentation = "1.13.2" itertools = "0.14.0" -[dependencies.git2] -version = "0.20" +[dependencies.gix] +version = "0.86" optional = true default-features = false +features = ["sha1", "blob-diff"] [dependencies.syntect] version = "5.3.0" @@ -87,20 +87,20 @@ default-features = false features = ["parsing"] [dependencies.clap] -version = "4.5.60" +version = "4.6.1" optional = true features = ["wrap_help", "cargo"] [target.'cfg(target_os = "macos")'.dependencies] -plist = "1.7.0" +plist = "1.9.0" [dev-dependencies] assert_cmd = "2.0.16" expect-test = "1.5.0" serial_test = { version = "2.0.0", default-features = false } -predicates = "3.1.3" +predicates = "3.1.4" wait-timeout = "0.2.1" -tempfile = "3.23.0" +tempfile = "3.27.0" serde = { version = "1.0", features = ["derive"] } [target.'cfg(unix)'.dev-dependencies] @@ -108,22 +108,22 @@ nix = { version = "0.31", default-features = false, features = ["term"] } [build-dependencies] anyhow = "1.0.97" -indexmap = { version = "2.13.0", features = ["serde"] } +indexmap = { version = "2.14.0", features = ["serde"] } itertools = "0.14.0" once_cell = "1.20" prettyplease = "0.2.37" proc-macro2 = "1.0.106" -quote = "1.0.40" -regex = "1.12.2" +quote = "1.0.45" +regex = "1.12.3" serde = "1.0" serde_derive = "1.0" serde_with = { version = "3.17.0", default-features = false, features = ["macros"] } syn = { version = "2.0.104", features = ["full"] } -toml = { version = "0.9.8", features = ["preserve_order"] } +toml = { version = "1.1.1", features = ["preserve_order"] } walkdir = "2.5" [build-dependencies.clap] -version = "4.5.60" +version = "4.6.1" optional = true features = ["wrap_help", "cargo"] diff --git a/README.md b/README.md index d5a68440..8826d4e0 100644 --- a/README.md +++ b/README.md @@ -19,20 +19,6 @@ [Русский]

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

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

- ### Syntax highlighting `bat` supports syntax highlighting for a large number of programming and markup @@ -208,12 +194,6 @@ Note that the [Manpage syntax](assets/syntaxes/02_Extra/Manpage.sublime-syntax) The [`prettybat`](https://github.com/eth-p/bat-extras/blob/master/doc/prettybat.md) script is a wrapper that will format code and print it with `bat`. -#### `Warp` - - - Warp - - #### Highlighting `--help` messages You can use `bat` to colorize help text: `$ cp --help | bat -plhelp` @@ -246,6 +226,14 @@ abbr -a --position anywhere -- -h '-h | bat -plhelp' This way, you can keep on using `cp --help`, but get colorized help pages. +> [!TIP] +> To remove these abbreviations later, run: +> ```fish +> abbr -e -- --help +> abbr -e -- -h +> ``` +> The `--` before the abbreviation name is required because `--help` and `-h` start with dashes, which would otherwise be interpreted as flags to `abbr` itself. + Be aware that in some cases, `-h` may not be a shorthand of `--help` (for example with `ls`). In cases where you need to use `-h` as a command argument you can prepend `\` to the argument (eg. `ls \-h`) to escape the aliasing defined above. @@ -319,7 +307,7 @@ pacman -S bat ### On Fedora -You can install [the `bat` package](https://koji.fedoraproject.org/koji/packageinfo?packageID=27506) from the official [Fedora Modular](https://docs.fedoraproject.org/en-US/modularity/using-modules/) repository. +You can install [the `bat` package](https://packages.fedoraproject.org/pkgs/rust-bat/bat/) from the official sources: ```bash dnf install bat @@ -519,6 +507,30 @@ and line numbers but no grid and no file header. Set the `BAT_STYLE` environment variable to make these changes permanent or use `bat`'s [configuration file](#configuration-file). +By default, `bat` enables `changes`, `grid`, `header-filename`, `numbers`, and `snip`. + +The available pre-defined styles are: + +| Style | Description | +|-------|-------------| +| `default` | Enables the recommended style components listed above. | +| `full` | Enables all available components. | +| `auto` | Same as `default`, unless the output is piped. | +| `plain` | Disables all available components. | + +The available individual components are: + +| Component | Description | +|-----------|-------------| +| `changes` | Show Git modification markers. | +| `header` | Alias for `header-filename`. | +| `header-filename` | Show filenames before the content. | +| `header-filesize` | Show file sizes before the content. | +| `grid` | Vertical/horizontal lines to separate the side bar and header from the content. | +| `rule` | Horizontal lines to delimit files. | +| `numbers` | Show line numbers in the side bar. | +| `snip` | Draw separation lines between distinct line ranges. | + >[!tip] > If you specify a default style in `bat`'s config file, you can change which components > are displayed during a single run of `bat` using the `--style` command-line argument. @@ -831,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. @@ -886,7 +902,7 @@ cargo install --path . --locked --force ``` If you want to build an application that uses `bat`'s pretty-printing -features as a library, check out the [the API documentation](https://docs.rs/bat/). +features as a library, check out the [API documentation](https://docs.rs/bat/). Note that you have to use either `regex-onig` or `regex-fancy` as a feature when you depend on `bat` as a library. diff --git a/assets/completions/_bat.ps1.in b/assets/completions/_bat.ps1.in index 97f76932..2f01f768 100644 --- a/assets/completions/_bat.ps1.in +++ b/assets/completions/_bat.ps1.in @@ -14,12 +14,12 @@ Register-ArgumentCompleter -Native -CommandName '{{PROJECT_EXECUTABLE}}' -Script $ArrayPrint = @('unicode', 'caret') function Get-MyThemes(){ - $themes = {{PROJECT_EXECUTABLE}} --list-themes | ForEach-Object {$_ -replace "^(.*)$", '''$1'''} | select-object + $themes = {{PROJECT_EXECUTABLE}} --no-paging --list-themes | ForEach-Object {$_ -replace "^(.*)$", '''$1'''} | select-object return $themes } function Get-MyLanguages(){ - $themes = {{PROJECT_EXECUTABLE}} --list-languages | ForEach-Object{[pscustomobject]@{MyParameter=$_.Substring(0,$_.IndexOf(":")).Trim();MyDescription=$_.Substring($_.IndexOf(":")+1)}} | select-object + $themes = {{PROJECT_EXECUTABLE}} --no-paging --list-languages | ForEach-Object{[pscustomobject]@{MyParameter=$_.Substring(0,$_.IndexOf(":")).Trim();MyDescription=$_.Substring($_.IndexOf(":")+1)}} | select-object return $themes } @@ -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'').') diff --git a/assets/completions/bat.bash.in b/assets/completions/bat.bash.in index 6e45bd19..5c4f646b 100644 --- a/assets/completions/bat.bash.in +++ b/assets/completions/bat.bash.in @@ -80,7 +80,7 @@ _bat() { -l | --language) local IFS=$'\n' COMPREPLY=($(compgen -W "$( - "$1" --list-languages | while IFS=: read -r lang _; do + "$1" --no-paging --list-languages | while IFS=: read -r lang _; do printf "%s\n" "$lang" done )" -- "$cur")) @@ -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 @@ -150,14 +154,14 @@ _bat() { ;; --theme) local IFS=$'\n' - COMPREPLY=($(compgen -W "auto${IFS}auto:always${IFS}auto:system${IFS}dark${IFS}light${IFS}$("$1" --list-themes)" -- "$cur")) + COMPREPLY=($(compgen -W "auto${IFS}auto:always${IFS}auto:system${IFS}dark${IFS}light${IFS}$("$1" --no-paging --list-themes)" -- "$cur")) __bat_escape_completions return 0 ;; --theme-dark | \ --theme-light) local IFS=$'\n' - COMPREPLY=($(compgen -W "$("$1" --list-themes)" -- "$cur")) + COMPREPLY=($(compgen -W "$("$1" --no-paging --list-themes)" -- "$cur")) __bat_escape_completions return 0 ;; @@ -221,6 +225,7 @@ _bat() { --list-themes --squeeze-blank --squeeze-limit + --sanitize --strip-ansi --style --line-range diff --git a/assets/completions/bat.fish.in b/assets/completions/bat.fish.in index 2100338c..b2e35664 100644 --- a/assets/completions/bat.fish.in +++ b/assets/completions/bat.fish.in @@ -15,11 +15,11 @@ function __bat_complete_files -a token end function __bat_complete_one_language -a comp - command $bat --list-languages | string split -f1 : | string match -e "$comp" + command $bat --no-paging --list-languages | string split -f1 : | string match -e "$comp" end function __bat_complete_list_languages - for spec in (command $bat --list-languages) + for spec in (command $bat --no-paging --list-languages) set -l name (string split -f1 : $spec) for ext in (string split -f2 : $spec | string split ,) test -n "$ext"; or continue @@ -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 @@ -234,11 +235,11 @@ complete -c $bat -l tabs -x -a "$tabs_opts" -d "Set tab width" -n __bat_no_excl_ complete -c $bat -l terminal-width -x -d "Set terminal , +, or -" -n __bat_no_excl_args -complete -c $bat -l theme -x -a "$special_themes(command $bat --list-themes | command cat)" -d "Set the syntax highlighting theme" -n __bat_no_excl_args +complete -c $bat -l theme -x -a "$special_themes(command $bat --no-paging --list-themes | command cat)" -d "Set the syntax highlighting theme" -n __bat_no_excl_args -complete -c $bat -l theme-dark -x -a "(command $bat --list-themes | command cat)" -d "Set the syntax highlighting theme for dark backgrounds" -n __bat_no_excl_args +complete -c $bat -l theme-dark -x -a "(command $bat --no-paging --list-themes | command cat)" -d "Set the syntax highlighting theme for dark backgrounds" -n __bat_no_excl_args -complete -c $bat -l theme-light -x -a "(command $bat --list-themes | command cat)" -d "Set the syntax highlighting theme for light backgrounds" -n __bat_no_excl_args +complete -c $bat -l theme-light -x -a "(command $bat --no-paging --list-themes | command cat)" -d "Set the syntax highlighting theme for light backgrounds" -n __bat_no_excl_args complete -c $bat -s u -l unbuffered -d "Enable unbuffered input reading for streaming use cases" -n __bat_no_excl_args diff --git a/assets/completions/bat.zsh.in b/assets/completions/bat.zsh.in index 3c3f81d1..775ae9df 100644 --- a/assets/completions/bat.zsh.in +++ b/assets/completions/bat.zsh.in @@ -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' @@ -90,20 +91,25 @@ _{{PROJECT_EXECUTABLE}}_main() { languages) local IFS=$'\n' local -a languages - languages=( $({{PROJECT_EXECUTABLE}} --list-languages | awk -F':|,' '{ for (i = 1; i <= NF; ++i) printf("%s:%s\n", $i, $1) }') ) + # `--list-languages` emits one `name:matchers` line per language, + # which `_describe` parses as `value:description`. Only the + # language name is offered as the completion value; the matchers + # show up as the menu description. See + # https://github.com/sharkdp/bat/issues/3735. + languages=( ${(f)"$({{PROJECT_EXECUTABLE}} --no-paging --color=never --decorations=never --list-languages)"} ) _describe 'language' languages && ret=0 ;; themes) local -a themes expl - themes=(${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --list-themes)"} ) + themes=(${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --no-paging --color=never --decorations=never --list-themes)"} ) _wanted themes expl 'theme' compadd -a themes && ret=0 ;; theme_preferences) local -a themes expl - themes=(auto dark light auto:always auto:system ${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --list-themes)"} ) + themes=(auto dark light auto:always auto:system ${(f)"$(_call_program themes {{PROJECT_EXECUTABLE}} --no-paging --color=never --decorations=never --list-themes)"} ) _wanted themes expl 'theme' compadd -a themes && ret=0 ;; diff --git a/assets/patches/Python.sublime-syntax.patch b/assets/patches/Python.sublime-syntax.patch index 36a50894..bacb9db9 100644 --- a/assets/patches/Python.sublime-syntax.patch +++ b/assets/patches/Python.sublime-syntax.patch @@ -2,6 +2,15 @@ diff --git syntaxes/01_Packages/Python/Python.sublime-syntax syntaxes/01_Package index 2acd86d8..86257f7b 100644 --- syntaxes/01_Packages/Python/Python.sublime-syntax +++ syntaxes/01_Packages/Python/Python.sublime-syntax +@@ -25,7 +31,7 @@ file_extensions: + - wscript + - bazel + - bzl +-first_line_match: ^#!\s*/.*\bpython(\d(\.\d)?)?\b ++first_line_match: ^#!\s*/.*\b(python(\d(\.\d)?)?|uv)\b + scope: source.python + + variables: @@ -988,10 +988,6 @@ contexts: - match: \} scope: punctuation.section.mapping-or-set.end.python diff --git a/assets/patches/Tcl.sublime-syntax.patch b/assets/patches/Tcl.sublime-syntax.patch new file mode 100644 index 00000000..487a1841 --- /dev/null +++ b/assets/patches/Tcl.sublime-syntax.patch @@ -0,0 +1,12 @@ +diff --git syntaxes/01_Packages/TCL/Tcl.sublime-syntax syntaxes/01_Packages/TCL/Tcl.sublime-syntax +index 1234567..abcdefg 100644 +--- syntaxes/01_Packages/TCL/Tcl.sublime-syntax ++++ syntaxes/01_Packages/TCL/Tcl.sublime-syntax +@@ -3,6 +3,7 @@ + # http://www.sublimetext.com/docs/3/syntax.html + name: Tcl + file_extensions: + - tcl ++first_line_match: ^\#!.*\b(tclsh|wish|expect)\b + scope: source.tcl + variables: diff --git a/assets/syntaxes/02_Extra/Caddy b/assets/syntaxes/02_Extra/Caddy new file mode 160000 index 00000000..cd7d132d --- /dev/null +++ b/assets/syntaxes/02_Extra/Caddy @@ -0,0 +1 @@ +Subproject commit cd7d132da38291b632995f84ae1e920458943b18 diff --git a/assets/syntaxes/02_Extra/Idris2 b/assets/syntaxes/02_Extra/Idris2 index bbfe50e0..4d8eb35a 160000 --- a/assets/syntaxes/02_Extra/Idris2 +++ b/assets/syntaxes/02_Extra/Idris2 @@ -1 +1 @@ -Subproject commit bbfe50e023e0edc74f5e0c003eb946528d49279f +Subproject commit 4d8eb35a38254d422030e77b4933530008dd3c6e diff --git a/assets/syntaxes/02_Extra/Kotlin b/assets/syntaxes/02_Extra/Kotlin index aeeed278..c3536941 160000 --- a/assets/syntaxes/02_Extra/Kotlin +++ b/assets/syntaxes/02_Extra/Kotlin @@ -1 +1 @@ -Subproject commit aeeed2780b04aea3d293c547c24cae27cafef0c5 +Subproject commit c353694169c047bd746c93c214289fc52d152cae diff --git a/assets/syntaxes/02_Extra/Kotlin.sublime-syntax b/assets/syntaxes/02_Extra/Kotlin.sublime-syntax deleted file mode 100644 index eab59192..00000000 --- a/assets/syntaxes/02_Extra/Kotlin.sublime-syntax +++ /dev/null @@ -1,398 +0,0 @@ -%YAML 1.2 ---- -# http://www.sublimetext.com/docs/3/syntax.html -name: Kotlin -file_extensions: - - kt - - kts -scope: source.Kotlin -contexts: - main: - - include: comments - - match: '^\s*(package)\b(?:\s*([^ ;$]+)\s*)?' - captures: - 1: keyword.other.kotlin - 2: entity.name.package.kotlin - - include: imports - - include: statements - classes: - - match: (?" - pop: true - - include: generics - - match: \( - push: - - match: \) - pop: true - - include: parameters - - match: (:) - captures: - 1: keyword.operator.declaration.kotlin - push: - - match: "(?={|$)" - pop: true - - match: \w+ - scope: entity.other.inherited-class.kotlin - - match: \( - push: - - match: \) - pop: true - - include: expressions - - match: '\{' - push: - - match: '\}' - pop: true - - include: statements - comments: - - match: /\* - captures: - 0: punctuation.definition.comment.kotlin - push: - - meta_scope: comment.block.kotlin - - match: \*/ - captures: - 0: punctuation.definition.comment.kotlin - pop: true - - match: \s*((//).*$\n?) - captures: - 1: comment.line.double-slash.kotlin - 2: punctuation.definition.comment.kotlin - constants: - - match: \b(true|false|null|this|super)\b - scope: constant.language.kotlin - - match: '\b((0(x|X)[0-9a-fA-F]*)|(([0-9]+\.?[0-9]*)|(\.[0-9]+))((e|E)(\+|-)?[0-9]+)?)([LlFf])?\b' - scope: constant.numeric.kotlin - - match: '\b([A-Z][A-Z0-9_]+)\b' - scope: constant.other.kotlin - expressions: - - match: \( - push: - - match: \) - pop: true - - include: expressions - - include: types - - include: strings - - include: constants - - include: comments - - include: keywords - functions: - - match: (?=\s*\b(?:fun)\b) - push: - - match: '(?=$|\})' - pop: true - - match: \b(fun)\b - captures: - 1: keyword.other.kotlin - push: - - match: (?=\() - pop: true - - match: < - push: - - match: ">" - pop: true - - include: generics - - match: '([\.<\?>\w]+\.)?(\w+)' - captures: - 2: entity.name.function.kotlin - - match: \( - push: - - match: \) - pop: true - - include: parameters - - match: (:) - captures: - 1: keyword.operator.declaration.kotlin - push: - - match: "(?={|=|$)" - pop: true - - include: types - - match: '\{' - push: - - match: '(?=\})' - pop: true - - include: statements - - match: (=) - captures: - 1: keyword.operator.assignment.kotlin - push: - - match: (?=$) - pop: true - - include: expressions - generics: - - match: (:) - captures: - 1: keyword.operator.declaration.kotlin - push: - - match: (?=,|>) - pop: true - - include: types - - include: keywords - - match: \w+ - scope: storage.type.generic.kotlin - getters-and-setters: - - match: \b(get)\b\s*\(\s*\) - captures: - 1: entity.name.function.kotlin - push: - - match: '\}|(?=\bset\b)|$' - pop: true - - match: (=) - captures: - 1: keyword.operator.assignment.kotlin - push: - - match: (?=$|\bset\b) - pop: true - - include: expressions - - match: '\{' - push: - - match: '\}' - pop: true - - include: expressions - - match: \b(set)\b\s*(?=\() - captures: - 1: entity.name.function.kotlin - push: - - match: '\}|(?=\bget\b)|$' - pop: true - - match: \( - push: - - match: \) - pop: true - - include: parameters - - match: (=) - captures: - 1: keyword.operator.assignment.kotlin - push: - - match: (?=$|\bset\b) - pop: true - - include: expressions - - match: '\{' - push: - - match: '\}' - pop: true - - include: expressions - imports: - - match: '^\s*(import)\s+[^ $]+\s+(as)?' - captures: - 1: keyword.other.kotlin - 2: keyword.other.kotlin - keywords: - - match: \b(var|val|public|private|protected|abstract|final|sealed|enum|open|attribute|annotation|override|inline|vararg|in|out|internal|data|tailrec|operator|infix|const|yield|typealias|typeof|reified|suspend)\b - scope: storage.modifier.kotlin - - match: \b(try|catch|finally|throw)\b - scope: keyword.control.catch-exception.kotlin - - match: \b(if|else|while|for|do|return|when|where|break|continue)\b - scope: keyword.control.kotlin - - match: \b(in|is|!in|!is|as|as\?|assert)\b - scope: keyword.operator.kotlin - - match: (==|!=|===|!==|<=|>=|<|>) - scope: keyword.operator.comparison.kotlin - - match: (=) - scope: keyword.operator.assignment.kotlin - - match: (::) - scope: keyword.operator.kotlin - - match: (:) - scope: keyword.operator.declaration.kotlin - - match: \b(by)\b - scope: keyword.other.by.kotlin - - match: (\?\.) - scope: keyword.operator.safenav.kotlin - - match: (\.) - scope: keyword.operator.dot.kotlin - - match: (\?:) - scope: keyword.operator.elvis.kotlin - - match: (\-\-|\+\+) - scope: keyword.operator.increment-decrement.kotlin - - match: (\+=|\-=|\*=|\/=) - scope: keyword.operator.arithmetic.assign.kotlin - - match: (\.\.) - scope: keyword.operator.range.kotlin - - match: (\-|\+|\*|\/|%) - scope: keyword.operator.arithmetic.kotlin - - match: (!|&&|\|\|) - scope: keyword.operator.logical.kotlin - - match: (;) - scope: punctuation.terminator.kotlin - namespaces: - - match: \b(namespace)\b - scope: keyword.other.kotlin - - match: '\{' - push: - - match: '\}' - pop: true - - include: statements - parameters: - - match: (:) - captures: - 1: keyword.operator.declaration.kotlin - push: - - match: (?=,|\)|=) - pop: true - - include: types - - match: (=) - captures: - 1: keyword.operator.declaration.kotlin - push: - - match: (?=,|\)) - pop: true - - include: expressions - - include: keywords - - match: \w+ - scope: variable.parameter.function.kotlin - statements: - - include: namespaces - - include: typedefs - - include: classes - - include: functions - - include: variables - - include: getters-and-setters - - include: expressions - strings: - - match: '"""' - captures: - 0: punctuation.definition.string.begin.kotlin - push: - - meta_scope: string.quoted.third.kotlin - - match: '"""' - captures: - 0: punctuation.definition.string.end.kotlin - pop: true - - match: '(\$\w+|\$\{[^\}]+\})' - scope: variable.parameter.template.kotlin - - match: \\. - scope: constant.character.escape.kotlin - - match: '"' - captures: - 0: punctuation.definition.string.begin.kotlin - push: - - meta_scope: string.quoted.double.kotlin - - match: '"' - captures: - 0: punctuation.definition.string.end.kotlin - pop: true - - match: '(\$\w+|\$\{[^\}]+\})' - scope: variable.parameter.template.kotlin - - match: \\. - scope: constant.character.escape.kotlin - - match: "'" - captures: - 0: punctuation.definition.string.begin.kotlin - push: - - meta_scope: string.quoted.single.kotlin - - match: "'" - captures: - 0: punctuation.definition.string.end.kotlin - pop: true - - match: \\. - scope: constant.character.escape.kotlin - - match: "`" - captures: - 0: punctuation.definition.string.begin.kotlin - push: - - meta_scope: string.quoted.single.kotlin - - match: "`" - captures: - 0: punctuation.definition.string.end.kotlin - pop: true - typedefs: - - match: (?=\s*(?:type)) - push: - - match: (?=$) - pop: true - - match: \b(type)\b - scope: keyword.other.kotlin - - match: < - push: - - match: ">" - pop: true - - include: generics - - include: expressions - types: - - match: \b(Nothing|Any|Unit|String|CharSequence|Int|Boolean|Char|Long|Double|Float|Short|Byte|dynamic)\b - scope: storage.type.buildin.kotlin - - match: \b(IntArray|BooleanArray|CharArray|LongArray|DoubleArray|FloatArray|ShortArray|ByteArray)\b - scope: storage.type.buildin.array.kotlin - - match: \b(Array|Collection|List|Map|Set|MutableList|MutableMap|MutableSet|Sequence)<\b - captures: - 1: storage.type.buildin.collection.kotlin - push: - - match: ">" - pop: true - - include: types - - include: keywords - - match: \w+< - push: - - match: ">" - pop: true - - include: types - - include: keywords - - match: '\{' - push: - - match: '\}' - pop: true - - include: statements - - match: \( - push: - - match: \) - pop: true - - include: types - - match: (->) - scope: keyword.operator.declaration.kotlin - variables: - - match: (?=\s*\b(?:var|val)\b) - push: - - match: (?=:|=|(\b(by)\b)|$) - pop: true - - match: \b(var|val)\b - captures: - 1: keyword.other.kotlin - push: - - match: (?=:|=|(\b(by)\b)|$) - pop: true - - match: < - push: - - match: ">" - pop: true - - include: generics - - match: '([\.<\?>\w]+\.)?(\w+)' - captures: - 2: entity.name.variable.kotlin - - match: (:) - captures: - 1: keyword.operator.declaration.kotlin - push: - - match: (?==|$) - pop: true - - include: types - - include: getters-and-setters - - match: \b(by)\b - captures: - 1: keyword.other.kotlin - push: - - match: (?=$) - pop: true - - include: expressions - - match: (=) - captures: - 1: keyword.operator.assignment.kotlin - push: - - match: (?=$) - pop: true - - include: expressions - - include: getters-and-setters diff --git a/assets/syntaxes/02_Extra/LESS b/assets/syntaxes/02_Extra/LESS index 836b47ec..d076a4dd 160000 --- a/assets/syntaxes/02_Extra/LESS +++ b/assets/syntaxes/02_Extra/LESS @@ -1 +1 @@ -Subproject commit 836b47ec61a9c6a6445b4007e8353337fe63e2c9 +Subproject commit d076a4dd416bcdb155bacf90cda72f7bf1c8abbb diff --git a/assets/syntaxes/02_Extra/PureScript b/assets/syntaxes/02_Extra/PureScript index 5acebc18..1773f4fd 160000 --- a/assets/syntaxes/02_Extra/PureScript +++ b/assets/syntaxes/02_Extra/PureScript @@ -1 +1 @@ -Subproject commit 5acebc18503697be09df047591964e68e80fcf8e +Subproject commit 1773f4fddb08560d6bcb354901088e61e9ea0908 diff --git a/assets/syntaxes/02_Extra/typst-syntax-highlight b/assets/syntaxes/02_Extra/typst-syntax-highlight index 363f0e76..5f71d12f 160000 --- a/assets/syntaxes/02_Extra/typst-syntax-highlight +++ b/assets/syntaxes/02_Extra/typst-syntax-highlight @@ -1 +1 @@ -Subproject commit 363f0e767c938c615a14912c302db7936f025fc2 +Subproject commit 5f71d12fa129165bbe51aa867292555cdff6eb75 diff --git a/assets/syntaxes/02_Extra/vscode-wgsl b/assets/syntaxes/02_Extra/vscode-wgsl index acf26718..a285c38f 160000 --- a/assets/syntaxes/02_Extra/vscode-wgsl +++ b/assets/syntaxes/02_Extra/vscode-wgsl @@ -1 +1 @@ -Subproject commit acf26718d7a327377641e31d8f9a9dab376efa84 +Subproject commit a285c38f74eba2eb5c5a06be8d95b9f581338509 diff --git a/build/syntax_mapping.rs b/build/syntax_mapping.rs index 64be4bb9..b0db01eb 100644 --- a/build/syntax_mapping.rs +++ b/build/syntax_mapping.rs @@ -47,12 +47,49 @@ impl ToTokens for MappingTarget { } } -#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr)] +/// Helper type for deserializing a `Matcher` from either a plain string or a +/// `{ glob = "...", case_sensitive = true }` struct. +#[derive(Deserialize)] +#[serde(untagged)] +enum RawMatcher { + Simple(String), + Full { + glob: String, + case_sensitive: Option, + }, +} + +/// Whether a glob pattern should be matched case-sensitively or case-insensitively. +/// +/// Mirrors the runtime `Case` type in `src/syntax_mapping.rs`. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +enum Case { + Sensitive, + Insensitive, +} +impl ToTokens for Case { + fn to_tokens(&self, tokens: &mut TokenStream) { + let t = match self { + Self::Sensitive => quote! { Case::Sensitive }, + Self::Insensitive => quote! { Case::Insensitive }, + }; + tokens.append_all(t); + } +} + /// A single matcher. /// /// Codegen converts this into a `Lazy>`. -struct Matcher(Vec); -/// Parse a matcher. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize)] +#[serde(try_from = "RawMatcher")] +struct Matcher { + segments: Vec, + /// Whether the glob pattern should be matched case-sensitively. + /// + /// Defaults to `Case::Insensitive` for backwards compatibility. + case: Case, +} +/// Parse the glob pattern of a matcher. /// /// Note that this implementation is rather strict: it will greedily interpret /// every valid environment variable replacement as such, then immediately @@ -66,68 +103,95 @@ struct Matcher(Vec); /// /// Revision history: /// - 2024-02-20: allow `{` and `}` (glob brace expansion) -impl FromStr for Matcher { - type Err = anyhow::Error; - fn from_str(s: &str) -> Result { - use MatcherSegment as Seg; - static VAR_REGEX: Lazy = Lazy::new(|| Regex::new(r"\$\{([\w\d_]+)\}").unwrap()); +fn parse_glob(s: &str) -> Result, anyhow::Error> { + use MatcherSegment as Seg; + static VAR_REGEX: Lazy = Lazy::new(|| Regex::new(r"\$\{([\w\d_]+)\}").unwrap()); - let mut segments = vec![]; - let mut text_start = 0; - for capture in VAR_REGEX.captures_iter(s) { - let match_0 = capture.get(0).unwrap(); + let mut segments = vec![]; + let mut text_start = 0; + for capture in VAR_REGEX.captures_iter(s) { + let match_0 = capture.get(0).unwrap(); - // text before this var - let text_end = match_0.start(); - segments.push(Seg::Text(s[text_start..text_end].into())); - text_start = match_0.end(); + // text before this var + let text_end = match_0.start(); + segments.push(Seg::Text(s[text_start..text_end].into())); + text_start = match_0.end(); - // this var - segments.push(Seg::Env(capture.get(1).unwrap().as_str().into())); + // this var + segments.push(Seg::Env(capture.get(1).unwrap().as_str().into())); + } + // possible trailing text + segments.push(Seg::Text(s[text_start..].into())); + + // cleanup empty text segments + let non_empty_segments = segments + .into_iter() + .filter(|seg| seg.text().map(|t| !t.is_empty()).unwrap_or(true)) + .collect_vec(); + + // sanity check + if non_empty_segments + .windows(2) + .any(|segs| segs[0].is_text() && segs[1].is_text()) + { + unreachable!("Parsed into consecutive text segments: {non_empty_segments:?}"); + } + + // guard empty case + if non_empty_segments.is_empty() { + bail!(r#"Parsed an empty matcher: "{s}""#); + } + + // guard variable syntax leftover fragments + if non_empty_segments + .iter() + .filter_map(Seg::text) + .any(|t| t.contains('$')) + { + bail!(r#"Invalid matcher: "{s}""#); + } + + Ok(non_empty_segments) +} +impl TryFrom for Matcher { + type Error = anyhow::Error; + fn try_from(raw: RawMatcher) -> Result { + const DEFAULT_CASE: Case = Case::Insensitive; + match &raw { + RawMatcher::Simple(s) => { + let segments = parse_glob(s)?; + Ok(Self { + segments, + case: DEFAULT_CASE, + }) + } + RawMatcher::Full { + glob, + case_sensitive, + } => { + let segments = parse_glob(glob)?; + let case = match case_sensitive { + None => DEFAULT_CASE, + Some(false) => Case::Insensitive, + Some(true) => Case::Sensitive, + }; + Ok(Self { segments, case }) + } } - // possible trailing text - segments.push(Seg::Text(s[text_start..].into())); - - // cleanup empty text segments - let non_empty_segments = segments - .into_iter() - .filter(|seg| seg.text().map(|t| !t.is_empty()).unwrap_or(true)) - .collect_vec(); - - // sanity check - if non_empty_segments - .windows(2) - .any(|segs| segs[0].is_text() && segs[1].is_text()) - { - unreachable!("Parsed into consecutive text segments: {non_empty_segments:?}"); - } - - // guard empty case - if non_empty_segments.is_empty() { - bail!(r#"Parsed an empty matcher: "{s}""#); - } - - // guard variable syntax leftover fragments - if non_empty_segments - .iter() - .filter_map(Seg::text) - .any(|t| t.contains('$')) - { - bail!(r#"Invalid matcher: "{s}""#); - } - - Ok(Self(non_empty_segments)) } } impl ToTokens for Matcher { fn to_tokens(&self, tokens: &mut TokenStream) { - let t = match self.0.as_slice() { + let case = &self.case; + let t = match self.segments.as_slice() { [] => unreachable!("0-length matcher should never be created"), [MatcherSegment::Text(text)] => { - quote! { Lazy::new(|| Some(build_matcher_fixed(#text))) } + quote! { Lazy::new(|| Some(build_matcher_fixed(#text, #case))) } } // parser logic ensures that this case can only happen when there are dynamic segments - segs @ [_, ..] => quote! { Lazy::new(|| build_matcher_dynamic(&[ #(#segs),* ])) }, + segs @ [_, ..] => { + quote! { Lazy::new(|| build_matcher_dynamic(&[ #(#segs),* ], #case)) } + } }; tokens.append_all(t); } @@ -175,6 +239,7 @@ impl MatcherSegment { /// A struct that models a single .toml file in /src/syntax_mapping/builtins/. #[derive(Clone, Debug, Deserialize)] struct MappingDefModel { + #[serde(default)] mappings: IndexMap>, } impl MappingDefModel { diff --git a/doc/assets.md b/doc/assets.md index f9297d42..e073cff0 100644 --- a/doc/assets.md +++ b/doc/assets.md @@ -97,7 +97,7 @@ The following files have been manually modified after converting from a `.tmLang as it is not kept in a standalone repository. The file is generated from https://github.com/open-policy-agent/opa/blob/master/misc/syntax/textmate/Rego.tmLanguage * `SML.sublime_syntax` has been added manually from - https://github.com/seanjames777/SML-Language-Definitiona as it is not + https://github.com/seanjames777/SML-Language-Definition as it is not kept in a standalone repository. The file generated is from https://github.com/seanjames777/SML-Language-Definition/blob/master/sml.tmLanguage * `Cabal.sublime_syntax` has been added manually from diff --git a/doc/long-help.txt b/doc/long-help.txt index 82878acd..5b549e07 100644 --- a/doc/long-help.txt +++ b/doc/long-help.txt @@ -77,7 +77,8 @@ Options: --terminal-width Explicitly set the width of the terminal instead of determining it automatically. If prefixed with '+' or '-', the value will be treated as an offset to the actual terminal - width. See also: '--wrap'. + width. This can also be configured via the BAT_WIDTH environment variable (e.g. export + BAT_WIDTH="100"). See also: '--wrap'. -n, --number Only show line numbers, no other decorations. This is an alias for '--style=numbers' @@ -163,6 +164,14 @@ Options: escape sequences unless the syntax highlighting language is plain text. Possible values: auto, always, *never*. + --sanitize + 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 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 diff --git a/doc/sponsors.md b/doc/sponsors.md index 24509077..641e5205 100644 --- a/doc/sponsors.md +++ b/doc/sponsors.md @@ -10,5 +10,3 @@ No issue will have a different priority based on sponsorship status of the reporter. Contributions from anybody are most welcomed, please see our [`CONTRIBUTING.md`](../CONTRIBUTING.md) guide. - -If you want to see our biggest sponsors, check the top of [`README.md`](../README.md#sponsors). diff --git a/doc/sponsors/graphite-logo.jpeg b/doc/sponsors/graphite-logo.jpeg deleted file mode 100644 index 00443818..00000000 Binary files a/doc/sponsors/graphite-logo.jpeg and /dev/null differ diff --git a/doc/sponsors/warp-logo.png b/doc/sponsors/warp-logo.png deleted file mode 100644 index f99dd38c..00000000 Binary files a/doc/sponsors/warp-logo.png and /dev/null differ diff --git a/doc/sponsors/warp-pack-header.png b/doc/sponsors/warp-pack-header.png deleted file mode 100644 index ed7efe6a..00000000 Binary files a/doc/sponsors/warp-pack-header.png and /dev/null differ diff --git a/src/assets.rs b/src/assets.rs index 70570d2b..e6adc123 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -158,7 +158,9 @@ impl HighlightingAssets { let syntax_match = mapping.get_syntax_for(path); if let Some(MappingTarget::MapToUnknown) = syntax_match { - return Err(Error::UndetectedSyntax(path.to_string_lossy().into())); + return Err(Error::UndetectedSyntax( + crate::preprocessor::sanitize_for_terminal(&path.to_string_lossy()), + )); } if let Some(MappingTarget::MapTo(syntax_name)) = syntax_match { @@ -175,13 +177,17 @@ impl HighlightingAssets { ) { (Some(syntax), _) => Ok(syntax), - (_, Some(MappingTarget::MapExtensionToUnknown)) => { - Err(Error::UndetectedSyntax(path.to_string_lossy().into())) - } + (_, Some(MappingTarget::MapExtensionToUnknown)) => Err(Error::UndetectedSyntax( + crate::preprocessor::sanitize_for_terminal(&path.to_string_lossy()), + )), _ => self .get_syntax_for_file_extension(file_name, &mapping.ignored_suffixes)? - .ok_or_else(|| Error::UndetectedSyntax(path.to_string_lossy().into())), + .ok_or_else(|| { + Error::UndetectedSyntax(crate::preprocessor::sanitize_for_terminal( + &path.to_string_lossy(), + )) + }), } } @@ -341,15 +347,15 @@ impl HighlightingAssets { file_name: &OsStr, ignored_suffixes: &IgnoredSuffixes, ) -> Result>> { - let mut syntax = self.find_syntax_by_extension(Path::new(file_name).extension())?; - if syntax.is_none() { - syntax = - ignored_suffixes.try_with_stripped_suffix(file_name, |stripped_file_name| { - // Note: recursion - self.get_syntax_for_file_extension(stripped_file_name, ignored_suffixes) - })?; + let stripped = + ignored_suffixes.try_with_stripped_suffix(file_name, |stripped_file_name| { + self.get_syntax_for_file_extension(stripped_file_name, ignored_suffixes) + .map(Some) + })?; + match stripped { + Some(syntax) => Ok(syntax), + None => self.find_syntax_by_extension(Path::new(file_name).extension()), } - Ok(syntax) } fn get_first_line_syntax( @@ -680,6 +686,48 @@ mod tests { ); } + #[test] + fn syntax_detection_ignored_suffix_falls_back_to_first_line() { + let mut test = SyntaxDetectionTest::new(); + + // By default a `.txt` file uses Plain Text, even with a shebang: the + // `.txt` extension wins and first-line detection is not reached. + assert_eq!( + test.syntax_for_file_with_content("test.txt", "#!/usr/bin/env bash"), + "Plain Text" + ); + + // Once `.txt` is an ignored suffix it is stripped before detection. The + // stripped name (`test`) has no extension, so detection falls back to the + // first line -- even though `.txt` is itself a registered extension that + // would otherwise match as Plain Text. See #2745. + test.syntax_mapping.insert_ignored_suffix(".txt"); + assert_eq!( + test.syntax_for_file_with_content("test.txt", "#!/usr/bin/env bash"), + "Bourne Again Shell (bash)" + ); + assert_eq!( + test.syntax_for_file_with_content("test.txt", " Plain Text match here. + assert_eq!( + test.syntax_for_file_with_content("notes.txt", "just some prose"), + "!no syntax!" + ); + + // Stripping that exposes a real extension still works: `.dev` is ignored, + // and the remaining `.json` extension is detected as usual. + test.syntax_mapping.insert_ignored_suffix(".dev"); + assert_eq!( + test.syntax_for_file_with_content("config.json.dev", ""), + "JSON" + ); + } + #[test] fn syntax_detection_is_case_insensitive() { let mut test = SyntaxDetectionTest::new(); diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index dddb5559..cc37075c 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -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::("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::("sanitize") + .map(|s| s.as_str()), + "--sanitize", + ); + let strip_ansi = parse_strip_ansi_value( + self.matches + .get_one::("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::("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(), @@ -590,7 +615,17 @@ impl App { // Plain if `--plain` is specified at least once. if self.matches.get_count("plain") > 0 { - return Some(StyleComponents(HashSet::from([StyleComponent::Plain]))); + let mut components = HashSet::from([StyleComponent::Plain]); + // When --diff is active, preserve change markers and snip separators + // so that diff output remains visually useful. + if self.matches.try_contains_id("diff").unwrap_or_default() + && self.matches.get_flag("diff") + { + #[cfg(feature = "git")] + components.insert(StyleComponent::Changes); + components.insert(StyleComponent::Snip); + } + return Some(StyleComponents(components)); } // Default behavior. @@ -635,7 +670,7 @@ impl App { Ok(styled_components) } - fn theme_options(&self) -> ThemeOptions { + pub(crate) fn theme_options(&self) -> ThemeOptions { Self::theme_options_from_matches(&self.matches) } diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs index 3636f081..decd0364 100644 --- a/src/bin/bat/clap_app.rs +++ b/src/bin/bat/clap_app.rs @@ -240,6 +240,7 @@ pub fn build_app(interactive_output: bool) -> Command { .arg( Arg::new("terminal-width") .long("terminal-width") + .overrides_with("terminal-width") .value_name("width") .hide_short_help(true) .allow_hyphen_values(true) @@ -255,10 +256,13 @@ pub fn build_app(interactive_output: bool) -> Command { }) .map_err(|e| e.to_string()) }) - .help( + .help("Explicitly set the width of the terminal instead of determining it automatically.") + .long_help( "Explicitly set the width of the terminal instead of determining it \ automatically. If prefixed with '+' or '-', the value will be treated \ - as an offset to the actual terminal width. See also: '--wrap'.", + as an offset to the actual terminal width. This can also be configured \ + via the BAT_WIDTH environment variable (e.g. export BAT_WIDTH=\"100\"). \ + See also: '--wrap'.", ), ) .arg( @@ -476,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") diff --git a/src/bin/bat/config.rs b/src/bin/bat/config.rs index 77d691c3..3e2fb72e 100644 --- a/src/bin/bat/config.rs +++ b/src/bin/bat/config.rs @@ -153,6 +153,7 @@ fn get_args_from_str(content: &str) -> Result, shell_words::ParseE pub fn get_args_from_env_vars() -> Vec { [ ("--tabs", "BAT_TABS"), + ("--terminal-width", "BAT_WIDTH"), ("--theme", bat::theme::env::BAT_THEME), ("--theme-dark", bat::theme::env::BAT_THEME_DARK), ("--theme-light", bat::theme::env::BAT_THEME_LIGHT), diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs index 7ca08b9d..705a28b9 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -17,7 +17,6 @@ use std::path::Path; use std::process; use bat::output::{OutputHandle, OutputType}; -use bat::theme::DetectColorScheme; use nu_ansi_term::Color::Green; use nu_ansi_term::Style; @@ -39,7 +38,7 @@ use bat::{ error::*, input::Input, style::{StyleComponent, StyleComponents}, - theme::{color_scheme, default_theme, ColorScheme}, + theme::{default_theme, theme, ColorScheme, ThemeOptions}, MappingTarget, PagingMode, }; @@ -153,7 +152,14 @@ pub fn get_languages(config: &Config, cache_dir: &Path) -> Result { let comma_separator = ", "; let separator = " "; // Line-wrapping for the possible file extension overflow. - let desired_width = config.term_width - longest - separator.len(); + // Clamp instead of subtracting: a tiny `--terminal-width` (smaller than the + // longest language name) would otherwise underflow `usize` and wrap to a huge + // value, silently disabling wrapping (and panicking in debug/overflow-checked + // builds). Mirrors the `saturating_sub` fix applied to `print_snip` in #3804. + let desired_width = config + .term_width + .saturating_sub(longest) + .saturating_sub(separator.len()); let style = if config.colored_output { Green.normal() @@ -197,7 +203,7 @@ pub fn list_themes( cfg: &Config, config_dir: &Path, cache_dir: &Path, - detect_color_scheme: DetectColorScheme, + theme_options: ThemeOptions, ) -> Result<()> { let assets = assets_from_cache_or_binary(cfg.use_custom_assets, cache_dir)?; let mut config = cfg.clone(); @@ -206,7 +212,7 @@ pub fn list_themes( config.language = Some("Rust"); config.style_components = StyleComponents(style); - let default_theme_name = default_theme(color_scheme(detect_color_scheme).unwrap_or_default()); + let default_theme_name = theme(theme_options).to_string(); let mut buf = String::new(); let mut handle = OutputHandle::FmtWrite(&mut buf); @@ -259,7 +265,9 @@ pub fn list_themes( fn set_terminal_title_to(new_terminal_title: String) { let osc_command_for_setting_terminal_title = "\x1b]0;"; let osc_end_command = "\x07"; - print!("{osc_command_for_setting_terminal_title}{new_terminal_title}{osc_end_command}"); + // Prevent BEL/ESC/C1 bytes in the title from terminating or nesting the OSC. + let safe_title = bat::sanitize_for_terminal(&new_terminal_title); + print!("{osc_command_for_setting_terminal_title}{safe_title}{osc_end_command}"); io::stdout().flush().unwrap(); } @@ -285,7 +293,28 @@ fn run_controller(inputs: Vec, config: &Config, cache_dir: &Path) -> Resu #[cfg(feature = "bugreport")] fn invoke_bugreport(app: &App, cache_dir: &Path) { - use bugreport::{bugreport, collector::*, format::Markdown}; + use bugreport::{bugreport, collector::*, format::Markdown, report::ReportEntry}; + + struct ColorSchemeCollector; + + impl Collector for ColorSchemeCollector { + fn description(&self) -> &str { + "Detected terminal color scheme" + } + + fn collect( + &mut self, + _: &bugreport::CrateInfo, + ) -> std::result::Result { + let color_scheme = bat::theme::color_scheme(bat::theme::DetectColorScheme::Always); + let text = match color_scheme { + Some(bat::theme::ColorScheme::Dark) => "dark", + Some(bat::theme::ColorScheme::Light) => "light", + None => "not detected", + }; + Ok(ReportEntry::Text(text.to_string())) + } + } let pager = bat::config::get_pager_executable( app.matches.get_one::("pager").map(|s| s.as_str()), ) @@ -307,6 +336,9 @@ fn invoke_bugreport(app: &App, cache_dir: &Path) { "BAT_STYLE", "BAT_TABS", "BAT_THEME", + "BAT_WIDTH", + bat::theme::env::BAT_THEME_DARK, + bat::theme::env::BAT_THEME_LIGHT, "COLORTERM", "LANG", "LC_ALL", @@ -326,6 +358,7 @@ fn invoke_bugreport(app: &App, cache_dir: &Path) { custom_assets_metadata, )) .info(DirectoryEntries::new("Custom assets", cache_dir)) + .info(ColorSchemeCollector) .info(CompileTimeInformation::default()); #[cfg(feature = "paging")] @@ -394,14 +427,12 @@ fn run() -> Result { if app.matches.get_flag("list-languages") { let languages: String = get_languages(&config, cache_dir)?; let inputs: Vec = 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, DetectColorScheme::default())?; + list_themes(&config, config_dir, cache_dir, app.theme_options())?; Ok(true) } else if app.matches.get_flag("config-file") { println!("{}", config_file().to_string_lossy()); diff --git a/src/config.rs b/src/config.rs index 97720fb5..121097d9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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, diff --git a/src/diff.rs b/src/diff.rs index 78d20c30..06d25927 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -1,12 +1,13 @@ #![cfg(feature = "git")] +use gix::diff::blob::pipeline::{Mode, WorktreeRoots}; +use gix::diff::blob::{Algorithm, HunkIter, ResourceKind}; +use gix::object::tree::EntryKind; +use path_abs::PathInfo; use std::collections::HashMap; -use std::fs; use std::path::Path; -use git2::{DiffOptions, IntoCString, Repository}; - -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum LineChange { Added, RemovedAbove, @@ -16,68 +17,181 @@ pub enum LineChange { pub type LineChanges = HashMap; -pub fn get_git_diff(filename: &Path) -> Option { - let repo = Repository::discover(filename).ok()?; - - let repo_path_absolute = fs::canonicalize(repo.workdir()?).ok()?; - - let filepath_absolute = fs::canonicalize(filename).ok()?; - let filepath_relative_to_repo = filepath_absolute.strip_prefix(&repo_path_absolute).ok()?; - - let mut diff_options = DiffOptions::new(); - let pathspec = filepath_relative_to_repo.into_c_string().ok()?; - diff_options.pathspec(pathspec); - diff_options.context_lines(0); - - let diff = repo - .diff_index_to_workdir(None, Some(&mut diff_options)) - .ok()?; - - let mut line_changes: LineChanges = HashMap::new(); - - let mark_section = - |line_changes: &mut LineChanges, start: u32, end: i32, change: LineChange| { - for line in start..=end as u32 { - line_changes.insert(line, change); +fn collect_changes_from_hunks(hunks: HunkIter) -> Option { + let mut changes: LineChanges = HashMap::new(); + for hunk in hunks { + if hunk.before.is_empty() && !hunk.after.is_empty() { + for line in hunk.after { + changes.insert(line + 1, LineChange::Added); } - }; - - let _ = diff.foreach( - &mut |_, _| true, - None, - Some(&mut |delta, hunk| { - let path = delta.new_file().path().unwrap_or_else(|| Path::new("")); - - if filepath_relative_to_repo != path { - return false; - } - - let old_lines = hunk.old_lines(); - let new_start = hunk.new_start(); - let new_lines = hunk.new_lines(); - let new_end = (new_start + new_lines) as i32 - 1; - - if old_lines == 0 && new_lines > 0 { - mark_section(&mut line_changes, new_start, new_end, LineChange::Added); - } else if new_lines == 0 && old_lines > 0 { - if new_start == 0 { - mark_section(&mut line_changes, 1, 1, LineChange::RemovedAbove); - } else { - mark_section( - &mut line_changes, - new_start, - new_start as i32, - LineChange::RemovedBelow, - ); - } + } else if hunk.after.is_empty() && !hunk.before.is_empty() { + if hunk.after.start == 0 { + changes.insert(1, LineChange::RemovedAbove); } else { - mark_section(&mut line_changes, new_start, new_end, LineChange::Modified); + changes.insert(hunk.after.start, LineChange::RemovedBelow); } + } else { + for line in hunk.after { + changes.insert(line + 1, LineChange::Modified); + } + } + } - true - }), - None, + Some(changes) +} + +pub fn get_git_diff(filename: &Path) -> Option { + let filepath_absolute = filename.canonicalize().ok()?; + let repository = gix::discover(filepath_absolute.parent().ok()?).ok()?; + let repo_path_absolute = repository.workdir()?.canonicalize().ok()?; + let filepath_relative_to_repo = gix::path::to_unix_separators_on_windows(gix::path::into_bstr( + filepath_absolute.strip_prefix(&repo_path_absolute).ok()?, + )); + let index = repository.index_or_load_from_head_or_empty().ok()?; + let index_entry = index.entry_by_path(filepath_relative_to_repo.as_ref())?; + let mut cache = repository + .diff_resource_cache( + Mode::ToGit, + WorktreeRoots { + old_root: None, + new_root: repository.workdir().map(Path::to_path_buf), + }, + ) + .ok()?; + cache + .set_resource( + index_entry.id, + index_entry.mode.to_tree_entry_mode()?.kind(), + filepath_relative_to_repo.as_ref(), + ResourceKind::OldOrSource, + &repository, + ) + .ok()?; + cache + .set_resource( + repository.object_hash().null(), + EntryKind::Blob, + filepath_relative_to_repo.as_ref(), + ResourceKind::NewOrDestination, + &repository, + ) + .ok()?; + let diff = gix::diff::blob::diff_with_slider_heuristics( + Algorithm::Histogram, + &cache.prepare_diff().ok()?.interned_input(), ); - Some(line_changes) + collect_changes_from_hunks(diff.hunks()) +} + +#[cfg(test)] +mod tests { + use super::{get_git_diff, LineChange}; + use std::path::{Path, PathBuf}; + use std::process::Command; + + fn git(repo: &Path, args: &[&str]) { + let output = Command::new("git") + .args(args) + .current_dir(repo) + .output() + .expect("git command can run"); + assert!( + output.status.success(), + "git {args:?} failed\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + + fn setup_repo() -> tempfile::TempDir { + let dir = tempfile::tempdir().expect("can create temporary directory"); + let repo = dir.path(); + git(repo, &["init"]); + git(repo, &["config", "user.email", "test@test.test"]); + git(repo, &["config", "user.name", "Test"]); + dir + } + + fn create_and_track_file(repo: &tempfile::TempDir, filename: &str) -> PathBuf { + let filepath = repo.path().join(filename); + std::fs::write(&filepath, "file\n").expect("can write file"); + git(repo.path(), &["add", filename]); + git(repo.path(), &["commit", "-m", "initial"]); + + filepath.to_owned() + } + + #[test] + fn not_a_git_repository() { + let dir = tempfile::tempdir().expect("can create temporary directory"); + let file = dir.path().join("file.txt"); + std::fs::write(&file, "line 1\n").expect("can write file"); + + assert_eq!(get_git_diff(&file), None); + } + + #[test] + fn diff_is_calculated_against_index() { + let repo = setup_repo(); + let file = create_and_track_file(&repo, "file.txt"); + + std::fs::write(&file, "line 1\nline 2 modified\n").expect("can write file"); + // Add modified file to index -> should find 0 changes but not none + git(repo.path(), &["add", "file.txt"]); + + assert_eq!(get_git_diff(&file).expect("empty map").len(), 0); + } + + #[test] + fn diff_is_calculated_against_index_2() { + let repo = setup_repo(); + let file = create_and_track_file(&repo, "file.txt"); + + std::fs::write(&file, "line 1\nline 2 modified\n").expect("can write file"); + git(repo.path(), &["add", "file.txt"]); + // modify the second line again which should show a single change + std::fs::write(&file, "line 1\nline 2 modified again\n").expect("can write file"); + + assert_eq!(get_git_diff(&file).expect("one change").len(), 1); + } + + #[test] + fn diff_is_calculated_correctly() { + let repo = setup_repo(); + create_and_track_file(&repo, "committed.txt"); + let filename = "committed2.txt"; + let file = create_and_track_file(&repo, filename); + + std::fs::write(&file, "file\nline 2 added\n").expect("can write file"); + let mut changes = get_git_diff(&file).expect("multiple changes"); + assert_eq!(changes.get(&2), Some(&LineChange::Added)); + git(repo.path(), &["add", filename]); + + std::fs::write(&file, "file\nline 2 modified\n").expect("can write file"); + changes = get_git_diff(&file).expect("multiple changes"); + assert_eq!(changes.get(&2), Some(&LineChange::Modified)); + git(repo.path(), &["add", filename]); + + std::fs::write(&file, "line 2 modified\nline 3 added").expect("can write file"); + changes = get_git_diff(&file).expect("multiple changes"); + assert_eq!(changes.get(&1), Some(&LineChange::RemovedAbove)); + assert_eq!(changes.get(&2), Some(&LineChange::Added)); + } + + #[test] + fn faulty_git_version_does_not_panic() { + let repo = setup_repo(); + let file = create_and_track_file(&repo, "file.txt"); + std::fs::write(&file, "line 1\nline 2 modified\n").expect("can write file"); + // changes are detected + assert_eq!(get_git_diff(&file).expect("one change").len(), 2); + // write invalid repositoryformatversion + git( + repo.path(), + &["config", "core.repositoryformatversion", "one"], + ); + // changes are no longer detected + assert_eq!(get_git_diff(&file), None); + } } diff --git a/src/input.rs b/src/input.rs index 29846abe..facd64bf 100644 --- a/src/input.rs +++ b/src/input.rs @@ -207,7 +207,7 @@ impl<'a> Input<'a> { kind: OpenedInputKind::StdIn, description, metadata: self.metadata, - reader: InputReader::new(stdin), + reader: InputReader::try_new(stdin)?, }) } @@ -216,34 +216,34 @@ impl<'a> Input<'a> { description, metadata: self.metadata, reader: { - let mut file = File::open(&path) - .map_err(|e| format!("'{}': {e}", path.to_string_lossy()))?; + let path_display = + crate::preprocessor::sanitize_for_terminal(&path.to_string_lossy()); + let mut file = + File::open(&path).map_err(|e| format!("'{path_display}': {e}"))?; if file.metadata()?.is_dir() { - return Err(format!("'{}' is a directory.", path.to_string_lossy()).into()); + return Err(format!("'{path_display}' is a directory.").into()); } if let Some(stdout) = stdout_identifier { - let input_identifier = Identifier::try_from(file).map_err(|e| { - format!("{}: Error identifying file: {e}", path.to_string_lossy()) - })?; + let input_identifier = Identifier::try_from(file) + .map_err(|e| format!("{path_display}: Error identifying file: {e}"))?; if stdout.surely_conflicts_with(&input_identifier) { return Err(format!( - "IO circle detected. The input from '{}' is also an output. Aborting to avoid infinite loop.", - path.to_string_lossy() + "IO circle detected. The input from '{path_display}' is also an output. Aborting to avoid infinite loop.", ) .into()); } file = input_identifier.into_inner().expect("The file was lost in the clircle::Identifier, this should not have happened..."); } - InputReader::new(BufReader::new(file)) + InputReader::try_new(BufReader::new(file))? }, }), InputKind::CustomReader(reader) => Ok(OpenedInput { description, kind: OpenedInputKind::CustomReader, metadata: self.metadata, - reader: InputReader::new(BufReader::new(reader)), + reader: InputReader::try_new(BufReader::new(reader))?, }), } } @@ -257,28 +257,29 @@ pub(crate) struct InputReader<'a> { } impl<'a> InputReader<'a> { - pub(crate) fn new(mut reader: R) -> InputReader<'a> { - let mut first_line = vec![]; - reader.read_until(b'\n', &mut first_line).ok(); + #[cfg(test)] + pub(crate) fn new(reader: R) -> InputReader<'a> { + Self::try_new(reader).expect("reading the first line failed") + } - let content_type = if first_line.is_empty() { - None - } else { - Some(content_inspector::inspect(&first_line[..])) - }; + pub(crate) fn try_new(mut reader: R) -> io::Result> { + let mut first_line = vec![]; + reader.read_until(b'\n', &mut first_line)?; + + let content_type = inspect_content_type(&first_line); if content_type == Some(ContentType::UTF_16LE) { - read_utf16_line(&mut reader, &mut first_line, 0x00, 0x0A).ok(); + read_utf16_line(&mut reader, &mut first_line, 0x00, 0x0A)?; } else if content_type == Some(ContentType::UTF_16BE) { - read_utf16_line(&mut reader, &mut first_line, 0x0A, 0x00).ok(); + read_utf16_line(&mut reader, &mut first_line, 0x0A, 0x00)?; } - InputReader { + Ok(InputReader { inner: Box::new(reader), first_line, content_type, unbuffered: false, - } + }) } pub(crate) fn read_line(&mut self, buf: &mut Vec) -> io::Result { @@ -319,6 +320,25 @@ impl<'a> InputReader<'a> { } } +fn inspect_content_type(first_line: &[u8]) -> Option { + if first_line.is_empty() { + return None; + } + + let content_type = content_inspector::inspect(first_line); + if content_type == ContentType::UTF_8 && has_zip_signature(first_line) { + Some(ContentType::BINARY) + } else { + Some(content_type) + } +} + +fn has_zip_signature(bytes: &[u8]) -> bool { + [b"PK\x03\x04", b"PK\x05\x06", b"PK\x07\x08"] + .into_iter() + .any(|signature| bytes.starts_with(signature)) +} + fn read_utf16_line( reader: &mut R, buf: &mut Vec, @@ -374,6 +394,43 @@ fn basic() { assert!(buffer.is_empty()); } +#[test] +fn zip_magic_headers_are_treated_as_binary() { + for content in [b"PK\x03\x04hello", b"PK\x05\x06hello", b"PK\x07\x08hello"] { + let reader = InputReader::new(&content[..]); + assert_eq!(Some(ContentType::BINARY), reader.content_type); + } +} + +#[test] +fn non_zip_pk_prefix_is_not_treated_as_binary() { + assert_eq!( + Some(ContentType::UTF_8), + inspect_content_type(b"PK\x03\x03hello") + ); +} + +#[test] +fn input_open_returns_initial_read_errors() { + struct FailingRead; + + impl Read for FailingRead { + fn read(&mut self, _buf: &mut [u8]) -> io::Result { + Err(io::Error::other("initial read failed")) + } + } + + let input = Input::from_reader(Box::new(FailingRead)); + let result = input.open(io::empty(), None); + + assert!(result.is_err()); + assert!(result + .err() + .unwrap() + .to_string() + .contains("initial read failed")); +} + #[test] fn utf16le() { let content = b"\xFF\xFE\x73\x00\x0A\x00\x64\x00"; diff --git a/src/lessopen.rs b/src/lessopen.rs index 966bc2c0..ad8fe69a 100644 --- a/src/lessopen.rs +++ b/src/lessopen.rs @@ -14,6 +14,53 @@ use crate::{ input::{Input, InputKind, InputReader, OpenedInput, OpenedInputKind}, }; +/// Wrap `s` in POSIX single quotes. An embedded `'` is encoded as `'\''` +/// (close, escaped quote, reopen) since `'...'` has no escape character. +fn shell_quote(s: &str) -> String { + if !s.contains('\'') { + let mut quoted = String::with_capacity(s.len() + 2); + quoted.push('\''); + quoted.push_str(s); + quoted.push('\''); + return quoted; + } + let mut quoted = String::with_capacity(s.len() + 2); + quoted.push('\''); + for c in s.chars() { + if c == '\'' { + quoted.push_str("'\\''"); + } else { + quoted.push(c); + } + } + quoted.push('\''); + quoted +} + +/// Substitute the first `%s` in a $LESSOPEN/$LESSCLOSE template with a quoted value. +fn shell_substitute(template: &str, replacement: &str) -> String { + template.replacen("%s", &shell_quote(replacement), 1) +} + +/// Substitute the first two `%s` occurrences positionally in one pass; chaining +/// `shell_substitute` twice would mis-target if `first` itself contains `%s`. +fn shell_substitute_two(template: &str, first: &str, second: &str) -> String { + let mut out = String::with_capacity(template.len() + first.len() + second.len() + 4); + let mut remaining = template; + let mut to_substitute = [Some(first), Some(second)]; + for slot in &mut to_substitute { + if let Some(idx) = remaining.find("%s") { + out.push_str(&remaining[..idx]); + out.push_str(&shell_quote(slot.take().unwrap())); + remaining = &remaining[idx + 2..]; + } else { + break; + } + } + out.push_str(remaining); + out +} + /// Preprocess files and/or stdin using $LESSOPEN and $LESSCLOSE pub(crate) struct LessOpenPreprocessor { lessopen: String, @@ -87,7 +134,7 @@ impl LessOpenPreprocessor { None => return input.open(stdin, stdout_identifier), }; - let mut lessopen_command = shell(self.lessopen.replacen("%s", path_str, 1)); + let mut lessopen_command = shell(shell_substitute(&self.lessopen, path_str)); lessopen_command.stdout(Stdio::piped()); let lessopen_output = match lessopen_command.execute_output() { @@ -122,7 +169,7 @@ impl LessOpenPreprocessor { let mut stdin_buffer = Vec::new(); stdin.read_to_end(&mut stdin_buffer)?; - let mut lessopen_command = shell(self.lessopen.replacen("%s", "-", 1)); + let mut lessopen_command = shell(shell_substitute(&self.lessopen, "-")); lessopen_command.stdout(Stdio::piped()); let lessopen_output = match lessopen_command.execute_input_output(&stdin_buffer) @@ -155,7 +202,7 @@ impl LessOpenPreprocessor { Ok(OpenedInput { kind, - reader: InputReader::new(BufReader::new( + reader: InputReader::try_new(BufReader::new( if matches!(self.kind, LessOpenKind::TempFile) { let lessopen_string = match String::from_utf8(lessopen_stdout) { Ok(string) => string, @@ -181,7 +228,7 @@ impl LessOpenPreprocessor { lessclose: self .lessclose .as_ref() - .map(|s| s.replacen("%s", &path_str, 1).replacen("%s", &stdout, 1)), + .map(|s| shell_substitute_two(s, &path_str, &stdout)), } } else { Preprocessed { @@ -189,10 +236,10 @@ impl LessOpenPreprocessor { lessclose: self .lessclose .as_ref() - .map(|s| s.replacen("%s", &path_str, 1).replacen("%s", "-", 1)), + .map(|s| shell_substitute_two(s, &path_str, "-")), } }, - )), + ))?, metadata: input.metadata, description: input.description, }) @@ -261,7 +308,7 @@ impl Drop for Preprocessed { } }; - if lessclose_output.status.success() { + if !lessclose_output.status.success() { bat_warning!("$LESSCLOSE exited with nonzero exit code",) }; } @@ -384,4 +431,65 @@ mod tests { Ok(()) } + + #[test] + fn shell_quote_plain_filename() { + assert_eq!(super::shell_quote("file.txt"), "'file.txt'"); + assert_eq!(super::shell_quote("with space.txt"), "'with space.txt'"); + assert_eq!(super::shell_quote(""), "''"); + assert_eq!(super::shell_quote("-"), "'-'"); + } + + #[test] + fn shell_quote_metacharacters_neutralised() { + assert_eq!(super::shell_quote("; rm -rf ~ ;"), "'; rm -rf ~ ;'"); + assert_eq!(super::shell_quote("$(payload)"), "'$(payload)'"); + assert_eq!(super::shell_quote("`payload`"), "'`payload`'"); + assert_eq!(super::shell_quote("a|b&c;d"), "'a|b&c;d'"); + assert_eq!(super::shell_quote("..>/dev/null"), "'..>/dev/null'"); + assert_eq!(super::shell_quote("\n\t"), "'\n\t'"); + } + + #[test] + fn shell_quote_embedded_single_quote() { + assert_eq!(super::shell_quote("it's"), "'it'\\''s'"); + assert_eq!(super::shell_quote("'"), "''\\'''"); + assert_eq!(super::shell_quote("a'b'c"), "'a'\\''b'\\''c'"); + } + + #[test] + fn shell_substitute_only_replaces_first_percent_s() { + assert_eq!(super::shell_substitute("echo %s", "x"), "echo 'x'"); + assert_eq!(super::shell_substitute("echo %s %s", "x"), "echo 'x' %s"); + } + + #[test] + fn shell_substitute_protects_against_filename_injection() { + let template = "|preproc %s"; + let attacker_filename = "; rm -rf ~ ;"; + let result = super::shell_substitute(template, attacker_filename); + assert_eq!(result, "|preproc '; rm -rf ~ ;'"); + } + + #[test] + fn shell_substitute_two_replaces_first_two_placeholders() { + assert_eq!( + super::shell_substitute_two("echo %s and %s done", "a", "b"), + "echo 'a' and 'b' done" + ); + // A third %s is left alone. + assert_eq!( + super::shell_substitute_two("echo %s %s %s", "a", "b"), + "echo 'a' 'b' %s" + ); + } + + #[test] + fn shell_substitute_two_handles_percent_s_in_first_argument() { + // The chained-substitute approach would replace the injected `%s` here. + assert_eq!( + super::shell_substitute_two("a %s b %s c", "%s", "second"), + "a '%s' b 'second' c" + ); + } } diff --git a/src/lib.rs b/src/lib.rs index 4c60f10e..685214dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,6 +54,7 @@ mod vscreen; pub(crate) mod wrapping; pub use nonprintable_notation::{BinaryBehavior, NonprintableNotation}; +pub use preprocessor::sanitize_for_terminal; pub use preprocessor::StripAnsiMode; pub use pretty_printer::{Input, PrettyPrinter, Syntax}; pub use syntax_mapping::{MappingTarget, SyntaxMapping}; diff --git a/src/output.rs b/src/output.rs index 0205f48e..8054be46 100644 --- a/src/output.rs +++ b/src/output.rs @@ -23,6 +23,18 @@ pub struct BuiltinPager { impl BuiltinPager { fn new() -> Self { let pager = minus::Pager::new(); + + let mut input_register = minus::input::HashedEventRegister::default(); + input_register.add_key_events(&["home"], |_, _| { + minus::input::InputEvent::UpdateUpperMark(0) + }); + input_register.add_key_events(&["end"], |_, _| { + minus::input::InputEvent::UpdateUpperMark(usize::MAX) + }); + pager + .set_input_classifier(Box::new(input_register)) + .expect("failed to set input classifier on newly created pager"); + let handle = { let pager = pager.clone(); Some(spawn(move || { diff --git a/src/preprocessor.rs b/src/preprocessor.rs index 6b4e2935..a9b8e712 100644 --- a/src/preprocessor.rs +++ b/src/preprocessor.rs @@ -139,16 +139,140 @@ 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 { + if !input + .chars() + .any(|c| matches!(c, '\x00'..='\x08' | '\x0A'..='\x1F' | '\x7F'..='\u{9F}')) + { + return input.to_owned(); + } + + let mut out = String::with_capacity(input.len() + 8); + for c in input.chars() { + match c { + '\t' => out.push('\t'), + '\x00'..='\x1F' => { + out.push('^'); + out.push(char::from_u32(0x40 + c as u32).unwrap_or('?')); + } + '\x7F' => out.push_str("^?"), + '\u{80}'..='\u{9F}' => { + use std::fmt::Write as _; + let _ = write!(out, "\\u{{{:x}}}", c as u32); + } + other => out.push(other), + } + } + out +} + /// Strips overstrike sequences (backspace formatting) from input. /// /// Overstrike formatting is used by man pages and some help output: @@ -241,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) @@ -261,3 +492,40 @@ fn test_strip_overstrike() { // Unicode with overstrike assert_eq!(strip_overstrike("ä\x08äöü", 2), "äöü"); } + +#[test] +fn test_sanitize_for_terminal_passthrough() { + assert_eq!(sanitize_for_terminal(""), ""); + assert_eq!(sanitize_for_terminal("hello.txt"), "hello.txt"); + assert_eq!(sanitize_for_terminal("résumé.pdf"), "résumé.pdf"); + assert_eq!(sanitize_for_terminal("日本語.md"), "日本語.md"); + assert_eq!( + sanitize_for_terminal("path/with spaces/file.log"), + "path/with spaces/file.log" + ); + assert_eq!(sanitize_for_terminal("a\tb"), "a\tb"); +} + +#[test] +fn test_sanitize_for_terminal_c0_controls() { + assert_eq!( + sanitize_for_terminal("\x1b[31mINJECTED\x1b[0m.txt"), + "^[[31mINJECTED^[[0m.txt" + ); + assert_eq!(sanitize_for_terminal("bad\x07rest"), "bad^Grest"); + assert_eq!(sanitize_for_terminal("\x00\x08\n\r\x7F"), "^@^H^J^M^?"); + assert_eq!(sanitize_for_terminal("\u{9b}31m"), "\\u{9b}31m"); + assert_eq!( + sanitize_for_terminal("\u{9d}0;pwned\x07"), + "\\u{9d}0;pwned^G" + ); +} + +#[test] +fn test_sanitize_for_terminal_idempotent_on_sanitized() { + let dirty = "\x1b]0;pwned\x07file.txt"; + let clean = sanitize_for_terminal(dirty); + assert_eq!(sanitize_for_terminal(&clean), clean); + assert!(!clean.contains('\x1b')); + assert!(!clean.contains('\x07')); +} diff --git a/src/pretty_printer.rs b/src/pretty_printer.rs index 89a9854e..1dc510f4 100644 --- a/src/pretty_printer.rs +++ b/src/pretty_printer.rs @@ -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; diff --git a/src/printer.rs b/src/printer.rs index 6a57fb62..42405902 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -29,7 +29,10 @@ use crate::error::*; use crate::input::OpenedInput; use crate::line_range::{MaxBufferedLineNumber, RangeCheckResult}; use crate::output::OutputHandle; -use crate::preprocessor::{expand_tabs, replace_nonprintable, strip_ansi, strip_overstrike}; +use crate::preprocessor::{ + expand_tabs, replace_nonprintable, sanitize, sanitize_for_terminal, strip_ansi, + strip_overstrike, +}; use crate::style::StyleComponent; use crate::terminal::{as_terminal_escaped, to_ansi_color}; use crate::vscreen::{AnsiStyle, EscapeSequence, EscapeSequenceIterator}; @@ -37,6 +40,16 @@ use crate::wrapping::WrappingMode; use crate::BinaryBehavior; use crate::StripAnsiMode; +// Return the displayed width of a character. +// +// Control characters (0x00..=0x1F and 0x7F) are rendered by the terminal +// in caret notation (e.g. ^@, ^A, ..., ^?), which occupies two columns. +// UnicodeWidthChar::width() returns None for these, so we map them to 2 +// here instead of the previous default of 0. +fn char_width(c: char) -> usize { + c.width().unwrap_or(if c.is_control() { 2 } else { 0 }) +} + const ANSI_UNDERLINE_ENABLE: EscapeSequence = EscapeSequence::CSI { raw_sequence: "\x1B[4m", parameters: "4", @@ -198,6 +211,7 @@ pub(crate) struct InteractivePrinter<'a> { background_color_highlight: Option, consecutive_empty_lines: usize, strip_ansi: bool, + sanitize: bool, strip_overstrike: bool, } @@ -261,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 @@ -307,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, @@ -320,6 +344,7 @@ impl<'a> InteractivePrinter<'a> { background_color_highlight, consecutive_empty_lines: 0, strip_ansi, + sanitize, strip_overstrike, }) } @@ -479,7 +504,7 @@ impl Printer for InteractivePrinter<'_> { (but will be present if the output of 'bat' is piped). You can use 'bat -A' \ to show the binary file contents.", Yellow.paint("[bat warning]"), - input.description.summary(), + sanitize_for_terminal(&input.description.summary()), )?; } else if self.config.style_components.grid() { self.print_horizontal_line(handle, '┬')?; @@ -533,9 +558,11 @@ impl Printer for InteractivePrinter<'_> { "{}{}{mode}", description .kind() - .map(|kind| format!("{kind}: ")) + .map(|kind| format!("{}: ", sanitize_for_terminal(kind))) .unwrap_or_else(|| "".into()), - self.colors.header_value.paint(description.title()), + self.colors + .header_value + .paint(sanitize_for_terminal(description.title())), ); self.print_header_multiline_component(handle, &header_filename) } @@ -589,11 +616,23 @@ impl Printer for InteractivePrinter<'_> { let title = "8<"; let title_count = title.chars().count(); - let snip_left = "─ ".repeat((self.config.term_width - panel_count - (title_count / 2)) / 4); + let snip_left = "─ ".repeat( + self.config + .term_width + .saturating_sub(panel_count) + .saturating_sub(title_count / 2) + / 4, + ); let snip_left_count = snip_left.chars().count(); // Can't use .len() with Unicode. - let snip_right = - " ─".repeat((self.config.term_width - panel_count - snip_left_count - title_count) / 2); + let snip_right = " ─".repeat( + self.config + .term_width + .saturating_sub(panel_count) + .saturating_sub(snip_left_count) + .saturating_sub(title_count) + / 2, + ); writeln!( handle, @@ -649,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() } @@ -793,7 +834,7 @@ impl Printer for InteractivePrinter<'_> { for c in text.chars() { // calculate the displayed width for next character - let cw = c.width().unwrap_or(0); + let cw = char_width(c); current_width += cw; // Track whitespace positions for word wrapping. @@ -868,10 +909,8 @@ impl Printer for InteractivePrinter<'_> { if let Some(rs) = rest_start { // Word wrap: carry remainder to next line. let remainder = line_buf[rs..].to_string(); - let rem_width: usize = remainder - .chars() - .map(|ch| ch.width().unwrap_or(0)) - .sum(); + let rem_width: usize = + remainder.chars().map(char_width).sum(); line_buf.clear(); line_buf.push_str(&remainder); current_width = rem_width + cw; diff --git a/src/syntax_mapping.rs b/src/syntax_mapping.rs index 0cd2d655..584a5cb6 100644 --- a/src/syntax_mapping.rs +++ b/src/syntax_mapping.rs @@ -17,9 +17,16 @@ use ignored_suffixes::IgnoredSuffixes; mod builtin; pub mod ignored_suffixes; -fn make_glob_matcher(from: &str) -> Result { +/// Whether a glob pattern should be matched case-sensitively or case-insensitively. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Case { + Sensitive, + Insensitive, +} + +fn make_glob_matcher(from: &str, case: Case) -> Result { let matcher = GlobBuilder::new(from) - .case_insensitive(true) + .case_insensitive(matches!(case, Case::Insensitive)) .literal_separator(true) .build()? .compile_matcher(); @@ -97,7 +104,14 @@ impl<'a> SyntaxMapping<'a> { } pub fn insert(&mut self, from: &str, to: MappingTarget<'a>) -> Result<()> { - let matcher = make_glob_matcher(from)?; + let matcher = make_glob_matcher(from, Case::Insensitive)?; + self.custom_mappings.push((matcher, to)); + Ok(()) + } + + /// Like [`Self::insert`], but the glob pattern is matched case-sensitively. + pub fn insert_case_sensitive(&mut self, from: &str, to: MappingTarget<'a>) -> Result<()> { + let matcher = make_glob_matcher(from, Case::Sensitive)?; self.custom_mappings.push((matcher, to)); Ok(()) } @@ -261,4 +275,41 @@ mod tests { Some(MappingTarget::MapTo("alpha")) ); } + + #[test] + fn case_sensitive_custom_mappings_work() { + let mut map = SyntaxMapping::new(); + map.insert_case_sensitive("MY_SPECIAL_FILE", MappingTarget::MapTo("Python")) + .ok(); + + // Exact case matches + assert_eq!( + map.get_syntax_for("/path/to/MY_SPECIAL_FILE"), + Some(MappingTarget::MapTo("Python")) + ); + // Different case should NOT match the case-sensitive rule + assert_eq!(map.get_syntax_for("/path/to/my_special_file"), None); + assert_eq!(map.get_syntax_for("/path/to/My_Special_File"), None); + } + + #[test] + fn builtin_mappings_build_is_case_sensitive() { + let map = SyntaxMapping::new(); + + // "BUILD" (uppercase) should map to Python via case-sensitive builtin + assert_eq!( + map.get_syntax_for("/path/to/BUILD"), + Some(MappingTarget::MapTo("Python")) + ); + // "build" (lowercase) should still map to MapToUnknown + assert_eq!( + map.get_syntax_for("/path/to/build"), + Some(MappingTarget::MapToUnknown) + ); + // Mixed case should NOT match the Python rule + assert_eq!( + map.get_syntax_for("/path/to/Build"), + Some(MappingTarget::MapToUnknown) + ); + } } diff --git a/src/syntax_mapping/builtin.rs b/src/syntax_mapping/builtin.rs index 1822be57..79d298c3 100644 --- a/src/syntax_mapping/builtin.rs +++ b/src/syntax_mapping/builtin.rs @@ -3,7 +3,7 @@ use std::env; use globset::GlobMatcher; use once_cell::sync::Lazy; -use crate::syntax_mapping::{make_glob_matcher, MappingTarget}; +use crate::syntax_mapping::{make_glob_matcher, Case, MappingTarget}; // Static syntax mappings generated from /src/syntax_mapping/builtins/ by the // build script (/build/syntax_mapping.rs). @@ -53,8 +53,8 @@ include!(concat!( /// A failure to compile is a fatal error. /// /// Used internally by `Lazy>`'s lazy evaluation closure. -fn build_matcher_fixed(from: &str) -> GlobMatcher { - make_glob_matcher(from).expect("A builtin fixed glob matcher failed to compile") +fn build_matcher_fixed(from: &str, case: Case) -> GlobMatcher { + make_glob_matcher(from, case).expect("A builtin fixed glob matcher failed to compile") } /// Join a list of matcher segments to create a glob string, replacing all @@ -64,7 +64,7 @@ fn build_matcher_fixed(from: &str) -> GlobMatcher { /// to compile. /// /// Used internally by `Lazy>`'s lazy evaluation closure. -fn build_matcher_dynamic(segs: &[MatcherSegment]) -> Option { +fn build_matcher_dynamic(segs: &[MatcherSegment], case: Case) -> Option { // join segments let mut buf = String::new(); for seg in segs { @@ -77,7 +77,7 @@ fn build_matcher_dynamic(segs: &[MatcherSegment]) -> Option { } } // compile glob matcher - let matcher = make_glob_matcher(&buf).ok()?; + let matcher = make_glob_matcher(&buf, case).ok()?; Some(matcher) } diff --git a/src/syntax_mapping/builtins/README.md b/src/syntax_mapping/builtins/README.md index 29cf43ee..220e45df 100644 --- a/src/syntax_mapping/builtins/README.md +++ b/src/syntax_mapping/builtins/README.md @@ -20,12 +20,10 @@ syntax mappings defined by all TOML files, and embed them into the binary. ## File syntax -Each TOML file should contain a single section named `mappings`, with each of -its keys being a language identifier (first column of `bat -L`; also referred to -as "target"). +Each TOML file should contain a single section named `mappings`, with each of its keys being a language +identifier (first column of `bat -L`; also referred to as "target"). -The value of each key should be an array of strings, with each item being a glob -matcher. We will call each of these items a "rule". +The value of each key should be an array of "rules". The rules are expected to be objects with a `glob` string and a `case_sensitive` boolean. For simplification, a rule can be just a glob string, which is shorthand for the default case insensitive mode. For example, if `foo-application` uses both TOML and YAML configuration files, we could write something like this: @@ -98,6 +96,15 @@ like this: ] ``` +### Case sensitivity + +By default, all glob patterns are matched case-insensitively. To match a pattern case-sensitively, use the object form of the rule with the `case_sensitive` option: + +```toml +[mappings] +"Python" = [{ glob = "BUILD", case_sensitive = true }] +``` + ## Ordering At compile time, all TOML files applicable to the target are processed in diff --git a/src/syntax_mapping/builtins/common/50-bazel.toml b/src/syntax_mapping/builtins/common/50-bazel.toml new file mode 100644 index 00000000..2ced1399 --- /dev/null +++ b/src/syntax_mapping/builtins/common/50-bazel.toml @@ -0,0 +1,2 @@ +[mappings] +"Python" = [{ glob = "BUILD", case_sensitive = true }] diff --git a/src/syntax_mapping/builtins/common/50-dotnet-xml.toml b/src/syntax_mapping/builtins/common/50-dotnet-xml.toml index 1e3a860a..70319f83 100644 --- a/src/syntax_mapping/builtins/common/50-dotnet-xml.toml +++ b/src/syntax_mapping/builtins/common/50-dotnet-xml.toml @@ -1,2 +1,2 @@ [mappings] -"XML" = ["*.csproj", "*.vbproj", "*.props", "*.targets"] +"XML" = ["*.csproj", "*.vbproj", "*.props", "*.targets", "*.slnx"] diff --git a/src/syntax_mapping/builtins/common/50-json.toml b/src/syntax_mapping/builtins/common/50-json.toml index 6b3252e8..3555d34b 100644 --- a/src/syntax_mapping/builtins/common/50-json.toml +++ b/src/syntax_mapping/builtins/common/50-json.toml @@ -1,3 +1,3 @@ # JSON Lines is a simple variation of JSON #2535 [mappings] -"JSON" = ["*.jsonl", "*.jsonc", "*.jsonld", "*.geojson", "*.ndjson"] +"JSON" = ["*.jsonl", "*.jsonc", "*.jsonld", "*.geojson", "*.ndjson", "*.code-workspace"] diff --git a/src/syntax_mapping/builtins/common/50-just.toml b/src/syntax_mapping/builtins/common/50-just.toml new file mode 100644 index 00000000..5ca19c85 --- /dev/null +++ b/src/syntax_mapping/builtins/common/50-just.toml @@ -0,0 +1,2 @@ +[mappings] +"Makefile" = ["justfile", "Justfile", ".justfile", "*.justfile"] diff --git a/src/syntax_mapping/builtins/common/50-ssh.toml b/src/syntax_mapping/builtins/common/50-ssh.toml index 6ec24050..10bce980 100644 --- a/src/syntax_mapping/builtins/common/50-ssh.toml +++ b/src/syntax_mapping/builtins/common/50-ssh.toml @@ -1,2 +1,2 @@ [mappings] -"SSH Config" = ["**/.ssh/config"] +"SSH Config" = ["**/.ssh/**/config"] diff --git a/src/syntax_mapping/builtins/linux/50-dnf.toml b/src/syntax_mapping/builtins/linux/50-dnf.toml new file mode 100644 index 00000000..054fc6ce --- /dev/null +++ b/src/syntax_mapping/builtins/linux/50-dnf.toml @@ -0,0 +1,2 @@ +[mappings] +"INI" = ["/etc/dnf/dnf.conf", "/etc/yum.repos.d/*.repo"] diff --git a/src/syntax_mapping/builtins/unix-family/50-ghostty.toml b/src/syntax_mapping/builtins/unix-family/50-ghostty.toml new file mode 100644 index 00000000..71804e4b --- /dev/null +++ b/src/syntax_mapping/builtins/unix-family/50-ghostty.toml @@ -0,0 +1,2 @@ +[mappings] +"INI" = ["**/ghostty/**/*.ghostty", "**/ghostty/themes/*"] diff --git a/src/vscreen.rs b/src/vscreen.rs index 3acbb72f..6478f577 100644 --- a/src/vscreen.rs +++ b/src/vscreen.rs @@ -386,12 +386,23 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> { } fn next_text(&mut self) -> Option { - 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 { let (start_sequence, c) = self.chars.next().expect("to not be finished"); + + // Handle 8-bit C1 introducers as their 7-bit `ESC ` 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 { 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 { + 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 { + 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 { 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 { + 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 { @@ -543,13 +648,25 @@ impl Iterator for EscapeSequenceOffsetsIterator<'_> { type Item = EscapeSequenceOffsets; fn next(&mut self) -> Option { 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"); diff --git a/tests/examples/regression_tests/issue_2745.txt b/tests/examples/regression_tests/issue_2745.txt new file mode 100644 index 00000000..b548c1a2 --- /dev/null +++ b/tests/examples/regression_tests/issue_2745.txt @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +NAME="${0##*/}" +for i in {1..3}; do + echo "hello $NAME $i" +done diff --git a/tests/examples/regression_tests/issue_3631.txt b/tests/examples/regression_tests/issue_3631.txt new file mode 100644 index 00000000..8449691f Binary files /dev/null and b/tests/examples/regression_tests/issue_3631.txt differ diff --git a/tests/examples/regression_tests/issue_3647_expect b/tests/examples/regression_tests/issue_3647_expect new file mode 100644 index 00000000..3786df69 --- /dev/null +++ b/tests/examples/regression_tests/issue_3647_expect @@ -0,0 +1,2 @@ +#!/usr/bin/expect -f +set timeout 30 diff --git a/tests/examples/regression_tests/issue_3647_tclsh b/tests/examples/regression_tests/issue_3647_tclsh new file mode 100644 index 00000000..0f4b3c5c --- /dev/null +++ b/tests/examples/regression_tests/issue_3647_tclsh @@ -0,0 +1,2 @@ +#!/usr/bin/env tclsh +puts "Hello from tclsh" diff --git a/tests/examples/regression_tests/issue_3647_wish b/tests/examples/regression_tests/issue_3647_wish new file mode 100644 index 00000000..8bfe4baf --- /dev/null +++ b/tests/examples/regression_tests/issue_3647_wish @@ -0,0 +1,2 @@ +#!/usr/bin/wish +button .b -text "Click" diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 9c7cc285..1263e5a5 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -305,6 +305,19 @@ fn line_range_multiple() { .stdout("line 1\nline 2\nline 4\n"); } +#[test] +fn snip_at_terminal_width_one_does_not_panic() { + bat() + .arg("multiline.txt") + .arg("--style=snip") + .arg("--color=always") + .arg("--terminal-width=1") + .arg("--line-range=1:2") + .arg("--line-range=4:4") + .assert() + .success(); +} + #[test] fn line_range_multiple_with_context() { bat() @@ -613,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] @@ -1047,6 +1066,57 @@ fn tabs_4_arg_overrides_env_noconfig() { ); } +#[test] +fn terminal_width_env_var_is_respected() { + let tmp_dir = tempdir().expect("can create temporary directory"); + let tmp_path = tmp_dir.path().join("long.txt"); + std::fs::write( + &tmp_path, + "0123456789abcdef0123456789abcdef0123456789abcdef\n", + ) + .expect("can write temporary file"); + + bat() + .env("BAT_WIDTH", "20") + .arg(&tmp_path) + .arg("--paging=never") + .arg("--color=never") + .arg("--style=numbers") + .arg("--decorations=always") + .arg("--wrap=character") + .assert() + .success() + .stdout(" 1 0123456789abcde\n f0123456789abcd\n ef0123456789abc\n def\n") + .stderr(""); +} + +#[test] +fn terminal_width_arg_overrides_env() { + let tmp_dir = tempdir().expect("can create temporary directory"); + let tmp_path = tmp_dir.path().join("long.txt"); + std::fs::write( + &tmp_path, + "0123456789abcdef0123456789abcdef0123456789abcdef\n", + ) + .expect("can write temporary file"); + + bat() + .env("BAT_WIDTH", "20") + .arg(&tmp_path) + .arg("--paging=never") + .arg("--color=never") + .arg("--style=numbers") + .arg("--decorations=always") + .arg("--wrap=character") + .arg("--terminal-width=10") + .assert() + .success() + .stdout( + " 1 01234\n 56789\n abcde\n f0123\n 45678\n 9abcd\n ef012\n 34567\n 89abc\n def\n", + ) + .stderr(""); +} + #[test] fn fail_non_existing() { bat().arg("non-existing-file").assert().failure(); @@ -1474,6 +1544,7 @@ fn diagnostic_sanity_check() { .assert() .success() .stdout(predicate::str::contains("BAT_PAGER=")) + .stdout(predicate::str::contains("BAT_WIDTH=")) .stderr(""); } @@ -2093,6 +2164,24 @@ fn header_binary() { .stderr(""); } +#[test] +fn header_zip_file_is_binary() { + let tmp_dir = tempdir().expect("can create temporary directory"); + let tmp_path = tmp_dir.path().join("test.zip"); + std::fs::write(&tmp_path, b"PK\x03\x04hello").expect("can write temporary file"); + + bat() + .arg(&tmp_path) + .arg("--decorations=always") + .arg("--style=header") + .arg("-r=0:0") + .arg("--file-name=test.zip") + .assert() + .success() + .stdout("File: test.zip \n") + .stderr(""); +} + #[test] fn header_full_binary() { bat() @@ -2820,6 +2909,25 @@ fn binary_as_text() { .stderr(""); } +#[test] +fn binary_as_text_control_char_width() { + // Control characters are displayed as caret notation (e.g. ^@) by the + // terminal, occupying 2 columns each. With 20 NUL bytes (40 columns) + + // "END" (3 columns) = 43 columns, wrapping at terminal width 40 must + // produce 2 lines, not 1. See #3631. + bat() + .arg("--binary=as-text") + .arg("--wrap=character") + .arg("--terminal-width=40") + .arg("--decorations=always") + .arg("--style=plain") + .arg("--color=never") + .arg("regression_tests/issue_3631.txt") + .assert() + .success() + .stdout(predicate::function(|s: &str| s.lines().count() == 2)); +} + #[test] fn no_strip_overstrike_for_plain_text() { // Overstrike is preserved for plain text files (no syntax highlighting) @@ -3881,6 +3989,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() { @@ -4104,3 +4364,218 @@ fn word_wrap_short_line_no_wrap() { .success() .stdout("Single Line\n"); } + +#[cfg(unix)] +#[cfg(feature = "git")] +fn setup_diff_test_repo() -> tempfile::TempDir { + use std::process::Command; + + let dir = tempfile::tempdir().expect("can create temporary directory"); + let repo = dir.path(); + + // Initialize a git repo and commit a file + Command::new("git") + .args(["init"]) + .current_dir(repo) + .output() + .expect("git init"); + + Command::new("git") + .args(["config", "user.email", "test@test.com"]) + .current_dir(repo) + .output() + .expect("git config email"); + + Command::new("git") + .args(["config", "user.name", "Test"]) + .current_dir(repo) + .output() + .expect("git config name"); + + std::fs::write(repo.join("test.txt"), "line 1\nline 2\nline 3\n").expect("can write test file"); + + Command::new("git") + .args(["add", "test.txt"]) + .current_dir(repo) + .output() + .expect("git add"); + + Command::new("git") + .args(["commit", "-m", "initial"]) + .current_dir(repo) + .output() + .expect("git commit"); + + // Modify the file so --diff has something to show + std::fs::write( + repo.join("test.txt"), + "line 1\nline 2 modified\nline 3\nline 4 added\n", + ) + .expect("can write modified test file"); + + dir +} + +#[cfg(unix)] +#[cfg(feature = "git")] +#[test] +fn diff_plain_preserves_change_markers() { + let repo = setup_diff_test_repo(); + + // With --diff --plain, output should contain the change marker column + // but not other decorations like line numbers or grid + let output = bat() + .current_dir(repo.path()) + .arg("--diff") + .arg("--plain") + .arg("--color=never") + .arg("--decorations=always") + .arg("test.txt") + .assert() + .success() + .get_output() + .stdout + .clone(); + + let stdout = std::str::from_utf8(&output).expect("valid utf-8"); + + // The output should contain the modified and added lines + assert!( + stdout.contains("line 2 modified"), + "diff plain output should contain modified line, got: {stdout}" + ); + assert!( + stdout.contains("line 4 added"), + "diff plain output should contain added line, got: {stdout}" + ); + + // Should NOT contain line numbers (a decoration that --plain disables) + assert!( + !stdout.contains(" 1"), + "diff plain output should not contain line numbers, got: {stdout}" + ); +} + +#[cfg(unix)] +#[cfg(feature = "git")] +#[test] +fn diff_plain_does_not_show_grid_or_header() { + let repo = setup_diff_test_repo(); + + let output = bat() + .current_dir(repo.path()) + .arg("--diff") + .arg("--plain") + .arg("--color=never") + .arg("--decorations=always") + .arg("--terminal-width=80") + .arg("test.txt") + .assert() + .success() + .get_output() + .stdout + .clone(); + + let stdout = std::str::from_utf8(&output).expect("valid utf-8"); + + // Grid lines use box-drawing characters + assert!( + !stdout.contains('─'), + "diff plain output should not contain grid lines, got: {stdout}" + ); + assert!( + !stdout.contains('│'), + "diff plain output should not contain grid separators, got: {stdout}" + ); + + // Header shows "File: " + assert!( + !stdout.contains("File:"), + "diff plain output should not contain file header, got: {stdout}" + ); +} + +#[cfg(unix)] +#[cfg(feature = "git")] +#[test] +fn plain_without_diff_still_works() { + let repo = setup_diff_test_repo(); + + // --plain without --diff should output file content with no decorations at all + bat() + .current_dir(repo.path()) + .arg("--plain") + .arg("--color=never") + .arg("--decorations=always") + .arg("test.txt") + .assert() + .success() + .stdout("line 1\nline 2 modified\nline 3\nline 4 added\n"); +} + +#[test] +fn tcl_shebang_detection_tclsh() { + bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--decorations=always") + .arg("regression_tests/issue_3647_tclsh") + .assert() + .success(); +} + +#[test] +fn tcl_shebang_detection_wish() { + bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--decorations=always") + .arg("regression_tests/issue_3647_wish") + .assert() + .success(); +} + +#[test] +fn tcl_shebang_detection_expect() { + bat() + .arg("--color=always") + .arg("--style=plain") + .arg("--decorations=always") + .arg("regression_tests/issue_3647_expect") + .assert() + .success(); +} + +#[test] +fn ignored_suffix_enables_first_line_detection() { + // A shebang shell script saved with a `.txt` extension is Plain Text by + // default (the extension wins). With `--ignored-suffix .txt` the suffix is + // stripped before detection, so it falls back to the first line and is + // highlighted exactly as if its language were forced to bash. See #2745. + let fixture = "regression_tests/issue_2745.txt"; + let common = ["--color=always", "--decorations=never", "--style=plain"]; + + let stdout = |args: &[&str]| -> Vec { + let assert = bat() + .args(common) + .args(args) + .arg(fixture) + .assert() + .success(); + assert.get_output().stdout.clone() + }; + + let forced_bash = stdout(&["--language", "bash"]); + let with_ignored_suffix = stdout(&["--ignored-suffix", ".txt"]); + let default = stdout(&[]); + + // The fixture really is being highlighted (forcing bash is not a no-op). + assert!( + forced_bash.windows(2).any(|w| w == b"\x1b["), + "forced-bash output should contain ANSI color codes" + ); + // With the ignored suffix, detection matches forced bash highlighting... + assert_eq!(with_ignored_suffix, forced_bash); + // ...while the default (extension wins) stays plain and differs. + assert_ne!(default, forced_bash); +} diff --git a/tests/snapshots/generate_snapshots.py b/tests/snapshots/generate_snapshots.py old mode 100755 new mode 100644 index bb7d69ba..db7839e9 --- a/tests/snapshots/generate_snapshots.py +++ b/tests/snapshots/generate_snapshots.py @@ -4,6 +4,7 @@ import itertools import subprocess import pathlib import shutil +from typing import Iterable def generate_snapshots(): @@ -19,22 +20,23 @@ def generate_snapshots(): def generate_style_snapshot(style): - generate_snapshot(style.replace(",", "_"), "--style={}".format(style)) + generate_snapshot(style.replace(",", "_"), ["--style={}".format(style)]) -def generate_snapshot(name, arguments): - command = "cargo run -- --paging=never --color=never --decorations=always " - command += "{args} sample.rs > output/{name}.snapshot.txt".format( - name=name, - args=arguments - ) +def generate_snapshot(name: str, arguments: Iterable[str]): + output_file = "output/{name}.snapshot.txt".format(name=name) + command = [ + "cargo", "run", "--", "--paging=never", "--color=never", + "--decorations=always", *arguments, "sample.rs" + ] print("generating snapshot for {}".format(name)) - subprocess.call(command, shell=True) + with open(output_file, "w") as f: + subprocess.call(command, stdout=f) def build_bat(): print("building bat") - subprocess.call("cargo build", cwd="../..", shell=True) + subprocess.call(["cargo", "build"], cwd="../..") def prepare_output_dir(): @@ -49,7 +51,7 @@ def modify_sample_file(): def undo_sample_file_modification(): print("undoing sample.rs modifications") - subprocess.call("git checkout -- sample.rs", shell=True) + subprocess.call(["git", "checkout", "--", "sample.rs"]) build_bat() diff --git a/tests/syntax-tests/highlighted/Caddyfile/Caddyfile b/tests/syntax-tests/highlighted/Caddyfile/Caddyfile new file mode 100644 index 00000000..ab2f6a55 --- /dev/null +++ b/tests/syntax-tests/highlighted/Caddyfile/Caddyfile @@ -0,0 +1,25 @@ +(logging) { + log { + output file /var/log/caddy.log + } +} + +# a comment +localhost:8080, example.com { + root * /var/www/site + file_server + reverse_proxy /.well-known/matrix/* localhost:8008 { + header_up Host {upstream_hostport} + } + import logging +} + +www.example.com { + redir https://example.com{uri} permanent + import logging +} + +status.example.com { + reverse_proxy localhost:3002 + import logging +} diff --git a/tests/syntax-tests/highlighted/Kotlin/test.kt b/tests/syntax-tests/highlighted/Kotlin/test.kt index dcb1560e..bb17aba2 100644 --- a/tests/syntax-tests/highlighted/Kotlin/test.kt +++ b/tests/syntax-tests/highlighted/Kotlin/test.kt @@ -1,85 +1,85 @@ -import kotlin.math.* +import kotlin.math.* -data class Example( - val name: String, - val numbers: List<Int?> +data class Example( + val name: String, + val numbers: List<Int?> ) -fun interface JokeInterface { - fun isFunny(): Boolean +fun interface JokeInterface { + fun isFunny(): Boolean } -abstract class AbstractJoke : JokeInterface { - override fun isFunny() = false - abstract fun content(): String +abstract class AbstractJoke : JokeInterface { + override fun isFunny() = false + abstract fun content(): String } -class Joke : AbstractJoke() { - override fun isFunny(): Boolean { +class Joke : AbstractJoke() { + override fun isFunny(): Boolean {  return true - } - override fun content(): String = "content of joke here, haha" + } + override fun content(): String = "content of joke here, haha" } -class DelegatedJoke(val joke: Joke) : JokeInterface by joke { - val number: Long = 123L +class DelegatedJoke(val joke: Joke) : JokeInterface by joke { + val number: Long = 123L - companion object { - const val someConstant = "some constant text" - } + companion object { + const val someConstant = "some constant text" + } } -object SomeSingleton +object SomeSingleton -sealed class Shape { - abstract fun area(): Double +sealed class Shape { + abstract fun area(): Double } -data class Square(val sideLength: Double) : Shape() { - override fun area(): Double = sideLength.pow(2) +data class Square(val sideLength: Double) : Shape() { + override fun area(): Double = sideLength.pow(2) } -object Point : Shape() { - override fun area() = .0 +object Point : Shape() { + override fun area() = .0 } -class Circle(val radius: Double) : Shape() { - override fun area(): Double { - return PI * radius * radius - } +class Circle(val radius: Double) : Shape() { + override fun area(): Double { + return PI * radius * radius + } } -fun String.extensionMethod() = "test" +fun String.extensionMethod() = "test" -fun main() { - val name = """ +fun main() { + val name = """  multiline  string    some numbers: 123123 42 - """.trimIndent() - val example = Example(name = name, numbers = listOf(512, 42, null, -1)) + """.trimIndent() + val example = Example(name = name, numbers = listOf(512, 42, null, -1)) - example.numbers - .filterNotNull() - .forEach { println(it) } + example.numbers + .filterNotNull() + .forEach { println(it) } - setOf(Joke(), DelegatedJoke(Joke()).joke) - .filter(JokeInterface::isFunny) - .map(AbstractJoke::content) - .forEachIndexed { index: Int, joke -> - println("I heard a funny joke(#${index + 1}): $joke") - } + setOf(Joke(), DelegatedJoke(Joke()).joke) + .filter(JokeInterface::isFunny) + .map(AbstractJoke::content) + .forEachIndexed { index: Int, joke -> + println("I heard a funny joke(#${index + 1}): $joke") + } - listOf(Square(12.3), Point, Circle(5.2)) - .associateWith(Shape::area) - .toList() - .sortedBy { it.second } - .forEach { - println("${it.first}: ${it.second}") - } + listOf(Square(12.3), Point, Circle(5.2)) + .associateWith(Shape::area) + .toList() + .sortedBy { it.second } + .forEach { + println("${it.first}: ${it.second}") + } - println("some string".extensionMethod()) + println("some string".extensionMethod()) - require(SomeSingleton::class.simpleName == "SomeSingletonName") { "something does not seem right..." } + require(SomeSingleton::class.simpleName == "SomeSingletonName") { "something does not seem right..." } } diff --git a/tests/syntax-tests/highlighted/Tcl/expect_shebang b/tests/syntax-tests/highlighted/Tcl/expect_shebang new file mode 100644 index 00000000..871eed6c --- /dev/null +++ b/tests/syntax-tests/highlighted/Tcl/expect_shebang @@ -0,0 +1,7 @@ +#!/usr/bin/expect -f +# Expect script detected via expect shebang +set timeout 30 +spawn ssh user@host +expect "password:" +send "secret\r" +expect eof diff --git a/tests/syntax-tests/highlighted/Tcl/tclsh_shebang b/tests/syntax-tests/highlighted/Tcl/tclsh_shebang new file mode 100644 index 00000000..5461f5f8 --- /dev/null +++ b/tests/syntax-tests/highlighted/Tcl/tclsh_shebang @@ -0,0 +1,7 @@ +#!/usr/bin/env tclsh +# Tcl script detected via tclsh shebang +puts "Hello from tclsh" +set x 42 +if {$x > 0} { + puts "positive" +} diff --git a/tests/syntax-tests/highlighted/Tcl/wish_shebang b/tests/syntax-tests/highlighted/Tcl/wish_shebang new file mode 100644 index 00000000..94fb971f --- /dev/null +++ b/tests/syntax-tests/highlighted/Tcl/wish_shebang @@ -0,0 +1,5 @@ +#!/usr/bin/wish +# Tk script detected via wish shebang +package require Tk +button .b -text "Click" -command {puts "clicked"} +pack .b diff --git a/tests/syntax-tests/highlighted/XML/solution.slnx b/tests/syntax-tests/highlighted/XML/solution.slnx new file mode 100644 index 00000000..7946e72b --- /dev/null +++ b/tests/syntax-tests/highlighted/XML/solution.slnx @@ -0,0 +1,7 @@ +<Solution> + <Folder Name="/Build/"> + <File Path="Directory.Build.props" /> + <File Path="projectname.targets" /> +  + <Project Path="console.csproj" /> + diff --git a/tests/syntax-tests/source/Caddyfile/Caddyfile b/tests/syntax-tests/source/Caddyfile/Caddyfile new file mode 100644 index 00000000..1fa41b9d --- /dev/null +++ b/tests/syntax-tests/source/Caddyfile/Caddyfile @@ -0,0 +1,25 @@ +(logging) { + log { + output file /var/log/caddy.log + } +} + +# a comment +localhost:8080, example.com { + root * /var/www/site + file_server + reverse_proxy /.well-known/matrix/* localhost:8008 { + header_up Host {upstream_hostport} + } + import logging +} + +www.example.com { + redir https://example.com{uri} permanent + import logging +} + +status.example.com { + reverse_proxy localhost:3002 + import logging +} diff --git a/tests/syntax-tests/source/Tcl/expect_shebang b/tests/syntax-tests/source/Tcl/expect_shebang new file mode 100644 index 00000000..ef288ba8 --- /dev/null +++ b/tests/syntax-tests/source/Tcl/expect_shebang @@ -0,0 +1,7 @@ +#!/usr/bin/expect -f +# Expect script detected via expect shebang +set timeout 30 +spawn ssh user@host +expect "password:" +send "secret\r" +expect eof diff --git a/tests/syntax-tests/source/Tcl/tclsh_shebang b/tests/syntax-tests/source/Tcl/tclsh_shebang new file mode 100644 index 00000000..08faa4d6 --- /dev/null +++ b/tests/syntax-tests/source/Tcl/tclsh_shebang @@ -0,0 +1,7 @@ +#!/usr/bin/env tclsh +# Tcl script detected via tclsh shebang +puts "Hello from tclsh" +set x 42 +if {$x > 0} { + puts "positive" +} diff --git a/tests/syntax-tests/source/Tcl/wish_shebang b/tests/syntax-tests/source/Tcl/wish_shebang new file mode 100644 index 00000000..690e73ef --- /dev/null +++ b/tests/syntax-tests/source/Tcl/wish_shebang @@ -0,0 +1,5 @@ +#!/usr/bin/wish +# Tk script detected via wish shebang +package require Tk +button .b -text "Click" -command {puts "clicked"} +pack .b diff --git a/tests/syntax-tests/source/XML/solution.slnx b/tests/syntax-tests/source/XML/solution.slnx new file mode 100644 index 00000000..e836bb51 --- /dev/null +++ b/tests/syntax-tests/source/XML/solution.slnx @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tests/tester/mod.rs b/tests/tester/mod.rs index b25a634c..538d77c2 100644 --- a/tests/tester/mod.rs +++ b/tests/tester/mod.rs @@ -1,15 +1,17 @@ +use gix::actor::SignatureRef; +use gix::bstr::BString; +use gix::bstr::ByteSlice; +use gix::date::time::Format; +use gix::date::Time; +use gix::objs::tree; use std::env; use std::fs::{self, File}; use std::io::Read; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::process::Command; use tempfile::TempDir; -use git2::build::CheckoutBuilder; -use git2::Repository; -use git2::Signature; - pub struct BatTester { /// Temporary working directory temp_dir: TempDir, @@ -59,35 +61,42 @@ impl Default for BatTester { } } -fn create_sample_directory() -> Result { +fn create_sample_directory() -> Result> { // Create temp directory and initialize repository let temp_dir = TempDir::new().expect("Temp directory"); - let repo = Repository::init(&temp_dir)?; + let repo = gix::init(&temp_dir)?; + let mut tree = gix::objs::Tree::empty(); - // Copy over `sample.rs` - let sample_path = temp_dir.path().join("sample.rs"); - println!("{sample_path:?}"); - fs::copy("tests/snapshots/sample.rs", &sample_path).expect("successful copy"); + // Create sample.rs from snapshot file + let blob_id = repo.write_blob_stream(File::open("tests/snapshots/sample.rs")?)?; + let entry = tree::Entry { + mode: tree::EntryMode::from(tree::EntryKind::Blob), + oid: blob_id.object()?.id, + filename: BString::from("sample.rs"), + }; + tree.entries.push(entry); + let tree_id = repo.write_object(tree)?; - // Commit - let mut index = repo.index()?; - index.add_path(Path::new("sample.rs"))?; - - let oid = index.write_tree()?; - let signature = Signature::now("bat test runner", "bat@test.runner")?; - let tree = repo.find_tree(oid)?; - let _ = repo.commit( - Some("HEAD"), // point HEAD to our new commit - &signature, // author - &signature, // committer + let author = SignatureRef { + name: "test".as_bytes().as_bstr(), + email: "test@test.test".as_bytes().as_bstr(), + time: &Time::now_local_or_utc().format_or_unix(Format::Raw), + }; + let commit_id = repo.commit_as( + author, + author, + "HEAD", "initial commit", - &tree, - &[], - ); - let mut opts = CheckoutBuilder::new(); - repo.checkout_head(Some(opts.force()))?; + tree_id, + gix::commit::NO_PARENT_IDS, + )?; + assert_eq!(commit_id, repo.head_id()?); - fs::copy("tests/snapshots/sample.modified.rs", &sample_path).expect("successful copy"); + fs::copy( + "tests/snapshots/sample.modified.rs", + temp_dir.path().join("sample.rs"), + ) + .expect("successful copy"); Ok(temp_dir) }