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("&", "&") # escape '&' 169 text = text.replace("<", "<") # escape '<' 170 text = text.replace(">", ">") # 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