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