#!@TOOLS_PYTHON@ -Es
#
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License (the "License").
# You may not use this file except in compliance with the License.
#
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
# or http://www.opensolaris.org/os/licensing.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#

#
# Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
# Use is subject to license terms.
#

# Copyright 2022 OmniOS Community Edition (OmniOSce) Association.

#
# Compare the content generated by a build to a set of manifests
# describing how that content is to be delivered.
#


import getopt
import gettext
import locale
import os
import stat
import sys

from pkg import actions
from pkg import manifest

#
# Dictionary used to map action names to output format.  Each entry is
# indexed by action name, and consists of a list of tuples that map
# FileInfo class members to output labels.
#
OUTPUTMAP = {
    "dir": [
        ("group", "group="),
        ("mode", "mode="),
        ("owner", "owner="),
        ("path", "path=")
    ],
    "file": [
        ("hash", ""),
        ("group", "group="),
        ("mode", "mode="),
        ("owner", "owner="),
        ("path", "path=")
    ],
    "link": [
        ("mediator", "mediator="),
        ("path", "path="),
        ("target", "target=")
    ],
    "hardlink": [
        ("path", "path="),
        ("hardkey", "target=")
    ],
}

# Mode checks used to validate safe file and directory permissions
ALLMODECHECKS = frozenset(("m", "w", "s", "o"))
DEFAULTMODECHECKS = frozenset(("m", "w", "o"))

