#!/bin/sh

PRE15_RC_DUT_FAIL=10
PRE15_RC_RUNNER_FAIL=20
PRE15_RC_INFRA_BLOCKED=21
PRE15_RC_STOP=22

pre15_require_paths()
{
	: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
	PRE15_DUT=${PRE15_DUT:-$(CDPATH= cd -- "$PRE15_LIB_DIR/../../.." && pwd -P)}
	PRE15_ROOT=${PRE15_ROOT:-$(CDPATH= cd -- "$PRE15_DUT/.." && pwd -P)}
	PRE15_SCHEMA=${PRE15_SCHEMA:-$PRE15_DUT/tests/pre15/EVIDENCE-SCHEMA.json}
	PRE15_FREEBSD_SRC=${PRE15_FREEBSD_SRC:-${FREEBSD_SRC:-/work/build/freebsd-src}}
	PRE15_EVIDENCE_ROOT=${PRE15_EVIDENCE_ROOT:-${TMPDIR:-/tmp}/erofs-pre15-evidence}
	export PRE15_DUT PRE15_ROOT PRE15_SCHEMA PRE15_FREEBSD_SRC PRE15_EVIDENCE_ROOT
}

pre15_utc_now()
{
	date -u '+%Y-%m-%dT%H:%M:%SZ'
}

pre15_tree_sha256()
{
	python3 - "$1" <<'PY'
from __future__ import annotations

import hashlib
import os
from pathlib import Path
import stat
import sys


root = Path(sys.argv[1])
if not root.is_dir():
    raise SystemExit(f"tree root is not a directory: {root}")

digest = hashlib.sha256()
for current, directories, files in os.walk(root, followlinks=False):
    directories.sort(key=os.fsencode)
    files.sort(key=os.fsencode)
    current_path = Path(current)
    names = [(name, True) for name in directories]
    names.extend((name, False) for name in files)
    names.sort(key=lambda item: os.fsencode(item[0]))
    for name, is_directory in names:
        path = current_path / name
        metadata = path.lstat()
        relative = os.fsencode(path.relative_to(root))
        if stat.S_ISLNK(metadata.st_mode):
            kind = b"L"
        elif is_directory:
            kind = b"D"
        elif stat.S_ISREG(metadata.st_mode):
            kind = b"F"
        else:
            raise SystemExit(f"unsupported tree entry: {path}")
        digest.update(kind + b"\0" + relative + b"\0")
        digest.update(f"{stat.S_IMODE(metadata.st_mode):04o}\0".encode("ascii"))
        if kind == b"L":
            digest.update(os.fsencode(os.readlink(path)) + b"\0")
        elif kind == b"F":
            digest.update(f"{metadata.st_size}\0".encode("ascii"))
            with path.open("rb") as source:
                for chunk in iter(lambda: source.read(1024 * 1024), b""):
                    digest.update(chunk)
            digest.update(b"\0")

print(digest.hexdigest())
PY
}

pre15_tree_metadata_sha256()
{
	python3 - "$1" <<'PY'
from __future__ import annotations

import hashlib
import os
from pathlib import Path
import stat
import sys


root = Path(sys.argv[1])
if not root.is_dir():
    raise SystemExit(f"tree root is not a directory: {root}")

digest = hashlib.sha256()
for current, directories, files in os.walk(root, followlinks=False):
    directories.sort(key=os.fsencode)
    files.sort(key=os.fsencode)
    current_path = Path(current)
    names = [(name, True) for name in directories]
    names.extend((name, False) for name in files)
    names.sort(key=lambda item: os.fsencode(item[0]))
    for name, is_directory in names:
        path = current_path / name
        metadata = path.lstat()
        relative = os.fsencode(path.relative_to(root))
        if stat.S_ISLNK(metadata.st_mode):
            kind = b"L"
        elif is_directory:
            kind = b"D"
        elif stat.S_ISREG(metadata.st_mode):
            kind = b"F"
        else:
            raise SystemExit(f"unsupported tree entry: {path}")
        digest.update(kind + b"\0" + relative + b"\0")
        fields = (
            stat.S_IMODE(metadata.st_mode),
            metadata.st_size,
            metadata.st_mtime_ns,
            metadata.st_ctime_ns,
            metadata.st_dev,
            metadata.st_ino,
        )
        digest.update(("\0".join(map(str, fields)) + "\0").encode("ascii"))
        if kind == b"L":
            digest.update(os.fsencode(os.readlink(path)) + b"\0")

print(digest.hexdigest())
PY
}

