diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..676007d1b --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,22 @@ +[target.'cfg(any(target_env = "msvc", target_env = "musl"))'] +rustflags = ["-C", "target-feature=+crt-static"] + +[target.'cfg(target_arch = "x86_64")'] +rustflags = ["-C", "target-cpu=x86-64-v3"] + +[target.'cfg(all(target_arch = "aarch64", not(target_vendor = "apple")))'] +rustflags = ["-C", "target-cpu=cortex-a76"] + +[target.'cfg(all(windows, target_env = "msvc"))'] +linker = "rust-lld" + +# Avoid linking with vcruntime140.dll by statically linking everything, +# and then explicitly linking with ucrtbase.dll dynamically. +# We do this, because vcruntime140.dll is an optional Windows component. +[target.'cfg(all(target_os = "windows", target_env = "msvc"))'] +rustflags = [ + "-Clink-args=/DEFAULTLIB:ucrt.lib", + "-Clink-args=/NODEFAULTLIB:vcruntime.lib", + "-Clink-args=/NODEFAULTLIB:msvcrt.lib", + "-Clink-args=/NODEFAULTLIB:libucrt.lib", +] diff --git a/.gitattributes b/.gitattributes index 24db350c9..8baee6069 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,18 @@ -# Auto detect text files and perform normalization -* text=auto +# Auto detect text files and perform LF normalization +* text=auto -*.rs text diff=rust -*.toml text diff=toml -Cargo.lock text + +*.ts text eol=lf merge=union +*.tsx text eol=lf merge=union +*.rs text eol=lf merge=union +*.js text eol=lf merge=union +*.json text eol=lf merge=union +*.debug text eol=lf merge=union + +# Generated codes +index.js linguist-detectable=false +index.d.ts linguist-detectable=false +anthelion-komac.wasi-browser.js linguist-detectable=false +anthelion-komac.wasi.cjs linguist-detectable=false +wasi-worker-browser.mjs linguist-detectable=false +wasi-worker.mjs linguist-detectable=false diff --git a/.github/workflows/release_anthelion.yml b/.github/workflows/release_anthelion.yml new file mode 100644 index 000000000..18d22c48b --- /dev/null +++ b/.github/workflows/release_anthelion.yml @@ -0,0 +1,287 @@ +name: Release + +on: + workflow_dispatch: + +env: + DEBUG: napi:* + APP_NAME: anthelion-komac + DEV_DRIVE_WORKSPACE: ${{ github.workspace }} + +defaults: + run: + shell: bash -euo pipefail {0} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + build-cli: + name: Build CLI - ${{ matrix.platform.target }} + + strategy: + fail-fast: false + + # Cross compiling from arm64 runners is faster than compiling on x64 runners + matrix: + platform: + - os: windows-11-arm + target: x86_64-pc-windows-msvc + - os: windows-11-arm + target: aarch64-pc-windows-msvc + - os: ubuntu-24.04-arm + target: x86_64-unknown-linux-musl + - os: ubuntu-24.04-arm + target: aarch64-unknown-linux-musl + - os: macos-26 + target: aarch64-apple-darwin + - os: macos-26 + target: x86_64-apple-darwin + + runs-on: ${{ matrix.platform.os }} + + env: + RUST_TARGET: ${{ matrix.platform.target }} + + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Setup Dev Drive + if: ${{ contains(matrix.platform.os, 'windows') }} + uses: samypr100/setup-dev-drive@30f0f98ae5636b2b6501e181dfb3631b9974818d # v4.0.0 + with: + workspace-copy: true + drive-size: 12GB + drive-format: ReFS + env-mapping: | + CARGO_HOME,{{ DEV_DRIVE }}/.cargo + RUSTUP_HOME,{{ DEV_DRIVE }}/.rustup + + - parallel: + - name: Setup Rust + uses: moonrepo/setup-rust@abb2d32350334249b178c401e5ec5836e0cd88d3 # v1.3.0 + with: + channel: nightly + targets: ${{ matrix.platform.target }} + cache: false + + - name: Setup Zig + uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + if: ${{ contains(matrix.platform.target, 'linux') }} + with: + version: master + use-cache: false + + - name: Install Tools + uses: taiki-e/install-action@bffeee26d4db9be238a4ea78d8826604ebcb594d # v2.82.5 + if: ${{ contains(matrix.platform.target, 'linux') }} + with: + tool: cargo-zigbuild + + - name: Build Binary + working-directory: ${{ env.DEV_DRIVE_WORKSPACE }} + run: | + [[ "${RUNNER_OS}" == "Linux" ]] && subcommand="zigbuild" || subcommand="build" + + cargo "${subcommand}" --target "${RUST_TARGET}" --bin komac --release --locked + + - name: Package Binary + id: package + working-directory: ${{ env.DEV_DRIVE_WORKSPACE }} + run: | + bin="komac" + artifact_name="komac-nightly-${RUST_TARGET}" + + if [[ "${RUNNER_OS}" == "Windows" ]]; then + bin="komac.exe" + artifact_name+=".exe" + mv "target/${RUST_TARGET}/release/${bin}" "${artifact_name}" + echo "file=${artifact_name}" >> "$GITHUB_OUTPUT" + else + archive_name="${artifact_name}.tar.zst" + tar -cvf "${archive_name}" \ + --use-compress-program="zstd" \ + -C "target/${RUST_TARGET}/release" \ + "${bin}" + echo "file=${archive_name}" >> "$GITHUB_OUTPUT" + fi + + echo "artifact_name=cli-${RUST_TARGET}" >> "$GITHUB_OUTPUT" + + - name: Upload Artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ steps.package.outputs.artifact_name }} + path: ${{ env.DEV_DRIVE_WORKSPACE }}/${{ steps.package.outputs.file }} + if-no-files-found: error + + build-js-bindings: + name: Build JS Bindings - ${{ matrix.platform.target }} + + strategy: + fail-fast: false + + # Cross compiling from arm64 runners is faster than compiling on x64 runners + matrix: + platform: + - os: windows-11-arm + target: x86_64-pc-windows-msvc + - os: windows-11-arm + target: aarch64-pc-windows-msvc + - os: ubuntu-24.04-arm + target: x86_64-unknown-linux-gnu + - os: ubuntu-24.04-arm + target: aarch64-unknown-linux-gnu + - os: ubuntu-24.04-arm + target: x86_64-unknown-linux-musl + - os: ubuntu-24.04-arm + target: aarch64-unknown-linux-musl + - os: macos-26 + target: aarch64-apple-darwin + - os: macos-26 + target: x86_64-apple-darwin + + runs-on: ${{ matrix.platform.os }} + + env: + RUST_TARGET: ${{ matrix.platform.target }} + + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Setup Dev Drive + if: ${{ contains(matrix.platform.os, 'windows') }} + uses: samypr100/setup-dev-drive@30f0f98ae5636b2b6501e181dfb3631b9974818d # v4.0.0 + with: + workspace-copy: true + drive-size: 12GB + drive-format: ReFS + env-mapping: | + CARGO_HOME,{{ DEV_DRIVE }}/.cargo + RUSTUP_HOME,{{ DEV_DRIVE }}/.rustup + + - parallel: + - name: Setup Rust + uses: moonrepo/setup-rust@abb2d32350334249b178c401e5ec5836e0cd88d3 # v1.3.0 + with: + channel: nightly + targets: ${{ matrix.platform.target }} + cache: false + + - name: Setup Zig + uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + if: ${{ contains(matrix.platform.target, 'linux') }} + with: + version: master + use-cache: false + + - name: Install Tools + uses: taiki-e/install-action@bffeee26d4db9be238a4ea78d8826604ebcb594d # v2.82.5 + if: ${{ contains(matrix.platform.target, 'linux') }} + with: + tool: cargo-zigbuild + + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + + - name: Install Dependencies + working-directory: ${{ env.DEV_DRIVE_WORKSPACE }} + run: bun ci + + - name: Build Binary + working-directory: ${{ env.DEV_DRIVE_WORKSPACE }} + run: | + [[ "${RUNNER_OS}" == "Linux" ]] && zigbuild_flag="-x" || zigbuild_flag="" + + bun run build --target "${RUST_TARGET}" ${zigbuild_flag} -- --locked + + - name: Upload Artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: bindings-${{ matrix.platform.target }} + path: | + ${{ env.DEV_DRIVE_WORKSPACE }}/${{ env.APP_NAME }}.*.node + ${{ env.DEV_DRIVE_WORKSPACE }}/${{ env.APP_NAME }}.*.wasm + ${{ env.DEV_DRIVE_WORKSPACE }}/index.js + ${{ env.DEV_DRIVE_WORKSPACE }}/index.d.ts + if-no-files-found: error + + release: + name: Publish Release + + needs: + - build-cli + - build-js-bindings + + runs-on: ubuntu-slim + + permissions: + contents: write # Required to publish GitHub release + id-token: write # Required for npm Trusted Publishing + + steps: + - parallel: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Download Artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: ./artifacts + + - parallel: + - name: Create Release + run: | + shopt -s extglob + gh release delete nightly --yes --cleanup-tag 2>/dev/null || true + sleep 5 # cli/cli#8458 + gh release create nightly \ + --target "anthelion" \ + --title "Anthelion komac Build" \ + --latest \ + --notes "Custom build of komac for use with [UnownPlain/anthelion](https://github.com/UnownPlain/anthelion)" \ + artifacts/**/!(*.js|*.ts|*.d.ts) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + + # Publish npm + + - name: Overwrite README + run: | + echo 'komac JS bindings for Anthelion' > README.md + + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + no-cache: true + + - name: Copy JS files + run: cp artifacts/bindings-x86_64-unknown-linux-gnu/{index.js,index.d.ts} . + + - name: Install Dependencies + run: bun ci + + - name: Create npm Directories + run: bun napi create-npm-dirs + + - name: Move Artifacts + run: bun run artifacts + + - name: List Packages + run: ls -R ./npm + + - name: Publish to npm + run: | + npm publish --provenance --access public diff --git a/.github/workflows/renovate.yml b/.github/workflows/renovate.yml deleted file mode 100644 index 5fb748d8d..000000000 --- a/.github/workflows/renovate.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Validate Renovate Config - -on: - push: - paths: - - .github/renovate.json - pull_request: - paths: - - .github/renovate.json - -jobs: - validate: - name: Validate Renovate Config - runs-on: ubuntu-latest - container: ghcr.io/renovatebot/renovate - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Validate renovate.json - run: renovate-config-validator diff --git a/.github/workflows/setup-dev-drive.ps1 b/.github/workflows/setup-dev-drive.ps1 new file mode 100644 index 000000000..c20e8cf66 --- /dev/null +++ b/.github/workflows/setup-dev-drive.ps1 @@ -0,0 +1,82 @@ +# Configures a drive for testing in CI. +# +# When using standard GitHub Actions runners, a `D:` drive is present and has +# similar or better performance characteristics than a ReFS dev drive. Sometimes +# using a larger runner is still more performant (e.g., when running the test +# suite) and we need to create a dev drive. This script automatically configures +# the appropriate drive. +# +# When using GitHub Actions' "larger runners", the `D:` drive is not present and +# we create a DevDrive mount on `C:`. This is purported to be more performant +# than an ReFS drive, though we did not see a change when we switched over. +# +# When using self-hosted runners, the underlying infrastructure may not +# support Hyper-V. The `New-VHD` commandlet only works with Hyper-V, but we can +# create a ReFS drive using `diskpart` and `format` directory. We cannot use a +# DevDrive, as that also requires Hyper-V. Self-hosted runners may use `D:` +# already, so we must check if it's self-hosted first, and we use `V:` as the +# target instead. + + +if ($env:RUNNER_ENVIRONMENT -eq "self-hosted") { + Write-Output "Self-hosted runner detected, setting up custom dev drive..." + + # Create VHD and configure drive using diskpart + $vhdPath = "C:\dev_drive.vhdx" + @" +create vdisk file="$vhdPath" maximum=25600 type=expandable +attach vdisk +create partition primary +active +assign letter=V +"@ | diskpart + + # Format the drive as ReFS + format V: /fs:ReFS /q /y + $Drive = "V:" + + Write-Output "Custom dev drive created at $Drive" +} elseif (Test-Path "D:\") { + # Note `Get-PSDrive` is not sufficient because the drive letter is assigned. + Write-Output "Using existing drive at D:" + $Drive = "D:" +} else { + # The size (25 GB) is chosen empirically to be large enough for our + # workflows; larger drives can take longer to set up. + $Volume = New-VHD -Path C:/dev_drive.vhdx -SizeBytes 25GB | + Mount-VHD -Passthru | + Initialize-Disk -Passthru | + New-Partition -AssignDriveLetter -UseMaximumSize | + Format-Volume -DevDrive -Confirm:$false -Force + + $Drive = "$($Volume.DriveLetter):" + + # Set the drive as trusted + # See https://learn.microsoft.com/en-us/windows/dev-drive/#how-do-i-designate-a-dev-drive-as-trusted + fsutil devdrv trust $Drive + + # Disable antivirus filtering on dev drives + # See https://learn.microsoft.com/en-us/windows/dev-drive/#how-do-i-configure-additional-filters-on-dev-drive + fsutil devdrv enable /disallowAv + + # Remount so the changes take effect + Dismount-VHD -Path C:/dev_drive.vhdx + Mount-VHD -Path C:/dev_drive.vhdx + + # Show some debug information + Write-Output $Volume + fsutil devdrv query $Drive + + Write-Output "Using Dev Drive at $Volume" +} + +# Prepare Cargo and rustup homes on the dev drive before setup-rust installs the toolchain. +New-Item -Path "$($Drive)/.cargo/bin" -ItemType Directory -Force +New-Item -Path "$($Drive)/.rustup" -ItemType Directory -Force + +Write-Output ` + "DEV_DRIVE=$($Drive)" ` + "RUSTUP_HOME=$($Drive)/.rustup" ` + "CARGO_HOME=$($Drive)/.cargo" ` + "PATH=$($Drive)/.cargo/bin;$env:PATH" ` + >> $env:GITHUB_ENV diff --git a/.github/workflows/update-graphql-schema.yml b/.github/workflows/update-graphql-schema.yml deleted file mode 100644 index 3aa1d9903..000000000 --- a/.github/workflows/update-graphql-schema.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Update local copy of GraphQL schema - -on: - workflow_dispatch: - schedule: - - cron: '0 0 1 * *' - -jobs: - update-schema: - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - - steps: - - name: Clone repository - uses: actions/checkout@v6 - - - name: Download latest GitHub GraphQL Schema - run: | - curl \ - --location \ - --fail \ - --silent \ - --show-error \ - --dump-header headers.txt \ - --output assets/github.graphql \ - https://docs.github.com/public/fpt/schema.docs.graphql - - - - name: Get last modified date - id: schema_last_modified - run: | - last_modified=$( - grep --ignore-case '^last-modified:' headers.txt 2>/dev/null \ - | sed --expression 's/^[Ll]ast-[Mm]odified:[[:space:]]*//' \ - | tr --delete '\r' - ) - - rm --force headers.txt - - if [ -n "$last_modified" ]; then - echo "Schema Last-Modified header: ${last_modified}" - if ! last_modified=$(date --utc --date "$last_modified" '+%A %d %b %Y %H:%M:%S' 2>/dev/null); then - echo "Failed to parse Last-Modified date, using current UTC time" - last_modified=$(date --utc '+%A %d %b %Y %H:%M:%S') - fi - else - echo "No Last-Modified header found, using current UTC time" - last_modified=$(date --utc '+%A %d %b %Y %H:%M:%S') - fi - - echo "LAST_MODIFIED=${last_modified}" >> $GITHUB_OUTPUT - - - name: Create Pull Request - uses: peter-evans/create-pull-request@v8 - with: - commit-message: "Update GitHub GraphQL Schema" - branch: update-github-graphql-schema - title: "Update GitHub GraphQL Schema" - body: "This is an automated pull request to update the local GitHub GraphQL schema as of ${{ steps.schema_last_modified.outputs.LAST_MODIFIED }}" diff --git a/.github/workflows/update-inno-dependencies.yml b/.github/workflows/update-inno-dependencies.yml deleted file mode 100644 index ff7446127..000000000 --- a/.github/workflows/update-inno-dependencies.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Update local copy of Inno Setup Dependency Installer script - -on: - workflow_dispatch: - schedule: - - cron: '0 9 1 * *' - -jobs: - update-schema: - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - - steps: - - name: Clone repository - uses: actions/checkout@v6 - - - name: Download latest Inno Setup Dependency Installer script - run: | - curl --location \ - --output assets/inno/CodeDependencies.iss \ - --fail \ - --silent \ - --show-error \ - https://github.com/DomGries/InnoDependencyInstaller/raw/HEAD/CodeDependencies.iss - - - name: Create Pull Request - uses: peter-evans/create-pull-request@v8 - with: - commit-message: "Update Inno Setup Dependency Installer script" - branch: update-inno-dependencies-script - title: "Update Inno Setup Dependency Installer script" - body: "This is an automated pull request to update the Inno Setup Dependency Installer script from [DomGries/InnoDependencyInstaller](https://github.com/DomGries/InnoDependencyInstaller)" diff --git a/.github/workflows/vhs.yml b/.github/workflows/vhs.yml deleted file mode 100644 index ca84c021b..000000000 --- a/.github/workflows/vhs.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: vhs -on: - workflow_dispatch: - inputs: - tape: - description: 'Tape' - required: true - default: 'all' - type: choice - options: - - all - - demo.tape - - new_package.tape - - sync.tape - -jobs: - vhs: - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Install Rust - uses: moonrepo/setup-rust@v1 - with: - cache-target: release - - - name: Setup Go - uses: actions/setup-go@v6 - - - name: Install VHS - run: | - sudo apt update - sudo apt install -y ffmpeg ttyd - go install github.com/charmbracelet/vhs@latest - - - name: Install komac - run: cargo install --locked --path . - - - name: Recreate all VHS GIFs - if: ${{ inputs.tape == 'all' }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - for tape in demo.tape new_package.tape sync.tape; do - vhs "assets/vhs/$tape" - done - - - name: Recreate specific VHS GIF - if: ${{ inputs.tape != 'all' }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: vhs "assets/vhs/${{ inputs.tape }}" - - - name: Set PR metadata - id: prmeta - run: | - if [ "${{ inputs.tape }}" = "all" ]; then - echo "title=Update all VHS GIFs" >> $GITHUB_OUTPUT - echo "branch=update-all-gifs" >> $GITHUB_OUTPUT - echo "body=This pull request updates all demo recordings." >> $GITHUB_OUTPUT - else - base="${TAPE%.tape}" - echo "title=Update ${base}.gif" >> $GITHUB_OUTPUT - echo "branch=update-${base}-gif" >> $GITHUB_OUTPUT - echo "body=This pull request updates the ${base}.gif recording from assets/vhs/${TAPE}." >> $GITHUB_OUTPUT - fi - - - name: Create Pull Request - uses: peter-evans/create-pull-request@v8 - with: - commit-message: ${{ steps.prmeta.outputs.title }} - branch: ${{ steps.prmeta.outputs.branch }} - title: ${{ steps.prmeta.outputs.title }} - body: ${{ steps.prmeta.outputs.body }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1e9f70916..3fd453881 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,74 @@ target .idea/ .env + +# Anthelion Start + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# dotenv environment variables file +.env +.env.test + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +*.node +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions +/npm +index.js +index.d.ts +test.ts + +# Anthelion End diff --git a/Cargo.lock b/Cargo.lock index e7b55aefa..6cf8fae95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,9 +121,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "apple-native-keyring-store" @@ -147,9 +147,20 @@ dependencies = [ [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "async-trait" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] [[package]] name = "atomic" @@ -168,15 +179,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.16.2" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" dependencies = [ "aws-lc-sys", "zeroize", @@ -184,14 +195,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.39.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] @@ -271,9 +283,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", ] @@ -298,9 +310,9 @@ dependencies = [ [[package]] name = "bon" -version = "3.9.1" +version = "3.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe" +checksum = "a602c73c7b0148ec6d12af6fd5cc7a46e2eacc8878271a999abac56eed12f561" dependencies = [ "bon-macros", "rustversion", @@ -308,9 +320,9 @@ dependencies = [ [[package]] name = "bon-macros" -version = "3.9.1" +version = "3.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" +checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f" dependencies = [ "darling 0.23.0", "ident_case", @@ -318,7 +330,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -332,9 +344,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "by_address" @@ -362,7 +374,7 @@ checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -379,9 +391,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "bzip2" @@ -406,9 +418,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.2" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" dependencies = [ "serde_core", ] @@ -442,9 +454,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.57" +version = "1.2.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" dependencies = [ "find-msvc-tools", "jobserver", @@ -452,12 +464,6 @@ dependencies = [ "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.14.0" @@ -483,13 +489,13 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "rand_core 0.10.0", + "rand_core 0.10.1", ] [[package]] @@ -550,9 +556,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.6.5" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772" +checksum = "db8b397918185f0161ff3d6fcaa9e4bfc09b8367caf6e1d4a2848e5477ed027b" dependencies = [ "clap", ] @@ -566,7 +572,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -577,9 +583,9 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] @@ -639,9 +645,9 @@ dependencies = [ [[package]] name = "console" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" dependencies = [ "encode_unicode", "libc", @@ -686,6 +692,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -753,18 +768,18 @@ checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crossterm" @@ -822,6 +837,12 @@ dependencies = [ "phf 0.11.3", ] +[[package]] +name = "ctor" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb22e947478ccf9dc44d8922042c677a63fbb88f2cb468521d1145816e5087cb" + [[package]] name = "cynic" version = "3.13.2" @@ -851,7 +872,7 @@ dependencies = [ "quote", "rkyv", "strsim 0.10.0", - "syn 2.0.117", + "syn 2.0.118", "thiserror 1.0.69", ] @@ -875,7 +896,7 @@ dependencies = [ "cynic-codegen", "darling 0.20.11", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -909,7 +930,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -922,7 +943,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -933,7 +954,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -944,18 +965,18 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "dbus" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" dependencies = [ "libc", "libdbus-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -987,6 +1008,37 @@ dependencies = [ "keyring-core", ] +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "deltae" version = "0.3.2" @@ -999,7 +1051,6 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", "serde_core", ] @@ -1018,11 +1069,11 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "convert_case", + "convert_case 0.10.0", "proc-macro2", "quote", "rustc_version", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1038,24 +1089,24 @@ dependencies = [ [[package]] name = "digest" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "const-oid", "crypto-common 0.2.2", ] [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1101,15 +1152,15 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "embed-resource" -version = "3.0.8" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63a1d0de4f2249aa0ff5884d7080814f446bb241a559af6c170a41e878ed2d45" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" dependencies = [ "cc", "memchr", @@ -1185,17 +1236,11 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" -[[package]] -name = "fast32" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35a73237400bde66c82e38387343f90d7182a2f2f22729e096a2abd57d75db9" - [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "filedescriptor" @@ -1244,12 +1289,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -1286,6 +1325,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -1293,6 +1347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1301,6 +1356,17 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.32" @@ -1315,7 +1381,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1332,9 +1398,9 @@ checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" @@ -1342,6 +1408,7 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1371,6 +1438,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -1391,25 +1467,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 5.3.0", "wasip2", - "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", - "rand_core 0.10.0", - "wasip2", - "wasip3", + "rand_core 0.10.1", + "wasm-bindgen", ] [[package]] @@ -1426,9 +1500,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "h2" -version = "0.4.13" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", @@ -1458,15 +1532,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - [[package]] name = "hashbrown" version = "0.16.1" @@ -1475,25 +1540,25 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.2.0", + "foldhash", ] [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.2.0", + "foldhash", ] [[package]] name = "heapless" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" dependencies = [ "hash32", "serde_core", @@ -1566,9 +1631,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -1605,18 +1670,18 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hybrid-array" -version = "0.4.8" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ "typenum", ] [[package]] name = "hyper" -version = "1.8.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -1628,7 +1693,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -1636,15 +1700,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -1717,13 +1780,14 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", "serde", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1731,9 +1795,9 @@ dependencies = [ [[package]] name = "icu_locale" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532b11722e350ab6bf916ba6eb0efe3ee54b932666afec989465f9243fe6dd60" +checksum = "d5a396343c7208121dc86e35623d3dfe19814a7613cfd14964994cdc9c9a2e26" dependencies = [ "icu_collections", "icu_locale_core", @@ -1746,9 +1810,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1760,9 +1824,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1774,15 +1838,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1794,15 +1858,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1815,12 +1879,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -1840,9 +1898,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1872,7 +1930,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -1893,19 +1951,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f85dac6c239acc85fd61934c572292d93adfd2de459d9c032aa22b553506e915" dependencies = [ "either", - "itertools", + "itertools 0.14.0", "proc-macro2", "quote", "strum 0.27.2", - "syn 2.0.117", + "syn 2.0.118", "thiserror 2.0.18", ] [[package]] name = "indicatif" -version = "0.18.4" +version = "0.18.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" dependencies = [ "console", "portable-atomic", @@ -1935,7 +1993,7 @@ dependencies = [ "crc32fast", "encoding_rs", "flate2", - "itertools", + "itertools 0.14.0", "liblzma", "nt-time 0.13.2", "thiserror 2.0.18", @@ -1986,7 +2044,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1995,16 +2053,6 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -[[package]] -name = "iri-string" -version = "0.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is-docker" version = "0.2.0" @@ -2039,6 +2087,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -2047,10 +2104,11 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.23" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +checksum = "ccfe6121cbe750cf81efa362d85c0bde7ea298ec43092d3a193baca59cdbd634" dependencies = [ + "defmt", "jiff-static", "log", "portable-atomic", @@ -2060,38 +2118,43 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.23" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "e165e897f662d428f3cd3828a919dbe067c2d42bb1031eede74ef9d27ecdedd2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "jni" -version = "0.21.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "cesu8", "cfg-if", "combine", - "jni-sys 0.3.1", + "jni-macros", + "jni-sys", "log", - "thiserror 1.0.69", + "simd_cesu8", + "thiserror 2.0.18", "walkdir", - "windows-sys 0.45.0", + "windows-link", ] [[package]] -name = "jni-sys" -version = "0.3.1" +name = "jni-macros" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" dependencies = [ - "jni-sys 0.4.1", + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.118", ] [[package]] @@ -2110,26 +2173,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -2187,24 +2251,30 @@ dependencies = [ "indoc", "inno", "inquire", - "itertools", + "itertools 0.14.0", "itoa", "keyring-core", "liblzma", "memchr", "msi", + "napi", + "napi-build", + "napi-derive", "nt-time 0.15.0", "num_cpus", "open", "ordinal", "owo-colors", "percent-encoding", + "pulldown-cmark", "quick-xml", - "rand 0.10.1", + "rand 0.10.2", "ratatui", "ratatui-textarea", "regex", "reqwest", + "reqwest-middleware", + "reqwest-retry", "rstest", "secrecy", "serde", @@ -2273,23 +2343,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libbz2-rs-sys" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" +checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libdbus-sys" @@ -2301,20 +2365,30 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "liblzma" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6033b77c21d1f56deeae8014eb9fbe7bdf1765185a6c508b5ca82eeaed7f899" +checksum = "45aec2360b3933207e27908049d8e4df4e476b58180afb1e56b2a4fb72efe4ba" dependencies = [ "liblzma-sys", ] [[package]] name = "liblzma-sys" -version = "0.4.5" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f2db66f3268487b5033077f266da6777d057949b8f93c8ad82e441df25e6186" +checksum = "a046c7f353ba30f810545151e04f63545833803f5b86ee3ddf1517247fe560a5" dependencies = [ "cc", "libc", @@ -2335,9 +2409,9 @@ checksum = "2e126dda6f34391ab7b444f9922055facc83c07a910da3eb16f1e4d9c45dc777" [[package]] name = "libz-sys" -version = "1.1.25" +version = "1.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" dependencies = [ "cc", "pkg-config", @@ -2346,9 +2420,9 @@ dependencies = [ [[package]] name = "line-clipping" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" +checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" dependencies = [ "bitflags 2.13.0", ] @@ -2361,9 +2435,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "litrs" @@ -2382,9 +2456,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "logos" @@ -2407,7 +2481,7 @@ dependencies = [ "proc-macro2", "quote", "regex-syntax", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2425,7 +2499,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" dependencies = [ - "hashbrown 0.17.0", + "hashbrown 0.17.1", ] [[package]] @@ -2436,15 +2510,15 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "lzma-rust2" -version = "0.16.2" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae" +checksum = "ca93e534d1142d1d0dcca6d25fe302508a5dfb40b302802904577725ea0b695b" [[package]] name = "lzxd" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b29dffab797218e12e4df08ef5d15ab9efca2504038b1b32b9b32fc844b39c9" +checksum = "c17f346186eccb574ba5581acefc514f0c70a642db4f96e245034a0a158a7168" [[package]] name = "mac_address" @@ -2521,9 +2595,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "log", @@ -2560,7 +2634,65 @@ checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", +] + +[[package]] +name = "napi" +version = "3.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c71997d6f7ad4a756966e452426848ac27d3b37a295302d63afbbcce0270f93" +dependencies = [ + "bitflags 2.13.0", + "ctor", + "futures", + "napi-build", + "napi-sys", + "nohash-hasher", + "rustc-hash", + "tokio", +] + +[[package]] +name = "napi-build" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1" + +[[package]] +name = "napi-derive" +version = "3.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ba572deef53e2c386759a8c2014175a62679d74ff83adc205c8bc0e0285727" +dependencies = [ + "convert_case 0.11.0", + "ctor", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "napi-derive-backend" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd961eb2aa8965e3f29722d754f3a86907eb1984e2fbcbe3fe87b9a02d6bfba" +dependencies = [ + "convert_case 0.11.0", + "proc-macro2", + "quote", + "semver", + "syn 2.0.118", +] + +[[package]] +name = "napi-sys" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f5bcdf71abd3a50d00b49c1c2c75251cb3c913777d6139cd37dabc093a5e400" +dependencies = [ + "libloading", ] [[package]] @@ -2599,6 +2731,12 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "nom" version = "7.1.3" @@ -2628,7 +2766,7 @@ dependencies = [ "chrono", "dos-date-time 0.3.0", "jiff", - "rand 0.10.1", + "rand 0.10.2", "time", ] @@ -2657,9 +2795,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", @@ -2676,9 +2814,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-derive" @@ -2688,7 +2826,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2773,26 +2911,24 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "open" -version = "5.3.5" +version = "5.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" +checksum = "cd8d3b65c44123a56e0133d2cd06ce4361bd3ca99d41198b2f25e3c3db9b8b4a" dependencies = [ "is-wsl", "libc", - "pathdiff", ] [[package]] name = "openssl" -version = "0.10.76" +version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ "bitflags 2.13.0", "cfg-if", "foreign-types", "libc", - "once_cell", "openssl-macros", "openssl-sys", ] @@ -2805,7 +2941,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2816,18 +2952,18 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-src" -version = "300.5.5+3.5.5" +version = "300.6.1+3.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709" +checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.112" +version = "0.9.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" dependencies = [ "cc", "libc", @@ -2872,7 +3008,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2883,14 +3019,12 @@ checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] name = "package-family-name" -version = "2.1.2" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78e7c144b7a3401bea37f4f0a17bf2cf33d188db130e9967f172b5d7b2dc3310" +checksum = "cbdea4b26c3ae1459694aa1237ad5246dc46b9f8840a8401f6da26b8eefa8833" dependencies = [ - "fast32", - "heapless", "serde_core", - "sha2 0.10.9", + "sha2 0.11.0", "thiserror 2.0.18", ] @@ -2915,7 +3049,7 @@ dependencies = [ "by_address", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2941,12 +3075,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - [[package]] name = "percent-encoding" version = "2.3.2" @@ -2955,9 +3083,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.6" +version = "2.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +checksum = "47627dd7305c6a2d6c8c6bcd24c5a4c17dbbf425f4f9c5313e724b38fc9782e9" dependencies = [ "memchr", "ucd-trie", @@ -2965,9 +3093,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.6" +version = "2.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +checksum = "4b4254325ecad416ab689e27ba51da03ba01a9632bc6e108f5fe7c3c4ad29d58" dependencies = [ "pest", "pest_generator", @@ -2975,25 +3103,24 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.6" +version = "2.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +checksum = "6c4c0e91ead7a8f7acecbca6f003fc2e8282b1dbe2dd9c9d2f16aba42995e0a7" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "pest_meta" -version = "2.8.6" +version = "2.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +checksum = "f9744bc48116fee06334924bb5f2bad41eed5e89bd26e29b0b799f9a3f82c210" dependencies = [ "pest", - "sha2 0.10.9", ] [[package]] @@ -3043,7 +3170,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -3066,7 +3193,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3101,9 +3228,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "portable-atomic" @@ -3113,18 +3240,18 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "serde_core", "writeable", @@ -3143,15 +3270,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "precomputed-hash" version = "0.1.1" @@ -3165,7 +3283,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3194,7 +3312,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "version_check", "yansi", ] @@ -3216,27 +3334,46 @@ checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] -name = "quick-xml" -version = "0.40.1" +name = "pulldown-cmark" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2474bd2e5029e7ccb6abb2ba48cf2383a333851dedf495901544281590c7da7f" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" dependencies = [ + "bitflags 2.13.0", + "getopts", "memchr", - "serde", + "pulldown-cmark-escape", + "unicase", ] [[package]] -name = "quinn" -version = "0.11.9" +name = "pulldown-cmark-escape" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "quick-xml" +version = "0.40.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2474bd2e5029e7ccb6abb2ba48cf2383a333851dedf495901544281590c7da7f" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", @@ -3251,15 +3388,16 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" dependencies = [ "aws-lc-rs", "bytes", - "getrandom 0.3.4", + "getrandom 0.4.3", "lru-slab", - "rand 0.9.2", + "rand 0.10.2", + "rand_pcg", "ring", "rustc-hash", "rustls", @@ -3273,23 +3411,23 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -3308,51 +3446,31 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rancor" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" +checksum = "daff8b7b3ccf5f7ba270b3e7a0a4d4c701c5797e38dec27c7e2c3dbb830fed1c" dependencies = [ "ptr_meta", ] [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "rand_core 0.6.4", ] [[package]] name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha", - "rand_core 0.9.5", -] - -[[package]] -name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", - "getrandom 0.4.2", - "rand_core 0.10.0", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", + "getrandom 0.4.3", + "rand_core 0.10.1", ] [[package]] @@ -3363,29 +3481,30 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "rand_core" -version = "0.9.5" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] -name = "rand_core" -version = "0.10.0" +name = "rand_pcg" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] [[package]] name = "ratatui" -version = "0.30.1" +version = "0.30.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1695748e3a735b34968c887ceea5a380b43545903868ae8f5b666593100f6b68" +checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d" dependencies = [ "instability", "ratatui-core", "ratatui-crossterm", "ratatui-macros", + "ratatui-termina", "ratatui-termwiz", "ratatui-widgets", "serde", @@ -3393,16 +3512,15 @@ dependencies = [ [[package]] name = "ratatui-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3603f354bba8c595fa47860e60142d7372b7210c27044c6a7d0e1a4336b44" +checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" dependencies = [ "bitflags 2.13.0", "compact_str", "critical-section", - "hashbrown 0.17.0", - "indoc", - "itertools", + "hashbrown 0.17.1", + "itertools 0.14.0", "kasuari", "lru", "palette", @@ -3416,9 +3534,9 @@ dependencies = [ [[package]] name = "ratatui-crossterm" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2867bedcbd6a690ca4f8672a687b730ec07660c79844517b084311b529980c" +checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0" dependencies = [ "cfg-if", "crossterm", @@ -3428,19 +3546,30 @@ dependencies = [ [[package]] name = "ratatui-macros" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80fac59720679490d89d200df411faa249be728681adcabed3d047ae72c48f1d" +checksum = "ed7dc68daa7498a43e4d68e0eb078427e10c38fbcfbb1e42d955f1fa2140d814" dependencies = [ "ratatui-core", "ratatui-widgets", ] +[[package]] +name = "ratatui-termina" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0bf912d9e66f057a759d92e386a280ea886b352ab757d6ac4d653c7ed2c43c2" +dependencies = [ + "instability", + "ratatui-core", + "termina", +] + [[package]] name = "ratatui-termwiz" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "386b8ff8f74ed749509391c56d549761a2fcdb408e1f42e467286bcb7dac8967" +checksum = "faf03e0380b7744054d6cb74224fe3adf062a029754933f575ca1e3b4c2ce977" dependencies = [ "ratatui-core", "termwiz", @@ -3448,9 +3577,9 @@ dependencies = [ [[package]] name = "ratatui-textarea" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2950274c0e155944158cf766848dd87bd6db6dad27da1c23ee4a7c8de71dbf1f" +checksum = "3c78d5ba0f26f97baed69a4c479f268a31c7b5b89d68ab939842152e383d6e73" dependencies = [ "ratatui-core", "ratatui-crossterm", @@ -3462,15 +3591,15 @@ dependencies = [ [[package]] name = "ratatui-widgets" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef4f17dd7ac3abf5adc2b920a03c61eee4bfe6a88fa5191936895525371d79c" +checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" dependencies = [ "bitflags 2.13.0", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "indoc", "instability", - "itertools", + "itertools 0.14.0", "line-clipping", "ratatui-core", "serde", @@ -3506,7 +3635,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3546,9 +3675,9 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "rend" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" +checksum = "663ba70707f96e871406fe10d68128412e619b06d1d47cb91c3a4c6501176240" dependencies = [ "bytecheck", ] @@ -3599,6 +3728,50 @@ dependencies = [ "web-sys", ] +[[package]] +name = "reqwest-middleware" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc3f1384cffa4f274dad2d4ddd73aed32fed8f786d96c6be8aa4e5fd3c3b58" +dependencies = [ + "anyhow", + "async-trait", + "http", + "reqwest", + "serde", + "thiserror 2.0.18", + "tower-service", +] + +[[package]] +name = "reqwest-retry" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe2412db2af7d2268e7a5406be0431f37d9eb67ff390f35b395716f5f06c2eaa" +dependencies = [ + "anyhow", + "async-trait", + "futures", + "getrandom 0.2.17", + "http", + "hyper", + "reqwest", + "reqwest-middleware", + "retry-policies", + "thiserror 2.0.18", + "tokio", + "wasmtimer", +] + +[[package]] +name = "retry-policies" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc05fbf560421a0357a750cbe78c7ca19d4923918490daabba313d5dbc871e47" +dependencies = [ + "rand 0.10.2", +] + [[package]] name = "ring" version = "0.17.14" @@ -3615,13 +3788,13 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.8.15" +version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a30e631b7f4a03dee9056b8ef6982e8ba371dd5bedb74d3ec86df4499132c70" +checksum = "815cc8a37159a463064825246cadb07961e25cd9885908606f6d08a98d8f8874" dependencies = [ "bytecheck", "bytes", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "indexmap 2.14.0", "munge", "ptr_meta", @@ -3634,13 +3807,13 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.8.15" +version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8100bb34c0a1d0f907143db3149e6b4eea3c33b9ee8b189720168e818303986f" +checksum = "c0ed1a78a1b19d184b0daa629dd9a024573173ec7d485b287cb369fb3607cc1c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3668,7 +3841,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.117", + "syn 2.0.118", "unicode-ident", ] @@ -3680,9 +3853,9 @@ checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -3708,9 +3881,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "aws-lc-rs", "once_cell", @@ -3722,9 +3895,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -3734,9 +3907,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ "web-time", "zeroize", @@ -3744,9 +3917,9 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", @@ -3771,9 +3944,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -3783,9 +3956,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" @@ -3875,9 +4048,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -3906,7 +4079,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3925,9 +4098,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -3961,7 +4134,7 @@ dependencies = [ "darling 0.23.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3976,15 +4149,15 @@ dependencies = [ [[package]] name = "sevenz-rust2" -version = "0.21.0" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbbd24232798280d6bc896e3429a3469174de008ec8b1b591a96618b46664195" +checksum = "0b5d71c87d24cb22976f3f6e6c2c9296ef1f6958394e2b868d56ca01b082798a" dependencies = [ "aes 0.9.1", "bzip2", "cbc 0.2.1", "crc32fast", - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "lzma-rust2", "ppmd-rust", @@ -4011,7 +4184,7 @@ checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "digest 0.11.2", + "digest 0.11.3", ] [[package]] @@ -4025,9 +4198,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook" @@ -4062,9 +4235,19 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] [[package]] name = "simdutf8" @@ -4080,9 +4263,9 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -4092,15 +4275,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -4187,7 +4370,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4199,7 +4382,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4227,9 +4410,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -4253,7 +4436,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4284,7 +4467,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", @@ -4292,12 +4475,24 @@ dependencies = [ [[package]] name = "tendril" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" dependencies = [ "new_debug_unreachable", - "utf-8", +] + +[[package]] +name = "termina" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e" +dependencies = [ + "bitflags 2.13.0", + "parking_lot", + "rustix", + "signal-hook", + "windows-sys 0.61.2", ] [[package]] @@ -4389,7 +4584,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4400,7 +4595,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4414,12 +4609,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ "deranged", - "itoa", "libc", "num-conv", "num_threads", @@ -4431,15 +4625,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ "num-conv", "time-core", @@ -4447,9 +4641,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "serde_core", @@ -4495,7 +4689,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4533,63 +4727,54 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.12+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "indexmap 2.14.0", "serde_core", "serde_spanned", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime", "toml_parser", "toml_writer", - "winnow 0.7.15", -] - -[[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", + "winnow", ] [[package]] name = "toml_datetime" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.25.8+spec-1.1.0" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ "indexmap 2.14.0", - "toml_datetime 1.1.0+spec-1.1.0", + "toml_datetime", "toml_parser", - "winnow 1.0.0", + "winnow", ] [[package]] name = "toml_parser" -version = "1.1.0+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.0", + "winnow", ] [[package]] name = "toml_writer" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tower" @@ -4608,20 +4793,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags 2.13.0", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -4655,7 +4840,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4711,9 +4896,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.26.9" +version = "0.26.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dab76d0b724ba557954125188cf0633a1ca43199ced82d95c7b9c32cc3de1f3" +checksum = "3c343ed63e3f5c64d1acdecb5d2c13d4e169cb5fde0052106ebaa6c6f27f9e55" dependencies = [ "cc", "regex", @@ -4725,9 +4910,9 @@ dependencies = [ [[package]] name = "tree-sitter-highlight" -version = "0.26.9" +version = "0.26.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fbfa3f35520ff730da507e2c9a526a98156f417e0008c73de873741d7e4ecc" +checksum = "51d5c68e5dba9c2818166330961143812b22a286e51a107a0733766b50dfc87d" dependencies = [ "regex", "streaming-iterator", @@ -4765,9 +4950,9 @@ checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ucd-trie" @@ -4775,6 +4960,12 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -4783,9 +4974,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.13.1" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da36089a805484bcccfffe0739803392c8298778a2d2f09febf76fac5ad9025b" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-truncate" @@ -4793,7 +4984,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" dependencies = [ - "itertools", + "itertools 0.14.0", "unicode-segmentation", "unicode-width", ] @@ -4835,12 +5026,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -4855,12 +5040,12 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.3" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ "atomic", - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "serde_core", "wasm-bindgen", @@ -4961,27 +5146,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -4992,23 +5168,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5016,48 +5188,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap 2.14.0", - "wasm-encoder", - "wasmparser", -] - [[package]] name = "wasm-streams" version = "0.5.0" @@ -5072,22 +5222,24 @@ dependencies = [ ] [[package]] -name = "wasmparser" -version = "0.244.0" +name = "wasmtimer" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" dependencies = [ - "bitflags 2.13.0", - "hashbrown 0.15.5", - "indexmap 2.14.0", - "semver", + "futures", + "js-sys", + "parking_lot", + "pin-utils", + "slab", + "wasm-bindgen", ] [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -5105,9 +5257,9 @@ dependencies = [ [[package]] name = "web_atoms" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" dependencies = [ "phf 0.13.1", "phf_codegen 0.13.1", @@ -5117,9 +5269,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" dependencies = [ "rustls-pki-types", ] @@ -5248,7 +5400,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5259,7 +5411,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5309,22 +5461,13 @@ dependencies = [ "windows-link", ] -[[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", + "windows-targets", ] [[package]] @@ -5333,16 +5476,7 @@ 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", + "windows-targets", ] [[package]] @@ -5354,90 +5488,34 @@ dependencies = [ "windows-link", ] -[[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", - "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", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[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_exe_info" version = "0.5.2" @@ -5448,111 +5526,46 @@ dependencies = [ "embed-resource", ] -[[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 = "winget-types" version = "0.4.3" +source = "git+https://github.com/UnownPlain/winget-types.git?branch=anthelion#9009288f5ef6df9c939ad8bdef62b9c939572ce4" dependencies = [ "base16ct", "bitflags 2.13.0", @@ -5562,7 +5575,7 @@ dependencies = [ "compact_str", "heapless", "icu_locale", - "itertools", + "itertools 0.15.0", "jiff", "package-family-name", "percent-encoding", @@ -5576,15 +5589,9 @@ dependencies = [ [[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.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] @@ -5601,97 +5608,15 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap 2.14.0", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.13.0", - "indexmap 2.14.0", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap 2.14.0", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "yaml_serde" @@ -5714,9 +5639,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -5725,93 +5650,94 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.52" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.52" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", "zerofrom", + "zerovec", ] [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "serde", "yoke", @@ -5821,13 +5747,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5846,9 +5772,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" +checksum = "5431d5661c32445236631278f27946e444ddafe4684cac70b185272d4f9c52d5" [[package]] name = "zmij" diff --git a/Cargo.toml b/Cargo.toml index 958a8153f..47ef37780 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ debug = true codegen-units = 1 lto = true strip = true +opt-level = 3 # Anthelion [dependencies] anstream = "1.0.0" @@ -79,6 +80,8 @@ ratatui = "0.30.1" ratatui-textarea = { version = "0.9.1", features = ["search"] } regex = "1.12.4" reqwest = { version = "0.13.4", default-features = false, features = ["charset", "http2", "stream", "system-proxy"] } +reqwest-middleware = { version = "0.5.2", default-features = false, features = ["json"] } +reqwest-retry = { version = "0.9.1", default-features = false } secrecy = "0.10.3" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.150" @@ -94,7 +97,7 @@ supports-hyperlinks = "3.2.0" tempfile = "3.27.0" thiserror = "2.0.18" tokio = { version = "1.52.3", features = ["rt-multi-thread", "macros", "fs", "parking_lot"] } -tracing = "0.1.44" +tracing = { version = "0.1.44", features = ["release_max_level_trace"] } tracing-indicatif = "0.3.14" tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } tree-sitter-highlight = "0.26.9" @@ -106,6 +109,11 @@ walkdir = "2.5.0" winget-types = { version = "0.4.3", features = ["serde", "std", "chrono"] } zerocopy = { version = "0.8.52", features = ["derive", "std"] } zip = { version = "8.6.0", default-features = false, features = ["deflate"] } +# Anthelion Start +napi = { version = "3.9.4", features = ["async", "tokio_rt"] } +napi-derive = "3.5.7" +pulldown-cmark = "0.13.4" +# Anthelion End [target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))'.dependencies] dbus-secret-service-keyring-store = { version = "1.0.0", features = ["vendored"] } @@ -124,6 +132,16 @@ rustls = ["dbus-secret-service-keyring-store/crypto-rust", "reqwest/rustls"] [build-dependencies] cynic-codegen = { version = "3.13.2", features = ["rkyv"] } windows_exe_info = { version = "0.5.2", features = ["manifest"] } +# Anthelion Start +napi-build = "2.3.2" + +[lib] +name = "anthelion_komac" +crate-type = ["cdylib"] + +[patch.crates-io] +winget-types = { git = "https://github.com/UnownPlain/winget-types.git", branch = "anthelion" } +# Anthelion End [dev-dependencies] indoc = "2.0.7" @@ -133,6 +151,3 @@ rstest = "0.26.1" assets = [ { source = "target/release/komac", dest = "/usr/bin/komac", mode = "755" }, ] - -[patch.crates-io] -winget-types = { path = "vendor/winget-types" } diff --git a/build.rs b/build.rs index 09d79e263..8587fc569 100644 --- a/build.rs +++ b/build.rs @@ -9,4 +9,7 @@ fn main() { .unwrap(); windows_exe_info::icon::icon_ico("assets/branding/logo.ico"); windows_exe_info::versioninfo::link_cargo_env(); + // Anthelion Start + napi_build::setup(); + // Anthelion End } diff --git a/bun.lock b/bun.lock new file mode 100644 index 000000000..b8e179611 --- /dev/null +++ b/bun.lock @@ -0,0 +1,229 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "anthelion-komac", + "devDependencies": { + "@napi-rs/cli": "3.7.2", + }, + }, + }, + "packages": { + "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@inquirer/ansi": ["@inquirer/ansi@2.0.7", "", {}, "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q=="], + + "@inquirer/checkbox": ["@inquirer/checkbox@5.2.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/core": "^11.2.1", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw=="], + + "@inquirer/confirm": ["@inquirer/confirm@6.1.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ=="], + + "@inquirer/core": ["@inquirer/core@11.2.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA=="], + + "@inquirer/editor": ["@inquirer/editor@5.2.2", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/external-editor": "^3.0.3", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg=="], + + "@inquirer/expand": ["@inquirer/expand@5.1.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g=="], + + "@inquirer/external-editor": ["@inquirer/external-editor@3.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA=="], + + "@inquirer/figures": ["@inquirer/figures@2.0.7", "", {}, "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw=="], + + "@inquirer/input": ["@inquirer/input@5.1.2", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg=="], + + "@inquirer/number": ["@inquirer/number@4.1.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA=="], + + "@inquirer/password": ["@inquirer/password@5.1.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg=="], + + "@inquirer/prompts": ["@inquirer/prompts@8.5.2", "", { "dependencies": { "@inquirer/checkbox": "^5.2.1", "@inquirer/confirm": "^6.1.1", "@inquirer/editor": "^5.2.2", "@inquirer/expand": "^5.1.1", "@inquirer/input": "^5.1.2", "@inquirer/number": "^4.1.1", "@inquirer/password": "^5.1.1", "@inquirer/rawlist": "^5.3.1", "@inquirer/search": "^4.2.1", "@inquirer/select": "^5.2.1" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g=="], + + "@inquirer/rawlist": ["@inquirer/rawlist@5.3.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og=="], + + "@inquirer/search": ["@inquirer/search@4.2.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g=="], + + "@inquirer/select": ["@inquirer/select@5.2.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/core": "^11.2.1", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw=="], + + "@inquirer/type": ["@inquirer/type@4.0.7", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g=="], + + "@napi-rs/cli": ["@napi-rs/cli@3.7.2", "", { "dependencies": { "@inquirer/prompts": "^8.5.2", "@napi-rs/cross-toolchain": "^1.0.3", "@napi-rs/wasm-tools": "^1.0.1", "@octokit/rest": "^22.0.1", "clipanion": "^4.0.0-rc.4", "colorette": "^2.0.20", "emnapi": "^1.11.1", "es-toolkit": "^1.47.0", "js-yaml": "^4.2.0", "obug": "^2.1.2", "semver": "^7.8.2", "typanion": "^3.14.0" }, "peerDependencies": { "@emnapi/runtime": "^1.7.1" }, "optionalPeers": ["@emnapi/runtime"], "bin": { "napi": "dist/cli.js", "napi-raw": "cli.mjs" } }, "sha512-shDW0Td/XZQpP04Yy+OsMt1ILMKGGkoLcy1zVAsSAK0fLfWm0Upgkmfs/NOV2ZhMQwkgpR3ZEdyHmTwgrUDQuA=="], + + "@napi-rs/cross-toolchain": ["@napi-rs/cross-toolchain@1.0.3", "", { "dependencies": { "@napi-rs/lzma": "^1.4.5", "@napi-rs/tar": "^1.1.0", "debug": "^4.4.1" }, "peerDependencies": { "@napi-rs/cross-toolchain-arm64-target-aarch64": "^1.0.3", "@napi-rs/cross-toolchain-arm64-target-armv7": "^1.0.3", "@napi-rs/cross-toolchain-arm64-target-ppc64le": "^1.0.3", "@napi-rs/cross-toolchain-arm64-target-s390x": "^1.0.3", "@napi-rs/cross-toolchain-arm64-target-x86_64": "^1.0.3", "@napi-rs/cross-toolchain-x64-target-aarch64": "^1.0.3", "@napi-rs/cross-toolchain-x64-target-armv7": "^1.0.3", "@napi-rs/cross-toolchain-x64-target-ppc64le": "^1.0.3", "@napi-rs/cross-toolchain-x64-target-s390x": "^1.0.3", "@napi-rs/cross-toolchain-x64-target-x86_64": "^1.0.3" }, "optionalPeers": ["@napi-rs/cross-toolchain-arm64-target-aarch64", "@napi-rs/cross-toolchain-arm64-target-armv7", "@napi-rs/cross-toolchain-arm64-target-ppc64le", "@napi-rs/cross-toolchain-arm64-target-s390x", "@napi-rs/cross-toolchain-arm64-target-x86_64", "@napi-rs/cross-toolchain-x64-target-aarch64", "@napi-rs/cross-toolchain-x64-target-armv7", "@napi-rs/cross-toolchain-x64-target-ppc64le", "@napi-rs/cross-toolchain-x64-target-s390x", "@napi-rs/cross-toolchain-x64-target-x86_64"] }, "sha512-ENPfLe4937bsKVTDA6zdABx4pq9w0tHqRrJHyaGxgaPq03a2Bd1unD5XSKjXJjebsABJ+MjAv1A2OvCgK9yehg=="], + + "@napi-rs/lzma": ["@napi-rs/lzma@1.4.5", "", { "optionalDependencies": { "@napi-rs/lzma-android-arm-eabi": "1.4.5", "@napi-rs/lzma-android-arm64": "1.4.5", "@napi-rs/lzma-darwin-arm64": "1.4.5", "@napi-rs/lzma-darwin-x64": "1.4.5", "@napi-rs/lzma-freebsd-x64": "1.4.5", "@napi-rs/lzma-linux-arm-gnueabihf": "1.4.5", "@napi-rs/lzma-linux-arm64-gnu": "1.4.5", "@napi-rs/lzma-linux-arm64-musl": "1.4.5", "@napi-rs/lzma-linux-ppc64-gnu": "1.4.5", "@napi-rs/lzma-linux-riscv64-gnu": "1.4.5", "@napi-rs/lzma-linux-s390x-gnu": "1.4.5", "@napi-rs/lzma-linux-x64-gnu": "1.4.5", "@napi-rs/lzma-linux-x64-musl": "1.4.5", "@napi-rs/lzma-wasm32-wasi": "1.4.5", "@napi-rs/lzma-win32-arm64-msvc": "1.4.5", "@napi-rs/lzma-win32-ia32-msvc": "1.4.5", "@napi-rs/lzma-win32-x64-msvc": "1.4.5" } }, "sha512-zS5LuN1OBPAyZpda2ZZgYOEDC+xecUdAGnrvbYzjnLXkrq/OBC3B9qcRvlxbDR3k5H/gVfvef1/jyUqPknqjbg=="], + + "@napi-rs/lzma-android-arm-eabi": ["@napi-rs/lzma-android-arm-eabi@1.4.5", "", { "os": "android", "cpu": "arm" }, "sha512-Up4gpyw2SacmyKWWEib06GhiDdF+H+CCU0LAV8pnM4aJIDqKKd5LHSlBht83Jut6frkB0vwEPmAkv4NjQ5u//Q=="], + + "@napi-rs/lzma-android-arm64": ["@napi-rs/lzma-android-arm64@1.4.5", "", { "os": "android", "cpu": "arm64" }, "sha512-uwa8sLlWEzkAM0MWyoZJg0JTD3BkPknvejAFG2acUA1raXM8jLrqujWCdOStisXhqQjZ2nDMp3FV6cs//zjfuQ=="], + + "@napi-rs/lzma-darwin-arm64": ["@napi-rs/lzma-darwin-arm64@1.4.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0Y0TQLQ2xAjVabrMDem1NhIssOZzF/y/dqetc6OT8mD3xMTDtF8u5BqZoX3MyPc9FzpsZw4ksol+w7DsxHrpMA=="], + + "@napi-rs/lzma-darwin-x64": ["@napi-rs/lzma-darwin-x64@1.4.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-vR2IUyJY3En+V1wJkwmbGWcYiT8pHloTAWdW4pG24+51GIq+intst6Uf6D/r46citObGZrlX0QvMarOkQeHWpw=="], + + "@napi-rs/lzma-freebsd-x64": ["@napi-rs/lzma-freebsd-x64@1.4.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-XpnYQC5SVovO35tF0xGkbHYjsS6kqyNCjuaLQ2dbEblFRr5cAZVvsJ/9h7zj/5FluJPJRDojVNxGyRhTp4z2lw=="], + + "@napi-rs/lzma-linux-arm-gnueabihf": ["@napi-rs/lzma-linux-arm-gnueabihf@1.4.5", "", { "os": "linux", "cpu": "arm" }, "sha512-ic1ZZMoRfRMwtSwxkyw4zIlbDZGC6davC9r+2oX6x9QiF247BRqqT94qGeL5ZP4Vtz0Hyy7TEViWhx5j6Bpzvw=="], + + "@napi-rs/lzma-linux-arm64-gnu": ["@napi-rs/lzma-linux-arm64-gnu@1.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-asEp7FPd7C1Yi6DQb45a3KPHKOFBSfGuJWXcAd4/bL2Fjetb2n/KK2z14yfW8YC/Fv6x3rBM0VAZKmJuz4tysg=="], + + "@napi-rs/lzma-linux-arm64-musl": ["@napi-rs/lzma-linux-arm64-musl@1.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-yWjcPDgJ2nIL3KNvi4536dlT/CcCWO0DUyEOlBs/SacG7BeD6IjGh6yYzd3/X1Y3JItCbZoDoLUH8iB1lTXo3w=="], + + "@napi-rs/lzma-linux-ppc64-gnu": ["@napi-rs/lzma-linux-ppc64-gnu@1.4.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-0XRhKuIU/9ZjT4WDIG/qnX7Xz7mSQHYZo9Gb3MP2gcvBgr6BA4zywQ9k3gmQaPn9ECE+CZg2V7DV7kT+x2pUMQ=="], + + "@napi-rs/lzma-linux-riscv64-gnu": ["@napi-rs/lzma-linux-riscv64-gnu@1.4.5", "", { "os": "linux", "cpu": "none" }, "sha512-QrqDIPEUUB23GCpyQj/QFyMlr8SGxxyExeZz9OWFnHfb70kXdTLWrHS/hEI1Ru+lSbQ/6xRqeoGyQ4Aqdg+/RA=="], + + "@napi-rs/lzma-linux-s390x-gnu": ["@napi-rs/lzma-linux-s390x-gnu@1.4.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-k8RVM5aMhW86E9H0QXdquwojew4H3SwPxbRVbl49/COJQWCUjGi79X6mYruMnMPEznZinUiT1jgKbFo2A00NdA=="], + + "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-6rMtBgnIq2Wcl1rQdZsnM+rtCcVCbws1nF8S2NzaUsVaZv8bjrPiAa0lwg4Eqnn1d9lgwqT+cZgm5m+//K08Kw=="], + + "@napi-rs/lzma-linux-x64-musl": ["@napi-rs/lzma-linux-x64-musl@1.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-eiadGBKi7Vd0bCArBUOO/qqRYPHt/VQVvGyYvDFt6C2ZSIjlD+HuOl+2oS1sjf4CFjK4eDIog6EdXnL0NE6iyQ=="], + + "@napi-rs/lzma-wasm32-wasi": ["@napi-rs/lzma-wasm32-wasi@1.4.5", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.3" }, "cpu": "none" }, "sha512-+VyHHlr68dvey6fXc2hehw9gHVFIW3TtGF1XkcbAu65qVXsA9D/T+uuoRVqhE+JCyFHFrO0ixRbZDRK1XJt1sA=="], + + "@napi-rs/lzma-win32-arm64-msvc": ["@napi-rs/lzma-win32-arm64-msvc@1.4.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-eewnqvIyyhHi3KaZtBOJXohLvwwN27gfS2G/YDWdfHlbz1jrmfeHAmzMsP5qv8vGB+T80TMHNkro4kYjeh6Deg=="], + + "@napi-rs/lzma-win32-ia32-msvc": ["@napi-rs/lzma-win32-ia32-msvc@1.4.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-OeacFVRCJOKNU/a0ephUfYZ2Yt+NvaHze/4TgOwJ0J0P4P7X1mHzN+ig9Iyd74aQDXYqc7kaCXA2dpAOcH87Cg=="], + + "@napi-rs/lzma-win32-x64-msvc": ["@napi-rs/lzma-win32-x64-msvc@1.4.5", "", { "os": "win32", "cpu": "x64" }, "sha512-T4I1SamdSmtyZgDXGAGP+y5LEK5vxHUFwe8mz6D4R7Sa5/WCxTcCIgPJ9BD7RkpO17lzhlaM2vmVvMy96Lvk9Q=="], + + "@napi-rs/tar": ["@napi-rs/tar@1.1.0", "", { "optionalDependencies": { "@napi-rs/tar-android-arm-eabi": "1.1.0", "@napi-rs/tar-android-arm64": "1.1.0", "@napi-rs/tar-darwin-arm64": "1.1.0", "@napi-rs/tar-darwin-x64": "1.1.0", "@napi-rs/tar-freebsd-x64": "1.1.0", "@napi-rs/tar-linux-arm-gnueabihf": "1.1.0", "@napi-rs/tar-linux-arm64-gnu": "1.1.0", "@napi-rs/tar-linux-arm64-musl": "1.1.0", "@napi-rs/tar-linux-ppc64-gnu": "1.1.0", "@napi-rs/tar-linux-s390x-gnu": "1.1.0", "@napi-rs/tar-linux-x64-gnu": "1.1.0", "@napi-rs/tar-linux-x64-musl": "1.1.0", "@napi-rs/tar-wasm32-wasi": "1.1.0", "@napi-rs/tar-win32-arm64-msvc": "1.1.0", "@napi-rs/tar-win32-ia32-msvc": "1.1.0", "@napi-rs/tar-win32-x64-msvc": "1.1.0" } }, "sha512-7cmzIu+Vbupriudo7UudoMRH2OA3cTw67vva8MxeoAe5S7vPFI7z0vp0pMXiA25S8IUJefImQ90FeJjl8fjEaQ=="], + + "@napi-rs/tar-android-arm-eabi": ["@napi-rs/tar-android-arm-eabi@1.1.0", "", { "os": "android", "cpu": "arm" }, "sha512-h2Ryndraj/YiKgMV/r5by1cDusluYIRT0CaE0/PekQ4u+Wpy2iUVqvzVU98ZPnhXaNeYxEvVJHNGafpOfaD0TA=="], + + "@napi-rs/tar-android-arm64": ["@napi-rs/tar-android-arm64@1.1.0", "", { "os": "android", "cpu": "arm64" }, "sha512-DJFyQHr1ZxNZorm/gzc1qBNLF/FcKzcH0V0Vwan5P+o0aE2keQIGEjJ09FudkF9v6uOuJjHCVDdK6S6uHtShAw=="], + + "@napi-rs/tar-darwin-arm64": ["@napi-rs/tar-darwin-arm64@1.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Zz2sXRzjIX4e532zD6xm2SjXEym6MkvfCvL2RMpG2+UwNVDVscHNcz3d47Pf3sysP2e2af7fBB3TIoK2f6trPw=="], + + "@napi-rs/tar-darwin-x64": ["@napi-rs/tar-darwin-x64@1.1.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-EI+CptIMNweT0ms9S3mkP/q+J6FNZ1Q6pvpJOEcWglRfyfQpLqjlC0O+dptruTPE8VamKYuqdjxfqD8hifZDOA=="], + + "@napi-rs/tar-freebsd-x64": ["@napi-rs/tar-freebsd-x64@1.1.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J0PIqX+pl6lBIAckL/c87gpodLbjZB1OtIK+RDscKC9NLdpVv6VGOxzUV/fYev/hctcE8EfkLbgFOfpmVQPg2g=="], + + "@napi-rs/tar-linux-arm-gnueabihf": ["@napi-rs/tar-linux-arm-gnueabihf@1.1.0", "", { "os": "linux", "cpu": "arm" }, "sha512-SLgIQo3f3EjkZ82ZwvrEgFvMdDAhsxCYjyoSuWfHCz0U16qx3SuGCp8+FYOPYCECHN3ZlGjXnoAIt9ERd0dEUg=="], + + "@napi-rs/tar-linux-arm64-gnu": ["@napi-rs/tar-linux-arm64-gnu@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-d014cdle52EGaH6GpYTQOP9Py7glMO1zz/+ynJPjjzYFSxvdYx0byrjumZk2UQdIyGZiJO2MEFpCkEEKFSgPYA=="], + + "@napi-rs/tar-linux-arm64-musl": ["@napi-rs/tar-linux-arm64-musl@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-L/y1/26q9L/uBqiW/JdOb/Dc94egFvNALUZV2WCGKQXc6UByPBMgdiEyW2dtoYxYYYYc+AKD+jr+wQPcvX2vrQ=="], + + "@napi-rs/tar-linux-ppc64-gnu": ["@napi-rs/tar-linux-ppc64-gnu@1.1.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EPE1K/80RQvPbLRJDJs1QmCIcH+7WRi0F73+oTe1582y9RtfGRuzAkzeBuAGRXAQEjRQw/RjtNqr6UTJ+8UuWQ=="], + + "@napi-rs/tar-linux-s390x-gnu": ["@napi-rs/tar-linux-s390x-gnu@1.1.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-B2jhWiB1ffw1nQBqLUP1h4+J1ovAxBOoe5N2IqDMOc63fsPZKNqF1PvO/dIem8z7LL4U4bsfmhy3gBfu547oNQ=="], + + "@napi-rs/tar-linux-x64-gnu": ["@napi-rs/tar-linux-x64-gnu@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-tbZDHnb9617lTnsDMGo/eAMZxnsQFnaRe+MszRqHguKfMwkisc9CCJnks/r1o84u5fECI+J/HOrKXgczq/3Oww=="], + + "@napi-rs/tar-linux-x64-musl": ["@napi-rs/tar-linux-x64-musl@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-dV6cODlzbO8u6Anmv2N/ilQHq/AWz0xyltuXoLU3yUyXbZcnWYZuB2rL8OBGPmqNcD+x9NdScBNXh7vWN0naSQ=="], + + "@napi-rs/tar-wasm32-wasi": ["@napi-rs/tar-wasm32-wasi@1.1.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.3" }, "cpu": "none" }, "sha512-jIa9nb2HzOrfH0F8QQ9g3WE4aMH5vSI5/1NYVNm9ysCmNjCCtMXCAhlI3WKCdm/DwHf0zLqdrrtDFXODcNaqMw=="], + + "@napi-rs/tar-win32-arm64-msvc": ["@napi-rs/tar-win32-arm64-msvc@1.1.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-vfpG71OB0ijtjemp3WTdmBKJm9R70KM8vsSExMsIQtV0lVzP07oM1CW6JbNRPXNLhRoue9ofYLiUDk8bE0Hckg=="], + + "@napi-rs/tar-win32-ia32-msvc": ["@napi-rs/tar-win32-ia32-msvc@1.1.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-hGPyPW60YSpOSgzfy68DLBHgi6HxkAM+L59ZZZPMQ0TOXjQg+p2EW87+TjZfJOkSpbYiEkULwa/f4a2hcVjsqQ=="], + + "@napi-rs/tar-win32-x64-msvc": ["@napi-rs/tar-win32-x64-msvc@1.1.0", "", { "os": "win32", "cpu": "x64" }, "sha512-L6Ed1DxXK9YSCMyvpR8MiNAyKNkQLjsHsHK9E0qnHa8NzLFqzDKhvs5LfnWxM2kJ+F7m/e5n9zPm24kHb3LsVw=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + + "@napi-rs/wasm-tools": ["@napi-rs/wasm-tools@1.0.1", "", { "optionalDependencies": { "@napi-rs/wasm-tools-android-arm-eabi": "1.0.1", "@napi-rs/wasm-tools-android-arm64": "1.0.1", "@napi-rs/wasm-tools-darwin-arm64": "1.0.1", "@napi-rs/wasm-tools-darwin-x64": "1.0.1", "@napi-rs/wasm-tools-freebsd-x64": "1.0.1", "@napi-rs/wasm-tools-linux-arm64-gnu": "1.0.1", "@napi-rs/wasm-tools-linux-arm64-musl": "1.0.1", "@napi-rs/wasm-tools-linux-x64-gnu": "1.0.1", "@napi-rs/wasm-tools-linux-x64-musl": "1.0.1", "@napi-rs/wasm-tools-wasm32-wasi": "1.0.1", "@napi-rs/wasm-tools-win32-arm64-msvc": "1.0.1", "@napi-rs/wasm-tools-win32-ia32-msvc": "1.0.1", "@napi-rs/wasm-tools-win32-x64-msvc": "1.0.1" } }, "sha512-enkZYyuCdo+9jneCPE/0fjIta4wWnvVN9hBo2HuiMpRF0q3lzv1J6b/cl7i0mxZUKhBrV3aCKDBQnCOhwKbPmQ=="], + + "@napi-rs/wasm-tools-android-arm-eabi": ["@napi-rs/wasm-tools-android-arm-eabi@1.0.1", "", { "os": "android", "cpu": "arm" }, "sha512-lr07E/l571Gft5v4aA1dI8koJEmF1F0UigBbsqg9OWNzg80H3lDPO+auv85y3T/NHE3GirDk7x/D3sLO57vayw=="], + + "@napi-rs/wasm-tools-android-arm64": ["@napi-rs/wasm-tools-android-arm64@1.0.1", "", { "os": "android", "cpu": "arm64" }, "sha512-WDR7S+aRLV6LtBJAg5fmjKkTZIdrEnnQxgdsb7Cf8pYiMWBHLU+LC49OUVppQ2YSPY0+GeYm9yuZWW3kLjJ7Bg=="], + + "@napi-rs/wasm-tools-darwin-arm64": ["@napi-rs/wasm-tools-darwin-arm64@1.0.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qWTI+EEkiN0oIn/N2gQo7+TVYil+AJ20jjuzD2vATS6uIjVz+Updeqmszi7zq7rdFTLp6Ea3/z4kDKIfZwmR9g=="], + + "@napi-rs/wasm-tools-darwin-x64": ["@napi-rs/wasm-tools-darwin-x64@1.0.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-bA6hubqtHROR5UI3tToAF/c6TDmaAgF0SWgo4rADHtQ4wdn0JeogvOk50gs2TYVhKPE2ZD2+qqt7oBKB+sxW3A=="], + + "@napi-rs/wasm-tools-freebsd-x64": ["@napi-rs/wasm-tools-freebsd-x64@1.0.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-90+KLBkD9hZEjPQW1MDfwSt5J1L46EUKacpCZWyRuL6iIEO5CgWU0V/JnEgFsDOGyyYtiTvHc5bUdUTWd4I9Vg=="], + + "@napi-rs/wasm-tools-linux-arm64-gnu": ["@napi-rs/wasm-tools-linux-arm64-gnu@1.0.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-rG0QlS65x9K/u3HrKafDf8cFKj5wV2JHGfl8abWgKew0GVPyp6vfsDweOwHbWAjcHtp2LHi6JHoW80/MTHm52Q=="], + + "@napi-rs/wasm-tools-linux-arm64-musl": ["@napi-rs/wasm-tools-linux-arm64-musl@1.0.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jAasbIvjZXCgX0TCuEFQr+4D6Lla/3AAVx2LmDuMjgG4xoIXzjKWl7c4chuaD+TI+prWT0X6LJcdzFT+ROKGHQ=="], + + "@napi-rs/wasm-tools-linux-x64-gnu": ["@napi-rs/wasm-tools-linux-x64-gnu@1.0.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Plgk5rPqqK2nocBGajkMVbGm010Z7dnUgq0wtnYRZbzWWxwWcXfZMPa8EYxrK4eE8SzpI7VlZP1tdVsdjgGwMw=="], + + "@napi-rs/wasm-tools-linux-x64-musl": ["@napi-rs/wasm-tools-linux-x64-musl@1.0.1", "", { "os": "linux", "cpu": "x64" }, "sha512-GW7AzGuWxtQkyHknHWYFdR0CHmW6is8rG2Rf4V6GNmMpmwtXt/ItWYWtBe4zqJWycMNazpfZKSw/BpT7/MVCXQ=="], + + "@napi-rs/wasm-tools-wasm32-wasi": ["@napi-rs/wasm-tools-wasm32-wasi@1.0.1", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.3" }, "cpu": "none" }, "sha512-/nQVSTrqSsn7YdAc2R7Ips/tnw5SPUcl3D7QrXCNGPqjbatIspnaexvaOYNyKMU6xPu+pc0BTnKVmqhlJJCPLA=="], + + "@napi-rs/wasm-tools-win32-arm64-msvc": ["@napi-rs/wasm-tools-win32-arm64-msvc@1.0.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-PFi7oJIBu5w7Qzh3dwFea3sHRO3pojMsaEnUIy22QvsW+UJfNQwJCryVrpoUt8m4QyZXI+saEq/0r4GwdoHYFQ=="], + + "@napi-rs/wasm-tools-win32-ia32-msvc": ["@napi-rs/wasm-tools-win32-ia32-msvc@1.0.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-gXkuYzxQsgkj05Zaq+KQTkHIN83dFAwMcTKa2aQcpYPRImFm2AQzEyLtpXmyCWzJ0F9ZYAOmbSyrNew8/us6bw=="], + + "@napi-rs/wasm-tools-win32-x64-msvc": ["@napi-rs/wasm-tools-win32-x64-msvc@1.0.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rEAf05nol3e3eei2sRButmgXP+6ATgm0/38MKhz9Isne82T4rPIMYsCIFj0kOisaGeVwoi2fnm7O9oWp5YVnYQ=="], + + "@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], + + "@octokit/core": ["@octokit/core@7.0.6", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q=="], + + "@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + + "@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="], + + "@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + + "@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@14.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw=="], + + "@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="], + + "@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@17.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw=="], + + "@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + + "@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + + "@octokit/rest": ["@octokit/rest@22.0.1", "", { "dependencies": { "@octokit/core": "^7.0.6", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^17.0.0" } }, "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw=="], + + "@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], + + "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], + + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + + "clipanion": ["clipanion@4.0.0-rc.4", "", { "dependencies": { "typanion": "^3.8.0" } }, "sha512-CXkMQxU6s9GklO/1f714dkKBMu1lopS1WFF0B8o4AxPykR1hpozxSiUZ5ZUeBjfPgCWqbcNOtZVFhB8Lkfp1+Q=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "emnapi": ["emnapi@1.11.1", "", { "peerDependencies": { "node-addon-api": ">= 6.1.0" }, "optionalPeers": ["node-addon-api"] }, "sha512-kSRjhIcxjMFsBqk7ORvoc9aA5SBKDmecrtF5RMcmOTao0kD/zamaxsuTxMI8C1//wGUuvE7a+19pCE7AEhGVnA=="], + + "es-toolkit": ["es-toolkit@1.47.1", "", {}, "sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q=="], + + "fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="], + + "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], + + "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], + + "fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], + + "obug": ["obug@2.1.3", "", {}, "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typanion": ["typanion@3.14.0", "", {}, "sha512-ZW/lVMRabETuYCd9O9ZvMhAh8GslSqaUjxmK/JLPCh6l73CvLBiuXswj/+7LdnWOgYsQ130FqLzFz5aGT4I3Ug=="], + + "universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="], + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..5345f4ad3 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "@unownplain/anthelion-komac", + "version": "0.0.48", + "description": "komac JS bindings for Anthelion", + "license": "GPL-3.0-or-later", + "repository": { + "type": "git", + "url": "git+https://github.com/unpn-org/Komac.git" + }, + "files": [ + "index.d.ts", + "index.js" + ], + "type": "module", + "main": "index.js", + "scripts": { + "artifacts": "napi artifacts", + "build": "napi build --platform --release --esm --strip", + "build:debug": "napi build --platform --esm", + "prepublishOnly": "napi prepublish -t npm --no-gh-release", + "preversion": "napi build --platform && git add .", + "version": "napi version" + }, + "devDependencies": { + "@napi-rs/cli": "3.7.2" + }, + "napi": { + "binaryName": "anthelion-komac", + "targets": [ + "x86_64-pc-windows-msvc", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + "x86_64-unknown-linux-musl", + "aarch64-apple-darwin", + "aarch64-unknown-linux-gnu", + "aarch64-unknown-linux-musl", + "aarch64-pc-windows-msvc" + ] + }, + "engines": { + "node": ">=22.0.0" + } +} diff --git a/src/analysis/analyzer.rs b/src/analysis/analyzer.rs index db08bdc40..6c2233d0e 100644 --- a/src/analysis/analyzer.rs +++ b/src/analysis/analyzer.rs @@ -4,18 +4,19 @@ use std::{ }; use camino::Utf8Path; -use color_eyre::eyre::{Result, bail}; +use color_eyre::eyre::Result; use winget_types::{ PackageVersion, installer::{Architecture, Installer}, locale::{Copyright, PackageName, Publisher}, + utils::ValidFileExtensions, }; -use super::extensions::{APPX, APPX_BUNDLE, EXE, MSI, MSIX, MSIX_BUNDLE, ZIP}; +use super::PeInfo; use crate::analysis::{ Installers, installers::{ - Exe, Msi, Zip, + Exe, Font, Msi, Zip, msix_family::{Msix, bundle::MsixBundle}, }, }; @@ -26,22 +27,28 @@ pub struct Analyzer<'reader, R: Read + Seek> { pub package_name: Option, pub package_version: Option, pub publisher: Option, + #[allow(dead_code)] + pub file_version: Option, + #[allow(dead_code)] + pub product_version: Option, + pub pe_info: Option, pub installers: Vec, pub zip: Option>, } impl<'reader, R: Read + Seek> Analyzer<'reader, R> { pub fn new(reader: &'reader mut R, file_name: &str) -> Result { - let extension = Utf8Path::new(file_name) - .extension() - .unwrap_or_default() - .to_ascii_lowercase(); + let extension = ValidFileExtensions::from_path(Utf8Path::new(file_name))?; - let installers = match extension.as_str() { - MSI => Msi::new(reader)?.installers(), - MSIX | APPX => Msix::new(reader)?.installers(), - MSIX_BUNDLE | APPX_BUNDLE => MsixBundle::new(reader)?.installers(), - ZIP => { + let installers = match extension { + ValidFileExtensions::Msi => Msi::new(reader)?.installers(), + ValidFileExtensions::Msix | ValidFileExtensions::Appx => { + Msix::new(reader)?.installers() + } + ValidFileExtensions::MsixBundle | ValidFileExtensions::AppxBundle => { + MsixBundle::new(reader)?.installers() + } + ValidFileExtensions::Zip => { let mut scoped_zip = Zip::new(reader)?; let installers = mem::take(&mut scoped_zip.installers); return Ok(Self { @@ -50,7 +57,7 @@ impl<'reader, R: Read + Seek> Analyzer<'reader, R> { ..Self::default() }); } - EXE => { + ValidFileExtensions::Exe => { let mut exe = Exe::new(reader)?; let file_name_lower = file_name.to_lowercase(); let installers = exe @@ -85,10 +92,17 @@ impl<'reader, R: Read + Seek> Analyzer<'reader, R> { .company_name .take() .and_then(|company_name| Publisher::new(company_name).ok()), + file_version: exe.file_version.take(), + product_version: exe.product_version.take(), + pe_info: exe.pe_info.take(), ..Self::default() }); } - _ => bail!(r#"Unsupported file extension: "{extension}""#), + ValidFileExtensions::Fnt + | ValidFileExtensions::Otc + | ValidFileExtensions::Otf + | ValidFileExtensions::Ttc + | ValidFileExtensions::Ttf => Font::new(reader, file_name)?.installers(), }; Ok(Self { installers, @@ -105,6 +119,9 @@ impl Default for Analyzer<'_, R> { package_name: None, package_version: None, publisher: None, + file_version: None, + product_version: None, + pe_info: None, installers: Vec::default(), zip: None, } diff --git a/src/analysis/extensions.rs b/src/analysis/extensions.rs deleted file mode 100644 index 1770c97c2..000000000 --- a/src/analysis/extensions.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub const EXE: &str = "exe"; -pub const MSI: &str = "msi"; -pub const MSIX: &str = "msix"; -pub const APPX: &str = "appx"; -pub const MSIX_BUNDLE: &str = "msixbundle"; -pub const APPX_BUNDLE: &str = "appxbundle"; -pub const ZIP: &str = "zip"; diff --git a/src/analysis/installers/advanced/mod.rs b/src/analysis/installers/advanced/mod.rs index 28aed50e1..d3bafcc5a 100644 --- a/src/analysis/installers/advanced/mod.rs +++ b/src/analysis/installers/advanced/mod.rs @@ -15,7 +15,7 @@ use sevenz_rust2::{ArchiveReader, Password}; use thiserror::Error; use tracing::{debug, warn}; use winget_types::installer::{ - AppsAndFeaturesEntry, ExpectedReturnCodes, Installer, InstallerReturnCode, InstallerSwitches, + AppsAndFeaturesEntry, ExpectedReturnCode, Installer, InstallerReturnCode, InstallerSwitches, InstallerType, ReturnResponse, }; use zerocopy::IntoBytes; @@ -110,7 +110,7 @@ impl Installers for AdvancedInstaller { .iter() .map(|msi| { let mut installer = msi.installers().into_iter().next().unwrap_or_default(); - installer.r#type = Some(InstallerType::Exe); + installer.r#type = Some(InstallerType::AdvancedInstaller); // https://www.advancedinstaller.com/user-guide/exe-setup-file.html#proprietary-command-line-switches-for-the-exe-setup installer.switches = InstallerSwitches::builder() @@ -158,7 +158,7 @@ impl Installers for AdvancedInstaller { } } -fn expected_return_codes() -> BTreeSet { +fn expected_return_codes() -> BTreeSet { use ReturnResponse::{ AlreadyInstalled, BlockedByPolicy, CancelledByUser, ContactSupport, InstallInProgress, InvalidParameter, RebootInitiated, RebootRequiredToFinish, SystemNotSupported, @@ -187,10 +187,8 @@ fn expected_return_codes() -> BTreeSet { (3010, RebootRequiredToFinish), ] .into_iter() - .map(|(code, response)| ExpectedReturnCodes { - installer_return_code: InstallerReturnCode::new(code), - return_response: response, - return_response_url: None, + .map(|(code, response)| { + ExpectedReturnCode::new(InstallerReturnCode::new(code).unwrap(), response) }) .collect() } diff --git a/src/analysis/installers/burn/mod.rs b/src/analysis/installers/burn/mod.rs index 9aed17bfd..15d69e4b2 100644 --- a/src/analysis/installers/burn/mod.rs +++ b/src/analysis/installers/burn/mod.rs @@ -151,28 +151,6 @@ impl Installers for Burn { let manifest = self.manifest.as_ref().unwrap_or_else(|| unreachable!()); - let mut apps_and_features_entries = AppsAndFeaturesEntries::from( - AppsAndFeaturesEntry::builder() - .display_name(manifest.registration.arp.display_name()) - .maybe_publisher(manifest.registration.arp.publisher()) - .display_version(manifest.registration.arp.display_version().clone()) - .product_code( - manifest - .registration - .code() - .encode_upper(&mut Uuid::encode_buffer()) - .to_string(), - ) - .maybe_upgrade_code(manifest.related_bundles.first().map(|bundle| { - bundle - .code() - .encode_upper(&mut Uuid::encode_buffer()) - .to_string() - })) - .installer_type(InstallerType::Burn) - .build(), - ); - let variables = manifest .variables .iter() @@ -191,7 +169,7 @@ impl Installers for Burn { ]) .collect::>(); - for msi_package in manifest + let msi_packages = manifest .chain .packages .iter() @@ -204,7 +182,31 @@ impl Installers for Burn { !msi_package.is_arp_system_component() }) .filter(|msi_package| msi_package.evaluate_install_condition(&variables)) - { + .collect::>(); + + let mut apps_and_features_entries = AppsAndFeaturesEntries::from( + AppsAndFeaturesEntry::builder() + .display_name(manifest.registration.arp.display_name()) + .maybe_publisher(manifest.registration.arp.publisher()) + .display_version(manifest.registration.arp.display_version().clone()) + .product_code( + manifest + .registration + .code() + .encode_upper(&mut Uuid::encode_buffer()) + .to_string(), + ) + .maybe_upgrade_code(manifest.related_bundles.first().map(|bundle| { + bundle + .code() + .encode_upper(&mut Uuid::encode_buffer()) + .to_string() + })) + .maybe_installer_type((!msi_packages.is_empty()).then_some(InstallerType::Burn)) + .build(), + ); + + for msi_package in msi_packages { apps_and_features_entries.push( AppsAndFeaturesEntry::builder() .maybe_display_name( @@ -234,11 +236,14 @@ impl Installers for Burn { vec![Installer { architecture: self.architecture, r#type: Some(InstallerType::Burn), + // User scope should not be set for MSI installers until the below issue is fixed + // https://github.com/microsoft/winget-cli/issues/3011 scope: match manifest.registration.scope() { WixBundleScope::Machine => Some(Scope::Machine), WixBundleScope::User => Some(Scope::User), WixBundleScope::MachineOrUser | WixBundleScope::UserOrMachine => None, - }, + } + .filter(|scope| !scope.is_user()), apps_and_features_entries, installation_metadata: manifest .variables diff --git a/src/analysis/installers/exe.rs b/src/analysis/installers/exe.rs index 84ebcc5e0..4bdef43c3 100644 --- a/src/analysis/installers/exe.rs +++ b/src/analysis/installers/exe.rs @@ -9,15 +9,18 @@ use super::{ super::Installers, AdvancedInstaller, Burn, InstallShield, Nsis, Qt, SevenZipSfx, Squirrel, }; use crate::{ - analysis::installers::{ - advanced::AdvancedInstallerError, - burn::BurnError, - installshield::InstallShieldError, - nsis::NsisError, - pe::{PE, VSVersionInfo}, - qt::QtError, - sevenzip_sfx::SevenZipSfxError, - squirrel::SquirrelError, + analysis::{ + PeInfo, + installers::{ + advanced::AdvancedInstallerError, + burn::BurnError, + installshield::InstallShieldError, + nsis::NsisError, + pe::{PE, VSVersionInfo}, + qt::QtError, + sevenzip_sfx::SevenZipSfxError, + squirrel::SquirrelError, + }, }, traits::IntoWingetArchitecture, }; @@ -31,6 +34,9 @@ pub struct Exe { pub legal_copyright: Option, pub product_name: Option, pub company_name: Option, + pub file_version: Option, + pub product_version: Option, + pub pe_info: Option, } pub enum ExeType { @@ -53,6 +59,7 @@ impl Exe { let vs_version_info = vs_version_info_bytes .as_deref() .and_then(|version_info_bytes| VSVersionInfo::read_from(version_info_bytes).ok()); + let pe_info = vs_version_info.as_ref().map(PeInfo::from_version_info); let mut string_table = vs_version_info.as_ref().map(VSVersionInfo::string_table); let legal_copyright = string_table .as_mut() @@ -66,6 +73,14 @@ impl Exe { .as_mut() .and_then(|table| table.swap_remove("CompanyName")) .map(str::to_owned); + let file_version = pe_info + .as_ref() + .and_then(PeInfo::file_version) + .map(str::to_owned); + let product_version = pe_info + .as_ref() + .and_then(PeInfo::product_version) + .map(str::to_owned); match AdvancedInstaller::new(&mut reader) { Ok(advanced) => { @@ -74,6 +89,9 @@ impl Exe { legal_copyright, product_name, company_name, + file_version, + product_version, + pe_info, }); } Err(AdvancedInstallerError::NotAdvancedInstallerFile) => {} @@ -87,6 +105,9 @@ impl Exe { legal_copyright, product_name, company_name, + file_version, + product_version, + pe_info, }); } Err(BurnError::NotBurnFile) => {} @@ -100,6 +121,9 @@ impl Exe { legal_copyright, product_name, company_name, + file_version, + product_version, + pe_info, }); } Err(InnoError::NotInnoFile) => {} @@ -113,6 +137,9 @@ impl Exe { legal_copyright, product_name, company_name, + file_version, + product_version, + pe_info, }); } Err(InstallShieldError::NotInstallShieldFile) => {} @@ -126,6 +153,9 @@ impl Exe { legal_copyright, product_name, company_name, + file_version, + product_version, + pe_info, }); } Err(NsisError::NotNsisFile) => {} @@ -139,6 +169,9 @@ impl Exe { legal_copyright, product_name, company_name, + file_version, + product_version, + pe_info, }); } Err(QtError::NotQtFile) => {} @@ -152,6 +185,9 @@ impl Exe { legal_copyright, product_name, company_name, + file_version, + product_version, + pe_info, }); } Err(SevenZipSfxError::NotSevenZipSfx) => {} @@ -166,6 +202,9 @@ impl Exe { legal_copyright, product_name, company_name, + file_version, + product_version, + pe_info, }); } Err(SquirrelError::NotSquirrelFile) => {} @@ -220,6 +259,9 @@ impl Exe { legal_copyright, product_name, company_name, + file_version, + product_version, + pe_info, }) } } diff --git a/src/analysis/installers/font.rs b/src/analysis/installers/font.rs new file mode 100644 index 000000000..e1337b0d1 --- /dev/null +++ b/src/analysis/installers/font.rs @@ -0,0 +1,63 @@ +use std::io::{self, Read, Seek, SeekFrom}; + +use camino::Utf8PathBuf; +use thiserror::Error; +use winget_types::installer::{Architecture, Installer, InstallerType}; + +use super::super::Installers; + +/// https://learn.microsoft.com/en-us/typography/opentype/spec/otff#organization-of-an-opentype-font +const TRUETYPE_SIGNATURE: [u8; 4] = [0x00, 0x01, 0x00, 0x00]; +const OPENTYPE_SIGNATURE: [u8; 4] = *b"OTTO"; +/// https://learn.microsoft.com/en-us/typography/opentype/spec/otff#ttc-header +const TRUETYPE_COLLECTION_SIGNATURE: [u8; 4] = *b"ttcf"; +/// First 2 bytes are the little-endian FNT version (0x0200 or 0x0300). +const WINDOWS_FNT_SIGNATURES: [[u8; 2]; 2] = [[0x00, 0x02], [0x00, 0x03]]; + +const FONT_SIGNATURES: [[u8; 4]; 3] = [ + TRUETYPE_SIGNATURE, + OPENTYPE_SIGNATURE, + TRUETYPE_COLLECTION_SIGNATURE, +]; + +#[derive(Error, Debug)] +pub enum FontError { + #[error("{path} is not a valid font file")] + NotFontFile { path: Utf8PathBuf }, + #[error(transparent)] + Io(#[from] io::Error), +} + +pub struct Font; + +impl Font { + pub fn new(mut reader: R, path: &str) -> Result { + reader.seek(SeekFrom::Start(0))?; + + let mut signature = [0u8; 4]; + reader.read_exact(&mut signature)?; + + if FONT_SIGNATURES.contains(&signature) { + return Ok(Self); + } + + let fnt_version = [signature[0], signature[1]]; + if WINDOWS_FNT_SIGNATURES.contains(&fnt_version) { + return Ok(Self); + } + + Err(FontError::NotFontFile { + path: Utf8PathBuf::from(path), + }) + } +} + +impl Installers for Font { + fn installers(&self) -> Vec { + vec![Installer { + r#type: Some(InstallerType::Font), + architecture: Architecture::Neutral, + ..Installer::default() + }] + } +} diff --git a/src/analysis/installers/inno/mod.rs b/src/analysis/installers/inno/mod.rs index 575d4f16d..404c4e552 100644 --- a/src/analysis/installers/inno/mod.rs +++ b/src/analysis/installers/inno/mod.rs @@ -30,22 +30,44 @@ impl Installers for Inno { fn installers(&self) -> Vec { let scope = self.header.privileges_required().to_scope(); - let install_dir = self - .header - .default_dir_name() - .map(str::to_owned) - .map(|install_dir| to_relative_install_dir(install_dir, scope)) - .filter(|dir| !dir.contains(['{', '}'])); + let install_dir = scope.and_then(|scope| { + self.header + .default_dir_name() + .map(str::to_owned) + .map(|install_dir| to_relative_install_dir(install_dir, scope)) + .filter(|dir| !dir.contains(['{', '}'])) + }); let product_code = self .header .product_code() .filter(|code| !code.starts_with(CODE)); + let app_versioned_name = self + .header + .app_versioned_name() + .filter(|code| !code.starts_with(CODE)) + .map(str::to_owned) + .or_else(|| { + if self.version().major() >= 7 { + Some(format!( + "{} {}", + self.header.app_name()?, + self.header.app_version()? + )) + } else { + Some(format!( + "{} version {}", + self.header.app_name()?, + self.header.app_version()? + )) + } + }); + let display_name = self .header .uninstall_name() - .or_else(|| self.header.app_versioned_name()) + .or(app_versioned_name.as_deref()) .filter(|name| !name.starts_with(CODE)); let publisher = self @@ -67,7 +89,7 @@ impl Installers for Inno { }), architecture: WingetArchitecture::from_inno(self.header.architectures_allowed()), r#type: Some(InstallerType::Inno), - scope: Some(scope), + scope, url: DecodedUrl::default(), sha_256: Sha256String::default(), unsupported_os_architectures: UnsupportedOSArchitecture::from_inno( @@ -124,6 +146,10 @@ impl Installers for Inno { { vec![installer] } else { + let Some(scope) = scope else { + return vec![installer]; + }; + let has_scope_switch = self .header .privileges_required_overrides_allowed() @@ -173,7 +199,7 @@ trait PrivilegeLevelExt { overrides: PrivilegesRequiredOverrides, ) -> Option; - fn to_scope(&self) -> Scope; + fn to_scope(&self) -> Option; } impl PrivilegeLevelExt for inno::header::PrivilegeLevel { @@ -182,16 +208,17 @@ impl PrivilegeLevelExt for inno::header::PrivilegeLevel { overrides: PrivilegesRequiredOverrides, ) -> Option { match self { - Self::Admin | Self::PowerUser => Some(ElevationRequirement::ElevatesSelf), + Self::None | Self::PowerUser | Self::Admin => Some(ElevationRequirement::ElevatesSelf), _ if !overrides.is_empty() => Some(ElevationRequirement::ElevatesSelf), _ => None, } } - fn to_scope(&self) -> Scope { + fn to_scope(&self) -> Option { match self { - Self::Lowest | Self::None => Scope::User, - Self::Admin | Self::PowerUser => Scope::Machine, + Self::Lowest => Some(Scope::User), + Self::PowerUser | Self::Admin => Some(Scope::Machine), + Self::None => None, } } } diff --git a/src/analysis/installers/installshield/return_codes.rs b/src/analysis/installers/installshield/return_codes.rs index 023af2de3..2fbc3e8a9 100644 --- a/src/analysis/installers/installshield/return_codes.rs +++ b/src/analysis/installers/installshield/return_codes.rs @@ -1,6 +1,6 @@ use std::collections::BTreeSet; -use winget_types::installer::{ExpectedReturnCodes, InstallerReturnCode, ReturnResponse}; +use winget_types::installer::{ExpectedReturnCode, InstallerReturnCode, ReturnResponse}; const COMMON_CODES: &[(i32, ReturnResponse)] = { use ReturnResponse::*; @@ -36,7 +36,7 @@ const MSI_CODES: &[(i32, ReturnResponse)] = { ] }; -pub fn expected_return_codes(msi_based: bool) -> BTreeSet { +pub fn expected_return_codes(msi_based: bool) -> BTreeSet { let codes: Box> = if msi_based { Box::new(COMMON_CODES.iter().chain(MSI_CODES)) } else { @@ -44,10 +44,8 @@ pub fn expected_return_codes(msi_based: bool) -> BTreeSet { }; codes - .map(|&(code, response)| ExpectedReturnCodes { - installer_return_code: InstallerReturnCode::new(code), - return_response: response, - return_response_url: None, + .filter_map(|&(code, response)| { + Some(ExpectedReturnCode::new(InstallerReturnCode::new(code)?, response)) }) .collect() } diff --git a/src/analysis/installers/mod.rs b/src/analysis/installers/mod.rs index 5d658104e..5a2e6ab4b 100644 --- a/src/analysis/installers/mod.rs +++ b/src/analysis/installers/mod.rs @@ -1,6 +1,7 @@ mod advanced; pub mod burn; mod exe; +pub mod font; pub mod inno; mod installshield; mod msi; @@ -16,6 +17,7 @@ mod zip; pub use advanced::AdvancedInstaller; pub use burn::Burn; pub use exe::Exe; +pub use font::Font; pub use installshield::InstallShield; pub use msi::Msi; pub use nsis::Nsis; diff --git a/src/analysis/installers/msi/mod.rs b/src/analysis/installers/msi/mod.rs index 7ce692cb7..7dcc4e895 100644 --- a/src/analysis/installers/msi/mod.rs +++ b/src/analysis/installers/msi/mod.rs @@ -11,6 +11,7 @@ use msi::{Language, Package, Select}; use property_table::PropertyTable; use winget_types::{ LanguageTag, + icu_locale::langid, installer::{ AppsAndFeaturesEntries, AppsAndFeaturesEntry, Architecture, InstallationMetadata, Installer, InstallerSwitches, InstallerType, Scope, @@ -221,6 +222,7 @@ impl Msi { .tag() .parse::() .ok() + .filter(|language_tag| language_tag != &LanguageTag::new(langid!("und"))) } fn wix_ui_install_dir(&self) -> Option<&str> { @@ -268,7 +270,9 @@ impl Installers for Msi { } else { InstallerType::Msi }), - scope: self.find_scope(), + // User scope should not be set for MSI installers until the below issue is fixed + // https://github.com/microsoft/winget-cli/issues/3011 + scope: self.find_scope().filter(|scope| !scope.is_user()), product_code: product_code.map(str::to_owned), apps_and_features_entries: if product_name.is_some() || manufacturer.is_some() diff --git a/src/analysis/installers/msix_family/bundle/mod.rs b/src/analysis/installers/msix_family/bundle/mod.rs index 0d37ee573..744bac91b 100644 --- a/src/analysis/installers/msix_family/bundle/mod.rs +++ b/src/analysis/installers/msix_family/bundle/mod.rs @@ -24,7 +24,7 @@ use crate::analysis::Installers; pub struct MsixBundle { pub signature_sha_256: Sha256String, - pub package_family_name: PackageFamilyName<'static>, + pub package_family_name: PackageFamilyName, pub msix_files: Vec, } @@ -59,10 +59,8 @@ impl MsixBundle { } } - let package_family_name = PackageFamilyName::new( - bundle.identity.name().to_owned(), - bundle.identity.publisher(), - ); + let package_family_name = + PackageFamilyName::new(bundle.identity.name(), bundle.identity.publisher()); Ok(Self { msix_files: bundle diff --git a/src/analysis/installers/msix_family/mod.rs b/src/analysis/installers/msix_family/mod.rs index 06b126776..87be4d808 100644 --- a/src/analysis/installers/msix_family/mod.rs +++ b/src/analysis/installers/msix_family/mod.rs @@ -15,14 +15,12 @@ use winget_types::{ Installer, InstallerType, MinimumOSVersion, PackageFamilyName, Platform, RestrictedCapability, UpgradeBehavior, }, + utils::ValidFileExtensions, }; use zip::ZipArchive; use super::msix_family::utils::{get_install_location, hash_signature, read_manifest}; -use crate::{ - analysis::{Installers, extensions::MSIX}, - traits::AsciiExt, -}; +use crate::{analysis::Installers, traits::AsciiExt}; pub struct Msix { appx_manifest: String, @@ -182,7 +180,9 @@ impl Installers for Msix { .target_device_family .iter() .all(|target_device_family| target_device_family.min_version < MSIX_MIN_VERSION) - && !self.appx_manifest.contains_ignore_ascii_case(MSIX); + && !self + .appx_manifest + .contains_ignore_ascii_case(ValidFileExtensions::Msix.as_str()); vec![Installer { platform: self @@ -218,7 +218,7 @@ impl Installers for Msix { .supported_file_types .clone(), package_family_name: Some(PackageFamilyName::new( - self.manifest.identity.name.clone(), + &self.manifest.identity.name, &self.manifest.identity.publisher, )), capabilities: self.manifest.capabilities.unrestricted.clone(), diff --git a/src/analysis/installers/nsis/entry/mod.rs b/src/analysis/installers/nsis/entry/mod.rs index 9afa091bc..1d4aeadd5 100644 --- a/src/analysis/installers/nsis/entry/mod.rs +++ b/src/analysis/installers/nsis/entry/mod.rs @@ -255,8 +255,8 @@ pub enum Entry { } = 40u32.to_le(), Execute { complete_command_line: I32, - wait_flag: I32, output_error_code: I32, + wait_flag: I32, } = 41u32.to_le(), GetFileTime { file: I32, @@ -787,10 +787,10 @@ impl Entry { } } - if !source.is_empty() { - state - .variables - .insert(variable.get().unsigned_abs() as usize, result); + if result.is_empty() { + state.variables.remove(&index); + } else { + state.variables.insert(index, result); } } Self::StrCmp { @@ -1079,10 +1079,17 @@ impl Entry { } Self::Execute { complete_command_line, - wait_flag, output_error_code, + wait_flag, } => { debug!("Execute: {complete_command_line} {wait_flag} {output_error_code}"); + if *wait_flag != I32::ZERO + && let Ok(output_error_code) = usize::try_from(output_error_code.get()) + { + state + .variables + .insert(output_error_code, Cow::Borrowed("0")); + } } Self::GetFileTime { file, @@ -1116,6 +1123,18 @@ impl Entry { && let Some(call) = state.stack.pop() { state.mock_caller.call(&call); + } else if dll_file_name.ends_with("NSISdl.dll") && function == "download" { + // NSISdl::download consumes file, URL, and optional switches, then pushes + // "success" or an error message. Treat downloads as successful so analysis + // can continue past prerequisite bootstrapper steps. + while state.stack.last().is_some_and(|arg| arg.starts_with('/')) { + state.stack.pop(); + } + if state.stack.len() >= 2 { + state.stack.pop(); + state.stack.pop(); + } + state.stack.push(Cow::Borrowed("success")); } debug!( "CallInstDLL: {dll_file_name} {function}{}", diff --git a/src/analysis/installers/nsis/header/decoder.rs b/src/analysis/installers/nsis/header/decoder.rs index fa148036c..40efb8670 100644 --- a/src/analysis/installers/nsis/header/decoder.rs +++ b/src/analysis/installers/nsis/header/decoder.rs @@ -4,9 +4,12 @@ use bzip2::read::BzDecoder; use flate2::read::ZlibDecoder; use liblzma::read::XzDecoder; +use super::nsis_bzip2; + pub enum Decoder { Lzma(XzDecoder), BZip2(BzDecoder), + NsisBZip2(nsis_bzip2::Decoder), Zlib(ZlibDecoder), None(R), } @@ -16,6 +19,7 @@ impl Decoder { match self { Self::Lzma(reader) => reader.into_inner(), Self::BZip2(reader) => reader.into_inner(), + Self::NsisBZip2(reader) => reader.into_inner(), Self::Zlib(reader) => reader.into_inner(), Self::None(reader) => reader, } @@ -27,6 +31,7 @@ impl Read for Decoder { match self { Self::Lzma(reader) => reader.read(buf), Self::BZip2(reader) => reader.read(buf), + Self::NsisBZip2(reader) => reader.read(buf), Self::Zlib(reader) => reader.read(buf), Self::None(reader) => reader.read(buf), } diff --git a/src/analysis/installers/nsis/header/mod.rs b/src/analysis/installers/nsis/header/mod.rs index 56603611d..daeee8589 100644 --- a/src/analysis/installers/nsis/header/mod.rs +++ b/src/analysis/installers/nsis/header/mod.rs @@ -2,13 +2,13 @@ pub mod block; mod compression; mod decoder; pub mod flags; +pub(super) mod nsis_bzip2; use std::{ fmt, io, io::{Error, ErrorKind, Read, Seek}, }; -use bzip2::read::BzDecoder; pub use compression::Compression; pub use decoder::Decoder; use flate2::{Decompress, read::ZlibDecoder}; @@ -247,12 +247,17 @@ impl Header { debug!(?compression, is_solid, compressed_header_size); + let expected_size = first_header.length_of_header() as usize; let mut decoder = match compression { Compression::Lzma(_) => { let stream = LzmaStreamHeader::from_reader(&mut reader)?; Decoder::Lzma(XzDecoder::new_stream(reader, stream)) } - Compression::BZip2 => Decoder::BZip2(BzDecoder::new(reader)), + Compression::BZip2 => Decoder::NsisBZip2(nsis_bzip2::Decoder::new( + reader, + (!is_solid).then_some(compressed_header_size as usize), + expected_size + if is_solid { size_of::() } else { 0 }, + )?), Compression::Zlib => Decoder::Zlib(ZlibDecoder::new_with_decompress( reader, Decompress::new(false), diff --git a/src/analysis/installers/nsis/header/nsis_bzip2.rs b/src/analysis/installers/nsis/header/nsis_bzip2.rs new file mode 100644 index 000000000..c99967d75 --- /dev/null +++ b/src/analysis/installers/nsis/header/nsis_bzip2.rs @@ -0,0 +1,648 @@ +/* + * Adapted from https://github.com/BinFlip/nsis-rs/blob/main/src/decompress/bzip2.rs + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2025 Johann Kempter + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::io::{Cursor, Error, ErrorKind, Read, Result}; + +const BZ_MAX_ALPHA_SIZE: usize = 258; +const BZ_MAX_ALPHA_SIZE_I32: i32 = 258; +const BZ_MAX_CODE_LEN: usize = 23; +const BZ_N_GROUPS: usize = 6; +const BZ_G_SIZE_I32: i32 = 50; +const BZ_MAX_SELECTORS_I32: i32 = 18_002; +const MTFA_SIZE: usize = 4096; +const MTFL_SIZE: usize = 16; +const BLOCK_SIZE: usize = 900_000; +const BLOCK_SIZE_I32: i32 = 900_000; +const BZ_RUNA: i32 = 0; +const BZ_RUNB: i32 = 1; + +pub struct Decoder { + inner: R, + decompressed: Cursor>, +} + +impl Decoder { + pub fn new(mut reader: R, compressed_size: Option, max_output: usize) -> Result { + let compressed = if let Some(compressed_size) = compressed_size { + let mut compressed = vec![0; compressed_size]; + reader.read_exact(&mut compressed)?; + compressed + } else { + let mut compressed = Vec::new(); + reader.read_to_end(&mut compressed)?; + compressed + }; + + Ok(Self { + inner: reader, + decompressed: Cursor::new(decompress(&compressed, max_output)?), + }) + } + + pub fn into_inner(self) -> R { + self.inner + } +} + +impl Read for Decoder { + fn read(&mut self, buf: &mut [u8]) -> Result { + self.decompressed.read(buf) + } +} + +struct BitReader<'a> { + data: &'a [u8], + position: usize, + buffer: u32, + live: i32, +} + +impl<'a> BitReader<'a> { + const fn new(data: &'a [u8]) -> Self { + Self { + data, + position: 0, + buffer: 0, + live: 0, + } + } + + fn get_bits(&mut self, count: i32) -> Result { + loop { + if self.live >= count { + let value = (self.buffer >> (self.live - count)) & ((1 << count) - 1); + self.live -= count; + return i32::try_from(value) + .map_err(|_| invalid_data("NSIS bzip2 bit value out of range")); + } + + let byte = self + .data + .get(self.position) + .ok_or_else(|| invalid_data("Unexpected end of NSIS bzip2 input"))?; + self.buffer = (self.buffer << 8) | u32::from(*byte); + self.live += 8; + self.position += 1; + } + } + + #[inline] + fn get_bit(&mut self) -> Result { + self.get_bits(1) + } + + #[inline] + fn get_u8(&mut self) -> Result { + self.get_bits(8) + } +} + +struct HuffmanTables { + selector: Vec, + min_lens: [i32; BZ_N_GROUPS], + limit: [[i32; BZ_MAX_ALPHA_SIZE]; BZ_N_GROUPS], + perm: [[i32; BZ_MAX_ALPHA_SIZE]; BZ_N_GROUPS], + base: [[i32; BZ_MAX_ALPHA_SIZE]; BZ_N_GROUPS], + n_selectors: i32, + group_no: i32, + group_pos: i32, +} + +pub fn decompress(compressed: &[u8], max_output: usize) -> Result> { + if compressed.is_empty() { + return Err(invalid_data("Empty NSIS bzip2 input")); + } + + let mut reader = BitReader::new(compressed); + let mut output = Vec::with_capacity(max_output.min(BLOCK_SIZE)); + + loop { + match reader.get_u8()? { + 0x17 => break, + 0x31 => decompress_block(&mut reader, &mut output, max_output)?, + header => { + return Err(invalid_data(format!( + "Invalid NSIS bzip2 block header 0x{header:02X}", + ))); + } + } + + if output.len() >= max_output { + output.truncate(max_output); + break; + } + } + + Ok(output) +} + +#[allow(clippy::too_many_lines)] +fn decompress_block( + reader: &mut BitReader<'_>, + output: &mut Vec, + max_output: usize, +) -> Result<()> { + let b0 = reader.get_u8()?; + let b1 = reader.get_u8()?; + let b2 = reader.get_u8()?; + let orig_ptr = (b0 << 16) | (b1 << 8) | b2; + + if !(0..=10 + BLOCK_SIZE_I32).contains(&orig_ptr) { + return Err(invalid_data(format!( + "NSIS bzip2 origPtr out of range: {orig_ptr}", + ))); + } + + let mut in_use16 = [false; 16]; + for item in &mut in_use16 { + *item = reader.get_bit()? == 1; + } + + let mut in_use = [false; 256]; + for (i, group_used) in in_use16.into_iter().enumerate() { + if group_used { + for j in 0..16 { + in_use[i * 16 + j] = reader.get_bit()? == 1; + } + } + } + let mut seq_to_unseq = [0; 256]; + let mut n_in_use = 0; + for (symbol, used) in in_use.into_iter().enumerate() { + if used { + seq_to_unseq[n_in_use] = + u8::try_from(symbol).map_err(|_| invalid_data("NSIS bzip2 symbol out of range"))?; + n_in_use += 1; + } + } + if n_in_use == 0 { + return Err(invalid_data("NSIS bzip2 block has no symbols in use")); + } + + let alpha_size = n_in_use + 2; + let n_groups = reader.get_bits(3)?; + if !(2..=6).contains(&n_groups) { + return Err(invalid_data(format!( + "NSIS bzip2 nGroups out of range: {n_groups}", + ))); + } + let n_groups = + usize::try_from(n_groups).map_err(|_| invalid_data("NSIS bzip2 nGroups out of range"))?; + + let n_selectors = reader.get_bits(15)?; + if !(1..=BZ_MAX_SELECTORS_I32).contains(&n_selectors) { + return Err(invalid_data(format!( + "NSIS bzip2 nSelectors out of range: {n_selectors}", + ))); + } + let n_selectors = usize::try_from(n_selectors) + .map_err(|_| invalid_data("NSIS bzip2 nSelectors out of range"))?; + + let mut selector_mtf = vec![0; n_selectors]; + for selector in &mut selector_mtf { + let mut value = 0; + loop { + if reader.get_bit()? == 0 { + break; + } + value += 1; + if value >= n_groups { + return Err(invalid_data("NSIS bzip2 selector MTF value out of range")); + } + } + *selector = + u8::try_from(value).map_err(|_| invalid_data("NSIS bzip2 selector out of range"))?; + } + + let mut selector = vec![0; n_selectors]; + let mut positions = [0; BZ_N_GROUPS]; + for (value, position) in positions.iter_mut().enumerate().take(n_groups) { + *position = u8::try_from(value) + .map_err(|_| invalid_data("NSIS bzip2 selector position out of range"))?; + } + for (i, selector_mtf) in selector_mtf.into_iter().enumerate() { + let value = selector_mtf as usize; + let selected = positions[value]; + for index in (1..=value).rev() { + positions[index] = positions[index - 1]; + } + positions[0] = selected; + selector[i] = selected; + } + + let mut lengths = [[0; BZ_MAX_ALPHA_SIZE]; BZ_N_GROUPS]; + for table in lengths.iter_mut().take(n_groups) { + let mut current = reader.get_bits(5)?; + for slot in table.iter_mut().take(alpha_size) { + loop { + if !(1..=20).contains(¤t) { + return Err(invalid_data(format!( + "NSIS bzip2 code length out of range: {current}", + ))); + } + if reader.get_bit()? == 0 { + break; + } + if reader.get_bit()? == 0 { + current += 1; + } else { + current -= 1; + } + } + *slot = u8::try_from(current) + .map_err(|_| invalid_data("NSIS bzip2 code length out of range"))?; + } + } + + let mut huffman = HuffmanTables { + selector, + min_lens: [0; BZ_N_GROUPS], + limit: [[0; BZ_MAX_ALPHA_SIZE]; BZ_N_GROUPS], + perm: [[0; BZ_MAX_ALPHA_SIZE]; BZ_N_GROUPS], + base: [[0; BZ_MAX_ALPHA_SIZE]; BZ_N_GROUPS], + n_selectors: i32::try_from(n_selectors) + .map_err(|_| invalid_data("NSIS bzip2 nSelectors out of range"))?, + group_no: -1, + group_pos: 0, + }; + + for (table_index, table_lengths) in lengths.iter().enumerate().take(n_groups) { + let mut min_len = 32; + let mut max_len = 0; + for &length in table_lengths.iter().take(alpha_size) { + let length = i32::from(length); + min_len = min_len.min(length); + max_len = max_len.max(length); + } + create_decode_tables( + &mut huffman.limit[table_index], + &mut huffman.base[table_index], + &mut huffman.perm[table_index], + table_lengths, + min_len, + max_len, + alpha_size, + ); + huffman.min_lens[table_index] = min_len; + } + + let eob = i32::try_from(n_in_use + 1) + .map_err(|_| invalid_data("NSIS bzip2 EOB symbol out of range"))?; + let mut unzftab = [0; 256]; + let mut mtfa = [0; MTFA_SIZE]; + let mut mtfbase = [0; 256 / MTFL_SIZE]; + + let mut kk = MTFA_SIZE - 1; + for ii in (0..(256 / MTFL_SIZE)).rev() { + for jj in (0..MTFL_SIZE).rev() { + mtfa[kk] = u8::try_from(ii * MTFL_SIZE + jj) + .map_err(|_| invalid_data("NSIS bzip2 MTF value out of range"))?; + kk = kk.wrapping_sub(1); + } + mtfbase[ii] = kk.wrapping_add(1); + } + + let mut tt = vec![0; BLOCK_SIZE]; + let mut nblock = 0; + let mut next_sym = get_mtf_val(reader, &mut huffman)?; + + loop { + if next_sym == eob { + break; + } + + if next_sym == BZ_RUNA || next_sym == BZ_RUNB { + let mut es = -1; + let mut power = 1; + while next_sym == BZ_RUNA || next_sym == BZ_RUNB { + if next_sym == BZ_RUNA { + es += power; + } + power <<= 1; + if next_sym == BZ_RUNB { + es += power; + } + next_sym = get_mtf_val(reader, &mut huffman)?; + } + + es += 1; + let symbol = seq_to_unseq[mtfa[mtfbase[0]] as usize]; + unzftab[symbol as usize] += es; + + let es = usize::try_from(es) + .map_err(|_| invalid_data("NSIS bzip2 run length out of range"))?; + if nblock + es > BLOCK_SIZE { + return Err(invalid_data("NSIS bzip2 block overflow during RLE")); + } + for _ in 0..es { + tt[nblock] = u32::from(symbol); + nblock += 1; + } + continue; + } + + if nblock >= BLOCK_SIZE { + return Err(invalid_data("NSIS bzip2 block overflow")); + } + + let symbol = mtf_decode(next_sym, &mut mtfa, &mut mtfbase); + let unseq = seq_to_unseq[symbol as usize]; + unzftab[unseq as usize] += 1; + tt[nblock] = u32::from(unseq); + nblock += 1; + next_sym = get_mtf_val(reader, &mut huffman)?; + } + + let orig_ptr = + usize::try_from(orig_ptr).map_err(|_| invalid_data("NSIS bzip2 origPtr out of range"))?; + if orig_ptr >= nblock { + return Err(invalid_data(format!( + "NSIS bzip2 origPtr {orig_ptr} out of range for nblock {nblock}", + ))); + } + + let mut cftab = [0; 257]; + for i in 1..=256 { + cftab[i] = unzftab[i - 1] + cftab[i - 1]; + } + if cftab[256] + != i32::try_from(nblock).map_err(|_| invalid_data("NSIS bzip2 block is too large"))? + { + return Err(invalid_data("NSIS bzip2 cftab consistency check failed")); + } + + for i in 0..nblock { + let symbol = (tt[i] & 0xff) as usize; + let destination = usize::try_from(cftab[symbol]) + .map_err(|_| invalid_data("NSIS bzip2 cftab index out of range"))?; + tt[destination] |= + u32::try_from(i).map_err(|_| invalid_data("NSIS bzip2 block index out of range"))? << 8; + cftab[symbol] += 1; + } + + decode_bwt_output(&tt, orig_ptr, nblock, output, max_output) +} + +fn create_decode_tables( + limit: &mut [i32], + base: &mut [i32], + perm: &mut [i32], + length: &[u8], + min_len: i32, + max_len: i32, + alpha_size: usize, +) { + let mut pp = 0; + for i in min_len..=max_len { + for (j, &len_j) in length.iter().enumerate().take(alpha_size) { + if i32::from(len_j) == i { + perm[pp] = i32::try_from(j).expect("NSIS bzip2 alpha size fits in i32"); + pp += 1; + } + } + } + + base.iter_mut() + .take(BZ_MAX_CODE_LEN) + .for_each(|item| *item = 0); + for &len_j in length.iter().take(alpha_size) { + let index = len_j as usize + 1; + if index < BZ_MAX_CODE_LEN { + base[index] += 1; + } + } + for i in 1..BZ_MAX_CODE_LEN { + base[i] += base[i - 1]; + } + + limit + .iter_mut() + .take(BZ_MAX_CODE_LEN) + .for_each(|item| *item = 0); + let mut value = 0; + for i in min_len..=max_len { + let index = usize::try_from(i).expect("NSIS bzip2 code length is positive"); + value += base[index + 1] - base[index]; + limit[index] = value - 1; + value <<= 1; + } + for i in (min_len + 1)..=max_len { + let index = usize::try_from(i).expect("NSIS bzip2 code length is positive"); + base[index] = ((limit[index - 1] + 1) << 1) - base[index]; + } +} + +fn get_mtf_val(reader: &mut BitReader<'_>, huffman: &mut HuffmanTables) -> Result { + if huffman.group_pos == 0 { + huffman.group_no += 1; + if huffman.group_no >= huffman.n_selectors { + return Err(invalid_data("NSIS bzip2 ran out of selectors")); + } + huffman.group_pos = BZ_G_SIZE_I32; + } + huffman.group_pos -= 1; + + let group_no = usize::try_from(huffman.group_no) + .map_err(|_| invalid_data("NSIS bzip2 group index out of range"))?; + let selected = usize::from(huffman.selector[group_no]); + let mut zn = huffman.min_lens[selected]; + let mut zvec = reader.get_bits(zn)?; + + loop { + if zn > 20 { + return Err(invalid_data("NSIS bzip2 Huffman code length exceeds 20")); + } + let zn_index = + usize::try_from(zn).map_err(|_| invalid_data("NSIS bzip2 code length out of range"))?; + if zvec <= huffman.limit[selected][zn_index] { + break; + } + zn += 1; + zvec = (zvec << 1) | reader.get_bit()?; + } + + let zn_index = + usize::try_from(zn).map_err(|_| invalid_data("NSIS bzip2 code length out of range"))?; + let index = zvec - huffman.base[selected][zn_index]; + if !(0..BZ_MAX_ALPHA_SIZE_I32).contains(&index) { + return Err(invalid_data("NSIS bzip2 Huffman index out of range")); + } + Ok(huffman.perm[selected][usize::try_from(index) + .map_err(|_| invalid_data("NSIS bzip2 Huffman index out of range"))?]) +} + +fn mtf_decode( + next_sym: i32, + mtfa: &mut [u8; MTFA_SIZE], + mtfbase: &mut [usize; 256 / MTFL_SIZE], +) -> u8 { + let nn = usize::try_from(next_sym - 1).expect("NSIS bzip2 MTF symbol is positive"); + + if nn < MTFL_SIZE { + let position = mtfbase[0]; + let symbol = mtfa[position + nn]; + for index in (1..=nn).rev() { + mtfa[position + index] = mtfa[position + index - 1]; + } + mtfa[position] = symbol; + return symbol; + } + + let list = nn / MTFL_SIZE; + let offset = nn % MTFL_SIZE; + let mut position = mtfbase[list] + offset; + let symbol = mtfa[position]; + + while position > mtfbase[list] { + mtfa[position] = mtfa[position - 1]; + position -= 1; + } + mtfbase[list] += 1; + + for current_list in (1..=list).rev() { + mtfbase[current_list] -= 1; + mtfa[mtfbase[current_list]] = mtfa[mtfbase[current_list - 1] + MTFL_SIZE - 1]; + } + mtfbase[0] -= 1; + mtfa[mtfbase[0]] = symbol; + + if mtfbase[0] == 0 { + let mut kk = MTFA_SIZE - 1; + for ii in (0..(256 / MTFL_SIZE)).rev() { + for jj in (0..MTFL_SIZE).rev() { + mtfa[kk] = mtfa[mtfbase[ii] + jj]; + kk = kk.wrapping_sub(1); + } + mtfbase[ii] = kk.wrapping_add(1); + } + } + + symbol +} + +fn decode_bwt_output( + tt: &[u32], + orig_ptr: usize, + nblock: usize, + output: &mut Vec, + max_output: usize, +) -> Result<()> { + let mut t_pos = tt[orig_ptr] >> 8; + let mut nblock_used = 0; + let mut k0 = bz_get_fast(tt, &mut t_pos)?; + nblock_used += 1; + + let mut state_out_len = 0; + let mut state_out_ch = 0; + + while nblock_used <= nblock { + if output.len() >= max_output { + return Ok(()); + } + + if state_out_len > 0 { + let emit_count = usize::try_from(state_out_len) + .map_err(|_| invalid_data("NSIS bzip2 output run length out of range"))? + .min(max_output - output.len()); + output.extend(std::iter::repeat_n(state_out_ch, emit_count)); + state_out_len -= i32::try_from(emit_count) + .map_err(|_| invalid_data("NSIS bzip2 output run length out of range"))?; + if state_out_len > 0 || output.len() >= max_output { + return Ok(()); + } + continue; + } + + state_out_ch = k0; + let mut count = 1_usize; + + if nblock_used < nblock { + k0 = bz_get_fast(tt, &mut t_pos)?; + nblock_used += 1; + if k0 != state_out_ch { + output.push(state_out_ch); + continue; + } + count = 2; + + if nblock_used < nblock { + k0 = bz_get_fast(tt, &mut t_pos)?; + nblock_used += 1; + if k0 != state_out_ch { + push_repeat(output, state_out_ch, 2, max_output); + continue; + } + count = 3; + + if nblock_used < nblock { + k0 = bz_get_fast(tt, &mut t_pos)?; + nblock_used += 1; + if k0 != state_out_ch { + push_repeat(output, state_out_ch, 3, max_output); + continue; + } + count = 4; + + if nblock_used < nblock { + k0 = bz_get_fast(tt, &mut t_pos)?; + nblock_used += 1; + state_out_len = i32::from(k0) + + i32::try_from(count) + .expect("NSIS bzip2 literal repeat count fits in i32"); + if nblock_used < nblock { + k0 = bz_get_fast(tt, &mut t_pos)?; + nblock_used += 1; + } + continue; + } + } + } + } + + push_repeat(output, state_out_ch, count, max_output); + } + + if state_out_len > 0 { + let emit_count = usize::try_from(state_out_len) + .map_err(|_| invalid_data("NSIS bzip2 output run length out of range"))? + .min(max_output - output.len()); + output.extend(std::iter::repeat_n(state_out_ch, emit_count)); + } + + Ok(()) +} + +fn bz_get_fast(tt: &[u32], t_pos: &mut u32) -> Result { + let entry = *tt + .get(*t_pos as usize) + .ok_or_else(|| invalid_data("NSIS bzip2 BWT position out of range"))?; + *t_pos = entry >> 8; + Ok((entry & 0xff) as u8) +} + +fn push_repeat(output: &mut Vec, byte: u8, count: usize, max_output: usize) { + let count = count.min(max_output.saturating_sub(output.len())); + output.extend(std::iter::repeat_n(byte, count)); +} + +fn invalid_data(message: impl Into) -> Error { + Error::new(ErrorKind::InvalidData, message.into()) +} diff --git a/src/analysis/installers/nsis/mod.rs b/src/analysis/installers/nsis/mod.rs index 0936337f7..865d9393f 100644 --- a/src/analysis/installers/nsis/mod.rs +++ b/src/analysis/installers/nsis/mod.rs @@ -34,24 +34,23 @@ use winget_types::{ AppsAndFeaturesEntries, AppsAndFeaturesEntry, Architecture, InstallationMetadata, Installer, InstallerType, Scope, }, + utils::ValidFileExtensions, }; +use zerocopy::LE; use super::{ - super::extensions::EXE, nsis::{ entry::{Entry, EntryError}, file_system::FsEntry, - first_header::{ - FirstHeader, - HeaderFlags, - }, - header::{Compression, Decoder, Decompressed, Header}, + first_header::{FirstHeader, HeaderFlags}, + header::{Compression, Decoder, Decompressed, Header, nsis_bzip2}, }, pe::{PE, utils::machine_from_exe_reader}, utils::{LzmaStreamHeader, RELATIVE_PROGRAM_FILES_64, RELATIVE_TEMP_FOLDER}, }; use crate::{ analysis::Installers, + read::ReadBytesExt, traits::{FromMachine, IntoWingetArchitecture}, }; @@ -156,6 +155,22 @@ impl Nsis { debug!(%state.registry, %state.file_system); architecture = architecture + .or_else(|| { + let mut has_32_bit_section = false; + let mut has_64_bit_section = false; + + for section in header.blocks().sections(&decompressed_data) { + let name = state.get_string(section.name_offset()); + has_32_bit_section |= name.contains("32Bit") || name.contains("32-bit"); + has_64_bit_section |= name.contains("64Bit") || name.contains("64-bit"); + } + + match (has_32_bit_section, has_64_bit_section) { + (true, true) => Some(Architecture::X86), + (false, true) => Some(Architecture::X64), + _ => None, + } + }) .or_else(|| { state .variables @@ -169,9 +184,8 @@ impl Nsis { .file_system .files() .filter(|file| { - Utf8Path::new(file.name()) - .extension() - .is_some_and(|extension| extension.eq_ignore_ascii_case(EXE)) + ValidFileExtensions::from_path(Utf8Path::new(file.name())) + .is_ok_and(|extension| extension == ValidFileExtensions::Exe) }) .min_by_key(|file| levenshtein(file.name(), &app_name)) .and_then(|file| { @@ -182,6 +196,22 @@ impl Nsis { + size_of::() as u64; } + if !is_solid && compression == Compression::BZip2 { + let reader = decoder.into_inner(); + reader + .seek(SeekFrom::Start(position - size_of::() as u64)) + .ok()?; + let compressed_size = reader.read_u32::().ok()? & !0x8000_0000; + let decoder = nsis_bzip2::Decoder::new( + reader, + Some(compressed_size as usize), + 1 << 20, + ) + .ok()?; + let machine = machine_from_exe_reader(decoder).ok()?; + return Some(Architecture::from_machine(machine)); + } + let mut decoder = if is_solid { let decoder = decoder.into_inner(); decoder.seek(SeekFrom::Start(position)).ok()?; @@ -240,6 +270,19 @@ impl Nsis { impl Installers for Nsis { fn installers(&self) -> Vec { let product_code = self.registry.product_code(); + let detected_scope = self.registry.product_code_scope(); + let install_directory_scope = self + .install_directory + .as_deref() + .and_then(Scope::from_install_directory); + let scope = match (detected_scope, install_directory_scope) { + (Some(detected_scope), Some(install_directory_scope)) + if detected_scope != install_directory_scope => + { + None + } + (detected_scope, install_directory_scope) => detected_scope.or(install_directory_scope), + }; let display_name = self.display_name(); let publisher = self.registry.get_value_by_name("Publisher"); let display_version = self.registry.get_value_by_name("DisplayVersion"); @@ -255,10 +298,7 @@ impl Installers for Nsis { } else { Some(InstallerType::Nullsoft) }, - scope: self - .install_directory - .as_deref() - .and_then(Scope::from_install_directory), + scope, product_code: product_code.map(str::to_owned), apps_and_features_entries: if display_name.is_some() || publisher.is_some() @@ -294,3 +334,45 @@ impl Installers for Nsis { vec![installer] } } + +#[cfg(test)] +mod tests { + use registry::RegRoot; + + use super::*; + + const UNINSTALL_KEY: &str = r"Software\Microsoft\Windows\CurrentVersion\Uninstall\Test.App"; + + fn nsis_with_scope_signals(root: RegRoot, install_directory: &str) -> Nsis { + let mut registry = Registry::new(); + registry.insert_value(root, UNINSTALL_KEY, "DisplayName", "Test App"); + + Nsis { + architecture: Architecture::X64, + is_portable: false, + registry, + primary_language_id: 1033, + install_directory: Some(Utf8WindowsPathBuf::from(install_directory)), + } + } + + #[test] + fn keeps_nsis_scope_when_detected_scope_matches_install_location_scope() { + let installer = + nsis_with_scope_signals(RegRoot::HKEY_LOCAL_MACHINE, r"%ProgramFiles%\Test App") + .installers() + .remove(0); + + assert_eq!(installer.scope, Some(Scope::Machine)); + } + + #[test] + fn does_not_set_nsis_scope_when_detected_scope_conflicts_with_install_location_scope() { + let installer = + nsis_with_scope_signals(RegRoot::HKEY_CURRENT_USER, r"%ProgramFiles%\Test App") + .installers() + .remove(0); + + assert_eq!(installer.scope, None); + } +} diff --git a/src/analysis/installers/nsis/registry/mod.rs b/src/analysis/installers/nsis/registry/mod.rs index 849d9ed70..41728b79e 100644 --- a/src/analysis/installers/nsis/registry/mod.rs +++ b/src/analysis/installers/nsis/registry/mod.rs @@ -6,6 +6,7 @@ use std::{borrow::Borrow, collections::BTreeMap, fmt}; use itertools::{Itertools, Position}; pub use root::RegRoot; pub use r#type::RegType; +use winget_types::installer::Scope; type Key = String; @@ -55,6 +56,22 @@ impl Registry { }) } + pub fn product_code_scope(&self) -> Option { + self.0.iter().find_map(|(root, keys)| { + keys.keys() + .any(|key| { + key.rsplit_once('\\') + .is_some_and(|(parent, _product_code)| parent == CURRENT_VERSION_UNINSTALL) + }) + .then(|| match root { + root if root.is_current_user() => Some(Scope::User), + root if root.is_local_machine() => Some(Scope::Machine), + _ => None, + }) + .flatten() + }) + } + /// Inserts the value into the registry. /// /// If the registry did not have this value name present, [`None`] is returned. diff --git a/src/analysis/installers/nsis/registry/root.rs b/src/analysis/installers/nsis/registry/root.rs index e0920dc6a..1f6a23a5a 100644 --- a/src/analysis/installers/nsis/registry/root.rs +++ b/src/analysis/installers/nsis/registry/root.rs @@ -61,6 +61,20 @@ impl RegRoot { self.0.get() } + pub fn is_current_user(self) -> bool { + matches!( + self, + Self::HKEY_CURRENT_USER | Self::HKEY_CURRENT_USER32 | Self::HKEY_CURRENT_USER64 + ) + } + + pub fn is_local_machine(self) -> bool { + matches!( + self, + Self::HKEY_LOCAL_MACHINE | Self::HKEY_LOCAL_MACHINE32 | Self::HKEY_LOCAL_MACHINE64 + ) + } + /// Returns the registry root as a static string slice if it's known, or `None` otherwise. const fn as_str(self) -> Option<&'static str> { match self { diff --git a/src/analysis/installers/qt.rs b/src/analysis/installers/qt.rs index eadd8443e..862f1ab98 100644 --- a/src/analysis/installers/qt.rs +++ b/src/analysis/installers/qt.rs @@ -8,7 +8,7 @@ use thiserror::Error; use winget_types::{ Version, installer::{ - AppsAndFeaturesEntries, AppsAndFeaturesEntry, Architecture, ExpectedReturnCodes, + AppsAndFeaturesEntries, AppsAndFeaturesEntry, Architecture, ExpectedReturnCode, InstallModes, Installer, InstallerReturnCode, InstallerSwitches, InstallerType, ReturnResponse, }, @@ -143,17 +143,15 @@ impl Installers for Qt { } // https://doc.qt.io/qtinstallerframework/qinstaller-packagemanagercore.html#Status-enum -fn expected_return_codes() -> BTreeSet { +fn expected_return_codes() -> BTreeSet { [ (1, ReturnResponse::ContactSupport), (2, ReturnResponse::InstallInProgress), (3, ReturnResponse::CancelledByUser), ] .into_iter() - .map(|(code, response)| ExpectedReturnCodes { - installer_return_code: InstallerReturnCode::new(code), - return_response: response, - return_response_url: None, + .filter_map(|(code, response)| { + Some(ExpectedReturnCode::new(InstallerReturnCode::new(code)?, response)) }) .collect() } diff --git a/src/analysis/installers/squirrel/mod.rs b/src/analysis/installers/squirrel/mod.rs index c4c2c3add..84bcc3371 100644 --- a/src/analysis/installers/squirrel/mod.rs +++ b/src/analysis/installers/squirrel/mod.rs @@ -154,7 +154,11 @@ impl Installers for Squirrel { vec![Installer { architecture: self.architecture, - r#type: Some(InstallerType::Exe), + r#type: Some(if self.is_velopack { + InstallerType::Velopack + } else { + InstallerType::Squirrel + }), scope: Some(Scope::User), product_code: Some(nuspec.id().to_owned()), apps_and_features_entries: AppsAndFeaturesEntry::builder() diff --git a/src/analysis/installers/zip.rs b/src/analysis/installers/zip.rs index 265836e3b..8b9398b57 100644 --- a/src/analysis/installers/zip.rs +++ b/src/analysis/installers/zip.rs @@ -8,26 +8,113 @@ use std::{ use camino::{Utf8Path, Utf8PathBuf}; use color_eyre::eyre::{Result, bail}; use inquire::{CustomType, MultiSelect, min_length}; +use regex::Regex; +use thiserror::Error; use tracing::debug; -use winget_types::installer::{ - Installer, InstallerType, NestedInstallerFiles, PortableCommandAlias, +use winget_types::{ + installer::{Installer, InstallerType, NestedInstallerFiles, PortableCommandAlias}, + utils::ValidFileExtensions, }; use zip::ZipArchive; use super::super::Analyzer; -use crate::prompts::handle_inquire_error; - -const VALID_NESTED_FILE_EXTENSIONS: [&str; 6] = - ["msix", "msi", "appx", "exe", "msixbundle", "appxbundle"]; +use crate::{prompts::handle_inquire_error, traits::path::LowercaseExtension}; const IGNORABLE_FOLDERS: [&str; 2] = ["__MACOSX", "resources"]; +#[derive(Debug, Error)] +#[error("{path} is not a valid nested installer file")] +struct InvalidNestedInstallerError { + path: Utf8PathBuf, + #[source] + source: Box, +} + +enum NestedFileMatch { + Contains(String), + Glob(Regex), +} + +impl NestedFileMatch { + fn new(pattern: &str) -> Result { + if pattern.contains(['*', '?', '[']) { + Ok(Self::Glob(Regex::new(&glob_to_regex(pattern))?)) + } else { + Ok(Self::Contains(pattern.to_ascii_lowercase())) + } + } + + fn matches(&self, path: &Utf8Path) -> bool { + match self { + Self::Contains(pattern) => path.as_str().to_ascii_lowercase().contains(pattern), + Self::Glob(pattern) => { + let path = path.as_str().to_ascii_lowercase(); + let file_name = Utf8Path::new(&path).file_name().unwrap_or(path.as_str()); + + pattern.is_match(&path) || pattern.is_match(file_name) + } + } + } +} + +fn glob_to_regex(pattern: &str) -> String { + let pattern = pattern.replace('\\', "/").to_ascii_lowercase(); + let mut regex = String::from("^"); + let mut chars = pattern.chars().peekable(); + + while let Some(character) = chars.next() { + match character { + '*' => { + if chars.next_if_eq(&'*').is_some() { + regex.push_str(".*"); + } else { + regex.push_str("[^/]*"); + } + } + '?' => regex.push_str("[^/]"), + '[' => { + regex.push('['); + if chars.next_if_eq(&'!').is_some() { + regex.push('^'); + } else if chars.next_if_eq(&'^').is_some() { + regex.push('\\'); + regex.push('^'); + } + + for character in chars.by_ref() { + if character == ']' { + regex.push(']'); + break; + } + if character == '\\' { + regex.push('/'); + } else { + regex.push(character); + } + } + } + _ => regex.push_str(®ex::escape(&character.to_string())), + } + } + + regex.push('$'); + regex +} + pub struct Zip { archive: ZipArchive, pub possible_installer_files: Vec, pub installers: Vec, } +pub struct MatchedInstaller { + pub installer: Installer, + #[allow(dead_code)] + pub file_version: Option, + #[allow(dead_code)] + pub product_version: Option, +} + impl Zip { pub fn new(reader: R) -> Result { let mut zip = ZipArchive::new(reader)?; @@ -36,11 +123,8 @@ impl Zip { .file_names() .map(Utf8Path::new) .filter(|file_name| { - VALID_NESTED_FILE_EXTENSIONS.iter().any(|file_extension| { - file_name - .extension() - .is_some_and(|extension| extension.eq_ignore_ascii_case(file_extension)) - }) + ValidFileExtensions::from_path(file_name) + .is_ok_and(ValidFileExtensions::is_valid_nested_installer) }) .filter(|file_name| { // Ignore folders that the main executable is unlikely to be in @@ -59,8 +143,6 @@ impl Zip { bail!("ZIP contains no valid installer files (exe, msi, msix, appx, etc.)"); } - let mut nested_installer_files = BTreeSet::new(); - let mut installers = None; let exe_candidates = possible_installer_files .iter() .filter(|file_name| { @@ -83,48 +165,40 @@ impl Zip { None }; - if let Some(chosen_file_name) = chosen_file_name { - nested_installer_files = BTreeSet::from([NestedInstallerFiles { - relative_file_path: (*chosen_file_name).clone(), + let installers = if let Some(chosen_file_name) = chosen_file_name { + let nested_installer_files = BTreeSet::from([NestedInstallerFiles { + relative_file_path: chosen_file_name.lowercase_extension(), portable_command_alias: None, }]); - if let Ok(mut chosen_file) = zip.by_name(chosen_file_name.as_str()) { - let mut temp_file = tempfile::tempfile()?; - io::copy(&mut chosen_file, &mut temp_file)?; - temp_file.seek(SeekFrom::Start(0))?; - let file_analyzer = Analyzer::new(&mut temp_file, chosen_file_name.as_str())?; - installers = Some( - file_analyzer - .installers - .into_iter() - .map(|installer| Installer { - r#type: Some(InstallerType::Zip), - nested_installer_type: installer - .r#type - .and_then(|installer_type| installer_type.try_into().ok()), - nested_installer_files: nested_installer_files.clone(), - ..installer - }) - .collect::>(), - ); - } - } + let file_installers = Self::analyze_nested_file_in_archive(&mut zip, chosen_file_name)?; + + file_installers + .into_iter() + .map(|installer| Installer { + r#type: Some(InstallerType::Zip), + nested_installer_type: installer + .r#type + .and_then(|installer_type| installer_type.try_into().ok()), + nested_installer_files: nested_installer_files.clone(), + ..installer + }) + .collect() + } else { + vec![Installer { + r#type: Some(InstallerType::Zip), + ..Installer::default() + }] + }; Ok(Self { archive: zip, possible_installer_files, - installers: installers.unwrap_or_else(|| { - vec![Installer { - r#type: Some(InstallerType::Zip), - nested_installer_files, - ..Installer::default() - }] - }), + installers, }) } pub fn prompt(&mut self) -> Result<()> { - if !&self.possible_installer_files.is_empty() { + if !self.possible_installer_files.is_empty() { let chosen = MultiSelect::new( "Select the nested files", mem::take(&mut self.possible_installer_files), @@ -132,21 +206,22 @@ impl Zip { .with_validator(min_length!(1)) .prompt() .map_err(handle_inquire_error)?; - let first_choice = chosen.first().unwrap(); - let mut temp_file = tempfile::tempfile()?; - io::copy( - &mut self.archive.by_name(first_choice.as_str())?, - &mut temp_file, + let mut chosen_paths = chosen.iter(); + let first_file_installers = Self::analyze_nested_file_in_archive( + &mut self.archive, + chosen_paths.next().unwrap(), )?; - temp_file.seek(SeekFrom::Start(0))?; - let file_analyzer = Analyzer::new(&mut temp_file, first_choice.file_name().unwrap())?; + for path in chosen_paths { + Self::analyze_nested_file_in_archive(&mut self.archive, path)?; + } + let first_file_is_portable = first_file_installers + .first() + .is_some_and(|installer| installer.r#type == Some(InstallerType::Portable)); let nested_installer_files = chosen .into_iter() .map(|path| { Ok(NestedInstallerFiles { - portable_command_alias: if file_analyzer.installers[0].r#type - == Some(InstallerType::Portable) - { + portable_command_alias: if first_file_is_portable { CustomType::::new(&format!( "Portable command alias for {path}:", )) @@ -155,14 +230,14 @@ impl Zip { } else { None }, - relative_file_path: path, + relative_file_path: path.lowercase_extension(), }) }) .collect::>>()?; - self.installers = file_analyzer - .installers + self.installers = first_file_installers .into_iter() .map(|installer| Installer { + r#type: Some(InstallerType::Zip), nested_installer_type: installer .r#type .and_then(|installer_type| installer_type.try_into().ok()), @@ -173,4 +248,175 @@ impl Zip { } Ok(()) } + + #[allow(dead_code)] + pub fn analyze_matches(&mut self, matches: &[String]) -> Result> { + Ok(self + .analyze_matches_with_metadata(matches)? + .into_iter() + .map(|analysis| analysis.installer) + .collect()) + } + + pub fn analyze_matches_with_metadata( + &mut self, + matches: &[String], + ) -> Result> { + let matches = matches + .iter() + .map(|pattern| NestedFileMatch::new(pattern)) + .collect::>>()?; + + let installers = self + .possible_installer_files + .iter() + .filter(|path| matches.iter().any(|file_match| file_match.matches(path))) + .map(|path| { + let mut nested_file = self.archive.by_name(path.as_str())?; + let mut temp_file = tempfile::tempfile()?; + io::copy(&mut nested_file, &mut temp_file)?; + temp_file.seek(SeekFrom::Start(0))?; + + let nested_analyzer = + Analyzer::new(&mut temp_file, path.as_str()).map_err(|source| { + InvalidNestedInstallerError { + path: path.clone(), + source: source.into(), + } + })?; + let nested_installer_files = BTreeSet::from([NestedInstallerFiles { + relative_file_path: path.lowercase_extension(), + portable_command_alias: None, + }]); + let file_version = nested_analyzer.file_version; + let product_version = nested_analyzer.product_version; + + Ok(nested_analyzer + .installers + .into_iter() + .map(move |installer| Installer { + r#type: Some(InstallerType::Zip), + nested_installer_type: installer + .r#type + .and_then(|installer_type| installer_type.try_into().ok()), + nested_installer_files: nested_installer_files.clone(), + ..installer + }) + .map({ + let file_version = file_version.clone(); + let product_version = product_version.clone(); + move |installer| MatchedInstaller { + installer, + file_version: file_version.clone(), + product_version: product_version.clone(), + } + })) + }) + .collect::>>()? + .into_iter() + .flatten() + .collect::>(); + + Ok(installers) + } + + fn analyze_nested_file_in_archive( + archive: &mut ZipArchive, + path: &Utf8Path, + ) -> Result> { + let mut chosen_file = archive.by_name(path.as_str())?; + let mut temp_file = tempfile::tempfile()?; + io::copy(&mut chosen_file, &mut temp_file)?; + temp_file.seek(SeekFrom::Start(0))?; + let analyzer = Analyzer::new(&mut temp_file, path.as_str()).map_err(|source| { + InvalidNestedInstallerError { + path: path.to_owned(), + source: source.into(), + } + })?; + Ok(analyzer.installers) + } +} + +#[cfg(test)] +mod tests { + use std::io::{Cursor, Write}; + + use color_eyre::eyre::Result; + use zip::{CompressionMethod, ZipWriter, write::SimpleFileOptions}; + + use super::*; + + const TTF_SIGNATURE: [u8; 4] = [0x00, 0x01, 0x00, 0x00]; + + fn zip_with_files(files: &[(&str, &[u8])]) -> Result> { + let mut buffer = Cursor::new(Vec::new()); + { + let mut writer = ZipWriter::new(&mut buffer); + let options = + SimpleFileOptions::default().compression_method(CompressionMethod::Stored); + + for (path, contents) in files { + writer.start_file(path, options)?; + writer.write_all(contents)?; + } + + writer.finish()?; + } + + Ok(buffer.into_inner()) + } + + #[test] + fn selected_nested_files_reject_invalid_file_with_valid_extension() -> Result<()> { + let zip_bytes = zip_with_files(&[("valid.ttf", &TTF_SIGNATURE), ("invalid.ttf", b"nope")])?; + let mut zip = Zip::new(Cursor::new(zip_bytes))?; + let selected_files = vec![ + Utf8PathBuf::from("valid.ttf"), + Utf8PathBuf::from("invalid.ttf"), + ]; + + let error = selected_files + .iter() + .map(|path| Zip::analyze_nested_file_in_archive(&mut zip.archive, path)) + .collect::>>() + .unwrap_err(); + + assert_eq!( + error.to_string(), + "invalid.ttf is not a valid nested installer file" + ); + Ok(()) + } + + #[test] + fn selected_nested_file_accepts_valid_file() -> Result<()> { + let zip_bytes = zip_with_files(&[ + ("valid.ttf", &TTF_SIGNATURE), + ("ignored.txt", b"not an installer"), + ])?; + let mut zip = Zip::new(Cursor::new(zip_bytes))?; + let selected_file = Utf8PathBuf::from("valid.ttf"); + + let installers = Zip::analyze_nested_file_in_archive(&mut zip.archive, &selected_file)?; + + assert_eq!(installers[0].r#type, Some(InstallerType::Font)); + Ok(()) + } + + #[test] + fn multiple_nested_candidates_do_not_infer_nested_installer() -> Result<()> { + let zip_bytes = zip_with_files(&[ + ("first.exe", b"not an exe"), + ("second.exe", b"not an exe"), + ("valid.ttf", &TTF_SIGNATURE), + ])?; + + let zip = Zip::new(Cursor::new(zip_bytes))?; + + assert_eq!(zip.installers[0].r#type, Some(InstallerType::Zip)); + assert_eq!(zip.installers[0].nested_installer_type, None); + assert!(zip.installers[0].nested_installer_files.is_empty()); + Ok(()) + } } diff --git a/src/analysis/mod.rs b/src/analysis/mod.rs index b85e010fc..0f4ba0469 100644 --- a/src/analysis/mod.rs +++ b/src/analysis/mod.rs @@ -1,7 +1,8 @@ mod analyzer; -mod extensions; pub mod installers; +mod pe_info; mod r#trait; pub use analyzer::Analyzer; +pub use pe_info::PeInfo; pub use r#trait::Installers; diff --git a/src/analysis/pe_info.rs b/src/analysis/pe_info.rs new file mode 100644 index 000000000..cd98a13c9 --- /dev/null +++ b/src/analysis/pe_info.rs @@ -0,0 +1,94 @@ +use std::collections::BTreeMap; + +use serde::Serialize; + +use crate::analysis::installers::pe::VSVersionInfo; + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct PeInfo { + pub fixed_file_info: PeFixedFileInfo, + + #[serde(skip_serializing_if = "BTreeMap::is_empty")] + pub string_file_info: BTreeMap, +} + +impl PeInfo { + pub fn from_version_info(version_info: &VSVersionInfo<'_>) -> Self { + Self { + fixed_file_info: PeFixedFileInfo::from(version_info), + string_file_info: string_file_info_from_entries(version_info.string_table()), + } + } + + pub fn file_version(&self) -> Option<&str> { + self.string_file_info.get("FileVersion").map(String::as_str) + } + + pub fn product_version(&self) -> Option<&str> { + self.string_file_info + .get("ProductVersion") + .map(String::as_str) + } +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct PeFixedFileInfo { + pub file_version: String, + pub product_version: String, + #[serde(rename = "FileOS")] + pub file_os: u32, + pub file_type: u32, + pub file_subtype: u32, +} + +impl From<&VSVersionInfo<'_>> for PeFixedFileInfo { + fn from(version_info: &VSVersionInfo<'_>) -> Self { + let fixed_file_info = version_info.fixed; + + Self { + file_version: format_version(fixed_file_info.file_version_raw()), + product_version: format_version(fixed_file_info.product_version_raw()), + file_os: fixed_file_info.file_os(), + file_type: fixed_file_info.file_type(), + file_subtype: fixed_file_info.file_subtype(), + } + } +} + +fn format_version((major, minor, patch, build): (u16, u16, u16, u16)) -> String { + format!("{major}.{minor}.{patch}.{build}") +} + +fn string_file_info_from_entries<'a>( + entries: impl IntoIterator, +) -> BTreeMap { + entries + .into_iter() + .filter(|(_, value)| !value.trim().is_empty()) + .map(|(key, value)| (key.to_owned(), value.to_owned())) + .collect() +} + +#[cfg(test)] +mod tests { + use super::string_file_info_from_entries; + + #[test] + fn empty_string_file_info_values_are_omitted() { + let string_file_info = string_file_info_from_entries([ + ("FileVersion", "1.2.3"), + ("ProductName", ""), + ("CompanyName", " "), + ]); + + assert_eq!(string_file_info.len(), 1); + assert_eq!( + string_file_info.get("FileVersion").map(String::as_str), + Some("1.2.3") + ); + assert!(!string_file_info.contains_key("ProductName")); + assert!(!string_file_info.contains_key("CompanyName")); + } +} diff --git a/src/anthelion/analyze_installer.rs b/src/anthelion/analyze_installer.rs new file mode 100644 index 000000000..672b19e28 --- /dev/null +++ b/src/anthelion/analyze_installer.rs @@ -0,0 +1,120 @@ +use std::{num::NonZeroUsize, str::FromStr}; + +use napi::bindgen_prelude::*; +use napi_derive::napi; + +use super::types::{ + AnalyzeInstallerResult, AppsAndFeaturesEntryAnalysis, InstallerAnalysis, + NestedInstallerFileAnalysis, +}; +use crate::{download::Downloader, manifests::Url}; + +/// Analyze an installer from a URL and return installer analysis information. +/// +/// When the installer is a ZIP, `matches` can be used to select nested installer +/// files. Plain strings are matched as case-insensitive substrings, while values +/// containing glob metacharacters are matched as glob patterns. +/// +/// # Errors +/// +/// Returns `InvalidArg` if `url` is invalid. +/// Returns `GenericFailure` if downloading or analyzing the installer fails. +#[napi] +pub async fn analyze_installer( + url: String, + matches: Option>, +) -> napi::Result { + let installer_url = Url::from_str(&url) + .map_err(|e| Error::new(Status::InvalidArg, format!("Invalid URL: {e}")))?; + + let downloader = Downloader::new_with_concurrent_and_progress(NonZeroUsize::MIN, false) + .map_err(|e| { + Error::new( + Status::GenericFailure, + format!("Failed to create downloader: {e}"), + ) + })?; + + let mut downloaded_file = downloader + .download([installer_url]) + .await + .map_err(|e| { + Error::new( + Status::GenericFailure, + format!("Failed to download installer: {e}"), + ) + })? + .into_iter() + .next() + .ok_or_else(|| Error::new(Status::GenericFailure, "No installer was downloaded"))?; + + let mut analyzer = + crate::analysis::Analyzer::new(&mut downloaded_file.file, &downloaded_file.file_name) + .map_err(|e| { + Error::new( + Status::GenericFailure, + format!("Failed to analyze installer: {e}"), + ) + })?; + + if let (Some(zip), Some(matches)) = (&mut analyzer.zip, matches.as_ref()) + && !matches.is_empty() + { + analyzer.installers = zip.analyze_matches(matches).map_err(|e| { + Error::new( + Status::GenericFailure, + format!("Failed to analyze matching ZIP installers: {e}"), + ) + })?; + } + + for installer in &mut analyzer.installers { + installer.url = downloaded_file.url.inner().clone(); + installer.release_date = downloaded_file.last_modified; + } + + let analysis = analyzer + .installers + .iter() + .map(|installer| InstallerAnalysis { + installer_locale: installer + .locale + .as_ref() + .map(std::string::ToString::to_string), + architecture: installer.architecture.to_string(), + installer_type: installer.r#type.map(|t| t.to_string()), + nested_installer_type: installer.nested_installer_type.map(|t| t.to_string()), + nested_installer_files: installer + .nested_installer_files + .iter() + .map(|f| NestedInstallerFileAnalysis { + relative_file_path: f.relative_file_path.to_string(), + }) + .collect(), + apps_and_features_entries: installer + .apps_and_features_entries + .iter() + .map(|entry| AppsAndFeaturesEntryAnalysis { + display_name: entry.display_name().map(str::to_owned), + publisher: entry.publisher().map(str::to_owned), + display_version: entry + .display_version() + .map(std::string::ToString::to_string), + product_code: entry.product_code().map(str::to_owned), + upgrade_code: entry.upgrade_code().map(str::to_owned), + installer_type: entry.installer_type().map(|t| t.to_string()), + }) + .collect(), + scope: installer.scope.map(|s| s.to_string()), + installer_url: installer.url.to_string(), + installer_sha256: installer.sha_256.to_string(), + release_date: installer.release_date.map(|d| d.to_string()), + }) + .collect(); + + Ok(AnalyzeInstallerResult { + analysis, + file_version: analyzer.file_version, + product_version: analyzer.product_version, + }) +} diff --git a/src/anthelion/get_existing_pull_request.rs b/src/anthelion/get_existing_pull_request.rs new file mode 100644 index 000000000..91d45fa5c --- /dev/null +++ b/src/anthelion/get_existing_pull_request.rs @@ -0,0 +1,74 @@ +use napi::bindgen_prelude::*; +use napi_derive::napi; +use winget_types::{PackageIdentifier, PackageVersion}; + +use super::{ + token::resolve_github_token, + types::{ExistingPullRequestResult, GetExistingPullRequestOptions}, +}; +use crate::github::{client::GitHub, graphql::types::PullRequestState}; + +/// Get an existing pull request for a package version in winget-pkgs. +/// +/// # Errors +/// +/// Returns `InvalidArg` if `package_identifier` or `version` are invalid. +/// Returns `GenericFailure` if creating the GitHub client or querying pull requests fails. +#[napi] +pub async fn get_existing_pull_request( + options: GetExistingPullRequestOptions, +) -> napi::Result> { + let package_identifier: PackageIdentifier = + options.package_identifier.parse().map_err(|e| { + Error::new( + Status::InvalidArg, + format!("Invalid package identifier: {e}"), + ) + })?; + + let package_version: PackageVersion = options + .version + .parse() + .map_err(|e| Error::new(Status::InvalidArg, format!("Invalid package version: {e}")))?; + + let token = resolve_github_token(options.token.as_deref())?; + + let github = GitHub::new(&token).map_err(|e| { + Error::new( + Status::GenericFailure, + format!("Failed to create GitHub client: {e}"), + ) + })?; + + let ignore_other_users = options + .ignore_pull_requests_created_by_other_users + .unwrap_or_default(); + let pull_request = github + .get_existing_pull_request(&package_identifier, &package_version, ignore_other_users) + .await + .map_err(|e| { + Error::new( + Status::GenericFailure, + format!("Failed to get existing pull request: {e}"), + ) + })?; + + let Some(pull_request) = pull_request else { + return Ok(None); + }; + + let created_by = pull_request.author_login().cloned().unwrap_or_default(); + + Ok(Some(ExistingPullRequestResult { + pull_request_url: pull_request.url.to_string(), + created_by, + created_by_authenticated_user: pull_request.viewer_did_author, + state: match pull_request.state { + PullRequestState::Open => "open", + PullRequestState::Closed => "closed", + PullRequestState::Merged => "merged", + } + .to_string(), + created_at: pull_request.created_at.to_rfc3339(), + })) +} diff --git a/src/anthelion/github_configuration.rs b/src/anthelion/github_configuration.rs new file mode 100644 index 000000000..47865a9d8 --- /dev/null +++ b/src/anthelion/github_configuration.rs @@ -0,0 +1,62 @@ +use std::sync::OnceLock; + +use napi::bindgen_prelude::*; +use napi_derive::napi; + +use crate::github::{MICROSOFT, WINGET_PKGS}; + +static CONFIG: OnceLock = OnceLock::new(); + +struct Config { + github_token: Option, + dry_run: Option, +} + +impl Config { + fn get() -> &'static Self { + CONFIG.get_or_init(|| Self { + github_token: None, + dry_run: None, + }) + } +} + +pub fn github_token() -> Option<&'static str> { + Config::get().github_token.as_deref() +} + +pub fn dry_run() -> Option { + Config::get().dry_run +} + +fn get_env(env: &Object, name: &str) -> napi::Result> { + Ok(env + .get_named_property::>(name)? + .filter(|value| !value.trim().is_empty())) +} + +/// Copies configuration from Node's `process.env` into the native binding. +#[napi(module_exports)] +pub fn configure(env: Env) -> napi::Result<()> { + let process: Object = env.get_global()?.get_named_property("process")?; + let process_env: Object = process.get_named_property("env")?; + + if let Some(owner) = get_env(&process_env, "KOMAC_GITHUB_OWNER")? { + MICROSOFT.set(owner); + } + if let Some(repo) = get_env(&process_env, "KOMAC_GITHUB_REPO")? { + WINGET_PKGS.set(repo); + } + + let dry_run = get_env(&process_env, "DRY_RUN")? + .map(|value| value.parse()) + .transpose() + .map_err(|_| Error::new(Status::InvalidArg, "DRY_RUN must be true or false"))?; + + let _ = CONFIG.set(Config { + github_token: get_env(&process_env, "GITHUB_TOKEN")?, + dry_run, + }); + + Ok(()) +} diff --git a/src/anthelion/mod.rs b/src/anthelion/mod.rs new file mode 100644 index 000000000..a71e1ad1a --- /dev/null +++ b/src/anthelion/mod.rs @@ -0,0 +1,20 @@ +mod analyze_installer; +mod get_existing_pull_request; +mod github_configuration; +mod release_notes; +mod token; +mod types; +mod update_helpers; +mod update_version; + +pub use analyze_installer::analyze_installer; +pub use get_existing_pull_request::get_existing_pull_request; +pub use release_notes::{ + get_formatted_github_release_notes, html_to_plain_text, markdown_to_plain_text, +}; +pub use types::{ + AnalyzeInstallerResult, AppsAndFeaturesEntryAnalysis, ExistingPullRequestResult, + GetExistingPullRequestOptions, InstallerAnalysis, ManifestChange, NestedInstallerFileAnalysis, + UpdateVersionOptions, UpdateVersionResult, +}; +pub use update_version::update_version; diff --git a/src/anthelion/release_notes.rs b/src/anthelion/release_notes.rs new file mode 100644 index 000000000..46b1f4c53 --- /dev/null +++ b/src/anthelion/release_notes.rs @@ -0,0 +1,127 @@ +use napi::bindgen_prelude::*; +use napi_derive::napi; +use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd}; +use winget_types::locale::ReleaseNotes; + +use super::token::resolve_github_token; +use crate::{ + github::{client::GitHub, graphql::types::Html}, + traits::FromHtml, +}; + +/// Fetch, normalize, and return release notes for a GitHub release tag. +/// +/// # Errors +/// +/// Returns `GenericFailure` if creating the GitHub client or fetching release data fails. +/// Returns an error from token resolution when no usable token is available. +#[napi] +pub async fn get_formatted_github_release_notes( + owner: String, + repo: String, + tag: String, + token: Option, +) -> napi::Result> { + let token = resolve_github_token(token.as_deref())?; + + let github = GitHub::new(&token).map_err(|e| { + Error::new( + Status::GenericFailure, + format!("Failed to create GitHub client: {e}"), + ) + })?; + + let github_values = github + .get_all_values() + .owner(owner) + .repo(repo) + .tag_name(tag) + .send() + .await + .map_err(|e| { + Error::new( + Status::GenericFailure, + format!("Failed to fetch GitHub release values: {e}"), + ) + })?; + + Ok(github_values.release_notes.map(|notes| notes.to_string())) +} + +#[napi] +/// Convert HTML release notes content to plain text. +/// +/// # Errors +/// +/// Returns `GenericFailure` if the conversion task panics. +pub async fn html_to_plain_text(html: String) -> napi::Result> { + tokio::task::spawn_blocking(move || { + ReleaseNotes::from_html(&Html::new(html)).map(|notes| notes.to_string()) + }) + .await + .map_err(|e| Error::new(Status::GenericFailure, format!("Task panicked: {e}"))) +} + +#[napi] +/// Convert Markdown release notes content to plain text. +/// +/// # Errors +/// +/// Returns `GenericFailure` if the conversion task panics. +pub async fn markdown_to_plain_text(markdown: String) -> napi::Result> { + tokio::task::spawn_blocking(move || { + let mut text = String::new(); + let mut seen_heading = false; + + for event in Parser::new_ext(&markdown, Options::all()) { + match event { + Event::Start(Tag::Heading { .. }) => { + if seen_heading && !text.ends_with("\n\n") { + if !text.ends_with('\n') { + text.push('\n'); + } + text.push('\n'); + } + seen_heading = true; + } + Event::Start(Tag::Item) => { + if !text.ends_with('\n') && !text.is_empty() { + text.push('\n'); + } + text.push_str("- "); + } + Event::Text(content) + | Event::Code(content) + | Event::Html(content) + | Event::InlineHtml(content) => text.push_str(&content), + Event::SoftBreak | Event::HardBreak => text.push('\n'), + Event::Rule if !text.ends_with('\n') => text.push('\n'), + Event::TaskListMarker(checked) => { + text.push_str(if checked { "[x] " } else { "[ ] " }); + } + Event::End(tag) + if matches!( + tag, + TagEnd::Paragraph + | TagEnd::Heading(..) + | TagEnd::BlockQuote(..) + | TagEnd::CodeBlock + | TagEnd::Item + | TagEnd::List(..) + | TagEnd::Table + | TagEnd::TableHead + | TagEnd::TableRow + ) && !text.ends_with('\n') => + { + text.push('\n'); + } + _ => {} + } + } + + let plain_text = text.trim(); + (!plain_text.is_empty()).then(|| plain_text.to_string()) + }) + .await + .map_err(|e| Error::new(Status::GenericFailure, format!("Task panicked: {e}"))) +} diff --git a/src/anthelion/token.rs b/src/anthelion/token.rs new file mode 100644 index 000000000..f02af0f43 --- /dev/null +++ b/src/anthelion/token.rs @@ -0,0 +1,39 @@ +use napi::bindgen_prelude::*; +use secrecy::SecretString; + +use super::github_configuration; + +pub struct GitHubToken { + token: SecretString, +} + +impl GitHubToken { + fn from_string(token: String) -> Self { + Self { + token: SecretString::new(token.into_boxed_str()), + } + } +} + +impl AsRef for GitHubToken { + fn as_ref(&self) -> &SecretString { + &self.token + } +} + +pub fn resolve_github_token(token: Option<&str>) -> napi::Result { + if let Some(env_token) = github_configuration::github_token() { + return Ok(GitHubToken::from_string(env_token.to_owned())); + } + + if let Some(token) = token + && !token.trim().is_empty() + { + return Ok(GitHubToken::from_string(token.to_owned())); + } + + Err(Error::new( + Status::InvalidArg, + "No GitHub token provided. Set GITHUB_TOKEN or pass token.", + )) +} diff --git a/src/anthelion/types.rs b/src/anthelion/types.rs new file mode 100644 index 000000000..fe4ef2e99 --- /dev/null +++ b/src/anthelion/types.rs @@ -0,0 +1,149 @@ +use napi_derive::napi; + +/// Options for update version. +#[napi(object)] +pub struct UpdateVersionOptions { + /// The package's unique identifier (e.g. "Microsoft.VisualStudioCode") + pub package_identifier: String, + /// The new package version. + /// + /// You can also pass one of: + /// - `displayVersion` + /// - `productVersion` + /// - `fileVersion` + /// + /// In that case, the version will be resolved from installer analysis. + pub version: String, + /// List of installer URLs (may include "|architecture" suffix) + pub urls: Vec, + /// ZIP nested installer match patterns used during installer analysis. + /// These matches also affect `displayVersion`, `productVersion`, and + /// `fileVersion` version selector resolution. + /// + /// Plain strings are matched as case-insensitive substrings, while values + /// containing glob metacharacters are matched as glob patterns. + pub installer_matches: Option>, + /// URL to the release notes + pub release_notes_url: Option, + /// Release notes text for the manifest + pub release_notes: Option, + /// Run without submitting a PR + pub dry_run: Option, + /// Version to replace (use "latest" for the latest version) + pub replace: Option, + /// Look for the package under fonts instead of probing manifests first + pub font: Option, + /// GitHub personal access token with the `public_repo` scope + pub token: Option, +} + +/// Options for get existing pull request. +#[napi(object)] +pub struct GetExistingPullRequestOptions { + /// The package's unique identifier (e.g. "Microsoft.VisualStudioCode") + pub package_identifier: String, + /// The package version to search for + pub version: String, + /// GitHub personal access token with the `public_repo` scope. + /// If omitted, `GITHUB_TOKEN` from the environment is used. + pub token: Option, + /// Ignore pull requests not created by the authenticated user. + pub ignore_pull_requests_created_by_other_users: Option, +} + +/// Existing pull request metadata. +#[napi(object)] +pub struct ExistingPullRequestResult { + /// The URL of the existing pull request + pub pull_request_url: String, + /// The GitHub login of the user or bot that created the pull request + pub created_by: String, + /// Whether the pull request was created by the authenticated user + pub created_by_authenticated_user: bool, + /// The current state of the pull request (`open`, `closed`, or `merged`) + pub state: String, + /// The pull request creation timestamp in RFC3339 format + pub created_at: String, +} + +/// Result of the update version operation. +#[napi(object)] +pub struct UpdateVersionResult { + /// The URL of the created pull request, if submitted + pub pull_request_url: Option, + /// The generated manifest changes as a list of (path, content) pairs + pub changes: Vec, + /// The package identifier that was updated + pub package_identifier: String, + /// The version that was created + pub version: String, +} + +/// A single manifest file change. +#[napi(object)] +pub struct ManifestChange { + /// The file path within the winget-pkgs repository + pub path: String, + /// The YAML content of the manifest + pub content: String, +} + +/// Result of the analyze installer operation. +#[napi(object)] +pub struct AnalyzeInstallerResult { + /// Detected installer information. + pub analysis: Vec, + /// PE `FileVersion` string from the version info resource, if present. + pub file_version: Option, + /// PE `ProductVersion` string from the version info resource, if present. + pub product_version: Option, +} + +/// Installer information detected during analysis. +#[napi(object)] +pub struct InstallerAnalysis { + /// Installer locale (if present). + pub installer_locale: Option, + /// Installer architecture. + pub architecture: String, + /// Installer type (if detected). + pub installer_type: Option, + /// Nested installer type (if present). + pub nested_installer_type: Option, + /// Nested installer files within archives. + pub nested_installer_files: Vec, + /// Apps and Features / ARP entries detected for this installer. + pub apps_and_features_entries: Vec, + /// Install scope (if present). + pub scope: Option, + /// Installer download URL. + pub installer_url: String, + /// Installer SHA-256 hash. + pub installer_sha256: String, + /// Installer release date, if available. + pub release_date: Option, +} + +/// Apps and Features / ARP entry information detected during analysis. +#[napi(object)] +pub struct AppsAndFeaturesEntryAnalysis { + /// Display name registered in Apps and Features, if present. + pub display_name: Option, + /// Publisher registered in Apps and Features, if present. + pub publisher: Option, + /// Display version registered in Apps and Features, if present. + pub display_version: Option, + /// Product code registered in Apps and Features, if present. + pub product_code: Option, + /// Upgrade code registered in Apps and Features, if present. + pub upgrade_code: Option, + /// Installer type registered in Apps and Features, if present. + pub installer_type: Option, +} + +/// Nested installer file information. +#[napi(object)] +pub struct NestedInstallerFileAnalysis { + /// Relative path to the nested installer file. + pub relative_file_path: String, +} diff --git a/src/anthelion/update_helpers.rs b/src/anthelion/update_helpers.rs new file mode 100644 index 000000000..20a473490 --- /dev/null +++ b/src/anthelion/update_helpers.rs @@ -0,0 +1,89 @@ +use winget_types::{PackageVersion, installer::NestedInstallerFiles}; + +use crate::{ + analysis::installers::Zip, + github::{ + GITHUB_HOST, GitHubError, + client::{GitHub, GitHubValues}, + }, + manifests::Url, + traits::path::{LowercaseExtension, NormalizePath}, +}; + +pub fn resolve_replace_version<'a>( + replace: Option<&'a PackageVersion>, + versions: &'a std::collections::BTreeSet, + latest_version: &'a PackageVersion, + package_version: &PackageVersion, +) -> std::result::Result, String> { + let replace_version = replace + .map(|version| { + if version.is_latest() { + latest_version + } else { + version + } + }) + .filter(|&version| version.as_str() != package_version.as_str()); + + if let Some(version) = replace_version + && !versions.contains(version) + { + if let Some(closest) = version.closest(versions) { + return Err(format!( + "Replacement version {version} does not exist. The closest version is {closest}" + )); + } + return Err(format!("Replacement version {version} does not exist")); + } + + Ok(replace_version) +} + +pub async fn fetch_github_values( + github: &GitHub, + urls: &[Url], +) -> std::result::Result, GitHubError> { + if let Some(url) = urls.iter().find(|url| url.host_str() == Some(GITHUB_HOST)) { + github + .get_all_values_from_url(url.clone().into_inner()) + .await + .transpose() + } else { + Ok(None) + } +} + +pub fn fix_relative_paths( + nested_installer_files: std::collections::BTreeSet, + zip: Option<&Zip>, +) -> std::collections::BTreeSet { + let Some(zip) = zip else { + return nested_installer_files; + }; + + nested_installer_files + .into_iter() + .filter_map(|nested_installer_files| { + if zip + .possible_installer_files + .contains(&nested_installer_files.relative_file_path.normalize()) + { + Some(nested_installer_files) + } else { + zip.possible_installer_files + .iter() + .min_by_key(|file_path| { + strsim::levenshtein( + file_path.as_str(), + nested_installer_files.relative_file_path.as_str(), + ) + }) + .map(|path| NestedInstallerFiles { + relative_file_path: path.lowercase_extension(), + ..nested_installer_files + }) + } + }) + .collect() +} diff --git a/src/anthelion/update_version.rs b/src/anthelion/update_version.rs new file mode 100644 index 000000000..1b17256b7 --- /dev/null +++ b/src/anthelion/update_version.rs @@ -0,0 +1,465 @@ +use std::{mem, num::NonZeroUsize, str::FromStr}; + +use futures_util::TryFutureExt; +use itertools::Itertools; +use napi::bindgen_prelude::*; +use napi_derive::napi; +use tokio::try_join; +use winget_types::{ + PackageIdentifier, PackageVersion, + installer::{InstallerType, MinimumOSVersion}, + locale::ReleaseNotes, + url::ReleaseNotesUrl, +}; + +use super::{ + github_configuration, + token::resolve_github_token, + types::{ManifestChange, UpdateVersionOptions, UpdateVersionResult}, + update_helpers::{fetch_github_values, fix_relative_paths, resolve_replace_version}, +}; +use crate::{ + download::Downloader, + github::{ + client::GitHub, + utils::{PackagePath, pull_request::pr_changes}, + }, + manifests::Url, + match_installers::match_installers, + traits::LocaleExt, +}; + +enum VersionSelector { + Explicit(Box), + ProductVersion, + FileVersion, + DisplayVersion, +} + +fn parse_version_selector(version: &str) -> napi::Result { + let trimmed = version.trim(); + if trimmed.is_empty() { + return Err(Error::new(Status::InvalidArg, "A version is required")); + } + + Ok(match trimmed { + "productVersion" => VersionSelector::ProductVersion, + "fileVersion" => VersionSelector::FileVersion, + "displayVersion" => VersionSelector::DisplayVersion, + value => VersionSelector::Explicit(Box::new(value.parse().map_err(|e| { + Error::new(Status::InvalidArg, format!("Invalid package version: {e}")) + })?)), + }) +} + +/// Update an existing package version in winget-pkgs. +/// +/// # Errors +/// +/// Returns `InvalidArg` when provided arguments are invalid (identifier, URLs, versions, or selectors). +/// Returns `GenericFailure` when downloading installers, analyzing content, loading manifests, +/// or creating the pull request fails. +#[napi] +pub async fn update_version(options: UpdateVersionOptions) -> napi::Result { + let package_identifier: PackageIdentifier = + options.package_identifier.parse().map_err(|e| { + Error::new( + Status::InvalidArg, + format!("Invalid package identifier: {e}"), + ) + })?; + + let version_selector = parse_version_selector(&options.version)?; + + let urls: Vec = options + .urls + .iter() + .map(|u| Url::from_str(u)) + .collect::, _>>() + .map_err(|e| Error::new(Status::InvalidArg, format!("Invalid URL: {e}")))?; + + if urls.is_empty() { + return Err(Error::new( + Status::InvalidArg, + "At least one URL is required", + )); + } + + let release_notes_url: Option = options + .release_notes_url + .map(|u| u.parse()) + .transpose() + .map_err(|e| { + Error::new( + Status::InvalidArg, + format!("Invalid release notes URL: {e}"), + ) + })?; + + let release_notes: Option = options + .release_notes + .map(ReleaseNotes::new) + .transpose() + .map_err(|e| Error::new(Status::InvalidArg, format!("Invalid release notes: {e}")))?; + + let dry_run = options + .dry_run + .or_else(github_configuration::dry_run) + .unwrap_or(false); + + let replace: Option = options + .replace + .map(|v| v.parse()) + .transpose() + .map_err(|e| Error::new(Status::InvalidArg, format!("Invalid replace version: {e}")))?; + + let force_font = options.font.unwrap_or_default(); + + let token = resolve_github_token(options.token.as_deref())?; + + let github = GitHub::new(&token).map_err(|e| { + Error::new( + Status::GenericFailure, + format!("Failed to create GitHub client: {e}"), + ) + })?; + + let (versions, font) = github + .get_versions(&package_identifier, force_font.then_some(true)) + .await + .map_err(|e| { + Error::new( + Status::GenericFailure, + format!("Failed to get versions: {e}"), + ) + })?; + + let latest_version = versions + .last() + .ok_or_else(|| Error::new(Status::GenericFailure, "No versions found for package"))?; + + let downloader = Downloader::new_with_concurrent_and_progress( + NonZeroUsize::new(num_cpus::get()).unwrap_or(NonZeroUsize::MIN), + false, + ) + .map_err(|e| { + Error::new( + Status::GenericFailure, + format!("Failed to create downloader: {e}"), + ) + })?; + + let (mut manifests, mut github_values, mut files) = try_join!( + github + .get_manifests(&package_identifier, latest_version, font) + .map_err(|e| Error::new( + Status::GenericFailure, + format!("Failed to get manifests: {e}") + )), + fetch_github_values(&github, &urls).map_err(|e| Error::new( + Status::GenericFailure, + format!("Failed to get GitHub values: {e}") + )), + downloader + .download(urls.iter().cloned()) + .map_err(|e| Error::new( + Status::GenericFailure, + format!("Failed to download installers: {e}") + )), + )?; + + let mut download_results = { + use std::collections::HashMap; + + use winget_types::{installer::Architecture, url::DecodedUrl}; + + use crate::analysis::Analyzer; + + let mut results = HashMap::new(); + for file in &mut files { + let mut file_analyzer = + Analyzer::new(&mut file.file, &file.file_name).map_err(|e| { + Error::new( + Status::GenericFailure, + format!("Failed to analyze file: {e}"), + ) + })?; + if let (Some(zip), Some(installer_matches)) = + (&mut file_analyzer.zip, options.installer_matches.as_ref()) + && !installer_matches.is_empty() + { + let matched_analysis = zip + .analyze_matches_with_metadata(installer_matches) + .map_err(|e| { + Error::new( + Status::GenericFailure, + format!("Failed to analyze matching ZIP installers: {e}"), + ) + })?; + + if let Some(file_version) = matched_analysis + .iter() + .filter_map(|analysis| analysis.file_version.as_deref()) + .map(str::trim) + .find(|value| !value.is_empty()) + { + file_analyzer.file_version = Some(file_version.to_owned()); + } + + if let Some(product_version) = matched_analysis + .iter() + .filter_map(|analysis| analysis.product_version.as_deref()) + .map(str::trim) + .find(|value| !value.is_empty()) + { + file_analyzer.product_version = Some(product_version.to_owned()); + } + + file_analyzer.installers = matched_analysis + .into_iter() + .map(|analysis| analysis.installer) + .collect(); + } + let architecture = file + .url + .override_architecture() + .or_else(|| Architecture::from_url(file.url.as_str())); + for installer in &mut file_analyzer.installers { + if let Some(architecture) = architecture { + installer.architecture = architecture; + } + installer.url = file.url.inner().clone(); + installer.sha_256 = file.sha_256.clone(); + installer.release_date = file.last_modified; + } + file_analyzer.file_name = mem::take(&mut file.file_name); + let url_key: DecodedUrl = mem::take(file.url.inner_mut()); + results.insert(url_key, file_analyzer); + } + results + }; + + let installer_results = download_results + .iter_mut() + .flat_map(|(_url, analyzer)| mem::take(&mut analyzer.installers)) + .collect::>(); + + let product_version = download_results + .values() + .filter_map(|analyzer| analyzer.product_version.as_deref()) + .map(str::trim) + .find(|value| !value.is_empty()); + let file_version = download_results + .values() + .filter_map(|analyzer| analyzer.file_version.as_deref()) + .map(str::trim) + .find(|value| !value.is_empty()); + let display_version = installer_results + .iter() + .flat_map(|installer| installer.apps_and_features_entries.iter()) + .filter_map(|entry| { + entry + .display_version() + .map(std::string::ToString::to_string) + }) + .find(|value| !value.trim().is_empty()); + + let package_version: PackageVersion = match version_selector { + VersionSelector::Explicit(package_version) => *package_version, + VersionSelector::ProductVersion => product_version + .ok_or_else(|| { + Error::new( + Status::InvalidArg, + "`version` was set to `productVersion`, but no ProductVersion was found during analysis", + ) + })? + .parse() + .map_err(|e| Error::new(Status::InvalidArg, format!("Invalid ProductVersion: {e}")))?, + VersionSelector::FileVersion => file_version + .ok_or_else(|| { + Error::new( + Status::InvalidArg, + "`version` was set to `fileVersion`, but no FileVersion was found during analysis", + ) + })? + .parse() + .map_err(|e| Error::new(Status::InvalidArg, format!("Invalid FileVersion: {e}")))?, + VersionSelector::DisplayVersion => display_version + .ok_or_else(|| { + Error::new( + Status::InvalidArg, + "`version` was set to `displayVersion`, but no DisplayVersion was found during analysis", + ) + })? + .parse() + .map_err(|e| Error::new(Status::InvalidArg, format!("Invalid DisplayVersion: {e}")))?, + }; + + let replace_version = resolve_replace_version( + replace.as_ref(), + &versions, + latest_version, + &package_version, + ) + .map_err(|e| Error::new(Status::InvalidArg, e))?; + + let previous_installers = mem::take(&mut manifests.installer.installers) + .into_iter() + .map(|mut installer| { + if manifests.installer.r#type.is_some() { + installer.r#type = manifests.installer.r#type; + } + if manifests.installer.nested_installer_type.is_some() { + installer.nested_installer_type = manifests.installer.nested_installer_type; + } + if manifests.installer.scope.is_some() { + installer.scope = manifests.installer.scope; + } + installer + }) + .collect::>(); + + let duplicate_urls = previous_installers + .iter() + .map(|installer| installer.url.clone()) + .duplicates() + .collect::>(); + + manifests.default_locale.package_version = package_version.clone(); + let matched_installers = match_installers(&previous_installers, &installer_results); + let mut installers = matched_installers + .into_iter() + .map(|(previous_installer, new_installer)| { + let analyzer = &download_results[&new_installer.url]; + let installer_type = match previous_installer.r#type { + Some(InstallerType::Portable) => previous_installer.r#type, + _ => match new_installer.r#type { + Some(InstallerType::Portable) => previous_installer.r#type, + _ => new_installer.r#type, + }, + }; + + let previous_nested_files = previous_installer.nested_installer_files.clone(); + let previous_url = previous_installer.url.clone(); + let previous_architecture = previous_installer.architecture; + + let mut installer = new_installer.clone().merge_with(previous_installer); + installer.r#type = installer_type; + installer.url.clone_from(&new_installer.url); + + let nested_files_to_fix = [ + &previous_nested_files, + &manifests.installer.nested_installer_files, + &installer.nested_installer_files, + ] + .into_iter() + .find(|files| !files.is_empty()) + .cloned(); + + if let Some(nested_files) = nested_files_to_fix { + installer.nested_installer_files = + fix_relative_paths(nested_files, analyzer.zip.as_ref()); + } + + if duplicate_urls.contains(&previous_url) { + installer.architecture = previous_architecture; + } + + for entry in &mut installer.apps_and_features_entries { + entry.deduplicate(&manifests.default_locale); + } + installer + }) + .collect::>(); + + if installers + .iter() + .flat_map(|installer| &installer.locale) + .all_equal() + { + for installer in &mut installers { + installer.locale = None; + } + } + + manifests.installer.locale = None; + + manifests.installer.package_version = package_version.clone(); + manifests.installer.minimum_os_version = manifests + .installer + .minimum_os_version + .filter(|minimum_os_version| *minimum_os_version != MinimumOSVersion::new(10, 0, 0, 0)); + manifests.installer.installers = installers; + manifests.installer.optimize(); + + manifests.default_locale.update( + &package_version, + &mut github_values, + release_notes_url.as_ref(), + ); + + if let Some(release_notes) = release_notes { + manifests.default_locale.release_notes = Some(release_notes); + } + + manifests.locales.iter_mut().for_each(|locale| { + locale.update(&package_version, &mut github_values, None); + }); + + manifests.version.update(&package_version); + + let package_path = PackagePath::new(&package_identifier, Some(&package_version), None, font); + let changes = pr_changes() + .package_identifier(&package_identifier) + .manifests(&manifests) + .package_path(&package_path) + .create() + .map_err(|e| { + Error::new( + Status::GenericFailure, + format!("Failed to create PR changes: {e}"), + ) + })?; + + if dry_run { + return Ok(UpdateVersionResult { + pull_request_url: None, + changes: changes + .iter() + .map(|(path, content)| ManifestChange { + path: path.clone(), + content: content.clone(), + }) + .collect(), + package_identifier: package_identifier.to_string(), + version: package_version.to_string(), + }); + } + + let pull_request_url = github + .add_version() + .identifier(&package_identifier) + .version(&package_version) + .versions(&versions) + .changes(changes.clone()) + .maybe_replace_version(replace_version) + .issue_resolves(&[]) + .send() + .await + .map_err(|e| { + Error::new( + Status::GenericFailure, + format!("Failed to create pull request: {e}"), + ) + })?; + + Ok(UpdateVersionResult { + pull_request_url: Some(pull_request_url.url().to_string()), + changes: changes + .into_iter() + .map(|(path, content)| ManifestChange { path, content }) + .collect(), + package_identifier: package_identifier.to_string(), + version: package_version.to_string(), + }) +} diff --git a/src/commands/analyze.rs b/src/commands/analyze.rs index 5719156f2..9105c15d7 100644 --- a/src/commands/analyze.rs +++ b/src/commands/analyze.rs @@ -8,10 +8,14 @@ use anstream::stdout; use camino::{Utf8Path, Utf8PathBuf}; use clap::Parser; use color_eyre::{Result, eyre::ensure}; +use serde::Serialize; use sha2::{Digest, Sha256, digest::Output}; -use winget_types::Sha256String; +use winget_types::{Sha256String, installer::Installer}; -use crate::{analysis::Analyzer, manifests::print_manifest}; +use crate::{ + analysis::{Analyzer, PeInfo}, + manifests::{print_manifest, to_yaml_string}, +}; /// Analyzes a file and outputs information about it #[derive(Parser)] @@ -48,17 +52,31 @@ impl Analyze { .file_path .file_name() .unwrap_or_else(|| self.file_path.as_str()); - let mut installers = Analyzer::new(&mut file, file_name)?.installers; - if self.hash { - file.seek(SeekFrom::Start(0))?; - let sha_256 = Sha256String::from_digest(&sha256_digest(file)?); - for installer in &mut installers { + let sha_256 = self + .hash + .then(|| { + let sha_256 = Sha256String::from_digest(&sha256_digest(&mut file)?); + file.seek(SeekFrom::Start(0))?; + Ok::<_, io::Error>(sha_256) + }) + .transpose()?; + + let mut analyzer = Analyzer::new(&mut file, file_name)?; + if let Some(sha_256) = sha_256 { + for installer in &mut analyzer.installers { installer.sha_256 = sha_256.clone(); } } - let yaml = match installers.as_slice() { - [installer] => serde_yaml::to_string(installer)?, - installers => serde_yaml::to_string(installers)?, + let yaml = match (analyzer.pe_info.as_ref(), analyzer.installers.as_slice()) { + (Some(pe_info), [installer]) => { + to_yaml_string(&AnalyzeSingleOutput { pe_info, installer })? + } + (Some(pe_info), installers) => to_yaml_string(&AnalyzeMultiOutput { + pe_info, + installers, + })?, + (None, [installer]) => to_yaml_string(installer)?, + (None, installers) => to_yaml_string(&installers)?, }; let mut lock = stdout().lock(); print_manifest(&mut lock, &yaml); @@ -66,6 +84,22 @@ impl Analyze { } } +#[derive(Serialize)] +#[serde(rename_all = "PascalCase")] +struct AnalyzeSingleOutput<'a> { + #[serde(rename = "PEInfo")] + pe_info: &'a PeInfo, + installer: &'a Installer, +} + +#[derive(Serialize)] +#[serde(rename_all = "PascalCase")] +struct AnalyzeMultiOutput<'a> { + #[serde(rename = "PEInfo")] + pe_info: &'a PeInfo, + installers: &'a [Installer], +} + fn sha256_digest(mut reader: R) -> io::Result> { let mut digest = Sha256::new(); let mut buffer = [0; 1 << 13]; diff --git a/src/commands/autoupdate.rs b/src/commands/autoupdate.rs index eb803725e..e0e4bff69 100644 --- a/src/commands/autoupdate.rs +++ b/src/commands/autoupdate.rs @@ -597,7 +597,7 @@ impl AutoUpdate { let latest_version = if let Some(version) = latest_version { version } else { - let versions = github.get_versions(&package_identifier).await?; + let (versions, _) = github.get_versions(&package_identifier, None).await?; versions.last().cloned().unwrap_or_else(|| unreachable!()) }; @@ -773,6 +773,7 @@ impl AutoUpdate { dry_run: self.dry_run, replace: self.replace.clone(), skip_pr_check: self.skip_pr_check, + font: false, token: Some(token.clone()), } .run() @@ -896,7 +897,7 @@ async fn latest_version_from_manifest( github: &GitHub, package_identifier: &PackageIdentifier, ) -> Result { - let versions = github.get_versions(package_identifier).await?; + let (versions, _) = github.get_versions(package_identifier, None).await?; Ok(versions.last().cloned().unwrap_or_else(|| unreachable!())) } @@ -906,7 +907,7 @@ async fn sources_from_manifest_for_version( latest_version: &PackageVersion, ) -> Result> { let manifests = github - .get_manifests(package_identifier, latest_version) + .get_manifests(package_identifier, latest_version, false) .await?; let mut seen = HashSet::new(); diff --git a/src/commands/compare_installers.rs b/src/commands/compare_installers.rs index 84c6b4d33..960580c38 100644 --- a/src/commands/compare_installers.rs +++ b/src/commands/compare_installers.rs @@ -71,8 +71,8 @@ impl CompareInstallers { let progress = ProgressBar::new_spinner().with_message("Fetching package versions..."); progress.enable_steady_tick(SPINNER_TICK_RATE); - let versions = match github.get_versions(&package_identifier).await { - Ok(v) => v, + let versions = match github.get_versions(&package_identifier, None).await { + Ok((v, _)) => v, Err(err) => { progress.finish_and_clear(); println!( @@ -151,7 +151,7 @@ impl CompareInstallers { identifier: &PackageIdentifier, version: &PackageVersion, ) -> Result> { - let mut manifests = github.get_manifests(identifier, version).await?; + let mut manifests = github.get_manifests(identifier, version, false).await?; let original_yaml = serde_yaml::to_string(&manifests.installer)?; let urls: Vec<_> = manifests .installer @@ -190,7 +190,7 @@ impl CompareInstallers { }) .collect::>(); - let matched_installers = match_installers(previous_installers, &installer_results); + let matched_installers = match_installers(&previous_installers, &installer_results); let installers = matched_installers .into_iter() .map(|(previous_installer, new_installer)| { diff --git a/src/commands/complete.rs b/src/commands/complete.rs index bb9c0d1be..549e5490b 100644 --- a/src/commands/complete.rs +++ b/src/commands/complete.rs @@ -1,9 +1,7 @@ -use clap::{CommandFactory, Parser}; -use clap_complete::{Shell, generate}; +use clap::Parser; +use clap_complete::Shell; use color_eyre::{Result, Section, eyre::eyre}; -use crate::Cli; - /// Outputs an autocompletion script for the given shell. Example usage: /// /// Bash: echo "source <(komac complete bash)" >> ~/.bashrc @@ -22,7 +20,15 @@ pub struct Complete { } impl Complete { - pub fn run(self) -> Result<()> { + /// Generate shell completions. + /// + /// When invoked from the binary, the caller should provide a closure that + /// generates completions for the given shell (since the `Cli` type is only + /// available in `main.rs`). + pub fn run_with(self, generate_fn: F) -> Result<()> + where + F: FnOnce(Shell), + { let Some(shell) = self.shell.or_else(Shell::from_env) else { return Err( eyre!("Unable to determine the current shell from the environment") @@ -30,10 +36,14 @@ impl Complete { ); }; - let mut command = Cli::command(); - let command_name = command.get_name().to_owned(); - generate(shell, &mut command, command_name, &mut anstream::stdout()); - + generate_fn(shell); Ok(()) } + + #[allow(dead_code)] + pub fn run() -> Result<()> { + Err(eyre!( + "Completion generation is not available in library mode" + )) + } } diff --git a/src/commands/list_versions.rs b/src/commands/list_versions.rs index 7e201c9d9..b31b9f25e 100644 --- a/src/commands/list_versions.rs +++ b/src/commands/list_versions.rs @@ -22,6 +22,10 @@ pub struct ListVersions { #[arg(long)] count: bool, + /// Look for the package under fonts instead of probing manifests first + #[arg(long)] + font: bool, + /// GitHub personal access token with the `public_repo` scope #[arg(short, long, env = "GITHUB_TOKEN", hide_env_values = true)] token: Option, @@ -48,7 +52,9 @@ impl ListVersions { let token_manager = TokenManager::handle(self.token).await?; let github = GitHub::new(token_manager)?; - let versions = github.get_versions(&self.package_identifier).await?; + let (versions, _) = github + .get_versions(&self.package_identifier, self.font.then_some(true)) + .await?; let mut stdout_lock = anstream::stdout().lock(); match self.output_type { diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 44141569e..2c9d32300 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -4,6 +4,7 @@ pub mod cleanup; pub mod compare_installers; pub mod complete; pub mod list_versions; +pub mod new_locale; pub mod new_version; pub mod remove_dead_versions; pub mod remove_version; diff --git a/src/commands/new_locale.rs b/src/commands/new_locale.rs new file mode 100644 index 000000000..1454b6437 --- /dev/null +++ b/src/commands/new_locale.rs @@ -0,0 +1,371 @@ +use std::{collections::BTreeSet, mem, num::NonZeroU32}; + +use anstream::println; +use camino::Utf8PathBuf; +use clap::Parser; +use color_eyre::eyre::{Result, bail}; +use indicatif::ProgressBar; +use owo_colors::OwoColorize; +use secrecy::SecretString; +use winget_types::{ + LanguageTag, ManifestType, ManifestVersion, PackageIdentifier, PackageVersion, + locale::{ + Author, Copyright, Description, License, LocaleManifest, PackageName, Publisher, + ShortDescription, Tag, + }, + url::{ + CopyrightUrl, DecodedUrl, LicenseUrl, PackageUrl, PublisherSupportUrl, PublisherUrl, + ReleaseNotesUrl, + }, +}; + +use crate::{ + commands::utils::{SPINNER_TICK_RATE, SubmitOption, write_changes_to_dir}, + github::{WINGET_PKGS_FULL_NAME, client::GitHub, utils::PackagePath}, + manifests::{Manifests, build_manifest_string}, + prompts::{ + list::list_prompt_with_help, + text::{optional_prompt_with_help, required_prompt}, + }, + token::TokenManager, +}; + +/// Create a new locale manifest for an existing package version +#[allow(clippy::struct_excessive_bools, reason = "CLI flags")] +#[derive(Parser)] +#[clap(visible_alias = "add-locale")] +pub struct NewLocale { + /// The package's unique identifier + #[arg()] + package_identifier: Option, + + /// The package locale to create + #[arg()] + package_locale: Option, + + /// The package's version + #[arg(short = 'v', long = "version")] + package_version: Option, + + #[arg(long)] + publisher: Option, + + #[arg(long, value_hint = clap::ValueHint::Url)] + publisher_url: Option, + + #[arg(long, value_hint = clap::ValueHint::Url)] + publisher_support_url: Option, + + #[arg(long)] + author: Option, + + #[arg(long)] + package_name: Option, + + #[arg(long, value_hint = clap::ValueHint::Url)] + package_url: Option, + + #[arg(long)] + license: Option, + + #[arg(long, value_hint = clap::ValueHint::Url)] + license_url: Option, + + #[arg(long)] + copyright: Option, + + #[arg(long, value_hint = clap::ValueHint::Url)] + copyright_url: Option, + + #[arg(long)] + short_description: Option, + + #[arg(long)] + description: Option, + + #[arg(long = "tag", value_name = "TAG")] + tags: Vec, + + #[arg(long, value_hint = clap::ValueHint::Url)] + release_notes_url: Option, + + /// List of issues that adding this locale would resolve + #[arg(long)] + resolves: Vec, + + /// Automatically submit a pull request + #[arg(short, long)] + submit: bool, + + /// Name of external tool that invoked Komac + #[arg(long, env = "KOMAC_CREATED_WITH")] + created_with: Option, + + /// URL to external tool that invoked Komac + #[arg(long, env = "KOMAC_CREATED_WITH_URL", value_hint = clap::ValueHint::Url)] + created_with_url: Option, + + /// Directory to output the manifest to + #[arg(short, long, env = "OUTPUT_DIRECTORY", value_hint = clap::ValueHint::DirPath)] + output: Option, + + /// Open pull request link automatically + #[arg(long, env = "OPEN_PR")] + open_pr: bool, + + /// Run without submitting + #[arg(long, env = "DRY_RUN")] + dry_run: bool, + + /// GitHub personal access token with the `public_repo` scope + #[arg(short, long, env = "GITHUB_TOKEN", hide_env_values = true)] + token: Option, +} + +impl NewLocale { + pub async fn run(mut self) -> Result<()> { + let token_manager = TokenManager::handle(self.token.take()).await?; + let github = GitHub::new(&token_manager)?; + + let package_identifier = required_prompt(self.package_identifier.take(), None::<&str>)?; + let (versions, font) = github.get_versions(&package_identifier, None).await?; + let latest_version = versions.last().unwrap_or_else(|| unreachable!()); + + println!("Latest version of {package_identifier}: {latest_version}"); + + let package_version = + required_prompt(self.package_version.take(), Some(latest_version.as_str()))?; + if !versions.contains(&package_version) { + if let Some(closest) = package_version.closest(&versions) { + bail!( + "{} version {} does not exist in {WINGET_PKGS_FULL_NAME}. The closest version is {closest}", + package_identifier, + package_version, + ); + } + + bail!( + "{} version {} does not exist in {WINGET_PKGS_FULL_NAME}", + package_identifier, + package_version, + ); + } + + let package_locale = required_prompt(self.package_locale.take(), None::<&str>)?; + + let mut manifests = github + .get_manifests(&package_identifier, &package_version, font) + .await?; + + validate_new_locale(&manifests, &package_locale)?; + + let locale_manifest = self.create_locale_manifest( + &manifests, + package_identifier.clone(), + package_version.clone(), + package_locale, + )?; + manifests.locales.push(locale_manifest); + + let package_path = + PackagePath::new(&package_identifier, Some(&package_version), None, font); + let mut changes = new_locale_changes( + &package_identifier, + &manifests, + &package_path, + self.created_with.as_deref(), + )?; + + let submit_option = SubmitOption::prompt( + &mut changes, + &package_identifier, + &package_version, + self.submit, + self.dry_run, + )?; + + if let Some(output) = self + .output + .as_ref() + .map(|out| out.join(package_path.as_str())) + { + write_changes_to_dir(&changes, output.as_path()).await?; + println!( + "{} written locale manifest to {output}", + "Successfully".green() + ); + } + + if submit_option.is_exit() { + return Ok(()); + } + + let pr_progress = ProgressBar::new_spinner().with_message(format!( + "Creating a pull request for {package_identifier} {package_version}" + )); + pr_progress.enable_steady_tick(SPINNER_TICK_RATE); + + let pull_request = github + .add_version() + .identifier(&package_identifier) + .version(&package_version) + .versions(&versions) + .changes(changes) + .issue_resolves(&self.resolves) + .maybe_created_with(self.created_with.as_deref()) + .maybe_created_with_url(self.created_with_url.as_ref()) + .send() + .await?; + + pr_progress.finish_and_clear(); + + pull_request.print_success(); + + if self.open_pr { + open::that(pull_request.url().as_str())?; + } + + Ok(()) + } + + fn create_locale_manifest( + &mut self, + manifests: &Manifests, + package_identifier: PackageIdentifier, + package_version: PackageVersion, + package_locale: LanguageTag, + ) -> Result { + let default_locale = &manifests.default_locale; + + Ok(LocaleManifest { + package_identifier, + package_version, + package_locale, + publisher: optional_prompt_with_help( + self.publisher.take(), + default_locale_help(Some(&default_locale.publisher)), + )?, + publisher_url: optional_prompt_with_help( + self.publisher_url.take(), + default_locale_help(default_locale.publisher_url.as_ref()), + )?, + publisher_support_url: optional_prompt_with_help( + self.publisher_support_url.take(), + default_locale_help(default_locale.publisher_support_url.as_ref()), + )?, + privacy_url: None, + author: optional_prompt_with_help( + self.author.take(), + default_locale_help(default_locale.author.as_ref()), + )?, + package_name: optional_prompt_with_help( + self.package_name.take(), + default_locale_help(Some(&default_locale.package_name)), + )?, + package_url: optional_prompt_with_help( + self.package_url.take(), + default_locale_help(default_locale.package_url.as_ref()), + )?, + license: optional_prompt_with_help( + self.license.take(), + default_locale_help(Some(&default_locale.license)), + )?, + license_url: optional_prompt_with_help( + self.license_url.take(), + default_locale_help(default_locale.license_url.as_ref()), + )?, + copyright: optional_prompt_with_help( + self.copyright.take(), + default_locale_help(default_locale.copyright.as_ref()), + )?, + copyright_url: optional_prompt_with_help( + self.copyright_url.take(), + default_locale_help(default_locale.copyright_url.as_ref()), + )?, + short_description: optional_prompt_with_help( + self.short_description.take(), + default_locale_help(Some(&default_locale.short_description)), + )?, + description: optional_prompt_with_help( + self.description.take(), + default_locale_help(default_locale.description.as_ref()), + )?, + tags: if self.tags.is_empty() { + list_prompt_with_help::(default_locale_tags_help(&default_locale.tags))? + } else { + mem::take(&mut self.tags) + .into_iter() + .collect::>() + }, + agreements: BTreeSet::new(), + release_notes: None, + release_notes_url: optional_prompt_with_help( + self.release_notes_url.take(), + default_locale_help(default_locale.release_notes_url.as_ref()), + )?, + purchase_url: None, + installation_notes: None, + documentations: BTreeSet::new(), + icons: BTreeSet::new(), + manifest_type: ManifestType::Locale, + manifest_version: ManifestVersion::default(), + }) + } +} + +fn default_locale_help(value: Option) -> Option +where + T: AsRef, +{ + value.map(|value| format!("Default locale: {}", value.as_ref())) +} + +fn default_locale_tags_help(tags: &BTreeSet) -> String { + if tags.is_empty() { + String::from("Default locale: (none)") + } else { + format!( + "Default locale: {}", + tags.iter() + .map(ToString::to_string) + .collect::>() + .join(", ") + ) + } +} + +fn validate_new_locale(manifests: &Manifests, package_locale: &LanguageTag) -> Result<()> { + if package_locale == &manifests.version.default_locale { + bail!("{package_locale} is already the default locale for {manifests}"); + } + + if manifests + .locales + .iter() + .any(|locale| &locale.package_locale == package_locale) + { + bail!("{package_locale} already exists for {manifests}"); + } + + Ok(()) +} + +fn new_locale_changes( + package_identifier: &PackageIdentifier, + manifests: &Manifests, + package_path: &PackagePath, + created_with: Option<&str>, +) -> Result> { + let locale_manifest = manifests + .locales + .last() + .unwrap_or_else(|| unreachable!("new locale should have been pushed")); + + Ok(vec![( + format!( + "{package_path}/{package_identifier}.locale.{}.yaml", + locale_manifest.package_locale + ), + build_manifest_string(locale_manifest, created_with)?, + )]) +} diff --git a/src/commands/new_version.rs b/src/commands/new_version.rs index dc8e86204..28ba2e24d 100644 --- a/src/commands/new_version.rs +++ b/src/commands/new_version.rs @@ -34,7 +34,8 @@ use winget_types::{ use crate::{ commands::utils::{ - SPINNER_TICK_RATE, SubmitOption, prompt_existing_pull_request, write_changes_to_dir, + SPINNER_TICK_RATE, SubmitOption, check_package_type, should_abort_for_existing_pr, + write_changes_to_dir, }, download::{DownloadedFile, Downloader}, download_file::process_files, @@ -154,6 +155,10 @@ pub struct NewVersion { #[arg(long, env)] skip_pr_check: bool, + /// Look for the package under fonts instead of probing manifests first + #[arg(long)] + font: bool, + /// GitHub personal access token with the `public_repo` scope #[arg(short, long, env = "GITHUB_TOKEN", hide_env_values = true)] token: Option, @@ -175,7 +180,11 @@ impl NewVersion { let package_identifier = required_prompt(self.package_identifier, None::<&str>)?; - let versions = github.get_versions(&package_identifier).await.ok(); + let (versions, font) = github + .get_versions(&package_identifier, self.font.then_some(true)) + .await + .ok() + .map_or((None, false), |(versions, font)| (Some(versions), font)); let latest_version = versions.as_ref().and_then(BTreeSet::last); @@ -184,17 +193,21 @@ impl NewVersion { } let manifests = - latest_version.map(|version| github.get_manifests(&package_identifier, version)); + latest_version.map(|version| github.get_manifests(&package_identifier, version, font)); let package_version = required_prompt(self.package_version, None::<&str>)?; - if !self.skip_pr_check - && !self.dry_run - && let Some(pull_request) = github - .get_existing_pull_request(&package_identifier, &package_version) - .await? - && !prompt_existing_pull_request(&package_identifier, &package_version, &pull_request)? - { + let existing_pr = github + .get_existing_pull_request(&package_identifier, &package_version, false) + .await?; + + if should_abort_for_existing_pr( + &package_identifier, + &package_version, + existing_pr, + self.skip_pr_check, + self.dry_run, + )? { return Ok(()); } @@ -301,30 +314,37 @@ impl NewVersion { let mut installer_manifest = InstallerManifest { package_identifier: package_identifier.clone(), package_version: package_version.clone(), - install_modes: if installers + installers, + manifest_type: ManifestType::Installer, + ..InstallerManifest::default() + }; + + let is_font = check_package_type(&installer_manifest)?; + + if !is_font { + installer_manifest.install_modes = if installer_manifest + .installers .iter() .any(|installer| installer.r#type == Some(InstallerType::Inno)) { InstallModes::all() } else { check_prompt::()? - }, - success_codes: list_prompt::()?, - upgrade_behavior: Some(radio_prompt::()?), - commands: list_prompt::()?, - protocols: list_prompt::()?, - file_extensions: if installers + }; + installer_manifest.success_codes = list_prompt::()?; + installer_manifest.upgrade_behavior = Some(radio_prompt::()?); + installer_manifest.commands = list_prompt::()?; + installer_manifest.protocols = list_prompt::()?; + installer_manifest.file_extensions = if installer_manifest + .installers .iter() .all(|installer| installer.file_extensions.is_empty()) { list_prompt::()? } else { BTreeSet::new() - }, - installers, - manifest_type: ManifestType::Installer, - ..InstallerManifest::default() - }; + }; + } let mut github_values = match github_values.await? { Some(future) => Some(future?), @@ -456,7 +476,8 @@ impl NewVersion { version: version_manifest, }; - let package_path = PackagePath::new(&package_identifier, Some(&package_version), None); + let package_path = + PackagePath::new(&package_identifier, Some(&package_version), None, is_font); let changes = pr_changes() .package_identifier(&package_identifier) .manifests(&manifests) diff --git a/src/commands/remove_dead_versions.rs b/src/commands/remove_dead_versions.rs index fb57d2c0b..7245ad436 100644 --- a/src/commands/remove_dead_versions.rs +++ b/src/commands/remove_dead_versions.rs @@ -60,6 +60,10 @@ pub struct RemoveDeadVersions { #[arg(short = 'r', long = "reason")] deletion_reason: Option, + /// Look for the package under fonts instead of probing manifests first + #[arg(long)] + font: bool, + /// Number of versions to check concurrently #[arg(short, long, default_value_t = NonZeroUsize::new(num_cpus::get()).unwrap())] concurrent: NonZeroUsize, @@ -74,12 +78,12 @@ impl RemoveDeadVersions { let token_manager = TokenManager::handle(self.token).await?; let github = GitHub::new(token_manager)?; - let (fork, winget_pkgs, versions) = try_join!( + let (fork, winget_pkgs, (versions, font)) = try_join!( github .get_username() .and_then(|current_user| github.get_winget_pkgs().owner(current_user).send()), github.get_winget_pkgs().send(), - github.get_versions(&self.package_identifier) + github.get_versions(&self.package_identifier, self.font.then_some(true)) )?; let client = Client::builder() @@ -159,6 +163,7 @@ impl RemoveDeadVersions { .reason(&deletion_reason) .fork(&fork) .winget_pkgs(&winget_pkgs) + .font(font) .send() .await?; @@ -199,6 +204,7 @@ impl RemoveDeadVersions { package_identifier, &version, ManifestTypeWithLocale::Installer, + font, ) .await? .installers @@ -281,7 +287,7 @@ async fn confirm_removal( remove_all: bool, ) -> Result { if let Some(pull_request) = github - .get_existing_pull_request(identifier, version) + .get_existing_pull_request(identifier, version, false) .await? && pull_request.is_open() { diff --git a/src/commands/remove_version.rs b/src/commands/remove_version.rs index 0247efc36..699eeac76 100644 --- a/src/commands/remove_version.rs +++ b/src/commands/remove_version.rs @@ -14,6 +14,7 @@ use tokio::try_join; use winget_types::{PackageIdentifier, PackageVersion}; use crate::{ + commands::utils::should_abort_for_existing_pr, github::{WINGET_PKGS_FULL_NAME, client::GitHub}, prompts::{handle_inquire_error, text::confirm_prompt}, token::TokenManager, @@ -50,6 +51,10 @@ pub struct RemoveVersion { #[arg(long, env = "OPEN_PR")] open_pr: bool, + /// Look for the package under fonts instead of probing manifests first + #[arg(long)] + font: bool, + /// GitHub personal access token with the `public_repo` scope #[arg(short, long, env = "GITHUB_TOKEN", hide_env_values = true)] token: Option, @@ -71,12 +76,12 @@ impl RemoveVersion { let github = GitHub::new(&token_manager)?; - let (fork, winget_pkgs, versions) = try_join!( + let (fork, winget_pkgs, (versions, font)) = try_join!( github .get_username() .and_then(|current_user| github.get_winget_pkgs().owner(current_user).send()), github.get_winget_pkgs().send(), - github.get_versions(&self.package_identifier) + github.get_versions(&self.package_identifier, self.font.then_some(true)) )?; if !versions.contains(&self.package_version) { @@ -92,6 +97,22 @@ impl RemoveVersion { "Latest version of {}: {latest_version}", &self.package_identifier ); + + let existing_pr = github + .get_existing_pull_request(&self.package_identifier, &self.package_version, false) + .await? + .filter(|pull_request| pull_request.is_open()); + + if should_abort_for_existing_pr( + &self.package_identifier, + &self.package_version, + existing_pr, + false, + false, + )? { + return Ok(()); + } + let deletion_reason = match self.deletion_reason { Some(reason) => reason, None => Text::new(&format!( @@ -120,6 +141,7 @@ impl RemoveVersion { .reason(&deletion_reason) .fork(&fork) .winget_pkgs(&winget_pkgs) + .font(font) .issue_resolves(&self.resolves) .send() .await?; diff --git a/src/commands/show_version.rs b/src/commands/show_version.rs index 0781ca32f..fc4c9ceca 100644 --- a/src/commands/show_version.rs +++ b/src/commands/show_version.rs @@ -3,7 +3,11 @@ use color_eyre::Result; use secrecy::SecretString; use winget_types::{PackageIdentifier, PackageVersion}; -use crate::{github::client::GitHub, manifests::print_changes, token::TokenManager}; +use crate::{ + github::client::GitHub, + manifests::{print_changes, to_yaml_string}, + token::TokenManager, +}; /// Output the manifests for a given package and version #[expect(clippy::struct_excessive_bools)] @@ -33,6 +37,10 @@ pub struct ShowVersion { #[arg(long)] version_manifest: bool, + /// Look for the package under fonts instead of probing manifests first + #[arg(long)] + font: bool, + /// GitHub personal access token with the `public_repo` scope #[arg(short, long, env = "GITHUB_TOKEN", hide_env_values = true)] token: Option, @@ -44,7 +52,9 @@ impl ShowVersion { let github = GitHub::new(&token_manager)?; // Get a list of all versions for the given package - let mut versions = github.get_versions(&self.package_identifier).await?; + let (mut versions, font) = github + .get_versions(&self.package_identifier, self.font.then_some(true)) + .await?; // Get the manifests for the latest or specified version let manifests = github @@ -53,6 +63,7 @@ impl ShowVersion { &self .package_version .unwrap_or_else(|| versions.pop_last().unwrap_or_else(|| unreachable!())), + font, ) .await?; @@ -67,22 +78,20 @@ impl ShowVersion { ); let mut contents = Vec::new(); + if all || self.installer_manifest { - contents.push(serde_yaml::to_string(&manifests.installer)?); + contents.push(to_yaml_string(&manifests.installer)?); } if all || self.default_locale_manifest { - contents.push(serde_yaml::to_string(&manifests.default_locale)?); + contents.push(to_yaml_string(&manifests.default_locale)?); } if all || self.locale_manifests { - contents.extend( - manifests - .locales - .into_iter() - .flat_map(|locale_manifest| serde_yaml::to_string(&locale_manifest)), - ); + for locale_manifest in &manifests.locales { + contents.push(to_yaml_string(locale_manifest)?); + } } if all || self.version_manifest { - contents.push(serde_yaml::to_string(&manifests.version)?); + contents.push(to_yaml_string(&manifests.version)?); } print_changes(contents); diff --git a/src/commands/strategies/github_releases.rs b/src/commands/strategies/github_releases.rs index e91591bf1..3f31d8837 100644 --- a/src/commands/strategies/github_releases.rs +++ b/src/commands/strategies/github_releases.rs @@ -11,8 +11,8 @@ use tokio::sync::Mutex; use tracing::debug; use winget_types::{ PackageIdentifier, PackageVersion, - installer::VALID_FILE_EXTENSIONS, url::{DecodedUrl, ReleaseNotesUrl}, + utils::ValidFileExtensions, }; use super::UpdateVersionStrategyResult; @@ -360,7 +360,7 @@ pub async fn resolve( let file_name_lower = file_name.to_ascii_lowercase(); // TODO We should also check for valid portables, but this skips downloading - let is_valid = VALID_FILE_EXTENSIONS.contains(&extension.as_str()) + let is_valid = extension.as_str().parse::().is_ok() && !file_name_lower.contains("darwin") && !file_name_lower.contains("linux") && !file_name_lower.contains("mac") diff --git a/src/commands/strategies/html_page.rs b/src/commands/strategies/html_page.rs index cab4702de..13c377c46 100644 --- a/src/commands/strategies/html_page.rs +++ b/src/commands/strategies/html_page.rs @@ -5,7 +5,7 @@ use color_eyre::eyre::Result; use regex::Regex; use reqwest::Client; use thiserror::Error; -use winget_types::{PackageVersion, installer::VALID_FILE_EXTENSIONS, url::DecodedUrl}; +use winget_types::{PackageVersion, url::DecodedUrl, utils::ValidFileExtensions}; use super::UpdateVersionStrategyResult; use crate::manifests::Url; @@ -70,7 +70,7 @@ fn extract_installer_urls(html: &str, base_url: &DecodedUrl) -> Vec { .filter(|name| !name.is_empty()) .and_then(|name| Utf8Path::new(name).extension()) .map(str::to_ascii_lowercase) - .is_some_and(|ext| VALID_FILE_EXTENSIONS.contains(&ext.as_str())); + .is_some_and(|ext| ext.as_str().parse::().is_ok()); if has_valid_extension && seen.insert(resolved.to_string()) diff --git a/src/commands/strategies/sourceforge.rs b/src/commands/strategies/sourceforge.rs index 3a54d2a0c..15cc7a1a6 100644 --- a/src/commands/strategies/sourceforge.rs +++ b/src/commands/strategies/sourceforge.rs @@ -11,8 +11,8 @@ use thiserror::Error; use tokio::{sync::Mutex, time::sleep}; use winget_types::{ PackageIdentifier, PackageVersion, - installer::VALID_FILE_EXTENSIONS, url::{DecodedUrl, ReleaseNotesUrl}, + utils::ValidFileExtensions, }; use super::UpdateVersionStrategyResult; @@ -129,7 +129,7 @@ fn is_supported_sourceforge_url(url: &Url) -> bool { return false; }; - VALID_FILE_EXTENSIONS.contains(&extension.as_str()) + extension.as_str().parse::().is_ok() } fn package_version_from_release_filename(filename: &str) -> Option { @@ -269,7 +269,7 @@ pub async fn resolve( .date(); let manifests = github - .get_manifests(package_identifier, latest_version) + .get_manifests(package_identifier, latest_version, false) .await?; let should_update = match manifests.installer.release_date { diff --git a/src/commands/strategies/vanity_url.rs b/src/commands/strategies/vanity_url.rs index 1f9d528a3..cca589e41 100644 --- a/src/commands/strategies/vanity_url.rs +++ b/src/commands/strategies/vanity_url.rs @@ -80,7 +80,7 @@ pub async fn resolve( .ok_or_else(|| VanityUrlError::MissingLastModified(source_url.clone()))?; let manifests = github - .get_manifests(package_identifier, latest_version) + .get_manifests(package_identifier, latest_version, false) .await?; manifests diff --git a/src/commands/submit.rs b/src/commands/submit.rs index 72ba148bc..0d2d09ccd 100644 --- a/src/commands/submit.rs +++ b/src/commands/submit.rs @@ -13,7 +13,7 @@ use walkdir::WalkDir; use winget_types::{GenericManifest, ManifestType, ManifestVersion}; use crate::{ - commands::utils::{RateLimit, SPINNER_TICK_RATE, SubmitOption}, + commands::utils::{RateLimit, SPINNER_TICK_RATE, SubmitOption, check_package_type}, github::{ client::GitHub, utils::{PackagePath, pull_request::pr_changes}, @@ -155,7 +155,8 @@ impl Submit { locale_manifest.manifest_version = ManifestVersion::default(); } - let package_path = PackagePath::new(&identifier, Some(&version), None); + let is_font = check_package_type(&manifest.installer)?; + let package_path = PackagePath::new(&identifier, Some(&version), None, is_font); let changes = pr_changes() .package_identifier(&identifier) .manifests(&manifest) @@ -175,7 +176,11 @@ impl Submit { continue; } - let versions = github.get_versions(&identifier).await.ok(); + let (versions, _) = github + .get_versions(&identifier, Some(is_font)) + .await + .ok() + .unzip(); rate_limit.wait().await; diff --git a/src/commands/token/update.rs b/src/commands/token/update.rs index 70d7b88ae..36612c3d9 100644 --- a/src/commands/token/update.rs +++ b/src/commands/token/update.rs @@ -2,10 +2,13 @@ use anstream::println; use clap::Parser; use color_eyre::eyre::Result; use owo_colors::OwoColorize; -use reqwest::Client; +use reqwest::Client as ReqwestClient; use secrecy::{ExposeSecret, SecretString}; -use crate::token::{TokenManager, default_headers}; +use crate::{ + github::retry::client as retrying_client, + token::{TokenManager, default_headers}, +}; /// Update the stored token #[derive(Parser)] @@ -20,9 +23,11 @@ impl UpdateToken { pub async fn run(self) -> Result<()> { let credential = TokenManager::credential()?; - let client = Client::builder() - .default_headers(default_headers(None)) - .build()?; + let client = retrying_client( + ReqwestClient::builder() + .default_headers(default_headers(None)) + .build()?, + ); let token = match self.token { Some(token) => { diff --git a/src/commands/update_version.rs b/src/commands/update_version.rs index 5017ea222..217bffa63 100644 --- a/src/commands/update_version.rs +++ b/src/commands/update_version.rs @@ -40,7 +40,10 @@ use crate::{ match_installers::match_installers, prompts::text::optional_prompt, token::TokenManager, - traits::{LocaleExt, path::NormalizePath}, + traits::{ + LocaleExt, + path::{LowercaseExtension, NormalizePath}, + }, }; /// Add a version to a pre-existing package @@ -107,6 +110,10 @@ pub struct UpdateVersion { #[arg(long, env)] pub(super) skip_pr_check: bool, + /// Look for the package under fonts instead of probing manifests first + #[arg(long)] + pub(super) font: bool, + /// GitHub personal access token with the `public_repo` scope #[arg(short, long, env = "GITHUB_TOKEN", hide_env_values = true)] pub(super) token: Option, @@ -125,7 +132,9 @@ impl UpdateVersion { ); } - let versions = github.get_versions(&self.package_identifier).await?; + let (versions, font) = github + .get_versions(&self.package_identifier, self.font.then_some(true)) + .await?; let latest_version = versions.last().unwrap_or_else(|| unreachable!()); println!( @@ -146,7 +155,7 @@ impl UpdateVersion { let (mut manifests, mut github_values, mut files) = try_join!( github - .get_manifests(&self.package_identifier, latest_version) + .get_manifests(&self.package_identifier, latest_version, font) .map_err(Error::new), self.fetch_github_values(&github).map_err(Error::new), async { @@ -209,6 +218,7 @@ impl UpdateVersion { installer }) .collect::>(); + manifests.installer.installers.clear(); let duplicate_urls = previous_installers .iter() @@ -217,7 +227,7 @@ impl UpdateVersion { .collect::>(); manifests.default_locale.package_version = self.package_version.as_ref().unwrap().clone(); - let matched_installers = match_installers(previous_installers, &installer_results); + let matched_installers = match_installers(&previous_installers, &installer_results); let mut installers = matched_installers .into_iter() .map(|(previous_installer, new_installer)| { @@ -274,6 +284,18 @@ impl UpdateVersion { .filter(|installer| !matched_urls.contains(&installer.url)), ); + if installers + .iter() + .flat_map(|installer| &installer.locale) + .all_equal() + { + for installer in &mut installers { + installer.locale = None; + } + } + + manifests.installer.locale = None; + manifests.installer.package_version = package_version.clone(); manifests.installer.minimum_os_version = manifests .installer @@ -313,7 +335,8 @@ impl UpdateVersion { manifests.version.update(package_version); - let package_path = PackagePath::new(&self.package_identifier, Some(package_version), None); + let package_path = + PackagePath::new(&self.package_identifier, Some(package_version), None, font); let changes = pr_changes() .package_identifier(&self.package_identifier) .manifests(&manifests) @@ -413,7 +436,7 @@ impl UpdateVersion { package_version: &PackageVersion, ) -> Result { if let Some(ref pull_request) = github - .get_existing_pull_request(&self.package_identifier, package_version) + .get_existing_pull_request(&self.package_identifier, package_version, false) .await? && !self.skip_pr_check && !self.dry_run @@ -509,7 +532,7 @@ fn fix_relative_paths( ) }) .map(|path| NestedInstallerFiles { - relative_file_path: path.to_path_buf(), + relative_file_path: path.lowercase_extension(), ..nested_installer_files }) } diff --git a/src/commands/utils/environment.rs b/src/commands/utils/environment.rs index 09c5f68c1..d1370ca59 100644 --- a/src/commands/utils/environment.rs +++ b/src/commands/utils/environment.rs @@ -5,3 +5,9 @@ pub static CI: LazyLock = pub static VHS: LazyLock = LazyLock::new(|| env::var("VHS").is_ok_and(|vhs| vhs.parse() == Ok(true))); + +pub static EDITOR: LazyLock> = LazyLock::new(|| { + ["KOMAC_EDITOR", "EDITOR"] + .into_iter() + .find_map(|key| env::var(key).ok().filter(|val| !val.is_empty())) +}); diff --git a/src/commands/utils/mod.rs b/src/commands/utils/mod.rs index 0a56c9cc8..f78b7138f 100644 --- a/src/commands/utils/mod.rs +++ b/src/commands/utils/mod.rs @@ -7,18 +7,21 @@ use std::time::Duration; use anstream::println; use camino::Utf8Path; use chrono::Local; -use color_eyre::Result; +use color_eyre::{Result, eyre::bail}; use futures_util::{StreamExt, TryStreamExt, stream}; use inquire::error::InquireResult; use owo_colors::OwoColorize; pub use rate_limit::RateLimit; pub use submit_option::SubmitOption; use tokio::{fs, fs::File, io::AsyncWriteExt}; -use winget_types::{PackageIdentifier, PackageVersion}; +use winget_types::{ + PackageIdentifier, PackageVersion, + installer::{InstallerManifest, InstallerType, NestedInstallerType}, +}; use crate::{ commands::utils::environment::CI, github::graphql::get_existing_pull_request::PullRequest, - prompts::text::confirm_prompt, + prompts::text::confirm_prompt, traits::InstallerManifestExt, }; pub const SPINNER_TICK_RATE: Duration = Duration::from_millis(50); @@ -46,6 +49,27 @@ pub fn prompt_existing_pull_request( } } +pub fn should_abort_for_existing_pr( + identifier: &PackageIdentifier, + version: &PackageVersion, + existing_pr: T, + skip_pr_check: bool, + dry_run: bool, +) -> Result +where + T: Into>, +{ + if let Some(ref pull_request) = existing_pr.into() + && !skip_pr_check + && !dry_run + && !prompt_existing_pull_request(identifier, version, pull_request)? + { + return Ok(true); + } + + Ok(false) +} + pub async fn write_changes_to_dir(changes: &[(String, String)], output: &Utf8Path) -> Result<()> { fs::create_dir_all(output).await?; stream::iter(changes.iter()) @@ -60,3 +84,23 @@ pub async fn write_changes_to_dir(changes: &[(String, String)], output: &Utf8Pat .try_collect() .await } + +pub fn check_package_type(manifest: &InstallerManifest) -> Result { + let (mut has_font, mut has_installer) = (false, false); + + for installer in manifest.inherit_manifest_properties() { + if installer.r#type == Some(InstallerType::Font) + || installer.nested_installer_type == Some(NestedInstallerType::Font) + { + has_font = true; + } else { + has_installer = true; + } + + if has_font && has_installer { + bail!("Application and font installers cannot be mixed in the same manifest"); + } + } + + Ok(has_font) +} diff --git a/src/commands/utils/submit_option.rs b/src/commands/utils/submit_option.rs index 17ddb5b1d..89154fb3b 100644 --- a/src/commands/utils/submit_option.rs +++ b/src/commands/utils/submit_option.rs @@ -2,11 +2,14 @@ use std::fmt; use color_eyre::Result; use inquire::Select; +use tracing::error; use tracing_indicatif::suspend_tracing_indicatif; use winget_types::{PackageIdentifier, PackageVersion}; use crate::{ - commands::utils::environment::VHS, editor::Editor, manifests::print_changes, + commands::utils::environment::{EDITOR, VHS}, + editor::{Editor, EditorError, edit_externally}, + manifests::print_changes, prompts::handle_inquire_error, }; @@ -48,11 +51,16 @@ impl SubmitOption { .map_err(handle_inquire_error)? }; - if submit_option.is_edit() { - Editor::new(changes).run()?; - } else { + if !submit_option.is_edit() { break; } + + if let Err(error) = edit_externally(EDITOR.as_deref(), changes) { + if !matches!(error, EditorError::CommandEmpty) { + error!("External editor failed to load: {}", error); + } + Editor::new(changes).run()?; + } } Ok(submit_option) diff --git a/src/download/downloader.rs b/src/download/downloader.rs index 1b474c127..eb7c65559 100644 --- a/src/download/downloader.rs +++ b/src/download/downloader.rs @@ -3,7 +3,7 @@ use std::{fmt, num::NonZeroUsize}; use chrono::DateTime; use color_eyre::{Result, eyre::bail}; use futures_util::{StreamExt, TryStreamExt, stream}; -use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; +use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle}; use itertools::{Itertools, Position}; use reqwest::{ Client, @@ -26,6 +26,7 @@ use super::{Download, DownloadedFile}; pub struct Downloader { client: Client, concurrent_downloads: NonZeroUsize, + show_progress: bool, } impl Downloader { @@ -67,12 +68,29 @@ impl Downloader { /// /// [`ClientBuilder::build`]: reqwest::ClientBuilder::build pub fn new_with_concurrent(concurrent_downloads: NonZeroUsize) -> reqwest::Result { + Self::new_with_concurrent_and_progress(concurrent_downloads, true) + } + + /// Creates a new Downloader with a specified number of maximum concurrent downloads and + /// optional progress rendering. + /// + /// # Errors + /// + /// Propagates the error from [`ClientBuilder::build`] which fails if a TLS backend cannot be + /// initialized, or the resolver cannot load the system configuration. + /// + /// [`ClientBuilder::build`]: reqwest::ClientBuilder::build + pub fn new_with_concurrent_and_progress( + concurrent_downloads: NonZeroUsize, + show_progress: bool, + ) -> reqwest::Result { Ok(Self { client: Client::builder() .default_headers(Self::headers()) .referer(false) .build()?, concurrent_downloads, + show_progress, }) } @@ -81,14 +99,20 @@ impl Downloader { I: IntoIterator, D: Into, { - let multi_progress = crate::terminal::multi_progress(); + let multi_progress = if self.show_progress { + MultiProgress::new() + } else { + MultiProgress::with_draw_target(ProgressDrawTarget::hidden()) + }; let downloaded_files = stream::iter(downloads.into_iter().map(D::into).unique()) - .map(|download| self.fetch(&self.client, download, multi_progress)) + .map(|download| self.fetch(&self.client, download, &multi_progress)) .buffer_unordered(self.concurrent_downloads.get()) .try_collect::>() .await?; + multi_progress.clear()?; + Ok(downloaded_files) } @@ -108,12 +132,15 @@ impl Downloader { download: &Download, content_types: GetAll, ) -> Result<(), ContentTypeError> { - if content_types.iter().all(|content_type| { - content_type != Self::OCTET_STREAM - && !content_type - .as_bytes() - .starts_with(Self::APPLICATION.as_bytes()) - }) { + // Some download servers omit Content-Type, so only reject explicitly invalid values. + if content_types.iter().next().is_some() + && content_types.iter().all(|content_type| { + content_type != Self::OCTET_STREAM + && !content_type + .as_bytes() + .starts_with(Self::APPLICATION.as_bytes()) + }) + { return Err(ContentTypeError::new(download.clone(), content_types)); } @@ -265,3 +292,30 @@ impl fmt::Display for ContentTypeError { ) } } + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::*; + use crate::manifests::Url; + + #[rstest] + #[case::missing(&[], true)] + #[case::application(&["application/octet-stream"], true)] + #[case::binary_octet_stream(&["binary/octet-stream"], true)] + #[case::non_application(&["text/html"], false)] + #[case::one_valid(&["text/html", "application/octet-stream"], true)] + fn checks_content_types(#[case] content_types: &[&str], #[case] expected: bool) { + let download = Download::new("https://example.com/installer.exe".parse::().unwrap()); + let mut headers = HeaderMap::new(); + for content_type in content_types { + headers.append(CONTENT_TYPE, content_type.parse().unwrap()); + } + + assert_eq!( + Downloader::check_content_types(&download, headers.get_all(CONTENT_TYPE)).is_ok(), + expected + ); + } +} diff --git a/src/download/mod.rs b/src/download/mod.rs index f9afe070b..5fc044311 100644 --- a/src/download/mod.rs +++ b/src/download/mod.rs @@ -9,7 +9,7 @@ pub use downloader::Downloader; pub use file::DownloadedFile; use reqwest::{Client, ClientBuilder, Response, header::HeaderValue, redirect::Policy}; use uuid::Uuid; -use winget_types::installer::VALID_FILE_EXTENSIONS; +use winget_types::utils::ValidFileExtensions; use crate::{github::GITHUB_HOST, manifests::Url}; @@ -83,9 +83,7 @@ impl Download { .path_segments() .and_then(|mut segments| segments.next_back()) .filter(|last_segment| { - Utf8Path::new(last_segment) - .extension() - .is_some_and(|extension| VALID_FILE_EXTENSIONS.contains(&extension)) + ValidFileExtensions::from_path(Utf8Path::new(last_segment)).is_ok() }) .or_else(|| { final_url diff --git a/src/editor.rs b/src/editor.rs index 7416dc27b..262d0b890 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -1,5 +1,6 @@ -use std::{borrow::Cow, fmt::Display, io, ops::Add}; +use std::{borrow::Cow, fmt::Display, fs, io, ops::Add, process::Command}; +use camino::Utf8Path; use ratatui::{ DefaultTerminal, layout::{Constraint, Direction, Layout}, @@ -8,6 +9,85 @@ use ratatui::{ widgets::{Block, Borders, Paragraph}, }; use ratatui_textarea::{CursorMove, DataCursor, Input, Key, TextArea}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum EditorError { + #[error("command is empty")] + CommandEmpty, + #[error("process exited with code {0}")] + NonZeroExit(i32), + #[error("process terminated by signal")] + SignalTermination(), + #[error(transparent)] + Io(#[from] io::Error), +} + +pub fn edit_externally( + editor: Option<&str>, + changes: &mut [(String, String)], +) -> Result<(), EditorError> { + let Some(editor) = editor else { + return Err(EditorError::CommandEmpty); + }; + + let temp_dir = tempfile::tempdir()?; + + let file_paths: Vec<_> = changes + .iter() + .map(|(path, content)| { + let file_name = Utf8Path::new(path).file_name().unwrap_or(path.as_str()); + let file_path = temp_dir.path().join(file_name); + fs::write(&file_path, content.as_bytes())?; + Ok(file_path) + }) + .collect::>()?; + + println!("Waiting for your editor to close the files..."); + + #[cfg(windows)] + let status = { + // Command::new behavior is weird on Windows, spawn a shell to avoid that + let file_args = file_paths + .iter() + .map(|path| { + let escaped = path.to_string_lossy().replace('\'', "''"); + format!("'{escaped}'") + }) + .collect::>() + .join(" "); + + Command::new("powershell") + .arg("-NoProfile") + .arg("-Command") + .arg(format!("{editor} {file_args}")) + .status()? + }; + + #[cfg(not(windows))] + let status = { + let mut parts = editor.split_whitespace(); + let program = parts.next().ok_or(EditorError::CommandEmpty)?; + + Command::new(program) + .args(parts) + .args(&file_paths) + .status()? + }; + + if !status.success() { + match status.code() { + Some(status) => return Err(EditorError::NonZeroExit(status)), + None => return Err(EditorError::SignalTermination()), + } + } + + for (file_path, (_, content)) in file_paths.iter().zip(changes.iter_mut()) { + *content = fs::read_to_string(file_path)?; + } + + Ok(()) +} struct SearchBox<'a> { textarea: TextArea<'a>, diff --git a/src/github/client.rs b/src/github/client.rs index 7fb8ef551..43db66480 100644 --- a/src/github/client.rs +++ b/src/github/client.rs @@ -2,12 +2,13 @@ use std::{borrow::Cow, collections::BTreeSet, num::NonZeroU32, str::FromStr}; use bon::bon; use color_eyre::eyre::eyre; -use cynic::{GraphQlResponse, Id, MutationBuilder, QueryBuilder, http::ReqwestExt}; +use cynic::{GraphQlResponse, Id, MutationBuilder, QueryBuilder}; use futures_util::future::OptionFuture; use indexmap::IndexMap; use indicatif::ProgressBar; use itertools::Itertools; use reqwest::Client; +use reqwest_middleware::ClientWithMiddleware; use secrecy::SecretString; use serde::de::DeserializeOwned; use url::Url; @@ -25,7 +26,6 @@ use crate::{ github::{ MICROSOFT, WINGET_PKGS, WINGET_PKGS_FULL_NAME, graphql::{ - GRAPHQL_URL, create_commit::{FileAddition, FileDeletion}, create_ref::{CreateRef, CreateRefVariables, Ref as CreateBranchRef}, get_all_values::{GetAllValues, GetAllValuesGitObject, GetAllValuesVariables, Tree}, @@ -49,7 +49,7 @@ use crate::{ #[derive(Clone)] #[repr(transparent)] -pub struct GitHub(pub(super) Client); +pub struct GitHub(pub(super) ClientWithMiddleware); #[bon] impl GitHub { @@ -57,19 +57,20 @@ impl GitHub { where T: AsRef, { - Ok(Self( + Ok(Self(super::retry::client( Client::builder() .default_headers(default_headers(Some(token.as_ref()))) .build()?, - )) + ))) } pub async fn get_manifests( &self, identifier: &PackageIdentifier, latest_version: &PackageVersion, + font: bool, ) -> Result { - let full_package_path = PackagePath::new(identifier, Some(latest_version), None); + let full_package_path = PackagePath::new(identifier, Some(latest_version), None, font); let content = self .get_directory_content_with_text(MICROSOFT, WINGET_PKGS, &full_package_path) .await? @@ -94,7 +95,7 @@ impl GitHub { ) }) .map(|file| serde_yaml::from_str::(&file.text)) - .collect::>()?; + .collect::>()?; let default_locale_manifest = content .iter() @@ -134,11 +135,10 @@ impl GitHub { repo: &str, path: &PackagePath, ) -> Result, GitHubError> { + let expression = format!("HEAD:{path}"); let GraphQlResponse { data, errors } = self - .0 - .post(GRAPHQL_URL) - .run_graphql(GetDirectoryContentWithText::build( - GetDirectoryContentVariables::new(&owner, &repo, &format!("HEAD:{path}")), + .run_graphql_with_retry(&GetDirectoryContentWithText::build( + GetDirectoryContentVariables::new(&owner, &repo, &expression), )) .await?; @@ -162,8 +162,9 @@ impl GitHub { identifier: &PackageIdentifier, version: &PackageVersion, manifest_type: ManifestTypeWithLocale, + font: bool, ) -> Result { - let path = PackagePath::new(identifier, Some(version), Some(&manifest_type)); + let path = PackagePath::new(identifier, Some(version), Some(&manifest_type), font); let content = self.get_file_content(MICROSOFT, WINGET_PKGS, &path).await?; let manifest = serde_yaml::from_str::(&content)?; Ok(manifest) @@ -184,9 +185,7 @@ impl GitHub { name: &str, ) -> Result { let GraphQlResponse { data, errors } = self - .0 - .post(GRAPHQL_URL) - .run_graphql(GetRepositoryInfo::build(RepositoryVariables::new( + .run_graphql_with_retry(&GetRepositoryInfo::build(RepositoryVariables::new( owner, name, ))) .await?; @@ -231,12 +230,11 @@ impl GitHub { branch_name: &str, oid: GitObjectId, ) -> Result { + let ref_name = format!("refs/heads/{branch_name}"); let GraphQlResponse { data, errors } = self - .0 - .post(GRAPHQL_URL) - .run_graphql(CreateRef::build( + .run_graphql_with_retry(&CreateRef::build( CreateRefVariables::builder() - .name(&format!("refs/heads/{branch_name}")) + .name(&ref_name) .oid(oid) .repository_id(fork_id) .build(), @@ -260,9 +258,7 @@ impl GitHub { loop { let GraphQlResponse { data, errors } = self - .0 - .post(GRAPHQL_URL) - .run_graphql(GetBranches::build(GetBranchesVariables { + .run_graphql_with_retry(&GetBranches::build(GetBranchesVariables { owner: user, name: WINGET_PKGS, cursor: cursor.as_deref(), @@ -289,14 +285,10 @@ impl GitHub { for branch in branches.into_iter().filter(|branch| { branch.name != default_branch.name - && branch - .associated_pull_requests - .pull_requests - .iter() - .all(|pull_request| !pull_request.state.is_open()) + && branch.open_pull_requests.pull_requests.is_empty() }) { if let Some(pull_request) = branch - .associated_pull_requests + .closed_or_merged_pull_requests .pull_requests .into_iter() .filter(|pull_request| match merge_state { @@ -330,9 +322,7 @@ impl GitHub { T: Into, { let GraphQlResponse { data, errors } = self - .0 - .post(GRAPHQL_URL) - .run_graphql(UpdateRefs::build(UpdateRefsInput::new( + .run_graphql_with_retry(&UpdateRefs::build(UpdateRefsInput::new( RefUpdate::delete_branches(branch_names), repository_id, ))) @@ -374,9 +364,7 @@ impl GitHub { #[builder(into)] tag_name: Cow<'a, str>, ) -> Result { let GraphQlResponse { data, errors } = self - .0 - .post(GRAPHQL_URL) - .run_graphql(GetAllValues::build(GetAllValuesVariables { + .run_graphql_with_retry(&GetAllValues::build(GetAllValuesVariables { name: &repo, owner: &owner, tag_name: &tag_name, @@ -462,6 +450,7 @@ impl GitHub { reason: &str, fork: &RepositoryData, winget_pkgs: &RepositoryData, + font: bool, #[builder(default)] issue_resolves: &[NonZeroU32], ) -> Result { // Create an indeterminate progress bar to show as a pull request is being created @@ -483,7 +472,7 @@ impl GitHub { .get_directory_content() .owner(&fork.owner) .branch_name(&branch_name) - .path(&PackagePath::new(identifier, Some(version), None)) + .path(&PackagePath::new(identifier, Some(version), None, font)) .call() .await? .map(FileDeletion::new) @@ -529,6 +518,7 @@ impl GitHub { created_with: Option<&str>, created_with_url: Option<&DecodedUrl>, ) -> Result { + let font = changes.iter().any(|(path, _)| path.starts_with("fonts/")); let (current_user, winget_pkgs) = tokio::try_join!(self.get_username(), self.get_winget_pkgs().send())?; let fork = self.get_winget_pkgs().owner(¤t_user).send().await?; @@ -545,7 +535,7 @@ impl GitHub { self.get_directory_content() .owner(¤t_user) .branch_name(&branch_name) - .path(&PackagePath::new(identifier, replace_version, None)) + .path(&PackagePath::new(identifier, replace_version, None, font)) .call() .await? .map(FileDeletion::new) diff --git a/src/github/error.rs b/src/github/error.rs index e2d5f57bc..f5e881740 100644 --- a/src/github/error.rs +++ b/src/github/error.rs @@ -23,6 +23,8 @@ pub enum GitHubError { #[error(transparent)] Reqwest(#[from] reqwest::Error), #[error(transparent)] + ReqwestMiddleware(#[from] reqwest_middleware::Error), + #[error(transparent)] CynicRequest(#[from] CynicReqwestError), #[error(transparent)] YamlError(#[from] serde_yaml::Error), diff --git a/src/github/graphql/create_commit.rs b/src/github/graphql/create_commit.rs index d5c4fd37b..ca52ceb0a 100644 --- a/src/github/graphql/create_commit.rs +++ b/src/github/graphql/create_commit.rs @@ -2,12 +2,12 @@ use std::borrow::Cow; use bon::{Builder, bon}; use color_eyre::eyre::eyre; -use cynic::{GraphQlResponse, Id, MutationBuilder, http::ReqwestExt}; +use cynic::{GraphQlResponse, Id, MutationBuilder}; use url::Url; use super::{ super::{GitHubError, client::GitHub}, - GRAPHQL_URL, github_schema as schema, + github_schema as schema, types::{Base64String, GitObjectId}, }; @@ -140,9 +140,7 @@ impl GitHub { #[builder(default)] deletions: Vec>, ) -> Result { let GraphQlResponse { data, errors } = self - .0 - .post(GRAPHQL_URL) - .run_graphql(CreateCommit::build(CreateCommitVariables { + .run_graphql_with_retry(&CreateCommit::build(CreateCommitVariables { input: CreateCommitOnBranchInput::builder() .branch(CommittableBranch::new(branch_id)) .expected_head_oid(head_sha) diff --git a/src/github/graphql/create_pull_request.rs b/src/github/graphql/create_pull_request.rs index 028302b0a..db8b9c2a9 100644 --- a/src/github/graphql/create_pull_request.rs +++ b/src/github/graphql/create_pull_request.rs @@ -2,13 +2,13 @@ use std::io::Write; use bon::Builder; use color_eyre::eyre::eyre; -use cynic::{GraphQlResponse, MutationBuilder, http::ReqwestExt}; +use cynic::{GraphQlResponse, MutationBuilder}; use owo_colors::OwoColorize; use url::Url; use super::{ super::{GitHubError, client::GitHub}, - GRAPHQL_URL, github_schema as schema, + github_schema as schema, }; use crate::terminal::{Hyperlinkable, SUPPORTS_HYPERLINKS}; @@ -118,8 +118,7 @@ impl GitHub { .build(), }); - let GraphQlResponse { data, errors } = - self.0.post(GRAPHQL_URL).run_graphql(operation).await?; + let GraphQlResponse { data, errors } = self.run_graphql_with_retry(&operation).await?; data.and_then(|data| data.create_pull_request?.pull_request) .ok_or_else(|| { diff --git a/src/github/graphql/get_branches.rs b/src/github/graphql/get_branches.rs index 3bf468d83..99f7d169d 100644 --- a/src/github/graphql/get_branches.rs +++ b/src/github/graphql/get_branches.rs @@ -47,8 +47,12 @@ pub struct PageInfo { #[cynic(graphql_type = "Ref")] pub struct PullRequestBranchRef { pub name: String, - #[arguments(first: 5)] - pub associated_pull_requests: PullRequestConnection, + #[cynic(rename = "associatedPullRequests", alias)] + #[arguments(first: 1, states: [OPEN])] + pub open_pull_requests: PullRequestConnection, + #[cynic(rename = "associatedPullRequests", alias)] + #[arguments(first: 1, states: [CLOSED, MERGED], orderBy: { field: CREATED_AT, direction: DESC })] + pub closed_or_merged_pull_requests: PullRequestConnection, } /// @@ -107,7 +111,17 @@ mod tests { refs(first: 100, after: $cursor, refPrefix: "refs/heads/") { nodes { name - associatedPullRequests(first: 5) { + open_pull_requests: associatedPullRequests(first: 1, states: [OPEN]) { + nodes { + title + url + state + repository { + nameWithOwner + } + } + } + closed_or_merged_pull_requests: associatedPullRequests(first: 1, states: [CLOSED, MERGED], orderBy: {field: CREATED_AT, direction: DESC}) { nodes { title url diff --git a/src/github/graphql/get_current_user.rs b/src/github/graphql/get_current_user.rs index c0fdf9011..5efe6dc8e 100644 --- a/src/github/graphql/get_current_user.rs +++ b/src/github/graphql/get_current_user.rs @@ -1,11 +1,11 @@ use std::env; use color_eyre::eyre::eyre; -use cynic::{GraphQlResponse, QueryBuilder, http::ReqwestExt}; +use cynic::{GraphQlResponse, QueryBuilder}; use super::{ super::{GitHubError, client::GitHub}, - GRAPHQL_URL, github_schema as schema, + github_schema as schema, }; /// @@ -41,9 +41,7 @@ impl GitHub { Ok(login) } else { let GraphQlResponse { data, errors } = self - .0 - .post(GRAPHQL_URL) - .run_graphql(GetCurrentUserLogin::build(())) + .run_graphql_with_retry(&GetCurrentUserLogin::build(())) .await?; let Some(data) = data else { diff --git a/src/github/graphql/get_directory_content.rs b/src/github/graphql/get_directory_content.rs index b65dcf2fb..0824b3fb8 100644 --- a/src/github/graphql/get_directory_content.rs +++ b/src/github/graphql/get_directory_content.rs @@ -2,11 +2,11 @@ use std::fmt; use bon::bon; use color_eyre::eyre::eyre; -use cynic::{GraphQlResponse, QueryBuilder, http::ReqwestExt}; +use cynic::{GraphQlResponse, QueryBuilder}; use super::{ super::{GitHubError, MICROSOFT, WINGET_PKGS, client::GitHub, utils::PackagePath}, - GRAPHQL_URL, GetFileContent, github_schema as schema, + GetFileContent, github_schema as schema, }; #[derive(cynic::QueryVariables)] @@ -86,13 +86,12 @@ impl GitHub { R: AsRef, P: fmt::Display, { + let expression = format!("HEAD:{path}"); let GraphQlResponse { data, errors } = self - .0 - .post(GRAPHQL_URL) - .run_graphql(GetFileContent::build(GetDirectoryContentVariables::new( + .run_graphql_with_retry(&GetFileContent::build(GetDirectoryContentVariables::new( &owner, &repo, - &format!("HEAD:{path}"), + &expression, ))) .await?; @@ -108,11 +107,10 @@ impl GitHub { #[builder(default = "HEAD")] branch_name: &str, path: &PackagePath, ) -> Result, GitHubError> { + let expression = format!("{branch_name}:{path}"); let GraphQlResponse { data, errors } = self - .0 - .post(GRAPHQL_URL) - .run_graphql(GetDirectoryContent::build( - GetDirectoryContentVariables::new(&owner, &repo, &format!("{branch_name}:{path}")), + .run_graphql_with_retry(&GetDirectoryContent::build( + GetDirectoryContentVariables::new(&owner, &repo, &expression), )) .await?; let entries = data diff --git a/src/github/graphql/get_existing_pull_request.rs b/src/github/graphql/get_existing_pull_request.rs index d1eab0885..c266e2922 100644 --- a/src/github/graphql/get_existing_pull_request.rs +++ b/src/github/graphql/get_existing_pull_request.rs @@ -1,11 +1,11 @@ use chrono::{DateTime, Utc}; -use cynic::{QueryBuilder, http::ReqwestExt}; +use cynic::QueryBuilder; use url::Url; use winget_types::{PackageIdentifier, PackageVersion}; use super::{ super::{GitHubError, WINGET_PKGS_FULL_NAME, client::GitHub}, - GRAPHQL_URL, github_schema as schema, + github_schema as schema, types::PullRequestState, }; @@ -42,9 +42,25 @@ pub struct PullRequest { pub url: Url, pub state: PullRequestState, pub created_at: DateTime, + #[allow(dead_code)] + pub viewer_did_author: bool, + #[allow(dead_code)] + pub author: Option, +} + +#[derive(cynic::QueryFragment)] +pub struct Actor { + #[allow(dead_code)] + pub login: String, } impl PullRequest { + #[allow(dead_code)] + #[inline] + pub fn author_login(&self) -> Option<&String> { + self.author.as_ref().map(|author| &author.login) + } + /// Returns `true` if the pull request has been closed without being merged. #[expect(unused)] #[inline] @@ -87,40 +103,45 @@ impl GitHub { &self, identifier: &PackageIdentifier, version: &PackageVersion, + authenticated_user_only: bool, ) -> Result, GitHubError> { - self - .0 - .post(GRAPHQL_URL) - .run_graphql(GetExistingPullRequest::build(GetExistingPullRequestVariables { - query: &format!("repo:{WINGET_PKGS_FULL_NAME} is:pull-request in:title {identifier} {version}"), - })) - .await - .map(|response| { - response - .data? - .into_pull_requests() - .find(|pull_request| { - let title = &*pull_request.title; - // Check that the identifier and version are used in their entirety and not - // part of another package identifier or version. For example, ensuring we - // match against "Microsoft.Excel" not "Microsoft.Excel.Beta", or "1.2.3" - // and not "1.2.3-beta" as `in:title` in the query only does a 'contains' - // rather than a word boundary match. - [identifier.as_str(), version.as_str()] - .into_iter() - .all(|needle| { - title.match_indices(needle).any(|(index, matched)| { - let before = title[..index].chars().next_back(); - let after = title[index + matched.len()..].chars().next(); - // Check whether the characters before and after the identifier - // are either None (at the boundary of the title) or whitespace - before.is_none_or(char::is_whitespace) - && after.is_none_or(char::is_whitespace) - }) - }) + let author_filter = if authenticated_user_only { + " author:@me" + } else { + "" + }; + let query = format!( + "repo:{WINGET_PKGS_FULL_NAME} is:pull-request in:title {identifier} {version}{author_filter}" + ); + + let response = self + .run_graphql_with_retry(&GetExistingPullRequest::build( + GetExistingPullRequestVariables { query: &query }, + )) + .await?; + + Ok(response.data.and_then(|data| { + data.into_pull_requests().find(|pull_request| { + let title = &*pull_request.title; + // Check that the identifier and version are used in their entirety and not + // part of another package identifier or version. For example, ensuring we + // match against "Microsoft.Excel" not "Microsoft.Excel.Beta", or "1.2.3" + // and not "1.2.3-beta" as `in:title` in the query only does a 'contains' + // rather than a word boundary match. + [identifier.as_str(), version.as_str()] + .into_iter() + .all(|needle| { + title.match_indices(needle).any(|(index, matched)| { + let before = title[..index].chars().next_back(); + let after = title[index + matched.len()..].chars().next(); + // Check whether the characters before and after the identifier + // are either None (at the boundary of the title) or whitespace + before.is_none_or(char::is_whitespace) + && after.is_none_or(char::is_whitespace) + }) }) }) - .map_err(GitHubError::CynicRequest) + })) } } @@ -143,6 +164,10 @@ mod tests { url state createdAt + viewerDidAuthor + author { + login + } } } } diff --git a/src/github/graphql/update_refs.rs b/src/github/graphql/update_refs.rs index 21f875838..2b2d9ec61 100644 --- a/src/github/graphql/update_refs.rs +++ b/src/github/graphql/update_refs.rs @@ -55,7 +55,7 @@ impl<'id> UpdateRefsInput<'id> { #[derive(cynic::QueryFragment)] #[cynic(graphql_type = "Mutation", variables = "UpdateRefsInput")] pub struct UpdateRefs { - #[expect(dead_code)] + #[allow(dead_code)] #[arguments(input: { clientMutationId: $client_mutation_id, refUpdates: $ref_updates, repositoryId: $repository_id })] pub update_refs: Option, } @@ -63,7 +63,7 @@ pub struct UpdateRefs { #[derive(cynic::QueryFragment)] pub struct UpdateRefsPayload { /// A unique identifier for the client performing the mutation. - #[expect(dead_code)] + #[allow(dead_code)] pub client_mutation_id: Option, } diff --git a/src/github/mod.rs b/src/github/mod.rs index 0ba9cc3d6..4dd95beba 100644 --- a/src/github/mod.rs +++ b/src/github/mod.rs @@ -2,12 +2,151 @@ pub mod client; mod error; pub mod graphql; mod rest; +pub(crate) mod retry; pub mod utils; -use const_format::formatcp; +use std::{ + env, + fmt::{self, Display, Formatter}, + ops::Deref, + sync::OnceLock, +}; + pub use error::GitHubError; -pub const MICROSOFT: &str = "pl4nty"; -pub const WINGET_PKGS: &str = "winget-extras"; -pub const WINGET_PKGS_FULL_NAME: &str = formatcp!("{MICROSOFT}/{WINGET_PKGS}"); -pub const GITHUB_HOST: &str = "github.com"; +pub struct EnvStr { + env_var: &'static str, + default: &'static str, + value: OnceLock, +} + +impl EnvStr { + pub const fn new(env_var: &'static str, default: &'static str) -> Self { + Self { + env_var, + default, + value: OnceLock::new(), + } + } + + pub fn as_str(&self) -> &str { + self.value + .get_or_init(|| { + env::var(self.env_var) + .ok() + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| self.default.to_owned()) + }) + .as_str() + } + + pub fn set(&self, value: String) { + if !value.is_empty() { + let _ = self.value.set(value); + } + } +} + +impl AsRef for EnvStr { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl Deref for EnvStr { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_str() + } +} + +impl Display for EnvStr { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +impl PartialEq<&EnvStr> for &str { + fn eq(&self, other: &&EnvStr) -> bool { + *self == other.as_str() + } +} + +impl PartialEq<&str> for &EnvStr { + fn eq(&self, other: &&str) -> bool { + self.as_str() == *other + } +} + +pub struct GitHubFullName { + value: OnceLock, +} + +impl GitHubFullName { + pub const fn new() -> Self { + Self { + value: OnceLock::new(), + } + } + + pub fn as_str(&self) -> &str { + self.value + .get_or_init(|| format!("{}/{}", MICROSOFT.as_str(), WINGET_PKGS.as_str())) + .as_str() + } +} + +impl AsRef for GitHubFullName { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl Deref for GitHubFullName { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_str() + } +} + +impl Display for GitHubFullName { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +impl PartialEq<&GitHubFullName> for String { + fn eq(&self, other: &&GitHubFullName) -> bool { + self.as_str() == other.as_str() + } +} + +impl PartialEq for &GitHubFullName { + fn eq(&self, other: &String) -> bool { + self.as_str() == other.as_str() + } +} + +impl PartialEq<&GitHubFullName> for &str { + fn eq(&self, other: &&GitHubFullName) -> bool { + *self == other.as_str() + } +} + +impl PartialEq<&str> for &GitHubFullName { + fn eq(&self, other: &&str) -> bool { + self.as_str() == *other + } +} + +static MICROSOFT_VALUE: EnvStr = EnvStr::new("KOMAC_GITHUB_OWNER", "pl4nty"); +static WINGET_PKGS_VALUE: EnvStr = EnvStr::new("KOMAC_GITHUB_REPO", "winget-extras"); +static WINGET_PKGS_FULL_NAME_VALUE: GitHubFullName = GitHubFullName::new(); +static GITHUB_HOST_VALUE: EnvStr = EnvStr::new("KOMAC_GITHUB_HOST", "github.com"); + +pub static MICROSOFT: &EnvStr = &MICROSOFT_VALUE; +pub static WINGET_PKGS: &EnvStr = &WINGET_PKGS_VALUE; +pub static WINGET_PKGS_FULL_NAME: &GitHubFullName = &WINGET_PKGS_FULL_NAME_VALUE; +pub static GITHUB_HOST: &EnvStr = &GITHUB_HOST_VALUE; diff --git a/src/github/rest/tree/mod.rs b/src/github/rest/tree/mod.rs index f2091e8af..a4b75370c 100644 --- a/src/github/rest/tree/mod.rs +++ b/src/github/rest/tree/mod.rs @@ -134,16 +134,49 @@ impl GitHub { Ok(response.json::().await?) } + /// Retrieves all available versions for a given package identifier. + /// + /// If `font` is `None`, this function first attempts to fetch versions from the manifest path. + /// If that fails, it tries the font path. + /// + /// Returns a tuple containing: + /// * `BTreeSet` - A set of all available package versions + /// * `bool` - `true` if the package is a font (found in the font path) pub async fn get_versions( &self, package_identifier: &PackageIdentifier, - ) -> Result, GitHubError> { + font: Option, + ) -> Result<(BTreeSet, bool), GitHubError> { + if let Some(font) = font { + return self + .get_all_versions( + MICROSOFT, + WINGET_PKGS, + PackagePath::new(package_identifier, None, None, font), + ) + .await + .map(|versions| (versions, font)) + .map_err(|_| GitHubError::PackageNonExistent(package_identifier.clone())); + } + + if let Ok(versions) = self + .get_all_versions( + MICROSOFT, + WINGET_PKGS, + PackagePath::new(package_identifier, None, None, false), + ) + .await + { + return Ok((versions, false)); + } + self.get_all_versions( MICROSOFT, WINGET_PKGS, - PackagePath::new(package_identifier, None, None), + PackagePath::new(package_identifier, None, None, true), ) .await + .map(|versions| (versions, true)) .map_err(|_| GitHubError::PackageNonExistent(package_identifier.clone())) } @@ -167,7 +200,7 @@ impl GitHub { let response = self .0 - .get(endpoint) + .get(&endpoint) .header(ACCEPT, GITHUB_JSON_MIME) .header(X_GITHUB_API_VERSION, REST_API_VERSION) .send() diff --git a/src/github/retry.rs b/src/github/retry.rs new file mode 100644 index 000000000..bcce6ed1f --- /dev/null +++ b/src/github/retry.rs @@ -0,0 +1,236 @@ +use std::{error::Error as _, time::Duration}; + +use cynic::{GraphQlError, GraphQlResponse, Operation, http::CynicReqwestError}; +use reqwest::{ + Client, Response, StatusCode, + header::{HeaderMap, RETRY_AFTER}, +}; +use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; +use reqwest_retry::{ + Jitter, RetryTransientMiddleware, Retryable, RetryableStrategy, default_on_request_failure, + default_on_request_success, policies::ExponentialBackoff, +}; +use serde::{Serialize, de::DeserializeOwned}; + +use super::{GitHubError, client::GitHub, graphql::GRAPHQL_URL}; + +const MAX_GITHUB_REQUEST_RETRIES: u32 = 3; +const MIN_RETRY_INTERVAL: Duration = Duration::from_secs(1); +const MAX_RETRY_INTERVAL: Duration = Duration::from_secs(30); + +pub(crate) fn client(client: Client) -> ClientWithMiddleware { + let retry_policy = ExponentialBackoff::builder() + .retry_bounds(MIN_RETRY_INTERVAL, MAX_RETRY_INTERVAL) + .jitter(Jitter::Bounded) + .build_with_max_retries(MAX_GITHUB_REQUEST_RETRIES); + + ClientBuilder::new(client) + .with(RetryTransientMiddleware::new_with_policy_and_strategy( + retry_policy, + GitHubRetryableStrategy, + )) + .build() +} + +pub(crate) fn is_connect_error(error: &reqwest_middleware::Error) -> bool { + if error.is_connect() { + return true; + } + + let mut source = error.source(); + while let Some(error) = source { + if error + .downcast_ref::() + .is_some_and(reqwest::Error::is_connect) + { + return true; + } + + source = error.source(); + } + + false +} + +impl GitHub { + pub(super) async fn run_graphql_with_retry( + &self, + operation: &Operation, + ) -> Result, GitHubError> + where + ResponseData: DeserializeOwned + 'static, + Vars: Serialize, + { + // TEMPORARY TEST EXIT 1: remove this flag after validating workflow retries. + let mut retried_transient_graphql_error = false; + + for retry in 0..=MAX_GITHUB_REQUEST_RETRIES { + let response = + deserialize_graphql_response(self.0.post(GRAPHQL_URL).json(operation).send().await) + .await?; + + if retry == MAX_GITHUB_REQUEST_RETRIES || !is_retryable_graphql_response(&response) { + // TEMPORARY TEST EXIT 1: remove this block after validating workflow retries. + if retried_transient_graphql_error { + return Err(GitHubError::GraphQL(color_eyre::eyre::eyre!( + "temporary forced failure after GitHub GraphQL retry for workflow retry validation" + ))); + } + + return Ok(response); + } + + // TEMPORARY TEST EXIT 1: remove this assignment after validating workflow retries. + retried_transient_graphql_error = true; + + let delay = graphql_retry_delay(retry); + tracing::info!( + retry = retry + 1, + max_retries = MAX_GITHUB_REQUEST_RETRIES, + delay_secs = delay.as_secs(), + "Retrying GitHub GraphQL request after transient error" + ); + tokio::time::sleep(delay).await; + } + + unreachable!("GraphQL retry loop must return before exceeding max retries"); + } +} + +struct GitHubRetryableStrategy; + +impl RetryableStrategy for GitHubRetryableStrategy { + fn handle(&self, result: &Result) -> Option { + match result { + Ok(response) if is_retryable_response(response) => Some(Retryable::Transient), + Ok(response) => default_on_request_success(response), + Err(error) => default_on_request_failure(error), + } + } +} + +async fn deserialize_graphql_response( + response: reqwest_middleware::Result, +) -> Result, GitHubError> +where + ResponseData: DeserializeOwned, +{ + let response = response?; + + let status = response.status(); + if status.is_success() { + Ok(response + .json() + .await + .map_err(CynicReqwestError::ReqwestError)?) + } else { + let text = response.text().await?; + + if let Ok(response) = serde_json::from_str(&text) { + Ok(response) + } else { + Err(CynicReqwestError::ErrorResponse(status, text).into()) + } + } +} + +fn is_retryable_response(response: &Response) -> bool { + is_retryable_status(response.status(), response.headers()) +} + +fn is_retryable_graphql_response(response: &GraphQlResponse) -> bool { + response + .errors + .as_ref() + .is_some_and(|errors| errors.iter().any(is_retryable_graphql_error)) +} + +fn is_retryable_graphql_error(error: &GraphQlError) -> bool { + error + .message + .starts_with("Something went wrong while executing your query") +} + +fn is_retryable_status(status: StatusCode, headers: &HeaderMap) -> bool { + status == StatusCode::REQUEST_TIMEOUT + || status == StatusCode::TOO_MANY_REQUESTS + || status.is_server_error() + || (status == StatusCode::FORBIDDEN && headers.contains_key(RETRY_AFTER)) +} + +fn graphql_retry_delay(retry: u32) -> Duration { + MIN_RETRY_INTERVAL + .saturating_mul(2_u32.saturating_pow(retry)) + .min(MAX_RETRY_INTERVAL) +} + +#[cfg(test)] +mod tests { + use reqwest::header::HeaderValue; + + use super::*; + + #[test] + fn retryable_status_codes() { + let headers = HeaderMap::new(); + + assert!(is_retryable_status(StatusCode::REQUEST_TIMEOUT, &headers)); + assert!(is_retryable_status(StatusCode::TOO_MANY_REQUESTS, &headers)); + assert!(is_retryable_status(StatusCode::BAD_GATEWAY, &headers)); + assert!(is_retryable_status( + StatusCode::SERVICE_UNAVAILABLE, + &headers + )); + assert!(is_retryable_status(StatusCode::GATEWAY_TIMEOUT, &headers)); + + assert!(!is_retryable_status(StatusCode::UNAUTHORIZED, &headers)); + assert!(!is_retryable_status(StatusCode::FORBIDDEN, &headers)); + assert!(!is_retryable_status(StatusCode::NOT_FOUND, &headers)); + } + + #[test] + fn forbidden_with_retry_after_is_retryable() { + let mut headers = HeaderMap::new(); + headers.insert(RETRY_AFTER, HeaderValue::from_static("1")); + + assert!(is_retryable_status(StatusCode::FORBIDDEN, &headers)); + } + + #[test] + fn github_internal_graphql_error_is_retryable() { + let response = GraphQlResponse::<()> { + data: None, + errors: Some(vec![GraphQlError::new( + "Something went wrong while executing your query on 2026-05-30T14:08:34Z. Please include `1601:106F03:698CB68:1920E102:6A1AEF61` when reporting this issue.".to_owned(), + None, + None, + None, + )]), + }; + + assert!(is_retryable_graphql_response(&response)); + } + + #[test] + fn validation_graphql_error_is_not_retryable() { + let response = GraphQlResponse::<()> { + data: None, + errors: Some(vec![GraphQlError::new( + "Could not resolve to a Repository with the name 'owner/repo'.".to_owned(), + None, + None, + None, + )]), + }; + + assert!(!is_retryable_graphql_response(&response)); + } + + #[test] + fn graphql_retry_delay_uses_exponential_backoff_bounds() { + assert_eq!(graphql_retry_delay(0), Duration::from_secs(1)); + assert_eq!(graphql_retry_delay(1), Duration::from_secs(2)); + assert_eq!(graphql_retry_delay(2), Duration::from_secs(4)); + assert_eq!(graphql_retry_delay(u32::MAX), MAX_RETRY_INTERVAL); + } +} diff --git a/src/github/utils/mod.rs b/src/github/utils/mod.rs index 046ce9dad..5d8e79367 100644 --- a/src/github/utils/mod.rs +++ b/src/github/utils/mod.rs @@ -5,7 +5,6 @@ pub mod pull_request; use std::{env, fmt::Write, num::NonZeroU32}; use bon::builder; -use clap::{crate_name, crate_version}; pub use commit_title::CommitTitle; use itertools::Itertools; pub use package_path::PackagePath; @@ -21,6 +20,14 @@ const YAML_EXTENSION: &str = ".yaml"; const LOCALE_PART: &str = ".locale."; const INSTALLER_PART: &str = ".installer"; +fn is_running_via_cli() -> bool { + std::env::current_exe().ok().is_some_and(|path| { + !["bun", "deno", "node"] + .iter() + .any(|bin| path.to_string_lossy().contains(bin)) + }) +} + pub fn is_manifest_file( file_name: &str, package_identifier: &PackageIdentifier, @@ -72,55 +79,23 @@ pub fn is_manifest_file( pub fn pull_request_body( #[builder(default)] issue_resolves: &[NonZeroU32], alternative_text: Option<&str>, - created_with: Option<&str>, - created_with_url: Option<&DecodedUrl>, + _created_with: Option<&str>, + _created_with_url: Option<&DecodedUrl>, ) -> String { - const FRUITS: [&str; 16] = [ - "apple", - "banana", - "blueberries", - "cherries", - "grapes", - "green_apple", - "kiwi_fruit", - "lemon", - "mango", - "melon", - "peach", - "pear", - "pineapple", - "strawberry", - "tangerine", - "watermelon", - ]; + const EMOJIS: [&str; 10] = ["🌌", "🌠", "⭐", "✨", "🌟", "☄️", "🚀", "🛰️", "🌠", "🔭"]; let mut body = String::new(); if let Some(alternative_text) = alternative_text { let _ = writeln!(body, "### {alternative_text}"); } else { let mut rng = rand::rng(); + let emoji = EMOJIS[rng.random_range(0..EMOJIS.len())]; - let emoji = if rng.random_ratio(1, 50) { - FRUITS[rng.random_range(0..FRUITS.len())] - } else { - "rocket" - }; - - body.push_str("### Pull request has been created with "); - - if let (Some(tool_name), Some(tool_url)) = (created_with, created_with_url) { - let _ = write!(body, "[{tool_name}]({tool_url})"); + if is_running_via_cli() { + let _ = write!(body, "Created by {emoji} Anthelion."); } else { - let _ = write!( - body, - "[{}]({}) v{}", - crate_name!(), - env!("CARGO_PKG_REPOSITORY"), - crate_version!() - ); + let _ = write!(body, "Automatically updated by {emoji} Anthelion."); } - - let _ = writeln!(body, " :{emoji}:"); } if !issue_resolves.is_empty() { @@ -130,6 +105,34 @@ pub fn pull_request_body( } } + if env::var("GITHUB_ACTIONS").is_ok_and(|v| v == "true") { + if !body.ends_with('\n') { + body.push('\n'); + } + body.push('\n'); + let _ = writeln!(body, "
"); + let _ = writeln!(body, "Debug"); + let _ = writeln!(body); + + if let (Ok(repository), Ok(run_id)) = + (env::var("GITHUB_REPOSITORY"), env::var("GITHUB_RUN_ID")) + { + let server_url = + env::var("GITHUB_SERVER_URL").unwrap_or_else(|_| "https://github.com".to_owned()); + let _ = writeln!( + body, + "Run URL: {server_url}/{repository}/actions/runs/{run_id}" + ); + } + let _ = writeln!( + body, + "Anthelion komac SHA: {}", + option_env!("GITHUB_SHA").unwrap_or("N/A") + ); + + let _ = writeln!(body, "
"); + } + body } diff --git a/src/github/utils/package_path.rs b/src/github/utils/package_path.rs index 3dead372c..621214486 100644 --- a/src/github/utils/package_path.rs +++ b/src/github/utils/package_path.rs @@ -13,6 +13,7 @@ impl PackagePath { identifier: &PackageIdentifier, version: Option<&PackageVersion>, manifest_type: Option<&ManifestTypeWithLocale>, + font: bool, ) -> Self { let first_character = identifier.as_str().chars().next().map_or_else( || unreachable!("Package identifiers cannot be empty"), @@ -23,7 +24,8 @@ impl PackagePath { ); // manifests/p - let mut result = format!("manifests/{first_character}"); + let root = if font { "fonts" } else { "manifests" }; + let mut result = format!("{root}/{first_character}"); // manifests/p/Package/Identifier for part in identifier.as_str().split('.') { @@ -109,8 +111,18 @@ mod tests { let identifier = identifier.parse::().unwrap(); let version = version.and_then(|version| version.parse().ok()); assert_eq!( - PackagePath::new(&identifier, version.as_ref(), manifest_type.as_ref()).as_str(), + PackagePath::new(&identifier, version.as_ref(), manifest_type.as_ref(), false).as_str(), expected ) } + + #[test] + fn font_package_path_uses_fonts_root() { + let identifier = "Package.Font".parse::().unwrap(); + + assert_eq!( + PackagePath::new(&identifier, None, None, true).as_str(), + "fonts/p/Package/Font" + ); + } } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 000000000..4298a8b3c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,19 @@ +#![allow(dead_code)] + +mod analysis; +mod anthelion; +mod commands; +mod download; +mod download_file; +mod editor; +mod github; +mod manifests; +mod match_installers; +mod prompts; +mod read; +mod terminal; +mod token; +mod traits; +mod update_state; + +pub use anthelion::*; diff --git a/src/main.rs b/src/main.rs index c82049381..0ddea5e2a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ use crate::{ compare_installers::CompareInstallers, complete::Complete, list_versions::ListVersions, + new_locale::NewLocale, new_version::NewVersion, remove_dead_versions::RemoveDeadVersions, remove_version::RemoveVersion, @@ -52,6 +53,7 @@ async fn main() -> Result<()> { match Cli::parse().command { Commands::New(new_version) => new_version.run().await, + Commands::NewLocale(new_locale) => new_locale.run().await, Commands::Update(update_version) => update_version.run().await, Commands::AutoUpdate(autoupdate) => autoupdate.run().await, Commands::Cleanup(cleanup) => cleanup.run().await, @@ -63,7 +65,13 @@ async fn main() -> Result<()> { Commands::List(list_versions) => list_versions.run().await, Commands::Show(show_version) => show_version.run().await, Commands::Sync(sync_fork) => sync_fork.run().await, - Commands::Complete(complete) => complete.run(), + Commands::Complete(complete) => complete.run_with(|shell| { + use clap::CommandFactory; + use clap_complete::generate; + let mut command = Cli::command(); + let command_name = command.get_name().to_owned(); + generate(shell, &mut command, command_name, &mut anstream::stdout()); + }), Commands::Analyze(analyse) => analyse.run(), Commands::CompareInstallers(compare) => compare.run().await, Commands::RemoveDeadVersions(remove_dead_versions) => remove_dead_versions.run().await, @@ -113,6 +121,7 @@ struct Cli { #[derive(Subcommand)] enum Commands { New(Box), // Comparatively large so boxed to store on the heap + NewLocale(Box), // Comparatively large so boxed to store on the heap Update(Box), // Comparatively large so boxed to store on the heap AutoUpdate(AutoUpdate), Remove(RemoveVersion), diff --git a/src/manifests/mod.rs b/src/manifests/mod.rs index 47ed05346..70d16990a 100644 --- a/src/manifests/mod.rs +++ b/src/manifests/mod.rs @@ -107,6 +107,13 @@ pub fn print_manifest(lock: &mut AutoStream>, manifest: &str } } +pub fn to_yaml_string(value: &T) -> serde_yaml::Result +where + T: Serialize, +{ + serde_yaml::to_string(value) +} + pub fn build_manifest_string( manifest: &T, created_with: Option<&str>, @@ -121,7 +128,7 @@ where let _ = writeln!(result, "{} v{}", crate_name!(), crate_version!()); let _ = writeln!(result, "# yaml-language-server: $schema={}", T::SCHEMA); let _ = writeln!(result); - let _ = write!(result, "{}", serde_yaml::to_string(manifest)?); + let _ = write!(result, "{}", to_yaml_string(manifest)?); Ok(convert_to_crlf(&result).into_owned()) } diff --git a/src/match_installers.rs b/src/match_installers.rs index 9f5a46b38..8bff69186 100644 --- a/src/match_installers.rs +++ b/src/match_installers.rs @@ -1,10 +1,92 @@ use std::collections::HashMap; -use camino::Utf8Path; -use winget_types::installer::{Architecture, Installer, Scope, VALID_FILE_EXTENSIONS}; +use winget_types::{ + installer::{Architecture, ElevationRequirement, Installer, Scope}, + utils::ValidFileExtensions, +}; + +fn locale_score(previous_installer: &Installer, new_installer: &Installer) -> f64 { + let Some(previous_locale) = previous_installer.locale.as_ref() else { + return 0.0; + }; + + // Compare directly if installer analysis can detect a locale + if let Some(new_locale) = new_installer.locale.as_ref() { + if new_locale == previous_locale { + return 3.0; + } + if new_locale.language() == previous_locale.language() { + return 2.0; + } + return 0.0; + } + + // Otherwise fall back to inferring locale from the URL + let url = new_installer.url.as_str().to_ascii_lowercase(); + let url_tokens: Vec<&str> = url + .split(|c: char| !c.is_ascii_alphanumeric()) + .filter(|t| !t.is_empty()) + .collect(); + let has_token = |s: &str| url_tokens.contains(&s.to_ascii_lowercase().as_str()); + let mut score = 0.0; + + if has_token(previous_locale.language().as_str()) { + score += 2.0; + } + if let Some(script) = previous_locale.script() + && has_token(script.as_str()) + { + score += 1.0; + } + if let Some(region) = previous_locale.region() + && has_token(region.as_str()) + { + score += 1.0; + } + score +} + +fn url_similarity_score(previous_url: &str, new_url: &str) -> f64 { + let previous_url = previous_url.to_ascii_lowercase(); + let new_url = new_url.to_ascii_lowercase(); + + strsim::jaro_winkler(&previous_url, &new_url) * 3.0 +} + +fn duplicate_elevation_scope(installer: &Installer, installers: &[Installer]) -> Option { + let mut matching_installers = installers.iter().filter(|candidate| { + candidate.url == installer.url && candidate.architecture == installer.architecture + }); + let first_installer = matching_installers.next()?; + let second_installer = matching_installers.next()?; + + if matching_installers.next().is_some() + || first_installer.scope.is_some() + || second_installer.scope.is_some() + { + return None; + } + + let elevation_requirements = [ + first_installer.elevation_requirement, + second_installer.elevation_requirement, + ]; + + if !elevation_requirements.contains(&None) + || !elevation_requirements.contains(&Some(ElevationRequirement::ElevationRequired)) + { + return None; + } + + match installer.elevation_requirement { + None => Some(Scope::User), + Some(ElevationRequirement::ElevationRequired) => Some(Scope::Machine), + _ => None, + } +} pub fn match_installers( - previous_installers: Vec, + previous_installers: &[Installer], new_installers: &[Installer], ) -> HashMap { let found_architectures = new_installers @@ -24,40 +106,46 @@ pub fn match_installers( .collect::>(); previous_installers - .into_iter() - .map(|previous_installer| { - let mut max_score = 0; + .iter() + .cloned() + .map(|mut previous_installer| { + if previous_installer.scope.is_none() { + previous_installer.scope = + duplicate_elevation_scope(&previous_installer, previous_installers); + } + + let mut max_score = 0.0; let mut best_match = None; for new_installer in new_installers { let installer_url = &new_installer.url; - let mut score = 0; + let mut score = 0.0; if new_installer.architecture == previous_installer.architecture { - score += 1; + score += 1.0; } if found_architectures.get(installer_url) == Some(&previous_installer.architecture) { - score += 1; + score += 1.0; } if new_installer.r#type == previous_installer.r#type { - score += 3; + score += 3.0; } if new_installer.nested_installer_type == previous_installer.nested_installer_type { - score += 3; + score += 3.0; } if new_installer.scope == previous_installer.scope { - score += 1; + score += 1.0; } - let new_extension = Utf8Path::new(new_installer.url.as_str()) - .extension() - .filter(|extension| VALID_FILE_EXTENSIONS.contains(extension)) - .unwrap_or_default(); - let previous_extension = Utf8Path::new(previous_installer.url.as_str()) - .extension() - .filter(|extension| VALID_FILE_EXTENSIONS.contains(extension)) - .unwrap_or_default(); + score += locale_score(&previous_installer, new_installer); + score += url_similarity_score( + previous_installer.url.as_str(), + new_installer.url.as_str(), + ); + + let new_extension = ValidFileExtensions::from_url(&new_installer.url); + let previous_extension = ValidFileExtensions::from_url(&previous_installer.url); if new_extension != previous_extension { - score = 0; + score = 0.0; } let is_new_architecture = !found_architectures.is_empty() @@ -66,7 +154,8 @@ pub fn match_installers( !found_scopes.is_empty() && !found_scopes.contains_key(installer_url); if score > max_score - || (score == max_score && (is_new_architecture || is_new_scope)) + || (score.total_cmp(&max_score).is_eq() + && (is_new_architecture || is_new_scope)) || best_match.is_none() { max_score = score; @@ -83,8 +172,9 @@ pub fn match_installers( mod tests { use std::{collections::HashMap, str::FromStr}; + use rstest::rstest; use winget_types::{ - installer::{Architecture, Installer, Scope}, + installer::{Architecture, ElevationRequirement, Installer, Scope}, url::DecodedUrl, }; @@ -139,8 +229,231 @@ mod tests { (previous_machine_x64, installer_x64), ]); assert_eq!( - match_installers(previous_installers, &new_installers), + match_installers(&previous_installers, &new_installers), expected ); } + + #[rstest] + #[case::x64( + Architecture::X64, + "https://example.com/installer-1.0.exe", + "https://example.com/installer-2.0.exe" + )] + #[case::arm64( + Architecture::Arm64, + "https://example.com/installer-1.0-arm64.exe", + "https://example.com/installer-2.0-arm64.exe" + )] + fn old_scope_regression_shape_generates_scope_values_after_matching( + #[case] architecture: Architecture, + #[case] previous_url: &str, + #[case] new_url: &str, + ) { + let previous_url = DecodedUrl::from_str(previous_url).unwrap(); + let previous_installers = vec![ + Installer { + architecture, + url: previous_url.clone(), + ..Installer::default() + }, + Installer { + architecture, + url: previous_url, + elevation_requirement: Some(ElevationRequirement::ElevationRequired), + ..Installer::default() + }, + ]; + let new_installers = vec![Installer { + architecture, + url: DecodedUrl::from_str(new_url).unwrap(), + ..Installer::default() + }]; + + let mut installers = match_installers(&previous_installers, &new_installers) + .into_iter() + .map(|(previous_installer, new_installer)| { + new_installer.clone().merge_with(previous_installer) + }) + .collect::>(); + installers.sort_unstable(); + + assert_eq!( + installers + .iter() + .map(|installer| installer.scope) + .collect::>(), + vec![Some(Scope::User), Some(Scope::Machine)] + ); + } + + #[rstest] + #[case::exact_english( + "en-US", + "https://www.example.com/downloads/installer_2eae0ed.exe", + "en-US", + "https://www.example.com/downloads/installer_419b1e8.exe", + "fr-FR", + "https://www.example.com/downloads/installer_aba97fd.exe" + )] + #[case::exact_french( + "fr-FR", + "https://www.example.com/downloads/installer_8a4a542.exe", + "fr-FR", + "https://www.example.com/downloads/installer_aba97fd.exe", + "en-US", + "https://www.example.com/downloads/installer_419b1e8.exe" + )] + #[case::language_only_english( + "en-US", + "https://www.example.com/downloads/installer_2eae0ed.exe", + "en-GB", + "https://www.example.com/downloads/installer_419b1e8.exe", + "fr-CA", + "https://www.example.com/downloads/installer_aba97fd.exe" + )] + #[case::language_only_french( + "fr-FR", + "https://www.example.com/downloads/installer_8a4a542.exe", + "fr-CA", + "https://www.example.com/downloads/installer_aba97fd.exe", + "en-GB", + "https://www.example.com/downloads/installer_419b1e8.exe" + )] + fn matches_locales_by_direct_analysis( + #[case] previous_locale: &str, + #[case] previous_url: &str, + #[case] expected_locale: &str, + #[case] expected_url: &str, + #[case] competing_locale: &str, + #[case] competing_url: &str, + ) { + let previous_installer = Installer { + locale: Some(previous_locale.parse().unwrap()), + url: DecodedUrl::from_str(previous_url).unwrap(), + ..Installer::default() + }; + let expected_installer = Installer { + locale: Some(expected_locale.parse().unwrap()), + url: DecodedUrl::from_str(expected_url).unwrap(), + ..Installer::default() + }; + let competing_installer = Installer { + locale: Some(competing_locale.parse().unwrap()), + url: DecodedUrl::from_str(competing_url).unwrap(), + ..Installer::default() + }; + + assert_eq!( + match_installers( + std::slice::from_ref(&previous_installer), + &[competing_installer, expected_installer.clone()], + ), + HashMap::from([(previous_installer, expected_installer)]) + ); + } + + #[rstest] + #[case::icu_english( + "en-US", + "https://www.example.com/app-1.0.en-US.exe", + "https://www.example.com/app-2.0.en-US.exe" + )] + #[case::icu_french( + "fr-FR", + "https://www.example.com/app-1.0.fr-FR.exe", + "https://www.example.com/app-2.0.fr-FR.exe" + )] + #[case::icu_german( + "de-DE", + "https://www.example.com/app-1.0.de-DE.exe", + "https://www.example.com/app-2.0.de.exe" + )] + #[case::icu_portuguese( + "pt-BR", + "https://www.example.com/app-1.0.pt-BR.exe", + "https://www.example.com/app-2.0.pt-BR.exe" + )] + #[case::icu_simplified_chinese( + "zh-CN", + "https://www.example.com/app-1.0.zh.exe", + "https://www.example.com/app-2.0.zh.exe" + )] + #[case::icu_traditional_chinese( + "zh-TW", + "https://www.example.com/app-1.0.zh-TW.exe", + "https://www.example.com/app-2.0.zh-TW.exe" + )] + #[case::similarity_english( + "en-GB", + "https://www.example.com/downloads/app_2_8_English.exe", + "https://www.example.com/downloads/app_2_9_English.exe" + )] + #[case::similarity_french( + "fr-FR", + "https://www.example.com/downloads/app_2_8_French.exe", + "https://www.example.com/downloads/app_2_9_French.exe" + )] + #[case::similarity_italian( + "it-IT", + "https://www.example.com/downloads/app_2_8_Italian.exe", + "https://www.example.com/downloads/app_2_9_Italian.exe" + )] + #[case::similarity_portuguese( + "pt-PT", + "https://www.example.com/downloads/app_2_8_Portuguese.exe", + "https://www.example.com/downloads/app_2_9_Portuguese.exe" + )] + #[case::similarity_russian( + "ru-RU", + "https://www.example.com/downloads/app_2_8_Russian.exe", + "https://www.example.com/downloads/app_2_9_Russian.exe" + )] + #[case::similarity_spanish( + "es-AR", + "https://www.example.com/downloads/app_2_8_Espanol.exe", + "https://www.example.com/downloads/app_2_9_Espanol.exe" + )] + #[case::similarity_turkish( + "tr-TR", + "https://www.example.com/downloads/app_2_8_Turkish.exe", + "https://www.example.com/downloads/app_2_9_Turkish.exe" + )] + #[case::similarity_german( + "de-DE", + "https://www.example.com/downloads/app_2_8_Deutsch.exe", + "https://www.example.com/downloads/app_2_9_Deutsch.exe" + )] + #[case::similarity_simplified_chinese( + "zh-CN", + "https://www.example.com/downloads/app_2_8_SCN.exe", + "https://www.example.com/downloads/app_2_9_SCN.exe" + )] + fn matches_locale_by_url( + #[case] locale: &str, + #[case] previous_url: &str, + #[case] expected_url: &str, + ) { + let previous_installer = Installer { + locale: Some(locale.parse().unwrap()), + url: DecodedUrl::from_str(previous_url).unwrap(), + ..Installer::default() + }; + let expected_installer = Installer { + url: DecodedUrl::from_str(expected_url).unwrap(), + ..Installer::default() + }; + let competing_installer = Installer { + url: DecodedUrl::from_str("https://www.example.com/downloads/installer_419b1e8.exe") + .unwrap(), + ..Installer::default() + }; + assert_eq!( + match_installers( + std::slice::from_ref(&previous_installer), + &[competing_installer, expected_installer.clone()], + ), + HashMap::from([(previous_installer, expected_installer)]) + ); + } } diff --git a/src/prompts/list.rs b/src/prompts/list.rs index 2388ee0ac..978a5a0dc 100644 --- a/src/prompts/list.rs +++ b/src/prompts/list.rs @@ -48,13 +48,22 @@ pub fn list_prompt() -> color_eyre::Result> where T: FromStr + ListPrompt + Ord, ::Err: Display, +{ + list_prompt_with_help::(T::HELP_MESSAGE) +} + +pub fn list_prompt_with_help(help_message: U) -> color_eyre::Result> +where + T: FromStr + ListPrompt + Ord, + ::Err: Display, + U: AsRef, { if *CI { return Ok(BTreeSet::new()); } const DELIMITERS: [char; 2] = [' ', ',']; let items = Text::new(&format!("{}:", T::PLURAL_NAME)) - .with_help_message(T::HELP_MESSAGE) + .with_help_message(help_message.as_ref()) .with_validator(|input: &str| { let items = input .split(|char| DELIMITERS.contains(&char)) diff --git a/src/prompts/text.rs b/src/prompts/text.rs index 52ae8ff9f..9a2a8e373 100644 --- a/src/prompts/text.rs +++ b/src/prompts/text.rs @@ -99,11 +99,37 @@ where T: FromStr + TextPrompt, ::Err: Display + Debug + Sync + Send + 'static, U: AsRef, +{ + let default = default.as_ref().map(U::as_ref); + optional_prompt_inner(parameter, default, None) +} + +pub fn optional_prompt_with_help( + parameter: Option, + help_message: Option, +) -> InquireResult> +where + T: FromStr + TextPrompt, + ::Err: Display + Debug + Sync + Send + 'static, + U: AsRef, +{ + let help_message = help_message.as_ref().map(U::as_ref); + optional_prompt_inner(parameter, None, help_message) +} + +fn optional_prompt_inner( + parameter: Option, + default: Option<&str>, + help_message: Option<&str>, +) -> InquireResult> +where + T: FromStr + TextPrompt, + ::Err: Display + Debug + Sync + Send + 'static, { if let Some(value) = parameter { Ok(Some(value)) } else if *CI { - Ok(default.as_ref().and_then(|d| d.as_ref().parse::().ok())) + Ok(default.and_then(|d| d.parse::().ok())) } else { let message = format!("{}:", ::NAME); let mut prompt = Text::new(&message).with_validator(|input: &str| { @@ -116,13 +142,12 @@ where } } }); - if let Some(help_message) = T::HELP_MESSAGE { + if let Some(help_message) = help_message.or(T::HELP_MESSAGE) { prompt = prompt.with_help_message(help_message); } if let Some(placeholder) = T::PLACEHOLDER { prompt = prompt.with_placeholder(placeholder); } - let default = default.as_ref().map(U::as_ref); if let Some(default) = default { prompt = prompt.with_default(default); } diff --git a/src/token.rs b/src/token.rs index bc68cc752..c23ff36aa 100644 --- a/src/token.rs +++ b/src/token.rs @@ -5,14 +5,19 @@ use color_eyre::eyre::Result; use inquire::{InquireError, Password, error::InquireResult, validator::Validation}; use keyring_core::Entry; use reqwest::{ - Client, StatusCode, + Client as ReqwestClient, StatusCode, header::{AUTHORIZATION, DNT, HeaderMap, HeaderValue, USER_AGENT}, }; +use reqwest_middleware::ClientWithMiddleware; use secrecy::{ExposeSecret, SecretString}; use thiserror::Error; use tokio::runtime::Handle; -use crate::{commands::utils::environment::CI, prompts::handle_inquire_error}; +use crate::{ + commands::utils::environment::CI, + github::retry::{client as retrying_client, is_connect_error}, + prompts::handle_inquire_error, +}; const GITHUB_API_ENDPOINT: &str = "https://api.github.com/octocat"; @@ -33,6 +38,8 @@ pub enum TokenError { #[error(transparent)] Request(#[from] reqwest::Error), #[error(transparent)] + RequestMiddleware(#[from] reqwest_middleware::Error), + #[error(transparent)] Inquire(#[from] InquireError), } @@ -49,9 +56,11 @@ impl TokenManager { // * In CI: if no token or if stored token is invalid -> error (never prompt). // * Interactive: if no stored token or stored token is invalid -> prompt and store. - let client = Client::builder() - .default_headers(default_headers(None)) - .build()?; + let client = retrying_client( + ReqwestClient::builder() + .default_headers(default_headers(None)) + .build()?, + ); let token_passed = token.is_some(); @@ -103,7 +112,7 @@ impl TokenManager { #[builder] pub fn prompt( - client: &Client, + client: &ClientWithMiddleware, #[builder(default = "Enter a GitHub token")] message: &str, ) -> InquireResult { tokio::task::block_in_place(|| { @@ -125,7 +134,7 @@ impl TokenManager { }) } - pub async fn validate(client: &Client, token: &str) -> Result<(), TokenError> { + pub async fn validate(client: &ClientWithMiddleware, token: &str) -> Result<(), TokenError> { match client .get(GITHUB_API_ENDPOINT) .bearer_auth(token) @@ -137,7 +146,7 @@ impl TokenManager { _ => Ok(()), }, Err(error) => { - if error.is_connect() { + if is_connect_error(&error) { Err(TokenError::FailedToConnect) } else { Err(error.into()) diff --git a/src/traits/mod.rs b/src/traits/mod.rs index e450c4b9b..268c70f38 100644 --- a/src/traits/mod.rs +++ b/src/traits/mod.rs @@ -1,7 +1,7 @@ mod ascii_ext; pub mod name; pub mod path; -use std::{mem, sync::LazyLock}; +use std::{cell::Cell, mem, sync::LazyLock}; pub use ascii_ext::AsciiExt; use html2text::render::{TaggedLine, TextDecorator}; @@ -9,7 +9,7 @@ pub use name::Name; use regex::Regex; use winget_types::{ Manifest, ManifestVersion, PackageVersion, - installer::Architecture, + installer::{Architecture, Installer, InstallerManifest}, locale::{DefaultLocaleManifest, LocaleManifest, ReleaseNotes}, url::ReleaseNotesUrl, }; @@ -69,7 +69,33 @@ impl IntoWingetArchitecture for PE { } } -struct GitHubHtmlDecorator; +pub trait InstallerManifestExt { + fn inherit_manifest_properties(&self) -> impl Iterator + '_; +} + +impl InstallerManifestExt for InstallerManifest { + fn inherit_manifest_properties(&self) -> impl Iterator + '_ { + self.installers.iter().cloned().map(|mut installer| { + if self.r#type.is_some() { + installer.r#type = self.r#type; + } + if self.nested_installer_type.is_some() { + installer.nested_installer_type = self.nested_installer_type; + } + if self.scope.is_some() { + installer.scope = self.scope; + } + installer + }) + } +} + +#[derive(Default)] +struct GitHubHtmlDecorator { + seen_header: Cell, +} + +const HEADER_MARKER: &str = "__KOMAC_HEADER__ "; impl TextDecorator for GitHubHtmlDecorator { type Annotation = (); @@ -118,12 +144,13 @@ impl TextDecorator for GitHubHtmlDecorator { fn decorate_preformat_cont(&self) -> Self::Annotation {} - fn decorate_image(&mut self, _src: &str, title: &str) -> (String, Self::Annotation) { - (title.to_string(), ()) + fn decorate_image(&mut self, _src: &str, _title: &str) -> (String, Self::Annotation) { + (String::new(), ()) } fn header_prefix(&self, _level: usize) -> String { - String::new() + self.seen_header.set(true); + String::from(HEADER_MARKER) } fn quote_prefix(&self) -> String { @@ -139,7 +166,7 @@ impl TextDecorator for GitHubHtmlDecorator { } fn make_subblock_decorator(&self) -> Self { - Self + Self::default() } fn finalise(&mut self, _links: Vec) -> Vec> { @@ -153,15 +180,58 @@ pub trait FromHtml { Self: Sized; } +fn add_section_spacing(text: &str) -> String { + let mut output = String::with_capacity(text.len() + 32); + let mut seen_header = false; + let mut previous_line_blank = true; + + for line in text.lines() { + let is_blank = line.trim().is_empty(); + if let Some(header) = line.strip_prefix(HEADER_MARKER) { + if seen_header && !previous_line_blank { + output.push('\n'); + } + output.push_str(header); + output.push('\n'); + seen_header = true; + previous_line_blank = false; + continue; + } + + output.push_str(line); + output.push('\n'); + previous_line_blank = is_blank; + } + + output +} + impl FromHtml for ReleaseNotes { fn from_html(html: &Html) -> Option { // Strings that have whitespace before newlines get escaped and treated as literal strings // in YAML so this regex identifies any amount of whitespace and duplicate newlines static NEWLINE_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"\s+\n").unwrap()); + // GitHub release notes often end with compare lines such as: + // "Full Changelog: v2.14.0...v2.15.0" or "Full Changelog: 2.14.0...2.15.0". + // Remove any line containing a tag/version compare range. + static COMPARE_RANGE_LINE_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"(?im)^[^\n]*\b[0-9A-Za-z][0-9A-Za-z._+-]*\.\.\.[0-9A-Za-z][0-9A-Za-z._+-]*[^\n]*\n?") + .unwrap() + }); - html2text::from_read_with_decorator(html.as_bytes(), usize::MAX, GitHubHtmlDecorator) - .ok() - .and_then(|text| Self::new(NEWLINE_REGEX.replace_all(&text, "\n")).ok()) + html2text::from_read_with_decorator( + html.as_bytes(), + usize::MAX, + GitHubHtmlDecorator::default(), + ) + .ok() + .and_then(|text| { + let normalized_text = COMPARE_RANGE_LINE_REGEX.replace_all(&text, ""); + let normalized_text = NEWLINE_REGEX.replace_all(&normalized_text, "\n"); + let normalized_text = add_section_spacing(&normalized_text); + let normalized_text = normalized_text.replace("\\r\\n", "\n").replace("\\n", "\n"); + Self::new(normalized_text.trim()).ok() + }) } } diff --git a/src/traits/path.rs b/src/traits/path.rs index 17e88875e..f7d233318 100644 --- a/src/traits/path.rs +++ b/src/traits/path.rs @@ -9,6 +9,16 @@ pub trait NormalizePath { fn normalize(&self) -> Utf8PathBuf; } +pub trait LowercaseExtension { + fn lowercase_extension(&self) -> Utf8PathBuf; +} + +impl LowercaseExtension for Utf8PathBuf { + fn lowercase_extension(&self) -> Utf8PathBuf { + self.with_extension(self.extension().unwrap_or_default().to_ascii_lowercase()) + } +} + impl NormalizePath for Utf8Path { fn normalize(&self) -> Utf8PathBuf { let mut components = self.components().peekable();