#!/bin/sh
# ogma-fsck-rc-apply -- apply / remove / verify the OgmaProtect fsck auto-repair
# patch on the live /etc/rc (10.E9 S1, docs/PHASE-BOOT-FSCK-RESILIENCE.md §2.0).
#
#   ogma-fsck-rc-apply enable                apply the patch (idempotent)
#   ogma-fsck-rc-apply disable               remove it (idempotent)
#   ogma-fsck-rc-apply verify-against-stock  exit 0 iff /etc/rc == stock + patch
#   ogma-fsck-rc-apply status                print applied / not applied; exit 0/1
#
# WHY A PATCH AT ALL: /etc/rc is base-OS territory -- replaced by the etcXX.tgz
# set on every sysupgrade/sysmerge -- and the only hook for a preen failure is
# do_fsck's exit-8 arm inside it (there is no rc.conf knob). So the package
# ships the patch INERT under /etc/examples/ogmaprotect/ and the operator
# applies it; the daemons then watch the LIVE /etc/rc for the sentinel and
# report `rc_patch_applied=false` as a WARNING (the patch-presence drift
# domain, contract §2.1(b)) so a box on stock rc never reads CLEAN.
#
# Refusals are loud and leave /etc/rc untouched: a context mismatch (this
# patch was written against a specific rc; a release that moved the hunks
# refuses with `patch -C -F0`), a failed `ksh -n` syntax check after patching
# (restored from the backup), a missing helper file.
#
# rc.firsttime (contract §6.8): after a sysupgrade the installer writes its own
# commands into /etc/rc.firsttime; `enable` APPENDS a one-line re-apply hook
# to that file when it EXISTS (never creates or overwrites it -- a file we
# create would run and be consumed at the very next normal boot, long before
# any upgrade), so an upgrade staged while the patch is enabled re-applies it
# on the first post-upgrade boot. A mis-applied re-apply is not "one boot": it
# is permanently off until the WARNING is acted on, which is why the drift
# domain, not this hook, is the real fix.
#
# The INTENT marker (10.E9 S1 follow-up, VD-E9-2): the append-only hook above
# cannot cover the common case -- `enable` run on a settled box with no
# rc.firsttime, `sysupgrade` months later -- so `enable` also records the
# operator's intent durably ($INTENT, under /etc/ogmaprotect, which no
# etcXX.tgz set touches) and `disable` removes it. ogmaprotect-setup's
# every-boot convergence re-runs `enable` when the intent stands and the
# patch is absent (the post-upgrade stock rc), through this script's own
# refusal gates: a release whose rc moved a hunk is REFUSED loudly with
# /etc/rc untouched, and the box keeps the fsck_rc_patch drift WARNING --
# the contract path, never a fuzzy apply.
#
# Test seams (never operator knobs): OGMA_FSCK_RC (the rc path), OGMA_FSCK_PATCH,
# OGMA_FSCK_SUBR, OGMA_FSCK_FIRSTTIME, OGMA_FSCK_KSH (the syntax checker; the
# Linux CI leg has no ksh and sets it to `true`), OGMA_FSCK_RELEASE (the
# release the per-release lookup below keys on; defaults to `uname -r`),
# OGMA_FSCK_INTENT (the intent-marker path).

set -u

RC=${OGMA_FSCK_RC:-/etc/rc}
PATCH=${OGMA_FSCK_PATCH:-/etc/examples/ogmaprotect/rc.fsck.patch}
# VD-E9-5 (10.E9 S1 follow-up): per-release selection. When a release-specific
# patch ships BESIDE the generic one -- rc.fsck.patch.<uname -r>, the day a
# release moves a hunk -- it wins; until then the generic patch serves every
# release it applies to with zero fuzz (7.8 and 7.9 today, both fixtures in
# daemon/test/fixtures/rc/ proven apply + byte-identical reverse). The lookup
# is keyed on the patch path actually in use, so the test seam sees it too.
_rel=${OGMA_FSCK_RELEASE:-$(uname -r 2>/dev/null)}
if [ -n "$_rel" ] && [ -r "$PATCH.$_rel" ]; then
	PATCH=$PATCH.$_rel
fi
SUBR=${OGMA_FSCK_SUBR:-/etc/ogmaprotect/rc.fsck.subr}
FIRSTTIME=${OGMA_FSCK_FIRSTTIME:-/etc/rc.firsttime}
KSH=${OGMA_FSCK_KSH:-ksh}
SENTINEL='OGMA-FSCK-RESILIENCE'
# The dry-run flag: OpenBSD patch(1) spells it -C, GNU patch --dry-run. The
# production target is OpenBSD; the GNU arm exists so the CI unit can drive
# this script against the committed rc fixture on the Linux leg too.
if patch --version 2>/dev/null | grep -q GNU; then
	DRY=--dry-run
else
	DRY=-C
fi
HOOK='[ -x /usr/local/sbin/ogma-fsck-rc-apply ] && /usr/local/sbin/ogma-fsck-rc-apply enable  # OGMA-FSCK-RESILIENCE re-apply after upgrade'
ORIG=$RC.ogma-stock
INTENT=${OGMA_FSCK_INTENT:-/etc/ogmaprotect/fsck-autorepair.enabled}

usage() {
	echo "usage: ogma-fsck-rc-apply enable | disable | verify-against-stock | status" >&2
	exit 2
}

applied() {
	grep -q "$SENTINEL" "$RC" 2>/dev/null
}

need_patch() {
	[ -r "$PATCH" ] || { echo "ogma-fsck-rc-apply: patch file $PATCH is missing or unreadable" >&2; exit 1; }
	[ -r "$RC" ] || { echo "ogma-fsck-rc-apply: $RC is missing or unreadable" >&2; exit 1; }
}