pre15_fail_usage()
{
	printf 'RUNNER_FAIL: %s\n' "$*" >&2
	exit "$PRE15_RC_RUNNER_FAIL"
}

pre15_target_reached()
{
	: "${PRE15_TARGET_MARKER_FILE:?target marker path is unavailable}"
	printf 'reached\n' > "$PRE15_TARGET_MARKER_FILE"
}

pre15_set_reason()
{
	: "${PRE15_REASON_FILE:?reason path is unavailable}"
	printf '%s\n' "$*" > "$PRE15_REASON_FILE"
}

pre15_dut_fail()
{
	pre15_set_reason "$*"
	exit "$PRE15_RC_DUT_FAIL"
}

pre15_runner_fail()
{
	pre15_set_reason "$*"
	exit "$PRE15_RC_RUNNER_FAIL"
}

pre15_infra_blocked()
{
	pre15_set_reason "$*"
	exit "$PRE15_RC_INFRA_BLOCKED"
}

pre15_gate_stop()
{
	pre15_set_reason "$*"
	exit "$PRE15_RC_STOP"
}

pre15_register_resource()
{
	: "${PRE15_OWNERSHIP_FILE:?ownership manifest is unavailable}"
	pre15_resource_kind=$1
	pre15_resource_value=$2
	pre15_resource_label=$3
	case "$pre15_resource_kind$pre15_resource_value$pre15_resource_label" in
	*"	"*|*"
"*) pre15_runner_fail "resource records cannot contain tabs or newlines" ;;
	esac
	printf '%s\t%s\t%s\n' \
		"$pre15_resource_kind" "$pre15_resource_value" "$pre15_resource_label" \
		>> "$PRE15_OWNERSHIP_FILE"
}

pre15_own_path()
{
	pre15_register_resource path "$1" "${2:-temporary path}"
}

pre15_own_pid()
{
	case "$1" in
	''|*[!0-9]*) pre15_runner_fail "invalid owned PID: $1" ;;
	esac
	pre15_register_resource pid "$1" "${2:-process}"
}

pre15_own_port()
{
	case "$1" in
	''|*[!0-9]*) pre15_runner_fail "invalid owned port: $1" ;;
	esac
	pre15_register_resource port "$1" "${2:-forwarded port}"
}

pre15_own_ssh_control()
{
	pre15_register_resource ssh-control "$1" "${2:-SSH ControlMaster}"
}

pre15_own_base_image()
{
	pre15_base_path=$1
	test -f "$pre15_base_path" || pre15_infra_blocked "base image is absent: $pre15_base_path"
	pre15_base_hash=$(sha256sum "$pre15_base_path" | awk '{print $1}')
	pre15_register_resource base-image "$pre15_base_path" "$pre15_base_hash"
}

