xref: /titanic_52/usr/src/tools/scripts/validate_pkg.py (revision 93be19b94b8b631e9abb3b71f9415817c441c051)
1*93be19b9SAndy Fiddaman#!@TOOLS_PYTHON@
2ead1f93eSLiane Praza#
3ead1f93eSLiane Praza# CDDL HEADER START
4ead1f93eSLiane Praza#
5ead1f93eSLiane Praza# The contents of this file are subject to the terms of the
6ead1f93eSLiane Praza# Common Development and Distribution License (the "License").
7ead1f93eSLiane Praza# You may not use this file except in compliance with the License.
8ead1f93eSLiane Praza#
9ead1f93eSLiane Praza# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10ead1f93eSLiane Praza# or http://www.opensolaris.org/os/licensing.
11ead1f93eSLiane Praza# See the License for the specific language governing permissions
12ead1f93eSLiane Praza# and limitations under the License.
13ead1f93eSLiane Praza#
14ead1f93eSLiane Praza# When distributing Covered Code, include this CDDL HEADER in each
15ead1f93eSLiane Praza# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16ead1f93eSLiane Praza# If applicable, add the following below this CDDL HEADER, with the
17ead1f93eSLiane Praza# fields enclosed by brackets "[]" replaced with your own identifying
18ead1f93eSLiane Praza# information: Portions Copyright [yyyy] [name of copyright owner]
19ead1f93eSLiane Praza#
20ead1f93eSLiane Praza# CDDL HEADER END
21ead1f93eSLiane Praza#
22ead1f93eSLiane Praza
23ead1f93eSLiane Praza#
24ead1f93eSLiane Praza# Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
25ead1f93eSLiane Praza# Use is subject to license terms.
26ead1f93eSLiane Praza#
27ead1f93eSLiane Praza
28*93be19b9SAndy Fiddaman# Copyright 2018 OmniOS Community Edition (OmniOSce) Association.
29*93be19b9SAndy Fiddaman
30ead1f93eSLiane Praza#
31ead1f93eSLiane Praza# Compare the content generated by a build to a set of manifests
32ead1f93eSLiane Praza# describing how that content is to be delivered.
33ead1f93eSLiane Praza#
34ead1f93eSLiane Praza
35ead1f93eSLiane Praza
36ead1f93eSLiane Prazaimport getopt
37ead1f93eSLiane Prazaimport os
38ead1f93eSLiane Prazaimport stat
39ead1f93eSLiane Prazaimport sys
40ead1f93eSLiane Praza
41ead1f93eSLiane Prazafrom pkg import actions
42ead1f93eSLiane Prazafrom pkg import manifest
43ead1f93eSLiane Praza
44ead1f93eSLiane Praza
45ead1f93eSLiane Praza#
46ead1f93eSLiane Praza# Dictionary used to map action names to output format.  Each entry is
47ead1f93eSLiane Praza# indexed by action name, and consists of a list of tuples that map
48ead1f93eSLiane Praza# FileInfo class members to output labels.
49ead1f93eSLiane Praza#
50ead1f93eSLiane PrazaOUTPUTMAP = {
51ead1f93eSLiane Praza    "dir": [
52ead1f93eSLiane Praza        ("group", "group="),
53ead1f93eSLiane Praza        ("mode", "mode="),
54ead1f93eSLiane Praza        ("owner", "owner="),
55ead1f93eSLiane Praza        ("path", "path=")
56ead1f93eSLiane Praza    ],
57ead1f93eSLiane Praza    "file": [
58ead1f93eSLiane Praza        ("hash", ""),
59ead1f93eSLiane Praza        ("group", "group="),
60ead1f93eSLiane Praza        ("mode", "mode="),
61ead1f93eSLiane Praza        ("owner", "owner="),
62ead1f93eSLiane Praza        ("path", "path=")
63ead1f93eSLiane Praza    ],
64ead1f93eSLiane Praza    "link": [
653be32116SAlexander Pyhalov        ("mediator", "mediator="),
66ead1f93eSLiane Praza        ("path", "path="),
67ead1f93eSLiane Praza        ("target", "target=")
68ead1f93eSLiane Praza    ],
69ead1f93eSLiane Praza    "hardlink": [
70ead1f93eSLiane Praza        ("path", "path="),
71ead1f93eSLiane Praza        ("hardkey", "target=")
72ead1f93eSLiane Praza    ],
73ead1f93eSLiane Praza}
74ead1f93eSLiane Praza
75ead1f93eSLiane Praza# Mode checks used to validate safe file and directory permissions
76ead1f93eSLiane PrazaALLMODECHECKS = frozenset(("m", "w", "s", "o"))
77ead1f93eSLiane PrazaDEFAULTMODECHECKS = frozenset(("m", "w", "o"))
78ead1f93eSLiane Praza
79ead1f93eSLiane Prazaclass FileInfo(object):
80ead1f93eSLiane Praza    """Base class to represent a file.
81ead1f93eSLiane Praza
82ead1f93eSLiane Praza    Subclassed according to whether the file represents an actual filesystem
83ead1f93eSLiane Praza    object (RealFileInfo) or an IPS manifest action (ActionInfo).
84ead1f93eSLiane Praza    """
85ead1f93eSLiane Praza
86ead1f93eSLiane Praza    def __init__(self):
87ead1f93eSLiane Praza        self.path = None
88ead1f93eSLiane Praza        self.isdir = False
89ead1f93eSLiane Praza        self.target = None
90ead1f93eSLiane Praza        self.owner = None
91ead1f93eSLiane Praza        self.group = None
92ead1f93eSLiane Praza        self.mode = None
93ead1f93eSLiane Praza        self.hardkey = None
94ead1f93eSLiane Praza        self.hardpaths = set()
95ead1f93eSLiane Praza        self.editable = False
96ead1f93eSLiane Praza
97ead1f93eSLiane Praza    def name(self):
98ead1f93eSLiane Praza        """Return the IPS action name of a FileInfo object.
99ead1f93eSLiane Praza        """
100ead1f93eSLiane Praza        if self.isdir:
101ead1f93eSLiane Praza            return "dir"
102ead1f93eSLiane Praza
103ead1f93eSLiane Praza        if self.target:
104ead1f93eSLiane Praza            return "link"
105ead1f93eSLiane Praza
106ead1f93eSLiane Praza        if self.hardkey:
107ead1f93eSLiane Praza            return "hardlink"
108ead1f93eSLiane Praza
109ead1f93eSLiane Praza        return "file"
110ead1f93eSLiane Praza
111ead1f93eSLiane Praza    def checkmodes(self, modechecks):
112ead1f93eSLiane Praza        """Check for and report on unsafe permissions.
113ead1f93eSLiane Praza
114ead1f93eSLiane Praza        Returns a potentially empty list of warning strings.
115ead1f93eSLiane Praza        """
116ead1f93eSLiane Praza        w = []
117ead1f93eSLiane Praza
118ead1f93eSLiane Praza        t = self.name()
119ead1f93eSLiane Praza        if t in ("link", "hardlink"):
120ead1f93eSLiane Praza            return w
121ead1f93eSLiane Praza        m = int(self.mode, 8)
122ead1f93eSLiane Praza        o = self.owner
123ead1f93eSLiane Praza        p = self.path
124ead1f93eSLiane Praza
125ead1f93eSLiane Praza        if "s" in modechecks and t == "file":
126ead1f93eSLiane Praza            if m & (stat.S_ISUID | stat.S_ISGID):
127ead1f93eSLiane Praza                if m & (stat.S_IRGRP | stat.S_IROTH):
128ead1f93eSLiane Praza                    w.extend(["%s: 0%o: setuid/setgid file should not be " \
129ead1f93eSLiane Praza                        "readable by group or other" % (p, m)])
130ead1f93eSLiane Praza
131ead1f93eSLiane Praza        if "o" in modechecks and o != "root" and ((m & stat.S_ISUID) == 0):
132ead1f93eSLiane Praza            mu = (m & stat.S_IRWXU) >> 6
133ead1f93eSLiane Praza            mg = (m & stat.S_IRWXG) >> 3
134ead1f93eSLiane Praza            mo = m & stat.S_IRWXO
135ead1f93eSLiane Praza            e = self.editable
136ead1f93eSLiane Praza
137*93be19b9SAndy Fiddaman            if (((mu & 0o2) == 0 and (mo & mg & 0o4) == 0o4) or
138*93be19b9SAndy Fiddaman                (t == "file" and mo & 0o1 == 1) or
139ead1f93eSLiane Praza                (mg, mo) == (mu, mu) or
140ead1f93eSLiane Praza                ((t == "file" and not e or t == "dir" and o == "bin") and
141*93be19b9SAndy Fiddaman                (mg & 0o5 == mo & 0o5)) or
142*93be19b9SAndy Fiddaman                (t == "file" and o == "bin" and mu & 0o1 == 0o1) or
143*93be19b9SAndy Fiddaman                (m & 0o105 != 0 and p.startswith("etc/security/dev/"))):
144ead1f93eSLiane Praza                w.extend(["%s: owner \"%s\" may be safely " \
145ead1f93eSLiane Praza                    "changed to \"root\"" % (p, o)])
146ead1f93eSLiane Praza
147ead1f93eSLiane Praza        if "w" in modechecks and t == "file" and o != "root":
148ead1f93eSLiane Praza            uwx = stat.S_IWUSR | stat.S_IXUSR
149ead1f93eSLiane Praza            if m & uwx == uwx:
150ead1f93eSLiane Praza                w.extend(["%s: non-root-owned executable should not " \
151ead1f93eSLiane Praza                    "also be writable by owner." % p])
152ead1f93eSLiane Praza
153ead1f93eSLiane Praza        if ("m" in modechecks and
154ead1f93eSLiane Praza            m & (stat.S_IWGRP | stat.S_IWOTH) != 0 and
155ead1f93eSLiane Praza            m & stat.S_ISVTX == 0):
156ead1f93eSLiane Praza            w.extend(["%s: 0%o: should not be writable by group or other" %
157ead1f93eSLiane Praza                (p, m)])
158ead1f93eSLiane Praza
159ead1f93eSLiane Praza        return w
160ead1f93eSLiane Praza
161ead1f93eSLiane Praza    def __ne__(self, other):
162ead1f93eSLiane Praza        """Compare two FileInfo objects.
163ead1f93eSLiane Praza
164ead1f93eSLiane Praza        Note this is the "not equal" comparison, so a return value of False
165ead1f93eSLiane Praza        indicates that the objects are functionally equivalent.
166ead1f93eSLiane Praza        """
167ead1f93eSLiane Praza        #
168ead1f93eSLiane Praza        # Map the objects such that the lhs is always the ActionInfo,
169ead1f93eSLiane Praza        # and the rhs is always the RealFileInfo.
170ead1f93eSLiane Praza        #
171ead1f93eSLiane Praza        # It's only really important that the rhs not be an
172ead1f93eSLiane Praza        # ActionInfo; if we're comparing FileInfo the RealFileInfo, it
173ead1f93eSLiane Praza        # won't actually matter what we choose.
174ead1f93eSLiane Praza        #
175ead1f93eSLiane Praza        if isinstance(self, ActionInfo):
176ead1f93eSLiane Praza            lhs = self
177ead1f93eSLiane Praza            rhs = other
178ead1f93eSLiane Praza        else:
179ead1f93eSLiane Praza            lhs = other
180ead1f93eSLiane Praza            rhs = self
181ead1f93eSLiane Praza
182ead1f93eSLiane Praza        #
183ead1f93eSLiane Praza        # Because the manifest may legitimately translate a relative
184ead1f93eSLiane Praza        # path from the proto area into a different path on the installed
185ead1f93eSLiane Praza        # system, we don't compare paths here.  We only expect this comparison
186ead1f93eSLiane Praza        # to be invoked on items with identical relative paths in
187ead1f93eSLiane Praza        # first place.
188ead1f93eSLiane Praza        #
189ead1f93eSLiane Praza
190ead1f93eSLiane Praza        #
191ead1f93eSLiane Praza        # All comparisons depend on type.  For symlink and directory, they
192ead1f93eSLiane Praza        # must be the same.  For file and hardlink, see below.
193ead1f93eSLiane Praza        #
194ead1f93eSLiane Praza        typelhs = lhs.name()
195ead1f93eSLiane Praza        typerhs = rhs.name()
196ead1f93eSLiane Praza        if typelhs in ("link", "dir"):
197ead1f93eSLiane Praza            if typelhs != typerhs:
198ead1f93eSLiane Praza                return True
199ead1f93eSLiane Praza
200ead1f93eSLiane Praza        #
201ead1f93eSLiane Praza        # For symlinks, all that's left is the link target.
2023be32116SAlexander Pyhalov        # For mediated symlinks targets can differ.
203ead1f93eSLiane Praza        #
204ead1f93eSLiane Praza        if typelhs == "link":
2053be32116SAlexander Pyhalov            return (lhs.mediator is None) and (lhs.target != rhs.target)
206ead1f93eSLiane Praza
207ead1f93eSLiane Praza        #
208ead1f93eSLiane Praza        # For a directory, it's important that both be directories,
209ead1f93eSLiane Praza        # the modes be identical, and the paths are identical.  We already
210ead1f93eSLiane Praza        # checked all but the modes above.
211ead1f93eSLiane Praza        #
212ead1f93eSLiane Praza        # If both objects are files, then we're in the same boat.
213ead1f93eSLiane Praza        #
214ead1f93eSLiane Praza        if typelhs == "dir" or (typelhs == "file" and typerhs == "file"):
215ead1f93eSLiane Praza            return lhs.mode != rhs.mode
216ead1f93eSLiane Praza
217ead1f93eSLiane Praza        #
218ead1f93eSLiane Praza        # For files or hardlinks:
219ead1f93eSLiane Praza        #
220ead1f93eSLiane Praza        # Since the key space is different (inodes for real files and
221ead1f93eSLiane Praza        # actual link targets for hard links), and since the proto area will
222ead1f93eSLiane Praza        # identify all N occurrences as hardlinks, but the manifests as one
223ead1f93eSLiane Praza        # file and N-1 hardlinks, we have to compare files to hardlinks.
224ead1f93eSLiane Praza        #
225ead1f93eSLiane Praza
226ead1f93eSLiane Praza        #
227ead1f93eSLiane Praza        # If they're both hardlinks, we just make sure that
228ead1f93eSLiane Praza        # the same target path appears in both sets of
229ead1f93eSLiane Praza        # possible targets.
230ead1f93eSLiane Praza        #
231ead1f93eSLiane Praza        if typelhs == "hardlink" and typerhs == "hardlink":
232ead1f93eSLiane Praza            return len(lhs.hardpaths.intersection(rhs.hardpaths)) == 0
233ead1f93eSLiane Praza
234ead1f93eSLiane Praza        #
235ead1f93eSLiane Praza        # Otherwise, we have a mix of file and hardlink, so we
236ead1f93eSLiane Praza        # need to make sure that the file path appears in the
237ead1f93eSLiane Praza        # set of possible target paths for the hardlink.
238ead1f93eSLiane Praza        #
239ead1f93eSLiane Praza        # We already know that the ActionInfo, if present, is the lhs
240ead1f93eSLiane Praza        # operator.  So it's the rhs operator that's guaranteed to
241ead1f93eSLiane Praza        # have a set of hardpaths.
242ead1f93eSLiane Praza        #
243ead1f93eSLiane Praza        return lhs.path not in rhs.hardpaths
244ead1f93eSLiane Praza
245ead1f93eSLiane Praza    def __str__(self):
246ead1f93eSLiane Praza        """Return an action-style representation of a FileInfo object.
247ead1f93eSLiane Praza
248ead1f93eSLiane Praza        We don't currently quote items with embedded spaces.  If we
249ead1f93eSLiane Praza        ever decide to parse this output, we'll want to revisit that.
250ead1f93eSLiane Praza        """
251ead1f93eSLiane Praza        name = self.name()
252ead1f93eSLiane Praza        out = name
253ead1f93eSLiane Praza
254ead1f93eSLiane Praza        for member, label in OUTPUTMAP[name]:
255ead1f93eSLiane Praza            out += " " + label + str(getattr(self, member))
256ead1f93eSLiane Praza
257ead1f93eSLiane Praza        return out
258ead1f93eSLiane Praza
259ead1f93eSLiane Praza    def protostr(self):
260ead1f93eSLiane Praza        """Return a protolist-style representation of a FileInfo object.
261ead1f93eSLiane Praza        """
262ead1f93eSLiane Praza        target = "-"
263ead1f93eSLiane Praza        major = "-"
264ead1f93eSLiane Praza        minor = "-"
265ead1f93eSLiane Praza
266ead1f93eSLiane Praza        mode = self.mode
267ead1f93eSLiane Praza        owner = self.owner
268ead1f93eSLiane Praza        group = self.group
269ead1f93eSLiane Praza
270ead1f93eSLiane Praza        name = self.name()
271ead1f93eSLiane Praza        if name == "dir":
272ead1f93eSLiane Praza            ftype = "d"
273ead1f93eSLiane Praza        elif name in ("file", "hardlink"):
274ead1f93eSLiane Praza            ftype = "f"
275ead1f93eSLiane Praza        elif name == "link":
276ead1f93eSLiane Praza            ftype = "s"
277ead1f93eSLiane Praza            target = self.target
278ead1f93eSLiane Praza            mode = "777"
279ead1f93eSLiane Praza            owner = "root"
280ead1f93eSLiane Praza            group = "other"
281ead1f93eSLiane Praza
282ead1f93eSLiane Praza        out = "%c %-30s %-20s %4s %-5s %-5s %6d %2ld  -  -" % \
283ead1f93eSLiane Praza            (ftype, self.path, target, mode, owner, group, 0, 1)
284ead1f93eSLiane Praza
285ead1f93eSLiane Praza        return out
286ead1f93eSLiane Praza
287ead1f93eSLiane Praza
288ead1f93eSLiane Prazaclass ActionInfo(FileInfo):
289ead1f93eSLiane Praza    """Object to track information about manifest actions.
290ead1f93eSLiane Praza
291ead1f93eSLiane Praza    This currently understands file, link, dir, and hardlink actions.
292ead1f93eSLiane Praza    """
293ead1f93eSLiane Praza
294ead1f93eSLiane Praza    def __init__(self, action):
295ead1f93eSLiane Praza        FileInfo.__init__(self)
296ead1f93eSLiane Praza        #
297ead1f93eSLiane Praza        # Currently, all actions that we support have a "path"
298ead1f93eSLiane Praza        # attribute.  If that changes, then we'll need to
299ead1f93eSLiane Praza        # catch a KeyError from this assignment.
300ead1f93eSLiane Praza        #
301ead1f93eSLiane Praza        self.path = action.attrs["path"]
302ead1f93eSLiane Praza
303ead1f93eSLiane Praza        if action.name == "file":
304ead1f93eSLiane Praza            self.owner = action.attrs["owner"]
305ead1f93eSLiane Praza            self.group = action.attrs["group"]
306ead1f93eSLiane Praza            self.mode = action.attrs["mode"]
307ead1f93eSLiane Praza            self.hash = action.hash
308ead1f93eSLiane Praza            if "preserve" in action.attrs:
309ead1f93eSLiane Praza                self.editable = True
310ead1f93eSLiane Praza        elif action.name == "link":
311ead1f93eSLiane Praza            target = action.attrs["target"]
312ead1f93eSLiane Praza            self.target = os.path.normpath(target)
3133be32116SAlexander Pyhalov            self.mediator = action.attrs.get("mediator")
314ead1f93eSLiane Praza        elif action.name == "dir":
315ead1f93eSLiane Praza            self.owner = action.attrs["owner"]
316ead1f93eSLiane Praza            self.group = action.attrs["group"]
317ead1f93eSLiane Praza            self.mode = action.attrs["mode"]
318ead1f93eSLiane Praza            self.isdir = True
319ead1f93eSLiane Praza        elif action.name == "hardlink":
320ead1f93eSLiane Praza            target = os.path.normpath(action.get_target_path())
321ead1f93eSLiane Praza            self.hardkey = target
322ead1f93eSLiane Praza            self.hardpaths.add(target)
323ead1f93eSLiane Praza
324ead1f93eSLiane Praza    @staticmethod
325ead1f93eSLiane Praza    def supported(action):
326ead1f93eSLiane Praza        """Indicates whether the specified IPS action time is
327ead1f93eSLiane Praza        correctly handled by the ActionInfo constructor.
328ead1f93eSLiane Praza        """
329ead1f93eSLiane Praza        return action in frozenset(("file", "dir", "link", "hardlink"))
330ead1f93eSLiane Praza
331ead1f93eSLiane Praza
332ead1f93eSLiane Prazaclass UnsupportedFileFormatError(Exception):
333ead1f93eSLiane Praza    """This means that the stat.S_IFMT returned something we don't
334ead1f93eSLiane Praza    support, ie a pipe or socket.  If it's appropriate for such an
335ead1f93eSLiane Praza    object to be in the proto area, then the RealFileInfo constructor
336ead1f93eSLiane Praza    will need to evolve to support it, or it will need to be in the
337ead1f93eSLiane Praza    exception list.
338ead1f93eSLiane Praza    """
339ead1f93eSLiane Praza    def __init__(self, path, mode):
340ead1f93eSLiane Praza        Exception.__init__(self)
341ead1f93eSLiane Praza        self.path = path
342ead1f93eSLiane Praza        self.mode = mode
343ead1f93eSLiane Praza
344ead1f93eSLiane Praza    def __str__(self):
345ead1f93eSLiane Praza        return '%s: unsupported S_IFMT %07o' % (self.path, self.mode)
346ead1f93eSLiane Praza
347ead1f93eSLiane Praza
348ead1f93eSLiane Prazaclass RealFileInfo(FileInfo):
349ead1f93eSLiane Praza    """Object to track important-to-packaging file information.
350ead1f93eSLiane Praza
351ead1f93eSLiane Praza    This currently handles regular files, directories, and symbolic links.
352ead1f93eSLiane Praza
353ead1f93eSLiane Praza    For multiple RealFileInfo objects with identical hardkeys, there
354ead1f93eSLiane Praza    is no way to determine which of the hard links should be
355ead1f93eSLiane Praza    delivered as a file, and which as hardlinks.
356ead1f93eSLiane Praza    """
357ead1f93eSLiane Praza
358ead1f93eSLiane Praza    def __init__(self, root=None, path=None):
359ead1f93eSLiane Praza        FileInfo.__init__(self)
360ead1f93eSLiane Praza        self.path = path
361ead1f93eSLiane Praza        path = os.path.join(root, path)
362ead1f93eSLiane Praza        lstat = os.lstat(path)
363ead1f93eSLiane Praza        mode = lstat.st_mode
364ead1f93eSLiane Praza
365ead1f93eSLiane Praza        #
366ead1f93eSLiane Praza        # Per stat.py, these cases are mutually exclusive.
367ead1f93eSLiane Praza        #
368ead1f93eSLiane Praza        if stat.S_ISREG(mode):
369ead1f93eSLiane Praza            self.hash = self.path
370ead1f93eSLiane Praza        elif stat.S_ISDIR(mode):
371ead1f93eSLiane Praza            self.isdir = True
372ead1f93eSLiane Praza        elif stat.S_ISLNK(mode):
373ead1f93eSLiane Praza            self.target = os.path.normpath(os.readlink(path))
3743be32116SAlexander Pyhalov            self.mediator = None
375ead1f93eSLiane Praza        else:
376ead1f93eSLiane Praza            raise UnsupportedFileFormatError(path, mode)
377ead1f93eSLiane Praza
378ead1f93eSLiane Praza        if not stat.S_ISLNK(mode):
379ead1f93eSLiane Praza            self.mode = "%04o" % stat.S_IMODE(mode)
380ead1f93eSLiane Praza            #
381ead1f93eSLiane Praza            # Instead of reading the group and owner from the proto area after
382ead1f93eSLiane Praza            # a non-root build, just drop in dummy values.  Since we don't
383ead1f93eSLiane Praza            # compare them anywhere, this should allow at least marginally
384ead1f93eSLiane Praza            # useful comparisons of protolist-style output.
385ead1f93eSLiane Praza            #
386ead1f93eSLiane Praza            self.owner = "owner"
387ead1f93eSLiane Praza            self.group = "group"
388ead1f93eSLiane Praza
389ead1f93eSLiane Praza        #
390ead1f93eSLiane Praza        # refcount > 1 indicates a hard link
391ead1f93eSLiane Praza        #
392ead1f93eSLiane Praza        if lstat.st_nlink > 1:
393ead1f93eSLiane Praza            #
394ead1f93eSLiane Praza            # This could get ugly if multiple proto areas reside
395ead1f93eSLiane Praza            # on different filesystems.
396ead1f93eSLiane Praza            #
397ead1f93eSLiane Praza            self.hardkey = lstat.st_ino
398ead1f93eSLiane Praza
399ead1f93eSLiane Praza
400ead1f93eSLiane Prazaclass DirectoryTree(dict):
401ead1f93eSLiane Praza    """Meant to be subclassed according to population method.
402ead1f93eSLiane Praza    """
403ead1f93eSLiane Praza    def __init__(self, name):
404ead1f93eSLiane Praza        dict.__init__(self)
405ead1f93eSLiane Praza        self.name = name
406ead1f93eSLiane Praza
407ead1f93eSLiane Praza    def compare(self, other):
408ead1f93eSLiane Praza        """Compare two different sets of FileInfo objects.
409ead1f93eSLiane Praza        """
410*93be19b9SAndy Fiddaman        keys1 = frozenset(list(self.keys()))
411*93be19b9SAndy Fiddaman        keys2 = frozenset(list(other.keys()))
412ead1f93eSLiane Praza
413ead1f93eSLiane Praza        common = keys1.intersection(keys2)
414ead1f93eSLiane Praza        onlykeys1 = keys1.difference(common)
415ead1f93eSLiane Praza        onlykeys2 = keys2.difference(common)
416ead1f93eSLiane Praza
417ead1f93eSLiane Praza        if onlykeys1:
418*93be19b9SAndy Fiddaman            print("Entries present in %s but not %s:" % \
419*93be19b9SAndy Fiddaman                (self.name, other.name))
420ead1f93eSLiane Praza            for path in sorted(onlykeys1):
421*93be19b9SAndy Fiddaman                print(("\t%s" % str(self[path])))
422*93be19b9SAndy Fiddaman            print("")
423ead1f93eSLiane Praza
424ead1f93eSLiane Praza        if onlykeys2:
425*93be19b9SAndy Fiddaman            print("Entries present in %s but not %s:" % \
426*93be19b9SAndy Fiddaman                (other.name, self.name))
427ead1f93eSLiane Praza            for path in sorted(onlykeys2):
428*93be19b9SAndy Fiddaman                print(("\t%s" % str(other[path])))
429*93be19b9SAndy Fiddaman            print("")
430ead1f93eSLiane Praza
431ead1f93eSLiane Praza        nodifferences = True
432ead1f93eSLiane Praza        for path in sorted(common):
433ead1f93eSLiane Praza            if self[path] != other[path]:
434ead1f93eSLiane Praza                if nodifferences:
435ead1f93eSLiane Praza                    nodifferences = False
436*93be19b9SAndy Fiddaman                    print("Entries that differ between %s and %s:" \
437*93be19b9SAndy Fiddaman                        % (self.name, other.name))
438*93be19b9SAndy Fiddaman                print(("%14s %s" % (self.name, self[path])))
439*93be19b9SAndy Fiddaman                print(("%14s %s" % (other.name, other[path])))
440ead1f93eSLiane Praza        if not nodifferences:
441*93be19b9SAndy Fiddaman            print("")
442ead1f93eSLiane Praza
443ead1f93eSLiane Praza
444ead1f93eSLiane Prazaclass BadProtolistFormat(Exception):
445ead1f93eSLiane Praza    """This means that the user supplied a file via -l, but at least
446ead1f93eSLiane Praza    one line from that file doesn't have the right number of fields to
447ead1f93eSLiane Praza    parse as protolist output.
448ead1f93eSLiane Praza    """
449ead1f93eSLiane Praza    def __str__(self):
450ead1f93eSLiane Praza        return 'bad proto list entry: "%s"' % Exception.__str__(self)
451ead1f93eSLiane Praza
452ead1f93eSLiane Praza
453ead1f93eSLiane Prazaclass ProtoTree(DirectoryTree):
454ead1f93eSLiane Praza    """Describes one or more proto directories as a dictionary of
455ead1f93eSLiane Praza    RealFileInfo objects, indexed by relative path.
456ead1f93eSLiane Praza    """
457ead1f93eSLiane Praza
458ead1f93eSLiane Praza    def adddir(self, proto, exceptions):
459ead1f93eSLiane Praza        """Extends the ProtoTree dictionary with RealFileInfo
460ead1f93eSLiane Praza        objects describing the proto dir, indexed by relative
461ead1f93eSLiane Praza        path.
462ead1f93eSLiane Praza        """
463ead1f93eSLiane Praza        newentries = {}
464ead1f93eSLiane Praza
465ead1f93eSLiane Praza        pdir = os.path.normpath(proto)
466ead1f93eSLiane Praza        strippdir = lambda r, n: os.path.join(r, n)[len(pdir)+1:]
467ead1f93eSLiane Praza        for root, dirs, files in os.walk(pdir):
468ead1f93eSLiane Praza            for name in dirs + files:
469ead1f93eSLiane Praza                path = strippdir(root, name)
470ead1f93eSLiane Praza                if path not in exceptions:
471ead1f93eSLiane Praza                    try:
472ead1f93eSLiane Praza                        newentries[path] = RealFileInfo(pdir, path)
473*93be19b9SAndy Fiddaman                    except OSError as e:
474ead1f93eSLiane Praza                        sys.stderr.write("Warning: unable to stat %s: %s\n" %
475ead1f93eSLiane Praza                            (path, e))
476ead1f93eSLiane Praza                        continue
477ead1f93eSLiane Praza                else:
478ead1f93eSLiane Praza                    exceptions.remove(path)
479ead1f93eSLiane Praza                    if name in dirs:
480ead1f93eSLiane Praza                        dirs.remove(name)
481ead1f93eSLiane Praza
482ead1f93eSLiane Praza        #
483ead1f93eSLiane Praza        # Find the sets of paths in this proto dir that are hardlinks
484ead1f93eSLiane Praza        # to the same inode.
485ead1f93eSLiane Praza        #
486ead1f93eSLiane Praza        # It seems wasteful to store this in each FileInfo, but we
487ead1f93eSLiane Praza        # otherwise need a linking mechanism.  With this information
488ead1f93eSLiane Praza        # here, FileInfo object comparison can be self contained.
489ead1f93eSLiane Praza        #
490ead1f93eSLiane Praza        # We limit this aggregation to a single proto dir, as
491ead1f93eSLiane Praza        # represented by newentries.  That means we don't need to care
492ead1f93eSLiane Praza        # about proto dirs on separate filesystems, or about hardlinks
493ead1f93eSLiane Praza        # that cross proto dir boundaries.
494ead1f93eSLiane Praza        #
495ead1f93eSLiane Praza        hk2path = {}
496*93be19b9SAndy Fiddaman        for path, fileinfo in newentries.items():
497ead1f93eSLiane Praza            if fileinfo.hardkey:
498ead1f93eSLiane Praza                hk2path.setdefault(fileinfo.hardkey, set()).add(path)
499*93be19b9SAndy Fiddaman        for fileinfo in newentries.values():
500ead1f93eSLiane Praza            if fileinfo.hardkey:
501ead1f93eSLiane Praza                fileinfo.hardpaths.update(hk2path[fileinfo.hardkey])
502ead1f93eSLiane Praza        self.update(newentries)
503ead1f93eSLiane Praza
504ead1f93eSLiane Praza    def addprotolist(self, protolist, exceptions):
505ead1f93eSLiane Praza        """Read in the specified file, assumed to be the
506ead1f93eSLiane Praza        output of protolist.
507ead1f93eSLiane Praza
508ead1f93eSLiane Praza        This has been tested minimally, and is potentially useful for
509ead1f93eSLiane Praza        comparing across the transition period, but should ultimately
510ead1f93eSLiane Praza        go away.
511ead1f93eSLiane Praza        """
512ead1f93eSLiane Praza
513ead1f93eSLiane Praza        try:
514ead1f93eSLiane Praza            plist = open(protolist)
515*93be19b9SAndy Fiddaman        except IOError as exc:
516ead1f93eSLiane Praza            raise IOError("cannot open proto list: %s" % str(exc))
517ead1f93eSLiane Praza
518ead1f93eSLiane Praza        newentries = {}
519ead1f93eSLiane Praza
520ead1f93eSLiane Praza        for pline in plist:
521ead1f93eSLiane Praza            pline = pline.split()
522ead1f93eSLiane Praza            #
523ead1f93eSLiane Praza            # Use a FileInfo() object instead of a RealFileInfo()
524ead1f93eSLiane Praza            # object because we want to avoid the RealFileInfo
525ead1f93eSLiane Praza            # constructor, because there's nothing to actually stat().
526ead1f93eSLiane Praza            #
527ead1f93eSLiane Praza            fileinfo = FileInfo()
528ead1f93eSLiane Praza            try:
529ead1f93eSLiane Praza                if pline[1] in exceptions:
530ead1f93eSLiane Praza                    exceptions.remove(pline[1])
531ead1f93eSLiane Praza                    continue
532ead1f93eSLiane Praza                if pline[0] == "d":
533ead1f93eSLiane Praza                    fileinfo.isdir = True
534ead1f93eSLiane Praza                fileinfo.path = pline[1]
535ead1f93eSLiane Praza                if pline[2] != "-":
536ead1f93eSLiane Praza                    fileinfo.target = os.path.normpath(pline[2])
537ead1f93eSLiane Praza                fileinfo.mode = int("0%s" % pline[3])
538ead1f93eSLiane Praza                fileinfo.owner = pline[4]
539ead1f93eSLiane Praza                fileinfo.group = pline[5]
540ead1f93eSLiane Praza                if pline[6] != "0":
541ead1f93eSLiane Praza                    fileinfo.hardkey = pline[6]
542ead1f93eSLiane Praza                newentries[pline[1]] = fileinfo
543ead1f93eSLiane Praza            except IndexError:
544ead1f93eSLiane Praza                raise BadProtolistFormat(pline)
545ead1f93eSLiane Praza
546ead1f93eSLiane Praza        plist.close()
547ead1f93eSLiane Praza        hk2path = {}
548*93be19b9SAndy Fiddaman        for path, fileinfo in newentries.items():
549ead1f93eSLiane Praza            if fileinfo.hardkey:
550ead1f93eSLiane Praza                hk2path.setdefault(fileinfo.hardkey, set()).add(path)
551*93be19b9SAndy Fiddaman        for fileinfo in newentries.values():
552ead1f93eSLiane Praza            if fileinfo.hardkey:
553ead1f93eSLiane Praza                fileinfo.hardpaths.update(hk2path[fileinfo.hardkey])
554ead1f93eSLiane Praza        self.update(newentries)
555ead1f93eSLiane Praza
556ead1f93eSLiane Praza
557ead1f93eSLiane Prazaclass ManifestParsingError(Exception):
558ead1f93eSLiane Praza    """This means that the Manifest.set_content() raised an
559ead1f93eSLiane Praza    ActionError.  We raise this, instead, to tell us which manifest
560ead1f93eSLiane Praza    could not be parsed, rather than what action error we hit.
561ead1f93eSLiane Praza    """
562ead1f93eSLiane Praza    def __init__(self, mfile, error):
563ead1f93eSLiane Praza        Exception.__init__(self)
564ead1f93eSLiane Praza        self.mfile = mfile
565ead1f93eSLiane Praza        self.error = error
566ead1f93eSLiane Praza
567ead1f93eSLiane Praza    def __str__(self):
568ead1f93eSLiane Praza        return "unable to parse manifest %s: %s" % (self.mfile, self.error)
569ead1f93eSLiane Praza
570ead1f93eSLiane Praza
571ead1f93eSLiane Prazaclass ManifestTree(DirectoryTree):
572ead1f93eSLiane Praza    """Describes one or more directories containing arbitrarily
573ead1f93eSLiane Praza    many manifests as a dictionary of ActionInfo objects, indexed
574ead1f93eSLiane Praza    by the relative path of the data source within the proto area.
575ead1f93eSLiane Praza    That path may or may not be the same as the path attribute of the
576ead1f93eSLiane Praza    given action.
577ead1f93eSLiane Praza    """
578ead1f93eSLiane Praza
579ead1f93eSLiane Praza    def addmanifest(self, root, mfile, arch, modechecks, exceptions):
580ead1f93eSLiane Praza        """Treats the specified input file as a pkg(5) package
581ead1f93eSLiane Praza        manifest, and extends the ManifestTree dictionary with entries
582ead1f93eSLiane Praza        for the actions therein.
583ead1f93eSLiane Praza        """
584ead1f93eSLiane Praza        mfest = manifest.Manifest()
585ead1f93eSLiane Praza        try:
586ead1f93eSLiane Praza            mfest.set_content(open(os.path.join(root, mfile)).read())
587*93be19b9SAndy Fiddaman        except IOError as exc:
588ead1f93eSLiane Praza            raise IOError("cannot read manifest: %s" % str(exc))
589*93be19b9SAndy Fiddaman        except actions.ActionError as exc:
590ead1f93eSLiane Praza            raise ManifestParsingError(mfile, str(exc))
591ead1f93eSLiane Praza
592ead1f93eSLiane Praza        #
593ead1f93eSLiane Praza        # Make sure the manifest is applicable to the user-specified
594ead1f93eSLiane Praza        # architecture.  Assumption: if variant.arch is not an
595ead1f93eSLiane Praza        # attribute of the manifest, then the package should be
596ead1f93eSLiane Praza        # installed on all architectures.
597ead1f93eSLiane Praza        #
598ead1f93eSLiane Praza        if arch not in mfest.attributes.get("variant.arch", (arch,)):
599ead1f93eSLiane Praza            return
600ead1f93eSLiane Praza
601ead1f93eSLiane Praza        modewarnings = set()
602ead1f93eSLiane Praza        for action in mfest.gen_actions():
603ead1f93eSLiane Praza            if "path" not in action.attrs or \
604ead1f93eSLiane Praza                not ActionInfo.supported(action.name):
605ead1f93eSLiane Praza                continue
606ead1f93eSLiane Praza
607ead1f93eSLiane Praza            #
608ead1f93eSLiane Praza            # The dir action is currently fully specified, in that it
609ead1f93eSLiane Praza            # lists owner, group, and mode attributes.  If that
610ead1f93eSLiane Praza            # changes in pkg(5) code, we'll need to revisit either this
611ead1f93eSLiane Praza            # code or the ActionInfo() constructor.  It's possible
612ead1f93eSLiane Praza            # that the pkg(5) system could be extended to provide a
613ead1f93eSLiane Praza            # mechanism for specifying directory permissions outside
614ead1f93eSLiane Praza            # of the individual manifests that deliver files into
615ead1f93eSLiane Praza            # those directories.  Doing so at time of manifest
616ead1f93eSLiane Praza            # processing would mean that validate_pkg continues to work,
617ead1f93eSLiane Praza            # but doing so at time of publication would require updates.
618ead1f93eSLiane Praza            #
619ead1f93eSLiane Praza
620ead1f93eSLiane Praza            #
621ead1f93eSLiane Praza            # See pkgsend(1) for the use of NOHASH for objects with
622ead1f93eSLiane Praza            # datastreams.  Currently, that means "files," but this
623ead1f93eSLiane Praza            # should work for any other such actions.
624ead1f93eSLiane Praza            #
625ead1f93eSLiane Praza            if getattr(action, "hash", "NOHASH") != "NOHASH":
626ead1f93eSLiane Praza                path = action.hash
627ead1f93eSLiane Praza            else:
628ead1f93eSLiane Praza                path = action.attrs["path"]
629ead1f93eSLiane Praza
630ead1f93eSLiane Praza            #
631ead1f93eSLiane Praza            # This is the wrong tool in which to enforce consistency
632ead1f93eSLiane Praza            # on a set of manifests.  So instead of comparing the
633ead1f93eSLiane Praza            # different actions with the same "path" attribute, we
634ead1f93eSLiane Praza            # use the first one.
635ead1f93eSLiane Praza            #
636ead1f93eSLiane Praza            if path in self:
637ead1f93eSLiane Praza                continue
638ead1f93eSLiane Praza
639ead1f93eSLiane Praza            #
640ead1f93eSLiane Praza            # As with the manifest itself, if an action has specified
641ead1f93eSLiane Praza            # variant.arch, we look for the target architecture
642ead1f93eSLiane Praza            # therein.
643ead1f93eSLiane Praza            #
644fe89515eSRichard Lowe            var = None
645fe89515eSRichard Lowe
646fe89515eSRichard Lowe            #
647fe89515eSRichard Lowe            # The name of this method changed in pkg(5) build 150, we need to
648fe89515eSRichard Lowe            # work with both sets.
649fe89515eSRichard Lowe            #
650fe89515eSRichard Lowe            if hasattr(action, 'get_variants'):
651ead1f93eSLiane Praza                var = action.get_variants()
652fe89515eSRichard Lowe            else:
653fe89515eSRichard Lowe                var = action.get_variant_template()
654ead1f93eSLiane Praza            if "variant.arch" in var and arch not in var["variant.arch"]:
655ead1f93eSLiane Praza                return
656ead1f93eSLiane Praza
657ead1f93eSLiane Praza            self[path] = ActionInfo(action)
658ead1f93eSLiane Praza            if modechecks is not None and path not in exceptions:
659ead1f93eSLiane Praza                modewarnings.update(self[path].checkmodes(modechecks))
660ead1f93eSLiane Praza
661ead1f93eSLiane Praza        if len(modewarnings) > 0:
662*93be19b9SAndy Fiddaman            print("warning: unsafe permissions in %s" % mfile)
663ead1f93eSLiane Praza            for w in sorted(modewarnings):
664*93be19b9SAndy Fiddaman                print(w)
665*93be19b9SAndy Fiddaman            print("")
666ead1f93eSLiane Praza
667ead1f93eSLiane Praza    def adddir(self, mdir, arch, modechecks, exceptions):
668ead1f93eSLiane Praza        """Walks the specified directory looking for pkg(5) manifests.
669ead1f93eSLiane Praza        """
670ead1f93eSLiane Praza        for mfile in os.listdir(mdir):
671ead1f93eSLiane Praza            if (mfile.endswith(".mog") and
672ead1f93eSLiane Praza                stat.S_ISREG(os.lstat(os.path.join(mdir, mfile)).st_mode)):
673ead1f93eSLiane Praza                try:
674ead1f93eSLiane Praza                    self.addmanifest(mdir, mfile, arch, modechecks, exceptions)
675*93be19b9SAndy Fiddaman                except IOError as exc:
676ead1f93eSLiane Praza                    sys.stderr.write("warning: %s\n" % str(exc))
677ead1f93eSLiane Praza
678ead1f93eSLiane Praza    def resolvehardlinks(self):
679ead1f93eSLiane Praza        """Populates mode, group, and owner for resolved (ie link target
680ead1f93eSLiane Praza        is present in the manifest tree) hard links.
681ead1f93eSLiane Praza        """
682*93be19b9SAndy Fiddaman        for info in list(self.values()):
683ead1f93eSLiane Praza            if info.name() == "hardlink":
684ead1f93eSLiane Praza                tgt = info.hardkey
685ead1f93eSLiane Praza                if tgt in self:
686ead1f93eSLiane Praza                    tgtinfo = self[tgt]
687ead1f93eSLiane Praza                    info.owner = tgtinfo.owner
688ead1f93eSLiane Praza                    info.group = tgtinfo.group
689ead1f93eSLiane Praza                    info.mode = tgtinfo.mode
690ead1f93eSLiane Praza
691ead1f93eSLiane Prazaclass ExceptionList(set):
692ead1f93eSLiane Praza    """Keep track of an exception list as a set of paths to be excluded
693ead1f93eSLiane Praza    from any other lists we build.
694ead1f93eSLiane Praza    """
695ead1f93eSLiane Praza
696ead1f93eSLiane Praza    def __init__(self, files, arch):
697ead1f93eSLiane Praza        set.__init__(self)
698ead1f93eSLiane Praza        for fname in files:
699ead1f93eSLiane Praza            try:
700ead1f93eSLiane Praza                self.readexceptionfile(fname, arch)
701*93be19b9SAndy Fiddaman            except IOError as exc:
702ead1f93eSLiane Praza                sys.stderr.write("warning: cannot read exception file: %s\n" %
703ead1f93eSLiane Praza                    str(exc))
704ead1f93eSLiane Praza
705ead1f93eSLiane Praza    def readexceptionfile(self, efile, arch):
706ead1f93eSLiane Praza        """Build a list of all pathnames from the specified file that
707ead1f93eSLiane Praza        either apply to all architectures (ie which have no trailing
708ead1f93eSLiane Praza        architecture tokens), or to the specified architecture (ie
709ead1f93eSLiane Praza        which have the value of the arch arg as a trailing
710ead1f93eSLiane Praza        architecture token.)
711ead1f93eSLiane Praza        """
712ead1f93eSLiane Praza
713ead1f93eSLiane Praza        excfile = open(efile)
714ead1f93eSLiane Praza
715ead1f93eSLiane Praza        for exc in excfile:
716ead1f93eSLiane Praza            exc = exc.split()
717ead1f93eSLiane Praza            if len(exc) and exc[0][0] != "#":
718ead1f93eSLiane Praza                if arch in (exc[1:] or arch):
719ead1f93eSLiane Praza                    self.add(os.path.normpath(exc[0]))
720ead1f93eSLiane Praza
721ead1f93eSLiane Praza        excfile.close()
722ead1f93eSLiane Praza
723ead1f93eSLiane Praza
724ead1f93eSLiane PrazaUSAGE = """%s [-v] -a arch [-e exceptionfile]... [-L|-M [-X check]...] input_1 [input_2]
725ead1f93eSLiane Praza
726ead1f93eSLiane Prazawhere input_1 and input_2 may specify proto lists, proto areas,
727ead1f93eSLiane Prazaor manifest directories.  For proto lists, use one or more
728ead1f93eSLiane Praza
729ead1f93eSLiane Praza    -l file
730ead1f93eSLiane Praza
731ead1f93eSLiane Prazaarguments.  For proto areas, use one or more
732ead1f93eSLiane Praza
733ead1f93eSLiane Praza    -p dir
734ead1f93eSLiane Praza
735ead1f93eSLiane Prazaarguments.  For manifest directories, use one or more
736ead1f93eSLiane Praza
737ead1f93eSLiane Praza    -m dir
738ead1f93eSLiane Praza
739ead1f93eSLiane Prazaarguments.
740ead1f93eSLiane Praza
741ead1f93eSLiane PrazaIf -L or -M is specified, then only one input source is allowed, and
742ead1f93eSLiane Prazait should be one or more manifest directories.  These two options are
743ead1f93eSLiane Prazamutually exclusive.
744ead1f93eSLiane Praza
745ead1f93eSLiane PrazaThe -L option is used to generate a proto list to stdout.
746ead1f93eSLiane Praza
747ead1f93eSLiane PrazaThe -M option is used to check for safe file and directory modes.
748ead1f93eSLiane PrazaBy default, this causes all mode checks to be performed.  Individual
749ead1f93eSLiane Prazamode checks may be turned off using "-X check," where "check" comes
750ead1f93eSLiane Prazafrom the following set of checks:
751ead1f93eSLiane Praza
752ead1f93eSLiane Praza    m   check for group or other write permissions
753ead1f93eSLiane Praza    w   check for user write permissions on files and directories
754ead1f93eSLiane Praza        not owned by root
755ead1f93eSLiane Praza    s   check for group/other read permission on executable files
756ead1f93eSLiane Praza        that have setuid/setgid bit(s)
757ead1f93eSLiane Praza    o   check for files that could be safely owned by root
758ead1f93eSLiane Praza""" % sys.argv[0]
759ead1f93eSLiane Praza
760ead1f93eSLiane Praza
761ead1f93eSLiane Prazadef usage(msg=None):
762ead1f93eSLiane Praza    """Try to give the user useful information when they don't get the
763ead1f93eSLiane Praza    command syntax right.
764ead1f93eSLiane Praza    """
765ead1f93eSLiane Praza    if msg:
766ead1f93eSLiane Praza        sys.stderr.write("%s: %s\n" % (sys.argv[0], msg))
767ead1f93eSLiane Praza    sys.stderr.write(USAGE)
768ead1f93eSLiane Praza    sys.exit(2)
769ead1f93eSLiane Praza
770ead1f93eSLiane Praza
771ead1f93eSLiane Prazadef main(argv):
772ead1f93eSLiane Praza    """Compares two out of three possible data sources: a proto list, a
773ead1f93eSLiane Praza    set of proto areas, and a set of manifests.
774ead1f93eSLiane Praza    """
775ead1f93eSLiane Praza    try:
776ead1f93eSLiane Praza        opts, args = getopt.getopt(argv, 'a:e:Ll:Mm:p:vX:')
777*93be19b9SAndy Fiddaman    except getopt.GetoptError as exc:
778ead1f93eSLiane Praza        usage(str(exc))
779ead1f93eSLiane Praza
780ead1f93eSLiane Praza    if args:
781ead1f93eSLiane Praza        usage()
782ead1f93eSLiane Praza
783ead1f93eSLiane Praza    arch = None
784ead1f93eSLiane Praza    exceptionlists = []
785ead1f93eSLiane Praza    listonly = False
786ead1f93eSLiane Praza    manifestdirs = []
787ead1f93eSLiane Praza    manifesttree = ManifestTree("manifests")
788ead1f93eSLiane Praza    protodirs = []
789ead1f93eSLiane Praza    prototree = ProtoTree("proto area")
790ead1f93eSLiane Praza    protolists = []
791ead1f93eSLiane Praza    protolist = ProtoTree("proto list")
792ead1f93eSLiane Praza    modechecks = set()
793ead1f93eSLiane Praza    togglemodechecks = set()
794ead1f93eSLiane Praza    trees = []
795ead1f93eSLiane Praza    comparing = set()
796ead1f93eSLiane Praza    verbose = False
797ead1f93eSLiane Praza
798ead1f93eSLiane Praza    for opt, arg in opts:
799ead1f93eSLiane Praza        if opt == "-a":
800ead1f93eSLiane Praza            if arch:
801ead1f93eSLiane Praza                usage("may only specify one architecture")
802ead1f93eSLiane Praza            else:
803ead1f93eSLiane Praza                arch = arg
804ead1f93eSLiane Praza        elif opt == "-e":
805ead1f93eSLiane Praza            exceptionlists.append(arg)
806ead1f93eSLiane Praza        elif opt == "-L":
807ead1f93eSLiane Praza            listonly = True
808ead1f93eSLiane Praza        elif opt == "-l":
809ead1f93eSLiane Praza            comparing.add("protolist")
810ead1f93eSLiane Praza            protolists.append(os.path.normpath(arg))
811ead1f93eSLiane Praza        elif opt == "-M":
812ead1f93eSLiane Praza            modechecks.update(DEFAULTMODECHECKS)
813ead1f93eSLiane Praza        elif opt == "-m":
814ead1f93eSLiane Praza            comparing.add("manifests")
815ead1f93eSLiane Praza            manifestdirs.append(os.path.normpath(arg))
816ead1f93eSLiane Praza        elif opt == "-p":
817ead1f93eSLiane Praza            comparing.add("proto area")
818ead1f93eSLiane Praza            protodirs.append(os.path.normpath(arg))
819ead1f93eSLiane Praza        elif opt == "-v":
820ead1f93eSLiane Praza            verbose = True
821ead1f93eSLiane Praza        elif opt == "-X":
822ead1f93eSLiane Praza            togglemodechecks.add(arg)
823ead1f93eSLiane Praza
824ead1f93eSLiane Praza    if listonly or len(modechecks) > 0:
825ead1f93eSLiane Praza        if len(comparing) != 1 or "manifests" not in comparing:
826ead1f93eSLiane Praza            usage("-L and -M require one or more -m args, and no -l or -p")
827ead1f93eSLiane Praza        if listonly and len(modechecks) > 0:
828ead1f93eSLiane Praza            usage("-L and -M are mutually exclusive")
829ead1f93eSLiane Praza    elif len(comparing) != 2:
830ead1f93eSLiane Praza        usage("must specify exactly two of -l, -m, and -p")
831ead1f93eSLiane Praza
832ead1f93eSLiane Praza    if len(togglemodechecks) > 0 and len(modechecks) == 0:
833ead1f93eSLiane Praza        usage("-X requires -M")
834ead1f93eSLiane Praza
835ead1f93eSLiane Praza    for s in togglemodechecks:
836ead1f93eSLiane Praza        if s not in ALLMODECHECKS:
837ead1f93eSLiane Praza            usage("unknown mode check %s" % s)
838ead1f93eSLiane Praza        modechecks.symmetric_difference_update((s))
839ead1f93eSLiane Praza
840ead1f93eSLiane Praza    if len(modechecks) == 0:
841ead1f93eSLiane Praza        modechecks = None
842ead1f93eSLiane Praza
843ead1f93eSLiane Praza    if not arch:
844ead1f93eSLiane Praza        usage("must specify architecture")
845ead1f93eSLiane Praza
846ead1f93eSLiane Praza    exceptions = ExceptionList(exceptionlists, arch)
847ead1f93eSLiane Praza    originalexceptions = exceptions.copy()
848ead1f93eSLiane Praza
849ead1f93eSLiane Praza    if len(manifestdirs) > 0:
850ead1f93eSLiane Praza        for mdir in manifestdirs:
851ead1f93eSLiane Praza            manifesttree.adddir(mdir, arch, modechecks, exceptions)
852ead1f93eSLiane Praza        if listonly:
853ead1f93eSLiane Praza            manifesttree.resolvehardlinks()
854*93be19b9SAndy Fiddaman            for info in list(manifesttree.values()):
855*93be19b9SAndy Fiddaman                print("%s" % info.protostr())
856ead1f93eSLiane Praza            sys.exit(0)
857ead1f93eSLiane Praza        if modechecks is not None:
858ead1f93eSLiane Praza            sys.exit(0)
859ead1f93eSLiane Praza        trees.append(manifesttree)
860ead1f93eSLiane Praza
861ead1f93eSLiane Praza    if len(protodirs) > 0:
862ead1f93eSLiane Praza        for pdir in protodirs:
863ead1f93eSLiane Praza            prototree.adddir(pdir, exceptions)
864ead1f93eSLiane Praza        trees.append(prototree)
865ead1f93eSLiane Praza
866ead1f93eSLiane Praza    if len(protolists) > 0:
867ead1f93eSLiane Praza        for plist in protolists:
868ead1f93eSLiane Praza            try:
869ead1f93eSLiane Praza                protolist.addprotolist(plist, exceptions)
870*93be19b9SAndy Fiddaman            except IOError as exc:
871ead1f93eSLiane Praza                sys.stderr.write("warning: %s\n" % str(exc))
872ead1f93eSLiane Praza        trees.append(protolist)
873ead1f93eSLiane Praza
874ead1f93eSLiane Praza    if verbose and exceptions:
875*93be19b9SAndy Fiddaman        print("Entries present in exception list but missing from proto area:")
876ead1f93eSLiane Praza        for exc in sorted(exceptions):
877*93be19b9SAndy Fiddaman            print("\t%s" % exc)
878*93be19b9SAndy Fiddaman        print("")
879ead1f93eSLiane Praza
880ead1f93eSLiane Praza    usedexceptions = originalexceptions.difference(exceptions)
881ead1f93eSLiane Praza    harmfulexceptions = usedexceptions.intersection(manifesttree)
882ead1f93eSLiane Praza    if harmfulexceptions:
883*93be19b9SAndy Fiddaman        print("Entries present in exception list but also in manifests:")
884ead1f93eSLiane Praza        for exc in sorted(harmfulexceptions):
885*93be19b9SAndy Fiddaman            print("\t%s" % exc)
886ead1f93eSLiane Praza            del manifesttree[exc]
887*93be19b9SAndy Fiddaman        print("")
888ead1f93eSLiane Praza
889ead1f93eSLiane Praza    trees[0].compare(trees[1])
890ead1f93eSLiane Praza
891ead1f93eSLiane Prazaif __name__ == '__main__':
892ead1f93eSLiane Praza    try:
893ead1f93eSLiane Praza        main(sys.argv[1:])
894ead1f93eSLiane Praza    except KeyboardInterrupt:
895ead1f93eSLiane Praza        sys.exit(1)
896ead1f93eSLiane Praza    except IOError:
897ead1f93eSLiane Praza        sys.exit(1)
898