#!/bin/sh

# checkapk - find ABI breakages in package upgrades
# Copyright (c) 2012 Natanael Copa <natanael.copa@gmail.com>
#
# Distributed under GPL-2.0-only
#

program_version=3.14.1
sharedir=${ABUILD_SHAREDIR:-/usr/share/abuild}

if ! [ -f "$sharedir/functions.sh" ]; then
	echo "$sharedir/functions.sh: not found" >&2
	exit 1
fi
. "$sharedir/functions.sh"


usage() {
	cat <<-__EOF__
		$program $program_version - find ABI breakages in package upgrades
		Usage: $program [-h|--help]

		Run in the directory of a built package.

		Options:
		  -h, --help  Print this help

	__EOF__
}

args=$(getopt -o h --long help \
	-n "$program" -- "$@")

if [ $? -ne 0 ]; then
	usage >&2
	exit 2
fi
eval set -- "$args"
while true; do
	case $1 in
		-h|--help) usage; exit 0;;
		--) shift; break;;
		*) exit 1;; # getopt error
	esac
	shift
done

if [ $# -gt 0 ]; then
	usage >&2
	exit 2
fi

if ! [ -f "$ABUILD_CONF" ] && ! [ -f "$ABUILD_USERCONF" ] && ! [ -f "$ABUILD_DEFCONF" ]; then
	die "no abuild.conf found"
fi

if ! [ -f APKBUILD ]; then
	die 'must be run in the directory of a built package'
fi

if ! [ -n "$CARCH" ]; then
	die "failed to detect CARCH"
fi

. ./APKBUILD

startdir="$PWD"
tmpdir=$(mktemp -d -t checkpkg-script.XXXXXX)
trap "rm -rf '$tmpdir'; exit" INT EXIT
cd "$tmpdir" || die "failed to create temp dir"

# storage for downloaded/copied apks
mkdir -p apks

# default to pigz for unpacking
gunzip="$(command -v pigz || echo gzip) -d"

for i in $pkgname $subpackages; do
	_pkgname=${i%%:*}
	pkg=${_pkgname}-$pkgver-r$pkgrel
	pkgfile=${pkg}.apk
	repodir=${startdir%/*}
	repo=${repodir##*/}

	for filepath in "$PKGDEST"/$pkgfile "$REPODEST"/$repo/$CARCH/$pkgfile "$startdir"/$pkgfile; do
		if [ -f "$filepath" ]; then
			break
		fi
	done
	[ -f "$filepath" ] || die "can't find $pkgfile"

	# generate a temp repositories file with only the http(s) repos
	grep -E "^https?:" /etc/apk/repositories > "$tmpdir"/repositories

	oldpkg=$(apk fetch --repositories-file "$tmpdir"/repositories --simulate $_pkgname 2>&1 | sed 's/^Downloading //')
	if [ "${oldpkg}" = "${pkg}" ]; then
		die "the built package ($_pkgname) is already in the repo"
	fi

	# apk info could return multiple lines if multiple packages share a provide
	# (e.g. dnsmasq); filter with the exact package name and take the second line:
	#  7zip-23.01-r0 installed size:
	#  1668 KiB
	newsize="$(apk info --repositories-file /dev/null --repository "$REPODEST"/$repo --size $_pkgname | \
		grep -F "$pkg" -A1 | \
		tail -n1)"
	oldsize="$(apk info --repositories-file "$tmpdir"/repositories --size "$_pkgname" | \
		grep -F "$oldpkg" -A1 | \
		tail -n1)"

	if [ "$oldsize" = "$newsize" ]; then
		msg "No size differences for $_pkgname."
	else
		msg "Size difference for $_pkgname: $oldsize -> $newsize"
	fi

	apk fetch --quiet --repositories-file "$tmpdir"/repositories --stdout "$_pkgname" > apks/old.apk \
		|| msg2 "Old apk for $_pkgname missing. (new package/arch? broken internet?)"

	# pre-uncompress to not decompress twice
	# we do a decompression + tar -t for the file list, but then later we might do a full extraction for sodiff.
	# to not decompress here and then later again, store the intermediate tar
	$gunzip -c 2>/dev/null < apks/old.apk > apks/old.tar &
	$gunzip -c "$filepath" < "$filepath" > apks/new.tar &
	wait
	tar -t -f apks/old.tar 2>/dev/null | grep -v '^\.SIGN\.' | sort > "filelist-$_pkgname-old" &
	tar -t -f apks/new.tar | grep -v '^\.SIGN\.' | sort > "filelist-$_pkgname-new" &
	wait

	diff -U3 "filelist-$_pkgname-old" "filelist-$_pkgname-new"

	if diff -U0 "filelist-$_pkgname-old" "filelist-$_pkgname-new" | grep -q '\.so'; then
		echo "SODIFF:"

		mkdir -p "$_pkgname-pkg-old" "$_pkgname-pkg-new"
		tar -C "$_pkgname-pkg-old" 2>/dev/null -x -f apks/old.tar > /dev/null &
		tar -C "$_pkgname-pkg-new" -x -f apks/new.tar > /dev/null &
		wait

		# filter to things that start with -+ but strip the header (---/+++)
		diff -U0 "filelist-$_pkgname-old" "filelist-$_pkgname-new" | grep -E '^(\+|-)[A-Za-z0-9]+' | grep '\.so' | while read -r diff_sofile; do
			case "$diff_sofile" in
			-*) path="$_pkgname-pkg-old"; sofile="${diff_sofile#\-}" ;;
			+*) path="$_pkgname-pkg-new"; sofile="${diff_sofile#\+}" ;;
			esac

			# skip symlinks (only adds duplicate output or is dangling), and things that aren't valid elfs
			# matching .so above matches anything with .so in the name, e.g. xyz.sourceforge
			if ! [ -L "$path"/"$sofile" ] && readelf -h "$path"/"$sofile" >/dev/null 2>&1; then
				echo "$diff_sofile: " "$(objdump -p "$path"/"$sofile" | grep SONAME)"
			fi
		done
	else
		msg "No soname differences for $_pkgname."
	fi
done
