1 r"""plistlib.py -- a tool to generate and parse MacOSX .plist files. 2 3 The PropertyList (.plist) file format is a simple XML pickle supporting 4 basic object types, like dictionaries, lists, numbers and strings. 5 Usually the top level object is a dictionary. 6 7 To write out a plist file, use the writePlist(rootObject, pathOrFile) 8 function. 'rootObject' is the top level object, 'pathOrFile' is a 9 filename or a (writable) file object. 10 11 To parse a plist from a file, use the readPlist(pathOrFile) function, 12 with a file name or a (readable) file object as the only argument. It 13 returns the top level object (again, usually a dictionary). 14 15 To work with plist data in strings, you can use readPlistFromString() 16 and writePlistToString(). 17 18 Values can be strings, integers, floats, booleans, tuples, lists, 19 dictionaries, Data or datetime.datetime objects. String values (including 20 dictionary keys) may be unicode strings -- they will be written out as 21 UTF-8. 22 23 The <data> plist type is supported through the Data class. This is a 24 thin wrapper around a Python string. 25 26 Generate Plist example: 27 28 pl = dict( 29 aString="Doodah", 30 aList=["A", "B", 12, 32.1, [1, 2, 3]], 31 aFloat=0.1, 32 anInt=728, 33 aDict=dict( 34 anotherString="<hello & hi there!>", 35 aUnicodeValue=u'M\xe4ssig, Ma\xdf', 36 aTrueValue=True, 37 aFalseValue=False, 38 ), 39 someData=Data("<binary gunk>"), 40 someMoreData=Data("<lots of binary gunk>" * 10), 41 aDate=datetime.datetime.fromtimestamp(time.mktime(time.gmtime())), 42 ) 43 # unicode keys are possible, but a little awkward to use: 44 pl[u'\xc5benraa'] = "That was a unicode key." 45 writePlist(pl, fileName) 46 47 Parse Plist example: 48 49 pl = readPlist(pathOrFile) 50 print pl["aKey"] 51 """ 52 53 54 __all__ = [ 55 "readPlist", "writePlist", "readPlistFromString", "writePlistToString", 56 "readPlistFromResource", "writePlistToResource", 57 "Plist", "Data", "Dict" 58 ] 59 # Note: the Plist and Dict classes have been deprecated. 60 61 import binascii 62 import datetime 63 from cStringIO import StringIO 64 import re 65 import warnings 66 67 68 def readPlist(pathOrFile): 69 """Read a .plist file. 'pathOrFile' may either be a file name or a 70 (readable) file object. Return the unpacked root object (which 71 usually is a dictionary). 72 """ 73 didOpen = 0 74 if isinstance(pathOrFile, (str, unicode)): 75 pathOrFile = open(pathOrFile) 76 didOpen = 1 77 p = PlistParser() 78 rootObject = p.parse(pathOrFile) 79 if didOpen: 80 pathOrFile.close() 81 return rootObject 82 83 84 def writePlist(rootObject, pathOrFile): 85 """Write 'rootObject' to a .plist file. 'pathOrFile' may either be a 86 file name or a (writable) file object. 87 """ 88 didOpen = 0 89 if isinstance(pathOrFile, (str, unicode)): 90 pathOrFile = open(pathOrFile, "w") 91 didOpen = 1 92 writer = PlistWriter(pathOrFile) 93 writer.writeln("<plist version=\"1.0\">") 94 writer.writeValue(rootObject) 95 writer.writeln("</plist>") 96 if didOpen: 97 pathOrFile.close() 98 99 100 def readPlistFromString(data): 101 """Read a plist data from a string. Return the root object. 102 """ 103 return readPlist(StringIO(data)) 104 105 106 def writePlistToString(rootObject): 107 """Return 'rootObject' as a plist-formatted string. 108 """ 109 f = StringIO() 110 writePlist(rootObject, f) 111 return f.getvalue() 112 113 114 def readPlistFromResource(path, restype='plst', resid=0): 115 """Read plst resource from the resource fork of path. 116 """ 117 warnings.warnpy3k("In 3.x, readPlistFromResource is removed.", 118 stacklevel=2) 119 from Carbon.File import FSRef, FSGetResourceForkName 120 from Carbon.Files import fsRdPerm 121 from Carbon import Res 122 fsRef = FSRef(path) 123 resNum = Res.FSOpenResourceFile(fsRef, FSGetResourceForkName(), fsRdPerm) 124 Res.UseResFile(resNum) 125 plistData = Res.Get1Resource(restype, resid).data 126 Res.CloseResFile(resNum) 127 return readPlistFromString(plistData) 128 129 130 def writePlistToResource(rootObject, path, restype='plst', resid=0): 131 """Write 'rootObject' as a plst resource to the resource fork of path. 132 """ 133 warnings.warnpy3k("In 3.x, writePlistToResource is removed.", stacklevel=2) 134 from Carbon.File import FSRef, FSGetResourceForkName 135 from Carbon.Files import fsRdWrPerm 136 from Carbon import Res 137 plistData = writePlistToString(rootObject) 138 fsRef = FSRef(path) 139 resNum = Res.FSOpenResourceFile(fsRef, FSGetResourceForkName(), fsRdWrPerm) 140 Res.UseResFile(resNum) 141 try: 142 Res.Get1Resource(restype, resid).RemoveResource() 143 except Res.Error: 144 pass 145 res = Res.Resource(plistData) 146 res.AddResource(restype, resid, '') 147 res.WriteResource() 148 Res.CloseResFile(resNum) 149 150 151 class DumbXMLWriter: 152 153 def __init__(self, file, indentLevel=0, indent="\t"): 154 self.file = file 155 self.stack = [] 156 self.indentLevel = indentLevel 157 self.indent = indent 158 159 def beginElement(self, element): 160 self.stack.append(element) 161 self.writeln("<%s>" % element) 162 self.indentLevel += 1 163 164 def endElement(self, element): 165 assert self.indentLevel > 0 166 assert self.stack.pop() == element 167 self.indentLevel -= 1 168 self.writeln("</%s>" % element) 169 170 def simpleElement(self, element, value=None): 171 if value is not None: 172 value = _escapeAndEncode(value) 173 self.writeln("<%s>%s</%s>" % (element, value, element)) 174 else: 175 self.writeln("<%s/>" % element) 176 177 def writeln(self, line): 178 if line: 179 self.file.write(self.indentLevel * self.indent + line + "\n") 180 else: 181 self.file.write("\n") 182 183 184 # Contents should conform to a subset of ISO 8601 185 # (in particular, YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z'. Smaller units may be omitted with 186 # a loss of precision) 187 _dateParser = re.compile(r"(?P<year>\d\d\d\d)(?:-(?P<month>\d\d)(?:-(?P<day>\d\d)(?:T(?P<hour>\d\d)(?::(?P<minute>\d\d)(?::(?P<second>\d\d))?)?)?)?)?Z") 188 189 def _dateFromString(s): 190 order = ('year', 'month', 'day', 'hour', 'minute', 'second') 191 gd = _dateParser.match(s).groupdict() 192 lst = [] 193 for key in order: 194 val = gd[key] 195 if val is None: 196 break 197 lst.append(int(val)) 198 return datetime.datetime(*lst) 199 200 def _dateToString(d): 201 return '%04d-%02d-%02dT%02d:%02d:%02dZ' % ( 202 d.year, d.month, d.day, 203 d.hour, d.minute, d.second 204 ) 205 206 207 # Regex to find any control chars, except for \t \n and \r 208 _controlCharPat = re.compile( 209 r"[\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f" 210 r"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f]") 211 212 def _escapeAndEncode(text): 213 m = _controlCharPat.search(text) 214 if m is not None: 215 raise ValueError("strings can't contains control characters; " 216 "use plistlib.Data instead") 217 text = text.replace("\r\n", "\n") # convert DOS line endings 218 text = text.replace("\r", "\n") # convert Mac line endings 219 text = text.replace("&", "&") # escape '&' 220 text = text.replace("<", "<") # escape '<' 221 text = text.replace(">", ">") # escape '>' 222 return text.encode("utf-8") # encode as UTF-8 223 224 225 PLISTHEADER = """\ 226 <?xml version="1.0" encoding="UTF-8"?> 227 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 228 """ 229 230 class PlistWriter(DumbXMLWriter): 231 232 def __init__(self, file, indentLevel=0, indent="\t", writeHeader=1): 233 if writeHeader: 234 file.write(PLISTHEADER) 235 DumbXMLWriter.__init__(self, file, indentLevel, indent) 236 237 def writeValue(self, value): 238 if isinstance(value, (str, unicode)): 239 self.simpleElement("string", value) 240 elif isinstance(value, bool): 241 # must switch for bool before int, as bool is a 242 # subclass of int... 243 if value: 244 self.simpleElement("true") 245 else: 246 self.simpleElement("false") 247 elif isinstance(value, (int, long)): 248 self.simpleElement("integer", "%d" % value) 249 elif isinstance(value, float): 250 self.simpleElement("real", repr(value)) 251 elif isinstance(value, dict): 252 self.writeDict(value) 253 elif isinstance(value, Data): 254 self.writeData(value) 255 elif isinstance(value, datetime.datetime): 256 self.simpleElement("date", _dateToString(value)) 257 elif isinstance(value, (tuple, list)): 258 self.writeArray(value) 259 else: 260 raise TypeError("unsuported type: %s" % type(value)) 261 262 def writeData(self, data): 263 self.beginElement("data") 264 self.indentLevel -= 1 265 maxlinelength = 76 - len(self.indent.replace("\t", " " * 8) * 266 self.indentLevel) 267 for line in data.asBase64(maxlinelength).split("\n"): 268 if line: 269 self.writeln(line) 270 self.indentLevel += 1 271 self.endElement("data") 272 273 def writeDict(self, d): 274 self.beginElement("dict") 275 items = d.items() 276 items.sort() 277 for key, value in items: 278 if not isinstance(key, (str, unicode)): 279 raise TypeError("keys must be strings") 280 self.simpleElement("key", key) 281 self.writeValue(value) 282 self.endElement("dict") 283 284 def writeArray(self, array): 285 self.beginElement("array") 286 for value in array: 287 self.writeValue(value) 288 self.endElement("array") 289 290 291 class _InternalDict(dict): 292 293 # This class is needed while Dict is scheduled for deprecation: 294 # we only need to warn when a *user* instantiates Dict or when 295 # the "attribute notation for dict keys" is used. 296 297 def __getattr__(self, attr): 298 try: 299 value = self[attr] 300 except KeyError: 301 raise AttributeError, attr 302 from warnings import warn 303 warn("Attribute access from plist dicts is deprecated, use d[key] " 304 "notation instead", PendingDeprecationWarning, 2) 305 return value 306 307 def __setattr__(self, attr, value): 308 from warnings import warn 309 warn("Attribute access from plist dicts is deprecated, use d[key] " 310 "notation instead", PendingDeprecationWarning, 2) 311 self[attr] = value 312 313 def __delattr__(self, attr): 314 try: 315 del self[attr] 316 except KeyError: 317 raise AttributeError, attr 318 from warnings import warn 319 warn("Attribute access from plist dicts is deprecated, use d[key] " 320 "notation instead", PendingDeprecationWarning, 2) 321 322 class Dict(_InternalDict): 323 324 def __init__(self, **kwargs): 325 from warnings import warn 326 warn("The plistlib.Dict class is deprecated, use builtin dict instead", 327 PendingDeprecationWarning, 2) 328 super(Dict, self).__init__(**kwargs) 329 330 331 class Plist(_InternalDict): 332 333 """This class has been deprecated. Use readPlist() and writePlist() 334 functions instead, together with regular dict objects. 335 """ 336 337 def __init__(self, **kwargs): 338 from warnings import warn 339 warn("The Plist class is deprecated, use the readPlist() and " 340 "writePlist() functions instead", PendingDeprecationWarning, 2) 341 super(Plist, self).__init__(**kwargs) 342 343 def fromFile(cls, pathOrFile): 344 """Deprecated. Use the readPlist() function instead.""" 345 rootObject = readPlist(pathOrFile) 346 plist = cls() 347 plist.update(rootObject) 348 return plist 349 fromFile = classmethod(fromFile) 350 351 def write(self, pathOrFile): 352 """Deprecated. Use the writePlist() function instead.""" 353 writePlist(self, pathOrFile) 354 355 356 def _encodeBase64(s, maxlinelength=76): 357 # copied from base64.encodestring(), with added maxlinelength argument 358 maxbinsize = (maxlinelength//4)*3 359 pieces = [] 360 for i in range(0, len(s), maxbinsize): 361 chunk = s[i : i + maxbinsize] 362 pieces.append(binascii.b2a_base64(chunk)) 363 return "".join(pieces) 364 365 class Data: 366 367 """Wrapper for binary data.""" 368 369 def __init__(self, data): 370 self.data = data 371 372 def fromBase64(cls, data): 373 # base64.decodestring just calls binascii.a2b_base64; 374 # it seems overkill to use both base64 and binascii. 375 return cls(binascii.a2b_base64(data)) 376 fromBase64 = classmethod(fromBase64) 377 378 def asBase64(self, maxlinelength=76): 379 return _encodeBase64(self.data, maxlinelength) 380 381 def __cmp__(self, other): 382 if isinstance(other, self.__class__): 383 return cmp(self.data, other.data) 384 elif isinstance(other, str): 385 return cmp(self.data, other) 386 else: 387 return cmp(id(self), id(other)) 388 389 def __repr__(self): 390 return "%s(%s)" % (self.__class__.__name__, repr(self.data)) 391 392 393 class PlistParser: 394 395 def __init__(self): 396 self.stack = [] 397 self.currentKey = None 398 self.root = None 399 400 def parse(self, fileobj): 401 from xml.parsers.expat import ParserCreate 402 parser = ParserCreate() 403 parser.StartElementHandler = self.handleBeginElement 404 parser.EndElementHandler = self.handleEndElement 405 parser.CharacterDataHandler = self.handleData 406 parser.ParseFile(fileobj) 407 return self.root 408 409 def handleBeginElement(self, element, attrs): 410 self.data = [] 411 handler = getattr(self, "begin_" + element, None) 412 if handler is not None: 413 handler(attrs) 414 415 def handleEndElement(self, element): 416 handler = getattr(self, "end_" + element, None) 417 if handler is not None: 418 handler() 419 420 def handleData(self, data): 421 self.data.append(data) 422 423 def addObject(self, value): 424 if self.currentKey is not None: 425 self.stack[-1][self.currentKey] = value 426 self.currentKey = None 427 elif not self.stack: 428 # this is the root object 429 self.root = value 430 else: 431 self.stack[-1].append(value) 432 433 def getData(self): 434 data = "".join(self.data) 435 try: 436 data = data.encode("ascii") 437 except UnicodeError: 438 pass 439 self.data = [] 440 return data 441 442 # element handlers 443 444 def begin_dict(self, attrs): 445 d = _InternalDict() 446 self.addObject(d) 447 self.stack.append(d) 448 def end_dict(self): 449 self.stack.pop() 450 451 def end_key(self): 452 self.currentKey = self.getData() 453 454 def begin_array(self, attrs): 455 a = [] 456 self.addObject(a) 457 self.stack.append(a) 458 def end_array(self): 459 self.stack.pop() 460 461 def end_true(self): 462 self.addObject(True) 463 def end_false(self): 464 self.addObject(False) 465 def end_integer(self): 466 self.addObject(int(self.getData())) 467 def end_real(self): 468 self.addObject(float(self.getData())) 469 def end_string(self): 470 self.addObject(self.getData()) 471 def end_data(self): 472 self.addObject(Data.fromBase64(self.getData())) 473 def end_date(self): 474 self.addObject(_dateFromString(self.getData())) 475