#!/bin/busybox ash
# shellcheck shell=ash
# alint APKBUILD - scan APKBUILD template for common mistakes
#
# Adapted from xlint from Void Linux's xtools to Alpine Linux
# https://github.com/leahneukirchen/xtools/
#
# Required packages (names are Alpine Linux pkgs):
# busybox - for grep, sed, tr, sort and other simple utiltiies

export LC_ALL=C

scan() {
	# shellcheck disable=2039
	# 2039: local operator is not posix but we can use it in ash/dash
	local rx="$1" msg="$2" tag="$3" severity="$4"
	grep -E -Hn -q -e "$rx" "$apkbuild" || return 1
	grep -E -Hn -e "$rx" "$apkbuild" |
		sed "s~^\([^:]*:[^:]*:\)\(.*\)~$severity:[$tag]:\1$msg~"
}

variables=$(echo -n "#.*
_.*
startdir
srcdir
pkgdir
subpkgdir
builddir
arch
depends
depends_dev
depends_doc
depends_openrc
depends_libs
depends_static
checkdepends
giturl
install
.*.pre-install
.*.post-install
.*.pre-upgrade
.*.post-upgrade
.*.pre-deinstall
.*.post-deinstall
install_if
license
makedepends
makedepends_build
makedepends_host
md5sums
sha256sums
sha512sums
options
pkgdesc
pkggroups
pkgname
pkgrel
pkgusers
pkgver
provides
provider_priority
replaces
replaces_priority
source
subpackages
triggers
ldpath
linguas
sonameprefix
somask
url
langdir
patch_args
pcprefix
HOSTCC" | tr '\n' '|')

valid_options="
!archcheck
!check
checkroot
net
!strip
suid
!tracedeps
chmod-clean
!dbg
toolchain
!fhs
libtool
charset.alias
textrels
!spdx
ldpath-recursive
sover-namecheck
lib64
setcap
$(echo "$CUSTOM_VALID_OPTIONS" | tr ' ' '\n')
"

# The variables should follow the order in the arch_to_hostspec()
# function in /usr/share/abuild/functions.sh
# We also add a version with '!' to match negation of arches
valid_arches="
aarch64
!aarch64
armel
!armel
armhf
!armhf
armv7
!armv7
mips
!mips
mips64
!mips64
mipsel
!mipsel
mips64el
!mips64el
ppc
!ppc
ppc64
!ppc64
ppc64le
!ppc64le
riscv32
!riscv32
riscv64
!riscv64
s390x
!s390x
x86
!x86
x86_64
!x86_64
all
noarch
$(echo "$CUSTOM_VALID_ARCHES" | tr ' ' '\n')
"

# This is a list of packages that need builddir= set because scripts/bootstrap.sh
# might want to build -bootstrap versionss
default_builddir_exception="
fortify-headers
linux-headers
musl
libc-dev
pkgconf
zlib
openssl
ca-certificates
libbsd
libtls-standalone
busybox
busybox-initscripts
binutils
make
apk-tools
file
gmp
mpfr4
mpc1
isl
cloog
gcc
openrc
alpine-conf
alpine-baselayout
alpine-keys
alpine-base
build-base
attr
libcap
patch
sudo
acl
fakeroot
tar
pax-utils
lzip
abuild
ncurses
libedit
openssh
libcap-ng
util-linux
libaio
lvm2
popt
xz
json-c
argon2
cryptsetup
kmod
lddtree
mkinitfs
community/go
libffi
community/ghc
linux-lts
linux-firmware
$(echo "$CUSTOM_BOOTSTRAP_PACKAGES" | tr ' ' '\n')
"

# This takes the format of $pkgname@$regex...
# you can have unlimited regexes and each one is checked
# they are checked with 'grep -E'
bad_versions="
wireshark@[0-9]\.[13579]*\..*[0-9]
nginx@[0-9]+\..*[13579]\..*[0-9]
mesa@.*\.0$
$(echo "$CUSTOM_BAD_VERSIONS" | tr ' ' '\n')
"

# One pkgname per-line, these are exempt from pkgname-has-uppercase
uppercase_exceptions="
R
$(echo "$CUSTOM_UPPERCASE_PKGNAMES" | tr ' ' '\n')
"

default_builddir_value() {
	[ "$SKIP_DEFAULT_BUILDDIR_VALUE" ] && return 0
	[ "$SKIP_AL1" ] && return 0

	# If the package is in our exceptions don't tell the user
	if printf "%s" "$default_builddir_exception" | grep -q -x -- "$pkgname"; then
		return 0
	fi

	if [ "$builddir" = "/$pkgname-$pkgver" ] || [ "$_builddir" = "/$pkgname-$pkgver" ]; then
		scan '^(_)?builddir=' "builddir can be removed as it is the default value" 'AL1' 'MC'
	fi
}

unnecessary_return_1() {
	[ "$SKIP_UNNECESSARY_RETURN_1" ] && return 0
	[ "$SKIP_AL2" ] && return 0
	scan '\|\| return 1' "|| return 1 is not required as set -e is used" 'AL2' 'MC'
	scan '\|\| exit 1' "|| exit 1 is not required as set -e is used" 'AL2' 'MC'
}

pkgname_quoted() {
	[ "$SKIP_PKGNAME_QUOTED" ] && return 0
	[ "$SKIP_AL3" ] && return 0
	scan "^pkgname=[\"'][^$]+[\"']" "pkgname must not be quoted" 'AL3' 'TP'
}

pkgver_quoted() {
	[ "$SKIP_PKGVER_QUOTED" ] && return 0
	[ "$SKIP_AL4" ] && return 0
	scan "^pkgver=[\"'][^$]+[\"']" "pkgver must not be quoted" 'AL4' 'TP'
}

empty_variable() {
	[ "$SKIP_EMPTY_VARIABLE" ] && return 0
	[ "$SKIP_AL5" ] && return 0
	scan '^[A-Za-z0-9_]*=(""|''|)$' "variable set to empty string: \2" 'AL5' 'MC'
}

custom_variable() {
	[ "$SKIP_CUSTOM_VARIABLE" ] && return 0
	[ "$SKIP_AL6" ] && return 0
	grep -E -oHn -- '^[\sA-Za-z0-9_]*=' "$apkbuild" | \
		sed "s|:[ 	]*|:|g" | \
		grep -E -v -- ':('"$variables"')=' | \
		sed "s/^\([^:]*:[^:]*:\)\(.*\)/IC:[AL6]:\1prefix custom variable with _: \2/"
}

indent_tabs() {
	[ "$SKIP_INDENT_TABS" ] && return 0
	[ "$SKIP_AL7" ] && return 0
	scan '^  ' "indent with tabs" 'AL7' 'IC'
}

trailing_whitespace() {
	[ "$SKIP_TRAILING_WHITESPACE" ] && return 0
	[ "$SKIP_AL8" ] && return 0
	scan '[	 ]$' "trailing whitespace" 'AL8' 'IC'
}

backticks_usage() {
	[ "$SKIP_BACKTICKS_USAGE" ] && return 0
	[ "$SKIP_AL25" ] && return 0
	scan '[^\\]`' "use \$() instead of backticks" 'AL25' 'SP'
}

function_keyword() {
	[ "$SKIP_FUNCTION_KEYWORD" ] && return 0
	[ "$SKIP_AL9" ] && return 0
	scan '^	*function\b' 'do not use the function keyword' 'AL9' 'SC'
}

space_before_function_parenthesis() {
	[ "$SKIP_SPACE_BEFORE_FUNCTION_PARENTHESIS" ] && return 0
	[ "$SKIP_AL10" ] && return 0
	scan '^	*[^ ]*  *\(\)' 'do not use space before function parenthesis' 'AL10' 'TC'
}

space_after_function_parenthesis() {
	[ "$SKIP_SPACE_AFTER_FUNCTION_PARENTHESIS" ] && return 0
	[ "$SKIP_AL11" ] && return 0
	scan '^	*[^ ]*\(\)(|   *)\{' 'use one space after function parenthesis' 'AL11' 'TC'
}

newline_opening_brace() {
	[ "$SKIP_NEWLINE_OPENING_BRACE" ] && return 0
	[ "$SKIP_AL12" ] && return 0
	scan '^	*[^ ]*\(\)$' 'do not use a newline before function opening brace' 'AL12' 'TC'
}

superfluous_cd_builddir() {
	[ "$SKIP_SUPERFLUOUS_CD_BUILDDIR" ] && return 0
	[ "$SKIP_AL13" ] && return 0
	# shellcheck disable=2039
	# 2039: local operator is not posix but we can use it in ash/dash
	local cds='' cdscount='' prevcd='' phase="$1"

	# All ocurrences of the 'cd' command being used
	# 1. Print file with line numbers.
	# 2. Print the function from the opening declaration up to the closing bracked
	# 3. grep for all ocurrences of the 'cd' command (ignore obviously invalid ones
	#	like matching 'cd' until the end of the line)
	cds="$(cat -n "$apkbuild" \
		   | sed -n "/^\s\+[0-9].*	$phase() {/,/[0-9].*	}/p" \
		   | grep '\bcd ')"

	# Number of ocurrences of the 'cd' command being used
	# Used to tell if we are in a phase() with a single cd statement
	# in that case we can be free to warn the user that their cd statement
	# is superfluous if it is to "$builddir", this avoids problems of previous
	# 'cd' statements to other places giving false positives
	cdscount="$(printf "%s\\n" "$cds" | wc -l)"

	# if the previous line had a 'cd "$builddir"' statement
	prevcd=0

	# If it is the first cd of the program
	firstcd=1

	# Use newline as our IFS delimiter, so we can iterate over lines with
	# the for construct, since the while loop will create a subshell that
	# prevents the value of the prevcd variable from being propagated
	# to future runs
	OLDIFS="$IFS"
	IFS="
"
	local line; for line in $(printf "%s\\n" "$cds"); do
		linenum="$(printf "%s\\n" "$line" | awk '{ print $1 }')"
		statement="$(printf "%s\\n" "$line" | awk '{ $1="" ; print $0 }')"
		[ -z "$statement" ] && continue
		if echo "$statement" | grep -E -q 'cd ["]?\$[{]?[_]?builddir["}]?+($| )' ; then
			if [ "$prevcd" -eq 1 ] || [ "$cdscount" -eq 1 ] || [ "$firstcd" -eq 1 ]; then
				printf "MP:[AL13]:%s:%s:cd \"\$builddir\" can be removed in phase '%s'\\n" \
					"$apkbuild" \
					"$linenum" \
					"$phase"
			fi
			prevcd=1
		else
			prevcd=0
		fi
		# Can be set to 0 in the first loop and the re-set it to 0 in any next loops
		firstcd=0
	done
	IFS="$OLDIFS"
}

pkgname_has_uppercase() {
	[ "$SKIP_PKGNAME_HAS_UPPERCASE" ] && return 0
	[ "$SKIP_AL14" ] && return 0

	# If the package is in our exceptions don't tell the user
	if printf "%s" "$uppercase_exceptions" | grep -q -x -- "$pkgname"; then
		return 0
	fi

	scan '^pkgname=[a-z0-9\._\-]*[A-Z]' 'pkgname must not have uppercase characters' 'AL14' 'SC'
}

pkgver_has_pkgrel() {
	[ "$SKIP_PKGVER_HAS_PKGREL" ] && return 0
	[ "$SKIP_AL15" ] && return 0
	scan '^pkgver=.*(-r|_r[^c])' 'pkgver must not have -r or _r' 'AL15' 'SC'
}

_builddir_is_set() {
	[ "$SKIP__BUILDDIR_IS_SET" ] && return 0
	[ "$SKIP_AL26" ] && return 0
	if [ -z "$builddir" ] && [ -n "$_builddir" ]; then
		scan '^_builddir=' 'rename _builddir to builddir' 'AL26' 'SP'
	fi
}

literal_integer_is_quoted() {
	[ "$SKIP_LITERAL_INTEGER_IS_QUOTED" ] && return 0
	[ "$SKIP_AL28" ] && return 0
	scan '^[A-Za-z0-9_]*=('\''|")[0-9]+("|'\'')' 'literal integers must not be quoted' 'AL28' 'MC'
}

