diff --git a/.dockerignore b/.dockerignore index e02d7a7..1142ba2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,5 @@ target/ -.github/ .git/ +.github/ +*.md Dockerfile diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..874bfc2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +# Markdown uses trailing whitespace for hard line breaks +[*.md] +trim_trailing_whitespace = false + +# Makefile recipes must be indented with real tabs +[Makefile] +indent_style = tab diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md deleted file mode 100644 index e69de29..0000000 diff --git a/.github/README.md b/.github/README.md index c7a98bc..a345bce 100644 --- a/.github/README.md +++ b/.github/README.md @@ -283,253 +283,23 @@ Below is an outline + current status of each workflow. | Workflow | Status | |----------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Build Docker image and push to registry | [![Build Docker Image 🐳](https://github.com/Lissy93/AdGuardian-Term/actions/workflows/build-docker.yml/badge.svg)](https://github.com/Lissy93/AdGuardian-Term/actions/workflows/build-docker.yml) | -| Compile binaries and upload artifacts to release | [![Compile Release πŸš€](https://github.com/Lissy93/AdGuardian-Term/actions/workflows/release-binaries.yml/badge.svg)](https://github.com/Lissy93/AdGuardian-Term/actions/workflows/release-binaries.yml) | -| Publish compiled app to crates.io | [![Publish to Crates.io πŸ“¦](https://github.com/Lissy93/AdGuardian-Term/actions/workflows/push-cargo.yml/badge.svg)](https://github.com/Lissy93/AdGuardian-Term/actions/workflows/push-cargo.yml) | +| Run quality + security checks on each PR and push | [![CI πŸ§ͺ](https://github.com/Lissy93/AdGuardian-Term/actions/workflows/ci.yml/badge.svg)](https://github.com/Lissy93/AdGuardian-Term/actions/workflows/ci.yml) | +| Bump version and push git tag on merge to main | [![Tag πŸ”–](https://github.com/Lissy93/AdGuardian-Term/actions/workflows/tag.yml/badge.svg)](https://github.com/Lissy93/AdGuardian-Term/actions/workflows/tag.yml) | +| Compile binaries and draft a release on each tag | [![Release πŸš€](https://github.com/Lissy93/AdGuardian-Term/actions/workflows/release.yml/badge.svg)](https://github.com/Lissy93/AdGuardian-Term/actions/workflows/release.yml) | +| Publish compiled app to crates.io | [![Publish crates πŸ“¦](https://github.com/Lissy93/AdGuardian-Term/actions/workflows/cargo.yml/badge.svg)](https://github.com/Lissy93/AdGuardian-Term/actions/workflows/cargo.yml) | +| Build Docker image and push to registry | [![Docker 🐳](https://github.com/Lissy93/AdGuardian-Term/actions/workflows/docker.yml/badge.svg)](https://github.com/Lissy93/AdGuardian-Term/actions/workflows/docker.yml) | | Generate documentation from Rustdoc, upload to GH pages | [![Generate Rust Docs πŸ“](https://github.com/Lissy93/AdGuardian-Term/actions/workflows/publish-docs.yml/badge.svg)](https://github.com/Lissy93/AdGuardian-Term/actions/workflows/publish-docs.yml) | | Sync repo with downstream codeberg mirror | [![Mirror to Codeberg πŸͺž](https://github.com/Lissy93/AdGuardian-Term/actions/workflows/mirror.yml/badge.svg)](https://github.com/Lissy93/AdGuardian-Term/actions/workflows/mirror.yml) | -| Insert list of contributors + sponsors into readme | [![Insert Contributors πŸ‘₯](https://github.com/Lissy93/AdGuardian-Term/actions/workflows/insert-contributors.yml/badge.svg)](https://github.com/Lissy93/AdGuardian-Term/actions/workflows/insert-contributors.yml) | --- ## Credits ### Contributors - - - - - - - - - - - - - - - -
- - liss-bot -
- Alicia Bot -
-
- - lissy93 -
- Alicia Sykes -
-
- - CrazyWolf13 -
- Tobias -
-
- - sdjnmxd -
- Milkfish -
-
- - shtrophic -
- Christoph -
-
- - ciallo-agent -
- Ciallo ⸜(q˃ α΅• Λ‚ )⸝♑ -
-
- - kianmeng -
- Kian-Meng Ang -
-
- - maxschipper -
- Max Schipper -
-
- - kiuber -
- Kiuber -
-
- - tromcho -
- Null -
-
- +[![contributors badge](https://readme-contribs.as93.net/contributors/lissy93/AdGuardian-Term?shape=squircle)](https://github.com/lissy93/AdGuardian-Term/graphs/contributors) ### Sponsors - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - BrunoBernardino -
- Bruno Bernardino -
-
- - bile0026 -
- Zach Biles -
-
- - UlisesGascon -
- Ulises GascΓ³n -
-
- - nrjohnstone -
- Nathan Johnstone -
-
- - InDieTasten -
- Null -
-
- - araguaci -
- Null -
-
- - elvito -
- Null -
-
- - vlad-tim -
- Vlad -
-
- - helixzz -
- HeliXZz -
-
- - Daechler -
- Daechler -
-
- - getumbrel -
- Umbrel -
-
- - ssdnodes -
- SSD Nodes -
-
- - frankdez93 -
- Null -
-
- - BigoudOps -
- John BigoudOps -
-
- - terminaltrove -
- Terminal Trove -
-
- - MichaelPerron -
- Michael Perron -
-
- - hudsonrock-partnerships -
- Null -
-
- - crmzx8 -
- CRMX8 -
-
- - hesreallyhim -
- Really Him -
-
- - gl0bal01 -
- Gl0bal01 πŸ’– 龴ↀ◑ↀ龴 -
-
- - Envisage-Cloud-Solutions -
- Envisage Cloud Solutions -
-
- +[![sponsors badge](https://readme-contribs.as93.net/sponsors/lissy93?shape=squircle)](https://github.com/sponsors/lissy93) ### Dependencies @@ -637,4 +407,3 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | / | / ~-. ~- _ |_____| |_____| ~ - . _ _~_-_ --> - diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml deleted file mode 100644 index cf6f8c0..0000000 --- a/.github/workflows/build-docker.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Build + Publish Docker Image 🐳 - -on: - workflow_dispatch: - push: - branches: - - main - tags: - - '*' - paths: - - src/** - - Cargo.toml - - Dockerfile - -env: - IMAGE_NAME: adguardian - DOCKER_USER: lissy93 - GHCR_REGISTRY: ghcr.io - DOCKERHUB_REGISTRY: docker.io - -jobs: - docker: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Extract tag name - shell: bash - run: echo "GIT_TAG=$(echo ${GITHUB_REF#refs/tags/} | sed 's/\//_/g')" >> $GITHUB_ENV - - - name: Compute tags - id: compute-tags - run: | - if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then - echo "GHCR_TAG=${GHCR_REGISTRY}/${DOCKER_USER}/${IMAGE_NAME}:latest" >> $GITHUB_ENV - echo "DOCKERHUB_TAG=${DOCKERHUB_REGISTRY}/${DOCKER_USER}/${IMAGE_NAME}:latest" >> $GITHUB_ENV - else - echo "GHCR_TAG=${GHCR_REGISTRY}/${DOCKER_USER}/${IMAGE_NAME}:${GIT_TAG}" >> $GITHUB_ENV - echo "DOCKERHUB_TAG=${DOCKERHUB_REGISTRY}/${DOCKER_USER}/${IMAGE_NAME}:${GIT_TAG}" >> $GITHUB_ENV - fi - - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v1 - with: - registry: ${{ env.GHCR_REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - registry: ${{ env.DOCKERHUB_REGISTRY }} - username: ${{ env.DOCKER_USER }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - - - name: Build and push Docker images - uses: docker/build-push-action@v2 - with: - context: . - file: ./Dockerfile - push: true - platforms: linux/amd64,linux/arm64,linux/arm/v7 - tags: | - ${{ env.GHCR_TAG }} - ${{ env.DOCKERHUB_TAG }} - diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml new file mode 100644 index 0000000..2729701 --- /dev/null +++ b/.github/workflows/cargo.yml @@ -0,0 +1,49 @@ +# Publishes the crate to Crates.io for each new tag +name: πŸ“¦ Publish crates + +on: + workflow_dispatch: + push: + tags: + - '*' + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + +jobs: + publish: + name: πŸ“¦ Publish + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: πŸ›ŽοΈ Checkout tag + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: πŸ¦€ Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: πŸ” Verify Cargo.toml version matches tag + if: github.ref_type == 'tag' + env: + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + VERSION=$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"(.*)".*/\1/') + if [ "$VERSION" != "$TAG" ]; then + echo "::error::Cargo.toml version ($VERSION) does not match tag ($TAG)" + exit 1 + fi + echo "Publishing adguardian v$VERSION" + + - name: βœ… Run tests + run: cargo test --locked + + - name: πŸ“¦ Publish to Crates.io + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} + run: cargo publish --locked diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1418aee --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,250 @@ +# CI checks for PRs +name: 🚦 CI + +on: + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + # Detect which files changed, to determine which checks to run + changes: + name: πŸ” Detect changes + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + docker: ${{ steps.filter.outputs.docker }} + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Filter changed paths + id: filter + uses: dorny/paths-filter@v4 + with: + filters: | + docker: + - 'Dockerfile*' + - 'Cargo.toml' + - 'Cargo.lock' + + # Run format check, to ensure code nice and pretty + fmt: + name: 🎨 Format + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + - name: Check formatting + run: cargo fmt --all --check + + # Run clippy, to ensure no rust warnings + clippy: + name: πŸ“Ž Clippy + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - name: Cache cargo build + uses: Swatinem/rust-cache@v2 + + - name: Run clippy + run: cargo clippy --all-targets --all-features --locked -- -D warnings + + # Execute the tests + test: + name: πŸ§ͺ Test + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo build + uses: Swatinem/rust-cache@v2 + + - name: Run tests + run: cargo test --all-features --locked + + # Do build check, ensure everything builds + build: + name: πŸ”¨ Build + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo build + uses: Swatinem/rust-cache@v2 + + - name: Build release + run: cargo build --release --locked + + # Check the docs build with no warnings or broken intra-doc links + docs: + name: πŸ“š Docs + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + RUSTDOCFLAGS: -D warnings + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo build + uses: Swatinem/rust-cache@v2 + + - name: Build docs + run: cargo doc --no-deps --all-features --locked + + # Dependencies audit for known vulnerabilities + # audit: + # name: πŸ›‘οΈ Audit + # runs-on: ubuntu-latest + # timeout-minutes: 15 + # steps: + # - name: Checkout + # uses: actions/checkout@v6 + # with: + # persist-credentials: false + + # - name: Install cargo-audit + # uses: taiki-e/install-action@v2 + # with: + # tool: cargo-audit + + # - name: Run cargo audit + # run: cargo audit + + # Run the Docker image build and smoke test + docker: + name: 🐳 Docker build + needs: changes + if: needs.changes.outputs.docker == 'true' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Build image (no push) + uses: docker/build-push-action@v7 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64 + push: false + load: false + cache-from: type=gha,scope=ci-docker + cache-to: type=gha,mode=max,scope=ci-docker + + # Review the workflow files are valid and free of security issues + workflow-lint: + name: βš™οΈ Workflow lint + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Run actionlint + uses: raven-actions/actionlint@v2 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: '3.x' + + - name: Run zizmor + run: | + python -m pip install --quiet zizmor + zizmor --offline .github/workflows/ + + # Check no super secret things committed + secrets: + name: πŸ”‘ Secret scan + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Scan for secrets with TruffleHog + uses: trufflesecurity/trufflehog@main + with: + extra_args: --only-verified + + # Dependency changes have no known vulnerabilities + dependency-review: + name: ⛓️‍πŸ’₯ Dependency review + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Review dependencies + uses: actions/dependency-review-action@v5 + with: + fail-on-severity: moderate + show-openssf-scorecard: false diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..b899bdf --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,184 @@ +name: 🐳 Docker + +on: + workflow_dispatch: + inputs: + tag: + description: 'Docker tag to publish. Leave empty to update latest' + required: false + type: string + push: + branches: + - main + tags: + - '*' + +permissions: + contents: read + packages: write + id-token: write + attestations: write + +concurrency: + group: docker-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + IMAGE_NAME: adguardian + DOCKER_USER: lissy93 + GHCR_REGISTRY: ghcr.io + DOCKERHUB_REGISTRY: docker.io + +jobs: + docker: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Compute image metadata + id: meta + shell: bash + env: + MANUAL_TAG: ${{ inputs.tag }} + DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + if [[ -n "${MANUAL_TAG}" ]]; then + TAG="${MANUAL_TAG}" + else + TAG="latest" + fi + elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then + TAG="${GITHUB_REF#refs/tags/}" + else + TAG="latest" + fi + + TAG="$(echo "${TAG}" | sed -E 's/[^A-Za-z0-9_.-]+/-/g; s/^[.-]+//' | cut -c1-128)" + if [[ -z "${TAG}" ]]; then + echo "Computed Docker tag is empty" >&2 + exit 1 + fi + + GHCR_IMAGE="${GHCR_REGISTRY}/${DOCKER_USER}/${IMAGE_NAME}" + PUBLISH_IMAGES="${GHCR_IMAGE}" + if [[ -n "${DOCKERHUB_PASSWORD}" ]]; then + DOCKERHUB_IMAGE="${DOCKERHUB_REGISTRY}/${DOCKER_USER}/${IMAGE_NAME}" + PUBLISH_IMAGES="${PUBLISH_IMAGES},${DOCKERHUB_IMAGE}" + echo "dockerhub_enabled=true" >> "$GITHUB_OUTPUT" + else + echo "dockerhub_enabled=false" >> "$GITHUB_OUTPUT" + fi + + echo "TAG=${TAG}" >> "$GITHUB_ENV" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "GHCR_IMAGE=${GHCR_IMAGE}" >> "$GITHUB_ENV" + echo "ghcr_image=${GHCR_IMAGE}" >> "$GITHUB_OUTPUT" + echo "PUBLISH_IMAGES=${PUBLISH_IMAGES}" >> "$GITHUB_ENV" + echo "publish_images=${PUBLISH_IMAGES}" >> "$GITHUB_OUTPUT" + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ${{ env.GHCR_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to DockerHub + if: steps.meta.outputs.dockerhub_enabled == 'true' + uses: docker/login-action@v4 + with: + registry: ${{ env.DOCKERHUB_REGISTRY }} + username: ${{ env.DOCKER_USER }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + # Build the ultra-lightweight from scratch version for 64-bit targets + - name: Build lightweight image (amd64, arm64) + id: light + uses: docker/build-push-action@v7 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + provenance: false + sbom: false + cache-from: type=gha,scope=light + cache-to: type=gha,mode=max,scope=light + outputs: type=image,"name=${{ steps.meta.outputs.publish_images }}",push-by-digest=true,name-canonical=true,push=true + + # Build the fat (Debian glibc) image for everything else + - name: Build full image (armv7, 386) + id: full + uses: docker/build-push-action@v7 + with: + context: . + file: ./Dockerfile.full + platforms: linux/arm/v7,linux/386 + provenance: false + sbom: false + cache-from: type=gha,scope=full + cache-to: type=gha,mode=max,scope=full + outputs: type=image,"name=${{ steps.meta.outputs.publish_images }}",push-by-digest=true,name-canonical=true,push=true + + # Merge the variants into single manifest + - name: Create combined manifest list + id: manifest + shell: bash + env: + LIGHT_DIGEST: ${{ steps.light.outputs.digest }} + FULL_DIGEST: ${{ steps.full.outputs.digest }} + run: | + IFS=',' read -ra IMAGES <<< "${PUBLISH_IMAGES}" + for IMG in "${IMAGES[@]}"; do + docker buildx imagetools create -t "${IMG}:${TAG}" \ + "${IMG}@${LIGHT_DIGEST}" \ + "${IMG}@${FULL_DIGEST}" + done + GHCR_DIGEST=$(docker buildx imagetools inspect "${GHCR_IMAGE}:${TAG}" --format '{{.Manifest.Digest}}') + echo "ghcr_digest=${GHCR_DIGEST}" >> "$GITHUB_OUTPUT" + + - name: Inspect published manifests + shell: bash + run: | + IFS=',' read -ra IMAGES <<< "${PUBLISH_IMAGES}" + for IMG in "${IMAGES[@]}"; do + echo "::group::${IMG}:${TAG}" + docker buildx imagetools inspect "${IMG}:${TAG}" + echo "::endgroup::" + done + + - name: Attest build provenance + uses: actions/attest-build-provenance@v4 + continue-on-error: true + with: + subject-name: ${{ steps.meta.outputs.ghcr_image }} + subject-digest: ${{ steps.manifest.outputs.ghcr_digest }} + push-to-registry: true + + - name: Generate SBOM + uses: anchore/sbom-action@v0.24.0 + continue-on-error: true + with: + image: ${{ steps.meta.outputs.ghcr_image }}@${{ steps.manifest.outputs.ghcr_digest }} + format: spdx-json + output-file: sbom.spdx.json + + - name: Attest SBOM + uses: actions/attest@v4 + continue-on-error: true + with: + subject-name: ${{ steps.meta.outputs.ghcr_image }} + subject-digest: ${{ steps.manifest.outputs.ghcr_digest }} + predicate-type: https://spdx.dev/Document + predicate-path: sbom.spdx.json + push-to-registry: true diff --git a/.github/workflows/insert-contributors.yml b/.github/workflows/insert-contributors.yml deleted file mode 100644 index bcc0308..0000000 --- a/.github/workflows/insert-contributors.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: πŸ‘₯ Insert Credits -on: - workflow_dispatch: - schedule: - - cron: '0 1 * * 0' -jobs: - - insert-sponsors: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Contributors - uses: akhilmhdh/contributors-readme-action@v2.3.6 - env: - GITHUB_TOKEN: ${{ secrets.BOT_TOKEN || secrets.GITHUB_TOKEN }} - with: - image_size: 80 - collaborators: all - readme_path: .github/README.md - columns_per_row: 6 - commit_message: 'Updates contributor list' - committer_username: liss-bot - committer_email: liss-bot@d0h.co diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index 59c4bac..25cb29f 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -1,16 +1,22 @@ -# Pushes the contents of the repo to the Codeberg mirror -name: πŸͺž Mirror to Codeberg +# Syncs the full source of the adguardian repo over to our Codeberg mirror +# This is then accessible over at https://codeberg.org/alicia/adguardian +name: πŸͺž Mirror on: workflow_dispatch: schedule: - - cron: '30 0 * * 0' # At 03:00 on Sunday + - cron: '30 3 * * 0' # At 03:30 on Sunday + +permissions: + contents: read + jobs: codeberg: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - with: { fetch-depth: 0 } - - uses: pixta-dev/repository-mirroring-action@v1 + - uses: lissy93/repo-mirror-action@v1.5.0 with: - target_repo_url: git@codeberg.org:alicia/adguardian.git - ssh_private_key: ${{ secrets.CODEBERG_SSH }} + ssh_key: ${{ secrets.CODEBERG_SSH }} + host: git@codeberg.org + user: alicia + repo: adguardian + force_push: false diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index a8cc966..4babf15 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -1,4 +1,7 @@ -name: Build Docs πŸ“ +# Generates Rustdoc API documentation and publishes it to the docs-src branch +# (served via GitHub Pages). Runs on pushes to main that touch the source. + +name: πŸ“ Docs on: workflow_dispatch: @@ -9,24 +12,34 @@ on: - src/** - Cargo.toml +permissions: + contents: write + +env: + CARGO_TERM_COLOR: always + jobs: build: + name: πŸ“ Build & publish docs runs-on: ubuntu-latest + timeout-minutes: 15 steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Generate docs - uses: actions-rs/cargo@v1 + - name: πŸ›ŽοΈ Checkout repository + uses: actions/checkout@v6 with: - command: doc - args: --no-deps + persist-credentials: false + + - name: πŸ¦€ Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: πŸ“ Generate docs + run: cargo doc --no-deps --locked - - name: Create index.html - run: echo "" > target/doc/index.html + - name: πŸ”— Create index.html redirect + run: echo '' > target/doc/index.html - - name: Publish docs - uses: peaceiris/actions-gh-pages@v3.9.3 + - name: πŸš€ Publish docs + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.BOT_TOKEN || secrets.GITHUB_TOKEN }} publish_dir: ./target/doc diff --git a/.github/workflows/push-cargo.yml b/.github/workflows/push-cargo.yml deleted file mode 100644 index 354c97b..0000000 --- a/.github/workflows/push-cargo.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Publish to Crates.io πŸ“¦ - -on: - workflow_dispatch: - release: - types: [published] - -jobs: - publish: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - - - name: Run tests - uses: actions-rs/cargo@v1 - with: - command: test - - - name: Publish to Crates.io - uses: actions-rs/cargo@v1 - with: - command: publish - args: --token ${{ secrets.CRATES_IO_TOKEN }} --allow-dirty diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml deleted file mode 100644 index 6f99da1..0000000 --- a/.github/workflows/release-binaries.yml +++ /dev/null @@ -1,275 +0,0 @@ -name: Release πŸš€ - -on: - workflow_dispatch: - push: - branches: - - main - paths: - - src/** - - Cargo.toml - -env: - CARGO_TERM_COLOR: always - -jobs: - release-x86_64: - name: Release x86_64 - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Get the version - id: get_version - run: echo ::set-output name=VERSION::$(grep "^version" Cargo.toml | awk '{print $3}' | tr -d '"') - - - name: Install dependencies - run: sudo apt-get update -y && sudo apt-get install -y musl-tools - - - name: Install rust toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - - name: Install target - run: rustup target add x86_64-unknown-linux-musl - - - name: Build for x86_64 - uses: actions-rs/cargo@v1 - with: - command: build - args: --release --target=x86_64-unknown-linux-musl - - - name: Upload x86_64 Binary - uses: actions/upload-artifact@v2 - with: - name: adguardian-x86_64 - path: ./target/x86_64-unknown-linux-musl/release/adguardian - - release-arm-v7: - name: Release ARM V7 - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Get the version - id: get_version - run: echo ::set-output name=VERSION::$(grep "^version" Cargo.toml | awk '{print $3}' | tr -d '"') - - - name: Install dependencies - run: sudo apt-get update -y && sudo apt-get install -y gcc-arm-linux-gnueabihf - - - name: Install rust toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - target: armv7-unknown-linux-gnueabihf - - - name: Install cross - run: cargo install cross - - - name: Build for ARM V7 - run: cross build --target=armv7-unknown-linux-gnueabihf --release - - - name: Upload ARM V7 Binary - uses: actions/upload-artifact@v2 - with: - name: adguardian-arm-v7 - path: ./target/armv7-unknown-linux-gnueabihf/release/adguardian - - release-arm64: - name: Release ARM64 - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Get the version - id: get_version - run: echo ::set-output name=VERSION::$(grep "^version" Cargo.toml | awk '{print $3}' | tr -d '"') - - - name: Install cross - run: cargo install cross - - - name: Install aarch64 target - run: rustup target add aarch64-unknown-linux-gnu - - - name: Build for ARM64 - run: cross build --target aarch64-unknown-linux-gnu --release - - - name: Upload ARM64 Binary - uses: actions/upload-artifact@v2 - with: - name: adguardian-arm64 - path: ./target/aarch64-unknown-linux-gnu/release/adguardian - - - release-macos: - name: Release MacOS - runs-on: macos-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Get the version - id: get_version - run: echo ::set-output name=VERSION::$(grep "^version" Cargo.toml | awk '{print $3}' | tr -d '"') - - - name: Install rust toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - - name: Build for MacOS - uses: actions-rs/cargo@v1 - with: - command: build - args: --release - - - name: Upload MacOS Binary - uses: actions/upload-artifact@v2 - with: - name: adguardian-macos - path: ./target/release/adguardian - - release-windows: - name: Release Windows - runs-on: windows-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Get the version - id: get_version - run: echo "::set-output name=VERSION::$(grep '^version' Cargo.toml | awk '{print $3}' | tr -d '"')" - - - name: Install rust toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - - name: Build for Windows - uses: actions-rs/cargo@v1 - with: - command: build - args: --release - - - name: Upload Windows Binary - uses: actions/upload-artifact@v2 - with: - name: adguardian-windows - path: ./target/release/adguardian.exe - - create-release: - name: Create Release - needs: [release-x86_64, release-arm-v7, release-arm64, release-macos, release-windows] - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Get the version - id: get_version - run: echo ::set-output name=VERSION::$(grep "^version" Cargo.toml | awk '{print $3}' | tr -d '"') - - - name: Update or Create Release - id: create_release - uses: softprops/action-gh-release@v1 - with: - tag_name: ${{ steps.get_version.outputs.VERSION }} - release_name: Release ${{ steps.get_version.outputs.VERSION }} - body_path: ./.github/CHANGELOG.md - draft: false - prerelease: false - env: - GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} - - - name: Download x86_64 Binary - uses: actions/download-artifact@v2 - with: - name: adguardian-x86_64 - path: ./target/x86_64-unknown-linux-musl/release/ - - - name: Download ARM V7 Binary - uses: actions/download-artifact@v2 - with: - name: adguardian-arm-v7 - path: ./target/armv7-unknown-linux-gnueabihf/release/ - - - name: Download ARM64 Binary - uses: actions/download-artifact@v2 - with: - name: adguardian-arm64 - path: ./target/aarch64-unknown-linux-gnu/release/ - - - name: Download MacOS Binary - uses: actions/download-artifact@v2 - with: - name: adguardian-macos - path: ./target/release/ - - - name: Download Windows Binary - uses: actions/download-artifact@v2 - with: - name: adguardian-windows - path: ./target/release/ - - - name: Upload x86_64 Binary - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./target/x86_64-unknown-linux-musl/release/adguardian - asset_name: adguardian-x86_64 - asset_content_type: application/octet-stream - - - name: Upload ARM V7 Binary - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./target/armv7-unknown-linux-gnueabihf/release/adguardian - asset_name: adguardian-arm-v7 - asset_content_type: application/octet-stream - - - name: Upload ARM64 Binary - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./target/aarch64-unknown-linux-gnu/release/adguardian - asset_name: adguardian-arm64 - asset_content_type: application/octet-stream - - - name: Upload MacOS Binary - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./target/release/adguardian - asset_name: adguardian-macos - asset_content_type: application/octet-stream - - - name: Upload Windows Binary - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./target/release/adguardian.exe - asset_name: adguardian-windows.exe - asset_content_type: application/octet-stream diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..494eaa8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,200 @@ +# Builds AdGuardian for all supported targets and drafts a GitHub release +# with the compiled binaries, a SHA256 checksum and provenance attestations +# The release is created as a draft, then human can review and hit publish +# +# Triggered by: +# - Push of any minor/major (X.Y.0) git tag (patch tags just rebuild Docker) +# - Manual dispatch with any existing tag (any version) + +name: πŸš€ Release + +on: + push: + tags: ['*.*.0'] + workflow_dispatch: + inputs: + tag: + description: 'Existing git tag to release (e.g. 1.7.0)' + required: true + +concurrency: + group: ${{ github.workflow }}-${{ inputs.tag || github.ref_name }} + cancel-in-progress: false + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + name: πŸ—οΈ Build ${{ matrix.name }} + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + env: + TAG: ${{ inputs.tag || github.ref_name }} + strategy: + fail-fast: false + matrix: + include: + - name: adguardian-linux + os: ubuntu-latest + target: x86_64-unknown-linux-musl + cross: false + apt: musl-tools + - name: adguardian-linux-arm64 + os: ubuntu-latest + target: aarch64-unknown-linux-gnu + cross: true + - name: adguardian-linux-armv7 + os: ubuntu-latest + target: armv7-unknown-linux-gnueabihf + cross: true + - name: adguardian-macos + os: macos-latest + target: aarch64-apple-darwin + cross: false + - name: adguardian-macos-x86_64 + os: macos-latest + target: x86_64-apple-darwin + cross: false + - name: adguardian-windows.exe + os: windows-latest + target: x86_64-pc-windows-msvc + cross: false + steps: + - name: πŸ›ŽοΈ Checkout tag + uses: actions/checkout@v6 + with: + ref: refs/tags/${{ env.TAG }} + persist-credentials: false + + - name: πŸ¦€ Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: πŸ“¦ Install apt dependencies + if: matrix.apt != '' + run: sudo apt-get update -y && sudo apt-get install -y ${{ matrix.apt }} + + - name: πŸ”§ Install cross + if: matrix.cross + uses: taiki-e/install-action@v2 + with: + tool: cross + + - name: πŸ—οΈ Build with cargo + if: ${{ !matrix.cross }} + run: cargo build --release --locked --target ${{ matrix.target }} + + - name: πŸ—οΈ Build with cross + if: matrix.cross + run: cross build --release --locked --target ${{ matrix.target }} + + - name: πŸ“€ Stage binary as ${{ matrix.name }} + shell: bash + run: | + set -euo pipefail + SRC="target/${{ matrix.target }}/release/adguardian" + [ -f "${SRC}.exe" ] && SRC="${SRC}.exe" + cp "$SRC" "${{ matrix.name }}" + + - name: ⬆️ Upload build artifact + uses: actions/upload-artifact@v7 + with: + name: ${{ matrix.name }} + path: ${{ matrix.name }} + if-no-files-found: error + retention-days: 1 + + release: + name: πŸš€ Draft Release + needs: build + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + id-token: write + attestations: write + env: + TAG: ${{ inputs.tag || github.ref_name }} + steps: + - name: πŸ›ŽοΈ Checkout tag + uses: actions/checkout@v6 + with: + ref: refs/tags/${{ env.TAG }} + fetch-depth: 0 + persist-credentials: false + + - name: πŸ“₯ Download all binaries + uses: actions/download-artifact@v8 + with: + path: artifacts + merge-multiple: true + + - name: πŸ”’ Generate SHA256 checksums + run: | + set -euo pipefail + cd artifacts + sha256sum adguardian-* > SHA256SUMS + cat SHA256SUMS + + - name: πŸͺͺ Generate build provenance attestation + uses: actions/attest-build-provenance@v4 + with: + subject-path: 'artifacts/adguardian-*' + + - name: πŸ”Ž Find previous release tag + id: prev + env: + CURRENT_TAG: ${{ env.TAG }} + run: | + set -euo pipefail + git fetch --tags --force + PREV=$({ echo "$CURRENT_TAG"; git tag | grep -E '^[0-9]+\.[0-9]+\.0$' || true; } \ + | sort -uV \ + | awk -v cur="$CURRENT_TAG" '$0 == cur { print prev; exit } { prev = $0 }') + echo "tag=$PREV" >> "$GITHUB_OUTPUT" + + - name: πŸ“ Create draft release + id: release + uses: softprops/action-gh-release@v3 # zizmor: ignore[superfluous-actions] + with: + tag_name: ${{ env.TAG }} + name: Release ${{ env.TAG }} + draft: true + prerelease: false + generate_release_notes: true + previous_tag: ${{ steps.prev.outputs.tag }} + fail_on_unmatched_files: true + files: | + artifacts/adguardian-* + artifacts/SHA256SUMS + token: ${{ secrets.BOT_TOKEN || secrets.GITHUB_TOKEN }} + + - name: πŸ“‹ Job summary + if: always() + env: + REPO_URL: ${{ github.server_url }}/${{ github.repository }} + PREV_TAG: ${{ steps.prev.outputs.tag }} + RELEASE_URL: ${{ steps.release.outputs.url }} + run: | + set -euo pipefail + { + echo "## πŸš€ Release Draft" + echo "" + echo "| Item | Value |" + echo "|------|-------|" + echo "| Tag | [\`${TAG}\`](${REPO_URL}/releases/tag/${TAG}) |" + if [ -n "$PREV_TAG" ]; then + echo "| Notes since | [\`${PREV_TAG}\`](${REPO_URL}/releases/tag/${PREV_TAG}) |" + fi + echo "| Assets | $(find artifacts -maxdepth 1 -type f -printf '%f ') |" + if [ -n "$RELEASE_URL" ]; then + echo "| Draft release | βœ… [Review and publish](${RELEASE_URL}) |" + else + echo "| Draft release | ❌ Failed |" + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml new file mode 100644 index 0000000..46de570 --- /dev/null +++ b/.github/workflows/tag.yml @@ -0,0 +1,411 @@ +# Creates a new git tag when a PR is merged, or on manual dispatch +# +# PR trigger flow: +# - Triggered whenever a PR is merged, if that PR made code changes +# - If version wasn't bumped in PR, increment patch version in Cargo.toml +# - Otherwise (if the PR did bump version) we use the new version from Cargo.toml +# - Creates and pushes a git tag for the new version +# - That git tag then triggers Docker publishing (any tag) and, for minor/major +# (X.Y.0) tags, the release-drafting workflow +# - Add labels and release comments to referenced issues (if applicable) +# - Finally, shows summary of actions taken and new tag published +# +# Manual dispatch flow: +# - If a version is provided, sets Cargo.toml to that version +# - If no version is provided, increments patch version automatically +# - Creates and pushes a git tag +# +# Note: pushing the tag must trigger downstream workflows (release, docker), so +# checkout uses a PAT (secrets.BOT_TOKEN). With the default GITHUB_TOKEN, GitHub +# suppresses workflow runs triggered by the tag push. + +name: πŸ”– Tag + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to tag (e.g. 1.7.0). Leave blank to auto-increment patch.' + required: false + type: string + # Btw, this is a safe and intentional trigger + # It only runs once reviewed PR merged, and secrets are scoped + pull_request_target: # zizmor: ignore[dangerous-triggers] + types: [closed] + branches: [main] + +concurrency: + group: auto-version-and-tag + cancel-in-progress: false + +permissions: + contents: write + pull-requests: read + issues: write + +env: + IS_MANUAL: ${{ github.event_name == 'workflow_dispatch' }} + +jobs: + version-and-tag: + if: >- + github.event_name == 'workflow_dispatch' + || github.event.pull_request.merged == true + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: πŸ”’ Validate manual dispatch + if: env.IS_MANUAL == 'true' + env: + INPUT_VERSION: ${{ inputs.version }} + DISPATCH_REF: ${{ github.ref }} + run: | + set -euo pipefail + if [ "$DISPATCH_REF" != "refs/heads/main" ]; then + echo "::error::Manual dispatch only allowed from main (got: $DISPATCH_REF)" + exit 1 + fi + if [ -n "$INPUT_VERSION" ] && ! printf '%s' "$INPUT_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error::Invalid version '${INPUT_VERSION}'. Must be semver (e.g. 1.7.0)." + exit 1 + fi + + - name: πŸ“‚ Check PR for code changes and version bump + id: check_pr + if: env.IS_MANUAL != 'true' + uses: actions/github-script@v8 + with: + script: | + const { owner, repo } = context.repo; + const pull_number = context.payload.pull_request.number; + + const files = await github.paginate( + github.rest.pulls.listFiles, { owner, repo, pull_number } + ); + const codePatterns = [ + /^src\//, /^build\.rs$/, /^Cargo\.lock$/, /^Dockerfile/, + ]; + const codeChanged = files.some(f => + codePatterns.some(p => p.test(f.filename)) + ); + const manifestChanged = files.some(f => f.filename === 'Cargo.toml'); + + if (!codeChanged && !manifestChanged) { + core.info('No code or Cargo.toml changes, skipping'); + core.setOutput('needs_bump', 'false'); + core.setOutput('needs_tag', 'false'); + return; + } + + const parseVersion = (text) => { + const m = text.match(/^version\s*=\s*"([^"]+)"/m); + return m ? m[1] : null; + }; + + let versionBumped = false; + if (manifestChanged) { + const mergeSha = context.payload.pull_request.merge_commit_sha; + const { data: mergeCommit } = await github.rest.git.getCommit({ + owner, repo, commit_sha: mergeSha, + }); + const parentSha = mergeCommit.parents[0].sha; + const getVersion = async (ref) => { + const { data } = await github.rest.repos.getContent({ + owner, repo, path: 'Cargo.toml', ref, + }); + return parseVersion(Buffer.from(data.content, 'base64').toString()); + }; + const [prevVersion, mergeVersion] = await Promise.all([ + getVersion(parentSha), getVersion(mergeSha), + ]); + versionBumped = prevVersion !== mergeVersion; + core.info(`Version: ${prevVersion} β†’ ${mergeVersion}`); + } + + const needsBump = codeChanged && !versionBumped; + const needsTag = codeChanged || versionBumped; + core.info(`Needs bump: ${needsBump}, Needs tag: ${needsTag}`); + core.setOutput('needs_bump', needsBump.toString()); + core.setOutput('needs_tag', needsTag.toString()); + + - name: πŸ” Validate release token + if: env.IS_MANUAL == 'true' || steps.check_pr.outputs.needs_tag == 'true' + env: + BOT_TOKEN: ${{ secrets.BOT_TOKEN }} + run: | + set -euo pipefail + if [ -z "$BOT_TOKEN" ]; then + echo "::error::BOT_TOKEN is required so pushed tags trigger downstream release workflows" + exit 1 + fi + + - name: πŸ›ŽοΈ Checkout repository + if: env.IS_MANUAL == 'true' || steps.check_pr.outputs.needs_tag == 'true' + uses: actions/checkout@v6 # zizmor: ignore[artipacked] + with: + ref: ${{ github.event.pull_request.merge_commit_sha || github.ref }} + token: ${{ secrets.BOT_TOKEN }} + + - name: πŸ‘€ Configure git identity + if: env.IS_MANUAL == 'true' || steps.check_pr.outputs.needs_tag == 'true' + run: | + git config user.name "Liss-Bot" + git config user.email "liss-bot@d0h.co" + + - name: πŸ”Ž Resolve tag version + id: version + if: env.IS_MANUAL == 'true' || steps.check_pr.outputs.needs_tag == 'true' + env: + INPUT_VERSION: ${{ inputs.version }} + NEEDS_BUMP: ${{ steps.check_pr.outputs.needs_bump }} + run: | + set -euo pipefail + read_version() { grep -m1 '^version' Cargo.toml | sed -E 's/.*"(.*)".*/\1/'; } + CUR=$(read_version) + if [ "$IS_MANUAL" = "true" ] && [ -n "$INPUT_VERSION" ]; then + VERSION="$INPUT_VERSION" + elif [ "$IS_MANUAL" = "true" ] || [ "$NEEDS_BUMP" = "true" ]; then + IFS=. read -r MA MI PA <<< "$CUR" + VERSION="${MA}.${MI}.$((PA + 1))" + else + VERSION="$CUR" + fi + + git fetch --tags --force + if git rev-parse "refs/tags/$VERSION" >/dev/null 2>&1; then + echo "::error::Tag $VERSION already exists" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + exit 1 + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: πŸ” Extract referenced issues + id: issues + if: env.IS_MANUAL != 'true' && steps.check_pr.outputs.needs_tag == 'true' + uses: actions/github-script@v8 + with: + script: | + const body = context.payload.pull_request.body || ''; + const prNumber = String(context.payload.pull_request.number); + const matches = body.match(/#(\d+)(?![a-fA-F0-9])/g); + if (!matches) { + core.info('No issue references found in PR body'); + core.setOutput('numbers', ''); + return; + } + const unique = [...new Set(matches.map(m => m.replace('#', '')))] + .filter(n => n !== prNumber && parseInt(n, 10) > 0); + if (unique.length === 0) { + core.info('No issue references after filtering'); + core.setOutput('numbers', ''); + return; + } + core.info(`Found issue references: ${unique.join(', ')}`); + core.setOutput('numbers', unique.join(',')); + + - name: ⬆️ Bump version + id: bump + if: >- + env.IS_MANUAL == 'true' + || steps.check_pr.outputs.needs_bump == 'true' + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + set -euo pipefail + read_version() { grep -m1 '^version' Cargo.toml | sed -E 's/.*"(.*)".*/\1/'; } + CUR=$(read_version) + NEW="$VERSION" + + if [ "$NEW" = "$CUR" ]; then + echo "Cargo.toml already at $NEW, nothing to commit" + echo "bumped=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Update the package version (first top-level `version = "..."` only) + sed -i -E "0,/^version = \".*\"/ s//version = \"${NEW}\"/" Cargo.toml + # Keep Cargo.lock in sync (version line directly follows the package name) + if [ -f Cargo.lock ]; then + sed -i -E "/^name = \"adguardian\"$/{n; s/^version = \".*\"/version = \"${NEW}\"/}" Cargo.lock + fi + + git add Cargo.toml Cargo.lock + if git diff --cached --quiet; then + echo "No changes to commit" + echo "bumped=false" >> "$GITHUB_OUTPUT" + else + git commit -m "Bump version to ${NEW}" + git push origin HEAD:main + echo "bumped=true" >> "$GITHUB_OUTPUT" + fi + + - name: 🏷️ Create and push tag + id: tag + if: env.IS_MANUAL == 'true' || steps.check_pr.outputs.needs_tag == 'true' + env: + PR_NUMBER: ${{ github.event.pull_request.number || '' }} + PR_TITLE: ${{ github.event.pull_request.title || '' }} + PR_AUTHOR: ${{ github.event.pull_request.user.login || github.actor }} + MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha || github.sha }} + ISSUES: ${{ steps.issues.outputs.numbers }} + VERSION: ${{ steps.version.outputs.version }} + run: | + set -euo pipefail + git fetch --tags --force + + { + printf 'AdGuardian v%s πŸš€\n\n' "$VERSION" + if [ -n "$PR_NUMBER" ]; then + printf 'PR: #%s - %s\n' "$PR_NUMBER" "$PR_TITLE" + else + printf 'Manual release by @%s\n' "$PR_AUTHOR" + fi + if [ -n "$ISSUES" ]; then + printf 'Resolves: %s\n' "$(echo "$ISSUES" | sed 's/,/, #/g; s/^/#/')" + fi + printf 'Author: @%s\n' "$PR_AUTHOR" + printf 'Commit: %s\n' "$MERGE_SHA" + } > tag-message.txt + + git tag -a "$VERSION" -F tag-message.txt + git push origin "$VERSION" + echo "result=created" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: πŸ›©οΈ Label referenced issues + id: label + if: >- + env.IS_MANUAL != 'true' + && steps.check_pr.outputs.needs_tag == 'true' + && steps.issues.outputs.numbers != '' + continue-on-error: true + uses: actions/github-script@v8 + env: + ISSUES: ${{ steps.issues.outputs.numbers }} + with: + github-token: ${{ secrets.BOT_TOKEN }} + script: | + const { owner, repo } = context.repo; + const manifest = require('fs').readFileSync('Cargo.toml', 'utf8'); + const version = manifest.match(/^version\s*=\s*"([^"]+)"/m)[1]; + const labelName = `πŸ›©οΈ Released ${version}`; + const issues = process.env.ISSUES.split(',').filter(Boolean); + + try { + await github.rest.issues.createLabel({ + owner, repo, + name: labelName, + color: 'EDEDED', + description: `Included in release v${version}`, + }); + core.info(`Created label: ${labelName}`); + } catch (e) { + if (e.status === 422) { + core.info(`Label already exists: ${labelName}`); + } else { + core.warning(`Failed to create label: ${e.message}`); + } + } + + const prNumber = context.payload.pull_request.number; + const prAuthor = context.payload.pull_request.user?.login; + const creditAuthor = prAuthor && prAuthor.toLowerCase() !== 'lissy93'; + const marker = `released-${version}`; + for (const num of issues) { + const issue_number = parseInt(num, 10); + try { + const [{ data: issue }, comments] = await Promise.all([ + github.rest.issues.get({ owner, repo, issue_number }), + github.rest.issues.listComments({ + owner, repo, issue_number, per_page: 100, + }), + ]); + const alreadyCommented = comments.data.some( + c => c.body?.includes(marker) + ); + if (!alreadyCommented) { + const author = issue.user?.login; + const greeting = author ? `Hey @${author},` : 'Hey,'; + const sixMonthsAgo = new Date(); + sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6); + const isOld = new Date(issue.created_at) < sixMonthsAgo; + const byLine = creditAuthor ? ` by @${prAuthor}` : ''; + const parts = [ + greeting, + `this has now been implemented${byLine} in #${prNumber},`, + `and will be released shortly in ${version} πŸ˜‡`, + ]; + if (isOld) parts.push(`\n\nWe're sorry this one took so long πŸ˜”`); + parts.push(``); + const body = parts.join(' '); + await github.rest.issues.createComment({ + owner, repo, issue_number, body, + }); + } + await github.rest.issues.addLabels({ + owner, repo, issue_number, labels: [labelName, 'βœ… Fixed'], + }); + core.info(alreadyCommented + ? `Already commented on #${num}, labels applied` + : `Commented and labeled #${num}`); + } catch (e) { + core.warning(`Failed to process #${num}: ${e.message}`); + } + } + + - name: πŸ“‹ Job summary + if: always() + env: + PR_NUMBER: ${{ github.event.pull_request.number || '' }} + PR_TITLE: ${{ github.event.pull_request.title || '' }} + REPO_URL: ${{ github.server_url }}/${{ github.repository }} + NEEDS_BUMP: ${{ steps.check_pr.outputs.needs_bump }} + NEEDS_TAG: ${{ steps.check_pr.outputs.needs_tag }} + ISSUES: ${{ steps.issues.outputs.numbers }} + BUMPED: ${{ steps.bump.outputs.bumped }} + TAG_OUTCOME: ${{ steps.tag.outcome }} + TAG_RESULT: ${{ steps.tag.outputs.result }} + TAG_VERSION: ${{ steps.tag.outputs.version }} + LABEL_OUTCOME: ${{ steps.label.outcome }} + run: | + set -euo pipefail + VERSION="${TAG_VERSION:-$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"(.*)".*/\1/' 2>/dev/null || echo "unknown")}" + + { + echo "## πŸ”– Auto Version & Tag" + echo "" + echo "| Step | Result |" + echo "|------|--------|" + + if [ "$IS_MANUAL" = "true" ]; then + echo "| Trigger | Manual dispatch |" + elif [ -n "$PR_NUMBER" ]; then + echo "| PR | [#${PR_NUMBER}](${REPO_URL}/pull/${PR_NUMBER}) β€” ${PR_TITLE} |" + fi + + if [ "$BUMPED" = "true" ]; then + echo "| Version bump | βœ… \`${VERSION}\` |" + else + echo "| Version bump | ⏭️ Skipped |" + fi + + if [ "$TAG_RESULT" = "created" ]; then + echo "| Tag | βœ… [\`${VERSION}\`](${REPO_URL}/releases/tag/${VERSION}) |" + elif [ "$TAG_RESULT" = "existed" ]; then + echo "| Tag | ⏭️ Already exists: \`${VERSION}\` |" + elif [ "$TAG_OUTCOME" = "failure" ]; then + echo "| Tag | ❌ Failed |" + else + echo "| Tag | ⏭️ Skipped |" + fi + + if [ -n "$ISSUES" ]; then + ISSUE_LINKS=$(echo "$ISSUES" | tr ',' '\n' | sed "s|.*|[#&](${REPO_URL}/issues/&)|" | paste -sd ' ' -) + if [ "$LABEL_OUTCOME" = "success" ]; then + echo "| Issues labeled | βœ… ${ISSUE_LINKS} |" + else + echo "| Issues labeled | ⚠️ ${ISSUE_LINKS} |" + fi + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 0000000..83d148e --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,16 @@ +# zizmor configuration β€” https://docs.zizmor.sh/configuration/ +# +# House style: actions are pinned to version tags (e.g. @v6), not commit SHAs. +# We rely on tag immutability plus Dependabot to keep them current, so allow a +# ref-pin for every action rather than requiring a hash. +rules: + unpinned-uses: + config: + policies: + "*": ref-pin + + # The crate is published to crates.io with an API token by deliberate choice; + # OIDC trusted publishing isn't configured for this project. + use-trusted-publishing: + ignore: + - cargo.yml diff --git a/Cargo.lock b/Cargo.lock index 896cd6d..64a3201 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,13 +1,13 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adguardian" version = "1.6.0" dependencies = [ "anyhow", - "base64 0.13.1", + "base64", "chrono", "colored", "crossterm 0.22.1", @@ -31,38 +31,49 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] -name = "atty" -version = "0.2.14" +name = "atomic-waker" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] -name = "base64" -version = "0.13.1" +name = "aws-lc-rs" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] [[package]] name = "base64" -version = "0.21.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" @@ -70,17 +81,23 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + [[package]] name = "bumpalo" -version = "3.12.1" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytes" -version = "1.4.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cassowary" @@ -90,57 +107,90 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.79" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", - "num-integer", "num-traits", - "time", "wasm-bindgen", - "winapi", + "windows-link", ] [[package]] -name = "codespan-reporting" -version = "0.11.1" +name = "cmake" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ - "termcolor", - "unicode-width", + "cc", ] [[package]] name = "colored" -version = "2.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "atty", - "lazy_static", - "winapi", + "windows-sys 0.61.2", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crossterm" @@ -148,8 +198,9 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c" dependencies = [ - "bitflags", + "bitflags 1.3.2", "crossterm_winapi", + "futures-core", "libc", "mio 0.7.14", "parking_lot 0.11.2", @@ -165,11 +216,11 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" dependencies = [ - "bitflags", + "bitflags 1.3.2", "crossterm_winapi", "libc", - "mio 0.8.6", - "parking_lot 0.12.1", + "mio 0.8.11", + "parking_lot 0.12.5", "signal-hook", "signal-hook-mio", "winapi", @@ -177,86 +228,66 @@ dependencies = [ [[package]] name = "crossterm_winapi" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" dependencies = [ "winapi", ] [[package]] -name = "cxx" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.94" +name = "displaydoc" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ - "cc", - "codespan-reporting", - "once_cell", "proc-macro2", "quote", - "scratch", - "syn 2.0.15", + "syn", ] [[package]] -name = "cxxbridge-flags" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.94" +name = "dunce" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.15", -] +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] -name = "encoding_rs" -version = "0.8.32" +name = "errno" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ - "cfg-if", + "libc", + "windows-sys 0.61.2", ] [[package]] -name = "fnv" -version = "1.0.7" +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" -version = "0.3.28" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -269,9 +300,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -279,15 +310,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -296,38 +327,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -337,249 +368,385 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "h2" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f8a914c2987b688368b5138aa05321db91f4090cf26118185672ad588bce21" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", "slab", - "tokio", - "tokio-util", - "tracing", ] [[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hermit-abi" -version = "0.1.19" +name = "getrandom" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ + "cfg-if", + "js-sys", "libc", + "wasi", + "wasm-bindgen", ] [[package]] -name = "hermit-abi" -version = "0.2.6" +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ + "cfg-if", + "js-sys", "libc", + "r-efi", + "wasip2", + "wasm-bindgen", ] [[package]] name = "http" -version = "0.2.9" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", - "fnv", "itoa", ] [[package]] name = "http-body" -version = "0.4.5" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", - "pin-project-lite", ] [[package]] -name = "httparse" -version = "1.8.0" +name = "http-body-util" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] [[package]] -name = "httpdate" -version = "1.0.2" +name = "httparse" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "0.14.26" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ + "atomic-waker", "bytes", "futures-channel", "futures-core", - "futures-util", - "h2", "http", "http-body", "httparse", - "httpdate", "itoa", "pin-project-lite", - "socket2", + "smallvec", "tokio", - "tower-service", - "tracing", "want", ] [[package]] name = "hyper-rustls" -version = "0.23.2" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", + "hyper-util", "rustls", "tokio", "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.56" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ - "cxx", - "cxx-build", + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", ] [[package]] name = "idna" -version = "0.3.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", ] [[package]] -name = "indexmap" -version = "1.9.3" +name = "idna_adapter" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ - "autocfg", - "hashbrown", + "icu_normalizer", + "icu_properties", ] [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", ] [[package]] name = "ipnet" -version = "2.7.2" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] -name = "js-sys" -version = "0.3.61" +name = "jni" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ - "wasm-bindgen", + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", ] [[package]] -name = "lazy_static" -version = "1.4.0" +name = "jni-sys" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] [[package]] -name = "libc" -version = "0.2.143" +name = "jni-sys-macros" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc207893e85c5d6be840e969b496b53d94cec8be2d501b214f50daa97fa8024" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] [[package]] -name = "link-cplusplus" -version = "1.0.8" +name = "jobserver" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "cc", + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", ] +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.17" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] -name = "memchr" -version = "2.5.0" +name = "lru-slab" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] -name = "mime" -version = "0.3.17" +name = "memchr" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "mio" @@ -596,14 +763,25 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.6" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.45.0", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -624,40 +802,26 @@ dependencies = [ "winapi", ] -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] -name = "num_cpus" -version = "1.15.0" +name = "once_cell" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" -dependencies = [ - "hermit-abi 0.2.6", - "libc", -] +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "once_cell" -version = "1.17.1" +name = "openssl-probe" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "parking_lot" @@ -672,12 +836,12 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", - "parking_lot_core 0.9.7", + "parking_lot_core 0.9.12", ] [[package]] @@ -689,67 +853,170 @@ dependencies = [ "cfg-if", "instant", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "winapi", ] [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", - "windows-sys 0.45.0", + "windows-link", ] [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] -name = "pin-utils" -version = "0.1.0" +name = "potential_utf" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] [[package]] -name = "proc-macro2" -version = "1.0.56" +name = "ppv-lite86" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "unicode-ident", + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", ] [[package]] name = "quote" -version = "1.0.26" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "ratatui" version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcc0d032bccba900ee32151ec0265667535c230169f5a011154cdcd984e16829" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cassowary", "crossterm 0.26.1", "unicode-segmentation", @@ -762,166 +1029,264 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", ] [[package]] name = "reqwest" -version = "0.11.17" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13293b639a097af28fc8a90f22add145a9c954e49d77da06263d58cf44d5fb91" +checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" dependencies = [ - "base64 0.21.0", + "base64", "bytes", - "encoding_rs", "futures-core", - "futures-util", - "h2", "http", "http-body", + "http-body-util", "hyper", "hyper-rustls", - "ipnet", + "hyper-util", "js-sys", "log", - "mime", - "once_cell", "percent-encoding", "pin-project-lite", + "quinn", "rustls", - "rustls-pemfile", + "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", - "serde_urlencoded", + "sync_wrapper", "tokio", "tokio-rustls", + "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", "webpki-roots", - "winreg", ] [[package]] name = "ring" -version = "0.16.20" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", + "cfg-if", + "getrandom 0.2.17", "libc", - "once_cell", - "spin", "untrusted", - "web-sys", - "winapi", + "windows-sys 0.52.0", ] +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustls" -version = "0.20.8" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", "ring", - "sct", - "webpki", + "rustls-pki-types", + "untrusted", ] [[package]] -name = "rustls-pemfile" -version = "1.0.2" +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ - "base64 0.21.0", + "winapi-util", ] [[package]] -name = "ryu" -version = "1.0.13" +name = "schannel" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "scratch" -version = "1.0.5" +name = "security-framework" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] [[package]] -name = "sct" -version = "0.7.0" +name = "security-framework-sys" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ - "ring", - "untrusted", + "core-foundation-sys", + "libc", ] [[package]] name = "semver" -version = "1.0.17" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.162" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71b2f6e1ab5c2b98c05f0f35b236b22e8df7ead6ffbf51d7808da7f8817e7ab6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.162" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a0814352fd64b58489904a44ea8d90cb1a91dcb6b4f5ebabc32c8318e93cb6" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", - "ryu", + "memchr", "serde", + "serde_core", + "zmij", ] [[package]] -name = "serde_urlencoded" -version = "0.7.1" +name = "shlex" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook" -version = "0.3.15" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" dependencies = [ "libc", "signal-hook-registry", @@ -929,61 +1294,65 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio 0.7.14", - "mio 0.8.6", + "mio 0.8.11", "signal-hook", ] [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] [[package]] name = "slab" -version = "0.4.8" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" -dependencies = [ - "autocfg", -] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.10.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.4.9" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "winapi", + "windows-sys 0.61.2", ] [[package]] -name = "spin" -version = "0.5.2" +name = "stable_deref_trait" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "1.0.109" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -991,41 +1360,80 @@ dependencies = [ ] [[package]] -name = "syn" -version = "2.0.15" +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "syn", ] [[package]] -name = "termcolor" -version = "1.2.0" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "winapi-util", + "thiserror-impl 1.0.69", ] [[package]] -name = "time" -version = "0.1.45" +name = "thiserror" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", ] [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -1038,205 +1446,216 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ - "autocfg", "bytes", "libc", - "mio 0.8.6", - "num_cpus", - "parking_lot 0.12.1", + "mio 1.2.1", + "parking_lot 0.12.5", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn", ] [[package]] name = "tokio-rustls" -version = "0.23.4" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", - "webpki", ] [[package]] -name = "tokio-util" -version = "0.7.8" +name = "tower" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ - "bytes", "futures-core", - "futures-sink", + "futures-util", "pin-project-lite", + "sync_wrapper", "tokio", - "tracing", + "tower-layer", + "tower-service", ] +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ - "cfg-if", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] [[package]] name = "try-lock" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" - -[[package]] -name = "unicode-bidi" -version = "0.3.13" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "unicode-ident" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" - -[[package]] -name = "unicode-normalization" -version = "0.1.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "untrusted" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.3.1" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] -name = "want" -version = "0.3.0" +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "walkdir" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ - "log", - "try-lock", + "same-file", + "winapi-util", ] [[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" +name = "want" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasm-bindgen" -version = "0.2.84" +name = "wasip2" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "cfg-if", - "wasm-bindgen-macro", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.84" +name = "wasm-bindgen" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ - "bumpalo", - "log", + "cfg-if", "once_cell", - "proc-macro2", - "quote", - "syn 1.0.109", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.34" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ - "cfg-if", "js-sys", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1244,50 +1663,62 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 1.0.109", - "wasm-bindgen-backend", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" -version = "0.3.61" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] -name = "webpki" -version = "0.22.0" +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ - "ring", - "untrusted", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", ] [[package]] name = "webpki-roots" -version = "0.22.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ - "webpki", + "rustls-pki-types", ] [[package]] @@ -1308,11 +1739,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -1322,12 +1753,62 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows" -version = "0.48.0" +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-targets 0.48.0", + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", ] [[package]] @@ -1345,7 +1826,25 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", ] [[package]] @@ -1365,17 +1864,33 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -1386,9 +1901,15 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -1398,9 +1919,15 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -1410,9 +1937,21 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -1422,9 +1961,15 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -1434,9 +1979,15 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -1446,9 +1997,15 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -1458,15 +2015,133 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] -name = "winreg" -version = "0.10.1" +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ - "winapi", + "stable_deref_trait", + "yoke-derive", + "zerofrom", ] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index f022445..bdefc86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,17 +14,17 @@ categories = ["command-line-utilities"] # Error objecr for idiomatic error handling anyhow = "1.0" # For encoding auth headers -base64 = "0.13" +base64 = "0.22" # Date + time parsing + manipulating chrono = "0.4" # Handling of terminal colors -colored = "2.0" +colored = "3" # Term manipulation for kb + mouse events -crossterm = { version = "0.22.0", features = ["serde"] } +crossterm = { version = "0.22.0", features = ["serde", "event-stream"] } # Extension of futures for async computation futures = "0.3" # HTTP client -reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls"] } +reqwest = { version = "0.13", default-features = false, features = ["json", "rustls", "webpki-roots"] } # Decerilization of responses serde = { version = "1.0", features = ["derive"] } # Decerilization of JSON responses diff --git a/Dockerfile b/Dockerfile index 3020bfd..140f8bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,22 @@ -# syntax=docker/dockerfile:1.2 +# syntax=docker/dockerfile:1 -# Build application - Copy assets, install deps and compile binary -FROM --platform=$BUILDPLATFORM rust:1.69.0-alpine AS builder -RUN apk add --no-cache pkgconfig openssl openssl-dev musl-dev -WORKDIR /usr/src/adguardian +# Build: compile a static musl binary for the TARGET arch +FROM rust:1.90-alpine AS builder +RUN apk add --no-cache build-base cmake perl ca-certificates +WORKDIR /app COPY . . -RUN cargo build --release +ARG TARGETPLATFORM +RUN --mount=type=cache,target=/usr/local/cargo/registry,id=cargo-reg-${TARGETPLATFORM} \ + cargo build --release && cp target/release/adguardian /adguardian -# Run application - Using lightweight base, execute the binary +# Runtime: just the static binary on an empty image --- FROM scratch -COPY --from=builder /usr/src/adguardian/target/release/adguardian / +LABEL org.opencontainers.image.title="AdGuardian-Term" \ + org.opencontainers.image.description="Real-time traffic monitoring for AdGuard Home, in your terminal" \ + org.opencontainers.image.source="https://github.com/Lissy93/AdGuardian-Term" \ + org.opencontainers.image.licenses="MIT" +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +COPY --from=builder /adguardian /adguardian +USER 65534:65534 ENTRYPOINT ["/adguardian"] diff --git a/Dockerfile.full b/Dockerfile.full new file mode 100644 index 0000000..28e09bd --- /dev/null +++ b/Dockerfile.full @@ -0,0 +1,28 @@ +# syntax=docker/dockerfile:1 + +# glibc (Debial) based Docker image for AdGuardian-Term +# Because the ultra-lightweight scratch version doesn't work on 32-bit arch + +# Build: compile the binary for the TARGET arch +FROM rust:1.90-slim-bookworm AS builder +RUN apt-get update \ + && apt-get install -y --no-install-recommends build-essential cmake clang perl \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY . . +ARG TARGETPLATFORM +RUN --mount=type=cache,target=/usr/local/cargo/registry,id=cargo-reg-full-${TARGETPLATFORM} \ + cargo build --release && cp target/release/adguardian /adguardian + +# Runtime: minimal glibc base with CA certs, run as non-root +FROM debian:bookworm-slim +LABEL org.opencontainers.image.title="AdGuardian-Term" \ + org.opencontainers.image.description="Real-time traffic monitoring for AdGuard Home, in your terminal" \ + org.opencontainers.image.source="https://github.com/Lissy93/AdGuardian-Term" \ + org.opencontainers.image.licenses="MIT" +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates \ + && rm -rf /var/lib/apt/lists/* +COPY --from=builder /adguardian /usr/local/bin/adguardian +USER 65534:65534 +ENTRYPOINT ["/usr/local/bin/adguardian"] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..8f9e576 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +tab_spaces = 2 # (don't come at me. the tab key is too far to reach) diff --git a/src/fetch/fetch_filters.rs b/src/fetch/fetch_filters.rs index 7649135..18ed770 100644 --- a/src/fetch/fetch_filters.rs +++ b/src/fetch/fetch_filters.rs @@ -1,34 +1,34 @@ -use reqwest::{Client, Response, header::HeaderMap}; +use base64::{engine::general_purpose::STANDARD, Engine as _}; +use reqwest::{header::HeaderMap, Client, Response}; use serde::Deserialize; #[derive(Deserialize)] pub struct AdGuardFilteringStatus { - pub filters: Option>, + pub filters: Option>, } #[derive(Deserialize)] pub struct Filter { - pub url: String, - pub name: String, - pub rules_count: u32, - pub enabled: bool, + pub name: String, + pub rules_count: u32, + pub enabled: bool, } pub async fn fetch_adguard_filter_list( - client: &Client, - endpoint: &str, - username: &str, - password: &str, -) -> Result { - let url = format!("{}/control/filtering/status", endpoint); + client: &Client, + endpoint: &str, + username: &str, + password: &str, +) -> Result { + let url = format!("{}/control/filtering/status", endpoint); - let auth_string = format!("{}:{}", username, password); - let auth_header_value = format!("Basic {}", base64::encode(&auth_string)); - let mut headers = HeaderMap::new(); - headers.insert("Authorization", auth_header_value.parse().unwrap()); + let auth_string = format!("{}:{}", username, password); + let auth_header_value = format!("Basic {}", STANDARD.encode(&auth_string)); + let mut headers = HeaderMap::new(); + headers.insert("Authorization", auth_header_value.parse()?); - let res: Response = client.get(&url).headers(headers).send().await?; - let status: AdGuardFilteringStatus = res.json().await?; + let res: Response = client.get(&url).headers(headers).send().await?; + let status: AdGuardFilteringStatus = res.json().await?; - Ok(status) + Ok(status) } diff --git a/src/fetch/fetch_query_log.rs b/src/fetch/fetch_query_log.rs index f0de9bf..2ee0972 100644 --- a/src/fetch/fetch_query_log.rs +++ b/src/fetch/fetch_query_log.rs @@ -1,31 +1,30 @@ -use reqwest::{ - header::{HeaderValue, CONTENT_LENGTH, AUTHORIZATION}, -}; +use base64::{engine::general_purpose::STANDARD, Engine as _}; +use reqwest::header::{HeaderValue, AUTHORIZATION, CONTENT_LENGTH}; use serde::Deserialize; #[derive(Deserialize)] pub struct QueryResponse { - pub data: Vec, + pub data: Vec, } #[derive(Deserialize)] pub struct Query { - pub cached: bool, - pub client: String, - pub upstream: String, - #[serde(rename = "elapsedMs")] - pub elapsed_ms: String, - pub question: Question, - pub reason: String, - pub time: String, + pub cached: bool, + pub client: String, + pub upstream: String, + #[serde(rename = "elapsedMs")] + pub elapsed_ms: String, + pub question: Question, + pub reason: String, + pub time: String, } #[derive(Deserialize)] pub struct Question { - pub class: String, - pub name: String, - #[serde(rename = "type")] - pub question_type: String, + pub class: String, + pub name: String, + #[serde(rename = "type")] + pub question_type: String, } pub async fn fetch_adguard_query_log( @@ -33,20 +32,23 @@ pub async fn fetch_adguard_query_log( endpoint: &str, username: &str, password: &str, + limit: u32, ) -> Result { let auth_string = format!("{}:{}", username, password); - let auth_header_value = format!("Basic {}", base64::encode(&auth_string)); + let auth_header_value = format!("Basic {}", STANDARD.encode(&auth_string)); let mut headers = reqwest::header::HeaderMap::new(); headers.insert(AUTHORIZATION, auth_header_value.parse()?); headers.insert(CONTENT_LENGTH, HeaderValue::from_static("0")); - let url = format!("{}/control/querylog", endpoint); + let url = format!("{}/control/querylog?limit={}", endpoint, limit); let response = client.get(&url).headers(headers).send().await?; if !response.status().is_success() { - return Err(anyhow::anyhow!("Request failed with status code {}", response.status())); + return Err(anyhow::anyhow!( + "Request failed with status code {}", + response.status() + )); } let data = response.json().await?; Ok(data) } - diff --git a/src/fetch/fetch_stats.rs b/src/fetch/fetch_stats.rs index 74516ad..c19bc09 100644 --- a/src/fetch/fetch_stats.rs +++ b/src/fetch/fetch_stats.rs @@ -1,78 +1,89 @@ +use base64::{engine::general_purpose::STANDARD, Engine as _}; /// This module fetches data from AdGuard's stats API /// This includes total number of blocked / allowed queries in each category, /// and 30-day query count history - -use reqwest::{ - header::{HeaderValue, CONTENT_LENGTH, AUTHORIZATION}, -}; +use reqwest::header::{HeaderValue, AUTHORIZATION, CONTENT_LENGTH}; use serde::Deserialize; use std::collections::HashMap; #[derive(Debug, Deserialize, Clone)] pub struct DomainData { - pub name: String, - pub count: i32, + pub name: String, + pub count: i32, } #[derive(Debug, Deserialize, Clone)] pub struct StatsResponse { - pub num_dns_queries: u64, - pub num_blocked_filtering: u64, - pub num_replaced_safebrowsing: u64, - pub num_replaced_safesearch: u64, - pub num_replaced_parental: u64, - pub avg_processing_time: f64, - pub dns_queries: Vec, - pub blocked_filtering: Vec, - pub replaced_safebrowsing: Vec, - pub replaced_parental: Vec, + pub num_dns_queries: u64, + pub num_blocked_filtering: u64, + pub num_replaced_safebrowsing: u64, + pub num_replaced_safesearch: u64, + pub num_replaced_parental: u64, + pub avg_processing_time: f64, + pub dns_queries: Vec, + pub blocked_filtering: Vec, + pub replaced_safebrowsing: Vec, + pub replaced_parental: Vec, - #[serde(default, skip_deserializing)] - pub dns_queries_chart: Vec<(f64, f64)>, - #[serde(default, skip_deserializing)] - pub blocked_filtering_chart: Vec<(f64, f64)>, + #[serde(default, skip_deserializing)] + pub dns_queries_chart: Vec<(f64, f64)>, + #[serde(default, skip_deserializing)] + pub blocked_filtering_chart: Vec<(f64, f64)>, - #[serde(rename = "top_queried_domains", deserialize_with = "deserialize_domains")] - pub top_queried_domains: Vec, - #[serde(rename = "top_blocked_domains", deserialize_with = "deserialize_domains")] - pub top_blocked_domains: Vec, - #[serde(rename = "top_clients", deserialize_with = "deserialize_domains")] - pub top_clients: Vec, + #[serde( + rename = "top_queried_domains", + deserialize_with = "deserialize_domains" + )] + pub top_queried_domains: Vec, + #[serde( + rename = "top_blocked_domains", + deserialize_with = "deserialize_domains" + )] + pub top_blocked_domains: Vec, + #[serde(rename = "top_clients", deserialize_with = "deserialize_domains")] + pub top_clients: Vec, } pub async fn fetch_adguard_stats( - client: &reqwest::Client, - endpoint: &str, - username: &str, - password: &str, + client: &reqwest::Client, + endpoint: &str, + username: &str, + password: &str, ) -> Result { - let auth_string = format!("{}:{}", username, password); - let auth_header_value = format!("Basic {}", base64::encode(&auth_string)); - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert(AUTHORIZATION, auth_header_value.parse()?); - headers.insert(CONTENT_LENGTH, HeaderValue::from_static("0")); + let auth_string = format!("{}:{}", username, password); + let auth_header_value = format!("Basic {}", STANDARD.encode(&auth_string)); + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert(AUTHORIZATION, auth_header_value.parse()?); + headers.insert(CONTENT_LENGTH, HeaderValue::from_static("0")); - let url = format!("{}/control/stats", endpoint); - let response = client.get(&url).headers(headers).send().await?; - if !response.status().is_success() { - return Err(anyhow::anyhow!("Request failed with status code {}", response.status())); - } + let url = format!("{}/control/stats", endpoint); + let response = client.get(&url).headers(headers).send().await?; + if !response.status().is_success() { + return Err(anyhow::anyhow!( + "Request failed with status code {}", + response.status() + )); + } - let data = response.json().await?; - Ok(data) + let data = response.json().await?; + Ok(data) } /// Deserialize a list of domains from the JSON data fn deserialize_domains<'de, D>(deserializer: D) -> Result, D::Error> where - D: serde::Deserializer<'de>, + D: serde::Deserializer<'de>, { - let raw_vec: Vec> = serde::Deserialize::deserialize(deserializer)?; - Ok(raw_vec - .into_iter() - .flat_map(|mut map| { - map.drain().map(|(name, count)| DomainData { name, count }).collect::>() - }) - .collect()) + let raw_vec: Vec> = serde::Deserialize::deserialize(deserializer)?; + Ok( + raw_vec + .into_iter() + .flat_map(|mut map| { + map + .drain() + .map(|(name, count)| DomainData { name, count }) + .collect::>() + }) + .collect(), + ) } - diff --git a/src/fetch/fetch_status.rs b/src/fetch/fetch_status.rs index b094683..f3c25f5 100644 --- a/src/fetch/fetch_status.rs +++ b/src/fetch/fetch_status.rs @@ -1,11 +1,10 @@ -use reqwest::{ - header::{HeaderValue, CONTENT_LENGTH, AUTHORIZATION}, -}; +use base64::{engine::general_purpose::STANDARD, Engine as _}; +use reqwest::header::{HeaderValue, AUTHORIZATION, CONTENT_LENGTH}; use serde::Deserialize; /// Represents the status response from the AdGuard Home API. /// -/// This struct is used to deserialize the JSON response from the +/// This struct is used to deserialize the JSON response from the /// `/control/status` endpoint. /// /// # Example @@ -24,31 +23,24 @@ use serde::Deserialize; /// # Fields /// /// * `version` - The version of the AdGuard Home instance. -/// * `language` - The language currently used in the AdGuard Home instance. -/// * `dns_addresses` - The DNS addresses used by the AdGuard Home instance. /// * `dns_port` - The port number on which the DNS server is running. /// * `http_port` - The port number on which the HTTP server is running. -/// * `protection_disabled_duration` - The duration for which protection is disabled (in seconds). /// * `protection_enabled` - Whether or not protection is currently enabled. /// * `dhcp_available` - Whether or not DHCP is available. /// * `running` - Whether or not the AdGuard Home instance is currently running. #[derive(Debug, Deserialize, Clone)] pub struct StatusResponse { - pub version: String, - pub language: String, - pub dns_addresses: Vec, - pub dns_port: u16, - pub http_port: u16, - pub protection_disabled_duration: u64, - pub protection_enabled: bool, - pub dhcp_available: bool, - pub running: bool, + pub version: String, + pub dns_port: u16, + pub http_port: u16, + pub protection_enabled: bool, + pub dhcp_available: bool, + pub running: bool, } - /// Fetches the current status from the AdGuard Home instance. /// -/// This function sends a GET request to the `/control/status` endpoint of the +/// This function sends a GET request to the `/control/status` endpoint of the /// AdGuard Home API, then deserializes the JSON response into a `StatusResponse`. /// /// # Arguments @@ -74,23 +66,26 @@ pub struct StatusResponse { /// println!("AdGuard Status: {:?}", status); /// ``` pub async fn fetch_adguard_status( - client: &reqwest::Client, - endpoint: &str, - username: &str, - password: &str, + client: &reqwest::Client, + endpoint: &str, + username: &str, + password: &str, ) -> Result { - let auth_string = format!("{}:{}", username, password); - let auth_header_value = format!("Basic {}", base64::encode(&auth_string)); - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert(AUTHORIZATION, auth_header_value.parse()?); - headers.insert(CONTENT_LENGTH, HeaderValue::from_static("0")); + let auth_string = format!("{}:{}", username, password); + let auth_header_value = format!("Basic {}", STANDARD.encode(&auth_string)); + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert(AUTHORIZATION, auth_header_value.parse()?); + headers.insert(CONTENT_LENGTH, HeaderValue::from_static("0")); - let url = format!("{}/control/status", endpoint); - let response = client.get(&url).headers(headers).send().await?; - if !response.status().is_success() { - return Err(anyhow::anyhow!("Request failed with status code {}", response.status())); - } + let url = format!("{}/control/status", endpoint); + let response = client.get(&url).headers(headers).send().await?; + if !response.status().is_success() { + return Err(anyhow::anyhow!( + "Request failed with status code {}", + response.status() + )); + } - let data = response.json().await?; - Ok(data) + let data = response.json().await?; + Ok(data) } diff --git a/src/fetch/mod.rs b/src/fetch/mod.rs index 61b6f3b..19d1e32 100644 --- a/src/fetch/mod.rs +++ b/src/fetch/mod.rs @@ -1,4 +1,4 @@ +pub mod fetch_filters; pub mod fetch_query_log; -pub mod fetch_status; pub mod fetch_stats; -pub mod fetch_filters; +pub mod fetch_status; diff --git a/src/main.rs b/src/main.rs index ccc2caf..57f85b7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,101 +1,127 @@ mod fetch; mod ui; -mod widgets; mod welcome; +mod widgets; -use std::{env, sync::Arc, time::Duration}; use reqwest::Client; -use tokio::time::interval; +use std::{env, time::Duration}; +use tokio::time::{interval, MissedTickBehavior}; use ui::draw_ui; use fetch::{ - fetch_query_log::fetch_adguard_query_log, - fetch_stats::fetch_adguard_stats, - fetch_status::fetch_adguard_status, - fetch_filters::fetch_adguard_filter_list + fetch_filters::fetch_adguard_filter_list, + fetch_query_log::{fetch_adguard_query_log, Query}, + fetch_stats::{fetch_adguard_stats, StatsResponse}, + fetch_status::{fetch_adguard_status, StatusResponse}, }; -async fn run() -> anyhow::Result<()> { - - // Create a reqwest client - let client = Client::new(); - - // AdGuard instance details, from env vars (verified in welcome.rs) - let ip = env::var("ADGUARD_IP")?; - let port = env::var("ADGUARD_PORT")?; - let protocol = env::var("ADGUARD_PROTOCOL").unwrap_or("http".to_string()); - let hostname = format!("{}://{}:{}", protocol, ip, port); - let username = env::var("ADGUARD_USERNAME")?; - let password = env::var("ADGUARD_PASSWORD")?; - - - // Fetch data that doesn't require updates - let filters = fetch_adguard_filter_list(&client, &hostname, &username, &password).await?; - - // Open channels for data fetching where updates are required - let (queries_tx, queries_rx) = tokio::sync::mpsc::channel(1); - let (stats_tx, stats_rx) = tokio::sync::mpsc::channel(1); - let (status_tx, status_rx) = tokio::sync::mpsc::channel(1); - - // Create a channel for the UI to notify the fetcher to shutdown - let shutdown = Arc::new(tokio::sync::Notify::new()); - - // Spawn the UI task, pass data and update channels - let draw_ui_task = tokio::spawn( - draw_ui(queries_rx, stats_rx, status_rx, filters, Arc::clone(&shutdown)) - ); - - // Get update interval (in seconds) - let interval_secs: u64 = env::var("ADGUARD_UPDATE_INTERVAL") - .unwrap_or_else(|_| "2".into()).parse()?; - let mut interval = interval(Duration::from_secs(interval_secs)); - - // Open loop for fetching data at the specified interval - loop { - tokio::select! { - _ = interval.tick() => { - let queries = fetch_adguard_query_log(&client, &hostname, &username, &password).await?; - if queries_tx.send(queries.data).await.is_err() { - return Err(anyhow::anyhow!("Failed to send query data")); - } - - let stats = fetch_adguard_stats(&client, &hostname, &username, &password).await?; - if stats_tx.send(stats).await.is_err() { - return Err(anyhow::anyhow!("Failed to send stats data")); - } +/// Fetch the query log, stats and status together, so a failure leaves the UI's +/// data in sync (all-or-nothing) rather than partially updated. +async fn fetch_all( + client: &Client, + hostname: &str, + username: &str, + password: &str, + query_log_limit: u32, +) -> anyhow::Result<(Vec, StatsResponse, StatusResponse)> { + let queries = + fetch_adguard_query_log(client, hostname, username, password, query_log_limit).await?; + let stats = fetch_adguard_stats(client, hostname, username, password).await?; + let status = fetch_adguard_status(client, hostname, username, password).await?; + Ok((queries.data, stats, status)) +} - let status = fetch_adguard_status(&client, &hostname, &username, &password).await?; - if status_tx.send(status).await.is_err() { - return Err(anyhow::anyhow!("Failed to send status data")); +async fn run() -> anyhow::Result<()> { + // Create a reqwest client + let client = Client::new(); + + // AdGuard instance details, from env vars (verified in welcome.rs) + let ip = env::var("ADGUARD_IP")?; + let port = env::var("ADGUARD_PORT")?; + let protocol = env::var("ADGUARD_PROTOCOL").unwrap_or("http".to_string()); + let hostname = format!("{}://{}:{}", protocol, ip, port); + let username = env::var("ADGUARD_USERNAME")?; + let password = env::var("ADGUARD_PASSWORD")?; + + // Fetch data that doesn't require updates + let filters = fetch_adguard_filter_list(&client, &hostname, &username, &password).await?; + + // Open channels for data fetching where updates are required + let (queries_tx, queries_rx) = tokio::sync::mpsc::channel(1); + let (stats_tx, stats_rx) = tokio::sync::mpsc::channel(1); + let (status_tx, status_rx) = tokio::sync::mpsc::channel(1); + + // Shutdown signal, set by the UI when the user quits + let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(false); + + // Spawn the UI task, pass data and update channels + let draw_ui_task = tokio::spawn(draw_ui( + queries_rx, + stats_rx, + status_rx, + filters, + shutdown_tx, + )); + + // Get update interval (in seconds) + let interval_secs: u64 = env::var("ADGUARD_UPDATE_INTERVAL") + .unwrap_or_else(|_| "2".into()) + .parse()?; + let mut interval = interval(Duration::from_secs(interval_secs)); + interval.set_missed_tick_behavior(MissedTickBehavior::Skip); + + // Max num of query log entries to fetch per update + let query_log_limit: u32 = env::var("ADGUARD_QUERYLOG_LIMIT") + .unwrap_or_else(|_| "100".into()) + .parse()?; + + // Open loop for fetching data at the specified interval + loop { + tokio::select! { + _ = interval.tick() => { + // Check data is ok, just skip this update on transient error + if let Ok((queries, stats, status)) = + fetch_all(&client, &hostname, &username, &password, query_log_limit).await + { + // A send error means the UI has shut down, so stop fetching + if queries_tx.send(queries).await.is_err() + || stats_tx.send(stats).await.is_err() + || status_tx.send(status).await.is_err() + { + break; } } - _ = shutdown.notified() => { - break; - } + } + // Resolves when the UI sets the shutdown flag, or drops the sender + _ = shutdown_rx.changed() => { + break; } } + } - draw_ui_task.await??; + draw_ui_task.await??; - Ok(()) + Ok(()) } fn main() { - let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(async { - welcome::welcome().await.map_err(|e| { - eprintln!("Failed to initialize: {}", e); - std::io::Error::new(std::io::ErrorKind::Other, "Failed to initialize") - }).unwrap(); - - run().await.map_err(|e| { - eprintln!("Failed to run: {}", e); - std::io::Error::new(std::io::ErrorKind::Other, format!("Failed to run: {}", e)) - }).unwrap_or_else(|e| { - eprintln!("Error: {}", e); - std::process::exit(1); - }); + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + welcome::welcome().await.unwrap_or_else(|e| { + eprintln!("Failed to initialize: {}", e); + std::process::exit(1); }); -} + run() + .await + .map_err(|e| { + eprintln!("Failed to run: {}", e); + std::io::Error::other(format!("Failed to run: {}", e)) + }) + .unwrap_or_else(|e| { + eprintln!("Error: {}", e); + std::process::exit(1); + }); + }); +} diff --git a/src/ui.rs b/src/ui.rs index 1435751..cc5f0c4 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,13 +1,15 @@ -use std::{ - io::stdout, - sync::Arc, - time::Duration, -}; use crossterm::{ - event::{poll, read, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers}, + cursor::Show, + event::{ + DisableMouseCapture, EnableMouseCapture, Event, EventStream, KeyCode, KeyEvent, KeyModifiers, + }, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; +use futures::StreamExt; +use std::io::stdout; +use std::sync::Arc; +use tokio::sync::watch; use tui::{ backend::CrosstermBackend, layout::{Constraint, Direction, Layout}, @@ -15,175 +17,190 @@ use tui::{ Terminal, }; -use crate::fetch::fetch_stats::StatsResponse; +use crate::fetch::fetch_filters::{AdGuardFilteringStatus, Filter}; use crate::fetch::fetch_query_log::Query; +use crate::fetch::fetch_stats::StatsResponse; use crate::fetch::fetch_status::StatusResponse; -use crate::fetch::fetch_filters::{AdGuardFilteringStatus, Filter}; -use crate::widgets::gauge::make_gauge; -use crate::widgets::table::make_query_table; use crate::widgets::chart::{make_history_chart, prepare_chart_data}; -use crate::widgets::status::render_status_paragraph; use crate::widgets::filters::make_filters_list; +use crate::widgets::gauge::make_gauge; use crate::widgets::list::make_list; +use crate::widgets::status::render_status_paragraph; +use crate::widgets::table::make_query_table; pub async fn draw_ui( - mut data_rx: tokio::sync::mpsc::Receiver>, - mut stats_rx: tokio::sync::mpsc::Receiver, - mut status_rx: tokio::sync::mpsc::Receiver, - filters: AdGuardFilteringStatus, - shutdown: Arc + mut data_rx: tokio::sync::mpsc::Receiver>, + mut stats_rx: tokio::sync::mpsc::Receiver, + mut status_rx: tokio::sync::mpsc::Receiver, + filters: AdGuardFilteringStatus, + shutdown_tx: watch::Sender, ) -> Result<(), anyhow::Error> { - enable_raw_mode()?; - let mut stdout = stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - terminal.clear()?; - + // Guard restores the terminal on drop, even if we return early via `?` + let _guard = TerminalGuard::new()?; + let backend = CrosstermBackend::new(stdout()); + let mut terminal = Terminal::new(backend)?; + terminal.clear()?; + + // Handle quit keys (q / Ctrl+C) in a separate task, so input never blocks on data + let shutdown_tx = Arc::new(shutdown_tx); + let input_shutdown_tx = Arc::clone(&shutdown_tx); + let input_task = tokio::spawn(async move { + let mut reader = EventStream::new(); + let mut shutdown_rx = input_shutdown_tx.subscribe(); loop { - // Receive query log and stats data from the fetcher - let data = match data_rx.recv().await { + tokio::select! { + maybe_event = reader.next() => { + match maybe_event { + Some(Ok(Event::Key(key))) if is_quit_key(key) => { + let _ = input_shutdown_tx.send(true); + break; + } + Some(Ok(_)) => {} + Some(Err(_)) | None => break, + } + } + // Stop if shutdown was triggered elsewhere (e.g. channels closed) + _ = shutdown_rx.changed() => break, + } + } + }); + + let mut shutdown_rx = shutdown_tx.subscribe(); + + loop { + // Wait for the next batch of data, but bail out immediately on shutdown + let data = tokio::select! { + biased; + _ = shutdown_rx.changed() => break, + maybe_data = data_rx.recv() => match maybe_data { Some(data) => data, - None => break, // Channel has been closed, so we break the loop - }; - let mut stats = match stats_rx.recv().await { - Some(stats) => stats, - None => break, - }; - let status = match status_rx.recv().await { - Some(status) => status, None => break, - }; - - // Prepare the data for the chart - prepare_chart_data(&mut stats); - - terminal.draw(|f| { - let size = f.size(); - - // Make the charts - let gauge = make_gauge(&stats); - let table = make_query_table(&data, size.width); - let graph = make_history_chart(&stats); - let paragraph = render_status_paragraph(&status, &stats); - let filter_items: &[Filter] = filters - .filters - .as_deref() - .unwrap_or(&[]); - let filters_list = make_filters_list(filter_items, size.width); - let top_queried_domains = make_list("Top Queried Domains", &stats.top_queried_domains, Color::Green, size.width); - let top_blocked_domains = make_list("Top Blocked Domains", &stats.top_blocked_domains, Color::Red, size.width); - let top_clients = make_list("Top Clients", &stats.top_clients, Color::Cyan, size.width); - - let constraints = if size.height > 42 { - vec![ - Constraint::Percentage(30), - Constraint::Min(1), - Constraint::Percentage(20) - ] - } else { - vec![ - Constraint::Percentage(30), - Constraint::Min(1), - Constraint::Percentage(0) - ] - }; - - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints(&*constraints) - .split(size); - - // Split the top part (charts + gauge) into left (gauge + block) and right (line chart) - let top_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints( - [ - Constraint::Percentage(30), - Constraint::Percentage(70), - ] - .as_ref(), - ) - .split(chunks[0]); - - // Split the left part of top (gauge + block) into top (gauge) and bottom (block) - let left_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Min(0), - Constraint::Length(3), - ] - .as_ref(), - ) - .split(top_chunks[0]); - - let bottom_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints( - [ - Constraint::Percentage(25), - Constraint::Percentage(25), - Constraint::Percentage(25), - Constraint::Percentage(25), - ] - .as_ref(), - ) - .split(chunks[2]); - - // Render the widgets to the UI - f.render_widget(paragraph, left_chunks[0]); - f.render_widget(gauge, left_chunks[1]); - f.render_widget(graph, top_chunks[1]); - f.render_widget(table, chunks[1]); - if size.height > 42 { - f.render_widget(filters_list, bottom_chunks[0]); - f.render_widget(top_queried_domains, bottom_chunks[1]); - f.render_widget(top_blocked_domains, bottom_chunks[2]); - f.render_widget(top_clients, bottom_chunks[3]); - } - })?; - - // Check for user input events - if poll(Duration::from_millis(100))? { - match read()? { - Event::Key(KeyEvent { - code: KeyCode::Char('q'), - .. - }) => { - // std::process::exit(0); - shutdown.notify_waiters(); - break; - } - Event::Key(KeyEvent { - code: KeyCode::Char('Q'), - .. - }) => { - shutdown.notify_waiters(); - break; - } - Event::Key(KeyEvent { - code: KeyCode::Char('c'), - modifiers: KeyModifiers::CONTROL, - }) => { - shutdown.notify_waiters(); - break; - } - Event::Resize(_, _) => {}, // Handle resize event, loop will redraw the UI - _ => {} - } - } + }, + }; + let mut stats = match stats_rx.recv().await { + Some(stats) => stats, + None => break, + }; + let status = match status_rx.recv().await { + Some(status) => status, + None => break, + }; + + // Prepare the data for the chart + prepare_chart_data(&mut stats); + + terminal.draw(|f| { + let size = f.size(); + + // Make the charts + let gauge = make_gauge(&stats); + let table = make_query_table(&data, size.width); + let graph = make_history_chart(&stats); + let paragraph = render_status_paragraph(&status, &stats); + let filter_items: &[Filter] = filters.filters.as_deref().unwrap_or(&[]); + let filters_list = make_filters_list(filter_items, size.width); + let top_queried_domains = make_list( + "Top Queried Domains", + &stats.top_queried_domains, + Color::Green, + size.width, + ); + let top_blocked_domains = make_list( + "Top Blocked Domains", + &stats.top_blocked_domains, + Color::Red, + size.width, + ); + let top_clients = make_list("Top Clients", &stats.top_clients, Color::Cyan, size.width); + + let constraints = if size.height > 42 { + vec![ + Constraint::Percentage(30), + Constraint::Min(1), + Constraint::Percentage(20), + ] + } else { + vec![ + Constraint::Percentage(30), + Constraint::Min(1), + Constraint::Percentage(0), + ] + }; + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints(&*constraints) + .split(size); + + // Split the top part (charts + gauge) into left (gauge + block) and right (line chart) + let top_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(30), Constraint::Percentage(70)].as_ref()) + .split(chunks[0]); + + // Split the left part of top (gauge + block) into top (gauge) and bottom (block) + let left_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(0), Constraint::Length(3)].as_ref()) + .split(top_chunks[0]); + + let bottom_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + ] + .as_ref(), + ) + .split(chunks[2]); + + // Render the widgets to the UI + f.render_widget(paragraph, left_chunks[0]); + f.render_widget(gauge, left_chunks[1]); + f.render_widget(graph, top_chunks[1]); + f.render_widget(table, chunks[1]); + if size.height > 42 { + f.render_widget(filters_list, bottom_chunks[0]); + f.render_widget(top_queried_domains, bottom_chunks[1]); + f.render_widget(top_blocked_domains, bottom_chunks[2]); + f.render_widget(top_clients, bottom_chunks[3]); + } + })?; + } + + // Signal shutdown to the input task and fetcher + let _ = shutdown_tx.send(true); + let _ = input_task.await; + Ok(()) +} - } +/// Enables raw mode + alternate screen, and restores them on drop. +struct TerminalGuard; - terminal.show_cursor()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - disable_raw_mode()?; - Ok(()) +impl TerminalGuard { + fn new() -> Result { + enable_raw_mode()?; + execute!(stdout(), EnterAlternateScreen, EnableMouseCapture)?; + Ok(Self) + } } +impl Drop for TerminalGuard { + fn drop(&mut self) { + let _ = execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture, Show); + let _ = disable_raw_mode(); + } +} + +/// Returns `true` if a key event should quit the app: `q`, `Q`, or Ctrl+C. +fn is_quit_key(key: KeyEvent) -> bool { + match key.code { + KeyCode::Char('q') | KeyCode::Char('Q') => true, + KeyCode::Char('c') => key.modifiers.contains(KeyModifiers::CONTROL), + _ => false, + } +} diff --git a/src/welcome.rs b/src/welcome.rs index 382d3cb..826818f 100644 --- a/src/welcome.rs +++ b/src/welcome.rs @@ -1,27 +1,28 @@ +use base64::{engine::general_purpose::STANDARD, Engine as _}; +use colored::*; +use reqwest::{Client, Error}; use std::{ - io:: {self, Write}, - env, - time::Duration + env, + io::{self, Write}, + time::Duration, }; -use reqwest::{Client, Error}; -use colored::*; -use serde_json::Value; +use semver::Version; use serde::Deserialize; -use semver::{Version}; +use serde_json::Value; /// Reusable function that just prints success messages to the console fn print_info(text: &str, is_secondary: bool) { - if is_secondary { - println!("{}", text.green().italic().dimmed()); - } else { - println!("{}", text.green()); - }; + if is_secondary { + println!("{}", text.green().italic().dimmed()); + } else { + println!("{}", text.green()); + }; } /// Prints the AdGuardian ASCII art to console fn print_ascii_art() { - let art = r" + let art = r" β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•— β–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•— β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β•β• β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘ @@ -29,202 +30,243 @@ fn print_ascii_art() { β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘ β•šβ•β• β•šβ•β•β•šβ•β•β•β•β•β• β•šβ•β•β•β•β•β• β•šβ•β•β•β•β•β• β•šβ•β• β•šβ•β•β•šβ•β• β•šβ•β•β•šβ•β•β•β•β•β• β•šβ•β•β•šβ•β• β•šβ•β•β•šβ•β• β•šβ•β•β•β• "; - print_info(art, false); - print_info("\nWelcome to AdGuardian Terminal Edition!", false); - print_info("Terminal-based, real-time traffic monitoring and statistics for your AdGuard Home instance", true); - print_info("For documentation and support, please visit: https://github.com/lissy93/adguardian-term", true); + print_info(art, false); + print_info("\nWelcome to AdGuardian Terminal Edition!", false); + print_info( + "Terminal-based, real-time traffic monitoring and statistics for your AdGuard Home instance", + true, + ); + print_info( + "For documentation and support, please visit: https://github.com/lissy93/adguardian-term", + true, + ); } /// Print error message, along with (optional) stack trace, then exit fn print_error(message: &str, sub_message: &str, error: Option<&Error>) { - eprintln!( - "{}{}{}", - format!("{}", message).red(), - match error { - Some(err) => format!("\n{}", err).red().dimmed(), - None => "".red().dimmed(), - }, - format!("\n{}", sub_message).yellow(), - ); + eprintln!( + "{}{}{}", + message.red(), + match error { + Some(err) => format!("\n{}", err).red().dimmed(), + None => "".red().dimmed(), + }, + format!("\n{}", sub_message).yellow(), + ); - std::process::exit(1); + std::process::exit(1); } /// Given a key, get the value from the environmental variables, and print it to the console fn get_env(key: &str) -> Result { - env::var(key).map(|v| { - println!( - "{}", - format!( - "{} is set to {}", - key.bold(), - if key.contains("PASSWORD") { "******" } else { &v } - ) - .green() - ); - v - }) + env::var(key).inspect(|v| { + println!( + "{}", + format!( + "{} is set to {}", + key.bold(), + if key.contains("PASSWORD") { + "******" + } else { + v + } + ) + .green() + ); + }) } /// Given a possibly undefined version number, check if it's present and supported fn check_version(version: Option<&str>) { - let min_version = Version::parse("0.107.29").unwrap(); - - match version { - Some(version_str) => { - let adguard_version = Version::parse(version_str.strip_prefix('v').unwrap_or(version_str)).unwrap(); - - if adguard_version < min_version { - print_error( - "AdGuard Home version is too old, and is now unsupported", - format!("You're running AdGuard {}. Please upgrade to v{} or later.", version_str, min_version.to_string()).as_str(), - None, - ); - } - }, - None => { - print_error( - "Unsupported AdGuard Home version", - format!( - concat!( - "Failed to get the version number of your AdGuard Home instance.\n", - "This usually means you're running an old, and unsupported version.\n", - "Please upgrade to v{} or later." - ), min_version.to_string() - ).as_str(), - None, - ); - } + let min_version = Version::parse("0.107.29").unwrap(); + + match version { + Some(version_str) => { + match Version::parse(version_str.strip_prefix('v').unwrap_or(version_str)) { + Ok(adguard_version) if adguard_version < min_version => print_error( + "AdGuard Home version is too old, and is now unsupported", + format!( + "You're running AdGuard {}. Please upgrade to v{} or later.", + version_str, min_version + ) + .as_str(), + None, + ), + Ok(_) => {} + Err(_) => print_error( + "Unsupported AdGuard Home version", + "Couldn't parse the version number reported by your AdGuard Home instance.", + None, + ), + } + } + None => { + print_error( + "Unsupported AdGuard Home version", + format!( + concat!( + "Failed to get the version number of your AdGuard Home instance.\n", + "This usually means you're running an old, and unsupported version.\n", + "Please upgrade to v{} or later." + ), + min_version + ) + .as_str(), + None, + ); } + } } /// With the users specified AdGuard details, verify the connection (exit on fail) async fn verify_connection( - client: &Client, - ip: String, - port: String, - protocol: String, - username: String, - password: String, + client: &Client, + ip: String, + port: String, + protocol: String, + username: String, + password: String, ) -> Result<(), Box> { - println!("{}", "\nVerifying connection to your AdGuard instance...".blue()); + println!( + "{}", + "\nVerifying connection to your AdGuard instance...".blue() + ); - let auth_string = format!("{}:{}", username, password); - let auth_header_value = format!("Basic {}", base64::encode(&auth_string)); - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert("Authorization", auth_header_value.parse()?); + let auth_string = format!("{}:{}", username, password); + let auth_header_value = format!("Basic {}", STANDARD.encode(&auth_string)); + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert("Authorization", auth_header_value.parse()?); - let url = format!("{}://{}:{}/control/status", protocol, ip, port); + let url = format!("{}://{}:{}/control/status", protocol, ip, port); - match client - .get(&url) - .headers(headers) - .timeout(Duration::from_secs(2)) - .send() - .await { - Ok(res) if res.status().is_success() => { - // Get version string (if present), and check if valid - exit if not - let body: Value = res.json().await?; - check_version(body["version"].as_str()); - // All good! Print success message :) - let safe_version = body["version"].as_str().unwrap_or("mystery version"); - println!("{}", format!("AdGuard ({}) connection successful!\n", safe_version).green()); - Ok(()) - } - // Connection failed to authenticate. Print error and exit - Ok(_) => { - print_error( - &format!("Authentication with AdGuard at {}:{} failed", ip, port), - "Please check your environmental variables and try again.", - None, - ); - Ok(()) - }, - // Connection failed to establish. Print error and exit - Err(e) => { - print_error( - &format!("Failed to connect to AdGuard at: {}:{}", ip, port), - "Please check your environmental variables and try again.", - Some(&e), - ); - Ok(()) - } + match client + .get(&url) + .headers(headers) + .timeout(Duration::from_secs(2)) + .send() + .await + { + Ok(res) if res.status().is_success() => { + // Get version string (if present), and check if valid - exit if not + let body: Value = res.json().await?; + check_version(body["version"].as_str()); + // All good! Print success message :) + let safe_version = body["version"].as_str().unwrap_or("mystery version"); + println!( + "{}", + format!("AdGuard ({}) connection successful!\n", safe_version).green() + ); + Ok(()) + } + // Connection failed to authenticate. Print error and exit + Ok(_) => { + print_error( + &format!("Authentication with AdGuard at {}:{} failed", ip, port), + "Please check your environmental variables and try again.", + None, + ); + Ok(()) } + // Connection failed to establish. Print error and exit + Err(e) => { + print_error( + &format!("Failed to connect to AdGuard at: {}:{}", ip, port), + "Please check your environmental variables and try again.", + Some(&e), + ); + Ok(()) + } + } } #[derive(Deserialize)] struct CratesIoResponse { - #[serde(rename = "crate")] - krate: Crate, + #[serde(rename = "crate")] + krate: Crate, } #[derive(Deserialize)] struct Crate { - max_version: String, + max_version: String, } /// Gets the latest version of the crate from crates.io async fn get_latest_version(crate_name: &str) -> Result> { - let url = format!("https://crates.io/api/v1/crates/{}", crate_name); - let client = reqwest::Client::new(); - let res = client.get(&url) - .header(reqwest::header::USER_AGENT, "version_check (adguardian.as93.net)") - .send() - .await?; + let url = format!("https://crates.io/api/v1/crates/{}", crate_name); + let client = reqwest::Client::new(); + let res = client + .get(&url) + .header( + reqwest::header::USER_AGENT, + "version_check (adguardian.as93.net)", + ) + .send() + .await?; - if res.status().is_success() { - let response: CratesIoResponse = res.json().await?; - Ok(response.krate.max_version) - } else { - let status = res.status(); - let body = res.text().await?; - Err(format!("Request failed with status {}: body: {}", status, body).into()) - } + if res.status().is_success() { + let response: CratesIoResponse = res.json().await?; + Ok(response.krate.max_version) + } else { + let status = res.status(); + let body = res.text().await?; + Err(format!("Request failed with status {}: body: {}", status, body).into()) + } } /// Checks for updates to the crate, and prints a message if an update is available async fn check_for_updates() { - // Get crate name and version from Cargo.toml - let crate_name = env!("CARGO_PKG_NAME"); - let crate_version = env!("CARGO_PKG_VERSION"); - println!("{}", "\nChecking for updates...".blue()); - // Parse the current version, and fetch and parse the latest version - let current_version = Version::parse(crate_version).unwrap_or_else(|_| { - Version::parse("0.0.0").unwrap() - }); - let latest_version = Version::parse( - &get_latest_version(crate_name).await.unwrap_or_else(|_| { - "0.0.0".to_string() - }) - ).unwrap(); + // Get crate name and version from Cargo.toml + let crate_name = env!("CARGO_PKG_NAME"); + let crate_version = env!("CARGO_PKG_VERSION"); + println!("{}", "\nChecking for updates...".blue()); + // Parse the current version, and fetch and parse the latest version + let current_version = + Version::parse(crate_version).unwrap_or_else(|_| Version::parse("0.0.0").unwrap()); + let latest_version = Version::parse( + &get_latest_version(crate_name) + .await + .unwrap_or_else(|_| "0.0.0".to_string()), + ) + .unwrap_or_else(|_| Version::parse("0.0.0").unwrap()); - // Compare the current and latest versions, and print the appropriate message - if current_version == Version::parse("0.0.0").unwrap() || latest_version == Version::parse("0.0.0").unwrap() { - println!("{}", "Unable to check for updates".yellow()); - } else if current_version < latest_version { - println!("{}", - format!( - "A new version of AdGuardian is available.\nUpdate from {} to {} for the best experience", - current_version.to_string().bold(), - latest_version.to_string().bold() - ).yellow() - ); - } else if current_version == latest_version { - println!( - "{}", - format!("AdGuardian is up-to-date, running version {}", current_version.to_string().bold()).green() - ); - } else if current_version > latest_version { - println!( - "{}", - format!("Running a pre-released edition of AdGuardian, version {}", current_version.to_string().bold()).green() - ); - } else { - println!("{}", "Unable to check for updates".yellow()); - } + // Compare the current and latest versions, and print the appropriate message + if current_version == Version::parse("0.0.0").unwrap() + || latest_version == Version::parse("0.0.0").unwrap() + { + println!("{}", "Unable to check for updates".yellow()); + } else if current_version < latest_version { + println!( + "{}", + format!( + "A new version of AdGuardian is available.\nUpdate from {} to {} for the best experience", + current_version.to_string().bold(), + latest_version.to_string().bold() + ) + .yellow() + ); + } else if current_version == latest_version { + println!( + "{}", + format!( + "AdGuardian is up-to-date, running version {}", + current_version.to_string().bold() + ) + .green() + ); + } else if current_version > latest_version { + println!( + "{}", + format!( + "Running a pre-released edition of AdGuardian, version {}", + current_version.to_string().bold() + ) + .green() + ); + } else { + println!("{}", "Unable to check for updates".yellow()); + } } - /// Initiate the welcome script /// This function will: /// - Print the AdGuardian ASCII art @@ -236,64 +278,71 @@ async fn check_for_updates() { /// - Verify the AdGuard Home version is supported /// - Then either print a success message, or show instructions to fix and exit pub async fn welcome() -> Result<(), Box> { - print_ascii_art(); + print_ascii_art(); - // Check for updates - check_for_updates().await; + // Check for updates + check_for_updates().await; - println!("{}", "\nStarting initialization checks...".blue()); + println!("{}", "\nStarting initialization checks...".blue()); - let client = Client::new(); + let client = Client::new(); - // List of available flags, ant their associated env vars - let flags = [ - ("--adguard-ip", "ADGUARD_IP"), - ("--adguard-port", "ADGUARD_PORT"), - ("--adguard-username", "ADGUARD_USERNAME"), - ("--adguard-password", "ADGUARD_PASSWORD"), - ]; + // List of available flags, ant their associated env vars + let flags = [ + ("--adguard-ip", "ADGUARD_IP"), + ("--adguard-port", "ADGUARD_PORT"), + ("--adguard-username", "ADGUARD_USERNAME"), + ("--adguard-password", "ADGUARD_PASSWORD"), + ]; - let protocol: String = env::var("ADGUARD_PROTOCOL").unwrap_or_else(|_| "http".into()).parse()?; - env::set_var("ADGUARD_PROTOCOL", protocol); + let protocol: String = env::var("ADGUARD_PROTOCOL") + .unwrap_or_else(|_| "http".into()) + .parse()?; + env::set_var("ADGUARD_PROTOCOL", protocol); - // Parse command line arguments - let mut args = std::env::args().peekable(); - while let Some(arg) = args.next() { - for &(flag, var) in &flags { - if arg == flag { - if let Some(value) = args.peek() { - env::set_var(var, value); - args.next(); - } - } + // Parse command line arguments + let mut args = std::env::args().peekable(); + while let Some(arg) = args.next() { + for &(flag, var) in &flags { + if arg == flag { + if let Some(value) = args.peek() { + env::set_var(var, value); + args.next(); } + } } + } - // If any of the env variables or flags are not yet set, prompt the user to enter them - for &key in &["ADGUARD_IP", "ADGUARD_PORT", "ADGUARD_USERNAME", "ADGUARD_PASSWORD"] { - if env::var(key).is_err() { - println!( - "{}", - format!("The {} environmental variable is not yet set", key.bold()).yellow() - ); - print!("{}", format!("β€Ί Enter a value for {}: ", key).blue().bold()); - io::stdout().flush()?; + // If any of the env variables or flags are not yet set, prompt the user to enter them + for &key in &[ + "ADGUARD_IP", + "ADGUARD_PORT", + "ADGUARD_USERNAME", + "ADGUARD_PASSWORD", + ] { + if env::var(key).is_err() { + println!( + "{}", + format!("The {} environmental variable is not yet set", key.bold()).yellow() + ); + print!("{}", format!("β€Ί Enter a value for {}: ", key).blue().bold()); + io::stdout().flush()?; - let mut value = String::new(); - io::stdin().read_line(&mut value)?; - env::set_var(key, value.trim()); - } + let mut value = String::new(); + io::stdin().read_line(&mut value)?; + env::set_var(key, value.trim()); } + } + + // Grab the values of the (now set) environmental variables + let ip = get_env("ADGUARD_IP")?; + let port = get_env("ADGUARD_PORT")?; + let protocol = get_env("ADGUARD_PROTOCOL")?; + let username = get_env("ADGUARD_USERNAME")?; + let password = get_env("ADGUARD_PASSWORD")?; - // Grab the values of the (now set) environmental variables - let ip = get_env("ADGUARD_IP")?; - let port = get_env("ADGUARD_PORT")?; - let protocol = get_env("ADGUARD_PROTOCOL")?; - let username = get_env("ADGUARD_USERNAME")?; - let password = get_env("ADGUARD_PASSWORD")?; - - // Verify that we can connect, authenticate, and that version is supported (exit on failure) - verify_connection(&client, ip, port, protocol, username, password).await?; + // Verify that we can connect, authenticate, and that version is supported (exit on failure) + verify_connection(&client, ip, port, protocol, username, password).await?; - Ok(()) + Ok(()) } diff --git a/src/widgets/chart.rs b/src/widgets/chart.rs index bb1da66..37371ee 100644 --- a/src/widgets/chart.rs +++ b/src/widgets/chart.rs @@ -1,56 +1,59 @@ - use tui::{ - style::{Color, Modifier, Style}, - widgets::{Axis, Block, Borders, Dataset, Chart}, - text::{Span}, - symbols, + style::{Color, Modifier, Style}, + symbols, + text::Span, + widgets::{Axis, Block, Borders, Chart, Dataset}, }; -use crate::fetch::fetch_stats::{StatsResponse}; - +use crate::fetch::fetch_stats::StatsResponse; pub fn make_history_chart(stats: &StatsResponse) -> Chart<'_> { - // Convert datasets into vector that can be consumed by chart - let datasets = make_history_datasets(stats); - // Find uppermost x and y-axis bounds for chart - let (x_bound, y_bound) = find_bounds(stats); - // Generate incremental labels from data's values, to render on axis - let x_labels = generate_x_labels(stats.dns_queries.len() as i32, 5); - let y_labels = generate_y_labels(y_bound as i32, 5); - // Create chart - let chart = Chart::new(datasets) - .block( - Block::default() - .title(Span::styled( - "History", - Style::default().add_modifier(Modifier::BOLD), - )) - .borders(Borders::ALL) - ) - .x_axis( - Axis::default() - .title("Time (Days ago)") - .bounds([0.0, x_bound]) - .labels(x_labels), - ) - .y_axis(Axis::default().title("Query Count").labels(y_labels).bounds([0.0, y_bound])); - - chart + // Convert datasets into vector that can be consumed by chart + let datasets = make_history_datasets(stats); + // Find uppermost x and y-axis bounds for chart + let (x_bound, y_bound) = find_bounds(stats); + // Generate incremental labels from data's values, to render on axis + let x_labels = generate_x_labels(stats.dns_queries.len() as i32, 5); + let y_labels = generate_y_labels(y_bound as i32, 5); + // Create chart + let chart = Chart::new(datasets) + .block( + Block::default() + .title(Span::styled( + "History", + Style::default().add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL), + ) + .x_axis( + Axis::default() + .title("Time (Days ago)") + .bounds([0.0, x_bound]) + .labels(x_labels), + ) + .y_axis( + Axis::default() + .title("Query Count") + .labels(y_labels) + .bounds([0.0, y_bound]), + ); + + chart } // Returns a dataset that's consumable by the chart widget fn make_history_datasets(stats: &StatsResponse) -> Vec> { let dns_queries_dataset = Dataset::default() - .name("DNS Queries") - .marker(symbols::Marker::Braille) - .style(Style::default().fg(Color::Green)) - .data(&stats.dns_queries_chart); + .name("DNS Queries") + .marker(symbols::Marker::Braille) + .style(Style::default().fg(Color::Green)) + .data(&stats.dns_queries_chart); let blocked_filtering_dataset = Dataset::default() - .name("Blocked Filtering") - .marker(symbols::Marker::Braille) - .style(Style::default().fg(Color::Red)) - .data(&stats.blocked_filtering_chart); + .name("Blocked Filtering") + .marker(symbols::Marker::Braille) + .style(Style::default().fg(Color::Red)) + .data(&stats.blocked_filtering_chart); let datasets = vec![dns_queries_dataset, blocked_filtering_dataset]; @@ -59,88 +62,102 @@ fn make_history_datasets(stats: &StatsResponse) -> Vec> { // Determine the uppermost bounds for the x and y axis fn find_bounds(stats: &StatsResponse) -> (f64, f64) { - let mut max_length = 0; - let mut max_value = f64::MIN; - - for dataset in &[&stats.dns_queries_chart, &stats.blocked_filtering_chart] { - let length = dataset.len(); - if length > max_length { - max_length = length; - } - - let max_in_dataset = dataset - .iter() - .map(|&(_, y)| y) - .fold(f64::MIN, f64::max); - if max_in_dataset > max_value { - max_value = max_in_dataset; - } + let mut max_length = 0; + // Floor at 0 so empty data can't leave this at f64::MIN (which would later + // overflow when cast to an axis-label step) + let mut max_value = 0.0; + + for dataset in &[&stats.dns_queries_chart, &stats.blocked_filtering_chart] { + let length = dataset.len(); + if length > max_length { + max_length = length; } - (max_length as f64, max_value) + + let max_in_dataset = dataset.iter().map(|&(_, y)| y).fold(f64::MIN, f64::max); + if max_in_dataset > max_value { + max_value = max_in_dataset; + } + } + (max_length as f64, max_value) } // Generate periodic labels to render on the y-axis (query count) fn generate_y_labels(max: i32, count: usize) -> Vec> { let step = max / (count - 1) as i32; - + (0..count) - .map(|x| Span::raw(format!("{}", x * step as usize))) - .collect::>>() + .map(|x| Span::raw(format!("{}", x * step as usize))) + .collect::>>() } // Generate periodic labels to render on the x-axis (days ago) fn generate_x_labels(max_days: i32, num_labels: i32) -> Vec> { - let step = max_days / (num_labels - 1); - (0..num_labels) - .map(|i| { - let day = (max_days - i * step).to_string(); - if i == num_labels - 1 { - Span::styled("Today", Style::default().add_modifier(Modifier::BOLD)) - } else { - Span::raw(day) - } - }) - .collect() + let step = max_days / (num_labels - 1); + (0..num_labels) + .map(|i| { + let day = (max_days - i * step).to_string(); + if i == num_labels - 1 { + Span::styled("Today", Style::default().add_modifier(Modifier::BOLD)) + } else { + Span::raw(day) + } + }) + .collect() } // Formats vector data into a format that can be consumed by the chart widget fn convert_to_chart_data(data: Vec) -> Vec<(f64, f64)> { - data.iter().enumerate().map(|(i, &v)| (i as f64, v)).collect() + data + .iter() + .enumerate() + .map(|(i, &v)| (i as f64, v)) + .collect() } // Interpolates data, adding n number of points, to make the chart look smoother -fn interpolate(input: &Vec, points_between: usize) -> Vec { - let mut output = Vec::new(); - - for window in input.windows(2) { - let start = window[0]; - let end = window[1]; - let step = (end - start) / (points_between as f64 + 1.0); - - output.push(start); - for i in 1..=points_between { - output.push(start + step * i as f64); - } +fn interpolate(input: &[f64], points_between: usize) -> Vec { + // Nothing to interpolate between - avoids a panic on the `last()` below + let Some(&last) = input.last() else { + return Vec::new(); + }; + + let mut output = Vec::new(); + for window in input.windows(2) { + let start = window[0]; + let end = window[1]; + let step = (end - start) / (points_between as f64 + 1.0); + + output.push(start); + for i in 1..=points_between { + output.push(start + step * i as f64); } + } - output.push(*input.last().unwrap()); - output + output.push(last); + output } // Adds data formatted for the time-series chart to the stats object pub fn prepare_chart_data(stats: &mut StatsResponse) { - let dns_queries = stats.dns_queries.iter().map(|&v| v as f64).collect::>(); - let interpolated_dns_queries = interpolate(&dns_queries, 3); - stats.dns_queries_chart = convert_to_chart_data(interpolated_dns_queries); - - let blocked_filtering: Vec = stats.blocked_filtering.iter() - .zip(&stats.replaced_safebrowsing) - .zip(&stats.replaced_parental) - .map(|((&b, &s), &p)| (b + s + p) as f64) - .collect(); - - let interpolated_blocked_filtering = interpolate(&blocked_filtering, 3); - let blocked_filtering_chart: Vec<(f64, f64)> = convert_to_chart_data(interpolated_blocked_filtering); - - stats.blocked_filtering_chart = blocked_filtering_chart; + let dns_queries = stats + .dns_queries + .iter() + .map(|&v| v as f64) + .collect::>(); + let interpolated_dns_queries = interpolate(&dns_queries, 3); + stats.dns_queries_chart = convert_to_chart_data(interpolated_dns_queries); + + let blocked_filtering: Vec = stats + .blocked_filtering + .iter() + .zip(&stats.replaced_safebrowsing) + .zip(&stats.replaced_parental) + .map(|((&b, &s), &p)| (b + s + p) as f64) + .collect(); + + let interpolated_blocked_filtering = interpolate(&blocked_filtering, 3); + let blocked_filtering_chart: Vec<(f64, f64)> = + convert_to_chart_data(interpolated_blocked_filtering); + + stats.blocked_filtering_chart = blocked_filtering_chart; } diff --git a/src/widgets/filters.rs b/src/widgets/filters.rs index 52148a0..f37803c 100644 --- a/src/widgets/filters.rs +++ b/src/widgets/filters.rs @@ -1,45 +1,50 @@ -// filters.rs - use tui::{ + style::{Color, Modifier, Style}, text::{Span, Spans}, widgets::{Block, Borders, List, ListItem}, - style::{Color, Style, Modifier}, }; use crate::fetch::fetch_filters::Filter; fn truncate(text: &str, width: usize) -> String { if text.chars().count() <= width { - text.to_string() + text.to_string() } else { - text.chars().take(width - 3).collect::() + "..." + text + .chars() + .take(width.saturating_sub(3)) + .collect::() + + "..." } } -pub fn make_filters_list(filters: &[Filter], width: u16) -> List { +pub fn make_filters_list(filters: &[Filter], width: u16) -> List<'_> { let items: Vec = filters .iter() .map(|filter| { - let (status_text, color) = if filter.enabled { - ("βœ”", Color::Green) - } else { - ("✘", Color::Red) - }; - let status = Span::styled(status_text, Style::default().fg(color)); - let rule_count = Span::styled(format!(" ({})", filter.rules_count), Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD)); - let name = Span::raw(format!(" {}", truncate(&filter.name, width as usize / 4 - 12))); - let content = Spans::from(vec![status, name, rule_count]); - ListItem::new(content) + let (status_text, color) = if filter.enabled { + ("βœ”", Color::Green) + } else { + ("✘", Color::Red) + }; + let status = Span::styled(status_text, Style::default().fg(color)); + let rule_count = Span::styled( + format!(" ({})", filter.rules_count), + Style::default() + .fg(Color::Magenta) + .add_modifier(Modifier::BOLD), + ); + let name = Span::raw(format!( + " {}", + truncate(&filter.name, (width as usize / 4).saturating_sub(12)) + )); + let content = Spans::from(vec![status, name, rule_count]); + ListItem::new(content) }) .collect(); - List::new(items) - .block( - Block::default() - .borders(Borders::ALL) - .title(Span::styled( - "Filters", - Style::default().add_modifier(Modifier::BOLD), - )), - ) + List::new(items).block(Block::default().borders(Borders::ALL).title(Span::styled( + "Filters", + Style::default().add_modifier(Modifier::BOLD), + ))) } diff --git a/src/widgets/gauge.rs b/src/widgets/gauge.rs index 0209424..feb0929 100644 --- a/src/widgets/gauge.rs +++ b/src/widgets/gauge.rs @@ -1,30 +1,35 @@ use tui::{ - style::{Color, Style, Modifier}, + style::{Color, Modifier, Style}, + text::Span, widgets::{Block, Borders, Gauge}, - text::{Span}, }; use crate::fetch::fetch_stats::StatsResponse; -pub fn make_gauge(stats: &StatsResponse) -> Gauge { - +pub fn make_gauge(stats: &StatsResponse) -> Gauge<'_> { let total_blocked = stats.num_blocked_filtering + stats.num_replaced_parental + stats.num_replaced_safebrowsing + stats.num_replaced_safesearch; - let percent = (total_blocked as f64 / stats.num_dns_queries as f64 * 100.0) as u16; + // `max(1)` avoids a divide-by-zero, and the clamp keeps it in the 0..=100 + // range that `Gauge::percent` requires (it panics otherwise) + let percent = + (total_blocked as f64 / stats.num_dns_queries.max(1) as f64 * 100.0).min(100.0) as u16; - let label = format!("Blocked {} out of {} ({}%)", total_blocked, stats.num_dns_queries, percent); + let label = format!( + "Blocked {} out of {} ({}%)", + total_blocked, stats.num_dns_queries, percent + ); Gauge::default() - .block( - Block::default() + .block( + Block::default() .title(Span::styled( "Block Percentage", Style::default().add_modifier(Modifier::BOLD), )) - .borders(Borders::ALL) + .borders(Borders::ALL), ) .gauge_style(Style::default().fg(Color::Red).bg(Color::Green)) .percent(percent) diff --git a/src/widgets/list.rs b/src/widgets/list.rs index 49ecf79..dbd8d0c 100644 --- a/src/widgets/list.rs +++ b/src/widgets/list.rs @@ -1,38 +1,43 @@ - use crate::fetch::fetch_stats::DomainData; - use tui::{ + style::{Color, Modifier, Style}, text::{Span, Spans}, widgets::{Block, Borders, List, ListItem}, - style::{Color, Style, Modifier}, }; fn truncate(text: &str, width: usize) -> String { if text.chars().count() <= width { - text.to_string() + text.to_string() } else { - text.chars().take(width - 3).collect::() + "..." + text + .chars() + .take(width.saturating_sub(3)) + .collect::() + + "..." } } pub fn make_list<'a>(title: &'a str, data: &[DomainData], color: Color, width: u16) -> List<'a> { let items: Vec = data - .iter() - .map(|data| { - - let name = Span::raw(format!(" {}", truncate(&data.name, width as usize / 4 - 12))); - let count = Span::styled(format!(" ({})", data.count), Style::default().fg(color).add_modifier(Modifier::BOLD)); - ListItem::new(Spans::from(vec![name, count])) - }) - .collect(); + .iter() + .map(|data| { + let name = Span::raw(format!( + " {}", + truncate(&data.name, (width as usize / 4).saturating_sub(12)) + )); + let count = Span::styled( + format!(" ({})", data.count), + Style::default().fg(color).add_modifier(Modifier::BOLD), + ); + ListItem::new(Spans::from(vec![name, count])) + }) + .collect(); List::new(items) - .block(Block::default().borders(Borders::ALL) - .title(Span::styled( - title, - Style::default().add_modifier(Modifier::BOLD), - ))) - .highlight_style(Style::default().add_modifier(Modifier::BOLD)) + .block(Block::default().borders(Borders::ALL).title(Span::styled( + title, + Style::default().add_modifier(Modifier::BOLD), + ))) + .highlight_style(Style::default().add_modifier(Modifier::BOLD)) } - diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index c3cfa0c..b36a30d 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -1,6 +1,6 @@ -pub mod gauge; -pub mod table; pub mod chart; +pub mod filters; +pub mod gauge; pub mod list; pub mod status; -pub mod filters; +pub mod table; diff --git a/src/widgets/status.rs b/src/widgets/status.rs index 2e71012..2565855 100644 --- a/src/widgets/status.rs +++ b/src/widgets/status.rs @@ -1,65 +1,80 @@ use tui::{ - style::{Color, Style, Modifier}, + style::{Color, Modifier, Style}, text::{Span, Spans}, - widgets::{Paragraph, Wrap, Borders, Block}, + widgets::{Block, Borders, Paragraph, Wrap}, }; -use crate::fetch::fetch_status::StatusResponse; use crate::fetch::fetch_stats::StatsResponse; +use crate::fetch::fetch_status::StatusResponse; -pub fn render_status_paragraph<'a>(status: &'a StatusResponse, stats: &'a StatsResponse) -> Paragraph<'a> { - - let block = Block::default() - .borders(Borders::ALL) - .title(Span::styled( - "Status", - Style::default().add_modifier(Modifier::BOLD), - )); +pub fn render_status_paragraph<'a>( + status: &'a StatusResponse, + stats: &'a StatsResponse, +) -> Paragraph<'a> { + let block = Block::default().borders(Borders::ALL).title(Span::styled( + "Status", + Style::default().add_modifier(Modifier::BOLD), + )); - let get_color = |enabled: bool| { - if enabled { Color::Green } else { Color::Red } - }; + let get_color = |enabled: bool| { + if enabled { + Color::Green + } else { + Color::Red + } + }; - let value_style = Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD); + let value_style = Style::default() + .fg(Color::Blue) + .add_modifier(Modifier::BOLD); - let coloured = |color: Color| { - Style::default().fg(color).add_modifier(Modifier::BOLD) - }; - + let coloured = |color: Color| Style::default().fg(color).add_modifier(Modifier::BOLD); let text = vec![ - Spans::from(vec![ - Span::styled("Running: ", Style::default()), - Span::styled( - format!("{}", status.running), - Style::default().fg(get_color(status.running)).add_modifier(Modifier::BOLD) - ), + Spans::from(vec![ + Span::styled("Running: ", Style::default()), + Span::styled( + format!("{}", status.running), + Style::default() + .fg(get_color(status.running)) + .add_modifier(Modifier::BOLD), + ), ]), - Spans::from(vec![ - Span::styled("Protection Enabled: ", Style::default()), - Span::styled( - format!("{}", status.protection_enabled), - Style::default().fg(get_color(status.protection_enabled)).add_modifier(Modifier::BOLD) - ), + Spans::from(vec![ + Span::styled("Protection Enabled: ", Style::default()), + Span::styled( + format!("{}", status.protection_enabled), + Style::default() + .fg(get_color(status.protection_enabled)) + .add_modifier(Modifier::BOLD), + ), ]), - Spans::from(vec![ - Span::styled("DHCP Available: ", Style::default()), - Span::styled( - format!("{}", status.dhcp_available), - Style::default().fg(get_color(status.dhcp_available)).add_modifier(Modifier::BOLD) - ), + Spans::from(vec![ + Span::styled("DHCP Available: ", Style::default()), + Span::styled( + format!("{}", status.dhcp_available), + Style::default() + .fg(get_color(status.dhcp_available)) + .add_modifier(Modifier::BOLD), + ), ]), Spans::from(vec![ Span::styled("Avg Processing Time: ", Style::default()), - Span::styled(format!("{}ms", (stats.avg_processing_time * 1000.0) as i16), value_style), + Span::styled( + format!("{}ms", (stats.avg_processing_time * 1000.0) as i16), + value_style, + ), ]), Spans::from(vec![ Span::styled("Version: ", Style::default()), Span::styled(status.version.to_string(), value_style), ]), Spans::from(vec![ - Span::styled("Ports: ", Style::default()), - Span::styled(format!(":{} (DNS), :{} (HTTP)", status.dns_port, status.http_port), value_style), + Span::styled("Ports: ", Style::default()), + Span::styled( + format!(":{} (DNS), :{} (HTTP)", status.dns_port, status.http_port), + value_style, + ), ]), Spans::from(vec![ Span::styled("Total Queries: ", Style::default()), @@ -67,22 +82,32 @@ pub fn render_status_paragraph<'a>(status: &'a StatusResponse, stats: &'a StatsR ]), Spans::from(vec![ Span::styled("Filtered: ", Style::default()), - Span::styled(stats.num_blocked_filtering.to_string(), coloured(Color::Yellow)), + Span::styled( + stats.num_blocked_filtering.to_string(), + coloured(Color::Yellow), + ), ]), Spans::from(vec![ Span::styled("Malware Blocked: ", Style::default()), - Span::styled(stats.num_replaced_safebrowsing.to_string(), coloured(Color::Red)), + Span::styled( + stats.num_replaced_safebrowsing.to_string(), + coloured(Color::Red), + ), ]), Spans::from(vec![ Span::styled("Parental Controls: ", Style::default()), - Span::styled(stats.num_replaced_parental.to_string(), coloured(Color::Magenta)), + Span::styled( + stats.num_replaced_parental.to_string(), + coloured(Color::Magenta), + ), ]), Spans::from(vec![ Span::styled("Safe Search: ", Style::default()), - Span::styled(stats.num_replaced_safesearch.to_string(), coloured(Color::Cyan)), + Span::styled( + stats.num_replaced_safesearch.to_string(), + coloured(Color::Cyan), + ), ]), ]; - Paragraph::new(text) - .wrap(Wrap { trim: true }) - .block(block) + Paragraph::new(text).wrap(Wrap { trim: true }).block(block) } diff --git a/src/widgets/table.rs b/src/widgets/table.rs index 57cbbcb..0607b0b 100644 --- a/src/widgets/table.rs +++ b/src/widgets/table.rs @@ -1,26 +1,26 @@ - +use chrono::{DateTime, Utc}; use tui::{ + layout::Constraint, style::{Color, Modifier, Style}, + text::Span, widgets::{Block, Borders, Cell, Row, Table}, - text::{Span}, - layout::{Constraint}, }; -use chrono::{DateTime, Utc}; use crate::fetch::fetch_query_log::{Query, Question}; pub fn make_query_table(data: &[Query], width: u16) -> Table<'_> { - let rows = data.iter().map(|query| { - let time = Cell::from( - time_ago(query.time.as_str()).unwrap_or("unknown".to_string()) - ).style(Style::default().fg(Color::Gray)); - + let rows = data + .iter() + .map(|query| { + let time = Cell::from(time_ago(query.time.as_str()).unwrap_or("unknown".to_string())) + .style(Style::default().fg(Color::Gray)); + let question = Cell::from(make_request_cell(&query.question).unwrap()) - .style(Style::default().add_modifier(Modifier::BOLD)); + .style(Style::default().add_modifier(Modifier::BOLD)); - let client = Cell::from(query.client.as_str()) - .style(Style::default().fg(Color::Blue)); + let client = Cell::from(query.client.as_str()).style(Style::default().fg(Color::Blue)); - let (time_taken, elapsed_color) = make_time_taken_and_color(&query.elapsed_ms).unwrap(); + let (time_taken, elapsed_color) = make_time_taken_and_color(&query.elapsed_ms) + .unwrap_or_else(|_| ("? ms".to_string(), Color::Gray)); let elapsed_ms = Cell::from(time_taken).style(Style::default().fg(elapsed_color)); let (status_txt, status_color) = block_status_text(&query.reason, query.cached); @@ -30,57 +30,52 @@ pub fn make_query_table(data: &[Query], width: u16) -> Table<'_> { let color = make_row_color(&query.reason); Row::new(vec![time, question, status, elapsed_ms, client, upstream]) - .style(Style::default().fg(color)) - }).collect::>(); + .style(Style::default().fg(color)) + }) + .collect::>(); - - let title = Span::styled( - "Query Log", - Style::default().add_modifier(Modifier::BOLD), - ); + let title = Span::styled("Query Log", Style::default().add_modifier(Modifier::BOLD)); - let block = Block::default() - .title(title) - .borders(Borders::ALL); + let block = Block::default().title(title).borders(Borders::ALL); let mut headers = vec![ - Cell::from(Span::raw("Time")), - Cell::from(Span::raw("Request")), - Cell::from(Span::raw("Status")), - Cell::from(Span::raw("Time Taken")), + Cell::from(Span::raw("Time")), + Cell::from(Span::raw("Request")), + Cell::from(Span::raw("Status")), + Cell::from(Span::raw("Time Taken")), ]; if width > 120 { - headers.extend(vec![ - Cell::from(Span::raw("Client")), - Cell::from(Span::raw("Upstream DNS")), - ]); - - let widths = &[ - Constraint::Percentage(15), - Constraint::Percentage(35), - Constraint::Percentage(10), - Constraint::Percentage(10), - Constraint::Percentage(15), - Constraint::Percentage(15), - ]; - - Table::new(rows) - .header(Row::new(headers)) - .widths(widths) - .block(block) + headers.extend(vec![ + Cell::from(Span::raw("Client")), + Cell::from(Span::raw("Upstream DNS")), + ]); + + let widths = &[ + Constraint::Percentage(15), + Constraint::Percentage(35), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(15), + Constraint::Percentage(15), + ]; + + Table::new(rows) + .header(Row::new(headers)) + .widths(widths) + .block(block) } else { - let widths = &[ - Constraint::Percentage(20), - Constraint::Percentage(40), - Constraint::Percentage(20), - Constraint::Percentage(20), - ]; - - Table::new(rows) - .header(Row::new(headers)) - .widths(widths) - .block(block) + let widths = &[ + Constraint::Percentage(20), + Constraint::Percentage(40), + Constraint::Percentage(20), + Constraint::Percentage(20), + ]; + + Table::new(rows) + .header(Row::new(headers)) + .widths(widths) + .block(block) } } @@ -93,9 +88,9 @@ fn time_ago(timestamp: &str) -> Result { let duration = now - datetime_utc; if duration.num_minutes() < 1 { - Ok(format!("{} sec ago", duration.num_seconds())) + Ok(format!("{} sec ago", duration.num_seconds())) } else { - Ok(format!("{} min ago", duration.num_minutes())) + Ok(format!("{} min ago", duration.num_minutes())) } } @@ -110,11 +105,11 @@ fn make_time_taken_and_color(elapsed: &str) -> Result<(String, Color), anyhow::E let rounded_elapsed = (elapsed_f64 * 100.0).round() / 100.0; let time_taken = format!("{:.2} ms", rounded_elapsed); let color = if elapsed_f64 < 1.0 { - Color::Green + Color::Green } else if (1.0..=20.0).contains(&elapsed_f64) { - Color::Yellow + Color::Yellow } else { - Color::Red + Color::Red }; Ok((time_taken, color)) } @@ -122,26 +117,24 @@ fn make_time_taken_and_color(elapsed: &str) -> Result<(String, Color), anyhow::E // Return color for a row, based on the allow/block reason fn make_row_color(reason: &str) -> Color { if reason == "NotFilteredNotFound" { - Color::Green + Color::Green } else if reason == "FilteredBlackList" { - Color::Red + Color::Red } else { - Color::Yellow + Color::Yellow } } // Return text and color for the status cell based on allow/ block reason fn block_status_text(reason: &str, cached: bool) -> (String, Color) { - let (text, color) = - if reason == "FilteredBlackList" { - ("Blacklisted".to_string(), Color::Red) + let (text, color) = if reason == "FilteredBlackList" { + ("Blacklisted".to_string(), Color::Red) } else if cached { - ("Cached".to_string(), Color::Cyan) + ("Cached".to_string(), Color::Cyan) } else if reason == "NotFilteredNotFound" { - ("Allowed".to_string(), Color::Green) + ("Allowed".to_string(), Color::Green) } else { - ("Other Block".to_string(), Color::Yellow) + ("Other Block".to_string(), Color::Yellow) }; (text, color) } -