#!/bin/sh
# script/check-censored: assert that the competitor name RULES.md bars appears
# nowhere in this repo, and nowhere in the built extension, except where it is
# deliberate. Our own extension to scripts-to-rule-them-all, run from
# script/check and from make build.
#
# Where the name is allowed, and why each one is not negotiable away:
#
#   - script/vendor-blocklist. Build-time tooling, never shipped. A pinned
#     source reference that does not say what the source is cannot be verified
#     by anyone, so it names it. Whole-file exemption.
#   - the two provider-shim identifiers in src/content/inpage.js. Protocol
#     identifiers dApps feature-detect on; renaming them does not rename them in
#     their code, it only stops this wallet working on their sites.
#   - the on-chain name of the MUSD ERC-20 in src/shared/tokenList.js. It is not
#     what backs symbol-spoof detection — that reads symbol and address — but
#     the wallet already surfaces the on-chain name of any token the user holds
#     (src/shared/balances.js), and this contract's on-chain name is that
#     string, so censoring the repo cannot stop the wallet displaying it.
#     Dropping the entry instead would cost the user MUSD spoof detection.
#
# Everything else fails, in the working tree and under dist/. The last two are
# literals rather than whole files, so they are enforced by counting, and each
# literal is scoped to the path allowed to carry it: a file may contain the name
# only as many times as it contains the literals permitted *there*, and zero
# times anywhere else. The emitted bundles carry them too, so a plain "the name
# must not appear in dist/" could never have passed.
#
# The name itself is not written in this file. script/vendor-blocklist is the
# one place in this repo that defines it, and this reads it back out of there —
# so the repo-wide grep this check exists to enforce keeps returning exactly the
# files named above, and this file is not one of them.
set -eu

ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"

# Absolute path to this script, resolved before anything cd's anywhere: the
# scan half runs in a re-invocation through xargs, so that the paths it works on
# arrive as arguments and cannot be reshaped by field splitting on the way in.
SELF="$(cd "$(dirname "$0")" && pwd -P)/$(basename "$0")"

# Internal re-entry flag. Not part of the command-line interface.
SCAN_FLAG="--scan-paths"

VENDOR_SCRIPT="$ROOT/script/vendor-blocklist"

# Set by extract_name / make_literals_file.
NAME=""
ALLOWED_LITERALS_FILE=""

FAILED=0

cleanup() {
    [ -z "$ALLOWED_LITERALS_FILE" ] || rm -f "$ALLOWED_LITERALS_FILE"
}
trap cleanup EXIT INT TERM

fail() {
    echo "check-censored: FAIL: $*" >&2
    exit 1
}

# The name, taken from the single place that defines it. A check scanning for a
# pattern it failed to read would pass against anything, so this refuses to
# continue unless it got something that looks like the definition.
extract_name() {
    [ -f "$VENDOR_SCRIPT" ] ||
        fail "$VENDOR_SCRIPT is missing, and it is where the name being
    checked for is defined. Nothing was scanned."

    NAME="$(grep -m1 '^UPSTREAM_ORG=' "$VENDOR_SCRIPT" | cut -d'"' -f2)" ||
        fail "could not read UPSTREAM_ORG from $VENDOR_SCRIPT. Nothing was
    scanned."

    case "$NAME" in
    "" | *[!A-Za-z0-9]*)
        fail "UPSTREAM_ORG in $VENDOR_SCRIPT did not yield a plain name
    (got: '$NAME'). Scanning for that would prove nothing. Nothing was
    scanned."
        ;;
    esac
}

make_literals_file() {
    ALLOWED_LITERALS_FILE="$(mktemp \
        "${TMPDIR:-/tmp}/autistmask-censored.XXXXXX")" ||
        fail "could not create a temporary file, so nothing was scanned."
}