pkgname_used_in_source() {
    [ "$SKIP_PKGNAME_USED_IN_SOURCE" ] && return 0
    [ "$SKIP_AL29" ] && return 0
	local i; for i in $distilled_source; do
		if printf "%s\\n" "$i" | grep -E -q '(.*?::)?[a-z]+://[^"]+\$\{?pkgname\}?'; then
			i="$(printf "%s\\n" "$i" | sed -e 's|\$|\\$|g' -e 's|{|\\\{|g' -e 's|}|\\\}|g')"
			# shellcheck disable=SC2016
			scan "$i" '$pkgname should not be used in the source url' 'AL29' 'MC'
		fi
	done
}

double_underscore_in_variable() {
	[ "$SKIP_DOUBLE_UNDERSCORE_IN_VARIABLE" ] && return 0
	[ "$SKIP_AL30" ] && return 0
	# Run this twice, once which will detect variables without the local keyword
	# which requires matching the = sign at the end. The second time will match
	# for variables declared with the local keyword which do not require the =
	# sign
	scan '(^|	)__[A-Za-z0-9_].*=' 'double underscore on variables are reserved' 'AL30' 'MC'
	scan '(^|	)local ([A-Za-z0-9_])?.*__[A-Za-z0-9_].*(=)?' 'double underscore on variables are reserved' 'AL30' 'MC'
}

