From 49f3b6c879817a7d6859cd674c8237b7e08a2921 Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Sun, 4 Oct 2026 21:36:22 -0400 Subject: [PATCH 1/6] feat: install chunkzero tools from the mise registry --- .github/workflows/ci.yml | 20 +++++++++++++++ LICENSE | 21 +++++++++++++++ README.md | 34 +++++++++++++++++++++++++ hooks/backend_exec_env.lua | 3 +++ hooks/backend_install.lua | 45 +++++++++++++++++++++++++++++++++ hooks/backend_list_versions.lua | 17 +++++++++++++ lib/registry.lua | 37 +++++++++++++++++++++++++++ metadata.lua | 8 ++++++ test/e2e.sh | 31 +++++++++++++++++++++++ test/registry/tools/chunk.json | 16 ++++++++++++ 10 files changed, 232 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 hooks/backend_exec_env.lua create mode 100644 hooks/backend_install.lua create mode 100644 hooks/backend_list_versions.lua create mode 100644 lib/registry.lua create mode 100644 metadata.lua create mode 100755 test/e2e.sh create mode 100644 test/registry/tools/chunk.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3dba941 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,20 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + e2e: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v5 + - uses: jdx/mise-action@v4 + with: + install: false + - run: test/e2e.sh diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..605be18 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 chunkzero + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a7f6b6b --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# mise-chunkzero + +A [mise](https://mise.jdx.dev) backend plugin for chunkzero tools. It installs releases listed in +[chunkzero/mise-registry](https://github.com/chunkzero/mise-registry), verifies each archive's sha256, and keeps the +runtime files bundled next to the executable. + +```toml +[plugins] +chunkzero = "https://github.com/chunkzero/mise-chunkzero" + +[tools] +# Newest nightly, or newest alpha/beta/rc with channel = "beta". +"chunkzero:chunk" = { version = "latest", channel = "nightly", prerelease = true, minimum_release_age = "0s" } +# An exact version from any channel. +"chunkzero:rpp" = "0.1.0-nightly.20261004062300.ge282f11816cd" +``` + +`channel` limits mise to that channel's releases, and `prerelease = true` lets `latest` select them. mise ignores +releases younger than `minimum_release_age` (24 hours by default), so nightlies need it lowered. With `mise.lock`, the +resolved version is pinned until `mise upgrade`. + +Stable releases don't need the plugin: + +```toml +[tools] +"github:chunkzero/chunk" = "latest" +``` + +Tools: `chunk`, `rpp`. Platforms: Linux and macOS on x64 and arm64, Windows on x64. + +## Development + +`test/e2e.sh` installs chunk from a local copy of the registry and checks that a wrong checksum is rejected. Set +`MISE_CHUNKZERO_REGISTRY` to point the plugin at another registry URL. diff --git a/hooks/backend_exec_env.lua b/hooks/backend_exec_env.lua new file mode 100644 index 0000000..aa0cbc2 --- /dev/null +++ b/hooks/backend_exec_env.lua @@ -0,0 +1,3 @@ +function PLUGIN:BackendExecEnv(ctx) + return { env_vars = { { key = "PATH", value = ctx.install_path } } } +end diff --git a/hooks/backend_install.lua b/hooks/backend_install.lua new file mode 100644 index 0000000..95baf84 --- /dev/null +++ b/hooks/backend_install.lua @@ -0,0 +1,45 @@ +local archiver = require("archiver") +local cmd = require("cmd") +local file = require("file") +local http = require("http") +local registry = require("registry") + +local function sha256(path) + local output + if RUNTIME.osType == "windows" then + output = cmd.exec('powershell -NoProfile -Command "(Get-FileHash -Algorithm SHA256 -LiteralPath \'' .. path + .. '\').Hash"') + elseif RUNTIME.osType == "darwin" then + output = cmd.exec("shasum -a 256 '" .. path .. "'") + else + output = cmd.exec("sha256sum '" .. path .. "'") + end + return output:match("^%s*(%x+)"):lower() +end + +function PLUGIN:BackendInstall(ctx) + local entry + for _, candidate in ipairs(registry.load(ctx.tool).versions) do + if candidate.version == ctx.version then + entry = candidate + end + end + if not entry then + error(ctx.tool .. " " .. ctx.version .. " isn't in the chunkzero registry") + end + local platform = registry.platform() + local asset = entry.assets[platform] + if not asset then + error(ctx.tool .. " " .. ctx.version .. " has no " .. platform .. " build") + end + + local archive = file.join_path(ctx.download_path, ctx.tool .. "-" .. ctx.version .. "-" .. platform .. ".tar.gz") + http.download_file({ url = asset.url }, archive) + local actual = sha256(archive) + if actual ~= asset.sha256 then + error("checksum mismatch for " .. asset.url .. ": expected " .. asset.sha256 .. ", got " .. actual) + end + -- Archives hold one top-level directory with the executable and the runtime files it loads relative to itself. + archiver.decompress(archive, ctx.install_path, { strip_components = 1 }) + return {} +end diff --git a/hooks/backend_list_versions.lua b/hooks/backend_list_versions.lua new file mode 100644 index 0000000..8da61a4 --- /dev/null +++ b/hooks/backend_list_versions.lua @@ -0,0 +1,17 @@ +local registry = require("registry") + +-- With `channel = "nightly"` or `channel = "beta"`, only that channel's releases are listed, so mise's `latest` picks +-- its newest. Without a channel every release is listed, so exact versions from any channel resolve. +function PLUGIN:BackendListVersions(ctx) + local channel = ctx.options and ctx.options.channel + if channel ~= nil and channel ~= "latest" and channel ~= "beta" and channel ~= "nightly" then + error("channel must be latest, beta or nightly, not " .. tostring(channel)) + end + local versions = {} + for _, entry in ipairs(registry.load(ctx.tool).versions) do + if channel == nil or registry.channel(entry.version) == channel then + table.insert(versions, entry.version) + end + end + return { versions = versions } +end diff --git a/lib/registry.lua b/lib/registry.lua new file mode 100644 index 0000000..329ff8f --- /dev/null +++ b/lib/registry.lua @@ -0,0 +1,37 @@ +local http = require("http") +local json = require("json") + +local M = {} + +-- MISE_CHUNKZERO_REGISTRY overrides the registry base URL, e.g. to test against a local copy. +M.BASE = os.getenv("MISE_CHUNKZERO_REGISTRY") or "https://raw.githubusercontent.com/chunkzero/mise-registry/main" + +function M.load(tool) + local response = http.get({ url = M.BASE .. "/tools/" .. tool .. ".json" }) + if response.status_code == 404 then + error("chunkzero has no tool named " .. tool .. "; see https://github.com/chunkzero/mise-registry/tree/main/tools") + end + if response.status_code ~= 200 then + error("couldn't read the chunkzero registry for " .. tool .. ": HTTP " .. response.status_code) + end + return json.decode(response.body) +end + +function M.channel(version) + if version:find("-nightly.", 1, true) then + return "nightly" + end + return version:find("-", 1, true) and "beta" or "latest" +end + +function M.platform() + local os_names = { linux = "linux", darwin = "darwin", windows = "windows" } + local arch_names = { amd64 = "x64", arm64 = "arm64" } + local os_name, arch = os_names[RUNTIME.osType], arch_names[RUNTIME.archType] + if not os_name or not arch then + error("chunkzero tools don't support " .. RUNTIME.osType .. "-" .. RUNTIME.archType) + end + return os_name .. "-" .. arch +end + +return M diff --git a/metadata.lua b/metadata.lua new file mode 100644 index 0000000..bdd3afe --- /dev/null +++ b/metadata.lua @@ -0,0 +1,8 @@ +PLUGIN = {} +PLUGIN.name = "chunkzero" +PLUGIN.version = "0.1.0" +PLUGIN.homepage = "https://github.com/chunkzero/mise-chunkzero" +PLUGIN.license = "MIT" +PLUGIN.description = "Installs chunkzero tools (chunk, rpp) from chunkzero/mise-registry" +PLUGIN.minRuntimeVersion = "0.3.0" +PLUGIN.notes = {} diff --git a/test/e2e.sh b/test/e2e.sh new file mode 100755 index 0000000..5f414c0 --- /dev/null +++ b/test/e2e.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Installs chunk through the plugin from a local registry copy, then checks that a wrong checksum is rejected. +set -euo pipefail +root=$(cd "$(dirname "$0")/.." && pwd) +work=$(mktemp -d) +trap 'kill "$server" 2>/dev/null; rm -rf "$work"' EXIT +cp -r "$root/test/registry" "$work/registry" +python3 -m http.server 8765 --bind 127.0.0.1 --directory "$work/registry" >/dev/null 2>&1 & +server=$! +sleep 1 + +export MISE_DATA_DIR="$work/data" MISE_CACHE_DIR="$work/cache" MISE_STATE_DIR="$work/state" MISE_CONFIG_DIR="$work/config" +export MISE_GLOBAL_CONFIG_FILE="$work/config/config.toml" MISE_YES=1 MISE_CHUNKZERO_REGISTRY=http://127.0.0.1:8765 +mkdir -p "$work/project" && cd "$work/project" +mise plugins link chunkzero "$root" +cat > mise.toml <<'TOML' +[tools] +"chunkzero:chunk" = { version = "latest", channel = "nightly", prerelease = true, minimum_release_age = "0s" } +TOML +mise trust -q +mise install +mise exec -- chunk --version | grep -qx 'chunk 0.1.0-nightly.20261004.ge282f11816cd' + +sed -i 's/"sha256": "[0-9a-f]*"/"sha256": "'"$(printf '0%.0s' {1..64})"'"/' "$work/registry/tools/chunk.json" +rm -rf "$MISE_DATA_DIR/installs" "$MISE_CACHE_DIR" +if mise install 2>"$work/error"; then + echo "installed despite a wrong checksum" >&2 + exit 1 +fi +grep -q "checksum mismatch" "$work/error" +echo "ok" diff --git a/test/registry/tools/chunk.json b/test/registry/tools/chunk.json new file mode 100644 index 0000000..185447e --- /dev/null +++ b/test/registry/tools/chunk.json @@ -0,0 +1,16 @@ +{ + "repository": "chunkzero/chunk", + "channels": { "nightly": "0.1.0-nightly.20261004.ge282f11816cd" }, + "versions": [ + { + "version": "0.1.0-nightly.20261004.ge282f11816cd", + "tag": "v0.1.0-nightly.20261004.ge282f11816cd", + "assets": { + "linux-x64": { + "url": "https://github.com/chunkzero/chunk/releases/download/v0.1.0-nightly.20261004.ge282f11816cd/chunk-0.1.0-nightly.20261004.ge282f11816cd-linux-x64.tar.gz", + "sha256": "e707dc45481899d3d49b759695c602e000a828089fea9430d23abf5225bdc96a" + } + } + } + ] +} From 002d58bb15bfcf3566bc45c8f7e71d8091360da1 Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Sun, 4 Oct 2026 21:46:22 -0400 Subject: [PATCH 2/6] fix: one tool per channel, strict registry versions and safe shell quoting --- README.md | 11 +++---- hooks/backend_install.lua | 27 ++++++++++------- hooks/backend_list_versions.lua | 11 +++---- lib/registry.lua | 51 ++++++++++++++++++++++++++++----- test/e2e.sh | 2 +- 5 files changed, 71 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index a7f6b6b..1529826 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,16 @@ runtime files bundled next to the executable. chunkzero = "https://github.com/chunkzero/mise-chunkzero" [tools] -# Newest nightly, or newest alpha/beta/rc with channel = "beta". -"chunkzero:chunk" = { version = "latest", channel = "nightly", prerelease = true, minimum_release_age = "0s" } +# The newest nightly; `chunkzero:rpp-beta` follows the newest alpha, beta or rc instead. +"chunkzero:chunk-nightly" = { version = "latest", prerelease = true } # An exact version from any channel. "chunkzero:rpp" = "0.1.0-nightly.20261004062300.ge282f11816cd" ``` -`channel` limits mise to that channel's releases, and `prerelease = true` lets `latest` select them. mise ignores -releases younger than `minimum_release_age` (24 hours by default), so nightlies need it lowered. With `mise.lock`, the -resolved version is pinned until `mise upgrade`. +Each channel is its own tool, so its releases are cached and installed separately from the others. `prerelease = true` +lets `latest` select nightly and beta versions. With `mise.lock`, the resolved version stays pinned; `mise upgrade` +moves to the channel's newest release. The registry has no release dates, so mise's `minimum_release_age` doesn't apply +to these tools. Stable releases don't need the plugin: diff --git a/hooks/backend_install.lua b/hooks/backend_install.lua index 95baf84..25cdc4d 100644 --- a/hooks/backend_install.lua +++ b/hooks/backend_install.lua @@ -7,33 +7,38 @@ local registry = require("registry") local function sha256(path) local output if RUNTIME.osType == "windows" then - output = cmd.exec('powershell -NoProfile -Command "(Get-FileHash -Algorithm SHA256 -LiteralPath \'' .. path - .. '\').Hash"') - elseif RUNTIME.osType == "darwin" then - output = cmd.exec("shasum -a 256 '" .. path .. "'") + local quoted = "'" .. path:gsub("'", "''") .. "'" + output = cmd.exec('powershell -NoProfile -Command "(Get-FileHash -Algorithm SHA256 -LiteralPath ' .. quoted + .. ').Hash"') else - output = cmd.exec("sha256sum '" .. path .. "'") + local quoted = "'" .. path:gsub("'", "'\\''") .. "'" + output = cmd.exec((RUNTIME.osType == "darwin" and "shasum -a 256 " or "sha256sum ") .. quoted) end - return output:match("^%s*(%x+)"):lower() + local digest = output:match("^%s*(%x+)") + if not digest or #digest ~= 64 then + error("couldn't compute the sha256 of " .. path .. ": " .. output) + end + return digest:lower() end function PLUGIN:BackendInstall(ctx) + local tool = registry.parse(ctx.tool) local entry - for _, candidate in ipairs(registry.load(ctx.tool).versions) do + for _, candidate in ipairs(registry.load(tool).versions) do if candidate.version == ctx.version then entry = candidate end end if not entry then - error(ctx.tool .. " " .. ctx.version .. " isn't in the chunkzero registry") + error(tool .. " " .. ctx.version .. " isn't in the chunkzero registry") end local platform = registry.platform() local asset = entry.assets[platform] - if not asset then - error(ctx.tool .. " " .. ctx.version .. " has no " .. platform .. " build") + if not asset or not asset.sha256:match("^%x+$") or #asset.sha256 ~= 64 then + error(tool .. " " .. ctx.version .. " has no " .. platform .. " build") end - local archive = file.join_path(ctx.download_path, ctx.tool .. "-" .. ctx.version .. "-" .. platform .. ".tar.gz") + local archive = file.join_path(ctx.download_path, tool .. "-" .. ctx.version .. "-" .. platform .. ".tar.gz") http.download_file({ url = asset.url }, archive) local actual = sha256(archive) if actual ~= asset.sha256 then diff --git a/hooks/backend_list_versions.lua b/hooks/backend_list_versions.lua index 8da61a4..09a7389 100644 --- a/hooks/backend_list_versions.lua +++ b/hooks/backend_list_versions.lua @@ -1,14 +1,11 @@ local registry = require("registry") --- With `channel = "nightly"` or `channel = "beta"`, only that channel's releases are listed, so mise's `latest` picks --- its newest. Without a channel every release is listed, so exact versions from any channel resolve. +-- `chunkzero:chunk-nightly` and `chunkzero:chunk-beta` list only that channel's releases, so mise's `latest` picks its +-- newest; as separate tools they also get separate caches and installs. `chunkzero:chunk` lists every release. function PLUGIN:BackendListVersions(ctx) - local channel = ctx.options and ctx.options.channel - if channel ~= nil and channel ~= "latest" and channel ~= "beta" and channel ~= "nightly" then - error("channel must be latest, beta or nightly, not " .. tostring(channel)) - end + local tool, channel = registry.parse(ctx.tool) local versions = {} - for _, entry in ipairs(registry.load(ctx.tool).versions) do + for _, entry in ipairs(registry.load(tool).versions) do if channel == nil or registry.channel(entry.version) == channel then table.insert(versions, entry.version) end diff --git a/lib/registry.lua b/lib/registry.lua index 329ff8f..d9a06c5 100644 --- a/lib/registry.lua +++ b/lib/registry.lua @@ -6,15 +6,18 @@ local M = {} -- MISE_CHUNKZERO_REGISTRY overrides the registry base URL, e.g. to test against a local copy. M.BASE = os.getenv("MISE_CHUNKZERO_REGISTRY") or "https://raw.githubusercontent.com/chunkzero/mise-registry/main" -function M.load(tool) - local response = http.get({ url = M.BASE .. "/tools/" .. tool .. ".json" }) - if response.status_code == 404 then - error("chunkzero has no tool named " .. tool .. "; see https://github.com/chunkzero/mise-registry/tree/main/tools") +local CHANNELS = { nightly = true, beta = true } + +--- Splits `chunk-nightly` into ("chunk", "nightly"); a bare `chunk` lists every channel. +function M.parse(name) + if not name:match("^%l[%l%d%-]*$") then + error("invalid chunkzero tool name " .. name) end - if response.status_code ~= 200 then - error("couldn't read the chunkzero registry for " .. tool .. ": HTTP " .. response.status_code) + local tool, channel = name:match("^(.-)%-(%l+)$") + if tool and CHANNELS[channel] then + return tool, channel end - return json.decode(response.body) + return name, nil end function M.channel(version) @@ -24,6 +27,40 @@ function M.channel(version) return version:find("-", 1, true) and "beta" or "latest" end +local VERSIONS = { + "^%d+%.%d+%.%d+$", + "^%d+%.%d+%.%d+%-alpha%.%d+$", + "^%d+%.%d+%.%d+%-beta%.%d+$", + "^%d+%.%d+%.%d+%-rc%.%d+$", + "^%d+%.%d+%.%d+%-nightly%.%d+%.g%x+$", +} + +function M.valid_version(version) + for _, pattern in ipairs(VERSIONS) do + if version:match(pattern) then + return true + end + end + return false +end + +function M.load(tool) + local response = http.get({ url = M.BASE .. "/tools/" .. tool .. ".json" }) + if response.status_code == 404 then + error("chunkzero has no tool named " .. tool .. "; see https://github.com/chunkzero/mise-registry/tree/main/tools") + end + if response.status_code ~= 200 then + error("couldn't read the chunkzero registry for " .. tool .. ": HTTP " .. response.status_code) + end + local registry = json.decode(response.body) + for _, entry in ipairs(registry.versions) do + if not M.valid_version(entry.version) then + error("the chunkzero registry lists an invalid " .. tool .. " version: " .. entry.version) + end + end + return registry +end + function M.platform() local os_names = { linux = "linux", darwin = "darwin", windows = "windows" } local arch_names = { amd64 = "x64", arm64 = "arm64" } diff --git a/test/e2e.sh b/test/e2e.sh index 5f414c0..3c28888 100755 --- a/test/e2e.sh +++ b/test/e2e.sh @@ -15,7 +15,7 @@ mkdir -p "$work/project" && cd "$work/project" mise plugins link chunkzero "$root" cat > mise.toml <<'TOML' [tools] -"chunkzero:chunk" = { version = "latest", channel = "nightly", prerelease = true, minimum_release_age = "0s" } +"chunkzero:chunk-nightly" = { version = "latest", prerelease = true } TOML mise trust -q mise install From aae79d47bbe3b84e4703dbc3fea47db0aeedb9a5 Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Sun, 4 Oct 2026 21:53:49 -0400 Subject: [PATCH 3/6] fix: keep installs within the requested channel and accept uppercase checksums --- hooks/backend_install.lua | 7 +++++-- test/e2e.sh | 7 ++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/hooks/backend_install.lua b/hooks/backend_install.lua index 25cdc4d..86e4d0c 100644 --- a/hooks/backend_install.lua +++ b/hooks/backend_install.lua @@ -22,7 +22,10 @@ local function sha256(path) end function PLUGIN:BackendInstall(ctx) - local tool = registry.parse(ctx.tool) + local tool, channel = registry.parse(ctx.tool) + if channel and registry.channel(ctx.version) ~= channel then + error(ctx.tool .. " only installs " .. channel .. " versions, not " .. ctx.version) + end local entry for _, candidate in ipairs(registry.load(tool).versions) do if candidate.version == ctx.version then @@ -41,7 +44,7 @@ function PLUGIN:BackendInstall(ctx) local archive = file.join_path(ctx.download_path, tool .. "-" .. ctx.version .. "-" .. platform .. ".tar.gz") http.download_file({ url = asset.url }, archive) local actual = sha256(archive) - if actual ~= asset.sha256 then + if actual ~= asset.sha256:lower() then error("checksum mismatch for " .. asset.url .. ": expected " .. asset.sha256 .. ", got " .. actual) end -- Archives hold one top-level directory with the executable and the runtime files it loads relative to itself. diff --git a/test/e2e.sh b/test/e2e.sh index 3c28888..15ea038 100755 --- a/test/e2e.sh +++ b/test/e2e.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Installs chunk through the plugin from a local registry copy, then checks that a wrong checksum is rejected. +# Installs chunk through the plugin from a local registry copy, then checks that other channels' versions and wrong checksums are rejected. set -euo pipefail root=$(cd "$(dirname "$0")/.." && pwd) work=$(mktemp -d) @@ -20,6 +20,11 @@ TOML mise trust -q mise install mise exec -- chunk --version | grep -qx 'chunk 0.1.0-nightly.20261004.ge282f11816cd' +if mise install chunkzero:chunk-beta@0.1.0-nightly.20261004.ge282f11816cd 2>"$work/error"; then + echo "installed a nightly as a beta" >&2 + exit 1 +fi +grep -q "only installs beta versions" "$work/error" sed -i 's/"sha256": "[0-9a-f]*"/"sha256": "'"$(printf '0%.0s' {1..64})"'"/' "$work/registry/tools/chunk.json" rm -rf "$MISE_DATA_DIR/installs" "$MISE_CACHE_DIR" From 865b49a4d43eac1cc9c451147cbbaefcd782ac0e Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Sun, 4 Oct 2026 21:59:27 -0400 Subject: [PATCH 4/6] fix: hash archives through stdin so escaped filenames can't break checksum parsing --- hooks/backend_install.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hooks/backend_install.lua b/hooks/backend_install.lua index 86e4d0c..460a3f2 100644 --- a/hooks/backend_install.lua +++ b/hooks/backend_install.lua @@ -12,7 +12,8 @@ local function sha256(path) .. ').Hash"') else local quoted = "'" .. path:gsub("'", "'\\''") .. "'" - output = cmd.exec((RUNTIME.osType == "darwin" and "shasum -a 256 " or "sha256sum ") .. quoted) + -- Hashing stdin keeps the output free of the filename, which sha256sum would escape. + output = cmd.exec((RUNTIME.osType == "darwin" and "shasum -a 256 < " or "sha256sum < ") .. quoted) end local digest = output:match("^%s*(%x+)") if not digest or #digest ~= 64 then From f349175bad34e9e642d64b9d6a19121887092cbf Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Sun, 4 Oct 2026 22:05:24 -0400 Subject: [PATCH 5/6] fix: pass the archive path to the hash command through the environment --- hooks/backend_install.lua | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/hooks/backend_install.lua b/hooks/backend_install.lua index 460a3f2..75987bc 100644 --- a/hooks/backend_install.lua +++ b/hooks/backend_install.lua @@ -5,15 +5,16 @@ local http = require("http") local registry = require("registry") local function sha256(path) + -- The path goes through the environment, so neither cmd.exe nor sh can expand or split it. + local options = { env = { CHUNKZERO_ARCHIVE = path } } local output if RUNTIME.osType == "windows" then - local quoted = "'" .. path:gsub("'", "''") .. "'" - output = cmd.exec('powershell -NoProfile -Command "(Get-FileHash -Algorithm SHA256 -LiteralPath ' .. quoted - .. ').Hash"') + output = cmd.exec( + 'powershell -NoProfile -Command "(Get-FileHash -Algorithm SHA256 -LiteralPath $env:CHUNKZERO_ARCHIVE).Hash"', + options) else - local quoted = "'" .. path:gsub("'", "'\\''") .. "'" - -- Hashing stdin keeps the output free of the filename, which sha256sum would escape. - output = cmd.exec((RUNTIME.osType == "darwin" and "shasum -a 256 < " or "sha256sum < ") .. quoted) + local command = RUNTIME.osType == "darwin" and "shasum -a 256" or "sha256sum" + output = cmd.exec(command .. ' < "$CHUNKZERO_ARCHIVE"', options) end local digest = output:match("^%s*(%x+)") if not digest or #digest ~= 64 then From 8b34680e4eca0d2b9ffab1388341813ccaed40a8 Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Sun, 4 Oct 2026 22:12:56 -0400 Subject: [PATCH 6/6] fix: hash on Windows with a fixed encoded PowerShell command --- hooks/backend_install.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/hooks/backend_install.lua b/hooks/backend_install.lua index 75987bc..e4ef436 100644 --- a/hooks/backend_install.lua +++ b/hooks/backend_install.lua @@ -9,9 +9,8 @@ local function sha256(path) local options = { env = { CHUNKZERO_ARCHIVE = path } } local output if RUNTIME.osType == "windows" then - output = cmd.exec( - 'powershell -NoProfile -Command "(Get-FileHash -Algorithm SHA256 -LiteralPath $env:CHUNKZERO_ARCHIVE).Hash"', - options) + -- `(Get-FileHash -Algorithm SHA256 -LiteralPath $env:CHUNKZERO_ARCHIVE).Hash`, encoded so the shell running the command can't expand it. + output = cmd.exec("powershell -NoProfile -EncodedCommand KABHAGUAdAAtAEYAaQBsAGUASABhAHMAaAAgAC0AQQBsAGcAbwByAGkAdABoAG0AIABTAEgAQQAyADUANgAgAC0ATABpAHQAZQByAGEAbABQAGEAdABoACAAJABlAG4AdgA6AEMASABVAE4ASwBaAEUAUgBPAF8AQQBSAEMASABJAFYARQApAC4ASABhAHMAaAA=", options) else local command = RUNTIME.osType == "darwin" and "shasum -a 256" or "sha256sum" output = cmd.exec(command .. ' < "$CHUNKZERO_ARCHIVE"', options)