diff --git a/.github/workflows/check-linux.yml b/.github/workflows/check-linux.yml index 12b3765..0b74f1a 100644 --- a/.github/workflows/check-linux.yml +++ b/.github/workflows/check-linux.yml @@ -47,6 +47,8 @@ jobs: run: | sudo apt-get update sudo apt-get install -y zsh + - name: "⚙️ Generate checksums" + run: sh ./public/sh/generate-checksums.sh - name: "⚙️ Check: unit fixtures" run: sh ./tests/installers.sh - name: "⚙️ Check: install.sh -- -i skip" diff --git a/.github/workflows/check-macos.yml b/.github/workflows/check-macos.yml index ab2189a..81e87d2 100644 --- a/.github/workflows/check-macos.yml +++ b/.github/workflows/check-macos.yml @@ -47,6 +47,8 @@ jobs: - name: ⚙️ Prepare dependencies run: | brew install zsh + - name: "⚙️ Generate checksums" + run: sh ./public/sh/generate-checksums.sh - name: "⚙️ Check: unit fixtures" run: sh ./tests/installers.sh - name: "⚙️ Check: install.sh -- -i skip" diff --git a/.github/workflows/checksum.yml b/.github/workflows/checksum.yml index 500f336..25e6d72 100644 --- a/.github/workflows/checksum.yml +++ b/.github/workflows/checksum.yml @@ -3,6 +3,7 @@ name: Checksum on: push: paths: + - "public/sh/generate-checksums.sh" - "public/sh/install_zpmod.sh" - "public/sh/install.sh" - "public/sh/sync-init.sh" @@ -25,14 +26,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: "🆗 Generate checksum" - uses: jmgilman/actions-generate-checksum@3ea6dc9bf8eecf28e2ecc982fab683484a1a8561 # v1.0.1 - with: - patterns: | - public/sh/install_zpmod.sh - public/sh/install.sh - public/sh/sync-init.sh - public/zsh/init.zsh - - run: mv checksum.txt public/ + run: sh ./public/sh/generate-checksums.sh - name: "🆗 Commit" uses: z-shell/.github/actions/commit@3222f53692e448ed6e2caf8b8722e74fb2ca2461 # v1.1.1 with: diff --git a/public/checksum.txt b/public/checksum.txt index d85758a..26ac375 100644 --- a/public/checksum.txt +++ b/public/checksum.txt @@ -1,4 +1,4 @@ 0140801e80b2d8767e95d215850b6feab3642ee2b6fd70fbf4aab09afcfe7fca public/sh/install_zpmod.sh -0cca0bb9f95101b4bf392d6aa3c23123602d99a7cbb0decacaa72973e016d89d public/sh/install.sh +f9d8ef697ba682bdfe66fdd67d4517caa4e62bb8baf2868ec8ed9932809b76aa public/sh/install.sh 08cc893ceb982fc99d17db1966c6c30790cc571e16e4f5392352d995f5252952 public/sh/sync-init.sh 5c7af31d7fc848d80f9fedf69c75e6844503f550a3d5e020bc7cdfdaca624763 public/zsh/init.zsh diff --git a/public/sh/generate-checksums.sh b/public/sh/generate-checksums.sh new file mode 100755 index 0000000..318dc9c --- /dev/null +++ b/public/sh/generate-checksums.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env sh +# -*- mode: sh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=sh sw=2 ts=2 et +# +# generate-checksums.sh — regenerate public/checksum.txt +# +# Usage (from any directory): +# sh public/sh/generate-checksums.sh +# +# This script is also invoked by the CI workflows before running +# tests/installers.sh so that public/checksum.txt is always current. + +set -eu + +ROOT="$( + unset CDPATH + cd "$(dirname "$0")/../.." 2>/dev/null && pwd +)" || { printf '%s\n' "generate-checksums: cannot determine repository root" >&2; exit 1; } + +CHECKSUM_FILE="${ROOT}/public/checksum.txt" + +sha256_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + else + printf '%s\n' "generate-checksums: sha256sum or shasum is required" >&2 + exit 1 + fi +} + +# Clear (or create) the checksum file before writing fresh entries with `: > file`. +: > "${CHECKSUM_FILE}" +for f in \ + public/sh/install_zpmod.sh \ + public/sh/install.sh \ + public/sh/sync-init.sh \ + public/zsh/init.zsh +do + hash="$(sha256_file "${ROOT}/${f}")" + printf '%s %s\n' "${hash}" "${f}" >> "${CHECKSUM_FILE}" +done + +printf '%s\n' "Checksums written to ${CHECKSUM_FILE}" diff --git a/public/sh/install.sh b/public/sh/install.sh index 1eeccc7..2062910 100755 --- a/public/sh/install.sh +++ b/public/sh/install.sh @@ -36,6 +36,17 @@ while getopts ":i:a:b:" opt; do done shift $((OPTIND - 1)) +# Validate BOPT to prevent sed delimiter injection when building init.zsh. +# | is the sed delimiter used in the substitution; \ and & are special in +# sed replacement expressions. The *[\\]* pattern matches a single backslash. +case "${BOPT}" in + # [\\] is a bracket expression for a literal backslash. + *'|'* | *[\\]* | *'&'* ) + printf '%s\n' "-- ERROR -- Invalid -b value: branch name must not contain '|', '\\', or '&'." >&2 + exit 1 + ;; +esac + SCRIPT_DIR="" LOCAL_INIT_ZSH="" LOCAL_INSTALL_ZPMOD="" @@ -147,6 +158,20 @@ fi command chmod a+x /tmp/zi/git-process-output.zsh if test -d "${ZI_HOME}/${ZI_BIN_DIR_NAME}/.git"; then + _zi_valid=0 + if test -f "${ZI_HOME}/${ZI_BIN_DIR_NAME}/zi.zsh"; then + # Canonical zi remote URLs (HTTPS and SSH, with and without .git suffix) + case "$(command git -C "${ZI_HOME}/${ZI_BIN_DIR_NAME}" remote get-url origin 2>/dev/null || true)" in + https://github.com/z-shell/zi | https://github.com/z-shell/zi.git \ + | git@github.com:z-shell/zi | git@github.com:z-shell/zi.git) + _zi_valid=1 ;; + esac + fi + if [ "${_zi_valid}" -ne 1 ]; then + printf '%s\n' "▓▒░ ${ZI_HOME}/${ZI_BIN_DIR_NAME} contains a .git directory but does not appear to be a zi repository." >&2 + printf '%s\n' "▓▒░ Expected zi.zsh and a z-shell/zi remote origin. Unset ZI_HOME/ZI_BIN_DIR_NAME or remove the directory to install fresh." >&2 + exit 1 + fi cd "${ZI_HOME}/${ZI_BIN_DIR_NAME}" || exit 1 printf '%s\n' "▓▒░ Updating (z-shell/zi) plugin manager at ${ZI_HOME}/${ZI_BIN_DIR_NAME}" command git clean -d -f -f diff --git a/tests/installers.sh b/tests/installers.sh index 2be2319..26a8418 100755 --- a/tests/installers.sh +++ b/tests/installers.sh @@ -139,6 +139,12 @@ EOF #!/usr/bin/env sh set -eu +# Strip -C flag if present (used by install.sh to check remote URL) +if [ "${1:-}" = "-C" ]; then + [ -n "${2:-}" ] || { printf '%s\n' "installers.sh git test double: -C requires a directory argument" >&2; exit 64; } + shift 2 +fi + cmd="${1:-}" [ "$#" -gt 0 ] && shift @@ -148,7 +154,7 @@ case "${cmd}" in for arg do dest="${arg}" done - [ -n "${dest}" ] || { printf '%s\n' "git test double: missing clone destination" >&2; exit 64; } + [ -n "${dest}" ] || { printf '%s\n' "installers.sh git test double: missing clone destination" >&2; exit 64; } mkdir -p "${dest}/.git" "${dest}/lib" printf '%s\n' '# fake zi.zsh' > "${dest}/zi.zsh" printf '%s\n' '# fake _zi completion' > "${dest}/lib/_zi" @@ -158,8 +164,21 @@ case "${cmd}" in log) printf '%s\n' 'abcdef0 - fake zi commit (now) ' ;; + remote) + # After -C strip (if any) and cmd shift, $1/$2 hold the remote subcommand args + if [ "${1:-}" != "get-url" ]; then + printf '%s\n' "installers.sh git test double: expected remote subcommand 'get-url', got '${1:-}'" >&2 + exit 65 + fi + if [ "${2:-}" != "origin" ]; then + printf '%s\n' "installers.sh git test double: expected remote name 'origin', got '${2:-}'" >&2 + exit 65 + fi + # Return a zi remote URL; override via ZI_SRC_TEST_FAKE_REMOTE env var + printf '%s\n' "${ZI_SRC_TEST_FAKE_REMOTE:-https://github.com/z-shell/zi}" + ;; *) - printf '%s\n' "git test double: unexpected command ${cmd}" >&2 + printf '%s\n' "installers.sh git test double: unexpected command ${cmd}" >&2 exit 65 ;; esac @@ -226,6 +245,70 @@ test_standalone_zpmod_delegation() { pass "standalone install.sh fetches zpmod helper" } +test_update_valid_zi_clone() { + home="${TMP_ROOT}/update-valid-home" + data="${TMP_ROOT}/update-valid-data" + zi_bin="${data}/zi/bin" + command mkdir -p "${home}" "${zi_bin}/.git" + printf '%s\n' '# fake zi.zsh' >"${zi_bin}/zi.zsh" + + HOME="${home}" \ + ZDOTDIR="${home}" \ + XDG_DATA_HOME="${data}" \ + ZI_SRC_TEST_ROOT="${ROOT}" \ + PATH="${FAKE_BIN}:${PATH}" \ + sh "${ROOT}/public/sh/install.sh" -i skip >/dev/null + + pass "update path accepts a valid zi clone" +} + +test_update_rejects_foreign_repo() { + home="${TMP_ROOT}/update-foreign-home" + data="${TMP_ROOT}/update-foreign-data" + zi_bin="${data}/zi/bin" + command mkdir -p "${home}" "${zi_bin}/.git" + # Deliberately no zi.zsh: this simulates an unrelated git repo + err="${TMP_ROOT}/update-foreign-err" + + set +e + HOME="${home}" \ + ZDOTDIR="${home}" \ + XDG_DATA_HOME="${data}" \ + ZI_SRC_TEST_ROOT="${ROOT}" \ + PATH="${FAKE_BIN}:${PATH}" \ + sh "${ROOT}/public/sh/install.sh" -i skip >/dev/null 2>"${err}" + exit_code="$?" + set -e + + [ "${exit_code}" -ne 0 ] || fail "install.sh should have rejected a foreign git repository" + contains "${err}" "does not appear to be a zi repository" + pass "update path rejects an unrecognised git repository" +} + +test_update_rejects_wrong_remote() { + home="${TMP_ROOT}/update-wrong-remote-home" + data="${TMP_ROOT}/update-wrong-remote-data" + zi_bin="${data}/zi/bin" + command mkdir -p "${home}" "${zi_bin}/.git" + printf '%s\n' '# fake zi.zsh' >"${zi_bin}/zi.zsh" + err="${TMP_ROOT}/update-wrong-remote-err" + + set +e + HOME="${home}" \ + ZDOTDIR="${home}" \ + XDG_DATA_HOME="${data}" \ + ZI_SRC_TEST_ROOT="${ROOT}" \ + ZI_SRC_TEST_FAKE_REMOTE="https://github.com/unrelated/project" \ + PATH="${FAKE_BIN}:${PATH}" \ + sh "${ROOT}/public/sh/install.sh" -i skip >/dev/null 2>"${err}" + exit_code="$?" + set -e + + [ "${exit_code}" -ne 0 ] || fail "install.sh should have rejected a repo with a non-zi remote" + contains "${err}" "does not appear to be a zi repository" + pass "update path rejects a repository with a non-zi remote origin" +} + test_sync_init() { local_file="${TMP_ROOT}/local-init.zsh" remote_file="${TMP_ROOT}/remote-init.zsh" @@ -265,4 +348,7 @@ write_fake_tools test_loader_install test_xdg_data_home_install test_standalone_zpmod_delegation +test_update_valid_zi_clone +test_update_rejects_foreign_repo +test_update_rejects_wrong_remote test_sync_init