#!/bin/sh -efu

# Copyright (C) 2024 Denis Medvedev <nbr@altlinux.org>
# Copyright (C) 2025 Paul Wolneykien <manowar@altlinux.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

# See https://www.gnu.org/software/coreutils/faq/coreutils-faq.html#Sort-does-not-sort-in-normal-order_0021.
export LC_ALL=C

PROG=${0##*/}
CMDLINE="$*"

VERSION='0.4.6'
SCRIPTDIR="$(realpath "/usr/libexec/checksumgen")"

NPROC="$(nproc)"

DEFAULT_INCLUDE='./lib/modules/*/**/*.ko
./lib/modules/*/**/*.ko.*
./lib/firmware/**/*'

usage()
{
    [ "$1" = 0 ] || exec >&2
    cat <<EOF
Usage: $PROG [ options ] [ REPODIR ARCH ]

Options:

  -b BASELIST, --baselist=BASELIST    calculate checksums only for
                                    packages with names listed in
                                  BASELIST file;

  -f VLIST, --pkglist=VLIST    calculate checksums only for
                             packages for which name-version is found
                           in VLIST (for instance, in a list-rpms.txt
                           file of mkimage-profiles);

  -l RPMLIST, --rpms=RPMLIST    calculate checksums for RPM files
                              by reading their paths from RPMLIST
                            file (incompatible with REPODIR and ARCH
                          params);

  -p PSUMS, --prev=PSUMS    calculate checksums for new RPMs only
                          with relation to previous checksum file
                          PSUMS;

  -d DELSED, --delsed=DELSED    output a sed script to delete the
                              checksums of removed and replaced RPMs
                              to DELSED file;

  -u, --update            delete outdated checksums on the fly
                          and output a complete checksum file
                          (requires PSUMS);

  -c, --concat            append new packages and versions to the
                          given PSUMS file and output a complete
                          checksum file;

  -o SUMS, --out=SUMS     write output to SUMS;

  -a, --append            append the output to SUMS;

  -m MISSINGS, --missings=MISSINGS    write filenames of RPM files
                                    not found under REPODIR to the
                                  MISSINGS file (the default is
                              ./missings);

  --list-keys             list public key fingerprints for selected
                          packages (i. e.: BASELIST and/or including
                          PSUMS) and exit;

  -j NPROC, --nproc=NPROC    use up to NPROC parallel threads (the
                           default is $NPROC);

  -I PATTTERN, --include=PATTERN    include files with path matching
                                  the given shell PATTERN (this option
                                may be specified multiple times);

  --no-default            do not use the default include patterns;

  -n, --no-fakeroot       do not use fakeroot;

  -v, --verbose           be verbose;

  -V, --version           print program version and exit;

  -h, --help              show this text and exit.

Report bugs to https://bugzilla.altlinux.org/.
EOF
    exit "${1:-0}"
}

TEMP="$(getopt -n "$PROG" -o b:p:d:ucl:f:o:am:j:I:nvVh -l baselist:,prev:,delsed:,update,concat,rpms:,pkglist:,out:,append,missings:,list-keys,nproc,include:,no-default,no-fakeroot,verbose,version,help -- "$@")" || usage 1
eval set -- "$TEMP"

prev=
baselist=
delsed=
update=
concat=
rpmlist=
pkglist=
output=
append=
missings=./missings
list_keys=
verbose=
basedir=
arch=
no_fakeroot=
include=
NO_DEFAULT=
while :; do
    case "$1" in
	-p|--prev)
	    shift
	    prev="$1"
	    ;;
	-b|--baselist)
	    shift
	    baselist="$1"
	    ;;
	-d|--delsed)
	    shift
	    delsed="$1"
	    ;;
	-u|--update)
	    update=y
	    ;;
	-c|--concat)
	    concat=y
	    ;;
	-l|--rpms)
	    shift
	    rpmlist="$1"
	    ;;
	-f|--pkglist)
	    shift
	    pkglist="$1"
	    ;;
	-o|--out)
	    shift
	    output="$1"
	    ;;
	-a|--append)
	    append=y
	    ;;
	-m|--missings)
	    shift
	    missings="$1"
	    ;;
	--list-keys)
	    list_keys=y
	    ;;
	-j|--nproc)
	    shift
	    NPROC="$1"
	    ;;
	-I|--include)
	    shift
	    __inc=
	    case "$1" in
		./*)
		    __inc="$1"
		    ;;
		*)
		    __inc="./$__inc"
		    ;;
	    esac
	    include="$include${include:+
}$__inc"
	    ;;
	--no-default)
	    NO_DEFAULT=1
	    ;;
	-n|--no-fakeroot)
	    no_fakeroot=1
	    ;;
	-v|--verbose)
	    verbose=y
	    ;;
        -h|--help)
	    usage 0
            ;;
	-V|--version)
	    cat <<EOF
$VERSION 2025
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
EOF
	    exit 0
	    ;;
        --)
	    shift
	    break
            ;;
        *)
	    echo "$PROG: unrecognized option: $1" >&2
	    usage 1
            ;;
    esac
    shift
done

if [ -n "$update" ] && [ -n "$concat" ]; then
    echo "ERROR: Incompatible options --update and --concat." >&2
    usage 1
fi

if [ -n "$baselist" ] && [ -n "$pkglist" ]; then
    echo "ERROR: Incompatible options --baselist and --pkglist." >&2
    usage 1
fi

if [ -n "$update" ] || [ -n "$concat" ] || [ -n "$delsed" ]; then
    if [ -z "$prev" ]; then
	echo "ERROR: --update, --concat and --delsed require --prev." >&2
	usage 1
    fi
fi

if [ -n "$rpmlist" ]; then
    if [ $# -ne 0 ]; then
	echo "ERROR: -l | --rpmlist is incompatible with REPODIR and ARCH." >&2
	usage 1
    fi
else
    [ $# -eq 2 ] || usage 1
    basedir="$1"
    arch="$2"
fi

INCLUDE=
[ -n "$NO_DEFAULT" ] || INCLUDE="$DEFAULT_INCLUDE"
[ -z "$include" ] || INCLUDE="$INCLUDE${INCLUDE:+
}$include"
export INCLUDE
export NO_DEFAULT

workdir=

#shellcheck disable=SC2329
#shellcheck disable=SC2317
cleanup()
{
    if [ -s "$workdir"/procs ]; then
	[ -z "$verbose" ] || \
	    echo "Waiting for unfinished threads..." >&2
	while read -r p; do
	    wait "$p" ||:
	done <"$workdir"/procs
    fi

    if [ "${DEBUG:-0}" -eq 0 ]; then
	[ -z "$workdir" ] || rm -rf "$workdir"
    else
	echo "DEBUG: Workdir: $workdir" >&2
    fi
}
trap 'cleanup' EXIT
workdir="$(mktemp -d --tmpdir "$PROG.XXXX")"

if [ "${DEBUG:-0}" -ne 0 ]; then
    if [ "${DEBUG:-0}" -gt 2 ]; then
	set -x
    fi
    export DEBUG
    echo "$PROG is running with the following set of options: $CMDLINE" >&2
fi

export WORKDIR="$workdir"
export VERBOSE="$verbose"
export NO_FAKEROOT="$no_fakeroot"

# shellcheck disable=SC3040
set -o pipefail ||:

[ -z "$verbose" ] || \
    echo "Preparing list of RPMs..." >&2

QF='%{name}-%{version}-%{release}.%{arch}.rpm/%{disttag}@%{buildtime}\n'

if [ -z "$rpmlist" ]; then
    if [ -z "$basedir" ]; then
	echo "ERROR: Specify the REPODIR or use the --rpmlist option." >&2
	exit 1
    fi
    if [ -z "$arch" ]; then
	echo "ERROR: Specify the ARCH or use the --rpmlist option." >&2
	exit 1
    fi
    (
	set +f
	xzcat "$basedir/$arch"/base/pkglist.*.xz | pkglist-query "%{name}-%{version}-%{release}.%{arch}.rpm\t$QF" -
    ) | \
	sort -u >"$workdir"/rpmlist

    if [ ! -s "$workdir"/rpmlist ]; then
	echo "WARNING: Got the empty RPM list for the arch $arch." >&2
    fi
else
    # (for stdin "-")
    #shellcheck disable=SC2002
    cat "$rpmlist" | while read -r f; do
	#shellcheck disable=SC3060
	rpm -q --qf="${f//%/%%}\t$QF" -p "$f"
    done | sort -u >"$workdir"/rpmlist
fi

if [ -n "$baselist$pkglist" ]; then
    [ -z "$verbose" ] || \
	echo "Preparing baselist filter..." >&2

    case "${arch:-}" in
	x86_64-i586)
	    parch='i586'
	    ;;
	*)
	    parch="${arch:-}"
	    ;;
    esac

    if [ -n "$pkglist" ]; then
	# produce a
	# \t<name>-<version>\.<arch>\.rpm/[^@/]\+@[0-9]\+$
	# regex pattern for each package:
	sed -e 's/\./\\./g' \
	    -e 's/^.*$/\t&\/[^@\/]\\+@[0-9]\\+$/' \
	    <"$pkglist" \
	    >"$workdir"/baselist.grep
    else
	# produce a
	# ^\(.*/\)*<name>-<any-version>\.<arch|any-arch>\.rpm/[^@/]\+@[0-9]\+$
	# regex pattern for each package name:
	sed -e 's/\./\\./g' -e "s/^.*\$/\\t&-[^-]\\\\+-[^-]\\\\+\\\\.${parch:-[a-zA-Z0-9_-]}\\\\.rpm\\/[^@\\/]\\\\+@[0-9]\\\\+\$/" \
	    "$baselist" \
	    >"$workdir"/baselist.grep
    fi
