From 32a10ec1199da00201912189f2e2d9c9599e2d67 Mon Sep 17 00:00:00 2001 From: Bartosz Blizniak Date: Mon, 20 Jul 2026 17:08:53 +0100 Subject: [PATCH 1/2] feat: preflight archive tooling --- CHANGELOG.md | 2 ++ README.md | 3 ++ install.sh | 22 +++++++++++++-- tests/install.bats | 70 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3514e45..608cac9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 version or target. - SHA-256 verification, archive safety checks, atomic installation, and concurrent-install locking. +- Preflight checks for archive and checksum tools before release archive + downloads. - Stable machine-readable output for CI/CD consumers. - Cross-platform test, security scanning, release, and integration-vendoring workflows. diff --git a/README.md b/README.md index 7b00ce7..a997917 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,9 @@ requested. Concurrent installations are serialized with a lock. | `install.sh` | Linux or macOS | `curl`, or GNU `wget` with `--https-only`; `tar`; `gzip`; and one of `sha256sum`, `shasum`, or `openssl` | | `install.ps1` | Windows x86-64, or Windows Arm64 with x86-64 emulation | Windows PowerShell 5.1 or PowerShell 7+ | +After resolving the release manifest, the shell installer checks the required +archive and checksum tools before downloading the release archive. + ## Configuration Command-line options take precedence over environment variables. diff --git a/install.sh b/install.sh index f0d6a4f..2d7ddbf 100755 --- a/install.sh +++ b/install.sh @@ -136,6 +136,25 @@ sha256_file() { fi } +check_archive_tools() { + case "$ARCHIVE_NAME" in + *.tar.gz|*.tgz) + command -v tar >/dev/null 2>&1 || die "tar is required" + command -v gzip >/dev/null 2>&1 || die "gzip is required" + ;; + *.zip) + command -v unzip >/dev/null 2>&1 || die "unzip is required" + ;; + *) die "unsupported archive format: $ARCHIVE_NAME" ;; + esac + + if ! command -v sha256sum >/dev/null 2>&1 && + ! command -v shasum >/dev/null 2>&1 && + ! command -v openssl >/dev/null 2>&1; then + die "sha256sum, shasum, or openssl is required" + fi +} + manifest_value() { awk -F= -v wanted="$1" ' $0 !~ /^[[:space:]]*#/ && $1 == wanted { @@ -289,13 +308,11 @@ validate_archive_listing() { validate_archive_entries() { case "$ARCHIVE_FILE" in *.tar.gz|*.tgz) - command -v tar >/dev/null 2>&1 || die "tar is required" tar -tvzf "$ARCHIVE_FILE" > "$WORK_DIR/archive-listing.txt" validate_archive_listing "$WORK_DIR/archive-listing.txt" tar -tzf "$ARCHIVE_FILE" > "$WORK_DIR/archive-entries.txt" ;; *.zip) - command -v unzip >/dev/null 2>&1 || die "unzip is required" unzip -Z -l "$ARCHIVE_FILE" > "$WORK_DIR/archive-listing.txt" validate_archive_listing "$WORK_DIR/archive-listing.txt" unzip -Z1 "$ARCHIVE_FILE" > "$WORK_DIR/archive-entries.txt" @@ -383,6 +400,7 @@ main() { return 0 fi + check_archive_tools download_archive stage_archive activate_staged_install diff --git a/tests/install.bats b/tests/install.bats index 0e22058..57e359a 100644 --- a/tests/install.bats +++ b/tests/install.bats @@ -37,6 +37,36 @@ make_path_without_curl() { printf '%s\n' "$bin" } +make_path_without_tar() { + local bin="$BATS_TEST_TMPDIR/path-without-tar" + mkdir -p "$bin" + + local cmd path + for cmd in sh mkdir mktemp rm rmdir awk tr gzip gunzip cat chmod mv dirname grep cp sha256sum shasum openssl sleep curl wget; do + path="$(command -v "$cmd" 2>/dev/null || true)" + if [ -n "$path" ] && [ ! -e "$bin/$cmd" ]; then + ln -s "$path" "$bin/$cmd" + fi + done + + printf '%s\n' "$bin" +} + +make_path_without_checksum_tool() { + local bin="$BATS_TEST_TMPDIR/path-without-checksum-tool" + mkdir -p "$bin" + + local cmd path + for cmd in sh mkdir mktemp rm rmdir awk tr tar gzip gunzip cat chmod mv dirname grep cp sleep curl wget; do + path="$(command -v "$cmd" 2>/dev/null || true)" + if [ -n "$path" ] && [ ! -e "$bin/$cmd" ]; then + ln -s "$path" "$bin/$cmd" + fi + done + + printf '%s\n' "$bin" +} + write_busybox_wget_stub() { local bin="$1" cat > "$bin/wget" <<'EOF' @@ -468,6 +498,46 @@ executable=$bin_dir/cloudsmith" [ ! -e "$INSTALL_ROOT/1.19.0/$TEST_TARGET/cloudsmith/cloudsmith" ] } +@test "missing tar fails before the release archive is downloaded" { + build_fixture "$FIXTURE_DIR" "1.19.0" "$TEST_TARGET" + local restricted_path archive_name + restricted_path="$(make_path_without_tar)" + archive_name="$(fixture_archive_name "1.19.0" "$TEST_TARGET")" + + run --separate-stderr env PATH="$restricted_path" \ + CLOUDSMITH_CLI_ALLOW_INSECURE_URLS=true \ + "$INSTALL_SH" \ + --version 1.19.0 \ + --target "$TEST_TARGET" \ + --install-root "$INSTALL_ROOT" \ + --manifest-url "$FIXTURE_URL/manifest.txt" + + [ "$status" -ne 0 ] + [[ "$stderr" == *"tar is required"* ]] + [[ "$(cat "$FIXTURE_SERVER_LOG")" == *"GET /manifest.txt"* ]] + [[ "$(cat "$FIXTURE_SERVER_LOG")" != *"GET /$archive_name"* ]] +} + +@test "missing checksum tools fail before the release archive is downloaded" { + build_fixture "$FIXTURE_DIR" "1.19.0" "$TEST_TARGET" + local restricted_path archive_name + restricted_path="$(make_path_without_checksum_tool)" + archive_name="$(fixture_archive_name "1.19.0" "$TEST_TARGET")" + + run --separate-stderr env PATH="$restricted_path" \ + CLOUDSMITH_CLI_ALLOW_INSECURE_URLS=true \ + "$INSTALL_SH" \ + --version 1.19.0 \ + --target "$TEST_TARGET" \ + --install-root "$INSTALL_ROOT" \ + --manifest-url "$FIXTURE_URL/manifest.txt" + + [ "$status" -ne 0 ] + [[ "$stderr" == *"sha256sum, shasum, or openssl is required"* ]] + [[ "$(cat "$FIXTURE_SERVER_LOG")" == *"GET /manifest.txt"* ]] + [[ "$(cat "$FIXTURE_SERVER_LOG")" != *"GET /$archive_name"* ]] +} + @test "BusyBox-style wget is rejected in secure mode because it cannot enforce HTTPS redirects" { local no_curl_path="$BATS_TEST_TMPDIR/no-curl-secure" no_curl_path="$(make_path_without_curl)" From e1a4b6c5429da71ac93aae9d3c9b4374bbaa26f1 Mon Sep 17 00:00:00 2001 From: Bartosz Blizniak Date: Tue, 21 Jul 2026 09:50:34 +0100 Subject: [PATCH 2/2] refactor: rename preflight tool check and clarify requirements check_archive_tools also validated SHA-256 tooling, so rename it to check_required_tools; document the conditional tar/gzip vs unzip requirement in the README. --- README.md | 2 +- install.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a997917..6a13059 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ requested. Concurrent installations are serialized with a lock. | Installer | Supported host | Required tools | | --- | --- | --- | -| `install.sh` | Linux or macOS | `curl`, or GNU `wget` with `--https-only`; `tar`; `gzip`; and one of `sha256sum`, `shasum`, or `openssl` | +| `install.sh` | Linux or macOS | `curl`, or GNU `wget` with `--https-only`; `tar` and `gzip` for `.tar.gz` archives or `unzip` for `.zip` archives; and one of `sha256sum`, `shasum`, or `openssl` | | `install.ps1` | Windows x86-64, or Windows Arm64 with x86-64 emulation | Windows PowerShell 5.1 or PowerShell 7+ | After resolving the release manifest, the shell installer checks the required diff --git a/install.sh b/install.sh index 2d7ddbf..5cc5d00 100755 --- a/install.sh +++ b/install.sh @@ -136,7 +136,7 @@ sha256_file() { fi } -check_archive_tools() { +check_required_tools() { case "$ARCHIVE_NAME" in *.tar.gz|*.tgz) command -v tar >/dev/null 2>&1 || die "tar is required" @@ -400,7 +400,7 @@ main() { return 0 fi - check_archive_tools + check_required_tools download_archive stage_archive activate_staged_install