#!/bin/sh -e
#
# $Id: sisyphus_check,v 1.18 2004/07/15 19:45:35 ldv Exp $
# Copyright (C) 2003, 2004  Stanislav Ievlev <inger@altlinux.org>,
#                           Dmitry V. Levin <ldv@altlinux.org>
# 
# The sisyphus_check utility.
#
# This file 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 2 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, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#

unset \
	LANG \
	LANGUAGE \
	LINGUAS \
	LC_CTYPE \
	LC_NUMERIC \
	LC_TIME \
	LC_COLLATE \
	LC_MONETARY \
	LC_MESSAGES \
	LC_PAPER \
	LC_NAME \
	LC_ADDRESS \
	LC_TELEPHONE \
	LC_MEASUREMENT \
	LC_IDENTIFICATION \
	LC_ALL \
	||:

PROG="${0##*/}"

Info()
{
	printf %s\\n "$PROG: $*" >&2
}

Fatal()
{
	printf %s\\n "$PROG: $*" >&2
	exit 1
}

quiet=
Message()
{
	[ -z "$quiet" ] || return 0
	printf %s\\n "$*"
}

show_bad_files=
FileError()
{
	local text="$1"
	shift || return
	local f="$1"

	if [ -n "$f" ]; then
		text="${text#$f: }"
		text="$f: $text"
		[ -z "$show_bad_files" ] || printf %s\\n "$f"
	fi
	printf %s\\n "$(printf %s "$text" |tr '[:cntrl:]' ' ')" >&2
}

# check package signature
export no_check_gpg=
check_gpg()
{
	[ -z "$no_check_gpg" ] || return 0
	local f="$1" && shift || return 1
	local text

	if ! text="$(LC_ALL=C GNUPGHOME=/usr/lib/alt-gpgkeys rpmsign -K "$f")"; then
		FileError "rpmsign failed" "$f"
		return 1
	fi
	if text="$(printf %s "$text" |grep -v ': md5 gpg OK$')"; then
		text="$(printf %s "$text" |tr -s '[:space:]' ' ')"
		text="${text#$f: }"
		FileError "bad SIGNATURE: $(printf %s "$text" |tr -s '[:space:]' ' ')" "$f"
		return 1
	fi
	return 0
}

# check for valid buildhost
export no_check_buildhost=
check_buildhost()
{
	[ -z "$no_check_buildhost" ] || return 0
	local f="$1" && shift || return 1

	# Do not check build host for source rpms.
	[ -n "${f%%*src.rpm}" ] || return 0

	if ! printf %s "$rpm_buildhost" |egrep -qs '^[^.]+\.(hasher|sandman)\.altlinux\.org$'; then
		FileError "wrong BUILDHOST: $rpm_buildhost" "$f"
		return 1
	fi
	return 0
}

# check for valid buildtime
export no_check_buildtime=
check_buildtime()
{
	[ -z "$no_check_buildtime" ] || return 0
	local f="$1" && shift || return 1

	if ! [ "$current_time" -ge "$rpm_buildtime" ]; then
		FileError "wrong BUILDTIME: $(date -d "1970-01-01 UTC $rpm_buildtime seconds")" "$f"
		return 1
	fi
	return 0
}

# check for printable summary, description, etc.
export no_check_printable=
check_printable()
{
	[ -z "$no_check_printable" ] || return 0
	local f="$1" && shift || return 1
	local text

	text="$rpm_name $rpm_version $rpm_release $rpm_group $rpm_packager $rpm_license $rpm_summary $rpm_description"

	if [ -n "$(printf %s "$text" |LC_ALL=C tr -d '[:print:][:space:]')" ]; then
		FileError "unprintable package information: $(printf %s "$text" |LC_ALL=C tr -d '[:print:][:space:]')" "$f"
		return 1
	fi
	return 0
}