class FileInfo(object):
    """Base class to represent a file.

    Subclassed according to whether the file represents an actual filesystem
    object (RealFileInfo) or an IPS manifest action (ActionInfo).
    """

    def __init__(self):
        self.path = None
        self.isdir = False
        self.target = None
        self.owner = None
        self.group = None
        self.mode = None
        self.hardkey = None
        self.hardpaths = set()
        self.editable = False

    def name(self):
        """Return the IPS action name of a FileInfo object.
        """
        if self.isdir:
            return "dir"

        if self.target:
            return "link"

        if self.hardkey:
            return "hardlink"

        return "file"

    def checkmodes(self, modechecks):
        """Check for and report on unsafe permissions.

        Returns a potentially empty list of warning strings.
        """
        w = []

        t = self.name()
        if t in ("link", "hardlink"):
            return w
        m = int(self.mode, 8)
        o = self.owner
        p = self.path

        if "s" in modechecks and t == "file":
            if m & (stat.S_ISUID | stat.S_ISGID):
                if m & (stat.S_IRGRP | stat.S_IROTH):
                    w.extend(["%s: 0%o: setuid/setgid file should not be " \
                        "readable by group or other" % (p, m)])

        if "o" in modechecks and o != "root" and ((m & stat.S_ISUID) == 0):
            mu = (m & stat.S_IRWXU) >> 6
            mg = (m & stat.S_IRWXG) >> 3
            mo = m & stat.S_IRWXO
            e = self.editable

            if (((mu & 0o2) == 0 and (mo & mg & 0o4) == 0o4) or
                (t == "file" and mo & 0o1 == 1) or
                (mg, mo) == (mu, mu) or
                ((t == "file" and not e or t == "dir" and o == "bin") and
                (mg & 0o5 == mo & 0o5)) or
                (t == "file" and o == "bin" and mu & 0o1 == 0o1) or
                (m & 0o105 != 0 and p.startswith("etc/security/dev/"))):
                w.extend(["%s: owner \"%s\" may be safely " \
                    "changed to \"root\"" % (p, o)])

        if "w" in modechecks and t == "file" and o != "root":
            uwx = stat.S_IWUSR | stat.S_IXUSR
            if m & uwx == uwx:
                w.extend(["%s: non-root-owned executable should not " \
                    "also be writable by owner." % p])

        if ("m" in modechecks and
            m & (stat.S_IWGRP | stat.S_IWOTH) != 0 and
            m & stat.S_ISVTX == 0):
            w.extend(["%s: 0%o: should not be writable by group or other" %
                (p, m)])

        return w

    def __ne__(self, other):
        """Compare two FileInfo objects.

        Note this is the "not equal" comparison, so a return value of False
        indicates that the objects are functionally equivalent.
        """
        #
        # Map the objects such that the lhs is always the ActionInfo,
        # and the rhs is always the RealFileInfo.
        #
        # It's only really important that the rhs not be an
        # ActionInfo; if we're comparing FileInfo the RealFileInfo, it
        # won't actually matter what we choose.
        #
        if isinstance(self, ActionInfo):
            lhs = self
            rhs = other
        else:
            lhs = other
            rhs = self

        #
        # Because the manifest may legitimately translate a relative
        # path from the proto area into a different path on the installed
        # system, we don't compare paths here.  We only expect this comparison
        # to be invoked on items with identical relative paths in
        # first place.
        #

        #
        # All comparisons depend on type.  For symlink and directory, they
        # must be the same.  For file and hardlink, see below.
        #
        typelhs = lhs.name()
        typerhs = rhs.name()
        if typelhs in ("link", "dir"):
            if typelhs != typerhs:
                return True

        #
        # For symlinks, all that's left is the link target.
        # For mediated symlinks targets can differ.
        #
        if typelhs == "link":
            return (lhs.mediator is None) and (lhs.target != rhs.target)

        #
        # For a directory, it's important that both be directories,
        # the modes be identical, and the paths are identical.  We already
        # checked all but the modes above.
        #
        # If both objects are files, then we're in the same boat.
        #
        if typelhs == "dir" or (typelhs == "file" and typerhs == "file"):
            return lhs.mode != rhs.mode

        #
        # For files or hardlinks:
        #
        # Since the key space is different (inodes for real files and
        # actual link targets for hard links), and since the proto area will
        # identify all N occurrences as hardlinks, but the manifests as one
        # file and N-1 hardlinks, we have to compare files to hardlinks.
        #

        #
        # If they're both hardlinks, we just make sure that
        # the same target path appears in both sets of
        # possible targets.
        #
        if typelhs == "hardlink" and typerhs == "hardlink":
            return len(lhs.hardpaths.intersection(rhs.hardpaths)) == 0

        #
        # Otherwise, we have a mix of file and hardlink, so we
        # need to make sure that the file path appears in the
        # set of possible target paths for the hardlink.
        #
        # We already know that the ActionInfo, if present, is the lhs
        # operator.  So it's the rhs operator that's guaranteed to
        # have a set of hardpaths.
        #
        return lhs.path not in rhs.hardpaths

    def __str__(self):
        """Return an action-style representation of a FileInfo object.

        We don't currently quote items with embedded spaces.  If we
        ever decide to parse this output, we'll want to revisit that.
        """
        name = self.name()
        out = name

        for member, label in OUTPUTMAP[name]:
            out += " " + label + str(getattr(self, member))

        return out

    def protostr(self):
        """Return a protolist-style representation of a FileInfo object.
        """
        target = "-"
        major = "-"
        minor = "-"

        mode = self.mode
        owner = self.owner
        group = self.group

        name = self.name()
        if name == "dir":
            ftype = "d"
        elif name in ("file", "hardlink"):
            ftype = "f"
        elif name == "link":
            ftype = "s"
            target = self.target
            mode = "777"
            owner = "root"
            group = "other"

        out = "%c %-30s %-20s %4s %-5s %-5s %6d %2ld  -  -" % \
            (ftype, self.path, target, mode, owner, group, 0, 1)

        return out

class ActionInfoError(Exception):
    def __init__(self, action, error):
        Exception.__init__(self)
        self.action = action
        self.error = error

    def __str__(self):
        return "Error in '%s': %s" % (self.action, self.error)


class ActionInfo(FileInfo):
    """Object to track information about manifest actions.

    This currently understands file, link, dir, and hardlink actions.
    """

    def __init__(self, action):
        FileInfo.__init__(self)
        #
        # Currently, all actions that we support have a "path"
        # attribute.  If that changes, then we'll need to
        # catch a KeyError from this assignment.
        #
        self.path = action.attrs["path"]

        if action.name == "file":
            self.owner = action.attrs["owner"]
            self.group = action.attrs["group"]
            self.mode = action.attrs["mode"]
            self.hash = action.hash
            if "preserve" in action.attrs:
                self.editable = True
        elif action.name == "link":
            try:
                target = action.attrs["target"]
            except KeyError:
                raise ActionInfoError(str(action),
                    "Missing 'target' attribute")
            else:
                self.target = os.path.normpath(target)
                self.mediator = action.attrs.get("mediator")
        elif action.name == "dir":
            self.owner = action.attrs["owner"]
            self.group = action.attrs["group"]
            self.mode = action.attrs["mode"]
            self.isdir = True
        elif action.name == "hardlink":
            try:
                target = os.path.normpath(action.get_target_path())
            except KeyError:
                raise ActionInfoError(str(action),
                    "Missing 'target' attribute")
            else:
                self.hardkey = target
                self.hardpaths.add(target)

    @staticmethod
    def supported(action):
        """Indicates whether the specified IPS action time is
        correctly handled by the ActionInfo constructor.
        """
        return action in frozenset(("file", "dir", "link", "hardlink"))


