codekingpro/portable-devtools
114k
1r"""plistlib.py -- a tool to generate and parse MacOSX .plist files.2 3The property list (.plist) file format is a simple XML pickle supporting4basic 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' is9a (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. It13returns 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, or20datetime.datetime objects.21 22Generate Plist example:23 24 import datetime as dt25 import plistlib26 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 = dt.datetime.now()41 )42 print(plistlib.dumps(pl).decode())43 44Parse Plist example:45 46 import plistlib47 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 binascii62import codecs63import datetime64import enum65from io import BytesIO66import itertools67import os68import re69import struct70from xml.parsers.expat import ParserCreate71 72 73PlistFormat = enum.Enum('PlistFormat', 'FMT_XML FMT_BINARY', module=__name__)74globals().update(PlistFormat.__members__)75 76# Data larger than this will be read in chunks, to prevent extreme77# overallocation.78_MIN_READ_BUF_SIZE = 1 << 2079 80class UID:81 def __init__(self, data):82 if not isinstance(data, int):83 raise TypeError("data must be an int")84 if data >= 1 << 64:85 raise ValueError("UIDs cannot be >= 2**64")86 if data < 0:87 raise ValueError("UIDs must be positive")88 self.data = data89 90 def __index__(self):91 return self.data92 93 def __repr__(self):94 return "%s(%s)" % (self.__class__.__name__, repr(self.data))95 96 def __reduce__(self):97 return self.__class__, (self.data,)98 99 def __eq__(self, other):100 if not isinstance(other, UID):101 return NotImplemented102 return self.data == other.data103 104 def __hash__(self):105 return hash(self.data)106 107#108# XML support109#110 111 112# XML 'header'113PLISTHEADER = b"""\114<?xml version="1.0" encoding="UTF-8"?>115<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">116"""117 118 119# Regex to find any control chars, except for \t \n and \r120_controlCharPat = re.compile(121 r"[\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f"122 r"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f]")123 124def _encode_base64(s, maxlinelength=76):125 # copied from base64.encodebytes(), with added maxlinelength argument126 maxbinsize = (maxlinelength//4)*3127 pieces = []128 for i in range(0, len(s), maxbinsize):129 chunk = s[i : i + maxbinsize]130 pieces.append(binascii.b2a_base64(chunk))131 return b''.join(pieces)132 133def _decode_base64(s):134 if isinstance(s, str):135 return binascii.a2b_base64(s.encode("utf-8"))136 137 else:138 return binascii.a2b_base64(s)139 140# Contents should conform to a subset of ISO 8601141# (in particular, YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z'. Smaller units142# may be omitted with # a loss of precision)143_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)144 145 146def _date_from_string(s, aware_datetime):147 order = ('year', 'month', 'day', 'hour', 'minute', 'second')148 gd = _dateParser.match(s).groupdict()149 lst = []150 for key in order:151 val = gd[key]152 if val is None:153 break154 lst.append(int(val))155 if aware_datetime:156 return datetime.datetime(*lst, tzinfo=datetime.UTC)157 return datetime.datetime(*lst)158 159 160def _date_to_string(d, aware_datetime):161 if aware_datetime:162 d = d.astimezone(datetime.UTC)163 return '%04d-%02d-%02dT%02d:%02d:%02dZ' % (164 d.year, d.month, d.day,165 d.hour, d.minute, d.second166 )167 168def _escape(text):169 m = _controlCharPat.search(text)170 if m is not None:171 raise ValueError("strings can't contain control characters; "172 "use bytes instead")173 text = text.replace("\r\n", "\n") # convert DOS line endings174 text = text.replace("\r", "\n") # convert Mac line endings175 text = text.replace("&", "&") # escape '&'176 text = text.replace("<", "<") # escape '<'177 text = text.replace(">", ">") # escape '>'178 return text179 180class _PlistParser:181 def __init__(self, dict_type, aware_datetime=False):182 self.stack = []183 self.current_key = None184 self.root = None185 self._dict_type = dict_type186 self._aware_datetime = aware_datetime187 188 def parse(self, fileobj):189 self.parser = ParserCreate()190 self.parser.StartElementHandler = self.handle_begin_element191 self.parser.EndElementHandler = self.handle_end_element192 self.parser.CharacterDataHandler = self.handle_data193 self.parser.EntityDeclHandler = self.handle_entity_decl194 self.parser.ParseFile(fileobj)195 return self.root196 197 def handle_entity_decl(self, entity_name, is_parameter_entity, value, base, system_id, public_id, notation_name):198 # Reject plist files with entity declarations to avoid XML vulnerabilities in expat.199 # Regular plist files don't contain those declarations, and Apple's plutil tool does not200 # accept them either.201 raise InvalidFileException("XML entity declarations are not supported in plist files")202 203 def handle_begin_element(self, element, attrs):204 self.data = []205 handler = getattr(self, "begin_" + element, None)206 if handler is not None:207 handler(attrs)208 209 def handle_end_element(self, element):210 handler = getattr(self, "end_" + element, None)211 if handler is not None:212 handler()213 214 def handle_data(self, data):215 self.data.append(data)216 217 def add_object(self, value):218 if self.current_key is not None:219 if not isinstance(self.stack[-1], dict):220 raise ValueError("unexpected element at line %d" %221 self.parser.CurrentLineNumber)222 self.stack[-1][self.current_key] = value223 self.current_key = None224 elif not self.stack:225 # this is the root object226 self.root = value227 else:228 if not isinstance(self.stack[-1], list):229 raise ValueError("unexpected element at line %d" %230 self.parser.CurrentLineNumber)231 self.stack[-1].append(value)232 233 def get_data(self):234 data = ''.join(self.data)235 self.data = []236 return data237 238 # element handlers239 240 def begin_dict(self, attrs):241 d = self._dict_type()242 self.add_object(d)243 self.stack.append(d)244 245 def end_dict(self):246 if self.current_key:247 raise ValueError("missing value for key '%s' at line %d" %248 (self.current_key,self.parser.CurrentLineNumber))249 self.stack.pop()250 251 def end_key(self):252 if self.current_key or not isinstance(self.stack[-1], dict):253 raise ValueError("unexpected key at line %d" %254 self.parser.CurrentLineNumber)255 self.current_key = self.get_data()256 257 def begin_array(self, attrs):258 a = []259 self.add_object(a)260 self.stack.append(a)261 262 def end_array(self):263 self.stack.pop()264 265 def end_true(self):266 self.add_object(True)267 268 def end_false(self):269 self.add_object(False)270 271 def end_integer(self):272 raw = self.get_data()273 if raw.startswith('0x') or raw.startswith('0X'):274 self.add_object(int(raw, 16))275 else:276 self.add_object(int(raw))277 278 def end_real(self):279 self.add_object(float(self.get_data()))280 281 def end_string(self):282 self.add_object(self.get_data())283 284 def end_data(self):285 self.add_object(_decode_base64(self.get_data()))286 287 def end_date(self):288 self.add_object(_date_from_string(self.get_data(),289 aware_datetime=self._aware_datetime))290 291 292class _DumbXMLWriter:293 def __init__(self, file, indent_level=0, indent="\t"):294 self.file = file295 self.stack = []296 self._indent_level = indent_level297 self.indent = indent298 299 def begin_element(self, element):300 self.stack.append(element)301 self.writeln("<%s>" % element)302 self._indent_level += 1303 304 def end_element(self, element):305 assert self._indent_level > 0306 assert self.stack.pop() == element307 self._indent_level -= 1308 self.writeln("</%s>" % element)309 310 def simple_element(self, element, value=None):311 if value is not None:312 value = _escape(value)313 self.writeln("<%s>%s</%s>" % (element, value, element))314 315 else:316 self.writeln("<%s/>" % element)317 318 def writeln(self, line):319 if line:320 # plist has fixed encoding of utf-8321 322 # XXX: is this test needed?323 if isinstance(line, str):324 line = line.encode('utf-8')325 self.file.write(self._indent_level * self.indent)326 self.file.write(line)327 self.file.write(b'\n')328 329 330class _PlistWriter(_DumbXMLWriter):331 def __init__(332 self, file, indent_level=0, indent=b"\t", writeHeader=1,333 sort_keys=True, skipkeys=False, aware_datetime=False):334 335 if writeHeader:336 file.write(PLISTHEADER)337 _DumbXMLWriter.__init__(self, file, indent_level, indent)338 self._sort_keys = sort_keys339 self._skipkeys = skipkeys340 self._aware_datetime = aware_datetime341 342 def write(self, value):343 self.writeln("<plist version=\"1.0\">")344 self.write_value(value)345 self.writeln("</plist>")346 347 def write_value(self, value):348 if isinstance(value, str):349 self.simple_element("string", value)350 351 elif value is True:352 self.simple_element("true")353 354 elif value is False:355 self.simple_element("false")356 357 elif isinstance(value, int):358 if -1 << 63 <= value < 1 << 64:359 self.simple_element("integer", "%d" % value)360 else:361 raise OverflowError(value)362 363 elif isinstance(value, float):364 self.simple_element("real", repr(value))365 366 elif isinstance(value, dict):367 self.write_dict(value)368 369 elif isinstance(value, (bytes, bytearray)):370 self.write_bytes(value)371 372 elif isinstance(value, datetime.datetime):373 self.simple_element("date",374 _date_to_string(value, self._aware_datetime))375 376 elif isinstance(value, (tuple, list)):377 self.write_array(value)378 379 else:380 raise TypeError("unsupported type: %s" % type(value))381 382 def write_bytes(self, data):383 self.begin_element("data")384 self._indent_level -= 1385 maxlinelength = max(386 16,387 76 - len((self.indent * self._indent_level).expandtabs()))388 389 for line in _encode_base64(data, maxlinelength).split(b"\n"):390 if line:391 self.writeln(line)392 self._indent_level += 1393 self.end_element("data")394 395 def write_dict(self, d):396 if d:397 self.begin_element("dict")398 if self._sort_keys:399 items = sorted(d.items())400 else:401 items = d.items()402 403 for key, value in items:404 if not isinstance(key, str):405 if self._skipkeys:406 continue407 raise TypeError("keys must be strings")408 self.simple_element("key", key)409 self.write_value(value)410 self.end_element("dict")411 412 else:413 self.simple_element("dict")414 415 def write_array(self, array):416 if array:417 self.begin_element("array")418 for value in array:419 self.write_value(value)420 self.end_element("array")421 422 else:423 self.simple_element("array")424 425 426def _is_fmt_xml(header):427 prefixes = (b'<?xml', b'<plist')428 429 for pfx in prefixes:430 if header.startswith(pfx):431 return True432 433 # Also check for alternative XML encodings, this is slightly434 # overkill because the Apple tools (and plistlib) will not435 # generate files with these encodings.436 for bom, encoding in (437 (codecs.BOM_UTF8, "utf-8"),438 (codecs.BOM_UTF16_BE, "utf-16-be"),439 (codecs.BOM_UTF16_LE, "utf-16-le"),440 # expat does not support utf-32441 #(codecs.BOM_UTF32_BE, "utf-32-be"),442 #(codecs.BOM_UTF32_LE, "utf-32-le"),443 ):444 if not header.startswith(bom):445 continue446 447 for start in prefixes:448 prefix = bom + start.decode('ascii').encode(encoding)449 if header[:len(prefix)] == prefix:450 return True451 452 return False453 454#455# Binary Plist456#457 458 459class InvalidFileException (ValueError):460 def __init__(self, message="Invalid file"):461 ValueError.__init__(self, message)462 463_BINARY_FORMAT = {1: 'B', 2: 'H', 4: 'L', 8: 'Q'}464 465_undefined = object()466 467class _BinaryPlistParser:468 """469 Read or write a binary plist file, following the description of the binary470 format. Raise InvalidFileException in case of error, otherwise return the471 root object.472 473 see also: http://opensource.apple.com/source/CF/CF-744.18/CFBinaryPList.c474 """475 def __init__(self, dict_type, aware_datetime=False):476 self._dict_type = dict_type477 self._aware_datime = aware_datetime478 479 def parse(self, fp):480 try:481 # The basic file format:482 # HEADER483 # object...484 # refid->offset...485 # TRAILER486 self._fp = fp487 self._fp.seek(-32, os.SEEK_END)488 trailer = self._fp.read(32)489 if len(trailer) != 32:490 raise InvalidFileException()491 (492 offset_size, self._ref_size, num_objects, top_object,493 offset_table_offset494 ) = struct.unpack('>6xBBQQQ', trailer)495 self._fp.seek(offset_table_offset)496 self._object_offsets = self._read_ints(num_objects, offset_size)497 self._objects = [_undefined] * num_objects498 return self._read_object(top_object)499 500 except (OSError, IndexError, struct.error, OverflowError,501 ValueError):502 raise InvalidFileException()503 504 def _get_size(self, tokenL):505 """ return the size of the next object."""506 if tokenL == 0xF:507 m = self._fp.read(1)[0] & 0x3508 s = 1 << m509 f = '>' + _BINARY_FORMAT[s]510 return struct.unpack(f, self._fp.read(s))[0]511 512 return tokenL513 514 def _read(self, size):515 cursize = min(size, _MIN_READ_BUF_SIZE)516 data = self._fp.read(cursize)517 while True:518 if len(data) != cursize:519 raise InvalidFileException520 if cursize == size:521 return data522 delta = min(cursize, size - cursize)523 data += self._fp.read(delta)524 cursize += delta525 526 def _read_ints(self, n, size):527 data = self._read(size * n)528 if size in _BINARY_FORMAT:529 return struct.unpack(f'>{n}{_BINARY_FORMAT[size]}', data)530 else:531 if not size:532 raise InvalidFileException()533 return tuple(int.from_bytes(data[i: i + size], 'big')534 for i in range(0, size * n, size))535 536 def _read_refs(self, n):537 return self._read_ints(n, self._ref_size)538 539 def _read_object(self, ref):540 """541 read the object by reference.542 543 May recursively read sub-objects (content of an array/dict/set)544 """545 result = self._objects[ref]546 if result is not _undefined:547 return result548 549 offset = self._object_offsets[ref]550 self._fp.seek(offset)551 token = self._fp.read(1)[0]552 tokenH, tokenL = token & 0xF0, token & 0x0F553 554 if token == 0x00:555 result = None556 557 elif token == 0x08:558 result = False559 560 elif token == 0x09:561 result = True562 563 # The referenced source code also mentions URL (0x0c, 0x0d) and564 # UUID (0x0e), but neither can be generated using the Cocoa libraries.565 566 elif token == 0x0f:567 result = b''568 569 elif tokenH == 0x10: # int570 result = int.from_bytes(self._fp.read(1 << tokenL),571 'big', signed=tokenL >= 3)572 573 elif token == 0x22: # real574 result = struct.unpack('>f', self._fp.read(4))[0]575 576 elif token == 0x23: # real577 result = struct.unpack('>d', self._fp.read(8))[0]578 579 elif token == 0x33: # date580 f = struct.unpack('>d', self._fp.read(8))[0]581 # timestamp 0 of binary plists corresponds to 1/1/2001582 # (year of Mac OS X 10.0), instead of 1/1/1970.583 if self._aware_datime:584 epoch = datetime.datetime(2001, 1, 1, tzinfo=datetime.UTC)585 else:586 epoch = datetime.datetime(2001, 1, 1)587 result = epoch + datetime.timedelta(seconds=f)588 589 elif tokenH == 0x40: # data590 s = self._get_size(tokenL)591 result = self._read(s)592 593 elif tokenH == 0x50: # ascii string594 s = self._get_size(tokenL)595 data = self._read(s)596 result = data.decode('ascii')597 598 elif tokenH == 0x60: # unicode string599 s = self._get_size(tokenL) * 2600 data = self._read(s)601 result = data.decode('utf-16be')602 603 elif tokenH == 0x80: # UID604 # used by Key-Archiver plist files605 result = UID(int.from_bytes(self._fp.read(1 + tokenL), 'big'))606 607 elif tokenH == 0xA0: # array608 s = self._get_size(tokenL)609 obj_refs = self._read_refs(s)610 result = []611 self._objects[ref] = result612 for x in obj_refs:613 result.append(self._read_object(x))614 615 # tokenH == 0xB0 is documented as 'ordset', but is not actually616 # implemented in the Apple reference code.617 618 # tokenH == 0xC0 is documented as 'set', but sets cannot be used in619 # plists.620 621 elif tokenH == 0xD0: # dict622 s = self._get_size(tokenL)623 key_refs = self._read_refs(s)624 obj_refs = self._read_refs(s)625 result = self._dict_type()626 self._objects[ref] = result627 try:628 for k, o in zip(key_refs, obj_refs):629 result[self._read_object(k)] = self._read_object(o)630 except TypeError:631 raise InvalidFileException()632 else:633 raise InvalidFileException()634 635 self._objects[ref] = result636 return result637 638def _count_to_size(count):639 if count < 1 << 8:640 return 1641 642 elif count < 1 << 16:643 return 2644 645 elif count < 1 << 32:646 return 4647 648 else:649 return 8650 651_scalars = (str, int, float, datetime.datetime, bytes)652 653class _BinaryPlistWriter (object):654 def __init__(self, fp, sort_keys, skipkeys, aware_datetime=False):655 self._fp = fp656 self._sort_keys = sort_keys657 self._skipkeys = skipkeys658 self._aware_datetime = aware_datetime659 660 def write(self, value):661 662 # Flattened object list:663 self._objlist = []664 665 # Mappings from object->objectid666 # First dict has (type(object), object) as the key,667 # second dict is used when object is not hashable and668 # has id(object) as the key.669 self._objtable = {}670 self._objidtable = {}671 672 # Create list of all objects in the plist673 self._flatten(value)674 675 # Size of object references in serialized containers676 # depends on the number of objects in the plist.677 num_objects = len(self._objlist)678 self._object_offsets = [0]*num_objects679 self._ref_size = _count_to_size(num_objects)680 681 self._ref_format = _BINARY_FORMAT[self._ref_size]682 683 # Write file header684 self._fp.write(b'bplist00')685 686 # Write object list687 for obj in self._objlist:688 self._write_object(obj)689 690 # Write refnum->object offset table691 top_object = self._getrefnum(value)692 offset_table_offset = self._fp.tell()693 offset_size = _count_to_size(offset_table_offset)694 offset_format = '>' + _BINARY_FORMAT[offset_size] * num_objects695 self._fp.write(struct.pack(offset_format, *self._object_offsets))696 697 # Write trailer698 sort_version = 0699 trailer = (700 sort_version, offset_size, self._ref_size, num_objects,701 top_object, offset_table_offset702 )703 self._fp.write(struct.pack('>5xBBBQQQ', *trailer))704 705 def _flatten(self, value):706 # First check if the object is in the object table, not used for707 # containers to ensure that two subcontainers with the same contents708 # will be serialized as distinct values.709 if isinstance(value, _scalars):710 if (type(value), value) in self._objtable:711 return712 713 elif id(value) in self._objidtable:714 return715 716 # Add to objectreference map717 refnum = len(self._objlist)718 self._objlist.append(value)719 if isinstance(value, _scalars):720 self._objtable[(type(value), value)] = refnum721 else:722 self._objidtable[id(value)] = refnum723 724 # And finally recurse into containers725 if isinstance(value, dict):726 keys = []727 values = []728 items = value.items()729 if self._sort_keys:730 items = sorted(items)731 732 for k, v in items:733 if not isinstance(k, str):734 if self._skipkeys:735 continue736 raise TypeError("keys must be strings")737 keys.append(k)738 values.append(v)739 740 for o in itertools.chain(keys, values):741 self._flatten(o)742 743 elif isinstance(value, (list, tuple)):744 for o in value:745 self._flatten(o)746 747 def _getrefnum(self, value):748 if isinstance(value, _scalars):749 return self._objtable[(type(value), value)]750 else:751 return self._objidtable[id(value)]752 753 def _write_size(self, token, size):754 if size < 15:755 self._fp.write(struct.pack('>B', token | size))756 757 elif size < 1 << 8:758 self._fp.write(struct.pack('>BBB', token | 0xF, 0x10, size))759 760 elif size < 1 << 16:761 self._fp.write(struct.pack('>BBH', token | 0xF, 0x11, size))762 763 elif size < 1 << 32:764 self._fp.write(struct.pack('>BBL', token | 0xF, 0x12, size))765 766 else:767 self._fp.write(struct.pack('>BBQ', token | 0xF, 0x13, size))768 769 def _write_object(self, value):770 ref = self._getrefnum(value)771 self._object_offsets[ref] = self._fp.tell()772 if value is None:773 self._fp.write(b'\x00')774 775 elif value is False:776 self._fp.write(b'\x08')777 778 elif value is True:779 self._fp.write(b'\x09')780 781 elif isinstance(value, int):782 if value < 0:783 try:784 self._fp.write(struct.pack('>Bq', 0x13, value))785 except struct.error:786 raise OverflowError(value) from None787 elif value < 1 << 8:788 self._fp.write(struct.pack('>BB', 0x10, value))789 elif value < 1 << 16:790 self._fp.write(struct.pack('>BH', 0x11, value))791 elif value < 1 << 32:792 self._fp.write(struct.pack('>BL', 0x12, value))793 elif value < 1 << 63:794 self._fp.write(struct.pack('>BQ', 0x13, value))795 elif value < 1 << 64:796 self._fp.write(b'\x14' + value.to_bytes(16, 'big', signed=True))797 else:798 raise OverflowError(value)799 800 elif isinstance(value, float):801 self._fp.write(struct.pack('>Bd', 0x23, value))802 803 elif isinstance(value, datetime.datetime):804 if self._aware_datetime:805 dt = value.astimezone(datetime.UTC)806 offset = dt - datetime.datetime(2001, 1, 1, tzinfo=datetime.UTC)807 f = offset.total_seconds()808 else:809 f = (value - datetime.datetime(2001, 1, 1)).total_seconds()810 self._fp.write(struct.pack('>Bd', 0x33, f))811 812 elif isinstance(value, (bytes, bytearray)):813 self._write_size(0x40, len(value))814 self._fp.write(value)815 816 elif isinstance(value, str):817 try:818 t = value.encode('ascii')819 self._write_size(0x50, len(value))820 except UnicodeEncodeError:821 t = value.encode('utf-16be')822 self._write_size(0x60, len(t) // 2)823 824 self._fp.write(t)825 826 elif isinstance(value, UID):827 if value.data < 0:828 raise ValueError("UIDs must be positive")829 elif value.data < 1 << 8:830 self._fp.write(struct.pack('>BB', 0x80, value))831 elif value.data < 1 << 16:832 self._fp.write(struct.pack('>BH', 0x81, value))833 elif value.data < 1 << 32:834 self._fp.write(struct.pack('>BL', 0x83, value))835 elif value.data < 1 << 64:836 self._fp.write(struct.pack('>BQ', 0x87, value))837 else:838 raise OverflowError(value)839 840 elif isinstance(value, (list, tuple)):841 refs = [self._getrefnum(o) for o in value]842 s = len(refs)843 self._write_size(0xA0, s)844 self._fp.write(struct.pack('>' + self._ref_format * s, *refs))845 846 elif isinstance(value, dict):847 keyRefs, valRefs = [], []848 849 if self._sort_keys:850 rootItems = sorted(value.items())851 else:852 rootItems = value.items()853 854 for k, v in rootItems:855 if not isinstance(k, str):856 if self._skipkeys:857 continue858 raise TypeError("keys must be strings")859 keyRefs.append(self._getrefnum(k))860 valRefs.append(self._getrefnum(v))861 862 s = len(keyRefs)863 self._write_size(0xD0, s)864 self._fp.write(struct.pack('>' + self._ref_format * s, *keyRefs))865 self._fp.write(struct.pack('>' + self._ref_format * s, *valRefs))866 867 else:868 raise TypeError(value)869 870 871def _is_fmt_binary(header):872 return header[:8] == b'bplist00'873 874 875#876# Generic bits877#878 879_FORMATS={880 FMT_XML: dict(881 detect=_is_fmt_xml,882 parser=_PlistParser,883 writer=_PlistWriter,884 ),885 FMT_BINARY: dict(886 detect=_is_fmt_binary,887 parser=_BinaryPlistParser,888 writer=_BinaryPlistWriter,889 )890}891 892 893def load(fp, *, fmt=None, dict_type=dict, aware_datetime=False):894 """Read a .plist file. 'fp' should be a readable and binary file object.895 Return the unpacked root object (which usually is a dictionary).896 """897 if fmt is None:898 header = fp.read(32)899 fp.seek(0)900 for info in _FORMATS.values():901 if info['detect'](header):902 P = info['parser']903 break904 905 else:906 raise InvalidFileException()907 908 else:909 P = _FORMATS[fmt]['parser']910 911 p = P(dict_type=dict_type, aware_datetime=aware_datetime)912 return p.parse(fp)913 914 915def loads(value, *, fmt=None, dict_type=dict, aware_datetime=False):916 """Read a .plist file from a bytes object.917 Return the unpacked root object (which usually is a dictionary).918 """919 if isinstance(value, str):920 if fmt == FMT_BINARY:921 raise TypeError("value must be bytes-like object when fmt is "922 "FMT_BINARY")923 value = value.encode()924 fp = BytesIO(value)925 return load(fp, fmt=fmt, dict_type=dict_type, aware_datetime=aware_datetime)926 927 928def dump(value, fp, *, fmt=FMT_XML, sort_keys=True, skipkeys=False,929 aware_datetime=False):930 """Write 'value' to a .plist file. 'fp' should be a writable,931 binary file object.932 """933 if fmt not in _FORMATS:934 raise ValueError("Unsupported format: %r"%(fmt,))935 936 writer = _FORMATS[fmt]["writer"](fp, sort_keys=sort_keys, skipkeys=skipkeys,937 aware_datetime=aware_datetime)938 writer.write(value)939 940 941def dumps(value, *, fmt=FMT_XML, skipkeys=False, sort_keys=True,942 aware_datetime=False):943 """Return a bytes object with the contents for a .plist file.944 """945 fp = BytesIO()946 dump(value, fp, fmt=fmt, skipkeys=skipkeys, sort_keys=sort_keys,947 aware_datetime=aware_datetime)948 return fp.getvalue()949 