variable_capitalized() {
	[ "$SKIP_VARIABLE_CAPITALIZED" ] && return 0
	[ "$SKIP_AL31" ] && return 0
	scan '^[a-z0-9_]*[A-Z].*=' 'variables must not have capital letters' 'AL31' 'MC'
    scan '(^|   )local [a-z0-9_ ]*[A-Z].*(=)?' 'variables must not have capital letters' 'AL31' 'MC'
}

braced_variable() {
	[ "$SKIP_BRACED_VARIABLE" ] && return 0
	[ "$SKIP_AL32" ] && return 0
	# Match a Sigil ($) then a brace and any valid value until the end brace and then
	# match end-of-line or a character that can't be in the name of a variable
	grep -Eo -Hn -e '\$\{[A-Za-z0-9_]+\}($|["\./\(\)= -])' "$apkbuild" |
		sed "s/^\([^:]*:[^:]*:\)\(.*\)/MP:[AL32]:\1unnecessary usage of braces: \2/" |
		sed 's|[^}]$||g' # This strips the last match in the grep
}

cpan_variable() {
	[ "$SKIP_CPAN_VARIABLE" ] && return 0
	[ "$SKIP_AL35" ] && return 0
	scan '^cpandepends=' 'merge the contents of cpandepends into depends and remove it' 'AL35' 'MC'
	scan '^cpanmakedepends=' 'merge the contents of cpanmakedepends into makedepends and remove it' 'AL35' 'MC'
	scan '^cpancheckdepends=' 'merge the contents of cpancheckdepends into checkdepends and remove it' 'AL35' 'MC'
}