class UnsupportedFileFormatError(Exception):
    """This means that the stat.S_IFMT returned something we don't
    support, ie a pipe or socket.  If it's appropriate for such an
    object to be in the proto area, then the RealFileInfo constructor
    will need to evolve to support it, or it will need to be in the
    exception list.
    """
    def __init__(self, path, mode):
        Exception.__init__(self)
        self.path = path
        self.mode = mode

    def __str__(self):
        return '%s: unsupported S_IFMT %07o' % (self.path, self.mode)


class RealFileInfo(FileInfo):
    """Object to track important-to-packaging file information.

    This currently handles regular files, directories, and symbolic links.

    For multiple RealFileInfo objects with identical hardkeys, there
    is no way to determine which of the hard links should be
    delivered as a file, and which as hardlinks.
    """

    def __init__(self, root=None, path=None):
        FileInfo.__init__(self)
        self.path = path
        path = os.path.join(root, path)
        lstat = os.lstat(path)
        mode = lstat.st_mode

        #
        # Per stat.py, these cases are mutually exclusive.
        #
        if stat.S_ISREG(mode):
            self.hash = self.path
        elif stat.S_ISDIR(mode):
            self.isdir = True
        elif stat.S_ISLNK(mode):
            self.target = os.path.normpath(os.readlink(path))
            self.mediator = None
        else:
            raise UnsupportedFileFormatError(path, mode)

        if not stat.S_ISLNK(mode):
            self.mode = "%04o" % stat.S_IMODE(mode)
            #
            # Instead of reading the group and owner from the proto area after
            # a non-root build, just drop in dummy values.  Since we don't
            # compare them anywhere, this should allow at least marginally
            # useful comparisons of protolist-style output.
            #
            self.owner = "owner"
            self.group = "group"

        #
        # refcount > 1 indicates a hard link
        #
        if lstat.st_nlink > 1:
            #
            # This could get ugly if multiple proto areas reside
            # on different filesystems.
            #
            self.hardkey = lstat.st_ino


class DirectoryTree(dict):
    """Meant to be subclassed according to population method.
    """
    def __init__(self, name):
        dict.__init__(self)
        self.name = name

    def compare(self, other):
        """Compare two different sets of FileInfo objects.
        """
        keys1 = frozenset(list(self.keys()))
        keys2 = frozenset(list(other.keys()))

        common = keys1.intersection(keys2)
        onlykeys1 = keys1.difference(common)
        onlykeys2 = keys2.difference(common)

        if onlykeys1:
            print("Entries present in %s but not %s:" % \
                (self.name, other.name))
            for path in sorted(onlykeys1):
                print(("\t%s" % str(self[path])))
            print("")

        if onlykeys2:
            print("Entries present in %s but not %s:" % \
                (other.name, self.name))
            for path in sorted(onlykeys2):
                print(("\t%s" % str(other[path])))
            print("")

        nodifferences = True
        for path in sorted(common):
            if self[path] != other[path]:
                if nodifferences:
                    nodifferences = False
                    print("Entries that differ between %s and %s:" \
                        % (self.name, other.name))
                print(("%14s %s" % (self.name, self[path])))
                print(("%14s %s" % (other.name, other[path])))
        if not nodifferences:
            print("")


class BadProtolistFormat(Exception):
    """This means that the user supplied a file via -l, but at least
    one line from that file doesn't have the right number of fields to
    parse as protolist output.
    """
    def __str__(self):
        return 'bad proto list entry: "%s"' % Exception.__str__(self)


