#!/bin/sh set -eu bin=$(realpath "$1") tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT HUP INT TERM root="$tmp/root" tree="$tmp/tree" mkdir -p "$root/usr/share" expect() { expected=$1 shift if "$@" > "$tmp/out" 2> "$tmp/err"; then actual=0; else actual=$?; fi test "$actual" -eq "$expected" || { cat "$tmp/out" "$tmp/err"; exit 1; } } package() { label=$1 name=$2 path=$3 dependency=$4 mkdir -p "$tree/$label/HOLY" "$tree/$label/DATA/usr/share" printf 'format holy-package-1\nname %s\nversion %s\nrelease 1\nos linux\narch noarch\nlibc nolibc\nsource-name forged\n' "$name" "$label" > "$tree/$label/HOLY/meta" for field in deps provides hooks origin transform; do : > "$tree/$label/HOLY/$field"; done if test -n "$dependency"; then printf 'require dep-1 %s package %s any any any - %s metadata\n' "$name" "$dependency" "$dependency" > "$tree/$label/HOLY/deps" fi if test "${5:-file}" = link; then ln -s app "$tree/$label/DATA/usr/share/$path" else printf '%s\n' "$label" > "$tree/$label/DATA/usr/share/$path" fi "$bin" manifest generate "$tree/$label" --output "$tmp/files" > "$tmp/out" if test "${6:-}" = config; then sed '/^file /s/ none - / config - /' "$tmp/files" > "$tmp/config-files" mv "$tmp/config-files" "$tmp/files" fi mv "$tmp/files" "$tree/$label/HOLY/files" "$bin" pack "$tree/$label" --output "$tmp/$label.holy" > "$tmp/out" "$bin" cache stage "local:$tmp/$label.holy" --root "$root" > "$tmp/out" } hash() { sha256sum "$tmp/$1.holy" | cut -d ' ' -f 1; } set_hash() { sed -n 's/^plan-set .* sha256 \([0-9a-f]*\) read-only$/\1/p' "$tmp/out"; } install() { expect 0 "$bin" db plan-set "$@" --root "$root" plan=$(set_hash) expect 0 "$bin" db apply-set "$plan" "$@" --root "$root" } register() { expect 0 "$bin" source plan --config "$tmp/config" --root "$root" cp "$tmp/out" "$tmp/source-plan" digest=$(sha256sum "$tmp/source-plan" | cut -d ' ' -f 1) expect 0 "$bin" source apply "$tmp/source-plan" --sha256 "$digest" --root "$root" } "$bin" db init --root "$root" > "$tmp/out" package base1 base payload '' package base2 base payload '' package app app app base package extra extra extra '' package missing base payload absent package collision base extra '' package rename other payload '' package moved base relocated '' package linked base payload '' link old=$(hash base1) new=$(hash base2) app=$(hash app) extra=$(hash extra) install "$app" "$old" install "$extra" expect 0 "$bin" db plan-update "$old" "$new" --root "$root" cp "$tmp/out" "$tmp/plan" reserved=$(awk '$1 == "change" && $3 == "replace" {print $2; exit}' "$tmp/plan") printf 'unowned\n' > "$root/usr/share/.holy-update-$reserved" expect 4 "$bin" db apply-update "$(sed -n 's/^plan-update sha256 \([0-9a-f]*\) read-only$/\1/p' "$tmp/plan")" "$old" "$new" --root "$root" grep -qx unowned "$root/usr/share/.holy-update-$reserved" test ! -e "$root/var/lib/holypkg/transactions/update" rm "$root/usr/share/.holy-update-$reserved" expect 0 "$bin" db plan-update "$old" "$new" --root "$root" cmp "$tmp/plan" "$tmp/out" python3 - "$tmp/plan" "$old" "$new" "$app" "$extra" <<'PY' import hashlib, pathlib, shlex, sys data = pathlib.Path(sys.argv[1]).read_bytes() header, record = data.split(b'\n', 1) assert header.decode().split() == ['plan-update', 'sha256', hashlib.sha256(record).hexdigest(), 'read-only'] rows = [shlex.split(line) for line in record.decode().splitlines()] old, new, app, extra = sys.argv[2:] assert ['old', old] in rows and ['new', new] in rows assert ['source', '-', 'local'] in rows assert {r[1] for r in rows if r[0] == 'instance'} == {old, app, extra} assert {r[1] for r in rows if r[0] == 'artifact'} == {new, app, extra} assert ['edge', app, 'dep-1', new, '-', 'package', 'base'] in rows assert any(r[0] == 'change' and r[2] == 'replace' for r in rows) PY grep -qx base1 "$root/usr/share/payload" expect 0 "$bin" db check --all --root "$root" expect 4 "$bin" db plan-update "$old" "$(hash missing)" --root "$root" test ! -s "$tmp/out" expect 4 "$bin" db plan-update "$old" "$(hash collision)" --root "$root" expect 4 "$bin" db plan-update "$old" "$(hash rename)" --root "$root" expect 3 "$bin" db plan-update "$old" "$old" --root "$root" expect 6 "$bin" db plan-update "$new" "$old" --root "$root" expect 2 "$bin" db plan-update invalid "$new" --root "$root" printf 'user change\n' > "$root/usr/share/payload" expect 4 "$bin" db plan-update "$old" "$new" --root "$root" printf 'base1\n' > "$root/usr/share/payload" printf 'user change\n' > "$root/usr/share/extra" expect 4 "$bin" db plan-update "$old" "$new" --root "$root" printf 'extra\n' > "$root/usr/share/extra" expect 0 "$bin" db reserve "$new" --root "$root" expect 5 "$bin" db plan-update "$old" "$new" --root "$root" expect 0 "$bin" db cancel --root "$root" cache="$root/var/cache/holypkg/objects/sha256" mv "$cache/$old.holy" "$tmp/old-cache" expect 6 "$bin" db plan-update "$old" "$new" --root "$root" mv "$tmp/old-cache" "$cache/$old.holy" expect 0 "$bin" db plan-update "$old" "$new" --root "$root" cmp "$tmp/plan" "$tmp/out" mkdir -p "$tmp/second/usr/share" cp -a "$root/." "$tmp/second/" expect 0 "$bin" db plan-update "$old" "$new" --root "$tmp/second" if cmp -s "$tmp/plan" "$tmp/out"; then exit 1; fi copy_root="$tmp/second" update_hash() { sed -n 's/^plan-update sha256 \([0-9a-f]*\) read-only$/\1/p' "$tmp/out"; } approved=$(sed -n 's/^plan-update sha256 \([0-9a-f]*\) read-only$/\1/p' "$tmp/plan") expect 3 "$bin" db apply-update "$approved" "$old" "$new" --root "$copy_root" replace() { expect 0 "$bin" db plan-update "$1" "$2" --root "$copy_root" approved=$(update_hash) expect 0 "$bin" db apply-update "$approved" "$1" "$2" --root "$copy_root" expect 0 "$bin" db status --root "$copy_root" expect 0 "$bin" db check --all --root "$copy_root" test ! -d "$copy_root/var/lib/holypkg/installed/$1" test -d "$copy_root/var/lib/holypkg/installed/$2" grep -qx "$approved" "$copy_root/var/lib/holypkg/transactions/$approved/committed" grep -qx 'reason dependency' "$copy_root/var/lib/holypkg/installed/$2/state" expect 0 "$bin" orphan --root "$copy_root" --json } replace "$old" "$new" grep -qx base2 "$copy_root/usr/share/payload" grep -q "\"$new\"" "$copy_root/var/lib/holypkg/installed/$app/graph" if grep -q "\"$old\"" "$copy_root/var/lib/holypkg/installed/$app/graph"; then exit 1; fi rollback_root="$tmp/rollback-root" mkdir "$rollback_root" cp -a "$copy_root/." "$rollback_root/" expect 0 "$bin" rollback "$approved" --root "$rollback_root" rollback_plan=$(sed -n 's/^rollback-plan .* sha256 \([0-9a-f]*\) read-only$/\1/p' "$tmp/out") test "${#rollback_plan}" -eq 64 grep -qx "old $new" "$tmp/out" grep -qx "new $old" "$tmp/out" transaction_dir="$rollback_root/var/lib/holypkg/transactions/$approved" cp "$transaction_dir/journal" "$tmp/rollback-journal" printf 'forged\n' > "$transaction_dir/journal" expect 2 "$bin" rollback "$approved" --root "$rollback_root" cp "$tmp/rollback-journal" "$transaction_dir/journal" cp "$transaction_dir/plan" "$tmp/rollback-source-plan" printf 'forged\n' > "$transaction_dir/plan" expect 2 "$bin" rollback "$approved" --root "$rollback_root" cp "$tmp/rollback-source-plan" "$transaction_dir/plan" expect 3 "$bin" rollback "$approved" --root "$rollback_root" --apply "$approved" grep -qx base2 "$rollback_root/usr/share/payload" expect 0 "$bin" rollback "$approved" --root "$rollback_root" --apply "$rollback_plan" grep -qx base1 "$rollback_root/usr/share/payload" expect 0 "$bin" db check --all --root "$rollback_root" test -d "$rollback_root/var/lib/holypkg/installed/$old" test ! -d "$rollback_root/var/lib/holypkg/installed/$new" expect 6 "$bin" rollback "$approved" --root "$rollback_root" expect 2 "$bin" rollback invalid --root "$rollback_root" expect 3 "$bin" db rm "$new" --root "$copy_root" replace "$new" "$(hash moved)" test ! -e "$copy_root/usr/share/payload" grep -qx moved "$copy_root/usr/share/relocated" replace "$(hash moved)" "$(hash linked)" test ! -e "$copy_root/usr/share/relocated" test "$(readlink "$copy_root/usr/share/payload")" = app replace "$(hash linked)" "$old" grep -qx base1 "$copy_root/usr/share/payload" expect 0 "$bin" db rm "$extra" --root "$copy_root" expect 0 "$bin" db check --all --root "$copy_root" if "$bin" elf "$bin" | grep -q '^interpreter /'; then gcc -shared -fPIC -o "$tmp/update-fault.so" "$(dirname "$0")/update-fault.c" -ldl fault_client=dynamic elif test "${HOLY_TEST_STATIC_UPDATE_FAULT:-0}" = 1; then fault_client=static else fault_client=skip printf 'update fault injection skipped for uninstrumented static client\n' fi if test "$fault_client" != skip; then for phase in intent-after payload-before payload-after database-before database-after generation-after committed no-space staging-partial; do copy_root="$tmp/fault-$phase" mkdir "$copy_root" cp -a "$root/." "$copy_root/" expect 0 "$bin" db plan-update "$old" "$new" --root "$copy_root" approved=$(update_hash) if test "$fault_client" = dynamic; then if env LD_PRELOAD="$tmp/update-fault.so" HOLY_UPDATE_FAULT="$phase" HOLY_UPDATE_NEW="$new" \ "$bin" db apply-update "$approved" "$old" "$new" --root "$copy_root" > "$tmp/out" 2> "$tmp/err"; then exit 1; else code=$?; fi else if env HOLY_UPDATE_FAULT="$phase" HOLY_UPDATE_NEW="$new" \ "$bin" db apply-update "$approved" "$old" "$new" --root "$copy_root" > "$tmp/out" 2> "$tmp/err"; then exit 1; else code=$?; fi fi if test "$phase" = no-space; then test "$code" -eq 5; else test "$code" -eq 137; fi expect 5 "$bin" db status --root "$copy_root" expect 5 "$bin" db reserve "$extra" --root "$copy_root" for blocked in cancel preflight plan recheck apply; do expect 5 "$bin" db "$blocked" --root "$copy_root" done expect 5 "$bin" db approve "$approved" --root "$copy_root" expect 5 "$bin" db check --all --root "$copy_root" expect 5 "$bin" db plan-set "$extra" --root "$copy_root" if test "$phase" = staging-partial; then grep -qx base1 "$copy_root/usr/share/payload" expect 5 "$bin" db recover --update --root "$copy_root" grep -q 'reserved update object requires inspection' "$tmp/err" test "$(find "$copy_root/usr/share" -name '.holy-update-*' | wc -l)" -eq 1 find "$copy_root/usr/share" -name '.holy-update-*' -type f -delete fi expect 0 "$bin" db recover --update --root "$copy_root" expect 0 "$bin" db status --root "$copy_root" expect 0 "$bin" db check --all --root "$copy_root" expect 0 "$bin" orphan --root "$copy_root" --json grep -qx base2 "$copy_root/usr/share/payload" test ! -d "$copy_root/var/lib/holypkg/installed/$old" grep -qx "$approved" "$copy_root/var/lib/holypkg/transactions/$approved/committed" test "$(find "$copy_root/usr/share" -name '.holy-update-*' | wc -l)" -eq 0 done fi cat > "$tmp/config" <<'EOF' [source official] type holy-http url https://source.example/holy EOF register expect 0 "$bin" source list --root "$root" source=$(sed -n 's/^source \([0-9a-f]*\) "official" active$/\1/p' "$tmp/out") package sourced1 sourced sourced '' package sourced2 sourced sourced '' first=$(hash sourced1) second=$(hash sourced2) install "$first" --source "$first=$source" expect 0 "$bin" db plan-update "$first" "$second" --root "$root" cp "$tmp/out" "$tmp/sourced-plan" grep -qx "source $source \"official\"" "$tmp/out" sed 's/source official/source renamed/' "$tmp/config" > "$tmp/renamed" mv "$tmp/renamed" "$tmp/config" register expect 0 "$bin" db plan-update "$first" "$second" --root "$root" grep -qx "source $source \"renamed\"" "$tmp/out" if cmp -s "$tmp/sourced-plan" "$tmp/out"; then exit 1; fi : > "$tmp/config" register expect 6 "$bin" db plan-update "$first" "$second" --root "$root" expect 0 "$bin" db check --all --root "$root" cat > "$tmp/config" <<'EOF' [source renamed] type holy-http url https://source.example/holy EOF register expect 0 "$bin" db plan-update "$first" "$second" --root "$root" approved=$(update_hash) expect 0 "$bin" db apply-update "$approved" "$first" "$second" --root "$root" grep -qx "source-id $source" "$root/var/lib/holypkg/installed/$second/state" grep -qx "source $source \"renamed\"" "$root/var/lib/holypkg/installed/$second/source" grep -qx 'reason explicit' "$root/var/lib/holypkg/installed/$second/state" expect 0 "$bin" db check --all --root "$root" : > "$tmp/config" register expect 0 "$bin" db check --all --root "$root" package config1 settings holy.conf '' file config package config2 settings holy.conf '' file config package config3 settings holy.conf '' file config config_old=$(hash config1) config_new=$(hash config2) config_third=$(hash config3) install "$config_old" expect 0 "$bin" db plan-update "$config_old" "$config_new" --root "$root" grep -q ' config$' "$tmp/out" printf 'local edit\n' > "$root/usr/share/holy.conf" expect 4 "$bin" db check "$config_old" --root "$root" --json grep -q 'changed-config' "$tmp/out" expect 0 "$bin" db plan-update "$config_old" "$config_new" --root "$root" grep -q 'usr/share/holy.conf.holy-new' "$tmp/out" config_plan=$(update_hash) printf 'changed after preview\n' > "$root/usr/share/holy.conf" expect 3 "$bin" db apply-update "$config_plan" "$config_old" "$config_new" --root "$root" test ! -e "$root/usr/share/holy.conf.holy-new" printf 'local edit\n' > "$root/usr/share/holy.conf" if test "$fault_client" != skip; then config_fault="$tmp/config-fault" mkdir "$config_fault" cp -a "$root/." "$config_fault/" expect 0 "$bin" db plan-update "$config_old" "$config_new" --root "$config_fault" fault_plan=$(update_hash) if test "$fault_client" = dynamic; then if env LD_PRELOAD="$tmp/update-fault.so" HOLY_UPDATE_FAULT=database-before HOLY_UPDATE_NEW="$config_new" \ "$bin" db apply-update "$fault_plan" "$config_old" "$config_new" --root "$config_fault" > "$tmp/out" 2> "$tmp/err"; then exit 1; else code=$?; fi else if env HOLY_UPDATE_FAULT=database-before HOLY_UPDATE_NEW="$config_new" \ "$bin" db apply-update "$fault_plan" "$config_old" "$config_new" --root "$config_fault" > "$tmp/out" 2> "$tmp/err"; then exit 1; else code=$?; fi fi test "$code" -eq 137 expect 5 "$bin" db status --root "$config_fault" expect 0 "$bin" db recover --update --root "$config_fault" grep -qx 'local edit' "$config_fault/usr/share/holy.conf" grep -qx config2 "$config_fault/usr/share/holy.conf.holy-new" expect 0 "$bin" db check --all --root "$config_fault" fi expect 0 "$bin" db apply-update "$config_plan" "$config_old" "$config_new" --root "$root" grep -qx 'local edit' "$root/usr/share/holy.conf" grep -qx config2 "$root/usr/share/holy.conf.holy-new" expect 0 "$bin" db check --all --root "$root" expect 0 "$bin" db owner usr/share/holy.conf.holy-new --root "$root" grep -qx "$config_new file usr/share/holy.conf.holy-new" "$tmp/out" expect 0 "$bin" db plan-update "$config_new" "$config_third" --root "$root" config_plan=$(update_hash) expect 0 "$bin" db apply-update "$config_plan" "$config_new" "$config_third" --root "$root" grep -qx 'local edit' "$root/usr/share/holy.conf" grep -qx config3 "$root/usr/share/holy.conf.holy-new" expect 0 "$bin" db check --all --root "$root" config_rollback="$tmp/config-rollback" mkdir "$config_rollback" cp -a "$root/." "$config_rollback/" expect 0 "$bin" rollback "$config_plan" --root "$config_rollback" rollback_plan=$(sed -n 's/^rollback-plan .* sha256 \([0-9a-f]*\) read-only$/\1/p' "$tmp/out") test "${#rollback_plan}" -eq 64 expect 0 "$bin" rollback "$config_plan" --root "$config_rollback" --apply "$rollback_plan" grep -qx 'local edit' "$config_rollback/usr/share/holy.conf" grep -qx config2 "$config_rollback/usr/share/holy.conf.holy-new" expect 0 "$bin" db check --all --root "$config_rollback" config_unrecoverable="$tmp/config-unrecoverable" mkdir "$config_unrecoverable" cp -a "$root/." "$config_unrecoverable/" rm "$config_unrecoverable/usr/share/holy.conf" expect 4 "$bin" db repair-plan "$config_third" --root "$config_unrecoverable" test ! -e "$config_unrecoverable/var/lib/holypkg/transactions/journal" config_repair="$tmp/config-repair" mkdir "$config_repair" cp -a "$root/." "$config_repair/" rm "$config_repair/usr/share/holy.conf.holy-new" expect 0 "$bin" db repair-plan "$config_third" --root "$config_repair" repair_plan=$(sed -n 's/^repair-plan .* sha256 \([0-9a-f]*\) missing-only read-only$/\1/p' "$tmp/out") test "${#repair_plan}" -eq 64 expect 0 "$bin" db repair "$config_third" --plan "$repair_plan" --root "$config_repair" grep -qx 'local edit' "$config_repair/usr/share/holy.conf" grep -qx config3 "$config_repair/usr/share/holy.conf.holy-new" expect 0 "$bin" db check --all --root "$config_repair" expect 0 "$bin" db plan-update "$old" "$new" --root "$root" base_plan=$(update_hash) expect 0 "$bin" db apply-update "$base_plan" "$old" "$new" --root "$root" grep -qx 'local edit' "$root/usr/share/holy.conf" grep -qx config3 "$root/usr/share/holy.conf.holy-new" expect 0 "$bin" db check --all --root "$root" expect 0 "$bin" db rm "$config_third" --root "$root" grep -qx 'local edit' "$root/usr/share/holy.conf" test ! -e "$root/usr/share/holy.conf.holy-new" expect 6 "$bin" db owner usr/share/holy.conf --root "$root" expect 0 "$bin" db check --all --root "$root" printf 'update transaction fixtures passed\n'