# patch(1) options: -C dry-run, -s silent, -N never reverse-apply, -F0 refuse
# any fuzz (STRICT context), -p0 the bare `rc` name in the headers, and the
# target file given explicitly so the header paths are never consulted.
dry_apply()   { patch $DRY -s -N -F0 -p0 "$RC" "$PATCH" >/dev/null 2>&1; }
real_apply()  { patch      -s -N -F0 -p0 -z .ogma-bak "$RC" "$PATCH" >/dev/null 2>&1; }
dry_reverse() { patch $DRY -s -R -F0 -p0 "$RC" "$PATCH" >/dev/null 2>&1; }
real_reverse(){ patch      -s -R -F0 -p0 -z .ogma-bak "$RC" "$PATCH" >/dev/null 2>&1; }

cmd=${1:-}
[ -n "$cmd" ] || usage

case $cmd in
status)
	if applied; then
		echo "applied: $RC carries the $SENTINEL patch (patch file: $PATCH)"
		[ -r "$SUBR" ] || echo "WARNING: helper $SUBR is missing -- the patch is inert (stock behaviour)"
		exit 0
	fi
	echo "not applied: $RC is stock (a preen failure at boot will halt on the console)"
	exit 1
	;;
verify-against-stock)
	need_patch
	if ! applied; then
		echo "not applied" >&2
		exit 1
	fi
	# Reversible cleanly with zero fuzz <=> the live file is exactly the
	# stock release text plus this patch, nothing else in the hunks.
	if dry_reverse; then
		echo "verified: $RC == stock + $PATCH"
		exit 0
	fi
	echo "MISMATCH: $RC carries the sentinel but does not reverse cleanly against $PATCH (hand-edited, or a different release's patch)" >&2
	exit 1
	;;
enable)
	need_patch
	if applied; then
		echo "already applied: $RC carries the $SENTINEL patch"
	else
		if ! dry_apply; then
			echo "REFUSED: $RC does not match the context this patch was written for (a different OpenBSD release, or a hand-edited rc); $RC is untouched" >&2
			exit 1
		fi
		[ -f "$ORIG" ] || cp -p "$RC" "$ORIG"
		if ! real_apply; then
			echo "FAILED: patch did not apply cleanly; restoring $RC from $ORIG" >&2
			cp -p "$ORIG" "$RC"
			exit 1
		fi
		rm -f "$RC.ogma-bak"
		if ! $KSH -n "$RC" >/dev/null 2>&1; then
			echo "FAILED: patched $RC does not pass ksh -n; restoring from $ORIG" >&2
			cp -p "$ORIG" "$RC"
			exit 1
		fi
		echo "applied: $RC now carries the $SENTINEL patch (stock copy kept at $ORIG)"
	fi
	[ -r "$SUBR" ] || echo "WARNING: helper $SUBR is missing -- the patch is inert until the package installs it"
	# The re-apply hook: APPEND to an EXISTING rc.firsttime only (see above).
	if [ -f "$FIRSTTIME" ] && ! grep -q "$SENTINEL re-apply" "$FIRSTTIME" 2>/dev/null; then
		printf '%s\n' "$HOOK" >> "$FIRSTTIME"
		echo "appended the re-apply hook to $FIRSTTIME"
	fi
	# Record the operator's intent durably (survives sysupgrade; read by
	# ogmaprotect-setup's every-boot convergence -- see the header). Written
	# on the already-applied path too, so a box enabled before this marker
	# existed gains it on its next `enable`. `touch` (not `: >`): a redirection
	# failure on the `:` special built-in EXITS a non-interactive POSIX shell,
	# which would turn a missing marker directory into a failed `enable` -- the
	# image build points the rc seam at a staging root without one. `touch` is
	# a regular command, so a failure here is non-fatal and merely warns.
	if [ ! -f "$INTENT" ]; then
		touch "$INTENT" 2>/dev/null || echo "WARNING: could not record the enable intent at $INTENT -- the post-sysupgrade auto re-apply will not fire" >&2
	fi
	exit 0
	;;
disable)
	need_patch
	if ! applied; then
		echo "already stock: $RC carries no $SENTINEL patch"
	else
		if ! dry_reverse; then
			echo "REFUSED: $RC carries the sentinel but does not reverse cleanly against $PATCH; $RC is untouched (restore $ORIG by hand if you must)" >&2
			exit 1
		fi
		if ! real_reverse; then
			echo "FAILED: reverse patch did not apply cleanly; $RC may be inconsistent -- compare with $ORIG" >&2
			exit 1
		fi
		rm -f "$RC.ogma-bak"
		if ! $KSH -n "$RC" >/dev/null 2>&1; then
			echo "FAILED: $RC does not pass ksh -n after reversal -- compare with $ORIG" >&2
			exit 1
		fi
		echo "removed: $RC is stock again"
	fi
	if [ -f "$FIRSTTIME" ] && grep -q "$SENTINEL re-apply" "$FIRSTTIME" 2>/dev/null; then
		# Drop ONLY our hook line; every other line (OpenBSD's own) is kept.
		# grep -v exits 1 when nothing remains, which is a legal outcome.
		grep -v "$SENTINEL re-apply" "$FIRSTTIME" > "$FIRSTTIME.ogma-tmp"
		mv "$FIRSTTIME.ogma-tmp" "$FIRSTTIME"
		echo "removed the re-apply hook from $FIRSTTIME"
	fi
	# Clear the intent: a deliberate `disable` must stop the every-boot
	# convergence from putting the patch back.
	if [ -f "$INTENT" ]; then
		rm -f "$INTENT"
		echo "cleared the enable intent at $INTENT"
	fi
	exit 0
	;;
*)
	usage
	;;
esac