pre15_own_guest_mount()
{
	case "$1" in
	/*) ;;
	*) pre15_runner_fail "guest mountpoint must be absolute: $1" ;;
	esac
	pre15_register_resource guest-mount "$1" "${2:-guest mount}"
}

pre15_own_guest_md()
{
	case "$1" in
	md[0-9]*) ;;
	*) pre15_runner_fail "invalid guest md unit: $1" ;;
	esac
	pre15_register_resource guest-md "$1" "${2:-guest md}"
}

pre15_own_guest_kld()
{
	case "$1" in
	[A-Za-z0-9_.-]*) ;;
	*) pre15_runner_fail "invalid guest KLD name: $1" ;;
	esac
	pre15_register_resource guest-kld "$1" "${2:-guest KLD}"
}

pre15_record_fixture()
{
	pre15_fixture_label=$1
	pre15_fixture_path=$2
	test -f "$pre15_fixture_path" || pre15_runner_fail \
		"fixture is absent: $pre15_fixture_path"
	pre15_fixture_hash=$(sha256sum "$pre15_fixture_path" | awk '{print $1}')
	printf '%s\t%s\t%s\n' \
		"$pre15_fixture_hash" "$pre15_fixture_label" "$pre15_fixture_path" \
		>> "$PRE15_FIXTURE_HASHES"
}

pre15_record_module()
{
	pre15_module_path=$1
	test -f "$pre15_module_path" || pre15_runner_fail \
		"module is absent: $pre15_module_path"
	pre15_module_digest=$(sha256sum "$pre15_module_path" | awk '{print $1}')
	printf '%s\t%s\n' "$pre15_module_digest" "$pre15_module_path" \
		> "$PRE15_MODULE_HASH"
}

pre15_port_is_free()
{
	python3 - "$1" <<'PY'
import socket
import sys

port = int(sys.argv[1])
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    sock.settimeout(0.2)
    raise SystemExit(0 if sock.connect_ex(("127.0.0.1", port)) != 0 else 1)
PY
}

pre15_guest_ssh()
{
	: "${PRE15_QEMU_SSH_PORT:?SSH port is unavailable}"
	: "${PRE15_QEMU_SSH_USER:?SSH user is unavailable}"
	: "${PRE15_QEMU_SSH_KEY:?SSH key is unavailable}"
	ssh -o BatchMode=yes -o StrictHostKeyChecking=no \
		-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
		-i "$PRE15_QEMU_SSH_KEY" -p "$PRE15_QEMU_SSH_PORT" \
		"$PRE15_QEMU_SSH_USER@127.0.0.1" "$@"
}

pre15_guest_ssh_bounded()
{
	pre15_guest_deadline=${PRE15_GUEST_COMMAND_TIMEOUT:-60}
	case "$pre15_guest_deadline" in
	''|*[!0-9]*|0) pre15_runner_fail "invalid guest command deadline: $pre15_guest_deadline" ;;
	esac
	timeout -k 5 "$pre15_guest_deadline" ssh \
		-o BatchMode=yes -o StrictHostKeyChecking=no \
		-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \
		-i "$PRE15_QEMU_SSH_KEY" -p "$PRE15_QEMU_SSH_PORT" \
		"$PRE15_QEMU_SSH_USER@127.0.0.1" "$@"
}

pre15_cleanup_note()
{
	printf '%s\n' "$*" >> "$PRE15_CLEANUP_LOG"
}

pre15_cleanup_fail()
{
	PRE15_CLEANUP_STATUS_RESULT=FAIL
	pre15_cleanup_note "FAIL $*"
}

pre15_cleanup_pid()
{
	pre15_cleanup_pid_value=$1
	pre15_cleanup_pid_label=$2
	if ! kill -0 "$pre15_cleanup_pid_value" 2>/dev/null; then
		pre15_cleanup_note "PASS pid already exited: $pre15_cleanup_pid_value ($pre15_cleanup_pid_label)"
		return
	fi
	if ! kill -TERM "$pre15_cleanup_pid_value" 2>/dev/null; then
		pre15_cleanup_fail "could not TERM PID $pre15_cleanup_pid_value ($pre15_cleanup_pid_label)"
		return
	fi
	pre15_cleanup_wait=0
	while kill -0 "$pre15_cleanup_pid_value" 2>/dev/null && \
	    test "$pre15_cleanup_wait" -lt 50; do
		sleep 0.1
		pre15_cleanup_wait=$((pre15_cleanup_wait + 1))
	done
	if kill -0 "$pre15_cleanup_pid_value" 2>/dev/null; then
		if ! kill -KILL "$pre15_cleanup_pid_value" 2>/dev/null; then
			pre15_cleanup_fail "could not KILL PID $pre15_cleanup_pid_value ($pre15_cleanup_pid_label)"
			return
		fi
		sleep 0.2
	fi
	if kill -0 "$pre15_cleanup_pid_value" 2>/dev/null; then
		pre15_cleanup_fail "PID survived cleanup: $pre15_cleanup_pid_value ($pre15_cleanup_pid_label)"
	else
		pre15_cleanup_note "PASS stopped PID $pre15_cleanup_pid_value ($pre15_cleanup_pid_label)"
	fi
}

pre15_cleanup_guest()
{
	pre15_guest_kind=$1
	pre15_guest_value=$2
	case "$pre15_guest_kind" in
	guest-mount)
		if pre15_guest_ssh_bounded umount "$pre15_guest_value" >> "$PRE15_CLEANUP_LOG" 2>&1; then
			pre15_cleanup_note "PASS unmounted guest path $pre15_guest_value"
		else
			pre15_cleanup_fail "guest unmount failed: $pre15_guest_value"
		fi
		;;
	guest-md)
		if pre15_guest_ssh_bounded mdconfig -d -u "$pre15_guest_value" >> "$PRE15_CLEANUP_LOG" 2>&1; then
			pre15_cleanup_note "PASS detached guest md $pre15_guest_value"
		else
			pre15_cleanup_fail "guest md detach failed: $pre15_guest_value"
		fi
		;;
	guest-kld)
		if pre15_guest_ssh_bounded kldunload "$pre15_guest_value" >> "$PRE15_CLEANUP_LOG" 2>&1; then
			pre15_cleanup_note "PASS unloaded guest KLD $pre15_guest_value"
		else
			pre15_cleanup_fail "guest KLD unload failed: $pre15_guest_value"
		fi
		;;
	esac
}

pre15_cleanup_owned()
{
	PRE15_CLEANUP_STATUS_RESULT=PASS
	: > "$PRE15_CLEANUP_LOG"
	pre15_reverse_file=$PRE15_RUN_DIR/.ownership-reverse.$$
	awk '{ line[NR] = $0 } END { for (i = NR; i > 0; --i) print line[i] }' \
		"$PRE15_OWNERSHIP_FILE" > "$pre15_reverse_file"
	while IFS="$(printf '\t')" read -r pre15_kind pre15_value pre15_label; do
		test -n "$pre15_kind" || continue
		case "$pre15_kind" in
		guest-mount|guest-md|guest-kld)
			if test -n "${PRE15_QEMU_SSH_PORT:-}"; then
				pre15_cleanup_guest "$pre15_kind" "$pre15_value"
			else
				pre15_cleanup_fail "guest resource has no SSH endpoint: $pre15_kind $pre15_value"
			fi
			;;
		ssh-control)
			if test -S "$pre15_value"; then
				if timeout -k 5 "${PRE15_CLEANUP_COMMAND_TIMEOUT:-15}" \
				    ssh -S "$pre15_value" -O exit \
				    -p "${PRE15_QEMU_SSH_PORT:-22}" \
				    "${PRE15_QEMU_SSH_USER:-root}@127.0.0.1" \
				    >> "$PRE15_CLEANUP_LOG" 2>&1; then
					pre15_cleanup_note "PASS closed SSH control $pre15_value"
				else
					pre15_cleanup_fail "could not close SSH control $pre15_value"
				fi
			fi
			if test -e "$pre15_value" && ! rm -f -- "$pre15_value"; then
				pre15_cleanup_fail "could not remove SSH control $pre15_value"
			fi
			;;
		pid)
			pre15_cleanup_pid "$pre15_value" "$pre15_label"
			;;
		port)
			if pre15_port_is_free "$pre15_value"; then
				pre15_cleanup_note "PASS port is free: $pre15_value"
			else
				pre15_cleanup_fail "port remains in use: $pre15_value"
			fi
			;;
		path)
			case "$pre15_value" in
			"$PRE15_RUN_DIR"/*)
				if test -e "$pre15_value" && ! rm -rf -- "$pre15_value"; then
					pre15_cleanup_fail "could not remove owned path $pre15_value"
				else
					pre15_cleanup_note "PASS removed owned path $pre15_value"
				fi
				;;
			*) pre15_cleanup_fail "refused to remove path outside run directory: $pre15_value" ;;
			esac
			;;
		base-image)
			if test ! -f "$pre15_value"; then
				pre15_cleanup_fail "base image disappeared: $pre15_value"
			else
				pre15_cleanup_base_hash=$(sha256sum "$pre15_value" | awk '{print $1}')
				if test "$pre15_cleanup_base_hash" = "$pre15_label"; then
					pre15_cleanup_note "PASS base image unchanged: $pre15_value"
				else
					pre15_cleanup_fail "base image hash changed: $pre15_value"
				fi
			fi
			;;
		*) pre15_cleanup_fail "unknown ownership kind: $pre15_kind" ;;
		esac
	done < "$pre15_reverse_file"
	rm -f -- "$pre15_reverse_file"
	if grep -q '^FAIL ' "$PRE15_CLEANUP_LOG"; then
		PRE15_CLEANUP_STATUS_RESULT=FAIL
	fi
}

pre15_reason_text()
{
	if test -s "$PRE15_REASON_FILE"; then
		head -n 1 "$PRE15_REASON_FILE"
	else
		printf '%s\n' "$1"
	fi
}

pre15_classify()
{
	pre15_classify_rc=$1
	pre15_classify_target=$2
	pre15_classify_cleanup=$3
	PRE15_TIMED_OUT_RESULT=0
	if test "$pre15_classify_cleanup" != PASS; then
		PRE15_STATUS_RESULT=RUNNER_FAIL
		PRE15_ORIGIN_RESULT=runner
		PRE15_REASON_RESULT='owned resource cleanup failed'
		return
	fi
	case "$pre15_classify_rc" in
	0)
		if test "$pre15_classify_target" = reached; then
			PRE15_STATUS_RESULT=PASS
			PRE15_ORIGIN_RESULT=none
			PRE15_REASON_RESULT='target reached and all oracles passed'
		else
			PRE15_STATUS_RESULT=RUNNER_FAIL
			PRE15_ORIGIN_RESULT=runner
			PRE15_REASON_RESULT='command exited zero without a target marker'
		fi
		;;
	"$PRE15_RC_DUT_FAIL")
		if test "$pre15_classify_target" = reached; then
			PRE15_STATUS_RESULT=DUT_FAIL
			PRE15_ORIGIN_RESULT=dut
			PRE15_REASON_RESULT=$(pre15_reason_text 'DUT oracle mismatch')
		else
			PRE15_STATUS_RESULT=RUNNER_FAIL
			PRE15_ORIGIN_RESULT=runner
			PRE15_REASON_RESULT='DUT_FAIL was requested before the target marker'
		fi
		;;
	"$PRE15_RC_INFRA_BLOCKED")
		PRE15_STATUS_RESULT=INFRA_BLOCKED
		PRE15_ORIGIN_RESULT=infrastructure
		PRE15_REASON_RESULT=$(pre15_reason_text 'infrastructure prerequisite failed')
		;;
	"$PRE15_RC_STOP")
		PRE15_STATUS_RESULT=STOP
		PRE15_ORIGIN_RESULT=gate
		PRE15_REASON_RESULT=$(pre15_reason_text 'gate oracle returned STOP')
		;;
	124|137)
		PRE15_STATUS_RESULT=RUNNER_FAIL
		PRE15_ORIGIN_RESULT=runner
		PRE15_REASON_RESULT="hard timeout after ${PRE15_DEADLINE}s"
		PRE15_TIMED_OUT_RESULT=1
		;;
	*)
		PRE15_STATUS_RESULT=RUNNER_FAIL
		PRE15_ORIGIN_RESULT=runner
		PRE15_REASON_RESULT=$(pre15_reason_text "command exited $pre15_classify_rc")
		;;
	esac
}

pre15_write_identity()
{
	PRE15_DUT_HEAD=$(git -C "$PRE15_ROOT" rev-parse HEAD)
	PRE15_DUT_TREE=$(git -C "$PRE15_ROOT" rev-parse HEAD:repo-pre-15)
	PRE15_LINUX_SOURCE=$(git -C "$PRE15_ROOT" rev-parse HEAD:src-linux)
	pre15_freebsd_git_root=$(git -C "$PRE15_FREEBSD_SRC" \
		rev-parse --show-toplevel 2>/dev/null || true)
	if test -n "$pre15_freebsd_git_root" && \
	    test "$(CDPATH= cd -- "$pre15_freebsd_git_root" && pwd -P)" = \
	    "$(CDPATH= cd -- "$PRE15_FREEBSD_SRC" && pwd -P)"; then
		pre15_freebsd_head=$(git -C "$PRE15_FREEBSD_SRC" rev-parse HEAD)
		pre15_freebsd_dirty=$(git -C "$PRE15_FREEBSD_SRC" status --short | sha256sum | awk '{print $1}')
		PRE15_FREEBSD_SOURCE="$pre15_freebsd_head dirty-sha256:$pre15_freebsd_dirty"
	else
		pre15_freebsd_metadata=$(pre15_tree_metadata_sha256 "$PRE15_FREEBSD_SRC")
		if test -n "${PRE15_FREEBSD_TREE_SHA256:-}" || \
		    test -n "${PRE15_FREEBSD_TREE_METADATA_SHA256:-}"; then
			case "${PRE15_FREEBSD_TREE_SHA256:-}" in
			????????????????????????????????????????????????????????????????)
				case "$PRE15_FREEBSD_TREE_SHA256" in
				*[!0-9a-f]*) pre15_fail_usage 'cached FreeBSD tree SHA256 is invalid' ;;
				esac
				;;
			*) pre15_fail_usage 'cached FreeBSD tree SHA256 is invalid' ;;
			esac
			case "${PRE15_FREEBSD_TREE_METADATA_SHA256:-}" in
			????????????????????????????????????????????????????????????????)
				case "$PRE15_FREEBSD_TREE_METADATA_SHA256" in
				*[!0-9a-f]*) pre15_fail_usage 'cached FreeBSD metadata SHA256 is invalid' ;;
				esac
				;;
			*) pre15_fail_usage 'cached FreeBSD metadata SHA256 is invalid' ;;
			esac
			test "$pre15_freebsd_metadata" = \
			    "$PRE15_FREEBSD_TREE_METADATA_SHA256" || \
				pre15_fail_usage 'FreeBSD source changed after its content snapshot'
			pre15_freebsd_tree=$PRE15_FREEBSD_TREE_SHA256
		else
			pre15_freebsd_tree=$(pre15_tree_sha256 "$PRE15_FREEBSD_SRC")
		fi
		PRE15_FREEBSD_SOURCE="tree-sha256:$pre15_freebsd_tree metadata-sha256:$pre15_freebsd_metadata non-git-snapshot"
	fi
	PRE15_WORKTREE_DIFF_SHA256=$(git -C "$PRE15_ROOT" diff --binary HEAD -- repo-pre-15 | sha256sum | awk '{print $1}')
	export PRE15_DUT_HEAD PRE15_DUT_TREE PRE15_LINUX_SOURCE PRE15_FREEBSD_SOURCE
	export PRE15_WORKTREE_DIFF_SHA256
	PRE15_IDENTITY_HOST=$(uname -a)
	PRE15_IDENTITY_GIT_STATUS=$(git -C "$PRE15_ROOT" status --short | sha256sum | awk '{print $1}')
	export PRE15_IDENTITY_HOST PRE15_IDENTITY_GIT_STATUS
	python3 - "$PRE15_IDENTITY_PATH" <<'PY'
import json
import os
from pathlib import Path
import platform
import sys

identity = {
    "host_uname": os.environ["PRE15_IDENTITY_HOST"],
    "python": platform.python_version(),
    "git_status_sha256": os.environ["PRE15_IDENTITY_GIT_STATUS"],
    "freebsd_source_path": os.environ["PRE15_FREEBSD_SRC"],
    "freebsd_source_identity": os.environ["PRE15_FREEBSD_SOURCE"],
}
with Path(sys.argv[1]).open("x", encoding="ascii") as output:
    json.dump(identity, output, ensure_ascii=True, indent=2, sort_keys=True)
    output.write("\n")
PY
}

pre15_new_run()
{
	umask 077
	mkdir -p "$PRE15_EVIDENCE_ROOT"
	pre15_run_stamp=$(date -u '+%Y%m%dT%H%M%SZ')
	pre15_run_attempt=0
	while :; do
		PRE15_RUN_ID="$pre15_run_stamp-$PRE15_MODE-$PRE15_CASE_ID-$$-$pre15_run_attempt"
		PRE15_RUN_DIR=$PRE15_EVIDENCE_ROOT/$PRE15_RUN_ID
		if mkdir "$PRE15_RUN_DIR" 2>/dev/null; then
			break
		fi
		pre15_run_attempt=$((pre15_run_attempt + 1))
		test "$pre15_run_attempt" -lt 100 || pre15_fail_usage \
			"could not allocate immutable run directory"
	done
	PRE15_STDOUT_PATH=$PRE15_RUN_DIR/stdout.log
	PRE15_STDERR_PATH=$PRE15_RUN_DIR/stderr.log
	PRE15_TARGET_MARKER_FILE=$PRE15_RUN_DIR/target.marker
	PRE15_REASON_FILE=$PRE15_RUN_DIR/reason.txt
	PRE15_OWNERSHIP_FILE=$PRE15_RUN_DIR/ownership.tsv
	PRE15_CLEANUP_LOG=$PRE15_RUN_DIR/cleanup.log
	PRE15_FIXTURE_HASHES=$PRE15_RUN_DIR/fixture-hashes.tsv
	PRE15_MODULE_HASH=$PRE15_RUN_DIR/module-hash.tsv
	PRE15_ARGV_PATH=$PRE15_RUN_DIR/command-argv.json
	PRE15_IDENTITY_PATH=$PRE15_RUN_DIR/identity.json
	PRE15_MANIFEST_PATH=$PRE15_RUN_DIR/manifest.json
	PRE15_CASE_TMP=$PRE15_RUN_DIR/case-tmp
	: > "$PRE15_STDOUT_PATH"
	: > "$PRE15_STDERR_PATH"
	: > "$PRE15_OWNERSHIP_FILE"
	: > "$PRE15_CLEANUP_LOG"
	: > "$PRE15_FIXTURE_HASHES"
	: > "$PRE15_MODULE_HASH"
	mkdir "$PRE15_CASE_TMP"
	pre15_own_path "$PRE15_CASE_TMP" 'case temporary directory'
	export PRE15_RUN_ID PRE15_RUN_DIR PRE15_STDOUT_PATH PRE15_STDERR_PATH
	export PRE15_TARGET_MARKER_FILE PRE15_REASON_FILE PRE15_OWNERSHIP_FILE
	export PRE15_CLEANUP_LOG PRE15_FIXTURE_HASHES PRE15_MODULE_HASH
	export PRE15_ARGV_PATH PRE15_IDENTITY_PATH PRE15_MANIFEST_PATH PRE15_CASE_TMP
}

pre15_freeze_run()
{
	find "$PRE15_RUN_DIR" -type f -exec chmod 0444 {} +
	find "$PRE15_RUN_DIR" -type d -exec chmod 0555 {} +
}

pre15_parent_signal()
{
	pre15_cleanup_owned
	exit "$PRE15_RC_RUNNER_FAIL"
}

pre15_run_command()
{
	pre15_require_paths
	PRE15_MODE=$1
	PRE15_CASE_ID=$2
	PRE15_DEADLINE=$3
	shift 3
	case "$PRE15_CASE_ID" in
	''|*[!A-Za-z0-9._-]*) pre15_fail_usage "invalid case ID: $PRE15_CASE_ID" ;;
	esac
	case "$PRE15_DEADLINE" in
	''|*[!0-9]*|0) pre15_fail_usage "invalid deadline: $PRE15_DEADLINE" ;;
	esac
	command -v timeout >/dev/null 2>&1 || pre15_fail_usage 'timeout is required'
	command -v python3 >/dev/null 2>&1 || pre15_fail_usage 'python3 is required'
	test -f "$PRE15_SCHEMA" || pre15_fail_usage "schema is absent: $PRE15_SCHEMA"
	pre15_new_run
	pre15_write_identity
	pre15_write_argv_json "$PRE15_ARGV_PATH" "$@"
	PRE15_START_UTC=$(pre15_utc_now)
	export PRE15_MODE PRE15_CASE_ID PRE15_DEADLINE PRE15_START_UTC
	trap pre15_parent_signal HUP INT TERM
	if timeout -k "${PRE15_TIMEOUT_KILL_AFTER:-10}" "$PRE15_DEADLINE" "$@" \
	    > "$PRE15_STDOUT_PATH" 2> "$PRE15_STDERR_PATH"; then
		PRE15_EXIT_CODE=0
	else
		PRE15_EXIT_CODE=$?
	fi
	trap - HUP INT TERM
	if test -f "$PRE15_TARGET_MARKER_FILE" && \
	    test "$(cat "$PRE15_TARGET_MARKER_FILE")" = reached; then
		PRE15_TARGET_STATE=reached
	else
		PRE15_TARGET_STATE=not_reached
	fi
	pre15_cleanup_owned
	PRE15_CLEANUP_STATUS=$PRE15_CLEANUP_STATUS_RESULT
	pre15_classify "$PRE15_EXIT_CODE" "$PRE15_TARGET_STATE" "$PRE15_CLEANUP_STATUS"
	PRE15_STATUS=$PRE15_STATUS_RESULT
	PRE15_FAILURE_ORIGIN=$PRE15_ORIGIN_RESULT
	PRE15_REASON=$PRE15_REASON_RESULT
	PRE15_TIMED_OUT=$PRE15_TIMED_OUT_RESULT
	PRE15_END_UTC=$(pre15_utc_now)
	export PRE15_EXIT_CODE PRE15_TARGET_STATE PRE15_CLEANUP_STATUS PRE15_STATUS
	export PRE15_FAILURE_ORIGIN PRE15_REASON PRE15_TIMED_OUT PRE15_END_UTC
	pre15_write_manifest
	pre15_validate_manifest "$PRE15_MANIFEST_PATH" "$PRE15_SCHEMA"
	printf 'PRE15_RESULT status=%s cleanup=%s run_id=%s evidence=%s\n' \
		"$PRE15_STATUS" "$PRE15_CLEANUP_STATUS" "$PRE15_RUN_ID" \
		"$PRE15_MANIFEST_PATH"
	pre15_freeze_run
	case "$PRE15_STATUS" in
	PASS) return 0 ;;
	DUT_FAIL) return "$PRE15_RC_DUT_FAIL" ;;
	INFRA_BLOCKED) return "$PRE15_RC_INFRA_BLOCKED" ;;
	STOP) return "$PRE15_RC_STOP" ;;
	*) return "$PRE15_RC_RUNNER_FAIL" ;;
	esac
}

pre15_control_run()
{
	pre15_control_dir=$1
	pre15_control_deadline=$2
	shift 2
	(
		PRE15_RUN_DIR=$pre15_control_dir
		PRE15_TARGET_MARKER_FILE=$pre15_control_dir/target.marker
		PRE15_REASON_FILE=$pre15_control_dir/reason.txt
		PRE15_OWNERSHIP_FILE=$pre15_control_dir/ownership.tsv
		PRE15_CLEANUP_LOG=$pre15_control_dir/cleanup.log
		PRE15_DEADLINE=$pre15_control_deadline
		export PRE15_RUN_DIR PRE15_TARGET_MARKER_FILE PRE15_REASON_FILE
		export PRE15_OWNERSHIP_FILE PRE15_CLEANUP_LOG PRE15_DEADLINE
		mkdir "$pre15_control_dir"
		: > "$PRE15_OWNERSHIP_FILE"
		: > "$PRE15_CLEANUP_LOG"
		if timeout -k 2 "$pre15_control_deadline" "$@" \
		    > "$pre15_control_dir/stdout.log" \
		    2> "$pre15_control_dir/stderr.log"; then
			pre15_control_rc=0
		else
			pre15_control_rc=$?
		fi
		if test -f "$PRE15_TARGET_MARKER_FILE" && \
		    test "$(cat "$PRE15_TARGET_MARKER_FILE")" = reached; then
			pre15_control_target=reached
		else
			pre15_control_target=not_reached
		fi
		pre15_cleanup_owned
		pre15_classify "$pre15_control_rc" "$pre15_control_target" \
			"$PRE15_CLEANUP_STATUS_RESULT"
		printf '%s\t%s\t%s\t%s\n' "$PRE15_STATUS_RESULT" \
			"$PRE15_ORIGIN_RESULT" "$PRE15_CLEANUP_STATUS_RESULT" \
			"$PRE15_TIMED_OUT_RESULT" > "$pre15_control_dir/result.tsv"
	)
}

pre15_find_case()
{
	pre15_case_id=$1
	case "$pre15_case_id" in
	''|*[!A-Za-z0-9._-]*) pre15_fail_usage "invalid case ID: $pre15_case_id" ;;
	esac
	pre15_case_path=$PRE15_DUT/tests/pre15/cases/$pre15_case_id.sh
	test -f "$pre15_case_path" || pre15_fail_usage "unknown case: $pre15_case_id"
	printf '%s\n' "$pre15_case_path"
}
