123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911 |
- r"""plistlib.py -- a tool to generate and parse MacOSX .plist files.
- The property list (.plist) file format is a simple XML pickle supporting
- basic object types, like dictionaries, lists, numbers and strings.
- Usually the top level object is a dictionary.
- To write out a plist file, use the dump(value, file)
- function. 'value' is the top level object, 'file' is
- a (writable) file object.
- To parse a plist from a file, use the load(file) function,
- with a (readable) file object as the only argument. It
- returns the top level object (again, usually a dictionary).
- To work with plist data in bytes objects, you can use loads()
- and dumps().
- Values can be strings, integers, floats, booleans, tuples, lists,
- dictionaries (but only with string keys), Data, bytes, bytearray, or
- datetime.datetime objects.
- Generate Plist example:
- import datetime
- import plistlib
- pl = dict(
- aString = "Doodah",
- aList = ["A", "B", 12, 32.1, [1, 2, 3]],
- aFloat = 0.1,
- anInt = 728,
- aDict = dict(
- anotherString = "<hello & hi there!>",
- aThirdString = "M\xe4ssig, Ma\xdf",
- aTrueValue = True,
- aFalseValue = False,
- ),
- someData = b"<binary gunk>",
- someMoreData = b"<lots of binary gunk>" * 10,
- aDate = datetime.datetime.now()
- )
- print(plistlib.dumps(pl).decode())
- Parse Plist example:
- import plistlib
- plist = b'''<plist version="1.0">
- <dict>
- <key>foo</key>
- <string>bar</string>
- </dict>
- </plist>'''
- pl = plistlib.loads(plist)
- print(pl["foo"])
- """
- __all__ = [
- "InvalidFileException", "FMT_XML", "FMT_BINARY", "load", "dump", "loads", "dumps", "UID"
- ]
- import binascii
- import codecs
- import datetime
- import enum
- from io import BytesIO
- import itertools
- import os
- import re
- import struct
- from xml.parsers.expat import ParserCreate
- PlistFormat = enum.Enum('PlistFormat', 'FMT_XML FMT_BINARY', module=__name__)
- globals().update(PlistFormat.__members__)
- class UID:
- def __init__(self, data):
- if not isinstance(data, int):
- raise TypeError("data must be an int")
- if data >= 1 << 64:
- raise ValueError("UIDs cannot be >= 2**64")
- if data < 0:
- raise ValueError("UIDs must be positive")
- self.data = data
- def __index__(self):
- return self.data
- def __repr__(self):
- return "%s(%s)" % (self.__class__.__name__, repr(self.data))
- def __reduce__(self):
- return self.__class__, (self.data,)
- def __eq__(self, other):
- if not isinstance(other, UID):
- return NotImplemented
- return self.data == other.data
- def __hash__(self):
- return hash(self.data)
- #
- # XML support
- #
- # XML 'header'
- PLISTHEADER = b"""\
- <?xml version="1.0" encoding="UTF-8"?>
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
- """
- # Regex to find any control chars, except for \t \n and \r
- _controlCharPat = re.compile(
- r"[\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f"
- r"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f]")
- def _encode_base64(s, maxlinelength=76):
- # copied from base64.encodebytes(), with added maxlinelength argument
- maxbinsize = (maxlinelength//4)*3
- pieces = []
- for i in range(0, len(s), maxbinsize):
- chunk = s[i : i + maxbinsize]
- pieces.append(binascii.b2a_base64(chunk))
- return b''.join(pieces)
- def _decode_base64(s):
- if isinstance(s, str):
- return binascii.a2b_base64(s.encode("utf-8"))
- else:
- return binascii.a2b_base64(s)
- # Contents should conform to a subset of ISO 8601
- # (in particular, YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z'. Smaller units
- # may be omitted with # a loss of precision)
- _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)
- def _date_from_string(s):
- order = ('year', 'month', 'day', 'hour', 'minute', 'second')
- gd = _dateParser.match(s).groupdict()
- lst = []
- for key in order:
- val = gd[key]
- if val is None:
- break
- lst.append(int(val))
- return datetime.datetime(*lst)
- def _date_to_string(d):
- return '%04d-%02d-%02dT%02d:%02d:%02dZ' % (
- d.year, d.month, d.day,
- d.hour, d.minute, d.second
- )
- def _escape(text):
- m = _controlCharPat.search(text)
- if m is not None:
- raise ValueError("strings can't contain control characters; "
- "use bytes instead")
- text = text.replace("\r\n", "\n") # convert DOS line endings
- text = text.replace("\r", "\n") # convert Mac line endings
- text = text.replace("&", "&") # escape '&'
- text = text.replace("<", "<") # escape '<'
- text = text.replace(">", ">") # escape '>'
- return text
- class _PlistParser:
- def __init__(self, dict_type):
- self.stack = []
- self.current_key = None
- self.root = None
- self._dict_type = dict_type
- def parse(self, fileobj):
- self.parser = ParserCreate()
- self.parser.StartElementHandler = self.handle_begin_element
- self.parser.EndElementHandler = self.handle_end_element
- self.parser.CharacterDataHandler = self.handle_data
- self.parser.EntityDeclHandler = self.handle_entity_decl
- self.parser.ParseFile(fileobj)
- return self.root
- def handle_entity_decl(self, entity_name, is_parameter_entity, value, base, system_id, public_id, notation_name):
- # Reject plist files with entity declarations to avoid XML vulnerabilities in expat.
- # Regular plist files don't contain those declarations, and Apple's plutil tool does not
- # accept them either.
- raise InvalidFileException("XML entity declarations are not supported in plist files")
- def handle_begin_element(self, element, attrs):
- self.data = []
- handler = getattr(self, "begin_" + element, None)
- if handler is not None:
- handler(attrs)
- def handle_end_element(self, element):
- handler = getattr(self, "end_" + element, None)
- if handler is not None:
- handler()
- def handle_data(self, data):
- self.data.append(data)
- def add_object(self, value):
- if self.current_key is not None:
- if not isinstance(self.stack[-1], dict):
- raise ValueError("unexpected element at line %d" %
- self.parser.CurrentLineNumber)
- self.stack[-1][self.current_key] = value
- self.current_key = None
- elif not self.stack:
- # this is the root object
- self.root = value
- else:
- if not isinstance(self.stack[-1], list):
- raise ValueError("unexpected element at line %d" %
- self.parser.CurrentLineNumber)
- self.stack[-1].append(value)
- def get_data(self):
- data = ''.join(self.data)
- self.data = []
- return data
- # element handlers
- def begin_dict(self, attrs):
- d = self._dict_type()
- self.add_object(d)
- self.stack.append(d)
- def end_dict(self):
- if self.current_key:
- raise ValueError("missing value for key '%s' at line %d" %
- (self.current_key,self.parser.CurrentLineNumber))
- self.stack.pop()
- def end_key(self):
- if self.current_key or not isinstance(self.stack[-1], dict):
- raise ValueError("unexpected key at line %d" %
- self.parser.CurrentLineNumber)
- self.current_key = self.get_data()
- def begin_array(self, attrs):
- a = []
- self.add_object(a)
- self.stack.append(a)
- def end_array(self):
- self.stack.pop()
- def end_true(self):
- self.add_object(True)
- def end_false(self):
- self.add_object(False)
- def end_integer(self):
- raw = self.get_data()
- if raw.startswith('0x') or raw.startswith('0X'):
- self.add_object(int(raw, 16))
- else:
- self.add_object(int(raw))
- def end_real(self):
- self.add_object(float(self.get_data()))
- def end_string(self):
- self.add_object(self.get_data())
- def end_data(self):
- self.add_object(_decode_base64(self.get_data()))
- def end_date(self):
- self.add_object(_date_from_string(self.get_data()))
- class _DumbXMLWriter:
- def __init__(self, file, indent_level=0, indent="\t"):
- self.file = file
- self.stack = []
- self._indent_level = indent_level
- self.indent = indent
- def begin_element(self, element):
- self.stack.append(element)
- self.writeln("<%s>" % element)
- self._indent_level += 1
- def end_element(self, element):
- assert self._indent_level > 0
- assert self.stack.pop() == element
- self._indent_level -= 1
- self.writeln("</%s>" % element)
- def simple_element(self, element, value=None):
- if value is not None:
- value = _escape(value)
- self.writeln("<%s>%s</%s>" % (element, value, element))
- else:
- self.writeln("<%s/>" % element)
- def writeln(self, line):
- if line:
- # plist has fixed encoding of utf-8
- # XXX: is this test needed?
- if isinstance(line, str):
- line = line.encode('utf-8')
- self.file.write(self._indent_level * self.indent)
- self.file.write(line)
- self.file.write(b'\n')
- class _PlistWriter(_DumbXMLWriter):
- def __init__(
- self, file, indent_level=0, indent=b"\t", writeHeader=1,
- sort_keys=True, skipkeys=False):
- if writeHeader:
- file.write(PLISTHEADER)
- _DumbXMLWriter.__init__(self, file, indent_level, indent)
- self._sort_keys = sort_keys
- self._skipkeys = skipkeys
- def write(self, value):
- self.writeln("<plist version=\"1.0\">")
- self.write_value(value)
- self.writeln("</plist>")
- def write_value(self, value):
- if isinstance(value, str):
- self.simple_element("string", value)
- elif value is True:
- self.simple_element("true")
- elif value is False:
- self.simple_element("false")
- elif isinstance(value, int):
- if -1 << 63 <= value < 1 << 64:
- self.simple_element("integer", "%d" % value)
- else:
- raise OverflowError(value)
- elif isinstance(value, float):
- self.simple_element("real", repr(value))
- elif isinstance(value, dict):
- self.write_dict(value)
- elif isinstance(value, (bytes, bytearray)):
- self.write_bytes(value)
- elif isinstance(value, datetime.datetime):
- self.simple_element("date", _date_to_string(value))
- elif isinstance(value, (tuple, list)):
- self.write_array(value)
- else:
- raise TypeError("unsupported type: %s" % type(value))
- def write_bytes(self, data):
- self.begin_element("data")
- self._indent_level -= 1
- maxlinelength = max(
- 16,
- 76 - len(self.indent.replace(b"\t", b" " * 8) * self._indent_level))
- for line in _encode_base64(data, maxlinelength).split(b"\n"):
- if line:
- self.writeln(line)
- self._indent_level += 1
- self.end_element("data")
- def write_dict(self, d):
- if d:
- self.begin_element("dict")
- if self._sort_keys:
- items = sorted(d.items())
- else:
- items = d.items()
- for key, value in items:
- if not isinstance(key, str):
- if self._skipkeys:
- continue
- raise TypeError("keys must be strings")
- self.simple_element("key", key)
- self.write_value(value)
- self.end_element("dict")
- else:
- self.simple_element("dict")
- def write_array(self, array):
- if array:
- self.begin_element("array")
- for value in array:
- self.write_value(value)
- self.end_element("array")
- else:
- self.simple_element("array")
- def _is_fmt_xml(header):
- prefixes = (b'<?xml', b'<plist')
- for pfx in prefixes:
- if header.startswith(pfx):
- return True
- # Also check for alternative XML encodings, this is slightly
- # overkill because the Apple tools (and plistlib) will not
- # generate files with these encodings.
- for bom, encoding in (
- (codecs.BOM_UTF8, "utf-8"),
- (codecs.BOM_UTF16_BE, "utf-16-be"),
- (codecs.BOM_UTF16_LE, "utf-16-le"),
- # expat does not support utf-32
- #(codecs.BOM_UTF32_BE, "utf-32-be"),
- #(codecs.BOM_UTF32_LE, "utf-32-le"),
- ):
- if not header.startswith(bom):
- continue
- for start in prefixes:
- prefix = bom + start.decode('ascii').encode(encoding)
- if header[:len(prefix)] == prefix:
- return True
- return False
- #
- # Binary Plist
- #
- class InvalidFileException (ValueError):
- def __init__(self, message="Invalid file"):
- ValueError.__init__(self, message)
- _BINARY_FORMAT = {1: 'B', 2: 'H', 4: 'L', 8: 'Q'}
- _undefined = object()
- class _BinaryPlistParser:
- """
- Read or write a binary plist file, following the description of the binary
- format. Raise InvalidFileException in case of error, otherwise return the
- root object.
- see also: http://opensource.apple.com/source/CF/CF-744.18/CFBinaryPList.c
- """
- def __init__(self, dict_type):
- self._dict_type = dict_type
- def parse(self, fp):
- try:
- # The basic file format:
- # HEADER
- # object...
- # refid->offset...
- # TRAILER
- self._fp = fp
- self._fp.seek(-32, os.SEEK_END)
- trailer = self._fp.read(32)
- if len(trailer) != 32:
- raise InvalidFileException()
- (
- offset_size, self._ref_size, num_objects, top_object,
- offset_table_offset
- ) = struct.unpack('>6xBBQQQ', trailer)
- self._fp.seek(offset_table_offset)
- self._object_offsets = self._read_ints(num_objects, offset_size)
- self._objects = [_undefined] * num_objects
- return self._read_object(top_object)
- except (OSError, IndexError, struct.error, OverflowError,
- ValueError):
- raise InvalidFileException()
- def _get_size(self, tokenL):
- """ return the size of the next object."""
- if tokenL == 0xF:
- m = self._fp.read(1)[0] & 0x3
- s = 1 << m
- f = '>' + _BINARY_FORMAT[s]
- return struct.unpack(f, self._fp.read(s))[0]
- return tokenL
- def _read_ints(self, n, size):
- data = self._fp.read(size * n)
- if size in _BINARY_FORMAT:
- return struct.unpack(f'>{n}{_BINARY_FORMAT[size]}', data)
- else:
- if not size or len(data) != size * n:
- raise InvalidFileException()
- return tuple(int.from_bytes(data[i: i + size], 'big')
- for i in range(0, size * n, size))
- def _read_refs(self, n):
- return self._read_ints(n, self._ref_size)
- def _read_object(self, ref):
- """
- read the object by reference.
- May recursively read sub-objects (content of an array/dict/set)
- """
- result = self._objects[ref]
- if result is not _undefined:
- return result
- offset = self._object_offsets[ref]
- self._fp.seek(offset)
- token = self._fp.read(1)[0]
- tokenH, tokenL = token & 0xF0, token & 0x0F
- if token == 0x00:
- result = None
- elif token == 0x08:
- result = False
- elif token == 0x09:
- result = True
- # The referenced source code also mentions URL (0x0c, 0x0d) and
- # UUID (0x0e), but neither can be generated using the Cocoa libraries.
- elif token == 0x0f:
- result = b''
- elif tokenH == 0x10: # int
- result = int.from_bytes(self._fp.read(1 << tokenL),
- 'big', signed=tokenL >= 3)
- elif token == 0x22: # real
- result = struct.unpack('>f', self._fp.read(4))[0]
- elif token == 0x23: # real
- result = struct.unpack('>d', self._fp.read(8))[0]
- elif token == 0x33: # date
- f = struct.unpack('>d', self._fp.read(8))[0]
- # timestamp 0 of binary plists corresponds to 1/1/2001
- # (year of Mac OS X 10.0), instead of 1/1/1970.
- result = (datetime.datetime(2001, 1, 1) +
- datetime.timedelta(seconds=f))
- elif tokenH == 0x40: # data
- s = self._get_size(tokenL)
- result = self._fp.read(s)
- if len(result) != s:
- raise InvalidFileException()
- elif tokenH == 0x50: # ascii string
- s = self._get_size(tokenL)
- data = self._fp.read(s)
- if len(data) != s:
- raise InvalidFileException()
- result = data.decode('ascii')
- elif tokenH == 0x60: # unicode string
- s = self._get_size(tokenL) * 2
- data = self._fp.read(s)
- if len(data) != s:
- raise InvalidFileException()
- result = data.decode('utf-16be')
- elif tokenH == 0x80: # UID
- # used by Key-Archiver plist files
- result = UID(int.from_bytes(self._fp.read(1 + tokenL), 'big'))
- elif tokenH == 0xA0: # array
- s = self._get_size(tokenL)
- obj_refs = self._read_refs(s)
- result = []
- self._objects[ref] = result
- result.extend(self._read_object(x) for x in obj_refs)
- # tokenH == 0xB0 is documented as 'ordset', but is not actually
- # implemented in the Apple reference code.
- # tokenH == 0xC0 is documented as 'set', but sets cannot be used in
- # plists.
- elif tokenH == 0xD0: # dict
- s = self._get_size(tokenL)
- key_refs = self._read_refs(s)
- obj_refs = self._read_refs(s)
- result = self._dict_type()
- self._objects[ref] = result
- try:
- for k, o in zip(key_refs, obj_refs):
- result[self._read_object(k)] = self._read_object(o)
- except TypeError:
- raise InvalidFileException()
- else:
- raise InvalidFileException()
- self._objects[ref] = result
- return result
- def _count_to_size(count):
- if count < 1 << 8:
- return 1
- elif count < 1 << 16:
- return 2
- elif count < 1 << 32:
- return 4
- else:
- return 8
- _scalars = (str, int, float, datetime.datetime, bytes)
- class _BinaryPlistWriter (object):
- def __init__(self, fp, sort_keys, skipkeys):
- self._fp = fp
- self._sort_keys = sort_keys
- self._skipkeys = skipkeys
- def write(self, value):
- # Flattened object list:
- self._objlist = []
- # Mappings from object->objectid
- # First dict has (type(object), object) as the key,
- # second dict is used when object is not hashable and
- # has id(object) as the key.
- self._objtable = {}
- self._objidtable = {}
- # Create list of all objects in the plist
- self._flatten(value)
- # Size of object references in serialized containers
- # depends on the number of objects in the plist.
- num_objects = len(self._objlist)
- self._object_offsets = [0]*num_objects
- self._ref_size = _count_to_size(num_objects)
- self._ref_format = _BINARY_FORMAT[self._ref_size]
- # Write file header
- self._fp.write(b'bplist00')
- # Write object list
- for obj in self._objlist:
- self._write_object(obj)
- # Write refnum->object offset table
- top_object = self._getrefnum(value)
- offset_table_offset = self._fp.tell()
- offset_size = _count_to_size(offset_table_offset)
- offset_format = '>' + _BINARY_FORMAT[offset_size] * num_objects
- self._fp.write(struct.pack(offset_format, *self._object_offsets))
- # Write trailer
- sort_version = 0
- trailer = (
- sort_version, offset_size, self._ref_size, num_objects,
- top_object, offset_table_offset
- )
- self._fp.write(struct.pack('>5xBBBQQQ', *trailer))
- def _flatten(self, value):
- # First check if the object is in the object table, not used for
- # containers to ensure that two subcontainers with the same contents
- # will be serialized as distinct values.
- if isinstance(value, _scalars):
- if (type(value), value) in self._objtable:
- return
- elif id(value) in self._objidtable:
- return
- # Add to objectreference map
- refnum = len(self._objlist)
- self._objlist.append(value)
- if isinstance(value, _scalars):
- self._objtable[(type(value), value)] = refnum
- else:
- self._objidtable[id(value)] = refnum
- # And finally recurse into containers
- if isinstance(value, dict):
- keys = []
- values = []
- items = value.items()
- if self._sort_keys:
- items = sorted(items)
- for k, v in items:
- if not isinstance(k, str):
- if self._skipkeys:
- continue
- raise TypeError("keys must be strings")
- keys.append(k)
- values.append(v)
- for o in itertools.chain(keys, values):
- self._flatten(o)
- elif isinstance(value, (list, tuple)):
- for o in value:
- self._flatten(o)
- def _getrefnum(self, value):
- if isinstance(value, _scalars):
- return self._objtable[(type(value), value)]
- else:
- return self._objidtable[id(value)]
- def _write_size(self, token, size):
- if size < 15:
- self._fp.write(struct.pack('>B', token | size))
- elif size < 1 << 8:
- self._fp.write(struct.pack('>BBB', token | 0xF, 0x10, size))
- elif size < 1 << 16:
- self._fp.write(struct.pack('>BBH', token | 0xF, 0x11, size))
- elif size < 1 << 32:
- self._fp.write(struct.pack('>BBL', token | 0xF, 0x12, size))
- else:
- self._fp.write(struct.pack('>BBQ', token | 0xF, 0x13, size))
- def _write_object(self, value):
- ref = self._getrefnum(value)
- self._object_offsets[ref] = self._fp.tell()
- if value is None:
- self._fp.write(b'\x00')
- elif value is False:
- self._fp.write(b'\x08')
- elif value is True:
- self._fp.write(b'\x09')
- elif isinstance(value, int):
- if value < 0:
- try:
- self._fp.write(struct.pack('>Bq', 0x13, value))
- except struct.error:
- raise OverflowError(value) from None
- elif value < 1 << 8:
- self._fp.write(struct.pack('>BB', 0x10, value))
- elif value < 1 << 16:
- self._fp.write(struct.pack('>BH', 0x11, value))
- elif value < 1 << 32:
- self._fp.write(struct.pack('>BL', 0x12, value))
- elif value < 1 << 63:
- self._fp.write(struct.pack('>BQ', 0x13, value))
- elif value < 1 << 64:
- self._fp.write(b'\x14' + value.to_bytes(16, 'big', signed=True))
- else:
- raise OverflowError(value)
- elif isinstance(value, float):
- self._fp.write(struct.pack('>Bd', 0x23, value))
- elif isinstance(value, datetime.datetime):
- f = (value - datetime.datetime(2001, 1, 1)).total_seconds()
- self._fp.write(struct.pack('>Bd', 0x33, f))
- elif isinstance(value, (bytes, bytearray)):
- self._write_size(0x40, len(value))
- self._fp.write(value)
- elif isinstance(value, str):
- try:
- t = value.encode('ascii')
- self._write_size(0x50, len(value))
- except UnicodeEncodeError:
- t = value.encode('utf-16be')
- self._write_size(0x60, len(t) // 2)
- self._fp.write(t)
- elif isinstance(value, UID):
- if value.data < 0:
- raise ValueError("UIDs must be positive")
- elif value.data < 1 << 8:
- self._fp.write(struct.pack('>BB', 0x80, value))
- elif value.data < 1 << 16:
- self._fp.write(struct.pack('>BH', 0x81, value))
- elif value.data < 1 << 32:
- self._fp.write(struct.pack('>BL', 0x83, value))
- elif value.data < 1 << 64:
- self._fp.write(struct.pack('>BQ', 0x87, value))
- else:
- raise OverflowError(value)
- elif isinstance(value, (list, tuple)):
- refs = [self._getrefnum(o) for o in value]
- s = len(refs)
- self._write_size(0xA0, s)
- self._fp.write(struct.pack('>' + self._ref_format * s, *refs))
- elif isinstance(value, dict):
- keyRefs, valRefs = [], []
- if self._sort_keys:
- rootItems = sorted(value.items())
- else:
- rootItems = value.items()
- for k, v in rootItems:
- if not isinstance(k, str):
- if self._skipkeys:
- continue
- raise TypeError("keys must be strings")
- keyRefs.append(self._getrefnum(k))
- valRefs.append(self._getrefnum(v))
- s = len(keyRefs)
- self._write_size(0xD0, s)
- self._fp.write(struct.pack('>' + self._ref_format * s, *keyRefs))
- self._fp.write(struct.pack('>' + self._ref_format * s, *valRefs))
- else:
- raise TypeError(value)
- def _is_fmt_binary(header):
- return header[:8] == b'bplist00'
- #
- # Generic bits
- #
- _FORMATS={
- FMT_XML: dict(
- detect=_is_fmt_xml,
- parser=_PlistParser,
- writer=_PlistWriter,
- ),
- FMT_BINARY: dict(
- detect=_is_fmt_binary,
- parser=_BinaryPlistParser,
- writer=_BinaryPlistWriter,
- )
- }
- def load(fp, *, fmt=None, dict_type=dict):
- """Read a .plist file. 'fp' should be a readable and binary file object.
- Return the unpacked root object (which usually is a dictionary).
- """
- if fmt is None:
- header = fp.read(32)
- fp.seek(0)
- for info in _FORMATS.values():
- if info['detect'](header):
- P = info['parser']
- break
- else:
- raise InvalidFileException()
- else:
- P = _FORMATS[fmt]['parser']
- p = P(dict_type=dict_type)
- return p.parse(fp)
- def loads(value, *, fmt=None, dict_type=dict):
- """Read a .plist file from a bytes object.
- Return the unpacked root object (which usually is a dictionary).
- """
- fp = BytesIO(value)
- return load(fp, fmt=fmt, dict_type=dict_type)
- def dump(value, fp, *, fmt=FMT_XML, sort_keys=True, skipkeys=False):
- """Write 'value' to a .plist file. 'fp' should be a writable,
- binary file object.
- """
- if fmt not in _FORMATS:
- raise ValueError("Unsupported format: %r"%(fmt,))
- writer = _FORMATS[fmt]["writer"](fp, sort_keys=sort_keys, skipkeys=skipkeys)
- writer.write(value)
- def dumps(value, *, fmt=FMT_XML, skipkeys=False, sort_keys=True):
- """Return a bytes object with the contents for a .plist file.
- """
- fp = BytesIO()
- dump(value, fp, fmt=fmt, skipkeys=skipkeys, sort_keys=sort_keys)
- return fp.getvalue()
|