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

shopt -s extglob

parseopts() {
    local opt='' optarg='' i='' shortopts="$1"
    local -a longopts=() unused_argv=()

    shift
    while [[ -n "$1" && "$1" != '--' ]]; do
        longopts+=("$1")
        shift
    done
    shift

    longoptmatch() {
        local o longmatch=()
        for o in "${longopts[@]}"; do
            if [[ "${o%:}" == "$1" ]]; then
                longmatch=("$o")
                break
            fi
            [[ "${o%:}" == "$1"* ]] && longmatch+=("$o")
        done

        case "${#longmatch[*]}" in
            1)
                # success, override with opt and return arg req (0 == none, 1 == required)
                opt="${longmatch%:}"
                if [[ "${longmatch[*]}" == *: ]]; then
                    return 1
                else
                    return 0
                fi
                ;;
            0)
                # fail, no match found
                return 255
                ;;
            *)
                # fail, ambiguous match
                printf "%s: option '%s' is ambiguous; possibilities:%s\n" "${0##*/}" \
                    "--$1" "$(printf " '%s'" "${longmatch[@]%:}")"
                return 254
                ;;
        esac
    }

    while (( $# )); do
        case "$1" in
            --) # explicit end of options
                shift
                break
                ;;
            -[!-]*) # short option
                for (( i = 1; i < ${#1}; i++ )); do
                    opt=${1:i:1}

                    # option doesn't exist
                    if [[ $shortopts != *$opt* ]]; then
                        printf "%s: invalid option -- '%s'\n" "${0##*/}" "$opt"
                        OPTRET=(--)
                        return 1
                    fi

                    OPTRET+=("-$opt")
                    # option requires optarg
                    if [[ "$shortopts" == *"${opt}:"* ]]; then
                        # if we're not at the end of the option chunk, the rest is the optarg
                        if (( i < ${#1} - 1 )); then
                            OPTRET+=("${1:i+1}")
                            break
                        # if we're at the end, grab the the next positional, if it exists
                        elif (( i == ${#1} - 1 )) && [[ -n "$2" ]]; then
                            OPTRET+=("$2")
                            shift
                            break
                        # parse failure
                        else
                            printf "%s: option '%s' requires an argument\n" "${0##*/}" "-$opt"
                            OPTRET=(--)
                            return 1
                        fi
                    fi
                done
                ;;
            --?*=* | --?*) # long option
                IFS='=' read -r opt optarg <<<"${1#--}"
                longoptmatch "$opt"
                case $? in
                    0)
                        if [[ -n "$optarg" ]]; then
                            printf "%s: option '--%s' doesn't allow an argument\n" "${0##*/}" "$opt"
                            OPTRET=(--)
                            return 1
                        else
                            OPTRET+=("--$opt")
                        fi
                        ;;
                    1)
                        # --longopt=optarg
                        if [[ -n "$optarg" ]]; then
                            OPTRET+=("--$opt" "$optarg")
                        # --longopt optarg
                        elif [[ -n "$2" ]]; then
                            OPTRET+=("--$opt" "$2")
                            shift
                        else
                            printf "%s: option '--%s' requires an argument\n" "${0##*/}" "$opt"
                            OPTRET=(--)
                            return 1
                        fi
                        ;;
                    254)
                        # ambiguous option -- error was reported for us by longoptmatch()
                        OPTRET=(--)
                        return 1
                        ;;
                    255)
                        # parse failure
                        printf "%s: unrecognized option '%s'\n" "${0##*/}" "--$opt"
                        OPTRET=(--)
                        return 1
                        ;;
                esac
                ;;
            *) # non-option arg encountered, add it as a parameter
                unused_argv+=("$1")
                ;;
        esac
        shift
    done

    # add end-of-opt terminator and any leftover positional parameters
    OPTRET+=('--' "${unused_argv[@]}" "$@")
    unset longoptmatch

    return 0
}

kver_x86() {
    local kver
    local -i offset
    # scrape the version out of the kernel image. locate the offset
    # to the version string by reading 2 bytes out of image at at
    # address 0x20E. this leads us to a string of, at most, 128 bytes.
    # read the first word from this string as the kernel version.
    offset="$(od -An -j0x20E -dN2 "$1")" || return

    read -r kver _ < \
        <(dd if="$1" bs=1 count=127 skip=$((offset + 0x200)) 2>/dev/null)

    printf '%s' "$kver"
}

detect_compression() {
    local file="$1" offset="${2:-0}" bytes

    bytes="$(od -An -t x1 -N6 -j "$offset" "$file" | tr -dc '[:alnum:]')"
    case "$bytes" in
        'fd377a585a00')
            echo 'xz'
            return
            ;;
    esac

    bytes="$(od -An -t x1 -N4 -j "$offset" "$file" | tr -dc '[:alnum:]')"
    if [[ "$bytes" == '894c5a4f' ]]; then
        echo 'lzop'
        return
    fi

    bytes="$(od -An -t x2 -N2 -j "$offset" "$file" | tr -dc '[:alnum:]')"
    if [[ "$bytes" == '8b1f' ]]; then
        echo 'gzip'
        return
    fi

    bytes="$(od -An -t x4 -N4 -j "$offset" "$file" | tr -dc '[:alnum:]')"
    case "$bytes" in
        '184d2204')
            error 'Newer lz4 stream format detected! This may not boot!'
            echo 'lz4'
            return
            ;;
        '184c2102')
            echo 'lz4 -l'
            return
            ;;
        'fd2fb528')
            echo 'zstd'
            return
            ;;
    esac

    bytes="$(od -An -c -N3 -j "$offset" "$file" | tr -dc '[:alnum:]')"
    if [[ "$bytes" == 'BZh' ]]; then
        echo 'bzip2'
        return
    fi

    # lzma detection sucks and there's really no good way to
    # do it without reading large portions of the stream. this
    # check is good enough for GNU tar, apparently, so it's good
    # enough for me.
    bytes="$(od -An -t x1 -N3 -j "$offset" "$file" | tr -dc '[:alnum:]')"
    if [[ "$bytes" == '5d0000' ]]; then
        echo 'lzma'
        return
    fi

    read -rd '' bytes < <(od -An -j $((offset + 0x04)) -t c -N4 "$file" | tr -dc '[:alnum:]')
    if [[ "$bytes" == 'zimg' ]]; then
        echo 'zimg'
        return
    fi

    # out of ideas, assuming uncompressed
}

