185 lines
7.8 KiB
Bash
Executable File
185 lines
7.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# The desktop-hijacking ledger is complete, and `panama test --safe` obeys it.
|
|
#
|
|
# `panama test --safe` exists so the suite can be run from inside the session it
|
|
# tests. That promise is only as good as tests/desktop-hijacking: a contract
|
|
# that takes over the live shell and is not listed there is run by --safe, and
|
|
# the desktop goes away in the middle of somebody's work -- with the command
|
|
# line having just claimed it would not.
|
|
#
|
|
# A hand-kept list decays, so it is not trusted on its own. This sweeps tests/
|
|
# for the shapes a hijacking contract has and fails on any that are missing from
|
|
# the ledger. The heuristics live here, in the thing that runs, so a new
|
|
# hijacking contract cannot stay unlisted quietly:
|
|
#
|
|
# * it calls `qs ipc call` without booting its own `qs -p` harness, so the
|
|
# instance answering is the shell you are looking at;
|
|
# * it restarts panama-quickshell.service;
|
|
# * it calls a bare `qs kill`, which kills that same shell.
|
|
#
|
|
# The sweep is one direction only. The ledger is deliberately larger than what
|
|
# these three shapes find -- a contract that rotates the real monitor or
|
|
# rewrites a real xdg-mime default hijacks the session just as thoroughly and
|
|
# looks like nothing in particular from the outside -- so an entry the sweep
|
|
# does not reach is not a finding.
|
|
#
|
|
# Nothing here runs a listed contract. The one live check drives `--safe` with a
|
|
# pattern that matches only ledger entries, so the run selects them, skips them
|
|
# all, and executes nothing.
|
|
|
|
set -uo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
ledger="$repo_dir/tests/desktop-hijacking"
|
|
panama="$repo_dir/bin/panama"
|
|
|
|
findings=()
|
|
note() { findings+=("$1"); }
|
|
|
|
[[ -r "$ledger" ]] || { printf 'desktop hijacking contract: %s is missing\n' "$ledger" >&2; exit 1; }
|
|
|
|
# ── The entries ──────────────────────────────────────────────────────────────
|
|
#
|
|
# Every line names a contract that exists AND that `panama test` would collect.
|
|
# A listed path the runner never picks up (no executable bit, not a *_test.py)
|
|
# is skipped by --safe in name only, which reads as protection and is not.
|
|
|
|
entries=()
|
|
commented=()
|
|
pending_comment=0
|
|
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
case "$line" in
|
|
'#'*) pending_comment=1; continue ;;
|
|
''|[[:space:]]*'') ;;
|
|
esac
|
|
trimmed="${line%%#*}"
|
|
trimmed="${trimmed#"${trimmed%%[![:space:]]*}"}"
|
|
trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"
|
|
if [[ -z "$trimmed" ]]; then
|
|
[[ -z "$line" ]] && pending_comment=0
|
|
continue
|
|
fi
|
|
entries+=("$trimmed")
|
|
commented+=("$pending_comment")
|
|
pending_comment=0
|
|
done < "$ledger"
|
|
|
|
(( ${#entries[@]} > 0 )) || note 'the ledger lists no contracts at all'
|
|
|
|
for index in "${!entries[@]}"; do
|
|
entry="${entries[$index]}"
|
|
path="$repo_dir/$entry"
|
|
|
|
[[ "$entry" == tests/* ]] \
|
|
|| note "\"$entry\" is not a repo-relative path under tests/"
|
|
|
|
if [[ ! -e "$path" ]]; then
|
|
note "the ledger lists $entry, which does not exist"
|
|
continue
|
|
fi
|
|
|
|
[[ -x "$path" || "$entry" == *_test.py ]] \
|
|
|| note "$entry is listed but 'panama test' would never collect it, so skipping it protects nothing"
|
|
|
|
# The ledger's whole job is saying what a contract does to the session. An
|
|
# entry with no comment is a path somebody has to go and read.
|
|
(( commented[index] )) \
|
|
|| note "$entry is listed with no comment saying what it does to the live session"
|
|
done
|
|
|
|
duplicates="$(printf '%s\n' "${entries[@]}" | sort | uniq -d)"
|
|
[[ -z "$duplicates" ]] || note "the ledger lists these twice: ${duplicates//$'\n'/, }"
|
|
|
|
# ── The honesty sweep ────────────────────────────────────────────────────────
|
|
#
|
|
# Whole-line comments are dropped first: contracts discuss `qs ipc call` in
|
|
# their headers, and a header is not a call. What survives is matched only at a
|
|
# command position -- start of line, or after a pipe, semicolon, &&, (, or ! --
|
|
# so the same words quoted inside a grep pattern or a failure message do not
|
|
# count as driving anything.
|
|
|
|
command_position='(^|[|;&({!]|\$\()[[:space:]]*'
|
|
|
|
is_listed() {
|
|
local candidate="$1" listed
|
|
for listed in "${entries[@]}"; do
|
|
[[ "$listed" == "$candidate" ]] && return 0
|
|
done
|
|
return 1
|
|
}
|
|
|
|
while IFS= read -r file; do
|
|
[[ -x "$file" || "$file" == *_test.py ]] || continue
|
|
|
|
code="$(grep -v '^[[:space:]]*#' "$file")"
|
|
rel="tests/${file#"$repo_dir"/tests/}"
|
|
reason=""
|
|
|
|
if grep -qE "${command_position}qs[[:space:]]+ipc[[:space:]]+call" <<<"$code"; then
|
|
# Its own harness means its own Quickshell instance: `qs -p <entry>`
|
|
# addresses that root, not the shell running the desktop.
|
|
grep -qE "${command_position}[A-Za-z_]*[[:space:]]*=?[[:space:]]*.*qs[[:space:]]+-p" <<<"$code" \
|
|
|| reason='calls `qs ipc call` without booting its own `qs -p` harness'
|
|
fi
|
|
|
|
if [[ -z "$reason" ]] && grep -qE "${command_position}systemctl.*restart.*panama-quickshell\.service" <<<"$code"; then
|
|
reason='restarts panama-quickshell.service'
|
|
fi
|
|
|
|
if [[ -z "$reason" ]] && grep -qE "${command_position}qs[[:space:]]+kill" <<<"$code"; then
|
|
reason='calls a bare `qs kill`, which stops the live shell'
|
|
fi
|
|
|
|
[[ -n "$reason" ]] || continue
|
|
is_listed "$rel" \
|
|
|| note "$rel $reason, but is not in tests/desktop-hijacking"
|
|
done < <(find "$repo_dir/tests" -type f -not -path '*/fixtures/*' -not -path '*__pycache__*' | sort)
|
|
|
|
# ── --safe actually reads it ─────────────────────────────────────────────────
|
|
#
|
|
# Static first, because the summary line is the only thing telling a reader that
|
|
# anything was left out, and a --safe run that silently skips is worse than one
|
|
# that does not skip at all.
|
|
|
|
if [[ ! -r "$panama" ]]; then
|
|
note 'bin/panama is missing'
|
|
elif ! test_body="$(sed -n '/^cmd_test()/,/^}/p' "$panama")" || [[ -z "$test_body" ]]; then
|
|
note 'cmd_test could not be found in bin/panama'
|
|
else
|
|
grep -q -- '--safe' <<<"$test_body" \
|
|
|| note 'cmd_test does not handle --safe'
|
|
grep -qE 'DESKTOP_HIJACKING_LEDGER|desktop-hijacking' <<<"$test_body" \
|
|
|| note 'cmd_test never consults the desktop-hijacking ledger, so --safe skips nothing'
|
|
grep -qF 'desktop-hijacking contract(s)' <<<"$test_body" \
|
|
|| note '--safe no longer reports how many contracts it skipped'
|
|
fi
|
|
|
|
grep -qF 'tests/desktop-hijacking' "$panama" \
|
|
|| note 'bin/panama never names tests/desktop-hijacking'
|
|
|
|
# Then for real. The pattern is the first ledger entry with its 'tests/' prefix
|
|
# removed, which cmd_test matches against the full path -- so it selects that
|
|
# one contract, --safe removes it, and nothing is left to run. A --safe that
|
|
# ignored the ledger would run it instead, which is the failure this catches.
|
|
if (( ${#entries[@]} > 0 )) && [[ -x "$panama" ]]; then
|
|
probe="${entries[0]#tests/}"
|
|
output="$("$panama" test --safe "$probe" 2>&1)"
|
|
status=$?
|
|
if (( status == 0 )); then
|
|
note "'panama test --safe $probe' ran a ledger-listed contract instead of skipping it"
|
|
elif ! grep -qF 'desktop-hijacking' <<<"$output"; then
|
|
note "'panama test --safe $probe' refused without mentioning the ledger: $output"
|
|
fi
|
|
fi
|
|
|
|
if (( ${#findings[@]} > 0 )); then
|
|
mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u)
|
|
printf 'desktop hijacking contract: %d finding(s)\n' "${#findings[@]}" >&2
|
|
printf ' - %s\n' "${findings[@]}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
printf 'desktop hijacking contract: PASS (%d contracts listed; the sweep found none unlisted)\n' "${#entries[@]}"
|