utils/guard-lib/lib/file_guard.sh
Krzysztof kuhy Rudnicki 085d08b5c9 Split the two shell scripts and the gate's own config
aur-publish/publish.sh   439 -> 148 + lib/aur_registration 107
                                      + lib/aur_verify 122 + lib/aur_publish_one 107
  guard-lib/lib/file_guard.sh  350 -> 140 + file_guard_unlock 214
  file_length/config.py        252 -> 45 + _tables 202 + exemptions 77

The libs are sourced, never executed: they read the readonly config the entry
script declares, and a `readonly` re-run inside a twice-sourced lib aborts
under `set -e`. Verified by function parity rather than by eye — publish.sh
resolves the same 17 functions as before the split, guardctl the same 15 —
plus `shellcheck -x` clean on every file and a real `--help` / bad-option run.

file_length is the gate's own code, so a broken import there turns the gate
into a no-op. Split three ways to avoid an import cycle without a `# noqa`:
_tables holds the data, exemptions the rules that read it, config re-exports
both so `from file_length.config import is_generated` still resolves. The
first attempt was wrong — EXCLUDED_DIRS was left behind and every invocation
raised — caught because the gate was re-run in both modes rather than
assumed. Now verified: exit 0 on a clean file, exit 1 on an over-cap one,
--explain intact, and the same 61 exemption decisions as HEAD.

Also deletes baseline-2026-08-14.json and SURVEY-2026-08-14.txt: the task
forbids a baseline file, and nothing in the tree read either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 14:59:27 +02:00

141 lines
4.6 KiB
Bash

#!/bin/bash
# guardctl file-guard subcommands. Sourced by guardctl, never executed directly.
restore_from_canonical() {
local name="$1"
[[ -e "$CANONICAL" ]] || return 1
# shellcheck disable=SC2153 # BIND_MOUNT set by load_target_conf, not a typo
[[ "$BIND_MOUNT" == "yes" ]] && collapse_bind_mount "$TARGET"
unlock_file "$TARGET"
mkdir -p "$(dirname "$TARGET")"
cp --preserve=mode,ownership,timestamps "$CANONICAL" "$TARGET"
log_hook "file-guard $name: restored $TARGET from canonical (drift/missing detected)"
}
snapshot_canonical() {
local name="$1"
mkdir -p "$(dirname "$CANONICAL")"
unlock_file "$CANONICAL"
cp --preserve=mode,ownership,timestamps "$TARGET" "$CANONICAL"
lock_file "$CANONICAL"
log_hook "file-guard $name: snapshotted $TARGET as new canonical"
}
fg_ensure_templates_installed() {
for unit in guard-file@.path guard-file@.service guard-bind-mount@.service; do
[[ -f "$GUARD_LIB_SYSTEMD_DIR/$unit" ]] || \
die "missing $GUARD_LIB_SYSTEMD_DIR/$unit - run guard-lib's install.sh first"
done
}
fg_install() {
local name="$1"
shift
require_name "$name"
local target="" canonical="" bind_mount="no" plugin="" also_watch=()
while [[ $# -gt 0 ]]; do
case "$1" in
--target) target="$2"; shift 2 ;;
--canonical) canonical="$2"; shift 2 ;;
--bind-mount) bind_mount="yes"; shift ;;
--plugin) plugin="$2"; shift 2 ;;
--also-watch) also_watch+=("$2"); shift 2 ;;
*) die "file-guard install: unknown argument '$1'" ;;
esac
done
[[ -n "$target" ]] || die "file-guard install: --target is required"
target="$(realpath -m "$target")"
[[ -n "$plugin" ]] && plugin="$(realpath -m "$plugin")"
require_root
fg_ensure_templates_installed
local conf
conf="$(target_conf_path "$name")"
[[ -f "$conf" ]] && die "file-guard instance '$name' already installed (uninstall it first)"
[[ -n "$canonical" ]] || canonical="$GUARD_LIB_CONF_DIR/canonical/$name"
mkdir -p "$GUARD_LIB_TARGETS_DIR" "$(dirname "$canonical")"
cat >"$conf" <<EOF
TARGET="$target"
CANONICAL="$canonical"
BIND_MOUNT="$bind_mount"
PLUGIN="$plugin"
ALSO_WATCH="${also_watch[*]}"
EOF
TARGET="$target"
CANONICAL="$canonical"
if [[ ! -e "$canonical" ]]; then
[[ -e "$target" ]] || die "file-guard install: target '$target' does not exist and no canonical to seed from"
snapshot_canonical "$name"
fi
mkdir -p "$GUARD_LIB_SYSTEMD_DIR/guard-file@$name.path.d"
{
echo "[Path]"
echo "PathModified=$target"
for w in "${also_watch[@]}"; do
echo "PathModified=$w"
done
} >"$GUARD_LIB_SYSTEMD_DIR/guard-file@$name.path.d/override.conf"
systemctl daemon-reload
systemctl enable --now "guard-file@$name.path"
if [[ "$bind_mount" == "yes" ]]; then
systemctl enable --now "guard-bind-mount@$name.service"
fi
fg_enforce "$name"
log_hook "file-guard $name: installed (target=$target bind_mount=$bind_mount)"
echo "installed file-guard '$name' for $target"
}
fg_enforce() {
local name="$1"
load_target_conf "$name"
# shellcheck disable=SC2153 # PLUGIN is set by load_target_conf, not a typo of local `plugin`
load_plugin "$PLUGIN"
plugin_call pre_action
if [[ ! -e "$TARGET" ]]; then
restore_from_canonical "$name" || die "cannot restore $TARGET: no canonical copy exists"
fi
local validated=1
if declare -f validate >/dev/null 2>&1; then
if ! validate "$TARGET"; then
validated=0
if [[ -e "$CANONICAL" ]]; then
restore_from_canonical "$name"
else
plugin_call emergency_fix
fi
plugin_call post_restore_action
fi
fi
if [[ "$validated" -eq 1 ]]; then
if [[ -e "$CANONICAL" ]]; then
if ! cmp -s "$TARGET" "$CANONICAL"; then
restore_from_canonical "$name"
plugin_call post_restore_action
fi
else
snapshot_canonical "$name"
fi
fi
# Collapse before the final chattr dance too, not just for restores:
# if this instance is bind-mounted and already mounted read-only
# (e.g. every time enforce runs with no drift to restore), chattr
# itself fails against a read-only mount - silently leaving the
# target with no immutable flag at all.
[[ "$BIND_MOUNT" == "yes" ]] && collapse_bind_mount "$TARGET"
unlock_file "$TARGET"
lock_file "$TARGET"
[[ "$BIND_MOUNT" == "yes" ]] && _reapply_bind_mount "$name"
log_hook "file-guard $name: enforce ok"
}