kver_zimage() {
    # Generic EFI zboot added since kernel 6.1
    # https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/firmware/efi/libstub/Makefile.zboot?h=v6.1
    # https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/firmware/efi/libstub/zboot-header.S?h=v6.1

    local kver='' reader start size comp_type

    # Reading 4 bytes from address 0x08 is the starting offset of compressed data
    start="$(od -An -j0x08 -t u4 -N4 "$1" | tr -dc '[:alnum:]')"

    # Reading 4 bytes from address 0x0c is the size of compressed data,
    # but it needs to be corrected according to the compressed type.
    size="$(od -An -j0x0c -t u4 -N4 "$1" | tr -dc '[:alnum:]')"

    # Read 36 bytes (before 0x3c) from address 0x18,
    # which is a nul-terminated string representing the compressed type.
    read -rd '' comp_type < <(od -An -j0x18 -t a -N32 "$1" | sed 's/ nul//g' | tr -dc '[:alnum:]')

    [[ "$start" =~ ^[0-9]+$ ]] || return 1
    [[ "$size" =~ ^[0-9]+$ ]] || return 1

    case "$comp_type" in
        'gzip')
            reader='zcat'
            ;;
        'lz4')
            reader='lz4cat'
            size="$((size + 4))"
            ;;
        'lzma')
            reader='xzcat'
            size="$((size + 4))"
            ;;
        'lzo')
            reader="lzop -d"
            size="$((size + 4))"
            ;;
        'xzkern')
            reader='xzcat'
            size="$((size + 4))"
            ;;
        'zstd22')
            reader='zstdcat'
            size="$((size + 4))"
            ;;
        *)
            reader="$comp_type"
            size="$((size + 4))"
            ;;
    esac

    read -r _ _ kver _ < <(dd if="$1" bs=1 count="$size" skip="$start" 2>/dev/null | $reader - | grep -m1 -aoE  'Linux version .(\.[-[:alnum:]+]+)+')

    printf '%s' "$kver"
}

kver_generic() {
    # For unknown architectures, we can try to grep the uncompressed or gzipped
    # image for the boot banner.
    # This should work at least for ARM when run on /boot/Image, or RISC-V on
    # gzipped /boot/vmlinuz-linuz. On other architectures it may be worth trying
    # rather than bailing, and inform the user if none was found.

    # Loosely grep for `linux_banner`:
    # https://elixir.bootlin.com/linux/v5.7.2/source/init/version.c#L46
    local kver='' reader='cat'
    local comp_type=''

    comp_type="$(detect_compression "$1")"

    if [[ "$comp_type" == 'zimg' ]]; then
        # Generic EFI zboot image
        kver_zimage "$1"
        return 0
    elif [[ "$comp_type" == 'gzip' ]]; then
        reader='zcat'
    fi

    if [[ $1 =~ "/zImage" || $1 =~ /kernel7.img ]]; then
        # Check for xz zImage
        skip=$(LC_ALL=C grep -a -b -o $'\xFD\x37\x7A\x58\x5A\x00' $1 | tail -n 1 | cut -d ':' -f 1)
        if [[ ! -z $skip ]]; then
            read -r _ _ kver _ < <(dd if=$1 iflag=skip_bytes skip=$skip | xzcat | grep -m1 -aoE 'Linux version .(\.[-[:alnum:]+]+)+')
        fi
        if [[ -z "$kver" ]]; then
            # Check for gzip zImage
            skip=$(LC_ALL=C grep -a -b -o $'\x1f\x8b\x08\x00\x00\x00\x00\x00' $1 | head -n 1 | cut -d ':' -f 1)
            if [[ ! -z $skip ]]; then
              read -r _ _ kver _ < <(dd if=$1 iflag=skip_bytes skip=$skip | zcat | grep -m1 -aoE 'Linux version .(\.[-[:alnum:]+]+)+')
            fi
        fi
    else
        [[ "$(detect_compression "$1")" == 'gzip' ]] && reader='zcat'

        read -r _ _ kver _ < <($reader "$1" | grep -m1 -aoE 'Linux version .(\.[-[:alnum:]+]+)+')

        # try if the image is gzip compressed
        if [[ -z "$kver" ]]; then
            read _ _ kver _ < <(gzip -c -d "$1" | grep -m1 -aoE 'Linux version .(\.[-[:alnum:]]+)+')
        fi
    fi

    printf '%s' "$kver"
}