overwrite_xflags() {
	[ "$SKIP_OVERWRITE_XFLAGS" ] && return 0
	[ "$SKIP_AL36" ] && return 0
	# shellcheck disable=SC2016 disable=SC2086
	# We need to match either the start of the line or a whitespace
	# otherwise we can end up matching variables wrongly, like a variable
	# can be called 'LUA_CFLAGS' and we end up matching it because we only check
	# 'CFLAGS'
    #
    # The second grep checks if there is a '$FLAG' and also if there is a '${FLAG...}'
    # this should deal with cases where we must absolutely get rid of a variable like
    # in mips64 and GOFLAGS where we must remove the default '-buildmode=pie'
    #
	# The sed call is to strip any leading whitespace that is created
	grep -E -oHn -- "(^| |	)$1=\".*\"" $apkbuild |
		sed "s|:[ 	]*$1|:$1|g" |
        grep -vE -- "(\\\$$1|\\\$\{$1.*\})" |
		sed "s/^\([^:]*:[^:]*:\)\(.*\)/SP:[AL36]:\1$1 should not be overwritten, add \$$1 to it/"
}

invalid_option() {
	[ "$SKIP_INVALID_OPTION" ] && return 0
	[ "$SKIP_AL49" ] && return 0
	local i; for i in $options; do
		if ! echo "$valid_options" | grep -q -x -- "$i"; then
			scan "options=.*$i" "invalid option '$i'" 'AL49' 'MC' || \
				# Deal with multiline options by reading the whole file
				# bad!
				cat -n "$apkbuild" | \
				sed -n '/^\s\+[0-9].*options="/,/[0-9].*"/p' | \
				while IFS= read -r line; do
					local linenum
					linenum="$(printf '%s' "$line" | cut -d "$(printf '\t')" -f1 | xargs)"
					printf '%s' "$line" | grep -q "$i" && \
						printf "MC:[AL49]:%s:%s:invalid option '%s'\\n" \
							"$apkbuild" \
							"$linenum" \
							"$i"
				done
		fi
	done
}

missing_default_prepare() {
	[ "$SKIP_MISSING_DEFAULT_PREPARE" ] && return 0
	[ "$SKIP_AL54" ] && return 0

	# Check if we have prepare() defined
	# shellcheck disable=SC2086
	grep -q "^prepare() {" $apkbuild || return 0

	# shellcheck disable=SC2086
	# The '( | )' part of the sed call is not by mistake, the first component is a whitespace
	# the second component is a literal tab character, created by doing 'CTRL + V, <tab>'
	if ! sed -n '/^prepare() {/,/^}/p' $apkbuild | grep -q -E "^( |	)*default_prepare"; then
		scan "^prepare\(\) \{" "prepare() is missing call to 'default_prepare'" 'AL54' 'SC'
	fi
}

