#!/bin/sh
# scraps-repo - build a repository INDEX from a directory of .spz packages
#
# .spz is the Scraps Package Zip: a tar.xz archive.
#
#   scraps-repo gen  <dir> [arch]   write <dir>/<arch>/INDEX
#   scraps-repo add  <dir> <pkg>... file packages into the repo and reindex
#   scraps-repo show <dir>          summarise a repo
#   scraps-repo ports <dir>         write <dir>/ports.idx from the recipes
#
# The INDEX is a tab separated table, one package per line:
#   name  version  release  arch  dlsize  isize  sha256  deps  desc
# shellcheck shell=sh disable=SC2039

set -eu

for p in /usr/lib/scraps/libscraps.sh ./libscraps.sh "$(dirname "$0")/libscraps.sh"; do
	[ -f "$p" ] && { . "$p"; break; }
done
[ -n "${SCRAPS_VERSION:-}" ] || { echo "scraps-repo: cannot locate libscraps.sh" >&2; exit 1; }

extract_pkginfo() {
	if have bsdtar; then bsdtar -xOf "$1" .PKGINFO 2>/dev/null
	else tar -xOf "$1" ./.PKGINFO 2>/dev/null || tar -xOf "$1" .PKGINFO 2>/dev/null; fi
}

gen() {
	dir=${1:?usage: scraps-repo gen <dir> [arch]}
	arch=${2:-x86_64}
	d="$dir/$arch"
	[ -d "$d" ] || die "no such directory: $d"
	idx="$d/INDEX"
	tmp="$idx.new"

	msg "Indexing $d"
	{
		printf '# ScrapLinux repository index\n'
		printf '# format\t%s\n' "$SCRAPS_FORMAT"
		printf '# generated\t%s\n' "$(date '+%Y-%m-%d %H:%M:%S')"
		printf '# fields\tname version release arch dlsize isize sha256 deps desc\n'
	} >"$tmp"

	n=0
	# Fed in ascending version-release order (sort -V on the filename,
	# which embeds both) so that when two builds of the same package sit
	# in the directory at once, the later one processed - the actually
	# newer one - is what ends up in the index. Without this, a plain
	# directory-order pass indexed both as separate lines with the same
	# name, and idx_lookup()'s "first match wins" picked whichever
	# sorted first alphabetically - the older release, by accident,
	# often enough that a rebuilt fix silently never reached anyone
	# resolving that name.
	for f in $(printf '%s\n' "$d"/*.spz | sort -V); do
		[ -f "$f" ] || continue
		pi=$(extract_pkginfo "$f") || { warn "$(basename "$f"): no .PKGINFO, skipped"; continue; }
		echo "$pi" >"$tmp.pi"
		nm=$(meta "$tmp.pi" name)
		[ -n "$nm" ] || { warn "$(basename "$f"): unnamed, skipped"; continue; }
		ver=$(meta "$tmp.pi" version)
		rel=$(meta "$tmp.pi" release); : "${rel:=1}"
		ar=$(meta "$tmp.pi" arch);     : "${ar:=$arch}"
		isz=$(meta "$tmp.pi" isize);   : "${isz:=0}"
		dsc=$(meta "$tmp.pi" desc);    : "${dsc:=$nm}"
		dep=$(metaall "$tmp.pi" depend | tr '\n' ' ' | sed 's/ *$//')
		[ -n "$dep" ] || dep="-"
		dsz=$(wc -c <"$f" | tr -d ' ')
		sum=$(sha256 "$f")
		printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
			"$nm" "$ver" "$rel" "$ar" "$dsz" "$isz" "$sum" "$dep" "$dsc" >>"$tmp.raw"
		printf '  %s+%s %s-%s-%s\n' "$A_MINT" "$C_R" "$nm" "$ver" "$rel"
	done
	rm -f "$tmp.pi"
	n=0
	if [ -f "$tmp.raw" ]; then
		awk -F'\t' '{line[$1]=$0} END{for (k in line) print line[k]}' "$tmp.raw" >>"$tmp"
		n=$(cut -f1 "$tmp.raw" | sort -u | wc -l | tr -d ' ')
		rm -f "$tmp.raw"
	fi
	mv "$tmp" "$idx"

	# A detached checksum so clients can spot a truncated index. It says
	# nothing about who wrote it - anyone who can replace the index can
	# replace the checksum beside it - which is what the signature below is
	# for.
	sha256 "$idx" >"$idx.sha256"

	# Signed when a key is available. SCRAPS_SIGN_KEY is a signify secret key
	# and lives on the machine that publishes, never in a repository: the
	# package trees are public, and a signing key in one of them would be
	# worth less than no signature at all.
	if [ -n "${SCRAPS_SIGN_KEY:-}" ]; then
		if [ ! -f "$SCRAPS_SIGN_KEY" ]; then
			die "SCRAPS_SIGN_KEY is set but $SCRAPS_SIGN_KEY does not exist"
		elif ! have signify; then
			die "SCRAPS_SIGN_KEY is set but signify is not installed"
		fi
		rm -f "$idx.sig"
		signify -S -s "$SCRAPS_SIGN_KEY" -m "$idx" -x "$idx.sig" \
			|| die "signing $idx failed"
		ok "signed with $(basename "$SCRAPS_SIGN_KEY")"
	else
		rm -f "$idx.sig"
	fi
	ok "$n package(s) indexed -> $idx"
}

add() {
	dir=${1:?usage: scraps-repo add <dir> <pkg.spz>...}
	shift
	for f in "$@"; do
		[ -f "$f" ] || die "no such package: $f"
		pi=$(extract_pkginfo "$f") || die "$f is not a valid .spz"
		# /tmp/.scrapsrepo.$$ was a predictable name in a world-writable
		# directory, written by a tool that runs as root: anyone able to
		# create a file in /tmp could put a symlink there first and have this
		# truncate and overwrite whatever it pointed at. The arch is one line
		# of the .PKGINFO already in $pi - read it out here instead of round
		# tripping it through a file at all.
		a=$(printf '%s\n' "$pi" | sed -n 's/^[ 	]*arch[ 	]*=[ 	]*//p' | head -1)
		: "${a:=x86_64}"
		case "$a" in
		""|.|..|*/*) die "$f has an unusable arch field ('$a')" ;;
		esac
		mkdir -p "$dir/$a"
		cp -f "$f" "$dir/$a/"
		msg2 "filed $(basename "$f") into $dir/$a"
	done
	for a in "$dir"/*/; do
		[ -d "$a" ] && gen "$dir" "$(basename "$a")"
	done
}

show() {
	dir=${1:?usage: scraps-repo show <dir>}
	for a in "$dir"/*/; do
		[ -d "$a" ] || continue
		arch=$(basename "$a")
		idx="$a/INDEX"
		[ -f "$idx" ] || { warn "$arch has no INDEX"; continue; }
		c=$(grep -cv '^#' "$idx" || echo 0)
		sz=$(du -sh "$a" | cut -f1)
		printf '\n  %s%s%s  %s packages, %s\n' "$A_TEAL$C_B" "$arch" "$C_R" "$c" "$sz"
		grep -v '^#' "$idx" | awk -F'\t' -v c="$A_GREY" -v r="$C_R" \
			'{printf "    %-26s %s%s-%s%s\n", $1, c, $2, $3, r}'
	done
	printf '\n'
}

ports() {
	_po_d=${1:?usage: scraps-repo ports <dir>}
	[ -d "$_po_d" ] || die "no such directory: $_po_d"
	# one awk pass, values are plain literals so nothing gets executed
	awk '
	function emit() {
		if (f == "") return
		n = split(f, p, "/"); dir = p[n-1]; repo = p[n-2]
		if (v["name"] != dir) { print f ": name=" v["name"] " does not match its directory" > "/dev/stderr"; bad = 1 }
		if (v["name"] in seen) { print v["name"] " is defined twice" > "/dev/stderr"; bad = 1 }
		seen[v["name"]] = 1
		printf "%s\t%s\t%s\t%s\t%s\n", repo, v["name"], v["version"], v["release"], v["desc"]
		delete v
	}
	BEGIN { print "# generated from the recipes by scraps-repo ports, never edit"
	        print "# repo\tname\tversion\trelease\tdesc" }
	FNR == 1 { emit(); f = FILENAME }
	/^(name|version|release|desc)=/ {
		k = substr($0, 1, index($0, "=") - 1)
		if (!(k in v)) { x = substr($0, index($0, "=") + 1); gsub(/^"|"$/, "", x); v[k] = x }
	}
	END { emit(); exit bad }
	' "$_po_d"/*/*/recipe >"$_po_d/ports.idx.tmp" || { rm -f "$_po_d/ports.idx.tmp"; die "ports index not written"; }
	mv -f "$_po_d/ports.idx.tmp" "$_po_d/ports.idx"
	ok "$(grep -vc '^#' "$_po_d/ports.idx") recipe(s) indexed -> $_po_d/ports.idx"
}

case "${1:-}" in
gen|index)  shift; gen "$@" ;;
add|file)   shift; add "$@" ;;
show|list)  shift; show "$@" ;;
ports)      shift; ports "$@" ;;
*) cat <<EOF
scraps-repo $SCRAPS_VERSION - ScrapLinux repository indexer

  scraps-repo gen  <dir> [arch]      regenerate <dir>/<arch>/INDEX
  scraps-repo add  <dir> <pkg>...    file .spz packages in and reindex
  scraps-repo show <dir>             summarise a repository
  scraps-repo ports <dir>            write <dir>/ports.idx from the recipes
EOF
 ;;
esac
