#!/bin/bash
#
###########################################################################
#                                                                         #
# update-repository  -  v1.5.2  -  Debian Repository Generator            #
#                                                                         #
# This file is part of the afulinux.de Debian repository.                 #
#                                                                         #
# (C) 2003-2023 Andreas Stempfhuber https://www.afulinux.de/e-mail        #
#                                                                         #
# 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 3 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, see <http://www.gnu.org/licenses/>.   #
#                                                                         #
###########################################################################

CONF="conf/update-repository.conf"

# Keep if file has changed
# keep_if_changed FILE
keep_if_changed () {
	if ! cmp -s $1.new $1; then
		mv $1.new $1
		echo "${1##*/} for $DIST updated."

		return 0
	else
		rm $1.new
	fi

	return 1
}

# Get configuration from an APT config tree (since apt-config can't handle trees themselves)
# apt_config_tree CONFIGFILE DIST OPTIONS...
apt_config_tree () {
	local CONF=$1
	local DIST=$2
	shift 2
	local OPTIONS=$@
	local RESULT

	eval $(sed -n "/^Tree[[:space:]]*\"$DIST\"[[:space:]]*{/,/^};/p" "$CONF" | apt-config -c /dev/stdin shell RESULT "$OPTIONS")
	echo "$RESULT"
}

# Escape html characters
# echo text | escape_html > text.html
escape_html () {
	sed -e 's/&/\&amp;/' -e 's/</\&lt;/' -e 's/>/\&gt;/' -e 's/"/\&quot;/' -e "s/'/\&apos;/"
}


# Change working directory if configuration file is not relative to current directory
[[ -e $CONF ]] || cd "${0%/*}"

# Check if configuration file exists
if [[ ! -e $CONF ]]; then
	echo "$CONF: file not found"
	exit 1
fi

# Get start time
EPOCH=$(date +%s)

# Note: Using sed, as apt-config cannot read multiple Tree sections
DISTS=$(sed -ne 's/^Tree[[:space:]]*"\([^"]\+\)"[[:space:]]*{.*/\1/p' "$CONF")

# Get archive signing key
eval $(apt-config -c "$CONF" shell GPGKEY UpdateRepository::Release::GpgKey)

# Get configuration options for readme and sources.list files
eval $(apt-config -c "$CONF" shell SOURCES_URL UpdateRepository::Sources::URL)
eval $(apt-config -c "$CONF" shell URL UpdateRepository::Repository::URL)
eval $(apt-config -c "$CONF" shell MAINTAINER UpdateRepository::Maintainer)
eval $(apt-config -c "$CONF" shell ARCHIVE_URL UpdateRepository::Archive::URL)
eval $(apt-config -c "$CONF" shell IMPRESSUM_URL UpdateRepository::Impressum::URL)
eval $(apt-config -c "$CONF" shell EMAIL_URL UpdateRepository::E-mail::URL)
eval $(apt-config -c "$CONF" shell ORIGIN APT::FTPArchive::Release::Origin)
eval $(apt-config -c "$CONF" shell LABEL APT::FTPArchive::Release::Label)
eval $(apt-config -c "$CONF" shell DESCRIPTION APT::FTPArchive::Release::Description)

# Get configuration options for pin priority
eval $(apt-config -c "$CONF" shell NOT_AUTO APT::FTPArchive::Release::NotAutomatic)
eval $(apt-config -c "$CONF" shell BUT_AUTO_UPGRADES APT::FTPArchive::Release::ButAutomaticUpgrades)