invalid_arch() {
	[ "$SKIP_INVALID_ARCH" ] && return 0
	[ "$SKIP_AL57" ] && return 0
	local i; for i in $arch; do
	if ! echo "$valid_arches" | grep -q -x -- "$i"; then
			scan "arch=.*$i" "invalid arch '$i'" 'AL57' 'SC'
		fi
	done
}

bad_version() {
	[ "$SKIP_BAD_VERSION" ] && return 0
	[ "$SKIP_AL61" ] && return 0
	local line
	line="$(printf "%s\\n" "$bad_versions" | grep -o -- "^$pkgname@.*")"
	[ -z "$line" ] && return 0

	printf "%s\\n" "$line" | sed "s|^$pkgname@||" | tr '@' '\n' | while read -r regex; do
		if printf "%s\\n" "$pkgver" | grep -E -q -- "$regex"; then
			scan "^pkgver=" "Version '$pkgver' matches a bad version regex" 'AL61' 'SC'
		fi
	done
}

volatile_source() {
	[ "$SKIP_VOLATILE_SOURCE" ] && return 0
	[ "$SKIP_AL62" ] && return 0
	local i; for i in $distilled_source; do
		if printf "%s\\n" "$i" | grep -q -E '^http(s)?.*(gitlab|github)*\.(patch|diff)$'; then
			i="$(printf "%s\\n" "$i" | sed -e 's|\$|\\$|g' -e 's|{|\\\{|g' -e 's|}|\\\}|g')"
			scan "$i" "volatile source '$i'" 'AL62' 'SC'
		fi
	done
}

# Remove variable and it's surrounding double-quotes
# this allows for iterating over all strings inside
# a variable, first written for source= which can have
# multiple values and we need to check each of them but
# we can't source the variable because we need to check
# for bad usage of $pkgname and other variables.
_distill() {
	local variable="$1" apkbuild="$2"
	# Check if we are dealing with an unquoted variable
	if ! grep -q -o -- "^$variable=\"" "$apkbuild"; then
		grep -o -- "^$variable=.*" "$apkbuild" | sed -e "s|$variable=||"
	fi
	if grep -q -o -- "^$variable=\".*\"" "$apkbuild"; then
		grep -o -- "^$variable=\".*\"" "$apkbuild" \
			| sed -e "s|$variable=\"||" -e 's|"||g'
	else
		sed -n -e "/$variable=\"/,/\"/p" "$apkbuild" \
			| sed -e "s|$variable=\"||" -e 's|"||g' -e 's|^\s*||'
	fi
}

ret=0
for apkbuild; do
	if [ -f "$apkbuild" ]; then

    case "$apkbuild" in
        /*|./*);;
        *) apkbuild=./"$apkbuild"
    esac

	# Source apkbuild, we need some nice values
	srcdir="" . "$apkbuild" || {
		echo "Failed to source APKBUILD in '$apkbuild'" ;
		exit 1;
	}
	# Distill the source by grabbing everything inside it, removing
	# source="" and remove all whitespace, it should end with a list:
	# source1
	# source2
	# source3d
	distilled_source="$(_distill "source" "$apkbuild")"

	default_builddir_value &
	_builddir_is_set &

	pkgname_quoted &
	pkgver_quoted &
	unnecessary_return_1 &
	empty_variable &
	custom_variable &
	indent_tabs &
	trailing_whitespace &
	backticks_usage &
	function_keyword &
	pkgname_has_uppercase &
	pkgver_has_pkgrel &
	space_before_function_parenthesis &
	space_after_function_parenthesis &
	newline_opening_brace &
	literal_integer_is_quoted &
	pkgname_used_in_source &
	double_underscore_in_variable &
	variable_capitalized &
	braced_variable &
	cpan_variable &
	overwrite_xflags "CFLAGS" &
	overwrite_xflags "GOFLAGS" &
	overwrite_xflags "CPPFLAGS" &
	overwrite_xflags "CXXFLAGS" &
	overwrite_xflags "FFLAGS" &
	overwrite_xflags "LDFLAGS" &
	overwrite_xflags "GOFLAGS" &
	missing_default_prepare &
	bad_version &
	[ "$arch" ] && invalid_arch &
	[ "$options" ] && invalid_option &
	[ "$source" ] && volatile_source &

	for phase in prepare build check package; do
		superfluous_cd_builddir "$phase" &
	done
	wait
	else
	echo no such apkbuild "$apkbuild" 1>&2
	fi | sort -t: -V | grep . && ret=1
done
exit $ret
