summaryrefslogtreecommitdiff
path: root/Lib/plistlib.py
diff options
context:
space:
mode:
authorRonald Oussoren <ronaldoussoren@mac.com>2013-11-21 15:46:49 +0100
committerRonald Oussoren <ronaldoussoren@mac.com>2013-11-21 15:46:49 +0100
commitc5cf7973422dce0ed59849aaf2d708d4c6b7320d (patch)
tree5d34eeec1f0af4bd2805464b305e18d27af63ca9 /Lib/plistlib.py
parent8455723cfb0cdb0fc8d908210fa21b63b9d09a2b (diff)
downloadcpython-git-c5cf7973422dce0ed59849aaf2d708d4c6b7320d.tar.gz
Issue #14455: plistlib now supports binary plists and has an updated API.
This patch adds support for binary plists on OSX to plistlib (based on a patch by 'dpounces'). The patch also cleans up the API for the plistlib module.
Diffstat (limited to 'Lib/plistlib.py')
-rw-r--r--Lib/plistlib.py1098
1 files changed, 827 insertions, 271 deletions
diff --git a/Lib/plistlib.py b/Lib/plistlib.py
index 2b0b634100..bdb8ab352e 100644
--- a/Lib/plistlib.py
+++ b/Lib/plistlib.py
@@ -4,25 +4,20 @@ The property list (.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 write out a plist file, use the dump(value, file)
+function. 'value' is the top level object, 'file' is
+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
+To parse a plist from a file, use the load(file) function,
+with 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().
+To work with plist data in bytes objects, you can use loads()
+and dumps().
Values can be strings, integers, floats, booleans, tuples, lists,
-dictionaries (but only with string keys), Data or datetime.datetime objects.
-String values (including dictionary keys) have to 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. Use 'Data' if your strings
-contain control characters.
+dictionaries (but only with string keys), Data, bytes, bytearray, or
+datetime.datetime objects.
Generate Plist example:
@@ -37,226 +32,48 @@ Generate Plist example:
aTrueValue = True,
aFalseValue = False,
),
- someData = Data(b"<binary gunk>"),
- someMoreData = Data(b"<lots of binary gunk>" * 10),
+ someData = b"<binary gunk>",
+ someMoreData = b"<lots of binary gunk>" * 10,
aDate = datetime.datetime.fromtimestamp(time.mktime(time.gmtime())),
)
- writePlist(pl, fileName)
+ with open(fileName, 'wb') as fp:
+ dump(pl, fp)
Parse Plist example:
- pl = readPlist(pathOrFile)
- print pl["aKey"]
+ with open(fileName, 'rb') as fp:
+ pl = load(fp)
+ print(pl["aKey"])
"""
-
-
__all__ = [
"readPlist", "writePlist", "readPlistFromBytes", "writePlistToBytes",
- "Plist", "Data", "Dict"
+ "Plist", "Data", "Dict", "FMT_XML", "FMT_BINARY",
+ "load", "dump", "loads", "dumps"
]
-# Note: the Plist and Dict classes have been deprecated.
import binascii
+import codecs
+import contextlib
import datetime
+import enum
from io import BytesIO
+import itertools
+import os
import re
+import struct
+from warnings import warn
+from xml.parsers.expat import ParserCreate
-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
- try:
- if isinstance(pathOrFile, str):
- pathOrFile = open(pathOrFile, 'rb')
- didOpen = True
- p = PlistParser()
- rootObject = p.parse(pathOrFile)
- finally:
- 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
- try:
- 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>")
- finally:
- 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()
-
-
-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", re.ASCII)
-
-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//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("unsupported type: %s" % type(value))
-
- def writeData(self, data):
- self.beginElement("data")
- self.indentLevel -= 1
- maxlinelength = max(16, 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")
+PlistFormat = enum.Enum('PlistFormat', 'FMT_XML FMT_BINARY', module=__name__)
+globals().update(PlistFormat.__members__)
- def writeDict(self, d):
- if 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")
- else:
- self.simpleElement("dict")
- def writeArray(self, array):
- if array:
- self.beginElement("array")
- for value in array:
- self.writeValue(value)
- self.endElement("array")
- else:
- self.simpleElement("array")
+#
+#
+# Deprecated functionality
+#
+#
class _InternalDict(dict):
@@ -264,19 +81,18 @@ 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.
+ __slots__ = ()
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", DeprecationWarning, 2)
return value
def __setattr__(self, attr, value):
- from warnings import warn
warn("Attribute access from plist dicts is deprecated, use d[key] "
"notation instead", DeprecationWarning, 2)
self[attr] = value
@@ -286,56 +102,111 @@ class _InternalDict(dict):
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", DeprecationWarning, 2)
+
class Dict(_InternalDict):
def __init__(self, **kwargs):
- from warnings import warn
warn("The plistlib.Dict class is deprecated, use builtin dict instead",
DeprecationWarning, 2)
super().__init__(**kwargs)
-class Plist(_InternalDict):
+@contextlib.contextmanager
+def _maybe_open(pathOrFile, mode):
+ if isinstance(pathOrFile, str):
+ with open(pathOrFile, mode) as fp:
+ yield fp
+
+ else:
+ yield pathOrFile
+
- """This class has been deprecated. Use readPlist() and writePlist()
+class Plist(_InternalDict):
+ """This class has been deprecated. Use dump() and load()
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", DeprecationWarning, 2)
+ warn("The Plist class is deprecated, use the load() and "
+ "dump() functions instead", DeprecationWarning, 2)
super().__init__(**kwargs)
+ @classmethod
def fromFile(cls, pathOrFile):
- """Deprecated. Use the readPlist() function instead."""
- rootObject = readPlist(pathOrFile)
+ """Deprecated. Use the load() function instead."""
+ with maybe_open(pathOrFile, 'rb') as fp:
+ value = load(fp)
plist = cls()
- plist.update(rootObject)
+ plist.update(value)
return plist
- fromFile = classmethod(fromFile)
def write(self, pathOrFile):
- """Deprecated. Use the writePlist() function instead."""
- writePlist(self, pathOrFile)
+ """Deprecated. Use the dump() function instead."""
+ with _maybe_open(pathOrFile, 'wb') as fp:
+ dump(self, fp)
-def _encodeBase64(s, maxlinelength=76):
- # copied from base64.encodebytes(), 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)
+def readPlist(pathOrFile):
+ """
+ Read a .plist from a path or file. pathOrFile should either
+ be a file name, or a readable binary file object.
+
+ This function is deprecated, use load instead.
+ """
+ warn("The readPlist function is deprecated, use load() instead",
+ DeprecationWarning, 2)
+
+ with _maybe_open(pathOrFile, 'rb') as fp:
+ return load(fp, fmt=None, use_builtin_types=False,
+ dict_type=_InternalDict)
+
+def writePlist(value, pathOrFile):
+ """
+ Write 'value' to a .plist file. 'pathOrFile' may either be a
+ file name or a (writable) file object.
+
+ This function is deprecated, use dump instead.
+ """
+ warn("The writePlist function is deprecated, use dump() instead",
+ DeprecationWarning, 2)
+ with _maybe_open(pathOrFile, 'wb') as fp:
+ dump(value, fp, fmt=FMT_XML, sort_keys=True, skipkeys=False)
+
+
+def readPlistFromBytes(data):
+ """
+ Read a plist data from a bytes object. Return the root object.
+
+ This function is deprecated, use loads instead.
+ """
+ warn("The readPlistFromBytes function is deprecated, use loads() instead",
+ DeprecationWarning, 2)
+ return load(BytesIO(data), fmt=None, use_builtin_types=False,
+ dict_type=_InternalDict)
+
+
+def writePlistToBytes(value):
+ """
+ Return 'value' as a plist-formatted bytes object.
+
+ This function is deprecated, use dumps instead.
+ """
+ warn("The writePlistToBytes function is deprecated, use dumps() instead",
+ DeprecationWarning, 2)
+ f = BytesIO()
+ dump(value, f, fmt=FMT_XML, sort_keys=True, skipkeys=False)
+ return f.getvalue()
+
class Data:
+ """
+ Wrapper for binary data.
- """Wrapper for binary data."""
+ This class is deprecated, use a bytes object instead.
+ """
def __init__(self, data):
if not isinstance(data, bytes):
@@ -346,10 +217,10 @@ class Data:
def fromBase64(cls, data):
# base64.decodebytes just calls binascii.a2b_base64;
# it seems overkill to use both base64 and binascii.
- return cls(binascii.a2b_base64(data))
+ return cls(_decode_base64(data))
def asBase64(self, maxlinelength=76):
- return _encodeBase64(self.data, maxlinelength)
+ return _encode_base64(self.data, maxlinelength)
def __eq__(self, other):
if isinstance(other, self.__class__):
@@ -362,43 +233,119 @@ class Data:
def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, repr(self.data))
-class PlistParser:
+#
+#
+# End of deprecated functionality
+#
+#
+
+
+#
+# XML support
+#
+
+
+# XML 'header'
+PLISTHEADER = b"""\
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+"""
+
+
+# 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 _encode_base64(s, maxlinelength=76):
+ # copied from base64.encodebytes(), 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)
+
+def _decode_base64(s):
+ if isinstance(s, str):
+ return binascii.a2b_base64(s.encode("utf-8"))
+
+ else:
+ return binascii.a2b_base64(s)
- def __init__(self):
+# 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", re.ASCII)
+
+
+def _date_from_string(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 _date_to_string(d):
+ return '%04d-%02d-%02dT%02d:%02d:%02dZ' % (
+ d.year, d.month, d.day,
+ d.hour, d.minute, d.second
+ )
+
+def _escape(text):
+ m = _controlCharPat.search(text)
+ if m is not None:
+ raise ValueError("strings can't contains control characters; "
+ "use bytes 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
+
+class _PlistParser:
+ def __init__(self, use_builtin_types, dict_type):
self.stack = []
- self.currentKey = None
+ self.current_key = None
self.root = None
+ self._use_builtin_types = use_builtin_types
+ self._dict_type = dict_type
def parse(self, fileobj):
- from xml.parsers.expat import ParserCreate
self.parser = ParserCreate()
- self.parser.StartElementHandler = self.handleBeginElement
- self.parser.EndElementHandler = self.handleEndElement
- self.parser.CharacterDataHandler = self.handleData
+ self.parser.StartElementHandler = self.handle_begin_element
+ self.parser.EndElementHandler = self.handle_end_element
+ self.parser.CharacterDataHandler = self.handle_data
self.parser.ParseFile(fileobj)
return self.root
- def handleBeginElement(self, element, attrs):
+ def handle_begin_element(self, element, attrs):
self.data = []
handler = getattr(self, "begin_" + element, None)
if handler is not None:
handler(attrs)
- def handleEndElement(self, element):
+ def handle_end_element(self, element):
handler = getattr(self, "end_" + element, None)
if handler is not None:
handler()
- def handleData(self, data):
+ def handle_data(self, data):
self.data.append(data)
- def addObject(self, value):
- if self.currentKey is not None:
+ def add_object(self, value):
+ if self.current_key is not None:
if not isinstance(self.stack[-1], type({})):
raise ValueError("unexpected element at line %d" %
self.parser.CurrentLineNumber)
- self.stack[-1][self.currentKey] = value
- self.currentKey = None
+ self.stack[-1][self.current_key] = value
+ self.current_key = None
elif not self.stack:
# this is the root object
self.root = value
@@ -408,7 +355,7 @@ class PlistParser:
self.parser.CurrentLineNumber)
self.stack[-1].append(value)
- def getData(self):
+ def get_data(self):
data = ''.join(self.data)
self.data = []
return data
@@ -416,39 +363,648 @@ class PlistParser:
# element handlers
def begin_dict(self, attrs):
- d = _InternalDict()
- self.addObject(d)
+ d = self._dict_type()
+ self.add_object(d)
self.stack.append(d)
+
def end_dict(self):
- if self.currentKey:
+ if self.current_key:
raise ValueError("missing value for key '%s' at line %d" %
- (self.currentKey,self.parser.CurrentLineNumber))
+ (self.current_key,self.parser.CurrentLineNumber))
self.stack.pop()
def end_key(self):
- if self.currentKey or not isinstance(self.stack[-1], type({})):
+ if self.current_key or not isinstance(self.stack[-1], type({})):
raise ValueError("unexpected key at line %d" %
self.parser.CurrentLineNumber)
- self.currentKey = self.getData()
+ self.current_key = self.get_data()
def begin_array(self, attrs):
a = []
- self.addObject(a)
+ self.add_object(a)
self.stack.append(a)
+
def end_array(self):
self.stack.pop()
def end_true(self):
- self.addObject(True)
+ self.add_object(True)
+
def end_false(self):
- self.addObject(False)
+ self.add_object(False)
+
def end_integer(self):
- self.addObject(int(self.getData()))
+ self.add_object(int(self.get_data()))
+
def end_real(self):
- self.addObject(float(self.getData()))
+ self.add_object(float(self.get_data()))
+
def end_string(self):
- self.addObject(self.getData())
+ self.add_object(self.get_data())
+
def end_data(self):
- self.addObject(Data.fromBase64(self.getData().encode("utf-8")))
+ if self._use_builtin_types:
+ self.add_object(_decode_base64(self.get_data()))
+
+ else:
+ self.add_object(Data.fromBase64(self.get_data()))
+
def end_date(self):
- self.addObject(_dateFromString(self.getData()))
+ self.add_object(_date_from_string(self.get_data()))
+
+
+class _DumbXMLWriter:
+ def __init__(self, file, indent_level=0, indent="\t"):
+ self.file = file
+ self.stack = []
+ self._indent_level = indent_level
+ self.indent = indent
+
+ def begin_element(self, element):
+ self.stack.append(element)
+ self.writeln("<%s>" % element)
+ self._indent_level += 1
+
+ def end_element(self, element):
+ assert self._indent_level > 0
+ assert self.stack.pop() == element
+ self._indent_level -= 1
+ self.writeln("</%s>" % element)
+
+ def simple_element(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
+
+ # XXX: is this test needed?
+ if isinstance(line, str):
+ line = line.encode('utf-8')
+ self.file.write(self._indent_level * self.indent)
+ self.file.write(line)
+ self.file.write(b'\n')
+
+
+class _PlistWriter(_DumbXMLWriter):
+ def __init__(
+ self, file, indent_level=0, indent=b"\t", writeHeader=1,
+ sort_keys=True, skipkeys=False):
+
+ if writeHeader:
+ file.write(PLISTHEADER)
+ _DumbXMLWriter.__init__(self, file, indent_level, indent)
+ self._sort_keys = sort_keys
+ self._skipkeys = skipkeys
+
+ def write(self, value):
+ self.writeln("<plist version=\"1.0\">")
+ self.write_value(value)
+ self.writeln("</plist>")
+
+ def write_value(self, value):
+ if isinstance(value, str):
+ self.simple_element("string", value)
+
+ elif value is True:
+ self.simple_element("true")
+
+ elif value is False:
+ self.simple_element("false")
+
+ elif isinstance(value, int):
+ self.simple_element("integer", "%d" % value)
+
+ elif isinstance(value, float):
+ self.simple_element("real", repr(value))
+
+ elif isinstance(value, dict):
+ self.write_dict(value)
+
+ elif isinstance(value, Data):
+ self.write_data(value)
+
+ elif isinstance(value, (bytes, bytearray)):
+ self.write_bytes(value)
+
+ elif isinstance(value, datetime.datetime):
+ self.simple_element("date", _date_to_string(value))
+
+ elif isinstance(value, (tuple, list)):
+ self.write_array(value)
+
+ else:
+ raise TypeError("unsupported type: %s" % type(value))
+
+ def write_data(self, data):
+ self.write_bytes(data.data)
+
+ def write_bytes(self, data):
+ self.begin_element("data")
+ self._indent_level -= 1
+ maxlinelength = max(
+ 16,
+ 76 - len(self.indent.replace(b"\t", b" " * 8) * self._indent_level))
+
+ for line in _encode_base64(data, maxlinelength).split(b"\n"):
+ if line:
+ self.writeln(line)
+ self._indent_level += 1
+ self.end_element("data")
+
+ def write_dict(self, d):
+ if d:
+ self.begin_element("dict")
+ if self._sort_keys:
+ items = sorted(d.items())
+ else:
+ items = d.items()
+
+ for key, value in items:
+ if not isinstance(key, str):
+ if self._skipkeys:
+ continue
+ raise TypeError("keys must be strings")
+ self.simple_element("key", key)
+ self.write_value(value)
+ self.end_element("dict")
+
+ else:
+ self.simple_element("dict")
+
+ def write_array(self, array):
+ if array:
+ self.begin_element("array")
+ for value in array:
+ self.write_value(value)
+ self.end_element("array")
+
+ else:
+ self.simple_element("array")
+
+
+def _is_fmt_xml(header):
+ prefixes = (b'<?xml', b'<plist')
+
+ for pfx in prefixes:
+ if header.startswith(pfx):
+ return True
+
+ # Also check for alternative XML encodings, this is slightly
+ # overkill because the Apple tools (and plistlib) will not
+ # generate files with these encodings.
+ for bom, encoding in (
+ (codecs.BOM_UTF8, "utf-8"),
+ (codecs.BOM_UTF16_BE, "utf-16-be"),
+ (codecs.BOM_UTF16_LE, "utf-16-le"),
+ # expat does not support utf-32
+ #(codecs.BOM_UTF32_BE, "utf-32-be"),
+ #(codecs.BOM_UTF32_LE, "utf-32-le"),
+ ):
+ if not header.startswith(bom):
+ continue
+
+ for start in prefixes:
+ prefix = bom + start.decode('ascii').encode(encoding)
+ if header[:len(prefix)] == prefix:
+ return True
+
+ return False
+
+#
+# Binary Plist
+#
+
+
+class InvalidFileException (ValueError):
+ def __init__(self, message="Invalid file"):
+ ValueError.__init__(self, message)
+
+_BINARY_FORMAT = {1: 'B', 2: 'H', 4: 'L', 8: 'Q'}
+
+class _BinaryPlistParser:
+ """
+ Read or write a binary plist file, following the description of the binary
+ format. Raise InvalidFileException in case of error, otherwise return the
+ root object.
+
+ see also: http://opensource.apple.com/source/CF/CF-744.18/CFBinaryPList.c
+ """
+ def __init__(self, use_builtin_types, dict_type):
+ self._use_builtin_types = use_builtin_types
+ self._dict_type = dict_type
+
+ def parse(self, fp):
+ try:
+ # The basic file format:
+ # HEADER
+ # object...
+ # refid->offset...
+ # TRAILER
+ self._fp = fp
+ self._fp.seek(-32, os.SEEK_END)
+ trailer = self._fp.read(32)
+ if len(trailer) != 32:
+ raise InvalidFileException()
+ (
+ offset_size, self._ref_size, num_objects, top_object,
+ offset_table_offset
+ ) = struct.unpack('>6xBBQQQ', trailer)
+ self._fp.seek(offset_table_offset)
+ offset_format = '>' + _BINARY_FORMAT[offset_size] * num_objects
+ self._ref_format = _BINARY_FORMAT[self._ref_size]
+ self._object_offsets = struct.unpack(
+ offset_format, self._fp.read(offset_size * num_objects))
+ return self._read_object(self._object_offsets[top_object])
+
+ except (OSError, IndexError, struct.error):
+ raise InvalidFileException()
+
+ def _get_size(self, tokenL):
+ """ return the size of the next object."""
+ if tokenL == 0xF:
+ m = self._fp.read(1)[0] & 0x3
+ s = 1 << m
+ f = '>' + _BINARY_FORMAT[s]
+ return struct.unpack(f, self._fp.read(s))[0]
+
+ return tokenL
+
+ def _read_refs(self, n):
+ return struct.unpack(
+ '>' + self._ref_format * n, self._fp.read(n * self._ref_size))
+
+ def _read_object(self, offset):
+ """
+ read the object at offset.
+
+ May recursively read sub-objects (content of an array/dict/set)
+ """
+ self._fp.seek(offset)
+ token = self._fp.read(1)[0]
+ tokenH, tokenL = token & 0xF0, token & 0x0F
+
+ if token == 0x00:
+ return None
+
+ elif token == 0x08:
+ return False
+
+ elif token == 0x09:
+ return True
+
+ # The referenced source code also mentions URL (0x0c, 0x0d) and
+ # UUID (0x0e), but neither can be generated using the Cocoa libraries.
+
+ elif token == 0x0f:
+ return b''
+
+ elif tokenH == 0x10: # int
+ return int.from_bytes(self._fp.read(1 << tokenL), 'big')
+
+ elif token == 0x22: # real
+ return struct.unpack('>f', self._fp.read(4))[0]
+
+ elif token == 0x23: # real
+ return struct.unpack('>d', self._fp.read(8))[0]
+
+ elif token == 0x33: # date
+ f = struct.unpack('>d', self._fp.read(8))[0]
+ # timestamp 0 of binary plists corresponds to 1/1/2001
+ # (year of Mac OS X 10.0), instead of 1/1/1970.
+ return datetime.datetime.utcfromtimestamp(f + (31 * 365 + 8) * 86400)
+
+ elif tokenH == 0x40: # data
+ s = self._get_size(tokenL)
+ if self._use_builtin_types:
+ return self._fp.read(s)
+ else:
+ return Data(self._fp.read(s))
+
+ elif tokenH == 0x50: # ascii string
+ s = self._get_size(tokenL)
+ result = self._fp.read(s).decode('ascii')
+ return result
+
+ elif tokenH == 0x60: # unicode string
+ s = self._get_size(tokenL)
+ return self._fp.read(s * 2).decode('utf-16be')
+
+ # tokenH == 0x80 is documented as 'UID' and appears to be used for
+ # keyed-archiving, not in plists.
+
+ elif tokenH == 0xA0: # array
+ s = self._get_size(tokenL)
+ obj_refs = self._read_refs(s)
+ return [self._read_object(self._object_offsets[x])
+ for x in obj_refs]
+
+ # tokenH == 0xB0 is documented as 'ordset', but is not actually
+ # implemented in the Apple reference code.
+
+ # tokenH == 0xC0 is documented as 'set', but sets cannot be used in
+ # plists.
+
+ elif tokenH == 0xD0: # dict
+ s = self._get_size(tokenL)
+ key_refs = self._read_refs(s)
+ obj_refs = self._read_refs(s)
+ result = self._dict_type()
+ for k, o in zip(key_refs, obj_refs):
+ result[self._read_object(self._object_offsets[k])
+ ] = self._read_object(self._object_offsets[o])
+ return result
+
+ raise InvalidFileException()
+
+def _count_to_size(count):
+ if count < 1 << 8:
+ return 1
+
+ elif count < 1 << 16:
+ return 2
+
+ elif count << 1 << 32:
+ return 4
+
+ else:
+ return 8
+
+class _BinaryPlistWriter (object):
+ def __init__(self, fp, sort_keys, skipkeys):
+ self._fp = fp
+ self._sort_keys = sort_keys
+ self._skipkeys = skipkeys
+
+ def write(self, value):
+
+ # Flattened object list:
+ self._objlist = []
+
+ # Mappings from object->objectid
+ # First dict has (type(object), object) as the key,
+ # second dict is used when object is not hashable and
+ # has id(object) as the key.
+ self._objtable = {}
+ self._objidtable = {}
+
+ # Create list of all objects in the plist
+ self._flatten(value)
+
+ # Size of object references in serialized containers
+ # depends on the number of objects in the plist.
+ num_objects = len(self._objlist)
+ self._object_offsets = [0]*num_objects
+ self._ref_size = _count_to_size(num_objects)
+
+ self._ref_format = _BINARY_FORMAT[self._ref_size]
+
+ # Write file header
+ self._fp.write(b'bplist00')
+
+ # Write object list
+ for obj in self._objlist:
+ self._write_object(obj)
+
+ # Write refnum->object offset table
+ top_object = self._getrefnum(value)
+ offset_table_offset = self._fp.tell()
+ offset_size = _count_to_size(offset_table_offset)
+ offset_format = '>' + _BINARY_FORMAT[offset_size] * num_objects
+ self._fp.write(struct.pack(offset_format, *self._object_offsets))
+
+ # Write trailer
+ sort_version = 0
+ trailer = (
+ sort_version, offset_size, self._ref_size, num_objects,
+ top_object, offset_table_offset
+ )
+ self._fp.write(struct.pack('>5xBBBQQQ', *trailer))
+
+ def _flatten(self, value):
+ # First check if the object is in the object table, not used for
+ # containers to ensure that two subcontainers with the same contents
+ # will be serialized as distinct values.
+ if isinstance(value, (
+ str, int, float, datetime.datetime, bytes, bytearray)):
+ if (type(value), value) in self._objtable:
+ return
+
+ elif isinstance(value, Data):
+ if (type(value.data), value.data) in self._objtable:
+ return
+
+ # Add to objectreference map
+ refnum = len(self._objlist)
+ self._objlist.append(value)
+ try:
+ if isinstance(value, Data):
+ self._objtable[(type(value.data), value.data)] = refnum
+ else:
+ self._objtable[(type(value), value)] = refnum
+ except TypeError:
+ self._objidtable[id(value)] = refnum
+
+ # And finally recurse into containers
+ if isinstance(value, dict):
+ keys = []
+ values = []
+ items = value.items()
+ if self._sort_keys:
+ items = sorted(items)
+
+ for k, v in items:
+ if not isinstance(k, str):
+ if self._skipkeys:
+ continue
+ raise TypeError("keys must be strings")
+ keys.append(k)
+ values.append(v)
+
+ for o in itertools.chain(keys, values):
+ self._flatten(o)
+
+ elif isinstance(value, (list, tuple)):
+ for o in value:
+ self._flatten(o)
+
+ def _getrefnum(self, value):
+ try:
+ if isinstance(value, Data):
+ return self._objtable[(type(value.data), value.data)]
+ else:
+ return self._objtable[(type(value), value)]
+ except TypeError:
+ return self._objidtable[id(value)]
+
+ def _write_size(self, token, size):
+ if size < 15:
+ self._fp.write(struct.pack('>B', token | size))
+
+ elif size < 1 << 8:
+ self._fp.write(struct.pack('>BBB', token | 0xF, 0x10, size))
+
+ elif size < 1 << 16:
+ self._fp.write(struct.pack('>BBH', token | 0xF, 0x11, size))
+
+ elif size < 1 << 32:
+ self._fp.write(struct.pack('>BBL', token | 0xF, 0x12, size))
+
+ else:
+ self._fp.write(struct.pack('>BBQ', token | 0xF, 0x13, size))
+
+ def _write_object(self, value):
+ ref = self._getrefnum(value)
+ self._object_offsets[ref] = self._fp.tell()
+ if value is None:
+ self._fp.write(b'\x00')
+
+ elif value is False:
+ self._fp.write(b'\x08')
+
+ elif value is True:
+ self._fp.write(b'\x09')
+
+ elif isinstance(value, int):
+ if value < 1 << 8:
+ self._fp.write(struct.pack('>BB', 0x10, value))
+ elif value < 1 << 16:
+ self._fp.write(struct.pack('>BH', 0x11, value))
+ elif value < 1 << 32:
+ self._fp.write(struct.pack('>BL', 0x12, value))
+ else:
+ self._fp.write(struct.pack('>BQ', 0x13, value))
+
+ elif isinstance(value, float):
+ self._fp.write(struct.pack('>Bd', 0x23, value))
+
+ elif isinstance(value, datetime.datetime):
+ f = (value - datetime.datetime(2001, 1, 1)).total_seconds()
+ self._fp.write(struct.pack('>Bd', 0x33, f))
+
+ elif isinstance(value, Data):
+ self._write_size(0x40, len(value.data))
+ self._fp.write(value.data)
+
+ elif isinstance(value, (bytes, bytearray)):
+ self._write_size(0x40, len(value))
+ self._fp.write(value)
+
+ elif isinstance(value, str):
+ try:
+ t = value.encode('ascii')
+ self._write_size(0x50, len(value))
+ except UnicodeEncodeError:
+ t = value.encode('utf-16be')
+ self._write_size(0x60, len(value))
+
+ self._fp.write(t)
+
+ elif isinstance(value, (list, tuple)):
+ refs = [self._getrefnum(o) for o in value]
+ s = len(refs)
+ self._write_size(0xA0, s)
+ self._fp.write(struct.pack('>' + self._ref_format * s, *refs))
+
+ elif isinstance(value, dict):
+ keyRefs, valRefs = [], []
+
+ if self._sort_keys:
+ rootItems = sorted(value.items())
+ else:
+ rootItems = value.items()
+
+ for k, v in rootItems:
+ if not isinstance(k, str):
+ if self._skipkeys:
+ continue
+ raise TypeError("keys must be strings")
+ keyRefs.append(self._getrefnum(k))
+ valRefs.append(self._getrefnum(v))
+
+ s = len(keyRefs)
+ self._write_size(0xD0, s)
+ self._fp.write(struct.pack('>' + self._ref_format * s, *keyRefs))
+ self._fp.write(struct.pack('>' + self._ref_format * s, *valRefs))
+
+ else:
+ raise InvalidFileException()
+
+
+def _is_fmt_binary(header):
+ return header[:8] == b'bplist00'
+
+
+#
+# Generic bits
+#
+
+_FORMATS={
+ FMT_XML: dict(
+ detect=_is_fmt_xml,
+ parser=_PlistParser,
+ writer=_PlistWriter,
+ ),
+ FMT_BINARY: dict(
+ detect=_is_fmt_binary,
+ parser=_BinaryPlistParser,
+ writer=_BinaryPlistWriter,
+ )
+}
+
+
+def load(fp, *, fmt=None, use_builtin_types=True, dict_type=dict):
+ """Read a .plist file. 'fp' should be (readable) file object.
+ Return the unpacked root object (which usually is a dictionary).
+ """
+ if fmt is None:
+ header = fp.read(32)
+ fp.seek(0)
+ for info in _FORMATS.values():
+ if info['detect'](header):
+ p = info['parser'](
+ use_builtin_types=use_builtin_types,
+ dict_type=dict_type,
+ )
+ break
+
+ else:
+ raise InvalidFileException()
+
+ else:
+ p = _FORMATS[fmt]['parser'](use_builtin_types=use_builtin_types)
+
+ return p.parse(fp)
+
+
+def loads(value, *, fmt=None, use_builtin_types=True, dict_type=dict):
+ """Read a .plist file from a bytes object.
+ Return the unpacked root object (which usually is a dictionary).
+ """
+ fp = BytesIO(value)
+ return load(
+ fp, fmt=fmt, use_builtin_types=use_builtin_types, dict_type=dict_type)
+
+
+def dump(value, fp, *, fmt=FMT_XML, sort_keys=True, skipkeys=False):
+ """Write 'value' to a .plist file. 'fp' should be a (writable)
+ file object.
+ """
+ if fmt not in _FORMATS:
+ raise ValueError("Unsupported format: %r"%(fmt,))
+
+ writer = _FORMATS[fmt]["writer"](fp, sort_keys=sort_keys, skipkeys=skipkeys)
+ writer.write(value)
+
+
+def dumps(value, *, fmt=FMT_XML, skipkeys=False, sort_keys=True):
+ """Return a bytes object with the contents for a .plist file.
+ """
+ fp = BytesIO()
+ dump(value, fp, fmt=fmt, skipkeys=skipkeys, sort_keys=sort_keys)
+ return fp.getvalue()