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