#!/bin/sh # Public downloads only. Never starts Meowland or changes shell profiles/data. set -eu umask 077 LC_ALL=C export LC_ALL unset TAR_OPTIONS fail() { printf '%s\n' "Meowland: $*" >&2; exit 1; } case "$(uname -s)" in Darwin) platform=darwin ;; Linux) platform=linux ;; *) fail 'Unsupported operating system (macOS and Linux only).' ;; esac case "$(uname -m)" in arm64|aarch64) architecture=arm64 ;; x86_64|amd64) architecture=x64 ;; *) fail 'Unsupported architecture (arm64 and x64 only).' ;; esac target=$platform-$architecture for tool in curl tar awk mktemp mkdir mv ln readlink chmod rmdir; do command -v "$tool" >/dev/null 2>&1 || fail "Required command not found: $tool" done if command -v sha256sum >/dev/null 2>&1; then hash_tool=sha256sum elif command -v shasum >/dev/null 2>&1; then hash_tool=shasum else fail 'Install sha256sum or shasum before continuing.' fi # Absolute, non-root paths only. Spaces are fine; control characters are not. safe_path() { case "$1" in /*) ;; *) fail 'Install and bin paths must be absolute.' ;; esac case "$1" in /|*/|*//*|*/./*|*/../*|*/.|*/..) fail 'Unsafe install or bin path.' ;; esac printf '%s\n' "$1" | awk 'NR != 1 || /[[:cntrl:]]/ { bad=1 } END { exit bad }' || fail 'Invalid path characters.' [ "$1" != "${HOME:-}" ] || fail 'Use a dedicated directory, not your home directory.' } install_dir=${MEOWLAND_INSTALL_DIR:-${HOME:?HOME is required}/.local/share/meowland} bin_dir=${MEOWLAND_BIN_DIR:-${HOME:?HOME is required}/.local/bin} safe_path "$install_dir" safe_path "$bin_dir" [ "$install_dir" != "$bin_dir" ] || fail 'Install and bin directories must differ.' base_url=${MEOWLAND_DOWNLOAD_BASE_URL:-https://meowland.dev} base_url=${base_url%/} local_test=false case "$base_url" in https://*) ;; http://127.0.0.1:*|http://localhost:*|http://\[::1\]:*) # Explicit loopback override only; no HTTP redirects, credentials or queries. printf '%s\n' "$base_url" | awk ' /^http:\/\/(127\.0\.0\.1|localhost|\[::1\]):[0-9]+(\/[A-Za-z0-9._~\/-]*)?$/ { ok=1 } END { exit !ok }' || fail 'Invalid loopback download base URL.' local_test=true ;; *) fail 'Download base URL must use HTTPS (explicit loopback HTTP is allowed for tests).' ;; esac case "$base_url" in *'@'*|*'?'*|*'#'*) fail 'Invalid download base URL.' ;; esac download() { case "$1" in https://*) curl -q --fail --silent --show-error --location --proto '=https' --proto-redir '=https' --connect-timeout 15 --max-time 900 --output "$2" "$1" ;; "$base_url"/*) [ "$local_test" = true ] || fail 'Archive URL must use HTTPS.' curl -q --fail --silent --show-error --proto '=http' --connect-timeout 15 --max-time 900 --output "$2" "$1" ;; *) fail 'Archive URL must use HTTPS, or the explicit loopback test base.' ;; esac } # Keep a failed stage available for inspection; never recursively delete files. stage=$(mktemp -d "${TMPDIR:-/tmp}/meowland-install.XXXXXX") || fail 'Cannot create temporary staging directory.' lock_dir= link_stage= cleanup() { if [ -n "$lock_dir" ]; then rmdir "$lock_dir" 2>/dev/null || :; fi if [ -n "$link_stage" ]; then rmdir "$link_stage" 2>/dev/null || :; fi printf '%s\n' "Installer staging retained: $stage" >&2 } trap cleanup 0 trap 'exit 130' INT trap 'exit 143' TERM HUP download "$base_url/releases/latest.txt" "$stage/latest.txt" || fail 'Cannot download release manifest.' # Select exactly one row, with no shell evaluation of manifest values. awk -F '\t' -v target="$target" ' $1 == target { if (NF != 4 || ++count != 1 || $2 !~ /^v?[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9]+([.-][A-Za-z0-9]+)*)?$/ || $3 !~ /^https?:\/\// || $3 ~ /[[:space:]@]/ || length($4) != 64 || $4 ~ /[^a-fA-F0-9]/) bad=1 version=$2; url=$3; checksum=tolower($4) } END { if (bad || count != 1) exit 1; print version; print url; print checksum } ' "$stage/latest.txt" > "$stage/selected" || fail "No valid unique release for $target in the manifest. This platform may not be published yet." { IFS= read -r version IFS= read -r archive_url IFS= read -r expected_hash } < "$stage/selected" release_name=$version-$target archive_root=meowland-$release_name download "$archive_url" "$stage/release.tar.gz" || fail 'Cannot download release archive.' if [ "$hash_tool" = sha256sum ]; then sha256sum "$stage/release.tar.gz" > "$stage/checksum" else shasum -a 256 "$stage/release.tar.gz" > "$stage/checksum" fi actual_hash=$(awk '{print tolower($1)}' "$stage/checksum") [ "$actual_hash" = "$expected_hash" ] || fail 'SHA-256 mismatch. Existing installation is unchanged.' # Validate both names and entry types before extraction. Releases have no links. tar -tzf "$stage/release.tar.gz" > "$stage/paths" || fail 'Cannot list archive.' awk -v root="$archive_root" ' { if ($0 !~ /^[A-Za-z0-9_.\/+ @-]+$/ || ($0 != root && $0 != root "/" && index($0,root "/") != 1)) exit 1 n=split($0,parts,"/") for (i=1;i<=n;i++) if (parts[i]==".." || parts[i]=="." || (parts[i]=="" && i "$stage/types" || fail 'Cannot inspect archive entry types.' awk 'substr($0,1,1)!="-" && substr($0,1,1)!="d" { exit 1 }' "$stage/types" || fail 'Archive links and special files are not allowed.' mkdir "$stage/unpacked" tar -xzf "$stage/release.tar.gz" --no-same-owner --no-same-permissions -C "$stage/unpacked" || fail 'Cannot extract verified archive.' release=$stage/unpacked/$archive_root [ -f "$release/release.json" ] && [ -f "$release/bin/meowland" ] && [ -x "$release/bin/meowland" ] || fail 'Archive is missing release.json or executable bin/meowland.' # Serialize installers; the running application uses a different launcher lock. mkdir -p "$install_dir" "$bin_dir" install_dir=$(CDPATH= cd -P "$install_dir" && pwd) bin_dir=$(CDPATH= cd -P "$bin_dir" && pwd) safe_path "$install_dir" safe_path "$bin_dir" [ "$install_dir" != "$bin_dir" ] || fail 'Install and bin directories resolve to the same directory.' mkdir "$install_dir/.install.lock" 2>/dev/null || fail 'Another install is active, or .install.lock remains after interruption; inspect it before retrying.' lock_dir=$install_dir/.install.lock [ ! -L "$install_dir/releases" ] || fail 'The releases directory must not be a symlink.' mkdir -p "$install_dir/releases" destination=$install_dir/releases/$release_name [ ! -e "$destination" ] && [ ! -L "$destination" ] || fail "Release $release_name already exists; it was not overwritten." launcher_link=$bin_dir/meowland if [ -L "$launcher_link" ]; then previous=$(readlink "$launcher_link") case "$previous" in "$install_dir"/releases/*/bin/meowland) ;; *) fail 'Existing meowland symlink is not managed by this install directory.' ;; esac [ ! -d "$launcher_link" ] || fail 'Existing meowland link points to a directory.' elif [ -e "$launcher_link" ]; then fail 'Existing meowland command is not an installer-managed symlink.' fi # Stage on the destination filesystem before publishing the complete directory. publish_stage=$(mktemp -d "$install_dir/releases/.install.XXXXXX") mv "$release" "$publish_stage/$release_name" mv "$publish_stage/$release_name" "$destination" rmdir "$publish_stage" link_stage=$(mktemp -d "$bin_dir/.meowland-link.XXXXXX") ln -s "$destination/bin/meowland" "$link_stage/meowland" mv -f "$link_stage/meowland" "$launcher_link" printf '%s\n' "Installed Meowland $version ($target)." "Command: $launcher_link" 'Run meowland when ready. No application was started.' case ":${PATH:-}:" in *":$bin_dir:"*) ;; *) printf '%s\n' "Add $bin_dir to your PATH, or run the absolute command above. Shell profiles were not changed." ;; esac