1 r"""plistlib.py -- a tool to generate and parse MacOSX .plist files.
2
3 The property list (.plist) file format is a simple XML pickle supporting
4 basic object types, like dictionaries, lists, numbers and strings.
5 Usually the top level object is a dictionary.
6
7 To write out a plist file, use the dump(value, file)
8 function. 'value' is the top level object, 'file' is
9 a (writable) file object.
10
11 To parse a plist from a file, use the load(file) function,
12 with a (readable) file object as the only argument. It
13 returns the top level object (again, usually a dictionary).
14
15 To work with plist data in bytes objects, you can use loads()
16 and dumps().
17
18 Values can be strings, integers, floats, booleans, tuples, lists,
19 dictionaries (but only with string keys), Data, bytes, bytearray, or
20 datetime.datetime objects.
21
22 Generate 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
44 Parse 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
61 import binascii
62 import codecs
63 import datetime
64 import enum
65 from io import BytesIO
66 import itertools
67 import os
68 import re
69 import struct
70 from xml.parsers.expat import ParserCreate
71
72
73 PlistFormat = enum.Enum('PlistFormat', 'FMT_XML FMT_BINARY', module=__name__)
74 globals().update(PlistFormat.__members__)
75
76
77 class ESC[4;38;5;81mUID:
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'
110 PLISTHEADER = 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
121 def _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
130 def _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
143 def _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
155 def _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
161 def _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("&", "&") # escape '&'
169 text = text.replace("<", "<") # escape '<'
170 text = text.replace(">", ">") # escape '>'
171 return text
172
173 class ESC[4;38;5;81m_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], dict):
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], list):
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], dict):
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
283 class ESC[4;38;5;81m_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
321 class ESC[4;38;5;81m_PlistWriter(ESC[4;38;5;149m_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
415 def _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
448 class ESC[4;38;5;81mInvalidFileException (ESC[4;38;5;149mValueError):
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
456 class ESC[4;38;5;81m_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
616 def _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
631 class ESC[4;38;5;81m_BinaryPlistWriter (ESC[4;38;5;149mobject):
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
843 def _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
865 def 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
887 def 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
895 def 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
906 def 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()