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