12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025 |
- 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:
- pl = dict(
- aString = "Doodah",
- aList = ["A", "B", 12, 32.1, [1, 2, 3]],
- aFloat = 0.1,
- anInt = 728,
- aDict = dict(
- anotherString = "<hello & hi there!>",
- aUnicodeValue = "M\xe4ssig, Ma\xdf",
- aTrueValue = True,
- aFalseValue = False,
- ),
- someData = b"<binary gunk>",
- someMoreData = b"<lots of binary gunk>" * 10,
- aDate = datetime.datetime.fromtimestamp(time.mktime(time.gmtime())),
- )
- with open(fileName, 'wb') as fp:
- dump(pl, fp)
- Parse Plist example:
- with open(fileName, 'rb') as fp:
- pl = load(fp)
- print(pl["aKey"])
- """
- __all__ = [
- "readPlist", "writePlist", "readPlistFromBytes", "writePlistToBytes",
- "Plist", "Data", "Dict", "FMT_XML", "FMT_BINARY",
- "load", "dump", "loads", "dumps"
- ]
- import binascii
- import codecs
- import contextlib
- import datetime
- import enum
- from io import BytesIO
- import itertools
- import os
- import re
- import struct
- from warnings import warn
- from xml.parsers.expat import ParserCreate
- PlistFormat = enum.Enum('PlistFormat', 'FMT_XML FMT_BINARY', module=__name__)
- globals().update(PlistFormat.__members__)
- #
- #
- # Deprecated functionality
- #
- #
- class _InternalDict(dict):
- # This class is needed while Dict is scheduled for deprecation:
- # we only need to warn when a *user* instantiates Dict or when
- # the "attribute notation for dict keys" is used.
- __slots__ = ()
- def __getattr__(self, attr):
- try:
- value = self[attr]
- except KeyError:
- raise AttributeError(attr)
- warn("Attribute access from plist dicts is deprecated, use d[key] "
- "notation instead", DeprecationWarning, 2)
- return value
- def __setattr__(self, attr, value):
- warn("Attribute access from plist dicts is deprecated, use d[key] "
- "notation instead", DeprecationWarning, 2)
- self[attr] = value
- def __delattr__(self, attr):
- try:
- del self[attr]
- except KeyError:
- raise AttributeError(attr)
- warn("Attribute access from plist dicts is deprecated, use d[key] "
- "notation instead", DeprecationWarning, 2)
- class Dict(_InternalDict):
- def __init__(self, **kwargs):
- warn("The plistlib.Dict class is deprecated, use builtin dict instead",
- DeprecationWarning, 2)
- super().__init__(**kwargs)
- @contextlib.contextmanager
- def _maybe_open(pathOrFile, mode):
- if isinstance(pathOrFile, str):
- with open(pathOrFile, mode) as fp:
- yield fp
- else:
- yield pathOrFile
- class Plist(_InternalDict):
- """This class has been deprecated. Use dump() and load()
- functions instead, together with regular dict objects.
- """
- def __init__(self, **kwargs):
- warn("The Plist class is deprecated, use the load() and "
- "dump() functions instead", DeprecationWarning, 2)
- super().__init__(**kwargs)
- @classmethod
- def fromFile(cls, pathOrFile):
- """Deprecated. Use the load() function instead."""
- with _maybe_open(pathOrFile, 'rb') as fp:
- value = load(fp)
- plist = cls()
- plist.update(value)
- return plist
- def write(self, pathOrFile):
- """Deprecated. Use the dump() function instead."""
- with _maybe_open(pathOrFile, 'wb') as fp:
- dump(self, fp)
- def readPlist(pathOrFile):
- """
- Read a .plist from a path or file. pathOrFile should either
- be a file name, or a readable binary file object.
- This function is deprecated, use load instead.
- """
- warn("The readPlist function is deprecated, use load() instead",
- DeprecationWarning, 2)
- with _maybe_open(pathOrFile, 'rb') as fp:
- return load(fp, fmt=None, use_builtin_types=False,
- dict_type=_InternalDict)
- def writePlist(value, pathOrFile):
- """
- Write 'value' to a .plist file. 'pathOrFile' may either be a
- file name or a (writable) file object.
- This function is deprecated, use dump instead.
- """
- warn("The writePlist function is deprecated, use dump() instead",
- DeprecationWarning, 2)
- with _maybe_open(pathOrFile, 'wb') as fp:
- dump(value, fp, fmt=FMT_XML, sort_keys=True, skipkeys=False)
- def readPlistFromBytes(data):
- """
- Read a plist data from a bytes object. Return the root object.
- This function is deprecated, use loads instead.
- """
- warn("The readPlistFromBytes function is deprecated, use loads() instead",
- DeprecationWarning, 2)
- return load(BytesIO(data), fmt=None, use_builtin_types=False,
- dict_type=_InternalDict)
- def writePlistToBytes(value):
- """
- Return 'value' as a plist-formatted bytes object.
- This function is deprecated, use dumps instead.
- """
- warn("The writePlistToBytes function is deprecated, use dumps() instead",
- DeprecationWarning, 2)
- f = BytesIO()
- dump(value, f, fmt=FMT_XML, sort_keys=True, skipkeys=False)
- return f.getvalue()
- class Data:
- """
- Wrapper for binary data.
- This class is deprecated, use a bytes object instead.
- """
- def __init__(self, data):
- if not isinstance(data, bytes):
- raise TypeError("data must be as bytes")
- self.data = data
- @classmethod
- def fromBase64(cls, data):
- # base64.decodebytes just calls binascii.a2b_base64;
- # it seems overkill to use both base64 and binascii.
- return cls(_decode_base64(data))
- def asBase64(self, maxlinelength=76):
- return _encode_base64(self.data, maxlinelength)
- def __eq__(self, other):
- if isinstance(other, self.__class__):
- return self.data == other.data
- elif isinstance(other, bytes):
- return self.data == other
- else:
- return NotImplemented
- def __repr__(self):
- return "%s(%s)" % (self.__class__.__name__, repr(self.data))
- #
- #
- # End of deprecated functionality
- #
- #
- #
- # 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 contains 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, use_builtin_types, dict_type):
- self.stack = []
- self.current_key = None
- self.root = None
- self._use_builtin_types = use_builtin_types
- 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.ParseFile(fileobj)
- return self.root
- 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], type({})):
- 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], type([])):
- 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], type({})):
- 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):
- self.add_object(int(self.get_data()))
- 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):
- if self._use_builtin_types:
- self.add_object(_decode_base64(self.get_data()))
- else:
- self.add_object(Data.fromBase64(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, Data):
- self.write_data(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_data(self, data):
- self.write_bytes(data.data)
- 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'}
- 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, use_builtin_types, dict_type):
- self._use_builtin_types = use_builtin_types
- 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)
- return self._read_object(self._object_offsets[top_object])
- except (OSError, IndexError, struct.error):
- 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('>' + _BINARY_FORMAT[size] * n, data)
- else:
- 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, offset):
- """
- read the object at offset.
- May recursively read sub-objects (content of an array/dict/set)
- """
- self._fp.seek(offset)
- token = self._fp.read(1)[0]
- tokenH, tokenL = token & 0xF0, token & 0x0F
- if token == 0x00:
- return None
- elif token == 0x08:
- return False
- elif token == 0x09:
- return 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:
- return b''
- elif tokenH == 0x10: # int
- return int.from_bytes(self._fp.read(1 << tokenL),
- 'big', signed=tokenL >= 3)
- elif token == 0x22: # real
- return struct.unpack('>f', self._fp.read(4))[0]
- elif token == 0x23: # real
- return 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.
- return datetime.datetime(2001, 1, 1) + datetime.timedelta(seconds=f)
- elif tokenH == 0x40: # data
- s = self._get_size(tokenL)
- if self._use_builtin_types:
- return self._fp.read(s)
- else:
- return Data(self._fp.read(s))
- elif tokenH == 0x50: # ascii string
- s = self._get_size(tokenL)
- result = self._fp.read(s).decode('ascii')
- return result
- elif tokenH == 0x60: # unicode string
- s = self._get_size(tokenL)
- return self._fp.read(s * 2).decode('utf-16be')
- # tokenH == 0x80 is documented as 'UID' and appears to be used for
- # keyed-archiving, not in plists.
- elif tokenH == 0xA0: # array
- s = self._get_size(tokenL)
- obj_refs = self._read_refs(s)
- return [self._read_object(self._object_offsets[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()
- for k, o in zip(key_refs, obj_refs):
- result[self._read_object(self._object_offsets[k])
- ] = self._read_object(self._object_offsets[o])
- return result
- raise InvalidFileException()
- 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
- 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, (
- str, int, float, datetime.datetime, bytes, bytearray)):
- if (type(value), value) in self._objtable:
- return
- elif isinstance(value, Data):
- if (type(value.data), value.data) in self._objtable:
- return
- # Add to objectreference map
- refnum = len(self._objlist)
- self._objlist.append(value)
- try:
- if isinstance(value, Data):
- self._objtable[(type(value.data), value.data)] = refnum
- else:
- self._objtable[(type(value), value)] = refnum
- except TypeError:
- 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):
- try:
- if isinstance(value, Data):
- return self._objtable[(type(value.data), value.data)]
- else:
- return self._objtable[(type(value), value)]
- except TypeError:
- 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, Data):
- self._write_size(0x40, len(value.data))
- self._fp.write(value.data)
- 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(value))
- self._fp.write(t)
- 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, use_builtin_types=True, dict_type=dict):
- """Read a .plist file. 'fp' should be (readable) 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(use_builtin_types=use_builtin_types, dict_type=dict_type)
- return p.parse(fp)
- def loads(value, *, fmt=None, use_builtin_types=True, 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, use_builtin_types=use_builtin_types, 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)
- 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()
|