#!/usr/bin/ash
# SPDX-License-Identifier: GPL-2.0-only

# This file contains common functions used in init and in hooks

# logging targets
_rdlog_file="$(( 1 << 0 ))"
_rdlog_kmsg="$(( 1 << 1 ))"
_rdlog_cons="$(( 1 << 2 ))"
_rdlog_all="$(( (1 << 3) - 1 ))"

msg() {
    local quiet
    quiet="$(getarg quiet)"
    [ "${quiet}" != "y" ] && echo "$@"
}
err() {
    echo "ERROR: $*"
}

log_kmsg() {
    local fmt="$1"; shift
    # shellcheck disable=SC2059
    printf "<31>initramfs: $fmt\n" "$@"
}

poll_device() {
    # busybox ash supports string replacements
    # shellcheck disable=SC3060
    local device="$1" seconds="${2//[!0-9]/}"

    [ "${seconds:-x}" = x ] && seconds=10
    deciseconds="$(( seconds * 10 ))"

    # tenths of a second
    sleepinterval=1

    [ -b "$device" ] && return 0

    # assigned in /init
    # shellcheck disable=SC2154
    if [ "$udevd_running" -eq 1 ]; then
        msg "Waiting $seconds seconds for device $device ..." >&2
        while [ ! -b "$device" ] && [ "$deciseconds" -gt 0 ]; do
            if [ "$sleepinterval" -ge 10 ]; then
                sleep 1
                deciseconds="$(( deciseconds - 10 ))"
            else
                sleep ".${sleepinterval}"
                deciseconds="$(( deciseconds - sleepinterval ))"
                sleepinterval="$(( sleepinterval * 2 ))"
            fi
        done
    fi

    [ -b "$device" ]
}

# shellcheck disable=SC2120
launch_interactive_shell() {
    export PS1='[rootfs \W]\$ '

    # explicitly redirect to /dev/console in case we're logging. note that
    # anything done in the rescue shell will NOT be logged.
    {
        [ "$1" = "--exec" ] && exec sh -i
        sh -i
    } 0</dev/console 1>/dev/console 2>/dev/console
}

bitfield_has_bit() {
    [ "$(( $1 & $2 ))" -eq "$2" ]
}

major_minor_to_device() {
    local dev

    [ -e "/sys/dev/block/${1}:${2}" ] || return 1

    if dev="$(readlink -f "/sys/dev/block/${1}:${2}" 2>/dev/null)"; then
        echo "/dev/${dev##*/}"
        return 0
    fi

    return 1
}

run_hookfunctions() {
    local hook fn="$1" desc="$2"

    shift 2
    for hook in "$@"; do
        [ -x "/hooks/$hook" ] || continue

        unset "$fn"
        # shellcheck disable=SC1090
        . "/hooks/$hook"
        type "$fn" >/dev/null || continue

        msg ":: running $desc [$hook]"
        "$fn"
    done
}

set_log_option() {
    local opt

    # busybox ash supports string replacements
    # shellcheck disable=SC3060
    for opt in ${1//|/ }; do
        case "$opt" in
            all)
                rd_logmask="$_rdlog_all"
                ;;
            kmsg)
                rd_logmask="$(( rd_logmask | _rdlog_kmsg ))"
                ;;
            file)
                rd_logmask="$(( rd_logmask | _rdlog_file ))"
                ;;
            console)
                rd_logmask="$(( rd_logmask | _rdlog_cons ))"
                ;;
            *)
                err "unknown rd.log parameter: '$opt'"
                ;;
        esac
    done
}

startswith() {
    local word="$1" prefix="$2"

    case "$word" in
        $prefix*)
            return 0
            ;;
    esac

    return 1
}

endswith() {
    local word="$1" suffix="$2"

    case "$word" in
        *$suffix)
            return 0
            ;;
    esac

    return 1
}

