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