1r"""plistlib.py -- a tool to generate and parse MacOSX .plist files.
2
3The PropertyList (.plist) file format is a simple XML pickle supporting
4basic object types, like dictionaries, lists, numbers and strings.
5Usually the top level object is a dictionary.
6
7To write out a plist file, use the writePlist(rootObject, pathOrFile)
8function. 'rootObject' is the top level object, 'pathOrFile' is a
9filename or a (writable) file object.
10
11To parse a plist from a file, use the readPlist(pathOrFile) function,
12with a file name or a (readable) file object as the only argument. It
13returns the top level object (again, usually a dictionary).
14
15To work with plist data in strings, you can use readPlistFromString()
16and writePlistToString().
17
18Values can be strings, integers, floats, booleans, tuples, lists,
19dictionaries, Data or datetime.datetime objects. String values (including
20dictionary keys) may be unicode strings -- they will be written out as
21UTF-8.
22
23The <data> plist type is supported through the Data class. This is a
24thin wrapper around a Python string.
25
26Generate 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
47Parse 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
61import binascii
62import datetime
63from cStringIO import StringIO
64import re
65import warnings
66
67
68def 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
84def 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
100def readPlistFromString(data):
101    """Read a plist data from a string. Return the root object.
102    """
103    return readPlist(StringIO(data))
104
105
106def writePlistToString(rootObject):
107    """Return 'rootObject' as a plist-formatted string.
108    """
109    f = StringIO()
110    writePlist(rootObject, f)
111    return f.getvalue()
112
113
114def 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
130def 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
151class 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
189def _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
200def _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
212def _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
225PLISTHEADER = """\
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
230class 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
291class _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
322class 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
331class 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
356def _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
365class 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
393class 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