parse_cmdline_item() {
    local key="$1" value="$2"

    case "$key" in
        rw | ro)
            rwopt="$key"
            ;;
        fstype)
            # The kernel understands 'rootfstype', but mkinitcpio has (without
            # documentation) supported 'fstype' instead. Ensure we support both
            # for backwards compat, but make fstype legacy.
            rootfstype="$value"
            ;;
        fsck.mode)
            case "$value" in
                force)
                    forcefsck=y
                    ;;
                skip)
                    fastboot=y
                    ;;
                *)
                    err "unknown fsck.mode parameter: '$value'"
                    ;;
            esac
            ;;
        rd.debug)
            rd_debug=y
            ;;
        rd.log)
            if [ -n "$value" ]; then
                set_log_option "$value"
            else
                rd_logmask="$(( _rdlog_kmsg | _rdlog_cons ))"
            fi
            ;;
        [![:alpha:]_]* | [[:alpha:]_]*[![:alnum:]_]*)
            # invalid shell variable, ignore it
            ;;
        *)
            # valid shell variable
            eval "$key"='${value:-y}'
            ;;
    esac
}

process_cmdline_param() {
    local item_callback="$1" key="$2" value="$3" cmdline_cache="$4"

    # maybe unquote the value
    # busybox ash supports string indexing
    # shellcheck disable=SC3057
    if startswith "$value" "[\"']" && endswith "$value" "${value:0:1}"; then
        value="${value#?}" value="${value%?}"
    fi

    # store the parsed cmdline for getarg
    printf '%s=%s\n' "$key" "${value:-y}" >> "$cmdline_cache"

    "$item_callback" "$key" "$value"
}

parse_cmdline() {
    local item_callback="${1:-parse_cmdline_item}"
    local cmdline_cache="${2:-/tmp/cmdline}"
    local cmdline word quoted key value

    set -f
    read -r cmdline
    # shellcheck disable=SC2086
    set -- $cmdline
    set +f

    for word; do
        if [ -n "$quoted" ]; then
            value="$value $word"
        else
            case "$word" in
                *=*)
                    key="${word%%=*}"
                    value="${word#*=}"

                    if startswith "$value" "[\"']"; then
                        # busybox ash supports string indexing
                        # shellcheck disable=SC3057
                        quoted="${value:0:1}"
                    fi
                    ;;
                '#'*)
                    break
                    ;;
                *)
                    key="$word"
                    ;;
            esac
        fi

        if [ -n "$quoted" ]; then
            if endswith "$value" "$quoted"; then
                unset quoted
            else
                continue
            fi
        fi

        process_cmdline_param "$item_callback" "$key" "$value" "$cmdline_cache"
        unset key value
    done

    if [ -n "$key" ]; then
        process_cmdline_param "$item_callback" "$key" "$value" "$cmdline_cache"
    fi
}

getarg() {
    local arg="$1" default="$2" cmdline_cache="${3:-/tmp/cmdline}" raw value

    raw="$(grep -s "^$arg=" "$cmdline_cache" | tail -1)"

    if [ -n "$raw" ]; then
        value="${raw#*=}"
        printf '%s' "${value:-$default}"
    else
        printf '%s' "$default"
    fi
}

fsck_device() {
    local device
    [ -x /sbin/fsck ] || return 255

    device="$(resolve_device "$1")"
    if [ ! -b "$device" ]; then
        err "device '$1' not found. Skipping fsck."
        return 255
    fi

    # If the device is specified via a tag supported by util-linux fsck,
    # pass the unmodified tag to fsck.
    case "$1" in
        'UUID='* | 'LABEL='* | 'PARTUUID='* | 'PARTLABEL='*) device="$1" ;;
        *) : ;;
    esac

    if [ -n "$fastboot" ]; then
        msg ":: skipping fsck on '$1'"
        return
    fi

    msg ":: performing fsck on '$1'"
    fsck -Ta -C"$FSCK_FD" "$device" -- ${forcefsck+-f}
}