# check for valid nvr
export no_check_nvr=
check_nvr()
{
	[ -z "$no_check_nvr" ] || return 0
	local f="$1" && shift || return 1
	local rc=0

	if printf %s "$rpm_version" |grep -qs '[%<=>]'; then
		FileError "invalid package VERSION: $rpm_version" "$f"
		rc=1
	fi

	if printf %s "$rpm_release" |grep -qs '[%<=>]'; then
		FileError "invalid package RELEASE: $rpm_release" "$f"
		rc=1
	fi

	if ! printf %s "$rpm_release" |egrep -qs '^(alt|ipl)[0-9]'; then
		FileError "invalid package RELEASE: $rpm_release" "$f"
		rc=1
	fi

	return $rc
}

# check for valid summary
export no_check_summary=
check_summary()
{
	[ -z "$no_check_summary" ] || return 0
	local f="$1" && shift || return 1

	if ! printf %s "$rpm_summary" |egrep -qs '[[:alpha:]]'; then
		FileError "invalid SUMMARY: $rpm_summary" "$f"
		return 1
	fi
	return 0
}

# check for valid description
export no_check_description=
check_description()
{
	[ -z "$no_check_description" ] || return 0
	local f="$1" && shift || return 1

	if ! printf %s "$rpm_description" |egrep -qs '[[:alpha:]]'; then
		FileError "invalid DESCRIPTION: $(printf %s "$rpm_description" |tr -s '[:space:]' ' ')" "$f"
		return 1
	fi
	return 0
}

# check for valid group
export no_check_group=
check_group()
{
	[ -z "$no_check_group" ] || return 0
	local f="$1" && shift || return 1

	if ! grep -q "^$rpm_group$" /usr/lib/rpm/GROUPS; then
		FileError "wrong GROUP: $rpm_group" "$f"
		return 1
	fi
	return 0
}

# check for valid format of PACKAGER tag
export no_check_packager=
check_packager()
{
	[ -z "$no_check_packager" ] || return 0
	local f="$1" && shift || return 1

	if ! printf %s "$rpm_packager" |egrep -qs "$packager_pattern"; then
		FileError "wrong PACKAGER: $rpm_packager" "$f"
		return 1
	fi
	return 0
}

# check changelog format
export no_check_changelog=
check_changelog()
{
	[ -z "$no_check_changelog" ] || return 0
	local f="$1" && shift || return 1
	local rc=0

	if ! [ "$((current_time+86400))" -ge "$rpm_changelogtime" ] 2>/dev/null; then
		FileError "wrong CHANGELOGTIME: $(date -d "1970-01-01 UTC $rpm_changelogtime seconds")" "$f"
		rc=1
	fi

	if ! printf %s "$rpm_changelogtext" |sed -e 's/[^[:alnum:]]//g' |grep -qsv '^$'; then
		FileError "invalid CHANGELOGTEXT: $(printf %s "$rpm_changelogtext" |tr -s '[:space:]' ' ')" "$f"
		rc=1
	fi

	# Do not check changelog name for binary rpms.
	if [ -z "${f%%*src.rpm}" ]; then
		local c_found
		if ! c_found=`printf %s "$rpm_changelogname" |sed -ne '/^(none)$/q;s/[^<]\+<[^>]\+> *\(.\+\)$/\1/pg'`; then
			FileError "sed failed" "$f"
			rc=1
		fi

		local c_expected
		[ -z "$rpm_serial" ] && c_expected= || c_expected="$rpm_serial:"
		c_expected="$c_expected$rpm_version-$rpm_release"

		if [ "$c_expected" != "$c_found" ]; then
			FileError "wrong CHANGELOGNAME: expected \"$c_expected\", found \"$c_found\"" "$f"
			rc=1
		fi

		if ! printf %s "$rpm_changelogname" |egrep -qs "$packager_pattern"; then
			FileError "wrong packager in CHANGELOGNAME: $rpm_changelogname" "$f"
			rc=1
		fi
	fi

	return $rc
}

# check for inacceptable dependencies
export no_check_deps=
check_deps()
{
	[ -z "$no_check_deps" ] || return 0
	local f="$1" && shift || return 1
	local rc=0
	local bad

	bad="$(printf %s "$rpm_deps" |grep '[$%]')"
	if [ -n "$bad" ]; then
		FileError "invalid dependencies: $(printf %s "$bad" |tr -s '[:space:]' ' ')" "$f"
		rc=1
	fi

	bad="$(printf %s "$rpm_requires" |egrep '^(fileutils|initscripts|sh-utils|textutils|/etc/rc\.d/init\.d\((daemon|killproc|pidof)\)|pam_stack\.so)$')"
	if [ -n "$bad" ]; then
		FileError "forbidden requires: $(printf %s "$bad" |tr -s '[:space:]' ' ')" "$f"
		rc=1
	fi

	return $rc
}

