• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1r"""plistlib.py -- a tool to generate and parse MacOSX .plist files.
2
3The property list (.plist) file format is a simple XML pickle supporting
4basic object types, like dictionaries, lists, numbers and strings.
5Usually the top level object is a dictionary.
6
7To write out a plist file, use the dump(value, file)
8function. 'value' is the top level object, 'file' is
9a (writable) file object.
10
11To parse a plist from a file, use the load(file) function,
12with a (readable) file object as the only argument. It
13returns the top level object (again, usually a dictionary).
14
15To work with plist data in bytes objects, you can use loads()
16and dumps().
17
18Values can be strings, integers, floats, booleans, tuples, lists,
19dictionaries (but only with string keys), Data, bytes, bytearray, or
20datetime.datetime objects.
21
22Generate Plist example:
23
24    pl = dict(
25        aString = "Doodah",
26        aList = ["A", "B", 12, 32.1, [1, 2, 3]],
27        aFloat = 0.1,
28        anInt = 728,
29        aDict = dict(
30            anotherString = "<hello & hi there!>",
31            aUnicodeValue = "M\xe4ssig, Ma\xdf",
32            aTrueValue = True,
33            aFalseValue = False,
34        ),
35        someData = b"<binary gunk>",
36        someMoreData = b"<lots of binary gunk>" * 10,
37        aDate = datetime.datetime.fromtimestamp(time.mktime(time.gmtime())),
38    )
39    with open(fileName, 'wb') as fp:
40        dump(pl, fp)
41
42Parse Plist example:
43
44    with open(fileName, 'rb') as fp:
45        pl = load(fp)
46    print(pl["aKey"])
47"""
48__all__ = [
49    "readPlist", "writePlist", "readPlistFromBytes", "writePlistToBytes",
50    "Data", "InvalidFileException", "FMT_XML", "FMT_BINARY",
51    "load", "dump", "loads", "dumps"
52]
53
54import binascii
55import codecs
56import contextlib
57import datetime
58import enum
59from io import BytesIO
60import itertools
61import os
62import re
63import struct
64from warnings import warn
65from xml.parsers.expat import ParserCreate
66
67
68PlistFormat = enum.Enum('PlistFormat', 'FMT_XML FMT_BINARY', module=__name__)
69globals().update(PlistFormat.__members__)
70
71
72#
73#
74# Deprecated functionality
75#
76#
77
78
79@contextlib.contextmanager
80def _maybe_open(pathOrFile, mode):
81    if isinstance(pathOrFile, str):
82        with open(pathOrFile, mode) as fp:
83            yield fp
84
85    else:
86        yield pathOrFile
87
88
89def readPlist(pathOrFile):
90    """
91    Read a .plist from a path or file. pathOrFile should either
92    be a file name, or a readable binary file object.
93
94    This function is deprecated, use load instead.
95    """
96    warn("The readPlist function is deprecated, use load() instead",
97        DeprecationWarning, 2)
98
99    with _maybe_open(pathOrFile, 'rb') as fp:
100        return load(fp, fmt=None, use_builtin_types=False)
101
102def writePlist(value, pathOrFile):
103    """
104    Write 'value' to a .plist file. 'pathOrFile' may either be a
105    file name or a (writable) file object.
106
107    This function is deprecated, use dump instead.
108    """
109    warn("The writePlist function is deprecated, use dump() instead",
110        DeprecationWarning, 2)
111    with _maybe_open(pathOrFile, 'wb') as fp:
112        dump(value, fp, fmt=FMT_XML, sort_keys=True, skipkeys=False)
113
114
115def readPlistFromBytes(data):
116    """
117    Read a plist data from a bytes object. Return the root object.
118
119    This function is deprecated, use loads instead.
120    """
121    warn("The readPlistFromBytes function is deprecated, use loads() instead",
122        DeprecationWarning, 2)
123    return load(BytesIO(data), fmt=None, use_builtin_types=False)
124
125
126def writePlistToBytes(value):
127    """
128    Return 'value' as a plist-formatted bytes object.
129
130    This function is deprecated, use dumps instead.
131    """
132    warn("The writePlistToBytes function is deprecated, use dumps() instead",
133        DeprecationWarning, 2)
134    f = BytesIO()
135    dump(value, f, fmt=FMT_XML, sort_keys=True, skipkeys=False)
136    return f.getvalue()
137
138
139class Data:
140    """
141    Wrapper for binary data.
142
143    This class is deprecated, use a bytes object instead.
144    """
145
146    def __init__(self, data):
147        if not isinstance(data, bytes):
148            raise TypeError("data must be as bytes")
149        self.data = data
150
151    @classmethod
152    def fromBase64(cls, data):
153        # base64.decodebytes just calls binascii.a2b_base64;
154        # it seems overkill to use both base64 and binascii.
155        return cls(_decode_base64(data))
156
157    def asBase64(self, maxlinelength=76):
158        return _encode_base64(self.data, maxlinelength)
159
160    def __eq__(self, other):
161        if isinstance(other, self.__class__):
162            return self.data == other.data
163        elif isinstance(other, bytes):
164            return self.data == other
165        else:
166            return NotImplemented
167
168    def __repr__(self):
169        return "%s(%s)" % (self.__class__.__name__, repr(self.data))
170
171#
172#
173# End of deprecated functionality
174#
175#
176
177
178#
179# XML support
180#
181
182
183# XML 'header'
184PLISTHEADER = b"""\
185<?xml version="1.0" encoding="UTF-8"?>
186<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
187"""
188
189
190# Regex to find any control chars, except for \t \n and \r
191_controlCharPat = re.compile(
192    r"[\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f"
193    r"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f]")
194
195def _encode_base64(s, maxlinelength=76):
196    # copied from base64.encodebytes(), with added maxlinelength argument
197    maxbinsize = (maxlinelength//4)*3
198    pieces = []
199    for i in range(0, len(s), maxbinsize):
200        chunk = s[i : i + maxbinsize]
201        pieces.append(binascii.b2a_base64(chunk))
202    return b''.join(pieces)
203
204def _decode_base64(s):
205    if isinstance(s, str):
206        return binascii.a2b_base64(s.encode("utf-8"))
207
208    else:
209        return binascii.a2b_base64(s)
210
211# Contents should conform to a subset of ISO 8601
212# (in particular, YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z'.  Smaller units
213# may be omitted with #  a loss of precision)
214_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)
215
216
217def _date_from_string(s):
218    order = ('year', 'month', 'day', 'hour', 'minute', 'second')
219    gd = _dateParser.match(s).groupdict()
220    lst = []
221    for key in order:
222        val = gd[key]
223        if val is None:
224            break
225        lst.append(int(val))
226    return datetime.datetime(*lst)
227
228
229def _date_to_string(d):
230    return '%04d-%02d-%02dT%02d:%02d:%02dZ' % (
231        d.year, d.month, d.day,
232        d.hour, d.minute, d.second
233    )
234
235def _escape(text):
236    m = _controlCharPat.search(text)
237    if m is not None:
238        raise ValueError("strings can't contains control characters; "
239                         "use bytes instead")
240    text = text.replace("\r\n", "\n")       # convert DOS line endings
241    text = text.replace("\r", "\n")         # convert Mac line endings
242    text = text.replace("&", "&amp;")       # escape '&'
243    text = text.replace("<", "&lt;")        # escape '<'
244    text = text.replace(">", "&gt;")        # escape '>'
245    return text
246
247class _PlistParser:
248    def __init__(self, use_builtin_types, dict_type):
249        self.stack = []
250        self.current_key = None
251        self.root = None
252        self._use_builtin_types = use_builtin_types
253        self._dict_type = dict_type
254
255    def parse(self, fileobj):
256        self.parser = ParserCreate()
257        self.parser.StartElementHandler = self.handle_begin_element
258        self.parser.EndElementHandler = self.handle_end_element
259        self.parser.CharacterDataHandler = self.handle_data
260        self.parser.ParseFile(fileobj)
261        return self.root
262
263    def handle_begin_element(self, element, attrs):
264        self.data = []
265        handler = getattr(self, "begin_" + element, None)
266        if handler is not None:
267            handler(attrs)
268
269    def handle_end_element(self, element):
270        handler = getattr(self, "end_" + element, None)
271        if handler is not None:
272            handler()
273
274    def handle_data(self, data):
275        self.data.append(data)
276
277    def add_object(self, value):
278        if self.current_key is not None:
279            if not isinstance(self.stack[-1], type({})):
280                raise ValueError("unexpected element at line %d" %
281                                 self.parser.CurrentLineNumber)
282            self.stack[-1][self.current_key] = value
283            self.current_key = None
284        elif not self.stack:
285            # this is the root object
286            self.root = value
287        else:
288            if not isinstance(self.stack[-1], type([])):
289                raise ValueError("unexpected element at line %d" %
290                                 self.parser.CurrentLineNumber)
291            self.stack[-1].append(value)
292
293    def get_data(self):
294        data = ''.join(self.data)
295        self.data = []
296        return data
297
298    # element handlers
299
300    def begin_dict(self, attrs):
301        d = self._dict_type()
302        self.add_object(d)
303        self.stack.append(d)
304
305    def end_dict(self):
306        if self.current_key:
307            raise ValueError("missing value for key '%s' at line %d" %
308                             (self.current_key,self.parser.CurrentLineNumber))
309        self.stack.pop()
310
311    def end_key(self):
312        if self.current_key or not isinstance(self.stack[-1], type({})):
313            raise ValueError("unexpected key at line %d" %
314                             self.parser.CurrentLineNumber)
315        self.current_key = self.get_data()
316
317    def begin_array(self, attrs):
318        a = []
319        self.add_object(a)
320        self.stack.append(a)
321
322    def end_array(self):
323        self.stack.pop()
324
325    def end_true(self):
326        self.add_object(True)
327
328    def end_false(self):
329        self.add_object(False)
330
331    def end_integer(self):
332        self.add_object(int(self.get_data()))
333
334    def end_real(self):
335        self.add_object(float(self.get_data()))
336
337    def end_string(self):
338        self.add_object(self.get_data())
339
340    def end_data(self):
341        if self._use_builtin_types:
342            self.add_object(_decode_base64(self.get_data()))
343
344        else:
345            self.add_object(Data.fromBase64(self.get_data()))
346
347    def end_date(self):
348        self.add_object(_date_from_string(self.get_data()))
349
350
351class _DumbXMLWriter:
352    def __init__(self, file, indent_level=0, indent="\t"):
353        self.file = file
354        self.stack = []
355        self._indent_level = indent_level
356        self.indent = indent
357
358    def begin_element(self, element):
359        self.stack.append(element)
360        self.writeln("<%s>" % element)
361        self._indent_level += 1
362
363    def end_element(self, element):
364        assert self._indent_level > 0
365        assert self.stack.pop() == element
366        self._indent_level -= 1
367        self.writeln("</%s>" % element)
368
369    def simple_element(self, element, value=None):
370        if value is not None:
371            value = _escape(value)
372            self.writeln("<%s>%s</%s>" % (element, value, element))
373
374        else:
375            self.writeln("<%s/>" % element)
376
377    def writeln(self, line):
378        if line:
379            # plist has fixed encoding of utf-8
380
381            # XXX: is this test needed?
382            if isinstance(line, str):
383                line = line.encode('utf-8')
384            self.file.write(self._indent_level * self.indent)
385            self.file.write(line)
386        self.file.write(b'\n')
387
388
389class _PlistWriter(_DumbXMLWriter):
390    def __init__(
391            self, file, indent_level=0, indent=b"\t", writeHeader=1,
392            sort_keys=True, skipkeys=False):
393
394        if writeHeader:
395            file.write(PLISTHEADER)
396        _DumbXMLWriter.__init__(self, file, indent_level, indent)
397        self._sort_keys = sort_keys
398        self._skipkeys = skipkeys
399
400    def write(self, value):
401        self.writeln("<plist version=\"1.0\">")
402        self.write_value(value)
403        self.writeln("</plist>")
404
405    def write_value(self, value):
406        if isinstance(value, str):
407            self.simple_element("string", value)
408
409        elif value is True:
410            self.simple_element("true")
411
412        elif value is False:
413            self.simple_element("false")
414
415        elif isinstance(value, int):
416            if -1 << 63 <= value < 1 << 64:
417                self.simple_element("integer", "%d" % value)
418            else:
419                raise OverflowError(value)
420
421        elif isinstance(value, float):
422            self.simple_element("real", repr(value))
423
424        elif isinstance(value, dict):
425            self.write_dict(value)
426
427        elif isinstance(value, Data):
428            self.write_data(value)
429
430        elif isinstance(value, (bytes, bytearray)):
431            self.write_bytes(value)
432
433        elif isinstance(value, datetime.datetime):
434            self.simple_element("date", _date_to_string(value))
435
436        elif isinstance(value, (tuple, list)):
437            self.write_array(value)
438
439        else:
440            raise TypeError("unsupported type: %s" % type(value))
441
442    def write_data(self, data):
443        self.write_bytes(data.data)
444
445    def write_bytes(self, data):
446        self.begin_element("data")
447        self._indent_level -= 1
448        maxlinelength = max(
449            16,
450            76 - len(self.indent.replace(b"\t", b" " * 8) * self._indent_level))
451
452        for line in _encode_base64(data, maxlinelength).split(b"\n"):
453            if line:
454                self.writeln(line)
455        self._indent_level += 1
456        self.end_element("data")
457
458    def write_dict(self, d):
459        if d:
460            self.begin_element("dict")
461            if self._sort_keys:
462                items = sorted(d.items())
463            else:
464                items = d.items()
465
466            for key, value in items:
467                if not isinstance(key, str):
468                    if self._skipkeys:
469                        continue
470                    raise TypeError("keys must be strings")
471                self.simple_element("key", key)
472                self.write_value(value)
473            self.end_element("dict")
474
475        else:
476            self.simple_element("dict")
477
478    def write_array(self, array):
479        if array:
480            self.begin_element("array")
481            for value in array:
482                self.write_value(value)
483            self.end_element("array")
484
485        else:
486            self.simple_element("array")
487
488
489def _is_fmt_xml(header):
490    prefixes = (b'<?xml', b'<plist')
491
492    for pfx in prefixes:
493        if header.startswith(pfx):
494            return True
495
496    # Also check for alternative XML encodings, this is slightly
497    # overkill because the Apple tools (and plistlib) will not
498    # generate files with these encodings.
499    for bom, encoding in (
500                (codecs.BOM_UTF8, "utf-8"),
501                (codecs.BOM_UTF16_BE, "utf-16-be"),
502                (codecs.BOM_UTF16_LE, "utf-16-le"),
503                # expat does not support utf-32
504                #(codecs.BOM_UTF32_BE, "utf-32-be"),
505                #(codecs.BOM_UTF32_LE, "utf-32-le"),
506            ):
507        if not header.startswith(bom):
508            continue
509
510        for start in prefixes:
511            prefix = bom + start.decode('ascii').encode(encoding)
512            if header[:len(prefix)] == prefix:
513                return True
514
515    return False
516
517#
518# Binary Plist
519#
520
521
522class InvalidFileException (ValueError):
523    def __init__(self, message="Invalid file"):
524        ValueError.__init__(self, message)
525
526_BINARY_FORMAT = {1: 'B', 2: 'H', 4: 'L', 8: 'Q'}
527
528_undefined = object()
529
530class _BinaryPlistParser:
531    """
532    Read or write a binary plist file, following the description of the binary
533    format.  Raise InvalidFileException in case of error, otherwise return the
534    root object.
535
536    see also: http://opensource.apple.com/source/CF/CF-744.18/CFBinaryPList.c
537    """
538    def __init__(self, use_builtin_types, dict_type):
539        self._use_builtin_types = use_builtin_types
540        self._dict_type = dict_type
541
542    def parse(self, fp):
543        try:
544            # The basic file format:
545            # HEADER
546            # object...
547            # refid->offset...
548            # TRAILER
549            self._fp = fp
550            self._fp.seek(-32, os.SEEK_END)
551            trailer = self._fp.read(32)
552            if len(trailer) != 32:
553                raise InvalidFileException()
554            (
555                offset_size, self._ref_size, num_objects, top_object,
556                offset_table_offset
557            ) = struct.unpack('>6xBBQQQ', trailer)
558            self._fp.seek(offset_table_offset)
559            self._object_offsets = self._read_ints(num_objects, offset_size)
560            self._objects = [_undefined] * num_objects
561            return self._read_object(top_object)
562
563        except (OSError, IndexError, struct.error, OverflowError,
564                UnicodeDecodeError):
565            raise InvalidFileException()
566
567    def _get_size(self, tokenL):
568        """ return the size of the next object."""
569        if tokenL == 0xF:
570            m = self._fp.read(1)[0] & 0x3
571            s = 1 << m
572            f = '>' + _BINARY_FORMAT[s]
573            return struct.unpack(f, self._fp.read(s))[0]
574
575        return tokenL
576
577    def _read_ints(self, n, size):
578        data = self._fp.read(size * n)
579        if size in _BINARY_FORMAT:
580            return struct.unpack('>' + _BINARY_FORMAT[size] * n, data)
581        else:
582            if not size or len(data) != size * n:
583                raise InvalidFileException()
584            return tuple(int.from_bytes(data[i: i + size], 'big')
585                         for i in range(0, size * n, size))
586
587    def _read_refs(self, n):
588        return self._read_ints(n, self._ref_size)
589
590    def _read_object(self, ref):
591        """
592        read the object by reference.
593
594        May recursively read sub-objects (content of an array/dict/set)
595        """
596        result = self._objects[ref]
597        if result is not _undefined:
598            return result
599
600        offset = self._object_offsets[ref]
601        self._fp.seek(offset)
602        token = self._fp.read(1)[0]
603        tokenH, tokenL = token & 0xF0, token & 0x0F
604
605        if token == 0x00:
606            result = None
607
608        elif token == 0x08:
609            result = False
610
611        elif token == 0x09:
612            result = True
613
614        # The referenced source code also mentions URL (0x0c, 0x0d) and
615        # UUID (0x0e), but neither can be generated using the Cocoa libraries.
616
617        elif token == 0x0f:
618            result = b''
619
620        elif tokenH == 0x10:  # int
621            result = int.from_bytes(self._fp.read(1 << tokenL),
622                                    'big', signed=tokenL >= 3)
623
624        elif token == 0x22: # real
625            result = struct.unpack('>f', self._fp.read(4))[0]
626
627        elif token == 0x23: # real
628            result = struct.unpack('>d', self._fp.read(8))[0]
629
630        elif token == 0x33:  # date
631            f = struct.unpack('>d', self._fp.read(8))[0]
632            # timestamp 0 of binary plists corresponds to 1/1/2001
633            # (year of Mac OS X 10.0), instead of 1/1/1970.
634            result = (datetime.datetime(2001, 1, 1) +
635                      datetime.timedelta(seconds=f))
636
637        elif tokenH == 0x40:  # data
638            s = self._get_size(tokenL)
639            if self._use_builtin_types:
640                result = self._fp.read(s)
641            else:
642                result = Data(self._fp.read(s))
643
644        elif tokenH == 0x50:  # ascii string
645            s = self._get_size(tokenL)
646            result =  self._fp.read(s).decode('ascii')
647            result = result
648
649        elif tokenH == 0x60:  # unicode string
650            s = self._get_size(tokenL)
651            result = self._fp.read(s * 2).decode('utf-16be')
652
653        # tokenH == 0x80 is documented as 'UID' and appears to be used for
654        # keyed-archiving, not in plists.
655
656        elif tokenH == 0xA0:  # array
657            s = self._get_size(tokenL)
658            obj_refs = self._read_refs(s)
659            result = []
660            self._objects[ref] = result
661            result.extend(self._read_object(x) for x in obj_refs)
662
663        # tokenH == 0xB0 is documented as 'ordset', but is not actually
664        # implemented in the Apple reference code.
665
666        # tokenH == 0xC0 is documented as 'set', but sets cannot be used in
667        # plists.
668
669        elif tokenH == 0xD0:  # dict
670            s = self._get_size(tokenL)
671            key_refs = self._read_refs(s)
672            obj_refs = self._read_refs(s)
673            result = self._dict_type()
674            self._objects[ref] = result
675            for k, o in zip(key_refs, obj_refs):
676                result[self._read_object(k)] = self._read_object(o)
677
678        else:
679            raise InvalidFileException()
680
681        self._objects[ref] = result
682        return result
683
684def _count_to_size(count):
685    if count < 1 << 8:
686        return 1
687
688    elif count < 1 << 16:
689        return 2
690
691    elif count << 1 << 32:
692        return 4
693
694    else:
695        return 8
696
697_scalars = (str, int, float, datetime.datetime, bytes)
698
699class _BinaryPlistWriter (object):
700    def __init__(self, fp, sort_keys, skipkeys):
701        self._fp = fp
702        self._sort_keys = sort_keys
703        self._skipkeys = skipkeys
704
705    def write(self, value):
706
707        # Flattened object list:
708        self._objlist = []
709
710        # Mappings from object->objectid
711        # First dict has (type(object), object) as the key,
712        # second dict is used when object is not hashable and
713        # has id(object) as the key.
714        self._objtable = {}
715        self._objidtable = {}
716
717        # Create list of all objects in the plist
718        self._flatten(value)
719
720        # Size of object references in serialized containers
721        # depends on the number of objects in the plist.
722        num_objects = len(self._objlist)
723        self._object_offsets = [0]*num_objects
724        self._ref_size = _count_to_size(num_objects)
725
726        self._ref_format = _BINARY_FORMAT[self._ref_size]
727
728        # Write file header
729        self._fp.write(b'bplist00')
730
731        # Write object list
732        for obj in self._objlist:
733            self._write_object(obj)
734
735        # Write refnum->object offset table
736        top_object = self._getrefnum(value)
737        offset_table_offset = self._fp.tell()
738        offset_size = _count_to_size(offset_table_offset)
739        offset_format = '>' + _BINARY_FORMAT[offset_size] * num_objects
740        self._fp.write(struct.pack(offset_format, *self._object_offsets))
741
742        # Write trailer
743        sort_version = 0
744        trailer = (
745            sort_version, offset_size, self._ref_size, num_objects,
746            top_object, offset_table_offset
747        )
748        self._fp.write(struct.pack('>5xBBBQQQ', *trailer))
749
750    def _flatten(self, value):
751        # First check if the object is in the object table, not used for
752        # containers to ensure that two subcontainers with the same contents
753        # will be serialized as distinct values.
754        if isinstance(value, _scalars):
755            if (type(value), value) in self._objtable:
756                return
757
758        elif isinstance(value, Data):
759            if (type(value.data), value.data) in self._objtable:
760                return
761
762        elif id(value) in self._objidtable:
763            return
764
765        # Add to objectreference map
766        refnum = len(self._objlist)
767        self._objlist.append(value)
768        if isinstance(value, _scalars):
769            self._objtable[(type(value), value)] = refnum
770        elif isinstance(value, Data):
771            self._objtable[(type(value.data), value.data)] = refnum
772        else:
773            self._objidtable[id(value)] = refnum
774
775        # And finally recurse into containers
776        if isinstance(value, dict):
777            keys = []
778            values = []
779            items = value.items()
780            if self._sort_keys:
781                items = sorted(items)
782
783            for k, v in items:
784                if not isinstance(k, str):
785                    if self._skipkeys:
786                        continue
787                    raise TypeError("keys must be strings")
788                keys.append(k)
789                values.append(v)
790
791            for o in itertools.chain(keys, values):
792                self._flatten(o)
793
794        elif isinstance(value, (list, tuple)):
795            for o in value:
796                self._flatten(o)
797
798    def _getrefnum(self, value):
799        if isinstance(value, _scalars):
800            return self._objtable[(type(value), value)]
801        elif isinstance(value, Data):
802            return self._objtable[(type(value.data), value.data)]
803        else:
804            return self._objidtable[id(value)]
805
806    def _write_size(self, token, size):
807        if size < 15:
808            self._fp.write(struct.pack('>B', token | size))
809
810        elif size < 1 << 8:
811            self._fp.write(struct.pack('>BBB', token | 0xF, 0x10, size))
812
813        elif size < 1 << 16:
814            self._fp.write(struct.pack('>BBH', token | 0xF, 0x11, size))
815
816        elif size < 1 << 32:
817            self._fp.write(struct.pack('>BBL', token | 0xF, 0x12, size))
818
819        else:
820            self._fp.write(struct.pack('>BBQ', token | 0xF, 0x13, size))
821
822    def _write_object(self, value):
823        ref = self._getrefnum(value)
824        self._object_offsets[ref] = self._fp.tell()
825        if value is None:
826            self._fp.write(b'\x00')
827
828        elif value is False:
829            self._fp.write(b'\x08')
830
831        elif value is True:
832            self._fp.write(b'\x09')
833
834        elif isinstance(value, int):
835            if value < 0:
836                try:
837                    self._fp.write(struct.pack('>Bq', 0x13, value))
838                except struct.error:
839                    raise OverflowError(value) from None
840            elif value < 1 << 8:
841                self._fp.write(struct.pack('>BB', 0x10, value))
842            elif value < 1 << 16:
843                self._fp.write(struct.pack('>BH', 0x11, value))
844            elif value < 1 << 32:
845                self._fp.write(struct.pack('>BL', 0x12, value))
846            elif value < 1 << 63:
847                self._fp.write(struct.pack('>BQ', 0x13, value))
848            elif value < 1 << 64:
849                self._fp.write(b'\x14' + value.to_bytes(16, 'big', signed=True))
850            else:
851                raise OverflowError(value)
852
853        elif isinstance(value, float):
854            self._fp.write(struct.pack('>Bd', 0x23, value))
855
856        elif isinstance(value, datetime.datetime):
857            f = (value - datetime.datetime(2001, 1, 1)).total_seconds()
858            self._fp.write(struct.pack('>Bd', 0x33, f))
859
860        elif isinstance(value, Data):
861            self._write_size(0x40, len(value.data))
862            self._fp.write(value.data)
863
864        elif isinstance(value, (bytes, bytearray)):
865            self._write_size(0x40, len(value))
866            self._fp.write(value)
867
868        elif isinstance(value, str):
869            try:
870                t = value.encode('ascii')
871                self._write_size(0x50, len(value))
872            except UnicodeEncodeError:
873                t = value.encode('utf-16be')
874                self._write_size(0x60, len(t) // 2)
875
876            self._fp.write(t)
877
878        elif isinstance(value, (list, tuple)):
879            refs = [self._getrefnum(o) for o in value]
880            s = len(refs)
881            self._write_size(0xA0, s)
882            self._fp.write(struct.pack('>' + self._ref_format * s, *refs))
883
884        elif isinstance(value, dict):
885            keyRefs, valRefs = [], []
886
887            if self._sort_keys:
888                rootItems = sorted(value.items())
889            else:
890                rootItems = value.items()
891
892            for k, v in rootItems:
893                if not isinstance(k, str):
894                    if self._skipkeys:
895                        continue
896                    raise TypeError("keys must be strings")
897                keyRefs.append(self._getrefnum(k))
898                valRefs.append(self._getrefnum(v))
899
900            s = len(keyRefs)
901            self._write_size(0xD0, s)
902            self._fp.write(struct.pack('>' + self._ref_format * s, *keyRefs))
903            self._fp.write(struct.pack('>' + self._ref_format * s, *valRefs))
904
905        else:
906            raise TypeError(value)
907
908
909def _is_fmt_binary(header):
910    return header[:8] == b'bplist00'
911
912
913#
914# Generic bits
915#
916
917_FORMATS={
918    FMT_XML: dict(
919        detect=_is_fmt_xml,
920        parser=_PlistParser,
921        writer=_PlistWriter,
922    ),
923    FMT_BINARY: dict(
924        detect=_is_fmt_binary,
925        parser=_BinaryPlistParser,
926        writer=_BinaryPlistWriter,
927    )
928}
929
930
931def load(fp, *, fmt=None, use_builtin_types=True, dict_type=dict):
932    """Read a .plist file. 'fp' should be (readable) file object.
933    Return the unpacked root object (which usually is a dictionary).
934    """
935    if fmt is None:
936        header = fp.read(32)
937        fp.seek(0)
938        for info in _FORMATS.values():
939            if info['detect'](header):
940                P = info['parser']
941                break
942
943        else:
944            raise InvalidFileException()
945
946    else:
947        P = _FORMATS[fmt]['parser']
948
949    p = P(use_builtin_types=use_builtin_types, dict_type=dict_type)
950    return p.parse(fp)
951
952
953def loads(value, *, fmt=None, use_builtin_types=True, dict_type=dict):
954    """Read a .plist file from a bytes object.
955    Return the unpacked root object (which usually is a dictionary).
956    """
957    fp = BytesIO(value)
958    return load(
959        fp, fmt=fmt, use_builtin_types=use_builtin_types, dict_type=dict_type)
960
961
962def dump(value, fp, *, fmt=FMT_XML, sort_keys=True, skipkeys=False):
963    """Write 'value' to a .plist file. 'fp' should be a (writable)
964    file object.
965    """
966    if fmt not in _FORMATS:
967        raise ValueError("Unsupported format: %r"%(fmt,))
968
969    writer = _FORMATS[fmt]["writer"](fp, sort_keys=sort_keys, skipkeys=skipkeys)
970    writer.write(value)
971
972
973def dumps(value, *, fmt=FMT_XML, skipkeys=False, sort_keys=True):
974    """Return a bytes object with the contents for a .plist file.
975    """
976    fp = BytesIO()
977    dump(value, fp, fmt=fmt, skipkeys=skipkeys, sort_keys=sort_keys)
978    return fp.getvalue()
979