fsck_root() {
    local root
    root="$(getarg root)"
    fsck_device "$root"
    fsckret="$?"

    if [ -n "$fsckret" ] && [ "$fsckret" -ne 255 ]; then
        # handle error conditions; do nothing on success.
        if bitfield_has_bit "$fsckret" 4; then
            # shellcheck disable=SC2086
            run_hookfunctions 'run_emergencyhook' 'emergency hook' $EMERGENCYHOOKS
            err "Bailing out. Run 'fsck $root' manually"
            printf '%s\n' \
                "********** FILESYSTEM CHECK FAILED **********" \
                "*                                           *" \
                "*  Please run fsck manually. After leaving  *" \
                "*  this maintenance shell, the system will  *" \
                "*  reboot automatically.                    *" \
                "*                                           *" \
                "*********************************************"
            # shellcheck disable=SC2119
            launch_interactive_shell
            echo ":: Automatic reboot in progress"
            sleep 2
            reboot -f
        elif bitfield_has_bit "$fsckret" 2; then
            # shellcheck disable=SC2086
            run_hookfunctions 'run_emergencyhook' 'emergency hook' $EMERGENCYHOOKS
            printf '%s\n' \
                "************** REBOOT REQUIRED **************" \
                "*                                           *" \
                "*   automatically restarting in 10 seconds  *" \
                "*                                           *" \
                "*********************************************"
            sleep 10
            reboot -f
        elif bitfield_has_bit "$fsckret" 8; then
            err "fsck failed on '$root'"
        elif bitfield_has_bit "$fsckret" 16; then
            err "Failed to invoke fsck: usage or syntax error"
        elif bitfield_has_bit "$fsckret" 32; then
            echo ":: fsck cancelled on user request"
        elif bitfield_has_bit "$fsckret" 128; then
            err "fatal error invoking fsck"
        fi

        # ensure that root is going to be mounted rw. Otherwise, systemd
        # might fsck the device again. Annoy the user so that they fix this.
        if [ "${rwopt:-ro}" != 'rw' ]; then
            echo "********************** WARNING **********************"
            echo "*                                                   *"
            echo "*  The root device is not configured to be mounted  *"
            echo "*  read-write! It may be fsck'd again later.        *"
            echo "*                                                   *"
            echo "*****************************************************"
        fi
    fi
}

