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()
Values can be strings, integers, floats, booleans, tuples, lists,
dictionaries (but only with string keys), Data, bytes, bytearray, or
datetime.datetime objects.
aList = ["A", "B", 12, 32.1, [1, 2, 3]],
anotherString = "<hello & hi there!>",
aUnicodeValue = "M\xe4ssig, Ma\xdf",
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:
with open(fileName, 'rb') as fp:
"readPlist", "writePlist", "readPlistFromBytes", "writePlistToBytes",
"Data", "InvalidFileException", "FMT_XML", "FMT_BINARY",
"load", "dump", "loads", "dumps", "UID"
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
@contextlib.contextmanager
def _maybe_open(pathOrFile, mode):
if isinstance(pathOrFile, str):
with open(pathOrFile, mode) as 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",
with _maybe_open(pathOrFile, 'rb') as fp:
return load(fp, fmt=None, use_builtin_types=False)
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",
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",
return load(BytesIO(data), fmt=None, use_builtin_types=False)
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",
dump(value, f, fmt=FMT_XML, sort_keys=True, skipkeys=False)
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")
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)
if isinstance(other, self.__class__):
return self.data == other.data
elif isinstance(other, bytes):
return self.data == other
return "%s(%s)" % (self.__class__.__name__, repr(self.data))
# End of deprecated functionality
def __init__(self, data):
if not isinstance(data, int):
raise TypeError("data must be an int")
raise ValueError("UIDs cannot be >= 2**64")
raise ValueError("UIDs must be positive")
return "%s(%s)" % (self.__class__.__name__, repr(self.data))
return self.__class__, (self.data,)
if not isinstance(other, UID):
return self.data == other.data
<?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
for i in range(0, len(s), maxbinsize):
chunk = s[i : i + maxbinsize]
pieces.append(binascii.b2a_base64(chunk))
return binascii.a2b_base64(s.encode("utf-8"))
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()
return datetime.datetime(*lst)
return '%04d-%02d-%02dT%02d:%02d:%02dZ' % (
d.hour, d.minute, d.second
m = _controlCharPat.search(text)
raise ValueError("strings can't contains control characters; "
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 '>'
def __init__(self, use_builtin_types, dict_type):
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.EntityDeclHandler = self.handle_entity_decl
self.parser.ParseFile(fileobj)
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 vulnerabilies in expat.
# Regular plist files don't contain those declerations, and Apple's plutil tool does not
raise InvalidFileException("XML entity declarations are not supported in plist files")
def handle_begin_element(self, element, attrs):
handler = getattr(self, "begin_" + element, None)
def handle_end_element(self, element):
handler = getattr(self, "end_" + element, None)
def handle_data(self, 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
# this is the root object
if not isinstance(self.stack[-1], type([])):
raise ValueError("unexpected element at line %d" %
self.parser.CurrentLineNumber)
self.stack[-1].append(value)
data = ''.join(self.data)
def begin_dict(self, attrs):
raise ValueError("missing value for key '%s' at line %d" %
(self.current_key,self.parser.CurrentLineNumber))
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):
if raw.startswith('0x') or raw.startswith('0X'):
self.add_object(int(raw, 16))
self.add_object(int(raw))
self.add_object(float(self.get_data()))
self.add_object(self.get_data())
if self._use_builtin_types:
self.add_object(_decode_base64(self.get_data()))
self.add_object(Data.fromBase64(self.get_data()))
self.add_object(_date_from_string(self.get_data()))
def __init__(self, file, indent_level=0, indent="\t"):
self._indent_level = indent_level
def begin_element(self, element):
self.stack.append(element)
self.writeln("<%s>" % element)
def end_element(self, element):
assert self._indent_level > 0
assert self.stack.pop() == element
self.writeln("</%s>" % element)
def simple_element(self, element, value=None):
self.writeln("<%s>%s</%s>" % (element, value, element))
self.writeln("<%s/>" % element)
# 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)
class _PlistWriter(_DumbXMLWriter):
self, file, indent_level=0, indent=b"\t", writeHeader=1,
sort_keys=True, skipkeys=False):
_DumbXMLWriter.__init__(self, file, indent_level, indent)
self._sort_keys = sort_keys
self._skipkeys = skipkeys
self.writeln("<plist version=\"1.0\">")
def write_value(self, value):
if isinstance(value, str):
self.simple_element("string", value)
self.simple_element("true")
self.simple_element("false")
elif isinstance(value, int):
if -1 << 63 <= value < 1 << 64:
self.simple_element("integer", "%d" % value)
raise OverflowError(value)
elif isinstance(value, float):
self.simple_element("real", repr(value))
elif isinstance(value, dict):
elif isinstance(value, Data):
elif isinstance(value, (bytes, bytearray)):
elif isinstance(value, datetime.datetime):
self.simple_element("date", _date_to_string(value))
elif isinstance(value, (tuple, list)):
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")
76 - len(self.indent.replace(b"\t", b" " * 8) * self._indent_level))
for line in _encode_base64(data, maxlinelength).split(b"\n"):
self.begin_element("dict")