[
  {
    "path": ".cargo/config.toml",
    "content": "[target.x86_64-pc-windows-msvc]\nrustflags = [\"-C\", \"target-feature=+crt-static\"]\n\n[target.i686-pc-windows-msvc]\nrustflags = [\"-C\", \"target-feature=+crt-static\"]\n"
  },
  {
    "path": ".dockerignore",
    "content": "target\n"
  },
  {
    "path": ".editorconfig",
    "content": "[*.rs]\nindent_style = space\nindent_size = 4\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: svenstaro\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: cargo\n    directory: \"/\"\n    schedule:\n      interval: monthly\n    groups:\n      all-dependencies:\n        patterns:\n          - \"*\"\n"
  },
  {
    "path": ".github/workflows/build-release.yml",
    "content": "name: Build/publish release\n\non: [push, pull_request]\n\njobs:\n  publish:\n    name: Binary ${{ matrix.target }} (on ${{ matrix.os }})\n    runs-on: ${{ matrix.os }}\n    outputs:\n      version: ${{ steps.extract_version.outputs.version }}\n    strategy:\n      matrix:\n        include:\n          - os: ubuntu-latest\n            target: x86_64-unknown-linux-musl\n            compress: true\n            cargo_flags: \"\"\n          - os: ubuntu-latest\n            target: x86_64-unknown-linux-gnu\n            compress: true\n            cargo_flags: \"\"\n          - os: ubuntu-latest\n            target: aarch64-unknown-linux-musl\n            compress: true\n            cargo_flags: \"\"\n          - os: ubuntu-latest\n            target: aarch64-unknown-linux-gnu\n            compress: true\n            cargo_flags: \"\"\n          - os: ubuntu-latest\n            target: armv7-unknown-linux-musleabihf\n            compress: true\n            cargo_flags: \"\"\n          - os: ubuntu-latest\n            target: armv7-unknown-linux-gnueabihf\n            compress: true\n            cargo_flags: \"\"\n          - os: ubuntu-latest\n            target: arm-unknown-linux-musleabihf\n            compress: true\n            cargo_flags: \"\"\n          - os: ubuntu-latest\n            target: riscv64gc-unknown-linux-gnu\n            compress: false\n            cargo_flags: \"--no-default-features\"\n          - os: windows-latest\n            target: x86_64-pc-windows-msvc\n            compress: true\n            cargo_flags: \"\"\n          - os: windows-latest\n            target: i686-pc-windows-msvc\n            compress: true\n            cargo_flags: \"\"\n          - os: macos-latest\n            target: x86_64-apple-darwin\n            compress: false\n            cargo_flags: \"\"\n          - os: macos-latest\n            target: aarch64-apple-darwin\n            compress: false\n            cargo_flags: \"\"\n          - os: ubuntu-latest\n            target: x86_64-unknown-freebsd\n            compress: false\n            cargo_flags: \"\"\n          - os: ubuntu-latest\n            target: x86_64-unknown-illumos\n            compress: false\n            cargo_flags: \"\"\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v5\n\n      - name: Setup Rust toolchain\n        uses: dtolnay/rust-toolchain@stable\n\n      - run: sudo apt install musl-tools\n        if: startsWith(matrix.os, 'ubuntu')\n\n      - name: cargo build\n        uses: houseabsolute/actions-rust-cross@v0\n        with:\n          command: build\n          args: --release --locked ${{ matrix.cargo_flags }}\n          target: ${{ matrix.target }}\n\n      - name: Set exe extension for Windows\n        run: echo \"EXE=.exe\" >> $env:GITHUB_ENV\n        if: startsWith(matrix.os, 'windows')\n\n      - name: Compress binaries\n        uses: svenstaro/upx-action@v2\n        with:\n          files: target/${{ matrix.target }}/release/miniserve${{ env.EXE }}\n          args: --best --lzma\n          strip: false  # We're stripping already in Cargo.toml\n        if: ${{ matrix.compress }}\n\n      - name: Upload artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: ${{ matrix.target }}\n          path: target/${{ matrix.target }}/release/miniserve${{ env.EXE }}\n\n      - name: Get version from tag\n        id: extract_version\n        run: |\n          echo \"version=${GITHUB_REF_NAME#v}\" >> \"$GITHUB_OUTPUT\"\n        shell: bash\n\n      - name: Install CHANGELOG parser\n        uses: taiki-e/install-action@parse-changelog\n\n      - name: Get CHANGELOG entry\n        run: parse-changelog CHANGELOG.md ${{ steps.extract_version.outputs.version }} | tee changelog_entry\n        if: startsWith(github.ref_name, 'v') && github.ref_type == 'tag'\n        shell: bash\n\n      - name: Read changelog entry from file\n        id: changelog_entry\n        uses: juliangruber/read-file-action@v1\n        with:\n          path: ./changelog_entry\n        if: startsWith(github.ref_name, 'v') && github.ref_type == 'tag'\n\n      - name: Release\n        uses: svenstaro/upload-release-action@v2\n        with:\n          repo_token: ${{ secrets.GITHUB_TOKEN }}\n          file: target/${{ matrix.target }}/release/miniserve${{ env.EXE }}\n          tag: ${{ github.ref_name }}\n          asset_name: miniserve-${{ steps.extract_version.outputs.version }}-${{ matrix.target }}${{ env.EXE }}\n          body: ${{ steps.changelog_entry.outputs.content }}\n        if: startsWith(github.ref_name, 'v') && github.ref_type == 'tag'\n\n  container-images:\n    name: Publish images\n    runs-on: ubuntu-latest\n    needs: publish\n    # Run for tags and pushes to the default branch\n    if: (startsWith(github.ref_name, 'v') && github.ref_type == 'tag') || github.event.repository.default_branch == github.ref_name\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v5\n\n      - name: Download artifact aarch64-unknown-linux-gnu\n        uses: actions/download-artifact@v4\n        with:\n          name: aarch64-unknown-linux-gnu\n          path: target/aarch64-unknown-linux-gnu/release\n\n      - name: Download artifact x86_64-unknown-linux-gnu\n        uses: actions/download-artifact@v4\n        with:\n          name: x86_64-unknown-linux-gnu\n          path: target/x86_64-unknown-linux-gnu/release\n\n      - name: Download artifact armv7-unknown-linux-gnueabihf\n        uses: actions/download-artifact@v4\n        with:\n          name: armv7-unknown-linux-gnueabihf\n          path: target/armv7-unknown-linux-gnueabihf/release\n\n      - name: Download artifact aarch64-unknown-linux-musl\n        uses: actions/download-artifact@v4\n        with:\n          name: aarch64-unknown-linux-musl\n          path: target/aarch64-unknown-linux-musl/release\n\n      - name: Download artifact x86_64-unknown-linux-musl\n        uses: actions/download-artifact@v4\n        with:\n          name: x86_64-unknown-linux-musl\n          path: target/x86_64-unknown-linux-musl/release\n\n      - name: Download artifact armv7-unknown-linux-musleabihf\n        uses: actions/download-artifact@v4\n        with:\n          name: armv7-unknown-linux-musleabihf\n          path: target/armv7-unknown-linux-musleabihf/release\n\n      - name: podman login\n        run: podman login --username ${{ secrets.DOCKERHUB_USERNAME }} --password ${{ secrets.DOCKERHUB_TOKEN }} docker.io\n\n      - name: podman build linux/arm64\n        run: podman build --format docker --platform linux/arm64/v8 --manifest miniserve -f Containerfile target/aarch64-unknown-linux-gnu/release\n\n      - name: podman build linux/amd64\n        run: podman build --format docker --platform linux/amd64 --manifest miniserve -f Containerfile target/x86_64-unknown-linux-gnu/release\n\n      - name: podman build linux/arm\n        run: podman build --format docker --platform linux/arm/v7 --manifest miniserve -f Containerfile target/armv7-unknown-linux-gnueabihf/release\n\n      - name: podman manifest push latest\n        run: podman manifest push miniserve docker.io/svenstaro/miniserve:latest\n\n      - name: podman manifest push tag version\n        run: podman manifest push miniserve docker.io/svenstaro/miniserve:${{ needs.publish.outputs.version }}\n        if: startsWith(github.ref_name, 'v')\n\n      - name: podman build linux/arm64 (alpine edition)\n        run: podman build --format docker --platform linux/arm64/v8 --manifest miniserve-alpine -f Containerfile.alpine target/aarch64-unknown-linux-musl/release\n\n      - name: podman build linux/amd64 (alpine edition)\n        run: podman build --format docker --platform linux/amd64 --manifest miniserve-alpine -f Containerfile.alpine target/x86_64-unknown-linux-musl/release\n\n      - name: podman build linux/arm (alpine edition)\n        run: podman build --format docker --platform linux/arm/v7 --manifest miniserve-alpine -f Containerfile.alpine target/armv7-unknown-linux-musleabihf/release\n\n      - name: podman manifest push latest (alpine edition)\n        run: podman manifest push miniserve-alpine docker.io/svenstaro/miniserve:alpine\n\n      - name: podman manifest push tag version (alpine edition)\n        run: podman manifest push miniserve-alpine docker.io/svenstaro/miniserve:${{ needs.publish.outputs.version }}-alpine\n        if: startsWith(github.ref_name, 'v')\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non: [push, pull_request]\n\njobs:\n  ci:\n    name: ${{ matrix.os }}\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v5\n\n      - name: Setup Rust toolchain\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          components: rustfmt, clippy\n\n      - name: cargo build\n        run: cargo build\n\n      - name: cargo test\n        run: cargo test -- --test-threads 1\n\n      - name: cargo fmt\n        run: cargo fmt --all -- --check\n\n      - name: cargo clippy\n        run: cargo clippy -- -D warnings\n"
  },
  {
    "path": ".gitignore",
    "content": "# Generated by Cargo\n# will have compiled files and executables\n/target/\n\n# These are backup files generated by rustfmt\n**/*.rs.bk\n\n# Editor-specific ignores\n.idea/\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](http://keepachangelog.com/)\nand this project adheres to [Semantic Versioning](http://semver.org/).\n\n<!-- next-header -->\n\n## [Unreleased] - ReleaseDate\n- Add Ayu Dark theme [#1551](https://github.com/svenstaro/miniserve/pull/1551) (thanks @rysb-dev)\n\n## [0.33.0] - 2026-02-16\n- Add `--log-color` to explicitly control when to print colors [#1529](https://github.com/svenstaro/miniserve/pull/1529) (thanks @MrCroxx)\n- Fix `--rm-files` not working with `--route-preifx` [#1531](https://github.com/svenstaro/miniserve/pull/1531) (thanks @Jianchi-Chen)\n- Add illumos builds [#1543](https://github.com/svenstaro/miniserve/pull/1543)\n- Ignore failure on file hash calculation during upload [#1542](https://github.com/svenstaro/miniserve/pull/1542) (thanks @outloudvi)\n- Add `--quiet` flag to reduce output and silence warnings [#1539](https://github.com/svenstaro/miniserve/pull/1539) (thanks @joshleeb)\n- Enable some markdown rendering extensions [#1545](https://github.com/svenstaro/miniserve/pull/1545) (thanks @duskmoon314)\n- Add `--pastebin` flag to enable pastebin form [#1546](https://github.com/svenstaro/miniserve/pull/1546) (thanks @rktjmp)\n- Always use forward slashes in zip files [#1534](https://github.com/svenstaro/miniserve/pull/1534) (thanks @pzhlkj6612)\n\n## [0.32.0] - 2025-09-17\n- Skip hash calculation when crypto.subtle is not available [#1507](https://github.com/svenstaro/miniserve/pull/1507) (thanks @outloudvi)\n- Fix hostname:port getting repeated in wget footer if TLS is used [#1515](https://github.com/svenstaro/miniserve/pull/1515) (thanks @xtay573269555)\n- Add `--chmod` to set file mode after upload on unix [#1506](https://github.com/svenstaro/miniserve/pull/1506) (thanks @lilydjwg)\n- Add `--rm-files` to allow file deletion [#1518](https://github.com/svenstaro/miniserve/pull/1518) (thanks @NOBLESSE and @cyqsimon)\n\n## [0.31.0] - 2025-06-27\n- Fix filtering symlinks when hosting WebDAV [#1502](https://github.com/svenstaro/miniserve/pull/1502) (thanks @ahti)\n- Enable renaming during file upload if duplicate exists [#1453](https://github.com/svenstaro/miniserve/pull/1453) (thanks @Atreyagaurav)\n\n## [0.30.0] - 2025-06-26\n- Add `--file-external-url` to generate links pointing to another server [#1492](https://github.com/svenstaro/miniserve/pull/1492) (thanks @jankeymeulen)\n- Add date pill and sort links for mobile views [#1473](https://github.com/svenstaro/miniserve/pull/1473) (thanks @Flat)\n- Add upload progress bar and allow for multiple concurrent file uploads [#1431](https://github.com/svenstaro/miniserve/pull/1431) (thanks @AlecDivito)\n- Add `--size-display` to allow for toggling file size display between `human` and `exact` [#1261](https://github.com/svenstaro/miniserve/pull/1261) (thanks @Lzzzzzt)\n- Add well-known healthcheck route at `/__miniserve_internal/healthcheck` (of `/<prefix>/__miniserve_internal/healthcheck` when using `--route-prefix`)\n- Add asynchronous recursive directory size counting [#1482](https://github.com/svenstaro/miniserve/pull/1482)\n- Add link to miniserve GitHub page to footer\n- Add `--directory-size` flag to enable directory size counting\n- Fix --no-symlinks not filtering files and dirs nested in symlinks [#1495](https://github.com/svenstaro/miniserve/pull/1495) (thanks @ahti)\n\n## [0.29.0] - 2025-02-06\n- Make URL encoding fully WHATWG-compliant [#1454](https://github.com/svenstaro/miniserve/pull/1454) (thanks @cyqsimon)\n- Fix `OVERWRITE_FILES` env var not being prefixed by `MINISERVE_` [#1457](https://github.com/svenstaro/miniserve/issues/1457)\n- Change `font-weight` of regular files to be `normal` to improve readability [#1471](https://github.com/svenstaro/miniserve/pull/1471) (thanks @shaicoleman)\n- Add webdav support [#1415](https://github.com/svenstaro/miniserve/pull/1415) (thanks @ahti)\n- Move favicon and css to stable, non-random routes [#1472](https://github.com/svenstaro/miniserve/pull/1472) (thanks @ahti)\n\n## [0.28.0] - 2024-09-12\n- Fix wrapping text in mobile view when the file name too long [#1379](https://github.com/svenstaro/miniserve/pull/1379) (thanks @chaibiq)\n- Fix missing drag-form when dragging file in to browser [#1390](https://github.com/svenstaro/miniserve/pull/1390) (thanks @chaibiq)\n- Improve documentation for the --header parameter [#1389](https://github.com/svenstaro/miniserve/pull/1389) (thanks @orwithout)\n- Don't show mkdir option when the directory is not upload allowed [#1442](https://github.com/svenstaro/miniserve/pull/1442) (thanks @Atreyagaurav)\n\n## [0.27.1] - 2024-03-16\n- Add `Add file and folder symbols` [#1365](https://github.com/svenstaro/miniserve/pull/1365) (thanks @chaibiq)\n\n## [0.27.0] - 2024-03-16\n- Add `-C/--compress-response` to enable response compression [1315](https://github.com/svenstaro/miniserve/pull/1315) (thanks @zuisong)\n- Refactor errors [#1331](https://github.com/svenstaro/miniserve/pull/1331) (thanks @cyqsimon)\n- Add `-I/--disable-inexing` [#1329](https://github.com/svenstaro/miniserve/pull/1329) (thanks @dyc3)\n\n## [0.26.0] - 2024-01-13\n- Properly handle read-only errors on Windows [#1310](https://github.com/svenstaro/miniserve/pull/1310) (thanks @ViRb3)\n- Use `tokio::fs` instead of `std::fs` to enable async file operations [#445](https://github.com/svenstaro/miniserve/issues/445)\n- Add `-S`/`--default-sorting-method` and `-O`/`--default-sorting-order` flags [#1308](https://github.com/svenstaro/miniserve/pull/1308) (thanks @ElliottLandsborough)\n\n## [0.25.0] - 2024-01-07\n- Add `--pretty-urls` [#1193](https://github.com/svenstaro/miniserve/pull/1193) (thanks @nlopes)\n- Fix single quote display with `--show-wget-footer` [#1191](https://github.com/svenstaro/miniserve/pull/1191) (thanks @d-air1)\n- Remove header Content-Encoding when archiving [#1290](https://github.com/svenstaro/miniserve/pull/1290) (thanks @5long)\n- Prevent illegal request path from crashing program [#1285](https://github.com/svenstaro/miniserve/pull/1285) (thanks @cyqsimon)\n- Fixed issue where serving files with a newline would fail [#1294](https://github.com/svenstaro/miniserve/issues/1294)\n\n## [0.24.0] - 2023-07-06\n- Fix ANSI color codes are printed when not a tty [#1095](https://github.com/svenstaro/miniserve/pull/1095)\n- Allow parameters to be provided via environment variables [#1160](https://github.com/svenstaro/miniserve/pull/1160)\n\n## [0.23.2] - 2023-04-28\n- Build Windows build with static CRT [#1107](https://github.com/svenstaro/miniserve/pull/1107)\n\n## [0.23.1] - 2023-04-17\n- Add EC key support [#1080](https://github.com/svenstaro/miniserve/issues/1080)\n\n## [0.23.0] - 2023-03-01\n- Update to clap v4\n- Show localized datetime [#949](https://github.com/svenstaro/miniserve/pull/949) (thanks @IvkinStanislav)\n- Fix sorting breaks subdir downloading [#991](https://github.com/svenstaro/miniserve/pull/991) (thanks @Vam-Jam)\n- Fix wget footer [#1043](https://github.com/svenstaro/miniserve/pull/1043) (thanks @Yusuto)\n\n## [0.22.0] - 2022-09-20\n- Faster QR code generation [#848](https://github.com/svenstaro/miniserve/pull/848) (thanks @cyqsimon)\n- Make `--readme` support not only `README.md` but also `README` and `README.txt` rendered as\n  plaintext [#911](https://github.com/svenstaro/miniserve/pull/911) (thanks @Atreyagaurav)\n- Change `-u/--upload-files` slightly in the sense that it can now either be provided by itself as\n  before or receive a file path to restrict uploading to only that path. Can be provided multiple\n  times for multiple allowed paths [#858](https://github.com/svenstaro/miniserve/pull/858) (thanks\n  @jonasdiemer)\n\n## [0.21.0] - 2022-09-15\n- Fix bug where static files would be served incorrectly when using `--random-route` [#835](https://github.com/svenstaro/miniserve/pull/835) (thanks @solarknight)\n- Add `--readme` to render the README in the current directory after the file listing [#860](https://github.com/svenstaro/miniserve/pull/860) (thanks @Atreyagaurav)\n- Add more architectures (and also additional container images)\n\n## [0.20.0] - 2022-06-26\n- Fixed security issue where it was possible to upload files to locations pointed to by symlinks\n  even when symlinks were disabled [#781](https://github.com/svenstaro/miniserve/pull/781) (thanks @sheepy0125)\n- Added `--hide-theme-selector` flag to hide the theme selector functionality in the frontend [#805](https://github.com/svenstaro/miniserve/pull/805https://github.com/svenstaro/miniserve/pull/805) (thanks @flamingoodev)\n- Added `--mkdir` flag to allow for uploading directories [#781](https://github.com/svenstaro/miniserve/pull/781) (thanks @sheepy0125)\n\n## [0.19.5] - 2022-05-18\n- Fix security issue where `--no-symlinks` would only hide symlinks from listing but it would\n  still be possible to follow them if the path was known\n\n## [0.19.4] - 2022-04-02\n- Fix random route leaking on error pages [#764](https://github.com/svenstaro/miniserve/pull/764) (thanks @steffhip)\n\n## [0.19.3] - 2022-03-15\n- Allow to set the accept input attribute to arbitrary values using `-m` and `-M` [#755](https://github.com/svenstaro/miniserve/pull/755) (thanks @mayjs)\n\n## [0.19.2] - 2022-02-21\n- Add man page support via `--print-manpage` [#738](https://github.com/svenstaro/miniserve/pull/738)\n\n## [0.19.1] - 2022-02-16\n- Better MIME type guessing support due to updated mime_guess\n\n## [0.19.0] - 2022-02-06\n- Fix panic when using TLS in some instances [#670](https://github.com/svenstaro/miniserve/issues/670) (thanks @aliemjay)\n- Add `--route-prefix` to add a fixed route prefix [#728](https://github.com/svenstaro/miniserve/pull/728) (thanks @aliemjay and @Jikstra)\n- Allow tapping the whole row in mobile view [#729](https://github.com/svenstaro/miniserve/pull/729)\n\n## [0.18.0] - 2021-10-26\n- Add raw mode and raw mode footer display [#508](https://github.com/svenstaro/miniserve/pull/508) (thanks @Jikstra)\n- Add SPA mode [#515](https://github.com/svenstaro/miniserve/pull/515) (thanks @sinking-point)\n\n## [0.17.0] - 2021-09-04\n- Print QR codes on terminal [#524](https://github.com/svenstaro/miniserve/pull/524) (thanks @aliemjay)\n- Fix mobile layout info pills taking whole width [#591](https://github.com/svenstaro/miniserve/issues/591)\n- Fix security exploit when uploading is enabled [#590](https://github.com/svenstaro/miniserve/pull/590) [#518](https://github.com/svenstaro/miniserve/issues/518) (thanks @aliemjay)\n- Fix uploading to symlink directories [#590](https://github.com/svenstaro/miniserve/pull/590) [#466](https://github.com/svenstaro/miniserve/issues/466) (thanks @aliemjay)\n\n## [0.16.0] - 2021-08-31\n- Fix serving files with backslashes in their names [#578](https://github.com/svenstaro/miniserve/pull/578) (thanks @Jikstra)\n- Fix behavior of downloading symlinks by upgrading to actix-web 4 [#582](https://github.com/svenstaro/miniserve/pull/582) [#462](https://github.com/svenstaro/miniserve/issues/462) (thanks @aliemjay)\n- List directory if index file not found [#583](https://github.com/svenstaro/miniserve/pull/583) [#275](https://github.com/svenstaro/miniserve/pull/583) (thanks @aliemjay)\n- Add special colors for visited links [#521](https://github.com/svenstaro/miniserve/pull/521) (thanks @raffomania)\n- Switch from structopt to clap v3 [#587](https://github.com/svenstaro/miniserve/pull/587)\n\n  This enables slightly nicer help output as well as much better completions.\n- Fix network interface handling [#500](https://github.com/svenstaro/miniserve/pull/500) [#470](https://github.com/svenstaro/miniserve/issues/470) [#405](https://github.com/svenstaro/miniserve/issues/405) [#422](https://github.com/svenstaro/miniserve/issues/422) (thanks @aliemjay)\n- Implement show symlink destination [#542](https://github.com/svenstaro/miniserve/pull/542) [#499](https://github.com/svenstaro/miniserve/issues/499) (thanks @deantvv)\n- Fix error page not being correctly themed [#529](https://github.com/svenstaro/miniserve/pull/529) [#588](https://github.com/svenstaro/miniserve/issues/588) (@aliemjay)\n\n## [0.15.0] - 2021-08-27\n- Add hardened systemd template unit file to `packaging/miniserve@.service`\n- Fix qrcodegen dependency problem [#568](https://github.com/svenstaro/miniserve/issues/568)\n- Remove animation on QR code hover (it was kind of annoying as it makes things less snappy)\n- Add TLS support [#576](https://github.com/svenstaro/miniserve/pull/576)\n\n## [0.14.0] - 2021-04-18\n- Fix breadcrumbs for right-to-left languages [#489](https://github.com/svenstaro/miniserve/pull/489) (thanks @aliemjay)\n- Fix URL percent encoding for special characters [#485](https://github.com/svenstaro/miniserve/pull/485) (thanks @aliemjay)\n- Wrap breadcrumbs at any char [#496](https://github.com/svenstaro/miniserve/pull/496) (thanks @aliemjay)\n- Add separate flags for compressed and uncompressed tar archives [#492](https://github.com/svenstaro/miniserve/pull/492) (thanks @deantvv)\n- Bump deps\n- Fix Firefox becoming confused when opening a `.gz` file directly [#160](https://github.com/svenstaro/miniserve/issues/160)\n- Prefer UTF8 for text responses [#263](https://github.com/svenstaro/miniserve/issues/263)\n- Resolve symlinks on directory listing [#479](https://github.com/svenstaro/miniserve/pull/479) (thanks @aliemjay)\n\n## [0.13.0] - 2021-03-28\n- Change default log level to `Warn`\n- Change some messages a bit to be more clear\n- Add `--print-completions` to print shell completions for various supported shells [#482](https://github.com/svenstaro/miniserve/pull/482) (thanks @rouge8)\n- Don't print some messages if not attached to an interactive terminal\n- Refuse to start if not attached to interactive terminal and no explicit path is provided\n\n  This is a security consideration as you wouldn't want to run miniserve without an explicit path\n  as a service. You could end up serving `/` or `/root` in case those working directories are set.\n\n## [0.12.1] - 2021-03-27\n- Fix QR code not showing when using both `--random-route` and `--qrcode` [#480](https://github.com/svenstaro/miniserve/pull/480) (thanks @rouge8)\n- Add FreeBSD binaries\n\n## [0.12.0] - 2021-03-20\n- Add option `-H`/`--hidden` to show hidden files\n- Start instantly in case an explicit index is chosen\n- Fix DoS issue when deliberately sending unconforming URL paths\n- Add footer [#456](https://github.com/svenstaro/miniserve/pull/456) (thanks @levaitamas)\n- Switched from failure to thiserror for error handling\n\n## [0.11.0] - 2021-02-28\n- Add binaries for more architectures\n- Upgrade lockfile which fixes some security issues\n- Allow multiple file upload [#434](https://github.com/svenstaro/miniserve/pull/434) (thanks @mhuesch)\n- Allow for setting custom headers via `--header` [#452](https://github.com/svenstaro/miniserve/pull/452) (thanks @deantvv)\n\n## [0.10.4] - 2021-01-05\n- Add `--dirs-first`/`-D` option to list directories first [#423](https://github.com/svenstaro/miniserve/pull/423) (thanks @levaitamas)\n\n## [0.10.3] - 2020-11-09\n- Actually fix publish workflow\n\n## [0.10.2] - 2020-11-09\n- Fix publish workflow\n\n## [0.10.1] - 2020-11-09\n- Now compiles on stable! :D\n\n## [0.10.0] - 2020-10-02\n- Add embedded favicon [#364](https://github.com/svenstaro/miniserve/issues/364)\n- Add `--title` option which can be used to set the page title [#378](https://github.com/svenstaro/miniserve/pull/378) (thanks @ahti)\n- Default title is now the same host received in the request [#378](https://github.com/svenstaro/miniserve/pull/378) (thanks @ahti)\n- Client-side color-scheme handling [#380](https://github.com/svenstaro/miniserve/pull/380) (thanks @ahti)\n\n## [0.9.0] - 2020-09-16\n- Added prebuilt binaries for AARCH64, ARMv7, and ARM [#350](https://github.com/svenstaro/miniserve/pull/350)\n- Remove percent-encoding in heading and title [#362](https://github.com/svenstaro/miniserve/pull/362) (thanks @ahti)\n- Make name ordering case-insensitive [#362](https://github.com/svenstaro/miniserve/pull/362) (thanks @ahti)\n- Give name column more space [#362](https://github.com/svenstaro/miniserve/pull/362) (thanks @ahti)\n- Fix double-escaping [#354](https://github.com/svenstaro/miniserve/issues/354)\n- Upgrade to actix-web 3.0\n- Fix time display for files created \"now\" [#373](https://github.com/svenstaro/miniserve/pull/373) (thanks @imp and @KevCui)\n\n## [0.8.0] - 2020-07-22\n- Accept port 0 to find a random free port and use that [#327](https://github.com/svenstaro/miniserve/pull/327) (thanks @parrotmac)\n- Show QR code in interface [#330](https://github.com/svenstaro/miniserve/pull/330) (thanks @wyhaya)\n- Ported to actix-web 2 and futures 0.3 [#343](https://github.com/svenstaro/miniserve/pull/343) (thanks @equal-l2)\n\n## [0.7.0] - 2020-05-14\n- Add zip archiving [#297](https://github.com/svenstaro/miniserve/pull/297) (thanks @marawan31)\n\n## [0.6.0] - 2020-03-14\n- Add option to disable archives [#235](https://github.com/svenstaro/miniserve/pull/235) (thanks @DamianX)\n- Fix minor bug when using `--random-route` [#219](https://github.com/svenstaro/miniserve/pull/219)\n- Add a default index serving option [#189](https://github.com/svenstaro/miniserve/pull/189)\n\n## [0.5.0] - 2019-06-24\n- Add streaming download of tar archives (thanks @gyscos)\n- Add support for hashed passwords (thanks @KSXGitHub)\n- Add support for multiple auth flags (thanks @KSXGitHub)\n- Some theme related bug fixes (thanks @boastful-squirrel)\n\n<!-- next-url -->\n[Unreleased]: https://github.com/svenstaro/miniserve/compare/v0.33.0...HEAD\n[0.33.0]: https://github.com/svenstaro/miniserve/compare/v0.32.0...v0.33.0\n[0.32.0]: https://github.com/svenstaro/miniserve/compare/v0.31.0...v0.32.0\n[0.31.0]: https://github.com/svenstaro/miniserve/compare/v0.30.0...v0.31.0\n[0.30.0]: https://github.com/svenstaro/miniserve/compare/v0.29.0...v0.30.0\n[0.29.0]: https://github.com/svenstaro/miniserve/compare/v0.28.0...v0.29.0\n[0.28.0]: https://github.com/svenstaro/miniserve/compare/v0.27.1...v0.28.0\n[0.27.1]: https://github.com/svenstaro/miniserve/compare/v0.27.0...v0.27.1\n[0.27.0]: https://github.com/svenstaro/miniserve/compare/v0.26.0...v0.27.0\n[0.26.0]: https://github.com/svenstaro/miniserve/compare/v0.25.0...v0.26.0\n[0.25.0]: https://github.com/svenstaro/miniserve/compare/v0.24.0...v0.25.0\n[0.24.0]: https://github.com/svenstaro/miniserve/compare/v0.23.2...v0.24.0\n[0.23.2]: https://github.com/svenstaro/miniserve/compare/v0.23.1...v0.23.2\n[0.23.1]: https://github.com/svenstaro/miniserve/compare/v0.23.0...v0.23.1\n[0.23.0]: https://github.com/svenstaro/miniserve/compare/v0.22.0...v0.23.0\n[0.22.0]: https://github.com/svenstaro/miniserve/compare/v0.21.0...v0.22.0\n[0.21.0]: https://github.com/svenstaro/miniserve/compare/v0.20.0...v0.21.0\n[0.20.0]: https://github.com/svenstaro/miniserve/compare/v0.19.5...v0.20.0\n[0.19.5]: https://github.com/svenstaro/miniserve/compare/v0.19.4...v0.19.5\n[0.19.4]: https://github.com/svenstaro/miniserve/compare/v0.19.3...v0.19.4\n[0.19.3]: https://github.com/svenstaro/miniserve/compare/v0.19.2...v0.19.3\n[0.19.2]: https://github.com/svenstaro/miniserve/compare/v0.19.1...v0.19.2\n[0.19.1]: https://github.com/svenstaro/miniserve/compare/v0.19.0...v0.19.1\n[0.19.0]: https://github.com/svenstaro/miniserve/compare/v0.18.0...v0.19.0\n[0.18.0]: https://github.com/svenstaro/miniserve/compare/v0.17.0...v0.18.0\n[0.17.0]: https://github.com/svenstaro/miniserve/compare/v0.16.0...v0.17.0\n[0.16.0]: https://github.com/svenstaro/miniserve/compare/v0.15.0...v0.16.0\n[0.15.0]: https://github.com/svenstaro/miniserve/compare/v0.14.0...v0.15.0\n[0.14.0]: https://github.com/svenstaro/miniserve/compare/v0.13.0...v0.14.0\n[0.13.0]: https://github.com/svenstaro/miniserve/compare/v0.12.1...v0.13.0\n[0.12.1]: https://github.com/svenstaro/miniserve/compare/v0.12.0...v0.12.1\n[0.12.0]: https://github.com/svenstaro/miniserve/compare/v0.11.0...v0.12.0\n[0.11.0]: https://github.com/svenstaro/miniserve/compare/v0.10.4...v0.11.0\n[0.10.4]: https://github.com/svenstaro/miniserve/compare/v0.10.3...v0.10.4\n[0.10.3]: https://github.com/svenstaro/miniserve/compare/v0.10.2...v0.10.3\n[0.10.2]: https://github.com/svenstaro/miniserve/compare/v0.10.1...v0.10.2\n[0.10.1]: https://github.com/svenstaro/miniserve/compare/v0.10.0...v0.10.1\n[0.10.0]: https://github.com/svenstaro/miniserve/compare/v0.9.0...v0.10.0\n[0.9.0]: https://github.com/svenstaro/miniserve/compare/v0.8.0...v0.9.0\n[0.8.0]: https://github.com/svenstaro/miniserve/compare/v0.7.0...v0.8.0\n[0.7.0]: https://github.com/svenstaro/miniserve/compare/v0.6.0...v0.7.0\n[0.6.0]: https://github.com/svenstaro/miniserve/compare/v0.5.0...v0.6.0\n[0.5.0]: https://github.com/svenstaro/miniserve/compare/v0.4.0...v0.5.0\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"miniserve\"\nversion = \"0.33.0\"\ndescription = \"For when you really just want to serve some files over HTTP right now!\"\nauthors = [\"Sven-Hendrik Haase <svenstaro@gmail.com>\", \"Boastful Squirrel <boastful.squirrel@gmail.com>\"]\nrepository = \"https://github.com/svenstaro/miniserve\"\nlicense = \"MIT\"\nreadme = \"README.md\"\nkeywords = [\"serve\", \"http-server\", \"static-files\", \"http\", \"server\"]\ncategories = [\"command-line-utilities\", \"network-programming\", \"web-programming::http-server\"]\nedition = \"2024\"\n\n[profile.release]\ncodegen-units = 1\nlto = true\nopt-level = 'z'\npanic = 'abort'\nstrip = true\n\n[dependencies]\nactix-files = \"0.6.9\"\nactix-multipart = \"0.7\"\nactix-web = { version = \"4\", features = [\"macros\", \"compress-brotli\", \"compress-gzip\", \"compress-zstd\"], default-features = false }\nactix-web-httpauth = \"0.8\"\nalphanumeric-sort = \"1\"\nanyhow = \"1\"\nasync-walkdir = \"2.1.0\"\nbytesize = \"2\"\nchrono = \"0.4\"\nchrono-humanize = \"0.2\"\nclap = { version = \"4\", features = [\"derive\", \"cargo\", \"wrap_help\", \"deprecated\", \"env\"] }\nclap_complete = \"4\"\nclap_mangen = \"0.2\"\ncolored = \"3\"\ncomrak = { version = \"0.50\", default-features = false }\ndav-server = { version = \"0.11\", features = [\"actix-compat\"] }\nfast_qr = { version = \"0.13\", features = [\"svg\"] }\nfutures = \"0.3\"\ngrass = { version = \"0.13\", features = [\"macro\"], default-features = false }\nhex = \"0.4\"\nhttparse = \"1\"\nif-addrs = \"0.15\"\nlibflate = \"2\"\nlog = \"0.4\"\nmaud = \"0.27\"\nmime = \"0.3\"\nnanoid = \"0.4\"\npercent-encoding = \"2\"\nport_check = \"0.3\"\nregex = \"1\"\nrustix = { version = \"1.1.4\", features = [\"process\", \"fs\"] }\nrustls = { version = \"0.23\", features = [\"ring\"], optional = true, default-features = false }\nrustls-pemfile = { version = \"2\", optional = true }\nserde = { version = \"1\", features = [\"derive\"] }\nsha2 = \"0.10\"\nsimplelog = \"0.12\"\nsocket2 = \"0.6\"\nstrum = { version = \"0.28\", features = [\"derive\"] }\ntar = \"0.4\"\ntempfile = \"3.26.0\"\nthiserror = \"2\"\ntokio = { version = \"1.47.1\", features = [\"fs\", \"macros\"] }\nzip = { version = \"8\", default-features = false }\n\n[features]\ndefault = [\"tls\"]\n# This feature allows us to use rustls only on architectures supported by ring.\n# See also https://github.com/briansmith/ring/issues/1182\n# and https://github.com/briansmith/ring/issues/562\n# and https://github.com/briansmith/ring/issues/1367\ntls = [\"rustls\", \"rustls-pemfile\", \"actix-web/rustls-0_23\"]\n\n[dev-dependencies]\nassert_cmd = \"2\"\nassert_fs = \"1\"\npredicates = \"3\"\npretty_assertions = \"1.2\"\nregex = \"1\"\nreqwest = { version = \"0.13\", features = [\"blocking\", \"multipart\", \"json\", \"rustls-no-provider\"], default-features = false }\nreqwest_dav = { version = \"0.3\", default-features = false }\nrstest = \"0.26\"\nselect = \"0.6\"\nurl = \"2\"\n\n[target.'cfg(not(windows))'.dev-dependencies]\n# fake_tty does not support Windows for now\nfake-tty = \"0.3.1\"\n"
  },
  {
    "path": "Containerfile",
    "content": "FROM debian:testing-slim\nCOPY --chmod=755 miniserve /app/\nENTRYPOINT [\"/app/miniserve\"]\n"
  },
  {
    "path": "Containerfile.alpine",
    "content": "FROM docker.io/alpine\nCOPY --chmod=755 miniserve /app/\nENTRYPOINT [\"/app/miniserve\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Sven-Hendrik Haase\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": "Makefile",
    "content": ".PHONY: local\nlocal:\n\tcargo build --release\n\n.PHONY: run\nrun:\nifndef ARGS\n\t@echo Run \"make run\" with ARGS set to pass arguments…\nendif\n\tcargo run --release -- $(ARGS)\n\n.PHONY: build-linux\nbuild-linux:\n\tcargo build --target x86_64-unknown-linux-musl --release --locked\n\tstrip target/x86_64-unknown-linux-musl/release/miniserve\n\tupx --lzma target/x86_64-unknown-linux-musl/release/miniserve\n\n.PHONY: build-win\nbuild-win:\n\tRUSTFLAGS=\"-C linker=x86_64-w64-mingw32-gcc\" cargo build --target x86_64-pc-windows-gnu --release --locked\n\tstrip target/x86_64-pc-windows-gnu/release/miniserve.exe\n\tupx --lzma target/x86_64-pc-windows-gnu/release/miniserve.exe\n\n.PHONY: build-apple\nbuild-apple:\n\tcargo build --target x86_64-apple-darwin --release --locked\n\tstrip target/x86_64-apple-darwin/release/miniserve\n\tupx --lzma target/x86_64-apple-darwin/release/miniserve\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <img src=\"data/logo.svg\" alt=\"miniserve - a CLI tool to serve files and dirs over HTTP\"><br>\n</p>\n\n# miniserve - a CLI tool to serve files and dirs over HTTP\n\n[![CI](https://github.com/svenstaro/miniserve/workflows/CI/badge.svg)](https://github.com/svenstaro/miniserve/actions)\n[![Docker Hub](https://img.shields.io/docker/pulls/svenstaro/miniserve)](https://cloud.docker.com/repository/docker/svenstaro/miniserve/)\n[![Crates.io](https://img.shields.io/crates/v/miniserve.svg)](https://crates.io/crates/miniserve)\n[![license](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/svenstaro/miniserve/blob/master/LICENSE)\n[![Stars](https://img.shields.io/github/stars/svenstaro/miniserve.svg)](https://github.com/svenstaro/miniserve/stargazers)\n[![Downloads](https://img.shields.io/github/downloads/svenstaro/miniserve/total.svg)](https://github.com/svenstaro/miniserve/releases)\n[![Lines of Code](https://tokei.rs/b1/github/svenstaro/miniserve)](https://github.com/svenstaro/miniserve)\n\n**For when you really just want to serve some files over HTTP right now!**\n\n**miniserve** is a small, self-contained cross-platform CLI tool that allows you to just grab the binary and serve some file(s) via HTTP.\nSometimes this is just a more practical and quick way than doing things properly.\n\n## Screenshot\n\n![Screenshot](screenshot.png)\n\n## How to use\n\n### Serve a directory:\n\n    miniserve linux-distro-collection/\n\n### Serve a single file:\n\n    miniserve linux-distro.iso\n\n### Set a custom index file to serve instead of a file listing:\n\n    miniserve --index test.html\n\n### Serve an SPA (Single Page Application) so that non-existent paths are forwarded to the SPA's router instead\n\n    miniserve --spa --index index.html\n\n### Require username/password:\n\n    miniserve --auth joe:123 unreleased-linux-distros/\n\n### Require username/password as hash:\n\n    pw=$(echo -n \"123\" | sha256sum | cut -f 1 -d ' ')\n    miniserve --auth joe:sha256:$pw unreleased-linux-distros/\n\n### Require username/password from file (separate logins with new lines):\n\n    miniserve --auth-file auth.txt unreleased-linux-distros/\n\n### Generate random 6-hexdigit URL:\n\n    miniserve -i 192.168.0.1 --random-route /tmp\n    # Serving path /private/tmp at http://192.168.0.1/c789b6\n\n### Bind to multiple interfaces:\n\n    miniserve -i 192.168.0.1 -i 10.13.37.10 -i ::1 /tmp/myshare\n\n### Insert custom headers\n\n    miniserve --header \"Cache-Control:no-cache\" --header \"X-Custom-Header:custom-value\" -p 8080 /tmp/myshare\n    # Check headers in another terminal\n    curl -I http://localhost:8080\n\nIf a header is already set or previously inserted, it will not be overwritten.\n\n### Start with TLS:\n\n    miniserve --tls-cert my.cert --tls-key my.key /tmp/myshare\n    # Fullchain TLS and HTTP Strict Transport Security (HSTS)\n    miniserve --tls-cert fullchain.pem --tls-key my.key --header \"Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\" /tmp/myshare\n\nIf the parameter value has spaces, be sure to wrap it in quotes.\n(To achieve an A+ rating at https://www.ssllabs.com/ssltest/, enabling both fullchain TLS and HSTS is necessary.)\n\n### Upload a file using `curl`:\n\n    # in one terminal\n    miniserve -u -- .\n    # in another terminal\n    curl -F \"path=@$FILE\" http://localhost:8080/upload\\?path\\=/\n\n(where `$FILE` is the path to the file. This uses miniserve's default port of 8080)\n\nNote that for uploading, we have to use `--` to disambiguate the argument to `-u`.\nThis is because `-u` can also take a path (or multiple). If a path argument to `-u` is given,\nuploading will only be possible to the provided paths as opposed to every path.\n\nAnother effect of this is that you can't just combine flags like this `-uv` when `-u` is used. In\nthis example, you'd need to use `-u -v`.\n\n### Create a directory using `curl`:\n\n    # in one terminal\n    miniserve --upload-files --mkdir .\n    # in another terminal\n    curl -F \"mkdir=$DIR_NAME\" http://localhost:8080/upload\\?path=\\/\n\n(where `$DIR_NAME` is the name of the directory. This uses miniserve's default port of 8080.)\n\n### Use the raw renderer for use with simple viewers\n\nYou can pass `?raw=true` with requests where you only require minimal HTML output for CLI-based browsers such as `lynx` or `w3m`.\nThis is enabled by default without any extra flags:\n\n    miniserve .\n    curl http://localhost:8080?raw=true\n\nYou can enable a convenient copy-pastable footer for `wget` using `--show-wget-footer`:\n\n    miniserve --show-wget-footer .\n\nAfterwards, check the bottom of any rendered page.\nIt'll have a neat `wget` command you can easily copy-paste to recursively grab the current directory.\n\n### Take pictures and upload them from smartphones:\n\n    miniserve -u -m image -q\n\nThis uses the `--media-type` option, which sends a hint for the expected media type to the browser.\nSome mobile browsers like Firefox on Android will offer to open the camera app when seeing this.\n\n## Features\n\n- Easy to use\n- Just works: Correct MIME types handling out of the box\n- Single binary drop-in with no extra dependencies required\n- Authentication support with username and password (and hashed password)\n- Mega fast and highly parallel (thanks to [Rust](https://www.rust-lang.org/) and [Actix](https://actix.rs/))\n- Folder download (compressed on the fly as `.tar.gz` or `.zip`)\n- File uploading\n- Directory creation\n- Pretty themes (with light and dark theme support)\n- Scan QR code for quick access\n- Shell completions\n- Sane and secure defaults\n- TLS (for supported architectures)\n- Supports README.md rendering like on GitHub\n- Range requests\n- WebDAV support\n- Healthcheck route (at `/__miniserve_internal/healthcheck`)\n\n## Usage\n\n```\nFor when you really just want to serve some files over HTTP right now!\n\nUsage: miniserve [OPTIONS] [PATH]\n\nArguments:\n  [PATH]\n          Which path to serve\n\n          [env: MINISERVE_PATH=]\n\nOptions:\n  -v, --verbose\n          Be verbose, includes emitting access logs\n\n          [env: MINISERVE_VERBOSE=]\n\n      --index <INDEX>\n          The name of a directory index file to serve, like \"index.html\"\n\n          Normally, when miniserve serves a directory, it creates a listing for that directory. However, if a\n          directory contains this file, miniserve will serve that file instead.\n\n          [env: MINISERVE_INDEX=]\n\n      --spa\n          Activate SPA (Single Page Application) mode\n\n          This will cause the file given by --index to be served for all non-existing file paths. In effect,\n          this will serve the index file whenever a 404 would otherwise occur in order to allow the SPA\n          router to handle the request instead.\n\n          [env: MINISERVE_SPA=]\n\n      --pretty-urls\n          Activate Pretty URLs mode\n\n          This will cause the server to serve the equivalent `.html` file indicated by the path.\n\n          `/about` will try to find `about.html` and serve it.\n\n          [env: MINISERVE_PRETTY_URLS=]\n\n  -p, --port <PORT>\n          Port to use\n\n          [env: MINISERVE_PORT=]\n          [default: 8080]\n\n  -i, --interfaces <INTERFACES>\n          Interface to listen on\n\n          [env: MINISERVE_INTERFACE=]\n\n  -a, --auth <AUTH>\n          Set authentication\n\n          Currently supported formats:\n          username:password, username:sha256:hash, username:sha512:hash\n          (e.g. joe:123, joe:sha256:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3)\n\n          [env: MINISERVE_AUTH=]\n\n      --auth-file <AUTH_FILE>\n          Read authentication values from a file\n\n          Example file content:\n\n          joe:123\n          bob:sha256:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3\n          bill:\n\n          [env: MINISERVE_AUTH_FILE=]\n\n      --route-prefix <ROUTE_PREFIX>\n          Use a specific route prefix\n\n          [env: MINISERVE_ROUTE_PREFIX=]\n\n      --random-route\n          Generate a random 6-hexdigit route\n\n          [env: MINISERVE_RANDOM_ROUTE=]\n\n      --file-external-url <FILE_BASE_URL>\n          Optional external URL (e.g., 'http://external.example.com:8081') prepended to file links in listings.\n          Allows serving files from a different URL than the browsing instance. Useful for setups like:\n          one authenticated instance for browsing, linking files (via this option) to a second,\n          non-indexed (-I) instance for direct downloads. This obscures the full file list on\n          the download server, while users can still copy direct file URLs for sharing.\n          The external URL is put verbatim in front of the relative location of the file, including the protocol.\n          The user should take care this results in a valid URL, no further checks are being done.\n\n          [env: MINISERVE_FILE_EXTERNAL_URL=]\n\n  -P, --no-symlinks\n          Hide symlinks in listing and prevent them from being followed\n\n          [env: MINISERVE_NO_SYMLINKS=]\n\n  -H, --hidden\n          Show hidden files\n\n          [env: MINISERVE_HIDDEN=]\n\n  -S, --default-sorting-method <DEFAULT_SORTING_METHOD>\n          Default sorting method for file list\n\n          [env: MINISERVE_DEFAULT_SORTING_METHOD=]\n          [default: name]\n\n          Possible values:\n          - name: Sort by name\n          - size: Sort by size\n          - date: Sort by last modification date (natural sort: follows alphanumerical order)\n\n  -O, --default-sorting-order <DEFAULT_SORTING_ORDER>\n          Default sorting order for file list\n\n          [env: MINISERVE_DEFAULT_SORTING_ORDER=]\n          [default: desc]\n\n          Possible values:\n          - asc:  Ascending order\n          - desc: Descending order\n\n  -c, --color-scheme <COLOR_SCHEME>\n          Default color scheme\n\n          [env: MINISERVE_COLOR_SCHEME=]\n          [default: squirrel]\n          [possible values: squirrel, archlinux, zenburn, monokai]\n\n  -d, --color-scheme-dark <COLOR_SCHEME_DARK>\n          Default color scheme\n\n          [env: MINISERVE_COLOR_SCHEME_DARK=]\n          [default: archlinux]\n          [possible values: squirrel, archlinux, zenburn, monokai]\n\n  -q, --qrcode\n          Enable QR code display\n\n          [env: MINISERVE_QRCODE=]\n\n  -u, --upload-files [<ALLOWED_UPLOAD_DIR>]\n          Enable file uploading (and optionally specify for which directory)\n\n          The provided path is not a physical file system path. Instead, it's relative to the serve dir. For\n          instance, if the serve dir is '/home/hello', set this to '/upload' to allow uploading to\n          '/home/hello/upload'. When specified via environment variable, a path always needs to be specified.\n\n          [env: MINISERVE_ALLOWED_UPLOAD_DIR=]\n\n      --web-upload-files-concurrency <WEB_UPLOAD_CONCURRENCY>\n          Configure amount of concurrent uploads when visiting the website. Must have upload-files option enabled for this setting to matter.\n\n          [env: MINISERVE_WEB_UPLOAD_CONCURRENCY=]\n          [default: 0]\n\n  -U, --mkdir\n          Enable creating directories\n\n          [env: MINISERVE_MKDIR_ENABLED=]\n\n  -m, --media-type <MEDIA_TYPE>\n          Specify uploadable media types\n\n          [env: MINISERVE_MEDIA_TYPE=]\n          [possible values: image, audio, video]\n\n  -M, --raw-media-type <MEDIA_TYPE_RAW>\n          Directly specify the uploadable media type expression\n\n          [env: MINISERVE_RAW_MEDIA_TYPE=]\n\n  -o, --on-duplicate-files <ON_DUPLICATE_FILES>\n          What to do if existing files with same name is present during file upload\n\n          If you enable renaming files, the renaming will occur by adding numerical suffix to the filename before the final extension. For example file.txt will be uploaded\n          as file-1.txt, the number will be increased until an available filename is found.\n\n          [env: MINISERVE_ON_DUPLICATE_FILES=]\n          [default: error]\n          [possible values: error, overwrite, rename]\n\n  -R, --rm-files [<ALLOWED_RM_DIR>]\n          Enable file and directory deletion (and optionally specify for which directory)\n\n          [env: MINISERVE_ALLOWED_RM_DIR=]\n\n  -r, --enable-tar\n          Enable uncompressed tar archive generation\n\n          [env: MINISERVE_ENABLE_TAR=]\n\n  -g, --enable-tar-gz\n          Enable gz-compressed tar archive generation\n\n          [env: MINISERVE_ENABLE_TAR_GZ=]\n\n  -z, --enable-zip\n          Enable zip archive generation\n\n          WARNING: Zipping large directories can result in out-of-memory exception because zip generation is\n          done in memory and cannot be sent on the fly\n\n          [env: MINISERVE_ENABLE_ZIP=]\n\n  -C, --compress-response\n          Compress response\n\n          WARNING: Enabling this option may slow down transfers due to CPU overhead, so it is disabled by\n          default.\n\n          Only enable this option if you know that your users have slow connections or if you want to\n          minimize your server's bandwidth usage.\n\n          [env: MINISERVE_COMPRESS_RESPONSE=]\n\n  -D, --dirs-first\n          List directories first\n\n          [env: MINISERVE_DIRS_FIRST=]\n\n  -t, --title <TITLE>\n          Shown instead of host in page title and heading\n\n          [env: MINISERVE_TITLE=]\n\n      --header <HEADER>\n          Inserts custom headers into the responses. Specify each header as a 'Header:Value' pair. This\n          parameter can be used multiple times to add multiple headers.\n\n          Example: --header \"Header1:Value1\" --header \"Header2:Value2\" (If a header is already set or\n          previously inserted, it will not be overwritten.)\n\n          [env: MINISERVE_HEADER=]\n\n  -l, --show-symlink-info\n          Visualize symlinks in directory listing\n\n          [env: MINISERVE_SHOW_SYMLINK_INFO=]\n\n  -F, --hide-version-footer\n          Hide version footer\n\n          [env: MINISERVE_HIDE_VERSION_FOOTER=]\n\n      --hide-theme-selector\n          Hide theme selector\n\n          [env: MINISERVE_HIDE_THEME_SELECTOR=]\n\n  -W, --show-wget-footer\n          If enabled, display a wget command to recursively download the current directory\n\n          [env: MINISERVE_SHOW_WGET_FOOTER=]\n\n      --print-completions <shell>\n          Generate completion file for a shell\n\n          [possible values: bash, elvish, fish, powershell, zsh]\n\n      --print-manpage\n          Generate man page\n\n      --tls-cert <TLS_CERT>\n          TLS certificate to use\n\n          [env: MINISERVE_TLS_CERT=]\n\n      --tls-key <TLS_KEY>\n          TLS private key to use\n\n          [env: MINISERVE_TLS_KEY=]\n\n      --readme\n          Enable README.md rendering in directories\n\n          [env: MINISERVE_README=]\n\n  -I, --disable-indexing\n          Disable indexing\n\n          This will prevent directory listings from being generated and return an error instead.\n\n          [env: MINISERVE_DISABLE_INDEXING=]\n\n      --enable-webdav\n          Enable read-only WebDAV support (PROPFIND requests)\n\n          Currently incompatible with -P|--no-symlinks (see\n          https://github.com/messense/dav-server-rs/issues/37)\n\n          [env: MINISERVE_ENABLE_WEBDAV=]\n\n      --log-color <LOG_COLOR>\n          Set the color style of the log output\n\n          \"auto\" (default) enables colors only when the output is a terminal. \"always\" always enables colors.\n          \"never\" always disables colors.\n\n          [env: MINISERVE_LOG_COLOR=]\n          [default: auto]\n          [possible values: auto, always, never]\n\n  -h, --help\n          Print help (see a summary with '-h')\n\n  -V, --version\n          Print version\n```\n\n## How to install\n\n<a href=\"https://repology.org/project/miniserve/versions\"><img align=\"right\" src=\"https://repology.org/badge/vertical-allrepos/miniserve.svg\" alt=\"Packaging status\"></a>\n\n**On Linux**: Download `miniserve-linux` from [the releases page](https://github.com/svenstaro/miniserve/releases) and run\n\n    chmod +x miniserve-linux\n    ./miniserve-linux\n\nAlternatively, if you are on **Arch Linux**, you can do\n\n    pacman -S miniserve\n\nOn [Termux](https://termux.com/)\n\n    pkg install miniserve\n\n**On OSX**: Download `miniserve-osx` from [the releases page](https://github.com/svenstaro/miniserve/releases) and run\n\n    chmod +x miniserve-osx\n    ./miniserve-osx\n\nAlternatively install with [Homebrew](https://brew.sh/):\n\n    brew install miniserve\n    miniserve\n\n**On Windows**: Download `miniserve-win.exe` from [the releases page](https://github.com/svenstaro/miniserve/releases) and run\n\n    miniserve-win.exe\n\nAlternatively install with [Scoop](https://scoop.sh/):\n\n    scoop install miniserve\n\n**With Cargo**: Make sure you have a recent version of Rust. Then you can run\n\n    cargo install --locked miniserve\n    miniserve\n\n**With Docker:** Make sure the Docker daemon is running and then run\n\n    docker run -v /tmp:/tmp -p 8080:8080 --rm -it docker.io/svenstaro/miniserve /tmp\n\n**With Podman:** Just run\n\n    podman run -v /tmp:/tmp -p 8080:8080 --rm -it docker.io/svenstaro/miniserve /tmp\n\n**With Helm:** See [this third-party Helm chart](https://codeberg.org/wrenix/helm-charts/src/branch/main/miniserve) by @wrenix.\n\n## Shell completions\n\nIf you'd like to make use of the built-in shell completion support, you need to run `miniserve\n--print-completions <your-shell>` and put the completions in the correct place for your shell. A\nfew examples with common paths are provided below:\n\n    # For bash\n    miniserve --print-completions bash > ~/.local/share/bash-completion/completions/miniserve\n    # For zsh\n    miniserve --print-completions zsh > /usr/local/share/zsh/site-functions/_miniserve\n    # For fish\n    miniserve --print-completions fish > ~/.config/fish/completions/miniserve.fish\n\n## systemd\n\nA hardened systemd-compatible unit file can be found in `packaging/miniserve@.service`. You could\ninstall this to `/etc/systemd/system/miniserve@.service` and start and enable `miniserve` as a\ndaemon on a specific serve path `/my/serve/path` like this:\n\n    systemctl enable --now miniserve@-my-serve-path\n\nKeep in mind that you'll have to use `systemd-escape` to properly escape a path for this usage.\n\nIn case you want to customize the particular flags that miniserve launches with, you can use\n\n    systemctl edit miniserve@-my-serve-path\n\nand set the `[Service]` part in the resulting `override.conf` file. For instance:\n\n    [Service]\n    ExecStart=\n    ExecStart=/usr/bin/miniserve --enable-tar --enable-zip --no-symlinks --verbose -i ::1 -p 1234 --title hello --color-scheme monokai --color-scheme-dark monokai -- %I\n\nMake sure to leave the `%I` at the very end in place or the wrong path might be served.\nAlternatively, you can configure the service via environment variables:\n\n    [Service]\n    Environment=MINISERVE_ENABLE_TAR=true\n    Environment=MINISERVE_ENABLE_ZIP=true\n    Environment=\"MINISERVE_TITLE=hello world\"\n    ...\n\nYou might additionally have to override `IPAddressAllow` and `IPAddressDeny` if you plan on making\nminiserve directly available on a public interface.\n\n## Binding behavior\n\nFor convenience reasons, miniserve will try to bind on all interfaces by default (if no `-i` is provided).\nIt will also do that if explicitly provided with `-i 0.0.0.0` or `-i ::`.\nIn all of the aforementioned cases, it will bind on both IPv4 and IPv6.\nIf provided with an explicit non-default interface, it will ONLY bind to that interface.\nYou can provide `-i` multiple times to bind to multiple interfaces at the same time.\n\n## Why use this over alternatives?\n\n- darkhttpd: Not easily available on Windows and it's not as easy as download-and-go.\n- Python built-in webserver: Need to have Python installed, it's low performance, and also doesn't do correct MIME type handling in some cases.\n- netcat: Not as convenient to use and sending directories is [somewhat involved](https://nakkaya.com/2009/04/15/using-netcat-for-file-transfers/).\n\n## Releasing\n\nThis is mostly a note for me on how to release this thing:\n\n- Make sure [CHANGELOG.md](./CHANGELOG.md) is up to date.\n- `cargo release <version>`\n- `cargo release --execute <version>`\n- Releases will automatically be deployed by GitHub Actions.\n- Update Arch package.\n"
  },
  {
    "path": "data/style.scss",
    "content": "@use \"themes/archlinux\" with ($generate_default: false);\n@use \"themes/ayu_dark\" with ($generate_default: false);\n@use \"themes/monokai\" with ($generate_default: false);\n@use \"themes/squirrel\" with ($generate_default: false);\n@use \"themes/zenburn\" with ($generate_default: false);\n\n// theme colors can be found at the bottom\n$themes: squirrel, archlinux, ayu_dark, monokai, zenburn;\n\nhtml {\n  font-smoothing: antialiased;\n  text-rendering: optimizeLegibility;\n  width: 100%;\n  height: 100%;\n}\n\nbody {\n  margin: 0;\n  font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-weight: normal;\n  color: var(--text_color);\n  background: var(--background);\n  position: relative;\n  min-height: 100%;\n}\n\n.container {\n  padding: 1.5rem 5rem;\n}\n\n$upload_container_height: 18rem;\n\n.upload_area {\n  display: block;\n  position: fixed;\n  bottom: 1rem;\n  right: -105%;\n  color: #ffffff;\n  box-shadow: 0 3px 6px -1px rgba(0, 0, 0, 0.12), 0 10px 36px -4px rgba(77, 96, 232, 0.3);\n  background: linear-gradient(135deg, #73a5ff, #5477f5);\n  padding: 0px;\n  margin: 0px;\n  opacity: 1; // Change this\n  transition: all 0.4s cubic-bezier(0.215, 0.61, 0.355, 1);\n  border-radius: 4px;\n  text-decoration: none;\n  min-width: 400px;\n  max-width: 600px;\n  z-index: 2147483647;\n  max-height: $upload_container_height;\n  overflow: hidden;\n\n  &.active {\n    right: 1rem;\n  }\n\n  #upload-toggle {\n    display: none;\n    transition: transform 0.3s ease;\n    cursor: pointer;\n  }\n}\n\n.upload_container {\n  max-height: $upload_container_height;\n  display: flex;\n  flex-direction: column;\n}\n\n.upload_header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  padding: 1rem;\n  background-color: var(--upload_modal_header_background);\n  color: var(--upload_modal_header_color);\n}\n\n.upload_header svg {\n  width: 24px;\n  height: 24px;\n}\n\n.upload_action {\n  background-color: var(--upload_modal_sub_header_background);\n  color: var(--upload_modal_header_color);\n  padding: 0.25rem 1rem;\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  font-size: 0.75em;\n  font-weight: 500;\n}\n\n.upload_cancel {\n  background: none;\n  border: none;\n  font-weight: 500;\n  cursor: pointer;\n}\n\n.upload_files {\n  padding: 0px;\n  margin: 0px;\n  flex: 1;\n  overflow-y: auto;\n  max-height: inherit;\n}\n\n.upload_file_list {\n  background-color: var(--upload_modal_file_item_background);\n  color: var(--upload_modal_file_item_color);\n  padding: 0px;\n  margin: 0px;\n  list-style: none;\n  list-style: none;\n  display: flex;\n  flex-direction: column;\n  align-items: stretch;\n  overflow-y: scroll;\n}\n\n.upload_file_container {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: 1rem 1rem calc(1rem - 2px) 1rem;\n}\n\n.upload_file_action {\n  display: flex;\n  justify-content: right;\n}\n\n.file_progress_bar {\n  width: 0%;\n  border-top: 2px solid var(--progress_bar_background);\n  transition: width 0.25s ease;\n\n  &.cancelled {\n    border-color: var(--error_color);\n  }\n\n  &.failed {\n    border-color: var(--error_color);\n  }\n\n  &.complete {\n    border-color: var(--success_color);\n  }\n}\n\n.upload_file_text {\n  font-size: 0.80em;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n\n  &.cancelled {\n    text-decoration: line-through;\n  }\n\n  &.failed {\n    text-decoration: line-through;\n  }\n}\n\n.file_cancel_upload {\n  padding-left: 0.25rem;\n  font-size: 1em;\n  cursor: pointer;\n  border: none;\n  background: inherit;\n  font-size: 1em;\n  color: var(--error_color);\n\n  &.complete {\n    color: var(--success_color);\n  }\n}\n\n.title {\n  word-break: break-all;\n}\n\n.title a {\n  font-weight: bold;\n  color: var(--directory_link_color);\n}\n\n.footer {\n  text-align: center;\n  padding-top: 1.5rem;\n  font-size: 0.7em;\n  color: var(--footer_color);\n\n  .downloadDirectory {\n    display: flex;\n    flex-direction: row;\n    justify-content: center;\n    flex-wrap: wrap;\n\n    .cmd {\n      margin: 0;\n      padding-left: 5px;\n\n      line-height: 13px;\n      font-family: monospace;\n    }\n  }\n}\n\na {\n  text-decoration: none;\n}\n\na.root,\na.root:visited,\n.root-chevron {\n  font-weight: bold;\n  color: var(--root_link_color);\n}\n\na:hover {\n  text-decoration: underline;\n}\n\na.directory {\n  font-weight: bold;\n  color: var(--directory_link_color);\n\n  &:visited {\n    color: var(--directory_link_color_visited);\n  }\n}\n\na.file,\n.error-back {\n  color: var(--file_link_color);\n\n  &:visited {\n    color: var(--file_link_color_visited);\n  }\n}\n\na.file::before {\n  content: \"📃 \";\n}\n\na.directory::before {\n  content: \"📁 \";\n}\n\na.directory:hover {\n  color: var(--directory_link_color);\n}\n\na.file:hover {\n  color: var(--file_link_color);\n}\n\na.symlink,\na.symlink:visited {\n  font-weight: bold;\n  color: var(--symlink_color);\n}\n\n.symlink-symbol::after {\n  content: \"⇢\";\n  display: inline-block;\n  border: 1px solid;\n  margin-left: 0.5rem;\n  margin-right: 0.5rem;\n  border-radius: 0.2rem;\n  padding: 0 0.1rem;\n}\n\nnav {\n  padding: 0 5rem;\n  display: flex;\n  justify-content: flex-end;\n}\n\nnav>div {\n  position: relative;\n  margin-left: 0.5rem;\n}\n\nnav p {\n  padding: 0.5rem 1rem;\n  width: 8rem;\n  text-align: center;\n  background: var(--switch_theme_background);\n  color: var(--change_theme_link_color);\n}\n\nnav p+* {\n  display: none;\n  position: absolute;\n  left: 0;\n  right: 0;\n  top: 100%;\n}\n\n@keyframes show {\n  from {\n    opacity: 0;\n  }\n\n  to {\n    opacity: 1;\n  }\n}\n\nnav>div:hover p {\n  cursor: pointer;\n  color: var(--switch_theme_link_color);\n}\n\nnav>div:hover p+* {\n  display: block;\n  border-top: 1px solid var(--switch_theme_border);\n}\n\nnav .qrcode {\n  padding: 0.5rem;\n  background: var(--switch_theme_background);\n}\n\nnav .qrcode svg {\n  display: block;\n}\n\nnav .theme {\n  margin: 0;\n  padding: 0;\n  list-style-type: none;\n}\n\nnav .theme li {\n  width: 100%;\n  background: var(--switch_theme_background);\n}\n\nnav .theme li a {\n  display: block;\n  width: 100%;\n  padding: 0.5rem 0;\n  text-align: center;\n  color: var(--switch_theme_link_color);\n}\n\nnav .theme li a:visited {\n  color: var(--switch_theme_link_color);\n}\n\nnav .theme li a:hover {\n  text-decoration: underline;\n  color: var(--change_theme_link_color_hover);\n}\n\np {\n  margin: 0;\n  padding: 0;\n}\n\nh1 {\n  margin-top: 0;\n  font-size: 1.5rem;\n}\n\ntable {\n  margin-top: 2rem;\n  width: 100%;\n  border: 0;\n  table-layout: auto;\n  background: var(--table_background);\n}\n\ntable thead tr th,\ntable tbody tr td {\n  padding: 0.5625rem 0.625rem;\n  font-size: 0.875rem;\n  color: var(--table_text_color);\n  text-align: left;\n  line-height: 1.125rem;\n}\n\ntable tbody tr td p {\n  display: flex;\n  align-items: center;\n}\n\ntable thead tr th {\n  padding: 0.5rem 0.625rem 0.625rem;\n  font-weight: bold;\n  color: var(--table_header_text_color);\n}\n\ntable thead th.size {\n  width: 6em;\n}\n\ntable thead th.date {\n  width: 21em;\n}\n\ntable thead th.actions {\n    width: 4em;\n}\n\ntable tbody tr:nth-child(odd) {\n  background: var(--odd_row_background);\n}\n\ntable tbody tr:nth-child(even) {\n  background: var(--even_row_background);\n}\n\ntable thead {\n  background: var(--table_header_background);\n}\n\ntable tbody tr:hover {\n  background: var(--active_row_color);\n}\n\ntd.size-cell {\n  text-align: right;\n}\n\ntd.date-cell {\n  display: flex;\n  justify-content: space-between;\n}\n\ntr.entry-type-directory .size-cell {\n  &:not([data-size])::after {\n    content: \"xxxx KiB\"; // hidden placeholder to get text-like width and height\n    color: transparent;\n\n    animation:\n      shimmer 2.5s ease-in-out reverse infinite,\n      bump 1.25s ease-out alternate-reverse infinite;\n\n    background:\n      linear-gradient(to left, #e6e6e633 0%, #e6e6e633 20%, #e7e7e744 40%, #ececec70 45%, #e7e7e755 60%, #e6e6e633 80%, #e6e6e633 100%),\n      linear-gradient(to bottom, #ffffff00 40%, #ffffff18 60%, #ffffff50 80%);\n\n    background-size: 500% 160%;\n    border-radius: 4px;\n  }\n\n  &[data-size]::after {\n    content: attr(data-size);\n  }\n\n  @keyframes bump {\n    from { background-position-y: 30%; }\n    to   { background-position-y: 0; }\n  }\n\n  @keyframes shimmer {\n    from { background-position-x: 0; }\n    to   { background-position-x: 100%; }\n  }\n}\n\ntd.actions-cell button {\n    padding: 0.1rem 0.3rem;\n    border-radius: 0.2rem;\n    border: none;\n}\n\ntd.actions-cell .rm_form {\n    display: flex;\n    place-content: center;\n}\n\ntd.actions-cell .rm_form button {\n    background: var(--rm_button_background);\n    color: var(--rm_button_text_color);\n}\n\n.history {\n  color: var(--date_text_color);\n}\n\nspan.size,\nspan.mobile-info.history {\n  white-space: nowrap;\n  border-radius: 1rem;\n  background: var(--size_background_color);\n  padding: 0 0.25rem;\n  margin: 0 0.25rem;\n  font-size: 0.7rem;\n  color: var(--size_text_color);\n}\n\n.mobile-info {\n  display: none;\n}\n\n.mobile-info a,\n.mobile-info a:visited {\n  color: var(--size_text_color);\n}\n\nth a,\nth a:visited,\n.chevron {\n  color: var(--table_header_text_color);\n}\n\n.chevron,\n.root-chevron {\n  margin-right: 0.5rem;\n  font-size: 1.2em;\n  font-weight: bold;\n}\n\nth span.active a,\nth span.active span {\n  color: var(--table_header_active_color);\n}\n\n.back {\n  position: fixed;\n  width: 3rem;\n  height: 3rem;\n  align-items: center;\n  justify-content: center;\n  bottom: 3rem;\n  right: 3.75rem;\n  background: var(--back_button_background);\n  border-radius: 100%;\n  box-shadow: 0 0 8px -4px #888888;\n  color: var(--back_button_link_color);\n  display: none;\n  padding: 0;\n  font-size: 2em;\n}\n\n.back:visited {\n  color: var(--back_button_link_color);\n}\n\n.back:hover {\n  color: var(--back_button_link_color_hover);\n  font-weight: bold;\n  text-decoration: none;\n  background: var(--back_button_background_hover);\n}\n\n//\n// Toolbar & tools inside the bar.\n//\n\n.toolbar {\n  --tool-gap-between: 0.5rem;\n  --tool-spacing-inside: 0.5rem;\n}\n\n.toolbar .tool_row {\n  margin-top: 1rem;\n  display: flex;\n  gap: var(--tool-gap-between);\n  flex-direction: column;\n  @media (min-width: 760px) {\n    flex-direction: row;\n  }\n}\n\n// Upload tools has 4 configurations\n//\n// a) Upload enabled,\n// b) Upload enabled, mkdir enabled\n// c) Upload enabled, paste enabled\n// d) Upload enabled, mkdir enabled, paste enabled\n//\n// At larger screen sizes, for a and b, we render the tools horizontal, as at\n// min-content width. For c and d, we render upload (and mkdir) at min-content\n// width, stacked vertically and let paste fill the remaining space.\n//\n// At smaller screen sizes we render any available elememnts in a full-width\n// stack.\n//\n// We render via grid not flex as it affords us better control of the\n// stack/unstack in b.\n\n@media (min-width: 760px) {\n  .toolbar .tool_row.upload_tools {\n    display: grid;\n    grid-template-columns: min-content min-content;\n\n    .tool[data-tool=\"upload\"] {\n      grid-column: 1 / 2;\n      grid-row: 1 / 2;\n    }\n\n    .tool[data-tool=\"mkdir\"] {\n      grid-column: 2 / 3;\n      grid-row: 1 / 2;\n    }\n\n    &:has([data-tool=\"pastebin\"]) {\n      grid-template-columns: min-content auto;\n      .tool[data-tool=\"upload\"] {\n        grid-column: 1 / 2;\n        grid-row: 1 / 2;\n      }\n      .tool[data-tool=\"mkdir\"] {\n        grid-column: 1 / 2;\n        grid-row: 2 / 3;\n      }\n      .tool[data-tool=\"pastebin\"] {\n        grid-column: 2 / 3;\n        grid-row: 1 / 3;\n      }\n    }\n  }\n}\n\n.toolbar form.tool {\n  padding: 1rem;\n  border: 1px solid var(--upload_form_border_color);\n  background: var(--upload_form_background);\n  & > * {\n    margin-bottom: var(--tool-spacing-inside);\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n  p {\n    font-size: 0.8rem;\n    color: var(--upload_text_color);\n  }\n  input {\n    padding: 0.5rem;\n    margin-right: 0.2rem;\n    border-radius: 0.2rem;\n    border: 0;\n    display: inline;\n  }\n  button {\n    background: var(--upload_button_background);\n    padding: 0.5rem;\n    border-radius: 0.2rem;\n    color: var(--upload_button_text_color);\n    border: none;\n    min-width: max-content;\n  }\n  div {\n    display: flex;\n    align-items: baseline;\n    justify-content: space-between;\n  }\n}\n\n//\n// Toolbar tool specific styling\n//\n\n.toolbar .tool[data-tool=\"download\"] {\n  padding: 0.125rem;\n  display: flex;\n  flex-direction: row;\n  align-items: flex-start;\n  flex-wrap: wrap;\n\n  a {\n    background: var(--download_button_background);\n    padding: 0.5rem;\n    border-radius: 0.2rem;\n  }\n\n  a, a:visited {\n    color: var(--download_button_link_color);\n  }\n\n  a:hover {\n    background: var(--download_button_background_hover);\n    color: var(--download_button_link_color_hover);\n  }\n\n  a:not(:last-of-type) {\n    margin-right: 1rem;\n  }\n}\n\n.toolbar .tool[data-tool=\"pastebin\"] {\n  textarea {\n    width: 100%;\n    resize: vertical;\n    min-height: 4rem;\n    padding: 0.5rem;\n    border-radius: 0.2rem;\n    border: 0;\n  }\n}\n\n.form,\n.drag-form {\n  display: none;\n  background: var(--drag_background);\n  position: absolute;\n  border: 0.5rem dashed var(--drag_border_color);\n  width: calc(100% - 1rem);\n  height: calc(100% - 1rem);\n  text-align: center;\n  z-index: 2;\n  margin: 0 5px;\n}\n\n.form_title {\n  position: fixed;\n  color: var(--drag_text_color);\n  top: 50%;\n  width: 100%;\n  text-align: center;\n}\n\n.error {\n  margin: 2rem;\n}\n\n.error p {\n  margin: 1rem 0;\n  font-size: 0.9rem;\n  word-break: break-all;\n}\n\n.error p:first-of-type {\n  font-size: 1.25rem;\n  color: var(--error_color);\n  margin-bottom: 2rem;\n}\n\n.error p:nth-of-type(2) {\n  font-weight: bold;\n}\n\n.error-nav {\n  margin-top: 4rem;\n}\n\n@media (max-width: 760px) {\n  nav {\n    padding: 0 2.5rem;\n  }\n\n  .container {\n    padding: 1.5rem 2.5rem;\n  }\n\n  h1 {\n    font-size: 1.4em;\n  }\n\n  td:not(:nth-child(1)),\n  th:not(:nth-child(1)) {\n    display: none;\n  }\n\n  .mobile-info {\n    display: inline-flex;\n    align-items: center;\n    margin: auto;\n  }\n\n  table tbody tr td {\n    padding-top: 0;\n    padding-bottom: 0;\n  }\n\n  a {\n    padding: 0.5625rem 0;\n  }\n\n  a.directory {\n    flex-grow: 1;\n  }\n\n  .file-entry {\n    align-items: center;\n  }\n\n  a.root,\n  a.file {\n    flex-grow: 1;\n  }\n\n  .back {\n    display: flex;\n  }\n\n  .back {\n    right: 1.5rem;\n  }\n\n  $upload_container_height_mobile: 100vh;\n\n  .upload_area {\n    width: 100%;\n    height: 136px;\n    max-height: $upload_container_height_mobile;\n    max-width: unset;\n    min-width: unset;\n    bottom: 0;\n    transition: height 0.3s ease;\n\n    &.active {\n      right: 0;\n      left: 0;\n    }\n\n    #upload-toggle {\n      display: block;\n      transition: transform 0.3s ease;\n    }\n  }\n\n  .upload_container {\n    max-height: $upload_container_height_mobile;\n  }\n}\n\n@media (max-width: 600px) {\n  h1 {\n    font-size: 1.375em;\n  }\n\n  nav {\n    padding: 0 1rem;\n  }\n\n  .container {\n    padding: 1rem;\n  }\n}\n\n@media (max-width: 400px) {\n  nav {\n    padding: 0 0.5rem;\n  }\n\n  .container {\n    padding: 0.5rem;\n  }\n\n  h1 {\n    font-size: 1.375em;\n  }\n\n  .back {\n    right: 1.5rem;\n  }\n}\n\n@mixin theme($name) {\n  @if $name ==squirrel {\n    @include squirrel.theme();\n  }\n\n  @else if $name ==archlinux {\n    @include archlinux.theme();\n  }\n\n  @else if $name ==ayu_dark {\n    @include ayu_dark.theme();\n  }\n\n  @else if $name ==monokai {\n    @include monokai.theme();\n  }\n\n  @else if $name ==zenburn {\n    @include zenburn.theme();\n  }\n\n  @else {\n    @error \"Invalid theme: #{$name}\";\n  }\n}\n\n%active_theme_link {\n  font-weight: bold;\n  color: var(--switch_theme_active);\n}\n\n// when no specific theme is applied, highlight the `default` theme button in\n// the theme menu\nbody:not([data-theme]) nav .theme li[data-theme=\"default\"] a {\n  @extend %active_theme_link;\n}\n\n@each $theme in $themes {\n  body[data-theme=\"#{$theme}\"] {\n    @include theme($theme);\n\n    // highlight the currently active theme in the theme selection menu\n    nav .theme li[data-theme=\"#{$theme}\"] a {\n      @extend %active_theme_link;\n    }\n  }\n}\n"
  },
  {
    "path": "data/themes/archlinux.scss",
    "content": "$generate_default: true !default;\n\n@mixin theme {\n  --background: #383c4a;\n  --text_color: #fefefe;\n  --directory_link_color: #03a9f4;\n  --directory_link_color_visited: #0388f4;\n  --file_link_color: #ea95ff;\n  --file_link_color_visited: #a595ff;\n  --symlink_color: #66d9ef;\n  --table_background: #353946;\n  --table_text_color: #eeeeee;\n  --table_header_background: #5294e2;\n  --table_header_text_color: #eeeeee;\n  --table_header_active_color: #ffffff;\n  --active_row_color: #5194e259;\n  --odd_row_background: #404552;\n  --even_row_background: #4b5162;\n  --root_link_color: #abb2bb;\n  --download_button_background: #ea95ff;\n  --download_button_background_hover: #eea7ff;\n  --download_button_link_color: #ffffff;\n  --download_button_link_color_hover: #ffffff;\n  --back_button_background: #ea95ff;\n  --back_button_background_hover: #ea95ff;\n  --back_button_link_color: #ffffff;\n  --back_button_link_color_hover: #ffffff;\n  --date_text_color: #9ebbdc;\n  --at_color: #9ebbdc;\n  --switch_theme_background: #4b5162;\n  --switch_theme_link_color: #fefefe;\n  --switch_theme_active: #ea95ff;\n  --switch_theme_border: #6a728a;\n  --change_theme_link_color: #fefefe;\n  --change_theme_link_color_hover: #fefefe;\n  --upload_text_color: #fefefe;\n  --upload_form_border_color: #353946;\n  --upload_form_background: #4b5162;\n  --upload_button_background: #ea95ff;\n  --upload_button_text_color: #ffffff;\n  --rm_button_background: #ea95ff;\n  --rm_button_text_color: #ffffff;\n  --drag_background: #3333338f;\n  --drag_border_color: #fefefe;\n  --drag_text_color: #fefefe;\n  --size_background_color: #5294e2;\n  --size_text_color: #fefefe;\n  --error_color: #e44b4b;\n  --footer_color: #8eabcc;\n  --success_color: #52e28a;\n  --upload_modal_header_background: #5294e2;\n  --upload_modal_header_color: #eeeeee;\n  --upload_modal_sub_header_background: #35547a;\n  --upload_modal_file_item_background: #eeeeee;\n  --upload_modal_file_item_color: #111111;\n  --upload_modal_file_upload_complete_background: #cccccc;\n  --progress_bar_background: #5294e2;\n};\n\n@if $generate_default {\n  body {\n    @include theme;\n  }\n}\n"
  },
  {
    "path": "data/themes/ayu_dark.scss",
    "content": "$generate_default: true !default;\n\n@mixin theme {\n  --background: #0d1017;\n  --text_color: #bfbdb6;\n  --directory_link_color: #e6b450;\n  --directory_link_color_visited: #c99a3a;\n  --file_link_color: #aad94c;\n  --file_link_color_visited: #7fb032;\n  --symlink_color: #73b8ff;\n  --table_background: #131721;\n  --table_text_color: #bfbdb6;\n  --table_header_background: #1c2130;\n  --table_header_text_color: #bfbdb6;\n  --table_header_active_color: #e6b450;\n  --active_row_color: #e6b45030;\n  --odd_row_background: #111720;\n  --even_row_background: #0f1219;\n  --root_link_color: #39bae6;\n  --download_button_background: #e6b450;\n  --download_button_background_hover: #d9a53e;\n  --download_button_link_color: #0d1017;\n  --download_button_link_color_hover: #0d1017;\n  --back_button_background: #e6b450;\n  --back_button_background_hover: #d9a53e;\n  --back_button_link_color: #0d1017;\n  --back_button_link_color_hover: #0d1017;\n  --date_text_color: #39bae6;\n  --at_color: #39bae6;\n  --switch_theme_background: #131721;\n  --switch_theme_link_color: #bfbdb6;\n  --switch_theme_active: #e6b450;\n  --switch_theme_border: #1c2130;\n  --change_theme_link_color: #bfbdb6;\n  --change_theme_link_color_hover: #e6b450;\n  --upload_text_color: #bfbdb6;\n  --upload_form_border_color: #1c2130;\n  --upload_form_background: #131721;\n  --upload_button_background: #e6b450;\n  --upload_button_text_color: #0d1017;\n  --rm_button_background: #f26d78;\n  --rm_button_text_color: #0d1017;\n  --drag_background: #0d10178f;\n  --drag_border_color: #e6b450;\n  --drag_text_color: #bfbdb6;\n  --size_background_color: #1c2130;\n  --size_text_color: #bfbdb6;\n  --error_color: #d95757;\n  --footer_color: #39bae6;\n  --success_color: #7fd962;\n  --upload_modal_header_background: #1c2130;\n  --upload_modal_header_color: #bfbdb6;\n  --upload_modal_sub_header_background: #0d1017;\n  --upload_modal_file_item_background: #bfbdb6;\n  --upload_modal_file_item_color: #0d1017;\n  --upload_modal_file_upload_complete_background: #636a72;\n  --progress_bar_background: #e6b450;\n};\n\n@if $generate_default {\n  body {\n    @include theme;\n  }\n}\n"
  },
  {
    "path": "data/themes/monokai.scss",
    "content": "$generate_default: true !default;\n\n@mixin theme {\n  --background: #272822;\n  --text_color: #f8f8f2;\n  --directory_link_color: #f92672;\n  --directory_link_color_visited: #bc39a7;\n  --file_link_color: #a6e22e;\n  --file_link_color_visited: #4cb936;\n  --symlink_color: #29b8db;\n  --table_background: #3b3a32;\n  --table_text_color: #f8f8f0;\n  --table_header_background: #75715e;\n  --table_header_text_color: #f8f8f2;\n  --table_header_active_color: #e6db74;\n  --active_row_color: #ae81fe3d;\n  --odd_row_background: #3e3d32;\n  --even_row_background: #49483e;\n  --root_link_color: #66d9ef;\n  --download_button_background: #ae81ff;\n  --download_button_background_hover: #c6a6ff;\n  --download_button_link_color: #f8f8f0;\n  --download_button_link_color_hover: #f8f8f0;\n  --back_button_background: #ae81ff;\n  --back_button_background_hover: #ae81ff;\n  --back_button_link_color: #f8f8f0;\n  --back_button_link_color_hover: #f8f8f0;\n  --date_text_color: #66d9ef;\n  --at_color: #66d9ef;\n  --switch_theme_background: #3b3a32;\n  --switch_theme_link_color: #f8f8f2;\n  --switch_theme_active: #a6e22e;\n  --switch_theme_border: #49483e;\n  --change_theme_link_color: #f8f8f2;\n  --change_theme_link_color_hover: #f8f8f2;\n  --upload_text_color: #f8f8f2;\n  --upload_form_border_color: #3b3a32;\n  --upload_form_background: #49483e;\n  --upload_button_background: #ae81ff;\n  --upload_button_text_color: #f8f8f0;\n  --rm_button_background: #ae81ff;\n  --rm_button_text_color: #f8f8f0;\n  --drag_background: #3333338f;\n  --drag_border_color: #f8f8f2;\n  --drag_text_color: #f8f8f2;\n  --size_background_color: #75715e;\n  --size_text_color: #f8f8f2;\n  --error_color: #d02929;\n  --footer_color: #56c9df;\n  --success_color: #52e28a;\n  --upload_modal_header_background: #75715e;\n  --upload_modal_header_color: #eeeeee;\n  --upload_modal_sub_header_background: #323129;\n  --upload_modal_file_item_background: #eeeeee;\n  --upload_modal_file_item_color: #111111;\n  --upload_modal_file_upload_complete_background: #cccccc;\n  --progress_bar_background: #5294e2;\n};\n\n@if $generate_default {\n  body {\n    @include theme;\n  }\n}\n"
  },
  {
    "path": "data/themes/squirrel.scss",
    "content": "$generate_default: true !default;\n\n@mixin theme {\n  --background: #ffffff;\n  --text_color: #323232;\n  --directory_link_color: #d02474;\n  --directory_link_color_visited: #9b1985;\n  --file_link_color: #0086b3;\n  --file_link_color_visited: #974ec2;\n  --symlink_color: #ADD8E6;\n  --table_background: #ffffff;\n  --table_text_color: #323232;\n  --table_header_background: #323232;\n  --table_header_text_color: #f5f5f5;\n  --table_header_active_color: #ffffff;\n  --active_row_color: #f6f8fa;\n  --odd_row_background: #fbfbfb;\n  --even_row_background: #f2f2f2;\n  --root_link_color: #323232;\n  --download_button_background: #d02474;\n  --download_button_background_hover: #f52d8a;\n  --download_button_link_color: #ffffff;\n  --download_button_link_color_hover: #ffffff;\n  --back_button_background: #d02474;\n  --back_button_background_hover: #d02474;\n  --back_button_link_color: #ffffff;\n  --back_button_link_color_hover: #ffffff;\n  --date_text_color: #797979;\n  --at_color: #797979;\n  --switch_theme_background: #323232;\n  --switch_theme_link_color: #f5f5f5;\n  --switch_theme_active: #d02474;\n  --switch_theme_border: #49483e;\n  --change_theme_link_color: #f5f5f5;\n  --change_theme_link_color_hover: #f5f5f5;\n  --upload_text_color: #323232;\n  --upload_form_border_color: #d2d2d2;\n  --upload_form_background: #f2f2f2;\n  --upload_button_background: #d02474;\n  --upload_button_text_color: #ffffff;\n  --rm_button_background: #d02474;\n  --rm_button_text_color: #ffffff;\n  --drag_background: #3333338f;\n  --drag_border_color: #ffffff;\n  --drag_text_color: #ffffff;\n  --size_background_color: #323232;\n  --size_text_color: #ffffff;\n  --error_color: #d02424;\n  --footer_color: #898989;\n  --success_color: #52e28a;\n  --upload_modal_header_background: #323232;\n  --upload_modal_header_color: #eeeeee;\n  --upload_modal_sub_header_background: #171616;\n  --upload_modal_file_item_background: #eeeeee;\n  --upload_modal_file_item_color: #111111;\n  --upload_modal_file_upload_complete_background: #cccccc;\n  --progress_bar_background: #5294e2;\n};\n\n@if $generate_default {\n  body {\n    @include theme;\n  }\n}\n"
  },
  {
    "path": "data/themes/zenburn.scss",
    "content": "$generate_default: true !default;\n\n@mixin theme {\n  --background: #3f3f3f;\n  --text_color: #efefef;\n  --directory_link_color: #f0dfaf;\n  --directory_link_color_visited: #ebc390;\n  --file_link_color: #87d6d5;\n  --file_link_color_visited: #a7b9ec;\n  --symlink_color: #11a8cd;\n  --table_background: #4a4949;\n  --table_text_color: #efefef;\n  --table_header_background: #7f9f7f;\n  --table_header_text_color: #efefef;\n  --table_header_active_color: #efef8f;\n  --active_row_color: #7e9f7f9c;\n  --odd_row_background: #777777;\n  --even_row_background: #5a5a5a;\n  --root_link_color: #dca3a3;\n  --download_button_background: #cc9393;\n  --download_button_background_hover: #dca3a3;\n  --download_button_link_color: #efefef;\n  --download_button_link_color_hover: #efefef;\n  --back_button_background: #cc9393;\n  --back_button_background_hover: #cc9393;\n  --back_button_link_color: #efefef;\n  --back_button_link_color_hover: #efefef;\n  --date_text_color: #cfbfaf;\n  --at_color: #cfbfaf;\n  --switch_theme_background: #4a4949;\n  --switch_theme_link_color: #efefef;\n  --switch_theme_active: #efef8f;\n  --switch_theme_border: #5a5a5a;\n  --change_theme_link_color: #efefef;\n  --change_theme_link_color_hover: #efefef;\n  --upload_text_color: #efefef;\n  --upload_form_border_color: #4a4949;\n  --upload_form_background: #777777;\n  --upload_button_background: #cc9393;\n  --upload_button_text_color: #efefef;\n  --rm_button_background: #cc9393;\n  --rm_button_text_color: #efefef;\n  --drag_background: #3333338f;\n  --drag_border_color: #efefef;\n  --drag_text_color: #efefef;\n  --size_background_color: #7f9f7f;\n  --size_text_color: #efefef;\n  --error_color: #d06565;\n  --footer_color: #bfaf9f;\n  --success_color: #52e28a;\n  --upload_modal_header_background: #7f9f7f;\n  --upload_modal_header_color: #eeeeee;\n  --upload_modal_sub_header_background: #404e40;\n  --upload_modal_file_item_background: #eeeeee;\n  --upload_modal_file_item_color: #111111;\n  --upload_modal_file_upload_complete_background: #cccccc;\n  --progress_bar_background: #5294e2;\n};\n\n@if $generate_default {\n  body {\n    @include theme;\n  }\n}\n"
  },
  {
    "path": "packaging/miniserve@.service",
    "content": "[Unit]\nDescription=miniserve for %i\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nExecStart=/usr/bin/miniserve -- %I\n\nIPAccounting=yes\nIPAddressAllow=localhost\nIPAddressDeny=any\nDynamicUser=yes\nPrivateTmp=yes\nPrivateUsers=yes\nPrivateDevices=yes\nNoNewPrivileges=true\nProtectSystem=strict\nProtectHome=yes\nProtectClock=yes\nProtectControlGroups=yes\nProtectKernelLogs=yes\nProtectKernelModules=yes\nProtectKernelTunables=yes\nProtectProc=invisible\nCapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_DAC_READ_SEARCH\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "release.toml",
    "content": "sign-commit = true\nsign-tag = true\npre-release-replacements = [\n  {file=\"CHANGELOG.md\", search=\"Unreleased\", replace=\"{{version}}\"},\n  {file=\"CHANGELOG.md\", search=\"\\\\.\\\\.\\\\.HEAD\", replace=\"...{{tag_name}}\", exactly=1},\n  {file=\"CHANGELOG.md\", search=\"ReleaseDate\", replace=\"{{date}}\"},\n  {file=\"CHANGELOG.md\", search=\"<!-- next-header -->\", replace=\"<!-- next-header -->\\n\\n## [Unreleased] - ReleaseDate\"},\n  {file=\"CHANGELOG.md\", search=\"<!-- next-url -->\", replace=\"<!-- next-url -->\\n[Unreleased]: https://github.com/svenstaro/miniserve/compare/{{tag_name}}...HEAD\", exactly=1},\n]\n# Get rid of the default cargo-release \"chore: \" prefix in messages as we don't\n# use semantic commits in this repository.\npre-release-commit-message = \"Release {{crate_name}} version {{version}}\"\ntag-message = \"Release {{crate_name}} version {{version}}\"\n"
  },
  {
    "path": "rustfmt.toml",
    "content": "# This empty config file ensures the default formatter settings are enforced for\n# all contributors, regardless of their custom global settings.\n"
  },
  {
    "path": "src/archive.rs",
    "content": "use std::fs::File;\nuse std::io::{Cursor, Read, Write};\nuse std::path::{Path, PathBuf};\n\nuse libflate::gzip::Encoder;\nuse serde::Deserialize;\nuse strum::{Display, EnumIter, EnumString};\nuse tar::Builder;\nuse zip::{ZipWriter, write};\n\nuse crate::errors::RuntimeError;\n\n/// Available archive methods\n#[derive(Deserialize, Clone, Copy, EnumIter, EnumString, Display)]\n#[serde(rename_all = \"snake_case\")]\n#[strum(serialize_all = \"snake_case\")]\npub enum ArchiveMethod {\n    /// Gzipped tarball\n    TarGz,\n\n    /// Regular tarball\n    Tar,\n\n    /// Regular zip\n    Zip,\n}\n\nimpl ArchiveMethod {\n    pub fn extension(self) -> String {\n        match self {\n            Self::TarGz => \"tar.gz\",\n            Self::Tar => \"tar\",\n            Self::Zip => \"zip\",\n        }\n        .to_string()\n    }\n\n    pub fn content_type(self) -> String {\n        match self {\n            Self::TarGz => \"application/gzip\",\n            Self::Tar => \"application/tar\",\n            Self::Zip => \"application/zip\",\n        }\n        .to_string()\n    }\n\n    pub fn is_enabled(self, tar_enabled: bool, tar_gz_enabled: bool, zip_enabled: bool) -> bool {\n        match self {\n            Self::TarGz => tar_gz_enabled,\n            Self::Tar => tar_enabled,\n            Self::Zip => zip_enabled,\n        }\n    }\n\n    /// Make an archive out of the given directory, and write the output to the given writer.\n    ///\n    /// Recursively includes all files and subdirectories.\n    ///\n    /// If `skip_symlinks` is `true`, symlinks fill not be followed and will just be ignored.\n    pub fn create_archive<T, W>(\n        self,\n        dir: T,\n        skip_symlinks: bool,\n        out: W,\n    ) -> Result<(), RuntimeError>\n    where\n        T: AsRef<Path>,\n        W: std::io::Write,\n    {\n        let dir = dir.as_ref();\n        match self {\n            Self::TarGz => tar_gz(dir, skip_symlinks, out),\n            Self::Tar => tar_dir(dir, skip_symlinks, out),\n            Self::Zip => zip_dir(dir, skip_symlinks, out),\n        }\n    }\n}\n\n/// Write a gzipped tarball of `dir` in `out`.\nfn tar_gz<W>(dir: &Path, skip_symlinks: bool, out: W) -> Result<(), RuntimeError>\nwhere\n    W: std::io::Write,\n{\n    let mut out = Encoder::new(out).map_err(|e| RuntimeError::IoError(\"GZIP\".to_string(), e))?;\n\n    tar_dir(dir, skip_symlinks, &mut out)?;\n\n    out.finish()\n        .into_result()\n        .map_err(|e| RuntimeError::IoError(\"GZIP finish\".to_string(), e))?;\n\n    Ok(())\n}\n\n/// Write a tarball of `dir` in `out`.\n///\n/// The target directory will be saved as a top-level directory in the archive.\n///\n/// For example, consider this directory structure:\n///\n/// ```ignore\n/// a\n/// └── b\n///     └── c\n///         ├── e\n///         ├── f\n///         └── g\n/// ```\n///\n/// Making a tarball out of `\"a/b/c\"` will result in this archive content:\n///\n/// ```ignore\n/// c\n/// ├── e\n/// ├── f\n/// └── g\n/// ```\nfn tar_dir<W>(dir: &Path, skip_symlinks: bool, out: W) -> Result<(), RuntimeError>\nwhere\n    W: std::io::Write,\n{\n    let inner_folder = dir.file_name().ok_or_else(|| {\n        RuntimeError::InvalidPathError(\"Directory name terminates in \\\"..\\\"\".to_string())\n    })?;\n\n    let directory = inner_folder.to_str().ok_or_else(|| {\n        RuntimeError::InvalidPathError(\n            \"Directory name contains invalid UTF-8 characters\".to_string(),\n        )\n    })?;\n\n    tar(dir, directory.to_string(), skip_symlinks, out)\n        .map_err(|e| RuntimeError::ArchiveCreationError(\"tarball\".to_string(), Box::new(e)))\n}\n\n/// Writes a tarball of `dir` in `out`.\n///\n/// The content of `src_dir` will be saved in the archive as a folder named `inner_folder`.\nfn tar<W>(\n    src_dir: &Path,\n    inner_folder: String,\n    skip_symlinks: bool,\n    out: W,\n) -> Result<(), RuntimeError>\nwhere\n    W: std::io::Write,\n{\n    let mut tar_builder = Builder::new(out);\n\n    tar_builder.follow_symlinks(!skip_symlinks);\n\n    // Recursively adds the content of src_dir into the archive stream\n    tar_builder\n        .append_dir_all(inner_folder, src_dir)\n        .map_err(|e| {\n            RuntimeError::IoError(\n                format!(\n                    \"Failed to append the content of '{}' to the TAR archive\",\n                    src_dir.to_str().unwrap_or(\"file\")\n                ),\n                e,\n            )\n        })?;\n\n    // Finish the archive\n    tar_builder.into_inner().map_err(|e| {\n        RuntimeError::IoError(\"Failed to finish writing the TAR archive\".to_string(), e)\n    })?;\n\n    Ok(())\n}\n\n/// Write a zip of `dir` in `out`.\n///\n/// The target directory will be saved as a top-level directory in the archive.\n///\n/// For example, consider this directory structure:\n///\n/// ```ignore\n/// a\n/// └── b\n///     └── c\n///         ├── e\n///         ├── f\n///         └── g\n/// ```\n///\n/// Making a zip out of `\"a/b/c\"` will result in this archive content:\n///\n/// ```ignore\n/// c\n/// ├── e\n/// ├── f\n/// └── g\n/// ```\nfn create_zip_from_directory<W>(\n    out: W,\n    directory: &Path,\n    skip_symlinks: bool,\n) -> Result<(), RuntimeError>\nwhere\n    W: std::io::Write + std::io::Seek,\n{\n    let options =\n        write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);\n    let mut paths_queue: Vec<PathBuf> = vec![directory.to_path_buf()];\n    let zip_root_folder_name = directory.file_name().ok_or_else(|| {\n        RuntimeError::InvalidPathError(\"Directory name terminates in \\\"..\\\"\".to_string())\n    })?;\n\n    let mut zip_writer = ZipWriter::new(out);\n    let mut buffer = Vec::new();\n    while !paths_queue.is_empty() {\n        let next = paths_queue.pop().ok_or_else(|| {\n            RuntimeError::ArchiveCreationDetailError(\"Could not get path from queue\".to_string())\n        })?;\n        let current_dir = next.as_path();\n        let directory_entry_iterator = std::fs::read_dir(current_dir)\n            .map_err(|e| RuntimeError::IoError(\"Could not read directory\".to_string(), e))?;\n        let zip_directory = Path::new(zip_root_folder_name).join(\n            current_dir.strip_prefix(directory).map_err(|_| {\n                RuntimeError::ArchiveCreationDetailError(\n                    \"Could not append base directory\".to_string(),\n                )\n            })?,\n        );\n\n        for entry in directory_entry_iterator {\n            let entry_path = entry\n                .ok()\n                .ok_or_else(|| {\n                    RuntimeError::InvalidPathError(\n                        \"Directory name terminates in \\\"..\\\"\".to_string(),\n                    )\n                })?\n                .path();\n            let entry_metadata = std::fs::metadata(entry_path.clone()).map_err(|e| {\n                RuntimeError::IoError(\n                    format!(\n                        \"Could not get file metadata of '{}'\",\n                        entry_path.to_string_lossy()\n                    )\n                    .to_string(),\n                    e,\n                )\n            })?;\n\n            if entry_metadata.file_type().is_symlink() && skip_symlinks {\n                continue;\n            }\n            let current_entry_name = entry_path.file_name().ok_or_else(|| {\n                RuntimeError::InvalidPathError(\"Invalid file or directory name\".to_string())\n            })?;\n\n            // To let every software correctly parse the file structure in ZIP files that are produced\n            // on any platform (esp. Windows), always use forward slashes. The documentation:\n            // https://users.cs.jmu.edu/buchhofp/forensics/formats/pkzip.html\n            let relative_path = if cfg!(windows) {\n                let branch = zip_directory\n                    .as_os_str()\n                    .to_string_lossy()\n                    .trim_end_matches(r\"\\\") // every branch ends with two backslashes \"\\\\\".\n                    .replace(r\"\\\", \"/\"); // every branch uses backslash \"\\\" as path separators.\n                let leaf = current_entry_name.to_string_lossy();\n                format!(\"{branch}/{leaf}\") // construct a Unix-style path in the simplest way.\n            } else {\n                zip_directory\n                    .join(current_entry_name)\n                    .into_os_string()\n                    .to_string_lossy()\n                    .into_owned()\n            };\n\n            if entry_metadata.is_file() {\n                let mut f = File::open(&entry_path)\n                    .map_err(|e| RuntimeError::IoError(\"Could not open file\".to_string(), e))?;\n                f.read_to_end(&mut buffer).map_err(|e| {\n                    RuntimeError::IoError(\"Could not read from file\".to_string(), e)\n                })?;\n                zip_writer.start_file(relative_path, options).map_err(|_| {\n                    RuntimeError::ArchiveCreationDetailError(\n                        \"Could not add file path to ZIP\".to_string(),\n                    )\n                })?;\n                zip_writer.write(buffer.as_ref()).map_err(|_| {\n                    RuntimeError::ArchiveCreationDetailError(\n                        \"Could not write file to ZIP\".to_string(),\n                    )\n                })?;\n                buffer.clear();\n            } else if entry_metadata.is_dir() {\n                zip_writer\n                    .add_directory(relative_path, options)\n                    .map_err(|_| {\n                        RuntimeError::ArchiveCreationDetailError(\n                            \"Could not add directory path to ZIP\".to_string(),\n                        )\n                    })?;\n                paths_queue.push(entry_path.clone());\n            }\n        }\n    }\n\n    zip_writer.finish().map_err(|_| {\n        RuntimeError::ArchiveCreationDetailError(\"Could not finish writing ZIP archive\".to_string())\n    })?;\n    Ok(())\n}\n\n/// Writes a zip of `dir` in `out`.\n///\n/// The content of `src_dir` will be saved in the archive as the  folder named .\nfn zip_data<W>(src_dir: &Path, skip_symlinks: bool, mut out: W) -> Result<(), RuntimeError>\nwhere\n    W: std::io::Write,\n{\n    let mut data = Vec::new();\n    let memory_file = Cursor::new(&mut data);\n    create_zip_from_directory(memory_file, src_dir, skip_symlinks).map_err(|e| {\n        RuntimeError::ArchiveCreationError(\n            \"Failed to create the ZIP archive\".to_string(),\n            Box::new(e),\n        )\n    })?;\n\n    out.write_all(data.as_mut_slice())\n        .map_err(|e| RuntimeError::IoError(\"Failed to write the ZIP archive\".to_string(), e))?;\n\n    Ok(())\n}\n\nfn zip_dir<W>(dir: &Path, skip_symlinks: bool, out: W) -> Result<(), RuntimeError>\nwhere\n    W: std::io::Write,\n{\n    let inner_folder = dir.file_name().ok_or_else(|| {\n        RuntimeError::InvalidPathError(\"Directory name terminates in \\\"..\\\"\".to_string())\n    })?;\n\n    inner_folder.to_str().ok_or_else(|| {\n        RuntimeError::InvalidPathError(\n            \"Directory name contains invalid UTF-8 characters\".to_string(),\n        )\n    })?;\n\n    zip_data(dir, skip_symlinks, out)\n        .map_err(|e| RuntimeError::ArchiveCreationError(\"zip\".to_string(), Box::new(e)))\n}\n"
  },
  {
    "path": "src/args.rs",
    "content": "use std::fmt::Display;\nuse std::net::IpAddr;\nuse std::path::PathBuf;\n\nuse actix_web::http::header::{HeaderMap, HeaderName, HeaderValue};\nuse clap::{Parser, ValueEnum, ValueHint};\n\nuse crate::auth;\nuse crate::listing::{SortingMethod, SortingOrder};\nuse crate::renderer::ThemeSlug;\n\n#[derive(ValueEnum, Clone)]\npub enum MediaType {\n    Image,\n    Audio,\n    Video,\n}\n\n#[derive(Debug, ValueEnum, Clone, Default, Copy)]\npub enum DuplicateFile {\n    #[default]\n    Error,\n    Overwrite,\n    Rename,\n}\n\n#[derive(ValueEnum, Clone)]\npub enum SizeDisplay {\n    Human,\n    Exact,\n}\n\nimpl Display for SizeDisplay {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            SizeDisplay::Human => write!(f, \"human\"),\n            SizeDisplay::Exact => write!(f, \"exact\"),\n        }\n    }\n}\n\n#[derive(Debug, ValueEnum, Clone, Copy, Default)]\npub enum LogColor {\n    #[default]\n    Auto,\n    Always,\n    Never,\n}\n\n#[derive(Parser)]\n#[command(name = \"miniserve\", author, about, version)]\npub struct CliArgs {\n    /// Be verbose, includes emitting access logs\n    #[arg(short = 'v', long = \"verbose\", env = \"MINISERVE_VERBOSE\")]\n    pub verbose: bool,\n\n    /// Which path to serve\n    #[arg(value_hint = ValueHint::AnyPath, env = \"MINISERVE_PATH\")]\n    pub path: Option<PathBuf>,\n\n    /// The path to where file uploads will be written to before being moved to their\n    /// correct location. It's wise to make sure that this directory will be written to\n    /// disk and not into memory.\n    ///\n    /// This value will only be used **IF** file uploading is enabled. If this option is\n    /// not set, the operating system default temporary directory will be used.\n    #[arg(\n        long = \"temp-directory\",\n        value_hint = ValueHint::FilePath,\n        requires = \"allowed_upload_dir\",\n        value_parser(validate_is_dir_and_exists),\n        env = \"MINISERVER_TEMP_UPLOAD_DIRECTORY\")\n    ]\n    pub temp_upload_directory: Option<PathBuf>,\n\n    /// The name of a directory index file to serve, like \"index.html\"\n    ///\n    /// Normally, when miniserve serves a directory, it creates a listing for that directory.\n    /// However, if a directory contains this file, miniserve will serve that file instead.\n    #[arg(long, value_hint = ValueHint::FilePath, env = \"MINISERVE_INDEX\")]\n    pub index: Option<PathBuf>,\n\n    /// Activate SPA (Single Page Application) mode\n    ///\n    /// This will cause the file given by --index to be served for all non-existing file paths. In\n    /// effect, this will serve the index file whenever a 404 would otherwise occur in order to\n    /// allow the SPA router to handle the request instead.\n    #[arg(long, requires = \"index\", env = \"MINISERVE_SPA\")]\n    pub spa: bool,\n\n    /// Reduce output and silence warnings.\n    #[arg(long, env = \"MINISERVE_QUIET\")]\n    pub quiet: bool,\n\n    /// Activate Pretty URLs mode\n    ///\n    /// This will cause the server to serve the equivalent `.html` file indicated by the path.\n    ///\n    /// `/about` will try to find `about.html` and serve it.\n    #[arg(long, env = \"MINISERVE_PRETTY_URLS\")]\n    pub pretty_urls: bool,\n\n    /// Port to use\n    #[arg(\n        short = 'p',\n        long = \"port\",\n        default_value = \"8080\",\n        env = \"MINISERVE_PORT\"\n    )]\n    pub port: u16,\n\n    /// Interface to listen on\n    #[arg(\n        short = 'i',\n        long = \"interfaces\",\n        value_parser(parse_interface),\n        num_args(1),\n        env = \"MINISERVE_INTERFACE\"\n    )]\n    pub interfaces: Vec<IpAddr>,\n\n    /// Set authentication\n    ///\n    /// Currently supported formats:\n    /// username:password, username:sha256:hash, username:sha512:hash\n    /// (e.g. joe:123, joe:sha256:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3)\n    #[arg(\n        short = 'a',\n        long = \"auth\",\n        value_parser(parse_auth),\n        num_args(1),\n        env = \"MINISERVE_AUTH\",\n        verbatim_doc_comment\n    )]\n    pub auth: Vec<auth::RequiredAuth>,\n\n    /// Read authentication values from a file\n    ///\n    /// Example file content:\n    ///\n    /// joe:123\n    /// bob:sha256:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3\n    /// bill:\n    #[arg(long, value_hint = ValueHint::FilePath, env = \"MINISERVE_AUTH_FILE\", verbatim_doc_comment)]\n    pub auth_file: Option<PathBuf>,\n\n    /// Use a specific route prefix\n    #[arg(long = \"route-prefix\", env = \"MINISERVE_ROUTE_PREFIX\")]\n    pub route_prefix: Option<String>,\n\n    /// Generate a random 6-hexdigit route\n    #[arg(\n        long = \"random-route\",\n        conflicts_with(\"route_prefix\"),\n        env = \"MINISERVE_RANDOM_ROUTE\"\n    )]\n    pub random_route: bool,\n\n    /// Hide symlinks in listing and prevent them from being followed\n    #[arg(short = 'P', long = \"no-symlinks\", env = \"MINISERVE_NO_SYMLINKS\")]\n    pub no_symlinks: bool,\n\n    /// Show hidden files\n    #[arg(short = 'H', long = \"hidden\", env = \"MINISERVE_HIDDEN\")]\n    pub hidden: bool,\n\n    /// Default sorting method for file list\n    #[arg(\n        short = 'S',\n        long = \"default-sorting-method\",\n        default_value = \"name\",\n        ignore_case = true,\n        env = \"MINISERVE_DEFAULT_SORTING_METHOD\"\n    )]\n    pub default_sorting_method: SortingMethod,\n\n    /// Default sorting order for file list\n    #[arg(\n        short = 'O',\n        long = \"default-sorting-order\",\n        default_value = \"desc\",\n        ignore_case = true,\n        env = \"MINISERVE_DEFAULT_SORTING_ORDER\"\n    )]\n    pub default_sorting_order: SortingOrder,\n\n    /// Default color scheme\n    #[arg(\n        short = 'c',\n        long = \"color-scheme\",\n        default_value = \"squirrel\",\n        ignore_case = true,\n        env = \"MINISERVE_COLOR_SCHEME\"\n    )]\n    pub color_scheme: ThemeSlug,\n\n    /// Default color scheme\n    #[arg(\n        short = 'd',\n        long = \"color-scheme-dark\",\n        default_value = \"archlinux\",\n        ignore_case = true,\n        env = \"MINISERVE_COLOR_SCHEME_DARK\"\n    )]\n    pub color_scheme_dark: ThemeSlug,\n\n    /// Enable QR code display\n    #[arg(short = 'q', long = \"qrcode\", env = \"MINISERVE_QRCODE\")]\n    pub qrcode: bool,\n\n    /// Enable file uploading (and optionally specify for which directory)\n    ///\n    /// The provided path is not a physical file system path. Instead, it's relative to the serve\n    /// dir. For instance, if the serve dir is '/home/hello', set this to '/upload' to allow\n    /// uploading to '/home/hello/upload'.\n    /// When specified via environment variable, a path always needs to be specified.\n    #[arg(short = 'u', long = \"upload-files\", value_hint = ValueHint::FilePath, num_args(0..=1), value_delimiter(','), env = \"MINISERVE_ALLOWED_UPLOAD_DIR\")]\n    pub allowed_upload_dir: Option<Vec<PathBuf>>,\n\n    /// Configure amount of concurrent uploads when visiting the website. Must have\n    /// upload-files option enabled for this setting to matter.\n    ///\n    /// For example, a value of 4 would mean that the web browser will only upload\n    /// 4 files at a time to the web server when using the web browser interface.\n    ///\n    /// When the value is kept at 0, it attempts to resolve all the uploads at once\n    /// in the web browser.\n    ///\n    /// NOTE: Web pages have a limit of how many active HTTP connections that they\n    /// can make at one time, so even though you might set a concurrency limit of\n    /// 100, the browser might only make progress on the max amount of connections\n    /// it allows the web page to have open.\n    #[arg(\n        long = \"web-upload-files-concurrency\",\n        env = \"MINISERVE_WEB_UPLOAD_CONCURRENCY\",\n        default_value = \"0\"\n    )]\n    pub web_upload_concurrency: usize,\n\n    /// Set unix file permissions of uploaded files\n    ///\n    /// This takes an octal number, for example 0600. By default 0666 & ~umask is used to simulate\n    /// the system's default behavior.\n    #[cfg(unix)]\n    #[arg(\n        long = \"chmod\",\n        value_parser(parse_file_mode),\n        env = \"MINISERVE_CHMOD\",\n        requires = \"allowed_upload_dir\"\n    )]\n    pub chmod: Option<u16>,\n\n    /// Enable recursive directory size calculation\n    ///\n    /// This is disabled by default because it is a potentially fairly IO intensive operation.\n    #[arg(long = \"directory-size\", env = \"MINISERVE_DIRECTORY_SIZE\")]\n    pub directory_size: bool,\n\n    /// Enable creating directories\n    #[arg(\n        short = 'U',\n        long = \"mkdir\",\n        requires = \"allowed_upload_dir\",\n        env = \"MINISERVE_MKDIR_ENABLED\"\n    )]\n    pub mkdir_enabled: bool,\n\n    /// Enable creating pastebin 'pastes'\n    ///\n    /// 'pastes' are plaintext files created in the current directory. Creation requires file\n    /// uploads be enabled.\n    #[arg(\n        long = \"pastebin\",\n        requires = \"allowed_upload_dir\",\n        env = \"MINISERVE_PASTEBIN_ENABLED\"\n    )]\n    pub pastebin_enabled: bool,\n\n    /// Specify uploadable media types\n    #[arg(\n        short = 'm',\n        long = \"media-type\",\n        requires = \"allowed_upload_dir\",\n        env = \"MINISERVE_MEDIA_TYPE\"\n    )]\n    pub media_type: Option<Vec<MediaType>>,\n\n    /// Directly specify the uploadable media type expression\n    #[arg(\n        short = 'M',\n        long = \"raw-media-type\",\n        requires = \"allowed_upload_dir\",\n        conflicts_with = \"media_type\",\n        env = \"MINISERVE_RAW_MEDIA_TYPE\"\n    )]\n    pub media_type_raw: Option<String>,\n\n    /// What to do if existing files with same name is present during file upload\n    ///\n    /// If you enable renaming files, the renaming will occur by\n    /// adding a numerical suffix to the filename before the final\n    /// extension. For example file.txt will be uploaded as\n    /// file-1.txt, the number will be increased until an available\n    /// filename is found.\n    #[arg(\n        short = 'o',\n        long = \"on-duplicate-files\",\n        env = \"MINISERVE_ON_DUPLICATE_FILES\",\n        default_value = \"error\"\n    )]\n    pub on_duplicate_files: DuplicateFile,\n\n    /// Enable file and directory deletion (and optionally specify for which directory)\n    #[arg(\n        short = 'R',\n        long = \"rm-files\",\n        value_hint = ValueHint::DirPath,\n        num_args(0..=1),\n        value_delimiter(','),\n        env = \"MINISERVE_ALLOWED_RM_DIR\"\n    )]\n    pub allowed_rm_dir: Option<Vec<PathBuf>>,\n\n    /// Enable uncompressed tar archive generation\n    #[arg(short = 'r', long = \"enable-tar\", env = \"MINISERVE_ENABLE_TAR\")]\n    pub enable_tar: bool,\n\n    /// Enable gz-compressed tar archive generation\n    #[arg(short = 'g', long = \"enable-tar-gz\", env = \"MINISERVE_ENABLE_TAR_GZ\")]\n    pub enable_tar_gz: bool,\n\n    /// Enable zip archive generation\n    ///\n    /// WARNING: Zipping large directories can result in out-of-memory exception\n    /// because zip generation is done in memory and cannot be sent on the fly\n    #[arg(short = 'z', long = \"enable-zip\", env = \"MINISERVE_ENABLE_ZIP\")]\n    pub enable_zip: bool,\n\n    /// Compress response\n    ///\n    /// WARNING: Enabling this option may slow down transfers due to CPU overhead, so it is\n    /// disabled by default.\n    ///\n    /// Only enable this option if you know that your users have slow connections or if you want to\n    /// minimize your server's bandwidth usage.\n    #[arg(\n        short = 'C',\n        long = \"compress-response\",\n        env = \"MINISERVE_COMPRESS_RESPONSE\"\n    )]\n    pub compress_response: bool,\n\n    /// List directories first\n    #[arg(short = 'D', long = \"dirs-first\", env = \"MINISERVE_DIRS_FIRST\")]\n    pub dirs_first: bool,\n\n    /// Shown instead of host in page title and heading\n    #[arg(short = 't', long = \"title\", env = \"MINISERVE_TITLE\")]\n    pub title: Option<String>,\n\n    /// Inserts custom headers into the responses. Specify each header as a 'Header:Value' pair.\n    /// This parameter can be used multiple times to add multiple headers.\n    ///\n    /// Example:\n    /// --header \"Header1:Value1\" --header \"Header2:Value2\"\n    /// (If a header is already set or previously inserted, it will not be overwritten.)\n    #[arg(\n        long = \"header\",\n        value_parser(parse_header),\n        num_args(1),\n        env = \"MINISERVE_HEADER\"\n    )]\n    pub header: Vec<HeaderMap>,\n\n    /// Visualize symlinks in directory listing\n    #[arg(\n        short = 'l',\n        long = \"show-symlink-info\",\n        env = \"MINISERVE_SHOW_SYMLINK_INFO\"\n    )]\n    pub show_symlink_info: bool,\n\n    /// Hide version footer\n    #[arg(\n        short = 'F',\n        long = \"hide-version-footer\",\n        env = \"MINISERVE_HIDE_VERSION_FOOTER\"\n    )]\n    pub hide_version_footer: bool,\n\n    /// Hide theme selector\n    #[arg(long = \"hide-theme-selector\", env = \"MINISERVE_HIDE_THEME_SELECTOR\")]\n    pub hide_theme_selector: bool,\n\n    /// If enabled, display a wget command to recursively download the current directory\n    #[arg(\n        short = 'W',\n        long = \"show-wget-footer\",\n        env = \"MINISERVE_SHOW_WGET_FOOTER\"\n    )]\n    pub show_wget_footer: bool,\n\n    /// Generate completion file for a shell\n    #[arg(long = \"print-completions\", value_name = \"shell\")]\n    pub print_completions: Option<clap_complete::Shell>,\n\n    /// Generate man page\n    #[arg(long = \"print-manpage\")]\n    pub print_manpage: bool,\n\n    /// TLS certificate to use\n    #[cfg(feature = \"tls\")]\n    #[arg(long = \"tls-cert\", requires = \"tls_key\", value_hint = ValueHint::FilePath, env = \"MINISERVE_TLS_CERT\")]\n    pub tls_cert: Option<PathBuf>,\n\n    /// TLS private key to use\n    #[cfg(feature = \"tls\")]\n    #[arg(long = \"tls-key\", requires = \"tls_cert\", value_hint = ValueHint::FilePath, env = \"MINISERVE_TLS_KEY\")]\n    pub tls_key: Option<PathBuf>,\n\n    /// Enable README.md rendering in directories\n    #[arg(long, env = \"MINISERVE_README\")]\n    pub readme: bool,\n\n    /// Disable indexing\n    ///\n    /// This will prevent directory listings from being generated\n    /// and return an error instead.\n    #[arg(short = 'I', long, env = \"MINISERVE_DISABLE_INDEXING\")]\n    pub disable_indexing: bool,\n\n    /// Enable read-only WebDAV support (PROPFIND requests)\n    #[arg(long, env = \"MINISERVE_ENABLE_WEBDAV\")]\n    pub enable_webdav: bool,\n\n    /// Show served file size in exact bytes\n    #[arg(long, default_value_t = SizeDisplay::Human, env = \"MINISERVE_SIZE_DISPLAY\")]\n    pub size_display: SizeDisplay,\n\n    /// Optional external URL (e.g., 'http://external.example.com:8081') prepended to file links in listings.\n    ///\n    /// Allows serving files from a different URL than the browsing instance. Useful for setups like:\n    /// one authenticated instance for browsing, linking files (via this option) to a second,\n    /// non-indexed (-I) instance for direct downloads. This obscures the full file list on\n    /// the download server, while users can still copy direct file URLs for sharing.\n    /// The external URL is put verbatim in front of the relative location of the file, including the protocol.\n    /// The user should take care this results in a valid URL, no further checks are being done.\n    #[arg(long = \"file-external-url\", env = \"MINISERVE_FILE_EXTERNAL_URL\")]\n    pub file_external_url: Option<String>,\n\n    /// Set the color style of the log output\n    ///\n    /// \"auto\" (default) enables colors only when the output is a terminal.\n    /// \"always\" always enables colors.\n    /// \"never\" always disables colors.\n    #[arg(\n        long = \"log-color\",\n        env = \"MINISERVE_LOG_COLOR\",\n        default_value = \"auto\"\n    )]\n    pub log_color: LogColor,\n}\n\n/// Checks whether an interface is valid, i.e. it can be parsed into an IP address\nfn parse_interface(src: &str) -> Result<IpAddr, std::net::AddrParseError> {\n    src.parse::<IpAddr>()\n}\n\n/// Validate that a path passed in is a directory and it exists.\nfn validate_is_dir_and_exists(s: &str) -> Result<PathBuf, String> {\n    let path = PathBuf::from(s);\n    if path.exists() && path.is_dir() {\n        Ok(path)\n    } else {\n        Err(format!(\n            \"Upload temporary directory must exist and be a directory. \\\n            Validate that path {path:?} meets those requirements.\"\n        ))\n    }\n}\n\n#[derive(Clone, Debug, thiserror::Error)]\npub enum AuthParseError {\n    /// Might occur if the HTTP credential string does not respect the expected format\n    #[error(\n        \"Invalid format for credentials string. Expected username:password, username:sha256:hash or username:sha512:hash\"\n    )]\n    InvalidAuthFormat,\n\n    /// Might occur if the hash method is neither sha256 nor sha512\n    #[error(\"{0} is not a valid hashing method. Expected sha256 or sha512\")]\n    InvalidHashMethod(String),\n\n    /// Might occur if the HTTP auth hash password is not a valid hex code\n    #[error(\"Invalid format for password hash. Expected hex code\")]\n    InvalidPasswordHash,\n\n    /// Might occur if the HTTP auth password exceeds 255 characters\n    #[error(\"HTTP password length exceeds 255 characters\")]\n    PasswordTooLong,\n}\n\n/// Parse authentication requirement\npub fn parse_auth(src: &str) -> Result<auth::RequiredAuth, AuthParseError> {\n    use AuthParseError as E;\n\n    let mut split = src.splitn(3, ':');\n    let invalid_auth_format = Err(E::InvalidAuthFormat);\n\n    let username = match split.next() {\n        Some(username) => username,\n        None => return invalid_auth_format,\n    };\n\n    // second_part is either password in username:password or method in username:method:hash\n    let second_part = match split.next() {\n        // This allows empty passwords, as the spec does not forbid it\n        Some(password) => password,\n        None => return invalid_auth_format,\n    };\n\n    let password = if let Some(hash_hex) = split.next() {\n        let hash_bin = hex::decode(hash_hex).map_err(|_| E::InvalidPasswordHash)?;\n\n        match second_part {\n            \"sha256\" => auth::RequiredAuthPassword::Sha256(hash_bin),\n            \"sha512\" => auth::RequiredAuthPassword::Sha512(hash_bin),\n            _ => return Err(E::InvalidHashMethod(second_part.to_owned())),\n        }\n    } else {\n        // To make it Windows-compatible, the password needs to be shorter than 255 characters.\n        // After 255 characters, Windows will truncate the value.\n        // As for the username, the spec does not mention a limit in length\n        if second_part.len() > 255 {\n            return Err(E::PasswordTooLong);\n        }\n\n        auth::RequiredAuthPassword::Plain(second_part.to_owned())\n    };\n\n    Ok(auth::RequiredAuth {\n        username: username.to_owned(),\n        password,\n    })\n}\n\n/// Custom header parser (allow multiple headers input)\npub fn parse_header(src: &str) -> Result<HeaderMap, httparse::Error> {\n    let mut headers = [httparse::EMPTY_HEADER; 1];\n    let header = format!(\"{src}\\n\");\n    httparse::parse_headers(header.as_bytes(), &mut headers)?;\n\n    let mut header_map = HeaderMap::new();\n    if let Some(h) = headers.first()\n        && h.name != httparse::EMPTY_HEADER.name\n    {\n        header_map.insert(\n            HeaderName::from_bytes(h.name.as_bytes()).unwrap(),\n            HeaderValue::from_bytes(h.value).unwrap(),\n        );\n    }\n\n    Ok(header_map)\n}\n\n#[cfg(unix)]\npub fn parse_file_mode(src: &str) -> Result<u16, std::num::ParseIntError> {\n    u16::from_str_radix(src, 8)\n}\n\n#[rustfmt::skip]\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use rstest::rstest;\n    use pretty_assertions::assert_eq;\n\n    /// Helper function that creates a `RequiredAuth` structure\n    fn create_required_auth(username: &str, password: &str, encrypt: &str) -> auth::RequiredAuth {\n        use auth::*;\n        use RequiredAuthPassword::*;\n\n        let password = match encrypt {\n            \"plain\" => Plain(password.to_owned()),\n            \"sha256\" => Sha256(hex::decode(password).unwrap()),\n            \"sha512\" => Sha512(hex::decode(password).unwrap()),\n            _ => panic!(\"Unknown encryption type\"),\n        };\n\n        auth::RequiredAuth {\n            username: username.to_owned(),\n            password,\n        }\n    }\n\n    #[rstest(\n        auth_string, username, password, encrypt,\n        case(\"username:password\", \"username\", \"password\", \"plain\"),\n        case(\"username:sha256:abcd\", \"username\", \"abcd\", \"sha256\"),\n        case(\"username:sha512:abcd\", \"username\", \"abcd\", \"sha512\")\n    )]\n    fn parse_auth_valid(auth_string: &str, username: &str, password: &str, encrypt: &str) {\n        assert_eq!(\n            parse_auth(auth_string).unwrap(),\n            create_required_auth(username, password, encrypt),\n        );\n    }\n\n    #[rstest(\n        auth_string, err_msg,\n        case(\n            \"foo\",\n            \"Invalid format for credentials string. Expected username:password, username:sha256:hash or username:sha512:hash\"\n        ),\n        case(\n            \"username:blahblah:abcd\",\n            \"blahblah is not a valid hashing method. Expected sha256 or sha512\"\n        ),\n        case(\n            \"username:sha256:invalid\",\n            \"Invalid format for password hash. Expected hex code\"\n        ),\n        case(\n            \"username:sha512:invalid\",\n            \"Invalid format for password hash. Expected hex code\"\n        ),\n    )]\n    fn parse_auth_invalid(auth_string: &str, err_msg: &str) {\n        let err = parse_auth(auth_string).unwrap_err();\n        assert_eq!(format!(\"{err}\"), err_msg.to_owned());\n    }\n}\n"
  },
  {
    "path": "src/auth.rs",
    "content": "use actix_web::{HttpMessage, dev::ServiceRequest, web};\nuse actix_web_httpauth::extractors::basic::BasicAuth;\nuse sha2::{Digest, Sha256, Sha512};\n\nuse crate::errors::RuntimeError;\n\n#[derive(Clone, Debug)]\n/// HTTP Basic authentication parameters\npub struct BasicAuthParams {\n    pub username: String,\n    pub password: String,\n}\n\nimpl From<BasicAuth> for BasicAuthParams {\n    fn from(auth: BasicAuth) -> Self {\n        Self {\n            username: auth.user_id().to_string(),\n            password: auth.password().unwrap_or_default().to_string(),\n        }\n    }\n}\n\n#[derive(Clone, Debug, PartialEq, Eq)]\n/// `password` field of `RequiredAuth`\npub enum RequiredAuthPassword {\n    Plain(String),\n    Sha256(Vec<u8>),\n    Sha512(Vec<u8>),\n}\n\n#[derive(Clone, Debug, PartialEq, Eq)]\n/// Authentication structure to match `BasicAuthParams` against\npub struct RequiredAuth {\n    pub username: String,\n    pub password: RequiredAuthPassword,\n}\n\n/// Return `true` if `basic_auth` is matches any of `required_auth`\npub fn match_auth(basic_auth: &BasicAuthParams, required_auth: &[RequiredAuth]) -> bool {\n    required_auth\n        .iter()\n        .any(|RequiredAuth { username, password }| {\n            basic_auth.username == *username && compare_password(&basic_auth.password, password)\n        })\n}\n\n/// Return `true` if `basic_auth_pwd` meets `required_auth_pwd`'s requirement\npub fn compare_password(basic_auth_pwd: &str, required_auth_pwd: &RequiredAuthPassword) -> bool {\n    match &required_auth_pwd {\n        RequiredAuthPassword::Plain(required_password) => *basic_auth_pwd == *required_password,\n        RequiredAuthPassword::Sha256(password_hash) => {\n            compare_hash::<Sha256>(basic_auth_pwd, password_hash)\n        }\n        RequiredAuthPassword::Sha512(password_hash) => {\n            compare_hash::<Sha512>(basic_auth_pwd, password_hash)\n        }\n    }\n}\n\n/// Return `true` if hashing of `password` by `T` algorithm equals to `hash`\npub fn compare_hash<T: Digest>(password: &str, hash: &[u8]) -> bool {\n    get_hash::<T>(password) == hash\n}\n\n/// Get hash of a `text`\npub fn get_hash<T: Digest>(text: &str) -> Vec<u8> {\n    let mut hasher = T::new();\n    hasher.update(text);\n    hasher.finalize().to_vec()\n}\n\npub struct CurrentUser {\n    pub name: String,\n}\n\npub async fn handle_auth(\n    req: ServiceRequest,\n    cred: BasicAuth,\n) -> actix_web::Result<ServiceRequest, (actix_web::Error, ServiceRequest)> {\n    let required_auth = &req\n        .app_data::<web::Data<crate::MiniserveConfig>>()\n        .unwrap()\n        .auth;\n\n    req.extensions_mut().insert(CurrentUser {\n        name: cred.user_id().to_string(),\n    });\n\n    if match_auth(&cred.into(), required_auth) {\n        Ok(req)\n    } else {\n        Err((RuntimeError::InvalidHttpCredentials.into(), req))\n    }\n}\n\n#[rustfmt::skip]\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use rstest::{rstest, fixture};\n    use pretty_assertions::assert_eq;\n\n    /// Return a hashing function corresponds to given name\n    fn get_hash_func(name: &str) -> impl FnOnce(&str) -> Vec<u8> {\n        match name {\n            \"sha256\" => get_hash::<Sha256>,\n            \"sha512\" => get_hash::<Sha512>,\n            _ => panic!(\"Invalid hash method\"),\n        }\n    }\n\n    #[rstest(\n        password, hash_method, hash,\n        case(\"abc\", \"sha256\", \"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad\"),\n        case(\"abc\", \"sha512\", \"ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f\"),\n    )]\n    fn test_get_hash(password: &str, hash_method: &str, hash: &str) {\n        let hash_func = get_hash_func(hash_method);\n        let expected = hex::decode(hash).expect(\"Provided hash is not a valid hex code\");\n        let received = hash_func(password);\n        assert_eq!(received, expected);\n    }\n\n    /// Helper function that creates a `RequiredAuth` structure and encrypt `password` if necessary\n    fn create_required_auth(username: &str, password: &str, encrypt: &str) -> RequiredAuth {\n        use RequiredAuthPassword::*;\n\n        let password = match encrypt {\n            \"plain\" => Plain(password.to_owned()),\n            \"sha256\" => Sha256(get_hash::<sha2::Sha256>(password)),\n            \"sha512\" => Sha512(get_hash::<sha2::Sha512>(password)),\n            _ => panic!(\"Unknown encryption type\"),\n        };\n\n        RequiredAuth {\n            username: username.to_owned(),\n            password,\n        }\n    }\n\n    #[rstest(\n        should_pass, param_username, param_password, required_username, required_password, encrypt,\n        case(true, \"obi\", \"hello there\", \"obi\", \"hello there\", \"plain\"),\n        case(false, \"obi\", \"hello there\", \"obi\", \"hi!\", \"plain\"),\n        case(true, \"obi\", \"hello there\", \"obi\", \"hello there\", \"sha256\"),\n        case(false, \"obi\", \"hello there\", \"obi\", \"hi!\", \"sha256\"),\n        case(true, \"obi\", \"hello there\", \"obi\", \"hello there\", \"sha512\"),\n        case(false, \"obi\", \"hello there\", \"obi\", \"hi!\", \"sha512\")\n    )]\n    fn test_single_auth(\n        should_pass: bool,\n        param_username: &str,\n        param_password: &str,\n        required_username: &str,\n        required_password: &str,\n        encrypt: &str,\n    ) {\n        assert_eq!(\n            match_auth(\n                &BasicAuthParams {\n                    username: param_username.to_owned(),\n                    password: param_password.to_owned(),\n                },\n                &[create_required_auth(required_username, required_password, encrypt)],\n            ),\n            should_pass,\n        )\n    }\n\n    /// Helper function that creates a sample of multiple accounts\n    #[fixture]\n    fn account_sample() -> Vec<RequiredAuth> {\n        [\n            (\"usr0\", \"pwd0\", \"plain\"),\n            (\"usr1\", \"pwd1\", \"plain\"),\n            (\"usr2\", \"pwd2\", \"sha256\"),\n            (\"usr3\", \"pwd3\", \"sha256\"),\n            (\"usr4\", \"pwd4\", \"sha512\"),\n            (\"usr5\", \"pwd5\", \"sha512\"),\n        ]\n            .iter()\n            .map(|(username, password, encrypt)| create_required_auth(username, password, encrypt))\n            .collect()\n    }\n\n    #[rstest(\n        username, password,\n        case(\"usr0\", \"pwd0\"),\n        case(\"usr1\", \"pwd1\"),\n        case(\"usr2\", \"pwd2\"),\n        case(\"usr3\", \"pwd3\"),\n        case(\"usr4\", \"pwd4\"),\n        case(\"usr5\", \"pwd5\"),\n    )]\n    fn test_multiple_auth_pass(\n        account_sample: Vec<RequiredAuth>,\n        username: &str,\n        password: &str,\n    ) {\n        assert!(match_auth(\n            &BasicAuthParams {\n                username: username.to_owned(),\n                password: password.to_owned(),\n            },\n            &account_sample,\n        ));\n    }\n\n    #[rstest]\n    fn test_multiple_auth_wrong_username(account_sample: Vec<RequiredAuth>) {\n        assert_eq!(match_auth(\n            &BasicAuthParams {\n                username: \"unregistered user\".to_owned(),\n                password: \"pwd0\".to_owned(),\n            },\n            &account_sample,\n        ), false);\n    }\n\n    #[rstest(\n        username, password,\n        case(\"usr0\", \"pwd5\"),\n        case(\"usr1\", \"pwd4\"),\n        case(\"usr2\", \"pwd3\"),\n        case(\"usr3\", \"pwd2\"),\n        case(\"usr4\", \"pwd1\"),\n        case(\"usr5\", \"pwd0\"),\n    )]\n    fn test_multiple_auth_wrong_password(\n        account_sample: Vec<RequiredAuth>,\n        username: &str,\n        password: &str,\n    ) {\n        assert_eq!(match_auth(\n            &BasicAuthParams {\n                username: username.to_owned(),\n                password: password.to_owned(),\n            },\n            &account_sample,\n        ), false);\n    }\n}\n"
  },
  {
    "path": "src/config.rs",
    "content": "use std::{\n    fs::File,\n    io::{BufRead, BufReader},\n    net::{IpAddr, Ipv4Addr, Ipv6Addr},\n    path::{Path, PathBuf},\n};\n\nuse actix_web::http::header::HeaderMap;\nuse anyhow::{Context, Result, anyhow};\n\n#[cfg(feature = \"tls\")]\nuse rustls_pemfile as pemfile;\n\n#[cfg(unix)]\nuse crate::file_utils::get_default_filemode;\nuse crate::{\n    args::{CliArgs, DuplicateFile, LogColor, MediaType, parse_auth},\n    auth::RequiredAuth,\n    file_utils::sanitize_path,\n    listing::{SortingMethod, SortingOrder},\n    renderer::ThemeSlug,\n};\n\n/// Possible characters for random routes\nconst ROUTE_ALPHABET: [char; 16] = [\n    '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f',\n];\n\n#[derive(Debug, Clone)]\n/// Configuration of the Miniserve application\npub struct MiniserveConfig {\n    /// Enable verbose mode\n    pub verbose: bool,\n\n    /// Path to be served by miniserve\n    pub path: std::path::PathBuf,\n\n    /// Temporary directory that should be used when files are uploaded to the server\n    pub temp_upload_directory: Option<std::path::PathBuf>,\n\n    /// Port on which miniserve will be listening\n    pub port: u16,\n\n    /// IP address(es) on which miniserve will be available\n    pub interfaces: Vec<IpAddr>,\n\n    /// Enable HTTP basic authentication\n    pub auth: Vec<RequiredAuth>,\n\n    /// If false, miniserve will serve the current working directory\n    pub path_explicitly_chosen: bool,\n\n    /// Enable symlink resolution\n    pub no_symlinks: bool,\n\n    /// Show hidden files\n    pub show_hidden: bool,\n\n    /// Default sorting method\n    pub default_sorting_method: SortingMethod,\n\n    /// Default sorting order\n    pub default_sorting_order: SortingOrder,\n\n    /// Route prefix; Either empty or prefixed with slash\n    pub route_prefix: String,\n\n    /// Well-known healthcheck route (prefixed if route_prefix is provided)\n    pub healthcheck_route: String,\n\n    /// Well-known API route (prefixed if route_prefix is provided)\n    pub api_route: String,\n\n    /// Well-known favicon route (prefixed if route_prefix is provided)\n    pub favicon_route: String,\n\n    /// Well-known css route (prefixed if route_prefix is provided)\n    pub css_route: String,\n\n    /// Default color scheme\n    pub default_color_scheme: ThemeSlug,\n\n    /// Default dark mode color scheme\n    pub default_color_scheme_dark: ThemeSlug,\n\n    /// The name of a directory index file to serve, like \"index.html\"\n    ///\n    /// Normally, when miniserve serves a directory, it creates a listing for that directory.\n    /// However, if a directory contains this file, miniserve will serve that file instead.\n    pub index: Option<std::path::PathBuf>,\n\n    /// Activate SPA (Single Page Application) mode\n    ///\n    /// This will cause the file given by `index` to be served for all non-existing file paths. In\n    /// effect, this will serve the index file whenever a 404 would otherwise occur in order to\n    /// allow the SPA router to handle the request instead.\n    pub spa: bool,\n\n    /// Reduce output and silence warnings.\n    pub quiet: bool,\n\n    /// Activate Pretty URLs mode\n    ///\n    /// This will cause the server to serve the equivalent `.html` file indicated by the path.\n    ///\n    /// `/about` will try to find `about.html` and serve it.\n    pub pretty_urls: bool,\n\n    /// Enable QR code display\n    pub show_qrcode: bool,\n\n    /// Enable recursive directory size calculation\n    pub directory_size: bool,\n\n    /// Enable creating directories\n    pub mkdir_enabled: bool,\n\n    /// Enable file upload\n    pub file_upload: bool,\n\n    /// Enable pastepin creation\n    pub pastebin_enabled: bool,\n\n    /// Max amount of concurrency when uploading multiple files\n    pub web_upload_concurrency: usize,\n\n    /// chmod permissions of uploaded files\n    #[cfg(unix)]\n    pub upload_chmod: u16,\n\n    /// List of allowed upload directories\n    pub allowed_upload_dir: Vec<String>,\n\n    /// HTML accept attribute value\n    pub uploadable_media_type: Option<String>,\n\n    /// What to do on upload if filename already exists\n    pub on_duplicate_files: DuplicateFile,\n\n    /// Enable file and directory deletion\n    pub rm_enabled: bool,\n\n    /// List of allowed deletion directories\n    pub allowed_rm_dir: Vec<String>,\n\n    /// If false, creation of uncompressed tar archives is disabled\n    pub tar_enabled: bool,\n\n    /// If false, creation of gz-compressed tar archives is disabled\n    pub tar_gz_enabled: bool,\n\n    /// If false, creation of zip archives is disabled\n    pub zip_enabled: bool,\n\n    /// Enable  compress response\n    pub compress_response: bool,\n\n    /// If enabled, directories are listed first\n    pub dirs_first: bool,\n\n    /// Shown instead of host in page title and heading\n    pub title: Option<String>,\n\n    /// If specified, header will be added\n    pub header: Vec<HeaderMap>,\n\n    /// If specified, symlink destination will be shown\n    pub show_symlink_info: bool,\n\n    /// If enabled, version footer is hidden\n    pub hide_version_footer: bool,\n\n    /// If enabled, theme selector is hidden\n    pub hide_theme_selector: bool,\n\n    /// If enabled, display a wget command to recursively download the current directory\n    pub show_wget_footer: bool,\n\n    /// If enabled, render the readme from the current directory\n    pub readme: bool,\n\n    /// If enabled, indexing is disabled.\n    pub disable_indexing: bool,\n\n    /// If enabled, respond to WebDAV requests (read-only).\n    pub webdav_enabled: bool,\n\n    /// If enabled, will show in exact byte size of the file\n    pub show_exact_bytes: bool,\n\n    /// If set, use provided rustls config for TLS\n    #[cfg(feature = \"tls\")]\n    pub tls_rustls_config: Option<rustls::ServerConfig>,\n\n    #[cfg(not(feature = \"tls\"))]\n    pub tls_rustls_config: Option<()>,\n\n    /// Optional external URL to prepend to file links in listings\n    pub file_external_url: Option<String>,\n\n    /// Color choice for the log output\n    pub log_color: LogColor,\n}\n\nimpl MiniserveConfig {\n    /// Parses the command line arguments\n    pub fn try_from_args(args: CliArgs) -> Result<Self> {\n        let interfaces = if !args.interfaces.is_empty() {\n            args.interfaces\n        } else {\n            vec![\n                IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)),\n                IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),\n            ]\n        };\n\n        let route_prefix = match (args.route_prefix, args.random_route) {\n            (Some(prefix), _) => format!(\"/{}\", prefix.trim_matches('/')),\n            (_, true) => format!(\"/{}\", nanoid::nanoid!(6, &ROUTE_ALPHABET)),\n            _ => \"\".to_owned(),\n        };\n\n        let mut auth = args.auth;\n\n        if let Some(path) = args.auth_file {\n            let file = File::open(path)?;\n            let lines = BufReader::new(file).lines();\n\n            for line in lines {\n                auth.push(parse_auth(line?.as_str())?);\n            }\n        }\n\n        // Format some well-known routes at paths that are very unlikely to conflict with real\n        // files.\n        // If --random-route is enabled, in order to not leak the random generated route, we must not use it\n        // as static files prefix.\n        // Otherwise, we should apply route_prefix to static files.\n        let (healthcheck_route, api_route, favicon_route, css_route) = if args.random_route {\n            (\n                \"/__miniserve_internal/healthcheck\".into(),\n                \"/__miniserve_internal/api\".into(),\n                \"/__miniserve_internal/favicon.svg\".into(),\n                \"/__miniserve_internal/style.css\".into(),\n            )\n        } else {\n            (\n                format!(\"{}/{}\", route_prefix, \"__miniserve_internal/healthcheck\"),\n                format!(\"{}/{}\", route_prefix, \"__miniserve_internal/api\"),\n                format!(\"{}/{}\", route_prefix, \"__miniserve_internal/favicon.svg\"),\n                format!(\"{}/{}\", route_prefix, \"__miniserve_internal/style.css\"),\n            )\n        };\n\n        let default_color_scheme = args.color_scheme;\n        let default_color_scheme_dark = args.color_scheme_dark;\n\n        let path_explicitly_chosen = args.path.is_some() || args.index.is_some();\n\n        let port = match args.port {\n            0 => port_check::free_local_port().context(\"No free ports available\")?,\n            _ => args.port,\n        };\n\n        #[cfg(feature = \"tls\")]\n        let tls_rustls_server_config =\n            if let (Some(tls_cert), Some(tls_key)) = (args.tls_cert, args.tls_key) {\n                let cert_file = &mut BufReader::new(\n                    File::open(&tls_cert)\n                        .context(format!(\"Couldn't access TLS certificate {tls_cert:?}\"))?,\n                );\n                let key_file = &mut BufReader::new(\n                    File::open(&tls_key).context(format!(\"Couldn't access TLS key {tls_key:?}\"))?,\n                );\n                let cert_chain = pemfile::certs(cert_file)\n                    .map(|cert| cert.expect(\"Invalid certificate in certificate chain\"))\n                    .collect();\n                let private_key = pemfile::private_key(key_file)\n                    .context(\"Reading private key file\")?\n                    .expect(\"No private key found\");\n                let server_config = rustls::ServerConfig::builder()\n                    .with_no_client_auth()\n                    .with_single_cert(cert_chain, private_key)?;\n                Some(server_config)\n            } else {\n                None\n            };\n\n        #[cfg(not(feature = \"tls\"))]\n        let tls_rustls_server_config = None;\n\n        let uploadable_media_type = args.media_type_raw.or_else(|| {\n            args.media_type.map(|types| {\n                types\n                    .into_iter()\n                    .map(|t| match t {\n                        MediaType::Audio => \"audio/*\",\n                        MediaType::Image => \"image/*\",\n                        MediaType::Video => \"video/*\",\n                    })\n                    .collect::<Vec<_>>()\n                    .join(\",\")\n            })\n        });\n\n        let allowed_upload_dir = args\n            .allowed_upload_dir\n            .as_ref()\n            .map(|paths| validate_allowed_paths(paths, args.hidden))\n            .transpose()?\n            .unwrap_or_default();\n\n        let allowed_rm_dir = args\n            .allowed_rm_dir\n            .as_ref()\n            .map(|paths| validate_allowed_paths(paths, args.hidden))\n            .transpose()?\n            .unwrap_or_default();\n\n        let show_exact_bytes = match args.size_display {\n            crate::args::SizeDisplay::Human => false,\n            crate::args::SizeDisplay::Exact => true,\n        };\n        #[cfg(unix)]\n        let upload_chmod = args.chmod.unwrap_or_else(get_default_filemode);\n\n        Ok(Self {\n            verbose: args.verbose,\n            path: args.path.unwrap_or_else(|| PathBuf::from(\".\")),\n            temp_upload_directory: args.temp_upload_directory,\n            port,\n            interfaces,\n            auth,\n            path_explicitly_chosen,\n            no_symlinks: args.no_symlinks,\n            show_hidden: args.hidden,\n            default_sorting_method: args.default_sorting_method,\n            default_sorting_order: args.default_sorting_order,\n            route_prefix,\n            healthcheck_route,\n            api_route,\n            favicon_route,\n            css_route,\n            default_color_scheme,\n            default_color_scheme_dark,\n            index: args.index,\n            spa: args.spa,\n            quiet: args.quiet,\n            pretty_urls: args.pretty_urls,\n            on_duplicate_files: args.on_duplicate_files,\n            show_qrcode: args.qrcode,\n            directory_size: args.directory_size,\n            mkdir_enabled: args.mkdir_enabled,\n            file_upload: args.allowed_upload_dir.is_some(),\n            pastebin_enabled: args.pastebin_enabled,\n            web_upload_concurrency: args.web_upload_concurrency,\n            #[cfg(unix)]\n            upload_chmod,\n            allowed_upload_dir,\n            uploadable_media_type,\n            rm_enabled: args.allowed_rm_dir.is_some(),\n            allowed_rm_dir,\n            tar_enabled: args.enable_tar,\n            tar_gz_enabled: args.enable_tar_gz,\n            zip_enabled: args.enable_zip,\n            dirs_first: args.dirs_first,\n            title: args.title,\n            header: args.header,\n            show_symlink_info: args.show_symlink_info,\n            hide_version_footer: args.hide_version_footer,\n            hide_theme_selector: args.hide_theme_selector,\n            show_wget_footer: args.show_wget_footer,\n            readme: args.readme,\n            disable_indexing: args.disable_indexing,\n            webdav_enabled: args.enable_webdav,\n            tls_rustls_config: tls_rustls_server_config,\n            compress_response: args.compress_response,\n            show_exact_bytes,\n            file_external_url: args.file_external_url,\n            log_color: args.log_color,\n        })\n    }\n}\n\nfn validate_allowed_paths(paths: &[impl AsRef<Path>], allow_hidden: bool) -> Result<Vec<String>> {\n    paths\n        .iter()\n        .map(|p| {\n            sanitize_path(p, allow_hidden)\n                .map(|p| p.display().to_string().replace('\\\\', \"/\"))\n                .ok_or(anyhow!(\"Illegal path {:?}\", p.as_ref()))\n        })\n        .collect()\n}\n"
  },
  {
    "path": "src/consts.rs",
    "content": "use fast_qr::ECL;\n\n/// The error correction level to use for all QR code generation.\npub const QR_EC_LEVEL: ECL = ECL::L;\n\n/// The margin size for the SVG QR code on the webpage.\npub const SVG_QR_MARGIN: usize = 1;\n"
  },
  {
    "path": "src/errors.rs",
    "content": "use std::str::FromStr;\n\nuse actix_web::{\n    HttpRequest, HttpResponse, ResponseError,\n    body::{BoxBody, MessageBody},\n    dev::{ResponseHead, ServiceRequest, ServiceResponse},\n    http::{StatusCode, header},\n    middleware::Next,\n    web,\n};\nuse thiserror::Error;\n\nuse crate::{MiniserveConfig, renderer::render_error};\n\n#[derive(Debug, Error)]\npub enum StartupError {\n    /// Any kind of IO errors\n    #[error(\"{0}\\ncaused by: {1}\")]\n    IoError(String, std::io::Error),\n\n    /// In case miniserve was invoked without an interactive terminal and without an explicit path\n    #[error(\"Refusing to start as no explicit serve path was set and no interactive terminal was attached\nPlease set an explicit serve path like: `miniserve /my/path`\")]\n    NoExplicitPathAndNoTerminal,\n\n    /// In case miniserve was invoked with --no-symlinks but the serve path is a symlink\n    #[error(\"The -P|--no-symlinks option was provided but the serve path '{0}' is a symlink\")]\n    NoSymlinksOptionWithSymlinkServePath(String),\n\n    #[error(\"The --enable-webdav option was provided, but the serve path '{0}' is a file\")]\n    WebdavWithFileServePath(String),\n}\n\n#[derive(Debug, Error)]\npub enum RuntimeError {\n    /// Any kind of IO errors\n    #[error(\"{0}\\ncaused by: {1}\")]\n    IoError(String, std::io::Error),\n\n    /// Might occur during file upload, when processing the multipart request fails\n    #[error(\"Failed to process multipart request\\ncaused by: {0}\")]\n    MultipartError(String),\n\n    /// Might occur during file upload\n    #[error(\"File already exists, and the on_duplicate_files option is set to error out\")]\n    DuplicateFileError,\n\n    /// Uploaded hash not correct\n    #[error(\"File hash that was provided did not match checksum of uploaded file\")]\n    UploadHashMismatchError,\n\n    /// Upload not allowed\n    #[error(\"Upload not allowed to this directory\")]\n    UploadForbiddenError,\n\n    /// Remove not allowed\n    #[error(\"Remove not allowed to this directory\")]\n    RmForbiddenError,\n\n    /// Any error related to an invalid path (failed to retrieve entry name, unexpected entry type, etc)\n    #[error(\"Invalid path\\ncaused by: {0}\")]\n    InvalidPathError(String),\n\n    /// Might occur if the user has insufficient permissions to create an entry in a given directory\n    #[error(\"Insufficient permissions to create file in {0}\")]\n    InsufficientPermissionsError(String),\n\n    /// Any error related to parsing\n    #[error(\"Failed to parse {0}\\ncaused by: {1}\")]\n    ParseError(String, String),\n\n    /// Might occur when the creation of an archive fails\n    #[error(\"An error occurred while creating the {0}\\ncaused by: {1}\")]\n    ArchiveCreationError(String, Box<RuntimeError>),\n\n    /// More specific archive creation failure reason\n    #[error(\"{0}\")]\n    ArchiveCreationDetailError(String),\n\n    /// Might occur when the HTTP credentials are not correct\n    #[error(\"Invalid credentials for HTTP authentication\")]\n    InvalidHttpCredentials,\n\n    /// Might occur when an HTTP request is invalid\n    #[error(\"Invalid HTTP request\\ncaused by: {0}\")]\n    InvalidHttpRequestError(String),\n\n    /// Might occur when trying to access a page that does not exist\n    #[error(\"Route {0} could not be found\")]\n    RouteNotFoundError(String),\n}\n\nimpl ResponseError for RuntimeError {\n    fn status_code(&self) -> StatusCode {\n        use RuntimeError as E;\n        use StatusCode as S;\n        match self {\n            E::IoError(_, _) => S::INTERNAL_SERVER_ERROR,\n            E::UploadHashMismatchError => S::BAD_REQUEST,\n            E::MultipartError(_) => S::BAD_REQUEST,\n            E::DuplicateFileError => S::CONFLICT,\n            E::UploadForbiddenError => S::FORBIDDEN,\n            E::RmForbiddenError => S::FORBIDDEN,\n            E::InvalidPathError(_) => S::BAD_REQUEST,\n            E::InsufficientPermissionsError(_) => S::FORBIDDEN,\n            E::ParseError(_, _) => S::BAD_REQUEST,\n            E::ArchiveCreationError(_, err) => err.status_code(),\n            E::ArchiveCreationDetailError(_) => S::INTERNAL_SERVER_ERROR,\n            E::InvalidHttpCredentials => S::UNAUTHORIZED,\n            E::InvalidHttpRequestError(_) => S::BAD_REQUEST,\n            E::RouteNotFoundError(_) => S::NOT_FOUND,\n        }\n    }\n\n    fn error_response(&self) -> HttpResponse {\n        log_error_chain(self.to_string());\n\n        let mut resp = HttpResponse::build(self.status_code());\n        if let Self::InvalidHttpCredentials = self {\n            resp.append_header((\n                header::WWW_AUTHENTICATE,\n                header::HeaderValue::from_static(\"Basic realm=\\\"miniserve\\\"\"),\n            ));\n        }\n\n        resp.content_type(mime::TEXT_PLAIN_UTF_8)\n            .body(self.to_string())\n    }\n}\n\n/// Middleware to convert plain-text error responses to user-friendly web pages\npub async fn error_page_middleware(\n    req: ServiceRequest,\n    next: Next<impl MessageBody + 'static>,\n) -> Result<ServiceResponse<impl MessageBody>, actix_web::Error> {\n    let res = next.call(req).await?.map_into_boxed_body();\n\n    if (res.status().is_client_error() || res.status().is_server_error())\n        && res.request().path() != \"/upload\"\n        && res\n            .headers()\n            .get(header::CONTENT_TYPE)\n            .map(AsRef::as_ref)\n            .and_then(|s| std::str::from_utf8(s).ok())\n            .and_then(|s| mime::Mime::from_str(s).ok())\n            .as_ref()\n            .map(mime::Mime::essence_str)\n            == Some(mime::TEXT_PLAIN.as_ref())\n    {\n        let req = res.request().clone();\n        Ok(res.map_body(|head, body| map_error_page(&req, head, body)))\n    } else {\n        Ok(res)\n    }\n}\n\nfn map_error_page(req: &HttpRequest, head: &mut ResponseHead, body: BoxBody) -> BoxBody {\n    let error_msg = match body.try_into_bytes() {\n        Ok(bytes) => bytes,\n        Err(body) => return body,\n    };\n\n    let error_msg = match std::str::from_utf8(&error_msg) {\n        Ok(msg) => msg,\n        _ => return BoxBody::new(error_msg),\n    };\n\n    let conf = req.app_data::<web::Data<MiniserveConfig>>().unwrap();\n    let return_address = req\n        .headers()\n        .get(header::REFERER)\n        .and_then(|h| h.to_str().ok())\n        .unwrap_or(\"/\");\n\n    head.headers.insert(\n        header::CONTENT_TYPE,\n        mime::TEXT_HTML_UTF_8.essence_str().try_into().unwrap(),\n    );\n\n    BoxBody::new(render_error(error_msg, head.status, conf, return_address).into_string())\n}\n\npub fn log_error_chain(description: String) {\n    for cause in description.lines() {\n        log::error!(\"{cause}\");\n    }\n}\n"
  },
  {
    "path": "src/file_op.rs",
    "content": "//! Handlers for file upload and removal\n\n#[cfg(target_family = \"unix\")]\nuse std::collections::HashSet;\n\nuse std::io::ErrorKind;\n\n#[cfg(target_family = \"unix\")]\nuse std::os::unix::fs::MetadataExt;\n\nuse std::path::{Component, Path, PathBuf};\n\n#[cfg(target_family = \"unix\")]\nuse std::sync::Arc;\n\nuse actix_web::{HttpRequest, HttpResponse, http::header, web};\nuse async_walkdir::WalkDir;\nuse futures::{StreamExt, TryStreamExt};\nuse log::{error, info, warn};\nuse serde::Deserialize;\nuse sha2::digest::DynDigest;\nuse sha2::{Digest, Sha256, Sha512};\nuse tempfile::NamedTempFile;\nuse tokio::fs;\nuse tokio::io::AsyncWriteExt;\n\n#[cfg(target_family = \"unix\")]\nuse tokio::sync::RwLock;\n\nuse crate::{\n    args::DuplicateFile, config::MiniserveConfig, errors::RuntimeError,\n    file_utils::contains_symlink, file_utils::sanitize_path,\n};\n\nenum FileHash {\n    SHA256(String),\n    SHA512(String),\n}\n\nimpl FileHash {\n    pub fn get_hasher(&self) -> Box<dyn DynDigest> {\n        match self {\n            Self::SHA256(_) => Box::new(Sha256::new()),\n            Self::SHA512(_) => Box::new(Sha512::new()),\n        }\n    }\n\n    pub fn get_hash(&self) -> &str {\n        match self {\n            Self::SHA256(string) => string,\n            Self::SHA512(string) => string,\n        }\n    }\n}\n\n/// Get the recursively calculated dir size for a given dir\n///\n/// Counts hardlinked files only once if the OS supports hardlinks.\n///\n/// Expects `dir` to be sanitized. This function doesn't do any sanitization itself.\npub async fn recursive_dir_size(dir: &Path) -> Result<u64, RuntimeError> {\n    #[cfg(target_family = \"unix\")]\n    let seen_inodes = Arc::new(RwLock::new(HashSet::new()));\n\n    let mut entries = WalkDir::new(dir);\n\n    let mut total_size = 0;\n    loop {\n        match entries.next().await {\n            Some(Ok(entry)) => {\n                if let Ok(metadata) = entry.metadata().await\n                    && metadata.is_file()\n                {\n                    // On Unix, we want to filter inodes that we've already seen so we get a\n                    // more accurate count of real size used on disk.\n                    #[cfg(target_family = \"unix\")]\n                    {\n                        let (device_id, inode) = (metadata.dev(), metadata.ino());\n\n                        // Check if this file has been seen before based on its device ID and\n                        // inode number\n                        if seen_inodes.read().await.contains(&(device_id, inode)) {\n                            continue;\n                        } else {\n                            seen_inodes.write().await.insert((device_id, inode));\n                        }\n                    }\n                    total_size += metadata.len();\n                }\n            }\n            Some(Err(e)) => {\n                if let Some(io_err) = e.into_io() {\n                    match io_err.kind() {\n                        ErrorKind::PermissionDenied => warn!(\n                            \"Error trying to read file when calculating dir size: {io_err}, ignoring\"\n                        ),\n                        _ => return Err(RuntimeError::InvalidPathError(io_err.to_string())),\n                    }\n                }\n            }\n            None => break,\n        }\n    }\n    Ok(total_size)\n}\n\n/// Saves file data from a multipart form field (`field`) to `file_path`. Optionally overwriting\n/// existing file and comparing the uploaded file checksum to the user provided `file_hash`.\n///\n/// Returns total bytes written to file.\nasync fn save_file(\n    field: &mut actix_multipart::Field,\n    mut file_path: PathBuf,\n    on_duplicate_files: DuplicateFile,\n    file_checksum: Option<&FileHash>,\n    temporary_upload_directory: Option<&PathBuf>,\n    #[cfg(unix)] chmod: u16,\n) -> Result<u64, RuntimeError> {\n    if file_path.exists() {\n        match on_duplicate_files {\n            DuplicateFile::Error => return Err(RuntimeError::DuplicateFileError),\n            DuplicateFile::Overwrite => (),\n            DuplicateFile::Rename => {\n                // extract extension of the file and the file stem without extension\n                // file.txt => (file, txt)\n                let file_name = file_path.file_stem().unwrap_or_default().to_string_lossy();\n                let file_ext = file_path.extension().map(|s| s.to_string_lossy());\n                for i in 1.. {\n                    // increment the number N in {file_name}-{N}.{file_ext}\n                    // format until available name is found (e.g. file-1.txt, file-2.txt, etc)\n                    let fp = if let Some(ext) = &file_ext {\n                        file_path.with_file_name(format!(\"{file_name}-{i}.{ext}\"))\n                    } else {\n                        file_path.with_file_name(format!(\"{file_name}-{i}\"))\n                    };\n                    // If we have a file name that doesn't exist yet then we'll use that.\n                    if !fp.exists() {\n                        file_path = fp;\n                        break;\n                    }\n                }\n            }\n        }\n    }\n\n    let temp_upload_directory = temporary_upload_directory.cloned();\n    // Tempfile doesn't support async operations, so we'll do it on a background thread.\n    let temp_upload_directory_task = tokio::task::spawn_blocking(move || {\n        // If the user provided a temporary directory path, then use it.\n        if let Some(temp_directory) = temp_upload_directory {\n            NamedTempFile::new_in(temp_directory)\n        } else {\n            NamedTempFile::new()\n        }\n    });\n\n    // Validate that the temporary task completed successfully.\n    let named_temp_file_task = match temp_upload_directory_task.await {\n        Ok(named_temp_file) => Ok(named_temp_file),\n        Err(err) => Err(RuntimeError::MultipartError(format!(\n            \"Failed to complete spawned task to create named temp file. {err}\",\n        ))),\n    }?;\n\n    // Validate the the temporary file was created successfully.\n    let named_temp_file = match named_temp_file_task {\n        Err(err) if err.kind() == ErrorKind::PermissionDenied => Err(\n            RuntimeError::InsufficientPermissionsError(file_path.display().to_string()),\n        ),\n        Err(err) => Err(RuntimeError::IoError(\n            format!(\"Failed to create temporary file {}\", file_path.display()),\n            err,\n        )),\n        Ok(file) => Ok(file),\n    }?;\n\n    // Convert the temporary file into a non-temporary file. This allows us\n    // to control the lifecycle of the file. This is useful for us because\n    // we need to convert the temporary file into an async enabled file and\n    // on successful upload, we want to move it to the target directory.\n    let (file, temp_path) = named_temp_file\n        .keep()\n        .map_err(|err| RuntimeError::IoError(\"Failed to keep temporary file\".into(), err.error))?;\n    let mut temp_file = tokio::fs::File::from_std(file);\n\n    let mut written_len = 0;\n    let mut hasher = file_checksum.as_ref().map(|h| h.get_hasher());\n    let mut save_upload_file_error: Option<RuntimeError> = None;\n\n    // This while loop take a stream (in this case `field`) and awaits\n    // new chunks from the websocket connection. The while loop reads\n    // the file from the HTTP connection and writes it to disk or until\n    // the stream from the multipart request is aborted.\n    while let Some(Ok(bytes)) = field.next().await {\n        // If the hasher exists (if the user has also sent a chunksum with the request)\n        // then we want to update the hasher with the new bytes uploaded.\n        if let Some(hasher) = hasher.as_mut() {\n            hasher.update(&bytes)\n        }\n        // Write the bytes from the stream into our temporary file.\n        if let Err(e) = temp_file.write_all(&bytes).await {\n            // Failed to write to file. Drop it and return the error\n            save_upload_file_error =\n                Some(RuntimeError::IoError(\"Failed to write to file\".into(), e));\n            break;\n        }\n        // record the bytes written to the file.\n        written_len += bytes.len() as u64;\n    }\n\n    if save_upload_file_error.is_none() {\n        // Flush the changes to disk so that we are sure they are there.\n        if let Err(e) = temp_file.flush().await {\n            save_upload_file_error = Some(RuntimeError::IoError(\n                \"Failed to flush all the file writes to disk\".into(),\n                e,\n            ));\n        }\n    }\n\n    // Drop the file expcitly here because IF there is an error when writing to the\n    // temp file, we won't be able to remove as per the comment in `tokio::fs::remove_file`\n    // > Note that there is no guarantee that the file is immediately deleted\n    // > (e.g. depending on platform, other open file descriptors may prevent immediate removal).\n    drop(temp_file);\n\n    // If there was an error during uploading.\n    if let Some(e) = save_upload_file_error {\n        // If there was an error when writing the file to disk, remove it and return\n        // the error that was encountered.\n        let _ = tokio::fs::remove_file(temp_path).await;\n        return Err(e);\n    }\n\n    // There isn't a way to get notified when a request is cancelled\n    // by the user in actix it seems. References:\n    // - https://github.com/actix/actix-web/issues/1313\n    // - https://github.com/actix/actix-web/discussions/3011\n    // Therefore, we are relying on the fact that the web UI uploads a\n    // hash of the file to determine if it was completed uploaded or not.\n    if let Some(hasher) = hasher\n        && let Some(expected_hash) = file_checksum.as_ref().map(|f| f.get_hash())\n    {\n        let actual_hash = hex::encode(hasher.finalize());\n        if actual_hash != expected_hash {\n            warn!(\n                \"The expected file hash {expected_hash} did not match the calculated hash of {actual_hash}. This can be caused if a file upload was aborted.\"\n            );\n            let _ = tokio::fs::remove_file(&temp_path).await;\n            return Err(RuntimeError::UploadHashMismatchError);\n        }\n    }\n\n    info!(\"File upload successful to {temp_path:?}. Moving to {file_path:?}\",);\n    if let Err(err) = tokio::fs::rename(&temp_path, &file_path).await {\n        match err.kind() {\n            ErrorKind::CrossesDevices => {\n                warn!(\n                    \"File writen to {temp_path:?} must be copied to {file_path:?} because it's on a different filesystem\"\n                );\n                let copy_result = tokio::fs::copy(&temp_path, &file_path).await;\n                if let Err(e) = tokio::fs::remove_file(&temp_path).await {\n                    error!(\"Failed to clean up temp file at {temp_path:?} with error {e:?}\");\n                }\n                copy_result.map_err(|e| {\n                    RuntimeError::IoError(\n                        format!(\"Failed to copy file from {temp_path:?} to {file_path:?}\"),\n                        e,\n                    )\n                })?;\n            }\n            _ => {\n                let _ = tokio::fs::remove_file(&temp_path).await;\n                return Err(RuntimeError::IoError(\n                    format!(\"Failed to move temporary file {temp_path:?} to {file_path:?}\",),\n                    err,\n                ));\n            }\n        }\n    }\n\n    #[cfg(unix)]\n    {\n        info!(\"Changing file mode (chmod) to {chmod:o}\");\n        use std::os::unix::fs::PermissionsExt;\n        let perms = std::fs::Permissions::from_mode(chmod.into());\n        if let Err(err) = tokio::fs::set_permissions(&file_path, perms).await {\n            return Err(RuntimeError::IoError(\n                format!(\"Failed to chmod {chmod:o} {file_path:?}\"),\n                err,\n            ));\n        }\n    }\n\n    Ok(written_len)\n}\n\nstruct HandleMultipartOpts<'a> {\n    on_duplicate_files: DuplicateFile,\n    allow_mkdir: bool,\n    allow_hidden_paths: bool,\n    allow_symlinks: bool,\n    file_hash: Option<&'a FileHash>,\n    upload_directory: Option<&'a PathBuf>,\n}\n\n/// Handles a single field in a multipart form\nasync fn handle_multipart(\n    mut field: actix_multipart::Field,\n    path: PathBuf,\n    opts: HandleMultipartOpts<'_>,\n    #[cfg(unix)] chmod: u16,\n) -> Result<u64, RuntimeError> {\n    let HandleMultipartOpts {\n        on_duplicate_files,\n        allow_mkdir,\n        allow_hidden_paths,\n        allow_symlinks,\n        file_hash,\n        upload_directory,\n    } = opts;\n    let field_name = field.name().expect(\"No name field found\").to_string();\n\n    match tokio::fs::metadata(&path).await {\n        Err(_) => Err(RuntimeError::InsufficientPermissionsError(\n            path.display().to_string(),\n        )),\n        Ok(metadata) if !metadata.is_dir() => Err(RuntimeError::InvalidPathError(format!(\n            \"cannot upload file to {}, since it's not a directory\",\n            &path.display()\n        ))),\n        Ok(_) => Ok(()),\n    }?;\n\n    if field_name == \"mkdir\" {\n        if !allow_mkdir {\n            return Err(RuntimeError::InsufficientPermissionsError(\n                path.display().to_string(),\n            ));\n        }\n\n        let mut user_given_path = PathBuf::new();\n        let mut absolute_path = path.clone();\n\n        // Get the path the user gave\n        let mkdir_path_bytes = field.try_next().await;\n        match mkdir_path_bytes {\n            Ok(Some(mkdir_path_bytes)) => {\n                let mkdir_path = std::str::from_utf8(&mkdir_path_bytes).map_err(|e| {\n                    RuntimeError::ParseError(\n                        \"Failed to parse 'mkdir' path\".to_string(),\n                        e.to_string(),\n                    )\n                })?;\n                let mkdir_path = mkdir_path.replace('\\\\', \"/\");\n                absolute_path.push(&mkdir_path);\n                user_given_path.push(&mkdir_path);\n            }\n            _ => {\n                return Err(RuntimeError::ParseError(\n                    \"Failed to parse 'mkdir' path\".to_string(),\n                    \"\".to_string(),\n                ));\n            }\n        };\n\n        // Disallow using `..` (parent) in mkdir path\n        if user_given_path\n            .components()\n            .any(|c| c == Component::ParentDir)\n        {\n            return Err(RuntimeError::InvalidPathError(\n                \"Cannot use '..' in mkdir path\".to_string(),\n            ));\n        }\n        // Hidden paths check\n        sanitize_path(&user_given_path, allow_hidden_paths).ok_or_else(|| {\n            RuntimeError::InvalidPathError(\"Cannot use hidden paths in mkdir path\".to_string())\n        })?;\n\n        // Ensure there are no illegal symlinks\n        if !allow_symlinks {\n            match contains_symlink(&absolute_path) {\n                Err(err) => Err(RuntimeError::InsufficientPermissionsError(err.to_string()))?,\n                Ok(true) => Err(RuntimeError::InsufficientPermissionsError(format!(\n                    \"{user_given_path:?} traverses through a symlink\"\n                )))?,\n                Ok(false) => (),\n            }\n        }\n\n        return match tokio::fs::create_dir_all(&absolute_path).await {\n            Err(err) if err.kind() == ErrorKind::PermissionDenied => Err(\n                RuntimeError::InsufficientPermissionsError(path.display().to_string()),\n            ),\n            Err(err) => Err(RuntimeError::IoError(\n                format!(\"Failed to create {}\", user_given_path.display()),\n                err,\n            )),\n            Ok(_) => Ok(0),\n        };\n    }\n\n    let filename = field\n        .content_disposition()\n        .expect(\"No content-disposition field found\")\n        .get_filename()\n        .ok_or_else(|| {\n            RuntimeError::ParseError(\n                \"HTTP header\".to_string(),\n                \"Failed to retrieve the name of the file to upload\".to_string(),\n            )\n        })?;\n\n    let filename_path = sanitize_path(Path::new(&filename), allow_hidden_paths)\n        .ok_or_else(|| RuntimeError::InvalidPathError(\"Invalid file name to upload\".to_string()))?;\n\n    // Ensure there are no illegal symlinks in the file upload path\n    if !allow_symlinks {\n        match contains_symlink(&path) {\n            Err(err) => Err(RuntimeError::InsufficientPermissionsError(err.to_string()))?,\n            Ok(true) => Err(RuntimeError::InsufficientPermissionsError(format!(\n                \"{path:?} traverses through a symlink\"\n            )))?,\n            Ok(false) => (),\n        }\n    }\n\n    save_file(\n        &mut field,\n        path.join(filename_path),\n        on_duplicate_files,\n        file_hash,\n        upload_directory,\n        #[cfg(unix)]\n        chmod,\n    )\n    .await\n}\n\n/// Query parameters used by upload and rm APIs\n#[derive(Deserialize, Default)]\npub struct FileOpQueryParameters {\n    path: PathBuf,\n}\n\n/// Handle incoming request to upload a file or create a directory.\n/// Target file path is expected as path parameter in URI and is interpreted as relative from\n/// server root directory. Any path which will go outside of this directory is considered\n/// invalid.\n/// This method returns future.\npub async fn upload_file(\n    req: HttpRequest,\n    query: web::Query<FileOpQueryParameters>,\n    payload: web::Payload,\n) -> Result<HttpResponse, RuntimeError> {\n    let conf = req.app_data::<web::Data<MiniserveConfig>>().unwrap();\n    let upload_path = sanitize_path(&query.path, conf.show_hidden).ok_or_else(|| {\n        RuntimeError::InvalidPathError(\"Invalid value for 'path' parameter\".to_string())\n    })?;\n    let app_root_dir = conf.path.canonicalize().map_err(|e| {\n        RuntimeError::IoError(\"Failed to resolve path served by miniserve\".to_string(), e)\n    })?;\n\n    // Disallow paths outside of allowed directories\n    let upload_allowed = conf.allowed_upload_dir.is_empty()\n        || conf\n            .allowed_upload_dir\n            .iter()\n            .any(|s| upload_path.starts_with(s));\n\n    if !upload_allowed {\n        return Err(RuntimeError::UploadForbiddenError);\n    }\n\n    // Disallow the target path to go outside of the served directory\n    // The target directory shouldn't be canonicalized when it gets passed to\n    // handle_multipart so that it can check for symlinks if needed\n    let non_canonicalized_target_dir = app_root_dir.join(upload_path);\n    match non_canonicalized_target_dir.canonicalize() {\n        Ok(path) if !conf.no_symlinks => Ok(path),\n        Ok(path) if path.starts_with(&app_root_dir) => Ok(path),\n        _ => Err(RuntimeError::InvalidHttpRequestError(\n            \"Invalid value for 'path' parameter\".to_string(),\n        )),\n    }?;\n\n    let upload_directory = conf.temp_upload_directory.as_ref();\n\n    let file_hash = if let (Some(hash), Some(hash_function)) = (\n        req.headers()\n            .get(\"X-File-Hash\")\n            .and_then(|h| h.to_str().ok()),\n        req.headers()\n            .get(\"X-File-Hash-Function\")\n            .and_then(|h| h.to_str().ok()),\n    ) {\n        match hash_function.to_ascii_uppercase().as_str() {\n            \"SHA256\" => Some(FileHash::SHA256(hash.to_string())),\n            \"SHA512\" => Some(FileHash::SHA512(hash.to_string())),\n            sha => {\n                return Err(RuntimeError::InvalidHttpRequestError(format!(\n                    \"Invalid header value found for 'X-File-Hash-Function'. Supported values are SHA256 or SHA512. Found {sha}.\",\n                )));\n            }\n        }\n    } else {\n        None\n    };\n\n    let hash_ref = file_hash.as_ref();\n    actix_multipart::Multipart::new(req.headers(), payload)\n        .map_err(|x| RuntimeError::MultipartError(x.to_string()))\n        .and_then(|field| {\n            handle_multipart(\n                field,\n                non_canonicalized_target_dir.clone(),\n                HandleMultipartOpts {\n                    on_duplicate_files: conf.on_duplicate_files,\n                    allow_mkdir: conf.mkdir_enabled,\n                    allow_hidden_paths: conf.show_hidden,\n                    allow_symlinks: !conf.no_symlinks,\n                    file_hash: hash_ref,\n                    upload_directory,\n                },\n                #[cfg(unix)]\n                conf.upload_chmod,\n            )\n        })\n        .try_collect::<Vec<u64>>()\n        .await?;\n\n    let return_path = req\n        .headers()\n        .get(header::REFERER)\n        .and_then(|h| h.to_str().ok())\n        .unwrap_or(\"/\");\n\n    Ok(HttpResponse::SeeOther()\n        .append_header((header::LOCATION, return_path))\n        .finish())\n}\n\n/// Handle incoming request to remove a file or directory.\n///\n/// Target file path is expected as path parameter in URI and is interpreted as relative from\n/// server root directory. Any path which will go outside of this directory is considered\n/// invalid.\npub async fn rm_file(\n    req: HttpRequest,\n    query: web::Query<FileOpQueryParameters>,\n) -> Result<HttpResponse, RuntimeError> {\n    let conf = req.app_data::<web::Data<MiniserveConfig>>().unwrap();\n    let rm_path = sanitize_path(&query.path, conf.show_hidden).ok_or_else(|| {\n        RuntimeError::InvalidPathError(\"Invalid value for 'path' parameter\".to_string())\n    })?;\n\n    let app_root_dir = conf.path.canonicalize().map_err(|e| {\n        RuntimeError::IoError(\"Failed to resolve path served by miniserve\".to_string(), e)\n    })?;\n\n    // Disallow paths outside of allowed directories\n    let rm_allowed = conf.allowed_rm_dir.is_empty()\n        || conf.allowed_rm_dir.iter().any(|s| rm_path.starts_with(s));\n\n    if !rm_allowed {\n        return Err(RuntimeError::RmForbiddenError);\n    }\n\n    // Disallow the target path to go outside of the served directory\n    let canonicalized_rm_path = match app_root_dir.join(&rm_path).canonicalize() {\n        Ok(path) if !conf.no_symlinks => Ok(path),\n        Ok(path) if path.starts_with(&app_root_dir) => Ok(path),\n        _ => Err(RuntimeError::InvalidHttpRequestError(\n            \"Invalid value for 'path' parameter\".to_string(),\n        )),\n    }?;\n\n    // Handle non-existent path\n    if !canonicalized_rm_path.exists() {\n        return Err(RuntimeError::RouteNotFoundError(format!(\n            \"{rm_path:?} does not exist\"\n        )));\n    }\n\n    // Remove\n    let rm_res = if canonicalized_rm_path.is_dir() {\n        fs::remove_dir_all(&canonicalized_rm_path).await\n    } else {\n        fs::remove_file(&canonicalized_rm_path).await\n    };\n    if let Err(err) = rm_res {\n        Err(RuntimeError::IoError(\n            format!(\"Failed to remove {rm_path:?}\"),\n            err,\n        ))?;\n    }\n\n    let return_path = req\n        .headers()\n        .get(header::REFERER)\n        .and_then(|h| h.to_str().ok())\n        .unwrap_or(\"/\");\n\n    Ok(HttpResponse::SeeOther()\n        .append_header((header::LOCATION, return_path))\n        .finish())\n}\n"
  },
  {
    "path": "src/file_utils.rs",
    "content": "#[cfg(unix)]\nuse rustix::{fs::Mode, process::umask};\nuse std::{\n    io,\n    path::{Component, Path, PathBuf},\n};\n\n/// Guarantee that the path is relative and cannot traverse back to parent directories\n/// and optionally prevent traversing hidden directories.\n///\n/// See the unit tests tests::test_sanitize_path* for examples\npub fn sanitize_path(path: impl AsRef<Path>, traverse_hidden: bool) -> Option<PathBuf> {\n    let mut buf = PathBuf::new();\n\n    for comp in path.as_ref().components() {\n        match comp {\n            Component::Normal(name) => buf.push(name),\n            Component::ParentDir => {\n                buf.pop();\n            }\n            _ => (),\n        }\n    }\n\n    // Double-check that all components are Normal and check for hidden dirs\n    for comp in buf.components() {\n        match comp {\n            Component::Normal(_) if traverse_hidden => (),\n            Component::Normal(name) if !name.to_str()?.starts_with('.') => (),\n            _ => return None,\n        }\n    }\n\n    Some(buf)\n}\n\n/// Checks if any segment of the path is a symlink.\n///\n/// This function fails if [`std::fs::symlink_metadata`] fails, which usually\n/// means user has no permission to access the path.\npub fn contains_symlink(path: impl AsRef<Path>) -> io::Result<bool> {\n    let contains_symlink = path\n        .as_ref()\n        .ancestors()\n        // On Windows, `\\\\?\\` won't exist even though it's the root, but there's no need to check it\n        // So we filter it out\n        .filter(|p| p.exists())\n        .map(|p| p.symlink_metadata())\n        .collect::<Result<Vec<_>, _>>()?\n        .into_iter()\n        .any(|p| p.file_type().is_symlink());\n\n    Ok(contains_symlink)\n}\n\n/// Get default file creation permissions by umask\n#[cfg(unix)]\npub fn get_default_filemode() -> u16 {\n    let old = umask(Mode::all());\n    umask(old);\n    let mode = 0o666 & (!old).as_raw_mode();\n    mode as u16\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use pretty_assertions::assert_eq;\n    use rstest::rstest;\n\n    #[rstest]\n    #[case(\"/foo\", \"foo\")]\n    #[case(\"////foo\", \"foo\")]\n    #[case(\"C:/foo\", if cfg!(windows) { \"foo\" } else { \"C:/foo\" })]\n    #[case(\"../foo\", \"foo\")]\n    #[case(\"../foo/../bar/abc\", \"bar/abc\")]\n    fn test_sanitize_path(#[case] input: &str, #[case] output: &str) {\n        assert_eq!(\n            sanitize_path(Path::new(input), true).unwrap(),\n            Path::new(output)\n        );\n        assert_eq!(\n            sanitize_path(Path::new(input), false).unwrap(),\n            Path::new(output)\n        );\n    }\n\n    #[rstest]\n    #[case(\".foo\")]\n    #[case(\"/.foo\")]\n    #[case(\"foo/.bar/foo\")]\n    fn test_sanitize_path_no_hidden_files(#[case] input: &str) {\n        assert_eq!(sanitize_path(Path::new(input), false), None);\n    }\n}\n"
  },
  {
    "path": "src/listing.rs",
    "content": "#![allow(clippy::format_push_string)]\nuse std::io;\nuse std::path::{Component, Path};\nuse std::time::SystemTime;\n\nuse actix_web::{\n    HttpMessage, HttpRequest, HttpResponse, dev::ServiceResponse, http::Uri, web, web::Query,\n};\nuse bytesize::ByteSize;\nuse clap::ValueEnum;\nuse comrak::{Options as ComrakOptions, markdown_to_html};\nuse percent_encoding::{percent_decode_str, utf8_percent_encode};\nuse regex::Regex;\nuse serde::Deserialize;\nuse strum::{Display, EnumString};\n\nuse self::percent_encode_sets::COMPONENT;\nuse crate::archive::ArchiveMethod;\nuse crate::auth::CurrentUser;\nuse crate::errors::{self, RuntimeError};\nuse crate::renderer;\n\n/// \"percent-encode sets\" as defined by WHATWG specs:\n/// https://url.spec.whatwg.org/#percent-encoded-bytes\npub mod percent_encode_sets {\n    use percent_encoding::{AsciiSet, CONTROLS};\n    pub const QUERY: &AsciiSet = &CONTROLS.add(b' ').add(b'\"').add(b'#').add(b'<').add(b'>');\n    pub const PATH: &AsciiSet = &QUERY.add(b'?').add(b'`').add(b'{').add(b'}');\n    pub const USERINFO: &AsciiSet = &PATH\n        .add(b'/')\n        .add(b':')\n        .add(b';')\n        .add(b'=')\n        .add(b'@')\n        .add(b'[')\n        .add(b'\\\\')\n        .add(b']')\n        .add(b'^')\n        .add(b'|');\n    pub const COMPONENT: &AsciiSet = &USERINFO.add(b'$').add(b'%').add(b'&').add(b'+').add(b',');\n}\n\n/// Query parameters used by listing APIs\n#[derive(Deserialize, Default)]\npub struct ListingQueryParameters {\n    pub sort: Option<SortingMethod>,\n    pub order: Option<SortingOrder>,\n    pub raw: Option<bool>,\n    download: Option<ArchiveMethod>,\n}\n\n/// Available sorting methods\n#[derive(Debug, Deserialize, Default, Clone, EnumString, Display, Copy, ValueEnum)]\n#[serde(rename_all = \"snake_case\")]\n#[strum(serialize_all = \"snake_case\")]\npub enum SortingMethod {\n    #[default]\n    /// Sort by name\n    Name,\n\n    /// Sort by size\n    Size,\n\n    /// Sort by last modification date (natural sort: follows alphanumerical order)\n    Date,\n}\n\n/// Available sorting orders\n#[derive(Debug, Deserialize, Default, Clone, EnumString, Display, Copy, ValueEnum)]\npub enum SortingOrder {\n    /// Ascending order\n    #[serde(alias = \"asc\")]\n    #[strum(serialize = \"asc\")]\n    Asc,\n\n    /// Descending order\n    #[default]\n    #[serde(alias = \"desc\")]\n    #[strum(serialize = \"desc\")]\n    Desc,\n}\n\n/// Possible entry types\n#[derive(PartialEq, Clone, Display, Eq)]\n#[strum(serialize_all = \"snake_case\")]\npub enum EntryType {\n    /// Entry is a directory\n    Directory,\n\n    /// Entry is a file\n    File,\n}\n\n/// Entry\npub struct Entry {\n    /// Name of the entry\n    pub name: String,\n\n    /// Type of the entry\n    pub entry_type: EntryType,\n\n    /// URL of the entry\n    pub link: String,\n\n    /// Size in byte of the entry. Only available for EntryType::File\n    pub size: Option<bytesize::ByteSize>,\n\n    /// Last modification date\n    pub last_modification_date: Option<SystemTime>,\n\n    /// Path of symlink pointed to\n    pub symlink_info: Option<String>,\n}\n\nimpl Entry {\n    fn new(\n        name: String,\n        entry_type: EntryType,\n        link: String,\n        size: Option<bytesize::ByteSize>,\n        last_modification_date: Option<SystemTime>,\n        symlink_info: Option<String>,\n    ) -> Self {\n        Self {\n            name,\n            entry_type,\n            link,\n            size,\n            last_modification_date,\n            symlink_info,\n        }\n    }\n\n    /// Returns whether the entry is a directory\n    pub fn is_dir(&self) -> bool {\n        self.entry_type == EntryType::Directory\n    }\n\n    /// Returns whether the entry is a file\n    pub fn is_file(&self) -> bool {\n        self.entry_type == EntryType::File\n    }\n}\n\n/// One entry in the path to the listed directory\npub struct Breadcrumb {\n    /// Name of directory\n    pub name: String,\n\n    /// Link to get to directory, relative to listed directory\n    pub link: String,\n}\n\nimpl Breadcrumb {\n    fn new(name: String, link: String) -> Self {\n        Self { name, link }\n    }\n}\n\npub async fn file_handler(req: HttpRequest) -> actix_web::Result<actix_files::NamedFile> {\n    let path = &req\n        .app_data::<web::Data<crate::MiniserveConfig>>()\n        .unwrap()\n        .path;\n    actix_files::NamedFile::open(path).map_err(Into::into)\n}\n\n/// List a directory and renders a HTML file accordingly\n/// Adapted from https://docs.rs/actix-web/0.7.13/src/actix_web/fs.rs.html#564\npub fn directory_listing(\n    dir: &actix_files::Directory,\n    req: &HttpRequest,\n) -> io::Result<ServiceResponse> {\n    let extensions = req.extensions();\n    let current_user: Option<&CurrentUser> = extensions.get::<CurrentUser>();\n\n    let conf = req.app_data::<web::Data<crate::MiniserveConfig>>().unwrap();\n    if conf.disable_indexing {\n        return Ok(ServiceResponse::new(\n            req.clone(),\n            HttpResponse::NotFound()\n                .content_type(mime::TEXT_PLAIN_UTF_8)\n                .body(\"File not found.\"),\n        ));\n    }\n    let serve_path = req.path();\n\n    let base = Path::new(serve_path);\n    let random_route_abs = format!(\"/{}\", conf.route_prefix);\n    let abs_uri = {\n        let res = Uri::builder()\n            .scheme(req.connection_info().scheme())\n            .authority(req.connection_info().host())\n            .path_and_query(req.path())\n            .build();\n        match res {\n            Ok(uri) => uri,\n            Err(err) => return Ok(ServiceResponse::from_err(err, req.clone())),\n        }\n    };\n    let is_root = base.parent().is_none() || Path::new(&req.path()) == Path::new(&random_route_abs);\n\n    let encoded_dir = match base.strip_prefix(random_route_abs) {\n        Ok(c_d) => Path::new(\"/\").join(c_d),\n        Err(_) => base.to_path_buf(),\n    }\n    .display()\n    .to_string();\n\n    let breadcrumbs = {\n        let title = conf\n            .title\n            .clone()\n            .unwrap_or_else(|| req.connection_info().host().into());\n\n        let decoded = percent_decode_str(&encoded_dir).decode_utf8_lossy();\n\n        let mut res: Vec<Breadcrumb> = Vec::new();\n        let mut link_accumulator = format!(\"{}/\", &conf.route_prefix);\n        let mut components = Path::new(&*decoded).components().peekable();\n\n        while let Some(c) = components.next() {\n            let name;\n\n            match c {\n                Component::RootDir => {\n                    name = title.clone();\n                }\n                Component::Normal(s) => {\n                    name = s.to_string_lossy().to_string();\n                    link_accumulator\n                        .push_str(&(utf8_percent_encode(&name, COMPONENT).to_string() + \"/\"));\n                }\n                _ => name = \"\".to_string(),\n            };\n\n            res.push(Breadcrumb::new(\n                name,\n                if components.peek().is_some() {\n                    link_accumulator.clone()\n                } else {\n                    \".\".to_string()\n                },\n            ));\n        }\n        res\n    };\n\n    let query_params = extract_query_parameters(req);\n    let mut entries: Vec<Entry> = Vec::new();\n    let mut readme: Option<(String, String)> = None;\n    let readme_rx: Regex = Regex::new(\"^readme([.](md|txt))?$\").unwrap();\n\n    for entry in dir.path.read_dir()? {\n        if dir.is_visible(&entry) || conf.show_hidden {\n            let entry = entry?;\n            // show file url as relative to static path\n            let file_name = entry.file_name().to_string_lossy().to_string();\n            let (is_symlink, metadata) = match entry.metadata() {\n                Ok(metadata) if metadata.file_type().is_symlink() => {\n                    // for symlinks, get the metadata of the original file\n                    (true, std::fs::metadata(entry.path()))\n                }\n                res => (false, res),\n            };\n            let symlink_dest = (is_symlink && conf.show_symlink_info)\n                .then(|| entry.path())\n                .and_then(|path| std::fs::read_link(path).ok())\n                .map(|path| path.to_string_lossy().into_owned());\n            let file_url = base\n                .join(utf8_percent_encode(&file_name, COMPONENT).to_string())\n                .to_string_lossy()\n                .to_string();\n\n            // if file is a directory, add '/' to the end of the name\n            if let Ok(metadata) = metadata {\n                if conf.no_symlinks && is_symlink {\n                    continue;\n                }\n                let last_modification_date = metadata.modified().ok();\n\n                if metadata.is_dir() {\n                    entries.push(Entry::new(\n                        file_name,\n                        EntryType::Directory,\n                        file_url,\n                        None,\n                        last_modification_date,\n                        symlink_dest,\n                    ));\n                } else if metadata.is_file() {\n                    let file_link = match &conf.file_external_url {\n                        Some(external_url) => {\n                            // Construct the full relative path including subdirectories\n                            // encoded_dir holds the current directory path relative to the prefix (e.g., /subdir1/subdir2)\n                            let current_relative_dir = encoded_dir.trim_matches('/'); // Remove leading/trailing slashes if any\n\n                            // Combine the relative directory path and the filename\n                            let full_relative_path = if current_relative_dir.is_empty() {\n                                // If in the root directory, just use the filename\n                                utf8_percent_encode(&file_name, COMPONENT).to_string()\n                            } else {\n                                // Otherwise, join directory and filename\n                                format!(\n                                    \"{}/{}\",\n                                    current_relative_dir,\n                                    utf8_percent_encode(&file_name, COMPONENT)\n                                )\n                            };\n\n                            // Join the external external URL with the full relative path\n                            format!(\n                                \"{}/{}\",\n                                external_url.trim_end_matches('/'), // Base URL without trailing slash\n                                full_relative_path // Relative path (dir + file) - should not have leading slash here\n                            )\n                        }\n                        None => file_url,\n                    };\n                    entries.push(Entry::new(\n                        file_name.clone(),\n                        EntryType::File,\n                        file_link,\n                        Some(ByteSize::b(metadata.len())),\n                        last_modification_date,\n                        symlink_dest,\n                    ));\n                    if conf.readme && readme_rx.is_match(&file_name.to_lowercase()) {\n                        let ext = file_name.split('.').next_back().unwrap().to_lowercase();\n                        readme = Some((\n                            file_name.to_string(),\n                            if ext == \"md\" {\n                                let mut options = ComrakOptions::default();\n\n                                // Enable some GFM extensions\n                                options.extension.strikethrough = true;\n                                options.extension.table = true;\n                                options.extension.autolink = true;\n                                options.extension.tasklist = true;\n\n                                markdown_to_html(&std::fs::read_to_string(entry.path())?, &options)\n                            } else {\n                                format!(\"<pre>{}</pre>\", &std::fs::read_to_string(entry.path())?)\n                            },\n                        ));\n                    }\n                }\n            } else {\n                continue;\n            }\n        }\n    }\n\n    match query_params.sort.unwrap_or(conf.default_sorting_method) {\n        SortingMethod::Name => entries.sort_by(|e1, e2| {\n            alphanumeric_sort::compare_str(e1.name.to_lowercase(), e2.name.to_lowercase())\n        }),\n        SortingMethod::Size => entries.sort_by(|e1, e2| {\n            // If we can't get the size of the entry (directory for instance)\n            // let's consider it's 0b\n            e2.size\n                .unwrap_or_else(|| ByteSize::b(0))\n                .cmp(&e1.size.unwrap_or_else(|| ByteSize::b(0)))\n        }),\n        SortingMethod::Date => entries.sort_by(|e1, e2| {\n            // If, for some reason, we can't get the last modification date of an entry\n            // let's consider it was modified on UNIX_EPOCH (01/01/19270 00:00:00)\n            e2.last_modification_date\n                .unwrap_or(SystemTime::UNIX_EPOCH)\n                .cmp(&e1.last_modification_date.unwrap_or(SystemTime::UNIX_EPOCH))\n        }),\n    };\n\n    if let SortingOrder::Asc = query_params.order.unwrap_or(conf.default_sorting_order) {\n        entries.reverse()\n    }\n\n    // List directories first\n    if conf.dirs_first {\n        entries.sort_by_key(|e| !e.is_dir());\n    }\n\n    if let Some(archive_method) = query_params.download {\n        if !archive_method.is_enabled(conf.tar_enabled, conf.tar_gz_enabled, conf.zip_enabled) {\n            return Ok(ServiceResponse::new(\n                req.clone(),\n                HttpResponse::Forbidden()\n                    .content_type(mime::TEXT_PLAIN_UTF_8)\n                    .body(\"Archive creation is disabled.\"),\n            ));\n        }\n        log::info!(\n            \"Creating an archive ({extension}) of {path}...\",\n            extension = archive_method.extension(),\n            path = &dir.path.display().to_string()\n        );\n\n        let file_name = format!(\n            \"{}.{}\",\n            dir.path.file_name().unwrap().to_str().unwrap(),\n            archive_method.extension()\n        );\n\n        // We will create the archive in a separate thread, and stream the content using a pipe.\n        // The pipe is made of a futures channel, and an adapter to implement the `Write` trait.\n        // Include 10 messages of buffer for erratic connection speeds.\n        let (tx, rx) = futures::channel::mpsc::channel::<io::Result<actix_web::web::Bytes>>(10);\n        let pipe = crate::pipe::Pipe::new(tx);\n\n        // Start the actual archive creation in a separate thread.\n        let dir = dir.path.to_path_buf();\n        let skip_symlinks = conf.no_symlinks;\n        std::thread::spawn(move || {\n            if let Err(err) = archive_method.create_archive(dir, skip_symlinks, pipe) {\n                log::error!(\"Error during archive creation: {err:?}\");\n            }\n        });\n\n        Ok(ServiceResponse::new(\n            req.clone(),\n            HttpResponse::Ok()\n                .content_type(archive_method.content_type())\n                .append_header((\"Content-Transfer-Encoding\", \"binary\"))\n                .append_header((\n                    \"Content-Disposition\",\n                    format!(\"attachment; filename={file_name:?}\"),\n                ))\n                .body(actix_web::body::BodyStream::new(rx)),\n        ))\n    } else {\n        Ok(ServiceResponse::new(\n            req.clone(),\n            HttpResponse::Ok().content_type(mime::TEXT_HTML_UTF_8).body(\n                renderer::page(\n                    entries,\n                    readme,\n                    &abs_uri,\n                    is_root,\n                    query_params,\n                    &breadcrumbs,\n                    &encoded_dir,\n                    conf,\n                    current_user,\n                )\n                .into_string(),\n            ),\n        ))\n    }\n}\n\npub fn extract_query_parameters(req: &HttpRequest) -> ListingQueryParameters {\n    match Query::<ListingQueryParameters>::from_query(req.query_string()) {\n        Ok(Query(query_params)) => query_params,\n        Err(e) => {\n            let err = RuntimeError::ParseError(\"query parameters\".to_string(), e.to_string());\n            errors::log_error_chain(err.to_string());\n            ListingQueryParameters::default()\n        }\n    }\n}\n"
  },
  {
    "path": "src/main.rs",
    "content": "use std::io::{self, IsTerminal, Write};\nuse std::net::{IpAddr, SocketAddr, TcpListener};\nuse std::thread;\nuse std::time::Duration;\n\nuse actix_files::NamedFile;\nuse actix_web::middleware::from_fn;\nuse actix_web::{\n    App, HttpRequest, HttpResponse, Responder,\n    dev::{ServiceRequest, ServiceResponse, fn_service},\n    guard,\n    http::{Method, header::ContentType},\n    middleware, web,\n};\nuse actix_web_httpauth::middleware::HttpAuthentication;\nuse anyhow::Result;\nuse bytesize::ByteSize;\nuse clap::{CommandFactory, Parser, crate_version};\nuse colored::*;\nuse dav_server::{\n    DavHandler, DavMethodSet,\n    actix::{DavRequest, DavResponse},\n};\nuse fast_qr::QRBuilder;\nuse log::{error, info, trace, warn};\nuse percent_encoding::percent_decode_str;\nuse serde::Deserialize;\n\nmod archive;\nmod args;\nmod auth;\nmod config;\nmod consts;\nmod errors;\nmod file_op;\nmod file_utils;\nmod listing;\nmod pipe;\nmod renderer;\nmod webdav_fs;\n\nuse crate::args::LogColor;\nuse crate::config::MiniserveConfig;\nuse crate::errors::{RuntimeError, StartupError};\nuse crate::file_op::recursive_dir_size;\nuse crate::webdav_fs::RestrictedFs;\n\nstatic STYLESHEET: &str = grass::include!(\"data/style.scss\");\n\nfn main() -> Result<()> {\n    let args = args::CliArgs::parse();\n\n    if let Some(shell) = args.print_completions {\n        let mut clap_app = args::CliArgs::command();\n        let app_name = clap_app.get_name().to_string();\n        clap_complete::generate(shell, &mut clap_app, app_name, &mut io::stdout());\n        return Ok(());\n    }\n\n    if args.print_manpage {\n        let clap_app = args::CliArgs::command();\n        let man = clap_mangen::Man::new(clap_app);\n        man.render(&mut io::stdout())?;\n        return Ok(());\n    }\n\n    let miniserve_config = MiniserveConfig::try_from_args(args)?;\n\n    run(miniserve_config).inspect_err(|e| {\n        errors::log_error_chain(e.to_string());\n    })?;\n\n    Ok(())\n}\n\n#[actix_web::main(miniserve)]\nasync fn run(miniserve_config: MiniserveConfig) -> Result<(), StartupError> {\n    let log_level = if miniserve_config.verbose {\n        simplelog::LevelFilter::Info\n    } else {\n        simplelog::LevelFilter::Warn\n    };\n\n    let color_choice = match miniserve_config.log_color {\n        LogColor::Auto => {\n            if io::stdout().is_terminal() {\n                simplelog::ColorChoice::Auto\n            } else {\n                simplelog::ColorChoice::Never\n            }\n        }\n        LogColor::Always => {\n            colored::control::SHOULD_COLORIZE.set_override(true);\n            simplelog::ColorChoice::Always\n        }\n        LogColor::Never => {\n            colored::control::SHOULD_COLORIZE.set_override(false);\n            simplelog::ColorChoice::Never\n        }\n    };\n\n    trace!(\n        \"Set log color, simplelog = {:?}, colored = {:?}\",\n        color_choice,\n        colored::control::SHOULD_COLORIZE.should_colorize(),\n    );\n\n    simplelog::TermLogger::init(\n        log_level,\n        simplelog::ConfigBuilder::new()\n            .set_time_format_rfc2822()\n            .build(),\n        simplelog::TerminalMode::Mixed,\n        color_choice,\n    )\n    .or_else(|_| simplelog::SimpleLogger::init(log_level, simplelog::Config::default()))\n    .expect(\"Couldn't initialize logger\");\n\n    if miniserve_config.no_symlinks && miniserve_config.path.is_symlink() {\n        return Err(StartupError::NoSymlinksOptionWithSymlinkServePath(\n            miniserve_config.path.to_string_lossy().to_string(),\n        ));\n    }\n\n    if miniserve_config.webdav_enabled && miniserve_config.path.is_file() {\n        return Err(StartupError::WebdavWithFileServePath(\n            miniserve_config.path.to_string_lossy().to_string(),\n        ));\n    }\n\n    let inside_config = miniserve_config.clone();\n\n    let canon_path = miniserve_config\n        .path\n        .canonicalize()\n        .map_err(|e| StartupError::IoError(\"Failed to resolve path to be served\".to_string(), e))?;\n\n    // warn if --index is specified but not found\n    if let Some(ref index) = miniserve_config.index\n        && !canon_path.join(index).exists()\n        && !miniserve_config.quiet\n    {\n        warn!(\n            \"The file '{}' provided for option --index could not be found.\",\n            index.to_string_lossy(),\n        );\n    }\n\n    let path_string = canon_path.to_string_lossy();\n\n    if !miniserve_config.quiet {\n        println!(\n            \"{name} v{version}\",\n            name = \"miniserve\".bold(),\n            version = crate_version!()\n        );\n    }\n    if !miniserve_config.path_explicitly_chosen {\n        // If the path to serve has NOT been explicitly chosen and if this is NOT an interactive\n        // terminal, we should refuse to start for security reasons. This would be the case when\n        // running miniserve as a service but forgetting to set the path. This could be pretty\n        // dangerous if given with an undesired context path (for instance /root or /).\n        if !io::stdout().is_terminal() {\n            return Err(StartupError::NoExplicitPathAndNoTerminal);\n        }\n\n        if !miniserve_config.quiet {\n            warn!(\n                \"miniserve has been invoked without an explicit path so it will serve the current directory after a short delay.\"\n            );\n            warn!(\n                \"Invoke with -h|--help to see options or invoke as `miniserve .` to hide this advice.\"\n            );\n            print!(\"Starting server in \");\n            io::stdout()\n                .flush()\n                .map_err(|e| StartupError::IoError(\"Failed to write data\".to_string(), e))?;\n            for c in \"3… 2… 1… \\n\".chars() {\n                print!(\"{c}\");\n                io::stdout()\n                    .flush()\n                    .map_err(|e| StartupError::IoError(\"Failed to write data\".to_string(), e))?;\n                thread::sleep(Duration::from_millis(500));\n            }\n        }\n    }\n\n    let display_urls = {\n        let (mut ifaces, wildcard): (Vec<_>, Vec<_>) = miniserve_config\n            .interfaces\n            .clone()\n            .into_iter()\n            .partition(|addr| !addr.is_unspecified());\n\n        // Replace wildcard addresses with local interface addresses\n        if !wildcard.is_empty() {\n            let all_ipv4 = wildcard.iter().any(|addr| addr.is_ipv4());\n            let all_ipv6 = wildcard.iter().any(|addr| addr.is_ipv6());\n            ifaces = if_addrs::get_if_addrs()\n                .unwrap_or_else(|e| {\n                    error!(\"Failed to get local interface addresses: {e}\");\n                    Default::default()\n                })\n                .into_iter()\n                .map(|iface| iface.ip())\n                .filter(|ip| (all_ipv4 && ip.is_ipv4()) || (all_ipv6 && ip.is_ipv6()))\n                .collect();\n            ifaces.sort();\n        }\n\n        ifaces\n            .into_iter()\n            .map(|addr| match addr {\n                IpAddr::V4(_) => format!(\"{}:{}\", addr, miniserve_config.port),\n                IpAddr::V6(_) => format!(\"[{}]:{}\", addr, miniserve_config.port),\n            })\n            .map(|addr| match miniserve_config.tls_rustls_config {\n                Some(_) => format!(\"https://{addr}\"),\n                None => format!(\"http://{addr}\"),\n            })\n            .map(|url| format!(\"{}{}\", url, miniserve_config.route_prefix))\n            .collect::<Vec<_>>()\n    };\n\n    let socket_addresses = miniserve_config\n        .interfaces\n        .iter()\n        .map(|&interface| SocketAddr::new(interface, miniserve_config.port))\n        .collect::<Vec<_>>();\n\n    let display_sockets = socket_addresses\n        .iter()\n        .map(|sock| sock.to_string().green().bold().to_string())\n        .collect::<Vec<_>>();\n\n    let stylesheet = web::Data::new(\n        [\n            STYLESHEET,\n            inside_config.default_color_scheme.css(),\n            inside_config.default_color_scheme_dark.css_dark().as_str(),\n        ]\n        .join(\"\\n\"),\n    );\n\n    let srv = actix_web::HttpServer::new(move || {\n        App::new()\n            .wrap(configure_header(&inside_config.clone()))\n            .app_data(web::Data::new(inside_config.clone()))\n            .app_data(stylesheet.clone())\n            .wrap(from_fn(errors::error_page_middleware))\n            .wrap(middleware::Logger::default())\n            .wrap(middleware::Condition::new(\n                miniserve_config.compress_response,\n                middleware::Compress::default(),\n            ))\n            .route(&inside_config.healthcheck_route, web::get().to(healthcheck))\n            .route(&inside_config.api_route, web::post().to(api))\n            .route(&inside_config.favicon_route, web::get().to(favicon))\n            .route(&inside_config.css_route, web::get().to(css))\n            .service(\n                web::scope(&inside_config.route_prefix)\n                    .wrap(middleware::Condition::new(\n                        !inside_config.auth.is_empty(),\n                        actix_web::middleware::Compat::new(HttpAuthentication::basic(\n                            auth::handle_auth,\n                        )),\n                    ))\n                    .configure(|c| configure_app(c, &inside_config)),\n            )\n            .default_service(web::get().to(error_404))\n    });\n\n    let srv = socket_addresses.iter().try_fold(srv, |srv, addr| {\n        let listener = create_tcp_listener(*addr)\n            .map_err(|e| StartupError::IoError(format!(\"Failed to bind server to {addr}\"), e))?;\n\n        #[cfg(feature = \"tls\")]\n        let srv = match &miniserve_config.tls_rustls_config {\n            Some(tls_config) => srv.listen_rustls_0_23(listener, tls_config.clone()),\n            None => srv.listen(listener),\n        };\n\n        #[cfg(not(feature = \"tls\"))]\n        let srv = srv.listen(listener);\n\n        srv.map_err(|e| StartupError::IoError(format!(\"Failed to bind server to {addr}\"), e))\n    })?;\n\n    let srv = srv.shutdown_timeout(0).run();\n\n    if !miniserve_config.quiet {\n        println!(\"Bound to {}\", display_sockets.join(\", \"));\n        println!(\"Serving path {}\", path_string.yellow().bold());\n        println!(\n            \"Available at (non-exhaustive list):\\n    {}\\n\",\n            display_urls\n                .iter()\n                .map(|url| url.green().bold().to_string())\n                .collect::<Vec<_>>()\n                .join(\"\\n    \"),\n        );\n    }\n\n    // print QR code to terminal\n    if miniserve_config.show_qrcode && io::stdout().is_terminal() {\n        for url in display_urls\n            .iter()\n            .filter(|url| !url.contains(\"//127.0.0.1:\") && !url.contains(\"//[::1]:\"))\n        {\n            match QRBuilder::new(url.clone()).ecl(consts::QR_EC_LEVEL).build() {\n                Ok(qr) => {\n                    println!(\"QR code for {}:\", url.green().bold());\n                    qr.print();\n                }\n                Err(e) => {\n                    error!(\"Failed to render QR to terminal: {e:?}\");\n                }\n            };\n        }\n    }\n\n    if !miniserve_config.quiet && io::stdout().is_terminal() {\n        println!(\"Quit by pressing CTRL-C\");\n    }\n\n    srv.await\n        .map_err(|e| StartupError::IoError(\"\".to_owned(), e))\n}\n\n/// Allows us to set low-level socket options\n///\n/// This mainly used to set `set_only_v6` socket option\n/// to get a consistent behavior across platforms.\n/// see: https://github.com/svenstaro/miniserve/pull/500\nfn create_tcp_listener(addr: SocketAddr) -> io::Result<TcpListener> {\n    use socket2::{Domain, Protocol, Socket, Type};\n    let socket = Socket::new(Domain::for_address(addr), Type::STREAM, Some(Protocol::TCP))?;\n    if addr.is_ipv6() {\n        socket.set_only_v6(true)?;\n    }\n    socket.set_reuse_address(true)?;\n    socket.bind(&addr.into())?;\n    socket.listen(1024 /* Default backlog */)?;\n    Ok(TcpListener::from(socket))\n}\n\nfn configure_header(conf: &MiniserveConfig) -> middleware::DefaultHeaders {\n    conf.header.iter().flatten().fold(\n        middleware::DefaultHeaders::new(),\n        |headers, (header_name, header_value)| headers.add((header_name, header_value)),\n    )\n}\n\n/// Configures the Actix application\n///\n/// This is where we configure the app to serve an index file, the file listing, or a single file.\nfn configure_app(app: &mut web::ServiceConfig, conf: &MiniserveConfig) {\n    let dir_service = || {\n        // use routing guard so propfind and options requests fall through to the webdav handler\n        let mut files = actix_files::Files::new(\"\", &conf.path)\n            .guard(guard::Any(guard::Get()).or(guard::Head()));\n\n        // Use specific index file if one was provided.\n        if let Some(ref index_file) = conf.index {\n            files = files.index_file(index_file.to_string_lossy());\n            // Handle SPA option.\n            //\n            // Note: --spa requires --index in clap.\n            if conf.spa {\n                files = files.default_handler(\n                    NamedFile::open(conf.path.join(index_file))\n                        .expect(\"Can't open SPA index file.\"),\n                );\n            }\n        }\n\n        // Handle --pretty-urls options.\n        //\n        // We rewrite the request to append \".html\" to the path and serve the file. If the\n        // path ends with a `/`, we remove it before appending \".html\".\n        //\n        // This is done to allow for pretty URLs, e.g. \"/about\" instead of \"/about.html\".\n        if conf.pretty_urls {\n            files = files.default_handler(fn_service(|req: ServiceRequest| async {\n                let (req, _) = req.into_parts();\n                let conf = req\n                    .app_data::<web::Data<MiniserveConfig>>()\n                    .expect(\"Could not get miniserve config\");\n                let mut path_base = req.path()[1..].to_string();\n                if path_base.ends_with('/') {\n                    path_base.pop();\n                }\n                if !path_base.ends_with(\"html\") {\n                    path_base = format!(\"{path_base}.html\");\n                }\n                let file = NamedFile::open_async(conf.path.join(path_base)).await?;\n                let res = file.into_response(&req);\n                Ok(ServiceResponse::new(req, res))\n            }));\n        }\n\n        if conf.show_hidden {\n            files = files.use_hidden_files();\n        }\n\n        let base_path = conf.path.clone();\n        let no_symlinks = conf.no_symlinks;\n        files\n            .show_files_listing()\n            .files_listing_renderer(listing::directory_listing)\n            .prefer_utf8(true)\n            .redirect_to_slash_directory()\n            .path_filter(move |path, _| {\n                if !no_symlinks {\n                    // no_symlinks not enabled => nothing to filter\n                    return true;\n                }\n\n                // append path to base_path component by component and check for symlink at each step\n                let mut full_path = base_path.clone();\n                for component in path.components() {\n                    full_path.push(component);\n                    if full_path.is_symlink() {\n                        // path contains symlink component while no_symlink is active => filter\n                        return false;\n                    }\n                }\n\n                // path didn't include a symlink component => don't filter\n                true\n            })\n    };\n\n    if conf.path.is_file() {\n        // Handle single files\n        app.service(web::resource([\"\", \"/\"]).route(web::to(listing::file_handler)));\n    } else {\n        if conf.file_upload {\n            // Allow file upload\n            app.service(web::resource(\"/upload\").route(web::post().to(file_op::upload_file)));\n        }\n        if conf.rm_enabled {\n            // Allow file and directory deletion\n            app.service(web::resource(\"/rm\").route(web::post().to(file_op::rm_file)));\n        }\n        // Handle directories\n        app.service(dir_service());\n    }\n\n    if conf.webdav_enabled {\n        let fs = RestrictedFs::new(&conf.path, conf.show_hidden, conf.no_symlinks);\n\n        let dav_server = DavHandler::builder()\n            .filesystem(fs)\n            .methods(DavMethodSet::WEBDAV_RO)\n            .hide_symlinks(false) // we handle filtering symlinks ourselves in RestrictedFs\n            .strip_prefix(conf.route_prefix.to_owned())\n            .build_handler();\n\n        app.app_data(web::Data::new(dav_server.clone()));\n\n        app.service(\n            // actix requires tail segment to be named, even if unused\n            web::resource(\"/{tail}*\")\n                .guard(\n                    guard::Any(guard::Options())\n                        .or(guard::Method(Method::from_bytes(b\"PROPFIND\").unwrap())),\n                )\n                .to(dav_handler),\n        );\n    }\n}\n\nasync fn dav_handler(req: DavRequest, davhandler: web::Data<DavHandler>) -> DavResponse {\n    davhandler.handle(req.request).await.into()\n}\n\nasync fn error_404(req: HttpRequest) -> Result<HttpResponse, RuntimeError> {\n    Err(RuntimeError::RouteNotFoundError(req.path().to_string()))\n}\n\nasync fn healthcheck() -> impl Responder {\n    HttpResponse::Ok().body(\"OK\")\n}\n\n#[derive(Deserialize, Debug)]\nenum ApiCommand {\n    /// Request the size of a particular directory\n    DirSize(String),\n}\n\n/// This \"API\" is pretty shitty but frankly miniserve doesn't really need a very fancy API. Or at\n/// least I hope so.\nasync fn api(\n    command: web::Json<ApiCommand>,\n    config: web::Data<MiniserveConfig>,\n) -> Result<impl Responder, RuntimeError> {\n    match command.into_inner() {\n        ApiCommand::DirSize(path) => {\n            if config.directory_size {\n                // The dir argument might be percent-encoded so let's decode it just in case.\n                let decoded_path = percent_decode_str(&path)\n                    .decode_utf8()\n                    .map_err(|e| RuntimeError::ParseError(path.clone(), e.to_string()))?;\n\n                // Convert the relative dir to an absolute path on the system.\n                let sanitized_path = file_utils::sanitize_path(&*decoded_path, true)\n                    .expect(\"Expected a path to directory\");\n\n                let full_path = config\n                    .path\n                    .canonicalize()\n                    .expect(\"Couldn't canonicalize path\")\n                    .join(sanitized_path);\n                info!(\"Requested directory listing for {full_path:?}\");\n\n                let dir_size = recursive_dir_size(&full_path).await?;\n                if config.show_exact_bytes {\n                    Ok(format!(\"{dir_size} B\"))\n                } else {\n                    let dir_size = ByteSize::b(dir_size);\n                    Ok(dir_size.to_string())\n                }\n            } else {\n                Ok(\"-\".to_string())\n            }\n        }\n    }\n}\n\nasync fn favicon() -> impl Responder {\n    let logo = include_str!(\"../data/logo.svg\");\n    HttpResponse::Ok()\n        .insert_header(ContentType(mime::IMAGE_SVG))\n        .body(logo)\n}\n\nasync fn css(stylesheet: web::Data<String>) -> impl Responder {\n    HttpResponse::Ok()\n        .insert_header(ContentType(mime::TEXT_CSS))\n        .body(stylesheet.to_string())\n}\n"
  },
  {
    "path": "src/pipe.rs",
    "content": "//! Define an adapter to implement `std::io::Write` on `Sender<Bytes>`.\nuse std::io::{self, Error, ErrorKind, Write};\n\nuse actix_web::web::{Bytes, BytesMut};\nuse futures::channel::mpsc::Sender;\nuse futures::executor::block_on;\nuse futures::sink::SinkExt;\n\n/// Adapter to implement the `std::io::Write` trait on a `Sender<Bytes>` from a futures channel.\n///\n/// It uses an intermediate buffer to transfer packets.\npub struct Pipe {\n    dest: Sender<io::Result<Bytes>>,\n    bytes: BytesMut,\n}\n\nimpl Pipe {\n    /// Wrap the given sender in a `Pipe`.\n    pub fn new(destination: Sender<io::Result<Bytes>>) -> Self {\n        Self {\n            dest: destination,\n            bytes: BytesMut::new(),\n        }\n    }\n}\n\nimpl Drop for Pipe {\n    fn drop(&mut self) {\n        let _ = block_on(self.dest.close());\n    }\n}\n\nimpl Write for Pipe {\n    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {\n        // We are given a slice of bytes we do not own, so we must start by copying it.\n        self.bytes.extend_from_slice(buf);\n\n        // Then, take the buffer and send it in the channel.\n        block_on(self.dest.send(Ok(self.bytes.split().into())))\n            .map_err(|e| Error::new(ErrorKind::UnexpectedEof, e))?;\n\n        // Return how much we sent - all of it.\n        Ok(buf.len())\n    }\n\n    fn flush(&mut self) -> io::Result<()> {\n        block_on(self.dest.flush()).map_err(|e| Error::new(ErrorKind::UnexpectedEof, e))\n    }\n}\n"
  },
  {
    "path": "src/renderer.rs",
    "content": "use std::time::SystemTime;\n\nuse actix_web::http::{StatusCode, Uri};\nuse chrono::{DateTime, Local};\nuse chrono_humanize::Humanize;\nuse clap::{ValueEnum, crate_name, crate_version};\nuse fast_qr::{\n    QRBuilder,\n    convert::{Builder, svg::SvgBuilder},\n    qr::QRCodeError,\n};\nuse maud::{DOCTYPE, Markup, PreEscaped, html};\nuse strum::{Display, IntoEnumIterator};\n\nuse crate::auth::CurrentUser;\nuse crate::consts;\nuse crate::listing::{Breadcrumb, Entry, ListingQueryParameters, SortingMethod, SortingOrder};\nuse crate::{MiniserveConfig, archive::ArchiveMethod};\n\n#[allow(clippy::too_many_arguments)]\n/// Renders the file listing\npub fn page(\n    entries: Vec<Entry>,\n    readme: Option<(String, String)>,\n    abs_uri: &Uri,\n    is_root: bool,\n    query_params: ListingQueryParameters,\n    breadcrumbs: &[Breadcrumb],\n    encoded_dir: &str,\n    conf: &MiniserveConfig,\n    current_user: Option<&CurrentUser>,\n) -> Markup {\n    // If query_params.raw is true, we want render a minimal directory listing\n    if query_params.raw.is_some() && query_params.raw.unwrap() {\n        return raw(entries, is_root, conf);\n    }\n\n    let upload_route = format!(\"{}/upload\", &conf.route_prefix);\n    let rm_route = format!(\"{}/rm\", &conf.route_prefix);\n    let (sort_method, sort_order) = (query_params.sort, query_params.order);\n\n    let upload_action = build_upload_action(&upload_route, encoded_dir, sort_method, sort_order);\n    let mkdir_action = build_mkdir_action(&upload_route, encoded_dir);\n\n    let title_path = breadcrumbs_to_path_string(breadcrumbs);\n\n    let upload_allowed = conf.allowed_upload_dir.is_empty()\n        || conf\n            .allowed_upload_dir\n            .iter()\n            .any(|x| encoded_dir.starts_with(&format!(\"/{x}\")));\n    let rm_allowed = conf.allowed_rm_dir.is_empty()\n        || conf\n            .allowed_rm_dir\n            .iter()\n            .any(|x| encoded_dir.starts_with(&format!(\"/{x}\")));\n\n    // OR with other conditions in the future if more actions are added\n    let show_actions = conf.rm_enabled && rm_allowed;\n    let actions_conf = show_actions.then(|| ActionsConf {\n        rm_route: &rm_route,\n    });\n\n    html! {\n        (DOCTYPE)\n        html {\n            (page_header(&title_path, conf.file_upload, conf.web_upload_concurrency, &conf.api_route, &conf.favicon_route, &conf.css_route))\n\n            body #drop-container\n            {\n                div.toolbar_box_group {\n                    @if conf.file_upload {\n                        div.drag-form {\n                            div.form_title {\n                                h1 { \"Drop your file here to upload it\" }\n                            }\n                        }\n                    }\n\n                    @if conf.mkdir_enabled {\n                        div.form {\n                            div.form_title {\n                                h1 { \"Create a new directory\" }\n                            }\n                        }\n                    }\n                }\n                nav {\n                    (qr_spoiler(conf.show_qrcode, abs_uri))\n                    (color_scheme_selector(conf.hide_theme_selector))\n                }\n                div.container {\n                    span #top { }\n                    h1.title dir=\"ltr\" {\n                        @for el in breadcrumbs {\n                            @if el.link == \".\" {\n                                // wrapped in span so the text doesn't shift slightly when it turns into a link\n                                span { bdi { (el.name) } }\n                            } @else {\n                                a href=(parametrized_link(&el.link, sort_method, sort_order, false)) {\n                                    bdi { (el.name) }\n                                }\n                            }\n                            \"/\"\n                        }\n                    }\n                    div.toolbar {\n                        @if conf.tar_enabled || conf.tar_gz_enabled || conf.zip_enabled {\n                            div.tool_row.download_tools {\n                                div.tool data-tool=\"download\" {\n                                    @for archive_method in ArchiveMethod::iter() {\n                                        @if archive_method.is_enabled(conf.tar_enabled, conf.tar_gz_enabled, conf.zip_enabled) {\n                                            (archive_button(archive_method, sort_method, sort_order))\n                                        }\n                                    }\n                                }\n                            }\n                        }\n\n                        div.tool_row.upload_tools {\n                            @if conf.file_upload && upload_allowed {\n                                form.tool id=\"file_submit\" data-tool=\"upload\" action=(upload_action) method=\"POST\" enctype=\"multipart/form-data\" {\n                                    p { \"Select a file to upload or drag it anywhere into the window\" }\n                                    div {\n                                        @match &conf.uploadable_media_type {\n                                            Some(accept) => {input #file-input accept=(accept) type=\"file\" name=\"file_to_upload\" required=\"\" multiple {}},\n                                            None => {input #file-input type=\"file\" name=\"file_to_upload\" required=\"\" multiple {}}\n                                        }\n                                        button type=\"submit\" title=\"Upload File\" { \"Upload file\" }\n                                    }\n                                }\n                            }\n                            @if conf.mkdir_enabled && upload_allowed {\n                                form.tool id=\"mkdir\" data-tool=\"mkdir\" action=(mkdir_action) method=\"POST\" enctype=\"multipart/form-data\" {\n                                    p { \"Specify a directory name to create\" }\n                                    div {\n                                        input type=\"text\" name=\"mkdir\" required=\"\" placeholder=\"Directory name\" {}\n                                        button type=\"submit\" title=\"Create directory\" { \"Create directory\" }\n                                    }\n                                }\n                            }\n                            @if conf.pastebin_enabled && upload_allowed {\n                                form.tool id=\"pastebin\" data-tool=\"pastebin\" {\n                                    p { \"Create a text file in the current directory, a random filename will be generated, or you may specify one.\" }\n                                    div {\n                                        textarea #pastebin_content name=\"paste_content\" title=\"Text content\" required=\"\" { }\n                                    }\n                                    div {\n                                        input type=\"text\" name=\"paste_filename\" title=\"Filename\" placeholder=\"Filename (Optional)\" autocomplete=\"off\" {}\n                                        button type=\"submit\" title=\"Create file\" { \"Create file\" }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                    table {\n                        thead {\n                            th.name { (sortable_title(\"name\", \"Name\", sort_method, sort_order)) }\n                            th.size { (sortable_title(\"size\", \"Size\", sort_method, sort_order)) }\n                            th.date { (sortable_title(\"date\", \"Last modification\", sort_method, sort_order)) }\n                            @if show_actions {\n                                th.actions { span { \"Actions\" } }\n                            }\n                        }\n                        tbody {\n                            @if !is_root {\n                                tr {\n                                    td colspan=(3 + show_actions as usize) {\n                                        p {\n                                            span.root-chevron { (chevron_left()) }\n                                            a.root href=(parametrized_link(\"../\", sort_method, sort_order, false)) {\n                                                \"Parent directory\"\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                            @for entry in entries {\n                                (entry_row(entry, sort_method, sort_order, false, conf.show_exact_bytes, actions_conf, &conf.route_prefix))\n                            }\n                        }\n                    }\n                    @if let Some(readme) = readme {\n                        div id=\"readme\" {\n                            h3 id=\"readme-filename\" { (readme.0) }\n                            div id=\"readme-contents\" {\n                                (PreEscaped (readme.1))\n                            };\n                        }\n                    }\n                    a.back href=\"#top\" {\n                        (arrow_up())\n                    }\n                    div.footer {\n                        @if conf.show_wget_footer {\n                            (wget_footer(abs_uri, conf.title.as_deref(), current_user.map(|x| &*x.name),\n                                conf.file_external_url.as_deref()))\n                        }\n                        @if !conf.hide_version_footer {\n                            (version_footer())\n                        }\n                    }\n                }\n                div.upload_area id=\"upload_area\" {\n                    template id=\"upload_file_item\" {\n                        li.upload_file_item {\n                            div.upload_file_container {\n                                div.upload_file_text {\n                                    span.file_upload_percent { \"\" }\n                                    {\" - \"}\n                                    span.file_size { \"\" }\n                                    {\" - \"}\n                                    span.file_name { \"\" }\n                                }\n                                button.file_cancel_upload { \"✖\" }\n                            }\n                            div.file_progress_bar {}\n                        }\n                    }\n                    div.upload_container {\n                        div.upload_header {\n                            h4 style=\"margin:0px\" id=\"upload_title\" {}\n                            svg id=\"upload-toggle\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"size-6\" {\n                              path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m4.5 15.75 7.5-7.5 7.5 7.5\" {}\n                            }\n                        }\n                        div.upload_action {\n                            p id=\"upload_action_text\" { \"Starting upload...\" }\n                            button.upload_cancel id=\"upload_cancel\" { \"CANCEL\" }\n                        }\n                        div.upload_files {\n                            ul.upload_file_list id=\"upload_file_list\" {\n\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n/// Renders the file listing\npub fn raw(entries: Vec<Entry>, is_root: bool, conf: &MiniserveConfig) -> Markup {\n    html! {\n        (DOCTYPE)\n        html {\n            body {\n                table {\n                    thead {\n                        th.name { \"Name\" }\n                        th.size { \"Size\" }\n                        th.date { \"Last modification\" }\n                    }\n                    tbody {\n                        @if !is_root {\n                            tr {\n                                td colspan=\"3\" {\n                                    p {\n                                        a.root href=(parametrized_link(\"../\", None, None, true)) {\n                                            \"..\"\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                        @for entry in entries {\n                            (entry_row(entry, None, None, true, conf.show_exact_bytes, None, &conf.route_prefix))\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n/// Renders the QR code SVG\nfn qr_code_svg(url: &Uri, margin: usize) -> Result<String, QRCodeError> {\n    let qr = QRBuilder::new(url.to_string())\n        .ecl(consts::QR_EC_LEVEL)\n        .build()?;\n    let svg = SvgBuilder::default().margin(margin).to_str(&qr);\n\n    Ok(svg)\n}\n\n/// Build a path string from a list of breadcrumbs.\nfn breadcrumbs_to_path_string(breadcrumbs: &[Breadcrumb]) -> String {\n    breadcrumbs\n        .iter()\n        .map(|el| el.name.clone())\n        .collect::<Vec<_>>()\n        .join(\"/\")\n}\n\n// Partial: version footer\nfn version_footer() -> Markup {\n    html! {\n       div.version {\n            a href=\"https://github.com/svenstaro/miniserve\" {\n               (crate_name!())\n           }\n           (format!(\"/{}\", crate_version!()))\n       }\n    }\n}\n\nfn wget_footer(\n    abs_path: &Uri,\n    root_dir_name: Option<&str>,\n    current_user: Option<&str>,\n    file_external_url: Option<&str>,\n) -> Markup {\n    fn escape_apostrophes(x: &str) -> String {\n        x.replace('\\'', \"'\\\"'\\\"'\")\n    }\n\n    // Directory depth, 0 is root directory\n    let cut_dirs = match abs_path.path().matches('/').count() - 1 {\n        // Put all the files in a folder of this name\n        0 => format!(\n            \" -P '{}'\",\n            escape_apostrophes(\n                root_dir_name.unwrap_or_else(|| abs_path.authority().unwrap().as_str())\n            )\n        ),\n        1 => String::new(),\n        // Avoids putting the files in excessive directories\n        x => format!(\" --cut-dirs={}\", x - 1),\n    };\n\n    // Ask for password if authentication is required\n    let user_params = match current_user {\n        Some(user) => format!(\" --ask-password --user '{}'\", escape_apostrophes(user)),\n        None => String::new(),\n    };\n\n    // Add the -H option to span hosts when serving files from another instance\n    let span_hosts_option = if file_external_url.is_some() {\n        \" -H\"\n    } else {\n        \" -nH\"\n    };\n\n    let encoded_abs_path = abs_path.to_string().replace('\\'', \"%27\");\n    let command = format!(\n        \"wget -rcnp -R 'index.html*'{span_hosts_option}{cut_dirs}{user_params} '{encoded_abs_path}?raw=true'\"\n    );\n    let click_to_copy = format!(\"navigator.clipboard.writeText(\\\"{command}\\\")\");\n\n    html! {\n        div.downloadDirectory {\n            p { \"Download folder:\" }\n            a.cmd title=\"Click to copy!\" style=\"cursor: pointer;\" onclick=(click_to_copy) { (command) }\n        }\n    }\n}\n\n/// Build the action of the upload form\nfn build_upload_action(\n    upload_route: &str,\n    encoded_dir: &str,\n    sort_method: Option<SortingMethod>,\n    sort_order: Option<SortingOrder>,\n) -> String {\n    let mut upload_action = format!(\"{upload_route}?path={encoded_dir}\");\n    if let Some(sorting_method) = sort_method {\n        upload_action = format!(\"{}&sort={}\", upload_action, &sorting_method);\n    }\n    if let Some(sorting_order) = sort_order {\n        upload_action = format!(\"{}&order={}\", upload_action, &sorting_order);\n    }\n\n    upload_action\n}\n\n/// Build the action of the mkdir form\nfn build_mkdir_action(mkdir_route: &str, encoded_dir: &str) -> String {\n    format!(\"{mkdir_route}?path={encoded_dir}\")\n}\n\nconst THEME_PICKER_CHOICES: &[(&str, &str)] = &[\n    (\"Default (light/dark)\", \"default\"),\n    (\"Squirrel (light)\", \"squirrel\"),\n    (\"Arch Linux (dark)\", \"archlinux\"),\n    (\"Ayu Dark (dark)\", \"ayu_dark\"),\n    (\"Zenburn (dark)\", \"zenburn\"),\n    (\"Monokai (dark)\", \"monokai\"),\n];\n\n#[derive(Debug, Clone, ValueEnum, Display)]\npub enum ThemeSlug {\n    #[strum(serialize = \"squirrel\")]\n    Squirrel,\n    #[strum(serialize = \"archlinux\")]\n    Archlinux,\n    #[strum(serialize = \"ayu_dark\")]\n    AyuDark,\n    #[strum(serialize = \"zenburn\")]\n    Zenburn,\n    #[strum(serialize = \"monokai\")]\n    Monokai,\n}\n\nimpl ThemeSlug {\n    pub fn css(&self) -> &str {\n        match self {\n            Self::Squirrel => grass::include!(\"data/themes/squirrel.scss\"),\n            Self::Archlinux => grass::include!(\"data/themes/archlinux.scss\"),\n            Self::AyuDark => grass::include!(\"data/themes/ayu_dark.scss\"),\n            Self::Zenburn => grass::include!(\"data/themes/zenburn.scss\"),\n            Self::Monokai => grass::include!(\"data/themes/monokai.scss\"),\n        }\n    }\n\n    pub fn css_dark(&self) -> String {\n        format!(\"@media (prefers-color-scheme: dark) {{\\n{}}}\", self.css())\n    }\n}\n\n/// Partial: qr code spoiler\nfn qr_spoiler(show_qrcode: bool, content: &Uri) -> Markup {\n    html! {\n        @if show_qrcode {\n            div {\n                p {\n                    \"QR code\"\n                }\n                div.qrcode #qrcode title=(PreEscaped(content.to_string())) {\n                    @match qr_code_svg(content, consts::SVG_QR_MARGIN) {\n                        Ok(svg) => (PreEscaped(svg)),\n                        Err(err) => (format!(\"QR generation error: {err:?}\")),\n                    }\n                }\n            }\n        }\n    }\n}\n\n/// Partial: color scheme selector\nfn color_scheme_selector(hide_theme_selector: bool) -> Markup {\n    html! {\n        @if !hide_theme_selector {\n            div {\n                p {\n                    \"Change theme...\"\n                }\n                ul.theme {\n                    @for color_scheme in THEME_PICKER_CHOICES {\n                        li data-theme=(color_scheme.1) {\n                            (color_scheme_link(color_scheme))\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n// /// Partial: color scheme link\nfn color_scheme_link(color_scheme: &(&str, &str)) -> Markup {\n    let title = format!(\"Switch to {} theme\", color_scheme.0);\n\n    html! {\n        a href=(format!(\"javascript:updateColorScheme(\\\"{}\\\")\", color_scheme.1)) title=(title) {\n            (color_scheme.0)\n        }\n    }\n}\n\n/// Partial: archive button\nfn archive_button(\n    archive_method: ArchiveMethod,\n    sort_method: Option<SortingMethod>,\n    sort_order: Option<SortingOrder>,\n) -> Markup {\n    let link = if sort_method.is_none() && sort_order.is_none() {\n        format!(\"?download={archive_method}\")\n    } else {\n        format!(\n            \"{}&download={}\",\n            parametrized_link(\"\", sort_method, sort_order, false),\n            archive_method\n        )\n    };\n\n    let text = format!(\"Download .{}\", archive_method.extension());\n\n    html! {\n        a href=(link) {\n            (text)\n        }\n    }\n}\n\n/// Ensure that there's always a trailing slash behind the `link`.\nfn make_link_with_trailing_slash(link: &str) -> String {\n    if link.is_empty() || link.ends_with('/') {\n        link.to_string()\n    } else {\n        format!(\"{link}/\")\n    }\n}\n\n/// If they are set, adds query parameters to links to keep them across pages\nfn parametrized_link(\n    link: &str,\n    sort_method: Option<SortingMethod>,\n    sort_order: Option<SortingOrder>,\n    raw: bool,\n) -> String {\n    if raw {\n        return format!(\"{}?raw=true\", make_link_with_trailing_slash(link));\n    }\n\n    if let Some(method) = sort_method\n        && let Some(order) = sort_order\n    {\n        let parametrized_link = format!(\n            \"{}?sort={}&order={}\",\n            make_link_with_trailing_slash(link),\n            method,\n            order,\n        );\n\n        return parametrized_link;\n    }\n\n    make_link_with_trailing_slash(link)\n}\n\n/// Partial: table header link\nfn sortable_title(\n    name: &str,\n    title: &str,\n    sort_method: Option<SortingMethod>,\n    sort_order: Option<SortingOrder>,\n) -> Markup {\n    let mut link = format!(\"?sort={name}&order=asc\");\n    let mut help = format!(\"Sort by {name} in ascending order\");\n    let mut chevron = chevron_down();\n    let mut class = \"\";\n\n    if let Some(method) = sort_method\n        && method.to_string() == name\n    {\n        class = \"active\";\n        if let Some(order) = sort_order\n            && order.to_string() == \"asc\"\n        {\n            link = format!(\"?sort={name}&order=desc\");\n            help = format!(\"Sort by {name} in descending order\");\n            chevron = chevron_up();\n        }\n    };\n\n    html! {\n        span class=(class) {\n            span.chevron { (chevron) }\n            a href=(link) title=(help) { (title) }\n        }\n    }\n}\n\n/// Partial: rm form\nfn rm_form(rm_route: &str, encoded_path: &str, prefix: &str) -> Markup {\n    let stripped_path = encoded_path.strip_prefix(prefix).unwrap_or(encoded_path);\n    let rm_action = format!(\"{rm_route}?path={stripped_path}\");\n\n    html! {\n        form class=\"rm_form\" action=(rm_action) method=\"POST\" {\n            button type=\"submit\" title=\"Delete\" { \"✗\" }\n        }\n    }\n}\n\n#[derive(Copy, Clone, Debug)]\nstruct ActionsConf<'a> {\n    /// Route prefix for file removal POST requests.\n    rm_route: &'a str,\n}\n\n/// Partial: row for an entry\nfn entry_row(\n    entry: Entry,\n    sort_method: Option<SortingMethod>,\n    sort_order: Option<SortingOrder>,\n    raw: bool,\n    show_exact_bytes: bool,\n    actions_conf: Option<ActionsConf>,\n    route_prefix: &str,\n) -> Markup {\n    html! {\n        @let entry_type = entry.entry_type.clone();\n        tr .{ \"entry-type-\" (entry_type) } {\n            td {\n                p {\n                    @if entry.is_dir() {\n                        @if let Some(ref symlink_dest) = entry.symlink_info {\n                            a.symlink href=(parametrized_link(&entry.link, sort_method, sort_order, raw)) {\n                                (entry.name) \"/\"\n                                span.symlink-symbol { }\n                                a.directory {(symlink_dest) \"/\"}\n                            }\n                        }@else {\n                            a.directory href=(parametrized_link(&entry.link, sort_method, sort_order, raw)) {\n                                (entry.name) \"/\"\n                            }\n                        }\n                    } @else if entry.is_file() {\n                        @if let Some(ref symlink_dest) = entry.symlink_info {\n                            a.symlink href=(&entry.link) {\n                                (entry.name)\n                                span.symlink-symbol { }\n                                a.file {(symlink_dest)}\n                            }\n                        }@else {\n                            a.file href=(&entry.link) {\n                                (entry.name)\n                            }\n                        }\n\n                        @if !raw {\n                            @if let Some(size) = entry.size {\n                                @if show_exact_bytes {\n                                    span.mobile-info.size {\n                                        (maud::display(format!(\"{} B\", size.as_u64())))\n                                    }\n                                }@else {\n                                    span.mobile-info.size {\n                                        (sortable_title(\"size\", &format!(\"{size}\"), sort_method, sort_order))\n                                    }\n                                }\n                            }\n                            @if let Some(modification_timer) = humanize_systemtime(entry.last_modification_date) {\n                                span.mobile-info.history {\n                                    (sortable_title(\"date\", &modification_timer, sort_method, sort_order))\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n            td.size-cell {\n                @if let Some(size) = entry.size {\n                    @if show_exact_bytes {\n                        (maud::display(format!(\"{} B\", size.as_u64())))\n                    }@else {\n                        (maud::display(size))\n                    }\n                }\n            }\n            td.date-cell {\n                @if let Some(modification_date) = convert_to_local(entry.last_modification_date) {\n                    span {\n                        (modification_date) \" \"\n                    }\n                }\n                @if let Some(modification_timer) = humanize_systemtime(entry.last_modification_date) {\n                    span.history {\n                        (modification_timer)\n                    }\n                }\n            }\n            @if let Some(conf) = actions_conf {\n                td.actions-cell {\n                    (rm_form(conf.rm_route, &entry.link, route_prefix))\n                }\n            }\n        }\n    }\n}\n\n/// Partial: up arrow\nfn arrow_up() -> Markup {\n    PreEscaped(\"⇪\".to_string())\n}\n\n/// Partial: chevron left\nfn chevron_left() -> Markup {\n    PreEscaped(\"◂\".to_string())\n}\n\n/// Partial: chevron up\nfn chevron_up() -> Markup {\n    PreEscaped(\"▴\".to_string())\n}\n\n/// Partial: chevron up\nfn chevron_down() -> Markup {\n    PreEscaped(\"▾\".to_string())\n}\n\n/// Partial: page header\nfn page_header(\n    title: &str,\n    file_upload: bool,\n    web_file_concurrency: usize,\n    api_route: &str,\n    favicon_route: &str,\n    css_route: &str,\n) -> Markup {\n    html! {\n        head {\n            meta charset=\"utf-8\";\n            meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\";\n            meta name=\"viewport\" content=\"width=device-width, initial-scale=1\";\n            meta name=\"color-scheme\" content=\"dark light\";\n\n            link rel=\"icon\" type=\"image/svg+xml\" href={ (favicon_route) };\n            link rel=\"stylesheet\" href={ (css_route) };\n\n            title { (title) }\n\n            script {\n                (PreEscaped(r#\"\n                    // updates the color scheme by setting the theme data attribute\n                    // on body and saving the new theme to local storage\n                    function updateColorScheme(name) {\n                        if (name && name != \"default\") {\n                            localStorage.setItem('theme', name);\n                            document.body.setAttribute(\"data-theme\", name)\n                        } else {\n                            localStorage.removeItem('theme');\n                            document.body.removeAttribute(\"data-theme\")\n                        }\n                    }\n\n                    // read theme from local storage and apply it to body\n                    function loadColorScheme() {\n                        var name = localStorage.getItem('theme');\n                        updateColorScheme(name);\n                    }\n\n                    // load saved theme on page load\n                    addEventListener(\"load\", loadColorScheme);\n                    // load saved theme when local storage is changed (synchronize between tabs)\n                    addEventListener(\"storage\", loadColorScheme);\n                \"#))\n            }\n\n            script {\n                (format!(\"const API_ROUTE = '{api_route}';\"))\n                (PreEscaped(r#\"\n                    let dirSizeCache = {};\n\n                    // Query the directory size from the miniserve API\n                    function fetchDirSize(dir) {\n                        return fetch(API_ROUTE, {\n                            headers: {\n                                'Accept': 'application/json',\n                                'Content-Type': 'application/json'\n                            },\n                            method: 'POST',\n                            body: JSON.stringify({\n                                DirSize: dir\n                            })\n                        }).then(resp => resp.ok ? resp.text() : \"~\")\n                    }\n\n                    function updateSizeCells() {\n                        const directoryCells = document.querySelectorAll('tr.entry-type-directory .size-cell');\n\n                        directoryCells.forEach(cell => {\n                            // Get the dir from the sibling anchor tag.\n                            const href = cell.parentNode.querySelector('a').href;\n                            const target = new URL(href).pathname;\n\n                            // First check our local cache\n                            if (target in dirSizeCache) {\n                                cell.dataset.size = dirSizeCache[target];\n                            } else {\n                                fetchDirSize(target).then(dir_size => {\n                                    cell.dataset.size = dir_size;\n                                    dirSizeCache[target] = dir_size;\n                                })\n                                .catch(error => console.error(\"Error fetching dir size:\", error));\n                            }\n                        })\n                    }\n                    setInterval(updateSizeCells, 1000);\n                \"#))\n            }\n\n            @if file_upload {\n                script {\n                    (format!(\"const CONCURRENCY = {web_file_concurrency};\"))\n                    (PreEscaped(r#\"\n                    window.onload = function() {\n                        // Constants\n                        const UPLOADING = 'uploading', PENDING = 'pending', COMPLETE = 'complete', CANCELLED = 'cancelled', FAILED = 'failed'\n                        const UPLOAD_ITEM_ORDER = { UPLOADING: 0, PENDING: 1, COMPLETE: 2, CANCELLED: 3, FAILED: 4 }\n                        let CANCEL_UPLOAD = false;\n\n                        // File Upload dom elements. Used for interacting with the\n                        // upload container.\n                        const form = document.querySelector('#file_submit');\n                        const uploadArea = document.querySelector('#upload_area');\n                        const uploadTitle = document.querySelector('#upload_title');\n                        const uploadActionText = document.querySelector('#upload_action_text');\n                        const uploadCancelButton = document.querySelector('#upload_cancel');\n                        const uploadList = document.querySelector('#upload_file_list');\n                        const fileUploadItemTemplate = document.querySelector('#upload_file_item');\n                        const uploadWidgetToggle = document.querySelector('#upload-toggle');\n\n                        const dropContainer = document.querySelector('#drop-container');\n                        const dragForm = document.querySelector('.drag-form');\n                        const fileInput = document.querySelector('#file-input');\n                        const collection = [];\n\n                        dropContainer.ondragover = function(e) {\n                            e.preventDefault();\n                        }\n\n                        dropContainer.ondragenter = function(e) {\n                            e.preventDefault();\n                            if (collection.length === 0) {\n                                dragForm.style.display = 'initial';\n                            }\n                            collection.push(e.target);\n                        };\n\n                        dropContainer.ondragleave = function(e) {\n                            e.preventDefault();\n                            collection.splice(collection.indexOf(e.target), 1);\n                            if (collection.length === 0) {\n                                dragForm.style.display = 'none';\n                            }\n                        };\n\n                        dropContainer.ondrop = function(e) {\n                            e.preventDefault();\n                            fileInput.files = e.dataTransfer.files;\n                            form.requestSubmit();\n                            dragForm.style.display = 'none';\n                        };\n\n                        // Event listener for toggling the upload widget display on mobile.\n                        uploadWidgetToggle.addEventListener('click', function (e) {\n                            e.preventDefault();\n                            if (uploadArea.style.height === \"100vh\") {\n                                uploadArea.style = \"\"\n                                document.body.style = \"\"\n                                uploadWidgetToggle.style = \"\"\n                            } else {\n                                uploadArea.style.height = \"100vh\"\n                                document.body.style = \"overflow: hidden\"\n                                uploadWidgetToggle.style = \"transform: rotate(180deg)\"\n                            }\n                        })\n\n                        // Cancel all active and pending uploads\n                        uploadCancelButton.addEventListener('click', function (e) {\n                            e.preventDefault();\n                            CANCEL_UPLOAD = true;\n                        })\n\n                        form.addEventListener('submit', function (e) {\n                            e.preventDefault()\n                            uploadFiles()\n                        })\n\n                        // When uploads start, finish or are cancelled, the UI needs to reactively shows those\n                        // updates of the state. This function updates the text on the upload widget to accurately\n                        // show the state of all uploads.\n                        function updateUploadTextAndList() {\n                            // All state is kept as `data-*` attributed on the HTML node.\n                            const queryLength = (state) => document.querySelectorAll(`[data-state='${state}']`).length;\n                            const total = document.querySelectorAll(\"[data-state]\").length;\n                            const uploads = queryLength(UPLOADING);\n                            const pending = queryLength(PENDING);\n                            const completed = queryLength(COMPLETE);\n                            const cancelled = queryLength(CANCELLED);\n                            const failed = queryLength(FAILED);\n                            const allCompleted = completed + cancelled + failed;\n\n                            // Update header text based on remaining uploads\n                            let headerText = `${total - allCompleted} uploads remaining...`;\n                            if (total === allCompleted) {\n                                headerText = `Complete! Reloading Page!`\n                            }\n\n                            // Build a summary of statuses for sub header\n                            const statuses = []\n                            if (uploads > 0) { statuses.push(`Uploading ${uploads}`) }\n                            if (pending > 0) { statuses.push(`Pending ${pending}`) }\n                            if (completed > 0) { statuses.push(`Complete ${completed}`) }\n                            if (cancelled > 0) { statuses.push(`Cancelled ${cancelled}`) }\n                            if (failed > 0) { statuses.push(`Failed ${failed}`) }\n\n                            uploadTitle.textContent = headerText\n                            uploadActionText.textContent = statuses.join(', ')\n                        }\n\n                        // Initiates the file upload process by disabling the ability for more files to be\n                        // uploaded and creating async callbacks for each file that needs to be uploaded.\n                        // Given the concurrency set by the server input arguments, it will try to process\n                        // that many uploads at once\n                        function uploadFiles() {\n                            fileInput.disabled = true;\n\n                            // Map all the files into async callbacks (uploadFile is a function that returns a function)\n                            const callbacks = Array.from(fileInput.files).map(uploadFile);\n\n                            // Get a list of all the callbacks\n                            const concurrency = CONCURRENCY === 0 ? callbacks.length : CONCURRENCY;\n\n                            // Worker function that continuously pulls tasks from the shared queue.\n                            async function worker() {\n                                while (callbacks.length > 0) {\n                                    // Remove a task from the front of the queue.\n                                    const task = callbacks.shift();\n                                    if (task) {\n                                        await task();\n                                        updateUploadTextAndList();\n                                    }\n                                }\n                            }\n\n                            // Create a work stealing shared queue, split up between `concurrency` amount of workers.\n                            const workers = Array.from({ length: concurrency }).map(worker);\n\n                            // Wait for all the workers to complete\n                            Promise.allSettled(workers)\n                                .finally(() => {\n                                    updateUploadTextAndList();\n                                    form.reset();\n                                    setTimeout(() => { uploadArea.classList.remove('active'); }, 1000)\n                                    setTimeout(() => { window.location.reload(); }, 1500)\n                                })\n\n                            updateUploadTextAndList();\n                            uploadArea.classList.add('active')\n                            uploadList.scrollTo(0, 0)\n                        }\n\n                        function formatBytes(bytes, decimals) {\n                            if (bytes == 0) return '0 Bytes';\n                            var k = 1024,\n                                dm = decimals || 2,\n                                sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],\n                                i = Math.floor(Math.log(bytes) / Math.log(k));\n                            return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];\n                        }\n\n                        document.querySelector('input[type=\"file\"]').addEventListener('change', async (e) => {\n                          const file = e.target.files[0];\n                        });\n\n                        async function get256FileHash(file) {\n                          const arrayBuffer = await file.arrayBuffer();\n                          const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);\n                          const hashArray = Array.from(new Uint8Array(hashBuffer));\n                          return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');\n                        }\n\n                        // Upload a file. This function will create a upload item in the upload\n                        // widget from an HTML template. It then returns a promise which will\n                        // be used to upload the file to the server and control the styles and\n                        // interactions on the HTML list item.\n                        function uploadFile(file) {\n                            const fileUploadItem = fileUploadItemTemplate.content.cloneNode(true)\n                            const itemContainer = fileUploadItem.querySelector(\".upload_file_item\")\n                            const itemText = fileUploadItem.querySelector(\".upload_file_text\")\n                            const size = fileUploadItem.querySelector(\".file_size\")\n                            const name = fileUploadItem.querySelector(\".file_name\")\n                            const percentText = fileUploadItem.querySelector(\".file_upload_percent\")\n                            const bar = fileUploadItem.querySelector(\".file_progress_bar\")\n                            const cancel = fileUploadItem.querySelector(\".file_cancel_upload\")\n                            let preCancel = false;\n\n                            itemContainer.dataset.state = PENDING\n                            name.textContent = file.name\n                            size.textContent = formatBytes(file.size)\n                            percentText.textContent = \"0%\"\n\n                            uploadList.append(fileUploadItem)\n\n                            // Cancel an upload before it even started.\n                            function preCancelUpload() {\n                                preCancel = true;\n                                itemText.classList.add(CANCELLED);\n                                bar.classList.add(CANCELLED);\n                                itemContainer.dataset.state = CANCELLED;\n                                itemContainer.style.background = 'var(--upload_modal_file_upload_complete_background)';\n                                cancel.disabled = true;\n                                cancel.removeEventListener(\"click\", preCancelUpload);\n                                uploadCancelButton.removeEventListener(\"click\", preCancelUpload);\n                                updateUploadTextAndList();\n                            }\n\n                            uploadCancelButton.addEventListener(\"click\", preCancelUpload)\n                            cancel.addEventListener(\"click\", preCancelUpload)\n\n                            // A callback function is return so that the upload doesn't start until\n                            // we want it to. This is so that we have control over our desired concurrency.\n                            return () => {\n                                if (preCancel) {\n                                    return Promise.resolve()\n                                }\n\n                                // Upload the single file in a multipart request.\n                                return new Promise(async (resolve, reject) => {\n                                    // File hash calculation may fail at times:\n                                    //   1. `crypto.subtle` is not available in nonsecure context (e.g. non-HTTPS LAN).\n                                    //      See https://developer.mozilla.org/en-US/docs/Web/API/Crypto/subtle\n                                    //   2. For files larger than 2GB, Firefox will refuse to calculate the SHA-256 value,\n                                    //      while Chrome will refuse to create a ArrayBuffer (#1541).\n                                    const fileHash = await get256FileHash(file).catch(() => \"\");\n                                    const xhr = new XMLHttpRequest();\n                                    const formData = new FormData();\n                                    formData.append('file', file);\n\n                                    function onReadyStateChange(e) {\n                                        if (e.target.readyState == 4) {\n                                            if (e.target.status == 200) {\n                                                completeSuccess()\n                                            } else {\n                                                failedUpload(e.target.status)\n                                            }\n                                        }\n                                    }\n\n                                    function onError(e) {\n                                        failedUpload()\n                                    }\n\n                                    function onAbort(e) {\n                                        cancelUpload()\n                                    }\n\n                                    function onProgress (e) {\n                                        update(Math.round((e.loaded / e.total) * 100));\n                                    }\n\n                                    function update(uploadPercent) {\n                                        let wholeNumber = Math.floor(uploadPercent)\n                                        percentText.textContent = `${wholeNumber}%`\n                                        bar.style.width = `${wholeNumber}%`\n                                    }\n\n                                    function completeSuccess() {\n                                        cancel.textContent = '✔';\n                                        cancel.classList.add(COMPLETE);\n                                        bar.classList.add(COMPLETE);\n                                        cleanUp(COMPLETE)\n                                    }\n\n                                    function failedUpload(statusCode) {\n                                        cancel.textContent = `${statusCode} ⚠`;\n                                        itemText.classList.add(FAILED);\n                                        bar.classList.add(FAILED);\n                                        cleanUp(FAILED);\n                                    }\n\n                                    function cancelUpload() {\n                                        xhr.abort()\n                                        itemText.classList.add(CANCELLED);\n                                        bar.classList.add(CANCELLED);\n                                        cleanUp(CANCELLED);\n                                    }\n\n                                    function cleanUp(state) {\n                                        itemContainer.dataset.state = state;\n                                        itemContainer.style.background = 'var(--upload_modal_file_upload_complete_background)';\n                                        cancel.disabled = true;\n                                        cancel.removeEventListener(\"click\", cancelUpload)\n                                        uploadCancelButton.removeEventListener(\"click\", cancelUpload)\n                                        xhr.removeEventListener('readystatechange', onReadyStateChange);\n                                        xhr.removeEventListener(\"error\", onError);\n                                        xhr.removeEventListener(\"abort\", onAbort);\n                                        xhr.upload.removeEventListener('progress', onProgress);\n                                        resolve()\n                                    }\n\n                                    uploadCancelButton.addEventListener(\"click\", cancelUpload)\n                                    cancel.addEventListener(\"click\", cancelUpload)\n\n                                    if (CANCEL_UPLOAD) {\n                                        cancelUpload()\n                                    } else {\n                                        itemContainer.dataset.state = UPLOADING\n                                        xhr.addEventListener('readystatechange', onReadyStateChange);\n                                        xhr.addEventListener(\"error\", onError);\n                                        xhr.addEventListener(\"abort\", onAbort);\n                                        xhr.upload.addEventListener('progress', onProgress);\n                                        xhr.open('post', form.getAttribute(\"action\"), true);\n                                        if (fileHash) {\n                                            xhr.setRequestHeader('X-File-Hash', fileHash);\n                                            xhr.setRequestHeader('X-File-Hash-Function', 'SHA256');\n                                        }\n                                        xhr.send(formData);\n                                    }\n                                })\n                            }\n                        }\n\n                        // Bind pastebin submission to create a text/plain blob which is injected\n                        // into the upload input then submitted. A title is automatically generated\n                        // if none is given.\n                        const fileUploadForm = document.querySelector('#file_submit');\n                        const fileUploadInput = document.querySelector('#file_submit input[type=file]');\n                        const pastebinForm = document.querySelector('form#pastebin');\n                        const pastebinFilename = pastebinForm.querySelector('input[name=paste_filename]');\n                        const pastebinContent = pastebinForm.querySelector('textarea');\n                        pastebinContent.addEventListener('keydown', (event) => {\n                            // common convenience of ctrl-enter to submit\n                            if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {\n                                event.preventDefault();\n                                event.target.form.requestSubmit();\n                            }\n                        });\n\n                        pastebinForm.addEventListener('submit', (event) => {\n                            // The pastebin form is \"dead\" and should not cause any page-submit\n                            // events. We capture the pastebin form content, convert it into a\n                            // in-memory blob, then pass that blob to the regular fileUpload form\n                            // for submission, as if a user and selected a real file.\n                            event.preventDefault();\n                            const text = pastebinContent.value;\n                            const title = ((inputValue) => {\n                                const title = inputValue.trim();\n                                if (title.length === 0) {\n                                    const suffix = crypto.randomUUID().substring(0,6);\n                                    return `paste-${suffix}.txt`;\n                                } else {\n                                    // use given extension if one is present, otherwise make it\n                                    // .txt. We're quite liberal in what we consider an extension,\n                                    // any number of alpha-numeric after a dot.\n                                    if (/\\.[0-9a-z]+$/i.test(title)) {\n                                        return title;\n                                    } else {\n                                        return `${title}.txt`;\n                                    }\n                                }\n                            })(pastebinFilename.value);\n                            // Package text as a file and submit\n                            const blob = new Blob([text], {type: 'text/plain'});\n                            const file = new File([blob], title, {type: 'text/plain'});\n                            const container = new DataTransfer();\n                            container.items.add(file);\n                            fileUploadInput.files = container.files;\n                            fileUploadForm.submit();\n                        });\n                    }\n                    \"#))\n                }\n            }\n        }\n    }\n}\n\n/// Converts a SystemTime object to a strings tuple (date, time)\nfn convert_to_local(src_time: Option<SystemTime>) -> Option<String> {\n    src_time\n        .map(DateTime::<Local>::from)\n        .map(|date_time| date_time.format(\"%Y-%m-%d %H:%M:%S %:z\").to_string())\n}\n\n/// Converts a SystemTime to a string readable by a human,\n/// and gives a rough approximation of the elapsed time since\nfn humanize_systemtime(time: Option<SystemTime>) -> Option<String> {\n    time.map(|time| time.humanize())\n}\n\n/// Renders an error on the webpage\npub fn render_error(\n    error_description: &str,\n    error_code: StatusCode,\n    conf: &MiniserveConfig,\n    return_address: &str,\n) -> Markup {\n    html! {\n        (DOCTYPE)\n        html {\n            (page_header(&error_code.to_string(), false, conf.web_upload_concurrency, &conf.api_route, &conf.favicon_route, &conf.css_route))\n\n            body\n            {\n                div.error {\n                    p { (error_code.to_string()) }\n                    @for error in error_description.lines() {\n                        p { (error) }\n                    }\n                    // WARN don't expose random route!\n                    @if conf.route_prefix.is_empty() && !conf.disable_indexing {\n                        div.error-nav {\n                            a.error-back href=(return_address) {\n                                \"Go back to file listing\"\n                            }\n                        }\n                    }\n                    @if !conf.hide_version_footer {\n                        p.footer {\n                            (version_footer())\n                        }\n\n                    }\n                }\n            }\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    fn to_html(wget_part: &str) -> String {\n        format!(\n            r#\"<div class=\"downloadDirectory\"><p>Download folder:</p><a class=\"cmd\" title=\"Click to copy!\" style=\"cursor: pointer;\" onclick=\"navigator.clipboard.writeText(&quot;wget -rcnp -R 'index.html*' {wget_part}/?raw=true'&quot;)\">wget -rcnp -R 'index.html*' {wget_part}/?raw=true'</a></div>\"#\n        )\n    }\n\n    fn uri(x: &str) -> Uri {\n        Uri::try_from(x).unwrap()\n    }\n\n    #[test]\n    fn test_wget_footer_trivial() {\n        let to_be_tested: String =\n            wget_footer(&uri(\"https://github.com/\"), None, None, None).into();\n        let expected = to_html(\"-nH -P 'github.com' 'https://github.com\");\n        assert_eq!(to_be_tested, expected);\n    }\n\n    #[test]\n    fn test_wget_footer_with_root_dir() {\n        let to_be_tested: String = wget_footer(\n            &uri(\"https://github.com/svenstaro/miniserve/\"),\n            Some(\"Miniserve\"),\n            None,\n            None,\n        )\n        .into();\n        let expected = to_html(\"-nH --cut-dirs=1 'https://github.com/svenstaro/miniserve\");\n        assert_eq!(to_be_tested, expected);\n    }\n\n    #[test]\n    fn test_wget_footer_with_root_dir_and_user() {\n        let to_be_tested: String = wget_footer(\n            &uri(\"http://1und1.de/\"),\n            Some(\"1&1 - Willkommen!!!\"),\n            Some(\"Marcell D'Avis\"),\n            None,\n        )\n        .into();\n        let expected = to_html(\n            \"-nH -P '1&amp;1 - Willkommen!!!' --ask-password --user 'Marcell D'&quot;'&quot;'Avis' 'http://1und1.de\",\n        );\n        assert_eq!(to_be_tested, expected);\n    }\n\n    #[test]\n    fn test_wget_footer_escaping() {\n        let to_be_tested: String = wget_footer(\n            &uri(\"http://127.0.0.1:1234/geheime_dokumente.php/\"),\n            Some(\"Streng Geheim!!!\"),\n            Some(\"uøý`¶'7ÅÛé\"),\n            None,\n        )\n        .into();\n        let expected = to_html(\n            \"-nH --ask-password --user 'uøý`¶'&quot;'&quot;'7ÅÛé' 'http://127.0.0.1:1234/geheime_dokumente.php\",\n        );\n        assert_eq!(to_be_tested, expected);\n    }\n\n    #[test]\n    fn test_wget_footer_ip() {\n        let to_be_tested: String =\n            wget_footer(&uri(\"http://127.0.0.1:420/\"), None, None, None).into();\n        let expected = to_html(\"-nH -P '127.0.0.1:420' 'http://127.0.0.1:420\");\n        assert_eq!(to_be_tested, expected);\n    }\n\n    #[test]\n    fn test_wget_footer_externalurl() {\n        let to_be_tested: String = wget_footer(\n            &uri(\"https://github.com/\"),\n            None,\n            None,\n            Some(\"https://gitlab.com\"),\n        )\n        .into();\n        let expected = to_html(\"-H -P 'github.com' 'https://github.com\");\n        assert_eq!(to_be_tested, expected);\n    }\n\n    #[test]\n    fn test_rm_form_strips_prefix() {\n        let rm_route = \"/rm\";\n        let prefix = \"/prefix\";\n        let encoded_path = \"/prefix/some/path/file.txt\";\n\n        let html = rm_form(rm_route, encoded_path, prefix);\n        let expected_action = r#\"action=\"/rm?path=/some/path/file.txt\"\"#;\n\n        assert!(\n            html.0.contains(expected_action),\n            \"Actual HTML: {}\\nExpected to contain: {}\",\n            html.0,\n            expected_action\n        )\n    }\n}\n"
  },
  {
    "path": "src/webdav_fs.rs",
    "content": "//! Helper types and functions to allow configuring hidden files visibility\n//! for WebDAV handlers\n\nuse dav_server::{\n    davpath::DavPath,\n    fs::{\n        DavDirEntry, DavFile, DavFileSystem, DavMetaData, FsError as DavFsError,\n        FsFuture as DavFsFuture, FsStream as DavFsStream, OpenOptions as DavOpenOptions,\n        ReadDirMeta as DavReadDirMeta,\n    },\n    localfs::LocalFs,\n};\nuse futures::StreamExt;\nuse std::ffi::OsStr;\n#[cfg(target_family = \"unix\")]\nuse std::os::unix::ffi::OsStrExt;\nuse std::path::{Component, Path, PathBuf};\nuse tokio::fs;\n\n/// A dav_server local filesystem backend that can be configured to deny access\n/// to files and directories with names starting with a dot.\n#[derive(Clone)]\npub struct RestrictedFs {\n    local: Box<LocalFs>,\n    base_path: PathBuf,\n    show_hidden: bool,\n    no_symlinks: bool,\n}\n\nimpl RestrictedFs {\n    /// Creates a new RestrictedFs serving the local path at \"base\".\n    /// If \"show_hidden\" is false, access to hidden files is prevented.\n    /// If \"no_symlinks\" is true, access to symlinks is prevented.\n    pub fn new<P: AsRef<Path>>(base: P, show_hidden: bool, no_symlinks: bool) -> Box<RestrictedFs> {\n        let base_path = base.as_ref().to_path_buf();\n        let local = LocalFs::new(base, false, false, false);\n        Box::new(RestrictedFs {\n            local,\n            base_path,\n            show_hidden,\n            no_symlinks,\n        })\n    }\n\n    /// true if the path is allowed to appear in responses (not hidden and/or not a symlink, depending on flags)\n    async fn is_path_allowed(&self, path: &DavPath) -> bool {\n        if self.no_symlinks && path_has_symlink_components(path, &self.base_path).await {\n            return false;\n        }\n        if !self.show_hidden && path_has_hidden_components(path) {\n            return false;\n        }\n        true\n    }\n}\n\n/// true if any normal component of path either starts with dot or can't be turned into a str\nfn path_has_hidden_components(path: &DavPath) -> bool {\n    path.as_rel_ospath().components().any(|c| match c {\n        Component::Normal(name) => name.to_str().is_none_or(|s| s.starts_with('.')),\n        _ => panic!(\"dav path should not contain any non-normal components\"),\n    })\n}\n\n/// true if any component in `path` (relative to `base_path`) is a symlink\nasync fn path_has_symlink_components(path: &DavPath, base_path: &Path) -> bool {\n    let mut current_path = base_path.to_path_buf();\n    for comp in path.as_rel_ospath().components() {\n        match comp {\n            Component::Normal(name) => {\n                current_path.push(name);\n                if let Ok(md) = fs::symlink_metadata(&current_path).await\n                    && md.file_type().is_symlink()\n                {\n                    return true;\n                }\n            }\n            _ => {\n                panic!(\"dav path should not contain any non-normal components\")\n            }\n        }\n    }\n    false\n}\n\nimpl DavFileSystem for RestrictedFs {\n    fn open<'a>(\n        &'a self,\n        path: &'a DavPath,\n        options: DavOpenOptions,\n    ) -> DavFsFuture<'a, Box<dyn DavFile>> {\n        Box::pin(async move {\n            if !self.is_path_allowed(path).await {\n                Err(DavFsError::NotFound)\n            } else {\n                self.local.open(path, options).await\n            }\n        })\n    }\n\n    fn read_dir<'a>(\n        &'a self,\n        path: &'a DavPath,\n        meta: DavReadDirMeta,\n    ) -> DavFsFuture<'a, DavFsStream<Box<dyn DavDirEntry>>> {\n        Box::pin(async move {\n            if !self.is_path_allowed(path).await {\n                return Err(DavFsError::NotFound);\n            }\n\n            if self.show_hidden && !self.no_symlinks {\n                return self.local.read_dir(path, meta).await;\n            }\n\n            let dav_path = path.as_rel_ospath();\n            let base_path = self.base_path.join(dav_path);\n            let show_hidden = self.show_hidden;\n            let no_symlinks = self.no_symlinks;\n\n            let stream = self.local.read_dir(path, meta).await?;\n\n            let filtered = stream.filter_map(move |entry_res| {\n                let base_path = base_path.clone();\n                async move {\n                    match entry_res {\n                        Ok(e) => {\n                            if !show_hidden && e.name().starts_with(b\".\") {\n                                return None;\n                            }\n                            if no_symlinks {\n                                let name = e.name();\n                                #[cfg(not(target_os = \"windows\"))]\n                                let os_string = OsStr::from_bytes(&name);\n                                #[cfg(target_os = \"windows\")]\n                                let os_string: &OsStr =\n                                    std::str::from_utf8(&name).unwrap().as_ref();\n                                let entry_path = base_path.join(os_string);\n                                if let Ok(md) = fs::symlink_metadata(&entry_path).await\n                                    && md.file_type().is_symlink()\n                                {\n                                    return None;\n                                }\n                            }\n                            Some(Ok(e))\n                        }\n                        Err(e) => Some(Err(e)),\n                    }\n                }\n            });\n\n            Ok(Box::pin(filtered) as DavFsStream<Box<dyn DavDirEntry>>)\n        })\n    }\n\n    fn metadata<'a>(&'a self, path: &'a DavPath) -> DavFsFuture<'a, Box<dyn DavMetaData>> {\n        Box::pin(async move {\n            if !self.is_path_allowed(path).await {\n                Err(DavFsError::NotFound)\n            } else {\n                self.local.metadata(path).await\n            }\n        })\n    }\n\n    fn symlink_metadata<'a>(&'a self, path: &'a DavPath) -> DavFsFuture<'a, Box<dyn DavMetaData>> {\n        Box::pin(async move {\n            if !self.is_path_allowed(path).await {\n                Err(DavFsError::NotFound)\n            } else {\n                self.local.symlink_metadata(path).await\n            }\n        })\n    }\n}\n"
  },
  {
    "path": "tests/api.rs",
    "content": "use std::collections::HashMap;\n\nuse percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};\nuse reqwest::{StatusCode, blocking::Client};\nuse rstest::rstest;\n\nmod fixtures;\n\nuse crate::fixtures::{DIRECTORIES, Error, TestServer, reqwest_client, server};\n\n/// Test that we can get dir size for plain paths as well as percent-encoded paths\n#[rstest]\n#[case(DIRECTORIES[0].to_string())]\n#[case(DIRECTORIES[1].to_string())]\n#[case(DIRECTORIES[2].to_string())]\n#[case(utf8_percent_encode(DIRECTORIES[0], NON_ALPHANUMERIC).to_string())]\n#[case(utf8_percent_encode(DIRECTORIES[1], NON_ALPHANUMERIC).to_string())]\n#[case(utf8_percent_encode(DIRECTORIES[2], NON_ALPHANUMERIC).to_string())]\nfn api_dir_size(\n    #[case] dir: String,\n    #[with(&[\"--directory-size\"])] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let mut command = HashMap::new();\n    command.insert(\"DirSize\", dir);\n\n    let resp = reqwest_client\n        .post(server.url().join(\"__miniserve_internal/api\")?)\n        .json(&command)\n        .send()?\n        .error_for_status()?;\n\n    assert_eq!(resp.status(), StatusCode::OK);\n    assert_ne!(resp.text()?, \"0 B\");\n\n    Ok(())\n}\n\n/// Test for path traversal vulnerability (CWE-22) in DirSize parameter.\n#[rstest]\n#[case(\"/tmp\")] // Not CWE-22, but `foo` isn't a directory\n#[case(\"/../foo\")]\n#[case(\"../foo\")]\n#[case(\"../tmp\")]\n#[case(\"/tmp\")]\n#[case(\"/foo\")]\n#[case(\"C:/foo\")]\n#[case(r\"C:\\foo\")]\n#[case(r\"\\foo\")]\nfn api_dir_size_prevent_path_transversal_attacks(\n    #[case] path: &str,\n    #[with(&[\"--directory-size\"])] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let mut command = HashMap::new();\n    command.insert(\"DirSize\", path);\n\n    let resp = reqwest_client\n        .post(server.url().join(\"__miniserve_internal/api\")?)\n        .json(&command)\n        .send()?;\n\n    assert_eq!(resp.status(), StatusCode::BAD_REQUEST);\n\n    Ok(())\n}\n"
  },
  {
    "path": "tests/archive.rs",
    "content": "use std::io::Cursor;\n\nuse reqwest::{StatusCode, blocking::Client};\nuse rstest::rstest;\nuse select::{document::Document, predicate::Text};\nuse zip::ZipArchive;\n\nmod fixtures;\n\nuse crate::fixtures::{Error, TestServer, reqwest_client, server};\n\nenum ArchiveKind {\n    TarGz,\n    Tar,\n    Zip,\n}\n\nimpl ArchiveKind {\n    fn server_option(&self) -> &'static str {\n        match self {\n            ArchiveKind::TarGz => \"--enable-tar-gz\",\n            ArchiveKind::Tar => \"--enable-tar\",\n            ArchiveKind::Zip => \"--enable-zip\",\n        }\n    }\n\n    fn link_text(&self) -> &'static str {\n        match self {\n            ArchiveKind::TarGz => \"Download .tar.gz\",\n            ArchiveKind::Tar => \"Download .tar\",\n            ArchiveKind::Zip => \"Download .zip\",\n        }\n    }\n\n    fn download_param(&self) -> &'static str {\n        match self {\n            ArchiveKind::TarGz => \"?download=tar_gz\",\n            ArchiveKind::Tar => \"?download=tar\",\n            ArchiveKind::Zip => \"?download=zip\",\n        }\n    }\n}\n\nfn fetch_index_document(\n    reqwest_client: &Client,\n    server: &TestServer,\n    expected: StatusCode,\n) -> Result<Document, Error> {\n    let resp = reqwest_client.get(server.url()).send()?;\n    assert_eq!(resp.status(), expected);\n\n    Ok(Document::from_read(resp)?)\n}\n\nfn download_archive_bytes(\n    reqwest_client: &Client,\n    server: &TestServer,\n    kind: ArchiveKind,\n) -> Result<(StatusCode, usize), Error> {\n    let resp = reqwest_client\n        .get(server.url().join(kind.download_param())?)\n        .send()?;\n\n    Ok((resp.status(), resp.bytes()?.len()))\n}\n\nfn assert_link_presence(document: &Document, present: &[&str], absent: &[&str]) {\n    let contains_text =\n        |document: &Document, text: &str| document.find(Text).any(|x| x.text() == text);\n\n    for text in present {\n        assert!(\n            contains_text(document, text),\n            \"Expected link text '{text}' to be present\",\n        );\n    }\n\n    for text in absent {\n        assert!(\n            !contains_text(document, text),\n            \"Expected link text '{text}' to be absent\",\n        );\n    }\n}\n\n/// By default, all archive links are hidden.\n#[rstest]\nfn archives_are_disabled_links(server: TestServer, reqwest_client: Client) -> Result<(), Error> {\n    let document = fetch_index_document(&reqwest_client, &server, StatusCode::OK)?;\n    assert_link_presence(\n        &document,\n        &[],\n        &[\n            ArchiveKind::TarGz.link_text(),\n            ArchiveKind::Tar.link_text(),\n            ArchiveKind::Zip.link_text(),\n        ],\n    );\n\n    Ok(())\n}\n\n/// By default, downloading archives is forbidden.\n#[rstest]\n#[case(ArchiveKind::TarGz)]\n#[case(ArchiveKind::Tar)]\n#[case(ArchiveKind::Zip)]\nfn archives_are_disabled_downloads(\n    #[case] kind: ArchiveKind,\n    server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let (status_code, _) = download_archive_bytes(&reqwest_client, &server, kind)?;\n    assert_eq!(status_code, StatusCode::FORBIDDEN);\n\n    Ok(())\n}\n\n/// When indexing is disabled, archive links are hidden despite enabled archive options.\n#[rstest]\nfn archives_are_disabled_when_indexing_disabled_links(\n    #[with(&[\"--disable-indexing\", \"--enable-tar-gz\", \"--enable-tar\", \"--enable-zip\"])]\n    server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let document = fetch_index_document(&reqwest_client, &server, StatusCode::NOT_FOUND)?;\n    assert_link_presence(\n        &document,\n        &[],\n        &[\n            ArchiveKind::TarGz.link_text(),\n            ArchiveKind::Tar.link_text(),\n            ArchiveKind::Zip.link_text(),\n        ],\n    );\n\n    Ok(())\n}\n\n/// When indexing is disabled, archive downloads are not found despite enabled archive options.\n#[rstest]\n#[case(ArchiveKind::TarGz)]\n#[case(ArchiveKind::Tar)]\n#[case(ArchiveKind::Zip)]\nfn archives_are_disabled_when_indexing_disabled_downloads(\n    #[case] kind: ArchiveKind,\n    #[with(&[\"--disable-indexing\", \"--enable-tar-gz\", \"--enable-tar\", \"--enable-zip\"])]\n    server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let (status_code, _) = download_archive_bytes(&reqwest_client, &server, kind)?;\n    assert_eq!(status_code, StatusCode::NOT_FOUND);\n\n    Ok(())\n}\n\n/// Ensure the link and download to the specified archive is available and others are not\n#[rstest]\n#[case::tar_gz(ArchiveKind::TarGz)]\n#[case::tar(ArchiveKind::Tar)]\n#[case::zip(ArchiveKind::Zip)]\nfn archives_links_and_downloads(\n    #[case] kind: ArchiveKind,\n    #[with(&[kind.server_option()])] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let document = fetch_index_document(&reqwest_client, &server, StatusCode::OK)?;\n\n    let (link_text, other_links, tar_gz_status, tar_status, zip_status) = match kind {\n        ArchiveKind::TarGz => (\n            ArchiveKind::TarGz.link_text(),\n            [ArchiveKind::Tar.link_text(), ArchiveKind::Zip.link_text()],\n            StatusCode::OK,\n            StatusCode::FORBIDDEN,\n            StatusCode::FORBIDDEN,\n        ),\n        ArchiveKind::Tar => (\n            ArchiveKind::Tar.link_text(),\n            [ArchiveKind::TarGz.link_text(), ArchiveKind::Zip.link_text()],\n            StatusCode::FORBIDDEN,\n            StatusCode::OK,\n            StatusCode::FORBIDDEN,\n        ),\n        ArchiveKind::Zip => (\n            ArchiveKind::Zip.link_text(),\n            [ArchiveKind::TarGz.link_text(), ArchiveKind::Tar.link_text()],\n            StatusCode::FORBIDDEN,\n            StatusCode::FORBIDDEN,\n            StatusCode::OK,\n        ),\n    };\n\n    assert_link_presence(&document, &[link_text], &other_links);\n\n    for (kind, expected) in [\n        (ArchiveKind::TarGz, tar_gz_status),\n        (ArchiveKind::Tar, tar_status),\n        (ArchiveKind::Zip, zip_status),\n    ] {\n        let (status, _) = download_archive_bytes(&reqwest_client, &server, kind)?;\n        assert_eq!(status, expected);\n    }\n\n    Ok(())\n}\n\nenum ExpectedLen {\n    /// Exact byte length expected.\n    Exact(usize),\n    /// Minimum byte length expected.\n    Min(usize),\n}\n\n/// Broken symlinks (from [`fixtures::BROKEN_SYMLINK`]) yield different archive behaviors:\n/// - tar_gz: a file with only partial header fields. See \"rfc1952 § 2.3.1. Member header and trailer\".\n/// - tar: a tarball containing a subset of files.\n/// - zip: an empty file.\n#[rstest]\n#[case::tar_gz(ArchiveKind::TarGz, ExpectedLen::Exact(10))]\n#[case::tar(ArchiveKind::Tar, ExpectedLen::Min(512 + 512 + 2 * 512))]\n#[case::zip(ArchiveKind::Zip, ExpectedLen::Exact(0))]\nfn archive_behave_differently_with_broken_symlinks(\n    #[case] kind: ArchiveKind,\n    #[case] expected: ExpectedLen,\n    #[with(&[ArchiveKind::TarGz.server_option(), ArchiveKind::Tar.server_option(), ArchiveKind::Zip.server_option()])]\n    server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let (status_code, byte_len) = download_archive_bytes(&reqwest_client, &server, kind)?;\n    assert_eq!(status_code, StatusCode::OK);\n\n    match expected {\n        ExpectedLen::Exact(len) => assert_eq!(byte_len, len),\n        ExpectedLen::Min(len) => assert!(byte_len >= len),\n    }\n\n    Ok(())\n}\n\n/// ZIP archives store entry names using unix-style paths (no backslashes).\n/// The \"someDir\" dir is constructed by [`fixtures`] and all items in it can be correctly processed.\n#[rstest]\nfn zip_archives_store_entry_name_in_unix_style(\n    #[with(&[\"--enable-zip\"])] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let resp = reqwest_client\n        .get(server.url().join(\"someDir/?download=zip\")?)\n        .send()?\n        .error_for_status()?;\n\n    assert_eq!(resp.status(), StatusCode::OK);\n\n    let mut archive = ZipArchive::new(Cursor::new(resp.bytes()?))?;\n    for i in 0..archive.len() {\n        let entry = archive.by_index(i)?;\n        let name = entry.name();\n\n        assert!(\n            !name.contains(r\"\\\"),\n            \"ZIP entry '{}' contains a backslash\",\n            name\n        );\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "tests/auth.rs",
    "content": "use pretty_assertions::assert_eq;\nuse reqwest::{StatusCode, blocking::Client};\nuse rstest::rstest;\nuse select::{document::Document, predicate::Text};\n\nmod fixtures;\n\nuse crate::fixtures::{Error, FILES, TestServer, reqwest_client, server};\n\n#[rstest]\n#[case(\"testuser:testpassword\", \"testuser\", \"testpassword\")]\n#[case(\n    \"testuser:sha256:9f735e0df9a1ddc702bf0a1a7b83033f9f7153a00c29de82cedadc9957289b05\",\n    \"testuser\",\n    \"testpassword\"\n)]\n#[case(\n    \"testuser:sha512:e9e633097ab9ceb3e48ec3f70ee2beba41d05d5420efee5da85f97d97005727587fda33ef4ff2322088f4c79e8133cc9cd9f3512f4d3a303cbdb5bc585415a00\",\n    \"testuser\",\n    \"testpassword\"\n)]\nfn auth_accepts(\n    #[case] _cli_auth_arg: &str,\n    #[case] client_username: &str,\n    #[case] client_password: &str,\n    reqwest_client: Client,\n    #[with(&[\"-a\", _cli_auth_arg])] server: TestServer,\n) -> Result<(), Error> {\n    let response = reqwest_client\n        .get(server.url())\n        .basic_auth(client_username, Some(client_password))\n        .send()?;\n\n    let status_code = response.status();\n    assert_eq!(status_code, StatusCode::OK);\n\n    let body = response.error_for_status()?;\n    let parsed = Document::from_read(body)?;\n    for &file in FILES {\n        assert!(parsed.find(Text).any(|x| x.text() == file));\n    }\n\n    Ok(())\n}\n\n#[rstest]\n#[case(\"rightuser:rightpassword\", \"wronguser\", \"rightpassword\")]\n#[case(\n    \"rightuser:sha256:314eee236177a721d0e58d3ca4ff01795cdcad1e8478ba8183a2e58d69c648c0\",\n    \"wronguser\",\n    \"rightpassword\"\n)]\n#[case(\n    \"rightuser:sha512:84ec4056571afeec9f5b59453305877e9a66c3f9a1d91733fde759b370c1d540b9dc58bfc88c5980ad2d020c3a8ee84f21314a180856f5a82ba29ecba29e2cab\",\n    \"wronguser\",\n    \"rightpassword\"\n)]\n#[case(\"rightuser:rightpassword\", \"rightuser\", \"wrongpassword\")]\n#[case(\n    \"rightuser:sha256:314eee236177a721d0e58d3ca4ff01795cdcad1e8478ba8183a2e58d69c648c0\",\n    \"rightuser\",\n    \"wrongpassword\"\n)]\n#[case(\n    \"rightuser:sha512:84ec4056571afeec9f5b59453305877e9a66c3f9a1d91733fde759b370c1d540b9dc58bfc88c5980ad2d020c3a8ee84f21314a180856f5a82ba29ecba29e2cab\",\n    \"rightuser\",\n    \"wrongpassword\"\n)]\nfn auth_rejects(\n    #[case] _cli_auth_arg: &str,\n    #[case] client_username: &str,\n    #[case] client_password: &str,\n    #[with(&[\"-a\", _cli_auth_arg])] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let status = reqwest_client\n        .get(server.url())\n        .basic_auth(client_username, Some(client_password))\n        .send()?\n        .status();\n\n    assert_eq!(status, StatusCode::UNAUTHORIZED);\n\n    Ok(())\n}\n\n/// Command line arguments that register multiple accounts\nstatic ACCOUNTS: &[&str] = &[\n    \"--auth\",\n    \"usr0:pwd0\",\n    \"--auth\",\n    \"usr1:pwd1\",\n    \"--auth\",\n    \"usr2:sha256:149d2937d1bce53fa683ae652291bd54cc8754444216a9e278b45776b76375af\", // pwd2\n    \"--auth\",\n    \"usr3:sha256:ffc169417b4146cebe09a3e9ffbca33db82e3e593b4d04c0959a89c05b87e15d\", // pwd3\n    \"--auth\",\n    \"usr4:sha512:68050a967d061ac480b414bc8f9a6d368ad0082203edcd23860e94c36178aad1a038e061716707d5479e23081a6d920dc6e9f88e5eb789cdd23e211d718d161a\", // pwd4\n    \"--auth\",\n    \"usr5:sha512:be82a7dccd06122f9e232e9730e67e69e30ec61b268fd9b21a5e5d42db770d45586a1ce47816649a0107e9fadf079d9cf0104f0a3aaa0f67bad80289c3ba25a8\",\n    // pwd5\n];\n\n#[rstest]\n#[case(\"usr0\", \"pwd0\")]\n#[case(\"usr1\", \"pwd1\")]\n#[case(\"usr2\", \"pwd2\")]\n#[case(\"usr3\", \"pwd3\")]\n#[case(\"usr4\", \"pwd4\")]\n#[case(\"usr5\", \"pwd5\")]\nfn auth_multiple_accounts_pass(\n    #[case] username: &str,\n    #[case] password: &str,\n    #[with(ACCOUNTS)] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let response = reqwest_client\n        .get(server.url())\n        .basic_auth(username, Some(password))\n        .send()?;\n\n    let status = response.status();\n    assert_eq!(status, StatusCode::OK);\n\n    let body = response.error_for_status()?;\n    let parsed = Document::from_read(body)?;\n    for &file in FILES {\n        assert!(parsed.find(Text).any(|x| x.text() == file));\n    }\n\n    Ok(())\n}\n\n#[rstest]\nfn auth_multiple_accounts_wrong_username(\n    #[with(ACCOUNTS)] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let status = reqwest_client\n        .get(server.url())\n        .basic_auth(\"unregistered user\", Some(\"pwd0\"))\n        .send()?\n        .status();\n\n    assert_eq!(status, StatusCode::UNAUTHORIZED);\n\n    Ok(())\n}\n\n#[rstest]\n#[case(\"usr0\", \"pwd5\")]\n#[case(\"usr1\", \"pwd4\")]\n#[case(\"usr2\", \"pwd3\")]\n#[case(\"usr3\", \"pwd2\")]\n#[case(\"usr4\", \"pwd1\")]\n#[case(\"usr5\", \"pwd0\")]\nfn auth_multiple_accounts_wrong_password(\n    #[case] username: &str,\n    #[case] password: &str,\n    #[with(ACCOUNTS)] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let status = reqwest_client\n        .get(server.url())\n        .basic_auth(username, Some(password))\n        .send()?\n        .status();\n\n    assert_eq!(status, StatusCode::UNAUTHORIZED);\n\n    Ok(())\n}\n"
  },
  {
    "path": "tests/auth_file.rs",
    "content": "use reqwest::{StatusCode, blocking::Client};\nuse rstest::rstest;\nuse select::{document::Document, predicate::Text};\n\nmod fixtures;\n\nuse crate::fixtures::{Error, FILES, TestServer, reqwest_client, server};\n\n#[rstest]\n#[case(\"joe\", \"123\")]\n#[case(\"bob\", \"123\")]\n#[case(\"bill\", \"\")]\nfn auth_file_accepts(\n    #[with(&[\"--auth-file\", \"tests/data/auth1.txt\"])] server: TestServer,\n    reqwest_client: Client,\n    #[case] client_username: &str,\n    #[case] client_password: &str,\n) -> Result<(), Error> {\n    let response = reqwest_client\n        .get(server.url())\n        .basic_auth(client_username, Some(client_password))\n        .send()?;\n\n    let status_code = response.status();\n    assert_eq!(status_code, StatusCode::OK);\n\n    let body = response.error_for_status()?;\n    let parsed = Document::from_read(body)?;\n    for &file in FILES {\n        assert!(parsed.find(Text).any(|x| x.text() == file));\n    }\n\n    Ok(())\n}\n\n#[rstest]\n#[case(\"joe\", \"wrongpassword\")]\n#[case(\"bob\", \"\")]\n#[case(\"nonexistentuser\", \"wrongpassword\")]\nfn auth_file_rejects(\n    #[with(&[\"--auth-file\", \"tests/data/auth1.txt\"])] server: TestServer,\n    reqwest_client: Client,\n    #[case] client_username: &str,\n    #[case] client_password: &str,\n) -> Result<(), Error> {\n    let status = reqwest_client\n        .get(server.url())\n        .basic_auth(client_username, Some(client_password))\n        .send()?\n        .status();\n\n    assert_eq!(status, StatusCode::UNAUTHORIZED);\n\n    Ok(())\n}\n"
  },
  {
    "path": "tests/bind.rs",
    "content": "use std::io::{BufRead, BufReader};\nuse std::process::{Command, Stdio};\n\nuse assert_cmd::{cargo, prelude::*};\nuse assert_fs::fixture::TempDir;\nuse regex::Regex;\nuse reqwest::blocking::Client;\nuse rstest::rstest;\n\nmod fixtures;\n\nuse crate::fixtures::{Error, TestServer, port, reqwest_client, server, tmpdir};\n\n#[rstest]\n#[case(&[\"-i\", \"12.123.234.12\"])]\n#[case(&[\"-i\", \"::\", \"-i\", \"12.123.234.12\"])]\nfn bind_fails(tmpdir: TempDir, port: u16, #[case] args: &[&str]) -> Result<(), Error> {\n    Command::new(cargo::cargo_bin!(\"miniserve\"))\n        .arg(tmpdir.path())\n        .arg(\"-p\")\n        .arg(port.to_string())\n        .args(args)\n        .assert()\n        .stderr(predicates::str::contains(\"Failed to bind server to\"))\n        .failure();\n\n    Ok(())\n}\n\n#[rstest]\n#[case(server(&[] as &[&str]), true, true)]\n#[case(server(&[\"-i\", \"::\"]), false, true)]\n#[case(server(&[\"-i\", \"0.0.0.0\"]), true, false)]\n#[case(server(&[\"-i\", \"::\", \"-i\", \"0.0.0.0\"]), true, true)]\nfn bind_ipv4_ipv6(\n    #[case] server: TestServer,\n    reqwest_client: Client,\n    #[case] bind_ipv4: bool,\n    #[case] bind_ipv6: bool,\n) -> Result<(), Error> {\n    assert_eq!(\n        reqwest_client\n            .get(format!(\"http://127.0.0.1:{}\", server.port()).as_str())\n            .send()\n            .is_ok(),\n        bind_ipv4\n    );\n    assert_eq!(\n        reqwest_client\n            .get(format!(\"http://[::1]:{}\", server.port()).as_str())\n            .send()\n            .is_ok(),\n        bind_ipv6\n    );\n\n    Ok(())\n}\n\n#[rstest]\n#[case(&[] as &[&str])]\n#[case(&[\"-i\", \"::\"])]\n#[case(&[\"-i\", \"127.0.0.1\"])]\n#[case(&[\"-i\", \"0.0.0.0\"])]\n#[case(&[\"-i\", \"::\", \"-i\", \"0.0.0.0\"])]\n#[case(&[\"--random-route\"])]\n#[case(&[\"--route-prefix\", \"/prefix\"])]\nfn validate_printed_urls(\n    reqwest_client: Client,\n    tmpdir: TempDir,\n    port: u16,\n    #[case] args: &[&str],\n) -> Result<(), Error> {\n    let mut child = Command::new(cargo::cargo_bin!(\"miniserve\"))\n        .arg(tmpdir.path())\n        .arg(\"-p\")\n        .arg(port.to_string())\n        .args(args)\n        .stdout(Stdio::piped())\n        .spawn()?;\n\n    // WARN assumes urls list is terminated by an empty line\n    let url_lines = BufReader::new(child.stdout.take().unwrap())\n        .lines()\n        .map(|line| line.expect(\"Error reading stdout\"))\n        .take_while(|line| !line.is_empty()) /* non-empty lines */\n        .collect::<Vec<_>>();\n    let url_lines = url_lines.join(\"\\n\");\n\n    let urls = Regex::new(r\"http://[a-zA-Z0-9\\.\\[\\]:/]+\")\n        .unwrap()\n        .captures_iter(url_lines.as_str())\n        .map(|caps| caps.get(0).unwrap().as_str())\n        .collect::<Vec<_>>();\n\n    assert!(!urls.is_empty());\n\n    for url in urls {\n        reqwest_client.get(url).send()?.error_for_status()?;\n    }\n\n    child.kill()?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "tests/cli.rs",
    "content": "use std::process::Command;\n\nuse assert_cmd::{cargo, prelude::*};\nuse clap::{ValueEnum, crate_name, crate_version};\nuse clap_complete::Shell;\n\nmod fixtures;\n\nuse crate::fixtures::Error;\n\n#[test]\n/// Show help and exit.\nfn help_shows() -> Result<(), Error> {\n    Command::new(cargo::cargo_bin!(\"miniserve\"))\n        .arg(\"-h\")\n        .assert()\n        .success();\n\n    Ok(())\n}\n\n#[test]\n/// Show version and exit.\nfn version_shows() -> Result<(), Error> {\n    Command::new(cargo::cargo_bin!(\"miniserve\"))\n        .arg(\"-V\")\n        .assert()\n        .success()\n        .stdout(format!(\"{} {}\\n\", crate_name!(), crate_version!()));\n\n    Ok(())\n}\n\n#[test]\n/// Print completions and exit.\nfn print_completions() -> Result<(), Error> {\n    for shell in Shell::value_variants() {\n        Command::new(cargo::cargo_bin!(\"miniserve\"))\n            .arg(\"--print-completions\")\n            .arg(shell.to_string())\n            .assert()\n            .success();\n    }\n\n    Ok(())\n}\n\n#[test]\n/// Print completions rejects invalid shells.\nfn print_completions_invalid_shell() -> Result<(), Error> {\n    Command::new(cargo::cargo_bin!(\"miniserve\"))\n        .arg(\"--print-completions\")\n        .arg(\"fakeshell\")\n        .assert()\n        .failure();\n\n    Ok(())\n}\n"
  },
  {
    "path": "tests/create_directories.rs",
    "content": "use reqwest::blocking::{Client, multipart};\nuse rstest::rstest;\nuse select::{\n    document::Document,\n    predicate::{Attr, Text},\n};\n\nmod fixtures;\n\nuse crate::fixtures::{DIRECTORY_SYMLINK, Error, TestServer, reqwest_client, server};\n\n/// This should work because the flags for uploading files and creating directories\n/// are set, and the directory name and path are valid.\n#[rstest]\nfn creating_directories_works(\n    #[with(&[\"--upload-files\", \"--mkdir\"])] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let test_directory_name = \"hello\";\n\n    // Before creating, check whether the directory does not yet exist.\n    let body = reqwest_client\n        .get(server.url())\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n    assert!(parsed.find(Text).all(|x| x.text() != test_directory_name));\n\n    // Perform the actual creation.\n    let create_action = parsed\n        .find(Attr(\"id\", \"mkdir\"))\n        .next()\n        .expect(\"Couldn't find element with id=mkdir\")\n        .attr(\"action\")\n        .expect(\"Directory form doesn't have action attribute\");\n    let form = multipart::Form::new();\n    let part = multipart::Part::text(test_directory_name);\n    let form = form.part(\"mkdir\", part);\n\n    reqwest_client\n        .post(server.url().join(create_action)?)\n        .multipart(form)\n        .send()?\n        .error_for_status()?;\n\n    // After creating, check whether the directory is now getting listed.\n    let body = reqwest_client.get(server.url()).send()?;\n    let parsed = Document::from_read(body)?;\n    assert!(\n        parsed\n            .find(Text)\n            .any(|x| x.text() == test_directory_name.to_owned() + \"/\")\n    );\n\n    Ok(())\n}\n\n/// This should fail because the server does not allow for creating directories\n/// as the flags for uploading files and creating directories are not set.\n/// The directory name and path are valid.\n#[rstest]\nfn creating_directories_is_prevented(\n    server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let test_directory_name = \"hello\";\n\n    // Before creating, check whether the directory does not yet exist.\n    let body = reqwest_client\n        .get(server.url())\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n    assert!(parsed.find(Text).all(|x| x.text() != test_directory_name));\n\n    // Ensure the directory creation form is not present\n    assert!(parsed.find(Attr(\"id\", \"mkdir\")).next().is_none());\n\n    // Then try to create anyway\n    let form = multipart::Form::new();\n    let part = multipart::Part::text(test_directory_name);\n    let form = form.part(\"mkdir\", part);\n\n    // This should fail\n    assert!(\n        reqwest_client\n            .post(server.url().join(\"/upload?path=/\")?)\n            .multipart(form)\n            .send()?\n            .error_for_status()\n            .is_err()\n    );\n\n    // After creating, check whether the directory is now getting listed (shouldn't).\n    let body = reqwest_client.get(server.url()).send()?;\n    let parsed = Document::from_read(body)?;\n    assert!(\n        parsed\n            .find(Text)\n            .all(|x| x.text() != test_directory_name.to_owned() + \"/\")\n    );\n\n    Ok(())\n}\n\n/// This should fail because directory creation through symlinks should not be possible\n/// when the the no symlinks flag is set.\n#[rstest]\nfn creating_directories_through_symlinks_is_prevented(\n    #[with(&[\"--upload-files\", \"--mkdir\", \"--no-symlinks\"])] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    // Before attempting to create, ensure the symlink does not exist.\n    let body = reqwest_client\n        .get(server.url())\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n    assert!(parsed.find(Text).all(|x| x.text() != DIRECTORY_SYMLINK));\n\n    // Attempt to perform directory creation.\n    let form = multipart::Form::new();\n    let part = multipart::Part::text(DIRECTORY_SYMLINK);\n    let form = form.part(\"mkdir\", part);\n\n    // This should fail\n    assert!(\n        reqwest_client\n            .post(\n                server\n                    .url()\n                    .join(format!(\"/upload?path=/{DIRECTORY_SYMLINK}\").as_str())?\n            )\n            .multipart(form)\n            .send()?\n            .error_for_status()\n            .is_err()\n    );\n\n    Ok(())\n}\n\n/// Test for path traversal vulnerability (CWE-22) in both path parameter of query string and in\n/// mkdir name (Content-Disposition)\n///\n/// see: https://github.com/svenstaro/miniserve/issues/518\n#[rstest]\n#[case(\"foo\", \"bar\", \"foo/bar\")] // Not CWE-22, but `foo` isn't a directory\n#[case(\"/../foo\", \"bar\", \"foo/bar\")]\n#[case(\"/foo\", \"/../bar\", \"foo/bar\")]\n#[case(\"C:/foo\", \"C:/bar\", if cfg!(windows) { \"foo/bar\" } else { \"C:/foo/C:/bar\" })]\n#[case(r\"C:\\foo\", r\"C:\\bar\", if cfg!(windows) { \"foo/bar\" } else { r\"C:\\foo/C:\\bar\" })]\n#[case(r\"\\foo\", r\"\\..\\bar\", if cfg!(windows) { \"foo/bar\" } else { r\"\\foo/\\..\\bar\" })]\nfn prevent_path_transversal_attacks(\n    #[with(&[\"--upload-files\", \"--mkdir\"])] server: TestServer,\n    reqwest_client: Client,\n    #[case] path: &str,\n    #[case] dir_name: &'static str,\n    #[case] expected: &str,\n) -> Result<(), Error> {\n    let expected_path = server.path().join(expected);\n    assert!(!expected_path.exists());\n\n    let form = multipart::Form::new();\n    let part = multipart::Part::text(dir_name);\n    let form = form.part(\"mkdir\", part);\n\n    // This should fail\n    assert!(\n        reqwest_client\n            .post(server.url().join(&format!(\"/upload/path={path}\"))?)\n            .multipart(form)\n            .send()?\n            .error_for_status()\n            .is_err()\n    );\n\n    Ok(())\n}\n"
  },
  {
    "path": "tests/data/auth1.txt",
    "content": "joe:123\nbob:sha256:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3\nbill:\n"
  },
  {
    "path": "tests/data/cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFCTCCAvGgAwIBAgIUJUf2QS/pOdHEW4EHTfdXxeTvtM8wDQYJKoZIhvcNAQEL\nBQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIxMDgyNzAwMzEyOFoXDTMxMDgy\nNTAwMzEyOFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF\nAAOCAg8AMIICCgKCAgEAwmYOqToI0R30lPyYtF9bSuhIOCp9cp0jl2nuHaO8mpr1\ngMiJKKN4HjAdgac+3hYkTRFqK2mKKpV9QdVKR24Ib7mC45Ek7BlLw3VbxPRKrK/j\nrKW3M3ui+453B24yf6K8dH36x9gZo4glzghFxuodFakIX2zNKo6tEx0XVkbhsu/w\nvj2s+0L3oToPAYZaiOB/7xYU6Yu9n7Tn6rE9/orDfK1DlrZDP3hzyxLzuf6tqXCh\n66cgaPQTh+xyyWZcvl60kbB4H3bdhqbYGMMQO8bUxXTQXjwvUsvl0yn9qCpMIn99\nPm9xhfDQSF3zawM3CQ/lmn9uFQzdOEfYlO6oaidTqxLtBhVUcEutIcmoW9nmmv2g\nEi49/3OmvWQcEdMWt8xwxSrMvKDSeUdF3rbalTHBFQHJlJiKRX9wTNtSZ5T8FTU7\n4Ip4EzAtP8wY5NDv253mddANoyKsVRGytS35LDFkCS/TxuVDZrjluc86yqUId/jf\nHZAzQ7ifpC890aG0JOq/0mmVDvbn7MzdTsTWwhE8UaOiFljTiNQX3QjX3TaEu32M\nXHKo5nebNqDVRGnFMFmfXw2ZP8lgQCWk1HxLr0qhRxIy8XmIK1ZUz7Uc4Cba73XB\npSxcIPytpDuuKotslBjoIYu9DY07n1Hu4zYPvpP9DnaunEW6zmANEtjSyrE/TQ0C\nAwEAAaNTMFEwHQYDVR0OBBYEFH0VzGnFqGVB+11uyvqea2qXYxQKMB8GA1UdIwQY\nMBaAFH0VzGnFqGVB+11uyvqea2qXYxQKMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI\nhvcNAQELBQADggIBAA4FqzX34FCU1WYzmBRcq7QHSrc7LcuTbxhESB7mYbI8IPFt\ncgrtXL1mTP+nz5nN+E6fyA8Y9zIyXm/6svYpJzXgUTtbdgDW22v5iN+YZvOaQ3Jt\n/0eEtkx7wdNjLsN0aM6OjPXDw0mAVFDdevE7wgnra6x6/VHOt6pksNJa76ZVPX5X\ndlLj+OU4eQPPMVxhL7p3xdSPFDZzXY7mNfVycO3tK5Fzrwko7OQKqEBMtc0oZxLd\nm/FvqcJveHYHfXZl5XKMcsCNO8bG0XXDhwg0CLTf1p0hmp1oLieqplekOWs54Alo\nFF4EBNdDaIFdQ4FAYaAU+9KLoPstorTl+3Owj/k3xhDB+0sGwGeX/e88nhs/ppEy\nbxOt0j4AruwapkcvkwhQeMpQJRYyOrcvlbUEZqFABozZ9gbGRQvnConDNg7tz5zc\nnVUupszA7zs0Vn9b1zVLOcOcS2ziQvoCyh687MsVbjw65Y6tkhvLI35G68zrFKsl\nMS5mqnK4DZYFc1gGGI/rjsFUf3dD4ww6PTnwv3Ga2yBvXi7EckEeEqB+dRlVdvob\ncH/grVUum3s5Y4PTnxyNAUFZlFNZ8jlOcgXtAFuTnJ/jcvboZdE7Oja2OIMJo53d\nrbkqAPNGhQ98QDuTwWjHUq/Th1CQK4ALI/wqoc22TJpSh/mme5Dj4HhB7LWl\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/data/cert_ec.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBujCCAUCgAwIBAgIUd5MqZqnOPFxMKaYipL6S6B3D3cswCgYIKoZIzj0EAwIw\nFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDQxNjEzNDUxNVoXDTMzMDQxMzEz\nNDUxNVowFDESMBAGA1UEAwwJbG9jYWxob3N0MHYwEAYHKoZIzj0CAQYFK4EEACID\nYgAEPcNbYA4vrYfVtR1EZgRZ9/iRvRXtpx9ww3avV9echQj80xZ3KpCTA0T1gQ35\n0pCHpd9gMTnDZdHqWfSpBYrFBll26q8rRvSwSTqRQv5YdgrI/yAgZb+CXmjxAZWf\nGoHso1MwUTAdBgNVHQ4EFgQU301zfi7+pUOUzs1ObVTwxNHM908wHwYDVR0jBBgw\nFoAU301zfi7+pUOUzs1ObVTwxNHM908wDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjO\nPQQDAgNoADBlAjEAxoOsn/1Kj1sfc3DhoA/UGetA+porz5dSHJOuuHVl5jS5nuAO\nIrQmVvINVuVV3oQyAjBd8HoemembnGCz78Y40v7vLbd2yqWZt8sKX90f/Mj2hGsn\nKjraTX2m52X4W8KLhMU=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/data/cert_rsa.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFCTCCAvGgAwIBAgIUTUIU8j6S7RXbFFhW/yFftSQvUCYwDQYJKoZIhvcNAQEL\nBQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDQxNjEzNDUxNVoXDTMzMDQx\nMzEzNDUxNVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF\nAAOCAg8AMIICCgKCAgEArVsX/kqzqJAvuTiXiJZUAyYy3rxo1krwBY4KejzKw3JY\nUFP63utA8EgGwXjnABua8bA1WKbl+MZwSOz2QOytuR6kkqwKWWdFeCzhOZDAAudd\nLTqkkAxPX56Iv7y/v0emYfc0XNYjSaoq6yhvELVfeWabZ+WuZF9fisJ+Up1NvnLQ\n6K5c+NB++gVHS+URgy0Z4jIaDnm5ofETDZSS9cjDLBMKK9X0bC+JFIXa9x5pQU2u\nNTnYWtvRuaqAtt0fozT3hqzjAdBf0jhmKOySy/tK0W9n2FHJrqlo68gN0uP1PhQR\n9EUCB4ZkElOXpBdoDub4iKVVjtdpBfppOuq+I2hSxtosP+9N0vBSBPrmDPGpdUP+\nlhxWEo1wKVW1AhPcOSDhKQM50SLxTk6ZmSKd0Z/obgjttVxqNlHw9GUoXZbztwZb\nVPDVVEGJ6OZYSm2KENb1VxhLg/6dS/WXN7J240At9yj8PDEEaIa5YTJhGa2j4fDi\nqsGiEx4AQEgliDUPhXhzmROMBdvynITU55au7bnadxPiGPOWj//qZL0URhCNDl2d\nHabm7mlgEgaP90R+il5QTe9R+Lg4bTR4f01lXVdB/GE7oent5m82zqpV1TDnCM0Q\nUneD6nfZjt9oqtTxr/ldcDFFtQhDD0SMtX82+wll+tzVU6lAUCgRC7+4Ob2CEVcC\nAwEAAaNTMFEwHQYDVR0OBBYEFExLPelY6s7HpSVKWjUL6QFbQUeKMB8GA1UdIwQY\nMBaAFExLPelY6s7HpSVKWjUL6QFbQUeKMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI\nhvcNAQELBQADggIBAFzYxVXZTtsT8QAn6lCTcQ59sFRuhPKeuINHlcQRVsYDBukD\n/ErkamNNycXrpRvQaYj8/cmk+mPaoleOf0e0iTlOs/IIzWKNy2drlmBuRi8FU5Gg\nlsWt4FvEG3RRsDfTo7gjrqKpwnfkGxx7RZ74ZWg5pVXrzFV7N2XoA1Ln49+2oMJV\niZYYSdWdgGxHgivUfEhjliG2CDw6Z5NlEtbu/dXA4sBrSMtP2uW04F/8N61NEhSf\nKNMQ2/qXkYHyYcwtsnxAjmgpQJK/yR95vLyTnuC5IT33icvXNLKCIa5gnyok2Lxj\nqIx1SCPp7o6onB3otKkrniZEBpTqW4y5ELW1SHLGkSXqm3cwbxK0Fm2bphF0mhIJ\nair31ZiENy1ICYGDPv/EetG/bkzy2Cqc9pa+/N5N/9nKh6C9Wi/OsGCn72DvW9OV\n6GW/W3HQwiZdTuLYvk0dpsfuFZvEaZrKsvLwkJGA1cS1IHv2V+mPxaHAMPPd+SyA\nnaZj/1vBWxHAenQoUBRxW2qpK708ITJ/PGodi6l0Kv6NSMlSq+JB/gU/fmBJXgL8\nNmc7pn5UJX74CSaBHnxjEfa3F6RPz00nNUStNl0ZjVZW3kKwQfbsCTiSXIvS/whM\niQxO+l9Ae7ebWcQ4Qd38ZCMjA6e5CUaCj3oJA4VhNh4zNva8fkQ4mfKWleVv\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/data/generate_tls_certs.sh",
    "content": "#!/usr/bin/env bash\nopenssl req -subj '/CN=localhost' -x509 -newkey rsa:4096 -keyout key_pkcs8.pem -out cert_rsa.pem -nodes -days 3650\nopenssl rsa -in key_pkcs8.pem -out key_pkcs1.pem\nopenssl req -subj '/CN=localhost' -x509 -nodes -newkey ec -pkeyopt ec_paramgen_curve:secp384r1 -keyout key_ec.pem -out cert_ec.pem -days 3650\n"
  },
  {
    "path": "tests/data/key_ec.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBZiwh8yRBpDgAx+Oa4\nqgvw0OOBiDnOHgY9+WuIA74dfGbPQDPwAVIVXfUX0a8YHa6hZANiAAQ9w1tgDi+t\nh9W1HURmBFn3+JG9Fe2nH3DDdq9X15yFCPzTFncqkJMDRPWBDfnSkIel32AxOcNl\n0epZ9KkFisUGWXbqrytG9LBJOpFC/lh2Csj/ICBlv4JeaPEBlZ8agew=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "tests/data/key_pkcs1.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCtWxf+SrOokC+5\nOJeIllQDJjLevGjWSvAFjgp6PMrDclhQU/re60DwSAbBeOcAG5rxsDVYpuX4xnBI\n7PZA7K25HqSSrApZZ0V4LOE5kMAC510tOqSQDE9fnoi/vL+/R6Zh9zRc1iNJqirr\nKG8QtV95Zptn5a5kX1+Kwn5SnU2+ctDorlz40H76BUdL5RGDLRniMhoOebmh8RMN\nlJL1yMMsEwor1fRsL4kUhdr3HmlBTa41Odha29G5qoC23R+jNPeGrOMB0F/SOGYo\n7JLL+0rRb2fYUcmuqWjryA3S4/U+FBH0RQIHhmQSU5ekF2gO5viIpVWO12kF+mk6\n6r4jaFLG2iw/703S8FIE+uYM8al1Q/6WHFYSjXApVbUCE9w5IOEpAznRIvFOTpmZ\nIp3Rn+huCO21XGo2UfD0ZShdlvO3BltU8NVUQYno5lhKbYoQ1vVXGEuD/p1L9Zc3\nsnbjQC33KPw8MQRohrlhMmEZraPh8OKqwaITHgBASCWINQ+FeHOZE4wF2/KchNTn\nlq7tudp3E+IY85aP/+pkvRRGEI0OXZ0dpubuaWASBo/3RH6KXlBN71H4uDhtNHh/\nTWVdV0H8YTuh6e3mbzbOqlXVMOcIzRBSd4Pqd9mO32iq1PGv+V1wMUW1CEMPRIy1\nfzb7CWX63NVTqUBQKBELv7g5vYIRVwIDAQABAoICAByL65+MXZlcZP9zOkDbwGnk\nWGwlSn4/SNchVMhcSmd05OYVbjJXOxJWSgaCCkgSQ6mZAq/ei/AzfToFC2gVkWXy\njdc5TVr7jo0DlvMLyxKvVsCj74VpAYkVah9ozYqKGfP36T+AY781rmua9O8jbt1m\n8CBjyhvtOKZ48KRaEvtRnOU0EUtHyiERzXPJ/OBFBQYiiffoQ5FPSXvrA2hF7x3K\n5NnjGaTXDxO6FxyqfVqrmAxbwiz0Fc0lLpzuPM97YWdkAN3DmoPblbcXffTpJKDo\nX4lXroZ8jzKEdwJLV48pbutykar7jm8WJNp4oEIT9slJsJUdE8ZQPhPdpAHgpACl\nbZZsPU5n6LVFaeudm100889eIM36WVu2gYO0xewVunMRLhnqPBjlNNA45RIj+XLo\nCSHSkAP+wzrr5tEScIZKAEArQt0WjZq7/+UIAaE0niwtbQivpwBvlmDV+4EygUSU\n+PQmDDcQysinQtD9gOFivZ5Kg784i+zwssqNleTzHeg7+CN5eZ+MRHl3S/STF5cI\nKph23Ml5r/PApke8O7qeGnEHi1NhtGUmofdhAD9PJ1Z/VS52E61Re9cY87Vo0pM+\n+KEi63XDZ+tpYPuL6z3AJ9xTVHxVmqPK7nKvKeO8HXKVbBJRPY6fH85ALDUs+aqq\nX438ACeNRbNuwrX9PPjBAoIBAQDbDiQNcy1uUestQHNYCFhMsw6krCwwiySPyvYJ\n/WhiQu9L7PZsjCxcSl/rrZUZyNFm9fKCYXNHV8CHZ1l3hCBjpCbXkWr6Ui0wE8Ye\nlOBkTiB33FU5LE1TFlMwJ4UuE6npSls7PuIgjfRzfhu+U+3kPR1FgIFllzE6vj59\nzYwB+Ord2WnKlI4Zc2AZyNtaTihmXiV9xSsAAxWibRIj9sNpDfjRwBHvbT9rJEer\n2SokCBLkECjq2HH51UN8lSeHxK7Mk61FKbfJSFGKzXJb8p42x2xQxLgIRcWCJ98E\ncOqFBv5zL111IKkrIZS2dUp3KpmP+gL6wItzJQ5b4rL9H88DAoIBAQDKl9mb7iFN\n1ai+rJjaLOgzzr9nVt900kcBkV8y0nXCEZY83udrSw9RcE30NBomlXxWJ1lxDDZC\nye8AD6cbCIaij3d02jNx2iVgPUf2vxN0YDhwZMpFiy5M8fxRDnxKmIVF03EE8c9X\ng3GhlGvN3k6sdPJwgb3VwAh9kGQVwK5KMetFcJUypWK6Ot6xViJWJopFe5YPBxdr\neCoVq7+JELjJ7j1EEGYhVMVw7t1CsGANVrV6L3TTNQNESdaN8pjz7PsvCX62TlGX\nwPjSklO5Ru1CFV1w7YJHj2OD6b+JOw6QNLIyQi9oP9EExVTIIQRr7QN1OZ9aGHL6\nEDgc53EecIodAoIBADxPvGVnnM6PB21CHX/TbFxRwGpebRxAcySUAQHnH2JOg4wo\nBgEE5wHSCG7fL/oVbHIorUhwhEjURFIDhoJ9gl1syLT5eLbLAV4HU7j/zHhRemcF\n5wECzZdewjCz8Nsq1tFAg7XgLmpAK1nREtpoSUtZ+EE2jGnoIsnFr3b7rNyuKBxE\ny/fWxvkC5yayQpKuijkFGtVx/9DVCJPb6+6y9kJqcmNtuoJtVdSt/H24IP4iqvDX\n8iwWw+rBaP9YIbYj1OzGjCJKxitJGgpZXm8qcZ0rcwsZ3oGIlEStrZ2PaUKPFmeo\nVtb00x7o9AT4bjQ5KmaVs1ROxxZA0Z9C330J0PkCggEAd1OTZ6WN1jN3bb9pVHBI\n4GLxF+PyP/OuwPyn7t5JX+JN9FJySh7uyc/1ClY55On9Tx1kMBK6TwJzlDyj92dB\nLbSE7r2quW98vj+6CFqpEc2u0Hx9KxL8VXPeYru+d414ShVtJzVqI6iXIE20ZZCA\nFFHZjmzMrH6sQZDvcmSIA8l9Quw55JfHG9ua2SbbmJSgsqZFT1qk77baSuNbMFc6\nEC4TxehGz3EHzinTBvmtyY193JbhH5nE787x4a+3aUz28dCM4sIkitateBGZ4LIn\nAtpkrCQorQ+G1Oaz2xd+z29KWhHjrGqSKVY1Rp8z5IG4nK4w7rch2an98wBa/0vX\n/QKCAQEArzbuZOzgG3UGgeExE8mdUk6OLNFUzSeFOT57CEDcYB+YQUQwMgOI4+k+\noJb9tnUxvd8+mIcppHeDP0+fHU8Uz9OTZ8xPkGoBJ912K2FLFAel8g6vqW0Eu3ER\npCiz73Ea+0mTZBZZfZPVwL6mGgbsWJWsX2yCzmJ+Noq0bUxu2QE4LBDr7NiE52An\nsLG2ruWKIfJOl8BgRfqmoncgFrc2UztTV8PDV8gmp9oIBq7LqXyH70Knz8Flh1U4\n9A/5hCqd5ovBmHknQUXcp4vnPu4UzQCNcwuvHGj12UcFAywYD1LYXrwmyWQp57lC\nu4ePzLXxbfS9k7lshmbr2ix5s1Ypjg==\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "tests/data/key_pkcs8.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCtWxf+SrOokC+5\nOJeIllQDJjLevGjWSvAFjgp6PMrDclhQU/re60DwSAbBeOcAG5rxsDVYpuX4xnBI\n7PZA7K25HqSSrApZZ0V4LOE5kMAC510tOqSQDE9fnoi/vL+/R6Zh9zRc1iNJqirr\nKG8QtV95Zptn5a5kX1+Kwn5SnU2+ctDorlz40H76BUdL5RGDLRniMhoOebmh8RMN\nlJL1yMMsEwor1fRsL4kUhdr3HmlBTa41Odha29G5qoC23R+jNPeGrOMB0F/SOGYo\n7JLL+0rRb2fYUcmuqWjryA3S4/U+FBH0RQIHhmQSU5ekF2gO5viIpVWO12kF+mk6\n6r4jaFLG2iw/703S8FIE+uYM8al1Q/6WHFYSjXApVbUCE9w5IOEpAznRIvFOTpmZ\nIp3Rn+huCO21XGo2UfD0ZShdlvO3BltU8NVUQYno5lhKbYoQ1vVXGEuD/p1L9Zc3\nsnbjQC33KPw8MQRohrlhMmEZraPh8OKqwaITHgBASCWINQ+FeHOZE4wF2/KchNTn\nlq7tudp3E+IY85aP/+pkvRRGEI0OXZ0dpubuaWASBo/3RH6KXlBN71H4uDhtNHh/\nTWVdV0H8YTuh6e3mbzbOqlXVMOcIzRBSd4Pqd9mO32iq1PGv+V1wMUW1CEMPRIy1\nfzb7CWX63NVTqUBQKBELv7g5vYIRVwIDAQABAoICAByL65+MXZlcZP9zOkDbwGnk\nWGwlSn4/SNchVMhcSmd05OYVbjJXOxJWSgaCCkgSQ6mZAq/ei/AzfToFC2gVkWXy\njdc5TVr7jo0DlvMLyxKvVsCj74VpAYkVah9ozYqKGfP36T+AY781rmua9O8jbt1m\n8CBjyhvtOKZ48KRaEvtRnOU0EUtHyiERzXPJ/OBFBQYiiffoQ5FPSXvrA2hF7x3K\n5NnjGaTXDxO6FxyqfVqrmAxbwiz0Fc0lLpzuPM97YWdkAN3DmoPblbcXffTpJKDo\nX4lXroZ8jzKEdwJLV48pbutykar7jm8WJNp4oEIT9slJsJUdE8ZQPhPdpAHgpACl\nbZZsPU5n6LVFaeudm100889eIM36WVu2gYO0xewVunMRLhnqPBjlNNA45RIj+XLo\nCSHSkAP+wzrr5tEScIZKAEArQt0WjZq7/+UIAaE0niwtbQivpwBvlmDV+4EygUSU\n+PQmDDcQysinQtD9gOFivZ5Kg784i+zwssqNleTzHeg7+CN5eZ+MRHl3S/STF5cI\nKph23Ml5r/PApke8O7qeGnEHi1NhtGUmofdhAD9PJ1Z/VS52E61Re9cY87Vo0pM+\n+KEi63XDZ+tpYPuL6z3AJ9xTVHxVmqPK7nKvKeO8HXKVbBJRPY6fH85ALDUs+aqq\nX438ACeNRbNuwrX9PPjBAoIBAQDbDiQNcy1uUestQHNYCFhMsw6krCwwiySPyvYJ\n/WhiQu9L7PZsjCxcSl/rrZUZyNFm9fKCYXNHV8CHZ1l3hCBjpCbXkWr6Ui0wE8Ye\nlOBkTiB33FU5LE1TFlMwJ4UuE6npSls7PuIgjfRzfhu+U+3kPR1FgIFllzE6vj59\nzYwB+Ord2WnKlI4Zc2AZyNtaTihmXiV9xSsAAxWibRIj9sNpDfjRwBHvbT9rJEer\n2SokCBLkECjq2HH51UN8lSeHxK7Mk61FKbfJSFGKzXJb8p42x2xQxLgIRcWCJ98E\ncOqFBv5zL111IKkrIZS2dUp3KpmP+gL6wItzJQ5b4rL9H88DAoIBAQDKl9mb7iFN\n1ai+rJjaLOgzzr9nVt900kcBkV8y0nXCEZY83udrSw9RcE30NBomlXxWJ1lxDDZC\nye8AD6cbCIaij3d02jNx2iVgPUf2vxN0YDhwZMpFiy5M8fxRDnxKmIVF03EE8c9X\ng3GhlGvN3k6sdPJwgb3VwAh9kGQVwK5KMetFcJUypWK6Ot6xViJWJopFe5YPBxdr\neCoVq7+JELjJ7j1EEGYhVMVw7t1CsGANVrV6L3TTNQNESdaN8pjz7PsvCX62TlGX\nwPjSklO5Ru1CFV1w7YJHj2OD6b+JOw6QNLIyQi9oP9EExVTIIQRr7QN1OZ9aGHL6\nEDgc53EecIodAoIBADxPvGVnnM6PB21CHX/TbFxRwGpebRxAcySUAQHnH2JOg4wo\nBgEE5wHSCG7fL/oVbHIorUhwhEjURFIDhoJ9gl1syLT5eLbLAV4HU7j/zHhRemcF\n5wECzZdewjCz8Nsq1tFAg7XgLmpAK1nREtpoSUtZ+EE2jGnoIsnFr3b7rNyuKBxE\ny/fWxvkC5yayQpKuijkFGtVx/9DVCJPb6+6y9kJqcmNtuoJtVdSt/H24IP4iqvDX\n8iwWw+rBaP9YIbYj1OzGjCJKxitJGgpZXm8qcZ0rcwsZ3oGIlEStrZ2PaUKPFmeo\nVtb00x7o9AT4bjQ5KmaVs1ROxxZA0Z9C330J0PkCggEAd1OTZ6WN1jN3bb9pVHBI\n4GLxF+PyP/OuwPyn7t5JX+JN9FJySh7uyc/1ClY55On9Tx1kMBK6TwJzlDyj92dB\nLbSE7r2quW98vj+6CFqpEc2u0Hx9KxL8VXPeYru+d414ShVtJzVqI6iXIE20ZZCA\nFFHZjmzMrH6sQZDvcmSIA8l9Quw55JfHG9ua2SbbmJSgsqZFT1qk77baSuNbMFc6\nEC4TxehGz3EHzinTBvmtyY193JbhH5nE787x4a+3aUz28dCM4sIkitateBGZ4LIn\nAtpkrCQorQ+G1Oaz2xd+z29KWhHjrGqSKVY1Rp8z5IG4nK4w7rch2an98wBa/0vX\n/QKCAQEArzbuZOzgG3UGgeExE8mdUk6OLNFUzSeFOT57CEDcYB+YQUQwMgOI4+k+\noJb9tnUxvd8+mIcppHeDP0+fHU8Uz9OTZ8xPkGoBJ912K2FLFAel8g6vqW0Eu3ER\npCiz73Ea+0mTZBZZfZPVwL6mGgbsWJWsX2yCzmJ+Noq0bUxu2QE4LBDr7NiE52An\nsLG2ruWKIfJOl8BgRfqmoncgFrc2UztTV8PDV8gmp9oIBq7LqXyH70Knz8Flh1U4\n9A/5hCqd5ovBmHknQUXcp4vnPu4UzQCNcwuvHGj12UcFAywYD1LYXrwmyWQp57lC\nu4ePzLXxbfS9k7lshmbr2ix5s1Ypjg==\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "tests/fixtures/mod.rs",
    "content": "use std::io::{BufRead, BufReader};\nuse std::process::{Child, Command, Stdio};\nuse std::thread;\nuse std::thread::sleep;\nuse std::time::{Duration, Instant};\n\nuse assert_cmd::cargo;\nuse assert_fs::fixture::TempDir;\nuse assert_fs::prelude::*;\nuse port_check::free_local_port;\nuse reqwest::Url;\nuse reqwest::blocking::{Client, ClientBuilder};\nuse rstest::fixture;\n\n/// Error type used by tests\npub type Error = Box<dyn std::error::Error>;\n\n/// File names for testing purpose\npub static FILES: &[&str] = &[\n    \"test.txt\",\n    \"test.html\",\n    \"test.mkv\",\n    #[cfg(not(windows))]\n    \"test \\\" \\' & < >.csv\",\n    #[cfg(not(windows))]\n    \"new\\nline\",\n    \"😀.data\",\n    \"⎙.mp4\",\n    \"#[]{}()@!$&'`+,;= %20.test\",\n    #[cfg(unix)]\n    \":?#[]{}<>()@!$&'`|*+,;= %20.test\",\n    #[cfg(not(windows))]\n    \"foo\\\\bar.test\",\n];\n\n/// Hidden files for testing purpose\npub static HIDDEN_FILES: &[&str] = &[\".hidden_file1\", \".hidden_file2\"];\n\n/// Directory names for testing purpose\npub static DIRECTORIES: &[&str] = &[\"dira/\", \"dirb/\", \"dir space/\"];\n\n/// Hidden directories for testing purpose\npub static HIDDEN_DIRECTORIES: &[&str] = &[\".hidden_dir1/\", \".hidden space dir/\"];\n\n/// Name of a deeply nested file\npub static DEEPLY_NESTED_FILE: &str = \"very/deeply/nested/test.rs\";\n\n/// Name of a symlink pointing to a directory\npub static DIRECTORY_SYMLINK: &str = \"dir_symlink/\";\n\n/// Name of a directory inside a symlinked directory\n#[allow(unused)]\npub static DIR_BEHIND_SYMLINKED_DIR: &str = \"dir_symlink/nested\";\n\n/// Name of a file inside a directory inside a symlinked directory\npub static FILE_IN_DIR_BEHIND_SYMLINKED_DIR: &str = \"dir_symlink/nested/file\";\n\n/// Name of a symlink pointing to a file\npub static FILE_SYMLINK: &str = \"file_symlink\";\n\n/// Name of a symlink pointing to a path that doesn't exist\npub static BROKEN_SYMLINK: &str = \"broken_symlink\";\n\n/// Default reqwest client with some defaults set.\n#[fixture]\npub fn reqwest_client() -> Client {\n    if rustls::crypto::CryptoProvider::get_default().is_none() {\n        let _ = rustls::crypto::ring::default_provider().install_default();\n    }\n    let reqwest_client = ClientBuilder::new().tls_danger_accept_invalid_certs(true);\n    reqwest_client.build().unwrap()\n}\n\n/// Test fixture which creates a temporary directory with a few files and directories inside.\n/// The directories also contain files.\n#[fixture]\npub fn tmpdir() -> TempDir {\n    let tmpdir = assert_fs::TempDir::new().expect(\"Couldn't create a temp dir for tests\");\n    let mut files = FILES.to_vec();\n    files.extend_from_slice(HIDDEN_FILES);\n    for file in &files {\n        tmpdir\n            .child(file)\n            .write_str(\"Test Hello Yes\")\n            .expect(\"Couldn't write to file\");\n    }\n\n    let mut directories = DIRECTORIES.to_vec();\n    directories.extend_from_slice(HIDDEN_DIRECTORIES);\n    for directory in directories {\n        for file in &files {\n            tmpdir\n                .child(format!(\"{directory}{file}\"))\n                .write_str(&format!(\"This is {directory}{file}\"))\n                .expect(\"Couldn't write to file\");\n        }\n    }\n\n    tmpdir\n        .child(DEEPLY_NESTED_FILE)\n        .write_str(\"File in a deeply nested directory.\")\n        .expect(\"Couldn't write to file\");\n\n    // someDir structure that rm_files tests expect\n    tmpdir\n        .child(\"someDir/alpha\")\n        .write_str(\"alpha file content\")\n        .expect(\"Couldn't write to someDir/alpha\");\n\n    tmpdir\n        .child(\"someDir/some_sub_dir/bravo\")\n        .write_str(\"bravo file content\")\n        .expect(\"Couldn't write to someDir/some_sub_dir/bravo\");\n\n    tmpdir\n        .child(DIRECTORY_SYMLINK.strip_suffix(\"/\").unwrap())\n        .symlink_to_dir(DIRECTORIES[0].strip_suffix(\"/\").unwrap())\n        .expect(\"Couldn't create symlink to dir\");\n\n    tmpdir\n        .child(FILE_SYMLINK)\n        .symlink_to_file(FILES[0])\n        .expect(\"Couldn't create symlink to file\");\n\n    tmpdir\n        .child(BROKEN_SYMLINK)\n        .symlink_to_file(\"broken_symlink\")\n        .expect(\"Couldn't create broken symlink\");\n\n    tmpdir\n        .child(FILE_IN_DIR_BEHIND_SYMLINKED_DIR)\n        .write_str(\"something\")\n        .expect(\"Couldn't write symlink nexted file\");\n\n    tmpdir\n}\n\n/// Get a free port.\n#[fixture]\npub fn port() -> u16 {\n    free_local_port().expect(\"Couldn't find a free local port\")\n}\n\n/// Run miniserve as a server; Start with a temporary directory, a free port and some\n/// optional arguments then wait for a while for the server setup to complete.\n#[fixture]\npub fn server<I>(#[default(&[] as &[&str])] args: I) -> TestServer\nwhere\n    I: IntoIterator + Clone,\n    I::Item: AsRef<std::ffi::OsStr>,\n{\n    let port = port();\n    let tmpdir = tmpdir();\n    let mut child = Command::new(cargo::cargo_bin!(\"miniserve\"))\n        .arg(tmpdir.path())\n        .arg(\"-v\")\n        .arg(\"-p\")\n        .arg(port.to_string())\n        .args(args.clone())\n        .stdout(Stdio::piped())\n        .stderr(Stdio::piped())\n        .spawn()\n        .expect(\"Couldn't run test binary\");\n    let is_tls = args\n        .into_iter()\n        .any(|x| x.as_ref().to_str().unwrap().contains(\"tls\"));\n\n    // Read from stdout/stderr in the background and print/eprint everything read.\n    // This dance is required to allow test output capturing to work as expected.\n    // See https://github.com/rust-lang/rust/issues/92370 and https://github.com/rust-lang/rust/issues/90785\n\n    let stdout = child.stdout.take().expect(\"Child process stdout is None\");\n    thread::spawn(move || {\n        BufReader::new(stdout)\n            .lines()\n            .map_while(Result::ok)\n            .for_each(|line| println!(\"[miniserve stdout] {line}\"));\n    });\n\n    let stderr = child.stderr.take().expect(\"Child process stderr is None\");\n    thread::spawn(move || {\n        BufReader::new(stderr)\n            .lines()\n            .map_while(Result::ok)\n            .for_each(|line| eprintln!(\"[miniserve stderr] {line}\"));\n    });\n\n    wait_for_port(port);\n    TestServer::new(port, tmpdir, child, is_tls)\n}\n\n/// Wait a max of 1s for the port to become available.\nfn wait_for_port(port: u16) {\n    let start_wait = Instant::now();\n\n    while !port_check::is_port_reachable(format!(\"localhost:{port}\")) {\n        sleep(Duration::from_millis(100));\n\n        if start_wait.elapsed().as_secs() > 5 {\n            panic!(\"timeout waiting for port {port}\");\n        }\n    }\n}\n\npub struct TestServer {\n    port: u16,\n    tmpdir: TempDir,\n    child: Child,\n    is_tls: bool,\n}\n\n#[allow(dead_code)]\nimpl TestServer {\n    pub fn new(port: u16, tmpdir: TempDir, child: Child, is_tls: bool) -> Self {\n        Self {\n            port,\n            tmpdir,\n            child,\n            is_tls,\n        }\n    }\n\n    pub fn url(&self) -> Url {\n        let protocol = if self.is_tls { \"https\" } else { \"http\" };\n        Url::parse(&format!(\"{}://localhost:{}\", protocol, self.port)).unwrap()\n    }\n\n    pub fn path(&self) -> &std::path::Path {\n        self.tmpdir.path()\n    }\n\n    pub fn port(&self) -> u16 {\n        self.port\n    }\n}\n\nimpl Drop for TestServer {\n    fn drop(&mut self) {\n        self.child.kill().expect(\"Couldn't kill test server\");\n        self.child.wait().unwrap();\n    }\n}\n"
  },
  {
    "path": "tests/header.rs",
    "content": "use reqwest::blocking::Client;\nuse rstest::rstest;\n\nmod fixtures;\n\nuse crate::fixtures::{Error, reqwest_client, server};\n\n#[rstest]\n#[case(vec![\"x-info: 123\".to_string()])]\n#[case(vec![\"x-info1: 123\".to_string(), \"x-info2: 345\".to_string()])]\nfn custom_header_set(#[case] headers: Vec<String>, reqwest_client: Client) -> Result<(), Error> {\n    let server = server(headers.iter().flat_map(|h| vec![\"--header\", h]));\n    let resp = reqwest_client.get(server.url()).send()?;\n\n    for header in headers {\n        let mut header_split = header.splitn(2, ':');\n        let header_name = header_split.next().unwrap();\n        let header_value = header_split.next().unwrap().trim();\n        assert_eq!(resp.headers().get(header_name).unwrap(), header_value);\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "tests/navigation.rs",
    "content": "use std::path::{Component, Path};\nuse std::process::{Command, Stdio};\n\nuse pretty_assertions::{assert_eq, assert_ne};\nuse reqwest::blocking::Client;\nuse rstest::rstest;\nuse select::document::Document;\n\nmod fixtures;\nmod utils;\n\nuse crate::fixtures::{DEEPLY_NESTED_FILE, DIRECTORIES, Error, TestServer, reqwest_client, server};\nuse crate::utils::{get_link_from_text, get_link_hrefs_with_prefix};\n\n#[rstest]\n#[case(\"\", \"/\")]\n#[case(\"/dira\", \"/dira/\")]\n#[case(\"/dirb/\", \"/dirb/\")]\n#[case(\"/very/deeply/nested\", \"/very/deeply/nested/\")]\n/// Directories get a trailing slash.\nfn index_gets_trailing_slash(\n    server: TestServer,\n    reqwest_client: Client,\n    #[case] input: &str,\n    #[case] expected: &str,\n) -> Result<(), Error> {\n    let resp = reqwest_client.get(server.url().join(input)?).send()?;\n    assert!(resp.url().as_str().ends_with(expected));\n\n    Ok(())\n}\n\n#[rstest]\n/// Can't navigate up the root.\nfn cant_navigate_up_the_root(server: TestServer) -> Result<(), Error> {\n    // We're using curl for this as it has the option `--path-as-is` which doesn't normalize\n    // invalid urls. A useful feature in this particular case.\n    let base_url = server.url();\n    let curl_successful = Command::new(\"curl\")\n        .arg(\"-s\")\n        .arg(\"--fail\")\n        .arg(\"--path-as-is\")\n        .arg(format!(\"{base_url}/../\"))\n        .stdout(Stdio::null())\n        .status()?\n        .success();\n    assert!(curl_successful);\n\n    Ok(())\n}\n\n#[rstest]\n/// We can navigate into directories and back using shown links.\nfn can_navigate_into_dirs_and_back(\n    server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let base_url = server.url();\n    let initial_body = reqwest_client\n        .get(base_url.as_str())\n        .send()?\n        .error_for_status()?;\n    let initial_parsed = Document::from_read(initial_body)?;\n    for &directory in DIRECTORIES {\n        let dir_elem = get_link_from_text(&initial_parsed, directory).expect(\"Dir not found.\");\n        let body = reqwest_client\n            .get(format!(\"{base_url}{dir_elem}\"))\n            .send()?\n            .error_for_status()?;\n        let parsed = Document::from_read(body)?;\n        let back_link =\n            get_link_from_text(&parsed, \"Parent directory\").expect(\"Back link not found.\");\n        let resp = reqwest_client\n            .get(format!(\"{base_url}{back_link}\"))\n            .send()?;\n\n        // Now check that we can actually get back to the original location we came from using the\n        // link.\n        assert_eq!(resp.url().as_str(), base_url.as_str());\n    }\n\n    Ok(())\n}\n\n#[rstest]\n/// We can navigate deep into the file tree and back using shown links.\nfn can_navigate_deep_into_dirs_and_back(\n    server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    // Create a vector of parent directory names.\n    let dir_names = Path::new(DEEPLY_NESTED_FILE)\n        .parent()\n        .unwrap()\n        .components()\n        .map(|comp| {\n            let Component::Normal(dir) = comp else {\n                unreachable!()\n            };\n            dir.to_str().unwrap()\n        })\n        .collect::<Vec<_>>();\n    let base_url = server.url();\n\n    // First we'll go forwards through the directory tree and then we'll go backwards.\n    // In the end, we'll have to end up where we came from.\n    let mut next_url = base_url.clone();\n    for dir_name in dir_names.iter() {\n        let resp = reqwest_client.get(next_url.as_str()).send()?;\n        let body = resp.error_for_status()?;\n        let parsed = Document::from_read(body)?;\n        let dir_elem =\n            get_link_from_text(&parsed, &format!(\"{dir_name}/\")).expect(\"Dir not found.\");\n        next_url = next_url.join(&dir_elem)?;\n    }\n    assert_ne!(base_url, next_url);\n\n    // Now try to get out the tree again using links only.\n    while next_url != base_url {\n        let resp = reqwest_client.get(next_url.as_str()).send()?;\n        let body = resp.error_for_status()?;\n        let parsed = Document::from_read(body)?;\n        let dir_elem =\n            get_link_from_text(&parsed, \"Parent directory\").expect(\"Back link not found.\");\n        next_url = next_url.join(&dir_elem)?;\n    }\n    assert_eq!(base_url, next_url);\n\n    Ok(())\n}\n\n#[rstest]\n#[case(server(&[\"--title\", \"some title\"]), \"some title\")]\n#[case(server(None::<&str>), format!(\"localhost:{}\", server.port()))]\n/// We can use breadcrumbs to navigate.\nfn can_navigate_using_breadcrumbs(\n    #[case] server: TestServer,\n    reqwest_client: Client,\n    #[case] title_name: String,\n) -> Result<(), Error> {\n    let dir = Path::new(DEEPLY_NESTED_FILE)\n        .parent()\n        .unwrap()\n        .to_str()\n        .unwrap();\n\n    let base_url = server.url();\n    let nested_url = base_url.join(dir)?;\n\n    let resp = reqwest_client.get(nested_url.as_str()).send()?;\n    let body = resp.error_for_status()?;\n    let parsed = Document::from_read(body)?;\n\n    // can go back to root dir by clicking title\n    let title_link = get_link_from_text(&parsed, &title_name).expect(\"Root dir link not found.\");\n    assert_eq!(\"/\", title_link);\n\n    // can go to intermediate dir\n    let intermediate_dir_link =\n        get_link_from_text(&parsed, \"very\").expect(\"Intermediate dir link not found.\");\n    assert_eq!(\"/very/\", intermediate_dir_link);\n\n    // current dir is not linked\n    let current_dir_link = get_link_from_text(&parsed, \"nested\");\n    assert_eq!(None, current_dir_link);\n\n    Ok(())\n}\n\n#[rstest]\n#[case(server(&[\"--default-sorting-method\", \"name\", \"--default-sorting-order\", \"asc\"]), \"name\", \"asc\")]\n#[case(server(&[\"--default-sorting-method\", \"name\", \"--default-sorting-order\", \"desc\"]), \"name\", \"desc\")]\n/// We can specify the default sorting order\nfn can_specify_default_sorting_order(\n    #[case] server: TestServer,\n    reqwest_client: Client,\n    #[case] method: String,\n    #[case] order: String,\n) -> Result<(), Error> {\n    let resp = reqwest_client.get(server.url()).send()?;\n    let body = resp.error_for_status()?;\n    let parsed = Document::from_read(body)?;\n\n    let links = get_link_hrefs_with_prefix(&parsed, \"/\");\n    let dir_iter = server.path();\n    let mut dir_entries = dir_iter\n        .read_dir()\n        .unwrap()\n        .map(|x| x.unwrap().file_name().into_string().unwrap())\n        .map(|x| format!(\"/{x}\"))\n        .collect::<Vec<_>>();\n    dir_entries.sort();\n\n    if method == \"name\" && order == \"asc\" {\n        assert_eq!(\n            *dir_entries.last().unwrap(),\n            *percent_encoding::percent_decode_str(links.first().unwrap()).decode_utf8_lossy()\n        );\n    } else if method == \"name\" && order == \"desc\" {\n        assert_eq!(\n            *dir_entries.first().unwrap(),\n            *percent_encoding::percent_decode_str(links.first().unwrap()).decode_utf8_lossy()\n        );\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "tests/paste.rs",
    "content": "use reqwest::blocking::Client;\nuse rstest::rstest;\nuse select::{document::Document, predicate::Attr};\n\nmod fixtures;\n\nuse crate::fixtures::{Error, TestServer, reqwest_client, server};\n\n// There are few tests here because the pastebin is implemented by converting a textareas content\n// into an in-memory blob/file, and adding that file to the existing file upload form. We can't\n// test the JS here, and any testing the actual \"upload\" would just be retesting the existing\n// uploader.\n\n#[rstest]\n#[case::without_flag(&[\"--upload-files\"], false)]\n#[case::with_flag(&[\"--upload-files\", \"--pastebin\"], true)]\nfn paste_entry_only_appears_with_flag(\n    #[case] _flags: &[&str],\n    #[case] should_exist: bool,\n    #[with(_flags)] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let body = reqwest_client\n        .get(server.url())\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n    let exists = parsed.find(Attr(\"id\", \"pastebin\")).next().is_some();\n\n    assert_eq!(\n        exists, should_exist,\n        \"Expected exists(#pastebin) to return {}, but got {}\",\n        should_exist, exists\n    );\n\n    Ok(())\n}\n"
  },
  {
    "path": "tests/qrcode.rs",
    "content": "use std::process::{Command, Stdio};\nuse std::thread::sleep;\nuse std::time::Duration;\n\nuse assert_cmd::cargo;\nuse assert_fs::TempDir;\nuse reqwest::blocking::Client;\nuse rstest::rstest;\nuse select::{document::Document, predicate::Attr};\n\nmod fixtures;\n\nuse crate::fixtures::{Error, TestServer, port, reqwest_client, server, tmpdir};\n\n#[rstest]\nfn webpage_hides_qrcode_when_disabled(\n    server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let body = reqwest_client\n        .get(server.url())\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n    assert!(parsed.find(Attr(\"id\", \"qrcode\")).next().is_none());\n\n    Ok(())\n}\n\n#[rstest]\nfn webpage_shows_qrcode_when_enabled(\n    #[with(&[\"-q\"])] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let body = reqwest_client\n        .get(server.url())\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n    let qr_container = parsed\n        .find(Attr(\"id\", \"qrcode\"))\n        .next()\n        .ok_or(\"QR container not found\")?;\n    let tooltip = qr_container\n        .attr(\"title\")\n        .ok_or(\"QR container has no title\")?;\n    assert_eq!(tooltip, server.url().as_str());\n\n    Ok(())\n}\n\n#[cfg(not(windows))]\nfn run_in_faketty_kill_and_get_stdout(template: &Command) -> Result<String, Error> {\n    use fake_tty::{bash_command, get_stdout};\n\n    let cmd = {\n        let bin = template.get_program().to_str().expect(\"not UTF8\");\n        let args = template\n            .get_args()\n            .map(|s| s.to_str().expect(\"not UTF8\"))\n            .collect::<Vec<_>>()\n            .join(\" \");\n        format!(\"{bin} {args}\")\n    };\n    let mut child = bash_command(&cmd)?.stdin(Stdio::null()).spawn()?;\n\n    sleep(Duration::from_secs(1));\n\n    child.kill()?;\n    let output = child.wait_with_output().expect(\"Failed to read stdout\");\n    let all_text = get_stdout(output.stdout)?;\n\n    Ok(all_text)\n}\n\n#[rstest]\n// Disabled for Windows because `fake_tty` does not currently support it.\n#[cfg(not(windows))]\nfn qrcode_hidden_in_tty_when_disabled(tmpdir: TempDir, port: u16) -> Result<(), Error> {\n    let mut template = Command::new(cargo::cargo_bin!(\"miniserve\"));\n    template.arg(\"-p\").arg(port.to_string()).arg(tmpdir.path());\n\n    let output = run_in_faketty_kill_and_get_stdout(&template)?;\n\n    assert!(!output.contains(\"QR code for \"));\n    Ok(())\n}\n\n#[rstest]\n// Disabled for Windows because `fake_tty` does not currently support it.\n#[cfg(not(windows))]\nfn qrcode_shown_in_tty_when_enabled(tmpdir: TempDir, port: u16) -> Result<(), Error> {\n    let mut template = Command::new(cargo::cargo_bin!(\"miniserve\"));\n    template\n        .arg(\"-p\")\n        .arg(port.to_string())\n        .arg(\"-q\")\n        .arg(tmpdir.path());\n\n    let output = run_in_faketty_kill_and_get_stdout(&template)?;\n\n    assert!(output.contains(\"QR code for \"));\n    Ok(())\n}\n\n#[rstest]\nfn qrcode_hidden_in_non_tty_when_enabled(tmpdir: TempDir, port: u16) -> Result<(), Error> {\n    let mut child = Command::new(cargo::cargo_bin!(\"miniserve\"))\n        .arg(\"-p\")\n        .arg(port.to_string())\n        .arg(\"-q\")\n        .arg(tmpdir.path())\n        .stdout(Stdio::piped())\n        .stderr(Stdio::piped())\n        .spawn()?;\n\n    sleep(Duration::from_secs(1));\n\n    child.kill()?;\n    let output = child.wait_with_output().expect(\"Failed to read stdout\");\n    let stdout = String::from_utf8(output.stdout)?;\n\n    assert!(!stdout.contains(\"QR code for \"));\n    Ok(())\n}\n"
  },
  {
    "path": "tests/raw.rs",
    "content": "use pretty_assertions::assert_eq;\nuse reqwest::blocking::Client;\nuse rstest::rstest;\nuse select::{\n    document::Document,\n    predicate::{Class, Name},\n};\n\nmod fixtures;\n\nuse crate::fixtures::{Error, TestServer, reqwest_client, server};\n\n/// The footer displays the correct wget command to download the folder recursively\n// This test can't test all aspects of the wget footer,\n// a more detailed unit test is available\n#[rstest]\n#[case(0, \"\")]\n#[case(1, \"dira/\")]\n#[case(2, \"very/deeply/\")]\n#[case(3, \"very/deeply/nested/\")]\nfn ui_displays_wget_element(\n    #[case] depth: u8,\n    #[case] dir: &str,\n    #[with(&[\"--show-wget-footer\"])] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let body = reqwest_client\n        .get(format!(\"{}{}\", server.url(), dir))\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n    let wget_url = parsed\n        .find(Class(\"downloadDirectory\"))\n        .next()\n        .unwrap()\n        .find(Class(\"cmd\"))\n        .next()\n        .unwrap()\n        .text();\n    let cut_dirs = match depth {\n        // Put all the files in a folder of this name\n        0 => format!(\" -P 'localhost:{}'\", server.port()),\n        1 => String::new(),\n        // Avoids putting the files in excessive directories\n        x => format!(\" --cut-dirs={}\", x - 1),\n    };\n    assert_eq!(\n        wget_url,\n        format!(\n            \"wget -rcnp -R 'index.html*' -nH{} '{}{}?raw=true'\",\n            cut_dirs,\n            server.url(),\n            dir\n        )\n    );\n\n    Ok(())\n}\n\n/// All hrefs in raw mode are links to directories or files & directories end with ?raw=true\n#[rstest]\n#[case(\"\")]\n#[case(\"very/\")]\n#[case(\"very/deeply/\")]\n#[case(\"very/deeply/nested/\")]\nfn raw_mode_links_to_directories_end_with_raw_true(\n    #[case] dir: &str,\n    #[with(&[\"--show-wget-footer\"])] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    fn verify_a_tags(parsed: Document) {\n        // Ensure all links end with ?raw=true or are files\n        for node in parsed.find(Name(\"a\")) {\n            if let Some(class) = node.attr(\"class\") {\n                if class == \"root\" || class == \"directory\" {\n                    assert!(\n                        node.attr(\"href\").unwrap().ends_with(\"?raw=true\"),\n                        \"doesn't end with ?raw=true\"\n                    );\n                } else if class == \"file\" {\n                    return;\n                } else {\n                    panic!(\n                        \"This node is a link and neither of class directory, root or file: {node:?}\"\n                    );\n                }\n            }\n        }\n    }\n\n    // Ensure the links to the archives are not present\n    let body = reqwest_client\n        .get(format!(\"{}{}?raw=true\", server.url(), dir))\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n    verify_a_tags(parsed);\n\n    Ok(())\n}\n"
  },
  {
    "path": "tests/readme.rs",
    "content": "use std::fs::{File, remove_file};\nuse std::io::Write;\nuse std::path::PathBuf;\n\nuse reqwest::blocking::Client;\nuse rstest::rstest;\nuse select::predicate::Attr;\nuse select::{document::Document, node::Node};\n\nmod fixtures;\n\nuse fixtures::{DIRECTORIES, Error, FILES, TestServer, server};\n\nuse crate::fixtures::reqwest_client;\n\nfn write_readme_contents(path: PathBuf, filename: &str) -> PathBuf {\n    let readme_path = path.join(filename);\n    let mut readme_file = File::create(&readme_path).unwrap();\n    readme_file\n        .write_all(format!(\"Contents of {filename}\").as_bytes())\n        .expect(\"Couldn't write readme\");\n    readme_path\n}\n\nfn assert_readme_contents(parsed_dom: &Document, filename: &str) {\n    assert!(parsed_dom.find(Attr(\"id\", \"readme\")).next().is_some());\n    assert!(\n        parsed_dom\n            .find(Attr(\"id\", \"readme-filename\"))\n            .next()\n            .is_some()\n    );\n    assert!(\n        parsed_dom\n            .find(Attr(\"id\", \"readme-filename\"))\n            .next()\n            .unwrap()\n            .text()\n            == filename\n    );\n    assert!(\n        parsed_dom\n            .find(Attr(\"id\", \"readme-contents\"))\n            .next()\n            .is_some()\n    );\n    assert!(\n        parsed_dom\n            .find(Attr(\"id\", \"readme-contents\"))\n            .next()\n            .unwrap()\n            .text()\n            .trim()\n            .contains(&format!(\"Contents of {filename}\"))\n    );\n}\n\n/// Do not show readme contents by default\n#[rstest]\nfn no_readme_contents(server: TestServer, reqwest_client: Client) -> Result<(), Error> {\n    let body = reqwest_client\n        .get(server.url())\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n\n    // Check that the regular file listing still works.\n    for &file in FILES {\n        assert!(parsed.find(|x: &Node| x.text() == file).next().is_some());\n    }\n    for &dir in DIRECTORIES {\n        assert!(parsed.find(|x: &Node| x.text() == dir).next().is_some());\n    }\n\n    // Check that there is no readme stuff here.\n    assert!(parsed.find(Attr(\"id\", \"readme\")).next().is_none());\n    assert!(parsed.find(Attr(\"id\", \"readme-filename\")).next().is_none());\n    assert!(parsed.find(Attr(\"id\", \"readme-contents\")).next().is_none());\n\n    Ok(())\n}\n\n/// Show readme contents when told to if there is a readme file in the root\n#[rstest]\n#[case(\"Readme.md\")]\n#[case(\"readme.md\")]\n#[case(\"README.md\")]\n#[case(\"README.MD\")]\n#[case(\"ReAdMe.Md\")]\nfn show_root_readme_contents(\n    #[with(&[\"--readme\"])] server: TestServer,\n    reqwest_client: Client,\n    #[case] readme_name: &str,\n) -> Result<(), Error> {\n    let readme_path = write_readme_contents(server.path().to_path_buf(), readme_name);\n    let body = reqwest_client\n        .get(server.url())\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n\n    // All the files are still getting listed...\n    for &file in FILES {\n        assert!(parsed.find(|x: &Node| x.text() == file).next().is_some());\n    }\n    // ...in addition to the readme contents below the file listing.\n    assert_readme_contents(&parsed, readme_name);\n    remove_file(readme_path).unwrap();\n    Ok(())\n}\n\n/// Show readme contents when told to if there is a readme file in any of the directories\n#[rstest]\n#[case(\"Readme.md\")]\n#[case(\"readme.md\")]\n#[case(\"README.md\")]\n#[case(\"README.MD\")]\n#[case(\"ReAdMe.Md\")]\n#[case(\"Readme.txt\")]\n#[case(\"README.txt\")]\n#[case(\"README\")]\n#[case(\"ReAdMe\")]\nfn show_nested_readme_contents(\n    #[with(&[\"--readme\"])] server: TestServer,\n    reqwest_client: Client,\n    #[case] readme_name: &str,\n) -> Result<(), Error> {\n    for dir in DIRECTORIES {\n        let readme_path = write_readme_contents(server.path().join(dir), readme_name);\n        let body = reqwest_client\n            .get(server.url().join(dir)?)\n            .send()?\n            .error_for_status()?;\n        let parsed = Document::from_read(body)?;\n\n        // All the files are still getting listed...\n        for &file in FILES {\n            assert!(parsed.find(|x: &Node| x.text() == file).next().is_some());\n        }\n        // ...in addition to the readme contents below the file listing.\n        assert_readme_contents(&parsed, readme_name);\n        remove_file(readme_path).unwrap();\n    }\n    Ok(())\n}\n"
  },
  {
    "path": "tests/rm_files.rs",
    "content": "mod fixtures;\n\nuse assert_fs::fixture::TempDir;\nuse fixtures::{Error, TestServer, server, tmpdir};\nuse percent_encoding::utf8_percent_encode;\nuse reqwest::StatusCode;\nuse reqwest::blocking::Client;\nuse rstest::rstest;\nuse std::path::{Component, Path};\nuse url::Url;\n\nuse crate::fixtures::{\n    DEEPLY_NESTED_FILE, DIRECTORIES, FILES, HIDDEN_DIRECTORIES, HIDDEN_FILES, reqwest_client,\n};\n\nconst NESTED_FILES_UNDER_SINGLE_ROOT: &[&str] = &[\"someDir/alpha\", \"someDir/some_sub_dir/bravo\"];\n\n/// Construct a path for a GET request,\n/// with each path component being separately encoded.\nfn make_get_path(unencoded_path: impl AsRef<Path>) -> String {\n    unencoded_path\n        .as_ref()\n        .components()\n        .map(|comp| match comp {\n            Component::Prefix(_) | Component::RootDir => unreachable!(\"Not currently used\"),\n            Component::CurDir => \".\",\n            Component::ParentDir => \"..\",\n            Component::Normal(comp) => comp.to_str().unwrap(),\n        })\n        .map(|comp| utf8_percent_encode(comp, percent_encoding::NON_ALPHANUMERIC).to_string())\n        .collect::<Vec<_>>()\n        .join(\"/\")\n}\n\n/// Construct a path for a deletion POST request without any further encoding.\n///\n/// This should be kept consistent with implementation.\nfn make_del_path(unencoded_path: impl AsRef<Path>) -> String {\n    format!(\"rm?path=/{}\", make_get_path(unencoded_path))\n}\n\n/// Tests that deletion requests succeed as expected.\n/// Verifies that the path exists, can be deleted, and is no longer accessible after deletion.\nfn assert_rm_ok(\n    reqwest_client: &Client,\n    base_url: Url,\n    unencoded_path: impl AsRef<Path>,\n) -> Result<(), Error> {\n    let file_path = unencoded_path.as_ref();\n\n    // encode\n    let get_url = base_url.join(&make_get_path(file_path))?;\n    let del_url = base_url.join(&make_del_path(file_path))?;\n\n    // check path exists\n    let _get_res = reqwest_client\n        .get(get_url.clone())\n        .send()?\n        .error_for_status()?;\n\n    // delete\n    let _del_res = reqwest_client.post(del_url).send()?.error_for_status()?;\n\n    // check path is gone\n    let get_res = reqwest_client.get(get_url).send()?;\n    if get_res.status() != StatusCode::NOT_FOUND {\n        return Err(format!(\"Unexpected status code: {}\", get_res.status()).into());\n    }\n\n    Ok(())\n}\n\n/// Tests that deletion requests fail as expected.\n/// The `check_path_exists` parameter allows skipping this check before and after\n/// the deletion attempt in case the path should be inaccessible via GET.\nfn assert_rm_err(\n    reqwest_client: &Client,\n    base_url: Url,\n    unencoded_path: impl AsRef<Path>,\n    check_path_exists: bool,\n) -> Result<(), Error> {\n    let file_path = unencoded_path.as_ref();\n\n    // encode\n    let get_url = base_url.join(&make_get_path(file_path))?;\n    let del_url = base_url.join(&make_del_path(file_path))?;\n\n    // check path exists\n    if check_path_exists {\n        let _get_res = reqwest_client\n            .get(get_url.clone())\n            .send()?\n            .error_for_status()?;\n    }\n\n    // delete\n    let del_res = reqwest_client.post(del_url).send()?;\n    if !del_res.status().is_client_error() {\n        return Err(format!(\"Unexpected status code: {}\", del_res.status()).into());\n    }\n\n    // check path still exists\n    if check_path_exists {\n        let _get_res = reqwest_client.get(get_url).send()?.error_for_status()?;\n    }\n\n    Ok(())\n}\n\n#[rstest]\n#[case(FILES[0])]\n#[case(FILES[1])]\n#[case(FILES[2])]\n#[case(DIRECTORIES[0])]\n#[case(DIRECTORIES[1])]\n#[case(DIRECTORIES[2])]\n#[case(DEEPLY_NESTED_FILE)]\nfn rm_disabled_by_default(\n    server: TestServer,\n    reqwest_client: Client,\n    #[case] path: &str,\n) -> Result<(), Error> {\n    assert_rm_err(&reqwest_client, server.url(), path, true)\n}\n\n#[rstest]\n#[case(FILES[0])]\n#[case(FILES[1])]\n#[case(FILES[2])]\n#[case(HIDDEN_FILES[0])]\n#[case(HIDDEN_FILES[1])]\n#[case(DIRECTORIES[0])]\n#[case(DIRECTORIES[1])]\n#[case(DIRECTORIES[2])]\n#[case(HIDDEN_DIRECTORIES[0])]\n#[case(HIDDEN_DIRECTORIES[1])]\n#[case(DEEPLY_NESTED_FILE)]\nfn rm_disabled_by_default_with_hidden(\n    reqwest_client: Client,\n    #[with(&[\"-H\"])] server: TestServer,\n    #[case] path: &str,\n) -> Result<(), Error> {\n    assert_rm_err(&reqwest_client, server.url(), path, true)\n}\n\n#[rstest]\n#[case(FILES[0])]\n#[case(FILES[1])]\n#[case(FILES[2])]\n#[case(DIRECTORIES[0])]\n#[case(DIRECTORIES[1])]\n#[case(DIRECTORIES[2])]\n#[case(DEEPLY_NESTED_FILE)]\nfn rm_works(\n    #[with(&[\"-R\"])] server: TestServer,\n    reqwest_client: Client,\n    #[case] path: &str,\n) -> Result<(), Error> {\n    assert_rm_ok(&reqwest_client, server.url(), path)\n}\n\n#[rstest]\n#[case(HIDDEN_FILES[0])]\n#[case(HIDDEN_FILES[1])]\n#[case(HIDDEN_DIRECTORIES[0])]\n#[case(HIDDEN_DIRECTORIES[1])]\nfn cannot_rm_hidden_when_disallowed(\n    #[with(&[\"-R\"])] server: TestServer,\n    reqwest_client: Client,\n    #[case] path: &str,\n) -> Result<(), Error> {\n    assert_rm_err(&reqwest_client, server.url(), path, false)\n}\n\n#[rstest]\n#[case(HIDDEN_FILES[0])]\n#[case(HIDDEN_FILES[1])]\n#[case(HIDDEN_DIRECTORIES[0])]\n#[case(HIDDEN_DIRECTORIES[1])]\nfn can_rm_hidden_when_allowed(\n    #[with(&[\"-R\", \"-H\"])] server: TestServer,\n    reqwest_client: Client,\n    #[case] path: &str,\n) -> Result<(), Error> {\n    assert_rm_ok(&reqwest_client, server.url(), path)\n}\n\n/// This test runs the server with --allowed-rm-dir argument and checks that\n/// deletions in a different directory are actually prevented.\n#[rstest]\n#[case(server(&[\"-R\", \"someOtherDir\"]), NESTED_FILES_UNDER_SINGLE_ROOT[0])]\n#[case(server(&[\"-R\", \"someOtherDir\"]), NESTED_FILES_UNDER_SINGLE_ROOT[1])]\n#[case(server(&[\"-R\", \"someDir/some_other_sub_dir\"]), NESTED_FILES_UNDER_SINGLE_ROOT[0])]\n#[case(server(&[\"-R\", \"someDir/some_other_sub_dir\"]), NESTED_FILES_UNDER_SINGLE_ROOT[1])]\nfn rm_is_restricted(\n    #[case] server: TestServer,\n    reqwest_client: Client,\n    #[case] path: &str,\n) -> Result<(), Error> {\n    assert_rm_err(&reqwest_client, server.url(), path, true)\n}\n\n/// This test runs the server with --allowed-rm-dir argument and checks that\n/// deletions of the specified directories themselves are allowed.\n///\n/// Both ways of specifying multiple directories are tested.\n#[rstest]\n#[case(server(&[\"-R\", \"dira,dirb,dir space\"]), DIRECTORIES[0])]\n#[case(server(&[\"-R\", \"dira,dirb,dir space\"]), DIRECTORIES[1])]\n#[case(server(&[\"-R\", \"dira,dirb,dir space\"]), DIRECTORIES[2])]\n#[case(server(&[\"-R\", \"dira\", \"-R\", \"dirb\", \"-R\", \"dir space\"]), DIRECTORIES[0])]\n#[case(server(&[\"-R\", \"dira\", \"-R\", \"dirb\", \"-R\", \"dir space\"]), DIRECTORIES[1])]\n#[case(server(&[\"-R\", \"dira\", \"-R\", \"dirb\", \"-R\", \"dir space\"]), DIRECTORIES[2])]\nfn can_rm_allowed_dir(\n    #[case] server: TestServer,\n    reqwest_client: Client,\n    #[case] path: &str,\n) -> Result<(), Error> {\n    assert_rm_ok(&reqwest_client, server.url(), path)\n}\n\n/// This tests that we can delete from directories specified by --allow-rm-dir.\n#[rstest]\n#[case(server(&[\"-R\", \"someDir\"]), \"someDir/alpha\")]\n#[case(server(&[\"-R\", \"someDir\"]), \"someDir//alpha\")]\n#[case(server(&[\"-R\", \"someDir\"]), \"someDir/././alpha\")]\n#[case(server(&[\"-R\", \"someDir\"]), \"someDir/some_sub_dir\")]\n#[case(server(&[\"-R\", \"someDir\"]), \"someDir/some_sub_dir/\")]\n#[case(server(&[\"-R\", \"someDir\"]), \"someDir//some_sub_dir\")]\n#[case(server(&[\"-R\", \"someDir\"]), \"someDir/./some_sub_dir\")]\n#[case(server(&[\"-R\", \"someDir\"]), \"someDir/some_sub_dir/bravo\")]\n#[case(server(&[\"-R\", \"someDir\"]), \"someDir//some_sub_dir//bravo\")]\n#[case(server(&[\"-R\", \"someDir\"]), \"someDir/./some_sub_dir/../some_sub_dir/bravo\")]\n#[case(server(&[\"-R\", \"someDir/some_sub_dir\"]), \"someDir/some_sub_dir/bravo\")]\n#[case(server(&[\"-R\", Path::new(\"someDir/some_sub_dir\").to_str().unwrap()]),\n    \"someDir/some_sub_dir/bravo\")]\nfn can_rm_from_allowed_dir(\n    #[case] server: TestServer,\n    reqwest_client: Client,\n    #[case] file: &str,\n) -> Result<(), Error> {\n    assert_rm_ok(&reqwest_client, server.url(), file)\n}\n\n/// Test deleting from symlinked directories that point to outside the server root.\n#[rstest]\n#[case(server(&[\"-R\"]), true)]\n#[case(server(&[\"-R\", \"--no-symlinks\"]), false)]\nfn rm_from_symlinked_dir(\n    #[case] server: TestServer,\n    #[case] should_succeed: bool,\n    #[from(tmpdir)] target: TempDir,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    #[cfg(unix)]\n    use std::os::unix::fs::symlink as symlink_dir;\n    #[cfg(windows)]\n    use std::os::windows::fs::symlink_dir;\n\n    // create symlink\n    let link: &Path = Path::new(\"linked\");\n    symlink_dir(target.path(), server.path().join(link))?;\n\n    let files_through_link = [FILES, DIRECTORIES]\n        .concat()\n        .iter()\n        .map(|name| link.join(name))\n        .collect::<Vec<_>>();\n    if should_succeed {\n        for file_path in &files_through_link {\n            assert_rm_ok(&reqwest_client, server.url(), file_path)?;\n        }\n    } else {\n        for file_path in &files_through_link {\n            assert_rm_err(&reqwest_client, server.url(), file_path, false)?;\n        }\n    }\n    Ok(())\n}\n"
  },
  {
    "path": "tests/serve_request.rs",
    "content": "use std::process::{Command, Stdio};\nuse std::thread::sleep;\nuse std::time::Duration;\n\nuse assert_cmd::cargo;\nuse assert_fs::fixture::TempDir;\nuse fixtures::BROKEN_SYMLINK;\nuse regex::Regex;\nuse reqwest::StatusCode;\nuse reqwest::blocking::Client;\nuse rstest::rstest;\nuse select::{document::Document, node::Node, predicate::Attr};\n\nmod fixtures;\n\nuse crate::fixtures::{\n    DIR_BEHIND_SYMLINKED_DIR, DIRECTORIES, DIRECTORY_SYMLINK, Error,\n    FILE_IN_DIR_BEHIND_SYMLINKED_DIR, FILE_SYMLINK, FILES, HIDDEN_DIRECTORIES, HIDDEN_FILES,\n    TestServer, port, reqwest_client, server, tmpdir,\n};\n\n#[rstest]\nfn serves_requests_with_no_options(reqwest_client: Client, tmpdir: TempDir) -> Result<(), Error> {\n    let mut child = Command::new(cargo::cargo_bin!(\"miniserve\"))\n        .arg(tmpdir.path())\n        .stdout(Stdio::null())\n        .spawn()?;\n\n    sleep(Duration::from_secs(1));\n\n    let body = reqwest_client\n        .get(\"http://localhost:8080\")\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n    for &file in FILES {\n        assert!(parsed.find(|x: &Node| x.text() == file).next().is_some());\n    }\n    for &dir in DIRECTORIES {\n        assert!(parsed.find(|x: &Node| x.text() == dir).next().is_some());\n    }\n\n    child.kill()?;\n\n    Ok(())\n}\n\n#[rstest]\nfn serves_requests_with_non_default_port(\n    server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let body = reqwest_client\n        .get(server.url())\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n\n    for &file in FILES {\n        let f = parsed.find(|x: &Node| x.text() == file).next().unwrap();\n        reqwest_client\n            .get(server.url().join(f.attr(\"href\").unwrap())?)\n            .send()?\n            .error_for_status()?;\n        assert_eq!(\n            format!(\"/{file}\"),\n            percent_encoding::percent_decode_str(f.attr(\"href\").unwrap()).decode_utf8_lossy(),\n        );\n    }\n\n    for &directory in DIRECTORIES {\n        assert!(\n            parsed\n                .find(|x: &Node| x.text() == directory)\n                .next()\n                .is_some()\n        );\n        let dir_body = reqwest_client\n            .get(server.url().join(directory)?)\n            .send()?\n            .error_for_status()?;\n        let dir_body_parsed = Document::from_read(dir_body)?;\n        for &file in FILES {\n            assert!(\n                dir_body_parsed\n                    .find(|x: &Node| x.text() == file)\n                    .next()\n                    .is_some()\n            );\n        }\n    }\n\n    Ok(())\n}\n\n#[rstest]\n#[case(\"__miniserve_internal/healthcheck\", server(None::<&str>))]\n#[case(\"__miniserve_internal/favicon.svg\", server(None::<&str>))]\n#[case(\"__miniserve_internal/style.css\", server(None::<&str>))]\n#[case(\"testlol/__miniserve_internal/healthcheck\", server(&[\"--route-prefix\", \"testlol\"]))]\n#[case(\"testlol/__miniserve_internal/favicon.svg\", server(&[\"--route-prefix\", \"testlol\"]))]\n#[case(\"testlol/__miniserve_internal/style.css\", server(&[\"--route-prefix\", \"testlol\"]))]\n#[case(\"__miniserve_internal/healthcheck\", server(&[\"--random-route\"]))]\n#[case(\"__miniserve_internal/favicon.svg\", server(&[\"--random-route\"]))]\n#[case(\"__miniserve_internal/style.css\", server(&[\"--random-route\"]))]\n#[case(\"__miniserve_internal/healthcheck\", server(&[\"--auth\", \"doesnt:matter\"]))]\n#[case(\"__miniserve_internal/favicon.svg\", server(&[\"--auth\", \"doesnt:matter\"]))]\n#[case(\"__miniserve_internal/style.css\", server(&[\"--auth\", \"doesnt:matter\"]))]\nfn serves_requests_for_special_routes(\n    reqwest_client: Client,\n    #[case] route: &str,\n    #[case] server: TestServer,\n) -> Result<(), Error> {\n    reqwest_client\n        .get(format!(\"{}{}\", server.url(), route))\n        .send()?\n        .error_for_status()?;\n\n    Ok(())\n}\n\n#[rstest]\nfn serves_requests_hidden_files(\n    #[with(&[\"--hidden\"])] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let body = reqwest_client\n        .get(server.url())\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n\n    for &file in FILES.iter().chain(HIDDEN_FILES) {\n        let f = parsed.find(|x: &Node| x.text() == file).next().unwrap();\n        assert_eq!(\n            format!(\"/{file}\"),\n            percent_encoding::percent_decode_str(f.attr(\"href\").unwrap()).decode_utf8_lossy(),\n        );\n    }\n\n    for &directory in DIRECTORIES.iter().chain(HIDDEN_DIRECTORIES) {\n        assert!(\n            parsed\n                .find(|x: &Node| x.text() == directory)\n                .next()\n                .is_some()\n        );\n        let dir_body = reqwest_client\n            .get(server.url().join(directory)?)\n            .send()?\n            .error_for_status()?;\n        let dir_body_parsed = Document::from_read(dir_body)?;\n        for &file in FILES.iter().chain(HIDDEN_FILES) {\n            assert!(\n                dir_body_parsed\n                    .find(|x: &Node| x.text() == file)\n                    .next()\n                    .is_some()\n            );\n        }\n    }\n\n    Ok(())\n}\n\n#[rstest]\nfn serves_requests_no_hidden_files_without_flag(\n    server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let body = reqwest_client\n        .get(server.url())\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n\n    for &hidden_item in HIDDEN_FILES.iter().chain(HIDDEN_DIRECTORIES) {\n        assert!(\n            parsed\n                .find(|x: &Node| x.text() == hidden_item)\n                .next()\n                .is_none()\n        );\n        let resp = reqwest_client.get(server.url().join(hidden_item)?).send()?;\n        assert_eq!(resp.status(), StatusCode::BAD_REQUEST);\n    }\n\n    Ok(())\n}\n\n#[rstest]\n#[case(server(None::<&str>), StatusCode::OK)]\n#[case(server(&[\"--no-symlinks\"]), StatusCode::NOT_FOUND)]\nfn serves_requests_nested_in_symlinks(\n    #[case] server: TestServer,\n    reqwest_client: Client,\n    #[case] expected_status: StatusCode,\n) -> Result<(), Error> {\n    let file_status = reqwest_client\n        .get(server.url().join(DIRECTORY_SYMLINK)?.join(FILES[0])?)\n        .send()?\n        .status();\n    assert_eq!(file_status, expected_status);\n\n    let dir_status = reqwest_client\n        .get(server.url().join(DIR_BEHIND_SYMLINKED_DIR)?)\n        .send()?\n        .status();\n    assert_eq!(dir_status, expected_status);\n\n    let nested_file_status = reqwest_client\n        .get(server.url().join(FILE_IN_DIR_BEHIND_SYMLINKED_DIR)?)\n        .send()?\n        .status();\n    assert_eq!(nested_file_status, expected_status);\n\n    Ok(())\n}\n\n#[rstest]\n#[case(true, false, server(&[\"--no-symlinks\"]))]\n#[case(true, true, server(&[\"--no-symlinks\", \"--show-symlink-info\"]))]\n#[case(false, false, server(None::<&str>))]\nfn serves_requests_symlinks(\n    #[case] no_symlinks: bool,\n    #[case] show_symlink_info: bool,\n    #[case] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let body = reqwest_client\n        .get(server.url())\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n\n    for &entry in &[FILE_SYMLINK, DIRECTORY_SYMLINK] {\n        let status = reqwest_client\n            .get(server.url().join(entry)?)\n            .send()?\n            .status();\n        // We expect a 404 here for when `no_symlinks` is `true`.\n        if no_symlinks {\n            assert_eq!(status, StatusCode::NOT_FOUND);\n        } else {\n            assert_eq!(status, StatusCode::OK);\n        }\n\n        let node = parsed\n            .find(|x: &Node| x.name().unwrap_or_default() == \"a\" && x.text() == entry)\n            .next();\n\n        // If symlinks are deactivated, none should be shown in the listing.\n        dbg!(&node);\n        assert_eq!(node.is_none(), no_symlinks);\n        if node.is_some() && show_symlink_info {\n            assert_eq!(node.unwrap().attr(\"class\").unwrap(), \"symlink\");\n        }\n\n        // If following symlinks is deactivated, we can just skip this iteration as we assorted\n        // above the no entries in the listing can be found for symlinks in that case.\n        if no_symlinks {\n            continue;\n        }\n\n        let node = node.unwrap();\n        assert_eq!(node.attr(\"href\").unwrap().strip_prefix('/').unwrap(), entry);\n        if entry.ends_with('/') {\n            let node = parsed\n                .find(|x: &Node| x.name().unwrap_or_default() == \"a\" && x.text() == DIRECTORIES[0])\n                .next();\n            assert_eq!(node.unwrap().attr(\"class\").unwrap(), \"directory\");\n        } else {\n            let node = parsed\n                .find(|x: &Node| x.name().unwrap_or_default() == \"a\" && x.text() == FILES[0])\n                .next();\n            assert_eq!(node.unwrap().attr(\"class\").unwrap(), \"file\");\n        }\n    }\n    assert!(\n        parsed\n            .find(|x: &Node| x.text() == BROKEN_SYMLINK)\n            .next()\n            .is_none()\n    );\n\n    Ok(())\n}\n\n#[rstest]\nfn serves_requests_with_randomly_assigned_port(tmpdir: TempDir) -> Result<(), Error> {\n    let mut child = Command::new(cargo::cargo_bin!(\"miniserve\"))\n        .arg(tmpdir.path())\n        .arg(\"-p\")\n        .arg(\"0\")\n        .stdout(Stdio::piped())\n        .spawn()?;\n\n    sleep(Duration::from_secs(1));\n    child.kill()?;\n\n    let output = child.wait_with_output().expect(\"Failed to read stdout\");\n    let all_text = String::from_utf8(output.stdout)?;\n\n    let re = Regex::new(r\"http://127.0.0.1:(\\d+)\").unwrap();\n    let caps = re.captures(all_text.as_str()).unwrap();\n    let port_num = caps.get(1).unwrap().as_str().parse::<u16>().unwrap();\n\n    assert!(port_num > 0);\n\n    Ok(())\n}\n\n#[rstest]\nfn serves_requests_custom_index_notice(tmpdir: TempDir, port: u16) -> Result<(), Error> {\n    let mut child = Command::new(cargo::cargo_bin!(\"miniserve\"))\n        .arg(\"--index=not.html\")\n        .arg(\"-p\")\n        .arg(port.to_string())\n        .arg(tmpdir.path())\n        .stdout(Stdio::piped())\n        .stderr(Stdio::piped())\n        .spawn()?;\n\n    sleep(Duration::from_secs(1));\n\n    child.kill()?;\n    let output = child.wait_with_output().expect(\"Failed to read stdout\");\n    let all_text = String::from_utf8(output.stdout);\n\n    assert!(\n        all_text?.contains(\"The file 'not.html' provided for option --index could not be found.\")\n    );\n\n    Ok(())\n}\n\n#[rstest]\n#[case(server(&[\"--index\", FILES[0]]))]\n#[case(server(&[\"--index\", \"does-not-exist.html\"]))]\nfn index_fallback_to_listing(\n    #[case] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    // If index file is not found, show directory listing instead both cases should return `Ok`\n    reqwest_client\n        .get(server.url())\n        .send()?\n        .error_for_status()?;\n\n    Ok(())\n}\n\n#[rstest]\n#[case(server(&[\"--spa\", \"--index\", FILES[0]]), \"/\")]\n#[case(server(&[\"--spa\", \"--index\", FILES[0]]), \"/spa-route\")]\n#[case(server(&[\"--index\", FILES[0]]), \"/\")]\nfn serve_index_instead_of_404_in_spa_mode(\n    #[case] server: TestServer,\n    reqwest_client: Client,\n    #[case] url: &str,\n) -> Result<(), Error> {\n    let body = reqwest_client\n        .get(format!(\"{}{}\", server.url(), url))\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n    assert!(\n        parsed\n            .find(|x: &Node| x.text() == \"Test Hello Yes\")\n            .next()\n            .is_some()\n    );\n\n    Ok(())\n}\n\n#[rstest]\n#[case(server(&[\"--pretty-urls\", \"--index\", FILES[1]]), \"/\")]\n#[case(server(&[\"--pretty-urls\", \"--index\", FILES[1]]), \"test.html\")]\n#[case(server(&[\"--pretty-urls\", \"--index\", FILES[1]]), \"test\")]\nfn serve_file_instead_of_404_in_pretty_urls_mode(\n    #[case] server: TestServer,\n    reqwest_client: Client,\n    #[case] url: &str,\n) -> Result<(), Error> {\n    let body = reqwest_client\n        .get(format!(\"{}{}\", server.url(), url))\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n    assert!(\n        parsed\n            .find(|x: &Node| x.text() == \"Test Hello Yes\")\n            .next()\n            .is_some()\n    );\n\n    Ok(())\n}\n\n#[rstest]\n#[case(server(&[\"--route-prefix\", \"foobar\"]))]\n#[case(server(&[\"--route-prefix\", \"/foobar/\"]))]\nfn serves_requests_with_route_prefix(\n    #[case] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let url_without_route = server.url();\n    let status = reqwest_client.get(url_without_route).send()?.status();\n    assert_eq!(status, StatusCode::NOT_FOUND);\n\n    let url_with_route = server.url().join(\"foobar\")?;\n    let status = reqwest_client.get(url_with_route).send()?.status();\n    assert_eq!(status, StatusCode::OK);\n\n    Ok(())\n}\n\n#[rstest]\n#[case(server(&[] as &[&str]), \"/__miniserve_internal/[a-z.]+\")]\n#[case(server(&[\"--random-route\"]), \"/__miniserve_internal/[a-z.]+\")]\n#[case(server(&[\"--route-prefix\", \"foobar\"]), \"/foobar/__miniserve_internal/[a-z.]+\")]\nfn serves_requests_static_file_check(\n    #[case] server: TestServer,\n    reqwest_client: Client,\n    #[case] static_file_pattern: String,\n) -> Result<(), Error> {\n    let body = reqwest_client.get(server.url()).send()?;\n    let parsed = Document::from_read(body)?;\n    let re = Regex::new(&static_file_pattern).unwrap();\n\n    assert!(\n        parsed\n            .find(Attr(\"rel\", \"stylesheet\"))\n            .all(|x| re.is_match(x.attr(\"href\").unwrap()))\n    );\n    assert!(\n        parsed\n            .find(Attr(\"rel\", \"icon\"))\n            .all(|x| re.is_match(x.attr(\"href\").unwrap()))\n    );\n\n    Ok(())\n}\n\n#[rstest]\n#[case(server(&[\"--disable-indexing\"]))]\nfn serves_no_directory_if_indexing_disabled(\n    #[case] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let body = reqwest_client.get(server.url()).send()?;\n    assert_eq!(body.status(), StatusCode::NOT_FOUND);\n    let parsed = Document::from_read(body)?;\n\n    assert!(\n        parsed\n            .find(|x: &Node| x.text() == FILES[0])\n            .next()\n            .is_none()\n    );\n    assert!(\n        parsed\n            .find(|x: &Node| x.text() == DIRECTORIES[0])\n            .next()\n            .is_none()\n    );\n    assert!(\n        parsed\n            .find(|x: &Node| x.text() == \"404 Not Found\")\n            .next()\n            .is_some()\n    );\n    assert!(\n        parsed\n            .find(|x: &Node| x.text() == \"File not found.\")\n            .next()\n            .is_some()\n    );\n\n    Ok(())\n}\n\n#[rstest]\n#[case(server(&[\"--disable-indexing\"]))]\nfn serves_file_requests_when_indexing_disabled(\n    #[case] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    reqwest_client\n        .get(format!(\"{}{}\", server.url(), FILES[0]))\n        .send()?\n        .error_for_status()?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "tests/tls.rs",
    "content": "use assert_cmd::{Command, cargo};\nuse predicates::str::contains;\nuse reqwest::blocking::Client;\nuse rstest::rstest;\nuse select::{document::Document, node::Node};\n\nmod fixtures;\n\nuse crate::fixtures::{Error, FILES, TestServer, reqwest_client, server};\n\n/// Can start the server with TLS and receive encrypted responses.\n#[rstest]\n#[case(server(&[\n        \"--tls-cert\", \"tests/data/cert_rsa.pem\",\n        \"--tls-key\", \"tests/data/key_pkcs8.pem\",\n]))]\n#[case(server(&[\n        \"--tls-cert\", \"tests/data/cert_rsa.pem\",\n        \"--tls-key\", \"tests/data/key_pkcs1.pem\",\n]))]\n#[case(server(&[\n        \"--tls-cert\", \"tests/data/cert_ec.pem\",\n        \"--tls-key\", \"tests/data/key_ec.pem\",\n]))]\nfn tls_works(#[case] server: TestServer, reqwest_client: Client) -> Result<(), Error> {\n    let body = reqwest_client\n        .get(server.url())\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n    for &file in FILES {\n        assert!(parsed.find(|x: &Node| x.text() == file).next().is_some());\n    }\n\n    Ok(())\n}\n\n/// Wrong path for cert throws error.\n#[rstest]\nfn wrong_path_cert() -> Result<(), Error> {\n    Command::new(cargo::cargo_bin!(\"miniserve\"))\n        .args([\"--tls-cert\", \"wrong\", \"--tls-key\", \"tests/data/key.pem\"])\n        .assert()\n        .failure()\n        .stderr(contains(\"Error: Couldn't access TLS certificate \\\"wrong\\\"\"));\n\n    Ok(())\n}\n\n/// Wrong paths for key throws errors.\n#[rstest]\nfn wrong_path_key() -> Result<(), Error> {\n    Command::new(cargo::cargo_bin!(\"miniserve\"))\n        .args([\"--tls-cert\", \"tests/data/cert.pem\", \"--tls-key\", \"wrong\"])\n        .assert()\n        .failure()\n        .stderr(contains(\"Error: Couldn't access TLS key \\\"wrong\\\"\"));\n\n    Ok(())\n}\n"
  },
  {
    "path": "tests/upload_files.rs",
    "content": "use std::fs::create_dir_all;\nuse std::path::Path;\n\nuse assert_fs::fixture::TempDir;\nuse reqwest::blocking::{Client, multipart};\nuse reqwest::header::HeaderMap;\nuse rstest::rstest;\nuse select::document::Document;\nuse select::predicate::{Attr, Text};\n\nmod fixtures;\n\nuse crate::fixtures::{Error, TestServer, reqwest_client, server, tmpdir};\n\n// Generate the hashes using the following\n// ```bash\n// $ sha256 -s 'this should be uploaded'\n// $ sha512 -s 'this should be uploaded'\n// ```\n#[rstest]\n#[case::no_hash(None, None)]\n#[case::only_hash(None, Some(\"test\"))]\n#[case::partial_sha256_hash(Some(\"SHA256\"), None)]\n#[case::partial_sha512_hash(Some(\"SHA512\"), None)]\n#[case::sha256_hash(\n    Some(\"SHA256\"),\n    Some(\"e37b14e22e7b3f50dadaf821c189af80f79b1f39fd5a8b3b4f536103735d4620\")\n)]\n#[case::sha512_hash(\n    Some(\"SHA512\"),\n    Some(\n        \"03bcfc52c53904e34e06b95e8c3ee1275c66960c441417892e977d52687e28afae85b6039509060ee07da739e4e7fc3137acd142162c1456f723604f8365e154\"\n    )\n)]\nfn uploading_files_works(\n    #[with(&[\"-u\"])] server: TestServer,\n    reqwest_client: Client,\n    #[case] sha_func: Option<&str>,\n    #[case] sha: Option<&str>,\n) -> Result<(), Error> {\n    let test_file_name = \"uploaded test file.txt\";\n\n    // Before uploading, check whether the uploaded file does not yet exist.\n    let body = reqwest_client\n        .get(server.url())\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n    assert!(parsed.find(Text).all(|x| x.text() != test_file_name));\n\n    // Perform the actual upload.\n    let upload_action = parsed\n        .find(Attr(\"id\", \"file_submit\"))\n        .next()\n        .expect(\"Couldn't find element with id=file_submit\")\n        .attr(\"action\")\n        .expect(\"Upload form doesn't have action attribute\");\n    let form = multipart::Form::new();\n    let part = multipart::Part::text(\"this should be uploaded\")\n        .file_name(test_file_name)\n        .mime_str(\"text/plain\")?;\n    let form = form.part(\"file_to_upload\", part);\n\n    let mut headers = HeaderMap::new();\n    if let Some(sha_func) = sha_func.as_ref() {\n        headers.insert(\"X-File-Hash-Function\", sha_func.parse()?);\n    }\n    if let Some(sha) = sha.as_ref() {\n        headers.insert(\"X-File-Hash\", sha.parse()?);\n    }\n\n    reqwest_client\n        .post(server.url().join(upload_action)?)\n        .headers(headers)\n        .multipart(form)\n        .send()?\n        .error_for_status()?;\n\n    // After uploading, check whether the uploaded file is now getting listed.\n    let body = reqwest_client.get(server.url()).send()?;\n    let parsed = Document::from_read(body)?;\n    assert!(parsed.find(Text).any(|x| x.text() == test_file_name));\n\n    Ok(())\n}\n\n#[rstest]\nfn uploading_files_is_prevented(server: TestServer, reqwest_client: Client) -> Result<(), Error> {\n    let test_file_name = \"uploaded test file.txt\";\n\n    // Before uploading, check whether the uploaded file does not yet exist.\n    let body = reqwest_client\n        .get(server.url())\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n    assert!(parsed.find(Text).all(|x| x.text() != test_file_name));\n\n    // Ensure the file upload form is not present\n    assert!(parsed.find(Attr(\"id\", \"file_submit\")).next().is_none());\n\n    // Then try to upload anyway\n    let form = multipart::Form::new();\n    let part = multipart::Part::text(\"this should not be uploaded\")\n        .file_name(test_file_name)\n        .mime_str(\"text/plain\")?;\n    let form = form.part(\"file_to_upload\", part);\n\n    // Ensure uploading fails and returns an error\n    assert!(\n        reqwest_client\n            .post(server.url().join(\"/upload?path=/\")?)\n            .multipart(form)\n            .send()?\n            .error_for_status()\n            .is_err()\n    );\n\n    // After uploading, check whether the uploaded file is NOT getting listed.\n    let body = reqwest_client.get(server.url()).send()?;\n    let parsed = Document::from_read(body)?;\n    assert!(!parsed.find(Text).any(|x| x.text() == test_file_name));\n\n    Ok(())\n}\n\n// Generated hashs with the following\n// ```bash\n// echo \"invalid\" | base64 | sha256\n// echo \"invalid\" | base64 | sha512\n// ```\n#[rstest]\n#[case::sha256_hash(\n    Some(\"SHA256\"),\n    Some(\"f4ddf641a44e8fe8248cc086532cafaa8a914a21a937e40be67926ea074b955a\")\n)]\n#[case::sha512_hash(\n    Some(\"SHA512\"),\n    Some(\n        \"d3fe39ab560dd7ba91e6e2f8c948066d696f2afcfc90bf9df32946512f6934079807f301235b88b72bf746b6a88bf111bc5abe5c711514ed0731d286985297ba\"\n    )\n)]\n#[case::sha128_hash(Some(\"SHA128\"), Some(\"invalid\"))]\nfn uploading_files_with_invalid_sha_func_is_prevented(\n    #[with(&[\"-u\"])] server: TestServer,\n    reqwest_client: Client,\n    #[case] sha_func: Option<&str>,\n    #[case] sha: Option<&str>,\n) -> Result<(), Error> {\n    let test_file_name = \"uploaded test file.txt\";\n\n    // Before uploading, check whether the uploaded file does not yet exist.\n    let body = reqwest_client\n        .get(server.url())\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n    assert!(parsed.find(Text).all(|x| x.text() != test_file_name));\n\n    // Perform the actual upload.\n    let form = multipart::Form::new();\n    let part = multipart::Part::text(\"this should be uploaded\")\n        .file_name(test_file_name)\n        .mime_str(\"text/plain\")?;\n    let form = form.part(\"file_to_upload\", part);\n\n    let mut headers = HeaderMap::new();\n    if let Some(sha_func) = sha_func.as_ref() {\n        headers.insert(\"X-File-Hash-Function\", sha_func.parse()?);\n    }\n    if let Some(sha) = sha.as_ref() {\n        headers.insert(\"X-File-Hash\", sha.parse()?);\n    }\n\n    assert!(\n        reqwest_client\n            .post(server.url().join(\"/upload?path=/\")?)\n            .headers(headers)\n            .multipart(form)\n            .send()?\n            .error_for_status()\n            .is_err()\n    );\n\n    // After uploading, check whether the uploaded file is NOT getting listed.\n    let body = reqwest_client.get(server.url()).send()?;\n    let parsed = Document::from_read(body)?;\n    assert!(!parsed.find(Text).any(|x| x.text() == test_file_name));\n\n    Ok(())\n}\n\n/// This test runs the server with --allowed-upload-dir argument and\n/// checks that file upload to a different directory is actually prevented.\n#[rstest]\n#[case(server(&[\"-u\", \"someDir\"]))]\n#[case(server(&[\"-u\", \"someDir/some_sub_dir\"]))]\nfn uploading_files_is_restricted(\n    #[case] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let test_file_name = \"uploaded test file.txt\";\n\n    // Then try to upload file to root directory (which is not the --allowed-upload-dir)\n    let form = multipart::Form::new();\n    let part = multipart::Part::text(\"this should not be uploaded\")\n        .file_name(test_file_name)\n        .mime_str(\"text/plain\")?;\n    let form = form.part(\"file_to_upload\", part);\n\n    // Ensure uploading fails and returns an error\n    assert_eq!(\n        403,\n        reqwest_client\n            .post(server.url().join(\"/upload?path=/\")?)\n            .multipart(form)\n            .send()?\n            .status()\n    );\n\n    // After uploading, check whether the uploaded file is NOT getting listed.\n    let body = reqwest_client.get(server.url()).send()?;\n    let parsed = Document::from_read(body)?;\n    assert!(!parsed.find(Text).any(|x| x.text() == test_file_name));\n\n    Ok(())\n}\n\n/// This tests that we can upload files to the directory specified by --allow-upload-dir\n#[rstest]\n#[case(server(&[\"-u\", \"someDir\"]), vec![\"someDir\"])]\n#[case(server(&[\"-u\", \"./-someDir\"]), vec![\"./-someDir\"])]\n#[case(server(&[\"-u\", Path::new(\"someDir/some_sub_dir\").to_str().unwrap()]),\n  vec![\"someDir/some_sub_dir\"])]\n#[case(server(&[\"-u\", Path::new(\"someDir/some_sub_dir\").to_str().unwrap(),\n                \"-u\", Path::new(\"someDir/some_other_dir\").to_str().unwrap()]),\n       vec![\"someDir/some_sub_dir\", \"someDir/some_other_dir\"])]\nfn uploading_files_to_allowed_dir_works(\n    #[case] server: TestServer,\n    reqwest_client: Client,\n    #[case] upload_dirs: Vec<&str>,\n) -> Result<(), Error> {\n    let test_file_name = \"uploaded test file.txt\";\n\n    for upload_dir in upload_dirs {\n        // Create test directory\n        create_dir_all(server.path().join(Path::new(upload_dir))).unwrap();\n\n        // Before uploading, check whether the uploaded file does not yet exist.\n        let body = reqwest_client\n            .get(server.url().join(upload_dir)?)\n            .send()?\n            .error_for_status()?;\n        let parsed = Document::from_read(body)?;\n        assert!(parsed.find(Text).all(|x| x.text() != test_file_name));\n\n        // Perform the actual upload.\n        let upload_action = parsed\n            .find(Attr(\"id\", \"file_submit\"))\n            .next()\n            .expect(\"Couldn't find element with id=file_submit\")\n            .attr(\"action\")\n            .expect(\"Upload form doesn't have action attribute\");\n        let form = multipart::Form::new();\n        let part = multipart::Part::text(\"this should be uploaded\")\n            .file_name(test_file_name)\n            .mime_str(\"text/plain\")?;\n        let form = form.part(\"file_to_upload\", part);\n\n        reqwest_client\n            .post(server.url().join(upload_action)?)\n            .multipart(form)\n            .send()?\n            .error_for_status()?;\n\n        // After uploading, check whether the uploaded file is now getting listed.\n        let body = reqwest_client.get(server.url().join(upload_dir)?).send()?;\n        let parsed = Document::from_read(body)?;\n        assert!(parsed.find(Text).any(|x| x.text() == test_file_name));\n    }\n    Ok(())\n}\n\n#[rstest]\n#[case(server(&[\"-u\"]))]\n#[case(server(&[\"-u\", \"-o\", \"error\"]))]\n#[case(server(&[\"-u\", \"--on-duplicate-files\", \"error\"]))]\nfn uploading_duplicate_file_is_prevented(\n    #[case] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let test_file_name = \"duplicate test file.txt\";\n    let test_file_contents = \"Test File Contents\";\n    let test_file_contents_new = \"New Uploaded Test File Contents\";\n\n    // create the file\n    let test_file_path = server.path().join(test_file_name);\n    std::fs::write(&test_file_path, test_file_contents)?;\n\n    // Before uploading, make sure the file is there.\n    let body = reqwest_client\n        .get(server.url())\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n    assert!(parsed.find(Text).any(|x| x.text() == test_file_name));\n\n    // Perform the actual upload.\n    let upload_action = parsed\n        .find(Attr(\"id\", \"file_submit\"))\n        .next()\n        .expect(\"Couldn't find element with id=file_submit\")\n        .attr(\"action\")\n        .expect(\"Upload form doesn't have action attribute\");\n    // Then try to upload anyway\n    let form = multipart::Form::new();\n    let part = multipart::Part::text(test_file_contents_new)\n        .file_name(test_file_name)\n        .mime_str(\"text/plain\")?;\n    let form = form.part(\"file_to_upload\", part);\n\n    // Ensure uploading fails and returns an error\n    assert!(\n        reqwest_client\n            .post(server.url().join(upload_action)?)\n            .multipart(form)\n            .send()?\n            .error_for_status()\n            .is_err()\n    );\n\n    // After uploading, uploaded file is still getting listed.\n    let body = reqwest_client.get(server.url()).send()?;\n    let parsed = Document::from_read(body)?;\n    assert!(parsed.find(Text).any(|x| x.text() == test_file_name));\n    // and assert the contents is the same as before\n    assert_file_contents(&test_file_path, test_file_contents);\n\n    Ok(())\n}\n\n#[rstest]\n#[case(server(&[\"-u\", \"-o\", \"overwrite\"]))]\n#[case(server(&[\"-u\", \"--on-duplicate-files\", \"overwrite\"]))]\nfn overwrite_duplicate_file(\n    #[case] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let test_file_name = \"duplicate test file.txt\";\n    let test_file_contents = \"Test File Contents\";\n    let test_file_contents_new = \"New Uploaded Test File Contents\";\n\n    // create the file\n    let test_file_path = server.path().join(test_file_name);\n    let _ = std::fs::write(&test_file_path, test_file_contents);\n\n    // Before uploading, make sure the file is there.\n    let body = reqwest_client\n        .get(server.url())\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n    assert!(parsed.find(Text).any(|x| x.text() == test_file_name));\n\n    // Perform the actual upload.\n    let upload_action = parsed\n        .find(Attr(\"id\", \"file_submit\"))\n        .next()\n        .expect(\"Couldn't find element with id=file_submit\")\n        .attr(\"action\")\n        .expect(\"Upload form doesn't have action attribute\");\n    // Then try to upload anyway\n    let form = multipart::Form::new();\n    let part = multipart::Part::text(test_file_contents_new)\n        .file_name(test_file_name)\n        .mime_str(\"text/plain\")?;\n    let form = form.part(\"file_to_upload\", part);\n\n    reqwest_client\n        .post(server.url().join(upload_action)?)\n        .multipart(form)\n        .send()?\n        .error_for_status()?;\n\n    // After uploading, verify the listing has the file\n    let body = reqwest_client.get(server.url()).send()?;\n    let parsed = Document::from_read(body)?;\n    assert!(parsed.find(Text).any(|x| x.text() == test_file_name));\n    // and assert the contents is from recently uploaded file\n    assert_file_contents(&test_file_path, test_file_contents_new);\n\n    Ok(())\n}\n\n#[rstest]\n#[case(server(&[\"-u\", \"-o\", \"rename\"]))]\n#[case(server(&[\"-u\", \"--on-duplicate-files\", \"rename\"]))]\nfn rename_duplicate_file(#[case] server: TestServer, reqwest_client: Client) -> Result<(), Error> {\n    let test_file_name = \"duplicate test file.txt\";\n    let test_file_contents = \"Test File Contents\";\n    let test_file_name_new = \"duplicate test file-1.txt\";\n    let test_file_contents_new = \"New Uploaded Test File Contents\";\n\n    // create the file\n    let test_file_path = server.path().join(test_file_name);\n    let _ = std::fs::write(&test_file_path, test_file_contents);\n\n    // Before uploading, make sure the file is there.\n    let body = reqwest_client\n        .get(server.url())\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n    assert!(parsed.find(Text).any(|x| x.text() == test_file_name));\n\n    // Perform the actual upload.\n    let upload_action = parsed\n        .find(Attr(\"id\", \"file_submit\"))\n        .next()\n        .expect(\"Couldn't find element with id=file_submit\")\n        .attr(\"action\")\n        .expect(\"Upload form doesn't have action attribute\");\n    // Then try to upload anyway\n    let form = multipart::Form::new();\n    let part = multipart::Part::text(test_file_contents_new)\n        .file_name(test_file_name)\n        .mime_str(\"text/plain\")?;\n    let form = form.part(\"file_to_upload\", part);\n\n    reqwest_client\n        .post(server.url().join(upload_action)?)\n        .multipart(form)\n        .send()?\n        .error_for_status()?;\n\n    // After uploading, assert the old file is still getting listed, and the new file is also in listing\n    let body = reqwest_client.get(server.url()).send()?;\n    let parsed = Document::from_read(body)?;\n    assert!(parsed.find(Text).any(|x| x.text() == test_file_name));\n    assert!(parsed.find(Text).any(|x| x.text() == test_file_name_new));\n    // and assert the contents is the same as before for old file, and new contents for new file\n    assert_file_contents(&test_file_path, test_file_contents);\n    assert_file_contents(\n        &server.path().join(test_file_name_new),\n        test_file_contents_new,\n    );\n\n    Ok(())\n}\n\n/// Test for path traversal vulnerability (CWE-22) in both path parameter of query string and in\n/// file name (Content-Disposition)\n///\n/// see: https://github.com/svenstaro/miniserve/issues/518\n#[rstest]\n#[case(\"foo\", \"bar\", \"foo/bar\")]\n#[case(\"/../foo\", \"bar\", \"foo/bar\")]\n#[case(\"/foo\", \"/../bar\", \"foo/bar\")]\n#[case(\"C:/foo\", \"C:/bar\", if cfg!(windows) { \"foo/bar\" } else { \"C:/foo/C:/bar\" })]\n#[case(r\"C:\\foo\", r\"C:\\bar\", if cfg!(windows) { \"foo/bar\" } else { r\"C:\\foo/C:\\bar\" })]\n#[case(r\"\\foo\", r\"\\..\\bar\", if cfg!(windows) { \"foo/bar\" } else { r\"\\foo/\\..\\bar\" })]\nfn prevent_path_traversal_attacks(\n    #[with(&[\"-u\"])] server: TestServer,\n    reqwest_client: Client,\n    #[case] path: &str,\n    #[case] filename: &'static str,\n    #[case] expected: &str,\n) -> Result<(), Error> {\n    // Create test directories\n    create_dir_all(server.path().join(\"foo\")).unwrap();\n    if !cfg!(windows) {\n        for dir in &[\"C:/foo/C:\", r\"C:\\foo\", r\"\\foo\"] {\n            create_dir_all(server.path().join(dir))\n                .unwrap_or_else(|_| panic!(\"failed to create: {dir:?}\"));\n        }\n    }\n\n    let expected_path = server.path().join(expected);\n    assert!(!expected_path.exists());\n\n    // Perform the actual upload.\n    let part = multipart::Part::text(\"this should be uploaded\")\n        .file_name(filename)\n        .mime_str(\"text/plain\")?;\n    let form = multipart::Form::new().part(\"file_to_upload\", part);\n\n    reqwest_client\n        .post(server.url().join(&format!(\"/upload?path={path}\"))?)\n        .multipart(form)\n        .send()?\n        .error_for_status()?;\n\n    // Make sure that the file was uploaded to the expected path\n    assert!(expected_path.exists());\n\n    Ok(())\n}\n\n/// Test uploading to symlink directories that point outside the server root.\n/// See https://github.com/svenstaro/miniserve/issues/466\n#[rstest]\n#[case(server(&[\"-u\"]), true)]\n#[case(server(&[\"-u\", \"--no-symlinks\"]), false)]\nfn upload_to_symlink_directory(\n    #[case] server: TestServer,\n    reqwest_client: Client,\n    #[case] ok: bool,\n    tmpdir: TempDir,\n) -> Result<(), Error> {\n    #[cfg(unix)]\n    use std::os::unix::fs::symlink as symlink_dir;\n    #[cfg(windows)]\n    use std::os::windows::fs::symlink_dir;\n\n    // Create symlink directory \"foo\" to point outside the root\n    let (dir, filename) = (\"foo\", \"bar\");\n    symlink_dir(tmpdir.path(), server.path().join(dir)).unwrap();\n\n    let full_path = server.path().join(dir).join(filename);\n    assert!(!full_path.exists());\n\n    // Try to upload\n    let part = multipart::Part::text(\"this should be uploaded\")\n        .file_name(filename)\n        .mime_str(\"text/plain\")?;\n    let form = multipart::Form::new().part(\"file_to_upload\", part);\n\n    let status = reqwest_client\n        .post(server.url().join(&format!(\"/upload?path={dir}\"))?)\n        .multipart(form)\n        .send()?\n        .error_for_status();\n\n    // Make sure upload behave as expected\n    assert_eq!(status.is_ok(), ok);\n    assert_eq!(full_path.exists(), ok);\n\n    Ok(())\n}\n\n/// Test setting the HTML accept attribute using -m and -M.\n#[rstest]\n#[case(server(&[\"-u\"]), None)]\n#[case(server(&[\"-u\", \"-m\", \"image\"]), Some(\"image/*\"))]\n#[case(server(&[\"-u\", \"-m\", \"image\", \"-m\", \"audio\", \"-m\", \"video\"]), Some(\"image/*,audio/*,video/*\"))]\n#[case(server(&[\"-u\", \"-m\", \"audio\", \"-m\", \"image\", \"-m\", \"video\"]), Some(\"audio/*,image/*,video/*\"))]\n#[case(server(&[\"-u\", \"-M\", \"test_value\"]), Some(\"test_value\"))]\nfn set_media_type(\n    #[case] server: TestServer,\n    reqwest_client: Client,\n    #[case] expected_accept_value: Option<&str>,\n) -> Result<(), Error> {\n    let body = reqwest_client\n        .get(server.url())\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n\n    let input = parsed.find(Attr(\"id\", \"file-input\")).next().unwrap();\n    assert_eq!(input.attr(\"accept\"), expected_accept_value);\n\n    Ok(())\n}\n\nfn assert_file_contents(file_path: &Path, contents: &str) {\n    let file_contents = std::fs::read_to_string(file_path).unwrap();\n    assert!(file_contents == contents)\n}\n\n/// Test --chmod change file permissions as intended\n#[cfg(unix)]\n#[rstest]\n#[case(server(&[\"-u\", \"--chmod\", \"660\"]), 0o660)]\n#[case(server(&[\"-u\", \"--chmod\", \"644\"]), 0o644)]\n#[case(server(&[\"-u\", \"--chmod\", \"0600\"]), 0o600)]\nfn chmod(\n    #[case] server: TestServer,\n    reqwest_client: Client,\n    #[case] expected_mode: u16,\n) -> Result<(), Error> {\n    let test_file_name = \"chmod-file.txt\";\n    let test_file_contents = \"Test File Contents\";\n\n    // Perform the actual upload.\n    let body = reqwest_client\n        .get(server.url())\n        .send()?\n        .error_for_status()?;\n    let parsed = Document::from_read(body)?;\n    let upload_action = parsed\n        .find(Attr(\"id\", \"file_submit\"))\n        .next()\n        .expect(\"Couldn't find element with id=file_submit\")\n        .attr(\"action\")\n        .expect(\"Upload form doesn't have action attribute\");\n\n    let form = multipart::Form::new();\n    let part = multipart::Part::text(test_file_contents)\n        .file_name(test_file_name)\n        .mime_str(\"text/plain\")?;\n    let form = form.part(\"file_to_upload\", part);\n\n    reqwest_client\n        .post(server.url().join(upload_action)?)\n        .multipart(form)\n        .send()?\n        .error_for_status()?;\n\n    // assert the mode of file\n    use std::os::unix::fs::MetadataExt;\n    let test_file_path = server.path().join(test_file_name);\n    let meta = std::fs::metadata(&test_file_path)?;\n    // the returned mode has filetype in it\n    let mode = meta.mode() & 0o7777;\n    assert_eq!(mode as u16, expected_mode);\n\n    Ok(())\n}\n"
  },
  {
    "path": "tests/utils/mod.rs",
    "content": "use select::document::Document;\nuse select::node::Node;\nuse select::predicate::Name;\nuse select::predicate::Predicate;\n\n/// Return the href attribute content for the closest anchor found by `text`.\npub fn get_link_from_text(document: &Document, text: &str) -> Option<String> {\n    let a_elem = document\n        .find(Name(\"a\").and(|x: &Node| x.children().any(|x| x.text() == text)))\n        .next()?;\n    Some(a_elem.attr(\"href\")?.to_string())\n}\n\n/// Return the href attributes of all links that start with the specified `prefix`.\npub fn get_link_hrefs_with_prefix(document: &Document, prefix: &str) -> Vec<String> {\n    let mut vec: Vec<String> = Vec::new();\n\n    let a_elements = document.find(Name(\"a\"));\n\n    for element in a_elements {\n        let s = element.attr(\"href\").unwrap_or(\"\");\n        if s.to_string().starts_with(prefix) {\n            vec.push(s.to_string());\n        }\n    }\n\n    vec\n}\n"
  },
  {
    "path": "tests/webdav.rs",
    "content": "use std::process::Command;\n\nuse assert_cmd::{cargo, prelude::*};\nuse assert_fs::TempDir;\nuse predicates::str::contains;\nuse reqwest::{Method, blocking::Client};\nuse reqwest_dav::{\n    ClientBuilder as DavClientBuilder,\n    list_cmd::{ListEntity, ListFile, ListFolder},\n};\nuse rstest::rstest;\n\nmod fixtures;\n\nuse crate::fixtures::{\n    DIR_BEHIND_SYMLINKED_DIR, DIRECTORIES, DIRECTORY_SYMLINK, Error,\n    FILE_IN_DIR_BEHIND_SYMLINKED_DIR, FILE_SYMLINK, FILES, HIDDEN_DIRECTORIES, HIDDEN_FILES,\n    TestServer, reqwest_client, server, tmpdir,\n};\n\n#[rstest]\n#[case(server(&[\"--enable-webdav\"]), true)]\n#[case(server(&[] as &[&str]), false)]\nfn webdav_flag_works(\n    #[case] server: TestServer,\n    reqwest_client: Client,\n    #[case] should_respond: bool,\n) -> Result<(), Error> {\n    let response = reqwest_client\n        .request(Method::from_bytes(b\"PROPFIND\").unwrap(), server.url())\n        .header(\"Depth\", \"1\")\n        .send()?;\n\n    assert_eq!(should_respond, response.status().is_success());\n\n    Ok(())\n}\n\n#[rstest]\nfn webdav_advertised_in_options(\n    #[with(&[\"--enable-webdav\"])] server: TestServer,\n    reqwest_client: Client,\n) -> Result<(), Error> {\n    let response = reqwest_client\n        .request(Method::OPTIONS, server.url())\n        .send()?\n        .error_for_status()?;\n\n    let headers = response.headers();\n    let allow = headers.get(\"allow\").unwrap().to_str()?;\n\n    assert!(allow.contains(\"OPTIONS\") && allow.contains(\"PROPFIND\"));\n    assert!(headers.get(\"dav\").is_some());\n\n    Ok(())\n}\n\nfn list_webdav(url: url::Url, path: &str) -> Result<Vec<ListEntity>, reqwest_dav::Error> {\n    // Make sure that tests using this can run in isolation. For this, we need to make sure\n    // that the crypto provider for rustls is initialized.\n    if rustls::crypto::CryptoProvider::get_default().is_none() {\n        let _ = rustls::crypto::ring::default_provider().install_default();\n    }\n\n    let client = DavClientBuilder::new().set_host(url.to_string()).build()?;\n\n    let rt = tokio::runtime::Runtime::new().unwrap();\n\n    rt.block_on(async { client.list(path, reqwest_dav::Depth::Number(1)).await })\n}\n\n#[rstest]\n#[case(server(&[\"--enable-webdav\"]), false)]\n#[case(server(&[\"--enable-webdav\", \"--hidden\"]), true)]\nfn webdav_respects_hidden_flag(\n    #[case] server: TestServer,\n    #[case] hidden_should_show: bool,\n) -> Result<(), Error> {\n    let list = list_webdav(server.url(), \"/\")?;\n\n    assert_eq!(\n        hidden_should_show,\n        list.iter().any(|el|\n            matches!(el, ListEntity::File(ListFile { href, .. }) if href.contains(HIDDEN_FILES[0]))\n        )\n    );\n\n    assert_eq!(\n        hidden_should_show,\n        list.iter().any(|el|\n            matches!(el, ListEntity::Folder(ListFolder { href, .. }) if href.contains(HIDDEN_DIRECTORIES[0]))\n        )\n    );\n\n    Ok(())\n}\n\n#[rstest]\n#[case(server(&[\"--enable-webdav\"]), true)]\n#[case(server(&[\"--enable-webdav\", \"--no-symlinks\"]), false)]\nfn webdav_respects_no_symlink_flag(#[case] server: TestServer, #[case] symlinks_should_show: bool) {\n    let list = list_webdav(server.url(), \"/\").unwrap();\n\n    assert_eq!(\n        symlinks_should_show,\n        list.iter().any(|el|\n            matches!(el, ListEntity::File(ListFile { href, .. }) if href.contains(FILE_SYMLINK))\n        ),\n    );\n\n    assert_eq!(\n        symlinks_should_show,\n        list.iter().any(|el|\n            matches!(el, ListEntity::Folder(ListFolder { href, .. }) if href.contains(DIRECTORY_SYMLINK))\n        ),\n    );\n\n    let list_linked = list_webdav(server.url(), &format!(\"/{DIRECTORY_SYMLINK}\"));\n    assert_eq!(symlinks_should_show, list_linked.is_ok());\n\n    let list_nested_dir = list_webdav(server.url(), &format!(\"/{DIR_BEHIND_SYMLINKED_DIR}\"));\n    assert_eq!(symlinks_should_show, list_nested_dir.is_ok());\n\n    let list_nested_file = list_webdav(\n        server.url(),\n        &format!(\"/{FILE_IN_DIR_BEHIND_SYMLINKED_DIR}\"),\n    );\n    assert_eq!(symlinks_should_show, list_nested_file.is_ok());\n}\n\n#[rstest]\nfn webdav_works_with_route_prefix(\n    #[with(&[\"--enable-webdav\", \"--route-prefix\", \"test-prefix\"])] server: TestServer,\n) -> Result<(), Error> {\n    let prefixed_list = list_webdav(server.url().join(\"test-prefix\")?, \"/\")?;\n\n    assert!(\n        prefixed_list.iter().any(|el|\n            matches!(el, ListEntity::Folder(ListFolder { href, .. }) if href.contains(DIRECTORIES[0]))\n        )\n    );\n\n    let root_list = list_webdav(server.url(), \"/\");\n\n    assert!(root_list.is_err());\n\n    Ok(())\n}\n\n// timeout is used in case the binary does not exit as expected and starts waiting for requests\n#[rstest]\n#[timeout(std::time::Duration::from_secs(1))]\nfn webdav_single_file_refuses_starting(tmpdir: TempDir) {\n    Command::new(cargo::cargo_bin!(\"miniserve\"))\n        .current_dir(tmpdir.path())\n        .arg(FILES[0])\n        .arg(\"--enable-webdav\")\n        .assert()\n        .failure()\n        .stderr(contains(format!(\n            \"Error: The --enable-webdav option was provided, but the serve path '{}' is a file\",\n            FILES[0]\n        )));\n}\n"
  }
]