# TODO: this really needs to follow the logic of systemd's encode_devnode_name
# function more closely.
tag_to_udev_path() {
    awk -v "tag=$1" -v "value=$2" '
    BEGIN {
        gsub(/\//, "\\x2f", value)
        printf "/dev/disk/by-%s/%s\n", tolower(tag), value
    }'
}

resolve_device() {
    local major minor dev device="$1" rootdelay

    # attempt to resolve devices immediately. if this fails
    # and udev is running, fall back on lazy resolution using
    # /dev/disk/by-* symlinks. this is flexible enough to support
    # usage of tags without udev and "slow" devices like root on
    # USB, which might not immediately show up.
    case "$device" in
        'UUID='* | 'LABEL='* | 'PARTUUID='* | 'PARTLABEL='*)
            dev="$(blkid -lt "$device" -o device)"
            if [ -z "$dev" ] && [ "$udevd_running" -eq 1 ]; then
                dev="$(tag_to_udev_path "${device%%=*}" "${device#*=}")"
            fi
            ;;
    esac

    [ -n "$dev" ] && device="$dev"

    case "$device" in
        # path to kernel named block device
        /dev/*)
            rootdelay="$(getarg rootdelay)"
            if poll_device "$device" "$rootdelay"; then
                echo "$device"
                return 0
            fi
            ;;
        # hex encoded major/minor, such as from LILO
        [0-9a-fA-F][0-9a-fA-F][0-9a-fA-F] | [0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])
            major="$(( 0x0$device >> 8 ))"
            minor="$(( 0x0$device & 0xff ))"
            ;;
        0x[0-9a-fA-F][0-9a-fA-F]*)
            major="$(( device >> 8 ))"
            minor="$(( device & 0xff ))"
            ;;
    esac

    if [ -n "$major" ] && [ -n "$minor" ]; then
        device="$(major_minor_to_device "$major" "$minor" || echo '/dev/root')"

        if [ ! -b "$device" ]; then
            msg "Creating device node with major $major and minor $minor." >&2
            mknod "$device" b "$major" "$minor"
        fi
        echo "$device"
        return 0
    fi

    return 1
}

default_mount_handler() {
    msg ":: mounting '$root' on real root"
    if ! mount -t "${rootfstype:-auto}" -o "${rwopt:-ro}${rootflags:+,$rootflags}" "$root" "$1"; then
        # shellcheck disable=SC2086
        run_hookfunctions 'run_emergencyhook' 'emergency hook' $EMERGENCYHOOKS
        err "Failed to mount '$root' on real root"
        echo "You are now being dropped into an emergency shell."
        # shellcheck disable=SC2119
        launch_interactive_shell
        msg "Trying to continue (this will most likely fail) ..."
    fi
}

rdlogger_start() {
    # rd.debug implies rd.log=console if rd.log(=.*)? isn't present
    if [ "$rd_logmask" -eq 0 ] && [ -n "$rd_debug" ]; then
        rd_logmask="$_rdlog_cons"
    fi

    [ "$rd_logmask" -gt 0 ] || return

    mkfifo /run/initramfs/rdlogger.pipe

    rdlogger </run/initramfs/rdlogger.pipe >/dev/console 2>&1 &
    printf %s "$!" >/run/initramfs/rdlogger.pid

    exec >/run/initramfs/rdlogger.pipe 2>&1
    [ -n "$rd_debug" ] && set -x

    # messages would be otherwise lost if we don't unset quiet. this does,
    # however, mean that passing rd.log=console will negate the effects of
    # 'quiet' for initramfs console output.
    unset quiet
}

rdlogger_stop() {
    local i=0 pid

    [ -e /run/initramfs/rdlogger.pipe ] || return

    [ -n "$rd_debug" ] && { set +x; } 2>/dev/null

    # this nudges rdlogger to exit
    exec 0<>/dev/console 1<>/dev/console 2<>/dev/console

    # wait up to 1 second for a graceful shutdown
    until [ ! -e /run/initramfs/rdlogger.pipe ] || [ "$i" -eq 10 ]; do
        sleep 0.1
        i="$(( i + 1 ))"
    done

    if [ "$i" -eq 10 ]; then
        # racy! the logger might still go away on its own
        read -r pid </run/initramfs/rdlogger.pid 2>/dev/null
        if [ -n "$pid" ]; then
            kill "$pid" 2>/dev/null
        fi
    fi
}

rdlogger() {
    local line

    # establish logging FDs. Either redirect to an appropriate target or to
    # /dev/null. This way, log methods can simply write and the associated FD
    # will Do The Right Thing™.

    # rd.log=console
    if bitfield_has_bit "$rd_logmask" "$_rdlog_cons"; then
        exec 4>/dev/console
    else
        exec 4>/dev/null
    fi

    # rd.log=kmsg
    if [ -c /dev/kmsg ] && bitfield_has_bit "$rd_logmask" "$_rdlog_kmsg"; then
        exec 5>/dev/kmsg
    else
        exec 5>/dev/null
    fi

    # rd.log=file
    if bitfield_has_bit "$rd_logmask" "$_rdlog_file"; then
        exec 6>/run/initramfs/init.log
    else
        exec 6>/dev/null
    fi

    while read -r line; do
        # rd.log=console
        printf '%s\n' "$line" >&4

        # rd.log=kmsg
        log_kmsg '%s' "$line" >&5

        # rd.log=file
        printf '%s\n' "$line" >&6
    done

    # EOF, shutting down...
    exec 4>&- 5>&- 6>&-
    rm -f /run/initramfs/rdlogger.pipe /run/initramfs/rdlogger.pid
}

mount_setup() {
    mount -t proc proc /proc -o nosuid,noexec,nodev
    mount -t sysfs sys /sys -o nosuid,noexec,nodev
    mount -t devtmpfs dev /dev -o mode=0755,nosuid
    mount -t tmpfs run /run -o nosuid,nodev,mode=0755
    mkdir -m755 /run/initramfs

    if [ -e /sys/firmware/efi ]; then
        mount -t efivarfs efivarfs /sys/firmware/efi/efivars -o nosuid,nodev,noexec
    fi

    # Setup /dev symlinks
    if [ -e /proc/kcore ]; then
        ln -sfT /proc/kcore /dev/core
    fi
    ln -sfT /proc/self/fd /dev/fd
    ln -sfT /proc/self/fd/0 /dev/stdin
    ln -sfT /proc/self/fd/1 /dev/stdout
    ln -sfT /proc/self/fd/2 /dev/stderr
}

# vim: set ft=sh ts=4 sw=4 et:
