#!/usr/bin/env bash
# -*- tab-width: 4 ; sh-basic-offset: 4 -*-
# pkgbuild-check-nonfree

# Copyright 2010 Haase Hernández
# Copyright 2010 Joseph Graham
# Copyright 2010 Joshua Ismael
# Copyright 2010 Nicolás Reynolds
# Copyright 2012-2013 Luke Shumaker
#
# This file is part of Parabola.
#
# Parabola 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.
#
# Parabola 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 Parabola.  If not, see <http://www.gnu.org/licenses/>.

# I appologize that this program got *huge*.
# It's not complicated, just long.

. $(librelib messages)
. $(librelib conf)
. $(librelib blacklist)

usage() {
	print "Usage: %s [OPTIONS] [PKGBUILD1 PKGBUILD2 ...]" "${0##*/}"
	print "Analyzes a PKGBUILD for freedom issues"
	echo
	prose 'If no PKGBUILD is specified, `./PKGBUILD` is implied.'
	echo
	print "Exit status (add them for combinations):"
	print "   0: Everything OK, no freedom issues"
	print "   1: Ran with error"
	print "Warning-level freedom issues:"
	print "   2: Uses unrecognized licenses, check them"
	print "   4: Uses GPL-incompatible licenses"
	print "Error-level freedom issues:"
	print "   8: Uses known unacceptable licenses"
	print "  16: Has nonfree dependencies"
	print "  32: Is a known nonfree package"
	echo
	print "Options:"
	flag '-c'            'Use the cached blacklist, do not try downloading'
	flag '-f'            'Allow running as root user'
	echo
	flag '-q'            'Be quiet'
	flag '-v'            'Be verbose'
	echo
	flag '-h'            'Show this message'
}
# Make sure these match pkgbuild-summarize-nonfree
declare -ri _E_OK=0
declare -ri _E_ERROR=1
declare -ri _E_LIC_UNKNOWN=2
declare -ri _E_LIC_NOGPL=4
declare -ri _E_LIC_NONFREE=8
declare -ri _E_DEP_NONFREE=16
declare -ri _E_PKG_NONFREE=32