for DIST in $DISTS; do
	CHANGED=0	# Nothing was changed yet
	SECTIONS=$(apt_config_tree "$CONF" "$DIST" Tree::Sections)
	ARCHS=$(apt_config_tree "$CONF" "$DIST" Tree::Architectures)
	SIGNED_BY=$(apt_config_tree "$CONF" "$DIST" Tree::UpdateRepository::Signed-By)

	# Create directories
	for SECTION in $SECTIONS; do
		for ARCH in $ARCHS; do
			if [[ $ARCH != source ]]; then
				mkdir -p "dists/$DIST/$SECTION/binary-$ARCH"
			else
				mkdir -p "dists/$DIST/$SECTION/$ARCH"
			fi
		done

		mkdir -p "pool/$DIST-$SECTION"
	done


	# Export public archive signing key
	KEYRING=${DIST##*-}-archive-keyring
	if gpg --export "$GPGKEY" > dists/$DIST/$KEYRING.gpg.new; then
		keep_if_changed dists/$DIST/$KEYRING.gpg && CHANGED=1
	fi

	# Get one package example based on SECTIONS order and with KEYRING package excluded
	EXAMPLE=$(find $(eval echo pool/$DIST-{${SECTIONS// /,}}/*) -type d -not -name "$KEYRING" -printf "%f\n" -quit 2>/dev/null)

	# Write sources.list file
	if [[ $SIGNED_BY = false ]]; then
		unset SIGNED_BY_OPT
	else
		SIGNED_BY_OPT="[signed-by=/usr/share/keyrings/$KEYRING.gpg] "
	fi

	# Write sources.list
	(
		echo "# $LABEL"
		echo "deb $SIGNED_BY_OPT$SOURCES_URL/ $DIST $SECTIONS"
		echo "deb-src $SIGNED_BY_OPT$SOURCES_URL/ $DIST $SECTIONS"
	) > dists/$DIST/${DIST##*-}.list.new
	keep_if_changed dists/$DIST/${DIST##*-}.list && CHANGED=1

	# Get main (first) section
	SECTION=${SECTIONS%% *}

	# Check if anything was changed and update keyring.deb
	# (can be overwritten with --force command line option)
	if [[ $CHANGED -eq 1 || ! -d pool/$DIST-$SECTION/$KEYRING || $1 == --force ]];then
		TDIR=$(mktemp -t -d "${0##*/}.XXXXXXXXXX") || exit 1
		VERSION=$(date +%Y.%m.%d.%-H%M%S)

		mkdir -p "$TDIR/$KEYRING-$VERSION"/{etc/apt/sources.list.d,etc/apt/preferences.d,usr/share/doc/$KEYRING,DEBIAN}
		cp dists/$DIST/${DIST##*-}.list "$TDIR/$KEYRING-$VERSION/etc/apt/sources.list.d/"
		echo "/etc/apt/preferences.d/${DIST##*-}.pref" >> "$TDIR/$KEYRING-$VERSION/DEBIAN/conffiles"
		echo "/etc/apt/sources.list.d/${DIST##*-}.list" >> "$TDIR/$KEYRING-$VERSION/DEBIAN/conffiles"

		if [[ $SIGNED_BY = false ]]; then
			# Jessie and older with no signed-by support
			mkdir -p "$TDIR/$KEYRING-$VERSION/etc/apt/trusted.gpg.d/"
			cp dists/$DIST/$KEYRING.gpg "$TDIR/$KEYRING-$VERSION/etc/apt/trusted.gpg.d/"
			echo "/etc/apt/trusted.gpg.d/$KEYRING.gpg" >> "$TDIR/$KEYRING-$VERSION/DEBIAN/conffiles"
		else
			# New distributions with signed-by support
			mkdir -p "$TDIR/$KEYRING-$VERSION/usr/share/keyrings/"
			cp dists/$DIST/$KEYRING.gpg "$TDIR/$KEYRING-$VERSION/usr/share/keyrings/"
		fi

		# Get domain
		PIN_ORIGIN=${SOURCES_URL#*://}
		PIN_ORIGIN=${PIN_ORIGIN%%/*}
		PIN_PRIORITY=500

		if [[ $NOT_AUTO = yes ]]; then
			PIN_PRIORITY=1

			if [[ $BUT_AUTO_UPGRADES = yes ]]; then
				PIN_PRIORITY=100
			fi
		fi

		# Write preferences file
		# (this is a precaution if the repository configuration gets manipulated)
		(
			echo "# $LABEL"
			echo "Package: *"
			echo "Pin: origin $PIN_ORIGIN"
			echo "Pin-Priority: $PIN_PRIORITY"
		) > "$TDIR/$KEYRING-$VERSION/etc/apt/preferences.d/${DIST##*-}.pref"

		# Write changelog.gz file
		(
			echo "$KEYRING ($VERSION) ${DIST%-*}; urgency=important"
			echo
			echo "  * $LABEL keyring and configuration."
			echo
			echo " -- $MAINTAINER  $(date -R)"
		) > "$TDIR/$KEYRING-$VERSION/usr/share/doc/$KEYRING/changelog"
		gzip "$TDIR/$KEYRING-$VERSION/usr/share/doc/$KEYRING/changelog"

		# Write copyright file
		(
			echo "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/"
			echo "Upstream-Name: $KEYRING"
			echo "Source: $URL"
			echo
			echo "Files: *"
			echo "Copyright: 2003-$(date +%Y) $MAINTAINER"
			echo "License: GPL-3.0+"
			echo
			echo "License: GPL-3.0+"
			echo " This program is free software: you can redistribute it and/or modify"
			echo " it under the terms of the GNU General Public License as published by"
			echo " the Free Software Foundation, either version 3 of the License, or"
			echo " (at your option) any later version."
			echo " ."
			echo " This package is distributed in the hope that it will be useful,"
			echo " but WITHOUT ANY WARRANTY; without even the implied warranty of"
			echo " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the"
			echo " GNU General Public License for more details."
			echo " ."
			echo " You should have received a copy of the GNU General Public License"
			echo " along with this program. If not, see <https://www.gnu.org/licenses/>."
			echo " ."
			echo " On Debian systems, the complete text of the GNU General"
			echo " Public License version 3 can be found in \"/usr/share/common-licenses/GPL-3\"."
		) > "$TDIR/$KEYRING-$VERSION/usr/share/doc/$KEYRING/copyright"

		# Write control file
		(
			echo "Package: $KEYRING"
			echo "Version: $VERSION"
			echo "Architecture: all"
			echo "Maintainer: $MAINTAINER"
			echo "Installed-Size: $(du --exclude=DEBIAN -s "$TDIR/$KEYRING-$VERSION" | cut -f 1)"
			echo "Section: misc"
			echo "Priority: important"
			echo "Multi-Arch: foreign"
			echo "Homepage: $URL"
			echo "Description: GnuPG archive key and configuration of the $LABEL"
			echo " The $LABEL digitally signs its Release files."
			echo " This package contains the archive key used for that and takes care of the APT"
			echo " sources and preferences configuration."
		) > "$TDIR/$KEYRING-$VERSION/DEBIAN/control"

		# Write md5sums file and build deb package
		(
			cd "$TDIR/$KEYRING-$VERSION/"
			find * -type f -not -path "*DEBIAN*" -exec md5sum '{}' \; > DEBIAN/md5sums

			cd ..
			echo "pool/$DIST-$SECTION/$KEYRING:"
			dpkg-deb --root-owner-group --build $KEYRING-$VERSION ${KEYRING}_${VERSION}_all.deb
		)

		# Move deb package to its place
		mkdir -p pool/$DIST-$SECTION/$KEYRING
		rm -f pool/$DIST-$SECTION/$KEYRING/${KEYRING}_*_all.deb
		mv "$TDIR/${KEYRING}_${VERSION}_all.deb" pool/$DIST-$SECTION/$KEYRING/

		# Set symlink for easier access and shorter links
		rm -f dists/$DIST/${KEYRING}_*_all.deb
		ln -s ../../pool/$DIST-$SECTION/$KEYRING/${KEYRING}_${VERSION}_all.deb dists/$DIST/${KEYRING}_${VERSION}_all.deb

		# Clear temporary directory
		rm -rf "$TDIR"

		# Write description file
		rm -f pool/$DIST-$SECTION/$KEYRING/${KEYRING}_*.dsc-repo
		(
			echo "# This is not a Debian .dsc file!"
			echo "Binary: $KEYRING"
			echo "Version: $VERSION"
		) > pool/$DIST-$SECTION/$KEYRING/${KEYRING}_${VERSION}.dsc-repo
	fi

	# Create readme.html based on readme-release.conf
	if [[ -r conf/readme.conf ]]; then
		KEYRING_DEB=$(ls -1 dists/$DIST/${KEYRING}_*_all.deb | head -n 1)
		. conf/readme-release.conf > dists/$DIST/readme.html.new
		keep_if_changed dists/$DIST/readme.html
		rm -f dists/$DIST/README dists/$DIST/README.html
	fi
done

# Create Packages, Sources and Contents files
apt-ftparchive generate "$CONF" 2> >(egrep -v " (has no (source|binary) override entry( either|)|hat keinen Eintrag in der (Source|Binary)-Override-Liste.)$" 1>&2)

# Clean archive databases
apt-ftparchive clean "$CONF"
echo "Database cleaned."

for DIST in $DISTS; do
	# Calculate execution speed in minutes
	MIN=$((($(date +%s)-$EPOCH)/60+1))

	# Check if anything was changed (avoid changing Release file if nothing was changed)
	# (can be overwritten with the --force command line option)
	if find dists/$DIST -type f -mmin -$MIN | fgrep -q '' || [[ $1 == --force ]]; then
		SECTIONS=$(apt_config_tree "$CONF" "$DIST" Tree::Sections)
		ARCHS=$(apt_config_tree "$CONF" "$DIST" Tree::Architectures)

		# Create Release file
		apt-ftparchive -c "$CONF" \
			-o APT::FTPArchive::Release::Suite="$DIST" \
			-o APT::FTPArchive::Release::Codename="$DIST" \
			-o APT::FTPArchive::Release::Architectures="$ARCHS" \
			-o APT::FTPArchive::Release::Components="$SECTIONS" \
			release dists/$DIST > dists/$DIST/Release

		# Workaround for Debian Jessie where apt-ftparchive does not support ButAutomaticUpgrades
		if [[ $BUT_AUTO_UPGRADES ]] && ! grep -q "^ButAutomaticUpgrades:" dists/$DIST/Release; then
			sed -i -e "s/^\(Components:.*\)/\1\nButAutomaticUpgrades: $BUT_AUTO_UPGRADES/" dists/$DIST/Release
		fi

		# Workaround for Debian Jessie where apt-ftparchive does not support NotAutomatic
		if [[ $NOT_AUTO ]] && ! grep -q "^NotAutomatic:" dists/$DIST/Release; then
			sed -i -e "s/^\(Components:.*\)/\1\nNotAutomatic: $NOT_AUTO/" dists/$DIST/Release
		fi
	
		# Export public archive signing key
		KEYRING=${DIST##*-}-archive-keyring
		if gpg --armor --export "$GPGKEY" > dists/$DIST/$KEYRING.asc.new; then
			keep_if_changed dists/$DIST/$KEYRING.asc

			# Sign Release file
			# ( -digest-algo SHA256 required for Debian 9 Stretch and newer)
			rm -f "dists/$DIST/Release.gpg" "dists/$DIST/InRelease"
			gpg --digest-algo SHA256 -abs -u "$GPGKEY" -o "dists/$DIST/Release.gpg" "dists/$DIST/Release"
			gpg --digest-algo SHA256  --clearsign -u "$GPGKEY" -o "dists/$DIST/InRelease" "dists/$DIST/Release"
		fi

		# Foreign secton handling
		FOREIGNS=$(apt_config_tree "$CONF" "$DIST" Tree::UpdateRepository::Sections::Foreign)

		for FOREIGN in $FOREIGNS; do
			for DIR in $(ls -1 pool/$DIST-$FOREIGN 2>/dev/null); do
				DIR=pool/$DIST-$FOREIGN/$DIR
				if [[ ! -e $DIR/*.dsc ]]; then
					unset BINARIES
					for DEB in $DIR/*.deb; do
						if [[ -z $BINARIES ]]; then
							BINARIES=$(dpkg-deb -f $DEB Package)
							VERSION=$(dpkg-deb -f $DEB Version)
						else
							BINARIES="$BINARIES, $(dpkgdeb -f $DEB Package)"

							if [[ $(dpkg-deb -f $DEB Version) != $VERSION ]]; then
								echo "ERROR in $DIR: multiple *.deb files with different version are unsupported!"
								echo "ERROR: All *.deb files in a directory must came from the same source version!"
								exit 1
							fi
						fi
					done

					# Write description file
					(
						echo "# This is not a Debian .dsc file!"
						echo "Binary: $BINARIES"
						echo "Version: $VERSION"
					) > ${DEB%*.deb}.dsc-repo.new
					keep_if_changed ${DEB%*.deb}.dsc-repo
				fi
			done
		done

		# Generate packages.html
		. conf/packages-header.conf > dists/$DIST/packages.html.new

		for DSC in $(find pool/$DIST-* -name "*.dsc" -o -name "*.dsc-repo" | sort -t / -k 3 ); do
			SECTION=$(echo "$DSC" | sed -ne "s|^pool/$DIST-\([^/]*\).*|\1|p")
			PACKAGES=$(sed -ne 's/^Binary: //p' $DSC)
			PACKAGE=${PACKAGES%%,*}		# Get first package name
			DEB=$(ls -1 "${DSC%/*}"/${PACKAGE}_*.deb | head -n 1)

			# Write packages.html
			(
				echo "<dt>"
				echo "  <span class='packages'>$PACKAGES</span>"
				echo "  <span class='version'>$(sed -ne 's/^Version: //p' $DSC)</span>"
			
				if [[ $SECTION != main ]]; then
					echo "  <span class='section'>$SECTION</span>"
				fi

				echo "</dt>"
				echo "<dl>"
				echo "  <span class='description'>$(dpkg-deb -f "$DEB" Description | head -n 1 | escape_html)</span>"
				echo "  <span class='long-description'>$(dpkg-deb -f "$DEB" Description | tail -n +2 | escape_html | sed -e 's/^ .$/<br>/' -e 's/^  \(.*\)/<pre>\1<\/pre>/')</span>"

				if ls -1 "${DSC%/*}"/*.changes >/dev/null 2>&1; then
					echo "  <span class='changes'>$(sed -ne 's/^   //p' "$(ls -1 "${DSC%/*}"/*.changes | head -n 1)" | escape_html | sed -e 's/^\*/<li class="level1">/' -e 's/^ *-/<li class="level2">/')</li></span>"
				fi

				echo "  <span class='architectures'>$(ls -1 "${DSC%/*}"/*.deb | sed -ne 's/.*_\([^.\]*\).*/\1/p' | sort -u | tr '\n' ' ')</span>"
				echo "</dl>"
				echo
			) >> dists/$DIST/packages.html.new
		done

		. conf/packages-footer.conf >> dists/$DIST/packages.html.new
		keep_if_changed dists/$DIST/packages.html
	fi
done


# Create global readme.html based on readme.conf
if [[ -r conf/readme.conf ]]; then
	. conf/readme.conf > readme.html.new
	keep_if_changed readme.html

	rm -f README README.html
fi

