From 16c7280e1a4935aca62de5cccf9b32ccfead1948 Mon Sep 17 00:00:00 2001 From: bea-claude Date: Fri, 2 Oct 2026 23:09:42 +0200 Subject: [PATCH 1/8] Verify SHA256SUMS before extracting a download `be` piped curl straight into tar, so a download's integrity rested on TLS alone. Every beplus CLI release publishes SHA256SUMS beside its tarballs (release-binary.yml: `sha256sum *.tar.gz > SHA256SUMS`), on GitHub and on the S3 mirrors, so check against it. Before downloading, `install` fetches SHA256SUMS from the tarball's own mirror and version directory and requires the tarball's line. It then downloads the tarball into the version's cache directory, compares its sha256 (sha256sum where it exists, otherwise shasum, which macOS has), extracts it and removes it. A missing SHA256SUMS, a missing line or a mismatch aborts with the URL and extracts nothing; a mismatch leaves be.lock, like a half-finished download, so the next install downloads again. The new tests install from a local mirror laid out like the release; the three refusals fail without the check. Co-Authored-By: Igor Lamos --- bin/be | 41 ++++++++++++++++--- test/tests/checksums.bats | 69 ++++++++++++++++++++++++++++++++ test/tests/shared-functions.bash | 49 +++++++++++++++++++++++ 3 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 test/tests/checksums.bats diff --git a/bin/be b/bin/be index 925bd78..34dacf2 100755 --- a/bin/be +++ b/bin/be @@ -664,6 +664,14 @@ install() { local url="$(tarball_url "$version")" is_ok "${url}" || abort "download preflight failed for '$version' (${url})" + # The release publishes SHA256SUMS beside its tarballs. Read this tarball's line before + # downloading it, and extract only a download that matches it. + local sums_url="${url%/*}/SHA256SUMS" + local sums + sums="$(do_get_index "${sums_url}")" || abort "failed to download SHA256SUMS for $version (${sums_url})" + local checksum="$(echo "${sums}" | awk -v name="${url##*/}" '$2 == name { print $1; exit }')" + [[ -n "${checksum}" ]] || abort "no checksum for ${url##*/} in ${sums_url}" + log mkdir "$dir" mkdir -p "$dir" || abort "sudo required (or change ownership, or define BE_PREFIX)" touch "$dir/be.lock" @@ -671,15 +679,21 @@ install() { cd "${dir}" || abort "Failed to cd to ${dir}" log fetch "$url" - do_get "${url}" | tar "$tarflag" --no-same-owner -f - # --strip-components=1 - pipe_results=( "${PIPESTATUS[@]}" ) - if [[ "${pipe_results[0]}" -ne 0 ]]; then + local archive="${dir}/${url##*/}" + if ! do_get "${url}" > "${archive}"; then + rm -f "${archive}" abort "failed to download archive for $version" fi - if [[ "${pipe_results[1]}" -ne 0 ]]; then - abort "failed to extract archive for $version" - fi [ "$GET_SHOWS_PROGRESS" = "true" ] && erase_line + local download_checksum="$(display_sha256 "${archive}")" + if [[ "${download_checksum}" != "${checksum}" ]]; then + rm -f "${archive}" + abort "checksum mismatch for ${url}: ${sums_url} lists ${checksum}, the download is ${download_checksum}" + fi + tar "$tarflag" --no-same-owner -f "${archive}" # --strip-components=1 + local tar_status=$? + rm -f "${archive}" + [[ "${tar_status}" -eq 0 ]] || abort "failed to extract archive for $version" rm -f "$dir/be.lock" disable_pax_mprotect bin/beplus @@ -713,6 +727,21 @@ function do_get() { fi } +# +# Synopsis: display_sha256 file +# sha256sum where it exists, otherwise shasum, which macOS has. +# + +function display_sha256() { + local line + if command -v sha256sum &> /dev/null; then + line="$(sha256sum "$1")" + else + line="$(shasum -a 256 "$1")" + fi + echo "${line%% *}" +} + # # Synopsis: do_get_index [option...] url # Call curl or wget with combination of global and passed options, diff --git a/test/tests/checksums.bats b/test/tests/checksums.bats new file mode 100644 index 0000000..a0b6e7e --- /dev/null +++ b/test/tests/checksums.bats @@ -0,0 +1,69 @@ +#!/usr/bin/env bats +# +# be extracts a download only when it matches its line in the SHA256SUMS the beplus CLI release +# publishes beside its tarballs. These install from a local mirror laid out like the release. + +load shared-functions +load '../../node_modules/bats-support/load' +load '../../node_modules/bats-assert/load' + + +function setup() { + unset_n_env + setup_tmp_prefix + setup_tmp_mirror 2.7.0 + RELEASE="${TMP_MIRROR_DIR}/v2.7.0" + TARBALL="beplus-cli-v2.7.0-$(tarball_platform).tar.gz" + CACHED="${BE_PREFIX}/be/versions/beplus/2.7.0" +} + + +function teardown() { + rm -rf "${TMP_PREFIX_DIR}" "${TMP_MIRROR_DIR}" +} + + +@test "a download that matches SHA256SUMS is installed, and its tarball removed" { + be 2.7.0 + output="$(beplus --version | cli_version)" + assert_equal "${output}" "2.7.0" + [ ! -e "${CACHED}/${TARBALL}" ] + [ ! -e "${CACHED}/be.lock" ] +} + + +@test "a download that does not match SHA256SUMS is not extracted" { + echo "0000000000000000000000000000000000000000000000000000000000000000 ${TARBALL}" > "${RELEASE}/SHA256SUMS" + + run be 2.7.0 + assert_failure + assert_output --partial "checksum mismatch for ${BE_MIRROR}/v2.7.0/${TARBALL}" + [ ! -e "${CACHED}/beplus" ] + [ ! -e "${CACHED}/${TARBALL}" ] + # Like a half-finished download, so the next install downloads it again. + [ -e "${CACHED}/be.lock" ] + [ ! -e "${BE_PREFIX}/bin/beplus" ] +} + + +@test "a download SHA256SUMS has no line for is not extracted" { + # The right checksum, but listed under another platform's tarball only. + echo "$(cut -d ' ' -f 1 "${RELEASE}/SHA256SUMS") beplus-cli-v2.7.0-aix-ppc64.tar.gz" > "${RELEASE}/SHA256SUMS" + + run be 2.7.0 + assert_failure + assert_output --partial "no checksum for ${TARBALL} in ${BE_MIRROR}/v2.7.0/SHA256SUMS" + [ ! -e "${CACHED}/beplus" ] + [ ! -e "${BE_PREFIX}/bin/beplus" ] +} + + +@test "a release without SHA256SUMS is not extracted" { + rm "${RELEASE}/SHA256SUMS" + + run be 2.7.0 + assert_failure + assert_output --partial "failed to download SHA256SUMS for 2.7.0 (${BE_MIRROR}/v2.7.0/SHA256SUMS)" + [ ! -e "${CACHED}/beplus" ] + [ ! -e "${BE_PREFIX}/bin/beplus" ] +} diff --git a/test/tests/shared-functions.bash b/test/tests/shared-functions.bash index 82ff9e1..86e65f0 100644 --- a/test/tests/shared-functions.bash +++ b/test/tests/shared-functions.bash @@ -34,6 +34,55 @@ function setup_tmp_prefix() { export PATH="${BE_PREFIX}/bin:${PATH}" } + +# Create a local mirror laid out as the beplus CLI release publishes one, and point be at it: +# v/ holds this machine's tarball, with a stub beplus that prints the version banner, +# and SHA256SUMS beside it. +# Globals: +# TMP_MIRROR_DIR +# BE_MIRROR + +function setup_tmp_mirror() { + local version="$1" + TMP_MIRROR_DIR="$(mktemp -d)" + [ -d "${TMP_MIRROR_DIR}" ] || exit 2 + export TMP_MIRROR_DIR + + local release="${TMP_MIRROR_DIR}/v${version}" + local tarball="beplus-cli-v${version}-$(tarball_platform).tar.gz" + mkdir -p "${release}" "${TMP_MIRROR_DIR}/stub" + printf '#!/bin/sh\necho "beplus CLI v%s"\n' "${version}" > "${TMP_MIRROR_DIR}/stub/beplus" + chmod +x "${TMP_MIRROR_DIR}/stub/beplus" + tar -czf "${release}/${tarball}" -C "${TMP_MIRROR_DIR}/stub" beplus + if command -v sha256sum &> /dev/null; then + (cd "${release}" && sha256sum "${tarball}" > SHA256SUMS) + else + (cd "${release}" && shasum -a 256 "${tarball}" > SHA256SUMS) + fi + + export BE_MIRROR="file://${TMP_MIRROR_DIR}" +} + +# +# @todo Duplicate +# Synopsis: tarball_platform +# The platform in a release tarball's name, as be picks it for this machine. +# + +function tarball_platform() { + local os arch + case "$(uname -s)" in + Linux) os="linux" ;; + Darwin) os="macos" ;; + esac + case "$(uname -m)" in + x86_64) arch="x64" ;; + aarch64 | armv8l) arch="arm64" ;; + *) arch="$(uname -m)" ;; + esac + echo "${os}-${arch}" +} + # # Synopsis: beplus --version | cli_version # The version a beplus binary reports, without the leading v. 2.x prints a banner whose first From 68e43a4915578817130ea398d324f34bdb8eeb16 Mon Sep 17 00:00:00 2001 From: bea-claude Date: Fri, 2 Oct 2026 23:11:40 +0200 Subject: [PATCH 2/8] Detect the installed version by its file, which makes prune safe `set_active_beplus` looked for `/beplus//bin/beplus`. Under 2.x none of that holds: `--version` prints a banner rather than a version, the cache keeps the binary at `/beplus`, and `beplus` on PATH need not be the one be installed. Detection found nothing, and since `be prune` removes every downloaded version except the detected one, it also removed the installed one. Activation copies `/beplus//beplus` to `$BE_PREFIX/bin/beplus`, so the installed version is the download that file is identical to. Compare the files, which runs no binary and works for every version, rather than ask `version --json`. `cmp -s` stops at the first difference, where macOS's diff reads both ~110 MB binaries (about 0.6 s per download). The interactive menu's preselection uses the same detection. The new test installs one of three downloads and prunes; without the fix the installed version is deleted too. Co-Authored-By: Igor Lamos --- bin/be | 27 +++++++++++---------------- test/tests/prune.bats | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 16 deletions(-) create mode 100644 test/tests/prune.bats diff --git a/bin/be b/bin/be index 34dacf2..96435a8 100755 --- a/bin/be +++ b/bin/be @@ -383,28 +383,23 @@ display_be_version() { # # Synopsis: set_active_beplus -# Checks cached downloads for a binary matching the active beplus CLI. +# Checks cached downloads for the one the installed beplus CLI was copied from: activate copies +# /beplus//beplus to $BE_PREFIX/bin/beplus, so the two files are identical. # Globals modified: # - g_active_beplus # function set_active_beplus() { g_active_beplus= - local beplus_path="$(command -v beplus)" - if [[ -x "${beplus_path}" ]]; then - local installed_version=$(beplus --version) - installed_version=${installed_version#v} - for dir in "${CACHE_DIR}"/*/ ; do - local folder_name="${dir%/}" - folder_name="${folder_name##*/}" - if diff &> /dev/null \ - "${CACHE_DIR}/${folder_name}/${installed_version}/bin/beplus" \ - "${beplus_path}" ; then - g_active_beplus="${folder_name}/${installed_version}" - break - fi - done - fi + local cached + for cached in "${CACHE_DIR}"/*/*/beplus; do + # cmp stops at the first difference; diff may read both binaries (~110 MB each) whole. + if cmp -s "${cached}" "${BE_PREFIX}/bin/beplus"; then + cached="${cached#"${CACHE_DIR}/"}" + g_active_beplus="${cached%/beplus}" + break + fi + done } # diff --git a/test/tests/prune.bats b/test/tests/prune.bats new file mode 100644 index 0000000..f1d2086 --- /dev/null +++ b/test/tests/prune.bats @@ -0,0 +1,38 @@ +#!/usr/bin/env bats +# +# `be prune` removes every downloaded version except the installed one, so it has to recognise +# the installed one: activate copies /beplus//beplus to $BE_PREFIX/bin/beplus. + +load shared-functions +load '../../node_modules/bats-support/load' +load '../../node_modules/bats-assert/load' + + +function setup() { + unset_n_env + setup_tmp_prefix + # KISS and make the downloads by hand rather than do actual installs: a stub beplus that + # prints its version banner, as 2.x does. + local version + for version in 2.5.0 2.6.0 2.7.0; do + mkdir -p "${BE_PREFIX}/be/versions/beplus/${version}" + printf '#!/bin/sh\necho "beplus CLI v%s"\n' "${version}" > "${BE_PREFIX}/be/versions/beplus/${version}/beplus" + chmod +x "${BE_PREFIX}/be/versions/beplus/${version}/beplus" + done +} + + +function teardown() { + rm -rf "${TMP_PREFIX_DIR}" +} + + +@test "be prune keeps the installed version" { + be 2.6.0 + + be prune + output="$(be ls)" + assert_equal "${output}" "beplus/2.6.0" + output="$(beplus --version | cli_version)" + assert_equal "${output}" "2.6.0" +} From 4ffb8f1752af2a1a0ccaff42330ad9e5a46f3e6a Mon Sep 17 00:00:00 2001 From: bea-claude Date: Fri, 2 Oct 2026 23:12:12 +0200 Subject: [PATCH 3/8] Stop `be uninstall` at end of input, deleting nothing The confirmation loop treated a failed `read` as an unclear answer and asked again, so with stdin closed it printed "Please answer yes or no." for ever. End of input now answers no: nothing is deleted and be exits as it does for "n". The new test closes stdin; without the fix it gets the endless loop, which `head` cuts short so the suite cannot hang. Co-Authored-By: Igor Lamos --- bin/be | 3 ++- test/tests/uninstall.bats | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/bin/be b/bin/be index 96435a8..8a241a1 100755 --- a/bin/be +++ b/bin/be @@ -1241,7 +1241,8 @@ function delete_with_echo() { uninstall_installed() { while true; do - read -r -p "Do you wish to delete beplus CLI from ${BE_PREFIX}? " yn + # End of input answers no. + read -r -p "Do you wish to delete beplus CLI from ${BE_PREFIX}? " yn || yn="n" case $yn in [Yy]* ) break ;; [Nn]* ) exit ;; diff --git a/test/tests/uninstall.bats b/test/tests/uninstall.bats index 96266d0..31d4067 100644 --- a/test/tests/uninstall.bats +++ b/test/tests/uninstall.bats @@ -36,3 +36,15 @@ function teardown() { output="$(find "${BE_PREFIX}" -not -type d)" assert_equal "$output" "" } + + +@test "be uninstall at end of input deletes nothing" { + mkdir -p "${BE_PREFIX}/bin" + touch "${BE_PREFIX}/bin/beplus" + + # It used to ask again for ever; head ends that loop rather than hang the suite. + run bash -c 'set -o pipefail; be uninstall < /dev/null | head -n 1' + assert_success + assert_output "" + [ -f "${BE_PREFIX}/bin/beplus" ] +} From 0cdd23c3f8e1c245f3bfb45c592244674d2ad5cf Mon Sep 17 00:00:00 2001 From: bea-claude Date: Fri, 2 Oct 2026 23:13:16 +0200 Subject: [PATCH 4/8] Always download the .tar.gz `update_xz_settings_for_version` switched to `.tar.xz` from major 4 on, a rule inherited from Node's downloads. The beplus CLI release publishes `.tar.gz` only, so a 4.x CLI would have failed its download preflight. With the switch gone nothing chooses xz any more, so its machinery goes too: `BE_USE_XZ`, `can_use_xz`, the `--use-xz`/`--no-use-xz` options and their help line. The URL always ends in `.tar.gz` and tar always runs `-zx`. The new test installs a 4.0.0 from a local mirror; without the fix be asks for the `.tar.xz`. Co-Authored-By: Igor Lamos --- bin/be | 76 ++-------------------------------- test/bin/run-all-tests | 2 +- test/tests/tarball-format.bats | 27 ++++++++++++ 3 files changed, 31 insertions(+), 74 deletions(-) create mode 100644 test/tests/tarball-format.bats diff --git a/bin/be b/bin/be index 8a241a1..805c246 100755 --- a/bin/be +++ b/bin/be @@ -83,18 +83,6 @@ BE_DOWNLOAD_MIRROR=${BE_DOWNLOAD_MIRROR:-https://github.com/beplus/cli/releases/ BE_DOWNLOAD_MIRROR=${BE_DOWNLOAD_MIRROR%/} readonly BE_DOWNLOAD_MIRROR -# Using xz instead of gzip is enabled by default, if xz compatibility checks pass. -# User may set BE_USE_XZ to 0 to disable, or set to anything else to enable. -# May also be overridden by command line flags. - -# Normalise external values to true/false -if [[ "${BE_USE_XZ}" = "0" ]]; then - BE_USE_XZ="false" -elif [[ -n "${BE_USE_XZ+defined}" ]]; then - BE_USE_XZ="true" -fi -# Not setting to readonly. Overriden by CLI flags, and update_xz_settings_for_version. - BE_MAX_REMOTE_MATCHES=${BE_MAX_REMOTE_MATCHES:-20} g_mirror_url=${BE_MIRROR} @@ -188,22 +176,6 @@ display_major_version() { echo "${version}" } -# -# Synopsis: update_xz_settings_for_version numeric-version -# Globals modified: -# - BE_USE_XZ -# - -function update_xz_settings_for_version() { - # tarballs in xz format were available in later version of iojs, but KISS and only use xz from v4. - if [[ "${BE_USE_XZ}" = "true" ]]; then - local major_version="$(display_major_version "$1")" - if [[ "${major_version}" -lt 4 ]]; then - BE_USE_XZ="false" - fi - fi -} - # # Synopsis: update_arch_settings_for_version numeric-version # Globals modified: @@ -327,7 +299,6 @@ Options: -a, --arch Override system architecture --all ls-remote displays all matches instead of last 20 --insecure Turn off certificate checking for https requests (may be needed from behind a proxy server) - --use-xz/--no-use-xz Override automatic detection of xz support and enable/disable use of xz compressed beplus CLI downloads. Aliases: @@ -633,18 +604,10 @@ install() { get_latest_resolved_version "$1" || return 2 version="${g_target_beplus}" [[ -n "${version}" ]] || abort "no version found for '$1'" - update_xz_settings_for_version "${version}" update_arch_settings_for_version "${version}" local dir="${CACHE_DIR}/${g_mirror_folder_name}/${version}" - # Note: decompression flags ignored with default Darwin tar which autodetects. - if test "$BE_USE_XZ" = "true"; then - local tarflag="-Jx" - else - local tarflag="-zx" - fi - if test -d "$dir"; then if [[ ! -e "$dir/be.lock" ]] ; then if [[ "$DOWNLOAD" == "false" ]] ; then @@ -685,7 +648,7 @@ install() { rm -f "${archive}" abort "checksum mismatch for ${url}: ${sums_url} lists ${checksum}, the download is ${download_checksum}" fi - tar "$tarflag" --no-same-owner -f "${archive}" # --strip-components=1 + tar -zx --no-same-owner -f "${archive}" # --strip-components=1 local tar_status=$? rm -f "${archive}" [[ "${tar_status}" -eq 0 ]] || abort "failed to extract archive for $version" @@ -860,30 +823,6 @@ function is_ok() { fi } -# -# Synopsis: can_use_xz -# Test system to see if xz decompression is supported by tar. -# - -function can_use_xz() { - # Be conservative and only enable if xz is likely to work. Unfortunately we can't directly query tar itself. - # For research, see https://github.com/shadowspawn/nvh/issues/8 - local uname_s="$(uname -s)" - if [[ "${uname_s}" = "Linux" ]] && command -v xz &> /dev/null ; then - # tar on linux is likely to support xz if it is available as a command - return 0 - elif [[ "${uname_s}" = "Darwin" ]]; then - local macos_version="$(sw_vers -productVersion)" - local macos_major_version="$(echo "${macos_version}" | cut -d '.' -f 1)" - local macos_minor_version="$(echo "${macos_version}" | cut -d '.' -f 2)" - if [[ "${macos_major_version}" -gt 10 || "${macos_minor_version}" -gt 8 ]]; then - # tar on recent Darwin has xz support built-in - return 0 - fi - fi - return 2 # not supported -} - # # Synopsis: display_tarball_platform # @@ -950,10 +889,8 @@ function tarball_url() { # Artifact version (filename MUST NOT contain +suffix) local artifact_version="${version%%+*}" - local ext="gz" - [ "$BE_USE_XZ" = "true" ] && ext="xz" - - echo "${g_mirror_url}/v${url_version}/beplus-cli-v${artifact_version}-$(display_tarball_platform).tar.${ext}" + # The beplus CLI release publishes .tar.gz only. + echo "${g_mirror_url}/v${url_version}/beplus-cli-v${artifact_version}-$(display_tarball_platform).tar.gz" } # @@ -1457,8 +1394,6 @@ while [[ $# -ne 0 ]]; do -q|--quiet) set_quiet ;; -d|--download) DOWNLOAD="true" ;; --insecure) set_insecure ;; - --use-xz) BE_USE_XZ="true" ;; - --no-use-xz) BE_USE_XZ="false" ;; --latest) display_remote_versions latest; exit ;; --stable) display_remote_versions latest; exit ;; -a|--arch) shift; set_arch "$1";; # set arch and continue @@ -1477,11 +1412,6 @@ while [[ $# -ne 0 ]]; do shift done -if [[ -z "${BE_USE_XZ+defined}" ]]; then - BE_USE_XZ="true" # Default to using xz - can_use_xz || BE_USE_XZ="false" -fi - set -- "${unprocessed_args[@]}" if test $# -eq 0; then diff --git a/test/bin/run-all-tests b/test/bin/run-all-tests index 5dad75d..83ae2b5 100755 --- a/test/bin/run-all-tests +++ b/test/bin/run-all-tests @@ -1,7 +1,7 @@ #!/usr/bin/env bash BIN_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# We want to cover curl and wget especially, gz and xz and variety of OS a bonus. +# We want to cover curl and wget especially, and variety of OS a bonus. services=( ubuntu-wget ) # amazonlinux-curl archlinux-curl ubuntu-curl cd "$(dirname "${BIN_DIRECTORY}")" || exit 2 diff --git a/test/tests/tarball-format.bats b/test/tests/tarball-format.bats new file mode 100644 index 0000000..13033b5 --- /dev/null +++ b/test/tests/tarball-format.bats @@ -0,0 +1,27 @@ +#!/usr/bin/env bats +# +# The beplus CLI release publishes .tar.gz only, whatever the major version. be used to ask for +# .tar.xz from major 4 on, a rule inherited from Node's downloads, and would have found nothing. + +load shared-functions +load '../../node_modules/bats-support/load' +load '../../node_modules/bats-assert/load' + + +function setup() { + unset_n_env + setup_tmp_prefix + setup_tmp_mirror 4.0.0 +} + + +function teardown() { + rm -rf "${TMP_PREFIX_DIR}" "${TMP_MIRROR_DIR}" +} + + +@test "be 4.0.0 downloads the .tar.gz" { + be 4.0.0 + output="$(beplus --version | cli_version)" + assert_equal "${output}" "4.0.0" +} From f671d1a657ce7f472b2245bdc160ed4ca4cf8c65 Mon Sep 17 00:00:00 2001 From: bea-claude Date: Fri, 2 Oct 2026 23:13:54 +0200 Subject: [PATCH 5/8] Bump the version to v0.9.0 + update CHANGELOG Describe the four fixes under the v0.9.0 heading, which becomes the GitHub Release notes. Co-Authored-By: Igor Lamos --- CHANGELOG.md | 19 ++++++++++++++++++- bin/be | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22e35c1..449797d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -https://github.com/beplus/be/compare/v0.8.0...main +https://github.com/beplus/be/compare/v0.9.0...main + +## [v0.9.0] (10/02/2026) + +Verify every download against the `SHA256SUMS` the beplus CLI release publishes beside its +tarballs, and extract only a match. A missing `SHA256SUMS`, a tarball it does not list or a +mismatch aborts with the URL and extracts nothing; until now a download's integrity rested on TLS +alone. Verifying uses `sha256sum`, or `shasum` where there is none. + +Detect the installed version by comparing `$BE_PREFIX/bin/beplus` with the downloads it was copied +from. With a 2.x CLI detection always came up empty, so `be prune` deleted the installed version +along with the rest. + +Stop `be uninstall` at end of input, deleting nothing, as if answered no. It used to ask again for +ever. + +Always download the `.tar.gz`. From major 4 on, `be` asked for a `.tar.xz`, which the beplus CLI +release does not publish; `--use-xz`, `--no-use-xz` and `BE_USE_XZ` are gone with it. ## [v0.8.0] (10/02/2026) diff --git a/bin/be b/bin/be index 805c246..4109129 100755 --- a/bin/be +++ b/bin/be @@ -64,7 +64,7 @@ function n_grep() { # Setup and state # -VERSION="v0.8.0" +VERSION="v0.9.0" BE_PREFIX="${BE_PREFIX-/usr/local}" BE_PREFIX=${BE_PREFIX%/} diff --git a/package-lock.json b/package-lock.json index e245e6e..c2194d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@beplus/be", - "version": "0.8.0", + "version": "0.9.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@beplus/be", - "version": "0.8.0", + "version": "0.9.0", "license": "MIT", "os": [ "!win32" diff --git a/package.json b/package.json index 2831648..691b8b0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@beplus/be", "description": "Interactively Manage beplus CLI Versions", - "version": "0.8.0", + "version": "0.9.0", "author": "Igor Lamos ", "homepage": "https://github.com/beplus/be", "bugs": "https://github.com/beplus/be", From 13fb8c58fe6bc7f917493ace06451a6881a5e199 Mon Sep 17 00:00:00 2001 From: bea-claude Date: Fri, 2 Oct 2026 23:51:44 +0200 Subject: [PATCH 6/8] Read v1.0.5's `name: hash` SHA256SUMS lines v1.0.5's SHA256SUMS, on GitHub and on all three buckets, lists `: ` with no trailing newline, where every 2.x release lists ` ` as sha256sum writes it. The checksum lookup matched only the 2.x form, so `be 1` and `be 1.0.5` aborted with "no checksum", which main installs fine. The lookup now reads both forms. The new test installs from a local mirror whose SHA256SUMS is in the v1 form; without the fix it aborts with "no checksum". Co-Authored-By: Igor Lamos --- CHANGELOG.md | 3 ++- bin/be | 5 +++-- test/tests/checksums.bats | 10 ++++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 449797d..f124707 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,8 @@ https://github.com/beplus/be/compare/v0.9.0...main Verify every download against the `SHA256SUMS` the beplus CLI release publishes beside its tarballs, and extract only a match. A missing `SHA256SUMS`, a tarball it does not list or a mismatch aborts with the URL and extracts nothing; until now a download's integrity rested on TLS -alone. Verifying uses `sha256sum`, or `shasum` where there is none. +alone. Verifying uses `sha256sum`, or `shasum` where there is none. Both line forms the releases +use are read: ` ` from 2.x on, and v1.0.5's `: `. Detect the installed version by comparing `$BE_PREFIX/bin/beplus` with the downloads it was copied from. With a 2.x CLI detection always came up empty, so `be prune` deleted the installed version diff --git a/bin/be b/bin/be index 4109129..83b6d7a 100755 --- a/bin/be +++ b/bin/be @@ -623,11 +623,12 @@ install() { is_ok "${url}" || abort "download preflight failed for '$version' (${url})" # The release publishes SHA256SUMS beside its tarballs. Read this tarball's line before - # downloading it, and extract only a download that matches it. + # downloading it, and extract only a download that matches it. 2.x lists ` `, as + # sha256sum writes it; v1.0.5 lists `: `. local sums_url="${url%/*}/SHA256SUMS" local sums sums="$(do_get_index "${sums_url}")" || abort "failed to download SHA256SUMS for $version (${sums_url})" - local checksum="$(echo "${sums}" | awk -v name="${url##*/}" '$2 == name { print $1; exit }')" + local checksum="$(echo "${sums}" | awk -v name="${url##*/}" '$2 == name { print $1; exit } $1 == name ":" { print $2; exit }')" [[ -n "${checksum}" ]] || abort "no checksum for ${url##*/} in ${sums_url}" log mkdir "$dir" diff --git a/test/tests/checksums.bats b/test/tests/checksums.bats index a0b6e7e..fa621d1 100644 --- a/test/tests/checksums.bats +++ b/test/tests/checksums.bats @@ -32,6 +32,16 @@ function teardown() { } +@test "a download that matches a v1-style SHA256SUMS line is installed" { + # v1.0.5 published `: ` lines, without a trailing newline. + printf '%s: %s' "${TARBALL}" "$(cut -d ' ' -f 1 "${RELEASE}/SHA256SUMS")" > "${RELEASE}/SHA256SUMS" + + be 2.7.0 + output="$(beplus --version | cli_version)" + assert_equal "${output}" "2.7.0" +} + + @test "a download that does not match SHA256SUMS is not extracted" { echo "0000000000000000000000000000000000000000000000000000000000000000 ${TARBALL}" > "${RELEASE}/SHA256SUMS" From 1d4986c1f83fa467fd320e4fc9c3a46055c2ea28 Mon Sep 17 00:00:00 2001 From: bea-claude Date: Fri, 2 Oct 2026 23:52:31 +0200 Subject: [PATCH 7/8] Hash the download from stdin display_sha256 hashed the archive by path and took the line's first word. Given a path with a backslash in it, GNU sha256sum and shasum escape the name and put a backslash before the hash, so every install under such a BE_PREFIX or BE_CACHE_PREFIX failed as a checksum mismatch. Hashing stdin prints no name to escape. The new test installs under a cache prefix with a backslash in it; without the fix it fails as a mismatch wherever sha256sum is GNU's or shasum is used (macOS's own /sbin/sha256sum does not escape). Co-Authored-By: Igor Lamos --- bin/be | 8 +++++--- test/tests/checksums.bats | 11 +++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/bin/be b/bin/be index 83b6d7a..ed296ad 100755 --- a/bin/be +++ b/bin/be @@ -688,15 +688,17 @@ function do_get() { # # Synopsis: display_sha256 file -# sha256sum where it exists, otherwise shasum, which macOS has. +# sha256sum where it exists, otherwise shasum, which macOS has. Both hash stdin rather than a +# named file: given a name with a backslash in it, they escape it and put a backslash before the +# hash. # function display_sha256() { local line if command -v sha256sum &> /dev/null; then - line="$(sha256sum "$1")" + line="$(sha256sum < "$1")" else - line="$(shasum -a 256 "$1")" + line="$(shasum -a 256 < "$1")" fi echo "${line%% *}" } diff --git a/test/tests/checksums.bats b/test/tests/checksums.bats index fa621d1..da12953 100644 --- a/test/tests/checksums.bats +++ b/test/tests/checksums.bats @@ -42,6 +42,17 @@ function teardown() { } +@test "a download under a path with a backslash in it is installed" { + # Given a file name with a backslash, GNU sha256sum and shasum escape it and put a backslash + # in front of the hash. + export BE_CACHE_PREFIX="${TMP_PREFIX_DIR}/back\\slash" + + be 2.7.0 + output="$(beplus --version | cli_version)" + assert_equal "${output}" "2.7.0" +} + + @test "a download that does not match SHA256SUMS is not extracted" { echo "0000000000000000000000000000000000000000000000000000000000000000 ${TARBALL}" > "${RELEASE}/SHA256SUMS" From 51e9f703d6558c49d7b5d8f3b3a9a65d2d07cc44 Mon Sep 17 00:00:00 2001 From: bea-claude Date: Fri, 2 Oct 2026 23:53:47 +0200 Subject: [PATCH 8/8] Skip the local-mirror tests where there is no curl The mirror tests point BE_MIRROR at a file:// URL. be reads it with curl only: without curl it uses wget, which fetches http, https and ftp, so in `npm test`'s ubuntu-wget container all seven failed at the download preflight. They now skip there, saying why. The wget leg still downloads and verifies real releases in the install tests. Co-Authored-By: Igor Lamos --- test/tests/shared-functions.bash | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/tests/shared-functions.bash b/test/tests/shared-functions.bash index 86e65f0..2a18844 100644 --- a/test/tests/shared-functions.bash +++ b/test/tests/shared-functions.bash @@ -37,12 +37,14 @@ function setup_tmp_prefix() { # Create a local mirror laid out as the beplus CLI release publishes one, and point be at it: # v/ holds this machine's tarball, with a stub beplus that prints the version banner, -# and SHA256SUMS beside it. +# and SHA256SUMS beside it. The mirror is a file:// URL, which be reads with curl only (wget +# fetches http, https and ftp), so without curl the test is skipped. # Globals: # TMP_MIRROR_DIR # BE_MIRROR function setup_tmp_mirror() { + command -v curl > /dev/null || skip "the local mirror is a file:// URL, which needs curl" local version="$1" TMP_MIRROR_DIR="$(mktemp -d)" [ -d "${TMP_MIRROR_DIR}" ] || exit 2