fi

# In the case baselist is specified, filters out packages
# not listed in it.
basefilter() {
    if [ -s "$workdir"/baselist.grep ]; then
	grep -f "$workdir"/baselist.grep
    else
	cat
    fi
}

if [ -n "$prev" ]; then
    [ -z "$verbose" ] || \
	echo "Preparing previuos checksum filter..." >&2

    if [ ! -e "$prev" ]; then
	echo "ERROR: $prev doesn't exist!" >&2
	exit 1
    fi
    if [ -s "$prev" ]; then
	grep '^.*-[^-]\+-[^-]\+\.[^.]\+\.rpm/[^@/]\+@[0-9]\+$' "$prev" | \
	    sort -u >"$workdir"/prevlist
	if [ ! -s "$workdir"/prevlist ]; then
	    echo "ERROR: $prev doesn't look like a checksum file!" >&2
	    exit 1
	fi
	sed -e 's/^.*$/\t&/' \
	    "$workdir"/prevlist \
	    >"$workdir"/prevlist.grep
    else
	touch "$workdir"/prevlist
	touch "$workdir"/prevlist.grep
    fi
fi

# In the case previous checksum file is specified, filters out
# RPM filenames (i. e. exact versions) listed in it.
prevfilter() {
    if [ -s "$workdir"/prevlist.grep ]; then
	grep -v -F -f "$workdir"/prevlist.grep
    else
	cat
    fi
}

