#!/usr/bin/env bash # Carrying settings to another machine. # # The rules: # # 1. Export is an allow-list read from the preference schema, not a deny-list # of things to strip. A key added later that happens to hold a token must # not be able to leak into a file somebody emails to themselves. Being # wrong this way loses a setting; being wrong the other way publishes a # secret. # 2. What describes the machine stays on the machine. The display arrangement # is keyed by output names that mean nothing elsewhere. # 3. Every value is validated again on arrival, per key, with a reason. A file # from an older Panama is a normal thing to have, and refusing it wholesale # because one key changed shape would make the feature useless exactly when # it is most wanted. # 4. Validation covers every type the schema actually uses. It did not: "real" # was spelled "float" and enums were assumed to be words, so 36 settings -- # including every numeric enum -- were accepted unchecked, and numeric # enums were then refused outright once that was noticed. # 5. Import is a merge. Settings the file does not mention are left alone. # # Runs entirely against a temporary config home. The real settings store is read # for the export and never written. set -uo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" helper="$repo_dir/config/dot/quickshell/scripts/panama-settings-sync" schema="$repo_dir/config/dot/quickshell/config/PreferenceSchema.qml" fail() { printf 'settings sync contract: %s\n' "$1" >&2 exit 1 } [[ -x "$helper" ]] || fail 'panama-settings-sync is not executable' [[ -r "$schema" ]] || fail 'the preference schema is missing' work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT bundle="$work/export.json" field() { python3 -c "import json,sys; print(json.load(sys.stdin)$1)"; } # ── 1 & 2. Export carries taste, not hardware ─────────────────────────────── "$helper" export "$bundle" >"$work/export-result.json" || fail 'export failed' reason="$(field "['error']" <"$work/export-result.json")" [[ -z "$reason" ]] || fail "export reported: $reason" [[ "$(stat -c '%a' "$bundle")" == "600" ]] \ || fail 'the export is readable by other accounts' python3 - "$bundle" "$schema" "${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json" <<'PY' || fail 'the export carried the wrong things, in one direction or the other' import json, re, sys bundle = json.load(open(sys.argv[1])) schema = open(sys.argv[2]).read() settings = bundle["settings"] if not settings: raise SystemExit('the export carried nothing at all') declared = set(re.findall(r'key:\s*"([A-Za-z0-9_]+)"', schema)) for key in settings: if key not in declared: raise SystemExit(f'{key} is not declared in the schema but was exported') for forbidden in ("displays", "lastPage", "schemaVersion"): if forbidden in settings: raise SystemExit(f'{forbidden} describes this machine and must not travel') # Nothing credential-shaped, whatever the schema says about it. raw = json.dumps(settings) for marker in ("BEGIN ", "PRIVATE KEY", "Bearer "): if marker in raw: raise SystemExit(f'the export contains {marker!r}') for key, value in settings.items(): if isinstance(value, str) and len(value) > 300: raise SystemExit(f'{key} is long enough to be something other than a setting') # The export must carry what it should, not merely refrain from carrying what it # should not. Checking only the latter passes trivially when a whole class of # setting is silently dropped -- which is exactly what happened: numeric enums # were unreadable, so they never reached the bundle and every "did it arrive" # check was satisfied by their absence. present = set(settings) current = json.load(open(sys.argv[3])) if len(sys.argv) > 3 else {} for key, value in current.items(): if key in ("displays", "lastPage", "schemaVersion"): continue if key in declared and key not in present: raise SystemExit(f'{key} is set on this machine and declared, but was not carried') PY # ── 3, 4. Arrival is validated per key, and the types are all covered ─────── export XDG_CONFIG_HOME="$work/config" python3 - "$bundle" "$work/tampered.json" <<'PY' import json, sys bundle = json.load(open(sys.argv[1])) bundle["settings"].update({ "gapsIn": 9999, # above the schema maximum "colorScheme": "chartreuse", # not one of a word enum's choices "vrrPolicy": 47, # not one of a NUMERIC enum's choices "blurEnabled": "yes please", # wrong type entirely "displays": {"DP-9": "elsewhere"}, # machine-specific, injected "someFutureToken": "sk-abcdef123456", # a key this version does not know }) json.dump(bundle, open(sys.argv[2], "w")) PY "$helper" preview "$work/tampered.json" >"$work/preview.json" || fail 'preview failed' python3 - "$work/preview.json" <<'PY' || fail 'a bad value was not refused with its reason' import json, sys plan = json.load(open(sys.argv[1])) skipped = {entry["key"]: entry["reason"] for entry in plan["skipped"]} for key in ("gapsIn", "colorScheme", "vrrPolicy", "blurEnabled", "displays", "someFutureToken"): if key not in skipped: raise SystemExit(f'{key} was accepted and should not have been') if not skipped[key].strip(): raise SystemExit(f'{key} was skipped without saying why') changed = {entry["key"] for entry in plan["changes"]} for key in ("gapsIn", "colorScheme", "vrrPolicy", "blurEnabled", "displays", "someFutureToken"): if key in changed: raise SystemExit(f'{key} was refused and queued for application anyway') PY # A clean bundle must arrive intact. Refusing valid settings is the failure this # contract exists to catch as much as accepting invalid ones -- fixing the enum # check the first time turned every numeric enum into a rejection. "$helper" import "$bundle" >"$work/import.json" || fail 'import failed' python3 - "$bundle" "$work/config/panama/settings.json" <<'PY' || fail 'the round trip lost or changed a setting' import json, sys sent = json.load(open(sys.argv[1]))["settings"] landed = json.load(open(sys.argv[2])) missing = [k for k in sent if k not in landed] if missing: raise SystemExit(f'{len(missing)} settings did not arrive, starting with {missing[:3]}') wrong = [k for k, v in sent.items() if landed[k] != v] if wrong: raise SystemExit(f'{len(wrong)} arrived with a different value, starting with {wrong[:3]}') PY # ── 5. Import merges rather than replaces ─────────────────────────────────── python3 - "$work/config/panama/settings.json" <<'PY' import json, sys store = json.load(open(sys.argv[1])) store["aSettingTheBundleNeverMentions"] = "kept" json.dump(store, open(sys.argv[1], "w")) PY "$helper" import "$bundle" >/dev/null || fail 'second import failed' python3 - "$work/config/panama/settings.json" <<'PY' || fail 'import replaced the store instead of merging into it' import json, sys store = json.load(open(sys.argv[1])) if store.get("aSettingTheBundleNeverMentions") != "kept": raise SystemExit('a setting the bundle did not mention was removed') PY applied="$("$helper" import "$bundle" | field "['applied']")" [[ "$applied" == "0" ]] \ || fail "importing the same bundle twice applied $applied changes the second time" # ── Refusals ──────────────────────────────────────────────────────────────── printf 'not json at all\n' >"$work/junk.json" reason="$("$helper" import "$work/junk.json" | field "['error']")" [[ "$reason" == *"not a settings export"* ]] \ || fail "a file that is not an export was not refused with a reason (got: $reason)" reason="$("$helper" import "$work/absent.json" | field "['error']")" [[ "$reason" == *"does not exist"* ]] \ || fail "a missing file was not refused with a reason (got: $reason)" printf 'settings sync contract: ok\n'