# The literals $1 may carry, and nothing else may. Each contains the name
# exactly once, which is what makes counting them sound; each is scoped to its
# path, so a file with no business carrying the name fails even when it spells
# it the way shipped code has to. Scoping is the point: permitting these
# literals in any file is what once let this check pass its own prose.
#
# The emitted paths are listed next to the sources they come from. If the
# bundler moves one, this goes red and the new path gets added deliberately,
# rather than a wildcard over dist/ covering whatever lands there.
allowed_literals_for() {
    : >"$ALLOWED_LITERALS_FILE"
    case "$1" in
    src/content/inpage.js | dist/*/src/content/inpage.js)
        printf 'is%s\n_%s\n' "$NAME" "$NAME" >"$ALLOWED_LITERALS_FILE"
        ;;
    src/shared/tokenList.js | dist/*/src/background/index.js | \
        dist/*/src/popup/index.js)
        printf '%s USD\n' "$NAME" >"$ALLOWED_LITERALS_FILE"
        ;;
    esac
}

# How many times does $1 contain the name (TOTAL), and how many of those are one
# of the allowed literals (ALLOWED)? Same discipline the rest of this repo's
# shell checks apply to grep: exit 0 and 1 are answers about the file, anything
# else means the file was not searched and is not an answer at all.
count_matches() {
    _cm_status=0
    _cm_out="$(grep -a -o -i -F -e "$NAME" -- "$1")" || _cm_status=$?
    case "$_cm_status" in
    0) TOTAL="$(printf '%s\n' "$_cm_out" | grep -c .)" ;;
    1) TOTAL=0 ;;
    *)
        fail "grep exited $_cm_status reading $1, so the file was never
    searched and nothing was established about it. That is a permissions or I/O
    fault, not a clean file. Refusing to report success."
        ;;
    esac

    if [ "$TOTAL" -eq 0 ]; then
        ALLOWED=0
        return 0
    fi

    # No literal is permitted at this path, so every occurrence is a violation.
    # Handled here rather than by grep, which is not required to say anything
    # useful about an empty pattern file.
    if [ ! -s "$ALLOWED_LITERALS_FILE" ]; then
        ALLOWED=0
        return 0
    fi

    _cm_status=0
    _cm_out="$(grep -a -o -i -F -f "$ALLOWED_LITERALS_FILE" -- "$1")" ||
        _cm_status=$?
    case "$_cm_status" in
    0) ALLOWED="$(printf '%s\n' "$_cm_out" | grep -c .)" ;;
    1) ALLOWED=0 ;;
    *)
        fail "grep exited $_cm_status matching the allowed literals in $1.
    Refusing to report success."
        ;;
    esac
}

# The per-path half, run in a re-invocation of this script so it uses the same
# counting as everything else rather than a second copy of it.
scan_paths() {
    for _file in "$@"; do
        # dist/ arrives absolute (find) and the worktree relative (git
        # ls-files). The allowlist is keyed on repo-relative paths, so both
        # forms are reduced to one before anything is decided about them.
        _rel="$_file"
        case "$_rel" in
        "$ROOT"/*) _rel="${_rel#"$ROOT"/}" ;;
        esac

        case "$_rel" in
        script/vendor-blocklist) continue ;;
        esac
        [ -f "$_file" ] || continue

        allowed_literals_for "$_rel"
        count_matches "$_file"
        [ "$TOTAL" -gt "$ALLOWED" ] || continue

        FAILED=$((FAILED + 1))
        echo "check-censored: $_rel: $TOTAL occurrence(s) of the name," \
            "$ALLOWED of them allowed at this path" >&2
        grep -a -n -i -F -e "$NAME" -- "$_file" | cut -c1-140 | head -5 >&2
    done
    [ "$FAILED" -eq 0 ]
}

# Hand a NUL-delimited listing to the scan half. Returns non-zero if any path
# failed, or if the scan could not be run at all.
scan_listing() {
    xargs -0 "$SELF" "$SCAN_FLAG" <"$1"
}

# Every file git tracks, plus everything untracked and not ignored: the working
# tree as a reviewer would see it, and never node_modules or dist/ (both are
# ignored; dist/ is walked separately below).
check_worktree() {
    _list="$(mktemp "${TMPDIR:-/tmp}/autistmask-censored-tree.XXXXXX")" ||
        fail "could not create a temporary file, so nothing was scanned."
    _status=0
    git ls-files -z --cached --others --exclude-standard >"$_list" ||
        _status=$?
    [ "$_status" -eq 0 ] || {
        rm -f "$_list"
        fail "git ls-files exited $_status, so the working tree was never
    enumerated and nothing was established about it."
    }

    # Repo-relative paths. The scan half cd's to the repo root before it opens
    # anything, so they reach it intact and unjoined.
    WORKTREE_COUNT="$(tr -dc '\0' <"$_list" | wc -c | tr -d ' ')"

    _status=0
    scan_listing "$_list" || _status=$?
    rm -f "$_list"
    return "$_status"
}

check_dist() {
    _list="$(mktemp "${TMPDIR:-/tmp}/autistmask-censored-dist.XXXXXX")" ||
        fail "could not create a temporary file, so dist/ was not scanned."
    _status=0
    find "$ROOT/dist" -type f -print0 >"$_list" || _status=$?
    [ "$_status" -eq 0 ] || {
        rm -f "$_list"
        fail "find exited $_status enumerating dist/, so part of the emitted
    tree was never walked and an unchecked file there went unchecked. Refusing
    to report success."
    }

    DIST_COUNT="$(tr -dc '\0' <"$_list" | wc -c | tr -d ' ')"

    _status=0
    scan_listing "$_list" || _status=$?
    rm -f "$_list"
    return "$_status"
}

usage() {
    echo "usage: script/check-censored [--require-dist]" >&2
    exit 2
}

main() {
    cd "$ROOT"

    # Internal re-entry from scan_listing's xargs.
    if [ "${1-}" = "$SCAN_FLAG" ]; then
        shift
        extract_name
        make_literals_file
        scan_paths "$@"
        return $?
    fi

    require_dist=no
    case "${1-}" in
    "") ;;
    --require-dist) require_dist=yes ;;
    *) usage ;;
    esac

    extract_name
    make_literals_file

    echo "Checking for censored names..."

    tree_status=0
    check_worktree || tree_status=$?

    dist_status=0
    dist_inspected=no
    DIST_COUNT=0
    if [ -d "$ROOT/dist" ]; then
        dist_inspected=yes
        check_dist || dist_status=$?
    fi

    if [ "$tree_status" -ne 0 ] || [ "$dist_status" -ne 0 ]; then
        fail "the name appears outside the deliberate exceptions (reported
    above). See the header of script/check-censored for what is allowed and
    why."
    fi

    if [ "$dist_inspected" = no ]; then
        if [ "$require_dist" = yes ]; then
            fail "there is no dist/ to inspect and this run was asked to
    require one. Run make build."
        fi
        cat <<EOF
################################################################################
## WARNING: dist/ WAS NOT INSPECTED BY THIS RUN AND IS NOT PROVEN CLEAN BY IT.
## There is no dist/ in this tree. The working tree is clean, but a build can
## carry text no source file does — a dependency's, or a bundler's. Every
## make build runs this check again with dist/ required, so a release artifact
## is always covered; this run simply had none to look at.
################################################################################
EOF
    fi

    echo "check-censored: $WORKTREE_COUNT tracked file(s) inspected," \
        "$DIST_COUNT file(s) under dist/"
}

main "$@"
