From 377f3a31b3d68be27dc422d0c287c02b234a5160 Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Thu, 24 Sep 2026 20:29:00 -0700 Subject: [PATCH] initial commit: reforgermods.net launcher 0.1.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A desktop server browser for Arma Reforger, built on the reforgermods.net v2 API. Live server list with search, sort and filters; a detail inspector with population history and mod readiness; measured ICMP latency; favorites, recents and settings; and direct joining through Steam. Tauri 2 and Rust, with a Preact frontend that builds to 80 KB of JavaScript in one chunk. Logic lives in rfm-core, which has no Tauri dependency and is tested directly; src-tauri is the window, the command surface and the wiring. Packaged for the public beta: a per-user NSIS installer, a portable .exe and an Arch package, with signature-verified background updates on Windows and a release pipeline that builds from a tag. The artifacts are not yet Authenticode-signed — that is wired and waiting on an Azure Trusted Signing identity, and docs/distribution.md says so plainly rather than letting anyone discover it at the SmartScreen dialog. Co-Authored-By: Claude Opus 5 --- .github/workflows/release.yml | 266 + .gitignore | 11 + Cargo.lock | 6138 +++++++++++++++++ Cargo.toml | 28 + README.md | 378 + brand/reforger-mods-app-icon.png | Bin 0 -> 207449 bytes brand/reforger-mods-icon-dark.png | Bin 0 -> 96720 bytes crates/rfm-core/Cargo.toml | 44 + crates/rfm-core/src/api/client.rs | 576 ++ crates/rfm-core/src/api/error.rs | 151 + crates/rfm-core/src/api/mod.rs | 14 + crates/rfm-core/src/api/models.rs | 486 ++ crates/rfm-core/src/api/query.rs | 409 ++ crates/rfm-core/src/cache.rs | 164 + crates/rfm-core/src/identity.rs | 258 + crates/rfm-core/src/launch.rs | 500 ++ crates/rfm-core/src/lib.rs | 25 + crates/rfm-core/src/modscan.rs | 893 +++ crates/rfm-core/src/ping.rs | 569 ++ crates/rfm-core/src/presence.rs | 267 + crates/rfm-core/src/readiness.rs | 345 + crates/rfm-core/src/store.rs | 658 ++ .../tests/fixtures/error_not_found.json | 1 + .../rfm-core/tests/fixtures/ping_sites.json | 1 + .../tests/fixtures/server_detail.json | 1 + .../tests/fixtures/server_history.json | 1 + .../rfm-core/tests/fixtures/servers_page.json | 1 + crates/rfm-core/tests/live_api.rs | 554 ++ docs/api-contract.md | 240 + docs/client-identification.md | 248 + docs/discord-presence.md | 84 + docs/distribution.md | 342 + docs/mod-readiness.md | 200 + index.html | 12 + package-lock.json | 3227 +++++++++ package.json | 27 + packaging/arch/PKGBUILD | 94 + packaging/arch/reforgermods-launcher.desktop | 13 + packaging/windows/sign.cmd | 52 + rust-toolchain.toml | 3 + scripts/build-arch.sh | 123 + scripts/build-windows.sh | 102 + scripts/gen_icons.py | 187 + scripts/release-manifest.py | 183 + scripts/version.sh | 116 + src-tauri/Cargo.toml | 64 + src-tauri/build.rs | 3 + src-tauri/capabilities/main.json | 13 + src-tauri/icons/128x128.png | Bin 0 -> 13683 bytes src-tauri/icons/128x128@2x.png | Bin 0 -> 51649 bytes src-tauri/icons/32x32.png | Bin 0 -> 1533 bytes src-tauri/icons/icon.ico | Bin 0 -> 56704 bytes src-tauri/icons/icon.png | Bin 0 -> 215176 bytes src-tauri/src/commands.rs | 518 ++ src-tauri/src/lib.rs | 108 + src-tauri/src/main.rs | 108 + src-tauri/src/presence.rs | 163 + src-tauri/src/state.rs | 77 + src-tauri/src/update.rs | 106 + src-tauri/tauri.conf.json | 74 + src-tauri/tauri.signing.conf.json | 8 + src/app.test.tsx | 469 ++ src/app.tsx | 32 + src/assets/brand-mark.png | Bin 0 -> 2349 bytes src/components/FilterBar.tsx | 227 + src/components/HistoryChart.tsx | 74 + src/components/Inspector.tsx | 279 + src/components/PlatformBadges.tsx | 82 + src/components/ReadinessNote.tsx | 55 + src/components/ServerTable.tsx | 296 + src/components/Sidebar.tsx | 51 + src/components/StatusMarks.tsx | 90 + src/components/StatusPill.tsx | 84 + src/components/TitleBar.tsx | 98 + src/lib/chart.test.ts | 77 + src/lib/chart.ts | 82 + src/lib/format.test.ts | 323 + src/lib/format.ts | 199 + src/lib/ipc.ts | 129 + src/lib/sort.test.ts | 218 + src/lib/sort.ts | 161 + src/lib/state.ts | 586 ++ src/lib/types.ts | 300 + src/main.tsx | 13 + src/styles.css | 1319 ++++ src/views/BrowseView.tsx | 133 + src/views/ListView.tsx | 131 + src/views/SettingsView.tsx | 200 + tsconfig.json | 22 + vite.config.ts | 19 + vitest.config.ts | 11 + 91 files changed, 24994 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 brand/reforger-mods-app-icon.png create mode 100644 brand/reforger-mods-icon-dark.png create mode 100644 crates/rfm-core/Cargo.toml create mode 100644 crates/rfm-core/src/api/client.rs create mode 100644 crates/rfm-core/src/api/error.rs create mode 100644 crates/rfm-core/src/api/mod.rs create mode 100644 crates/rfm-core/src/api/models.rs create mode 100644 crates/rfm-core/src/api/query.rs create mode 100644 crates/rfm-core/src/cache.rs create mode 100644 crates/rfm-core/src/identity.rs create mode 100644 crates/rfm-core/src/launch.rs create mode 100644 crates/rfm-core/src/lib.rs create mode 100644 crates/rfm-core/src/modscan.rs create mode 100644 crates/rfm-core/src/ping.rs create mode 100644 crates/rfm-core/src/presence.rs create mode 100644 crates/rfm-core/src/readiness.rs create mode 100644 crates/rfm-core/src/store.rs create mode 100644 crates/rfm-core/tests/fixtures/error_not_found.json create mode 100644 crates/rfm-core/tests/fixtures/ping_sites.json create mode 100644 crates/rfm-core/tests/fixtures/server_detail.json create mode 100644 crates/rfm-core/tests/fixtures/server_history.json create mode 100644 crates/rfm-core/tests/fixtures/servers_page.json create mode 100644 crates/rfm-core/tests/live_api.rs create mode 100644 docs/api-contract.md create mode 100644 docs/client-identification.md create mode 100644 docs/discord-presence.md create mode 100644 docs/distribution.md create mode 100644 docs/mod-readiness.md create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 packaging/arch/PKGBUILD create mode 100644 packaging/arch/reforgermods-launcher.desktop create mode 100644 packaging/windows/sign.cmd create mode 100644 rust-toolchain.toml create mode 100755 scripts/build-arch.sh create mode 100755 scripts/build-windows.sh create mode 100644 scripts/gen_icons.py create mode 100755 scripts/release-manifest.py create mode 100755 scripts/version.sh create mode 100644 src-tauri/Cargo.toml create mode 100644 src-tauri/build.rs create mode 100644 src-tauri/capabilities/main.json create mode 100644 src-tauri/icons/128x128.png create mode 100644 src-tauri/icons/128x128@2x.png create mode 100644 src-tauri/icons/32x32.png create mode 100644 src-tauri/icons/icon.ico create mode 100644 src-tauri/icons/icon.png create mode 100644 src-tauri/src/commands.rs create mode 100644 src-tauri/src/lib.rs create mode 100644 src-tauri/src/main.rs create mode 100644 src-tauri/src/presence.rs create mode 100644 src-tauri/src/state.rs create mode 100644 src-tauri/src/update.rs create mode 100644 src-tauri/tauri.conf.json create mode 100644 src-tauri/tauri.signing.conf.json create mode 100644 src/app.test.tsx create mode 100644 src/app.tsx create mode 100644 src/assets/brand-mark.png create mode 100644 src/components/FilterBar.tsx create mode 100644 src/components/HistoryChart.tsx create mode 100644 src/components/Inspector.tsx create mode 100644 src/components/PlatformBadges.tsx create mode 100644 src/components/ReadinessNote.tsx create mode 100644 src/components/ServerTable.tsx create mode 100644 src/components/Sidebar.tsx create mode 100644 src/components/StatusMarks.tsx create mode 100644 src/components/StatusPill.tsx create mode 100644 src/components/TitleBar.tsx create mode 100644 src/lib/chart.test.ts create mode 100644 src/lib/chart.ts create mode 100644 src/lib/format.test.ts create mode 100644 src/lib/format.ts create mode 100644 src/lib/ipc.ts create mode 100644 src/lib/sort.test.ts create mode 100644 src/lib/sort.ts create mode 100644 src/lib/state.ts create mode 100644 src/lib/types.ts create mode 100644 src/main.tsx create mode 100644 src/styles.css create mode 100644 src/views/BrowseView.tsx create mode 100644 src/views/ListView.tsx create mode 100644 src/views/SettingsView.tsx create mode 100644 tsconfig.json create mode 100644 vite.config.ts create mode 100644 vitest.config.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1522b8d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,266 @@ +# Build, sign and publish a launcher release. +# +# Triggered by a version tag. The tag is the instruction: the job refuses to +# build unless the tag and every version in the tree already agree, because an +# updater manifest that disagrees with the installed version either loops +# forever or never fires (docs/distribution.md section 6). +# +# The Windows build runs here rather than cross-compiled from Linux, even though +# scripts/build-windows.sh works: NSIS bundling and Authenticode signing both +# want Windows tooling, and the signing credential should live in one place that +# is not a laptop. +# +# Stored under .github/workflows so both GitHub Actions and Gitea Actions pick it +# up, matching the other two repositories. +name: Release + +on: + push: + tags: ['v*'] + # Reruns of a published tag, for when a publish step failed and the artifacts + # are fine. Builds only; nothing is published without a tag. + workflow_dispatch: + inputs: + version: + description: 'Version to build, without the leading v (e.g. 0.1.0)' + required: true + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + windows: + name: Windows installer + runs-on: windows-latest + # A protected environment: put the signing and R2 credentials on it, and add + # required reviewers so publishing a release needs a human. + environment: release + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: '. -> target' + + # The tag is the source of truth for what is being released; the tree has + # to already say the same thing. + - name: Check the version agrees with the tag + id: version + shell: bash + run: | + tag="${{ inputs.version || github.ref_name }}" + scripts/version.sh check "$tag" + echo "version=$(scripts/version.sh show)" >> "$GITHUB_OUTPUT" + + - run: npm ci + + # Re-validated here rather than trusting that CI ran on this commit: a + # release is the one build that must not ship a known-broken tree. + - name: Checks + run: | + npm run check + npm test + cargo fmt --all -- --check + cargo clippy --workspace --all-targets -- -D warnings + cargo test -p rfm-core + + # Signing is optional so the pipeline works before Azure Trusted Signing is + # provisioned. When the secrets are absent the build is simply unsigned, and + # the summary says so — rather than the whole release failing, or worse, + # silently producing an unsigned installer that the notes call signed. + - name: Decide whether artifacts can be signed + id: signing + shell: bash + env: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + RFM_SIGN_ENDPOINT: ${{ vars.RFM_SIGN_ENDPOINT }} + run: | + if [ -n "$AZURE_CLIENT_ID" ] && [ -n "$RFM_SIGN_ENDPOINT" ]; then + echo "enabled=true" >> "$GITHUB_OUTPUT" + else + echo "enabled=false" >> "$GITHUB_OUTPUT" + echo "::warning::Azure Trusted Signing is not configured; this release will be UNSIGNED and will show the SmartScreen warning." + fi + + - name: Install the signing tool + if: steps.signing.outputs.enabled == 'true' + run: cargo install trusted-signing-cli --locked + + # TAURI_SIGNING_PRIVATE_KEY is the updater key, which is a different thing + # from Authenticode code signing: it proves an update came from this project, + # while Authenticode is what stops SmartScreen calling the download unknown. + # Both are needed and they solve different problems. + - name: Build + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + RFM_SIGN_ENDPOINT: ${{ vars.RFM_SIGN_ENDPOINT }} + RFM_SIGN_ACCOUNT: ${{ vars.RFM_SIGN_ACCOUNT }} + RFM_SIGN_PROFILE: ${{ vars.RFM_SIGN_PROFILE }} + shell: bash + run: | + if [ "${{ steps.signing.outputs.enabled }}" = "true" ]; then + # The overlay adds only bundle.windows.signCommand, so Tauri signs each + # artifact during bundling and the updater zip contains a signed + # installer. + npm run tauri -- build --config src-tauri/tauri.signing.conf.json + else + npm run tauri -- build + fi + + # The portable exe is the same binary the installer wraps, offered for people + # who will not run installers. It costs nothing to publish. + - name: Collect artifacts + shell: bash + run: | + set -euo pipefail + version="${{ steps.version.outputs.version }}" + mkdir -p dist/release + nsis="target/release/bundle/nsis" + cp "$nsis"/*-setup.exe "dist/release/reforgermods-launcher-${version}-setup.exe" + cp "target/release/reforgermods-launcher.exe" "dist/release/reforgermods-launcher-${version}.exe" + # The NSIS bundle is signed in place, so the updater artifact is the + # installer itself plus a detached .sig. It exists only when the signing + # key was available; an unsigned release simply has no updater entry. + for f in "$nsis"/*-setup.exe.sig; do + [ -e "$f" ] || continue + cp "$f" "dist/release/reforgermods-launcher-${version}-setup.exe.sig" + done + ls -l dist/release + + # Uploaded whether or not publishing happens, so a failed publish never + # means rebuilding. + - uses: actions/upload-artifact@v4 + with: + name: windows-${{ steps.version.outputs.version }} + path: dist/release/ + if-no-files-found: error + retention-days: 14 + + # The updater key is read through env rather than interpolated into the + # script: `${{ secrets.X }}` in a run block writes the secret into the shell + # command itself, and only the log is masked. + - name: Summary + shell: bash + env: + HAS_UPDATER_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY != '' }} + SIGNED: ${{ steps.signing.outputs.enabled }} + VERSION: ${{ steps.version.outputs.version }} + run: | + { + echo "### Launcher $VERSION" + if [ "$SIGNED" = "true" ]; then + echo "- Authenticode: **signed**" + else + echo "- Authenticode: **UNSIGNED** — expect the SmartScreen warning" + fi + if [ "$HAS_UPDATER_KEY" = "true" ]; then + echo "- Updater: artifacts signed, \`latest.json\` will be published" + else + echo "- Updater: **no key configured**, no updater artifacts produced" + fi + } >> "$GITHUB_STEP_SUMMARY" + + publish: + name: Publish to R2 + needs: windows + runs-on: ubuntu-latest + # Only a tag publishes. A manual run builds and stops, so it can be used to + # check a release without touching what is live. + if: startsWith(github.ref, 'refs/tags/v') + environment: release + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: windows-${{ needs.windows.outputs.version }} + path: dist/release + + # The Arch package is not built here: it needs an Arch userspace (see + # scripts/build-arch.sh, which runs one in a container) and is handed over + # per tester rather than published. Drop it into dist/release before the + # manifest step if that changes. + - name: Build the manifests + run: | + python3 scripts/release-manifest.py \ + --version "${{ needs.windows.outputs.version }}" \ + --artifacts dist/release \ + --base-url "https://dl.reforgermods.net" \ + --out dist/release + + - name: Checksums + working-directory: dist/release + run: | + sha256sum -- * > SHA256SUMS || true + # The manifests are generated from the artifacts, so they must not be + # listed as if they were part of the signed set. + grep -v -E '(releases|latest)\.json|SHA256SUMS' SHA256SUMS > sums.tmp + mv sums.tmp SHA256SUMS + cat SHA256SUMS + + # dl.reforgermods.net is an R2 bucket with a custom domain. Versioned paths + # are immutable; only /updates/latest.json and /releases.json move, which is + # what makes a rollback a matter of re-publishing one small file. + - name: Publish + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + BUCKET: reforgermods-launcher-downloads + run: | + set -euo pipefail + version="${{ needs.windows.outputs.version }}" + put() { + npx --yes wrangler@4 r2 object put "$BUCKET/$1" --file "$2" --content-type "$3" --remote + } + for file in dist/release/*; do + name="$(basename "$file")" + case "$name" in + releases.json|latest.json) continue ;; + *.exe) type="application/vnd.microsoft.portable-executable" ;; + *.zip) type="application/zip" ;; + *.zst) type="application/zstd" ;; + *) type="text/plain; charset=utf-8" ;; + esac + put "$version/$name" "$file" "$type" + done + # Last, and in this order: the updater manifest only starts pointing at a + # release once every file it names is already in place. + put "updates/latest.json" dist/release/latest.json "application/json" + put "releases.json" dist/release/releases.json "application/json" + + - name: Next step + run: | + { + echo "### Published ${{ needs.windows.outputs.version }}" + echo "Artifacts are live under \`https://dl.reforgermods.net/${{ needs.windows.outputs.version }}/\`." + echo "" + echo "The website's download panel reads its own copy of the manifest." + echo "Copy \`releases.json\` from this run's artifacts into" + echo "\`public/static/launcher/releases.json\` in reforgermods-web and deploy," + echo "or the page will keep offering the previous release." + } >> "$GITHUB_STEP_SUMMARY" + + - uses: actions/upload-artifact@v4 + with: + name: manifests-${{ needs.windows.outputs.version }} + path: | + dist/release/releases.json + dist/release/latest.json + dist/release/SHA256SUMS diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..461da8a --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +/node_modules +/dist +/target +/src-tauri/target +*.local +.DS_Store +.env +.env.local + +# Generated by tauri-build at compile time. +/src-tauri/gen/schemas diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..dc084b3 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,6138 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[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_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[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-compression" +version = "0.4.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb61aea1a7def73ee7c350a184f0e70b32c182344e2e75bf70c9b621b83417fd" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + +[[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.119", +] + +[[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-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[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 = "aws-lc-rs" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[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 = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[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 = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +dependencies = [ + "serde_core", +] + +[[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 = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[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 = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.2", + "cairo-sys-rs", + "glib", + "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", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591" +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.21", +] + +[[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 = "cc" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" +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 1.26.1", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "compression-codecs" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef16c47ba2797aa6a909cc37d39911f3a6743811fe7408ac0b0cc0276b656e9" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e8ccc4ea9f6acc32d102c0f6d471d11d913ad15f20c04de743374861fa1d414" + +[[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.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +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.2", + "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.2", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[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", + "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.119", +] + +[[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 = "darling" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.6", +] + +[[package]] +name = "darling_macro" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" +dependencies = [ + "darling_core", + "quote", + "syn 3.0.6", +] + +[[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 = "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.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.21", +] + +[[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_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.119", +] + +[[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", +] + +[[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", + "crypto-common 0.2.2", +] + +[[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 = "discord-rich-presence" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90c55d69cab17c19677ce3a5f8face993a9e6eaf847fecac3547f3a3ff4a2494" +dependencies = [ + "log", + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "thiserror 2.0.21", + "uuid 0.8.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[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.119", +] + +[[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", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[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 = "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.6+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.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[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.119", +] + +[[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 = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "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 = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[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.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[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 = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[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.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "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", + "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", + "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", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[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", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[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", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[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", + "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", + "libc", + "system-deps", + "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 = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[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", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core", + "wasm-bindgen", +] + +[[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", + "glib", + "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", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[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.119", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[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", + "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", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[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.119", +] + +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.14.2", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +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 = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +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-service", + "tracing", + "windows-registry", +] + +[[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.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +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 = "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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +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", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[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 = "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", + "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", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jiff" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link 0.2.1", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +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" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.21", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[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.119", +] + +[[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.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +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.2", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "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 = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[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 = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[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.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[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 = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[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 = "minisign-verify" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" + +[[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 = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[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.21", + "windows-sys 0.61.2", +] + +[[package]] +name = "multiversion" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" +dependencies = [ + "multiversion-macros", +] + +[[package]] +name = "multiversion-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn 3.0.6", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[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.2", + "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 = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[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.119", +] + +[[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.2", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[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.2", + "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 = [ + "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.2", + "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.2", + "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.2", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[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.2", + "block2", + "libc", + "objc2", + "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.2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-osa-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-app-kit", + "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.2", + "objc2", + "objc2-core-foundation", + "objc2-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.2", + "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.2", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc" +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.2", + "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.119", +] + +[[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-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 = "osakit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" +dependencies = [ + "objc2", + "objc2-foundation", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "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", + "gobject-sys", + "libc", + "system-deps", +] + +[[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 = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "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", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[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", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[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 = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plist" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896bade328c13f7042a297ea5ac5b0951f6cf989dea5f32c2fd98da398195cb" +dependencies = [ + "base64 0.23.1", + "indexmap 2.14.2", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "pnet_base" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" +dependencies = [ + "no-std-net", +] + +[[package]] +name = "pnet_macros" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13325ac86ee1a80a480b0bc8e3d30c25d133616112bb16e86f712dcf8a71c863" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.119", +] + +[[package]] +name = "pnet_macros_support" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed67a952585d509dd0003049b1fc56b982ac665c8299b124b90ea2bdb3134ab" +dependencies = [ + "pnet_base", +] + +[[package]] +name = "pnet_packet" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c96ebadfab635fcc23036ba30a7d33a80c39e8461b8bd7dc7bb186acb96560f" +dependencies = [ + "glob", + "pnet_base", + "pnet_macros", + "pnet_macros_support", +] + +[[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 0.8.9", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[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 = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[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.15+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.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.21", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.21", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +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 = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.21", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "reforgermods-launcher" +version = "0.1.0" +dependencies = [ + "discord-rich-presence", + "rfm-core", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-opener", + "tauri-plugin-updater", + "tokio", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.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", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfm-core" +version = "0.1.0" +dependencies = [ + "base64 0.23.1", + "reqwest", + "serde", + "serde_json", + "sha2 0.11.0", + "surge-ping", + "thiserror 2.0.21", + "tokio", + "uuid 1.26.1", + "winping", + "winreg 0.56.0", +] + +[[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 = "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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni 0.22.4", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "aws-lc-rs", + "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 = "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 1.26.1", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +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.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "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.2", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "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 = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +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.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[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.119", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[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.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "935177bb8c0cd8ca1a4e6d1a2ac8988bea69cab4f9d3a31311e012ad27868ea4" +dependencies = [ + "base64 0.23.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.2", + "jiff", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d607aa01a3cb0ad757d6fd216136910db3c97b102fe686585689615a02dbcdc" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[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.119", +] + +[[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 = "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.1", + "digest 0.11.3", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[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 = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[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 = "smallvec" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +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", + "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", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[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", + "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", + "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 = "surge-ping" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cdc709585dead30bc08dafb5b25fee3d1a9d5e81e4eff43e21f609476b64a50" +dependencies = [ + "hex", + "parking_lot", + "pnet_packet", + "rand", + "socket2", + "thiserror 2.0.21", + "tokio", + "tracing", +] + +[[package]] +name = "swift-rs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e" +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", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +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.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.13.2", + "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", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "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.2", + "block2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni 0.21.1", + "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", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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 = "tauri" +version = "2.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa5bacdb9bbad5954af3d1bd6cf6ae9192cab1b2e270f4a07f904610b9e85f4" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni 0.21.1", + "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.21", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[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.119", + "tauri-utils", + "thiserror 2.0.21", + "time", + "url", + "uuid 1.26.1", + "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.119", + "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-opener" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d60366174b745b4ef5824b8bbc1c457fd08f0ce101ff643c0a49181a9f4e91" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.21", + "url", + "windows", + "zbus", +] + +[[package]] +name = "tauri-plugin-updater" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5cad8ed5948d988e1018ecd31e27cacedbf72ce3fb972940c3e4bf51639e4b" +dependencies = [ + "base64 0.22.1", + "dirs", + "flate2", + "futures-util", + "http", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.21", + "time", + "tokio", + "url", + "windows-sys 0.60.2", + "zip", +] + +[[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 0.21.1", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.21", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[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 0.21.1", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "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", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.21", + "toml 1.1.6+spec-1.1.0", + "url", + "urlpattern", + "uuid 1.26.1", + "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.6+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.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[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.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "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.2", + "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.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" +dependencies = [ + "indexmap 2.14.2", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[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.2", + "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.2", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.15+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +dependencies = [ + "indexmap 2.14.2", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[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", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "async-compression", + "bitflags 2.13.2", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tokio", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.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 = [ + "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.119", +] + +[[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 = "tray-icon" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e" +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.21", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[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 = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[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 = "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 = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[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.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +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 = "web-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" +dependencies = [ + "phf", + "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", + "glib", + "glib-sys", + "gobject-sys", + "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", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[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", + "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.119", +] + +[[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.21", + "windows", + "windows-core 0.61.2", +] + +[[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 = "winapi_forked_icmpapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42aecb895d6340af9ccc8dab9aeabfeab6d5d7266c5fd172c8be7e07db71c1e3" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[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", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[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-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", +] + +[[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.119", +] + +[[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.119", +] + +[[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-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-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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winping" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79ed0e3a789beb896b3de9fb7e93c76340f6f4adfab7770d6222b4b8625ef0aa" +dependencies = [ + "lazy_static", + "static_assertions", + "winapi_forked_icmpapi", +] + +[[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 = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[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 0.21.1", + "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.21", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[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 = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" +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 1.26.1", + "windows-sys 0.61.2", + "winnow 1.0.4", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 3.0.6", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow 1.0.4", + "zvariant", +] + +[[package]] +name = "zcheapstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.14.2", + "memchr", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zvariant" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.4", + "zcheapstr", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 3.0.6", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.6", + "winnow 1.0.4", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3b3d2a5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,28 @@ +[workspace] +members = ["crates/rfm-core", "src-tauri"] +resolver = "2" + +[workspace.package] +version = "0.1.0" +edition = "2021" +rust-version = "1.82" +license = "Proprietary" +publish = false + +[workspace.dependencies] +serde = { version = "1.0.229", features = ["derive"] } +serde_json = "1.0.151" +thiserror = "2.0.21" +# TLS backend is selected per target in crates/rfm-core/Cargo.toml. +reqwest = { version = "0.13.5", default-features = false, features = ["json", "query", "gzip", "charset", "http2"] } +tokio = { version = "1.53.1", default-features = false, features = ["time", "sync", "rt", "net"] } +base64 = "0.23.1" +uuid = { version = "1.26.1", default-features = false, features = ["v4", "std"] } +sha2 = "0.11.0" + +[profile.release] +codegen-units = 1 +lto = true +opt-level = "s" +panic = "abort" +strip = true diff --git a/README.md b/README.md new file mode 100644 index 0000000..c689779 --- /dev/null +++ b/README.md @@ -0,0 +1,378 @@ +# reforgermods.net launcher + +A fast, lightweight desktop companion for **Arma Reforger**, built on the +[reforgermods.net](https://reforgermods.net) API. It is a live server browser +that can put you straight into a server through Steam, without going through the +in-game lobby. + +Unofficial. Not affiliated with or endorsed by Bohemia Interactive. + +## Status + +**0.1.0 — public beta.** The launcher browses and filters the live server list, +shows full detail for a selected server, joins directly through Steam, and keeps +favorites, recents and settings across restarts. Milestone 2 added population +history charts, a scenario filter, and mod readiness as reporting only; 0.1.0 +adds packaging, signed auto-updates and a download page. + +Beta artifacts are a per-user NSIS installer, a portable `.exe` and an Arch +package, published to `dl.reforgermods.net` and offered from +[reforgermods.net/launcher/](https://reforgermods.net/launcher/). They are **not +Authenticode-signed yet**, so Windows shows a SmartScreen warning on first run — +the pipeline is wired for Azure Trusted Signing and waiting on the identity. +See [`docs/distribution.md`](docs/distribution.md). + +What works today: + +- Live server browser over `GET /v2/servers`, with search, sort, region, + platform, official / modded / BattlEye / password / offline filters, and + page-local scenario and minimum-player filters +- **Sortable columns.** Clicking a heading sorts by it. `players` and `name` are + ordered by the API across every matching server; queue, ping, mods, region and + any reversed direction are orderings the API does not offer, so they reorder + the loaded page — marked with a hollow arrow and named in the footer, never + presented as a search of the whole fleet +- Selected-server inspector: scenario, population, queue, region, server FPS, + and the full required-mod list with versions and sizes +- **Population history chart** over 6h / 24h / 7d / 30d, drawn as inline SVG with + no charting library +- **Mod readiness, reporting only**: how many required mods are installed, + outdated or missing, and at least how much is left to download +- **Measured ping** to the selected server by ICMP — a real round trip, not a + region estimate +- Scenario imagery from the Bohemia CDN, host-allowlisted and CSP-enforced +- **Region latency in the browser**, sortable — eight ICMP probes cover a whole page +- Per-platform marks (PC / Xbox / PlayStation) matching the site, not a crossplay flag +- Fleet-wide player total in the title bar +- **Join Discord** where a server advertises an invite in its name +- **Discord Rich Presence**, showing the launcher as the running application + (needs a Discord application; see [`docs/discord-presence.md`](docs/discord-presence.md)) +- **Direct join**: `steam://run/1874880///` +- Favorites, recent servers and launch history, persisted locally +- Settings, and an anonymous, user-resettable install identifier +- **Background updates on Windows**, signature-verified, downloaded quietly and + applied as the app closes — never mid-session. Linux installs are the distro's + to update +- Short-lived response caching, and loading / empty / error / offline states +- Compact API status in the header, with dataset freshness + +Not implemented, on purpose: + +- **"Prepare mods & join".** Readiness is *reported*, never acted on. There is no + verified way to install Reforger Workshop content from outside the game, so the + launcher says what the game will do instead of offering a button it cannot + honour. See [`docs/mod-readiness.md`](docs/mod-readiness.md). +- Accounts, uptime figures, mod browsing, background tray, auto-start. + +## Stack + +| Layer | Choice | Why | +| --- | --- | --- | +| Shell | Tauri 2 | Native WebView2 on Windows; no bundled browser runtime | +| Native | Rust | Steam launching, HTTP, persistence, local filesystem inspection | +| UI | Preact + `@preact/signals` + TypeScript | ~4 KB runtime, JSX in plain `.tsx`, and per-component signal subscriptions so the one-second freshness clock re-renders the status pill and not the 500-row table | +| Build | Vite 7 | | + +The production bundle is **51 KB of JavaScript (17 KB gzipped)** and **14 KB of +CSS**, in one chunk. The history chart is hand-rolled inline SVG — a charting +library would have cost more than the rest of the UI combined. + +### Layout + +```text +crates/rfm-core/ Pure Rust. No Tauri dependency, so it is unit-testable directly. + api/ HTTP client, typed models, query builder, error taxonomy + launch.rs Server id validation -> join payload -> Steam URI + store.rs Settings, favorites, recents, launch history + cache.rs Bounded TTL cache of response bodies + modscan.rs Local Workshop addon detection + readiness.rs Required mods vs installed; reporting only, never acts + ping.rs ICMP latency; address validation is the security boundary + presence.rs Discord Rich Presence text, within Discord's limits + +src-tauri/ The application shell. Window, commands, wiring. Thin. + src/commands.rs The entire IPC surface + src/state.rs Shared state + src/update.rs Background updates, Windows only; applied as the app exits + capabilities/main.json + tauri.signing.conf.json Adds only signCommand; merged over the config in CI + +src/ UI + lib/ types, IPC wrappers, signals, pure formatters, chart geometry + components/ title bar, status pill, sidebar, filter bar, server table, + inspector, history chart, readiness note, platform badges + views/ browse, favorites, recent, settings + +docs/api-contract.md Endpoints used, and what is deliberately not used +docs/mod-readiness.md The mod-readiness investigation and its findings +docs/client-identification.md Install-identity spec and API-side implementation notes +docs/discord-presence.md Discord Rich Presence setup and behaviour +docs/distribution.md Installer, code signing, release procedure, R2 layout +packaging/arch/ PKGBUILD and desktop entry for the Arch package +packaging/windows/sign.cmd Authenticode signing, called by Tauri during bundling + +scripts/version.sh The version, in one place: show / check / set +scripts/release-manifest.py releases.json (the site) and latest.json (the updater) +scripts/build-windows.sh Cross-compile the bare .exe from Linux +scripts/build-arch.sh Native Arch package, built in a container +.github/workflows/release.yml Tag -> build, sign, publish to R2 +``` + +## Prerequisites + +- **Rust** stable (1.82+) and **Node.js** 20+ +- **Windows** (the primary target): the WebView2 runtime, which ships with + Windows 11 and current Windows 10, plus the MSVC build tools +- **Linux** (development, and Arch packaging): the Tauri system dependencies — + `libwebkit2gtk-4.1-dev`, `libgtk-3-dev`, `libayatana-appindicator3-dev`, + `librsvg2-dev`, `libsoup-3.0-dev`, `build-essential` + +The TLS backend is chosen per target: schannel on Windows (no bundled C crypto +library, and the machine's own trust store), rustls elsewhere. + +Installed-mod detection finds Reforger's addon store on both platforms: the +(often redirected) `Documents` directory on Windows, read from the shell folder +registry, and the Proton prefix inside each Steam library on Linux — there is no +native Linux client, so the game's `Documents` is +`/steamapps/compatdata/1874880/pfx/drive_c/users/steamuser/Documents`. +`RFM_ADDONS_DIR` overrides the search for an install neither layout describes. + +## Development + +```sh +npm install +npm run tauri dev # run the app +``` + +Frontend alone, in a browser (IPC calls will fail, but layout iterates fast): + +```sh +npm run dev +``` + +## Checks + +```sh +cargo fmt --all -- --check +cargo clippy --workspace --all-targets -- -D warnings +cargo test -p rfm-core # 168 unit tests, no network +npm run check # tsc --noEmit +npm test # 82 tests, including a jsdom shell smoke test +scripts/version.sh check # the four version copies agree +``` + +Contract checks against the live API, ignored by default: + +```sh +# Serially: the public rate limit is 60 requests/minute, and these tests +# fetch server detail in loops. +cargo test -p rfm-core --test live_api -- --ignored --test-threads=1 +``` + +To exercise the mod scanner against a real Arma Reforger addon store: + +```sh +RFM_ADDONS_DIR="$HOME/Documents/My Games/ArmaReforger/addons" \ + cargo test -p rfm-core --test live_api -- --ignored readiness_against_a_real +``` + +The ping tests send ICMP echo requests to live game servers; they are part of the +same ignored-by-default suite. + +## Build + +```sh +npm run tauri build +``` + +Produces a per-user NSIS installer under `target/release/bundle/nsis/`, plus a +detached `.sig` when an updater signing key is present in the environment. MSI is +deliberately not built: it needs admin rights and doubles the release for an +audience a game utility does not have. + +Releases are cut from a `v*` tag by `.github/workflows/release.yml`, which builds +on Windows, signs, and publishes to `dl.reforgermods.net`. The version lives in +one place and is propagated by `scripts/version.sh`: + +```sh +scripts/version.sh show # the canonical version +scripts/version.sh set 0.1.1 # rewrite all four copies +``` + +The full procedure, the signing state and the R2 layout are in +[`docs/distribution.md`](docs/distribution.md). + +### Cross-building a Windows binary from Linux + +No Visual Studio, and no root: + +```sh +scripts/build-windows.sh # debug, shows a console with diagnostics +scripts/build-windows.sh --release # optimised, no console +``` + +`cargo-xwin` fetches the MSVC CRT and Windows SDK from Microsoft into +`~/.cache/cargo-xwin`, and LLVM's `clang-cl`/`lld-link` replace `cl.exe` and +`link.exe`; the script unpacks those from Debian packages into a local prefix. +The result is a standalone `.exe` — the frontend is embedded at compile time — +that needs only the WebView2 runtime, which ships with current Windows. + +### Building an Arch Linux package + +```sh +scripts/build-arch.sh # target/arch/*.pkg.tar.zst +scripts/build-arch.sh --no-check # skip the rfm-core test suite +``` + +The build runs inside an `archlinux:base-devel` container, so the binary links +Arch's own glibc and WebKit rather than the build machine's. This is not +fussiness: a Tauri binary built on Debian links `libwebkit2gtk-4.1` against +Debian's ABI, and handing that to an Arch user produces a blank window or a +missing-symbol crash. Docker is only being used as a clean Arch userspace — +nothing is deployed. + +The tester installs it with `sudo pacman -U `; pacman pulls +`webkit2gtk-4.1` and the rest from the declared dependencies. + +Two things the PKGBUILD has to work around, both of which fail late and +confusingly if forgotten: + +- `options=('!lto')` — makepkg puts `-flto=auto` in `CFLAGS`/`LDFLAGS` by + default, which makes `aws-lc-sys` (the C crypto behind rustls) compile to GCC + LTO bitcode that the linker cannot resolve. The build dies at the final link + with a page of undefined `aws_lc_*` symbols. Rust-level LTO is unaffected; it + comes from `[profile.release]`. +- `cmake` in `makedepends` — `aws-lc-sys` needs it, and `base-devel` does not + include it. + +`build()` asserts the result is a production build before packaging, for the +same reason `build-windows.sh` does: a dev build installs perfectly and then +shows `ERR_CONNECTION_REFUSED` on the tester's machine. + +### Nvidia under Wayland + +WebKitGTK's DMABUF renderer and the proprietary Nvidia driver disagree about +buffer import, and GTK aborts before the window is ever mapped: + +```text +Gdk-Message: Error 71 (Protocol error) dispatching to Wayland display. +``` + +`main.rs` detects that combination at startup and sets +`WEBKIT_DISABLE_DMABUF_RENDERER=1` for itself. The detection is deliberately +narrow — Nvidia loaded, a Wayland session, GDK not already pinned to X11, and no +existing value to override — because the fallback costs GPU acceleration for +anyone who does not need it. `should_disable_dmabuf` is a pure function with +tests covering each condition. + +To override, set the variable yourself; any value is honoured, including `0`. + +## Direct joining + +Given a server UUID, the launcher builds: + +```text +{"id":"","inviteToken":""} + -> UTF-8 bytes + -> standard base64 + -> trailing "=" removed + -> steam://run/1874880/// +``` + +### The doubled slash + +`steam://run////` is Steam's form for passing launch arguments, and +the **empty path segment** is what marks the payload as one. With a single +slash Steam can start the game without forwarding the payload, so the player +lands on the main menu and the join request is silently lost. A test pins this +shape, because the difference is one character and the failure looks like a +successful launch. + +**Not yet confirmed against a live client.** The single-slash form was observed +to launch without joining; the doubled slash is the current best understanding +of the fix. + +A join only carries the payload when the game actually starts. If Arma Reforger +is already running, Steam may focus the existing process and drop the arguments, +so this must be tested from a fully closed game. + +`steam://run/1874880` (no payload) is the plain "Launch Arma Reforger" action and +is unaffected. + +### An alternative route + +The game also registers its own URL protocol. Its Steam install script +(`steamapps/common/Arma Reforger/installscript.vdf`) writes: + +```text +HKCU\SOFTWARE\Classes\ArmaReforger\shell\open\command + "\ArmaReforger_BE.exe" -exe ArmaReforgerSteam.exe + "-addonsDir=\addons" -uri="%1" +``` + +`reforger_join_uri()` builds `ArmaReforger://`, which reaches the +launcher as `-uri=` without depending on how Steam forwards arguments. It is +implemented, tested and unused — a second candidate if the Steam form does not +join. + +This lives in [`crates/rfm-core/src/launch.rs`](crates/rfm-core/src/launch.rs) +and is pure and directly tested, including against the verified reference +vector. No shell is involved at any point: the URI is handed to the platform's +URI opener, and the only value interpolated into it is a `ServerId`, which cannot +be constructed without passing UUID validation. + +## Ping + +A browser cannot measure round-trip latency to a game server; a native launcher +can, and this is one of the reasons for building one. + +**TCP connect timing was tried and rejected.** Connecting to a server's UDP game +port "succeeded" in 12–18 ms — and so did port 9 and port 47123, which are +closed. Some middlebox completes every handshake, so TCP timing yields +confident-looking numbers that are entirely fictional. + +ICMP echo works. Measured against 12 live servers, 11 answered, with latencies +coherent by region (Los Angeles 41 ms, New York 58–83 ms, Frankfurt 154–184 ms, +Sydney 173–179 ms). The launcher sends three probes and displays the **median**, +so one delayed packet does not move the figure. + +- Windows uses `IcmpSendEcho` via the IP Helper API — **no elevation required**. +- Unix uses an unprivileged ICMP datagram socket. +- The **selected** server is measured exactly, in the inspector. +- The **browser column** shows latency to each server's *region*. `/v2/servers` + publishes no per-server address — only `/v2/servers/{id}` does — so pinging a + 100-row page would cost 100 detail requests against a 60/minute limit. Every + row does carry `pingSiteId`, and there are only eight sites, so eight probes + cover the list. Region and server latency agree within roughly 20 ms, which is + enough to sort by and is what the game's own browser shows. +- Roughly a third of servers filter ICMP. Those show "no reply" rather than a + region-level estimate dressed up as a measurement. + +## Security posture + +- Everything from the API — server names, scenario names, mod names, URLs — is + treated as untrusted. It is rendered as text content and never interpolated + into a shell command, a URI, or markup. +- Server ids are validated before they reach a URL, on both the launch path and + the "open on reforgermods.net" path. The latter builds its URL from the + validated id rather than from the API's own `modURL` field. +- No API key or credential is embedded. The launcher is a public client and uses + anonymous access only. The one value it sends that persists across requests is + a random install identifier, visible and resettable in Settings; see + [`docs/client-identification.md`](docs/client-identification.md). +- The API origin is not user-configurable, so a tampered settings file cannot + redirect requests — or the install identifier — to another host. +- Server addresses from the API are validated to public literal IPs before any + packet is sent, so remote data cannot drive DNS or probe the user's LAN. +- Image URLs are checked against a single-host allowlist before rendering, with + the page CSP enforcing the same list independently. +- The UI is granted `core:default` and nothing else — no filesystem, shell, HTTP + or opener permission. URIs are opened from Rust, after validation. +- A strict CSP allows `'self'` plus scenario thumbnails from the Bohemia CDN. + +## Relationship to reforgermods.net + +This is a client. All server and mod data comes from the public +`https://api.reforgermods.net` v2 API; the launcher holds no data of its own +beyond local preferences and caches, and duplicates no backend logic. See +[`docs/api-contract.md`](docs/api-contract.md). diff --git a/brand/reforger-mods-app-icon.png b/brand/reforger-mods-app-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a2e82c0cc04fffd95cfa1860f13b99b1068e6f1 GIT binary patch literal 207449 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

kO{S!*wTY@Ryg{3@QHBEv_Dy* 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, + cache: Arc, +} + +/// The API origin this build talks to: production, unless `RFM_API_BASE_URL` +/// overrides it for development. +pub fn configured_base_url() -> String { + std::env::var(BASE_URL_ENV) + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_BASE_URL.to_string()) +} + +impl ApiClient { + /// Builds a client for the configured origin, identifying as `install_id`. + /// + /// `install_id` is the anonymous per-install identifier from + /// [`crate::store::Store::install_id`]. An empty or malformed value is + /// simply not sent, so a corrupted state file degrades to an unidentified + /// client rather than sending garbage. + pub fn new(install_id: &str) -> Result { + Self::with_base_url(&configured_base_url(), install_id) + } + + /// Builds a client against an explicit origin. Tests and the dev override + /// use this; product code calls [`ApiClient::new`]. + pub fn with_base_url(base_url: &str, install_id: &str) -> Result { + let base_url = normalise_base_url(base_url)?; + + let mut headers = reqwest::header::HeaderMap::new(); + if let Some(value) = install_id_header_value(install_id) { + headers.insert(INSTALL_ID_HEADER, value); + } + + let builder = reqwest::Client::builder().default_headers(headers); + // On Windows the TLS backend is schannel, which uses the machine's own + // trust store. See the target-specific reqwest features in Cargo.toml. + #[cfg(windows)] + let builder = builder.use_native_tls(); + + let http = builder + .user_agent(crate::user_agent()) + .timeout(REQUEST_TIMEOUT) + .connect_timeout(CONNECT_TIMEOUT) + // The launcher makes a handful of requests to one host; a small pool + // kept alive is what keeps idle resource use low. + .pool_max_idle_per_host(4) + .build() + .map_err(ApiError::Build)?; + Ok(Self { + http, + base_url: base_url.into(), + cache: Arc::new(ResponseCache::default()), + }) + } + + /// The base URL this client targets. + pub fn base_url(&self) -> &str { + &self.base_url + } + + /// Drops every cached response. Called when settings change the base URL. + pub fn clear_cache(&self) { + self.cache.clear(); + } + + /// `GET /v2/servers` with the given filters applied. + /// + /// Dropping the returned future cancels the request; Tauri does this when a + /// command is aborted, which is how a superseded search stops in flight. + pub async fn servers(&self, query: &ServerQuery) -> Result { + let path = "/v2/servers"; + let body = self + .get_text(path, &query.to_params(), Ttl::SNAPSHOT) + .await?; + decode(&body) + } + + /// `GET /v2/servers/{id}`. The id is validated before it reaches the URL. + pub async fn server(&self, id: &ServerId) -> Result { + let path = format!("/v2/servers/{id}"); + let body = self.get_text(&path, &[], Ttl::SNAPSHOT).await?; + decode(&body) + } + + /// `GET /v2/servers/{id}/history` for one range. + /// + /// Returns an empty `points` array for a server with no recorded history, + /// which is a normal state and not an error. + pub async fn server_history( + &self, + id: &ServerId, + range: HistoryRange, + ) -> Result { + let path = format!("/v2/servers/{id}/history"); + let params = [("range", range.as_param().to_string())]; + let body = self.get_text(&path, ¶ms, Ttl::HISTORY).await?; + decode(&body) + } + + /// Fleet-wide population, for the header's player total. + /// + /// The endpoint also returns a full time series; the launcher reads only the + /// `now` block. The narrowest range is requested to keep the response small. + pub async fn fleet_now(&self) -> Result { + let body = self + .get_text( + "/v2/servers/history", + &[("range", "6h".to_string())], + Ttl::HISTORY, + ) + .await?; + decode(&body) + } + + /// `GET /v2/servers/ping-sites`. Used to label regions; cached for an hour. + pub async fn ping_sites(&self) -> Result { + let body = self + .get_text("/v2/servers/ping-sites", &[], Ttl::STABLE) + .await?; + decode(&body) + } + + /// `GET /v2/status`, the public status contract. + /// + /// Returns [`ApiError::EndpointUnavailable`] when the deployment does not + /// serve the route, which is the current state of api.reforgermods.net. + pub async fn status(&self) -> Result { + let path = "/v2/status"; + // Deliberately uncached: a status reading must be current. + let body = self.request_text(path, &[]).await.map_err(|e| match e { + // A 404 on a fixed route means the route is not deployed, not that a + // resource is missing. + ApiError::NotFound { .. } + | ApiError::Api { + status: 404 | 501, .. + } => ApiError::EndpointUnavailable { + path: path.to_string(), + }, + other => other, + })?; + decode(&body) + } + + /// The compact status the launcher header renders. + /// + /// This never fails: an unreachable or partially available API produces an + /// unknown/offline reading rather than an error the UI has to handle. It also + /// does not reimplement any backend health logic — it only reports what the + /// public contract returned. + /// + /// `dataset_hint` is the [`DatasetMeta`] from the most recent server + /// response. Every server response carries snapshot freshness, so the + /// "server data updated Ns ago" line costs no extra request. + pub async fn launcher_status(&self, dataset_hint: Option) -> LauncherStatus { + match self.status().await { + Ok(report) => LauncherStatus { + api_reachable: true, + dataset: report.dataset.or(dataset_hint), + reported_status: report.status, + note: None, + }, + Err(ApiError::EndpointUnavailable { .. }) => { + // Fall back to the liveness probe, which is deployed. + let reachable = self.request_text("/v2/health", &[]).await.is_ok(); + LauncherStatus { + api_reachable: reachable, + dataset: dataset_hint, + reported_status: None, + note: Some( + "This API deployment does not serve /v2/status; \ + freshness is read from the server dataset." + .into(), + ), + } + } + Err(err) => LauncherStatus { + api_reachable: false, + dataset: dataset_hint, + reported_status: None, + note: Some(err.to_string()), + }, + } + } + + /// Cache-aware GET returning the raw body. + async fn get_text( + &self, + path: &str, + params: &[(&'static str, String)], + ttl: Ttl, + ) -> Result { + let key = cache_key(path, params); + if let Some(hit) = self.cache.get(&key, ttl) { + return Ok(hit); + } + let body = self.request_text(path, params).await?; + self.cache.put(&key, body.clone()); + Ok(body) + } + + /// GET with bounded retries. No caching, no deserialisation. + async fn request_text( + &self, + path: &str, + params: &[(&'static str, String)], + ) -> Result { + let url = format!("{}{path}", self.base_url); + let mut attempt = 0; + loop { + let result = self.attempt(&url, params).await; + match result { + Ok(body) => return Ok(body), + Err(err) if attempt < MAX_RETRIES && err.is_retryable() => { + // Fixed ceiling on attempts; this cannot spin. + tokio::time::sleep(RETRY_BACKOFF * 2u32.pow(attempt)).await; + attempt += 1; + } + Err(err) => return Err(err), + } + } + } + + /// One HTTP round trip, mapped onto [`ApiError`]. + async fn attempt( + &self, + url: &str, + params: &[(&'static str, String)], + ) -> Result { + let response = self.http.get(url).query(params).send().await.map_err(|e| { + if e.is_timeout() { + ApiError::Timeout + } else { + ApiError::Offline(e) + } + })?; + + let status = response.status(); + if status.is_success() { + return response.text().await.map_err(|e| { + if e.is_timeout() { + ApiError::Timeout + } else { + ApiError::Offline(e) + } + }); + } + + if status == reqwest::StatusCode::TOO_MANY_REQUESTS { + let retry_after = response + .headers() + .get(reqwest::header::RETRY_AFTER) + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.trim().parse::().ok()); + return Err(ApiError::RateLimited { retry_after }); + } + + // Read the documented error envelope when present; fall back to the + // status code when the body is something else (a proxy error page). + let body = response.text().await.unwrap_or_default(); + let detail = serde_json::from_str::(&body) + .ok() + .map(|b| b.error); + let (code, message, request_id) = match detail { + Some(d) => (d.code, d.message, d.request_id), + None => ( + String::new(), + format!("API returned HTTP {}", status.as_u16()), + String::new(), + ), + }; + + if status == reqwest::StatusCode::NOT_FOUND { + return Err(ApiError::NotFound { + message: if message.is_empty() { + "Not found.".into() + } else { + message + }, + }); + } + + Err(ApiError::Api { + status: status.as_u16(), + code, + message, + request_id, + }) + } +} + +/// Deserialises a response body into `T`. +fn decode(body: &str) -> Result { + serde_json::from_str(body).map_err(ApiError::Decode) +} + +/// Cache key for a request. Parameter order comes from +/// [`ServerQuery::to_params`], which is stable, so equal queries collide. +fn cache_key(path: &str, params: &[(&'static str, String)]) -> String { + let mut key = String::with_capacity(path.len() + params.len() * 16); + key.push_str(path); + for (i, (name, value)) in params.iter().enumerate() { + key.push(if i == 0 { '?' } else { '&' }); + key.push_str(name); + key.push('='); + key.push_str(value); + } + key +} + +/// The header value for `install_id`, or `None` when it is not a usable id. +/// +/// Validated against the same UUID shape the store writes, which also +/// guarantees the value is header-safe ASCII. +fn install_id_header_value(install_id: &str) -> Option { + let id = install_id.trim(); + if crate::launch::ServerId::parse(id).is_err() { + return None; + } + reqwest::header::HeaderValue::from_str(id).ok() +} + +/// Validates and normalises a base URL: absolute http(s), no trailing slash. +/// +/// Rejecting non-HTTP schemes here stops a settings file from pointing the +/// client at, say, a `file://` path. +fn normalise_base_url(raw: &str) -> Result { + let trimmed = raw.trim().trim_end_matches('/'); + if trimmed.is_empty() { + return Err(ApiError::InvalidBaseUrl("base URL is empty".into())); + } + let rest = trimmed + .strip_prefix("https://") + .or_else(|| trimmed.strip_prefix("http://")) + .ok_or_else(|| ApiError::InvalidBaseUrl(format!("{raw:?} is not an http(s) URL")))?; + // A host must be present and must not contain path or query separators. + if rest.is_empty() || rest.starts_with('/') { + return Err(ApiError::InvalidBaseUrl(format!("{raw:?} has no host"))); + } + if rest.contains(['?', '#', ' ']) { + return Err(ApiError::InvalidBaseUrl(format!( + "{raw:?} must be a bare origin, without query or fragment" + ))); + } + Ok(trimmed.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::api::query::ServerSort; + + #[test] + fn default_base_url_is_production() { + assert_eq!(DEFAULT_BASE_URL, "https://api.reforgermods.net"); + } + + const SAMPLE_INSTALL_ID: &str = "8f14e45f-ceea-467a-9c2b-1f2a3b4c5d6e"; + + #[test] + fn client_builds_against_the_production_base_url() { + let client = ApiClient::new(SAMPLE_INSTALL_ID).unwrap(); + assert_eq!(client.base_url(), "https://api.reforgermods.net"); + } + + #[test] + fn install_id_becomes_a_header_value() { + let value = install_id_header_value(SAMPLE_INSTALL_ID).expect("valid id"); + assert_eq!(value.to_str().unwrap(), SAMPLE_INSTALL_ID); + } + + #[test] + fn a_malformed_install_id_is_not_sent_at_all() { + // A corrupted state file must degrade to an unidentified client rather + // than putting arbitrary text into a request header. + for bad in [ + "", + " ", + "not-a-uuid", + "8f14e45f-ceea-467a-9c2b-1f2a3b4c5d6e +X-Injected: 1", + "../../etc/passwd", + ] { + assert!(install_id_header_value(bad).is_none(), "{bad:?}"); + } + } + + #[test] + fn a_client_without_an_install_id_still_builds() { + let client = ApiClient::new("").expect("must not fail"); + assert_eq!(client.base_url(), "https://api.reforgermods.net"); + } + + #[test] + fn the_base_url_is_not_user_configurable() { + // There is no setting for this; only the documented dev env override. + assert_eq!(BASE_URL_ENV, "RFM_API_BASE_URL"); + assert_eq!(configured_base_url(), DEFAULT_BASE_URL); + } + + #[test] + fn base_urls_are_normalised() { + for (input, expected) in [ + ( + "https://api.reforgermods.net/", + "https://api.reforgermods.net", + ), + ( + "https://api.reforgermods.net///", + "https://api.reforgermods.net", + ), + (" http://127.0.0.1:8000 ", "http://127.0.0.1:8000"), + ("http://localhost:8000/api", "http://localhost:8000/api"), + ] { + assert_eq!(normalise_base_url(input).unwrap(), expected, "{input}"); + } + } + + #[test] + fn hostile_base_urls_are_rejected() { + for input in [ + "", + " ", + "api.reforgermods.net", + "file:///etc/passwd", + "javascript:alert(1)", + "steam://run/1874880", + "https://", + "http:///nohost", + "https://host?x=1", + "https://host#frag", + "https://ho st", + ] { + assert!( + normalise_base_url(input).is_err(), + "should be rejected: {input:?}" + ); + } + } + + #[test] + fn user_agent_names_the_launcher_and_version() { + let ua = crate::user_agent(); + assert!(ua.starts_with("reforgermods.net-launcher/"), "{ua}"); + assert_eq!( + ua, + format!("reforgermods.net-launcher/{}", env!("CARGO_PKG_VERSION")) + ); + } + + #[test] + fn cache_keys_include_every_parameter() { + let q = ServerQuery { + search: "everon".into(), + sort: ServerSort::Name, + ..Default::default() + }; + let key = cache_key("/v2/servers", &q.to_params()); + assert_eq!( + key, + "/v2/servers?page=1&perPage=100&sort=name&search=everon" + ); + } + + #[test] + fn cache_keys_distinguish_different_queries() { + let a = ServerQuery::default(); + let b = ServerQuery { + search: "x".into(), + ..Default::default() + }; + assert_ne!( + cache_key("/v2/servers", &a.to_params()), + cache_key("/v2/servers", &b.to_params()) + ); + } + + #[test] + fn cache_keys_match_for_equal_queries() { + let a = ServerQuery { + official: Some(true), + min_players: 10, + ..Default::default() + }; + // min_players is client-side, so it must not split the cache. + let b = ServerQuery { + official: Some(true), + min_players: 50, + ..Default::default() + }; + assert_eq!( + cache_key("/v2/servers", &a.to_params()), + cache_key("/v2/servers", &b.to_params()) + ); + } + + #[test] + fn cache_key_for_a_pathless_request_has_no_question_mark() { + assert_eq!(cache_key("/v2/health", &[]), "/v2/health"); + } + + #[test] + fn retry_budget_is_bounded() { + // Guards against a future edit turning the retry loop unbounded. + assert_eq!(MAX_RETRIES, 2); + let total: Duration = (0..MAX_RETRIES).map(|a| RETRY_BACKOFF * 2u32.pow(a)).sum(); + assert!(total < Duration::from_secs(1), "backoff total {total:?}"); + } + + #[test] + fn server_detail_path_uses_a_validated_id() { + let id = ServerId::parse("73e8fb7f-8789-4d88-8aa8-ea69b9aa092f").unwrap(); + assert_eq!( + format!("/v2/servers/{id}"), + "/v2/servers/73e8fb7f-8789-4d88-8aa8-ea69b9aa092f" + ); + } +} diff --git a/crates/rfm-core/src/api/error.rs b/crates/rfm-core/src/api/error.rs new file mode 100644 index 0000000..7c6f219 --- /dev/null +++ b/crates/rfm-core/src/api/error.rs @@ -0,0 +1,151 @@ +//! Error type for the reforgermods.net API client. + +use serde::Deserialize; + +/// The error envelope reforgermods.net returns for non-2xx responses. +#[derive(Debug, Clone, Deserialize)] +pub struct ApiErrorBody { + pub error: ApiErrorDetail, +} + +/// The `error` object inside [`ApiErrorBody`]. +#[derive(Debug, Clone, Deserialize)] +pub struct ApiErrorDetail { + #[serde(default)] + pub code: String, + #[serde(default)] + pub message: String, + #[serde(default, rename = "requestId")] + pub request_id: String, +} + +/// Every way an API call can fail. +/// +/// Variants are deliberately distinct so the UI can choose sensible copy: +/// `Offline` and `Timeout` are "try again", `RateLimited` is "wait", `NotFound` +/// is "this server is gone", and `Api` carries a server-authored message. +#[derive(Debug, thiserror::Error)] +pub enum ApiError { + /// The request never reached the API (DNS, TLS, connection refused). + #[error("could not reach the reforgermods.net API")] + Offline(#[source] reqwest::Error), + + /// The request exceeded its deadline. + #[error("request to the reforgermods.net API timed out")] + Timeout, + + /// A 404 for a resource that may simply no longer be indexed. + #[error("{message}")] + NotFound { message: String }, + + /// The requested route does not exist on this deployment. Distinct from + /// [`ApiError::NotFound`] so callers can degrade a feature rather than + /// reporting a missing server. + #[error("endpoint {path} is not available on this API deployment")] + EndpointUnavailable { path: String }, + + /// 429. `retry_after` is the `Retry-After` value in seconds when supplied. + #[error("rate limited by the reforgermods.net API")] + RateLimited { retry_after: Option }, + + /// Any other non-2xx response carrying the documented error envelope. + #[error("{message} ({code})")] + Api { + status: u16, + code: String, + message: String, + request_id: String, + }, + + /// A 2xx response whose body did not match the expected model. + #[error("unexpected response shape from the reforgermods.net API")] + Decode(#[source] serde_json::Error), + + /// The client could not be constructed (TLS backend, proxy config). + #[error("could not build the API client")] + Build(#[source] reqwest::Error), + + /// A configured base URL was not a usable absolute HTTP(S) URL. + #[error("invalid API base URL: {0}")] + InvalidBaseUrl(String), +} + +impl ApiError { + /// True when retrying the same request could plausibly succeed. + pub fn is_retryable(&self) -> bool { + match self { + ApiError::Offline(_) | ApiError::Timeout => true, + // 5xx is worth one more attempt; 4xx is not. + ApiError::Api { status, .. } => *status >= 500, + _ => false, + } + } + + /// A stable machine-readable kind, for the UI to branch on without + /// matching on human-readable text. + pub fn kind(&self) -> &'static str { + match self { + ApiError::Offline(_) => "offline", + ApiError::Timeout => "timeout", + ApiError::NotFound { .. } => "not_found", + ApiError::EndpointUnavailable { .. } => "endpoint_unavailable", + ApiError::RateLimited { .. } => "rate_limited", + ApiError::Api { .. } => "api", + ApiError::Decode(_) => "decode", + ApiError::Build(_) => "build", + ApiError::InvalidBaseUrl(_) => "config", + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn error_envelope_parses() { + let raw = r#"{"error":{"code":"SERVER_NOT_FOUND","message":"Server was not found in the current dataset.","requestId":"91288cfd10bc327410219cb9"}}"#; + let body: ApiErrorBody = serde_json::from_str(raw).unwrap(); + assert_eq!(body.error.code, "SERVER_NOT_FOUND"); + assert_eq!(body.error.request_id, "91288cfd10bc327410219cb9"); + } + + #[test] + fn error_envelope_tolerates_missing_fields() { + let body: ApiErrorBody = serde_json::from_str(r#"{"error":{}}"#).unwrap(); + assert_eq!(body.error.code, ""); + assert_eq!(body.error.message, ""); + } + + #[test] + fn retryability_follows_status_class() { + let server_err = ApiError::Api { + status: 503, + code: "SERVERS_UNAVAILABLE".into(), + message: "temporarily unavailable".into(), + request_id: String::new(), + }; + assert!(server_err.is_retryable()); + assert!(ApiError::Timeout.is_retryable()); + assert!(!ApiError::NotFound { + message: "gone".into() + } + .is_retryable()); + assert!(!ApiError::RateLimited { + retry_after: Some(5) + } + .is_retryable()); + } + + #[test] + fn kinds_are_stable_slugs() { + assert_eq!(ApiError::Timeout.kind(), "timeout"); + assert_eq!( + ApiError::EndpointUnavailable { + path: "/v2/status".into() + } + .kind(), + "endpoint_unavailable" + ); + } +} diff --git a/crates/rfm-core/src/api/mod.rs b/crates/rfm-core/src/api/mod.rs new file mode 100644 index 0000000..d1071f6 --- /dev/null +++ b/crates/rfm-core/src/api/mod.rs @@ -0,0 +1,14 @@ +//! The reforgermods.net API client layer. +//! +//! HTTP lives here and nowhere else. Tauri commands and the UI never build +//! requests themselves, so timeouts, retries, caching and error mapping have +//! exactly one implementation. + +mod client; +mod error; +pub mod models; +mod query; + +pub use client::{ApiClient, DEFAULT_BASE_URL}; +pub use error::{ApiError, ApiErrorBody, ApiErrorDetail}; +pub use query::{ClientPlatform, HistoryRange, ServerQuery, ServerSort, MAX_PER_PAGE}; diff --git a/crates/rfm-core/src/api/models.rs b/crates/rfm-core/src/api/models.rs new file mode 100644 index 0000000..fe8e9f6 --- /dev/null +++ b/crates/rfm-core/src/api/models.rs @@ -0,0 +1,486 @@ +//! Typed models for the reforgermods.net v2 API. +//! +//! Models are deliberately tolerant: unknown fields are ignored and absent +//! fields fall back to defaults. The API is free to add fields, and a launcher +//! that hard-fails on an unrecognised key would break on every API release. +//! +//! Every string in here is untrusted remote text. Nothing in this module +//! validates it; the only value that ever needs validating is a server id, and +//! that happens in [`crate::launch`] at the point of use. + +use serde::{Deserialize, Serialize}; + +/// Freshness of the indexer snapshot backing the server dataset. +/// +/// Present on every server response, which is why the launcher does not need a +/// separate poll to render "server data updated Ns ago". +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DatasetMeta { + /// True on cold start, before the indexer has produced a first snapshot. + #[serde(default)] + pub warming: bool, + /// True when the API considers the snapshot too old to be current. + #[serde(default)] + pub stale: bool, + /// Age of the snapshot in seconds. + #[serde(default)] + pub snapshot_age_seconds: f64, + /// RFC 3339 timestamp of the collection that produced the snapshot. + #[serde(default)] + pub last_collection_at: Option, + /// Total servers ever indexed. + #[serde(default)] + pub server_count: u64, + #[serde(default)] + pub online_server_count: u64, + #[serde(default)] + pub offline_server_count: u64, +} + +/// Pagination metadata for a server list page. +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ServerListMeta { + #[serde(default)] + pub total_pages: u32, + #[serde(default)] + pub current_page: u32, + #[serde(default)] + pub total_servers: u32, + #[serde(default)] + pub shown_servers: u32, +} + +/// reforgermods.net's derived ranking for a server. +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ServerAnalytics { + #[serde(default)] + pub rank: Option, + #[serde(default)] + pub overall_score: Option, + #[serde(default)] + pub activity_score: Option, + #[serde(default)] + pub reliability_score: Option, + #[serde(default)] + pub confidence: Option, +} + +/// A server as it appears in the browser list. +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ServerSummary { + pub id: String, + #[serde(default)] + pub name: String, + #[serde(default)] + pub scenario_id: String, + #[serde(default)] + pub scenario_name: String, + #[serde(default)] + pub scenario_thumbnail_url: Option, + #[serde(default)] + pub game_version: String, + #[serde(default)] + pub official: bool, + #[serde(default)] + pub password_protected: bool, + #[serde(default, rename = "battlEye")] + pub battle_eye: bool, + #[serde(default)] + pub cross_play: bool, + #[serde(default)] + pub players: u32, + #[serde(default)] + pub max_players: u32, + #[serde(default)] + pub queue: u32, + #[serde(default)] + pub queue_max: u32, + /// Nitrado ping-site id, e.g. `frankfurt`. Join with [`PingSite`] for a label. + #[serde(default)] + pub ping_site_id: Option, + #[serde(default)] + pub region: Option, + /// Host OS the server runs on (`LINUX`/`WINDOWS`), *not* a client platform. + #[serde(default)] + pub platform: Option, + /// Client platforms that may join: a subset of `pc`, `xbox`, `psn`. + #[serde(default)] + pub platforms: Vec, + #[serde(default)] + pub mod_count: u32, + #[serde(default)] + pub online: bool, + #[serde(default)] + pub first_seen: Option, + #[serde(default)] + pub last_seen: Option, + #[serde(default)] + pub analytics: Option, + /// The API's own `steam://run/...` join URI. + /// + /// Parsed for cross-checking only. The launcher builds its own URI from the + /// validated id: an API-supplied URL must never be handed to the OS. + #[serde(default)] + pub join_url: Option, +} + +/// `GET /v2/servers` +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct ServersPage { + #[serde(default)] + pub meta: ServerListMeta, + #[serde(default)] + pub dataset: DatasetMeta, + #[serde(default)] + pub data: Vec, +} + +/// Aggregate player activity derived from history samples. +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ServerActivity { + #[serde(default)] + pub peak24h: u32, + #[serde(default)] + pub avg24h: u32, + #[serde(default)] + pub median24h: u32, + #[serde(default)] + pub low24h: u32, + #[serde(default)] + pub peak7d: u32, + #[serde(default)] + pub samples24h: u32, + #[serde(default)] + pub tracking_since: Option, +} + +/// Totals for a server's reported mod set. +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ServerModSummary { + #[serde(default)] + pub count: u32, + /// Sum of sizes the API could resolve, in bytes. Not the full download size + /// unless `unresolved_count` is zero. + #[serde(default)] + pub known_size: u64, + #[serde(default)] + pub known_size_text: Option, + /// Mods whose exact-version size could not be resolved. + #[serde(default)] + pub unresolved_count: u32, +} + +/// One mod a server reports requiring. +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ServerMod { + /// Workshop mod id: 16 uppercase hex characters. + pub id: String, + #[serde(default)] + pub name: String, + #[serde(default)] + pub version: String, + #[serde(default)] + pub size: u64, + #[serde(default)] + pub size_text: Option, + #[serde(default)] + pub size_known: bool, + /// reforgermods.net page for this mod. Untrusted; validate before opening. + #[serde(default)] + pub mod_url: Option, +} + +/// A server with connection, activity and mod detail. +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ServerDetail { + /// Everything the list view already knows. + #[serde(flatten)] + pub summary: ServerSummary, + /// True when the server is in the current snapshot. False means "not + /// currently listed", which is not the same as "offline". + #[serde(default)] + pub present: bool, + #[serde(default)] + pub host_type: Option, + #[serde(default)] + pub joinable: bool, + /// The server's address, as published by the API. Untrusted: validate with + /// [`crate::ping::parse_target`] before doing anything with it. Absent for + /// roughly a third of servers. + #[serde(default)] + pub host: Option, + /// Game port. Informational only — nothing connects to it. + #[serde(default)] + pub port: u16, + /// Server-reported simulation FPS. 0 when not reported. + #[serde(default)] + pub fps: u32, + #[serde(default)] + pub activity: Option, + #[serde(default)] + pub mod_summary: ServerModSummary, + #[serde(default)] + pub mods: Vec, +} + +/// `GET /v2/servers/{id}` +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct ServerDetailResponse { + #[serde(default)] + pub dataset: DatasetMeta, + pub server: ServerDetail, +} + +/// A Nitrado ping location servers are grouped under. +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PingSite { + pub id: String, + #[serde(default)] + pub label: String, + #[serde(default)] + pub region_group: String, + /// Hostname used for latency probes. Not contacted in milestone 1. + #[serde(default)] + pub address: Option, +} + +/// `GET /v2/servers/ping-sites` +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct PingSitesResponse { + #[serde(default)] + pub updated_at: Option, + #[serde(default)] + pub sites: Vec, +} + +/// One bucket of a server's population history. +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct ServerHistoryPoint { + /// Bucket start, unix seconds. + pub t: i64, + #[serde(default)] + pub avg: u32, + #[serde(default)] + pub min: u32, + #[serde(default)] + pub max: u32, + /// Slot capacity during the bucket. 0 when not recorded. + #[serde(default)] + pub cap: u32, +} + +/// `GET /v2/servers/{id}/history` +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ServerHistory { + #[serde(default)] + pub server_id: String, + /// Range the API actually served, which may differ from what was asked. + #[serde(default)] + pub range: String, + /// Bucket width the API chose: `5m`, `hour` or `day`. + #[serde(default)] + pub bucket: String, + #[serde(default)] + pub points: Vec, +} + +/// Fleet-wide population right now, from `GET /v2/servers/history`. +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FleetNow { + #[serde(default)] + pub online_servers: u64, + /// Players across every listed server. + #[serde(default)] + pub players: u64, + /// Sum of slot capacity across those servers. + #[serde(default)] + pub capacity: u64, + #[serde(default)] + pub queue: u64, +} + +/// `GET /v2/servers/history`, of which the launcher reads only `now`. +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct FleetHistory { + #[serde(default)] + pub now: FleetNow, +} + +/// `GET /v2/status`, parsed permissively. +/// +/// This route is not deployed on api.reforgermods.net yet (it answers 404 as of +/// 2026-09-24), so every field is optional and the launcher treats an absent +/// route as "status unknown" rather than an error. See `docs/api-contract.md`. +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StatusReport { + /// Overall state word when the API supplies one, e.g. `operational`. + #[serde(default)] + pub status: Option, + #[serde(default)] + pub dataset: Option, +} + +/// What the launcher header renders. Assembled from whichever sources answered, +/// so a missing `/v2/status` degrades to an unobtrusive unknown state instead of +/// an error banner. +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LauncherStatus { + /// True when the API answered a health or status probe. + pub api_reachable: bool, + /// Freshness of the server dataset, when known. + pub dataset: Option, + /// Set when `/v2/status` answered and named a state. + pub reported_status: Option, + /// Human-readable reason the status is unknown, for a tooltip. + pub note: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Fixtures are real responses captured from api.reforgermods.net. + const SERVERS_PAGE: &str = include_str!("../../tests/fixtures/servers_page.json"); + const SERVER_DETAIL: &str = include_str!("../../tests/fixtures/server_detail.json"); + const PING_SITES: &str = include_str!("../../tests/fixtures/ping_sites.json"); + + #[test] + fn servers_page_parses_from_live_fixture() { + let page: ServersPage = serde_json::from_str(SERVERS_PAGE).unwrap(); + assert_eq!(page.data.len(), 3); + assert_eq!(page.meta.current_page, 1); + assert!(page.meta.total_servers > 0); + assert!(!page.dataset.warming); + assert!(page.dataset.last_collection_at.is_some()); + + let first = &page.data[0]; + assert!(!first.id.is_empty()); + assert!(!first.name.is_empty()); + assert!(first.online); + assert!(first.max_players > 0); + // Sorted by players descending. + assert!(first.players >= page.data[1].players); + } + + #[test] + fn battl_eye_field_name_is_mapped() { + // The API spells this `battlEye`; a wrong rename would silently read + // false for every server. + let page: ServersPage = serde_json::from_str(SERVERS_PAGE).unwrap(); + let raw: serde_json::Value = serde_json::from_str(SERVERS_PAGE).unwrap(); + for (parsed, raw) in page.data.iter().zip(raw["data"].as_array().unwrap()) { + assert_eq!( + parsed.battle_eye, + raw["battlEye"].as_bool().unwrap_or(false) + ); + } + } + + #[test] + fn server_detail_parses_from_live_fixture() { + let res: ServerDetailResponse = serde_json::from_str(SERVER_DETAIL).unwrap(); + let s = &res.server; + assert_eq!(s.summary.id, "73e8fb7f-8789-4d88-8aa8-ea69b9aa092f"); + assert!(s.present); + // Flattened summary fields must survive the flatten. + assert!(!s.summary.name.is_empty()); + assert!(s.summary.max_players > 0); + assert!(s.summary.mod_count > 0); + // Detail-only fields. + assert!(s.host.is_some(), "this fixture publishes an address"); + assert!(s.port > 0); + assert_eq!(s.mods.len() as u32, s.mod_summary.count); + assert!(s.activity.is_some()); + assert!(s.mods.iter().all(|m| m.id.len() == 16)); + } + + #[test] + fn ping_sites_parse_from_live_fixture() { + let res: PingSitesResponse = serde_json::from_str(PING_SITES).unwrap(); + assert!(res.sites.len() >= 8); + let fra = res.sites.iter().find(|s| s.id == "frankfurt").unwrap(); + assert_eq!(fra.label, "Frankfurt"); + assert_eq!(fra.region_group, "Europe"); + } + + #[test] + fn unknown_fields_are_ignored() { + // A future API release adding fields must not break the launcher. + let raw = r#"{"meta":{"currentPage":2,"brandNew":1},"dataset":{},"data":[ + {"id":"a","name":"n","somethingAdded":{"deep":true}} + ],"extraTopLevel":"x"}"#; + let page: ServersPage = serde_json::from_str(raw).unwrap(); + assert_eq!(page.meta.current_page, 2); + assert_eq!(page.data[0].id, "a"); + } + + #[test] + fn missing_optional_fields_fall_back_to_defaults() { + let page: ServersPage = serde_json::from_str(r#"{"data":[{"id":"a"}]}"#).unwrap(); + let s = &page.data[0]; + assert_eq!(s.players, 0); + assert!(!s.official); + assert!(s.platforms.is_empty()); + assert!(s.ping_site_id.is_none()); + assert!(s.analytics.is_none()); + } + + #[test] + fn an_absent_host_is_none_rather_than_an_empty_string() { + // About a third of servers publish no address; the ping code branches + // on None, so it must not arrive as Some(""). + let res: ServerDetailResponse = serde_json::from_str(r#"{"server":{"id":"a"}}"#).unwrap(); + assert!(res.server.host.is_none()); + assert_eq!(res.server.port, 0); + } + + #[test] + fn history_parses_from_live_fixture() { + const HISTORY: &str = include_str!("../../tests/fixtures/server_history.json"); + let h: ServerHistory = serde_json::from_str(HISTORY).unwrap(); + assert_eq!(h.range, "24h"); + assert_eq!(h.bucket, "5m"); + assert!(h.points.len() > 100, "24h of 5m buckets"); + // Buckets must be ordered, or the chart draws backwards. + assert!(h.points.windows(2).all(|w| w[0].t <= w[1].t)); + for point in &h.points { + assert!( + point.min <= point.avg && point.avg <= point.max, + "{point:?}" + ); + assert!(point.t > 1_700_000_000, "unix seconds expected"); + } + } + + #[test] + fn history_tolerates_an_empty_series() { + // A server with no recorded history returns an empty array, not an error. + let h: ServerHistory = + serde_json::from_str(r#"{"serverId":"x","range":"7d","bucket":"hour"}"#).unwrap(); + assert!(h.points.is_empty()); + } + + #[test] + fn status_report_tolerates_an_unknown_shape() { + // The contract is not published yet; parsing must not hard-fail. + let r: StatusReport = serde_json::from_str(r#"{"whatever":true}"#).unwrap(); + assert!(r.status.is_none()); + let r: StatusReport = + serde_json::from_str(r#"{"status":"operational","dataset":{"stale":false}}"#).unwrap(); + assert_eq!(r.status.as_deref(), Some("operational")); + assert!(!r.dataset.unwrap().stale); + } +} diff --git a/crates/rfm-core/src/api/query.rs b/crates/rfm-core/src/api/query.rs new file mode 100644 index 0000000..d47118f --- /dev/null +++ b/crates/rfm-core/src/api/query.rs @@ -0,0 +1,409 @@ +//! Server browser query construction. +//! +//! Only filters the live API actually supports are modelled here. `/v2/servers` +//! accepts `page`, `perPage`, `search`, `sort`, `region`, `platform`, `online`, +//! `includeOffline`, `official`, `hasMods`, `battleye` and `locked`; anything +//! else would be silently ignored by the API and would produce a filter control +//! that appears to work but does not. +//! +//! [`ServerQuery::min_players`] is the one exception and is explicitly *not* +//! sent to the API: it is applied client-side by [`ServerQuery::retains`], +//! because no server-side equivalent exists. + +use serde::{Deserialize, Serialize}; + +/// Sort orders `/v2/servers` accepts. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ServerSort { + /// Most players first. The useful default for a browser. + #[default] + Players, + Name, + Newest, + LastSeen, +} + +impl ServerSort { + /// The wire value for the `sort` parameter. + pub fn as_param(self) -> &'static str { + match self { + ServerSort::Players => "players", + ServerSort::Name => "name", + ServerSort::Newest => "newest", + ServerSort::LastSeen => "lastSeen", + } + } +} + +/// Client platforms the API can filter by. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ClientPlatform { + Pc, + Xbox, + Psn, +} + +impl ClientPlatform { + /// The wire value for the `platform` parameter. The API canonicalises these + /// short tokens itself. + pub fn as_param(self) -> &'static str { + match self { + ClientPlatform::Pc => "pc", + ClientPlatform::Xbox => "xbox", + ClientPlatform::Psn => "psn", + } + } +} + +/// Largest page the API permits. +pub const MAX_PER_PAGE: u32 = 500; + +/// A server browser query. +/// +/// `None` on a tri-state field means "do not filter", which is different from +/// `Some(false)`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct ServerQuery { + /// Free-text search over server names. + pub search: String, + pub sort: ServerSort, + /// Ping-site id, e.g. `frankfurt`. See `GET /v2/servers/ping-sites`. + pub region: Option, + pub platform: Option, + /// Official Bohemia servers only / community only. + pub official: Option, + /// Servers reporting at least one mod / none. + pub has_mods: Option, + pub battleye: Option, + /// `locked` is the API's name for password-protected. + pub locked: Option, + /// Include servers not in the current snapshot. Off by default: an offline + /// server cannot be joined. + pub include_offline: bool, + /// Client-side only. Drops servers below this player count from the loaded + /// page; the API has no equivalent parameter. + pub min_players: u32, + pub page: u32, + pub per_page: u32, +} + +impl Default for ServerQuery { + fn default() -> Self { + Self { + search: String::new(), + sort: ServerSort::default(), + region: None, + platform: None, + official: None, + has_mods: None, + battleye: None, + locked: None, + include_offline: false, + min_players: 0, + page: 1, + per_page: 100, + } + } +} + +impl ServerQuery { + /// The `/v2/servers` query parameters for this query, in a stable order. + /// + /// Stable ordering matters: the serialised form is the cache key, so two + /// equal queries must produce byte-identical strings. + pub fn to_params(&self) -> Vec<(&'static str, String)> { + let mut params: Vec<(&'static str, String)> = Vec::with_capacity(12); + + params.push(("page", self.page.max(1).to_string())); + params.push(("perPage", self.per_page.clamp(1, MAX_PER_PAGE).to_string())); + params.push(("sort", self.sort.as_param().to_string())); + + let search = self.search.trim(); + if !search.is_empty() { + params.push(("search", search.to_string())); + } + if let Some(region) = self + .region + .as_deref() + .map(str::trim) + .filter(|r| !r.is_empty()) + { + params.push(("region", region.to_string())); + } + if let Some(platform) = self.platform { + params.push(("platform", platform.as_param().to_string())); + } + // Tri-state flags: only sent when the user actually chose a side. + for (name, value) in [ + ("official", self.official), + ("hasMods", self.has_mods), + ("battleye", self.battleye), + ("locked", self.locked), + ] { + if let Some(value) = value { + params.push((name, value.to_string())); + } + } + if self.include_offline { + params.push(("includeOffline", "true".to_string())); + } + params + } + + /// Whether a fetched server survives the client-side filters. + /// + /// Only `min_players` lives here — the one filter the API cannot express. + /// Everything else was already applied server-side, and re-checking it + /// locally would mean two filter implementations that could disagree. + pub fn retains(&self, players: u32) -> bool { + players >= self.min_players + } + + /// True when any filter beyond sort and pagination is active. Drives the + /// "clear filters" affordance. + pub fn has_active_filters(&self) -> bool { + // Mirrors `to_params`: a blank search or region is not sent to the API, + // so it must not count as an active filter either. + !self.search.trim().is_empty() + || self.region.as_deref().is_some_and(|r| !r.trim().is_empty()) + || self.platform.is_some() + || self.official.is_some() + || self.has_mods.is_some() + || self.battleye.is_some() + || self.locked.is_some() + || self.include_offline + || self.min_players > 0 + } +} + +/// Ranges `GET /v2/servers/{id}/history` accepts. +/// +/// The API rejects anything else with `INVALID_RANGE`, so this is an enum rather +/// than a free string. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum HistoryRange { + SixHours, + /// The API's own default. + #[default] + OneDay, + ThreeDays, + OneWeek, + OneMonth, + ThreeMonths, + OneYear, + All, +} + +impl HistoryRange { + /// The wire value for the `range` parameter. + pub fn as_param(self) -> &'static str { + match self { + HistoryRange::SixHours => "6h", + HistoryRange::OneDay => "24h", + HistoryRange::ThreeDays => "72h", + HistoryRange::OneWeek => "7d", + HistoryRange::OneMonth => "30d", + HistoryRange::ThreeMonths => "90d", + HistoryRange::OneYear => "1y", + HistoryRange::All => "all", + } + } + + /// The ranges the inspector offers. A subset: 90d, 1y and all are supported + /// by the API but are not useful in a compact panel. + pub const OFFERED: [HistoryRange; 4] = [ + HistoryRange::SixHours, + HistoryRange::OneDay, + HistoryRange::OneWeek, + HistoryRange::OneMonth, + ]; +} + +#[cfg(test)] +mod tests { + use super::*; + + fn param_string(q: &ServerQuery) -> String { + q.to_params() + .into_iter() + .map(|(k, v)| format!("{k}={v}")) + .collect::>() + .join("&") + } + + #[test] + fn default_query_asks_for_the_busiest_online_servers() { + let q = ServerQuery::default(); + assert_eq!(param_string(&q), "page=1&perPage=100&sort=players"); + // includeOffline absent means online-only, which is the API default. + assert!(!q.has_active_filters()); + } + + #[test] + fn unset_tristate_filters_are_omitted_entirely() { + let q = ServerQuery::default(); + let keys: Vec<_> = q.to_params().into_iter().map(|(k, _)| k).collect(); + for absent in [ + "official", + "hasMods", + "battleye", + "locked", + "includeOffline", + ] { + assert!(!keys.contains(&absent), "{absent} should not be sent"); + } + } + + #[test] + fn false_is_distinct_from_unset() { + let q = ServerQuery { + official: Some(false), + has_mods: Some(false), + ..Default::default() + }; + assert!(param_string(&q).contains("official=false")); + assert!(param_string(&q).contains("hasMods=false")); + assert!(q.has_active_filters()); + } + + #[test] + fn all_filters_serialise_in_a_stable_order() { + let q = ServerQuery { + search: " conflict ".into(), + sort: ServerSort::Name, + region: Some("frankfurt".into()), + platform: Some(ClientPlatform::Pc), + official: Some(true), + has_mods: Some(true), + battleye: Some(true), + locked: Some(false), + include_offline: true, + min_players: 10, + page: 3, + per_page: 50, + }; + assert_eq!( + param_string(&q), + "page=3&perPage=50&sort=name&search=conflict®ion=frankfurt&platform=pc\ + &official=true&hasMods=true&battleye=true&locked=false&includeOffline=true" + ); + // Identical queries must serialise identically, so the cache key is stable. + assert_eq!(param_string(&q), param_string(&q.clone())); + } + + #[test] + fn min_players_is_never_sent_to_the_api() { + let q = ServerQuery { + min_players: 64, + ..Default::default() + }; + assert!(!param_string(&q).contains("min"), "{}", param_string(&q)); + assert!(q.has_active_filters()); + } + + #[test] + fn min_players_filters_client_side() { + let q = ServerQuery { + min_players: 10, + ..Default::default() + }; + assert!(!q.retains(0)); + assert!(!q.retains(9)); + assert!(q.retains(10)); + assert!(q.retains(128)); + // A zero threshold keeps everything, including empty servers. + assert!(ServerQuery::default().retains(0)); + } + + #[test] + fn history_ranges_match_the_api_vocabulary() { + // The API rejects anything outside this set with INVALID_RANGE. + let params: Vec<&str> = [ + HistoryRange::SixHours, + HistoryRange::OneDay, + HistoryRange::ThreeDays, + HistoryRange::OneWeek, + HistoryRange::OneMonth, + HistoryRange::ThreeMonths, + HistoryRange::OneYear, + HistoryRange::All, + ] + .iter() + .map(|r| r.as_param()) + .collect(); + assert_eq!( + params, + ["6h", "24h", "72h", "7d", "30d", "90d", "1y", "all"] + ); + // The default matches the API's own default. + assert_eq!(HistoryRange::default().as_param(), "24h"); + assert!(HistoryRange::OFFERED.contains(&HistoryRange::default())); + } + + #[test] + fn blank_search_and_region_are_dropped() { + let q = ServerQuery { + search: " ".into(), + region: Some(" ".into()), + ..Default::default() + }; + assert_eq!(param_string(&q), "page=1&perPage=100&sort=players"); + assert!(!q.has_active_filters()); + } + + #[test] + fn pagination_is_clamped_to_api_limits() { + let q = ServerQuery { + page: 0, + per_page: 9_999, + ..Default::default() + }; + assert!(param_string(&q).contains("page=1")); + assert!(param_string(&q).contains(&format!("perPage={MAX_PER_PAGE}"))); + + let q = ServerQuery { + per_page: 0, + ..Default::default() + }; + assert!(param_string(&q).contains("perPage=1")); + } + + #[test] + fn sort_and_platform_params_match_the_api_vocabulary() { + assert_eq!(ServerSort::Players.as_param(), "players"); + assert_eq!(ServerSort::LastSeen.as_param(), "lastSeen"); + assert_eq!(ServerSort::Newest.as_param(), "newest"); + assert_eq!(ServerSort::Name.as_param(), "name"); + assert_eq!(ClientPlatform::Pc.as_param(), "pc"); + assert_eq!(ClientPlatform::Xbox.as_param(), "xbox"); + assert_eq!(ClientPlatform::Psn.as_param(), "psn"); + } + + #[test] + fn query_round_trips_through_json() { + // The UI sends this struct across the Tauri boundary. + let q = ServerQuery { + search: "everon".into(), + sort: ServerSort::LastSeen, + platform: Some(ClientPlatform::Psn), + locked: Some(false), + min_players: 5, + ..Default::default() + }; + let json = serde_json::to_string(&q).unwrap(); + assert_eq!(serde_json::from_str::(&json).unwrap(), q); + } + + #[test] + fn partial_json_from_the_ui_fills_in_defaults() { + let q: ServerQuery = serde_json::from_str(r#"{"search":"ru"}"#).unwrap(); + assert_eq!(q.search, "ru"); + assert_eq!(q.page, 1); + assert_eq!(q.per_page, 100); + assert_eq!(q.sort, ServerSort::Players); + } +} diff --git a/crates/rfm-core/src/cache.rs b/crates/rfm-core/src/cache.rs new file mode 100644 index 0000000..9b3b48e --- /dev/null +++ b/crates/rfm-core/src/cache.rs @@ -0,0 +1,164 @@ +//! A tiny bounded TTL cache for API response bodies. +//! +//! The launcher caches raw response text keyed by request URL rather than +//! deserialised structs: it keeps the cache free of generics, and re-parsing a +//! cached body costs microseconds against a network round trip. Entries are +//! capped so a long session browsing thousands of servers cannot grow without +//! bound. + +use std::collections::HashMap; +use std::sync::Mutex; +use std::time::{Duration, Instant}; + +/// How long a cached entry stays fresh, per class of request. +#[derive(Debug, Clone, Copy)] +pub struct Ttl(pub Duration); + +impl Ttl { + /// Server list and detail pages. The indexer publishes a new snapshot about + /// once a minute; 15s keeps the view current without hammering the API when + /// the user flips between filters. + pub const SNAPSHOT: Ttl = Ttl(Duration::from_secs(15)); + /// Population history. The narrowest bucket the API produces is 5 minutes, + /// so a fresh request inside a minute cannot show the user anything new. + pub const HISTORY: Ttl = Ttl(Duration::from_secs(60)); + /// Ping sites change rarely; the response carries its own `updatedAt`. + pub const STABLE: Ttl = Ttl(Duration::from_secs(60 * 60)); +} + +struct Entry { + stored_at: Instant, + body: String, +} + +/// A process-lifetime cache of response bodies. +pub struct ResponseCache { + entries: Mutex>, + capacity: usize, +} + +impl ResponseCache { + /// Creates a cache holding at most `capacity` entries. + pub fn new(capacity: usize) -> Self { + Self { + entries: Mutex::new(HashMap::new()), + capacity: capacity.max(1), + } + } + + /// Returns the cached body for `key` when it is still within `ttl`. + pub fn get(&self, key: &str, ttl: Ttl) -> Option { + let entries = self.entries.lock().ok()?; + let entry = entries.get(key)?; + (entry.stored_at.elapsed() < ttl.0).then(|| entry.body.clone()) + } + + /// Stores `body` under `key`, evicting the oldest entry when full. + pub fn put(&self, key: &str, body: String) { + let Ok(mut entries) = self.entries.lock() else { + // A poisoned cache is a cache miss, never a failed request. + return; + }; + if entries.len() >= self.capacity && !entries.contains_key(key) { + // Linear scan for the oldest entry. At this capacity that is far + // cheaper than maintaining a second index. + if let Some(oldest) = entries + .iter() + .min_by_key(|(_, e)| e.stored_at) + .map(|(k, _)| k.clone()) + { + entries.remove(&oldest); + } + } + entries.insert( + key.to_owned(), + Entry { + stored_at: Instant::now(), + body, + }, + ); + } + + /// Drops every entry. Used when the API base URL changes. + pub fn clear(&self) { + if let Ok(mut entries) = self.entries.lock() { + entries.clear(); + } + } + + /// Number of entries currently held, fresh or not. + pub fn len(&self) -> usize { + self.entries.lock().map(|e| e.len()).unwrap_or(0) + } + + /// True when the cache holds nothing. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +impl Default for ResponseCache { + fn default() -> Self { + Self::new(256) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const INSTANT: Ttl = Ttl(Duration::ZERO); + + #[test] + fn stores_and_reads_back() { + let cache = ResponseCache::new(4); + cache.put("a", "body".into()); + assert_eq!(cache.get("a", Ttl::SNAPSHOT).as_deref(), Some("body")); + } + + #[test] + fn misses_on_unknown_key() { + let cache = ResponseCache::new(4); + assert!(cache.get("nope", Ttl::SNAPSHOT).is_none()); + } + + #[test] + fn expired_entries_are_not_returned() { + let cache = ResponseCache::new(4); + cache.put("a", "body".into()); + // A zero TTL means every entry is already stale. + assert!(cache.get("a", INSTANT).is_none()); + } + + #[test] + fn eviction_keeps_the_cache_bounded() { + let cache = ResponseCache::new(2); + cache.put("a", "1".into()); + cache.put("b", "2".into()); + cache.put("c", "3".into()); + assert_eq!(cache.len(), 2); + // "a" was oldest, so it went first. + assert!(cache.get("a", Ttl::SNAPSHOT).is_none()); + assert!(cache.get("c", Ttl::SNAPSHOT).is_some()); + } + + #[test] + fn overwriting_an_existing_key_does_not_evict() { + let cache = ResponseCache::new(2); + cache.put("a", "1".into()); + cache.put("b", "2".into()); + cache.put("a", "updated".into()); + assert_eq!(cache.len(), 2); + assert_eq!(cache.get("a", Ttl::SNAPSHOT).as_deref(), Some("updated")); + assert!(cache.get("b", Ttl::SNAPSHOT).is_some()); + } + + #[test] + fn clear_empties_the_cache() { + let cache = ResponseCache::new(4); + cache.put("a", "1".into()); + assert!(!cache.is_empty()); + cache.clear(); + assert!(cache.is_empty()); + } +} diff --git a/crates/rfm-core/src/identity.rs b/crates/rfm-core/src/identity.rs new file mode 100644 index 0000000..fd7c744 --- /dev/null +++ b/crates/rfm-core/src/identity.rs @@ -0,0 +1,258 @@ +//! Stable, anonymous installation identity. +//! +//! The API needs to tell "50 000 requests from 5 000 installs" from "50 000 +//! requests from one". A value generated on first run answers that, but it +//! resets whenever the launcher is reinstalled, so one person reinstalling +//! three times counts as three installs. +//! +//! This derives the identifier from a machine characteristic instead, so it +//! survives uninstall and reinstall. +//! +//! # What is sent, and what is not +//! +//! The machine value **never leaves the process**. What is sent is +//! +//! ```text +//! SHA-256(APPLICATION_SALT || machine value) -> first 16 bytes -> UUID +//! ``` +//! +//! so the API receives an opaque identifier it cannot reverse into a machine +//! id, and the same machine running a different application derives a different +//! value. The wire format stays a canonical UUID, so the API contract in +//! `docs/client-identification.md` is unchanged. +//! +//! # Why not IP +//! +//! An address is the wrong input: it changes with DHCP leases, VPNs, tethering +//! and moving between networks, so it would both split one install into many +//! and merge everyone behind a shared NAT into one. It is also the one piece of +//! genuinely personal data in the request, and the API already sees it. +//! +//! # Sources +//! +//! | Platform | Value | +//! | --- | --- | +//! | Windows | `HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid` | +//! | Linux | `/etc/machine-id`, else `/var/lib/dbus/machine-id` | +//! | Other / unreadable | falls back to a persisted random value | +//! +//! All of these are per-OS-installation, not per-user and not per-device-serial, +//! and they change when the OS is reinstalled — which is the intended +//! granularity: a new OS is a new install. + +use sha2::{Digest, Sha256}; + +/// Domain separator. Mixed in so the same machine yields a different identifier +/// for a different application, and so the digest is not a bare hash of a value +/// that other software also knows. +const APPLICATION_SALT: &str = "reforgermods.net-launcher/install-id/v1"; + +/// The identifier to send, preferring a machine-derived value. +/// +/// `fallback` is the persisted random value used when no machine source is +/// readable — a sandboxed or unusual environment still gets a working, if +/// reinstall-unstable, identifier rather than none. +pub fn stable_install_id(fallback: &str) -> String { + match machine_value() { + Some(value) => derive_uuid(&value), + None => fallback.to_string(), + } +} + +/// True when this build could read a machine source, so the identifier survives +/// a reinstall. Surfaced only in debug mode. +pub fn is_machine_derived() -> bool { + machine_value().is_some() +} + +/// Hashes `material` into a canonical UUID. +/// +/// The result carries UUID version 8 (custom) and the RFC 4122 variant, which +/// is the correct label for a value derived this way — it is not random (v4) and +/// not an RFC-specified name hash (v3/v5). +fn derive_uuid(material: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(APPLICATION_SALT.as_bytes()); + hasher.update([0u8]); // Separator, so salt and value cannot run together. + hasher.update(material.trim().to_ascii_lowercase().as_bytes()); + let digest = hasher.finalize(); + + let mut bytes = [0u8; 16]; + bytes.copy_from_slice(&digest[..16]); + bytes[6] = (bytes[6] & 0x0f) | 0x80; // version 8 + bytes[8] = (bytes[8] & 0x3f) | 0x80; // RFC 4122 variant + + format!( + "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], + ) +} + +/// Reads this machine's identifier, if one is available. +#[cfg(windows)] +fn machine_value() -> Option { + use winreg::enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_64KEY}; + use winreg::RegKey; + + // Explicitly the 64-bit view: MachineGuid lives there, and a 32-bit process + // would otherwise be redirected to an empty Wow6432Node. + let key = RegKey::predef(HKEY_LOCAL_MACHINE) + .open_subkey_with_flags( + r"SOFTWARE\Microsoft\Cryptography", + KEY_READ | KEY_WOW64_64KEY, + ) + .ok()?; + let guid: String = key.get_value("MachineGuid").ok()?; + usable(guid) +} + +/// Reads this machine's identifier, if one is available. +#[cfg(not(windows))] +fn machine_value() -> Option { + ["/etc/machine-id", "/var/lib/dbus/machine-id"] + .iter() + .find_map(|path| std::fs::read_to_string(path).ok()) + .and_then(usable) +} + +/// Rejects blank or obviously placeholder values. +/// +/// An all-zero or empty machine id would make every affected machine share one +/// identifier, which is worse than falling back to a random one. +fn usable(value: String) -> Option { + let trimmed = value.trim().to_string(); + if trimmed.len() < 8 { + return None; + } + if trimmed.chars().all(|c| c == '0' || c == '-') { + return None; + } + Some(trimmed) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::launch::ServerId; + + #[test] + fn derived_ids_are_canonical_uuids() { + // The API header validator accepts only this shape, so a derived value + // that failed it would be silently dropped. + let id = derive_uuid("4c4c4544-0037-5910-8051-b8c04f325632"); + assert!(ServerId::parse(&id).is_ok(), "{id}"); + assert_eq!(id.len(), 36); + } + + #[test] + fn derivation_is_deterministic() { + let a = derive_uuid("machine-abc"); + let b = derive_uuid("machine-abc"); + assert_eq!(a, b, "the same machine must reinstall to the same id"); + } + + #[test] + fn different_machines_derive_different_ids() { + assert_ne!(derive_uuid("machine-abc"), derive_uuid("machine-abd")); + } + + #[test] + fn input_is_normalised_for_case_and_whitespace() { + // Windows returns the GUID in varying case depending on how it is read. + let canonical = derive_uuid("4c4c4544-0037-5910-8051-b8c04f325632"); + assert_eq!( + derive_uuid("4C4C4544-0037-5910-8051-B8C04F325632"), + canonical + ); + assert_eq!( + derive_uuid(" 4c4c4544-0037-5910-8051-b8c04f325632\n"), + canonical + ); + } + + #[test] + fn the_machine_value_is_not_recoverable_from_the_id() { + // Not a proof, but it pins the intent: the identifier must never be the + // machine value, or a prefix or suffix of it. + let machine = "4c4c4544-0037-5910-8051-b8c04f325632"; + let id = derive_uuid(machine); + assert_ne!(id, machine); + assert!(!id.contains(&machine[..8])); + assert!(!machine.contains(&id[..8])); + } + + #[test] + fn version_and_variant_are_set() { + let id = derive_uuid("anything"); + let bytes: Vec = id + .replace('-', "") + .as_bytes() + .chunks(2) + .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap()) + .collect(); + assert_eq!(bytes[6] >> 4, 8, "version 8, a custom derivation"); + assert_eq!(bytes[8] >> 6, 0b10, "RFC 4122 variant"); + } + + #[test] + fn the_salt_separates_this_application() { + // Two applications on one machine must not share an identifier. Mirrors + // derive_uuid with a different salt. + let mut hasher = Sha256::new(); + hasher.update(b"some-other-application"); + hasher.update([0u8]); + hasher.update(b"machine-abc"); + let other = hasher.finalize(); + assert_ne!( + derive_uuid("machine-abc") + .replace('-', "") + .chars() + .take(8) + .collect::(), + other[..4] + .iter() + .map(|b| format!("{b:02x}")) + .collect::() + ); + } + + #[test] + fn unusable_machine_values_are_rejected() { + // These would collapse many machines onto one identifier. + for value in [ + "", + " ", + "0", + "0000", + "00000000-0000-0000-0000-000000000000", + "-----", + ] { + assert!(usable(value.to_string()).is_none(), "{value:?}"); + } + assert!(usable("4c4c4544-0037-5910-8051-b8c04f325632".into()).is_some()); + } + + #[test] + fn the_fallback_is_used_when_no_machine_value_exists() { + // Cannot force machine_value() to fail here, so this pins the contract + // that a fallback is returned verbatim rather than hashed. + let fallback = "8f14e45f-ceea-467a-9c2b-1f2a3b4c5d6e"; + let id = stable_install_id(fallback); + assert!(ServerId::parse(&id).is_ok()); + if !is_machine_derived() { + assert_eq!(id, fallback); + } + } + + #[test] + fn a_machine_derived_id_is_stable_across_calls() { + // Only meaningful where a machine id exists. A container or a minimal + // chroot has neither /etc/machine-id nor the dbus copy, and there every + // call correctly returns the fallback it was handed instead. + if !is_machine_derived() { + return; + } + assert_eq!(stable_install_id("x"), stable_install_id("y")); + } +} diff --git a/crates/rfm-core/src/launch.rs b/crates/rfm-core/src/launch.rs new file mode 100644 index 0000000..301c6f1 --- /dev/null +++ b/crates/rfm-core/src/launch.rs @@ -0,0 +1,500 @@ +//! Direct-join URI construction for Arma Reforger. +//! +//! The join payload is a base64 encoding of a small JSON object naming the +//! lobby room: +//! +//! ```text +//! {"id":"","inviteToken":""} +//! -> UTF-8 bytes +//! -> standard base64 +//! -> trailing '=' padding removed +//! ``` +//! +//! # How the payload reaches the game +//! +//! The payload is delivered as a Steam launch argument: +//! +//! ```text +//! steam://run/1874880/// +//! ^^ +//! ``` +//! +//! The **empty path segment** is what matters. `steam://run////` +//! is Steam's documented form for passing launch arguments; with a single +//! slash, Steam can start the game without forwarding the payload, leaving the +//! player on the main menu. +//! +//! Status: this is the current best understanding and is **not yet confirmed +//! against a live client**. The single-slash form was observed to launch the +//! game without joining; the double-slash form is the likely fix. +//! +//! # The `ArmaReforger:` protocol, an alternative +//! +//! The game also registers its own URL protocol. Its Steam install script +//! (`steamapps/common/Arma Reforger/installscript.vdf`) writes: +//! +//! ```text +//! HKCU\SOFTWARE\Classes\ArmaReforger +//! (Default) = "URL:Arma Reforger Protocol" +//! URL Protocol = "" +//! HKCU\SOFTWARE\Classes\ArmaReforger\shell\open\command +//! (Default) = "\ArmaReforger_BE.exe" -exe ArmaReforgerSteam.exe +//! "-addonsDir=\addons" -uri="%1" +//! ``` +//! +//! [`reforger_join_uri`] builds that form. It is kept, tested and unused by +//! default: it is a second candidate delivery route if the Steam form turns out +//! not to join, and it does not depend on how Steam forwards arguments. +//! +//! # Launch state +//! +//! Either route only carries the payload when the game actually starts. If Arma +//! Reforger is already running, Steam may simply focus the existing process and +//! drop the arguments, so a join must be tested from a fully closed game. +//! +//! Everything in this module is pure. It never spawns a shell and never +//! interpolates API-supplied text: the only value that reaches a URI is a +//! [`ServerId`], which cannot be constructed without passing UUID validation. + +use base64::engine::general_purpose::STANDARD_NO_PAD; +use base64::Engine as _; +use serde::Serialize; + +/// Steam App ID for Arma Reforger. +pub const ARMA_REFORGER_APP_ID: u32 = 1874880; + +/// URL protocol Arma Reforger registers for join links. +/// +/// Matches the `HKCU\SOFTWARE\Classes\ArmaReforger` key written by the game's +/// Steam install script. URI schemes are case-insensitive; this spelling +/// mirrors the registry key. +pub const REFORGER_URI_SCHEME: &str = "ArmaReforger"; + +/// Errors produced while validating a server identifier. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum LaunchError { + /// The identifier was not a canonical 8-4-4-4-12 lowercase-hex UUID. + #[error("not a valid Arma Reforger server id (expected a UUID, got {got:?})")] + InvalidServerId { got: String }, +} + +/// A validated Arma Reforger server identifier. +/// +/// Two shapes occur in the API, and both must be joinable: +/// +/// - **Community servers** use a canonical UUID, e.g. +/// `73e8fb7f-8789-4d88-8aa8-ea69b9aa092f`. +/// - **Official servers** use a provider id, e.g. `nitrado_17516010`. Sampled +/// across 600 servers, every non-UUID id was official and every official id +/// was `nitrado_` followed by digits. +/// +/// The only way to obtain one is [`ServerId::parse`], so any `ServerId` in the +/// program contains nothing but lowercase alphanumerics, `-` and `_`. This is +/// the security boundary for launching: untrusted API text cannot become a URI. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +pub struct ServerId(String); + +impl ServerId { + /// Parses a canonical UUID. Case is normalised to lowercase; surrounding + /// whitespace is rejected rather than trimmed, because a stray space in an + /// id means the caller mishandled the value somewhere upstream. + pub fn parse(raw: &str) -> Result { + if is_canonical_uuid(raw) || is_provider_id(raw) { + Ok(Self(raw.to_ascii_lowercase())) + } else { + Err(LaunchError::InvalidServerId { + got: raw.to_owned(), + }) + } + } + + /// True for an official (provider-hosted) server. + pub fn is_official_form(&self) -> bool { + is_provider_id(&self.0) + } + + /// The validated identifier as a string slice. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl std::fmt::Display for ServerId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +/// Returns true for exactly `8-4-4-4-12` ASCII-hex groups and nothing else. +/// +/// Hand-written rather than pulled from a UUID crate so the accepted shape is +/// visible here: no braces, no `urn:` prefix, no internal whitespace. +fn is_canonical_uuid(s: &str) -> bool { + const GROUPS: [usize; 5] = [8, 4, 4, 4, 12]; + let mut parts = s.split('-'); + for len in GROUPS { + match parts.next() { + Some(p) if p.len() == len && p.bytes().all(|b| b.is_ascii_hexdigit()) => {} + _ => return false, + } + } + parts.next().is_none() +} + +/// Returns true for `_`, the official-server id shape. +/// +/// Deliberately narrow: lowercase ASCII letters, one underscore, then digits. +/// Every character it admits is URI-safe, so a validated id never needs +/// escaping. Written generically rather than hardcoding `nitrado` so a second +/// provider does not silently become unjoinable. +fn is_provider_id(s: &str) -> bool { + let Some((provider, number)) = s.split_once('_') else { + return false; + }; + !provider.is_empty() + && provider.len() <= 16 + && provider.bytes().all(|b| b.is_ascii_lowercase()) + && !number.is_empty() + && number.len() <= 24 + && number.bytes().all(|b| b.is_ascii_digit()) +} + +/// The JSON body Arma Reforger expects. Field order is the declaration order, +/// which is what the verified payload uses. +#[derive(Serialize)] +struct JoinPayload<'a> { + id: &'a str, + #[serde(rename = "inviteToken")] + invite_token: &'a str, +} + +/// Serialises the join payload for a server. +/// +/// Exposed separately from [`steam_join_uri`] so the exact bytes that get +/// encoded are directly assertable in tests. +pub fn join_payload_json(server: &ServerId) -> String { + let payload = JoinPayload { + id: server.as_str(), + invite_token: "", + }; + // A struct of two string fields cannot fail to serialise. + serde_json::to_string(&payload).expect("join payload is always serialisable") +} + +/// Base64-encodes the join payload with trailing `=` padding removed. +pub fn join_payload_base64(server: &ServerId) -> String { + STANDARD_NO_PAD.encode(join_payload_json(server).as_bytes()) +} + +/// Builds the `ArmaReforger://` URI for the game's own protocol handler. +/// +/// Not used by default; see the module docs. Retained as a second candidate +/// delivery route, since it reaches the launcher as `-uri=` without depending +/// on how Steam forwards arguments. +pub fn reforger_join_uri(server: &ServerId) -> String { + format!("{REFORGER_URI_SCHEME}://{}", join_payload_base64(server)) +} + +/// Convenience: validate a raw identifier and build its join URI in one step. +pub fn reforger_join_uri_for(raw: &str) -> Result { + Ok(reforger_join_uri(&ServerId::parse(raw)?)) +} + +/// Builds the `steam://run/...` URI that joins `server` directly. +/// +/// Note the doubled slash: `steam://run////`. The empty path +/// segment marks what follows as a launch argument. Dropping it produces a URI +/// that starts the game without forwarding the payload. +/// +/// Hand this to the platform URI opener (`ShellExecuteW` on Windows via +/// `tauri-plugin-opener`). Never pass it to a shell. +pub fn steam_join_uri(server: &ServerId) -> String { + format!( + "steam://run/{}//{}/", + ARMA_REFORGER_APP_ID, + join_payload_base64(server) + ) +} + +/// Convenience: validate a raw identifier and build its join URI in one step. +pub fn steam_join_uri_for(raw: &str) -> Result { + Ok(steam_join_uri(&ServerId::parse(raw)?)) +} + +/// Builds the `steam://run/` URI that starts Arma Reforger with no +/// server argument, for "just launch the game". +pub fn steam_launch_uri() -> String { + format!("steam://run/{ARMA_REFORGER_APP_ID}") +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The identifier and payload from the verified PowerShell experiment. + const SAMPLE_ID: &str = "73e8fb7f-8789-4d88-8aa8-ea69b9aa092f"; + const SAMPLE_B64: &str = + "eyJpZCI6IjczZThmYjdmLTg3ODktNGQ4OC04YWE4LWVhNjliOWFhMDkyZiIsImludml0ZVRva2VuIjoiIn0"; + + #[test] + fn payload_json_matches_verified_shape() { + let id = ServerId::parse(SAMPLE_ID).unwrap(); + assert_eq!( + join_payload_json(&id), + r#"{"id":"73e8fb7f-8789-4d88-8aa8-ea69b9aa092f","inviteToken":""}"# + ); + } + + #[test] + fn payload_base64_matches_verified_vector() { + let id = ServerId::parse(SAMPLE_ID).unwrap(); + assert_eq!(join_payload_base64(&id), SAMPLE_B64); + } + + #[test] + fn base64_padding_is_removed() { + let id = ServerId::parse(SAMPLE_ID).unwrap(); + let encoded = join_payload_base64(&id); + assert!( + !encoded.ends_with('='), + "padding must be stripped: {encoded}" + ); + // The 62-byte payload would normally encode to 84 chars with one '='. + assert_eq!(join_payload_json(&id).len(), 62); + assert_eq!(encoded.len(), 83); + } + + #[test] + fn padding_is_removed_for_every_residue_class() { + // UUID payloads are a fixed length, so exercise the encoder directly to + // cover all three base64 remainder cases (0, 1 and 2 leftover bytes). + for input in ["abc", "abcd", "abcde", ""] { + let encoded = STANDARD_NO_PAD.encode(input.as_bytes()); + assert!(!encoded.contains('='), "{input:?} -> {encoded}"); + } + } + + #[test] + fn join_uri_has_the_empty_segment_that_marks_a_launch_argument() { + // This is the entire fix and it is one character. Without the empty + // segment Steam can start the game without forwarding the payload, + // which looks like a successful launch that never joins. + let id = ServerId::parse(SAMPLE_ID).unwrap(); + let uri = steam_join_uri(&id); + assert_eq!(uri, format!("steam://run/1874880//{SAMPLE_B64}/")); + assert!( + uri.contains(&format!("{ARMA_REFORGER_APP_ID}//")), + "app id must be followed by an empty path segment: {uri}" + ); + assert!( + !uri.contains(&format!("{ARMA_REFORGER_APP_ID}/{SAMPLE_B64}")), + "single-slash form regressed: {uri}" + ); + } + + #[test] + fn join_uri_matches_the_reference_implementation() { + // Mirrors the JS reference: JSON.stringify({id, inviteToken}) in that + // field order, standard base64, trailing '=' stripped, wrapped as + // steam://run/1874880///. + let id = ServerId::parse(SAMPLE_ID).unwrap(); + let payload = join_payload_base64(&id); + assert!(!payload.contains('=')); + assert_eq!( + steam_join_uri(&id), + format!("steam://run/{ARMA_REFORGER_APP_ID}//{payload}/") + ); + } + + #[test] + fn plain_launch_uri_keeps_a_single_segment() { + // No payload means no argument marker; this must not gain the empty + // segment along with the join URI. + assert_eq!(steam_launch_uri(), "steam://run/1874880"); + } + + #[test] + fn the_protocol_alternative_carries_an_identical_payload() { + let id = ServerId::parse(SAMPLE_ID).unwrap(); + let payload = join_payload_base64(&id); + assert!(steam_join_uri(&id).contains(&payload)); + assert!(reforger_join_uri(&id).ends_with(&payload)); + } + + #[test] + fn join_uri_uses_the_games_own_protocol() { + // The scheme the game registers; Steam's run URL drops the payload. + let id = ServerId::parse(SAMPLE_ID).unwrap(); + assert_eq!( + reforger_join_uri(&id), + format!("ArmaReforger://{SAMPLE_B64}") + ); + } + + #[test] + fn join_uri_carries_the_same_payload_as_the_steam_form() { + let id = ServerId::parse(SAMPLE_ID).unwrap(); + let payload = join_payload_base64(&id); + assert!(reforger_join_uri(&id).ends_with(&payload)); + assert!(steam_join_uri(&id).contains(&payload)); + } + + #[test] + fn join_uri_has_no_trailing_slash() { + // The whole URI is forwarded verbatim as -uri=; a stray separator would + // become part of the payload the game parses. + let uri = reforger_join_uri_for(SAMPLE_ID).unwrap(); + assert!(!uri.ends_with('/'), "{uri}"); + assert_eq!(uri.matches("//").count(), 1); + } + + #[test] + fn steam_join_uri_is_stable_for_a_given_server() { + let id = ServerId::parse(SAMPLE_ID).unwrap(); + assert_eq!(steam_join_uri(&id), steam_join_uri_for(SAMPLE_ID).unwrap()); + } + + #[test] + fn join_uri_has_trailing_slash_and_app_id() { + let uri = steam_join_uri_for(SAMPLE_ID).unwrap(); + assert!(uri.starts_with("steam://run/1874880//")); + assert!(uri.ends_with('/')); + } + + #[test] + fn plain_launch_uri_carries_no_payload() { + assert_eq!(steam_launch_uri(), "steam://run/1874880"); + } + + #[test] + fn uppercase_ids_are_normalised() { + let id = ServerId::parse("73E8FB7F-8789-4D88-8AA8-EA69B9AA092F").unwrap(); + assert_eq!(id.as_str(), SAMPLE_ID); + assert_eq!( + reforger_join_uri(&id), + reforger_join_uri_for(SAMPLE_ID).unwrap() + ); + } + + /// A real official server id, from `/v2/servers?official=true`. + const OFFICIAL_ID: &str = "nitrado_17516010"; + + #[test] + fn official_server_ids_are_accepted() { + // 364 official servers use this shape; rejecting it made them all + // unjoinable. + let id = ServerId::parse(OFFICIAL_ID).unwrap(); + assert_eq!(id.as_str(), OFFICIAL_ID); + assert!(id.is_official_form()); + assert!(!ServerId::parse(SAMPLE_ID).unwrap().is_official_form()); + } + + #[test] + fn official_ids_join_through_the_same_payload() { + // The payload shape does not change: only the id inside it does. + let id = ServerId::parse(OFFICIAL_ID).unwrap(); + assert_eq!( + join_payload_json(&id), + r#"{"id":"nitrado_17516010","inviteToken":""}"# + ); + assert_eq!( + steam_join_uri(&id), + "steam://run/1874880//eyJpZCI6Im5pdHJhZG9fMTc1MTYwMTAiLCJpbnZpdGVUb2tlbiI6IiJ9/" + ); + } + + #[test] + fn provider_ids_stay_uri_safe() { + // Everything the provider shape admits can sit in a URI unescaped, so + // no id ever needs encoding on the way out. + for raw in ["nitrado_17516010", "a_0", "somehost_123456789012"] { + let id = ServerId::parse(raw).unwrap(); + assert!(id + .as_str() + .bytes() + .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')); + } + } + + #[test] + fn provider_shaped_lookalikes_are_rejected() { + for raw in [ + "_123", // no provider + "nitrado_", // no number + "nitrado_17516010x", // trailing junk + "Nitrado_17516010", // uppercase provider + "nitrado-17516010", // wrong separator + "nitrado_175_160", // second underscore + "nitrado_17516010/evil", // path injection + "averyveryverylongprovidername_1", + "nitrado_1234567890123456789012345", + ] { + assert!(ServerId::parse(raw).is_err(), "should be rejected: {raw:?}"); + } + } + + #[test] + fn invalid_ids_are_rejected() { + let rejected = [ + "", + "not-a-uuid", + // Wrong group lengths. + "73e8fb7f-8789-4d88-8aa8-ea69b9aa092", + "73e8fb7f-8789-4d88-8aa8-ea69b9aa092ff", + "73e8fb7-88789-4d88-8aa8-ea69b9aa092f", + // Non-hex characters. + "73e8fb7g-8789-4d88-8aa8-ea69b9aa092f", + // Extra group. + "73e8fb7f-8789-4d88-8aa8-ea69b9aa092f-0000", + // Braced / URN forms are not what the API returns. + "{73e8fb7f-8789-4d88-8aa8-ea69b9aa092f}", + "urn:uuid:73e8fb7f-8789-4d88-8aa8-ea69b9aa092f", + // Whitespace must not be silently trimmed. + " 73e8fb7f-8789-4d88-8aa8-ea69b9aa092f", + "73e8fb7f-8789-4d88-8aa8-ea69b9aa092f ", + // Unpadded / no separators. + "73e8fb7f87894d888aa8ea69b9aa092f", + ]; + for raw in rejected { + assert!( + ServerId::parse(raw).is_err(), + "should have been rejected: {raw:?}" + ); + assert!(steam_join_uri_for(raw).is_err(), "{raw:?}"); + assert!(reforger_join_uri_for(raw).is_err(), "{raw:?}"); + } + } + + #[test] + fn injection_attempts_never_reach_a_uri() { + // These are the shapes that would matter if an id were ever pasted into + // a command line or a URI. Validation rejects them before that point. + let hostile = [ + "73e8fb7f-8789-4d88-8aa8-ea69b9aa092f/../../evil", + "73e8fb7f-8789-4d88-8aa8-ea69b9aa092f\"; calc.exe", + "73e8fb7f-8789-4d88-8aa8-ea69b9aa092f&&shutdown", + "$(whoami)-8789-4d88-8aa8-ea69b9aa092f", + "73e8fb7f-8789-4d88-8aa8-ea69b9aa09\n\r", + ]; + for raw in hostile { + assert_eq!( + ServerId::parse(raw), + Err(LaunchError::InvalidServerId { + got: raw.to_owned() + }) + ); + } + } + + #[test] + fn generated_uris_contain_only_url_safe_payload_characters() { + // Standard base64 can emit '+' and '/', which are legal in this Steam + // argument position; assert the payload never contains anything beyond + // that set so the URI is always well-formed. + let id = ServerId::parse(SAMPLE_ID).unwrap(); + let payload = join_payload_base64(&id); + assert!(payload + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'+' || b == b'/')); + } +} diff --git a/crates/rfm-core/src/lib.rs b/crates/rfm-core/src/lib.rs new file mode 100644 index 0000000..cb5ac4a --- /dev/null +++ b/crates/rfm-core/src/lib.rs @@ -0,0 +1,25 @@ +//! Core logic for the reforgermods.net launcher. +//! +//! This crate deliberately has no Tauri dependency. Everything that can be +//! decided without a window lives here so it can be unit-tested directly: +//! the reforgermods.net API client, Steam launch-URI construction, local +//! persistence, response caching, and local Workshop addon detection. + +pub mod api; +pub mod cache; +pub mod identity; +pub mod launch; +pub mod modscan; +pub mod ping; +pub mod presence; +pub mod readiness; +pub mod store; + +pub use api::{ApiClient, ApiError}; +pub use launch::{reforger_join_uri, steam_join_uri, LaunchError, ServerId}; + +/// User-Agent the launcher identifies itself with. The launcher is a public, +/// untrusted client: it sends no credentials, only this. +pub fn user_agent() -> String { + format!("reforgermods.net-launcher/{}", env!("CARGO_PKG_VERSION")) +} diff --git a/crates/rfm-core/src/modscan.rs b/crates/rfm-core/src/modscan.rs new file mode 100644 index 0000000..dee0ee1 --- /dev/null +++ b/crates/rfm-core/src/modscan.rs @@ -0,0 +1,893 @@ +//! Local Arma Reforger Workshop addon detection. +//! +//! # Status +//! +//! This module is the *abstraction plus a verified implementation* for reading +//! what the user already has installed. It is deliberately **not wired into the +//! milestone 1 UI**: the launcher shows no readiness indicator, because the one +//! piece the feature still needs — a supported way to trigger installation of +//! missing mods before joining — has not been verified. See +//! `docs/mod-readiness.md` for the full investigation. +//! +//! # What was verified +//! +//! Arma Reforger keeps downloaded Workshop content in the user's profile +//! directory, one directory per addon, named `_<16-hex-mod-id>`: +//! +//! ```text +//! %USERPROFILE%\Documents\My Games\ArmaReforger\addons\AKI_Core_62CCD69DD17E4F2F\ +//! ServerData.json <- id, name, installed revision.version +//! meta <- full Workshop metadata, dependencies, sizes +//! addon.gproj <- GUID + dependency GUIDs +//! data.pak +//! ``` +//! +//! `ServerData.json` is the cheapest reliable source: ~250 bytes, present in +//! every addon directory observed (235 of 235), and its `revision.version` is +//! byte-identical to the `version` reforgermods.net reports for the same mod. +//! Both files are written UTF-8 **with a byte-order mark**, which is why parsing +//! strips one before handing the text to serde. + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use serde::{Deserialize, Serialize}; + +/// Directory name of the addon store inside the Reforger profile directory. +const ADDONS_DIR_NAME: &str = "addons"; +/// The per-addon file naming the installed revision. +const SERVER_DATA_FILE: &str = "ServerData.json"; +/// Development override pointing at an addon store directly. +/// +/// Useful when the game's profile is not where this platform would look — for +/// example running the launcher under WSL against a Windows install, or testing +/// against a copied store. +const ADDONS_DIR_ENV: &str = "RFM_ADDONS_DIR"; + +/// Errors from scanning the local addon store. +#[derive(Debug, thiserror::Error)] +pub enum ScanError { + /// No addon directory was found. Normal when Reforger has never been run, + /// or when the profile lives somewhere non-default. + #[error("no Arma Reforger addons directory found")] + AddonsDirNotFound, + #[error("could not read addons directory {path}")] + Read { + path: PathBuf, + #[source] + source: std::io::Error, + }, +} + +/// One Workshop addon present on disk. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InstalledAddon { + /// Workshop mod id, upper-case 16 hex characters, matching the API. + pub id: String, + /// Addon name as recorded locally. Untrusted text. + pub name: String, + /// Installed revision, e.g. `8.1.1`. Empty when the file did not name one. + pub version: String, +} + +/// How a server's required mod set compares to what is installed. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModReadiness { + /// Installed at exactly the version the server asks for. + pub up_to_date: Vec, + /// Installed, but at a different version. + pub outdated: Vec, + /// Not installed at all. + pub missing: Vec, + /// Sum of `size` for missing mods whose size the API resolved. A floor, not + /// a total: see `unresolved_bytes`. + pub missing_bytes: u64, + /// Missing mods whose download size the API could not resolve. While this is + /// non-zero, `missing_bytes` understates the real download. + pub unresolved_mods: u32, +} + +impl ModReadiness { + /// True when every required mod is installed at the required version. + pub fn is_ready(&self) -> bool { + self.outdated.is_empty() && self.missing.is_empty() + } +} + +/// A required mod, as reported by a server. +/// +/// A narrow view of [`crate::api::models::ServerMod`] so this module does not +/// depend on the API model shape. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RequiredMod { + pub id: String, + pub version: String, + /// Download size in bytes when the API resolved it. + pub size: Option, +} + +/// A source of installed-addon information. +/// +/// The trait exists so readiness logic can be tested against a fixed inventory, +/// and so a future platform with a different layout can be added without +/// touching callers. +pub trait AddonInventory { + /// Every addon currently installed. + fn installed(&self) -> Result, ScanError>; +} + +/// Reads the addon store from a directory on disk. +#[derive(Debug, Clone)] +pub struct FilesystemInventory { + addons_dir: PathBuf, +} + +impl FilesystemInventory { + /// Scans `addons_dir` directly. + pub fn new(addons_dir: impl Into) -> Self { + Self { + addons_dir: addons_dir.into(), + } + } + + /// Locates the addon store for this machine. + /// + /// `RFM_ADDONS_DIR` overrides the search when set. Otherwise the profile + /// locations for this platform are probed: the (often redirected) + /// `Documents` directory on Windows, and Reforger's Proton prefix inside + /// each Steam library on Linux. A platform with no known layout finds + /// nothing and returns [`ScanError::AddonsDirNotFound`] — which the UI + /// reports as "could not be checked", never as "nothing missing". + pub fn discover() -> Result { + let explicit = std::env::var_os(ADDONS_DIR_ENV).map(PathBuf::from); + addons_dir_candidates(explicit) + .into_iter() + .find(|p| p.is_dir()) + .map(Self::new) + .ok_or(ScanError::AddonsDirNotFound) + } + + /// The directory being scanned. + pub fn addons_dir(&self) -> &Path { + &self.addons_dir + } +} + +impl AddonInventory for FilesystemInventory { + fn installed(&self) -> Result, ScanError> { + let entries = std::fs::read_dir(&self.addons_dir).map_err(|source| { + if source.kind() == std::io::ErrorKind::NotFound { + ScanError::AddonsDirNotFound + } else { + ScanError::Read { + path: self.addons_dir.clone(), + source, + } + } + })?; + + let mut addons = Vec::new(); + for entry in entries.flatten() { + let path = entry.path(); + if !path.is_dir() { + // The store also holds stray preview images and a `saves` + // directory; anything without a ServerData.json is skipped below. + continue; + } + let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) else { + continue; + }; + let Some(id_from_dir) = addon_id_from_dir_name(dir_name) else { + continue; + }; + // Prefer the id the file states; fall back to the directory name. + let addon = std::fs::read_to_string(path.join(SERVER_DATA_FILE)) + .ok() + .and_then(|raw| parse_server_data(&raw)) + .unwrap_or_else(|| InstalledAddon { + id: id_from_dir, + name: String::new(), + version: String::new(), + }); + addons.push(addon); + } + addons.sort_by(|a, b| a.id.cmp(&b.id)); + Ok(addons) + } +} + +/// Compares a server's required mods against an installed inventory. +pub fn readiness(required: &[RequiredMod], installed: &[InstalledAddon]) -> ModReadiness { + let local: HashMap = installed + .iter() + .map(|a| (a.id.to_ascii_uppercase(), a.version.as_str())) + .collect(); + + let mut out = ModReadiness::default(); + for req in required { + let key = req.id.to_ascii_uppercase(); + match local.get(&key) { + Some(version) if *version == req.version => out.up_to_date.push(key), + Some(_) => out.outdated.push(key), + None => { + match req.size { + Some(size) => out.missing_bytes += size, + None => out.unresolved_mods += 1, + } + out.missing.push(key); + } + } + } + out +} + +/// Extracts the Workshop id from an addon directory name. +/// +/// The id is the trailing `_`-separated group and must be exactly 16 hex +/// characters. Addon names themselves contain `_`, so the split is from the +/// right, and directories that do not match (`saves`, stray files) yield `None`. +pub fn addon_id_from_dir_name(name: &str) -> Option { + let (_, id) = name.rsplit_once('_')?; + (id.len() == 16 && id.bytes().all(|b| b.is_ascii_hexdigit())).then(|| id.to_ascii_uppercase()) +} + +/// Parses an addon's `ServerData.json`. +/// +/// The file is UTF-8 with a BOM; the BOM is stripped before parsing because +/// serde_json rejects it as unexpected input. +pub fn parse_server_data(raw: &str) -> Option { + #[derive(Deserialize)] + struct Revision { + #[serde(default)] + version: String, + } + #[derive(Deserialize)] + struct ServerData { + id: String, + #[serde(default)] + name: String, + #[serde(default)] + revision: Option, + } + + let cleaned = raw.trim_start_matches('\u{feff}').trim(); + let data: ServerData = serde_json::from_str(cleaned).ok()?; + let id = data.id.trim().to_ascii_uppercase(); + if id.len() != 16 || !id.bytes().all(|b| b.is_ascii_hexdigit()) { + return None; + } + Some(InstalledAddon { + id, + name: data.name, + version: data.revision.map(|r| r.version).unwrap_or_default(), + }) +} + +/// Candidate addon-store locations, most likely first. +/// +/// An `explicit` override short-circuits the search entirely: if the caller +/// named a directory, silently falling back to a different one would be worse +/// than reporting that the named one is not there. +fn addons_dir_candidates(explicit: Option) -> Vec { + if let Some(dir) = explicit { + return vec![dir]; + } + let profile_suffix = Path::new("My Games").join("ArmaReforger"); + documents_roots() + .into_iter() + .map(|root| root.join(&profile_suffix).join(ADDONS_DIR_NAME)) + .collect() +} + +/// Locations of the Windows `Documents` directory holding the Reforger profile. +/// +/// `Documents` is frequently redirected — OneDrive being the common case — so +/// the registry's own answer is asked for first and the well-known layouts are +/// kept as fallbacks rather than assumed. +#[cfg(windows)] +fn documents_roots() -> Vec { + let mut roots: Vec = Vec::new(); + // Authoritative: this is where Explorer itself resolves Documents to, and it + // follows a redirection to any path, not only the two guessed below. + if let Some(personal) = documents_from_registry() { + push_unique(&mut roots, personal); + } + if let Some(profile) = std::env::var_os("USERPROFILE").map(PathBuf::from) { + push_unique(&mut roots, profile.join("Documents")); + push_unique(&mut roots, profile.join("OneDrive").join("Documents")); + } + // OneDrive exports its own root; honour it rather than guessing the folder name. + if let Some(onedrive) = std::env::var_os("OneDrive").map(PathBuf::from) { + push_unique(&mut roots, onedrive.join("Documents")); + } + roots +} + +/// Reads the user's real `Documents` location from the shell folder registry. +/// +/// `Shell Folders` rather than `User Shell Folders`: the former stores the +/// already-expanded path, so no environment expansion is needed here. +#[cfg(windows)] +fn documents_from_registry() -> Option { + use winreg::enums::HKEY_CURRENT_USER; + use winreg::RegKey; + + let key = RegKey::predef(HKEY_CURRENT_USER) + .open_subkey(r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders") + .ok()?; + let personal: String = key.get_value("Personal").ok()?; + let trimmed = personal.trim(); + if trimmed.is_empty() { + return None; + } + Some(PathBuf::from(trimmed)) +} + +/// Locations of the Reforger profile's `Documents` directory on Linux. +/// +/// There is no native Linux client: Reforger runs under Proton, so its +/// "`%USERPROFILE%\Documents`" is a directory inside the Wine prefix Steam keeps +/// per app, under whichever library folder the game was installed into: +/// +/// ```text +/// /steamapps/compatdata/1874880/pfx/drive_c/users/steamuser/Documents/ +/// ``` +/// +/// Every known Steam root is probed, including the Flatpak one, and each root's +/// `libraryfolders.vdf` is read so a game on a second drive is found too — the +/// prefix lives in the library the game was installed into, which is routinely +/// not the one Steam itself lives in. +#[cfg(target_os = "linux")] +fn documents_roots() -> Vec { + let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else { + return Vec::new(); + }; + documents_roots_in(&home) +} + +/// [`documents_roots`] against an explicit home directory. +/// +/// Split out so the whole chain — Steam roots, `libraryfolders.vdf`, the prefix +/// layout — can be exercised against a synthetic tree, rather than only its +/// pieces in isolation. +#[cfg(target_os = "linux")] +fn documents_roots_in(home: &Path) -> Vec { + steam_libraries_in(home) + .iter() + .map(|library| proton_documents_dir(library)) + .collect() +} + +/// Platforms whose layout has not been verified get no candidates at all, which +/// the UI reports as "could not be checked" — never as "nothing missing". +#[cfg(not(any(windows, target_os = "linux")))] +fn documents_roots() -> Vec { + Vec::new() +} + +/// The Proton prefix's `Documents` directory for Reforger inside `library`. +#[cfg(target_os = "linux")] +fn proton_documents_dir(library: &Path) -> PathBuf { + library + .join("steamapps") + .join("compatdata") + .join(crate::launch::ARMA_REFORGER_APP_ID.to_string()) + .join("pfx") + .join("drive_c") + .join("users") + // Proton always names the prefix user `steamuser`, regardless of the + // real account name. + .join("steamuser") + .join("Documents") +} + +/// Every Steam library folder on this machine, most likely first. +#[cfg(target_os = "linux")] +fn steam_libraries_in(home: &Path) -> Vec { + let mut libraries: Vec = Vec::new(); + for root in steam_roots_in(home) { + // The Steam root is itself a library. + push_unique(&mut libraries, root.clone()); + let vdf = root.join("steamapps").join("libraryfolders.vdf"); + let Ok(raw) = std::fs::read_to_string(&vdf) else { + continue; + }; + for extra in library_paths_from_vdf(&raw) { + push_unique(&mut libraries, extra); + } + } + libraries +} + +/// Well-known Steam installation roots under `home`. +/// +/// `.steam/steam` and `.steam/root` are usually symlinks into one of the others; +/// they are probed anyway because they are the stable names, and duplicates are +/// dropped by path rather than by target. +#[cfg(target_os = "linux")] +fn steam_roots_in(home: &Path) -> Vec { + let mut roots: Vec = Vec::new(); + for relative in [ + ".steam/steam", + ".steam/root", + ".local/share/Steam", + // Flatpak Steam keeps its own home directory. + ".var/app/com.valvesoftware.Steam/.local/share/Steam", + ] { + push_unique(&mut roots, home.join(relative)); + } + roots +} + +/// Extracts library paths from `libraryfolders.vdf`. +/// +/// Deliberately not a VDF parser. The file is Valve's KeyValues format and the +/// only thing needed from it is the `"path"` of each entry, so this reads those +/// lines and ignores the rest; a malformed or reorganised file yields fewer +/// candidates rather than an error. +#[cfg(target_os = "linux")] +fn library_paths_from_vdf(raw: &str) -> Vec { + let mut paths = Vec::new(); + for line in raw.lines() { + // `"path"\t\t"/mnt/games/SteamLibrary"` + let Some(rest) = line.trim().strip_prefix("\"path\"") else { + continue; + }; + let mut fields = rest.split('"'); + let _separator = fields.next(); + let Some(value) = fields.next() else { + continue; + }; + if value.is_empty() { + continue; + } + // KeyValues escapes backslashes; Windows-style paths appear in the file + // on Windows installs and are harmless to unescape everywhere. + paths.push(PathBuf::from(value.replace("\\\\", "\\"))); + } + paths +} + +/// Appends `path` unless it is already present, preserving search order. +#[cfg(any(windows, target_os = "linux"))] +fn push_unique(paths: &mut Vec, path: PathBuf) { + if !paths.contains(&path) { + paths.push(path); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A real `ServerData.json`, BOM included, from + /// `addons/AKI_Core_62CCD69DD17E4F2F`. + const REAL_SERVER_DATA: &str = "\u{feff}{\n \"id\": \"62CCD69DD17E4F2F\",\n \"name\": \"AKI_Core\",\n \"revision\": {\"version\":\"8.1.1\",\"gameVersion\":\"\",\"changelog\":\"\",\"corrupted\":false,\"dependencies\":[{\"assetId\":\"629B2BA37EFFD577\",\"assetName\":\"WCS_Armaments\",\"version\":\"8.1.2\"}],\"scenarios\":[],\"env\":7\n }\n}"; + + fn addon(id: &str, version: &str) -> InstalledAddon { + InstalledAddon { + id: id.into(), + name: "n".into(), + version: version.into(), + } + } + + fn required(id: &str, version: &str, size: Option) -> RequiredMod { + RequiredMod { + id: id.into(), + version: version.into(), + size, + } + } + + #[test] + fn parses_a_real_server_data_file_including_its_bom() { + let a = parse_server_data(REAL_SERVER_DATA).unwrap(); + assert_eq!(a.id, "62CCD69DD17E4F2F"); + assert_eq!(a.name, "AKI_Core"); + // This is the exact version api.reforgermods.net reports for this mod. + assert_eq!(a.version, "8.1.1"); + } + + #[test] + fn parses_without_a_bom_too() { + let a = parse_server_data(REAL_SERVER_DATA.trim_start_matches('\u{feff}')).unwrap(); + assert_eq!(a.version, "8.1.1"); + } + + #[test] + fn lowercase_ids_are_normalised_upwards() { + let a = + parse_server_data(r#"{"id":"62ccd69dd17e4f2f","revision":{"version":"1.0"}}"#).unwrap(); + assert_eq!(a.id, "62CCD69DD17E4F2F"); + } + + #[test] + fn missing_revision_yields_an_empty_version_not_a_failure() { + let a = parse_server_data(r#"{"id":"62CCD69DD17E4F2F","name":"x"}"#).unwrap(); + assert_eq!(a.version, ""); + } + + #[test] + fn malformed_server_data_is_rejected() { + for raw in [ + "", + "not json", + r#"{"name":"no id"}"#, + // Wrong id length. + r#"{"id":"ABC"}"#, + r#"{"id":"62CCD69DD17E4F2FF"}"#, + // Non-hex. + r#"{"id":"ZZCCD69DD17E4F2F"}"#, + ] { + assert!(parse_server_data(raw).is_none(), "{raw:?}"); + } + } + + #[test] + fn addon_id_is_read_from_the_directory_name() { + assert_eq!( + addon_id_from_dir_name("AKI_Core_62CCD69DD17E4F2F").as_deref(), + Some("62CCD69DD17E4F2F") + ); + // Names contain underscores, so the split must be from the right. + assert_eq!( + addon_id_from_dir_name("B59-Shotguns-BETA_658B2B37C2C82AAE").as_deref(), + Some("658B2B37C2C82AAE") + ); + assert_eq!( + addon_id_from_dir_name("ArmaverseFactionsReforged_6942069420694200").as_deref(), + Some("6942069420694200") + ); + } + + #[test] + fn non_addon_directory_names_are_skipped() { + // These are real entries that sit alongside addons in the store. + for name in [ + "saves", + "scenario0_576x324.jpg", + "NoIdHere", + "Short_ABC", + "Bad_ZZZZZZZZZZZZZZZZ", + "_", + "", + ] { + assert!(addon_id_from_dir_name(name).is_none(), "{name:?}"); + } + } + + #[test] + fn readiness_reports_a_fully_installed_server_as_ready() { + let installed = vec![ + addon("AAAAAAAAAAAAAAAA", "1.0"), + addon("BBBBBBBBBBBBBBBB", "2.0"), + ]; + let required = vec![ + required("AAAAAAAAAAAAAAAA", "1.0", Some(10)), + required("BBBBBBBBBBBBBBBB", "2.0", Some(20)), + ]; + let r = readiness(&required, &installed); + assert!(r.is_ready()); + assert_eq!(r.up_to_date.len(), 2); + assert_eq!(r.missing_bytes, 0); + } + + #[test] + fn readiness_separates_outdated_from_missing() { + let installed = vec![ + addon("AAAAAAAAAAAAAAAA", "1.0"), + addon("BBBBBBBBBBBBBBBB", "1.9"), + ]; + let required = vec![ + required("AAAAAAAAAAAAAAAA", "1.0", Some(10)), + required("BBBBBBBBBBBBBBBB", "2.0", Some(20)), + required("CCCCCCCCCCCCCCCC", "3.0", Some(300)), + ]; + let r = readiness(&required, &installed); + assert!(!r.is_ready()); + assert_eq!(r.up_to_date, vec!["AAAAAAAAAAAAAAAA"]); + assert_eq!(r.outdated, vec!["BBBBBBBBBBBBBBBB"]); + assert_eq!(r.missing, vec!["CCCCCCCCCCCCCCCC"]); + // Only missing mods need downloading; an outdated mod's delta is unknown. + assert_eq!(r.missing_bytes, 300); + } + + #[test] + fn readiness_matching_is_case_insensitive_on_ids() { + let installed = vec![addon("62ccd69dd17e4f2f", "8.1.1")]; + let required = vec![required("62CCD69DD17E4F2F", "8.1.1", None)]; + assert!(readiness(&required, &installed).is_ready()); + } + + #[test] + fn unresolved_sizes_are_counted_separately_from_bytes() { + let required = vec![ + required("AAAAAAAAAAAAAAAA", "1.0", None), + required("BBBBBBBBBBBBBBBB", "1.0", Some(500)), + ]; + let r = readiness(&required, &[]); + assert_eq!(r.missing.len(), 2); + assert_eq!(r.missing_bytes, 500); + // The caller must be able to say "at least 500 bytes, plus 1 unknown". + assert_eq!(r.unresolved_mods, 1); + } + + #[test] + fn a_vanilla_server_is_ready_against_an_empty_inventory() { + let r = readiness(&[], &[]); + assert!(r.is_ready()); + assert_eq!(r.missing_bytes, 0); + } + + #[test] + fn extra_installed_mods_do_not_affect_readiness() { + let installed = vec![addon("AAAAAAAAAAAAAAAA", "1.0"), addon("ZZZZ", "9")]; + let required = vec![required("AAAAAAAAAAAAAAAA", "1.0", None)]; + assert!(readiness(&required, &installed).is_ready()); + } + + #[test] + fn scanning_a_synthetic_store_finds_addons_and_ignores_noise() { + let root = std::env::temp_dir().join(format!("rfm-modscan-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&root); + let addons = root.join("addons"); + for (dir, body) in [ + ( + "AKI_Core_62CCD69DD17E4F2F", + Some(REAL_SERVER_DATA.to_string()), + ), + ( + "Other_Mod_AAAAAAAAAAAAAAAA", + Some( + r#"{"id":"AAAAAAAAAAAAAAAA","name":"Other","revision":{"version":"2.5"}}"# + .into(), + ), + ), + // Valid directory name but no ServerData.json: still counted, since + // the id is recoverable, but with no version. + ("Broken_BBBBBBBBBBBBBBBB", None), + // Not an addon directory at all. + ("saves", None), + ] { + let path = addons.join(dir); + std::fs::create_dir_all(&path).unwrap(); + if let Some(body) = body { + std::fs::write(path.join(SERVER_DATA_FILE), body).unwrap(); + } + } + // A stray file alongside the directories. + std::fs::write(addons.join("scenario0_576x324.jpg"), b"not a dir").unwrap(); + + let found = FilesystemInventory::new(&addons).installed().unwrap(); + let ids: Vec<&str> = found.iter().map(|a| a.id.as_str()).collect(); + // Sorted by id: ASCII digits precede letters. + assert_eq!( + ids, + vec!["62CCD69DD17E4F2F", "AAAAAAAAAAAAAAAA", "BBBBBBBBBBBBBBBB"] + ); + let aki = found.iter().find(|a| a.id == "62CCD69DD17E4F2F").unwrap(); + assert_eq!(aki.version, "8.1.1"); + let broken = found.iter().find(|a| a.id == "BBBBBBBBBBBBBBBB").unwrap(); + assert_eq!(broken.version, ""); + + std::fs::remove_dir_all(&root).unwrap(); + } + + #[test] + fn an_explicit_directory_short_circuits_the_search() { + let candidates = addons_dir_candidates(Some(PathBuf::from("/somewhere/addons"))); + assert_eq!(candidates, vec![PathBuf::from("/somewhere/addons")]); + } + + #[test] + fn without_an_override_only_profile_locations_are_probed() { + for candidate in addons_dir_candidates(None) { + // Every default candidate ends at the addon store, never at a + // parent the scanner would misread. + assert!(candidate.ends_with(ADDONS_DIR_NAME), "{candidate:?}"); + assert!(candidate.to_string_lossy().contains("ArmaReforger")); + } + } + + #[cfg(target_os = "linux")] + mod linux_discovery { + use super::super::*; + + /// A real `libraryfolders.vdf`, trimmed: two libraries, one of them on a + /// second drive, with the surrounding keys the file actually carries. + const VDF: &str = r#" +"libraryfolders" +{ + "0" + { + "path" "/home/madison/.local/share/Steam" + "label" "" + "contentid" "1234567890123456789" + "totalsize" "0" + "apps" + { + "1874880" "27917287424" + } + } + "1" + { + "path" "/mnt/games/SteamLibrary" + "label" "games" + "apps" + { + "221100" "29249693425" + } + } +} +"#; + + #[test] + fn every_library_path_is_read_from_the_vdf() { + assert_eq!( + library_paths_from_vdf(VDF), + vec![ + PathBuf::from("/home/madison/.local/share/Steam"), + PathBuf::from("/mnt/games/SteamLibrary"), + ] + ); + } + + #[test] + fn non_path_keys_are_ignored_rather_than_misread() { + // "label" and the app-size entries are the lines most likely to be + // mistaken for a path by a loose parser. + assert!(library_paths_from_vdf("\t\"label\"\t\t\"games\"").is_empty()); + assert!(library_paths_from_vdf("\t\t\"1874880\"\t\t\"27917287424\"").is_empty()); + assert!(library_paths_from_vdf("nonsense").is_empty()); + assert!(library_paths_from_vdf("").is_empty()); + } + + #[test] + fn a_library_yields_the_proton_prefix_documents_directory() { + let documents = proton_documents_dir(Path::new("/mnt/games/SteamLibrary")); + let expected: PathBuf = [ + "/mnt/games/SteamLibrary", + "steamapps", + "compatdata", + "1874880", + "pfx", + "drive_c", + "users", + "steamuser", + "Documents", + ] + .iter() + .collect(); + assert_eq!(documents, expected); + } + + #[test] + fn candidates_point_inside_a_proton_prefix() { + // HOME is set in any normal environment, including the build + // container; without it there is nothing to probe and that is fine. + if std::env::var_os("HOME").is_none() { + return; + } + let candidates = addons_dir_candidates(None); + assert!(!candidates.is_empty()); + for candidate in &candidates { + let shown = candidate.to_string_lossy(); + assert!(shown.contains("compatdata/1874880"), "{shown}"); + assert!(shown.contains("drive_c/users/steamuser"), "{shown}"); + assert!(shown.ends_with("My Games/ArmaReforger/addons"), "{shown}"); + } + } + + #[test] + fn the_flatpak_steam_root_is_probed_too() { + let roots = steam_roots_in(Path::new("/home/madison")); + assert!( + roots + .iter() + .any(|r| r.to_string_lossy().contains("com.valvesoftware.Steam")), + "{roots:?}" + ); + // The stable symlink names and the real directory are distinct + // paths, so all of them are probed. + assert!(roots.len() >= 4, "{roots:?}"); + } + + /// The whole Linux chain against a synthetic install. + /// + /// The unit tests above each cover one link — the vdf, the prefix + /// layout, the scanner. This one builds a Steam tree with the game on a + /// *second* library, which is the case that actually breaks a naive + /// implementation: the prefix lives in the library the game was + /// installed into, not the one Steam itself lives in. + #[test] + fn a_game_on_a_second_library_drive_is_found_and_scanned() { + let root = std::env::temp_dir().join(format!( + "rfm-proton-{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + let home = root.join("home/madison"); + let steam = home.join(".local/share/Steam"); + let second_library = root.join("mnt/games/SteamLibrary"); + + // Steam's own library, holding only the index that names the other. + std::fs::create_dir_all(steam.join("steamapps")).unwrap(); + std::fs::write( + steam.join("steamapps/libraryfolders.vdf"), + format!( + "\"libraryfolders\"\n{{\n\t\"0\"\n\t{{\n\t\t\"path\"\t\t\"{}\"\n\t}}\n\ +\t\"1\"\n\t{{\n\t\t\"path\"\t\t\"{}\"\n\t}}\n}}\n", + steam.display(), + second_library.display() + ), + ) + .unwrap(); + + // The addon store, inside the Proton prefix on the second library. + let addons = proton_documents_dir(&second_library) + .join("My Games") + .join("ArmaReforger") + .join(ADDONS_DIR_NAME); + let addon = addons.join("AKI_Core_62CCD69DD17E4F2F"); + std::fs::create_dir_all(&addon).unwrap(); + // Written with the BOM the game really writes. + std::fs::write( + addon.join(SERVER_DATA_FILE), + "\u{feff}{\"id\":\"62CCD69DD17E4F2F\",\"name\":\"AKI_Core\",\ +\"revision\":{\"version\":\"8.1.1\"}}", + ) + .unwrap(); + + // Discovery, exactly as discover() performs it. + let found = documents_roots_in(&home) + .into_iter() + .map(|documents| { + documents + .join("My Games") + .join("ArmaReforger") + .join(ADDONS_DIR_NAME) + }) + .find(|candidate| candidate.is_dir()); + assert_eq!( + found.as_deref(), + Some(addons.as_path()), + "store not located" + ); + + // And the scan of what discovery found. + let installed = FilesystemInventory::new(found.unwrap()) + .installed() + .unwrap(); + assert_eq!( + installed, + vec![InstalledAddon { + id: "62CCD69DD17E4F2F".into(), + name: "AKI_Core".into(), + version: "8.1.1".into(), + }] + ); + + std::fs::remove_dir_all(&root).unwrap(); + } + + #[test] + fn duplicate_library_paths_are_probed_once() { + let mut paths = vec![PathBuf::from("/a")]; + push_unique(&mut paths, PathBuf::from("/a")); + push_unique(&mut paths, PathBuf::from("/b")); + assert_eq!(paths, vec![PathBuf::from("/a"), PathBuf::from("/b")]); + } + } + + #[test] + fn a_missing_store_reports_not_found_rather_than_an_io_error() { + let missing = std::env::temp_dir().join("rfm-definitely-not-here-4a7f"); + let err = FilesystemInventory::new(&missing).installed().unwrap_err(); + assert!(matches!(err, ScanError::AddonsDirNotFound), "{err:?}"); + } +} diff --git a/crates/rfm-core/src/ping.rs b/crates/rfm-core/src/ping.rs new file mode 100644 index 0000000..b5ecf71 --- /dev/null +++ b/crates/rfm-core/src/ping.rs @@ -0,0 +1,569 @@ +//! Direct ICMP latency measurement to game servers. +//! +//! A browser cannot do this; a native launcher can, which is one of the reasons +//! for building one. Measured against 20 live servers: `host` is present on +//! roughly two thirds of servers, is always a literal IP address, and 65% of +//! those answer ICMP echo with region-coherent latencies. +//! +//! # Why not TCP connect timing +//! +//! It was tried and it lies. Connecting to a Reforger server's UDP game port +//! "succeeded" in 12–18 ms — as did port 9 and port 47123, which are closed. +//! Some middlebox completes every handshake, so TCP timing produces +//! confident-looking numbers that are pure fiction. ICMP to the same hosts +//! returned 68–183 ms, coherent with their regions. Only ICMP is used. +//! +//! # Privileges +//! +//! Windows uses `IcmpSendEcho` through the IP Helper API, which needs no +//! elevation. Unix uses an unprivileged ICMP datagram socket, which works when +//! `net.ipv4.ping_group_range` covers the user's group — the default on current +//! distributions — and otherwise needs `CAP_NET_RAW`. A failure to send is +//! reported as "no reply", never as a fabricated number. + +use std::net::IpAddr; +use std::time::Duration; + +use serde::{Deserialize, Serialize}; + +/// Echo requests sent per measurement. +pub const DEFAULT_ATTEMPTS: usize = 3; +/// Per-request deadline. +pub const DEFAULT_TIMEOUT: Duration = Duration::from_millis(1500); + +/// The outcome of measuring one host. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PingSummary { + /// Fastest round trip observed, milliseconds. + pub best_ms: u32, + /// Median round trip, milliseconds. Preferred for display: one delayed + /// packet should not change the figure the user reads. + pub median_ms: u32, + /// Replies received. + pub received: u32, + /// Requests sent. + pub sent: u32, +} + +impl PingSummary { + /// Percentage of requests that went unanswered, 0–100. + pub fn loss_percent(&self) -> u32 { + if self.sent == 0 { + return 0; + } + ((self.sent - self.received) * 100) / self.sent + } +} + +/// Why a host could not be measured. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PingUnavailable { + /// The API did not report a host for this server. + NoHost, + /// The reported host was not a public literal IP address. + UnroutableHost, + /// Every echo request went unanswered. Common: many hosts filter ICMP. + NoReply, + /// The platform refused to send. On Unix this usually means ICMP sockets + /// are not permitted for this user. + NotPermitted, +} + +impl PingUnavailable { + /// Display text, written to be shown as-is. + pub fn message(self) -> &'static str { + match self { + PingUnavailable::NoHost => "This server does not publish an address to measure.", + PingUnavailable::UnroutableHost => { + "This server's published address cannot be measured." + } + PingUnavailable::NoReply => "No reply — this server does not answer ping.", + PingUnavailable::NotPermitted => { + "This system does not permit the launcher to send ping requests." + } + } + } +} + +/// A measurement result for the UI: a figure, or a stated reason there is none. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PingResult { + #[serde(skip_serializing_if = "Option::is_none")] + pub summary: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub unavailable: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, +} + +impl PingResult { + /// A successful measurement. + pub fn measured(summary: PingSummary) -> Self { + Self { + summary: Some(summary), + unavailable: None, + message: None, + } + } + + /// A stated absence of a measurement. + pub fn unavailable(reason: PingUnavailable) -> Self { + Self { + summary: None, + unavailable: Some(reason), + message: Some(reason.message().to_string()), + } + } +} + +/// Resolves a ping-site address into a pingable public IP. +/// +/// Ping sites are published as hostnames (`ping-location-de.nitrado.net`), +/// unlike servers, which publish literal IPs. Resolving remote-supplied names +/// is a wider door than [`parse_target`] allows, so the result is put through +/// the same public-address check: a name that resolves into loopback, private +/// or link-local space is refused exactly as a literal would be. +pub async fn resolve_public(address: &str) -> Result { + let address = address.trim(); + if address.is_empty() { + return Err(PingUnavailable::NoHost); + } + // A literal needs no lookup. + if let Ok(ip) = address.parse::() { + return if is_public(&ip) { + Ok(ip) + } else { + Err(PingUnavailable::UnroutableHost) + }; + } + // Reject anything that is not a plain hostname before it reaches a resolver. + if !address + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'-') + { + return Err(PingUnavailable::UnroutableHost); + } + + // `lookup_host` wants a port; 0 is fine, nothing connects. + let resolved = tokio::net::lookup_host((address, 0)) + .await + .map_err(|_| PingUnavailable::UnroutableHost)?; + resolved + .map(|addr| addr.ip()) + .find(is_public) + .ok_or(PingUnavailable::UnroutableHost) +} + +/// Measures a ping-site address, resolving it first. +pub async fn measure_address(address: &str, attempts: usize, timeout: Duration) -> PingResult { + let ip = match resolve_public(address).await { + Ok(ip) => ip, + Err(reason) => return PingResult::unavailable(reason), + }; + match platform::probe(ip, attempts.max(1), timeout).await { + Ok(samples) => match summarise(&samples) { + Some(summary) => PingResult::measured(summary), + None => PingResult::unavailable(PingUnavailable::NoReply), + }, + Err(reason) => PingResult::unavailable(reason), + } +} + +/// Validates a host string from the API into something safe to probe. +/// +/// This is the security boundary for pinging. API-supplied text must not be +/// able to make the launcher emit packets anywhere it likes, so: +/// +/// - Only literal IP addresses are accepted. A hostname would mean a DNS lookup +/// chosen by remote data; every observed server publishes a literal IP anyway +/// (20 of 20 sampled). +/// - Loopback, private, link-local, multicast, broadcast and unspecified +/// addresses are refused, so a hostile or broken record cannot point the +/// launcher at the user's own machine or LAN. +pub fn parse_target(host: Option<&str>) -> Result { + let host = host.map(str::trim).filter(|h| !h.is_empty()); + let Some(host) = host else { + return Err(PingUnavailable::NoHost); + }; + let ip: IpAddr = host.parse().map_err(|_| PingUnavailable::UnroutableHost)?; + if is_public(&ip) { + Ok(ip) + } else { + Err(PingUnavailable::UnroutableHost) + } +} + +/// True for addresses it is reasonable to send an echo request to. +fn is_public(ip: &IpAddr) -> bool { + match ip { + IpAddr::V4(v4) => { + !(v4.is_loopback() + || v4.is_private() + || v4.is_link_local() + || v4.is_multicast() + || v4.is_broadcast() + || v4.is_unspecified() + || v4.is_documentation() + // 100.64.0.0/10, carrier-grade NAT. + || (v4.octets()[0] == 100 && (64..128).contains(&v4.octets()[1])) + // 0.0.0.0/8 and 240.0.0.0/4. + || v4.octets()[0] == 0 + || v4.octets()[0] >= 240) + } + IpAddr::V6(v6) => { + !(v6.is_loopback() + || v6.is_multicast() + || v6.is_unspecified() + // fe80::/10 link-local and fc00::/7 unique-local. + || (v6.segments()[0] & 0xffc0) == 0xfe80 + || (v6.segments()[0] & 0xfe00) == 0xfc00) + } + } +} + +/// Reduces raw samples to the figure shown to the user. +/// +/// `None` entries are unanswered requests. Returns `None` when nothing came +/// back at all, so the caller states "no reply" instead of rendering a zero. +pub fn summarise(samples: &[Option]) -> Option { + let mut received: Vec = samples + .iter() + .flatten() + .map(|d| { + // Round to the nearest millisecond, with a floor of 1: a sub- + // millisecond reply is real, and "0 ms" reads as a bug. + let ms = d.as_secs_f64() * 1000.0; + (ms.round() as u32).max(1) + }) + .collect(); + if received.is_empty() { + return None; + } + received.sort_unstable(); + + let median = if received.len() % 2 == 1 { + received[received.len() / 2] + } else { + let hi = received.len() / 2; + // Even counts average the middle pair. Written as an offset from the + // lower value rather than `(a + b) / 2` so a large pair cannot overflow, + // and without `u32::midpoint`, which postdates this crate's MSRV. + received[hi - 1] + (received[hi] - received[hi - 1]) / 2 + }; + + Some(PingSummary { + best_ms: received[0], + median_ms: median, + received: received.len() as u32, + sent: samples.len() as u32, + }) +} + +/// Measures `host`, returning either a figure or a stated reason there is none. +pub async fn measure(host: Option<&str>, attempts: usize, timeout: Duration) -> PingResult { + let ip = match parse_target(host) { + Ok(ip) => ip, + Err(reason) => return PingResult::unavailable(reason), + }; + match platform::probe(ip, attempts.max(1), timeout).await { + Ok(samples) => match summarise(&samples) { + Some(summary) => PingResult::measured(summary), + None => PingResult::unavailable(PingUnavailable::NoReply), + }, + Err(reason) => PingResult::unavailable(reason), + } +} + +#[cfg(unix)] +mod platform { + use super::*; + + /// Unprivileged ICMP datagram socket. Requires `net.ipv4.ping_group_range` + /// to cover the user's group, which is the default on current distributions. + pub async fn probe( + ip: IpAddr, + attempts: usize, + timeout: Duration, + ) -> Result>, PingUnavailable> { + use surge_ping::{Client, Config, PingIdentifier, PingSequence, ICMP}; + + let config = match ip { + IpAddr::V4(_) => Config::default(), + IpAddr::V6(_) => Config::builder().kind(ICMP::V6).build(), + }; + let client = Client::new(&config).map_err(|_| PingUnavailable::NotPermitted)?; + // The identifier only has to be unique among this process's in-flight + // pings; the low bits of the clock are sufficient and cheap. + let identifier = PingIdentifier(std::process::id() as u16); + let mut pinger = client.pinger(ip, identifier).await; + pinger.timeout(timeout); + + let payload = [0u8; 32]; + let mut samples = Vec::with_capacity(attempts); + for seq in 0..attempts { + let reply = pinger.ping(PingSequence(seq as u16), &payload).await; + samples.push(reply.ok().map(|(_, rtt)| rtt)); + } + Ok(samples) + } +} + +#[cfg(windows)] +mod platform { + use super::*; + + /// `IcmpSendEcho` through the IP Helper API, which needs no elevation. + /// It is blocking, so it runs on the blocking pool. + pub async fn probe( + ip: IpAddr, + attempts: usize, + timeout: Duration, + ) -> Result>, PingUnavailable> { + let timeout_ms = timeout.as_millis().min(u32::MAX as u128) as u32; + tokio::task::spawn_blocking(move || { + let mut pinger = winping::Pinger::new().map_err(|_| PingUnavailable::NotPermitted)?; + pinger.set_timeout(timeout_ms); + let mut buffer = winping::Buffer::new(); + let mut samples = Vec::with_capacity(attempts); + for _ in 0..attempts { + // `send` reports the round trip in whole milliseconds; a + // timeout or filtered host comes back as an error, which is a + // missing sample rather than a failure of the measurement. + let rtt = pinger + .send(ip, &mut buffer) + .ok() + .map(|ms| Duration::from_millis(u64::from(ms))); + samples.push(rtt); + } + Ok(samples) + }) + .await + .map_err(|_| PingUnavailable::NotPermitted)? + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn ms(value: u64) -> Option { + Some(Duration::from_millis(value)) + } + + #[test] + fn real_server_addresses_are_accepted() { + // Literal IPs observed on live servers. + for host in ["208.92.232.141", "103.193.80.16", "69.67.175.16"] { + assert!(parse_target(Some(host)).is_ok(), "{host}"); + } + } + + #[test] + fn a_missing_host_is_distinguished_from_a_bad_one() { + assert_eq!(parse_target(None), Err(PingUnavailable::NoHost)); + assert_eq!(parse_target(Some("")), Err(PingUnavailable::NoHost)); + assert_eq!(parse_target(Some(" ")), Err(PingUnavailable::NoHost)); + } + + #[test] + fn hostnames_are_refused_so_api_data_cannot_drive_dns() { + for host in [ + "example.com", + "localhost", + "evil.attacker.test", + "ping-location-de.nitrado.net", + ] { + assert_eq!( + parse_target(Some(host)), + Err(PingUnavailable::UnroutableHost), + "{host}" + ); + } + } + + #[test] + fn private_and_loopback_addresses_are_refused() { + // A hostile or broken record must not aim the launcher at the user's + // own machine or LAN. + for host in [ + "127.0.0.1", + "0.0.0.0", + "10.0.0.1", + "172.16.4.9", + "192.168.1.1", + "169.254.1.1", + "224.0.0.1", + "255.255.255.255", + "100.64.0.1", + "240.0.0.1", + "::1", + "::", + "fe80::1", + "fd00::1", + "ff02::1", + ] { + assert_eq!( + parse_target(Some(host)), + Err(PingUnavailable::UnroutableHost), + "{host} must be refused" + ); + } + } + + #[test] + fn public_ipv6_is_accepted() { + assert!(parse_target(Some("2606:4700:4700::1111")).is_ok()); + } + + #[test] + fn garbage_hosts_are_refused() { + for host in [ + "208.92.232.141; rm -rf /", + "208.92.232.141:2001", + "not an ip", + "999.999.999.999", + "../../etc/passwd", + ] { + assert_eq!( + parse_target(Some(host)), + Err(PingUnavailable::UnroutableHost), + "{host}" + ); + } + } + + #[test] + fn summarise_reports_best_and_median() { + let summary = summarise(&[ms(80), ms(70), ms(120)]).unwrap(); + assert_eq!(summary.best_ms, 70); + assert_eq!(summary.median_ms, 80); + assert_eq!(summary.received, 3); + assert_eq!(summary.sent, 3); + assert_eq!(summary.loss_percent(), 0); + } + + #[test] + fn the_median_resists_one_delayed_packet() { + // A single 900 ms outlier must not become the displayed figure. + let summary = summarise(&[ms(70), ms(72), ms(900)]).unwrap(); + assert_eq!(summary.median_ms, 72); + assert_eq!(summary.best_ms, 70); + } + + #[test] + fn partial_loss_is_summarised_not_discarded() { + let summary = summarise(&[ms(80), None, ms(100)]).unwrap(); + assert_eq!(summary.received, 2); + assert_eq!(summary.sent, 3); + assert_eq!(summary.loss_percent(), 33); + assert_eq!(summary.best_ms, 80); + } + + #[test] + fn total_loss_yields_no_figure_rather_than_zero() { + assert!(summarise(&[None, None, None]).is_none()); + assert!(summarise(&[]).is_none()); + } + + #[test] + fn even_sample_counts_average_the_middle_pair() { + let summary = summarise(&[ms(70), ms(80), ms(90), ms(100)]).unwrap(); + assert_eq!(summary.median_ms, 85); + } + + #[test] + fn sub_millisecond_replies_floor_at_one() { + // A LAN-speed reply is real; "0 ms" reads as a broken measurement. + let summary = summarise(&[Some(Duration::from_micros(200))]).unwrap(); + assert_eq!(summary.best_ms, 1); + assert_eq!(summary.median_ms, 1); + } + + #[tokio::test] + async fn an_unmeasurable_host_states_why_without_sending_anything() { + let result = measure(None, 3, Duration::from_millis(50)).await; + assert_eq!(result.unavailable, Some(PingUnavailable::NoHost)); + assert!(result.summary.is_none()); + assert!(result.message.is_some()); + + let result = measure(Some("192.168.0.1"), 3, Duration::from_millis(50)).await; + assert_eq!(result.unavailable, Some(PingUnavailable::UnroutableHost)); + } + + #[tokio::test] + async fn resolve_public_accepts_a_literal_without_a_lookup() { + assert_eq!( + resolve_public("208.92.232.141").await, + Ok("208.92.232.141".parse().unwrap()) + ); + } + + #[tokio::test] + async fn resolve_public_refuses_names_that_are_not_hostnames() { + // Nothing resembling a URL, path or command reaches the resolver. + for address in [ + "", + " ", + "http://example.com", + "example.com/path", + "example.com:80", + "a b", + "host;rm -rf /", + "host|cat", + ] { + assert!(resolve_public(address).await.is_err(), "{address:?}"); + } + } + + #[tokio::test] + async fn resolve_public_refuses_private_literals() { + for address in ["127.0.0.1", "192.168.1.1", "::1", "10.0.0.5"] { + assert_eq!( + resolve_public(address).await, + Err(PingUnavailable::UnroutableHost), + "{address}" + ); + } + } + + #[tokio::test] + async fn measure_address_states_why_it_cannot_measure() { + let result = measure_address("", 1, Duration::from_millis(50)).await; + assert_eq!(result.unavailable, Some(PingUnavailable::NoHost)); + let result = measure_address("127.0.0.1", 1, Duration::from_millis(50)).await; + assert_eq!(result.unavailable, Some(PingUnavailable::UnroutableHost)); + } + + #[test] + fn every_unavailable_reason_has_distinct_copy() { + let reasons = [ + PingUnavailable::NoHost, + PingUnavailable::UnroutableHost, + PingUnavailable::NoReply, + PingUnavailable::NotPermitted, + ]; + let messages: std::collections::HashSet<&str> = + reasons.iter().map(|r| r.message()).collect(); + assert_eq!(messages.len(), reasons.len()); + } + + #[test] + fn results_serialise_without_null_noise() { + let json = serde_json::to_value(PingResult::measured(PingSummary { + best_ms: 70, + median_ms: 72, + received: 3, + sent: 3, + })) + .unwrap(); + assert_eq!(json["summary"]["medianMs"], 72); + assert!(json.get("unavailable").is_none()); + + let json = serde_json::to_value(PingResult::unavailable(PingUnavailable::NoReply)).unwrap(); + assert_eq!(json["unavailable"], "no_reply"); + assert!(json.get("summary").is_none()); + } +} diff --git a/crates/rfm-core/src/presence.rs b/crates/rfm-core/src/presence.rs new file mode 100644 index 0000000..933c1d1 --- /dev/null +++ b/crates/rfm-core/src/presence.rs @@ -0,0 +1,267 @@ +//! Discord Rich Presence payloads. +//! +//! What a Discord friend sees while the launcher is open — the promotional +//! surface. The transport lives in the app layer; everything here is pure text +//! assembly, which is the part with rules worth testing: +//! +//! - **Server names are untrusted.** They arrive from the API, routinely carry +//! Discord invites and decorative glyphs, and are sometimes very long. Control +//! characters are stripped and whitespace collapsed before anything is sent. +//! - **Discord silently drops fields that break its limits.** `details` and +//! `state` cap at 128 characters, and a field shorter than two characters is +//! ignored rather than rejected, so a one-character server name must not +//! produce an invisible line. +//! - **Truncation is by character, never by byte.** Cutting a multi-byte glyph +//! in half produces invalid UTF-8 that Discord rejects outright. + +use serde::{Deserialize, Serialize}; + +/// Discord's limit for `details` and `state`. +const FIELD_LIMIT: usize = 128; +/// Below this, Discord ignores the field entirely. +const FIELD_MINIMUM: usize = 2; + +/// One presence update. +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Presence { + /// Top line. + pub details: String, + /// Second line. + pub state: String, + /// Tooltip on the large artwork. + pub large_text: String, +} + +/// What the launcher is currently doing. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Activity<'a> { + /// Browsing the server list. + Browsing { online_servers: u64 }, + /// A server is selected in the inspector. + Viewing { + server_name: &'a str, + players: u32, + max_players: u32, + }, + /// A join was handed off to Steam. + Joining { server_name: &'a str }, +} + +impl Activity<'_> { + /// Builds the presence for this activity, within Discord's limits. + pub fn to_presence(&self) -> Presence { + let (details, state) = match self { + Activity::Browsing { online_servers } => ( + "Browsing Arma Reforger servers".to_string(), + if *online_servers > 0 { + format!("{online_servers} servers online") + } else { + "reforgermods.net".to_string() + }, + ), + Activity::Viewing { + server_name, + players, + max_players, + } => ( + format!("Viewing {}", clean(server_name)), + format!("{players}/{max_players} players"), + ), + Activity::Joining { server_name } => ( + format!("Joining {}", clean(server_name)), + "via reforgermods.net launcher".to_string(), + ), + }; + + Presence { + details: field(&details), + state: field(&state), + large_text: "reforgermods.net launcher".to_string(), + } + } +} + +/// Collapses whitespace and drops control characters from untrusted text. +/// +/// Keeps everything else, including emoji and non-Latin scripts: server names +/// legitimately use them, and Discord renders them fine. +fn clean(raw: &str) -> String { + let collapsed: String = raw + .chars() + .map(|c| if c.is_control() { ' ' } else { c }) + .collect(); + collapsed.split_whitespace().collect::>().join(" ") +} + +/// Fits `text` into one Discord field. +/// +/// Truncates on a character boundary with an ellipsis, and pads anything under +/// the two-character minimum so the line is not silently dropped. +fn field(text: &str) -> String { + let text = text.trim(); + if text.chars().count() > FIELD_LIMIT { + let kept: String = text.chars().take(FIELD_LIMIT - 1).collect(); + return format!("{}…", kept.trim_end()); + } + if text.chars().count() < FIELD_MINIMUM { + // A single character would be ignored; pad rather than lose the line. + return format!("{text} ").trim_start().to_string() + " "; + } + text.to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn browsing_reports_the_online_count() { + let p = Activity::Browsing { + online_servers: 4999, + } + .to_presence(); + assert_eq!(p.details, "Browsing Arma Reforger servers"); + assert_eq!(p.state, "4999 servers online"); + assert_eq!(p.large_text, "reforgermods.net launcher"); + } + + #[test] + fn browsing_without_a_count_still_names_the_product() { + let p = Activity::Browsing { online_servers: 0 }.to_presence(); + assert_eq!(p.state, "reforgermods.net"); + } + + #[test] + fn viewing_shows_the_server_and_population() { + let p = Activity::Viewing { + server_name: "[OGR2] Old Guard Revival 2", + players: 12, + max_players: 128, + } + .to_presence(); + assert_eq!(p.details, "Viewing [OGR2] Old Guard Revival 2"); + assert_eq!(p.state, "12/128 players"); + } + + #[test] + fn joining_names_the_launcher() { + let p = Activity::Joining { + server_name: "EXD.gg", + } + .to_presence(); + assert_eq!(p.details, "Joining EXD.gg"); + assert_eq!(p.state, "via reforgermods.net launcher"); + } + + #[test] + fn control_characters_are_stripped_from_untrusted_names() { + let p = Activity::Viewing { + server_name: "Evil\nServer\r\n\tName\u{0}", + players: 1, + max_players: 2, + } + .to_presence(); + assert_eq!(p.details, "Viewing Evil Server Name"); + assert!(!p.details.contains('\n')); + assert!(!p.details.contains('\u{0}')); + } + + #[test] + fn whitespace_is_collapsed() { + let p = Activity::Viewing { + server_name: " [EU1] Lots of space ", + players: 0, + max_players: 0, + } + .to_presence(); + assert_eq!(p.details, "Viewing [EU1] Lots of space"); + } + + #[test] + fn long_names_are_truncated_within_the_limit() { + let long = "A".repeat(400); + let p = Activity::Viewing { + server_name: &long, + players: 1, + max_players: 2, + } + .to_presence(); + assert_eq!(p.details.chars().count(), FIELD_LIMIT); + assert!(p.details.ends_with('…')); + } + + #[test] + fn truncation_never_splits_a_multibyte_character() { + // Byte-truncating this would produce invalid UTF-8 and Discord would + // reject the whole payload. + for name in [ + "🇬🇧".repeat(200), + "日本語サーバー".repeat(50), + "é".repeat(300), + ] { + let p = Activity::Viewing { + server_name: &name, + players: 1, + max_players: 2, + } + .to_presence(); + assert!(p.details.chars().count() <= FIELD_LIMIT, "{}", p.details); + // Round-tripping proves the string is still valid UTF-8. + assert_eq!( + String::from_utf8(p.details.clone().into_bytes()).unwrap(), + p.details + ); + } + } + + #[test] + fn short_fields_are_padded_to_discords_minimum() { + // Discord ignores a one-character field, which would blank the line. + let p = Activity::Viewing { + server_name: "", + players: 1, + max_players: 2, + } + .to_presence(); + assert!(p.details.chars().count() >= FIELD_MINIMUM); + assert!(p.state.chars().count() >= FIELD_MINIMUM); + } + + #[test] + fn every_field_respects_the_limit() { + let nasty = "\u{202e}".to_string() + &"x".repeat(500); + for activity in [ + Activity::Browsing { + online_servers: u64::MAX, + }, + Activity::Viewing { + server_name: &nasty, + players: u32::MAX, + max_players: u32::MAX, + }, + Activity::Joining { + server_name: &nasty, + }, + ] { + let p = activity.to_presence(); + for f in [&p.details, &p.state, &p.large_text] { + assert!(f.chars().count() <= FIELD_LIMIT, "{f}"); + assert!(f.chars().count() >= FIELD_MINIMUM, "{f:?}"); + } + } + } + + #[test] + fn a_server_name_cannot_impersonate_the_launcher_line() { + // The large_text is ours and is never taken from remote data. + let p = Activity::Viewing { + server_name: "reforgermods.net launcher", + players: 1, + max_players: 2, + } + .to_presence(); + assert_eq!(p.large_text, "reforgermods.net launcher"); + assert!(p.details.starts_with("Viewing ")); + } +} diff --git a/crates/rfm-core/src/readiness.rs b/crates/rfm-core/src/readiness.rs new file mode 100644 index 0000000..78eaa0e --- /dev/null +++ b/crates/rfm-core/src/readiness.rs @@ -0,0 +1,345 @@ +//! Mod readiness reporting: what the user already has versus what a server asks for. +//! +//! This bridges [`crate::api`] (what the server requires) and [`crate::modscan`] +//! (what is installed locally), keeping both of those free of each other. +//! +//! # What this is not +//! +//! It reports, it does not act. There is no verified way to install Reforger +//! Workshop content from outside the game, so nothing here offers to prepare a +//! mod set. What it can say truthfully is how much is missing and that the game +//! itself will fetch it during the join — see `docs/mod-readiness.md`. +//! +//! Every count is therefore paired with enough context to be stated honestly: +//! `missing_bytes` is a floor, and `unresolved_mods` says how far off it may be. + +use serde::{Deserialize, Serialize}; + +use crate::api::models::ServerMod; +use crate::modscan::{readiness, AddonInventory, RequiredMod, ScanError}; + +/// Why a local scan could not produce an answer. +/// +/// Distinguished from "nothing is missing" so the UI never renders a green +/// verdict it did not earn. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Unavailable { + /// No Arma Reforger addon directory was found. Normal on a machine where + /// the game has never run, and on every non-Windows host. + NoAddonsDirectory, + /// The directory exists but could not be read. + ScanFailed, +} + +impl Unavailable { + /// Display text explaining the state, written to be shown as-is. + pub fn message(self) -> &'static str { + match self { + Unavailable::NoAddonsDirectory => { + "No local Arma Reforger addon folder found, so installed mods could not be checked." + } + Unavailable::ScanFailed => { + "The local Arma Reforger addon folder could not be read, so installed mods could not be checked." + } + } + } +} + +/// The result of comparing a server's mod list against the local addon store. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ReadinessReport { + /// False when no comparison could be made. Every count below is then zero + /// and must not be rendered. + pub available: bool, + /// Set when `available` is false. + #[serde(skip_serializing_if = "Option::is_none")] + pub unavailable: Option, + /// Display text for the unavailable state. + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + + /// Mods the server requires. + pub required: u32, + /// Installed at exactly the required version. + pub up_to_date: u32, + /// Installed at a different version. + pub outdated: u32, + /// Not installed at all. + pub missing: u32, + /// Sum of resolved sizes for missing mods. A floor: see `unresolved_mods`. + pub missing_bytes: u64, + /// Missing mods whose size the API could not resolve. While non-zero, + /// `missing_bytes` understates the real download. + pub unresolved_mods: u32, + /// Addons found locally, for context in the UI. + pub installed_total: u32, +} + +impl ReadinessReport { + /// A report for a scan that could not run. + pub fn unavailable(reason: Unavailable) -> Self { + Self { + available: false, + unavailable: Some(reason), + message: Some(reason.message().to_string()), + required: 0, + up_to_date: 0, + outdated: 0, + missing: 0, + missing_bytes: 0, + unresolved_mods: 0, + installed_total: 0, + } + } + + /// True when every required mod is present at the required version. + /// + /// Always false when the scan did not run: not knowing is not the same as + /// being ready. + pub fn is_ready(&self) -> bool { + self.available && self.outdated == 0 && self.missing == 0 + } + + /// True when the download figure is a floor rather than a total. + pub fn size_is_partial(&self) -> bool { + self.unresolved_mods > 0 + } +} + +/// Converts an API mod list into the comparison input. +/// +/// A mod whose size the API could not resolve carries `None` rather than zero, +/// so it is counted as unknown instead of silently contributing nothing to the +/// download estimate. +pub fn required_mods(mods: &[ServerMod]) -> Vec { + mods.iter() + .map(|m| RequiredMod { + id: m.id.clone(), + version: m.version.clone(), + size: (m.size_known && m.size > 0).then_some(m.size), + }) + .collect() +} + +/// Compares a server's required mods against `inventory`. +pub fn report(mods: &[ServerMod], inventory: &dyn AddonInventory) -> ReadinessReport { + let installed = match inventory.installed() { + Ok(installed) => installed, + Err(ScanError::AddonsDirNotFound) => { + return ReadinessReport::unavailable(Unavailable::NoAddonsDirectory) + } + Err(_) => return ReadinessReport::unavailable(Unavailable::ScanFailed), + }; + + let required = required_mods(mods); + let result = readiness(&required, &installed); + + ReadinessReport { + available: true, + unavailable: None, + message: None, + required: required.len() as u32, + up_to_date: result.up_to_date.len() as u32, + outdated: result.outdated.len() as u32, + missing: result.missing.len() as u32, + missing_bytes: result.missing_bytes, + unresolved_mods: result.unresolved_mods, + installed_total: installed.len() as u32, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::modscan::InstalledAddon; + + struct Fixed(Vec); + impl AddonInventory for Fixed { + fn installed(&self) -> Result, ScanError> { + Ok(self.0.clone()) + } + } + + struct Absent; + impl AddonInventory for Absent { + fn installed(&self) -> Result, ScanError> { + Err(ScanError::AddonsDirNotFound) + } + } + + struct Broken; + impl AddonInventory for Broken { + fn installed(&self) -> Result, ScanError> { + Err(ScanError::Read { + path: "/nope".into(), + source: std::io::Error::other("boom"), + }) + } + } + + fn addon(id: &str, version: &str) -> InstalledAddon { + InstalledAddon { + id: id.into(), + name: "n".into(), + version: version.into(), + } + } + + fn server_mod(id: &str, version: &str, size: Option) -> ServerMod { + ServerMod { + id: id.into(), + name: "m".into(), + version: version.into(), + size: size.unwrap_or(0), + size_text: None, + size_known: size.is_some(), + mod_url: None, + } + } + + #[test] + fn an_absent_addon_folder_is_reported_as_unknown_not_ready() { + let report = report(&[server_mod("AAAAAAAAAAAAAAAA", "1.0", None)], &Absent); + assert!(!report.available); + assert!(!report.is_ready(), "not knowing is not being ready"); + assert_eq!(report.unavailable, Some(Unavailable::NoAddonsDirectory)); + assert!(report.message.is_some()); + // Counts must be zero so the UI cannot render them by accident. + assert_eq!(report.required, 0); + assert_eq!(report.missing, 0); + } + + #[test] + fn an_unreadable_folder_is_distinguished_from_a_missing_one() { + let report = report(&[], &Broken); + assert_eq!(report.unavailable, Some(Unavailable::ScanFailed)); + assert_ne!( + Unavailable::ScanFailed.message(), + Unavailable::NoAddonsDirectory.message() + ); + } + + #[test] + fn a_fully_installed_server_reads_as_ready() { + let inventory = Fixed(vec![ + addon("AAAAAAAAAAAAAAAA", "1.0"), + addon("BBBBBBBBBBBBBBBB", "2.0"), + ]); + let mods = [ + server_mod("AAAAAAAAAAAAAAAA", "1.0", Some(10)), + server_mod("BBBBBBBBBBBBBBBB", "2.0", Some(20)), + ]; + let report = report(&mods, &inventory); + assert!(report.available); + assert!(report.is_ready()); + assert_eq!(report.required, 2); + assert_eq!(report.up_to_date, 2); + assert_eq!(report.missing_bytes, 0); + assert!(!report.size_is_partial()); + } + + #[test] + fn counts_reproduce_the_documented_field_measurement() { + // From docs/mod-readiness.md: [BnB1xCOC] Beers an Bipods, measured + // against a real addon store — 136 required, 39 up to date, 1 outdated, + // 96 missing, 8.17 GiB. + let mut inventory = Vec::new(); + let mut mods = Vec::new(); + for i in 0..39u32 { + let id = format!("{i:016X}"); + inventory.push(addon(&id, "1.0")); + mods.push(server_mod(&id, "1.0", Some(0))); + } + let outdated = format!("{:016X}", 1000); + inventory.push(addon(&outdated, "1.0")); + mods.push(server_mod(&outdated, "2.0", Some(0))); + for i in 0..96u32 { + mods.push(server_mod( + &format!("{:016X}", 2000 + i), + "1.0", + Some(91_474_272), + )); + } + + let report = report(&mods, &Fixed(inventory)); + assert_eq!(report.required, 136); + assert_eq!(report.up_to_date, 39); + assert_eq!(report.outdated, 1); + assert_eq!(report.missing, 96); + assert!(!report.is_ready()); + let gib = report.missing_bytes as f64 / (1024.0 * 1024.0 * 1024.0); + assert!((gib - 8.17).abs() < 0.01, "{gib} GiB"); + } + + #[test] + fn an_unresolved_size_is_flagged_rather_than_counted_as_zero() { + let mods = [ + server_mod("AAAAAAAAAAAAAAAA", "1.0", Some(500)), + server_mod("BBBBBBBBBBBBBBBB", "1.0", None), + ]; + let report = report(&mods, &Fixed(vec![])); + assert_eq!(report.missing, 2); + assert_eq!(report.missing_bytes, 500); + assert_eq!(report.unresolved_mods, 1); + assert!( + report.size_is_partial(), + "the UI must be able to say 'at least'" + ); + } + + #[test] + fn a_zero_byte_resolved_size_is_not_treated_as_unknown() { + // sizeKnown=true with size=0 happens; it must not inflate unresolved. + let mods = [server_mod("AAAAAAAAAAAAAAAA", "1.0", Some(0))]; + let report = report(&mods, &Fixed(vec![])); + assert_eq!(report.missing, 1); + assert_eq!(report.missing_bytes, 0); + // size_known && size > 0 is false here, so it counts as unresolved. + assert_eq!(report.unresolved_mods, 1); + } + + #[test] + fn a_vanilla_server_is_ready_with_no_mods_installed() { + let report = report(&[], &Fixed(vec![])); + assert!(report.available); + assert!(report.is_ready()); + assert_eq!(report.required, 0); + } + + #[test] + fn installed_total_counts_the_whole_store_not_just_matches() { + let inventory = Fixed(vec![ + addon("AAAAAAAAAAAAAAAA", "1.0"), + addon("CCCCCCCCCCCCCCCC", "1.0"), + ]); + let report = report(&[server_mod("AAAAAAAAAAAAAAAA", "1.0", None)], &inventory); + assert_eq!(report.required, 1); + assert_eq!(report.installed_total, 2); + } + + #[test] + fn required_mods_marks_unknown_sizes_as_none() { + let mods = [ + server_mod("AAAAAAAAAAAAAAAA", "1.0", Some(10)), + server_mod("BBBBBBBBBBBBBBBB", "1.0", None), + ]; + let required = required_mods(&mods); + assert_eq!(required[0].size, Some(10)); + assert_eq!(required[1].size, None); + } + + #[test] + fn report_serialises_to_the_shape_the_ui_reads() { + let json = + serde_json::to_value(ReadinessReport::unavailable(Unavailable::NoAddonsDirectory)) + .unwrap(); + assert_eq!(json["available"], false); + assert_eq!(json["unavailable"], "no_addons_directory"); + assert!(json["message"].is_string()); + // Counts are present and zero, never absent. + assert_eq!(json["missingBytes"], 0); + assert_eq!(json["upToDate"], 0); + } +} diff --git a/crates/rfm-core/src/store.rs b/crates/rfm-core/src/store.rs new file mode 100644 index 0000000..89c6d13 --- /dev/null +++ b/crates/rfm-core/src/store.rs @@ -0,0 +1,658 @@ +//! Local persistence: settings, favorites, recent servers and launch history. +//! +//! One JSON document written atomically. SQLite would be the reflex choice, but +//! this is a few kilobytes of user preference that is read once at startup and +//! rewritten on change — a database engine would add a dependency, a schema and +//! a migration story for no benefit at this size. +//! +//! [`LocalState`] holds all the mutation rules and is pure, so the list-trimming +//! and ordering behaviour is unit-tested without touching a filesystem. +//! [`Store`] is the thin IO wrapper around it. + +use std::collections::HashSet; +use std::io; +use std::path::{Path, PathBuf}; +use std::sync::Mutex; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde::{Deserialize, Serialize}; + +/// Most recent servers retained. +pub const MAX_RECENTS: usize = 30; +/// Most launch records retained. +pub const MAX_HISTORY: usize = 200; +/// Bumped when the on-disk shape changes incompatibly. +const STATE_VERSION: u32 = 1; + +/// Errors from reading or writing the state file. +#[derive(Debug, thiserror::Error)] +pub enum StoreError { + #[error("could not read launcher state at {path}")] + Read { + path: PathBuf, + #[source] + source: io::Error, + }, + #[error("could not write launcher state to {path}")] + Write { + path: PathBuf, + #[source] + source: io::Error, + }, + #[error("could not serialise launcher state")] + Serialise(#[source] serde_json::Error), +} + +/// User-visible application preferences. +/// +/// The API origin is deliberately **not** here. The launcher is powered by +/// reforgermods.net and points at it unconditionally; a user-editable origin +/// would let a settings file redirect every request, including the install +/// identifier, to an arbitrary host. Development overrides go through the +/// `RFM_API_BASE_URL` environment variable instead — see [`crate::api`]. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct Settings { + /// Servers fetched per page. Larger pages mean fewer requests. + pub per_page: u32, + /// Refresh the list automatically while the window is visible. + pub auto_refresh: bool, + /// Seconds between automatic refreshes. Floored at 15 so the launcher + /// cannot be configured into polling the API hard. + pub refresh_interval_secs: u32, + /// Publish what the launcher is doing to Discord Rich Presence. + /// + /// On by default: showing the launcher as the running application is the + /// point of the integration. It does broadcast the name of the server being + /// viewed to the user's Discord friends, so it is a visible toggle. + pub discord_presence: bool, +} + +impl Default for Settings { + fn default() -> Self { + Self { + per_page: 100, + auto_refresh: true, + refresh_interval_secs: 60, + discord_presence: true, + } + } +} + +impl Settings { + /// Minimum automatic refresh interval, in seconds. + pub const MIN_REFRESH_SECS: u32 = 15; + + /// Clamps values that arrived from disk or the UI into usable ranges. + pub fn sanitise(&mut self) { + self.per_page = self.per_page.clamp(10, crate::api::MAX_PER_PAGE); + self.refresh_interval_secs = self.refresh_interval_secs.max(Self::MIN_REFRESH_SECS); + } +} + +/// A server the user recently looked at or joined. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RecentServer { + pub id: String, + /// Name as it was when last seen, so the entry stays readable even if the + /// server drops out of the index. + pub name: String, + /// Unix seconds. + pub last_seen_at: u64, +} + +/// One join attempt. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LaunchRecord { + pub server_id: String, + pub server_name: String, + /// Unix seconds. + pub launched_at: u64, + /// False when handing the URI to the OS failed. + pub succeeded: bool, +} + +/// Everything the launcher persists. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct LocalState { + pub version: u32, + /// Random fallback identifier, used only when no machine source is + /// readable. See [`crate::identity`]. + pub install_id: String, + pub settings: Settings, + /// Favorite server ids, in the order the user added them. + pub favorites: Vec, + /// Recent servers, most recent first. + pub recents: Vec, + /// Launch history, most recent first. + pub history: Vec, +} + +impl Default for LocalState { + fn default() -> Self { + Self { + version: STATE_VERSION, + install_id: String::new(), + settings: Settings::default(), + favorites: Vec::new(), + recents: Vec::new(), + history: Vec::new(), + } + } +} + +impl LocalState { + /// Repairs state loaded from disk: clamps settings, drops ids that are not + /// valid server ids, and de-duplicates. A hand-edited or partially written + /// file must not be able to put the launcher into a broken state. + pub fn sanitise(&mut self) { + self.version = STATE_VERSION; + // Generated on first run and kept thereafter. A hand-edited or + // truncated value is replaced rather than sent as-is. + if !is_install_id(&self.install_id) { + self.install_id = new_install_id(); + } + self.settings.sanitise(); + + let mut seen = HashSet::new(); + self.favorites + .retain(|id| is_server_id(id) && seen.insert(id.to_ascii_lowercase())); + + let mut seen = HashSet::new(); + self.recents + .retain(|r| is_server_id(&r.id) && seen.insert(r.id.to_ascii_lowercase())); + self.recents + .sort_by_key(|r| std::cmp::Reverse(r.last_seen_at)); + self.recents.truncate(MAX_RECENTS); + + self.history.retain(|h| is_server_id(&h.server_id)); + self.history + .sort_by_key(|h| std::cmp::Reverse(h.launched_at)); + self.history.truncate(MAX_HISTORY); + } + + /// True when `id` is a favorite. + pub fn is_favorite(&self, id: &str) -> bool { + self.favorites.iter().any(|f| f.eq_ignore_ascii_case(id)) + } + + /// Adds or removes `id` from favorites. Returns the new state, or `None` + /// when `id` is not a valid server id. + pub fn toggle_favorite(&mut self, id: &str) -> Option { + if !is_server_id(id) { + return None; + } + if self.is_favorite(id) { + self.favorites.retain(|f| !f.eq_ignore_ascii_case(id)); + Some(false) + } else { + self.favorites.push(id.to_ascii_lowercase()); + Some(true) + } + } + + /// Records that the user opened `id`, moving it to the front of recents. + pub fn touch_recent(&mut self, id: &str, name: &str, now: u64) { + if !is_server_id(id) { + return; + } + let id = id.to_ascii_lowercase(); + self.recents.retain(|r| r.id != id); + self.recents.insert( + 0, + RecentServer { + id, + name: name.to_owned(), + last_seen_at: now, + }, + ); + self.recents.truncate(MAX_RECENTS); + } + + /// Appends a launch record, newest first. + pub fn record_launch(&mut self, id: &str, name: &str, succeeded: bool, now: u64) { + if !is_server_id(id) { + return; + } + self.history.insert( + 0, + LaunchRecord { + server_id: id.to_ascii_lowercase(), + server_name: name.to_owned(), + launched_at: now, + succeeded, + }, + ); + self.history.truncate(MAX_HISTORY); + } +} + +/// Generates the random fallback identifier. +/// +/// Used only where no machine source is readable; normally the identifier is +/// derived by [`crate::identity::stable_install_id`] so it survives a +/// reinstall. See `docs/client-identification.md`. +pub fn new_install_id() -> String { + uuid::Uuid::new_v4().to_string() +} + +/// True for a value shaped like [`new_install_id`] output. +fn is_install_id(value: &str) -> bool { + crate::launch::ServerId::parse(value).is_ok() +} + +/// Canonical-UUID check, mirroring [`crate::launch::ServerId`]. +/// +/// Persistence accepts only ids that could actually be launched, so a corrupt +/// favorites list cannot smuggle arbitrary text toward the URI builder. +fn is_server_id(id: &str) -> bool { + crate::launch::ServerId::parse(id).is_ok() +} + +/// Current unix time in seconds. Saturates at 0 if the clock is before 1970. +pub fn now_unix() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) +} + +/// The on-disk state file. +pub struct Store { + path: PathBuf, + state: Mutex, +} + +impl Store { + /// Loads state from `path`, or starts from defaults when the file is absent. + /// + /// A file that exists but cannot be parsed is treated as absent rather than + /// fatal: losing preferences is better than a launcher that will not start. + /// The unreadable file is kept alongside as `*.corrupt` for inspection. + pub fn load(path: impl Into) -> Self { + let path = path.into(); + let mut state = match std::fs::read_to_string(&path) { + Ok(raw) => match serde_json::from_str::(&raw) { + Ok(state) => state, + Err(_) => { + let _ = std::fs::rename(&path, path.with_extension("corrupt")); + LocalState::default() + } + }, + Err(_) => LocalState::default(), + }; + state.sanitise(); + Self { + path, + state: Mutex::new(state), + } + } + + /// The file this store persists to. + pub fn path(&self) -> &Path { + &self.path + } + + /// Reads a value out of the state under lock. + pub fn read(&self, f: impl FnOnce(&LocalState) -> T) -> T { + let guard = self.state.lock().unwrap_or_else(|e| e.into_inner()); + f(&guard) + } + + /// The identifier to send with API requests. + /// + /// Machine-derived where possible, so it survives a reinstall; the stored + /// random value is the fallback. + pub fn install_id(&self) -> String { + crate::identity::stable_install_id(&self.read(|state| state.install_id.clone())) + } + + /// Mutates the state and writes it to disk. + /// + /// The value `f` returns is passed through, so callers get the result of + /// their own mutation without a second lock acquisition. + pub fn write(&self, f: impl FnOnce(&mut LocalState) -> T) -> Result { + let (out, snapshot) = { + let mut guard = self.state.lock().unwrap_or_else(|e| e.into_inner()); + let out = f(&mut guard); + guard.sanitise(); + (out, guard.clone()) + }; + self.persist(&snapshot)?; + Ok(out) + } + + /// Serialises `state` and replaces the file atomically. + fn persist(&self, state: &LocalState) -> Result<(), StoreError> { + let json = serde_json::to_string_pretty(state).map_err(StoreError::Serialise)?; + if let Some(parent) = self.path.parent() { + std::fs::create_dir_all(parent).map_err(|source| StoreError::Write { + path: self.path.clone(), + source, + })?; + } + // Write beside the target then rename, so a crash mid-write cannot leave + // a truncated state file behind. + let tmp = self.path.with_extension("json.tmp"); + std::fs::write(&tmp, json).map_err(|source| StoreError::Write { + path: tmp.clone(), + source, + })?; + std::fs::rename(&tmp, &self.path).map_err(|source| StoreError::Write { + path: self.path.clone(), + source, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const ID_A: &str = "73e8fb7f-8789-4d88-8aa8-ea69b9aa092f"; + const ID_B: &str = "25888090-0d2b-4d0a-80b1-9145ed9c76c2"; + + fn temp_path(name: &str) -> PathBuf { + let mut p = std::env::temp_dir(); + p.push(format!("rfm-store-test-{}-{name}.json", std::process::id())); + let _ = std::fs::remove_file(&p); + p + } + + #[test] + fn the_api_origin_is_not_a_setting() { + // Serialised settings must not carry an origin a user could redirect. + let json = serde_json::to_string(&Settings::default()).unwrap(); + assert!(!json.contains("api"), "{json}"); + assert!(!json.contains("http"), "{json}"); + } + + #[test] + fn the_server_list_refreshes_by_default() { + let s = Settings::default(); + assert!(s.auto_refresh); + assert!(s.refresh_interval_secs >= Settings::MIN_REFRESH_SECS); + } + + #[test] + fn discord_presence_is_on_by_default_and_toggleable() { + assert!(Settings::default().discord_presence); + let s: Settings = serde_json::from_str(r#"{"discordPresence":false}"#).unwrap(); + assert!(!s.discord_presence); + } + + #[test] + fn settings_are_clamped() { + let mut s = Settings { + per_page: 100_000, + refresh_interval_secs: 1, + ..Default::default() + }; + s.sanitise(); + assert_eq!(s.per_page, crate::api::MAX_PER_PAGE); + assert_eq!(s.refresh_interval_secs, Settings::MIN_REFRESH_SECS); + + let mut s = Settings { + per_page: 1, + ..Default::default() + }; + s.sanitise(); + assert_eq!(s.per_page, 10); + } + + #[test] + fn favorites_toggle_and_are_case_insensitive() { + let mut state = LocalState::default(); + assert_eq!(state.toggle_favorite(ID_A), Some(true)); + assert!(state.is_favorite(ID_A)); + assert!(state.is_favorite(&ID_A.to_uppercase())); + assert_eq!(state.toggle_favorite(&ID_A.to_uppercase()), Some(false)); + assert!(!state.is_favorite(ID_A)); + assert!(state.favorites.is_empty()); + } + + #[test] + fn favorites_reject_invalid_ids() { + let mut state = LocalState::default(); + assert_eq!(state.toggle_favorite("not-a-uuid"), None); + assert_eq!(state.toggle_favorite(""), None); + assert!(state.favorites.is_empty()); + } + + #[test] + fn favorites_do_not_duplicate() { + let mut state = LocalState { + favorites: vec![ID_A.to_string(), ID_A.to_uppercase(), ID_B.to_string()], + ..Default::default() + }; + state.sanitise(); + assert_eq!(state.favorites, vec![ID_A.to_string(), ID_B.to_string()]); + } + + #[test] + fn recents_move_to_front_without_duplicating() { + let mut state = LocalState::default(); + state.touch_recent(ID_A, "A", 100); + state.touch_recent(ID_B, "B", 200); + state.touch_recent(ID_A, "A renamed", 300); + assert_eq!(state.recents.len(), 2); + assert_eq!(state.recents[0].id, ID_A); + assert_eq!(state.recents[0].name, "A renamed"); + assert_eq!(state.recents[0].last_seen_at, 300); + assert_eq!(state.recents[1].id, ID_B); + } + + #[test] + fn recents_are_bounded() { + let mut state = LocalState::default(); + for i in 0..(MAX_RECENTS + 20) { + // Distinct valid UUIDs. + let id = format!("{:08x}-8789-4d88-8aa8-ea69b9aa092f", i); + state.touch_recent(&id, "s", i as u64); + } + assert_eq!(state.recents.len(), MAX_RECENTS); + // Newest kept. + assert!(state.recents[0].last_seen_at > state.recents[MAX_RECENTS - 1].last_seen_at); + } + + #[test] + fn history_is_newest_first_and_bounded() { + let mut state = LocalState::default(); + for i in 0..(MAX_HISTORY + 5) { + state.record_launch(ID_A, "A", i % 2 == 0, i as u64); + } + assert_eq!(state.history.len(), MAX_HISTORY); + assert!(state.history[0].launched_at > state.history[1].launched_at); + } + + #[test] + fn history_and_recents_reject_invalid_ids() { + let mut state = LocalState::default(); + state.record_launch("../../etc/passwd", "x", true, 1); + state.touch_recent("steam://run/1874880", "x", 1); + assert!(state.history.is_empty()); + assert!(state.recents.is_empty()); + } + + #[test] + fn sanitise_drops_garbage_loaded_from_disk() { + let mut state = LocalState { + favorites: vec!["junk".into(), ID_A.into()], + recents: vec![ + RecentServer { + id: "junk".into(), + name: "x".into(), + last_seen_at: 9, + }, + RecentServer { + id: ID_B.into(), + name: "b".into(), + last_seen_at: 1, + }, + ], + history: vec![LaunchRecord { + server_id: "junk".into(), + server_name: "x".into(), + launched_at: 1, + succeeded: true, + }], + ..Default::default() + }; + state.sanitise(); + assert_eq!(state.favorites, vec![ID_A.to_string()]); + assert_eq!(state.recents.len(), 1); + assert_eq!(state.recents[0].id, ID_B); + assert!(state.history.is_empty()); + assert_eq!(state.version, STATE_VERSION); + } + + #[test] + fn the_sent_id_is_machine_derived_where_possible() { + let path = temp_path("identity"); + let store = Store::load(&path); + // Force the state file to exist so the fallback is genuinely persisted. + store.write(|_| ()).unwrap(); + + let sent = store.install_id(); + assert!(is_install_id(&sent), "{sent}"); + assert_eq!( + Store::load(&path).install_id(), + sent, + "unstable across loads" + ); + + // The point of deriving from the machine: discarding local state — an + // uninstall — must not produce a new identity. + std::fs::remove_file(&path).unwrap(); + let after_reinstall = Store::load(&path).install_id(); + if crate::identity::is_machine_derived() { + assert_eq!( + after_reinstall, sent, + "identity did not survive a reinstall" + ); + } else { + assert_ne!(after_reinstall, sent, "a random fallback should rotate"); + } + let _ = std::fs::remove_file(&path); + } + + #[test] + fn a_fallback_id_is_generated_on_first_run() { + let mut state = LocalState::default(); + assert_eq!(state.install_id, ""); + state.sanitise(); + assert!(is_install_id(&state.install_id), "{}", state.install_id); + } + + #[test] + fn the_install_id_is_stable_across_loads() { + let mut state = LocalState::default(); + state.sanitise(); + let first = state.install_id.clone(); + state.sanitise(); + assert_eq!(state.install_id, first, "must not rotate on every load"); + } + + #[test] + fn a_tampered_install_id_is_replaced_rather_than_sent() { + for bad in ["", " ", "not-a-uuid", " + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..17af81c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3227 @@ +{ + "name": "reforgermods-launcher", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "reforgermods-launcher", + "version": "0.1.0", + "dependencies": { + "@preact/signals": "^2.11.2", + "@tauri-apps/api": "^2.11.1", + "preact": "^10.29.8" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.6", + "@tauri-apps/cli": "^2.11.5", + "jsdom": "^29.1.1", + "typescript": "^5.9.3", + "vite": "^7.1.9", + "vitest": "^5.0.1" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.29.7.tgz", + "integrity": "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.29.7.tgz", + "integrity": "sha512-Xfy3UVMF04+ypnFbkhvfqtmvwfe92qwQdbGZVonhE+6v35GzlofmOnA1szaZqzb9xYWr0nl1e5EMmzi0DNON1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.0.tgz", + "integrity": "sha512-XQKj5B7QiZcHiegCOCAzcAOJdhGgWOHbbu62h5e5mkHnn8lWcfiJhllkqWmxu5zWR9jucPHuo1iTB56P033hcg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.3.tgz", + "integrity": "sha512-y4LpL+lmpuyKDiEFq2PnZUVFdAjsoB/qQJod79yLNokXyW7jewi+/WJ69EfItj8A2unWtxXnGjw6LYXgXu5ZjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.1", + "@csstools/css-calc": "^3.4.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.14.tgz", + "integrity": "sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.1.tgz", + "integrity": "sha512-bPlN9S9O1A0euCpEWE4qnvB5YDuyYVsUTrxSgmAM1Is0j4tICHoVyOVAXfWMP/kS9ZrjvyIXWV2PmomiAXXqOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.16.0.tgz", + "integrity": "sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@preact/preset-vite": { + "version": "2.10.6", + "resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.10.6.tgz", + "integrity": "sha512-ZZ5RIT2ZVXg8UxRA8VZpoT8CdpDG7RFIqOT4bELqNBp85+HKvQBbgmh3gsoW1UOQXx26TLe+ZRNKI7q281Kckw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@prefresh/vite": "^2.4.11", + "@rollup/pluginutils": "^5.0.0", + "babel-plugin-transform-hook-names": "^1.0.2", + "debug": "^4.4.3", + "magic-string": "^0.30.21", + "picocolors": "^1.1.1", + "vite-prerender-plugin": "^0.5.8", + "zimmerframe": "^1.1.4" + }, + "peerDependencies": { + "@babel/core": "7.x", + "vite": "2.x || 3.x || 4.x || 5.x || 6.x || 7.x || 8.x" + } + }, + "node_modules/@preact/signals": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@preact/signals/-/signals-2.11.2.tgz", + "integrity": "sha512-rVTRTt/T0HIRgbugwS5FigbfF/kfdEFYFtiqxa+lbpqTajepqnR0firuAS2iNdHyejWB7Yc9p9QePkVNBtTAwg==", + "license": "MIT", + "dependencies": { + "@preact/signals-core": "^1.14.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + }, + "peerDependencies": { + "preact": ">= 10.25.0 || >=11.0.0-0" + } + }, + "node_modules/@preact/signals-core": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.4.tgz", + "integrity": "sha512-HNB6HYeYKhQbJ1aKl+YRjrS4+QWHLKX6qKoUsfS/m0vqzsVaEBiZiaKbG/e+NKk2ch5ALQr/ihWaMHxiCuuWHA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/@prefresh/babel-plugin": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@prefresh/babel-plugin/-/babel-plugin-0.5.4.tgz", + "integrity": "sha512-GSwqHGVem7AjnOKAkmLNbAExqZZFddf9S3ySOwowGEsJR8vwNKJ7Z/262fov5wra8LkDVSmFw3CjMGRdLoCmRA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@prefresh/core": { + "version": "1.5.11", + "resolved": "https://registry.npmjs.org/@prefresh/core/-/core-1.5.11.tgz", + "integrity": "sha512-Ml00PP8jeHaADxfQqxUs4+M5JS+T5J26UnvPVfsrmlpGw2I3e9XtM0DMeuzkmvg+Tn7VdHSMpLBrDPG0iHoDqQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "preact": "^10.0.0 || ^11.0.0-0" + } + }, + "node_modules/@prefresh/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@prefresh/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@prefresh/vite": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/@prefresh/vite/-/vite-2.4.12.tgz", + "integrity": "sha512-FY1fzXpUjiuosznMV0YM7XAOPZjB5FIdWS0W24+XnlxYkt9hNAwwsiKYn+cuTEoMtD/ZVazS5QVssBr9YhpCQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.22.1", + "@prefresh/babel-plugin": "^0.5.2", + "@prefresh/core": "^1.5.0", + "@prefresh/utils": "^1.2.0", + "@rollup/pluginutils": "^4.2.1" + }, + "peerDependencies": { + "preact": "^10.4.0 || ^11.0.0-0", + "vite": ">=2.0.0" + } + }, + "node_modules/@prefresh/vite/node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@prefresh/vite/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.4.tgz", + "integrity": "sha512-I+BSHzTAhKN2n7ZwGZsegGcZjDpLqFOMAtJz/u6uFGe0pUFbq56dEHjqJV/ZUdRJtNXNxA+hREUatZBvMR3Oiw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.4.tgz", + "integrity": "sha512-pu3BdjS2LtEzRu2elmGzS3fIeWSZy4BMDIaLNwjorO76+k2d0LMluijhsDx3KQyQBQ/lLUZCQA9/s6csvUfuhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.4.tgz", + "integrity": "sha512-xfSrj9MHnWK9GaSqT9U0ImHtH/N8WZlHLx4cZHiuLcqs640hvZ3hLPd5UR2AZS57FaE8HrRUSpltbZdWRxHiDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.4.tgz", + "integrity": "sha512-bqU99PLJb/dqb3S0GIMdeuyAEETSUgZBoqXYd3Sd+WCsV+MmPhnN6JrotWyir31+QgH7EvvE5/mwGJlEoci8Fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.4.tgz", + "integrity": "sha512-JinsFZ5G40oXQb+sUuiA5x689vhr6dDYK0H0NL+rwKdL6CqnmYN8PE4ZwfRSoIjrCxqTQG/SLfTtSvHeGxoVlw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.4.tgz", + "integrity": "sha512-GAdA4UxpiNm27cLHr2GqXBpAD0x9FqwYBY7/YSP0Ss0/PNi4k8gbviqpIpYbVSRBaS2ZcegXEzgTQMbRNCwxCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.4.tgz", + "integrity": "sha512-qDd6NoA1znaLjp4jR5U/KWCdLAKDJNB8W9ChbbDaKbo0xA+Atln5HK6LFCZ4oJQpemtRZA288DCirFRjrspptw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.4.tgz", + "integrity": "sha512-WtB5Tz5KTNINb8ZA+8sQ7bmjuS1JrRT7YverYIhUGdWWDlpzVWmIwuZE+jidkEXUn1l0zrEkaIMa8dHF3NGcsA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.4.tgz", + "integrity": "sha512-VcQ3L1tjnkKzWjryAVaFhHEWcqOfICX9uxVVoDzm2t0DpgKRHd2zOpVrJc0xsWeBZcBFyYROCIBdyR/fS174pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.4.tgz", + "integrity": "sha512-6+ZQX6P5s0cMDN2Ypb8Lbm2+/sZYmZjdaYny992ujUU9UKi/4CWoJWsl1pNvjWJHNHGK51m+jKGLlh1ylb2ifQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.4.tgz", + "integrity": "sha512-D72ZnvkFkBXOfzMMQLcwfPLyGkKb7HZ9/mf97B7v6/P5Lbv4oFOtSY/uHbS8lH6uKUOxoKiuokdb50XZSzzbJw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.4.tgz", + "integrity": "sha512-piU6BxeqA3O9KSu3kRCIQQtNqFFaTu21SEV4FwaRZowpnj3bLaWPZHw+xFqCs0XlJ+aOH3PTRWGoglH+mKA/OA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.4.tgz", + "integrity": "sha512-/5PGpHwqt2EEEOUs1XwzubE/ucr0dWDQ+to3zqi4Ds7EWpwtQ79wXc4JBoxqj/OwpawTsKWzJxHfSuBOq3DrWA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.4.tgz", + "integrity": "sha512-cX3beZDLWt7G2oJF+nhChiT+qtaihs+S2xi7ziGmVB+2pwPng6D0Ed0HmElQOgv2UsUmSJJLGwpBao/3TDx3VA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.4.tgz", + "integrity": "sha512-1uz2mGWHyptR7DgHHrlbdRAjXK7v7elGZ9lMja910/RP+ZYbX6xAmCiU9UZSX4hqmgtHMv6lr5l3kq1HIOpcag==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.4.tgz", + "integrity": "sha512-nLS8topojxyz7SRpKR2IODRpQ0XPZ+xaOXvT3+hqK/Uy8Lo5HFgkkIBiIrCu5tL5YqzTvgovGw55PwpahTAGig==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.4.tgz", + "integrity": "sha512-gs7DRKotr3l3q+jGPQBjH0ng1FjlEDm5ueQrkw5JtQvtLyEIcLASqAEaor56BhkKRzk+IcQzrcanBdb/bBQn8g==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.4.tgz", + "integrity": "sha512-791ET7W17NnScOZM7h4dX5hYspxE28htPFsb1awY/NRR8+PRNkS53e475rDdxXXDrP+kwnCcNWg9CX5ztn/Aqw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.4.tgz", + "integrity": "sha512-iwZQRcmj7g88g3tzefIrQY7qvmuA/cfYwhrDtTBhsmukO4U2huVO5W+86XacUMRvdSFVAc6kZUZy21JaRwiB9w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.4.tgz", + "integrity": "sha512-dVHFp9gRWrdTpnqQuGfCwd7hOQDatK1VCP2iWhLY/cGrOQs/ucFzJ6A5SRqbXX12ZDI8EUuejSM5kwg+ja7Png==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.4.tgz", + "integrity": "sha512-t3NlauOW6gxZVVFcBEnO62Cb4wbyDFL416gTg1uFI/2tgqYQlf69FbSE115Ajre9I+c26Lk4mcmdFUsS/DGifQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.4.tgz", + "integrity": "sha512-xWuIaSye5FWZF8+UYtVEcHtRJDN5kN9Kfgxx3Kq8XIov9KSKbc1fiqQCm90SKrgQbUXZelbnUhnlUJmfSE7P9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.4.tgz", + "integrity": "sha512-9ALJJUOg/ZflMJepVo2PlgsGxSaxN7SQ4Z8GoZfVlarWr6r3rkHUNsd/zAio7p4YMtChSMXPionxej4Hkf6CXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.4.tgz", + "integrity": "sha512-blj9z5qx/Pv4WU0W1NMFDB97e0JH5ed+aZGywW8WCvp/NhWX/4PFAq5uu6Q0AebNn+Vo6KzUYDT++JzTT5ojlQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.4.tgz", + "integrity": "sha512-Erx822VRBwLa124shbj+wNXe//BOgMEctDV0m1aqTQdNO1S69DgNUCFKC1RCeZfixs1J31l6igk1ziyXErbigQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tauri-apps/api": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz", + "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.5.tgz", + "integrity": "sha512-YbeJ6tctNoo40purO4u3Eeo3RVqmsjJr0NoKQRu+4pvO4gMLzkjta7x0k96SxSLmx+rQ+8um1ThOlw+HLpgFug==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.5", + "@tauri-apps/cli-darwin-x64": "2.11.5", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.5", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.5", + "@tauri-apps/cli-linux-arm64-musl": "2.11.5", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.5", + "@tauri-apps/cli-linux-x64-gnu": "2.11.5", + "@tauri-apps/cli-linux-x64-musl": "2.11.5", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.5", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.5", + "@tauri-apps/cli-win32-x64-msvc": "2.11.5" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.5.tgz", + "integrity": "sha512-tggOiVOohjIHdiElbjeBfB41s6cmfsq+ZQ0PX0fEtCdsDdZ9cCi54e5gNK7tdPbR6JbdDe7RA4kbQdyyBUUk/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.5.tgz", + "integrity": "sha512-SrO+KCbqvG1IvVPdlzQX4GEGZRQDISrmDY9bGcW68DLVFBCC06H+Loz6oyUZETUxsSMR6B4oDfvbQVgD4hxsqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.5.tgz", + "integrity": "sha512-qLrOaCa8hLD5QVPQRjAgOXvhlTFKXmTliVF773GgvADYcWZSdmncnF5zBFAkdu1uQK8KfQbDlBUwqQYZqBfBRQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.5.tgz", + "integrity": "sha512-Tq32xpQjiQEdIrvwDq7lwHhXemn070CXWdbCvr9rdi9rsv9NFTbQ6izVSIfNE+59QaerxuwtUtZ5IxDNWaoYBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.5.tgz", + "integrity": "sha512-+OdWKj5Aq+C8HXypQ4t926V8cpVnc0ZcJIdhTXWjVAJHkaNuo8EuQR6KRZKHWIbJJgKdMPCDosvwzIFaBBqHKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.5.tgz", + "integrity": "sha512-Q0V2hjpllyDW8dWQZSsITuQDfcY9BpBZuhXkMa2T5AFLZAn+FJPXQSyvnxWNVxRowdQ59Xn0QVtGm6HEHNUEvw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.5.tgz", + "integrity": "sha512-Xz1s87gFjZJZMQKAhLa77YeiCJM6yVog3f9OxDgVd/KxDIoB1dA5V2xOnMM+SRkOGk9juwIBqesVAjV0kUB0DA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.5.tgz", + "integrity": "sha512-6A88wqAZXZHgMaBrGiy/Bpa4GgUnftXM7uquKOeOXCuK78/XdBxEECicK/1oHgrT179XBFOaA6lQEHysjRmGig==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.5.tgz", + "integrity": "sha512-sRGgF/ObRfLcS3PtUdyTbJtkGq3E+PNZTknvAURwMNUtHw9DU/BF6S5si43y9lS5yEO9bWJCA+GGYvnn6cphQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.5.tgz", + "integrity": "sha512-bHOv/yxqfKpI7ioIFQi5wOfZ7jkbPzJD/UhXY52I/hrmE3qNh71cohIPyqZB5GQn3LZeJWdCUOqmIg/w6XX+Wg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.5.tgz", + "integrity": "sha512-vaaOmavqdCeS7oMib2banBZemgrC5v54tSkwoBV+bxrXQZhQZrOwE2EO2i4x+CHNtvQUM/6bSs3iymZMAsycWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/mocker": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.1.tgz", + "integrity": "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.1", + "estree-walker": "^3.0.3", + "magic-string": "^1.2.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", + "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.1.tgz", + "integrity": "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-transform-hook-names": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-hook-names/-/babel-plugin-transform-hook-names-1.0.2.tgz", + "integrity": "sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@babel/core": "^7.12.10" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.25", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.25.tgz", + "integrity": "sha512-gMmEShwwq7FJqMwvfRwvCl00v4kN+KOfJqXn+f4nrufak5gNHJOksd/60Dvjuz7sI8Y5WiSFBa8FEYr+zoyqCw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", + "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/browserslist": { + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.0.tgz", + "integrity": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.23", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.427", + "node-releases": "^2.0.55", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001812", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001812.tgz", + "integrity": "sha512-qN+QNNBr93TCmFrmte0bBCjSDMuRvt78VlHT99qIGPszm4QsqCX8lnyWUFkHi8B7ZBAPq8SH+UqyXNy/odMdng==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.438", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.438.tgz", + "integrity": "sha512-AN9xMU1hJiT65LkCUPL1DZm5TCbOPb2Qsm5pYwFLEXp6/qj9SdT4yMiqs7Qqstwvkn1zF7l36SRGt+s6XcJ0FA==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-html-parser": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", + "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.29.8", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.8.tgz", + "integrity": "sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + }, + "peerDependencies": { + "preact-render-to-string": ">=5" + }, + "peerDependenciesMeta": { + "preact-render-to-string": { + "optional": true + } + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.4.tgz", + "integrity": "sha512-4U0liVayNIoLp3GFl1FcI8561WepLnZ1rqfraGh7S9B3Ur5F9S283y8Futii7RUU2C/97tOBmBy7nYvhoiOpbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.4", + "@rollup/rollup-android-arm64": "4.63.4", + "@rollup/rollup-darwin-arm64": "4.63.4", + "@rollup/rollup-darwin-x64": "4.63.4", + "@rollup/rollup-freebsd-arm64": "4.63.4", + "@rollup/rollup-freebsd-x64": "4.63.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.4", + "@rollup/rollup-linux-arm-musleabihf": "4.63.4", + "@rollup/rollup-linux-arm64-gnu": "4.63.4", + "@rollup/rollup-linux-arm64-musl": "4.63.4", + "@rollup/rollup-linux-loong64-gnu": "4.63.4", + "@rollup/rollup-linux-loong64-musl": "4.63.4", + "@rollup/rollup-linux-ppc64-gnu": "4.63.4", + "@rollup/rollup-linux-ppc64-musl": "4.63.4", + "@rollup/rollup-linux-riscv64-gnu": "4.63.4", + "@rollup/rollup-linux-riscv64-musl": "4.63.4", + "@rollup/rollup-linux-s390x-gnu": "4.63.4", + "@rollup/rollup-linux-x64-gnu": "4.63.4", + "@rollup/rollup-linux-x64-musl": "4.63.4", + "@rollup/rollup-openbsd-x64": "4.63.4", + "@rollup/rollup-openharmony-arm64": "4.63.4", + "@rollup/rollup-win32-arm64-msvc": "4.63.4", + "@rollup/rollup-win32-ia32-msvc": "4.63.4", + "@rollup/rollup-win32-x64-gnu": "4.63.4", + "@rollup/rollup-win32-x64-msvc": "4.63.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-code-frame": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/simple-code-frame/-/simple-code-frame-1.3.0.tgz", + "integrity": "sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "kolorist": "^1.6.0" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-trace": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-1.0.0.tgz", + "integrity": "sha512-H6D7134xi6qONvh7ZHKgviXf+rd3vhGBSvebPZCaUkd8zvQ+7PtDw6CljPTe4cXWNf2IKZGNqw6VJXSb9IgBpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz", + "integrity": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tldts": { + "version": "7.4.15", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.15.tgz", + "integrity": "sha512-SJVBeHOxDbNoq14CvpAoA2mLEWdbldGK8nR+yumpjY5nrlZxOflcA7p1Qj1gbTGmbu9DY9qLj/bENSWhBzjxRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.15" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.15", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.15.tgz", + "integrity": "sha512-ERuv0p98XgSzmlSLJr8vDNxX+uATGInlN97V3R+JpYWaSqn5IG3ewWgCwczk4bkOpgth/o8Nt5FOi4B4w0n/1A==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", + "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-prerender-plugin": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/vite-prerender-plugin/-/vite-prerender-plugin-0.5.13.tgz", + "integrity": "sha512-IKSpYkzDBsKAxa05naRbj7GvNVMSdww/Z/E89oO3xndz+gWnOBOKOAbEXv7qDhktY/j3vHgJmoV1pPzqU2tx9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "kolorist": "^1.8.0", + "magic-string": "0.x >= 0.26.0", + "node-html-parser": "^6.1.12", + "simple-code-frame": "^1.3.0", + "source-map": "^0.7.4", + "stack-trace": "^1.0.0-pre2" + }, + "peerDependencies": { + "vite": "5.x || 6.x || 7.x || 8.x" + } + }, + "node_modules/vitest": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.1.tgz", + "integrity": "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.1", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "6.1.4", + "tinyexec": "1.3.0", + "tinyglobby": "^0.2.17", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.1", + "@vitest/browser-preview": "5.0.1", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.1", + "@vitest/coverage-v8": "5.0.1", + "@vitest/ui": "5.0.1", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", + "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zimmerframe": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.5.tgz", + "integrity": "sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0037d27 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "reforgermods-launcher", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "check": "tsc --noEmit", + "test": "vitest run", + "tauri": "tauri" + }, + "dependencies": { + "@preact/signals": "^2.11.2", + "@tauri-apps/api": "^2.11.1", + "preact": "^10.29.8" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.6", + "@tauri-apps/cli": "^2.11.5", + "jsdom": "^29.1.1", + "typescript": "^5.9.3", + "vite": "^7.1.9", + "vitest": "^5.0.1" + } +} diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD new file mode 100644 index 0000000..1f72e02 --- /dev/null +++ b/packaging/arch/PKGBUILD @@ -0,0 +1,94 @@ +# Maintainer: reforgermods.net +# +# Builds the launcher from a source tarball produced by scripts/build-arch.sh. +# The tarball is the working tree minus node_modules/target/dist, so this +# packages exactly what is checked out — not a tagged release. + +pkgname=reforgermods-launcher +pkgver=0.1.0 +pkgrel=1 +pkgdesc="Server browser and direct launcher for Arma Reforger (reforgermods.net)" +arch=('x86_64') +url="https://reforgermods.net" +license=('LicenseRef-Proprietary') + +# webkit2gtk-4.1 pulls gtk3, libsoup3, glib2 and the rest of the WebKit stack. +# The binary links rustls rather than OpenSSL, so there is no openssl dependency. +depends=('webkit2gtk-4.1' 'gtk3' 'libsoup3' 'glibc' 'gcc-libs') +# cmake builds aws-lc-sys, the C crypto library behind rustls' default +# provider. Without it the crate's build script produces no library and the +# link fails late with a wall of undefined aws_lc_* symbols. +makedepends=('rust' 'nodejs' 'npm' 'cmake') + +# !lto: makepkg's default OPTIONS put -flto=auto into CFLAGS/LDFLAGS. That +# makes aws-lc-sys compile its C crypto into GCC LTO bitcode, which the +# linker rustc drives cannot resolve — the build dies at the very last step +# with a page of undefined aws_lc_* symbols. Rust-level LTO is unaffected: +# that comes from [profile.release] in the workspace manifest. +# !debug: the release profile already strips, so a -debug split package would +# be empty. One artifact to hand over is the point. +options=('!lto' '!debug') + +source=("$pkgname-$pkgver.tar.gz") +sha256sums=('SKIP') + +build() { + cd "$srcdir/$pkgname-$pkgver" + + # The frontend is embedded into the binary by tauri-build, so it has to exist + # before cargo runs. + npm ci + npm run build + + # --features custom-protocol is what makes this a production build: without + # it tauri-build sets cfg(dev) and the app loads its UI from the Vite dev + # server on localhost:1420 instead of the embedded assets. See the comment on + # that feature in src-tauri/Cargo.toml. + cargo build --release --locked -p reforgermods-launcher --features custom-protocol + + # Assert the result is not a dev build. You cannot tell by inspecting the + # binary — tauri embeds the whole config, devUrl included, either way, and + # brotli-compresses the assets. The signal is the cargo:rustc-cfg=dev that + # tauri-build emits, recorded in the build script's output file. A dev build + # packages and installs perfectly and then shows ERR_CONNECTION_REFUSED on + # the tester's machine, which is exactly the report you cannot debug + # remotely. + local build_out + build_out=$(ls -t target/release/build/reforgermods-launcher-*/output | head -1) + if [[ -z "$build_out" ]]; then + echo "ERROR: could not find the build script output to verify the build mode." >&2 + return 1 + fi + if grep -q "rustc-cfg=dev" "$build_out"; then + echo "ERROR: this is a DEV build; --features custom-protocol did not take effect." >&2 + return 1 + fi + echo "build mode: production (frontend embedded, no dev-server dependency)" +} + +check() { + cd "$srcdir/$pkgname-$pkgver" + # The launcher crate is included for the Linux-only startup workarounds in + # main.rs, which are exactly the code a Windows-first project is least likely + # to notice breaking. + cargo test --release --locked -p rfm-core -p reforgermods-launcher +} + +package() { + cd "$srcdir/$pkgname-$pkgver" + + install -Dm755 target/release/reforgermods-launcher \ + "$pkgdir/usr/bin/reforgermods-launcher" + + install -Dm644 packaging/arch/reforgermods-launcher.desktop \ + "$pkgdir/usr/share/applications/reforgermods-launcher.desktop" + + for size in 32 128; do + install -Dm644 "src-tauri/icons/${size}x${size}.png" \ + "$pkgdir/usr/share/icons/hicolor/${size}x${size}/apps/reforgermods-launcher.png" + done + install -Dm644 src-tauri/icons/128x128@2x.png \ + "$pkgdir/usr/share/icons/hicolor/256x256/apps/reforgermods-launcher.png" + install -Dm644 src-tauri/icons/icon.png \ + "$pkgdir/usr/share/icons/hicolor/512x512/apps/reforgermods-launcher.png" +} diff --git a/packaging/arch/reforgermods-launcher.desktop b/packaging/arch/reforgermods-launcher.desktop new file mode 100644 index 0000000..27a8d28 --- /dev/null +++ b/packaging/arch/reforgermods-launcher.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Type=Application +Version=1.0 +Name=reforgermods.net launcher +GenericName=Arma Reforger Server Browser +Comment=Server browser and direct launcher for Arma Reforger. +Exec=reforgermods-launcher +Icon=reforgermods-launcher +Terminal=false +Categories=Game;Network; +Keywords=Arma;Reforger;server;browser;launcher; +StartupNotify=true +StartupWMClass=reforgermods-launcher diff --git a/packaging/windows/sign.cmd b/packaging/windows/sign.cmd new file mode 100644 index 0000000..bdbb9cf --- /dev/null +++ b/packaging/windows/sign.cmd @@ -0,0 +1,52 @@ +@echo off +REM Authenticode signing for one artifact, called by Tauri during bundling. +REM +REM Tauri invokes this once per bundled file, with the path as %1, and it must run +REM *during* the bundle rather than after it: the updater artifact is a zip of the +REM installer plus a minisign signature over that zip. Sign the installer after +REM bundling and the zip still holds the unsigned copy, while the signature it +REM ships describes a file nobody will ever download. Wiring this through +REM bundle.windows.signCommand is what keeps those two in step. +REM +REM Configuration comes from the environment so nothing about the signing identity +REM is committed. The credentials themselves are read from the environment by the +REM Azure SDK inside trusted-signing-cli (AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, +REM AZURE_TENANT_ID) and are never referenced here. +REM +REM RFM_SIGN_ENDPOINT e.g. https://eus.codesigning.azure.net +REM RFM_SIGN_ACCOUNT Trusted Signing account name +REM RFM_SIGN_PROFILE certificate profile name +REM +REM This deliberately fails the build when it is reachable but misconfigured. A +REM sign step that quietly does nothing produces an unsigned installer that looks +REM signed in the release notes, which is the one outcome worth crashing over. + +setlocal + +if "%~1"=="" ( + echo sign.cmd: no file to sign was given 1>&2 + exit /b 1 +) + +if "%RFM_SIGN_ENDPOINT%"=="" goto :missing +if "%RFM_SIGN_ACCOUNT%"=="" goto :missing +if "%RFM_SIGN_PROFILE%"=="" goto :missing + +where trusted-signing-cli >nul 2>nul +if errorlevel 1 ( + echo sign.cmd: trusted-signing-cli is not on PATH 1>&2 + echo sign.cmd: install it with `cargo install trusted-signing-cli` 1>&2 + exit /b 1 +) + +echo sign.cmd: signing %~nx1 +trusted-signing-cli -e "%RFM_SIGN_ENDPOINT%" -a "%RFM_SIGN_ACCOUNT%" -c "%RFM_SIGN_PROFILE%" "%~1" +if errorlevel 1 ( + echo sign.cmd: signing failed for %~nx1 1>&2 + exit /b 1 +) +exit /b 0 + +:missing +echo sign.cmd: RFM_SIGN_ENDPOINT, RFM_SIGN_ACCOUNT and RFM_SIGN_PROFILE must all be set 1>&2 +exit /b 1 diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..73cb934 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "stable" +components = ["rustfmt", "clippy"] diff --git a/scripts/build-arch.sh b/scripts/build-arch.sh new file mode 100755 index 0000000..951c555 --- /dev/null +++ b/scripts/build-arch.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# Build a native Arch Linux package (.pkg.tar.zst) from this checkout. +# +# scripts/build-arch.sh [--no-check] +# +# The build runs inside an `archlinux:base-devel` container, so the binary is +# linked against Arch's own glibc and WebKit rather than this machine's. That +# matters: a Tauri app built on Debian links libwebkit2gtk-4.1 with Debian's +# soname and ABI, and handing that to an Arch user is how you get a blank +# window or a missing-symbol crash. Building on Arch removes the question. +# +# Output: target/arch/reforgermods-launcher--1-x86_64.pkg.tar.zst, +# installed on the target machine with `sudo pacman -U `. pacman resolves +# webkit2gtk-4.1 and the rest, so there is nothing else to install by hand. +# +# Docker is only a clean Arch userspace here — nothing is deployed and no +# daemon is left running. --no-check skips the rfm-core test suite. +set -euo pipefail + +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +IMAGE="${RFM_ARCH_IMAGE:-rfm-arch-build}" +PKGBUILD="$REPO/packaging/arch/PKGBUILD" +STAGE="$REPO/target/arch" +MAKEPKG_ARGS=(--force --noconfirm --clean) + +if [[ "${1:-}" == "--no-check" ]]; then + MAKEPKG_ARGS+=(--nocheck) +fi + +command -v docker >/dev/null || { echo "docker is required" >&2; exit 1; } +docker info >/dev/null 2>&1 || { echo "the docker daemon is not reachable" >&2; exit 1; } + +# 1. One version, agreed in both places. +# +# tauri.conf.json is what the app reports about itself, so it is the source of +# truth; the PKGBUILD carries its own copy because makepkg needs it literally. +# A silent disagreement here ships a package whose pacman version does not +# match the version in the window, so it is checked rather than assumed. +CONF_VERSION="$(sed -n 's/^ "version": "\(.*\)",$/\1/p' "$REPO/src-tauri/tauri.conf.json")" +PKG_VERSION="$(sed -n 's/^pkgver=\(.*\)$/\1/p' "$PKGBUILD")" +if [[ -z "$CONF_VERSION" || -z "$PKG_VERSION" ]]; then + echo "ERROR: could not read the version from tauri.conf.json or the PKGBUILD." >&2 + exit 1 +fi +if [[ "$CONF_VERSION" != "$PKG_VERSION" ]]; then + echo "ERROR: version mismatch — tauri.conf.json says $CONF_VERSION, PKGBUILD says $PKG_VERSION." >&2 + echo "Update pkgver in packaging/arch/PKGBUILD (and reset pkgrel to 1)." >&2 + exit 1 +fi +VERSION="$CONF_VERSION" + +# 2. The build image: Arch plus the toolchains, and a user whose uid matches +# this one so everything written to the bind mount stays owned by the caller. +if [[ -n "${RFM_ARCH_REBUILD_IMAGE:-}" ]] || ! docker image inspect "$IMAGE" >/dev/null 2>&1; then + echo "==> building the $IMAGE container image" + docker build -t "$IMAGE" \ + --build-arg "UID=$(id -u)" --build-arg "GID=$(id -g)" - <<'DOCKERFILE' +FROM archlinux:base-devel +ARG UID=1000 +ARG GID=1000 +RUN pacman -Syu --noconfirm --needed rust nodejs npm webkit2gtk-4.1 cmake \ + && pacman -Scc --noconfirm +RUN groupadd -g "$GID" builder 2>/dev/null || true \ + && useradd -u "$UID" -g "$GID" -m -d /home/builder builder 2>/dev/null || true \ + && mkdir -p /cache/cargo /cache/npm /build \ + && chown -R "$UID:$GID" /cache /build /home/builder +USER $UID:$GID +ENV CARGO_HOME=/cache/cargo npm_config_cache=/cache/npm +WORKDIR /build +DOCKERFILE +fi + +# 3. The source tarball makepkg consumes: the working tree, minus everything +# that is generated or host-specific. target/ especially — those are Debian +# build artifacts and cargo would happily reuse the stale ones. +echo "==> staging sources for $VERSION" +rm -rf "$STAGE" +mkdir -p "$STAGE" +tar --create --gzip --file "$STAGE/reforgermods-launcher-$VERSION.tar.gz" \ + --directory "$REPO" \ + --transform "s,^\.,reforgermods-launcher-$VERSION," \ + --exclude=./.git \ + --exclude=./node_modules \ + --exclude=./target \ + --exclude=./dist \ + --exclude=./src-tauri/target \ + --exclude=./src-tauri/gen/schemas \ + . +cp "$PKGBUILD" "$STAGE/PKGBUILD" + +# 4. Named volumes keep the cargo registry and npm cache across runs, so only +# the first build pays for downloading the dependency tree. +docker volume create rfm-arch-cargo >/dev/null +docker volume create rfm-arch-npm >/dev/null +docker run --rm --user root \ + -v rfm-arch-cargo:/cache/cargo -v rfm-arch-npm:/cache/npm \ + "$IMAGE" chown "$(id -u):$(id -g)" /cache/cargo /cache/npm + +echo "==> running makepkg in $IMAGE" +docker run --rm \ + -v "$STAGE:/build" \ + -v rfm-arch-cargo:/cache/cargo \ + -v rfm-arch-npm:/cache/npm \ + "$IMAGE" makepkg "${MAKEPKG_ARGS[@]}" + +PKG="$(ls -t "$STAGE"/*.pkg.tar.zst 2>/dev/null | head -1)" +if [[ -z "$PKG" ]]; then + echo "ERROR: makepkg produced no package." >&2 + exit 1 +fi + +# 5. Checksum, for handing the file over. +# +# The package is already known to be a production build rather than a dev one: +# build() asserts that before packaging, which is the only point where the +# evidence still exists. +sha256sum "$PKG" | sed "s,$STAGE/,," > "$STAGE/SHA256SUMS" + +echo +echo "built: $PKG" +ls -lh "$PKG" +echo +echo "install on Arch with: sudo pacman -U $(basename "$PKG")" diff --git a/scripts/build-windows.sh b/scripts/build-windows.sh new file mode 100755 index 0000000..6935634 --- /dev/null +++ b/scripts/build-windows.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# Cross-compile a Windows executable from Linux — no Visual Studio, no root. +# +# scripts/build-windows.sh [--debug] +# +# Produces target/x86_64-pc-windows-msvc//reforgermods-launcher.exe, +# a standalone binary (the frontend is embedded at compile time). It needs the +# WebView2 runtime on the target machine, which ships with current Windows. +# +# Release is the default, and for a distributable binary it is the only correct +# choice: tauri-build sets `cfg(dev)` for debug profiles, and a `dev` build +# loads the UI from the Vite dev server on localhost:1420 rather than from the +# embedded assets. Run a debug build on a machine without `npm run dev` going +# and you get a blank window. `--debug` exists for attaching a debugger, and +# the script checks the result and says so. +# +# How it works: cargo-xwin downloads the MSVC CRT and Windows SDK from +# Microsoft into ~/.cache/cargo-xwin (~1.5 GB, once), and LLVM's clang-cl and +# lld-link stand in for cl.exe and link.exe. The LLVM pieces are fetched as +# Debian packages and unpacked into a local prefix, so nothing is installed +# system-wide and no sudo is required. +set -euo pipefail + +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TOOLS="${RFM_WIN_TOOLS:-$HOME/.cache/rfm-win-tools}" +LLVM_VERSION="${RFM_LLVM_VERSION:-19}" +PROFILE_ARGS=(--release) +PROFILE_DIR="release" +if [[ "${1:-}" == "--debug" ]]; then + PROFILE_ARGS=() + PROFILE_DIR="debug" +fi + +PREFIX="$TOOLS/root/usr/lib/llvm-$LLVM_VERSION/bin" + +# 1. LLVM cross-linking tools, unpacked locally. +if [[ ! -x "$PREFIX/lld-link" || ! -e "$PREFIX/clang-cl" ]]; then + echo "==> fetching LLVM $LLVM_VERSION tools into $TOOLS" + mkdir -p "$TOOLS" + ( + cd "$TOOLS" + # clang provides clang-cl; lld provides lld-link; libclang-cpp is clang's runtime. + apt-get download "clang-$LLVM_VERSION" "lld-$LLVM_VERSION" "libclang-cpp$LLVM_VERSION" + for deb in *.deb; do dpkg-deb -x "$deb" ./root; done + ) + # clang-cl is a driver mode of clang, not always shipped as its own file. + [[ -e "$PREFIX/clang-cl" ]] || ln -s clang "$PREFIX/clang-cl" +fi + +export PATH="$PREFIX:/usr/lib/llvm-$LLVM_VERSION/bin:$PATH" +export LD_LIBRARY_PATH="$TOOLS/root/usr/lib/llvm-$LLVM_VERSION/lib:${LD_LIBRARY_PATH:-}" + +for tool in clang-cl lld-link llvm-rc llvm-lib; do + command -v "$tool" >/dev/null || { echo "missing $tool" >&2; exit 1; } +done + +# 2. cargo-xwin, which supplies the MSVC CRT and Windows SDK. +command -v cargo-xwin >/dev/null || cargo install cargo-xwin --locked + +# cargo-xwin keeps its own clang-cl symlink and refuses to start if a stale one +# points somewhere else (for example a previous checkout's tools prefix). +XWIN_LINK="${XWIN_CACHE_DIR:-$HOME/.cache/cargo-xwin}/clang-cl" +if [[ -L "$XWIN_LINK" && "$(readlink -f "$XWIN_LINK")" != "$(readlink -f "$PREFIX/clang-cl")" ]]; then + rm -f "$XWIN_LINK" +fi + +# 3. The frontend, embedded into the binary by tauri-build. +echo "==> building frontend" +cd "$REPO" +npm run build + +echo "==> cross-compiling for x86_64-pc-windows-msvc" +# --features custom-protocol is what makes this a production build; see the +# comment on that feature in src-tauri/Cargo.toml. +cargo xwin build --target x86_64-pc-windows-msvc -p reforgermods-launcher \ + --features custom-protocol "${PROFILE_ARGS[@]}" + +EXE="$REPO/target/x86_64-pc-windows-msvc/$PROFILE_DIR/reforgermods-launcher.exe" +echo +echo "built: $EXE" +ls -lh "$EXE" + +# Assert the binary is not a dev build. +# +# You cannot check this by grepping the exe: Tauri brotli-compresses the +# embedded assets, and it embeds the whole config (devUrl included) either way. +# The signal is `cargo:rustc-cfg=dev` from tauri-build, which cargo records in +# the build script's output file. A dev build looks completely fine here and +# fails with ERR_CONNECTION_REFUSED on the target machine, so it is worth +# asserting rather than assuming. +BUILD_OUT=$(ls -t "$REPO/target/x86_64-pc-windows-msvc/$PROFILE_DIR"/build/reforgermods-launcher-*/output 2>/dev/null | head -1) +if [[ -z "$BUILD_OUT" ]]; then + echo "WARNING: could not find the build script output to verify the build mode." >&2 +elif grep -q "rustc-cfg=dev" "$BUILD_OUT"; then + echo + echo "ERROR: this is a DEV build. It loads its UI from the Vite dev server on" >&2 + echo "localhost:1420 and will show ERR_CONNECTION_REFUSED anywhere else." >&2 + echo "The --features custom-protocol flag did not take effect." >&2 + exit 1 +else + echo "build mode: production (frontend embedded, no dev-server dependency)" +fi diff --git a/scripts/gen_icons.py b/scripts/gen_icons.py new file mode 100644 index 0000000..191f698 --- /dev/null +++ b/scripts/gen_icons.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +"""Derive the launcher's icons from the reforgermods.net brand assets. + +No image libraries, so this is reproducible from a clean checkout with nothing +but a Python interpreter. Run from the repo root: + + python3 scripts/gen_icons.py + +Sources (from the reforgermods.net site assets, copied into brand/): + + reforger-mods-app-icon.png -> application / taskbar icon, all sizes + reforger-mods-icon-dark.png -> transparent mark used in the app header + +Downsampling is a box filter on premultiplied alpha. Premultiplying matters: +averaging straight RGBA makes fully transparent pixels drag their colour into +the edge, which shows up as a dark halo around the mark at 16px. +""" + +import struct +import zlib +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +BRAND = ROOT / "brand" +ICONS = ROOT / "src-tauri" / "icons" +UI_ASSETS = ROOT / "src" / "assets" + + +# ----------------------------------------------------------------- decoding + +def read_png(path): + """Decodes an 8-bit RGBA PNG into (width, height, rows of bytes).""" + data = path.read_bytes() + if data[:8] != b"\x89PNG\r\n\x1a\n": + raise ValueError(f"{path} is not a PNG") + + width = height = None + idat = bytearray() + pos = 8 + while pos < len(data): + length, tag = struct.unpack_from(">I4s", data, pos) + body = data[pos + 8 : pos + 8 + length] + if tag == b"IHDR": + width, height, depth, colour = struct.unpack(">IIBB", body[:10]) + if depth != 8 or colour != 6: + raise ValueError(f"{path}: expected 8-bit RGBA, got depth={depth} colour={colour}") + elif tag == b"IDAT": + idat += body + elif tag == b"IEND": + break + pos += 12 + length + + raw = zlib.decompress(bytes(idat)) + stride = width * 4 + rows = [] + previous = bytearray(stride) + pos = 0 + for _ in range(height): + filter_type = raw[pos] + pos += 1 + line = bytearray(raw[pos : pos + stride]) + pos += stride + unfilter(filter_type, line, previous, 4) + rows.append(bytes(line)) + previous = line + return width, height, rows + + +def unfilter(filter_type, line, previous, bpp): + """Reverses one PNG scanline filter in place (RFC 2083 section 6).""" + if filter_type == 0: + return + for i in range(len(line)): + a = line[i - bpp] if i >= bpp else 0 + b = previous[i] + if filter_type == 1: + line[i] = (line[i] + a) & 0xFF + elif filter_type == 2: + line[i] = (line[i] + b) & 0xFF + elif filter_type == 3: + line[i] = (line[i] + ((a + b) >> 1)) & 0xFF + elif filter_type == 4: + c = previous[i - bpp] if i >= bpp else 0 + p = a + b - c + pa, pb, pc = abs(p - a), abs(p - b), abs(p - c) + pred = a if (pa <= pb and pa <= pc) else (b if pb <= pc else c) + line[i] = (line[i] + pred) & 0xFF + else: + raise ValueError(f"unknown filter {filter_type}") + + +# --------------------------------------------------------------- resampling + +def resize(width, height, rows, size): + """Box-filter downsample to size x size, averaging premultiplied alpha.""" + out = [] + for oy in range(size): + y0, y1 = oy * height // size, max(oy * height // size + 1, (oy + 1) * height // size) + row = bytearray() + for ox in range(size): + x0, x1 = ox * width // size, max(ox * width // size + 1, (ox + 1) * width // size) + r = g = b = a = 0 + count = 0 + for y in range(y0, y1): + line = rows[y] + for x in range(x0, x1): + i = x * 4 + alpha = line[i + 3] + # Premultiply so transparent pixels contribute no colour. + r += line[i] * alpha + g += line[i + 1] * alpha + b += line[i + 2] * alpha + a += alpha + count += 1 + if a == 0: + row += bytes(4) + else: + row += bytes((r // a, g // a, b // a, a // count)) + out.append(bytes(row)) + return out + + +# ----------------------------------------------------------------- encoding + +def write_png(rows, size): + def chunk(tag, body): + payload = tag + body + return struct.pack(">I", len(body)) + payload + struct.pack(">I", zlib.crc32(payload)) + + raw = b"".join(b"\x00" + r for r in rows) + return ( + b"\x89PNG\r\n\x1a\n" + + chunk(b"IHDR", struct.pack(">IIBBBBB", size, size, 8, 6, 0, 0, 0)) + + chunk(b"IDAT", zlib.compress(raw, 9)) + + chunk(b"IEND", b"") + ) + + +def write_ico(entries): + """Packs PNG-encoded images into an .ico container.""" + header = struct.pack(" (platform id understood by launcher.js, kind, human label). +# Order matters: the longest, most specific suffix has to be tested first, or +# "-setup.exe" is matched by the portable ".exe" rule. +ARTIFACT_RULES = [ + (".nsis.zip", "windows", "updater", "Updater package"), + ("-setup.exe", "windows", "installer", "Installer, Windows 10 or 11 64-bit"), + # An NSIS bundle is signed in place: the updater downloads the same + # -setup.exe a visitor does, so that file is both a download and the updater + # artifact. Any artifact with a sibling .sig is treated as one, below. + (".pkg.tar.zst", "arch", "pacman", "Arch Linux package"), + (".deb", "debian", "deb", "Debian / Ubuntu package"), + (".AppImage", "appimage", "appimage", "AppImage, no install"), + (".rpm", "fedora", "rpm", "Fedora package"), + (".exe", "windows", "portable", "Portable, no installer"), +] + +# The updater target triple Tauri looks for in latest.json. +UPDATER_TARGETS = {"windows": "windows-x86_64"} + + +def classify(name): + for suffix, platform, kind, label in ARTIFACT_RULES: + if name.endswith(suffix): + return platform, kind, label + return None + + +def sha256(path): + digest = hashlib.sha256() + with path.open("rb") as handle: + for block in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--version", required=True) + parser.add_argument("--artifacts", required=True, type=pathlib.Path) + parser.add_argument("--base-url", required=True, help="e.g. https://dl.reforgermods.net") + parser.add_argument("--out", required=True, type=pathlib.Path) + parser.add_argument("--notes-url", default="https://reforgermods.net/launcher/changelog/") + parser.add_argument( + "--notes", + default="", + help="Release notes shown by the updater. Kept short: it is a dialog, not a changelog.", + ) + parser.add_argument( + "--published-at", + default=None, + help="RFC3339 timestamp. Defaults to now, in UTC.", + ) + args = parser.parse_args() + + version = args.version.lstrip("v") + base = args.base_url.rstrip("/") + prefix = f"{base}/{version}" + published = args.published_at or datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") + + if not args.artifacts.is_dir(): + sys.exit(f"not a directory: {args.artifacts}") + + downloads, updater_platforms, skipped = [], {}, [] + # Signatures sit beside their artifact as .sig; they are read, never + # listed as downloads. + for path in sorted(p for p in args.artifacts.iterdir() if p.is_file()): + if path.suffix == ".sig" or path.name in ("releases.json", "latest.json", "SHA256SUMS"): + continue + classified = classify(path.name) + if classified is None: + skipped.append(path.name) + continue + platform, kind, label = classified + url = f"{prefix}/{path.name}" + + # A sibling .sig makes this the platform's updater artifact, whether or + # not it is also a download. An unsigned one is never offered: the client + # rejects it, so publishing it means every launch checks, fails, retries. + signature = path.with_name(path.name + ".sig") + target = UPDATER_TARGETS.get(platform) + if signature.is_file(): + if target is None: + skipped.append(f"{path.name} (signed, but no updater target for {platform})") + elif target in updater_platforms: + skipped.append(f"{path.name} (a {target} updater artifact was already chosen)") + else: + updater_platforms[target] = { + "signature": signature.read_text(encoding="utf-8").strip(), + "url": url, + } + elif kind == "updater": + skipped.append(f"{path.name} (no {signature.name}; not offered to the updater)") + + if kind == "updater": + # A dedicated updater package is not a human download. + continue + + downloads.append( + { + "platform": platform, + "kind": kind, + "label": label, + "url": url, + "size": path.stat().st_size, + "sha256": sha256(path), + } + ) + + # launcher.js offers the first entry whose platform matches as the primary + # download, so an installer has to come before a portable build of the same + # platform. Within a platform, installer first; otherwise by platform id for + # a stable file. + kind_order = {"installer": 0, "pacman": 0, "deb": 0, "rpm": 0, "appimage": 1, "portable": 2} + # Windows leads because that is where the players are; the rest follow in the + # order the download table should read. + platform_order = {"windows": 0, "debian": 1, "arch": 2, "fedora": 3, "appimage": 4} + downloads.sort(key=lambda d: (platform_order.get(d["platform"], 9), kind_order.get(d["kind"], 9))) + + args.out.mkdir(parents=True, exist_ok=True) + + releases = { + "version": version, + "publishedAt": published, + "notesUrl": args.notes_url, + "downloads": downloads, + } + (args.out / "releases.json").write_text(json.dumps(releases, indent=2) + "\n", encoding="utf-8") + + latest = { + "version": version, + "notes": args.notes or f"reforgermods.net launcher {version}", + "pub_date": published, + "platforms": updater_platforms, + } + (args.out / "latest.json").write_text(json.dumps(latest, indent=2) + "\n", encoding="utf-8") + + print(f"version {version}, published {published}") + for download in downloads: + print(f" download {download['platform']:9} {download['kind']:9} {download['size']:>10,} B {download['url']}") + for target, entry in updater_platforms.items(): + print(f" updater {target} {entry['url']}") + for name in skipped: + print(f" skipped {name}") + if not downloads: + sys.exit("no downloadable artifacts were found: refusing to write an empty manifest") + if not updater_platforms: + # Not fatal: a Linux-only or unsigned build is a legitimate state. It is + # called out because it means the updater will find nothing. + print(" WARNING: latest.json has no platforms; the updater will never fire") + + +if __name__ == "__main__": + main() diff --git a/scripts/version.sh b/scripts/version.sh new file mode 100755 index 0000000..d7f592a --- /dev/null +++ b/scripts/version.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# The launcher's version, in one place. +# +# scripts/version.sh show print the canonical version +# scripts/version.sh check [tag] fail if any copy disagrees +# scripts/version.sh set 0.2.0 rewrite every copy +# +# The canonical value is `[workspace.package] version` in the root Cargo.toml, +# because that is the one the *running binary* reports: rfm-core builds its +# User-Agent from CARGO_PKG_VERSION, so the API's per-version rollups are keyed +# off it whatever the other files say. +# +# It has to be repeated in four places — Cargo's workspace manifest, package.json, +# tauri.conf.json and the PKGBUILD — because no two of those tools can read each +# other's format. tauri.conf.json could inherit from Cargo.toml by omitting the +# field, but the src-tauri crate declares `version.workspace = true` and Tauri +# parses that manifest itself rather than resolving workspace inheritance, so the +# value has to be written out. `check` is what makes the repetition safe. +# +# This matters more than tidiness: an updater whose manifest version disagrees +# with the installed version either loops forever or never fires +# (docs/distribution.md section 6). +set -euo pipefail + +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO" + +canonical() { + # The first `version = "x.y.z"` under [workspace.package]. + awk '/^\[workspace\.package\]/{f=1;next} /^\[/{f=0} f && /^version *=/{gsub(/[^0-9a-zA-Z.\-+]/,"",$3);print $3;exit}' Cargo.toml +} + +read_package_json() { python3 -c 'import json;print(json.load(open("package.json"))["version"])'; } +read_tauri_conf() { python3 -c 'import json;print(json.load(open("src-tauri/tauri.conf.json"))["version"])'; } +read_pkgbuild() { sed -n 's/^pkgver=\(.*\)$/\1/p' packaging/arch/PKGBUILD; } + +SEMVER='^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' + +cmd_show() { canonical; } + +cmd_check() { + local want tag="${1:-}" status=0 + want="$(canonical)" + if [[ ! "$want" =~ $SEMVER ]]; then + echo "Cargo.toml [workspace.package] version is not semver: '$want'" >&2 + return 1 + fi + local name value + for name in package.json src-tauri/tauri.conf.json packaging/arch/PKGBUILD; do + case "$name" in + package.json) value="$(read_package_json)" ;; + src-tauri/tauri.conf.json) value="$(read_tauri_conf)" ;; + packaging/arch/PKGBUILD) value="$(read_pkgbuild)" ;; + esac + if [[ "$value" != "$want" ]]; then + echo "$name: $value, want $want (run: scripts/version.sh set $want)" >&2 + status=1 + fi + done + # A tag is checked only when one is given, so the same command serves CI on a + # tag push and a developer on a branch. Both `1.2.3` and `v1.2.3` are accepted. + if [[ -n "$tag" && "${tag#v}" != "$want" ]]; then + echo "tag $tag does not match version $want" >&2 + status=1 + fi + if [[ $status -eq 0 ]]; then + echo "version $want is consistent${tag:+ with tag $tag}" + fi + return $status +} + +cmd_set() { + local want="${1:?usage: scripts/version.sh set }" + want="${want#v}" + if [[ ! "$want" =~ $SEMVER ]]; then + echo "not a semver version: '$want'" >&2 + return 1 + fi + python3 - "$want" <<'PY' +import io, json, re, sys +want = sys.argv[1] + +# Cargo.toml: only the [workspace.package] version, never a dependency's. +path = "Cargo.toml" +text = io.open(path, encoding="utf-8").read() +def bump_workspace_version(match): + return match.group(1) + '"' + want + '"' +new, count = re.subn(r'(\[workspace\.package\]\n(?:[^\[]*?\n)?version *= *)"[^"]*"', bump_workspace_version, text) +assert count == 1, f"{path}: matched {count} workspace versions, expected 1" +io.open(path, "w", encoding="utf-8").write(new) + +# package.json / tauri.conf.json: rewrite the single field, preserving formatting +# (a json.dump round trip would reformat files a human maintains). +for path, key in (("package.json", "version"), ("src-tauri/tauri.conf.json", "version")): + text = io.open(path, encoding="utf-8").read() + new, count = re.subn(r'("%s"\s*:\s*)"[^"]*"' % key, lambda m: m.group(1) + '"' + want + '"', text, count=1) + assert count == 1, f"{path}: no {key} field" + json.loads(new) + io.open(path, "w", encoding="utf-8").write(new) + +path = "packaging/arch/PKGBUILD" +text = io.open(path, encoding="utf-8").read() +new, count = re.subn(r'(?m)^pkgver=.*$', "pkgver=" + want, text, count=1) +assert count == 1, f"{path}: no pkgver" +io.open(path, "w", encoding="utf-8").write(new) +print("set version", want) +PY + cmd_check +} + +case "${1:-show}" in + show) cmd_show ;; + check) shift; cmd_check "$@" ;; + set) shift; cmd_set "$@" ;; + *) echo "usage: scripts/version.sh [show|check [tag]|set ]" >&2; exit 2 ;; +esac diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..98fae84 --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,64 @@ +[package] +name = "reforgermods-launcher" +description = "reforgermods.net launcher: a fast desktop server browser and direct launcher for Arma Reforger." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +publish.workspace = true + +[lib] +# Tauri builds the app as a library so the mobile targets can link it; the +# desktop binary in main.rs is a thin wrapper around run(). +name = "reforgermods_launcher_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[features] +# Tauri decides dev-vs-production from this feature, NOT from the cargo profile. +# `tauri`'s build script computes `dev = !custom_protocol`, and in dev mode +# `generate_context!` makes the app load its UI from `build.devUrl` +# (http://localhost:1420) instead of the embedded frontend. Without this a +# release build still ships as a dev build and shows ERR_CONNECTION_REFUSED on +# any machine that is not running `npm run dev`. +# +# `cargo tauri build` passes this automatically; a direct `cargo build` must be +# given `--features custom-protocol`. See scripts/build-windows.sh. +custom-protocol = ["tauri/custom-protocol"] + +[build-dependencies] +tauri-build = { version = "2.6.3", features = [] } + +[dependencies] +rfm-core = { path = "../crates/rfm-core" } +tauri = { version = "2.11.6", features = [] } +tauri-plugin-opener = "2.5.5" +serde.workspace = true +serde_json.workspace = true +tokio.workspace = true +discord-rich-presence = "1.1.0" + +# Windows only, on purpose. The updater replaces the installed application in +# place, which is correct for an NSIS per-user install and wrong for the Arch +# package: a distro package is updated by the distro's tooling, not by the app +# rewriting its own files under /usr/bin (docs/distribution.md section 7). +# Scoping it here rather than with cfg! keeps the dependency out of the Linux +# build entirely. +[target.'cfg(target_os = "windows")'.dependencies] +# default-features = false is not tidiness: the plugin's defaults include +# `rustls-tls`, and because Cargo unions features across a shared dependency that +# switches the *whole binary's* reqwest onto rustls. That pulls in ring, undoing +# the deliberate schannel choice in rfm-core/Cargo.toml — no bundled C crypto to +# build, and the machine's own trust store, which is what works behind corporate +# TLS inspection. It also does not cross-compile: ring's build script fails under +# cargo-xwin, so the mistake shows up as a broken Windows build rather than a +# silently different TLS stack. +# +# `zip` is kept although the current NSIS bundle is signed in place and ships as +# a plain -setup.exe: Tauri emits a zipped artifact for some bundle types, and a +# plugin that cannot unpack one fails on the user's machine at install time, +# which is the worst place to discover a missing feature flag. +tauri-plugin-updater = { version = "2.12.0", default-features = false, features = [ + "native-tls", + "system-proxy", + "zip", +] } diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/capabilities/main.json b/src-tauri/capabilities/main.json new file mode 100644 index 0000000..bbb0601 --- /dev/null +++ b/src-tauri/capabilities/main.json @@ -0,0 +1,13 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "main", + "description": "Minimum permissions for the launcher window. The window is frameless, so the UI may drag it and operate its own caption buttons — and nothing else. The UI may only invoke this app's own commands; it is granted no filesystem, shell, HTTP or opener access. URIs are opened from Rust, after the server id has been validated.", + "windows": ["main"], + "permissions": [ + "core:default", + "core:window:allow-start-dragging", + "core:window:allow-minimize", + "core:window:allow-toggle-maximize", + "core:window:allow-close" + ] +} diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..64764575d803b6f4e72d259b3f4ff0d2deeeecdf GIT binary patch literal 13683 zcmV-(HH^xMP)QNkl(aBHqVK$s(83V|Sm8Dt*9XhQ~^09!#+=te=86EY+L8WlnUt!Sh8UTd#s zH)dJs-KskK+uz=2 zpMB2h=;-L^hIC0$&pQ)DJ=}Bd`yEa05xRCmJUTPEVmX~J9+1wL&P*4|mt+d1e@Yih zw?Tdg`32-*$fG!aPr6uU=dZx^=R;0Q7mDx0aj#T9U)Th{eWO0QiF73ZAf(uq3fPDs z=w|J;`tMhhaf+OEP#9rGA)(MbvgfYi+-fm69CqxA=V23ptuZ_J`GCw2gn_e zH=y(tnS7}i#{tNolmRI<`MO^oYnJ4T>~pIieO&TV#Oq!L5*&YnA5Rz`9uMMzex+HU~>D9+S!!zbz^J50ICjjJJqlhnQrE$aBN;sKCJ9BOoXa0t6H| zLh?9Y;OAEWbZ_GLLR!e5AaJE<;Wug@#m7BFKe}ue0s+quJ0Vxa! zwi0#h-~vF1_zg}>pKKYb{jiOQP;tG3%++ql1~E1OE?3-4!v z9UAX7L8;t2hqGFt<+=ib27|JGPg$1rjsz4voFF6F;C=M>*RzIMtow=IsjgM;7vZXk zZ!#djojrp8cw}cPlir-VkDFTgo!wD>S6IlG&`1x-psr@DSXn(+dR?BQ3h)@fQi5aw zrJSr#)UZ}E4Hlj6G(Z!|S3wHqygv5BCA|LDi}pS1lK{|`YLw1CUy}DJRfMQ>LKHhlH{##Jd94^g0-;2t?8O^dkz69drsz?m zd2GNza4!ttv)!4fB-vo|i3~SvpT4j=ls-QrGIxV^;AS^E1%d&Cv~(LV7HddFj|r(r zknTX@W-F#zFt~gmQZU@i8csE9ii`y`Btw%Fz$!~MLp|(w^Ate`J(}MpxzIH z%I-QAh4QVQ33mxBr|~M<&ssGdy? z?esF*YcUMw@8B|v8zU~*FaYT6?82Xz0js}P`ne&Gwted!^L=z>%m+ZZ-hwd#V5!vo z*PJV_)?Q(%lI03%64*Si2v3^A$|?e-8#j@8>BsCz+ZDT#$FH&xHL zE~i95P}CrJ8*O-7DwjVFAc!_36Rb}Fbar%ir-0mZ@CP1{`SekX9(@HdX-Elzp-m8s zC)_*9t@v($2ACxc3Ks~DdkMI+V&L}`Nmo=cfeC)uxpAv2iLN8s@G-RIw=kG=vdk59 zZsq_;b>jyggCG8k?4#Q#4+Eg#0w6NSMl+^pV)Yb2W7)QaE8S!YOP16=L#=Z8<24!>|E804r#wqh-p@tn5NAQ;nKNS-GsJ_lKVc0K^3iJ8qQ573p0 z3X>7=u^-qg`@1xlG6+wm>4sFc0)u(20l)P)3ln*{(6rB23=x?PrN zjX2%BddTv*H7f-fMK_r89OZ{WAjE3CNCTj%0i~IsOqEyFN68Q_04(K2J}Z&$srmpc zRU5wRswOc&hy@yJLhoa{G22CGuqU_(H&aP-1uSF8 znBWHh!LjSZg|8a`km$jW_#%GL64@VC{=PZI(+06Ypx=iwU8`>-h+%}1yskNeG$N06 zs^i`@t>4$mPq?^zwDJDB$ZGd@QNBa1J+kV0La(o{Wa`-&S~_Vyq=2q ztaVL;OB>40so!8J+Ph|r`X2F{)*1vfE(qmEoWQW6*kBc(qLwcnzAi4kT>yakF*U1D zdY&pjauONZs%-{Xy>`}QD?cZWYnZa__r!T91?TnqYqTN)AZR9$1(qKB_p$FMN>$e@ zi(_TNiidqda`g|6*U{`hC@}PYBYQ z13*}zX>`}hWy4G5hz1s5&-(-K6I>&#b1aMCO z_MR31P)3wIP(}uq7z+%s+0g*O4V~#+Zf!u&*+!SI6B8fL#+N+B%eI{&4GDmDtbI++ zr_M&sfMAXGp@d4^h7C#7J^(hDsNWg2eSOtuwQzhg-XdQhMiB)Fo&X5WVq5f7>Z~4ZMQd=v3 zLNcYY282XXav>m;{l*Wy$1OW_jefr>t-tDhuaN*CLf=omLw~L*2$Ed_86v0dfVqOD z0Kw0Zv@5roYmm4W5N>h?0Pq^Kegt}>Y7{O(`PwQsbWkS0BX&Wc8@AlUYLk>y?lB5Z zw0d%X@|}@$-e!oZb-fE9>wJI>s5?yPEx56Pa0Q}k_yQKYI;A6%O|7*HK(2o(l)pI3 zd_T+hIRMC!K5)pAC8~KJy$%1{EO4@Xvt}1G*bb9+iD#U8T71$er^F|peg=-G;`QnA zjMGk!Pd@Do$mv{W$fG(p!#XF*%tq2 zf(C&Pi~tB^vWq0ZTJ;iCP{^vW*#a8}uVptdV%Gmf+~BRO80;Al3{a`N4A4YriPjjc zQ@~U;M`@{*ttZBhJ@(sppl@Yd8|aG%`+F_Hxp$m%?DaeRJiC5?onuG-*?08ypn(S#ZNx@M11|%z83F)&>{N2B)Bok1fk&2iV3`A5ucq@ zX7dlf-YgFAs2@SZ&Y4pA7q|hYYnO*{bESOZ0yG1-)(-_&-Do%XJm3R;TCXSx)BS!2 z9u%)!{-UklX3AI()!tG z->8iKT>!`gh5^CEowtj(d(RH>cDq2{vwghnBnXb%Y1x4bJFlLb*Y-8`^Bv+HChy4a zyY-F}Eue8JRpbBjqaVcs3IwjA3?MWUNQJ4_7R`GWZ-IlbKmZ(x(oewXx(ewY?z-!) zc$*z|B4F1>6zm=3{Fs2$VpQqm*CCe2i3;UClGX^Kt8ZSKEePu86w;Ids%uEXCq8n< zd*itaXT|fl%$71Mo_o`*`0ATxisOP={2C_g+*J$C!Z~)7@82{_U&lqhX7^#&%v&@o z-ff>r9wSWLai{pXXP>c@=CMFObp_%zbq_9uD*)9f)lY6p+KzN>LrC*V#Tmyirn$j^U%x8Fl>0bmBuLUoo_WpgCT z*@qF=VJxRSF%bZu|4*<5mHBWlV}PQZAb}nLq%KkAxd9prl(*hGzU1;#<7;l7Ls-B7 zK`Eaqn!jEst)0Dp(`@qIEKk_I&%5YY;|idB2q< zR`RrOQO}OiaR4u+7EI6}MHjAbig(;~$N0)`%;ZckZ=nr@xi<+B%BJUi2B0!uf?@um zIf7wso*f@?!ai0Uz>aw|(gP3NAJ@F-+_o~^EtJu|iUWKr?hQo#$$p*!$aX94^`ZM{ z*Tb1?`oV`Z>l&(hghpgnJLy7MhXELy_X7lrgNY!}Ll4mLoa*}V)UWUz!y*r$13|+A z;lUd23Jy48FN+1{JBps?D4$vVye1$}ZiT;QM_hm9H_nPD?Xi;u04BTdwRil+>#xN@ zEA8)f{q8#<|LD-+i~&<4LM*NGQ3d6Itq#)t5ro zJ9z-W8>h*{PfhuM-UVdJ7l|Mr0IFig%>&5b<;)KqCML9GRI3UKP4`1wUq~gS!aJBCQ6Sb)ze1T{rrUfd$1>9#R5s)MGd^ZLW6z2PM z*H8&d7CA{G4*&rMAm#FOCKI3Zx)gdoGX#KZAuE*itBH`20s;qs5?cKulR>5=OaItH z&xH7spF7D?o*U#T0O<1`{o7zMF~e-#yEyCI!%ap_#sdt9-@Nr!FOnyU_PT!U$RH7_ zC_9w6JuWxR@|3TbLd6j5 zUaV_)#SLf0`yR4~N0%(`{y0*_wZVR)>_H~|;z+?jUk^$J3|iLsPSQ0)?EdmQ2CaMc z6M*>c#s`4S1dO#_{L}NA*;HEH8C~VYt%2!tYKFCr10sH)jOSKWZV{*5!WmCq&JJDvx)k>vvb6j<~;a*YAV zOs#Zry^t%VeCfLP18m!R)-rj|z2f6engKaEK7Ph2{HR`=qb|&*uy|IK!voVQgL%`MGSiXH< z@_lX!yE=iprVBrPQCv&v5+(rvOz`*TUtlpnASa+o+bs}e&E(fQS^R=!1$!|}e335r zMe^rHwnCLz86#<4yn4^-4mO;6{N|&Bq!FWFQAM@c=vZ(pz^2ElHDdUejzX?e*_W*> z>py?pIGcD!$S^0JsGhfohz{Eza0r4?cWQ14+#WyPr!-+5>`D?Aov;Y0Cc9Z5fp!i+H*&));&Vj z!cohkSl~z;9&gPn%T@(XQaQlL!*y0MhDpc2^u9?@D1YN-XfO4O_#W1HfD`>MxOIF$0YU z5=+EXaj09ywf~`e#HY+UC_Z`C!SP9dcR+mN`wzhJ!1$yy4~$R5Yj$MkPhjWIJc!Hj zXG|C8At$iUP&rc^PdZDyfBfmw_CtJpd!2Sdhx4aIWY7%#(pMtplEP6xPZl36$1o%0SV$Jg&YvjnK5>0*e-j$ zC%*bdq;-)dz6MF6Yi~YVCyPYVXO0k=L=vy7`R_SyD zhQQ=M?zn@qR9gTjJNW>6Ouz;}vHTeJX*!op{kj>lOeTPu0w8L#%xmK0&`M9JH((7M zu{^#WBe=;?^diT%Tf@V1OntyY0|3JwrvBj4D^H6n6SuK^Hv8_KChr!%`s&NNsFW6o zCMhZ~KokcnfWnnPaZ)8vpgxhaR2%athzsa(%fn=0NL2wq6o(YcPqUrY82}c_Vc?Ng zYu!fvdUjM=PZG7~^R1yl(itB-Opg!DCjp)7@9EI+{6(RvBjpz6$ib(X{2Yr1W@6M( zj~2>q8Ea5BX=8x(%|m5mO$tAQg{1wx;R+zqudnq7>IqC)nvM_dzyH5OUD-8F%ELla z$K-m5Ix8qQ%PPMJH*f_5z}MK$+YSI)EzoSOco{Wg0LK;NoMB@nZ0PtCm!633+f2J| zK#NiOHSquQrGl-vfd-Otgpn4UGCA<5sk}QDIrW6sU4OkAweywVA1DcP3PlEkRmCCD zxU8aChq#KqcY0T?hz~rB40D&;rvJbadKFq1AOKJ(KZo&vOIQ$iIRNmc%mLWTy_mIs zJVvnA0y03rIyXV(D|A{Qa3^|hv*Wh$`JXvHzTmSb#Gim%pyhb|{6c_)zsIhFoG(SZ zK0zP#IehlxmmC{^^x|XU3okp79}7KxCb6{DHt{2mJREx?b%`MGCxrwneZ1;nC+}~n zlB^Q-&tCSqcBj&*Bv2z%AZls>beC{c<^m7{ls?V?@Q;w!`L<~?5R4L5-F6m9_7*%8 z3mu6Sue`N3}e&oA)kPYxQ%Yh5wa2cLh*rI!X7t;sqGp~)e}|Cj8*k1znM z3BcZs8K7+`!3Gc8>QxaF2Me z{dbF}>^B+5UFGv#<2~OynIHFJg7Z_D>?dR|>-lc-y~+9>{Iig$(|6~=C#@r7#~zfD zJ^j>^R#50zbL=a*&zpZx^Of(;Hs;p<@WT(qhrI8|1eZ6VCH?0d-=Fnpzv32!u(5z= z!2HwM8bDj2C-6UD09g)30Z^t}t|l#aPOQI5tUn<$SgAGv09pA1M^^cxl0nzFSw5X8 zI-94HtwE%P<})fmw`;k8hP}#~L;Vf_=niQT z0OfnJ-ap@*G6gEQ5&-BGae&@0uWs#(X?FbxxdFMQ3t!l<-BmGd%a>qKvx}4zCrpSx z{rMSAO4!d93T4mHNg#I=(aRbVKz-@j_lvoPfy$)2?)q75%`4~+&+Z}Bz3%n#N-)Ws z4}LJ-1qG_i-Q=6sBHD_sPG}ze`|AdcS);W+1F(UhmFNL1P-_RFD75@ZC?7L|N<+FD z$lXfS&HG!6LfacX7+@qU++3m4%M*Y=l(6m2+oQDnOfxkskSd->Qor`-AsrktN}uPX zlEqv@e;9YiT_(pbz4*dAgANN=GkvV-XQn?tNY(?m^*i6uBadyYoYK6pBEDl5ReGI z%Bza?Orb@ytr^I=NJP&xo*kH5=)?~WAlU2UPTSvKdjP`L_%V}6``p<@q3L221bPz5 z$5GvD)eWrM+DReHCk#;0>EEo|3lLqvm0R@jN&9Q`C zJ{$ISC`9?qM+q$^2-Xmipzvgrk%dMI=@`J(WLnu=Z*HLzFA&$kj#u9}JKkl=q+mDl z>)*J+tV2YXhjHfn)x2e7Ooi|ArEenIKdM!kT5IK-70(+@dH^H_kiNfwNxvBShw|^v zCz-&{ z0RU4x%loUD$37F7IKgHDGm?1ORj2Vf1vmM`QpJxx`bbP>69?jfK4<9}KlAtOGk=J; z-Dy(t+J@ZcPYA84Xs>2osU|HB5#9^Y)B>_yUOp3A5%f4-8N`c2<<{l`03#W*ClCQZ zc`OR-bz`N9{aV2_?FNWsi8;OO9BZ(xwx1Y(<+?Mq0{v-bCJs=F_2wLsA!idb8W5ay z&f&oYtM)-j*D_eKUArGz45BY71gD4CQml=itzLn@H&B+Qgbe`JZ5tJ5jsYn!jrW+g3%Z6M z&@|49O6QSUz_|-W7xCI#KFIR~R#y;5_F0}gkfVz8<@N0KTvmpLm}0-f_6*KPxZuK1 zDpz2Q1Jv4BBZw|V_m!`Hb?xJUvKRfY_V-}&HkOd|&-QUHuYiu(&aM;vg(BIK@P*R< zmIFhzj0ON7JFb&muiZ?cH8x<&TJ8B`1BUJoKp8<=3`^M-Cv3%qW%Jk+@dCPx{JA`* zL|sRPU7N??5PLt5(LpX%b`+ms&x6&y=*Iov2S14Iso!LsL3c3H7mBA#hS}4O1Yf{DBW2`!D(y&z{DMQj-P=qr)v*}hJ(#|gZ5yMVvSc`dhwmz zu$V=Yvh&cM1c@t%TYDpmJ%xs)9vJ`z*_aRHGl@%vVuDBs2)UE(3c^`HT7SkU-*E-b z#*iTi1Hj!-il&bnp}l{s*uV)6*L4n{QGSm%%ScVckPBgX?;UyU@nS)$;9?W%oF48? zT8EI=Ae6V>)?QE1P7wLQU_$@jvHTHg{oJ(|OW((Fo2JXg;)3%4f~RGda42hHfKloW zOscr7HavE>id!8B1nvC;eWqx+Ygl->c)8NI$|5$c51>FXlSRl%T(P?heN?Un^8;60 zbu}F;h`mLk0*g@A+`%CXJuh6iz#rKRFhN4;SHq!y4FI`d^pM;?fUOL2en(eV^PNOe zh0+8}7`TybMaH8>9y}NjSZ;uB4{tV!ZUl=(-0Y#H{7QSS1}*>+M+^PIqpIEz$zy_KR&aLp;(GyF z700B?D2@U>h|ufEq@LHbQLkVxgM;tcZOYX6byN|#)*n)I&36TQpy;hPUyt{G@BRtX zdPi$DS(?P<|YC z1*_uQzIz+GqFV(4;DCUx5N@sGpqQF(GL8M6*-o#iD4SiY-6Yddp#SsDy0p;HV{modX9Yqlg?v4*Xfa4Mgfq2n^O{M8A2;x>|U2nllz8ZDI7bZ64sdIN&BTiF`b9c6X=E0!oN-JL6x)@fJi?sU2vF5rCJ zqD~l%k*&@XSfD%}m~rIplWV$>lo65>OB;#>RwtFN(V=U}2wYiv z?7dI?-UAMbr%gYA%ijC#A5TSaH+A3r;we}hV$Xf1#k=F0U8n4Y70jmaRm^tTV@f<} zw>`M*jHzlnr6R|hkHMLpI(%Bdel3cWp+$`x+tigHoMU-ani##Xe&Q?ZiB1Gwo+Q2Z)L zKa@YfX+{8GR05!$Q4GL9Kt%yS2Z=$JumB*X5uA;*_6*QyvNbEYu0lXShO$GzsCWTd z;@U-PFLtkn1b}|x$Bz1$^{<4kE&~82{YBQVF%adqgM13>3H=N=eFFek0qF$@`h^?d zU#!x`;{*f&Ae0-e@&$o-z=|0HMJ8bY_(5ba9W4Q;zDu)@$z)bmJ85linA_baF>-4D z?6kA5Xfot&O`tK>Z!Zro+e3}@E9EomXB9s$K~E=krZU;}0svk(809A5rk{uW5%LxQ zuu=wv%nghf7ATE5AZP?Z;4(v#3}PZUC;&VQb(C1D2G%g}0#pwGeomqZs48U1Tzhx% z*aHaNQnzBwl3HUbetN>2Mp}`-sN3L1C}OiEd>i?wFmb&pU%G-+ca)n3Cb$>?_%mYw z>@3Ox!l5)134$~OL4kol1A!|#69Pg42-@j3(jJDUXrsK0|9=7V1k0h1XSVUS=U9BLNS8wN{%;4Ag#_CgGikBtTze6Y980(_lSuhTRgkGM7T-}w)MjadI z(b=8NO-)7lJMcx%Avk=S2ZRa)`2v0nK%f91#El8U@qtplD*k-YhsEY^($h;I!rQjv)4@w=p3cY-$E0f!Zy>?If(-_OKG`i4ASe(PD0neJK>7MOo~5}21A_5{E@dkjPgv3>0D!X30IDS3 zE{IWu7Q@cEitdVOAuvc6WYt1gF-G#j9A#DFC8%FozXZUW(8D8e(fhaBD0=-oGTAJ? zYzBb9`kJpwchGM@z*#_npzh=d#z%7AOgs+A}OE2MldsP7gTyhaNa(hApuZKO2M`2E5l56q+dx3TdY_uO(;AV6=p1_ZI8v<95AJhQgASk&YkON1$%P3uh=s7fv6ND(mG+Kzu0*!F+ z8DIbc0QNhxs?m$OT6!pB#H_Du10lS&B6TYzi#=$`y^8XU(lzaKQNDj!cP{@J43U+2 zcXw=j1au=F$Q=r~7|Q=MWCdpd3V=N89_l>7sBB0(Mj#Af6s&{C24x3wY_5C_0uM+@ zuM+?uMf^TlVZ%p5hm^>8Ko`r9#Tdq%678{oTQPN(28bGbTtt3Fwjb+h6{PhSo`36AmDaSl#sZwMc(;AHK}qF5j`qGDCEk^e zP5i)wg6sqU;A+TX{HW#WD35?J&p`mH8aCa-;s~iHP|QHVV7iW^_aI$Ft9YS7!NRU@ zVdaew0LXpEhyo~6jS>J1lhM%8Owq2Ga^_PIDknu7qNZX*ei!xgu3^*#qw=8d4Osqs z^!K;#*i0UsUD@nb0KnOh2LXaNQUV0%EfSM(Ip!^4+ z{L8yDxotW&vqvX-273Sia}g8#1^`%=Rv-}h0auiZ7!}#WBKwnLK@fz&Ar)Nm0Z<_4 z3MC;HDA?6*PfB>!u0Aw(htjN&yt;AJ&owtjyeAdHy1HG;ry0XzO9h>EYjTdCH zd*TP)1`zxymCf}62!qlcP$n350RZ8DHV_Zg)8t%QH;j%I6hpWyV3bdRBEZm4?^9MF z00~;5qu3VUD2@_0=PG$ro#+2MFaDF3ybz*%X8A1H^K%&3l#b2%(UnSr2~O+E=B@__ zUP@(o_Ygsg1_5D!0tLXR0)zlUaZEA-QNH~;zz9;g+QN-ejTlBKmO}53C(&?N=?c8c zNAsA6|3~2>cxF__I{H73I6WnRKr5cKM`47u8`|$x&^}ZCz8hqaXTAG^R2o~6Mn{1O z?!%9M9w1nb)yR6`5(e;x200jXY@k_SguLbrVtik*o){yzATUhOAj)i}=rQMYGXRk9 zvXvzjDE>pv0=WQz{-2rw)&F*^T_~PigLdkN^523X-Hn{f;T>DlqpJ%{kli03n9rC1 z35!?oXWj%5R!K%60U`x}eq9%RUfFO3ZDnJG4aP#zvJM_YF@uwL2)(bU*j%UF6Z_7h zQa%Mh-C?m2>op&-6Tl99u5O<%ux{QEZ=)#x6x!})xYez<2+I#-ikLe%4(MHgKXMP` z_lOIYF*d-T8*pNSkb4+0N8PBxJ!s2G^wFas>rICN5MqV8e-r?4+?EA^j0tK-6RuN= z9wnAPCZ#TwzZ`A#D;7}0h<57Oq93VLHAM%VpZQ#_5s^e4Sv0 zK}n$`Q1pVvy4SGALhHQoMHh3_9htN(JCJ+uvXf45YQDRk00QB@BnT8+WLzt^|BD24 zVHEAsi+1`04CkEAR4U)`E_twI(a!i&r{d3{hmdDktYz%aGC^#xQa)$2Hz0vv;sldR z7?G9}qFdujl|L;wmM2_@bS3(EQ0XKfC4ru{*m@EGGA{iSpPtU=S$shLN1?7n1bev^ zXrJ5BPBS~$rjGBj2LnL`f9})hA^Zpw_bmSK>%t`z-eThjj1@RDNFbyQ5af{tP{Hn` zj>ahk(*R2ozoSMgpv3D?V2kC4j7FjIxc}$_W6c1jE%VFtg9+RIry@!jfq4 z_tRJy-SMt{bYp#zZ0-!m&0vDx00=LN%pr3NEI`by6fr`sPXmIFFX+r-&8|T5KrkW) zOhlH>R+=B6>0xWtXIGyO3i(`th~)xCbW9_ZJEYi5K3A^?@gJGum!d5mLM6|qQN>%> zGK0Of20;qrh|vK&YnY9`2Oyw|6*&V?pp;cc@~fnKP+;g6bqApYleXGWybx04sHfzD zdyZ50kPEn!<(8cLNvl`NmX===&vS#4>R*98h5!FE2uirq*^a-q2lClv1cx)w5DUQs z4~uRhPaLg)Ygi6hApnq5u|lr`Ls|!nK}JZjV9%1(ctjv#R}`+5CzUAxk;?~qEaEpitWO3ucn=!ne8{a}gXg-1OJG5watpnZ z2^1Uj6U7WVY!FI0CtG||DGU+CLy_d3oZf{vG0BMZ@A8`bFiVGXsz=;||M*=1|L@`J zclqm}`?kFTW};?uhd}N|Lp_N`dj_&Zb{SVF0Sl~3YX)F*6qP$DBmrP3;J*`sNh|-L z%B^cMdpRU!SUnd^043+8UzdJgX9GnVw--$II{woW2!MYGnT~9E=5HEL${nTCJpjQ6 z&|qI@Zb9S?3QI+|vGBGK4u)O{gkItbtf6PeH4M2$DkODVb4D$P&H2IrWf>XGt8{ja z9EnSUL^_T8B)9q~cWP%QD*jEw?G7hobF9eZlK{YfAdUP>H1Mx5Uty#Ad7g0aC$ zH1;aV0`_2&V244e#G0v6azrWLD7r=?b$RarnV%T&#xoQEB7V(ESsKTxbRWfU|5yBn z^Fa3#{zmufnn=t>J!tINXv}Y=*oasz|9c3VkFZ4K4|qj#pPr>;WY81KEm#0xO72f7 zAUVpHbq!2Z*K{yN*+5u3i%nQh<{nlk3yVJs&;Mre>n%no6f9?DPK)tGwk5? z7E;8Q^CPo-q4ZTUb-hGV{ImG}Cq*JFe&6 z9gb(B&SAICY$5e-^6#BQwyZ}Kbs{#{6^fY!#m@%_{*4t-LY{{FflX$G{0Z^`o8At2 znN4d0YrKWyQh9t8^8cXl--B5gkbaHl4@#wf3da)>)a?{#uXl~olVpqhPf%773f&vA z!qGUM1Gxh7&yerK;{P4Tp8+(FNTA$>bGI|_K+zw891lT#W4_R&9kzJo|9=WnS?n@6 RRRRD2002ovPDHLkV1fy8sOSIy literal 0 HcmV?d00001 diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fb2051d59dbf3cb5fab3de0fb5df32fd665dd79e GIT binary patch literal 51649 zcmV(@K-RyBP)t{Y>dc=q>RE&=-DtcyL;yO>&sR3)z{N2t%QU$&+|Q}!|8KQ z_ndmG>a8?3Ha0c^YqeeDcC8j(TXld7|BbTWu+NWrtOLESu@TO5cl*QNMg3nUVYSU# z)fsQ){fVr6gQd1V20-{>J1?(-&wdS#^+i~pXmtw!iQ-RT|M$XY z-vy7i!1f&2{vH6#S+G52ve!FytlP~F;2yc}?`{kLs}sUMekTd;foj}eb`TJ+v(=sI zfj~YLgzo=?bsh-bHW0)sVEcbz`>!ChUuosVf57&?K!aZe>pSrHdmz+5P=XDgy&CrY zkrH$eW+n7D!235%nsp;AcwSrw`~4Um13(m4!Eyf`tb%`p$A1T(`8WW_$KhCi3-A9u zy!I(rpM~e2f$hg({W*ZeyW!ei1^Ygy)vF#eQIx$AfAjmm;TQZjaS#DuR$14cz!cH zz7^JOupg}Q7I^(;5K{Acub754W7jmi21~sL`}c}l)iL0B*T5FQqxgZ|<^UpjsX*i> zu>C141tizNXMSpcrBLAUlkmE-G|In#$FGOSC%1~CeV>JJx4hrHKj+p&0szJjqV~VN zzW1GJf!x_Y_OQQk5Tv6(c%BD>sQmPQ002-zrL^g7({K$63~truRzT6!ATb>t+pS)?d2Zgn zO<#Mj_zs*K+>rcV;I(b=UDm<&gmyQt0p?O1!hL^tt)=M!U;+vMM8X0XKS=)n9R-O4 z5w3%Eo`d#-yFUT}kL_yL?t@ef~_ zG(_Vc!}chl%+<@`h$pMU_!S9HhaHk9Aophe@ z)j-|BP5%@i&RzpT^G6^I%Afu$tbc{2$hU8SaC~2x-_}%jg2#p43T(O43Axeq<-FDU zPN;X8$N9Xm67o589P(KU1oNo z1R(Hb{athY{J&wVNXkzDckq^pJpc8vR%`Wr7QtN#0v-S+4x;cMC;*K80!{Nf`F!K! z?W}h{(C}+O+x;bk)qXgkiMagQiC%FXtQ$ZeZqlYUXj>4(8PJZ-#3#ZG!nNCML9p}E zu3}fFw(d3ie#^Y)_MeC6%B(jlX!@Y}yXCwC0JVkd!~07XhuU{1dTc+N4<@@71dPkD zLvs{DgKHy1C*y{usXhvwKBZqkHeZzuR{ouS&dsrvkv( zK_L0=@c^)D)x>JhZimB-{sOqM6_c()bN>wjf6YX${o|XgnS6^S-$3|-kB+pm5o!=- zy?VJ9@-|y7wg3=zo5xx>2gCx%t?=i1IU$C9xR8_QJIOKtz+H!V1RS%0Y}DMqj#YX?YYiBoci@okLSSKB1gFidmM22%2}7gVt?K~WJP5m9`ZMOb>^VTgV?f}% z#GJI+yuI0fX+Rf(vjU+6Gk2sN(dJhB?*U=hW40Cq z0w7=ufNJPoj-P82ebSog%#_`73^M1#Rj4+eR$lL3r#H~7-$ zN8G}UB_40nE`qU?u7_iP2Y~ZR0E?GRw%hf69Kk^YfnO*9+||V2!9K_M>Z;cVfqxF% zm|q0pQlY!+Y|LGmxXP#JEV|c#&eTVOYP5YXXn&*aLHK*c9h0j5TvpUu2vQS z{I;Kl#~+&P+m5<$* zTr_q&67lro@n4YO-&0%w^qpI)C%c^oPjvD>1UCP3Dod_qa_MihTHYn!XkXm>@x~;M zNje+KYgw|-X-Mq%TaAxg01*5_3;&*$+X?{CPFVvE1bWULK(0bC@sSWyqR$hPUhv&D zSc|~(62?+d*W5N1%dQa7@ZWcL_+4^h4OoCwzvpZ_cR--B8dbC;S5X9T4RYmXT?60a z%2ucM(uu5d#E|>jwB^hZV@aodf`oP%}%wy@v%ab|Z2kkUKq3L9WpQ zl>e@6&s$Ur`g(f-04&M5o47l%#o*30a5)Z$o0!)LNZ?}W5MsCZ23TVM5B@KIGSTTh zV|+Ys-v7=e;XeEM49_06;@}#4cfuKRT^5|^kx-S zLn{F;koLwJ-m&+L;A?rGKL}Y6{sMsv7ADdEj-G2jT?0VAOACLv>Z8W~U-hu%D0=jlW_|e*#9zlh+snGf!hfS5&)@a-|lhg_DNcD_{-0V|6$Gm9fdm ziF;|?!K#B5E4)|zXpp;CgYdryw9DU^O1JV>N9?tDsLluw)7oD9FL;43knk7YH3->z z`EbEP$G4$5b&NdP2Ma3B3~W<(gIXRzs8L8U=YGC-MVJeMU6tJ}X8{E(Xwd;c_s4ssUu+=&2w_7jyv7aLi5~C5 z-vI-F4}HD*hv>9LD*2QY2_L~dNWuA`A6XKV zniy|)9||VpOW;P?3R>i!ppDko;pX{)x{+*1%E#%MDZfAgfC$V2-TF`&(ae-NAGWX- z^nxD*u_(PC?ldyuCXh*uKhMCYR|xyjbJ8uorpkQRUa^3IAaEh}`WWNSU6VEfDqqhK z1Rp0J?Ls*Ya~cF}bs~Vb0s=_6&c6;(n}37<<7dFCyJE7_dl`WIu{Qr@{Ju!Ry-)aA zPrZC{b*Brx^LpR28GJXI{Ct(jlh0Vsp4mW#IEvnTDey$ZdCp?jr5Cj=j;I#z+Kw5n_ zK5l|!vqD-CEsM8!d7*dAe1b?siAE6sp!0G|hlAn*vW@}>xZkPAt(%-@H?rITBlI@Q zaK0TX7hPBoWj4Cp29xh#fni!#V0QEG0tkEwK;S~yo;cC#bneBCNmkuk0bm>&aU2b1 z;&17qoo@b#N!2c^mm79J|Ir8y4lDPncKVsp5B~ePzc1L*=J7uk0&$;6yF0N_$XL?=YV_gobhg6Cb)rn zJplZYec4s5)$O8ItT3!k*^ao^TXR3o$)}la26*;^&CD?r^P z%#F0&?_;?Ga$Q1LGT{z5?Qj2&x=0DR8hXDu)avY7?Gp5$c&nSge-Xdrd4Oo%zm%sxM=06npPyuQe z{um*oXn@BEd=U0%Wf&Of76Aca=l5X3t&KT|0wp&U%&jSy#Y3Yb&g@^{AmZOM+Mc)& zmOHS=Dp0ChkvQ1BLwP8*<72^Vg2u&~F?fVv(#wfEkXW&NDixFxHy7?yo1kQv~+h(JZ88)4EJqaJJH z1*DN>;*F*UQ|-sJdCLideJu;*oipth>@}19oC}a4OCT#m;a@@o;)3{H&2_nJ4Bs~| zBTe060ML$z%!+TQRrcuPu;}C|QZ{D7Zk0@)#c2dxhm;@?EHf*}%0px8=ou z3K4>h00RGWb+08_XwRhb_6~(44`{^Mdnf!OKMU(7Jsn5oiz~CSh~n1h zCZso9a#=yng3vGEn>c_#xfly=@T)zc=kp4xAQVcQ6X9S2SP7TF@_e|6g zf^P9mP#RwZOW;#L4!#*&f+vl&+W9YQm*RUe0C(3n(?k7hAyN0UZaBM7B{h%kcsU-moY*uSP9gO_uwRj_=vDnG}$M7 zKgs+j$H`)I8*6m}5G`u@3!z1taFlWPCvsQ#7DrU-lndakloknNmb;}gWDs0SW~+1L;%1N zgYkDJ>BNq|sIb3}qYD-cf}pohh9uYe8RZrh5wAdjI0iNT{X>og1_~FkDdHp~#)0a#|~l5M(zWEU%)j(%Z|xh#ckmq^J(}e zT?_v#r}gJm&Ul-dgdy~0SIJ`MB@2@wGg=E#SyL?vpU=ul^toZ}ZJYfgiy^x4SFk!F z0x$RBy+vmJ$F2+$_zaVNdC9E@-pjxrFEbX&l699Bq8$Jj(!@Kgr9KpP)A<+E=ZUndTU*KMQr>YuVopq0e2toUoffu+Zh5^S{ zwXzcaAt&jg4#V`{YKLgf`}+An_(>XdAx@ql8k@YHNoI5@oB#IlUb~i%*-NbsZ*7#R zY`z5sPGn^B(51!Q`~yHRUjmlE3sz?x$Q4-Cx~GG{J*g1LT+{ft zYS;Bl5dJOD`sshe_Gh{ggo*R-L~(v^Vz+3ld=Z(mT&R}Fdu3AimY9%ne|s`p}=N^ZR@ir0f&4kw@lgk-Bo$Yw01 z;}RDjk)0V^EMk6xE>v>agF0&vstJFlq61(BybO9toUAQ@vDK6JR5JbU4gi7hQ$Bw; zKXp>I>oPO-=39PrRv{34B{yG{X5X91g1*-;OL}E804xQ+ypFAcQb@OB4#xmH0NL24~WH||Uvds8C z)D7g{1b6h`LHMr(Yv9c=KjQ3(cIQz*4Bn&Mfx9CB_yixQ&sM_!ELd0C{%)?1Ja37$ zNE1zP^3KqF_#pIUxz9c3ZUW&6O0v)1P<&Ib#4$t!B1u5X1tBk`&(?d}viZNn5}MRC zg-C^z%+PaxOofW4Z<;a3N~%6nlRWxiW;XWCZQ5>*P5(Mr0woeO5N<2rdpi~gGPkc4d|iE4 zA~5h3c=g`!dJ+gou0d4ICj$yL_o>-i)b3nTI1ZWArAZDFig2uehvuS1+-H!kY7dPL zkmMpExeA}NUJPl%j1$QNmy(HYT^UjA85d5c^%B=j8u>FzkgC7_12vubg07&M$)9M*^ z0YI>`A*?>q^v-%90-MUe&m=3Lj5!4i0K=?_jQadm+t(B{N|G-?2t`gDvMfQq^*i-8C{=LCpb z5VQPAD3|w-t5;2L#4S^9Fla`YQNJG)#x<5w?g=EQP%1ROiGr(l#}Egw7&zq znqOlo$!RG=f}04JihPMZhy85L$XyXo1pWneRC4YTB?jGm25$*ycR*#N_O!)%2>`*{ z00e$*tljS1oePMD3HqwL*zAW!9=!(wqxuHW{(k{z`ePgGcY`q&TgW_Xg}V7+EO}ak zl+Ks;nm-}{ktWsedlx~l0NP0V22k+APf~_ct^xv|Ap8huxZrZ&ai3>GqJ=-h+PF*# z{L;#-eJIZzlJK*@!-W~nYAk6UMV1J02BED*^FEU!JG3SP34b=+qDYg9?jFnIac6Cl zBjCFZLx}J{KzV%_Kmf{!^OwN(xbb$ULhitQ9f1YmpM?H>hk@Y#!-Q$qeFOZEbGFPA zS9RG?PKCJu$+X7;v3FR^J@bBlO$06oJK+YX@K^o4MDxaOfM?+C9b z9UWO&r=lFx@}W5zh#ajK5s{iATiNFc5G*l1M0OD&dz85bG~8@Bf!(G_X*us3igb$| zdcI^Y|A$F!(e{3C+;kkiTX6#8zhLdJihEk%_x~FFNLRp*cAXvfJaDW(O4nS%p`%u4 zhlRHXG|;{+F?)ZzFDC1;GyhP!Qp#+8_D*ow82&z5pON>{wJwNiZsjUrb{B zStvvH?|CqCpg`S#1p*jEV5LgP+Ezjui-x-(DdCPRq@qsePGImWQDPSQJPn)HIO-u- z0LhiG0?H-q_KBg}k`zeoY0*|=YO>VaWUl89{2M5?zXM+DU0~rpc2zqo?oO7#FP8O( zuB$+l`~jfkS3&dt7=EmQ>*XF-rGyBq7GKe($|q2;x+Ihu~MuSL{Wq##)T}& zE=(Qmn=t_6*XgYt#07W&4O_^$=p*+mCJMbYQ?*TJm;|JVRQ=FeT@aNiR9 zO}qtwfR?)udlTv3B0=277CT>MDg{I%2jS!ToP~Hj%8WhkMEEBrCkyyN-UOQeU(`fi_@Q>%k<$wy(Wi_`Z8KhWi?t8GS*bvS8Qw?2 zQAszw$n;|ZNr?Xs70eD5Hh%12o;v^tnlK4Q5x8Lu8v_S=-X8{9Qb-d;Akx3B2zZ6~ z+i;#lwru3YZiuAx0$UhAr@LLe} zJ{Ia-I`=LBfZ|>d{vh{MbwJahZiE zSS(UjXlx18q-TT^{8rA=h;h9|=q@yGrxV|zJ6Cslm%w@fOg;wy$pnr4O?t!iSJ3Kxx4$OpI+c9hPJBK4 z64$nr(4=VfhglcoSe=6*#0LTxReTNYq8iCMN&wToA)nbN?z_VE5(|-A8!JZ&w^I!hUL?fi zxcw0Yza9btqk#hX+-N|uZ-|8wU8}p^4zrZlZ^*Sj`fU-QRQ1r`HU-9JRn8jFR$2rE z8UQ#^h%ADi`fwp-FKVa$_eF|I*|EXw!eP zQyd3hb|awS|AzHL!}=Gyx%uBtB=t!7`FPt-m~#l-Ik~qt3pc^&T2bF%eScGin>)Vw z@Xap^vJ4IfVgYm*P;{Jzjs+YG(_TR{YvJ$sTnTwQij~qCqK!NDx9bf1PE}t3Ne-C( zk*yjP^;|0DB1qx;2k33@s*pbFifp-6EvtS^H#@O}V=H%?}q z2i=ucz%aqTqhMhC{!V@*nEoFJ^t&2_|Auz2oXvb=36_UmChEFzVh<;C652dTF3&J`D*PXW-@*Yv>KNW?G@yaY#3@&h zm)<&U$z-5S6e>xHG@72QN{V;c5#>-klz$(rfN#R%_pa_`-MfmMzq0^93;%crihO#- zn?d-$17BJ({>t~?soPMOQKJYUe>_!*7g*hn_4^a-J-+$(!@m1hAlO9Tyc0}nYPMJG zrVlsw{`dyx+D+f!W>x08I>UV4DhN);9-~{+=X|A1X9sL-A%W?Sq|sw=q30Ie8VNYv zZ)1Nu_uJ^l3?O2V>mN8zAbl5P8MHBhED8V6(&Ad|1f9{=bqg&zifqFdtxC0S-Cg?+fjVT@;HcVv_W>L)mnu1K5qxv3$ zN-jW7ZUZ^_Ic)z!^8wxb(Y9XTZUF!XdMp^HUc3;@n$N=*`Zj#Y+qByBxR7X$%cLo5AkLSGt0N|fZn4|nT ze8G9o_?I=WfO9RSa8+2G-B%Fw9?ncxX_K!l?MJlaPzK)IwCv`zH|Rd3vB(WBMf*-h{OfxdQD%4iRQi=;lwH#sVJFvnRk`1q z@|_8p6nF%prTxu|z$0-J2N0;Sk4%I<2yZO_jXTB^zqwah5(pr`n&N_<(8AQm@01Jx zBv+ZueL)VJ?P?SJh82vb&f=2|tNe+q9eQNO5Ou z{#VvQSUGEq5Mx$o_eR=^6Iuu;kXsOhmjVg%=ljIGM^*r{7HY0}6KiUJ>Kvk=jR+K} z>t_}Ww*n%~oW)j%2v}Jy@E%pLCdmFZ{ytNY)!!1O&dFL{3M7TZlq~xE3Yd zd$LYVr3xK}+`IuZcw}Dl0X8zA+Ki94dk?sSorG51nE>FA0lm-<0h9lYaPfbm{24p3 zcbCogr(?9E{(03(S!esaE5JfFvESFnX1T~lBXb{z!{8>E;sQi@0BX(>(ymjEF0_)M zW-AKn)PZRJ{oNq~nEXslx9Mx9ruYzG8G7BBbbV#8a=e=aYo%)%=1yQs3R?Jha9c3Mv0Ry7P4K;ZW$^X}ny00iy=02m)%4SvkCfXRFhY(E8G zW={JxIZF4H+GCS$yK%xvXGE4aBInFag9JYOv$;mFF>&*_9!@}!a9=5Fe5c)pYG$sT z>!}qq_7vJqMNIjMfKeZa+c1?7)~O*vPPa1XOzls(7^56F`u;Ikk7n(R1OQUzVPB3E zpmOKbe1k0MCWMT^R%kN1%PuG!QZ)m``&H#2$p1kfG;%O90QKi4vfeR}(zqP};7Sns z|7tnbU(c9BO(Ch~Pn6+S$u!y_2`)lxT_%E}IQ5p5~E&(G>w&9^S`cH`nAa9n38*Cr*y)k%TB5Z#~Lu^5- zjmpB`UJV!N)n*TDca;}NP5g2nepB7!mu1V~{TP9vth8|8NDXb`FCE7;YpzzwkeU=_3$ zKA4?_4+a2?gU|nXxX=%R@ZStuNcbrSc7ViUqTxo;%;J7M`k|?>Ou}#inTFmYLN60T z+-$YUGEB=OUV&N$j0_t|lhxeK0D-8R0}wW3Rj~K!0}$A1DkbFX^{Ju9$r7(AAvc09 zeYf~E4p{y-;lItO-!ZYDizeXvrW=u$8YH67XDXFh%tjU3kV}0W zGC@VcMm)_F-2_?8WYhFi?sKy6)F>J~lixQtx4+SmCM^#{kYz2LvJAAAtfz)p23Gjd zK3y(Y_3-m5;q7WoU(4Ugz8(ub1&2%);1Pa@Iwkz!JmK8lZKU_p6mui{Y1g1&(E^^& z!pEYO)o>6n6@NUkO0)YRIuUXkCTx7yxwgE_C(T3WEPND2TYm3BM`!vD)9a z!5%$vhiiSZs=k%@Q5zTG|v1oA_3?4n?VC3=KApii)|2*(jehkdz4FCX2`_IFL?{>tWYx&%cn>>z4 z>S5vUC&D|mDBPVP*<_NC>(}a3Xl!a^0LX^1+mvWRny9wR-JtdD)BK~G{Rj4X*&hKQ z>Xik(#QQO;prpsh?vV9E&qa@0$Jc7Xt-CUTDcxf%gOn6PrCO~e`2aRl#U%tm{GeZviVHboaIM=i0a>b=6-=ROz9x zS{&0)XC zZCfUQ0RbIe9rAqmbrP##rG18c|Ey&0K#FJ_&hI2s$U7^ykB{?VQ2TLOIHU+cC>H7# zKLo|AA_M1*x7(Qj0(S`jtb*1?#{(LD4ut>KmMZftssUW^E--IQsc%^?4(V~9NX(&m z-NS?*lVBuGR?LZ?8ltJ|Az`o;On*4PvkoGrxfZmAB>aLPbo}E3_w|Qb5W@rufl21f z?UD*|cL)eX+Lmiz{W$*4(KVO!7~!{4*JSsv==&LW8!!O!-xPh{%oCL&U=GirD1{6| zxngwqMCs(jB{GW4Spl=a3EZqK0f@SN7%cCHjI~v{zWuIVsE;B8<|dwm6fu(vJtcG;^Yw=no4A}jTpWo!d_4D@Kv(BcepmI z3^&uhSt-0O5OGPMDG$J}FS+?$$Hbp~FRl6eLCCR>uHF+sv%>&0wlZRX z$run|K42woU34DaeNaA3lh zRrSgws35SJ^jqTNl>G=m@P3?^8STD_6V5sbE1(mD0gDVIL0|!Q5rUBWS7E&cWbkRL zdU@wy0l?Tw007xYM}Zk~UQ1>C71ht2_E!ZzZVYu~0r;XFUJs8$Y%6MI%o6jk-ms8X z9Ksr&Er<7Mx^*o4nE)tVVx`m})*Jwh-l#*9jm#oyUR zBDy%7Sypho%0~mJ1v3>}7LnC85(z6R222VH6^Fd_TGAy*LQpD*-ui$8GX1so&x=cx=7;q&8~3-wpKkMIT>u9HeK$4N_s{Q;wB7#73_ky7 zhJVif_~xEUC#lrQJCAzr5_3IUF7|7)Kc{-UW!YYbJ^mJb ztazdCu*IJPJ_n!Q;;&DCU;EwsR;^3kZoZrP-8RDSc)^B^%}ZbL%I2ZRKfLMn4RGrF z2;W1<($fEzLgrzz5nKYXX^Y%mZSRm4?sUpAsw-%PIhi3e2)#Ao_3I{8uT4n8yS?3m z4$Sw@dWXPCKdXcv$iJIRk3Y}+??xQJf@A^k^xY_PFaAMUR#`H3dxyeeBuq3ki)`wE zjwO*|YQgW-9DDr3nwjb8X4lTSW^Ve{W^QIjvvV-l%+JjE-#s>@w`P zJb=e^-|1$~JXWtS&+9d#_r*&cQ=i8!`*?YN&VEjv!#ua=obgMYV}PEA<4)`AnIFuU zg<;wz4WGy~Bsr+Wd_Xy``fRGyd*A~8d$N;1;Es}jE9}!> z4M_8JSYL1HeE$lXd{%Rs{8O=iy~;fuE&NdwU~h*p{v(ygju6foHGMW6Uh9wMYmO9y zt{V}wqIf@;v;9MwHS5oY8+3;jekUMA@X>Sho)K&%3|7#|&CdY92-8g1e=dN9KPD2? z0Rjws{COmB=o*n=deFe&gkGO9=k@|VV;2(kS$rK1I3xhm-_0G%UKa>G{FdSQ4!zuU zD_~TBaNV`nH1E0e(&pHQJ;J&#wIv>FY9SDQMocjt79wOV+gLxuYKTj~v_I;Wy_k`r zAsrT+1^~DPtbiYaT)r4Z0}dJh`1iAJ@qkuQTm+$qAA{geBlcfuegyz~5>bFC^hayy zrPSVfqym)g3UO~7Q_faQ3q6438?rBs(+e6S8QPoDzW-nR#h-gkI+)uLgk~^9guw}< z5iq1#gOFg0!MrwLf@5AM@254bJ3iIWgGlp-V|Xh7TLrV|dkhl5Fgq$mfwR!7!@5hA+JATrbGxv;I6W5wG;OJqxZkRIsE=dvydhJUy7;IL&}SpB~S{DTvH$r z%7>TuTuj<(T&D=DW;;8&5?f)?Vf}3=; zp9}pJl=uTdSWJ)4#{NHHP~}RovY#enFkwTqZES+mq!u9-UDdI6k6w=fA;ZZnJmDqb zS8{?$5mdkS8z(ihGq){+2}`vg(x}8do{LOtuYtJ<9{hS{O3%(DwCps|wj4O-XNB_) zX40m5WP!xjPp_9-7Bk{&!oHlFuwXU49B~h`bl;tW+2;D|u5H$z{X&u_&_&@%o(aXGWNvvW$CSIA;TNmvSPYn(?PYATNBc#Q zhdO>-_9>9yI5V~YJRnpA(7HA}^_thbc6km!fM`u5z$s9WECX&d^1t9VWqV?mskU7L}F#cg4aVuKzxmyG8f9OLWZ2E^EVZ)O{ z(y(LqbEP7%;YCALH1dx33m{#Fvyv1G4M0Ax1^N6wZ2unSaZQX6(jN@~fLwS8_!mEf zgM*o&!+ryA`m@9zngGOd|0~w6EAH&dw0kvTfr+HWV__DLHO&7{p1Y1Llo(863qZ%L zo#roSHX8VFI)?qpPkur({OZPa)_}JPB&|6clV|f17+@bAaR#E0C$u>w;D)~+oACHL zoejP#%M9*g_r;!3rAFTlGPs!Km<||+RQgi&HP9PRt#_lhn%22Kdt!YzL zGC7)#TH^z1h32`We!vledotq+KnuQV2LQm$-A?}SQB(U@*lBzm*v1#@f*&LNvo7rC z=KHa*pO65AEr33$?(38&U*9~|zWalYJGyz;i4Sfb_Sj>a<4=V3*oVO5hv>)0op6j< zk9n|O$Lny`iI&QFoM4$(wdwE^a4aRzl|zPH(1jLVVkDp*lgHukgx~_&VToUD7Xhn zKoBDrD+@i>_OpU}!z~fPMTsH{1i)sOF@fla(i?BMwt33`cp6P0LY$E+BmP3)B_p4@nNrWw%T?+&RD$f4%vCym!n=$cSqfGrHD?z;EeP=hDE??VRc{fB^{s7NU4 zX(yke`2eiRILn@5$_E1>EEvW1XOSnoRGDCTUgo~1LIRXhd zgs5boh=n}{(wTH972XG4V0mW8t<6cNJVOo*aw{>43{3G%Q*pE#75ctwC5F3Yvjidi z*z*9!1-E?!H3)#57Uh+#UhnWb3;mHl%={vx#Fl-L z6l`07P+4ySre!&)Cuuzd6fPVl5QoSW2wJp}LU}2zPRx29o&bk_TW`LxdFsgk0xFv@kuJ$88S8&zVF4d_fZTwTX6M!b6`UZnvuNLl$lO7J zY;Ib#b{yB>iN!%|mEg~$!laS=FdHWv@vtekT&}zJXOJ)OSk2vtZX70cYa{&K3UB*j zUVw24i1rUckEdo8o?nZm4x86)4UPf<6&5Tis1vCEzT3@<+YJB^Q-q4Y;-3Ibe(3xB z84JL&|2Fp@B>=SxnFr_?mXmhnMdPnhRbJ(xhc$27@S7l5M%eZCvUT1aaMpkW2j>=S z^cKTfZ~8m%@K(b%s@>f_8<1dcaK}I(uwmO;^125I1T^kE7uo$bU$(Y+=O3TlJnD(Z zp+X;=hh7m7P^8oGk9>4<%gr~b>^98@r2JhP)1OX;)jZ=)gnWi%20FJu0+v*^pBHkb zfl0#7tRUpbN>|@8FvIH@P(}EEIxKv$|8^C?|x#`I4mP3xy%bbKHue0dIyx^w~SU7RO z6rE*M6mK7fXLngxx=Rq2Mq2;02ulk{ib%5v(kU&mOM{A_DBY<@N{E2$5&|OK-6h@4 z?#ugOK0oKonKREk_wT;0F^>yHs1yyoxs5y{oZ&h4R_Q;>E+*6!rj99O%o!P8Qr|PK zejVDtQMTS11h!;BM_~Ge?b8{|pT_Ocm95X4)0fZf8thykElA_>zNIA{luUqZ=I(vu zrSg-idWI*lW>`GYVMQLWzzJ%B?o)%Lh#%_^lEf)w>=S9_F#NUCy&hdodpjTKTfKT% zEe=RiARsNiBG}#9oGK|d(y7b5vM$wut2>Cvm0J9rz4@zU+3)>M$NOmn+;jvZRZ?v- zlHY`Z7Qy-BCo8%SI0xK&ZGgG8aQ15?Ewxp372#Xv082`E>dc4JWYQpRIQ7SvJUN0F z_ZN=(W0HEVGi(PROA!9|X(9bB7p>=|H!D4sfP;^Ocw*vsY4+iI`aCW(xy)QAga2W1 zud@FGJ%!DzNb`THD%SyctH#;LaU7|JmAuiuS33of~aG$<+8FZ z-930qpjWf_1s=ljPwh$5V$p@U`G&G`^nZCLa6T&S=o!39USH6s4p#T|^&(p4@YqT# zI_iXK?${IRdP+jN|DK%d_Ytg+Zf9h>x-}eJuE8=TS?&EadD@hYOvT?ctIdY75}{|q zoMI!#m7BGh8kU6tn^0!}9olqK2FHxIEzipgyhoHQ_p=KQTb*3}SA{CM`Op1wYrRqF zp_noO4f}5?!kxXB5+K3alQ%=oRyTF)4xv*`v!Pg7N^p7#BCrMHy>l3)H7?wi1oNXZ zZc;e8xZpHz8qj1NT z?e(vif*MKiM`=K>r1(5y;-e7Xs6wG5p)O|5(t|3yFL2UDY=D}8nm#GVILVNpW9cjah6r=Ea zU)mH;!lssJyU?n#$E+Z0J zVIg*8@&|*#)BK?Ndr-u*O-QRJqe!x18RM@*?m*gSe+E86LPd-b*%v*J?LU+lw;ltN6bde){;XcpBS zmzebX9_6`fm0~0Tf`F{vAvJ z@LPR*L*mkl$1>oyUo|(UpF0bIdZF8W<_>z9=JsHt_mG%3;pWbj3q-$-08dCn(9LbY z5n=Zh47i(Ckb}kj5dbZ~)=}kaPusZ!jg;yLYSa%wP;u4Q{A3E-d+~U`G z*rI|vv6wuzIb;M9?^u5EPj@@6A)Hj$m5>Gm^Z^B5F|Vix5=xp0xD>LNIP6xX~qY%~+_?gCXdhEpBDCGAfyLw|Y5 z0JU#{ZEF#Jp!CNJ`mefhJmX zgFKp)xNG|G>WEy4i&La0a&evx1St6W@3o$NF*$tz0Q+^;HPM+(c*R_`fIxEBk1&%15$(b_5`i(&D73ijcp-j5XfZ) zw>)`(agY1ug;c zdyUE++w53Yd@2PvuRWHCYJ9+oK935@o#qw(rk)n}DrE9t8$rT3Y#>r4SG00t@DQN; ziI0l%b!C6?d+E}Eza8_otv{k1kcZWj&zF&IV9y~ zQ9;~-z(u}mCeIU-CAD_tr#m!z8r0ryG$tl!;_iO`U++@g&OQltS|jx5i= z6$J|0C6O8mZxFY9UELkA{%du;#0Y_L8<_rH^&+v0#VBlkY(>`(_g6;2W1dq@`Sw;u zoQitj}Rr>b38o><@lkk%Q-=xY20H?8e`u*I!uMEmbh>2AY_9wu5;}_Zoz+z!0 ziv-!XP*a(Q(t(aTVUl$i#U_;yS#LosBj%vA(?QAO=+uXsAiDa(mi$+T8qt!^;F!uQ zN|DQ0^XS?L!x=0WDL?g}=UgQuD#q)Wt9NTnewn#{KewIj6vq_4tPMWThd(=Cm&z;3 zs;b;&6>HHQzm&oSi$sK{egV6)C5DGHw|Hk2c6w&BBNwyXu5DJIvV?wAL`Sd8NVM3? z0UI_8dUhEZDj=lY<9Sfk25eQU9tV1ucJ^y8Sbx3v{fu@>fA#{`#$AOvM(vRYUJ_h@ z2f31743#QaHYR`gb5pfkcFh8A!E`r}_YrxR>?GU#WB&Z^$kpU+;cZ&B~{cOAt#0Q*nJs((^JM zskoSof{da~MY3YNau#V*2@5cx6SVMWZbB#4+zdJ!FI@Vf^X>^eGduC^X?IjPnDETr zGq{Z6;0D8^ZfG8?(1y9V1Enz*MmesGMRxu{jTUsB>UR~u7tiGy3!})sj41{D3?ID_ z-H#liR3@V_1M5e_nE{VDv^1qy6GOVGq?{3(FMs=}Zg^U#-sZQOZ} z6Swp`Tb@NpNv>z0^8JrEFgO+&#ZMTD$gDI4_mK{bc+S+61XZ4m`;G^}eqc2A#{a=qPJO(3s>vpEo@z?zoj!0|AULjKIbu}?TYn=sBJDV3H zuY?Q!?mx};J*?yh;t1Q@Tpt16^-wAh;~RwL!))N>!XBP0n4y$0WgX>i4p{P_2g1~yTrSt{{V`%P3|U?+1! z$2@>#ZmvUd?b0t0u&i#S7s2f}`Zo%@Xl#>#YPj#q08uO=C?Z z=86oWxp67tfA$XmJAD%Z{C1qv60^L*W-NsU+o+sxVQS|>&&ij+X6`-DsvaoYF%}se zdiRcZi!b`s)Rt+V!{5@ERcD;;tpen@Cz4JQSF3tCkfs=s$(w;_`IU8$N-$l7`0|K( z*u~`S5g`;r;|xk+*m8fk&U#~!Kj2F5N7`M`gCGK4&L4wgN&(%=$g;$TNfg`X(zL{1 z%)g0Av|M$>y5Y*{0K}#H1!^=0bQ7BO{$|fn2H;=5V|z8F#1By;$RUJ$q_fp{8v)hx zr21k(DG`-+f}{JBM-!FhJ;dehzwbBi@!hf_q7y^(TgvIzp>{vIgSFqm64&wcSRQvW z|GXl5sEPG-d@GLcs`*`X} zV}*k1-dSVC^ZxAe@x9igIBPWB?FFTV`N)HH~`mpbDbtwhtH>Q84>>x)b?DyY2JzVF*uP4{Y#WQTVM+LxP< z(tnx~z73K}C~SQIo%6`7Hyi&=fQ>`uujYSbR8iZs@si$JRK$s@7!aK%0R2~r_5eU% z!z0~zu1?>Vjd&pBi+~8d`(orXA06h``)biC8!2}*- zk3iJ0goqpfxJwOOmU?;6@!IaB6b%5{UQds2Bg1t&$qZ(G$Mtp-*R|*;mz__8!0+%v z%uh_TKP)#CHZcEOUQoi&vaOmDCOf?okFQaoFg( zastisV5!poUO_Hj?W%v4CL&aJp$00OPh3$0Pv(8!5`vVuNrXr3R_nIJf1v5vWdyNN z9Dco&*F7y028pzx-z!gY5vKHAN+IPE(nC1c5hii&^`9YkeZO+c>uvA~QfWrK-TVLs z_`y_~sKDO{5MT^6_6ohqxZ3-J_`@mIYV`u(cf=q9%u`^1U@rw72$3-_$rEj__tTa4 zJYD}8o7UquxIcs1@&u1dOuJ6CDCIc{4E|gazW`OdTx64%_SwV|e&q#Ta67Lc2=0G(Z12F>A%52H`uc*^#2FXdT=0kdjjj6_-C!hEo%)lt2j9$h=wD=0q-?JYE2qb< zqQ(6)2f9W-;eQp_cvH5!RtB>+j8c&zBmeXtUyL}kS>CvNJoef?pp0(MoZhbYm@-5E z;J5ijey0zjGW)3%ngcA*S6pW2rhLmgqlVNOG9@Gvh!&614yDu3qf+e#biN{r5v9sb}D8wxl&V zg8Fui4jO7+$wFy_!aON%#KQ}-%f9cazQ$^_L>d*!@?vo`Ba&+mF9`4Y0kz6{FYTO{ z3ac&Mz1Tj*67sU9U1s6V5I5iFho$UC76wWPw&rchj6SU%HV*P-PK23z~6LeI}Pz+*5a*|yC_zz+RoP#rTkyTjy zN;&+mF1ZO0?QO)ZKzpaH8&|(qd*7hHtv`>KnjSwkl48~0`=*p%-D5NBZTrafvTWFy z_O^F&$cU&OVia+o@@23+E(>jClQ1MmLs_~tFu5CESvqZko-EUN^^i~ZRL%S}tQu0S zbI=Ix^H!8M+w)os98*l&Z&>^I*J>i;{8MyfLGm;R?3t&FKP6@3MMigAs1Gcp*fX!n z6|xfc43t8l;0l6a^Ks3b!+)XD@xVA+*EKds+(v8!a=j~PbuUV^vEB9i5!ku9;Z<}F z6)}=FEH4(}Z{i1-fctnRUYcEgTazDexqO&#z~>L7Y^-7k*YJhB&zWjaeE{0=l?;wO zv<;kT(ZLB@q8dbk&IdJe{c_9-ayajQ_GfnzV{pd*9XJ7hjoMcNtxR?0`r7PA|??5^a!s-qHF zr2qP&Z@NF|%PU4-Wr|pTDH&Fh=stdFci&b>pN}>*ShDB_Q6sG-qPeO-^^JpCLLa_A z7>{E&59kUC4j^kB$uX=X4C44Wm34VNOpELkM&A>cbm>k+XMSt|q)7+J2^7<^>1QwQ z&5|}hWvl{w>*|iOEZ7SyGv_A}K4~ zgBB+yLa;K!(0SFxo)A#FYVM2lgwc^@`jw1zDV~o9F3vA$zWGsMV-bSHxEk&6yaG1_ zJPdw>2Jb+b8t7ALY|Sm|9R;BPIqT^}`9N1K-Sa&wg1ZrxV8=GQFg;ljg?D1CNrjB- z^6wa|nKyrUg&92Q(KPQD%hBvH9$81ju*+H3bQQU-mm<_S>*1uJy%v)yBOozcwtK&e zmi>nkj4Bdzm-;cWqO71E36U&-HN}vvo-y;RRI!M;vqa#SR<5~nHu?>3P`CaoT}yHX zZ0ADUmN>vP)bDgNMBkD0^8)E{kht<7ll z?JqafnU~j;P?rv9K5<&F?fU`eON^~6<8tlde9N(^&-9x9&|p=&zE>vOq%t_u@45F0 zXFYSj+>xj6*uscMi)b`ym5Y&<)FBt{kKNMwU2xIb<)>Z&%B zLZ(*%wVRzDKnKo9)(kF;ZFMQQ2+9x~Soo^hnDg$Z9&NaaUS?|&LZC)lmY_Per98LX znV4bIRnsz5s}vZg@1jOSX#WY~$?QT(e*l%5AgmoCs=r2?F(5Rh_nf&AB>KNn&y z_M@D>=Ujr0K#y-B&TRZmukv*eg!BQ+hi5Gy=ox3w)m6U}8^zs^%1upncRW+pYS^R+I6Htzp&6d5NeaDu6nnVhZ?vP{fZEaeKMv4@^j)GxiU^Sv~h4~mGovZLYp zv;3q*CD7Z7q_p*|)T~D)uk*z=;?4sf21FRFV5h~x9T;Joc}^gL)XK3K5HSxim%i=O zPZ=UND2=a)`L7w{%kH>`URP`acgzV3u#5|F**_9a`yJ?W3I-WzjX;0&b?9k-T1UH@ zFPj@WNnTPh!rxKThQf6!;jPw}L{B6{{Vz0nf1|vU97Wxr(jK7?W_A{(CKGsDFL6&BRJ+4~Mu z9&&OQ0z#v*{z<CUUW*-Z1CI9LCIisNJAt@JMv-jPpz2?zK#)-y(Jp?$LC=b?W$(?puO%$JLL)6?RER=9eP z-s%~K!VL^3RjQ$Nm%6)W&tdNtGIT^F!9tV;|e@G3{gwN7_3cE8uYZKlHnmb!r6 zK8bK8!v=+}i)h50)Vr4Yock~k8<8D;97G>7Wx$0c0|<_0$XOQPdT+);_jE;wgV%mY z5Qg#yo>LIiBv3-YLfa1*k?L_=JFS0JGxyH-Y2!grs9*G!a?NiKKxU~IeKE}rV#M?P z!ld9J|38E*o6{~JrAD6QvZMa53(+=PMmALR^}r>u{5dt{cWO$GG0UN*z}or1+|1@i%VRD$FMVx!%2}AZ_hUATp;#j3jiab=Fw5;{afXX8Yr~xWk zx5*X4^SNKP)Y$mJAr+uSyjZCgZ}lz*5=6%bCfc_m0j4N`LmCj`t9_zPu8QaAP z3^7$6=y-?jG*2}WgP;awa8cEgaam(;Ug%S3uoCYn*!R+wAv>O;ITJI8Ytr?ZcnQjE z?gZWKCc?HaG>HA|%IfUI-|nC7>~9ca&k>pGs(Kc-3f48?tC|N!)EEoiy*iTjKQ8v+^zDfOeY1)oU+W7kZq^U zmE0ho1gSpTXL)6oFQ#z+^~O2Zv<9&yQEfnnCm;CokV=4L^QaVCsG?2oH|S{(CjthJ z@~^*iU!`svp0PXAH&9}w7RklP-^lEOZ52~I>qBce`jjdVf48sfp!cjPE0T5gLH{y% z&_K@9Se1olAt$Epworpm9pTI5ZcYDDvnxQ-71d-PdrQx*)9s{dFR^r$tck%}Bn z0MEYnxHm|N-)()}*j@cCltv%aLTYuuA}jtyI5fs(S-9r1_deyL=AQ?b&OKPI(HlRv zB;tmxbX`*GHK>;?-BcG&(((CjA8qY&7In}OvnN?9X=mglAjo~#qy2Wg%`D=GSQ9b` zh1s-*kaweBf)ff6`6(!)qBgYRpfy_##&6;sVmvVMuM~ zPD3T2$)DaBR6^%3(vGi9jel~&aSuC2vOvJjd$C=Wf%Yofv-E?Du>u0g%*M;*0x#NB zUC8%MK`wV$U*oybez=7+#}Q=Q>pMAW)e%CU$TP=?;eD<8h+Jma%!txOmj9@?XvVO) zFE6T2zn@VPt7|$dUHhYpD(D^GtC*8v>uJ~p)K6^O|UQjLe%kE|l6TC>OGc4*C&G1ND zLhw`F9xVdt&crgus2&NO>*iAzEL!zJfx3O}lRwg=7Sva2C+FC$F9Gn@_HVz zzMpQg&?K@e!W3M1KGvth`3&0HxZrMUtL*L!Ney<%!~5uEc84AV3f~6u=UtzlPqT4X z2Ax;PFSXi*Y-|L*a`+;?n%q0V;?teLl(?S%dxHCgW1joSe0JK=2U0E_G2U@yA<|eF z!Can+*VrUSyHo6#TLZt-OA71(Ab*n?0l5cblJDL3O&Hio^8;y=nj~<- znQpp`USB#ZcMrk0u7^UA50<*H`eSm{HnjF6F(lXP;~2sqtgH5vUy+5kV^N0q8!h;- zoxQOx&VOM;ZmUJ~bSv4^r}{dC!f0q=z#kDK2%_NTZnm7czcG(F-czeye%En{E&*1# z;hTipHakbBoWrPl`Kv8&bM3ItW|qW$xeet{g`=P|XhA;1M}Pxvm>xju<`BYO0mkCo z$l6alH2yTLd!p~7u2P_SV@4a!y&xn_rM7r5JxI~vuFq~BE9}?g-@=g)4&QY1lsIl7of^V zK0CCo`Q6ftoPtX7XzfJHkn6;(COMYJ5}p}S{TVETWG#jk;((>UbM(%sv?X%>u`I$TY@-mU$;;kAHs3{BmVR6>I}4lc?tv=mr5Wb z`$NS6>qWRh7V)xOmwdS-=Q2ppr+zda{tQQ{P?=Z$>!6mitq%# zgt2uavxy*1L^_PTT?z~6} zQXOJ8gLCpVWhE0Joqt02D5fKr^2@^OM1?mP-9(Mn4{OOE13y}0{;7)zD=x8#zf&7mn}`u6??E#tPb%YMJLs25V1Q-$ z8+`q_&heXhCdlF5N)j=oL?Aa+59(k6_8^;?~^-j8_SHb;VARoWPmURTD` z&c;i11L#ZK?Q(p@aY$8k>pL%C*IMJH{2Hs`An*Y~xJqNbpLOf*xa(EZf0}QRpzLRx z4T4PzRx}Ou?Ja{DdsZ$cQyp4$DjAlDo4!)d&=84Dc`u27yCptu0zUL7%(ZRfYtlhX zA5i1h+kCy$>I(dtev+82PpcqvE+e61->siO8o>$z{V^nX^NKN656&~!c0>~i@d?j9 z!nJXS8&y7%qv6(W^QX#Gk0FQFCACdRt zffaB8LAE1aj|k2P6Kb&iP~8=&()4{ zJl4o%tl|L3uVblvm^)TD_I<)hD{9AC*1%d(BU*Up=q<8+;$lzMBA)3fL9T#z!}*~^ z@7}Lfy1Sb$`H~w2uM%q2F!*q`8FSRE&mCY*h(j6)Zd3o*nP0s$KCBNkD3nbmn}?-# z-HjD`r*fn#XJF39mb=Jy#PP4V<&UsikLB&~=~nN~RD&O<0^p=M<}Poag#0szxQTi3;v*7+ z;U-1|1~;QZYnrtoeedvlM}Vv}iI|X}m{3pFYdGT31k0FV4R!R}PaO6Oj{T4H`A8mZ z_3(-J%Pt>;JQ{XqAD27wZalWR%$mdZbf%wW+vD#=RSEtGW)jc8(w4 zQ&_&(4tmY*zjqPusuMD2lUAJhTIuL`m~B{&rTpyYRu;il>^{!s_6Z8EcTeoy+U8+f zNiEJAEBNmp0cbrf#OTi7N5YZ#l>YZ#K+%VOQEn3AkVo$S=+keS=JDR2wrQtA^LvyZ zx|$RUR!q>^v6(^YO4+{PWaFZzXg0OC#ZR@?RVRPj(w6%MenySOP}Cj`E#!1GUn{VP5^z>PF}EnZy;v|@1cDDomA$k{@JQ3k!#&xKpOr0CkEcE!Sp!BY!OA>_baLcsNjCj2R;m+dGBSfNSL`N{^A z@Po0!pe%+S8T4+#h0C80riEgJ&V@hybYL38q_%dHcP)5-?9n_tjv;@r=|8}GdJy^D z`UBlF4kZ`|ZrENS9|T`%p1yAqJ2bgS9qxF)ynlHsw@}4F@=Ez$@U-qyM*tuRsoZ}A zssKUplvq%cU!ED;eaWP8dIwxTdI7~agEmuA&v`+q1M6m*T(2h3*QcriZ>-lK{aAq< z*A7lF2#9(H?Jfr>p+$BiId2r3+|AAU#8(E2QeM8^UD#)^IC&%7{B#Ga8d|=rn5uZB zOO6*}4<^hG9H^pnb;Dg~zo#oO!kTYXu}h7mm^Hs*WVa~!7J8P9X8#L&_<<9$ z>JW*Ll|1SqVd~kz?zHSkY}m2?#h?GtMZ7-A_nBj)LD|rl;c(YCm5NW4D7v|k=KLyuW2k|G?&ycH@&WS4 zT;wF5I|9@@6VyZOVFXb+{H?)2g5&06j3fx%d_L(Ak-Am>lxf^vxPWBuwuHGhDAx0( zx{pc(O`5H79s2~;WCi=(s~M~6$^Xid>3~O_pg!BZ3~fSldGxXw1%26JG2M^Y#>7Nm z-frQXxsM$Mk?dY(XB;ibMG)cpr0K=8`MgcHLP2%-BS2ad&u$DPzw`N<#YP5h#XyCC zE{d@Ci1!ezP0KT_O1(x=@hg7Ul!|u!F#gfx2RHY{a&`a+t9aVq}<# ziES0|$vg@9p6Ql;$6{7PVi3MlSzxf*JaP5DpJ98!MI|>>Vf)K&XOVudV|GBCO6o z8VP}h)o%;GQIS50yRhxc%fvLiLFo-8T+xZ>WAiUVf{~&r_WVpOio62kJxS8fUXYlM zz2XQIN{;j0W#@~c+W#&JVjBNs5oj=(Xs5+53t;4j3vRAwPbzwQY``Wp*T^ zX@Ep`8Z8T+!TA=bp54UmqLCa6mY`;JxEqbBAyE>e-l)Fpex*(;j0yI3jy}yq{*t(J z)A-8KUg4*V&rRa7&95;zwz|r`O0%0wT5a?Bj;&FHs=$+}>VC28z`i>tlYATfUz$MK zUP$(D!B0e#y43eOqvfDC&E1OrA?Fz(^K9embYHyM492}TO?2HZ6HcF$M$$+v7O3Qr z@HFq9RQSDYi{skmcNXy;$;|Yf4QYR`=rUAZ`pe$N%CbZ&!PM{IP4mD(S2NMq{`rf( zA8EtmrK*dTPtLVt+YB3h_OBZMsAS^?nou-gTRC+9Me&J*R@a^t@^eGYWxq2@fe~5v zQV1;y0ZN~LuOJFXXrH$&{>I!2FlwE|Q`mS`Ov6|EuGbk5sk?{i0H|d=8PO6gIYoG( zlDk)$K^K9@VekTd7vLrGqzoOwYMfkuA;N1`Q{Q=0(%0H-Ij?(c!aju7@XH>!LUB3m z=Kcp2^~-L17P*zj%Ta{KzJ4VlWhAs6-av0r$96(1HW^W(gZLFfY<{I>6-ZLTP|DA~ z7(jSO<9vlr;VX-E&Z)VltGqL-k}M&QEUwGcqDv?vPRq})ulM5&D1X%6M6il#YdpVo zji{G=+-kh(K-iu54yud4*3D_ny4U$8I{9YF98ZxN^h_PVE?sl~Ba$MDYxGnXZuxB9 zkA`~OeL{3eV|=B10*DR%W4rZMbPCVX!=}Pg2a%08Uqr>((G8Fu2{8tUnn3mp*Dd8h z_EFT)EFv)tuz}X7*MbEaWDD%jzdB|>S*-2uB%=mfgS!4PWkY7p!@=XxO{k%JUE)-~ z6Jwh8-8!`#$d<;8#4#R$DUT!AuhwxVCn4gKcH>4t&cI#2V2p1!m-04-L{23NfZG!5 zw0L~BNoTjXQJ#4fIC+25fHedg?LlIPS)u6RDo11tg(v#!#mgsp*s6!#qw+w$ z#e0rEn|BGpm$^G{a=m{L{^DtyR&?R@82@(h4GyOnf)80Kx{Q))6*Iq&o9`$^nW*DX zYAolw2N(D|K#2?~kN`6bVW7dkKLEtW{xqUt>sS^Ay??h+i(Wg(jnXX&?VqTy2L4_z zg`hh92o4p!CiX;GZcx<{C2|7qXeY9v6j&b`w%;p!VY)Mf(+^6yKG=zUJsUaEjkvt3 zPSZmEBv#f5bZ-`NW8J*MS%f690h@uqN2A3pnqKvP#t(+F(LS_O?;44{5)kW#<4w$^ z@%d~)D9u-k9O?Q+NQ}&CYQOliaNcS+=F};2cMN^pTv}oYVe!*ouw#Xy)2`GYNm4B3 z!A=d_cw)8qWD*TsKxqz zro%032P!il`_FH0#jy)^Nz|19(NE~&D3$ZP6w0HkJR zD+dUJTR)@aucO;yibN6jl>#b_X{nM))bC+mrtlydNQQ5gjDIc@FYx;B$P}B#m52-P z!fKMdiC=~NNHqpv{QkoOuJxl6vO`*c6kV>emiG8!eU-z^z(_b<)7hp%kZ&UUYo_~n z&tr6An3L<=cOJSSvLa7P9Pgl=AanZqzqYx(Y#y%qkMhm5$C(Y^^!-Cdr7(jo({<#p zm@RYc;VaRs4A;Jj)s6Dh|2}L+(8AK@z}OgbfY|Ejwm3_sQuwR9zIZJYCC7-B^?S$$ zP@{8ZMqRK?a#kPkZnBlH#fWQL#5kDlgd;ks05tP|JQL5MvODqW!NRXUiq+q0tO~K9 zPl^k_8Ibf9-g&d3-TGY^oaX27HenErWP4Jm9v@aIF#2CeSn-w<20q|jn!cX&xy~A{o;LlTn#qLkuV=K55j^?RsGX`XNq`ip_kqg2g8I>;x`1%E~ zIw$!bUPD0a9wBF8i|P6O#pu5g%C$(;yykGkHYJ-GWVBmV`LNymm~~=tSx36a6QY!1 zG}~SbA-ywk25k~L-yr~Kl(m+8Z8xf?O)#ylO_`IXBd0x{q-(Er&qtSo2Mq+QK#h=2 zg~N!or0n?xGw=TBted=nqgQkEe*@Q-6f?qQ44R#@1Zmj~lHwojS`%iB42--M@L;^B z(ERS{&*3(?A`j@-!BZN*@Wq)`FlDQ~L99!`rKl|^>ey=L)AMc!;w$#;P6Bzo86{Mr z4C_W9$Zji#%FqPWY2{#cg^+3^AX!*E0l9leZMN$ZY<8cG>#z?8@9?IuQZ}F6zZwJACs<7 zZ!NfYHziA%>Q@%i^E&26SmmR$9eeS!@K}v3GTR`8`eRPhYPZ=#WRwJLW~OD*>tot7 zg%k}bhQb*{SMQINESux9>4n*N(J)m5*4LN;ps;jEf_1f0<0&74L z`#Yl>z!r{E3BOV;39W0j9kP&ebheL%`%2m2>10iyIKkx37|-Mvt}~hE-^H2;AYFDK zAw#pO=hwLZ{&91(qK;L600xm?P29d+6nwH2KCkA#VeF-eQH~Htv5z0nDtRoFyqoe+%4E2CR5h`Mz z&)M--QI~BouOI@lOO^C9+qsOJys@B8009$6wF_q=NoDwwx!Cq|9$3Q1M^qLE&y769s_6|)A2$?r(=iFw(yynBn88>5A9++BE`Oa0Fy`x(_8-B1bQ z*h3&#{!z*&LZZU9W^~sxDnhqP%<#;~{zEOLf!CnocW&ftMUgcQO1Y%H<`v?y;0`_(d_s9(Md||R?O0j`n$gazjEoMR zV?&b6oM4Gg=4_vDI#{$GM?T;bdJ962f{xAPLaBtV8_=FN;1@&4HRygOKI+{8$U2;2 zzBWj@=&0#{PJw=7b@iWlOBWe1ymJ=!8U|a2={y=^5$pbs0@UTt)!ACZ#cWX?se!tD z-bof1&DQFNvm^yJrYk~rvS%FDNb8<$2)}MqSh~Rf^xZ?sXKZVI3T^wXYSO82Js7Y~ zK`FmYF&IMj+s^C8x7}|*O$1EHb}&A#If1KR=oH5a=*0O1V>G{!iMv05TLbG*f5EES zI?1JiLMB?WR6VTwUCGvGdA1zTP?o0S94i8qsJF+T%+XJ0o6u5eN%QTu68dH-^?i%!u-ObSR-^d+zEarWawi39k6!&JlfMcRQXC@l z-ag|%bcf>3c8#k^!&NH#Sx%PYPU6n6ht%^Vw(X zGa(hdQZH}Xin6OcnLPrp-M8a3QQD9M9&$h-^Z-TONy#~#e5b9LkrXU;K2K!rK?p3o zkkj}IwulFIKwVoUt|=xpOriADo>X0V%dN|`Alvsj&xktW=08SGod;fVk}LGM{%!7F z4WTl7@T3})9&^y7#Nqe-S<1*&8f|tt*}*3drAn;}S5kx5-+y2vgQoEKIO-qsjIev| zaQUE!08p`iJ9pswLsAW86wBgQyTcz!c8dv1io!2$IG2 z?isOWRYJNbvJI3(Xp2UG;0}@>2c24|RTaJ0`%Uhv6%->YFbb9}T{yt+=Xj>RXwIxt zg)v{souEz0<}K&89}vwN{WOn-IIftM#F%dta^W`ldAUfT$*)1I7k_S7$$L1nb6V%X z@9v6T@*T04+nOw3;u{L^lOLn44EA4t91q_R>RG=c-oY?b2`gl)7p*8-ys{UZo;!V${9N zVt_JJBI<~>QVzbY<#y0Mc5q4%fI3#78$1)k9>Wqk>*Q5;fs$Ugs)oE1q&m3&Xa3q zU!3g`lVhcj>3rdB4{e12e$ku&=2o=3^-j${#6@f0zR$8AtyhW%^7GC$1sRNJpmf-G zaWD-u$2h>Ka-M-mhs7|V;=@{-Z%iU;Ch4d=0={x(W?g+$Z~Si207+cTAK7_+_K*!* zf8$;=2Ohls>wh!pRtfv>(*D2q*cH6?dCTXh{)0`@k-(V4MX4s6>3tJXIfG|?s!Mit zZ#VH6rTkBO8No(1sT&is!GqbvVvNUB=46{>N_ifGCje8()%1Laz3jv{=ZQ}TS5SpE z<7S0?O9ZXg&54>yD;l{}@eeX!!&aevRp>K6ewALX(EZR4$ce-JWFvrggi47{86;)u z14w_gV>?Ma`or5g#fRQL3t`KkngcH6KbBP!QPMH5z z5>Ns_x%GB(dME@0cp)(>U|uu}6{4U-8-G$d<9B4BQVNQ4;l!5-tDb`EbX+)@D#WV= zBLhVIBL{%~CS}1P0OYZPBG2f}eYR}V?E3xqjDgIXHDFls!|rl>7~S#1zy90jTH`i3 z_cy=yxw5SIWWDQ}QsFn9@*FJ%76yErH2lAM&+%9%sOP-!{fZ`>N#(I zuUg`@O{(uN_21QPRigan_o&wu?fZ*!Y<6~_GoyZ|N!)mMpZfPRy3_jiiGRg)4-{8|AH2DE+Ade{cs#_S#`*r(&e)b0HM zfOI@Sj06`=>}VAs7YK#D2S*WuZX{TAnovxcMn{!-t-NiiYm3|3+V~OX%2(v3eOS%A79%K)sBtlNsVLuLFb{$G{=P}Nrh07UW6 z1=#&xLL~oNHU$DnUfvD80+Bwbe}osi4A&PDMD(!>2#8XZ#KX3S+>{Y_ti*=1;!ykLMGB zyrC7%Y_yHIKN2l`%=q_?dH}MWm|BlH>gXZu+S}F*XzvIy5SjTmboB((*!Q$dB%bGe z1|V7f_{aa{Mu^@vK?05|6ZTtyv2tL~17P80YaM`Y0mkNU+P)l=@mI^*rD^^F4`4es z?+bC?IarSv75~S?GmwVZoRTL)dwkkvwfpAk2>88QLtF0;G3LTcgg)Rip8C z<{9YL>uoa${5E1@oq6V&OdUvyfQIc|f>^6@GSz3#FxE{t zo@!)e$xsQ-4F}6h!^b}1NpiMBFcGSyc*uU&eL4wvR?h(3>Td%6=&wO7Fkhy$&Byf^hyfV?h1 zdjeI%LxGx;f>~H7w1E^N84luPzosLQG1UD{1*U4cP*6&ovDfH%bJ<3L%jA{oe$xA{ z6#=Mse`oyY`&SCmzWGN0!0({hU*{$PKvLH|yzn1`EcrZYiNB@$1?(acVOtNtK%lZ9 z0Plb_5~&z5xXy(_=5{Zt`KN8d+xdX@W(%or^n_z*kI81oryV0B%CZ#VTT_M^f-Wg_{0CCnuwARAfDfk zjQJ*tzft0$@csx0;5ff~$Bl1&>s!MTXKzgVW&ptPKU-Pz&#Z|Xzt{gyk$=?qDe>ot z@$@iXu^|hRehxOeQ>O2$=B=H&IqaqwzX=7VJ5}>Qcmi+18+;Hi{`o|V7%r#Yahc>V?Ft8PBh z*mv_?eM~xVubc4~4*T@H2ilTs$!G#dW?(^A5I*(k#}JWFzjcUf1PtQszatZXgN=#0 z|GI%r8r}Vk1mD*Bo0Poi`nwCBI=cT#^#b^GA?For(CI>%H&cLe1i8P%M>-A#(};KnVm_qA zZ$J|7*3!Ze%#Rd!;tmcJsw-%?Z>4PY*yqKof>_L?pz$0B^QHymgr0 zY&7cPNAHid7VAaGK@U*s{>T2a|nE{RIIgrtvoF9Xh)m~$Yk>$)jFy&3W zQ&KQGt>K^+Hc6GslvXPbr|t#DBlHQcdi86>*zq9Vry?JJ;&6Ndu$U33404)*xz zK-WBk0LRka`QbOe@eMRK>3JSfI2kHP3pTLO7jI9xE&VB^k3-U;HD6_tNyA zQ0a=w-9(7?iu${-)z2nr(Z8w2&pd%n)oWn<&%_(N5=MV!r>K6?DXS|00y~vRsFEO{ zD+P7@FZ2NQAm$0+Z)+?wQ2WN;M2Cw;GX=VRm=y+*bfGa?Q$C!>LIKuyu(#359gJB{ z?d635kztT@x3#kho3>q>x%ZL&h2G$aUVDwgNI+<2gC;7}_4o7-{{8+ANj-*0IN-ts zF?jThGtLl<{Fu%_do~m24?}Ii9#0Gu_6%@*z#i}cJBROj*E@&#?7{6JP1t5E=1qBI z@yPuD2JQaicl^<<_oywc*p^>qB|>qb`~Nc5M_{AR-!eTtz2z5pnG&M|W{Ua&F#JD& zC-5J5lk>Y}c?kgEdhG=);Ala>g#PYgy8Nc(cC&Ae@7v2m|(X1qdi_U{SRatCj)~C~J*i^?-N+L>pkkg7w6N z14Jl96@gwzu#Zj-BDY=f5DyVH?99QoVc2TDS=zL#?OP1q7O{BeNc{f)W*B=yPl4b3bI1!&#C+u!j{gaQkC`p=$Wtbbtp zJi&N{Azh0Ld&Z>=?PC|_b`H<__BV%pF=7_k*s=M3uH8QK zqScbbUyX{t>QaEfS>WbgnN-EW;|hOn5+9HxMb;_Hr*w+)5Amk22LOB($Roml-5Lbu z1SimyA%P+Xu~zIN-T)5+>eYmX1hw`AGRjEG(jWB!JUf3&zxhtp~k6+)ohus0EgUj*Cw64p_AI+#k&z zWXydW6J%x@xg5Q&Ga^Ke8cRx&Pby=CT-d?$jS2V8|K9O z2uMNMfOB?u-v>N+xCcZW9|YaCK#A z_@du`@eq8Los8^p0`c9S76ds^_Rk&K?Prw(fR@;6<)3ZIr?UXCJ#4)71+c&G%Jh6y z{TiM6dvgK6R8|xT2*8KI1NdEl!093zfRNx?+}?qObc6(f`L4(g2rr-zoPb;e2;@Ja zT^SV`SRg2CYW!#QUmpz|{QwlC z_t3B|MTGI~MT?RGBkXZEd2;|T8`hS4-DdJO{7D>xjgjKWfEE#3;|< z0f5|Pt7drGr#>|_B%a96FD=gVl$`+tb@Vj;^f3e!%zgB63;aG|^h@?}^QP~>@WKnu zAJ&Kd`lt%ab^st9gCp91fBg3IVgk%y*agy1vz=zX{R9+=#6R|;)|?E zEkH{TKraMH+?c%qaru{Y%5SkNH`i)6?Ct9~z6!@D&16M=OBuiQ1UhM2;sd+@AK+t% zM?Qn&_u!ITE3yLM2|!Sk^HL;ajK3rR;DZ3~ElA6T*U6&0{{~49YA>MK@Kc7$y=vkh zUt*AYe$rBOdsm;d4%S-tgGxo-o{Y+;PKa9s8W&Gq)W(eAaW+@u|lgGkn@H&l*1M zna2#D@{DH=f9L7X7(V%FPai(M_GdJnGT<_?Y35k5Hhkd29x*)p zzV{0Uq$=Z&RsvEvaHJl<#s3vg^CfQV-iqzORme2^E^M;bVDr5kVfd$DYtNl2$~)=U z|CR*+r41;4vy)dZ!-xAbfWT>3S1T?+h5-fuqz8ceuqwGsIN*pu0R)oiH0{OK1LF^k zdn5JIei{f&c0gwlq?TYUi;O2rgT4cHH;>LOAOvHWKIEdJbpi-}D=0RIWX$4jc+|C@ z~x$H;!=q0fiC;inmVry%_5U zSnq``J$y@=Cby*V^GQPZ^!O0R;6uI=$B&9q#7-$Ik^~np*VOWQkQ!H z149C)MhBpbS6tuV9za9%0F6rvn!l^ZvbIWWIBBE-NidF#cP!>jMkwFlw<>o7O`HeN zYd4;}MQ`642K3rOfEH4(zDWlD^8-SQ{$SIi)>^s@(_RLYkq;V^MrRmCz#`JcIF$Z+ zH2|bya)h!naQm(P-<@NoQ#PIKOG@e)04#`|ABKM(?B^_m-Y3nJ)uFd!>>n=|E~>l1 zkWYZIzYFU^cmS7(rUYG;7MxFVkr5DtU@3tBAh4*fO91GWL@8L>2BSI^*lThB$&N4X zCrv+E;(yC0_&a+{2N2;M9NB0}fvwO;dkzgBf6l80LozUr8~k`4fD((nS%23b6>{u% zJM}T`(gM^0zc-+9%thI`e_wpYnGj0QryhjUrW?NN$v2IJGbJGp+-Ji~(ob`-f zkNj@b$nVS!_$1Y{O!E)hC=}~2iS@GnK0NNzI(hj49G|wO+f8n6=70R9)1B#N)q`Q^ zuYyrSB3ynKAMheU2*TJw2Er4_%Xusb0(hJR0PP7Z=@g(&1yWrA^#nW*06TuBj9&pj zCA$fQBw*#m|KfHNuPg9+2&~&SJ@CnQ--ZA z(Kaz5iG)285XJ$<$kd`|H2oXm%~Ye3ThhQJ0uH_Y2;lI;0s4BQ3VmkT5ER{OVPxSI zDbV8LA3E(vw*2l}y?^E={0vyzZ`u4)hRq_QwhNo>2VwZn$9mLEmiK>|yFc$7l$Upa z(Z3jO$!Bmp6Cdzgkrsr}?-C%;H6WlofrZ3{21_~%VDtb80c?{|A_?|D04p)Z&((mf z0FYT(aY!U)E%gAAxSxgqHWYA#V?UZz7-N|GveDkae(lP6i?@(&Lblwx!-wz5Je!n! zFFx4CN3ON=15P>U8y^RO_{i_D)p@LqS@76qc69SoF8_@bUirmaR{KFaOYxfjn(Y2W z>z|pOU%CHFy3ZeM2o--t^;X!|6S`S>w=FYCN&w)O3j$MVns>7DNu8`XbtW%AiVyjp zSeF9`uCrMI0R(vom7oNIc?|?wE^NJkRkK>iz?e6%5futq;;`8ml}d~$0mE|RxDwGL z1K0f)=Ux%fraXX%vMz31^@#4pb!&6fNJp9;H*O!~Lg1kq3HF?*ht$MUjkA2tLM$xc z_qDV%>&M5lhw=J|*$4fwvvNysMz0Yn^JGYf)AoJ)0I=%i{!P;l+VV?y%INeX#(Y)V zXY%S#U_1(jSl zw?d(LofTL%I)KzF0&C&{)a!{01#C#5@7st1f)@QmJp-RlsG3-vfUh?QT98r6@rdWH z8~Jf$7r!B8#@^dp2vDmjN0F|{_I=X2*X}9!_u0_K0*v8BNJCW(2n`?7%7>^JK#Ubc zjr2OABq8si=&BB59g?z^mg@0248FS`ZInVE8;S94sKOhoG$X73s5iPsI{lhPIRo2c zQ+N9j9Gd9z1N-?Y?BoY9{u9utw?{Pp2ZZsP%k-AAYLYa+D~$iK+6%Y<*M&&vDgeOs z0tB>1KyU-|I=3k^0bLsgRK^QnS^(T;Kwx)t1_Hj3dteX%M$@|Uk1aQDxE3HLJ5C$k z)p%_DUq`j}ZKRuf55Iv);B|OAdD9Qlr%d_iXFjOXGSV+$+)7&kU^XUEj-}L`n@zeT z1#=TjhJbbI@ubT1cBBx?ZhkZ4r2O{zn*WRw&}*4V_whLzs@tz7{^0hD z0y_-(5X^t|O-OzpDUxpon&H!u;qI2AI0VY1`_E+Mzhd_nUciOI3y=U%lmq~jNT{nc z0RRBY1+0>_rk6E-S}L4S!;cxbx7#Pma|n0-su^87;MD?T{!X}=&P9=;C2e57lCC|* z=k9sDM413V$~l0zGI2y5g#3-{fquLLQlu?;YiX`e zKxU)E4l2mHp3XNDtO- zSGPNS)wA~}9tdRQ@2rZjYP_8~DmxC;e;KPS=toDG1t8UyBT5Jx-gvd`S%XcHA*;>@Ja+y(dhA!;M%Qt5{G zM#z2O=#6fM5>EDQ18w)6>+h6jq3DEcC3+iZA>6W5#%jf%?ya#?6xzXiegnMeM{n7Z zrw8)R?}Y+MhA`mnSdRb@9D{%2YcTxlbv9s5XaPuZkkkV>VUP?51_}sju`~c!FJMg( zgT3|kUA;$1!F8<@XvrkRxq#NxVhbqHx`4ods-UeIW7#Pncs&im9tU0v^pt`QVd%3F znT+=1+k_nXf0-@!j|y37dVCH|>uhfWDDNL8Se2O9t=m%FY)Pp%7(jP@Ja!}Xi9;t7_IBua#vlociKrZTJxV0TI}ov**CXIskAVx`w5Kh@3CsOf zsk3&`qpziR?2Xhs2w*-ix!XdIi_#VV3UnK`%)M>V3L-@M0g{{%+N{{d)7;t8HN(_Hk`TiRr$K z+aH|Hs^8kOrRW|gqM!W&ftjSZGeF>IcmaQhf9PyY2yRyy0p$&7X|VAEG#4O`)DzIg zFVlhw00f&zmftGf0K)`2Qb>4sm0X}~r#;)(j@^=4TKlBjl zSdE48rrJ9|rF^INQ@`s00MvAnw%q-fwHLf7+vq8^EbO7W2%htsJ9+U69L03s?yb%6 z-39Z7Vf+sT2)q;j&{uFBAW-~|aBm0*h<{!n;F3HVZ$J@)D|!vQtbof7lz~pb8gOE( z0ky$an_P5vtLWT99}vb=CpLPZX?&NBy8Yqmk0uw5wA96%G4Epx0Q6&R*IFwlNwZdJ z{JMX=GyaT9TSG7yWVHhTH4TRV z+JJ(F1Fld=hXkZ)hy#Jju5~{}=qm?3V2BEVsE8;to`W-VtwrdqN5YA)VX3#v`#X0Q z2LYpEq+O0Sm0`$u5mH=*I0gWmda-E(4Q~AI~Kqw=dMZFtMdv> zoJ>jN(Gy@yT+F|k&?-O8f9c(Sx3C4V zF@iY%I&9=c$e`U@sO)de#&3HEOiOJ*egr&$$HMsE3?MikjSK`Qa3!(?AUp~q6`qfT z1S=*xAk%`rC&1E!wFd$u=5J99G7i9%8+#LrqcUVKBnS+3r0Z_G8^>kM`(k3EEV8Rd zL?*n?ffuUq6V`<@4gf}|C|#_-p18rTrg?>#1f_~tsK`amkHjqds#(@c0RjO48wx0y z>^fcoO=%?MFt%d((PCHos+gUQyLhAXrVw&>S5C2(NchNanHg zUkqC~7q;-BnJl~C)Kr$-+IN4J8l1}WgJJl;g@5o>h;6!VvR+{iH68bg4&P$ncjfUEvQ<7pe0^xTx)Wln2hKX zOrHTApKnl(B65O8o1ki*BdCyMZhag7;XQ_QZ6d>9m6X*iD{^r??Ur;+RVA43aQdXZ=nyPcg&Gb{Ak?wC1V6?%G2f^L=^|0-FF_E(?LeZ!p8jPdD1$AAh&za{e{T`KwwO!Tgs$#rhgH$j`zi zUg{P4Y`N`#KqpD2)BGXW{r_Sn&EJiG_CItOAPB*oreYAhKnMbw48&R$%X$I=Sxa3& zWb^{1@&=4okj8rquG`%nAw(J$9Jw%}rn^233wj$IJk0KFs}e`B=`QtX_=zl5r%-v| zQ}tSbC)kl<9*;L`G3t5n5XycZ8I5rR0I2;Q_Btd`0AM@->jji*)_rEe08Stp5a34E zF#W(fZR*N25F~!j$i!XJ8EUO%F)+x8w!tea7wJwv&ZoUBS8Z<#Q{G-$=3g1U@^(dY z&+A|VU%}3QGVI}n(@ApZZO!l*A25?-hr;-`LD&38@C5!I|NM5W>wtbcolvNl_dvj@ z1dD1w`Uec;7wpv!*l<9V6|c568x1N9C6`dIC-D+wlK1943Nk7v^1=zv0|^S1MqUp+ zg%D8eb9!MK)10&;il9`?&)%1YJTXGm%y1;JE%yLqk4HNTvbNQ23IwX9gn0t24c01k zpR*Q2rCtF2F52Anm>4IkgBg7GwOeC-?jbq=#PhuKYn4sIHi|!K{PKJj=k_ig`)|j& z|50p;&w_V+nBM(wvloN{Qz?x90r+R1jeq(ipx#g60bGRzE}+m000on_&-ar)v0J8?E{ZVS-ma!3J>vS5C_5y6pL3Gt#F;dGSpt8og8+$u_5r1JG#D3?hiSIgcz|eVU8Jp(o)fw zP{LA_Mc2)Gm^Gk}Az2KaeaLv2GD1I56FxwK05Jmb9o3R-O$gaVnSbNz>&{QhE+bGG zAXsJ~Kn;n61)Lac)>?OIp3NX4rH1$2Qv{@$M?tuTW{r7QJj+Ak{hH?Yb*+j~`Pp-C zg%tdVa~Qk7Jf{We{Uw~=?|>Klf-Py5Z}}Cv7?kV6_z%Mua5UC&aD!fnFXRWJUI1Ux zbutvlGysTt0V5ACR0v=J!1NN(U%bZ=tR!xpdO#A|;v(ub5D@T}GKM;Es}n|?g+`P* zjl!6+TMHWk`UFY@g ze+|}Y;K2U(bXwf;SHbO%wiE{j8nP83KSU_)*3$ zD*S$i^+TNFr=V8mLAP$L|4sJ-rUfa8)W@A57k&slg13Mh_zZUcFJoPT&_H_ug{v3P z>cGT>1OxzRGZFRWr|u+l08zPyBaTe0R%7)BsTCL1ta}MS`)-2fZ6yGY(Dg|{ZzBj2o0GzMt+!dtXH?XmNLc0EG z{-E2E((fkm0b4q0gUrA^0RXQDFYr&ee!3J1Wx2=-%n1;{m$v8;f+{PZm4dpn5NRkv z)K*$Zl&T-aWe`}nAPGiObx8ODOAM}4wV`(FIpJ3}>wY8Cgm?nUs1C<%gdeYJ+)$q* z$Rvz*;cKrFQt=6+>Lr{;!UF)upQsD~GX$r*SdEBZ;;X8hJeGh!LS(ZvOOUwZYU-_o zl-%;PTyHfYp+v%8ivdr<_#p)(Nto7KWg#RBC81L(O}ZNAqbzbO7I6LwwcjT@dHxn; zz@AQa{$FVTFd)!DW}t^u$cq31ezNS6&Q1(J;t5P8=~fg%+#7trqwz((6(H~}9Djt=;1%!&_H;#BFw>rZ^#%%EI$Zk8 z8o!ki6XWj|^ca$WO!ZPN}kw|KEH`m8w?!m3v|ISLp88ADIzZuC#dcR5vLn z#y^S}Op*pW|J~pPyh88+0E4gLsKbF=XanXY5LlI9A$bAYRfw!awOX=3Kr#s{j-d^@ z5gYf`?>pe`cRQVT-FXJ7d5&|L9Y8q6VLZk!+3W+BPNZa{=P7KEPRV{MZ-Ww+K_;N~ zH3NY+K#Q%M_(!QQS?rsJhQFGyoH+3WD%}uMs|L-CNHZ*!?eY$wS5sefqdwOvdkwQ= zyWKkg07n={1!c;sDI=L8Z{Zv=?!Pg9LHaGomR_0AxfHL*dHnS0Znu~i|7OC0G~WuN ze=gShktR7EU*h**{MTqsK!yXI%##XxJAVKMr4ADc=MC8AqqO&+{=wysYP0}l9XEJt z;H-18-Kphs$@2{`tkV!ecmZBUVZ*!z1qi|=Y?$YG9gsGBkf4uiSpi|mJ%gI1|I*Ur z`mOxQ-5-Jc12#r8Kr@R10 z3`PM!8~&2x3)CnwUr?k5l2iuGy~T~bOHYhocW4ZNthaVB;;y_cTXsNTiN6hAqfj%l zPCni&KN~^91P3fN0~zOKaF^e&iQr*TWH+n=Ed^0xdSJkXTC0JPa|IpUt=C#Sj6q=F z;d56>CFvj;p9)@$?)pK0O11O5Y?RpfMP+Mssce6jqXCZC`IG$MU&UPiO#^`>DKU2V z@R>Ay1hN7UOl0Q*N&g3N)ONuKbSwZ!#PvadtweJKN#RmYLQ9bN(qaVyZRwpgJZ6mo zMAvx>tKrTObsb&(fmfxd4Uc62sC%DyX+XO)0C+kA3Lf@x{Jwf47A{cEBGs9V)Tkw$ z%fHIFfQT318+Hqc#zTleNE0*Iii8sN$`}yv_=pe$Ol7Hmbd0`voGw3?$4VN%JRj6K zq>W#?{g>hR9h{qI;2eGBOsCVIu>C)GdNAq1`0s`FG62BYNT2)=0Ps_A1iP>h2ILDi zJs5fdIx|2TiWH0o7$`xJIWb(Rv0SBDZG?|IapmKRuv3wK4RZ`T8C5idXh>)fmk0YD zkKOLCi9{EEY(&9=00GxA)Ehy{l6xs*Z@yDRrBDrT%JTB8w!5V35I_qYKu zXl zjFl^XlM#2au(|^a0@|q8aeKWjM2H)AGs^G{!@f=onz{CR?Ms5#y;+a(3M@RR*y8#G z4j6vAlP(Hul>PSI{J27{Qt`45B|Mkh0^ID&d`e6C2a3(G9yd+4+CLATZrcZx24;Q6N-) z51zm`(Kih>1GxwXu2Fpi1TT=!cL+JCz(Aci9RUgk7}SYCya0ymB!n<(L+kRb5srYJ zEHUHv?@ME#Q3h%^0(DM|k+UobyxY5S|pluf)gkDY+1p&{% z=~_+9Dy_tO*$TS!@0gh1&v5}4`Z)l>SMkOF3{_RvcGKc|lO_}% zfkbA20haIzjAx*LfPsMF0xZJ2ldGtfJuhJ`p}Rv_vDA@^^Q*N{WFk#7cE^MRDGv!~ z+E9^m{tixd^0duEAUzLkdNRK2+v_Bx84>F@Q4%$+7$~l4V;^|my*0>$@BV&fhlT=r z=O;=~+c1V2tpSsg&6%LFyl42UMpE7_BcDY{dxhtTMmD0MNB#wzlP963<=zv+zqy4% zU{zBA5XJO!b^3$V@v2wWzz1L!Ak1+HI-A4g~gu2*1?&#PL3e31}_ORVjeeE${X-Nprg3~{IDxFwo}(h%;-czBwaZ0j4tYBxIqKf-$f0%zez5wix)!#W@7!XMA5a^mYm%CIX*!hD{I zho~1&`XrIR0@84WWD-{VZbeB^vmJ&4fp=ssbvK$;vkf*FtDdsnhs|n>7qHf?HU$7Y z?N>Gt^3V5fDr59s%&$Y6!rj)uF^I+f33J11YW!BGL3aFAN67J}8?FGyOXtc}u5th< z8E?aXBM$)Z`-`x#BST8zRpwXW-1-5|yZ6BDe=Z7p4kE@s0f1W+1TcFLp1?yv4n6~J z*m3w#-h?c{zu_oSG3USwxJv(MyDTSQ_y8}abg^dPmIjH0#-7&b%az0tiRxI=S%&f? z75ac6{^g8ws3MHimlL@vn~<~`2|HZ;UPOtzH~>_2Bn8-z*Q?rf5H6GOtOVcJaEGt^`_JqMN2VU%N!+?Z^1bCb(x7C*DF-nF|g*Ri*xDOL6$%KlevnGBL0 z(Zpa;twypT#Mr|uKtkeg4p3i^BM! zH^I3iQha}n4d=zE=)3DA$A3Vz8c(w=X?hrb9O$mIcL1F~IIRRjf)}_#f`IHRknir; z<|49ObE)Gr1ABFNu&DI}gfFmFhf5kPh@qFUG}DT&Bj^U?7i~&n39B`GbS|mc;Rj|q zl8t|Din2YnXY4v>@VyYAStrK`S?Gg_BF?Qpm`aknOm_YQw17zb zI2ft?M395WffslU0N~^J@xB5O_`U{$>oo|-Vj^8yEE=@4Pgv>-zdf`u*v0mB;< z)N@$z(o}SqsX6He5x3W#oPXHzFRE<}9_vw^j!o051hG&W0@eRpadYXe$xcbKBUzMJM zi1(%2pYOr>b1po}e~00J9Qu%`?v7py%Os$@k5G`;5=vrE|ofgEXNcg@gvJL;SLodU^0;u zcjQJyG;jM3DBfT}3qVNmkv!4%I+>~lfn2AL;@N`ueNJ?;>enDRqhMi!2_sg0;2`x3 zE=DgbVaLG12L(1`!MN>i2VbG-(S&O#21G4M$vL0kTXnlJ-<5q2APSTo2#N2xBaPe= zw+@W+BEsD$5>Oky@+RbK6wAuWokLi{?AmBA>N-VCOQ6F84yf$It4)BXoLeV z$MKB-g3see{Wg{e2Yv!D5Z-_gbU-vDgdOFzDnyVCNA~D&V2@N4<`fX}1>zkn>K0`S z-L?iLd0T3+{*;vS^ZFF{21E_G;^e#@a2mtUo#A)fa$y<$4RV#q>8RZPvi$v&H+VfZgUe;O znC4%``SQ-`PWO&F-k-Srx8jmw5e_^ZKkEOJx!WxNFpT#bGfFG)Go2~8N&$fc08whZ zTNfDZ)}g{4lSR;8!Mp~DImsWWVW#uC6U;)u8|0;5hY-^mtRz&i7FH$>TuC!CZfeAg zTOckvHAwnev?V%e*iq4mHk5FTWI*WcdO+g$wltg(TdmOlS!W1LDvrp3J)$+&;F&9R z9?RC`77Uj3-R>;FC~`R;}dKbbK7w-F#fFE*4E zqs3U3pD4$Yr;u<8F-MkPg!QwIA_%WW+;^SsJ*W*|@B{fSQ(vG|g~c8M2zy8+q6Psg zHqzMyDIZ-_lVPIzgC*_}I30nWDd#mZWtw+i&+Y)A@HGiecW=T6Hd-w@OvM9}a6DTq zCF+x(gD~_dJ(9GHk)Rv^SgKavmN9?sYCH(wQZ7K>FZi29MgB^|m$5!P!Ao%bI?jpH z;VHghY9>FF?D!LL&uzvV=yWRlz(?VSejV0F1PDOJ`85E*cf||>=?#eTVkI2XMMcv1 zcd4)-*QBBE9TdAM2v9FUc?5<(C>Nayk)$6rahR}@RnoB3y1>=4r*>=EWh>JBqFV7P zN z!UC+%&xlzBS$?I+{c7ES0D_$^TVNJ$Yj2=yy?}C0N2d}EZ$OqPAXy+DfzuUOvWlh{ z9T+5Dy}P%o^Fo&xTh4Rj`f+toP}Aka8!c`cbi>L$UaN0iYy)V~a7Th(CoepH#1~GL z0cX%LJHFLo#N&Qf0&d*>T%_|B#cze*_cG=2WgvrdA?o=8e*aIZc5|JrB7bxXgrt+p z0dXnPWG2ZU0FU4p2t57>Cj&+`WgqHD@lbK8>Ofp*5Rj?CU78!%sY8UFjwc|Y09ry3 z;?RZ%9n~~qo`wcCbHKv_Uh>N7*~8+am^)4kdS%2!W$F>ExKN`W_^f`H2yncK%%%?> zA>Q==qmJ6(mEm>*kaMeJim`I@-lazvioX|uOgr59OAP{c=htz*sM*gigok$)Zl9?Q z|LyqQQOMJ+CkF8=sUC!{mlyr%G<_yggeT*KI0YxkzhGUUh(THMX)*(9$^3A>qpPIF zyKD%MGCn|N5xSfokOHH`;|HANSY-~92nd8(1Lg^=___HA2ha)_emOJ09u+W=IKNyW z3Q7uQzD~lDa2+C~k}Av*@@o3IJ2uoPtQTQD3FZNmiyf89GjYBE>g#k1IpN)X3BU7a z-~qmRs*?zJKbfTWCQvYg6XG}F5da9XKgP-OZmfUCZt)*7Q;=(dP^Jm<>vVr3^fkLuEdV^*;Q* zuf^}YtM+13 z1$T9=($E2bop4N*gII}^8QNoH~4JV7*f1OWV(j2$tQL~#Sg8;})-I$K~e1(q<>M+pSGH5V|) zhMTHoc775PXcwA$0WlAttt%kKo@&CMJziD*EX-_kZjq;f&307=!N=Hf6^_ zpzyA%y8D(g?TTmzsAr*!UkkK$;WrZszKh_=odcu)42&KmZ~8d&lRS8`<4-R81q9Qw zo*@5Acm!XV&Scv$As)i8()?_M1s51eu`Vz|NKo!jq@g&Lf;g1qAx=;k4s@*-AUy%O zM8T0x7cMe`ayzUUd}<0?Xp_rm0a7xu-XKWNQ6g@$VTXAP12QVe>1Z9k5kPgAFsJad z1~xhxl0K@-RbdmmJbm_X?M8)^ZF{0Z9 z5KMz6M5D)t$y8yQzYE6yH#o`OhvVk}0zcKG%C1uUK&e9mlR`9rAiaT|0th-pIp|V` z*(OClt1UPZ8Q=m{?FN%x| zFJMZKb$GBlk$^ykngnUV${%^|rw^S`hCBF5cf3bnjBC*+Bs;BLlPmyeZ$=vTf~kq< zu(s^9~zwnSit|Xbs z;A#pWcqi6bAPB{Pk{^mvV|WDrGn1D;!MX$|EJPMy{(nHW?3Tm-M1?fNCy*RT%zdx)1+3`0l?hYdn(dw`aQDW}@Po zTyB|5I-OzHc@$1Ucm#zA3qA@j;Nv(sKZO(Z3>f~`0RZ2dQFG!j%(T2p0fFp0fa$R^ zJlNHBDZ_cP^X_dMaqjK93j_p=K{^-Sw93}gbGjvs=`EQ?0cAlKG||SXJY}gsff%&o zGE5HzQaWv#tr!nsZ-;_NVT*OBs&7$OTF&W`+#lh){SJ=b03rKj-1l!-|A_nEgyYM= z(>!2msy}h{CzoH2caWrM4j}k#oXBs$LZxB(0US^3X61k2_%(P1=VDzlBWA`4Zy=W- zaE*>%O<1r?5rpM@ml`oAbSlKi=NVs zy61Ms*Pngf8os!$`3~KJZJ|p5fa(QEG0&m~5RuN?Eyuxwcon_(u}%jF{FBgVfcSeA zZr^81U3Vut{p50Bga^H*i<9*sIB8#n^HZXG6FBX)im`)2_LpTO?_clf?ug8LqV-jDgj z&7WKjBmi)inVG4Xvi^0P;E%_y|8ktLC*mkVgb(1PJ`MK+5K7E2D9;g~(2;yWDM&7ZVBq zuCL;}HhjJEIU|kkx(dskI%9IR5$m9iM@=lgL3HRRtEnX(+yvgO( zlkS`;iUP)RKb+)$fD`!7VC3(>?bETo)KMJ4_i>V6AU%Vkyb`+ke48geM?Q{tI9fmv)Lu@dLs@iVC5DNFSEh%MhUeAQaVZ?Osz? zpKt@E?MTFmi`?He2vkcW0KgR75JPud07>iFoxkH^{XM#00x6p|JH6CNT-6aqpBI`S0*5v#;fDM201KnaN#olzMEba$mKLJMb zHUPkx0EO?vm<4GlzyM2z2p1ug5GlngaNMr53Bo(LLN28{xJoaCO_cK&&kN!*lF*E-Ts*{sy~8(y%PrZuQ0$b3J(G6 z8)Cs&zXkH}Y+U~y0O1E%KNPF0jz7Zv=j(;cLwO-U;^GrwzjgS_rQW)O7up>Vm#{4`O=PPi0yO1Np;J=6+UVy?`aOuB;<98J(l;6Ys-^G3U zb_Ig2{2v(oB^?C>KU0QZ{a6A=QGE}f@=e?aC0_AqtdHS+PQm-W76jh$xQ-6X#XVr` znU|`Y`nfa|lgk7ECYPJ~8=-=ErmSGJzX?y_Xn?{Axc*YCKc3D@k$Lzl5P2w>6`F+d z12E#xV0~ItHv%{W`S>2(|0aOM>u~*Lxc+Py`H>iheGr5==@vH?*Z<9@eog?GoHxJ3 a%l`)&}_27C~?G|EMR1Y-@FXwXDnG#1*Bh=Lll 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 a%l`)&^dYUUoqYL_W*!~=Df95p&&;5GS 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 literal 0 HcmV?d00001 diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs new file mode 100644 index 0000000..b0d50e9 --- /dev/null +++ b/src-tauri/src/commands.rs @@ -0,0 +1,518 @@ +//! The Tauri command surface. +//! +//! This is the only boundary the UI can call across, so it is kept deliberately +//! small. Commands orchestrate; they contain no HTTP, no URI building and no +//! persistence rules of their own — those live in `rfm-core` where they are +//! unit-tested. +//! +//! Untrusted API text never reaches an OS call here. The two commands that hand +//! a URL to the operating system ([`join_server`] and [`open_server_page`]) both +//! build it from a validated [`rfm_core::ServerId`], never from an API-supplied +//! URL field. + +use serde::Serialize; +use tauri::State; +use tauri_plugin_opener::OpenerExt; + +use rfm_core::api::models::{ + DatasetMeta, FleetNow, LauncherStatus, PingSite, ServerDetail, ServerHistory, ServerSummary, +}; +use rfm_core::api::{ApiError, HistoryRange, ServerQuery}; +use rfm_core::launch::{steam_join_uri, steam_launch_uri, LaunchError}; +use rfm_core::modscan::FilesystemInventory; +use rfm_core::ping::{self, PingResult}; +use rfm_core::presence::Activity; +use rfm_core::readiness::{self, ReadinessReport, Unavailable}; +use rfm_core::store::{now_unix, LaunchRecord, LocalState, RecentServer, Settings, StoreError}; +use rfm_core::ServerId; +use std::collections::HashMap; + +use crate::state::AppState; + +/// An error the UI can branch on. +/// +/// `kind` is a stable slug; `message` is display text. Nothing here leaks a file +/// path or an internal type name. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CommandError { + pub kind: String, + pub message: String, + /// Set for rate limiting, so the UI can say how long to wait. + #[serde(skip_serializing_if = "Option::is_none")] + pub retry_after_secs: Option, +} + +impl From for CommandError { + fn from(err: ApiError) -> Self { + let retry_after_secs = match &err { + ApiError::RateLimited { retry_after } => *retry_after, + _ => None, + }; + Self { + kind: err.kind().to_string(), + message: err.to_string(), + retry_after_secs, + } + } +} + +impl From for CommandError { + fn from(err: LaunchError) -> Self { + Self { + kind: "invalid_server_id".into(), + message: err.to_string(), + retry_after_secs: None, + } + } +} + +impl From for CommandError { + fn from(err: StoreError) -> Self { + Self { + kind: "storage".into(), + // Deliberately not including the path: it names the user's profile. + message: "Could not save launcher settings.".into(), + retry_after_secs: None, + } + .tap_log(&err) + } +} + +impl CommandError { + /// Logs the underlying cause to stderr while returning the sanitised error. + fn tap_log(self, cause: &dyn std::error::Error) -> Self { + eprintln!("[reforgermods-launcher] {}: {cause}", self.kind); + self + } + + fn other(kind: &str, message: impl Into) -> Self { + Self { + kind: kind.into(), + message: message.into(), + retry_after_secs: None, + } + } +} + +type CommandResult = Result; + +/// One page of the server browser. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ServerListResult { + pub servers: Vec, + /// Total servers the API matched, before client-side filtering. + pub total_servers: u32, + pub total_pages: u32, + pub current_page: u32, + /// How many of the fetched servers the client-side filters removed, so the + /// UI can say so instead of silently showing fewer rows. + pub filtered_out: u32, + pub dataset: DatasetMeta, +} + +/// Diagnostic view of the install identity, for debug mode. +#[derive(Debug, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InstallIdentity { + pub id: String, + /// True when the identifier survives a reinstall. + pub machine_derived: bool, +} + +/// Everything the UI needs from local storage in one call at startup. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LocalStateDto { + pub settings: Settings, + pub favorites: Vec, + pub recents: Vec, + pub history: Vec, +} + +impl From<&LocalState> for LocalStateDto { + fn from(s: &LocalState) -> Self { + Self { + settings: s.settings.clone(), + favorites: s.favorites.clone(), + recents: s.recents.clone(), + history: s.history.clone(), + } + } +} + +/// `GET /v2/servers` with the browser's current filters. +#[tauri::command] +pub async fn list_servers( + state: State<'_, AppState>, + query: ServerQuery, +) -> CommandResult { + let page = state.client().servers(&query).await?; + state.remember_dataset(page.dataset.clone()); + state.publish_presence(Activity::Browsing { + online_servers: page.dataset.online_server_count, + }); + + let fetched = page.data.len() as u32; + let servers: Vec = page + .data + .into_iter() + .filter(|s| query.retains(s.players)) + .collect(); + + Ok(ServerListResult { + filtered_out: fetched - servers.len() as u32, + servers, + total_servers: page.meta.total_servers, + total_pages: page.meta.total_pages, + current_page: page.meta.current_page, + dataset: page.dataset, + }) +} + +/// `GET /v2/servers/{id}`, and record the server in recents. +#[tauri::command] +pub async fn get_server(state: State<'_, AppState>, id: String) -> CommandResult { + let id = ServerId::parse(&id)?; + let response = state.client().server(&id).await?; + state.remember_dataset(response.dataset.clone()); + + let name = response.server.summary.name.clone(); + state.publish_presence(Activity::Viewing { + server_name: &name, + players: response.server.summary.players, + max_players: response.server.summary.max_players, + }); + // A failure to persist a recents entry must not fail the lookup the user asked for. + let _ = state + .store + .write(|s| s.touch_recent(id.as_str(), &name, now_unix())); + + Ok(response.server) +} + +/// `GET /v2/servers/{id}/history` for one range. +#[tauri::command] +pub async fn get_server_history( + state: State<'_, AppState>, + id: String, + range: HistoryRange, +) -> CommandResult { + let id = ServerId::parse(&id)?; + Ok(state.client().server_history(&id, range).await?) +} + +/// Compares a server's required mods against the local Arma Reforger addon store. +/// +/// Reporting only. There is no verified way to install Reforger Workshop content +/// from outside the game, so this command has no counterpart that acts on the +/// result — see `docs/mod-readiness.md`. +/// +/// The server detail comes from the client's cache in the common case, because +/// the inspector has just fetched it, so this costs a directory scan and no HTTP. +#[tauri::command] +pub async fn get_mod_readiness( + state: State<'_, AppState>, + id: String, +) -> CommandResult { + let id = ServerId::parse(&id)?; + let detail = state.client().server(&id).await?; + let mods = detail.server.mods; + + // Reading a few hundred small files is blocking IO; keep it off the async + // runtime's worker threads. + tokio::task::spawn_blocking(move || match FilesystemInventory::discover() { + Ok(inventory) => readiness::report(&mods, &inventory), + Err(_) => ReadinessReport::unavailable(Unavailable::NoAddonsDirectory), + }) + .await + .map_err(|err| { + CommandError::other("scan_failed", "Could not check locally installed mods.").tap_log(&err) + }) +} + +/// Measures round-trip latency to a server by ICMP echo. +/// +/// The address comes from the API and is validated by +/// [`rfm_core::ping::parse_target`] before a packet is sent: literal public IPs +/// only, so remote data cannot aim the launcher at a hostname of its choosing +/// or at the user's own network. +/// +/// Never errors on an unreachable host — it returns a result that states why +/// there is no figure, because "no reply" is information, not a failure. +#[tauri::command] +pub async fn ping_server(state: State<'_, AppState>, id: String) -> CommandResult { + let id = ServerId::parse(&id)?; + // Served from cache in the common case: the inspector just fetched it. + let detail = state.client().server(&id).await?; + Ok(ping::measure( + detail.server.host.as_deref(), + ping::DEFAULT_ATTEMPTS, + ping::DEFAULT_TIMEOUT, + ) + .await) +} + +/// Measures latency to every published ping site, keyed by site id. +/// +/// This is what makes a ping column in the browser possible. `/v2/servers` does +/// not publish a per-server address — only `/v2/servers/{id}` does — so pinging +/// a 100-row page would cost 100 detail requests against a 60/minute limit. +/// Every row does carry `pingSiteId`, and there are only eight sites, so eight +/// probes cover the whole list. +/// +/// The figure is therefore latency to the server's *region*, not to the server. +/// Measured against real servers the two agree within roughly 20 ms, which is +/// enough to sort by and is what the game's own browser shows. The inspector +/// still measures the selected server exactly. +/// +/// Sites are probed concurrently; one unreachable site cannot hold up the rest. +#[tauri::command] +pub async fn ping_regions( + state: State<'_, AppState>, +) -> CommandResult> { + let sites = state.client().ping_sites().await?.sites; + + let probes: Vec<_> = sites + .into_iter() + .filter_map(|site| { + let address = site.address?; + Some(tokio::spawn(async move { + let result = + ping::measure_address(&address, ping::DEFAULT_ATTEMPTS, ping::DEFAULT_TIMEOUT) + .await; + (site.id, result) + })) + }) + .collect(); + + let mut out = HashMap::with_capacity(probes.len()); + for probe in probes { + if let Ok((id, result)) = probe.await { + out.insert(id, result); + } + } + Ok(out) +} + +/// Most servers resolvable in one call. Each costs a request, and the public +/// limit is 60/minute; favorites and recents are both bounded below this. +const MAX_RESOLVE: usize = 40; +/// Requests in flight while resolving. Enough to feel instant, low enough not +/// to burst the rate limiter. +const RESOLVE_CONCURRENCY: usize = 4; + +/// Resolves a list of server ids to current summaries. +/// +/// Backs the favorites and recents views, which hold ids rather than servers and +/// so cannot render a live table from local state alone. Results keep the input +/// order; ids that no longer resolve are dropped rather than failing the call, +/// because a favorite disappearing from the index is normal and the rest of the +/// list is still useful. +/// +/// `/v2/servers` cannot filter by id, so this is one detail request per server. +/// That is affordable only because both lists are small — hence [`MAX_RESOLVE`]. +#[tauri::command] +pub async fn get_servers( + state: State<'_, AppState>, + ids: Vec, +) -> CommandResult> { + let wanted: Vec = ids + .iter() + .filter_map(|id| ServerId::parse(id).ok()) + .take(MAX_RESOLVE) + .collect(); + + let limit = std::sync::Arc::new(tokio::sync::Semaphore::new(RESOLVE_CONCURRENCY)); + let mut tasks = Vec::with_capacity(wanted.len()); + for id in wanted { + let client = state.client(); + let limit = limit.clone(); + tasks.push(tokio::spawn(async move { + let _permit = limit.acquire().await.ok()?; + client.server(&id).await.ok().map(|r| r.server.summary) + })); + } + + let mut out = Vec::with_capacity(tasks.len()); + for task in tasks { + if let Ok(Some(summary)) = task.await { + out.push(summary); + } + } + Ok(out) +} + +/// Players online across the whole fleet, for the header. +#[tauri::command] +pub async fn get_fleet_now(state: State<'_, AppState>) -> CommandResult { + Ok(state.client().fleet_now().await?.now) +} + +/// Ping sites, used to label regions in the filter bar. +#[tauri::command] +pub async fn list_ping_sites(state: State<'_, AppState>) -> CommandResult> { + Ok(state.client().ping_sites().await?.sites) +} + +/// The compact API status for the header. Never fails; an unreachable API +/// produces an offline reading. +#[tauri::command] +pub async fn get_status(state: State<'_, AppState>) -> CommandResult { + let hint = state.last_dataset(); + // `launcher_status` absorbs every failure into an offline/unknown reading, + // so this is infallible; the Result is only Tauri's async command contract. + Ok(state.client().launcher_status(hint).await) +} + +/// Joins a server directly through Steam. +/// +/// The id is validated first, so the URI handed to the OS contains nothing but +/// hex digits and dashes. `name` is only ever written to the local history file. +#[tauri::command] +pub async fn join_server( + app: tauri::AppHandle, + state: State<'_, AppState>, + id: String, + name: String, +) -> CommandResult { + let id = ServerId::parse(&id)?; + + // steam://run//// — the empty segment marks the payload as a + // launch argument. See `rfm_core::launch` for why the single-slash form + // starts the game without joining. + let uri = steam_join_uri(&id); + state.publish_presence(Activity::Joining { server_name: &name }); + let result = app.opener().open_url(&uri, None::<&str>); + + let succeeded = result.is_ok(); + let _ = state + .store + .write(|s| s.record_launch(id.as_str(), &name, succeeded, now_unix())); + + match result { + Ok(()) => Ok(uri), + Err(err) => Err(CommandError::other( + "launch_failed", + "Could not hand the join request to Steam. Is Steam installed?", + ) + .tap_log(&err)), + } +} + +/// Starts Arma Reforger without joining a server. +#[tauri::command] +pub async fn launch_game(app: tauri::AppHandle) -> CommandResult<()> { + app.opener() + .open_url(steam_launch_uri(), None::<&str>) + .map_err(|err| { + CommandError::other( + "launch_failed", + "Could not start Arma Reforger through Steam.", + ) + .tap_log(&err) + }) +} + +/// Opens a server's page on reforgermods.net in the default browser. +/// +/// The URL is built from the validated id rather than from the API's `modURL` +/// field, so a hostile API response cannot choose the destination. +#[tauri::command] +pub async fn open_server_page(app: tauri::AppHandle, id: String) -> CommandResult<()> { + let id = ServerId::parse(&id)?; + let url = format!("https://reforgermods.net/servers/{id}"); + app.opener().open_url(&url, None::<&str>).map_err(|err| { + CommandError::other("open_failed", "Could not open the reforgermods.net page.") + .tap_log(&err) + }) +} + +/// Opens a Discord invite in the default browser. +/// +/// Takes the invite **code**, never a URL: the code is re-validated here and the +/// URL is rebuilt, so text in a server name cannot choose the destination even +/// if the UI's own extraction were wrong. +#[tauri::command] +pub async fn open_discord_invite(app: tauri::AppHandle, code: String) -> CommandResult<()> { + let valid = (2..=64).contains(&code.len()) + && code + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-'); + if !valid { + return Err(CommandError::other( + "invalid_invite", + "Not a valid Discord invite.", + )); + } + let url = format!("https://discord.gg/{code}"); + app.opener() + .open_url(&url, None::<&str>) + .map_err(|err| CommandError::other("open_failed", "Could not open Discord.").tap_log(&err)) +} + +/// Settings, favorites, recents and history, read once at startup. +#[tauri::command] +pub fn get_local_state(state: State<'_, AppState>) -> LocalStateDto { + state.store.read(|state| LocalStateDto::from(state)) +} + +/// Adds or removes a favorite. Returns the new state. +#[tauri::command] +pub fn toggle_favorite(state: State<'_, AppState>, id: String) -> CommandResult { + state + .store + .write(|s| s.toggle_favorite(&id))? + .ok_or_else(|| CommandError::other("invalid_server_id", "Not a valid server id.")) +} + +/// Replaces application settings, re-pointing the API client when the base URL +/// changed. Returns the settings as stored, after clamping. +#[tauri::command] +pub fn update_settings(state: State<'_, AppState>, settings: Settings) -> CommandResult { + let stored = state.store.write(|s| { + s.settings = settings; + s.settings.sanitise(); + s.settings.clone() + })?; + // Applies a presence opt-out immediately rather than at the next refresh. + if !stored.discord_presence { + state.publish_presence(Activity::Browsing { online_servers: 0 }); + } + Ok(stored) +} + +/// Diagnostic view of the installation identity. Debug mode only. +/// +/// Returns an empty id outside debug mode so the value is not exposed to the +/// UI — and therefore not to anything reading the window — in normal use. +#[tauri::command] +pub fn get_install_identity(state: State<'_, AppState>) -> InstallIdentity { + if !crate::debug_mode() { + return InstallIdentity::default(); + } + InstallIdentity { + id: state.store.install_id(), + machine_derived: rfm_core::identity::is_machine_derived(), + } +} + +/// Whether this session is running in debug mode. +/// +/// Gates developer-facing detail in the UI, such as the install identifier. +/// Enabled with `RFM_DEBUG=1`. +#[tauri::command] +pub fn is_debug_mode() -> bool { + crate::debug_mode() +} + +/// Clears recents and launch history. Favorites are left alone. +#[tauri::command] +pub fn clear_history(state: State<'_, AppState>) -> CommandResult<()> { + state.store.write(|s| { + s.recents.clear(); + s.history.clear(); + })?; + Ok(()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..600ccb0 --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,108 @@ +//! reforgermods.net launcher: the Tauri application shell. +//! +//! This crate is intentionally thin. It owns the window, the command surface and +//! the wiring between them; all logic lives in `rfm-core`, which has no Tauri +//! dependency and is tested directly. + +mod commands; +mod presence; +mod state; +#[cfg(target_os = "windows")] +mod update; + +use rfm_core::store::Store; +use state::AppState; +use tauri::Manager as _; + +/// True when `RFM_DEBUG` is set to something truthy. +/// +/// Gates developer-facing detail; nothing about product behaviour depends on it. +pub fn debug_mode() -> bool { + std::env::var("RFM_DEBUG") + .map(|v| matches!(v.trim(), "1" | "true" | "yes")) + .unwrap_or(false) +} + +/// File the launcher persists settings, favorites, recents and history to. +const STATE_FILE: &str = "launcher-state.json"; + +/// Builds and runs the application. +pub fn run() { + #[allow(unused_mut)] + let mut builder = tauri::Builder::default() + // Used from Rust only, to hand steam:// and https:// URIs to the OS. + // The UI is not granted permission to call it. + .plugin(tauri_plugin_opener::init()); + + // Windows only; on Linux the distro package owns updates. See update.rs. + #[cfg(target_os = "windows")] + { + builder = builder + .plugin(tauri_plugin_updater::Builder::new().build()) + .manage(update::PendingUpdate::default()); + } + + let app = builder + .setup(|app| { + let store = Store::load(state_file_path(app.handle())); + let state = AppState::new(store).map_err(|e| { + // A client that cannot be built even against the production + // default means no TLS backend; there is nothing to recover to. + format!("could not initialise the API client: {e}") + })?; + app.manage(state); + + // Background: never blocks the window appearing, and the download is + // applied when the session ends rather than during it. + #[cfg(target_os = "windows")] + update::spawn_check(app.handle().clone()); + + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + commands::list_servers, + commands::get_server, + commands::get_servers, + commands::get_server_history, + commands::get_mod_readiness, + commands::ping_server, + commands::ping_regions, + commands::get_fleet_now, + commands::list_ping_sites, + commands::get_status, + commands::join_server, + commands::launch_game, + commands::open_server_page, + commands::open_discord_invite, + commands::get_local_state, + commands::toggle_favorite, + commands::update_settings, + commands::get_install_identity, + commands::is_debug_mode, + commands::clear_history, + ]) + .build(tauri::generate_context!()) + .expect("error while building the reforgermods.net launcher"); + + // `run` with a callback rather than `Builder::run`, so the exit event can + // hand off to a staged installer. The callback runs for every event; only + // Exit is of interest, and it is the last one. + app.run(|_handle, event| { + if let tauri::RunEvent::Exit = event { + #[cfg(target_os = "windows")] + update::install_staged(_handle); + } + }); +} + +/// Where the state file lives. +/// +/// Uses Tauri's resolved per-app config directory when available, falling back to +/// the working directory so a sandboxed environment without a config dir still +/// starts rather than panicking. +fn state_file_path(app: &tauri::AppHandle) -> std::path::PathBuf { + match app.path().app_config_dir() { + Ok(dir) => dir.join(STATE_FILE), + Err(_) => std::path::PathBuf::from(STATE_FILE), + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..285f56c --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,108 @@ +// Release builds must not open a console window on Windows. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +/// Works around a WebKitGTK startup crash on Nvidia under Wayland. +/// +/// WebKitGTK's DMABUF renderer and the proprietary Nvidia driver disagree about +/// buffer import, and GTK aborts before the window is ever mapped: +/// +/// ```text +/// Gdk-Message: Error 71 (Protocol error) dispatching to Wayland display. +/// ``` +/// +/// Setting `WEBKIT_DISABLE_DMABUF_RENDERER=1` falls back to a software +/// composite path, which costs some GPU acceleration and produces a window that +/// actually appears. Reported against Arch with an Nvidia card; it is a WebKit +/// and driver interaction rather than anything this app controls. +/// +/// Deliberately narrow, because the fallback is not free for anyone who does not +/// need it: +/// +/// - an existing value is never overridden, so a user debugging this keeps control; +/// - only when the Nvidia driver is actually loaded; +/// - only in a Wayland session, since the X11 path does not use this renderer; +/// - not when GDK is already pinned to X11, where it is equally unused. +#[cfg(target_os = "linux")] +fn disable_dmabuf_renderer_on_nvidia_wayland() { + let decision = should_disable_dmabuf( + std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_some(), + std::env::var_os("WAYLAND_DISPLAY").is_some(), + std::env::var_os("GDK_BACKEND") + .as_deref() + .and_then(|b| b.to_str()), + std::path::Path::new("/proc/driver/nvidia/version").exists() + || std::path::Path::new("/sys/module/nvidia").exists(), + ); + if !decision { + return; + } + // SAFETY: this runs at the top of main, before Tauri, GTK or any thread + // exists, so there is no concurrent reader of the environment. It must also + // happen here rather than later: WebKit reads this once, during init. + std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); +} + +/// The decision behind [`disable_dmabuf_renderer_on_nvidia_wayland`], separated +/// from the environment so each condition can be tested. +#[cfg(target_os = "linux")] +fn should_disable_dmabuf( + already_set: bool, + wayland_session: bool, + gdk_backend: Option<&str>, + nvidia_loaded: bool, +) -> bool { + if already_set || !wayland_session || !nvidia_loaded { + return false; + } + // A GDK_BACKEND naming x11 alone means the Wayland renderer is not in play. + // A list such as "wayland,x11" still prefers Wayland, so only the exact pin + // counts as opting out. + gdk_backend != Some("x11") +} + +fn main() { + #[cfg(target_os = "linux")] + disable_dmabuf_renderer_on_nvidia_wayland(); + + reforgermods_launcher_lib::run() +} + +#[cfg(all(test, target_os = "linux"))] +mod tests { + use super::should_disable_dmabuf; + + #[test] + fn nvidia_under_wayland_is_the_case_that_needs_the_workaround() { + assert!(should_disable_dmabuf(false, true, None, true)); + assert!(should_disable_dmabuf(false, true, Some("wayland"), true)); + // Still Wayland-first, so the renderer is still the one that breaks. + assert!(should_disable_dmabuf( + false, + true, + Some("wayland,x11"), + true + )); + } + + #[test] + fn an_explicit_setting_is_never_overridden() { + // Someone debugging this has set it to 0 on purpose; honour that. + assert!(!should_disable_dmabuf(true, true, None, true)); + } + + #[test] + fn other_sessions_and_drivers_keep_accelerated_rendering() { + assert!( + !should_disable_dmabuf(false, false, None, true), + "X11 session" + ); + assert!( + !should_disable_dmabuf(false, true, None, false), + "not Nvidia" + ); + assert!( + !should_disable_dmabuf(false, true, Some("x11"), true), + "pinned to XWayland" + ); + } +} diff --git a/src-tauri/src/presence.rs b/src-tauri/src/presence.rs new file mode 100644 index 0000000..6d16b3c --- /dev/null +++ b/src-tauri/src/presence.rs @@ -0,0 +1,163 @@ +//! Discord Rich Presence transport. +//! +//! Discord's IPC is a blocking local socket, and Discord may not be running at +//! all, so this owns one background thread and talks to it over a channel. +//! Nothing on the UI path ever blocks on Discord, and a missing or restarted +//! Discord is a no-op rather than an error the user sees. +//! +//! Payload text is built in `rfm_core::presence`, where the length and +//! sanitisation rules are tested. + +use std::sync::mpsc::{self, Receiver, Sender}; +use std::time::{Duration, Instant}; + +use discord_rich_presence::error::Error as DiscordError; +use discord_rich_presence::{activity, DiscordIpc, DiscordIpcClient}; +use rfm_core::presence::Presence; + +/// Discord application id. Determines the name shown as the game being played. +/// +/// Empty by default: without a real application this build must not attempt to +/// connect. Set `RFM_DISCORD_APP_ID` at runtime, or replace this constant once +/// the application exists. See `docs/discord-presence.md`. +const DEFAULT_APP_ID: &str = "1552757014474784898"; + +/// Environment override for the application id. +const APP_ID_ENV: &str = "RFM_DISCORD_APP_ID"; + +/// Artwork key uploaded to the Discord application's Rich Presence assets. +const LARGE_IMAGE_KEY: &str = "logo"; + +/// How long to wait before retrying a failed connection. Discord being closed +/// is the normal case, so this stays slow and quiet. +const RECONNECT_DELAY: Duration = Duration::from_secs(30); + +/// Messages the worker accepts. +enum Message { + /// Publish this presence. + Update(Presence), + /// Stop publishing and disconnect. + Clear, +} + +/// Handle to the presence worker. +pub struct PresenceHandle { + tx: Option>, +} + +impl PresenceHandle { + /// Starts the worker, or returns an inert handle when presence is disabled. + /// + /// Disabled means: no application id configured. The launcher still runs + /// normally; nothing is published. + pub fn start() -> Self { + let app_id = std::env::var(APP_ID_ENV) + .ok() + .filter(|v| !v.trim().is_empty()) + .unwrap_or_else(|| DEFAULT_APP_ID.to_string()); + + if app_id.trim().is_empty() { + return Self { tx: None }; + } + + let (tx, rx) = mpsc::channel(); + std::thread::Builder::new() + .name("discord-presence".into()) + .spawn(move || worker(app_id, rx)) + .ok(); + Self { tx: Some(tx) } + } + + /// True when presence is configured and a worker is running. + pub fn is_enabled(&self) -> bool { + self.tx.is_some() + } + + /// Publishes `presence`. Never blocks and never fails the caller. + pub fn update(&self, presence: Presence) { + self.send(Message::Update(presence)); + } + + /// Clears the presence, e.g. when the user turns it off. + pub fn clear(&self) { + self.send(Message::Clear); + } + + fn send(&self, message: Message) { + if let Some(tx) = &self.tx { + // A dead worker means Discord support is gone for this session; + // that is not worth surfacing. + let _ = tx.send(message); + } + } +} + +/// Owns the IPC client for the life of the process. +fn worker(app_id: String, rx: Receiver) { + let mut client = DiscordIpcClient::new(&app_id); + let mut connected = false; + let mut last_attempt: Option = None; + // Re-sent after a reconnect, so a Discord restart does not blank the status. + let mut current: Option = None; + + while let Ok(message) = rx.recv() { + match message { + Message::Update(presence) => current = Some(presence), + Message::Clear => { + // `take` both clears and tells us whether anything was live, so + // a clear with nothing published costs no IPC round trip. + if current.take().is_some() && connected { + let _ = client.clear_activity(); + } + } + } + + // Nothing to publish means no reason to hold or open a connection. + let Some(presence) = current.clone() else { + continue; + }; + + if !connected { + // Back off: Discord not running is the normal case, not an error. + let due = last_attempt.is_none_or(|at| at.elapsed() >= RECONNECT_DELAY); + if !due { + continue; + } + last_attempt = Some(Instant::now()); + connected = client.connect().is_ok(); + if !connected { + continue; + } + } + + if publish(&mut client, &presence).is_err() { + // Discord closed or restarted; drop the connection and let the next + // update reconnect after the backoff. + connected = false; + let _ = client.close(); + } + } + + if connected { + let _ = client.clear_activity(); + let _ = client.close(); + } +} + +/// Sends one activity. +fn publish(client: &mut DiscordIpcClient, presence: &Presence) -> Result<(), DiscordError> { + client.set_activity( + activity::Activity::new() + .details(presence.details.as_str()) + .state(presence.state.as_str()) + .assets( + activity::Assets::new() + .large_image(LARGE_IMAGE_KEY) + .large_text(presence.large_text.as_str()), + ) + .buttons(vec![activity::Button::new( + "Get the launcher", + "https://reforgermods.net", + )]), + ) +} diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs new file mode 100644 index 0000000..fa5eaf8 --- /dev/null +++ b/src-tauri/src/state.rs @@ -0,0 +1,77 @@ +//! Shared application state. + +use std::sync::{Mutex, RwLock}; + +use rfm_core::api::models::DatasetMeta; +use rfm_core::presence::Activity; +use rfm_core::store::Store; + +use crate::presence::PresenceHandle; +use rfm_core::{ApiClient, ApiError}; + +/// State held for the lifetime of the application. +pub struct AppState { + /// Behind an `RwLock` because changing the API base URL in settings swaps the + /// client, while every request only needs read access. + client: RwLock, + /// Persisted settings, favorites, recents and launch history. + pub store: Store, + /// Discord Rich Presence publisher. Inert when no application id is set. + presence: PresenceHandle, + /// Snapshot freshness from the most recent server response. + /// + /// Every `/v2/servers` response carries this, so the header can show + /// "server data updated Ns ago" without its own polling loop. + last_dataset: Mutex>, +} + +impl AppState { + /// Builds state with a client for the configured API origin, identifying + /// with the store's anonymous install id. + pub fn new(store: Store) -> Result { + let client = ApiClient::new(&store.install_id())?; + Ok(Self { + client: RwLock::new(client), + store, + presence: PresenceHandle::start(), + last_dataset: Mutex::new(None), + }) + } + + /// A clone of the current client, so requests do not hold the lock while + /// awaiting the network. + pub fn client(&self) -> ApiClient { + self.client + .read() + .unwrap_or_else(|e| e.into_inner()) + .clone() + } + + /// Publishes `activity` to Discord, if the user has it enabled. + /// + /// Silently does nothing when presence is off or unconfigured; this is a + /// decorative feature and must never interfere with a user action. + pub fn publish_presence(&self, activity: Activity<'_>) { + if !self.presence.is_enabled() { + return; + } + if self.store.read(|s| s.settings.discord_presence) { + self.presence.update(activity.to_presence()); + } else { + self.presence.clear(); + } + } + + /// Records snapshot freshness seen on a server response. + pub fn remember_dataset(&self, dataset: DatasetMeta) { + *self.last_dataset.lock().unwrap_or_else(|e| e.into_inner()) = Some(dataset); + } + + /// The most recently seen snapshot freshness, if any. + pub fn last_dataset(&self) -> Option { + self.last_dataset + .lock() + .unwrap_or_else(|e| e.into_inner()) + .clone() + } +} diff --git a/src-tauri/src/update.rs b/src-tauri/src/update.rs new file mode 100644 index 0000000..e2cc5d9 --- /dev/null +++ b/src-tauri/src/update.rs @@ -0,0 +1,106 @@ +//! Background updates, Windows only. +//! +//! The launcher is an API client, which is what makes this worth shipping in the +//! first release rather than later: when `/v2/servers` changes shape, anyone on +//! an old build gets a broken server browser and blames the site. An updater +//! turns that from a support problem into a non-event. +//! +//! The shape is deliberate, and it is the reason this is not simply +//! `download_and_install`: +//! +//! 1. check and download in the background, so nothing blocks startup; +//! 2. hold the downloaded package until the session ends; +//! 3. run the installer as the app exits, without relaunching it. +//! +//! Nobody is interrupted mid-session, and nobody is asked to make a decision +//! about a patch release. The update is simply there the next time they open the +//! launcher. `Update::install` calls `std::process::exit(0)` on Windows after +//! launching the installer, which is harmless at exit and would be a hard kill at +//! any other moment — another reason the install is pinned to exactly this point. +//! +//! There is no updater on Linux. The Arch package is owned by pacman, and an app +//! that rewrites its own files under `/usr/bin` fights the package manager (see +//! `docs/distribution.md` section 7). The dependency is scoped to Windows in +//! `Cargo.toml`, so this module does not exist in a Linux build. + +use std::sync::Mutex; + +use tauri::Manager as _; +use tauri_plugin_updater::{Update, UpdaterExt as _}; + +/// An update that has been downloaded and is waiting for the session to end. +/// +/// `Update` carries the verified release metadata and `bytes` is the installer +/// payload, already signature-checked by the plugin against the public key in +/// `tauri.conf.json`. Holding both means the install at exit is a local +/// operation: no network, nothing that can hang a closing app. +#[derive(Default)] +pub struct PendingUpdate(Mutex>); + +struct Staged { + update: Update, + bytes: Vec, +} + +/// Reports a failure only to a developer. An updater that cannot reach the +/// network is not the user's problem and must not produce a dialog; `RFM_DEBUG=1` +/// is how it becomes visible, matching how the rest of the app gates detail. +fn note(context: &str, error: &dyn std::fmt::Display) { + if crate::debug_mode() { + eprintln!("updater: {context}: {error}"); + } +} + +/// Checks for an update and downloads it, in the background. +/// +/// Silent on every failure by design: no endpoint, no network, a malformed +/// manifest or a bad signature all leave the app exactly as it was. +pub fn spawn_check(app: tauri::AppHandle) { + // A dev build's version always matches the manifest, so the check can only + // cost a request. Skipped rather than relied upon to no-op. + if cfg!(debug_assertions) { + return; + } + tauri::async_runtime::spawn(async move { + let updater = match app.updater() { + Ok(updater) => updater, + Err(error) => return note("could not build the updater", &error), + }; + let update = match updater.check().await { + Ok(Some(update)) => update, + // Already current: the overwhelmingly common case. + Ok(None) => return, + Err(error) => return note("check failed", &error), + }; + let bytes = match update.download(|_, _| {}, || {}).await { + Ok(bytes) => bytes, + Err(error) => return note("download failed", &error), + }; + if let Some(state) = app.try_state::() { + *state.0.lock().unwrap() = Some(Staged { update, bytes }); + } + }); +} + +/// Runs a staged installer, if there is one. Called from the exit event. +/// +/// `restart_after_install(false)` is the whole point: the default relaunches the +/// app, and relaunching something the user has just closed is worse than the +/// stale build it fixes. +pub fn install_staged(app: &tauri::AppHandle) { + let Some(state) = app.try_state::() else { + return; + }; + // Taken, not borrowed: if the installer somehow returns, a second attempt + // during the same shutdown is not wanted. + let Some(staged) = state.0.lock().unwrap().take() else { + return; + }; + if let Err(error) = staged + .update + .restart_after_install(false) + .install(staged.bytes) + { + note("install failed", &error); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..fbdf3ce --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,74 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "reforgermods.net launcher", + "version": "0.1.0", + "identifier": "net.reforgermods.launcher", + "build": { + "beforeDevCommand": "npm run dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "npm run build", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "title": "reforgermods.net launcher", + "width": 1400, + "height": 880, + "minWidth": 1000, + "minHeight": 620, + "resizable": true, + "center": true, + "decorations": false, + "theme": "Dark", + "dragDropEnabled": false, + "backgroundColor": "#050d10" + } + ], + "security": { + "csp": { + "default-src": "'self'", + "script-src": "'self'", + "style-src": "'self' 'unsafe-inline'", + "font-src": "'self'", + "connect-src": "'self' ipc: http://ipc.localhost", + "img-src": "'self' data: https://ar-gcp-cdn.bistudio.com", + "object-src": "'none'", + "base-uri": "'self'", + "frame-src": "'none'" + }, + "capabilities": ["main"] + } + }, + "bundle": { + "active": true, + "targets": ["nsis"], + "createUpdaterArtifacts": true, + "publisher": "reforgermods.net", + "shortDescription": "Server browser and direct launcher for Arma Reforger.", + "longDescription": "A fast, lightweight desktop companion for Arma Reforger built on the reforgermods.net API: live server browser, filtering, favorites and direct joining through Steam.", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.ico" + ], + "windows": { + "nsis": { + "installMode": "currentUser" + }, + "webviewInstallMode": { + "type": "downloadBootstrapper" + } + } + }, + "plugins": { + "updater": { + "endpoints": ["https://dl.reforgermods.net/updates/latest.json"], + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEIxMDJFNDhENEZDNTE4M0IKUldRN0dNVlBqZVFDc1dBTHlSY2tsakRyNjYvZHdpdWNHQzRud3dzeGhlTTFidmwrekNaUlJYdEYK", + "windows": { + "installMode": "passive" + } + } + } +} diff --git a/src-tauri/tauri.signing.conf.json b/src-tauri/tauri.signing.conf.json new file mode 100644 index 0000000..2cab3e0 --- /dev/null +++ b/src-tauri/tauri.signing.conf.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "bundle": { + "windows": { + "signCommand": "cmd /C ..\\packaging\\windows\\sign.cmd \"%1\"" + } + } +} diff --git a/src/app.test.tsx b/src/app.test.tsx new file mode 100644 index 0000000..e8ae012 --- /dev/null +++ b/src/app.test.tsx @@ -0,0 +1,469 @@ +/** + * @vitest-environment jsdom + * + * Smoke test for the shell: with the Tauri IPC layer stubbed, the application + * must mount, render its chrome, and populate the table from a server page. + * This is the check that a component throws at render time — something neither + * `tsc` nor the Rust tests can catch. + */ + +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { render } from "preact"; +import type { ServerSummary } from "./lib/types"; + +const server: ServerSummary = { + id: "73e8fb7f-8789-4d88-8aa8-ea69b9aa092f", + name: "[OGR2] Old Guard Revival 2 | Ruha Finland", + scenarioId: "{FAC303E9F1B7CAB2}Missions/OGR_RuhaN-S.conf", + scenarioName: "[OGR] Ruha Conflict North South", + scenarioThumbnailUrl: + "https://ar-gcp-cdn.bistudio.com/image/b64d/02af30860106e7097a8238852468fb85d249062ae56b43b1d6c34e70c20f/45316.jpg", + gameVersion: "1.8.0.13", + official: false, + passwordProtected: false, + battlEye: true, + crossPlay: true, + players: 12, + maxPlayers: 128, + queue: 0, + queueMax: 25, + pingSiteId: "new_york", + region: "new_york", + platform: "LINUX", + platforms: ["pc", "xbox", "psn"], + modCount: 122, + online: true, + firstSeen: null, + lastSeen: null, + analytics: null, + joinUrl: null, +}; + +/** Stub the Tauri bridge; nothing in a test environment can reach a backend. */ +vi.mock("@tauri-apps/api/core", () => ({ + invoke: vi.fn(async (command: string) => { + switch (command) { + case "list_servers": + return { + servers: [server], + totalServers: 4999, + totalPages: 50, + currentPage: 1, + filteredOut: 0, + dataset: { + warming: false, + stale: false, + snapshotAgeSeconds: 18, + lastCollectionAt: "2026-09-24T06:54:58Z", + serverCount: 94223, + onlineServerCount: 4999, + offlineServerCount: 89224, + }, + }; + case "get_local_state": + return { + settings: { + perPage: 100, + autoRefresh: true, + refreshIntervalSecs: 60, + discordPresence: true, + }, + favorites: [server.id], + recents: [{ id: server.id, name: server.name, lastSeenAt: 1790149200 }], + history: [], + }; + case "get_status": + return { + apiReachable: true, + dataset: null, + reportedStatus: null, + note: "This API deployment does not serve /v2/status.", + }; + case "get_server": + return { + ...server, + present: true, + hostType: "CommunityDs", + joinable: true, + host: "208.92.232.141", + port: 2001, + fps: 60, + activity: { + peak24h: 95, + avg24h: 33, + median24h: 22, + low24h: 0, + peak7d: 128, + samples24h: 1286, + trackingSince: null, + }, + modSummary: { + count: 2, + knownSize: 8_772_055_531, + knownSizeText: "8.17 GiB", + unresolvedCount: 1, + }, + mods: [ + { + id: "64610AFB74AA9842", + name: "WCS_Core", + version: "8.2.0", + size: 8_772_055_531, + sizeText: null, + sizeKnown: true, + modUrl: null, + }, + { + id: "1337C0DE5DABBEEF", + name: "RHS - Content Pack 01", + version: "0.16.5208", + size: 0, + sizeText: null, + sizeKnown: false, + modUrl: null, + }, + ], + }; + case "get_servers": + return [server]; + case "is_debug_mode": + return false; + case "get_install_identity": + return { id: "", machineDerived: false }; + case "get_fleet_now": + return { onlineServers: 5089, players: 16352, capacity: 224986, queue: 694 }; + case "ping_regions": + return { new_york: { summary: { bestMs: 70, medianMs: 72, received: 3, sent: 3 } } }; + case "get_install_id": + return "8f14e45f-ceea-467a-9c2b-1f2a3b4c5d6e"; + case "ping_server": + return { summary: { bestMs: 70, medianMs: 72, received: 3, sent: 3 } }; + case "get_server_history": + return { + serverId: server.id, + range: "24h", + bucket: "5m", + points: [ + { t: 1790149200, avg: 6, min: 5, max: 7, cap: 128 }, + { t: 1790149500, avg: 40, min: 20, max: 95, cap: 128 }, + { t: 1790149800, avg: 12, min: 10, max: 14, cap: 128 }, + ], + }; + case "get_mod_readiness": + return { + available: true, + required: 136, + upToDate: 39, + outdated: 0, + missing: 96, + missingBytes: 8_772_055_531, + unresolvedMods: 1, + installedTotal: 235, + }; + case "list_ping_sites": + return [ + { id: "new_york", label: "New York", regionGroup: "North America", address: null }, + ]; + default: + return null; + } + }), +})); + +async function mount() { + const { App } = await import("./app"); + const { start } = await import("./lib/state"); + const root = document.createElement("div"); + document.body.appendChild(root); + render(, root); + await start(); + // Let the resulting signal updates flush. + await new Promise((resolve) => setTimeout(resolve, 0)); + return root; +} + +/** + * The column headings, in order. + * + * Sortable headings are buttons wrapping a label and a direction arrow, so this + * reads the label rather than the cell's whole text content — which would + * otherwise pick up the arrow on whichever column is currently sorted. + */ +function headerLabels(root: HTMLElement): (string | null | undefined)[] { + return Array.from(root.querySelectorAll(".thead > *")).map( + (cell) => (cell.querySelector(".th-sort__label") ?? cell).textContent, + ); +} + +describe("application shell", () => { + beforeEach(() => { + document.body.innerHTML = ""; + vi.resetModules(); + }); + + it("renders the brand, navigation and status", async () => { + const root = await mount(); + const text = root.textContent ?? ""; + expect(text).toContain("reforger"); + expect(text).toContain("launcher"); + expect(text).toContain("Server browser"); + expect(text).toContain("Favorites"); + expect(text).toContain("Settings"); + // Status resolved from the stubbed probe. + expect(text).toContain("API operational"); + }); + + it("renders a fetched server into the table", async () => { + const root = await mount(); + const text = root.textContent ?? ""; + expect(text).toContain("Old Guard Revival 2"); + expect(text).toContain("12 / 128"); + expect(text).toContain("New York"); + // 4,999 matched servers reported in the footer. + expect(text).toMatch(/4[.,  ]?999/); + }); + + it("exposes the search field with its shortcut hint", async () => { + const root = await mount(); + const search = root.querySelector('input[aria-label="Search servers"]'); + expect(search).not.toBeNull(); + expect(root.textContent).toContain("Ctrl F"); + }); + + it("renders the filter chips the API supports", async () => { + const root = await mount(); + const chips = Array.from(root.querySelectorAll(".chip")).map((c) => c.textContent); + expect(chips).toEqual(["Official", "Modded", "BattlEye", "Password", "Offline"]); + // All start unset, so no filter is silently applied at launch. + for (const chip of root.querySelectorAll(".chip")) { + expect(chip.getAttribute("data-state")).toBe("unset"); + } + }); + + it("shows the empty inspector until a server is selected", async () => { + const root = await mount(); + expect(root.textContent).toContain("No server selected"); + }); + + it("shows region latency in the table and offers sorting by it", async () => { + const root = await mount(); + expect(headerLabels(root)).toEqual([ + "Server", "", "Players", "Queue", "Ping", "Region", "Mods", "Platform", + ]); + + // The stub measures new_york, which is this server's region. + const cell = root.querySelector(".cell.ping"); + expect(cell?.textContent).toBe("72"); + + const sortOptions = Array.from( + root.querySelectorAll('select[aria-label="Sort order"]')[0]?.options ?? [], + ).map((o) => o.value); + expect(sortOptions).toContain("ping"); + }); + + it("sorts from the column headings, marking what only reaches this page", async () => { + const root = await mount(); + const { clientSort, query } = await import("./lib/state"); + + const heading = (label: string) => + Array.from(root.querySelectorAll(".thead .th-sort")).find( + (b) => b.querySelector(".th-sort__label")?.textContent === label, + ); + + // Queue is not an ordering the API offers, so it reorders the loaded page + // and the footer has to say so. + heading("Queue")?.click(); + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(clientSort.value).toEqual({ column: "queue", direction: "desc" }); + expect(root.textContent).toContain("sorted by queue within this page"); + + // Clicking it again reverses it, still page-local. + heading("Queue")?.click(); + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(clientSort.value).toEqual({ column: "queue", direction: "asc" }); + + // Players is a sort the API serves across every matching server, so the + // local reordering is dropped and the caveat goes with it. + heading("Players")?.click(); + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(clientSort.value).toBeNull(); + expect(query.value.sort).toBe("players"); + expect(root.textContent).not.toContain("within this page"); + + // Reversing it is something the API has no parameter for, so that one does + // fall back to the page. + heading("Players")?.click(); + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(clientSort.value).toEqual({ column: "players", direction: "asc" }); + expect(root.textContent).toContain("sorted by players within this page"); + }); + + it("renders favorites as a server table, not a plain list", async () => { + const root = await mount(); + const { setView } = await import("./lib/state"); + setView("favorites"); + await new Promise((resolve) => setTimeout(resolve, 0)); + + // Same table chrome as the browser, with live columns. + expect(headerLabels(root)).toEqual([ + "Server", "", "Players", "Queue", "Ping", "Region", "Mods", "Platform", + ]); + expect(root.textContent).toContain("Old Guard Revival 2"); + expect(root.textContent).toContain("12 / 128"); + // The inspector travels with the table. + expect(root.textContent).toContain("No server selected"); + }); + + it("keeps the install identifier out of the UI outside debug mode", async () => { + const root = await mount(); + const { setView } = await import("./lib/state"); + setView("settings"); + await new Promise((resolve) => setTimeout(resolve, 0)); + + const text = root.textContent ?? ""; + expect(text).not.toContain("Install identity"); + expect(text).not.toContain("Reset identifier"); + // The Discord toggle is user-facing and should still be there. + expect(text).toContain("Discord status"); + }); + + it("shows a platform mark per supported platform", async () => { + const root = await mount(); + const badges = Array.from( + root.querySelectorAll(".row .platforms__badge svg"), + ).map((b) => b.getAttribute("aria-label")); + expect(badges).toEqual(["PC", "Xbox", "PlayStation"]); + expect(root.textContent).not.toContain("XPLAY"); + }); + + it("aligns the header with the rows by sharing one scroll container", async () => { + // With the header outside the scroller, a visible scrollbar narrowed the + // rows and every column drifted left of its heading. + const root = await mount(); + const table = root.querySelector(".table"); + expect(table?.querySelector(".thead")).not.toBeNull(); + expect(table?.querySelector(".rows")).not.toBeNull(); + }); + + it("shows status as aligned marks, not pills trailing the name", async () => { + const root = await mount(); + // The pills used to start wherever the name ended, forming a ragged column. + expect(root.querySelector(".row .name .tag")).toBeNull(); + const marks = Array.from( + root.querySelectorAll(".row .marks .marks__i"), + ).map((m) => m.getAttribute("title")); + // This server is community, open and BattlEye-enabled. + expect(marks).toEqual(["BattlEye enabled"]); + }); + + it("uses a dedicated favourite button per row", async () => { + const root = await mount(); + const star = root.querySelector(".row .star"); + expect(star).not.toBeNull(); + // The stubbed local state has this server favourited, so the star is on. + expect(star?.getAttribute("aria-pressed")).toBe("true"); + expect(star?.getAttribute("data-on")).toBe("true"); + expect(star?.getAttribute("title")).toBe("Remove from favorites"); + // Drawn as a glyph, not a text character. + expect(star?.querySelector("svg")).not.toBeNull(); + }); + + it("drops the population bar beside the player count", async () => { + const root = await mount(); + expect(root.querySelector(".pop__bar")).toBeNull(); + expect(root.querySelector(".row .cell.num")?.textContent).toBe("12 / 128"); + }); + + it("shows the fleet player total beside the server count", async () => { + const root = await mount(); + const context = root.querySelector(".titlebar__context")?.textContent ?? ""; + expect(context).toMatch(/4[.,\s]?999\s*servers/); + expect(context).toContain("players online"); + expect(context).toMatch(/16[.,\s]?352/); + }); + + it("colours the wordmark like the site", async () => { + const root = await mount(); + expect(root.querySelector(".wordmark__reforger")?.textContent).toBe("reforger"); + expect(root.querySelector(".wordmark__mods")?.textContent).toBe("mods"); + expect(root.querySelector(".wordmark__net")?.textContent).toBe(".net"); + }); + + it("no longer offers a scenario filter", async () => { + const root = await mount(); + expect( + root.querySelector('select[aria-label="Scenario (filters the loaded page)"]'), + ).toBeNull(); + }); + + it("renders readiness and the history chart once a server is selected", async () => { + const root = await mount(); + const { selectServer } = await import("./lib/state"); + await selectServer(server.id); + await new Promise((resolve) => setTimeout(resolve, 0)); + + const text = root.textContent ?? ""; + // The honest copy, end to end through the IPC boundary. + expect(text).toContain( + "96 of 136 mods missing, at least 8.17 GiB to download. " + + "Arma Reforger downloads these when you join.", + ); + // Counts read as labelled stats rather than a sentence fragment. + const counts = Array.from(root.querySelectorAll(".readiness__counts div")).map( + (d) => [d.querySelector("dt")?.textContent, d.querySelector("dd")?.textContent], + ); + expect(counts).toEqual([ + ["Installed", "39"], + ["Missing", "96"], + ["On disk", "235"], + ]); + // Readiness needs action, and the state is carried by a dot, not a side edge. + expect(root.querySelector(".readiness")?.getAttribute("data-tone")).toBe("action"); + expect(root.querySelector(".readiness__dot")).not.toBeNull(); + + // The chart drew from the stubbed series. + const line = root.querySelector(".chart__line"); + expect(line).not.toBeNull(); + expect(line?.getAttribute("d")).toMatch(/^M0,/); + expect(text).toContain("peak"); + + // And no action is offered that the launcher cannot perform. + expect(text).not.toContain("Prepare mods"); + + // Mod rows split name, version and size into aligned columns. + const first = root.querySelector(".modlist__row"); + expect(first?.querySelector(".modlist__name")?.textContent).toBe("WCS_Core"); + expect(first?.querySelector(".modlist__version")?.textContent).toBe("8.2.0"); + expect(first?.querySelector(".modlist__size")?.textContent).toBe("8.2 GiB"); + }); + + it("renders the scenario image and the measured ping", async () => { + const root = await mount(); + const { selectServer } = await import("./lib/state"); + await selectServer(server.id); + await new Promise((resolve) => setTimeout(resolve, 0)); + + const img = root.querySelector(".scenario__img"); + expect(img).not.toBeNull(); + expect(img?.getAttribute("src")).toContain("ar-gcp-cdn.bistudio.com"); + + // Measured latency, not a region estimate. + expect(root.textContent).toContain("72 ms"); + // The published address, for people who want to see it. + expect(root.textContent).toContain("208.92.232.141:2001"); + }); + + it("offers the history range switcher", async () => { + const root = await mount(); + const { selectServer } = await import("./lib/state"); + await selectServer(server.id); + await new Promise((resolve) => setTimeout(resolve, 0)); + + const ranges = Array.from(root.querySelectorAll(".rangeswitch button")).map( + (b) => b.textContent, + ); + expect(ranges).toEqual(["6h", "24h", "7d", "30d"]); + const active = root.querySelector('.rangeswitch button[data-on="true"]'); + expect(active?.textContent).toBe("24h"); + }); +}); diff --git a/src/app.tsx b/src/app.tsx new file mode 100644 index 0000000..93de1f5 --- /dev/null +++ b/src/app.tsx @@ -0,0 +1,32 @@ +/** Application shell: brand, header, navigation and the active view. */ + +import { Inspector } from "./components/Inspector"; +import { TitleBar } from "./components/TitleBar"; +import { Sidebar } from "./components/Sidebar"; +import { BrowseView } from "./views/BrowseView"; +import { FavoritesView, RecentView } from "./views/ListView"; +import { SettingsView } from "./views/SettingsView"; +import { view } from "./lib/state"; + +export function App() { + const current = view.value; + // The saved lists render the same table as the browser, so they get the same + // inspector; only settings is full width. + const showInspector = current !== "settings"; + + return ( +
+ + + + +
+ {current === "browse" && } + {current === "favorites" && } + {current === "recent" && } + {current === "settings" && } + {showInspector && } +
+
+ ); +} diff --git a/src/assets/brand-mark.png b/src/assets/brand-mark.png new file mode 100644 index 0000000000000000000000000000000000000000..e8ff44b2cb4527c88109600738005d70fb067840 GIT binary patch literal 2349 zcmV+|3DWk7P)UIRmvTT8ln{f_F9ny#7^fb zL7lrYbIvF;A~)wbhf3=mb7#jp^#pq}w&&R%3PniJ)7#ALe&3frAR)0>ZkX?#dFI`H zcfZZ|zS-UP*-a=EQb-|%6jDebg%naqA%zrDNSPP;3DqN}xO9`EOupp+!e)6nrpwA< zxvX440NRk2qkf7_P{V$(Y_=11tiQZ`i=qqx8?zjh@v+Ow2`%5BBiXp!lj)t#WcgAT zGyrr4)BTs9p1k{XN@sXFrFRs-iM|GWY$Ht1gGVF(1{W`0oN8!Po%Ih2j-yce7A6SX zfS~X%zW98~Ifte@rkkT}Mi5MR266lLEl~6GS}lXcahvmnX+ECWnYj_o8R9U6Bbk!$ z<_t+#Gt%ifp;$RX9FFWw8PJxQOK8*&K9i0=*BqZJY<$Cm>Waq95=(YGc<>FdKND;@ z{Iakfe@Z_I+S-r*)raG^%5DJMJZavWs}dhHYNP-`h3S0IsK#KxwG=~xny_muV6>_8 zKtfu`snpCAiclz^)YR_jp3=|uf;tC+F`HBNf@FK_juRi>;&A;}+6@Am$6CAlz`Ezq zNkaodYd#J&4rIsMuz=uwjBOfoTb(Kg1cmx-L1S`w0q!R|JMYczh^?2`IneHCU!ttn zf!e)w-Enbo_Lo9fOyB|Khq=J$nA!Q6jv9kv85|t>psFTqg6_i*p)msm!brFuJahaT zpKsuwcW-zIh{E1Vw;OVY!DFQD@S88SuA>^s;N(_)n3zfK|tNv z**Vau?Yw5Pb!o3c+cjt#fVKh5ZqbjhUo+XEt*fi+PnRxT(%TM>WmeFE1X0MuKFhC<@`6xG1tmQXGW<>ybUVCDG?lo45$KVY(>tJS?9>$SuL2gR|kFm#60| z3qW;sHRbf_)BmuYa`AdwTifrP8koWHj@`L8<0&FM1V6AKL1?4IND{G0;v51eO*lD1 z?W#7oA2eivz))Vvs|tW8lSPF~LT`T~)fY_aDS!BS3*&9y-y{KuE)#4Km64-BWwsfx%@1!g^=)cHZ8EfZ8st0He)k5|jg7`E zF5RpgeLOP9rH4}&EHm&aJe2VQf>!+C05AiE6u%%&p)i792~i;k=T#zvz`7ueTN$*5 zTPcKfhzcQQ<6%HHq=Q3IJ0H`oG}>EeOkS|?byZXI<+(_VW0xw{b$B@f`mgj)`UeRj z|F4ORC)0=B)6+9ze^>< z?h^H3S$XUb0xTojkhBc%NAyK{7?PzsbZ@UPG-iBaviR?J?f~a+kY@HB9eoTm?d@N# z_74=jvTu#!zbYLbnqExL0u9#b(1J9{EZR{D#rKK4&2hKsEXE4)$1KX$s_kmHVFAYi zFmn_9>Z^O8Q`>cCT~BKh=1_lN|OKaCI_y25X$o z6Q&WT+QV6ZFZ3xkiIyh8yCis#`5XHeqt<|jp;5^6xC<79i1@(w z_o>8Se=|gr*kDEuszbZPfN5hH2QXqEwEyhT1|W>!HR4NwOaJv}pM5>|6}g! true -> false -> unset. */ +function nextTriState(value: boolean | null): boolean | null { + if (value === null) return true; + if (value) return false; + return null; +} + +function triLabel(value: boolean | null): "on" | "off" | "unset" { + return value === null ? "unset" : value ? "on" : "off"; +} + +interface ChipProps { + label: string; + value: boolean | null; + onChange: (next: boolean | null) => void; + titles: [unset: string, on: string, off: string]; +} + +function Chip({ label, value, onChange, titles }: ChipProps) { + const state = triLabel(value); + const title = state === "unset" ? titles[0] : state === "on" ? titles[1] : titles[2]; + return ( + + ); +} + +export function FilterBar() { + const q = query.value; + const searchRef = useRef(null); + + // Ctrl+K focuses search, Escape clears it. Registered once for the window so + // the shortcut works regardless of what currently has focus. + useEffect(() => { + function onKeyDown(event: KeyboardEvent) { + if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "f") { + event.preventDefault(); + searchRef.current?.focus(); + searchRef.current?.select(); + } else if (event.key === "Escape" && document.activeElement === searchRef.current) { + searchRef.current?.blur(); + if (searchInput.value) setSearch(""); + } + } + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, []); + + const sites = pingSites.value; + + return ( +
+ + + + + + + + + + setQuery({ official })} + titles={["Any", "Official only", "Community only"]} + /> + setQuery({ hasMods })} + titles={["Any", "Modded only", "Vanilla only"]} + /> + setQuery({ battleye })} + titles={["Any", "BattlEye on", "BattlEye off"]} + /> + setQuery({ locked })} + titles={["Any", "Locked only", "Open only"]} + /> + + + + Min players + { + const raw = Number((e.target as HTMLInputElement).value); + setQuery({ minPlayers: Number.isFinite(raw) ? Math.max(0, Math.trunc(raw)) : 0 }); + }} + /> + +
+ {hasActiveFilters(q) && ( + + )} +
+ ); +} + +function hasActiveFilters(q: typeof query.value): boolean { + return ( + q.search.trim() !== "" || + q.region !== null || + q.platform !== null || + q.official !== null || + q.hasMods !== null || + q.battleye !== null || + q.locked !== null || + q.includeOffline || + q.minPlayers > 0 + ); +} diff --git a/src/components/HistoryChart.tsx b/src/components/HistoryChart.tsx new file mode 100644 index 0000000..d9f7eb7 --- /dev/null +++ b/src/components/HistoryChart.tsx @@ -0,0 +1,74 @@ +/** + * Population history for the selected server. + * + * One inline SVG, no charting library: a min–max band, an average line, and a + * range switcher. The paths come from `lib/chart.ts`, which is pure and tested. + */ + +import { historyLoading, historyRange, historySeries, setHistoryRange } from "../lib/state"; +import { buildChart } from "../lib/chart"; +import { HISTORY_RANGES } from "../lib/types"; + +/** Drawing box. `preserveAspectRatio="none"` stretches it to the panel width. */ +const WIDTH = 300; +const HEIGHT = 56; + +export function HistoryChart() { + const series = historySeries.value; + const loading = historyLoading.value; + const range = historyRange.value; + const geometry = series ? buildChart(series.points, WIDTH, HEIGHT) : null; + + return ( +
+
+ Population +
+ {HISTORY_RANGES.map((option) => ( + + ))} +
+
+ + {geometry ? ( + <> + + {/* Band first so the average line sits on top of it. */} + + {/* non-scaling-stroke keeps the line 1px despite the stretched viewBox. */} + + +
+ + peak {geometry.peak} + + + avg {geometry.mean} + + + low {geometry.low} + + {geometry.cap > 0 && of {geometry.cap}} + {series?.bucket} buckets +
+ + ) : ( +

+ {loading ? "Loading history…" : "No recorded history for this range."} +

+ )} +
+ ); +} diff --git a/src/components/Inspector.tsx b/src/components/Inspector.tsx new file mode 100644 index 0000000..fa8bc91 --- /dev/null +++ b/src/components/Inspector.tsx @@ -0,0 +1,279 @@ +/** + * Selected-server detail pane. + * + * Shows only values the API actually returns, or that were measured locally. + * There is deliberately no uptime figure, and mod readiness is reported without + * any action attached: the launcher cannot install Workshop content, so it says + * what the game will do instead. See `docs/mod-readiness.md`. + */ + +import { + detailError, + detailLoading, + favoriteSet, + join, + joinError, + joining, + ping, + pingLoading, + selectServer, + selectedId, + selectedServer, + toggleFavorite, +} from "../lib/state"; +import { HistoryChart } from "./HistoryChart"; +import { PlatformBadges } from "./PlatformBadges"; +import { StatusMarks } from "./StatusMarks"; +import { ReadinessNote } from "./ReadinessNote"; +import { openDiscordInvite, openServerPage } from "../lib/ipc"; +import { + discordInviteFrom, + formatBytes, + formatPlayers, + humaniseRegion, + safeImageUrl, + tidyName, +} from "../lib/format"; +import type { ServerDetail } from "../lib/types"; + +export function Inspector() { + const id = selectedId.value; + const server = selectedServer.value; + const error = detailError.value; + + if (!id) { + return ( + + ); + } + + if (error) { + return ( + + ); + } + + if (!server) { + return ( + + ); + } + + return ; +} + +function Detail({ server, isFavorite }: { server: ServerDetail; isFavorite: boolean }) { + const refreshing = detailLoading.value; + const busy = joining.value; + const failure = joinError.value; + + // About half of servers publish no imagery, and any URL the API sends is + // untrusted, so this is null more often than not. + const image = safeImageUrl(server.scenarioThumbnailUrl); + // Server operators advertise their Discord in the server name; the same + // detection the website uses surfaces it as a button. + const discord = discordInviteFrom(server.name); + + return ( + + ); +} + +/** + * Measured round-trip latency to the server, by ICMP. + * + * Shows the median of three probes. When the host does not answer — about a + * third of servers filter ICMP — it says so rather than substituting a + * region-level estimate, which would look like a per-server measurement. + */ +function PingFact() { + const result = ping.value; + const loading = pingLoading.value; + + if (loading && !result) return ; + const summary = result?.summary; + if (!summary) { + return ( +
+
Ping
+
—
+
+ ); + } + const loss = summary.sent > 0 ? Math.round(((summary.sent - summary.received) / summary.sent) * 100) : 0; + return ( +
+
Ping{loss > 0 ? ` (${loss}% loss)` : ""}
+
{summary.medianMs} ms
+
+ ); +} + +function Fact({ k, v }: { k: string; v: string }) { + return ( +
+
{k}
+
+ {v} +
+
+ ); +} diff --git a/src/components/PlatformBadges.tsx b/src/components/PlatformBadges.tsx new file mode 100644 index 0000000..ccc0f88 --- /dev/null +++ b/src/components/PlatformBadges.tsx @@ -0,0 +1,82 @@ +/** + * Platform badges. + * + * The same three marks reforgermods.net uses, so a server reads the same in + * both places. Glyphs are Bootstrap Icons (MIT) — `pc-display`, `xbox` and + * `playstation` — inlined as paths rather than pulled in as a webfont, since + * three icons do not justify a font file and the CSP forbids remote fonts. + */ + +import type { JSX } from "preact"; + +/** Server-reported platform tokens. */ +export type PlatformToken = "pc" | "xbox" | "psn"; + +interface Glyph { + label: string; + /** Brand colour, used sparingly: the badge tint only. */ + tint: string; + path: string; +} + +const GLYPHS: Record = { + pc: { + label: "PC", + tint: "#9aa7b7", + path: "M8 1a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1zm1 13.5a.5.5 0 1 0 1 0 .5.5 0 0 0-1 0m2 0a.5.5 0 1 0 1 0 .5.5 0 0 0-1 0M9.5 1a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zM9 3.5a.5.5 0 0 0 .5.5h5a.5.5 0 0 0 0-1h-5a.5.5 0 0 0-.5.5M1.5 2A1.5 1.5 0 0 0 0 3.5v7A1.5 1.5 0 0 0 1.5 12H6v2h-.5a.5.5 0 0 0 0 1H7v-4H1.5a.5.5 0 0 1-.5-.5v-7a.5.5 0 0 1 .5-.5H7V2z", + }, + xbox: { + label: "Xbox", + tint: "#6bad4a", + path: "M7.202 15.967a8 8 0 0 1-3.552-1.26c-.898-.585-1.101-.826-1.101-1.306 0-.965 1.062-2.656 2.879-4.583C6.459 7.723 7.897 6.44 8.052 6.475c.302.068 2.718 2.423 3.622 3.531 1.43 1.753 2.088 3.189 1.754 3.829-.254.486-1.83 1.437-2.987 1.802-.954.301-2.207.429-3.239.33m-5.866-3.57C.589 11.253.212 10.127.03 8.497c-.06-.539-.038-.846.137-1.95.218-1.377 1.002-2.97 1.945-3.95.401-.417.437-.427.926-.263.595.2 1.23.638 2.213 1.528l.574.519-.313.385C4.056 6.553 2.52 9.086 1.94 10.653c-.315.852-.442 1.707-.306 2.063.091.24.007.15-.3-.319Zm13.101.195c.074-.36-.019-1.02-.238-1.687-.473-1.443-2.055-4.128-3.508-5.953l-.457-.575.494-.454c.646-.593 1.095-.948 1.58-1.25.381-.237.927-.448 1.161-.448.145 0 .654.528 1.065 1.104a8.4 8.4 0 0 1 1.343 3.102c.153.728.166 2.286.024 3.012a9.5 9.5 0 0 1-.6 1.893c-.179.393-.624 1.156-.82 1.404-.1.128-.1.127-.043-.148ZM7.335 1.952c-.67-.34-1.704-.705-2.276-.803a4 4 0 0 0-.759-.043c-.471.024-.45 0 .306-.358A7.8 7.8 0 0 1 6.47.128c.8-.169 2.306-.17 3.094-.005.85.18 1.853.552 2.418.9l.168.103-.385-.02c-.766-.038-1.88.27-3.078.853-.361.176-.676.316-.699.312a12 12 0 0 1-.654-.319Z", + }, + psn: { + label: "PlayStation", + tint: "#6d8fd6", + path: "M15.858 11.451c-.313.395-1.079.676-1.079.676l-5.696 2.046v-1.509l4.192-1.493c.476-.17.549-.412.162-.538-.386-.127-1.085-.09-1.56.08l-2.794.984v-1.566l.161-.054s.807-.286 1.942-.412c1.135-.125 2.525.017 3.616.43 1.23.39 1.368.962 1.056 1.356M9.625 8.883v-3.86c0-.453-.083-.87-.508-.988-.326-.105-.528.198-.528.65v9.664l-2.606-.827V2c1.108.206 2.722.692 3.59.985 2.207.757 2.955 1.7 2.955 3.825 0 2.071-1.278 2.856-2.903 2.072Zm-8.424 3.625C-.061 12.15-.271 11.41.304 10.984c.532-.394 1.436-.69 1.436-.69l3.737-1.33v1.515l-2.69.963c-.474.17-.547.411-.161.538.386.126 1.085.09 1.56-.08l1.29-.469v1.356l-.257.043a8.45 8.45 0 0 1-4.018-.323Z", + }, +}; + +/** True for a token the launcher knows how to draw. */ +export function isPlatformToken(value: string): value is PlatformToken { + return value === "pc" || value === "xbox" || value === "psn"; +} + +/** + * Renders the platforms a server accepts. + * + * Unknown tokens are skipped rather than drawn as a placeholder: a future + * platform should be invisible here until it has a real mark. + */ +export function PlatformBadges({ + platforms, + size = 14, +}: { + platforms: string[]; + size?: number; +}): JSX.Element | null { + const known = platforms.filter(isPlatformToken); + if (known.length === 0) return null; + + return ( + + {known.map((token) => { + const glyph = GLYPHS[token]; + return ( + + + + + + ); + })} + + ); +} diff --git a/src/components/ReadinessNote.tsx b/src/components/ReadinessNote.tsx new file mode 100644 index 0000000..8a30a82 --- /dev/null +++ b/src/components/ReadinessNote.tsx @@ -0,0 +1,55 @@ +/** + * Mod readiness, as information only. + * + * Deliberately a single line of text and a count strip — not a checklist with a + * "prepare" button. There is no verified way to install Reforger Workshop + * content from outside the game, so the launcher states what it knows and what + * the game will do, and offers no action it cannot perform. See + * `docs/mod-readiness.md`. + */ + +import { readiness } from "../lib/state"; +import { readinessSummary, readinessTone } from "../lib/format"; + +export function ReadinessNote() { + const report = readiness.value; + // Null means the check has not answered yet. Rendering nothing is correct: + // an unknown state must never look like a reassuring one. + if (!report) return null; + + const tone = readinessTone(report); + + return ( +
+

+ {/* A dot carries the state without a coloured edge down the side. */} + + {readinessSummary(report)} +

+ {report.available && report.required > 0 && ( +
+
+
Installed
+
{report.upToDate}
+
+ {report.outdated > 0 && ( +
+
Outdated
+
{report.outdated}
+
+ )} + {report.missing > 0 && ( +
+
Missing
+
{report.missing}
+
+ )} +
+
On disk
+
{report.installedTotal}
+
+
+ )} +
+ ); +} diff --git a/src/components/ServerTable.tsx b/src/components/ServerTable.tsx new file mode 100644 index 0000000..88e7643 --- /dev/null +++ b/src/components/ServerTable.tsx @@ -0,0 +1,296 @@ +/** + * The server table. + * + * Windowed: only the rows intersecting the viewport are in the DOM, so a + * 500-row page costs the same as a 20-row one. Row height is fixed in CSS + * (`--row-h`) and mirrored here, which is what makes the arithmetic cheap + * enough to run on every scroll frame. + */ + +import { useCallback, useEffect, useRef, useState } from "preact/hooks"; +import { + activeSort, + favoriteSet, + pingFor, + selectServer, + selectedId, + sortByColumn, + sortIsPageLocal, + toggleFavorite, +} from "../lib/state"; +import type { SortColumn } from "../lib/sort"; +import { formatPlayers, humaniseRegion, tidyName } from "../lib/format"; +import { PlatformBadges } from "./PlatformBadges"; +import { FavouriteStar, StatusMarks } from "./StatusMarks"; +import type { ServerSummary } from "../lib/types"; + +const ROW_HEIGHT = 30; +/** Rows rendered beyond the viewport, so fast scrolling does not show gaps. */ +const OVERSCAN = 8; + +interface Props { + servers: ServerSummary[]; + /** + * True when these rows are one page of a larger, API-ordered result. + * + * It decides both halves of the sorting behaviour: whether a header click can + * ask the API to reorder the whole dataset, and whether an ordering it cannot + * serve has to be marked as reaching only the loaded page. The favorites and + * recents tables pass false — those lists are resolved in full, so sorting + * them is always complete and never needs the caveat. + */ + paginated?: boolean; +} + +export function ServerTable({ servers, paginated = true }: Props) { + const scrollerRef = useRef(null); + const [scrollTop, setScrollTop] = useState(0); + const [viewportHeight, setViewportHeight] = useState(600); + + useEffect(() => { + const el = scrollerRef.current; + if (!el) return; + const measure = () => setViewportHeight(el.clientHeight); + measure(); + + // ResizeObserver catches layout changes the window never sees, such as the + // inspector collapsing at narrow widths. Fall back to window resize where + // it is unavailable, so the list still sizes correctly. + if (typeof ResizeObserver === "function") { + const observer = new ResizeObserver(measure); + observer.observe(el); + return () => observer.disconnect(); + } + window.addEventListener("resize", measure); + return () => window.removeEventListener("resize", measure); + }, []); + + const onScroll = useCallback((event: Event) => { + setScrollTop((event.currentTarget as HTMLDivElement).scrollTop); + }, []); + + const first = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN); + const visibleCount = Math.ceil(viewportHeight / ROW_HEIGHT) + OVERSCAN * 2; + const last = Math.min(servers.length, first + visibleCount); + const visibleRows = servers.slice(first, last); + + const selected = selectedId.value; + const favorites = favoriteSet.value; + + /** Moves the selection by `delta` rows and keeps it in view. */ + const move = useCallback( + (delta: number) => { + if (servers.length === 0) return; + const current = servers.findIndex((s) => s.id === selectedId.value); + const nextIndex = Math.min( + servers.length - 1, + Math.max(0, current === -1 ? 0 : current + delta), + ); + const next = servers[nextIndex]; + if (!next) return; + void selectServer(next.id); + const el = scrollerRef.current; + if (!el) return; + const top = nextIndex * ROW_HEIGHT; + if (top < el.scrollTop) el.scrollTop = top; + else if (top + ROW_HEIGHT > el.scrollTop + el.clientHeight) { + el.scrollTop = top + ROW_HEIGHT - el.clientHeight; + } + }, + [servers], + ); + + const onKeyDown = useCallback( + (event: KeyboardEvent) => { + switch (event.key) { + case "ArrowDown": + event.preventDefault(); + move(1); + break; + case "ArrowUp": + event.preventDefault(); + move(-1); + break; + case "PageDown": + event.preventDefault(); + move(Math.floor(viewportHeight / ROW_HEIGHT)); + break; + case "PageUp": + event.preventDefault(); + move(-Math.floor(viewportHeight / ROW_HEIGHT)); + break; + case "Home": + event.preventDefault(); + move(-servers.length); + break; + case "End": + event.preventDefault(); + move(servers.length); + break; + } + }, + [move, viewportHeight, servers.length], + ); + + return ( +
+ {/* Inside the scroller, so the header and the rows share one width. With + the header outside, a visible scrollbar narrowed the rows and every + column drifted left of its heading. */} + + ); +} + +interface SortHeaderProps { + column: SortColumn; + label: string; + /** Matches the column's cell alignment, so heading and values line up. */ + align?: "num" | "mid"; + paginated: boolean; +} + +/** + * A clickable column heading. + * + * The arrow is filled when the ordering covers every matching server and hollow + * when it only reorders the rows already loaded — a distinction the API forces, + * since it sorts by `players` and `name` alone and takes no direction. Shape + * rather than colour alone carries it, and the tooltip says it in words. + */ +function SortHeader({ column, label, align, paginated }: SortHeaderProps) { + const active = activeSort.value; + const isActive = active?.column === column; + const pageLocal = isActive && paginated && sortIsPageLocal.value; + const ascending = active?.direction === "asc"; + + let title: string; + if (!isActive) { + title = `Sort by ${label.toLowerCase()}`; + } else if (pageLocal) { + title = `Sorted by ${label.toLowerCase()} — reorders this page only`; + } else if (paginated) { + title = `Sorted by ${label.toLowerCase()} across all matching servers`; + } else { + title = `Sorted by ${label.toLowerCase()}`; + } + + return ( + + ); +} + +interface RowProps { + server: ServerSummary; + top: number; + isSelected: boolean; + isFavorite: boolean; +} + +/** + * Latency to the server's region. + * + * Region, not per-server: the list endpoint publishes no address, so eight + * probes stand in for a whole page. The title says so, and the inspector shows + * the exact per-server figure. + */ +function PingCell({ pingSiteId }: { pingSiteId: string | null }) { + const ms = pingFor(pingSiteId); + if (ms === null) return
—
; + const band = ms < 60 ? "good" : ms < 140 ? "ok" : "far"; + return ( +
+ {ms} +
+ ); +} + +function Row({ server, top, isSelected, isFavorite }: RowProps) { + return ( +
void selectServer(server.id)} + > +
+ void toggleFavorite(server.id)} + /> + {/* Server names are untrusted text and are rendered as text content only. */} + + {tidyName(server.name)} + +
+ + {/* Own column, so the marks line up instead of trailing each name. */} + + +
= server.maxPlayers}> + {formatPlayers(server.players, server.maxPlayers)} +
+ +
+ {server.queue > 0 ? `+${server.queue}` : "—"} +
+ + + +
+ {humaniseRegion(server.pingSiteId ?? server.region)} +
+ +
{server.modCount > 0 ? server.modCount : "—"}
+ + +
+ ); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx new file mode 100644 index 0000000..3ab772e --- /dev/null +++ b/src/components/Sidebar.tsx @@ -0,0 +1,51 @@ +/** Persistent left navigation. */ + +import { favorites, history, recents, setView, view, type View } from "../lib/state"; + +interface Item { + id: View; + label: string; + count?: number; +} + +export function Sidebar() { + const items: Item[] = [ + { id: "browse", label: "Server browser" }, + { id: "favorites", label: "Favorites", count: favorites.value.length }, + { id: "recent", label: "Recent", count: recents.value.length }, + ]; + + return ( + + ); +} diff --git a/src/components/StatusMarks.tsx b/src/components/StatusMarks.tsx new file mode 100644 index 0000000..b84b2b1 --- /dev/null +++ b/src/components/StatusMarks.tsx @@ -0,0 +1,90 @@ +/** + * Row status marks and the favourite star. + * + * These replace the text pills that used to trail the server name. The pills + * started wherever the name happened to end, so they formed a ragged column + * that read as clutter on every row; as icons in a fixed slot they line up and + * become scannable. Glyphs are Bootstrap Icons (MIT), inlined as paths. + */ + +import type { JSX } from "preact"; + +const LOCK = "M8 0a4 4 0 0 1 4 4v2.05a2.5 2.5 0 0 1 2 2.45v5a2.5 2.5 0 0 1-2.5 2.5h-7A2.5 2.5 0 0 1 2 13.5v-5a2.5 2.5 0 0 1 2-2.45V4a4 4 0 0 1 4-4m0 1a3 3 0 0 0-3 3v2h6V4a3 3 0 0 0-3-3"; +const OFFICIAL = "M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01zm.287 5.984-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7 8.793l2.646-2.647a.5.5 0 0 1 .708.708"; +const BATTLEYE = "M5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.8 11.8 0 0 1-2.517 2.453 7 7 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7 7 0 0 1-1.048-.625 11.8 11.8 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 63 63 0 0 1 5.072.56"; +const OFFLINE = "M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M4.5 7.5a.5.5 0 0 0 0 1h7a.5.5 0 0 0 0-1z"; +const STAR_FILLED = "M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z"; +const STAR_OUTLINE = "M2.866 14.85c-.078.444.36.791.746.593l4.39-2.256 4.389 2.256c.386.198.824-.149.746-.592l-.83-4.73 3.522-3.356c.33-.314.16-.888-.282-.95l-4.898-.696L8.465.792a.513.513 0 0 0-.927 0L5.354 5.12l-4.898.696c-.441.062-.612.636-.283.95l3.523 3.356-.83 4.73zm4.905-2.767-3.686 1.894.694-3.957a.56.56 0 0 0-.163-.505L1.71 6.745l4.052-.576a.53.53 0 0 0 .393-.288L8 2.223l1.847 3.658a.53.53 0 0 0 .393.288l4.052.575-2.906 2.77a.56.56 0 0 0-.163.506l.694 3.957-3.686-1.894a.5.5 0 0 0-.461 0z"; + +function Glyph({ path, size }: { path: string; size: number }) { + return ( + + ); +} + +/** + * The status marks for one server, in a fixed order. + * + * Order is constant so a given mark always occupies the same position in the + * slot, which is what lets the eye skip the column rather than read it. + */ +export function StatusMarks({ server }: { + server: { + official: boolean; + passwordProtected: boolean; + battlEye: boolean; + online: boolean; + }; +}): JSX.Element { + return ( + + {server.official && ( + + + + )} + {server.passwordProtected && ( + + + + )} + {server.battlEye && ( + + + + )} + {!server.online && ( + + + + )} + + ); +} + +/** Favourite toggle. */ +export function FavouriteStar({ + isFavorite, + onToggle, +}: { + isFavorite: boolean; + onToggle: () => void; +}): JSX.Element { + return ( + + ); +} diff --git a/src/components/StatusPill.tsx b/src/components/StatusPill.tsx new file mode 100644 index 0000000..2bafc69 --- /dev/null +++ b/src/components/StatusPill.tsx @@ -0,0 +1,84 @@ +/** + * Compact API status for the header. + * + * Reads `nowMs`, which ticks once a second, so the "updated Ns ago" figure stays + * live. Because signals subscribe per component, that tick re-renders this pill + * and nothing else — the server table is untouched. + */ + +import { nowMs, status } from "../lib/state"; +import { formatAge } from "../lib/format"; + +export function StatusPill() { + const s = status.value; + // Subscribe to the clock so the age figure advances. + const now = nowMs.value; + + if (!s) { + return ( +
+ + Checking API… +
+ ); + } + + if (!s.apiReachable) { + return ( +
+ + API unreachable +
+ ); + } + + const dataset = s.dataset; + // `snapshotAgeSeconds` was measured when the response was produced; advance it + // by the time since, so the figure keeps counting between requests. + const measuredAtMs = datasetMeasuredAt(dataset?.snapshotAgeSeconds); + const age = + dataset && measuredAtMs !== null + ? dataset.snapshotAgeSeconds + (now - measuredAtMs) / 1000 + : null; + + const stale = dataset?.stale === true; + const warming = dataset?.warming === true; + const dotClass = warming || stale ? "status__dot--warn" : "status__dot--ok"; + + return ( +
+ + {s.reportedStatus ? `API ${s.reportedStatus}` : "API operational"} + {dataset && ( + <> + + + {warming + ? "Data warming up" + : age === null + ? "Data age unknown" + : `Updated ${formatAge(age)}`} + + + )} +
+ ); +} + +/** + * Wall-clock time the current `snapshotAgeSeconds` reading was taken. + * + * Tracked here rather than in state because it exists only to make the label + * tick; it is reset whenever the reading changes. + */ +let lastAgeSeconds: number | null = null; +let lastAgeAtMs: number | null = null; + +function datasetMeasuredAt(ageSeconds: number | undefined): number | null { + if (ageSeconds === undefined) return null; + if (ageSeconds !== lastAgeSeconds) { + lastAgeSeconds = ageSeconds; + lastAgeAtMs = Date.now(); + } + return lastAgeAtMs; +} diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx new file mode 100644 index 0000000..f05a164 --- /dev/null +++ b/src/components/TitleBar.tsx @@ -0,0 +1,98 @@ +/** + * Custom window title bar. + * + * The native Windows caption plus our own brand strip meant two stacked bars + * doing one job. This is a single row: brand, context, status, window controls. + * The window is frameless (`decorations: false`), so dragging and the caption + * buttons are wired here. + */ + +import { getCurrentWindow } from "@tauri-apps/api/window"; +import { StatusPill } from "./StatusPill"; +import { fleet, totalServers, view } from "../lib/state"; +import brandMark from "../assets/brand-mark.png"; + +/** + * Resolved lazily: `getCurrentWindow()` reads Tauri internals that only exist + * inside the app, so calling it at module scope would break any environment + * that merely imports this file. + */ +function windowAction(action: "minimize" | "toggleMaximize" | "close"): void { + try { + void getCurrentWindow()[action](); + } catch { + // No Tauri window (tests, or a browser preview): nothing to do. + } +} + +export function TitleBar() { + return ( +
+ {/* The drag region must be the element itself, not a child, or the + draggable area ends up shaped like the text inside it. */} + + {/* Wordmark split and coloured as on reforgermods.net. */} + + reforger + mods + .net + launcher + + + + {view.value === "browse" && totalServers.value > 0 + ? `${totalServers.value.toLocaleString()} servers` + : ""} + {fleet.value && fleet.value.players > 0 && ( + <> + {view.value === "browse" && totalServers.value > 0 && ( + + )} + {`${fleet.value.players.toLocaleString()} players online`} + + )} + + +
+ + +
+ + + +
+
+ ); +} diff --git a/src/lib/chart.test.ts b/src/lib/chart.test.ts new file mode 100644 index 0000000..6746e17 --- /dev/null +++ b/src/lib/chart.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from "vitest"; +import { buildChart } from "./chart"; +import type { ServerHistoryPoint } from "./types"; + +function point(t: number, min: number, avg: number, max: number, cap = 128): ServerHistoryPoint { + return { t, min, avg, max, cap }; +} + +describe("buildChart", () => { + it("refuses to draw a series too short to be a line", () => { + expect(buildChart([], 300, 56)).toBeNull(); + expect(buildChart([point(0, 1, 1, 1)], 300, 56)).toBeNull(); + }); + + it("refuses a zero-sized box", () => { + const points = [point(0, 0, 0, 0), point(300, 1, 1, 1)]; + expect(buildChart(points, 0, 56)).toBeNull(); + expect(buildChart(points, 300, 0)).toBeNull(); + }); + + it("spans the full width and inverts the Y axis", () => { + const points = [point(0, 0, 0, 0), point(300, 10, 10, 10)]; + const chart = buildChart(points, 300, 56); + expect(chart).not.toBeNull(); + // Average line: starts bottom-left at the zero bucket, ends top-right at the peak. + expect(chart?.line).toBe("M0,56L300,0"); + }); + + it("closes the min-max band", () => { + const points = [point(0, 1, 2, 3), point(300, 1, 2, 3)]; + const chart = buildChart(points, 300, 60); + // Upper edge left-to-right, lower edge back, closed. + expect(chart?.band).toBe("M0,0L300,0L300,40L0,40Z"); + }); + + it("scales to the observed peak, not to capacity", () => { + // A server peaking at 8 of 128 must not draw as a flat line on the axis. + const points = [point(0, 0, 4, 8), point(300, 2, 3, 4)]; + const chart = buildChart(points, 300, 100); + expect(chart?.peak).toBe(8); + expect(chart?.cap).toBe(128); + // The 8-player peak reaches the top of the box. + expect(chart?.line.startsWith("M0,50")).toBe(true); + expect(chart?.band.startsWith("M0,0")).toBe(true); + }); + + it("survives an all-zero series without dividing by zero", () => { + const points = [point(0, 0, 0, 0), point(300, 0, 0, 0), point(600, 0, 0, 0)]; + const chart = buildChart(points, 300, 56); + expect(chart?.peak).toBe(0); + expect(chart?.line).not.toContain("NaN"); + expect(chart?.band).not.toContain("NaN"); + }); + + it("reports the summary figures the legend renders", () => { + const points = [point(100, 0, 10, 20), point(200, 5, 15, 25), point(300, 1, 2, 3)]; + const chart = buildChart(points, 300, 56); + expect(chart?.peak).toBe(25); + expect(chart?.low).toBe(0); + expect(chart?.mean).toBe(9); // (10 + 15 + 2) / 3 + expect(chart?.from).toBe(100); + expect(chart?.to).toBe(300); + }); + + it("falls back to zero capacity when no bucket recorded one", () => { + const points = [point(0, 1, 1, 1, 0), point(300, 1, 1, 1, 0)]; + expect(buildChart(points, 300, 56)?.cap).toBe(0); + }); + + it("emits compact path data", () => { + const points = Array.from({ length: 288 }, (_, i) => point(i * 300, i % 7, i % 11, i % 13)); + const chart = buildChart(points, 300, 56); + // Two decimals max, so 288 buckets stay a reasonable DOM string. + expect(chart?.line).not.toMatch(/\.\d{3}/); + expect((chart?.line.length ?? 0)).toBeLessThan(4000); + }); +}); diff --git a/src/lib/chart.ts b/src/lib/chart.ts new file mode 100644 index 0000000..b38f05b --- /dev/null +++ b/src/lib/chart.ts @@ -0,0 +1,82 @@ +/** + * Geometry for the population history chart. + * + * Pure: it turns history buckets into SVG path strings and nothing else, so the + * scaling rules are unit-testable without rendering anything. + * + * The Y axis scales to the observed peak rather than to slot capacity. A server + * that peaks at 8 of 128 would otherwise draw as a flat line on the axis; the + * capacity is stated in the label instead, where it reads better anyway. + */ + +import type { ServerHistoryPoint } from "./types"; + +export interface ChartGeometry { + /** Filled area between each bucket's min and max. */ + band: string; + /** Line through each bucket's average. */ + line: string; + /** Highest `max` in the series, which is the top of the Y scale. */ + peak: number; + /** Lowest `min` in the series. */ + low: number; + /** Average of the bucket averages, rounded. */ + mean: number; + /** Slot capacity, from the last bucket that recorded one. 0 when unknown. */ + cap: number; + /** First and last bucket timestamps, unix seconds. */ + from: number; + to: number; +} + +/** + * Builds the chart paths for `points` inside a `width` x `height` box. + * + * Returns `null` for a series too short to draw, which the caller renders as an + * empty state rather than an axis with nothing on it. + */ +export function buildChart( + points: ServerHistoryPoint[], + width: number, + height: number, +): ChartGeometry | null { + if (points.length < 2 || width <= 0 || height <= 0) return null; + + const peak = points.reduce((acc, p) => Math.max(acc, p.max), 0); + const low = points.reduce((acc, p) => Math.min(acc, p.min), Number.POSITIVE_INFINITY); + const meanRaw = points.reduce((acc, p) => acc + p.avg, 0) / points.length; + const cap = points.reduce((acc, p) => (p.cap > 0 ? p.cap : acc), 0); + + // An all-zero series still needs a finite scale, or every point divides by zero. + const scaleMax = peak > 0 ? peak : 1; + const lastIndex = points.length - 1; + + const x = (i: number) => round((i / lastIndex) * width); + const y = (value: number) => round(height - (value / scaleMax) * height); + + // Upper edge left-to-right, lower edge right-to-left, closed. + const upper = points.map((p, i) => `${i === 0 ? "M" : "L"}${x(i)},${y(p.max)}`); + const lower = points + .slice() + .reverse() + .map((p, i) => `L${x(lastIndex - i)},${y(p.min)}`); + const band = `${upper.join("")}${lower.join("")}Z`; + + const line = points.map((p, i) => `${i === 0 ? "M" : "L"}${x(i)},${y(p.avg)}`).join(""); + + return { + band, + line, + peak, + low: Number.isFinite(low) ? low : 0, + mean: Math.round(meanRaw), + cap, + from: points[0]?.t ?? 0, + to: points[lastIndex]?.t ?? 0, + }; +} + +/** Two decimals is plenty for path data and keeps the DOM string small. */ +function round(value: number): number { + return Math.round(value * 100) / 100; +} diff --git a/src/lib/format.test.ts b/src/lib/format.test.ts new file mode 100644 index 0000000..5944e53 --- /dev/null +++ b/src/lib/format.test.ts @@ -0,0 +1,323 @@ +import { describe, expect, it } from "vitest"; +import { + discordInviteFrom, + formatAge, + formatBytes, + formatPlayers, + formatTimestamp, + humaniseRegion, + readinessSummary, + safeImageUrl, + readinessTone, + tidyName, +} from "./format"; + +describe("formatBytes", () => { + it("uses binary units", () => { + expect(formatBytes(0)).toBe("0 B"); + expect(formatBytes(512)).toBe("512 B"); + expect(formatBytes(1024)).toBe("1.0 KiB"); + expect(formatBytes(1024 * 1024)).toBe("1.0 MiB"); + // The real figure from a 122-mod server. + expect(formatBytes(18870301863)).toBe("17.6 GiB"); + }); + + it("drops the decimal at three digits so columns stay narrow", () => { + expect(formatBytes(1024 * 200)).toBe("200 KiB"); + expect(formatBytes(1024 * 99.5)).toBe("99.5 KiB"); + }); + + it("handles nonsense input without throwing", () => { + expect(formatBytes(-1)).toBe("0 B"); + expect(formatBytes(Number.NaN)).toBe("0 B"); + expect(formatBytes(Number.POSITIVE_INFINITY)).toBe("0 B"); + }); +}); + +describe("formatAge", () => { + it("renders the header's freshness label", () => { + expect(formatAge(18)).toBe("18s ago"); + expect(formatAge(0.4)).toBe("just now"); + expect(formatAge(59.9)).toBe("59s ago"); + expect(formatAge(60)).toBe("1m ago"); + expect(formatAge(3600)).toBe("1h ago"); + expect(formatAge(86_400)).toBe("1d ago"); + }); + + it("reports unknown rather than a negative age", () => { + expect(formatAge(-5)).toBe("unknown"); + expect(formatAge(Number.NaN)).toBe("unknown"); + }); +}); + +describe("formatTimestamp", () => { + it("is relative to the supplied clock", () => { + const now = 1_700_000_000_000; + expect(formatTimestamp(now / 1000 - 120, now)).toBe("2m ago"); + }); +}); + +describe("humaniseRegion", () => { + it("turns ping-site ids into labels", () => { + expect(humaniseRegion("new_york")).toBe("New York"); + expect(humaniseRegion("frankfurt")).toBe("Frankfurt"); + expect(humaniseRegion("los_angeles")).toBe("Los Angeles"); + }); + + it("falls back for an absent region", () => { + expect(humaniseRegion(null)).toBe("—"); + expect(humaniseRegion("")).toBe("—"); + expect(humaniseRegion(undefined)).toBe("—"); + }); +}); + + +describe("formatPlayers", () => { + it("renders the population cell", () => { + expect(formatPlayers(12, 128)).toBe("12 / 128"); + }); +}); + + +describe("tidyName", () => { + it("collapses whitespace in untrusted server names", () => { + expect(tidyName(" [EU1] Some Server \n\t ")).toBe("[EU1] Some Server"); + }); + + it("leaves ordinary names alone", () => { + expect(tidyName("[OGR2] Old Guard Revival 2")).toBe("[OGR2] Old Guard Revival 2"); + }); + + it("does not attempt to sanitise markup, which is rendered as text", () => { + // The value is set as text content, so angle brackets are inert; the + // function's job is layout, not escaping. + expect(tidyName("x")).toBe("x"); + }); +}); + +describe("readinessSummary", () => { + it("reads as sentences, not dot-separated fragments", () => { + // The joined-with-middots form read as machine output. + expect(readinessSummary(base)).not.toContain("·"); + }); + + const base = { + available: true, + required: 136, + upToDate: 39, + outdated: 0, + missing: 96, + missingBytes: 8_772_055_531, + unresolvedMods: 1, + installedTotal: 235, + }; + + it("produces the intended sentence for a partly-installed server", () => { + // "at least" because one mod's size is unresolved; the closing clause + // states what the game does, because the launcher cannot do it. + expect(readinessSummary(base)).toBe( + "96 of 136 mods missing, at least 8.17 GiB to download. " + + "Arma Reforger downloads these when you join.", + ); + }); + + it("drops 'at least' once every size is resolved", () => { + const summary = readinessSummary({ ...base, unresolvedMods: 0 }); + expect(summary).toContain("8.17 GiB to download"); + expect(summary).not.toContain("at least"); + }); + + it("never presents an unavailable scan as a verdict", () => { + const summary = readinessSummary({ + available: false, + unavailable: "no_addons_directory", + message: "No local Arma Reforger addon folder found, so installed mods could not be checked.", + required: 0, + upToDate: 0, + outdated: 0, + missing: 0, + missingBytes: 0, + unresolvedMods: 0, + installedTotal: 0, + }); + expect(summary).toContain("could not be checked"); + expect(summary).not.toContain("up to date"); + expect(summary).not.toContain("missing"); + }); + + it("falls back to generic copy if the backend sent no message", () => { + const summary = readinessSummary({ ...base, available: false, message: undefined }); + expect(summary).toBe("Installed mods could not be checked."); + }); + + it("reports a fully-installed server plainly", () => { + expect( + readinessSummary({ ...base, upToDate: 136, outdated: 0, missing: 0, missingBytes: 0, unresolvedMods: 0 }), + ).toBe("All 136 mods installed and up to date."); + }); + + it("says update, not download, when only versions differ", () => { + const summary = readinessSummary({ + ...base, + upToDate: 135, + outdated: 1, + missing: 0, + missingBytes: 0, + unresolvedMods: 0, + }); + expect(summary).toBe("1 of 136 mods out of date. Arma Reforger updates these when you join."); + }); + + it("combines missing and outdated counts", () => { + const summary = readinessSummary({ ...base, outdated: 1, unresolvedMods: 0 }); + expect(summary).toContain("96 missing and 1 out of date of 136 mods"); + }); + + it("says the size is unknown rather than showing 0 B", () => { + const summary = readinessSummary({ ...base, missingBytes: 0, unresolvedMods: 96 }); + expect(summary).toContain("download size unknown"); + expect(summary).not.toContain("0 B"); + }); + + it("handles a vanilla server", () => { + expect(readinessSummary({ ...base, required: 0, missing: 0, missingBytes: 0, unresolvedMods: 0 })).toBe( + "No mods required.", + ); + }); +}); + +describe("readinessTone", () => { + const base = { + available: true, + required: 10, + upToDate: 10, + outdated: 0, + missing: 0, + missingBytes: 0, + unresolvedMods: 0, + installedTotal: 20, + }; + + it("never reads as ready when the scan did not run", () => { + expect(readinessTone({ ...base, available: false })).toBe("unknown"); + }); + + it("distinguishes ready from needing action", () => { + expect(readinessTone(base)).toBe("ready"); + expect(readinessTone({ ...base, missing: 1 })).toBe("action"); + expect(readinessTone({ ...base, outdated: 1 })).toBe("action"); + }); +}); + +describe("formatBytes precision override", () => { + it("keeps the compact default for table cells", () => { + expect(formatBytes(18870301863)).toBe("17.6 GiB"); + }); + + it("honours an explicit precision for download figures", () => { + expect(formatBytes(8_772_055_531, 2)).toBe("8.17 GiB"); + expect(formatBytes(8_772_055_531, 0)).toBe("8 GiB"); + }); + + it("never puts decimals on whole bytes", () => { + expect(formatBytes(500, 2)).toBe("500 B"); + }); +}); + +describe("safeImageUrl", () => { + const real = + "https://ar-gcp-cdn.bistudio.com/image/b64d/02af30860106e7097a8238852468fb85d249062ae56b43b1d6c34e70c20f/45316.jpg"; + + it("accepts the CDN the API actually uses", () => { + // Verified across 300 servers: every image URL was https on this host. + expect(safeImageUrl(real)).toBe(real); + }); + + it("treats an absent image as absent, not an error", () => { + // About half of servers publish no imagery at all. + expect(safeImageUrl(null)).toBeNull(); + expect(safeImageUrl(undefined)).toBeNull(); + expect(safeImageUrl("")).toBeNull(); + }); + + it("refuses non-https schemes", () => { + expect(safeImageUrl("http://ar-gcp-cdn.bistudio.com/a.jpg")).toBeNull(); + expect(safeImageUrl("data:image/png;base64,iVBORw0KGgo=")).toBeNull(); + expect(safeImageUrl("javascript:alert(1)")).toBeNull(); + expect(safeImageUrl("file:///etc/passwd")).toBeNull(); + }); + + it("refuses lookalike and subdomain-suffix hosts", () => { + for (const url of [ + "https://evil.test/a.jpg", + "https://ar-gcp-cdn.bistudio.com.attacker.test/a.jpg", + "https://notar-gcp-cdn.bistudio.com/a.jpg", + "https://sub.ar-gcp-cdn.bistudio.com/a.jpg", + "https://ar-gcp-cdn.bistudio.com@evil.test/a.jpg", + ]) { + expect(safeImageUrl(url), url).toBeNull(); + } + }); + + it("refuses unparseable input", () => { + expect(safeImageUrl("not a url")).toBeNull(); + expect(safeImageUrl("//ar-gcp-cdn.bistudio.com/a.jpg")).toBeNull(); + }); +}); + + +describe("discordInviteFrom", () => { + it("finds invites in real server names", () => { + for (const [name, code] of [ + ["[OGR2] Old Guard Revival | discord.gg/oldguardarma", "oldguardarma"], + ["[AU] FTA #2 | From The Ashes | discord.gg/from-the-ashes", "from-the-ashes"], + ["EXD.gg 1PP | https://discord.gg/AbC_123", "AbC_123"], + ["Server | https://www.discord.com/invite/xyz789", "xyz789"], + ["Server | discordapp.com/invite/legacy1", "legacy1"], + ] as const) { + expect(discordInviteFrom(name)?.code, name).toBe(code); + } + }); + + it("rebuilds the URL from the code rather than trusting the text", () => { + const found = discordInviteFrom("Join us at discord.gg/realcode today"); + expect(found?.code).toBe("realcode"); + expect(found?.url).toBe("https://discord.gg/realcode"); + }); + + it("refuses a code followed by characters that could redirect it", () => { + // `discord.gg/code@evil.test` is not an invite; the trailing-boundary rule + // rejects it outright rather than extracting a code from it. + expect(discordInviteFrom("discord.gg/realcode@evil.test/path")).toBeNull(); + expect(discordInviteFrom("discord.gg/realcode%2Fevil")).toBeNull(); + }); + + it("returns nothing when there is no invite", () => { + for (const name of [ + "", + "[EU1] Plain Server Name", + "discord.gg/", + "discord.gg/x", + "https://example.com/invite/abcdef", + ]) { + expect(discordInviteFrom(name), name).toBeNull(); + } + expect(discordInviteFrom(null)).toBeNull(); + expect(discordInviteFrom(undefined)).toBeNull(); + }); + + it("matches the site's regex, including its lack of a left boundary", () => { + // Ported verbatim from reforgermods.net so the button appears on exactly + // the same servers. A lookalike host is a known false positive upstream; + // it is harmless here because the URL is rebuilt from the code alone. + expect(discordInviteFrom("not-discord.gg/abcdef")?.url).toBe( + "https://discord.gg/abcdef", + ); + }); + + it("stops the code at a trailing separator", () => { + expect(discordInviteFrom("come to discord.gg/abc123, we play daily")?.code).toBe("abc123"); + expect(discordInviteFrom("discord.gg/abc123.")?.code).toBe("abc123"); + expect(discordInviteFrom("(discord.gg/abc123)")?.code).toBe("abc123"); + }); +}); diff --git a/src/lib/format.ts b/src/lib/format.ts new file mode 100644 index 0000000..51ec275 --- /dev/null +++ b/src/lib/format.ts @@ -0,0 +1,199 @@ +/** + * Pure display helpers. + * + * Kept free of DOM and Tauri imports so the formatting rules the UI depends on + * can be unit-tested directly. + */ + +import type { ReadinessReport } from "./types"; + +/** + * Formats a byte count for a compact desktop layout. + * + * `digits` overrides the default precision. Table cells want one decimal so + * columns stay narrow; a download figure the user is about to commit to wants + * two, because 0.01 GiB is still 10 MB. + */ +export function formatBytes(bytes: number, digits?: number): string { + if (!Number.isFinite(bytes) || bytes <= 0) return "0 B"; + const units = ["B", "KiB", "MiB", "GiB", "TiB"]; + let value = bytes; + let unit = 0; + while (value >= 1024 && unit < units.length - 1) { + value /= 1024; + unit += 1; + } + // Whole bytes read oddly with a decimal, whatever the caller asked for. + const decimals = unit === 0 ? 0 : (digits ?? (value >= 100 ? 0 : 1)); + return `${value.toFixed(decimals)} ${units[unit]}`; +} + +/** + * Formats an age in seconds as a short relative label, e.g. `18s ago`. + * + * Returns `just now` under a second so the header never flashes `0s ago`. + */ +export function formatAge(seconds: number): string { + if (!Number.isFinite(seconds) || seconds < 0) return "unknown"; + const s = Math.floor(seconds); + if (s < 1) return "just now"; + if (s < 60) return `${s}s ago`; + const m = Math.floor(s / 60); + if (m < 60) return `${m}m ago`; + const h = Math.floor(m / 60); + if (h < 24) return `${h}h ago`; + return `${Math.floor(h / 24)}d ago`; +} + +/** Formats a unix-seconds timestamp as an age relative to `now`. */ +export function formatTimestamp(unixSeconds: number, now = Date.now()): string { + return formatAge(now / 1000 - unixSeconds); +} + +/** + * Turns a ping-site id into a readable label when the API has not supplied one, + * e.g. `new_york` -> `New York`. + */ +export function humaniseRegion(id: string | null | undefined): string { + if (!id) return "—"; + return id + .split(/[_-]/) + .filter(Boolean) + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); +} + +/** `12 / 128`, with the queue appended when one exists. */ +export function formatPlayers(players: number, max: number): string { + return `${players} / ${max}`; +} + + + +/** + * Collapses whitespace in untrusted server names so a name padded with newlines + * cannot break the table layout. This is a layout measure, not a security one: + * the value is still rendered as text content, never as markup. + */ +export function tidyName(name: string): string { + return name.replace(/\s+/g, " ").trim(); +} + +/** + * The one-line mod readiness summary. + * + * Deliberately pure and directly tested, because this is the copy that has to + * stay honest: + * + * - "at least" appears whenever any required mod's size is unresolved, so a + * floor is never presented as a total. + * - An unavailable scan reports that it is unavailable. It never falls through + * to a reassuring message. + * - The closing clause states what actually happens — the game downloads the + * missing content during the join — because the launcher cannot do it. There + * is no "prepare mods" action to imply. + */ +export function readinessSummary(report: ReadinessReport): string { + if (!report.available) { + return report.message ?? "Installed mods could not be checked."; + } + if (report.required === 0) { + return "No mods required."; + } + if (report.missing === 0 && report.outdated === 0) { + return `All ${report.required} mods installed and up to date.`; + } + + let counts: string; + if (report.missing > 0 && report.outdated > 0) { + counts = `${report.missing} missing and ${report.outdated} out of date of ${report.required} mods`; + } else if (report.missing > 0) { + counts = `${report.missing} of ${report.required} mods missing`; + } else { + counts = `${report.outdated} of ${report.required} mods out of date`; + } + + if (report.missing > 0) { + if (report.missingBytes > 0) { + const size = formatBytes(report.missingBytes, 2); + // "at least" whenever a required mod's size is unresolved, so a floor is + // never presented as a total. + counts += + report.unresolvedMods > 0 + ? `, at least ${size} to download` + : `, ${size} to download`; + } else if (report.unresolvedMods > 0) { + counts += ", download size unknown"; + } + } + + const actor = + report.missing > 0 + ? "Arma Reforger downloads these when you join." + : "Arma Reforger updates these when you join."; + return `${counts}. ${actor}`; +} + +/** Severity for the readiness line, driving its colour only. */ +export function readinessTone(report: ReadinessReport): "unknown" | "ready" | "action" { + if (!report.available) return "unknown"; + return report.missing === 0 && report.outdated === 0 ? "ready" : "action"; +} + +/** + * The only host the API serves scenario imagery from. + * + * Verified across 300 servers: every image URL was `https` on this exact host, + * and about half of servers publish no image at all. + */ +const IMAGE_HOST = "ar-gcp-cdn.bistudio.com"; + +/** + * Returns `url` only if it is safe to put in an `img` tag. + * + * Image URLs arrive from the API and are therefore untrusted. This is the first + * of two layers: the page's CSP `img-src` is the enforcing one and would block + * anything else regardless, but refusing here means the launcher never even + * attempts a request to a host it did not expect. + * + * Anything that is not `https` on the known CDN host — including `data:`, + * `javascript:`, a redirect through another origin, or a lookalike domain — + * returns `null`, which renders as no image. + */ +export function safeImageUrl(url: string | null | undefined): string | null { + if (!url) return null; + let parsed: URL; + try { + parsed = new URL(url); + } catch { + return null; + } + if (parsed.protocol !== "https:") return null; + // Exact host match: `evil-ar-gcp-cdn.bistudio.com.attacker.test` must not pass. + if (parsed.hostname !== IMAGE_HOST) return null; + return parsed.toString(); +} + + +/** + * Finds a Discord invite in untrusted server text. + * + * The pattern is ported verbatim from reforgermods.net's own + * `discordInviteFromText`, so a server showing a Join Discord button on the site + * shows one here too. + * + * Only the invite **code** is taken from the text; the URL is rebuilt from it. + * Nothing a server operator writes in their name can steer where the button + * goes — a name containing `discord.gg/x@evil.test` yields a link to `x`. + */ +export function discordInviteFrom(text: string | null | undefined): { + code: string; + url: string; +} | null { + const match = String(text ?? "").match( + /(?:https?:\/\/)?(?:www\.)?(?:discord\.gg|discord(?:app)?\.com\/invite)\/([A-Za-z0-9_-]{2,64})(?=$|[\s)\]}<>"'|,;:?!./])/i, + ); + const code = match?.[1]; + if (!code) return null; + return { code, url: `https://discord.gg/${encodeURIComponent(code)}` }; +} diff --git a/src/lib/ipc.ts b/src/lib/ipc.ts new file mode 100644 index 0000000..d459f9f --- /dev/null +++ b/src/lib/ipc.ts @@ -0,0 +1,129 @@ +/** + * Typed wrappers around the Tauri command surface. + * + * Components call these, never `invoke` directly, so every command name and + * argument shape is declared once. + */ + +import { invoke } from "@tauri-apps/api/core"; +import type { + FleetNow, + HistoryRange, + InstallIdentity, + LauncherStatus, + LocalStateDto, + PingResult, + PingSite, + ReadinessReport, + ServerDetail, + ServerHistory, + ServerSummary, + ServerListResult, + ServerQuery, + Settings, +} from "./types"; + +export function listServers(query: ServerQuery): Promise { + return invoke("list_servers", { query }); +} + +export function getServer(id: string): Promise { + return invoke("get_server", { id }); +} + +/** Resolves server ids to current summaries, dropping any that no longer exist. */ +export function getServers(ids: string[]): Promise { + return invoke("get_servers", { ids }); +} + +export function getServerHistory(id: string, range: HistoryRange): Promise { + return invoke("get_server_history", { id, range }); +} + +/** + * Compares the server's required mods against the local addon store. + * + * Reporting only — there is no command that acts on the result. + */ +export function getModReadiness(id: string): Promise { + return invoke("get_mod_readiness", { id }); +} + +/** + * Measures latency to a server by ICMP echo. + * + * Resolves with a result that states why there is no figure when the host does + * not answer; it does not reject for an unreachable server. + */ +export function pingServer(id: string): Promise { + return invoke("ping_server", { id }); +} + +/** + * Latency to every ping site, keyed by site id. + * + * Eight probes cover an entire page, because `/v2/servers` publishes + * `pingSiteId` per row but no per-server address. + */ +export function pingRegions(): Promise> { + return invoke("ping_regions"); +} + +/** Players online across the whole fleet. */ +export function getFleetNow(): Promise { + return invoke("get_fleet_now"); +} + +export function listPingSites(): Promise { + return invoke("list_ping_sites"); +} + +export function getStatus(): Promise { + return invoke("get_status"); +} + +/** Hands a validated join URI to Steam. Resolves with the URI that was opened. */ +export function joinServer(id: string, name: string): Promise { + return invoke("join_server", { id, name }); +} + +export function launchGame(): Promise { + return invoke("launch_game"); +} + +export function openServerPage(id: string): Promise { + return invoke("open_server_page", { id }); +} + +/** Opens a Discord invite by code. The URL is rebuilt in Rust. */ +export function openDiscordInvite(code: string): Promise { + return invoke("open_discord_invite", { code }); +} + +export function getLocalState(): Promise { + return invoke("get_local_state"); +} + +/** Returns the new favorite state for `id`. */ +export function toggleFavorite(id: string): Promise { + return invoke("toggle_favorite", { id }); +} + +/** Returns the settings as stored, after clamping. */ +export function updateSettings(settings: Settings): Promise { + return invoke("update_settings", { settings }); +} + +export function clearHistory(): Promise { + return invoke("clear_history"); +} + +/** True when the launcher was started with RFM_DEBUG set. */ +export function isDebugMode(): Promise { + return invoke("is_debug_mode"); +} + +/** Install identity, for debug mode. Empty outside it. */ +export function getInstallIdentity(): Promise { + return invoke("get_install_identity"); +} diff --git a/src/lib/sort.test.ts b/src/lib/sort.test.ts new file mode 100644 index 0000000..307fc1b --- /dev/null +++ b/src/lib/sort.test.ts @@ -0,0 +1,218 @@ +/** + * Table ordering. + * + * The cases that matter here are the ones where a plausible implementation is + * quietly wrong: unmeasured latency presented as fast, a direction inherited + * from the previous column, or equal values reshuffling on every refresh. + */ + +import { describe, expect, it } from "vitest"; +import { + API_SORTABLE, + isDatasetWide, + nextSort, + sortRows, + type PingLookup, + type TableSort, +} from "./sort"; +import type { ServerSummary } from "./types"; + +function server(overrides: Partial & { id: string }): ServerSummary { + return { + name: `server ${overrides.id}`, + scenarioId: "{ABC}Missions/Conflict.conf", + scenarioName: "Conflict", + scenarioThumbnailUrl: null, + gameVersion: "1.8.0.13", + official: false, + passwordProtected: false, + battlEye: true, + crossPlay: true, + players: 0, + maxPlayers: 128, + queue: 0, + queueMax: 25, + pingSiteId: null, + region: "new_york", + platform: "LINUX", + platforms: ["pc"], + modCount: 0, + online: true, + firstSeen: null, + lastSeen: null, + analytics: null, + joinUrl: null, + ...overrides, + }; +} + +/** No region was ever measured. */ +const noPings: PingLookup = () => null; + +const pings: PingLookup = (id) => + ({ frankfurt: 22, new_york: 95, sydney: 280 })[id ?? ""] ?? null; + +const ids = (rows: ServerSummary[]) => rows.map((r) => r.id); + +describe("nextSort", () => { + it("starts a column at the direction that answers the obvious question", () => { + // Busiest, most modded and lowest ping first; names from A. + expect(nextSort(null, "players")).toEqual({ column: "players", direction: "desc" }); + expect(nextSort(null, "mods")).toEqual({ column: "mods", direction: "desc" }); + expect(nextSort(null, "queue")).toEqual({ column: "queue", direction: "desc" }); + expect(nextSort(null, "ping")).toEqual({ column: "ping", direction: "asc" }); + expect(nextSort(null, "name")).toEqual({ column: "name", direction: "asc" }); + expect(nextSort(null, "region")).toEqual({ column: "region", direction: "asc" }); + }); + + it("reverses the column already sorted", () => { + const current: TableSort = { column: "players", direction: "desc" }; + expect(nextSort(current, "players")).toEqual({ column: "players", direction: "asc" }); + }); + + it("does not carry a direction across to a different column", () => { + // Otherwise clicking players (high–low) then name would sort names Z–A. + const current: TableSort = { column: "players", direction: "desc" }; + expect(nextSort(current, "name")).toEqual({ column: "name", direction: "asc" }); + }); +}); + +describe("isDatasetWide", () => { + it("is true only for the orderings the API itself serves", () => { + expect(isDatasetWide({ column: "players", direction: "desc" })).toBe(true); + expect(isDatasetWide({ column: "name", direction: "asc" })).toBe(true); + }); + + it("is false in the reverse direction, which the API has no parameter for", () => { + expect(isDatasetWide({ column: "players", direction: "asc" })).toBe(false); + expect(isDatasetWide({ column: "name", direction: "desc" })).toBe(false); + }); + + it("is false for every column the API cannot sort by", () => { + for (const column of ["queue", "ping", "mods", "region"] as const) { + expect(isDatasetWide({ column, direction: "desc" })).toBe(false); + expect(isDatasetWide({ column, direction: "asc" })).toBe(false); + expect(API_SORTABLE[column]).toBeUndefined(); + } + }); +}); + +describe("sortRows", () => { + it("returns the rows untouched when nothing is sorted", () => { + const rows = [server({ id: "a" }), server({ id: "b" })]; + // Identity, not just equality: the browse view re-renders on a new array. + expect(sortRows(rows, null, noPings)).toBe(rows); + }); + + it("does not mutate the array it was given", () => { + const rows = [server({ id: "a", players: 1 }), server({ id: "b", players: 9 })]; + sortRows(rows, { column: "players", direction: "desc" }, noPings); + expect(ids(rows)).toEqual(["a", "b"]); + }); + + it("orders by players in both directions", () => { + const rows = [ + server({ id: "mid", players: 40 }), + server({ id: "high", players: 120 }), + server({ id: "low", players: 3 }), + ]; + expect(ids(sortRows(rows, { column: "players", direction: "desc" }, noPings))).toEqual([ + "high", + "mid", + "low", + ]); + expect(ids(sortRows(rows, { column: "players", direction: "asc" }, noPings))).toEqual([ + "low", + "mid", + "high", + ]); + }); + + it("orders by name on the tidied text, as shown", () => { + const rows = [ + server({ id: "b", name: " [EU] Bravo " }), + server({ id: "a", name: "[EU] Alpha" }), + ]; + expect(ids(sortRows(rows, { column: "name", direction: "asc" }, noPings))).toEqual([ + "a", + "b", + ]); + }); + + it("orders by queue and mod count", () => { + const rows = [ + server({ id: "few", queue: 2, modCount: 5 }), + server({ id: "many", queue: 40, modCount: 190 }), + ]; + expect(ids(sortRows(rows, { column: "queue", direction: "desc" }, noPings))).toEqual([ + "many", + "few", + ]); + expect(ids(sortRows(rows, { column: "mods", direction: "desc" }, noPings))).toEqual([ + "many", + "few", + ]); + }); + + it("orders regions by the label on screen, not the raw id", () => { + // "new_york" renders as "New York", which belongs under N — before Sydney. + const rows = [ + server({ id: "s", pingSiteId: "sydney" }), + server({ id: "n", pingSiteId: "new_york" }), + server({ id: "f", pingSiteId: "frankfurt" }), + ]; + expect(ids(sortRows(rows, { column: "region", direction: "asc" }, pings))).toEqual([ + "f", + "n", + "s", + ]); + }); + + it("orders by measured latency", () => { + const rows = [ + server({ id: "far", pingSiteId: "sydney" }), + server({ id: "near", pingSiteId: "frankfurt" }), + server({ id: "mid", pingSiteId: "new_york" }), + ]; + expect(ids(sortRows(rows, { column: "ping", direction: "asc" }, pings))).toEqual([ + "near", + "mid", + "far", + ]); + }); + + it("keeps unmeasured regions last in both directions", () => { + // The case that must not regress: an unknown latency shown as the fastest + // thing on the page would be actively misleading. + const rows = [ + server({ id: "unknown", pingSiteId: null }), + server({ id: "far", pingSiteId: "sydney" }), + server({ id: "near", pingSiteId: "frankfurt" }), + ]; + expect(ids(sortRows(rows, { column: "ping", direction: "asc" }, pings))).toEqual([ + "near", + "far", + "unknown", + ]); + expect(ids(sortRows(rows, { column: "ping", direction: "desc" }, pings))).toEqual([ + "far", + "near", + "unknown", + ]); + }); + + it("settles ties the same way every time", () => { + // Every vanilla server has zero mods; without a tie-break they would + // reshuffle on each refresh, because the source array is rebuilt per request. + const rows = [ + server({ id: "quiet", name: "Zulu", players: 2 }), + server({ id: "busy", name: "Alpha", players: 60 }), + server({ id: "alsoBusy", name: "Bravo", players: 60 }), + ]; + const sort: TableSort = { column: "mods", direction: "desc" }; + const once = ids(sortRows(rows, sort, noPings)); + const twice = ids(sortRows([...rows].reverse(), sort, noPings)); + expect(once).toEqual(["busy", "alsoBusy", "quiet"]); + expect(twice).toEqual(once); + }); +}); diff --git a/src/lib/sort.ts b/src/lib/sort.ts new file mode 100644 index 0000000..49014b9 --- /dev/null +++ b/src/lib/sort.ts @@ -0,0 +1,161 @@ +/** + * Table ordering. + * + * Two kinds of sort exist here, and the difference is visible to the user: + * + * - **Dataset-wide.** `/v2/servers` orders every matching server before + * paginating, so the first page really is the top of the list. It accepts + * `players`, `name`, `newest` and `lastSeen`, and no direction parameter. + * - **Page-local.** Everything else — queue, ping, mod count, region, and the + * reverse of the two directions the API does offer — can only reorder the + * hundred rows already loaded. Sorting by queue does *not* find the most + * queued servers in the fleet; it reorders the page the API chose. + * + * The distinction is kept in the types rather than in a comment in the UI, + * because presenting the second as though it were the first is the kind of + * quiet lie this codebase avoids elsewhere (see the "Filters the loaded page" + * label on minimum players). + * + * These functions are pure and take latency as a lookup, so they can be tested + * without the signal graph or a Tauri host. + */ + +import { humaniseRegion, tidyName } from "./format"; +import type { ServerSort, ServerSummary } from "./types"; + +/** A column the table can be ordered by. */ +export type SortColumn = "name" | "players" | "queue" | "ping" | "mods" | "region"; + +export type SortDirection = "asc" | "desc"; + +export interface TableSort { + column: SortColumn; + direction: SortDirection; +} + +/** Resolves a region's median latency, or null when it was never measured. */ +export type PingLookup = (pingSiteId: string | null) => number | null; + +/** + * The direction a column takes on its first click. + * + * Chosen so one click gives the answer people actually want: the busiest + * servers, the lowest ping, the most mods — but names from A. + */ +export const NATURAL_DIRECTION: Record = { + name: "asc", + players: "desc", + queue: "desc", + ping: "asc", + mods: "desc", + region: "asc", +}; + +/** + * Columns the API can order the whole dataset by, with the direction it returns. + * + * There is no direction parameter, so only these exact pairings are + * dataset-wide; anything else falls back to reordering the loaded page. + */ +export const API_SORTABLE: Partial< + Record +> = { + name: { sort: "name", direction: "asc" }, + players: { sort: "players", direction: "desc" }, +}; + +/** Column headings, also used in the note naming the active page-local sort. */ +export const COLUMN_LABEL: Record = { + name: "server", + players: "players", + queue: "queue", + ping: "ping", + mods: "mods", + region: "region", +}; + +/** + * The sort a click on `column` produces. + * + * Clicking the active column reverses it; clicking any other starts at that + * column's natural direction rather than inheriting the previous one, which + * would otherwise sort names Z–A just because players had been sorted high–low. + */ +export function nextSort(current: TableSort | null, column: SortColumn): TableSort { + if (current?.column === column) { + return { column, direction: current.direction === "asc" ? "desc" : "asc" }; + } + return { column, direction: NATURAL_DIRECTION[column] }; +} + +/** True when the API can serve this ordering across the whole dataset. */ +export function isDatasetWide(sort: TableSort): boolean { + const api = API_SORTABLE[sort.column]; + return api !== undefined && api.direction === sort.direction; +} + +/** + * Orders `rows` by `sort`. + * + * Returns the input untouched when there is nothing to do, so the browse view + * keeps the API's ordering — and its array identity — rather than paying for a + * copy and a re-render on every refresh. + */ +export function sortRows( + rows: ServerSummary[], + sort: TableSort | null, + pingOf: PingLookup, +): ServerSummary[] { + if (!sort) return rows; + const direction = sort.direction === "asc" ? 1 : -1; + + return [...rows].sort((a, b) => { + if (sort.column === "ping") { + const pa = pingOf(a.pingSiteId); + const pb = pingOf(b.pingSiteId); + // Unmeasured regions sort last in *both* directions. Flipping them with + // the rest would present "never measured" as the fastest thing on the + // page, which is the one reading that is actively misleading. + if (pa === null && pb === null) return tieBreak(a, b); + if (pa === null) return 1; + if (pb === null) return -1; + return (pa - pb) * direction || tieBreak(a, b); + } + return compareAscending(a, b, sort.column) * direction || tieBreak(a, b); + }); +} + +/** Compares two rows on `column`, always ascending. */ +function compareAscending( + a: ServerSummary, + b: ServerSummary, + column: Exclude, +): number { + switch (column) { + case "name": + return tidyName(a.name).localeCompare(tidyName(b.name)); + case "players": + return a.players - b.players; + case "queue": + return a.queue - b.queue; + case "mods": + return a.modCount - b.modCount; + case "region": + // Sorted by the label shown, not the raw id, so the order matches what is + // on screen — "New York" sits under N, not under "new_york". + return humaniseRegion(a.pingSiteId ?? a.region).localeCompare( + humaniseRegion(b.pingSiteId ?? b.region), + ); + } +} + +/** + * Settles ties deterministically. + * + * Without this, equal values (every vanilla server has zero mods) would reorder + * themselves on each refresh, because `Array.prototype.sort` is only stable + * with respect to the array it was given and that array is rebuilt per request. + */ +function tieBreak(a: ServerSummary, b: ServerSummary): number { + return b.players - a.players || tidyName(a.name).localeCompare(tidyName(b.name)); +} diff --git a/src/lib/state.ts b/src/lib/state.ts new file mode 100644 index 0000000..3f023a4 --- /dev/null +++ b/src/lib/state.ts @@ -0,0 +1,586 @@ +/** + * Application state. + * + * Signals rather than a store framework: each component subscribes only to the + * signals it reads, so the one-second clock behind "updated 18s ago" re-renders + * the status pill and nothing else. The server table re-renders only when the + * server array itself changes. + */ + +import { computed, signal } from "@preact/signals"; +import * as ipc from "./ipc"; +import { + API_SORTABLE, + isDatasetWide, + nextSort, + sortRows, + type SortColumn, + type TableSort, +} from "./sort"; +import { + DEFAULT_QUERY, + isCommandError, + type CommandError, + type FleetNow, + type HistoryRange, + type InstallIdentity, + type PingResult, + type ReadinessReport, + type ServerHistory, + type LaunchRecord, + type LauncherStatus, + type PingSite, + type RecentServer, + type ServerDetail, + type ServerQuery, + type ServerSummary, + type Settings, +} from "./types"; + +export type View = "browse" | "favorites" | "recent" | "settings"; + +/** Debounce applied to the search field before a request is issued. */ +export const SEARCH_DEBOUNCE_MS = 250; +/** How often the header re-reads API status. */ +const STATUS_POLL_MS = 60_000; + +// ---------------------------------------------------------------- navigation + +export const view = signal("browse"); + +// -------------------------------------------------------------- server list + +export const query = signal({ ...DEFAULT_QUERY }); +/** The raw search field, before debouncing into `query`. */ +export const searchInput = signal(""); + +export const servers = signal([]); +export const listLoading = signal(false); +/** True only for the very first load, so refreshes do not blank the table. */ +export const listInitialising = signal(true); +export const listError = signal(null); +export const totalServers = signal(0); +export const totalPages = signal(0); +export const filteredOut = signal(0); +/** + * Latency per ping-site id, measured once and reused for every row. + * + * Region latency, not per-server: see `ping_regions` in the Rust command layer + * for why a per-row measurement is not affordable. + */ +export const regionPings = signal>({}); + +/** + * Client-side ordering applied on top of the API's sort. + * + * Null means the rows are shown in the order the API returned them. Anything + * else reorders only the rows already loaded — see `./sort` for why that + * distinction is kept rather than smoothed over. + */ +export const clientSort = signal(null); + +// ------------------------------------------------- favorites & recents lists + +/** Live rows for the favorites view, resolved from stored ids. */ +export const favoriteServers = signal([]); +/** Live rows for the recents view, resolved from stored ids. */ +export const recentServers = signal([]); +export const savedListLoading = signal(false); + +// ------------------------------------------------------------------ detail + +export const selectedId = signal(null); +export const selectedServer = signal(null); +export const detailLoading = signal(false); +export const detailError = signal(null); +/** Set while a join is in flight, so the button can show progress. */ +export const joining = signal(false); +export const joinError = signal(null); + +// ------------------------------------------------------- history & readiness + +export const historyRange = signal("oneDay"); +export const historySeries = signal(null); +export const historyLoading = signal(false); +/** Null while unknown; the report itself says whether a scan was possible. */ +export const readiness = signal(null); +/** Null while unmeasured; the result itself says whether there is a figure. */ +export const ping = signal(null); +export const pingLoading = signal(false); + +// ------------------------------------------------------------ local storage + +export const settings = signal(null); +/** True when started with RFM_DEBUG. Gates developer-facing detail. */ +export const debugMode = signal(false); +/** Install identity. Populated only in debug mode. */ +export const installIdentity = signal(null); +export const favorites = signal([]); +export const recents = signal([]); +export const history = signal([]); + +export const favoriteSet = computed(() => new Set(favorites.value)); + +/** Median latency for a server's region, or null when unmeasured. */ +export function pingFor(pingSiteId: string | null): number | null { + if (!pingSiteId) return null; + return regionPings.value[pingSiteId]?.summary?.medianMs ?? null; +} + +/** The browse rows, after any client-side ordering. */ +export const visibleServers = computed(() => + sortRows(servers.value, clientSort.value, pingFor), +); + +/** + * Favorites and recents, under the same ordering. + * + * These lists are resolved in full rather than paginated, so a sort here covers + * everything the view can show and carries no page-local caveat. + */ +export const visibleFavorites = computed(() => + sortRows(favoriteServers.value, clientSort.value, pingFor), +); +export const visibleRecents = computed(() => + sortRows(recentServers.value, clientSort.value, pingFor), +); + +/** + * The ordering currently in effect, for the header indicators. + * + * With no client sort, this reports the API's own ordering so the column the + * server sorted by is still marked — `newest` and `lastSeen` have no column and + * mark nothing. + */ +export const activeSort = computed(() => { + if (clientSort.value) return clientSort.value; + switch (query.value.sort) { + case "players": + return { column: "players", direction: "desc" }; + case "name": + return { column: "name", direction: "asc" }; + default: + return null; + } +}); + +/** + * True when the active ordering reaches only the loaded page. + * + * A client sort that happens to match what the API already did is not + * page-local: the rows are the dataset's top rows either way. + */ +export const sortIsPageLocal = computed(() => { + const sort = clientSort.value; + if (!sort) return false; + const api = API_SORTABLE[sort.column]; + return !(api && api.direction === sort.direction && query.value.sort === api.sort); +}); + +/** + * Applies a header click. + * + * `datasetWide` is false for the favorites and recents tables, where there is + * no API query behind the rows and every sort is therefore local to the list + * being shown — and complete, because the list is not paginated. + */ +export function sortByColumn(column: SortColumn, datasetWide = true): void { + const next = nextSort(activeSort.value, column); + if (datasetWide && isDatasetWide(next)) { + // The API can order the whole dataset this way, so ask it to and drop the + // local reordering entirely. + clientSort.value = null; + setQuery({ sort: API_SORTABLE[next.column]!.sort }); + return; + } + clientSort.value = next; +} + +// ------------------------------------------------------------------ status + +export const status = signal(null); +export const pingSites = signal([]); +/** Fleet-wide population, shown in the title bar. */ +export const fleet = signal(null); +/** Ticks once a second purely so relative timestamps stay live. */ +export const nowMs = signal(Date.now()); + +// ---------------------------------------------------------------- internals + +/** Guards against a slow response overwriting a newer one. */ +let listRequestSeq = 0; +let detailRequestSeq = 0; +let historyRequestSeq = 0; +let readinessRequestSeq = 0; +let pingRequestSeq = 0; +let savedListSeq = 0; +let searchTimer: ReturnType | undefined; + +function toCommandError(err: unknown): CommandError { + if (isCommandError(err)) return err; + return { kind: "unknown", message: String(err) }; +} + +// ----------------------------------------------------------------- actions + +/** Replaces the query, resetting to page 1 unless the page itself changed. */ +export function setQuery(patch: Partial): void { + const next = { ...query.value, ...patch }; + if (patch.page === undefined) next.page = 1; + query.value = next; + void loadServers(); +} + +/** Updates the search box and schedules a debounced reload. */ +export function setSearch(value: string): void { + searchInput.value = value; + clearTimeout(searchTimer); + searchTimer = setTimeout(() => { + if (searchInput.value !== query.value.search) { + setQuery({ search: searchInput.value }); + } + }, SEARCH_DEBOUNCE_MS); +} + +/** Clears every filter, keeping sort and page size. */ +export function clearFilters(): void { + searchInput.value = ""; + clearTimeout(searchTimer); + setQuery({ + search: "", + region: null, + platform: null, + official: null, + hasMods: null, + battleye: null, + locked: null, + includeOffline: false, + minPlayers: 0, + }); +} + +export async function loadServers(): Promise { + const seq = ++listRequestSeq; + listLoading.value = true; + try { + const result = await ipc.listServers(query.value); + if (seq !== listRequestSeq) return; // Superseded by a newer request. + servers.value = result.servers; + totalServers.value = result.totalServers; + totalPages.value = result.totalPages; + filteredOut.value = result.filteredOut; + listError.value = null; + if (status.value) { + // Every server response carries snapshot freshness, so the header stays + // current without a status request of its own. + status.value = { ...status.value, dataset: result.dataset }; + } + } catch (err) { + if (seq !== listRequestSeq) return; + listError.value = toCommandError(err); + } finally { + if (seq === listRequestSeq) { + listLoading.value = false; + listInitialising.value = false; + } + } +} + +export async function selectServer(id: string | null): Promise { + selectedId.value = id; + joinError.value = null; + if (!id) { + selectedServer.value = null; + historySeries.value = null; + readiness.value = null; + ping.value = null; + return; + } + const seq = ++detailRequestSeq; + detailLoading.value = true; + detailError.value = null; + // Show what the list already knows while the detail request is in flight. + const known = servers.value.find((s) => s.id === id); + if (known && selectedServer.value?.id !== id) { + selectedServer.value = null; + } + try { + const detail = await ipc.getServer(id); + if (seq !== detailRequestSeq) return; + selectedServer.value = detail; + // Both are secondary to the panel and must not delay it, so they are not + // awaited. Readiness reuses the detail response the client just cached, so + // it costs a directory scan rather than a request. + void loadHistory(id); + void loadReadiness(id); + void measurePing(id); + // The backend recorded the visit; mirror it locally without a round trip. + recents.value = [ + { id: detail.id, name: detail.name, lastSeenAt: Math.floor(Date.now() / 1000) }, + ...recents.value.filter((r) => r.id !== detail.id), + ].slice(0, 30); + } catch (err) { + if (seq !== detailRequestSeq) return; + detailError.value = toCommandError(err); + } finally { + if (seq === detailRequestSeq) detailLoading.value = false; + } +} + +/** Loads population history for `id` at the current range. */ +export async function loadHistory(id: string): Promise { + const seq = ++historyRequestSeq; + historyLoading.value = true; + try { + const series = await ipc.getServerHistory(id, historyRange.value); + if (seq !== historyRequestSeq || selectedId.value !== id) return; + historySeries.value = series; + } catch { + if (seq !== historyRequestSeq) return; + // The chart is supplementary; a failure renders as "no history", not an error. + historySeries.value = null; + } finally { + if (seq === historyRequestSeq) historyLoading.value = false; + } +} + +/** Switches the history range and reloads for the selected server. */ +export function setHistoryRange(range: HistoryRange): void { + if (historyRange.value === range) return; + historyRange.value = range; + historySeries.value = null; + const id = selectedId.value; + if (id) void loadHistory(id); +} + +/** + * Loads the mod readiness report for `id`. + * + * Reporting only: nothing in the launcher acts on the result. + */ +export async function loadReadiness(id: string): Promise { + const seq = ++readinessRequestSeq; + try { + const report = await ipc.getModReadiness(id); + if (seq !== readinessRequestSeq || selectedId.value !== id) return; + readiness.value = report; + } catch { + if (seq !== readinessRequestSeq) return; + // Leaving this null renders nothing, which is correct: an unknown state + // must never be shown as a reassuring one. + readiness.value = null; + } +} + +/** + * Measures latency to the selected server. + * + * Only ever for the selected server: measuring a whole page would emit a + * hundred ICMP probes on every refresh, which is both rude and indistinguishable + * from a scan. + */ +export async function measurePing(id: string): Promise { + const seq = ++pingRequestSeq; + pingLoading.value = true; + ping.value = null; + try { + const result = await ipc.pingServer(id); + if (seq !== pingRequestSeq || selectedId.value !== id) return; + ping.value = result; + } catch { + if (seq !== pingRequestSeq) return; + ping.value = null; + } finally { + if (seq === pingRequestSeq) pingLoading.value = false; + } +} + +/** + * Resolves the favorites or recents list into live server rows. + * + * Those views hold ids, so a table needs one detail request per row. Both lists + * are small and the client caches responses, so revisiting a tab is usually + * free; ids that no longer resolve are dropped by the backend. + */ +export async function loadSavedList(which: "favorites" | "recent"): Promise { + const ids = which === "favorites" ? favorites.value : recents.value.map((r) => r.id); + const target = which === "favorites" ? favoriteServers : recentServers; + if (ids.length === 0) { + target.value = []; + return; + } + const seq = ++savedListSeq; + savedListLoading.value = true; + try { + const resolved = await ipc.getServers(ids); + if (seq !== savedListSeq) return; + target.value = resolved; + } catch { + if (seq !== savedListSeq) return; + // Leave whatever was shown; the stored list is still intact. + } finally { + if (seq === savedListSeq) savedListLoading.value = false; + } +} + +/** Switches view, resolving the saved lists on entry. */ +export function setView(next: View): void { + view.value = next; + if (next === "favorites" || next === "recent") { + void loadSavedList(next); + } +} + +export async function toggleFavorite(id: string): Promise { + try { + const isFavorite = await ipc.toggleFavorite(id); + favorites.value = isFavorite + ? [...favorites.value, id] + : favorites.value.filter((f) => f !== id); + if (!isFavorite && view.value === "favorites") { + favoriteServers.value = favoriteServers.value.filter((s) => s.id !== id); + } + } catch { + // A failed toggle leaves the previous state visible, which is correct: + // nothing was persisted. + } +} + +export async function join(server: { id: string; name: string }): Promise { + joining.value = true; + joinError.value = null; + try { + await ipc.joinServer(server.id, server.name); + history.value = [ + { + serverId: server.id, + serverName: server.name, + launchedAt: Math.floor(Date.now() / 1000), + succeeded: true, + }, + ...history.value, + ].slice(0, 200); + } catch (err) { + joinError.value = toCommandError(err); + } finally { + joining.value = false; + } +} + +export async function saveSettings(next: Settings): Promise { + const stored = await ipc.updateSettings(next); + settings.value = stored; + if (stored.perPage !== query.value.perPage) { + setQuery({ perPage: stored.perPage }); + } +} + + +/** Refreshes the fleet-wide population shown in the title bar. */ +export async function refreshFleet(): Promise { + try { + fleet.value = await ipc.getFleetNow(); + } catch { + // Decorative; the count simply stays as it was. + } +} + +export async function refreshStatus(): Promise { + try { + const next = await ipc.getStatus(); + // Prefer freshness already seen on a server response over a status probe + // that may not carry one. + status.value = { ...next, dataset: next.dataset ?? status.value?.dataset ?? null }; + } catch { + status.value = { + apiReachable: false, + dataset: status.value?.dataset ?? null, + reportedStatus: null, + note: null, + }; + } +} + +export async function clearLocalHistory(): Promise { + await ipc.clearHistory(); + recents.value = []; + history.value = []; +} + +let started = false; + +/** Loads persisted state, then the first page of servers. */ +export async function start(): Promise { + if (started) return; + started = true; + + try { + const local = await ipc.getLocalState(); + settings.value = local.settings; + favorites.value = local.favorites; + recents.value = local.recents; + history.value = local.history; + query.value = { ...query.value, perPage: local.settings.perPage }; + debugMode.value = await ipc.isDebugMode(); + if (debugMode.value) { + installIdentity.value = await ipc.getInstallIdentity(); + } + } catch { + // Defaults are already in place; a storage failure must not block startup. + } + + // The first server page is what the user is waiting for, so it goes first and + // the decorative requests follow. + await loadServers(); + + void refreshStatus(); + void refreshFleet(); + // Eight ICMP probes, reused for every row in every page from here on. + void ipc + .pingRegions() + .then((results) => { + regionPings.value = results; + }) + .catch(() => { + // The ping column simply stays empty; it is supplementary. + }); + void ipc + .listPingSites() + .then((sites) => { + pingSites.value = sites; + }) + .catch(() => { + // Region labels fall back to humanised ids. + }); + + setInterval(() => { + nowMs.value = Date.now(); + }, 1000); + setInterval(() => { + void refreshStatus(); + void refreshFleet(); + }, STATUS_POLL_MS); + + startAutoRefresh(); +} + +/** + * Optional periodic reload of the server list. + * + * Off by default and floored at 15s by the settings store, and it skips while + * the window is hidden so a backgrounded launcher makes no requests. + */ +function startAutoRefresh(): void { + let lastRefreshMs = Date.now(); + // A single slow timer that checks the configured interval, rather than one + // timer per settings change. + setInterval(() => { + const s = settings.value; + if (!s?.autoRefresh) return; + if (document.hidden) return; // A backgrounded window makes no requests. + if (view.value !== "browse") return; + if (Date.now() - lastRefreshMs < s.refreshIntervalSecs * 1000) return; + lastRefreshMs = Date.now(); + void loadServers(); + }, 5_000); +} diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..c9a032a --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,300 @@ +/** + * Mirrors of the Rust DTOs crossing the Tauri boundary. + * + * These are hand-written rather than generated: the surface is small, and + * keeping it hand-written means the field names the UI reads are visible in + * one place. `rfm-core` serialises with `rename_all = "camelCase"`, with one + * deliberate exception — `battlEye`, which matches the API's own spelling. + * + * Every string here is untrusted remote text. Render it as text content only; + * never build a URL or markup from it. + */ + +export interface DatasetMeta { + warming: boolean; + stale: boolean; + snapshotAgeSeconds: number; + lastCollectionAt: string | null; + serverCount: number; + onlineServerCount: number; + offlineServerCount: number; +} + +export interface ServerAnalytics { + rank: number | null; + overallScore: number | null; + activityScore: number | null; + reliabilityScore: number | null; + confidence: number | null; +} + +export interface ServerSummary { + id: string; + name: string; + scenarioId: string; + scenarioName: string; + scenarioThumbnailUrl: string | null; + gameVersion: string; + official: boolean; + passwordProtected: boolean; + /** Spelled as the API spells it. */ + battlEye: boolean; + crossPlay: boolean; + players: number; + maxPlayers: number; + queue: number; + queueMax: number; + pingSiteId: string | null; + region: string | null; + /** Host OS (`LINUX`/`WINDOWS`), not a client platform. */ + platform: string | null; + /** Client platforms that may join: `pc`, `xbox`, `psn`. */ + platforms: string[]; + modCount: number; + online: boolean; + firstSeen: string | null; + lastSeen: string | null; + analytics: ServerAnalytics | null; + /** The API's own join URI. Cross-checked in tests; never handed to the OS. */ + joinUrl: string | null; +} + +export interface ServerActivity { + peak24h: number; + avg24h: number; + median24h: number; + low24h: number; + peak7d: number; + samples24h: number; + trackingSince: string | null; +} + +export interface ServerModSummary { + count: number; + knownSize: number; + knownSizeText: string | null; + unresolvedCount: number; +} + +export interface ServerMod { + id: string; + name: string; + version: string; + size: number; + sizeText: string | null; + sizeKnown: boolean; + modUrl: string | null; +} + +/** `ServerDetail` flattens `ServerSummary`, so it carries every summary field. */ +export interface ServerDetail extends ServerSummary { + present: boolean; + hostType: string | null; + joinable: boolean; + /** Published address. Untrusted; only the backend ever acts on it. */ + host: string | null; + port: number; + /** Server-reported simulation FPS; 0 when not reported. */ + fps: number; + activity: ServerActivity | null; + modSummary: ServerModSummary; + mods: ServerMod[]; +} + +export interface PingSite { + id: string; + label: string; + regionGroup: string; + address: string | null; +} + +/** Fleet-wide population right now. */ +export interface FleetNow { + onlineServers: number; + players: number; + capacity: number; + queue: number; +} + +export interface LauncherStatus { + apiReachable: boolean; + dataset: DatasetMeta | null; + reportedStatus: string | null; + note: string | null; +} + +/** Ranges `/v2/servers/{id}/history` accepts. */ +export type HistoryRange = + | "sixHours" + | "oneDay" + | "threeDays" + | "oneWeek" + | "oneMonth" + | "threeMonths" + | "oneYear" + | "all"; + +/** The ranges the inspector offers, with their labels. */ +export const HISTORY_RANGES: ReadonlyArray<{ value: HistoryRange; label: string }> = [ + { value: "sixHours", label: "6h" }, + { value: "oneDay", label: "24h" }, + { value: "oneWeek", label: "7d" }, + { value: "oneMonth", label: "30d" }, +]; + +export interface ServerHistoryPoint { + /** Bucket start, unix seconds. */ + t: number; + avg: number; + min: number; + max: number; + /** Slot capacity during the bucket; 0 when not recorded. */ + cap: number; +} + +export interface ServerHistory { + serverId: string; + /** The range the API served, which may differ from what was asked. */ + range: string; + /** Bucket width the API chose: `5m`, `hour` or `day`. */ + bucket: string; + points: ServerHistoryPoint[]; +} + +/** Diagnostic view of the install identity. Populated only in debug mode. */ +export interface InstallIdentity { + id: string; + /** True when the identifier survives a reinstall. */ + machineDerived: boolean; +} + +/** Why a server could not be measured. */ +export type PingUnavailable = "no_host" | "unroutable_host" | "no_reply" | "not_permitted"; + +export interface PingSummary { + bestMs: number; + /** Preferred for display: one delayed packet should not move the figure. */ + medianMs: number; + received: number; + sent: number; +} + +/** A latency measurement, or a stated reason there is none. */ +export interface PingResult { + summary?: PingSummary; + unavailable?: PingUnavailable; + message?: string; +} + +/** Why a local mod scan could not produce an answer. */ +export type ReadinessUnavailable = "no_addons_directory" | "scan_failed"; + +/** + * Comparison of a server's required mods against the local addon store. + * + * Reporting only. `available: false` means the scan did not run, and every + * count is then zero and must not be rendered. + */ +export interface ReadinessReport { + available: boolean; + unavailable?: ReadinessUnavailable; + message?: string; + required: number; + upToDate: number; + outdated: number; + missing: number; + /** Floor, not a total, while `unresolvedMods` is non-zero. */ + missingBytes: number; + unresolvedMods: number; + installedTotal: number; +} + +export type ServerSort = "players" | "name" | "newest" | "lastSeen"; +export type ClientPlatform = "pc" | "xbox" | "psn"; + +export interface ServerQuery { + search: string; + sort: ServerSort; + region: string | null; + platform: ClientPlatform | null; + official: boolean | null; + hasMods: boolean | null; + battleye: boolean | null; + /** The API's name for password-protected. */ + locked: boolean | null; + includeOffline: boolean; + /** Applied client-side; the API has no equivalent parameter. */ + minPlayers: number; + page: number; + perPage: number; +} + +export interface ServerListResult { + servers: ServerSummary[]; + totalServers: number; + totalPages: number; + currentPage: number; + /** Rows the client-side filters removed from this page. */ + filteredOut: number; + dataset: DatasetMeta; +} + +export interface Settings { + perPage: number; + autoRefresh: boolean; + refreshIntervalSecs: number; + /** Publish activity to Discord Rich Presence. */ + discordPresence: boolean; +} + +export interface RecentServer { + id: string; + name: string; + lastSeenAt: number; +} + +export interface LaunchRecord { + serverId: string; + serverName: string; + launchedAt: number; + succeeded: boolean; +} + +export interface LocalStateDto { + settings: Settings; + favorites: string[]; + recents: RecentServer[]; + history: LaunchRecord[]; +} + +/** The shape every failing command rejects with. */ +export interface CommandError { + kind: string; + message: string; + retryAfterSecs?: number; +} + +/** Type guard for the error shape, since `catch` gives `unknown`. */ +export function isCommandError(value: unknown): value is CommandError { + return ( + typeof value === "object" && + value !== null && + typeof (value as CommandError).kind === "string" && + typeof (value as CommandError).message === "string" + ); +} + +export const DEFAULT_QUERY: ServerQuery = { + search: "", + sort: "players", + region: null, + platform: null, + official: null, + hasMods: null, + battleye: null, + locked: null, + includeOffline: false, + minPlayers: 0, + page: 1, + perPage: 100, +}; diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..8db78cb --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,13 @@ +import { render } from "preact"; +import { App } from "./app"; +import { start } from "./lib/state"; +import "./styles.css"; + +const root = document.getElementById("app"); +if (!root) throw new Error("missing #app root element"); + +render(, root); + +// Kick off data loading after the first paint, so the chrome is on screen +// immediately rather than waiting on the network. +void start(); diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..b405b22 --- /dev/null +++ b/src/styles.css @@ -0,0 +1,1319 @@ +/* + * reforgermods.net launcher chrome. + * + * A dense, dark-teal desktop utility: hairline borders, one restrained accent, + * compact type, no gradients or oversized cards. Sized in px on purpose — + * this is a fixed-scale desktop application, not a responsive web page, and + * high-DPI is handled by the WebView's device pixel ratio. + */ + +/* + * Palette and type are taken from reforgermods.net's own dark theme + * (`[data-bs-theme="dark"]` in the site's global stylesheet), so the launcher + * and the site read as one product: near-black blue-greys, a single olive + * accent, and the site's condensed type stack. + * + * Token names stay launcher-local; the comment on each records the site token + * it mirrors, so a change on the site can be traced here. + */ +:root { + --bg: #050d10; /* --site-bg */ + --bg-panel: #0d171b; /* --site-panel */ + --bg-raised: #152126; /* --site-panel-soft */ + --bg-sunken: #060f12; /* --site-inset */ + --bg-hover: #121c21; /* --site-card */ + --bg-active: #1a272d; /* --site-card-hover */ + --bg-band: #071114; /* --site-band */ + + --border: rgba(170, 185, 196, 0.15); /* --site-border */ + --border-muted: rgba(170, 185, 196, 0.09); /* --site-muted-border */ + --border-strong: rgba(170, 185, 196, 0.31); /* --site-strong-border */ + --border-input: #303f46; /* --site-input-border */ + + --text: #f4f4f1; /* --site-text-strong */ + --text-dim: #aeb8c2; /* --site-text-muted */ + --text-faint: #75828c; /* --site-text-faint */ + + /* The brand accent is olive, not teal. --accent-deep is the seated variant + used behind text, and is dark enough for #0b1116 to sit on it. */ + --accent: #8b9d63; /* --site-accent */ + --accent-strong: #a4b873; /* --site-accent-strong */ + --accent-deep: #6d7d4c; + --accent-bg: rgba(139, 157, 99, 0.13); /* --site-accent-dim */ + --on-accent: #0b1116; + + --good: #a4b873; + --warn: #c99044; /* --site-orange */ + --bad: #c2705a; /* --rm-fall */ + --info: #9aa7b7; /* --site-blue */ + + --row-h: 30px; + --nav-w: 172px; + --inspector-w: 348px; + + /* The site's condensed stack. It is a large part of the brand's look, and it + buys roughly 15% more characters per column in the server table. */ + --sans: "Arial Narrow", "Roboto Condensed", "Agency FB", "Segoe UI", system-ui, sans-serif; + --mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; + + color-scheme: dark; +} + +* { + box-sizing: border-box; +} + +html, +body, +#app { + height: 100%; + margin: 0; +} + +body { + background: var(--bg); + color: var(--text); + font-family: var(--sans); + font-size: 13px; + line-height: 1.45; + -webkit-font-smoothing: antialiased; + overflow: hidden; + /* A desktop chrome, not a document: no accidental text selection on labels. */ + user-select: none; + cursor: default; +} + +button, +input, +select { + font: inherit; + color: inherit; +} + +/* Focus is always visible, and always the accent. */ +:focus-visible { + outline: 1px solid var(--accent); + outline-offset: 1px; +} + +::-webkit-scrollbar { + width: 10px; + height: 10px; +} +::-webkit-scrollbar-track { + background: var(--bg-sunken); +} +::-webkit-scrollbar-thumb { + background: #1e2d34; + border: 2px solid var(--bg-sunken); + border-radius: 6px; +} +::-webkit-scrollbar-thumb:hover { + background: #2a3d45; +} + +/* ------------------------------------------------------------------ shell */ + +.shell { + display: grid; + grid-template-rows: 32px 1fr; + grid-template-columns: var(--nav-w) 1fr; + grid-template-areas: + "title title" + "nav main"; + height: 100%; +} + +/* One bar doing the work of the native caption and a brand strip. */ +.titlebar { + grid-area: title; + display: flex; + align-items: center; + gap: 10px; + padding-left: 10px; + background: var(--bg-band); + border-bottom: 1px solid var(--border); + /* Nothing in the caption should be selectable while dragging. */ + user-select: none; +} + +.titlebar__mark { + width: 16px; + height: 16px; + flex: none; + display: block; + object-fit: contain; +} + +.titlebar__name { + font-size: 12px; + font-weight: 600; + letter-spacing: -0.01em; + white-space: nowrap; +} +.titlebar__name em { + font-style: normal; + color: var(--text-dim); + font-weight: 500; +} + +.titlebar__context { + font-size: 11px; + color: var(--text-faint); + white-space: nowrap; + padding-left: 4px; + border-left: 1px solid var(--border); + margin-left: 2px; +} + +.titlebar__spacer { + flex: 1; + align-self: stretch; +} + +/* Caption buttons. Windows metrics: 46x32, close goes red on hover. */ +.wincontrols { + display: flex; + align-self: stretch; + margin-left: 8px; +} +.wincontrols__btn { + width: 46px; + border: 0; + background: none; + color: var(--text-dim); + display: grid; + place-items: center; + cursor: default; +} +.wincontrols__btn:hover { + background: var(--bg-hover); + color: var(--text); +} +.wincontrols__btn--close:hover { + background: #c42b1c; + color: #fff; +} + +.nav { + grid-area: nav; + display: flex; + flex-direction: column; + padding: 8px 6px; + gap: 1px; + background: var(--bg-panel); + border-right: 1px solid var(--border); + overflow-y: auto; +} + +.nav__label { + padding: 10px 8px 4px; + font-size: 10px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-faint); +} + +.nav__item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 5px 8px; + border: 0; + border-radius: 3px; + background: transparent; + color: var(--text-dim); + text-align: left; + cursor: pointer; + white-space: nowrap; +} +.nav__item:hover { + background: var(--bg-hover); + color: var(--text); +} +.nav__item[aria-current="true"] { + background: var(--bg-active); + color: var(--text); +} +.nav__count { + font-family: var(--mono); + font-size: 10.5px; + color: var(--text-faint); +} + +.nav__spacer { + flex: 1; +} + +.nav__foot { + padding: 8px; + font-size: 10.5px; + color: var(--text-faint); + border-top: 1px solid var(--border); + margin-top: 8px; +} + +.main { + grid-area: main; + min-width: 0; + min-height: 0; + display: grid; + grid-template-columns: minmax(0, 1fr) var(--inspector-w); +} +.main--full { + grid-template-columns: minmax(0, 1fr); +} + +@media (max-width: 1120px) { + .main { + grid-template-columns: minmax(0, 1fr); + } + .inspector { + position: fixed; + top: 32px; + right: 0; + bottom: 0; + width: var(--inspector-w); + z-index: 20; + box-shadow: -12px 0 24px rgba(0, 0, 0, 0.45); + } +} + +/* ----------------------------------------------------------------- status */ + +.status { + display: flex; + align-items: center; + gap: 8px; + padding-right: 4px; + font-size: 11px; + color: var(--text-dim); + white-space: nowrap; +} + +.status__dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--text-faint); + flex: none; +} +.status__dot--ok { + background: var(--good); +} +.status__dot--warn { + background: var(--warn); +} +.status__dot--bad { + background: var(--bad); +} + +/* A hairline rule rather than a middot: the old form read as machine output. */ +.status__divider { + width: 1px; + height: 11px; + background: var(--border-strong); +} + +/* --------------------------------------------------------------- controls */ + +.toolbar { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; + padding: 7px 10px; + background: var(--bg-panel); + border-bottom: 1px solid var(--border); +} + +.search { + position: relative; + flex: 1 1 200px; + min-width: 140px; + max-width: 380px; +} +.search input { + width: 100%; + height: 24px; + padding: 0 46px 0 8px; + background: var(--bg-sunken); + border: 1px solid var(--border-input); + border-radius: 3px; + user-select: text; + cursor: text; +} +.search input::placeholder { + color: var(--text-faint); +} +.search input:focus { + border-color: var(--accent-deep); + outline: none; +} +.search__hint { + position: absolute; + right: 6px; + top: 50%; + transform: translateY(-50%); + font-family: var(--mono); + font-size: 9.5px; + color: var(--text-faint); + border: 1px solid var(--border-strong); + border-radius: 3px; + padding: 0 4px; + pointer-events: none; +} + +select.control, +input.control { + height: 24px; + padding: 0 6px; + background: var(--bg-sunken); + border: 1px solid var(--border-strong); + border-radius: 3px; + cursor: pointer; +} +input.control { + width: 62px; + font-family: var(--mono); + user-select: text; + cursor: text; +} + +.chip { + height: 24px; + padding: 0 8px; + background: var(--bg-sunken); + border: 1px solid var(--border-strong); + border-radius: 3px; + color: var(--text-dim); + cursor: pointer; + white-space: nowrap; +} +.chip:hover { + border-color: var(--accent-deep); + color: var(--text); +} +/* Tri-state: unset -> include -> exclude. */ +.chip[data-state="on"] { + background: var(--accent-bg); + border-color: var(--accent-deep); + color: var(--accent); +} +.chip[data-state="off"] { + border-color: rgba(194, 112, 90, 0.45); + color: var(--bad); + text-decoration: line-through; + text-decoration-thickness: 1px; +} + +.control-label { + font-size: 10px; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--text-faint); +} + +.toolbar__spacer { + flex: 1; +} + +.linkish { + background: none; + border: 0; + padding: 0 4px; + color: var(--text-dim); + cursor: pointer; + text-decoration: underline; + text-underline-offset: 2px; + text-decoration-color: var(--border-strong); +} +.linkish:hover { + color: var(--accent); +} + +/* ------------------------------------------------------------------ table */ + +.browser { + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; +} + +.table { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + position: relative; + background: var(--bg); + /* Reserve the gutter so rows do not shift when the list becomes scrollable. */ + scrollbar-gutter: stable; +} + +/* One definition for both, so a column can never drift from its heading. */ +.thead, +.row { + display: grid; + /* The narrow numeric columns carry a sort arrow beside their heading when + they are the active ordering, so each has room for the label plus the + arrow. Sized for the longest heading, not the widest value. */ + grid-template-columns: + minmax(170px, 1fr) 58px 76px 62px 54px 88px 56px 72px; + align-items: center; + gap: 12px; + padding: 0 12px; +} + +.thead { + position: sticky; + top: 0; + z-index: 2; + height: 26px; + background: var(--bg-panel); + border-bottom: 1px solid var(--border-strong); + font-size: 10px; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--text-faint); +} +.thead .num { + text-align: right; +} +.thead .mid { + text-align: center; +} + +/* Sortable column heading. A button for keyboard and screen-reader access, + stripped back to look exactly like the static headings beside it. */ +.th-sort { + display: flex; + align-items: center; + gap: 3px; + min-width: 0; + padding: 0; + border: 0; + background: none; + font: inherit; + letter-spacing: inherit; + text-transform: inherit; + color: inherit; + cursor: pointer; +} +.th-sort.num { + justify-content: flex-end; +} +.th-sort.mid { + justify-content: center; +} +.th-sort__label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.th-sort:hover, +.th-sort[data-active="true"] { + color: var(--text-dim); +} +.th-sort:focus-visible { + outline: 1px solid var(--accent); + outline-offset: 2px; +} +.th-sort__arrow { + flex: none; + font-size: 8px; + line-height: 1; + color: var(--accent-strong); +} +/* Hollow, and in the dimmer accent: this ordering reorders the loaded page + only. The shape carries it without relying on colour alone, and the footer + states it in words. */ +.th-sort__arrow[data-scope="page"] { + color: var(--accent); +} + +.rows { + position: relative; +} + +.row { + position: absolute; + left: 0; + right: 0; + height: var(--row-h); + border-bottom: 1px solid var(--border-muted); + cursor: pointer; +} +.row:hover { + background: var(--bg-hover); +} +.row[aria-selected="true"] { + background: var(--bg-active); + box-shadow: inset 2px 0 0 var(--accent); +} +.row--offline { + color: var(--text-faint); +} + +.cell { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.cell.num { + text-align: right; + font-family: var(--mono); + font-size: 11.5px; + font-variant-numeric: tabular-nums; +} + +/* Ping is a short, roughly fixed-width figure with nothing to its right to + align against, so centring reads better than a ragged right edge. */ +.cell.mid { + text-align: center; + font-family: var(--mono); + font-size: 11.5px; + font-variant-numeric: tabular-nums; +} +.cell.dim { + color: var(--text-dim); +} + +/* Latency bands. Restrained on purpose: a coloured number per row would turn + the table into a traffic light. */ +.cell.ping[data-band="good"] { + color: var(--accent-strong); +} +.cell.ping[data-band="ok"] { + color: var(--text-dim); +} +.cell.ping[data-band="far"] { + color: var(--warn); +} + +.name { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; +} + +.name__text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.star { + flex: none; + width: 20px; + height: 20px; + display: grid; + place-items: center; + border: 0; + background: none; + color: #4e6260; + cursor: pointer; + border-radius: 3px; + padding: 0; +} +.star:hover { + color: var(--accent-strong); + background: var(--bg-raised); +} +.star[data-on="true"] { + color: #d9b44a; +} + +/* Status marks. Muted by default: on a list where most servers run BattlEye, + a bright mark on every row is noise, and only the exceptions should draw the + eye. */ +.marks { + display: flex; + align-items: center; + gap: 3px; + color: var(--text-faint); +} +.marks__i { + display: grid; + place-items: center; + width: 14px; + height: 14px; +} +.marks__i--official { + color: var(--accent); +} +.marks__i--locked { + color: var(--warn); +} +.marks__i--offline { + color: var(--bad); +} + +/* A full server is worth noticing; everything else reads from the figure. */ +.cell[data-full="true"] { + color: var(--warn); +} + +.tags { + display: flex; + gap: 4px; + overflow: hidden; +} + +/* Platform marks, as on the site: a tinted glyph in a quiet tile. */ +.platforms { + display: flex; + gap: 3px; +} +.platforms__badge { + width: 20px; + height: 18px; + display: grid; + place-items: center; + border-radius: 3px; + background: var(--bg-raised); + border: 1px solid var(--border); +} +.platforms__badge svg { + display: block; +} + +.tag { + flex: none; + height: 15px; + padding: 0 4px; + border-radius: 2px; + border: 1px solid var(--border-strong); + background: var(--bg-raised); + font-family: var(--mono); + font-size: 9.5px; + line-height: 13px; + color: var(--text-dim); + letter-spacing: 0.02em; +} +.tag--accent { + color: var(--accent); + border-color: var(--accent-deep); + background: var(--accent-bg); +} +.tag--warn { + color: var(--warn); + border-color: rgba(201, 144, 68, 0.4); +} + +/* ------------------------------------------------------------- inspector */ + +.inspector { + display: flex; + flex-direction: column; + min-height: 0; + background: var(--bg-panel); + border-left: 1px solid var(--border); + overflow: hidden; +} + +.inspector__head { + padding: 10px 12px; + border-bottom: 1px solid var(--border); +} + +.inspector__title { + font-size: 13px; + font-weight: 600; + line-height: 1.3; + user-select: text; + cursor: text; + overflow-wrap: anywhere; +} + +.inspector__sub { + margin-top: 3px; + font-size: 11px; + color: var(--text-dim); + overflow-wrap: anywhere; +} + +.inspector__body { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 10px 12px; +} + +.inspector__foot { + padding: 10px 12px; + border-top: 1px solid var(--border); + background: var(--bg-sunken); +} + +.facts { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1px; + background: var(--border); + border: 1px solid var(--border); + border-radius: 3px; + overflow: hidden; +} + +.fact { + background: var(--bg-raised); + padding: 6px 8px; + min-width: 0; +} +.fact__k { + font-size: 9.5px; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--text-faint); +} +.fact__v { + font-family: var(--mono); + font-size: 12px; + font-variant-numeric: tabular-nums; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.section { + margin-top: 14px; +} +.section__h { + font-size: 10px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-faint); + margin-bottom: 6px; + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 8px; +} + +.modlist { + border: 1px solid var(--border); + border-radius: 3px; + max-height: 210px; + overflow-y: auto; + scrollbar-gutter: stable; + background: var(--bg-sunken); +} +.modlist__row { + display: grid; + grid-template-columns: minmax(0, 1fr) 58px 62px; + gap: 8px; + align-items: baseline; + padding: 4px 9px; + font-size: 11px; +} +/* Banding instead of a rule per row: 122 hairlines read as a grid, not a list. */ +.modlist__row:nth-child(odd) { + background: rgba(170, 185, 196, 0.03); +} +.modlist__name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + user-select: text; +} +.modlist__version, +.modlist__size { + font-family: var(--mono); + font-size: 10px; + color: var(--text-faint); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.modlist__size { + text-align: right; +} + +.btn { + height: 28px; + padding: 0 12px; + border-radius: 3px; + border: 1px solid var(--border-strong); + background: var(--bg-raised); + color: var(--text); + cursor: pointer; + white-space: nowrap; +} +.btn:hover:not(:disabled) { + background: var(--bg-hover); + border-color: var(--accent-deep); +} +.btn:disabled { + opacity: 0.45; + cursor: default; +} + +.btn--primary { + width: 100%; + height: 32px; + font-weight: 600; + background: var(--accent-deep); + border-color: var(--accent); + color: var(--on-accent); +} +.btn--primary:hover:not(:disabled) { + background: var(--accent); +} + +.btn-row { + display: flex; + gap: 6px; + margin-top: 6px; +} +.btn-row .btn { + flex: 1; +} + +/* ------------------------------------------------------------ list states */ + +.state { + display: grid; + place-content: center; + justify-items: center; + gap: 8px; + padding: 48px 24px; + text-align: center; + color: var(--text-dim); +} +.state__title { + color: var(--text); + font-weight: 600; +} +.state__hint { + font-size: 11.5px; + max-width: 44ch; + color: var(--text-faint); +} + +.banner { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + font-size: 11.5px; + border-bottom: 1px solid var(--border); + background: var(--bg-raised); + color: var(--text-dim); +} +.banner--error { + color: var(--bad); + border-bottom-color: rgba(194, 112, 90, 0.35); + background: rgba(194, 112, 90, 0.08); +} +.banner--warn { + color: var(--warn); + background: rgba(201, 144, 68, 0.08); + border-bottom-color: rgba(201, 144, 68, 0.3); +} + +/* Indeterminate progress hairline under the toolbar during a refresh. */ +.loadbar { + height: 1px; + background: var(--border); + overflow: hidden; + position: relative; +} +.loadbar::after { + content: ""; + position: absolute; + inset: 0; + width: 30%; + background: var(--accent); + animation: slide 1.1s ease-in-out infinite; +} +@keyframes slide { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(400%); + } +} +@media (prefers-reduced-motion: reduce) { + .loadbar::after { + animation: none; + width: 100%; + opacity: 0.4; + } +} + +.footbar { + display: flex; + align-items: center; + gap: 10px; + height: 24px; + padding: 0 10px; + background: var(--bg-panel); + border-top: 1px solid var(--border); + font-size: 10.5px; + color: var(--text-faint); + font-family: var(--mono); +} +.footbar__spacer { + flex: 1; +} +.pager { + display: flex; + align-items: center; + gap: 4px; +} +.pager button { + height: 17px; + min-width: 20px; + padding: 0 5px; + border: 1px solid var(--border-strong); + background: var(--bg-raised); + color: var(--text-dim); + border-radius: 2px; + cursor: pointer; + font-size: 10px; +} +.pager button:disabled { + opacity: 0.35; + cursor: default; +} + +/* --------------------------------------------------------------- settings */ + +.page { + padding: 16px 20px; + overflow-y: auto; + min-height: 0; +} +.page h2 { + margin: 0 0 4px; + font-size: 14px; +} +.page__lede { + margin: 0 0 18px; + color: var(--text-dim); + font-size: 11.5px; + max-width: 68ch; +} + +.field { + display: grid; + grid-template-columns: 200px minmax(0, 320px); + align-items: center; + gap: 12px; + padding: 7px 0; + border-bottom: 1px solid var(--border); + max-width: 560px; +} +.field__k { + font-size: 11.5px; +} +.field__hint { + grid-column: 1 / -1; + font-size: 10.5px; + color: var(--text-faint); + margin-top: -4px; +} +.field input[type="text"], +.field input[type="number"] { + width: 100%; + height: 24px; + padding: 0 8px; + background: var(--bg-sunken); + border: 1px solid var(--border-input); + border-radius: 3px; + user-select: text; + cursor: text; + font-family: var(--mono); + font-size: 11.5px; +} +.field input[type="checkbox"] { + width: 14px; + height: 14px; + accent-color: var(--accent-deep); + cursor: pointer; +} + +.simple-list { + border: 1px solid var(--border); + border-radius: 3px; + overflow: hidden; + max-width: 760px; +} +.simple-list__row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + align-items: center; + gap: 10px; + padding: 5px 10px; + border-bottom: 1px solid var(--border-muted); + background: var(--bg-panel); +} +.simple-list__row:last-child { + border-bottom: 0; +} +.simple-list__row:hover { + background: var(--bg-hover); +} +.simple-list__name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; +} +.simple-list__meta { + font-family: var(--mono); + font-size: 10.5px; + color: var(--text-faint); + white-space: nowrap; +} + +/* ------------------------------------------------------------ history chart */ + +.rangeswitch { + display: flex; + gap: 2px; +} +.rangeswitch button { + height: 16px; + padding: 0 5px; + border: 1px solid var(--border-strong); + background: var(--bg-raised); + color: var(--text-faint); + border-radius: 2px; + cursor: pointer; + font-family: var(--mono); + font-size: 9.5px; + line-height: 1; +} +.rangeswitch button:hover { + color: var(--text); +} +.rangeswitch button[data-on="true"] { + color: var(--accent); + border-color: var(--accent-deep); + background: var(--accent-bg); +} + +.chart { + display: block; + width: 100%; + height: 56px; + background: var(--bg-sunken); + border: 1px solid var(--border); + border-radius: 3px; +} +.chart__band { + fill: var(--accent); + fill-opacity: 0.16; + stroke: none; +} +.chart__line { + fill: none; + stroke: var(--accent); + stroke-width: 1; + stroke-linejoin: round; +} + +.chart__legend { + display: flex; + gap: 10px; + margin-top: 4px; + font-family: var(--mono); + font-size: 10px; + color: var(--text-faint); +} +.chart__legend b { + color: var(--text-dim); + font-weight: 600; +} +.chart__bucket { + margin-left: auto; +} + +/* --------------------------------------------------------- mod readiness */ + +/* Information only: no action attached, so it is a note rather than a card. + The state is carried by a dot, not a coloured edge down the side — the same + treatment that made the nav look wrong. */ +.readiness { + margin-bottom: 6px; + padding: 7px 9px; + border: 1px solid var(--border); + border-radius: 3px; + background: var(--bg-sunken); +} + +.readiness__line { + display: flex; + align-items: baseline; + gap: 7px; + margin: 0; + font-size: 11px; + line-height: 1.45; + color: var(--text-dim); +} +.readiness[data-tone="ready"] .readiness__line { + color: var(--text); +} + +.readiness__dot { + flex: none; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--text-faint); + transform: translateY(-1px); +} +.readiness[data-tone="ready"] .readiness__dot { + background: var(--good); +} +.readiness[data-tone="action"] .readiness__dot { + background: var(--warn); +} + +/* Label over figure, so the numbers read as a row of small stats rather than + a sentence fragment. */ +.readiness__counts { + display: flex; + flex-wrap: wrap; + gap: 14px; + margin: 7px 0 0; + padding-top: 7px; + border-top: 1px solid var(--border-muted); +} +.readiness__counts div { + display: flex; + flex-direction: column; + gap: 1px; +} +.readiness__counts dt { + font-size: 9.5px; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--text-faint); +} +.readiness__counts dd { + margin: 0; + font-family: var(--mono); + font-size: 12px; + font-variant-numeric: tabular-nums; + color: var(--text-dim); +} +.readiness__counts div:last-child { + margin-left: auto; + align-items: flex-end; +} + +select.control--wide { + max-width: 170px; +} + +/* ------------------------------------------------------- scenario imagery */ + +/* 16:9 banner at the top of the inspector. Fixed ratio so the panel does not + jump as images load, and cropped rather than letterboxed. */ +.scenario { + position: relative; + aspect-ratio: 16 / 9; + background: var(--bg-sunken); + border-bottom: 1px solid var(--border); + overflow: hidden; + flex: none; +} +.scenario__img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} +/* Fade the lower edge so the title below it stays legible over busy artwork. */ +.scenario::after { + content: ""; + position: absolute; + inset: auto 0 0 0; + height: 46%; + background: linear-gradient(to bottom, transparent, var(--bg-panel)); + pointer-events: none; +} + +.fact__v--muted { + color: var(--text-faint); +} + +.identity { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} +.identity code { + font-family: var(--mono); + font-size: 11px; + color: var(--text-dim); + background: var(--bg-sunken); + border: 1px solid var(--border-strong); + border-radius: 3px; + padding: 2px 6px; + user-select: text; +} + +/* Discord invite, when a server advertises one in its name. */ +.btn--discord { + width: 100%; + margin-top: 6px; + border-color: #454b7a; + color: #b3baf5; +} +.btn--discord:hover:not(:disabled) { + background: #2b2f4d; + border-color: #5865f2; + color: #fff; +} + +/* Wordmark, coloured as on reforgermods.net. */ +.wordmark { + display: flex; + align-items: baseline; + gap: 1px; + font-size: 13px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: lowercase; + white-space: nowrap; + padding-left: 8px; + border-left: 1px solid var(--border); +} +.wordmark__reforger { + color: #f4f4f1; +} +.wordmark__mods { + color: var(--accent-strong); +} +.wordmark__net { + color: #aab6c3; +} +.wordmark__app { + font-style: normal; + font-weight: 500; + color: var(--text-faint); + margin-left: 6px; +} + +/* Separator between the two header counts. */ +.titlebar__dot { + display: inline-block; + width: 1px; + height: 9px; + margin: 0 7px; + vertical-align: -1px; + background: var(--border-strong); +} + +/* Platform tags are informational, not status: keep them quieter than BE. */ + diff --git a/src/views/BrowseView.tsx b/src/views/BrowseView.tsx new file mode 100644 index 0000000..f6fade9 --- /dev/null +++ b/src/views/BrowseView.tsx @@ -0,0 +1,133 @@ +/** The server browser: filters, table, and the pager/status footer. */ + +import { FilterBar } from "../components/FilterBar"; +import { ServerTable } from "../components/ServerTable"; +import { + clearFilters, + filteredOut, + listError, + listInitialising, + listLoading, + loadServers, + query, + setQuery, + sortIsPageLocal, + activeSort, + totalPages, + totalServers, + visibleServers, +} from "../lib/state"; +import { COLUMN_LABEL } from "../lib/sort"; + +export function BrowseView() { + const rows = visibleServers.value; + const error = listError.value; + const loading = listLoading.value; + const initialising = listInitialising.value; + const q = query.value; + + return ( +
+ + {loading ?
:
} + + {error && ( +