kver() {
    # this is intentionally very loose. only ensure that we're
    # dealing with some sort of string that starts with something
    # resembling dotted decimal notation. remember that there's no
    # requirement for CONFIG_LOCALVERSION to be set.
    local kver re='^[[:digit:]]+(\.[[:digit:]]+)+'

    local arch
    arch="$(uname -m)"
    if [[ $arch == @(i?86|x86_64) ]]; then
        kver="$(kver_x86 "$1")"
    else
        kver="$(kver_generic "$1")"
    fi

    [[ "$kver" =~ $re ]] || return 1

    printf '%s' "$kver"
}

plain() {
    local mesg="$1"; shift
    # shellcheck disable=SC2059
    printf "    $_color_bold$mesg$_color_none\n" "$@" >&1
}

quiet() {
    # _optquiet is assigned in mkinitcpio
    # shellcheck disable=SC2154
    (( _optquiet )) || plain "$@"
}

msg() {
    local mesg="$1"; shift
    # shellcheck disable=SC2059
    printf "$_color_green==>$_color_none $_color_bold$mesg$_color_none\n" "$@" >&1
}

msg2() {
    local mesg="$1"; shift
    # shellcheck disable=SC2059
    printf "  $_color_blue->$_color_none $_color_bold$mesg$_color_none\n" "$@" >&1
}

warning() {
    local mesg="$1"; shift
    # shellcheck disable=SC2059
    printf "$_color_yellow==> WARNING:$_color_none $_color_bold$mesg$_color_none\n" "$@" >&2
}

error() {
    local mesg="$1"; shift
    # shellcheck disable=SC2059
    printf "$_color_red==> ERROR:$_color_none $_color_bold$mesg$_color_none\n" "$@" >&2
    return 1
}

die() {
    error "$@"
    cleanup 1
}

