#!/usr/bin/env bash # 30-assets.sh fetches the per-match files. Two things here are easy to break and # invisible until a real launch: # # - $MM_HOME/userdata is a symlink onto the tmpfs, and mkdir -p cannot create # through a dangling symlink. The image ships it dangling on purpose. # - a camo that fails to download must not end the match. It is cosmetic, and # `fetch` is fatal by design, so the camo path has to use try_fetch. # - the index is what the match host paints from, so a camo that failed its # digest must not be named in it. A file that is present but wrong is worse # than one that is absent: absent is default colours, wrong is whatever the # bytes turned out to be. set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT pass=0; fail=0 check() { if "${@:2}"; then echo "ok $1"; pass=$((pass+1)); else echo "FAIL $1"; fail=$((fail+1)); fi; } # Stage the image's layout: a MegaMek home whose userdata dangles onto the tmpfs. mkdir -p "$TMP/run" "$TMP/mm" ln -s "$TMP/run/userdata" "$TMP/mm/userdata" mkdir -p "$TMP/mm/data/scenarios" printf 'MMSVersion=1\nFactions=A,B\n' > "$TMP/mm/data/scenarios/scenario.mms" run_assets() { # local run="$TMP/run" # A container starts with an empty tmpfs. Without this, files staged by an # earlier case in this file are still there and every count below is of the # whole session rather than of the run being checked. rm -rf "$run/camo" printf '%s' "$1" > "$run/manifest.json" ARENA_HOME="$ROOT" ARENA_RUN="$run" MM_HOME="$TMP/mm" \ bash "$ROOT/container/init/30-assets.sh" 2>&1 } MANIFEST="$(jq -nc \ '{version:1, matchId:"t", scenario:{name:"scenario.mms"}, players:[{slot:"A",control:"human",did:"did:plc:x"},{slot:"B",control:"bot"}]}')" out="$(run_assets "$MANIFEST")"; status=$? check "runs against a dangling userdata symlink" test "$status" -eq 0 [ "$status" -eq 0 ] || printf '%s\n' "$out" | sed 's/^/ /' check "userdata target created through the symlink, on the tmpfs" \ test -d "$TMP/run/userdata" check "scenario fetched" test -f "$TMP/run/scenario/match.mms" check "scenario.env written" grep -q 'ARENA_SCENARIO=' "$TMP/run/scenario.env" # Which fight this is, for the result document: the staged path is match.mms # whichever form the manifest used, so the source and the library path are the # only things that say what was played. check "and it says the scenario came from the library" \ grep -qx 'ARENA_SCENARIO_SOURCE=library' "$TMP/run/scenario.env" check "and which one it picked" \ grep -qx 'ARENA_SCENARIO_LIBRARY=scenario.mms' "$TMP/run/scenario.env" # A scenario nobody baked into the image: the manifest carries the file itself. # This is what a generated fight - a daily challenge - is launched from. INLINE="$(jq -nc \ '{version:1, matchId:"t", scenario:{content:"MMSVersion=1\nName=Carried\nFactions=A,B\n"}, players:[{slot:"A",control:"human",did:"did:plc:x"},{slot:"B",control:"bot"}]}')" run_assets "$INLINE" >/dev/null 2>&1 check "a scenario carried by the manifest is staged" \ grep -qx 'Name=Carried' "$TMP/run/scenario/match.mms" check "and it is what the host is pointed at" \ grep -q "ARENA_SCENARIO=$TMP/run/scenario/match.mms" "$TMP/run/scenario.env" check "a carried scenario is named as carried" \ grep -qx 'ARENA_SCENARIO_SOURCE=manifest' "$TMP/run/scenario.env" # There is no library path for a scenario that was never in a library, and an # invented one would point at a file this image does not have. check "and names no library path" \ grep -qx "ARENA_SCENARIO_LIBRARY=''" "$TMP/run/scenario.env" # Both, which is a manifest disagreeing with itself about which fight this is. # Picking one would hand somebody the wrong match. BOTH="$(jq -nc \ '{version:1, matchId:"t", scenario:{name:"scenario.mms", content:"MMSVersion=1\nFactions=A,B\n"}, players:[{slot:"A",control:"human",did:"did:plc:x"},{slot:"B",control:"bot"}]}')" out="$(run_assets "$BOTH" 2>&1)" && status=0 || status=$? check "a manifest with both a name and content is refused" test "$status" -ne 0 check "and it says which two it was given" \ grep -q 'exactly one' <<< "$out" check "a manifest with no camo leaves an empty index" \ test ! -s "$TMP/run/camo/index.tsv" # Cosmetic, so it must not be fatal - fetch() exits the script, try_fetch does not. CAMO="$(jq -nc \ '{version:1, matchId:"t", scenario:{name:"scenario.mms"}, players:[{slot:"A",control:"human",did:"did:plc:x", camo:{url:"file:///nonexistent/nope.png", sha256:"dead"}}, {slot:"B",control:"bot"}]}')" run_assets "$CAMO" >/dev/null 2>&1 check "an unfetchable camo does not end the match" test $? -eq 0 check "an unfetchable camo is not named in the index" \ test ! -s "$TMP/run/camo/index.tsv" # A camo that arrives whole: named in the index, under the slot the scenario # spells, pointing at the file that was actually written. printf 'not really a png' > "$TMP/real.png" REAL_SHA="$(sha256sum "$TMP/real.png" | cut -d' ' -f1)" GOOD="$(jq -nc --arg url "file://$TMP/real.png" --arg sha "$REAL_SHA" \ '{version:1, matchId:"t", scenario:{name:"scenario.mms"}, players:[{slot:"A",control:"human",did:"did:plc:x", camo:{url:$url, sha256:$sha}}, {slot:"B",control:"bot"}]}')" run_assets "$GOOD" >/dev/null 2>&1 check "a good camo is indexed under its slot" \ grep -qx "$(printf 'A\t1-A.png')" "$TMP/run/camo/index.tsv" check "the indexed file is the one on disk" test -f "$TMP/run/camo/1-A.png" check "the bot slot is not in the index" \ test "$(wc -l < "$TMP/run/camo/index.tsv")" -eq 1 # Two slots that sanitise to the same filename. Before the counter, the second # download landed on the first one's file - so one player's camo was painted # onto the other player's units, which is the one thing this path must not do. COLLIDE="$(jq -nc --arg url "file://$TMP/real.png" --arg sha "$REAL_SHA" \ '{version:1, matchId:"t", scenario:{name:"scenario.mms"}, players:[{slot:"Team A",control:"human",did:"did:plc:x", camo:{url:$url, sha256:$sha}}, {slot:"Team_A",control:"human",did:"did:plc:y", camo:{url:$url, sha256:$sha}}]}')" run_assets "$COLLIDE" >/dev/null 2>&1 check "two slots that sanitise alike get two files" \ test "$(ls "$TMP"/run/camo/*.png | wc -l)" -eq 2 check "and two index lines, one per slot" \ test "$(wc -l < "$TMP/run/camo/index.tsv")" -eq 2 check "each index line names a file that exists" \ bash -c 'cd "$1/run/camo" && while IFS=$'"'"'\t'"'"' read -r _ f; do [ -f "$f" ] || exit 1; done < index.tsv' _ "$TMP" # The same bytes under a digest that does not match them. BAD="$(jq -nc --arg url "file://$TMP/real.png" \ '{version:1, matchId:"t", scenario:{name:"scenario.mms"}, players:[{slot:"A",control:"human",did:"did:plc:x", camo:{url:$url, sha256:"00"}}, {slot:"B",control:"bot"}]}')" run_assets "$BAD" >/dev/null 2>&1 check "a camo that fails its digest is not indexed" \ test ! -s "$TMP/run/camo/index.tsv" echo echo "$pass passed, $fail failed" [ "$fail" -eq 0 ]