# check files intersections with filesystem
export no_check_content=
check_content()
{
	[ -z "$no_check_content" ] || return 0
	local f="$1" && shift || return 1

	if printf %s "$rpm_filelist" |egrep -e '^(/usr(/X11R6)?)?/lib/lib[^/]+\.la$' >&2; then
		FileError "forbidden .la files" "$f"
		return 1
	fi

	return 0
}

# check for FHS-2.2 violations
export no_check_fhs=
check_fhs()
{
	[ -z "$no_check_fhs" ] || return 0
	local f="$1" && shift || return 1
	local list pattern

	# Do not check filesystem package.
	[ "$rpm_name" != filesystem ] || return 0

	list=$(printf %s "$rpm_filelist" |grep -e '^/')
	[ -n "$list" ] || return 0

	pattern="$(grep '^[^#]' /etc/sisyphus_check/fhs |
		while read -r n v; do
			[ -n "$n" -a -n "$v" -a -z "${rpm_name##$n}" ] || continue
			printf %s\\n "$v"
		done |LC_COLLATE=C sort -u)" || return 1
	pattern="$(printf %s "$pattern" |tr -s '\n' '|')"
	[ "$pattern" != '' ] || pattern='^$'

	list="$(printf %s "$list" |egrep -v "$pattern")" ||
		return 0

	if printf %s "$list" |egrep -vqse '^/(bin|boot|etc|lib|sbin|usr/(X11R6|bin|etc|games|include|lib|sbin|share|src)|var/(cache|lib|lock|log|run|spool|www|yp))/.*' >&2; then
		FileError "FHS-2.2 violations: $(printf %s "$list" |tr -s '[:space:]' ' ')" "$f"
		return 1
	fi
	return 0
}

# check files permissions
export no_check_perms=
check_perms()
{
	[ -z "$no_check_perms" ] || return 0
	local f="$1" && shift || return 1
	local rc=0

	if printf %s "$rpm_long_filelist" |egrep -e '^-..s(r|.w|...r|....w)|^-...((r.|.w)s|..s(r|.w))' >&2; then
		FileError "bad permissions for suid/sgid files" "$f"
		rc=1
	fi
	if printf %s "$rpm_long_filelist" |egrep -e '^[^l]....(w|...w)[^/]+/usr/' >&2; then
		FileError "writable files in /usr/" "$f"
		rc=1
	fi
	if [ -z "${f%%*src.rpm}" ]; then
		if printf %s "$rpm_long_filelist" |egrep -e '^-([^r]|.[^w])' >&2; then
			FileError "bad permissions in source archive" "$f"
			rc=1
		fi
	fi

	return $rc
}

# check files intersections with filesystem
export no_check_intersects=
check_intersects()
{
	[ -z "$no_check_intersects" ] || return 0
	local f="$1" && shift || return 1
	local rc=0
	local text

	# Do not check filesystem package.
	[ "$rpm_name" != filesystem ] || return 0

	if ! text="$(printf %s\\n "$rpm_filelist" |LC_COLLATE=C comm -12 -- "$fs_list" -)"; then
		FileError "comm failed" "$f"
		rc=1
	fi

	if [ -n "$text" ]; then
		FileError "filesystem intersections: $(printf %s "$text" |tr -s '[:space:]' ' ')" "$f"
		rc=1
	fi

	return $rc
}

fs_list=
exit_handler()
{
	local rc=$?
	trap - EXIT
	[ -z "$fs_list" ] || rm -f -- "$fs_list"
	exit $rc
}

init_check()
{
	unset current_time packager_pattern
	local fs_filelist

	trap exit_handler HUP PIPE INT QUIT TERM EXIT
	fs_list="$(mktemp -t "$PROG.XXXXXXXXXX")"

	fs_filelist="$(LC_ALL=C rpmquery -l filesystem)" || Fatal "rpmquery filesystem failed."
	printf %s "$fs_filelist" |LC_COLLATE=C sort -u >"$fs_list" || Fatal "sort failed."
	current_time="$(date +%s)" || Fatal "date failed."
	packager_pattern='<[^@]+(@| at )(packages\.)?altlinux(\.| dot )(com|net|org|ru)>'
}

