#!/usr/bin/python3
#
# Univention Configuration Registry
"""
List modified and not updated UCR templates.
"""
#
# Copyright 2011-2021 Univention GmbH
#
# https://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <https://www.gnu.org/licenses/>.
from __future__ import print_function
import os
import sys
from hashlib import md5
from argparse import ArgumentParser
from textwrap import dedent

from debian.deb822 import Deb822

PREFIX = '/etc/univention'
SUFFIX = ('.dpkg-new', '.dpkg-dist')
K64 = 1 << 16


def main():
	opt = parse_cmdline()
	modified = set()
	if opt.dpkg:
		modified |= check_find(opt.verbose)
	if opt.md5:
		modified |= check_md5(opt.verbose)
	if modified:
		print(dedent("""\
			WARNING: The following UCR files are modified locally.
			Updated versions will be named FILENAME.dpkg-*.
			The files should be checked for differences.
			"""), file=sys.stderr)
		print('\n'.join(sorted(modified)))
		return 1
	return 0


def parse_cmdline():
	description = sys.modules[__name__].__doc__
	parser = ArgumentParser(description=description)
	parser.add_argument(
		'--md5',
		action='store_false', default=True,
		help='Disable checking MD5 sums.')
	parser.add_argument(
		'--dpkg',
		action='store_false', default=True,
		help='Disable checking for renamed files.')
	parser.add_argument(
		'--verbose', '-v',
		action='store_true',
		help='Enable verbose output.')
	return parser.parse_args()


def check_find(verbose=False):
	modified = set()
	for dirpath, dirnames, filenames in os.walk(PREFIX):
		for filename in filenames:
			for suffix in SUFFIX:
				if filename.endswith(suffix):
					filepath = os.path.join(dirpath, filename)
					if verbose:
						print(filepath, file=sys.stderr)
					basepath = filepath[:-len(suffix)]
					if not os.path.exists(basepath):
						continue
					modified.add(basepath)
	return modified


def check_md5(verbose=False):
	modified = set()
	original = set()
	try:
		for filepath, expected in iter_templates():
			if filepath in original or filepath in modified:
				continue

			current = md5sum(filepath)
			if verbose:
				print("%s %s %s" % (filepath, expected, current), file=sys.stderr)
			if expected == current:
				original.add(filepath)
			elif current is None:
				continue
			else:
				modified.add(filepath)
		return modified
	except IOError as ex:
		print(ex, file=sys.stderr)
		sys.exit(2)


def iter_templates():
	with open('/var/lib/dpkg/status', 'r') as dpkg_status:
		for pkg in Deb822.iter_paragraphs(dpkg_status, ["Conffiles"], use_apt_pkg=True):
			try:
				conffiles = pkg["Conffiles"]
			except KeyError:
				continue
			for conffile in conffiles.splitlines():
				fields = [_.strip() for _ in conffile.rsplit(' ', 1) if _]
				# skip obsolete and new conffiles
				if not fields or 'newconffile' in fields or 'obsolete' in fields:
					continue
				filepath, fmd5 = fields
				if filepath.startswith(PREFIX):
					yield filepath, fmd5


def md5sum(filepath):
	digest = md5()
	try:
		with open(filepath, 'rb') as stream:
			while True:
				buf = stream.read(65536)
				if not buf:
					break
				digest.update(buf)
	except IOError:
		return None

	return digest.hexdigest()


if __name__ == '__main__':
	sys.exit(main())
