#!/usr/bin/env bash # Snapshots of the settings store. # # The restore path overwrites the file the whole desktop reads, so the # properties that matter are: a corrupt snapshot is never restored over a # working configuration, a restore snapshots what it replaces so it is itself # undoable, and a snapshot name cannot be used to reach a file outside the # backup directory. set -euo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" helper="$repo_dir/config/dot/quickshell/scripts/panama-settings-backup" work="$(mktemp -d /tmp/panama-backup-contract.XXXXXX)" fail() { printf 'settings backup contract: %s\n' "$1" >&2 exit 1 } cleanup() { rm -rf "$work"; } trap cleanup EXIT settings="$work/config/panama/settings.json" backups="$work/state/panama/backups" mkdir -p "$(dirname "$settings")" run() { XDG_CONFIG_HOME="$work/config" XDG_STATE_HOME="$work/state" "$helper" "$@"; } # ── Nothing to back up ─────────────────────────────────────────────────────── run save >/dev/null 2>&1 && fail 'backing up a missing settings file reported success' [[ "$(run list)" == "[]" ]] || fail 'an empty backup directory did not list as empty' # ── A snapshot round-trips ─────────────────────────────────────────────────── printf '{"gapsOut":24,"windowRounding":6}' >"$settings" run save >/dev/null || fail 'save failed on a valid settings file' name="$(run list | jq -r '.[0].name')" [[ "$name" =~ ^settings-[0-9]{8}-[0-9]{9}\.json$ ]] || fail "unexpected snapshot name: $name" [[ "$(run list | jq -r '.[0].keys')" == "2" ]] || fail 'snapshot key count is wrong' printf '{"gapsOut":99}' >"$settings" run restore "$name" >/dev/null || fail 'restore failed' [[ "$(jq -r .gapsOut "$settings")" == "24" ]] || fail 'restore did not bring back the snapshot contents' [[ "$(jq -r .windowRounding "$settings")" == "6" ]] || fail 'restore lost a key' # ── Restoring snapshots what it replaced, so it is undoable ────────────────── count="$(run list | jq 'length')" [[ "$count" -ge 2 ]] || fail "restore did not snapshot the replaced settings (only $count snapshots)" # ── A corrupt snapshot is refused ──────────────────────────────────────────── bad="settings-19990101-000000000.json" mkdir -p "$backups" printf '{ truncated' >"$backups/$bad" run restore "$bad" >/dev/null 2>&1 && fail 'a corrupt snapshot was restored' [[ "$(jq -r .gapsOut "$settings")" == "24" ]] || fail 'a refused restore still damaged the settings file' # ── A snapshot cannot name a path outside the backup directory ─────────────── printf '{"pwned":true}' >"$work/outside.json" run restore "../../outside.json" >/dev/null 2>&1 && fail 'a traversing snapshot name was accepted' run restore "/etc/passwd" >/dev/null 2>&1 && fail 'an absolute snapshot path was accepted' jq -e 'has("pwned") | not' "$settings" >/dev/null || fail 'a file outside the backup directory was restored' # ── A snapshot that is not listed is refused ───────────────────────────────── run restore "settings-20000101-000000000.json" >/dev/null 2>&1 && fail 'a missing snapshot was reported restored' # ── Snapshots are capped ───────────────────────────────────────────────────── for _ in $(seq 1 20); do printf '{"n":%s}' "$RANDOM" >"$settings" run save >/dev/null done kept="$(run list | jq 'length')" [[ "$kept" -le 15 ]] || fail "snapshots are not capped: $kept kept" [[ "$kept" -ge 10 ]] || fail "snapshot pruning was too aggressive: only $kept kept" trap - EXIT cleanup printf 'settings backup contract: PASS\n'