Home | History | Annotate | Download | only in Lib
      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("&", "&amp;")       # escape '&'
    220     text = text.replace("<", "&lt;")        # escape '<'
    221     text = text.replace(">", "&gt;")        # 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 = max(16, 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