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             if "mediator" in action.attrs:
 312                 self.mediator = action.attrs["mediator"]
 313             else:
 314                 self.mediator = None
 315         elif action.name == "dir":
 316             self.owner = action.attrs["owner"]
 317             self.group = action.attrs["group"]
 318             self.mode = action.attrs["mode"]
 319             self.isdir = True
 320         elif action.name == "hardlink":
 321             target = os.path.normpath(action.get_target_path())
 322             self.hardkey = target
 323             self.hardpaths.add(target)
 324 
 325     @staticmethod
 326     def supported(action):
 327         """Indicates whether the specified IPS action time is
 328         correctly handled by the ActionInfo constructor.
 329         """
 330         return action in frozenset(("file", "dir", "link", "hardlink"))
 331 
 332 
 333 class UnsupportedFileFormatError(Exception):
 334     """This means that the stat.S_IFMT returned something we don't
 335     support, ie a pipe or socket.  If it's appropriate for such an
 336     object to be in the proto area, then the RealFileInfo constructor
 337     will need to evolve to support it, or it will need to be in the
 338     exception list.
 339     """
 340     def __init__(self, path, mode):
 341         Exception.__init__(self)
 342         self.path = path
 343         self.mode = mode
 344 
 345     def __str__(self):
 346         return '%s: unsupported S_IFMT %07o' % (self.path, self.mode)
 347 
 348 
 349 class RealFileInfo(FileInfo):
 350     """Object to track important-to-packaging file information.
 351 
 352     This currently handles regular files, directories, and symbolic links.
 353 
 354     For multiple RealFileInfo objects with identical hardkeys, there
 355     is no way to determine which of the hard links should be
 356     delivered as a file, and which as hardlinks.
 357     """
 358 
 359     def __init__(self, root=None, path=None):
 360         FileInfo.__init__(self)
 361         self.path = path
 362         path = os.path.join(root, path)
 363         lstat = os.lstat(path)
 364         mode = lstat.st_mode
 365 
 366         #
 367         # Per stat.py, these cases are mutually exclusive.
 368         #
 369         if stat.S_ISREG(mode):
 370             self.hash = self.path
 371         elif stat.S_ISDIR(mode):
 372             self.isdir = True
 373         elif stat.S_ISLNK(mode):
 374             self.target = os.path.normpath(os.readlink(path))
 375             self.mediator = None
 376         else:
 377             raise UnsupportedFileFormatError(path, mode)
 378 
 379         if not stat.S_ISLNK(mode):
 380             self.mode = "%04o" % stat.S_IMODE(mode)
 381             #
 382             # Instead of reading the group and owner from the proto area after
 383             # a non-root build, just drop in dummy values.  Since we don't
 384             # compare them anywhere, this should allow at least marginally
 385             # useful comparisons of protolist-style output.
 386             #
 387             self.owner = "owner"
 388             self.group = "group"
 389 
 390         #
 391         # refcount > 1 indicates a hard link
 392         #
 393         if lstat.st_nlink > 1:
 394             #
 395             # This could get ugly if multiple proto areas reside
 396             # on different filesystems.
 397             #
 398             self.hardkey = lstat.st_ino
 399 
 400 
 401 class DirectoryTree(dict):
 402     """Meant to be subclassed according to population method.
 403     """
 404     def __init__(self, name):
 405         dict.__init__(self)
 406         self.name = name
 407 
 408     def compare(self, other):
 409         """Compare two different sets of FileInfo objects.
 410         """
 411         keys1 = frozenset(self.keys())
 412         keys2 = frozenset(other.keys())
 413 
 414         common = keys1.intersection(keys2)
 415         onlykeys1 = keys1.difference(common)
 416         onlykeys2 = keys2.difference(common)
 417 
 418         if onlykeys1:
 419             print "Entries present in %s but not %s:" % \
 420                 (self.name, other.name)
 421             for path in sorted(onlykeys1):
 422                 print("\t%s" % str(self[path]))
 423             print ""
 424 
 425         if onlykeys2:
 426             print "Entries present in %s but not %s:" % \
 427                 (other.name, self.name)
 428             for path in sorted(onlykeys2):
 429                 print("\t%s" % str(other[path]))
 430             print ""
 431 
 432         nodifferences = True
 433         for path in sorted(common):
 434             if self[path] != other[path]:
 435                 if nodifferences:
 436                     nodifferences = False
 437                     print "Entries that differ between %s and %s:" \
 438                         % (self.name, other.name)
 439                 print("%14s %s" % (self.name, self[path]))
 440                 print("%14s %s" % (other.name, other[path]))
 441         if not nodifferences:
 442             print ""
 443 
 444 
 445 class BadProtolistFormat(Exception):
 446     """This means that the user supplied a file via -l, but at least
 447     one line from that file doesn't have the right number of fields to
 448     parse as protolist output.
 449     """
 450     def __str__(self):
 451         return 'bad proto list entry: "%s"' % Exception.__str__(self)
 452 
 453 
 454 class ProtoTree(DirectoryTree):
 455     """Describes one or more proto directories as a dictionary of
 456     RealFileInfo objects, indexed by relative path.
 457     """
 458 
 459     def adddir(self, proto, exceptions):
 460         """Extends the ProtoTree dictionary with RealFileInfo
 461         objects describing the proto dir, indexed by relative
 462         path.
 463         """
 464         newentries = {}
 465 
 466         pdir = os.path.normpath(proto)
 467         strippdir = lambda r, n: os.path.join(r, n)[len(pdir)+1:]
 468         for root, dirs, files in os.walk(pdir):
 469             for name in dirs + files:
 470                 path = strippdir(root, name)
 471                 if path not in exceptions:
 472                     try:
 473                         newentries[path] = RealFileInfo(pdir, path)
 474                     except OSError, e:
 475                         sys.stderr.write("Warning: unable to stat %s: %s\n" %
 476                             (path, e))
 477                         continue
 478                 else:
 479                     exceptions.remove(path)
 480                     if name in dirs:
 481                         dirs.remove(name)
 482 
 483         #
 484         # Find the sets of paths in this proto dir that are hardlinks
 485         # to the same inode.
 486         #
 487         # It seems wasteful to store this in each FileInfo, but we
 488         # otherwise need a linking mechanism.  With this information
 489         # here, FileInfo object comparison can be self contained.
 490         #
 491         # We limit this aggregation to a single proto dir, as
 492         # represented by newentries.  That means we don't need to care
 493         # about proto dirs on separate filesystems, or about hardlinks
 494         # that cross proto dir boundaries.
 495         #
 496         hk2path = {}
 497         for path, fileinfo in newentries.iteritems():
 498             if fileinfo.hardkey:
 499                 hk2path.setdefault(fileinfo.hardkey, set()).add(path)
 500         for fileinfo in newentries.itervalues():
 501             if fileinfo.hardkey:
 502                 fileinfo.hardpaths.update(hk2path[fileinfo.hardkey])
 503         self.update(newentries)
 504 
 505     def addprotolist(self, protolist, exceptions):
 506         """Read in the specified file, assumed to be the
 507         output of protolist.
 508 
 509         This has been tested minimally, and is potentially useful for
 510         comparing across the transition period, but should ultimately
 511         go away.
 512         """
 513 
 514         try:
 515             plist = open(protolist)
 516         except IOError, exc:
 517             raise IOError("cannot open proto list: %s" % str(exc))
 518 
 519         newentries = {}
 520 
 521         for pline in plist:
 522             pline = pline.split()
 523             #
 524             # Use a FileInfo() object instead of a RealFileInfo()
 525             # object because we want to avoid the RealFileInfo
 526             # constructor, because there's nothing to actually stat().
 527             #
 528             fileinfo = FileInfo()
 529             try:
 530                 if pline[1] in exceptions:
 531                     exceptions.remove(pline[1])
 532                     continue
 533                 if pline[0] == "d":
 534                     fileinfo.isdir = True
 535                 fileinfo.path = pline[1]
 536                 if pline[2] != "-":
 537                     fileinfo.target = os.path.normpath(pline[2])
 538                 fileinfo.mode = int("0%s" % pline[3])
 539                 fileinfo.owner = pline[4]
 540                 fileinfo.group = pline[5]
 541                 if pline[6] != "0":
 542                     fileinfo.hardkey = pline[6]
 543                 newentries[pline[1]] = fileinfo
 544             except IndexError:
 545                 raise BadProtolistFormat(pline)
 546 
 547         plist.close()
 548         hk2path = {}
 549         for path, fileinfo in newentries.iteritems():
 550             if fileinfo.hardkey:
 551                 hk2path.setdefault(fileinfo.hardkey, set()).add(path)
 552         for fileinfo in newentries.itervalues():
 553             if fileinfo.hardkey:
 554                 fileinfo.hardpaths.update(hk2path[fileinfo.hardkey])
 555         self.update(newentries)
 556 
 557 
 558 class ManifestParsingError(Exception):
 559     """This means that the Manifest.set_content() raised an
 560     ActionError.  We raise this, instead, to tell us which manifest
 561     could not be parsed, rather than what action error we hit.
 562     """
 563     def __init__(self, mfile, error):
 564         Exception.__init__(self)
 565         self.mfile = mfile
 566         self.error = error
 567 
 568     def __str__(self):
 569         return "unable to parse manifest %s: %s" % (self.mfile, self.error)
 570 
 571 
 572 class ManifestTree(DirectoryTree):
 573     """Describes one or more directories containing arbitrarily
 574     many manifests as a dictionary of ActionInfo objects, indexed
 575     by the relative path of the data source within the proto area.
 576     That path may or may not be the same as the path attribute of the
 577     given action.
 578     """
 579 
 580     def addmanifest(self, root, mfile, arch, modechecks, exceptions):
 581         """Treats the specified input file as a pkg(5) package
 582         manifest, and extends the ManifestTree dictionary with entries
 583         for the actions therein.
 584         """
 585         mfest = manifest.Manifest()
 586         try:
 587             mfest.set_content(open(os.path.join(root, mfile)).read())
 588         except IOError, exc:
 589             raise IOError("cannot read manifest: %s" % str(exc))
 590         except actions.ActionError, exc:
 591             raise ManifestParsingError(mfile, str(exc))
 592 
 593         #
 594         # Make sure the manifest is applicable to the user-specified
 595         # architecture.  Assumption: if variant.arch is not an
 596         # attribute of the manifest, then the package should be
 597         # installed on all architectures.
 598         #
 599         if arch not in mfest.attributes.get("variant.arch", (arch,)):
 600             return
 601 
 602         modewarnings = set()
 603         for action in mfest.gen_actions():
 604             if "path" not in action.attrs or \
 605                 not ActionInfo.supported(action.name):
 606                 continue
 607 
 608             #
 609             # The dir action is currently fully specified, in that it
 610             # lists owner, group, and mode attributes.  If that
 611             # changes in pkg(5) code, we'll need to revisit either this
 612             # code or the ActionInfo() constructor.  It's possible
 613             # that the pkg(5) system could be extended to provide a
 614             # mechanism for specifying directory permissions outside
 615             # of the individual manifests that deliver files into
 616             # those directories.  Doing so at time of manifest
 617             # processing would mean that validate_pkg continues to work,
 618             # but doing so at time of publication would require updates.
 619             #
 620 
 621             #
 622             # See pkgsend(1) for the use of NOHASH for objects with
 623             # datastreams.  Currently, that means "files," but this
 624             # should work for any other such actions.
 625             #
 626             if getattr(action, "hash", "NOHASH") != "NOHASH":
 627                 path = action.hash
 628             else:
 629                 path = action.attrs["path"]
 630 
 631             #
 632             # This is the wrong tool in which to enforce consistency
 633             # on a set of manifests.  So instead of comparing the
 634             # different actions with the same "path" attribute, we
 635             # use the first one.
 636             #
 637             if path in self:
 638                 continue
 639 
 640             #
 641             # As with the manifest itself, if an action has specified
 642             # variant.arch, we look for the target architecture
 643             # therein.
 644             #
 645             var = None
 646 
 647             #
 648             # The name of this method changed in pkg(5) build 150, we need to
 649             # work with both sets.
 650             #
 651             if hasattr(action, 'get_variants'):
 652                 var = action.get_variants()
 653             else:
 654                 var = action.get_variant_template()
 655             if "variant.arch" in var and arch not in var["variant.arch"]:
 656                 return
 657 
 658             self[path] = ActionInfo(action)
 659             if modechecks is not None and path not in exceptions:
 660                 modewarnings.update(self[path].checkmodes(modechecks))
 661 
 662         if len(modewarnings) > 0:
 663             print "warning: unsafe permissions in %s" % mfile
 664             for w in sorted(modewarnings):
 665                 print w
 666             print ""
 667 
 668     def adddir(self, mdir, arch, modechecks, exceptions):
 669         """Walks the specified directory looking for pkg(5) manifests.
 670         """
 671         for mfile in os.listdir(mdir):
 672             if (mfile.endswith(".mog") and
 673                 stat.S_ISREG(os.lstat(os.path.join(mdir, mfile)).st_mode)):
 674                 try:
 675                     self.addmanifest(mdir, mfile, arch, modechecks, exceptions)
 676                 except IOError, exc:
 677                     sys.stderr.write("warning: %s\n" % str(exc))
 678 
 679     def resolvehardlinks(self):
 680         """Populates mode, group, and owner for resolved (ie link target
 681         is present in the manifest tree) hard links.
 682         """
 683         for info in self.values():
 684             if info.name() == "hardlink":
 685                 tgt = info.hardkey
 686                 if tgt in self:
 687                     tgtinfo = self[tgt]
 688                     info.owner = tgtinfo.owner
 689                     info.group = tgtinfo.group
 690                     info.mode = tgtinfo.mode
 691 
 692 class ExceptionList(set):
 693     """Keep track of an exception list as a set of paths to be excluded
 694     from any other lists we build.
 695     """
 696 
 697     def __init__(self, files, arch):
 698         set.__init__(self)
 699         for fname in files:
 700             try:
 701                 self.readexceptionfile(fname, arch)
 702             except IOError, exc:
 703                 sys.stderr.write("warning: cannot read exception file: %s\n" %
 704                     str(exc))
 705 
 706     def readexceptionfile(self, efile, arch):
 707         """Build a list of all pathnames from the specified file that
 708         either apply to all architectures (ie which have no trailing
 709         architecture tokens), or to the specified architecture (ie
 710         which have the value of the arch arg as a trailing
 711         architecture token.)
 712         """
 713 
 714         excfile = open(efile)
 715 
 716         for exc in excfile:
 717             exc = exc.split()
 718             if len(exc) and exc[0][0] != "#":
 719                 if arch in (exc[1:] or arch):
 720                     self.add(os.path.normpath(exc[0]))
 721 
 722         excfile.close()
 723 
 724 
 725 USAGE = """%s [-v] -a arch [-e exceptionfile]... [-L|-M [-X check]...] input_1 [input_2]
 726 
 727 where input_1 and input_2 may specify proto lists, proto areas,
 728 or manifest directories.  For proto lists, use one or more
 729 
 730     -l file
 731 
 732 arguments.  For proto areas, use one or more
 733 
 734     -p dir
 735 
 736 arguments.  For manifest directories, use one or more
 737 
 738     -m dir
 739 
 740 arguments.
 741 
 742 If -L or -M is specified, then only one input source is allowed, and
 743 it should be one or more manifest directories.  These two options are
 744 mutually exclusive.
 745 
 746 The -L option is used to generate a proto list to stdout.
 747 
 748 The -M option is used to check for safe file and directory modes.
 749 By default, this causes all mode checks to be performed.  Individual
 750 mode checks may be turned off using "-X check," where "check" comes
 751 from the following set of checks:
 752 
 753     m   check for group or other write permissions
 754     w   check for user write permissions on files and directories
 755         not owned by root
 756     s   check for group/other read permission on executable files
 757         that have setuid/setgid bit(s)
 758     o   check for files that could be safely owned by root
 759 """ % sys.argv[0]
 760 
 761 
 762 def usage(msg=None):
 763     """Try to give the user useful information when they don't get the
 764     command syntax right.
 765     """
 766     if msg:
 767         sys.stderr.write("%s: %s\n" % (sys.argv[0], msg))
 768     sys.stderr.write(USAGE)
 769     sys.exit(2)
 770 
 771 
 772 def main(argv):
 773     """Compares two out of three possible data sources: a proto list, a
 774     set of proto areas, and a set of manifests.
 775     """
 776     try:
 777         opts, args = getopt.getopt(argv, 'a:e:Ll:Mm:p:vX:')
 778     except getopt.GetoptError, exc:
 779         usage(str(exc))
 780 
 781     if args:
 782         usage()
 783 
 784     arch = None
 785     exceptionlists = []
 786     listonly = False
 787     manifestdirs = []
 788     manifesttree = ManifestTree("manifests")
 789     protodirs = []
 790     prototree = ProtoTree("proto area")
 791     protolists = []
 792     protolist = ProtoTree("proto list")
 793     modechecks = set()
 794     togglemodechecks = set()
 795     trees = []
 796     comparing = set()
 797     verbose = False
 798 
 799     for opt, arg in opts:
 800         if opt == "-a":
 801             if arch:
 802                 usage("may only specify one architecture")
 803             else:
 804                 arch = arg
 805         elif opt == "-e":
 806             exceptionlists.append(arg)
 807         elif opt == "-L":
 808             listonly = True
 809         elif opt == "-l":
 810             comparing.add("protolist")
 811             protolists.append(os.path.normpath(arg))
 812         elif opt == "-M":
 813             modechecks.update(DEFAULTMODECHECKS)
 814         elif opt == "-m":
 815             comparing.add("manifests")
 816             manifestdirs.append(os.path.normpath(arg))
 817         elif opt == "-p":
 818             comparing.add("proto area")
 819             protodirs.append(os.path.normpath(arg))
 820         elif opt == "-v":
 821             verbose = True
 822         elif opt == "-X":
 823             togglemodechecks.add(arg)
 824 
 825     if listonly or len(modechecks) > 0:
 826         if len(comparing) != 1 or "manifests" not in comparing:
 827             usage("-L and -M require one or more -m args, and no -l or -p")
 828         if listonly and len(modechecks) > 0:
 829             usage("-L and -M are mutually exclusive")
 830     elif len(comparing) != 2:
 831         usage("must specify exactly two of -l, -m, and -p")
 832 
 833     if len(togglemodechecks) > 0 and len(modechecks) == 0:
 834         usage("-X requires -M")
 835 
 836     for s in togglemodechecks:
 837         if s not in ALLMODECHECKS:
 838             usage("unknown mode check %s" % s)
 839         modechecks.symmetric_difference_update((s))
 840 
 841     if len(modechecks) == 0:
 842         modechecks = None
 843 
 844     if not arch:
 845         usage("must specify architecture")
 846 
 847     exceptions = ExceptionList(exceptionlists, arch)
 848     originalexceptions = exceptions.copy()
 849 
 850     if len(manifestdirs) > 0:
 851         for mdir in manifestdirs:
 852             manifesttree.adddir(mdir, arch, modechecks, exceptions)
 853         if listonly:
 854             manifesttree.resolvehardlinks()
 855             for info in manifesttree.values():
 856                 print "%s" % info.protostr()
 857             sys.exit(0)
 858         if modechecks is not None:
 859             sys.exit(0)
 860         trees.append(manifesttree)
 861 
 862     if len(protodirs) > 0:
 863         for pdir in protodirs:
 864             prototree.adddir(pdir, exceptions)
 865         trees.append(prototree)
 866 
 867     if len(protolists) > 0:
 868         for plist in protolists:
 869             try:
 870                 protolist.addprotolist(plist, exceptions)
 871             except IOError, exc:
 872                 sys.stderr.write("warning: %s\n" % str(exc))
 873         trees.append(protolist)
 874 
 875     if verbose and exceptions:
 876         print "Entries present in exception list but missing from proto area:"
 877         for exc in sorted(exceptions):
 878             print "\t%s" % exc
 879         print ""
 880 
 881     usedexceptions = originalexceptions.difference(exceptions)
 882     harmfulexceptions = usedexceptions.intersection(manifesttree)
 883     if harmfulexceptions:
 884         print "Entries present in exception list but also in manifests:"
 885         for exc in sorted(harmfulexceptions):
 886             print "\t%s" % exc
 887             del manifesttree[exc]
 888         print ""
 889 
 890     trees[0].compare(trees[1])
 891 
 892 if __name__ == '__main__':
 893     try:
 894         main(sys.argv[1:])
 895     except KeyboardInterrupt:
 896         sys.exit(1)
 897     except IOError:
 898         sys.exit(1)