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