diff options
Diffstat (limited to 'Lib/plistlib.py')
-rw-r--r-- | Lib/plistlib.py | 469 |
1 files changed, 469 insertions, 0 deletions
diff --git a/Lib/plistlib.py b/Lib/plistlib.py new file mode 100644 index 0000000..db5eea1 --- /dev/null +++ b/Lib/plistlib.py @@ -0,0 +1,469 @@ +"""plistlib.py -- a tool to generate and parse MacOSX .plist files. + +The PropertList (.plist) file format is a simple XML pickle supporting +basic object types, like dictionaries, lists, numbers and strings. +Usually the top level object is a dictionary. + +To write out a plist file, use the writePlist(rootObject, pathOrFile) +function. 'rootObject' is the top level object, 'pathOrFile' is a +filename or a (writable) file object. + +To parse a plist from a file, use the readPlist(pathOrFile) function, +with a file name or a (readable) file object as the only argument. It +returns the top level object (again, usually a dictionary). + +To work with plist data in bytes objects, you can use readPlistFromBytes() +and writePlistToBytes(). + +Values can be strings, integers, floats, booleans, tuples, lists, +dictionaries, Data or datetime.datetime objects. String values (including +dictionary keys) may be unicode strings -- they will be written out as +UTF-8. + +The <data> plist type is supported through the Data class. This is a +thin wrapper around a Python bytes object. + +Generate Plist example: + + pl = dict( + aString="Doodah", + aList=["A", "B", 12, 32.1, [1, 2, 3]], + aFloat = 0.1, + anInt = 728, + aDict=dict( + anotherString="<hello & hi there!>", + aUnicodeValue=u'M\xe4ssig, Ma\xdf', + aTrueValue=True, + aFalseValue=False, + ), + someData = Data(b"<binary gunk>"), + someMoreData = Data(b"<lots of binary gunk>" * 10), + aDate = datetime.datetime.fromtimestamp(time.mktime(time.gmtime())), + ) + # unicode keys are possible, but a little awkward to use: + pl[u'\xc5benraa'] = "That was a unicode key." + writePlist(pl, fileName) + +Parse Plist example: + + pl = readPlist(pathOrFile) + print pl["aKey"] +""" + + +__all__ = [ + "readPlist", "writePlist", "readPlistFromBytes", "writePlistToBytes", + "readPlistFromResource", "writePlistToResource", + "Plist", "Data", "Dict" +] +# Note: the Plist and Dict classes have been deprecated. + +import binascii +import datetime +from io import BytesIO +import re + + +def readPlist(pathOrFile): + """Read a .plist file. 'pathOrFile' may either be a file name or a + (readable) file object. Return the unpacked root object (which + usually is a dictionary). + """ + didOpen = False + if isinstance(pathOrFile, str): + pathOrFile = open(pathOrFile, 'rb') + didOpen = True + p = PlistParser() + rootObject = p.parse(pathOrFile) + if didOpen: + pathOrFile.close() + return rootObject + + +def writePlist(rootObject, pathOrFile): + """Write 'rootObject' to a .plist file. 'pathOrFile' may either be a + file name or a (writable) file object. + """ + didOpen = False + if isinstance(pathOrFile, str): + pathOrFile = open(pathOrFile, 'wb') + didOpen = True + writer = PlistWriter(pathOrFile) + writer.writeln("<plist version=\"1.0\">") + writer.writeValue(rootObject) + writer.writeln("</plist>") + if didOpen: + pathOrFile.close() + + +def readPlistFromBytes(data): + """Read a plist data from a bytes object. Return the root object. + """ + return readPlist(BytesIO(data)) + + +def writePlistToBytes(rootObject): + """Return 'rootObject' as a plist-formatted bytes object. + """ + f = BytesIO() + writePlist(rootObject, f) + return f.getvalue() + + +def readPlistFromResource(path, restype='plst', resid=0): + """Read plst resource from the resource fork of path. + """ + from Carbon.File import FSRef, FSGetResourceForkName + from Carbon.Files import fsRdPerm + from Carbon import Res + fsRef = FSRef(path) + resNum = Res.FSOpenResourceFile(fsRef, FSGetResourceForkName(), fsRdPerm) + Res.UseResFile(resNum) + plistData = Res.Get1Resource(restype, resid).data + Res.CloseResFile(resNum) + return readPlistFromString(plistData) + + +def writePlistToResource(rootObject, path, restype='plst', resid=0): + """Write 'rootObject' as a plst resource to the resource fork of path. + """ + from Carbon.File import FSRef, FSGetResourceForkName + from Carbon.Files import fsRdWrPerm + from Carbon import Res + plistData = writePlistToString(rootObject) + fsRef = FSRef(path) + resNum = Res.FSOpenResourceFile(fsRef, FSGetResourceForkName(), fsRdWrPerm) + Res.UseResFile(resNum) + try: + Res.Get1Resource(restype, resid).RemoveResource() + except Res.Error: + pass + res = Res.Resource(plistData) + res.AddResource(restype, resid, '') + res.WriteResource() + Res.CloseResFile(resNum) + + +class DumbXMLWriter: + def __init__(self, file, indentLevel=0, indent="\t"): + self.file = file + self.stack = [] + self.indentLevel = indentLevel + self.indent = indent + + def beginElement(self, element): + self.stack.append(element) + self.writeln("<%s>" % element) + self.indentLevel += 1 + + def endElement(self, element): + assert self.indentLevel > 0 + assert self.stack.pop() == element + self.indentLevel -= 1 + self.writeln("</%s>" % element) + + def simpleElement(self, element, value=None): + if value is not None: + value = _escape(value) + self.writeln("<%s>%s</%s>" % (element, value, element)) + else: + self.writeln("<%s/>" % element) + + def writeln(self, line): + if line: + # plist has fixed encoding of utf-8 + if isinstance(line, str): + line = line.encode('utf-8') + self.file.write(self.indentLevel * self.indent) + self.file.write(line) + self.file.write(b'\n') + + +# Contents should conform to a subset of ISO 8601 +# (in particular, YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z'. Smaller units may be omitted with +# a loss of precision) +_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") + +def _dateFromString(s): + order = ('year', 'month', 'day', 'hour', 'minute', 'second') + gd = _dateParser.match(s).groupdict() + lst = [] + for key in order: + val = gd[key] + if val is None: + break + lst.append(int(val)) + return datetime.datetime(*lst) + +def _dateToString(d): + return '%04d-%02d-%02dT%02d:%02d:%02dZ' % ( + d.year, d.month, d.day, + d.hour, d.minute, d.second + ) + + +# Regex to find any control chars, except for \t \n and \r +_controlCharPat = re.compile( + r"[\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f" + r"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f]") + +def _escape(text): + m = _controlCharPat.search(text) + if m is not None: + raise ValueError("strings can't contains control characters; " + "use plistlib.Data instead") + text = text.replace("\r\n", "\n") # convert DOS line endings + text = text.replace("\r", "\n") # convert Mac line endings + text = text.replace("&", "&") # escape '&' + text = text.replace("<", "<") # escape '<' + text = text.replace(">", ">") # escape '>' + return text + + +PLISTHEADER = b"""\ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +""" + +class PlistWriter(DumbXMLWriter): + + def __init__(self, file, indentLevel=0, indent=b"\t", writeHeader=1): + if writeHeader: + file.write(PLISTHEADER) + DumbXMLWriter.__init__(self, file, indentLevel, indent) + + def writeValue(self, value): + if isinstance(value, str): + self.simpleElement("string", value) + elif isinstance(value, bool): + # must switch for bool before int, as bool is a + # subclass of int... + if value: + self.simpleElement("true") + else: + self.simpleElement("false") + elif isinstance(value, int): + self.simpleElement("integer", "%d" % value) + elif isinstance(value, float): + self.simpleElement("real", repr(value)) + elif isinstance(value, dict): + self.writeDict(value) + elif isinstance(value, Data): + self.writeData(value) + elif isinstance(value, datetime.datetime): + self.simpleElement("date", _dateToString(value)) + elif isinstance(value, (tuple, list)): + self.writeArray(value) + else: + raise TypeError("unsuported type: %s" % type(value)) + + def writeData(self, data): + self.beginElement("data") + self.indentLevel -= 1 + maxlinelength = 76 - len(self.indent.replace(b"\t", b" " * 8) * + self.indentLevel) + for line in data.asBase64(maxlinelength).split(b"\n"): + if line: + self.writeln(line) + self.indentLevel += 1 + self.endElement("data") + + def writeDict(self, d): + self.beginElement("dict") + items = sorted(d.items()) + for key, value in items: + if not isinstance(key, str): + raise TypeError("keys must be strings") + self.simpleElement("key", key) + self.writeValue(value) + self.endElement("dict") + + def writeArray(self, array): + self.beginElement("array") + for value in array: + self.writeValue(value) + self.endElement("array") + + +class _InternalDict(dict): + + # This class is needed while Dict is scheduled for deprecation: + # we only need to warn when a *user* instantiates Dict or when + # the "attribute notation for dict keys" is used. + + def __getattr__(self, attr): + try: + value = self[attr] + except KeyError: + raise AttributeError(attr) + from warnings import warn + warn("Attribute access from plist dicts is deprecated, use d[key] " + "notation instead", PendingDeprecationWarning) + return value + + def __setattr__(self, attr, value): + from warnings import warn + warn("Attribute access from plist dicts is deprecated, use d[key] " + "notation instead", PendingDeprecationWarning) + self[attr] = value + + def __delattr__(self, attr): + try: + del self[attr] + except KeyError: + raise AttributeError(attr) + from warnings import warn + warn("Attribute access from plist dicts is deprecated, use d[key] " + "notation instead", PendingDeprecationWarning) + +class Dict(_InternalDict): + + def __init__(self, **kwargs): + from warnings import warn + warn("The plistlib.Dict class is deprecated, use builtin dict instead", + PendingDeprecationWarning) + super().__init__(**kwargs) + + +class Plist(_InternalDict): + + """This class has been deprecated. Use readPlist() and writePlist() + functions instead, together with regular dict objects. + """ + + def __init__(self, **kwargs): + from warnings import warn + warn("The Plist class is deprecated, use the readPlist() and " + "writePlist() functions instead", PendingDeprecationWarning) + super().__init__(**kwargs) + + def fromFile(cls, pathOrFile): + """Deprecated. Use the readPlist() function instead.""" + rootObject = readPlist(pathOrFile) + plist = cls() + plist.update(rootObject) + return plist + fromFile = classmethod(fromFile) + + def write(self, pathOrFile): + """Deprecated. Use the writePlist() function instead.""" + writePlist(self, pathOrFile) + + +def _encodeBase64(s, maxlinelength=76): + # copied from base64.encodestring(), with added maxlinelength argument + maxbinsize = (maxlinelength//4)*3 + pieces = [] + for i in range(0, len(s), maxbinsize): + chunk = s[i : i + maxbinsize] + pieces.append(binascii.b2a_base64(chunk)) + return b''.join(pieces) + +class Data: + + """Wrapper for binary data.""" + + def __init__(self, data): + if not isinstance(data, bytes): + raise TypeError("data must be as bytes") + self.data = data + + @classmethod + def fromBase64(cls, data): + # base64.decodestring just calls binascii.a2b_base64; + # it seems overkill to use both base64 and binascii. + return cls(binascii.a2b_base64(data)) + + def asBase64(self, maxlinelength=76): + return _encodeBase64(self.data, maxlinelength) + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self.data == other.data + elif isinstance(other, str): + return self.data == other + else: + return id(self) == id(other) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self.data)) + + +class PlistParser: + + def __init__(self): + self.stack = [] + self.currentKey = None + self.root = None + + def parse(self, fileobj): + from xml.parsers.expat import ParserCreate + parser = ParserCreate() + parser.StartElementHandler = self.handleBeginElement + parser.EndElementHandler = self.handleEndElement + parser.CharacterDataHandler = self.handleData + parser.ParseFile(fileobj) + return self.root + + def handleBeginElement(self, element, attrs): + self.data = [] + handler = getattr(self, "begin_" + element, None) + if handler is not None: + handler(attrs) + + def handleEndElement(self, element): + handler = getattr(self, "end_" + element, None) + if handler is not None: + handler() + + def handleData(self, data): + self.data.append(data) + + def addObject(self, value): + if self.currentKey is not None: + self.stack[-1][self.currentKey] = value + self.currentKey = None + elif not self.stack: + # this is the root object + self.root = value + else: + self.stack[-1].append(value) + + def getData(self): + data = ''.join(self.data) + self.data = [] + return data + + # element handlers + + def begin_dict(self, attrs): + d = _InternalDict() + self.addObject(d) + self.stack.append(d) + def end_dict(self): + self.stack.pop() + + def end_key(self): + self.currentKey = self.getData() + + def begin_array(self, attrs): + a = [] + self.addObject(a) + self.stack.append(a) + def end_array(self): + self.stack.pop() + + def end_true(self): + self.addObject(True) + def end_false(self): + self.addObject(False) + def end_integer(self): + self.addObject(int(self.getData())) + def end_real(self): + self.addObject(float(self.getData())) + def end_string(self): + self.addObject(self.getData()) + def end_data(self): + self.addObject(Data.fromBase64(self.getData().encode("utf-8"))) + def end_date(self): + self.addObject(_dateFromString(self.getData())) |