Merge pull request #1579 from fhgffy:fix-bundled-manpage-install

PiperOrigin-RevId: 1005429944
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 4e5d64d..cb9bd13 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -14,3 +14,5 @@
       everything:
         patterns:
           - "*" # Everything
+    cooldown:
+      default-days: 7
diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml
index e436038..2797771 100644
--- a/.github/workflows/build_test.yml
+++ b/.github/workflows/build_test.yml
@@ -210,13 +210,22 @@
             os: windows-latest
 
     env:
+      BAZEL_PROJECT: ${{ matrix.bazel_project }}
+      C_COMPILER: ${{ matrix.c_compiler || '' }}
+      C_FLAGS: ${{ matrix.c_flags || '' }}
       CC: ${{ matrix.c_compiler || 'gcc' }}
+      CMAKE_ARGS: ${{ matrix.cmake_args || '' }}
+      CMAKE_CONFIG: ${{ matrix.cmake_config || '' }}
+      CXX_COMPILER: ${{ matrix.cxx_compiler || '' }}
       CXX: ${{ matrix.cxx_compiler || 'gcc' }}
+      EXTRA_PACKAGES: ${{ matrix.extra_apt_pkgs || '' }}
+      SANITIZER: ${{ matrix.sanitizer || '' }}
+      SKIP_TESTS: ${{ matrix.skip_tests || '' }}
 
     steps:
 
     - name: Harden Runner
-      uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
+      uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
       with:
         egress-policy: audit
 
@@ -224,12 +233,11 @@
       if: ${{ runner.os == 'Linux' }}
       # Already installed: bazel, clang{13-15}, cmake, gcc{9.5-13.1}, java{8,11,17,21}, maven, python{3.10}
       run: |
-        EXTRA_PACKAGES="${{ matrix.extra_apt_pkgs || '' }}"
         sudo apt update
         sudo apt install -y ${EXTRA_PACKAGES}
 
     - name: Checkout the source
-      uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+      uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
       with:
         submodules: false
         fetch-depth: 1
@@ -239,20 +247,20 @@
       if: ${{ matrix.build_system == 'cmake' }}
       run: |
         export ASAN_OPTIONS=detect_leaks=0
-        declare -a CMAKE_OPTIONS=(${{ matrix.cmake_args || '' }})
+        declare -a CMAKE_OPTIONS=(${CMAKE_ARGS})
         CMAKE_OPTIONS+=("-DCMAKE_VERBOSE_MAKEFILE=ON")
-        [ ! -z '${{ matrix.c_compiler || '' }}' ] && CMAKE_OPTIONS+=(-DCMAKE_C_COMPILER='${{ matrix.c_compiler }}')
-        [ ! -z '${{ matrix.cxx_compiler || '' }}' ] && CMAKE_OPTIONS+=(-DCMAKE_CXX_COMPILER='${{ matrix.cxx_compiler }}')
-        [ ! -z '${{ matrix.sanitizer || '' }}' ] && CMAKE_OPTIONS+=(-DENABLE_SANITIZER='${{ matrix.sanitizer }}')
+        [ -n "${C_COMPILER}" ] && CMAKE_OPTIONS+=(-DCMAKE_C_COMPILER="${C_COMPILER}")
+        [ -n "${CXX_COMPILER}" ] && CMAKE_OPTIONS+=(-DCMAKE_CXX_COMPILER="${CXX_COMPILER}")
+        [ -n "${SANITIZER}" ] && CMAKE_OPTIONS+=(-DENABLE_SANITIZER="${SANITIZER}")
         declare -a CMAKE_BUILD_OPTIONS=()
-        [ ! -z '${{ matrix.cmake_config || '' }}' ] && CMAKE_BUILD_OPTIONS+=(--config '${{ matrix.cmake_config }}')
+        [ -n "${CMAKE_CONFIG}" ] && CMAKE_BUILD_OPTIONS+=(--config "${CMAKE_CONFIG}")
         declare -a CMAKE_TEST_OPTIONS=()