[ -z "$verbose" ] || \
    echo "Filtering RPMs..." >&2

basefilter <"$workdir"/rpmlist >"$workdir"/existing ||:

# Transforms RPM package filename into a regexp pattern
rpmtopat() {
    sed -n -e '
# for all RPM file names
/^.*-[^-]\+-[^-]\+\.[^.]\+\.rpm\/[^@\/]\+@[0-9]\+$/ {
  # prefix with ^, append $
  s//^&$/
  # replace all . with \.
  s/\./\\./g
  # print
  p
}
'
}

if [ -n "$output" ]; then
    if [ -z "$append" ]; then
	exec >"$output"
    else
	exec >>"$output"
    fi
fi

if [ -n "$update" ] || [ -n "$delsed" ]; then
    [ -z "$verbose" ] || \
	echo "Preparing 'delsed' script..." >&2
    sed -e 's/^.*\t//' "$workdir"/existing | \
    comm -23 "$workdir"/prevlist - | \
	rpmtopat | \
	sed -e 's|/|\\/|g' -e 's|^.*$|/&/,/^$/ d|' >"$workdir"/delsed
    if [ -n "$update" ] && [ -n "$prev" ]; then
	[ -z "$verbose" ] || \
	    echo "Updating $prev..." >&2
	sed -f "$workdir"/delsed "$prev"
    fi
    if [ -n "$delsed" ]; then
	cat "$workdir"/delsed >"$delsed"
    fi