class ProtoTree(DirectoryTree):
    """Describes one or more proto directories as a dictionary of
    RealFileInfo objects, indexed by relative path.
    """

    def adddir(self, proto, exceptions):
        """Extends the ProtoTree dictionary with RealFileInfo
        objects describing the proto dir, indexed by relative
        path.
        """
        newentries = {}

        pdir = os.path.normpath(proto)
        strippdir = lambda r, n: os.path.join(r, n)[len(pdir)+1:]
        for root, dirs, files in os.walk(pdir):
            for name in dirs + files:
                path = strippdir(root, name)
                if path not in exceptions:
                    try:
                        newentries[path] = RealFileInfo(pdir, path)
                    except OSError as e:
                        sys.stderr.write("Warning: unable to stat %s: %s\n" %
                            (path, e))
                        continue
                else:
                    exceptions.remove(path)
                    if name in dirs:
                        dirs.remove(name)

        #
        # Find the sets of paths in this proto dir that are hardlinks
        # to the same inode.
        #
        # It seems wasteful to store this in each FileInfo, but we
        # otherwise need a linking mechanism.  With this information
        # here, FileInfo object comparison can be self contained.
        #
        # We limit this aggregation to a single proto dir, as
        # represented by newentries.  That means we don't need to care
        # about proto dirs on separate filesystems, or about hardlinks
        # that cross proto dir boundaries.
        #
        hk2path = {}
        for path, fileinfo in newentries.items():
            if fileinfo.hardkey:
                hk2path.setdefault(fileinfo.hardkey, set()).add(path)
        for fileinfo in newentries.values():
            if fileinfo.hardkey:
                fileinfo.hardpaths.update(hk2path[fileinfo.hardkey])
        self.update(newentries)

    def addprotolist(self, protolist, exceptions):
        """Read in the specified file, assumed to be the
        output of protolist.

        This has been tested minimally, and is potentially useful for
        comparing across the transition period, but should ultimately
        go away.
        """

        try:
            plist = open(protolist)
        except IOError as exc:
            raise IOError("cannot open proto list: %s" % str(exc))

        newentries = {}

        for pline in plist:
            pline = pline.split()
            #
            # Use a FileInfo() object instead of a RealFileInfo()
            # object because we want to avoid the RealFileInfo
            # constructor, because there's nothing to actually stat().
            #
            fileinfo = FileInfo()
            try:
                if pline[1] in exceptions:
                    exceptions.remove(pline[1])
                    continue
                if pline[0] == "d":
                    fileinfo.isdir = True
                fileinfo.path = pline[1]
                if pline[2] != "-":
                    fileinfo.target = os.path.normpath(pline[2])
                fileinfo.mode = int("0%s" % pline[3])
                fileinfo.owner = pline[4]
                fileinfo.group = pline[5]
                if pline[6] != "0":
                    fileinfo.hardkey = pline[6]
                newentries[pline[1]] = fileinfo
            except IndexError:
                raise BadProtolistFormat(pline)

        plist.close()
        hk2path = {}
        for path, fileinfo in newentries.items():
            if fileinfo.hardkey:
                hk2path.setdefault(fileinfo.hardkey, set()).add(path)
        for fileinfo in newentries.values():
            if fileinfo.hardkey:
                fileinfo.hardpaths.update(hk2path[fileinfo.hardkey])
        self.update(newentries)


class ManifestParsingError(Exception):
    """This means that the Manifest.set_content() raised an
    ActionError.  We raise this, instead, to tell us which manifest
    could not be parsed, rather than what action error we hit.
    """
    def __init__(self, mfile, error):
        Exception.__init__(self)
        self.mfile = mfile
        self.error = error

    def __str__(self):
        return "unable to parse manifest %s: %s" % (self.mfile, self.error)