-        [ ! -z '${{ matrix.cmake_config || '' }}' ] && CMAKE_TEST_OPTIONS+=(-C '${{ matrix.cmake_config }}')
+        [ -n "${CMAKE_CONFIG}" ] && CMAKE_TEST_OPTIONS+=(-C "${CMAKE_CONFIG}")
 
-        cmake -B out . ${CMAKE_OPTIONS[*]} -DCMAKE_C_FLAGS='${{ matrix.c_flags || '' }}'
+        cmake -B out . ${CMAKE_OPTIONS[*]} -DCMAKE_C_FLAGS="${C_FLAGS}"
         cmake --build out ${CMAKE_BUILD_OPTIONS[*]}
         cd out
-        [ ! -z '${{ matrix.skip_tests || '' }}' ] || ctest ${CMAKE_TEST_OPTIONS[*]}
+        [ -n "${SKIP_TESTS}" ] || ctest ${CMAKE_TEST_OPTIONS[*]}
         cd ..
 
     - name: Test bundled CMake installation
@@ -283,7 +291,7 @@
     - name: Build with Bazel
       if: ${{ matrix.build_system == 'bazel' }}
       run: |
-        cd ${GITHUB_WORKSPACE}/${{ matrix.bazel_project }}
+        cd ${GITHUB_WORKSPACE}/${BAZEL_PROJECT}
         bazelisk build -c opt ...:all --java_runtime_version=remotejdk_21
 
     - name: Fix symlinks for Bazel (Windows)
@@ -294,7 +302,7 @@
         import os
         import os.path
         from shutil import copyfile
-        os.chdir('${{ matrix.bazel_project }}')
+        os.chdir(os.environ['BAZEL_PROJECT'])
         print('Searching for manifests in ' + os.getcwd())
         matches = []
         for root, dirnames, filenames in os.walk('bazel-bin\\org\\brotli'):
@@ -327,7 +335,7 @@
     - name: Test with Bazel
       if: ${{ matrix.build_system == 'bazel' }}
       run: |
-        cd ${GITHUB_WORKSPACE}/${{ matrix.bazel_project }}
+        cd ${GITHUB_WORKSPACE}/${BAZEL_PROJECT}
         bazelisk query "tests(...)" --output=label > ${RUNNER_TEMP}/tests.lst
         [ -s ${RUNNER_TEMP}/tests.lst ] && bazelisk test -c opt ...:all --java_runtime_version=remotejdk_21
         bazelisk clean
@@ -342,7 +350,7 @@
         # cd integration
         # mvn -B verify
 
-    - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
+    - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
       if: ${{ matrix.build_system == 'python' }}
       with:
         python-version: ${{ matrix.python_version }}
@@ -356,13 +364,39 @@
         python setup.py build_ext --inplace
         pytest ./python/tests
 
