[
  {
    "path": ".github/workflows/ci.yaml",
    "content": "name: CI\n\non:\n  pull_request:\n  push:\n    branches: [master]\n  schedule:\n    - cron: \"00 00 * * *\"\n\njobs:\n  test:\n    name: Test\n    runs-on: ${{ matrix.job.os }}\n    timeout-minutes: 15\n    strategy:\n      matrix:\n        job:\n          - target: x86_64-unknown-linux-gnu\n            os: ubuntu-latest\n            flags: --features=native-tls\n          - target: x86_64-unknown-linux-gnu\n            os: ubuntu-latest\n            flags: --no-default-features --features=native-tls,online-tests # disables rustls\n          - target: x86_64-unknown-linux-gnu\n            os: ubuntu-latest\n            flags: --features=http3\n            rustflags: --cfg reqwest_unstable\n          - target: x86_64-apple-darwin\n            os: macos-15-intel\n            flags: --features=native-tls\n          - target: aarch64-apple-darwin\n            os: macos-latest\n            flags: --features=native-tls\n          - target: x86_64-pc-windows-msvc\n            os: windows-latest\n            flags: --features=native-tls\n          - target: x86_64-unknown-linux-musl\n            os: ubuntu-latest\n            use-cross: true\n          - target: aarch64-unknown-linux-musl\n            os: ubuntu-latest\n            use-cross: true\n            flags: -- --test-threads=4\n          - target: arm-unknown-linux-gnueabihf\n            os: ubuntu-latest\n            use-cross: true\n            flags: -- --test-threads=4\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: dtolnay/rust-toolchain@master\n        with:\n          toolchain: 1.85.0 # minimum supported rust version\n          targets: ${{ matrix.job.target }}\n\n      - uses: Swatinem/rust-cache@v2\n        with:\n          key: v2-${{ matrix.job.target }}\n\n      - name: Set RUSTFLAGS env variable\n        if: matrix.job.rustflags\n        shell: bash\n        run: echo \"RUSTFLAGS=${{ matrix.job.rustflags }}\" >> $GITHUB_ENV\n\n      - uses: ClementTsang/cargo-action@v0.0.6\n        with:\n          use-cross: ${{ !!matrix.job.use-cross }}\n          command: test\n          args: --target ${{ matrix.job.target }} ${{ matrix.job.flags }}\n\n  fmt-and-clippy:\n    name: Rustfmt and clippy\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: dtolnay/rust-toolchain@master\n        with:\n          toolchain: stable\n          components: rustfmt, clippy\n\n      - uses: Swatinem/rust-cache@v2\n\n      - name: Rustfmt\n        run: cargo fmt --check\n\n      - name: Clippy (default features)\n        run: cargo clippy --tests -- -D warnings -A unknown-lints\n\n      - name: Clippy (all features)\n        run: cargo clippy --all-features --tests -- -D warnings -A unknown-lints\n        env:\n          RUSTFLAGS: --cfg reqwest_unstable\n\n      - name: Clippy (native-tls only)\n        run: cargo clippy --no-default-features --features=native-tls,online-tests --tests -- -D warnings -A unknown-lints\n"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "name: release\n\non:\n  push:\n    tags: [v*.*.*]\n\njobs:\n  test:\n    name: Test\n    runs-on: ${{ matrix.job.os }}\n    timeout-minutes: 15\n    strategy:\n      matrix:\n        job:\n          - target: x86_64-unknown-linux-gnu\n            os: ubuntu-latest\n            flags: --features=native-tls\n          - target: x86_64-unknown-linux-gnu\n            os: ubuntu-latest\n            flags: --no-default-features --features=native-tls,online-tests # disables rustls\n          - target: x86_64-apple-darwin\n            os: macos-15-intel\n            flags: --features=native-tls\n          - target: aarch64-apple-darwin\n            os: macos-latest\n            flags: --features=native-tls\n          - target: x86_64-pc-windows-msvc\n            os: windows-latest\n            flags: --features=native-tls\n          - target: x86_64-unknown-linux-musl\n            os: ubuntu-latest\n            use-cross: true\n          - target: aarch64-unknown-linux-musl\n            os: ubuntu-latest\n            use-cross: true\n            flags: -- --test-threads=4\n          - target: arm-unknown-linux-gnueabihf\n            os: ubuntu-latest\n            use-cross: true\n            flags: -- --test-threads=4\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: dtolnay/rust-toolchain@master\n        with:\n          toolchain: 1.85.0 # minimum supported rust version\n          targets: ${{ matrix.job.target }}\n\n      - uses: ClementTsang/cargo-action@v0.0.6\n        with:\n          use-cross: ${{ !!matrix.job.use-cross }}\n          command: test\n          args: --target ${{ matrix.job.target }} ${{ matrix.job.flags }}\n\n  deploy:\n    name: Deploy\n    needs: [test]\n    runs-on: ${{ matrix.job.os }}\n    strategy:\n      matrix:\n        job:\n          - os: ubuntu-latest\n            target: aarch64-unknown-linux-musl\n            binutils: aarch64-linux-gnu\n            use-cross: true\n          - os: ubuntu-latest\n            target: arm-unknown-linux-gnueabihf\n            binutils: arm-linux-gnueabihf\n            use-cross: true\n          - os: ubuntu-latest\n            target: x86_64-unknown-linux-musl\n            use-cross: true\n          - os: macos-15-intel\n            target: x86_64-apple-darwin\n            flags: --features=native-tls\n          - os: macos-latest\n            target: aarch64-apple-darwin\n            flags: --features=native-tls\n          - os: windows-latest\n            target: x86_64-pc-windows-msvc\n            flags: --features=native-tls\n            rustflags: -C target-feature=+crt-static\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Set RUSTFLAGS env variable\n        if: matrix.job.rustflags\n        shell: bash\n        run: echo \"RUSTFLAGS=${{ matrix.job.rustflags }}\" >> $GITHUB_ENV\n\n      - name: Build target\n        uses: ClementTsang/cargo-action@v0.0.6\n        with:\n          use-cross: ${{ !!matrix.job.use-cross }}\n          command: build\n          args: --release --target ${{ matrix.job.target }} ${{ matrix.job.flags }}\n        env:\n          CARGO_PROFILE_RELEASE_LTO: true\n\n      - name: Strip release binary (linux and macOS)\n        if: matrix.job.os != 'windows-latest'\n        run: |\n          if [ \"${{ matrix.job.binutils }}\" != \"\" ]; then\n            sudo apt update\n            sudo apt -y install \"binutils-${{ matrix.job.binutils }}\"\n            \"${{ matrix.job.binutils }}-strip\" \"target/${{ matrix.job.target }}/release/xh\"\n          else\n            strip \"target/${{ matrix.job.target }}/release/xh\"\n          fi\n\n      - name: Package\n        shell: bash\n        run: |\n          if [ \"${{ matrix.job.os }}\" = \"windows-latest\" ]; then\n            bin=\"target/${{ matrix.job.target }}/release/xh.exe\"\n          else\n            bin=\"target/${{ matrix.job.target }}/release/xh\"\n          fi\n          staging=\"xh-${{ github.ref_name }}-${{ matrix.job.target }}\"\n\n          mkdir -p \"$staging\"/{doc,completions}\n          cp LICENSE README.md $bin $staging\n          cp CHANGELOG.md doc/xh.1 \"$staging\"/doc\n          cp completions/* \"$staging\"/completions\n\n          if [ \"${{ matrix.job.os }}\" = \"windows-latest\" ]; then\n            7z a \"$staging.zip\" $staging\n          elif [[ \"${{ matrix.job.os }}\" =~ \"macos\" ]]; then\n            gtar czvf \"$staging.tar.gz\" $staging\n          else\n            tar czvf \"$staging.tar.gz\" $staging\n          fi\n\n      - name: Package (debian)\n        if: matrix.job.target == 'x86_64-unknown-linux-musl'\n        shell: bash\n        run: |\n          cargo install --git https://github.com/blyxxyz/cargo-deb --locked --branch xh-patches cargo-deb\n          cargo deb --no-build --target ${{ matrix.job.target }}\n          cp \"target/${{ matrix.job.target }}/debian\"/*.deb ./\n\n      - name: Publish\n        uses: softprops/action-gh-release@v1\n        with:\n          files: \"xh*\"\n          draft: true\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "/.direnv\n/.envrc\n/target\n/.vscode\n.DS_Store\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## Unreleased\n### Features\n- Pretty-print XML responses, see #450 (@o1x3)\n- Add experimental HTTP message signatures (RFC 9421) support, see #448 (@zuisong)\n\n### Other\n- Upgrade to reqwest v0.13, see #441 (@ducaale)\n\n## [0.25.3] - 2025-12-16\n### Features\n- Add colors to `--help`/`-h`, see #432 (@starsep)\n\n### Bug fixes\n- Don't fail on error code 416 if resuming download, see #434 (@simonomi)\n\n### Other\n- Upgrade brotli to latest version, see #438 (@ducaale)\n\n## [0.25.0] - 2025-09-19\n### Features\n- Add `--unix-socket` for calling Unix Domain Sockets, see #427 (@ducaale)\n- Support binding to interface name on macOS, see #421 (@ducaale)\n- Add experimental HTTP/3 support, see #425 (@ducaale)\n\n## [0.24.1] - 2025-05-02\n### Features\n- Support RFC 5987 encoding for Content-Disposition filenames, see #416 (@zuisong)\n\n### Bug fixes\n- Fix crash on empty zstd response body, see #411 (@blyxxyz)\n\n### Other\n- Improve rustls errors for invalid certificates, see #413 (@blyxxyz)\n\n## [0.24.0] - 2025-02-18\n### Features\n- Add `--generate` option to generate the man page and shell completions at runtime,\n  see #393 (@fgimian)\n- Add support for Elvish and Nushell shell completions, see #393 (@fgimian)\n- Add `--compress` for compressing request body, see #403 (@zuisong)\n\n### Bug fixes\n- Store default paths for cookies without an explicit path attribute,\n  see #401 (@otaconix)\n\n### Other\n- Support generating man page with reproducible timestamp via `SOURCE_DATE_EPOCH`,\n  see #402 (@nc7s)\n- Upgrade cookie_store to 0.21.1, see #397 (@kranurag7)\n\n## [0.23.1] - 2025-01-02\n### Security fixes\n- Upgrade to ruzstd v0.7.3 to fix RUSTSEC-2024-0400, see #396 (@zuisong)\n\n### Bug fixes\n- Warn on combination of `--continue` and `Range` header, #394 (@blyxxyz)\n\n### Other\n- Enable logging in `rustls` and `tracing`-using dependencies, see #390 (@blyxxyz)\n\n## [0.23.0] - 2024-10-12\n### Features\n- Handle responses compressed in zstd format, see #364 (@zuisong)\n- Suppress warnings when `-qq` flag is used, see #371 (@blyxxyz)\n- Add `--debug` option for logging and backtraces, see #371 (@blyxxyz)\n- Decode `content-disposition` and `location` headers as UTF-8, see #375 (@zuisong)\n- Print headers as latin1, with the UTF-8 decoding also shown if applicable,\n  see #377 (@blyxxyz)\n- Print the actual reason phrase sent by the server instead of guessing it from\n  the status code, see #377 (@blyxxyz)\n\n### Bug fixes\n- Apply TLS options to non-HTTPS URLs, see #372 (@blyxxyz)\n\n### Other\n- Ignore `NO_COLOR` if set to empty string, see #370 (@blyxxyz)\n\n## [0.22.2] - 2024-07-08\n### Security fixes\n- Prevent directory traversal in server-supplied filenames, see #379 (@blyxxyz)\n\n## [0.22.0] - 2024-04-13\n### Features\n- Support http2-prior-knowledge, see #356 (@zuisong)\n- Directly bind to interface name on supported platforms, see #359 (@ducaale)\n- Enable stream when content-type is `text/event-stream`, see #360 (@zuisong)\n- Decode utf-8 encoded string when formatting non-streaming JSON response,\n  see #361 (@zuisong)\n\n### Other\n- Upgrade to hyper v1, see #357 (@zuisong)\n- Use `serde-transcode` to optimize JSON formatting, see #362 (@blyxxyz)\n\n## [0.21.0] - 2024-01-28\n### Features\n- Display remote address in metadata when `-vv` or `--meta` flag is used,\n  see #348 (@zuisong)\n\n### Other\n- Default `XH_CONFIG_DIR` to `~/.config/xh` in macOS, see #353 (@ducaale)\n\n## [0.20.1] - 2023-11-19\n### Features\n- Add `--resolve` for overriding DNS resolution, see #327 (@ducaale)\n\n## [0.19.4] - 2023-10-22\n### Other\n- Explicitly enable serde's derive feature, see #334 (@jayvdb)\n\n## [0.19.3] - 2023-10-21\n### Other\n- Make `network-interface` an optional dependency, see #332 (@blyxxyz)\n\n## [0.19.2] - 2023-10-21\n### Features\n- Add `--interface` for binding to a local IP address or interface, see #307 (@ducaale)\n- Translate `--raw` flag when using `--curl`, see #308 (@ducaale)\n- Support duplicate header keys in session files, see #313 (@ducaale)\n- Support persisting cookies from multiple domains, see #314 (@ducaale)\n- Control output formatting (JSON indent-level, header sorting, etc)\n  via `--format-options`, see #318 (@Bnyro) and #319 (@ducaale)\n\n### Bug fixes\n- Disable cURL's URL globbing, see #325 (@ducaale)\n- Improve PATH handling in `install.ps1`, see #264 (@henno)\n\n### Other\n- Update Rustls to v0.21.0, see #311 (@ducaale)\n\n## [0.18.0] - 2023-02-20\n### Features\n- Support reading query param and header values from a file, see #288 (@ducaale)\n- Highlight Syntax errors found while tokenizing a JSON path, see #260 (@ducaale)\n- Support outputting the metadata of a response via `--meta`, `--print=m` or `-vv`,\n  see #240 (@ducaale)\n\n### Bug fixes\n- Fix panic when when parsing connection timeout, see #295 (@sorairolake)\n\n### Breaking changes\n- Remove `-m` as a short flag for `--multipart`, see #299 (@ducaale)\n\n## [0.17.0] - 2022-11-08\n### Features\n- Add support for nested json syntax, see #217 (@ducaale)\n- Add Support for bearer auth in `.netrc`, see #267 (@porglezomp)\n- Support forcing ipv4/ipv6, see #276 (@zuisong)\n\n### Other\n- Allow building xh using native-tls only, see #281 (@jirutka)\n- Warn users when translating `--follow` + non GET method, see #280 (@jgoday)\n\n## [0.16.1] - 2022-05-22\n### Bug fixes\n- fix HEAD request failing on compressed response, see #257 (@ducaale)\n\n### Other\n- Configurable install dir for `install.sh` via `XH_BINDIR` env variable,\n  see #256 (@lispyclouds)\n- Use exit status 2 and 6 for `request timeout` and `too many redirects` errors\n  respectively, see #258 (@sorairolake)\n\n## [0.16.0] - 2022-04-17\n### Features\n- Add support for URLs with leading `://` to allow quick conversion of\n  pasted URLs into HTTPie/xh command e.g `http://httpbin.org/json` →\n  `$ http ://httpbin.org/json`, see #232 (@ducaale)\n- Support sending multiple request headers with the same key, see #242 (@ducaale)\n\n### Bug fixes\n- Don't remove `content-encoding` and `content-length` headers while processing\n  gzip/deflate/brotli encoded responses, see #241 (@ducaale)\n\n### Other\n- Replace structopt with clap3.x, see #216 (@ducaale) and #235 (@blyxxyz)\n- Improve output coloring performance by switching to incremental highlighting,\n  see #228 (@blyxxyz)\n- Faster `--stream` output formatting by switching to full buffering and manual\n  flushing, see #233 (@blyxxyz) \n- Automate the generation of negation flags, see #234 (@blyxxyz)\n- Display download's elapsed time as seconds, see #236 (@ducaale)\n\n## [0.15.0] - 2022-01-27\n### Features\n- Add support for `--raw` flag, see #202 (@ducaale)\n- Add Fruity theme, see #206 (@ducaale)\n- Use a custom netrc parser that supports comments and is more faithful\n  to HTTPie, see #207 (@blyxxyz)\n- Add browser-style text encoding detection, see #203 (@blyxxyz)\n- Enable using OS certificate store with rustls, see #225 (@austinbutler)\n- Improve quoting and update options from `--curl`, see #200 (@blyxxyz)\n\n### Bug fixes\n- Expand tilde in request items that contain a path, see #209 (@ducaale)\n- Get version from `-V` when generating manpages, see #214 (@ducaale)\n\n### Other\n- Statically link C-runtime for MSVC Windows, see #221 (@ducaale)\n- Add `install.ps1` for Windows, see #220 (@ChrisK-0)\n- Add aarch64 support, see #213 (@myhro)\n\n## [0.14.1] - 2021-11-26\n### Bug fixes\n- Do not print response body unconditionally, see #197 (@blyxxyz)\n\n### Other\n- Do not rebuild when no syntax or theme file has changed, see #194 (@blyxxyz)\n- Remove curl from `dev-dependencies` by replacing httpmock with hyper, see #190 (@ducaale)\n\n## [0.14.0] - 2021-11-15\n### Features\n- Add `--http-version` for forcing a specific http version, see #161 (@ducaale)\n- Support overwriting response's mime and charset via `--response-mime` and `--response-charset`\n  respectively, see #184 (@ducaale)\n- Add support for digest authentication, see #176 (@ducaale)\n- Add `--ssl` option for forcing a specific TLS version, see #168 (@blyxxyz)\n\n### Bug fixes\n- Preserve case of `--verify` path, see #181 (@blyxxyz)\n\n### Other\n- Enable LTO on the release profile, see #177 (@sorairolake)\n- Replace `lazy_static` with `once_cell`, see #187 (@sorairolake)\n- Include enabled features in `--version` flag's output, see #188 and #191 (@sorairolake)\n- Support displaying units smaller than a second in download result, see #192 (@sorairolake)\n- Change to use binary prefix in `--download`, see #193 (@sorairolake)\n\n## [0.13.0] - 2021-09-16\n### Features\n- Add `--all` flag for printing intermediate requests and responses, see #137 (@ducaale)\n- Support customising what sections are printed from intermediary requests and responses\n  via the `--history-print` flag, see #137 (@ducaale)\n\n### Bug fixes\n- Apply header title case for consecutive dashes, see #170 (@blyxxyz)\n- Avoid printing unnecessary line separators when `--all` flag is used, see #174 (@ducaale)\n\n### Other\n- Include Debian package in release artifacts, see #172 (@ducaale)\n\n## [0.12.0] - 2021-08-06\n### Features\n- Add support for HTTPie's [Sessions](https://httpie.io/docs#sessions), see #125 (@ducaale)\n- Send and display headers names as title case for non-HTTP/2 requests and responses, see #167 (@blyxxyz)\n- Support using the system's TLS library via `--native-tls` flag, see #154 (@blyxxyz)\n- Support reading args from a config file, see #165 (@ducaale)\n\n## [0.11.0] - 2021-07-26\n### Features\n- Support `REQUESTS_CA_BUNDLE` & `CURL_CA_BUNDLE` env variables, see #146 (@ducaale)\n- Enable color and wrapping for `--help`, see #151 (@QuarticCat)\n- Add monokai theme, #157 (@ducaale)\n- handle responses compressed in deflate format, see #158 (@ducaale)\n- Support setting the filename for multipart uploads, see #164 (@blyxxyz)\n\n### Bug fixes\n- Do not hardcode `/tmp` in the install script, see #149 (@blyxxyz)\n\n### Other\n- Re-enable HTTP/2 adaptive window, see #150 (@blyxxyz)\n\n### Breaking changes\n- `--check-status` is now on by default. You can opt-out of this change by enabling xh's\n  [strict compatibility mode](https://github.com/ducaale/xh#strict-compatibility-mode),\n  see #155 (@ducaale)\n\n## [0.10.0] - 2021-05-17\n### Features\n- Support reading DataField and JsonField value from a file, see #118 (@ducaale)\n- Add percentage of progress to download progress bar, see #119 (@sorairolake)\n- Add the timeout flag, see #131 (@sorairolake)\n- Support installation via a shell script, see #122 (@ducaale)\n- Support reading request body from file, see #140 (@blyxxyz)\n\n### Bug fixes\n- Fix progress bar ETA when resuming download, see #116 (@blyxxyz)\n- Replace `deflate` in Accept-Encoding to `br`, see #128 (@sorairolake)\n- Set Accept-Encoding to `identity` in download mode, see #130 (@sorairolake)\n- Replace HTTP/2 adaptive window by fixed window to prevent crashes, see #138 (@blyxxyz)\n- Fix a bug where same file cannot be re-downloaded, see #139 (@ducaale)\n- Enforce accept-encoding to be `identity` in download mode, see #141 (@ducaale)\n\n### Other\n- Unvendor jsonxf, see #124 (@blyxxyz)\n- Add config file for clippy, see #123 (@sorairolake)\n\n## [0.9.2] - 2021-03-24\n### Bug fixes\n- Escape backslash in JSON highlighting definition, see #108 (@blyxxyz)\n- Do not require filenames to be valid unicode, see #112 (@blyxxyz)\n- Preserve the order of JSON keys in requests, see #113 (@ducaale)\n- Keep bar coloring consistent with other colored output\n  (e.g. don't color it if $NO_COLOR is set), see #114 (@blyxxyz)\n- Prevent mitsuhiko/indicatif#144 in narrow terminals, see #114 (@blyxxyz)\n\n### Other\n- JSON records are now separated by double newlines, see #109 (@blyxxyz)\n- Writing to a redirect or a file now doesn't stream unless you use --stream, like HTTPie,\n  and it properly decodes the response when it needs to, see #111 (@blyxxyz)\n- Writing formatted JSON to a file is now significantly faster, see #111 (@blyxxyz)\n- Use adaptive window for HTTP/2, see #115 (@blyxxyz)\n\n## [0.9.1] - 2021-03-16\n### Bug fixes\n- Don't include the `--verify` flag in usage when it is not used, see #100 (@ducaale)\n- Don't color progress indicators when color is disabled, see #103 (@ducaale)\n\n### Other\n- JSON requests coloring is now twice as fast, see #96 (@blyxxyz)\n- Unify flags and options in help, see #100 (@ducaale)\n- Replace ansi_term by termcolor for better Windows support, see #105 (@blyxxyz)\n\n## [0.9.0] - 2021-03-08\n### Features\n- Add `--no-FLAG` variants of flags. This is useful for disabling any flags you might have in your\n  alias, see #86 (@blyxxyz)\n- Support non-standard HTTP methods, see #89 (@blyxxyz)\n- Add support for getting credentials from .netrc plus a `--ignore-netrc` flag to disable that\n  functionality, see #87 (@dwink)\n\n## [0.8.1] - 2021-03-01\n### Features\n- Highlight Javascript and CSS, see #82 (@blyxxyz)\n- Check if text is actually JSON before formatting it, see #82 (@blyxxyz)\n- Default to a content-type of application/json when reading a body from stdin, see #82 (@blyxxyz)\n\n## [0.8.0] - 2021-02-28\n### Features\n- More robust detection of the method and URL arguments, see #55 (@blyxxyz)\n- Improvements to the generation downloaded filenames, see #56 (@blyxxyz)\n- `--continue` now works for resuming downloads. It was incomplete before, see #59 (@blyxxyz)\n- `--check-status` is supported, and is automatically active for downloads\n  (so you don't download error pages), see #59 (@blyxxyz)\n- Add the `--proxy` option, see #62 (@otaconix)\n- Add `--bearer` flag for Bearer Authentication and remove `--auth-type`, see #64 (@blyxxyz)\n- Add support for manpages, see #64 (@blyxxyz)\n- Add _help_ subcommand for printing long help and update `--help` to print short help, see #64 (@blyxxyz)\n- Support escaping characters in request items with backslash, see #66 (@blyxxyz)\n- Add support for `--verify` to skip the host’s SSL certificate verification, see #44 (@jihchi, @otaconix)\n- Add support for `--cert/cert-key` for using client side certificate for the SSL communication, see #44 (@jihchi, @otaconix)\n- Add `--curl` flag to print equivalent curl command, see #69 (@blyxxyz)\n- Replace `--default-scheme` by `--https`. `--default-scheme` is still kept as an undocumented flag, see #73 (@blyxxyz)\n- If `xh` is invoked as `xhs`, `https`, or `xhttps`, run as if `--https` was used, see #73 (@blyxxyz)\n- Support `NO_COLOR` environment variable to turn colors off by default, see #73 (@blyxxyz)\n- Make `--json`/`--form`/`--multipart` override each other and force content-type. If you use multiple of those flags,\n  all but the last will be ignored. And if you use them without request items the appropriate headers will still be set,\n  see #73 (@blyxxyz)\n- Try to detect undeclared JSON response bodies: If the response is javascript or plain text,\n  check if it's JSON, see #73 (@blyxxyz)\n- Add shell autocompletion generation, see #76 (@blyxxyz)\n\n### Other\n- Make structopt usage more consistent, see #67 (@blyxxyz)\n- Remove use of async, make --stream work consistently, see #41 (@blyxxyz)\n- Introduce clippy and fmt in CI, see #75 (@ducaale)\n\n## [0.7.0] - 2021-02-12\n### Features\n- Follow redirects if downloading a file, see #51 (@blyxxyz)\n- Allow form value regex to match newlines, see #46 (@blyxxyz)\n- Adds --headers option, see #42 (@sanpii)\n\n### Other\n- Rename ht binary to xh\n\n## [0.6.0] - 2021-02-08\n### Features\n- Add support for OPTIONS HTTP method, see #17 (@plombard)\n- Add `--body` flag for printing only response body, see #38 (@idanski)\n- Add content length to file upload stream, see #32 (@blyxxyz)\n- Include User-Agent header in outgoing requests, see #33 (@blyxxyz)\n\n### Other\n- Ensure filename from `content-disposition` doesn't overwrite existing files,\n  isn't a hidden file, or doesn't end up outside the current directory,\n  see #37 (@blyxxyz)\n- Bubble errors up to main() instead of panicking see #37 (@blyxxyz)\n\n## [0.5.0] - 2021-02-07\n### Features\n- Add support for HEAD requests, see #16 (@Till--H)\n- Support setting the content-type for files in multipart requests e.g\n  `ht httpbin.org/post --multipart pic@cat.png;type=image/png`\n- Add `--follow` and `--max-redirects` for configuring redirect behaviour, see #19 (@Till--H)\n\n### Bug fixes\n- Render white text as the default foreground color, see #21 (@blyxxyz)\n- Don't insert lines when streaming json.\n- Do not explicitly add `Host` header, see #26 (@blyxxyz)\n\n### Other\n- Init parsing regex for RequestItem once, see #22 (@jRimbault)\n\n## [0.4.0] - 2021-02-06\n### Features\n- Support streaming responses. This on by default for unformatted responses and can also\n  be enabled via the `--stream` flag\n\n## [0.3.5] - 2021-01-31\n### Features\n- Support output redirection for downloads e.g `ht -d httpbin.org/json > temp.json`\n\n### Other\n- Upgrade to Tokio 1.x.\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"xh\"\nversion = \"0.25.3\"\nauthors = [\"ducaale <sharaf.13@hotmail.com>\"]\nedition = \"2024\"\nrust-version = \"1.85.0\"\nlicense = \"MIT\"\ndescription = \"Friendly and fast tool for sending HTTP requests\"\ndocumentation = \"https://github.com/ducaale/xh\"\nhomepage = \"https://github.com/ducaale/xh\"\nrepository = \"https://github.com/ducaale/xh\"\nreadme = \"README.md\"\nkeywords = [\"http\"]\ncategories = [\"command-line-utilities\"]\nexclude = [\"assets/xhs\", \"assets/xhs.1.gz\"]\n\n[dependencies]\nanyhow = \"1.0.38\"\nbrotli = { version = \"8\", default-features = false, features = [\"std\"] }\nchardetng = \"0.1.15\"\nclap = { version = \"4.4\", features = [\"derive\", \"wrap_help\", \"string\"] }\nclap_complete = \"4.4\"\nclap_complete_nushell = \"4.4\"\ncookie_store = { version = \"0.22.0\", features = [\"preserve_order\"] }\ndigest_auth = \"0.3.0\"\ndirs = \"6.0\"\nencoding_rs = \"0.8.28\"\nencoding_rs_io = \"0.1.7\"\nflate2 = \"1.0.22\"\n# Add \"tracing\" feature to hyper once it stabilizes\nhyper = { version = \"1.2\", default-features = false }\nindicatif = \"0.18\"\njsonxf = \"1.1.0\"\nmemchr = \"2.4.1\"\nmime = \"0.3.16\"\nmime2ext = \"0.1.0\"\nmime_guess = \"2.0\"\nos_display = \"0.1.3\"\npem = \"3.0\"\nregex-lite = \"0.1.5\"\nreqwest_cookie_store = \"0.10.0\"\nroff = \"0.2.1\"\nrpassword = \"7.2.0\"\nserde = { version = \"1.0\", features = [\"derive\"] }\nserde-transcode = \"1.1.1\"\nserde_json = { version = \"1.0\", features = [\"preserve_order\"] }\nserde_urlencoded = \"0.7.0\"\nsupports-hyperlinks = \"3.0.0\"\ntermcolor = \"1.1.2\"\ntime = \"0.3.16\"\nhumantime = \"2.2.0\"\nunicode-width = \"0.1.9\"\nurl = \"2.2.2\"\nruzstd = { version = \"0.7\", default-features = false, features = [\"std\"] }\nenv_logger = { version = \"0.11.3\", default-features = false, features = [\"color\", \"auto-color\", \"humantime\"] }\nlog = \"0.4.21\"\nbase64 = \"0.22.1\"\nform_urlencoded = \"1.0.1\"\nhttpsig-hyper = { version = \"0.0.24\", optional = true, default-features = false, features = [\"blocking\", \"rsa-signature\"] }\nsha2 = { version = \"0.10\", optional = true, default-features = false }\n\n# Enable logging in transitive dependencies.\n# The rustls version number should be kept in sync with hyper/reqwest.\nrustls = { version = \"0.23.25\", optional = true, default-features = false, features = [\"logging\"] }\ntracing = { version = \"0.1.41\", default-features = false, features = [\"log\"] }\npercent-encoding = \"2.3.1\"\nsanitize-filename = \"0.6.0\"\nquick-xml = \"0.38\"\n\n[dependencies.reqwest]\nversion = \"0.13.2\"\ndefault-features = false\nfeatures = [\"json\", \"form\", \"multipart\", \"blocking\", \"socks\", \"cookies\", \"http2\", \"system-proxy\"]\n\n[dependencies.syntect]\nversion = \"5.1\"\ndefault-features = false\nfeatures = [\"parsing\", \"dump-load\", \"regex-onig\"]\n\n[target.'cfg(not(any(target_os = \"android\", target_os = \"fuchsia\", target_os = \"illumos\", target_os = \"ios\", target_os = \"linux\", target_os = \"macos\", target_os = \"solaris\", target_os = \"tvos\", target_os = \"visionos\", target_os = \"watchos\")))'.dependencies]\nnetwork-interface = { version = \"1.0.0\", optional = true }\n\n[build-dependencies.syntect]\nversion = \"5.1\"\ndefault-features = false\nfeatures = [\"dump-create\", \"plist-load\", \"regex-onig\", \"yaml-load\"]\n\n[dev-dependencies]\nassert_cmd = \"2.0.8\"\nindoc = \"2.0\"\nrand = \"0.8.3\"\npredicates = \"3.0\"\nhyper = { version = \"1.2\", features = [\"server\"] }\ntokio = { version = \"1\", features = [\"rt\", \"sync\", \"time\"] }\ntempfile = \"3.2.0\"\nhyper-util = { version = \"0.1.3\", features = [\"server\"] }\nhttp-body-util = \"0.1.1\"\n\n[features]\ndefault = [\"online-tests\", \"rustls\", \"network-interface\"]\nnative-tls = [\"reqwest/native-tls\"]\nrustls = [\"reqwest/rustls\", \"dep:rustls\"]\nhttp3 = [\"reqwest/http3\"]\nhttp-message-signatures = [\"dep:httpsig-hyper\", \"dep:sha2\"]\n\n# To be used by platforms that don't support binding to interface via SO_BINDTODEVICE\n# Ideally, this would be auto-disabled on platforms that don't need it\n# However: https://github.com/rust-lang/cargo/issues/1197\n# Also, see https://github.com/ducaale/xh/issues/330\nnetwork-interface = [\"dep:network-interface\"]\n\nonline-tests = []\nipv6-tests = []\n\n[package.metadata.cross.build.env]\npassthrough = [\"CARGO_PROFILE_RELEASE_LTO\"]\n\n[package.metadata.deb]\nfeatures = []\nsection = \"web\"\nlicense-file = \"LICENSE\"\npreserve-symlinks = true\nassets = [\n  [\"target/release/xh\", \"usr/bin/\", \"755\"],\n  [\"assets/xhs\", \"usr/bin/\", \"777\"],\n  [\"CHANGELOG.md\", \"usr/share/doc/xh/NEWS\", \"644\"],\n  [\"README.md\", \"usr/share/doc/xh/README\", \"644\"],\n  [\"doc/xh.1\", \"usr/share/man/man1/xh.1\", \"644\"],\n  [\"assets/xhs.1.gz\", \"usr/share/man/man1/xhs.1.gz\", \"777\"],\n  [\"completions/xh.bash\", \"usr/share/bash-completion/completions/xh\", \"644\"],\n  [\"completions/xh.fish\", \"usr/share/fish/vendor_completions.d/xh.fish\", \"644\"],\n  [\"completions/_xh\", \"usr/share/zsh/vendor-completions/\", \"644\"],\n]\nextended-description = \"\"\"\\\nxh is a friendly and fast tool for sending HTTP requests.\nIt reimplements as much as possible of HTTPie's excellent design, with a focus\non improved performance.\n\"\"\"\n"
  },
  {
    "path": "FAQ.md",
    "content": "<h3 name=\"header-value-encoding\">Why do some HTTP headers show up mangled?</h3>\n\nHTTP header values are officially only supposed to contain ASCII. Other bytes are \"opaque data\":\n\n> Historically, HTTP has allowed field content with text in the ISO-8859-1 charset [[ISO-8859-1](https://datatracker.ietf.org/doc/html/rfc7230#ref-ISO-8859-1)], supporting other charsets only through use of [[RFC2047](https://datatracker.ietf.org/doc/html/rfc2047)] encoding.  In practice, most HTTP header field values use only a subset of the US-ASCII charset [[USASCII](https://datatracker.ietf.org/doc/html/rfc7230#ref-USASCII)].  Newly defined header fields SHOULD limit their field values to US-ASCII octets.  A recipient SHOULD treat other octets in field content (obs-text) as opaque data.\n\n([RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4))\n\nIn practice some headers are for some purposes treated like UTF-8, which supports all languages and characters in Unicode. But if you try to access header values through a browser's `fetch()` API or view them in the developer tools then they tend to be decoded as ISO-8859-1, which only supports a very limited number of characters and may not be the actual intended encoding.\n\nxh as of version 0.23.0 shows the ISO-8859-1 decoding by default to avoid a confusing difference with web browsers. If the value looks like valid UTF-8 then it additionally shows the UTF-8 decoding.\n\nThat is, the following request:\n```console\nxh -v https://example.org Smile:☺\n```\nDisplays the `Smile` header like this:\n```\nSmile: â�º (UTF-8: ☺)\n```\nThe server will probably see `â�º` instead of the smiley. Or it might see `☺` after all. It depends!\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Mohamed Daahir\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# xh\n[![Version info](https://img.shields.io/crates/v/xh.svg)](https://crates.io/crates/xh)\n[![Packaging status](https://repology.org/badge/tiny-repos/xh.svg)](https://repology.org/project/xh/versions)\n\n`xh` is a friendly and fast tool for sending HTTP requests. It reimplements as much\nas possible of [HTTPie's](https://httpie.io/) excellent design, with a focus\non improved performance.\n\n[![asciicast](/assets/xh-demo.gif)](https://asciinema.org/a/475190)\n\n## Installation\n\n### via cURL (Linux & macOS)\n\n```\ncurl -sfL https://raw.githubusercontent.com/ducaale/xh/master/install.sh | sh\n```\n\n### via Powershell (Windows)\n\n```\niwr -useb https://raw.githubusercontent.com/ducaale/xh/master/install.ps1 | iex\n```\n\n\n### via a package manager\n\n| OS                            | Method     | Command                                    |\n|-------------------------------|------------|--------------------------------------------|\n| Any                           | Cargo\\*    | `cargo install xh --locked`                |\n| Any                           | [Huber]    | `huber install xh`                         |\n| Android ([Termux])            | pkg        | `pkg install xh`                           |\n| Android ([Magisk]/[KernelSU]) | MMRL\\*\\*   | `mmrl install xhhttp`                      |\n| Alpine Linux                  | apk\\*\\*\\*  | `apk add xh`                               |\n| Arch Linux                    | Pacman     | `pacman -S xh`                             |\n| Debian & Ubuntu               | Apt\\*\\*\\*\\*| `sudo apt install xh`                      |\n| FreeBSD                       | FreshPorts | `pkg install xh`                           |\n| NetBSD                        | pkgsrc     | `pkgin install xh`                         |\n| Linux & macOS                 | Nixpkgs    | `nix-env -iA nixpkgs.xh`                   |\n| Linux & macOS                 | [Flox]     | `flox install xh`                          |\n| Linux & macOS                 | Homebrew   | `brew install xh`                          |\n| Linux & macOS                 | [Hermit]   | `hermit install xh`                        |\n| macOS                         | MacPorts   | `sudo port install xh`                     |\n| Windows                       | Scoop      | `scoop install xh`                         |\n| Windows                       | Chocolatey | `choco install xh`                         |\n| Windows                       | Winget     | `winget add ducaale.xh`                    |\n\n\\* Make sure that you have Rust 1.85 or later installed\n\n\\*\\* You will need to install the [MMRL CLI](https://github.com/DerGoogler/MMRL-CLI/releases)\n\n\\*\\*\\* Built with native-tls only.\n\n\\*\\*\\*\\* Available since Debian 13 and Ubuntu 25.04. Built with native-tls only.\n\n[Huber]: https://github.com/innobead/huber#installing-huber\n[Magisk]: https://github.com/topjohnwu/Magisk\n[KernelSU]: https://kernelsu.org\n[Termux]: https://github.com/termux/termux-app\n[Flox]: https://flox.dev/docs/\n[Hermit]: https://cashapp.github.io/hermit/\n\n### via pre-built binaries\nThe [release page](https://github.com/ducaale/xh/releases) contains prebuilt binaries for Linux, macOS and Windows.\n\n## Usage\n```\nUsage: xh [OPTIONS] <[METHOD] URL> [REQUEST_ITEM]...\n\nArguments:\n  <[METHOD] URL>     The request URL, preceded by an optional HTTP method\n  [REQUEST_ITEM]...  Optional key-value pairs to be included in the request.\n\nOptions:\n  -j, --json                             (default) Serialize data items from the command line as a JSON object\n  -f, --form                             Serialize data items from the command line as form fields\n      --multipart                        Like --form, but force a multipart/form-data request even without files\n      --raw <RAW>                        Pass raw request data without extra processing\n      --pretty <STYLE>                   Controls output processing [possible values: all, colors, format, none]\n      --format-options <FORMAT_OPTIONS>  Set output formatting options\n  -s, --style <THEME>                    Output coloring style [possible values: auto, solarized, monokai, fruity]\n      --response-charset <ENCODING>      Override the response encoding for terminal display purposes\n      --response-mime <MIME_TYPE>        Override the response mime type for coloring and formatting for the terminal\n  -p, --print <FORMAT>                   String specifying what the output should contain\n  -h, --headers                          Print only the response headers. Shortcut for --print=h\n  -b, --body                             Print only the response body. Shortcut for --print=b\n  -m, --meta                             Print only the response metadata. Shortcut for --print=m\n  -v, --verbose...                       Print the whole request as well as the response\n      --debug                            Print full error stack traces and debug log messages\n      --all                              Show any intermediary requests/responses while following redirects with --follow\n  -P, --history-print <FORMAT>           The same as --print but applies only to intermediary requests/responses\n  -q, --quiet...                         Do not print to stdout or stderr\n  -S, --stream                           Always stream the response body\n  -x, --compress...                      Content compressed (encoded) with Deflate algorithm\n  -o, --output <FILE>                    Save output to FILE instead of stdout\n  -d, --download                         Download the body to a file instead of printing it\n  -c, --continue                         Resume an interrupted download. Requires --download and --output\n      --session <FILE>                   Create, or reuse and update a session\n      --session-read-only <FILE>         Create or read a session without updating it from the request/response exchange\n  -A, --auth-type <AUTH_TYPE>            Specify the auth mechanism [possible values: basic, bearer, digest]\n  -a, --auth <USER[:PASS] | TOKEN>       Authenticate as USER with PASS (-A basic|digest) or with TOKEN (-A bearer)\n      --ignore-netrc                     Do not use credentials from .netrc\n      --offline                          Construct HTTP requests without sending them anywhere\n      --check-status                     (default) Exit with an error status code if the server replies with an error\n  -F, --follow                           Do follow redirects\n      --max-redirects <NUM>              Number of redirects to follow. Only respected if --follow is used\n      --timeout <SEC>                    Connection timeout of the request\n      --proxy <PROTOCOL:URL>             Use a proxy for a protocol. For example: --proxy https:http://proxy.host:8080\n      --verify <VERIFY>                  If \"no\", skip SSL verification. If a file path, use it as a CA bundle\n      --cert <FILE>                      Use a client side certificate for SSL\n      --cert-key <FILE>                  A private key file to use with --cert\n      --ssl <VERSION>                    Force a particular TLS version [possible values: auto, tls1, tls1.1, tls1.2, tls1.3]\n      --https                            Make HTTPS requests if not specified in the URL\n      --http-version <VERSION>           HTTP version to use [possible values: 1.0, 1.1, 2, 2-prior-knowledge, 3-prior-knowledge]\n      --resolve <HOST:ADDRESS>           Override DNS resolution for specific domain to a custom IP\n      --interface <NAME>                 Bind to a network interface or local IP address\n  -4, --ipv4                             Resolve hostname to ipv4 addresses only\n  -6, --ipv6                             Resolve hostname to ipv6 addresses only\n      --unix-socket <FILE>               Connect using a Unix domain socket\n  -I, --ignore-stdin                     Do not attempt to read stdin\n      --curl                             Print a translation to a curl command\n      --curl-long                        Use the long versions of curl's flags\n      --generate <KIND>                  Generate shell completions or man pages\n      --help                             Print help\n  -V, --version                          Print version\n\nEach option can be reset with a --no-OPTION argument.\n```\n\nRun `xh help` for more detailed information.\n\n### Request Items\n\n`xh` uses [HTTPie's request-item syntax](https://httpie.io/docs/cli/request-items) to set headers, request body, query string, etc.\n\n- `=`/`:=` for setting the request body's JSON or form fields (`=` for strings and `:=` for other JSON types).\n- `==` for adding query strings.\n- `@` for including files in multipart requests e.g `picture@hello.jpg` or `picture@hello.jpg;type=image/jpeg;filename=goodbye.jpg`.\n- `:` for adding or removing headers e.g `connection:keep-alive` or `connection:`.\n- `;` for including headers with empty values e.g `header-without-value;`.\n\nAn `@` prefix can be used to read a value from a file. For example: `x-api-key:@api-key.txt`.\n\nThe request body can also be read from standard input, or from a file using `@filename`.\n\nTo construct a complex JSON object, a JSON path can be used as a key e.g `app[container][0][id]=090-5`.\nFor more information on this syntax, refer to https://httpie.io/docs/cli/nested-json.\n\n### Shorthand form for URLs\n\nSimilar to HTTPie, specifying the scheme portion of the request URL is optional, and a leading colon works as shorthand\nfor localhost. `:8000` is equivalent to `localhost:8000`, and `:/path` is equivalent to `localhost/path`.\n\nURLs can have a leading `://` which allows quickly converting a URL into a valid xh or HTTPie command. For example\n`http://httpbin.org/json` becomes `http ://httpbin.org/json`.\n\n\n```sh\nxh http://localhost:3000/users # resolves to http://localhost:3000/users\nxh localhost:3000/users        # resolves to http://localhost:3000/users\nxh :3000/users                 # resolves to http://localhost:3000/users\nxh :/users                     # resolves to http://localhost:80/users\nxh example.com                 # resolves to http://example.com\nxh ://example.com              # resolves to http://example.com\n```\n\n### Making HTTPS requests by default\n\n`xh` will default to HTTPS scheme if the binary name is one of `xhs`, `https`, or `xhttps`. If you have installed `xh`\nvia a package manager, both `xh` and `xhs` should be available by default. Otherwise, you need to create one like this:\n\n```sh\ncd /path/to/xh && ln -s ./xh ./xhs\nxh httpbin.org/get  # resolves to http://httpbin.org/get\nxhs httpbin.org/get # resolves to https://httpbin.org/get\n```\n\n### Strict compatibility mode\n\nIf `xh` is invoked as `http` or `https` (by renaming the binary), or if the `XH_HTTPIE_COMPAT_MODE` environment variable is set,\nit will run in HTTPie compatibility mode. The only current difference is that `--check-status` is not enabled by default.\n\n## Examples\n\n```sh\n# Send a GET request\nxh httpbin.org/json\n\n# Send a POST request with body {\"name\": \"ahmed\", \"age\": 24}\nxh httpbin.org/post name=ahmed age:=24\n\n# Send a GET request with querystring id=5&sort=true\nxh get httpbin.org/json id==5 sort==true\n\n# Send a GET request and include a header named x-api-key with value 12345\nxh get httpbin.org/json x-api-key:12345\n\n# Send a POST request with body read from stdin.\necho \"[1, 2, 3]\" | xh post httpbin.org/post\n\n# Send a PUT request and pipe the result to less\nxh put httpbin.org/put id:=49 age:=25 | less\n\n# Download and save to res.json\nxh -d httpbin.org/json -o res.json\n\n# Make a request with a custom user agent\nxh httpbin.org/get user-agent:foobar\n```\n\n## How xh compares to HTTPie\n\n### Advantages\n\n- Improved startup speed.\n- Available as a single statically linked binary that's easy to install and carry around.\n- HTTP/2 support.\n- Builtin translation to curl commands with the `--curl` flag.\n- Short, cheatsheet-style output from `--help`. (For longer output, pass `help`.)\n\n### Disadvantages\n\n- Not all of HTTPie's features are implemented. ([#4](https://github.com/ducaale/xh/issues/4))\n- No plugin system.\n- General immaturity. HTTPie is old and well-tested.\n- Worse documentation.\n\n## Similar or related Projects\n\n- [curlie](https://github.com/rs/curlie) - frontend to cURL that adds the ease of use of httpie\n- [httpie-go](https://github.com/nojima/httpie-go) - httpie-like HTTP client written in Go\n- [curl2httpie](https://github.com/dcb9/curl2httpie) - convert command arguments between cURL and HTTPie\n"
  },
  {
    "path": "RELEASE-CHECKLIST.md",
    "content": "## Release Checklist\n\n- Update `README.md`'s Usage section with the output of `xh --help`\n- Update `CHANGELOG.md` (rename unreleased header to the current date, add any missing changes).\n- Run `cargo update` to update dependencies.\n- Bump up the version in `Cargo.toml` and run `cargo check` to update `Cargo.lock`.\n- Run the following to update shell-completion files and man pages.\n  ```sh\n  cargo run --features=native-tls -- --generate complete-bash > completions/xh.bash\n  cargo run --features=native-tls -- --generate complete-elvish > completions/xh.elv\n  cargo run --features=native-tls -- --generate complete-fish > completions/xh.fish\n  cargo run --features=native-tls -- --generate complete-nushell > completions/xh.nu\n  cargo run --features=native-tls -- --generate complete-powershell > completions/_xh.ps1\n  cargo run --features=native-tls -- --generate complete-zsh > completions/_xh\n  cargo run --features=native-tls -- --generate man > doc/xh.1\n  ```\n- Commit changes and push them to remote.\n- Add git tag e.g `git tag v0.9.0`.\n- Push the local tags to remote i.e `git push --tags` which will start the CI release action.\n- Publish to crates.io by running `cargo publish`.\n"
  },
  {
    "path": "assets/README.md",
    "content": "## Syntaxes and themes used\n- [Sublime-HTTP](https://github.com/samsalisbury/Sublime-HTTP)\n- [json-kv](https://github.com/aurule/json-kv)\n- [Sublime Packages](https://github.com/sublimehq/Packages/tree/fa6b8629c95041bf262d4c1dab95c456a0530122)\n- [ansi-dark theme](https://github.com/sharkdp/bat/blob/master/assets/themes/ansi-dark.tmTheme)\n- Solarized and Monokai are based on ansi-dark with color values taken from the [pygments](https://github.com/pygments/pygments) library\n  - [Solarized](https://github.com/pygments/pygments/blob/master/pygments/styles/solarized.py)\n  - [Monokai](https://github.com/pygments/pygments/blob/master/pygments/styles/monokai.py)\n  - [Fruity](https://github.com/pygments/pygments/blob/master/pygments/styles/fruity.py)\n\n## Tools used to create xh-demo.gif\n- [asciinema](https://github.com/asciinema/asciinema) for the initial recording.\n- [asciinema-edit](https://github.com/cirocosta/asciinema-edit) to speed up the recording.\n- [asciicast2gif](https://github.com/asciinema/asciicast2gif) to produce the final GIF. The\n  default font didn't look great so I modified it to use [Cascadia Mono](https://github.com/microsoft/cascadia-code).\n"
  },
  {
    "path": "assets/syntax/basic/json.sublime-syntax",
    "content": "%YAML 1.2\n---\n# http://www.sublimetext.com/docs/3/syntax.html\nname: JSON Key-Value\nfile_extensions:\n  - json\nscope: source.json\ncontexts:\n  main:\n    - match: //.*\n      comment: Single-line comment\n      scope: comment.single.line.jsonkv\n    - match: /\\*\n      comment: Multi-line comment\n      push:\n        - meta_scope: comment.block.jsonkv\n        - match: \\*/\n          pop: true\n    - match: '(\")(?i)([^\\\\\"]+)(\")\\s*?:'\n      comment: Key names\n      captures:\n        1: keyword.other.name.jsonkv.start\n        2: keyword.other.name.jsonkv\n        3: keyword.other.name.jsonkv.end\n    - match: '\"'\n      comment: String values\n      push:\n        - meta_scope: string.quoted.jsonkv\n        - match: '\"'\n          pop: true\n        - match: '\\\\[tnr\"]'\n          comment: Escape characters\n          scope: constant.character.escape.jsonkv\n    - match: \\d+(?:.\\d+)?\n      comment: Numeric values\n      scope: constant.numeric.jsonkv\n    - match: true|false\n      comment: Boolean values\n      scope: constant.language.boolean.jsonkv\n    - match: \"null\"\n      comment: Null value\n      scope: constant.language.null.jsonkv\n"
  },
  {
    "path": "assets/syntax/large/css.sublime-syntax",
    "content": "%YAML 1.2\n---\n# Derived from https://github.com/i-akhmadullin/Sublime-CSS3\nname: CSS\nfile_extensions:\n  - css\n  - css.erb\n  - css.liquid\nscope: source.css\nvariables:\n  # Many variable names taken directly from https://www.w3.org/TR/css3-selectors/#lex\n  unicode: '\\\\\\h{1,6}[ \\t\\n\\f]?'\n  escape: '(?:{{unicode}}|\\\\[^\\n\\f\\h])'\n  nonascii: '[\\p{L}\\p{M}\\p{S}\\p{N}&&[^[:ascii:]]]'\n  nmstart: '(?:[[_a-zA-Z]{{nonascii}}]|{{escape}})'\n  nmchar: '(?:[[-\\w]{{nonascii}}]|{{escape}})'\n  ident: '(?:--{{nmchar}}+|-?{{nmstart}}{{nmchar}}*)'\n\n  # Types\n  # https://www.w3.org/TR/css3-values/#numeric-types\n  integer: '(?:[-+]?\\d+)'\n  number: '[-+]?(?:(?:\\d*\\.\\d+(?:[eE]{{integer}})*)|{{integer}})'\n\n  # Units\n  # https://www.w3.org/TR/css3-values/#lengths\n  font_relative_lengths: '(?i:em|ex|ch|rem)'\n  viewport_percentage_lengths: '(?i:vw|vh|vmin|vmax)'\n  absolute_lengths: '(?i:cm|mm|q|in|pt|pc|px|fr)'\n  angle_units: '(?i:deg|grad|rad|turn)'\n  duration_units: '(?i:s|ms)'\n  frequency_units: '(?i:Hz|kHz)'\n  resolution_units: '(?i:dpi|dpcm|dppx)'\n\n  custom_element_chars: |-\n    (?x:\n        [-_a-z0-9\\x{00B7}]\n      | \\\\\\.\n      | [\\x{00C0}-\\x{00D6}]\n      | [\\x{00D8}-\\x{00F6}]\n      | [\\x{00F8}-\\x{02FF}]\n      | [\\x{0300}-\\x{037D}]\n      | [\\x{037F}-\\x{1FFF}]\n      | [\\x{200C}-\\x{200D}]\n      | [\\x{203F}-\\x{2040}]\n      | [\\x{2070}-\\x{218F}]\n      | [\\x{2C00}-\\x{2FEF}]\n      | [\\x{3001}-\\x{D7FF}]\n      | [\\x{F900}-\\x{FDCF}]\n      | [\\x{FDF0}-\\x{FFFD}]\n      | [\\x{10000}-\\x{EFFFF}]\n    )\n\n  combinators: '(?:>{1,3}|[~+])'\n\n  # Predefined Counter Styles\n  # https://drafts.csswg.org/css-counter-styles-3/#predefined-counters\n  counter_styles: |-\n    (?xi:\n        arabic-indic | armenian | bengali | cambodian | circle\n      | cjk-decimal | cjk-earthly-branch | cjk-heavenly-stem | decimal-leading-zero\n      | decimal | devanagari | disclosure-closed | disclosure-open | disc\n      | ethiopic-numeric | georgian | gujarati | gurmukhi | hebrew\n      | hiragana-iroha | hiragana | japanese-formal | japanese-informal\n      | kannada | katakana-iroha | katakana | khmer\n      | korean-hangul-formal | korean-hanja-formal | korean-hanja-informal | lao\n      | lower-alpha | lower-armenian | lower-greek | lower-latin | lower-roman\n      | malayalam | mongolian | myanmar | oriya | persian\n      | simp-chinese-formal | simp-chinese-informal\n      | square | tamil | telugu | thai | tibetan\n      | trad-chinese-formal | trad-chinese-informal\n      | upper-alpha | upper-armenian | upper-latin | upper-roman\n    )\n\ncontexts:\n  main:\n    - include: comment-block\n    - include: selector\n    - include: at-rules\n    - include: property-list\n\n  at-rules:\n    - include: at-charset\n    - include: at-counter-style\n    - include: at-custom-media\n    - include: at-document\n    - include: at-font-face\n    - include: at-import\n    - include: at-keyframes\n    - include: at-media\n    - include: at-namespace\n    - include: at-page\n    - include: at-supports\n\n  # When including `color-values` and `color-adjuster-functions`, make sure it is\n  # included after the color adjustors to prevent `color-values` from consuming\n  # conflicting function names & color constants such as `red`, `green`, or `blue`.\n  color-values:\n    - include: color-functions\n      # https://www.w3.org/TR/CSS22/syndata.html#color-units\n    - match: \\b(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow)\\b\n      scope: support.constant.color.w3c-standard-color-name.css\n      # https://www.w3.org/TR/css3-color/#svg-color\n    - match: \\b(aliceblue|antiquewhite|aquamarine|azure|beige|bisque|blanchedalmond|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|gainsboro|ghostwhite|gold|goldenrod|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|limegreen|linen|magenta|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|oldlace|olivedrab|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|rebeccapurple|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|thistle|tomato|turquoise|violet|wheat|whitesmoke|yellowgreen)\\b\n      scope: support.constant.color.w3c-extended-color-keywords.css\n      # Special Color Keywords\n      # https://www.w3.org/TR/css3-color/#currentcolor\n      # https://www.w3.org/TR/css3-color/#transparent-def\n    - match: \\b((?i)currentColor|transparent)\\b\n      scope: support.constant.color.w3c-special-color-keyword.css\n      # Hex Color\n    - match: '(#)(\\h{3}|\\h{6})\\b'\n      scope: constant.other.color.rgb-value.css\n      captures:\n        1: punctuation.definition.constant.css\n      # RGBA Hexadecimal Colors\n      # https://en.wikipedia.org/wiki/RGBA_color_space#RGBA_hexadecimal_.28word-order.29\n    - match: '(#)(\\h{4}|\\h{8})\\b'\n      scope: constant.other.color.rgba-value.css\n      captures:\n        1: punctuation.definition.constant.css\n\n  comment-block:\n    - match: /\\*\n      scope: punctuation.definition.comment.css\n      push:\n        - meta_scope: comment.block.css\n        - match: \\*/\n          scope: punctuation.definition.comment.css\n          pop: true\n\n  at-charset:\n    - match: \\s*((@)charset\\b)\\s*\n      captures:\n        1: keyword.control.at-rule.charset.css\n        2: punctuation.definition.keyword.css\n      push:\n        - meta_scope: meta.at-rule.charset.css\n        - include: at-rule-punctuation\n        - include: literal-string\n\n  # @counter-style\n  # https://drafts.csswg.org/css-counter-styles-3/#the-counter-style-rule\n  at-counter-style:\n    - match: \\s*((@)counter-style\\b)\\s+(?:(?i:\\b(decimal|none)\\b)|({{ident}}))\\s*(?=\\{|$)\n      captures:\n        1: keyword.control.at-rule.counter-style.css\n        2: punctuation.definition.keyword.css\n        3: invalid.illegal.counter-style-name.css\n        4: entity.other.counter-style-name.css\n      push:\n        - meta_scope: meta.at-rule.counter-style.css\n        - include: comment-block\n        - include: rule-list-terminator\n        - include: rule-list\n\n  at-custom-media:\n    - match: (?=\\s*@custom-media\\b)\n      push:\n        - match: ;\n          scope: punctuation.terminator.css\n          pop: true\n        - match: \\s*((@)custom-media)\n          captures:\n            1: keyword.control.at-rule.custom-media.css\n            2: punctuation.definition.keyword.css\n            3: support.constant.custom-media.css\n          push:\n            - meta_scope: meta.at-rule.custom-media.css\n            - match: \\s*(?=;)\n              pop: true\n            - include: media-query-list\n\n  # @document\n  # https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#at-document\n  at-document:\n    - match: '((@)document)'\n      captures:\n        1: keyword.control.at-rule.document.css\n        2: punctuation.definition.keyword.css\n      push:\n        - meta_scope: meta.at-rule.document.css\n        - match: '\\{'\n          scope: punctuation.definition.block.begin.css\n          push:\n            - meta_scope: meta.block.css\n            - match: '(?=\\})'\n              pop: true\n            - include: main\n        - match: '\\}'\n          scope: meta.block.css punctuation.definition.block.end.css\n          pop: true\n        - include: comment-block\n        - include: url-function\n        - include: url-prefix-function\n        - include: domain-function\n        - include: regexp-function\n        - include: comma-delimiter\n\n  at-font-face:\n    - match: '\\s*((@)font-face)\\s*(?=\\{|$)'\n      captures:\n        1: keyword.control.at-rule.font-face.css\n        2: punctuation.definition.keyword.css\n      push:\n        - meta_scope: meta.at-rule.font-face.css\n        - include: comment-block\n        - include: rule-list-terminator\n        - include: rule-list\n\n  at-import:\n    - match: \\s*((@)import\\b)\\s*\n      captures:\n        1: keyword.control.at-rule.import.css\n        2: punctuation.definition.keyword.css\n      push:\n        - meta_scope: meta.at-rule.import.css\n        - include: at-rule-punctuation\n        - include: literal-string\n        - include: url-function\n        - include: media-query-list\n\n  # https://drafts.csswg.org/css-animations/#propdef-animation-name\n  keyframe-name:\n    - match: '\\s*({{ident}})?'\n      captures:\n        1: entity.other.animation-name.css\n      push:\n        - match: '\\s*(?:(,)|(?=[{;]))'\n          captures:\n            1: punctuation.definition.arbitrary-repetition.css\n          pop: true\n\n  # @keyframes\n  # https://drafts.csswg.org/css-animations/#keyframes\n  at-keyframes:\n    - match: (?=\\s*@(?:-webkit-|-moz-|-o-)?keyframes\\b)\n      push:\n        - include: rule-list-terminator\n        - match: \\s*((@)(-webkit-|-moz-|-o-)?keyframes)\n          captures:\n            1: keyword.control.at-rule.keyframe.css\n            2: punctuation.definition.keyword.css\n            3: support.type.property-vendor.css\n            4: support.constant.keyframe.css\n          push:\n            - meta_scope: meta.at-rule.keyframe.css\n            - match: '\\s*(?=\\{)'\n              pop: true\n            - match: '\\s*(?=[^{;])'\n              push:\n                - match: '\\s*(?=[{;])'\n                  pop: true\n                - include: keyframe-name\n        - match: '\\s*(\\{)'\n          captures:\n            1: punctuation.section.property-list.css\n          push:\n            - match: '(?=\\})'\n              pop: true\n            - match: '\\s*(?:(from|to)|((?:\\.[0-9]+|[0-9]+(?:\\.[0-9]*)?)(%)))\\s*,?\\s*'\n              captures:\n                1: keyword.keyframe-selector.css\n                2: constant.numeric.css\n                3: keyword.other.unit.css\n            - include: main\n\n  at-media:\n    - match: (?=\\s*@media\\b)\n      push:\n        - include: rule-list-terminator\n        - match: \\s*((@)media)\n          captures:\n            1: keyword.control.at-rule.media.css\n            2: punctuation.definition.keyword.css\n            3: support.constant.media.css\n          push:\n            - meta_scope: meta.at-rule.media.css\n            - match: '\\s*(?=\\{)'\n              pop: true\n            - include: media-query-list\n        - match: '\\s*(\\{)'\n          captures:\n            1: punctuation.section.property-list.css\n          push:\n            - match: '(?=\\})'\n              pop: true\n            - include: main\n\n  media-query:\n    # Media Types: https://www.w3.org/TR/CSS21/media.html\n    - include: comment-block\n    - match: \\b(?i:all|aural|braille|embossed|handheld|print|projection|screen|speech|tty|tv)\\b\n      scope: support.constant.media.css\n    - match: '\\b(?i:and|or|not|only)\\b'\n      scope: keyword.operator.logic.media.css\n    - match: ','\n      scope: punctuation.definition.arbitrary-repetition.css\n    - match: \\(\n      scope: punctuation.definition.group.begin.css\n      push:\n        - match: \\)\n          scope: punctuation.definition.group.end.css\n          pop: true\n        - include: comment-block\n        - match: |-\n            (?x)\n            (\n                (-webkit-|-o-)?\n                ((min|max)-)?\n                (-moz-)?\n                (\n                    ((device-)?(height|width|aspect-ratio|pixel-ratio))|\n                    (color(-index)?)|monochrome|resolution\n                )\n            )|grid|scan|orientation\n            \\s*(?=[:)])\n          captures:\n            0: support.type.property-name.media.css\n            2: support.type.vendor-prefix.css\n            5: support.type.vendor-prefix.css\n          push:\n            - match: (:)|(?=\\))\n              captures:\n                1: punctuation.separator.key-value.css\n              pop: true\n        - match: \\b(portrait|landscape|progressive|interlace)\n          scope: support.constant.property-value.css\n        - match: \\s*(\\d+)(/)(\\d+)\n          captures:\n            1: constant.numeric.css\n            2: keyword.operator.arithmetic.css\n            3: constant.numeric.css\n        - include: numeric-values\n\n  media-query-list:\n    - match: '\\s*(?=[^{;])'\n      push:\n        - match: '\\s*(?=[{;])'\n          pop: true\n        - include: media-query\n\n  # @namespace\n  # https://www.w3.org/TR/css3-namespace/\n  at-namespace:\n    - match: '\\s*((@)namespace)\\s+({{ident}})?'\n      captures:\n        1: keyword.control.at-rule.namespace.css\n        2: punctuation.definition.keyword.css\n        3: entity.other.namespace-prefix.css\n      push:\n        - meta_scope: meta.at-rule.namespace.css\n        - include: at-rule-punctuation\n        - include: literal-string\n\n  # @page\n  # https://www.w3.org/TR/CSS2/page.html\n  at-page:\n    - match: '\\s*((@)page)\\s*(?:(:)(first|left|right))?\\s*(?=\\{|$)'\n      captures:\n        1: keyword.control.at-rule.page.css\n        2: punctuation.definition.keyword.css\n        3: punctuation.definition.entity.css\n        4: entity.other.pseudo-class.css\n      push:\n        - meta_scope: meta.at-rule.page.css\n        - include: comment-block\n        - include: rule-list-terminator\n        - include: rule-list\n\n  # @supports\n  # https://drafts.csswg.org/css-conditional-3/#at-supports\n  at-supports:\n    - match: '((@)supports)'\n      captures:\n        1: keyword.control.at-rule.supports.css\n        2: punctuation.definition.keyword.css\n      push:\n        - meta_scope: meta.at-rule.supports.css\n        - match: '\\{'\n          scope: punctuation.definition.block.begin.css\n          push:\n            - meta_scope: meta.block.css\n            - match: '(?=\\})'\n              pop: true\n            - include: rule-list-body\n            - include: main\n        - match: '\\}'\n          scope: meta.block.css punctuation.definition.block.end.css\n          pop: true\n        - include: at-supports-operators\n        - include: at-supports-parens\n\n  at-supports-operators:\n    - match: '\\b(?i:and|or|not)\\b'\n      scope: keyword.operator.logic.css\n\n  at-supports-parens:\n    - match: '\\('\n      scope: punctuation.definition.group.begin.css\n      push:\n        - meta_scope: meta.group.css\n        - match: '\\)'\n          scope: punctuation.definition.group.end.css\n          pop: true\n        - include: at-supports-operators\n        - include: at-supports-parens\n        - include: rule-list-body\n\n  property-list:\n    - match: '(?=\\{)'\n      push:\n        - match: '\\}'\n          scope: punctuation.section.property-list.css\n          pop: true\n        - include: rule-list\n\n  property-value-constants:\n    - match: |-\n            (?x)\\b(\n                absolute|active|add\n              | all(-(petite|small)-caps|-scroll)?\n              | alpha(betic)?\n              | alternate(-reverse)?\n              | always|annotation|antialiased|at\n              | auto(hiding-scrollbar)?\n              | avoid(-column|-page|-region)?\n              | background(-color|-image|-position|-size)?\n              | backwards|balance|baseline|below|bevel|bicubic|bidi-override|blink\n              | block(-line-height)?\n              | blur\n              | bold(er)?\n              | border(-bottom|-left|-right|-top)?-(color|radius|width|style)\n              | border-(bottom|top)-(left|right)-radius\n              | border-image(-outset|-repeat|-slice|-source|-width)?\n              | border(-bottom|-left|-right|-top|-collapse|-spacing|-box)?\n              | both|bottom\n              | box(-shadow)?\n              | break-(all|word)\n              | brightness\n              | butt(on)?\n              | capitalize\n              | cent(er|ral)\n              | char(acter-variant)?\n              | cjk-ideographic|clip|clone|close-quote\n              | closest-(corner|side)\n              | col-resize|collapse\n              | color(-stop|-burn|-dodge)?\n              | column((-count|-gap|-reverse|-rule(-color|-width)?|-width)|s)?\n              | common-ligatures|condensed|consider-shifts|contain\n              | content(-box|s)?\n              | contextual|contrast|cover\n              | crisp(-e|E)dges\n              | crop\n              | cross(hair)?\n              | da(rken|shed)\n              | default|dense|diagonal-fractions|difference|disabled\n              | discretionary-ligatures|disregard-shifts\n              | distribute(-all-lines|-letter|-space)?\n              | dotted|double|drop-shadow\n              | (nwse|nesw|ns|ew|sw|se|nw|ne|w|s|e|n)-resize\n              | ease(-in-out|-in|-out)?\n              | element|ellipsis|embed|end|EndColorStr|evenodd\n              | exclu(de(-ruby)?|sion)\n              | expanded\n              | (extra|semi|ultra)-(condensed|expanded)\n              | farthest-(corner|side)?\n              | fill(-box|-opacity)?\n              | filter|fixed|flat\n              | flex((-basis|-end|-grow|-shrink|-start)|box)?\n              | flip|flood-color\n              | font(-size(-adjust)?|-stretch|-weight)?\n              | forwards\n              | from(-image)?\n              | full-width|geometricPrecision|glyphs|gradient|grayscale\n              | grid(-height)?\n              | groove|hand|hanging|hard-light|height|help|hidden|hide\n              | historical-(forms|ligatures)\n              | horizontal(-tb)?\n              | hue\n              | ideograph(-alpha|-numeric|-parenthesis|-space|ic)\n              | inactive|include-ruby|infinite|inherit|initial\n              | inline(-block|-box|-flex(box)?|-line-height|-table)?\n              | inset|inside\n              | inter(-ideograph|-word|sect)\n              | invert|isolat(e|ion)|italic\n              | jis(04|78|83|90)\n              | justify(-all)?\n              | keep-all\n              | large[r]?\n              | last|left|letter-spacing\n              | light(e[nr]|ing-color)\n              | line(-edge|-height|-through)?\n              | linear(-gradient|RGB)?\n              | lining-nums|list-item|local|loose|lowercase|lr-tb|ltr\n              | lumin(osity|ance)|manual\n              | margin(-bottom|-box|-left|-right|-top)?\n              | marker(-offset|s)?\n              | mathematical\n              | max-(content|height|lines|size|width)\n              | medium|middle\n              | min-(content|height|width)\n              | miter|mixed|move|multiply|newspaper\n              | no-(change|clip|(close|open)-quote|(common|discretionary|historical)-ligatures|contextual|drop|repeat)\n              | none|nonzero|normal|not-allowed|nowrap|oblique\n              | offset(-after|-before|-end|-start)?\n              | oldstyle-nums|opacity|open-quote\n              | optimize(Legibility|Precision|Quality|Speed)\n              | order|ordinal|ornaments\n              | outline(-color|-offset|-width)?\n              | outset|outside|over(line|-edge|lay)\n              | padding(-bottom|-box|-left|-right|-top)?\n              | page|painted|paused\n              | perspective-origin\n              | petite-caps|pixelated|pointer\n              | pre(-line|-wrap)?\n              | preserve-3d\n              | progid:DXImageTransform.Microsoft.(Alpha|Blur|dropshadow|gradient|Shadow)\n              | progress\n              | proportional-(nums|width)\n              | radial-gradient|recto|region|relative\n              | repeat(-[xy])?\n              | repeating-(linear|radial)-gradient\n              | replaced|reset-size|reverse|ridge|right\n              | round\n              | row(-resize|-reverse)?\n              | run-in\n              | ruby(-base|-text)?(-container)?\n              | rtl|running|saturat(e|ion)|screen\n              | scroll(-position|bar)?\n              | separate|sepia\n              | scale-down\n              | shape-(image-threshold|margin|outside)\n              | show\n              | sideways(-lr|-rl)?\n              | simplified\n              | slashed-zero|slice\n              | small(-caps|er)?\n              | smooth|snap|solid|soft-light\n              | space(-around|-between)?\n              | span|sRGB\n              | stack(ed-fractions)?\n              | start(ColorStr)?\n              | static\n              | step-(end|start)\n              | sticky\n              | stop-(color|opacity)\n              | stretch|strict\n              | stroke(-box|-dash(array|offset)|-miterlimit|-opacity|-width)?\n              | style(set)?\n              | stylistic\n              | sub(grid|pixel-antialiased|tract)?\n              | super|swash\n              | table(-caption|-cell|(-column|-footer|-header|-row)-group|-column|-row)?\n              | tabular-nums|tb-rl\n              | text((-bottom|-(decoration|emphasis)-color|-indent|-(over|under|after|before)-edge|-shadow|-size(-adjust)?|-top)|field)?\n              | thi(ck|n)\n              | titling-ca(ps|se)\n              | to[p]?\n              | touch|traditional\n              | transform(-origin)?\n              | under(-edge|line)?\n              | unicase|unset|uppercase|upright\n              | use-(glyph-orientation|script)\n              | verso\n              | vertical(-align|-ideographic|-lr|-rl|-text)?\n              | view-box\n              | viewport-fill(-opacity)?\n              | visibility\n              | visible(Fill|Painted|Stroke)?\n              | wait|wavy|weight|whitespace|width|word-spacing\n              | wrap(-reverse)?\n              | x{1,2}-(large|small)\n              | z-index|zero\n              | zoom(-in|-out)?\n              | ({{counter_styles}})\n            )\\b\n      scope: support.constant.property-value.css\n      # Generic Font Families: https://www.w3.org/TR/CSS2/fonts.html\n    - match: \\b(?i:sans-serif|serif|monospace|fantasy|cursive|system-ui)\\b(?=\\s*[;,\\n}])\n      scope: support.constant.font-name.css\n\n  property-values:\n    - include: comment-block\n    - include: vendor-prefix\n    - include: builtin-functions\n    - include: unicode-range\n    - include: numeric-values\n    - include: color-values\n    - include: property-value-constants\n    - include: literal-string\n    - match: \\!\\s*important\n      scope: keyword.other.important.css\n\n  rule-list-terminator:\n    - match: '\\s*(\\})'\n      captures:\n        1: punctuation.section.property-list.css\n      pop: true\n\n  rule-list:\n    - match: '\\{'\n      scope: punctuation.section.property-list.css\n      push:\n        - meta_scope: meta.property-list.css\n        - match: '(?=\\s*\\})'\n          pop: true\n        - include: rule-list-body\n\n  rule-list-body:\n    - include: comment-block\n    - match: \"(?=[-a-z])\"\n      push:\n        - meta_scope: meta.property-name.css\n        - match: \"$|(?![-a-z])\"\n          pop: true\n        - include: vendor-prefix\n        - match: '\\b(var-)({{ident}})(?=\\s)'\n          scope: invalid.deprecated.custom-property.css\n          captures:\n            1: keyword.other.custom-property.prefix.css\n            2: support.type.custom-property.name.css\n        - include: custom-property-name\n        - match: \\bfont(-family)?(?!-)\\b\n          scope: support.type.property-name.css\n          push:\n            - match: (:)([ \\t]*)\n              captures:\n                1: punctuation.separator.key-value.css\n                2: meta.property-value.css\n              push:\n                - meta_content_scope: meta.property-value.css\n                - match: '\\s*(;)|(?=[})])'\n                  captures:\n                    1: punctuation.terminator.rule.css\n                  pop: true\n                - include: property-values\n                - match: '{{ident}}(\\s+{{ident}})*'\n                  scope: string.unquoted.css\n                - match: ','\n                  scope: punctuation.separator.css\n            - match: ''\n              pop: true\n        # Property names are sorted by popularity in descending order.\n        # Popularity data taken from https://www.chromestatus.com/metrics/css/popularity\n        - match: |-\n            \\b(?x)(\n                display|width|background-color|height|position|font-family|font-weight\n              | top|opacity|cursor|background-image|right|visibility|box-sizing\n              | user-select|left|float|margin-left|margin-top|line-height\n              | padding-left|z-index|margin-bottom|margin-right|margin\n              | vertical-align|padding-top|white-space|border-radius|padding-bottom\n              | padding-right|padding|bottom|clear|max-width|box-shadow|content\n              | border-color|min-height|min-width|font-style|border-width\n              | border-collapse|background-size|text-overflow|max-height|text-transform\n              | text-shadow|text-indent|border-style|overflow-y|list-style-type\n              | word-wrap|border-spacing|appearance|zoom|overflow-x|border-top-left-radius\n              | border-bottom-left-radius|border-top-color|pointer-events\n              | border-bottom-color|align-items|justify-content|letter-spacing\n              | border-top-right-radius|border-bottom-right-radius|border-right-width\n              | font-smoothing|border-bottom-width|border-right-color|direction\n              | border-top-width|src|border-left-color|border-left-width\n              | tap-highlight-color|table-layout|background-clip|word-break\n              | transform-origin|resize|filter|backface-visibility|text-rendering\n              | box-orient|transition-property|transition-duration|word-spacing\n              | quotes|outline-offset|animation-timing-function|animation-duration\n              | animation-name|transition-timing-function|border-bottom-style\n              | border-bottom|transition-delay|transition|unicode-bidi|border-top-style\n              | border-top|unicode-range|list-style-position|orphans|outline-width\n              | line-clamp|order|flex-direction|box-pack|animation-fill-mode\n              | outline-color|list-style-image|list-style|touch-action|flex-grow\n              | border-left-style|border-left|animation-iteration-count\n              | page-break-inside|box-flex|box-align|page-break-after|animation-delay\n              | widows|border-right-style|border-right|flex-align|outline-style\n              | outline|background-origin|animation-direction|fill-opacity\n              | background-attachment|flex-wrap|transform-style|counter-increment\n              | overflow-wrap|counter-reset|animation-play-state|animation\n              | will-change|box-ordinal-group|image-rendering|mask-image|flex-flow\n              | background-position-y|stroke-width|background-position-x|background-position\n              | background-blend-mode|flex-shrink|flex-basis|flex-order|flex-item-align\n              | flex-line-pack|flex-negative|flex-pack|flex-positive|flex-preferred-size\n              | flex|user-drag|font-stretch|column-count|empty-cells|align-self\n              | caption-side|mask-size|column-gap|mask-repeat|box-direction\n              | font-feature-settings|mask-position|align-content|object-fit\n              | columns|text-fill-color|clip-path|stop-color|font-kerning\n              | page-break-before|stroke-dasharray|size|fill-rule|border-image-slice\n              | column-width|break-inside|column-break-before|border-image-width\n              | stroke-dashoffset|border-image-repeat|border-image-outset|line-break\n              | stroke-linejoin|stroke-linecap|stroke-miterlimit|stroke-opacity\n              | stroke|shape-rendering|border-image-source|border-image|border\n              | tab-size|writing-mode|perspective-origin-y|perspective-origin-x\n              | perspective-origin|perspective|text-align-last|text-align|clip-rule\n              | clip|text-anchor|column-rule-color|box-decoration-break|column-fill\n              | fill|column-rule-style|mix-blend-mode|text-emphasis-color\n              | baseline-shift|dominant-baseline|page|alignment-baseline\n              | column-rule-width|column-rule|break-after|font-variant-ligatures\n              | transform-origin-y|transform-origin-x|transform|object-position\n              | break-before|column-span|isolation|shape-outside|all\n              | color-interpolation-filters|marker|marker-end|marker-start\n              | marker-mid|color-rendering|color-interpolation|background-repeat-x\n              | background-repeat-y|background-repeat|background|mask-type\n              | flood-color|flood-opacity|text-orientation|mask-composite\n              | text-emphasis-style|paint-order|lighting-color|shape-margin\n              | text-emphasis-position|text-emphasis|shape-image-threshold\n              | mask-clip|mask-origin|mask|font-variant-caps|font-variant-alternates\n              | font-variant-east-asian|font-variant-numeric|font-variant-position\n              | font-variant|font-size-adjust|font-size|font-language-override\n              | font-display|font-synthesis|font|line-box-contain|text-justify\n              | text-decoration-color|text-decoration-style|text-decoration-line\n              | text-decoration|text-underline-position|grid-template-rows\n              | grid-template-columns|grid-template-areas|grid-template|rotate|scale\n              | translate|scroll-behavior|grid-column-start|grid-column-end\n              | grid-column-gap|grid-row-start|grid-row-end|grid-auto-rows\n              | grid-area|grid-auto-flow|grid-auto-columns|image-orientation\n              | hyphens|overflow-scrolling|overflow|color-profile|kerning\n              | nbsp-mode|color|image-resolution|grid-row-gap|grid-row|grid-column\n              | blend-mode|azimuth|pause-after|pause-before|pause|pitch-range|pitch\n              | text-height|system|negative|prefix|suffix|range|pad|fallback\n              | additive-symbols|symbols|speak-as|speak|grid-gap\n            )\\b\n          scope: support.type.property-name.css\n    - match: (:)([ \\t]*)\n      captures:\n        1: punctuation.separator.key-value.css\n        2: meta.property-value.css\n      push:\n        - meta_content_scope: meta.property-value.css\n        - match: '\\s*(;)|(?=[})])'\n          captures:\n            1: punctuation.terminator.rule.css\n          pop: true\n        - include: property-values\n\n  selector:\n    - match: '\\s*(?=[:.*#a-zA-Z\\[])'\n      push:\n        - meta_scope: meta.selector.css\n        - match: \"(?=[/@{)])\"\n          pop: true\n          # Custom Elements: http://w3c.github.io/webcomponents/spec/custom/#custom-elements-core-concepts\n        - match: '\\b([a-z](?:{{custom_element_chars}})*-(?:{{custom_element_chars}})*)\\b'\n          scope: entity.name.tag.custom.css\n        - match: '\\b(a|abbr|acronym|address|applet|area|article|aside|audio|b|base|basefont|bdi|bdo|big|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|content|data|datalist|dd|del|details|dfn|dir|dialog|div|dl|dt|element|em|embed|eventsource|fieldset|figure|figcaption|footer|form|frame|frameset|h[1-6]|head|header|hgroup|hr|html|i|iframe|img|input|ins|isindex|kbd|keygen|label|legend|li|link|main|map|mark|menu|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|picture|pre|progress|q|rp|rt|rtc|s|samp|script|section|select|shadow|small|source|span|strike|strong|style|sub|summary|sup|svg|table|tbody|td|template|textarea|tfoot|th|thead|time|title|tr|track|tt|u|ul|var|video|wbr|xmp|circle|clipPath|defs|ellipse|filter|foreignObject|g|glyph|glyphRef|image|line|linearGradient|marker|mask|path|pattern|polygon|polyline|radialGradient|rect|stop|switch|symbol|text|textPath|tref|tspan|use)\\b'\n          scope: entity.name.tag.css\n          # https://drafts.csswg.org/selectors-4/#class-html\n        - match: '(\\.){{ident}}'\n          scope: entity.other.attribute-name.class.css\n          captures:\n            1: punctuation.definition.entity.css\n          # https://drafts.csswg.org/selectors-4/#id-selectors\n        - match: \"(#){{ident}}\"\n          scope: entity.other.attribute-name.id.css\n          captures:\n            1: punctuation.definition.entity.css\n        - match: \\*\n          scope: entity.name.tag.wildcard.css\n          # Combinators\n          # https://drafts.csswg.org/selectors-4/#combinators\n          # https://drafts.csswg.org/css-scoping/#deep-combinator\n        - match: '({{combinators}})(?![>~+])'\n          scope: punctuation.separator.combinator.css\n        - match: '({{combinators}}){2,}'\n          scope: invalid.illegal.combinator.css\n        - include: pseudo-elements\n        - include: pseudo-classes # pseudo-classes must be included after pseudo-elements\n        # Attribute Selectors\n        # https://drafts.csswg.org/selectors-4/#attribute-selectors\n        - match: '\\['\n          scope: punctuation.definition.entity.css\n          push:\n            - meta_scope: meta.attribute-selector.css\n            - include: qualified-name\n            - match: '({{ident}})'\n              scope: entity.other.attribute-name.css\n            - match: '\\s*([~*|^$]?=)\\s*'\n              captures:\n                1: keyword.operator.attribute-selector.css\n              push:\n                - match: '[^\\s\\]\\[''\"]'\n                  scope: string.unquoted.css\n                - include: literal-string\n                - match: '(?=(\\s|\\]))'\n                  pop: true\n            - match: '(?:\\s+([iI]))?'  # case insensitive flag\n              captures:\n                1: keyword.other.css\n            - match: '\\]'\n              scope: punctuation.definition.entity.css\n              pop: true\n\n  # Pseudo Elements\n  # https://drafts.csswg.org/selectors-4/#pseudo-elements\n  pseudo-elements:\n    - match: |-\n        (?x:\n            (:{1,2})(?:before|after|first-line|first-letter) # CSS1 & CSS2 require : or ::\n          | (::)(-(?:moz|ms|webkit)-)?(?:{{ident}}) # CSS3 requires ::\n        )\\b\n      scope: entity.other.pseudo-element.css\n      captures:\n        1: punctuation.definition.entity.css\n        2: punctuation.definition.entity.css\n        3: support.type.vendor-prefix.css\n\n  # Pseudo Classes\n  # https://drafts.csswg.org/selectors-4/#pseudo-classes\n  pseudo-classes:\n      # Functional Pseudo Classes\n      # https://drafts.csswg.org/selectors-4/#functional-pseudo-class\n\n      # Functional Pseudo Classes with a single unquoted string\n    - match: '(:)(dir|lang)(?=\\()'\n      scope: entity.other.pseudo-class.css\n      captures:\n        1: punctuation.definition.entity.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: unquoted-string\n\n      # Functional Pseudo Classes with selector list\n    - match: '(:)(matches|not|has)(?=\\()'\n      scope: entity.other.pseudo-class.css\n      captures:\n        1: punctuation.definition.entity.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: selector\n\n      # Special :drop() pseudo-class\n    - match: '(:)(drop)(?=\\()'\n      scope: entity.other.pseudo-class.css\n      captures:\n        1: punctuation.definition.entity.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - match: \\b(active|valid|invalid)\\b\n            scope: keyword.other.pseudo-class.css\n\n      # Functional Pseudo Classes with `An+B` param\n      # An+B Notation: https://drafts.csswg.org/css-syntax/#anb\n      # nth-last-child(), nth-child(), nth-last-of-type(), nth-of-type()\n    - match: '(:)(nth-last-child|nth-child|nth-last-of-type|nth-of-type)(?=\\()'\n      scope: entity.other.pseudo-class.css\n      captures:\n        1: punctuation.definition.entity.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - match: \\b(even|odd)\\b\n            scope: keyword.other.pseudo-class.css\n          - match: '(?:[-+]?(?:\\d+)?(n)(\\s*[-+]\\s*\\d+)?|[-+]?\\s*\\d+)'\n            scope: constant.numeric.css\n            captures:\n              1: keyword.other.unit.css\n\n      # Regular Pseudo Classes\n    - match: '(:)({{ident}})'\n      scope: entity.other.pseudo-class.css\n      captures:\n        1: punctuation.definition.entity.css\n\n  builtin-functions:\n    - include: attr-function\n    - include: calc-function\n    - include: cross-fade-function\n    - include: filter-functions\n    - include: gradient-functions\n    - include: image-function\n    - include: image-set-function\n    - include: minmax-function\n    - include: url-function\n    - include: var-function\n    - include: color-adjuster-functions\n\n      # filter()\n      # https://drafts.fxtf.org/filters/#funcdef-filter\n    - match: '\\b(filter)(?=\\()'\n      scope: support.function.filter.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: comma-delimiter\n          - include: image-type\n          - include: literal-string\n          - include: filter-functions\n\n      # counter()\n      # https://drafts.csswg.org/css-lists-3/#funcdef-counter\n    - match: '\\b(counter)(?=\\()'\n      scope: support.function.counter.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - match: '({{ident}})'\n            scope: entity.other.counter-name.css string.unquoted.css\n          - match: '(?=,)'\n            push:\n              - match: '(?=\\))'\n                pop: true\n              - include: comma-delimiter\n              - match: '\\b({{counter_styles}}|none)\\b'\n                scope: support.constant.property-value.counter-style.css\n\n      # counters()\n      # https://drafts.csswg.org/css-lists-3/#funcdef-counters\n    - match: '\\b(counters)(?=\\()'\n      scope: support.function.counter.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - match: '({{ident}})'\n            scope: entity.other.counter-name.css string.unquoted.css\n          - match: '(?=,)'\n            push:\n              - match: '(?=\\))'\n                pop: true\n              - include: comma-delimiter\n              - include: literal-string\n              - match: '\\b({{counter_styles}}|none)\\b'\n                scope: support.constant.property-value.counter-style.css\n\n      # symbols()\n      # https://drafts.csswg.org/css-counter-styles-3/#symbols-function\n    - match: '\\b(symbols)(?=\\()'\n      scope: support.function.counter.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - match: '\\b(cyclic|numeric|alphabetic|symbolic|fixed)\\b'\n            scope: support.constant.symbol-type.css\n          - include: comma-delimiter\n          - include: literal-string\n          - include: image-type\n\n      # format()\n      # https://drafts.csswg.org/css-fonts-3/#descdef-src\n      # format() is also mentioned in `issue 2` at https://drafts.csswg.org/css-images-3/#issues-index\n      # but does not seem to be implemented in any manner\n    - match: '\\b(format)(?=\\()'\n      scope: support.function.font-face.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: literal-string\n\n      # local()\n      # https://drafts.csswg.org/css-fonts-3/#descdef-src\n    - match: '\\b(local)(?=\\()'\n      scope: support.function.font-face.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: unquoted-string\n\n      # Transform Functions\n      # https://www.w3.org/TR/css-transforms-1/#transform-functions\n\n      # transform functions with comma separated <number> types\n      # matrix(), scale(), matrix3d(), scale3d()\n    - match: '\\b(matrix3d|scale3d|matrix|scale)(?=\\()'\n      scope: support.function.transform.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: comma-delimiter\n          - include: number-type\n          - include: var-function\n\n      # transform functions with comma separated <number> or <length> types\n      # translate(), translate3d()\n    - match: '\\b(translate(3d)?)(?=\\()'\n      scope: support.function.transform.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: comma-delimiter\n          - include: percentage-type\n          - include: length-type\n          - include: number-type\n          - include: var-function\n\n      # transform functions with a single <number> or <length> type\n      # translateX(), translateY()\n    - match: '\\b(translate[XY])(?=\\()'\n      scope: support.function.transform.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: percentage-type\n          - include: length-type\n          - include: number-type\n\n      # transform functions with a single <angle> type\n      # rotate(), skewX(), skewY(), rotateX(), rotateY(), rotateZ()\n    - match: '\\b(rotate[XYZ]?|skew[XY])(?=\\()'\n      scope: support.function.transform.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: angle-type\n\n      # transform functions with comma separated <angle> types\n      # skew()\n    - match: '\\b(skew)(?=\\()'\n      scope: support.function.transform.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: comma-delimiter\n          - include: angle-type\n\n      # transform functions with a single <length> type\n      # translateZ(), perspective()\n    - match: '\\b(translateZ|perspective)(?=\\()'\n      scope: support.function.transform.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: length-type\n\n      # transform functions with a comma separated <number> or <angle> types\n      # rotate3d()\n    - match: '\\b(rotate3d)(?=\\()'\n      scope: support.function.transform.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: comma-delimiter\n          - include: angle-type\n          - include: number-type\n\n      # transform functions with a single <number> type\n      # scaleX(), scaleY(), scaleZ()\n    - match: '\\b(scale[XYZ])(?=\\()'\n      scope: support.function.transform.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: comma-delimiter\n          - include: number-type\n\n      # Timing Functions\n      # https://www.w3.org/TR/web-animations-1/#timing-functions\n\n      # cubic-bezier()\n      # https://www.w3.org/TR/web-animations-1/#cubic-bzier-timing-function\n    - match: '\\b(cubic-bezier)(?=\\()'\n      scope: support.function.timing.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: comma-delimiter\n          - include: number-type\n\n      # steps()\n      # https://www.w3.org/TR/web-animations-1/#step-timing-function\n    - match: '\\b(steps)(?=\\()'\n      scope: support.function.timing.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: comma-delimiter\n          - include: integer-type\n          - match: (end|middle|start)\n            scope: support.keyword.timing-direction.css\n\n      # Shape Functions\n      # https://www.w3.org/TR/css-shapes-1/#typedef-basic-shape\n\n      # rect() - Deprecated\n      # https://drafts.fxtf.org/css-masking-1/#funcdef-clip-rect\n    - match: '\\b(rect)(?=\\()'\n      scope: support.function.shape.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - match: \\bauto\\b\n            scope: support.constant.property-value.css\n          - include: length-type\n\n      # inset()\n      # https://www.w3.org/TR/css-shapes-1/#funcdef-inset\n    - match: '\\b(inset)(?=\\()'\n      scope: support.function.shape.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - match: '\\bround\\b'\n            scope: keyword.other.css\n          - include: length-type\n          - include: percentage-type\n\n      # circle()\n      # https://www.w3.org/TR/css-shapes-1/#funcdef-circle\n      # ellipse()\n      # https://www.w3.org/TR/css-shapes-1/#funcdef-ellipse\n    - match: '\\b(circle|ellipse)(?=\\()'\n      scope: support.function.shape.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - match: '\\bat\\b'\n            scope: keyword.other.css\n          - match: '\\b(top|right|bottom|left|center|closest-side|farthest-side)\\b'\n            scope: support.constant.property-value.css\n          - include: length-type\n          - include: percentage-type\n\n      # polygon()\n      # https://www.w3.org/TR/css-shapes-1/#funcdef-polygon\n    - match: '\\b(polygon)(?=\\()'\n      scope: support.function.shape.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - match: '\\b(nonzero|evenodd)\\b'\n            scope: support.constant.property-value.css\n          - include: length-type\n          - include: percentage-type\n\n      # toggle()\n      # https://www.w3.org/TR/css3-values/#toggle-notation\n    - match: '\\b(toggle)(?=\\()'\n      scope: support.function.toggle.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: comma-delimiter\n          - include: vendor-prefix\n          - include: property-value-constants\n          - include: numeric-values\n          - include: color-values\n          - include: literal-string\n\n      # repeat()\n      # https://drafts.csswg.org/css-grid/#funcdef-repeat\n    - match: '\\b(repeat)(?=\\()'\n      scope: support.function.grid.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: comma-delimiter\n          - include: length-type\n          - include: percentage-type\n          - include: minmax-function\n          - include: integer-type\n          - include: var-function\n          - match: \\b(auto-fill|auto-fit)\\b\n            scope: support.keyword.repetitions.css\n          - match: \\b(max-content|min-content|auto)\\b\n            scope: support.constant.property-value.css\n\n  # var()\n  # https://drafts.csswg.org/css-variables/#funcdef-var\n  var-function:\n    - match: '\\b(var)(?=\\()'\n      scope: support.function.var.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: comma-delimiter\n          - include: custom-property-name\n\n  # Filter Functions\n  # https://drafts.fxtf.org/filters/#typedef-filter-function\n  filter-functions:\n      # blur()\n      # https://drafts.fxtf.org/filters/#funcdef-filter-blur\n    - match: '\\b(blur)(?=\\()'\n      scope: support.function.filter.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: length-type\n\n      # brightness(), contrast(), grayscale(), invert(), opacity(), saturate(), sepia()\n      # https://drafts.fxtf.org/filters/#funcdef-filter-brightness\n    - match: '\\b(brightness|contrast|grayscale|invert|opacity|saturate|sepia)(?=\\()'\n      scope: support.function.filter.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: percentage-type\n          - include: number-type\n\n      # drop-shadow()\n      # https://drafts.fxtf.org/filters/#funcdef-filter-drop-shadow\n    - match: '\\b(drop-shadow)(?=\\()'\n      scope: support.function.filter.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: length-type\n          - include: color-values\n\n      # hue-rotate()\n      # https://drafts.fxtf.org/filters/#funcdef-filter-hue-rotate\n    - match: '\\b(hue-rotate)(?=\\()'\n      scope: support.function.filter.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: angle-type\n\n  # calc()\n  # https://www.w3.org/TR/css3-values/#funcdef-calc\n  calc-function:\n    - match: '\\b(calc)(?=\\()'\n      scope: support.function.calc.css\n      push:\n        - meta_scope: meta.function-call.css\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push: inside-calc-parens\n        - match: ''\n          pop: true\n\n  inside-calc-parens:\n    - meta_scope: meta.group.css\n    - match: '(?=\\))'\n      set: function-notation-terminator\n    - include: calc-function\n    - include: var-function\n    - include: numeric-values\n    - include: attr-function\n    - match: \"[-/*+]\"\n      scope: keyword.operator.css\n    - match: '\\('\n      scope: punctuation.definition.group.begin.css\n      push: inside-calc-parens\n\n  # attr()\n  # https://www.w3.org/TR/css3-values/#funcdef-attr\n  attr-function:\n    - match: '\\b(attr)(?=\\()'\n      scope: support.function.attr.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: qualified-name\n          - include: literal-string\n          - match: '({{ident}})'\n            scope: entity.other.attribute-name.css\n            push:\n            - match: |-\n                (?x)\\b(\n                    {{font_relative_lengths}}\n                  | {{viewport_percentage_lengths}}\n                  | {{absolute_lengths}}\n                  | {{angle_units}}\n                  | {{duration_units}}\n                  | {{frequency_units}}\n                  | {{resolution_units}}\n                )\\b\n              scope: keyword.other.unit.css\n            - match: '(?=\\))'\n              pop: true\n            - include: comma-delimiter\n            - include: property-value-constants\n            - include: numeric-values\n            - include: color-values\n\n  # url()\n  # https://drafts.csswg.org/css-images-3/#url-notation\n  url-function:\n    - match: '\\b(url)(?=\\()'\n      scope: support.function.url.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: literal-string\n          - include: unquoted-string\n\n  # url-prefix()\n  # https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#url-prefix\n  url-prefix-function:\n    - match: '\\b(url-prefix)(?=\\()'\n      scope: support.function.url-prefix.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: literal-string\n          - include: unquoted-string\n\n  # domain()\n  # https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#url-domain\n  domain-function:\n    - match: '\\b(domain)(?=\\()'\n      scope: support.function.domain.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: literal-string\n          - include: unquoted-string\n\n  # regexp()\n  # https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#url-regexp\n  regexp-function:\n    - match: '\\b(regexp)(?=\\()'\n      scope: support.function.regexp.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: literal-string\n\n  # image()\n  # https://drafts.csswg.org/css-images-3/#funcdef-image\n  image-function:\n    - match: '\\b(image)(?=\\()'\n      scope: support.function.image.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: image-type\n          - include: literal-string\n          - include: color-values\n          - include: comma-delimiter\n          - include: unquoted-string\n\n  # image-set()\n  # https://drafts.csswg.org/css-images-3/#funcdef-image-set\n  image-set-function:\n    - match: '\\b(image-set)(?=\\()'\n      scope: support.function.image.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: literal-string\n          - include: color-values\n          - include: comma-delimiter\n          - include: resolution-type\n          - include: image-type\n          - match: '[0-9]+(x)'\n            scope: constant.numeric.css\n            captures:\n              1: keyword.other.unit.css\n          - include: unquoted-string\n\n  # Gradient Functions\n  # https://drafts.csswg.org/css-images-3/#gradients\n  gradient-functions:\n      # linear-gradient()\n      # https://drafts.csswg.org/css-images-3/#linear-gradients\n      # repeating-linear-gradient()\n      # https://drafts.csswg.org/css-images-3/#funcdef-repeating-linear-gradient\n    - match: '\\b((?:repeating-)?linear-gradient)(?=\\()'\n      scope: support.function.gradient.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: angle-type\n          - include: comma-delimiter\n          - include: color-values\n          - include: percentage-type\n          - include: length-type\n          - match: '\\bto\\b'\n            scope: keyword.other.css\n          - match: \\b(top|right|bottom|left)\\b\n            scope: support.constant.property-value.css\n\n      # radial-gradient()\n      # https://drafts.csswg.org/css-images-3/#radial-gradients\n      # repeating-radial-gradient()\n      # https://drafts.csswg.org/css-images-3/#funcdef-repeating-radial-gradient\n    - match: '\\b((?:repeating-)?radial-gradient)(?=\\()'\n      scope: support.function.gradient.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: comma-delimiter\n          - include: color-values\n          - include: percentage-type\n          - include: length-type\n          - match: '\\b(at|circle|ellipse)\\b'\n            scope: keyword.other.css\n          - match: |-\n              (?x)\\b(\n                  left\n                | center\n                | right\n                | top\n                | bottom\n                | closest-corner\n                | closest-side\n                | farthest-corner\n                | farthest-side\n              )\\b\n            scope: support.constant.property-value.css\n\n  # cross-fade()\n  # https://drafts.csswg.org/css-images-3/#cross-fade-function\n  cross-fade-function:\n    - match: '\\b(cross-fade)(?=\\()'\n      scope: support.function.image.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: comma-delimiter\n          - include: percentage-type\n          - include: color-values\n          - include: image-type\n          - include: literal-string\n          - include: unquoted-string\n\n  # minmax()\n  # https://drafts.csswg.org/css-grid/#valdef-grid-template-columns-minmax\n  minmax-function:\n    - match: '\\b(minmax)(?=\\()'\n      scope: support.function.grid.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: comma-delimiter\n          - include: length-type\n          - match: \\b(max-content|min-content)\\b\n            scope: support.constant.property-value.css\n\n  # Color Functions\n  # https://drafts.csswg.org/css-color\n  color-functions:\n      # rgb(), rgba()\n      # https://drafts.csswg.org/css-color/#rgb-functions\n    - match: '\\b(rgba?)(?=\\()'\n      scope: support.function.color.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: comma-delimiter\n          - include: percentage-type\n          - include: number-type\n\n      # hsl(), hsla()\n      # https://drafts.csswg.org/css-color/#the-hsl-notation\n      # hwb() - Not yet implemented by browsers\n      # https://drafts.csswg.org/css-color/#funcdef-hwb\n    - match: '\\b(hsla?|hwb)(?=\\()'\n      scope: support.function.color.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: comma-delimiter\n          - include: angle-type\n          - include: percentage-type\n          - include: number-type\n\n      # gray() - Not yet implemented by browsers\n      # https://drafts.csswg.org/css-color/#funcdef-gray\n    - match: '\\b(gray)(?=\\()'\n      scope: support.function.color.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: comma-delimiter\n          - include: percentage-type\n          - include: number-type\n\n      # device-cmyk() - Not yet implemented by browsers\n      # https://drafts.csswg.org/css-color/#funcdef-device-cmyk\n    - match: '\\b(device-cmyk)(?=\\()'\n      scope: support.function.color.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: comma-delimiter\n          - include: color-adjuster-functions # must be included before `color-values`\n          - include: color-values\n          - include: percentage-type\n          - include: number-type\n\n      # color-mod() - Not yet implemented by browsers\n      # https://drafts.csswg.org/css-color/#funcdef-color-mod\n    - match: '\\b(color)(?=\\()'\n      scope: support.function.color.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: comma-delimiter\n          - include: color-adjuster-functions # must be included before `color-values`\n          - include: var-function\n          - include: color-values\n          - include: angle-type\n          - include: number-type\n\n  # Color Adjuster Functions - Not yet implemented by browsers\n  # https://drafts.csswg.org/css-color/#typedef-color-adjuster\n  color-adjuster-functions:\n      # red(), green(), blue(), alpha() - Not yet implemented by browsers\n    - match: '\\b(red|green|blue|alpha|a)(?=\\()'\n      scope: support.function.color.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: color-adjuster-operators\n          - include: percentage-type\n          - include: number-type\n\n      # hue() - Not yet implemented by browsers\n    - match: '\\b(hue|h)(?=\\()'\n      scope: support.function.color.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: color-adjuster-operators\n          - include: angle-type\n\n      # saturation(), lightness(), whiteness(), blackness() - Not yet implemented by browsers\n    - match: '\\b(saturation|lightness|whiteness|blackness|[slwb])(?=\\()'\n      scope: support.function.color.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: color-adjuster-operators\n          - include: percentage-type\n\n      # tint(), shade(), contrast() - Not yet implemented by browsers\n      # contrast() interferes with the contrast() filter function;\n      # therefore, it is not yet implemented here\n    - match: '\\b(tint|shade)(?=\\()'\n      scope: support.function.color.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - include: percentage-type\n\n      # blend(), blenda() - Not yet implemented by browsers\n    - match: '\\b(blenda|blend)(?=\\()'\n      scope: support.function.color.css\n      push:\n        - meta_scope: meta.function-call.css\n        - include: function-notation-terminator\n        - match: '\\('\n          scope: punctuation.definition.group.begin.css\n          push:\n          - meta_scope: meta.group.css\n          - match: '(?=\\))'\n            pop: true\n          - match: '\\b(rgb|hsl|hwb)\\b'\n            scope: keyword.other.color-space.css\n          - include: color-values\n          - include: percentage-type\n          - include: var-function\n\n  unicode-range:\n    - match: |-\n        (?xi)\n            (u\\+)\n            ([0-9a-f?]{1,6}\n            (?:(-)[0-9a-f]{1,6})?)\n      scope: support.unicode-range.css\n      captures:\n        1: support.constant.unicode-range.prefix.css\n        2: constant.codepoint-range.css\n        3: punctuation.section.range.css\n\n  # Qualified Name\n  # https://drafts.csswg.org/css-namespaces-3/#css-qnames\n  qualified-name:\n    - match: '(?:({{ident}})|(\\*))?([|])(?!=)'\n      captures:\n        1: entity.other.namespace-prefix.css\n        2: entity.name.namespace.wildcard.css\n        3: punctuation.separator.namespace.css\n\n  # Custom Properties\n  # https://drafts.csswg.org/css-variables/#typedef-custom-property-name\n  custom-property-name:\n    - match: '(--)({{nmchar}}+)'\n      scope: support.type.custom-property.css\n      captures:\n        1: punctuation.definition.custom-property.css\n        2: support.type.custom-property.name.css\n\n  color-adjuster-operators:\n    - match: '[\\-\\+*](?=\\s+)'\n      scope: keyword.operator.css\n\n  comma-delimiter:\n    - match: '\\s*(,)\\s*'\n      captures:\n        1: punctuation.separator.css\n\n  vendor-prefix:\n    - match: \"-(?:webkit|moz|ms|o)-\"\n      scope: support.type.vendor-prefix.css\n\n  function-notation-terminator:\n    - match: '\\)'\n      scope: meta.group.css punctuation.definition.group.end.css\n      pop: true\n\n  at-rule-punctuation:\n    - match: \\;\n      scope: punctuation.terminator.rule.css\n    - match: (?=;|$)\n      pop: true\n\n  unquoted-string:\n    - match: '[^\\s''\"]'\n      scope: string.unquoted.css\n\n  literal-string:\n    - match: \"'\"\n      scope: punctuation.definition.string.begin.css\n      push:\n        - meta_scope: string.quoted.single.css\n        - match: (')|(\\n)\n          captures:\n            1: punctuation.definition.string.end.css\n            2: invalid.illegal.newline.css\n          pop: true\n        - include: string-content\n    - match: '\"'\n      scope: punctuation.definition.string.begin.css\n      push:\n        - meta_scope: string.quoted.double.css\n        - match: (\")|(\\n)\n          captures:\n            1: punctuation.definition.string.end.css\n            2: invalid.illegal.newline.css\n          pop: true\n        - include: string-content\n\n  string-content:\n    - match: \\\\\\s*\\n\n      scope: constant.character.escape.newline.css\n    - match: '\\\\(\\h{1,6}|.)'\n      scope: constant.character.escape.css\n\n  # https://www.w3.org/TR/css3-values/#numeric-types\n  numeric-values:\n    - include: dimensions\n    - include: percentage-type\n    - include: number-type\n\n  integer-type:\n    - match: '{{integer}}'\n      scope: constant.numeric.css\n\n  # Make sure `number-type` is included after any other numeric values\n  # as `number-type` will consume all numeric values.\n  number-type:\n    - match: '{{number}}'\n      scope: constant.numeric.css\n\n  percentage-type:\n    - match: '{{number}}(%)'\n      scope: constant.numeric.css\n      captures:\n        1: keyword.other.unit.css\n\n  dimensions:\n    - include: angle-type\n    - include: frequency-type\n    - include: length-type\n    - include: resolution-type\n    - include: time-type\n\n  length-type:\n    - match: '{{number}}({{font_relative_lengths}}|{{viewport_percentage_lengths}}|{{absolute_lengths}})\\b'\n      scope: constant.numeric.css\n      captures:\n        1: keyword.other.unit.css\n    - match: '0\\b(?!%)'\n      scope: constant.numeric.css\n\n  time-type:\n    - match: '{{number}}({{duration_units}})\\b'\n      scope: constant.numeric.css\n      captures:\n        1: keyword.other.unit.css\n\n  frequency-type:\n    - match: '{{number}}({{frequency_units}})\\b'\n      scope: constant.numeric.css\n      captures:\n        1: keyword.other.unit.css\n\n  resolution-type:\n    - match: '{{number}}({{resolution_units}})\\b'\n      scope: constant.numeric.css\n      captures:\n        1: keyword.other.unit.css\n\n  angle-type:\n    - match: '{{number}}({{angle_units}})\\b'\n      scope: constant.numeric.css\n      captures:\n        1: keyword.other.unit.css\n\n  # https://drafts.csswg.org/css-images-3/#typedef-image\n  image-type:\n    - include: cross-fade-function\n    - include: gradient-functions\n    - include: image-function\n    - include: image-set-function\n    - include: url-function"
  },
  {
    "path": "assets/syntax/large/html.sublime-syntax",
    "content": "%YAML 1.2\n---\nname: HTML\nfile_extensions:\n  - html\n  - htm\n  - shtml\n  - xhtml\n  - inc\n  - tmpl\n  - tpl\nfirst_line_match: (?i)<(!DOCTYPE\\s*)?html\nscope: text.html.basic\ncontexts:\n  main:\n    - match: (<\\?)(xml)\n      captures:\n        1: punctuation.definition.tag.begin.html\n        2: entity.name.tag.xml.html\n      push:\n        - meta_scope: meta.tag.preprocessor.xml.html\n        - match: '\\?>'\n          scope: punctuation.definition.tag.end.html\n          pop: true\n        - include: tag-generic-attribute\n        - include: string-double-quoted\n        - include: string-single-quoted\n    - match: <!--\n      scope: punctuation.definition.comment.begin.html\n      push:\n        - meta_scope: comment.block.html\n        - match: '(-*)--\\s*>'\n          scope: punctuation.definition.comment.end.html\n          captures:\n            1: invalid.illegal.bad-comments-or-CDATA.html\n          pop: true\n        - match: -{2,}\n          scope: invalid.illegal.bad-comments-or-CDATA.html\n    - match: <!\n      scope: punctuation.definition.tag.html\n      push:\n        - meta_scope: meta.tag.sgml.html\n        - match: \">\"\n          scope: punctuation.definition.tag.html\n          pop: true\n        - match: (?i:DOCTYPE)\n          scope: entity.name.tag.doctype.html\n          push:\n            - meta_scope: meta.tag.sgml.doctype.html\n            - match: (?=>)\n              pop: true\n            - match: '\"[^\">]*\"'\n              scope: string.quoted.double.doctype.identifiers-and-DTDs.html\n        - match: '\\[CDATA\\['\n          push:\n            - meta_scope: constant.other.inline-data.html\n            - match: \"]](?=>)\"\n              pop: true\n        - match: (\\s*)(?!--|>)\\S(\\s*)\n          scope: invalid.illegal.bad-comments-or-CDATA.html\n    - match: (</?)([a-z_][a-z0-9:_]*-[a-z0-9:_-]+)\n      captures:\n        1: punctuation.definition.tag.begin.html\n        2: entity.name.tag.custom.html\n      push:\n        - meta_scope: meta.tag.custom.html\n        - match: '(?: ?/)?>'\n          scope: punctuation.definition.tag.end.html\n          pop: true\n        - include: tag-stuff\n    - match: '(?:^\\s+)?(<)((?i:style))\\b(?![^>]*/>)'\n      captures:\n        0: meta.tag.style.begin.html\n        1: punctuation.definition.tag.begin.html\n        2: entity.name.tag.style.html\n      push:\n        - match: (?i)(</)(style)(>)\n          captures:\n            0: meta.tag.style.end.html\n            1: punctuation.definition.tag.begin.html\n            2: entity.name.tag.style.html\n            3: punctuation.definition.tag.end.html\n          pop: true\n        - match: '(>)\\s*'\n          captures:\n            1: meta.tag.style.begin.html punctuation.definition.tag.end.html\n          embed: scope:source.css\n          embed_scope: source.css.embedded.html\n          escape: (?i)(?=</style)\n        - match: ''\n          push:\n            - meta_scope: meta.tag.style.begin.html\n            - match: '(?=>)'\n              pop: true\n            - include: tag-stuff\n    - match: '(<)((?i:script))\\b(?![^>]*/>)(?![^>]*(?i:type.?=.?text/((?!javascript).*)))'\n      captures:\n        0: meta.tag.script.begin.html\n        1: punctuation.definition.tag.begin.html\n        2: entity.name.tag.script.html\n      push:\n        - match: (?i)(-->)?\\s*(</)(script)(>)\n          captures:\n            0: meta.tag.script.end.html\n            1: comment.block.html punctuation.definition.comment.html\n            2: punctuation.definition.tag.begin.html\n            3: entity.name.tag.script.html\n            4: punctuation.definition.tag.end.html\n          pop: true\n        - match: '(>)\\s*(<!--)?'\n          captures:\n            1: meta.tag.script.begin.html punctuation.definition.tag.end.html\n            2: comment.block.html punctuation.definition.comment.html\n          embed: scope:source.js\n          embed_scope: source.js.embedded.html\n          escape: (?i)(?=(-->)?\\s*</script)\n        - match: ''\n          push:\n            - meta_scope: meta.tag.script.begin.html\n            - match: '(?=>)'\n              pop: true\n            - include: tag-stuff\n    - match: (</?)((?i:body|head|html)\\b)\n      captures:\n        1: punctuation.definition.tag.begin.html\n        2: entity.name.tag.structure.any.html\n      push:\n        - meta_scope: meta.tag.structure.any.html\n        - match: '>'\n          scope: punctuation.definition.tag.end.html\n          pop: true\n        - include: tag-stuff\n    - match: (</?)((?i:address|blockquote|dd|div|section|article|aside|header|footer|nav|menu|dl|dt|frame|frameset|h1|h2|h3|h4|h5|h6|iframe|noframes|object|ol|p|ul|applet|center|dir|pre)\\b)\n      captures:\n        1: punctuation.definition.tag.begin.html\n        2: entity.name.tag.block.any.html\n      push:\n        - meta_scope: meta.tag.block.any.html\n        - match: '>'\n          scope: punctuation.definition.tag.end.html\n          pop: true\n        - include: tag-stuff\n    - match: (</?)((?i:hr)\\b)\n      captures:\n        1: punctuation.definition.tag.begin.html\n        2: entity.name.tag.block.any.html\n      push:\n        - meta_scope: meta.tag.block.any.html\n        - match: '(?: ?/)?>'\n          scope: punctuation.definition.tag.end.html\n          pop: true\n        - include: tag-stuff\n    - match: (</?)((?i:form|fieldset)\\b)\n      captures:\n        1: punctuation.definition.tag.begin.html\n        2: entity.name.tag.block.form.html\n      push:\n        - meta_scope: meta.tag.block.form.html\n        - match: '>'\n          scope: punctuation.definition.tag.end.html\n          pop: true\n        - include: tag-stuff\n    - match: (</?)((?i:abbr|acronym|area|b|base|basefont|bdo|big|br|caption|cite|code|del|dfn|em|font|head|html|i|img|ins|isindex|kbd|li|link|map|meta|noscript|param|q|s|samp|script|small|span|strike|strong|style|sub|sup|title|tt|u|var)\\b)\n      captures:\n        1: punctuation.definition.tag.begin.html\n        2: entity.name.tag.inline.any.html\n      push:\n        - meta_scope: meta.tag.inline.any.html\n        - match: '(?: ?/)?>'\n          scope: punctuation.definition.tag.end.html\n          pop: true\n        - include: tag-stuff\n    - match: (</?)((?i:button|input|label|legend|optgroup|option|select|textarea)\\b)\n      captures:\n        1: punctuation.definition.tag.begin.html\n        2: entity.name.tag.inline.form.html\n      push:\n        - meta_scope: meta.tag.inline.form.html\n        - match: '(?: ?/)?>'\n          scope: punctuation.definition.tag.end.html\n          pop: true\n        - include: tag-stuff\n    - match: (</?)((?i:a)\\b)\n      captures:\n        1: punctuation.definition.tag.begin.html\n        2: entity.name.tag.inline.a.html\n      push:\n        - meta_scope: meta.tag.inline.a.html\n        - match: '(?: ?/)?>'\n          scope: punctuation.definition.tag.end.html\n          pop: true\n        - include: tag-stuff\n    - match: (</?)((?i:col|colgroup|table|tbody|td|tfoot|th|thead|tr)\\b)\n      captures:\n        1: punctuation.definition.tag.begin.html\n        2: entity.name.tag.inline.table.html\n      push:\n        - meta_scope: meta.tag.inline.table.html\n        - match: '(?: ?/)?>'\n          scope: punctuation.definition.tag.end.html\n          pop: true\n        - include: tag-stuff\n    - match: (</?)([A-Za-z0-9:_]+-[A-Za-z0-9:_-]+)\n      captures:\n        1: punctuation.definition.tag.begin.html\n        2: invalid.illegal.uppercase-custom-tag-name.html\n      push:\n        - meta_scope: meta.tag.custom.html\n        - match: '(?: ?/)?>'\n          scope: punctuation.definition.tag.end.html\n          pop: true\n        - include: tag-stuff\n    - match: \"(</?)([a-zA-Z0-9:]+)\"\n      captures:\n        1: punctuation.definition.tag.begin.html\n        2: entity.name.tag.other.html\n      push:\n        - meta_scope: meta.tag.other.html\n        - match: '(?: ?/)?>'\n          scope: punctuation.definition.tag.end.html\n          pop: true\n        - include: tag-stuff\n    - include: entities\n    - match: <>\n      scope: invalid.illegal.incomplete.html\n  entities-common:\n    - match: \"(&)([a-zA-Z0-9]+|#[0-9]+|#x[0-9a-fA-F]+)(;)\"\n      scope: constant.character.entity.html\n      captures:\n        1: punctuation.definition.entity.html\n        3: punctuation.definition.entity.html\n  attribute-entities:\n    - include: entities-common\n  entities:\n    - include: entities-common\n    - match: \"&\"\n      scope: invalid.illegal.bad-ampersand.html\n  string-double-quoted:\n    - match: '\"'\n      scope: punctuation.definition.string.begin.html\n      push:\n        - meta_scope: string.quoted.double.html\n        - match: '\"'\n          scope: punctuation.definition.string.end.html\n          pop: true\n        - include: entities\n  string-single-quoted:\n    - match: \"'\"\n      scope: punctuation.definition.string.begin.html\n      push:\n        - meta_scope: string.quoted.single.html\n        - match: \"'\"\n          scope: punctuation.definition.string.end.html\n          pop: true\n        - include: entities\n\n  tag-generic-attribute:\n    - match: '(?:^|\\s+)(([a-zA-Z0-9:\\-_.]+)\\s*(=)\\s*)'\n      captures:\n        1: meta.attribute-with-value.html\n        2: entity.other.attribute-name.html\n        3: punctuation.separator.key-value.html\n      push:\n        - match: '\"'\n          scope: punctuation.definition.string.begin.html\n          set:\n            - meta_scope: meta.attribute-with-value.html string.quoted.double.html\n            - match: '\"'\n              scope: punctuation.definition.string.end.html\n              pop: true\n            - include: attribute-entities\n        - match: \"'\"\n          scope: punctuation.definition.string.begin.html\n          set:\n            - meta_scope: meta.attribute-with-value.html string.quoted.single.html\n            - match: \"'\"\n              scope: punctuation.definition.string.end.html\n              pop: true\n            - include: attribute-entities\n        - match: '(?:[^\\s<>/''\"]|/(?!>))+'\n          scope: meta.attribute-with-value.html string.unquoted.html\n        - match: ''\n          pop: true\n    - match: '\\s+([a-zA-Z0-9:\\-_.]+)'\n      captures:\n        1: entity.other.attribute-name.html\n\n  tag-class-attribute:\n    - match: '(?:^|\\s+)\\b((class)\\b\\s*(=)\\s*)'\n      captures:\n        1: meta.attribute-with-value.class.html\n        2: entity.other.attribute-name.class.html\n        3: punctuation.separator.key-value.html\n      push:\n        - match: '\"'\n          scope: punctuation.definition.string.begin.html\n          set:\n            - meta_scope: meta.attribute-with-value.class.html string.quoted.double.html\n            - meta_content_scope: meta.class-name.html\n            - match: '\"'\n              scope: punctuation.definition.string.end.html\n              pop: true\n            - include: attribute-entities\n        - match: \"'\"\n          scope: punctuation.definition.string.begin.html\n          set:\n            - meta_scope: meta.attribute-with-value.class.html string.quoted.single.html\n            - meta_content_scope: meta.class-name.html\n            - match: \"'\"\n              scope: punctuation.definition.string.end.html\n              pop: true\n            - include: attribute-entities\n        - match: '(?:[^\\s<>/''\"]|/(?!>))+'\n          scope: meta.attribute-with-value.class.html string.unquoted.html meta.class-name.html\n        - match: ''\n          pop: true\n\n  tag-id-attribute:\n    - match: '(?:^|\\s+)\\b((id)\\b\\s*(=)\\s*)'\n      captures:\n        1: meta.attribute-with-value.id.html\n        2: entity.other.attribute-name.id.html\n        3: punctuation.separator.key-value.html\n      push:\n        - match: '\"'\n          scope: punctuation.definition.string.begin.html\n          set:\n            - meta_scope: meta.attribute-with-value.id.html string.quoted.double.html\n            - meta_content_scope: meta.toc-list.id.html\n            - match: '\"'\n              scope: punctuation.definition.string.end.html\n              pop: true\n            - include: attribute-entities\n        - match: \"'\"\n          scope: punctuation.definition.string.begin.html\n          set:\n            - meta_scope: meta.attribute-with-value.id.html string.quoted.single.html\n            - meta_content_scope: meta.toc-list.id.html\n            - match: \"'\"\n              scope: punctuation.definition.string.end.html\n              pop: true\n            - include: attribute-entities\n        - match: '(?:[^\\s<>/''\"]|/(?!>))+'\n          scope: meta.attribute-with-value.id.html string.unquoted.html meta.toc-list.id.html\n        - match: ''\n          pop: true\n\n  tag-style-attribute:\n    - match: '(?:^|\\s+)\\b((style)\\b\\s*(=)\\s*)'\n      captures:\n        1: meta.attribute-with-value.style.html\n        2: entity.other.attribute-name.style.html\n        3: punctuation.separator.key-value.html\n      push:\n        - match: '\"'\n          scope: meta.attribute-with-value.style.html string.quoted.double punctuation.definition.string.begin.html\n          embed: scope:source.css#rule-list-body\n          embed_scope: meta.attribute-with-value.style.html source.css\n          escape: '\"'\n          escape_captures:\n            0: meta.attribute-with-value.style.html string.quoted.double punctuation.definition.string.end.html\n        - match: \"'\"\n          scope: meta.attribute-with-value.style.html string.quoted.single punctuation.definition.string.begin.html\n          embed: scope:source.css#rule-list-body\n          embed_scope: meta.attribute-with-value.style.html source.css\n          escape: \"'\"\n          escape_captures:\n            0: meta.attribute-with-value.style.html string.quoted.single punctuation.definition.string.end.html\n        - match: ''\n          pop: true\n\n  tag-event-attribute:\n    - match: |-\n        (?x)\\s*\\b((\n        onabort|onautocomplete|onautocompleteerror|onblur|oncancel|oncanplay|\n        oncanplaythrough|onchange|onclick|onclose|oncontextmenu|oncuechange|\n        ondblclick|ondrag|ondragend|ondragenter|ondragexit|ondragleave|ondragover|\n        ondragstart|ondrop|ondurationchange|onemptied|onended|onerror|onfocus|\n        oninput|oninvalid|onkeydown|onkeypress|onkeyup|onload|onloadeddata|\n        onloadedmetadata|onloadstart|onmousedown|onmouseenter|onmouseleave|\n        onmousemove|onmouseout|onmouseover|onmouseup|onmousewheel|onpause|onplay|\n        onplaying|onprogress|onratechange|onreset|onresize|onscroll|onseeked|\n        onseeking|onselect|onshow|onsort|onstalled|onsubmit|onsuspend|\n        ontimeupdate|ontoggle|onvolumechange|onwaiting)\\b\\s*(=)\\s*)\n      captures:\n        1: meta.attribute-with-value.event.html\n        2: entity.other.attribute-name.event.html\n        3: punctuation.separator.key-value.html\n      push:\n        - match: '\"'\n          scope: meta.attribute-with-value.event.html string.quoted.double punctuation.definition.string.begin.html\n          embed: scope:source.js\n          embed_scope: meta.attribute-with-value.event.html\n          escape: '\"'\n          escape_captures:\n            0: meta.attribute-with-value.event.html string.quoted.double punctuation.definition.string.end.html\n        - match: \"'\"\n          scope: string.quoted.single punctuation.definition.string.begin.html meta.attribute-with-value.event.html\n          embed: scope:source.js\n          embed_scope: meta.attribute-with-value.event.html\n          escape: \"'\"\n          escape_captures:\n            0: meta.attribute-with-value.event.html string.quoted.single punctuation.definition.string.end.html\n        - match: ''\n          pop: true\n\n  tag-stuff:\n    - include: tag-id-attribute\n    - include: tag-class-attribute\n    - include: tag-style-attribute\n    - include: tag-event-attribute\n    - include: tag-generic-attribute"
  },
  {
    "path": "assets/syntax/large/js.sublime-syntax",
    "content": "%YAML 1.2\n---\n# Derived from JavaScript Next: https://github.com/Benvie/JavaScriptNext.tmLanguage\nname: JavaScript\nfile_extensions:\n  - js\n  - htc\nfirst_line_match: ^#!\\s*/.*\\b(node|js)\\b\nscope: source.js\nvariables:\n  identifier: '[_$[:alpha:]][_$[:alnum:]]*'\n  constant_identifier: '[[:upper:]][_$[:digit:][:upper:]]*\\b'\n  dollar_only_identifier: '\\$(?![_$[:alnum:]])'\n  dollar_identifier: '(\\$)[_$[:alnum:]]+'\n  func_lookahead: '\\s*\\b(async\\s+)?function\\b'\n  arrow_func_lookahead: '\\s*(\\basync\\s*)?([_$[:alpha:]][_$[:alnum:]]*|\\(([^()]|\\([^()]*\\))*\\))\\s*=>'\n\n  method_name: >-\n    (?x)(?:\n      {{identifier}}\n      | '(?:[^\\\\']|\\\\.)*'\n      | \"(?:[^\\\\\"]|\\\\.)*\"\n      | \\[ {{identifier}} (?:\\.{{identifier}})* \\]\n    )\n\n  line_continuation_lookahead: >-\n    (?x)\n    (?! \\+\\+ | -- )\n    (?=\n      != |\n      [ -+*/% ><= &|^ \\[( ;,.:? ]\n    )\n\ncontexts:\n  main:\n    - include: comments\n    - include: comments-top-level\n    - include: keywords-top-level\n    - include: statements\n\n  prototype:\n    - include: comments\n\n  comments:\n    - match: /\\*\\*(?!/)\n      scope: punctuation.definition.comment.js\n      push:\n        - meta_include_prototype: false\n        - meta_scope: comment.block.documentation.js\n        - match: \\*/\n          scope: punctuation.definition.comment.js\n          pop: true\n    - match: /\\*\n      scope: punctuation.definition.comment.js\n      push:\n        - meta_include_prototype: false\n        - meta_scope: comment.block.js\n        - match: \\*/\n          scope: punctuation.definition.comment.js\n          pop: true\n    - match: //\n      scope: punctuation.definition.comment.js\n      push:\n        - meta_include_prototype: false\n        - meta_scope: comment.line.double-slash.js\n        - match: \\n\n          pop: true\n\n  comments-top-level:\n    - match: ^(#!).*$\\n?\n      scope: comment.line.shebang.js\n      captures:\n        1: punctuation.definition.comment.js\n\n  else-pop:\n    - match: (?=\\S)\n      pop: true\n\n  immediately-pop:\n    - match: ''\n      pop: true\n\n  comma-separator:\n    - match: ','\n      scope: punctuation.separator.comma.js\n\n  keywords-top-level:\n    - match: \\bimport\\b\n      scope: keyword.control.import-export.js\n      push:\n        - import-meta\n        - import-export-final\n        - import-extended\n\n    - match: \\bexport\\b\n      scope: keyword.control.import-export.js\n      push:\n        - export-meta\n        - export-extended\n\n    - match: \\b(export|default|from|as)\\b\n      scope: keyword.control.import-export.js\n\n  import-meta:\n    - meta_scope: meta.import.js\n    - include: immediately-pop\n\n  import-export-alias:\n    - match: \\bas\\b\n      scope: keyword.control.import-export.js\n      set:\n        - match: \\bdefault\\b\n          scope: keyword.control.import-export.js\n          pop: true\n        - match: '{{identifier}}'\n          scope: variable.other.readwrite.js\n          pop: true\n        - include: else-pop\n    - include: else-pop\n\n  import-export-final:\n    - match: '\\bfrom\\b'\n      scope: keyword.control.import-export.js\n    - match: (?=['\"])\n      push: literal-string\n    - include: else-pop\n\n  import-extended:\n    - match: (?='|\")\n      pop: true\n    - match: (?=\\S)\n      set:\n        - import-list\n        - import-export-alias\n        - import-item\n\n  import-list:\n    - match: ','\n      scope: punctuation.separator.comma.js\n      push:\n        - import-export-alias\n        - import-item\n    - include: else-pop\n\n  import-item:\n    - match: '\\{'\n      scope: punctuation.section.block.js\n      set: import-brace\n    - match: '{{identifier}}'\n      scope: variable.other.readwrite.js\n      pop: true\n    - match: '\\*'\n      scope: constant.other.js\n      pop: true\n    - include: else-pop\n\n  import-brace:\n    - meta_scope: meta.block.js\n    - include: comma-separator\n    - match: '\\}'\n      scope: punctuation.section.block.js\n      pop: true\n    - match: '{{identifier}}'\n      scope: variable.other.readwrite.js\n      push: import-export-alias\n    - match: '\\*'\n      scope: constant.other.js\n      push: import-export-alias\n    - include: else-pop\n\n  export-meta:\n    - meta_scope: meta.export.js\n    - include: immediately-pop\n\n  export-extended:\n    - include: variable-declaration\n\n    - match: '\\bdefault\\b'\n      scope: keyword.control.import-export.js\n      set:\n        - match: (?=\\bclass\\b)\n          set: class\n\n        - match: (?=\\bfunction\\b)\n          set: function-declaration\n\n        - include: expression\n\n    - match: (?=\\S)\n      set:\n        - import-export-final\n        - export-list\n        - import-export-alias\n        - export-item\n\n  export-list:\n    - match: ','\n      scope: punctuation.separator.comma.js\n      push:\n        - import-export-alias\n        - export-item\n    - include: else-pop\n\n  export-item:\n    - match: '\\{'\n      scope: punctuation.section.block.js\n      set:\n        - export-brace\n    - match: '{{identifier}}'\n      scope: variable.other.readwrite.js\n      pop: true\n    - match: '\\*'\n      scope: constant.other.js\n      pop: true\n    - include: else-pop\n\n  export-brace:\n    - meta_scope: meta.block.js\n    - include: comma-separator\n    - match: '\\}'\n      scope: punctuation.section.block.js\n      pop: true\n    - match: '{{identifier}}'\n      scope: variable.other.readwrite.js\n      push: import-export-alias\n    - match: '\\*'\n      scope: constant.other.js\n      push: import-export-alias\n    - include: else-pop\n\n  statements:\n    - match: \\;\n      scope: punctuation.terminator.statement.js\n\n    - include: conditional\n    - match: '\\{'\n      scope: punctuation.section.block.js\n      push:\n        - meta_scope: meta.block.js\n        - match: '\\}'\n          scope: punctuation.section.block.js\n          pop: true\n        - include: statements\n    - include: label\n\n    - include: variable-declaration\n\n    - match: \\bthrow\\b\n      scope: keyword.control.trycatch.js\n      push: restricted-production\n\n    - match: \\b(break|continue|goto)\\b\n      scope: keyword.control.loop.js\n\n    - match: \\b(yield)\\b(?:\\s*(\\*))?\n      captures:\n        1: keyword.control.flow.js\n        2: keyword.generator.asterisk.js\n      push: restricted-production\n\n    - match: \\b(await|return)\\b\n      scope: keyword.control.flow.js\n      push: restricted-production\n\n    - include: function-or-class-declaration\n\n    - match: (?=\\S)\n      push: expression-statement\n\n  variable-declaration:\n    - match: \\b(const|let|var)\\b\n      scope: storage.type.js\n      push: expression-statement\n\n  function-or-class-declaration:\n    - match: (?=\\bclass\\b)\n      push: class\n\n    - match: (?=\\bfunction\\b)\n      push: function-declaration\n\n  expression-statement:\n    - match: (?=\\S)\n      set: [ expression-statement-end, expression-begin ]\n\n  expression-statement-end:\n    - match: \\n\n      set:\n        - match: '{{line_continuation_lookahead}}'\n          set: expression-statement-end\n        - include: else-pop\n    - include: expression-end\n\n  restricted-production:\n    - match: \\n\n      pop: true\n    - match: (?=\\S)\n      set: expression-statement\n\n  expect-case-colon:\n    - match: ':'\n      scope: punctuation.separator.js\n      pop: true\n    - include: else-pop\n\n  conditional:\n    - match: \\bswitch\\b\n      scope: keyword.control.switch.js\n      push:\n        - meta_scope: meta.switch.js\n        - match: (?=\\()\n          push: parenthesized-expression\n        - match: '\\}'\n          scope: meta.block.js punctuation.section.block.js\n          pop: true\n        - match: '\\{'\n          scope: punctuation.section.block.js\n          push:\n            - meta_scope: meta.block.js\n\n            - match: '(?=\\})'\n              pop: true\n\n            - match: \\b(case)\\b\n              scope: keyword.control.switch.js\n              push:\n                - expect-case-colon\n                - expression\n\n            - match: \\b(default)\\b\n              scope: keyword.control.switch.js\n              push:\n                - expect-case-colon\n\n            - include: statements\n\n    - match: \\bdo\\b\n      scope: keyword.control.loop.js\n      push:\n        - meta_scope: meta.do-while.js\n        - match: '\\{'\n          scope: punctuation.section.block.js\n          push:\n            - meta_scope: meta.block.js\n            - match: '\\}'\n              scope: punctuation.section.block.js\n              pop: true\n            - include: statements\n        - match: \\bwhile\\b\n          scope: keyword.control.loop.js\n        - match: '\\('\n          scope: punctuation.section.group.js\n          push:\n            - meta_scope: meta.group.js\n            - match: '(?=\\))'\n              pop: true\n            - match: (?=\\S)\n              push: expression\n        - match: '\\)'\n          scope: meta.group.js punctuation.section.group.js\n          pop: true\n\n    - match: \\bfor\\b\n      scope: keyword.control.loop.js\n      push:\n        - meta_scope: meta.for.js\n        - include: parens-block-scope\n\n    - match: \\bwhile\\b\n      scope: keyword.control.loop.js\n      push:\n        - meta_scope: meta.while.js\n        - include: parens-block-scope\n\n    - match: \\bwith\\b\n      scope: keyword.control.with.js\n      push:\n        - meta_scope: meta.with.js\n        - include: parens-block-scope\n\n    - match: \\b(else\\s+if|if)\\b\n      scope: keyword.control.conditional.js\n      push:\n        - meta_scope: meta.conditional.js\n        - include: parens-block-scope\n\n    - match: \\belse\\b\n      scope: keyword.control.conditional.js\n      push:\n        - meta_scope: meta.conditional.js\n        - include: block-scope\n\n    - match: \\btry\\b\n      scope: keyword.control.trycatch.js\n      push:\n        - meta_scope: meta.try.js\n        - include: block-scope\n\n    - match: \\bfinally\\b\n      scope: keyword.control.trycatch.js\n      push:\n        - meta_scope: meta.finally.js\n        - include: block-scope\n\n    - match: \\bcatch\\b\n      scope: keyword.control.trycatch.js\n      push:\n        - meta_scope: meta.catch.js\n        - include: parens-block-scope\n\n  parens-block-scope:\n    - match: '\\('\n      scope: punctuation.section.group.js\n      push:\n        - meta_scope: meta.group.js\n        - match: '\\)'\n          scope: punctuation.section.group.js\n          pop: true\n\n        - match: ;\n          scope: punctuation.terminator.statement.js\n\n        - match: \\b(const|let|var)\\b\n          scope: storage.type.js\n\n        - include: expression-list\n    - include: block-scope\n\n  block-scope:\n    - match: '\\}'\n      scope: meta.block.js punctuation.section.block.js\n      pop: true\n    - match: '\\{'\n      scope: punctuation.section.block.js\n      push:\n        - meta_scope: meta.block.js\n        - match: (?=})\n          pop: true\n        - include: statements\n    - include: else-pop\n\n  block-meta:\n    - meta_scope: meta.block.js\n    - include: immediately-pop\n\n  expression-break:\n    - match: (?=[;})\\]])\n      pop: true\n\n  expression:\n    - match: (?=\\S)\n      set: [ expression-end, expression-begin ]\n\n  expression-no-comma:\n    - match: (?=\\S)\n      set: [ expression-end-no-comma, expression-begin ]\n\n  expression-list:\n    - include: expression-break\n    - include: comma-separator\n    - match: (?=\\S)\n      push: expression-no-comma\n\n  expression-end:\n    - include: expression-break\n\n    - include: postfix-operators\n    - include: binary-operators\n    - include: ternary-operator\n\n    - include: property-access\n    - include: function-call\n\n    - include: fallthrough\n\n    - include: else-pop\n\n  expression-end-no-comma:\n    - match: (?=,)\n      pop: true\n    - include: expression-end\n\n  expression-begin:\n    - match: \\)\n      scope: invalid.illegal.stray-bracket-end.js\n      pop: true\n\n    - include: expression-break\n\n    - include: literal-prototype\n\n    - include: regexp-complete\n    - include: literal-string\n    - include: literal-string-template\n    - include: constructor\n    - include: prefix-operators\n\n    - include: class\n    - include: constants\n    - include: function-assignment\n    - include: either-function-declaration\n    - include: object-literal\n\n    - include: parenthesized-expression\n    - include: array-literal\n\n    - include: literal-number\n    - include: literal-call\n    - include: literal-variable\n\n    - include: else-pop\n\n  fallthrough:\n    # If an arrow function has the ( and ) on different lines, we won't have matched\n    - match: =>\n      scope: storage.type.function.arrow.js\n\n  literal-string:\n    - match: \"'\"\n      scope: punctuation.definition.string.begin.js\n      set:\n        - meta_include_prototype: false\n        - meta_scope: string.quoted.single.js\n        - match: (')|(\\n)\n          captures:\n            1: punctuation.definition.string.end.js\n            2: invalid.illegal.newline.js\n          pop: true\n        - include: string-content\n    - match: '\"'\n      captures:\n        0: punctuation.definition.string.begin.js\n      set:\n        - meta_include_prototype: false\n        - meta_scope: string.quoted.double.js\n        - match: (\")|(\\n)\n          captures:\n            1: punctuation.definition.string.end.js\n            2: invalid.illegal.newline.js\n          pop: true\n        - include: string-content\n\n  literal-string-template:\n    - match: '({{identifier}})?(`)'\n      captures:\n        1: variable.function.tagged-template.js\n        2: punctuation.definition.string.template.begin.js\n      set:\n        - meta_include_prototype: false\n        - meta_scope: string.template.js\n        - match: \"`\"\n          scope: punctuation.definition.string.template.end.js\n          pop: true\n        - match: '\\$\\{'\n          captures:\n            0: punctuation.definition.template-expression.begin.js\n          push:\n            - clear_scopes: 1\n            - meta_scope: meta.template.expression.js\n            - meta_content_scope: source.js.embedded.expression\n            - match: '\\}'\n              scope: punctuation.definition.template-expression.end.js\n              pop: true\n            - match: (?=\\S)\n              push: expression\n        - include: string-content\n\n  string-content:\n    - match: \\\\\\s*\\n\n      scope: constant.character.escape.newline.js\n    - match: '\\\\(x[\\da-fA-F][\\da-fA-F]|u[\\da-fA-F][\\da-fA-F][\\da-fA-F][\\da-fA-F]|.)'\n      scope: constant.character.escape.js\n\n  regexp-complete:\n    - match: '/'\n      scope: punctuation.definition.string.begin.js\n      set: regexp\n\n  regexp:\n      - meta_include_prototype: false\n      - meta_scope: string.regexp.js\n      - match: \"(/)([gimyu]*)\"\n        captures:\n          1: punctuation.definition.string.end.js\n          2: keyword.other.js\n        pop: true\n      - match: '(?=.|\\n)'\n        push:\n          - meta_include_prototype: false\n          - match: '(?=/)'\n            pop: true\n          - include: scope:source.regexp.js\n\n  constructor:\n    - match: '\\bnew\\b'\n      scope: keyword.operator.word.new.js\n      set:\n        - constructor-meta\n        - constructor-body\n\n  constructor-meta:\n    - meta_scope: meta.instance.constructor.js\n    - include: immediately-pop\n\n  constructor-body:\n    - match: ''\n      set:\n        - constructor-body-meta\n        - constructor-body-expect-arguments\n        - constructor-body-expect-property-access\n        - constructor-body-expect-class\n\n  constructor-body-meta:\n    - meta_scope: meta.function-call.constructor.js\n    - include: immediately-pop\n\n  constructor-body-expect-arguments:\n    - match: '(?=\\()'\n      set: function-call-params\n    - include: else-pop\n\n  constructor-body-expect-property-access:\n    - include: property-access\n    - include: else-pop\n\n  constructor-body-expect-class:\n    - include: expression-break\n\n    - include: regexp-complete\n    - include: literal-string\n    - include: literal-string-template\n\n    - include: class\n    - include: constants\n    - include: either-function-declaration\n    - include: object-literal\n\n    - include: parenthesized-expression\n    - include: array-literal\n\n    - include: literal-number\n\n    - include: well-known-identifiers\n    - include: language-identifiers\n\n    - match: '{{dollar_only_identifier}}'\n      scope: variable.type.dollar.only.js punctuation.dollar.js\n    - match: '{{dollar_identifier}}'\n      scope: variable.type.dollar.js\n      captures:\n        1: punctuation.dollar.js\n    - match: '{{identifier}}'\n      scope: variable.type.js\n      pop: true\n\n    - include: else-pop\n\n  prefix-operators:\n    - match: '~'\n      scope: keyword.operator.bitwise.js\n    - match: '!(?!=)'\n      scope: keyword.operator.logical.js\n    - match: '--'\n      scope: keyword.operator.arithmetic.js\n    - match: '\\+\\+'\n      scope: keyword.operator.arithmetic.js\n    - match: \\.\\.\\.\n      scope: keyword.operator.spread.js\n    - match: \\+|\\-\n      scope: keyword.operator.arithmetic.js\n\n    - match: \\bnew\\b\n      scope: keyword.operator.word.new.js\n    - match: \\b(?:delete|typeof|void)\\b\n      scope: keyword.operator.js\n\n  binary-operators:\n    - match: \\binstanceof\\b\n      scope: keyword.operator.js\n      push: expression-begin\n    - match: \\b(in|of)\\b\n      scope: keyword.operator.js\n      push: expression-begin\n    - match: '&&|\\|\\|'\n      scope: keyword.operator.logical.js\n      push: expression-begin\n    - match: '=(?![=>])'\n      scope: keyword.operator.assignment.js\n      push: expression-begin\n    - match: |-\n        (?x)\n        %=   | # assignment      right-to-left   both\n        &=   | # assignment      right-to-left   both\n        \\*=  | # assignment      right-to-left   both\n        \\+=  | # assignment      right-to-left   both\n        -=   | # assignment      right-to-left   both\n        /=   | # assignment      right-to-left   both\n        \\^=  | # assignment      right-to-left   both\n        \\|=  | # assignment      right-to-left   both\n        <<=  | # assignment      right-to-left   both\n        >>=  | # assignment      right-to-left   both\n        >>>=   # assignment      right-to-left   both\n      scope: keyword.operator.assignment.augmented.js\n      push: expression-begin\n    - match: |-\n        (?x)\n        <<   | # bitwise-shift   left-to-right   both\n        >>>  | # bitwise-shift   left-to-right   both\n        >>   | # bitwise-shift   left-to-right   both\n        &    | # bitwise-and     left-to-right   both\n        \\^   | # bitwise-xor     left-to-right   both\n        \\|     # bitwise-or      left-to-right   both\n      scope: keyword.operator.bitwise.js\n      push: expression-begin\n    - match: |-\n        (?x)\n        <=   | # relational      left-to-right   both\n        >=   | # relational      left-to-right   both\n        <    | # relational      left-to-right   both\n        >      # relational      left-to-right   both\n      scope: keyword.operator.relational.js\n      push: expression-begin\n    - match: |-\n        (?x)\n        ===  | # equality        left-to-right   both\n        !==  | # equality        left-to-right   both\n        ==   | # equality        left-to-right   both\n        !=     # equality        left-to-right   both\n      scope: keyword.operator.comparison.js\n      push: expression-begin\n    - match: |-\n        (?x)\n        /    | # division        left-to-right   both\n        %    | # modulus         left-to-right   both\n        \\*   | # multiplication  left-to-right   both\n        \\+   | # addition        left-to-right   both\n        -      # subtraction     left-to-right   both\n      scope: keyword.operator.arithmetic.js\n      push: expression-begin\n    - match: ','\n      scope: punctuation.separator.comma.js # TODO: Change to keyword.operator.comma.js ?\n      push: expression-begin\n\n  ternary-operator:\n    - match: '\\?'\n      scope: keyword.operator.ternary.js\n      set:\n        - ternary-operator-expect-colon\n        - expression-no-comma\n\n  ternary-operator-expect-colon:\n    - match: ':'\n      scope: keyword.operator.ternary.js\n      set: expression-no-comma\n    - include: else-pop\n\n  postfix-operators:\n    - match: '--'\n      scope: keyword.operator.arithmetic.js\n    - match: '\\+\\+'\n      scope: keyword.operator.arithmetic.js\n\n  class:\n    - match: \\bclass\\b\n      scope: storage.type.class.js\n      set:\n        - meta_scope: meta.class.js\n        - match: '\\{'\n          scope: punctuation.section.block.js\n          set: class-body\n        - match: '\\b(extends)\\b\\s+(?={{identifier}})'\n          captures:\n            1: storage.modifier.extends.js\n          push:\n            - match: '{{identifier}}'\n              scope: entity.other.inherited-class.js\n            - match: '\\.'\n              scope: punctuation.accessor.js\n            - include: else-pop\n        - match: '{{identifier}}'\n          scope: entity.name.class.js\n\n  class-body:\n    - meta_scope: meta.class.js meta.block.js\n    - match: '\\}'\n      scope: punctuation.section.block.js\n      pop: true\n    - include: method-declaration\n\n  constants:\n    - match: \\btrue\\b\n      scope: constant.language.boolean.true.js\n      pop: true\n    - match: \\bfalse\\b\n      scope: constant.language.boolean.false.js\n      pop: true\n    - match: \\bnull\\b\n      scope: constant.language.null.js\n      pop: true\n    - match: \\bundefined\\b\n      scope: constant.language.undefined.js\n      pop: true\n    - match: \\bNaN\\b\n      scope: constant.language.nan.js\n      pop: true\n\n  literal-prototype:\n    - match: '({{identifier}})\\s*(\\.)\\s*(prototype)(?=\\s*=\\s*({{func_lookahead}}|{{arrow_func_lookahead}}))'\n      scope: meta.prototype.declaration.js\n      captures:\n        1: support.class.js\n        2: punctuation.accessor.js\n        3: support.constant.prototype.js\n      set:\n        - meta_scope: meta.function.declaration.js\n        - match: '='\n          scope: keyword.operator.assignment.js\n        - match: '(?={{func_lookahead}})'\n          set: function-declaration\n        - match: '(?={{arrow_func_lookahead}})'\n          set: arrow-function-declaration\n        - include: else-pop\n    - match: '({{identifier}})\\s*(\\.)\\s*(prototype)\\s*(\\.)\\s*(?={{identifier}}\\s*=\\s*({{func_lookahead}}|{{arrow_func_lookahead}}))'\n      captures:\n        1: support.class.js\n        2: punctuation.accessor.js\n        3: support.constant.prototype.js\n        4: punctuation.accessor.js\n      set:\n        - meta_scope: meta.function.declaration.js\n        - match: '(?={{func_lookahead}})'\n          set: function-declaration\n        - match: '(?={{arrow_func_lookahead}})'\n          set: arrow-function-declaration\n        - include: function-declaration-final-identifier\n    - match: '({{identifier}})(\\.)(prototype)\\b'\n      scope: meta.prototype.access.js\n      captures:\n        1: support.class.js\n        2: punctuation.accessor.js\n        3: support.constant.prototype.js\n      pop: true\n\n  function-assignment:\n    - match: '(?=(({{identifier}})\\s*(\\.)\\s*)+({{identifier}})\\s*(=)\\s*({{func_lookahead}}|{{arrow_func_lookahead}}))'\n      set:\n        - meta_scope: meta.function.declaration.js\n        - match: '(?={{func_lookahead}})'\n          set: function-declaration\n        - match: '(?={{arrow_func_lookahead}})'\n          set: arrow-function-declaration\n        - include: function-declaration-identifiers\n    - match: '(?=({{identifier}})\\s*(=)\\s*({{func_lookahead}}|{{arrow_func_lookahead}}))'\n      set:\n        - meta_scope: meta.function.declaration.js\n        - match: '(?={{func_lookahead}})'\n          set: function-declaration\n        - match: '(?={{arrow_func_lookahead}})'\n          set: arrow-function-declaration\n        - include: function-declaration-single-identifier\n\n  function-declaration-identifiers:\n    - match: '(?={{identifier}}\\s*\\.)'\n      push:\n        - function-declaration-identifiers-expect-dot\n        - function-declaration-identifiers-expect-class\n    - include: function-declaration-final-identifier\n\n  function-declaration-identifiers-expect-dot:\n    - match: '\\.'\n      scope: punctuation.accessor.js\n      pop: true\n    - include: else-pop\n\n  function-declaration-identifiers-expect-class:\n    - match: '\\bprototype\\b'\n      scope: support.constant.prototype.js\n    - include: language-identifiers\n    - match: '{{dollar_only_identifier}}'\n      scope: support.class.dollar.only.js punctuation.dollar.js\n    - match: '{{dollar_identifier}}'\n      scope: support.class.dollar.js\n      captures:\n        1: punctuation.dollar.js\n    - match: '{{identifier}}'\n      scope: support.class.js\n    - include: else-pop\n\n  function-declaration-final-identifier:\n    - match: '(?={{identifier}}\\s*(=)\\s*)'\n      push:\n        - match: '{{dollar_only_identifier}}'\n          scope: meta.property.object.dollar.only.js punctuation.dollar.js entity.name.function.js\n        - match: '{{dollar_identifier}}'\n          scope: meta.property.object.dollar.js entity.name.function.js\n          captures:\n            1: punctuation.dollar.js\n        - match: '{{identifier}}'\n          scope: meta.property.object.js entity.name.function.js\n        - match: '\\s*(=)\\s*'\n          captures:\n            1: keyword.operator.assignment.js\n          pop: true\n\n  function-declaration-single-identifier:\n    - match: '\\s*(=)\\s*'\n      captures:\n        1: keyword.operator.assignment.js\n    - match: '(?={{identifier}})'\n      push:\n        # These matches have to be duplicated to get entity.name.function\n        # on the end of the scope stack since most color schemes require it\n        - match: '{{dollar_only_identifier}}'\n          scope: variable.other.dollar.only.js punctuation.dollar.js entity.name.function.js\n        - match: '{{dollar_identifier}}'\n          scope: variable.other.dollar.js entity.name.function.js\n          captures:\n            1: punctuation.dollar.js\n        - match: '{{constant_identifier}}'\n          scope: variable.other.constant.js entity.name.function.js\n        - match: '{{identifier}}'\n          scope: variable.other.readwrite.js entity.name.function.js\n        - match: (?=.)\n          pop: true\n\n  either-function-declaration:\n    - match: '(?={{func_lookahead}})'\n      set: function-declaration\n    - match: '(?={{arrow_func_lookahead}})'\n      set: arrow-function-declaration\n\n  function-declaration:\n    - match: ''\n      set:\n        - function-declaration-expect-body\n        - function-declaration-meta\n        - function-declaration-expect-parameters\n        - function-declaration-expect-name\n        - function-declaration-expect-function-keyword\n        - function-declaration-expect-async\n\n  function-declaration-expect-body:\n    - match: (?=\\S)\n      set: function-block\n\n  function-declaration-meta:\n    - meta_scope: meta.function.declaration.js\n    - include: immediately-pop\n\n  function-declaration-expect-parameters:\n    - include: function-declaration-parameters\n    - include: else-pop\n\n  function-declaration-expect-name:\n    - match: '{{identifier}}'\n      scope: entity.name.function.js\n      pop: true\n    - include: else-pop\n\n  function-declaration-expect-function-keyword:\n    - match: \\b(function)\\b\\s*(\\*)?\n      captures:\n        1: storage.type.function.js\n        2: keyword.generator.asterisk.js\n      pop: true\n    - include: else-pop\n\n  function-declaration-expect-async:\n    - match: '\\basync\\b'\n      scope: storage.type.js\n      pop: true\n    - include: else-pop\n\n  arrow-function-declaration:\n    - match: ''\n      set:\n        - arrow-function-expect-body\n        - function-declaration-meta\n        - arrow-function-expect-arrow\n        - arrow-function-expect-parameters\n        - function-declaration-expect-async\n\n  arrow-function-expect-body:\n    - match: (?=\\{)\n      set: function-block\n    - match: (?=\\S)\n      set:\n        - block-meta\n        - expression-no-comma\n\n  arrow-function-expect-arrow:\n    - match: '=>'\n      scope: storage.type.function.arrow.js\n      pop: true\n    - include: else-pop\n\n  arrow-function-expect-parameters:\n    - match: '{{identifier}}'\n      scope: variable.parameter.function.js\n      pop: true\n    - include: function-declaration-parameters\n    - include: else-pop\n\n  function-block:\n    - meta_scope: meta.block.js\n    - match: '\\}'\n      scope: punctuation.section.block.js\n      pop: true\n    - match: '\\{'\n      scope: punctuation.section.block.js\n      push:\n        - match: '(?=\\})'\n          pop: true\n        - include: statements\n    - include: else-pop\n\n  function-declaration-parameters:\n    - match: \\s+\n      scope: meta.function.declaration.js\n    - match: \\(\n      scope: punctuation.section.group.begin.js\n      push:\n        - match: \\)\n          scope: punctuation.section.group.end.js\n          pop: true\n        # Destructuring\n        - match: \\{\n          scope: punctuation.section.block.begin.js\n          push:\n            - meta_scope: meta.block.js\n            - match: \\}\n              scope: punctuation.section.block.end.js\n              pop: true\n            - match: '{{identifier}}'\n              scope: variable.parameter.function.js\n            - match: ','\n              scope: punctuation.separator.parameter.function.js\n            - match: '='\n              scope: keyword.operator.assignment.js\n              push:\n                - meta_scope: meta.parameter.optional.js\n                - match: \"(?=[,)}])\"\n                  pop: true\n                - match: (?=\\S)\n                  push: expression-no-comma\n        - match: \\.\\.\\.\n          scope: keyword.operator.spread.js\n        - match: '{{identifier}}'\n          scope: variable.parameter.function.js\n        - match: ','\n          scope: punctuation.separator.parameter.function.js\n        - match: '='\n          scope: keyword.operator.assignment.js\n          push:\n            - meta_scope: meta.parameter.optional.js\n            - match: \"(?=[,)])\"\n              pop: true\n            - match: (?=\\S)\n              push: expression-no-comma\n\n  label:\n    - match: '({{identifier}})\\s*(:)'\n      captures:\n        1: entity.name.label.js\n        2: punctuation.separator.js\n\n  object-literal:\n    - match: '\\{'\n      scope: punctuation.section.block.js\n      set:\n        - meta_scope: meta.object-literal.js\n        - match: '\\}'\n          scope: punctuation.section.block.js\n          pop: true\n\n        - match: >-\n            (?x)(?=\n              {{method_name}}\\s*:\n              (?: {{func_lookahead}} | {{arrow_func_lookahead}} )\n            )\n          push:\n            - either-function-declaration\n            - function-declaration-meta\n            - object-literal-expect-colon\n            - object-literal-meta-key\n            - method-name\n\n        - include: method-declaration\n\n        - match: '{{identifier}}(?=\\s*(?:[},]|$|//|/\\*))'\n          scope: variable.other.readwrite.js\n        - match: \\[\n          scope: punctuation.section.brackets.js\n          push:\n            - match: \\]\n              scope: punctuation.section.brackets.js\n              pop: true\n            - match: (?=\\S)\n              push: expression\n        - match: \"(?=\\\"|')\"\n          push:\n            - object-literal-meta-key\n            - literal-string\n        - match: '(\\$)[$\\w]*(?=\\s*:)'\n          scope: meta.object-literal.key.dollar.js\n          captures:\n            1: punctuation.dollar.js\n        - match: '{{identifier}}(?=\\s*:)'\n          scope: meta.object-literal.key.js\n        - match: (?=[-+]?(?:\\.[0-9]|0[bxo]|\\d))\n          push:\n            - meta_scope: meta.object-literal.key.js\n            - include: literal-number\n\n        - include: comma-separator\n        - match: ':'\n          scope: punctuation.separator.key-value.js\n          push: expression-no-comma\n\n  object-literal-meta-key:\n    - meta_scope: meta.object-literal.key.js \n    - include: else-pop\n\n  object-literal-expect-colon:\n    - match: ':'\n      scope: punctuation.separator.key-value.js\n    - include: else-pop\n\n  method-name:\n    - match: '(\\$)[_$[:alnum:]]*'\n      scope: meta.object-literal.key.dollar.js entity.name.function.js\n      captures:\n        1: punctuation.dollar.js\n      pop: true\n    - match: '{{identifier}}'\n      scope: entity.name.function.js\n      pop: true\n    - match: \"'\"\n      scope: punctuation.definition.string.begin.js\n      set:\n        - meta_include_prototype: false\n        - meta_scope: string.quoted.single.js\n        - meta_content_scope: entity.name.function.js\n        - match: (')|(\\n)\n          captures:\n            1: punctuation.definition.string.end.js\n            2: invalid.illegal.newline.js\n          pop: true\n        - include: string-content\n    - match: '\"'\n      scope: punctuation.definition.string.begin.js\n      set:\n        - meta_include_prototype: false\n        - meta_scope: string.quoted.double.js\n        - meta_content_scope: entity.name.function.js\n        - match: (\")|(\\n)\n          captures:\n            1: punctuation.definition.string.end.js\n            2: invalid.illegal.newline.js\n          pop: true\n        - include: string-content\n\n    - match: '(\\[)({{identifier}}(?:\\.{{identifier}}|\\.)*)?(\\])?'\n      captures:\n        1: punctuation.definition.symbol.begin.js\n        2: entity.name.function.js\n        3: punctuation.definition.symbol.end.js\n      pop: true\n\n    - include: else-pop\n\n  method-declaration:\n    - match: |-\n        (?x)(?=\n          \\b(?: get|set|async|static )\\b\n          | \\*\n          | {{method_name}} \\s* \\(\n        )\n      push:\n        - function-declaration-expect-body\n        - function-declaration-meta\n        - function-declaration-expect-parameters\n        - method-name\n        - method-declaration-expect-prefix\n\n  method-declaration-expect-prefix:\n    - match: \\*\n      scope: keyword.generator.asterisk.js\n    - match: \\b(get|set)\\b(?!\\s*\\()\n      scope: storage.type.accessor.js\n    - match: \\bstatic\\b\n      scope: storage.type.js\n    - include: else-pop\n\n  parenthesized-expression:\n    - match: \\(\n      scope: punctuation.section.group.js\n      set:\n        - meta_scope: meta.group.js\n        - match: \\)\n          scope: punctuation.section.group.js\n          pop: true\n        - match: (?=\\S)\n          push: expression\n    - match: \\)\n      scope: invalid.illegal.stray-bracket-end.js\n      pop: true\n\n  function-call:\n    - match: \\(\n      scope: punctuation.section.group.js\n      push:\n        - meta_scope: meta.group.js\n        - match: \\)\n          scope: punctuation.section.group.js\n          pop: true\n        - match: (?=\\S)\n          push: expression\n\n  array-literal:\n    - match: '\\['\n      scope: punctuation.section.brackets.js\n      set:\n        - meta_scope: meta.sequence.js\n        - match: '\\]'\n          scope: punctuation.section.brackets.js\n          pop: true\n        - include: expression-list\n\n  property-access:\n    - match: '\\['\n      scope: punctuation.section.brackets.js\n      push:\n        - meta_scope: meta.brackets.js\n        - match: '\\]'\n          scope: punctuation.section.brackets.js\n          pop: true\n        - match: (?=\\S)\n          push: expression\n\n    - match: \\.\n      scope: punctuation.accessor.js\n      push:\n      # All of these matches use set (or effectively a set via the final\n      # include/match/pop construct) instead of push so that we escape this\n      # accessor state once a match has been made. Otherwise identifiers\n      # following method definitions or method calls will be scoped as\n      # properties.\n        - match: '(?=({{identifier}})\\s*(=)\\s*({{func_lookahead}}|{{arrow_func_lookahead}}))'\n          set:\n            - meta_scope: meta.function.declaration.js\n            - match: '(?={{func_lookahead}})'\n              set: function-declaration\n            - match: '(?={{arrow_func_lookahead}})'\n              set: arrow-function-declaration\n            - include: function-declaration-final-identifier\n        - match: '(?={{identifier}}\\s*\\()'\n          set:\n            - include: method-call\n            - match: '(?=.|\\n)'\n              pop: true\n        - include: object-property\n\n  literal-number:\n    - match: '(?i)(?:\\B[-+]|\\b)0x[0-9a-f]*\\.(\\B|\\b[0-9]+)'\n      scope: invalid.illegal.numeric.hex.js\n      pop: true\n    - match: '(?:\\B[-+]|\\b)0[0-9]+\\.(\\B|\\b[0-9]+)'\n      scope: invalid.illegal.numeric.octal.js\n      pop: true\n    - match: |-\n        (?xi)\n        (?:\\B[-+])?\n        (?:\n          \\b0b[0-1]*|                 # binary\n          \\b0o[0-7]*|                 # octal\n          \\b0x[0-9a-f]*|              # hex\n          (\n            \\B\\.[0-9]+|               # e.g. .999\n            \\b[0-9]+(\\.[0-9]*)?       # e.g. 999.999, 999. or 999\n          )(e[-+]?[0-9]+)?            # e.g. e+123, E-123\n        )\n      scope: constant.numeric.js\n      pop: true\n    - match: '(?:\\B[-+]|\\b)(Infinity)\\b'\n      scope: constant.language.infinity.js\n      pop: true\n\n  literal-call:\n    - match: (\\$)(?=\\s*\\()\n      scope: variable.function.js variable.other.dollar.only.js punctuation.dollar.js\n      set:\n        - meta_scope: meta.function-call.js\n        - include: function-call-params\n    - match: \\b(clearTimeout|decodeURI|decodeURIComponent|encodeURI|encodeURIComponent|escape|eval|isFinite|isNaN|parseFloat|parseInt|setTimeout|super|unescape)\\b(?=\\()\n      scope: support.function.js\n      set:\n        - meta_scope: meta.function-call.js\n        - include: function-call-params\n    - match: '({{identifier}})(?=\\s*\\()'\n      scope: variable.function.js\n      set:\n        - meta_scope: meta.function-call.js\n        - include: function-call-params\n    - match: '(?={{identifier}}\\s*\\.\\s*{{identifier}}\\s*\\()'\n      set:\n        - match: \\b(console)(?:(\\.)(warn|info|log|error|time|timeEnd|assert|count|dir|group|groupCollapsed|groupEnd|profile|profileEnd|table|trace|timeStamp))?\\b\n          captures:\n            1: support.type.object.console.js\n            2: punctuation.accessor.js\n            3: support.function.console.js\n          set:\n            - meta_scope: meta.function-call.method.js\n            - include: function-call-params\n        - match: \\b(process)(?:(\\.)(abort|chdir|cwd|disconnect|exit|[sg]ete?[gu]id|send|[sg]etgroups|initgroups|kill|memoryUsage|nextTick|umask|uptime|hrtime))?\\b\n          captures:\n            1: support.type.object.process.js\n            2: punctuation.accessor.js\n            3: support.function.process.js\n          set:\n            - meta_scope: meta.function-call.method.js\n            - include: function-call-params\n        - match: '(?={{identifier}}\\s*\\.)'\n          push:\n            - include: well-known-identifiers\n            - include: language-identifiers\n            - match: '{{dollar_only_identifier}}'\n              scope: variable.other.object.dollar.only.js punctuation.dollar.js\n            - match: '{{dollar_identifier}}'\n              scope: variable.other.object.dollar.js\n              captures:\n                1: punctuation.dollar.js\n            - match: '{{identifier}}'\n              scope: variable.other.object.js\n            - match: \\.\n              scope: punctuation.accessor.js\n              pop: true\n        - match: \\.\n          scope: punctuation.accessor.js\n        - include: method-call\n        - match: '(?=[^ ])'\n          pop: true\n\n  method-call:\n    - match: \\b(shift|sort|splice|unshift|pop|push|reverse|copyWithin|fill)\\b(?=\\()\n      scope: support.function.mutator.js\n      set:\n        - meta_scope: meta.function-call.method.js\n        - include: function-call-params\n    - match: \\b(s(ub(stringData|mit)|plitText|e(t(NamedItem|Attribute(Node)?)|lect))|has(ChildNodes|Feature)|namedItem|c(l(ick|o(se|neNode))|reate(C(omment|DATASection|aption)|T(Head|extNode|Foot)|DocumentFragment|ProcessingInstruction|E(ntityReference|lement)|Attribute))|tabIndex|i(nsert(Row|Before|Cell|Data)|tem)|open|delete(Row|C(ell|aption)|T(Head|Foot)|Data)|focus|write(ln)?|a(dd|ppend(Child|Data))|re(set|place(Child|Data)|move(NamedItem|Child|Attribute(Node)?)?)|get(NamedItem|Element(sBy(Name|TagName)|ById)|Attribute(Node)?)|blur)\\b(?=\\()\n      scope: support.function.dom.js\n      set:\n        - meta_scope: meta.function-call.method.js\n        - include: function-call-params\n    - match: '({{identifier}})\\s*(?=\\()'\n      scope: variable.function.js\n      set:\n        - meta_scope: meta.function-call.method.js\n        - include: function-call-params\n\n  function-call-params:\n    - match: '\\)'\n      scope: meta.group.js punctuation.section.group.js\n      pop: true\n    - match: '\\('\n      scope: punctuation.section.group.js\n      push:\n        - meta_scope: meta.group.js\n        - match: '(?=\\))'\n          pop: true\n        # Consume comma plus any whitespace to prevent whitespace from\n        # getting meta scopes when they don't really apply\n        - match: '(,)\\s+'\n          captures:\n            1: punctuation.separator.comma.js\n        - match: (?=\\S)\n          push: expression-no-comma\n    - include: else-pop\n\n  literal-variable:\n    - include: well-known-identifiers\n    - include: language-identifiers\n    - include: dollar-identifiers\n    - include: support\n    - match: '\\b[[:upper:]][_$[:alnum:]]*(?=\\s*[\\[.])'\n      scope: support.class.js\n      pop: true\n    - match: '{{identifier}}(?=\\s*[\\[.])'\n      scope: variable.other.object.js\n      pop: true\n    - include: simple-identifiers\n\n  well-known-identifiers:\n    - match: \\b(Array|Boolean|Date|Function|Map|Math|Number|Object|Promise|Proxy|RegExp|Set|String|WeakMap|XMLHttpRequest)\\b\n      scope: support.class.builtin.js\n      pop: true\n    - match: \\b((Eval|Range|Reference|Syntax|Type|URI)?Error)\\b\n      scope: support.class.error.js\n      pop: true\n    - match: \\b(document|window|navigator)\\b\n      scope: support.type.object.dom.js\n      pop: true\n    - match: \\b(Buffer|EventEmitter|Server|Pipe|Socket|REPLServer|ReadStream|WriteStream|Stream|Inflate|Deflate|InflateRaw|DeflateRaw|GZip|GUnzip|Unzip|Zip)\\b\n      scope: support.class.node.js\n      pop: true\n\n  language-identifiers:\n    - match: \\b(arguments)\\b\n      scope: variable.language.arguments.js\n      pop: true\n    - match: \\b(super)\\b\n      scope: variable.language.super.js\n      pop: true\n    - match: \\b(this)\\b\n      scope: variable.language.this.js\n      pop: true\n    - match: \\b(self)\\b\n      scope: variable.language.self.js\n      pop: true\n\n  dollar-identifiers:\n    - match: '{{dollar_only_identifier}}'\n      scope: variable.other.dollar.only.js punctuation.dollar.js\n      pop: true\n    - match: '{{dollar_identifier}}'\n      scope: variable.other.dollar.js\n      captures:\n        1: punctuation.dollar.js\n      pop: true\n\n  simple-identifiers:\n    - match: '{{constant_identifier}}'\n      scope: variable.other.constant.js\n      pop: true\n    - match: '{{identifier}}'\n      scope: variable.other.readwrite.js\n      pop: true\n\n  support:\n    - match: \\bdebugger\\b\n      scope: keyword.other.js\n      pop: true\n    - match: |-\n        (?x)\n        \\b(\n          ELEMENT_NODE|ATTRIBUTE_NODE|TEXT_NODE|CDATA_SECTION_NODE|ENTITY_REFERENCE_NODE|ENTITY_NODE|PROCESSING_INSTRUCTION_NODE|COMMENT_NODE|\n          DOCUMENT_NODE|DOCUMENT_TYPE_NODE|DOCUMENT_FRAGMENT_NODE|NOTATION_NODE|INDEX_SIZE_ERR|DOMSTRING_SIZE_ERR|HIERARCHY_REQUEST_ERR|\n          WRONG_DOCUMENT_ERR|INVALID_CHARACTER_ERR|NO_DATA_ALLOWED_ERR|NO_MODIFICATION_ALLOWED_ERR|NOT_FOUND_ERR|NOT_SUPPORTED_ERR|INUSE_ATTRIBUTE_ERR\n        )\\b\n      scope: support.constant.dom.js\n      pop: true\n    - match: \\b(assert|buffer|child_process|cluster|constants|crypto|dgram|dns|domain|events|fs|http|https|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|timers|tls|tty|url|util|vm|zlib)\\b\n      scope: support.module.node.js\n      pop: true\n    - match: \\b(process)(?:(\\.)(arch|argv|config|connected|env|execArgv|execPath|exitCode|mainModule|pid|platform|release|stderr|stdin|stdout|title|version|versions))?\\b\n      captures:\n        1: support.type.object.process.js\n        2: punctuation.accessor.js\n        3: support.type.object.process.js\n      pop: true\n    - match: \\b(exports|module(?:(\\.)(exports|id|filename|loaded|parent|children))?)\\b\n      captures:\n        1: support.type.object.module.js\n        2: punctuation.accessor.js\n        3: support.type.object.module.js\n      pop: true\n    - match: \\b(global|GLOBAL|root|__dirname|__filename)\\b\n      scope: support.type.object.node.js\n      pop: true\n\n  object-property:\n    - match: \\b__proto__\\b\n      scope: variable.language.proto.js\n      pop: true\n    - match: \\bconstructor\\b\n      scope: variable.language.constructor.js\n      pop: true\n    - match: \\bprototype\\b\n      scope: variable.language.prototype.js\n      pop: true\n    - match: '{{dollar_only_identifier}}'\n      scope: meta.property.object.dollar.only.js punctuation.dollar.js\n      pop: true\n    - match: '{{dollar_identifier}}'\n      scope: meta.property.object.dollar.js\n      captures:\n        1: punctuation.dollar.js\n      pop: true\n    - match: '{{identifier}}'\n      scope: meta.property.object.js\n      pop: true\n    - match: \\b(s(hape|ystemId|c(heme|ope|rolling)|ta(ndby|rt)|ize|ummary|pecified|e(ctionRowIndex|lected(Index)?)|rc)|h(space|t(tpEquiv|mlFor)|e(ight|aders)|ref(lang)?)|n(o(Resize|tation(s|Name)|Shade|Href|de(Name|Type|Value)|Wrap)|extSibling|ame)|c(h(ildNodes|Off|ecked|arset)?|ite|o(ntent|o(kie|rds)|de(Base|Type)?|l(s|Span|or)|mpact)|ell(s|Spacing|Padding)|l(ear|assName)|aption)|t(ype|Bodies|itle|Head|ext|a(rget|gName)|Foot)|i(sMap|ndex|d|m(plementation|ages))|o(ptions|wnerDocument|bject)|d(i(sabled|r)|o(c(type|umentElement)|main)|e(clare|f(er|ault(Selected|Checked|Value)))|at(eTime|a))|useMap|p(ublicId|arentNode|r(o(file|mpt)|eviousSibling))|e(n(ctype|tities)|vent|lements)|v(space|ersion|alue(Type)?|Link|Align)|URL|f(irstChild|orm(s)?|ace|rame(Border)?)|width|l(ink(s)?|o(ngDesc|wSrc)|a(stChild|ng|bel))|a(nchors|c(ce(ssKey|pt(Charset)?)|tion)|ttributes|pplets|l(t|ign)|r(chive|eas)|xis|Link|bbr)|r(ow(s|Span|Index)|ules|e(v|ferrer|l|adOnly))|m(ultiple|e(thod|dia)|a(rgin(Height|Width)|xLength))|b(o(dy|rder)|ackground|gColor))\\b\n      scope: support.constant.dom.js\n      pop: true\n    - match: '(?=.|\\n)'\n      pop: true"
  },
  {
    "path": "assets/syntax/large/xml.sublime-syntax",
    "content": "%YAML 1.2\n---\nname: XML\nfile_extensions:\n  - xml\n  - xsd\n  - xslt\n  - tld\n  - dtml\n  - rss\n  - opml\n  - svg\nfirst_line_match: |-\n    (?x)\n    ^(?:\n        <\\?xml\\s\n     |  \\s*<([\\w-]+):Envelope\\s+xmlns:\\1\\s*=\\s*\"http://schemas.xmlsoap.org/soap/envelope/\"\\s*>\n     )\nscope: text.xml\nvariables:\n  # This is the full XML Name production, but should not be used where namespaces\n  # are possible. Those locations should use a qualified_name.\n  name: '[[:alpha:]:_][[:alnum:]:_.-]*'\n  # This is the form that allows a namespace prefix (ns:) followed by a local\n  # name. The captures are:\n  #  1: namespace prefix name\n  #  2: namespace prefix colon\n  #  3: local tag name\n  qualified_name: '(?:([[:alpha:]_][[:alnum:]_.-]*)(:))?([[:alpha:]_][[:alnum:]_.-]*)'\n\ncontexts:\n  main:\n    - match: '(<\\?)(xml)(?=\\s)'\n      captures:\n        1: punctuation.definition.tag.begin.xml\n        2: entity.name.tag.xml\n      push:\n        - meta_scope: meta.tag.preprocessor.xml\n        - match: \\?>\n          scope: punctuation.definition.tag.end.xml\n          pop: true\n        - match: '\\s+{{qualified_name}}(=)?'\n          captures:\n            1: entity.other.attribute-name.namespace.xml\n            2: entity.other.attribute-name.xml punctuation.separator.namespace.xml\n            3: entity.other.attribute-name.localname.xml\n            4: punctuation.separator.key-value.xml\n        - include: double-quoted-string\n        - include: single-quoted-string\n    - match: '(<!)(DOCTYPE)(?:\\s+({{name}}))?'\n      captures:\n        1: punctuation.definition.tag.begin.xml\n        2: keyword.doctype.xml\n        3: variable.documentroot.xml\n      push:\n        - meta_scope: meta.tag.sgml.doctype.xml\n        - match: \\s*(>)\n          captures:\n            1: punctuation.definition.tag.end.xml\n          pop: true\n        - include: internal-subset\n    - include: comment\n    - match: '(</?){{qualified_name}}([^/>\\s]*)'\n      captures:\n        1: punctuation.definition.tag.begin.xml\n        2: entity.name.tag.namespace.xml\n        3: entity.name.tag.xml punctuation.separator.namespace.xml\n        4: entity.name.tag.localname.xml\n        5: invalid.illegal.bad-tag-name.xml\n      push:\n        - meta_scope: meta.tag.xml\n        - match: /?>\n          scope: punctuation.definition.tag.end.xml\n          pop: true\n        - include: tag-stuff\n    - match: '(</?)([[:digit:].-][[:alnum:]:_.-]*)'\n      captures:\n        1: punctuation.definition.tag.begin.xml\n        2: invalid.illegal.bad-tag-name.xml\n      push:\n        - meta_scope: meta.tag.xml\n        - match: /?>\n          scope: punctuation.definition.tag.end.xml\n          pop: true\n        - include: tag-stuff\n    - match: '(<\\?)(xml-stylesheet|xml-model)(?=\\s|\\?>)'\n      captures:\n        1: punctuation.definition.tag.begin.xml\n        2: entity.name.tag.xml\n      push:\n        - meta_scope: meta.tag.preprocessor.xml\n        - match: \\?>\n          scope: punctuation.definition.tag.end.xml\n          pop: true\n        - include: tag-stuff\n    - match: '(<\\?)((?![xX][mM][lL]){{qualified_name}})(?=\\s|\\?>)'\n      captures:\n        1: punctuation.definition.tag.begin.xml\n        2: entity.name.tag.xml\n      push:\n        - meta_scope: meta.tag.preprocessor.xml\n        - match: \\?>\n          scope: punctuation.definition.tag.end.xml\n          pop: true\n    - include: entity\n    - match: '<!\\[CDATA\\['\n      scope: punctuation.definition.string.begin.xml\n      push:\n        - meta_scope: string.unquoted.cdata.xml\n        - match: ']]>'\n          scope: punctuation.definition.string.end.xml\n          pop: true\n    - match: ']]>'\n      scope: invalid.illegal.missing-entity.xml\n    - include: should-be-entity\n  should-be-entity:\n    - match: '&'\n      scope: invalid.illegal.bad-ampersand.xml\n    - match: '<'\n      scope: invalid.illegal.missing-entity.xml\n  double-quoted-string:\n    - match: '\"'\n      scope: punctuation.definition.string.begin.xml\n      push:\n        - meta_scope: string.quoted.double.xml\n        - match: '\"'\n          scope: punctuation.definition.string.end.xml\n          pop: true\n        - include: entity\n        - include: should-be-entity\n  entity:\n    - match: '(&)(?:{{name}}|#[0-9]+|#x\\h+)(;)'\n      scope: constant.character.entity.xml\n      captures:\n        1: punctuation.definition.constant.xml\n        2: punctuation.definition.constant.xml\n  comment:\n    - match: '<!--'\n      scope: punctuation.definition.comment.begin.xml\n      push:\n        - meta_scope: comment.block.xml\n        - match: '-->'\n          scope: punctuation.definition.comment.end.xml\n          pop: true\n        - match: '-{2,}'\n          scope: invalid.illegal.double-hyphen-within-comment.xml\n  internal-subset:\n    - match: \\[\n      scope: punctuation.definition.constant.xml\n      push:\n        - meta_scope: meta.internalsubset.xml\n        - match: \\]\n          pop: true\n        - include: comment\n        - include: entity-decl\n        - include: element-decl\n        - include: attlist-decl\n        - include: notation-decl\n        - include: parameter-entity\n  entity-decl:\n    - match: '(<!)(ENTITY)\\s+(%\\s+)?({{name}})(\\s+(?:SYSTEM|PUBLIC)\\s+)?'\n      captures:\n        1: punctuation.definition.tag.begin.xml\n        2: keyword.entity.xml\n        3: punctuation.definition.entity.xml\n        4: variable.entity.xml\n        5: keyword.entitytype.xml\n      push:\n        - match: '>'\n          scope: punctuation.definition.tag.end.xml\n          pop: true\n        - include: double-quoted-string\n        - include: single-quoted-string\n  element-decl:\n    - match: '(<!)(ELEMENT)\\s+({{name}})\\s+'\n      captures:\n        1: punctuation.definition.tag.begin.xml\n        2: keyword.element.xml\n        3: variable.element.xml\n      push:\n        - match: '>'\n          scope: punctuation.definition.tag.end.xml\n          pop: true\n        - match: '\\b(EMPTY|ANY)\\b'\n          scope: constant.other.xml\n        - include: element-parens\n  element-parens:\n    - match: \\(\n      scope: punctuation.definition.group.xml\n      push:\n        - match: (\\))([*?+])?\n          captures:\n            1: punctuation.definition.group.xml\n            2: keyword.operator.xml\n          pop: true\n        - match: '#PCDATA'\n          scope: constant.other.xml\n        - match: '[*?+]'\n          scope: keyword.operator.xml\n        - match: '[,|]'\n          scope: punctuation.separator.xml\n        - include: element-parens\n  attlist-decl:\n    - match: '(<!)(ATTLIST)\\s+({{name}})\\s+({{name}})'\n      captures:\n        1: punctuation.definition.tag.begin.xml\n        2: keyword.attlist.xml\n        3: variable.element.xml\n        4: variable.attribute-name.xml\n      push:\n        - match: '>'\n          scope: punctuation.definition.tag.end.xml\n          pop: true\n        - include: double-quoted-string\n        - include: single-quoted-string\n  notation-decl:\n    - match: '(<!)(NOTATION)\\s+({{name}})'\n      captures:\n        1: punctuation.definition.tag.begin.xml\n        2: keyword.notation.xml\n        3: variable.notation.xml\n      push:\n        - match: '>'\n          scope: punctuation.definition.tag.end.xml\n          pop: true\n        - include: double-quoted-string\n        - include: single-quoted-string\n  parameter-entity:\n    - match: '(%){{name}}(;)'\n      scope: constant.character.parameter-entity.xml\n      captures:\n        1: punctuation.definition.constant.xml\n        2: punctuation.definition.constant.xml\n  single-quoted-string:\n    - match: \"'\"\n      scope: punctuation.definition.string.begin.xml\n      push:\n        - meta_scope: string.quoted.single.xml\n        - match: \"'\"\n          scope: punctuation.definition.string.end.xml\n          pop: true\n        - include: entity\n        - include: should-be-entity\n  tag-stuff:\n    - match: '(?:\\s+|^){{qualified_name}}\\s*(=)'\n      captures:\n        1: entity.other.attribute-name.namespace.xml\n        2: entity.other.attribute-name.xml punctuation.separator.namespace.xml\n        3: entity.other.attribute-name.localname.xml\n        4: punctuation.separator.key-value.xml\n    - match: '(?:\\s+|^)([[:alnum:]:_.-]+)\\s*(=)'\n      captures:\n        1: invalid.illegal.bad-attribute-name.xml\n        2: punctuation.separator.key-value.xml\n    - include: double-quoted-string\n    - include: single-quoted-string"
  },
  {
    "path": "assets/themes/ansi.tmTheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <!--\n        The colors in this theme are encoded as #RRGGBBAA where RR is an ANSI\n        palette number from 00 to 0f, and AA is the special value 00 to indicate\n        that this encoding is being used.\n        -->\n        <key>name</key>\n        <string>ANSI Dark</string>\n        <key>colorSpaceName</key>\n        <string>sRGB</string>\n        <key>settings</key>\n        <array>\n            <dict>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#07000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Integers</string>\n                <key>scope</key>\n                <string>constant.numeric</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#04000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Constants</string>\n                <key>scope</key>\n                <string>constant</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#04000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Strings</string>\n                <key>scope</key>\n                <string>string.quoted, punctuation.definition.string.begin, punctuation.definition.string.end</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#03000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Doctype</string>\n                <key>scope</key>\n                <string>meta.tag.sgml, entity.name.tag.doctype</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#06000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Tags</string>\n                <key>scope</key>\n                <string>entity.name.tag</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#0C000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Attributes</string>\n                <key>scope</key>\n                <string>entity.other.attribute-name</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#06000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Header keys</string>\n                <key>scope</key>\n                <string>source.http http.requestheaders support.variable.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#06000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Header values</string>\n                <key>scope</key>\n                <string>source.http http.requestheaders string.other.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#07000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>HTTP version</string>\n                <key>scope</key>\n                <string>constant.numeric.http, keyword.other.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#04000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>HTTP reason phrase</string>\n                <key>scope</key>\n                <string>keyword.reason.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#06000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>HTTP method</string>\n                <key>scope</key>\n                <string>keyword.control.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#02000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>HTTP URL</string>\n                <key>scope</key>\n                <string>const.language.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>fontStyle</key>\n                    <string>underline</string>\n                    <key>foreground</key>\n                    <string>#06000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>JSON keys</string>\n                <key>scope</key>\n                <string>keyword.other.name.jsonkv</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#0C000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Error</string>\n                <key>scope</key>\n                <string>error</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#01000000</string>\n                </dict>\n            </dict>\n        </array>\n    </dict>\n</plist>\n"
  },
  {
    "path": "assets/themes/fruity.tmTheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <!--\n        The colors in this theme are encoded as #RRGGBBAA where RR is an ANSI\n        palette number from 00 to 0f, and AA is the special value 00 to indicate\n        that this encoding is being used.\n        -->\n        <key>name</key>\n        <string>Fruity</string>\n        <key>colorSpaceName</key>\n        <string>sRGB</string>\n        <key>settings</key>\n        <array>\n            <dict>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#0F000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Integers</string>\n                <key>scope</key>\n                <string>constant.numeric</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#21000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Constants</string>\n                <key>scope</key>\n                <string>constant</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#CA000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Strings</string>\n                <key>scope</key>\n                <string>string.quoted, punctuation.definition.string.begin, punctuation.definition.string.end</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#20000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Comments</string>\n                <key>scope</key>\n                <string>comment</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#1C000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Doctype</string>\n                <key>scope</key>\n                <string>meta.tag.sgml, entity.name.tag.doctype</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#40000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Tags</string>\n                <key>scope</key>\n                <string>entity.name.tag</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#CA000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Attributes</string>\n                <key>scope</key>\n                <string>entity.other.attribute-name</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#C6000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Header keys</string>\n                <key>scope</key>\n                <string>source.http http.requestheaders support.variable.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#C6000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Header values</string>\n                <key>scope</key>\n                <string>source.http http.requestheaders string.other.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#20000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>HTTP</string>\n                <key>scope</key>\n                <string>keyword.other.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#CA000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>HTTP version</string>\n                <key>scope</key>\n                <string>constant.numeric.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#21000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>HTTP method</string>\n                <key>scope</key>\n                <string>keyword.control.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#C6000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>JSON keys</string>\n                <key>scope</key>\n                <string>keyword.other.name.jsonkv</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#CA000000</string>\n                </dict>\n            </dict>\n            <!-- FIXME: does this color fit the theme? -->\n            <dict>\n                <key>name</key>\n                <string>Error</string>\n                <key>scope</key>\n                <string>error</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#01000000</string>\n                </dict>\n            </dict>\n        </array>\n    </dict>\n</plist>\n"
  },
  {
    "path": "assets/themes/monokai.tmTheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <!--\n        The colors in this theme are encoded as #RRGGBBAA where RR is an ANSI\n        palette number from 00 to 0f, and AA is the special value 00 to indicate\n        that this encoding is being used.\n        -->\n        <key>name</key>\n        <string>ANSI Dark</string>\n        <key>colorSpaceName</key>\n        <string>sRGB</string>\n        <key>settings</key>\n        <array>\n            <dict>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#0F000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Integers</string>\n                <key>scope</key>\n                <string>constant.numeric</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#8D000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Constants</string>\n                <key>scope</key>\n                <string>constant</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#51000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Strings</string>\n                <key>scope</key>\n                <string>string.quoted, punctuation.definition.string.begin, punctuation.definition.string.end</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#BA000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Comments</string>\n                <key>scope</key>\n                <string>comment</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#F2000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Doctype</string>\n                <key>scope</key>\n                <string>meta.tag.sgml, entity.name.tag.doctype</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#F2000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Tags</string>\n                <key>scope</key>\n                <string>entity.name.tag</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#C5000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Attributes</string>\n                <key>scope</key>\n                <string>entity.other.attribute-name</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#94000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Header keys</string>\n                <key>scope</key>\n                <string>source.http http.requestheaders support.variable.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#94000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Header values</string>\n                <key>scope</key>\n                <string>source.http http.requestheaders string.other.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#BA000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>HTTP</string>\n                <key>scope</key>\n                <string>keyword.other.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#51000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>HTTP separator</string>\n                <key>scope</key>\n                <string>punctuation.separator.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#C5000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>HTTP version</string>\n                <key>scope</key>\n                <string>constant.numeric.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#8D000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>HTTP reason phrase</string>\n                <key>scope</key>\n                <string>keyword.reason.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#94000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>HTTP method</string>\n                <key>scope</key>\n                <string>keyword.control.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#94000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>JSON keys</string>\n                <key>scope</key>\n                <string>keyword.other.name.jsonkv</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#C5000000</string>\n                </dict>\n            </dict>\n            <!-- FIXME: does this color fit the theme? -->\n            <dict>\n                <key>name</key>\n                <string>Error</string>\n                <key>scope</key>\n                <string>error</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#01000000</string>\n                </dict>\n            </dict>\n        </array>\n    </dict>\n</plist>\n"
  },
  {
    "path": "assets/themes/solarized.tmTheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <!--\n        The colors in this theme are encoded as #RRGGBBAA where RR is an ANSI\n        palette number from 00 to 0f, and AA is the special value 00 to indicate\n        that this encoding is being used.\n        -->\n        <key>name</key>\n        <string>Solarized</string>\n        <key>colorSpaceName</key>\n        <string>sRGB</string>\n        <key>settings</key>\n        <array>\n            <dict>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#F5000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Integers</string>\n                <key>scope</key>\n                <string>constant.numeric</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#25000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Constants</string>\n                <key>scope</key>\n                <string>constant</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#A6000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Strings</string>\n                <key>scope</key>\n                <string>string.quoted, punctuation.definition.string.begin, punctuation.definition.string.end</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#25000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Comments</string>\n                <key>scope</key>\n                <string>comment</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#EF000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Doctype</string>\n                <key>scope</key>\n                <string>meta.tag.sgml, entity.name.tag.doctype</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#40000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Tags</string>\n                <key>scope</key>\n                <string>entity.name.tag</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#21000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Attributes</string>\n                <key>scope</key>\n                <string>entity.other.attribute-name</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#F5000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Header keys</string>\n                <key>scope</key>\n                <string>source.http http.requestheaders support.variable.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#F5000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>Header values</string>\n                <key>scope</key>\n                <string>source.http http.requestheaders string.other.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#25000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>HTTP</string>\n                <key>scope</key>\n                <string>keyword.other.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#21000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>HTTP reason phrase</string>\n                <key>scope</key>\n                <string>keyword.reason.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#88000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>HTTP method</string>\n                <key>scope</key>\n                <string>keyword.control.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#21000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>HTTP URL</string>\n                <key>scope</key>\n                <string>const.language.http</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#F5000000</string>\n                </dict>\n            </dict>\n            <dict>\n                <key>name</key>\n                <string>JSON keys</string>\n                <key>scope</key>\n                <string>keyword.other.name.jsonkv</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#21000000</string>\n                </dict>\n            </dict>\n            <!-- FIXME: does this color fit the theme? -->\n            <dict>\n                <key>name</key>\n                <string>Error</string>\n                <key>scope</key>\n                <string>error</string>\n                <key>settings</key>\n                <dict>\n                    <key>foreground</key>\n                    <string>#01000000</string>\n                </dict>\n            </dict>\n        </array>\n    </dict>\n</plist>\n"
  },
  {
    "path": "build.rs",
    "content": "use std::env;\nuse std::fs::read_dir;\nuse std::path::Path;\n\nuse syntect::dumps::dump_to_file;\nuse syntect::highlighting::ThemeSet;\nuse syntect::parsing::SyntaxSetBuilder;\n\nfn build_syntax(dir: &str, out: &str) {\n    let out_dir = env::var_os(\"OUT_DIR\").unwrap();\n    let mut builder = SyntaxSetBuilder::new();\n    builder.add_from_folder(dir, true).unwrap();\n    let ss = builder.build();\n    dump_to_file(&ss, Path::new(&out_dir).join(out)).unwrap();\n}\n\nfn feature_status(feature: &str) -> String {\n    if env::var_os(format!(\n        \"CARGO_FEATURE_{}\",\n        feature.to_uppercase().replace('-', \"_\")\n    ))\n    .is_some()\n    {\n        format!(\"+{feature}\")\n    } else {\n        format!(\"-{feature}\")\n    }\n}\n\nfn features() -> String {\n    format!(\n        \"{} {}\",\n        &feature_status(\"native-tls\"),\n        &feature_status(\"rustls\")\n    )\n}\n\nfn main() {\n    for dir in [\n        \"assets/syntax\",\n        \"assets/syntax/basic\",\n        \"assets/syntax/large\",\n        \"assets/themes\",\n    ] {\n        println!(\"cargo:rerun-if-changed={dir}\");\n        for entry in read_dir(dir).unwrap() {\n            let path = entry.unwrap().path();\n            let path = path.to_str().unwrap();\n            if path.ends_with(\".sublime-syntax\") || path.ends_with(\".tmTheme\") {\n                println!(\"cargo:rerun-if-changed={path}\");\n            }\n        }\n    }\n\n    build_syntax(\"assets/syntax/basic\", \"basic.packdump\");\n    build_syntax(\"assets/syntax/large\", \"large.packdump\");\n\n    let out_dir = env::var_os(\"OUT_DIR\").unwrap();\n    let ts = ThemeSet::load_from_folder(\"assets/themes\").unwrap();\n    dump_to_file(&ts, Path::new(&out_dir).join(\"themepack.themedump\")).unwrap();\n\n    println!(\"cargo:rustc-env=XH_FEATURES={}\", features());\n}\n"
  },
  {
    "path": "completions/_xh",
    "content": "#compdef xh\n\nautoload -U is-at-least\n\n_xh() {\n    typeset -A opt_args\n    typeset -a _arguments_options\n    local ret=1\n\n    if is-at-least 5.2; then\n        _arguments_options=(-s -S -C)\n    else\n        _arguments_options=(-s -C)\n    fi\n\n    local context curcontext=\"$curcontext\" state line\n    _arguments \"${_arguments_options[@]}\" : \\\n'--raw=[Pass raw request data without extra processing]:RAW:_default' \\\n'--pretty=[Controls output processing]:STYLE:((all\\:\"(default) Enable both coloring and formatting\"\ncolors\\:\"Apply syntax highlighting to output\"\nformat\\:\"Pretty-print json and sort headers\"\nnone\\:\"Disable both coloring and formatting\"))' \\\n'*--format-options=[Set output formatting options]:FORMAT_OPTIONS:_default' \\\n'-s+[Output coloring style]:THEME:(auto solarized monokai fruity)' \\\n'--style=[Output coloring style]:THEME:(auto solarized monokai fruity)' \\\n'--response-charset=[Override the response encoding for terminal display purposes]:ENCODING:_default' \\\n'--response-mime=[Override the response mime type for coloring and formatting for the terminal]:MIME_TYPE:_default' \\\n'-p+[String specifying what the output should contain]:FORMAT:_default' \\\n'--print=[String specifying what the output should contain]:FORMAT:_default' \\\n'-P+[The same as --print but applies only to intermediary requests/responses]:FORMAT:_default' \\\n'--history-print=[The same as --print but applies only to intermediary requests/responses]:FORMAT:_default' \\\n'-o+[Save output to FILE instead of stdout]:FILE:_files' \\\n'--output=[Save output to FILE instead of stdout]:FILE:_files' \\\n'--session=[Create, or reuse and update a session]:FILE:_default' \\\n'(--session)--session-read-only=[Create or read a session without updating it from the request/response exchange]:FILE:_default' \\\n'-A+[Specify the auth mechanism]:AUTH_TYPE:(basic bearer digest)' \\\n'--auth-type=[Specify the auth mechanism]:AUTH_TYPE:(basic bearer digest)' \\\n'-a+[Authenticate as USER with PASS (-A basic|digest) or with TOKEN (-A bearer)]:USER[:PASS] | TOKEN:_default' \\\n'--auth=[Authenticate as USER with PASS (-A basic|digest) or with TOKEN (-A bearer)]:USER[:PASS] | TOKEN:_default' \\\n'--bearer=[Authenticate with a bearer token]:TOKEN:_default' \\\n'--max-redirects=[Number of redirects to follow. Only respected if --follow is used]:NUM:_default' \\\n'--timeout=[Connection timeout of the request]:SEC:_default' \\\n'*--proxy=[Use a proxy for a protocol. For example\\: --proxy https\\:http\\://proxy.host\\:8080]:PROTOCOL:URL:_default' \\\n'--verify=[If \"no\", skip SSL verification. If a file path, use it as a CA bundle]:VERIFY:_default' \\\n'--cert=[Use a client side certificate for SSL]:FILE:_files' \\\n'--cert-key=[A private key file to use with --cert]:FILE:_files' \\\n'--ssl=[Force a particular TLS version]:VERSION:(auto tls1 tls1.1 tls1.2 tls1.3)' \\\n'--default-scheme=[The default scheme to use if not specified in the URL]:SCHEME:_default' \\\n'--http-version=[HTTP version to use]:VERSION:(1.0 1.1 2 2-prior-knowledge 3-prior-knowledge)' \\\n'*--resolve=[Override DNS resolution for specific domain to a custom IP]:HOST:ADDRESS:_default' \\\n'--interface=[Bind to a network interface or local IP address]:NAME:_default' \\\n'--unix-socket=[Connect using a Unix domain socket]:FILE:_files' \\\n'()--generate=[Generate shell completions or man pages]:KIND:(complete-bash complete-elvish complete-fish complete-nushell complete-powershell complete-zsh man)' \\\n'-j[(default) Serialize data items from the command line as a JSON object]' \\\n'--json[(default) Serialize data items from the command line as a JSON object]' \\\n'-f[Serialize data items from the command line as form fields]' \\\n'--form[Serialize data items from the command line as form fields]' \\\n'(--raw -x --compress)--multipart[Like --form, but force a multipart/form-data request even without files]' \\\n'-h[Print only the response headers. Shortcut for --print=h]' \\\n'--headers[Print only the response headers. Shortcut for --print=h]' \\\n'-b[Print only the response body. Shortcut for --print=b]' \\\n'--body[Print only the response body. Shortcut for --print=b]' \\\n'-m[Print only the response metadata. Shortcut for --print=m]' \\\n'--meta[Print only the response metadata. Shortcut for --print=m]' \\\n'*-v[Print the whole request as well as the response]' \\\n'*--verbose[Print the whole request as well as the response]' \\\n'--debug[Print full error stack traces and debug log messages]' \\\n'--all[Show any intermediary requests/responses while following redirects with --follow]' \\\n'*-q[Do not print to stdout or stderr]' \\\n'*--quiet[Do not print to stdout or stderr]' \\\n'-S[Always stream the response body]' \\\n'--stream[Always stream the response body]' \\\n'*-x[Content compressed (encoded) with Deflate algorithm]' \\\n'*--compress[Content compressed (encoded) with Deflate algorithm]' \\\n'-d[Download the body to a file instead of printing it]' \\\n'--download[Download the body to a file instead of printing it]' \\\n'-c[Resume an interrupted download. Requires --download and --output]' \\\n'--continue[Resume an interrupted download. Requires --download and --output]' \\\n'--ignore-netrc[Do not use credentials from .netrc]' \\\n'--offline[Construct HTTP requests without sending them anywhere]' \\\n'--check-status[(default) Exit with an error status code if the server replies with an error]' \\\n'-F[Do follow redirects]' \\\n'--follow[Do follow redirects]' \\\n'--native-tls[Use the system TLS library instead of rustls (if enabled at compile time)]' \\\n'--https[Make HTTPS requests if not specified in the URL]' \\\n'-4[Resolve hostname to ipv4 addresses only]' \\\n'--ipv4[Resolve hostname to ipv4 addresses only]' \\\n'-6[Resolve hostname to ipv6 addresses only]' \\\n'--ipv6[Resolve hostname to ipv6 addresses only]' \\\n'-I[Do not attempt to read stdin]' \\\n'--ignore-stdin[Do not attempt to read stdin]' \\\n'--curl[Print a translation to a curl command]' \\\n'--curl-long[Use the long versions of curl'\\''s flags]' \\\n'--help[Print help]' \\\n'--no-json[]' \\\n'--no-form[]' \\\n'--no-multipart[]' \\\n'--no-raw[]' \\\n'--no-pretty[]' \\\n'--no-format-options[]' \\\n'--no-style[]' \\\n'--no-response-charset[]' \\\n'--no-response-mime[]' \\\n'--no-print[]' \\\n'--no-headers[]' \\\n'--no-body[]' \\\n'--no-meta[]' \\\n'--no-verbose[]' \\\n'--no-debug[]' \\\n'--no-all[]' \\\n'--no-history-print[]' \\\n'--no-quiet[]' \\\n'--no-stream[]' \\\n'--no-compress[]' \\\n'--no-output[]' \\\n'--no-download[]' \\\n'--no-continue[]' \\\n'--no-session[]' \\\n'--no-session-read-only[]' \\\n'--no-auth-type[]' \\\n'--no-auth[]' \\\n'--no-bearer[]' \\\n'--no-ignore-netrc[]' \\\n'--no-offline[]' \\\n'--no-check-status[]' \\\n'--no-follow[]' \\\n'--no-max-redirects[]' \\\n'--no-timeout[]' \\\n'--no-proxy[]' \\\n'--no-verify[]' \\\n'--no-cert[]' \\\n'--no-cert-key[]' \\\n'--no-ssl[]' \\\n'--no-native-tls[]' \\\n'--no-default-scheme[]' \\\n'--no-https[]' \\\n'--no-http-version[]' \\\n'--no-resolve[]' \\\n'--no-interface[]' \\\n'--no-ipv4[]' \\\n'--no-ipv6[]' \\\n'--no-unix-socket[]' \\\n'--no-ignore-stdin[]' \\\n'--no-curl[]' \\\n'--no-curl-long[]' \\\n'--no-generate[]' \\\n'--no-help[]' \\\n'-V[Print version]' \\\n'--version[Print version]' \\\n':raw_method_or_url -- The request URL, preceded by an optional HTTP method:_default' \\\n'*::raw_rest_args -- Optional key-value pairs to be included in the request.:_default' \\\n&& ret=0\n}\n\n(( $+functions[_xh_commands] )) ||\n_xh_commands() {\n    local commands; commands=()\n    _describe -t commands 'xh commands' commands \"$@\"\n}\n\nif [ \"$funcstack[1]\" = \"_xh\" ]; then\n    _xh \"$@\"\nelse\n    compdef _xh xh\nfi\n"
  },
  {
    "path": "completions/_xh.ps1",
    "content": "\nusing namespace System.Management.Automation\nusing namespace System.Management.Automation.Language\n\nRegister-ArgumentCompleter -Native -CommandName 'xh' -ScriptBlock {\n    param($wordToComplete, $commandAst, $cursorPosition)\n\n    $commandElements = $commandAst.CommandElements\n    $command = @(\n        'xh'\n        for ($i = 1; $i -lt $commandElements.Count; $i++) {\n            $element = $commandElements[$i]\n            if ($element -isnot [StringConstantExpressionAst] -or\n                $element.StringConstantType -ne [StringConstantType]::BareWord -or\n                $element.Value.StartsWith('-') -or\n                $element.Value -eq $wordToComplete) {\n                break\n        }\n        $element.Value\n    }) -join ';'\n\n    $completions = @(switch ($command) {\n        'xh' {\n            [CompletionResult]::new('--raw', '--raw', [CompletionResultType]::ParameterName, 'Pass raw request data without extra processing')\n            [CompletionResult]::new('--pretty', '--pretty', [CompletionResultType]::ParameterName, 'Controls output processing')\n            [CompletionResult]::new('--format-options', '--format-options', [CompletionResultType]::ParameterName, 'Set output formatting options')\n            [CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'Output coloring style')\n            [CompletionResult]::new('--style', '--style', [CompletionResultType]::ParameterName, 'Output coloring style')\n            [CompletionResult]::new('--response-charset', '--response-charset', [CompletionResultType]::ParameterName, 'Override the response encoding for terminal display purposes')\n            [CompletionResult]::new('--response-mime', '--response-mime', [CompletionResultType]::ParameterName, 'Override the response mime type for coloring and formatting for the terminal')\n            [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'String specifying what the output should contain')\n            [CompletionResult]::new('--print', '--print', [CompletionResultType]::ParameterName, 'String specifying what the output should contain')\n            [CompletionResult]::new('-P', '-P ', [CompletionResultType]::ParameterName, 'The same as --print but applies only to intermediary requests/responses')\n            [CompletionResult]::new('--history-print', '--history-print', [CompletionResultType]::ParameterName, 'The same as --print but applies only to intermediary requests/responses')\n            [CompletionResult]::new('-o', '-o', [CompletionResultType]::ParameterName, 'Save output to FILE instead of stdout')\n            [CompletionResult]::new('--output', '--output', [CompletionResultType]::ParameterName, 'Save output to FILE instead of stdout')\n            [CompletionResult]::new('--session', '--session', [CompletionResultType]::ParameterName, 'Create, or reuse and update a session')\n            [CompletionResult]::new('--session-read-only', '--session-read-only', [CompletionResultType]::ParameterName, 'Create or read a session without updating it from the request/response exchange')\n            [CompletionResult]::new('-A', '-A ', [CompletionResultType]::ParameterName, 'Specify the auth mechanism')\n            [CompletionResult]::new('--auth-type', '--auth-type', [CompletionResultType]::ParameterName, 'Specify the auth mechanism')\n            [CompletionResult]::new('-a', '-a', [CompletionResultType]::ParameterName, 'Authenticate as USER with PASS (-A basic|digest) or with TOKEN (-A bearer)')\n            [CompletionResult]::new('--auth', '--auth', [CompletionResultType]::ParameterName, 'Authenticate as USER with PASS (-A basic|digest) or with TOKEN (-A bearer)')\n            [CompletionResult]::new('--bearer', '--bearer', [CompletionResultType]::ParameterName, 'Authenticate with a bearer token')\n            [CompletionResult]::new('--max-redirects', '--max-redirects', [CompletionResultType]::ParameterName, 'Number of redirects to follow. Only respected if --follow is used')\n            [CompletionResult]::new('--timeout', '--timeout', [CompletionResultType]::ParameterName, 'Connection timeout of the request')\n            [CompletionResult]::new('--proxy', '--proxy', [CompletionResultType]::ParameterName, 'Use a proxy for a protocol. For example: --proxy https:http://proxy.host:8080')\n            [CompletionResult]::new('--verify', '--verify', [CompletionResultType]::ParameterName, 'If \"no\", skip SSL verification. If a file path, use it as a CA bundle')\n            [CompletionResult]::new('--cert', '--cert', [CompletionResultType]::ParameterName, 'Use a client side certificate for SSL')\n            [CompletionResult]::new('--cert-key', '--cert-key', [CompletionResultType]::ParameterName, 'A private key file to use with --cert')\n            [CompletionResult]::new('--ssl', '--ssl', [CompletionResultType]::ParameterName, 'Force a particular TLS version')\n            [CompletionResult]::new('--default-scheme', '--default-scheme', [CompletionResultType]::ParameterName, 'The default scheme to use if not specified in the URL')\n            [CompletionResult]::new('--http-version', '--http-version', [CompletionResultType]::ParameterName, 'HTTP version to use')\n            [CompletionResult]::new('--resolve', '--resolve', [CompletionResultType]::ParameterName, 'Override DNS resolution for specific domain to a custom IP')\n            [CompletionResult]::new('--interface', '--interface', [CompletionResultType]::ParameterName, 'Bind to a network interface or local IP address')\n            [CompletionResult]::new('--unix-socket', '--unix-socket', [CompletionResultType]::ParameterName, 'Connect using a Unix domain socket')\n            [CompletionResult]::new('--generate', '--generate', [CompletionResultType]::ParameterName, 'Generate shell completions or man pages')\n            [CompletionResult]::new('-j', '-j', [CompletionResultType]::ParameterName, '(default) Serialize data items from the command line as a JSON object')\n            [CompletionResult]::new('--json', '--json', [CompletionResultType]::ParameterName, '(default) Serialize data items from the command line as a JSON object')\n            [CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'Serialize data items from the command line as form fields')\n            [CompletionResult]::new('--form', '--form', [CompletionResultType]::ParameterName, 'Serialize data items from the command line as form fields')\n            [CompletionResult]::new('--multipart', '--multipart', [CompletionResultType]::ParameterName, 'Like --form, but force a multipart/form-data request even without files')\n            [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print only the response headers. Shortcut for --print=h')\n            [CompletionResult]::new('--headers', '--headers', [CompletionResultType]::ParameterName, 'Print only the response headers. Shortcut for --print=h')\n            [CompletionResult]::new('-b', '-b', [CompletionResultType]::ParameterName, 'Print only the response body. Shortcut for --print=b')\n            [CompletionResult]::new('--body', '--body', [CompletionResultType]::ParameterName, 'Print only the response body. Shortcut for --print=b')\n            [CompletionResult]::new('-m', '-m', [CompletionResultType]::ParameterName, 'Print only the response metadata. Shortcut for --print=m')\n            [CompletionResult]::new('--meta', '--meta', [CompletionResultType]::ParameterName, 'Print only the response metadata. Shortcut for --print=m')\n            [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Print the whole request as well as the response')\n            [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Print the whole request as well as the response')\n            [CompletionResult]::new('--debug', '--debug', [CompletionResultType]::ParameterName, 'Print full error stack traces and debug log messages')\n            [CompletionResult]::new('--all', '--all', [CompletionResultType]::ParameterName, 'Show any intermediary requests/responses while following redirects with --follow')\n            [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Do not print to stdout or stderr')\n            [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Do not print to stdout or stderr')\n            [CompletionResult]::new('-S', '-S ', [CompletionResultType]::ParameterName, 'Always stream the response body')\n            [CompletionResult]::new('--stream', '--stream', [CompletionResultType]::ParameterName, 'Always stream the response body')\n            [CompletionResult]::new('-x', '-x', [CompletionResultType]::ParameterName, 'Content compressed (encoded) with Deflate algorithm')\n            [CompletionResult]::new('--compress', '--compress', [CompletionResultType]::ParameterName, 'Content compressed (encoded) with Deflate algorithm')\n            [CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'Download the body to a file instead of printing it')\n            [CompletionResult]::new('--download', '--download', [CompletionResultType]::ParameterName, 'Download the body to a file instead of printing it')\n            [CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'Resume an interrupted download. Requires --download and --output')\n            [CompletionResult]::new('--continue', '--continue', [CompletionResultType]::ParameterName, 'Resume an interrupted download. Requires --download and --output')\n            [CompletionResult]::new('--ignore-netrc', '--ignore-netrc', [CompletionResultType]::ParameterName, 'Do not use credentials from .netrc')\n            [CompletionResult]::new('--offline', '--offline', [CompletionResultType]::ParameterName, 'Construct HTTP requests without sending them anywhere')\n            [CompletionResult]::new('--check-status', '--check-status', [CompletionResultType]::ParameterName, '(default) Exit with an error status code if the server replies with an error')\n            [CompletionResult]::new('-F', '-F ', [CompletionResultType]::ParameterName, 'Do follow redirects')\n            [CompletionResult]::new('--follow', '--follow', [CompletionResultType]::ParameterName, 'Do follow redirects')\n            [CompletionResult]::new('--native-tls', '--native-tls', [CompletionResultType]::ParameterName, 'Use the system TLS library instead of rustls (if enabled at compile time)')\n            [CompletionResult]::new('--https', '--https', [CompletionResultType]::ParameterName, 'Make HTTPS requests if not specified in the URL')\n            [CompletionResult]::new('-4', '-4', [CompletionResultType]::ParameterName, 'Resolve hostname to ipv4 addresses only')\n            [CompletionResult]::new('--ipv4', '--ipv4', [CompletionResultType]::ParameterName, 'Resolve hostname to ipv4 addresses only')\n            [CompletionResult]::new('-6', '-6', [CompletionResultType]::ParameterName, 'Resolve hostname to ipv6 addresses only')\n            [CompletionResult]::new('--ipv6', '--ipv6', [CompletionResultType]::ParameterName, 'Resolve hostname to ipv6 addresses only')\n            [CompletionResult]::new('-I', '-I ', [CompletionResultType]::ParameterName, 'Do not attempt to read stdin')\n            [CompletionResult]::new('--ignore-stdin', '--ignore-stdin', [CompletionResultType]::ParameterName, 'Do not attempt to read stdin')\n            [CompletionResult]::new('--curl', '--curl', [CompletionResultType]::ParameterName, 'Print a translation to a curl command')\n            [CompletionResult]::new('--curl-long', '--curl-long', [CompletionResultType]::ParameterName, 'Use the long versions of curl''s flags')\n            [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help')\n            [CompletionResult]::new('--no-json', '--no-json', [CompletionResultType]::ParameterName, 'no-json')\n            [CompletionResult]::new('--no-form', '--no-form', [CompletionResultType]::ParameterName, 'no-form')\n            [CompletionResult]::new('--no-multipart', '--no-multipart', [CompletionResultType]::ParameterName, 'no-multipart')\n            [CompletionResult]::new('--no-raw', '--no-raw', [CompletionResultType]::ParameterName, 'no-raw')\n            [CompletionResult]::new('--no-pretty', '--no-pretty', [CompletionResultType]::ParameterName, 'no-pretty')\n            [CompletionResult]::new('--no-format-options', '--no-format-options', [CompletionResultType]::ParameterName, 'no-format-options')\n            [CompletionResult]::new('--no-style', '--no-style', [CompletionResultType]::ParameterName, 'no-style')\n            [CompletionResult]::new('--no-response-charset', '--no-response-charset', [CompletionResultType]::ParameterName, 'no-response-charset')\n            [CompletionResult]::new('--no-response-mime', '--no-response-mime', [CompletionResultType]::ParameterName, 'no-response-mime')\n            [CompletionResult]::new('--no-print', '--no-print', [CompletionResultType]::ParameterName, 'no-print')\n            [CompletionResult]::new('--no-headers', '--no-headers', [CompletionResultType]::ParameterName, 'no-headers')\n            [CompletionResult]::new('--no-body', '--no-body', [CompletionResultType]::ParameterName, 'no-body')\n            [CompletionResult]::new('--no-meta', '--no-meta', [CompletionResultType]::ParameterName, 'no-meta')\n            [CompletionResult]::new('--no-verbose', '--no-verbose', [CompletionResultType]::ParameterName, 'no-verbose')\n            [CompletionResult]::new('--no-debug', '--no-debug', [CompletionResultType]::ParameterName, 'no-debug')\n            [CompletionResult]::new('--no-all', '--no-all', [CompletionResultType]::ParameterName, 'no-all')\n            [CompletionResult]::new('--no-history-print', '--no-history-print', [CompletionResultType]::ParameterName, 'no-history-print')\n            [CompletionResult]::new('--no-quiet', '--no-quiet', [CompletionResultType]::ParameterName, 'no-quiet')\n            [CompletionResult]::new('--no-stream', '--no-stream', [CompletionResultType]::ParameterName, 'no-stream')\n            [CompletionResult]::new('--no-compress', '--no-compress', [CompletionResultType]::ParameterName, 'no-compress')\n            [CompletionResult]::new('--no-output', '--no-output', [CompletionResultType]::ParameterName, 'no-output')\n            [CompletionResult]::new('--no-download', '--no-download', [CompletionResultType]::ParameterName, 'no-download')\n            [CompletionResult]::new('--no-continue', '--no-continue', [CompletionResultType]::ParameterName, 'no-continue')\n            [CompletionResult]::new('--no-session', '--no-session', [CompletionResultType]::ParameterName, 'no-session')\n            [CompletionResult]::new('--no-session-read-only', '--no-session-read-only', [CompletionResultType]::ParameterName, 'no-session-read-only')\n            [CompletionResult]::new('--no-auth-type', '--no-auth-type', [CompletionResultType]::ParameterName, 'no-auth-type')\n            [CompletionResult]::new('--no-auth', '--no-auth', [CompletionResultType]::ParameterName, 'no-auth')\n            [CompletionResult]::new('--no-bearer', '--no-bearer', [CompletionResultType]::ParameterName, 'no-bearer')\n            [CompletionResult]::new('--no-ignore-netrc', '--no-ignore-netrc', [CompletionResultType]::ParameterName, 'no-ignore-netrc')\n            [CompletionResult]::new('--no-offline', '--no-offline', [CompletionResultType]::ParameterName, 'no-offline')\n            [CompletionResult]::new('--no-check-status', '--no-check-status', [CompletionResultType]::ParameterName, 'no-check-status')\n            [CompletionResult]::new('--no-follow', '--no-follow', [CompletionResultType]::ParameterName, 'no-follow')\n            [CompletionResult]::new('--no-max-redirects', '--no-max-redirects', [CompletionResultType]::ParameterName, 'no-max-redirects')\n            [CompletionResult]::new('--no-timeout', '--no-timeout', [CompletionResultType]::ParameterName, 'no-timeout')\n            [CompletionResult]::new('--no-proxy', '--no-proxy', [CompletionResultType]::ParameterName, 'no-proxy')\n            [CompletionResult]::new('--no-verify', '--no-verify', [CompletionResultType]::ParameterName, 'no-verify')\n            [CompletionResult]::new('--no-cert', '--no-cert', [CompletionResultType]::ParameterName, 'no-cert')\n            [CompletionResult]::new('--no-cert-key', '--no-cert-key', [CompletionResultType]::ParameterName, 'no-cert-key')\n            [CompletionResult]::new('--no-ssl', '--no-ssl', [CompletionResultType]::ParameterName, 'no-ssl')\n            [CompletionResult]::new('--no-native-tls', '--no-native-tls', [CompletionResultType]::ParameterName, 'no-native-tls')\n            [CompletionResult]::new('--no-default-scheme', '--no-default-scheme', [CompletionResultType]::ParameterName, 'no-default-scheme')\n            [CompletionResult]::new('--no-https', '--no-https', [CompletionResultType]::ParameterName, 'no-https')\n            [CompletionResult]::new('--no-http-version', '--no-http-version', [CompletionResultType]::ParameterName, 'no-http-version')\n            [CompletionResult]::new('--no-resolve', '--no-resolve', [CompletionResultType]::ParameterName, 'no-resolve')\n            [CompletionResult]::new('--no-interface', '--no-interface', [CompletionResultType]::ParameterName, 'no-interface')\n            [CompletionResult]::new('--no-ipv4', '--no-ipv4', [CompletionResultType]::ParameterName, 'no-ipv4')\n            [CompletionResult]::new('--no-ipv6', '--no-ipv6', [CompletionResultType]::ParameterName, 'no-ipv6')\n            [CompletionResult]::new('--no-unix-socket', '--no-unix-socket', [CompletionResultType]::ParameterName, 'no-unix-socket')\n            [CompletionResult]::new('--no-ignore-stdin', '--no-ignore-stdin', [CompletionResultType]::ParameterName, 'no-ignore-stdin')\n            [CompletionResult]::new('--no-curl', '--no-curl', [CompletionResultType]::ParameterName, 'no-curl')\n            [CompletionResult]::new('--no-curl-long', '--no-curl-long', [CompletionResultType]::ParameterName, 'no-curl-long')\n            [CompletionResult]::new('--no-generate', '--no-generate', [CompletionResultType]::ParameterName, 'no-generate')\n            [CompletionResult]::new('--no-help', '--no-help', [CompletionResultType]::ParameterName, 'no-help')\n            [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version')\n            [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version')\n            break\n        }\n    })\n\n    $completions.Where{ $_.CompletionText -like \"$wordToComplete*\" } |\n        Sort-Object -Property ListItemText\n}\n"
  },
  {
    "path": "completions/xh.bash",
    "content": "_xh() {\n    local i cur prev opts cmd\n    COMPREPLY=()\n    if [[ \"${BASH_VERSINFO[0]}\" -ge 4 ]]; then\n        cur=\"$2\"\n    else\n        cur=\"${COMP_WORDS[COMP_CWORD]}\"\n    fi\n    prev=\"$3\"\n    cmd=\"\"\n    opts=\"\"\n\n    for i in \"${COMP_WORDS[@]:0:COMP_CWORD}\"\n    do\n        case \"${cmd},${i}\" in\n            \",$1\")\n                cmd=\"xh\"\n                ;;\n            *)\n                ;;\n        esac\n    done\n\n    case \"${cmd}\" in\n        xh)\n            opts=\"-j -f -s -p -h -b -m -v -P -q -S -x -o -d -c -A -a -F -4 -6 -I -V --json --form --multipart --raw --pretty --format-options --style --response-charset --response-mime --print --headers --body --meta --verbose --debug --all --history-print --quiet --stream --compress --output --download --continue --session --session-read-only --auth-type --auth --bearer --ignore-netrc --offline --check-status --follow --max-redirects --timeout --proxy --verify --cert --cert-key --ssl --native-tls --default-scheme --https --http-version --resolve --interface --ipv4 --ipv6 --unix-socket --ignore-stdin --curl --curl-long --generate --help --no-json --no-form --no-multipart --no-raw --no-pretty --no-format-options --no-style --no-response-charset --no-response-mime --no-print --no-headers --no-body --no-meta --no-verbose --no-debug --no-all --no-history-print --no-quiet --no-stream --no-compress --no-output --no-download --no-continue --no-session --no-session-read-only --no-auth-type --no-auth --no-bearer --no-ignore-netrc --no-offline --no-check-status --no-follow --no-max-redirects --no-timeout --no-proxy --no-verify --no-cert --no-cert-key --no-ssl --no-native-tls --no-default-scheme --no-https --no-http-version --no-resolve --no-interface --no-ipv4 --no-ipv6 --no-unix-socket --no-ignore-stdin --no-curl --no-curl-long --no-generate --no-help --version <[METHOD] URL> [REQUEST_ITEM]...\"\n            if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then\n                COMPREPLY=( $(compgen -W \"${opts}\" -- \"${cur}\") )\n                return 0\n            fi\n            case \"${prev}\" in\n                --raw)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --pretty)\n                    COMPREPLY=($(compgen -W \"all colors format none\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                --format-options)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --style)\n                    COMPREPLY=($(compgen -W \"auto solarized monokai fruity\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                -s)\n                    COMPREPLY=($(compgen -W \"auto solarized monokai fruity\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                --response-charset)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --response-mime)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --print)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                -p)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --history-print)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                -P)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --output)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                -o)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --session)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --session-read-only)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --auth-type)\n                    COMPREPLY=($(compgen -W \"basic bearer digest\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                -A)\n                    COMPREPLY=($(compgen -W \"basic bearer digest\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                --auth)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                -a)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --bearer)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --max-redirects)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --timeout)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --proxy)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --verify)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --cert)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --cert-key)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --ssl)\n                    COMPREPLY=($(compgen -W \"auto tls1 tls1.1 tls1.2 tls1.3\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                --default-scheme)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --http-version)\n                    COMPREPLY=($(compgen -W \"1.0 1.1 2 2-prior-knowledge 3-prior-knowledge\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                --resolve)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --interface)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --unix-socket)\n                    COMPREPLY=($(compgen -f \"${cur}\"))\n                    return 0\n                    ;;\n                --generate)\n                    COMPREPLY=($(compgen -W \"complete-bash complete-elvish complete-fish complete-nushell complete-powershell complete-zsh man\" -- \"${cur}\"))\n                    return 0\n                    ;;\n                *)\n                    COMPREPLY=()\n                    ;;\n            esac\n            COMPREPLY=( $(compgen -W \"${opts}\" -- \"${cur}\") )\n            return 0\n            ;;\n    esac\n}\n\nif [[ \"${BASH_VERSINFO[0]}\" -eq 4 && \"${BASH_VERSINFO[1]}\" -ge 4 || \"${BASH_VERSINFO[0]}\" -gt 4 ]]; then\n    complete -F _xh -o nosort -o bashdefault -o default xh\nelse\n    complete -F _xh -o bashdefault -o default xh\nfi\n"
  },
  {
    "path": "completions/xh.elv",
    "content": "\nuse builtin;\nuse str;\n\nset edit:completion:arg-completer[xh] = {|@words|\n    fn spaces {|n|\n        builtin:repeat $n ' ' | str:join ''\n    }\n    fn cand {|text desc|\n        edit:complex-candidate $text &display=$text' '(spaces (- 14 (wcswidth $text)))$desc\n    }\n    var command = 'xh'\n    for word $words[1..-1] {\n        if (str:has-prefix $word '-') {\n            break\n        }\n        set command = $command';'$word\n    }\n    var completions = [\n        &'xh'= {\n            cand --raw 'Pass raw request data without extra processing'\n            cand --pretty 'Controls output processing'\n            cand --format-options 'Set output formatting options'\n            cand -s 'Output coloring style'\n            cand --style 'Output coloring style'\n            cand --response-charset 'Override the response encoding for terminal display purposes'\n            cand --response-mime 'Override the response mime type for coloring and formatting for the terminal'\n            cand -p 'String specifying what the output should contain'\n            cand --print 'String specifying what the output should contain'\n            cand -P 'The same as --print but applies only to intermediary requests/responses'\n            cand --history-print 'The same as --print but applies only to intermediary requests/responses'\n            cand -o 'Save output to FILE instead of stdout'\n            cand --output 'Save output to FILE instead of stdout'\n            cand --session 'Create, or reuse and update a session'\n            cand --session-read-only 'Create or read a session without updating it from the request/response exchange'\n            cand -A 'Specify the auth mechanism'\n            cand --auth-type 'Specify the auth mechanism'\n            cand -a 'Authenticate as USER with PASS (-A basic|digest) or with TOKEN (-A bearer)'\n            cand --auth 'Authenticate as USER with PASS (-A basic|digest) or with TOKEN (-A bearer)'\n            cand --bearer 'Authenticate with a bearer token'\n            cand --max-redirects 'Number of redirects to follow. Only respected if --follow is used'\n            cand --timeout 'Connection timeout of the request'\n            cand --proxy 'Use a proxy for a protocol. For example: --proxy https:http://proxy.host:8080'\n            cand --verify 'If \"no\", skip SSL verification. If a file path, use it as a CA bundle'\n            cand --cert 'Use a client side certificate for SSL'\n            cand --cert-key 'A private key file to use with --cert'\n            cand --ssl 'Force a particular TLS version'\n            cand --default-scheme 'The default scheme to use if not specified in the URL'\n            cand --http-version 'HTTP version to use'\n            cand --resolve 'Override DNS resolution for specific domain to a custom IP'\n            cand --interface 'Bind to a network interface or local IP address'\n            cand --unix-socket 'Connect using a Unix domain socket'\n            cand --generate 'Generate shell completions or man pages'\n            cand -j '(default) Serialize data items from the command line as a JSON object'\n            cand --json '(default) Serialize data items from the command line as a JSON object'\n            cand -f 'Serialize data items from the command line as form fields'\n            cand --form 'Serialize data items from the command line as form fields'\n            cand --multipart 'Like --form, but force a multipart/form-data request even without files'\n            cand -h 'Print only the response headers. Shortcut for --print=h'\n            cand --headers 'Print only the response headers. Shortcut for --print=h'\n            cand -b 'Print only the response body. Shortcut for --print=b'\n            cand --body 'Print only the response body. Shortcut for --print=b'\n            cand -m 'Print only the response metadata. Shortcut for --print=m'\n            cand --meta 'Print only the response metadata. Shortcut for --print=m'\n            cand -v 'Print the whole request as well as the response'\n            cand --verbose 'Print the whole request as well as the response'\n            cand --debug 'Print full error stack traces and debug log messages'\n            cand --all 'Show any intermediary requests/responses while following redirects with --follow'\n            cand -q 'Do not print to stdout or stderr'\n            cand --quiet 'Do not print to stdout or stderr'\n            cand -S 'Always stream the response body'\n            cand --stream 'Always stream the response body'\n            cand -x 'Content compressed (encoded) with Deflate algorithm'\n            cand --compress 'Content compressed (encoded) with Deflate algorithm'\n            cand -d 'Download the body to a file instead of printing it'\n            cand --download 'Download the body to a file instead of printing it'\n            cand -c 'Resume an interrupted download. Requires --download and --output'\n            cand --continue 'Resume an interrupted download. Requires --download and --output'\n            cand --ignore-netrc 'Do not use credentials from .netrc'\n            cand --offline 'Construct HTTP requests without sending them anywhere'\n            cand --check-status '(default) Exit with an error status code if the server replies with an error'\n            cand -F 'Do follow redirects'\n            cand --follow 'Do follow redirects'\n            cand --native-tls 'Use the system TLS library instead of rustls (if enabled at compile time)'\n            cand --https 'Make HTTPS requests if not specified in the URL'\n            cand -4 'Resolve hostname to ipv4 addresses only'\n            cand --ipv4 'Resolve hostname to ipv4 addresses only'\n            cand -6 'Resolve hostname to ipv6 addresses only'\n            cand --ipv6 'Resolve hostname to ipv6 addresses only'\n            cand -I 'Do not attempt to read stdin'\n            cand --ignore-stdin 'Do not attempt to read stdin'\n            cand --curl 'Print a translation to a curl command'\n            cand --curl-long 'Use the long versions of curl''s flags'\n            cand --help 'Print help'\n            cand --no-json 'no-json'\n            cand --no-form 'no-form'\n            cand --no-multipart 'no-multipart'\n            cand --no-raw 'no-raw'\n            cand --no-pretty 'no-pretty'\n            cand --no-format-options 'no-format-options'\n            cand --no-style 'no-style'\n            cand --no-response-charset 'no-response-charset'\n            cand --no-response-mime 'no-response-mime'\n            cand --no-print 'no-print'\n            cand --no-headers 'no-headers'\n            cand --no-body 'no-body'\n            cand --no-meta 'no-meta'\n            cand --no-verbose 'no-verbose'\n            cand --no-debug 'no-debug'\n            cand --no-all 'no-all'\n            cand --no-history-print 'no-history-print'\n            cand --no-quiet 'no-quiet'\n            cand --no-stream 'no-stream'\n            cand --no-compress 'no-compress'\n            cand --no-output 'no-output'\n            cand --no-download 'no-download'\n            cand --no-continue 'no-continue'\n            cand --no-session 'no-session'\n            cand --no-session-read-only 'no-session-read-only'\n            cand --no-auth-type 'no-auth-type'\n            cand --no-auth 'no-auth'\n            cand --no-bearer 'no-bearer'\n            cand --no-ignore-netrc 'no-ignore-netrc'\n            cand --no-offline 'no-offline'\n            cand --no-check-status 'no-check-status'\n            cand --no-follow 'no-follow'\n            cand --no-max-redirects 'no-max-redirects'\n            cand --no-timeout 'no-timeout'\n            cand --no-proxy 'no-proxy'\n            cand --no-verify 'no-verify'\n            cand --no-cert 'no-cert'\n            cand --no-cert-key 'no-cert-key'\n            cand --no-ssl 'no-ssl'\n            cand --no-native-tls 'no-native-tls'\n            cand --no-default-scheme 'no-default-scheme'\n            cand --no-https 'no-https'\n            cand --no-http-version 'no-http-version'\n            cand --no-resolve 'no-resolve'\n            cand --no-interface 'no-interface'\n            cand --no-ipv4 'no-ipv4'\n            cand --no-ipv6 'no-ipv6'\n            cand --no-unix-socket 'no-unix-socket'\n            cand --no-ignore-stdin 'no-ignore-stdin'\n            cand --no-curl 'no-curl'\n            cand --no-curl-long 'no-curl-long'\n            cand --no-generate 'no-generate'\n            cand --no-help 'no-help'\n            cand -V 'Print version'\n            cand --version 'Print version'\n        }\n    ]\n    $completions[$command]\n}\n"
  },
  {
    "path": "completions/xh.fish",
    "content": "# Complete paths after @ in options:\nfunction __xh_complete_data\n    string match -qr '^(?<prefix>.*@)(?<path>.*)' -- (commandline -ct)\n    printf '%s\\n' -- $prefix(__fish_complete_path $path)\nend\ncomplete -c xh -n 'string match -qr \"@\" -- (commandline -ct)' -kxa \"(__xh_complete_data)\"\n\ncomplete -c xh -l raw -d 'Pass raw request data without extra processing' -r\ncomplete -c xh -l pretty -d 'Controls output processing' -r -f -a \"all\\t'(default) Enable both coloring and formatting'\ncolors\\t'Apply syntax highlighting to output'\nformat\\t'Pretty-print json and sort headers'\nnone\\t'Disable both coloring and formatting'\"\ncomplete -c xh -l format-options -d 'Set output formatting options' -r\ncomplete -c xh -s s -l style -d 'Output coloring style' -r -f -a \"auto\\t''\nsolarized\\t''\nmonokai\\t''\nfruity\\t''\"\ncomplete -c xh -l response-charset -d 'Override the response encoding for terminal display purposes' -r\ncomplete -c xh -l response-mime -d 'Override the response mime type for coloring and formatting for the terminal' -r\ncomplete -c xh -s p -l print -d 'String specifying what the output should contain' -r\ncomplete -c xh -s P -l history-print -d 'The same as --print but applies only to intermediary requests/responses' -r\ncomplete -c xh -s o -l output -d 'Save output to FILE instead of stdout' -r -F\ncomplete -c xh -l session -d 'Create, or reuse and update a session' -r\ncomplete -c xh -l session-read-only -d 'Create or read a session without updating it from the request/response exchange' -r\ncomplete -c xh -s A -l auth-type -d 'Specify the auth mechanism' -r -f -a \"basic\\t''\nbearer\\t''\ndigest\\t''\"\ncomplete -c xh -s a -l auth -d 'Authenticate as USER with PASS (-A basic|digest) or with TOKEN (-A bearer)' -r\ncomplete -c xh -l bearer -d 'Authenticate with a bearer token' -r\ncomplete -c xh -l max-redirects -d 'Number of redirects to follow. Only respected if --follow is used' -r\ncomplete -c xh -l timeout -d 'Connection timeout of the request' -r\ncomplete -c xh -l proxy -d 'Use a proxy for a protocol. For example: --proxy https:http://proxy.host:8080' -r\ncomplete -c xh -l verify -d 'If \"no\", skip SSL verification. If a file path, use it as a CA bundle' -r\ncomplete -c xh -l cert -d 'Use a client side certificate for SSL' -r -F\ncomplete -c xh -l cert-key -d 'A private key file to use with --cert' -r -F\ncomplete -c xh -l ssl -d 'Force a particular TLS version' -r -f -a \"auto\\t''\ntls1\\t''\ntls1.1\\t''\ntls1.2\\t''\ntls1.3\\t''\"\ncomplete -c xh -l default-scheme -d 'The default scheme to use if not specified in the URL' -r\ncomplete -c xh -l http-version -d 'HTTP version to use' -r -f -a \"1.0\\t''\n1.1\\t''\n2\\t''\n2-prior-knowledge\\t''\n3-prior-knowledge\\t''\"\ncomplete -c xh -l resolve -d 'Override DNS resolution for specific domain to a custom IP' -r\ncomplete -c xh -l interface -d 'Bind to a network interface or local IP address' -r\ncomplete -c xh -l unix-socket -d 'Connect using a Unix domain socket' -r -F\ncomplete -c xh -l generate -d 'Generate shell completions or man pages' -r -f -a \"complete-bash\\t''\ncomplete-elvish\\t''\ncomplete-fish\\t''\ncomplete-nushell\\t''\ncomplete-powershell\\t''\ncomplete-zsh\\t''\nman\\t''\"\ncomplete -c xh -s j -l json -d '(default) Serialize data items from the command line as a JSON object'\ncomplete -c xh -s f -l form -d 'Serialize data items from the command line as form fields'\ncomplete -c xh -l multipart -d 'Like --form, but force a multipart/form-data request even without files'\ncomplete -c xh -s h -l headers -d 'Print only the response headers. Shortcut for --print=h'\ncomplete -c xh -s b -l body -d 'Print only the response body. Shortcut for --print=b'\ncomplete -c xh -s m -l meta -d 'Print only the response metadata. Shortcut for --print=m'\ncomplete -c xh -s v -l verbose -d 'Print the whole request as well as the response'\ncomplete -c xh -l debug -d 'Print full error stack traces and debug log messages'\ncomplete -c xh -l all -d 'Show any intermediary requests/responses while following redirects with --follow'\ncomplete -c xh -s q -l quiet -d 'Do not print to stdout or stderr'\ncomplete -c xh -s S -l stream -d 'Always stream the response body'\ncomplete -c xh -s x -l compress -d 'Content compressed (encoded) with Deflate algorithm'\ncomplete -c xh -s d -l download -d 'Download the body to a file instead of printing it'\ncomplete -c xh -s c -l continue -d 'Resume an interrupted download. Requires --download and --output'\ncomplete -c xh -l ignore-netrc -d 'Do not use credentials from .netrc'\ncomplete -c xh -l offline -d 'Construct HTTP requests without sending them anywhere'\ncomplete -c xh -l check-status -d '(default) Exit with an error status code if the server replies with an error'\ncomplete -c xh -s F -l follow -d 'Do follow redirects'\ncomplete -c xh -l native-tls -d 'Use the system TLS library instead of rustls (if enabled at compile time)'\ncomplete -c xh -l https -d 'Make HTTPS requests if not specified in the URL'\ncomplete -c xh -s 4 -l ipv4 -d 'Resolve hostname to ipv4 addresses only'\ncomplete -c xh -s 6 -l ipv6 -d 'Resolve hostname to ipv6 addresses only'\ncomplete -c xh -s I -l ignore-stdin -d 'Do not attempt to read stdin'\ncomplete -c xh -l curl -d 'Print a translation to a curl command'\ncomplete -c xh -l curl-long -d 'Use the long versions of curl\\'s flags'\ncomplete -c xh -l help -d 'Print help'\ncomplete -c xh -l no-json\ncomplete -c xh -l no-form\ncomplete -c xh -l no-multipart\ncomplete -c xh -l no-raw\ncomplete -c xh -l no-pretty\ncomplete -c xh -l no-format-options\ncomplete -c xh -l no-style\ncomplete -c xh -l no-response-charset\ncomplete -c xh -l no-response-mime\ncomplete -c xh -l no-print\ncomplete -c xh -l no-headers\ncomplete -c xh -l no-body\ncomplete -c xh -l no-meta\ncomplete -c xh -l no-verbose\ncomplete -c xh -l no-debug\ncomplete -c xh -l no-all\ncomplete -c xh -l no-history-print\ncomplete -c xh -l no-quiet\ncomplete -c xh -l no-stream\ncomplete -c xh -l no-compress\ncomplete -c xh -l no-output\ncomplete -c xh -l no-download\ncomplete -c xh -l no-continue\ncomplete -c xh -l no-session\ncomplete -c xh -l no-session-read-only\ncomplete -c xh -l no-auth-type\ncomplete -c xh -l no-auth\ncomplete -c xh -l no-bearer\ncomplete -c xh -l no-ignore-netrc\ncomplete -c xh -l no-offline\ncomplete -c xh -l no-check-status\ncomplete -c xh -l no-follow\ncomplete -c xh -l no-max-redirects\ncomplete -c xh -l no-timeout\ncomplete -c xh -l no-proxy\ncomplete -c xh -l no-verify\ncomplete -c xh -l no-cert\ncomplete -c xh -l no-cert-key\ncomplete -c xh -l no-ssl\ncomplete -c xh -l no-native-tls\ncomplete -c xh -l no-default-scheme\ncomplete -c xh -l no-https\ncomplete -c xh -l no-http-version\ncomplete -c xh -l no-resolve\ncomplete -c xh -l no-interface\ncomplete -c xh -l no-ipv4\ncomplete -c xh -l no-ipv6\ncomplete -c xh -l no-unix-socket\ncomplete -c xh -l no-ignore-stdin\ncomplete -c xh -l no-curl\ncomplete -c xh -l no-curl-long\ncomplete -c xh -l no-generate\ncomplete -c xh -l no-help\ncomplete -c xh -s V -l version -d 'Print version'\n"
  },
  {
    "path": "completions/xh.nu",
    "content": "module completions {\n\n  def \"nu-complete xh pretty\" [] {\n    [ \"all\" \"colors\" \"format\" \"none\" ]\n  }\n\n  def \"nu-complete xh style\" [] {\n    [ \"auto\" \"solarized\" \"monokai\" \"fruity\" ]\n  }\n\n  def \"nu-complete xh auth_type\" [] {\n    [ \"basic\" \"bearer\" \"digest\" ]\n  }\n\n  def \"nu-complete xh ssl\" [] {\n    [ \"auto\" \"tls1\" \"tls1.1\" \"tls1.2\" \"tls1.3\" ]\n  }\n\n  def \"nu-complete xh http_version\" [] {\n    [ \"1.0\" \"1.1\" \"2\" \"2-prior-knowledge\" \"3-prior-knowledge\" ]\n  }\n\n  def \"nu-complete xh generate\" [] {\n    [ \"complete-bash\" \"complete-elvish\" \"complete-fish\" \"complete-nushell\" \"complete-powershell\" \"complete-zsh\" \"man\" ]\n  }\n\n  # xh is a friendly and fast tool for sending HTTP requests\n  export extern xh [\n    --json(-j)                # (default) Serialize data items from the command line as a JSON object\n    --form(-f)                # Serialize data items from the command line as form fields\n    --multipart               # Like --form, but force a multipart/form-data request even without files\n    --raw: string             # Pass raw request data without extra processing\n    --pretty: string@\"nu-complete xh pretty\" # Controls output processing\n    --format-options: string  # Set output formatting options\n    --style(-s): string@\"nu-complete xh style\" # Output coloring style\n    --response-charset: string # Override the response encoding for terminal display purposes\n    --response-mime: string   # Override the response mime type for coloring and formatting for the terminal\n    --print(-p): string       # String specifying what the output should contain\n    --headers(-h)             # Print only the response headers. Shortcut for --print=h\n    --body(-b)                # Print only the response body. Shortcut for --print=b\n    --meta(-m)                # Print only the response metadata. Shortcut for --print=m\n    --verbose(-v)             # Print the whole request as well as the response\n    --debug                   # Print full error stack traces and debug log messages\n    --all                     # Show any intermediary requests/responses while following redirects with --follow\n    --history-print(-P): string # The same as --print but applies only to intermediary requests/responses\n    --quiet(-q)               # Do not print to stdout or stderr\n    --stream(-S)              # Always stream the response body\n    --compress(-x)            # Content compressed (encoded) with Deflate algorithm\n    --output(-o): path        # Save output to FILE instead of stdout\n    --download(-d)            # Download the body to a file instead of printing it\n    --continue(-c)            # Resume an interrupted download. Requires --download and --output\n    --session: string         # Create, or reuse and update a session\n    --session-read-only: string # Create or read a session without updating it from the request/response exchange\n    --auth-type(-A): string@\"nu-complete xh auth_type\" # Specify the auth mechanism\n    --auth(-a): string        # Authenticate as USER with PASS (-A basic|digest) or with TOKEN (-A bearer)\n    --bearer: string          # Authenticate with a bearer token\n    --ignore-netrc            # Do not use credentials from .netrc\n    --offline                 # Construct HTTP requests without sending them anywhere\n    --check-status            # (default) Exit with an error status code if the server replies with an error\n    --follow(-F)              # Do follow redirects\n    --max-redirects: string   # Number of redirects to follow. Only respected if --follow is used\n    --timeout: string         # Connection timeout of the request\n    --proxy: string           # Use a proxy for a protocol. For example: --proxy https:http://proxy.host:8080\n    --verify: string          # If \"no\", skip SSL verification. If a file path, use it as a CA bundle\n    --cert: path              # Use a client side certificate for SSL\n    --cert-key: path          # A private key file to use with --cert\n    --ssl: string@\"nu-complete xh ssl\" # Force a particular TLS version\n    --native-tls              # Use the system TLS library instead of rustls (if enabled at compile time)\n    --default-scheme: string  # The default scheme to use if not specified in the URL\n    --https                   # Make HTTPS requests if not specified in the URL\n    --http-version: string@\"nu-complete xh http_version\" # HTTP version to use\n    --resolve: string         # Override DNS resolution for specific domain to a custom IP\n    --interface: string       # Bind to a network interface or local IP address\n    --ipv4(-4)                # Resolve hostname to ipv4 addresses only\n    --ipv6(-6)                # Resolve hostname to ipv6 addresses only\n    --unix-socket: path       # Connect using a Unix domain socket\n    --ignore-stdin(-I)        # Do not attempt to read stdin\n    --curl                    # Print a translation to a curl command\n    --curl-long               # Use the long versions of curl's flags\n    --generate: string@\"nu-complete xh generate\" # Generate shell completions or man pages\n    --help                    # Print help\n    --no-json\n    --no-form\n    --no-multipart\n    --no-raw\n    --no-pretty\n    --no-format-options\n    --no-style\n    --no-response-charset\n    --no-response-mime\n    --no-print\n    --no-headers\n    --no-body\n    --no-meta\n    --no-verbose\n    --no-debug\n    --no-all\n    --no-history-print\n    --no-quiet\n    --no-stream\n    --no-compress\n    --no-output\n    --no-download\n    --no-continue\n    --no-session\n    --no-session-read-only\n    --no-auth-type\n    --no-auth\n    --no-bearer\n    --no-ignore-netrc\n    --no-offline\n    --no-check-status\n    --no-follow\n    --no-max-redirects\n    --no-timeout\n    --no-proxy\n    --no-verify\n    --no-cert\n    --no-cert-key\n    --no-ssl\n    --no-native-tls\n    --no-default-scheme\n    --no-https\n    --no-http-version\n    --no-resolve\n    --no-interface\n    --no-ipv4\n    --no-ipv6\n    --no-unix-socket\n    --no-ignore-stdin\n    --no-curl\n    --no-curl-long\n    --no-generate\n    --no-help\n    --version(-V)             # Print version\n    raw_method_or_url: string # The request URL, preceded by an optional HTTP method\n    ...raw_rest_args: string  # Optional key-value pairs to be included in the request.\n  ]\n\n}\n\nexport use completions *\n"
  },
  {
    "path": "doc/man-template.roff",
    "content": ".TH XH 1 {{date}} {{version}} \"User Commands\"\n\n.SH NAME\nxh \\- Friendly and fast tool for sending HTTP requests\n\n.SH SYNOPSIS\n.B xh\n[\\fIOPTIONS\\fR]\n[\\fIMETHOD\\fR]\n\\fIURL\\fR\n[\\-\\-\\]\n[\\fIREQUEST_ITEM\\fR ...]\n\n.SH DESCRIPTION\n\n\\fBxh\\fR is an HTTP client with a friendly command line interface. It strives to\nhave readable output and easy-to-use options.\n\nxh is mostly compatible with HTTPie: see \\fBhttp\\fR(1).\n\nThe \\fB--curl\\fR option can be used to print a \\fBcurl\\fR(1) translation of the\ncommand instead of sending a request.\n\n.SH POSITIONAL ARGUMENTS\n.TP 4\n[\\fIMETHOD\\fR]\\fI\nThe HTTP method to use for the request.\n\nThis defaults to GET, or to POST if the request contains a body.\n.TP\n\\fIURL\\fR\nThe URL to request.\n\nThe URL scheme defaults to \"http://\" normally, or \"https://\" if\nthe program is invoked as \"xhs\".\n\nA leading colon works as shorthand for localhost. \":8000\" is equivalent\nto \"localhost:8000\", and \":/path\" is equivalent to \"localhost/path\".\n.TP\n[\\fIREQUEST_ITEM\\fR ...]\n{{request_items}}\n\n.SH OPTIONS\nEach --OPTION can be reset with a --no-OPTION argument.\n{{options}}\n\n.SH EXIT STATUS\n.TP 4\n.B 0\nSuccessful program execution.\n.TP\n.B 1\nUsage, syntax or network error.\n.TP\n.B 2\nRequest timeout.\n.TP\n.B 3\nUnexpected HTTP 3xx Redirection.\n.TP\n.B 4\nHTTP 4xx Client Error.\n.TP\n.B 5\nHTTP 5xx Server Error.\n.TP\n.B 6\nToo many redirects.\n\n.SH ENVIRONMENT\n.TP 4\n.B XH_CONFIG_DIR\nSpecifies where to look for config.json and named session data.\nThe default is ~/.config/xh for Linux/macOS and %APPDATA%\\\\xh for Windows.\n.TP\n.B XH_HTTPIE_COMPAT_MODE\nEnables the HTTPie Compatibility Mode. The only current difference is that\n\\-\\-check-status is not enabled by default. An alternative to setting this\nenvironment variable is to rename the binary to either http or https.\n.TP\n.BR REQUESTS_CA_BUNDLE \", \" CURL_CA_BUNDLE\nSets a custom CA bundle path.\n.TP\n.BR ALL_PROXY \"=[protocol://]<host>[:port]\"\nSets the proxy server for all requests (unless overridden for a specific protocol).\n.TP\n.BR HTTP_PROXY \"=[protocol://]<host>[:port]\"\nSets the proxy server to use for HTTP.\n.TP\n.BR HTTPS_PROXY \"=[protocol://]<host>[:port]\"\nSets the proxy server to use for HTTPS.\n.TP\n.B NO_PROXY\nList of comma-separated hosts for which to ignore the other proxy environment\nvariables. \"*\" matches all host names.\n.TP\n.B NETRC\nLocation of the .netrc file.\n.TP\n.B NO_COLOR\nDisables output coloring. See <https://no-color.org>\n.TP\n.B RUST_LOG\nConfigure low-level debug messages. See <https://docs.rs/env_logger/0.11.3/env_logger/#enabling-logging>\n\n.SH FILES\n.TP 4\n.I ~/.config/xh/config.json\nxh configuration file. The only configurable option is \"default_options\"\nwhich is a list of default shell arguments that gets passed to xh.\nExample:\n\n.RS\n{ \"default_options\": [\"--native-tls\", \"--style=solarized\"] }\n.RE\n.TP\n.IR ~/.netrc \", \" ~/_netrc\nAuto-login information file.\n.TP\n.I ~/.config/xh/sessions\nSession data directory grouped by domain and port number.\n\n.SH EXAMPLES\n.TP 4\n\\fBxh\\fR \\fIhttpbin.org/json\\fR\nSend a GET request.\n.TP\n\\fBxh\\fR \\fIhttpbin.org/post name=ahmed \\fIage:=24\\fR\nSend a POST request with body {\"name\": \"ahmed\", \"age\": 24}.\n.TP\n\\fBxh\\fR get \\fIhttpbin.org/json id==5 sort==true\\fR\nSend a GET request to http://httpbin.org/json?id=5&sort=true.\n.TP\n\\fBxh\\fR get \\fIhttpbin.org/json x-api-key:12345\\fR\nSend a GET request and include a header named X-Api-Key with value 12345.\n.TP\necho \"[1, 2, 3]\" | \\fBxh\\fR post \\fIhttpbin.org/post\nSend a POST request with body read from stdin.\n.TP\n\\fBxh\\fR put \\fIhttpbin.org/put id:=49 age:=25\\fR | less\nSend a PUT request and pipe the result to less.\n.TP\n\\fBxh\\fR -d \\fIhttpbin.org/json\\fR -o \\fIres.json\\fR\nDownload and save to res.json.\n.TP\n\\fBxh\\fR \\fIhttpbin.org/get user-agent:foobar\\fR\nMake a request with a custom user agent.\n.TP\n\\fBxhs\\fR \\fIexample.com\\fR\nMake an HTTPS request to https://example.com.\n\n.SH REPORTING BUGS\nxh's Github issues <https://github.com/ducaale/xh/issues>\n\n.SH SEE ALSO\n\\fBcurl\\fR(1), \\fBhttp\\fR(1)\n\nHTTPie's online documentation <https://httpie.io/docs/cli>\n"
  },
  {
    "path": "doc/xh.1",
    "content": ".TH XH 1 2026-01-14 0.25.3 \"User Commands\"\n\n.SH NAME\nxh \\- Friendly and fast tool for sending HTTP requests\n\n.SH SYNOPSIS\n.B xh\n[\\fIOPTIONS\\fR]\n[\\fIMETHOD\\fR]\n\\fIURL\\fR\n[\\-\\-\\]\n[\\fIREQUEST_ITEM\\fR ...]\n\n.SH DESCRIPTION\n\n\\fBxh\\fR is an HTTP client with a friendly command line interface. It strives to\nhave readable output and easy-to-use options.\n\nxh is mostly compatible with HTTPie: see \\fBhttp\\fR(1).\n\nThe \\fB--curl\\fR option can be used to print a \\fBcurl\\fR(1) translation of the\ncommand instead of sending a request.\n\n.SH POSITIONAL ARGUMENTS\n.TP 4\n[\\fIMETHOD\\fR]\\fI\nThe HTTP method to use for the request.\n\nThis defaults to GET, or to POST if the request contains a body.\n.TP\n\\fIURL\\fR\nThe URL to request.\n\nThe URL scheme defaults to \"http://\" normally, or \"https://\" if\nthe program is invoked as \"xhs\".\n\nA leading colon works as shorthand for localhost. \":8000\" is equivalent\nto \"localhost:8000\", and \":/path\" is equivalent to \"localhost/path\".\n.TP\n[\\fIREQUEST_ITEM\\fR ...]\nOptional key\\-value pairs to be included in the request.\n\nThe separator is used to determine the type:\n.RS 8\n.TP 4\nkey==value\nAdd a query string to the URL.\n.TP 4\nkey=value\nAdd a JSON property (\\-\\-json) or form field (\\-\\-form) to the request body.\n.TP 4\nkey:=value\nAdd a field with a literal JSON value to the request body.\n\nExample: \"numbers:=[1,2,3] enabled:=true\"\n.TP 4\nkey@filename\nUpload a file (requires \\-\\-form or \\-\\-multipart).\n\nTo set the filename and mimetype, \";type=\" and \";filename=\" can be used respectively.\n\nExample: \"pfp@ra.jpg;type=image/jpeg;filename=profile.jpg\"\n.TP 4\n@filename\nUse a file as the request body.\n.TP 4\nheader:value\nAdd a header, e.g. \"user\\-agent:foobar\"\n.TP 4\nheader:\nUnset a header, e.g. \"connection:\"\n.TP 4\nheader;\nAdd a header with an empty value.\n.RE\n\n.RS\nAn \"@\" prefix can be used to read a value from a file. For example: \"x\\-api\\-key:@api\\-key.txt\".\n\nA backslash can be used to escape special characters, e.g. \"weird\\\\:key=value\".\n\nTo construct a complex JSON object, the REQUEST_ITEM's key can be set to a JSON path instead of a field name. For more information on this syntax, refer to https://httpie.io/docs/cli/nested\\-json.\n.RE\n\n.SH OPTIONS\nEach --OPTION can be reset with a --no-OPTION argument.\n.TP 4\n\\fB\\-j\\fR, \\fB\\-\\-json\\fR\n(default) Serialize data items from the command line as a JSON object.\n\nOverrides both \\-\\-form and \\-\\-multipart.\n.TP 4\n\\fB\\-f\\fR, \\fB\\-\\-form\\fR\nSerialize data items from the command line as form fields.\n\nOverrides both \\-\\-json and \\-\\-multipart.\n.TP 4\n\\fB\\-\\-multipart\\fR\nLike \\-\\-form, but force a multipart/form\\-data request even without files.\n\nOverrides both \\-\\-json and \\-\\-form.\n.TP 4\n\\fB\\-\\-raw\\fR=\\fIRAW\\fR\nPass raw request data without extra processing.\n.TP 4\n\\fB\\-\\-pretty\\fR=\\fISTYLE\\fR\nControls output processing. Possible values are:\n\n    all      (default) Enable both coloring and formatting\n    colors   Apply syntax highlighting to output\n    format   Pretty\\-print json and sort headers\n    none     Disable both coloring and formatting\n\nDefaults to \"format\" if the NO_COLOR env is set and to \"none\" if stdout is not tty.\n.TP 4\n\\fB\\-\\-format\\-options\\fR=\\fIFORMAT_OPTIONS\\fR\nSet output formatting options. Supported option are:\n\n    json.indent:<NUM>\n    json.format:<true|false>\n    headers.sort:<true|false>\n\nExample: \\-\\-format\\-options=json.indent:2,headers.sort:false.\n.TP 4\n\\fB\\-s\\fR, \\fB\\-\\-style\\fR=\\fITHEME\\fR\nOutput coloring style.\n\n[possible values: auto, solarized, monokai, fruity]\n.TP 4\n\\fB\\-\\-response\\-charset\\fR=\\fIENCODING\\fR\nOverride the response encoding for terminal display purposes.\n\nExample: \\-\\-response\\-charset=latin1.\n.TP 4\n\\fB\\-\\-response\\-mime\\fR=\\fIMIME_TYPE\\fR\nOverride the response mime type for coloring and formatting for the terminal.\n\nExample: \\-\\-response\\-mime=application/json.\n.TP 4\n\\fB\\-p\\fR, \\fB\\-\\-print\\fR=\\fIFORMAT\\fR\nString specifying what the output should contain\n\n    'H' request headers\n    'B' request body\n    'h' response headers\n    'b' response body\n    'm' response metadata\n\nExample: \\-\\-print=Hb.\n.TP 4\n\\fB\\-h\\fR, \\fB\\-\\-headers\\fR\nPrint only the response headers. Shortcut for \\-\\-print=h.\n.TP 4\n\\fB\\-b\\fR, \\fB\\-\\-body\\fR\nPrint only the response body. Shortcut for \\-\\-print=b.\n.TP 4\n\\fB\\-m\\fR, \\fB\\-\\-meta\\fR\nPrint only the response metadata. Shortcut for \\-\\-print=m.\n.TP 4\n\\fB\\-v\\fR, \\fB\\-\\-verbose\\fR\nPrint the whole request as well as the response.\n\nAdditionally, this enables \\-\\-all for printing intermediary requests/responses while following redirects.\n\nUsing verbose twice i.e. \\-vv will print the response metadata as well.\n\nEquivalent to \\-\\-print=HhBb \\-\\-all.\n.TP 4\n\\fB\\-\\-debug\\fR\nPrint full error stack traces and debug log messages.\n\nLogging can be configured in more detail using the `$RUST_LOG` environment variable. Set `RUST_LOG=trace` to show even more messages. See https://docs.rs/env_logger/0.11.3/env_logger/#enabling\\-logging.\n.TP 4\n\\fB\\-\\-all\\fR\nShow any intermediary requests/responses while following redirects with \\-\\-follow.\n.TP 4\n\\fB\\-P\\fR, \\fB\\-\\-history\\-print\\fR=\\fIFORMAT\\fR\nThe same as \\-\\-print but applies only to intermediary requests/responses.\n.TP 4\n\\fB\\-q\\fR, \\fB\\-\\-quiet\\fR\nDo not print to stdout or stderr.\n\nUsing quiet twice i.e. \\-qq will suppress warnings as well.\n.TP 4\n\\fB\\-S\\fR, \\fB\\-\\-stream\\fR\nAlways stream the response body.\n.TP 4\n\\fB\\-x\\fR, \\fB\\-\\-compress\\fR\nContent compressed (encoded) with Deflate algorithm.\n\nThe Content\\-Encoding header is set to deflate.\n\nCompression is skipped if it appears that compression ratio is negative. Compression can be forced by repeating this option.\n\nNote: Compression cannot be used if the Content\\-Encoding request header is present.\n.TP 4\n\\fB\\-o\\fR, \\fB\\-\\-output\\fR=\\fIFILE\\fR\nSave output to FILE instead of stdout.\n.TP 4\n\\fB\\-d\\fR, \\fB\\-\\-download\\fR\nDownload the body to a file instead of printing it.\n\nThe Accept\\-Encoding header is set to identify and any redirects will be followed.\n.TP 4\n\\fB\\-c\\fR, \\fB\\-\\-continue\\fR\nResume an interrupted download. Requires \\-\\-download and \\-\\-output.\n.TP 4\n\\fB\\-\\-session\\fR=\\fIFILE\\fR\nCreate, or reuse and update a session.\n\nWithin a session, custom headers, auth credentials, as well as any cookies sent by the server persist between requests.\n.TP 4\n\\fB\\-\\-session\\-read\\-only\\fR=\\fIFILE\\fR\nCreate or read a session without updating it from the request/response exchange.\n.TP 4\n\\fB\\-A\\fR, \\fB\\-\\-auth\\-type\\fR=\\fIAUTH_TYPE\\fR\nSpecify the auth mechanism.\n\n[possible values: basic, bearer, digest]\n.TP 4\n\\fB\\-a\\fR, \\fB\\-\\-auth\\fR=\\fIUSER\\fR[\\fI:PASS\\fR] | \\fITOKEN\\fR\nAuthenticate as USER with PASS (\\-A basic|digest) or with TOKEN (\\-A bearer).\n\nPASS will be prompted if missing. Use a trailing colon (i.e. \"USER:\") to authenticate with just a username.\n\nTOKEN is expected if \\-\\-auth\\-type=bearer.\n.TP 4\n\\fB\\-\\-ignore\\-netrc\\fR\nDo not use credentials from .netrc.\n.TP 4\n\\fB\\-\\-offline\\fR\nConstruct HTTP requests without sending them anywhere.\n.TP 4\n\\fB\\-\\-check\\-status\\fR\n(default) Exit with an error status code if the server replies with an error.\n\nThe exit code will be 4 on 4xx (Client Error), 5 on 5xx (Server Error), or 3 on 3xx (Redirect) if \\-\\-follow isn't set.\n\nIf stdout is redirected then a warning is written to stderr.\n.TP 4\n\\fB\\-F\\fR, \\fB\\-\\-follow\\fR\nDo follow redirects.\n.TP 4\n\\fB\\-\\-max\\-redirects\\fR=\\fINUM\\fR\nNumber of redirects to follow. Only respected if \\-\\-follow is used.\n.TP 4\n\\fB\\-\\-timeout\\fR=\\fISEC\\fR\nConnection timeout of the request.\n\nThe default value is \"0\", i.e., there is no timeout limit.\n.TP 4\n\\fB\\-\\-proxy\\fR=\\fIPROTOCOL:URL\\fR\nUse a proxy for a protocol. For example: \\-\\-proxy https:http://proxy.host:8080.\n\nPROTOCOL can be \"all\", \"http\" or \"https\".\n\nIf your proxy requires credentials, put them in the URL, like so: \\-\\-proxy http:socks5://user:password@proxy.host:8000.\n\nYou can specify proxies for multiple protocols by repeating this option.\n\nThe environment variables \"ALL_PROXY\", \"HTTP_PROXY\" and \"HTTPS_PROXY\" can also be used, but are completely ignored if \\-\\-proxy is passed.\n.TP 4\n\\fB\\-\\-verify\\fR=\\fIVERIFY\\fR\nIf \"no\", skip SSL verification. If a file path, use it as a CA bundle.\n\nSpecifying a CA bundle will disable the system's built\\-in root certificates.\n\n\"false\" instead of \"no\" also works. The default is \"yes\" (\"true\").\n.TP 4\n\\fB\\-\\-cert\\fR=\\fIFILE\\fR\nUse a client side certificate for SSL.\n.TP 4\n\\fB\\-\\-cert\\-key\\fR=\\fIFILE\\fR\nA private key file to use with \\-\\-cert.\n\nOnly necessary if the private key is not contained in the cert file.\n.TP 4\n\\fB\\-\\-ssl\\fR=\\fIVERSION\\fR\nForce a particular TLS version.\n\n\"auto\" gives the default behavior of negotiating a version with the server.\n\n[possible values: auto, tls1, tls1.1, tls1.2, tls1.3]\n.TP 4\n\\fB\\-\\-native\\-tls\\fR\nUse the system TLS library instead of rustls (if enabled at compile time).\n.TP 4\n\\fB\\-\\-https\\fR\nMake HTTPS requests if not specified in the URL.\n.TP 4\n\\fB\\-\\-http\\-version\\fR=\\fIVERSION\\fR\nHTTP version to use.\n\n[possible values: 1.0, 1.1, 2, 2\\-prior\\-knowledge, 3\\-prior\\-knowledge]\n.TP 4\n\\fB\\-\\-resolve\\fR=\\fIHOST:ADDRESS\\fR\nOverride DNS resolution for specific domain to a custom IP.\n\nYou can override multiple domains by repeating this option.\n\nExample: \\-\\-resolve=example.com:127.0.0.1.\n.TP 4\n\\fB\\-\\-interface\\fR=\\fINAME\\fR\nBind to a network interface or local IP address.\n\nExample: \\-\\-interface=eth0 \\-\\-interface=192.168.0.2.\n.TP 4\n\\fB\\-4\\fR, \\fB\\-\\-ipv4\\fR\nResolve hostname to ipv4 addresses only.\n.TP 4\n\\fB\\-6\\fR, \\fB\\-\\-ipv6\\fR\nResolve hostname to ipv6 addresses only.\n.TP 4\n\\fB\\-\\-unix\\-socket\\fR=\\fIFILE\\fR\nConnect using a Unix domain socket.\n\nExample: xh :/index.html \\-\\-unix\\-socket=/var/run/temp.sock.\n.TP 4\n\\fB\\-I\\fR, \\fB\\-\\-ignore\\-stdin\\fR\nDo not attempt to read stdin.\n\nThis disables the default behaviour of reading the request body from stdin when a redirected input is detected.\n\nIt is recommended to pass this flag when using xh for scripting purposes. For more information, refer to https://httpie.io/docs/cli/best\\-practices.\n.TP 4\n\\fB\\-\\-curl\\fR\nPrint a translation to a curl command.\n\nFor translating the other way, try https://curl2httpie.online/.\n.TP 4\n\\fB\\-\\-curl\\-long\\fR\nUse the long versions of curl's flags.\n.TP 4\n\\fB\\-\\-generate\\fR=\\fIKIND\\fR\nGenerate shell completions or man pages. Possible values are:\n\n    complete\\-bash         Generate completions for bash\n    complete\\-elvish       Generate completions for elvish\n    complete\\-fish         Generage completions for fish\n    complete\\-nushell      Generate completions for nushell\n    complete\\-powershell   Generate completions for powershell\n    complete\\-zsh          Generate completions for zsh\n    man                   Generate manual page in roff format\n\nExample: xh \\-\\-generate=complete\\-bash > xh.bash.\n.TP 4\n\\fB\\-\\-help\\fR\nPrint help.\n.TP 4\n\\fB\\-V\\fR, \\fB\\-\\-version\\fR\nPrint version.\n\n.SH EXIT STATUS\n.TP 4\n.B 0\nSuccessful program execution.\n.TP\n.B 1\nUsage, syntax or network error.\n.TP\n.B 2\nRequest timeout.\n.TP\n.B 3\nUnexpected HTTP 3xx Redirection.\n.TP\n.B 4\nHTTP 4xx Client Error.\n.TP\n.B 5\nHTTP 5xx Server Error.\n.TP\n.B 6\nToo many redirects.\n\n.SH ENVIRONMENT\n.TP 4\n.B XH_CONFIG_DIR\nSpecifies where to look for config.json and named session data.\nThe default is ~/.config/xh for Linux/macOS and %APPDATA%\\\\xh for Windows.\n.TP\n.B XH_HTTPIE_COMPAT_MODE\nEnables the HTTPie Compatibility Mode. The only current difference is that\n\\-\\-check-status is not enabled by default. An alternative to setting this\nenvironment variable is to rename the binary to either http or https.\n.TP\n.BR REQUESTS_CA_BUNDLE \", \" CURL_CA_BUNDLE\nSets a custom CA bundle path.\n.TP\n.BR ALL_PROXY \"=[protocol://]<host>[:port]\"\nSets the proxy server for all requests (unless overridden for a specific protocol).\n.TP\n.BR HTTP_PROXY \"=[protocol://]<host>[:port]\"\nSets the proxy server to use for HTTP.\n.TP\n.BR HTTPS_PROXY \"=[protocol://]<host>[:port]\"\nSets the proxy server to use for HTTPS.\n.TP\n.B NO_PROXY\nList of comma-separated hosts for which to ignore the other proxy environment\nvariables. \"*\" matches all host names.\n.TP\n.B NETRC\nLocation of the .netrc file.\n.TP\n.B NO_COLOR\nDisables output coloring. See <https://no-color.org>\n.TP\n.B RUST_LOG\nConfigure low-level debug messages. See <https://docs.rs/env_logger/0.11.3/env_logger/#enabling-logging>\n\n.SH FILES\n.TP 4\n.I ~/.config/xh/config.json\nxh configuration file. The only configurable option is \"default_options\"\nwhich is a list of default shell arguments that gets passed to xh.\nExample:\n\n.RS\n{ \"default_options\": [\"--native-tls\", \"--style=solarized\"] }\n.RE\n.TP\n.IR ~/.netrc \", \" ~/_netrc\nAuto-login information file.\n.TP\n.I ~/.config/xh/sessions\nSession data directory grouped by domain and port number.\n\n.SH EXAMPLES\n.TP 4\n\\fBxh\\fR \\fIhttpbin.org/json\\fR\nSend a GET request.\n.TP\n\\fBxh\\fR \\fIhttpbin.org/post name=ahmed \\fIage:=24\\fR\nSend a POST request with body {\"name\": \"ahmed\", \"age\": 24}.\n.TP\n\\fBxh\\fR get \\fIhttpbin.org/json id==5 sort==true\\fR\nSend a GET request to http://httpbin.org/json?id=5&sort=true.\n.TP\n\\fBxh\\fR get \\fIhttpbin.org/json x-api-key:12345\\fR\nSend a GET request and include a header named X-Api-Key with value 12345.\n.TP\necho \"[1, 2, 3]\" | \\fBxh\\fR post \\fIhttpbin.org/post\nSend a POST request with body read from stdin.\n.TP\n\\fBxh\\fR put \\fIhttpbin.org/put id:=49 age:=25\\fR | less\nSend a PUT request and pipe the result to less.\n.TP\n\\fBxh\\fR -d \\fIhttpbin.org/json\\fR -o \\fIres.json\\fR\nDownload and save to res.json.\n.TP\n\\fBxh\\fR \\fIhttpbin.org/get user-agent:foobar\\fR\nMake a request with a custom user agent.\n.TP\n\\fBxhs\\fR \\fIexample.com\\fR\nMake an HTTPS request to https://example.com.\n\n.SH REPORTING BUGS\nxh's Github issues <https://github.com/ducaale/xh/issues>\n\n.SH SEE ALSO\n\\fBcurl\\fR(1), \\fBhttp\\fR(1)\n\nHTTPie's online documentation <https://httpie.io/docs/cli>\n"
  },
  {
    "path": "install.ps1",
    "content": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n\n$ProgressPreference = 'SilentlyContinue'\n$release = Invoke-RestMethod -Method Get -Uri \"https://api.github.com/repos/ducaale/xh/releases/latest\"\n$asset = $release.assets | Where-Object name -like *x86_64-pc-windows*.zip\n$destdir = \"$home\\bin\"\n$zipfile = \"$env:TEMP\\$( $asset.name )\"\n$zipfilename = [System.IO.Path]::GetFileNameWithoutExtension(\"$zipfile\")\n\nWrite-Output \"Downloading: $( $asset.name )\"\nInvoke-RestMethod -Method Get -Uri $asset.browser_download_url -OutFile $zipfile\n\n# Check if an older version of xh.exe (includes xhs.exe) exists in '$destdir', if yes, then delete it, if not then download latest zip to extract from\n$xhPath = \"${destdir}\\xh.exe\"\n$xhsPath = \"${destdir}\\xhs.exe\"\nif (Test-Path -Path $xhPath -PathType Leaf)\n{\n    Write-Output \"Removing previous installation of xh from $destdir\"\n    Remove-Item -r -fo $xhPath\n    Remove-Item -r -fo $xhsPath\n}\n\n# Create dir for result of extraction\nNew-Item -ItemType Directory -Path $destdir -Force | Out-Null\n\n# Decompress the zip file to the destination directory\nAdd-Type -Assembly System.IO.Compression.FileSystem\n$zip = [IO.Compression.ZipFile]::OpenRead($zipfile)\n$entries = $zip.Entries | Where-Object { $_.FullName -like '*.exe' }\n$entries | ForEach-Object { [IO.Compression.ZipFileExtensions]::ExtractToFile($_, $destdir + \"\\\" + $_.Name) }\n\n# Free the zipfile\n$zip.Dispose()\nRemove-Item -Path $zipfile\n\n# Copy xh.exe as xhs.exe into bin\nCopy-Item $xhPath $xhsPath\n\n# Get version from zip file name.\n$xhVersion = $($zipfilename.trim(\"xh-v -x86_64-pc-windows-msvc.zip\") )\n\n# Inform user where the executables have been put\nWrite-Output \"xh v$( $xhVersion ) has been installed to:`n - $xhPath`n - $xhsPath\"\n\n# Make sure destdir is in the path\n$userPath = [System.Environment]::GetEnvironmentVariable('Path', [System.EnvironmentVariableTarget]::User)\n$machinePath = [System.Environment]::GetEnvironmentVariable('Path', [System.EnvironmentVariableTarget]::Machine)\n\n# If userPath AND machinePath both do not contain bin, then add it to user path\nif (!($userPath.ToLower().Contains($destdir.ToLower())) -and !($machinePath.ToLower().Contains($destdir.ToLower())))\n{\n    # Update userPath\n    $userPath = $userPath.Trim(\";\") + \";$destdir\"\n\n    # Modify PATH for new windows\n    Write-Output \"`nAdding $destdir directory to the PATH variable.\"\n    [System.Environment]::SetEnvironmentVariable('Path', $userPath, [System.EnvironmentVariableTarget]::User)\n\n    # Modify PATH for current terminal\n    Write-Output \"`nRefreshing current terminal's PATH for you.\"\n    $Env:Path = $Env:Path.Trim(\";\") + \";$destdir\"\n\n    # Instruct how to modify PATH for other open terminals\n    Write-Output \"`nFor other terminals, restart them (or the entire IDE if they're within one).`n\"\n\n}\n"
  },
  {
    "path": "install.sh",
    "content": "#!/bin/sh\n\nset -e\n\nif [ \"$(uname -s)\" = \"Darwin\" ] && [ \"$(uname -m)\" = \"x86_64\" ]; then\n    target=\"x86_64-apple-darwin\"\nelif [ \"$(uname -s)\" = \"Darwin\" ] && [ \"$(uname -m)\" = \"arm64\" ]; then\n    target=\"aarch64-apple-darwin\"\nelif [ \"$(uname -s)\" = \"Linux\" ] && [ \"$(uname -m)\" = \"x86_64\" ]; then\n    target=\"x86_64-unknown-linux-musl\"\nelif [ \"$(uname -s)\" = \"Linux\" ] && [ \"$(uname -m)\" = \"aarch64\" ]; then\n    target=\"aarch64-unknown-linux-musl\"\nelif [ \"$(uname -s)\" = \"Linux\" ] && ( uname -m | grep -q -e '^arm' ); then\n    target=\"arm-unknown-linux-gnueabihf\"\nelse\n    echo \"Unsupported OS or architecture\"\n    exit 1\nfi\n\nfetch()\n{\n    if which curl > /dev/null; then\n        if [ \"$#\" -eq 2 ]; then curl -fL -o \"$1\" \"$2\"; else curl -fsSL \"$1\"; fi\n    elif which wget > /dev/null; then\n        if [ \"$#\" -eq 2 ]; then wget -O \"$1\" \"$2\"; else wget -nv -O - \"$1\"; fi\n    else\n        echo \"Can't find curl or wget, can't download package\"\n        exit 1\n    fi\n}\n\necho \"Detected target: $target\"\n\nreleases=$(fetch https://api.github.com/repos/ducaale/xh/releases/latest)\nurl=$(echo \"$releases\" | grep -wo -m1 \"https://.*$target.tar.gz\" || true)\nif ! test \"$url\"; then\n    echo \"Could not find release info\"\n    exit 1\nfi\n\necho \"Downloading xh...\"\n\ntemp_dir=$(mktemp -dt xh.XXXXXX)\ntrap 'rm -rf \"$temp_dir\"' EXIT INT TERM\ncd \"$temp_dir\"\n\nif ! fetch xh.tar.gz \"$url\"; then\n    echo \"Could not download tarball\"\n    exit 1\nfi\n\nuser_bin=\"$HOME/.local/bin\"\ncase $PATH in\n    *:\"$user_bin\":* | \"$user_bin\":* | *:\"$user_bin\")\n        default_bin=$user_bin\n        ;;\n    *)\n        default_bin='/usr/local/bin'\n        ;;\nesac\n\n_read_installdir() {\n    printf \"Install location [default: %s]: \" \"$default_bin\"\n    read -r xh_installdir < /dev/tty\n    xh_installdir=${xh_installdir:-$default_bin}\n}\n\nif [ -z \"$XH_BINDIR\" ]; then\n    _read_installdir\n\n    while ! test -d \"$xh_installdir\"; do\n        echo \"Directory $xh_installdir does not exist\"\n        _read_installdir\n    done\nelse\n    xh_installdir=${XH_BINDIR}\nfi\n\ntar xzf xh.tar.gz\n\nif test -w \"$xh_installdir\" || [ -n \"$XH_BINDIR\" ]; then\n    mv xh-*/xh \"$xh_installdir/\"\n    ln -sf \"$xh_installdir/xh\" \"$xh_installdir/xhs\"\nelse\n    sudo mv xh-*/xh \"$xh_installdir/\"\n    sudo ln -sf \"$xh_installdir/xh\" \"$xh_installdir/xhs\"\nfi\n\necho \"$(\"$xh_installdir\"/xh -V) has been installed to:\"\necho \" • $xh_installdir/xh\"\necho \" • $xh_installdir/xhs\"\n"
  },
  {
    "path": "src/auth.rs",
    "content": "use std::io;\n\nuse anyhow::Result;\nuse regex_lite::Regex;\nuse reqwest::StatusCode;\nuse reqwest::blocking::{Request, Response};\nuse reqwest::header::{AUTHORIZATION, HeaderValue, WWW_AUTHENTICATE};\n\nuse crate::cli::AuthType;\nuse crate::middleware::{Context, Middleware};\nuse crate::netrc;\nuse crate::utils::clone_request;\n\n#[derive(Debug, PartialEq, Eq)]\npub enum Auth {\n    Bearer(String),\n    Basic(String, Option<String>),\n    Digest(String, String),\n}\n\nimpl Auth {\n    pub fn from_str(auth: &str, auth_type: AuthType, host: &str) -> Result<Auth> {\n        match auth_type {\n            AuthType::Basic => {\n                let (username, password) = parse_auth(auth, host)?;\n                Ok(Auth::Basic(username, password))\n            }\n            AuthType::Digest => {\n                let (username, password) = parse_auth(auth, host)?;\n                Ok(Auth::Digest(\n                    username,\n                    password.unwrap_or_else(|| \"\".into()),\n                ))\n            }\n            AuthType::Bearer => Ok(Auth::Bearer(auth.into())),\n        }\n    }\n\n    pub fn from_netrc(auth_type: AuthType, entry: netrc::Entry) -> Option<Auth> {\n        match auth_type {\n            AuthType::Basic => Some(Auth::Basic(entry.login?, Some(entry.password))),\n            AuthType::Bearer => Some(Auth::Bearer(entry.password)),\n            AuthType::Digest => Some(Auth::Digest(entry.login?, entry.password)),\n        }\n    }\n}\n\npub fn parse_auth(auth: &str, host: &str) -> io::Result<(String, Option<String>)> {\n    if let Some(cap) = Regex::new(r\"^([^:]*):$\").unwrap().captures(auth) {\n        Ok((cap[1].to_string(), None))\n    } else if let Some(cap) = Regex::new(r\"^(.+?):(.+)$\").unwrap().captures(auth) {\n        let username = cap[1].to_string();\n        let password = cap[2].to_string();\n        Ok((username, Some(password)))\n    } else {\n        let username = auth.to_string();\n        let prompt = format!(\"http: password for {username}@{host}: \");\n        let password = rpassword::prompt_password(prompt)?;\n        Ok((username, Some(password)))\n    }\n}\n\npub struct DigestAuthMiddleware<'a> {\n    username: &'a str,\n    password: &'a str,\n}\n\nimpl<'a> DigestAuthMiddleware<'a> {\n    pub fn new(username: &'a str, password: &'a str) -> Self {\n        DigestAuthMiddleware { username, password }\n    }\n}\n\nimpl Middleware for DigestAuthMiddleware<'_> {\n    fn handle(&mut self, mut ctx: Context, mut request: Request) -> Result<Response> {\n        let mut response = self.next(&mut ctx, clone_request(&mut request)?)?;\n        match response.headers().get(WWW_AUTHENTICATE) {\n            Some(wwwauth) if response.status() == StatusCode::UNAUTHORIZED => {\n                let mut context = digest_auth::AuthContext::new(\n                    self.username,\n                    self.password,\n                    request.url().path(),\n                );\n                if let Some(cnonc) = std::env::var_os(\"XH_TEST_DIGEST_AUTH_CNONCE\") {\n                    context.set_custom_cnonce(cnonc.to_string_lossy().to_string());\n                }\n                let mut prompt = digest_auth::parse(wwwauth.to_str()?)?;\n                let answer = prompt.respond(&context)?.to_header_string();\n                request\n                    .headers_mut()\n                    .insert(AUTHORIZATION, HeaderValue::from_str(&answer)?);\n                self.print(&mut ctx, &mut response, &mut request)?;\n                Ok(self.next(&mut ctx, request)?)\n            }\n            _ => Ok(response),\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn parsing() {\n        let expected = vec![\n            (\"user:\", (\"user\", None)),\n            (\"user:password\", (\"user\", Some(\"password\"))),\n            (\"user:pass:with:colons\", (\"user\", Some(\"pass:with:colons\"))),\n            (\":\", (\"\", None)),\n        ];\n        for (input, output) in expected {\n            let (user, pass) = parse_auth(input, \"\").unwrap();\n            assert_eq!(output, (user.as_str(), pass.as_deref()));\n        }\n    }\n}\n"
  },
  {
    "path": "src/buffer.rs",
    "content": "//! The [`Buffer`] type is responsible for writing the program output, be it\n//! to a terminal or a pipe or a file. It supports colored output using\n//! `termcolor`'s `WriteColor` trait.\n//!\n//! It's always buffered, so `.flush()` should be called whenever no new\n//! output is immediately available. That's inconvenient, but improves\n//! throughput.\n//!\n//! We want slightly different implementations depending on the platform and\n//! the runtime conditions. Ansi<BufWriter> is fast, so we go through that\n//! when possible, but on Windows we often need a BufferedStandardStream\n//! instead to use the terminal APIs.\n//!\n//! Most of this code is boilerplate.\n\nuse std::{\n    env::var_os,\n    io::{self, Write},\n    path::Path,\n};\n\nuse crate::{\n    cli::Pretty,\n    utils::{test_default_color, test_pretend_term},\n};\n\npub use imp::Buffer;\n\n#[cfg(not(windows))]\nmod imp {\n    use std::io::{BufWriter, Write};\n\n    use termcolor::{Ansi, WriteColor};\n\n    pub struct Buffer {\n        inner: Ansi<BufWriter<Inner>>,\n        terminal: bool,\n        redirect: bool,\n    }\n\n    enum Inner {\n        File(std::fs::File),\n        Stdout(std::io::Stdout),\n        Stderr(std::io::Stderr),\n    }\n\n    impl Buffer {\n        pub fn stdout() -> Self {\n            Self {\n                inner: Ansi::new(BufWriter::new(Inner::Stdout(std::io::stdout()))),\n                terminal: true,\n                redirect: false,\n            }\n        }\n\n        pub fn stderr() -> Self {\n            Self {\n                inner: Ansi::new(BufWriter::new(Inner::Stderr(std::io::stderr()))),\n                terminal: true,\n                redirect: false,\n            }\n        }\n\n        pub fn redirect() -> Self {\n            Self {\n                inner: Ansi::new(BufWriter::new(Inner::Stdout(std::io::stdout()))),\n                terminal: crate::test_pretend_term(),\n                redirect: true,\n            }\n        }\n\n        pub fn file(file: std::fs::File) -> Self {\n            Self {\n                inner: Ansi::new(BufWriter::new(Inner::File(file))),\n                terminal: false,\n                redirect: false,\n            }\n        }\n\n        pub fn is_terminal(&self) -> bool {\n            self.terminal\n        }\n\n        pub fn is_redirect(&self) -> bool {\n            self.redirect\n        }\n\n        #[cfg(test)]\n        pub fn is_stdout(&self) -> bool {\n            matches!(self.inner.get_ref().get_ref(), Inner::Stdout(_))\n        }\n\n        #[cfg(test)]\n        pub fn is_stderr(&self) -> bool {\n            matches!(self.inner.get_ref().get_ref(), Inner::Stderr(_))\n        }\n\n        #[cfg(test)]\n        pub fn is_file(&self) -> bool {\n            matches!(self.inner.get_ref().get_ref(), Inner::File(_))\n        }\n    }\n\n    impl Write for Inner {\n        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {\n            match self {\n                Inner::File(w) => w.write(buf),\n                Inner::Stdout(w) => w.write(buf),\n                Inner::Stderr(w) => w.write(buf),\n            }\n        }\n\n        fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {\n            match self {\n                Inner::File(w) => w.write_all(buf),\n                Inner::Stdout(w) => w.write_all(buf),\n                Inner::Stderr(w) => w.write_all(buf),\n            }\n        }\n\n        fn flush(&mut self) -> std::io::Result<()> {\n            match self {\n                Inner::File(w) => w.flush(),\n                Inner::Stdout(w) => w.flush(),\n                Inner::Stderr(w) => w.flush(),\n            }\n        }\n    }\n\n    impl Write for Buffer {\n        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {\n            self.inner.write(buf)\n        }\n\n        fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {\n            // get_mut() to directly write into the BufWriter is significantly faster\n            // https://github.com/BurntSushi/termcolor/pull/56\n            self.inner.get_mut().write_all(buf)\n        }\n\n        fn flush(&mut self) -> std::io::Result<()> {\n            self.inner.flush()\n        }\n    }\n\n    impl WriteColor for Buffer {\n        fn supports_color(&self) -> bool {\n            true\n        }\n\n        fn set_color(&mut self, spec: &termcolor::ColorSpec) -> std::io::Result<()> {\n            self.inner.set_color(spec)\n        }\n\n        fn reset(&mut self) -> std::io::Result<()> {\n            self.inner.reset()\n        }\n    }\n}\n\n#[cfg(windows)]\nmod imp {\n    use std::io::{BufWriter, Write};\n\n    use termcolor::{Ansi, BufferedStandardStream, ColorChoice, WriteColor};\n\n    use crate::utils::test_default_color;\n\n    pub enum Buffer {\n        // Only escape codes make sense when the output isn't going directly\n        // to a terminal, so we use Ansi for some cases.\n        File(Ansi<BufWriter<std::fs::File>>),\n        Redirect(Ansi<BufWriter<std::io::Stdout>>),\n        Stdout(BufferedStandardStream),\n        Stderr(BufferedStandardStream),\n    }\n\n    impl Buffer {\n        pub fn stdout() -> Self {\n            Buffer::Stdout(BufferedStandardStream::stdout(if test_default_color() {\n                ColorChoice::AlwaysAnsi\n            } else {\n                ColorChoice::Always\n            }))\n        }\n\n        pub fn stderr() -> Self {\n            Buffer::Stderr(BufferedStandardStream::stderr(if test_default_color() {\n                ColorChoice::AlwaysAnsi\n            } else {\n                ColorChoice::Always\n            }))\n        }\n\n        pub fn redirect() -> Self {\n            Buffer::Redirect(Ansi::new(BufWriter::new(std::io::stdout())))\n        }\n\n        pub fn file(file: std::fs::File) -> Self {\n            Buffer::File(Ansi::new(BufWriter::new(file)))\n        }\n\n        pub fn is_terminal(&self) -> bool {\n            matches!(self, Buffer::Stdout(_) | Buffer::Stderr(_))\n        }\n\n        pub fn is_redirect(&self) -> bool {\n            matches!(self, Buffer::Redirect(_))\n        }\n\n        #[cfg(test)]\n        pub fn is_stdout(&self) -> bool {\n            matches!(self, Buffer::Stdout(_))\n        }\n\n        #[cfg(test)]\n        pub fn is_stderr(&self) -> bool {\n            matches!(self, Buffer::Stderr(_))\n        }\n\n        #[cfg(test)]\n        pub fn is_file(&self) -> bool {\n            matches!(self, Buffer::File(_))\n        }\n    }\n\n    impl Write for Buffer {\n        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {\n            match self {\n                Buffer::File(w) => w.write(buf),\n                Buffer::Redirect(w) => w.write(buf),\n                Buffer::Stdout(w) | Buffer::Stderr(w) => w.write(buf),\n            }\n        }\n\n        fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {\n            match self {\n                Buffer::File(w) => w.get_mut().write_all(buf),\n                Buffer::Redirect(w) => w.get_mut().write_all(buf),\n                Buffer::Stdout(w) | Buffer::Stderr(w) => w.write_all(buf),\n            }\n        }\n\n        fn flush(&mut self) -> std::io::Result<()> {\n            match self {\n                Buffer::File(w) => w.flush(),\n                Buffer::Redirect(w) => w.flush(),\n                Buffer::Stdout(w) | Buffer::Stderr(w) => w.flush(),\n            }\n        }\n    }\n\n    impl WriteColor for Buffer {\n        fn supports_color(&self) -> bool {\n            match self {\n                Buffer::File(w) => w.supports_color(),\n                Buffer::Redirect(w) => w.supports_color(),\n                Buffer::Stdout(w) | Buffer::Stderr(w) => w.supports_color(),\n            }\n        }\n\n        fn set_color(&mut self, spec: &termcolor::ColorSpec) -> std::io::Result<()> {\n            match self {\n                Buffer::File(w) => w.set_color(spec),\n                Buffer::Redirect(w) => w.set_color(spec),\n                Buffer::Stdout(w) | Buffer::Stderr(w) => w.set_color(spec),\n            }\n        }\n\n        fn reset(&mut self) -> std::io::Result<()> {\n            match self {\n                Buffer::File(w) => w.reset(),\n                Buffer::Redirect(w) => w.reset(),\n                Buffer::Stdout(w) | Buffer::Stderr(w) => w.reset(),\n            }\n        }\n\n        fn is_synchronous(&self) -> bool {\n            match self {\n                Buffer::File(w) => w.is_synchronous(),\n                Buffer::Redirect(w) => w.is_synchronous(),\n                Buffer::Stdout(w) | Buffer::Stderr(w) => w.is_synchronous(),\n            }\n        }\n    }\n}\n\nimpl Buffer {\n    pub fn new(download: bool, output: Option<&Path>, is_stdout_tty: bool) -> io::Result<Self> {\n        log::trace!(\"is_stdout_tty: {is_stdout_tty}\");\n        Ok(if download {\n            Buffer::stderr()\n        } else if let Some(output) = output {\n            log::trace!(\"creating file {output:?}\");\n            let file = std::fs::File::create(output)?;\n            Buffer::file(file)\n        } else if is_stdout_tty {\n            Buffer::stdout()\n        } else {\n            Buffer::redirect()\n        })\n    }\n\n    pub fn print(&mut self, s: &str) -> io::Result<()> {\n        self.write_all(s.as_bytes())\n    }\n\n    pub fn guess_pretty(&self) -> Pretty {\n        if test_default_color() {\n            Pretty::All\n        } else if test_pretend_term() {\n            Pretty::Format\n        } else if self.is_terminal() {\n            // Based on termcolor's logic for ColorChoice::Auto\n            if cfg!(test) {\n                Pretty::All\n            } else if var_os(\"NO_COLOR\").is_some_and(|val| !val.is_empty()) {\n                Pretty::Format\n            } else {\n                match var_os(\"TERM\") {\n                    Some(term) if term == \"dumb\" => Pretty::Format,\n                    Some(_) => Pretty::All,\n                    None if cfg!(windows) => Pretty::All,\n                    None => Pretty::Format,\n                }\n            }\n        } else {\n            Pretty::None\n        }\n    }\n}\n"
  },
  {
    "path": "src/cli.rs",
    "content": "use std::convert::TryFrom;\nuse std::env;\nuse std::ffi::OsString;\nuse std::fmt;\nuse std::fs;\nuse std::io::Write;\nuse std::mem;\nuse std::net::{IpAddr, Ipv6Addr};\nuse std::path::PathBuf;\nuse std::str::FromStr;\nuse std::time::Duration;\n\nuse anyhow::{Context, anyhow};\nuse clap::builder::Styles;\nuse clap::builder::styling::{AnsiColor, Effects};\nuse clap::{self, ArgAction, FromArgMatches, ValueEnum};\nuse encoding_rs::Encoding;\nuse regex_lite::Regex;\nuse reqwest::{Method, Url, tls};\nuse serde::Deserialize;\n\nuse crate::buffer::Buffer;\nuse crate::redacted::SecretString;\nuse crate::request_items::RequestItems;\nuse crate::utils::config_dir;\n\nconst STYLES: Styles = Styles::styled()\n    .header(AnsiColor::Blue.on_default().effects(Effects::BOLD))\n    .usage(AnsiColor::Blue.on_default().effects(Effects::BOLD))\n    .literal(AnsiColor::Cyan.on_default().effects(Effects::BOLD))\n    .placeholder(AnsiColor::Cyan.on_default())\n    .error(AnsiColor::Red.on_default().effects(Effects::BOLD))\n    .valid(AnsiColor::Green.on_default())\n    .invalid(AnsiColor::Yellow.on_default());\n\n// Some doc comments were copy-pasted from HTTPie\n\n// clap guidelines:\n// - Only use `short` with an explicit arg (`short = \"x\"`)\n// - Only use `long` with an implicit arg (just `long`)\n//   - Unless it needs a different name, but then also use `name = \"...\"`\n// - Add an uppercase value_name to options that take a value\n\n/// xh is a friendly and fast tool for sending HTTP requests.\n///\n/// It reimplements as much as possible of HTTPie's excellent design, with a focus\n/// on improved performance.\n#[derive(clap::Parser, Debug)]\n#[clap(\n    version,\n    long_version = long_version(),\n    disable_help_flag = true,\n    args_override_self = true,\n    styles = STYLES,\n)]\npub struct Cli {\n    #[clap(skip)]\n    pub httpie_compat_mode: bool,\n\n    /// (default) Serialize data items from the command line as a JSON object.\n    ///\n    /// Overrides both --form and --multipart.\n    #[clap(short = 'j', long, overrides_with_all = &[\"form\", \"multipart\"])]\n    pub json: bool,\n\n    /// Serialize data items from the command line as form fields.\n    ///\n    /// Overrides both --json and --multipart.\n    #[clap(short = 'f', long, overrides_with_all = &[\"json\", \"multipart\"])]\n    pub form: bool,\n\n    /// Like --form, but force a multipart/form-data request even without files.\n    ///\n    /// Overrides both --json and --form.\n    #[clap(long, conflicts_with_all = &[\"raw\", \"compress\"], overrides_with_all = &[\"json\", \"form\"])]\n    pub multipart: bool,\n\n    /// Pass raw request data without extra processing.\n    #[clap(long, value_name = \"RAW\")]\n    pub raw: Option<String>,\n\n    /// Controls output processing.\n    #[clap(\n        long,\n        value_enum,\n        value_name = \"STYLE\",\n        long_help = \"\\\nControls output processing. Possible values are:\n\n    all      (default) Enable both coloring and formatting\n    colors   Apply syntax highlighting to output\n    format   Pretty-print json and sort headers\n    none     Disable both coloring and formatting\n\nDefaults to \\\"format\\\" if the NO_COLOR env is set and to \\\"none\\\" if stdout is not tty.\"\n    )]\n    pub pretty: Option<Pretty>,\n\n    /// Set output formatting options.\n    #[clap(\n        long,\n        value_name = \"FORMAT_OPTIONS\",\n        long_help = \"\\\nSet output formatting options. Supported option are:\n\n    json.indent:<NUM>\n    json.format:<true|false>\n    xml.indent:<NUM>\n    xml.format:<true|false>\n    headers.sort:<true|false>\n\nExample: --format-options=json.indent:2,xml.indent:2,headers.sort:false\"\n    )]\n    pub format_options: Vec<FormatOptions>,\n\n    /// Output coloring style.\n    #[clap(short = 's', long, value_enum, value_name = \"THEME\")]\n    pub style: Option<Theme>,\n\n    /// Override the response encoding for terminal display purposes.\n    ///\n    /// Example: --response-charset=latin1\n    #[clap(long, value_name = \"ENCODING\", value_parser = parse_encoding)]\n    pub response_charset: Option<&'static Encoding>,\n\n    /// Override the response mime type for coloring and formatting for the terminal.\n    ///\n    /// Example: --response-mime=application/json\n    #[clap(long, value_name = \"MIME_TYPE\")]\n    pub response_mime: Option<String>,\n\n    /// String specifying what the output should contain\n    #[clap(\n        short = 'p',\n        long,\n        value_name = \"FORMAT\",\n        long_help = \"\\\nString specifying what the output should contain\n\n    'H' request headers\n    'B' request body\n    'h' response headers\n    'b' response body\n    'm' response metadata\n\nExample: --print=Hb\"\n    )]\n    pub print: Option<Print>,\n\n    /// Print only the response headers. Shortcut for --print=h.\n    #[clap(short = 'h', long)]\n    pub headers: bool,\n\n    /// Print only the response body. Shortcut for --print=b.\n    #[clap(short = 'b', long)]\n    pub body: bool,\n\n    /// Print only the response metadata. Shortcut for --print=m.\n    #[clap(short = 'm', long)]\n    pub meta: bool,\n\n    /// Print the whole request as well as the response.\n    ///\n    /// Additionally, this enables --all for printing intermediary\n    /// requests/responses while following redirects.\n    ///\n    /// Using verbose twice i.e. -vv will print the response metadata as well.\n    ///\n    /// Equivalent to --print=HhBb --all.\n    #[clap(short = 'v', long, action = ArgAction::Count)]\n    pub verbose: u8,\n\n    /// Print full error stack traces and debug log messages.\n    ///\n    /// Logging can be configured in more detail using the `$RUST_LOG` environment\n    /// variable. Set `RUST_LOG=trace` to show even more messages.\n    /// See https://docs.rs/env_logger/0.11.3/env_logger/#enabling-logging.\n    #[clap(long)]\n    pub debug: bool,\n\n    /// Show any intermediary requests/responses while following redirects with --follow.\n    #[clap(long)]\n    pub all: bool,\n\n    /// The same as --print but applies only to intermediary requests/responses.\n    #[clap(short = 'P', long, value_name = \"FORMAT\")]\n    pub history_print: Option<Print>,\n\n    /// Do not print to stdout or stderr.\n    ///\n    ///  Using quiet twice i.e. -qq will suppress warnings as well.\n    #[clap(short = 'q', long, action = ArgAction::Count)]\n    pub quiet: u8,\n\n    /// Always stream the response body.\n    #[clap(short = 'S', long = \"stream\", name = \"stream\")]\n    pub stream_raw: bool,\n\n    ///  Content compressed (encoded) with Deflate algorithm.\n    ///\n    ///  The Content-Encoding header is set to deflate.\n    ///\n    ///  Compression is skipped if it appears that compression ratio is negative.\n    ///  Compression can be forced by repeating this option.\n    ///\n    ///  Note: Compression cannot be used if the Content-Encoding request header is present.\n    #[clap(short = 'x', long = \"compress\", name = \"compress\", action = ArgAction::Count)]\n    pub compress: u8,\n\n    #[clap(skip)]\n    pub stream: Option<bool>,\n\n    /// Save output to FILE instead of stdout.\n    #[clap(short = 'o', long, value_name = \"FILE\")]\n    pub output: Option<PathBuf>,\n\n    /// Download the body to a file instead of printing it.\n    ///\n    /// The Accept-Encoding header is set to identify and any redirects will be followed.\n    #[clap(short = 'd', long)]\n    pub download: bool,\n\n    /// Resume an interrupted download. Requires --download and --output.\n    #[clap(\n        short = 'c',\n        long = \"continue\",\n        name = \"continue\",\n        requires = \"download\",\n        requires = \"output\"\n    )]\n    pub resume: bool,\n\n    /// Create, or reuse and update a session.\n    ///\n    /// Within a session, custom headers, auth credentials, as well as any cookies sent\n    /// by the server persist between requests.\n    #[clap(long, value_name = \"FILE\")]\n    pub session: Option<OsString>,\n\n    /// Create or read a session without updating it from the request/response exchange.\n    #[clap(long, value_name = \"FILE\", conflicts_with = \"session\")]\n    pub session_read_only: Option<OsString>,\n\n    #[clap(skip)]\n    pub is_session_read_only: bool,\n\n    /// Specify the auth mechanism.\n    #[clap(short = 'A', long, value_enum)]\n    pub auth_type: Option<AuthType>,\n\n    /// Authenticate as USER with PASS (-A basic|digest) or with TOKEN (-A bearer).\n    ///\n    /// PASS will be prompted if missing. Use a trailing colon (i.e. \"USER:\")\n    /// to authenticate with just a username.\n    ///\n    /// TOKEN is expected if --auth-type=bearer.\n    #[clap(short = 'a', long, value_name = \"USER[:PASS] | TOKEN\")]\n    pub auth: Option<SecretString>,\n\n    /// Authenticate with a bearer token.\n    #[clap(long, value_name = \"TOKEN\", hide = true)]\n    pub bearer: Option<SecretString>,\n\n    /// Do not use credentials from .netrc\n    #[clap(long)]\n    pub ignore_netrc: bool,\n\n    #[command(flatten)]\n    pub m_sig: MessageSignature,\n\n    /// Construct HTTP requests without sending them anywhere.\n    #[clap(long)]\n    pub offline: bool,\n\n    /// (default) Exit with an error status code if the server replies with an error.\n    ///\n    /// The exit code will be 4 on 4xx (Client Error), 5 on 5xx (Server Error),\n    /// or 3 on 3xx (Redirect) if --follow isn't set.\n    ///\n    /// If stdout is redirected then a warning is written to stderr.\n    #[clap(long = \"check-status\", name = \"check-status\")]\n    pub check_status_raw: bool,\n\n    #[clap(skip)]\n    pub check_status: Option<bool>,\n\n    /// Do follow redirects.\n    #[clap(short = 'F', long)]\n    pub follow: bool,\n\n    /// Number of redirects to follow. Only respected if --follow is used.\n    #[clap(long, value_name = \"NUM\")]\n    pub max_redirects: Option<usize>,\n\n    /// Connection timeout of the request.\n    ///\n    /// The default value is \"0\", i.e., there is no timeout limit.\n    #[clap(long, value_name = \"SEC\")]\n    pub timeout: Option<Timeout>,\n\n    /// Use a proxy for a protocol. For example: --proxy https:http://proxy.host:8080.\n    ///\n    /// PROTOCOL can be \"all\", \"http\" or \"https\".\n    ///\n    /// If your proxy requires credentials, put them in the URL, like so:\n    /// --proxy http:socks5://user:password@proxy.host:8000.\n    ///\n    /// You can specify proxies for multiple protocols by repeating this option.\n    ///\n    /// The environment variables \"ALL_PROXY\", \"HTTP_PROXY\" and \"HTTPS_PROXY\" can also be used, but\n    /// are completely ignored if --proxy is passed.\n    #[clap(long, value_name = \"PROTOCOL:URL\", number_of_values = 1)]\n    pub proxy: Vec<Proxy>,\n\n    /// If \"no\", skip SSL verification. If a file path, use it as a CA bundle.\n    ///\n    /// Specifying a CA bundle will disable the system's built-in root certificates.\n    ///\n    /// \"false\" instead of \"no\" also works. The default is \"yes\" (\"true\").\n    #[clap(long, value_name = \"VERIFY\", value_parser = VerifyParser)]\n    pub verify: Option<Verify>,\n\n    /// Use a client side certificate for SSL.\n    #[clap(long, value_name = \"FILE\")]\n    pub cert: Option<PathBuf>,\n\n    /// A private key file to use with --cert.\n    ///\n    /// Only necessary if the private key is not contained in the cert file.\n    #[clap(long, value_name = \"FILE\")]\n    pub cert_key: Option<PathBuf>,\n\n    /// Force a particular TLS version.\n    ///\n    /// \"auto\" gives the default behavior of negotiating a version\n    /// with the server.\n    #[clap(long, value_name = \"VERSION\", value_parser)]\n    pub ssl: Option<TlsVersion>,\n\n    /// Use the system TLS library instead of rustls (if enabled at compile time).\n    #[clap(long, hide = cfg!(not(all(feature = \"native-tls\", feature = \"rustls\"))))]\n    pub native_tls: bool,\n\n    /// The default scheme to use if not specified in the URL.\n    #[clap(long, value_name = \"SCHEME\", hide = true)]\n    pub default_scheme: Option<String>,\n\n    /// Make HTTPS requests if not specified in the URL.\n    #[clap(long)]\n    pub https: bool,\n\n    /// HTTP version to use\n    #[clap(long, value_name = \"VERSION\", value_parser)]\n    pub http_version: Option<HttpVersion>,\n\n    /// Override DNS resolution for specific domain to a custom IP.\n    ///\n    /// You can override multiple domains by repeating this option.\n    ///\n    /// Example: --resolve=example.com:127.0.0.1\n    #[clap(long, value_name = \"HOST:ADDRESS\")]\n    pub resolve: Vec<Resolve>,\n\n    /// Bind to a network interface or local IP address.\n    ///\n    /// Example: --interface=eth0 --interface=192.168.0.2\n    #[clap(long, value_name = \"NAME\")]\n    pub interface: Option<String>,\n\n    /// Resolve hostname to ipv4 addresses only.\n    #[clap(short = '4', long)]\n    pub ipv4: bool,\n\n    /// Resolve hostname to ipv6 addresses only.\n    #[clap(short = '6', long)]\n    pub ipv6: bool,\n\n    /// Connect using a Unix domain socket.\n    ///\n    /// Example: xh :/index.html --unix-socket=/var/run/temp.sock\n    #[clap(long, value_name = \"FILE\")]\n    pub unix_socket: Option<PathBuf>,\n\n    /// Do not attempt to read stdin.\n    ///\n    /// This disables the default behaviour of reading the request body from stdin\n    /// when a redirected input is detected.\n    ///\n    /// It is recommended to pass this flag when using xh for scripting purposes.\n    /// For more information, refer to https://httpie.io/docs/cli/best-practices.\n    #[clap(short = 'I', long)]\n    pub ignore_stdin: bool,\n\n    /// Print a translation to a curl command.\n    ///\n    /// For translating the other way, try https://curl2httpie.online/.\n    #[clap(long)]\n    pub curl: bool,\n\n    /// Use the long versions of curl's flags.\n    #[clap(long)]\n    pub curl_long: bool,\n\n    /// Generate shell completions or man pages.\n    #[arg(\n        long,\n        value_name = \"KIND\",\n        hide_possible_values = true,\n        long_help = \"\\\nGenerate shell completions or man pages. Possible values are:\n\n    complete-bash         Generate completions for bash\n    complete-elvish       Generate completions for elvish\n    complete-fish         Generage completions for fish\n    complete-nushell      Generate completions for nushell\n    complete-powershell   Generate completions for powershell\n    complete-zsh          Generate completions for zsh\n    man                   Generate manual page in roff format\n\nExample: xh --generate=complete-bash > xh.bash\",\n        conflicts_with = \"raw_method_or_url\"\n    )]\n    pub generate: Option<Generate>,\n\n    /// Print help.\n    #[clap(long, action = ArgAction::HelpShort)]\n    pub help: Option<bool>,\n\n    /// The request URL, preceded by an optional HTTP method.\n    ///\n    /// If the method is omitted, it will default to GET, or to POST\n    /// if the request contains a body.\n    ///\n    /// The URL scheme defaults to \"http://\" normally, or \"https://\" if\n    /// the program is invoked as \"xhs\".\n    ///\n    /// A leading colon works as shorthand for localhost. \":8000\" is equivalent\n    /// to \"localhost:8000\", and \":/path\" is equivalent to \"localhost/path\".\n    #[clap(value_name = \"[METHOD] URL\", required = true)]\n    raw_method_or_url: Option<String>,\n\n    /// Optional key-value pairs to be included in the request.\n    ///\n    /// The separator is used to determine the type:\n    ///\n    ///     key==value\n    ///         Add a query string to the URL.\n    ///\n    ///     key=value\n    ///         Add a JSON property (--json) or form field (--form) to the request body.\n    ///\n    ///     key:=value\n    ///         Add a field with a literal JSON value to the request body.\n    ///\n    ///         Example: \"numbers:=[1,2,3] enabled:=true\"\n    ///\n    ///     key@filename\n    ///         Upload a file (requires --form or --multipart).\n    ///\n    ///         To set the filename and mimetype, \";type=\" and \";filename=\" can be used respectively.\n    ///\n    ///         Example: \"pfp@ra.jpg;type=image/jpeg;filename=profile.jpg\"\n    ///\n    ///     @filename\n    ///         Use a file as the request body.\n    ///\n    ///     header:value\n    ///         Add a header, e.g. \"user-agent:foobar\"\n    ///\n    ///     header:\n    ///         Unset a header, e.g. \"connection:\"\n    ///\n    ///     header;\n    ///         Add a header with an empty value.\n    ///\n    /// An \"@\" prefix can be used to read a value from a file. For example: \"x-api-key:@api-key.txt\".\n    ///\n    /// A backslash can be used to escape special characters, e.g. \"weird\\:key=value\".\n    ///\n    /// To construct a complex JSON object, the REQUEST_ITEM's key can be set to a JSON path instead of a field name. For more information on this syntax, refer to https://httpie.io/docs/cli/nested-json.\n    #[clap(value_name = \"REQUEST_ITEM\", verbatim_doc_comment)]\n    raw_rest_args: Vec<String>,\n\n    /// The HTTP method, if supplied.\n    #[clap(skip)]\n    pub method: Option<Method>,\n\n    /// The request URL.\n    #[clap(skip = (\"http://placeholder\".parse::<Url>().unwrap()))]\n    pub url: Url,\n\n    /// Optional key-value pairs to be included in the request.\n    #[clap(skip)]\n    pub request_items: RequestItems,\n\n    /// The name of the binary.\n    #[clap(skip)]\n    pub bin_name: String,\n}\n\nimpl Cli {\n    pub fn parse() -> Self {\n        if let Some(default_args) = default_cli_args() {\n            let mut args = std::env::args_os();\n            Self::parse_from(\n                std::iter::once(args.next().unwrap_or_else(|| \"xh\".into()))\n                    .chain(default_args.into_iter().map(Into::into))\n                    .chain(args),\n            )\n        } else {\n            Self::parse_from(std::env::args_os())\n        }\n    }\n\n    pub fn parse_from<I>(iter: I) -> Self\n    where\n        I: IntoIterator,\n        I::Item: Into<OsString> + Clone,\n    {\n        match Self::try_parse_from(iter) {\n            Ok(cli) => cli,\n            Err(err) => err.exit(),\n        }\n    }\n\n    pub fn try_parse_from<I>(iter: I) -> clap::error::Result<Self>\n    where\n        I: IntoIterator,\n        I::Item: Into<OsString> + Clone,\n    {\n        let mut app = Self::into_app();\n        let matches = app.try_get_matches_from_mut(iter)?;\n        let mut cli = Self::from_arg_matches(&matches)?;\n\n        app.get_bin_name()\n            .and_then(|name| name.split('.').next())\n            .unwrap_or(\"xh\")\n            .clone_into(&mut cli.bin_name);\n\n        if cli.generate.is_some() {\n            return Ok(cli);\n        }\n\n        let mut raw_method_or_url = cli.raw_method_or_url.clone().unwrap();\n\n        if raw_method_or_url == \"help\" {\n            // opt-out of clap's auto-generated possible values help for --pretty\n            // as we already list them in the long_help\n            app = app.mut_arg(\"pretty\", |a| a.hide_possible_values(true));\n\n            app.print_long_help().unwrap();\n            safe_exit();\n        }\n\n        let mut rest_args = mem::take(&mut cli.raw_rest_args).into_iter();\n        let raw_url = match parse_method(&raw_method_or_url) {\n            Some(method) => {\n                cli.method = Some(method);\n                rest_args.next().ok_or_else(|| {\n                    app.error(\n                        clap::error::ErrorKind::MissingRequiredArgument,\n                        \"Missing <URL>\",\n                    )\n                })?\n            }\n            None => {\n                cli.method = None;\n                mem::take(&mut raw_method_or_url)\n            }\n        };\n        for request_item in rest_args {\n            cli.request_items.items.push(\n                request_item\n                    .parse()\n                    .map_err(|err: clap::error::Error| err.format(&mut app))?,\n            );\n        }\n\n        if matches!(cli.bin_name.as_str(), \"https\" | \"xhs\" | \"xhttps\") {\n            cli.https = true;\n        }\n        if matches!(cli.bin_name.as_str(), \"http\" | \"https\")\n            || env::var_os(\"XH_HTTPIE_COMPAT_MODE\").is_some()\n        {\n            cli.httpie_compat_mode = true;\n        }\n\n        cli.process_relations(&matches)?;\n\n        cli.url = construct_url(&raw_url, cli.default_scheme.as_deref()).map_err(|err| {\n            app.error(\n                clap::error::ErrorKind::ValueValidation,\n                format!(\"Invalid <URL>: {err}\"),\n            )\n        })?;\n\n        if cfg!(not(feature = \"rustls\")) {\n            cli.native_tls = true;\n        }\n\n        Ok(cli)\n    }\n\n    /// Set flags that are implied by other flags and report conflicting flags.\n    fn process_relations(&mut self, matches: &clap::ArgMatches) -> clap::error::Result<()> {\n        if self.verbose > 0 {\n            self.all = true;\n        }\n        if self.curl_long {\n            self.curl = true;\n        }\n        if self.https {\n            self.default_scheme = Some(\"https\".to_string());\n        }\n        if self.bearer.is_some() {\n            self.auth_type = Some(AuthType::Bearer);\n            self.auth = self.bearer.take();\n        }\n        self.check_status = match (self.check_status_raw, matches.get_flag(\"no-check-status\")) {\n            (true, true) => unreachable!(),\n            (true, false) => Some(true),\n            (false, true) => Some(false),\n            (false, false) => None,\n        };\n        self.stream = match (self.stream_raw, matches.get_flag(\"no-stream\")) {\n            (true, true) => unreachable!(),\n            (true, false) => Some(true),\n            (false, true) => Some(false),\n            (false, false) => None,\n        };\n        if self.download {\n            self.follow = true;\n            self.check_status = Some(true);\n        }\n        // `overrides_with_all` ensures that only one of these is true\n        if self.json {\n            self.request_items.body_type = BodyType::Json;\n        } else if self.form {\n            self.request_items.body_type = BodyType::Form;\n        } else if self.multipart {\n            self.request_items.body_type = BodyType::Multipart;\n        }\n        if self.raw.is_some() && !self.request_items.is_body_empty() {\n            return Err(Self::into_app().error(\n                clap::error::ErrorKind::ValueValidation,\n                \"Request body (from --raw) and request data (key=value) cannot be mixed.\",\n            ));\n        }\n        if self.session_read_only.is_some() {\n            self.is_session_read_only = true;\n            self.session = mem::take(&mut self.session_read_only);\n        }\n        Ok(())\n    }\n\n    pub fn into_app() -> clap::Command {\n        let app = <Self as clap::CommandFactory>::command();\n\n        // Every option should have a --no- variant that makes it as if it was\n        // never passed.\n        // https://github.com/clap-rs/clap/issues/815\n        // https://github.com/httpie/httpie/blob/225dccb2186f14f871695b6c4e0bfbcdb2e3aa28/httpie/cli/argparser.py#L312\n        // Unlike HTTPie we apply the options in order, so the --no- variant\n        // has to follow the original to apply. You could have a chain of\n        // --x=y --no-x --x=z where the last one takes precedence.\n        let negations: Vec<_> = app\n            .get_arguments()\n            .filter(|a| !a.is_positional())\n            .map(|opt| {\n                let long = opt.get_long().expect(\"long option\");\n                clap::Arg::new(format!(\"no-{long}\"))\n                    .long(format!(\"no-{long}\"))\n                    .hide(true)\n                    .action(ArgAction::SetTrue)\n                    // overrides_with is enough to make the flags take effect\n                    // We never have to check their values, they'll simply\n                    // unset previous occurrences of the original flag\n                    .overrides_with(opt.get_id())\n            })\n            .collect();\n\n        let mut app = app.args(negations)\n            .after_help(format!(\"Each option can be reset with a --no-OPTION argument.\\n\\nRun \\\"{} help\\\" for more complete documentation.\", env!(\"CARGO_PKG_NAME\")))\n            .after_long_help(\"Each option can be reset with a --no-OPTION argument.\");\n\n        app.build();\n        app\n    }\n\n    pub fn logger_config(&self) -> env_logger::Builder {\n        if self.debug || std::env::var_os(\"RUST_LOG\").is_some() {\n            let env = env_logger::Env::default().default_filter_or(\"debug\");\n            let mut builder = env_logger::Builder::from_env(env);\n\n            let start = std::time::Instant::now();\n            builder.format(move |buf, record| {\n                let time = start.elapsed().as_secs_f64();\n                let level = record.level();\n                let style = buf.default_level_style(level);\n                let module = record.module_path().unwrap_or(\"\");\n                let args = record.args();\n                writeln!(\n                    buf,\n                    \"[{time:.6}s {style}{level: <5}{style:#} {module}] {args}\"\n                )\n            });\n\n            builder\n        } else {\n            let env = env_logger::Env::default();\n            let mut builder = env_logger::Builder::from_env(env);\n            if self.quiet >= 2 {\n                builder.filter_level(log::LevelFilter::Error);\n            } else {\n                builder.filter_level(log::LevelFilter::Warn);\n            }\n\n            let bin_name = self.bin_name.clone();\n            builder.format(move |buf, record| {\n                let level = match record.level() {\n                    log::Level::Error => \"error\",\n                    log::Level::Warn => \"warning\",\n                    log::Level::Info => \"info\",\n                    log::Level::Debug => \"debug\",\n                    log::Level::Trace => \"trace\",\n                };\n                let args = record.args();\n                writeln!(buf, \"{bin_name}: {level}: {args}\")\n            });\n\n            builder\n        }\n    }\n}\n\n#[derive(Deserialize)]\nstruct Config {\n    default_options: Vec<String>,\n}\n\nfn default_cli_args() -> Option<Vec<String>> {\n    let content = match fs::read_to_string(config_dir()?.join(\"config.json\")) {\n        Ok(file) => Some(file),\n        Err(err) => {\n            if err.kind() != std::io::ErrorKind::NotFound {\n                // Can't use log::warn!() because logging isn't initialized yet\n                eprintln!(\n                    \"\\n{}: warning: Unable to read config file: {}\\n\",\n                    env!(\"CARGO_PKG_NAME\"),\n                    err\n                );\n            }\n            None\n        }\n    }?;\n\n    match serde_json::from_str::<Config>(&content) {\n        Ok(config) => Some(config.default_options),\n        Err(err) => {\n            eprintln!(\n                \"\\n{}: warning: Unable to parse config file: {}\\n\",\n                env!(\"CARGO_PKG_NAME\"),\n                err\n            );\n            None\n        }\n    }\n}\n\nfn parse_method(method: &str) -> Option<Method> {\n    // This unfortunately matches \"localhost\"\n    if !method.is_empty() && method.chars().all(|c| c.is_ascii_alphabetic()) {\n        // Method parsing seems to fail if the length is 0 or if there's a null byte\n        // Our checks rule those both out, so .unwrap() is safe\n        Some(method.to_ascii_uppercase().parse().unwrap())\n    } else {\n        None\n    }\n}\n\nfn construct_url(\n    url: &str,\n    default_scheme: Option<&str>,\n) -> std::result::Result<Url, url::ParseError> {\n    let mut default_scheme = default_scheme.unwrap_or(\"http://\").to_string();\n    if !default_scheme.ends_with(\"://\") {\n        default_scheme.push_str(\"://\");\n    }\n    let url: Url = if let Some(url) = url.strip_prefix(\"://\") {\n        // Allow users to quickly convert a URL copied from a clipboard to xh/HTTPie command\n        // by simply adding a space before `://`.\n        // Example: https://example.org -> https ://example.org\n        format!(\"{default_scheme}{url}\").parse()?\n    } else if url.starts_with(':') {\n        format!(\"{}{}{}\", default_scheme, \"localhost\", url).parse()?\n    } else if !Regex::new(\"[a-zA-Z0-9]://.+\").unwrap().is_match(url) {\n        format!(\"{default_scheme}{url}\").parse()?\n    } else {\n        url.parse()?\n    };\n    Ok(url)\n}\n\n#[derive(Default, ValueEnum, Copy, Clone, Debug, PartialEq, Eq)]\npub enum AuthType {\n    #[default]\n    Basic,\n    Bearer,\n    Digest,\n}\n\n#[derive(clap::Args, Debug, Clone)]\npub struct MessageSignature {\n    /// Message signature key identifier (RFC 9421).\n    #[arg(\n        long = \"unstable-m-sig-id\",\n        value_name = \"KEY_ID\",\n        requires = \"m_sig_key\",\n        hide = cfg!(not(feature = \"http-message-signatures\"))\n    )]\n    pub m_sig_id: Option<String>,\n\n    /// Message signature key material (RFC 9421).\n    ///\n    /// Can be a raw string or a file path starting with @.\n    #[arg(\n        long = \"unstable-m-sig-key\",\n        value_name = \"KEY\",\n        requires = \"m_sig_id\",\n        hide = cfg!(not(feature = \"http-message-signatures\"))\n    )]\n    pub m_sig_key: Option<String>,\n\n    /// Message signature algorithm (RFC 9421).\n    ///\n    /// Supported algorithms: hmac-sha256, ed25519, ecdsa-p256-sha256,\n    /// ecdsa-p384-sha384, rsa-v1_5-sha256, rsa-pss-sha512.\n    #[arg(\n        long = \"unstable-m-sig-alg\",\n        value_name = \"ALG\",\n        hide_possible_values = true,\n        requires = \"m_sig_key\",\n        hide = cfg!(not(feature = \"http-message-signatures\"))\n    )]\n    pub m_sig_alg: Option<MessageSignatureAlgorithm>,\n\n    /// Comma-separated list of message signature components (RFC 9421).\n    ///\n    /// If not specified, defaults to \"@method, @authority, @target-uri\".\n    /// This flag can be passed multiple times; values are appended in order.\n    /// \"@query-params\" is a shorthand for all query parameters.\n    /// \"content-digest\" is included if there's a body.\n    ///\n    /// Example: \"@method,@path,content-digest\"\n    #[arg(\n        long = \"unstable-m-sig-comp\",\n        value_name = \"COMPONENTS\",\n        hide = cfg!(not(feature = \"http-message-signatures\"))\n    )]\n    pub m_sig_comp: Vec<MessageSignatureComponents>,\n}\n\n#[allow(unused)]\nimpl MessageSignature {\n    pub fn has_key_pair(&self) -> bool {\n        self.m_sig_id.is_some() && self.m_sig_key.is_some()\n    }\n\n    pub fn key_pair(&self) -> Option<(&str, &str)> {\n        Some((self.m_sig_id.as_deref()?, self.m_sig_key.as_deref()?))\n    }\n\n    pub fn algorithm(&self) -> Option<MessageSignatureAlgorithm> {\n        self.m_sig_alg\n    }\n\n    pub fn has_components(&self) -> bool {\n        self.m_sig_comp\n            .iter()\n            .any(|components| !components.0.is_empty())\n    }\n\n    #[cfg(feature = \"http-message-signatures\")]\n    pub fn flattened_components(&self) -> Vec<String> {\n        self.m_sig_comp\n            .iter()\n            .flat_map(|components| components.0.iter().cloned())\n            .collect()\n    }\n}\n\n#[derive(Debug, Clone)]\npub struct MessageSignatureComponents(pub Vec<String>);\n\nimpl FromStr for MessageSignatureComponents {\n    type Err = std::convert::Infallible;\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        let components = s\n            .split(',')\n            .map(|s| {\n                let component = s.trim();\n                if let Some(idx) = component.find(';') {\n                    let (name, params) = component.split_at(idx);\n                    format!(\"{}{}\", name.to_lowercase(), params)\n                } else {\n                    component.to_lowercase()\n                }\n            })\n            .collect();\n        Ok(MessageSignatureComponents(components))\n    }\n}\n\n#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]\npub enum MessageSignatureAlgorithm {\n    #[clap(name = \"hmac-sha256\")]\n    HmacSha256,\n    #[clap(name = \"ed25519\")]\n    Ed25519,\n    #[clap(name = \"ecdsa-p256-sha256\")]\n    EcdsaP256Sha256,\n    #[clap(name = \"ecdsa-p384-sha384\")]\n    EcdsaP384Sha384,\n    #[clap(name = \"rsa-v1_5-sha256\")]\n    RsaV15Sha256,\n    #[clap(name = \"rsa-pss-sha512\")]\n    RsaPssSha512,\n}\n\n#[cfg(feature = \"http-message-signatures\")]\nimpl From<MessageSignatureAlgorithm> for httpsig_hyper::prelude::AlgorithmName {\n    fn from(value: MessageSignatureAlgorithm) -> Self {\n        match value {\n            MessageSignatureAlgorithm::HmacSha256 => Self::HmacSha256,\n            MessageSignatureAlgorithm::Ed25519 => Self::Ed25519,\n            MessageSignatureAlgorithm::EcdsaP256Sha256 => Self::EcdsaP256Sha256,\n            MessageSignatureAlgorithm::EcdsaP384Sha384 => Self::EcdsaP384Sha384,\n            MessageSignatureAlgorithm::RsaV15Sha256 => Self::RsaV1_5Sha256,\n            MessageSignatureAlgorithm::RsaPssSha512 => Self::RsaPssSha512,\n        }\n    }\n}\n\n#[derive(ValueEnum, Debug, Clone)]\npub enum TlsVersion {\n    // ssl2.3 is not a real version but it's how HTTPie spells \"auto\"\n    #[clap(name = \"auto\", alias = \"ssl2.3\")]\n    Auto,\n    #[clap(name = \"tls1\")]\n    Tls1_0,\n    #[clap(name = \"tls1.1\")]\n    Tls1_1,\n    #[clap(name = \"tls1.2\")]\n    Tls1_2,\n    #[clap(name = \"tls1.3\")]\n    Tls1_3,\n}\n\nimpl From<TlsVersion> for Option<tls::Version> {\n    fn from(version: TlsVersion) -> Self {\n        match version {\n            TlsVersion::Auto => None,\n            TlsVersion::Tls1_0 => Some(tls::Version::TLS_1_0),\n            TlsVersion::Tls1_1 => Some(tls::Version::TLS_1_1),\n            TlsVersion::Tls1_2 => Some(tls::Version::TLS_1_2),\n            TlsVersion::Tls1_3 => Some(tls::Version::TLS_1_3),\n        }\n    }\n}\n\n#[derive(ValueEnum, Debug, PartialEq, Eq, Clone, Copy)]\npub enum Pretty {\n    /// (default) Enable both coloring and formatting\n    All,\n    /// Apply syntax highlighting to output\n    Colors,\n    /// Pretty-print json and sort headers\n    Format,\n    /// Disable both coloring and formatting\n    None,\n}\n\nimpl Pretty {\n    pub fn color(self) -> bool {\n        matches!(self, Pretty::Colors | Pretty::All)\n    }\n\n    pub fn format(self) -> bool {\n        matches!(self, Pretty::Format | Pretty::All)\n    }\n}\n\n#[derive(Debug, Clone, Default, PartialEq, Eq)]\npub struct FormatOptions {\n    pub json_indent: Option<usize>,\n    pub json_format: Option<bool>,\n    pub xml_indent: Option<usize>,\n    pub xml_format: Option<bool>,\n    pub headers_sort: Option<bool>,\n}\n\nimpl FormatOptions {\n    pub fn merge(mut self, other: &Self) -> Self {\n        self.json_indent = other.json_indent.or(self.json_indent);\n        self.json_format = other.json_format.or(self.json_format);\n        self.xml_indent = other.xml_indent.or(self.xml_indent);\n        self.xml_format = other.xml_format.or(self.xml_format);\n        self.headers_sort = other.headers_sort.or(self.headers_sort);\n        self\n    }\n}\n\nimpl FromStr for FormatOptions {\n    type Err = anyhow::Error;\n    fn from_str(options: &str) -> anyhow::Result<FormatOptions> {\n        let mut format_options = FormatOptions::default();\n\n        for argument in options.to_lowercase().split(',') {\n            let (key, value) = argument\n                .split_once(':')\n                .context(\"Format options consist of a key and a value, separated by a \\\":\\\".\")?;\n\n            let value_error = || format!(\"Invalid value '{value}' in '{argument}'\");\n\n            match key {\n                \"json.indent\" => {\n                    format_options.json_indent = Some(value.parse().with_context(value_error)?);\n                }\n                \"json.format\" => {\n                    format_options.json_format = Some(value.parse().with_context(value_error)?);\n                }\n                \"headers.sort\" => {\n                    format_options.headers_sort = Some(value.parse().with_context(value_error)?);\n                }\n                \"xml.indent\" => {\n                    format_options.xml_indent = Some(value.parse().with_context(value_error)?);\n                }\n                \"xml.format\" => {\n                    format_options.xml_format = Some(value.parse().with_context(value_error)?);\n                }\n                \"json.sort_keys\" => {\n                    return Err(anyhow!(\"Unsupported option '{key}'\"));\n                }\n                _ => {\n                    return Err(anyhow!(\"Unknown option '{key}'\"));\n                }\n            }\n        }\n        Ok(format_options)\n    }\n}\n\n#[derive(Default, ValueEnum, Debug, PartialEq, Eq, Clone, Copy)]\npub enum Theme {\n    #[default]\n    Auto,\n    Solarized,\n    Monokai,\n    Fruity,\n}\n\nimpl Theme {\n    pub fn as_str(&self) -> &'static str {\n        match self {\n            Theme::Auto => \"ansi\",\n            Theme::Solarized => \"solarized\",\n            Theme::Monokai => \"monokai\",\n            Theme::Fruity => \"fruity\",\n        }\n    }\n\n    pub(crate) fn as_syntect_theme(&self) -> &'static syntect::highlighting::Theme {\n        &crate::formatting::THEMES.themes[self.as_str()]\n    }\n}\n\n#[derive(Debug, Clone, Copy)]\npub struct Print {\n    pub request_headers: bool,\n    pub request_body: bool,\n    pub response_headers: bool,\n    pub response_body: bool,\n    pub response_meta: bool,\n}\n\nimpl Print {\n    pub fn new(\n        verbose: u8,\n        headers: bool,\n        body: bool,\n        meta: bool,\n        quiet: bool,\n        offline: bool,\n        buffer: &Buffer,\n    ) -> Self {\n        if verbose > 0 {\n            Print {\n                request_headers: true,\n                request_body: true,\n                response_headers: true,\n                response_body: true,\n                response_meta: verbose > 1,\n            }\n        } else if quiet {\n            Print {\n                request_headers: false,\n                request_body: false,\n                response_headers: false,\n                response_body: false,\n                response_meta: false,\n            }\n        } else if offline {\n            Print {\n                request_headers: true,\n                request_body: true,\n                response_headers: false,\n                response_body: false,\n                response_meta: false,\n            }\n        } else if headers {\n            Print {\n                request_headers: false,\n                request_body: false,\n                response_headers: true,\n                response_body: false,\n                response_meta: false,\n            }\n        } else if body || !buffer.is_terminal() {\n            Print {\n                request_headers: false,\n                request_body: false,\n                response_headers: false,\n                response_body: true,\n                response_meta: false,\n            }\n        } else if meta {\n            Print {\n                request_headers: false,\n                request_body: false,\n                response_headers: false,\n                response_body: false,\n                response_meta: true,\n            }\n        } else {\n            Print {\n                request_headers: false,\n                request_body: false,\n                response_headers: true,\n                response_body: true,\n                response_meta: false,\n            }\n        }\n    }\n}\n\nimpl FromStr for Print {\n    type Err = anyhow::Error;\n    fn from_str(s: &str) -> anyhow::Result<Print> {\n        let mut request_headers = false;\n        let mut request_body = false;\n        let mut response_headers = false;\n        let mut response_body = false;\n        let mut response_meta = false;\n\n        for char in s.chars() {\n            match char {\n                'H' => request_headers = true,\n                'B' => request_body = true,\n                'h' => response_headers = true,\n                'b' => response_body = true,\n                'm' => response_meta = true,\n                char => return Err(anyhow!(\"{:?} is not a valid value\", char)),\n            }\n        }\n\n        let p = Print {\n            request_headers,\n            request_body,\n            response_headers,\n            response_body,\n            response_meta,\n        };\n        Ok(p)\n    }\n}\n\n#[derive(Debug, Clone)]\npub struct Timeout(Duration);\n\nimpl Timeout {\n    pub fn as_duration(&self) -> Option<Duration> {\n        Some(self.0).filter(|t| !t.is_zero())\n    }\n}\n\nimpl FromStr for Timeout {\n    type Err = anyhow::Error;\n\n    fn from_str(sec: &str) -> anyhow::Result<Timeout> {\n        match f64::from_str(sec) {\n            Ok(s) if !s.is_nan() => {\n                if s.is_sign_negative() {\n                    Err(anyhow!(\"Connection timeout is negative\"))\n                } else if s >= Duration::MAX.as_secs_f64() || s.is_infinite() {\n                    Err(anyhow!(\"Connection timeout is too big\"))\n                } else {\n                    Ok(Timeout(Duration::from_secs_f64(s)))\n                }\n            }\n            _ => Err(anyhow!(\"Connection timeout is not a valid number\")),\n        }\n    }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum Proxy {\n    Http(Url),\n    Https(Url),\n    All(Url),\n}\n\nimpl FromStr for Proxy {\n    type Err = anyhow::Error;\n\n    fn from_str(s: &str) -> anyhow::Result<Self> {\n        let split_arg: Vec<&str> = s.splitn(2, ':').collect();\n        match split_arg[..] {\n            [protocol, url] => {\n                let url = reqwest::Url::try_from(url).map_err(|e| {\n                    anyhow!(\n                        \"Invalid proxy URL '{}' for protocol '{}': {}\",\n                        url,\n                        protocol,\n                        e\n                    )\n                })?;\n\n                match protocol.to_lowercase().as_str() {\n                    \"http\" => Ok(Proxy::Http(url)),\n                    \"https\" => Ok(Proxy::Https(url)),\n                    \"all\" => Ok(Proxy::All(url)),\n                    _ => Err(anyhow!(\"Unknown protocol to set a proxy for: {}\", protocol)),\n                }\n            }\n            _ => Err(anyhow!(\n                \"The value passed to --proxy should be formatted as <PROTOCOL>:<PROXY_URL>\"\n            )),\n        }\n    }\n}\n\n#[derive(Debug, Clone)]\npub struct Resolve {\n    pub domain: String,\n    pub addr: IpAddr,\n}\n\nimpl FromStr for Resolve {\n    type Err = anyhow::Error;\n\n    fn from_str(s: &str) -> anyhow::Result<Self> {\n        if s.chars().filter(|&c| c == ':').count() == 2 {\n            // More than two colons could mean an IPv6 address.\n            // Exactly two colons probably means the user added a port, curl-style.\n            return Err(anyhow!(\n                \"Value should be formatted as <HOST>:<ADDRESS> (not <HOST>:<PORT>:<ADDRESS>)\"\n            ));\n        }\n\n        let (domain, raw_addr) = s\n            .split_once(':')\n            .context(\"Value should be formatted as <HOST>:<ADDRESS>\")?;\n\n        let addr = if raw_addr.starts_with('[') && raw_addr.ends_with(']') {\n            // Support IPv6 addresses enclosed in square brackets e.g. [::1]\n            Ipv6Addr::from_str(&raw_addr[1..raw_addr.len() - 1]).map(IpAddr::V6)\n        } else {\n            raw_addr.parse()\n        }\n        .with_context(|| format!(\"Invalid address '{raw_addr}'\"))?;\n\n        Ok(Resolve {\n            domain: domain.to_string(),\n            addr,\n        })\n    }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum Verify {\n    Yes,\n    No,\n    CustomCaBundle(PathBuf),\n}\n\nimpl clap::builder::ValueParserFactory for Verify {\n    type Parser = VerifyParser;\n    fn value_parser() -> Self::Parser {\n        VerifyParser\n    }\n}\n\n#[derive(Clone, Debug)]\npub struct VerifyParser;\nimpl clap::builder::TypedValueParser for VerifyParser {\n    type Value = Verify;\n\n    fn parse_ref(\n        &self,\n        _cmd: &clap::Command,\n        _arg: Option<&clap::Arg>,\n        value: &std::ffi::OsStr,\n    ) -> clap::error::Result<Self::Value, clap::Error> {\n        Ok(match value.to_ascii_lowercase().to_str() {\n            Some(\"no\") | Some(\"false\") => Verify::No,\n            Some(\"yes\") | Some(\"true\") => Verify::Yes,\n            _ => Verify::CustomCaBundle(PathBuf::from(value)),\n        })\n    }\n}\n\nimpl fmt::Display for Verify {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Verify::No => write!(f, \"no\"),\n            Verify::Yes => write!(f, \"yes\"),\n            Verify::CustomCaBundle(path) => write!(f, \"custom ca bundle: {}\", path.display()),\n        }\n    }\n}\n\n#[derive(Default, Debug, PartialEq, Eq, Copy, Clone)]\npub enum BodyType {\n    #[default]\n    Json,\n    Form,\n    Multipart,\n}\n\n#[derive(ValueEnum, Debug, Clone)]\npub enum HttpVersion {\n    #[clap(name = \"1.0\", alias = \"1\")]\n    Http10,\n    #[clap(name = \"1.1\")]\n    Http11,\n    #[clap(name = \"2\")]\n    Http2,\n    #[clap(name = \"2-prior-knowledge\")]\n    Http2PriorKnowledge,\n    #[clap(name = \"3-prior-knowledge\")]\n    Http3PriorKnowledge,\n}\n\n#[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)]\npub enum Generate {\n    CompleteBash,\n    CompleteElvish,\n    CompleteFish,\n    CompleteNushell,\n    CompletePowershell,\n    CompleteZsh,\n    Man,\n}\n\n/// HTTPie uses Python's str.decode(). That one's very accepting of different spellings.\n/// encoding_rs is not.\n///\n/// Python accepts `utf16` and `u16` (and even `~~~~UtF////16@@`), encoding_rs makes you\n/// spell it `utf-16`.\n///\n/// There are also some encodings which encoding_rs doesn't support but HTTPie does, e.g utf-7.\n///\n/// See https://github.com/ducaale/xh/pull/184#pullrequestreview-787528027\n///\n/// We interpret `utf-16` as LE (little-endian) UTF-16, but that's not quite right.\n/// In Python it turns on BOM sniffing: it defaults to LE (at least on LE machines)\n/// but if there's a byte order mark at the start of the document it may switch to\n/// BE instead.\nfn parse_encoding(encoding: &str) -> anyhow::Result<&'static Encoding> {\n    let normalized_encoding = encoding.to_lowercase().replace(\n        |c: char| !c.is_alphanumeric() && c != '_' && c != '-' && c != ':',\n        \"\",\n    );\n\n    match normalized_encoding.as_str() {\n        \"u8\" | \"utf\" => return Ok(encoding_rs::UTF_8),\n        \"u16\" => return Ok(encoding_rs::UTF_16LE),\n        _ => (),\n    }\n\n    for encoding in [\n        &normalized_encoding,\n        &normalized_encoding.replace(&['-', '_'][..], \"\"),\n        &normalized_encoding.replace('_', \"-\"),\n        &normalized_encoding.replace('-', \"_\"),\n    ] {\n        if let Some(encoding) = Encoding::for_label(encoding.as_bytes()) {\n            return Ok(encoding);\n        }\n    }\n\n    {\n        let mut encoding = normalized_encoding.replace(&['-', '_'][..], \"\");\n        if let Some(first_digit_index) = encoding.find(|c: char| c.is_ascii_digit()) {\n            encoding.insert(first_digit_index, '-');\n            if let Some(encoding) = Encoding::for_label(encoding.as_bytes()) {\n                return Ok(encoding);\n            }\n        }\n    }\n\n    Err(anyhow::anyhow!(\n        \"{} is not a supported encoding, please refer to https://encoding.spec.whatwg.org/#names-and-labels \\\n         for supported encodings\",\n        encoding\n    ))\n}\n\n/// Based on the function used by clap to abort\nfn safe_exit() -> ! {\n    let _ = std::io::stdout().lock().flush();\n    let _ = std::io::stderr().lock().flush();\n    std::process::exit(0);\n}\n\nfn long_version() -> &'static str {\n    concat!(env!(\"CARGO_PKG_VERSION\"), \"\\n\", env!(\"XH_FEATURES\"))\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    use crate::request_items::RequestItem;\n\n    fn parse<I>(args: I) -> clap::error::Result<Cli>\n    where\n        I: IntoIterator,\n        I::Item: Into<OsString> + Clone,\n    {\n        Cli::try_parse_from(\n            Some(\"xh\".into())\n                .into_iter()\n                .chain(args.into_iter().map(Into::into)),\n        )\n    }\n\n    #[test]\n    fn implicit_method() {\n        let cli = parse([\"example.org\"]).unwrap();\n        assert_eq!(cli.method, None);\n        assert_eq!(cli.url.to_string(), \"http://example.org/\");\n        assert!(cli.request_items.items.is_empty());\n    }\n\n    #[test]\n    fn explicit_method() {\n        let cli = parse([\"get\", \"example.org\"]).unwrap();\n        assert_eq!(cli.method, Some(Method::GET));\n        assert_eq!(cli.url.to_string(), \"http://example.org/\");\n        assert!(cli.request_items.items.is_empty());\n    }\n\n    #[test]\n    fn method_edge_cases() {\n        // \"localhost\" is interpreted as method; this is undesirable, but expected\n        parse([\"localhost\"]).unwrap_err();\n\n        // Non-standard method used by varnish\n        let cli = parse([\"purge\", \":\"]).unwrap();\n        assert_eq!(cli.method, Some(\"PURGE\".parse().unwrap()));\n        assert_eq!(cli.url.to_string(), \"http://localhost/\");\n\n        // Zero-length arg should not be interpreted as method, but fail to parse as URL\n        parse([\"\"]).unwrap_err();\n    }\n\n    #[test]\n    fn missing_url() {\n        parse([\"get\"]).unwrap_err();\n    }\n\n    #[test]\n    fn space_in_url() {\n        let cli = parse([\"post\", \"example.org/foo bar\"]).unwrap();\n        assert_eq!(cli.method, Some(Method::POST));\n        assert_eq!(cli.url.to_string(), \"http://example.org/foo%20bar\");\n        assert!(cli.request_items.items.is_empty());\n    }\n\n    #[test]\n    fn url_with_leading_double_slash_colon() {\n        let cli = parse([\"://example.org\"]).unwrap();\n        assert_eq!(cli.url.to_string(), \"http://example.org/\");\n    }\n\n    #[test]\n    fn url_with_leading_colon() {\n        let cli = parse([\":3000\"]).unwrap();\n        assert_eq!(cli.url.to_string(), \"http://localhost:3000/\");\n\n        let cli = parse([\":3000/users\"]).unwrap();\n        assert_eq!(cli.url.to_string(), \"http://localhost:3000/users\");\n\n        let cli = parse([\":\"]).unwrap();\n        assert_eq!(cli.url.to_string(), \"http://localhost/\");\n\n        let cli = parse([\":/users\"]).unwrap();\n        assert_eq!(cli.url.to_string(), \"http://localhost/users\");\n    }\n\n    #[test]\n    fn url_with_scheme() {\n        let cli = parse([\"https://example.org\"]).unwrap();\n        assert_eq!(cli.url.to_string(), \"https://example.org/\");\n    }\n\n    #[test]\n    fn url_without_scheme() {\n        let cli = parse([\"example.org\"]).unwrap();\n        assert_eq!(cli.url.to_string(), \"http://example.org/\");\n    }\n\n    #[test]\n    fn request_items() {\n        let cli = parse([\"get\", \"example.org\", \"foo=bar\"]).unwrap();\n        assert_eq!(cli.method, Some(Method::GET));\n        assert_eq!(cli.url.to_string(), \"http://example.org/\");\n        assert_eq!(\n            cli.request_items.items,\n            vec![RequestItem::DataField {\n                key: \"foo\".to_string(),\n                raw_key: \"foo\".to_string(),\n                value: \"bar\".to_string()\n            }]\n        );\n    }\n\n    #[test]\n    fn request_items_implicit_method() {\n        let cli = parse([\"example.org\", \"foo=bar\"]).unwrap();\n        assert_eq!(cli.method, None);\n        assert_eq!(cli.url.to_string(), \"http://example.org/\");\n        assert_eq!(\n            cli.request_items.items,\n            vec![RequestItem::DataField {\n                key: \"foo\".to_string(),\n                raw_key: \"foo\".to_string(),\n                value: \"bar\".to_string()\n            }]\n        );\n    }\n\n    #[test]\n    fn request_type_overrides() {\n        let cli = parse([\"--form\", \"--json\", \":\"]).unwrap();\n        assert_eq!(cli.request_items.body_type, BodyType::Json);\n        assert_eq!(cli.json, true);\n        assert_eq!(cli.form, false);\n        assert_eq!(cli.multipart, false);\n\n        let cli = parse([\"--json\", \"--form\", \":\"]).unwrap();\n        assert_eq!(cli.request_items.body_type, BodyType::Form);\n        assert_eq!(cli.json, false);\n        assert_eq!(cli.form, true);\n        assert_eq!(cli.multipart, false);\n\n        let cli = parse([\":\"]).unwrap();\n        assert_eq!(cli.request_items.body_type, BodyType::Json);\n        assert_eq!(cli.json, false);\n        assert_eq!(cli.form, false);\n        assert_eq!(cli.multipart, false);\n    }\n\n    #[test]\n    fn superfluous_arg() {\n        parse([\"get\", \"example.org\", \"foobar\"]).unwrap_err();\n    }\n\n    #[test]\n    fn superfluous_arg_implicit_method() {\n        parse([\"example.org\", \"foobar\"]).unwrap_err();\n    }\n\n    #[test]\n    fn multiple_methods() {\n        parse([\"get\", \"post\", \"example.org\"]).unwrap_err();\n    }\n\n    #[test]\n    fn proxy_invalid_protocol() {\n        Cli::try_parse_from([\n            \"xh\",\n            \"--proxy=invalid:http://127.0.0.1:8000\",\n            \"get\",\n            \"example.org\",\n        ])\n        .unwrap_err();\n    }\n\n    #[test]\n    fn proxy_invalid_proxy_url() {\n        Cli::try_parse_from([\"xh\", \"--proxy=http:127.0.0.1:8000\", \"get\", \"example.org\"])\n            .unwrap_err();\n    }\n\n    #[test]\n    fn proxy_http() {\n        let proxy = parse([\"--proxy=http:http://127.0.0.1:8000\", \"get\", \"example.org\"])\n            .unwrap()\n            .proxy;\n\n        assert_eq!(\n            proxy,\n            vec!(Proxy::Http(Url::parse(\"http://127.0.0.1:8000\").unwrap()))\n        );\n    }\n\n    #[test]\n    fn proxy_https() {\n        let proxy = parse([\"--proxy=https:http://127.0.0.1:8000\", \"get\", \"example.org\"])\n            .unwrap()\n            .proxy;\n\n        assert_eq!(\n            proxy,\n            vec!(Proxy::Https(Url::parse(\"http://127.0.0.1:8000\").unwrap()))\n        );\n    }\n\n    #[test]\n    fn proxy_all() {\n        let proxy = parse([\"--proxy=all:http://127.0.0.1:8000\", \"get\", \"example.org\"])\n            .unwrap()\n            .proxy;\n\n        assert_eq!(\n            proxy,\n            vec!(Proxy::All(Url::parse(\"http://127.0.0.1:8000\").unwrap()))\n        );\n    }\n\n    #[test]\n    fn executable_name() {\n        let args = Cli::try_parse_from([\"xhs\", \"example.org\"]).unwrap();\n        assert_eq!(args.https, true);\n    }\n\n    #[test]\n    fn executable_name_extension() {\n        let args = Cli::try_parse_from([\"xhs.exe\", \"example.org\"]).unwrap();\n        assert_eq!(args.https, true);\n    }\n\n    #[test]\n    fn negated_flags() {\n        let cli = parse([\"--no-offline\", \":\"]).unwrap();\n        assert_eq!(cli.offline, false);\n\n        // In HTTPie, the order doesn't matter, so this would be false\n        let cli = parse([\"--no-offline\", \"--offline\", \":\"]).unwrap();\n        assert_eq!(cli.offline, true);\n\n        // In HTTPie, this resolves to json, but that seems wrong\n        let cli = parse([\"--no-form\", \"--multipart\", \":\"]).unwrap();\n        assert_eq!(cli.request_items.body_type, BodyType::Multipart);\n        assert_eq!(cli.json, false);\n        assert_eq!(cli.form, false);\n        assert_eq!(cli.multipart, true);\n\n        let cli = parse([\"--multipart\", \"--no-form\", \":\"]).unwrap();\n        assert_eq!(cli.request_items.body_type, BodyType::Multipart);\n        assert_eq!(cli.json, false);\n        assert_eq!(cli.form, false);\n        assert_eq!(cli.multipart, true);\n\n        let cli = parse([\"--form\", \"--no-form\", \":\"]).unwrap();\n        assert_eq!(cli.request_items.body_type, BodyType::Json);\n        assert_eq!(cli.json, false);\n        assert_eq!(cli.form, false);\n        assert_eq!(cli.multipart, false);\n\n        let cli = parse([\"--form\", \"--json\", \"--no-form\", \":\"]).unwrap();\n        assert_eq!(cli.request_items.body_type, BodyType::Json);\n        assert_eq!(cli.json, true);\n        assert_eq!(cli.form, false);\n        assert_eq!(cli.multipart, false);\n\n        let cli = parse([\"--curl-long\", \"--no-curl-long\", \":\"]).unwrap();\n        assert_eq!(cli.curl_long, false);\n        let cli = parse([\"--no-curl-long\", \"--curl-long\", \":\"]).unwrap();\n        assert_eq!(cli.curl_long, true);\n\n        let cli = parse([\"-do=fname\", \"--continue\", \"--no-continue\", \":\"]).unwrap();\n        assert_eq!(cli.resume, false);\n        let cli = parse([\"-do=fname\", \"--no-continue\", \"--continue\", \":\"]).unwrap();\n        assert_eq!(cli.resume, true);\n\n        let cli = parse([\"-I\", \"--no-ignore-stdin\", \":\"]).unwrap();\n        assert_eq!(cli.ignore_stdin, false);\n        let cli = parse([\"--no-ignore-stdin\", \"-I\", \":\"]).unwrap();\n        assert_eq!(cli.ignore_stdin, true);\n\n        let cli = parse([\n            \"--proxy=http:http://foo\",\n            \"--proxy=http:http://bar\",\n            \"--no-proxy\",\n            \":\",\n        ])\n        .unwrap();\n        assert!(cli.proxy.is_empty());\n\n        let cli = parse([\n            \"--no-proxy\",\n            \"--proxy=http:http://foo\",\n            \"--proxy=https:http://bar\",\n            \":\",\n        ])\n        .unwrap();\n        assert_eq!(\n            cli.proxy,\n            vec![\n                Proxy::Http(\"http://foo\".parse().unwrap()),\n                Proxy::Https(\"http://bar\".parse().unwrap())\n            ]\n        );\n\n        let cli = parse([\n            \"--proxy=http:http://foo\",\n            \"--no-proxy\",\n            \"--proxy=https:http://bar\",\n            \":\",\n        ])\n        .unwrap();\n        assert_eq!(cli.proxy, vec![Proxy::Https(\"http://bar\".parse().unwrap())]);\n\n        let cli = parse([\"--bearer=baz\", \"--no-bearer\", \":\"]).unwrap();\n        assert_eq!(cli.bearer, None);\n\n        let cli = parse([\"--style=solarized\", \"--no-style\", \":\"]).unwrap();\n        assert_eq!(cli.style, None);\n\n        let cli = parse([\n            \"--auth=foo:bar\",\n            \"--auth-type=bearer\",\n            \"--no-auth-type\",\n            \":\",\n        ])\n        .unwrap();\n        assert_eq!(cli.bearer, None);\n        assert_eq!(cli.auth_type, None);\n    }\n\n    #[test]\n    fn negating_check_status() {\n        let cli = parse([\":\"]).unwrap();\n        assert_eq!(cli.check_status, None);\n\n        let cli = parse([\"--check-status\", \":\"]).unwrap();\n        assert_eq!(cli.check_status, Some(true));\n\n        let cli = parse([\"--no-check-status\", \":\"]).unwrap();\n        assert_eq!(cli.check_status, Some(false));\n\n        let cli = parse([\"--check-status\", \"--no-check-status\", \":\"]).unwrap();\n        assert_eq!(cli.check_status, Some(false));\n\n        let cli = parse([\"--no-check-status\", \"--check-status\", \":\"]).unwrap();\n        assert_eq!(cli.check_status, Some(true));\n    }\n\n    #[test]\n    fn negating_stream() {\n        let cli = parse([\":\"]).unwrap();\n        assert_eq!(cli.stream, None);\n\n        let cli = parse([\"--stream\", \":\"]).unwrap();\n        assert_eq!(cli.stream, Some(true));\n\n        let cli = parse([\"--no-stream\", \":\"]).unwrap();\n        assert_eq!(cli.stream, Some(false));\n\n        let cli = parse([\"--stream\", \"--no-stream\", \":\"]).unwrap();\n        assert_eq!(cli.stream, Some(false));\n\n        let cli = parse([\"--no-stream\", \"--stream\", \":\"]).unwrap();\n        assert_eq!(cli.stream, Some(true));\n    }\n\n    #[test]\n    fn parse_encoding_label() {\n        let test_cases = vec![\n            (\"~~~~UtF////16@@\", encoding_rs::UTF_16LE),\n            (\"utf16\", encoding_rs::UTF_16LE),\n            (\"utf_16_be\", encoding_rs::UTF_16BE),\n            (\"utf16be\", encoding_rs::UTF_16BE),\n            (\"utf-16-be\", encoding_rs::UTF_16BE),\n            (\"utf_8\", encoding_rs::UTF_8),\n            (\"utf8\", encoding_rs::UTF_8),\n            (\"utf-8\", encoding_rs::UTF_8),\n            (\"u8\", encoding_rs::UTF_8),\n            (\"iso8859_6\", encoding_rs::ISO_8859_6),\n            (\"iso_8859-2:1987\", encoding_rs::ISO_8859_2),\n            (\"l1\", encoding_rs::WINDOWS_1252),\n            (\"elot-928\", encoding_rs::ISO_8859_7),\n        ];\n\n        for (input, output) in test_cases {\n            assert_eq!(parse_encoding(input).unwrap(), output);\n        }\n\n        assert_eq!(parse_encoding(\"notreal\").is_err(), true);\n        assert_eq!(parse_encoding(\"\").is_err(), true);\n    }\n\n    #[test]\n    fn parse_format_options() {\n        let invalid_format_options = vec![\n            // malformed strings\n            \":8\",\n            \"json.indent:\",\n            \":\",\n            \"\",\n            \"json.format:true, json.indent:4\",\n            // invalid values\n            \"json.indent:-8\",\n            \"json.format:ffalse\",\n            // unsupported options\n            \"json.sort_keys:true\",\n            // invalid xml option values\n            \"xml.indent:false\",\n            // invalid options\n            \"toml.format:true\",\n        ];\n\n        for format_option in invalid_format_options {\n            assert!(FormatOptions::from_str(format_option).is_err());\n        }\n\n        assert!(\n            FormatOptions::from_str(\n                \"json.indent:8,json.format:true,headers.sort:false,JSON.FORMAT:TRUE\"\n            )\n            .is_ok()\n        );\n\n        assert!(FormatOptions::from_str(\"xml.format:true,xml.indent:4\").is_ok());\n        assert!(FormatOptions::from_str(\"xml.format:false\").is_ok());\n    }\n\n    #[test]\n    fn merge_format_options() {\n        let format_option_one = FormatOptions::from_str(\"json.indent:2\").unwrap();\n        let format_option_two =\n            FormatOptions::from_str(\"headers.sort:true,headers.sort:false\").unwrap();\n        assert_eq!(\n            format_option_one.merge(&format_option_two),\n            FormatOptions {\n                json_indent: Some(2),\n                json_format: None,\n                xml_indent: None,\n                xml_format: None,\n                headers_sort: Some(false),\n            }\n        )\n    }\n\n    #[test]\n    fn parse_repeated_message_signature_components() {\n        let cli = parse([\n            \"--unstable-m-sig-id=my-key\",\n            \"--unstable-m-sig-key=secret\",\n            \"--unstable-m-sig-comp=@method,@path\",\n            \"--unstable-m-sig-comp=date\",\n            \"get\",\n            \"example.org\",\n        ])\n        .unwrap();\n\n        assert_eq!(cli.m_sig.m_sig_comp.len(), 2);\n        assert_eq!(cli.m_sig.m_sig_comp[0].0, vec![\"@method\", \"@path\"]);\n        assert_eq!(cli.m_sig.m_sig_comp[1].0, vec![\"date\"]);\n    }\n\n    #[test]\n    fn parse_message_signature_algorithm() {\n        let cli = parse([\n            \"--unstable-m-sig-id=my-key\",\n            \"--unstable-m-sig-key=secret\",\n            \"--unstable-m-sig-alg=rsa-v1_5-sha256\",\n            \"get\",\n            \"example.org\",\n        ])\n        .unwrap();\n\n        assert_eq!(\n            cli.m_sig.m_sig_alg,\n            Some(MessageSignatureAlgorithm::RsaV15Sha256)\n        );\n        assert_eq!(\n            cli.m_sig.algorithm(),\n            Some(MessageSignatureAlgorithm::RsaV15Sha256)\n        );\n    }\n\n    #[test]\n    fn parse_resolve() {\n        let invalid_test_cases = [\n            \"example.com:[127.0.0.1]\",\n            \"example.com:80:[::1]\",\n            \"example.com::::1\",\n            \"example.com:1\",\n            \"example.com:example.com\",\n            \"http://example.com:127.0.0.1\",\n            \"http://example.com:[::1]\",\n            \"http://example.com:80:[::1]\",\n        ];\n\n        for input in invalid_test_cases {\n            assert!(Resolve::from_str(input).is_err())\n        }\n\n        assert!(Resolve::from_str(\"example.com:127.0.0.1\").is_ok());\n        assert!(Resolve::from_str(\"example.com:::1\").is_ok());\n        assert!(Resolve::from_str(\"example.com:[::1]\").is_ok());\n    }\n\n    #[test]\n    fn generate() {\n        let cli = parse([\"--generate\", \"complete-bash\"]).unwrap();\n        assert_eq!(cli.generate, Some(Generate::CompleteBash));\n        assert_eq!(cli.raw_method_or_url, None);\n    }\n\n    #[test]\n    fn generate_with_url() {\n        parse([\"--generate\", \"complete-zsh\", \"example.org\"]).unwrap_err();\n    }\n}\n"
  },
  {
    "path": "src/content_disposition.rs",
    "content": "use percent_encoding::percent_decode_str;\n\n/// Parse filename from Content-Disposition header\n/// Prioritizes filename* parameter if present, otherwise uses filename parameter\npub fn parse_filename_from_content_disposition(content_disposition: &str) -> Option<String> {\n    let parts: Vec<&str> = content_disposition\n        .split(';')\n        .map(|part| part.trim())\n        .collect();\n\n    // First try to find filename* parameter\n    for part in parts.iter() {\n        if let Some(value) = part.strip_prefix(\"filename*=\") {\n            if let Some(filename) = parse_encoded_filename(value) {\n                return Some(filename);\n            }\n        }\n    }\n\n    // If filename* is not found or parsing failed, try regular filename parameter\n    for part in parts {\n        if let Some(value) = part.strip_prefix(\"filename=\") {\n            return parse_regular_filename(value);\n        }\n    }\n\n    None\n}\n\n/// Parse regular filename parameter\n/// Handles both quoted and unquoted filenames\nfn parse_regular_filename(filename: &str) -> Option<String> {\n    // Content-Disposition: attachment; filename=\"file with \\\"quotes\\\".txt\"  // This won't occur\n    // Content-Disposition: attachment; filename*=UTF-8''file%20with%20quotes.txt  // This is the actual practice\n    //\n    // We don't need to handle escaped characters in Content-Disposition header parsing because:\n    //\n    // It's not a standard practice\n    // It rarely occurs in real-world scenarios\n    // When filenames contain special characters, they should use the filename* parameter\n\n    // Remove quotes if present\n    let filename = if filename.starts_with('\"') && filename.ends_with('\"') && filename.len() >= 2 {\n        &filename[1..(filename.len() - 1)]\n    } else {\n        filename\n    };\n\n    if filename.is_empty() {\n        return None;\n    }\n\n    Some(filename.to_string())\n}\n\n/// Parse RFC 5987 encoded filename (filename*)\n/// Format: charset'language'encoded-value\nfn parse_encoded_filename(content: &str) -> Option<String> {\n    // Remove \"filename*=\" prefix\n\n    // According to RFC 5987, format should be: charset'language'encoded-value\n    let parts: Vec<&str> = content.splitn(3, '\\'').collect();\n    if parts.len() != 3 {\n        return None;\n    }\n    let charset = parts[0];\n    let encoded_filename = parts[2];\n\n    // Percent-decode the encoded filename into bytes.\n    let decoded_bytes = percent_decode_str(encoded_filename).collect::<Vec<u8>>();\n\n    if charset.eq_ignore_ascii_case(\"UTF-8\") {\n        if let Ok(decoded_str) = String::from_utf8(decoded_bytes) {\n            return Some(decoded_str);\n        }\n    } else if charset.eq_ignore_ascii_case(\"ISO-8859-1\") {\n        // RFC 5987 says to use ISO/IEC 8859-1:1998.\n        // But Firefox and Chromium decode %99 as ™ so they're actually using\n        // Windows-1252. This mixup is common on the web.\n        // This affects the 0x80-0x9F range. According to ISO 8859-1 those are\n        // control characters. According to Windows-1252 most of them are\n        // printable characters.\n        // They agree on all the other characters, and filenames shouldn't have\n        // control characters, so Windows-1252 makes sense.\n        if let Some(decoded_str) = encoding_rs::WINDOWS_1252\n            .decode_without_bom_handling_and_without_replacement(&decoded_bytes)\n        {\n            return Some(decoded_str.into_owned());\n        }\n    } else {\n        // Unknown charset. As a fallback, try interpreting as UTF-8.\n        // Firefox also does this.\n        // Chromium makes up its own filename. (Even if `filename=` is present.)\n        if let Ok(decoded_str) = String::from_utf8(decoded_bytes) {\n            return Some(decoded_str);\n        }\n    }\n\n    None\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_simple_filename() {\n        let header = r#\"attachment; filename=\"example.pdf\"\"#;\n        assert_eq!(\n            parse_filename_from_content_disposition(header),\n            Some(\"example.pdf\".to_string())\n        );\n    }\n\n    #[test]\n    fn test_filename_without_quotes() {\n        let header = \"attachment; filename=example.pdf\";\n        assert_eq!(\n            parse_filename_from_content_disposition(header),\n            Some(\"example.pdf\".to_string())\n        );\n    }\n\n    #[test]\n    fn test_encoded_filename() {\n        // UTF-8 encoded Chinese filename \"测试.pdf\"\n        let header = \"attachment; filename*=UTF-8''%E6%B5%8B%E8%AF%95.pdf\";\n        assert_eq!(\n            parse_filename_from_content_disposition(header),\n            Some(\"测试.pdf\".to_string())\n        );\n    }\n\n    #[test]\n    fn test_both_filenames() {\n        // When both filename and filename* are present, filename* should be preferred\n        let header =\n            r#\"attachment; filename=\"fallback.pdf\"; filename*=UTF-8''%E6%B5%8B%E8%AF%95.pdf\"#;\n        assert_eq!(\n            parse_filename_from_content_disposition(header),\n            Some(\"测试.pdf\".to_string())\n        );\n    }\n    #[test]\n    fn test_decode_with_windows_1252() {\n        let header = \"content-disposition: attachment; filename*=iso-8859-1'en'a%99b\";\n        assert_eq!(\n            parse_filename_from_content_disposition(header),\n            Some(\"a™b\".to_string())\n        );\n    }\n\n    #[test]\n    fn test_both_filenames_with_bad_format() {\n        // When both filename and filename* are present, filename* with bad format, filename should be used\n        let header = r#\"attachment; filename=\"fallback.pdf\"; filename*=UTF-8'bad_format.pdf\"#;\n        assert_eq!(\n            parse_filename_from_content_disposition(header),\n            Some(\"fallback.pdf\".to_string())\n        );\n    }\n\n    #[test]\n    fn test_no_filename() {\n        let header = \"attachment\";\n        assert_eq!(parse_filename_from_content_disposition(header), None);\n    }\n\n    #[test]\n    fn test_iso_8859_1() {\n        let header = \"attachment;filename*=iso-8859-1'en'%A3%20rates\";\n        assert_eq!(\n            parse_filename_from_content_disposition(header),\n            Some(\"£ rates\".to_string())\n        );\n    }\n\n    #[test]\n    fn test_bad_encoding_fallback_to_utf8() {\n        let header = \"attachment;filename*=UTF-16''%E6%B5%8B%E8%AF%95.pdf\";\n        assert_eq!(\n            parse_filename_from_content_disposition(header),\n            Some(\"测试.pdf\".to_string())\n        );\n    }\n}\n"
  },
  {
    "path": "src/decoder.rs",
    "content": "use std::cell::Cell;\nuse std::io::{self, Read};\nuse std::rc::Rc;\nuse std::str::FromStr;\n\nuse brotli::Decompressor as BrotliDecoder;\nuse flate2::read::{GzDecoder, ZlibDecoder};\nuse reqwest::header::{CONTENT_ENCODING, CONTENT_LENGTH, HeaderMap, TRANSFER_ENCODING};\nuse ruzstd::{FrameDecoder, StreamingDecoder as ZstdDecoder};\n\n#[derive(Debug, Clone, Copy)]\npub enum CompressionType {\n    Gzip,\n    Deflate,\n    Brotli,\n    Zstd,\n}\n\nimpl FromStr for CompressionType {\n    type Err = anyhow::Error;\n    fn from_str(value: &str) -> anyhow::Result<CompressionType> {\n        match value {\n            // RFC 2616 section 3.5:\n            //   For compatibility with previous implementations of HTTP,\n            //   applications SHOULD consider \"x-gzip\" and \"x-compress\" to be\n            //   equivalent to \"gzip\" and \"compress\" respectively.\n            \"gzip\" | \"x-gzip\" => Ok(CompressionType::Gzip),\n            \"deflate\" => Ok(CompressionType::Deflate),\n            \"br\" => Ok(CompressionType::Brotli),\n            \"zstd\" => Ok(CompressionType::Zstd),\n            _ => Err(anyhow::anyhow!(\"unknown compression type\")),\n        }\n    }\n}\n\n// See https://github.com/seanmonstar/reqwest/blob/9bd4e90ec3401c2c5bc435c58954f3d52ab53e99/src/async_impl/decoder.rs#L150\npub fn get_compression_type(headers: &HeaderMap) -> Option<CompressionType> {\n    let mut compression_type = headers\n        .get_all(CONTENT_ENCODING)\n        .iter()\n        .find_map(|value| value.to_str().ok().and_then(|value| value.parse().ok()));\n\n    if compression_type.is_none() {\n        compression_type = headers\n            .get_all(TRANSFER_ENCODING)\n            .iter()\n            .find_map(|value| value.to_str().ok().and_then(|value| value.parse().ok()));\n    }\n\n    if compression_type.is_some() {\n        if let Some(content_length) = headers.get(CONTENT_LENGTH) {\n            if content_length == \"0\" {\n                return None;\n            }\n        }\n    }\n\n    compression_type\n}\n\n/// A wrapper that checks whether an error is an I/O error or a decoding error.\n///\n/// The main purpose of this is to suppress decoding errors that happen because\n/// of an empty input. This is behavior we inherited from HTTPie.\n///\n/// It's load-bearing in the case of HEAD requests, where responses don't have a\n/// body but may declare a Content-Encoding.\n///\n/// We also treat other empty response bodies like this, regardless of the request\n/// method. This matches all the user agents I tried (reqwest, requests/HTTPie, curl,\n/// wget, Firefox, Chromium) but I don't know if it's prescribed by any RFC.\n///\n/// As a side benefit we make I/O errors more focused by stripping decoding errors.\n///\n/// The reader is structured like this:\n///\n///      OuterReader ───────┐\n///   compression codec     ├── [Status]\n///     [InnerReader] ──────┘\n///    underlying I/O\n///\n/// The shared Status object is used to communicate.\nstruct OuterReader<'a> {\n    decoder: Box<dyn Read + 'a>,\n    status: Option<Rc<Status>>,\n}\n\nstruct Status {\n    has_read_data: Cell<bool>,\n    read_error: Cell<Option<io::Error>>,\n    error_msg: &'static str,\n}\n\nimpl Read for OuterReader<'_> {\n    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {\n        match self.decoder.read(buf) {\n            Ok(n) => Ok(n),\n            Err(err) => {\n                let Some(ref status) = self.status else {\n                    // No decoder, pass on as is\n                    return Err(err);\n                };\n                match status.read_error.take() {\n                    // If an I/O error happened, return that.\n                    Some(read_error) => Err(read_error),\n                    // If the input was empty, ignore the decoder error.\n                    None if !status.has_read_data.get() => Ok(0),\n                    // Otherwise, decorate the decoder error with a message.\n                    None => Err(io::Error::new(\n                        io::ErrorKind::InvalidData,\n                        DecodeError {\n                            msg: status.error_msg,\n                            err,\n                        },\n                    )),\n                }\n            }\n        }\n    }\n}\n\nstruct InnerReader<R: Read> {\n    reader: R,\n    status: Rc<Status>,\n}\n\nimpl<R: Read> Read for InnerReader<R> {\n    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {\n        self.status.read_error.set(None);\n        match self.reader.read(buf) {\n            Ok(0) => Ok(0),\n            Ok(len) => {\n                self.status.has_read_data.set(true);\n                Ok(len)\n            }\n            Err(err) => {\n                // Store the real error and return a placeholder.\n                // The placeholder is intercepted and replaced by the real error\n                // before leaving this module.\n                // We store the whole error instead of setting a flag because of zstd:\n                // - ZstdDecoder::new() fails with a custom error type and it's hard\n                //   to extract the underlying io::Error\n                // - ZstdDecoder::read() (unlike the other decoders) wraps custom errors\n                //   around the underlying io::Error\n                let msg = err.to_string();\n                let kind = err.kind();\n                self.status.read_error.set(Some(err));\n                Err(io::Error::new(kind, msg))\n            }\n        }\n    }\n}\n\n#[derive(Debug)]\nstruct DecodeError {\n    msg: &'static str,\n    err: io::Error,\n}\n\nimpl std::fmt::Display for DecodeError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.write_str(self.msg)\n    }\n}\n\nimpl std::error::Error for DecodeError {\n    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {\n        Some(&self.err)\n    }\n}\n\npub fn decompress(\n    reader: &mut impl Read,\n    compression_type: Option<CompressionType>,\n) -> impl Read + '_ {\n    let Some(compression_type) = compression_type else {\n        return OuterReader {\n            decoder: Box::new(reader),\n            status: None,\n        };\n    };\n\n    let status = Rc::new(Status {\n        has_read_data: Cell::new(false),\n        read_error: Cell::new(None),\n        error_msg: match compression_type {\n            CompressionType::Gzip => \"error decoding gzip response body\",\n            CompressionType::Deflate => \"error decoding deflate response body\",\n            CompressionType::Brotli => \"error decoding brotli response body\",\n            CompressionType::Zstd => \"error decoding zstd response body\",\n        },\n    });\n    let reader = InnerReader {\n        reader,\n        status: Rc::clone(&status),\n    };\n    OuterReader {\n        decoder: match compression_type {\n            CompressionType::Gzip => Box::new(GzDecoder::new(reader)),\n            CompressionType::Deflate => Box::new(ZlibDecoder::new(reader)),\n            // 32K is the default buffer size for gzip and deflate\n            CompressionType::Brotli => Box::new(BrotliDecoder::new(reader, 32 * 1024)),\n            CompressionType::Zstd => Box::new(LazyZstdDecoder::Uninit(Some(reader))),\n        },\n        status: Some(status),\n    }\n}\n\n/// [ZstdDecoder] reads from its input during construction.\n///\n/// We need to delay construction until [Read] so read errors stay read errors.\n#[allow(clippy::large_enum_variant)]\nenum LazyZstdDecoder<R: Read> {\n    Uninit(Option<R>),\n    Init(ZstdDecoder<R, FrameDecoder>),\n}\n\nimpl<R: Read> Read for LazyZstdDecoder<R> {\n    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {\n        match self {\n            LazyZstdDecoder::Uninit(reader) => match reader.take() {\n                Some(reader) => match ZstdDecoder::new(reader) {\n                    Ok(decoder) => {\n                        *self = LazyZstdDecoder::Init(decoder);\n                        self.read(buf)\n                    }\n                    Err(err) => Err(io::Error::other(err)),\n                },\n                // We seem to get here in --stream mode because another layer tries\n                // to read again after Ok(0).\n                None => Err(io::Error::other(\"failed to construct ZstdDecoder\")),\n            },\n            LazyZstdDecoder::Init(streaming_decoder) => streaming_decoder.read(buf),\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use std::error::Error;\n\n    use super::*;\n\n    #[test]\n    fn decode_errors_are_prepended_with_custom_message() {\n        let uncompressed_data = String::from(\"Hello world\");\n        let mut uncompressed_data = uncompressed_data.as_bytes();\n        let mut reader = decompress(&mut uncompressed_data, Some(CompressionType::Gzip));\n        let mut buffer = Vec::new();\n        match reader.read_to_end(&mut buffer) {\n            Ok(_) => unreachable!(\"gzip should fail to decompress an uncompressed data\"),\n            Err(e) => {\n                assert!(\n                    e.to_string()\n                        .starts_with(\"error decoding gzip response body\")\n                )\n            }\n        }\n    }\n\n    #[test]\n    fn underlying_read_errors_are_not_modified() {\n        struct SadReader;\n        impl Read for SadReader {\n            fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {\n                Err(io::Error::other(\"oh no!\"))\n            }\n        }\n\n        let mut sad_reader = SadReader;\n        let mut reader = decompress(&mut sad_reader, Some(CompressionType::Gzip));\n        let mut buffer = Vec::new();\n        match reader.read_to_end(&mut buffer) {\n            Ok(_) => unreachable!(\"SadReader should never be read\"),\n            Err(e) => {\n                assert!(e.to_string().starts_with(\"oh no!\"))\n            }\n        }\n    }\n\n    #[test]\n    fn interrupts_are_handled_gracefully() {\n        struct InterruptedReader {\n            step: u8,\n        }\n        impl Read for InterruptedReader {\n            fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {\n                self.step += 1;\n                match self.step {\n                    1 => Read::read(&mut b\"abc\".as_slice(), buf),\n                    2 => Err(io::Error::new(io::ErrorKind::Interrupted, \"interrupted\")),\n                    3 => Read::read(&mut b\"def\".as_slice(), buf),\n                    _ => Ok(0),\n                }\n            }\n        }\n\n        for compression_type in [\n            None,\n            Some(CompressionType::Brotli),\n            Some(CompressionType::Deflate),\n            Some(CompressionType::Gzip),\n            Some(CompressionType::Zstd),\n        ] {\n            let mut base_reader = InterruptedReader { step: 0 };\n            let mut reader = decompress(&mut base_reader, compression_type);\n            let mut buffer = Vec::with_capacity(16);\n            let res = reader.read_to_end(&mut buffer);\n            if compression_type.is_none() {\n                res.unwrap();\n                assert_eq!(buffer, b\"abcdef\");\n            } else {\n                res.unwrap_err();\n            }\n        }\n    }\n\n    #[test]\n    fn empty_inputs_do_not_cause_errors() {\n        for compression_type in [\n            None,\n            Some(CompressionType::Brotli),\n            Some(CompressionType::Deflate),\n            Some(CompressionType::Gzip),\n            Some(CompressionType::Zstd),\n        ] {\n            let mut input: &[u8] = b\"\";\n            let mut reader = decompress(&mut input, compression_type);\n            let mut buf = Vec::new();\n            reader.read_to_end(&mut buf).unwrap();\n            assert_eq!(buf, b\"\");\n\n            // Must accept repeated read attempts after EOF (this happens with --stream)\n            for _ in 0..10 {\n                reader.read_to_end(&mut buf).unwrap();\n                assert_eq!(buf, b\"\");\n            }\n        }\n    }\n\n    #[test]\n    fn read_errors_keep_their_context() {\n        #[derive(Debug)]\n        struct SpecialErr;\n        impl std::fmt::Display for SpecialErr {\n            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n                write!(f, \"{self:?}\")\n            }\n        }\n        impl std::error::Error for SpecialErr {}\n\n        struct SadReader;\n        impl Read for SadReader {\n            fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {\n                Err(io::Error::new(io::ErrorKind::WouldBlock, SpecialErr))\n            }\n        }\n\n        for compression_type in [\n            None,\n            Some(CompressionType::Brotli),\n            Some(CompressionType::Deflate),\n            Some(CompressionType::Gzip),\n            Some(CompressionType::Zstd),\n        ] {\n            let mut input = SadReader;\n            let mut reader = decompress(&mut input, compression_type);\n            let mut buf = Vec::new();\n            let err = reader.read_to_end(&mut buf).unwrap_err();\n            assert_eq!(err.kind(), io::ErrorKind::WouldBlock);\n            err.get_ref().unwrap().downcast_ref::<SpecialErr>().unwrap();\n        }\n    }\n\n    #[test]\n    fn true_decode_errors_are_preserved() {\n        for compression_type in [\n            CompressionType::Brotli,\n            CompressionType::Deflate,\n            CompressionType::Gzip,\n            CompressionType::Zstd,\n        ] {\n            let mut input: &[u8] = b\"bad\";\n            let mut reader = decompress(&mut input, Some(compression_type));\n            let mut buf = Vec::new();\n            let err = reader.read_to_end(&mut buf).unwrap_err();\n\n            assert_eq!(err.kind(), io::ErrorKind::InvalidData);\n            let decode_err = err\n                .get_ref()\n                .unwrap()\n                .downcast_ref::<DecodeError>()\n                .unwrap();\n            let real_err = decode_err.source().unwrap();\n            let real_err = real_err.downcast_ref::<io::Error>().unwrap();\n\n            // All four decoders make a different choice here...\n            // Still the easiest way to check that we're preserving the error\n            let expected_kind = match compression_type {\n                CompressionType::Gzip => io::ErrorKind::UnexpectedEof,\n                CompressionType::Deflate => io::ErrorKind::InvalidInput,\n                CompressionType::Brotli => io::ErrorKind::InvalidData,\n                CompressionType::Zstd => io::ErrorKind::Other,\n            };\n            assert_eq!(real_err.kind(), expected_kind);\n        }\n    }\n}\n"
  },
  {
    "path": "src/download.rs",
    "content": "use std::fs::{self, File, OpenOptions};\nuse std::io::{self, ErrorKind, IsTerminal};\nuse std::path::{Path, PathBuf};\nuse std::time::Instant;\n\nuse crate::content_disposition;\nuse crate::decoder::{decompress, get_compression_type};\nuse crate::utils::{HeaderValueExt, copy_largebuf, test_pretend_term};\nuse anyhow::{Context, Result, anyhow};\nuse indicatif::{HumanBytes, ProgressBar, ProgressStyle};\nuse mime2ext::mime2ext;\nuse regex_lite::Regex;\nuse reqwest::{\n    StatusCode,\n    blocking::Response,\n    header::{CONTENT_DISPOSITION, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, HeaderMap},\n};\n\nfn get_content_length(headers: &HeaderMap) -> Option<u64> {\n    headers\n        .get(CONTENT_LENGTH)\n        .and_then(|v| v.to_str().ok())\n        .and_then(|s| s.parse::<u64>().ok())\n}\n\n// This function is system-agnostic, so it's ok for it to use Strings instead\n// of PathBufs\nfn get_file_name(response: &Response, orig_url: &reqwest::Url) -> String {\n    fn from_header(response: &Response) -> Option<String> {\n        let header = response\n            .headers()\n            .get(CONTENT_DISPOSITION)?\n            .to_utf8_str()\n            .ok()?;\n        content_disposition::parse_filename_from_content_disposition(header)\n    }\n\n    fn from_url(url: &reqwest::Url) -> Option<String> {\n        let last_seg = url\n            .path_segments()?\n            .rev()\n            .find(|segment| !segment.is_empty())?;\n        Some(last_seg.to_string())\n    }\n\n    fn guess_extension(response: &Response) -> Option<&'static str> {\n        let mimetype = response.headers().get(CONTENT_TYPE)?.to_str().ok()?;\n        mime2ext(mimetype)\n    }\n\n    let filename = from_header(response)\n        .or_else(|| from_url(orig_url))\n        .unwrap_or_else(|| \"index\".to_string());\n\n    let filename = sanitize_filename::sanitize_with_options(\n        &filename,\n        sanitize_filename::Options {\n            replacement: \"_\",\n            ..Default::default()\n        },\n    );\n\n    let mut filename = filename.trim().trim_start_matches('.').to_string();\n\n    if !filename.contains('.') {\n        if let Some(extension) = guess_extension(response) {\n            filename.push('.');\n            filename.push_str(extension);\n        }\n    }\n\n    filename\n}\n\npub fn get_file_size(path: Option<&Path>) -> Option<u64> {\n    Some(fs::metadata(path?).ok()?.len())\n}\n\n/// Find a file name that doesn't exist yet.\nfn open_new_file(file_name: PathBuf) -> io::Result<(PathBuf, File)> {\n    fn try_open_new(file_name: &Path) -> io::Result<Option<File>> {\n        match OpenOptions::new()\n            .write(true)\n            .create_new(true)\n            .open(file_name)\n        {\n            Ok(file) => Ok(Some(file)),\n            Err(err) if err.kind() == ErrorKind::AlreadyExists => Ok(None),\n            Err(err) => Err(err),\n        }\n    }\n    if let Some(file) = try_open_new(&file_name)? {\n        return Ok((file_name, file));\n    }\n    for suffix in 1..u32::MAX {\n        let candidate = {\n            let mut candidate = file_name.clone().into_os_string();\n            candidate.push(format!(\"-{suffix}\"));\n            PathBuf::from(candidate)\n        };\n        if let Some(file) = try_open_new(&candidate)? {\n            return Ok((candidate, file));\n        }\n    }\n    panic!(\"Could not create file after unreasonable number of attempts\");\n}\n\n// https://github.com/httpie/httpie/blob/84c7327057/httpie/downloads.py#L44\n// https://tools.ietf.org/html/rfc7233#section-4.2\nfn total_for_content_range(header: &str, expected_start: u64) -> Result<u64> {\n    let re_range = Regex::new(concat!(\n        r\"^bytes (?P<first_byte_pos>\\d+)-(?P<last_byte_pos>\\d+)\",\n        r\"/(?:\\*|(?P<complete_length>\\d+))$\"\n    ))\n    .unwrap();\n    let caps = re_range\n        .captures(header)\n        // Could happen if header uses unit other than bytes\n        .ok_or_else(|| anyhow!(\"Can't parse Content-Range header, can't resume download\"))?;\n    let first_byte_pos: u64 = caps\n        .name(\"first_byte_pos\")\n        .unwrap()\n        .as_str()\n        .parse()\n        .context(\"Can't parse Content-Range first_byte_pos\")?;\n    let last_byte_pos: u64 = caps\n        .name(\"last_byte_pos\")\n        .unwrap()\n        .as_str()\n        .parse()\n        .context(\"Can't parse Content-Range last_byte_pos\")?;\n    let complete_length: Option<u64> = caps\n        .name(\"complete_length\")\n        .map(|num| {\n            num.as_str()\n                .parse()\n                .context(\"Can't parse Content-Range complete_length\")\n        })\n        .transpose()?;\n    // Note that last_byte_pos must be strictly less than complete_length\n    // If first_byte_pos == last_byte_pos exactly one byte is sent\n    if first_byte_pos > last_byte_pos {\n        return Err(anyhow!(\"Invalid Content-Range: {:?}\", header));\n    }\n    if let Some(complete_length) = complete_length {\n        if last_byte_pos >= complete_length {\n            return Err(anyhow!(\"Invalid Content-Range: {:?}\", header));\n        }\n        if complete_length != last_byte_pos + 1 {\n            return Err(anyhow!(\"Content-Range has wrong end: {:?}\", header));\n        }\n    }\n    if expected_start != first_byte_pos {\n        return Err(anyhow!(\"Content-Range has wrong start: {:?}\", header));\n    }\n    Ok(last_byte_pos + 1)\n}\n\nconst BAR_TEMPLATE: &str =\n    \"{spinner:.green} {percent}% [{wide_bar:.cyan/blue}] {bytes} {bytes_per_sec} ETA {eta}\";\nconst UNCOLORED_BAR_TEMPLATE: &str =\n    \"{spinner} {percent}% [{wide_bar}] {bytes} {bytes_per_sec} ETA {eta}\";\nconst SPINNER_TEMPLATE: &str = \"{spinner:.green} {bytes} {bytes_per_sec} {wide_msg}\";\nconst UNCOLORED_SPINNER_TEMPLATE: &str = \"{spinner} {bytes} {bytes_per_sec} {wide_msg}\";\n\npub fn download_file(\n    mut response: Response,\n    file_name: Option<PathBuf>,\n    // If we fall back on taking the filename from the URL it has to be the\n    // original URL, before redirects. That's less surprising and matches\n    // HTTPie. Hence this argument.\n    orig_url: &reqwest::Url,\n    mut resume: Option<u64>,\n    color: bool,\n    quiet: bool,\n) -> Result<()> {\n    if resume.is_some() && response.status() != StatusCode::PARTIAL_CONTENT {\n        resume = None;\n    }\n\n    let mut buffer: Box<dyn io::Write>;\n    let dest_name: PathBuf;\n\n    if let Some(file_name) = file_name {\n        let mut open_opts = OpenOptions::new();\n        open_opts.write(true).create(true);\n        if resume.is_some() {\n            open_opts.append(true);\n        } else {\n            open_opts.truncate(true);\n        }\n\n        dest_name = file_name;\n        buffer = Box::new(open_opts.open(&dest_name)?);\n    } else if test_pretend_term() || io::stdout().is_terminal() {\n        let (new_name, handle) = open_new_file(get_file_name(&response, orig_url).into())?;\n        dest_name = new_name;\n        buffer = Box::new(handle);\n    } else {\n        dest_name = \"<stdout>\".into();\n        buffer = Box::new(io::stdout());\n    }\n\n    let starting_length: u64;\n    let total_length: Option<u64>;\n    if let Some(resume) = resume {\n        let header = response\n            .headers()\n            .get(CONTENT_RANGE)\n            .ok_or_else(|| anyhow!(\"Missing Content-Range header\"))?\n            .to_str()\n            .map_err(|_| anyhow!(\"Bad Content-Range header\"))?;\n        starting_length = resume;\n        total_length = Some(total_for_content_range(header, starting_length)?);\n    } else {\n        starting_length = 0;\n        total_length = get_content_length(response.headers());\n    }\n\n    let starting_time = Instant::now();\n\n    let pb = if quiet {\n        None\n    } else if let Some(total_length) = total_length {\n        eprintln!(\n            \"Downloading {} to {:?}\",\n            HumanBytes(total_length - starting_length),\n            dest_name\n        );\n        let style = ProgressStyle::default_bar()\n            .template(if color {\n                BAR_TEMPLATE\n            } else {\n                UNCOLORED_BAR_TEMPLATE\n            })?\n            .progress_chars(\"#>-\");\n        Some(ProgressBar::new(total_length).with_style(style))\n    } else {\n        eprintln!(\"Downloading to {dest_name:?}\");\n        let style = ProgressStyle::default_bar().template(if color {\n            SPINNER_TEMPLATE\n        } else {\n            UNCOLORED_SPINNER_TEMPLATE\n        })?;\n        Some(ProgressBar::new_spinner().with_style(style))\n    };\n    if let Some(pb) = &pb {\n        pb.set_position(starting_length);\n        pb.reset_eta();\n    }\n\n    match pb {\n        Some(ref pb) => {\n            let compression_type = get_compression_type(response.headers());\n            copy_largebuf(\n                &mut decompress(&mut pb.wrap_read(response), compression_type),\n                &mut buffer,\n                false,\n            )?;\n            let downloaded_length = pb.position() - starting_length;\n            pb.finish_and_clear();\n            let time_taken = starting_time.elapsed();\n            if !time_taken.is_zero() {\n                eprintln!(\n                    \"Done. {} in {:.5}s ({}/s)\",\n                    HumanBytes(downloaded_length),\n                    time_taken.as_secs_f64(),\n                    HumanBytes((downloaded_length as f64 / time_taken.as_secs_f64()) as u64)\n                );\n            } else {\n                eprintln!(\"Done. {}\", HumanBytes(downloaded_length));\n            }\n        }\n        None => {\n            let compression_type = get_compression_type(response.headers());\n            copy_largebuf(\n                &mut decompress(&mut response, compression_type),\n                &mut buffer,\n                false,\n            )?;\n        }\n    }\n\n    Ok(())\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn content_range_parsing() {\n        let expected = vec![\n            (2, \"bytes 2-5/6\", Some(6)),\n            (2, \"bytes 2-5/*\", Some(6)),\n            (5, \"bytes 5-5/6\", Some(6)),\n            (2, \"bytes 3-5/6\", None),\n            (2, \"bytes 1-5/6\", None),\n            (2, \"bytes 2-4/6\", None),\n            (2, \"bytes 2-6/6\", None),\n        ];\n        for (start, header, result) in expected {\n            assert_eq!(total_for_content_range(header, start).ok(), result);\n        }\n    }\n}\n"
  },
  {
    "path": "src/error_reporting.rs",
    "content": "use std::process::ExitCode;\n\npub(crate) fn additional_messages(err: &anyhow::Error, native_tls: bool) -> Vec<String> {\n    let mut msgs = Vec::new();\n\n    #[cfg(feature = \"rustls\")]\n    msgs.extend(format_rustls_error(err));\n\n    if native_tls && err.root_cause().to_string() == \"invalid minimum TLS version for backend\" {\n        msgs.push(\"Try running without the --native-tls flag.\".into());\n    }\n\n    msgs\n}\n\n/// Format certificate expired/not valid yet messages. By default these print\n/// human-unfriendly Unix timestamps.\n///\n/// Other rustls error messages (e.g. wrong host) are readable enough.\n///\n/// Note this only works on platforms where rustls-platform-verifier uses webpki for verification.\n#[cfg(feature = \"rustls\")]\nfn format_rustls_error(err: &anyhow::Error) -> Option<String> {\n    use humantime::format_duration;\n    use rustls::CertificateError;\n    use rustls::pki_types::UnixTime;\n    use time::OffsetDateTime;\n\n    // Multiple layers of io::Error for some reason?\n    // This may be fragile\n    let err = err.root_cause().downcast_ref::<std::io::Error>()?;\n    let err = err.get_ref()?.downcast_ref::<std::io::Error>()?;\n    let err = err.get_ref()?.downcast_ref::<rustls::Error>()?;\n    let rustls::Error::InvalidCertificate(err) = err else {\n        return None;\n    };\n\n    fn conv_time(unix_time: &UnixTime) -> Option<OffsetDateTime> {\n        OffsetDateTime::from_unix_timestamp(unix_time.as_secs() as i64).ok()\n    }\n\n    match err {\n        CertificateError::ExpiredContext { time, not_after } => {\n            let time = conv_time(time)?;\n            let not_after = conv_time(not_after)?;\n            let diff = format_duration((time - not_after).try_into().ok()?);\n            Some(format!(\n                \"Certificate not valid after {not_after} ({diff} ago).\",\n            ))\n        }\n        CertificateError::NotValidYetContext { time, not_before } => {\n            let time = conv_time(time)?;\n            let not_before = conv_time(not_before)?;\n            let diff = format_duration((not_before - time).try_into().ok()?);\n            Some(format!(\n                \"Certificate not valid before {not_before} ({diff} from now).\",\n            ))\n        }\n        _ => None,\n    }\n}\n\npub(crate) fn exit_code(err: &anyhow::Error) -> ExitCode {\n    if let Some(err) = err.downcast_ref::<reqwest::Error>() {\n        if err.is_timeout() {\n            return ExitCode::from(2);\n        }\n    }\n\n    if err\n        .downcast_ref::<crate::redirect::TooManyRedirects>()\n        .is_some()\n    {\n        return ExitCode::from(6);\n    }\n\n    ExitCode::FAILURE\n}\n"
  },
  {
    "path": "src/formatting/headers.rs",
    "content": "use std::io::Result;\n\nuse reqwest::{\n    Method, StatusCode, Version,\n    header::{HeaderMap, HeaderName, HeaderValue},\n};\nuse syntect::highlighting::Theme;\nuse termcolor::WriteColor;\nuse url::Url;\n\nuse crate::utils::HeaderValueExt;\n\nsuper::palette::palette! {\n    struct HeaderPalette {\n        http_keyword: [\"keyword.other.http\"],\n        http_separator: [\"punctuation.separator.http\"],\n        http_version: [\"constant.numeric.http\"],\n        method: [\"keyword.control.http\"],\n        path: [\"const.language.http\"],\n        status_code: [\"constant.numeric.http\"],\n        status_reason: [\"keyword.reason.http\"],\n        header_name: [\"source.http\", \"http.requestheaders\", \"support.variable.http\"],\n        header_colon: [\"source.http\", \"http.requestheaders\", \"punctuation.separator.http\"],\n        header_value: [\"source.http\", \"http.requestheaders\", \"string.other.http\"],\n        error: [\"error\"],\n    }\n}\n\nmacro_rules! set_color {\n    ($self:ident, $color:ident) => {\n        if let Some(ref palette) = $self.palette {\n            $self.output.set_color(&palette.$color)\n        } else {\n            Ok(())\n        }\n    };\n}\n\npub(crate) struct HeaderFormatter<'a, W: WriteColor> {\n    output: &'a mut W,\n    palette: Option<HeaderPalette>,\n    is_terminal: bool,\n    sort_headers: bool,\n}\n\nimpl<'a, W: WriteColor> HeaderFormatter<'a, W> {\n    pub(crate) fn new(\n        output: &'a mut W,\n        theme: Option<&Theme>,\n        is_terminal: bool,\n        sort_headers: bool,\n    ) -> Self {\n        Self {\n            palette: theme.map(HeaderPalette::from),\n            output,\n            is_terminal,\n            sort_headers,\n        }\n    }\n\n    fn print(&mut self, text: &str) -> Result<()> {\n        self.output.write_all(text.as_bytes())\n    }\n\n    fn print_plain(&mut self, text: &str) -> Result<()> {\n        set_color!(self, default)?;\n        self.print(text)\n    }\n\n    pub(crate) fn print_request_headers(\n        &mut self,\n        method: &Method,\n        url: &Url,\n        version: Version,\n        headers: &HeaderMap,\n    ) -> Result<()> {\n        set_color!(self, method)?;\n        self.print(method.as_str())?;\n\n        self.print_plain(\" \")?;\n\n        set_color!(self, path)?;\n        self.print(url.path())?;\n        if let Some(query) = url.query() {\n            self.print(\"?\")?;\n            self.print(query)?;\n        }\n\n        self.print_plain(\" \")?;\n        self.print_http_version(version)?;\n\n        self.print_plain(\"\\n\")?;\n        self.print_headers(headers, version)?;\n\n        if self.palette.is_some() {\n            self.output.reset()?;\n        }\n        Ok(())\n    }\n\n    pub(crate) fn print_response_headers(\n        &mut self,\n        version: Version,\n        status: StatusCode,\n        reason_phrase: &str,\n        headers: &HeaderMap,\n    ) -> Result<()> {\n        self.print_http_version(version)?;\n\n        self.print_plain(\" \")?;\n\n        set_color!(self, status_code)?;\n        self.print(status.as_str())?;\n\n        self.print_plain(\" \")?;\n\n        set_color!(self, status_reason)?;\n        self.print(reason_phrase)?;\n\n        self.print_plain(\"\\n\")?;\n\n        self.print_headers(headers, version)?;\n\n        if self.palette.is_some() {\n            self.output.reset()?;\n        }\n        Ok(())\n    }\n\n    fn print_http_version(&mut self, version: Version) -> Result<()> {\n        let version = format!(\"{version:?}\");\n        let version = version.strip_prefix(\"HTTP/\").unwrap_or(&version);\n\n        set_color!(self, http_keyword)?;\n        self.print(\"HTTP\")?;\n        set_color!(self, http_separator)?;\n        self.print(\"/\")?;\n        set_color!(self, http_version)?;\n        self.print(version)?;\n\n        Ok(())\n    }\n\n    fn print_headers(&mut self, headers: &HeaderMap, version: Version) -> Result<()> {\n        let as_titlecase = match version {\n            Version::HTTP_09 | Version::HTTP_10 | Version::HTTP_11 => true,\n            Version::HTTP_2 | Version::HTTP_3 => false,\n            _ => false,\n        };\n        let mut headers: Vec<(&HeaderName, &HeaderValue)> = headers.iter().collect();\n        if self.sort_headers {\n            headers.sort_by_key(|(name, _)| name.as_str());\n        }\n\n        let mut namebuf = String::with_capacity(64);\n        for (name, value) in headers {\n            let key = if as_titlecase {\n                titlecase_header(name, &mut namebuf)\n            } else {\n                name.as_str()\n            };\n\n            set_color!(self, header_name)?;\n            self.print(key)?;\n            set_color!(self, header_colon)?;\n            self.print(\":\")?;\n            self.print_plain(\" \")?;\n\n            match value.to_ascii_or_latin1() {\n                Ok(ascii) => {\n                    set_color!(self, header_value)?;\n                    self.print(ascii)?;\n                }\n                Err(bad) => {\n                    const FAQ_URL: &str =\n                        \"https://github.com/ducaale/xh/blob/master/FAQ.md#header-value-encoding\";\n\n                    let mut latin1 = bad.latin1();\n                    if self.is_terminal {\n                        latin1 = sanitize_header_value(&latin1);\n                    }\n                    set_color!(self, error)?;\n                    self.print(&latin1)?;\n\n                    if let Some(utf8) = bad.utf8() {\n                        set_color!(self, default)?;\n                        if self.palette.is_some() && super::supports_hyperlinks() {\n                            self.print(\" (\")?;\n                            self.print(&super::create_hyperlink(\"UTF-8\", FAQ_URL))?;\n                            self.print(\": \")?;\n                        } else {\n                            self.print(\" (UTF-8: \")?;\n                        }\n\n                        set_color!(self, header_value)?;\n                        // We could escape these as well but latin1 has a much higher chance\n                        // to contain control characters because:\n                        // - ~14% of the possible latin1 codepoints are control characters,\n                        //   versus <0.1% for UTF-8.\n                        // - The latin1 text may not be intended as latin1, but if it's valid\n                        //   as UTF-8 then chances are that it really is UTF-8.\n                        // We should revisit this if we come up with a general policy for\n                        // escaping control characters, not just in headers.\n                        self.print(utf8)?;\n                        self.print_plain(\")\")?;\n                    }\n                }\n            }\n            self.print_plain(\"\\n\")?;\n        }\n\n        Ok(())\n    }\n}\n\nfn titlecase_header<'b>(name: &HeaderName, buffer: &'b mut String) -> &'b str {\n    let name = name.as_str();\n    buffer.clear();\n    buffer.reserve(name.len());\n    // Ought to be equivalent to how hyper does it\n    // https://github.com/hyperium/hyper/blob/f46b175bf71b202fbb907c4970b5743881b891e1/src/proto/h1/role.rs#L1332\n    // Header names are ASCII so operating on char or u8 is equivalent\n    let mut prev = '-';\n    for mut c in name.chars() {\n        if prev == '-' {\n            c.make_ascii_uppercase();\n        }\n        buffer.push(c);\n        prev = c;\n    }\n    buffer\n}\n\n/// Escape control characters. Firefox uses Unicode replacement characters,\n/// that seems like a good choice.\n///\n/// Header values can't contain ASCII control characters (like newlines)\n/// but if misencoded they frequently contain latin1 control characters.\n/// What we do here might not make sense for other strings.\nfn sanitize_header_value(value: &str) -> String {\n    const REPLACEMENT_CHARACTER: &str = \"\\u{FFFD}\";\n    value.replace(char::is_control, REPLACEMENT_CHARACTER)\n}\n\n#[cfg(test)]\nmod tests {\n    use indoc::indoc;\n\n    use super::*;\n\n    #[test]\n    fn test_header_casing() {\n        let mut headers = HeaderMap::new();\n        headers.insert(\"ab-cd\", \"0\".parse().unwrap());\n        headers.insert(\"-cd\", \"0\".parse().unwrap());\n        headers.insert(\"-\", \"0\".parse().unwrap());\n        headers.insert(\"ab-%c\", \"0\".parse().unwrap());\n        headers.insert(\"A-b--C\", \"0\".parse().unwrap());\n\n        let mut buf = termcolor::Ansi::new(Vec::new());\n        let mut formatter = HeaderFormatter::new(&mut buf, None, false, false);\n        formatter.print_headers(&headers, Version::HTTP_11).unwrap();\n        let buf = buf.into_inner();\n        assert_eq!(\n            buf,\n            indoc! {b\"\n                Ab-Cd: 0\n                -Cd: 0\n                -: 0\n                Ab-%c: 0\n                A-B--C: 0\n                \"\n            }\n        );\n\n        let mut buf = termcolor::Ansi::new(Vec::new());\n        let mut formatter = HeaderFormatter::new(&mut buf, None, false, false);\n        formatter.print_headers(&headers, Version::HTTP_2).unwrap();\n        let buf = buf.into_inner();\n        assert_eq!(\n            buf,\n            indoc! {b\"\n                ab-cd: 0\n                -cd: 0\n                -: 0\n                ab-%c: 0\n                a-b--c: 0\n                \"\n            }\n        );\n    }\n}\n"
  },
  {
    "path": "src/formatting/mod.rs",
    "content": "use std::{\n    io::{self, Write},\n    sync::{LazyLock, OnceLock},\n};\n\nuse quick_xml::events::Event;\nuse quick_xml::{Reader, Writer};\nuse syntect::dumps::from_binary;\nuse syntect::easy::HighlightLines;\nuse syntect::highlighting::ThemeSet;\nuse syntect::parsing::SyntaxSet;\nuse syntect::util::LinesWithEndings;\nuse termcolor::WriteColor;\n\nuse crate::{buffer::Buffer, cli::Theme};\n\npub(crate) mod headers;\npub(crate) mod palette;\n\npub fn get_json_formatter(indent_level: usize) -> jsonxf::Formatter {\n    let mut fmt = jsonxf::Formatter::pretty_printer();\n    fmt.indent = \" \".repeat(indent_level);\n    fmt.record_separator = String::from(\"\\n\\n\");\n    fmt.eager_record_separators = true;\n    fmt\n}\n\n/// Pretty-print an XML document. Whitespace-only text nodes (typically existing\n/// indentation) are stripped so that already-formatted XML gets re-indented cleanly.\npub fn format_xml(indent: usize, text: &str) -> io::Result<Vec<u8>> {\n    let mut reader = Reader::from_str(text);\n    let mut writer = Writer::new_with_indent(Vec::new(), b' ', indent);\n    loop {\n        match reader.read_event() {\n            Ok(Event::Eof) => break,\n            Ok(Event::Text(ref e)) if e.iter().all(|b| b.is_ascii_whitespace()) => {}\n            Ok(event) => writer.write_event(event)?,\n            Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e)),\n        }\n    }\n    Ok(writer.into_inner())\n}\n\n/// Format a JSON value using serde. Unlike jsonxf this decodes escaped Unicode values.\n///\n/// Note that if parsing fails this function will stop midway through and return an error.\n/// It should only be used with known-valid JSON.\npub fn serde_json_format(indent_level: usize, text: &str, write: impl Write) -> io::Result<()> {\n    let indent = \" \".repeat(indent_level);\n    let formatter = serde_json::ser::PrettyFormatter::with_indent(indent.as_bytes());\n    let mut serializer = serde_json::Serializer::with_formatter(write, formatter);\n    let mut deserializer = serde_json::Deserializer::from_str(text);\n    serde_transcode::transcode(&mut deserializer, &mut serializer)?;\n    Ok(())\n}\n\npub(crate) static THEMES: LazyLock<ThemeSet> = LazyLock::new(|| {\n    from_binary(include_bytes!(concat!(\n        env!(\"OUT_DIR\"),\n        \"/themepack.themedump\"\n    )))\n});\nstatic PS_BASIC: LazyLock<SyntaxSet> =\n    LazyLock::new(|| from_binary(include_bytes!(concat!(env!(\"OUT_DIR\"), \"/basic.packdump\"))));\nstatic PS_LARGE: LazyLock<SyntaxSet> =\n    LazyLock::new(|| from_binary(include_bytes!(concat!(env!(\"OUT_DIR\"), \"/large.packdump\"))));\n\npub struct Highlighter<'a> {\n    highlighter: HighlightLines<'static>,\n    syntax_set: &'static SyntaxSet,\n    out: &'a mut Buffer,\n}\n\n/// A wrapper around a [`Buffer`] to add syntax highlighting when printing.\nimpl<'a> Highlighter<'a> {\n    pub fn new(syntax: &'static str, theme: Theme, out: &'a mut Buffer) -> Self {\n        let syntax_set: &SyntaxSet = match syntax {\n            \"json\" => &PS_BASIC,\n            _ => &PS_LARGE,\n        };\n        let syntax = syntax_set\n            .find_syntax_by_extension(syntax)\n            .expect(\"syntax not found\");\n        Self {\n            highlighter: HighlightLines::new(syntax, theme.as_syntect_theme()),\n            syntax_set,\n            out,\n        }\n    }\n\n    /// Write a single piece of highlighted text.\n    /// May return a [`io::ErrorKind::Other`] when there is a problem\n    /// during highlighting.\n    pub fn highlight(&mut self, text: &str) -> io::Result<()> {\n        for line in LinesWithEndings::from(text) {\n            for (style, component) in self\n                .highlighter\n                .highlight_line(line, self.syntax_set)\n                .map_err(io::Error::other)?\n            {\n                self.out.set_color(&convert_style(style))?;\n                write!(self.out, \"{component}\")?;\n            }\n        }\n        Ok(())\n    }\n\n    pub fn highlight_bytes(&mut self, line: &[u8]) -> io::Result<()> {\n        self.highlight(&String::from_utf8_lossy(line))\n    }\n\n    pub fn flush(&mut self) -> io::Result<()> {\n        self.out.flush()\n    }\n}\n\nimpl Drop for Highlighter<'_> {\n    fn drop(&mut self) {\n        // This is just a best-effort attempt to restore the terminal, failure can be ignored\n        let _ = self.out.reset();\n    }\n}\n\nfn convert_style(style: syntect::highlighting::Style) -> termcolor::ColorSpec {\n    use syntect::highlighting::FontStyle;\n    let mut spec = termcolor::ColorSpec::new();\n    spec.set_fg(convert_color(style.foreground))\n        .set_underline(style.font_style.contains(FontStyle::UNDERLINE))\n        .set_bold(style.font_style.contains(FontStyle::BOLD))\n        .set_italic(style.font_style.contains(FontStyle::ITALIC));\n    spec\n}\n\n// https://github.com/sharkdp/bat/blob/3a85fd767bd1f03debd0a60ac5bc08548f95bc9d/src/terminal.rs\nfn convert_color(color: syntect::highlighting::Color) -> Option<termcolor::Color> {\n    use termcolor::Color;\n\n    if color.a == 0 {\n        // Themes can specify one of the user-configurable terminal colors by\n        // encoding them as #RRGGBBAA with AA set to 00 (transparent) and RR set\n        // to the 8-bit color palette number. The built-in themes ansi-light,\n        // ansi-dark, base16, and base16-256 use this.\n        match color.r {\n            // For the first 7 colors, use the Color enum to produce ANSI escape\n            // sequences using codes 30-37 (foreground) and 40-47 (background).\n            // For example, red foreground is \\x1b[31m. This works on terminals\n            // without 256-color support.\n            0x00 => Some(Color::Black),\n            0x01 => Some(Color::Red),\n            0x02 => Some(Color::Green),\n            0x03 => Some(Color::Yellow),\n            0x04 => Some(Color::Blue),\n            0x05 => Some(Color::Magenta),\n            0x06 => Some(Color::Cyan),\n            // The 8th color is white. Themes use it as the default foreground\n            // color, but that looks wrong on terminals with a light background.\n            // So keep that text uncolored instead.\n            0x07 => None,\n            // For all other colors, produce escape sequences using\n            // codes 38;5 (foreground) and 48;5 (background). For example,\n            // bright red foreground is \\x1b[38;5;9m. This only works on\n            // terminals with 256-color support.\n            n => Some(Color::Ansi256(n)),\n        }\n    } else {\n        Some(Color::Rgb(color.r, color.g, color.b))\n    }\n}\n\npub(crate) fn supports_hyperlinks() -> bool {\n    static SUPPORTS_HYPERLINKS: OnceLock<bool> = OnceLock::new();\n    *SUPPORTS_HYPERLINKS.get_or_init(supports_hyperlinks::supports_hyperlinks)\n}\n\npub(crate) fn create_hyperlink(text: &str, url: &str) -> String {\n    // https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda\n    format!(\"\\x1B]8;;{url}\\x1B\\\\{text}\\x1B]8;;\\x1B\\\\\")\n}\n"
  },
  {
    "path": "src/formatting/palette.rs",
    "content": "//! We used to use syntect for all of our coloring and we still use syntect-compatible\n//! files to store themes.\n//!\n//! But we've started coloring some things manually for better control (and potentially\n//! for better efficiency). This macro loads colors from themes and exposes them as\n//! fields on a struct. See [`super::headers`] for an example.\n\nmacro_rules! palette {\n    {\n        $vis:vis struct $name:ident {\n            $($color:ident: $scopes:expr,)*\n        }\n    } => {\n        $vis struct $name {\n            $(pub $color: ::termcolor::ColorSpec,)*\n            #[allow(unused)]\n            pub default: ::termcolor::ColorSpec,\n        }\n\n        impl From<&::syntect::highlighting::Theme> for $name {\n            fn from(theme: &::syntect::highlighting::Theme) -> Self {\n                let highlighter = ::syntect::highlighting::Highlighter::new(theme);\n                let mut parsed_scopes = ::std::vec::Vec::new();\n                Self {\n                    $($color: $crate::formatting::palette::util::extract_color(\n                        &highlighter,\n                        &$scopes,\n                        &mut parsed_scopes,\n                    ),)*\n                    default: $crate::formatting::palette::util::extract_default(theme),\n                }\n            }\n        }\n    }\n}\n\npub(crate) use palette;\n\npub(crate) mod util {\n    use syntect::{\n        highlighting::{Highlighter, Theme},\n        parsing::Scope,\n    };\n    use termcolor::ColorSpec;\n\n    use crate::formatting::{convert_color, convert_style};\n\n    #[inline(never)]\n    pub(crate) fn extract_color(\n        highlighter: &Highlighter,\n        scopes: &[&str],\n        parsebuf: &mut Vec<Scope>,\n    ) -> ColorSpec {\n        parsebuf.clear();\n        parsebuf.extend(scopes.iter().map(|s| s.parse::<Scope>().unwrap()));\n        let style = highlighter.style_for_stack(parsebuf);\n        convert_style(style)\n    }\n\n    #[inline(never)]\n    pub(crate) fn extract_default(theme: &Theme) -> ColorSpec {\n        let mut color = ColorSpec::new();\n        if let Some(foreground) = theme.settings.foreground {\n            color.set_fg(convert_color(foreground));\n        }\n        color\n    }\n}\n"
  },
  {
    "path": "src/generation.rs",
    "content": "use std::io;\n\nuse clap_complete::Shell;\nuse clap_complete_nushell::Nushell;\n\nuse crate::cli::Cli;\nuse crate::cli::Generate;\n\nconst MAN_TEMPLATE: &str = include_str!(\"../doc/man-template.roff\");\n\npub fn generate(bin_name: &str, generate: Generate) {\n    let mut app = Cli::into_app();\n\n    match generate {\n        Generate::CompleteBash => {\n            clap_complete::generate(Shell::Bash, &mut app, bin_name, &mut io::stdout());\n        }\n        Generate::CompleteElvish => {\n            clap_complete::generate(Shell::Elvish, &mut app, bin_name, &mut io::stdout());\n        }\n        Generate::CompleteFish => {\n            use std::io::Write;\n            let mut buf = Vec::new();\n            clap_complete::generate(Shell::Fish, &mut app, bin_name, &mut buf);\n            let mut stdout = io::stdout();\n            // Based on https://github.com/fish-shell/fish-shell/blob/1e61e6492db879ba6c32013f901d84b067ca22eb/share/completions/curl.fish#L1-L6\n            let preamble = format!(\n                r#\"# Complete paths after @ in options:\nfunction __{bin_name}_complete_data\n    string match -qr '^(?<prefix>.*@)(?<path>.*)' -- (commandline -ct)\n    printf '%s\\n' -- $prefix(__fish_complete_path $path)\nend\ncomplete -c {bin_name} -n 'string match -qr \"@\" -- (commandline -ct)' -kxa \"(__{bin_name}_complete_data)\"\n\n\"#,\n            );\n            stdout.write_all(preamble.as_bytes()).unwrap();\n            stdout.write_all(&buf).unwrap();\n        }\n        Generate::CompleteNushell => {\n            clap_complete::generate(Nushell, &mut app, bin_name, &mut io::stdout());\n        }\n        Generate::CompletePowershell => {\n            clap_complete::generate(Shell::PowerShell, &mut app, bin_name, &mut io::stdout());\n        }\n        Generate::CompleteZsh => {\n            clap_complete::generate(Shell::Zsh, &mut app, bin_name, &mut io::stdout());\n        }\n        Generate::Man => {\n            generate_manpages(&mut app);\n        }\n    }\n}\n\nfn generate_manpages(app: &mut clap::Command) {\n    use roff::{Roff, bold, italic, roman};\n    use time::OffsetDateTime as DateTime;\n\n    let items: Vec<_> = app.get_arguments().filter(|i| !i.is_hide_set()).collect();\n\n    let mut request_items_roff = Roff::new();\n    let request_items = items\n        .iter()\n        .find(|opt| opt.get_id() == \"raw_rest_args\")\n        .unwrap();\n    let request_items_help = request_items\n        .get_long_help()\n        .or_else(|| request_items.get_help())\n        .expect(\"request_items is missing help\")\n        .to_string();\n\n    // replace the indents in request_item help with proper roff controls\n    // For example:\n    //\n    // ```\n    // normal help normal help\n    // normal help normal help\n    //\n    //   request-item-1\n    //     help help\n    //\n    //   request-item-2\n    //     help help\n    //\n    // normal help normal help\n    // ```\n    //\n    // Should look like this with roff controls\n    //\n    // ```\n    // normal help normal help\n    // normal help normal help\n    // .RS 12\n    // .TP\n    // request-item-1\n    // help help\n    // .TP\n    // request-item-2\n    // help help\n    // .RE\n    //\n    // .RS\n    // normal help normal help\n    // .RE\n    // ```\n    let lines: Vec<&str> = request_items_help.lines().collect();\n    let mut rs = false;\n    for i in 0..lines.len() {\n        if lines[i].is_empty() {\n            let prev = lines[i - 1].chars().take_while(|&x| x == ' ').count();\n            let next = lines[i + 1].chars().take_while(|&x| x == ' ').count();\n            if prev != next && next > 0 {\n                if !rs {\n                    request_items_roff.control(\"RS\", [\"8\"]);\n                    rs = true;\n                }\n                request_items_roff.control(\"TP\", [\"4\"]);\n            } else if prev != next && next == 0 {\n                request_items_roff.control(\"RE\", []);\n                request_items_roff.text(vec![roman(\"\")]);\n                request_items_roff.control(\"RS\", []);\n            } else {\n                request_items_roff.text(vec![roman(lines[i])]);\n            }\n        } else {\n            request_items_roff.text(vec![roman(lines[i].trim())]);\n        }\n    }\n    request_items_roff.control(\"RE\", []);\n\n    let mut options_roff = Roff::new();\n    let non_pos_items = items\n        .iter()\n        .filter(|a| !a.is_positional())\n        .collect::<Vec<_>>();\n\n    for opt in non_pos_items {\n        let mut header = vec![];\n        if let Some(short) = opt.get_short() {\n            header.push(bold(format!(\"-{short}\")));\n        }\n        if let Some(long) = opt.get_long() {\n            if !header.is_empty() {\n                header.push(roman(\", \"));\n            }\n            header.push(bold(format!(\"--{long}\")));\n        }\n        if opt.get_action().takes_values() {\n            let value_name = &opt.get_value_names().unwrap();\n            if opt.get_long().is_some() {\n                header.push(roman(\"=\"));\n            } else {\n                header.push(roman(\" \"));\n            }\n\n            if opt.get_id() == \"auth\" {\n                header.push(italic(\"USER\"));\n                header.push(roman(\"[\"));\n                header.push(italic(\":PASS\"));\n                header.push(roman(\"] | \"));\n                header.push(italic(\"TOKEN\"));\n            } else {\n                header.push(italic(value_name.join(\" \")));\n            }\n        }\n        let mut body = vec![];\n\n        let mut help = opt\n            .get_long_help()\n            .or_else(|| opt.get_help())\n            .expect(\"option is missing help\")\n            .to_string();\n        if !help.ends_with('.') {\n            help.push('.')\n        }\n        body.push(roman(help));\n\n        let possible_values = opt.get_possible_values();\n        if !possible_values.is_empty()\n            && !opt.is_hide_possible_values_set()\n            && opt.get_id() != \"pretty\"\n        {\n            let possible_values_text = format!(\n                \"\\n\\n[possible values: {}]\",\n                possible_values\n                    .iter()\n                    .map(|v| v.get_name())\n                    .collect::<Vec<_>>()\n                    .join(\", \")\n            );\n            body.push(roman(possible_values_text));\n        }\n        options_roff.control(\"TP\", [\"4\"]);\n        options_roff.text(header);\n        options_roff.text(body);\n    }\n\n    let mut manpage = MAN_TEMPLATE.to_string();\n\n    let current_date = {\n        // https://reproducible-builds.org/docs/source-date-epoch/\n        let now = match std::env::var(\"SOURCE_DATE_EPOCH\") {\n            Ok(val) => DateTime::from_unix_timestamp(val.parse::<i64>().unwrap()).unwrap(),\n            Err(_) => DateTime::now_utc(),\n        };\n        let (year, month, day) = now.date().to_calendar_date();\n        format!(\"{}-{:02}-{:02}\", year, u8::from(month), day)\n    };\n\n    manpage = manpage.replace(\"{{date}}\", &current_date);\n    manpage = manpage.replace(\"{{version}}\", app.get_version().unwrap());\n    manpage = manpage.replace(\"{{request_items}}\", request_items_roff.to_roff().trim());\n    manpage = manpage.replace(\"{{options}}\", options_roff.to_roff().trim());\n\n    print!(\"{manpage}\");\n}\n"
  },
  {
    "path": "src/main.rs",
    "content": "#![allow(clippy::bool_assert_comparison)]\nmod auth;\nmod buffer;\nmod cli;\nmod content_disposition;\nmod decoder;\nmod download;\nmod error_reporting;\nmod formatting;\nmod generation;\n#[cfg(feature = \"http-message-signatures\")]\nmod message_signature;\nmod middleware;\nmod nested_json;\nmod netrc;\nmod printer;\nmod redacted;\nmod redirect;\nmod request_items;\nmod session;\nmod to_curl;\nmod utils;\n\nuse std::env;\nuse std::fs::File;\nuse std::io::{self, IsTerminal, Read, Write as _};\nuse std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};\nuse std::path::PathBuf;\nuse std::process::ExitCode;\nuse std::str::FromStr;\nuse std::sync::Arc;\n\nuse anyhow::{Context, Result, anyhow};\nuse cookie_store::{CookieStore, RawCookie};\nuse flate2::write::ZlibEncoder;\nuse hyper::header::CONTENT_ENCODING;\nuse redirect::RedirectFollower;\nuse reqwest::blocking::{Body as ReqwestBody, Client};\nuse reqwest::header::{\n    ACCEPT, ACCEPT_ENCODING, CONNECTION, CONTENT_TYPE, COOKIE, HeaderValue, RANGE, USER_AGENT,\n};\nuse reqwest::tls;\nuse url::Host;\nuse utils::reason_phrase;\n\nuse crate::auth::{Auth, DigestAuthMiddleware};\nuse crate::buffer::Buffer;\nuse crate::cli::{Cli, FormatOptions, HttpVersion, Print, Proxy, Verify};\nuse crate::download::{download_file, get_file_size};\nuse crate::middleware::ClientWithMiddleware;\nuse crate::printer::Printer;\nuse crate::request_items::{Body, FORM_CONTENT_TYPE, JSON_ACCEPT, JSON_CONTENT_TYPE};\nuse crate::session::Session;\nuse crate::utils::{test_mode, test_pretend_term, url_with_query};\n\n#[cfg(not(any(feature = \"native-tls\", feature = \"rustls\")))]\ncompile_error!(\"Either native-tls or rustls feature must be enabled!\");\n\nfn get_user_agent() -> &'static str {\n    if test_mode() {\n        // Hard-coded user agent for the benefit of tests\n        \"xh/0.0.0 (test mode)\"\n    } else {\n        concat!(env!(\"CARGO_PKG_NAME\"), \"/\", env!(\"CARGO_PKG_VERSION\"))\n    }\n}\n\nfn main() -> ExitCode {\n    let args = Cli::parse();\n\n    if args.debug {\n        setup_backtraces();\n    }\n    args.logger_config().init();\n    // HTTPie also prints the language version, library versions, and OS version.\n    // But those are harder to access for us (and perhaps less likely to cause quirks).\n    log::debug!(\"xh {} {}\", env!(\"CARGO_PKG_VERSION\"), env!(\"XH_FEATURES\"));\n    log::debug!(\"{args:#?}\");\n\n    let native_tls = args.native_tls;\n    let bin_name = args.bin_name.clone();\n\n    match run(args) {\n        Ok(exit_code) => exit_code,\n        Err(err) => {\n            log::debug!(\"{err:#?}\");\n            eprintln!(\"{bin_name}: error: {err:?}\");\n\n            for message in error_reporting::additional_messages(&err, native_tls) {\n                eprintln!();\n                eprintln!(\"{message}\");\n            }\n\n            error_reporting::exit_code(&err)\n        }\n    }\n}\n\nfn run(args: Cli) -> Result<ExitCode> {\n    if let Some(generate) = args.generate {\n        generation::generate(&args.bin_name, generate);\n        return Ok(ExitCode::SUCCESS);\n    }\n\n    if args.curl {\n        to_curl::print_curl_translation(args)?;\n        return Ok(ExitCode::SUCCESS);\n    }\n\n    let (mut headers, headers_to_unset) = args.request_items.headers()?;\n    let url = url_with_query(args.url, &args.request_items.query()?);\n    log::debug!(\"Complete URL: {url}\");\n\n    let use_stdin = !(args.ignore_stdin || io::stdin().is_terminal() || test_pretend_term());\n\n    let body = if use_stdin {\n        if !args.request_items.is_body_empty() {\n            if args.multipart {\n                // Multipart bodies are never \"empty\", so we can get here without request items\n                return Err(anyhow!(\"Cannot build a multipart request body from stdin\"));\n            } else {\n                return Err(anyhow!(\n                    \"Request body (from stdin) and request data (key=value) cannot be mixed. \\\n                    Pass --ignore-stdin to ignore standard input.\"\n                ));\n            }\n        }\n        if args.raw.is_some() {\n            return Err(anyhow!(\n                \"Request body from stdin and --raw cannot be mixed. \\\n                Pass --ignore-stdin to ignore standard input.\"\n            ));\n        }\n        let mut buffer = Vec::new();\n        io::stdin().read_to_end(&mut buffer)?;\n        Body::Raw(buffer)\n    } else if let Some(raw) = args.raw {\n        Body::Raw(raw.into_bytes())\n    } else {\n        args.request_items.body()?\n    };\n\n    let method = args.method.unwrap_or_else(|| body.pick_method());\n    log::debug!(\"HTTP method: {method}\");\n\n    let mut client = Client::builder()\n        .http1_title_case_headers()\n        .http2_adaptive_window(true)\n        .redirect(reqwest::redirect::Policy::none())\n        .timeout(args.timeout.and_then(|t| t.as_duration()))\n        .no_gzip()\n        .no_deflate()\n        .no_brotli();\n\n    #[cfg(feature = \"rustls\")]\n    if !args.native_tls {\n        client = client.use_rustls_tls();\n    }\n\n    if let Some(tls_version) = args.ssl.and_then(Into::into) {\n        client = client\n            .min_tls_version(tls_version)\n            .max_tls_version(tls_version);\n\n        #[cfg(feature = \"native-tls\")]\n        if !args.native_tls && tls_version < tls::Version::TLS_1_2 {\n            log::warn!(\n                \"rustls does not support older TLS versions. native-tls will be enabled. Use --native-tls to silence this warning.\"\n            );\n            client = client.use_native_tls();\n        }\n\n        #[cfg(not(feature = \"native-tls\"))]\n        if tls_version < tls::Version::TLS_1_2 {\n            log::warn!(\n                \"rustls does not support older TLS versions. Consider building with the `native-tls` feature enabled.\"\n            );\n        }\n    }\n\n    #[cfg(feature = \"native-tls\")]\n    if args.native_tls {\n        client = client.use_native_tls();\n    }\n\n    #[cfg(not(feature = \"native-tls\"))]\n    if args.native_tls {\n        return Err(anyhow!(\"This binary was built without native-tls support\"));\n    }\n\n    let mut failure_code = None;\n    let mut resume: Option<u64> = None;\n    let mut auth = None;\n    let mut save_auth_in_session = true;\n\n    let verify = args.verify.unwrap_or_else(|| {\n        // requests library which is used by HTTPie checks for both\n        // REQUESTS_CA_BUNDLE and CURL_CA_BUNDLE environment variables.\n        // See https://docs.python-requests.org/en/master/user/advanced/#ssl-cert-verification\n        if let Some(path) = env::var_os(\"REQUESTS_CA_BUNDLE\") {\n            Verify::CustomCaBundle(PathBuf::from(path))\n        } else if let Some(path) = env::var_os(\"CURL_CA_BUNDLE\") {\n            Verify::CustomCaBundle(PathBuf::from(path))\n        } else {\n            Verify::Yes\n        }\n    });\n    client = match verify {\n        Verify::Yes => client,\n        Verify::No => client.danger_accept_invalid_certs(true),\n        Verify::CustomCaBundle(path) => {\n            if args.native_tls {\n                // This is not a hard error in case it gets fixed upstream\n                // https://github.com/seanmonstar/reqwest/issues/1260\n                log::warn!(\"Custom CA bundles with native-tls are broken\");\n            }\n\n            let mut buffer = Vec::new();\n            let mut file = File::open(&path).with_context(|| {\n                format!(\"Failed to open the custom CA bundle: {}\", path.display())\n            })?;\n            file.read_to_end(&mut buffer).with_context(|| {\n                format!(\"Failed to read the custom CA bundle: {}\", path.display())\n            })?;\n\n            let mut certificates = vec![];\n            for pem in pem::parse_many(buffer)? {\n                let certificate = reqwest::Certificate::from_pem(pem::encode(&pem).as_bytes())\n                    .with_context(|| {\n                        format!(\"Failed to load the custom CA bundle: {}\", path.display())\n                    })?;\n                certificates.push(certificate);\n            }\n            client = client.tls_certs_only(certificates);\n            client\n        }\n    };\n\n    #[cfg(feature = \"rustls\")]\n    if let Some(cert) = args.cert {\n        if args.native_tls {\n            // Unlike the --verify case this is advertised to not work, so it's\n            // not an outright bug, but it's still imaginable that it'll start working\n            log::warn!(\"Client certificates are not supported for native-tls\");\n        }\n\n        let mut buffer = Vec::new();\n        let mut file = File::open(&cert)\n            .with_context(|| format!(\"Failed to open the cert file: {}\", cert.display()))?;\n        file.read_to_end(&mut buffer)\n            .with_context(|| format!(\"Failed to read the cert file: {}\", cert.display()))?;\n\n        if let Some(cert_key) = args.cert_key {\n            buffer.push(b'\\n');\n\n            let mut file = File::open(&cert_key).with_context(|| {\n                format!(\"Failed to open the cert key file: {}\", cert_key.display())\n            })?;\n            file.read_to_end(&mut buffer).with_context(|| {\n                format!(\"Failed to read the cert key file: {}\", cert_key.display())\n            })?;\n        }\n\n        // We may fail here if we can't parse it but also if we don't have the key\n        let identity = reqwest::Identity::from_pem(&buffer)\n            .context(\"Failed to load the cert/cert key files\")?;\n        client = client.identity(identity);\n    }\n    #[cfg(not(feature = \"rustls\"))]\n    if args.cert.is_some() {\n        // Unlike the --verify case this is advertised to not work, so it's\n        // not an outright bug, but it's still imaginable that it'll start working\n        log::warn!(\n            \"Client certificates are not supported for native-tls and this binary was built without rustls support\"\n        );\n    }\n\n    for proxy in args.proxy.into_iter().rev() {\n        client = client.proxy(match proxy {\n            Proxy::Http(url) => reqwest::Proxy::http(url),\n            Proxy::Https(url) => reqwest::Proxy::https(url),\n            Proxy::All(url) => reqwest::Proxy::all(url),\n        }?);\n    }\n\n    client = match args.http_version {\n        Some(HttpVersion::Http10 | HttpVersion::Http11) => client.http1_only(),\n        Some(HttpVersion::Http2PriorKnowledge) => client.http2_prior_knowledge(),\n        Some(HttpVersion::Http2) => client,\n        Some(HttpVersion::Http3PriorKnowledge) => {\n            #[cfg(feature = \"http3\")]\n            {\n                if args.native_tls {\n                    return Err(anyhow!(\"HTTP/3 is not supported when using native-tls\"));\n                }\n                client.http3_prior_knowledge()\n            }\n            #[cfg(not(feature = \"http3\"))]\n            {\n                return Err(anyhow!(\n                    \"This binary was built without support for HTTP/3. Enable the `http3` feature.\"\n                ));\n            }\n        }\n        None => client,\n    };\n\n    let cookie_jar = Arc::new(reqwest_cookie_store::CookieStoreMutex::default());\n    client = client.cookie_provider(cookie_jar.clone());\n\n    client = match (args.ipv4, args.ipv6) {\n        (true, false) => client.local_address(IpAddr::from(Ipv4Addr::UNSPECIFIED)),\n        (false, true) => client.local_address(IpAddr::from(Ipv6Addr::UNSPECIFIED)),\n        _ => client,\n    };\n\n    if let Some(name_or_ip) = &args.interface {\n        if let Ok(ip_addr) = IpAddr::from_str(name_or_ip) {\n            client = client.local_address(ip_addr);\n        } else {\n            #[cfg(any(\n                target_os = \"android\",\n                target_os = \"fuchsia\",\n                target_os = \"illumos\",\n                target_os = \"ios\",\n                target_os = \"linux\",\n                target_os = \"macos\",\n                target_os = \"solaris\",\n                target_os = \"tvos\",\n                target_os = \"visionos\",\n                target_os = \"watchos\",\n            ))]\n            {\n                client = client.interface(name_or_ip);\n            }\n\n            #[cfg(not(any(\n                target_os = \"android\",\n                target_os = \"fuchsia\",\n                target_os = \"illumos\",\n                target_os = \"ios\",\n                target_os = \"linux\",\n                target_os = \"macos\",\n                target_os = \"solaris\",\n                target_os = \"tvos\",\n                target_os = \"visionos\",\n                target_os = \"watchos\",\n            )))]\n            {\n                #[cfg(not(feature = \"network-interface\"))]\n                return Err(anyhow!(\n                    \"This binary was built without support for binding to interfaces. Enable the `network-interface` feature.\"\n                ));\n\n                #[cfg(feature = \"network-interface\")]\n                {\n                    use network_interface::{NetworkInterface, NetworkInterfaceConfig};\n                    let ip_addr = NetworkInterface::show()?\n                        .iter()\n                        .find_map(|interface| {\n                            if &interface.name == name_or_ip {\n                                if let Some(addr) = interface.addr.first() {\n                                    return Some(addr.ip());\n                                }\n                            }\n                            None\n                        })\n                        .with_context(|| format!(\"Couldn't bind to {:?}\", name_or_ip))?;\n                    log::debug!(\"Resolved {name_or_ip:?} to {ip_addr:?}\");\n                    client = client.local_address(ip_addr);\n                }\n            }\n        };\n    }\n\n    #[cfg(unix)]\n    if let Some(socket_path) = args.unix_socket {\n        client = client.unix_socket(socket_path);\n    }\n\n    #[cfg(not(unix))]\n    if args.unix_socket.is_some() {\n        return Err(anyhow::anyhow!(\n            \"--unix-socket is not supported on this platform\"\n        ));\n    }\n\n    for resolve in args.resolve {\n        client = client.resolve(&resolve.domain, SocketAddr::new(resolve.addr, 0));\n    }\n\n    log::trace!(\"Finalizing reqwest client\");\n    log::trace!(\"{client:#?}\");\n    let client = client.build()?;\n\n    let mut session = match &args.session {\n        Some(name_or_path) => Some(\n            Session::load_session(url.clone(), name_or_path.clone(), args.is_session_read_only)\n                .with_context(|| {\n                    format!(\"couldn't load session {:?}\", name_or_path.to_string_lossy())\n                })?,\n        ),\n        None => None,\n    };\n\n    if let Some(ref mut s) = session {\n        auth = s.auth()?;\n\n        headers = {\n            let mut session_headers = s.headers()?;\n            session_headers.extend(headers);\n            session_headers\n        };\n        s.save_headers(&headers)?;\n\n        let mut cookie_jar = cookie_jar.lock().unwrap();\n        *cookie_jar = CookieStore::from_cookies(s.cookies(), false)\n            .context(\"Failed to load cookies from session file\")?;\n\n        if let Some(cookie) = headers.remove(COOKIE) {\n            for cookie in RawCookie::split_parse(cookie.to_str()?) {\n                cookie_jar.insert_raw(&cookie?, &url)?;\n            }\n        }\n    }\n\n    let mut request = {\n        let mut request_builder = client\n            .request(method, url.clone())\n            .header(\n                ACCEPT_ENCODING,\n                HeaderValue::from_static(\"gzip, deflate, br, zstd\"),\n            )\n            .header(USER_AGENT, get_user_agent());\n\n        if matches!(\n            args.http_version,\n            Some(HttpVersion::Http10) | Some(HttpVersion::Http11) | None\n        ) {\n            request_builder =\n                request_builder.header(CONNECTION, HeaderValue::from_static(\"keep-alive\"));\n        }\n\n        request_builder = match args.http_version {\n            Some(HttpVersion::Http10) => request_builder.version(reqwest::Version::HTTP_10),\n            Some(HttpVersion::Http11) => request_builder.version(reqwest::Version::HTTP_11),\n            Some(HttpVersion::Http2 | HttpVersion::Http2PriorKnowledge) => {\n                request_builder.version(reqwest::Version::HTTP_2)\n            }\n            Some(HttpVersion::Http3PriorKnowledge) => {\n                request_builder.version(reqwest::Version::HTTP_3)\n            }\n            None => request_builder,\n        };\n\n        request_builder = match body {\n            Body::Form(body) => request_builder.form(&body),\n            Body::Multipart(body) => request_builder.multipart(body),\n            Body::Json(body) => {\n                // An empty JSON body would produce null instead of \"\", so\n                // this is the one kind of body that needs an is_null() check\n                if !body.is_null() {\n                    request_builder\n                        .header(ACCEPT, HeaderValue::from_static(JSON_ACCEPT))\n                        .json(&body)\n                } else if args.json {\n                    request_builder\n                        .header(ACCEPT, HeaderValue::from_static(JSON_ACCEPT))\n                        .header(CONTENT_TYPE, HeaderValue::from_static(JSON_CONTENT_TYPE))\n                } else {\n                    // We're here because this is the default request type\n                    // There's nothing to do\n                    request_builder\n                }\n            }\n            Body::Raw(body) => {\n                if args.form {\n                    request_builder\n                        .header(CONTENT_TYPE, HeaderValue::from_static(FORM_CONTENT_TYPE))\n                } else {\n                    request_builder\n                        .header(ACCEPT, HeaderValue::from_static(JSON_ACCEPT))\n                        .header(CONTENT_TYPE, HeaderValue::from_static(JSON_CONTENT_TYPE))\n                }\n            }\n            .body(body),\n            Body::File {\n                file_name,\n                file_type,\n                file_name_header,\n            } => {\n                if file_name_header.is_some() {\n                    // Content-Disposition headers aren't allowed in this context (only responses\n                    // and multipart request parts), so just ignore it\n                    // (Additional precedent: HTTPie ignores file_type here)\n                    log::warn!(\n                        \"Ignoring ;filename= tag for single-file body. Consider --multipart.\"\n                    );\n                }\n                request_builder.body(File::open(file_name)?).header(\n                    CONTENT_TYPE,\n                    file_type.unwrap_or_else(|| HeaderValue::from_static(JSON_CONTENT_TYPE)),\n                )\n            }\n        };\n\n        if args.resume {\n            if headers.contains_key(RANGE) {\n                // There are no good options here, and `--continue` works on a\n                // best-effort basis, so give up with a warning.\n                //\n                // - HTTPie:\n                //   - If the file does not exist, errors when the response has\n                //     an apparently incorrect content range, as though it sent\n                //     `Range: bytes=0-`.\n                //   - If the file already exists, ignores the manual header\n                //     and downloads what's probably the wrong data.\n                // - wget gives priority to the manual header and keeps failing\n                //   and retrying the download (with or without existing file).\n                // - curl gives priority to the manual header and reports that\n                //   the server does not support partial downloads. It also has\n                //   a --range CLI option which is mutually exclusive with its\n                //   --continue-at option.\n                log::warn!(\n                    \"--continue can't be used with a 'Range:' header. --continue will be disabled.\"\n                );\n            } else if let Some(file_size) = get_file_size(args.output.as_deref()) {\n                request_builder = request_builder.header(RANGE, format!(\"bytes={file_size}-\"));\n                resume = Some(file_size);\n            }\n        }\n\n        let auth_type = args.auth_type.unwrap_or_default();\n        if let Some(auth_from_arg) = args.auth {\n            auth = Some(Auth::from_str(\n                &auth_from_arg,\n                auth_type,\n                url.host_str().unwrap_or(\"<host>\"),\n            )?);\n        } else if !args.ignore_netrc {\n            // I don't know if it's possible for host() to return None\n            // But if it does we still want to use the default entry, if there is one\n            let host = url.host().unwrap_or(Host::Domain(\"\"));\n            if let Some(entry) = netrc::find_entry(host) {\n                auth = Auth::from_netrc(auth_type, entry);\n                save_auth_in_session = false;\n            }\n        }\n\n        if let Some(auth) = &auth {\n            if let Some(ref mut s) = session {\n                if save_auth_in_session {\n                    s.save_auth(auth);\n                }\n            }\n            request_builder = match auth {\n                Auth::Basic(username, password) => {\n                    request_builder.basic_auth(username, password.as_ref())\n                }\n                Auth::Bearer(token) => request_builder.bearer_auth(token),\n                Auth::Digest(..) => request_builder,\n            }\n        }\n\n        let mut request = request_builder.headers(headers).build()?;\n\n        if args.compress >= 1 {\n            if request.headers().contains_key(CONTENT_ENCODING) {\n                // HTTPie overrides the original Content-Encoding header in this case\n                log::warn!(\n                    \"--compress can't be used with a 'Content-Encoding:' header. --compress will be disabled.\"\n                );\n            } else if let Some(body) = request.body_mut() {\n                // TODO: Compress file body (File) without buffering\n                let body_bytes = body.buffer()?;\n                let mut encoder = ZlibEncoder::new(Vec::new(), Default::default());\n                encoder.write_all(body_bytes)?;\n                let output = encoder.finish()?;\n                if output.len() < body_bytes.len() || args.compress >= 2 {\n                    *body = ReqwestBody::from(output);\n                    request\n                        .headers_mut()\n                        .insert(CONTENT_ENCODING, HeaderValue::from_static(\"deflate\"));\n                }\n            }\n        }\n\n        for header in &headers_to_unset {\n            request.headers_mut().remove(header);\n        }\n\n        #[cfg(not(feature = \"http-message-signatures\"))]\n        if args.m_sig.m_sig_id.is_some()\n            || args.m_sig.m_sig_key.is_some()\n            || args.m_sig.m_sig_alg.is_some()\n            || args.m_sig.has_components()\n        {\n            return Err(anyhow!(\n                \"This binary was built without message signature support. Enable the `http-message-signatures` feature.\"\n            ));\n        }\n\n        #[cfg(feature = \"http-message-signatures\")]\n        if args.m_sig.has_components() && !args.m_sig.has_key_pair() {\n            return Err(anyhow!(\n                \"Message signature components require both --unstable-m-sig-id and --unstable-m-sig-key.\"\n            ));\n        }\n\n        #[cfg(feature = \"http-message-signatures\")]\n        if let Some((key_id, key_material)) = args.m_sig.key_pair() {\n            let m_sig_components = args.m_sig.flattened_components();\n            let m_sig_algorithm = args.m_sig.algorithm().map(Into::into);\n            message_signature::sign_request(\n                &mut request,\n                key_id,\n                key_material,\n                (!m_sig_components.is_empty()).then_some(m_sig_components.as_slice()),\n                m_sig_algorithm,\n            )?;\n        }\n\n        request\n    };\n\n    if args.download {\n        request\n            .headers_mut()\n            .insert(ACCEPT_ENCODING, HeaderValue::from_static(\"identity\"));\n    }\n\n    log::trace!(\"Built reqwest request\");\n    // Note: Debug impl is incomplete?\n    log::trace!(\"{request:#?}\");\n\n    let buffer = Buffer::new(\n        args.download,\n        args.output.as_deref(),\n        io::stdout().is_terminal() || test_pretend_term(),\n    )?;\n    let is_output_redirected = buffer.is_redirect();\n    let print = match args.print {\n        Some(print) => print,\n        None => Print::new(\n            args.verbose,\n            args.headers,\n            args.body,\n            args.meta,\n            args.quiet > 0,\n            args.offline,\n            &buffer,\n        ),\n    };\n    let theme = args.style.unwrap_or_default();\n    let pretty = args.pretty.unwrap_or_else(|| buffer.guess_pretty());\n    let format_options = args\n        .format_options\n        .iter()\n        .fold(FormatOptions::default(), FormatOptions::merge);\n    let mut printer = Printer::new(pretty, theme, args.stream, buffer, format_options);\n\n    let response_charset = args.response_charset;\n    let response_mime = args.response_mime.as_deref();\n\n    if print.request_headers {\n        printer.print_request_headers(&request, &*cookie_jar)?;\n    }\n    if print.request_body {\n        printer.print_request_body(&mut request)?;\n    }\n\n    if !args.offline {\n        let mut response = {\n            let history_print = args.history_print.unwrap_or(print);\n            let mut client = ClientWithMiddleware::new(&client);\n            if args.all {\n                client = client.with_printer(|prev_response, next_request| {\n                    if history_print.response_headers {\n                        printer.print_response_headers(prev_response)?;\n                    }\n                    if history_print.response_body {\n                        printer.print_response_body(\n                            prev_response,\n                            response_charset,\n                            response_mime,\n                        )?;\n                        printer.print_separator()?;\n                    }\n                    if history_print.response_meta {\n                        printer.print_response_meta(prev_response)?;\n                    }\n                    if history_print.request_headers {\n                        printer.print_request_headers(next_request, &*cookie_jar)?;\n                    }\n                    if history_print.request_body {\n                        printer.print_request_body(next_request)?;\n                    }\n                    Ok(())\n                });\n            }\n            if args.follow {\n                #[cfg(feature = \"http-message-signatures\")]\n                {\n                    let message_signature = args.m_sig.has_key_pair().then_some(args.m_sig.clone());\n\n                    client = client.with(RedirectFollower::new(\n                        args.max_redirects.unwrap_or(10),\n                        message_signature,\n                    ));\n                }\n                #[cfg(not(feature = \"http-message-signatures\"))]\n                {\n                    client = client.with(RedirectFollower::new(args.max_redirects.unwrap_or(10)));\n                }\n            }\n            if let Some(Auth::Digest(username, password)) = &auth {\n                client = client.with(DigestAuthMiddleware::new(username, password));\n            }\n            client.execute(request)?\n        };\n\n        let mut download_already_complete = false;\n        let status = response.status();\n        if args.check_status.unwrap_or(!args.httpie_compat_mode) {\n            match status.as_u16() {\n                300..=399 if !args.follow => failure_code = Some(ExitCode::from(3)),\n                416 if resume.is_some() => download_already_complete = true,\n                400..=499 => failure_code = Some(ExitCode::from(4)),\n                500..=599 => failure_code = Some(ExitCode::from(5)),\n                _ => (),\n            }\n\n            // Print this if the status code isn't otherwise ending up in the terminal.\n            // HTTPie looks at --quiet, since --quiet always suppresses the response\n            // headers even if you pass --print=h. But --print takes precedence for us.\n            if failure_code.is_some() && (is_output_redirected || !print.response_headers) {\n                log::warn!(\"HTTP {} {}\", status.as_u16(), reason_phrase(&response));\n            }\n        }\n\n        if print.response_headers {\n            printer.print_response_headers(&response)?;\n        }\n        if args.download {\n            if download_already_complete {\n                if let Some(output) = &args.output {\n                    eprintln!(\"Download {output:?} is already complete\");\n                } else {\n                    eprintln!(\"Download is already complete\");\n                }\n            } else if failure_code.is_none() {\n                download_file(\n                    response,\n                    args.output,\n                    &url,\n                    resume,\n                    pretty.color(),\n                    args.quiet > 0,\n                )?;\n            }\n        } else {\n            if print.response_body {\n                printer.print_response_body(&mut response, response_charset, response_mime)?;\n                if print.response_meta {\n                    printer.print_separator()?;\n                }\n            }\n            if print.response_meta {\n                printer.print_response_meta(&response)?;\n            }\n        }\n    }\n\n    if let Some(ref mut s) = session {\n        let cookie_jar = cookie_jar.lock().unwrap();\n        s.save_cookies(cookie_jar.iter_unexpired());\n        s.persist()\n            .with_context(|| format!(\"couldn't persist session {}\", s.path.display()))?;\n    }\n\n    Ok(failure_code.unwrap_or(ExitCode::SUCCESS))\n}\n\n/// Configure backtraces for standard panics and anyhow using `$RUST_BACKTRACE`.\n///\n/// Note: they only check the environment variable once, so this won't take effect if\n/// we do it after a panic has already happened or an anyhow error has already been\n/// created.\n///\n/// It's possible for CLI parsing to create anyhow errors before we call this function\n/// but it looks like those errors are always fatal.\n///\n/// https://github.com/rust-lang/rust/issues/93346 will become the preferred way to\n/// configure panic backtraces.\nfn setup_backtraces() {\n    if std::env::var_os(\"RUST_BACKTRACE\").is_some() {\n        // User knows best\n        return;\n    }\n\n    // SAFETY: No other threads are running at this time.\n    unsafe {\n        std::env::set_var(\"RUST_BACKTRACE\", \"1\");\n    }\n}\n"
  },
  {
    "path": "src/message_signature.rs",
    "content": "use std::collections::HashSet;\n\nuse anyhow::{Context, Result, anyhow, bail};\nuse base64::{Engine as _, engine::general_purpose::STANDARD};\nuse httpsig_hyper::prelude::{\n    AlgorithmName, HttpSigResult, HttpSignatureParams, SecretKey, SharedKey, SigningKey,\n    message_component::{HttpMessageComponentId, HttpMessageComponentName},\n};\nuse hyper::http;\nuse reqwest::blocking::{Body as ReqwestBody, Request};\nuse reqwest::header::{HeaderName, HeaderValue};\nuse sha2::{Digest, Sha256};\n\npub fn sign_request(\n    request: &mut Request,\n    key_id: &str,\n    key_material: &str,\n    components: Option<&[String]>,\n    algorithm_override: Option<AlgorithmName>,\n) -> Result<()> {\n    let key = parse_key_input(key_material)?;\n\n    let (signing_key, algorithm) = build_signing_key(&key, key_id, algorithm_override)?;\n\n    let components = resolve_components(request, components);\n    ensure_content_digest(request, &components)?;\n\n    let mut signature_params = build_signature_params(&components)?;\n    signature_params.set_alg(&algorithm);\n\n    // Ensure keyid is included in Signature-Input\n    signature_params.set_keyid(key_id);\n\n    // Preferred path: use upstream sync signing helper.\n    let mut http_request = http::Request::builder()\n        .version(request.version())\n        .method(request.method())\n        .uri(request.url().as_str())\n        .body(reqwest::Body::default())\n        .context(\"message-signature: Failed to build temporary HTTP request\")?;\n    *http_request.headers_mut() = request.headers().clone();\n\n    use httpsig_hyper::MessageSignatureReqSync;\n    http_request\n        .set_message_signature_sync(&signature_params, &signing_key, Some(\"sig1\"))\n        .context(\"message-signature: Failed to set message signature\")?;\n\n    let signature = http_request\n        .headers()\n        .get(\"signature\")\n        .context(\"message-signature: Signature header missing after signing\")?;\n    let signature_input = http_request\n        .headers()\n        .get(\"signature-input\")\n        .context(\"message-signature: Signature-Input header missing after signing\")?;\n\n    request\n        .headers_mut()\n        .insert(HeaderName::from_static(\"signature\"), signature.clone());\n    request.headers_mut().insert(\n        HeaderName::from_static(\"signature-input\"),\n        signature_input.clone(),\n    );\n    Ok(())\n}\n\n/// Resolves and expands message components for signature coverage.\n///\n/// This function handles:\n/// - Default components: If no components are specified, uses @method, @authority, @target-uri\n/// - @query-params expansion: Expands into individual @query-param components for each parameter\n/// - content-digest: Only includes if the request has a body\n///\n/// Note: @query-params is not a standard RFC 9421 component, but is commonly used as a\n/// convenience shorthand to sign all query parameters without listing them individually.\nfn resolve_components(request: &Request, components: Option<&[String]>) -> Vec<String> {\n    let mut resolved = Vec::new();\n    let source = if let Some(c) = components {\n        c\n    } else {\n        // RFC 9421 recommended minimal set for request signing\n        &[\n            \"@method\".to_string(),\n            \"@authority\".to_string(),\n            \"@target-uri\".to_string(),\n        ] as &[String]\n    };\n\n    for component in source {\n        if component == \"@query-params\" {\n            // According to some conventions (and this implementation), \"@query-params\"\n            // acts as a wildcard that expands into individual \"@query-param\" components\n            // for every parameter present in the request's query string.\n            //\n            // RFC 9421 does not define \"@query-params\" as a standard derived component,\n            // but many implementations use it to simplify signing all query parameters\n            // without listing them explicitly.\n            if let Some(query) = request.url().query() {\n                let mut seen = HashSet::new();\n                for (name, _) in form_urlencoded::parse(query.as_bytes()) {\n                    if seen.insert(name.to_string()) {\n                        resolved.push(format!(\"@query-param;name=\\\"{}\\\"\", name));\n                    }\n                }\n            }\n        } else if component == \"content-digest\" {\n            if request.body().is_some() {\n                resolved.push(component.clone());\n            }\n        } else {\n            resolved.push(component.clone());\n        }\n    }\n    resolved\n}\n\n/// Ensures the Content-Digest header is present if it's a covered component.\n///\n/// According to RFC 9530, the Content-Digest header uses the format:\n/// `sha-256=:<base64-encoded-hash>:`\n///\n/// This function:\n/// 1. Checks if \"content-digest\" is in the covered components\n/// 2. If yes and the header is missing, computes SHA-256 of the request body\n/// 3. Adds the Content-Digest header in the RFC 9530 format\nfn ensure_content_digest(request: &mut Request, components: &[String]) -> Result<()> {\n    if components\n        .iter()\n        .any(|c| c.eq_ignore_ascii_case(\"content-digest\"))\n        && !request.headers().contains_key(\"content-digest\")\n        && request.body().is_some()\n    {\n        let bytes = buffer_request_body(request)?;\n        let digest = Sha256::digest(&bytes);\n        // RFC 9530 format: algorithm=:base64-hash:\n        let value = format!(\"sha-256=:{}:\", STANDARD.encode(digest));\n        request.headers_mut().insert(\n            HeaderName::from_static(\"content-digest\"),\n            HeaderValue::from_str(&value)?,\n        );\n    }\n    Ok(())\n}\n\nfn build_signature_params(components: &[String]) -> Result<HttpSignatureParams> {\n    let mut component_ids = Vec::new();\n    let mut seen = HashSet::new();\n    for c in components {\n        let normalized = normalize_component_id(c);\n        let id = HttpMessageComponentId::try_from(normalized.as_str())\n            .with_context(|| format!(\"message-signature: Invalid component: {}\", c))?;\n        // RFC 9421 requires each covered component identifier to appear at most once.\n        // Equivalence is based on component id semantics, where parameter order does\n        // not create a distinct identifier.\n        let uniqueness_key = component_uniqueness_key(&id);\n        if !seen.insert(uniqueness_key) {\n            bail!(\n                \"message-signature: Duplicate covered component identifier: {}\",\n                id\n            );\n        }\n        component_ids.push(id);\n    }\n    HttpSignatureParams::try_new(&component_ids)\n        .context(\"message-signature: Failed to create signature params\")\n}\n\n/// Build a canonical key for RFC 9421 component-identifier uniqueness checks.\n///\n/// RFC 9421 treats component identifiers as unique entries in covered components,\n/// and two identifiers that differ only by parameter ordering are equivalent.\n/// We normalize:\n/// - component name (`HttpField` lowercased, derived names preserved), and\n/// - parameters (sorted, then joined),\n///\n/// so equivalent identifiers map to the same key.\nfn component_uniqueness_key(component_id: &HttpMessageComponentId) -> String {\n    let name = match &component_id.name {\n        HttpMessageComponentName::Derived(derived) => AsRef::<str>::as_ref(derived).to_string(),\n        HttpMessageComponentName::HttpField(field) => field.to_ascii_lowercase(),\n    };\n    let mut params: Vec<String> = component_id\n        .params\n        .0\n        .iter()\n        .cloned()\n        .map(Into::into)\n        .collect();\n    params.sort_unstable();\n    if params.is_empty() {\n        name\n    } else {\n        format!(\"{name};{}\", params.join(\";\"))\n    }\n}\n\n/// Normalizes component identifiers for RFC 9421 compliance.\n///\n/// According to RFC 9421, derived components (starting with @) that have parameters\n/// must be quoted. For example:\n/// - `@query-param;name=\"foo\"` -> `\"@query-param\";name=\"foo\"`\n/// - `@method` -> `@method` (no parameters, no quotes needed)\n/// - `content-type` -> `content-type` (not a derived component)\n///\n/// This normalization is required for proper signature base construction.\nfn normalize_component_id(component: &str) -> String {\n    if let Some(idx) = component.find(';') {\n        let (name, params) = component.split_at(idx);\n        if name.starts_with('@') && !name.starts_with('\"') {\n            // Derived component with parameters must be quoted\n            return format!(\"\\\"{}\\\"{}\", name, params);\n        }\n    }\n    component.to_string()\n}\n\nfn buffer_request_body(request: &mut Request) -> Result<Vec<u8>> {\n    if let Some(body) = request.body_mut() {\n        let bytes = body\n            .buffer()\n            .context(\"message-signature: Failed to buffer request body for Content-Digest\")?\n            .to_vec();\n        *body = ReqwestBody::from(bytes.clone());\n        Ok(bytes)\n    } else {\n        Ok(Vec::new())\n    }\n}\n\nfn parse_key_input(key_material: &str) -> Result<Vec<u8>> {\n    let key = if let Some(path) = key_material.strip_prefix('@') {\n        std::fs::read(crate::utils::expand_tilde(path))?\n    } else {\n        // Unlike some HTTPie plugins that force Base64 encoding for the secret key part\n        // of the --auth string, xh treats the raw string as the key material by default.\n        // This provides a more direct CLI experience, consistent with how xh handles\n        // standard passwords in `-a user:password`.\n        key_material.as_bytes().to_vec()\n    };\n    Ok(key)\n}\n\nfn build_signing_key(\n    key_material: &[u8],\n    key_id: &str,\n    algorithm_override: Option<AlgorithmName>,\n) -> Result<(MessageSigningKey, AlgorithmName)> {\n    if let Some(algorithm) = algorithm_override {\n        return build_signing_key_with_algorithm(key_material, key_id, &algorithm);\n    }\n\n    if let Ok(pem) = std::str::from_utf8(key_material) {\n        if pem.contains(\"-----BEGIN\") {\n            if let Some(secret) = parse_pem_secret_key(\n                pem,\n                &[\n                    AlgorithmName::Ed25519,\n                    AlgorithmName::EcdsaP256Sha256,\n                    AlgorithmName::EcdsaP384Sha384,\n                ],\n            ) {\n                let alg = secret.alg();\n                return Ok((MessageSigningKey::Secret(secret, key_id.to_string()), alg));\n            }\n            if parse_pem_secret_key(\n                pem,\n                &[AlgorithmName::RsaV1_5Sha256, AlgorithmName::RsaPssSha512],\n            )\n            .is_some()\n            {\n                bail!(\n                    \"message-signature: RSA private keys require an explicit algorithm. Use --unstable-m-sig-alg=rsa-v1_5-sha256 or --unstable-m-sig-alg=rsa-pss-sha512\"\n                );\n            }\n            bail!(\n                \"message-signature: Failed to parse PEM private key. Supported algorithms: ed25519, ecdsa-p256-sha256, ecdsa-p384-sha384, rsa-v1_5-sha256, rsa-pss-sha512\"\n            );\n        }\n    }\n\n    build_hmac_signing_key(key_material, key_id)\n}\n\nfn build_hmac_signing_key(\n    key_material: &[u8],\n    key_id: &str,\n) -> Result<(MessageSigningKey, AlgorithmName)> {\n    let encoded = STANDARD.encode(key_material);\n    let shared_key = SharedKey::from_base64(&AlgorithmName::HmacSha256, &encoded)\n        .map_err(|e| anyhow!(\"message-signature: Failed to create HMAC key: {:?}\", e))?;\n    Ok((\n        MessageSigningKey::Shared(shared_key, key_id.to_string()),\n        AlgorithmName::HmacSha256,\n    ))\n}\n\nfn build_signing_key_with_algorithm(\n    key_material: &[u8],\n    key_id: &str,\n    algorithm: &AlgorithmName,\n) -> Result<(MessageSigningKey, AlgorithmName)> {\n    if algorithm == &AlgorithmName::HmacSha256 {\n        return build_hmac_signing_key(key_material, key_id);\n    }\n\n    let secret = if let Ok(pem) = std::str::from_utf8(key_material) {\n        if pem.contains(\"-----BEGIN\") {\n            SecretKey::from_pem(algorithm, pem).with_context(|| {\n                format!(\n                    \"message-signature: Failed to parse PEM private key as {}\",\n                    algorithm.as_str()\n                )\n            })?\n        } else {\n            SecretKey::from_bytes(algorithm, key_material).with_context(|| {\n                format!(\n                    \"message-signature: Failed to parse private key bytes as {}\",\n                    algorithm.as_str()\n                )\n            })?\n        }\n    } else {\n        SecretKey::from_bytes(algorithm, key_material).with_context(|| {\n            format!(\n                \"message-signature: Failed to parse private key bytes as {}\",\n                algorithm.as_str()\n            )\n        })?\n    };\n    let alg = secret.alg();\n    Ok((MessageSigningKey::Secret(secret, key_id.to_string()), alg))\n}\n\nfn parse_pem_secret_key(pem: &str, algorithms: &[AlgorithmName]) -> Option<SecretKey> {\n    for alg in algorithms {\n        if let Ok(secret) = SecretKey::from_pem(alg, pem) {\n            return Some(secret);\n        }\n    }\n    None\n}\n\nenum MessageSigningKey {\n    Secret(SecretKey, String),\n    Shared(SharedKey, String),\n}\n\nimpl SigningKey for MessageSigningKey {\n    fn sign(&self, data: &[u8]) -> HttpSigResult<Vec<u8>> {\n        match self {\n            MessageSigningKey::Secret(inner, _) => inner.sign(data),\n            MessageSigningKey::Shared(inner, _) => inner.sign(data),\n        }\n    }\n\n    fn key_id(&self) -> String {\n        match self {\n            MessageSigningKey::Secret(_, id) => id.clone(),\n            MessageSigningKey::Shared(_, id) => id.clone(),\n        }\n    }\n\n    fn alg(&self) -> AlgorithmName {\n        match self {\n            MessageSigningKey::Secret(inner, _) => inner.alg(),\n            MessageSigningKey::Shared(inner, _) => inner.alg(),\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use reqwest::blocking::Client;\n\n    #[test]\n    fn test_content_digest_generation() {\n        let mut req = Client::new()\n            .post(\"http://example.com\")\n            .body(\"Hello, World!\")\n            .build()\n            .unwrap();\n\n        let components = vec![\"content-digest\".to_string()];\n        ensure_content_digest(&mut req, &components).unwrap();\n\n        let digest_header = req.headers().get(\"content-digest\").unwrap();\n        let digest_str = digest_header.to_str().unwrap();\n\n        // SHA-256 of \"Hello, World!\" is dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f\n        // Base64: 3/1gIbsr1bCvZ2KQgJ7DpTGR3YHH9wpLKGiKNiGCmG8=\n        // Header format: sha-256=:...:\n        assert_eq!(\n            digest_str,\n            \"sha-256=:3/1gIbsr1bCvZ2KQgJ7DpTGR3YHH9wpLKGiKNiGCmG8=:\"\n        );\n    }\n\n    #[test]\n    fn test_sign_request_with_query_param() {\n        let mut req = Client::new()\n            .get(\"https://example.com/?param=value\")\n            .build()\n            .unwrap();\n\n        let key_id = \"test-key\";\n        let key_material = \"secret\";\n\n        // Use the plural @query-params which expands automatically\n        // This will internally call resolve_components -> try_from(\"@query-param;name=\\\"param\\\"\")\n        // If this succeeds, then the logic is correct.\n        let components = vec![\"@method\".to_string(), \"@query-params\".to_string()];\n        sign_request(&mut req, key_id, key_material, Some(&components), None).unwrap();\n\n        let sig_input = req.headers()[\"signature-input\"].to_str().unwrap();\n        assert!(sig_input.contains(\"sig1=\"));\n        // Check that the expanded component is present\n        assert!(sig_input.contains(\"\\\"@query-param\\\";name=\\\"param\\\"\"));\n    }\n\n    #[test]\n    fn test_sign_request_hmac() {\n        let mut req = Client::new()\n            .post(\"https://example.com/foo\")\n            .body(\"data\")\n            .build()\n            .unwrap();\n\n        let key_id = \"test-key\";\n        let key_material = \"secret\"; // HMAC key\n\n        // Explicitly include content-digest\n        let components = vec![\n            \"@method\".to_string(),\n            \"@authority\".to_string(),\n            \"content-digest\".to_string(),\n        ];\n        sign_request(&mut req, key_id, key_material, Some(&components), None).unwrap();\n\n        assert!(req.headers().contains_key(\"signature\"));\n        assert!(req.headers().contains_key(\"signature-input\"));\n        assert!(req.headers().contains_key(\"content-digest\"));\n\n        let sig_input = req.headers()[\"signature-input\"].to_str().unwrap();\n        assert!(sig_input.contains(\"sig1=\"));\n        assert!(sig_input.contains(\"keyid=\\\"test-key\\\"\"));\n        assert!(sig_input.contains(\"content-digest\"));\n        assert!(sig_input.contains(\"alg=\\\"hmac-sha256\\\"\"));\n    }\n\n    #[test]\n    fn test_bs_parameter_unsupported() {\n        let mut req = Client::new()\n            .get(\"https://example.com\")\n            .header(\"x-data\", \"hello\")\n            .build()\n            .unwrap();\n\n        let key_id = \"test-key\";\n        let key_material = \"secret\";\n\n        // Attempt to sign with the ;bs parameter which is currently unsupported by the underlying library\n        let components = vec![\"\\\"x-data\\\";bs\".to_string()];\n        let result = sign_request(&mut req, key_id, key_material, Some(&components), None);\n\n        assert!(result.is_err());\n        let err_msg = format!(\"{:?}\", result.err().unwrap());\n        // The underlying library httpsig currently returns \"Not yet implemented: `bs` is not supported yet\"\n        assert!(err_msg.contains(\"not supported\"));\n    }\n\n    #[test]\n    fn test_sf_parameter_success() {\n        let mut req = Client::new()\n            .get(\"https://example.com\")\n            .header(\"x-struct\", \"a=1, b=2\")\n            .build()\n            .unwrap();\n\n        // ;sf is implemented in the underlying library\n        let components = vec![\"\\\"x-struct\\\";sf\".to_string()];\n        let result = sign_request(&mut req, \"key1\", \"secret\", Some(&components), None);\n        assert!(result.is_ok(), \"sf parameter should be supported\");\n    }\n\n    #[test]\n    fn test_key_parameter_success() {\n        let mut req = Client::new()\n            .get(\"https://example.com\")\n            .header(\"x-dict\", \"a=1, b=2\")\n            .build()\n            .unwrap();\n\n        // ;key is implemented in the underlying library\n        let components = vec![\"\\\"x-dict\\\";key=\\\"a\\\"\".to_string()];\n        let result = sign_request(&mut req, \"key1\", \"secret\", Some(&components), None);\n        assert!(result.is_ok(), \"key parameter should be supported\");\n    }\n\n    #[test]\n    fn test_tr_parameter_unsupported() {\n        let mut req = Client::new()\n            .get(\"https://example.com\")\n            .header(\"x-field\", \"value\")\n            .build()\n            .unwrap();\n\n        // ;tr is explicitly NOT implemented in the underlying library\n        let components = vec![\"\\\"x-field\\\";tr\".to_string()];\n        let result = sign_request(&mut req, \"key1\", \"secret\", Some(&components), None);\n\n        assert!(result.is_err());\n        let err_msg = format!(\"{:?}\", result.err().unwrap());\n        assert!(err_msg.contains(\"tr\") && err_msg.contains(\"supported\"));\n    }\n\n    #[test]\n    fn test_name_parameter_error_on_field() {\n        let mut req = Client::new()\n            .get(\"https://example.com\")\n            .header(\"x-field\", \"value\")\n            .build()\n            .unwrap();\n\n        // ;name is only for @query-param, using it on a regular field should error\n        let components = vec![\"\\\"x-field\\\";name=\\\"id\\\"\".to_string()];\n        let result = sign_request(&mut req, \"key1\", \"secret\", Some(&components), None);\n\n        assert!(result.is_err());\n        let err_msg = format!(\"{:?}\", result.err().unwrap());\n        // It could be either a validation error or a parsing error depending on the library version\n        assert!(err_msg.contains(\"name\"));\n    }\n\n    #[test]\n    fn test_resolve_components_defaults() {\n        let req = Client::new().get(\"http://a.com\").build().unwrap();\n\n        let defaults = resolve_components(&req, None);\n        assert_eq!(defaults, vec![\"@method\", \"@authority\", \"@target-uri\"]);\n    }\n\n    #[test]\n    fn test_resolve_components_query_params_deduplicates_names() {\n        let req = Client::new()\n            .get(\"https://example.com/?id=1&id=2&name=alice&id=3\")\n            .build()\n            .unwrap();\n        let input = vec![\"@query-params\".to_string()];\n\n        let resolved = resolve_components(&req, Some(&input));\n        assert_eq!(\n            resolved,\n            vec![\"@query-param;name=\\\"id\\\"\", \"@query-param;name=\\\"name\\\"\"]\n        );\n    }\n\n    #[test]\n    fn test_duplicate_component_rejected() {\n        let components = vec![\"@method\".to_string(), \"@method\".to_string()];\n        let result = build_signature_params(&components);\n        assert!(result.is_err());\n        assert!(format!(\"{:?}\", result.err().unwrap()).contains(\"Duplicate covered component\"));\n    }\n\n    #[test]\n    fn test_equivalent_component_with_different_param_order_rejected() {\n        let first = HttpMessageComponentId::try_from(\"\\\"x-field\\\";sf;tr\").unwrap();\n        let second = HttpMessageComponentId::try_from(\"\\\"x-field\\\";tr;sf\").unwrap();\n        assert_eq!(\n            component_uniqueness_key(&first),\n            component_uniqueness_key(&second)\n        );\n\n        let components = vec![\n            \"\\\"x-field\\\";sf;tr\".to_string(),\n            \"\\\"x-field\\\";tr;sf\".to_string(),\n        ];\n        let result = build_signature_params(&components);\n        assert!(result.is_err());\n        assert!(format!(\"{:?}\", result.err().unwrap()).contains(\"Duplicate covered component\"));\n    }\n\n    #[test]\n    fn test_normalize_component_id() {\n        // Should wrap @ components with parameters in quotes\n        assert_eq!(\n            normalize_component_id(\"@query-param;name=\\\"a\\\"\"),\n            \"\\\"@query-param\\\";name=\\\"a\\\"\"\n        );\n        // Should not wrap if already wrapped\n        assert_eq!(\n            normalize_component_id(\"\\\"@query-param\\\";name=\\\"a\\\"\"),\n            \"\\\"@query-param\\\";name=\\\"a\\\"\"\n        );\n        // Should not wrap regular headers\n        assert_eq!(normalize_component_id(\"content-type\"), \"content-type\");\n        // Should not wrap @ components without parameters\n        assert_eq!(normalize_component_id(\"@method\"), \"@method\");\n    }\n\n    #[test]\n    fn test_invalid_pem_key_does_not_fall_back_to_hmac() {\n        let mut req = Client::new().get(\"https://example.com\").build().unwrap();\n        let invalid_pem = \"-----BEGIN PRIVATE KEY-----\\nnot-a-valid-key\\n-----END PRIVATE KEY-----\";\n        let result = sign_request(&mut req, \"key1\", invalid_pem, None, None);\n\n        assert!(result.is_err());\n        let err_msg = format!(\"{:?}\", result.err().unwrap());\n        assert!(err_msg.contains(\"Failed to parse PEM private key\"));\n        assert!(!req.headers().contains_key(\"signature\"));\n    }\n\n    #[test]\n    fn test_rsa_pem_requires_explicit_algorithm() {\n        let rsa_key_path = format!(\n            \"{}/tests/fixtures/keys/rsa_private_key_pkcs8.pem\",\n            env!(\"CARGO_MANIFEST_DIR\")\n        );\n        let pem = std::fs::read(rsa_key_path).unwrap();\n        let result = build_signing_key(&pem, \"key1\", None);\n        assert!(result.is_err());\n        let err_msg = format!(\"{:?}\", result.err().unwrap());\n        assert!(err_msg.contains(\"RSA private keys require an explicit algorithm\"));\n    }\n\n    #[test]\n    fn test_rsa_pem_with_explicit_algorithm_succeeds() {\n        let rsa_key_path = format!(\n            \"{}/tests/fixtures/keys/rsa_private_key_pkcs8.pem\",\n            env!(\"CARGO_MANIFEST_DIR\")\n        );\n        let pem = std::fs::read(rsa_key_path).unwrap();\n        let result = build_signing_key(&pem, \"key1\", Some(AlgorithmName::RsaV1_5Sha256));\n        assert!(result.is_ok());\n    }\n}\n"
  },
  {
    "path": "src/middleware.rs",
    "content": "use std::time::{Duration, Instant};\n\nuse anyhow::Result;\nuse reqwest::blocking::{Client, Request, Response};\n\n#[derive(Clone)]\npub struct ResponseMeta {\n    pub request_duration: Duration,\n    pub content_download_duration: Option<Duration>,\n}\n\npub trait ResponseExt {\n    fn meta(&self) -> &ResponseMeta;\n    fn meta_mut(&mut self) -> &mut ResponseMeta;\n}\n\nimpl ResponseExt for Response {\n    fn meta(&self) -> &ResponseMeta {\n        self.extensions().get::<ResponseMeta>().unwrap()\n    }\n\n    fn meta_mut(&mut self) -> &mut ResponseMeta {\n        self.extensions_mut().get_mut::<ResponseMeta>().unwrap()\n    }\n}\n\ntype Printer<'a, 'b> = &'a mut (dyn FnMut(&mut Response, &mut Request) -> Result<()> + 'b);\n\npub struct Context<'a, 'b> {\n    client: &'a Client,\n    printer: Option<Printer<'a, 'b>>,\n    middlewares: &'a mut [Box<dyn Middleware + 'b>],\n}\n\nimpl<'a, 'b> Context<'a, 'b> {\n    fn new(\n        client: &'a Client,\n        printer: Option<Printer<'a, 'b>>,\n        middlewares: &'a mut [Box<dyn Middleware + 'b>],\n    ) -> Self {\n        Context {\n            client,\n            printer,\n            middlewares,\n        }\n    }\n\n    fn execute(&mut self, request: Request) -> Result<Response> {\n        match self.middlewares {\n            [] => {\n                let starting_time = Instant::now();\n                let mut response = self.client.execute(request)?;\n                response.extensions_mut().insert(ResponseMeta {\n                    request_duration: starting_time.elapsed(),\n                    content_download_duration: None,\n                });\n                Ok(response)\n            }\n            [head, tail @ ..] => head.handle(\n                #[allow(clippy::needless_option_as_deref)]\n                Context::new(self.client, self.printer.as_deref_mut(), tail),\n                request,\n            ),\n        }\n    }\n}\n\npub trait Middleware {\n    fn handle(&mut self, ctx: Context, request: Request) -> Result<Response>;\n\n    fn next(&self, ctx: &mut Context, request: Request) -> Result<Response> {\n        ctx.execute(request)\n    }\n\n    fn print(\n        &self,\n        ctx: &mut Context,\n        response: &mut Response,\n        request: &mut Request,\n    ) -> Result<()> {\n        if let Some(ref mut printer) = ctx.printer {\n            printer(response, request)?;\n        }\n\n        Ok(())\n    }\n}\n\npub struct ClientWithMiddleware<'a, T>\nwhere\n    T: FnMut(&mut Response, &mut Request) -> Result<()>,\n{\n    client: &'a Client,\n    printer: Option<T>,\n    middlewares: Vec<Box<dyn Middleware + 'a>>,\n}\n\nimpl<'a, T> ClientWithMiddleware<'a, T>\nwhere\n    T: FnMut(&mut Response, &mut Request) -> Result<()> + 'a,\n{\n    pub fn new(client: &'a Client) -> Self {\n        ClientWithMiddleware {\n            client,\n            printer: None,\n            middlewares: vec![],\n        }\n    }\n\n    pub fn with_printer(mut self, printer: T) -> Self {\n        self.printer = Some(printer);\n        self\n    }\n\n    pub fn with(mut self, middleware: impl Middleware + 'a) -> Self {\n        self.middlewares.push(Box::new(middleware));\n        self\n    }\n\n    pub fn execute(&mut self, request: Request) -> Result<Response> {\n        let mut ctx = Context::new(\n            self.client,\n            self.printer.as_mut().map(|p| p as _),\n            &mut self.middlewares[..],\n        );\n        ctx.execute(request)\n    }\n}\n"
  },
  {
    "path": "src/nested_json.rs",
    "content": "//! Insert value into JSON at an arbitrary location specified by a JSON path.\n//!\n//! Where JSON path is a string that satisfies the following syntax grammar:\n//! ```\n//!   start: root_path path*\n//!   root_path: (literal | index_path | append_path)\n//!   literal: TEXT | NUMBER\n//!\n//!   path: key_path | index_path | append_path\n//!   key_path: LEFT_BRACKET TEXT RIGHT_BRACKET\n//!   index_path: LEFT_BRACKET NUMBER RIGHT_BRACKET\n//!   append_path: LEFT_BRACKET RIGHT_BRACKET\n//! ```\n//!\n//! Additionally, a backslash character can be used to:\n//! - Escape characters with especial meaning such as `\\`, `[` and `]`.\n//! - Treat numbers as a key rather than as an index.\n\nuse std::{fmt, mem};\n\nuse anyhow::{Result, anyhow};\nuse serde_json::Value;\nuse serde_json::map::Map;\n\nuse crate::utils::unescape;\n\n#[derive(Debug, Clone)]\nenum Token {\n    LeftBracket(usize),\n    RightBracket(usize),\n    Text(String, (usize, usize)),\n    Number(usize, (usize, usize)),\n}\n\nimpl Token {\n    fn literal(json_path: &str, start: Option<usize>, end: Option<usize>) -> Token {\n        const SPECIAL_CHARS: &str = \"=@:;[]\\\\\";\n        let start = start.map_or(0, |s| s + 1);\n        let end = end.unwrap_or(json_path.len());\n        let span = (start, end);\n\n        if start == 0 {\n            // The first token is never interpreted as a number and therefore\n            // the number escaping rule doesn't apply to it.\n            Token::Text(unescape(&json_path[start..end], SPECIAL_CHARS), span)\n        } else {\n            let literal = &json_path[start..end];\n            if literal.starts_with('\\\\') && literal[1..].parse::<usize>().is_ok() {\n                Token::Text(literal[1..].to_string(), span)\n            } else if let Ok(n) = literal.parse::<usize>() {\n                Token::Number(n, span)\n            } else {\n                Token::Text(unescape(literal, SPECIAL_CHARS), span)\n            }\n        }\n    }\n\n    fn is_empty(&self) -> bool {\n        match self {\n            Token::Text(_, (start, end)) => start == end,\n            _ => false,\n        }\n    }\n}\n\nfn tokenize(json_path: &str) -> impl IntoIterator<Item = Token> {\n    let mut tokens = vec![];\n    let mut escaped = false;\n    let mut last_delim_pos = None;\n\n    for (i, ch) in json_path.char_indices() {\n        if ch == '\\\\' {\n            escaped = !escaped;\n        } else if ch == '[' && !escaped {\n            tokens.push(Token::literal(json_path, last_delim_pos, Some(i)));\n            tokens.push(Token::LeftBracket(i));\n            last_delim_pos = Some(i);\n            escaped = false;\n        } else if ch == ']' && !escaped {\n            tokens.push(Token::literal(json_path, last_delim_pos, Some(i)));\n            tokens.push(Token::RightBracket(i));\n            last_delim_pos = Some(i);\n            escaped = false;\n        } else {\n            escaped = false;\n        }\n    }\n    tokens.push(Token::literal(json_path, last_delim_pos, None));\n\n    tokens.into_iter().filter(|t| !t.is_empty())\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum PathAction {\n    Key(String, (usize, usize)),\n    Index(usize, (usize, usize)),\n    Append((usize, usize)),\n}\n\npub fn parse_path(json_path: &str) -> Result<Vec<PathAction>> {\n    use PathAction::*;\n    use Token::*;\n\n    let mut path = vec![];\n    let mut tokens_iter = tokenize(json_path).into_iter();\n\n    match tokens_iter.next() {\n        Some(LeftBracket(start)) => match tokens_iter.next() {\n            Some(Number(index, _)) => {\n                if let Some(RightBracket(end)) = tokens_iter.next() {\n                    path.push(Index(index, (start, end + 1)));\n                } else {\n                    return Err(syntax_error(\"']'\", start + 1, json_path));\n                }\n            }\n            Some(RightBracket(end)) => path.push(Append((start, end + 1))),\n            Some(Text(..) | LeftBracket(..)) | None => {\n                return Err(syntax_error(\"number or ']'\", start + 1, json_path));\n            }\n        },\n        Some(Text(key, span)) => path.push(Key(key, span)),\n        Some(Number(..) | RightBracket(..)) | None => {\n            return Err(syntax_error(\"text or '['\", 0, json_path));\n        }\n    }\n\n    while let Some(token) = tokens_iter.next() {\n        let start = match token {\n            LeftBracket(start) => start,\n            RightBracket(start) | Text(_, (start, _)) | Number(_, (start, _)) => {\n                return Err(syntax_error(\"'['\", start, json_path));\n            }\n        };\n\n        match tokens_iter.next() {\n            Some(Number(index, span)) => {\n                if let Some(RightBracket(end)) = tokens_iter.next() {\n                    path.push(Index(index, (start, end + 1)));\n                } else {\n                    return Err(syntax_error(\"']'\", span.1, json_path));\n                }\n            }\n            Some(Text(key, span)) => {\n                if let Some(RightBracket(end)) = tokens_iter.next() {\n                    path.push(Key(key, (start, end + 1)));\n                } else {\n                    return Err(syntax_error(\"']'\", span.1, json_path));\n                }\n            }\n            Some(RightBracket(end)) => path.push(Append((start, end + 1))),\n            Some(LeftBracket(..)) | None => {\n                return Err(syntax_error(\"text, number or ']'\", start + 1, json_path));\n            }\n        }\n    }\n\n    Ok(path)\n}\n\npub fn insert(\n    root: Option<Value>,\n    path: &[PathAction],\n    value: Value,\n) -> std::result::Result<Value, Box<TypeError>> {\n    assert!(!path.is_empty(), \"path should not be empty\");\n\n    Ok(match root {\n        Some(Value::Object(mut obj)) => {\n            let key = match &path[0] {\n                PathAction::Key(v, ..) => v.to_string(),\n                path_component @ (PathAction::Index(..) | PathAction::Append(..)) => {\n                    return Err(Box::new(TypeError::new(\n                        Value::Object(obj),\n                        path_component.clone(),\n                    )));\n                }\n            };\n            if path.len() == 1 {\n                obj.insert(key, value);\n            } else {\n                let temp = obj.remove(&key);\n                let value = insert(temp, &path[1..], value)?;\n                obj.insert(key, value);\n            };\n            Value::Object(obj)\n        }\n        Some(Value::Array(mut arr)) => {\n            let index = match &path[0] {\n                path_component @ PathAction::Key(..) => {\n                    return Err(Box::new(TypeError::new(\n                        Value::Array(arr),\n                        path_component.clone(),\n                    )));\n                }\n                PathAction::Index(v, ..) => *v,\n                PathAction::Append(..) => arr.len(),\n            };\n            if path.len() == 1 {\n                arr_insert(&mut arr, index, value);\n            } else {\n                let temp = remove_from_arr(&mut arr, index);\n                let value = insert(temp, &path[1..], value)?;\n                arr_insert(&mut arr, index, value);\n            };\n            Value::Array(arr)\n        }\n        Some(root) => {\n            return Err(Box::new(TypeError::new(root, path[0].clone())));\n        }\n        None => match path[0] {\n            PathAction::Key(..) => insert(Some(Value::Object(Map::new())), path, value)?,\n            PathAction::Index(..) | PathAction::Append(..) => {\n                insert(Some(Value::Array(vec![])), path, value)?\n            }\n        },\n    })\n}\n\n/// Inserts value into array at any index and pads empty slots\n/// with Value::null if needed\nfn arr_insert(arr: &mut Vec<Value>, index: usize, value: Value) {\n    while index >= arr.len() {\n        arr.push(Value::Null);\n    }\n    arr[index] = value;\n}\n\n/// Removes an element from array and replace it with `Value::Null`.\nfn remove_from_arr(arr: &mut [Value], index: usize) -> Option<Value> {\n    if index < arr.len() {\n        Some(mem::replace(&mut arr[index], Value::Null))\n    } else {\n        None\n    }\n}\n\nfn syntax_error(expected: &'static str, pos: usize, json_path: &str) -> anyhow::Error {\n    anyhow!(\n        \"expected {}\\n\\n{}\",\n        expected,\n        highlight_error(json_path, pos, pos + 1)\n    )\n}\n\nfn highlight_error(text: &str, start: usize, mut end: usize) -> String {\n    use unicode_width::UnicodeWidthStr;\n    // Apply right-padding so outside of the text could be highlighted\n    let text = format!(\"{text:<end$}\");\n    // Ensure end doesn't fall on non-char boundary\n    while !text.is_char_boundary(end) && end < text.len() {\n        end += 1;\n    }\n    format!(\n        \"  {}\\n  {}{}\",\n        text,\n        \" \".repeat(text[0..start].width()),\n        \"^\".repeat(text[start..end].width())\n    )\n}\n\n#[derive(Debug, Clone)]\npub struct TypeError {\n    root: Value,\n    path_component: PathAction,\n    json_path: Option<String>,\n}\n\nimpl TypeError {\n    fn new(root: Value, path_component: PathAction) -> Self {\n        TypeError {\n            root,\n            path_component,\n            json_path: None,\n        }\n    }\n\n    pub fn with_json_path(mut self, json_path: String) -> TypeError {\n        self.json_path = Some(json_path);\n        self\n    }\n}\n\nimpl std::error::Error for TypeError {}\n\nimpl fmt::Display for TypeError {\n    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {\n        let root_type = match self.root {\n            Value::Null => \"null\",\n            Value::Bool(_) => \"bool\",\n            Value::Number(_) => \"number\",\n            Value::String(_) => \"string\",\n            Value::Array(_) => \"array\",\n            Value::Object(_) => \"object\",\n        };\n        let (access_type, expected_root_type, (start, end)) = match self.path_component {\n            PathAction::Append(pos) => (\"append\", \"array\", pos),\n            PathAction::Index(_, pos) => (\"index\", \"array\", pos),\n            PathAction::Key(_, pos) => (\"key\", \"object\", pos),\n        };\n\n        if let Some(json_path) = &self.json_path {\n            write!(\n                f,\n                \"Can't perform '{}' based access on '{}' which has a type of '{}' but this operation requires a type of '{}'.\\n\\n{}\",\n                access_type,\n                &json_path[..start],\n                root_type,\n                expected_root_type,\n                highlight_error(json_path, start, end)\n            )\n        } else {\n            write!(\n                f,\n                \"Can't perform '{access_type}' based access on '{root_type}'\"\n            )\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    use serde_json::json;\n\n    #[test]\n    fn deeply_nested_object() {\n        let root = insert(None, &parse_path(\"foo[bar][baz]\").unwrap(), 5.into());\n        assert_eq!(root.unwrap(), json!({\"foo\": {\"bar\": {\"baz\": 5}}}));\n    }\n\n    #[test]\n    fn deeply_nested_array() {\n        let root = insert(None, &parse_path(\"[0][0][1]\").unwrap(), 5.into());\n        assert_eq!(root.unwrap(), json!([[[null, 5]]]));\n    }\n\n    #[test]\n    fn can_override_value() {\n        let root = insert(None, &parse_path(\"foo[x]\").unwrap(), 5.into());\n        assert_eq!(root.clone().unwrap(), json!({\"foo\": {\"x\": 5}}));\n\n        let root = insert(\n            root.unwrap().into(),\n            &parse_path(\"foo[y]\").unwrap(),\n            true.into(),\n        );\n        assert_eq!(root.clone().unwrap(), json!({\"foo\": {\"x\": 5, \"y\": true}}));\n\n        let root = insert(\n            root.unwrap().into(),\n            &parse_path(\"foo[x]\").unwrap(),\n            6.into(),\n        );\n        assert_eq!(root.unwrap(), json!({\"foo\": {\"x\": 6, \"y\": true}}));\n    }\n\n    #[test]\n    fn type_clashes() {\n        // object array clash\n        let root = insert(None, &parse_path(\"foo\").unwrap(), 5.into());\n        let root = insert(root.unwrap().into(), &parse_path(\"[0]\").unwrap(), 5.into());\n        assert!(root.is_err());\n\n        // array object clash\n        let root = insert(None, &parse_path(\"[0]\").unwrap(), 5.into());\n        let root = insert(root.unwrap().into(), &parse_path(\"foo\").unwrap(), 5.into());\n        assert!(root.is_err());\n\n        // number object clash\n        let root = insert(None, &parse_path(\"foo\").unwrap(), 5.into());\n        let root = insert(\n            root.unwrap().into(),\n            &parse_path(\"foo[x]\").unwrap(),\n            5.into(),\n        );\n        assert!(root.is_err());\n\n        // number array clash\n        let root = insert(None, &parse_path(\"foo\").unwrap(), 5.into());\n        let root = insert(\n            root.unwrap().into(),\n            &parse_path(\"foo[0]\").unwrap(),\n            5.into(),\n        );\n        assert!(root.is_err());\n    }\n\n    #[test]\n    fn json_path_parser() {\n        use PathAction::*;\n\n        assert_eq!(\n            parse_path(r\"foo\\[x\\][]\").unwrap(),\n            [Key(r\"foo[x]\".into(), (0, 8)), Append((8, 10))]\n        );\n        assert_eq!(\n            parse_path(r\"foo\\\\[x]\").unwrap(),\n            [Key(r\"foo\\\".into(), (0, 5)), Key(\"x\".into(), (5, 8))]\n        );\n        assert_eq!(\n            parse_path(r\"foo[ba\\[ar][9]\").unwrap(),\n            [\n                Key(\"foo\".into(), (0, 3)),\n                Key(\"ba[ar\".into(), (3, 11)),\n                Index(9, (11, 14))\n            ]\n        );\n        assert_eq!(\n            parse_path(r\"[0][foo]\").unwrap(),\n            [Index(0, (0, 3)), Key(\"foo\".into(), (3, 8))]\n        );\n        assert_eq!(\n            parse_path(r\"[][foo]\").unwrap(),\n            [Append((0, 2)), Key(\"foo\".into(), (2, 7))]\n        );\n        assert_eq!(\n            parse_path(r\"foo[0]\").unwrap(),\n            [Key(\"foo\".into(), (0, 3)), Index(0, (3, 6))]\n        );\n        assert_eq!(\n            parse_path(r\"foo[\\0]\").unwrap(),\n            [Key(\"foo\".into(), (0, 3)), Key(\"0\".into(), (3, 7))]\n        );\n        assert_eq!(\n            parse_path(r\"foo[\\\\0]\").unwrap(),\n            [Key(\"foo\".into(), (0, 3)), Key(r\"\\0\".into(), (3, 8)),]\n        );\n        // HTTPie currently escapes numbers regardless of whether they are between\n        // square brackets or not. See https://github.com/httpie/httpie/issues/1408\n        //\n        // $ http --offline --pretty=none --print=B : \\\\0[\\\\5]=x\n        // {\"0\":{\"5\": \"x\"}}\n        // $ xh --offline --pretty=none --print=B : \\\\0[\\\\5]=x\n        // {\"\\\\0\": {\"5\": \"x\"}}\n        assert_eq!(parse_path(r\"\\5\").unwrap(), [Key(r\"\\5\".into(), (0, 2))]);\n        assert_eq!(\n            parse_path(r\"5[x]\").unwrap(),\n            [Key(\"5\".into(), (0, 1)), Key(\"x\".into(), (1, 4))]\n        );\n\n        assert!(parse_path(r\"[y][5]\").is_err());\n        assert!(parse_path(r\"x[y]h[z]\").is_err());\n        assert!(parse_path(r\"x[y]h\").is_err());\n        assert!(parse_path(r\"[x][y]h\").is_err());\n        assert!(parse_path(\"foo[😀]x\").is_err());\n        assert!(parse_path(r\"foo[bar]\\[baz]\").is_err());\n        assert!(parse_path(r\"foo\\[bar][baz]\").is_err());\n\n        // shouldn't panic when highlighting a key with unicode chars\n        assert!(parse_path(\"[😀\").is_err());\n        assert!(parse_path(\"[][😀\").is_err());\n    }\n}\n"
  },
  {
    "path": "src/netrc.rs",
    "content": "//! See https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html\n//!\n//! And https://github.com/curl/curl/blob/b01165680450364bdc770da3c7ede190872286c8/lib/netrc.c\n//!\n//! HTTPie has this behavior:\n//!\n//! - Entries must have both a login and a password or they'll be ignored or misbehave.\n//!\n//! - Fields from a default entry are not mixed with those of another entry.\n//!\n//! - An incomplete entry doesn't allow the default entry as a fallback.\n//!\n//! - The default entry doesn't have to be at the end of the file.\n//!\n//! HTTPie uses the implementation from Python's standard library\n//! (with a wrapper from requests).\n//!\n//! This implementation is not at all strict, files are never rejected outright.\n//! We'd ignore errors anyway to match HTTPie so that might be for the best.\n//! (HTTPie's parser is strict, so a minor problem will silently stop the file\n//! from being used.)\n//!\n//! This implementation additionally handles entries with just a password and no login,\n//! to support using .netrc for bearer auth.\n//!\n//! This is too specialized for our use case to be a crate, but feel free to\n//! copy/paste into another project and modify.\n\nuse std::{\n    fs::File,\n    io::{self, BufRead, BufReader},\n};\n\nuse encoding_rs::UTF_8;\nuse encoding_rs_io::DecodeReaderBytesBuilder;\n\nuse crate::utils::get_home_dir;\n\n#[derive(Debug, PartialEq, Eq)]\npub struct Entry {\n    pub login: Option<String>,\n    pub password: String,\n}\n\npub fn find_entry(host: url::Host<&str>) -> Option<Entry> {\n    let file = open_netrc()?;\n    // UTF-16 is detected if it has a BOM.\n    // Invalid UTF-8 is sanitized with replacement characters. That way it\n    // at least won't stop us from parsing the rest of the file.\n    let file = DecodeReaderBytesBuilder::new()\n        .encoding(Some(UTF_8))\n        .bom_override(true)\n        .build(file);\n    let file = BufReader::new(file);\n    let parser = Parser::new(file, host);\n    // Logging I/O errors would be nice.\n    parser.parse().ok()?\n}\n\nfn open_netrc() -> Option<File> {\n    match std::env::var_os(\"NETRC\") {\n        Some(path) => File::open(path).ok(),\n        None => {\n            let home_dir = get_home_dir()?;\n            for name in [\".netrc\", \"_netrc\"] {\n                let path = home_dir.join(name);\n                if let Ok(file) = File::open(path) {\n                    return Some(file);\n                }\n            }\n            None\n        }\n    }\n}\n\n#[derive(Copy, Clone)]\nenum EntryState {\n    /// We're outside any entry, or in one for the wrong host.\n    Wrong,\n    /// We're inside the entry for the host we want.\n    Correct,\n    /// We're inside the default entry.\n    Default,\n}\n\nstruct Parser<'a, R> {\n    reader: R,\n    /// The current line.\n    buf: String,\n    /// The index in `buf` to start looking for the next word.\n    pos: usize,\n    /// The host we're looking for.\n    host: url::Host<&'a str>,\n    /// Info about the entry we're handling.\n    state: EntryState,\n    /// The data collected for the current entry.\n    login: Option<String>,\n    password: Option<String>,\n    account: Option<String>,\n    /// Whether to block the default entry from being returned.\n    suppress_default: bool,\n    /// The default entry, to return if no other can be found.\n    default: Option<Entry>,\n    /// A complete relevant entry, to be returned ASAP.\n    entry: Option<Entry>,\n}\n\nimpl<'a, R: BufRead> Parser<'a, R> {\n    fn new(reader: R, host: url::Host<&'a str>) -> Self {\n        Parser {\n            reader,\n            buf: String::new(),\n            pos: 0,\n            host,\n            state: EntryState::Wrong,\n            login: None,\n            password: None,\n            account: None,\n            suppress_default: false,\n            default: None,\n            entry: None,\n        }\n    }\n\n    fn parse(mut self) -> io::Result<Option<Entry>> {\n        while let Some(word) = self.word()? {\n            // curl does a case-insensitive comparison here but that\n            // seems unnecessary.\n            match word {\n                \"default\" => {\n                    // The default entry. Some implementations want you to put it at the\n                    // end of the file so they can unconditionally stop after finding it,\n                    // we'll use it as a true fallback (like Python does).\n                    self.finish_entry();\n                    self.state = EntryState::Default;\n                }\n                \"machine\" => {\n                    self.finish_entry();\n                    if let Some(new_host) = self.word()? {\n                        match url::Host::parse(new_host) {\n                            Ok(new_host) if self.host == new_host => {\n                                self.state = EntryState::Correct;\n                                self.suppress_default = true;\n                            }\n                            _ => {\n                                self.state = EntryState::Wrong;\n                            }\n                        }\n                    }\n                }\n                \"login\" => {\n                    if let Some(login) = self.arg()? {\n                        self.login = Some(login);\n                    }\n                }\n                \"password\" => {\n                    if let Some(password) = self.arg()? {\n                        // Some implementations check the permissions of the file here.\n                        // It should be owned by the current user and not be readable by\n                        // anyone else. (Unless it contains no passwords.)\n                        // But that's a lot of work and somewhat less vital in the\n                        // single-user age. Python's stdlib does it by default, but\n                        // requests/HTTPie avoids that check.\n                        self.password = Some(password);\n                    }\n                }\n                \"account\" => {\n                    // requests/HTTPie uses this as a fallback for login.\n                    if let Some(account) = self.arg()? {\n                        self.account = Some(account);\n                    }\n                }\n                \"macdef\" => {\n                    // Macro definition. We ignore these.\n                    self.finish_entry();\n                    // Consume the macro's name.\n                    self.word()?;\n                    // Skip until the next blank line.\n                    // (We consider a line with just whitespace blank.)\n                    self.advance_line()?;\n                    while !self.buf.trim().is_empty() {\n                        self.advance_line()?;\n                    }\n                }\n                word if word.starts_with('#') => {\n                    // Comment, skip the rest of the line.\n                    // By doing the check here instead of in Reader::word() we allow\n                    // arguments to machine/login/password/account to start with #. Curl\n                    // doesn't do this.\n                    // Python supports comments but seems to dislike blank lines inbetween\n                    // commented lines.\n                    self.advance_line()?;\n                }\n                _ => {\n                    // Unknown word. We don't crash, but do consider this the end\n                    // of the entry.\n                    self.finish_entry();\n                }\n            }\n            if let Some(entry) = self.entry {\n                return Ok(Some(entry));\n            }\n        }\n        self.finish_entry();\n        if let Some(entry) = self.entry {\n            Ok(Some(entry))\n        } else if self.suppress_default {\n            Ok(None)\n        } else {\n            Ok(self.default)\n        }\n    }\n\n    /// Reset the current entry state. Try to build an entry out of what was gathered.\n    fn finish_entry(&mut self) {\n        let login = self.login.take();\n        let account = self.account.take();\n        let password = self.password.take();\n\n        let state = self.state;\n        self.state = EntryState::Wrong;\n\n        if let (login, Some(password)) = (login.or(account), password) {\n            let entry = Entry { login, password };\n            match state {\n                EntryState::Wrong => unreachable!(\"netrc: Should not have been storing info\"),\n                EntryState::Correct => self.entry = Some(entry),\n                EntryState::Default => self.default = Some(entry),\n            }\n        }\n    }\n\n    /// Consume the next word. Return it only if we're processing a relevant entry.\n    fn arg(&mut self) -> io::Result<Option<String>> {\n        let state = self.state;\n        let word = self.word()?;\n        match state {\n            EntryState::Wrong => Ok(None),\n            EntryState::Correct | EntryState::Default => Ok(word.map(str::to_owned)),\n        }\n    }\n\n    /// Advance the reader/buffer to the next line.\n    fn advance_line(&mut self) -> io::Result<usize> {\n        self.buf.clear();\n        self.pos = 0;\n        self.reader.read_line(&mut self.buf)\n    }\n\n    /// Read the next word, if any.\n    fn word(&mut self) -> io::Result<Option<&str>> {\n        loop {\n            match self.buf[self.pos..].chars().next() {\n                Some(ch) if ch.is_whitespace() => self.pos += ch.len_utf8(),\n                Some(_) => {\n                    let text = self.buf[self.pos..].split_whitespace().next().unwrap();\n                    self.pos += text.len();\n                    return Ok(Some(text));\n                }\n                None => {\n                    if self.advance_line()? == 0 {\n                        return Ok(None);\n                    }\n                }\n            }\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    use std::net::Ipv4Addr;\n\n    #[test]\n    fn cases() {\n        const COM: url::Host<&str> = url::Host::Domain(\"example.com\");\n        const ORG: url::Host<&str> = url::Host::Domain(\"example.org\");\n        const UNI: url::Host<&str> = url::Host::Domain(\"xn--9ca.com\");\n        const IP1: url::Host<&str> = url::Host::Ipv4(Ipv4Addr::new(1, 1, 1, 1));\n        const IP2: url::Host<&str> = url::Host::Ipv4(Ipv4Addr::new(2, 2, 2, 2));\n\n        const SIMPLE: &str = \"\n            machine example.com\n            login user\n            password pass\n        \";\n        found(SIMPLE, COM, \"user\", \"pass\");\n        notfound(SIMPLE, ORG);\n        notfound(SIMPLE, UNI);\n        notfound(SIMPLE, IP1);\n\n        const ONELINE: &str = \"\n            machine example.com login user password pass\n        \";\n        found(ONELINE, COM, \"user\", \"pass\");\n        notfound(ONELINE, ORG);\n\n        const MULTI: &str = \"\n            machine example.com login user password pass\n            machine example.org login foo password bar\n        \";\n        found(MULTI, COM, \"user\", \"pass\");\n        found(MULTI, ORG, \"foo\", \"bar\");\n        notfound(MULTI, UNI);\n\n        const UNICODE: &str = \"\n            machine É.com login user password pass\n        \";\n        found(UNICODE, UNI, \"user\", \"pass\");\n        notfound(UNICODE, COM);\n\n        const MISSING_PASS: &str = \"\n            machine example.com login user\n        \";\n        notfound(MISSING_PASS, COM);\n\n        const MISSING_USER: &str = \"\n            machine example.com password pass\n            default login user\n        \";\n        found(MISSING_USER, COM, None, \"pass\");\n        notfound(MISSING_USER, ORG);\n\n        const DEFAULT_PASSWORD_MISSING_USER: &str = \"\n            machine example.com password pass\n            default password def\n        \";\n        found(DEFAULT_PASSWORD_MISSING_USER, COM, None, \"pass\");\n        found(DEFAULT_PASSWORD_MISSING_USER, ORG, None, \"def\");\n\n        const DEFAULT_LAST: &str = \"\n            machine example.com login ex password am\n            default login def password ault\n        \";\n        found(DEFAULT_LAST, COM, \"ex\", \"am\");\n        found(DEFAULT_LAST, ORG, \"def\", \"ault\");\n\n        const DEFAULT_FIRST: &str = \"\n            default login def password ault\n            machine example.com login ex password am\n        \";\n        found(DEFAULT_FIRST, COM, \"ex\", \"am\");\n        found(DEFAULT_FIRST, ORG, \"def\", \"ault\");\n\n        const ACCOUNT_FALLBACK: &str = \"\n            machine example.com account acc password pass\n        \";\n        found(ACCOUNT_FALLBACK, COM, \"acc\", \"pass\");\n\n        const ACCOUNT_NOT_PREFERRED: &str = \"\n            machine example.com password pass login log account acc\n            machine example.org password pass account acc login log\n        \";\n        found(ACCOUNT_NOT_PREFERRED, COM, \"log\", \"pass\");\n        found(ACCOUNT_NOT_PREFERRED, ORG, \"log\", \"pass\");\n\n        const WITH_IP: &str = \"\n            machine 1.1.1.1 login us password pa\n        \";\n        found(WITH_IP, IP1, \"us\", \"pa\");\n        notfound(WITH_IP, IP2);\n        notfound(WITH_IP, COM);\n\n        const WEIRD_IP: &str = \"\n            machine 16843009 login us password pa\n        \";\n        found(WEIRD_IP, IP1, \"us\", \"pa\");\n        notfound(WEIRD_IP, IP2);\n        notfound(WEIRD_IP, COM);\n\n        const MALFORMED: &str = \"\n            I'm a malformed netrc!\n        \";\n        notfound(MALFORMED, COM);\n\n        const COMMENT: &str = \"\n            # machine example.com login user password pass\n            machine example.org login lo password pa\n        \";\n        notfound(COMMENT, COM);\n        found(COMMENT, ORG, \"lo\", \"pa\");\n\n        const OCTOTHORPE_IN_VALUE: &str = \"\n            machine example.com login #!@$ password pass\n        \";\n        found(OCTOTHORPE_IN_VALUE, COM, \"#!@$\", \"pass\");\n\n        const SUDDEN_END: &str = \"\n            machine example.com login\n        \";\n        notfound(SUDDEN_END, COM);\n\n        const INCOMPLETE_AND_DEFAULT: &str = \"\n            machine example.com login user\n            default login u password p\n        \";\n        notfound(INCOMPLETE_AND_DEFAULT, COM);\n        found(INCOMPLETE_AND_DEFAULT, ORG, \"u\", \"p\");\n\n        const UNKNOWN_TOKEN_INTERRUPT: &str = \"\n            machine example.com\n            login user\n            foo bar\n            password pass\n        \";\n        notfound(UNKNOWN_TOKEN_INTERRUPT, COM);\n\n        const MACRO: &str = \"\n            macdef foo\n            machine example.com login mac password def\n            qux\n\n            machine example.com login user password pass\n        \";\n        found(MACRO, COM, \"user\", \"pass\");\n        notfound(MACRO, ORG);\n\n        const MACRO_UNTERMINATED: &str = \"\n            macdef foo\n            machine example.com login mac password def\n            qux\n            machine example.com login user password pass\";\n        notfound(MACRO_UNTERMINATED, COM);\n\n        const MACRO_BLANK_LINE_BEFORE_NAME: &str = \"\n            macdef\n\n            foo\n            machine example.com login mac password def\";\n        notfound(MACRO_BLANK_LINE_BEFORE_NAME, COM);\n\n        const MANY_LINES: &str = \"\n            machine\n            example.com\n            login\n\n            user\n            password\n            pass\n        \";\n        found(MANY_LINES, COM, \"user\", \"pass\");\n\n        const STRANGE_CHARACTERS: &str = \"\n            machine\\u{2029}oké\\t\\u{2029}login  u   password  p\\t\\t\\t\\r\\n\n        \";\n        notfound(STRANGE_CHARACTERS, COM);\n    }\n\n    #[track_caller]\n    fn found(\n        netrc: &str,\n        host: url::Host<&str>,\n        login: impl Into<Option<&'static str>>,\n        password: &str,\n    ) {\n        let entry = Parser::new(netrc.as_bytes(), host).parse().unwrap();\n        let entry = entry.expect(\"Didn't find entry\");\n        assert_eq!(entry.login.as_deref(), login.into());\n        assert_eq!(entry.password, password);\n    }\n\n    #[track_caller]\n    fn notfound(netrc: &str, host: url::Host<&str>) {\n        let entry = Parser::new(netrc.as_bytes(), host).parse().unwrap();\n        assert!(entry.is_none(), \"Found entry\");\n    }\n}\n"
  },
  {
    "path": "src/printer.rs",
    "content": "use std::borrow::Cow;\nuse std::io::{self, BufRead, BufReader, Read, Write};\nuse std::time::Instant;\n\nuse encoding_rs::Encoding;\nuse encoding_rs_io::DecodeReaderBytesBuilder;\nuse mime::Mime;\nuse reqwest::blocking::{Body, Request, Response};\nuse reqwest::cookie::CookieStore;\nuse reqwest::header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE, COOKIE, HOST, HeaderMap, HeaderValue};\nuse url::Url;\n\nuse crate::formatting::headers::HeaderFormatter;\nuse crate::utils::reason_phrase;\nuse crate::{\n    buffer::Buffer,\n    cli::FormatOptions,\n    cli::{Pretty, Theme},\n    decoder::{decompress, get_compression_type},\n    formatting::serde_json_format,\n    formatting::{Highlighter, format_xml, get_json_formatter},\n    middleware::ResponseExt,\n    utils::{BUFFER_SIZE, copy_largebuf, test_mode},\n};\n\nconst BINARY_SUPPRESSOR: &str = concat!(\n    \"+-----------------------------------------+\\n\",\n    \"| NOTE: binary data not shown in terminal |\\n\",\n    \"+-----------------------------------------+\\n\",\n    \"\\n\"\n);\n\n/// A wrapper around a reader that reads line by line, (optionally) returning\n/// an error if the line appears to be binary.\n///\n/// This is meant for streaming output. `checked` should typically be\n/// set to buffer.is_terminal(), but if you need neither checking nor\n/// highlighting then you may not need a `BinaryGuard` at all.\n///\n/// This reader does not validate UTF-8.\nstruct BinaryGuard<'a, T: Read> {\n    reader: BufReader<&'a mut T>,\n    buffer: Vec<u8>,\n    checked: bool,\n}\n\n#[derive(Debug)]\nstruct FoundBinaryData;\n\nimpl std::fmt::Display for FoundBinaryData {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.write_str(\"binary data not shown in terminal\")\n    }\n}\n\nimpl std::error::Error for FoundBinaryData {}\n\nimpl<'a, T: Read> BinaryGuard<'a, T> {\n    fn new(reader: &'a mut T, checked: bool) -> Self {\n        Self {\n            reader: BufReader::with_capacity(BUFFER_SIZE, reader),\n            buffer: Vec::new(),\n            checked,\n        }\n    }\n\n    /// Return at least one complete line.\n    ///\n    /// Compared to returning exactly one line, this gives you more information\n    /// about when data comes in. It's better to flush after each `read_lines`\n    /// call than to flush after each individual line.\n    ///\n    /// We only work with complete lines to accommodate the syntax highlighting\n    /// and the binary data (null byte) detection. HTTPie processes exactly\n    /// one line at a time.\n    ///\n    /// We work off the assumption that if the response contains a null byte\n    /// then none of it should be shown, and therefore the earlier we detect\n    /// the null byte, the better. This basically matches the non-streaming\n    /// behavior. But if it takes a while for the first null byte to show up\n    /// then it's unpredictable when the plain text output is cut off by the\n    /// binary suppressor. HTTPie is more consistent in this regard.\n    fn read_lines(&mut self) -> io::Result<Option<&[u8]>> {\n        self.buffer.clear();\n        loop {\n            let buf = match self.reader.fill_buf() {\n                Ok(buf) => buf,\n                Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,\n                Err(e) => return Err(e),\n            };\n            if self.checked && buf.contains(&b'\\0') {\n                return Err(io::Error::new(io::ErrorKind::InvalidData, FoundBinaryData));\n            } else if buf.is_empty() {\n                if self.buffer.is_empty() {\n                    return Ok(None);\n                } else {\n                    return Ok(Some(&self.buffer));\n                }\n            } else if let Some(ind) = memchr::memrchr(b'\\n', buf) {\n                // Potential optimization: return a slice of buf instead of copying.\n                // (We'd have to delay the call to .consume() until the next call.)\n                // (There is a weird borrow checker problem.)\n                self.buffer.extend_from_slice(&buf[..=ind]);\n                self.reader.consume(ind + 1);\n                return Ok(Some(&self.buffer));\n            } else {\n                self.buffer.extend_from_slice(buf);\n                let n = buf.len(); // borrow checker\n                self.reader.consume(n);\n                // It would be nice to return early if self.buffer is growing very large\n                // or if it's been a long time since the last read. But especially the\n                // second is hard to implement, and we'd want to pair this with flushing\n                // the output buffer. (HTTPie does nothing of this kind.)\n            }\n        }\n    }\n}\n\npub struct Printer {\n    format_json: bool,\n    json_indent_level: usize,\n    format_xml: bool,\n    xml_indent_level: usize,\n    sort_headers: bool,\n    color: bool,\n    theme: Theme,\n    stream: Option<bool>,\n    buffer: Buffer,\n}\n\nimpl Printer {\n    pub fn new(\n        pretty: Pretty,\n        theme: Theme,\n        stream: impl Into<Option<bool>>,\n        buffer: Buffer,\n        format_options: FormatOptions,\n    ) -> Self {\n        Printer {\n            format_json: format_options.json_format.unwrap_or(pretty.format()),\n            json_indent_level: format_options.json_indent.unwrap_or(4),\n            format_xml: format_options.xml_format.unwrap_or(pretty.format()),\n            xml_indent_level: format_options.xml_indent.unwrap_or(2),\n            sort_headers: format_options.headers_sort.unwrap_or(pretty.format()),\n            color: pretty.color(),\n            stream: stream.into(),\n            theme,\n            buffer,\n        }\n    }\n\n    fn get_highlighter(&mut self, syntax: &'static str) -> Highlighter<'_> {\n        Highlighter::new(syntax, self.theme, &mut self.buffer)\n    }\n\n    fn get_header_formatter(&mut self) -> HeaderFormatter<'_, Buffer> {\n        let is_terminal = self.buffer.is_terminal();\n        HeaderFormatter::new(\n            &mut self.buffer,\n            self.color.then(|| self.theme.as_syntect_theme()),\n            is_terminal,\n            self.sort_headers,\n        )\n    }\n\n    fn print_colorized_text(&mut self, text: &str, syntax: &'static str) -> io::Result<()> {\n        self.get_highlighter(syntax).highlight(text)\n    }\n\n    fn print_syntax_text(&mut self, text: &str, syntax: &'static str) -> io::Result<()> {\n        if self.color {\n            self.print_colorized_text(text, syntax)\n        } else {\n            self.buffer.print(text)\n        }\n    }\n\n    fn print_json_text(&mut self, text: &str, check_valid: bool) -> io::Result<()> {\n        if !self.format_json {\n            // We don't have to do anything specialized, so fall back to the generic version\n            return self.print_syntax_text(text, \"json\");\n        }\n\n        if check_valid && !valid_json(text) {\n            // JSONXF may mess up the text, e.g. by removing whitespace\n            // This is somewhat common as application/json is the default\n            // content type for requests\n            return self.print_syntax_text(text, \"json\");\n        }\n\n        if self.color {\n            let mut buf = Vec::new();\n            serde_json_format(self.json_indent_level, text, &mut buf)?;\n            buf.write_all(b\"\\n\\n\")?;\n            // in principle, buf should already be valid UTF-8,\n            // because JSONXF doesn't mangle it\n            let text = String::from_utf8_lossy(&buf);\n            self.print_colorized_text(&text, \"json\")\n        } else {\n            serde_json_format(self.json_indent_level, text, &mut self.buffer)?;\n            self.buffer.write_all(b\"\\n\\n\")?;\n            self.buffer.flush()?;\n            Ok(())\n        }\n    }\n\n    fn print_xml_text(&mut self, body: &str) -> io::Result<()> {\n        if !self.format_xml {\n            return self.print_syntax_text(body, \"xml\");\n        }\n\n        let mut buf = match format_xml(self.xml_indent_level, body) {\n            Ok(buf) => buf,\n            Err(err) => {\n                log::debug!(\"Failed to format XML: {err}\");\n                return self.print_syntax_text(body, \"xml\");\n            }\n        };\n        buf.extend_from_slice(b\"\\n\\n\");\n\n        if self.color {\n            let text = String::from_utf8_lossy(&buf);\n            self.print_colorized_text(&text, \"xml\")\n        } else {\n            self.buffer.write_all(&buf)?;\n            self.buffer.flush()?;\n            Ok(())\n        }\n    }\n\n    fn print_body_text(&mut self, content_type: ContentType, body: &str) -> io::Result<()> {\n        match content_type {\n            ContentType::Json => self.print_json_text(body, true),\n            ContentType::Xml => self.print_xml_text(body),\n            ContentType::Html => self.print_syntax_text(body, \"html\"),\n            ContentType::Css => self.print_syntax_text(body, \"css\"),\n            // In HTTPie part of this behavior is gated behind the --json flag\n            // But it does JSON formatting even without that flag, so doing\n            // this check unconditionally is fine\n            ContentType::Text | ContentType::JavaScript if valid_json(body) => {\n                self.print_json_text(body, false)\n            }\n            ContentType::JavaScript => self.print_syntax_text(body, \"js\"),\n            _ => self.buffer.print(body),\n        }\n    }\n\n    fn print_stream(&mut self, reader: &mut impl Read) -> io::Result<()> {\n        if !self.buffer.is_terminal() {\n            return copy_largebuf(reader, &mut self.buffer, true);\n        }\n        let mut guard = BinaryGuard::new(reader, true);\n        while let Some(lines) = guard.read_lines()? {\n            self.buffer.write_all(lines)?;\n            self.buffer.flush()?;\n        }\n        Ok(())\n    }\n\n    fn print_colorized_stream(\n        &mut self,\n        stream: &mut impl Read,\n        syntax: &'static str,\n    ) -> io::Result<()> {\n        let mut guard = BinaryGuard::new(stream, self.buffer.is_terminal());\n        let mut highlighter = self.get_highlighter(syntax);\n        while let Some(lines) = guard.read_lines()? {\n            for line in lines.split_inclusive(|&b| b == b'\\n') {\n                highlighter.highlight_bytes(line)?;\n            }\n            highlighter.flush()?;\n        }\n        Ok(())\n    }\n\n    fn print_syntax_stream(\n        &mut self,\n        stream: &mut impl Read,\n        syntax: &'static str,\n    ) -> io::Result<()> {\n        if self.color {\n            self.print_colorized_stream(stream, syntax)\n        } else {\n            self.print_stream(stream)\n        }\n    }\n\n    fn print_json_stream(&mut self, stream: &mut impl Read) -> io::Result<()> {\n        if !self.format_json {\n            // We don't have to do anything specialized, so fall back to the generic version\n            self.print_syntax_stream(stream, \"json\")\n        } else if self.color {\n            let mut guard = BinaryGuard::new(stream, self.buffer.is_terminal());\n            let mut formatter = get_json_formatter(self.json_indent_level);\n            let mut highlighter = self.get_highlighter(\"json\");\n            let mut buf = Vec::new();\n            while let Some(lines) = guard.read_lines()? {\n                formatter.format_buf(lines, &mut buf)?;\n                for line in buf.split_inclusive(|&b| b == b'\\n') {\n                    highlighter.highlight_bytes(line)?;\n                }\n                highlighter.flush()?;\n                buf.clear();\n            }\n            Ok(())\n        } else {\n            let mut formatter = get_json_formatter(self.json_indent_level);\n            if !self.buffer.is_terminal() {\n                let mut buf = vec![0; BUFFER_SIZE];\n                loop {\n                    match stream.read(&mut buf) {\n                        Ok(0) => return Ok(()),\n                        Ok(n) => {\n                            formatter.format_buf(&buf[0..n], &mut self.buffer)?;\n                            self.buffer.flush()?;\n                        }\n                        Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,\n                        Err(e) => return Err(e),\n                    }\n                }\n            }\n            let mut guard = BinaryGuard::new(stream, true);\n            while let Some(lines) = guard.read_lines()? {\n                formatter.format_buf(lines, &mut self.buffer)?;\n                self.buffer.flush()?;\n            }\n            Ok(())\n        }\n    }\n\n    fn print_body_stream(\n        &mut self,\n        content_type: ContentType,\n        body: &mut impl Read,\n    ) -> io::Result<()> {\n        match content_type {\n            ContentType::Json => self.print_json_stream(body),\n            ContentType::Xml => self.print_syntax_stream(body, \"xml\"),\n            ContentType::Html => self.print_syntax_stream(body, \"html\"),\n            ContentType::Css => self.print_syntax_stream(body, \"css\"),\n            // print_body_text() has fancy JSON detection, but we can't do that here\n            ContentType::JavaScript => self.print_syntax_stream(body, \"js\"),\n            _ => self.print_stream(body),\n        }\n    }\n\n    pub fn print_separator(&mut self) -> io::Result<()> {\n        self.buffer.print(\"\\n\")?;\n        self.buffer.flush()?;\n        Ok(())\n    }\n\n    pub fn print_request_headers<T>(&mut self, request: &Request, cookie_jar: &T) -> io::Result<()>\n    where\n        T: CookieStore,\n    {\n        let url = request.url();\n        let version = request.version();\n        let mut headers = request.headers().clone();\n\n        headers\n            .entry(ACCEPT)\n            .or_insert_with(|| HeaderValue::from_static(\"*/*\"));\n\n        if let Some(cookie) = cookie_jar.cookies(url) {\n            headers.insert(COOKIE, cookie);\n        }\n\n        // See https://github.com/seanmonstar/reqwest/issues/1030\n        // reqwest and hyper add certain headers, but only in the process of\n        // sending the request, which we haven't done yet\n        if let Some(body) = request.body().and_then(Body::as_bytes) {\n            // Added at https://github.com/seanmonstar/reqwest/blob/c4ebb07343/src/blocking/request.rs#L144\n            headers\n                .entry(CONTENT_LENGTH)\n                .or_insert_with(|| body.len().into());\n        }\n        if let Some(host) = request.url().host_str() {\n            // FIXME: in case of HTTP/2 we probably don't send this. But we probably\n            // do send the :authority pseudo-header, and without --http-version we don't\n            // even know if we're going to use HTTP/2 yet.\n            headers.entry(HOST).or_insert_with(|| {\n                // Added at https://github.com/hyperium/hyper-util/blob/53aadac50d/src/client/legacy/client.rs#L278\n                if test_mode() {\n                    HeaderValue::from_str(\"http.mock\")\n                } else if let Some(port) = request.url().port() {\n                    HeaderValue::from_str(&format!(\"{host}:{port}\"))\n                } else {\n                    HeaderValue::from_str(host)\n                }\n                .expect(\"hostname should already be validated/parsed\")\n            });\n        }\n\n        self.get_header_formatter().print_request_headers(\n            request.method(),\n            request.url(),\n            version,\n            &headers,\n        )?;\n\n        self.buffer.print(\"\\n\")?;\n        self.buffer.flush()?;\n        Ok(())\n    }\n\n    pub fn print_response_headers(&mut self, response: &Response) -> io::Result<()> {\n        self.get_header_formatter().print_response_headers(\n            response.version(),\n            response.status(),\n            &reason_phrase(response),\n            response.headers(),\n        )?;\n\n        self.buffer.print(\"\\n\")?;\n        self.buffer.flush()?;\n        Ok(())\n    }\n\n    pub fn print_request_body(&mut self, request: &mut Request) -> anyhow::Result<()> {\n        let content_type = get_content_type(request.headers());\n        if let Some(body) = request.body_mut() {\n            let body = body.buffer()?;\n            if body.contains(&b'\\0') {\n                self.buffer.print(BINARY_SUPPRESSOR)?;\n            } else {\n                self.print_body_text(content_type, &String::from_utf8_lossy(body))?;\n                self.buffer.print(\"\\n\")?;\n            }\n            // Breathing room between request and response\n            self.buffer.print(\"\\n\")?;\n            self.buffer.flush()?;\n        }\n        Ok(())\n    }\n\n    pub fn print_response_body(\n        &mut self,\n        response: &mut Response,\n        encoding: Option<&'static Encoding>,\n        mime: Option<&str>,\n    ) -> anyhow::Result<()> {\n        let starting_time = Instant::now();\n        let url = response.url().clone();\n        let content_type =\n            mime.map_or_else(|| get_content_type(response.headers()), ContentType::from);\n        let encoding = encoding.or_else(|| get_charset(response));\n        let compression_type = get_compression_type(response.headers());\n        let mut body = decompress(response, compression_type);\n\n        // Automatically activate stream mode when it hasn't been set by the user and the content type is stream\n        let stream = self.stream.unwrap_or(content_type.is_stream());\n\n        if !self.buffer.is_terminal() {\n            if (self.color || self.format_json || self.format_xml) && content_type.is_text() {\n                // The user explicitly asked for formatting even though this is\n                // going into a file, and the response is at least supposed to be\n                // text, so decode it\n\n                // TODO: HTTPie re-encodes output in the original encoding, we don't\n                // encoding_rs::Encoder::encode_from_utf8_to_vec_without_replacement()\n                // and guess_encoding() may help, but it'll require refactoring\n\n                // The current design is a bit unfortunate because there's no way to\n                // force UTF-8 output without coloring or formatting\n                // Unconditionally decoding is not an option because the body\n                // might not be text at all\n                if stream {\n                    self.print_body_stream(\n                        content_type,\n                        &mut decode_stream(&mut body, encoding, &url)?,\n                    )?;\n                } else {\n                    let mut buf = Vec::new();\n                    body.read_to_end(&mut buf)?;\n                    let text = decode_blob_unconditional(&buf, encoding, &url);\n                    self.print_body_text(content_type, &text)?;\n                }\n            } else if stream {\n                copy_largebuf(&mut body, &mut self.buffer, true)?;\n            } else {\n                let mut buf = Vec::new();\n                body.read_to_end(&mut buf)?;\n                self.buffer.write_all(&buf)?;\n            }\n        } else if stream {\n            match self\n                .print_body_stream(content_type, &mut decode_stream(&mut body, encoding, &url)?)\n            {\n                Ok(_) => {\n                    self.buffer.print(\"\\n\")?;\n                }\n                Err(err) if err.get_ref().is_some_and(|err| err.is::<FoundBinaryData>()) => {\n                    self.buffer.print(BINARY_SUPPRESSOR)?;\n                }\n                Err(err) => return Err(err.into()),\n            }\n        } else {\n            let mut buf = Vec::new();\n            body.read_to_end(&mut buf)?;\n            match decode_blob(&buf, encoding, &url) {\n                None => {\n                    self.buffer.print(BINARY_SUPPRESSOR)?;\n                }\n                Some(text) => {\n                    self.print_body_text(content_type, &text)?;\n                    self.buffer.print(\"\\n\")?;\n                }\n            };\n        }\n        self.buffer.flush()?;\n        drop(body); // silence the borrow checker\n        response.meta_mut().content_download_duration = Some(starting_time.elapsed());\n        Ok(())\n    }\n\n    pub fn print_response_meta(&mut self, response: &Response) -> anyhow::Result<()> {\n        let meta = response.meta();\n        let mut total_elapsed_time = meta.request_duration.as_secs_f64();\n        if let Some(content_download_duration) = meta.content_download_duration {\n            total_elapsed_time += content_download_duration.as_secs_f64();\n        }\n        self.buffer\n            .print(&format!(\"Elapsed time: {total_elapsed_time:.5}s\\n\"))?;\n\n        if let Some(remote_addr) = response.remote_addr() {\n            self.buffer\n                .print(&format!(\"Remote address: {remote_addr:?}\\n\"))?;\n        }\n\n        self.buffer.print(\"\\n\")?;\n        Ok(())\n    }\n}\n\nenum ContentType {\n    Json,\n    Html,\n    Xml,\n    JavaScript,\n    Css,\n    Text,\n    UrlencodedForm,\n    Multipart,\n    EventStream,\n    Unknown,\n}\n\nimpl ContentType {\n    fn is_text(&self) -> bool {\n        match self {\n            ContentType::Unknown | ContentType::UrlencodedForm | ContentType::Multipart => false,\n            ContentType::Json\n            | ContentType::Html\n            | ContentType::Xml\n            | ContentType::JavaScript\n            | ContentType::Css\n            | ContentType::Text\n            | ContentType::EventStream => true,\n        }\n    }\n    fn is_stream(&self) -> bool {\n        match self {\n            ContentType::EventStream => true,\n            ContentType::Json\n            | ContentType::Html\n            | ContentType::Xml\n            | ContentType::JavaScript\n            | ContentType::Css\n            | ContentType::Text\n            | ContentType::UrlencodedForm\n            | ContentType::Multipart\n            | ContentType::Unknown => false,\n        }\n    }\n}\n\nimpl From<&str> for ContentType {\n    fn from(content_type: &str) -> Self {\n        if content_type.contains(\"json\") {\n            ContentType::Json\n        } else if content_type.contains(\"xml\") {\n            ContentType::Xml\n        } else if content_type.contains(\"html\") {\n            ContentType::Html\n        } else if content_type.contains(\"multipart\") {\n            ContentType::Multipart\n        } else if content_type.contains(\"x-www-form-urlencoded\") {\n            ContentType::UrlencodedForm\n        } else if content_type.contains(\"javascript\") {\n            ContentType::JavaScript\n        } else if content_type.contains(\"css\") {\n            ContentType::Css\n        } else if content_type.contains(\"event-stream\") {\n            ContentType::EventStream\n        } else if content_type.contains(\"text\") {\n            // We later check if this one's JSON\n            // HTTPie checks for \"json\", \"javascript\" and \"text\" in one place:\n            // https://github.com/httpie/httpie/blob/a32ad344dd/httpie/output/formatters/json.py#L14\n            // We have it more spread out but it behaves more or less the same\n            ContentType::Text\n        } else {\n            ContentType::Unknown\n        }\n    }\n}\n\nfn get_content_type(headers: &HeaderMap) -> ContentType {\n    headers\n        .get(CONTENT_TYPE)\n        .and_then(|value| value.to_str().ok())\n        .map_or(ContentType::Unknown, ContentType::from)\n}\n\nfn valid_json(text: &str) -> bool {\n    serde_json::from_str::<serde::de::IgnoredAny>(text).is_ok()\n}\n\n/// Decode a response, using BOM sniffing or chardet if the encoding is unknown.\n///\n/// This is different from [`Response::text`], which assumes UTF-8 as a fallback.\n///\n/// Returns `None` if the decoded text would contain null codepoints (i.e., is binary).\nfn decode_blob<'a>(\n    raw: &'a [u8],\n    encoding: Option<&'static Encoding>,\n    url: &Url,\n) -> Option<Cow<'a, str>> {\n    let encoding = encoding.unwrap_or_else(|| detect_encoding(raw, true, url));\n    // If the encoding is ASCII-compatible then a null byte corresponds to a\n    // null codepoint and vice versa, so we can check for them before decoding.\n    // For a 11MB binary file this saves 100ms, that's worth doing.\n    // UTF-16 is not ASCII-compatible: all ASCII characters are padded with a\n    // null byte, so finding a null byte doesn't mean anything.\n    if encoding.is_ascii_compatible() && raw.contains(&0) {\n        return None;\n    }\n    // Don't allow the BOM to override the encoding. But do remove it if\n    // it matches the encoding.\n    let text = encoding.decode_with_bom_removal(raw).0;\n    if !encoding.is_ascii_compatible() && text.contains('\\0') {\n        None\n    } else {\n        Some(text)\n    }\n}\n\n/// Like [`decode_blob`], but without binary detection.\nfn decode_blob_unconditional<'a>(\n    raw: &'a [u8],\n    encoding: Option<&'static Encoding>,\n    url: &Url,\n) -> Cow<'a, str> {\n    let encoding = encoding.unwrap_or_else(|| detect_encoding(raw, true, url));\n    encoding.decode_with_bom_removal(raw).0\n}\n\n/// Decode a streaming response in a way that matches [`decode_blob`].\n///\n/// As-is this should do a lossy decode with replacement characters, so the\n/// output is valid UTF-8, but a differently configured DecodeReaderBytes can\n/// produce invalid UTF-8.\nfn decode_stream<'a>(\n    stream: &'a mut impl Read,\n    encoding: Option<&'static Encoding>,\n    url: &Url,\n) -> io::Result<impl Read + 'a> {\n    // 16 KiB is the largest initial read I could achieve.\n    // That was with a HTTP/2 miniserve running on Linux.\n    // I think this is a buffer size for hyper, it could change. But it seems\n    // large enough for a best-effort attempt.\n    // (16 is otherwise used because 0 seems dangerous, but it shouldn't matter.)\n    let capacity = if encoding.is_some() { 16 } else { 16 * 1024 };\n    let mut reader = BufReader::with_capacity(capacity, stream);\n    let encoding = match encoding {\n        Some(encoding) => encoding,\n        None => {\n            // We need to guess the encoding.\n            // The more data we have the better our guess, but we can't just wait\n            // for all of it to arrive. The user explicitly asked us to hurry.\n            // HTTPie solves this by detecting the encoding separately for each line,\n            // but that's silly, and we don't necessarily go linewise.\n            // We'll just hope we get enough data in the very first read.\n            let peek = reader.fill_buf()?;\n            detect_encoding(peek, false, url)\n        }\n    };\n    // We could set .utf8_passthru(true) to not sanitize invalid UTF-8. It would\n    // arrive more faithfully in the terminal.\n    // But that has questionable benefit and writing invalid UTF-8 to stdout\n    // causes an error on Windows (because the console is UTF-16).\n    let reader = DecodeReaderBytesBuilder::new()\n        .encoding(Some(encoding))\n        .build(reader);\n    Ok(reader)\n}\n\nfn detect_encoding(mut bytes: &[u8], mut complete: bool, url: &Url) -> &'static Encoding {\n    // chardetng doesn't seem to take BOMs into account, so check those manually.\n    // We trust them unconditionally. (Should we?)\n    if bytes.starts_with(b\"\\xEF\\xBB\\xBF\") {\n        return encoding_rs::UTF_8;\n    } else if bytes.starts_with(b\"\\xFF\\xFE\") {\n        return encoding_rs::UTF_16LE;\n    } else if bytes.starts_with(b\"\\xFE\\xFF\") {\n        return encoding_rs::UTF_16BE;\n    }\n\n    // 64 KiB takes 2-5 ms to check on my machine. So even on slower machines\n    // that should be acceptable.\n    // If we check the full document we can easily spend most of our runtime\n    // inside chardetng. That's especially problematic because we usually get\n    // here for binary files, which we won't even end up showing.\n    const CHARDET_PEEK_SIZE: usize = 64 * 1024;\n    if bytes.len() > CHARDET_PEEK_SIZE {\n        bytes = &bytes[..CHARDET_PEEK_SIZE];\n        complete = false;\n    }\n\n    // HTTPie uses https://pypi.org/project/charset-normalizer/\n    let mut detector = chardetng::EncodingDetector::new();\n    detector.feed(bytes, complete);\n    let tld = url.domain().and_then(get_tld).map(str::as_bytes);\n    // The `allow_utf8` parameter is meant for HTML content:\n    // https://hsivonen.fi/utf-8-detection/\n    // We always enable it because we're more geared toward APIs than\n    // toward plain webpages, and because we don't have a full HTML parser\n    // to implement proper UTF-8 detection.\n    detector.guess(tld, true)\n}\n\nfn get_tld(domain: &str) -> Option<&str> {\n    // Fully qualified domain names end with a .\n    domain.trim_end_matches('.').rsplit('.').next()\n}\n\n/// Get the response's encoding from its Content-Type.\n///\n/// reqwest doesn't provide an API for this, and we don't want a fixed default.\n///\n/// See https://github.com/seanmonstar/reqwest/blob/2940740493/src/async_impl/response.rs#L172\nfn get_charset(response: &Response) -> Option<&'static Encoding> {\n    let content_type = response.headers().get(CONTENT_TYPE)?.to_str().ok()?;\n    let mime: Mime = content_type.parse().ok()?;\n    let encoding_name = mime.get_param(\"charset\")?.as_str();\n    Encoding::for_label(encoding_name.as_bytes())\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::utils::random_string;\n    use crate::{buffer::Buffer, cli::Cli, vec_of_strings};\n\n    use super::*;\n\n    fn run_cmd(args: impl IntoIterator<Item = String>, is_stdout_tty: bool) -> Printer {\n        let args = Cli::try_parse_from(args).unwrap();\n        let theme = args.style.unwrap_or_default();\n        let buffer = Buffer::new(args.download, args.output.as_deref(), is_stdout_tty).unwrap();\n        let pretty = args.pretty.unwrap_or_else(|| buffer.guess_pretty());\n        Printer::new(pretty, theme, false, buffer, FormatOptions::default())\n    }\n\n    fn temp_path() -> String {\n        let mut dir = std::env::temp_dir();\n        let filename = random_string();\n        dir.push(filename);\n        dir.to_str().unwrap().to_owned()\n    }\n\n    #[test]\n    fn terminal_mode() {\n        let p = run_cmd(vec_of_strings![\"xh\", \"httpbin.org/get\"], true);\n        assert_eq!(p.color, true);\n        assert!(p.buffer.is_stdout());\n    }\n\n    #[test]\n    fn redirect_mode() {\n        let p = run_cmd(vec_of_strings![\"xh\", \"httpbin.org/get\"], false);\n        assert_eq!(p.color, false);\n        assert!(p.buffer.is_redirect());\n    }\n\n    #[test]\n    fn terminal_mode_with_output_file() {\n        let output = temp_path();\n        let p = run_cmd(vec_of_strings![\"xh\", \"httpbin.org/get\", \"-o\", output], true);\n        assert_eq!(p.color, false);\n        assert!(p.buffer.is_file());\n    }\n\n    #[test]\n    fn redirect_mode_with_output_file() {\n        let output = temp_path();\n        let p = run_cmd(\n            vec_of_strings![\"xh\", \"httpbin.org/get\", \"-o\", output],\n            false,\n        );\n        assert_eq!(p.color, false);\n        assert!(p.buffer.is_file());\n    }\n\n    #[test]\n    fn terminal_mode_download() {\n        let p = run_cmd(vec_of_strings![\"xh\", \"httpbin.org/get\", \"-d\"], true);\n        assert_eq!(p.color, true);\n        assert!(p.buffer.is_stderr());\n    }\n\n    #[test]\n    fn redirect_mode_download() {\n        let p = run_cmd(vec_of_strings![\"xh\", \"httpbin.org/get\", \"-d\"], false);\n        assert_eq!(p.color, true);\n        assert!(p.buffer.is_stderr());\n    }\n\n    #[test]\n    fn terminal_mode_download_with_output_file() {\n        let output = temp_path();\n        let p = run_cmd(\n            vec_of_strings![\"xh\", \"httpbin.org/get\", \"-d\", \"-o\", output],\n            true,\n        );\n        assert_eq!(p.color, true);\n        assert!(p.buffer.is_stderr());\n    }\n\n    #[test]\n    fn redirect_mode_download_with_output_file() {\n        let output = temp_path();\n        let p = run_cmd(\n            vec_of_strings![\"xh\", \"httpbin.org/get\", \"-d\", \"-o\", output],\n            false,\n        );\n        assert_eq!(p.color, true);\n        assert!(p.buffer.is_stderr());\n    }\n}\n"
  },
  {
    "path": "src/redacted.rs",
    "content": "use std::ffi::OsString;\nuse std::fmt::{self, Debug};\nuse std::ops::Deref;\nuse std::str::FromStr;\n\n/// A String that doesn't show up in Debug representations.\n///\n/// This is important for logging, where we maybe want to avoid outputting\n/// sensitive data.\n#[derive(Clone, PartialEq, Eq)]\npub struct SecretString(String);\n\nimpl FromStr for SecretString {\n    type Err = std::convert::Infallible;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        Ok(Self(s.to_owned()))\n    }\n}\n\nimpl Deref for SecretString {\n    type Target = String;\n\n    fn deref(&self) -> &String {\n        &self.0\n    }\n}\n\nimpl Debug for SecretString {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        // Uncomment this to see the string anyway:\n        // self.0.fmt(f);\n        // If that turns out to be frequently necessary we could\n        // make this configurable at runtime, e.g. by flipping an\n        // AtomicBool depending on an environment variable.\n        f.write_str(\"(redacted)\")\n    }\n}\n\nimpl From<SecretString> for OsString {\n    fn from(string: SecretString) -> OsString {\n        string.0.into()\n    }\n}\n"
  },
  {
    "path": "src/redirect.rs",
    "content": "use anyhow::Result;\nuse reqwest::blocking::{Request, Response};\nuse reqwest::header::{\n    AUTHORIZATION, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE, COOKIE, HeaderMap, LOCATION,\n    PROXY_AUTHORIZATION, TRANSFER_ENCODING, WWW_AUTHENTICATE,\n};\nuse reqwest::{Method, StatusCode, Url};\n\n#[cfg(feature = \"http-message-signatures\")]\nuse crate::cli::MessageSignature;\nuse crate::middleware::{Context, Middleware};\nuse crate::utils::{HeaderValueExt, clone_request};\n\npub struct RedirectFollower {\n    max_redirects: usize,\n    #[cfg(feature = \"http-message-signatures\")]\n    message_signature: Option<MessageSignature>,\n}\n\nimpl RedirectFollower {\n    pub fn new(\n        max_redirects: usize,\n        #[cfg(feature = \"http-message-signatures\")] message_signature: Option<MessageSignature>,\n    ) -> Self {\n        RedirectFollower {\n            max_redirects,\n            #[cfg(feature = \"http-message-signatures\")]\n            message_signature,\n        }\n    }\n}\n\nimpl Middleware for RedirectFollower {\n    fn handle(&mut self, mut ctx: Context, mut first_request: Request) -> Result<Response> {\n        // This buffers the body in case we need it again later\n        // reqwest does *not* do this, it ignores 307/308 with a streaming body\n        let mut request = clone_request(&mut first_request)?;\n        let mut response = self.next(&mut ctx, first_request)?;\n        let mut remaining_redirects = self.max_redirects - 1;\n\n        while let Some(mut next_request) = get_next_request(request, &response) {\n            if remaining_redirects > 0 {\n                remaining_redirects -= 1;\n            } else {\n                return Err(TooManyRedirects {\n                    max_redirects: self.max_redirects,\n                }\n                .into());\n            }\n\n            #[cfg(feature = \"http-message-signatures\")]\n            if let Some(signature) = &self.message_signature {\n                if let Some((key_id, key_material)) = signature.key_pair() {\n                    let components = signature.flattened_components();\n                    let algorithm = signature.algorithm().map(Into::into);\n                    crate::message_signature::sign_request(\n                        &mut next_request,\n                        key_id,\n                        key_material,\n                        (!components.is_empty()).then_some(components.as_slice()),\n                        algorithm,\n                    )?;\n                }\n            }\n\n            log::info!(\"Following redirect to {}\", next_request.url());\n            log::trace!(\"Remaining redirects: {remaining_redirects}\");\n            log::trace!(\"{next_request:#?}\");\n            self.print(&mut ctx, &mut response, &mut next_request)?;\n            request = clone_request(&mut next_request)?;\n            response = self.next(&mut ctx, next_request)?;\n        }\n\n        Ok(response)\n    }\n}\n\n#[derive(Debug)]\npub(crate) struct TooManyRedirects {\n    max_redirects: usize,\n}\n\nimpl std::fmt::Display for TooManyRedirects {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(\n            f,\n            \"Too many redirects (--max-redirects={})\",\n            self.max_redirects,\n        )\n    }\n}\n\nimpl std::error::Error for TooManyRedirects {}\n\n// See https://github.com/seanmonstar/reqwest/blob/bbeb1ede4e8098481c3de6f2cafb8ecca1db4ede/src/async_impl/client.rs#L1500-L1607\nfn get_next_request(mut request: Request, response: &Response) -> Option<Request> {\n    let get_next_url = |request: &Request| {\n        let location = response.headers().get(LOCATION)?;\n        let url = location\n            .to_utf8_str()\n            .ok()\n            .and_then(|location| request.url().join(location).ok());\n        if url.is_none() {\n            log::warn!(\"Redirect to invalid URL: {location:?}\");\n        }\n        url\n    };\n\n    match response.status() {\n        StatusCode::MOVED_PERMANENTLY | StatusCode::FOUND | StatusCode::SEE_OTHER => {\n            let next_url = get_next_url(&request)?;\n            log::trace!(\"Preparing redirect to {next_url}\");\n            let prev_url = request.url();\n            if is_cross_domain_redirect(&next_url, prev_url) {\n                remove_sensitive_headers(request.headers_mut());\n            }\n            remove_signature_headers(request.headers_mut());\n            remove_content_headers(request.headers_mut());\n            *request.url_mut() = next_url;\n            *request.body_mut() = None;\n            *request.method_mut() = match *request.method() {\n                Method::GET => Method::GET,\n                Method::HEAD => Method::HEAD,\n                _ => Method::GET,\n            };\n            Some(request)\n        }\n        StatusCode::TEMPORARY_REDIRECT | StatusCode::PERMANENT_REDIRECT => {\n            let next_url = get_next_url(&request)?;\n            log::trace!(\"Preparing redirect to {next_url}\");\n            let prev_url = request.url();\n            if is_cross_domain_redirect(&next_url, prev_url) {\n                remove_sensitive_headers(request.headers_mut());\n            }\n            remove_signature_headers(request.headers_mut());\n            *request.url_mut() = next_url;\n            Some(request)\n        }\n        _ => None,\n    }\n}\n\n// See https://github.com/seanmonstar/reqwest/blob/bbeb1ede4e8098481c3de6f2cafb8ecca1db4ede/src/redirect.rs#L234-L246\nfn is_cross_domain_redirect(next: &Url, previous: &Url) -> bool {\n    next.host_str() != previous.host_str()\n        || next.port_or_known_default() != previous.port_or_known_default()\n}\n\n// See https://github.com/seanmonstar/reqwest/blob/bbeb1ede4e8098481c3de6f2cafb8ecca1db4ede/src/redirect.rs#L234-L246\nfn remove_sensitive_headers(headers: &mut HeaderMap) {\n    log::debug!(\"Removing sensitive headers for cross-domain redirect\");\n    headers.remove(AUTHORIZATION);\n    headers.remove(COOKIE);\n    headers.remove(\"cookie2\");\n    headers.remove(PROXY_AUTHORIZATION);\n    headers.remove(WWW_AUTHENTICATE);\n}\n\n// See https://github.com/seanmonstar/reqwest/blob/bbeb1ede4e8098481c3de6f2cafb8ecca1db4ede/src/async_impl/client.rs#L1503-L1510\nfn remove_content_headers(headers: &mut HeaderMap) {\n    log::debug!(\"Removing content headers for redirect that strips body\");\n    headers.remove(TRANSFER_ENCODING);\n    headers.remove(CONTENT_ENCODING);\n    headers.remove(CONTENT_TYPE);\n    headers.remove(CONTENT_LENGTH);\n    headers.remove(\"content-digest\");\n}\n\nfn remove_signature_headers(headers: &mut HeaderMap) {\n    log::debug!(\"Removing signature headers before redirect\");\n    headers.remove(\"signature\");\n    headers.remove(\"signature-input\");\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use reqwest::header::HeaderValue;\n\n    #[test]\n    fn remove_content_headers_removes_content_digest() {\n        let mut headers = HeaderMap::new();\n        headers.insert(CONTENT_LENGTH, HeaderValue::from_static(\"1\"));\n        headers.insert(\"content-digest\", HeaderValue::from_static(\"sha-256=:abc=:\"));\n\n        remove_content_headers(&mut headers);\n\n        assert!(!headers.contains_key(CONTENT_LENGTH));\n        assert!(!headers.contains_key(\"content-digest\"));\n    }\n}\n"
  },
  {
    "path": "src/request_items.rs",
    "content": "use std::{\n    borrow::Cow,\n    collections::HashSet,\n    fs::{self, File},\n    io,\n    path::{Path, PathBuf},\n    str::FromStr,\n};\n\nuse anyhow::{Result, anyhow};\nuse reqwest::header::{HeaderMap, HeaderName, HeaderValue};\nuse reqwest::{Method, blocking::multipart};\n\nuse crate::cli::BodyType;\nuse crate::nested_json;\nuse crate::utils::{expand_tilde, unescape};\n\npub const FORM_CONTENT_TYPE: &str = \"application/x-www-form-urlencoded\";\npub const JSON_CONTENT_TYPE: &str = \"application/json\";\npub const JSON_ACCEPT: &str = \"application/json, */*;q=0.5\";\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum RequestItem {\n    HttpHeader(String, String),\n    HttpHeaderFromFile(String, String),\n    HttpHeaderToUnset(String),\n    UrlParam(String, String),\n    UrlParamFromFile(String, String),\n    DataField {\n        key: String,\n        raw_key: String,\n        value: String,\n    },\n    DataFieldFromFile {\n        key: String,\n        raw_key: String,\n        value: String,\n    },\n    JsonField(String, serde_json::Value),\n    JsonFieldFromFile(String, String),\n    FormFile {\n        key: String,\n        file_name: String,\n        file_type: Option<String>,\n        file_name_header: Option<String>,\n    },\n}\n\nimpl FromStr for RequestItem {\n    type Err = clap::Error;\n    fn from_str(request_item: &str) -> clap::error::Result<RequestItem> {\n        const SPECIAL_CHARS: &str = \"=@:;\\\\\";\n        const SEPS: &[&str] = &[\"==@\", \"=@\", \":=@\", \":@\", \"==\", \":=\", \"=\", \"@\", \":\"];\n\n        fn split(request_item: &str) -> Option<(&str, &'static str, &str)> {\n            let mut char_inds = request_item.char_indices();\n            while let Some((ind, ch)) = char_inds.next() {\n                if ch == '\\\\' {\n                    // If the next character is special it's escaped and can't be\n                    // the start of the separator\n                    // And if it's normal it can't be the start either\n                    // Just skip it without looking\n                    char_inds.next();\n                    continue;\n                }\n                for sep in SEPS {\n                    if let Some(value) = request_item[ind..].strip_prefix(sep) {\n                        let key = &request_item[..ind];\n                        return Some((key, sep, value));\n                    }\n                }\n            }\n            None\n        }\n\n        if let Some((raw_key, sep, value)) = split(request_item) {\n            let raw_key = raw_key.to_string();\n            let key = unescape(&raw_key, SPECIAL_CHARS);\n            let value = unescape(value, SPECIAL_CHARS);\n            match sep {\n                \"==\" => Ok(RequestItem::UrlParam(key, value)),\n                \"=\" => Ok(RequestItem::DataField {\n                    key,\n                    raw_key,\n                    value,\n                }),\n                \":=\" => Ok(RequestItem::JsonField(\n                    raw_key,\n                    serde_json::from_str(&value).map_err(|err| {\n                        clap::Error::raw(\n                            clap::error::ErrorKind::InvalidValue,\n                            format!(\n                                \"Invalid value for '[REQUEST_ITEM]...': {request_item:?} {err}\"\n                            ),\n                        )\n                    })?,\n                )),\n                \"@\" => {\n                    let PartWithParams {\n                        value,\n                        file_type,\n                        file_name_header,\n                    } = parse_part_params(&value);\n                    Ok(RequestItem::FormFile {\n                        key,\n                        file_name: value,\n                        file_type,\n                        file_name_header,\n                    })\n                }\n                \":\" if value.is_empty() => Ok(RequestItem::HttpHeaderToUnset(key)),\n                \":\" => Ok(RequestItem::HttpHeader(key, value)),\n                \"==@\" => Ok(RequestItem::UrlParamFromFile(key, value)),\n                \"=@\" => Ok(RequestItem::DataFieldFromFile {\n                    key,\n                    raw_key,\n                    value,\n                }),\n                \":=@\" => Ok(RequestItem::JsonFieldFromFile(raw_key, value)),\n                \":@\" => Ok(RequestItem::HttpHeaderFromFile(key, value)),\n                _ => unreachable!(),\n            }\n        } else if let Some(header) = request_item.strip_suffix(';') {\n            // Technically this is too permissive because the ; might be escaped\n            Ok(RequestItem::HttpHeader(header.to_owned(), \"\".to_owned()))\n        } else {\n            // TODO: We can also end up here if the method couldn't be parsed\n            // and was interpreted as a URL, making the actual URL a request\n            // item\n            Err(clap::Error::raw(\n                clap::error::ErrorKind::InvalidValue,\n                format!(\"Invalid value for '[REQUEST_ITEM]...': {request_item:?}\"),\n            ))\n        }\n    }\n}\n\n#[derive(Debug, PartialEq, Eq)]\nstruct PartWithParams {\n    value: String,\n    file_type: Option<String>,\n    file_name_header: Option<String>,\n}\n\n/// HTTPie's syntax for this is imitating curl's.\n///\n/// curl's syntax is pretty hairy. At the most basic level it's just key-value\n/// pairs separated by semicolons, but:\n/// - Values may be quoted. This stops spaces from being stripped and allows\n///   you to put semicolons in values. (Between quotes, quotes and backslashes\n///   have to be backslash-escaped.)\n/// - If a key is not recognized then it's skipped with a warning.\n///   - Unless it comes right after a mimetype, in which case it's seen as part\n///     of the last value, because mimetypes can use the exact same syntax\n///     (e.g. `text/html; charset=UTF-8`).\n///     `;type=text/plain;filename=foobar` will send Content-Type `text/plain`\n///     and filename `foobar`, but `;type=text/plain;foo=bar` will send\n///     Content-Type `text/plain;foo=bar`.\n///\n/// We'll cut some corners and just split on \";type=\" and \";filename=\". That should\n/// be good enough for most purposes. (HTTPie only splits on \";type=\".)\nfn parse_part_params(mut text: &str) -> PartWithParams {\n    const TYPE_SEP: &str = \";type=\";\n    const FNAME_SEP: &str = \";filename=\";\n\n    let mut file_type = None;\n    let mut file_name_header = None;\n\n    // Look for parameters starting from the right.\n    // Only look for a parameter as long as it hasn't been found yet.\n    // (There may be a cleaner way, this is the best I could come up with.)\n    let mut delims = vec![TYPE_SEP, FNAME_SEP];\n    while let Some((pre, delim, post)) = rsplit_once_any(text, &delims) {\n        match delim {\n            TYPE_SEP => file_type = Some(post.to_owned()),\n            FNAME_SEP => file_name_header = Some(post.to_owned()),\n            _ => unreachable!(),\n        }\n        delims.retain(|&x| x != delim);\n        text = pre;\n    }\n\n    PartWithParams {\n        value: text.to_owned(),\n        file_type,\n        file_name_header,\n    }\n}\n\n/// Find the rightmost match of any of the delimiters and do a split.\nfn rsplit_once_any<'a>(\n    text: &'a str,\n    delimiters: &[&'static str],\n) -> Option<(&'a str, &'static str, &'a str)> {\n    let mut res = None;\n    let mut best = 0;\n    for &delim in delimiters {\n        if let Some(pos) = text.rfind(delim) {\n            if pos >= best {\n                best = pos;\n                res = Some((&text[..pos], delim, &text[pos + delim.len()..]));\n            }\n        }\n    }\n    res\n}\n\n#[derive(Default, Debug)]\npub struct RequestItems {\n    pub items: Vec<RequestItem>,\n    pub body_type: BodyType,\n}\n\npub enum Body {\n    Json(serde_json::Value),\n    Form(Vec<(String, String)>),\n    Multipart(multipart::Form),\n    Raw(Vec<u8>),\n    File {\n        file_name: PathBuf,\n        file_type: Option<HeaderValue>,\n        file_name_header: Option<String>,\n    },\n}\n\nimpl Body {\n    pub fn is_empty(&self) -> bool {\n        match self {\n            Body::Json(value) => value.is_null(),\n            Body::Form(items) => items.is_empty(),\n            // A multipart form without items isn't empty, and we can't read\n            // a body from stdin because it has to match the header, so we\n            // should never consider this \"empty\"\n            // This is a slight divergence from HTTPie, which will simply\n            // discard stdin if it receives --multipart without request items,\n            // but that behavior is useless so there's no need to match it\n            Body::Multipart(..) => false,\n            Body::File { .. } => false,\n            Body::Raw(..) => false,\n        }\n    }\n\n    pub fn pick_method(&self) -> Method {\n        if self.is_empty() {\n            Method::GET\n        } else {\n            Method::POST\n        }\n    }\n}\n\nimpl RequestItems {\n    pub fn has_form_files(&self) -> bool {\n        self.items\n            .iter()\n            .any(|item| matches!(item, RequestItem::FormFile { .. }))\n    }\n\n    pub fn headers(&self) -> Result<(HeaderMap<HeaderValue>, HashSet<HeaderName>)> {\n        let mut headers = HeaderMap::new();\n        #[allow(clippy::mutable_key_type)]\n        let mut headers_to_unset = HashSet::new();\n        for item in &self.items {\n            match item {\n                RequestItem::HttpHeader(key, value) => {\n                    let key = HeaderName::from_bytes(key.as_bytes())?;\n                    let value = HeaderValue::from_str(value)?;\n                    headers_to_unset.remove(&key);\n                    headers.append(key, value);\n                }\n                RequestItem::HttpHeaderFromFile(key, value) => {\n                    let key = HeaderName::from_bytes(key.as_bytes())?;\n                    let value = fs::read_to_string(expand_tilde(value))?;\n                    let value = HeaderValue::from_str(value.trim())?;\n                    headers_to_unset.remove(&key);\n                    headers.append(key, value);\n                }\n                RequestItem::HttpHeaderToUnset(key) => {\n                    let key = HeaderName::from_bytes(key.as_bytes())?;\n                    headers.remove(&key);\n                    headers_to_unset.insert(key);\n                }\n                RequestItem::UrlParam(..) => {}\n                RequestItem::UrlParamFromFile(..) => {}\n                RequestItem::DataField { .. } => {}\n                RequestItem::DataFieldFromFile { .. } => {}\n                RequestItem::JsonField(..) => {}\n                RequestItem::JsonFieldFromFile(..) => {}\n                RequestItem::FormFile { .. } => {}\n            }\n        }\n        Ok((headers, headers_to_unset))\n    }\n\n    pub fn query(&self) -> Result<Vec<(&str, Cow<'_, str>)>> {\n        let mut query: Vec<(&str, Cow<str>)> = vec![];\n        for item in &self.items {\n            if let RequestItem::UrlParam(key, value) = item {\n                query.push((key, Cow::Borrowed(value)));\n            } else if let RequestItem::UrlParamFromFile(key, value) = item {\n                let value = fs::read_to_string(expand_tilde(value))?;\n                query.push((key, Cow::Owned(value)));\n            }\n        }\n        Ok(query)\n    }\n\n    fn body_as_json(self) -> Result<Body> {\n        use serde_json::Value;\n        let mut body = None;\n        for item in self.items {\n            let (raw_key, value) = match item {\n                RequestItem::JsonField(raw_key, value) => (raw_key, value),\n                RequestItem::JsonFieldFromFile(raw_key, value) => {\n                    let value = serde_json::from_str(&fs::read_to_string(expand_tilde(value))?)?;\n                    (raw_key, value)\n                }\n                RequestItem::DataField { raw_key, value, .. } => (raw_key, Value::String(value)),\n                RequestItem::DataFieldFromFile { raw_key, value, .. } => {\n                    let value = fs::read_to_string(expand_tilde(value))?;\n                    (raw_key, Value::String(value))\n                }\n                RequestItem::FormFile { .. } => unreachable!(),\n                RequestItem::HttpHeader(..)\n                | RequestItem::HttpHeaderFromFile(..)\n                | RequestItem::HttpHeaderToUnset(..)\n                | RequestItem::UrlParam(..)\n                | RequestItem::UrlParamFromFile(..) => continue,\n            };\n            let json_path = nested_json::parse_path(&raw_key)?;\n            body = nested_json::insert(body, &json_path, value)\n                .map_err(|e| e.with_json_path(raw_key))?\n                .into();\n        }\n        Ok(Body::Json(body.unwrap_or(Value::Null)))\n    }\n\n    fn body_as_form(self) -> Result<Body> {\n        let mut text_fields = Vec::<(String, String)>::new();\n        for item in self.items {\n            match item {\n                RequestItem::JsonField(..) | RequestItem::JsonFieldFromFile(..) => {\n                    return Err(anyhow!(\"JSON values are not supported in Form fields\"));\n                }\n                RequestItem::DataField { key, value, .. } => text_fields.push((key, value)),\n                RequestItem::DataFieldFromFile { key, value, .. } => {\n                    let path = expand_tilde(value);\n                    text_fields.push((key, fs::read_to_string(path)?));\n                }\n                RequestItem::FormFile { .. } => unreachable!(),\n                RequestItem::HttpHeader(..) => {}\n                RequestItem::HttpHeaderFromFile(..) => {}\n                RequestItem::HttpHeaderToUnset(..) => {}\n                RequestItem::UrlParam(..) => {}\n                RequestItem::UrlParamFromFile(..) => {}\n            }\n        }\n        Ok(Body::Form(text_fields))\n    }\n\n    fn body_as_multipart(self) -> Result<Body> {\n        let mut form = multipart::Form::new();\n        for item in self.items {\n            match item {\n                RequestItem::JsonField(..) | RequestItem::JsonFieldFromFile(..) => {\n                    return Err(anyhow!(\"JSON values are not supported in multipart fields\"));\n                }\n                RequestItem::DataField { key, value, .. } => {\n                    form = form.text(key, value);\n                }\n                RequestItem::DataFieldFromFile { key, value, .. } => {\n                    let path = expand_tilde(value);\n                    form = form.text(key, fs::read_to_string(path)?);\n                }\n                RequestItem::FormFile {\n                    key,\n                    file_name,\n                    file_type,\n                    file_name_header,\n                } => {\n                    let mut part = file_to_part(expand_tilde(file_name))?;\n                    if let Some(file_type) = file_type {\n                        part = part.mime_str(&file_type)?;\n                    }\n                    if let Some(file_name_header) = file_name_header {\n                        part = part.file_name(file_name_header);\n                    }\n                    form = form.part(key, part);\n                }\n                RequestItem::HttpHeader(..) => {}\n                RequestItem::HttpHeaderFromFile(..) => {}\n                RequestItem::HttpHeaderToUnset(..) => {}\n                RequestItem::UrlParam(..) => {}\n                RequestItem::UrlParamFromFile(..) => {}\n            }\n        }\n        Ok(Body::Multipart(form))\n    }\n\n    fn body_from_file(self) -> Result<Body> {\n        let mut body = None;\n        if self\n            .items\n            .iter()\n            .any(|item| matches!(item, RequestItem::FormFile {key, ..} if !key.is_empty()))\n        {\n            return Err(anyhow!(\n                \"Can't use file fields in JSON mode (perhaps you meant --form?)\"\n            ));\n        }\n        for item in self.items {\n            match item {\n                RequestItem::DataField { .. }\n                | RequestItem::JsonField(..)\n                | RequestItem::DataFieldFromFile { .. }\n                | RequestItem::JsonFieldFromFile(..) => {\n                    return Err(anyhow!(\n                        \"Request body (from a file) and request data (key=value) cannot be mixed.\"\n                    ));\n                }\n                RequestItem::FormFile {\n                    key,\n                    file_name,\n                    file_type,\n                    file_name_header,\n                } => {\n                    assert!(key.is_empty());\n                    if body.is_some() {\n                        return Err(anyhow!(\"Can't read request from multiple files\"));\n                    }\n                    body = Some(Body::File {\n                        file_type: file_type\n                            .as_deref()\n                            .or_else(|| mime_guess::from_path(&file_name).first_raw())\n                            .map(HeaderValue::from_str)\n                            .transpose()?,\n                        file_name: expand_tilde(file_name),\n                        file_name_header,\n                    });\n                }\n                RequestItem::HttpHeader(..)\n                | RequestItem::HttpHeaderFromFile(..)\n                | RequestItem::HttpHeaderToUnset(..)\n                | RequestItem::UrlParam(..)\n                | RequestItem::UrlParamFromFile(..) => {}\n            }\n        }\n        let body = body.expect(\"Should have had at least one file field\");\n        Ok(body)\n    }\n\n    pub fn body(self) -> Result<Body> {\n        match self.body_type {\n            BodyType::Multipart => self.body_as_multipart(),\n            BodyType::Form if self.has_form_files() => self.body_as_multipart(),\n            BodyType::Form => self.body_as_form(),\n            BodyType::Json if self.has_form_files() => self.body_from_file(),\n            BodyType::Json => self.body_as_json(),\n        }\n    }\n\n    /// Determine whether a multipart request should be used.\n    ///\n    /// This duplicates logic in `body()` for the benefit of `to_curl`.\n    pub fn is_multipart(&self) -> bool {\n        match self.body_type {\n            BodyType::Multipart => true,\n            BodyType::Form => self.has_form_files(),\n            BodyType::Json => false,\n        }\n    }\n\n    /// Guess which HTTP method would be appropriate for the return value of `body`.\n    ///\n    /// It's better to use `Body::pick_method`, if possible. This method is\n    /// for the benefit of `to_curl`, which sometimes has to process the\n    /// request items itself.\n    pub fn pick_method(&self) -> Method {\n        if self.is_body_empty() {\n            Method::GET\n        } else {\n            Method::POST\n        }\n    }\n\n    pub fn is_body_empty(&self) -> bool {\n        if self.body_type == BodyType::Multipart {\n            return false;\n        }\n        for item in &self.items {\n            match item {\n                RequestItem::HttpHeader(..)\n                | RequestItem::HttpHeaderFromFile(..)\n                | RequestItem::HttpHeaderToUnset(..)\n                | RequestItem::UrlParam(..)\n                | RequestItem::UrlParamFromFile(..) => continue,\n                RequestItem::DataField { .. }\n                | RequestItem::DataFieldFromFile { .. }\n                | RequestItem::JsonField(..)\n                | RequestItem::JsonFieldFromFile(..)\n                | RequestItem::FormFile { .. } => return false,\n            }\n        }\n        true\n    }\n}\n\npub fn file_to_part(path: impl AsRef<Path>) -> io::Result<multipart::Part> {\n    let path = path.as_ref();\n    let file_name = path\n        .file_name()\n        .map(|file_name| file_name.to_string_lossy().to_string());\n    let file = File::open(path)?;\n    let file_length = file.metadata()?.len();\n    let mut part = multipart::Part::reader_with_length(file, file_length);\n    if let Some(file_name) = file_name {\n        part = part.file_name(file_name);\n    }\n    Ok(part)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    use serde_json::json;\n\n    #[test]\n    fn request_item_parsing() {\n        use RequestItem::*;\n\n        fn parse(text: &str) -> RequestItem {\n            text.parse().unwrap()\n        }\n\n        // Data field\n        assert_eq!(\n            parse(\"foo=bar\"),\n            DataField {\n                key: \"foo\".into(),\n                raw_key: \"foo\".into(),\n                value: \"bar\".into()\n            }\n        );\n        // Data field from file\n        assert_eq!(\n            parse(\"foo=@data.json\"),\n            DataFieldFromFile {\n                key: \"foo\".into(),\n                raw_key: \"foo\".into(),\n                value: \"data.json\".into()\n            }\n        );\n        // URL param\n        assert_eq!(parse(\"foo==bar\"), UrlParam(\"foo\".into(), \"bar\".into()));\n        // URL param from file\n        assert_eq!(\n            parse(\"foo==@data.txt\"),\n            UrlParamFromFile(\"foo\".into(), \"data.txt\".into())\n        );\n        // Escaped right before separator\n        assert_eq!(\n            parse(r\"foo\\==bar\"),\n            DataField {\n                key: \"foo=\".into(),\n                raw_key: r\"foo\\=\".into(),\n                value: \"bar\".into()\n            }\n        );\n        // Header\n        assert_eq!(parse(\"foo:bar\"), HttpHeader(\"foo\".into(), \"bar\".into()));\n        // Header from file\n        assert_eq!(\n            parse(\"foo:@data.txt\"),\n            HttpHeaderFromFile(\"foo\".into(), \"data.txt\".into())\n        );\n        // JSON field\n        assert_eq!(parse(\"foo:=[1,2]\"), JsonField(\"foo\".into(), json!([1, 2])));\n        // JSON field from file\n        assert_eq!(\n            parse(\"foo:=@data.json\"),\n            JsonFieldFromFile(\"foo\".into(), \"data.json\".into())\n        );\n        // Bad JSON field\n        \"foo:=bar\".parse::<RequestItem>().unwrap_err();\n        // Can't escape normal chars\n        assert_eq!(\n            parse(r\"f\\o\\o=\\ba\\r\"),\n            DataField {\n                key: r\"f\\o\\o\".into(),\n                raw_key: r\"f\\o\\o\".into(),\n                value: r\"\\ba\\r\".into()\n            },\n        );\n        // Can escape special chars\n        assert_eq!(\n            parse(r\"f\\=\\:\\@\\;oo=b\\:\\:\\:ar\"),\n            DataField {\n                key: \"f=:@;oo\".into(),\n                raw_key: r\"f\\=\\:\\@\\;oo\".into(),\n                value: \"b:::ar\".into()\n            },\n        );\n        // Unset header\n        assert_eq!(parse(\"foobar:\"), HttpHeaderToUnset(\"foobar\".into()));\n        // Empty header\n        assert_eq!(parse(\"foobar;\"), HttpHeader(\"foobar\".into(), \"\".into()));\n        // Untyped file\n        assert_eq!(\n            parse(\"foo@bar\"),\n            FormFile {\n                key: \"foo\".into(),\n                file_name: \"bar\".into(),\n                file_type: None,\n                file_name_header: None,\n            }\n        );\n        // Typed file\n        assert_eq!(\n            parse(\"foo@bar;type=qux\"),\n            FormFile {\n                key: \"foo\".into(),\n                file_name: \"bar\".into(),\n                file_type: Some(\"qux\".into()),\n                file_name_header: None,\n            },\n        );\n        // Multi-typed file\n        assert_eq!(\n            parse(\"foo@bar;type=qux;type=qux\"),\n            FormFile {\n                key: \"foo\".into(),\n                file_name: \"bar;type=qux\".into(),\n                file_type: Some(\"qux\".into()),\n                file_name_header: None,\n            },\n        );\n        // Empty filename\n        // (rejecting this would be fine too, the main point is to see if it panics)\n        assert_eq!(\n            parse(\"foo@\"),\n            FormFile {\n                key: \"foo\".into(),\n                file_name: \"\".into(),\n                file_type: None,\n                file_name_header: None,\n            }\n        );\n        // No separator\n        \"foobar\".parse::<RequestItem>().unwrap_err();\n        \"\".parse::<RequestItem>().unwrap_err();\n        // Trailing backslash\n        assert_eq!(\n            parse(r\"foo=bar\\\"),\n            DataField {\n                key: \"foo\".into(),\n                raw_key: \"foo\".into(),\n                value: r\"bar\\\".into()\n            }\n        );\n        // Escaped backslash\n        assert_eq!(\n            parse(r\"foo\\\\=bar\"),\n            DataField {\n                key: r\"foo\\\".into(),\n                raw_key: r\"foo\\\\\".into(),\n                value: \"bar\".into()\n            }\n        );\n        // Unicode\n        assert_eq!(\n            parse(\"\\u{00B5}=\\u{00B5}\"),\n            DataField {\n                key: \"\\u{00B5}\".into(),\n                raw_key: \"\\u{00B5}\".into(),\n                value: \"\\u{00B5}\".into()\n            },\n        );\n        // Empty\n        assert_eq!(\n            parse(\"=\"),\n            DataField {\n                key: \"\".into(),\n                raw_key: \"\".into(),\n                value: \"\".into()\n            }\n        );\n    }\n\n    #[test]\n    fn param_parsing() {\n        assert_eq!(\n            parse_part_params(\"foo;type=bar;filename=baz\"),\n            PartWithParams {\n                value: \"foo\".into(),\n                file_type: Some(\"bar\".into()),\n                file_name_header: Some(\"baz\".into()),\n            }\n        );\n        assert_eq!(\n            parse_part_params(\";type=foo\"),\n            PartWithParams {\n                value: \"\".into(),\n                file_type: Some(\"foo\".into()),\n                file_name_header: None,\n            }\n        );\n        assert_eq!(\n            parse_part_params(\"foo;type=bar;type=baz;filename=qux\"),\n            PartWithParams {\n                value: \"foo;type=bar\".into(),\n                file_type: Some(\"baz\".into()),\n                file_name_header: Some(\"qux\".into()),\n            }\n        );\n        assert_eq!(\n            parse_part_params(\"foo;type=bar;filename=qux;type=baz\"),\n            PartWithParams {\n                value: \"foo;type=bar\".into(),\n                file_type: Some(\"baz\".into()),\n                file_name_header: Some(\"qux\".into()),\n            }\n        );\n        assert_eq!(\n            parse_part_params(\"foo;x=y\"),\n            PartWithParams {\n                value: \"foo;x=y\".into(),\n                file_type: None,\n                file_name_header: None,\n            }\n        );\n        assert_eq!(\n            parse_part_params(\"\"),\n            PartWithParams {\n                value: \"\".into(),\n                file_type: None,\n                file_name_header: None,\n            }\n        );\n    }\n}\n"
  },
  {
    "path": "src/session.rs",
    "content": "use std::collections::HashMap;\nuse std::convert::TryInto;\nuse std::ffi::OsString;\nuse std::fs;\nuse std::io::{self, Write};\nuse std::path::PathBuf;\n\nuse anyhow::{Context, Result, anyhow};\nuse reqwest::header::HeaderMap;\nuse serde::{Deserialize, Serialize};\nuse url::Url;\n\nuse crate::auth;\nuse crate::utils::{config_dir, test_mode};\n\n#[derive(Debug, Serialize, Deserialize)]\n#[serde(untagged)]\nenum Meta {\n    Xh {\n        about: String,\n        xh: String,\n    },\n    Httpie {\n        about: String,\n        help: String,\n        httpie: String,\n    },\n    Other(serde_json::Value),\n}\n\nimpl Default for Meta {\n    fn default() -> Self {\n        Meta::Xh {\n            about: \"xh session file\".into(),\n            xh: xh_version(),\n        }\n    }\n}\n\n#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]\nstruct Auth {\n    #[serde(rename = \"type\")]\n    auth_type: Option<String>,\n    raw_auth: Option<String>,\n}\n\n// Unlike xh, HTTPie serializes path, secure and expires with defaults of \"/\", false, and null respectively.\n#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]\nstruct LegacyCookie {\n    value: String,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    expires: Option<i64>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    path: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    secure: Option<bool>,\n}\n\n#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]\nstruct Cookie {\n    name: String,\n    value: String,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    expires: Option<i64>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    path: Option<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    secure: Option<bool>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    domain: Option<String>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\n#[serde(untagged)]\nenum Cookies {\n    // old cookie format kept for backward compatibility\n    Map(HashMap<String, LegacyCookie>),\n    // new cookie format that closely resembles a cookie jar\n    List(Vec<Cookie>),\n}\n\nimpl Default for Cookies {\n    fn default() -> Self {\n        Cookies::List(Vec::new())\n    }\n}\n\n#[derive(Debug, Serialize, Deserialize)]\nstruct Header {\n    name: String,\n    value: String,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\n#[serde(untagged)]\nenum Headers {\n    // old headers format kept for backward compatibility\n    Map(HashMap<String, String>),\n    // new header format that supports duplicate keys\n    List(Vec<Header>),\n}\n\nimpl Default for Headers {\n    fn default() -> Self {\n        Headers::List(Vec::new())\n    }\n}\n\n#[derive(Debug, Default, Serialize, Deserialize)]\nstruct Content {\n    #[serde(rename = \"__meta__\")]\n    meta: Meta,\n    auth: Auth,\n    cookies: Cookies,\n    headers: Headers,\n}\n\nimpl Content {\n    fn migrate(mut self) -> Self {\n        self.meta = Meta::default();\n        if let Headers::Map(headers) = self.headers {\n            self.headers = Headers::List(\n                headers\n                    .into_iter()\n                    .map(|(key, value)| Header { name: key, value })\n                    .collect(),\n            );\n        }\n        if let Cookies::Map(cookies) = self.cookies {\n            self.cookies = Cookies::List(\n                cookies\n                    .into_iter()\n                    .map(|(name, legacy_cookie)| Cookie {\n                        name,\n                        value: legacy_cookie.value,\n                        expires: legacy_cookie.expires,\n                        path: legacy_cookie.path,\n                        secure: legacy_cookie.secure,\n                        domain: None,\n                    })\n                    .collect(),\n            )\n        }\n\n        // HTTPie appends .local to cookies from localhost.\n        // See https://github.com/psf/requests/issues/5388\n        if let Cookies::List(ref mut cookies) = self.cookies {\n            for cookie in cookies {\n                if cookie.domain.as_deref() == Some(\"localhost.local\") {\n                    cookie.domain = Some(\"localhost\".to_string());\n                }\n            }\n        }\n\n        self\n    }\n}\n\npub struct Session {\n    url: Url,\n    pub path: PathBuf,\n    read_only: bool,\n    content: Content,\n}\n\nimpl Session {\n    pub fn load_session(url: Url, mut name_or_path: OsString, read_only: bool) -> Result<Self> {\n        let path = if is_path(&name_or_path) {\n            PathBuf::from(name_or_path)\n        } else {\n            let mut path = config_dir()\n                .context(\"couldn't get config directory\")?\n                .join(\"sessions\")\n                .join(path_from_url(&url)?);\n            name_or_path.push(\".json\");\n            path.push(name_or_path);\n            path\n        };\n\n        log::debug!(\"Checking for session in {path:?}\");\n        let content = match fs::read_to_string(&path) {\n            Ok(content) => serde_json::from_str::<Content>(&content)?.migrate(),\n            Err(err) if err.kind() == io::ErrorKind::NotFound => Content::default(),\n            Err(err) => return Err(err.into()),\n        };\n        log::debug!(\"Loaded session from {path:?}\");\n\n        Ok(Session {\n            url,\n            path,\n            read_only,\n            content,\n        })\n    }\n\n    pub fn headers(&self) -> Result<HeaderMap> {\n        match &self.content.headers {\n            Headers::Map(_) => unreachable!(\"headers should have been migrated to Headers::List\"),\n            Headers::List(headers) => headers\n                .iter()\n                .map(|Header { name, value }| Ok((name.try_into()?, value.try_into()?)))\n                .collect(),\n        }\n    }\n\n    pub fn save_headers(&mut self, headers: &HeaderMap) -> Result<()> {\n        let session_headers = match self.content.headers {\n            Headers::Map(_) => unreachable!(\"headers should have been migrated to Headers::List\"),\n            Headers::List(ref mut headers) => headers,\n        };\n\n        session_headers.clear();\n\n        for (key, value) in headers.iter() {\n            let key = key.as_str();\n            // HTTPie ignores headers that are specific to a particular request e.g content-length\n            // see https://github.com/httpie/httpie/commit/e09b74021c9c955fd7c3bab11f22801aaf9dc1b8\n            // we will also ignore cookies as they are taken care of by save_cookies()\n            if key != \"cookie\" && !key.starts_with(\"content-\") && !key.starts_with(\"if-\") {\n                session_headers.push(Header {\n                    name: key.into(),\n                    value: value.to_str()?.into(),\n                });\n            }\n        }\n        Ok(())\n    }\n\n    pub fn auth(&self) -> Result<Option<auth::Auth>> {\n        if let Auth {\n            auth_type: Some(auth_type),\n            raw_auth: Some(raw_auth),\n        } = &self.content.auth\n        {\n            match auth_type.as_str() {\n                \"basic\" => {\n                    let (username, password) = auth::parse_auth(raw_auth, \"\")?;\n                    Ok(Some(auth::Auth::Basic(username, password)))\n                }\n                \"digest\" => {\n                    let (username, password) = auth::parse_auth(raw_auth, \"\")?;\n                    Ok(Some(auth::Auth::Digest(\n                        username,\n                        password.unwrap_or_default(),\n                    )))\n                }\n                \"bearer\" => Ok(Some(auth::Auth::Bearer(raw_auth.into()))),\n                _ => Err(anyhow!(\"Unknown auth type {}\", raw_auth)),\n            }\n        } else {\n            Ok(None)\n        }\n    }\n\n    pub fn save_auth(&mut self, auth: &auth::Auth) {\n        match auth {\n            auth::Auth::Basic(username, password) => {\n                let password = password.as_deref().unwrap_or(\"\");\n                self.content.auth = Auth {\n                    auth_type: Some(\"basic\".into()),\n                    raw_auth: Some(format!(\"{username}:{password}\")),\n                }\n            }\n            auth::Auth::Digest(username, password) => {\n                self.content.auth = Auth {\n                    auth_type: Some(\"digest\".into()),\n                    raw_auth: Some(format!(\"{username}:{password}\")),\n                }\n            }\n            auth::Auth::Bearer(token) => {\n                self.content.auth = Auth {\n                    auth_type: Some(\"bearer\".into()),\n                    raw_auth: Some(token.into()),\n                }\n            }\n        }\n    }\n\n    pub fn cookies(&self) -> impl Iterator<Item = Result<cookie_store::Cookie<'static>>> + '_ {\n        match &self.content.cookies {\n            Cookies::Map(_) => unreachable!(),\n            Cookies::List(cookies) => cookies.iter().map(|cookie| {\n                let mut cookie_builder =\n                    cookie_store::RawCookie::build((cookie.name.clone(), cookie.value.clone()));\n\n                if let Some(expires) = cookie.expires {\n                    cookie_builder =\n                        cookie_builder.expires(time::OffsetDateTime::from_unix_timestamp(expires)?);\n                }\n                if let Some(path) = &cookie.path {\n                    cookie_builder = cookie_builder.path(path.clone());\n                }\n                if let Some(secure) = cookie.secure {\n                    cookie_builder = cookie_builder.secure(secure);\n                }\n\n                let mut cookie_url = self.url.clone();\n                if let Some(domain) = &cookie.domain {\n                    cookie_url = format!(\"http://{domain}\").parse()?;\n                    // The cookie's domain attribute cannot be an IP address.\n                    // See https://stackoverflow.com/a/30676300/5915221\n                    if let Some(url::Host::Domain(_)) = cookie_url.host() {\n                        cookie_builder = cookie_builder.domain(domain.clone());\n                    }\n                }\n\n                Ok(cookie_store::Cookie::try_from_raw_cookie(\n                    &cookie_builder.into(),\n                    &cookie_url,\n                )?)\n            }),\n        }\n    }\n\n    pub fn save_cookies<'b, I>(&mut self, cookies: I)\n    where\n        I: Iterator<Item = &'b cookie_store::Cookie<'static>>,\n    {\n        let session_cookies = match self.content.cookies {\n            Cookies::Map(_) => unreachable!(),\n            Cookies::List(ref mut cookies) => cookies,\n        };\n\n        session_cookies.clear();\n\n        for cookie in cookies {\n            let mut domain = cookie.domain();\n            if let cookie_store::CookieDomain::HostOnly(s) = &cookie.domain {\n                domain = Some(s);\n            }\n\n            session_cookies.push(Cookie {\n                name: cookie.name().into(),\n                value: cookie.value().into(),\n                expires: cookie\n                    .expires()\n                    .and_then(|v| v.datetime())\n                    .map(|v| v.unix_timestamp()),\n                path: Some(cookie.path.to_string()),\n                secure: cookie.secure(),\n                domain: domain.map(Into::into),\n            });\n        }\n    }\n\n    pub fn persist(&self) -> Result<()> {\n        if !self.path.exists() || !self.read_only {\n            if let Some(parent_path) = self.path.parent() {\n                fs::create_dir_all(parent_path)?;\n            }\n            let mut session_file = fs::File::create(&self.path)?;\n            log::debug!(\"Persisting session to {:?}\", self.path);\n            let formatter = serde_json::ser::PrettyFormatter::with_indent(b\"    \");\n            let mut ser = serde_json::Serializer::with_formatter(&mut session_file, formatter);\n            self.content.serialize(&mut ser)?;\n            session_file.write_all(b\"\\n\")?;\n        }\n        Ok(())\n    }\n}\n\nfn xh_version() -> String {\n    if test_mode() {\n        \"0.0.0\".into()\n    } else {\n        env!(\"CARGO_PKG_VERSION\").into()\n    }\n}\n\nfn is_path(value: &OsString) -> bool {\n    value.to_string_lossy().contains(std::path::is_separator)\n}\n\nfn path_from_url(url: &Url) -> Result<String> {\n    match (url.host_str(), url.port()) {\n        (Some(\".\"), _) | (Some(\"..\"), _) | (None, _) => {\n            Err(anyhow!(\"couldn't extract host from url\"))\n        }\n        (Some(host), Some(port)) => Ok(format!(\"{host}_{port}\")),\n        (Some(host), None) => Ok(host.into()),\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    use anyhow::Result;\n    use reqwest::header::HeaderValue;\n\n    fn load_session_from_str(s: &str) -> Result<Session> {\n        Ok(Session {\n            url: Url::parse(\"http://example.net\")?,\n            content: serde_json::from_str::<Content>(s)?.migrate(),\n            path: PathBuf::new(),\n            read_only: false,\n        })\n    }\n\n    #[test]\n    fn can_parse_old_httpie_session() -> Result<()> {\n        let session = load_session_from_str(indoc::indoc! {r#\"\n            {\n                \"__meta__\": {\n                    \"about\": \"HTTPie session file\",\n                    \"help\": \"https://httpie.org/doc#sessions\",\n                    \"httpie\": \"2.3.0\"\n                },\n                \"auth\": { \"password\": null, \"type\": null, \"username\": null },\n                \"cookies\": {\n                    \"baz\": { \"expires\": null, \"path\": \"/\", \"secure\": false, \"value\": \"quux\" }\n                },\n                \"headers\": { \"hello\": \"world\" }\n            }\n        \"#})?;\n\n        assert_eq!(\n            session.headers()?.get(\"hello\"),\n            Some(&HeaderValue::from_static(\"world\")),\n        );\n\n        let cookies = session.cookies().collect::<Result<Vec<_>>>()?;\n        assert_eq!(cookies[0].name_value(), (\"baz\", \"quux\"));\n        assert_eq!(cookies[0].path(), Some(\"/\"));\n        assert_eq!(cookies[0].secure(), Some(false));\n        assert_eq!(session.content.auth, Auth::default());\n\n        Ok(())\n    }\n\n    #[test]\n    fn can_parse_old_xh_session() -> Result<()> {\n        let session = load_session_from_str(indoc::indoc! {r#\"\n            {\n                \"__meta__\": {\n                    \"about\": \"xh session file\",\n                    \"xh\": \"0.0.0\"\n                },\n                \"auth\": { \"raw_auth\": \"secret-token\", \"type\": \"bearer\" },\n                \"cookies\": {\n                    \"baz\": { \"expires\": null, \"path\": \"/\", \"secure\": false, \"value\": \"quux\" }\n                },\n                \"headers\": { \"hello\": \"world\" }\n            }\n        \"#})?;\n\n        assert_eq!(\n            session.headers()?.get(\"hello\"),\n            Some(&HeaderValue::from_static(\"world\")),\n        );\n        let cookies = session.cookies().collect::<Result<Vec<_>>>()?;\n        assert_eq!(cookies[0].name_value(), (\"baz\", \"quux\"));\n        assert_eq!(cookies[0].path(), Some(\"/\"));\n        assert_eq!(cookies[0].secure(), Some(false));\n        assert_eq!(\n            session.content.auth,\n            Auth {\n                auth_type: Some(\"bearer\".into()),\n                raw_auth: Some(\"secret-token\".into())\n            },\n        );\n\n        Ok(())\n    }\n\n    #[test]\n    fn can_parse_session_with_unknown_meta() {\n        load_session_from_str(indoc::indoc! {r#\"\n            {\n                \"__meta__\": {},\n                \"auth\": { \"raw_auth\": \"secret-token\", \"type\": \"bearer\" },\n                \"cookies\": {\n                    \"baz\": { \"expires\": null, \"path\": \"/\", \"secure\": false, \"value\": \"quux\" }\n                },\n                \"headers\": { \"hello\": \"world\" }\n            }\n        \"#})\n        .unwrap();\n    }\n\n    #[test]\n    fn can_parse_session_with_new_style_headers() -> Result<()> {\n        let session = load_session_from_str(indoc::indoc! {r#\"\n            {\n                \"__meta__\": {\n                    \"about\": \"HTTPie session file\",\n                    \"help\": \"https://httpie.io/docs#sessions\",\n                    \"httpie\": \"3.0.2\"\n                },\n                \"auth\": {},\n                \"cookies\": {},\n                \"headers\": [\n                    { \"name\": \"X-Data\", \"value\": \"value\" },\n                    { \"name\": \"X-Foo\", \"value\": \"bar\" },\n                    { \"name\": \"X-Foo\", \"value\": \"baz\" }\n                ]\n            }\n        \"#})?;\n\n        let headers = session.headers()?;\n        assert_eq!(\n            headers.get(\"X-Data\"),\n            Some(&HeaderValue::from_static(\"value\"))\n        );\n\n        let mut x_foo_values = headers.get_all(\"X-Foo\").iter();\n        assert_eq!(x_foo_values.next(), Some(&HeaderValue::from_static(\"bar\")));\n        assert_eq!(x_foo_values.next(), Some(&HeaderValue::from_static(\"baz\")));\n\n        Ok(())\n    }\n\n    #[test]\n    fn can_parse_session_with_new_style_cookies() -> Result<()> {\n        let session = load_session_from_str(indoc::indoc! {r#\"\n            {\n                \"__meta__\": {\n                    \"about\": \"HTTPie session file\",\n                    \"help\": \"https://httpie.io/docs#sessions\",\n                    \"httpie\": \"3.0.2\"\n                },\n                \"auth\": {},\n                \"cookies\": [\n                    {\n                        \"name\": \"baz\",\n                        \"value\": \"quux\",\n                        \"expires\": null,\n                        \"path\": \"/\",\n                        \"secure\": false,\n                        \"domain\": \"example.com\"\n                    },\n                    {\n                        \"name\": \"foo\",\n                        \"value\": \"bar\",\n                        \"expires\": null,\n                        \"path\": \"/\",\n                        \"secure\": false,\n                        \"domain\": null\n                    },\n                    {\n                        \"domain\": \"localhost.local\",\n                        \"expires\": null,\n                        \"name\": \"hello\",\n                        \"path\": \"/cookies\",\n                        \"secure\": false,\n                        \"value\": \"world\"\n                    }\n                ],\n                \"headers\": []\n            }\n        \"#})?;\n\n        let cookies = session.cookies().collect::<Result<Vec<_>>>()?;\n\n        assert_eq!(cookies[0].name_value(), (\"baz\", \"quux\"));\n        assert_eq!(cookies[0].path(), Some(\"/\"));\n        assert_eq!(cookies[0].secure(), Some(false));\n        assert_eq!(cookies[0].domain(), Some(\"example.com\"));\n\n        assert_eq!(cookies[1].name_value(), (\"foo\", \"bar\"));\n        assert_eq!(cookies[1].path(), Some(\"/\"));\n        assert_eq!(cookies[1].secure(), Some(false));\n        assert_eq!(cookies[1].domain(), None);\n\n        assert_eq!(cookies[2].name_value(), (\"hello\", \"world\"));\n        assert_eq!(cookies[2].path(), Some(\"/cookies\"));\n        assert_eq!(cookies[2].secure(), Some(false));\n        assert_eq!(cookies[2].domain(), Some(\"localhost\"));\n\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "src/to_curl.rs",
    "content": "use std::io::{Write, stderr, stdout};\n\nuse anyhow::{Context, Result, anyhow};\nuse os_display::Quotable;\nuse reqwest::{Method, tls};\nuse std::ffi::OsString;\n\nuse crate::cli::{AuthType, Cli, HttpVersion, Verify};\nuse crate::request_items::{Body, FORM_CONTENT_TYPE, JSON_ACCEPT, JSON_CONTENT_TYPE, RequestItem};\nuse crate::utils::{HeaderValueExt, url_with_query};\n\npub fn print_curl_translation(args: Cli) -> Result<()> {\n    let cmd = translate(args)?;\n    let mut stderr = stderr();\n    for warning in &cmd.warnings {\n        writeln!(stderr, \"Warning: {warning}\")?;\n    }\n    if !cmd.warnings.is_empty() {\n        writeln!(stderr)?;\n    }\n    writeln!(stdout(), \"{cmd}\")?;\n    Ok(())\n}\n\npub struct Command {\n    pub long: bool,\n    pub args: Vec<OsString>,\n    pub env: Vec<(&'static str, String)>,\n    pub warnings: Vec<String>,\n}\n\nimpl Command {\n    fn new(long: bool) -> Command {\n        Command {\n            long,\n            args: Vec::new(),\n            env: Vec::new(),\n            warnings: Vec::new(),\n        }\n    }\n\n    fn opt(&mut self, short: &'static str, long: &'static str) {\n        if self.long {\n            self.args.push(long.into());\n        } else {\n            self.args.push(short.into());\n        }\n    }\n\n    fn arg(&mut self, arg: impl Into<OsString>) {\n        self.args.push(arg.into());\n    }\n\n    fn header(&mut self, name: &str, value: &str) {\n        self.opt(\"-H\", \"--header\");\n        self.arg(format!(\"{name}: {value}\"));\n    }\n\n    fn env(&mut self, var: &'static str, value: impl Into<String>) {\n        self.env.push((var, value.into()));\n    }\n\n    fn warn(&mut self, message: impl Into<String>) {\n        self.warnings.push(message.into());\n    }\n}\n\nimpl std::fmt::Display for Command {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        for (key, value) in &self.env {\n            // This is wrong for Windows, but there doesn't seem to be a\n            // right way\n            write!(f, \"{}={} \", key, value.maybe_quote())?;\n        }\n        write!(f, \"curl\")?;\n        for arg in &self.args {\n            write!(f, \" {}\", arg.maybe_quote().external(true))?;\n        }\n        Ok(())\n    }\n}\n\npub fn translate(args: Cli) -> Result<Command> {\n    let (headers, headers_to_unset) = args.request_items.headers()?;\n\n    let mut cmd = Command::new(args.curl_long);\n\n    let ignored = [\n        // No equivalent\n        (args.offline, \"--offline\"),\n        // Already the default\n        (args.body, \"-b/--body\"),\n        // No straightforward equivalent\n        (args.print.is_some(), \"-p/--print\"),\n        // No equivalent\n        (args.pretty.is_some(), \"--pretty\"),\n        // No equivalent\n        (args.style.is_some(), \"-s/--style\"),\n        // No equivalent\n        (args.m_sig.m_sig_id.is_some(), \"--unstable-m-sig-id\"),\n        // No equivalent\n        (args.m_sig.m_sig_key.is_some(), \"--unstable-m-sig-key\"),\n        // No equivalent\n        (args.m_sig.m_sig_alg.is_some(), \"--unstable-m-sig-alg\"),\n        // No equivalent\n        (args.m_sig.has_components(), \"--unstable-m-sig-comp\"),\n        // No equivalent\n        (args.compress > 0, \"-x/--compress\"),\n        // No equivalent\n        (args.response_charset.is_some(), \"--response-charset\"),\n        // No equivalent\n        (args.response_mime.is_some(), \"--response-mime\"),\n        // Already the default\n        (args.all, \"--all\"),\n        // No (straightforward?) equivalent\n        (args.history_print.is_some(), \"-P/--history-print\"),\n        // Might be possible to emulate with --cookie-jar but tricky\n        (args.session.is_some(), \"--session\"),\n        // Already the default (usually, depends on compile time options)\n        // Unclear if you can even change this at runtime\n        (args.native_tls, \"--native-tls\"),\n    ];\n\n    for (present, flag) in ignored {\n        if present {\n            cmd.warn(format!(\"Ignored {flag}\"));\n        }\n    }\n\n    if args.follow && !matches!(args.method, Some(Method::GET) | None) {\n        cmd.warn(\"Using a combination of -X/--request and -L/--location which may cause unintended side effects.\");\n    }\n\n    // Silently ignored:\n    // - .ignore_stdin: assumed by default\n    //   (to send stdin, --data-binary @- -H 'Content-Type: application/octet-stream')\n    // - .curl and .curl_long: you are here\n\n    // Output options\n    if args.verbose > 0 {\n        // Far from an exact match, but it does print the request headers\n        cmd.opt(\"-v\", \"--verbose\");\n    }\n    if args.quiet > 0 {\n        // Also not an exact match but it suppresses error messages which\n        // is sorta like suppressing warnings\n        cmd.opt(\"-s\", \"--silent\");\n    }\n    if args.debug {\n        // Again not an exact match but it's something\n        // This actually overrides --verbose\n        cmd.arg(\"--trace\");\n        cmd.arg(\"-\");\n    }\n    if args.stream == Some(true) {\n        // curl sorta streams by default, but its buffer stops it from\n        // showing up right away\n        cmd.opt(\"-N\", \"--no-buffer\");\n    }\n    // Since --fail is more disruptive than HTTPie's --check-status flag, we will not enable\n    // it unless the user explicitly sets the latter flag\n    if args.check_status == Some(true) {\n        // Suppresses output on failure, unlike us\n        cmd.opt(\"-f\", \"--fail\");\n    }\n\n    // HTTP options\n    if args.follow {\n        cmd.opt(\"-L\", \"--location\");\n    }\n    if let Some(num) = args.max_redirects {\n        cmd.arg(\"--max-redirs\");\n        cmd.arg(num.to_string());\n    }\n    if let Some(filename) = args.output {\n        let filename = filename.to_str().ok_or_else(|| anyhow!(\"Invalid UTF-8\"))?;\n        cmd.opt(\"-o\", \"--output\");\n        cmd.arg(filename);\n    } else if args.download {\n        cmd.opt(\"-O\", \"--remote-name\");\n    }\n    if args.resume {\n        cmd.opt(\"-C\", \"--continue-at\");\n        cmd.arg(\"-\"); // Tell curl to guess, like we do\n    }\n    match args.verify.unwrap_or(Verify::Yes) {\n        Verify::CustomCaBundle(filename) => {\n            cmd.arg(\"--cacert\");\n            cmd.arg(filename);\n        }\n        Verify::No => {\n            cmd.opt(\"-k\", \"--insecure\");\n        }\n        Verify::Yes => {}\n    }\n    if let Some(cert) = args.cert {\n        cmd.opt(\"-E\", \"--cert\");\n        cmd.arg(cert);\n    }\n    if let Some(keyfile) = args.cert_key {\n        cmd.arg(\"--key\");\n        cmd.arg(keyfile);\n    }\n    if let Some(tls_version) = args.ssl.and_then(Into::into) {\n        match tls_version {\n            tls::Version::TLS_1_0 => {\n                cmd.arg(\"--tlsv1.0\");\n                cmd.arg(\"--tls-max\");\n                cmd.arg(\"1.0\");\n            }\n            tls::Version::TLS_1_1 => {\n                cmd.arg(\"--tlsv1.1\");\n                cmd.arg(\"--tls-max\");\n                cmd.arg(\"1.1\");\n            }\n            tls::Version::TLS_1_2 => {\n                cmd.arg(\"--tlsv1.2\");\n                cmd.arg(\"--tls-max\");\n                cmd.arg(\"1.2\");\n            }\n            tls::Version::TLS_1_3 => {\n                cmd.arg(\"--tlsv1.3\");\n                cmd.arg(\"--tls-max\");\n                cmd.arg(\"1.3\");\n            }\n            _ => unreachable!(),\n        }\n    }\n    for proxy in args.proxy {\n        match proxy {\n            crate::cli::Proxy::All(proxy) => {\n                cmd.opt(\"-x\", \"--proxy\");\n                cmd.arg(String::from(proxy));\n            }\n            crate::cli::Proxy::Http(proxy) => {\n                // These don't seem to have corresponding flags\n                cmd.env(\"http_proxy\", proxy);\n            }\n            crate::cli::Proxy::Https(proxy) => {\n                cmd.env(\"https_proxy\", proxy);\n            }\n        }\n    }\n    if let Some(timeout) = args.timeout.and_then(|t| t.as_duration()) {\n        cmd.arg(\"--max-time\");\n        cmd.arg(timeout.as_secs_f64().to_string());\n    }\n    if let Some(http_version) = args.http_version {\n        match http_version {\n            HttpVersion::Http10 => cmd.arg(\"--http1.0\"),\n            HttpVersion::Http11 => cmd.arg(\"--http1.1\"),\n            HttpVersion::Http2 => cmd.arg(\"--http2\"),\n            HttpVersion::Http2PriorKnowledge => cmd.arg(\"--http2-prior-knowledge\"),\n            HttpVersion::Http3PriorKnowledge => cmd.arg(\"--http3-only\"),\n        }\n    }\n\n    if args.method == Some(Method::HEAD) {\n        cmd.opt(\"-I\", \"--head\");\n    } else if args.method == Some(Method::OPTIONS) {\n        // If you're sending an OPTIONS you almost certainly want to see the headers\n        cmd.opt(\"-i\", \"--include\");\n        cmd.opt(\"-X\", \"--request\");\n        cmd.arg(\"OPTIONS\");\n    } else if args.headers {\n        // The best option for printing just headers seems to be to use -I\n        // but with an explicit method as an override.\n        // But this is a hack that actually fails if data is sent.\n        // See discussion on https://lornajane.net/posts/2014/view-only-headers-with-curl\n\n        let method = match args.method {\n            Some(method) => method,\n            // unwrap_or_else causes borrowing issues\n            None => args.request_items.pick_method(),\n        };\n        cmd.opt(\"-I\", \"--head\");\n        cmd.opt(\"-X\", \"--request\");\n        cmd.arg(method.to_string());\n        if method != Method::GET {\n            cmd.warn(\n                \"-I/--head is incompatible with sending data. Consider omitting -h/--headers.\"\n                    .to_string(),\n            );\n        }\n    } else if let Some(method) = args.method {\n        cmd.opt(\"-X\", \"--request\");\n        cmd.arg(method.to_string());\n    } else {\n        // We assume that curl's automatic detection of when to do a POST matches\n        // ours so we can ignore the None case\n    }\n\n    let url = url_with_query(args.url, &args.request_items.query()?);\n\n    if url.as_str().contains(['[', ']', '{', '}']) {\n        cmd.opt(\"-g\", \"--globoff\")\n    }\n\n    cmd.arg(url.to_string());\n\n    // Force ipv4/ipv6 options\n    match (args.ipv4, args.ipv6) {\n        (true, false) => cmd.opt(\"-4\", \"--ipv4\"),\n        (false, true) => cmd.opt(\"-6\", \"--ipv6\"),\n        _ => (),\n    };\n\n    if let Some(interface) = args.interface {\n        cmd.arg(\"--interface\");\n        cmd.arg(interface);\n    };\n\n    if let Some(unix_socket) = args.unix_socket {\n        cmd.arg(\"--unix-socket\");\n        cmd.arg(unix_socket);\n    }\n\n    if !args.resolve.is_empty() {\n        let port = url\n            .port_or_known_default()\n            .with_context(|| format!(\"Unsupported URL scheme: '{}'\", url.scheme()))?;\n\n        cmd.warn(\"Inferred port number in --resolve from request URL.\");\n        for resolve in args.resolve {\n            cmd.arg(\"--resolve\");\n            cmd.arg(format!(\"{}:{}:{}\", resolve.domain, port, resolve.addr));\n        }\n    }\n\n    // Payload\n    for (header, value) in headers.iter() {\n        cmd.opt(\"-H\", \"--header\");\n        if value.is_empty() {\n            cmd.arg(format!(\"{header};\"));\n        } else {\n            cmd.arg(format!(\"{}: {}\", header, value.to_utf8_str()?));\n        }\n    }\n    for header in headers_to_unset {\n        cmd.opt(\"-H\", \"--header\");\n        cmd.arg(format!(\"{header}:\"));\n    }\n    if args.ignore_netrc {\n        // Already the default, so a bit questionable\n        cmd.arg(\"--no-netrc\");\n    }\n    if let Some(auth) = args.auth {\n        match args.auth_type.unwrap_or_default() {\n            AuthType::Basic => {\n                cmd.arg(\"--basic\");\n                // curl implements this flag the same way, including password prompt\n                cmd.opt(\"-u\", \"--user\");\n                cmd.arg(auth);\n            }\n            AuthType::Digest => {\n                cmd.arg(\"--digest\");\n                // curl implements this flag the same way, including password prompt\n                cmd.opt(\"-u\", \"--user\");\n                cmd.arg(auth);\n            }\n            AuthType::Bearer => {\n                cmd.arg(\"--oauth2-bearer\");\n                cmd.arg(auth);\n            }\n        }\n    }\n\n    if let Some(raw) = args.raw {\n        if args.form {\n            cmd.header(\"content-type\", FORM_CONTENT_TYPE);\n        } else {\n            cmd.header(\"content-type\", JSON_CONTENT_TYPE);\n            cmd.header(\"accept\", JSON_ACCEPT);\n        }\n\n        cmd.opt(\"-d\", \"--data\");\n        cmd.arg(raw);\n    } else if args.request_items.is_multipart() {\n        // We can't use .body() here because we can't look inside the multipart\n        // form after construction and we don't want to actually read the files\n        for item in args.request_items.items {\n            match item {\n                RequestItem::JsonField(..) | RequestItem::JsonFieldFromFile(..) => {\n                    return Err(anyhow!(\"JSON values are not supported in multipart fields\"));\n                }\n                RequestItem::DataField { key, value, .. } => {\n                    cmd.opt(\"-F\", \"--form\");\n                    cmd.arg(format!(\"{key}={value}\"));\n                }\n                RequestItem::DataFieldFromFile { key, value, .. } => {\n                    cmd.opt(\"-F\", \"--form\");\n                    cmd.arg(format!(\"{key}=<{value}\"));\n                }\n                RequestItem::FormFile {\n                    key,\n                    file_name,\n                    file_type,\n                    file_name_header,\n                } => {\n                    cmd.opt(\"-F\", \"--form\");\n                    let mut val = format!(\"{key}=@{file_name}\");\n                    if let Some(file_type) = file_type {\n                        val.push_str(\";type=\");\n                        val.push_str(&file_type);\n                    }\n                    if let Some(file_name_header) = file_name_header {\n                        val.push_str(\";filename=\");\n                        val.push_str(&file_name_header);\n                    }\n                    cmd.arg(val);\n                }\n                RequestItem::HttpHeader(..) => {}\n                RequestItem::HttpHeaderFromFile(..) => {}\n                RequestItem::HttpHeaderToUnset(..) => {}\n                RequestItem::UrlParam(..) => {}\n                RequestItem::UrlParamFromFile(..) => {}\n            }\n        }\n    } else {\n        match args.request_items.body()? {\n            Body::Form(items) => {\n                if items.is_empty() {\n                    // Force the header\n                    cmd.header(\"content-type\", FORM_CONTENT_TYPE);\n                }\n                for (key, value) in items {\n                    // More faithful than -F, but doesn't have a short version\n                    // New in curl 7.18.0 (January 28 2008), *probably* old enough\n                    // Otherwise passing --multipart helps\n                    cmd.arg(\"--data-urlencode\");\n                    // Encoding this is tricky: --data-urlencode expects name\n                    // to be encoded but not value and doesn't take strings\n                    let mut encoded = serde_urlencoded::to_string([(key, \"\")])?;\n                    encoded.push_str(&value);\n                    cmd.arg(encoded);\n                }\n            }\n            Body::Json(value) if !value.is_null() => {\n                cmd.header(\"content-type\", JSON_CONTENT_TYPE);\n                cmd.header(\"accept\", JSON_ACCEPT);\n\n                let json_string = value.to_string();\n                cmd.opt(\"-d\", \"--data\");\n                cmd.arg(json_string);\n            }\n            Body::Json(..) if args.json => {\n                cmd.header(\"content-type\", JSON_CONTENT_TYPE);\n                cmd.header(\"accept\", JSON_ACCEPT);\n            }\n            Body::Json(..) => {}\n            Body::Multipart { .. } => unreachable!(),\n            Body::Raw(..) => unreachable!(),\n            Body::File {\n                file_name,\n                file_type,\n                file_name_header: _,\n            } => {\n                if let Some(file_type) = file_type {\n                    cmd.header(\"content-type\", file_type.to_str()?);\n                } else {\n                    cmd.header(\"content-type\", JSON_CONTENT_TYPE);\n                }\n                cmd.arg(\"--data-binary\");\n                let mut arg = OsString::from(\"@\");\n                arg.push(file_name);\n                cmd.arg(arg);\n            }\n        }\n    }\n\n    Ok(cmd)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn examples() {\n        let expected = vec![\n            (\"xh httpbin.org/get\", \"curl http://httpbin.org/get\"),\n            (\"xh httpbin.org/get -4\", \"curl http://httpbin.org/get -4\"),\n            (\"xh httpbin.org/get -6\", \"curl http://httpbin.org/get -6\"),\n            (\n                \"xh httpbin.org/post x=3\",\n                #[cfg(not(windows))]\n                r#\"curl http://httpbin.org/post -H 'content-type: application/json' -H 'accept: application/json, */*;q=0.5' -d '{\"x\":\"3\"}'\"#,\n                #[cfg(windows)]\n                r#\"curl http://httpbin.org/post -H 'content-type: application/json' -H 'accept: application/json, */*;q=0.5' -d '{\\\"x\\\":\\\"3\\\"}'\"#,\n            ),\n            (\n                \"xh --form httpbin.org/post x\\\\=y=z=w\",\n                \"curl http://httpbin.org/post --data-urlencode 'x%3Dy=z=w'\",\n            ),\n            (\n                \"xh put httpbin.org/put\",\n                \"curl -X PUT http://httpbin.org/put\",\n            ),\n            (\n                \"xh --https httpbin.org/get x==3\",\n                \"curl 'https://httpbin.org/get?x=3'\",\n            ),\n            (\n                \"xhs httpbin.org/get x==3\",\n                \"curl 'https://httpbin.org/get?x=3'\",\n            ),\n            (\n                \"xh -h httpbin.org/get\",\n                \"curl -I -X GET http://httpbin.org/get\",\n            ),\n            (\n                \"xh options httpbin.org/get\",\n                \"curl -i -X OPTIONS http://httpbin.org/get\",\n            ),\n            (\n                \"xh --proxy http:localhost:1080 httpbin.org/get\",\n                \"http_proxy=localhost:1080 curl http://httpbin.org/get\",\n            ),\n            (\n                \"xh --proxy all:localhost:1080 httpbin.org/get\",\n                \"curl -x localhost:1080 http://httpbin.org/get\",\n            ),\n            (\n                \"xh httpbin.org/post x:=[3]\",\n                #[cfg(not(windows))]\n                r#\"curl http://httpbin.org/post -H 'content-type: application/json' -H 'accept: application/json, */*;q=0.5' -d '{\"x\":[3]}'\"#,\n                #[cfg(windows)]\n                r#\"curl http://httpbin.org/post -H 'content-type: application/json' -H 'accept: application/json, */*;q=0.5' -d '{\\\"x\\\":[3]}'\"#,\n            ),\n            (\n                \"xh --json httpbin.org/post\",\n                \"curl http://httpbin.org/post -H 'content-type: application/json' -H 'accept: application/json, */*;q=0.5'\",\n            ),\n            (\n                \"xh --form httpbin.org/post x@/dev/null\",\n                \"curl http://httpbin.org/post -F 'x=@/dev/null'\",\n            ),\n            (\n                \"xh --form httpbin.org/post\",\n                \"curl http://httpbin.org/post -H 'content-type: application/x-www-form-urlencoded'\",\n            ),\n            (\n                \"xh --bearer foobar post httpbin.org/post\",\n                \"curl -X POST http://httpbin.org/post --oauth2-bearer foobar\",\n            ),\n            (\n                \"xh httpbin.org/get foo:Bar baz; user-agent:\",\n                \"curl http://httpbin.org/get -H 'foo: Bar' -H 'baz;' -H user-agent:\",\n            ),\n            (\n                \"xh -d httpbin.org/get\",\n                \"curl -f -L -O http://httpbin.org/get\",\n            ),\n            (\n                \"xh -d -o foobar --continue httpbin.org/get\",\n                \"curl -f -L -o foobar -C - http://httpbin.org/get\",\n            ),\n            (\n                \"xh --curl-long -d -o foobar --continue httpbin.org/get\",\n                \"curl --fail --location --output foobar --continue-at - http://httpbin.org/get\",\n            ),\n            (\n                \"xh httpbin.org/post @foo.txt\",\n                #[cfg(not(windows))]\n                \"curl http://httpbin.org/post -H 'content-type: text/plain' --data-binary @foo.txt\",\n                #[cfg(windows)]\n                \"curl http://httpbin.org/post -H 'content-type: text/plain' --data-binary '@foo.txt'\",\n            ),\n            (\n                \"xh http://example.com/[1-100].png?q={80,90}\",\n                \"curl -g 'http://example.com/[1-100].png?q={80,90}'\",\n            ),\n            (\n                \"xh https://exmaple.com/ hello:你好\",\n                \"curl https://exmaple.com/ -H 'hello: 你好'\",\n            ),\n        ];\n        for (input, output) in expected {\n            let cli = Cli::try_parse_from(input.split_whitespace()).unwrap();\n            let cmd = translate(cli).unwrap();\n            assert_eq!(cmd.to_string(), output, \"Wrong output for {input:?}\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/utils.rs",
    "content": "use std::borrow::Cow;\nuse std::env::var_os;\nuse std::io::{self, Write};\nuse std::path::{Path, PathBuf};\nuse std::str::Utf8Error;\n\nuse anyhow::Result;\nuse reqwest::blocking::{Request, Response};\nuse reqwest::header::HeaderValue;\nuse url::Url;\n\npub fn unescape(text: &str, special_chars: &'static str) -> String {\n    let mut out = String::new();\n    let mut chars = text.chars();\n    while let Some(ch) = chars.next() {\n        if ch == '\\\\' {\n            match chars.next() {\n                Some(next) if special_chars.contains(next) => {\n                    // Escape this character\n                    out.push(next);\n                }\n                Some(next) => {\n                    // Do not escape this character, treat backslash\n                    // as ordinary character\n                    out.push(ch);\n                    out.push(next);\n                }\n                None => {\n                    out.push(ch);\n                }\n            }\n        } else {\n            out.push(ch);\n        }\n    }\n    out\n}\n\npub fn clone_request(request: &mut Request) -> Result<Request> {\n    if let Some(b) = request.body_mut().as_mut() {\n        b.buffer()?;\n    }\n    // This doesn't copy the contents of the buffer, cloning requests is cheap\n    // https://docs.rs/bytes/1.0.1/bytes/struct.Bytes.html\n    Ok(request.try_clone().unwrap()) // guaranteed to not fail if body is already buffered\n}\n\n/// Whether to make some things more deterministic for the benefit of tests\npub fn test_mode() -> bool {\n    // In integration tests the binary isn't compiled with cfg(test), so we\n    // use an environment variable.\n    // This isn't called very often currently but we could cache it using an\n    // atomic integer.\n    cfg!(test) || var_os(\"XH_TEST_MODE\").is_some()\n}\n\n/// Whether to behave as if stdin and stdout are terminals\npub fn test_pretend_term() -> bool {\n    var_os(\"XH_TEST_MODE_TERM\").is_some()\n}\n\npub fn test_default_color() -> bool {\n    var_os(\"XH_TEST_MODE_COLOR\").is_some()\n}\n\n#[cfg(test)]\npub fn random_string() -> String {\n    use rand::Rng;\n\n    rand::thread_rng()\n        .sample_iter(&rand::distributions::Alphanumeric)\n        .take(10)\n        .map(char::from)\n        .collect()\n}\n\npub fn config_dir() -> Option<PathBuf> {\n    if let Some(dir) = std::env::var_os(\"XH_CONFIG_DIR\") {\n        return Some(dir.into());\n    }\n\n    if cfg!(target_os = \"macos\") {\n        // On macOS dirs returns `~/Library/Application Support`.\n        // ~/.config is more usual so we switched to that. But first we check for\n        // the legacy location.\n        let legacy_config_dir = dirs::config_dir()?.join(\"xh\");\n        let config_home = match var_os(\"XDG_CONFIG_HOME\") {\n            Some(dir) => dir.into(),\n            None => dirs::home_dir()?.join(\".config\"),\n        };\n        let new_config_dir = config_home.join(\"xh\");\n        if legacy_config_dir.exists() && !new_config_dir.exists() {\n            Some(legacy_config_dir)\n        } else {\n            Some(new_config_dir)\n        }\n    } else {\n        Some(dirs::config_dir()?.join(\"xh\"))\n    }\n}\n\npub fn get_home_dir() -> Option<PathBuf> {\n    #[cfg(target_os = \"windows\")]\n    if let Some(path) = std::env::var_os(\"XH_TEST_MODE_WIN_HOME_DIR\") {\n        return Some(PathBuf::from(path));\n    }\n\n    dirs::home_dir()\n}\n\n/// Perform simple tilde expansion if `dirs::home_dir()` is `Some(path)`.\n///\n/// Note that prefixed tilde e.g `~foo` is ignored.\n///\n/// See https://www.gnu.org/software/bash/manual/html_node/Tilde-Expansion.html\npub fn expand_tilde(path: impl AsRef<Path>) -> PathBuf {\n    if let Ok(path) = path.as_ref().strip_prefix(\"~\") {\n        let mut expanded_path = PathBuf::new();\n        expanded_path.push(get_home_dir().unwrap_or_else(|| \"~\".into()));\n        expanded_path.push(path);\n        expanded_path\n    } else {\n        path.as_ref().into()\n    }\n}\n\npub fn url_with_query(mut url: Url, query: &[(&str, Cow<str>)]) -> Url {\n    if !query.is_empty() {\n        // If we run this even without adding pairs it adds a `?`, hence\n        // the .is_empty() check\n        let mut pairs = url.query_pairs_mut();\n        for (name, value) in query {\n            pairs.append_pair(name, value);\n        }\n    }\n    url\n}\n\n// https://stackoverflow.com/a/45145246/5915221\n#[macro_export]\nmacro_rules! vec_of_strings {\n    ($($str:expr),*) => ({\n        vec![$(String::from($str),)*] as Vec<String>\n    });\n}\n\n/// When downloading a large file from a local nginx, it seems that 128KiB\n/// is a bit faster than 64KiB but bumping it up to 256KiB doesn't help any\n/// more.\n/// When increasing the buffer size all the way to 1MiB I observe 408KiB as\n/// the largest read size. But this doesn't translate to a shorter runtime.\npub const BUFFER_SIZE: usize = 128 * 1024;\n\n/// io::copy, but with a larger buffer size.\n///\n/// io::copy's buffer is just 8 KiB. This noticeably slows down fast\n/// large downloads, especially with a progress bar.\n///\n/// If `flush` is true, the writer will be flushed after each write. This is\n/// appropriate for streaming output, where you don't want a delay between data\n/// arriving and being shown.\npub fn copy_largebuf(\n    reader: &mut impl io::Read,\n    writer: &mut impl Write,\n    flush: bool,\n) -> io::Result<()> {\n    let mut buf = vec![0; BUFFER_SIZE];\n    loop {\n        match reader.read(&mut buf) {\n            Ok(0) => return Ok(()),\n            Ok(len) => {\n                writer.write_all(&buf[..len])?;\n                if flush {\n                    writer.flush()?;\n                }\n            }\n            Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,\n            Err(e) => return Err(e),\n        }\n    }\n}\n\npub(crate) trait HeaderValueExt {\n    fn to_utf8_str(&self) -> Result<&str, Utf8Error>;\n\n    fn to_ascii_or_latin1(&self) -> Result<&str, BadHeaderValue<'_>>;\n}\n\nimpl HeaderValueExt for HeaderValue {\n    fn to_utf8_str(&self) -> Result<&str, Utf8Error> {\n        std::str::from_utf8(self.as_bytes())\n    }\n\n    /// If the value is pure ASCII, return Ok(). If not, return Err() with methods for\n    /// further handling.\n    ///\n    /// The Ok() version cannot contain control characters (not even ASCII ones).\n    fn to_ascii_or_latin1(&self) -> Result<&str, BadHeaderValue<'_>> {\n        self.to_str().map_err(|_| BadHeaderValue { value: self })\n    }\n}\n\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub(crate) struct BadHeaderValue<'a> {\n    value: &'a HeaderValue,\n}\n\nimpl<'a> BadHeaderValue<'a> {\n    /// Return the header value's latin1 decoding, AKA isomorphic decode,\n    /// AKA ISO-8859-1 decode. This is how browsers tend to handle it.\n    ///\n    /// Not to be confused with ISO 8859-1 (which leaves 0x8X and 0x9X unmapped)\n    /// or with Windows-1252 (which is how HTTP bodies are decoded if they\n    /// declare `Content-Encoding: iso-8859-1`).\n    ///\n    /// Is likely to contain control characters. Consider replacing these.\n    pub(crate) fn latin1(self) -> String {\n        // https://infra.spec.whatwg.org/#isomorphic-decode\n        self.value.as_bytes().iter().map(|&b| b as char).collect()\n    }\n\n    /// Return the header value's UTF-8 decoding. This is most likely what the\n    /// user expects, but when browsers prefer another encoding we should give\n    /// that one precedence.\n    pub(crate) fn utf8(self) -> Option<&'a str> {\n        self.value.to_utf8_str().ok()\n    }\n}\n\npub(crate) fn reason_phrase(response: &Response) -> Cow<'_, str> {\n    if let Some(reason) = response.extensions().get::<hyper::ext::ReasonPhrase>() {\n        // The server sent a non-standard reason phrase.\n        // Seems like some browsers interpret this as latin1 and others as UTF-8?\n        // Rare case and clients aren't supposed to pay attention to the reason\n        // phrase so let's just do UTF-8 for convenience.\n        // We could send the bytes straight to stdout/stderr in case they're some\n        // other encoding but that's probably not worth the effort.\n        String::from_utf8_lossy(reason.as_bytes())\n    } else if let Some(reason) = response.status().canonical_reason() {\n        // On HTTP/2+ no reason phrase is sent so we're just explaining the code\n        // to the user.\n        // On HTTP/1.1 and below this matches the reason the server actually sent\n        // or else hyper would have added a ReasonPhrase.\n        Cow::Borrowed(reason)\n    } else {\n        // Only reachable in case of an unknown status code over HTTP/2+.\n        // curl prints nothing in this case.\n        Cow::Borrowed(\"<unknown status code>\")\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_latin1() {\n        let good = HeaderValue::from_static(\"Rhodes\");\n        let good = good.to_ascii_or_latin1();\n\n        assert_eq!(good, Ok(\"Rhodes\"));\n\n        let bad = HeaderValue::from_bytes(\"Ῥόδος\".as_bytes()).unwrap();\n        let bad = bad.to_ascii_or_latin1().unwrap_err();\n\n        assert_eq!(bad.latin1(), \"á¿¬Ï\\u{8c}Î´Î¿Ï\\u{82}\");\n        assert_eq!(bad.utf8(), Some(\"Ῥόδος\"));\n\n        let worse = HeaderValue::from_bytes(b\"R\\xF3dos\").unwrap();\n        let worse = worse.to_ascii_or_latin1().unwrap_err();\n\n        assert_eq!(worse.latin1(), \"Ródos\");\n        assert_eq!(worse.utf8(), None);\n    }\n}\n"
  },
  {
    "path": "tests/cases/auth_message_signature.rs",
    "content": "use crate::{get_command, server};\nuse base64::engine::general_purpose::STANDARD;\nuse httpsig_hyper::HyperSigError;\nuse httpsig_hyper::prelude::*;\n\nconst KEY_MATERIAL: &str = \"secret-key-material\";\nconst RSA_KEY_FIXTURE: &str = \"tests/fixtures/keys/rsa_private_key_pkcs8.pem\";\n\nfn fixture_path(relative_path: &str) -> String {\n    format!(\"{}/{}\", env!(\"CARGO_MANIFEST_DIR\"), relative_path)\n}\n\nfn reconstruct_absolute_uri<B>(req: &mut hyper::Request<B>) {\n    // Reconstruct absolute URI for verification of @target-uri and @authority\n    if let Some(host) = req.headers().get(\"host\") {\n        let host_str = host.to_str().unwrap();\n        let uri_string = format!(\"http://{}{}\", host_str, req.uri());\n        *req.uri_mut() = uri_string.parse().unwrap();\n    }\n}\n\n#[test]\nfn message_signature_verification_on_server() {\n    let key_id = \"test-key\";\n    let key_material = KEY_MATERIAL;\n\n    let server = server::http(move |req| {\n        let key_id_inner = key_id.to_string();\n        let key_material_inner = key_material.to_string();\n        async move {\n            // 1. Prepare the verification key (HMAC SHA256)\n            use base64::Engine;\n            let key_base64 = STANDARD.encode(key_material_inner);\n            let shared_key =\n                SharedKey::from_base64(&AlgorithmName::HmacSha256, &key_base64).unwrap();\n\n            // 2. Verify the request using extension trait provided by httpsig-hyper\n            use httpsig_hyper::MessageSignatureReq;\n            let result: Result<String, HyperSigError> = req\n                .verify_message_signature(&shared_key, Some(&key_id_inner))\n                .await;\n\n            if result.is_ok() {\n                hyper::Response::new(Default::default())\n            } else {\n                hyper::Response::builder()\n                    .status(401)\n                    .body(Default::default())\n                    .unwrap()\n            }\n        }\n    });\n\n    get_command()\n        .arg(format!(\"--unstable-m-sig-id={}\", key_id))\n        .arg(format!(\"--unstable-m-sig-key={}\", key_material))\n        .arg(\"--unstable-m-sig-comp=@method,@path\")\n        .arg(\"--unstable-m-sig-comp=date\")\n        .arg(\"get\")\n        .arg(server.base_url())\n        .arg(\"date:Thu, 15 Jan 2026 12:00:00 GMT\")\n        .assert()\n        .success();\n}\n\n#[test]\nfn message_signature_redirect_follow_re_signs_request() {\n    let key = KEY_MATERIAL;\n    let key_id = \"my-key\";\n\n    let server = server::http(move |mut req| {\n        let key_inner = key.to_string();\n        let key_id_inner = key_id.to_string();\n        async move {\n            if req.uri().path() == \"/redirect\" {\n                return hyper::Response::builder()\n                    .status(302)\n                    .header(\"Location\", \"/final\")\n                    .body(Default::default())\n                    .unwrap();\n            }\n\n            assert_eq!(req.uri().path(), \"/final\");\n            reconstruct_absolute_uri(&mut req);\n\n            use base64::Engine;\n            let key_base64 = STANDARD.encode(&key_inner);\n            let shared_key =\n                SharedKey::from_base64(&AlgorithmName::HmacSha256, &key_base64).unwrap();\n\n            use httpsig_hyper::MessageSignatureReq;\n            let result = req\n                .verify_message_signature(&shared_key, Some(&key_id_inner))\n                .await;\n            assert!(\n                result.is_ok(),\n                \"Signature verification failed on redirected request: {:?}\",\n                result.err()\n            );\n\n            hyper::Response::default()\n        }\n    });\n\n    get_command()\n        .arg(\"--unstable-m-sig-id=my-key\")\n        .arg(format!(\"--unstable-m-sig-key={}\", key))\n        .arg(\"--follow\")\n        .arg(\"get\")\n        .arg(server.url(\"/redirect\"))\n        .assert()\n        .success();\n}\n\n#[test]\nfn message_signature_auth_defaults() {\n    let key = KEY_MATERIAL;\n    let key_id = \"my-key\";\n\n    let server = server::http(move |mut req| {\n        let key_inner = key.to_string();\n        let key_id_inner = key_id.to_string();\n        async move {\n            reconstruct_absolute_uri(&mut req);\n\n            assert_eq!(req.method(), \"POST\");\n            assert!(req.headers().contains_key(\"Signature\"));\n            assert!(req.headers().contains_key(\"Signature-Input\"));\n\n            let sig_input = req.headers()[\"Signature-Input\"].to_str().unwrap();\n\n            // Expect default components: @method, @authority, @target-uri\n            assert!(sig_input.contains(\"sig1=\"));\n            assert!(sig_input.contains(r#\"\"@method\" \"@authority\" \"@target-uri\"\"#));\n            assert!(sig_input.contains(r#\"keyid=\"my-key\"\"#));\n\n            // Verify the signature\n            use base64::Engine;\n            let key_base64 = STANDARD.encode(&key_inner);\n            let shared_key =\n                SharedKey::from_base64(&AlgorithmName::HmacSha256, &key_base64).unwrap();\n            use httpsig_hyper::MessageSignatureReq;\n            let result = req\n                .verify_message_signature(&shared_key, Some(&key_id_inner))\n                .await;\n            assert!(\n                result.is_ok(),\n                \"Signature verification failed: {:?}\",\n                result.err()\n            );\n\n            hyper::Response::default()\n        }\n    });\n\n    get_command()\n        .arg(\"--unstable-m-sig-id=my-key\")\n        .arg(format!(\"--unstable-m-sig-key={}\", key))\n        .arg(\"-v\")\n        .arg(\"post\")\n        .arg(server.base_url())\n        .arg(\"foo=bar\")\n        .assert()\n        .success()\n        .stdout(predicates::str::contains(\"Signature: sig1=\"))\n        .stdout(predicates::str::contains(\"Signature-Input: sig1=\"));\n}\n\n#[test]\nfn message_signature_auth_with_resolve_override() {\n    let key = KEY_MATERIAL;\n    let key_id = \"my-key\";\n\n    let server = server::http(move |mut req| {\n        let key_inner = key.to_string();\n        let key_id_inner = key_id.to_string();\n        async move {\n            reconstruct_absolute_uri(&mut req);\n\n            let host = req.headers()[\"host\"].to_str().unwrap();\n            assert!(\n                host.starts_with(\"example.com\"),\n                \"unexpected host header: {host}\"\n            );\n\n            use base64::Engine;\n            let key_base64 = STANDARD.encode(&key_inner);\n            let shared_key =\n                SharedKey::from_base64(&AlgorithmName::HmacSha256, &key_base64).unwrap();\n\n            use httpsig_hyper::MessageSignatureReq;\n            let result = req\n                .verify_message_signature(&shared_key, Some(&key_id_inner))\n                .await;\n            assert!(\n                result.is_ok(),\n                \"Signature verification failed: {:?}\",\n                result.err()\n            );\n\n            hyper::Response::default()\n        }\n    });\n\n    get_command()\n        .arg(\"--unstable-m-sig-id=my-key\")\n        .arg(format!(\"--unstable-m-sig-key={}\", key))\n        .arg(format!(\"--resolve=example.com:{}\", server.host()))\n        .arg(\"get\")\n        .arg(format!(\"http://example.com:{}/resolve\", server.port()))\n        .assert()\n        .success();\n}\n\n#[test]\nfn message_signature_auth_ipv6_authority() {\n    let key = KEY_MATERIAL;\n    let key_id = \"my-key\";\n\n    let server = match server::http_v6(move |mut req| {\n        let key_inner = key.to_string();\n        let key_id_inner = key_id.to_string();\n        async move {\n            reconstruct_absolute_uri(&mut req);\n\n            assert_eq!(req.method(), \"GET\");\n            assert!(req.headers().contains_key(\"Signature\"));\n            assert!(req.headers().contains_key(\"Signature-Input\"));\n\n            // Verify the signature\n            use base64::Engine;\n            let key_base64 = STANDARD.encode(&key_inner);\n            let shared_key =\n                SharedKey::from_base64(&AlgorithmName::HmacSha256, &key_base64).unwrap();\n            use httpsig_hyper::MessageSignatureReq;\n            let result = req\n                .verify_message_signature(&shared_key, Some(&key_id_inner))\n                .await;\n            assert!(\n                result.is_ok(),\n                \"Signature verification failed: {:?}\",\n                result.err()\n            );\n\n            hyper::Response::default()\n        }\n    }) {\n        Some(server) => server,\n        None => {\n            eprintln!(\"IPv6 not available; skipping test\");\n            return;\n        }\n    };\n\n    let host = server.host();\n    let url = if host.contains(':') {\n        format!(\"http://[{host}]:{}\", server.port())\n    } else {\n        format!(\"http://{host}:{}\", server.port())\n    };\n    let mut cmd = get_command();\n    cmd.arg(\"--unstable-m-sig-id=my-key\")\n        .arg(format!(\"--unstable-m-sig-key={}\", key))\n        .arg(\"-v\")\n        .arg(\"get\")\n        .arg(url)\n        .assert()\n        .success()\n        .stdout(predicates::str::contains(\"Signature: sig1=\"))\n        .stdout(predicates::str::contains(\"Signature-Input: sig1=\"));\n}\n\n#[test]\nfn message_signature_auth_with_custom_components_and_digest() {\n    let key = KEY_MATERIAL;\n    let key_id = \"my-key\";\n\n    let server = server::http(move |mut req| {\n        let key_inner = key.to_string();\n        let key_id_inner = key_id.to_string();\n        async move {\n            reconstruct_absolute_uri(&mut req);\n\n            assert_eq!(req.method(), \"POST\");\n            assert!(req.headers().contains_key(\"Signature\"));\n            assert!(req.headers().contains_key(\"Signature-Input\"));\n            assert!(req.headers().contains_key(\"Content-Digest\"));\n\n            let sig_input = req.headers()[\"Signature-Input\"].to_str().unwrap();\n            assert!(sig_input.contains(r#\"\"@method\" \"@target-uri\" \"content-digest\"\"#));\n            assert!(!sig_input.contains(r#\"\"@authority\"\"#)); // We overrode defaults\n\n            let digest = req.headers()[\"Content-Digest\"].to_str().unwrap();\n            assert!(digest.starts_with(\"sha-256=:\"));\n\n            // Verify the signature\n            use base64::Engine;\n            let key_base64 = STANDARD.encode(&key_inner);\n            let shared_key =\n                SharedKey::from_base64(&AlgorithmName::HmacSha256, &key_base64).unwrap();\n            use httpsig_hyper::MessageSignatureReq;\n            let result = req\n                .verify_message_signature(&shared_key, Some(&key_id_inner))\n                .await;\n            assert!(\n                result.is_ok(),\n                \"Signature verification failed: {:?}\",\n                result.err()\n            );\n\n            hyper::Response::default()\n        }\n    });\n\n    get_command()\n        .arg(\"--unstable-m-sig-id=my-key\")\n        .arg(format!(\"--unstable-m-sig-key={}\", key))\n        .arg(\"--unstable-m-sig-comp=@method,@target-uri,content-digest\")\n        .arg(\"-v\")\n        .arg(\"post\")\n        .arg(server.base_url())\n        .arg(\"foo=bar\")\n        .assert()\n        .success()\n        .stdout(predicates::str::contains(\"Signature: sig1=\"))\n        .stdout(predicates::str::contains(\"Signature-Input: sig1=\"))\n        .stdout(predicates::str::contains(\"Content-Digest: sha-256=\"));\n}\n\n#[test]\nfn message_signature_auth_with_multiple_set_cookie() {\n    let key = KEY_MATERIAL;\n    let key_id = \"my-key\";\n\n    let server = server::http(move |req| {\n        let key_inner = key.to_string();\n        let key_id_inner = key_id.to_string();\n        async move {\n            let sig_input = req.headers()[\"Signature-Input\"].to_str().unwrap();\n\n            // Assertions for correctness:\n            // 1. Label sig1 should be present\n            assert!(sig_input.contains(\"sig1=\"));\n            // 2. normalize_component_id: @method should NOT be quoted if no params\n            assert!(sig_input.contains(\"@method\"));\n            // 3. Set-Cookie should be present\n            assert!(sig_input.contains(r#\"\"set-cookie\"\"#));\n            // 4. keyid should be present\n            assert!(sig_input.contains(r#\"keyid=\"my-key\"\"#));\n\n            // Verify the signature\n            use base64::Engine;\n            let key_base64 = STANDARD.encode(&key_inner);\n            let shared_key =\n                SharedKey::from_base64(&AlgorithmName::HmacSha256, &key_base64).unwrap();\n            use httpsig_hyper::MessageSignatureReq;\n            let result = req\n                .verify_message_signature(&shared_key, Some(&key_id_inner))\n                .await;\n            assert!(\n                result.is_ok(),\n                \"Signature verification failed: {:?}\",\n                result.err()\n            );\n\n            hyper::Response::default()\n        }\n    });\n\n    get_command()\n        .arg(\"--unstable-m-sig-id=my-key\")\n        .arg(format!(\"--unstable-m-sig-key={}\", key))\n        .arg(\"--unstable-m-sig-comp=@method,set-cookie\")\n        .arg(\"-v\")\n        .arg(\"get\")\n        .arg(server.base_url())\n        .arg(\"set-cookie:a=1\")\n        .arg(\"set-cookie:b=2\")\n        .assert()\n        .success()\n        .stdout(predicates::str::contains(\"Signature: sig1=\"))\n        .stdout(predicates::str::contains(\"Signature-Input: sig1=\"));\n}\n\n#[test]\nfn message_signature_auth_sf_parameter() {\n    let key = KEY_MATERIAL;\n    let key_id = \"my-key\";\n\n    let server = server::http(move |req| {\n        let key_inner = key.to_string();\n        let key_id_inner = key_id.to_string();\n        async move {\n            let sig_input = req.headers()[\"Signature-Input\"].to_str().unwrap();\n            assert!(sig_input.contains(r#\"\"x-struct\";sf\"#));\n\n            // Verify the signature\n            use base64::Engine;\n            let key_base64 = STANDARD.encode(&key_inner);\n            let shared_key =\n                SharedKey::from_base64(&AlgorithmName::HmacSha256, &key_base64).unwrap();\n            use httpsig_hyper::MessageSignatureReq;\n            let result = req\n                .verify_message_signature(&shared_key, Some(&key_id_inner))\n                .await;\n            assert!(\n                result.is_ok(),\n                \"Signature verification failed: {:?}\",\n                result.err()\n            );\n\n            hyper::Response::default()\n        }\n    });\n\n    get_command()\n        .arg(\"--unstable-m-sig-id=my-key\")\n        .arg(format!(\"--unstable-m-sig-key={}\", key))\n        .arg(\"--unstable-m-sig-comp=\\\"x-struct\\\";sf\")\n        .arg(\"-v\")\n        .arg(\"get\")\n        .arg(server.base_url())\n        .arg(\"x-struct:a=1, b=2\")\n        .assert()\n        .success()\n        .stdout(predicates::str::contains(\"Signature-Input: sig1=\"));\n}\n\n#[test]\nfn message_signature_auth_key_parameter() {\n    let key = KEY_MATERIAL;\n    let key_id = \"my-key\";\n\n    let server = server::http(move |req| {\n        let key_inner = key.to_string();\n        let key_id_inner = key_id.to_string();\n        async move {\n            let sig_input = req.headers()[\"Signature-Input\"].to_str().unwrap();\n            assert!(sig_input.contains(r#\"\"x-dict\";key=\"a\"\"#));\n\n            // Verify the signature\n            use base64::Engine;\n            let key_base64 = STANDARD.encode(&key_inner);\n            let shared_key =\n                SharedKey::from_base64(&AlgorithmName::HmacSha256, &key_base64).unwrap();\n            use httpsig_hyper::MessageSignatureReq;\n            let result = req\n                .verify_message_signature(&shared_key, Some(&key_id_inner))\n                .await;\n\n            assert!(\n                result.is_ok(),\n                \"Signature verification failed: {:?}\",\n                result.err()\n            );\n\n            hyper::Response::default()\n        }\n    });\n\n    get_command()\n        .arg(\"--unstable-m-sig-id=my-key\")\n        .arg(format!(\"--unstable-m-sig-key={}\", key))\n        .arg(\"--unstable-m-sig-comp=\\\"x-dict\\\";key=\\\"a\\\"\")\n        .arg(\"-v\")\n        .arg(\"get\")\n        .arg(server.base_url())\n        .arg(\"x-dict:a=1, b=2\")\n        .assert()\n        .success()\n        .stdout(predicates::str::contains(\"Signature-Input: sig1=\"));\n}\n\n#[test]\nfn message_signature_auth_unsupported_parameters() {\n    let key = KEY_MATERIAL;\n    let url = \"http://localhost:1\";\n\n    // Test ;bs (Byte Sequence) - currently unsupported by httpsig\n    get_command()\n        .arg(\"--unstable-m-sig-id=my-key\")\n        .arg(format!(\"--unstable-m-sig-key={}\", key))\n        .arg(\"--unstable-m-sig-comp=\\\"x-data\\\";bs\")\n        .arg(\"get\")\n        .arg(url)\n        .arg(\"x-data:hello\")\n        .assert()\n        .failure()\n        .stderr(predicates::str::contains(\"not supported\"));\n\n    // Test ;tr (Trailers) - currently unsupported by httpsig\n    get_command()\n        .arg(\"--unstable-m-sig-id=my-key\")\n        .arg(format!(\"--unstable-m-sig-key={}\", key))\n        .arg(\"--unstable-m-sig-comp=\\\"x-field\\\";tr\")\n        .arg(\"get\")\n        .arg(url)\n        .arg(\"x-field:value\")\n        .assert()\n        .failure()\n        .stderr(predicates::str::contains(\"not supported\"));\n}\n\n#[test]\nfn message_signature_components_require_key_pair() {\n    get_command()\n        .arg(\"--offline\")\n        .arg(\"--unstable-m-sig-comp=@method\")\n        .arg(\"get\")\n        .arg(\"https://example.com\")\n        .assert()\n        .failure()\n        .stderr(predicates::str::contains(\n            \"Message signature components require both --unstable-m-sig-id and --unstable-m-sig-key.\",\n        ));\n}\n\n#[test]\nfn message_signature_with_basic_auth() {\n    let key = KEY_MATERIAL;\n    let key_id = \"my-key\";\n\n    let server = server::http(move |mut req| {\n        let key_inner = key.to_string();\n        let key_id_inner = key_id.to_string();\n        async move {\n            reconstruct_absolute_uri(&mut req);\n\n            assert!(req.headers().contains_key(\"Authorization\"));\n            assert!(req.headers().contains_key(\"Signature\"));\n            assert!(\n                req.headers()[\"Authorization\"]\n                    .to_str()\n                    .unwrap()\n                    .starts_with(\"Basic \")\n            );\n\n            // Verify the signature\n            use base64::Engine;\n            let key_base64 = STANDARD.encode(&key_inner);\n            let shared_key =\n                SharedKey::from_base64(&AlgorithmName::HmacSha256, &key_base64).unwrap();\n            use httpsig_hyper::MessageSignatureReq;\n            let result = req\n                .verify_message_signature(&shared_key, Some(&key_id_inner))\n                .await;\n            assert!(\n                result.is_ok(),\n                \"Signature verification failed: {:?}\",\n                result.err()\n            );\n\n            hyper::Response::default()\n        }\n    });\n\n    get_command()\n        .arg(\"--auth=user:pass\")\n        .arg(\"--auth-type=basic\")\n        .arg(\"--unstable-m-sig-id=my-key\")\n        .arg(format!(\"--unstable-m-sig-key={}\", key))\n        .arg(\"-v\")\n        .arg(\"get\")\n        .arg(server.base_url())\n        .assert()\n        .success()\n        .stdout(predicates::str::contains(\n            \"Authorization: Basic dXNlcjpwYXNz\",\n        ))\n        .stdout(predicates::str::contains(\"Signature: sig1=\"));\n}\n\n#[test]\nfn message_signature_auth_normalization_assertion() {\n    let key = KEY_MATERIAL;\n\n    let server = server::http(move |req| {\n        async move {\n            let sig_input = req.headers()[\"Signature-Input\"].to_str().unwrap();\n\n            // Assert normalize_component_id: \"@query-param\" should be quoted because it has params\n            // Even if input as @query-param;name=\"id\", it should be normalized to \"@query-param\";name=\"id\"\n            assert!(sig_input.contains(r#\"\"@query-param\";name=\"id\"\"#));\n\n            hyper::Response::default()\n        }\n    });\n\n    get_command()\n        .arg(\"--unstable-m-sig-id=my-key\")\n        .arg(format!(\"--unstable-m-sig-key={}\", key))\n        .arg(\"--unstable-m-sig-comp=@method,@query-param;name=\\\"id\\\"\")\n        .arg(\"-v\")\n        .arg(\"get\")\n        .arg(format!(\"{}/?id=123\", server.base_url()))\n        .assert()\n        .success()\n        .stdout(predicates::str::contains(\"Signature-Input: sig1=\"));\n}\n\n#[test]\nfn message_signature_auth_ed25519_pem() {\n    // Generated Ed25519 private key in PEM format\n    let key_pem = r#\"-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIJthSCf1pnwSYvdXIrXHikXUix0dmvLEm2JwWF+87xKG\n-----END PRIVATE KEY-----\"#;\n    let key_id = \"ed25519-key\";\n\n    let server = server::http(move |mut req| {\n        let key_pem_inner = key_pem.to_string();\n        let key_id_inner = key_id.to_string();\n        async move {\n            reconstruct_absolute_uri(&mut req);\n\n            let sig_input = req.headers()[\"Signature-Input\"].to_str().unwrap();\n            assert!(sig_input.contains(\"alg=\\\"ed25519\\\"\"));\n            assert!(sig_input.contains(r#\"keyid=\"ed25519-key\"\"#));\n\n            // Verify the signature using the public key\n            let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, &key_pem_inner).unwrap();\n            let public_key = secret_key.public_key();\n\n            use httpsig_hyper::MessageSignatureReq;\n            let result = req\n                .verify_message_signature(&public_key, Some(&key_id_inner))\n                .await;\n\n            assert!(\n                result.is_ok(),\n                \"Signature verification failed: {:?}\",\n                result.err()\n            );\n\n            hyper::Response::default()\n        }\n    });\n\n    get_command()\n        .arg(\"--unstable-m-sig-id=ed25519-key\")\n        .arg(format!(\"--unstable-m-sig-key={}\", key_pem))\n        .arg(\"get\")\n        .arg(server.base_url())\n        .assert()\n        .success();\n}\n\n#[test]\nfn message_signature_auth_rsa_pem() {\n    let key_pem = std::fs::read_to_string(fixture_path(RSA_KEY_FIXTURE)).unwrap();\n    let key_id = \"rsa-key\";\n\n    let server = server::http(move |mut req| {\n        let key_pem_inner = key_pem.clone();\n        let key_id_inner = key_id.to_string();\n        async move {\n            reconstruct_absolute_uri(&mut req);\n\n            let sig_input = req.headers()[\"Signature-Input\"].to_str().unwrap();\n            assert!(sig_input.contains(\"alg=\\\"rsa-v1_5-sha256\\\"\"));\n            assert!(sig_input.contains(r#\"keyid=\"rsa-key\"\"#));\n\n            let secret_key =\n                SecretKey::from_pem(&AlgorithmName::RsaV1_5Sha256, &key_pem_inner).unwrap();\n            let public_key = secret_key.public_key();\n\n            use httpsig_hyper::MessageSignatureReq;\n            let result = req\n                .verify_message_signature(&public_key, Some(&key_id_inner))\n                .await;\n            assert!(\n                result.is_ok(),\n                \"RSA signature verification failed: {:?}\",\n                result.err()\n            );\n\n            hyper::Response::default()\n        }\n    });\n\n    get_command()\n        .arg(\"--unstable-m-sig-id=rsa-key\")\n        .arg(\"--unstable-m-sig-alg=rsa-v1_5-sha256\")\n        .arg(format!(\n            \"--unstable-m-sig-key=@{}\",\n            fixture_path(RSA_KEY_FIXTURE)\n        ))\n        .arg(\"get\")\n        .arg(server.base_url())\n        .assert()\n        .success();\n}\n\n#[test]\nfn message_signature_auth_rsa_pem_requires_explicit_algorithm() {\n    get_command()\n        .arg(\"--unstable-m-sig-id=rsa-key\")\n        .arg(format!(\n            \"--unstable-m-sig-key=@{}\",\n            fixture_path(RSA_KEY_FIXTURE)\n        ))\n        .arg(\"get\")\n        .arg(\"http://localhost:1\")\n        .assert()\n        .failure()\n        .stderr(predicates::str::contains(\n            \"RSA private keys require an explicit algorithm\",\n        ));\n}\n"
  },
  {
    "path": "tests/cases/compress_request_body.rs",
    "content": "use std::{fs::OpenOptions, io::Read as _};\n\nuse hyper::header::HeaderValue;\nuse predicates::str::contains;\n\nuse crate::prelude::*;\nuse std::io::Write;\n\nfn zlib_decode(bytes: Vec<u8>) -> std::io::Result<String> {\n    let mut z = flate2::read::ZlibDecoder::new(&bytes[..]);\n    let mut s = String::new();\n    z.read_to_string(&mut s)?;\n    Ok(s)\n}\n\nfn server() -> server::Server {\n    server::http(|req| async move {\n        match req.uri().path() {\n            \"/deflate\" => {\n                assert_eq!(\n                    req.headers().get(hyper::header::CONTENT_ENCODING),\n                    Some(HeaderValue::from_static(\"deflate\")).as_ref()\n                );\n\n                let compressed_body = req.body().await;\n                let body = zlib_decode(compressed_body).unwrap();\n                hyper::Response::builder()\n                    .header(\"date\", \"N/A\")\n                    .header(\"Content-Type\", \"text/plain\")\n                    .body(body.into())\n                    .unwrap()\n            }\n            \"/normal\" => {\n                assert_eq!(req.headers().get(hyper::header::CONTENT_ENCODING), None);\n\n                let body = req.body_as_string().await;\n                hyper::Response::builder()\n                    .header(\"date\", \"N/A\")\n                    .header(\"Content-Type\", \"text/plain\")\n                    .body(body.into())\n                    .unwrap()\n            }\n            _ => panic!(\"unknown path\"),\n        }\n    })\n}\n\n#[test]\nfn compress_request_body_json() {\n    let server = server();\n\n    get_command()\n        .arg(format!(\"{}/deflate\", server.base_url()))\n        .args([\n            &format!(\"key={}\", \"1\".repeat(1000)),\n            \"-x\",\n            \"-j\",\n            \"--pretty=none\",\n        ])\n        .assert()\n        .stdout(indoc::formatdoc! {r#\"\n            HTTP/1.1 200 OK\n            Date: N/A\n            Content-Type: text/plain\n            Content-Length: 1010\n\n            {{\"key\":\"{c}\"}}\n        \"#, c = \"1\".repeat(1000),});\n}\n\n#[test]\nfn compress_request_body_form() {\n    let server = server();\n\n    get_command()\n        .arg(format!(\"{}/deflate\", server.base_url()))\n        .args([\n            &format!(\"key={}\", \"1\".repeat(1000)),\n            \"-x\",\n            \"-x\",\n            \"-f\",\n            \"--pretty=none\",\n        ])\n        .assert()\n        .stdout(indoc::formatdoc! {r#\"\n            HTTP/1.1 200 OK\n            Date: N/A\n            Content-Type: text/plain\n            Content-Length: 1004\n\n            key={c}\n        \"#, c = \"1\".repeat(1000),});\n}\n\n#[test]\nfn skip_compression_when_compression_ratio_is_negative() {\n    let server = server();\n    get_command()\n        .arg(format!(\"{}/normal\", server.base_url()))\n        .args([&format!(\"key={}\", \"1\"), \"-x\", \"-f\", \"--pretty=none\"])\n        .assert()\n        .stdout(indoc::formatdoc! {r#\"\n            HTTP/1.1 200 OK\n            Date: N/A\n            Content-Type: text/plain\n            Content-Length: 5\n\n            key={c}\n        \"#, c = \"1\"});\n}\n\n#[test]\nfn test_compress_force_with_negative_ratio() {\n    let server = server();\n    get_command()\n        .arg(format!(\"{}/deflate\", server.base_url()))\n        .args([&format!(\"key={}\", \"1\"), \"-xx\", \"-f\", \"--pretty=none\"])\n        .assert()\n        .stdout(indoc::formatdoc! {r#\"\n            HTTP/1.1 200 OK\n            Date: N/A\n            Content-Type: text/plain\n            Content-Length: 5\n\n            key={c}\n        \"#, c = \"1\"});\n}\n\n#[test]\nfn dont_compress_request_body_if_content_encoding_have_value() {\n    let server = server::http(|req| async move {\n        assert_eq!(\n            req.headers().get(hyper::header::CONTENT_ENCODING),\n            Some(HeaderValue::from_static(\"identity\")).as_ref()\n        );\n\n        let body = req.body_as_string().await;\n        hyper::Response::builder()\n            .header(\"date\", \"N/A\")\n            .header(\"Content-Type\", \"text/plain\")\n            .body(body.into())\n            .unwrap()\n    });\n    get_command()\n        .arg(format!(\"{}/\", server.base_url()))\n        .args([\n            &format!(\"key={}\", \"1\".repeat(1000)),\n            \"content-encoding:identity\",\n            \"-xx\",\n            \"-f\",\n            \"--pretty=none\",\n        ])\n        .assert()\n        .stdout(indoc::formatdoc! {r#\"\n            HTTP/1.1 200 OK\n            Date: N/A\n            Content-Type: text/plain\n            Content-Length: 1004\n\n            key={c}\n        \"#, c = \"1\".repeat(1000),})\n        .stderr(contains( \"warning: --compress can't be used with a 'Content-Encoding:' header. --compress will be disabled.\"))\n        .success()\n        ;\n}\n\n#[test]\nfn compress_body_from_file() {\n    let server = server::http(|req| async move {\n        assert_eq!(\n            req.headers().get(hyper::header::CONTENT_ENCODING),\n            Some(HeaderValue::from_static(\"deflate\")).as_ref()\n        );\n        assert_eq!(\"Hello world\\n\", zlib_decode(req.body().await).unwrap());\n        hyper::Response::default()\n    });\n\n    let dir = tempfile::tempdir().unwrap();\n    let filename = dir.path().join(\"input.txt\");\n    OpenOptions::new()\n        .create(true)\n        .truncate(true)\n        .write(true)\n        .open(&filename)\n        .unwrap()\n        .write_all(b\"Hello world\\n\")\n        .unwrap();\n\n    get_command()\n        .arg(server.base_url())\n        .arg(\"-xx\")\n        .arg(format!(\"@{}\", filename.to_string_lossy()))\n        .assert()\n        .success();\n}\n\n#[test]\nfn compress_body_from_file_unless_compress_rate_less_1() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers().get(hyper::header::CONTENT_ENCODING), None);\n        assert_eq!(\"Hello world\\n\", req.body_as_string().await);\n        hyper::Response::default()\n    });\n\n    let dir = tempfile::tempdir().unwrap();\n    let filename = dir.path().join(\"input.txt\");\n    OpenOptions::new()\n        .create(true)\n        .truncate(true)\n        .write(true)\n        .open(&filename)\n        .unwrap()\n        .write_all(b\"Hello world\\n\")\n        .unwrap();\n\n    get_command()\n        .arg(server.base_url())\n        .arg(\"-x\")\n        .arg(format!(\"@{}\", filename.to_string_lossy()))\n        .assert()\n        .success();\n}\n\n#[test]\nfn test_cannot_combine_compress_with_multipart() {\n    get_command()\n        .arg(format!(\"{}/deflate\", \"\"))\n        .args([\"--multipart\", \"-x\", \"a=1\"])\n        .assert()\n        .failure()\n        .stderr(contains(\n            \"the argument '--multipart' cannot be used with '--compress...'\",\n        ));\n}\n"
  },
  {
    "path": "tests/cases/download.rs",
    "content": "use std::{\n    fs::{self, OpenOptions},\n    io::Write,\n};\n\nuse predicates::str::contains;\nuse tempfile::tempdir;\n\nuse crate::prelude::*;\n\n#[test]\nfn download() {\n    let dir = tempdir().unwrap();\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .body(\"file contents\\n\".into())\n            .unwrap()\n    });\n\n    let outfile = dir.path().join(\"outfile\");\n    get_command()\n        .arg(\"--download\")\n        .arg(\"--output\")\n        .arg(&outfile)\n        .arg(server.base_url())\n        .assert()\n        .success();\n    assert_eq!(fs::read_to_string(&outfile).unwrap(), \"file contents\\n\");\n}\n\n#[test]\nfn accept_encoding_not_modifiable_in_download_mode() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[\"accept-encoding\"], \"identity\");\n        hyper::Response::builder()\n            .body(r#\"{\"ids\":[1,2,3]}\"#.into())\n            .unwrap()\n    });\n\n    let dir = tempdir().unwrap();\n    get_command()\n        .current_dir(&dir)\n        .args([&server.base_url(), \"--download\", \"accept-encoding:gzip\"])\n        .assert()\n        .success();\n}\n\n#[test]\nfn download_generated_filename() {\n    let dir = tempdir().unwrap();\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"Content-Type\", \"application/json\")\n            .body(\"file\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .args([\"--download\", &server.url(\"/foo/bar/\")])\n        .current_dir(&dir)\n        .assert()\n        .success();\n\n    get_command()\n        .args([\"--download\", &server.url(\"/foo/bar/\")])\n        .current_dir(&dir)\n        .assert()\n        .success();\n\n    assert_eq!(\n        fs::read_to_string(dir.path().join(\"bar.json\")).unwrap(),\n        \"file\"\n    );\n    assert_eq!(\n        fs::read_to_string(dir.path().join(\"bar.json-1\")).unwrap(),\n        \"file\"\n    );\n}\n\n#[test]\nfn download_supplied_filename() {\n    let dir = tempdir().unwrap();\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"Content-Disposition\", r#\"attachment; filename=\"foo.bar\"\"#)\n            .body(\"file\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .args([\"--download\", &server.base_url()])\n        .current_dir(&dir)\n        .assert()\n        .success();\n    assert_eq!(\n        fs::read_to_string(dir.path().join(\"foo.bar\")).unwrap(),\n        \"file\"\n    );\n}\n\n#[test]\nfn download_supplied_unicode_filename() {\n    let dir = tempdir().unwrap();\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"Content-Disposition\", r#\"attachment; filename=\"😀.bar\"\"#)\n            .body(\"file\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .args([\"--download\", &server.base_url()])\n        .current_dir(&dir)\n        .assert()\n        .success();\n    assert_eq!(\n        fs::read_to_string(dir.path().join(\"😀.bar\")).unwrap(),\n        \"file\"\n    );\n}\n\n#[test]\nfn download_support_filename_rfc_5987() {\n    let dir = tempdir().unwrap();\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\n                \"Content-Disposition\",\n                r#\"attachment; filename*=UTF-8''abcd1234.txt\"#,\n            )\n            .body(\"file\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .args([\"--download\", &server.base_url()])\n        .current_dir(&dir)\n        .assert()\n        .success();\n    assert_eq!(\n        fs::read_to_string(dir.path().join(\"abcd1234.txt\")).unwrap(),\n        \"file\"\n    );\n}\n#[test]\nfn download_support_filename_rfc_5987_percent_encoded() {\n    let dir = tempdir().unwrap();\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\n                \"Content-Disposition\",\n                r#\"attachment; filename*=UTF-8''%E6%B5%8B%E8%AF%95.txt\"#,\n            )\n            .body(\"file\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .args([\"--download\", &server.base_url()])\n        .current_dir(&dir)\n        .assert()\n        .success();\n    assert_eq!(\n        fs::read_to_string(dir.path().join(\"测试.txt\")).unwrap(),\n        \"file\"\n    );\n}\n\n#[test]\nfn download_support_filename_rfc_5987_percent_encoded_with_iso_8859_1() {\n    let dir = tempdir().unwrap();\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\n                \"Content-Disposition\",\n                r#\"attachment; filename*=iso-8859-1'en'%A3%20rates.txt\"#,\n            )\n            .body(\"file\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .args([\"--download\", &server.base_url()])\n        .current_dir(&dir)\n        .assert()\n        .success();\n    assert_eq!(\n        fs::read_to_string(dir.path().join(\"£ rates.txt\")).unwrap(),\n        \"file\"\n    );\n}\n\n#[test]\nfn download_filename_star_with_high_priority() {\n    let dir = tempdir().unwrap();\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\n                \"Content-Disposition\",\n                r#\"attachment; filename=\"fallback.txt\"; filename*=UTF-8''%E6%B5%8B%E8%AF%95.txt\"#,\n            )\n            .body(\"file\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .args([\"--download\", &server.base_url()])\n        .current_dir(&dir)\n        .assert()\n        .success();\n    assert_eq!(\n        fs::read_to_string(dir.path().join(\"测试.txt\")).unwrap(),\n        \"file\"\n    );\n}\n\n#[test]\nfn download_supplied_unquoted_filename() {\n    let dir = tempdir().unwrap();\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"Content-Disposition\", r#\"attachment; filename=foo bar baz\"#)\n            .body(\"file\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .args([\"--download\", &server.base_url()])\n        .current_dir(&dir)\n        .assert()\n        .success();\n    assert_eq!(\n        fs::read_to_string(dir.path().join(\"foo bar baz\")).unwrap(),\n        \"file\"\n    );\n}\n\n#[test]\nfn download_filename_with_directory_traversal() {\n    let dir = tempdir().unwrap();\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\n                \"Content-Disposition\",\n                r#\"attachment; filename=\"foo/baz/bar\"\"#,\n            )\n            .body(\"file\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .args([\"--download\", &server.base_url()])\n        .current_dir(&dir)\n        .assert()\n        .success();\n    assert_eq!(\n        fs::read_to_string(dir.path().join(\"foo_baz_bar\")).unwrap(),\n        \"file\"\n    );\n}\n\n#[cfg(windows)]\n#[test]\nfn download_filename_with_windows_directory_traversal() {\n    let dir = tempdir().unwrap();\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\n                \"Content-Disposition\",\n                r#\"attachment; filename=\"foo\\baz\\bar\"\"#,\n            )\n            .body(\"file\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .args([\"--download\", &server.base_url()])\n        .current_dir(&dir)\n        .assert()\n        .success();\n    assert_eq!(\n        fs::read_to_string(dir.path().join(\"foo_baz_bar\")).unwrap(),\n        \"file\"\n    );\n}\n\n// TODO: test implicit download filenames\n// For this we have to pretend the output is a tty\n// This intersects with both #41 and #59\n\n#[test]\nfn it_can_resume_a_download() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[hyper::header::RANGE], \"bytes=5-\");\n\n        hyper::Response::builder()\n            .status(206)\n            .header(hyper::header::CONTENT_RANGE, \"bytes 5-11/12\")\n            .body(\" world\\n\".into())\n            .unwrap()\n    });\n\n    let dir = tempfile::tempdir().unwrap();\n    let filename = dir.path().join(\"input.txt\");\n    OpenOptions::new()\n        .create(true)\n        .truncate(true)\n        .write(true)\n        .open(&filename)\n        .unwrap()\n        .write_all(b\"Hello\")\n        .unwrap();\n\n    get_command()\n        .arg(\"--download\")\n        .arg(\"--continue\")\n        .arg(\"--output\")\n        .arg(&filename)\n        .arg(server.base_url())\n        .assert()\n        .success();\n\n    assert_eq!(fs::read_to_string(&filename).unwrap(), \"Hello world\\n\");\n}\n\n#[test]\nfn it_can_resume_a_download_with_one_byte() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[hyper::header::RANGE], \"bytes=5-\");\n\n        hyper::Response::builder()\n            .status(206)\n            .header(hyper::header::CONTENT_RANGE, \"bytes 5-5/6\")\n            .body(\"!\".into())\n            .unwrap()\n    });\n\n    let dir = tempfile::tempdir().unwrap();\n    let filename = dir.path().join(\"input.txt\");\n    OpenOptions::new()\n        .create(true)\n        .truncate(true)\n        .write(true)\n        .open(&filename)\n        .unwrap()\n        .write_all(b\"Hello\")\n        .unwrap();\n\n    get_command()\n        .arg(\"--download\")\n        .arg(\"--continue\")\n        .arg(\"--output\")\n        .arg(&filename)\n        .arg(server.base_url())\n        .assert()\n        .success();\n\n    assert_eq!(fs::read_to_string(&filename).unwrap(), \"Hello!\");\n}\n\n#[test]\nfn it_rejects_incorrect_content_range_headers() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[hyper::header::RANGE], \"bytes=5-\");\n\n        hyper::Response::builder()\n            .status(206)\n            .header(hyper::header::CONTENT_RANGE, \"bytes 6-10/11\")\n            .body(\"world\\n\".into())\n            .unwrap()\n    });\n\n    let dir = tempfile::tempdir().unwrap();\n    let filename = dir.path().join(\"input.txt\");\n    OpenOptions::new()\n        .create(true)\n        .truncate(true)\n        .write(true)\n        .open(&filename)\n        .unwrap()\n        .write_all(b\"Hello\")\n        .unwrap();\n\n    get_command()\n        .arg(\"--download\")\n        .arg(\"--continue\")\n        .arg(\"--output\")\n        .arg(&filename)\n        .arg(server.base_url())\n        .assert()\n        .failure()\n        .stderr(contains(\"Content-Range has wrong start\"));\n}\n\n#[test]\nfn it_refuses_to_combine_continue_and_range() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[hyper::header::RANGE], \"bytes=20-30\");\n\n        hyper::Response::builder()\n            .status(206)\n            .header(hyper::header::CONTENT_RANGE, \"bytes 20-30/100\")\n            .body(\"lorem ipsum\".into())\n            .unwrap()\n    });\n\n    let dir = tempfile::tempdir().unwrap();\n    let filename = dir.path().join(\"input.txt\");\n    OpenOptions::new()\n        .create(true)\n        .truncate(true)\n        .write(true)\n        .open(&filename)\n        .unwrap()\n        .write_all(b\"Hello\")\n        .unwrap();\n\n    get_command()\n        .arg(\"--download\")\n        .arg(\"--continue\")\n        .arg(\"--output\")\n        .arg(&filename)\n        .arg(server.base_url())\n        .arg(\"Range:bytes=20-30\")\n        .assert()\n        .success()\n        .stderr(contains(\"warning: --continue can't be used with\"));\n\n    assert_eq!(fs::read_to_string(&filename).unwrap(), \"lorem ipsum\");\n}\n\n#[test]\nfn error_code_416_is_ignored_when_resuming_download() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .status(416)\n            .header(hyper::header::CONTENT_TYPE, \"image/png\")\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    let tempdir = tempfile::tempdir().unwrap();\n    let filename = tempdir.path().join(\"downloaded_file.png\");\n    OpenOptions::new()\n        .create(true)\n        .truncate(true)\n        .write(true)\n        .open(&filename)\n        .unwrap()\n        .write_all(b\"Hello\")\n        .unwrap();\n\n    let download_complete_message = format!(\n        \"Download {:?} is already complete\",\n        filename.to_str().unwrap()\n    );\n\n    get_command()\n        .arg(server.base_url())\n        .arg(\"--download\")\n        .arg(\"--continue\")\n        .args([\"--output\", filename.to_str().unwrap()])\n        .assert()\n        .success()\n        .code(0)\n        .stderr(contains(\"416\"))\n        .stderr(contains(download_complete_message));\n}\n\n#[test]\nfn error_code_416_is_not_ignored_when_not_resuming_download() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .status(416)\n            .header(hyper::header::CONTENT_TYPE, \"image/png\")\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    let filename = \"downloaded_file.png\";\n    get_command()\n        .arg(server.base_url())\n        .arg(\"--download\")\n        .args([\"--output\", filename])\n        .assert()\n        .failure()\n        .code(4)\n        .stderr(contains(\"416\"));\n\n    assert_eq!(fs::exists(filename).unwrap(), false);\n}\n"
  },
  {
    "path": "tests/cases/logging.rs",
    "content": "use hyper::header::HeaderValue;\nuse predicates::str::contains;\n\nuse crate::prelude::*;\n\n#[test]\nfn logs_are_printed_in_debug_mode() {\n    get_command()\n        .arg(\"--debug\")\n        .arg(\"--offline\")\n        .arg(\":\")\n        .env_remove(\"RUST_LOG\")\n        .assert()\n        .stderr(contains(\"DEBUG xh] Cli {\"))\n        .success();\n}\n\n#[test]\nfn logs_are_not_printed_outside_debug_mode() {\n    get_command()\n        .arg(\"--offline\")\n        .arg(\":\")\n        .env_remove(\"RUST_LOG\")\n        .assert()\n        .stderr(\"\")\n        .success();\n}\n\n#[test]\nfn backtrace_is_printed_in_debug_mode() {\n    let mut server = server::http(|_req| async move {\n        panic!(\"test crash\");\n    });\n    server.disable_hit_checks();\n    get_command()\n        .arg(\"--debug\")\n        .arg(server.base_url())\n        .env_remove(\"RUST_BACKTRACE\")\n        .env_remove(\"RUST_LIB_BACKTRACE\")\n        .assert()\n        .stderr(contains(\"Stack backtrace:\"))\n        .failure();\n}\n\n#[test]\nfn backtrace_is_not_printed_outside_debug_mode() {\n    let mut server = server::http(|_req| async move {\n        panic!(\"test crash\");\n    });\n    server.disable_hit_checks();\n    let cmd = get_command()\n        .arg(server.base_url())\n        .env_remove(\"RUST_BACKTRACE\")\n        .env_remove(\"RUST_LIB_BACKTRACE\")\n        .assert()\n        .failure();\n    assert!(\n        !std::str::from_utf8(&cmd.get_output().stderr)\n            .unwrap()\n            .contains(\"Stack backtrace:\")\n    );\n}\n\n#[test]\nfn checked_status_is_printed_with_single_quiet() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .status(404)\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .args([\"--quiet\", \"--check-status\", &server.base_url()])\n        .assert()\n        .code(4)\n        .stdout(\"\")\n        .stderr(\"xh: warning: HTTP 404 Not Found\\n\");\n}\n\n#[test]\nfn checked_status_is_not_printed_with_double_quiet() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .status(404)\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .args([\"--quiet\", \"--quiet\", \"--check-status\", &server.base_url()])\n        .assert()\n        .code(4)\n        .stdout(\"\")\n        .stderr(\"\");\n}\n\n#[test]\nfn warning_for_invalid_redirect() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .status(302)\n            .header(\"location\", \"//\")\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .args([\"--follow\", &server.base_url()])\n        .assert()\n        .stderr(\"xh: warning: Redirect to invalid URL: \\\"//\\\"\\n\");\n}\n\n#[test]\nfn warning_for_non_utf8_redirect() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .status(302)\n            .header(\"location\", HeaderValue::from_bytes(b\"\\xFF\").unwrap())\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .args([\"--follow\", &server.base_url()])\n        .assert()\n        .stderr(\"xh: warning: Redirect to invalid URL: \\\"\\\\xff\\\"\\n\");\n}\n\n/// This test should fail if rustls's version gets out of sync in Cargo.toml.\n#[cfg(feature = \"rustls\")]\n#[test]\nfn rustls_emits_logs() {\n    let mut server = server::http(|_req| async move {\n        unreachable!();\n    });\n    server.disable_hit_checks();\n    let cmd = get_command()\n        .arg(\"--debug\")\n        .arg(server.base_url().replace(\"http://\", \"https://\"))\n        .env_remove(\"RUST_LOG\")\n        .assert()\n        .failure();\n\n    assert!(\n        std::str::from_utf8(&cmd.get_output().stderr)\n            .unwrap()\n            .contains(\"rustls::\")\n    );\n}\n"
  },
  {
    "path": "tests/cases/mod.rs",
    "content": "#[cfg(feature = \"http-message-signatures\")]\nmod auth_message_signature;\nmod compress_request_body;\nmod download;\nmod logging;\nmod unix_socket;\nmod xml;\n"
  },
  {
    "path": "tests/cases/unix_socket.rs",
    "content": "#[cfg(unix)]\nuse indoc::indoc;\n\nuse crate::prelude::*;\n\n#[cfg(not(unix))]\n#[test]\nfn error_on_unsupported_platform() {\n    use predicates::str::contains;\n\n    get_command()\n        .arg(\"--unix-socket=/tmp/missing.sock\")\n        .arg(\":/index.html\")\n        .assert()\n        .failure()\n        .stderr(contains(\"--unix-socket is not supported on this platform\"));\n}\n\n#[cfg(unix)]\n#[test]\nfn json_post() {\n    let server = server::http_unix(|req| async move {\n        assert_eq!(req.method(), \"POST\");\n        assert_eq!(req.headers()[\"Content-Type\"], \"application/json\");\n        assert_eq!(req.body_as_string().await, \"{\\\"foo\\\":\\\"bar\\\"}\");\n\n        hyper::Response::builder()\n            .header(hyper::header::CONTENT_TYPE, \"application/json\")\n            .body(r#\"{\"status\":\"ok\"}\"#.into())\n            .unwrap()\n    });\n\n    get_command()\n        .arg(\"--print=b\")\n        .arg(\"--pretty=format\")\n        .arg(\"post\")\n        .arg(\"http://example.com\")\n        .arg(format!(\n            \"--unix-socket={}\",\n            server.socket_path().to_string_lossy()\n        ))\n        .arg(\"foo=bar\")\n        .assert()\n        .stdout(indoc! {r#\"\n            {\n                \"status\": \"ok\"\n            }\n\n\n        \"#});\n}\n\n#[cfg(unix)]\n#[test]\nfn redirects_stay_on_same_server() {\n    let server = server::http_unix(|req| async move {\n        match req.uri().to_string().as_str() {\n            \"/first_page\" => hyper::Response::builder()\n                .status(302)\n                .header(\"Date\", \"N/A\")\n                .header(\"Location\", \"http://localhost:8000/second_page\")\n                .body(\"redirecting...\".into())\n                .unwrap(),\n            \"/second_page\" => hyper::Response::builder()\n                .status(302)\n                .header(\"Date\", \"N/A\")\n                .header(\"Location\", \"/third_page\")\n                .body(\"redirecting...\".into())\n                .unwrap(),\n            \"/third_page\" => hyper::Response::builder()\n                .header(\"Date\", \"N/A\")\n                .body(\"final destination\".into())\n                .unwrap(),\n            _ => panic!(\"unknown path\"),\n        }\n    });\n\n    get_command()\n        .arg(\"http://example.com/first_page\")\n        .arg(format!(\n            \"--unix-socket={}\",\n            server.socket_path().to_string_lossy()\n        ))\n        .arg(\"--follow\")\n        .arg(\"--verbose\")\n        .arg(\"--all\")\n        .assert()\n        .stdout(indoc! {r#\"\n            GET /first_page HTTP/1.1\n            Accept: */*\n            Accept-Encoding: gzip, deflate, br, zstd\n            Connection: keep-alive\n            Host: http.mock\n            User-Agent: xh/0.0.0 (test mode)\n\n            HTTP/1.1 302 Found\n            Content-Length: 14\n            Date: N/A\n            Location: http://localhost:8000/second_page\n\n            redirecting...\n\n            GET /second_page HTTP/1.1\n            Accept: */*\n            Accept-Encoding: gzip, deflate, br, zstd\n            Connection: keep-alive\n            Host: http.mock\n            User-Agent: xh/0.0.0 (test mode)\n\n            HTTP/1.1 302 Found\n            Content-Length: 14\n            Date: N/A\n            Location: /third_page\n\n            redirecting...\n\n            GET /third_page HTTP/1.1\n            Accept: */*\n            Accept-Encoding: gzip, deflate, br, zstd\n            Connection: keep-alive\n            Host: http.mock\n            User-Agent: xh/0.0.0 (test mode)\n\n            HTTP/1.1 200 OK\n            Content-Length: 17\n            Date: N/A\n\n            final destination\n        \"#});\n\n    server.assert_hits(3);\n}\n"
  },
  {
    "path": "tests/cases/xml.rs",
    "content": "use indoc::indoc;\n\nuse crate::prelude::*;\n\n#[test]\nfn xml_pretty_printing() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"Content-Type\", \"application/xml\")\n            .body(r#\"<?xml version=\"1.0\"?><catalog><book id=\"1\"><author>Gambardella</author><title>XML Developer Guide</title></book></catalog>\"#.into())\n            .unwrap()\n    });\n    get_command()\n        .args([\"--print=b\", &server.base_url()])\n        .assert()\n        .stdout(indoc! {r#\"\n            <?xml version=\"1.0\"?>\n            <catalog>\n              <book id=\"1\">\n                <author>Gambardella</author>\n                <title>XML Developer Guide</title>\n              </book>\n            </catalog>\n\n\n        \"#});\n}\n\n#[test]\nfn xml_pretty_printing_text_xml_content_type() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"Content-Type\", \"text/xml\")\n            .body(\"<root><a>text</a></root>\".into())\n            .unwrap()\n    });\n    get_command()\n        .args([\"--print=b\", &server.base_url()])\n        .assert()\n        .stdout(indoc! {r#\"\n            <root>\n              <a>text</a>\n            </root>\n\n\n        \"#});\n}\n\n#[test]\nfn xml_format_disabled() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"Content-Type\", \"application/xml\")\n            .body(\"<root><a>text</a></root>\".into())\n            .unwrap()\n    });\n    get_command()\n        .args([\n            \"--print=b\",\n            \"--format-options=xml.format:false\",\n            &server.base_url(),\n        ])\n        .assert()\n        .stdout(\"<root><a>text</a></root>\\n\");\n}\n\n#[test]\nfn xml_custom_indent() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"Content-Type\", \"application/xml\")\n            .body(\"<root><a>text</a></root>\".into())\n            .unwrap()\n    });\n    get_command()\n        .args([\n            \"--print=b\",\n            \"--format-options=xml.indent:6\",\n            &server.base_url(),\n        ])\n        .assert()\n        .stdout(indoc! {r#\"\n            <root>\n                  <a>text</a>\n            </root>\n\n\n        \"#});\n}\n\n#[test]\nfn xml_invalid_falls_back_gracefully() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"Content-Type\", \"application/xml\")\n            .body(\"<a><b>text</a></b>\".into())\n            .unwrap()\n    });\n    get_command()\n        .args([\"--print=b\", &server.base_url()])\n        .assert()\n        .stdout(\"<a><b>text</a></b>\\n\");\n}\n\n#[test]\nfn xml_declaration_preserved() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"Content-Type\", \"application/xml\")\n            .body(r#\"<?xml version=\"1.0\" encoding=\"UTF-8\"?><root><a>text</a></root>\"#.into())\n            .unwrap()\n    });\n    get_command()\n        .args([\"--print=b\", &server.base_url()])\n        .assert()\n        .stdout(indoc! {r#\"\n            <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n            <root>\n              <a>text</a>\n            </root>\n\n\n        \"#});\n}\n\n#[test]\nfn xml_pretty_none() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"Content-Type\", \"application/xml\")\n            .body(\"<root><a>text</a></root>\".into())\n            .unwrap()\n    });\n    get_command()\n        .args([\"--print=b\", \"--pretty=none\", &server.base_url()])\n        .assert()\n        .stdout(\"<root><a>text</a></root>\\n\");\n}\n\n#[test]\nfn xml_streaming_skips_formatting() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"Content-Type\", \"application/xml\")\n            .body(\"<root><a>text</a><b>more</b></root>\".into())\n            .unwrap()\n    });\n    get_command()\n        .args([\"--print=b\", \"--stream\", &server.base_url()])\n        .assert()\n        .stdout(\"<root><a>text</a><b>more</b></root>\\n\");\n}\n\n#[test]\nfn xml_mixed_content_preserved() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"Content-Type\", \"application/xml\")\n            .body(\"<root><p>Hello <b>world</b> end</p></root>\".into())\n            .unwrap()\n    });\n    get_command()\n        .args([\"--print=b\", &server.base_url()])\n        .assert()\n        .stdout(indoc! {r#\"\n            <root>\n              <p>Hello <b>world</b> end</p>\n            </root>\n\n\n        \"#});\n}\n\n#[test]\nfn xml_already_formatted() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"Content-Type\", \"application/xml\")\n            .body(\"<root>\\n  <a>\\n    <b>text</b>\\n  </a>\\n</root>\".into())\n            .unwrap()\n    });\n    get_command()\n        .args([\"--print=b\", &server.base_url()])\n        .assert()\n        .stdout(indoc! {r#\"\n            <root>\n              <a>\n                <b>text</b>\n              </a>\n            </root>\n\n\n        \"#});\n}\n\n#[test]\nfn xml_xhtml_content_type() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"Content-Type\", \"application/xhtml+xml\")\n            .body(\"<html><body><p>hello</p></body></html>\".into())\n            .unwrap()\n    });\n    get_command()\n        .args([\"--print=b\", &server.base_url()])\n        .assert()\n        .stdout(indoc! {r#\"\n            <html>\n              <body>\n                <p>hello</p>\n              </body>\n            </html>\n\n\n        \"#});\n}\n"
  },
  {
    "path": "tests/cli.rs",
    "content": "#![allow(clippy::bool_assert_comparison)]\n\nmod cases;\nmod server;\n\nuse std::collections::{HashMap, HashSet};\nuse std::fs::{self, File, OpenOptions};\nuse std::future::Future;\nuse std::io::Write;\nuse std::iter::FromIterator;\nuse std::net::IpAddr;\nuse std::pin::Pin;\nuse std::str::FromStr;\nuse std::time::{Duration, SystemTime, UNIX_EPOCH};\n\nuse assert_cmd::cmd::Command;\nuse http_body_util::BodyExt;\nuse indoc::indoc;\nuse predicates::function::function;\nuse predicates::str::contains;\nuse reqwest::header::HeaderValue;\nuse serde_json::Value;\nuse tempfile::{NamedTempFile, TempDir, tempdir};\n\npub trait RequestExt {\n    fn query_params(&self) -> HashMap<String, String>;\n    fn body(self) -> Pin<Box<dyn Future<Output = Vec<u8>> + Send>>;\n    fn body_as_string(self) -> Pin<Box<dyn Future<Output = String> + Send>>;\n}\n\nimpl<T> RequestExt for hyper::Request<T>\nwhere\n    T: hyper::body::Body + Send + 'static,\n    T::Data: Send,\n    T::Error: std::fmt::Debug,\n{\n    fn query_params(&self) -> HashMap<String, String> {\n        form_urlencoded::parse(self.uri().query().unwrap().as_bytes())\n            .into_owned()\n            .collect::<HashMap<String, String>>()\n    }\n\n    fn body(self) -> Pin<Box<dyn Future<Output = Vec<u8>> + Send>> {\n        let fut = async { self.collect().await.unwrap().to_bytes().to_vec() };\n        Box::pin(fut)\n    }\n\n    fn body_as_string(self) -> Pin<Box<dyn Future<Output = String> + Send>> {\n        let fut = async { String::from_utf8(self.body().await).unwrap() };\n        Box::pin(fut)\n    }\n}\n\nfn random_string() -> String {\n    use rand::Rng;\n\n    rand::thread_rng()\n        .sample_iter(&rand::distributions::Alphanumeric)\n        .take(10)\n        .map(char::from)\n        .collect()\n}\n\n/// Cargo-cross for ARM runs tests using qemu.\n///\n/// It sets an environment variable like this:\n/// CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_RUNNER=qemu-arm\nfn find_runner() -> Option<String> {\n    for (key, value) in std::env::vars() {\n        if key.starts_with(\"CARGO_TARGET_\") && key.ends_with(\"_RUNNER\") && !value.is_empty() {\n            return Some(value);\n        }\n    }\n    None\n}\n\nfn get_base_command() -> Command {\n    let mut cmd;\n    let path = assert_cmd::cargo::cargo_bin!(\"xh\");\n    if let Some(runner) = find_runner() {\n        let mut runner = runner.split_whitespace();\n        cmd = Command::new(runner.next().unwrap());\n        for arg in runner {\n            cmd.arg(arg);\n        }\n        cmd.arg(path);\n    } else {\n        cmd = Command::new(path);\n    }\n    cmd.env(\"HOME\", \"\");\n    cmd.env(\"NETRC\", \"\");\n    cmd.env(\"XH_CONFIG_DIR\", \"\");\n    #[cfg(target_os = \"windows\")]\n    cmd.env(\"XH_TEST_MODE_WIN_HOME_DIR\", \"\");\n    cmd.env(\"RUST_BACKTRACE\", \"0\");\n    cmd\n}\n\n/// Sensible default command to test with. use [`get_base_command`] if this\n/// setup doesn't apply.\nfn get_command() -> Command {\n    let mut cmd = get_base_command();\n    cmd.env(\"XH_TEST_MODE\", \"1\");\n    cmd.env(\"XH_TEST_MODE_TERM\", \"1\");\n    cmd\n}\n\n/// Do not pretend the output goes to a terminal.\nfn redirecting_command() -> Command {\n    let mut cmd = get_base_command();\n    cmd.env(\"XH_TEST_MODE\", \"1\");\n    cmd\n}\n\n/// Color output (with ANSI colors) by default.\nfn color_command() -> Command {\n    let mut cmd = get_command();\n    cmd.env(\"XH_TEST_MODE_COLOR\", \"1\");\n    cmd\n}\n\nconst BINARY_SUPPRESSOR: &str = concat!(\n    \"+-----------------------------------------+\\n\",\n    \"| NOTE: binary data not shown in terminal |\\n\",\n    \"+-----------------------------------------+\\n\",\n    \"\\n\"\n);\n\n#[allow(unused)]\nmod prelude {\n    pub(crate) use super::BINARY_SUPPRESSOR;\n    pub(crate) use super::RequestExt;\n    pub(crate) use super::color_command;\n    pub(crate) use super::get_base_command;\n    pub(crate) use super::get_command;\n    pub(crate) use super::random_string;\n    pub(crate) use super::redirecting_command;\n    pub(crate) use super::server;\n}\n\n#[test]\nfn basic_json_post() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.method(), \"POST\");\n        assert_eq!(req.headers()[\"Content-Type\"], \"application/json\");\n        assert_eq!(req.body_as_string().await, \"{\\\"name\\\":\\\"ali\\\"}\");\n\n        hyper::Response::builder()\n            .header(hyper::header::CONTENT_TYPE, \"application/json\")\n            .body(r#\"{\"got\":\"name\",\"status\":\"ok\"}\"#.into())\n            .unwrap()\n    });\n    get_command()\n        .arg(\"--print=b\")\n        .arg(\"--pretty=format\")\n        .arg(\"post\")\n        .arg(server.base_url())\n        .arg(\"name=ali\")\n        .assert()\n        .stdout(indoc! {r#\"\n            {\n                \"got\": \"name\",\n                \"status\": \"ok\"\n            }\n\n\n        \"#});\n}\n#[test]\nfn full_json_response_utf8_decode() {\n    let server = server::http(|_| async move {\n        hyper::Response::builder()\n            .header(hyper::header::CONTENT_TYPE, \"application/json\")\n            .body(r#\"{\"hello\": \"\\u4f60\\u597d\"}\"#.into())\n            .unwrap()\n    });\n\n    get_command()\n        .arg(\"--print=b\")\n        .arg(\"-S\")\n        .arg(\"--pretty=format\")\n        .arg(\"post\")\n        .arg(server.base_url())\n        .assert()\n        .stdout(indoc! {r#\"\n            {\n                \"hello\": \"\\u4f60\\u597d\"\n            }\n\n\n        \"#});\n\n    get_command()\n        .arg(\"--print=b\")\n        .arg(\"--pretty=format\")\n        .arg(\"post\")\n        .arg(server.base_url())\n        .assert()\n        .stdout(indoc! {r#\"\n            {\n                \"hello\": \"你好\"\n            }\n\n\n        \"#});\n}\n\n#[test]\nfn basic_get() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.method(), \"GET\");\n        hyper::Response::builder().body(\"foobar\\n\".into()).unwrap()\n    });\n    get_command()\n        .args([\"--print=b\", \"get\", &server.base_url()])\n        .assert()\n        .stdout(\"foobar\\n\\n\");\n}\n\n#[test]\nfn basic_head() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.method(), \"HEAD\");\n        hyper::Response::default()\n    });\n    get_command()\n        .args([\"head\", &server.base_url()])\n        .assert()\n        .success();\n}\n\n#[test]\nfn basic_options() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.method(), \"OPTIONS\");\n        hyper::Response::builder()\n            .header(\"Allow\", \"GET, HEAD, OPTIONS\")\n            .body(\"\".into())\n            .unwrap()\n    });\n    get_command()\n        .args([\"-h\", \"options\", &server.base_url()])\n        .assert()\n        .stdout(contains(\"HTTP/1.1 200 OK\"))\n        .stdout(contains(\"Allow:\"));\n}\n\n#[test]\nfn multiline_value() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.method(), \"POST\");\n        assert_eq!(req.body_as_string().await, \"foo=bar%0Abaz\");\n        hyper::Response::default()\n    });\n\n    get_command()\n        .args([\"--form\", \"post\", &server.base_url(), \"foo=bar\\nbaz\"])\n        .assert()\n        .success();\n}\n\n#[test]\nfn post_empty_body() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.method(), \"POST\");\n        assert_eq!(req.headers().get(reqwest::header::TRANSFER_ENCODING), None);\n        assert_eq!(req.body_as_string().await, \"\");\n        hyper::Response::default()\n    });\n\n    get_command()\n        .args([\"post\", &server.base_url()])\n        .assert()\n        .success();\n}\n\n#[test]\nfn nested_json() {\n    let server = server::http(|req| async move {\n        assert_eq!(\n            req.body_as_string().await,\n            r#\"{\"shallow\":\"value\",\"object\":{\"key\":\"value\"},\"array\":[1,2,3],\"wow\":{\"such\":{\"deep\":[null,null,null,{\"much\":{\"power\":{\"!\":\"Amaze\"}}}]}}}\"#\n        );\n        hyper::Response::default()\n    });\n\n    get_command()\n        .args([\"post\", &server.base_url()])\n        .arg(\"shallow=value\")\n        .arg(\"object[key]=value\")\n        .arg(\"array[]:=1\")\n        .arg(\"array[1]:=2\")\n        .arg(\"array[2]:=3\")\n        .arg(\"wow[such][deep][3][much][power][!]=Amaze\")\n        .assert()\n        .success();\n}\n\n#[test]\nfn json_path_with_escaped_characters() {\n    get_command()\n        .arg(\"--print=B\")\n        .arg(\"--offline\")\n        .arg(\":\")\n        .arg(r\"f\\=\\:\\;oo\\[\\\\[\\@]=b\\:\\:\\:ar\")\n        .assert()\n        .stdout(indoc! {r#\"\n            {\n                \"f=:;oo[\\\\\": {\n                    \"@\": \"b:::ar\"\n                }\n            }\n\n\n\n        \"#});\n}\n\n#[test]\nfn nested_json_type_error() {\n    get_command()\n        .arg(\"--print=B\")\n        .arg(\"--offline\")\n        .arg(\":\")\n        .arg(\"x[x][2]=5\")\n        .arg(\"x[x][x]=2\")\n        .assert()\n        .failure()\n        .stderr(indoc! {r#\"\n            xh: error: Can't perform 'key' based access on 'x[x]' which has a type of 'array' but this operation requires a type of 'object'.\n\n              x[x][x]\n                  ^^^\n        \"#});\n\n    get_command()\n        .arg(\"--print=B\")\n        .arg(\"--offline\")\n        .arg(\":\")\n        .arg(\"foo[x]=5\")\n        .arg(\"[][x]=2\")\n        .assert()\n        .failure()\n        .stderr(indoc! {r#\"\n            xh: error: Can't perform 'append' based access on '' which has a type of 'object' but this operation requires a type of 'array'.\n\n              [][x]\n              ^^\n        \"#});\n}\n\n#[test]\nfn json_path_special_chars_not_escaped_in_form() {\n    get_command()\n        .arg(\"--print=B\")\n        .arg(\"--offline\")\n        .arg(\"--form\")\n        .arg(\":\")\n        .arg(r\"\\]=a\")\n        .assert()\n        .stdout(indoc! {r#\"\n            %5C%5D=a\n\n        \"#});\n}\n\n#[test]\nfn header() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[\"X-Foo\"], \"Bar\");\n        hyper::Response::default()\n    });\n    get_command()\n        .args([&server.base_url(), \"x-foo:Bar\"])\n        .assert()\n        .success();\n}\n\n#[test]\nfn multiple_headers_with_same_key() {\n    let server = server::http(|req| async move {\n        let mut hello_header = req.headers().get_all(\"hello\").iter();\n        assert_eq!(hello_header.next().unwrap(), &\"world\");\n        assert_eq!(hello_header.next().unwrap(), &\"people\");\n        hyper::Response::default()\n    });\n    get_command()\n        .args([&server.base_url(), \"hello:world\", \"hello:people\"])\n        .assert()\n        .success();\n}\n\n#[test]\nfn query_param() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.query_params()[\"foo\"], \"bar\");\n        hyper::Response::default()\n    });\n    get_command()\n        .args([&server.base_url(), \"foo==bar\"])\n        .assert()\n        .success();\n}\n\n#[test]\nfn json_param() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.body_as_string().await, \"{\\\"foo\\\":[1,2,3]}\");\n        hyper::Response::default()\n    });\n    get_command()\n        .args([&server.base_url(), \"foo:=[1,2,3]\"])\n        .assert()\n        .success();\n}\n\n#[test]\nfn verbose() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[\"Connection\"], \"keep-alive\");\n        assert_eq!(req.headers()[\"Content-Type\"], \"application/json\");\n        assert_eq!(req.headers()[\"Content-Length\"], \"9\");\n        assert_eq!(req.headers()[\"User-Agent\"], \"xh/0.0.0 (test mode)\");\n        assert_eq!(req.body_as_string().await, \"{\\\"x\\\":\\\"y\\\"}\");\n        hyper::Response::builder()\n            .header(\"X-Foo\", \"Bar\")\n            .header(\"Date\", \"N/A\")\n            .body(\"a body\".into())\n            .unwrap()\n    });\n    get_command()\n        .args([\"--verbose\", &server.base_url(), \"x=y\"])\n        .assert()\n        .stdout(indoc! {r#\"\n            POST / HTTP/1.1\n            Accept: application/json, */*;q=0.5\n            Accept-Encoding: gzip, deflate, br, zstd\n            Connection: keep-alive\n            Content-Length: 9\n            Content-Type: application/json\n            Host: http.mock\n            User-Agent: xh/0.0.0 (test mode)\n\n            {\n                \"x\": \"y\"\n            }\n\n\n\n            HTTP/1.1 200 OK\n            Content-Length: 6\n            Date: N/A\n            X-Foo: Bar\n\n            a body\n        \"#});\n}\n\n#[test]\nfn decode() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"Content-Type\", \"text/plain; charset=latin1\")\n            .body(b\"\\xe9\".as_ref().into())\n            .unwrap()\n    });\n\n    get_command()\n        .args([\"--print=b\", &server.base_url()])\n        .assert()\n        .stdout(\"é\\n\");\n}\n\n#[test]\nfn streaming_decode() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"Content-Type\", \"text/plain; charset=latin1\")\n            .body(b\"\\xe9\".as_ref().into())\n            .unwrap()\n    });\n\n    get_command()\n        .args([\"--print=b\", \"--stream\", &server.base_url()])\n        .assert()\n        .stdout(\"é\\n\");\n}\n\n#[test]\nfn only_decode_for_terminal() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"Content-Type\", \"text/plain; charset=latin1\")\n            .body(b\"\\xe9\".as_ref().into())\n            .unwrap()\n    });\n\n    let output = redirecting_command()\n        .arg(server.base_url())\n        .assert()\n        .get_output()\n        .stdout\n        .clone();\n    assert_eq!(&output, b\"\\xe9\"); // .stdout() doesn't support byte slices\n}\n\n#[test]\nfn do_decode_if_formatted() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"Content-Type\", \"text/plain; charset=latin1\")\n            .body(b\"\\xe9\".as_ref().into())\n            .unwrap()\n    });\n    redirecting_command()\n        .args([\"--pretty=all\", &server.base_url()])\n        .assert()\n        .stdout(\"é\");\n}\n\n#[test]\nfn never_decode_if_binary() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            // this mimetype with a charset may actually be incoherent\n            .header(\"Content-Type\", \"application/octet-stream; charset=latin1\")\n            .body(b\"\\xe9\".as_ref().into())\n            .unwrap()\n    });\n\n    let output = redirecting_command()\n        .args([\"--pretty=all\", &server.base_url()])\n        .assert()\n        .get_output()\n        .stdout\n        .clone();\n    assert_eq!(&output, b\"\\xe9\");\n}\n\n#[test]\nfn binary_detection() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .body(b\"foo\\0bar\".as_ref().into())\n            .unwrap()\n    });\n\n    get_command()\n        .args([\"--print=b\", &server.base_url()])\n        .assert()\n        .stdout(BINARY_SUPPRESSOR);\n}\n\n#[test]\nfn streaming_binary_detection() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .body(b\"foo\\0bar\".as_ref().into())\n            .unwrap()\n    });\n\n    get_command()\n        .args([\"--print=b\", \"--stream\", &server.base_url()])\n        .assert()\n        .stdout(BINARY_SUPPRESSOR);\n}\n\n#[test]\nfn request_binary_detection() {\n    redirecting_command()\n        .args([\"--print=B\", \"--offline\", \":\"])\n        .write_stdin(b\"foo\\0bar\".as_ref())\n        .assert()\n        .stdout(indoc! {r#\"\n            +-----------------------------------------+\n            | NOTE: binary data not shown in terminal |\n            +-----------------------------------------+\n\n\n        \"#});\n}\n\n#[test]\nfn timeout() {\n    let mut server = server::http(|_req| async move {\n        tokio::time::sleep(Duration::from_secs_f32(0.5)).await;\n        hyper::Response::default()\n    });\n    server.disable_hit_checks();\n\n    get_command()\n        .args([\"--timeout=0.1\", &server.base_url()])\n        .assert()\n        .code(2)\n        .stderr(contains(\"operation timed out\"));\n}\n\n#[test]\nfn timeout_no_limit() {\n    let server = server::http(|_req| async move {\n        tokio::time::sleep(Duration::from_secs_f32(0.5)).await;\n        hyper::Response::default()\n    });\n\n    get_command()\n        .args([\"--timeout=0\", &server.base_url()])\n        .assert()\n        .success();\n}\n\n#[test]\nfn timeout_invalid() {\n    get_command()\n        .args([\"--timeout=-0.01\", \"--offline\", \":\"])\n        .assert()\n        .failure()\n        .stderr(contains(\"Connection timeout is negative\"));\n\n    get_command()\n        .args([\"--timeout=18446744073709552000\", \"--offline\", \":\"])\n        .assert()\n        .failure()\n        .stderr(contains(\"Connection timeout is too big\"));\n\n    get_command()\n        .args([\"--timeout=inf\", \"--offline\", \":\"])\n        .assert()\n        .failure()\n        .stderr(contains(\"Connection timeout is too big\"));\n\n    get_command()\n        .args([\"--timeout=NaN\", \"--offline\", \":\"])\n        .assert()\n        .failure()\n        .stderr(contains(\"Connection timeout is not a valid number\"));\n\n    get_command()\n        .args([\"--timeout=SEC\", \"--offline\", \":\"])\n        .assert()\n        .failure()\n        .stderr(contains(\"Connection timeout is not a valid number\"));\n}\n\n#[test]\nfn check_status() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .status(404)\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .args([\"--check-status\", &server.base_url()])\n        .assert()\n        .code(4)\n        .stderr(\"\");\n}\n\n#[test]\nfn check_status_warning() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .status(501)\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    redirecting_command()\n        .args([\"--check-status\", &server.base_url()])\n        .assert()\n        .code(5)\n        .stderr(\"xh: warning: HTTP 501 Not Implemented\\n\");\n}\n\n#[test]\nfn check_status_is_implied() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .status(404)\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .arg(server.base_url())\n        .assert()\n        .code(4)\n        .stderr(\"\");\n}\n\n#[test]\nfn check_status_is_not_implied_in_compat_mode() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .status(404)\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .env(\"XH_HTTPIE_COMPAT_MODE\", \"\")\n        .arg(server.base_url())\n        .assert()\n        .code(0);\n}\n\n#[test]\nfn user_password_auth() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[\"Authorization\"], \"Basic dXNlcjpwYXNz\");\n        hyper::Response::default()\n    });\n\n    get_command()\n        .args([\"--auth=user:pass\", &server.base_url()])\n        .assert()\n        .success();\n}\n\n#[test]\nfn user_auth() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[\"Authorization\"], \"Basic dXNlcjo=\");\n        hyper::Response::default()\n    });\n\n    get_command()\n        .args([\"--auth=user:\", &server.base_url()])\n        .assert()\n        .success();\n}\n\n#[test]\nfn bearer_auth() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[\"Authorization\"], \"Bearer SomeToken\");\n        hyper::Response::default()\n    });\n\n    get_command()\n        .args([\"--bearer=SomeToken\", &server.base_url()])\n        .assert()\n        .success();\n}\n\n#[test]\nfn digest_auth() {\n    let server = server::http(|req| async move {\n        if req.headers().get(\"Authorization\").is_none() {\n            hyper::Response::builder()\n                .status(401)\n                .header(\"WWW-Authenticate\", r#\"Digest realm=\"me@xh.com\", nonce=\"e5051361f053723a807674177fc7022f\", qop=\"auth, auth-int\", opaque=\"9dcf562038f1ec1c8d02f218ef0e7a4b\", algorithm=MD5, stale=FALSE\"#)\n                .body(\"\".into())\n                .unwrap()\n        } else {\n            hyper::Response::builder()\n                .body(\"authenticated\".into())\n                .unwrap()\n        }\n    });\n\n    get_command()\n        .arg(\"--auth-type=digest\")\n        .arg(\"--auth=ahmed:12345\")\n        .arg(server.base_url())\n        .assert()\n        .stdout(contains(\"HTTP/1.1 200 OK\"));\n\n    server.assert_hits(2);\n}\n\n#[cfg(feature = \"online-tests\")]\n#[test]\nfn successful_digest_auth() {\n    get_command()\n        .arg(\"--auth-type=digest\")\n        .arg(\"--auth=ahmed:12345\")\n        .arg(\"httpbingo.org/digest-auth/auth/ahmed/12345\")\n        .assert()\n        .stdout(contains(\"HTTP/1.1 200 OK\"));\n}\n\n#[cfg(feature = \"online-tests\")]\n#[test]\nfn compress_request_body_online() {\n    get_command()\n        .arg(\"https://postman-echo.com/post\")\n        .args([\"--body\", \"-f\", &format!(\"a={}\", \"1\".repeat(1000))])\n        .assert()\n        .stdout(function(|body: &str| {\n            let json: Value = serde_json::from_str(body).unwrap();\n            assert_eq!(json[\"json\"][\"a\"], Value::String(\"1\".repeat(1000)));\n            let length: i32 = json[\"headers\"][\"content-length\"]\n                .as_str()\n                .unwrap()\n                .parse()\n                .unwrap();\n            assert_eq!(length, 1002);\n\n            true\n        }));\n    get_command()\n        .arg(\"https://postman-echo.com/post\")\n        .args([\"-x\", \"--body\", \"-f\", &format!(\"a={}\", \"1\".repeat(1000))])\n        .assert()\n        .stdout(function(|body: &str| {\n            let json: Value = serde_json::from_str(body).unwrap();\n            assert_eq!(json[\"json\"][\"a\"], Value::String(\"1\".repeat(1000)));\n            let length: i32 = json[\"headers\"][\"content-length\"]\n                .as_str()\n                .unwrap()\n                .parse()\n                .unwrap();\n            assert!(length < 1000);\n\n            true\n        }));\n}\n\n#[cfg(feature = \"online-tests\")]\n#[test]\nfn unsuccessful_digest_auth() {\n    get_command()\n        .arg(\"--auth-type=digest\")\n        .arg(\"--auth=ahmed:wrongpass\")\n        .arg(\"httpbingo.org/digest-auth/auth/ahmed/12345\")\n        .assert()\n        .stdout(contains(\"HTTP/1.1 401 Unauthorized\"));\n}\n\n#[test]\nfn digest_auth_with_redirection() {\n    let server = server::http(|req| async move {\n        match req.uri().path() {\n            \"/login_page\" => {\n                if req.headers().get(\"Authorization\").is_none() {\n                    hyper::Response::builder()\n                        .status(401)\n                        .header(\"WWW-Authenticate\", r#\"Digest realm=\"me@xh.com\", nonce=\"e5051361f053723a807674177fc7022f\", qop=\"auth, auth-int\", opaque=\"9dcf562038f1ec1c8d02f218ef0e7a4b\", algorithm=MD5, stale=FALSE\"#)\n                        .header(\"date\", \"N/A\")\n                        .body(\"\".into())\n                        .unwrap()\n                } else {\n                    hyper::Response::builder()\n                        .status(302)\n                        .header(\"location\", \"/admin_page\")\n                        .header(\"date\", \"N/A\")\n                        .body(\"authentication successful, redirecting...\".into())\n                        .unwrap()\n                }\n            }\n            \"/admin_page\" => {\n                if req.headers().get(\"Authorization\").is_none() {\n                    hyper::Response::builder()\n                        .header(\"date\", \"N/A\")\n                        .body(\"admin page\".into())\n                        .unwrap()\n                } else {\n                    hyper::Response::builder()\n                        .status(401)\n                        .body(\"unauthorized\".into())\n                        .unwrap()\n                }\n            }\n            _ => panic!(\"unknown path\"),\n        }\n    });\n\n    get_command()\n        .env(\"XH_TEST_DIGEST_AUTH_CNONCE\", \"f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ\")\n        .arg(\"--auth-type=digest\")\n        .arg(\"--auth=ahmed:12345\")\n        .arg(\"--follow\")\n        .arg(\"--verbose\")\n        .arg(server.url(\"/login_page\"))\n        .assert()\n        .stdout(indoc! {r#\"\n            GET /login_page HTTP/1.1\n            Accept: */*\n            Accept-Encoding: gzip, deflate, br, zstd\n            Connection: keep-alive\n            Host: http.mock\n            User-Agent: xh/0.0.0 (test mode)\n\n            HTTP/1.1 401 Unauthorized\n            Content-Length: 0\n            Date: N/A\n            Www-Authenticate: Digest realm=\"me@xh.com\", nonce=\"e5051361f053723a807674177fc7022f\", qop=\"auth, auth-int\", opaque=\"9dcf562038f1ec1c8d02f218ef0e7a4b\", algorithm=MD5, stale=FALSE\n\n\n\n            GET /login_page HTTP/1.1\n            Accept: */*\n            Accept-Encoding: gzip, deflate, br, zstd\n            Authorization: Digest username=\"ahmed\", realm=\"me@xh.com\", nonce=\"e5051361f053723a807674177fc7022f\", uri=\"/login_page\", qop=auth, nc=00000001, cnonce=\"f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ\", response=\"894fd5ee1dcc702df7e4a6abed37fd56\", opaque=\"9dcf562038f1ec1c8d02f218ef0e7a4b\", algorithm=MD5\n            Connection: keep-alive\n            Host: http.mock\n            User-Agent: xh/0.0.0 (test mode)\n\n            HTTP/1.1 302 Found\n            Content-Length: 41\n            Date: N/A\n            Location: /admin_page\n\n            authentication successful, redirecting...\n\n            GET /admin_page HTTP/1.1\n            Accept: */*\n            Accept-Encoding: gzip, deflate, br, zstd\n            Connection: keep-alive\n            Host: http.mock\n            User-Agent: xh/0.0.0 (test mode)\n\n            HTTP/1.1 200 OK\n            Content-Length: 10\n            Date: N/A\n\n            admin page\n        \"#});\n\n    server.assert_hits(3);\n}\n\n#[test]\nfn netrc_env_user_password_auth() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[\"Authorization\"], \"Basic dXNlcjpwYXNz\");\n        hyper::Response::default()\n    });\n\n    let mut netrc = NamedTempFile::new().unwrap();\n    writeln!(\n        netrc,\n        \"machine {}\\nlogin user\\npassword pass\",\n        server.host()\n    )\n    .unwrap();\n\n    get_command()\n        .env(\"NETRC\", netrc.path())\n        .arg(server.base_url())\n        .assert()\n        .success();\n}\n\n#[test]\nfn netrc_env_no_bearer_auth_unless_specified() {\n    // Test that we don't pass an authorization header if the .netrc contains no username,\n    // and the --auth-type=bearer flag isn't explicitly specified.\n    let server = server::http(|req| async move {\n        assert!(req.headers().get(\"Authorization\").is_none());\n        hyper::Response::default()\n    });\n\n    let mut netrc = NamedTempFile::new().unwrap();\n    writeln!(netrc, \"machine {}\\npassword pass\", server.host()).unwrap();\n\n    get_command()\n        .env(\"NETRC\", netrc.path())\n        .arg(server.base_url())\n        .assert()\n        .success();\n}\n\n#[test]\nfn netrc_env_auth_type_bearer() {\n    // If we're using --auth-type=bearer, test that it's properly sent with a .netrc that\n    // contains only a password and no username.\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[\"Authorization\"], \"Bearer pass\");\n        hyper::Response::default()\n    });\n\n    let mut netrc = NamedTempFile::new().unwrap();\n    writeln!(netrc, \"machine {}\\npassword pass\", server.host()).unwrap();\n\n    get_command()\n        .env(\"NETRC\", netrc.path())\n        .arg(server.base_url())\n        .arg(\"--auth-type=bearer\")\n        .assert()\n        .success();\n}\n\n#[test]\nfn netrc_file_user_password_auth() {\n    for netrc_file in [\".netrc\", \"_netrc\"] {\n        let server = server::http(|req| async move {\n            assert_eq!(req.headers()[\"Authorization\"], \"Basic dXNlcjpwYXNz\");\n            hyper::Response::default()\n        });\n\n        let homedir = TempDir::new().unwrap();\n        let netrc_path = homedir.path().join(netrc_file);\n        let mut netrc = File::create(netrc_path).unwrap();\n        writeln!(\n            netrc,\n            \"machine {}\\nlogin user\\npassword pass\",\n            server.host()\n        )\n        .unwrap();\n\n        netrc.flush().unwrap();\n\n        get_command()\n            .env(\"HOME\", homedir.path())\n            .env(\"XH_TEST_MODE_WIN_HOME_DIR\", homedir.path())\n            .env_remove(\"NETRC\")\n            .arg(server.base_url())\n            .assert()\n            .success();\n\n        drop(netrc);\n        homedir.close().unwrap();\n    }\n}\n\nfn get_proxy_command(\n    protocol_to_request: &str,\n    protocol_to_proxy: &str,\n    proxy_url: &str,\n) -> Command {\n    let mut cmd = get_command();\n    cmd.arg(\"--check-status\")\n        .arg(format!(\"--proxy={protocol_to_proxy}:{proxy_url}\"))\n        .arg(\"GET\")\n        .arg(format!(\"{protocol_to_request}://example.test/get\"));\n    cmd\n}\n\n#[test]\nfn proxy_http_proxy() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.method(), \"GET\");\n        assert_eq!(req.headers()[\"host\"], \"example.test\");\n        hyper::Response::default()\n    });\n\n    get_proxy_command(\"http\", \"http\", &server.base_url())\n        .assert()\n        .success();\n}\n\n#[test]\nfn proxy_https_proxy() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.method(), \"CONNECT\");\n        hyper::Response::builder()\n            .status(502)\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    get_proxy_command(\"https\", \"https\", &server.base_url())\n        .assert()\n        .stderr(contains(\"tunnel error: unsuccessful\"))\n        .failure();\n}\n\n#[test]\nfn proxy_http_all_proxy() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.method(), \"GET\");\n        hyper::Response::builder()\n            .status(502)\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    get_proxy_command(\"http\", \"all\", &server.base_url())\n        .assert()\n        .stdout(contains(\"HTTP/1.1 502 Bad Gateway\"))\n        .failure();\n}\n\n#[test]\nfn proxy_https_all_proxy() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.method(), \"CONNECT\");\n        hyper::Response::builder()\n            .status(502)\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    get_proxy_command(\"https\", \"all\", &server.base_url())\n        .assert()\n        .stderr(contains(\" tunnel error: unsuccessful\"))\n        .failure();\n}\n\n#[test]\nfn last_supplied_proxy_wins() {\n    let mut first_server = server::http(|req| async move {\n        assert_eq!(req.headers()[\"host\"], \"example.test\");\n        hyper::Response::builder()\n            .status(500)\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    let second_server = server::http(|req| async move {\n        assert_eq!(req.headers()[\"host\"], \"example.test\");\n        hyper::Response::builder()\n            .status(200)\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    let mut cmd = get_command();\n    cmd.args([\n        format!(\"--proxy=http:{}\", first_server.base_url()).as_str(),\n        format!(\"--proxy=http:{}\", second_server.base_url()).as_str(),\n        \"GET\",\n        \"http://example.test\",\n    ])\n    .assert()\n    .success();\n\n    first_server.disable_hit_checks();\n    first_server.assert_hits(0);\n    second_server.assert_hits(1);\n}\n\n#[test]\nfn proxy_multiple_valid_proxies() {\n    let mut cmd = get_command();\n    cmd.arg(\"--offline\")\n        .arg(\"--proxy=http:https://127.0.0.1:8000\")\n        .arg(\"--proxy=https:socks5://127.0.0.1:8000\")\n        .arg(\"--proxy=all:http://127.0.0.1:8000\")\n        .arg(\"GET\")\n        .arg(\"http://httpbingo.org/get\");\n\n    cmd.assert().success();\n}\n\n// temporarily disabled for builds not using rustls\n#[cfg(all(feature = \"online-tests\", feature = \"rustls\"))]\n#[test]\nfn verify_default_yes() {\n    use predicates::boolean::PredicateBooleanExt;\n\n    get_command()\n        .args([\"-v\", \"https://self-signed.badssl.com\"])\n        .assert()\n        .failure()\n        .stdout(contains(\"GET / HTTP/1.1\"))\n        // rustls or native-tls\n        .stderr(\n            contains(\"UnknownIssuer\")\n                .or(contains(\"certificate is not trusted\"))\n                .or(contains(\"certificate was not trusted\")),\n        );\n}\n\n// temporarily disabled for builds not using rustls\n#[cfg(all(feature = \"online-tests\", feature = \"rustls\"))]\n#[test]\nfn verify_explicit_yes() {\n    use predicates::boolean::PredicateBooleanExt;\n\n    get_command()\n        .args([\"-v\", \"--verify=yes\", \"https://self-signed.badssl.com\"])\n        .assert()\n        .failure()\n        .stdout(contains(\"GET / HTTP/1.1\"))\n        // rustls or native-tls\n        .stderr(\n            contains(\"UnknownIssuer\")\n                .or(contains(\"certificate is not trusted\"))\n                .or(contains(\"certificate was not trusted\")),\n        );\n}\n\n#[cfg(feature = \"online-tests\")]\n#[test]\nfn verify_no() {\n    get_command()\n        .args([\"-v\", \"--verify=no\", \"https://self-signed.badssl.com\"])\n        .assert()\n        .stdout(contains(\"GET / HTTP/1.1\"))\n        .stdout(contains(\"HTTP/1.1 200 OK\"))\n        .stderr(predicates::str::is_empty());\n}\n\n#[cfg(all(feature = \"rustls\", feature = \"online-tests\"))]\n#[test]\nfn verify_valid_file() {\n    get_command()\n        .arg(\"-v\")\n        .arg(\"--verify=tests/fixtures/certs/wildcard-self-signed.pem\")\n        .arg(\"https://self-signed.badssl.com\")\n        .assert()\n        .stdout(contains(\"GET / HTTP/1.1\"))\n        .stdout(contains(\"HTTP/1.1 200 OK\"))\n        .stderr(predicates::str::is_empty());\n}\n\n// This test may fail if https://github.com/seanmonstar/reqwest/issues/1260 is fixed\n// If that happens make sure to remove the warning, not just this test\n#[cfg(all(feature = \"native-tls\", feature = \"online-tests\"))]\n#[test]\nfn verify_valid_file_native_tls() {\n    get_command()\n        .arg(\"--native-tls\")\n        .arg(\"--verify=tests/fixtures/certs/wildcard-self-signed.pem\")\n        .arg(\"https://self-signed.badssl.com\")\n        .assert()\n        .stderr(contains(\"Custom CA bundles with native-tls are broken\"));\n}\n\n#[cfg(feature = \"online-tests\")]\n#[test]\nfn cert_without_key() {\n    get_command()\n        .args([\"-v\", \"https://client.badssl.com\"])\n        .assert()\n        .stdout(contains(\"400 No required SSL certificate was sent\"))\n        .stderr(predicates::str::is_empty());\n}\n\n// disabled for macos since it errors with: Other(OtherError(\"'*.badssl.com' certificate is expired: -67818\"))\n// disabled for windows since it errors with: Expired\n#[cfg(all(\n    feature = \"rustls\",\n    feature = \"online-tests\",\n    not(target_os = \"macos\"),\n    not(target_os = \"windows\")\n))]\n#[test]\nfn formatted_certificate_expired_message() {\n    get_command()\n        .arg(\"https://expired.badssl.com\")\n        .assert()\n        .failure()\n        .stderr(contains(\"Certificate not valid after 2015-04-12\"));\n}\n\n#[test]\nfn override_dns_resolution() {\n    let server = server::http(|req| async move {\n        let host = req.headers()[\"host\"].to_str().unwrap();\n        assert!(host.starts_with(\"example.com\"));\n\n        hyper::Response::builder()\n            .header(\"X-Foo\", \"Bar\")\n            .header(\"Date\", \"N/A\")\n            .header(\"Content-Type\", \"application/json\")\n            .body(r#\"{\"hello\":\"world\"}\"#.into())\n            .unwrap()\n    });\n\n    get_command()\n        .arg(\"--body\")\n        .arg(\"--resolve=example.com:127.0.0.1\")\n        .arg(format!(\"http://example.com:{}\", server.port()))\n        .assert()\n        .stdout(indoc! {r#\"\n            {\n                \"hello\": \"world\"\n            }\n\n\n        \"#});\n}\n\n#[cfg(feature = \"online-tests\")]\n#[test]\nfn use_ipv4() {\n    get_command()\n        .args([\"https://api64.ipify.org\", \"--body\", \"--ipv4\"])\n        .assert()\n        .stdout(function(|output: &str| {\n            IpAddr::from_str(output.trim()).unwrap().is_ipv4()\n        }))\n        .stderr(predicates::str::is_empty());\n}\n\n// real use ipv6\n#[cfg(all(feature = \"ipv6-tests\", feature = \"online-tests\"))]\n#[test]\nfn use_ipv6() {\n    get_command()\n        .args([\"https://api64.ipify.org\", \"--body\", \"--ipv6\"])\n        .assert()\n        .stdout(function(|output: &str| {\n            IpAddr::from_str(output.trim()).unwrap().is_ipv6()\n        }))\n        .stderr(predicates::str::is_empty());\n}\n\n#[cfg(feature = \"online-tests\")]\n#[ignore = \"certificate expired (I think)\"]\n#[test]\nfn cert_with_key() {\n    get_command()\n        .arg(\"-v\")\n        .arg(\"--cert=tests/fixtures/certs/client.badssl.com.crt\")\n        .arg(\"--cert-key=tests/fixtures/certs/client.badssl.com.key\")\n        .arg(\"https://client.badssl.com\")\n        .assert()\n        .stdout(contains(\"HTTP/1.1 200 OK\"))\n        .stdout(contains(\"client-authenticated\"))\n        .stderr(predicates::str::is_empty());\n}\n\n#[cfg(all(feature = \"native-tls\", feature = \"online-tests\"))]\n#[test]\nfn cert_with_key_native_tls() {\n    get_command()\n        .arg(\"--native-tls\")\n        .arg(\"--cert=tests/fixtures/certs/client.badssl.com.crt\")\n        .arg(\"--cert-key=tests/fixtures/certs/client.badssl.com.key\")\n        .arg(\"https://client.badssl.com\")\n        .assert()\n        .failure()\n        .stderr(contains(\n            \"Client certificates are not supported for native-tls\",\n        ));\n}\n\n#[cfg(not(feature = \"native-tls\"))]\n#[test]\nfn native_tls_flag_disabled() {\n    get_command()\n        .args([\"--native-tls\", \":\"])\n        .assert()\n        .failure()\n        .stderr(contains(\"built without native-tls support\"));\n}\n\n#[cfg(all(feature = \"native-tls\", feature = \"online-tests\"))]\n#[test]\nfn native_tls_works() {\n    get_command()\n        .args([\"--native-tls\", \"https://example.org\"])\n        .assert()\n        .success();\n}\n\n#[cfg(feature = \"online-tests\")]\n#[test]\nfn good_tls_version() {\n    get_command()\n        .arg(\"--ssl=tls1.2\")\n        .arg(\"https://tls-v1-2.badssl.com:1012/\")\n        .assert()\n        .success();\n}\n\n#[cfg(all(feature = \"native-tls\", feature = \"online-tests\"))]\n#[test]\nfn good_tls_version_nativetls() {\n    get_command()\n        .arg(\"--ssl=tls1.2\")\n        .arg(\"--native-tls\")\n        .arg(\"https://tls-v1-2.badssl.com:1012/\")\n        .assert()\n        .success();\n}\n\n#[test]\nfn bad_tls_version() {\n    get_command()\n        .arg(\"--ssl=tls1.3\")\n        .arg(\"https://tls-v1-2.badssl.com:1012/\")\n        .assert()\n        .failure();\n}\n\n#[cfg(feature = \"native-tls\")]\n#[test]\nfn bad_tls_version_nativetls() {\n    get_command()\n        .arg(\"--ssl=tls1.1\")\n        .arg(\"--native-tls\")\n        .arg(\"https://tls-v1-2.badssl.com:1012/\")\n        .assert()\n        .failure();\n}\n\n#[cfg(feature = \"native-tls\")]\n#[test]\nfn unsupported_tls_version_nativetls() {\n    get_command()\n        .arg(\"--ssl=tls1.3\")\n        .arg(\"--native-tls\")\n        .arg(\"https://example.org\")\n        .assert()\n        .failure()\n        .stderr(contains(\"invalid minimum TLS version\"))\n        .stderr(contains(\"running without the --native-tls\"));\n}\n\n#[cfg(feature = \"rustls\")]\n#[test]\nfn unsupported_tls_version_rustls() {\n    #[cfg(feature = \"native-tls\")]\n    const MSG: &str = \"native-tls will be enabled\";\n    #[cfg(not(feature = \"native-tls\"))]\n    const MSG: &str = \"Consider building with the `native-tls` feature enabled\";\n\n    get_command()\n        .arg(\"--offline\")\n        .arg(\"--ssl=tls1.1\")\n        .arg(\":\")\n        .assert()\n        .stderr(contains(\"rustls does not support older TLS versions\"))\n        .stderr(contains(MSG));\n}\n\n#[test]\nfn forced_json() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[\"content-type\"], \"application/json\");\n        assert_eq!(req.headers()[\"accept\"], \"application/json, */*;q=0.5\");\n        hyper::Response::default()\n    });\n\n    get_command()\n        .args([\"--json\", &server.base_url()])\n        .assert()\n        .success();\n}\n\n#[test]\nfn forced_form() {\n    let server = server::http(|req| async move {\n        assert_eq!(\n            req.headers()[\"content-type\"],\n            \"application/x-www-form-urlencoded\"\n        );\n        hyper::Response::default()\n    });\n    get_command()\n        .args([\"--form\", &server.base_url()])\n        .assert()\n        .success();\n}\n\n#[test]\nfn forced_multipart() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.method(), \"POST\");\n        assert_eq!(req.headers().get(\"content-type\").is_some(), true);\n        assert_eq!(req.body_as_string().await, \"\");\n        hyper::Response::default()\n    });\n    get_command()\n        .args([\"--multipart\", &server.base_url()])\n        .assert()\n        .success();\n}\n\n#[test]\nfn formatted_json_output() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"content-type\", \"application/json\")\n            .body(r#\"{\"\":0}\"#.into())\n            .unwrap()\n    });\n    get_command()\n        .args([\"--print=b\", &server.base_url()])\n        .assert()\n        .stdout(indoc! {r#\"\n            {\n                \"\": 0\n            }\n\n\n        \"#});\n}\n\n#[test]\nfn inferred_json_output() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"content-type\", \"text/plain\")\n            .body(r#\"{\"\":0}\"#.into())\n            .unwrap()\n    });\n    get_command()\n        .args([\"--print=b\", &server.base_url()])\n        .assert()\n        .stdout(indoc! {r#\"\n            {\n                \"\": 0\n            }\n\n\n        \"#});\n}\n\n#[test]\nfn inferred_json_javascript_output() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"content-type\", \"application/javascript\")\n            .body(r#\"{\"\":0}\"#.into())\n            .unwrap()\n    });\n    get_command()\n        .args([\"--print=b\", &server.base_url()])\n        .assert()\n        .stdout(indoc! {r#\"\n            {\n                \"\": 0\n            }\n\n\n        \"#});\n}\n\n#[test]\nfn inferred_nonjson_output() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"content-type\", \"text/plain\")\n            // Trailing comma makes it invalid JSON, though formatting would still work\n            .body(r#\"{\"\":0,}\"#.into())\n            .unwrap()\n    });\n    get_command()\n        .args([\"--print=b\", &server.base_url()])\n        .assert()\n        .stdout(indoc! {r#\"\n            {\"\":0,}\n        \"#});\n}\n\n#[test]\nfn noninferred_json_output() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            // Valid JSON, but not declared as text\n            .header(\"content-type\", \"application/octet-stream\")\n            .body(r#\"{\"\":0}\"#.into())\n            .unwrap()\n    });\n    get_command()\n        .args([\"--print=b\", &server.base_url()])\n        .assert()\n        .stdout(indoc! {r#\"\n            {\"\":0}\n        \"#});\n}\n\n#[test]\nfn empty_body_defaults_to_get() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.method(), \"GET\");\n        assert_eq!(req.body_as_string().await, \"\");\n        hyper::Response::default()\n    });\n\n    get_command().arg(server.base_url()).assert().success();\n}\n\n#[test]\nfn non_empty_body_defaults_to_post() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.method(), \"POST\");\n        assert_eq!(req.body_as_string().await, \"{\\\"x\\\":4}\");\n        hyper::Response::default()\n    });\n\n    get_command()\n        .args([&server.base_url(), \"x:=4\"])\n        .assert()\n        .success();\n}\n\n#[test]\nfn empty_raw_body_defaults_to_post() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.method(), \"POST\");\n        assert_eq!(req.body_as_string().await, \"\");\n        hyper::Response::default()\n    });\n\n    redirecting_command()\n        .arg(server.base_url())\n        .write_stdin(\"\")\n        .assert()\n        .success();\n}\n\n#[test]\nfn body_from_stdin() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.body_as_string().await, \"body from stdin\");\n        hyper::Response::default()\n    });\n\n    redirecting_command()\n        .arg(server.base_url())\n        .write_stdin(\"body from stdin\")\n        .assert()\n        .success();\n}\n\n#[test]\nfn body_from_raw() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.body_as_string().await, \"body from raw\");\n        hyper::Response::default()\n    });\n\n    get_command()\n        .args([\"--raw=body from raw\", &server.base_url()])\n        .assert()\n        .success();\n}\n\n#[test]\nfn support_utf8_header_value() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[\"hello\"].as_bytes(), \"你好\".as_bytes());\n        hyper::Response::builder()\n            .header(\"hello\", \"你好呀\")\n            .header(\"Date\", \"N/A\")\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .args([&server.base_url(), \"hello:你好\"])\n        .assert()\n        .stdout(indoc! {r#\"\n        HTTP/1.1 200 OK\n        Content-Length: 0\n        Date: N/A\n        Hello: ä½ å¥½å�� (UTF-8: 你好呀)\n\n\n        \"#})\n        .success();\n}\n\n#[test]\nfn support_latin1_header_value() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"hello\", HeaderValue::from_bytes(b\"R\\xF3dos\").unwrap())\n            .header(\"Date\", \"N/A\")\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .arg(server.base_url())\n        .assert()\n        .stdout(indoc! {r#\"\n        HTTP/1.1 200 OK\n        Content-Length: 0\n        Date: N/A\n        Hello: Ródos\n\n\n        \"#})\n        .success();\n}\n\n#[test]\nfn redirect_support_utf8_location() {\n    let server = server::http(|req| async move {\n        match req.uri().path() {\n            \"/first_page\" => hyper::Response::builder()\n                .status(302)\n                .header(\"Date\", \"N/A\")\n                .header(\"Location\", \"/page二\")\n                .body(\"redirecting...\".into())\n                .unwrap(),\n            \"/page%E4%BA%8C\" => hyper::Response::builder()\n                .header(\"Date\", \"N/A\")\n                .body(\"final destination\".into())\n                .unwrap(),\n            _ => panic!(\"unknown path\"),\n        }\n    });\n\n    get_command()\n        .args([&server.url(\"/first_page\"), \"--follow\", \"--verbose\", \"--all\"])\n        .assert()\n        .stdout(indoc! {r#\"\n            GET /first_page HTTP/1.1\n            Accept: */*\n            Accept-Encoding: gzip, deflate, br, zstd\n            Connection: keep-alive\n            Host: http.mock\n            User-Agent: xh/0.0.0 (test mode)\n\n            HTTP/1.1 302 Found\n            Content-Length: 14\n            Date: N/A\n            Location: /pageäº� (UTF-8: /page二)\n\n            redirecting...\n\n            GET /page%E4%BA%8C HTTP/1.1\n            Accept: */*\n            Accept-Encoding: gzip, deflate, br, zstd\n            Connection: keep-alive\n            Host: http.mock\n            User-Agent: xh/0.0.0 (test mode)\n\n            HTTP/1.1 200 OK\n            Content-Length: 17\n            Date: N/A\n\n            final destination\n        \"#});\n}\n\n#[test]\nfn mixed_stdin_request_items() {\n    redirecting_command()\n        .args([\"--offline\", \":\", \"x=3\"])\n        .write_stdin(\"\")\n        .assert()\n        .failure()\n        .stderr(contains(\n            \"Request body (from stdin) and request data (key=value) cannot be mixed\",\n        ));\n}\n\n#[test]\nfn mixed_stdin_raw() {\n    redirecting_command()\n        .args([\"--offline\", \"--raw=hello\", \":\"])\n        .write_stdin(\"\")\n        .assert()\n        .failure()\n        .stderr(contains(\n            \"Request body from stdin and --raw cannot be mixed\",\n        ));\n}\n\n#[test]\nfn mixed_raw_request_items() {\n    get_command()\n        .args([\"--offline\", \"--raw=hello\", \":\", \"x=3\"])\n        .assert()\n        .failure()\n        .stderr(contains(\n            \"Request body (from --raw) and request data (key=value) cannot be mixed\",\n        ));\n}\n\n#[test]\nfn multipart_stdin() {\n    redirecting_command()\n        .args([\"--offline\", \"--multipart\", \":\"])\n        .write_stdin(\"\")\n        .assert()\n        .failure()\n        .stderr(contains(\"Cannot build a multipart request body from stdin\"));\n}\n\n#[test]\nfn multipart_raw() {\n    get_command()\n        .args([\"--offline\", \"--raw=hello\", \"--multipart\", \":\"])\n        .assert()\n        .failure()\n        .stderr(contains(\"'--raw <RAW>' cannot be used with '--multipart'\"));\n}\n\n#[test]\nfn default_json_for_raw_body() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[\"content-type\"], \"application/json\");\n        hyper::Response::default()\n    });\n    redirecting_command()\n        .arg(server.base_url())\n        .write_stdin(\"\")\n        .assert()\n        .success();\n}\n\n#[test]\nfn multipart_file_upload() {\n    let server = server::http(|req| async move {\n        // This test may be fragile, it's conceivable that the headers will become\n        // lowercase in the future\n        // (so if this breaks all of a sudden, check that first)\n        let body = req.body_as_string().await;\n        assert!(body.contains(\"Hello world\"));\n        assert!(body.contains(concat!(\n            \"Content-Disposition: form-data; name=\\\"x\\\"; filename=\\\"input.txt\\\"\\r\\n\",\n            \"\\r\\n\",\n            \"Hello world\\n\"\n        )));\n        assert!(body.contains(concat!(\n            \"Content-Disposition: form-data; name=\\\"y\\\"; filename=\\\"foobar.htm\\\"\\r\\n\",\n            \"Content-Type: text/html\\r\\n\",\n            \"\\r\\n\",\n            \"Hello world\\n\",\n        )));\n\n        hyper::Response::default()\n    });\n\n    let dir = tempfile::tempdir().unwrap();\n    let filename = dir.path().join(\"input.txt\");\n    OpenOptions::new()\n        .create(true)\n        .truncate(true)\n        .write(true)\n        .open(&filename)\n        .unwrap()\n        .write_all(b\"Hello world\\n\")\n        .unwrap();\n\n    get_command()\n        .arg(\"--form\")\n        .arg(server.base_url())\n        .arg(format!(\"x@{}\", filename.to_string_lossy()))\n        .arg(format!(\n            \"y@{};type=text/html;filename=foobar.htm\",\n            filename.to_string_lossy()\n        ))\n        .assert()\n        .success();\n}\n\n#[test]\nfn warn_for_filename_tag_on_body() {\n    let dir = tempfile::tempdir().unwrap();\n    let filename = dir.path().join(\"input\");\n    OpenOptions::new()\n        .create(true)\n        .truncate(true)\n        .write(true)\n        .open(&filename)\n        .unwrap()\n        .write_all(b\"Hello world\\n\")\n        .unwrap();\n\n    get_command()\n        .arg(\"--offline\")\n        .arg(\":\")\n        .arg(format!(\n            \"@{};filename=hello.txt\",\n            filename.to_string_lossy()\n        ))\n        .assert()\n        .success()\n        .stderr(\n            \"xh: warning: Ignoring ;filename= tag for single-file body. Consider --multipart.\\n\",\n        );\n}\n\n#[test]\nfn body_from_file() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[\"content-type\"], \"text/plain\");\n        assert_eq!(req.body_as_string().await, \"Hello world\\n\");\n        hyper::Response::default()\n    });\n\n    let dir = tempfile::tempdir().unwrap();\n    let filename = dir.path().join(\"input.txt\");\n    OpenOptions::new()\n        .create(true)\n        .truncate(true)\n        .write(true)\n        .open(&filename)\n        .unwrap()\n        .write_all(b\"Hello world\\n\")\n        .unwrap();\n\n    get_command()\n        .arg(server.base_url())\n        .arg(format!(\"@{}\", filename.to_string_lossy()))\n        .assert()\n        .success();\n}\n\n#[test]\nfn body_from_file_with_explicit_mimetype() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[\"content-type\"], \"image/png\");\n        assert_eq!(req.body_as_string().await, \"Hello world\\n\");\n        hyper::Response::default()\n    });\n\n    let dir = tempfile::tempdir().unwrap();\n    let filename = dir.path().join(\"input.txt\");\n    OpenOptions::new()\n        .create(true)\n        .truncate(true)\n        .write(true)\n        .open(&filename)\n        .unwrap()\n        .write_all(b\"Hello world\\n\")\n        .unwrap();\n\n    get_command()\n        .arg(server.base_url())\n        .arg(format!(\"@{};type=image/png\", filename.to_string_lossy()))\n        .assert()\n        .success();\n}\n\n#[test]\nfn body_from_file_with_fallback_mimetype() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[\"content-type\"], \"application/json\");\n        assert_eq!(req.body_as_string().await, \"Hello world\\n\");\n        hyper::Response::default()\n    });\n\n    let dir = tempfile::tempdir().unwrap();\n    let filename = dir.path().join(\"input\");\n    OpenOptions::new()\n        .create(true)\n        .truncate(true)\n        .write(true)\n        .open(&filename)\n        .unwrap()\n        .write_all(b\"Hello world\\n\")\n        .unwrap();\n\n    get_command()\n        .arg(server.base_url())\n        .arg(format!(\"@{}\", filename.to_string_lossy()))\n        .assert()\n        .success();\n}\n\n#[test]\nfn no_double_file_body() {\n    get_command()\n        .args([\":\", \"@foo\", \"@bar\"])\n        .assert()\n        .failure()\n        .stderr(contains(\"Can't read request from multiple files\"));\n}\n\n#[test]\nfn print_body_from_file() {\n    let dir = tempfile::tempdir().unwrap();\n    let filename = dir.path().join(\"input\");\n    OpenOptions::new()\n        .create(true)\n        .truncate(true)\n        .write(true)\n        .open(&filename)\n        .unwrap()\n        .write_all(b\"Hello world\\n\")\n        .unwrap();\n\n    get_command()\n        .arg(\"--offline\")\n        .arg(\":\")\n        .arg(format!(\"@{}\", filename.to_string_lossy()))\n        .assert()\n        .success()\n        .stdout(contains(\"Hello world\"));\n}\n\n#[test]\nfn colored_headers() {\n    color_command()\n        .args([\"--offline\", \":\"])\n        .assert()\n        .success()\n        // Color\n        .stdout(contains(\"\\x1b[4m\"))\n        // Reset\n        .stdout(contains(\"\\x1b[0m\"));\n}\n\n#[test]\nfn colored_body() {\n    color_command()\n        .args([\"--offline\", \":\", \"x:=3\"])\n        .assert()\n        .success()\n        .stdout(contains(\"\\x1b[34m3\\x1b[0m\"));\n}\n\n#[test]\nfn force_color_pipe() {\n    redirecting_command()\n        .arg(\"--ignore-stdin\")\n        .arg(\"--offline\")\n        .arg(\"--pretty=colors\")\n        .arg(\":\")\n        .arg(\"x:=3\")\n        .assert()\n        .success()\n        .stdout(contains(\"\\x1b[34m3\\x1b[0m\"));\n}\n\n#[test]\nfn request_json_keys_order_is_preserved() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.body_as_string().await, r#\"{\"name\":\"ali\",\"age\":24}\"#);\n        hyper::Response::default()\n    });\n\n    get_command()\n        .args([\"get\", &server.base_url(), \"name=ali\", \"age:=24\"])\n        .assert()\n        .success();\n}\n\n#[test]\nfn data_field_from_file() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.body_as_string().await, r#\"{\"ids\":\"[1,2,3]\"}\"#);\n        hyper::Response::default()\n    });\n\n    let mut text_file = NamedTempFile::new().unwrap();\n    write!(text_file, \"[1,2,3]\").unwrap();\n\n    get_command()\n        .arg(server.base_url())\n        .arg(format!(\"ids=@{}\", text_file.path().to_string_lossy()))\n        .assert()\n        .success();\n}\n\n#[test]\nfn data_field_from_file_in_form_mode() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.body_as_string().await, r#\"message=hello+world\"#);\n        hyper::Response::default()\n    });\n\n    let mut text_file = NamedTempFile::new().unwrap();\n    write!(text_file, \"hello world\").unwrap();\n\n    get_command()\n        .arg(server.base_url())\n        .arg(\"--form\")\n        .arg(format!(\"message=@{}\", text_file.path().to_string_lossy()))\n        .assert()\n        .success();\n}\n\n#[test]\nfn json_field_from_file() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.body_as_string().await, r#\"{\"ids\":[1,2,3]}\"#);\n        hyper::Response::default()\n    });\n\n    let mut json_file = NamedTempFile::new().unwrap();\n    writeln!(json_file, \"[1,2,3]\").unwrap();\n\n    get_command()\n        .arg(server.base_url())\n        .arg(format!(\"ids:=@{}\", json_file.path().to_string_lossy()))\n        .assert()\n        .success();\n}\n\n#[test]\nfn header_from_file() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[\"x-api-key\"], \"hello1234\");\n        hyper::Response::default()\n    });\n\n    let mut text_file = NamedTempFile::new().unwrap();\n    writeln!(text_file, \"hello1234\").unwrap();\n\n    get_command()\n        .arg(server.base_url())\n        .arg(format!(\"x-api-key:@{}\", text_file.path().to_string_lossy()))\n        .assert()\n        .success();\n}\n\n#[test]\nfn query_param_from_file() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.query_params()[\"foo\"], \"bar+baz\\n\");\n        hyper::Response::default()\n    });\n\n    let mut text_file = NamedTempFile::new().unwrap();\n    writeln!(text_file, \"bar+baz\").unwrap();\n\n    get_command()\n        .arg(server.base_url())\n        .arg(format!(\"foo==@{}\", text_file.path().to_string_lossy()))\n        .assert()\n        .success();\n}\n\n#[test]\nfn can_unset_default_headers() {\n    get_command()\n        .args([\":\", \"user-agent:\", \"--offline\"])\n        .assert()\n        .stdout(indoc! {r#\"\n            GET / HTTP/1.1\n            Accept: */*\n            Accept-Encoding: gzip, deflate, br, zstd\n            Connection: keep-alive\n            Host: http.mock\n\n        \"#});\n}\n\n#[test]\nfn can_unset_headers() {\n    get_command()\n        .args([\":\", \"hello:world\", \"goodbye:world\", \"goodbye:\", \"--offline\"])\n        .assert()\n        .stdout(indoc! {r#\"\n            GET / HTTP/1.1\n            Accept: */*\n            Accept-Encoding: gzip, deflate, br, zstd\n            Connection: keep-alive\n            Hello: world\n            Host: http.mock\n            User-Agent: xh/0.0.0 (test mode)\n\n        \"#});\n}\n\n#[test]\nfn can_set_unset_header() {\n    get_command()\n        .args([\":\", \"hello:\", \"hello:world\", \"--offline\"])\n        .assert()\n        .stdout(indoc! {r#\"\n            GET / HTTP/1.1\n            Accept: */*\n            Accept-Encoding: gzip, deflate, br, zstd\n            Connection: keep-alive\n            Hello: world\n            Host: http.mock\n            User-Agent: xh/0.0.0 (test mode)\n\n        \"#});\n}\n\n#[test]\nfn named_sessions() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"set-cookie\", \"cook1=one; Path=/\")\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    let config_dir = tempdir().unwrap();\n    let random_name = random_string();\n\n    get_command()\n        .env(\"XH_CONFIG_DIR\", config_dir.path())\n        .arg(server.base_url())\n        .arg(format!(\"--session={random_name}\"))\n        .arg(\"--bearer=hello\")\n        .arg(\"cookie:lang=en\")\n        .assert()\n        .success();\n\n    server.assert_hits(1);\n\n    let path_to_session = config_dir.path().join::<std::path::PathBuf>(\n        [\n            \"sessions\",\n            &format!(\"127.0.0.1_{}\", server.port()),\n            &format!(\"{random_name}.json\"),\n        ]\n        .iter()\n        .collect(),\n    );\n\n    let session_content = fs::read_to_string(path_to_session).unwrap();\n\n    assert_eq!(\n        serde_json::from_str::<serde_json::Value>(&session_content).unwrap(),\n        serde_json::json!({\n            \"__meta__\": {\n                \"about\": \"xh session file\",\n                \"xh\": \"0.0.0\"\n            },\n            \"auth\": { \"type\": \"bearer\", \"raw_auth\": \"hello\" },\n            \"cookies\": [\n                { \"name\": \"lang\", \"value\": \"en\", \"path\": \"/\", \"domain\": \"127.0.0.1\" },\n                { \"name\": \"cook1\", \"value\": \"one\", \"path\": \"/\", \"domain\": \"127.0.0.1\" }\n            ],\n            \"headers\": []\n        })\n    );\n}\n\n#[test]\nfn anonymous_sessions() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"set-cookie\", \"cook1=one\")\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    let mut path_to_session = std::env::temp_dir();\n    let file_name = random_string();\n    path_to_session.push(file_name);\n\n    get_command()\n        .arg(server.base_url())\n        .arg(format!(\"--session={}\", path_to_session.to_string_lossy()))\n        .arg(\"--auth=me:pass\")\n        .arg(\"hello:world\")\n        .assert()\n        .success();\n\n    server.assert_hits(1);\n\n    let session_content = fs::read_to_string(path_to_session).unwrap();\n\n    assert_eq!(\n        serde_json::from_str::<serde_json::Value>(&session_content).unwrap(),\n        serde_json::json!({\n            \"__meta__\": {\n                \"about\": \"xh session file\",\n                \"xh\": \"0.0.0\"\n            },\n            \"auth\": { \"type\": \"basic\", \"raw_auth\": \"me:pass\" },\n            \"cookies\": [\n                { \"name\": \"cook1\", \"value\": \"one\", \"domain\": \"127.0.0.1\", \"path\": \"/\" }\n            ],\n            \"headers\": [\n                { \"name\": \"hello\", \"value\": \"world\" }\n            ]\n        })\n    );\n}\n\n#[test]\nfn anonymous_read_only_session() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"set-cookie\", \"lang=en\")\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    let session_file = NamedTempFile::new().unwrap();\n    let old_session_content = serde_json::json!({\n        \"__meta__\": { \"about\": \"xh session file\", \"xh\": \"0.0.0\" },\n        \"auth\": { \"type\": null, \"raw_auth\": null },\n        \"cookies\": [\n            { \"name\": \"cookie1\", \"value\": \"one\" }\n        ],\n        \"headers\": [\n            { \"name\": \"hello\", \"value\": \"world\" }\n        ]\n    });\n\n    std::fs::write(&session_file, old_session_content.to_string()).unwrap();\n\n    get_command()\n        .arg(server.base_url())\n        .arg(\"goodbye:world\")\n        .arg(format!(\n            \"--session-read-only={}\",\n            session_file.path().to_string_lossy()\n        ))\n        .assert()\n        .success();\n\n    assert_eq!(\n        serde_json::from_str::<serde_json::Value>(\n            &fs::read_to_string(session_file.path()).unwrap()\n        )\n        .unwrap(),\n        old_session_content\n    );\n}\n\n#[test]\nfn session_files_are_created_in_read_only_mode() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"set-cookie\", \"lang=ar\")\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    let mut path_to_session = std::env::temp_dir();\n    let file_name = random_string();\n    path_to_session.push(file_name);\n    assert_eq!(path_to_session.exists(), false);\n\n    get_command()\n        .arg(server.base_url())\n        .arg(\"hello:world\")\n        .arg(format!(\n            \"--session-read-only={}\",\n            path_to_session.to_string_lossy()\n        ))\n        .assert()\n        .success();\n\n    let session_content = fs::read_to_string(path_to_session).unwrap();\n    assert_eq!(\n        serde_json::from_str::<serde_json::Value>(&session_content).unwrap(),\n        serde_json::json!({\n            \"__meta__\": {\n                \"about\": \"xh session file\",\n                \"xh\": \"0.0.0\"\n            },\n            \"auth\": { \"type\": null, \"raw_auth\": null },\n            \"cookies\": [\n                { \"name\": \"lang\", \"value\": \"ar\", \"domain\": \"127.0.0.1\", \"path\": \"/\" }\n            ],\n            \"headers\": [\n                { \"name\": \"hello\", \"value\": \"world\" }\n            ]\n        })\n    );\n}\n\n#[test]\nfn named_read_only_session() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"set-cookie\", \"lang=en\")\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    let config_dir = tempdir().unwrap();\n    let random_name = random_string();\n    let path_to_session = config_dir.path().join::<std::path::PathBuf>(\n        [\n            \"xh\",\n            \"sessions\",\n            &format!(\"127.0.0.1_{}\", server.port()),\n            &format!(\"{random_name}.json\"),\n        ]\n        .iter()\n        .collect(),\n    );\n    let old_session_content = serde_json::json!({\n        \"__meta__\": { \"about\": \"xh session file\", \"xh\": \"0.0.0\" },\n        \"auth\": { \"type\": null, \"raw_auth\": null },\n        \"cookies\": [\n            { \"name\": \"cookie1\", \"value\": \"one\" }\n        ],\n        \"headers\": [\n            { \"name\": \"hello\", \"value\": \"world\" }\n        ]\n    });\n    fs::create_dir_all(path_to_session.parent().unwrap()).unwrap();\n    File::create(&path_to_session).unwrap();\n    std::fs::write(&path_to_session, old_session_content.to_string()).unwrap();\n\n    get_command()\n        .env(\"XH_CONFIG_DIR\", config_dir.path())\n        .arg(server.base_url())\n        .arg(\"goodbye:world\")\n        .arg(format!(\"--session-read-only={random_name}\"))\n        .assert()\n        .success();\n\n    assert_eq!(\n        serde_json::from_str::<serde_json::Value>(&fs::read_to_string(path_to_session).unwrap())\n            .unwrap(),\n        old_session_content\n    );\n}\n\n#[test]\nfn expired_cookies_are_removed_from_session() {\n    let future_timestamp = SystemTime::now()\n        .duration_since(UNIX_EPOCH)\n        .unwrap()\n        .as_secs()\n        + 1000;\n    let past_timestamp = 1_114_425_967; // 2005-04-25\n\n    let session_file = NamedTempFile::new().unwrap();\n\n    std::fs::write(\n        &session_file,\n        serde_json::json!({\n            \"__meta__\": { \"about\": \"xh session file\", \"xh\": \"0.0.0\" },\n            \"auth\": { \"type\": null, \"raw_auth\": null },\n            \"cookies\": [\n                {\n                    \"name\": \"expired_cookie\",\n                    \"value\": \"random_string\",\n                    \"expires\": past_timestamp,\n                    \"domain\": \"127.0.0.1\"\n                },\n                {\n                    \"name\": \"unexpired_cookie\",\n                    \"value\": \"random_string\",\n                    \"expires\": future_timestamp,\n                    \"domain\": \"127.0.0.1\"\n                },\n                {\n                    \"name\": \"with_out_expiry\",\n                    \"value\": \"random_string\",\n                    \"domain\": \"127.0.0.1\"\n                }\n            ],\n            \"headers\": []\n        })\n        .to_string(),\n    )\n    .unwrap();\n\n    get_command()\n        .arg(\"127.0.0.1\")\n        .arg(format!(\n            \"--session={}\",\n            session_file.path().to_string_lossy()\n        ))\n        .arg(\"--offline\")\n        .assert()\n        .success();\n\n    let session_content = fs::read_to_string(session_file.path()).unwrap();\n    assert_eq!(\n        serde_json::from_str::<serde_json::Value>(&session_content).unwrap(),\n        serde_json::json!({\n            \"__meta__\": { \"about\": \"xh session file\", \"xh\": \"0.0.0\" },\n            \"auth\": { \"type\": null, \"raw_auth\": null },\n            \"cookies\": [\n                {\n                    \"name\": \"unexpired_cookie\",\n                    \"value\": \"random_string\",\n                    \"expires\": future_timestamp,\n                    \"domain\": \"127.0.0.1\",\n                    \"path\": \"/\"\n                },\n                {\n                    \"name\": \"with_out_expiry\",\n                    \"value\": \"random_string\",\n                    \"domain\": \"127.0.0.1\",\n                    \"path\": \"/\"\n                }\n            ],\n            \"headers\": []\n        })\n    );\n}\n\nfn cookies_are_equal(c1: &str, c2: &str) -> bool {\n    HashSet::<_>::from_iter(c1.split(';').map(str::trim))\n        == HashSet::<_>::from_iter(c2.split(';').map(str::trim))\n}\n\n#[test]\nfn cookies_override_each_other_in_the_correct_order() {\n    // Cookies storage priority is: Server response > Command line request > Session file\n    // See https://httpie.io/docs#cookie-storage-behaviour\n    let server = server::http(|req| async move {\n        assert!(cookies_are_equal(\n            req.headers()[\"cookie\"].to_str().unwrap(),\n            \"lang=fr; cook1=two; cook2=two\"\n        ));\n        hyper::Response::builder()\n            .header(\"set-cookie\", \"lang=en\")\n            .header(\"set-cookie\", \"cook1=one\")\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    let session_file = NamedTempFile::new().unwrap();\n\n    std::fs::write(\n        &session_file,\n        serde_json::json!({\n            \"__meta__\": { \"about\": \"xh session file\", \"xh\": \"0.0.0\" },\n            \"auth\": { \"type\": null, \"raw_auth\": null },\n            \"cookies\": [\n                { \"name\": \"lang\", \"value\": \"fr\", \"domain\": \"127.0.0.1\" },\n                { \"name\": \"cook2\", \"value\": \"three\", \"domain\": \"127.0.0.1\" }\n            ],\n            \"headers\": []\n        })\n        .to_string(),\n    )\n    .unwrap();\n\n    get_command()\n        .arg(server.base_url())\n        .arg(\"cookie:cook1=two;cook2=two\")\n        .arg(format!(\n            \"--session={}\",\n            session_file.path().to_string_lossy()\n        ))\n        .arg(\"--no-check-status\")\n        .assert()\n        .success();\n\n    server.assert_hits(1);\n\n    let session_content = fs::read_to_string(session_file.path()).unwrap();\n    assert_eq!(\n        serde_json::from_str::<serde_json::Value>(&session_content).unwrap(),\n        serde_json::json!({\n            \"__meta__\": { \"about\": \"xh session file\", \"xh\": \"0.0.0\" },\n            \"auth\": { \"type\": null, \"raw_auth\": null },\n            \"cookies\": [\n                { \"name\": \"lang\", \"value\": \"en\", \"domain\": \"127.0.0.1\", \"path\": \"/\" },\n                { \"name\": \"cook2\", \"value\": \"two\", \"domain\": \"127.0.0.1\", \"path\": \"/\" },\n                { \"name\": \"cook1\", \"value\": \"one\", \"domain\": \"127.0.0.1\", \"path\": \"/\" },\n            ],\n            \"headers\": []\n        })\n    );\n}\n\n#[test]\nfn cookies_are_segmented_by_domain() {\n    let session_file = NamedTempFile::new().unwrap();\n\n    std::fs::write(\n        &session_file,\n        serde_json::json!({\n            \"__meta__\": { \"about\": \"xh session file\", \"xh\": \"0.0.0\" },\n            \"auth\": { \"type\": null, \"raw_auth\": null },\n            \"cookies\": [\n                // will be overwritten by set-cookie header from example.com\n                { \"name\": \"lang\", \"value\": \"fi\", \"domain\": \"example.com\" },\n                // will not be overwritten\n                { \"name\": \"lang\", \"value\": \"fr\", \"domain\": \"example.org\" },\n            ],\n            \"headers\": []\n        })\n        .to_string(),\n    )\n    .unwrap();\n\n    let server = server::http(|req| async move {\n        match req.uri().host() {\n            Some(\"example.com\") => {\n                assert_eq!(req.headers()[\"cookie\"].to_str().unwrap(), \"lang=fi\");\n                hyper::Response::builder()\n                    .header(\"set-cookie\", \"lang=en\")\n                    .body(\"\".into())\n                    .unwrap()\n            }\n            Some(\"example.net\") => {\n                assert!(req.headers().get(\"cookie\").is_none());\n                hyper::Response::builder()\n                    .header(\"set-cookie\", \"lang=ar\")\n                    .body(\"\".into())\n                    .unwrap()\n            }\n            _ => panic!(\"unknown path\"),\n        }\n    });\n\n    for url in [\"http://example.com\", \"http://example.net\"] {\n        get_command()\n            .arg(url)\n            .arg(format!(\"--proxy=all:{}\", server.base_url()))\n            .arg(format!(\n                \"--session={}\",\n                session_file.path().to_string_lossy()\n            ))\n            .assert()\n            .success();\n    }\n\n    let session_content = fs::read_to_string(session_file.path()).unwrap();\n    assert_eq!(\n        serde_json::from_str::<serde_json::Value>(&session_content).unwrap(),\n        serde_json::json!({\n            \"__meta__\": { \"about\": \"xh session file\", \"xh\": \"0.0.0\" },\n            \"auth\": { \"type\": null, \"raw_auth\": null },\n            \"cookies\": [\n                { \"name\": \"lang\", \"value\": \"en\", \"domain\": \"example.com\", \"path\": \"/\" },\n                { \"name\": \"lang\", \"value\": \"fr\", \"domain\": \"example.org\", \"path\": \"/\" },\n                { \"name\": \"lang\", \"value\": \"ar\", \"domain\": \"example.net\", \"path\": \"/\" }\n            ],\n            \"headers\": []\n        })\n    );\n}\n\n/// According to [RFC-6265: HTTP State Management\n/// Mechanism](https://httpwg.org/specs/rfc6265.html#cookie-path), cookies without an explicit path\n/// attribute must be interpreted to have a default path. If we don't store that default path, xh\n/// may erroneously send cookies in requests where it shouldn't have.\n#[test]\nfn cookies_are_stored_with_default_path() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"set-cookie\", \"cook1=one\")\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    let mut path_to_session = std::env::temp_dir();\n    let file_name = random_string();\n    path_to_session.push(file_name);\n\n    get_command()\n        .arg(format!(\"{}{}\", server.base_url(), \"/some/path/file\"))\n        .arg(format!(\"--session={}\", path_to_session.to_string_lossy()))\n        .arg(\"--auth=me:pass\")\n        .arg(\"hello:world\")\n        .assert()\n        .success();\n\n    server.assert_hits(1);\n\n    let session_content = fs::read_to_string(path_to_session).unwrap();\n\n    assert_eq!(\n        serde_json::from_str::<serde_json::Value>(&session_content).unwrap(),\n        serde_json::json!({\n            \"__meta__\": {\n                \"about\": \"xh session file\",\n                \"xh\": \"0.0.0\"\n            },\n            \"auth\": { \"type\": \"basic\", \"raw_auth\": \"me:pass\" },\n            \"cookies\": [\n                { \"name\": \"cook1\", \"value\": \"one\", \"domain\": \"127.0.0.1\", \"path\": \"/some/path\" }\n            ],\n            \"headers\": [\n                { \"name\": \"hello\", \"value\": \"world\" }\n            ]\n        })\n    );\n}\n\n#[test]\nfn basic_auth_from_session_is_used() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[\"authorization\"], \"Basic dXNlcjpwYXNz\");\n        hyper::Response::default()\n    });\n\n    let session_file = NamedTempFile::new().unwrap();\n\n    std::fs::write(\n        &session_file,\n        serde_json::json!({\n            \"__meta__\": { \"about\": \"xh session file\", \"xh\": \"0.0.0\" },\n            \"auth\": { \"type\": \"basic\", \"raw_auth\": \"user:pass\" },\n            \"cookies\": [],\n            \"headers\": []\n        })\n        .to_string(),\n    )\n    .unwrap();\n\n    get_command()\n        .arg(server.base_url())\n        .arg(format!(\n            \"--session={}\",\n            session_file.path().to_string_lossy()\n        ))\n        .arg(\"--no-check-status\")\n        .assert()\n        .success();\n}\n\n#[test]\nfn bearer_auth_from_session_is_used() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[\"authorization\"], \"Bearer secret-token\");\n        hyper::Response::default()\n    });\n\n    let session_file = NamedTempFile::new().unwrap();\n\n    std::fs::write(\n        &session_file,\n        serde_json::json!({\n            \"__meta__\": { \"about\": \"xh session file\", \"xh\": \"0.0.0\" },\n            \"auth\": { \"type\": \"bearer\", \"raw_auth\": \"secret-token\" },\n            \"cookies\": [],\n            \"headers\": []\n        })\n        .to_string(),\n    )\n    .unwrap();\n\n    get_command()\n        .arg(server.base_url())\n        .arg(format!(\n            \"--session={}\",\n            session_file.path().to_string_lossy()\n        ))\n        .arg(\"--no-check-status\")\n        .assert()\n        .success();\n}\n\n#[test]\nfn auth_netrc_is_not_persisted_in_session() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[\"authorization\"], \"Basic dXNlcjpwYXNz\");\n        hyper::Response::default()\n    });\n\n    let mut path_to_session = std::env::temp_dir();\n    let file_name = random_string();\n    path_to_session.push(file_name);\n    assert_eq!(path_to_session.exists(), false);\n\n    let mut netrc = NamedTempFile::new().unwrap();\n    writeln!(\n        netrc,\n        \"machine {}\\nlogin user\\npassword pass\",\n        server.host()\n    )\n    .unwrap();\n\n    get_command()\n        .env(\"NETRC\", netrc.path())\n        .arg(server.base_url())\n        .arg(\"hello:world\")\n        .arg(format!(\"--session={}\", path_to_session.to_string_lossy()))\n        .assert()\n        .success();\n\n    server.assert_hits(1);\n\n    let session_content = fs::read_to_string(path_to_session).unwrap();\n    assert_eq!(\n        serde_json::from_str::<serde_json::Value>(&session_content).unwrap(),\n        serde_json::json!({\n            \"__meta__\": {\n                \"about\": \"xh session file\",\n                \"xh\": \"0.0.0\"\n            },\n            \"auth\": { \"type\": null, \"raw_auth\": null },\n            \"cookies\": [],\n            \"headers\": [\n                { \"name\": \"hello\", \"value\": \"world\" }\n            ]\n        })\n    );\n}\n\n#[test]\nfn multiple_headers_with_same_key_in_session() {\n    let server = server::http(|req| async move {\n        use reqwest::header::HeaderValue;\n        assert_eq!(\n            req.headers()\n                .get_all(\"hello\")\n                .into_iter()\n                .collect::<Vec<_>>(),\n            [\n                &HeaderValue::from_static(\"world\"),\n                &HeaderValue::from_static(\"people\")\n            ]\n        );\n        hyper::Response::default()\n    });\n\n    let session_file = NamedTempFile::new().unwrap();\n\n    std::fs::write(\n        &session_file,\n        serde_json::json!({\n            \"__meta__\": { \"about\": \"xh session file\", \"xh\": \"0.0.0\" },\n            \"auth\": {},\n            \"cookies\": [],\n            \"headers\": [\n                { \"name\": \"hello\", \"value\": \"world\" },\n                { \"name\": \"hello\", \"value\": \"people\" },\n            ]\n        })\n        .to_string(),\n    )\n    .unwrap();\n\n    get_command()\n        .arg(server.base_url())\n        .arg(format!(\n            \"--session={}\",\n            session_file.path().to_string_lossy()\n        ))\n        .arg(\"--no-check-status\")\n        .assert()\n        .success();\n}\n\n#[test]\nfn headers_from_session_are_overwritten() {\n    let server = server::http(|req| async move {\n        use reqwest::header::HeaderValue;\n        assert_eq!(\n            req.headers()\n                .get_all(\"hello\")\n                .into_iter()\n                .collect::<Vec<_>>(),\n            [&HeaderValue::from_static(\"people\")]\n        );\n        hyper::Response::default()\n    });\n\n    let session_file = NamedTempFile::new().unwrap();\n\n    std::fs::write(\n        &session_file,\n        serde_json::json!({\n            \"__meta__\": { \"about\": \"xh session file\", \"xh\": \"0.0.0\" },\n            \"auth\": {},\n            \"cookies\": [],\n            \"headers\": [\n                { \"name\": \"hello\", \"value\": \"world\" },\n            ]\n        })\n        .to_string(),\n    )\n    .unwrap();\n\n    get_command()\n        .arg(server.base_url())\n        .arg(format!(\n            \"--session={}\",\n            session_file.path().to_string_lossy()\n        ))\n        .arg(\"--no-check-status\")\n        .arg(\"hello:people\")\n        .assert()\n        .success();\n}\n\n#[test]\nfn old_session_format_is_automatically_migrated() {\n    let server = server::http(|req| async move {\n        assert_eq!(req.headers()[\"hello\"], \"world\");\n        hyper::Response::default()\n    });\n\n    let session_file = NamedTempFile::new().unwrap();\n\n    let future_timestamp = SystemTime::now()\n        .duration_since(UNIX_EPOCH)\n        .unwrap()\n        .as_secs()\n        + 1000;\n\n    std::fs::write(\n        &session_file,\n        serde_json::json!({\n            \"__meta__\": { \"about\": \"xh session file\", \"xh\": \"0.0.0\" },\n            \"auth\": {},\n            \"cookies\": {\n                \"lang\": { \"value\": \"en\", \"expires\": future_timestamp, \"path\": \"/\", \"secure\": false },\n            },\n            \"headers\": { \"hello\": \"world\" }\n        })\n        .to_string(),\n    )\n    .unwrap();\n\n    get_command()\n        .arg(server.base_url())\n        .arg(format!(\n            \"--session={}\",\n            session_file.path().to_string_lossy()\n        ))\n        .assert()\n        .success();\n\n    let session_content = fs::read_to_string(session_file).unwrap();\n    assert_eq!(\n        serde_json::from_str::<serde_json::Value>(&session_content).unwrap(),\n        serde_json::json!({\n            \"__meta__\": { \"about\": \"xh session file\", \"xh\": \"0.0.0\" },\n            \"auth\": { \"type\": null, \"raw_auth\": null },\n            \"cookies\": [\n                {\n                    \"name\": \"lang\",\n                    \"value\": \"en\",\n                    \"expires\": future_timestamp,\n                    \"path\": \"/\",\n                    \"secure\": false,\n                    \"domain\": \"127.0.0.1\"\n                },\n            ],\n            \"headers\": [\n                { \"name\": \"hello\", \"value\": \"world\" }\n            ]\n        })\n    );\n}\n\n#[test]\nfn print_intermediate_requests_and_responses() {\n    let server = server::http(|req| async move {\n        match req.uri().path() {\n            \"/first_page\" => hyper::Response::builder()\n                .status(302)\n                .header(\"Date\", \"N/A\")\n                .header(\"Location\", \"/second_page\")\n                .body(\"redirecting...\".into())\n                .unwrap(),\n            \"/second_page\" => hyper::Response::builder()\n                .header(\"Date\", \"N/A\")\n                .body(\"final destination\".into())\n                .unwrap(),\n            _ => panic!(\"unknown path\"),\n        }\n    });\n\n    get_command()\n        .args([&server.url(\"/first_page\"), \"--follow\", \"--verbose\", \"--all\"])\n        .assert()\n        .stdout(indoc! {r#\"\n            GET /first_page HTTP/1.1\n            Accept: */*\n            Accept-Encoding: gzip, deflate, br, zstd\n            Connection: keep-alive\n            Host: http.mock\n            User-Agent: xh/0.0.0 (test mode)\n\n            HTTP/1.1 302 Found\n            Content-Length: 14\n            Date: N/A\n            Location: /second_page\n\n            redirecting...\n\n            GET /second_page HTTP/1.1\n            Accept: */*\n            Accept-Encoding: gzip, deflate, br, zstd\n            Connection: keep-alive\n            Host: http.mock\n            User-Agent: xh/0.0.0 (test mode)\n\n            HTTP/1.1 200 OK\n            Content-Length: 17\n            Date: N/A\n\n            final destination\n        \"#});\n}\n\n#[test]\nfn history_print() {\n    let server = server::http(|req| async move {\n        match req.uri().path() {\n            \"/first_page\" => hyper::Response::builder()\n                .status(302)\n                .header(\"Date\", \"N/A\")\n                .header(\"Location\", \"/second_page\")\n                .body(\"redirecting...\".into())\n                .unwrap(),\n            \"/second_page\" => hyper::Response::builder()\n                .header(\"Date\", \"N/A\")\n                .body(\"final destination\".into())\n                .unwrap(),\n            _ => panic!(\"unknown path\"),\n        }\n    });\n\n    get_command()\n        .arg(server.url(\"/first_page\"))\n        .arg(\"--follow\")\n        .arg(\"--print=HhBb\")\n        .arg(\"--history-print=Hh\")\n        .arg(\"--all\")\n        .assert()\n        .stdout(indoc! {r#\"\n            GET /first_page HTTP/1.1\n            Accept: */*\n            Accept-Encoding: gzip, deflate, br, zstd\n            Connection: keep-alive\n            Host: http.mock\n            User-Agent: xh/0.0.0 (test mode)\n\n            HTTP/1.1 302 Found\n            Content-Length: 14\n            Date: N/A\n            Location: /second_page\n\n            GET /second_page HTTP/1.1\n            Accept: */*\n            Accept-Encoding: gzip, deflate, br, zstd\n            Connection: keep-alive\n            Host: http.mock\n            User-Agent: xh/0.0.0 (test mode)\n\n            HTTP/1.1 200 OK\n            Content-Length: 17\n            Date: N/A\n\n            final destination\n        \"#});\n}\n\n#[test]\nfn max_redirects_is_enforced() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .status(302)\n            .header(\"Date\", \"N/A\")\n            .header(\"Location\", \"/\") // infinite redirect loop\n            .body(\"redirecting...\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .args([&server.base_url(), \"--follow\", \"--max-redirects=5\"])\n        .assert()\n        .stderr(contains(\"Too many redirects (--max-redirects=5)\"))\n        .code(6);\n}\n\n#[test]\nfn method_is_changed_when_following_302_redirect() {\n    let server = server::http(|req| async move {\n        match req.uri().path() {\n            \"/first_page\" => {\n                assert_eq!(req.method(), \"POST\");\n                assert!(req.headers().get(\"Content-Length\").is_some());\n                assert_eq!(req.body_as_string().await, r#\"{\"name\":\"ali\"}\"#);\n                hyper::Response::builder()\n                    .status(302)\n                    .header(\"Location\", \"/second_page\")\n                    .body(\"redirecting...\".into())\n                    .unwrap()\n            }\n            \"/second_page\" => {\n                assert_eq!(req.method(), \"GET\");\n                assert!(req.headers().get(\"Content-Length\").is_none());\n                hyper::Response::builder()\n                    .body(\"final destination\".into())\n                    .unwrap()\n            }\n            _ => panic!(\"unknown path\"),\n        }\n    });\n\n    get_command()\n        .args([\n            \"post\",\n            &server.url(\"/first_page\"),\n            \"--verbose\",\n            \"--follow\",\n            \"name=ali\",\n        ])\n        .assert()\n        .success()\n        .stdout(contains(\"POST /first_page HTTP/1.1\"))\n        .stdout(contains(\"GET /second_page HTTP/1.1\"));\n\n    server.assert_hits(2);\n}\n\n#[test]\nfn method_is_not_changed_when_following_307_redirect() {\n    let server = server::http(|req| async move {\n        match req.uri().path() {\n            \"/first_page\" => {\n                assert_eq!(req.method(), \"POST\");\n                assert_eq!(req.body_as_string().await, r#\"{\"name\":\"ali\"}\"#);\n                hyper::Response::builder()\n                    .status(307)\n                    .header(\"Location\", \"/second_page\")\n                    .body(\"redirecting...\".into())\n                    .unwrap()\n            }\n            \"/second_page\" => {\n                assert_eq!(req.method(), \"POST\");\n                assert_eq!(req.body_as_string().await, r#\"{\"name\":\"ali\"}\"#);\n                hyper::Response::builder()\n                    .body(\"final destination\".into())\n                    .unwrap()\n            }\n            _ => panic!(\"unknown path\"),\n        }\n    });\n\n    get_command()\n        .args([\n            \"post\",\n            &server.url(\"/first_page\"),\n            \"--verbose\",\n            \"--follow\",\n            \"name=ali\",\n        ])\n        .assert()\n        .success()\n        .stdout(contains(\"POST /first_page HTTP/1.1\"))\n        .stdout(contains(\"POST /second_page HTTP/1.1\"));\n\n    server.assert_hits(2);\n}\n\n#[test]\nfn sensitive_headers_are_removed_after_cross_domain_redirect() {\n    let server1 = server::http(|req| async move {\n        assert!(req.headers().get(\"Authorization\").is_none());\n        assert!(req.headers().get(\"Hello\").is_some());\n        hyper::Response::builder()\n            .header(\"Date\", \"N/A\")\n            .body(\"final destination\".into())\n            .unwrap()\n    });\n\n    let server1_base_url = server1.base_url();\n    let server2 = server::http(move |req| {\n        let server1_base_url = server1_base_url.clone();\n        async move {\n            assert!(req.headers().get(\"Authorization\").is_some());\n            assert!(req.headers().get(\"Hello\").is_some());\n            hyper::Response::builder()\n                .status(302)\n                .header(\"Location\", server1_base_url)\n                .body(\"redirecting...\".into())\n                .unwrap()\n        }\n    });\n\n    get_command()\n        .arg(server2.base_url())\n        .arg(\"--follow\")\n        .arg(\"--auth=user:pass\")\n        .arg(\"hello:world\")\n        .assert()\n        .success();\n\n    server1.assert_hits(1);\n    server2.assert_hits(1);\n}\n\n#[test]\nfn request_body_is_buffered_for_307_redirect() {\n    let server = server::http(|req| async move {\n        match req.uri().path() {\n            \"/first_page\" => hyper::Response::builder()\n                .status(307)\n                .header(\"Location\", \"/second_page\")\n                .body(\"redirecting...\".into())\n                .unwrap(),\n            \"/second_page\" => {\n                assert_eq!(req.body_as_string().await, \"hello world\\n\");\n                hyper::Response::builder()\n                    .body(\"final destination\".into())\n                    .unwrap()\n            }\n            _ => panic!(\"unknown path\"),\n        }\n    });\n\n    let mut file = NamedTempFile::new().unwrap();\n    writeln!(file, \"hello world\").unwrap();\n\n    get_command()\n        .arg(server.url(\"/first_page\"))\n        .arg(\"--follow\")\n        .arg(\"--all\")\n        .arg(\"--print=Hh\") // prevent Printer from buffering the request body by not using --verbose\n        .arg(format!(\"@{}\", file.path().to_string_lossy()))\n        .assert()\n        .success()\n        .stdout(contains(\"POST /second_page HTTP/1.1\"));\n\n    server.assert_hits(2);\n}\n\n#[test]\nfn read_args_from_config() {\n    let config_dir = tempdir().unwrap();\n    File::create(config_dir.path().join(\"config.json\")).unwrap();\n    std::fs::write(\n        config_dir.path().join(\"config.json\"),\n        serde_json::json!({\"default_options\": [\"--form\", \"--print=hbHB\"]}).to_string(),\n    )\n    .unwrap();\n\n    get_command()\n        .env(\"XH_CONFIG_DIR\", config_dir.path())\n        .arg(\":\")\n        .arg(\"--offline\")\n        .arg(\"--print=B\") // this should overwrite the value from config.json\n        .arg(\"sort=asc\")\n        .arg(\"limit=100\")\n        .assert()\n        .stdout(\"sort=asc&limit=100\\n\\n\")\n        .success();\n}\n\n#[test]\nfn warns_if_config_is_invalid() {\n    let config_dir = tempdir().unwrap();\n    File::create(config_dir.path().join(\"config.json\")).unwrap();\n    std::fs::write(\n        config_dir.path().join(\"config.json\"),\n        serde_json::json!({\"default_options\": \"--form\"}).to_string(),\n    )\n    .unwrap();\n\n    get_command()\n        .env(\"XH_CONFIG_DIR\", config_dir.path())\n        .args([\":\", \"--offline\"])\n        .assert()\n        .stderr(contains(\"Unable to parse config file\"))\n        .success();\n}\n\n#[cfg(feature = \"online-tests\")]\n#[test]\nfn http1_0() {\n    get_command()\n        .args([\n            \"--print=hH\",\n            \"--http-version=1.0\",\n            \"https://httpbingo.org/json\",\n        ])\n        .assert()\n        .success()\n        .stdout(contains(\"GET /json HTTP/1.0\"))\n        // Some servers i.e nginx respond with HTTP/1.1 to HTTP/1.0 requests, see https://serverfault.com/questions/442960/nginx-ignoring-clients-http-1-0-request-and-respond-by-http-1-1\n        // Fortunately, https://httpbingo.org is not one of those.\n        .stdout(contains(\"HTTP/1.0 200 OK\"));\n}\n\n#[cfg(feature = \"online-tests\")]\n#[test]\nfn http1_1() {\n    get_command()\n        .args([\n            \"--print=hH\",\n            \"--http-version=1.1\",\n            \"https://httpbingo.org/json\",\n        ])\n        .assert()\n        .success()\n        .stdout(contains(\"GET /json HTTP/1.1\"))\n        .stdout(contains(\"HTTP/1.1 200 OK\"));\n}\n\n#[cfg(feature = \"online-tests\")]\n#[test]\nfn http2() {\n    get_command()\n        .args([\n            \"--print=hH\",\n            \"--http-version=2\",\n            \"https://httpbingo.org/json\",\n        ])\n        .assert()\n        .success()\n        .stdout(contains(\"GET /json HTTP/2.0\"))\n        .stdout(contains(\"HTTP/2.0 200 OK\"));\n}\n\n#[test]\nfn http2_prior_knowledge() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .body(\"Hello HTTP/2.0\".into())\n            .unwrap()\n    });\n    get_command()\n        .arg(\"-v\")\n        .arg(\"--http-version=2\")\n        .arg(server.base_url())\n        .assert()\n        .failure()\n        .stderr(contains(\"UserUnsupportedVersion\"));\n\n    get_command()\n        .arg(\"-v\")\n        .arg(\"--http-version=2-prior-knowledge\")\n        .arg(server.base_url())\n        .assert()\n        .success()\n        .stdout(contains(\"GET / HTTP/2.0\"))\n        .stdout(contains(\"HTTP/2.0 200\"))\n        .stdout(contains(\"Hello HTTP/2.0\"));\n}\n\n#[cfg(all(feature = \"online-tests\", feature = \"http3\", feature = \"rustls\"))]\n#[test]\nfn http3_prior_knowledge() {\n    get_command()\n        .arg(\"-v\")\n        .arg(\"--http-version=3-prior-knowledge\")\n        .arg(\"https://hyper.rs\")\n        .assert()\n        .success()\n        .stdout(contains(\"GET / HTTP/3.0\"))\n        .stdout(contains(\"HTTP/3.0 200\"));\n}\n\n#[test]\nfn override_response_charset() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"Content-Type\", \"text/plain; charset=utf-8\")\n            .body(b\"\\xe9\".as_ref().into())\n            .unwrap()\n    });\n\n    get_command()\n        .arg(\"--print=b\")\n        .arg(\"--response-charset=latin1\")\n        .arg(server.base_url())\n        .assert()\n        .stdout(\"é\\n\");\n}\n\n#[test]\nfn override_response_mime() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"Content-Type\", \"text/html; charset=utf-8\")\n            .body(\"{\\\"status\\\": \\\"ok\\\"}\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .arg(\"--print=b\")\n        .arg(\"--response-mime=application/json\")\n        .arg(server.base_url())\n        .assert()\n        .stdout(indoc! {r#\"\n        {\n            \"status\": \"ok\"\n        }\n\n\n        \"#});\n}\n\n#[test]\nfn omit_response_body() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"date\", \"N/A\")\n            .body(\"Hello!\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .arg(\"--print=h\")\n        .arg(server.base_url())\n        .assert()\n        .stdout(indoc! {r#\"\n            HTTP/1.1 200 OK\n            Content-Length: 6\n            Date: N/A\n\n        \"#});\n}\n\n#[test]\nfn encoding_detection() {\n    fn case(\n        content_type: &'static str,\n        body: &'static (impl AsRef<[u8]> + ?Sized),\n        output: &'static str,\n    ) {\n        let body = body.as_ref();\n        let server = server::http(move |_| async move {\n            hyper::Response::builder()\n                .header(\"Content-Type\", content_type)\n                .body(body.into())\n                .unwrap()\n        });\n\n        get_command()\n            .arg(\"--print=b\")\n            .arg(server.base_url())\n            .assert()\n            .stdout(output);\n\n        get_command()\n            .arg(\"--print=b\")\n            .arg(\"--stream\")\n            .arg(server.base_url())\n            .assert()\n            .stdout(output);\n\n        server.assert_hits(2);\n    }\n\n    // UTF-8 is a typical fallback\n    case(\"text/plain\", \"é\", \"é\\n\");\n\n    // But headers take precedence\n    case(\"text/html; charset=latin1\", \"é\", \"Ã©\\n\");\n\n    // As do BOMs\n    case(\"text/html\", b\"\\xFF\\xFEa\\0b\\0\", \"ab\\n\");\n\n    // windows-1252 is another common fallback\n    case(\"text/plain\", b\"\\xFF\", \"ÿ\\n\");\n\n    // BOMs are stripped\n    case(\"text/plain\", b\"\\xFF\\xFEa\\0b\\0\", \"ab\\n\");\n    case(\"text/plain; charset=UTF-16\", b\"\\xFF\\xFEa\\0b\\0\", \"ab\\n\");\n    case(\"text/plain; charset=UTF-16LE\", b\"\\xFF\\xFEa\\0b\\0\", \"ab\\n\");\n    case(\"text/plain\", b\"\\xFE\\xFF\\0a\\0b\", \"ab\\n\");\n    case(\"text/plain; charset=UTF-16BE\", b\"\\xFE\\xFF\\0a\\0b\", \"ab\\n\");\n\n    // ...unless they're for a different encoding\n    case(\n        \"text/plain; charset=UTF-16LE\",\n        b\"\\xFE\\xFFa\\0b\\0\",\n        \"\\u{FFFE}ab\\n\",\n    );\n    case(\n        \"text/plain; charset=UTF-16BE\",\n        b\"\\xFF\\xFE\\0a\\0b\",\n        \"\\u{FFFE}ab\\n\",\n    );\n\n    // Binary content is detected\n    case(\"application/octet-stream\", \"foo\\0bar\", BINARY_SUPPRESSOR);\n\n    // (even for non-ASCII-compatible encodings)\n    case(\"text/plain; charset=UTF-16\", \"\\0\\0\", BINARY_SUPPRESSOR);\n}\n\n#[test]\nfn tilde_expanded_in_request_items() {\n    let homedir = TempDir::new().unwrap();\n\n    std::fs::write(homedir.path().join(\"secret_key.txt\"), \"sxemfalm.....\").unwrap();\n    get_command()\n        .env(\"HOME\", homedir.path())\n        .env(\"XH_TEST_MODE_WIN_HOME_DIR\", homedir.path())\n        .args([\"--offline\", \":\", \"key=@~/secret_key.txt\"])\n        .assert()\n        .stdout(contains(\"sxemfalm.....\"))\n        .success();\n\n    std::fs::write(homedir.path().join(\"ids.json\"), \"[102,111,164]\").unwrap();\n    get_command()\n        .env(\"HOME\", homedir.path())\n        .env(\"XH_TEST_MODE_WIN_HOME_DIR\", homedir.path())\n        .args([\"--offline\", \"--pretty=none\", \":\", \"ids:=@~/ids.json\"])\n        .assert()\n        .stdout(contains(\"[102,111,164]\"))\n        .success();\n\n    std::fs::write(homedir.path().join(\"moby-dick.txt\"), \"Call me Ishmael.\").unwrap();\n    get_command()\n        .env(\"HOME\", homedir.path())\n        .env(\"XH_TEST_MODE_WIN_HOME_DIR\", homedir.path())\n        .args([\"--offline\", \"--form\", \":\", \"content@~/moby-dick.txt\"])\n        .assert()\n        .stdout(contains(\"Call me Ishmael.\"))\n        .success();\n\n    std::fs::write(homedir.path().join(\"random_file\"), \"random data\").unwrap();\n    get_command()\n        .env(\"HOME\", homedir.path())\n        .env(\"XH_TEST_MODE_WIN_HOME_DIR\", homedir.path())\n        .args([\"--offline\", \":\", \"@~/random_file\"])\n        .assert()\n        .stdout(contains(\"random data\"))\n        .success();\n}\n\n#[test]\nfn gzip() {\n    let server = server::http(|_req| async move {\n        let compressed_bytes = fs::read(\"./tests/fixtures/responses/hello_world.gz\").unwrap();\n        hyper::Response::builder()\n            .header(\"date\", \"N/A\")\n            .header(\"content-encoding\", \"gzip\")\n            .body(compressed_bytes.into())\n            .unwrap()\n    });\n\n    get_command()\n        .arg(server.base_url())\n        .assert()\n        .stdout(indoc! {r#\"\n            HTTP/1.1 200 OK\n            Content-Encoding: gzip\n            Content-Length: 48\n            Date: N/A\n\n            Hello world\n\n        \"#});\n}\n\n#[test]\nfn deflate() {\n    let server = server::http(|_req| async move {\n        let compressed_bytes = fs::read(\"./tests/fixtures/responses/hello_world.zz\").unwrap();\n        hyper::Response::builder()\n            .header(\"date\", \"N/A\")\n            .header(\"content-encoding\", \"deflate\")\n            .body(compressed_bytes.into())\n            .unwrap()\n    });\n\n    get_command()\n        .arg(server.base_url())\n        .assert()\n        .stdout(indoc! {r#\"\n            HTTP/1.1 200 OK\n            Content-Encoding: deflate\n            Content-Length: 20\n            Date: N/A\n\n            Hello world\n\n        \"#});\n}\n\n#[test]\nfn brotli() {\n    let server = server::http(|_req| async move {\n        let compressed_bytes = fs::read(\"./tests/fixtures/responses/hello_world.br\").unwrap();\n        hyper::Response::builder()\n            .header(\"date\", \"N/A\")\n            .header(\"content-encoding\", \"br\")\n            .body(compressed_bytes.into())\n            .unwrap()\n    });\n\n    get_command()\n        .arg(server.base_url())\n        .assert()\n        .stdout(indoc! {r#\"\n            HTTP/1.1 200 OK\n            Content-Encoding: br\n            Content-Length: 17\n            Date: N/A\n\n            Hello world\n\n        \"#});\n}\n\n#[test]\nfn zstd() {\n    let server = server::http(|_req| async move {\n        let compressed_bytes = fs::read(\"./tests/fixtures/responses/hello_world.zst\").unwrap();\n        hyper::Response::builder()\n            .header(\"date\", \"N/A\")\n            .header(\"content-encoding\", \"zstd\")\n            .body(compressed_bytes.into())\n            .unwrap()\n    });\n\n    get_command()\n        .arg(server.base_url())\n        .assert()\n        .stdout(indoc! {r#\"\n            HTTP/1.1 200 OK\n            Content-Encoding: zstd\n            Content-Length: 25\n            Date: N/A\n\n            Hello world\n\n        \"#});\n}\n\n#[test]\nfn empty_response_with_content_encoding() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"date\", \"N/A\")\n            .header(\"content-encoding\", \"gzip\")\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .arg(server.base_url())\n        .assert()\n        .stdout(indoc! {r#\"\n            HTTP/1.1 200 OK\n            Content-Encoding: gzip\n            Content-Length: 0\n            Date: N/A\n\n\n        \"#});\n}\n\n#[test]\nfn empty_response_with_content_encoding_and_content_length() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"date\", \"N/A\")\n            .header(\"content-encoding\", \"gzip\")\n            .header(\"content-length\", \"100\")\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .arg(\"head\")\n        .arg(server.base_url())\n        .assert()\n        .stdout(indoc! {r#\"\n            HTTP/1.1 200 OK\n            Content-Encoding: gzip\n            Content-Length: 100\n            Date: N/A\n\n\n        \"#});\n}\n\n/// Regression test: this used to crash because ZstdDecoder::new() is fallible\n#[test]\nfn empty_zstd_response_with_content_encoding_and_content_length() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"date\", \"N/A\")\n            .header(\"content-encoding\", \"zstd\")\n            .header(\"content-length\", \"100\")\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .arg(\"head\")\n        .arg(server.base_url())\n        .assert()\n        .stdout(indoc! {r#\"\n            HTTP/1.1 200 OK\n            Content-Encoding: zstd\n            Content-Length: 100\n            Date: N/A\n\n\n        \"#});\n}\n\n/// After an initial fix this scenario still crashed\n#[test]\nfn streaming_empty_zstd_response_with_content_encoding_and_content_length() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"date\", \"N/A\")\n            .header(\"content-encoding\", \"zstd\")\n            .header(\"content-length\", \"100\")\n            .body(\"\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .arg(\"--stream\")\n        .arg(\"head\")\n        .arg(server.base_url())\n        .assert()\n        .stdout(indoc! {r#\"\n            HTTP/1.1 200 OK\n            Content-Encoding: zstd\n            Content-Length: 100\n            Date: N/A\n\n\n        \"#});\n}\n\n#[test]\nfn response_meta() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"date\", \"N/A\")\n            .body(\"Hello!\".into())\n            .unwrap()\n    });\n\n    get_command()\n        .arg(\"--print=m\")\n        .arg(server.base_url())\n        .assert()\n        .stdout(contains(\"Elapsed time: \"))\n        .stdout(contains(\"Remote address: \"));\n}\n\n#[test]\nfn redirect_with_response_meta() {\n    let server = server::http(|req| async move {\n        match req.uri().path() {\n            \"/first_page\" => hyper::Response::builder()\n                .status(302)\n                .header(\"Date\", \"N/A\")\n                .header(\"Location\", \"/second_page\")\n                .body(\"redirecting...\".into())\n                .unwrap(),\n            \"/second_page\" => hyper::Response::builder()\n                .header(\"Date\", \"N/A\")\n                .body(\"final destination\".into())\n                .unwrap(),\n            _ => panic!(\"unknown path\"),\n        }\n    });\n\n    get_command()\n        .arg(server.url(\"/first_page\"))\n        .arg(\"--follow\")\n        .arg(\"-vv\")\n        .assert()\n        .stdout(contains(\"Elapsed time: \").count(2))\n        .stdout(contains(\"Remote address: \").count(2));\n\n    get_command()\n        .arg(server.url(\"/first_page\"))\n        .arg(\"--follow\")\n        .arg(\"--meta\")\n        .assert()\n        .stdout(contains(\"Elapsed time: \").count(1))\n        .stdout(contains(\"Remote address: \").count(1));\n}\n\n#[cfg(feature = \"online-tests\")]\n#[test]\nfn digest_auth_with_response_meta() {\n    get_command()\n        .arg(\"--auth-type=digest\")\n        .arg(\"--auth=ahmed:12345\")\n        .arg(\"-vv\")\n        .arg(\"httpbingo.org/digest-auth/auth/ahmed/12345\")\n        .assert()\n        .stdout(contains(\"Elapsed time: \").count(2))\n        .stdout(contains(\"Remote address: \").count(2));\n}\n\n#[test]\nfn non_get_redirect_translation_warning() {\n    get_command()\n        .args([\"--follow\", \"--curl\", \"POST\", \"http://example.com\"])\n        .assert()\n        .stderr(contains(\"Using a combination of -X/--request and -L/--location which may cause unintended side effects.\"));\n}\n\n#[test]\nfn custom_json_indent_level() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"content-type\", \"application/json\")\n            .body(r#\"{\"hello\":\"world\"}\"#.into())\n            .unwrap()\n    });\n    get_command()\n        .args([\n            \"--print=b\",\n            \"--format-options=json.indent:2\",\n            &server.base_url(),\n        ])\n        .assert()\n        .stdout(indoc! {r#\"\n            {\n              \"hello\": \"world\"\n            }\n\n\n        \"#});\n}\n\n#[test]\nfn unsorted_headers() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"X-Foo\", \"Bar\")\n            .header(\"Date\", \"N/A\")\n            .header(\"Content-Type\", \"application/json\")\n            .body(r#\"{\"hello\":\"world\"}\"#.into())\n            .unwrap()\n    });\n    get_command()\n        .args([\"--format-options=headers.sort:false\", &server.base_url()])\n        .assert()\n        .stdout(indoc! {r#\"\n            HTTP/1.1 200 OK\n            X-Foo: Bar\n            Date: N/A\n            Content-Type: application/json\n            Content-Length: 17\n\n            {\n                \"hello\": \"world\"\n            }\n\n\n        \"#});\n}\n\n#[test]\nfn multiple_format_options_are_merged() {\n    let server = server::http(|_req| async move {\n        hyper::Response::builder()\n            .header(\"X-Foo\", \"Bar\")\n            .header(\"Date\", \"N/A\")\n            .header(\"Content-Type\", \"application/json\")\n            .body(r#\"{\"hello\":\"world\"}\"#.into())\n            .unwrap()\n    });\n    get_command()\n        .arg(\"--format-options=json.indent:2,json.indent:8\")\n        .arg(\"--format-options=headers.sort:false\")\n        .arg(server.base_url())\n        .assert()\n        .stdout(indoc! {r#\"\n            HTTP/1.1 200 OK\n            X-Foo: Bar\n            Date: N/A\n            Content-Type: application/json\n            Content-Length: 17\n\n            {\n                    \"hello\": \"world\"\n            }\n\n\n        \"#});\n}\n\n#[test]\nfn reason_phrase_is_preserved() {\n    let server = server::http(|_req| async move {\n        let mut response = hyper::Response::builder();\n        response\n            .extensions_mut()\n            .unwrap()\n            .insert(hyper::ext::ReasonPhrase::from_static(b\"Wonderful\"));\n        response.header(\"Date\", \"N/A\").body(\"\".into()).unwrap()\n    });\n    get_command()\n        .arg(server.base_url())\n        .assert()\n        .stdout(indoc! {r#\"\n            HTTP/1.1 200 Wonderful\n            Content-Length: 0\n            Date: N/A\n\n\n        \"#});\n}\n"
  },
  {
    "path": "tests/fixtures/certs/README.md",
    "content": "# Test Fixtures: HTTPS Certificates\n\n## Client certificate\n\nSource: https://github.com/jihchi/ht/pull/1#issuecomment-777902358 by [otaconix](https://github.com/otaconix)\n\n- `./client.badssl.com.crt`\n- `./client.badssl.com.key`\n\n## Self-signed Certificate\n\nSource: https://github.com/chromium/badssl.com/blob/master/certs/sets/prod/pregen/chain/wildcard-self-signed.pem\n\n- `./wildcard-self-signed.pem`\n"
  },
  {
    "path": "tests/fixtures/certs/client.badssl.com.crt",
    "content": "Bag Attributes\n    localKeyID: 41 C3 6C 33 C7 E3 36 DD EA 4A 1F C0 B7 23 B8 E6 9C DC D8 0F\nsubject=C = US, ST = California, L = San Francisco, O = BadSSL, CN = BadSSL Client Certificate\n\nissuer=C = US, ST = California, L = San Francisco, O = BadSSL, CN = BadSSL Client Root Certificate Authority\n\n-----BEGIN CERTIFICATE-----\nMIIEqDCCApCgAwIBAgIUK5Ns4y2CzosB/ZoFlaxjZqoBTIIwDQYJKoZIhvcNAQEL\nBQAwfjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM\nDVNhbiBGcmFuY2lzY28xDzANBgNVBAoMBkJhZFNTTDExMC8GA1UEAwwoQmFkU1NM\nIENsaWVudCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eTAeFw0xOTExMjcwMDE5\nNTdaFw0yMTExMjYwMDE5NTdaMG8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxp\nZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ8wDQYDVQQKDAZCYWRTU0wx\nIjAgBgNVBAMMGUJhZFNTTCBDbGllbnQgQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQDHN18R6x5Oz+u6SOXLoxIscz5GHR6cDcCLgyPa\nx2XfXHdJs+h6fTy61WGM+aXEhR2SIwbj5997s34m0MsbvkJrFmn0LHK1fuTLCihE\nEmxGdCGZA9xrwxFYAkEjP7D8v7cAWRMipYF/JP7VU7xNUo+QSkZ0sOi9k6bNkABK\nL3+yP6PqAzsBoKIN5lN/YRLrppsDmk6nrRDo4R3CD+8JQl9quEoOmL22Pc/qpOjL\n1jgOIFSE5y3gwbzDlfCYoAL5V+by1vu0yJShTTK8oo5wvphcFfEHaQ9w5jFg2htd\nq99UER3BKuNDuL+zejqGQZCWb0Xsk8S5WBuX8l3Brrg5giqNAgMBAAGjLTArMAkG\nA1UdEwQCMAAwEQYJYIZIAYb4QgEBBAQDAgeAMAsGA1UdDwQEAwIF4DANBgkqhkiG\n9w0BAQsFAAOCAgEAZBauLzFSOijkDadcippr9C6laHebb0oRS54xAV70E9k5GxfR\n/E2EMuQ8X+miRUMXxKquffcDsSxzo2ac0flw94hDx3B6vJIYvsQx9Lzo95Im0DdT\nDkHFXhTlv2kjQwFVnEsWYwyGpHMTjanvNkO7sBP9p1bN1qTE3QAeyMZNKWJk5xPl\nU298ERar6tl3Z2Cl8mO6yLhrq4ba6iPGw08SENxzuAJW+n8r0rq7EU+bMg5spgT1\nCxExzG8Bb0f98ZXMklpYFogkcuH4OUOFyRodotrotm3iRbuvZNk0Zz7N5n1oLTPl\nbGPMwBcqaGXvK62NlaRkwjnbkPM4MYvREM0bbAgZD2GHyANBTso8bdWvhLvmoSjs\nFSqJUJp17AZ0x/ELWZd69v2zKW9UdPmw0evyVR19elh/7dmtF6wbewc4N4jxQnTq\nIItuhIWKWB9edgJz65uZ9ubQWjXoa+9CuWcV/1KxuKCbLHdZXiboLrKm4S1WmMYW\nd0sJm95H9mJzcLyhLF7iX2kK6K9ug1y02YCVXBC9WGZc2x6GMS7lDkXSkJFy3EWh\nCmfxkmFGwOgwKt3Jd1pF9ftcSEMhu4WcMgxi9vZr9OdkJLxmk033sVKI/hnkPaHw\ng0Y2YBH5v0xmi8sYU7weOcwynkjZARpUltBUQ0pWCF5uJsEB8uE8PPDD3c4=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/fixtures/certs/client.badssl.com.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAxzdfEeseTs/rukjly6MSLHM+Rh0enA3Ai4Mj2sdl31x3SbPo\nen08utVhjPmlxIUdkiMG4+ffe7N+JtDLG75CaxZp9CxytX7kywooRBJsRnQhmQPc\na8MRWAJBIz+w/L+3AFkTIqWBfyT+1VO8TVKPkEpGdLDovZOmzZAASi9/sj+j6gM7\nAaCiDeZTf2ES66abA5pOp60Q6OEdwg/vCUJfarhKDpi9tj3P6qToy9Y4DiBUhOct\n4MG8w5XwmKAC+Vfm8tb7tMiUoU0yvKKOcL6YXBXxB2kPcOYxYNobXavfVBEdwSrj\nQ7i/s3o6hkGQlm9F7JPEuVgbl/Jdwa64OYIqjQIDAQABAoIBAFUQf7fW/YoJnk5c\n8kKRzyDL1Lt7k6Zu+NiZlqXEnutRQF5oQ8yJzXS5yH25296eOJI+AqMuT28ypZtN\nbGzcQOAZIgTxNcnp9Sf9nlPyyekLjY0Y6PXaxX0e+VFj0N8bvbiYUGNq6HCyC15r\n8uvRZRvnm04YfEj20zLTWkxTG+OwJ6ZNha1vfq8z7MG5JTsZbP0g7e/LrEb3wI7J\nZu9yHQUzq23HhfhpmLN/0l89YLtOaS8WNq4QvKYgZapw/0G1wWoWW4Y2/UpAxZ9r\ncqTBWSpCSCCgyWjiNhPbSJWfe/9J2bcanITLcvCLlPWGAHy1wpo9iBH57y7S+7YS\n3yi7lgECgYEA8lwaRIChc38tmtQCNPtai/7uVDdeJe0uv8Jsg04FTF8KMYcD0V1g\n+T7rUPA+rTHwv8uAGLdzl4NW5Qryw18rDY+UivnaZkEdEsnlo3fc8MSQF78dDHCX\nnwmHfOmBnBoSbLl+W5ByHkJRHOnX+8qKq9ePNFUMf/hZNYuma9BCFBUCgYEA0m2p\nVDn12YdhFUUBIH91aD5cQIsBhkHFU4vqW4zBt6TsJpFciWbrBrTeRzeDou59aIsn\nzGBrLMykOY+EwwRku9KTVM4U791Z/NFbH89GqyUaicb4or+BXw5rGF8DmzSsDo0f\nixJ9TVD5DmDi3c9ZQ7ljrtdSxPdA8kOoYPFsApkCgYEA08uZSPQAI6aoe/16UEK4\nRk9qhz47kHlNuVZ27ehoyOzlQ5Lxyy0HacmKaxkILOLPuUxljTQEWAv3DAIdVI7+\nWMN41Fq0eVe9yIWXoNtGwUGFirsA77YVSm5RcN++3GQMZedUfUAl+juKFvJkRS4j\nMTkXdGw+mDa3/wsjTGSa2mECgYABO6NCWxSVsbVf6oeXKSgG9FaWCjp4DuqZErjM\n0IZSDSVVFIT2SSQXZffncuvSiJMziZ0yFV6LZKeRrsWYXu44K4Oxe4Oj5Cgi0xc1\nmIFRf2YoaIIMchLP+8Wk3ummfyiC7VDB/9m8Gj1bWDX8FrrvKqbq31gcz1YSFVNn\nPgLkAQKBgFzG8NdL8os55YcjBcOZMUs5QTKiQSyZM0Abab17k9JaqsU0jQtzeFsY\nFTiwh2uh6l4gdO/dGC/P0Vrp7F05NnO7oE4T+ojDzVQMnFpCBeL7x08GfUQkphEG\nm0Wqhhi8/24Sy934t5Txgkfoltg8ahkx934WjP6WWRnSAu+cf+vW\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "tests/fixtures/certs/wildcard-self-signed.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDeTCCAmGgAwIBAgIJAIb7Tcjl3Q8YMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNV\nBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp\nc2NvMQ8wDQYDVQQKDAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTAeFw0x\nNjA4MDgyMTE3MDVaFw0xODA4MDgyMTE3MDVaMGIxCzAJBgNVBAYTAlVTMRMwEQYD\nVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ8wDQYDVQQK\nDAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTCCASIwDQYJKoZIhvcNAQEB\nBQADggEPADCCAQoCggEBAMIE7PiM7gTCs9hQ1XBYzJMY61yoaEmwIrX5lZ6xKyx2\nPmzAS2BMTOqytMAPgLaw+XLJhgL5XEFdEyt/ccRLvOmULlA3pmccYYz2QULFRtMW\nhyefdOsKnRFSJiFzbIRMeVXk0WvoBj1IFVKtsyjbqv9u/2CVSndrOfEk0TG23U3A\nxPxTuW1CrbV8/q71FdIzSOciccfCFHpsKOo3St/qbLVytH5aohbcabFXRNsKEqve\nww9HdFxBIuGa+RuT5q0iBikusbpJHAwnnqP7i/dAcgCskgjZjFeEU4EFy+b+a1SY\nQCeFxxC7c3DvaRhBB0VVfPlkPz0sw6l865MaTIbRyoUCAwEAAaMyMDAwCQYDVR0T\nBAIwADAjBgNVHREEHDAaggwqLmJhZHNzbC5jb22CCmJhZHNzbC5jb20wDQYJKoZI\nhvcNAQELBQADggEBALW4pad52T7VNw2nFMjPH98ZJNAQQgWyr3H2KlZN6IFGsonO\nnCC/Do8BPx6BnP3PFwovWMat1VvnRRoC8lw/30eEazWqBRGZWPz6LHTE3DNBJdc8\nxz6mh8q9RJX/PAj+YYGNElTu6qj49YT0BEhMF4U+dTQ0G8y3x4WNfiu9pGqyrp8d\nAzeidMfQ/pU01PpoPTDLvRDNkmMsABNE1fXBfJxDDGwfq1xY1j23Fm6BolwZC2y7\nn19h+vMYVWbGoovrf2/ibTvtcTyfDop7gl5Yy3OncZxokFj21rUZpLgx9ea4a9z3\nFzEz5ufynq03RhHTE1eu+gDzMEF0GNhGGsKqeA4=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/fixtures/keys/rsa_private_key_pkcs8.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDHaZKiFICB5Fbu\nkJ5Quzmj11SGXeEuwrbmmS/hC/ou2aTkwzTFfKmuOPLsigHhfufVIrGEk9vdMySq\n6qqGqB/w/LDtLxZNhlcgjjF1RVvmFpUA5rTtXv0NmRpvLN1dekSG9cELShKRS2HL\nk6XpfFw1hyxf9WBe0diRc7AvwiVJ/nsTZPigeuSA3JYnw5/g1AHl0NgeJTtiWv4m\n05LyoBOvUQUhC7rX7tC7JvrogvnO88jk+se4QQACNkeF/QiFApIbo1D0dW8Ac1vY\npjh8F5NvWpLuLK9pinQo1bZ2u7tc5BYk08CENKhYxFzeZ2BE517qaSRJoNLvpenH\nx1oXIg7fAgMBAAECggEAYGn4ZhogiezjZSQSD3l+ZGubp/2i/u9Q7Ex7fEVEuLst\nQRfqn2NnTN+nAFu3jhXENGY6Sx4MKzZrj6G3QjTugJ9EUeE22NPPs2NcoVUgGi6n\n61AggTYwho8UW1VnUCdqE5ClvfYZ5Rr71Sh1it7AXHcXKuwiJKY0Hhs/v8+QJOYk\ny36ze7ZIY9k1umKDnBKGwwcgChdXyyDWijYtF5oxtgQmjo88ehC6hD9Jtnlwjmd2\nRMIVWOVudJKeH4cT+uj0iKyJOPU8ajVN8AWulrHEKW+EsX2Zu9MArb5kcODH9nh3\n4o6lcGdcPbaqjRQ77zzA6RY9II6wO3yd1ED6B4rDnQKBgQD109GS5tZg3X8jJPkl\nsAUXrdibKfRK2pXgPvohHPf3r0i0cJ6ckcxFYHVUV5G36SzYKopSneCgkJsrqJiJ\npD+NmCpvlIo0M1tEyKLvLtUdXPkEl+EGs6lt8si2Hkh4T2FzJQN5hSCzcVKJ4/Xc\nt+OkUjuLJfBrVivKmkITRhxOXQKBgQDPqg4SRKuoziKf1VDCyxIJr71t7pXPBr6x\nSgaGHGttqqD/mNdA9qFh30AJtRVQfWHZPILBf+ivec4+hvjo2B4cShoA8rOCQfUN\nvZixc3y+0Jlj1SXgBFNdSk8FzglUUu9b6BW5yeHlpmmJbYInHAOWGBkfuDGE8AKP\nV3oqXqGmawKBgGH8k531q2AjCgltNG6EUhNVNXDr8TdhF7qx/6vxSxoMYXOjLGYG\na6D/yOTcnvXq2Pg1RLuXuLDn0yI86sh6kuaSz994GvqhufCZ9PBX/5TbuVrOW2D7\nfj6YNs75FTP3mEV4bIEkwpskQZ07I4ZeOjwGlzto3QM77uqsQEhEewX1AoGBAKJi\nWNSCLDU406xmUtuvjbBTYu5GpZCYtp7NwuI18O91gKW9r3yWHsX4nAu7NSqWkOAd\nSCXlTl+BAPy18IerD4iRjVn2btZJm0UeX/tK0l4nofqF3zMYTtPhWn+wiG0O2Srm\nBa8dJW69vUMAhcjtSASjXWoHT3mjcG0EO3xMOV13AoGBAKd4uL7YW09vcDBSfp5D\nhykQ8Qtqo/k0GA2x0waAmMoYWUGugdO6oBwB1roGcpR9ctCtyYMiLpYtQK2THL9V\njSEzKyBCU8RzCQSwyZ2rmr//jN7ztPasyGU2bbxEIQoNATxDRJXW1BrZ3OyTAbhF\n3BHaBNrexU/X3XnChxyuWQbs\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "tests/fixtures/responses/README.md",
    "content": "# Test Fixtures: Compressed Responses\n\n```sh\n$ echo \"Hello world\" > hello_world\n$ pigz hello_world # hello_world.gz\n\n$ echo \"Hello world\" > hello_world\n$ pigz -z hello_world # hello_world.zz\n\n$ echo \"Hello world\" > hello_world\n$ brotli hello_world # hello_world.br\n\n$ echo \"Hello world\" > hello_world\n$ zstd hello_world # hello_world.zst\n```\n"
  },
  {
    "path": "tests/server/mod.rs",
    "content": "// Copied from https://raw.githubusercontent.com/seanmonstar/reqwest/v0.12.0/tests/support/server.rs\n// with some slight tweaks\nuse std::convert::Infallible;\nuse std::future::Future;\nuse std::sync::mpsc as std_mpsc;\nuse std::sync::{Arc, Mutex};\nuse std::thread;\nuse std::time::Duration;\n\nuse http_body_util::Full;\nuse hyper::body::Bytes;\nuse hyper::service::service_fn;\nuse hyper::{Request, Response};\nuse hyper_util::rt::TokioIo;\nuse tokio::runtime;\nuse tokio::sync::oneshot;\n\ntype Body = Full<Bytes>;\ntype Builder = hyper_util::server::conn::auto::Builder<hyper_util::rt::TokioExecutor>;\n\nenum Listener {\n    TcpListener(tokio::net::TcpListener),\n    #[cfg(unix)]\n    UnixListener(tempfile::NamedTempFile<tokio::net::UnixListener>),\n}\n\npub struct Server {\n    listener: Arc<Listener>,\n    panic_rx: std_mpsc::Receiver<()>,\n    successful_hits: Arc<Mutex<u8>>,\n    total_hits: Arc<Mutex<u8>>,\n    no_hit_checks: bool,\n    shutdown_tx: Option<oneshot::Sender<()>>,\n}\n\nimpl Server {\n    pub fn base_url(&self) -> String {\n        match &*self.listener {\n            Listener::TcpListener(l) => {\n                let addr = l.local_addr().unwrap();\n                match addr.ip() {\n                    std::net::IpAddr::V6(_) => format!(\"http://[{}]:{}\", addr.ip(), addr.port()),\n                    std::net::IpAddr::V4(_) => format!(\"http://{}:{}\", addr.ip(), addr.port()),\n                }\n            }\n            #[cfg(unix)]\n            _ => panic!(\"no base_url for unix server\"),\n        }\n    }\n\n    pub fn url(&self, path: &str) -> String {\n        match &*self.listener {\n            Listener::TcpListener(l) => {\n                let addr = l.local_addr().unwrap();\n                match addr.ip() {\n                    std::net::IpAddr::V6(_) => {\n                        format!(\"http://[{}]:{}{}\", addr.ip(), addr.port(), path)\n                    }\n                    std::net::IpAddr::V4(_) => {\n                        format!(\"http://{}:{}{}\", addr.ip(), addr.port(), path)\n                    }\n                }\n            }\n            #[cfg(unix)]\n            _ => panic!(\"no url for unix server\"),\n        }\n    }\n\n    pub fn host(&self) -> String {\n        match &*self.listener {\n            Listener::TcpListener(l) => match l.local_addr().unwrap().ip() {\n                std::net::IpAddr::V6(addr) => addr.to_string(),\n                std::net::IpAddr::V4(addr) => addr.to_string(),\n            },\n            #[cfg(unix)]\n            _ => panic!(\"no host for unix server\"),\n        }\n    }\n\n    #[cfg(unix)]\n    pub fn socket_path(&self) -> std::path::PathBuf {\n        match &*self.listener {\n            Listener::UnixListener(l) => l\n                .as_file()\n                .local_addr()\n                .unwrap()\n                .as_pathname()\n                .unwrap()\n                .to_path_buf(),\n            _ => panic!(\"no socket_path for tcp server\"),\n        }\n    }\n\n    pub fn port(&self) -> u16 {\n        match &*self.listener {\n            Listener::TcpListener(l) => l.local_addr().unwrap().port(),\n            #[cfg(unix)]\n            _ => panic!(\"no port for unix server\"),\n        }\n    }\n\n    pub fn assert_hits(&self, hits: u8) {\n        assert_eq!(*self.successful_hits.lock().unwrap(), hits);\n    }\n\n    pub fn disable_hit_checks(&mut self) {\n        self.no_hit_checks = true;\n    }\n}\n\nimpl Drop for Server {\n    fn drop(&mut self) {\n        if let Some(tx) = self.shutdown_tx.take() {\n            let _ = tx.send(());\n        }\n\n        if !std::thread::panicking() && !self.no_hit_checks {\n            let total_hits = *self.total_hits.lock().unwrap();\n            let successful_hits = *self.successful_hits.lock().unwrap();\n            let failed_hits = total_hits - successful_hits;\n            assert!(total_hits > 0, \"test server exited without being called\");\n            assert_eq!(\n                failed_hits, 0,\n                \"numbers of panicked or in-progress requests: {failed_hits}\"\n            );\n        }\n\n        if !std::thread::panicking() {\n            self.panic_rx\n                .recv_timeout(Duration::from_secs(3))\n                .expect(\"test server should not panic\");\n        }\n    }\n}\n\n// http() is generic, http_inner() is not.\n// A generic function has to be compiled for every single type you use it with.\n// And every closure counts as a different type.\n// By making only http() generic a rebuild of the tests take 3-10 times less long.\n\npub fn http<F, Fut>(func: F) -> Server\nwhere\n    F: Fn(Request<hyper::body::Incoming>) -> Fut + Send + Sync + 'static,\n    Fut: Future<Output = Response<Body>> + Send + 'static,\n{\n    http_inner(\n        Arc::new(move |req| Box::new(Box::pin(func(req)))),\n        false,\n        None,\n    )\n}\n\n#[cfg(unix)]\npub fn http_unix<F, Fut>(func: F) -> Server\nwhere\n    F: Fn(Request<hyper::body::Incoming>) -> Fut + Send + Sync + 'static,\n    Fut: Future<Output = Response<Body>> + Send + 'static,\n{\n    http_inner(\n        Arc::new(move |req| Box::new(Box::pin(func(req)))),\n        true,\n        None,\n    )\n}\n\n#[cfg(feature = \"http-message-signatures\")]\npub fn http_v6<F, Fut>(func: F) -> Option<Server>\nwhere\n    F: Fn(Request<hyper::body::Incoming>) -> Fut + Send + Sync + 'static,\n    Fut: Future<Output = Response<Body>> + Send + 'static,\n{\n    let addr = std::net::SocketAddr::from((std::net::Ipv6Addr::LOCALHOST, 0));\n    if std::net::TcpListener::bind(addr).is_err() {\n        return None;\n    }\n\n    Some(http_inner(\n        Arc::new(move |req| Box::new(Box::pin(func(req)))),\n        false,\n        Some(addr),\n    ))\n}\n\ntype Serv = dyn Fn(Request<hyper::body::Incoming>) -> Box<ServFut> + Send + Sync;\ntype ServFut = dyn Future<Output = Response<Body>> + Send + Unpin;\n\nfn http_inner(\n    func: Arc<Serv>,\n    use_unix_socket: bool,\n    addr: Option<std::net::SocketAddr>,\n) -> Server {\n    // Spawn new runtime in thread to prevent reactor execution context conflict\n    thread::spawn(move || {\n        let rt = runtime::Builder::new_current_thread()\n            .enable_all()\n            .build()\n            .expect(\"new rt\");\n        let successful_hits = Arc::new(Mutex::new(0));\n        let total_hits = Arc::new(Mutex::new(0));\n\n        let listener = Arc::new(rt.block_on(async move {\n            if use_unix_socket {\n                #[cfg(not(unix))]\n                {\n                    panic!(\"unix server not supported\")\n                }\n                #[cfg(unix)]\n                {\n                    tempfile::Builder::new()\n                        .make(|path| tokio::net::UnixListener::bind(path))\n                        .map(Listener::UnixListener)\n                        .unwrap()\n                }\n            } else {\n                let addr = addr.unwrap_or(std::net::SocketAddr::from(([127, 0, 0, 1], 0)));\n                tokio::net::TcpListener::bind(&addr)\n                    .await\n                    .map(Listener::TcpListener)\n                    .unwrap()\n            }\n        }));\n\n        let (shutdown_tx, shutdown_rx) = oneshot::channel();\n        let (panic_tx, panic_rx) = std_mpsc::channel();\n        let thread_name = format!(\n            \"test({})-support-server\",\n            thread::current().name().unwrap_or(\"<unknown>\")\n        );\n\n        {\n            let successful_hits = successful_hits.clone();\n            let total_hits = total_hits.clone();\n            let listener = listener.clone();\n            thread::Builder::new()\n                .name(thread_name)\n                .spawn(move || {\n                    let task = rt.spawn(async move {\n                        let builder = Builder::new(hyper_util::rt::TokioExecutor::new());\n                        loop {\n                            let svc = {\n                                let func = func.clone();\n                                let successful_hits = successful_hits.clone();\n                                let total_hits = total_hits.clone();\n\n                                service_fn(move |req| {\n                                    let successful_hits = successful_hits.clone();\n                                    let total_hits = total_hits.clone();\n                                    let fut = func(req);\n                                    async move {\n                                        *total_hits.lock().unwrap() += 1;\n                                        let res = fut.await;\n                                        *successful_hits.lock().unwrap() += 1;\n                                        Ok::<_, Infallible>(res)\n                                    }\n                                })\n                            };\n\n                            let builder = builder.clone();\n\n                            match &*listener {\n                                Listener::TcpListener(listener) => {\n                                    let (io, _) = listener.accept().await.unwrap();\n                                    tokio::spawn(async move {\n                                        let _ =\n                                            builder.serve_connection(TokioIo::new(io), svc).await;\n                                    });\n                                }\n                                #[cfg(unix)]\n                                Listener::UnixListener(listener) => {\n                                    let (io, _) = listener.as_file().accept().await.unwrap();\n                                    tokio::spawn(async move {\n                                        let _ =\n                                            builder.serve_connection(TokioIo::new(io), svc).await;\n                                    });\n                                }\n                            };\n                        }\n                    });\n                    let _ = rt.block_on(shutdown_rx);\n                    task.abort();\n                    let _ = panic_tx.send(());\n                })\n                .expect(\"thread spawn\");\n        }\n        Server {\n            listener,\n            panic_rx,\n            shutdown_tx: Some(shutdown_tx),\n            successful_hits,\n            total_hits,\n            no_hit_checks: false,\n        }\n    })\n    .join()\n    .unwrap()\n}\n"
  }
]