fi

if [ -n "$concat" ] && [ -n "$prev" ]; then
    cat "$prev"
fi

[ -z "$verbose" ] || \
    echo "Processing RPMs..." >&2

locate_rpm() {
    case "$1" in
	/*)
	    echo "$1"
	    return 0
	    ;;
	*.noarch.rpm)
	    echo "$basedir/files/noarch/RPMS/$1"
	    return 0
	    ;;
	*.i586.rpm)
	    if [ "$arch" = 'x86_64' ]; then
		echo "$basedir/files/x86_64-i586/RPMS/$1"
		return 0
	    fi
	    ;;
    esac

    echo "$basedir/files/$arch/RPMS/$1"
}

if [ -n "$list_keys" ]; then
    prevfilter <"$workdir"/existing | sed -e 's/\t.*$//' | \
    while read -r rpm; do
	rpmfile="$(locate_rpm "$rpm")"
	if [ ! -e "$rpmfile" ]; then
	    echo "ERROR: RPM file $rpmfile not found!" >&2
	    exit 1
	fi
	signature="$(rpm -qip "$rpmfile" | grep '^Signature[[:space:]]*:' | sed -n -e 's/^.*[[:space:]]Key[[:space:]]\+ID[[:space:]]\+//p' | head -1)"
	if [ -n "$signature" ]; then
	    printf '%s\t%s\n' "$rpm" "$signature"
	fi
    done
    exit $?
fi

prevfilter <"$workdir"/existing | sed -e 's/\t.*$//' >"$workdir"/todo
l="$(wc -l <"$workdir"/todo)"

if [ "$l" -eq 0 ]; then
    [ -z "$verbose" ] || \
	echo "WARNING: The RPM list is empty! Check your filters (i. e., -p and -b options)." >&2
    exit 0
fi

if [ "${DEBUG:-0}" -ne 0 ]; then
    echo "-- todo list ($l rpms): --" >&2
    cat "$workdir"/todo >&2
    echo "-- END --" >&2
fi

[ "$l" -ge "${NPROC:-1}" ] || NPROC="$l"
(cd "$workdir" && split -a4 -x -n l/"${NPROC:-1}" todo todo_)

#shellcheck disable=SC2329
#shellcheck disable=SC2317
killprocs() {
    if [ -s "$workdir"/procs ]; then
	[ -z "$verbose" ] || \
	    echo "Terminating the threads..." >&2
	while read -r p; do
	    kill -TERM -"$p" 2>/dev/null ||:
	done <"$workdir"/procs
    fi
}
trap 'killprocs' INT QUIT TERM PIPE HUP

find "$workdir" -name 'todo_*' | sort >"$workdir"/todolist

# Enable job control (-m) to place each child process in a separate
# process group.
set -m
while read -r f; do
    [ -s "$f" ] || continue
    MISSINGS="$f".missing \
    "$SCRIPTDIR"/pkg_list_get_executables_checksums.sh \
        "$f" \
        "$basedir" \
        "$f".chksum \
        "${arch:-}" \
	&
    echo "$!" >>"$workdir"/procs
done <"$workdir"/todolist
set +m # disable job control

[ -z "$verbose" ] || \
    echo "Waiting for the threads..." >&2

ret=0
while read -r p; do
    wait "$p" || ret=$?
done <"$workdir"/procs
rm -f "$workdir"/procs

if [ "$ret" -ne 0 ]; then
    echo "WARNING! Some jobs have finished with error!" >&2
fi

while read -r f; do
    [ ! -e "$f".chksum ] || cat "$f".chksum
done <"$workdir"/todolist

while read -r f; do
    [ ! -s "$f".missing ] || cat "$f".missing >>"$missings"
done <"$workdir"/todolist

exit "$ret"
