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