main() {
	# Parse flags
	local cache=false
	local asroot=false
	local v=1
	while getopts 'cfqvh' arg; do
		case "$arg" in
			c) cache=true;;
			f) asroot=true;;
			q) v=0;;
			v) v=2;;
			h) usage; return $_E_OK;;
			*) usage >&2; return $_E_ERROR;;
		esac
	done
	shift $(($OPTIND - 1))
	if [[ $# -lt 1 ]]; then
		pkgbuilds=("`pwd`/PKGBUILD")
	else
		pkgbuilds=("$@")
	fi

	# Do a check to see if we are running as root
	if [[ -w / ]] && ! $asroot; then
		error "Run as normal user, or use the -f option to run as root."
		return 1
	fi

	# Adjust the verbosity
	if [[ $v == 0 ]]; then
		error() { :; }
		warning() { :; }
		plain() { :; }
		info() { :; }
	elif [[ $v == 1 ]]; then
		info() { :; }
	elif [[ $v == 2 ]]; then
		info() { plain "$@"; }
	fi

	# Update the blacklist
	$cache || blacklist-update || return $_E_ERROR

	# Do the work
	declare -i ret=0
	local pkgbuild
	for pkgbuild in "${pkgbuilds[@]}"; do
		pkgbuild_check "$pkgbuild" || ret=$(($ret|$?))
	done
	return $ret
}

# Helper functions #############################################################
# These should maybe be moved into lib/conf.sh

# Usage: var="$(pkgbuild_get_pkg_str ${pkgname} ${varname})"
# Gets a package-level string for a split-package
pkgbuild_get_pkg_str() {
	[[ $# == 2 ]] || panic 'malformed call to pkgbuild_get_pkg_str'
	local pkg=$1
	local var=$2

	local indirect=${!var}
	eval $(declare -f package_$pkg | sed -rn "s/^\s*${var}(\+?=)/indirect\1/p")
	printf '%s' "${indirect}"
}
# Usage: eval $(pkgbuild_get_pkg_ary ${pkgname} ${varname} [$variable_name_to_set])
# Gets a package-level array for a split-package
pkgbuild_get_pkg_ary() {
	[[ $# == 2 ]] || [[ $# == 3 ]] || panic 'malformed call to pkgbuild_get_pkg_ary'
	local pkg=$1
	local var=$2
	local out="${3:-$var}"

	local ary="${var}[@]"
	local indirect=("${!ary}")
	eval $(declare -f package_$pkg | sed -rn "s/^\s*${var}(\+?=)/indirect\1/p")
	declare -p indirect|sed "s/ indirect=/ ${out}=/"
}

# Checker functions ############################################################

# Usage: check_lic "${licence}"
# Check a license name to see if it is OK
check_lic() {
	[[ $# == 1 ]] || panic 'malformed call to check_license'
	local license=$1

	info 'Checking license: %s' "$license"

	if [[ -e "/usr/share/licenses/common/$license" ]]; then
		return $_E_OK
	else
		case "${license#custom:}" in
			WTFPL)
				# accept as common, I think it should be in the licenses package
				return $_E_OK;;
			BSD1|BSD2|BSD3|MIT|X11)
				# accept these as common; they can't be included in the
				# 'licenses' package because some text must be customized
				return $_E_OK;;
			BSD4)
				warning "The 4-clause BSD license is free but has practical problems."
				return $_E_LIC_NOGPL;;
			BSD)
				warning "License 'BSD' is ambiguous, use 'BSD{1..4}' to specify the number of clauses."
				return $_E_LIC_UNKNOWN;;
			JSON)
				error "License '%s' is a known non-free license." "$license"
				return $_E_LIC_NONFREE;;
			*)
				warning "License '%s' is not a common (recognized) license." "$license"
				return $_E_LIC_UNKNOWN;;
		esac
	fi
	panic 'code should never be reached'
}

# Usage: check_dep "${dependency}"
# Checks for ${dependency} in the blacklist
check_dep() {
	[[ $# == 1 ]] || panic 'malformed call to check_dep'
	local pkg=$1

	local line="$(blacklist-cat|blacklist-lookup "$pkg")"
	local rep="$(blacklist-get-rep <<<"$line")"
	if [[ -z $line ]]; then
		# not mentioned in blacklist; free
		info '%s: not blacklisted' "$pkg"
		return $_E_OK
	elif [[ -z $rep ]]; then
		# non-free with no replacement
		plain '%s: blacklisted' "$pkg"
		return $_E_DEP_NONFREE
	else
		# non-free with free replacement
		if [[ "$rep" == "$pkg" ]]; then
			info '%s: repackaged with the same name' "$pkg"
		else
			info '%s: replaced by %s' "$pkg" "$rep"
		fi
		return $_E_OK
	fi
	panic 'code should never be reached'
}

# Usage: check_pkg "${pkgname}"
# Checks for ${pkgname} in the blacklist
check_pkg() {
	[[ $# == 1 ]] || panic 'malformed call to check_pkg'
	check_dep "$@"
	case $? in
		$_E_OK)
			return $_E_OK;;
		$_E_DEP_NONFREE)
			return $_E_PKG_NONFREE;;
		*)
			panic 'unexpected return code from check_dep';;
	esac
	panic 'code should never be reached'
}

# Usage: pkgbuild_ckec $pkgbuild
# Check whether a PKGBUILD has any issues (using the above)
pkgbuild_check() (
	[[ $# == 1 ]] || panic 'malformed call to pkgbuild_check'
	local pkgbuild=$1

	load_PKGBUILD "$pkgbuild"
	if [[ -z $pkgname ]]; then
		return $_E_ERROR # not a PKGBUILD
	fi

	declare -i ret=0 # the return status
	local dep lic # iterators for us in `for` loops
	local ck_deps ck_lics # lists of deps and licenses that have been checked

	if [[ ${#pkgname[@]} == 1 ]]; then
		msg2 'Inspecting package pkgname=%q (%s)' "$pkgname" "$(get_full_version)"
	else
		msg2 'Inspecting split package pkgbase=%q (%s)' "${pkgbase:-${pkgname[0]}}" "$(get_full_version)"
	fi

	# Check if this is blacklisted
	check_pkg "${pkgbase:-${pkgname[0]}}" || ret=$(($ret|$?))
	# Check if dependencies are blacklisted
	for dep in "${depends[@]}" "${makedepends[@]}" "${checkdepends[@]}" \
               "${optdepends[@]%%:*}" "${mkdepends[@]}"
	do
		check_dep "$dep" || ret=$(($ret|$?))
		ck_deps+=("$dep")
	done
	# Check the licenses
	for lic in "${license[@]}"; do
		check_lic "$lic" || ret=$(($ret|$?))
		ck_lics+=("$lic")
	done

	if [[ ${#pkgname[@]} == 1 ]]; then
		# Non-split package
		# Make sure a license is set
		if [[ ${#ck_lics[@]} == 0 ]]; then
			error "The license array is empty"
			ret=$(($ret|$_E_ERROR))
		fi
	else
		# Split package
		# Check the individual split packages
		local _pkgname _license _depends _optdepends
		for _pkgname in "${pkgname[@]}"; do
			msg2 'Inspecting split package pkgname=%q (%s)' "$_pkgname" "$(get_full_version "$_pkgname")"
			eval $(pkgbuild_get_pkg_ary "$_pkgname" license _license)
			eval $(pkgbuild_get_pkg_ary "$_pkgname" depends _depends)
			eval $(pkgbuild_get_pkg_ary "$_pkgname" optdepends _optdepends)

			# Check if this is blacklisted
			check_pkg "$_pkgname" || ret=$(($ret|$?))
			# Check if dependencies are blacklisted
			for dep in "${_depends[@]}" "${_optdepends[@]%%:*}"; do
				if ! in_array "$dep" "${ck_deps[@]}"; then
					check_dep "$dep" || ret=$(($ret|$?))
				fi
			done
			# Check the licenses
			for lic in "${_license[@]}"; do
				if ! in_array "$lic" "${ck_lics[@]}"; then
					check_lic "$lic" || ret=$(($ret|$?))
				fi
			done

			if [[ ${#_license[@]} == 0 ]]; then
				error "The license array is empty"
				ret=$(($ret|$_E_ERROR))
			fi
		done
	fi

	return $ret
)

main "$@"