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