#!/bin/bash
#
# lsinitcpio - dump the contents of an initramfs image
#

shopt -s extglob

_list='--list'
_optcolor=1
_f_functions=functions

declare -A bsdcpio_options=(
    [list]='--list'
    [input]='-i'
    [quiet]='--quiet'
)

usage() {
    cat<<USAGE
lsinitcpio %VERSION%
usage: ${0##*/} [action] [options] <initramfs>

  Actions:
   -a, --analyze        analyze contents of image
   -c, --config         show configuration file image was built with
   -l, --list           list contents of the image (default)
   -x, --extract        extract image to disk

  Options:
   -h, --help           display this help
   -n, --nocolor        disable colorized output
   -V, --version        display version information
   -v, --verbose        more verbose output

USAGE
}

version() {
    cat<<EOF
lsinitcpio %VERSION%
EOF
}

decomp() {
    ${_compress:-cat} ${_compress:+-cd} "$@"
}

. "$_f_functions"

# override the die method from functions
die() {
    error "$@"
    exit 1
}

size_to_human() {
    awk -v size="$1" '
    BEGIN {
        suffix[1] = "B"
        suffix[2] = "KiB"
        suffix[3] = "MiB"
        suffix[4] = "GiB"
        suffix[5] = "TiB"
        count = 1

        while (size > 1024) {
            size /= 1024
            count++
        }

        sizestr = sprintf("%.2f", size)
        sub(/\.?0+$/, "", sizestr)
        printf("%s %s", sizestr, suffix[count])
    }'
}

detect_filetype() {
    case $(hexdump -n 6 -e '"%c"' "$1") in
        '070701')
            # no compression
            echo
            return
            ;;
        $'\xfd7zXZ')
            echo 'xz'
            return
            ;;
    esac

    if [[ $(hexdump -n 4 -e '"%c"' "$1") = $'\x89LZO' ]]; then
        echo 'lzop'
        return
    fi

    if [[ $(hexdump -n 2 -e '"%x"' "$1") = '8b1f' ]]; then
        echo 'gzip'
        return
    fi

    case $(hexdump -n 4 -e '"%x"' "$1") in
        184d2204)
            error 'Newer lz4 stream format detected! This may not boot!'
            echo 'lz4'
            return
            ;;
        184c2102)
            echo 'lz4 -l'
            return
            ;;
    esac

    if [[ $(hexdump -n 3 -e '"%c"' "$1") == '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.
    if [[ $(hexdump -n 3 -e '"%x"' "$1") = '5d' ]]; then
        echo 'lzma'
        return
    fi

    # still nothing? hrmm, maybe the user goofed and it's a kernel
    if kver "$1" >/dev/null; then
        die '%s is a kernel image, not an initramfs image!' "$1"
    fi

    # out of ideas, we're done here.
    die 'Unexpected file type. Are you sure %s is an initramfs?' "$1"
}

analyze_image() {
    local -a binaries explicitmod modules foundhooks hooks
    local kernver ratio columns=$(tput cols) image=$1

    workdir=$(mktemp -d --tmpdir lsinitcpio.XXXXXX)
    trap 'rm -rf "$workdir"' EXIT

    # fallback in case tput failed us
    columns=${columns:-80}

    zsize=$(stat -c %s "$_image")

    # calculate compression ratio
    decomptime=$(TIMEFORMAT=%R; { time decomp "$_image" >/dev/null; } 2>&1)
    if [[ $_compress ]]; then
        fullsize=$(decomp "$_image" | wc -c)
        ratio=.$(( zsize * 1000 / fullsize % 1000 ))
    fi

    # decompress the image since we need to read from it multiple times.
    decomp "$_image" | bsdtar -C "$workdir" -xf - || die 'Failed to decompress image'

    # collect stats
    kernver=("$workdir"/usr/lib/modules/*/)
    kernver=${kernver%/}
    kernver=${kernver##*/}

    modules=("$workdir/usr/lib/modules/$kernver"/kernel/*.ko*)
    if [[ -f ${modules[0]} ]]; then
        modules=("${modules[@]##*/}")
        modules=("${modules[@]%.ko*}")
    else
        unset modules
    fi

    foundhooks=("$workdir"/hooks/*)
    [[ -f ${foundhooks[0]} ]] && foundhooks=("${foundhooks[@]##*/}") || unset foundhooks

    mapfile -t binaries < <(find "$workdir/usr/bin" -type f -printf %f\\n)

    read -r version < "$workdir/VERSION"

    # source and read config
    . "$workdir/config"

    explicitmod=($MODULES)

    # print results
    imagename=$_image
    [[ -L $_image ]] && imagename+=" -> $(readlink -e "$_image")"
    msg 'Image: %s %s' "$imagename"
    [[ $version ]] && msg 'Created with mkinitcpio %s' "$version"
    msg 'Kernel: %s' "${kernver:-unknown}"
    msg 'Size: %s' "$(size_to_human "$zsize")"

    if [[ $_compress ]]; then
        msg 'Compressed with: %s' "$_compress"
        msg2 'Uncompressed size: %s (%s ratio)' "$(size_to_human "$fullsize")" "$ratio"
    fi
    msg2 'Estimated extraction time: %ss' "$decomptime"
    printf '\n'

    if (( ${#modules[*]} )); then
        msg 'Included modules:'
        for mod in "${modules[@]}"; do
            printf '  %s' "$mod"
            in_array "${mod//_/-}" "${explicitmod[@]//_/-}" && printf ' [explicit]'
            printf '\n'
        done | sort | column -c$columns
        printf '\n'
    fi

    msg 'Included binaries:'
    printf '  %s\n' "${binaries[@]}" | sort | column -c$columns
    printf '\n'

    if [[ $EARLYHOOKS ]]; then
        msg 'Early hook run order:'
        printf '  %s\n' $EARLYHOOKS
        printf '\n'
    fi

    if [[ $HOOKS ]]; then
        msg 'Hook run order:'
        printf '  %s\n' $HOOKS
        printf '\n'
    fi

    if [[ $LATEHOOKS ]]; then
        msg 'Late hook run order:'
        printf '  %s\n' $LATEHOOKS
        printf '\n'
    fi

    if [[ $CLEANUPHOOKS ]]; then
        msg 'Cleanup hook run order:'
        printf '  %s\n' $CLEANUPHOOKS
        printf '\n'
    fi
}

_opt_short='achlnVvx'
_opt_long=('analyze' 'config' 'help' 'list' 'nocolor' 'version' 'verbose' 'extract')

parseopts "$_opt_short" "${_opt_long[@]}" -- "$@" || exit
set -- "${OPTRET[@]}"
unset _opt_short _opt_long OPTRET

while :; do
    case $1 in
        -a|--analyze)
            _optanalyze=1
            ;;
        -c|--config)
            _optshowconfig=1
            ;;
        -h|--help)
            usage
            exit 0
            ;;
        -l|--list)
            _optlistcontents=1
            ;;
        -n|--nocolor)
            _optcolor=0
            ;;
        -V|--version)
            version
            exit 0
            ;;
        -v|--verbose)
            bsdcpio_options['verbose']='--verbose'
            ;;
        -x|--extract)
            unset 'bsdcpio_options[list]'
            ;;
        --)
            shift
            break 2
            ;;
    esac
    shift
done

_image=$1

if [[ -t 1 ]] && (( _optcolor )); then
    try_enable_color
fi

[[ $_image ]] || die "No image specified (use -h for help)"
[[ -f $_image ]] || die "No such file: %s" "$_image"

case $(( _optanalyze + _optlistcontents + _optshowconfig )) in
    0)
        # default action when none specified
        _optlistcontents=1 ;;
    [!1])
        die "Only one action may be specified at a time" ;;
esac

# read compression type
_compress=$(detect_filetype "$_image") || exit

if (( _optanalyze )); then
    analyze_image "$_image"
elif (( _optshowconfig )); then
    decomp "$_image" | bsdtar xOf - buildconfig 2>/dev/null ||
        die 'Failed to extract config from image (mkinitcpio too old?)'
else
    decomp "$_image" | bsdcpio "${bsdcpio_options[@]}"
fi

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