#!/hint/bash

# Copyright © Tavian Barnes <tavianator@tavianator.com>
# SPDX-License-Identifier: 0BSD

## Utility functions

# Portable realpath(1)
_realpath() (
    cd "$(dirname -- "$1")"
    echo "$PWD/$(basename -- "$1")"
)

# Globals
ROOT=$(_realpath "$(dirname -- "$TESTS")")
TESTS="$ROOT/tests"
BIN="$ROOT/bin"
MKSOCK="$BIN/tests/mksock"
PTYX="$BIN/tests/ptyx"
XTOUCH="$BIN/tests/xtouch"
UNAME=$(uname)

# Standardize the environment
stdenv() {
    export LC_ALL=C
    export TZ=UTC0

    local SAN_OPTIONS="abort_on_error=1:halt_on_error=1:log_to_syslog=0"
    export ASAN_OPTIONS="$SAN_OPTIONS"
    export LSAN_OPTIONS="$SAN_OPTIONS"
    export MSAN_OPTIONS="$SAN_OPTIONS"
    export TSAN_OPTIONS="$SAN_OPTIONS"
    export UBSAN_OPTIONS="$SAN_OPTIONS"

    export LS_COLORS=""
    unset BFS_COLORS
    unset LSCOLORS

    if [ "$UNAME" = Darwin ]; then
        # ASan on macOS likes to report
        #
        #     malloc: nano zone abandoned due to inability to preallocate reserved vm space.
        #
        # to syslog, which as a side effect opens a socket which might take the
        # place of one of the standard streams if the process is launched with
        # it closed.  This environment variable avoids the message.
        export MallocNanoZone=0
    fi

    # Count the inherited FDs
    if [ -d /proc/self/fd ]; then
        local fds=/proc/self/fd
    else
        local fds=/dev/fd
    fi
    # We use ls $fds on purpose, rather than e.g. ($fds/*), to avoid counting
    # internal bash fds that are not exposed to spawned processes
    NOPENFD=$(ls -1q "$fds/" 2>/dev/null | wc -l)
    NOPENFD=$((NOPENFD > 3 ? NOPENFD - 1 : 3))

    # Close stdin so bfs doesn't think we're interactive
    # dup() the standard fds for logging even when redirected
    exec </dev/null {DUPOUT}>&1 {DUPERR}>&2

    # Get the ttyname
    if [ -t $DUPOUT ]; then
        TTY=$(tty <&$DUPOUT)
    elif [ -t $DUPERR ]; then
        TTY=$(tty <&$DUPERR)
    else
        TTY=
    fi
}

# Drop root privileges or bail
drop_root() {
    if command -v capsh &>/dev/null; then
        if capsh --has-p=cap_dac_override &>/dev/null || capsh --has-p=cap_dac_read_search &>/dev/null; then
            if [ -n "${BFS_TRIED_DROP:-}" ]; then
                color cat >&2 <<EOF
${RED}error:${RST} Failed to drop capabilities.
EOF

                exit 1
            fi

            color cat >&2 <<EOF
${YLW}warning:${RST} Running as ${BLD}$(id -un)${RST} is not recommended.  Dropping ${BLD}cap_dac_override${RST} and
${BLD}cap_dac_read_search${RST}.

EOF

            BFS_TRIED_DROP=y exec capsh \
                --drop=cap_dac_override,cap_dac_read_search \
                --caps=cap_dac_override,cap_dac_read_search-eip \
                -- "$0" "$@"
        fi
    elif ((EUID == 0)); then
        UNLESS=
        if [ "$UNAME" = "Linux" ]; then
            UNLESS=" unless ${GRN}capsh${RST} is installed"
        fi

        color cat >&2 <<EOF
${RED}error:${RST} These tests expect filesystem permissions to be enforced, and therefore
will not work when run as ${BLD}$(id -un)${RST}${UNLESS}.
EOF
        exit 1
    fi
}

## Debugging

# Get the bash call stack
callers() {
    local frame=0
    while caller $frame; do
        ((++frame))
    done
}

# Print a message including path, line number, and command
debug() {
    local file="$1"
    local line="$2"
    local msg="$3"
    local cmd="$(awk "NR == $line" "$file" 2>/dev/null)" || :
    file="${file/#*\/tests\//tests/}"

    color printf "${BLD}%s:%d:${RST} %s\n    %s\n" "$file" "$line" "$msg" "$cmd"
}

## Deferred cleanup

# Quote a command safely for eval
quote() {
    printf '%q' "$1"
    shift
    if (($# > 0)); then
        printf ' %q' "$@"
    fi
}

DEFER_LEVEL=-1

# Run a command when this (sub)shell exits
defer() {
    # Check if the EXIT trap is already set
    if ((DEFER_LEVEL != BASH_SUBSHELL)); then
        DEFER_LEVEL=$BASH_SUBSHELL
        DEFER_CMDS=()
        DEFER_LINES=()
        DEFER_FILES=()
        trap pop_defers EXIT
    fi

    DEFER_CMDS+=("$(quote "$@")")

    local line file
    read -r line file < <(caller)
    DEFER_LINES+=("$line")
    DEFER_FILES+=("$file")
}

# Pop a single command from the defer stack and run it
pop_defer() {
    local cmd="${DEFER_CMDS[-1]}"
    local file="${DEFER_FILES[-1]}"
    local line="${DEFER_LINES[-1]}"
    unset "DEFER_CMDS[-1]"
    unset "DEFER_FILES[-1]"
    unset "DEFER_LINES[-1]"

    local ret=0
    eval "$cmd" || ret=$?

    if ((ret != 0)); then
        debug "$file" $line "${RED}error $ret${RST}" >&$DUPERR
    fi

    return $ret
}

# Run all deferred commands
pop_defers() {
    local ret=0

    while ((${#DEFER_CMDS[@]} > 0)); do
        pop_defer || ret=$?
    done

    return $ret
}

## Parallelism

# Get the number of processors
_nproc() {
    {
        nproc \
            || sysctl -n hw.ncpu \
            || getconf _NPROCESSORS_ONLN \
            || echo 1
    } 2>/dev/null
}

# Stores the PID that exited from the last _wait call
WAIT_PID=

# Run wait, looping if interrupted
_wait() {
    local ret

    while :; do
        ret=0
        wait -pWAIT_PID "$@" || ret=$?

        if [ "${WAIT_PID:-}" ]; then
            # If WAIT_PID is filled in, the child exited (possibly with a status > 128)
            break
        elif ((ret > 128)); then
            # https://www.gnu.org/software/bash/manual/bash.html#index-wait
            #
            #     If wait is interrupted by a signal, the return status will be greater than 128
            continue
        else
            # wait itself failed (e.g. invalid PID)
            break
        fi
    done

    return $ret
}