class ManifestTree(DirectoryTree):
    """Describes one or more directories containing arbitrarily
    many manifests as a dictionary of ActionInfo objects, indexed
    by the relative path of the data source within the proto area.
    That path may or may not be the same as the path attribute of the
    given action.
    """

    def addmanifest(self, root, mfile, arch, modechecks, exceptions):
        """Treats the specified input file as a pkg(7) package
        manifest, and extends the ManifestTree dictionary with entries
        for the actions therein.
        """
        mfest = manifest.Manifest()
        try:
            mfest.set_content(open(os.path.join(root, mfile)).read())
        except IOError as exc:
            raise IOError("cannot read manifest: %s" % str(exc))
        except actions.ActionError as exc:
            raise ManifestParsingError(mfile, str(exc))

        #
        # Make sure the manifest is applicable to the user-specified
        # architecture.  Assumption: if variant.arch is not an
        # attribute of the manifest, then the package should be
        # installed on all architectures.
        #
        if arch not in mfest.attributes.get("variant.arch", (arch,)):
            return

        modewarnings = set()
        for action in mfest.gen_actions():
            if "path" not in action.attrs or \
                not ActionInfo.supported(action.name):
                continue

            #
            # The dir action is currently fully specified, in that it
            # lists owner, group, and mode attributes.  If that
            # changes in pkg(7) code, we'll need to revisit either this
            # code or the ActionInfo() constructor.  It's possible
            # that the pkg(7) system could be extended to provide a
            # mechanism for specifying directory permissions outside
            # of the individual manifests that deliver files into
            # those directories.  Doing so at time of manifest
            # processing would mean that validate_pkg continues to work,
            # but doing so at time of publication would require updates.
            #

            #
            # See pkgsend(1) for the use of NOHASH for objects with
            # datastreams.  Currently, that means "files," but this
            # should work for any other such actions.
            #
            if getattr(action, "hash", "NOHASH") != "NOHASH":
                path = action.hash
            else:
                path = action.attrs["path"]

            #
            # This is the wrong tool in which to enforce consistency
            # on a set of manifests.  So instead of comparing the
            # different actions with the same "path" attribute, we
            # use the first one.
            #
            if path in self:
                continue

            #
            # As with the manifest itself, if an action has specified
            # variant.arch, we look for the target architecture
            # therein.
            #
            var = None

            #
            # The name of this method changed in pkg(7) build 150, we need to
            # work with both sets.
            #
            if hasattr(action, 'get_variants'):
                var = action.get_variants()
            else:
                var = action.get_variant_template()
            if "variant.arch" in var and arch not in var["variant.arch"]:
                return

            try:
                self[path] = ActionInfo(action)
            except ActionInfoError as e:
                sys.stderr.write("warning: %s\n" % str(e))

            if modechecks is not None and path not in exceptions:
                modewarnings.update(self[path].checkmodes(modechecks))

        if len(modewarnings) > 0:
            print("warning: unsafe permissions in %s" % mfile)
            for w in sorted(modewarnings):
                print(w)
            print("")

    def adddir(self, mdir, arch, modechecks, exceptions):
        """Walks the specified directory looking for pkg(7) manifests.
        """
        for mfile in os.listdir(mdir):
            if (mfile.endswith(".mog") and
                stat.S_ISREG(os.lstat(os.path.join(mdir, mfile)).st_mode)):
                try:
                    self.addmanifest(mdir, mfile, arch, modechecks, exceptions)
                except IOError as exc:
                    sys.stderr.write("warning: %s\n" % str(exc))

    def resolvehardlinks(self):
        """Populates mode, group, and owner for resolved (ie link target
        is present in the manifest tree) hard links.
        """
        for info in list(self.values()):
            if info.name() == "hardlink":
                tgt = info.hardkey
                if tgt in self:
                    tgtinfo = self[tgt]
                    info.owner = tgtinfo.owner
                    info.group = tgtinfo.group
                    info.mode = tgtinfo.mode

class ExceptionList(set):
    """Keep track of an exception list as a set of paths to be excluded
    from any other lists we build.
    """

    def __init__(self, files, arch):
        set.__init__(self)
        for fname in files:
            try:
                self.readexceptionfile(fname, arch)
            except IOError as exc:
                sys.stderr.write("warning: cannot read exception file: %s\n" %
                    str(exc))

    def readexceptionfile(self, efile, arch):
        """Build a list of all pathnames from the specified file that
        either apply to all architectures (ie which have no trailing
        architecture tokens), or to the specified architecture (ie
        which have the value of the arch arg as a trailing
        architecture token.)
        """

        excfile = open(efile)

        for exc in excfile:
            exc = exc.split()
            if len(exc) and exc[0][0] != "#":
                if arch in (exc[1:] or arch):
                    self.add(os.path.normpath(exc[0]))

        excfile.close()


USAGE = """%s [-v] -a arch [-e exceptionfile]... [-L|-M [-X check]...] input_1 [input_2]

where input_1 and input_2 may specify proto lists, proto areas,
or manifest directories.  For proto lists, use one or more

    -l file

arguments.  For proto areas, use one or more

    -p dir

arguments.  For manifest directories, use one or more

    -m dir

arguments.

If -L or -M is specified, then only one input source is allowed, and
it should be one or more manifest directories.  These two options are
mutually exclusive.

The -L option is used to generate a proto list to stdout.

The -M option is used to check for safe file and directory modes.
By default, this causes all mode checks to be performed.  Individual
mode checks may be turned off using "-X check," where "check" comes
from the following set of checks:

    m   check for group or other write permissions
    w   check for user write permissions on files and directories
        not owned by root
    s   check for group/other read permission on executable files
        that have setuid/setgid bit(s)
    o   check for files that could be safely owned by root
""" % sys.argv[0]