map() {
    local r=0
    for _ in "${@:2}"; do
        # shellcheck disable=SC1105,SC2210,SC2035
        "$1" "$_" || (( $# > 255 ? r=1 : ++r ))
    done
    return "$r"
}

arrayize_config() {
    set -f
    [[ ${MODULES@a} != *a* ]] && IFS=' ' read -r -a MODULES <<<"$MODULES"
    [[ ${BINARIES@a} != *a* ]] && IFS=' ' read -r -a BINARIES <<<"$BINARIES"
    [[ ${FILES@a} != *a* ]] && IFS=' ' read -r -a FILES <<<"$FILES"
    [[ ${HOOKS@a} != *a* ]] && IFS=' ' read -r -a HOOKS <<<"$HOOKS"
    [[ ${COMPRESSION_OPTIONS@a} != *a* ]] && IFS=' ' read -r -a COMPRESSION_OPTIONS <<<"$COMPRESSION_OPTIONS"
    set +f
}

in_array() {
    # Search for an element in an array.
    #   $1: needle
    #   ${@:2}: haystack

    local item='' needle="$1"; shift

    for item in "$@"; do
        [[ "$item" == "$needle" ]] && return 0 # Found
    done
    return 1 # Not Found
}

index_of() {
    # get the array index of an item. sets the global var _idx with
    # index and returns 0 if found, otherwise returns 1.
    local item="$1"; shift

    for (( _idx=1; _idx <= $#; _idx++ )); do
        if [[ "$item" == "${!_idx}" ]]; then
            (( --_idx ))
            return 0
        fi
    done

    # not found
    unset _idx
    return 1
}

funcgrep() {
    awk -v funcmatch="$1" '
        /^[[:space:]]*[[:alnum:]_]+[[:space:]]*\([[:space:]]*\)/ {
            match($1, funcmatch)
            print substr($1, RSTART, RLENGTH)
        }' "$2"
}

list_hookpoints() {
    local funcs script

    # _d_hooks is assigned in mkinitcpio
    # shellcheck disable=SC2154
    script="$(PATH="$_d_hooks" type -P "$1")" || return 0

    mapfile -t funcs < <(funcgrep '^run_[[:alnum:]_]+' "$script")

    echo
    msg "This hook has runtime scripts:"
    in_array run_earlyhook "${funcs[@]}" && msg2 "early hook"
    in_array run_hook "${funcs[@]}" && msg2 "pre-mount hook"
    in_array run_latehook "${funcs[@]}" && msg2 "post-mount hook"
    in_array run_cleanuphook "${funcs[@]}" && msg2 "cleanup hook"
    in_array run_emergencyhook "${funcs[@]}" && msg2 "emergency hook"
}

modprobe() {
    # _optmoduleroot is assigned in mkinitcpio
    # shellcheck disable=SC2154
    command modprobe -d "$_optmoduleroot" -S "$KERNELVERSION" "$@"
}

all_modules() {
    # Add modules to the initcpio, filtered by grep.
    #   $@: filter arguments to grep
    #   -f FILTER: ERE to filter found modules

    local -i count=0
    local mod='' OPTIND='' OPTARG='' modfilter=()

    while getopts ':f:' flag; do
        [[ "$flag" = "f" ]] && modfilter+=("$OPTARG")
    done
    shift $(( OPTIND - 1 ))

    # _d_kmoduledir is assigned in mkinitcpio
    # shellcheck disable=SC2154
    while read -r -d '' mod; do
        (( ++count ))

        for f in "${modfilter[@]}"; do
            [[ "$mod" =~ $f ]] && continue 2
        done

        mod="${mod##*/}"
        mod="${mod%.ko*}"
        printf '%s\n' "${mod//-/_}"
    done < <(LC_ALL=C.UTF-8 find "$_d_kmoduledir" -name '*.ko*' -print0 2>/dev/null | grep -EZz "$@")

    (( count ))
}

add_all_modules() {
    # Add modules to the initcpio.
    #   $@: arguments to all_modules

    local mod
    local -a mods

    mapfile -t mods < <(all_modules "$@")
    map add_module "${mods[@]}"

    return $(( !${#mods[*]} ))
}

add_checked_modules() {
    # Add modules to the initcpio, filtered by the list of autodetected
    # modules.
    #   $@: arguments to all_modules

    local mod
    local -a mods

    # _autodetect_cache is declared in mkinitcpio and assigned in install/autodetect
    # shellcheck disable=SC2154
    if (( ${#_autodetect_cache[*]} )); then
        mapfile -t mods < <(all_modules "$@" | grep -xFf <(printf '%s\n' "${!_autodetect_cache[@]}"))
    else
        mapfile -t mods < <(all_modules "$@")
    fi

    map add_module "${mods[@]}"

    return $(( !${#mods[*]} ))
}

add_firmware() {
    # add a firmware file to the image.
    #   $1: firmware path fragment

    local fw fwpath
    local -a fwfile
    local -i r=1

    for fw; do
        # _d_fwpath is assigned in mkinitcpio
        # shellcheck disable=SC2154
        for fwpath in "${_d_fwpath[@]}"; do
            # shellcheck disable=SC2153
            if ! compgen -G "${BUILDROOT}${fwpath}/${fw}?(.*)" &>/dev/null; then
                # modinfo has firmware entries with globs, read entries into an array
                if read -r fwfile < <(compgen -G "${fwpath}/${fw}?(.*)"); then
                    map add_file "${fwfile[@]}" && r=0
                    break
                fi
            else
                r=0
                break
            fi
        done
    done

    return "$r"
}

add_module() {
    # Add a kernel module to the initcpio image. Dependencies will be
    # discovered and added.
    #   $1: module name

    local target='' module='' softdeps=() deps=() field='' value='' firmware=()
    local ign_errors=0 found=0

    [[ "$KERNELVERSION" == 'none' ]] && return 0

    if [[ "$1" == *\? ]]; then
        ign_errors=1
        set -- "${1%?}"
    fi

    target="${1%.ko*}" target="${target//-/_}"

    # skip expensive stuff if this module has already been added
    (( _addedmodules["$target"] == 1 )) && return

    while IFS=':= ' read -r -d '' field value; do
        case "$field" in
            filename)
                # Only add modules with filenames that look like paths (e.g.
                # it might be reported as "(builtin)"). We'll defer actually
                # checking whether or not the file exists -- any errors can be
                # handled during module install time.
                if [[ "$value" == /* ]]; then
                    found=1
                    module="${value##*/}" module="${module%.ko*}"
                    quiet "adding module: %s (%s)" "$module" "$value"
                    _modpaths["$value"]=1
                    _addedmodules["${module//-/_}"]=1
                fi
                ;;
            depends)
                IFS=',' read -r -a deps <<<"$value"
                map add_module "${deps[@]}"
                ;;
            firmware)
                firmware+=("$value")
                ;;
            softdep)
                read -ra softdeps <<<"$value"
                for module in "${softdeps[@]}"; do
                    [[ $module == *: ]] && continue
                    add_module "$module?"
                done
                ;;
        esac
    done < <(modinfo -b "$_optmoduleroot" -k "$KERNELVERSION" -0 "$target" 2>/dev/null)

    if (( !found )); then
        (( ign_errors || _addedmodules["$target"] )) && return 0
        error "module not found: '%s'" "$target"
        return 1
    fi

    if (( ${#firmware[*]} )); then
        add_firmware "${firmware[@]}" ||
            warning "Possibly missing firmware for module: '%s'" "$target"
    fi

    # handle module quirks
    case "$target" in
        fat)
            add_module "nls_ascii?" # from CONFIG_FAT_DEFAULT_IOCHARSET
            add_module "nls_cp437?" # from CONFIG_FAT_DEFAULT_CODEPAGE
            ;;
        ocfs2)
            add_module "configfs?"
            ;;
        btrfs)
            add_module "libcrc32c?"
            ;;
        f2fs)
            add_module "crypto-crc32?"
            ;;
        ext4)
            add_module "crypto-crc32c?"
            ;;
    esac
}

add_full_dir() {
    # Add a directory and all its contents, recursively, to the initcpio image.
    # No parsing is performed and the contents of the directory is added as is.
    #   $1: path to directory
    #   $2: glob pattern to filter file additions (optional)
    #   $3: path prefix that will be stripped off from the image path (optional)

    local f='' filter="${2:-*}" strip_prefix="$3"

    if [[ -n "$1" && -d "$1" ]]; then
        add_dir "$1"

        for f in "$1"/*; do
            if [[ -L "$f" ]]; then
                # Explicit glob matching
                # shellcheck disable=SC2053
                if [[ "$f" == $filter ]]; then
                    add_symlink "${f#"${strip_prefix}"}" "$(readlink "$f")"
                fi
            elif [[ -d "$f" ]]; then
                add_full_dir "$f" "$filter" "$strip_prefix"
            elif [[ -f "$f" ]]; then
                # Explicit glob matching
                # shellcheck disable=SC2053
                if [[ "$f" == $filter ]]; then
                    add_file "$f" "${f#"${strip_prefix}"}"
                fi
            fi
        done
    fi
}

add_dir() {
    # add a directory (with parents) to $BUILDROOT
    #   $1: pathname on initcpio
    #   $2: mode (optional)

    if [[ -z "$1" || "$1" != /?* ]]; then
        return 1
    fi

    local path="$1" mode="${2:-755}"

    # shellcheck disable=SC2153
    if [[ -d "${BUILDROOT}${1}" ]]; then
        # ignore dir already exists
        return 0
    fi

    quiet "adding dir: %s" "$path"
    command install -dm"${mode}" "${BUILDROOT}${path}"
}

add_symlink() {
    # Add a symlink to the initcpio image. There is no checking done
    # to ensure that the target of the symlink exists.
    #   $1: pathname of symlink on image
    #   $2: absolute path to target of symlink (optional, can be read from $1)

    local name="$1" target="${2:-$1}" linkobject

    (( $# == 1 || $# == 2 )) || return 1

    # find out the link target
    if [[ "$name" == "$target" ]]; then
        linkobject="$(LC_ALL=C.UTF-8 find "$target" -prune -printf '%l')"
        # use relative path if the target is a file in the same directory as the link
        # anything more would lead to the insanity of parsing each element in its path
        if [[ "$linkobject" != *'/'* && ! -L "${name%/*}/${linkobject}" ]]; then
            target="$linkobject"
        else
            target="$(realpath -eq -- "$target")"
        fi
    elif [[ -L "$target" ]]; then
        target="$(realpath -eq -- "$target")"
    fi
    if [[ -z "$target" ]]; then
        error "invalid symlink: '%s'" "$name"
        return 1
    fi

    add_dir "${name%/*}"

    if [[ -L "${BUILDROOT}${1}" ]]; then
        quiet "overwriting symlink %s -> %s" "$name" "$target"
    else
        quiet "adding symlink: %s -> %s" "$name" "$target"
    fi
    ln -sfn "$target" "${BUILDROOT}${name}"
}

add_file() {
    # Add a plain file to the initcpio image. No parsing is performed and only
    # the singular file is added.
    #   $1: path to file
    #   $2: destination on initcpio (optional, defaults to same as source)
    #   $3: mode

    # determine source and destination
    local src="$1" dest="${2:-$1}" mode="$3" srcrealpath destrealpath

    if [[ ! -f "$src" ]]; then
        error "file not found: '%s'" "$src"
        return 1
    fi

    # Handle cases where the parent of the destination is a symlink.
    # What if there is a symlink higher up in the file path, you ask?
    # We simply hope that never happens...
    if [[ -z "$mode" && "$src" == "$dest" && -L "${dest%/*}" ]]; then
        destrealpath="$(realpath -- "${dest%/*}")"
        if [[ ! -e "${BUILDROOT}${dest%/*}" ]]; then
            add_dir "${destrealpath}"
            add_symlink "${dest%/*}"
        fi

        # Rewrite destination to reflect real path of target
        dest="${destrealpath}/${dest##*/}"
    fi

    # check if $src is a symlink
    if [[ -L "$src" ]]; then
        srcrealpath="$(realpath -- "$src")"
        if [[ "$srcrealpath" != "$dest" ]]; then
            # add the target file
            add_file "$srcrealpath" "$srcrealpath" "$mode"
            # create the symlink
            add_symlink "$dest" "$src"
            return
        fi
    fi

    # unlike install, cp does not create directories leading to the destination
    [[ -z "$mode" && ! -e "${BUILDROOT}${dest%/*}" ]] && add_dir "${dest%/*}"

    if [[ -e "${BUILDROOT}${dest}" ]]; then
        quiet 'overwriting file: %s' "$dest"
    else
        quiet 'adding file: %s' "$dest"
    fi
    if [[ -z "$mode" ]]; then
        command cp --remove-destination --preserve=mode,ownership "$src" "${BUILDROOT}${dest}"
    else
        command install -Dm"$mode" "$src" "${BUILDROOT}${dest}"
    fi
}

add_runscript() {
    # Adds a runtime script to the initcpio image. The name is derived from the
    # script which calls it as the basename of the caller.

    local fn script hookname="${BASH_SOURCE[1]##*/}"
    local -a funcs

    if ! script="$(PATH="$_d_hooks" type -P "$hookname")"; then
        error "runtime script for '%s' not found" "$hookname"
        return
    fi

    if [[ -L "$script" ]]; then
        script="$(realpath -- "$script")"
    fi
    add_file "$script" "/hooks/$hookname" 755

    mapfile -t funcs < <(funcgrep '^run_[[:alnum:]_]+' "$script")

    for fn in "${funcs[@]}"; do
        case $fn in
            run_earlyhook)
                _runhooks['early']+=" $hookname"
                ;;
            run_hook)
                _runhooks['hooks']+=" $hookname"
                ;;
            run_latehook)
                _runhooks['late']+=" $hookname"
                ;;
            run_cleanuphook)
                _runhooks['cleanup']="$hookname ${_runhooks['cleanup']}"
                ;;
            run_emergencyhook)
                _runhooks['emergency']="$hookname ${_runhooks['emergency']}"
                ;;
        esac
    done
}

add_binary() {
    # Add a binary file to the initcpio image. library dependencies will
    # be discovered and added.
    #   $1: path to binary
    #   $2: destination on initcpio (optional, defaults to same as source)

    local line='' regex='' binary='' dest='' mode='' sodep='' resolved='' shebang='' interpreter=''

    if [[ "${1:0:1}" != '/' ]]; then
        if ! binary="$(type -P "$1")"; then
            error "binary not found: '%s'" "$1"
            return 1
        fi
    else
        binary="$1"
    fi

    dest="${2:-$binary}"

    add_file "$binary" "$dest" || return 1

    # non-binaries
    if ! lddout="$(ldd "$binary" 2>/dev/null)"; then
        # detect if the file has a shebang
        if IFS='' LC_ALL=C.UTF-8 read -rn2 -d '' shebang <"$binary" && [[ "$shebang" == '#!' ]]; then
            read -r shebang <"$binary"
            interpreter="${shebang##\#\!*([[:space:]])}"
            # strip /usr/bin/env and warn if it is missing
            if [[ "$interpreter" == '/usr/bin/env'* ]]; then
                [[ -e "${BUILDROOT}/usr/bin/env" ]] || warning "Possibly missing '/usr/bin/env' for script: %s" "$binary"
                interpreter="${interpreter##'/usr/bin/env'+([[:space:]])}"
            fi
            # strip parameters
            interpreter="${interpreter%%[[:space:]]*}"
            # check if the interpreter exists in BUILDROOT
            if [[ "$interpreter" != '/'* ]] && PATH="${BUILDROOT}/usr/local/sbin:${BUILDROOT}/usr/local/bin:${BUILDROOT}/usr/bin" type -P "$interpreter" &>/dev/null; then
                :
            elif [[ -e "${BUILDROOT}/${interpreter}" ]]; then
                :
            else
                warning "Possibly missing '%s' for script: %s" "$interpreter" "$binary"
            fi
        fi
        return 0
    fi

    # resolve sodeps
    regex='^(|.+ )(/.+) \(0x[a-fA-F0-9]+\)'
    while read -r line; do
        if [[ "$line" =~ $regex ]]; then
            sodep="${BASH_REMATCH[2]}"
        elif [[ "$line" = *'not found' ]]; then
            error "binary dependency '%s' not found for '%s'" "${line%% *}" "$1"
            (( ++_builderrors ))
            continue
        fi

        if [[ -f "$sodep" && ! -e "${BUILDROOT}${sodep}" ]]; then
            add_file "$sodep" "$sodep"
        fi
    done <<<"$lddout"

    return 0
}

add_udev_rule() {
    # Add an udev rules file to the initcpio image. Dependencies on binaries
    # will be discovered and added.
    #   $1: path to rules file (or name of rules file)

    local rules="$1" rule=() key='' value='' binary=''

    if [[ "${rules:0:1}" != '/' ]]; then
        rules="$(PATH='/usr/lib/udev/rules.d:/lib/udev/rules.d' type -P "$rules")"
    fi
    if [[ -z "$rules" ]]; then
        # complain about not found rules
        return 1
    fi

    add_file "$rules" /usr/lib/udev/rules.d/"${rules##*/}"

    while IFS=, read -ra rule; do
        # skip empty lines, comments
        # rule is an array, but we are only checking if it's an empty string
        # shellcheck disable=SC2128
        [[ -z "$rule" || "$rule" == @(+([[:space:]])|#*) ]] && continue

        for pair in "${rule[@]}"; do
            IFS=' =' read -r key value <<<"$pair"
            case "$key" in
                'RUN{program}' | 'RUN+' | 'IMPORT{program}' | 'ENV{REMOVE_CMD}')
                    # strip quotes
                    binary="${value//[\"\']/}"
                    # just take the first word as the binary name
                    binary="${binary%% *}"
                    [[ "${binary:0:1}" == '$' ]] && continue
                    if [[ "${binary:0:1}" != '/' ]]; then
                        binary="$(PATH='/usr/lib/udev:/lib/udev' type -P "$binary")"
                    fi
                    add_binary "$binary"
                    ;;
            esac
        done
    done <"$rules"
}

parse_config() {
    # parse key global variables set by the config file.

    map add_module "${MODULES[@]}"
    map add_binary "${BINARIES[@]}"
    map add_file "${FILES[@]}"

    tee "$BUILDROOT/buildconfig" <"$1" | {
        # When MODULES is not an array (but instead implicitly converted at
        # startup), sourcing the config causes the string value of MODULES
        # to be assigned as MODULES[0]. Avoid this by explicitly unsetting
        # MODULES before re-sourcing the config.
        unset MODULES

        # shellcheck disable=SC1091
        . /dev/stdin

        # arrayize MODULES if necessary.
        [[ ${MODULES@a} != *a* ]] && read -ra MODULES <<<"${MODULES//-/_}"

        for mod in "${MODULES[@]%\?}"; do
            mod="${mod//-/_}"
            # only add real modules (2 == builtin)
            (( _addedmodules["$mod"] == 1 )) && add+=("$mod")
        done
        (( ${#add[*]} )) && printf 'MODULES="%s"\n' "${add[*]}"

        printf '%s="%s"\n' \
            'EARLYHOOKS' "${_runhooks['early']# }" \
            'HOOKS' "${_runhooks['hooks']# }" \
            'LATEHOOKS' "${_runhooks['late']# }" \
            'CLEANUPHOOKS' "${_runhooks['cleanup']% }" \
            'EMERGENCYHOOKS' "${_runhooks['emergency']% }"
    } >"$BUILDROOT/config"
}

initialize_buildroot() {
    # creates a temporary directory for the buildroot and initialize it with a
    # basic set of necessary directories and symlinks

    local kernver="$1" generatedir="$2" workdir arch buildroot osreleasefile
    arch="$(uname -m)"

    if ! workdir="$(mktemp -d --tmpdir mkinitcpio.XXXXXX 2>/dev/null)"; then
        error 'Failed to create temporary working directory in %s' "${TMPDIR:-/tmp}"
        return 1
    fi
    earlyroot="$workdir/early"
    buildroot="${generatedir:-$workdir/root}"

    if [[ ! -w "${generatedir:-$workdir}" ]]; then
        error 'Unable to write to build root: %s' "$buildroot"
        return 1
    fi

    # early root structure
    install -dm755 "$earlyroot"
    # this flag file is used by some tools
    echo 1 > "${earlyroot}/early_cpio"

    # base directory structure
    install -dm755 "$buildroot"/{new_root,proc,sys,dev,run,tmp,var,etc,usr/{local{,/bin,/sbin,/lib},lib,bin}}
    ln -s "usr/lib" "$buildroot/lib"
    ln -s "bin"     "$buildroot/usr/sbin"
    ln -s "usr/bin" "$buildroot/bin"
    ln -s "usr/bin" "$buildroot/sbin"
    ln -s "/run"    "$buildroot/var/run"

    case "$arch" in
        x86_64)
            ln -s "lib"     "$buildroot/usr/lib64"
            ln -s "usr/lib" "$buildroot/lib64"
            ;;
    esac

    # mkinitcpio version stamp
    # shellcheck disable=SC2154
    printf '%s' "$version" >"$buildroot/VERSION"

    # kernel module dir
    [[ "$kernver" != 'none' ]] && install -dm755 "$buildroot/usr/lib/modules/$kernver/kernel"

    # mount tables
    ln -s ../proc/self/mounts "$buildroot/etc/mtab"
    : >"$buildroot/etc/fstab"

    # add os-release and initrd-release for systemd
    if [[ -e /etc/os-release ]]; then
        if [[ -L /etc/os-release ]]; then
            osreleasefile="$(realpath -- /etc/os-release)"
            install -Dm0644 "$osreleasefile" "${buildroot}${osreleasefile}"
            cp -adT /etc/os-release "${buildroot}/etc/os-release"
            cp -adT /etc/os-release "${buildroot}/etc/initrd-release"
        else
            install -Dm0644 /etc/os-release "${buildroot}/etc/os-release"
            ln -sT os-release "${buildroot}/etc/initrd-release"
        fi
    else
        : >"$buildroot/etc/initrd-release"
    fi

    # add a blank ld.so.conf to keep ldconfig happy
    : >"$buildroot/etc/ld.so.conf"

    printf '%s' "$workdir"
}

run_build_hook() {
    local hook="$1" script='' resolved=''
    # shellcheck disable=SC2034
    local MODULES=() BINARIES=() FILES=() SCRIPT=''

    # find script in install dirs
    # _d_install is assigned in mkinitcpio
    # shellcheck disable=SC2154
    if ! script="$(PATH="$_d_install" type -P "$hook")"; then
        error "Hook '$hook' cannot be found"
        return 1
    fi

    # check for deprecation
    if resolved="$(readlink -e "$script")" && [[ "${script##*/}" != "${resolved##*/}" ]]; then
        warning "Hook '%s' is deprecated. Replace it with '%s' in your config" \
            "${script##*/}" "${resolved##*/}"
        script="$resolved"
    fi

    # source
    unset -f build
    # shellcheck disable=SC1090
    if ! . "$script"; then
        error 'Failed to read %s' "$script"
        return 1
    fi

    if ! declare -f build >/dev/null; then
        error "Hook '%s' has no build function" "${script}"
        return 1
    fi

    # run
    if (( _optquiet )); then
        msg2 "Running build hook: [%s]" "${script##*/}"
    else
        msg2 "Running build hook: [%s]" "$script"
    fi
    build

    # if we made it this far, return successfully. Hooks can
    # do their own error catching if it's severe enough, and
    # we already capture errors from the add_* functions.
    return 0
}

try_enable_color() {
    local colors

    if ! colors="$(tput colors 2>/dev/null)"; then
        warning "Failed to enable color. Check your TERM environment variable"
        return
    fi

    if (( colors > 0 )) && tput setaf 0 &>/dev/null; then
        _color_none="$(tput sgr0)"
        _color_bold="$(tput bold)"
        _color_blue="$_color_bold$(tput setaf 4)"
        _color_green="$_color_bold$(tput setaf 2)"
        _color_red="$_color_bold$(tput setaf 1)"
        _color_yellow="$_color_bold$(tput setaf 3)"
    fi
}

install_modules() {
    local m
    local -a xz_comp gz_comp zst_comp

    [[ "$KERNELVERSION" == 'none' ]] && return 0

    if (( $# == 0 )); then
        warning "No modules were added to the image. This is probably not what you want."
        return 0
    fi

    for m in "$@"; do
        add_file "$m"
        # unzip modules prior to recompression
        if [[ "$MODULES_DECOMPRESS" == 'yes' ]]; then
            case "$m" in
                *.xz)
                    xz_comp+=("$BUILDROOT/$m")
                    ;;
                *.gz)
                    gz_comp+=("$BUILDROOT/$m")
                    ;;
                *.zst)
                    zst_comp+=("$BUILDROOT/$m")
                    ;;
            esac
        fi
    done

    (( ${#xz_comp[*]} )) && xz -d "${xz_comp[@]}"
    (( ${#gz_comp[*]} )) && gzip -d "${gz_comp[@]}"
    (( ${#zst_comp[*]} )) && zstd -d --rm -q "${zst_comp[@]}"

    msg "Generating module dependencies"
    map add_file "$_d_kmoduledir"/modules.{builtin,builtin.modinfo,order}
    depmod -b "$BUILDROOT" "$KERNELVERSION"

    # remove all non-binary module.* files (except devname for on-demand module loading)
    rm "${BUILDROOT}${_d_kmoduledir}"/modules.!(*.bin|devname|softdep)
}

find_module_from_symbol() {
    # Find a module based off on the symbol
    #   $1: symbol to find
    #   $2: the directory to look at
    #
    # The directory can either be a:
    #   absolute directory with a leading /
    #   A subdirectory with a = prefix, like =drivers/hid

    local moduledir symbols="$1" directories=("${@:2}")
    for dir in "${directories[@]}"; do
        case "${dir::1}" in
            = )
                moduledir="/lib/modules/$KERNELVERSION/kernel/${dir:1}"
                ;;
            /*)
                moduledir="$dir"
                ;;
        esac


        decomp_to_stdout() {
            case "$1" in
                *.xz) xz -d "$1" -c ;;
                *.gz) gzip -d "$1" -c ;;
                *.zst) zstd -q -d "$1" -c ;;
            esac
        }

        while read -r -d '' mod; do
            if decomp_to_stdout "$mod" | grep -Eq "^($symbols)" &> /dev/null; then
                mod=${mod##*/}
                mod="${mod%.ko*}"
                printf '%s\n' "${mod//-/_}"
            fi
        done < <(LC_ALL=C.UTF-8 find "$moduledir" -name '*.ko*' -print0 2>/dev/null)
    done
}

add_all_modules_from_symbol() {
    local -a mods

    mapfile -t mods < <(find_module_from_symbol "$@")
    (( ${#mods[@]} )) || return 1
    map add_module "${mods[@]}"
}

add_checked_modules_from_symbol() {
    local -a mods

    mapfile -t mods < <(find_module_from_symbol "$@")
    (( ${#mods[@]} )) || return 1

    # _autodetect_cache is declared in mkinitcpio and assigned in install/autodetect
    # shellcheck disable=SC2154
    if (( ${#_autodetect_cache[@]} )); then
        mapfile -t mods < <(grep -xFf <(printf '%s\n' "${!_autodetect_cache[@]}") <<<"${mods[@]}")
        # Do not fail if no autodetected module has the symbol
        if (( ${#mods[@]} )); then
            return 0
        fi
    fi

    map add_module "${mods[@]}"
}

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