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.
diff --git a/Lib/plistlib.py b/Lib/plistlib.py
index 2b0b634..bdb8ab3 100644
--- a/Lib/plistlib.py
+++ b/Lib/plistlib.py
@@ -4,25 +4,20 @@
 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 @@
             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
+PlistFormat = enum.Enum('PlistFormat', 'FMT_XML FMT_BINARY', module=__name__)
+globals().update(PlistFormat.__members__)
 
 
-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")
-
-    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 @@
     # 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 @@
             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
 
-    """This class has been deprecated. Use readPlist() and writePlist()
+    else:
+        yield pathOrFile
+
+
+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 @@
     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 @@
     def __repr__(self):
         return "%s(%s)" % (self.__class__.__name__, repr(self.data))
 
-class PlistParser:
+#
+#
+# End of deprecated functionality
+#
+#
 
-    def __init__(self):
+
+#
+# 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)
+
+# 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 @@
                                  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 @@
     # 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()