summaryrefslogtreecommitdiffstats
path: root/Lib/plistlib.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/plistlib.py')
-rw-r--r--Lib/plistlib.py469
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("&", "&amp;") # escape '&'
+ text = text.replace("<", "&lt;") # escape '<'
+ text = text.replace(">", "&gt;") # 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()))