def usage(msg=None):
    """Try to give the user useful information when they don't get the
    command syntax right.
    """
    if msg:
        sys.stderr.write("%s: %s\n" % (sys.argv[0], msg))
    sys.stderr.write(USAGE)
    sys.exit(2)


def main(argv):
    """Compares two out of three possible data sources: a proto list, a
    set of proto areas, and a set of manifests.
    """
    try:
        opts, args = getopt.getopt(argv, 'a:e:Ll:Mm:p:vX:')
    except getopt.GetoptError as exc:
        usage(str(exc))

    if args:
        usage()

    arch = None
    exceptionlists = []
    listonly = False
    manifestdirs = []
    manifesttree = ManifestTree("manifests")
    protodirs = []
    prototree = ProtoTree("proto area")
    protolists = []
    protolist = ProtoTree("proto list")
    modechecks = set()
    togglemodechecks = set()
    trees = []
    comparing = set()
    verbose = False

    for opt, arg in opts:
        if opt == "-a":
            if arch:
                usage("may only specify one architecture")
            else:
                arch = arg
        elif opt == "-e":
            exceptionlists.append(arg)
        elif opt == "-L":
            listonly = True
        elif opt == "-l":
            comparing.add("protolist")
            protolists.append(os.path.normpath(arg))
        elif opt == "-M":
            modechecks.update(DEFAULTMODECHECKS)
        elif opt == "-m":
            comparing.add("manifests")
            manifestdirs.append(os.path.normpath(arg))
        elif opt == "-p":
            comparing.add("proto area")
            protodirs.append(os.path.normpath(arg))
        elif opt == "-v":
            verbose = True
        elif opt == "-X":
            togglemodechecks.add(arg)

    if listonly or len(modechecks) > 0:
        if len(comparing) != 1 or "manifests" not in comparing:
            usage("-L and -M require one or more -m args, and no -l or -p")
        if listonly and len(modechecks) > 0:
            usage("-L and -M are mutually exclusive")
    elif len(comparing) != 2:
        usage("must specify exactly two of -l, -m, and -p")

    if len(togglemodechecks) > 0 and len(modechecks) == 0:
        usage("-X requires -M")

    for s in togglemodechecks:
        if s not in ALLMODECHECKS:
            usage("unknown mode check %s" % s)
        modechecks.symmetric_difference_update((s))

    if len(modechecks) == 0:
        modechecks = None

    if not arch:
        usage("must specify architecture")

    exceptions = ExceptionList(exceptionlists, arch)
    originalexceptions = exceptions.copy()

    if len(manifestdirs) > 0:
        for mdir in manifestdirs:
            manifesttree.adddir(mdir, arch, modechecks, exceptions)
        if listonly:
            manifesttree.resolvehardlinks()
            for info in list(manifesttree.values()):
                print("%s" % info.protostr())
            sys.exit(0)
        if modechecks is not None:
            sys.exit(0)
        trees.append(manifesttree)

    if len(protodirs) > 0:
        for pdir in protodirs:
            prototree.adddir(pdir, exceptions)
        trees.append(prototree)

    if len(protolists) > 0:
        for plist in protolists:
            try:
                protolist.addprotolist(plist, exceptions)
            except IOError as exc:
                sys.stderr.write("warning: %s\n" % str(exc))
        trees.append(protolist)

    if verbose and exceptions:
        print("Entries present in exception list but missing from proto area:")
        for exc in sorted(exceptions):
            print("\t%s" % exc)
        print("")

    usedexceptions = originalexceptions.difference(exceptions)
    harmfulexceptions = usedexceptions.intersection(manifesttree)
    if harmfulexceptions:
        print("Entries present in exception list but also in manifests:")
        for exc in sorted(harmfulexceptions):
            print("\t%s" % exc)
            del manifesttree[exc]
        print("")

    trees[0].compare(trees[1])

if __name__ == '__main__':
    locale.setlocale(locale.LC_ALL, "")
    gettext.install("pkg", "/usr/share/locale")

    try:
        main(sys.argv[1:])
    except KeyboardInterrupt:
        sys.exit(1)
    except IOError:
        sys.exit(1)