#!/usr/bin/python3
# encoding=UTF-8

# Copyright © 2011-2016 Jakub Wilk <jwilk@jwilk.net>
# SPDX-License-Identifier: MIT

'''
Check MD5 sums for 'Multi-Arch: same' packages.
'''

from __future__ import print_function

import argparse
import collections
import contextlib
import io
import dbm
import os
import sys

import apt_pkg

import misc

if sys.version_info >= (3, 0):
    def text_wrapper(file):
        return io.TextIOWrapper(file, encoding='UTF-8')
else:
    @contextlib.contextmanager
    def text_wrapper(file):
        try:
            yield file
        finally:
            file.close()

class DummyCache(object):

    def __getitem__(self, key):
        raise KeyError

    def __setitem__(self, key, value):
        pass

    def keys(self):
        return ()

    def close(self):
        pass

def parse_md5sums_line(pkgdata, line, architecture):
    md5sum = line[:32]
    filename = line[34:-1]
    pkgdata[filename][md5sum].add(architecture)

def do_qa(options):
    seen_cache_keys = set()
    if options.cache:
        try:
            os.makedirs(os.path.dirname(options.cache), mode=0o700)
        except OSError:
            pass
        cache = dbm.open(options.cache, 'c')
    else:
        cache = DummyCache()
    data = collections.defaultdict(dict)
    if options.architectures is None:
        release_dist = options.distribution
        if release_dist in ('unstable', 'sid', 'experimental', 'rc-buggy'):
            release_dist = 'testing'
        url = '{mirror}/dists/{dist}/Release'.format(
            mirror=options.mirror,
            dist=release_dist
        )
        with misc.download(url) as release_tags:
            with text_wrapper(release_tags) as release_tags:
                for para in apt_pkg.TagFile(release_tags):
                    options.architectures = para['Architectures'].split()
    if options.skip_architectures is not None:
        options.architectures = [
            arch
            for arch in options.architectures
            if arch not in options.skip_architectures
        ]
    for architecture in options.architectures:
        for section in 'main', 'contrib', 'non-free':
            url = '{mirror}/dists/{dist}/{section}/binary-{arch}/Packages.xz'.format(
                mirror=options.mirror,
                dist=options.distribution,
                section=section,
                arch=architecture
            )
            with misc.download(url, pipe='xz -dc') as package_tags:
                with text_wrapper(package_tags) as package_tags:
                    for pkgdata in apt_pkg.TagFile(package_tags):
                        if pkgdata.get('Multi-Arch', '') == 'same':
                            pkgname = pkgdata['Package']
                            if pkgname not in options.packages:
                                continue
                            pkgversion = pkgdata['Version']
                            url = '{mirror}/{path}'.format(mirror=options.mirror, path=pkgdata['Filename'])
                            data[pkgname, pkgversion][architecture] = url
    last = None
    for (pkgname, pkgversion), urls in sorted(data.items()):
        if len(urls) <= 1:
            misc.log_action(pkgname, pkgversion, 'skip')
            continue
        misc.log_action(pkgname, pkgversion, 'download ({archs})'.format(archs=' '.join(urls.keys())))
        pkgdata = collections.defaultdict(
            lambda: collections.defaultdict(set)
        )
        for architecture, url in urls.items():
            cache_key = '{name}_{version}_{arch}'.format(name=pkgname, version=pkgversion, arch=architecture)
            cache_key = cache_key.encode('ASCII')
            try:
                cache_item = cache[cache_key]
                for line in cache_item.splitlines(True):
                    parse_md5sums_line(pkgdata, line, architecture)
            except KeyError:
                try:
                    cache_item = []
                    with misc.download(url, pipe='dpkg-deb -I /dev/stdin md5sums') as md5sums_file:
                        for line in md5sums_file:
                            parse_md5sums_line(pkgdata, line, architecture)
                            cache_item += [line]
                    cache[cache_key] = b''.join(cache_item)
                except misc.DownloadError as exc:
                    if 'contains no control component' in str(exc):
                        misc.log_error(pkgname, pkgversion, 'missing md5sums for {arch}'.format(arch=architecture))
                        continue
                    else:
                        raise
            seen_cache_keys.add(cache_key)
        for filename, md5sums in sorted(pkgdata.items()):
            if len(md5sums) <= 1:
                continue
            if last != (pkgname, pkgversion):
                if last is not None:
                    print()
                print('[{name} {ver}]'.format(name=pkgname, ver=pkgversion))
                last = (pkgname, pkgversion)
            print(filename.decode('UTF-8', 'replace'))
            if options.compact:
                if all(len(x) == 1 for x in md5sums.values()):
                    continue
            for md5sum, architectures in sorted(md5sums.items()):
                print('  {md5sum} {arch}'.format(
                    md5sum=md5sum.decode('ASCII'),
                    arch=' '.join(sorted(architectures))
                ))
    if options.prune_cache:
        for key in set(cache.keys()) - seen_cache_keys:
            del cache[key]
    cache.close()

class Universum(object):

    def __contains__(self, other):
        return True

def main():
    misc.setup_proxies()
    misc.setup_locale()
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--mirror', default=misc.default_mirror,
        metavar='<mirror>',
        help='use this mirror (default: {mirror})'.format(mirror=misc.default_mirror)
    )
    parser.add_argument('--distribution', default=misc.default_distribution,
        metavar='<dist>',
        help='check this distribution (default: {dist})'.format(dist=misc.default_distribution)
    )
    parser.add_argument('--architectures', nargs='+',
        metavar='<arch>',
        help='check these architectures (default: all release architectures)'
    )
    parser.add_argument('--skip-architectures', nargs='+',
        metavar='<arch>',
        help='don\'t check these architectures'
    )
    parser.add_argument('--packages', nargs='+', default=Universum(),
        metavar='<package>',
        help='check only these packages (default: check all)'
    )
    parser.add_argument('--compact', action='store_true',
        help='don\'t print MD5 sums if they are all different'
    )
    parser.add_argument('--cache', nargs='?',
        metavar='<file>', default=False,
        help='use cache file'
    )
    parser.add_argument('--prune-cache', action='store_true',
        help='remove unused items from cache'
    )
    parser.add_argument('--log-file', type=argparse.FileType('a'), default=sys.stderr,
        metavar='<file>',
        help='log progress into this file (default: stderr)'
    )
    options = parser.parse_args()
    misc.setup_log_file(options.log_file)
    if isinstance(options.packages, list):
        options.packages = frozenset(options.packages)
    if options.cache is None:
        options.cache = os.path.join((
            os.getenv('XDG_CACHE_HOME') or
            os.path.join(os.path.expanduser('~'), '.cache')
        ), 'debian', 'multi-arch-same-validator')
    do_qa(options)

if __name__ == '__main__':
    main()

# vim:ts=4 sts=4 sw=4 et
