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