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