+  build_python_wheels:
+    name: Python 3.15 wheels (${{ matrix.os }})
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest, windows-11-arm]
+    steps:
+      - name: Harden Runner
+        uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920
+        with:
+          egress-policy: audit
+      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
+        with:
+          persist-credentials: false
+      - uses: pypa/cibuildwheel@adba99c781d3746d38b3a09b1cf797965bcb7fe6
+        env:
+          CIBW_BUILD: cp315-*
+          CIBW_ARCHS_MACOS: x86_64 arm64
+          CIBW_TEST_REQUIRES: pytest
+          CIBW_TEST_COMMAND: python -m pytest {project}/python/tests
+      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
+        with:
+          name: python315-wheels-${{ matrix.os }}
+          path: wheelhouse/*.whl
+
   build_test_dotnet:
     name: Build and test with .NET
     runs-on: ubuntu-latest
     steps:
 
     - name: Checkout the source
-      uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+      uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
       with:
         submodules: false
         fetch-depth: 1
diff --git a/.github/workflows/build_test_wasm.yml b/.github/workflows/build_test_wasm.yml
index 12d69b7..8c4b766 100644
--- a/.github/workflows/build_test_wasm.yml
+++ b/.github/workflows/build_test_wasm.yml
@@ -35,18 +35,18 @@
     steps:
 
     - name: Harden Runner
-      uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
+      uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
       with:
         egress-policy: audit
 
-    - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
       with:
         submodules: true
         fetch-depth: 1
         persist-credentials: false
 
     - name: Install node
-      uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+      uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
       with:
         node-version: ${{env.NODE_VERSION}}
 
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 2256bec..de43955 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -22,9 +22,9 @@
     runs-on: 'ubuntu-latest'
     timeout-minutes: 360
     permissions:
-      actions: read
-      contents: read
-      security-events: write
+      actions: read # read the workflow files and repository action logs
+      contents: read # check out the repository code
+      security-events: write # upload the generated SARIF security findings
 
     strategy:
       fail-fast: false
@@ -35,18 +35,18 @@
     steps:
 
     - name: Harden Runner
-      uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
+      uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
       with:
         egress-policy: audit
 
     - name: Checkout repository
-      uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+      uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
       with:
         persist-credentials: false
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
+      uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
       with:
         languages: ${{ matrix.language }}
         # CodeQL is currently crashing on files with large lists:
@@ -58,7 +58,7 @@
 
     - if: matrix.language == 'cpp'
       name: Build CPP
-      uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
+      uses: github/codeql-action/autobuild@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
 
     - if: matrix.language == 'cpp' || matrix.language == 'java'
       name: Build Java
@@ -68,7 +68,7 @@
 
     - if: matrix.language == 'javascript'
       name: Build JS
-      uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
+      uses: github/codeql-action/autobuild@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
 
     - if: matrix.language == 'cpp' || matrix.language == 'python'
       name: Build Python
@@ -78,7 +78,7 @@
         python setup.py build_ext
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
+      uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
       with:
         category: "/language:${{matrix.language}}"
         ref: "${{ github.ref != 'master' && github.ref || '/refs/heads/master' }}"
diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml
index 1cde6cf..43d701f 100644
--- a/.github/workflows/fuzz.yml
+++ b/.github/workflows/fuzz.yml
@@ -17,31 +17,33 @@
   cancel-in-progress: ${{ github.event_name == 'pull_request' }}
 
 jobs:
-  Fuzzing:
+  fuzzing:
+    name: Fuzzing
     runs-on: ubuntu-latest
     steps:
 
     - name: Harden Runner
-      uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
+      uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
       with:
         egress-policy: audit
 
-    - name: Build Fuzzers
-      uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@3e6a7fd7bcd631647ab9beed1fe0897498e6af39 # 22.09.2025
-      with:
-        oss-fuzz-project-name: 'brotli'
-        dry-run: false
+    # Security folks are sabotaging security by not providing tags; how fascinating!
+    # - name: Build Fuzzers
+    #   uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@3e6a7fd7bcd631647ab9beed1fe0897498e6af39 # 22.09.2025
+    #   with:
+    #     oss-fuzz-project-name: 'brotli'
+    #     dry-run: false
 
-    - name: Run Fuzzers
-      uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@3e6a7fd7bcd631647ab9beed1fe0897498e6af39 # 22.09.2025
-      with:
-        oss-fuzz-project-name: 'brotli'
-        fuzz-seconds: 600
-        dry-run: false
+    # - name: Run Fuzzers
+    #   uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@3e6a7fd7bcd631647ab9beed1fe0897498e6af39 # 22.09.2025
+    #   with:
+    #     oss-fuzz-project-name: 'brotli'
+    #     fuzz-seconds: 600
+    #     dry-run: false
 
-    - name: Upload Crash
-      uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
-      if: failure()
-      with:
-        name: artifacts
-        path: ./out/artifacts
+    # - name: Upload Crash
+    #   uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+    #   if: failure()
+    #   with:
+    #     name: artifacts
+    #     path: ./out/artifacts
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 8d4237f..ce7f986 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -30,12 +30,12 @@
     steps:
 
     - name: Harden Runner
-      uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
+      uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
       with:
         egress-policy: audit
 
     - name: Checkout repository
-      uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+      uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
       with:
         persist-credentials: false
 
@@ -52,7 +52,7 @@
     - name: Audit workflows
       run: |
         eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
-        zizmor ./.github/workflows
+        zizmor --pedantic ./.github/workflows
 
     - name: Lint Python code
       run: |
diff --git a/.github/workflows/publish_to_bcr.yaml b/.github/workflows/publish_to_bcr.yaml
index e69d67a..17b95fd 100644
--- a/.github/workflows/publish_to_bcr.yaml
+++ b/.github/workflows/publish_to_bcr.yaml
@@ -13,17 +13,16 @@
         required: true
         type: string
 
+permissions:
+  contents: read
+
 jobs:
   publish:
-    permissions:
-      contents: write
-      id-token: write
-      attestations: write
-    uses: bazel-contrib/publish-to-bcr/.github/workflows/publish.yaml@ad6879fd015a01c3e3b1b318fe3c061f928ac2fb # 2026.07.03
+    uses: bazel-contrib/publish-to-bcr/.github/workflows/publish.yaml@fa5bb3803983c621c90721669ee7857afe139836 # v1.5.1
     with:
       tag_name: ${{ inputs.tag_name }}
       registry_fork: ${{ inputs.registry_fork }}
-      # Set to false since not using the release_ruleset workflow
-      attest: false
+      attest: false # requires {attestations, contents, id-token} write
+      download_default_release_artifacts: false
     secrets:
       publish_token: ${{ secrets.BCR_PUBLISH_TOKEN }}
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 19ae896..8fd3bd8 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -57,6 +57,9 @@
             runs_on: windows-11-arm
 
     env:
+      ARCH: ${{ matrix.arch }}
+      BUILD_SHARED_LIBS: ${{ matrix.build_shared_libs }}
+      TRIPLET: ${{ matrix.triplet }}
       VCPKG_VERSION: '2022.11.14'
       VCPKG_ROOT: vcpkg
       VCPKG_DISABLE_METRICS: 1
@@ -64,12 +67,12 @@
     steps:
 
     - name: Harden Runner
-      uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
+      uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
       with:
         egress-policy: audit
 
     - name: Checkout the source
-      uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+      uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
       with:
         submodules: false
         fetch-depth: 1
@@ -80,7 +83,7 @@
       id: cache-vcpkg
       with:
         path: vcpkg
-        key: release-${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.triplet }}
+        key: release-${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ env.TRIPLET }}
         # TODO(eustas): remove when https://github.com/zizmorcore/zizmor/issues/2071 is fixed
         lookup-only: ${{ startsWith(github.ref, 'refs/tags/') }}
 
@@ -88,7 +91,7 @@
       if: github.event_name == 'release' || steps.cache-vcpkg.outputs.cache-hit != 'true'
       shell: 'powershell'
       run: |
-        Invoke-WebRequest -Uri "https://github.com/microsoft/vcpkg/archive/refs/tags/${{ env.VCPKG_VERSION }}.zip" -OutFile "vcpkg.zip"
+        Invoke-WebRequest -Uri "https://github.com/microsoft/vcpkg/archive/refs/tags/${VCPKG_VERSION}.zip" -OutFile "vcpkg.zip"
 
     - name: Bootstrap vcpkg
       if: github.event_name == 'release' || steps.cache-vcpkg.outputs.cache-hit != 'true'
@@ -105,13 +108,13 @@
       run: |
         set -x
         mkdir out
-        cmake -Bout -H. ${{ matrix.arch }} \
+        cmake -Bout -H. ${ARCH} \
           -DBUILD_TESTING=OFF \
-          -DBUILD_SHARED_LIBS=${{ matrix.build_shared_libs }} \
+          -DBUILD_SHARED_LIBS=${BUILD_SHARED_LIBS} \
           -DCMAKE_BUILD_TYPE=Release \
           -DCMAKE_INSTALL_PREFIX=`pwd`/prefix \
           -DCMAKE_TOOLCHAIN_FILE=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \
-          -DVCPKG_TARGET_TRIPLET=${{ matrix.triplet }} \
+          -DVCPKG_TARGET_TRIPLET=${TRIPLET} \
         #
 
     - name: Build
@@ -131,10 +134,10 @@
       shell: 'powershell'
       run: |
         Compress-Archive -Path prefix\bin\* `
-          -DestinationPath brotli-${{matrix.triplet}}.zip
+          -DestinationPath brotli-${TRIPLET}.zip
 
     - name: Upload package
-      uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+      uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
       with:
         name: brotli-${{matrix.triplet}}
         path: brotli-${{matrix.triplet}}.zip
@@ -149,12 +152,12 @@
     steps:
 
     - name: Harden Runner
-      uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
+      uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
       with:
         egress-policy: audit
 
     - name: Checkout the source
-      uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+      uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
       with:
         submodules: false
         fetch-depth: 1
@@ -165,7 +168,7 @@
         tar cvfJ testdata.txz tests/testdata
 
     - name: Upload archive
-      uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+      uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
       with:
         name: testdata
         path: testdata.txz
@@ -177,18 +180,18 @@
     if: github.event_name == 'release'
     runs-on: [ubuntu-latest]
     permissions:
-      contents: write
+      contents: write # upload assets
 
     steps:
     - name: Checkout the source
-      uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+      uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
       with:
         submodules: false
         fetch-depth: 1
         persist-credentials: false
 
     - name: Download all artifacts
-      uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+      uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
       with:
         path: release_assets
         merge-multiple: true
@@ -210,12 +213,12 @@
     steps:
 
     - name: Harden Runner
-      uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
+      uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
       with:
         egress-policy: audit
 
     - name: Checkout the source
-      uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+      uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
       with:
         submodules: false
         fetch-depth: 1
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index 5a48434..db313eb 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -15,8 +15,8 @@
   push:
     branches: [ "master" ]
 
-# Declare default permissions as read only.
-permissions: read-all
+permissions:
+  contents: read
 
 concurrency:
   group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
@@ -27,27 +27,22 @@
     name: Scorecard analysis
     runs-on: ubuntu-latest
     permissions:
-      # Needed to upload the results to code-scanning dashboard.
-      security-events: write
-      # Needed to publish results and get a badge (see publish_results below).
-      id-token: write
-      # Uncomment the permissions below if installing in a private repository.
-      # contents: read
-      # actions: read
+      security-events: write # upload the results to code-scanning dashboard
+      id-token: write # publish results and get a badge
 
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
+        uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
         with:
           egress-policy: audit
 
       - name: "Checkout code"
-        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
         with:
           persist-credentials: false
 
       - name: "Run analysis"
-        uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
+        uses: ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e # v2.5.0
         with:
           results_file: results.sarif
           results_format: sarif
@@ -69,7 +64,7 @@
       # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
       # format to the repository Actions tab.
       - name: "Upload artifact"
-        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
         with:
           name: SARIF file
           path: results.sarif
@@ -77,6 +72,6 @@
 
       # Upload the results to GitHub's code scanning dashboard.
       - name: "Upload to code-scanning"
-        uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
+        uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
         with:
           sarif_file: results.sarif
diff --git a/c/dec/decode.c b/c/dec/decode.c
index 8d57c2b..1ab505e 100644
--- a/c/dec/decode.c
+++ b/c/dec/decode.c
@@ -1318,7 +1318,7 @@
   return DecodeDistanceBlockSwitchInternal(0, s);
 }
 
-static BROTLI_BOOL BROTLI_NOINLINE SafeDecodeDistanceBlockSwitch(
+static BROTLI_NOINLINE BrotliDecoderErrorCode SafeDecodeDistanceBlockSwitch(
     BrotliDecoderState* s) {
   return DecodeDistanceBlockSwitchInternal(1, s);
 }
diff --git a/c/enc/backward_references_inc.h b/c/enc/backward_references_inc.h
index 7a91793..7238fca 100644
--- a/c/enc/backward_references_inc.h
+++ b/c/enc/backward_references_inc.h
@@ -36,6 +36,8 @@
   FN(PrepareDistanceCache)(privat, dist_cache);
 
   size_t next_base64_pos = pos_end;
+  /* End of a payload whose trigger was covered by a backward reference. */
+  size_t base64_end = 0;
   if (params->base64_mode &&
       hasher->common.num_base64_regions < params->max_base64_regions) {
     next_base64_pos =
@@ -44,11 +46,19 @@
   while (position + FN(HashTypeLength)() < pos_end) {
     if (position >= next_base64_pos) {
       if (position > next_base64_pos) {
-        /* A previous backward match jumped over the trigger position.
-           Find the next trigger from the current position safely checking
-           pos_end. */
+        /* A previous backward match jumped over the trigger position. Note
+           where that payload ends, then find the next trigger. */
+        size_t payload_end = next_base64_pos + kBase64TriggerLen;
+        while (payload_end < pos_end &&
+               IsBase64Char(ringbuffer[payload_end & ringbuffer_mask])) {
+          payload_end++;
+        }
+        if (payload_end - (next_base64_pos + kBase64TriggerLen) >=
+            params->min_base64_region_len) {
+          base64_end = payload_end;
+        }
         next_base64_pos = FindNextBase64Trigger(ringbuffer, ringbuffer_mask,
-                                                position, pos_end);
+            BROTLI_MAX(size_t, position, payload_end), pos_end);
         if (position < next_base64_pos) {
           goto skip_base64_trigger;
         }
@@ -84,7 +94,6 @@
       BROTLI_BOOL is_dupe = BROTLI_FALSE;
       size_t copy_len = scan_pos - position;
       size_t dupe_dist = 0;
-      size_t matched_region_idx = 0;
       if (length > 0 && hasher->common.num_base64_regions > 0) {
         size_t r = hasher->common.num_base64_regions;
         while (r > 0) {
@@ -104,7 +113,6 @@
                                 copy_len)) {
             is_dupe = BROTLI_TRUE;
             dupe_dist = distance;
-            matched_region_idx = r;
             break;
           }
         }
@@ -126,8 +134,6 @@
         *num_literals += insert_length;
         insert_length = 0;
         position = scan_pos;
-        hasher->common.base64_regions[matched_region_idx].start_literal_pos =
-            start_pos;
         apply_random_heuristics =
             position + 2 * copy_len + random_heuristics_window_size;
         if (hasher->common.num_base64_regions < params->max_base64_regions) {
@@ -160,6 +166,14 @@
       }
     }
   skip_base64_trigger:;
+    if (position < base64_end && position > apply_random_heuristics) {
+      /* No backward reference for a while inside the payload: the rest of it
+         is high-entropy (e.g. image data). Emit it as literals without
+         searching. */
+      insert_length += base64_end - position;
+      position = base64_end;
+      continue;
+    }
     size_t max_length = pos_end - position;
     size_t max_distance = BROTLI_MIN(size_t, position, max_backward_limit);
     size_t dictionary_start = BROTLI_MIN(size_t,
@@ -183,7 +197,7 @@
     FN(FindLongestMatch)(privat, params->dictionary.contextual.dict[dict_id],
         ringbuffer, ringbuffer_mask, dist_cache, position, max_length,
         max_distance, dictionary_start + gap, params->dist.max_distance, &sr);
-    if (ENABLE_COMPOUND_DICTIONARY) {
+    if (ENABLE_COMPOUND_DICTIONARY && position >= base64_end) {
       LookupCompoundDictionaryMatch(&params->dictionary.compound, ringbuffer,
           ringbuffer_mask, dist_cache, position, max_length,
           dictionary_start, params->dist.max_distance, &sr);
@@ -214,7 +228,7 @@
             ringbuffer, ringbuffer_mask, dist_cache, position + 1, max_length,
             max_distance, dictionary_start + gap, params->dist.max_distance,
             &sr2);
-        if (ENABLE_COMPOUND_DICTIONARY) {
+        if (ENABLE_COMPOUND_DICTIONARY && position + 1 >= base64_end) {
           LookupCompoundDictionaryMatch(
               &params->dictionary.compound, ringbuffer,
               ringbuffer_mask, dist_cache, position + 1, max_length,
diff --git a/c/enc/backward_references_opt_inc.h b/c/enc/backward_references_opt_inc.h
index 149b43a..3ad6755 100644
--- a/c/enc/backward_references_opt_inc.h
+++ b/c/enc/backward_references_opt_inc.h
@@ -215,7 +215,7 @@
            dictionary heads[] line before the StoreRange loop. */
         if (ENABLE_COMPOUND_DICTIONARY) {
           PrefetchCompoundDictionaryHeadsOpt(&params->dictionary.compound,
-              ringbuffer, ringbuffer_mask, position + sr.len);
+              ringbuffer, ringbuffer_mask, range_end);
         }
         FN(StoreRange)(privat, ringbuffer, ringbuffer_mask, range_start,
                        range_end);
diff --git a/c/include/brotli/types.h b/c/include/brotli/types.h
index 2dc61ff..7b5a6f5 100644
--- a/c/include/brotli/types.h
+++ b/c/include/brotli/types.h
@@ -41,7 +41,7 @@
  * evaluated, for example: @code{.cpp}
  * if (SomeBrotliFunction(encoder, BROTLI_TRUE) &&
  *     !OtherBrotliFunction(decoder, BROTLI_FALSE)) {
- *   bool x = !!YetAnotherBrotliFunction(encoder, TO_BROLTI_BOOL(2 * 2 == 4));
+ *   bool x = !!YetAnotherBrotliFunction(encoder, TO_BROTLI_BOOL(2 * 2 == 4));
  *   DoSomething(x);
  * }
  * @endcode
diff --git a/c/tools/brotli.c b/c/tools/brotli.c
index 5554ced..433efd6 100644
--- a/c/tools/brotli.c
+++ b/c/tools/brotli.c
@@ -875,12 +875,6 @@
   if (stat(input_path, &statbuf) != 0) {
     return;
   }
-  /* Flush any stdio-buffered output before setting timestamps; otherwise the
-     buffered write() that fclose() issues would clobber the atime/mtime we
-     just set. */
-  if (fflush(fout) != 0) {
-    return;
-  }
   res = CopyTimeStat(&statbuf, output_path, fout);
   res = fchmod(fd, statbuf.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO));
   if (res != 0) {
@@ -1048,9 +1042,16 @@
                               BROTLI_BOOL rm_output) {
   BROTLI_BOOL is_ok = BROTLI_TRUE;
   if (!context->test_integrity && context->fout) {
+    /* Flush before the metadata copy, so a failed write is reported. */
+    if (fflush(context->fout) != 0) {
+      fprintf(stderr, "failed to write output [%s]: %s\n",
+              PrintablePath(context->current_output_path), strerror(errno));
+      is_ok = BROTLI_FALSE;
+    }
     /* Apply metadata via the still-open fd, before close, to close the TOCTOU
        window between fclose() and the metadata syscalls. */
-    if (!rm_output && context->copy_stat && context->current_output_path) {
+    if (is_ok && !rm_output && context->copy_stat &&
+        context->current_output_path) {
       CopyStat(context->current_input_path, context->current_output_path,
                context->fout);
     }
@@ -1075,7 +1076,7 @@
       is_ok = BROTLI_FALSE;
     }
   }
-  if (rm_input && context->current_input_path) {
+  if (rm_input && is_ok && context->current_input_path) {
     unlink(context->current_input_path);
   }
 
diff --git a/docs/types.h.3 b/docs/types.h.3
index 3d8a98c..34f2e89 100644
--- a/docs/types.h.3
+++ b/docs/types.h.3
@@ -58,7 +58,7 @@
 .nf
 if (SomeBrotliFunction(encoder, BROTLI_TRUE) &&
     !OtherBrotliFunction(decoder, BROTLI_FALSE)) {
-  bool x = !!YetAnotherBrotliFunction(encoder, TO_BROLTI_BOOL(2 * 2 == 4));
+  bool x = !!YetAnotherBrotliFunction(encoder, TO_BROTLI_BOOL(2 * 2 == 4));
   DoSomething(x);
 }
 
diff --git a/setup.py b/setup.py
index 88c1b38..8269582 100644
--- a/setup.py
+++ b/setup.py
@@ -150,6 +150,7 @@
     "Programming Language :: Python :: 3.12",
     "Programming Language :: Python :: 3.13",
     "Programming Language :: Python :: 3.14",
+    "Programming Language :: Python :: 3.15",
     "Programming Language :: Unix Shell",
     "Topic :: Software Development :: Libraries",
     "Topic :: Software Development :: Libraries :: Python Modules",