fast_check=
# cummulative check
check()
{
	local rc=0

	# quick arg check.
	local d
	for d in "$@"; do
		[ -d "$d" ] || { FileError "$d: not a directory"; rc=1; continue; }
	done
	[ $rc = 0 ] || return $rc

	init_check || Fatal "init_check failed."

	for d in "$@"; do
		[ -d "$d" ] || { FileError "$d: not a directory"; rc=1; continue; }
		local f
		for f in "$d"/*.rpm; do
			if [ ! -f "$f" ]; then
				[ "$f" != "$d/*.rpm" ] || continue
				FileError "not a regular file" "$f"
				rc=1
				continue
			fi

			if ! check_gpg "$f"; then
				Message "ERROR: you have problems with package signatures"
				rc=1
				[ -z "$fast_check" ] || continue
			fi

			local rpm_arch rpm_buildhost rpm_buildtime
			local rpm_changelogname rpm_changelogtext
			local rpm_changelogtime rpm_description
			local rpm_distribution rpm_group rpm_license rpm_name
			local rpm_packager rpm_platform rpm_release rpm_serial
			local rpm_size rpm_sourcerpm rpm_summary rpm_url
			local rpm_vendor rpm_version

			local rpm_deps rpm_requires
			local rpm_filelist rpm_long_filelist
			local values

			if ! values="$(LC_ALL=C rpmquery -p --qf='
rpm_arch=%{arch:shescape};
rpm_buildhost=%{buildhost:shescape};
rpm_buildtime=%{buildtime:shescape};
rpm_changelogname=%{changelogname:shescape};
rpm_changelogtext=%{changelogtext:shescape};
rpm_changelogtime=%{changelogtime:shescape};
rpm_description=%{description:shescape};
rpm_distribution=%{distribution:shescape};
rpm_group=%{group:shescape};
rpm_license=%{license:shescape};
rpm_name=%{name:shescape};
rpm_packager=%{packager:shescape};
rpm_platform=%{platform:shescape};
rpm_release=%{release:shescape};
rpm_serial=%|serial?{%{serial:shescape}}|;
rpm_size=%{size:shescape};
rpm_sourcerpm=%{sourcerpm:shescape};
rpm_summary=%{summary:shescape};
rpm_url=%{url:shescape};
rpm_vendor=%{vendor:shescape};
rpm_version=%{version:shescape};
				' -- $f)"; then
				FileError "rpmquery failed" "$f"
				rc=1
				continue
			fi
			eval "$values"
			unset values

			if ! rpm_deps="$(LC_ALL=C rpmquery -p --requires --provides --obsoletes --conflicts "$f")"; then
				FileError "rpmquery failed" "$f"
				rc=1
				continue
			fi

			if ! rpm_requires="$(LC_ALL=C rpmquery -p --qf '[%{REQUIRENAME}\n]' "$f")"; then
				FileError "rpmquery failed" "$f"
				rc=1
				continue
			fi

			if ! rpm_filelist="$(LC_ALL=C rpmquery -pl "$f")"; then
				FileError "rpmquery failed" "$f"
				rc=1
				continue
			fi

			if ! rpm_long_filelist="$(LC_ALL=C rpmquery -plv "$f")"; then
				FileError "rpmquery failed" "$f"
				rc=1
				continue
			fi

			rpm_filelist="$(printf %s "$rpm_filelist" |LC_COLLATE=C sort -u)" || Fatal "sort failed."
			rpm_long_filelist="$(printf %s "$rpm_long_filelist" |LC_COLLATE=C sort -u)" || Fatal "sort failed."

			if ! check_buildhost "$f"; then
				Message "ERROR: you have problems with buildhost name"
				rc=1
				[ -z "$fast_check" ] || continue
			fi

			if ! check_buildtime "$f"; then
				Message "ERROR: you have problems with buildtime"
				rc=1
				[ -z "$fast_check" ] || continue
			fi

			if ! check_packager "$f"; then
				Message "ERROR: you have problems with packager name"
				rc=1
				[ -z "$fast_check" ] || continue
			fi

			if ! check_printable "$f"; then
				Message "ERROR: you have problems with package information"
				rc=1
				[ -z "$fast_check" ] || continue
			fi

			if ! check_summary "$f"; then
				Message "ERROR: you have problems with package summary"
				rc=1
				[ -z "$fast_check" ] || continue
			fi

			if ! check_description "$f"; then
				Message "ERROR: you have problems with package description"
				rc=1
				[ -z "$fast_check" ] || continue
			fi

			if ! check_group "$f"; then
				Message "ERROR: you have problems with package group"
				rc=1
				[ -z "$fast_check" ] || continue
			fi

			if ! check_nvr "$f"; then
				Message "ERROR: you have problems with package name-version-release format"
				rc=1
				[ -z "$fast_check" ] || continue
			fi

			if ! check_changelog "$f"; then
				Message "ERROR: you have problems with changelog format"
				rc=1
				[ -z "$fast_check" ] || continue
			fi

			if ! check_deps "$f"; then
				Message "ERROR: you have problems with package dependencies"
				rc=1
				[ -z "$fast_check" ] || continue
			fi

			if ! check_content "$f"; then
				Message "ERROR: you have problems with package content"
				rc=1
				[ -z "$fast_check" ] || continue
			fi

			# More expensive checks go last.

			if ! check_fhs "$f"; then
				Message "ERROR: you have problems with standards"
				rc=1
				[ -z "$fast_check" ] || continue
			fi

			if ! check_perms "$f"; then
				Message "ERROR: you have problems with file permissions"
				rc=1
				[ -z "$fast_check" ] || continue
			fi

			if ! check_intersects "$f"; then
				Message "ERROR: you have package intersections"
				rc=1
				[ -z "$fast_check" ] || continue
			fi
		done
	done

	return $rc
}

check_env="$(printenv |sed -ne 's/^\(no_check_[a-z]\+\)=.*/\1/pg')"

Usage()
{
	[ "$1" = 0 ] || exec >&2
	cat <<EOF

sisyphus_check - check packages for acceptability for Sisyphus.

This program is free software, covered by the GNU General Public License.
sisyphus_check comes with ABSOLUTELY NO WARRANTY, see license for details.

Usage: $PROG [options] <directory>...

Valid options are:
  --quiet
  --verbose
  --fast-check
  --show-bad-files
  --no-check=LIST
EOF
	printf %s\\n "$check_env" |sed -ne 's/.*/  --&/pg' |tr _ -
	[ -n "$1" ] && exit "$1" || exit
}

TEMP=`getopt -n $PROG -o h,q,v -l help,quiet,verbose,fast-check,show-bad-files,no-check:,no-check-,$(printf %s "$check_env" |tr _ - |tr -s '[:space:]' ',') -- "$@"` || Usage
eval set -- "$TEMP"

while :; do
	case "$1" in
		--no-check)
			shift
			if [ -n "$(printf %s "$1" |tr -d '[:alpha:],[:space:]')" ]; then
				Info "--no-check: invalid argument: $1"
				Usage
			fi
			for arg in `printf %s "$1" |tr , ' '`; do
				name="no_check_$arg"
				if ! echo "$check_env" |grep -qs "^$name\$"; then
					Info "--no-check: invalid argument: $arg"
					Usage
				fi
				eval $name=1
			done
			;;
		--no-check-) Usage
			;;
		--no-check-[a-z]*)
			arg="no_check_${1##--no-check-}"
			eval $arg=1
			;;
		-q|--quiet) quiet=-q
			;;
		-v|--verbose) quiet=
			;;
		--fast-check) fast_check=1
			;;
		--show-bad-files) show_bad_files=1
			;;
		-h|--help) Usage 0
			;;
		--) shift; break
			;;
		*) Fatal "unrecognized option: $1"
			;;
	esac
	shift
done

# At least one argument, please.
if ! [ "$#" -ge 1 ]; then
	Info "at least one argument is required."
	Usage
fi

check "$@"
