mirror of
				https://github.com/python/cpython.git
				synced 2025-11-04 03:44:55 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			1025 lines
		
	
	
	
		
			31 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			1025 lines
		
	
	
	
		
			31 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
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, str):
 | 
						|
            return self.data == other
 | 
						|
        else:
 | 
						|
            return id(self) == id(other)
 | 
						|
 | 
						|
    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.utcfromtimestamp(f + (31 * 365 + 8) * 86400)
 | 
						|
 | 
						|
        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()
 |