diff --git a/Doc/library/plistlib.rst b/Doc/library/plistlib.rst --- a/Doc/library/plistlib.rst +++ b/Doc/library/plistlib.rst @@ -22,20 +22,15 @@ basic object types, like dictionaries, lists, numbers and strings. Usually the top level object is a dictionary. -To write out and to parse a plist file, use the :func:`writePlist` and -:func:`readPlist` functions. +To write out and to parse a plist file, use the :func:`dump` and +:func:`load` functions. -To work with plist data in bytes objects, use :func:`writePlistToBytes` -and :func:`readPlistFromBytes`. +To work with plist data in bytes objects, use :func:`dumps` +and :func:`loads`. Values can be strings, integers, floats, booleans, tuples, lists, dictionaries -(but only with string keys), :class:`Data` or :class:`datetime.datetime` -objects. String values (including dictionary keys) have to be unicode strings -- -they will be written out as UTF-8. - -The ```` plist type is supported through the :class:`Data` class. This is -a thin wrapper around a Python bytes object. Use :class:`Data` if your strings -contain control characters. +(but only with string keys), :class:`Data`, :class:`bytes` or +:class:`datetime.datetime` objects. .. seealso:: @@ -45,35 +40,137 @@ This module defines the following functions: -.. function:: readPlist(pathOrFile) +.. function:: load(fp, \*, fmt=None, use_builtin_types=True, dict_type=dict) - Read a plist file. *pathOrFile* may either be a file name or a (readable and - binary) file object. Return the unpacked root object (which usually is a + Read a plist file. *fp* should be a readable and binary file object. + Return the unpacked root object (which usually is a dictionary). - The XML data is parsed using the Expat parser from :mod:`xml.parsers.expat` - -- see its documentation for possible exceptions on ill-formed XML. - Unknown elements will simply be ignored by the plist parser. + The *fmt* is the format of the file and the following values are valid: + * :data:`None`: Autodetect the file format -.. function:: writePlist(rootObject, pathOrFile) + * :data:`FMT_XML`: XML file format - Write *rootObject* to a plist file. *pathOrFile* may either be a file name - or a (writable and binary) file object. + * :data:`FMT_BINARY`: Binary plist format - A :exc:`TypeError` will be raised if the object is of an unsupported type or - a container that contains objects of unsupported types. + If *use_builtin_types* is True (the default) binary data will be returned + as instances of :class:`bytes`, otherwise it is returned as instances of + :class:`Data`. + The *dict_type* is the type used for dictionaries that are read from the + plist file. The exact structure of the plist can be recovered by using + :class:`collections.OrderedDict` (although the order of keys shouldn't be + important in plist files). -.. function:: readPlistFromBytes(data) + XML data for the :data:`FMT_XML` format is parsed using the Expat parser + from :mod:`xml.parsers.expat` -- see its documentation for possible + exceptions on ill-formed XML. Unknown elements will simply be ignored + by the plist parser. + + The parser for the binary format raises :exc:`InvalidFileException` + when the file cannot be parsed. + + .. versionadded:: 3.4 + + +.. function:: loads(data, \*, fmt=None, use_builtin_types=True, dict_type=dict) + + Load a plist from a bytes object. See :func:`load` for an explanation of + the keyword arguments. + + +.. function:: 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. + + The *fmt* argument specifies the format of the plist file and can be + one of the following values: + + * :data:`FMT_XML`: XML formatted plist file + + * :data:`FMT_BINARY`: Binary formatted plist file + + When *sort_keys* is true (the default) the keys for dictionaries will be + written to the plist in sorted order, otherwise they will be written in + the iteration order of the dictionary. + + When *skipkeys* is false (the default) the function raises :exc:`TypeError` + when a key of a dictionary is not a string, otherwise such keys are skipped. + + A :exc:`TypeError` will be raised if the object is of an unsupported type or + a container that contains objects of unsupported types. + + .. versionchanged:: 3.4 + Added the *fmt*, *sort_keys* and *skipkeys* arguments. + + +.. function:: dumps(value, \*, fmt=FMT_XML, sort_keys=True, skipkeys=False) + + Return *value* as a plist-formatted bytes object. See + the documentation for :func:`dump` for an explanation of the keyword + arguments of this function. + + +The following functions are deprecated: + +.. function:: readPlist(pathOrFile, \*, fmt=None, use_builtin_types=False, dict_type=dict) + + Read a plist file. *pathOrFile* may be either a file name or a (readable + and binary) file object. Returns the unpacked root object (which usually + is a dictionary). + + This function calls :func:`load` to do the actual work, the the documentation + of :func:`that function ` for an explanation of the keyword arguments. + + .. note:: + + Dict values in the result have a ``__getattr__`` method that defers + to ``__getitem_``. This means that you can use attribute access to + access items of these dictionaries. + + .. deprecated: 3.4 Use :func:`load` instead. + + .. versionchanged:: 3.4 + Added the *fmt*, *use_builtin_types* and *dict_type* arguments. + + +.. function:: writePlist(rootObject, pathOrFile, \*, fmt=FMT_XML, sort_keys=True, skipkeys=False) + + Write *rootObject* to a plist file. *pathOrFile* may be either a file name + or a (writable and binary) file object + + .. deprecated: 3.4 Use :func:`dump` instead. + + +.. function:: readPlistFromBytes(data, \*, fmt=None, use_builtin_types=False, dict_type=dict) Read a plist data from a bytes object. Return the root object. + See :func:`load` for a description of the keyword arguments. -.. function:: writePlistToBytes(rootObject) + .. note:: + + Dict values in the result have a ``__getattr__`` method that defers + to ``__getitem_``. This means that you can use attribute access to + access items of these dictionaries. + + .. deprecated:: 3.4 Use :func:`loads` instead. + + .. versionchanged:: 3.4 + Added the *fmt*, *use_builtin_types* and *dict_type* arguments. + + +.. function:: writePlistToBytes(rootObject, \*, fmt=FMT_XML, sort_keys=True, skipkeys=False) Return *rootObject* as a plist-formatted bytes object. + .. deprecated:: 3.4 Use :func:`dumps` instead. + + .. versionchanged:: 3.4 + Added the *fmt*, *sort_keys* and *skipkeys* arguments. + The following class is available: @@ -86,6 +183,24 @@ It has one attribute, :attr:`data`, that can be used to retrieve the Python bytes object stored in it. + .. deprecated:: 3.4 Use a :class:`bytes` object instead + + +The following constants are avaiable: + +.. data:: FMT_XML + + The XML format for plist files. + + .. versionadded:: 3.4 + + +.. data:: FMT_BINARY + + The binary format for plist files + + .. versionadded:: 3.4 + Examples -------- diff --git a/Lib/plistlib.py b/Lib/plistlib.py --- a/Lib/plistlib.py +++ b/Lib/plistlib.py @@ -4,8 +4,8 @@ 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 writePlist(rootObject, pathOrFile) -function. 'rootObject' is the top level object, 'pathOrFile' is a +To write out a plist file, use the writePlist(value, pathOrFile) +function. 'value' is the top level object, 'pathOrFile' is a filename or a (writable) file object. To parse a plist from a file, use the readPlist(pathOrFile) function, @@ -48,71 +48,498 @@ pl = readPlist(pathOrFile) print pl["aKey"] """ - - __all__ = [ "readPlist", "writePlist", "readPlistFromBytes", "writePlistToBytes", - "Plist", "Data", "Dict" + "Plist", "Data", "Dict", "FMT_XML", "FMT_BINARY", ] # Note: the Plist and Dict classes have been deprecated. import binascii +import codecs import datetime from io import BytesIO +import os import re +import struct +import itertools +from xml.parsers.expat import ParserCreate +FMT_XML="xml1" +FMT_BINARY="binary1" -def readPlist(pathOrFile): - """Read a .plist file. 'pathOrFile' may either be a file name or a - (readable) file object. Return the unpacked root object (which - usually is a dictionary). +class InvalidFileException (ValueError): + def __init__(self, message="Invalid file"): + ValueError.__init__(self, message) + +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. + + def __getattr__(self, attr): + try: + value = self[attr] + except KeyError: + raise AttributeError(attr) + from warnings import warn + warn("Attribute access from plist dicts is deprecated, use d[key] " + "notation instead", DeprecationWarning, 2) + return value + + def __setattr__(self, attr, value): + from warnings import warn + 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) + from warnings import warn + warn("Attribute access from plist dicts is deprecated, use d[key] " + "notation instead", DeprecationWarning, 2) + +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.handleBeginElement + self.parser.EndElementHandler = self.handleEndElement + self.parser.CharacterDataHandler = self.handleData + self.parser.ParseFile(fileobj) + return self.root + + def handleBeginElement(self, element, attrs): + self.data = [] + handler = getattr(self, "begin_" + element, None) + if handler is not None: + handler(attrs) + + def handleEndElement(self, element): + handler = getattr(self, "end_" + element, None) + if handler is not None: + handler() + + def handleData(self, data): + self.data.append(data) + + def addObject(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 getData(self): + data = ''.join(self.data) + self.data = [] + return data + + # element handlers + + def begin_dict(self, attrs): + d = self._dict_type() + self.addObject(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.getData() + + def begin_array(self, attrs): + a = [] + self.addObject(a) + self.stack.append(a) + + def end_array(self): + self.stack.pop() + + def end_true(self): + self.addObject(True) + + def end_false(self): + self.addObject(False) + + def end_integer(self): + self.addObject(int(self.getData())) + + def end_real(self): + self.addObject(float(self.getData())) + + def end_string(self): + self.addObject(self.getData()) + + def end_data(self): + if self._use_builtin_types: + self.addObject(binascii.a2b_base64(self.getData().encode("utf-8"))) + else: + self.addObject(Data.fromBase64(self.getData().encode("utf-8"))) + + def end_date(self): + self.addObject(_date_from_string(self.getData())) + + +class _BinaryPlistParser(object): """ - didOpen = False - try: - if isinstance(pathOrFile, str): - pathOrFile = open(pathOrFile, 'rb') - didOpen = True - p = PlistParser() - rootObject = p.parse(pathOrFile) - finally: - if didOpen: - pathOrFile.close() - return rootObject + 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, as usual + 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 writePlist(rootObject, pathOrFile): - """Write 'rootObject' to a .plist file. 'pathOrFile' may either be a - file name or a (writable) file object. - """ - didOpen = False - try: - if isinstance(pathOrFile, str): - pathOrFile = open(pathOrFile, 'wb') - didOpen = True - writer = PlistWriter(pathOrFile) - writer.writeln("") - writer.writeValue(rootObject) - writer.writeln("") - finally: - if didOpen: - pathOrFile.close() + def parse(self, fp): + return self.read(fp) + def read(self, fp): + try: + # File format: + # HEADER + # object... + # refid->offset... + # TRAILER + self._fp = fp + self._fp.seek(-32, os.SEEK_END) # go to the trailer + 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) + offset_format = '>' + {1: 'B', 2: 'H', 4: 'L', 8: 'Q', }[offset_size] * num_objects + self._ref_format = {1: 'B', 2: 'H', 4: 'L', 8: 'Q', }[self._ref_size] + self._object_offsets = struct.unpack(offset_format, self._fp.read(offset_size * num_objects)) + return self._read_object(self._object_offsets[top_object]) -def readPlistFromBytes(data): - """Read a plist data from a bytes object. Return the root object. - """ - return readPlist(BytesIO(data)) + except (OSError, IndexError, struct.error): + raise InvalidFileException() + def _get_size(self, tokenL): + """ return the size of the next object.""" + if tokenL == 0xF: + m = int.from_bytes(self._fp.read(1), byteorder='big') & 0x3 + # {log2(bit_number): (bit_number, pattern to use with struct.unpack)} + int_format = {0: (1, '>B'), 1: (2, '>H'), 2: (4, '>L'), 3: (8, '>Q'), } + s, f = int_format[m] + return struct.unpack(f, self._fp.read(s))[0] -def writePlistToBytes(rootObject): - """Return 'rootObject' as a plist-formatted bytes object. - """ - f = BytesIO() - writePlist(rootObject, f) - return f.getvalue() + return tokenL + def _read_refs(self, n): + return struct.unpack('>' + self._ref_format * n, self._fp.read(n * self._ref_size)) -class DumbXMLWriter: + 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 #high and low parts + + 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') + + elif tokenH == 0x20 and tokenL == 0x02: #real + return struct.unpack('>f', self._fp.read(4))[0] + + elif tokenH == 0x20 and tokenL == 0x03: #real + return struct.unpack('>d', self._fp.read(8))[0] + + elif tokenH == 0x30 and tokenL == 0x03: #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() + +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 + if num_objects < 1 << 8: + self._ref_size = 1 + elif num_objects < 1 << 16: + self._ref_size = 2 + elif num_objects < 1 << 32: + self._ref_size = 4 + else: + self._ref_size = 8 + + self._ref_format = {1: 'B', 2: 'H', 4: 'L', 8: 'Q', }[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() + if offset_table_offset < 1 << 8: + offset_size = 1 + elif offset_table_offset < 1 << 16: + offset_size = 2 + elif offset_table_offset < 1 << 32: + offset_size = 4 + else: + offset_size = 8 + + offset_format = '>' + {1: 'B', 2: 'H', 4: 'L', 8: 'Q', }[offset_size] * num_objects + self._fp.write(struct.pack(offset_format, *self._object_offsets)) + + # Write trailer + sortVersion = 0 + trailer = sortVersion, 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 = [] + if self._sort_keys: + for k, v in sorted(value.items()): + if self._skipkeys and not isinstance(k, str): + continue + keys.append(k) + values.append(v) + else: + for k, v in value.items(): + if self._skipkeys and not isinstance(k, str): + continue + 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 == None: + self._fp.write(b'\x00') + + elif isinstance(value, bool) and value == False: + self._fp.write(b'\x08') + + elif isinstance(value, bool) and value == True: + self._fp.write(b'\x09') + + elif isinstance(value, int): + if 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)) + else: + self._fp.write(struct.pack('>BQ', 0x13, 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) or isinstance(value, 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 = [], [] + + before = (self._fp.tell() == 8) + + 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 InvalidFileException() + +class _DumbXMLWriter: def __init__(self, file, indentLevel=0, indent="\t"): self.file = file self.stack = [] @@ -146,59 +573,19 @@ self.file.write(line) self.file.write(b'\n') +class _PlistWriter(_DumbXMLWriter): -# 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\d\d\d\d)(?:-(?P\d\d)(?:-(?P\d\d)(?:T(?P\d\d)(?::(?P\d\d)(?::(?P\d\d))?)?)?)?)?Z", re.ASCII) - -def _dateFromString(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 _dateToString(d): - return '%04d-%02d-%02dT%02d:%02d:%02dZ' % ( - d.year, d.month, d.day, - d.hour, d.minute, d.second - ) - - -# 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 _escape(text): - m = _controlCharPat.search(text) - if m is not None: - raise ValueError("strings can't contains control characters; " - "use plistlib.Data 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 - - -PLISTHEADER = b"""\ - - -""" - -class PlistWriter(DumbXMLWriter): - - def __init__(self, file, indentLevel=0, indent=b"\t", writeHeader=1): + def __init__(self, file, indentLevel=0, indent=b"\t", writeHeader=1, sort_keys=True, skipkeys=False): if writeHeader: file.write(PLISTHEADER) - DumbXMLWriter.__init__(self, file, indentLevel, indent) + _DumbXMLWriter.__init__(self, file, indentLevel, indent) + self._sort_keys = sort_keys + self._skipkeys = skipkeys + + def write(self, value): + self.writeln("") + self.writeValue(value) + self.writeln("") def writeValue(self, value): if isinstance(value, str): @@ -218,19 +605,24 @@ self.writeDict(value) elif isinstance(value, Data): self.writeData(value) + elif isinstance(value, (bytes, bytearray)): + self.writeBytes(value) elif isinstance(value, datetime.datetime): - self.simpleElement("date", _dateToString(value)) + self.simpleElement("date", _date_to_string(value)) elif isinstance(value, (tuple, list)): self.writeArray(value) else: raise TypeError("unsupported type: %s" % type(value)) def writeData(self, data): + self.writeBytes(data.data) + + def writeBytes(self, data): self.beginElement("data") self.indentLevel -= 1 maxlinelength = max(16, 76 - len(self.indent.replace(b"\t", b" " * 8) * self.indentLevel)) - for line in data.asBase64(maxlinelength).split(b"\n"): + for line in _encode_base64(data, maxlinelength).split(b"\n"): if line: self.writeln(line) self.indentLevel += 1 @@ -239,9 +631,15 @@ def writeDict(self, d): if d: self.beginElement("dict") - items = sorted(d.items()) + 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.simpleElement("key", key) self.writeValue(value) @@ -258,37 +656,169 @@ else: self.simpleElement("array") +def _is_fmt_xml(header): + prefixes = (b'\d\d\d\d)(?:-(?P\d\d)(?:-(?P\d\d)(?:T(?P\d\d)(?::(?P\d\d)(?::(?P\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 + ) + + +# 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 _escape(text): + m = _controlCharPat.search(text) + if m is not None: + raise ValueError("strings can't contains control characters; " + "use plistlib.Data 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 + + +PLISTHEADER = b"""\ + + +""" + + + class Dict(_InternalDict): @@ -313,9 +843,9 @@ def fromFile(cls, pathOrFile): """Deprecated. Use the readPlist() function instead.""" - rootObject = readPlist(pathOrFile) + value = readPlist(pathOrFile) plist = cls() - plist.update(rootObject) + plist.update(value) return plist fromFile = classmethod(fromFile) @@ -324,7 +854,7 @@ writePlist(self, pathOrFile) -def _encodeBase64(s, maxlinelength=76): +def _encode_base64(s, maxlinelength=76): # copied from base64.encodebytes(), with added maxlinelength argument maxbinsize = (maxlinelength//4)*3 pieces = [] @@ -349,7 +879,7 @@ return cls(binascii.a2b_base64(data)) def asBase64(self, maxlinelength=76): - return _encodeBase64(self.data, maxlinelength) + return _encode_base64(self.data, maxlinelength) def __eq__(self, other): if isinstance(other, self.__class__): @@ -362,93 +892,25 @@ def __repr__(self): return "%s(%s)" % (self.__class__.__name__, repr(self.data)) -class PlistParser: - def __init__(self): - self.stack = [] - self.currentKey = None - self.root = None +# simular interface as the pickle and json modules: - def parse(self, fileobj): - from xml.parsers.expat import ParserCreate - self.parser = ParserCreate() - self.parser.StartElementHandler = self.handleBeginElement - self.parser.EndElementHandler = self.handleEndElement - self.parser.CharacterDataHandler = self.handleData - self.parser.ParseFile(fileobj) - return self.root +def dump(value, fp, *, skipkeys=False, sort_keys=True): + if not hasattr(fp, 'write'): + raise TypeError("fp must be a file-like object") - def handleBeginElement(self, element, attrs): - self.data = [] - handler = getattr(self, "begin_" + element, None) - if handler is not None: - handler(attrs) + writePlist(value, fp, fmt=fmt, sort_keys=sort_keys, skipkeys=skipkeys) - def handleEndElement(self, element): - handler = getattr(self, "end_" + element, None) - if handler is not None: - handler() +def dumps(value, *, skipkeys=False, sort_keys=True): + fp = BytesIO() + dump(value, fp, skipkeys=skipkeys, sort_keys=sort_keys) + return fp.getvalue() - def handleData(self, data): - self.data.append(data) +def load(fp, *, fmt=None, dict_type=dict): + if not hasattr(fp, 'read'): + raise TypeError("fp must be a file-like object") + return readPlist(fp, fmt=fmt, use_builtin_types=True, dict_type=dict_type) - def addObject(self, value): - if self.currentKey is not None: - if not isinstance(self.stack[-1], type({})): - raise ValueError("unexpected element at line %d" % - self.parser.CurrentLineNumber) - self.stack[-1][self.currentKey] = value - self.currentKey = 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 getData(self): - data = ''.join(self.data) - self.data = [] - return data - - # element handlers - - def begin_dict(self, attrs): - d = _InternalDict() - self.addObject(d) - self.stack.append(d) - def end_dict(self): - if self.currentKey: - raise ValueError("missing value for key '%s' at line %d" % - (self.currentKey,self.parser.CurrentLineNumber)) - self.stack.pop() - - def end_key(self): - if self.currentKey or not isinstance(self.stack[-1], type({})): - raise ValueError("unexpected key at line %d" % - self.parser.CurrentLineNumber) - self.currentKey = self.getData() - - def begin_array(self, attrs): - a = [] - self.addObject(a) - self.stack.append(a) - def end_array(self): - self.stack.pop() - - def end_true(self): - self.addObject(True) - def end_false(self): - self.addObject(False) - def end_integer(self): - self.addObject(int(self.getData())) - def end_real(self): - self.addObject(float(self.getData())) - def end_string(self): - self.addObject(self.getData()) - def end_data(self): - self.addObject(Data.fromBase64(self.getData().encode("utf-8"))) - def end_date(self): - self.addObject(_dateFromString(self.getData())) +def loads(value, *, fmt=None): + fp = BytesIO(value) + return load(fp, fmt=fmt) diff --git a/Lib/test/test_plistlib.py b/Lib/test/test_plistlib.py --- a/Lib/test/test_plistlib.py +++ b/Lib/test/test_plistlib.py @@ -4,92 +4,20 @@ import plistlib import os import datetime +import codecs +import collections from test import support +from io import BytesIO -# This test data was generated through Cocoa's NSDictionary class -TESTDATA = b""" - - - - aDate - 2004-10-26T10:33:33Z - aDict - - aFalseValue - - aTrueValue - - aUnicodeValue - M\xc3\xa4ssig, Ma\xc3\x9f - anotherString - <hello & 'hi' there!> - deeperDict - - a - 17 - b - 32.5 - c - - 1 - 2 - text - - - - aFloat - 0.5 - aList - - A - B - 12 - 32.5 - - 1 - 2 - 3 - - - aString - Doodah - anEmptyDict - - anEmptyList - - anInt - 728 - nestedData - - - PGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5r - PgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5 - IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBi - aW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3Rz - IG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQID - PGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAw== - - - someData - - PGJpbmFyeSBndW5rPg== - - someMoreData - - PGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8 - bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxs - b3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxv - dHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90 - cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAw== - - \xc3\x85benraa - That was a unicode key. - - -""".replace(b" " * 8, b"\t") # Apple as well as plistlib.py output hard tabs +ALL_FORMATS=(plistlib.FMT_XML, plistlib.FMT_BINARY) +# The testdata is generated using Mac/Tools/plistlib_generate_testdata.py +# (which using PyObjC to control the Cocoa classes for generating plists) +TESTDATA={ + plistlib.FMT_XML: b'\n\n\n\n\taDate\n\t2004-10-26T10:33:33Z\n\taDict\n\t\n\t\taFalseValue\n\t\t\n\t\taTrueValue\n\t\t\n\t\taUnicodeValue\n\t\tM\xc3\xa4ssig, Ma\xc3\x9f\n\t\tanotherString\n\t\t<hello & \'hi\' there!>\n\t\tdeeperDict\n\t\t\n\t\t\ta\n\t\t\t17\n\t\t\tb\n\t\t\t32.5\n\t\t\tc\n\t\t\t\n\t\t\t\t1\n\t\t\t\t2\n\t\t\t\ttext\n\t\t\t\n\t\t\n\t\n\taFloat\n\t0.5\n\taList\n\t\n\t\tA\n\t\tB\n\t\t12\n\t\t32.5\n\t\t\n\t\t\t1\n\t\t\t2\n\t\t\t3\n\t\t\n\t\n\taString\n\tDoodah\n\tanEmptyDict\n\t\n\tanEmptyList\n\t\n\tanInt\n\t728\n\tnestedData\n\t\n\t\t\n\t\tPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5r\n\t\tPgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5\n\t\tIGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBi\n\t\taW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3Rz\n\t\tIG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQID\n\t\tPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAw==\n\t\t\n\t\n\tsomeData\n\t\n\tPGJpbmFyeSBndW5rPg==\n\t\n\tsomeMoreData\n\t\n\tPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8\n\tbG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAzxs\n\tb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90cyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxv\n\tdHMgb2YgYmluYXJ5IGd1bms+AAECAzxsb3RzIG9mIGJpbmFyeSBndW5rPgABAgM8bG90\n\tcyBvZiBiaW5hcnkgZ3Vuaz4AAQIDPGxvdHMgb2YgYmluYXJ5IGd1bms+AAECAw==\n\t\n\t\xc3\x85benraa\n\tThat was a unicode key.\n\n\n', + plistlib.FMT_BINARY: b'bplist00\xdc\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e"#)*+,-/.0UaDateUaDictVaFloatUaListWaString[anEmptyDict[anEmptyListUanIntZnestedDataXsomeData\\someMoreDatag\x00\xc5\x00b\x00e\x00n\x00r\x00a\x00a3A\x9c\xb9}\xf4\x00\x00\x00\xd5\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18[aFalseValueZaTrueValue]aUnicodeValue]anotherStringZdeeperDict\x08\tk\x00M\x00\xe4\x00s\x00s\x00i\x00g\x00,\x00 \x00M\x00a\x00\xdf_\x10\x15\xd3\x19\x1a\x1b\x1c\x1d\x1eQaQbQc\x10\x11#@@@\x00\x00\x00\x00\x00\xa3\x1f !\x10\x01\x10\x02Ttext#?\xe0\x00\x00\x00\x00\x00\x00\xa5$%&\x1d\'QAQB\x10\x0c\xa3\x1f (\x10\x03VDoodah\xd0\xa0\x11\x02\xd8\xa1.O\x10\xfa\x00\x01\x02\x03\x00\x01\x02\x03\x00\x01\x02\x03\x00\x01\x02\x03\x00\x01\x02\x03\x00\x01\x02\x03\x00\x01\x02\x03\x00\x01\x02\x03\x00\x01\x02\x03\x00\x01\x02\x03M_\x10\x17That was a unicode key.\x00\x08\x00!\x00\'\x00-\x004\x00:\x00B\x00N\x00Z\x00`\x00k\x00t\x00\x81\x00\x90\x00\x99\x00\xa4\x00\xb0\x00\xbb\x00\xc9\x00\xd7\x00\xe2\x00\xe3\x00\xe4\x00\xfb\x01\x13\x01\x1a\x01\x1c\x01\x1e\x01 \x01"\x01+\x01/\x011\x013\x018\x01A\x01G\x01I\x01K\x01M\x01Q\x01S\x01Z\x01[\x01\\\x01_\x01a\x02^\x02l\x00\x00\x00\x00\x00\x00\x02\x01\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x86', +} class TestPlistlib(unittest.TestCase): @@ -99,7 +27,7 @@ except: pass - def _create(self): + def _create(self, fmt=None): pl = dict( aString="Doodah", aList=["A", "B", 12, 32.5, [1, 2, 3]], @@ -154,24 +82,149 @@ self.assertEqual(plistlib.readPlistFromBytes(plistlib.writePlistToBytes(data)), data) def test_appleformatting(self): - pl = plistlib.readPlistFromBytes(TESTDATA) - data = plistlib.writePlistToBytes(pl) - self.assertEqual(data, TESTDATA, - "generated data was not identical to Apple's output") + for use_builtin_types in (True, False): + for fmt in ALL_FORMATS: + with self.subTest(fmt=fmt, use_builtin_types=use_builtin_types): + pl = plistlib.readPlistFromBytes(TESTDATA[fmt], use_builtin_types=use_builtin_types) + data = plistlib.writePlistToBytes(pl, fmt=fmt) + self.assertEqual(data, TESTDATA[fmt], "generated data was not identical to Apple's output") + def test_appleformattingfromliteral(self): - pl = self._create() - pl2 = plistlib.readPlistFromBytes(TESTDATA) - self.assertEqual(dict(pl), dict(pl2), - "generated data was not identical to Apple's output") + self.maxDiff = None + for fmt in ALL_FORMATS: + with self.subTest(fmt=fmt): + pl = self._create(fmt=fmt) + pl2 = plistlib.readPlistFromBytes(TESTDATA[fmt]) + self.assertEqual(dict(pl), dict(pl2), "generated data was not identical to Apple's output") def test_bytesio(self): - from io import BytesIO - b = BytesIO() - pl = self._create() - plistlib.writePlist(pl, b) - pl2 = plistlib.readPlist(BytesIO(b.getvalue())) - self.assertEqual(dict(pl), dict(pl2)) + for fmt in ALL_FORMATS: + with self.subTest(fmt=fmt): + b = BytesIO() + pl = self._create(fmt=fmt) + plistlib.writePlist(pl, b, fmt=fmt) + pl2 = plistlib.readPlist(BytesIO(b.getvalue())) + self.assertEqual(dict(pl), dict(pl2)) + + def test_keysort_bytesio(self): + pl = collections.OrderedDict() + pl['b'] = 1 + pl['a'] = 2 + pl['c'] = 3 + + for fmt in ALL_FORMATS: + for sort_keys in (False, True): + with self.subTest(fmt=fmt, sort_keys=sort_keys): + b = BytesIO() + + plistlib.writePlist(pl, b, fmt=fmt, sort_keys=sort_keys) + pl2 = plistlib.readPlist(BytesIO(b.getvalue()), dict_type=collections.OrderedDict) + + self.assertEqual(dict(pl), dict(pl2)) + if sort_keys: + self.assertEqual(list(pl2.keys()), ['a', 'b', 'c']) + else: + self.assertEqual(list(pl2.keys()), ['b', 'a', 'c']) + + def test_keysort(self): + pl = collections.OrderedDict() + pl['b'] = 1 + pl['a'] = 2 + pl['c'] = 3 + + for fmt in ALL_FORMATS: + for sort_keys in (False, True): + with self.subTest(fmt=fmt, sort_keys=sort_keys): + data = plistlib.writePlistToBytes(pl, fmt=fmt, sort_keys=sort_keys) + pl2 = plistlib.readPlistFromBytes(data, dict_type=collections.OrderedDict) + + self.assertEqual(dict(pl), dict(pl2)) + if sort_keys: + self.assertEqual(list(pl2.keys()), ['a', 'b', 'c']) + else: + self.assertEqual(list(pl2.keys()), ['b', 'a', 'c']) + + def test_keys_no_string(self): + pl = { 42: 'aNumber' } + + for fmt in ALL_FORMATS: + with self.subTest(fmt=fmt): + self.assertRaises(TypeError, plistlib.writePlistToBytes, pl, fmt=fmt) + + b = BytesIO() + self.assertRaises(TypeError, plistlib.writePlist, pl, b, fmt=fmt) + + def test_skipkeys(self): + pl = { + 42: 'aNumber', + 'snake': 'aWord', + } + + for fmt in ALL_FORMATS: + with self.subTest(fmt=fmt): + data = plistlib.writePlistToBytes(pl, fmt=fmt, skipkeys=True, sort_keys=False) + pl2 = plistlib.readPlistFromBytes(data) + self.assertEqual(pl2, {'snake': 'aWord'}) + + fp = BytesIO() + data = plistlib.writePlist(pl, fp, fmt=fmt, skipkeys=True, sort_keys=False) + pl2 = plistlib.readPlistFromBytes(fp.getvalue()) + self.assertEqual(pl2, {'snake': 'aWord'}) + + def test_tuple_members(self): + pl = { + 'first': (1, 2), + 'second': (1, 2), + 'third': (3, 4), + } + + for fmt in ALL_FORMATS: + with self.subTest(fmt=fmt): + data = plistlib.writePlistToBytes(pl, fmt=fmt) + pl2 = plistlib.readPlistFromBytes(data) + self.assertEqual(pl2, { + 'first': [1, 2], + 'second': [1, 2], + 'third': [3, 4], + }) + self.assertIsNot(pl2['first'], pl2['second']) + + def test_list_members(self): + pl = { + 'first': [1, 2], + 'second': [1, 2], + 'third': [3, 4], + } + + for fmt in ALL_FORMATS: + with self.subTest(fmt=fmt): + data = plistlib.writePlistToBytes(pl, fmt=fmt) + pl2 = plistlib.readPlistFromBytes(data) + self.assertEqual(pl2, { + 'first': [1, 2], + 'second': [1, 2], + 'third': [3, 4], + }) + self.assertIsNot(pl2['first'], pl2['second']) + + def test_dict_members(self): + pl = { + 'first': {'a': 1}, + 'second': {'a': 1}, + 'third': {'b': 2 }, + } + + for fmt in ALL_FORMATS: + with self.subTest(fmt=fmt): + data = plistlib.writePlistToBytes(pl, fmt=fmt) + pl2 = plistlib.readPlistFromBytes(data) + self.assertEqual(pl2, { + 'first': {'a': 1}, + 'second': {'a': 1}, + 'third': {'b': 2 }, + }) + self.assertIsNot(pl2['first'], pl2['second']) def test_controlcharacters(self): for i in range(128): @@ -179,19 +232,21 @@ testString = "string containing %s" % c if i >= 32 or c in "\r\n\t": # \r, \n and \t are the only legal control chars in XML - plistlib.writePlistToBytes(testString) + plistlib.writePlistToBytes(testString, fmt=plistlib.FMT_XML) else: self.assertRaises(ValueError, plistlib.writePlistToBytes, testString) def test_nondictroot(self): - test1 = "abc" - test2 = [1, 2, 3, "abc"] - result1 = plistlib.readPlistFromBytes(plistlib.writePlistToBytes(test1)) - result2 = plistlib.readPlistFromBytes(plistlib.writePlistToBytes(test2)) - self.assertEqual(test1, result1) - self.assertEqual(test2, result2) + for fmt in ALL_FORMATS: + with self.subTest(fmt=fmt): + test1 = "abc" + test2 = [1, 2, 3, "abc"] + result1 = plistlib.readPlistFromBytes(plistlib.writePlistToBytes(test1, fmt=fmt)) + result2 = plistlib.readPlistFromBytes(plistlib.writePlistToBytes(test2, fmt=fmt)) + self.assertEqual(test1, result1) + self.assertEqual(test2, result2) def test_invalidarray(self): for i in ["key inside an array", @@ -219,6 +274,25 @@ self.assertRaises(ValueError, plistlib.readPlistFromBytes, b"not real") + def test_xml_encodings(self): + base = TESTDATA[plistlib.FMT_XML] + + for xml_encoding, encoding, bom in [ + (b'utf-8', 'utf-8', codecs.BOM_UTF8), + (b'utf-16', 'utf-16-le', codecs.BOM_UTF16_LE), + (b'utf-16', 'utf-16-be', codecs.BOM_UTF16_BE), + # Expat does not support UTF-32 + #(b'utf-32', 'utf-32-le', codecs.BOM_UTF32_LE), + #(b'utf-32', 'utf-32-be', codecs.BOM_UTF32_BE), + ]: + + pl = self._create(fmt=plistlib.FMT_XML) + with self.subTest(encoding=encoding): + data = base.replace(b'UTF-8', xml_encoding) + data = bom + data.decode('utf-8').encode(encoding) + pl2 = plistlib.readPlistFromBytes(data) + self.assertEqual(dict(pl), dict(pl2)) + def test_main(): support.run_unittest(TestPlistlib)