From 9624ec846d8de4761db7f75eaff4779a3b36e90a Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:13:17 +0400 Subject: [PATCH] refactor: harden tests --- .github/ISSUE_TEMPLATE/bug-report.yml | 7 +- .github/ISSUE_TEMPLATE/feature-request.yml | 5 + .github/workflows/app-e2e.yml | 120 +- .github/workflows/codeql.yml | 8 + .github/workflows/dependabot-automerge.yml | 12 +- .github/workflows/docker-sync.yml | 21 +- .github/workflows/issue-compliance.yml | 8 +- .github/workflows/issue-validation.yml | 86 +- .github/workflows/lint-js.yml | 8 + .github/workflows/lint-rs.yml | 8 + .github/workflows/pr-checks.yml | 7 +- .github/workflows/publish-repos.yml | 30 +- .github/workflows/release.yml | 13 +- .github/workflows/rolling-release.yml | 9 +- .github/workflows/spellcheck.yml | 8 + .github/workflows/sync-e2e.yml | 8 +- .gitignore | 1 + AGENTS.md | 22 +- e2e/README.md | 55 +- e2e/app/Cargo.lock | 9073 +++++++++++++++++ e2e/app/Cargo.toml | 12 + e2e/app/src/main.rs | 7 + e2e/lib/app.mjs | 107 + e2e/lib/diagnostics.mjs | 99 + e2e/lib/fixtures.mjs | 67 +- e2e/run.mjs | 252 +- e2e/tests/browser.test.mjs | 36 +- e2e/tests/coverage.test.mjs | 57 +- e2e/tests/diagnostics.test.mjs | 116 + e2e/tests/entities.test.mjs | 1 - e2e/tests/network.test.mjs | 605 ++ e2e/tests/smoke.test.mjs | 113 +- e2e/tests/ui.test.mjs | 340 +- package.json | 3 +- scripts/redact-sensitive-text.mjs | 117 + src-tauri/Cargo.lock | 301 +- src-tauri/Cargo.toml | 11 +- src-tauri/Info.plist | 22 +- src-tauri/capabilities/default.json | 31 +- src-tauri/copy-proxy-binary.mjs | 45 +- src-tauri/entitlements.plist | 30 +- src-tauri/src/app_auto_updater.rs | 6 +- src-tauri/src/app_dirs.rs | 37 + src-tauri/src/bin/proxy_server.rs | 49 +- src-tauri/src/browser_runner.rs | 48 +- src-tauri/src/cloud_auth.rs | 29 +- src-tauri/src/cookie_manager.rs | 69 +- src-tauri/src/downloaded_browsers_registry.rs | 4 +- src-tauri/src/downloader.rs | 2 +- src-tauri/src/lib.rs | 45 +- src-tauri/src/log_redaction.rs | 96 + src-tauri/src/profile/clear_on_close.rs | 2 +- src-tauri/src/proxy_manager.rs | 84 +- src-tauri/src/proxy_runner.rs | 73 +- src-tauri/src/proxy_server.rs | 15 +- src-tauri/src/proxy_storage.rs | 49 +- src-tauri/src/settings_manager.rs | 10 +- src-tauri/src/sync/subscription.rs | 2 +- src-tauri/src/synchronizer.rs | 6 +- src-tauri/src/vpn/socks5_server.rs | 2 +- src-tauri/src/vpn/wireguard.rs | 8 +- src-tauri/src/wayfern_manager.rs | 20 +- src-tauri/tauri.conf.json | 19 +- src-tauri/tests/donut_proxy_integration.rs | 17 +- src-tauri/tests/fixtures/test.conf | 7 +- src-tauri/tests/vpn_integration.rs | 14 +- src/components/custom-toast.tsx | 51 - src/components/multiple-selector.tsx | 28 +- src/components/ui/dialog.tsx | 51 +- src/components/ui/popover.tsx | 2 +- src/i18n/locales/en.json | 2 +- src/i18n/locales/es.json | 2 +- src/i18n/locales/fr.json | 2 +- src/i18n/locales/ja.json | 2 +- src/i18n/locales/ko.json | 2 +- src/i18n/locales/pt.json | 2 +- src/i18n/locales/ru.json | 2 +- src/i18n/locales/tr.json | 2 +- src/i18n/locales/vi.json | 2 +- src/i18n/locales/zh.json | 2 +- src/styles/globals.css | 8 +- 81 files changed, 11771 insertions(+), 983 deletions(-) create mode 100644 e2e/app/Cargo.lock create mode 100644 e2e/app/Cargo.toml create mode 100644 e2e/app/src/main.rs create mode 100644 e2e/lib/diagnostics.mjs create mode 100644 e2e/tests/diagnostics.test.mjs create mode 100644 e2e/tests/network.test.mjs create mode 100644 scripts/redact-sensitive-text.mjs create mode 100644 src-tauri/src/log_redaction.rs diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index bb6ce3c..b8cd72c 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -2,6 +2,11 @@ name: Bug Report description: Something isn't working labels: ["bug"] body: + - type: markdown + attributes: + value: | + Do not include passwords, access tokens, proxy credentials, personal information, or other secrets. Automated triage sends the issue title and body to OpenRouter after removing the logs/screenshots field and redacting common sensitive-data patterns. + - type: textarea id: description attributes: @@ -54,7 +59,7 @@ body: id: logs attributes: label: Error logs or screenshots - description: Run from terminal to get logs. Paste errors, screenshots, or screen recordings. + description: Use Settings → Advanced → Copy logs for a redacted log bundle. Review it before posting. Never include credentials or personal information. placeholder: Paste logs here or drag screenshots validations: required: false diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index b7745dc..72a51af 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -2,6 +2,11 @@ name: Feature Request description: Suggest a new feature labels: ["enhancement"] body: + - type: markdown + attributes: + value: | + Do not include passwords, access tokens, personal information, or other secrets. Automated triage sends the issue title and body to OpenRouter after redacting common sensitive-data patterns. + - type: textarea id: description attributes: diff --git a/.github/workflows/app-e2e.yml b/.github/workflows/app-e2e.yml index eeacc6b..41117b6 100644 --- a/.github/workflows/app-e2e.yml +++ b/.github/workflows/app-e2e.yml @@ -8,6 +8,19 @@ on: required: false default: true type: boolean + secrets: + TAURI_WEBDRIVER_TOKEN: + description: Read-only token for the test-driver repository + required: false + WAYFERN_TEST_TOKEN: + description: Token used by the full Wayfern integration suite + required: false + RESIDENTIAL_PROXY_URL_ONE_SOCKS: + description: SOCKS5 residential proxy used by the network suite + required: false + RESIDENTIAL_PROXY_URL_ONE_HTTP: + description: HTTP residential proxy used by the network suite + required: false workflow_dispatch: inputs: suite: @@ -20,6 +33,7 @@ on: - smoke - ui - entities + - network - integrations - sync - browser @@ -51,13 +65,15 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: path: donutbrowser + persist-credentials: false - - name: Checkout private cross-platform WebDriver + - name: Checkout test driver uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: - repository: ${{ vars.TAURI_WEBDRIVER_REPOSITORY || 'zhom/tauri-cross-platform-webdriver' }} + repository: ${{ vars.TAURI_WEBDRIVER_REPOSITORY }} token: ${{ secrets.TAURI_WEBDRIVER_TOKEN || github.token }} path: tauri-cross-platform-webdriver + persist-credentials: false - name: Install pnpm uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 @@ -81,6 +97,7 @@ jobs: with: workspaces: | donutbrowser/src-tauri -> target + donutbrowser/e2e/app -> target tauri-cross-platform-webdriver -> target - name: Install Tauri dependencies @@ -95,8 +112,6 @@ jobs: - name: Run isolated smoke suite working-directory: donutbrowser - env: - DONUT_E2E_KEEP_ARTIFACTS: "1" shell: bash run: | if [[ "${RUNNER_OS}" == "Linux" ]]; then @@ -106,17 +121,17 @@ jobs: fi - name: Upload E2E diagnostics - if: always() - uses: actions/upload-artifact@v7 + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: app-e2e-smoke-${{ matrix.os }} - path: ${{ runner.temp }}/donut-e2e-* + path: ${{ runner.temp }}/donut-e2e-*/diagnostics/** if-no-files-found: ignore retention-days: 7 full: name: Full UI, sync, encryption, and Wayfern - if: ${{ inputs.smoke_only != true }} + if: ${{ inputs.smoke_only != true && inputs.suite != 'network' }} runs-on: macos-latest timeout-minutes: 40 @@ -125,13 +140,15 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: path: donutbrowser + persist-credentials: false - - name: Checkout private cross-platform WebDriver + - name: Checkout test driver uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: - repository: ${{ vars.TAURI_WEBDRIVER_REPOSITORY || 'zhom/tauri-cross-platform-webdriver' }} + repository: ${{ vars.TAURI_WEBDRIVER_REPOSITORY }} token: ${{ secrets.TAURI_WEBDRIVER_TOKEN || github.token }} path: tauri-cross-platform-webdriver + persist-credentials: false - name: Install pnpm uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 @@ -155,6 +172,7 @@ jobs: with: workspaces: | donutbrowser/src-tauri -> target + donutbrowser/e2e/app -> target tauri-cross-platform-webdriver -> target - name: Install app dependencies @@ -164,16 +182,88 @@ jobs: - name: Run full isolated app suite working-directory: donutbrowser env: - DONUT_E2E_KEEP_ARTIFACTS: "1" DONUT_E2E_SUITE: ${{ inputs.suite || 'full' }} + DONUT_E2E_SKIP_NETWORK_TEST: "1" WAYFERN_TEST_TOKEN: ${{ secrets.WAYFERN_TEST_TOKEN }} - run: node e2e/run.mjs --suite=${DONUT_E2E_SUITE} + run: node e2e/run.mjs "--suite=${DONUT_E2E_SUITE}" - name: Upload E2E diagnostics - if: always() - uses: actions/upload-artifact@v7 + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: app-e2e-full-macos - path: ${{ runner.temp }}/donut-e2e-* + path: ${{ runner.temp }}/donut-e2e-*/diagnostics/** + if-no-files-found: ignore + retention-days: 7 + + network: + name: Real proxy and local WireGuard browser traffic + if: ${{ inputs.smoke_only != true && (inputs.suite == '' || inputs.suite == 'full' || inputs.suite == 'network') }} + runs-on: ubuntu-22.04 + timeout-minutes: 30 + + steps: + - name: Checkout Donut Browser + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + path: donutbrowser + persist-credentials: false + + - name: Checkout test driver + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: ${{ vars.TAURI_WEBDRIVER_REPOSITORY }} + token: ${{ secrets.TAURI_WEBDRIVER_TOKEN || github.token }} + path: tauri-cross-platform-webdriver + persist-credentials: false + + - name: Install pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + with: + run_install: false + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: donutbrowser/.node-version + cache: pnpm + cache-dependency-path: donutbrowser/pnpm-lock.yaml + + - name: Install Rust + uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master + with: + toolchain: stable + + - name: Cache Rust dependencies + uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + workspaces: | + donutbrowser/src-tauri -> target + donutbrowser/e2e/app -> target + tauri-cross-platform-webdriver -> target + + - name: Install Tauri dependencies + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libxdo-dev xvfb openvpn + + - name: Install app dependencies + working-directory: donutbrowser + run: pnpm install --frozen-lockfile + + - name: Run isolated real-network suite + working-directory: donutbrowser + env: + WAYFERN_TEST_TOKEN: ${{ secrets.WAYFERN_TEST_TOKEN }} + RESIDENTIAL_PROXY_URL_ONE_SOCKS: ${{ secrets.RESIDENTIAL_PROXY_URL_ONE_SOCKS }} + RESIDENTIAL_PROXY_URL_ONE_HTTP: ${{ secrets.RESIDENTIAL_PROXY_URL_ONE_HTTP }} + run: xvfb-run -a pnpm e2e:network + + - name: Upload E2E diagnostics + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: app-e2e-network-linux + path: ${{ runner.temp }}/donut-e2e-*/diagnostics/** if-no-files-found: ignore retention-days: 7 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7f6c54f..3f362e9 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,6 +2,12 @@ name: "CodeQL" on: workflow_call: + inputs: + checkout_ref: + description: Optional commit to check out instead of the triggering ref + required: false + type: string + default: "" push: branches: ["main"] pull_request: @@ -32,6 +38,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 + with: + ref: ${{ inputs.checkout_ref }} - name: Set up pnpm package manager uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 #v6.0.9 diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index 9af6229..a0fd625 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -30,7 +30,8 @@ jobs: name: Lint JavaScript/TypeScript if: github.repository == 'zhom/donutbrowser' && github.actor == 'dependabot[bot]' uses: ./.github/workflows/lint-js.yml - secrets: inherit + with: + checkout_ref: ${{ github.event.pull_request.head.sha }} permissions: contents: read @@ -38,7 +39,8 @@ jobs: name: Lint Rust if: github.repository == 'zhom/donutbrowser' && github.actor == 'dependabot[bot]' uses: ./.github/workflows/lint-rs.yml - secrets: inherit + with: + checkout_ref: ${{ github.event.pull_request.head.sha }} permissions: contents: read @@ -46,7 +48,8 @@ jobs: name: CodeQL if: github.repository == 'zhom/donutbrowser' && github.actor == 'dependabot[bot]' uses: ./.github/workflows/codeql.yml - secrets: inherit + with: + checkout_ref: ${{ github.event.pull_request.head.sha }} permissions: security-events: write contents: read @@ -57,7 +60,8 @@ jobs: name: Spell Check if: github.repository == 'zhom/donutbrowser' && github.actor == 'dependabot[bot]' uses: ./.github/workflows/spellcheck.yml - secrets: inherit + with: + checkout_ref: ${{ github.event.pull_request.head.sha }} permissions: contents: read diff --git a/.github/workflows/docker-sync.yml b/.github/workflows/docker-sync.yml index 0f561d2..e15b86f 100644 --- a/.github/workflows/docker-sync.yml +++ b/.github/workflows/docker-sync.yml @@ -11,6 +11,11 @@ on: description: "Docker tag (e.g., v1.0.0)" required: true type: string + secrets: + DOCKERHUB_USERNAME: + required: true + DOCKERHUB_TOKEN: + required: true workflow_dispatch: inputs: tag: @@ -43,23 +48,29 @@ jobs: - name: Determine tags id: tags + env: + INPUT_TAG: ${{ inputs.tag }} + EVENT_NAME: ${{ github.event_name }} + COMMIT_SHA: ${{ github.sha }} run: | TAGS="" - INPUT_TAG="${{ inputs.tag }}" if [ -n "$INPUT_TAG" ]; then # Called from release workflow or manual dispatch + if [[ ! "$INPUT_TAG" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]]; then + echo "Invalid Docker tag" >&2 + exit 1 + fi TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${INPUT_TAG}" TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" - elif [ "${{ github.event_name }}" = "push" ]; then + elif [ "$EVENT_NAME" = "push" ]; then # Push to main (nightly): tag with nightly and commit SHA - SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) + SHORT_SHA=${COMMIT_SHA:0:7} TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly" TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly-${SHORT_SHA}" fi - echo "tags=${TAGS}" >> "$GITHUB_OUTPUT" - echo "Tags: ${TAGS}" + printf 'tags=%s\n' "$TAGS" >> "$GITHUB_OUTPUT" - name: Build and push Docker image uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a #v7.3.0 diff --git a/.github/workflows/issue-compliance.yml b/.github/workflows/issue-compliance.yml index ee4636b..24381fa 100644 --- a/.github/workflows/issue-compliance.yml +++ b/.github/workflows/issue-compliance.yml @@ -29,8 +29,8 @@ jobs: ISSUE_TITLE: ${{ github.event.issue.title }} ISSUE_BODY: ${{ github.event.issue.body }} run: | - printf '%s' "$ISSUE_TITLE" > /tmp/issue-title.txt - printf '%s' "${ISSUE_BODY:-}" > /tmp/issue-body.txt + printf '%s' "$ISSUE_TITLE" | node scripts/redact-sensitive-text.mjs > /tmp/issue-title.txt + printf '%s' "${ISSUE_BODY:-}" | node scripts/redact-sensitive-text.mjs --issue-body > /tmp/issue-body.txt - name: Build prompt run: | @@ -92,11 +92,9 @@ jobs: sed -E 's/^```(json)?$//; s/```$//' /tmp/raw.txt > /tmp/result.json if ! jq -e . /tmp/result.json >/dev/null 2>&1; then echo "::warning::Model returned non-JSON; treating as compliant" - cat /tmp/raw.txt echo '{"is_compliant": true, "non_compliance_reasons": []}' > /tmp/result.json fi - echo "Result:" - cat /tmp/result.json + echo "Compliance response validated" - name: Build comment id: build diff --git a/.github/workflows/issue-validation.yml b/.github/workflows/issue-validation.yml index 3315dd1..2a5d3dd 100644 --- a/.github/workflows/issue-validation.yml +++ b/.github/workflows/issue-validation.yml @@ -14,7 +14,6 @@ permissions: contents: read issues: write pull-requests: write - id-token: write env: # Single source of truth for the model used by both triage and composer. @@ -49,8 +48,9 @@ jobs: env: ISSUE_BODY: ${{ github.event.issue.body }} run: | - node <<'EOF' - const fs = require('node:fs'); + node --input-type=module <<'EOF' + import fs from 'node:fs'; + import { redactIssueBody, redactSensitiveText } from './scripts/redact-sensitive-text.mjs'; const body = process.env.ISSUE_BODY || ''; // GitHub issue templates render fields as `### Heading\nValue` blocks. // Split on `###` at line start to recover them. @@ -61,29 +61,27 @@ jobs: if (nl < 0) continue; const heading = section.slice(0, nl).trim(); const value = section.slice(nl + 1).trim(); - fields[heading] = value === '_No response_' ? '' : value; + const normalized = value === '_No response_' ? '' : value; + fields[heading] = heading === 'Error logs or screenshots' + ? '[omitted from automated processing]' + : redactSensitiveText(normalized); } fs.writeFileSync('/tmp/issue-fields.json', JSON.stringify(fields, null, 2)); - // Convenience extractions for the prompt — empty string if missing. const get = (k) => fields[k] || ''; fs.writeFileSync('/tmp/issue-os.txt', get('Operating System')); fs.writeFileSync('/tmp/issue-version.txt', get('Donut Browser version')); fs.writeFileSync('/tmp/issue-wayfern-version.txt', get('Wayfern version')); fs.writeFileSync('/tmp/issue-repro.txt', get('Steps to reproduce')); - fs.writeFileSync('/tmp/issue-logs.txt', get('Error logs or screenshots')); fs.writeFileSync('/tmp/issue-what.txt', get('What happened?') || get('What do you want?')); + fs.writeFileSync('/tmp/issue-body.txt', redactIssueBody(body)); EOF - echo "Parsed fields:" - cat /tmp/issue-fields.json - name: Build repo context env: ISSUE_TITLE: ${{ github.event.issue.title }} - ISSUE_BODY: ${{ github.event.issue.body }} run: | cp CLAUDE.md /tmp/repo-context.txt - printf '%s' "$ISSUE_TITLE" > /tmp/issue-title.txt - printf '%s' "${ISSUE_BODY:-}" > /tmp/issue-body.txt + printf '%s' "$ISSUE_TITLE" | node scripts/redact-sensitive-text.mjs > /tmp/issue-title.txt # List all source files for the AI to choose from find . -type f \( -name "*.rs" -o -name "*.ts" -o -name "*.tsx" \) \ @@ -199,7 +197,7 @@ jobs: Return ONLY valid JSON. No preamble, no code fences. Schema: { "language": "en" or ISO 639-1 code, - "classification": one of ["bug-in-scope", "bug-template-violation", "feature-request", "fork-request", "regression", "ai-generated-junk", "question", "other"], + "classification": one of ["bug-in-scope", "bug-template-violation", "feature-request", "fork-request", "regression", "automated-content", "question", "other"], "operating_system": "macos" | "windows" | "linux" | "unknown", "is_paid_feature": true | false, "user_followed_template": true | false, @@ -211,11 +209,11 @@ jobs: Classification guidance: - "bug-template-violation": missing or filled-in nonsense for required template fields. - - "ai-generated-junk": cites fabricated "official docs" (context7, deepwiki, non-donutbrowser URLs) or has the polished AI-spam shape (long, structured, fabricated certainty). + - "automated-content": cites fabricated "official docs" (context7, deepwiki, non-donutbrowser URLs) or has a highly structured automated-submission pattern with fabricated certainty. - "fork-request": asks for support of CloverLabsAI/VulpineOS/etc. forks. - "regression": user names a prior version that worked. - File selection: pick files that an experienced reviewer would actually look at to act on this issue. If the issue is a fork-request or junk, set files_to_read to []. Otherwise pick concrete files relevant to the symptoms. + File selection: pick files that an experienced reviewer would actually look at to act on this issue. For a fork request or automated-content classification, set files_to_read to []. Otherwise pick concrete files relevant to the symptoms. TRIAGE_TAIL } > /tmp/triage-system.txt wc -c /tmp/triage-system.txt @@ -238,7 +236,7 @@ jobs: messages: [ { role: "system", content: $system_prompt }, { role: "user", - content: ("Issue title: " + $title + "\n\nBody:\n" + $body + "\n\nParsed template fields:\n" + $fields + "\n\nAll source files:\n" + $files) } + content: ("Issue title: " + $title + "\n\nSanitized body:\n" + $body + "\n\nSanitized template fields:\n" + $fields + "\n\nAll source files:\n" + $files) } ] }') @@ -249,14 +247,12 @@ jobs: jq -r '.choices[0].message.content // empty' <<< "$RESPONSE" > /tmp/triage-raw.txt - # Strip ```json fences if the model couldn't help itself. + # Normalize optional markdown fences before parsing. sed -E 's/^```(json)?$//; s/```$//' /tmp/triage-raw.txt > /tmp/triage.json - # Validate; if the model returned junk, fall back to a minimal stub so the - # composer still gets called and produces SOMETHING. + # Fall back to a safe classification when the response is not JSON. if ! jq -e . /tmp/triage.json >/dev/null 2>&1; then echo "::warning::Triage returned non-JSON; using fallback classification" - cat /tmp/triage-raw.txt jq -n '{ language: "en", classification: "bug-in-scope", @@ -270,17 +266,16 @@ jobs: }' > /tmp/triage.json fi - echo "Triage result:" - cat /tmp/triage.json + echo "Triage response validated" - name: Read files chosen by triage run: | : > /tmp/file-context.txt - # files_to_read may be empty (e.g. fork-request or junk) — that's fine. + # An empty file list is valid for classifications that need no source context. jq -r '.files_to_read[]? // empty' /tmp/triage.json | while IFS= read -r filepath; do filepath=$(echo "$filepath" | xargs) [ -z "$filepath" ] && continue - # Reject paths that escape the repo or look fishy + # Reject paths that escape the repository. case "$filepath" in /*|*..*|*$'\n'*) continue ;; esac @@ -331,7 +326,7 @@ jobs: The triage classification (`triage.classification`) determines the response shape: - `bug-in-scope`: ask for what is missing using the user's reported OS log path. Be concrete about how to obtain logs. - - `bug-template-violation` or `ai-generated-junk`: politely ask the user to refile using the bug-report template (the Operating System, Donut Browser version, Wayfern version, Steps to reproduce, Error logs sections). If they cited "documentation" from any non-`donutbrowser.com`/non-`github.com/zhom` URL (e.g. context7, deepwiki), gently note that those are AI-generated third-party summaries and the only authoritative sources are this repo and donutbrowser.com. + - `bug-template-violation` or `automated-content`: politely ask the user to refile using the bug-report template (the Operating System, Donut Browser version, Wayfern version, Steps to reproduce, Error logs sections). If they cited "documentation" from any non-`donutbrowser.com`/non-`github.com/zhom` URL (e.g. context7, deepwiki), gently note that those are AI-generated third-party summaries and the only authoritative sources are this repo and donutbrowser.com. - `feature-request`: one neutral sentence acknowledging, then ask only what is genuinely needed (concrete use case, whether a workaround would suffice). Do NOT validate. - `fork-request`: one neutral sentence acknowledging the request. Note that this would substantially increase support burden and the maintainer evaluates such requests on a case-by-case basis. Ask whether the alternative fork supports all platforms the user uses (macOS / Windows / Linux). No "clear enhancement" language. - `regression`: do NOT call known/expected. Ask which exact previous version was the last working one, what changed in the user's environment between then and now, and the specific delta in symptoms. @@ -408,8 +403,8 @@ jobs: + "Title: " + $title + "\nAuthor: " + $author + "\n\n## Triage result\n" + $triage - + "\n\n## Parsed template fields\n" + $fields - + "\n\n## Raw issue body\n" + $body + + "\n\n## Sanitized template fields\n" + $fields + + "\n\n## Sanitized issue body\n" + $body + "\n\n## Source files (selected by triage)\n" + $files) } ] }') @@ -423,8 +418,6 @@ jobs: if [ ! -s /tmp/ai-comment.txt ]; then echo "::error::Composer returned empty response" - echo "Raw response:" - echo "$RESPONSE" exit 1 fi @@ -484,8 +477,9 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} + HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} run: | - gh api "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" \ + gh api --paginate "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files?per_page=100" \ --jq '.[] | "- \(.filename) (\(.status)) +\(.additions)/-\(.deletions)"' \ > /tmp/pr-files.txt @@ -499,14 +493,27 @@ jobs: cp CLAUDE.md /tmp/repo-context.txt : > /tmp/related-file-contents.txt - gh api "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --jq '.[].filename' | while IFS= read -r filepath; do - if [ -f "$filepath" ] && file --mime "$filepath" | grep -q "text/"; then - echo "=== $filepath (full file) ===" >> /tmp/related-file-contents.txt - cat "$filepath" >> /tmp/related-file-contents.txt - echo "" >> /tmp/related-file-contents.txt + gh api --paginate "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files?per_page=100" \ + --jq '.[] | select(.status != "removed") | [.filename, .sha] | @tsv' | + while IFS=$'\t' read -r filepath blob_sha; do + case "$filepath" in + /*|*..*|*$'\n'*) continue ;; + esac + blob_file=$(mktemp) + if gh api "/repos/$HEAD_REPOSITORY/git/blobs/$blob_sha" --jq .content \ + | tr -d '\n' | base64 --decode > "$blob_file" 2>/dev/null \ + && file --mime "$blob_file" | grep -q "text/"; then + echo "=== $filepath (head revision) ===" >> /tmp/related-file-contents.txt + cat "$blob_file" >> /tmp/related-file-contents.txt + echo "" >> /tmp/related-file-contents.txt fi + rm -f "$blob_file" done head -c 100000 /tmp/related-file-contents.txt > /tmp/pr-file-context.txt + node scripts/redact-sensitive-text.mjs < /tmp/pr-diff.txt > /tmp/pr-diff.safe.txt + mv /tmp/pr-diff.safe.txt /tmp/pr-diff.txt + node scripts/redact-sensitive-text.mjs < /tmp/pr-file-context.txt > /tmp/pr-file-context.safe.txt + mv /tmp/pr-file-context.safe.txt /tmp/pr-file-context.txt - name: Analyze PR with AI env: @@ -525,8 +532,8 @@ jobs: GREETING='This is a first-time contributor. Start your comment with: "Thanks for your first PR!"' fi - printf '%s' "$PR_TITLE" > /tmp/pr-title.txt - printf '%s' "${PR_BODY:-}" > /tmp/pr-body.txt + printf '%s' "$PR_TITLE" | node scripts/redact-sensitive-text.mjs > /tmp/pr-title.txt + printf '%s' "${PR_BODY:-}" | node scripts/redact-sensitive-text.mjs > /tmp/pr-body.txt printf '%s' "$PR_AUTHOR" > /tmp/pr-author.txt printf '%s' "$PR_BASE" > /tmp/pr-base.txt printf '%s' "$PR_HEAD" > /tmp/pr-head.txt @@ -550,7 +557,7 @@ jobs: messages: [ { role: "system", - content: ("You are a code review bot for Donut Browser, an open-source anti-detect browser (Tauri desktop app: Rust backend + Next.js frontend).\n\nProject guidelines and structure:\n" + $repo_context + "\n\nContributing guidelines:\n" + $contributing + "\n\nYou have access to the full changed files and the diff. Use them to give a substantive review.\n\nReview this PR and produce a single comment. Format:\n\n1. One sentence summarizing what this PR does and whether the approach is sound.\n2. **Code review** - Specific observations about the actual code changes. Mention file names and what you see in the diff. Look for:\n - Bugs or logic errors in the changed code\n - Security issues (SQL injection, path traversal, XSS, command injection)\n - Missing error handling or edge cases\n - Breaking changes to existing APIs or behavior\n - If UI text was added/changed, check if all 7 translation files (en, es, fr, ja, pt, ru, zh) in src/i18n/locales/ were updated\n - If Tauri commands were added/removed, the unused-commands test in lib.rs needs updating\n3. **Suggestions** - Concrete improvements if any. Skip if the PR looks good.\n\nRules:\n- Be substantive. Review the actual diff, not just the description.\n- Do NOT nitpick formatting or style — the project has automated linting (biome + clippy + rustfmt).\n- Do NOT just summarize the PR description back to the user — they wrote it, they know what it says.\n- If the PR is good, say so briefly.\n- Never exceed 20 lines.") + content: ("You are a code review bot for Donut Browser, an open-source anti-detect browser (Tauri desktop app: Rust backend + Next.js frontend).\n\nProject guidelines and structure:\n" + $repo_context + "\n\nContributing guidelines:\n" + $contributing + "\n\nYou have access to sanitized head-revision contents for changed files and a sanitized diff. Use them to give a substantive review.\n\nReview this PR and produce a single comment. Format:\n\n1. One sentence summarizing what this PR does and whether the approach is sound.\n2. **Code review** - Specific observations about the actual code changes. Mention file names and what you see in the diff. Look for:\n - Bugs or logic errors in the changed code\n - Security issues (SQL injection, path traversal, XSS, command injection)\n - Missing error handling or edge cases\n - Breaking changes to existing APIs or behavior\n - If UI text was added or changed, verify the key exists in every JSON file under src/i18n/locales/\n - If Tauri commands were added or removed, verify e2e/coverage-map.mjs is updated exactly once per command\n3. **Suggestions** - Concrete improvements if any. Skip if the PR looks good.\n\nRules:\n- Be substantive. Review the actual diff, not just the description.\n- Do NOT nitpick formatting or style — the project has automated linting (biome + clippy + rustfmt).\n- Do NOT just summarize the PR description back to the user — they wrote it, they know what it says.\n- If the PR is good, say so briefly.\n- Never exceed 20 lines.") }, { role: "user", @@ -577,8 +584,6 @@ jobs: if [ ! -s /tmp/ai-comment.txt ]; then echo "::error::AI response was empty" - echo "Raw response:" - echo "$RESPONSE" exit 1 fi @@ -593,6 +598,9 @@ jobs: if: | github.repository == 'zhom/donutbrowser' && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') && + (github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR') && (contains(github.event.comment.body, ' /oc') || startsWith(github.event.comment.body, '/oc') || contains(github.event.comment.body, ' /opencode') || diff --git a/.github/workflows/lint-js.yml b/.github/workflows/lint-js.yml index cfba266..1dd7094 100644 --- a/.github/workflows/lint-js.yml +++ b/.github/workflows/lint-js.yml @@ -4,6 +4,12 @@ name: Lint Node.js on: workflow_call: + inputs: + checkout_ref: + description: Optional commit to check out instead of the triggering ref + required: false + type: string + default: "" push: branches: - main @@ -35,6 +41,8 @@ jobs: - name: Checkout repository code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 + with: + ref: ${{ inputs.checkout_ref }} - name: Set up pnpm package manager uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 #v6.0.9 diff --git a/.github/workflows/lint-rs.yml b/.github/workflows/lint-rs.yml index 68ae841..1d462ff 100644 --- a/.github/workflows/lint-rs.yml +++ b/.github/workflows/lint-rs.yml @@ -4,6 +4,12 @@ name: Lint Rust on: workflow_call: + inputs: + checkout_ref: + description: Optional commit to check out instead of the triggering ref + required: false + type: string + default: "" push: branches: - main @@ -42,6 +48,8 @@ jobs: - name: Checkout repository code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 + with: + ref: ${{ inputs.checkout_ref }} - name: Set up pnpm package manager uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 #v6.0.9 diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 7c8d685..485f6cb 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -15,14 +15,12 @@ jobs: lint-js: name: Lint JavaScript/TypeScript uses: ./.github/workflows/lint-js.yml - secrets: inherit permissions: contents: read lint-rust: name: Lint Rust uses: ./.github/workflows/lint-rs.yml - secrets: inherit permissions: contents: read @@ -41,14 +39,15 @@ jobs: sync-e2e: name: Sync E2E Tests uses: ./.github/workflows/sync-e2e.yml - secrets: inherit permissions: contents: read app-e2e: name: Cross-platform App E2E uses: ./.github/workflows/app-e2e.yml - secrets: inherit + secrets: + TAURI_WEBDRIVER_TOKEN: ${{ secrets.TAURI_WEBDRIVER_TOKEN }} + WAYFERN_TEST_TOKEN: ${{ secrets.WAYFERN_TEST_TOKEN }} permissions: contents: read diff --git a/.github/workflows/publish-repos.yml b/.github/workflows/publish-repos.yml index 3d78201..2b323f7 100644 --- a/.github/workflows/publish-repos.yml +++ b/.github/workflows/publish-repos.yml @@ -31,17 +31,24 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} INPUT_TAG: ${{ inputs.tag }} + EVENT_NAME: ${{ github.event_name }} + WORKFLOW_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + REPOSITORY: ${{ github.repository }} run: | if [[ -n "${INPUT_TAG:-}" ]]; then - echo "tag=${INPUT_TAG}" >> "$GITHUB_OUTPUT" - elif [[ "${{ github.event_name }}" == "workflow_run" ]]; then + TAG="$INPUT_TAG" + elif [[ "$EVENT_NAME" == "workflow_run" ]]; then # The Release workflow is triggered by a tag push (v*), # so head_branch is the tag name - echo "tag=${{ github.event.workflow_run.head_branch }}" >> "$GITHUB_OUTPUT" + TAG="$WORKFLOW_HEAD_BRANCH" else - TAG=$(gh release view --repo "${{ github.repository }}" --json tagName -q .tagName) - echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + TAG=$(gh release view --repo "$REPOSITORY" --json tagName -q .tagName) fi + if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Invalid release tag" >&2 + exit 1 + fi + printf 'tag=%s\n' "$TAG" >> "$GITHUB_OUTPUT" - name: Install tools run: | @@ -59,19 +66,12 @@ jobs: R2_ENDPOINT_URL: ${{ secrets.R2_ENDPOINT_URL }} R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_TAG: ${{ steps.tag.outputs.tag }} run: | - # GitHub injects secrets verbatim. If a value was pasted with - # surrounding quotes or a trailing newline — the local .env wraps all - # four R2_* values in double quotes — it reaches the script malformed: - # e.g. an endpoint of https://"host" yields - # `Could not connect to the endpoint URL`, and a quoted key yields - # `Unauthorized`. The local run is unaffected because publish-repo.sh - # sources .env through bash, which strips the quotes; CI has no .env, - # so strip here. No-op when the secrets are already clean. The script - # itself is intentionally left untouched. + # Normalize accidental quotes and whitespace in configured secrets. strip() { printf '%s' "$1" | tr -d '\r\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/^"\(.*\)"$/\1/' -e "s/^'\(.*\)'\$/\1/"; } export R2_ACCESS_KEY_ID="$(strip "$R2_ACCESS_KEY_ID")" export R2_SECRET_ACCESS_KEY="$(strip "$R2_SECRET_ACCESS_KEY")" export R2_ENDPOINT_URL="$(strip "$R2_ENDPOINT_URL")" export R2_BUCKET_NAME="$(strip "$R2_BUCKET_NAME")" - bash scripts/publish-repo.sh "${{ steps.tag.outputs.tag }}" + bash scripts/publish-repo.sh "$RELEASE_TAG" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 38df28c..dc3b6c2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,6 @@ jobs: if: github.repository == 'zhom/donutbrowser' name: Lint JavaScript/TypeScript uses: ./.github/workflows/lint-js.yml - secrets: inherit permissions: contents: read @@ -45,7 +44,6 @@ jobs: if: github.repository == 'zhom/donutbrowser' name: Lint Rust uses: ./.github/workflows/lint-rs.yml - secrets: inherit permissions: contents: read @@ -53,7 +51,6 @@ jobs: if: github.repository == 'zhom/donutbrowser' name: CodeQL uses: ./.github/workflows/codeql.yml - secrets: inherit permissions: security-events: write contents: read @@ -64,7 +61,6 @@ jobs: if: github.repository == 'zhom/donutbrowser' name: Spell Check uses: ./.github/workflows/spellcheck.yml - secrets: inherit permissions: contents: read @@ -218,6 +214,7 @@ jobs: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + TARGET: ${{ matrix.target }} # tauri-action invokes `pnpm tauri build`, which runs # `beforeBuildCommand` from tauri.conf.json. That rebuilds the # frontend in its own subprocess, so the env var MUST be forwarded @@ -559,7 +556,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Trigger Cloudflare Pages deployment - run: curl -fsSL -X POST "${{ secrets.CLOUDFLARE_WEB_DEPLOYMENT_HOOK }}" + env: + DEPLOYMENT_HOOK: ${{ secrets.CLOUDFLARE_WEB_DEPLOYMENT_HOOK }} + run: curl -fsSL -X POST "$DEPLOYMENT_HOOK" docker: if: github.repository == 'zhom/donutbrowser' @@ -567,7 +566,9 @@ jobs: uses: ./.github/workflows/docker-sync.yml with: tag: ${{ github.ref_name }} - secrets: inherit + secrets: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} update-flake: if: github.repository == 'zhom/donutbrowser' diff --git a/.github/workflows/rolling-release.yml b/.github/workflows/rolling-release.yml index 01e1bc9..dbf72ec 100644 --- a/.github/workflows/rolling-release.yml +++ b/.github/workflows/rolling-release.yml @@ -44,7 +44,6 @@ jobs: if: github.repository == 'zhom/donutbrowser' name: Lint JavaScript/TypeScript uses: ./.github/workflows/lint-js.yml - secrets: inherit permissions: contents: read @@ -52,7 +51,6 @@ jobs: if: github.repository == 'zhom/donutbrowser' name: Lint Rust uses: ./.github/workflows/lint-rs.yml - secrets: inherit permissions: contents: read @@ -60,7 +58,6 @@ jobs: if: github.repository == 'zhom/donutbrowser' name: CodeQL uses: ./.github/workflows/codeql.yml - secrets: inherit permissions: security-events: write contents: read @@ -71,7 +68,6 @@ jobs: if: github.repository == 'zhom/donutbrowser' name: Spell Check uses: ./.github/workflows/spellcheck.yml - secrets: inherit permissions: contents: read @@ -241,6 +237,7 @@ jobs: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + TARGET: ${{ matrix.target }} # tauri-action's inner `pnpm tauri build` re-runs beforeBuildCommand # which rebuilds dist/ in a subprocess. The env var must be here too. NEXT_PUBLIC_TURNSTILE: ${{ secrets.NEXT_PUBLIC_TURNSTILE }} @@ -419,7 +416,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Trigger Cloudflare Pages deployment - run: curl -fsSL -X POST "${{ secrets.CLOUDFLARE_WEB_DEPLOYMENT_HOOK }}" + env: + DEPLOYMENT_HOOK: ${{ secrets.CLOUDFLARE_WEB_DEPLOYMENT_HOOK }} + run: curl -fsSL -X POST "$DEPLOYMENT_HOOK" notify-discord: if: github.repository == 'zhom/donutbrowser' diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index a8c3a72..17bf9ca 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -5,6 +5,12 @@ permissions: on: workflow_call: + inputs: + checkout_ref: + description: Optional commit to check out instead of the triggering ref + required: false + type: string + default: "" push: branches: ["main"] pull_request: @@ -22,5 +28,7 @@ jobs: steps: - name: Checkout Actions Repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 + with: + ref: ${{ inputs.checkout_ref }} - name: Spell Check Repo uses: crate-ci/typos@bee27e3a4fd1ea2111cf90ab89cd076c870fce14 #v1.48.0 diff --git a/.github/workflows/sync-e2e.yml b/.github/workflows/sync-e2e.yml index 542259e..9d89123 100644 --- a/.github/workflows/sync-e2e.yml +++ b/.github/workflows/sync-e2e.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7.0.0 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install pnpm uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 #v6.0.9 @@ -40,7 +40,7 @@ jobs: run_install: false - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: "22" cache: "pnpm" @@ -73,7 +73,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7.0.0 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Start MinIO run: | @@ -99,7 +99,7 @@ jobs: run_install: false - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: "22" cache: "pnpm" diff --git a/.gitignore b/.gitignore index 84489c1..e0f0a6e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ # testing /coverage +/e2e/app/target/ # next.js /.next/ diff --git a/AGENTS.md b/AGENTS.md index 970f0e8..b838b3f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # ⛔ ABSOLUTE GIT RULE — READ FIRST (2026-06-11) -**NEVER run any git command that modifies git history OR the working tree, in ANY repo** (wayfern, wayfern-macos, wayfern-test, donutbrowser, build/src), **unless the user EXPLICITLY authorizes that exact command.** Forbidden without per-command authorization: `commit`, `revert`, `cherry-pick`, `restore`, `checkout` (files/branches), `reset`, `rebase`, `merge`, `stash`, `clean`, `apply`, `add`, `rm`, `push`, any force op. Only read-only git (`status`, `log`, `show`, `diff`, `ls-files`, `rev-parse`) is allowed without asking. **Authorization is per-command: 1 explicit authorization = exactly 1 command.** If a git mutation seems needed, STOP and ask for that one command. +**NEVER run any git command that modifies git history OR the working tree, in ANY repo**, **unless the user EXPLICITLY authorizes that exact command.** Forbidden without per-command authorization: `commit`, `revert`, `cherry-pick`, `restore`, `checkout` (files/branches), `reset`, `rebase`, `merge`, `stash`, `clean`, `apply`, `add`, `rm`, `push`, any force op. Only read-only git (`status`, `log`, `show`, `diff`, `ls-files`, `rev-parse`) is allowed without asking. **Authorization is per-command: 1 explicit authorization = exactly 1 command.** If a git mutation seems needed, STOP and ask for that one command. --- @@ -52,6 +52,7 @@ donutbrowser/ ├── donut-sync/ # NestJS sync server (self-hostable) │ └── src/ # Controllers, services, auth, S3 sync ├── e2e/ # Isolated native UI/sync/Wayfern E2E system +│ ├── app/ # Test-only Tauri harness that injects the private driver │ ├── lib/ # WebDriver, CDP, fixtures, app-session helpers │ └── tests/ # Smoke, UI, entity, integration, sync, browser suites ├── flake.nix # Nix development environment @@ -68,8 +69,8 @@ donutbrowser/ ### Native app E2E tests are mandatory for affected behavior -The native suites use the sibling `../tauri-cross-platform-webdriver/` repository and launch an -`e2e`-feature build. Every session gets its own temporary Donut data/cache/log root, home directory, +The native suites use a sibling private test-driver repository and launch an `e2e`-feature build. +Every session gets its own temporary Donut data/cache/log root, home directory, WebView store, ports, and sync bucket. Never point a suite at production or development data. After a behavior change, run the smallest affected subset below in addition to the standard @@ -79,17 +80,20 @@ suite passes: | Changed area | Required command | | --- | --- | | Startup, settings, persistence, window state, shortcuts, navigation | `pnpm e2e:smoke` | -| React components, dialogs, responsive layout, accessibility, onboarding | `pnpm e2e:ui` | -| Profiles, imports, groups, proxies, VPNs, extensions, DNS, cookies, passwords, traffic | `pnpm e2e:entities` | +| React components, dialogs, themes/appearance, responsive layout, accessibility, onboarding | `pnpm e2e:ui` | +| Profile/import/group/proxy/VPN/extension CRUD, DNS, cookies, passwords, traffic | `pnpm e2e:entities` | +| Profile/group/proxy/VPN/extension UI, proxy routing, VPN routing, or their browser-launch integration | `pnpm e2e:network` | | REST API/OpenAPI, MCP, cloud/update contracts, team locks, real-time synchronizer | `pnpm e2e:integrations` | | Sync client/server, manifests, timestamps, deletion, encryption, password rollover | `pnpm e2e:sync` | | Wayfern download/terms/fingerprint, browser runner, CDP, automation endpoints, process cleanup | `pnpm e2e:browser` | | E2E harness, WebDriver plugin/driver, app isolation hooks, or changes spanning multiple rows | Run every affected row; use `pnpm e2e` for cross-cutting changes | -`e2e:browser` and the full suite require `WAYFERN_TEST_TOKEN` in the environment or the local -`.env`; all other suites must run without credentials. Use `--no-build` only when the frontend, -Rust app, sidecar, and WebDriver binaries are already current. Keep failed artifacts and inspect the -per-session app/driver logs and screenshot before changing assertions. +`e2e:browser` requires `WAYFERN_TEST_TOKEN` in the environment or local `.env`. `e2e:network` +and the full suite additionally require Docker plus `RESIDENTIAL_PROXY_URL_ONE_HTTP` and +`RESIDENTIAL_PROXY_URL_ONE_SOCKS`. Other individual suites must run without credentials. Use +`--no-build` only when the frontend, Rust app, sidecar, and WebDriver binaries are already current. +Keep failed artifacts and inspect the per-session app/driver logs and screenshot before changing +assertions. When adding a Tauri command, assign it exactly once in `e2e/coverage-map.mjs` and add executable evidence to the owning suite. `e2e:smoke` fails if command registration and the coverage map drift. diff --git a/e2e/README.md b/e2e/README.md index 1b45734..d2e5eb9 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -1,7 +1,7 @@ # Donut Browser native E2E tests -These tests exercise the actual Tauri application through the private -`../tauri-cross-platform-webdriver/` driver. They do not replace Rust or React unit tests; they +These tests exercise the actual Tauri application through a sibling native test driver. They do +not replace Rust or React unit tests; they cover the process boundaries those tests cannot: WKWebView/WebView2/WebKitGTK UI, Tauri invokes, REST and MCP servers, two-device sync, S3 payload encryption, Wayfern, CDP, and child-process cleanup. @@ -13,14 +13,22 @@ Place both repositories beside each other: ```text Code/ ├── donutbrowser/ -└── tauri-cross-platform-webdriver/ +└── / ``` Install Donut dependencies with `pnpm install`. The browser suite also needs -`WAYFERN_TEST_TOKEN`. The runner reads it from the environment, Donut's `.env`, or -`../wayfern-test/.env` without printing it. If `../wayfern-test/test_extracted_app` contains a -Wayfern build, the runner links/copies it into the test data root; otherwise the browser suite -downloads the current published build into that root. +`WAYFERN_TEST_TOKEN`. The runner reads it from the environment or Donut's ignored `.env` without +printing it. When a local browser fixture is configured, the runner copies it into the test data +root (using an isolated APFS clone on macOS); otherwise the browser suite downloads the current +published build into that root. + +Set `DONUT_E2E_WAYFERN_PATH` to use a local browser fixture. Without it, the runner uses an ignored +cache fixture when present and otherwise downloads the published test build. + +The real-network suite additionally requires Docker plus +`RESIDENTIAL_PROXY_URL_ONE_HTTP` and `RESIDENTIAL_PROXY_URL_ONE_SOCKS`. It creates its own +WireGuard server and tunnel-only HTTP target in a disposable container. It never connects a test +profile to a developer or production VPN. Run one suite: @@ -28,15 +36,31 @@ Run one suite: pnpm e2e:smoke pnpm e2e:ui pnpm e2e:entities +pnpm e2e:network pnpm e2e:integrations pnpm e2e:sync pnpm e2e:browser ``` Run everything with `pnpm e2e`. A normal run builds the Next frontend, `donut-proxy`, the -`e2e`-feature app, and `tauri-wd`. Add `--no-build` to `node e2e/run.mjs --suite=` only when -all four outputs are current. `DONUT_E2E_KEEP_ARTIFACTS=1` retains successful runs; failed runs are -always retained and their location is printed. +private harness in `e2e/app`, and `tauri-wd`. The harness enables Donut's `e2e` feature and injects +the sibling WebDriver plugin without making the production crate depend on a private filesystem +path. Add `--no-build` to `node e2e/run.mjs --suite=` only when all four outputs are current. +`DONUT_E2E_KEEP_ARTIFACTS=1` retains successful local runs; failed runs are always retained and +their location is printed. Raw screenshots, captured HTML, logs, and isolated app state stay local. +The runner also creates a text-only `diagnostics/` directory whose logs are redacted and checked +against active test secrets. CI uploads only that directory on failure. Disposable copied browser +binaries are pruned so repeated failures do not consume gigabytes. + +The suites deliberately distinguish visible behavior from command coverage. `e2e:entities` +exercises isolated CRUD and persistence through Tauri commands. `e2e:network` visibly creates a +profile group, HTTP proxy, WireGuard VPN, extension, extension group, and Wayfern profile; assigns +the proxy and VPN in the profile table; validates both residential HTTP and SOCKS5 proxies; then +launches Wayfern through the residential proxy and through the local WireGuard tunnel. Normal test +sessions start with onboarding completed so the Welcome dialog cannot hide the feature under test. +The onboarding and Wayfern-terms scenarios explicitly opt into fresh state and test those dialogs. +`e2e:ui` selects predefined, preset, and manually customized themes through the native UI and +asserts their persisted settings and rendered CSS variables across rail navigation and app restart. ## Isolation contract @@ -60,9 +84,10 @@ the WebDriver plugin or this fallback. `.github/workflows/app-e2e.yml` runs smoke tests on macOS, Linux/Xvfb, and Windows for pull requests. Pushes to `main`, weekly schedules, and manual runs execute the full macOS suite, -including MinIO-backed sync and real Wayfern automation. +including MinIO-backed sync and real Wayfern automation, plus a Linux/Docker job for residential +proxy and local WireGuard browser traffic. -Because the WebDriver repository is private, CI needs a `TAURI_WEBDRIVER_TOKEN` secret with -read-only access. `TAURI_WEBDRIVER_REPOSITORY` may override its default -`zhom/tauri-cross-platform-webdriver` repository name. The full job also requires the -`WAYFERN_TEST_TOKEN` secret. +CI needs a `TAURI_WEBDRIVER_TOKEN` secret with read-only access and a +`TAURI_WEBDRIVER_REPOSITORY` repository variable identifying the test-driver checkout. The full +job also requires the `WAYFERN_TEST_TOKEN` secret. The network job requires that secret plus +`RESIDENTIAL_PROXY_URL_ONE_HTTP` and `RESIDENTIAL_PROXY_URL_ONE_SOCKS`. diff --git a/e2e/app/Cargo.lock b/e2e/app/Cargo.lock new file mode 100644 index 0000000..fca1cff --- /dev/null +++ b/e2e/app/Cargo.lock @@ -0,0 +1,9073 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aead" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" +dependencies = [ + "crypto-common 0.2.2", + "inout 0.2.2", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" +dependencies = [ + "cipher 0.5.2", + "cpubits", + "cpufeatures 0.3.0", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead 0.5.2", + "aes 0.8.4", + "cipher 0.4.4", + "ctr 0.9.2", + "ghash 0.5.1", + "subtle", +] + +[[package]] +name = "aes-gcm" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028" +dependencies = [ + "aead 0.6.1", + "aes 0.9.1", + "cipher 0.5.2", + "ctr 0.10.1", + "ghash 0.6.0", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" +dependencies = [ + "android_log-sys", + "env_filter 0.1.4", + "log", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "wl-clipboard-rs", + "x11rb", +] + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.2.17", + "password-hash", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-socks5" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da2537846e16b96d2972ee52a3b355663872a1a687ce6d57a3b6f6b6a181c89" +dependencies = [ + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib 0.18.5", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "base64 0.22.1", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "boringtun" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15dd6a8a89cbe8997f37ca0cf035e6ea4d64cd2ecea4aed83ffb9f99f7126939" +dependencies = [ + "aead 0.5.2", + "base64 0.22.1", + "blake2", + "chacha20poly1305", + "hex", + "hmac", + "ip_network", + "ip_network_table", + "libc", + "nix", + "parking_lot", + "portable-atomic", + "rand_core 0.6.4", + "ring", + "tracing", + "untrusted", + "x25519-dalek", +] + +[[package]] +name = "borsh" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8fb4fb5740e4b2c4884ff95f5f32f5e8479db1e8fd8eb49ddbe09eb09bb7c" +dependencies = [ + "once_cell", + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bstr" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" +dependencies = [ + "memchr", + "serde_core", +] + +[[package]] +name = "built" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byte-unit" +version = "5.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a813de7f2bbedb7dce265b64f1cf5908ebe4d56281ece8d847e98113788b9b0" +dependencies = [ + "rust_decimal", + "schemars 1.2.1", + "serde", + "utf8-width", +] + +[[package]] +name = "byte_string" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11aade7a05aa8c3a351cedc44c3fc45806430543382fcc4743a9b757a2a0b4ed" + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +dependencies = [ + "serde", +] + +[[package]] +name = "bzip2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" +dependencies = [ + "libbz2-rs-sys", +] + +[[package]] +name = "cab" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171228650e6721d5acc0868a462cd864f49ac5f64e4a42cde270406e64e404d2" +dependencies = [ + "byteorder", + "flate2", + "lzxd", + "time", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.0", + "cairo-sys-rs", + "glib 0.18.5", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys 0.18.1", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "camino" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cbc" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" +dependencies = [ + "cipher 0.5.2", +] + +[[package]] +name = "cc" +version = "1.2.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +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 = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfb" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a4f8e55be323b378facfcf1f06aa97f6ec17cf4ac84fb17325093aaf62da41" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon 0.12.16", +] + +[[package]] +name = "cfg-expr" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c" +dependencies = [ + "smallvec", + "target-lexicon 0.13.5", +] + +[[package]] +name = "cfg-if" +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 = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead 0.5.2", + "chacha20 0.9.1", + "cipher 0.4.4", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "chrono-tz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" +dependencies = [ + "chrono", + "phf 0.12.1", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout 0.1.4", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "inout 0.2.2", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "getrandom 0.4.3", + "hybrid-array", + "rand_core 0.10.1", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "ctr" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" +dependencies = [ + "cipher 0.5.2", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "defmt" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" +dependencies = [ + "defmt 1.1.1", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.118", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", +] + +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash 0.2.0", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "donutbrowser" +version = "0.28.2" +dependencies = [ + "aes 0.9.1", + "aes-gcm 0.11.0", + "argon2", + "async-socks5", + "async-trait", + "axum", + "base64 0.22.1", + "blake3", + "boringtun", + "bzip2", + "cbc", + "chrono", + "chrono-tz", + "clap", + "core-foundation 0.10.1", + "crossbeam-channel", + "directories", + "dirs", + "env_logger", + "flate2", + "futures-util", + "globset", + "http-body-util", + "hyper", + "hyper-util", + "image", + "lazy_static", + "libc", + "log", + "lzma-rs", + "maxminddb", + "mime_guess", + "msi-extract", + "nix", + "objc2", + "objc2-app-kit", + "once_cell", + "quick-xml", + "rand 0.10.2", + "regex-lite", + "reqwest", + "resvg", + "ring", + "rusqlite", + "serde", + "serde_json", + "serde_yaml", + "sha2 0.11.0", + "shadowsocks", + "smoltcp", + "subtle", + "sys-locale", + "sysinfo", + "tar", + "tauri", + "tauri-build", + "tauri-plugin-clipboard-manager", + "tauri-plugin-deep-link", + "tauri-plugin-dialog", + "tauri-plugin-fs", + "tauri-plugin-log", + "tauri-plugin-macos-permissions", + "tauri-plugin-opener", + "tauri-plugin-shell", + "tauri-plugin-single-instance", + "tauri-plugin-window-state", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-tungstenite", + "tokio-util", + "toml 1.1.2+spec-1.1.0", + "tower", + "tower-http 0.7.0", + "url", + "urlencoding", + "utoipa", + "utoipa-axum", + "uuid", + "windows 0.62.2", + "winreg 0.56.0", + "zip", +] + +[[package]] +name = "donutbrowser-e2e" +version = "0.1.0" +dependencies = [ + "donutbrowser", + "tauri-plugin-cross-platform-webdriver", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "dynosaur" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb33e6854ea615b65faf9c40d4ea9c4de9e9fa5d6772ad6ce57950a6da3957d" +dependencies = [ + "dynosaur_derive", +] + +[[package]] +name = "dynosaur_derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac0893f103ae7bf50181323d748d505f27b6255777c28b502030bdaf8749f4c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg 0.55.0", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "anstream", + "anstyle", + "env_filter 2.0.0", + "jiff", + "log", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fern" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" +dependencies = [ + "log", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree 0.20.0", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib 0.18.5", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib 0.18.5", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "pango-sys", + "pkg-config", + "system-deps 6.2.2", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "pkg-config", + "system-deps 6.2.2", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib 0.18.5", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys 0.18.1", + "libc", + "system-deps 6.2.2", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link 0.2.1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval 0.6.2", +] + +[[package]] +name = "ghash" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" +dependencies = [ + "polyval 0.7.1", +] + +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys 0.18.1", + "glib 0.18.5", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps 6.2.2", + "winapi", +] + +[[package]] +name = "gio-sys" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0071fe88dba8e40086c8ff9bbb62622999f49628344b1d1bf490a48a29d80f22" +dependencies = [ + "glib-sys 0.21.5", + "gobject-sys 0.21.5", + "libc", + "system-deps 7.0.8", + "windows-sys 0.61.2", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys 0.18.1", + "glib-macros 0.18.5", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b" +dependencies = [ + "bitflags 2.13.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys 0.21.5", + "glib-macros 0.21.5", + "glib-sys 0.21.5", + "gobject-sys 0.21.5", + "libc", + "memchr", + "smallvec", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "glib-macros" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf59b675301228a696fe01c3073974643365080a76cc3ed5bc2cbc466ad87f17" +dependencies = [ + "heck 0.5.0", + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "glib-sys" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d95e1a3a19ae464a7286e14af9a90683c64d70c02532d88d87ce95056af3e6c" +dependencies = [ + "libc", + "system-deps 7.0.8", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys 0.18.1", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "gobject-sys" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dca35da0d19a18f4575f3cb99fe1c9e029a2941af5662f326f738a21edaf294" +dependencies = [ + "glib-sys 0.21.5", + "libc", + "system-deps 7.0.8", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib 0.18.5", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "pango-sys", + "system-deps 6.2.2", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "hashlink" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "heapless" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-layer", + "tower-service", + "tracing", + "windows-registry 0.6.1", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[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 = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imagesize" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" + +[[package]] +name = "imgref" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb 0.7.3", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "block-padding", + "hybrid-array", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ip_network" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1" + +[[package]] +name = "ip_network_table" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4099b7cfc5c5e2fe8c5edf3f6f7adf7a714c9cc697534f63a5a5da30397cb2c0" +dependencies = [ + "ip_network", + "ip_network_table-deps-treebitmap", +] + +[[package]] +name = "ip_network_table-deps-treebitmap" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "ipnetwork" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib 0.18.5", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "jiff" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccfe6121cbe750cf81efa362d85c0bde7ea298ec43092d3a193baca59cdbd634" +dependencies = [ + "defmt 1.1.1", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e165e897f662d428f3cd3828a919dbe067c2d42bb1031eede74ef9d27ecdedd2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "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 = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kurbo" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" +dependencies = [ + "arrayvec", + "euclid", + "polycool", + "smallvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib 0.18.5", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libbz2-rs-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6c19a05435c21ac299d71b6a9c13db3e3f47c520517d58990a462a1397a61db" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +dependencies = [ + "value-bag", +] + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzxd" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17f346186eccb574ba5581acefc514f0c70a642db4f96e245034a0a158a7168" + +[[package]] +name = "macos-accessibility-client" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edf7710fbff50c24124331760978fb9086d6de6288dcdb38b25a97f8b1bdebbb" +dependencies = [ + "core-foundation 0.9.4", + "core-foundation-sys", +] + +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "maxminddb" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e84ef32bcbf18a95548989e880db4af6fafd563463753afb4b9a149fb2782c" +dependencies = [ + "ipnetwork", + "log", + "memchr", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[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]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "msi" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a2332f87a064dea9cce571408c879e0da8dc193b3af06a2b3b2604ee4182a32" +dependencies = [ + "byteorder", + "cfb 0.10.0", + "encoding_rs", + "uuid", +] + +[[package]] +name = "msi-extract" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32047fe35aac08636833dcae558307dafc7679726b97f56449c3c126ed4be108" +dependencies = [ + "cab", + "cfb 0.10.0", + "msi", + "thiserror 2.0.18", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.0", + "block2", + "libc", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.0", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-javascript-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" +dependencies = [ + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-open-directory" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d" +dependencies = [ + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-javascript-core", + "objc2-security", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "open" +version = "5.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd8d3b65c44123a56e0133d2cd06ce4361bd3ca99d41198b2f25e3c3db9b8b4a" +dependencies = [ + "dunce", + "is-wsl", + "libc", +] + +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib 0.18.5", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap 2.14.0", +] + +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_shared 0.12.1", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash 0.5.1", +] + +[[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash 0.5.1", +] + +[[package]] +name = "polyval" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfc63250416fea14f5749b90725916a6c903f599d51cb635aa7a52bfd03eede" +dependencies = [ + "cpubits", + "cpufeatures 0.3.0", + "universal-hash 0.6.1", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[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 = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +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 = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20 0.10.1", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[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 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[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 = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.4", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http 0.6.11", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "resvg" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be183ad6a216aa96f33e4c8033b0988b8b3ea6fd2359d19af5bac4643fd8e81" +dependencies = [ + "gif", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", + "zune-jpeg", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ring-compat" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccce7bae150b815f0811db41b8312fcb74bffa4cab9cee5429ee00f356dd5bd4" +dependencies = [ + "aead 0.5.2", + "ed25519", + "generic-array", + "pkcs8", + "ring", +] + +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "roxmltree" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] + +[[package]] +name = "rsqlite-vfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.18", +] + +[[package]] +name = "rusqlite" +version = "0.40.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11438310b19e3109b6446c33d1ed5e889428cf2e278407bc7896bc4aaea43323" +dependencies = [ + "bitflags 2.13.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rust_decimal" +version = "1.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be2a24f50780bc85f09cc6ac299bdf1424302742d77221106859c9d8b102126a" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.6", + "rkyv", + "serde", + "serde_json", + "wasm-bindgen", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.13.0", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.118", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "sealed" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.0", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "sendfd" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b183bfd5b1bc64ab0c1ef3ee06b008a9ef1b68a7d3a99ba566fbfe7a7c6d745b" +dependencies = [ + "libc", + "tokio", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[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.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "shadowsocks" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "482831bf9d55acf3c98e211b6c852c3dfdf1d1b0d23fdf1d887c5a4b2acad4e4" +dependencies = [ + "base64 0.22.1", + "blake3", + "byte_string", + "bytes", + "cfg-if", + "dynosaur", + "futures", + "libc", + "log", + "percent-encoding", + "pin-project", + "sealed", + "sendfd", + "serde", + "serde_json", + "serde_urlencoded", + "shadowsocks-crypto", + "socket2", + "spin", + "thiserror 2.0.18", + "tokio", + "tokio-tfo", + "trait-variant", + "url", + "windows-sys 0.61.2", +] + +[[package]] +name = "shadowsocks-crypto" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d038a3d17586f1c1ab3c1c3b9e4d5ef8fba98fb3890ad740c8487038b2e2ca5" +dependencies = [ + "aes-gcm 0.10.3", + "cfg-if", + "chacha20poly1305", + "hkdf", + "md-5", + "rand 0.9.4", + "ring-compat", + "sha1", +] + +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "smoltcp" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f73d40463bba65efc9adc6370b56df76d563cc46e2482bba58351b4afb7535e" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "cfg-if", + "defmt 0.3.100", + "heapless", + "managed", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib 0.18.5", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "svgtypes" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d" +dependencies = [ + "kurbo", + "siphasher", +] + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +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 = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "sysinfo" +version = "0.39.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8bd2130a9b60bee2581bf82cfe89ee836424d1f37dcfa4ce21509611684673" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "objc2-open-directory", + "windows 0.62.2", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr 0.15.8", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "system-deps" +version = "7.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" +dependencies = [ + "cfg-expr 0.20.8", + "heck 0.5.0", + "pkg-config", + "toml 1.1.2+spec-1.1.0", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.0", + "block2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tauri" +version = "2.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "image", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows 0.61.3", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2 0.10.9", + "syn 2.0.118", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-clipboard-manager" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206dc20af4ed210748ba945c2774e60fd0acd52b9a73a028402caf809e9b6ecf" +dependencies = [ + "arboard", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + +[[package]] +name = "tauri-plugin-cross-platform-webdriver" +version = "0.1.0" +dependencies = [ + "async-trait", + "axum", + "base64 0.22.1", + "block2", + "cairo-rs", + "glib 0.21.5", + "gtk", + "javascriptcore-rs", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-web-kit", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tempfile", + "tokio", + "tracing", + "uuid", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", + "windows-core 0.61.2", +] + +[[package]] +name = "tauri-plugin-deep-link" +version = "2.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ee75bc5627f77bfdf40c913255ebc258117b10ebe2b2239a1a1cf40b0b58aa" +dependencies = [ + "dunce", + "plist", + "rust-ini", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "tracing", + "url", + "windows-registry 0.5.3", + "windows-result 0.3.4", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-log" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7545bd67f070a4500432c826e2e0682146a1d6712aee22a2786490156b574d93" +dependencies = [ + "android_logger", + "byte-unit", + "fern", + "log", + "objc2", + "objc2-foundation", + "serde", + "serde_json", + "serde_repr", + "swift-rs", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "tauri-plugin-macos-permissions" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5607e0707d37d7b20e287cf0ce396d1efebe7b833b8e9cbd2ea4257091d9c604" +dependencies = [ + "macos-accessibility-client", + "objc2", + "objc2-foundation", + "serde", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "url", + "windows 0.61.3", + "zbus", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.18", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + +[[package]] +name = "tauri-plugin-window-state" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704" +dependencies = [ + "bitflags 2.13.0", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf 0.13.1", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "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 2.0.118", +] + +[[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 2.0.118", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tiny-skia" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png 0.18.1", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[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.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-tfo" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6ad2c3b3bb958ad992354a7ebc468fc0f7cdc9af4997bf4d3fd3cb28bad36dc" +dependencies = [ + "cfg-if", + "futures", + "libc", + "log", + "once_cell", + "pin-project", + "socket2", + "tokio", + "windows-sys 0.60.2", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +dependencies = [ + "futures-util", + "log", + "native-tls", + "tokio", + "tokio-native-tls", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.0", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-http" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233" +dependencies = [ + "bitflags 2.13.0", + "bytes", + "http", + "percent-encoding", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "trait-variant" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tray-icon" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "tree_magic_mini" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" +dependencies = [ + "memchr", + "nom", + "petgraph", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.9.4", + "sha1", + "thiserror 2.0.18", +] + +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "usvg" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d46cf96c5f498d36b7a9693bc6a7075c0bb9303189d61b2249b0dc3d309c07de" +dependencies = [ + "base64 0.22.1", + "data-url", + "flate2", + "fontdb", + "imagesize", + "kurbo", + "log", + "pico-args", + "roxmltree 0.21.1", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "ttf-parser", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "utf8-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "159a7cadce548703edd50d24069bc294c5415ecab0a480e0cd1ca06d112dc94a" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "utoipa" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bde15df68e80b16c7d16b9616e80770ad158988daa56a27dccd1e55558b0160" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-axum" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c25bae5bccc842449ec0c5ddc5cbb6a3a1eaeac4503895dc105a1138f8234a0" +dependencies = [ + "axum", + "paste", + "tower-layer", + "tower-service", + "utoipa", +] + +[[package]] +name = "utoipa-gen" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba0b99ee52df3028635d93840c797102da61f8a7bb3cf751032455895b52ef8" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.118", +] + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.13.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "git+https://github.com/Smithay/wayland-rs?rev=d07c4f91f28b42e5a485823ffd9d8d5a210b1053#d07c4f91f28b42e5a485823ffd9d8d5a210b1053" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +dependencies = [ + "phf 0.13.1", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys 0.18.1", + "glib 0.18.5", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps 6.2.2", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows 0.61.3", + "windows-core 0.61.2", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +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 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[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 2.0.118", +] + +[[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 2.0.118", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[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.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "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]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "winreg" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wl-clipboard-rs" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix", + "thiserror 2.0.18", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2 0.10.9", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "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 2.0.118", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28b97f866896a4be7aefd2b5a8e01bb6773d19a775d54ab28b4d094b9a4480e" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.3", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e05ad887425eecf5e8384dc2406a4a9313eb73468712fc1cdea362eb4fe0469" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039ca249fee9559680f3a9f05b55e0761fee51af4f6c1e7d8c1f31e549721d2" +dependencies = [ + "serde", + "winnow 1.0.3", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[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 2.0.118", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[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 2.0.118", +] + +[[package]] +name = "zip" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" +dependencies = [ + "crc32fast", + "flate2", + "indexmap 2.14.0", + "memchr", + "typed-path", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "5.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cf057bb00bf5c9ad77abb6147b0ca4818236a1858416e9d988e40d6322fefa7" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.3", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8118ca6bda77bfc0ab51d660db0c955f2505eef854c9a449435bccb616933b31" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.118", + "winnow 1.0.3", +] diff --git a/e2e/app/Cargo.toml b/e2e/app/Cargo.toml new file mode 100644 index 0000000..cde01b3 --- /dev/null +++ b/e2e/app/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "donutbrowser-e2e" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +donutbrowser-lib = { package = "donutbrowser", path = "../../src-tauri", features = ["e2e"] } +tauri-plugin-cross-platform-webdriver = { path = "../../../tauri-cross-platform-webdriver/crates/tauri-plugin-cross-platform-webdriver" } + +[patch.crates-io] +wayland-scanner = { git = "https://github.com/Smithay/wayland-rs", rev = "d07c4f91f28b42e5a485823ffd9d8d5a210b1053" } diff --git a/e2e/app/src/main.rs b/e2e/app/src/main.rs new file mode 100644 index 0000000..d062016 --- /dev/null +++ b/e2e/app/src/main.rs @@ -0,0 +1,7 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + donutbrowser_lib::run_with_builder(|builder| { + builder.plugin(tauri_plugin_cross_platform_webdriver::init()) + }); +} diff --git a/e2e/lib/app.mjs b/e2e/lib/app.mjs index 30fbf78..9f88a32 100644 --- a/e2e/lib/app.mjs +++ b/e2e/lib/app.mjs @@ -47,6 +47,8 @@ export class AppSession { extraEnv = {}, args = [], seedVersionCache = true, + onboardingCompleted = true, + wayfernTermsAccepted = true, }) { this.name = name; this.root = root; @@ -57,6 +59,8 @@ export class AppSession { this.extraEnv = extraEnv; this.args = args; this.seedVersionCache = seedVersionCache; + this.onboardingCompleted = onboardingCompleted; + this.wayfernTermsAccepted = wayfernTermsAccepted; this.session = null; } @@ -70,6 +74,69 @@ export class AppSession { mkdir(path.join(this.root, "tmp"), { recursive: true }), mkdir(path.join(this.root, "artifacts"), { recursive: true }), ]); + if (this.onboardingCompleted) { + const settingsFile = path.join( + this.dataRoot, + "data", + "settings", + "app_settings.json", + ); + await mkdir(path.dirname(settingsFile), { recursive: true }); + await writeFile( + settingsFile, + `${JSON.stringify( + { + language: "en", + onboarding_completed: true, + commercial_trial_acknowledged: true, + window_resize_warning_dismissed: true, + disable_auto_updates: true, + }, + null, + 2, + )}\n`, + { flag: "wx" }, + ).catch((error) => { + if (error.code !== "EEXIST") { + throw error; + } + }); + } + if (this.wayfernTermsAccepted) { + const termsFile = + process.platform === "darwin" + ? path.join( + this.root, + "home", + "Library", + "Application Support", + "Wayfern", + "license-accepted", + ) + : process.platform === "win32" + ? path.join( + this.root, + "windows", + "roaming", + "Wayfern", + "license-accepted", + ) + : path.join( + this.root, + "xdg", + "config", + "Wayfern", + "license-accepted", + ); + await mkdir(path.dirname(termsFile), { recursive: true }); + await writeFile(termsFile, `${Math.floor(Date.now() / 1000)}\n`, { + flag: "wx", + }).catch((error) => { + if (error.code !== "EEXIST") { + throw error; + } + }); + } if (this.seedVersionCache) { const versionCache = path.join( this.root, @@ -257,6 +324,44 @@ export class AppSession { await this.session.click(element); } + async clickTextIn( + containerSelector, + text, + { exact = true, roles = ["button", "tab", "menuitem", "link"] } = {}, + ) { + const element = await this.execute( + ` + const containers = [...document.querySelectorAll(arguments[0])]; + const wanted = arguments[1]; + const exact = arguments[2]; + const roles = new Set(arguments[3]); + const visible = (node) => { + const style = getComputedStyle(node); + const rect = node.getBoundingClientRect(); + return style.visibility !== "hidden" && style.display !== "none" && + rect.width > 0 && rect.height > 0; + }; + for (const container of containers.reverse()) { + if (!visible(container)) continue; + const candidates = [...container.querySelectorAll("button, a, [role], [data-slot='button']")]; + const match = candidates.find((node) => { + const role = node.getAttribute("role") || (node.tagName === "A" ? "link" : "button"); + const label = (node.getAttribute("aria-label") || node.innerText || node.textContent || "").trim(); + return roles.has(role) && visible(node) && (exact ? label === wanted : label.includes(wanted)); + }); + if (match) return match; + } + return null; + `, + [containerSelector, text, exact, roles], + ); + assert.ok( + element, + `No visible interactive element inside ${containerSelector} matched ${JSON.stringify(text)}`, + ); + await this.session.click(element); + } + async clickSelector(selector) { const element = await this.waitFor( () => @@ -366,6 +471,8 @@ export function appFromEnvironment(name, options = {}) { extraEnv: options.extraEnv, args: options.args, seedVersionCache: options.seedVersionCache, + onboardingCompleted: options.onboardingCompleted, + wayfernTermsAccepted: options.wayfernTermsAccepted, }); } diff --git a/e2e/lib/diagnostics.mjs b/e2e/lib/diagnostics.mjs new file mode 100644 index 0000000..7dbbc68 --- /dev/null +++ b/e2e/lib/diagnostics.mjs @@ -0,0 +1,99 @@ +import { chmod, mkdir, readdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { + redactSensitiveText, + sensitiveVariants, +} from "../../scripts/redact-sensitive-text.mjs"; + +const MAX_LOG_BYTES = 512 * 1024; + +async function logFiles(directory, fileNamePattern = /\.(?:log|txt)$/iu) { + const entries = await readdir(directory, { withFileTypes: true }).catch( + () => [], + ); + return entries + .filter((entry) => entry.isFile() && fileNamePattern.test(entry.name)) + .map((entry) => path.join(directory, entry.name)) + .sort(); +} + +async function diagnosticSources(runRoot) { + const sources = await logFiles(path.join(runRoot, "logs")); + const sessions = await readdir(path.join(runRoot, "sessions"), { + withFileTypes: true, + }).catch(() => []); + for (const session of sessions.filter((entry) => entry.isDirectory())) { + const root = path.join(runRoot, "sessions", session.name); + sources.push(...(await logFiles(path.join(root, "donut", "logs")))); + sources.push( + ...(await logFiles(path.join(root, "tmp"), /^donut-proxy-.*\.log$/iu)), + ); + } + return sources; +} + +export async function assertSafeDiagnostics( + diagnosticsRoot, + sensitiveValues = [], +) { + const entries = await readdir(diagnosticsRoot, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isFile() || !/\.(?:json|log)$/iu.test(entry.name)) { + throw new Error(`Unsafe diagnostics entry: ${entry.name}`); + } + const content = await readFile( + path.join(diagnosticsRoot, entry.name), + "utf8", + ); + for (const value of sensitiveVariants(sensitiveValues)) { + if (content.includes(value)) { + throw new Error( + `Sensitive value survived diagnostics redaction in ${entry.name}`, + ); + } + } + } +} + +export async function createSafeDiagnostics( + runRoot, + { suite, failed, sensitiveValues = [] }, +) { + const diagnosticsRoot = path.join(runRoot, "diagnostics"); + await mkdir(diagnosticsRoot, { recursive: true, mode: 0o700 }); + await chmod(diagnosticsRoot, 0o700); + + const sources = await diagnosticSources(runRoot); + for (const [index, source] of sources.entries()) { + const content = await readFile(source, "utf8").catch(() => ""); + const tail = content.slice(-MAX_LOG_BYTES); + const destination = path.join( + diagnosticsRoot, + `${String(index + 1).padStart(3, "0")}.log`, + ); + await writeFile( + destination, + redactSensitiveText(tail, { sensitiveValues }), + { mode: 0o600 }, + ); + await chmod(destination, 0o600); + } + + const summaryPath = path.join(diagnosticsRoot, "summary.json"); + await writeFile( + summaryPath, + `${JSON.stringify( + { + suite, + status: failed ? "failed" : "passed", + sanitized_log_files: sources.length, + }, + null, + 2, + )}\n`, + { mode: 0o600 }, + ); + await chmod(summaryPath, 0o600); + await assertSafeDiagnostics(diagnosticsRoot, sensitiveValues); + return diagnosticsRoot; +} diff --git a/e2e/lib/fixtures.mjs b/e2e/lib/fixtures.mjs index dae1af3..0109b06 100644 --- a/e2e/lib/fixtures.mjs +++ b/e2e/lib/fixtures.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; import { existsSync } from "node:fs"; -import { chmod, copyFile, mkdir, symlink, writeFile } from "node:fs/promises"; +import { chmod, copyFile, cp, mkdir, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; @@ -11,17 +11,13 @@ export function defaultWayfernPath(projectRoot) { if (process.env.DONUT_E2E_WAYFERN_PATH) { return path.resolve(process.env.DONUT_E2E_WAYFERN_PATH); } - const sibling = path.resolve( - projectRoot, - "../wayfern-test/test_extracted_app", - ); - if (process.platform === "darwin") { - return path.join(sibling, "Wayfern.app"); - } - if (process.platform === "win32") { - return path.join(sibling, "Wayfern.exe"); - } - return path.join(sibling, "wayfern"); + const fixtureRoot = path.join(projectRoot, ".cache", "e2e-wayfern-fixture"); + return process.platform === "darwin" + ? path.join(fixtureRoot, "Wayfern.app") + : path.join( + fixtureRoot, + process.platform === "win32" ? "Wayfern.exe" : "wayfern", + ); } export function wayfernExecutable(bundlePath) { @@ -60,18 +56,16 @@ export function inspectWayfern(bundlePath) { return { bundlePath, executable, version: match[1], output }; } -async function linkOrCopy(source, destination) { +async function cloneAppBundle(source, destination) { await mkdir(path.dirname(destination), { recursive: true }); try { - await symlink( - source, - destination, - process.platform === "win32" ? "junction" : undefined, - ); - } catch (error) { - if (error.code !== "EEXIST") { - throw error; - } + execFileSync("/bin/cp", ["-cR", source, destination]); + } catch (_error) { + await cp(source, destination, { + recursive: true, + preserveTimestamps: true, + errorOnExist: true, + }); } } @@ -85,7 +79,10 @@ export async function seedWayfern(dataRoot, wayfern) { ); await mkdir(installDir, { recursive: true }); if (process.platform === "darwin") { - await linkOrCopy(wayfern.bundlePath, path.join(installDir, "Wayfern.app")); + await cloneAppBundle( + wayfern.bundlePath, + path.join(installDir, "Wayfern.app"), + ); } else { const name = process.platform === "win32" ? "wayfern.exe" : "wayfern"; const destination = path.join(installDir, name); @@ -116,6 +113,30 @@ export async function seedWayfern(dataRoot, wayfern) { return installDir; } +export async function prepareWayfern(app, projectRoot) { + const localBundle = defaultWayfernPath(projectRoot); + if (existsSync(localBundle)) { + const wayfern = inspectWayfern(localBundle); + await seedWayfern(app.dataRoot, wayfern); + return { version: wayfern.version, source: "local fixture" }; + } + + if (!app.session) await app.start(); + const current = await app.invoke("fetch_browser_versions_with_count", { + browserStr: "wayfern", + }); + assert.ok( + current.versions.length > 0, + "No Wayfern build is published for this platform", + ); + const version = current.versions[0]; + await app.invoke("download_browser", { + browserStr: "wayfern", + version, + }); + return { version, source: "published download" }; +} + export function wireGuardFixture() { return [ "[Interface]", diff --git a/e2e/run.mjs b/e2e/run.mjs index 2e29c85..8c2de4d 100644 --- a/e2e/run.mjs +++ b/e2e/run.mjs @@ -7,7 +7,15 @@ import { existsSync, statSync, } from "node:fs"; -import { chmod, mkdir, mkdtemp, readFile, rename, rm } from "node:fs/promises"; +import { + chmod, + mkdir, + mkdtemp, + readdir, + readFile, + rename, + rm, +} from "node:fs/promises"; import http from "node:http"; import https from "node:https"; import net from "node:net"; @@ -15,6 +23,7 @@ import os from "node:os"; import path from "node:path"; import { pipeline } from "node:stream/promises"; import { fileURLToPath } from "node:url"; +import { createSafeDiagnostics } from "./lib/diagnostics.mjs"; const dirname = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = path.resolve(dirname, ".."); @@ -26,10 +35,11 @@ const isWindows = process.platform === "win32"; const executableSuffix = isWindows ? ".exe" : ""; const appBinary = path.join( projectRoot, - "src-tauri", + "e2e", + "app", "target", "debug", - `donutbrowser${executableSuffix}`, + `donutbrowser-e2e${executableSuffix}`, ); const driverBinary = path.join( webdriverRoot, @@ -39,17 +49,20 @@ const driverBinary = path.join( ); const suiteFiles = { - smoke: ["smoke.test.mjs", "coverage.test.mjs"], + smoke: ["diagnostics.test.mjs", "smoke.test.mjs", "coverage.test.mjs"], ui: ["ui.test.mjs"], entities: ["entities.test.mjs"], + network: ["network.test.mjs"], integrations: ["integrations.test.mjs"], sync: ["sync.test.mjs"], browser: ["browser.test.mjs"], full: [ + "diagnostics.test.mjs", "coverage.test.mjs", "smoke.test.mjs", "ui.test.mjs", "entities.test.mjs", + "network.test.mjs", "integrations.test.mjs", "sync.test.mjs", "browser.test.mjs", @@ -189,28 +202,42 @@ async function stopProcess(record) { record.stream.end(); } -async function loadLocalToken() { - if (process.env.WAYFERN_TEST_TOKEN) { - return process.env.WAYFERN_TEST_TOKEN; +function unquoteEnvValue(value) { + const trimmed = value.trim(); + const quote = trimmed[0]; + if ((quote === '"' || quote === "'") && trimmed.at(-1) === quote) { + return trimmed.slice(1, -1); } - for (const file of [ - path.join(projectRoot, ".env"), - path.resolve(projectRoot, "../wayfern-test/.env"), - ]) { + return trimmed; +} + +async function loadLocalValues(names) { + const values = Object.fromEntries( + names + .filter((name) => process.env[name]) + .map((name) => [name, process.env[name]]), + ); + for (const file of [path.join(projectRoot, ".env")]) { try { const content = await readFile(file, "utf8"); - const match = content.match( - /^\s*(?:export\s+)?WAYFERN_TEST_TOKEN\s*=\s*(.+?)\s*$/m, - ); - if (match) { - const raw = match[1].trim(); - return raw.replace(/^(['"])(.*)\1$/, "$2"); + for (const name of names) { + if (values[name]) continue; + const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const match = content.match( + new RegExp( + `^\\s*(?:export\\s+)?${escapedName}\\s*=\\s*(.+?)\\s*$`, + "m", + ), + ); + if (match) { + values[name] = unquoteEnvValue(match[1]); + } } } catch { - // A local token is only mandatory for the browser suite. + // Individual suites validate the secrets they require. } } - return ""; + return values; } function buildAll() { @@ -221,8 +248,8 @@ function buildAll() { run("pnpm", ["copy-proxy-binary"], projectRoot); run( "cargo", - ["build", "--features", "e2e", "--bin", "donutbrowser"], - path.join(projectRoot, "src-tauri"), + ["build", "--locked", "--manifest-path", "e2e/app/Cargo.toml"], + projectRoot, ); run( "cargo", @@ -474,21 +501,159 @@ async function startSyncInfrastructure(runRoot, options, records) { }; } +function runDocker(args, { allowFailure = false } = {}) { + const result = spawnSync("docker", args, { + encoding: "utf8", + timeout: 120_000, + maxBuffer: 10 * 1024 * 1024, + }); + if (!allowFailure && (result.error || result.status !== 0)) { + throw new Error( + `docker ${args[0]} failed: ${result.error?.message ?? result.stderr?.trim() ?? `exit ${result.status}`}`, + ); + } + return result; +} + +function dockerAvailable() { + const result = runDocker(["version"], { allowFailure: true }); + return !result.error && result.status === 0; +} + +async function startWireGuardInfrastructure() { + if (!dockerAvailable()) { + throw new Error( + "The network E2E suite requires a running Docker daemon for its local WireGuard peer", + ); + } + + const port = await freePort(); + const name = `donut-wg-e2e-${process.pid}-${Date.now()}`; + const image = + process.env.DONUT_E2E_WIREGUARD_IMAGE ?? + "lscr.io/linuxserver/wireguard:latest"; + log("Starting isolated local WireGuard peer"); + runDocker([ + "run", + "-d", + "--name", + name, + "--cap-add=NET_ADMIN", + "-p", + `${port}:51820/udp`, + "-e", + "PEERS=1", + "-e", + "SERVERURL=127.0.0.1", + "-e", + "SERVERPORT=51820", + "-e", + "PEERDNS=auto", + "-e", + "INTERNAL_SUBNET=10.64.0.0", + image, + ]); + + try { + const deadline = Date.now() + 45_000; + let config = ""; + while (Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 1_000)); + const configResult = runDocker( + ["exec", name, "cat", "/config/peer1/peer1.conf"], + { allowFailure: true }, + ); + const statusResult = runDocker(["exec", name, "wg", "show"], { + allowFailure: true, + }); + if ( + configResult.status === 0 && + statusResult.status === 0 && + statusResult.stdout.includes("listening port") + ) { + config = configResult.stdout; + break; + } + } + if (!config) { + throw new Error("Local WireGuard peer did not become ready within 45s"); + } + + const server = runDocker([ + "exec", + "-d", + name, + "sh", + "-c", + 'while true; do printf "HTTP/1.1 200 OK\\r\\nContent-Length: 13\\r\\nConnection: close\\r\\n\\r\\nWG-TUNNEL-OK\\n" | nc -l -p 8080 >> /tmp/donut-e2e-target-requests 2>/dev/null; done', + ]); + if (server.status !== 0) { + throw new Error("Failed to start the WireGuard tunnel target server"); + } + config = config.replace( + /^Endpoint\s*=.*$/m, + `Endpoint = 127.0.0.1:${port}`, + ); + return { + name, + config, + targetUrl: "http://10.64.0.1:8080/donut-e2e-wireguard", + }; + } catch (error) { + runDocker(["rm", "-f", name], { allowFailure: true }); + throw error; + } +} + +async function prepareRetainedArtifacts( + runRoot, + { suite, failed, sensitiveValues }, +) { + const sessionsRoot = path.join(runRoot, "sessions"); + const sessions = await readdir(sessionsRoot, { + withFileTypes: true, + }).catch(() => []); + await Promise.all( + sessions + .filter((entry) => entry.isDirectory()) + .map((entry) => + rm(path.join(sessionsRoot, entry.name, "donut", "data", "binaries"), { + recursive: true, + force: true, + }), + ), + ); + await createSafeDiagnostics(runRoot, { suite, failed, sensitiveValues }); +} + async function main() { const options = parseArgs(process.argv.slice(2)); const runRoot = await mkdtemp(path.join(os.tmpdir(), "donut-e2e-")); await mkdir(path.join(runRoot, "logs"), { recursive: true }); const records = []; let fixture; + let wireGuard; let failed = false; + const sensitiveValues = [ + "donut-e2e-sync-token-0123456789abcdef", + "minioadmin", + ]; const cleanup = async () => { await Promise.all(records.reverse().map(stopProcess)); if (fixture) { await new Promise((resolve) => fixture.server.close(resolve)); } + if (wireGuard) { + runDocker(["rm", "-f", wireGuard.name], { allowFailure: true }); + } if (!options.keep && !failed) { await rm(runRoot, { recursive: true, force: true }); } else { + await prepareRetainedArtifacts(runRoot, { + suite: options.suite, + failed, + sensitiveValues, + }); log(`Artifacts retained at ${runRoot}`); } }; @@ -536,22 +701,42 @@ async function main() { await waitForUrl(`http://127.0.0.1:${driverPort}/status`, 15_000, driver); const needsBrowser = - options.suite === "browser" || options.suite === "full"; + options.suite === "browser" || + options.suite === "network" || + options.suite === "full"; + const networkEnabled = + (options.suite === "network" || options.suite === "full") && + process.env.DONUT_E2E_SKIP_NETWORK_TEST !== "1"; const geoIpFixture = needsBrowser ? await ensureGeoIpFixture() : null; fixture = await startFixtureServer(geoIpFixture); let sync = {}; if (options.suite === "sync" || options.suite === "full") { sync = await startSyncInfrastructure(runRoot, options, records); } - - const token = await loadLocalToken(); - if ((options.suite === "browser" || options.suite === "full") && !token) { - throw new Error("WAYFERN_TEST_TOKEN is required by the browser suite"); + if (networkEnabled && process.env.DONUT_E2E_SKIP_VPN_TUNNEL !== "1") { + wireGuard = await startWireGuardInfrastructure(); } - const files = suiteFiles[options.suite].map((file) => - path.join(dirname, "tests", file), - ); + const localValues = await loadLocalValues([ + "WAYFERN_TEST_TOKEN", + "RESIDENTIAL_PROXY_URL_ONE_SOCKS", + "RESIDENTIAL_PROXY_URL_ONE_HTTP", + ]); + sensitiveValues.push(...Object.values(localValues)); + const token = localValues.WAYFERN_TEST_TOKEN ?? ""; + if (needsBrowser && !token) { + throw new Error("WAYFERN_TEST_TOKEN is required by the browser suite"); + } + if (wireGuard) { + sensitiveValues.push( + wireGuard.config, + Buffer.from(wireGuard.config).toString("base64"), + ); + } + + const files = suiteFiles[options.suite] + .filter((file) => networkEnabled || file !== "network.test.mjs") + .map((file) => path.join(dirname, "tests", file)); const testArgs = [ "--test", "--test-concurrency=1", @@ -570,9 +755,18 @@ async function main() { DONUT_E2E_FIXTURE_URL: `http://127.0.0.1:${fixture.port}`, DONUT_E2E_GEOIP_FIXTURE_READY: geoIpFixture ? "1" : "0", WAYFERN_TEST_TOKEN: token, + RESIDENTIAL_PROXY_URL_ONE_SOCKS: + localValues.RESIDENTIAL_PROXY_URL_ONE_SOCKS ?? "", + RESIDENTIAL_PROXY_URL_ONE_HTTP: + localValues.RESIDENTIAL_PROXY_URL_ONE_HTTP ?? "", DONUT_E2E_SYNC_URL: sync.syncUrl ?? "", DONUT_E2E_SYNC_TOKEN: sync.syncToken ?? "", DONUT_E2E_MINIO_URL: sync.minioUrl ?? "", + DONUT_E2E_WIREGUARD_CONFIG_BASE64: wireGuard + ? Buffer.from(wireGuard.config).toString("base64") + : "", + DONUT_E2E_WIREGUARD_TARGET_URL: wireGuard?.targetUrl ?? "", + DONUT_E2E_WIREGUARD_CONTAINER: wireGuard?.name ?? "", }, stdio: "inherit", }); diff --git a/e2e/tests/browser.test.mjs b/e2e/tests/browser.test.mjs index 58d0a3e..be278e7 100644 --- a/e2e/tests/browser.test.mjs +++ b/e2e/tests/browser.test.mjs @@ -7,11 +7,7 @@ import path from "node:path"; import test from "node:test"; import { appFromEnvironment } from "../lib/app.mjs"; import { CdpClient } from "../lib/cdp.mjs"; -import { - defaultWayfernPath, - inspectWayfern, - seedWayfern, -} from "../lib/fixtures.mjs"; +import { defaultWayfernPath, prepareWayfern } from "../lib/fixtures.mjs"; const fixtureUrl = process.env.DONUT_E2E_FIXTURE_URL; @@ -36,30 +32,6 @@ async function request(url, { method = "GET", token, body } = {}) { return { response, value }; } -async function prepareWayfern(app) { - const localBundle = defaultWayfernPath(process.env.DONUT_E2E_PROJECT_ROOT); - if (existsSync(localBundle)) { - const wayfern = inspectWayfern(localBundle); - await seedWayfern(app.dataRoot, wayfern); - return { version: wayfern.version, source: "local fixture" }; - } - - await app.start(); - const current = await app.invoke("fetch_browser_versions_with_count", { - browserStr: "wayfern", - }); - assert.ok( - current.versions.length > 0, - "No Wayfern build is published for this platform", - ); - const version = current.versions[0]; - await app.invoke("download_browser", { - browserStr: "wayfern", - version, - }); - return { version, source: "published download" }; -} - function processExists(pid) { if (!pid) return false; try { @@ -166,11 +138,15 @@ test("real Wayfern fingerprinting, terms, API automation, CDP, cookies, and proc ); const app = appFromEnvironment("browser-wayfern", { seedVersionCache: hasLocalWayfern, + wayfernTermsAccepted: false, }); let cdp; let browserPid; try { - const prepared = await prepareWayfern(app); + const prepared = await prepareWayfern( + app, + process.env.DONUT_E2E_PROJECT_ROOT, + ); if (!app.session) await app.start(); assert.equal(await app.invoke("check_wayfern_downloaded"), true); diff --git a/e2e/tests/coverage.test.mjs b/e2e/tests/coverage.test.mjs index bab35a5..85bb540 100644 --- a/e2e/tests/coverage.test.mjs +++ b/e2e/tests/coverage.test.mjs @@ -1,9 +1,18 @@ import assert from "node:assert/strict"; -import { readFile } from "node:fs/promises"; +import { + lstat, + mkdir, + mkdtemp, + readFile, + rm, + writeFile, +} from "node:fs/promises"; import http from "node:http"; +import os from "node:os"; import path from "node:path"; import test from "node:test"; import { allCoveredCommands, commandCoverage } from "../coverage-map.mjs"; +import { seedWayfern } from "../lib/fixtures.mjs"; import { WebDriverClient } from "../lib/webdriver.mjs"; function registeredCommands(source) { @@ -58,10 +67,15 @@ test("every Tauri command has exactly one E2E owner and evidence level", async ( continue; } - const suiteSource = await readFile( + const evidenceFiles = [ path.join(root, "e2e", "tests", `${entry.suite}.test.mjs`), - "utf8", - ); + ...(entry.suite === "browser" + ? [path.join(root, "e2e", "lib", "fixtures.mjs")] + : []), + ]; + const suiteSource = ( + await Promise.all(evidenceFiles.map((file) => readFile(file, "utf8"))) + ).join("\n"); for (const command of entry.commands) { assert.equal( commandHasExecutableEvidence(suiteSource, command), @@ -94,3 +108,38 @@ test("WebDriver client preserves application values that contain an error field" await new Promise((resolve) => server.close(resolve)); } }); + +test("Wayfern fixtures are copied into the isolated data root, never linked", async (t) => { + const root = await mkdtemp(path.join(os.tmpdir(), "donut-wayfern-copy-")); + t.after(() => rm(root, { recursive: true, force: true })); + const source = + process.platform === "darwin" + ? path.join(root, "source", "Wayfern.app", "Contents", "MacOS", "Wayfern") + : path.join( + root, + "source", + process.platform === "win32" ? "Wayfern.exe" : "wayfern", + ); + await mkdir(path.dirname(source), { recursive: true }); + await writeFile(source, "source-fixture"); + const bundlePath = + process.platform === "darwin" + ? path.join(root, "source", "Wayfern.app") + : source; + const installDir = await seedWayfern(path.join(root, "isolated"), { + bundlePath, + executable: source, + version: "1.2.3.4", + }); + const destination = + process.platform === "darwin" + ? path.join(installDir, "Wayfern.app", "Contents", "MacOS", "Wayfern") + : path.join( + installDir, + process.platform === "win32" ? "wayfern.exe" : "wayfern", + ); + + assert.equal((await lstat(destination)).isSymbolicLink(), false); + await writeFile(destination, "isolated-mutation"); + assert.equal(await readFile(source, "utf8"), "source-fixture"); +}); diff --git a/e2e/tests/diagnostics.test.mjs b/e2e/tests/diagnostics.test.mjs new file mode 100644 index 0000000..4243a5f --- /dev/null +++ b/e2e/tests/diagnostics.test.mjs @@ -0,0 +1,116 @@ +import assert from "node:assert/strict"; +import { + mkdir, + mkdtemp, + readdir, + readFile, + rm, + writeFile, +} from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { after, test } from "node:test"; +import { redactIssueBody } from "../../scripts/redact-sensitive-text.mjs"; +import { createSafeDiagnostics } from "../lib/diagnostics.mjs"; + +const roots = []; +after(async () => { + await Promise.all( + roots.map((root) => rm(root, { recursive: true, force: true })), + ); +}); + +test("shared E2E diagnostics contain only redacted text logs", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "donut-diagnostics-test-")); + roots.push(root); + const secretUrl = "http://real-user:real-password@proxy.example:8080"; + const token = ["github", "pat", "example", "token", "0123456789"].join("_"); + const logText = [ + `proxy=${secretUrl}`, + `Authorization: Bearer ${token}`, + "visited https://example.com/callback?code=private-code", + "exit IP 203.0.113.42", + "home /Users/private-person/Library/Application Support", + "email private.person@example.com", + "PrivateKey = wireguard-private-key", + ].join("\n"); + + await Promise.all([ + mkdir(path.join(root, "logs"), { recursive: true }), + mkdir(path.join(root, "sessions", "network", "donut", "logs"), { + recursive: true, + }), + mkdir(path.join(root, "sessions", "network", "donut", "data", "proxies"), { + recursive: true, + }), + mkdir(path.join(root, "sessions", "network", "artifacts"), { + recursive: true, + }), + ]); + await Promise.all([ + writeFile(path.join(root, "logs", "driver.log"), logText), + writeFile( + path.join(root, "sessions", "network", "donut", "logs", "app.log"), + logText, + ), + writeFile( + path.join( + root, + "sessions", + "network", + "donut", + "data", + "proxies", + "real.json", + ), + JSON.stringify({ upstream_url: secretUrl, token }), + ), + writeFile( + path.join(root, "sessions", "network", "artifacts", "page.html"), + `${secretUrl}`, + ), + ]); + + const diagnostics = await createSafeDiagnostics(root, { + suite: "network", + failed: true, + sensitiveValues: [secretUrl, token], + }); + const files = await readdir(diagnostics); + assert.deepEqual(files.sort(), ["001.log", "002.log", "summary.json"]); + const combined = ( + await Promise.all( + files.map((file) => readFile(path.join(diagnostics, file), "utf8")), + ) + ).join("\n"); + for (const value of [ + secretUrl, + "real-user", + "real-password", + "proxy.example", + token, + "private-code", + "203.0.113.42", + "private-person", + "private.person@example.com", + "wireguard-private-key", + ]) { + assert.ok(!combined.includes(value), `diagnostics leaked ${value}`); + } + assert.ok( + !files.some( + (file) => /\.(?:html|json)$/u.test(file) && file !== "summary.json", + ), + ); +}); + +test("automated issue processing omits the complete log field", () => { + const safe = redactIssueBody( + `### What happened?\nA failure at user@example.com\n\n### Error logs or screenshots\nARBITRARY_PRIVATE_LOG_CONTENT\npassword=hunter2\n\n### Operating System\nLinux`, + ); + assert.ok(!safe.includes("ARBITRARY_PRIVATE_LOG_CONTENT")); + assert.ok(!safe.includes("hunter2")); + assert.ok(!safe.includes("user@example.com")); + assert.match(safe, /omitted from automated processing/u); + assert.match(safe, /Operating System\nLinux/u); +}); diff --git a/e2e/tests/entities.test.mjs b/e2e/tests/entities.test.mjs index 1ee1dd8..78f2856 100644 --- a/e2e/tests/entities.test.mjs +++ b/e2e/tests/entities.test.mjs @@ -25,7 +25,6 @@ async function createProfile(app, name = "Entity Profile") { test("profile, group, proxy, tag, metadata, clone, and bulk-delete lifecycle", async () => { await withApp("entities-core", async (app) => { - await app.invoke("complete_onboarding"); const group = await app.invoke("create_profile_group", { name: "Research", }); diff --git a/e2e/tests/network.test.mjs b/e2e/tests/network.test.mjs new file mode 100644 index 0000000..1ff8a92 --- /dev/null +++ b/e2e/tests/network.test.mjs @@ -0,0 +1,605 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { readdir, readFile, writeFile } from "node:fs/promises"; +import { isIP } from "node:net"; +import path from "node:path"; +import test from "node:test"; +import { appFromEnvironment } from "../lib/app.mjs"; +import { CdpClient } from "../lib/cdp.mjs"; +import { + extensionZipBase64, + prepareWayfern, + wireGuardFixture, +} from "../lib/fixtures.mjs"; + +function proxySettings(raw, expectedKind) { + assert.ok(raw, `${expectedKind} residential proxy URL is required`); + const url = new URL(raw); + const rawType = url.protocol.slice(0, -1).toLowerCase(); + const proxyType = + rawType === "socks" || rawType === "socks5h" ? "socks5" : rawType; + if (expectedKind === "HTTP") { + assert.ok( + proxyType === "http" || proxyType === "https", + `Expected an HTTP proxy URL, got ${rawType}`, + ); + } else { + assert.equal(proxyType, "socks5"); + } + const port = Number(url.port); + assert.ok(url.hostname && port > 0 && port <= 65535); + return { + proxy_type: proxyType, + host: url.hostname, + port, + username: url.username ? decodeURIComponent(url.username) : null, + password: url.password ? decodeURIComponent(url.password) : null, + }; +} + +function wireGuardFields(config) { + let section = ""; + const fields = new Map(); + for (const rawLine of config.split(/\r?\n/)) { + const line = rawLine.trim(); + if (!line || line.startsWith("#")) continue; + if (line === "[Interface]") { + section = "interface"; + continue; + } + if (line === "[Peer]") { + section = "peer"; + continue; + } + const separator = line.indexOf("="); + if (separator === -1) continue; + fields.set( + `${section}.${line.slice(0, separator).trim()}`, + line.slice(separator + 1).trim(), + ); + } + return { + privateKey: fields.get("interface.PrivateKey"), + address: fields.get("interface.Address"), + dns: fields.get("interface.DNS") ?? "", + peerPublicKey: fields.get("peer.PublicKey"), + peerEndpoint: fields.get("peer.Endpoint"), + allowedIps: fields.get("peer.AllowedIPs") ?? "0.0.0.0/0", + persistentKeepalive: fields.get("peer.PersistentKeepalive") ?? "", + presharedKey: fields.get("peer.PresharedKey") ?? "", + }; +} + +async function request(url, { method = "GET", token, body } = {}) { + const response = await fetch(url, { + method, + headers: { + ...(token ? { authorization: `Bearer ${token}` } : {}), + ...(body === undefined ? {} : { "content-type": "application/json" }), + }, + body: body === undefined ? undefined : JSON.stringify(body), + }); + const text = await response.text(); + let value = text; + if (text) { + try { + value = JSON.parse(text); + } catch { + // Plain-text responses are intentional for some endpoints. + } + } + return { response, value }; +} + +async function createGroupThroughUi(app) { + await app.clickSelector('[aria-label="Groups"]'); + await app.waitForText("Profile groups"); + await app.clickSelector('[aria-label="Create"]'); + await app.waitForText("Create New Group"); + await app.fillSelector("#group-name", "Visible UI Group"); + await app.clickTextIn('[role="dialog"]', "Create", { roles: ["button"] }); + await app.waitForText("Visible UI Group"); + const groups = await app.invoke("get_profile_groups"); + return groups.find((group) => group.name === "Visible UI Group"); +} + +async function createProxyThroughUi(app, settings) { + await app.clickSelector('[aria-label="Network"]'); + await app.waitForText("New proxy"); + await app.clickSelector('[aria-label="New proxy"]'); + await app.waitForText("Add Proxy"); + await app.fillSelector("#proxy-name", "Visible Residential HTTP"); + await app.fillSelector("#proxy-host", settings.host); + await app.fillSelector("#proxy-port", String(settings.port)); + if (settings.username) + await app.fillSelector("#proxy-username", settings.username); + if (settings.password) + await app.fillSelector("#proxy-password", settings.password); + await app.clickTextIn('[role="dialog"]', "Add Proxy", { + roles: ["button"], + }); + await app.waitForText("Visible Residential HTTP"); + const proxies = await app.invoke("get_stored_proxies"); + return proxies.find((proxy) => proxy.name === "Visible Residential HTTP"); +} + +async function createVpnThroughUi(app, config) { + const fields = wireGuardFields(config); + assert.ok( + fields.privateKey && + fields.address && + fields.peerPublicKey && + fields.peerEndpoint, + "WireGuard fixture is missing required fields", + ); + await app.clickText("VPNs", { exact: false, roles: ["tab"] }); + await app.clickSelector('[aria-label="New VPN"]'); + await app.waitForText("Create WireGuard VPN"); + await app.fillSelector("#wg-name", "Visible Local WireGuard"); + await app.fillSelector("#wg-private-key", fields.privateKey); + await app.fillSelector("#wg-address", fields.address); + if (fields.dns) await app.fillSelector("#wg-dns", fields.dns); + await app.fillSelector("#wg-peer-public-key", fields.peerPublicKey); + await app.fillSelector("#wg-peer-endpoint", fields.peerEndpoint); + await app.fillSelector("#wg-allowed-ips", fields.allowedIps); + if (fields.persistentKeepalive) { + await app.fillSelector("#wg-keepalive", fields.persistentKeepalive); + } + if (fields.presharedKey) { + await app.fillSelector("#wg-preshared-key", fields.presharedKey); + } + await app.clickTextIn('[role="dialog"]', "Create VPN", { + roles: ["button"], + }); + await app.waitForText("Visible Local WireGuard"); + const vpns = await app.invoke("list_vpn_configs"); + return vpns.find((vpn) => vpn.name === "Visible Local WireGuard"); +} + +async function createExtensionsThroughUi(app) { + const extensionFile = path.join(app.root, "visible-extension.zip"); + await writeFile(extensionFile, Buffer.from(extensionZipBase64(), "base64")); + await app.clickSelector('[aria-label="Extensions"]'); + await app.waitForText("Upload"); + + await app.execute(` + const input = document.querySelector("#ext-file-input"); + input.classList.remove("hidden"); + input.style.position = "fixed"; + input.style.left = "12px"; + input.style.bottom = "12px"; + `); + const input = await app.session.findCss("#ext-file-input"); + await app.session.sendKeys(input, extensionFile); + await app.waitForText("visible-extension.zip"); + await app.fillSelector( + 'input[placeholder="Extension name"]', + "Visible UI Extension", + ); + await app.clickText("Add", { roles: ["button"] }); + await app.waitForText("Donut E2E Fixture"); + + await app.clickText("Groups", { exact: false, roles: ["tab"] }); + await app.clickSelector('[aria-label="New group"]'); + await app.fillSelector( + 'input[placeholder="Group name"]', + "Visible Extension Group", + ); + await app.clickText("Create", { roles: ["button"] }); + await app.waitForText("Visible Extension Group"); + + let [extensions, groups] = await Promise.all([ + app.invoke("list_extensions"), + app.invoke("list_extension_groups"), + ]); + const extension = extensions.find( + (item) => item.name === "Donut E2E Fixture", + ); + let group = groups.find((item) => item.name === "Visible Extension Group"); + assert.ok(extension && group); + + const editButton = await app.execute( + ` + const row = [...document.querySelectorAll("tr")].find((candidate) => + (candidate.innerText || "").includes(arguments[0]) + ); + return row?.querySelector("td:last-child button") ?? null; + `, + [group.name], + ); + assert.ok(editButton, "Extension group edit control was not visible"); + await app.session.click(editButton); + await app.waitForText("Edit Group"); + const extensionPicker = await app.execute(` + const dialogs = [...document.querySelectorAll('[role="dialog"]')]; + return dialogs.reverse().find( + (dialog) => (dialog.innerText || "").includes("Edit Group") + )?.querySelector('[role="combobox"]') ?? null; + `); + assert.ok(extensionPicker, "Extension picker was not visible"); + await app.session.click(extensionPicker); + await app.clickText(extension.name, { roles: ["option"] }); + await app.clickTextIn('[role="dialog"]', "Save", { roles: ["button"] }); + await app.waitFor( + async () => { + groups = await app.invoke("list_extension_groups"); + group = groups.find((item) => item.name === "Visible Extension Group"); + return group?.extension_ids.includes(extension.id); + }, + { description: "uploaded extension added to visible extension group" }, + ); + + return { + extension, + group, + }; +} + +async function createProfileThroughUi(app, groupName) { + await app.clickSelector('[aria-label="Profiles"]'); + await app.clickText(groupName, { exact: false, roles: ["button"] }); + await app.clickText("New", { roles: ["button"] }); + await app.waitFor( + async () => { + const text = await app.bodyText(); + return ( + text.includes("Create New Profile") || + text.includes("Create New Chromium Profile") + ); + }, + { description: "profile creation dialog" }, + ); + if (!(await app.visibleTextIncludes("Create New Chromium Profile"))) { + await app.clickText("Chromium", { exact: false, roles: ["button"] }); + await app.waitForText("Create New Chromium Profile"); + } + await app.fillSelector("#profile-name", "Visible Network Profile"); + await app.clickTextIn('[role="dialog"]', "Create", { roles: ["button"] }); + await app.waitForText("Visible Network Profile", 60_000); + await app.waitFor( + async () => + !(await app.execute(` + return [...document.querySelectorAll('[role="dialog"]')].some( + (dialog) => + (dialog.innerText || "").includes("Create New Chromium Profile") + ); + `)), + { description: "profile creation dialog to unmount" }, + ); + const profiles = await app.invoke("list_browser_profiles"); + return profiles.find((profile) => profile.name === "Visible Network Profile"); +} + +async function assignNetworkThroughUi(app, profileName, currentName, newName) { + const trigger = await app.execute( + ` + const row = [...document.querySelectorAll("tr")].find((candidate) => + (candidate.innerText || "").includes(arguments[0]) + ); + const expected = arguments[1].toLocaleLowerCase(); + return [...(row?.querySelectorAll('[aria-haspopup="dialog"]') ?? [])].find( + (trigger) => (trigger.innerText || trigger.textContent || "") + .toLocaleLowerCase() + .includes(expected) + ) ?? null; + `, + [profileName, currentName], + ); + assert.ok(trigger, `Network selector for ${profileName} was not visible`); + await app.session.click(trigger); + await app.clickText(newName, { exact: false, roles: ["option"] }); + await app.waitFor( + () => + app.execute( + ` + return ![...document.querySelectorAll('[data-slot="popover-content"]')] + .some((content) => (content.innerText || "").includes(arguments[0])); + `, + [newName], + ), + { description: `${newName} network picker to unmount` }, + ); +} + +async function assignExtensionGroupThroughUi( + app, + profileName, + currentName, + newName, +) { + const trigger = await app.execute( + ` + const row = [...document.querySelectorAll("tr")].find((candidate) => + (candidate.innerText || "").includes(arguments[0]) + ); + return [...(row?.querySelectorAll("button") ?? [])].find( + (button) => (button.innerText || button.textContent || "") + .trim() + .includes(arguments[1]) + ) ?? null; + `, + [profileName, currentName], + ); + assert.ok(trigger, `Extension selector for ${profileName} was not visible`); + await app.session.click(trigger); + await app.clickText(newName, { exact: false, roles: ["option"] }); + await app.waitFor( + () => + app.execute( + ` + return ![...document.querySelectorAll('[data-slot="popover-content"]')] + .some((content) => (content.innerText || "").includes(arguments[0])); + `, + [newName], + ), + { description: `${newName} extension picker to unmount` }, + ); +} + +async function runProfile(_app, base, token, profileId, url) { + const launched = await request(`${base}/v1/profiles/${profileId}/run`, { + method: "POST", + token, + body: { url, headless: true }, + }); + assert.equal(launched.response.status, 200, JSON.stringify(launched.value)); + const cdp = await CdpClient.connect(launched.value.remote_debugging_port); + return { launched: launched.value, cdp }; +} + +async function stopProfile(app, base, token, profileId, cdp) { + cdp.close(); + const stopped = await request(`${base}/v1/profiles/${profileId}/kill`, { + method: "POST", + token, + }); + assert.equal(stopped.response.status, 204); + await app.waitFor( + async () => { + const profile = (await app.invoke("list_browser_profiles")).find( + (item) => item.id === profileId, + ); + return !profile?.process_id; + }, + { timeoutMs: 20_000, description: "network profile process cleanup" }, + ); +} + +async function assertProxyWorkerLogsRedacted(app, settings) { + const files = (await readdir(path.join(app.root, "tmp"))).filter( + (file) => file.startsWith("donut-proxy-") && file.endsWith(".log"), + ); + assert.ok(files.length > 0, "No proxy worker diagnostic logs were created"); + const contents = ( + await Promise.all( + files.map((file) => readFile(path.join(app.root, "tmp", file), "utf8")), + ) + ).join("\n"); + for (const item of settings) { + if (!item.username) continue; + const rawAuth = `${item.username}:${item.password ?? ""}@`; + const encodedAuth = `${encodeURIComponent(item.username)}:${encodeURIComponent(item.password ?? "")}@`; + assert.equal( + contents.includes(rawAuth) || contents.includes(encodedAuth), + false, + "Proxy worker logs exposed upstream credentials", + ); + } +} + +function wireGuardTargetWasReached() { + const container = process.env.DONUT_E2E_WIREGUARD_CONTAINER; + assert.ok(container, "WireGuard fixture container name is required"); + return ( + spawnSync( + "docker", + [ + "exec", + container, + "grep", + "-q", + "GET /donut-e2e-wireguard ", + "/tmp/donut-e2e-target-requests", + ], + { stdio: "ignore", timeout: 2_000 }, + ).status === 0 + ); +} + +test("visible UI creates and assigns profiles, groups, proxies, VPNs, extensions, and extension groups", async () => { + const httpSettings = proxySettings( + process.env.RESIDENTIAL_PROXY_URL_ONE_HTTP, + "HTTP", + ); + const socksSettings = proxySettings( + process.env.RESIDENTIAL_PROXY_URL_ONE_SOCKS, + "SOCKS", + ); + const realWireGuardConfig = process.env.DONUT_E2E_WIREGUARD_CONFIG_BASE64 + ? Buffer.from( + process.env.DONUT_E2E_WIREGUARD_CONFIG_BASE64, + "base64", + ).toString("utf8") + : null; + const app = appFromEnvironment("network-visible-ui", { + wayfernTermsAccepted: false, + }); + let apiPort; + let activeCdp; + let activeVpnId; + try { + const prepared = await prepareWayfern( + app, + process.env.DONUT_E2E_PROJECT_ROOT, + ); + if (!app.session) await app.start(); + if (!(await app.invoke("check_wayfern_terms_accepted"))) { + await app.invoke("accept_wayfern_terms"); + await app.restart(); + } + assert.equal( + await app.visibleTextIncludes("Welcome to Donut Browser"), + false, + "completed test sessions must not leave the Welcome dialog over the UI", + ); + const group = await createGroupThroughUi(app); + assert.ok(group); + await app.capture("01-profile-group-created"); + + const httpProxy = await createProxyThroughUi(app, httpSettings); + assert.ok(httpProxy); + assert.equal( + httpProxy.proxy_settings.proxy_type === httpSettings.proxy_type && + httpProxy.proxy_settings.host === httpSettings.host && + httpProxy.proxy_settings.port === httpSettings.port && + httpProxy.proxy_settings.username === httpSettings.username && + httpProxy.proxy_settings.password === httpSettings.password, + true, + "The HTTP proxy created through the UI did not preserve its settings", + ); + const vpn = await createVpnThroughUi( + app, + realWireGuardConfig ?? wireGuardFixture(), + ); + assert.ok(vpn); + activeVpnId = vpn.id; + await app.capture("02-proxy-and-vpn-created"); + + const extensionEntities = await createExtensionsThroughUi(app); + assert.ok(extensionEntities.extension); + assert.ok(extensionEntities.group); + await app.capture("03-extension-and-group-created"); + + const profile = await createProfileThroughUi(app, group.name); + assert.ok(profile); + assert.equal(profile.version, prepared.version); + assert.equal(profile.group_id, group.id); + await assignExtensionGroupThroughUi( + app, + profile.name, + "Default", + extensionEntities.group.name, + ); + await app.waitFor( + async () => + (await app.invoke("list_browser_profiles")).find( + (item) => item.id === profile.id, + )?.extension_group_id === extensionEntities.group.id, + { description: "extension group assignment persisted" }, + ); + await app.capture("04-profile-created"); + + const socksProxy = await app.invoke("create_stored_proxy", { + name: "Residential SOCKS5", + proxySettings: socksSettings, + }); + const [httpCheck, socksCheck] = await Promise.all([ + app.invoke("check_proxy_validity", { + proxyId: httpProxy.id, + proxySettings: null, + }), + app.invoke("check_proxy_validity", { + proxyId: socksProxy.id, + proxySettings: null, + }), + ]); + assert.equal(httpCheck.is_valid, true); + assert.equal(socksCheck.is_valid, true); + assert.ok(isIP(httpCheck.ip)); + assert.ok(isIP(socksCheck.ip)); + + await assignNetworkThroughUi( + app, + profile.name, + "Not selected", + httpProxy.name, + ); + await app.waitFor( + async () => + (await app.invoke("list_browser_profiles")).find( + (item) => item.id === profile.id, + )?.proxy_id === httpProxy.id, + { description: "HTTP proxy assignment persisted" }, + ); + + const settings = await app.invoke("get_app_settings"); + const saved = await app.invoke("save_app_settings", { + settings: { + ...settings, + api_enabled: true, + api_port: 0, + api_token: null, + }, + }); + apiPort = await app.invoke("start_api_server", { port: 0 }); + const base = `http://127.0.0.1:${apiPort}`; + + const proxied = await runProfile( + app, + base, + saved.api_token, + profile.id, + "https://api.ipify.org/", + ); + activeCdp = proxied.cdp; + const browserExitIp = await activeCdp.waitFor( + `(() => { + const value = document.body?.innerText?.trim() ?? ""; + return /^[0-9a-f:.]+$/i.test(value) ? value : false; + })()`, + { timeoutMs: 30_000, description: "Wayfern residential proxy exit IP" }, + ); + assert.ok(isIP(browserExitIp)); + await stopProfile(app, base, saved.api_token, profile.id, activeCdp); + activeCdp = null; + await assertProxyWorkerLogsRedacted(app, [httpSettings, socksSettings]); + + await assignNetworkThroughUi(app, profile.name, httpProxy.name, vpn.name); + await app.waitFor( + async () => + (await app.invoke("list_browser_profiles")).find( + (item) => item.id === profile.id, + )?.vpn_id === vpn.id, + { description: "WireGuard assignment persisted" }, + ); + await app.capture("05-proxy-and-vpn-assigned"); + + if (realWireGuardConfig) { + const tunneled = await runProfile( + app, + base, + saved.api_token, + profile.id, + process.env.DONUT_E2E_WIREGUARD_TARGET_URL, + ); + activeCdp = tunneled.cdp; + await app.waitFor(wireGuardTargetWasReached, { + timeoutMs: 30_000, + description: "Wayfern GET through local WireGuard peer", + }); + await stopProfile(app, base, saved.api_token, profile.id, activeCdp); + activeCdp = null; + } + } catch (error) { + await app.capture("failure"); + throw error; + } finally { + activeCdp?.close(); + if (app.session) { + if (apiPort) await app.invoke("stop_api_server").catch(() => {}); + for (const profile of await app + .invoke("list_browser_profiles") + .catch(() => [])) { + if (profile.process_id) { + await app.invoke("kill_browser_profile", { profile }).catch(() => {}); + } + } + if (activeVpnId) { + await app + .invoke("disconnect_vpn", { vpnId: activeVpnId }) + .catch(() => {}); + } + } + await app.close(); + } +}); diff --git a/e2e/tests/smoke.test.mjs b/e2e/tests/smoke.test.mjs index 9a110c2..efcf6eb 100644 --- a/e2e/tests/smoke.test.mjs +++ b/e2e/tests/smoke.test.mjs @@ -5,59 +5,66 @@ import test from "node:test"; import { appFromEnvironment, withApp } from "../lib/app.mjs"; test("fresh app renders, completes onboarding, persists settings, and never touches real app roots", async () => { - await withApp("smoke-fresh", async (app) => { - assert.equal(typeof (await app.session.title()), "string"); - assert.match(await app.bodyText(), /New/); - await app.waitForText("No profiles yet"); + await withApp( + "smoke-fresh", + async (app) => { + assert.equal(typeof (await app.session.title()), "string"); + assert.match(await app.bodyText(), /New/); + await app.waitForText("No profiles yet"); - const initial = await app.invoke("get_app_settings"); - assert.equal(typeof initial.onboarding_completed, "boolean"); - await app.invoke("complete_onboarding"); - assert.equal(await app.invoke("get_onboarding_completed"), true); - await app.invoke("dismiss_window_resize_warning"); - assert.equal(await app.invoke("get_window_resize_warning_dismissed"), true); + const initial = await app.invoke("get_app_settings"); + assert.equal(typeof initial.onboarding_completed, "boolean"); + await app.invoke("complete_onboarding"); + assert.equal(await app.invoke("get_onboarding_completed"), true); + await app.invoke("dismiss_window_resize_warning"); + assert.equal( + await app.invoke("get_window_resize_warning_dismissed"), + true, + ); - const saved = await app.invoke("save_app_settings", { - settings: { - ...initial, - theme: "dark", - language: "en", - onboarding_completed: true, - disable_auto_updates: true, - }, - }); - assert.equal(saved.theme, "dark"); - assert.equal(saved.language, "en"); + const saved = await app.invoke("save_app_settings", { + settings: { + ...initial, + theme: "dark", + language: "en", + onboarding_completed: true, + disable_auto_updates: true, + }, + }); + assert.equal(saved.theme, "dark"); + assert.equal(saved.language, "en"); - await app.invoke("save_table_sorting_settings", { - sorting: { column: "browser", direction: "desc" }, - }); - assert.deepEqual(await app.invoke("get_table_sorting_settings"), { - column: "browser", - direction: "desc", - }); - assert.ok((await app.invoke("get_system_language")).length >= 2); - const system = await app.invoke("get_system_info"); - assert.ok(system && typeof system === "object"); - assert.equal(typeof (await app.invoke("read_log_files")), "string"); + await app.invoke("save_table_sorting_settings", { + sorting: { column: "browser", direction: "desc" }, + }); + assert.deepEqual(await app.invoke("get_table_sorting_settings"), { + column: "browser", + direction: "desc", + }); + assert.ok((await app.invoke("get_system_language")).length >= 2); + const system = await app.invoke("get_system_info"); + assert.ok(system && typeof system === "object"); + assert.equal(typeof (await app.invoke("read_log_files")), "string"); - await app.restart(); - const afterRestart = await app.invoke("get_app_settings"); - assert.equal(afterRestart.theme, "dark"); - assert.equal(afterRestart.language, "en"); - assert.equal(afterRestart.onboarding_completed, true); + await app.restart(); + const afterRestart = await app.invoke("get_app_settings"); + assert.equal(afterRestart.theme, "dark"); + assert.equal(afterRestart.language, "en"); + assert.equal(afterRestart.onboarding_completed, true); - const settingsFile = path.join( - app.dataRoot, - "data", - "settings", - "app_settings.json", - ); - await access(settingsFile); - const persisted = JSON.parse(await readFile(settingsFile, "utf8")); - assert.equal(persisted.api_token, null); - assert.equal(persisted.mcp_token, null); - }); + const settingsFile = path.join( + app.dataRoot, + "data", + "settings", + "app_settings.json", + ); + await access(settingsFile); + const persisted = JSON.parse(await readFile(settingsFile, "utf8")); + assert.equal(persisted.api_token, null); + assert.equal(persisted.mcp_token, null); + }, + { onboardingCompleted: false }, + ); }); test("two isolated sessions run concurrently and do not share frontend or backend state", async () => { @@ -89,15 +96,15 @@ test("two isolated sessions run concurrently and do not share frontend or backen test("keyboard command palette and major navigation surfaces are operable through native WebDriver", async () => { await withApp("smoke-ui", async (app) => { - await app.invoke("complete_onboarding"); const modifier = process.platform === "darwin" ? { meta: true } : { ctrl: true }; - await app.pressShortcut({ key: "k", ...modifier }); await app.waitFor( - () => - app.execute( + async () => { + await app.pressShortcut({ key: "k", ...modifier }); + return app.execute( `return Boolean(document.querySelector("[cmdk-input][placeholder='Type a command or search...']"));`, - ), + ); + }, { description: "open command palette" }, ); diff --git a/e2e/tests/ui.test.mjs b/e2e/tests/ui.test.mjs index b311edd..3eb7b4c 100644 --- a/e2e/tests/ui.test.mjs +++ b/e2e/tests/ui.test.mjs @@ -2,15 +2,236 @@ import assert from "node:assert/strict"; import test from "node:test"; import { withApp } from "../lib/app.mjs"; +const THEME_VARIABLES = [ + "--background", + "--foreground", + "--card", + "--card-foreground", + "--popover", + "--popover-foreground", + "--primary", + "--primary-foreground", + "--secondary", + "--secondary-foreground", + "--muted", + "--muted-foreground", + "--accent", + "--accent-foreground", + "--destructive", + "--destructive-foreground", + "--success", + "--success-foreground", + "--warning", + "--warning-foreground", + "--border", + "--chart-1", + "--chart-2", + "--chart-3", + "--chart-4", + "--chart-5", +]; + +const DRACULA_THEME = { + "--background": "#282a36", + "--foreground": "#f8f8f2", + "--card": "#44475a", + "--card-foreground": "#f8f8f2", + "--popover": "#44475a", + "--popover-foreground": "#f8f8f2", + "--primary": "#bd93f9", + "--primary-foreground": "#282a36", + "--secondary": "#8be9fd", + "--secondary-foreground": "#282a36", + "--muted": "#6272a4", + "--muted-foreground": "#f8f8f2", + "--accent": "#ff79c6", + "--accent-foreground": "#282a36", + "--destructive": "#ff5555", + "--destructive-foreground": "#f8f8f2", + "--success": "#50fa7b", + "--success-foreground": "#282a36", + "--warning": "#ffb86c", + "--warning-foreground": "#282a36", + "--border": "#6272a4", + "--chart-1": "#bd93f9", + "--chart-2": "#50fa7b", + "--chart-3": "#ff79c6", + "--chart-4": "#8be9fd", + "--chart-5": "#ffb86c", +}; + async function dismissSurface(app) { await app.pressShortcut({ key: "Escape" }); await new Promise((resolve) => setTimeout(resolve, 100)); } +async function themeSnapshot(app) { + return app.execute( + ` + const root = document.documentElement; + const rootStyle = getComputedStyle(root); + const bodyStyle = getComputedStyle(document.body); + const variables = arguments[0]; + return { + mode: root.classList.contains("light") + ? "light" + : root.classList.contains("dark") + ? "dark" + : "unset", + inline: Object.fromEntries( + variables.map((key) => [key, root.style.getPropertyValue(key).trim()]) + ), + resolved: Object.fromEntries( + variables.map((key) => [key, rootStyle.getPropertyValue(key).trim()]) + ), + bodyBackground: bodyStyle.backgroundColor, + bodyForeground: bodyStyle.color, + }; + `, + [THEME_VARIABLES], + ); +} + +async function waitForTheme(app, predicate, description) { + return app.waitFor( + async () => { + const snapshot = await themeSnapshot(app); + return predicate(snapshot) ? snapshot : false; + }, + { description }, + ); +} + +function themeVariablesEqual(actual, expected) { + return THEME_VARIABLES.every( + (key) => actual[key]?.toLowerCase() === expected[key]?.toLowerCase(), + ); +} + +async function chooseSelectOption(app, triggerSelector, option) { + await app.clickSelector(triggerSelector); + await app.clickText(option, { roles: ["option"] }); +} + +async function saveSettings(app) { + await app.clickText("Save Settings", { roles: ["button"] }); + await app.waitFor( + () => + app.execute(`return document.querySelector("#theme-select") === null;`), + { description: "Settings to close after saving" }, + ); +} + +async function assertThemeAcrossNavigation(app, expected) { + for (const surface of ["Network", "Extensions", "Profiles"]) { + await app.clickSelector(`[aria-label="${surface}"]`); + await app.waitFor( + async () => + JSON.stringify(await themeSnapshot(app)) === JSON.stringify(expected), + { description: `theme to remain unchanged on ${surface}` }, + ); + } +} + +async function dragBackgroundColorPicker(app) { + await app.clickSelector('[aria-label="Background"]'); + const drag = await app.waitFor( + () => + app.execute(` + const popover = document.querySelector('[data-slot="popover-content"]'); + const selection = [...(popover?.querySelectorAll("div") ?? [])].find( + (node) => node.style.background.includes("linear-gradient") + ); + if (!selection) return null; + const rect = selection.getBoundingClientRect(); + const points = []; + for (const yf of [0.2, 0.4, 0.6, 0.8]) { + for (const xf of [0.2, 0.4, 0.6, 0.8]) { + const point = { + x: Math.round(rect.left + rect.width * xf), + y: Math.round(rect.top + rect.height * yf), + }; + const hit = document.elementFromPoint(point.x, point.y); + if (hit === selection || selection.contains(hit)) points.push(point); + } + } + return points.length >= 2 + ? { start: points[0], end: points[points.length - 1] } + : null; + `), + { description: "two pointer-interactive background color picker points" }, + ); + await app.execute(` + window.__donutE2eThemePointerEvents = []; + for (const type of ["pointermove", "pointerdown", "pointerup"]) { + window.addEventListener(type, (event) => { + window.__donutE2eThemePointerEvents.push({ + type, + x: event.clientX, + y: event.clientY, + buttons: event.buttons, + target: event.target?.className ?? event.target?.tagName ?? "", + }); + }, true); + } + `); + await app.session.command("POST", "/actions", { + actions: [ + { + type: "pointer", + id: "theme-color-pointer", + actions: [ + { + type: "pointerMove", + x: drag.start.x, + y: drag.start.y, + origin: "viewport", + }, + { type: "pointerDown", button: 0 }, + { type: "pause", duration: 150 }, + { + type: "pointerMove", + x: drag.end.x, + y: drag.end.y, + duration: 100, + origin: "viewport", + }, + { type: "pointerUp", button: 0 }, + ], + }, + ], + }); + const pointerEvents = await app.execute( + `return window.__donutE2eThemePointerEvents ?? [];`, + ); + assert.deepEqual( + pointerEvents.map((event) => event.type), + ["pointermove", "pointerdown", "pointermove", "pointerup"], + ); + assert.match(pointerEvents[1].target, /cursor-pointer/); + assert.match(pointerEvents[2].target, /cursor-pointer/); + assert.equal(pointerEvents[2].buttons, 1); + await app.waitFor( + () => + app.execute( + `return document.querySelector("#theme-preset-select")?.textContent?.includes("Your Own") === true;`, + ), + { + description: `customized theme to be marked as Your Own after ${JSON.stringify(pointerEvents)}`, + }, + ); + await app.clickSelector('[aria-label="Background"]'); + await app.waitFor( + () => + app.execute( + `return document.querySelector('[data-slot="popover-content"]') === null;`, + ), + { description: "color picker to close" }, + ); +} + test("all primary navigation buttons and sub-page tabs render and remain interactive", async () => { await withApp("ui-navigation", async (app) => { - await app.invoke("complete_onboarding"); - await app.restart(); const surfaces = [ ["Settings", /General|Appearance|Sync/i], ["Network", /Proxies|VPNs|DNS/i], @@ -55,8 +276,6 @@ test("all primary navigation buttons and sub-page tabs render and remain interac test("settings tabs, command palette filtering, and responsive layout survive resize", async () => { await withApp("ui-settings-responsive", async (app) => { - await app.invoke("complete_onboarding"); - await app.restart(); await app.clickSelector('[aria-label="Settings"]'); await app.waitForText("Appearance"); for (const tab of ["Appearance", "Sync", "Encryption"]) { @@ -115,3 +334,116 @@ test("settings tabs, command palette filtering, and responsive layout survive re ); }); }); + +test("predefined theme remains rendered across navigation and restart", async () => { + await withApp("ui-theme-predefined", async (app) => { + await app.clickSelector('[aria-label="Settings"]'); + await app.waitForText("Appearance"); + await chooseSelectOption(app, "#theme-select", "Light"); + await saveSettings(app); + + const persisted = await app.invoke("get_app_settings"); + assert.equal(persisted.theme, "light"); + const selected = await waitForTheme( + app, + (snapshot) => + snapshot.mode === "light" && + Object.values(snapshot.inline).every((value) => value === ""), + "predefined light theme to render without custom variables", + ); + assert.notEqual(selected.bodyBackground, ""); + assert.notEqual(selected.bodyForeground, ""); + await assertThemeAcrossNavigation(app, selected); + + await app.restart(); + assert.equal((await app.invoke("get_app_settings")).theme, "light"); + await app.waitFor( + async () => + JSON.stringify(await themeSnapshot(app)) === JSON.stringify(selected), + { description: "predefined light theme after restart" }, + ); + }); +}); + +test("preset and manually customized themes survive navigation and restart", async () => { + await withApp("ui-theme-custom", async (app) => { + await app.clickSelector('[aria-label="Settings"]'); + await app.waitForText("Appearance"); + await chooseSelectOption(app, "#theme-select", "Custom"); + await chooseSelectOption(app, "#theme-preset-select", "Dracula"); + await saveSettings(app); + + const presetSettings = await app.invoke("get_app_settings"); + assert.equal(presetSettings.theme, "custom"); + assert.deepEqual(presetSettings.custom_theme, DRACULA_THEME); + const preset = await waitForTheme( + app, + (snapshot) => + snapshot.mode === "dark" && + themeVariablesEqual(snapshot.inline, DRACULA_THEME) && + themeVariablesEqual(snapshot.resolved, DRACULA_THEME), + "Dracula preset variables to render", + ); + await assertThemeAcrossNavigation(app, preset); + + await app.restart(); + assert.deepEqual( + (await app.invoke("get_app_settings")).custom_theme, + DRACULA_THEME, + ); + await app.waitFor( + async () => + JSON.stringify(await themeSnapshot(app)) === JSON.stringify(preset), + { description: "Dracula preset after restart" }, + ); + + await app.clickSelector('[aria-label="Settings"]'); + await app.waitForText("Appearance"); + assert.equal( + await app.execute( + `return document.querySelector("#theme-select")?.textContent?.trim();`, + ), + "Custom", + ); + assert.equal( + await app.execute( + `return document.querySelector("#theme-preset-select")?.textContent?.trim();`, + ), + "Dracula", + ); + await dragBackgroundColorPicker(app); + await saveSettings(app); + + const customizedSettings = await app.invoke("get_app_settings"); + assert.equal(customizedSettings.theme, "custom"); + assert.notEqual( + customizedSettings.custom_theme["--background"].toLowerCase(), + DRACULA_THEME["--background"], + ); + assert.deepEqual( + Object.keys(customizedSettings.custom_theme).sort(), + [...THEME_VARIABLES].sort(), + ); + const customized = await waitForTheme( + app, + (snapshot) => + snapshot.mode === "dark" && + themeVariablesEqual(snapshot.inline, customizedSettings.custom_theme) && + themeVariablesEqual(snapshot.resolved, customizedSettings.custom_theme), + "manually customized variables to render", + ); + assert.notEqual(customized.bodyBackground, preset.bodyBackground); + await assertThemeAcrossNavigation(app, customized); + + await app.restart(); + assert.deepEqual( + (await app.invoke("get_app_settings")).custom_theme, + customizedSettings.custom_theme, + ); + await app.waitFor( + async () => + JSON.stringify(await themeSnapshot(app)) === JSON.stringify(customized), + { description: "manually customized theme after restart" }, + ); + }); +}); diff --git a/package.json b/package.json index da62f0f..6dede1b 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "e2e:smoke": "node e2e/run.mjs --suite=smoke", "e2e:ui": "node e2e/run.mjs --suite=ui", "e2e:entities": "node e2e/run.mjs --suite=entities", + "e2e:network": "node e2e/run.mjs --suite=network", "e2e:integrations": "node e2e/run.mjs --suite=integrations", "e2e:sync": "node e2e/run.mjs --suite=sync", "e2e:browser": "node e2e/run.mjs --suite=browser", @@ -34,7 +35,7 @@ "unused-exports:js": "ts-unused-exports tsconfig.json", "check-unused-commands": "cd src-tauri && cargo test test_no_unused_tauri_commands", "copy-proxy-binary": "node src-tauri/copy-proxy-binary.mjs", - "prebuild": "pnpm copy-proxy-binary", + "copy-proxy-binary:release": "node src-tauri/copy-proxy-binary.mjs --release", "pretauri:dev": "pnpm copy-proxy-binary", "precargo": "pnpm copy-proxy-binary" }, diff --git a/scripts/redact-sensitive-text.mjs b/scripts/redact-sensitive-text.mjs new file mode 100644 index 0000000..dd9e027 --- /dev/null +++ b/scripts/redact-sensitive-text.mjs @@ -0,0 +1,117 @@ +import { Buffer } from "node:buffer"; +import process from "node:process"; +import { pathToFileURL } from "node:url"; + +const URL_PATTERN = /\b[a-z][a-z\d+.-]{1,20}:\/\/[^\s<>"'`]+/giu; +const PRIVATE_KEY_PATTERN = + /-----BEGIN [^-\r\n]*PRIVATE KEY-----[\s\S]*?-----END [^-\r\n]*PRIVATE KEY-----/giu; +const BEARER_PATTERN = /\bBearer\s+[A-Za-z\d._~+/=-]+/giu; +const SECRET_ASSIGNMENT_PATTERN = + /\b(?:api[_-]?key|authorization|password|passwd|private[_-]?key|proxy[_-]?(?:password|username)|refresh[_-]?token|secret|token|username)\b\s*[:=]\s*[^\s,;]+/giu; +const JWT_PATTERN = /\beyJ[A-Za-z\d_-]+\.[A-Za-z\d_-]+\.[A-Za-z\d_-]+\b/gu; +const TOKEN_PATTERN = + /\b(?:gh[oprsu]_[A-Za-z\d]{20,}|github_pat_[A-Za-z\d_]{20,}|sk-[A-Za-z\d_-]{20,}|xox[baprs]-[A-Za-z\d-]{20,})\b/gu; +const EMAIL_PATTERN = /\b[A-Z\d._%+-]+@[A-Z\d.-]+\.[A-Z]{2,}\b/giu; +const UNIX_HOME_PATTERN = /\/(?:Users|home)\/[^/\s]+/gu; +const WINDOWS_HOME_PATTERN = /\b[A-Z]:\\Users\\[^\\\s]+/giu; +const IPV4_PATTERN = + /\b(?:25[0-5]|2[0-4]\d|1?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|1?\d?\d)){3}\b/gu; +const DOMAIN_PATTERN = /\b(?:[a-z\d-]+\.)+[a-z]{2,}\b/giu; +const UUID_PATTERN = + /\b[\da-f]{8}-[\da-f]{4}-[1-8][\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}\b/giu; + +function safeUrlLabel(value) { + try { + const parsed = new URL(value); + return `${parsed.protocol}//`; + } catch { + return ""; + } +} + +export function sensitiveVariants(values) { + const variants = new Set(); + for (const rawValue of values ?? []) { + const value = String(rawValue ?? "").trim(); + if (value.length < 4) continue; + variants.add(value); + variants.add(encodeURIComponent(value)); + variants.add(Buffer.from(value).toString("base64")); + try { + const parsed = new URL(value); + for (const component of [ + parsed.username, + parsed.password, + parsed.hostname, + parsed.host, + ]) { + if (component.length >= 4) { + variants.add(component); + variants.add(decodeURIComponent(component)); + variants.add(encodeURIComponent(decodeURIComponent(component))); + } + } + } catch { + // Non-URL secrets are already covered by their literal and encoded forms. + } + } + return [...variants].sort((left, right) => right.length - left.length); +} + +export function redactSensitiveText(text, { sensitiveValues = [] } = {}) { + let redacted = String(text ?? ""); + for (const value of sensitiveVariants(sensitiveValues)) { + redacted = redacted.split(value).join(""); + } + return redacted + .replace(PRIVATE_KEY_PATTERN, "") + .replace(URL_PATTERN, safeUrlLabel) + .replace(BEARER_PATTERN, "Bearer ") + .replace(SECRET_ASSIGNMENT_PATTERN, "") + .replace(JWT_PATTERN, "") + .replace(TOKEN_PATTERN, "") + .replace(EMAIL_PATTERN, "") + .replace(UNIX_HOME_PATTERN, "/") + .replace(WINDOWS_HOME_PATTERN, "") + .replace(IPV4_PATTERN, "") + .replace(DOMAIN_PATTERN, "") + .replace(UUID_PATTERN, ""); +} + +export function redactIssueBody(text) { + const sections = String(text ?? "").split(/^###\s+/mu); + const preamble = redactSensitiveText(sections.shift() ?? "").trim(); + const safeSections = sections.map((section) => { + const newline = section.indexOf("\n"); + if (newline < 0) return redactSensitiveText(section); + const heading = section.slice(0, newline).trim(); + const value = section.slice(newline + 1).trim(); + const safeValue = /^(?:error logs or screenshots|logs|screenshots)$/iu.test( + heading, + ) + ? "[omitted from automated processing]" + : redactSensitiveText(value); + return `${heading}\n${safeValue}`; + }); + return [preamble, ...safeSections.map((section) => `### ${section}`)] + .filter(Boolean) + .join("\n\n"); +} + +async function runCli() { + let input = ""; + process.stdin.setEncoding("utf8"); + for await (const chunk of process.stdin) input += chunk; + process.stdout.write( + process.argv.includes("--issue-body") + ? redactIssueBody(input) + : redactSensitiveText(input), + ); +} + +if ( + process.argv[1] && + import.meta.url === pathToFileURL(process.argv[1]).href +) { + await runCli(); +} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 3945336..be2f36e 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -442,7 +442,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" dependencies = [ "atk-sys", - "glib 0.18.5", + "glib", "libc", ] @@ -452,10 +452,10 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" dependencies = [ - "glib-sys 0.18.1", - "gobject-sys 0.18.0", + "glib-sys", + "gobject-sys", "libc", - "system-deps 6.2.2", + "system-deps", ] [[package]] @@ -912,7 +912,7 @@ checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ "bitflags 2.13.0", "cairo-sys-rs", - "glib 0.18.5", + "glib", "libc", "once_cell", "thiserror 1.0.69", @@ -924,9 +924,9 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" dependencies = [ - "glib-sys 0.18.1", + "glib-sys", "libc", - "system-deps 6.2.2", + "system-deps", ] [[package]] @@ -1027,17 +1027,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" dependencies = [ "smallvec", - "target-lexicon 0.12.16", -] - -[[package]] -name = "cfg-expr" -version = "0.20.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c" -dependencies = [ - "smallvec", - "target-lexicon 0.13.5", + "target-lexicon", ] [[package]] @@ -1846,7 +1836,7 @@ dependencies = [ "objc2", "objc2-app-kit", "once_cell", - "quick-xml 0.41.0", + "quick-xml", "rand 0.10.2", "regex-lite", "reqwest", @@ -1867,7 +1857,6 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-clipboard-manager", - "tauri-plugin-cross-platform-webdriver", "tauri-plugin-deep-link", "tauri-plugin-dialog", "tauri-plugin-fs", @@ -2481,7 +2470,7 @@ dependencies = [ "gdk-pixbuf", "gdk-sys", "gio", - "glib 0.18.5", + "glib", "libc", "pango", ] @@ -2494,7 +2483,7 @@ checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" dependencies = [ "gdk-pixbuf-sys", "gio", - "glib 0.18.5", + "glib", "libc", "once_cell", ] @@ -2505,11 +2494,11 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" dependencies = [ - "gio-sys 0.18.1", - "glib-sys 0.18.1", - "gobject-sys 0.18.0", + "gio-sys", + "glib-sys", + "gobject-sys", "libc", - "system-deps 6.2.2", + "system-deps", ] [[package]] @@ -2520,13 +2509,13 @@ checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", - "gio-sys 0.18.1", - "glib-sys 0.18.1", - "gobject-sys 0.18.0", + "gio-sys", + "glib-sys", + "gobject-sys", "libc", "pango-sys", "pkg-config", - "system-deps 6.2.2", + "system-deps", ] [[package]] @@ -2536,11 +2525,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" dependencies = [ "gdk-sys", - "glib-sys 0.18.1", - "gobject-sys 0.18.0", + "glib-sys", + "gobject-sys", "libc", "pkg-config", - "system-deps 6.2.2", + "system-deps", ] [[package]] @@ -2552,7 +2541,7 @@ dependencies = [ "gdk", "gdkx11-sys", "gio", - "glib 0.18.5", + "glib", "libc", "x11", ] @@ -2564,9 +2553,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" dependencies = [ "gdk-sys", - "glib-sys 0.18.1", + "glib-sys", "libc", - "system-deps 6.2.2", + "system-deps", "x11", ] @@ -2664,8 +2653,8 @@ dependencies = [ "futures-core", "futures-io", "futures-util", - "gio-sys 0.18.1", - "glib 0.18.5", + "gio-sys", + "glib", "libc", "once_cell", "pin-project-lite", @@ -2679,26 +2668,13 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" dependencies = [ - "glib-sys 0.18.1", - "gobject-sys 0.18.0", + "glib-sys", + "gobject-sys", "libc", - "system-deps 6.2.2", + "system-deps", "winapi", ] -[[package]] -name = "gio-sys" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0071fe88dba8e40086c8ff9bbb62622999f49628344b1d1bf490a48a29d80f22" -dependencies = [ - "glib-sys 0.21.5", - "gobject-sys 0.21.5", - "libc", - "system-deps 7.0.8", - "windows-sys 0.61.2", -] - [[package]] name = "glib" version = "0.18.5" @@ -2711,10 +2687,10 @@ dependencies = [ "futures-executor", "futures-task", "futures-util", - "gio-sys 0.18.1", - "glib-macros 0.18.5", - "glib-sys 0.18.1", - "gobject-sys 0.18.0", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", "libc", "memchr", "once_cell", @@ -2722,27 +2698,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "glib" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b" -dependencies = [ - "bitflags 2.13.0", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys 0.21.5", - "glib-macros 0.21.5", - "glib-sys 0.21.5", - "gobject-sys 0.21.5", - "libc", - "memchr", - "smallvec", -] - [[package]] name = "glib-macros" version = "0.18.5" @@ -2757,19 +2712,6 @@ dependencies = [ "syn 2.0.118", ] -[[package]] -name = "glib-macros" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf59b675301228a696fe01c3073974643365080a76cc3ed5bc2cbc466ad87f17" -dependencies = [ - "heck 0.5.0", - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.118", -] - [[package]] name = "glib-sys" version = "0.18.1" @@ -2777,17 +2719,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" dependencies = [ "libc", - "system-deps 6.2.2", -] - -[[package]] -name = "glib-sys" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d95e1a3a19ae464a7286e14af9a90683c64d70c02532d88d87ce95056af3e6c" -dependencies = [ - "libc", - "system-deps 7.0.8", + "system-deps", ] [[package]] @@ -2815,20 +2747,9 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" dependencies = [ - "glib-sys 0.18.1", + "glib-sys", "libc", - "system-deps 6.2.2", -] - -[[package]] -name = "gobject-sys" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dca35da0d19a18f4575f3cb99fe1c9e029a2941af5662f326f738a21edaf294" -dependencies = [ - "glib-sys 0.21.5", - "libc", - "system-deps 7.0.8", + "system-deps", ] [[package]] @@ -2844,7 +2765,7 @@ dependencies = [ "gdk", "gdk-pixbuf", "gio", - "glib 0.18.5", + "glib", "gtk-sys", "gtk3-macros", "libc", @@ -2862,12 +2783,12 @@ dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", "gdk-sys", - "gio-sys 0.18.1", - "glib-sys 0.18.1", - "gobject-sys 0.18.0", + "gio-sys", + "glib-sys", + "gobject-sys", "libc", "pango-sys", - "system-deps 6.2.2", + "system-deps", ] [[package]] @@ -3506,7 +3427,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" dependencies = [ "bitflags 1.3.2", - "glib 0.18.5", + "glib", "javascriptcore-rs-sys", ] @@ -3516,10 +3437,10 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" dependencies = [ - "glib-sys 0.18.1", - "gobject-sys 0.18.0", + "glib-sys", + "gobject-sys", "libc", - "system-deps 6.2.2", + "system-deps", ] [[package]] @@ -3675,7 +3596,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" dependencies = [ - "glib 0.18.5", + "glib", "gtk", "gtk-sys", "libappindicator-sys", @@ -4357,16 +4278,6 @@ dependencies = [ "objc2-core-foundation", ] -[[package]] -name = "objc2-javascript-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" -dependencies = [ - "objc2", - "objc2-core-foundation", -] - [[package]] name = "objc2-open-directory" version = "0.3.2" @@ -4390,17 +4301,6 @@ dependencies = [ "objc2-foundation", ] -[[package]] -name = "objc2-security" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" -dependencies = [ - "bitflags 2.13.0", - "objc2", - "objc2-core-foundation", -] - [[package]] name = "objc2-ui-kit" version = "0.3.2" @@ -4444,8 +4344,6 @@ dependencies = [ "objc2-app-kit", "objc2-core-foundation", "objc2-foundation", - "objc2-javascript-core", - "objc2-security", ] [[package]] @@ -4563,7 +4461,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" dependencies = [ "gio", - "glib 0.18.5", + "glib", "libc", "once_cell", "pango-sys", @@ -4575,10 +4473,10 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" dependencies = [ - "glib-sys 0.18.1", - "gobject-sys 0.18.0", + "glib-sys", + "gobject-sys", "libc", - "system-deps 6.2.2", + "system-deps", ] [[package]] @@ -4788,7 +4686,7 @@ checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" dependencies = [ "base64 0.22.1", "indexmap 2.14.0", - "quick-xml 0.41.0", + "quick-xml", "serde", "time", ] @@ -5043,15 +4941,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" -[[package]] -name = "quick-xml" -version = "0.39.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" -dependencies = [ - "memchr", -] - [[package]] name = "quick-xml" version = "0.41.0" @@ -5392,8 +5281,8 @@ checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" dependencies = [ "block2", "dispatch2", - "glib-sys 0.18.1", - "gobject-sys 0.18.0", + "glib-sys", + "gobject-sys", "gtk-sys", "js-sys", "log", @@ -6251,7 +6140,7 @@ checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" dependencies = [ "futures-channel", "gio", - "glib 0.18.5", + "glib", "libc", "soup3-sys", ] @@ -6262,11 +6151,11 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" dependencies = [ - "gio-sys 0.18.1", - "glib-sys 0.18.1", - "gobject-sys 0.18.0", + "gio-sys", + "glib-sys", + "gobject-sys", "libc", - "system-deps 6.2.2", + "system-deps", ] [[package]] @@ -6465,26 +6354,13 @@ version = "6.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" dependencies = [ - "cfg-expr 0.15.8", + "cfg-expr", "heck 0.5.0", "pkg-config", "toml 0.8.2", "version-compare", ] -[[package]] -name = "system-deps" -version = "7.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" -dependencies = [ - "cfg-expr 0.20.8", - "heck 0.5.0", - "pkg-config", - "toml 1.1.2+spec-1.1.0", - "version-compare", -] - [[package]] name = "tao" version = "0.35.3" @@ -6559,12 +6435,6 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" -[[package]] -name = "target-lexicon" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" - [[package]] name = "tauri" version = "2.11.5" @@ -6710,36 +6580,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "tauri-plugin-cross-platform-webdriver" -version = "0.1.0" -dependencies = [ - "async-trait", - "axum", - "base64 0.22.1", - "block2", - "cairo-rs", - "glib 0.21.5", - "gtk", - "javascriptcore-rs", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "objc2-web-kit", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "tempfile", - "tokio", - "tracing", - "uuid", - "webkit2gtk", - "webview2-com", - "windows 0.61.3", - "windows-core 0.61.2", -] - [[package]] name = "tauri-plugin-deep-link" version = "2.4.9" @@ -8065,11 +7905,10 @@ dependencies = [ [[package]] name = "wayland-scanner" version = "0.31.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +source = "git+https://github.com/Smithay/wayland-rs?rev=d07c4f91f28b42e5a485823ffd9d8d5a210b1053#d07c4f91f28b42e5a485823ffd9d8d5a210b1053" dependencies = [ "proc-macro2", - "quick-xml 0.39.4", + "quick-xml", "quote", ] @@ -8115,10 +7954,10 @@ dependencies = [ "gdk", "gdk-sys", "gio", - "gio-sys 0.18.1", - "glib 0.18.5", - "glib-sys 0.18.1", - "gobject-sys 0.18.0", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", "gtk", "gtk-sys", "javascriptcore-rs", @@ -8137,15 +7976,15 @@ dependencies = [ "bitflags 1.3.2", "cairo-sys-rs", "gdk-sys", - "gio-sys 0.18.1", - "glib-sys 0.18.1", - "gobject-sys 0.18.0", + "gio-sys", + "glib-sys", + "gobject-sys", "gtk-sys", "javascriptcore-rs-sys", "libc", "pkg-config", "soup3-sys", - "system-deps 6.2.2", + "system-deps", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 27729db..d92bf4d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -31,7 +31,7 @@ resvg = "0.47" [dependencies] serde_json = "1" serde = { version = "1", features = ["derive"] } -tauri = { version = "2", features = ["devtools", "test", "tray-icon", "image-png"] } +tauri = { version = "2", features = ["tray-icon", "image-png"] } tauri-plugin-opener = "2" tauri-plugin-fs = "2" tauri-plugin-shell = "2" @@ -42,7 +42,6 @@ tauri-plugin-macos-permissions = "2" tauri-plugin-log = "2" tauri-plugin-clipboard-manager = "2" tauri-plugin-window-state = "2" -tauri-plugin-cross-platform-webdriver = { path = "../../tauri-cross-platform-webdriver/crates/tauri-plugin-cross-platform-webdriver", optional = true } log = "0.4" env_logger = "0.11" @@ -187,4 +186,10 @@ default = ["custom-protocol"] # this feature is used used for production builds where `devPath` points to the filesystem # DO NOT remove this custom-protocol = ["tauri/custom-protocol"] -e2e = ["dep:tauri-plugin-cross-platform-webdriver"] +e2e = [] + +# wayland-scanner 0.31.10 still pins vulnerable quick-xml 0.39. Upstream fixed +# RUSTSEC-2026-0194 and RUSTSEC-2026-0195, but has not published the fix yet. +# Remove this patch after the next wayland-scanner release is in the lockfile. +[patch.crates-io] +wayland-scanner = { git = "https://github.com/Smithay/wayland-rs", rev = "d07c4f91f28b42e5a485823ffd9d8d5a210b1053" } diff --git a/src-tauri/Info.plist b/src-tauri/Info.plist index 6e7bbca..5c154cc 100644 --- a/src-tauri/Info.plist +++ b/src-tauri/Info.plist @@ -8,26 +8,8 @@ Donut needs microphone access to enable microphone functionality in web browsers. Each website will still ask for your permission individually. NSLocalNetworkUsageDescription Donut has proxy functionality that requires local network access. You can deny this functionality if you don't plan on setting proxies for browser profiles. - CFBundleDisplayName - Donut - CFBundleName - Donut - CFBundleIdentifier - com.donutbrowser - CFBundlePackageType - APPL - CFBundleURLName - com.donutbrowser - CFBundleExecutable - donutbrowser - CFBundleVersion - 1 - CFBundleIconFile - icon.icns - LSApplicationCategoryType - public.app-category.productivity NSHumanReadableCopyright - Copyright © 2025 Donut + Copyright © 2026 Donut CFBundleDocumentTypes @@ -57,4 +39,4 @@ - \ No newline at end of file + diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index a8977b7..08515a8 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -5,25 +5,24 @@ "windows": ["main"], "webviews": ["main"], "permissions": [ - "core:default", "core:event:allow-listen", "core:event:allow-emit", "core:event:allow-emit-to", "core:event:allow-unlisten", - "core:image:default", - "core:menu:default", - "core:path:default", - "core:tray:default", - "core:webview:default", - "core:window:default", "core:window:allow-start-dragging", "core:window:allow-close", + "core:window:allow-is-maximized", "core:window:allow-minimize", "core:window:allow-toggle-maximize", - "opener:default", { "identifier": "opener:allow-open-url", "allow": [ + { + "url": "https://*" + }, + { + "url": "http://*" + }, { "url": "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone" }, @@ -32,28 +31,16 @@ } ] }, - "fs:default", - "shell:allow-execute", - "shell:allow-kill", - "shell:allow-open", - "shell:allow-spawn", - "shell:allow-stdin-write", - "deep-link:default", - "deep-link:allow-register", - "deep-link:allow-unregister", - "deep-link:allow-is-registered", + "fs:allow-read-text-file", + "fs:allow-write-text-file", "deep-link:allow-get-current", - "dialog:default", "dialog:allow-open", "dialog:allow-save", - "fs:allow-write-text-file", - "macos-permissions:default", "macos-permissions:allow-request-microphone-permission", "macos-permissions:allow-request-camera-permission", "macos-permissions:allow-check-microphone-permission", "macos-permissions:allow-check-camera-permission", "log:default", - "clipboard-manager:default", "clipboard-manager:allow-write-text" ] } diff --git a/src-tauri/copy-proxy-binary.mjs b/src-tauri/copy-proxy-binary.mjs index 77f8049..6f07f9c 100644 --- a/src-tauri/copy-proxy-binary.mjs +++ b/src-tauri/copy-proxy-binary.mjs @@ -4,7 +4,10 @@ import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; const MANIFEST_DIR = dirname(fileURLToPath(import.meta.url)); -const PROFILE = process.env.PROFILE || "debug"; +const PROFILE = + process.argv.includes("--release") || process.env.PROFILE === "release" + ? "release" + : "debug"; function getTarget() { if (process.env.TARGET) return process.env.TARGET; @@ -48,32 +51,22 @@ function copyBinary(baseName) { if (isWindows) destName += ".exe"; const dest = join(destDir, destName); - if (existsSync(source)) { - copyFileSync(source, dest); - console.log(`Copied ${binName} to ${dest}`); - } else { - console.log(`Warning: Binary not found at ${source}`); - console.log(`Building ${baseName} binary...`); - - const buildArgs = ["build", "--bin", baseName]; - if (PROFILE === "release") buildArgs.push("--release"); - if (TARGET !== "unknown" && TARGET !== HOST_TARGET) { - buildArgs.push("--target", TARGET); - } - - execFileSync("cargo", buildArgs, { - cwd: MANIFEST_DIR, - stdio: "inherit", - }); - - if (existsSync(source)) { - copyFileSync(source, dest); - console.log(`Built and copied ${binName} to ${dest}`); - } else { - console.error(`Error: Failed to build ${baseName} binary`); - process.exit(1); - } + const buildArgs = ["build", "--bin", baseName]; + if (PROFILE === "release") buildArgs.push("--release"); + if (TARGET !== "unknown" && TARGET !== HOST_TARGET) { + buildArgs.push("--target", TARGET); } + execFileSync("cargo", buildArgs, { + cwd: MANIFEST_DIR, + stdio: "inherit", + }); + + if (!existsSync(source)) { + console.error(`Error: Failed to build ${baseName} binary`); + process.exit(1); + } + copyFileSync(source, dest); + console.log(`Built and copied ${binName} to ${dest}`); } copyBinary("donut-proxy"); diff --git a/src-tauri/entitlements.plist b/src-tauri/entitlements.plist index 1695d18..afd3190 100644 --- a/src-tauri/entitlements.plist +++ b/src-tauri/entitlements.plist @@ -2,37 +2,9 @@ - com.apple.security.app-sandbox - - com.apple.security.network.client - - com.apple.security.network.server - - com.apple.security.files.user-selected.read-write - - com.apple.security.files.downloads.read-write - com.apple.security.device.camera - com.apple.security.device.audio-output - - com.apple.security.device.microphone - com.apple.security.device.audio-input - com.apple.security.cs.allow-jit - - com.apple.security.cs.allow-unsigned-executable-memory - - com.apple.security.cs.allow-dyld-environment-variables - - com.apple.security.cs.disable-library-validation - - com.apple.security.automation.apple-events - - com.apple.security.device.usb - - com.apple.security.inherit - - \ No newline at end of file + diff --git a/src-tauri/src/app_auto_updater.rs b/src-tauri/src/app_auto_updater.rs index a92fc54..e417ba0 100644 --- a/src-tauri/src/app_auto_updater.rs +++ b/src-tauri/src/app_auto_updater.rs @@ -967,7 +967,7 @@ impl AppAutoUpdater { // rejected before the multi-hundred-MB download, not after. let expected_sha256 = self.fetch_expected_checksum(update_info, &filename).await?; - log::info!("Downloading update from: {}", update_info.download_url); + log::info!("Downloading update"); let download_path = self .download_update_silent(&update_info.download_url, &temp_dir, &filename) @@ -2038,7 +2038,7 @@ rm "{}" #[tauri::command] pub async fn check_for_app_updates() -> Result, String> { #[cfg(feature = "e2e")] - if tauri_plugin_cross_platform_webdriver::automation_enabled() + if crate::e2e_automation_enabled() && std::env::var_os("DONUT_E2E_DISABLE_STARTUP_NETWORK").is_some() { log::info!("E2E: skipping automatic app update check"); @@ -2099,7 +2099,7 @@ pub async fn restart_application() -> Result<(), String> { #[tauri::command] pub async fn check_for_app_updates_manual() -> Result, String> { #[cfg(feature = "e2e")] - if tauri_plugin_cross_platform_webdriver::automation_enabled() + if crate::e2e_automation_enabled() && std::env::var_os("DONUT_E2E_DISABLE_STARTUP_NETWORK").is_some() { log::info!("E2E: skipping manual app update check"); diff --git a/src-tauri/src/app_dirs.rs b/src-tauri/src/app_dirs.rs index 91e0621..30f8a91 100644 --- a/src-tauri/src/app_dirs.rs +++ b/src-tauri/src/app_dirs.rs @@ -209,6 +209,30 @@ pub fn restrict_to_owner(path: &std::path::Path) { } } +/// Write sensitive data without creating a wider-permission file first. +pub fn create_owner_only(path: &std::path::Path) -> std::io::Result { + if path.exists() { + restrict_to_owner(path); + } + let mut options = std::fs::OpenOptions::new(); + options.create(true).truncate(true).write(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + let file = options.open(path)?; + restrict_to_owner(path); + Ok(file) +} + +pub fn write_owner_only(path: &std::path::Path, content: &[u8]) -> std::io::Result<()> { + use std::io::Write; + let mut file = create_owner_only(path)?; + file.write_all(content)?; + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -222,6 +246,19 @@ mod tests { ); } + #[cfg(unix)] + #[test] + fn owner_only_writer_uses_private_permissions() { + use std::os::unix::fs::PermissionsExt; + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("secret.json"); + write_owner_only(&path, b"secret").unwrap(); + assert_eq!( + std::fs::metadata(path).unwrap().permissions().mode() & 0o777, + 0o600 + ); + } + #[test] fn test_data_dir_returns_path() { let dir = data_dir(); diff --git a/src-tauri/src/bin/proxy_server.rs b/src-tauri/src/bin/proxy_server.rs index b7649f6..25f1743 100644 --- a/src-tauri/src/bin/proxy_server.rs +++ b/src-tauri/src/bin/proxy_server.rs @@ -2,8 +2,8 @@ use clap::{Arg, Command}; use donutbrowser_lib::proxy_runner::{ start_proxy_process_with_profile, stop_all_proxy_processes, stop_proxy_process, }; -use donutbrowser_lib::proxy_server::run_proxy_server; -use donutbrowser_lib::proxy_storage::get_proxy_config; +use donutbrowser_lib::proxy_server::{redacted_upstream, run_proxy_server}; +use donutbrowser_lib::proxy_storage::{build_proxy_url, get_proxy_config}; use std::process; fn set_high_priority() { @@ -55,31 +55,6 @@ fn set_high_priority() { } } -fn build_proxy_url( - proxy_type: &str, - host: &str, - port: u16, - username: Option<&str>, - password: Option<&str>, -) -> String { - let mut url = format!("{}://", proxy_type.to_lowercase()); - - if let (Some(user), Some(pass)) = (username, password) { - let encoded_user = urlencoding::encode(user); - let encoded_pass = urlencoding::encode(pass); - url.push_str(&format!("{}:{}@", encoded_user, encoded_pass)); - } else if let Some(user) = username { - let encoded_user = urlencoding::encode(user); - url.push_str(&format!("{}@", encoded_user)); - } - - url.push_str(host); - url.push(':'); - url.push_str(&port.to_string()); - - url -} - #[tokio::main(flavor = "multi_thread")] async fn main() { // Initialize logger to write to stderr (which will be redirected to file). @@ -129,8 +104,6 @@ async fn main() { .long("type") .help("Proxy type (http, https, socks4, socks5, ss)"), ) - .arg(Arg::new("username").long("username").help("Proxy username")) - .arg(Arg::new("password").long("password").help("Proxy password")) .arg( Arg::new("port") .short('p') @@ -243,16 +216,22 @@ async fn main() { start_matches.get_one::("proxy-port"), start_matches.get_one::("type"), ) { - let username = start_matches.get_one::("username"); - let password = start_matches.get_one::("password"); + let username = std::env::var("DONUT_PROXY_USERNAME").ok(); + let password = std::env::var("DONUT_PROXY_PASSWORD").ok(); upstream_url = Some(build_proxy_url( proxy_type, host, *port, - username.map(|s| s.as_str()), - password.map(|s| s.as_str()), + username.as_deref(), + password.as_deref(), )); } else if let Some(upstream) = start_matches.get_one::("upstream") { + if url::Url::parse(upstream) + .is_ok_and(|parsed| !parsed.username().is_empty() || parsed.password().is_some()) + { + eprintln!("Credentialed upstream URLs are not accepted as process arguments"); + process::exit(2); + } upstream_url = Some(upstream.clone()); } @@ -286,7 +265,7 @@ async fn main() { "id": config.id, "localPort": config.local_port, "localUrl": config.local_url, - "upstreamUrl": config.upstream_url, + "upstreamUrl": redacted_upstream(&config.upstream_url), }) ); process::exit(0); @@ -380,7 +359,7 @@ async fn main() { "Found config: id={}, port={:?}, upstream={}", config.id, config.local_port, - config.upstream_url + redacted_upstream(&config.upstream_url) ); break config; } diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index c069228..94f0303 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -115,10 +115,9 @@ impl BrowserRunner { } let url = parsed.to_string(); - let profile_name = profile.name.clone(); - let profile_id = profile.id.to_string(); + let url_label = crate::log_redaction::url_label(&url); - log::info!("Firing launch hook GET {url} for profile {profile_name} (ID: {profile_id})"); + log::info!("Firing launch hook GET {url_label}"); tokio::spawn(async move { let client = match reqwest::Client::builder() @@ -127,20 +126,23 @@ impl BrowserRunner { { Ok(c) => c, Err(e) => { - log::warn!("Launch hook client build failed for {url}: {e}"); + log::warn!( + "Launch hook client build failed: {}", + crate::log_redaction::text(&e.to_string()) + ); return; } }; match client.get(&url).send().await { Ok(resp) => { - log::info!( - "Launch hook {url} for profile {profile_name} returned status {}", - resp.status() - ); + log::info!("Launch hook {url_label} returned status {}", resp.status()); } Err(e) => { - log::warn!("Launch hook {url} for profile {profile_name} failed: {e}"); + log::warn!( + "Launch hook {url_label} failed: {}", + crate::log_redaction::text(&e.to_string()) + ); } } }); @@ -675,18 +677,18 @@ impl BrowserRunner { .unwrap_or_else(|| updated_profile.clone()); log::info!( - "Browser status check - Profile: {} (ID: {}), Running: {}, URL: {:?}, PID: {:?}", - final_profile.name, - final_profile.id, - is_running, - url, - final_profile.process_id + "Browser status check: running={is_running}, URL requested={}, PID present={}", + url.is_some(), + final_profile.process_id.is_some() ); if is_running && url.is_some() { // Browser is running and we have a URL to open if let Some(url_ref) = url.as_ref() { - log::info!("Opening URL in existing browser: {url_ref}"); + log::info!( + "Opening {} in existing browser", + crate::log_redaction::url_label(url_ref) + ); match self .open_url_in_existing_browser( @@ -702,7 +704,10 @@ impl BrowserRunner { Ok(final_profile) } Err(e) => { - log::info!("Failed to open URL in existing browser: {e}"); + log::info!( + "Failed to open URL in existing browser: {}", + crate::log_redaction::text(&e.to_string()) + ); // Fall back to launching a new instance log::info!( @@ -1163,18 +1168,21 @@ impl BrowserRunner { )); } - log::info!("Opening URL '{url}' with profile '{profile_id}'"); + log::info!("Opening URL with selected profile"); // Use launch_or_open_url which handles both launching new instances and opening in existing ones self .launch_or_open_url(app_handle, &profile, Some(url.clone()), None) .await .map_err(|e| { - log::info!("Failed to open URL with profile '{profile_id}': {e}"); + log::info!( + "Failed to open URL with selected profile: {}", + crate::log_redaction::text(&e.to_string()) + ); format!("Failed to open URL with profile: {e}") })?; - log::info!("Successfully opened URL '{url}' with profile '{profile_id}'"); + log::info!("Successfully opened URL with selected profile"); Ok(()) } } diff --git a/src-tauri/src/cloud_auth.rs b/src-tauri/src/cloud_auth.rs index 8969d40..af28734 100644 --- a/src-tauri/src/cloud_auth.rs +++ b/src-tauri/src/cloud_auth.rs @@ -609,9 +609,8 @@ impl CloudAuthManager { if !response.status().is_success() { let status = response.status(); - let body = response.text().await.unwrap_or_default(); - log::warn!("Token refresh failed ({status}): {body}"); - return Err(format!("Token refresh failed ({status}): {body}")); + log::warn!("Token refresh failed ({status})"); + return Err(format!("Token refresh failed ({status})")); } let result: RefreshTokenResponse = response @@ -923,14 +922,12 @@ impl CloudAuthManager { let status = response.status(); if status == reqwest::StatusCode::FORBIDDEN { - let body = response.text().await.unwrap_or_default(); - log::warn!("Proxy config returned 403: {body}"); + log::warn!("Proxy config returned 403"); return Err("__403__".to_string()); } if !response.status().is_success() { - let body = response.text().await.unwrap_or_default(); - return Err(format!("Proxy config fetch failed ({status}): {body}")); + return Err(format!("Proxy config fetch failed ({status})")); } response @@ -1192,8 +1189,7 @@ impl CloudAuthManager { if !response.status().is_success() { let status = response.status(); - let body = response.text().await.unwrap_or_default(); - return Err(format!("Wayfern token request failed ({status}): {body}")); + return Err(format!("Wayfern token request failed ({status})")); } let result: WayfernTokenResponse = response @@ -1209,17 +1205,10 @@ impl CloudAuthManager { let token = match result { Ok(token) => token, Err(e) => { - // The backend returns 403 (ForbiddenException) for paid-feature blocks: - // token-reuse throttle, "active subscription required", and the - // primary-device restriction (see donutbrowser-infra wayfern.service.ts). - // This is distinct from a 401 (dead access token) — the session is still - // valid, the user is just temporarily/conditionally not entitled. So we - // do NOT invalidate the session. Instead: drop the stale wayfern token so - // no browser launches half-authenticated, re-fetch the profile so the - // cached plan reflects the backend's real state (it may have changed), - // and signal the UI so the user learns why automation stopped working. + // A 403 rejects the entitlement without invalidating the login session. + // Clear the browser token and refresh account state before notifying UI. if e.contains("(403") || e.contains("Forbidden") { - log::warn!("Wayfern token blocked by backend (403): {e}"); + log::warn!("Wayfern token blocked by backend (403)"); self.clear_wayfern_token().await; if let Err(fetch_err) = self.fetch_profile().await { log::warn!("Profile re-fetch after wayfern block failed: {fetch_err}"); @@ -1239,7 +1228,7 @@ impl CloudAuthManager { /// Get the current wayfern token, if any. pub async fn get_wayfern_token(&self) -> Option { #[cfg(feature = "e2e")] - if tauri_plugin_cross_platform_webdriver::automation_enabled() { + if crate::e2e_automation_enabled() { if let Some(token) = std::env::var_os("WAYFERN_TEST_TOKEN") .filter(|token| !token.is_empty()) .and_then(|token| token.into_string().ok()) diff --git a/src-tauri/src/cookie_manager.rs b/src-tauri/src/cookie_manager.rs index 4bc5f2b..770ad61 100644 --- a/src-tauri/src/cookie_manager.rs +++ b/src-tauri/src/cookie_manager.rs @@ -1057,6 +1057,23 @@ impl CookieManager { mod tests { use super::*; + #[cfg(target_os = "macos")] + const SYNTHETIC_COOKIE_HOST: &str = ".example.test"; + #[cfg(target_os = "macos")] + const SYNTHETIC_COOKIE_VALUE: &str = "synthetic-cookie-value"; + #[cfg(target_os = "macos")] + const SYNTHETIC_OS_CRYPT_PASSWORD: &[u8] = b"donut-synthetic-cookie-key"; + #[cfg(target_os = "macos")] + const SYNTHETIC_ENCRYPTED_COOKIE_HEX: &str = "763130d83b9fd3e6d1b1c793769f55251f5e9d1193be72c0c08ea32e2cf068a85d9d0b97d8b2e6deca93a2b3c290e98e1a851f83d5566f9aa9314befe56dc6bdbd423d"; + + #[cfg(target_os = "macos")] + fn synthetic_encrypted_cookie() -> Vec { + (0..SYNTHETIC_ENCRYPTED_COOKIE_HEX.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&SYNTHETIC_ENCRYPTED_COOKIE_HEX[i..i + 2], 16).unwrap()) + .collect() + } + #[test] fn test_parse_netscape_cookies_valid() { let content = "# Netscape HTTP Cookie File\n\ @@ -1482,50 +1499,29 @@ mod tests { let _ = std::fs::remove_file(&tmp); } - /// Regression: decrypting a real v10-encrypted Chromium cookie with the - /// correct PBKDF2 iterations and the `SHA-256(host_key)` integrity-prefix - /// strip. Captured from a real Wayfern profile: - /// host_key = ".github.com" - /// name = "_octo" - /// password = "OSfgzI5GUqy/pK4ANrYugw==" (contents of os_crypt_key) - /// value = "GH1.1.2077424036.1774792325" - /// - /// If PBKDF2 iterations or the host-hash prefix handling ever regress, - /// this test fails and we instantly know why all copied cookies end up - /// with empty values — which is exactly the bug that shipped and made - /// issue-265-style silent failures reappear. #[test] #[cfg(target_os = "macos")] - fn test_decrypt_v10_cookie_with_real_vector() { + fn test_decrypt_v10_cookie_with_synthetic_vector() { let profile_dir = std::env::temp_dir().join(format!("donut_decrypt_vector_{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&profile_dir).unwrap(); std::fs::write( profile_dir.join("os_crypt_key"), - b"OSfgzI5GUqy/pK4ANrYugw==", + SYNTHETIC_OS_CRYPT_PASSWORD, ) .unwrap(); let key = chrome_decrypt::get_encryption_key(&profile_dir) .expect("should derive key from os_crypt_key file"); - let encrypted_hex = "76313077ad5b27e78f685a6ccc7b92a8a242e279e54b8d2ba8e55b433ca7e2421bec52369e29a57b593c02c839f50962245da3ed8617dce142fff67778950a271d2c07"; - let encrypted: Vec = (0..encrypted_hex.len()) - .step_by(2) - .map(|i| u8::from_str_radix(&encrypted_hex[i..i + 2], 16).unwrap()) - .collect(); - - let decrypted = chrome_decrypt::decrypt(&encrypted, ".github.com", &key) - .expect("decryption must succeed with correct key and host"); - assert_eq!(decrypted, "GH1.1.2077424036.1774792325"); + let decrypted = + chrome_decrypt::decrypt(&synthetic_encrypted_cookie(), SYNTHETIC_COOKIE_HOST, &key) + .expect("decryption must succeed with correct key and host"); + assert_eq!(decrypted, SYNTHETIC_COOKIE_VALUE); let _ = std::fs::remove_dir_all(&profile_dir); } - /// Sanity: decrypting with the wrong host_key (hash mismatch) must not - /// return a half-garbage value — it should fall back to the full - /// decrypted bytes, which for a modern cookie includes the 32-byte hash - /// prefix and therefore won't be valid UTF-8 → `None`. #[test] #[cfg(target_os = "macos")] fn test_decrypt_with_wrong_host_returns_none_or_raw() { @@ -1534,25 +1530,16 @@ mod tests { std::fs::create_dir_all(&profile_dir).unwrap(); std::fs::write( profile_dir.join("os_crypt_key"), - b"OSfgzI5GUqy/pK4ANrYugw==", + SYNTHETIC_OS_CRYPT_PASSWORD, ) .unwrap(); let key = chrome_decrypt::get_encryption_key(&profile_dir).unwrap(); - let encrypted_hex = "76313077ad5b27e78f685a6ccc7b92a8a242e279e54b8d2ba8e55b433ca7e2421bec52369e29a57b593c02c839f50962245da3ed8617dce142fff67778950a271d2c07"; - let encrypted: Vec = (0..encrypted_hex.len()) - .step_by(2) - .map(|i| u8::from_str_radix(&encrypted_hex[i..i + 2], 16).unwrap()) - .collect(); - - // Wrong host: the prefix won't match, so we fall through to - // `String::from_utf8(full_decrypted)` which fails on the binary hash - // bytes and returns `None`. Either way, we must NOT return the real - // value "GH1.1.2077424036.1774792325". - let result = chrome_decrypt::decrypt(&encrypted, ".facebook.com", &key); + let result = + chrome_decrypt::decrypt(&synthetic_encrypted_cookie(), ".wrong.example.test", &key); assert!( - result.as_deref() != Some("GH1.1.2077424036.1774792325"), - "decrypt must not return the real cookie value when host_key is wrong" + result.as_deref() != Some(SYNTHETIC_COOKIE_VALUE), + "decrypt must not return the cookie value when host_key is wrong" ); let _ = std::fs::remove_dir_all(&profile_dir); diff --git a/src-tauri/src/downloaded_browsers_registry.rs b/src-tauri/src/downloaded_browsers_registry.rs index 856e719..23a2565 100644 --- a/src-tauri/src/downloaded_browsers_registry.rs +++ b/src-tauri/src/downloaded_browsers_registry.rs @@ -1262,7 +1262,7 @@ pub async fn ensure_active_browsers_downloaded( app_handle: tauri::AppHandle, ) -> Result, String> { #[cfg(feature = "e2e")] - if tauri_plugin_cross_platform_webdriver::automation_enabled() + if crate::e2e_automation_enabled() && std::env::var_os("DONUT_E2E_DISABLE_STARTUP_NETWORK").is_some() { log::info!("E2E: skipping proactive browser download"); @@ -1419,7 +1419,7 @@ pub async fn ensure_all_binaries_exist( app_handle: tauri::AppHandle, ) -> Result, String> { #[cfg(feature = "e2e")] - if tauri_plugin_cross_platform_webdriver::automation_enabled() + if crate::e2e_automation_enabled() && std::env::var_os("DONUT_E2E_DISABLE_STARTUP_NETWORK").is_some() { log::info!("E2E: skipping proactive binary and GeoIP downloads"); diff --git a/src-tauri/src/downloader.rs b/src-tauri/src/downloader.rs index 6dcac42..ad82150 100644 --- a/src-tauri/src/downloader.rs +++ b/src-tauri/src/downloader.rs @@ -231,7 +231,7 @@ impl Downloader { let download_url = self .resolve_download_url(browser_type.clone(), version, download_info) .await?; - log::info!("Download URL resolved: {}", download_url); + log::info!("Download URL resolved"); // In-session resume: a large (~1GB) download over a flaky connection can // drop mid-stream. Rather than surfacing the first stall/chunk error as a diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b6c4a4e..059e6a3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -18,7 +18,8 @@ static QUIT_CONFIRMED: AtomicBool = AtomicBool::new(false); fn e2e_automation_enabled() -> bool { #[cfg(feature = "e2e")] { - tauri_plugin_cross_platform_webdriver::automation_enabled() + std::env::var("TAURI_AUTOMATION") + .is_ok_and(|value| value == "1" || value.eq_ignore_ascii_case("true")) } #[cfg(not(feature = "e2e"))] { @@ -26,6 +27,13 @@ fn e2e_automation_enabled() -> bool { } } +#[cfg(feature = "e2e")] +fn e2e_automation_profile_dir() -> Option { + e2e_automation_enabled() + .then(|| std::env::var_os("TAURI_AUTOMATION_PROFILE_DIR").map(std::path::PathBuf::from)) + .flatten() +} + mod api_client; mod api_server; mod app_auto_updater; @@ -47,6 +55,7 @@ mod geolocation; mod group_manager; mod human_typing; mod ip_utils; +mod log_redaction; mod platform_browser; mod profile; mod profile_importer; @@ -239,7 +248,7 @@ impl WindowExt for WebviewWindow { // Called internally for deep-link / startup URL handling — not invoked from the // frontend, so it is intentionally not a `#[tauri::command]`. async fn handle_url_open(app: tauri::AppHandle, url: String) -> Result<(), String> { - log::info!("handle_url_open called with URL: {url}"); + log::info!("Handling URL open request"); // Check if the main window exists and is ready if let Some(window) = app.get_webview_window("main") { @@ -1382,11 +1391,18 @@ fn setup_system_tray(app: &tauri::AppHandle) -> Result<(), Box) -> tauri::Builder, +) { let args: Vec = env::args().collect(); let startup_url = args.iter().find(|arg| arg.starts_with("http")).cloned(); if let Some(url) = startup_url.clone() { - log::info!("Found startup URL in command line: {url}"); + log::info!("Found startup URL in command line"); let mut pending = PENDING_URLS.lock().unwrap(); pending.push(url.clone()); } @@ -1405,10 +1421,7 @@ pub fn run() { }), }; - let builder = tauri::Builder::default(); - - #[cfg(feature = "e2e")] - let builder = builder.plugin(tauri_plugin_cross_platform_webdriver::init()); + let builder = configure_builder(tauri::Builder::default()); let builder = builder.plugin( tauri_plugin_log::Builder::new() @@ -1416,11 +1429,10 @@ pub fn run() { .target(Target::new(TargetKind::Stdout)) .target(Target::new(TargetKind::Webview)) .target(file_log_target) - // 5 MB per rotated file × KeepAll — the previous 100 KB limit - // truncated useful context in customer support reports; 50 MB - // turned out to be excessive disk pressure. + // Keep enough context for customer support without letting a long-running + // installation accumulate logs without bound. .max_file_size(5 * 1024 * 1024) - .rotation_strategy(tauri_plugin_log::RotationStrategy::KeepAll) + .rotation_strategy(tauri_plugin_log::RotationStrategy::KeepSome(10)) .level(log::LevelFilter::Info) .format(|out, message, record| { use chrono::Local; @@ -1503,8 +1515,7 @@ pub fn run() { .visible(true); #[cfg(feature = "e2e")] - let win_builder = - match tauri_plugin_cross_platform_webdriver::automation_profile_dir() { + let win_builder = match e2e_automation_profile_dir() { Some(profile_dir) => win_builder .data_directory(profile_dir.join("webview")) // WKWebView ignores data_directory on macOS. Incognito gives every @@ -1514,7 +1525,7 @@ pub fn run() { // DONUTBROWSER_DATA_ROOT; only browser-engine storage is ephemeral. .incognito(true), None => win_builder, - }; + }; #[cfg(target_os = "windows")] let win_builder = win_builder.decorations(false); @@ -1601,7 +1612,7 @@ pub fn run() { for url in urls { let url_string = url.to_string(); - log::info!("Deep link received: {url_string}"); + log::info!("Processing deep link URL"); let handle_clone = handle.clone(); tauri::async_runtime::spawn(async move { @@ -1617,7 +1628,7 @@ pub fn run() { if let Some(startup_url) = startup_url { let handle_clone = handle.clone(); tauri::async_runtime::spawn(async move { - log::info!("Processing startup URL from command line: {startup_url}"); + log::info!("Processing startup URL from command line"); if let Err(e) = handle_url_open(handle_clone, startup_url.clone()).await { log::error!("Failed to handle startup URL: {e}"); } @@ -1828,7 +1839,7 @@ pub fn run() { }; for url in pending_urls { - log::info!("Processing pending URL: {url}"); + log::info!("Processing pending URL"); if let Err(e) = handle_url_open(handle_pending.clone(), url).await { log::error!("Failed to handle pending URL: {e}"); } diff --git a/src-tauri/src/log_redaction.rs b/src-tauri/src/log_redaction.rs new file mode 100644 index 0000000..50abb90 --- /dev/null +++ b/src-tauri/src/log_redaction.rs @@ -0,0 +1,96 @@ +use regex_lite::Regex; +use std::sync::LazyLock; + +static URL_RE: LazyLock = LazyLock::new(|| { + Regex::new(r#"(?i)\b[a-z][a-z0-9+.-]{1,20}://[^\s<>"']+"#).expect("valid URL regex") +}); +static PRIVATE_KEY_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"(?is)-----BEGIN [^-\r\n]*PRIVATE KEY-----.*?-----END [^-\r\n]*PRIVATE KEY-----") + .expect("valid private-key regex") +}); +static BEARER_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(?i)\bBearer\s+[A-Za-z0-9._~+/=-]+").expect("valid bearer regex")); +static SECRET_RE: LazyLock = LazyLock::new(|| { + Regex::new( + r"(?i)\b(api[_-]?key|authorization|password|passwd|private[_-]?key|proxy[_-]?(password|username)|refresh[_-]?token|secret|token|username)\b\s*[:=]\s*[^\s,;]+", + ) + .expect("valid secret regex") +}); +static EMAIL_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"(?i)\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b").expect("valid email regex") +}); +static UNIX_HOME_RE: LazyLock = + LazyLock::new(|| Regex::new(r"/(Users|home)/[^/\s]+").expect("valid Unix home regex")); +static WINDOWS_HOME_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(?i)\b[A-Z]:\\Users\\[^\\\s]+").expect("valid Windows home regex")); +static IPV4_RE: LazyLock = + LazyLock::new(|| Regex::new(r"\b([0-9]{1,3}\.){3}[0-9]{1,3}\b").expect("valid IPv4 regex")); +static DOMAIN_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(?i)\b([a-z0-9-]+\.)+[a-z]{2,}\b").expect("valid domain regex")); +static UUID_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"(?i)\b[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b") + .expect("valid UUID regex") +}); + +pub fn url_label(value: &str) -> String { + url::Url::parse(value) + .map(|parsed| format!("{}://", parsed.scheme())) + .unwrap_or_else(|_| "".to_string()) +} + +pub fn text(value: &str) -> String { + let redacted = PRIVATE_KEY_RE.replace_all(value, ""); + let redacted = URL_RE.replace_all(&redacted, ""); + let redacted = BEARER_RE.replace_all(&redacted, "Bearer "); + let redacted = SECRET_RE.replace_all(&redacted, ""); + let redacted = EMAIL_RE.replace_all(&redacted, ""); + let redacted = UNIX_HOME_RE.replace_all(&redacted, "/"); + let redacted = WINDOWS_HOME_RE.replace_all(&redacted, ""); + let redacted = IPV4_RE.replace_all(&redacted, ""); + let redacted = DOMAIN_RE.replace_all(&redacted, ""); + UUID_RE + .replace_all(&redacted, "") + .into_owned() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn redacts_sensitive_log_content() { + let input = format!( + concat!( + "URL https://user:pass@example.com/callback?code=private\n", + "Authorization: Bearer secret-token\n", + "password=hunter2\n", + "user@example.com /Users/alice/Library C:\\Users\\alice\\AppData\n", + "exit 203.0.113.42\n", + "-----BEGIN {0} KEY-----\nprivate-material\n-----END {0} KEY-----\n", + ), + "PRIVATE" + ); + let output = text(&input); + for sensitive in [ + "user:pass", + "example.com", + "private-material", + "secret-token", + "hunter2", + "user@example.com", + "alice", + "203.0.113.42", + ] { + assert!(!output.contains(sensitive), "log output leaked {sensitive}"); + } + } + + #[test] + fn url_labels_retain_only_the_scheme() { + assert_eq!( + url_label("https://user:pass@example.com/path?token=value"), + "https://" + ); + assert_eq!(url_label("not a URL"), ""); + } +} diff --git a/src-tauri/src/profile/clear_on_close.rs b/src-tauri/src/profile/clear_on_close.rs index 9b33803..c537ccc 100644 --- a/src-tauri/src/profile/clear_on_close.rs +++ b/src-tauri/src/profile/clear_on_close.rs @@ -39,7 +39,7 @@ fn is_kept(name: &str) -> bool { /// Identity must not rest on `Preferences` existing. Chromium writes it lazily, /// so a crash — or a user deleting a corrupt copy, a standard troubleshooting /// step since it regenerates — leaves a populated `Default/` without it. Such a -/// directory would then be taken for junk and removed wholesale, destroying the +/// directory would then be treated as stale and removed wholesale, destroying the /// Extensions and Bookmarks this feature exists to preserve. fn is_profile_dir_name(name: &str) -> bool { matches!(name, "Default" | "Guest Profile" | "System Profile") diff --git a/src-tauri/src/proxy_manager.rs b/src-tauri/src/proxy_manager.rs index 8c5abae..eb99082 100644 --- a/src-tauri/src/proxy_manager.rs +++ b/src-tauri/src/proxy_manager.rs @@ -279,7 +279,7 @@ impl ProxyManager { ) -> Result<(), Box> { let cache_file = self.get_proxy_check_cache_file(proxy_id)?; let content = serde_json::to_string_pretty(result)?; - fs::write(&cache_file, content)?; + crate::app_dirs::write_owner_only(&cache_file, content.as_bytes())?; Ok(()) } @@ -404,7 +404,7 @@ impl ProxyManager { let proxy_file = self.get_proxy_file_path(&proxy.id); let content = serde_json::to_string_pretty(proxy)?; - fs::write(&proxy_file, content)?; + crate::app_dirs::write_owner_only(&proxy_file, content.as_bytes())?; Ok(()) } @@ -1636,12 +1636,13 @@ impl ProxyManager { .arg("--type") .arg(&proxy_settings.proxy_type); - // Add credentials if provided + // Keep credentials out of process arguments. The short-lived sidecar + // removes these variables before it spawns the detached worker. if let Some(username) = &proxy_settings.username { - proxy_cmd = proxy_cmd.arg("--username").arg(username); + proxy_cmd = proxy_cmd.env("DONUT_PROXY_USERNAME", username); } if let Some(password) = &proxy_settings.password { - proxy_cmd = proxy_cmd.arg("--password").arg(password); + proxy_cmd = proxy_cmd.env("DONUT_PROXY_PASSWORD", password); } } @@ -2526,7 +2527,7 @@ mod tests { Ok(()) } - // Test that validates the command line arguments are constructed correctly + // Validate that non-secret proxy settings remain command arguments. #[test] fn test_proxy_command_construction() { let proxy_settings = ProxySettings { @@ -2547,10 +2548,6 @@ mod tests { "8080", "--type", "http", - "--username", - "user", - "--password", - "pass", ]; // This test verifies the argument structure without actually running the command @@ -2674,61 +2671,6 @@ mod tests { Ok(()) } - // Test that validates URL encoding for special characters in credentials - #[tokio::test] - async fn test_proxy_credentials_encoding() -> Result<(), Box> { - let proxy_path = ensure_donut_proxy_binary().await?; - - // Test with credentials that include special characters - let mut cmd = Command::new(&proxy_path); - cmd - .arg("proxy") - .arg("start") - .arg("--host") - .arg("test.example.com") - .arg("--proxy-port") - .arg("8080") - .arg("--type") - .arg("http") - .arg("--username") - .arg("user@domain.com") - .arg("--password") - .arg("pass word!"); - - let output = tokio::time::timeout(Duration::from_secs(10), cmd.output()).await??; - - if output.status.success() { - let stdout = String::from_utf8(output.stdout)?; - let config: serde_json::Value = serde_json::from_str(&stdout)?; - - let upstream_url = config["upstreamUrl"].as_str().unwrap(); - - println!("Generated upstream URL: {upstream_url}"); - - // Verify that special characters are properly encoded - assert!(upstream_url.contains("user%40domain.com")); - assert!(upstream_url.contains("pass%20word")); - - println!("URL encoding test passed - special characters handled correctly"); - - // Clean up - let proxy_id = config["id"].as_str().unwrap(); - let mut stop_cmd = Command::new(&proxy_path); - stop_cmd.arg("proxy").arg("stop").arg("--id").arg(proxy_id); - let _ = stop_cmd.output().await; - } else { - let stdout = String::from_utf8(output.stdout)?; - let stderr = String::from_utf8(output.stderr)?; - println!("Command failed (expected for non-existent upstream):"); - println!("Stdout: {stdout}"); - println!("Stderr: {stderr}"); - - println!("URL encoding test completed - credentials should be properly encoded"); - } - - Ok(()) - } - // ────────────────────────────────────────────────────────────────────── // Complex proxy process monitoring tests // ────────────────────────────────────────────────────────────────────── @@ -3064,6 +3006,18 @@ mod tests { // Save save_proxy_config(&config).unwrap(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mode = + std::fs::metadata(crate::proxy_storage::get_storage_dir().join(format!("{id}.json"))) + .unwrap() + .permissions() + .mode() + & 0o777; + assert_eq!(mode, 0o600, "proxy credentials must be owner-only"); + } + // Load and compare let loaded = get_proxy_config(&id).expect("Config should be loadable"); assert_eq!(loaded.id, config.id); diff --git a/src-tauri/src/proxy_runner.rs b/src-tauri/src/proxy_runner.rs index 4e25192..3a7df1c 100644 --- a/src-tauri/src/proxy_runner.rs +++ b/src-tauri/src/proxy_runner.rs @@ -11,6 +11,44 @@ lazy_static::lazy_static! { } static SIDECAR_VERSION_VERIFIED: AtomicBool = AtomicBool::new(false); +const RETAINED_PROXY_LOGS: usize = 20; + +fn prune_stale_proxy_logs(temp_dir: &Path, retain: usize) { + let active_ids = PROXY_PROCESSES + .lock() + .map(|processes| processes.keys().cloned().collect::>()) + .unwrap_or_default(); + let Ok(entries) = std::fs::read_dir(temp_dir) else { + return; + }; + let mut logs = entries + .flatten() + .filter_map(|entry| { + let file_name = entry.file_name(); + let file_name = file_name.to_str()?; + let id = file_name + .strip_prefix("donut-proxy-")? + .strip_suffix(".log")?; + if active_ids.iter().any(|active_id| active_id == id) { + return None; + } + let modified = entry + .metadata() + .and_then(|metadata| metadata.modified()) + .unwrap_or(std::time::UNIX_EPOCH); + Some((modified, entry.path())) + }) + .collect::>(); + logs.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.0)); + for (_, path) in logs.into_iter().skip(retain) { + if let Err(error) = std::fs::remove_file(&path) { + log::debug!( + "Failed to prune stale proxy log {}: {error}", + path.display() + ); + } + } +} fn target_binary_name(base_name: &str) -> Option { let target = std::env::var("TARGET").ok()?; @@ -277,6 +315,10 @@ pub async fn start_proxy_process_with_profile( // Spawn proxy worker process in the background using std::process::Command // This ensures proper process detachment on Unix systems let exe = find_sidecar_executable("donut-proxy")?; + let temp_dir = std::env::temp_dir(); + let log_path = temp_dir.join(format!("donut-proxy-{id}.log")); + let log_file = crate::app_dirs::create_owner_only(&log_path); + prune_stale_proxy_logs(&temp_dir, RETAINED_PROXY_LOGS); #[cfg(unix)] { @@ -288,13 +330,14 @@ pub async fn start_proxy_process_with_profile( cmd.arg("start"); cmd.arg("--id"); cmd.arg(&id); + cmd.env_remove("DONUT_PROXY_USERNAME"); + cmd.env_remove("DONUT_PROXY_PASSWORD"); cmd.stdin(Stdio::null()); cmd.stdout(Stdio::null()); // Always log to file for diagnostics (both debug and release builds) - let log_path = std::env::temp_dir().join(format!("donut-proxy-{}.log", id)); - if let Ok(file) = std::fs::File::create(&log_path) { + if let Ok(file) = log_file { log::info!("Proxy worker stderr will be logged to: {:?}", log_path); cmd.stderr(Stdio::from(file)); } else { @@ -365,13 +408,14 @@ pub async fn start_proxy_process_with_profile( cmd.arg("start"); cmd.arg("--id"); cmd.arg(&id); + cmd.env_remove("DONUT_PROXY_USERNAME"); + cmd.env_remove("DONUT_PROXY_PASSWORD"); cmd.stdin(Stdio::null()); cmd.stdout(Stdio::null()); // Log to file for diagnostics (matching Unix behavior) - let log_path = std::env::temp_dir().join(format!("donut-proxy-{}.log", id)); - if let Ok(file) = std::fs::File::create(&log_path) { + if let Ok(file) = log_file { log::info!("Proxy worker stderr will be logged to: {:?}", log_path); cmd.stderr(Stdio::from(file)); } else { @@ -521,7 +565,9 @@ pub async fn stop_all_proxy_processes() -> Result<(), Box #[cfg(test)] mod tests { - use super::parse_sidecar_version; + use super::{parse_sidecar_version, prune_stale_proxy_logs}; + use std::fs; + use std::time::Duration; #[test] fn parses_exact_sidecar_version_output() { @@ -545,4 +591,21 @@ mod tests { None ); } + + #[test] + fn prunes_only_old_proxy_logs() { + let temp = tempfile::tempdir().unwrap(); + for id in ["oldest", "middle", "newest"] { + fs::write(temp.path().join(format!("donut-proxy-{id}.log")), id).unwrap(); + std::thread::sleep(Duration::from_millis(10)); + } + fs::write(temp.path().join("unrelated.log"), "keep").unwrap(); + + prune_stale_proxy_logs(temp.path(), 2); + + assert!(!temp.path().join("donut-proxy-oldest.log").exists()); + assert!(temp.path().join("donut-proxy-middle.log").exists()); + assert!(temp.path().join("donut-proxy-newest.log").exists()); + assert!(temp.path().join("unrelated.log").exists()); + } } diff --git a/src-tauri/src/proxy_server.rs b/src-tauri/src/proxy_server.rs index 93f6e63..7ec6e31 100644 --- a/src-tauri/src/proxy_server.rs +++ b/src-tauri/src/proxy_server.rs @@ -1351,9 +1351,8 @@ pub async fn handle_proxy_connection( /// Render an upstream proxy URL for logging with any embedded credentials /// stripped. `config.upstream_url` carries `scheme://user:pass@host:port`, and -/// these logs land in a world-readable file under the system temp dir, so the -/// userinfo must never be emitted. -fn redacted_upstream(upstream: &str) -> String { +/// diagnostic logs and command responses must never expose the userinfo. +pub fn redacted_upstream(upstream: &str) -> String { if upstream.is_empty() { return "none".to_string(); } @@ -2238,6 +2237,16 @@ mod tests { assert_eq!(upstream_userpass(&u), ("u@name".into(), String::new())); } + #[test] + fn upstream_log_value_never_contains_credentials() { + assert_eq!( + redacted_upstream("http://user:p%40ss@example.com:8080"), + "http://example.com:8080" + ); + assert_eq!(redacted_upstream("not a URL"), ""); + assert_eq!(redacted_upstream(""), "none"); + } + #[test] fn test_blocklist_exact_match() { let mut matcher = BlocklistMatcher::new(); diff --git a/src-tauri/src/proxy_storage.rs b/src-tauri/src/proxy_storage.rs index 06b60a7..8d5adf2 100644 --- a/src-tauri/src/proxy_storage.rs +++ b/src-tauri/src/proxy_storage.rs @@ -87,6 +87,29 @@ impl ProxyConfig { } } +pub fn build_proxy_url( + proxy_type: &str, + host: &str, + port: u16, + username: Option<&str>, + password: Option<&str>, +) -> String { + let mut url = format!("{}://", proxy_type.to_lowercase()); + if let (Some(user), Some(pass)) = (username, password) { + url.push_str(&format!( + "{}:{}@", + urlencoding::encode(user), + urlencoding::encode(pass) + )); + } else if let Some(user) = username { + url.push_str(&format!("{}@", urlencoding::encode(user))); + } + url.push_str(host); + url.push(':'); + url.push_str(&port.to_string()); + url +} + pub fn get_storage_dir() -> PathBuf { crate::app_dirs::proxy_workers_dir() } @@ -97,7 +120,7 @@ pub fn save_proxy_config(config: &ProxyConfig) -> Result<(), Box bool { return false; } - match serde_json::to_string_pretty(config) { - Ok(content) => fs::write(&file_path, content).is_ok(), - Err(_) => false, + let Ok(content) = serde_json::to_string_pretty(config) else { + return false; + }; + if crate::app_dirs::write_owner_only(&file_path, content.as_bytes()).is_err() { + return false; } + true } pub fn generate_proxy_id() -> String { @@ -195,6 +221,21 @@ pub fn is_process_running(pid: u32) -> bool { mod tests { use super::*; + #[test] + fn proxy_url_encodes_credentials() { + let url = build_proxy_url( + "HTTP", + "test.example.com", + 8080, + Some("user@domain.com"), + Some("pass word!"), + ); + assert_eq!( + url, + "http://user%40domain.com:pass%20word%21@test.example.com:8080" + ); + } + #[test] fn test_is_process_running_detects_current_process() { let pid = std::process::id(); diff --git a/src-tauri/src/settings_manager.rs b/src-tauri/src/settings_manager.rs index 6bf0248..2a6df41 100644 --- a/src-tauri/src/settings_manager.rs +++ b/src-tauri/src/settings_manager.rs @@ -852,7 +852,13 @@ pub async fn read_log_files(app_handle: tauri::AppHandle) -> Result= MAX_BYTES { break; } @@ -884,7 +890,7 @@ pub async fn read_log_files(app_handle: tauri::AppHandle) -> Result(); - Ok(final_out) + Ok(crate::log_redaction::text(&final_out)) } /// Reveal the log directory in the OS file manager. diff --git a/src-tauri/src/sync/subscription.rs b/src-tauri/src/sync/subscription.rs index 9567aca..ac1c1a1 100644 --- a/src-tauri/src/sync/subscription.rs +++ b/src-tauri/src/sync/subscription.rs @@ -207,7 +207,7 @@ impl SyncSubscription { )); } - log::info!("Connected to sync subscription at {url}"); + log::info!("Connected to sync subscription"); let _ = events::emit("sync-subscription-status", "connected"); let mut buffer = String::new(); diff --git a/src-tauri/src/synchronizer.rs b/src-tauri/src/synchronizer.rs index cca309d..f456f08 100644 --- a/src-tauri/src/synchronizer.rs +++ b/src-tauri/src/synchronizer.rs @@ -359,7 +359,7 @@ impl SynchronizerManager { log::info!("Synchronizer: leader CDP port = {leader_port}, getting WS URL"); let leader_ws_url = Self::get_page_ws_url(leader_port).await?; - log::info!("Synchronizer: connecting to leader page at {leader_ws_url}"); + log::info!("Synchronizer: connecting to leader page"); let (mut ws_stream, _) = connect_async(&leader_ws_url) .await @@ -504,7 +504,7 @@ impl SynchronizerManager { Ok(url) => { match tokio_tungstenite::connect_async(&url).await { Ok((ws, _)) => { - log::info!("Synchronizer: follower {} connected at {}", fp.name, url); + log::info!("Synchronizer: follower connected"); let (tx, rx) = tokio::sync::mpsc::unbounded_channel::(); follower_senders.insert(fid.clone(), tx); @@ -674,7 +674,7 @@ impl SynchronizerManager { if is_top { if let Some(url) = frame.get("url").and_then(|v| v.as_str()) { if !url.starts_with("about:") && !url.starts_with("chrome://") { - log::info!("Synchronizer: replaying address-bar navigation to {url}"); + log::info!("Synchronizer: replaying address-bar navigation"); let nav_event = CapturedEvent { event_type: "navigate".to_string(), url: Some(url.to_string()), diff --git a/src-tauri/src/vpn/socks5_server.rs b/src-tauri/src/vpn/socks5_server.rs index 6a0e4e8..4c6fd0c 100644 --- a/src-tauri/src/vpn/socks5_server.rs +++ b/src-tauri/src/vpn/socks5_server.rs @@ -1158,7 +1158,7 @@ mod tests { #[test] fn test_parse_key_valid() { - let key = "YEocP0e2o1WT5GlvBvQzVF7EeR6z9aCk+ZdZ5NKEuXA="; + let key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; assert!(parse_key(key).is_ok()); } diff --git a/src-tauri/src/vpn/wireguard.rs b/src-tauri/src/vpn/wireguard.rs index 745dd1d..80e8a5d 100644 --- a/src-tauri/src/vpn/wireguard.rs +++ b/src-tauri/src/vpn/wireguard.rs @@ -349,12 +349,11 @@ mod tests { fn create_test_config() -> WireGuardConfig { WireGuardConfig { - // These are test keys, not real ones - private_key: "YEocP0e2o1WT5GlvBvQzVF7EeR6z9aCk+ZdZ5NKEuXA=".to_string(), + private_key: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_string(), address: "10.0.0.2/24".to_string(), dns: Some("1.1.1.1".to_string()), mtu: Some(1420), - peer_public_key: "aGnF7JlG+U5t0BqB1PVf1yOuELHrWLGGcUJb0eCK9Aw=".to_string(), + peer_public_key: "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=".to_string(), peer_endpoint: "127.0.0.1:51820".to_string(), allowed_ips: vec!["0.0.0.0/0".to_string()], persistent_keepalive: Some(25), @@ -375,8 +374,7 @@ mod tests { #[test] fn test_parse_key_valid() { - // Valid base64-encoded 32-byte key - let key = "YEocP0e2o1WT5GlvBvQzVF7EeR6z9aCk+ZdZ5NKEuXA="; + let key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; let result = WireGuardTunnel::parse_key(key); assert!(result.is_ok()); assert_eq!(result.unwrap().len(), 32); diff --git a/src-tauri/src/wayfern_manager.rs b/src-tauri/src/wayfern_manager.rs index 205c8be..554ad2b 100644 --- a/src-tauri/src/wayfern_manager.rs +++ b/src-tauri/src/wayfern_manager.rs @@ -910,11 +910,6 @@ impl WayfernManager { ); } } - if let Some(ref token) = wayfern_token { - args.push(format!("--wayfern-token={token}")); - log::info!("Wayfern token passed as CLI flag (length: {})", token.len()); - } - if let Some(proxy) = proxy_url { // Map the local proxy scheme to the matching PAC directive. SOCKS5 lets // Chromium route UDP (QUIC/WebRTC) and resolve DNS through the proxy; @@ -942,6 +937,10 @@ impl WayfernManager { .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()); + if let Some(ref token) = wayfern_token { + command.env("WAYFERN_TOKEN", token); + log::info!("Wayfern authorization configured for browser process"); + } let child = command .spawn() @@ -1040,16 +1039,13 @@ impl WayfernManager { for target in &page_targets { if let Some(ws_url) = &target.websocket_debugger_url { - log::info!("Applying fingerprint to target via WebSocket: {}", ws_url); + log::info!("Applying fingerprint to page target"); match self .send_cdp_command(ws_url, "Wayfern.setFingerprint", fingerprint_params.clone()) .await { Ok(result) => { - log::info!( - "Successfully applied fingerprint to page target: {:?}", - result - ); + log::info!("Successfully applied fingerprint to page target"); // Wayfern.setFingerprint echoes back the fingerprint it actually // used, which may be UPGRADED from what we sent (e.g. when the // stored fingerprint targets an older browser version). Capture @@ -1080,7 +1076,7 @@ impl WayfernManager { // Geolocation is handled internally by the browser binary. if let Some(url) = url { - log::info!("Navigating to URL via CDP: {}", url); + log::info!("Navigating to URL via CDP"); if let Some(target) = page_targets.first() { if let Some(ws_url) = &target.websocket_debugger_url { if let Err(e) = self @@ -1212,7 +1208,7 @@ impl WayfernManager { return Err(format!("CDP /json/new returned HTTP {}", resp.status()).into()); } - log::info!("Opened URL in new tab via CDP: {}", url); + log::info!("Opened URL in new tab via CDP"); Ok(()) } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e5af1cd..a1d54d6 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -6,13 +6,24 @@ "build": { "beforeDevCommand": "pnpm copy-proxy-binary && pnpm dev", "devUrl": "http://localhost:12341", - "beforeBuildCommand": "pnpm copy-proxy-binary && (test -d ../dist || pnpm build)", + "beforeBuildCommand": "pnpm copy-proxy-binary:release && (test -d ../dist || pnpm build)", "frontendDist": "../dist" }, "app": { "windows": [], "security": { - "csp": null + "csp": { + "default-src": "'self' customprotocol: asset:", + "connect-src": "ipc: http://ipc.localhost http: https: ws: wss:", + "font-src": "'self' data:", + "img-src": "'self' asset: http://asset.localhost blob: data:", + "style-src": "'self' 'unsafe-inline'", + "script-src": "'self'", + "object-src": "'none'", + "base-uri": "'none'", + "form-action": "'none'", + "frame-ancestors": "'none'" + } } }, "bundle": { @@ -35,9 +46,7 @@ "signingIdentity": null, "providerShortName": null, "entitlements": "entitlements.plist", - "files": { - "Info.plist": "Info.plist" - } + "infoPlist": "Info.plist" }, "linux": { "deb": { diff --git a/src-tauri/tests/donut_proxy_integration.rs b/src-tauri/tests/donut_proxy_integration.rs index f4aee7f..7275b3a 100644 --- a/src-tauri/tests/donut_proxy_integration.rs +++ b/src-tauri/tests/donut_proxy_integration.rs @@ -1389,9 +1389,8 @@ async fn test_local_proxy_with_shadowsocks_upstream( } // Start donut-proxy with Shadowsocks upstream - let output = TestUtils::execute_command( - &binary_path, - &[ + let output = tokio::process::Command::new(&binary_path) + .args([ "proxy", "start", "--host", @@ -1400,13 +1399,11 @@ async fn test_local_proxy_with_shadowsocks_upstream( &ss_port.to_string(), "--type", "ss", - "--username", - ss_method, - "--password", - ss_password, - ], - ) - .await?; + ]) + .env("DONUT_PROXY_USERNAME", ss_method) + .env("DONUT_PROXY_PASSWORD", ss_password) + .output() + .await?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); diff --git a/src-tauri/tests/fixtures/test.conf b/src-tauri/tests/fixtures/test.conf index 6e1d6ac..36f4e04 100644 --- a/src-tauri/tests/fixtures/test.conf +++ b/src-tauri/tests/fixtures/test.conf @@ -1,13 +1,10 @@ -# Sample WireGuard configuration for testing -# This is NOT a real configuration - for unit test purposes only - [Interface] -PrivateKey = YEocP0e2o1WT5GlvBvQzVF7EeR6z9aCk+ZdZ5NKEuXA= +PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= Address = 10.0.0.2/24 DNS = 1.1.1.1 [Peer] -PublicKey = aGnF7JlG+U5t0BqB1PVf1yOuELHrWLGGcUJb0eCK9Aw= +PublicKey = AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE= AllowedIPs = 0.0.0.0/0, ::/0 Endpoint = vpn.example.com:51820 PersistentKeepalive = 25 diff --git a/src-tauri/tests/vpn_integration.rs b/src-tauri/tests/vpn_integration.rs index 093d932..14c8d4e 100644 --- a/src-tauri/tests/vpn_integration.rs +++ b/src-tauri/tests/vpn_integration.rs @@ -292,11 +292,11 @@ fn create_test_storage(temp_dir: &tempfile::TempDir) -> VpnStorage { #[serial] async fn test_wireguard_tunnel_init() { let config = WireGuardConfig { - private_key: "YEocP0e2o1WT5GlvBvQzVF7EeR6z9aCk+ZdZ5NKEuXA=".to_string(), + private_key: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_string(), address: "10.0.0.2/24".to_string(), dns: Some("1.1.1.1".to_string()), mtu: None, - peer_public_key: "aGnF7JlG+U5t0BqB1PVf1yOuELHrWLGGcUJb0eCK9Aw=".to_string(), + peer_public_key: "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=".to_string(), peer_endpoint: "127.0.0.1:51820".to_string(), allowed_ips: vec!["0.0.0.0/0".to_string()], persistent_keepalive: Some(25), @@ -780,13 +780,9 @@ async fn test_wireguard_traffic_flows_through_donut_proxy( } let binary_path = ensure_donut_proxy_binary().await?; - let wg_config = match test_harness::start_wireguard_server().await { - Ok(config) => config, - Err(error) => { - eprintln!("skipping WireGuard e2e test: {error}"); - return Ok(()); - } - }; + let wg_config = test_harness::start_wireguard_server() + .await + .map_err(|error| format!("failed to start Docker WireGuard fixture: {error}"))?; let vpn_config = new_test_vpn_config( "WireGuard E2E", diff --git a/src/components/custom-toast.tsx b/src/components/custom-toast.tsx index 32bb732..71003c1 100644 --- a/src/components/custom-toast.tsx +++ b/src/components/custom-toast.tsx @@ -1,54 +1,3 @@ -/** - * Unified Toast System - * - * This module provides a comprehensive toast system that solves styling issues - * and provides a single, flexible toast component for all use cases. - * - * Features: - * - Proper background styling (no transparency issues) - * - Loading states with spinners - * - Progress bars for downloads/updates - * - Success/error states - * - Customizable icons and content - * - Auto-update notifications - * - * Usage Examples: - * - * Simple loading toast: - * ``` - * import { showToast } from "./custom-toast"; - * showToast({ - * type: "loading", - * title: "Loading...", - * description: "Please wait..." - * }); - * ``` - * - * Auto-update toast: - * ``` - * showAutoUpdateToast("Wayfern", "149.0.7827.116"); - * ``` - * - * Download progress toast: - * ``` - * showToast({ - * type: "download", - * title: "Downloading Wayfern 149.0.7827.116", - * progress: { percentage: 45, speed: "2.5", eta: "30s" } - * }); - * ``` - * - * Version update progress: - * ``` - * showToast({ - * type: "version-update", - * title: "Updating browser versions", - * progress: { current: 3, total: 5, found: 12 } - * }); - * ``` - */ -/** biome-ignore-all lint/suspicious/noExplicitAny: TODO */ - import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { useTranslation } from "react-i18next"; import { diff --git a/src/components/multiple-selector.tsx b/src/components/multiple-selector.tsx index ab997dd..1d5b463 100644 --- a/src/components/multiple-selector.tsx +++ b/src/components/multiple-selector.tsx @@ -1,5 +1,3 @@ -/** biome-ignore-all lint/a11y/noStaticElementInteractions: temporary suppress until in active use */ -/** biome-ignore-all lint/a11y/useKeyWithClickEvents: temporary suppress until in active use */ "use client"; import { Command as CommandPrimitive, useCommandState } from "cmdk"; @@ -14,9 +12,9 @@ export interface Option { value: string; label?: string; disable?: boolean; - /** fixed option that can't be removed. */ + /** An option that cannot be removed. */ fixed?: boolean; - /** Group the options by providing key. */ + /** Group options by this property. */ [key: string]: string | boolean | undefined; } type GroupOption = Record; @@ -24,21 +22,20 @@ type GroupOption = Record; interface MultipleSelectorProps { value?: Option[]; defaultOptions?: Option[]; - /** manually controlled options */ + /** Manually controlled options. */ options?: Option[]; placeholder?: string; /** Loading component. */ loadingIndicator?: React.ReactNode; /** Empty component. */ emptyIndicator?: React.ReactNode; - /** Debounce time for async search. Only work with `onSearch`. */ + /** Debounce time for async search. Used only with `onSearch`. */ delay?: number; /** - * Only work with `onSearch` prop. Trigger search when `onFocus`. - * For example, when user click on the input, it will trigger the search to get initial options. + * Trigger `onSearch` when the input receives focus. **/ triggerSearchOnFocus?: boolean; - /** async search */ + /** Asynchronous search. */ onSearch?: (value: string) => Promise; onChange?: (options: Option[]) => void; /** Limit the maximum number of selected options. */ @@ -48,13 +45,12 @@ interface MultipleSelectorProps { /** Hide the placeholder when there are options selected. */ hidePlaceholderWhenSelected?: boolean; disabled?: boolean; - /** Group the options base on provided key. */ + /** Group options by the provided key. */ groupBy?: string; className?: string; badgeClassName?: string; /** - * First item selected is a default behavior by cmdk. That is why the default is true. - * This is a workaround solution by add a dummy item. + * Prevent cmdk from selecting the first item by inserting a dummy item. * * @reference: https://github.com/pacocoursey/cmdk/issues/171 */ @@ -375,7 +371,7 @@ const MultipleSelector = React.forwardRef< return Object.values(selectables).some((group) => group.length > 0); }, [selectables]); - /** Avoid Creatable Selector freezing or lagging when paste a long string. */ + // Skip fuzzy matching for creatable inputs to keep long pasted values responsive. const commandFilter = React.useCallback(() => { if (commandProps?.filter) { return commandProps.filter; @@ -401,13 +397,15 @@ const MultipleSelector = React.forwardRef< "relative h-auto overflow-visible bg-transparent", commandProps?.className, )} + // Consumers may override filtering even when search is asynchronous. shouldFilter={ commandProps?.shouldFilter !== undefined ? commandProps.shouldFilter : !onSearch - } // When onSearch is provided, we don't want to filter the options. You can still override it. + } filter={commandFilter()} > + {/* biome-ignore lint/a11y/noStaticElementInteractions: pointer focus is forwarded to the nested input; keyboard users focus nested controls directly */}
{ + onMouseDown={() => { if (disabled) return; inputRef.current?.focus(); }} diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 1f0c376..453ca19 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -1,6 +1,6 @@ "use client"; -import { AnimatePresence, type HTMLMotionProps, motion } from "motion/react"; +import { type HTMLMotionProps, motion } from "motion/react"; import { Dialog as DialogPrimitive } from "radix-ui"; import type * as React from "react"; import { useTranslation } from "react-i18next"; @@ -72,20 +72,24 @@ type DialogPortalProps = Omit< "forceMount" >; -function DialogPortal(props: DialogPortalProps) { +function DialogPortal({ + children, + container: portalContainer, + ...props +}: DialogPortalProps) { const { isOpen, container } = useDialog(); + if (!isOpen) return null; + return ( - - {isOpen && ( - - )} - + + {children} + ); } @@ -106,7 +110,6 @@ function DialogOverlay({ key="dialog-overlay" initial={{ opacity: 0, filter: "blur(4px)" }} animate={{ opacity: 1, filter: "blur(0px)" }} - exit={{ opacity: 0, filter: "blur(4px)" }} transition={transition} className={cn("fixed inset-0 z-9999 bg-background/50", className)} {...props} @@ -217,8 +220,9 @@ function DialogContent({ return ( - +