Utilities/binplistlib.py

changeset 1720
201622cf8a01
child 1965
96f5a76e1845
diff -r c65aefefa2ff -r 201622cf8a01 Utilities/binplistlib.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Utilities/binplistlib.py	Mon Mar 19 20:14:07 2012 +0100
@@ -0,0 +1,967 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2012 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a library for reading and writing binary property list files.
+
+Binary Property List (plist) files provide a faster and smaller serialization
+format for property lists on OS X. This is a library for generating binary
+plists which can be read by OS X, iOS, or other clients.
+
+The API models the plistlib API, and will call through to plistlib when
+XML serialization or deserialization is required.
+
+To generate plists with UID values, wrap the values with the Uid object. The
+value must be an int.
+
+To generate plists with NSData/CFData values, wrap the values with the
+Data object. The value must be a bytes object.
+
+Date values can only be datetime.datetime objects.
+
+The exceptions InvalidPlistException and NotBinaryPlistException may be 
+thrown to indicate that the data cannot be serialized or deserialized as
+a binary plist.
+
+Plist generation example:
+<pre>
+    from binplistlib import *
+    from datetime import datetime
+    plist = {'aKey':'aValue',
+             '0':1.322,
+             'now':datetime.now(),
+             'list':[1,2,3],
+             'tuple':('a','b','c')
+             }
+    try:
+        writePlist(plist, "example.plist")
+    except (InvalidPlistException, NotBinaryPlistException) as e:
+        print("Something bad happened:", e)
+</pre>
+Plist parsing example:
+<pre>
+    from binplistlib import *
+    try:
+        plist = readPlist("example.plist")
+        print(plist)
+    except (InvalidPlistException, NotBinaryPlistException) as e:
+        print("Not a plist:", e)
+</pre>
+"""
+
+#
+# Ported from the Python 2 biplist.py script.
+#
+# Original License:
+#
+# Copyright (c) 2010, Andrew Wooster
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+#    * Redistributions of source code must retain the above copyright notice,
+#      this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above copyright
+#      notice, this list of conditions and the following disclaimer in the
+#      documentation and/or other materials provided with the distribution.
+#    * Neither the name of biplist nor the names of its contributors may be
+#      used to endorse or promote products derived from this software without 
+#      specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+from collections import namedtuple
+from io import BytesIO
+import calendar
+import datetime
+import math
+import plistlib
+from struct import pack, unpack
+
+__all__ = [
+    'Uid', 'Data', 'readPlist', 'writePlist', 'readPlistFromBytes',
+    'writePlistToBytes', 'InvalidPlistException', 'NotBinaryPlistException'
+]
+
+apple_reference_date_offset = 978307200
+
+
+class Uid(int):
+    """
+    Class implementing a wrapper around integers for representing UID values.
+    
+    This is used in keyed archiving.
+    """
+    def __repr__(self):
+        return "Uid(%d)" % self
+
+
+class Data(bytes):
+    """
+    Class implementing a wrapper around bytes types for representing Data values.
+    """
+    pass
+
+
+class InvalidPlistException(Exception):
+    """
+    Exception raised when the plist is incorrectly formatted.
+    """
+    pass
+
+
+class NotBinaryPlistException(Exception):
+    """
+    Exception raised when a binary plist was expected but not encountered.
+    """
+    pass
+
+
+def readPlist(pathOrFile):
+    """
+    Module function to read a plist file.
+    
+    @param pathOrFile name of the plist file (string) or an open file (file object)
+    @return reference to the read object
+    @exception InvalidPlistException raised to signal an invalid plist file
+    """
+    didOpen = False
+    result = None
+    if isinstance(pathOrFile, str):
+        pathOrFile = open(pathOrFile, 'rb')
+        didOpen = True
+    try:
+        reader = PlistReader(pathOrFile)
+        result = reader.parse()
+    except NotBinaryPlistException as e:
+        try:
+            pathOrFile.seek(0)
+            result = plistlib.readPlist(pathOrFile)
+        except Exception as e:
+            raise InvalidPlistException(e)
+    if didOpen:
+        pathOrFile.close()
+    return result
+
+def writePlist(rootObject, pathOrFile, binary=True):
+    """
+    Module function to write a plist file.
+    
+    @param rootObject reference to the object to be written
+    @param pathOrFile name of the plist file (string) or an open file (file object)
+    @param binary flag indicating the generation of a binary plist file (boolean)
+    """
+    if not binary:
+        plistlib.writePlist(rootObject, pathOrFile)
+        return
+    else:
+        didOpen = False
+        if isinstance(pathOrFile, str):
+            pathOrFile = open(pathOrFile, 'wb')
+            didOpen = True
+        writer = PlistWriter(pathOrFile)
+        writer.writeRoot(rootObject)
+        if didOpen:
+            pathOrFile.close()
+        return
+
+def readPlistFromBytes(data):
+    """
+    Module function to read from a plist bytes object.
+    
+    @param data plist data (bytes)
+    @return reference to the read object
+    @exception InvalidPlistException raised to signal an invalid plist file
+    """
+    return readPlist(BytesIO(data))
+
+def writePlistToBytes(rootObject, binary=True):
+    """
+    Module function to write a plist bytes object.
+    
+    @param rootObject reference to the object to be written
+    @param binary flag indicating the generation of a binary plist bytes object (boolean)
+    """
+    if not binary:
+        return plistlib.writePlistToBytes(rootObject)
+    else:
+        io = BytesIO()
+        writer = PlistWriter(io)
+        writer.writeRoot(rootObject)
+        return io.getvalue()
+
+def is_stream_binary_plist(stream):
+    """
+    Module function to check, if the stream is a binary plist.
+    
+    @param stream plist stream (file object)
+    @return flag indicating a binary plist (boolean)
+    """
+    stream.seek(0)
+    header = stream.read(7)
+    if header == b'bplist0':
+        return True
+    else:
+        return False
+
+PlistTrailer = namedtuple('PlistTrailer',
+    'offsetSize, objectRefSize, offsetCount, topLevelObjectNumber, offsetTableOffset')
+PlistByteCounts = namedtuple('PlistByteCounts',
+    'nullBytes, boolBytes, intBytes, realBytes, dateBytes, dataBytes, stringBytes, '
+    'uidBytes, arrayBytes, setBytes, dictBytes')
+
+class PlistReader(object):
+    """
+    Class implementing the plist reader.
+    """
+    file = None
+    contents = b''
+    offsets = None
+    trailer = None
+    currentOffset = 0
+    
+    def __init__(self, fileOrStream):
+        """
+        Constructor
+        
+        @param fileOrStream open file containing the plist data (file object)
+        """
+        self.reset()
+        self.file = fileOrStream
+    
+    def parse(self):
+        """
+        Public method to parse the plist data.
+        
+        @return unpickled object
+        """
+        return self.readRoot()
+    
+    def reset(self):
+        """
+        Private method to reset the instance object.
+        """
+        self.trailer = None
+        self.contents = b''
+        self.offsets = []
+        self.currentOffset = 0
+    
+    def readRoot(self):
+        """
+        Private method to read the root object.
+        
+        @return unpickled object
+        """
+        result = None
+        self.reset()
+        # Get the header, make sure it's a valid file.
+        if not is_stream_binary_plist(self.file):
+            raise NotBinaryPlistException()
+        self.file.seek(0)
+        self.contents = self.file.read()
+        if len(self.contents) < 32:
+            raise InvalidPlistException("File is too short.")
+        trailerContents = self.contents[-32:]
+        try:
+            self.trailer = PlistTrailer._make(unpack("!xxxxxxBBQQQ", trailerContents))
+            offset_size = self.trailer.offsetSize * self.trailer.offsetCount
+            offset = self.trailer.offsetTableOffset
+            offset_contents = self.contents[offset:offset+offset_size]
+            offset_i = 0
+            while offset_i < self.trailer.offsetCount:
+                begin = self.trailer.offsetSize*offset_i
+                tmp_contents = offset_contents[begin:begin+self.trailer.offsetSize]
+                tmp_sized = self.getSizedInteger(tmp_contents, self.trailer.offsetSize)
+                self.offsets.append(tmp_sized)
+                offset_i += 1
+            self.setCurrentOffsetToObjectNumber(self.trailer.topLevelObjectNumber)
+            result = self.readObject()
+        except TypeError as e:
+            raise InvalidPlistException(e)
+        return result
+    
+    def setCurrentOffsetToObjectNumber(self, objectNumber):
+        """
+        Private method to set the current offset.
+        
+        @param objectNumber number of the object (integer)
+        """
+        self.currentOffset = self.offsets[objectNumber]
+    
+    def readObject(self):
+        """
+        Private method to read the object data.
+        
+        @return unpickled object
+        """
+        result = None
+        tmp_byte = self.contents[self.currentOffset:self.currentOffset+1]
+        marker_byte = unpack("!B", tmp_byte)[0]
+        format = (marker_byte >> 4) & 0x0f
+        extra = marker_byte & 0x0f
+        self.currentOffset += 1
+        
+        def proc_extra(extra):
+            if extra == 0b1111:
+                #self.currentOffset += 1
+                extra = self.readObject()
+            return extra
+        
+        # bool, null, or fill byte
+        if format == 0b0000:
+            if extra == 0b0000:
+                result = None
+            elif extra == 0b1000:
+                result = False
+            elif extra == 0b1001:
+                result = True
+            elif extra == 0b1111:
+                pass # fill byte
+            else:
+                raise InvalidPlistException(
+                    "Invalid object found at offset: {0}".format(self.currentOffset - 1))
+        # int
+        elif format == 0b0001:
+            extra = proc_extra(extra)
+            result = self.readInteger(pow(2, extra))
+        # real
+        elif format == 0b0010:
+            extra = proc_extra(extra)
+            result = self.readReal(extra)
+        # date
+        elif format == 0b0011 and extra == 0b0011:
+            result = self.readDate()
+        # data
+        elif format == 0b0100:
+            extra = proc_extra(extra)
+            result = self.readData(extra)
+        # ascii string
+        elif format == 0b0101:
+            extra = proc_extra(extra)
+            result = self.readAsciiString(extra)
+        # Unicode string
+        elif format == 0b0110:
+            extra = proc_extra(extra)
+            result = self.readUnicode(extra)
+        # uid
+        elif format == 0b1000:
+            result = self.readUid(extra)
+        # array
+        elif format == 0b1010:
+            extra = proc_extra(extra)
+            result = self.readArray(extra)
+        # set
+        elif format == 0b1100:
+            extra = proc_extra(extra)
+            result = set(self.readArray(extra))
+        # dict
+        elif format == 0b1101:
+            extra = proc_extra(extra)
+            result = self.readDict(extra)
+        else:    
+            raise InvalidPlistException(
+                "Invalid object found: {{format: {0}, extra: {1}}}".format(
+                    bin(format), bin(extra)))
+        return result
+    
+    def readInteger(self, bytes):
+        """
+        Private method to read an Integer object.
+        
+        @param bytes length of the object (integer)
+        @return integer object
+        """
+        result = 0
+        original_offset = self.currentOffset
+        data = self.contents[self.currentOffset:self.currentOffset+bytes]
+        result = self.getSizedInteger(data, bytes)
+        self.currentOffset = original_offset + bytes
+        return result
+    
+    def readReal(self, length):
+        """
+        Private method to read a Real object.
+        
+        @param length length of the object (integer)
+        @return float object
+        """
+        result = 0.0
+        to_read = pow(2, length)
+        data = self.contents[self.currentOffset:self.currentOffset+to_read]
+        if length == 2: # 4 bytes
+            result = unpack('>f', data)[0]
+        elif length == 3: # 8 bytes
+            result = unpack('>d', data)[0]
+        else:
+            raise InvalidPlistException(
+                "Unknown real of length {0} bytes".format(to_read))
+        return result
+    
+    def readRefs(self, count):    
+        """
+        Private method to read References.
+        
+        @param count amount of the references (integer)
+        @return list of references (list of integers)
+        """
+        refs = []
+        i = 0
+        while i < count:
+            fragment = self.contents[
+                self.currentOffset:self.currentOffset+self.trailer.objectRefSize]
+            ref = self.getSizedInteger(fragment, len(fragment))
+            refs.append(ref)
+            self.currentOffset += self.trailer.objectRefSize
+            i += 1
+        return refs
+    
+    def readArray(self, count):
+        """
+        Private method to read an Array object.
+        
+        @param count number of array elements (integer)
+        @return list of unpickled objects
+        """
+        result = []
+        values = self.readRefs(count)
+        i = 0
+        while i < len(values):
+            self.setCurrentOffsetToObjectNumber(values[i])
+            value = self.readObject()
+            result.append(value)
+            i += 1
+        return result
+    
+    def readDict(self, count):
+        """
+        Private method to read a Dictionary object.
+        
+        @param count number of dictionary elements (integer)
+        @return dictionary of unpickled objects
+        """
+        result = {}
+        keys = self.readRefs(count)
+        values = self.readRefs(count)
+        i = 0
+        while i < len(keys):
+            self.setCurrentOffsetToObjectNumber(keys[i])
+            key = self.readObject()
+            self.setCurrentOffsetToObjectNumber(values[i])
+            value = self.readObject()
+            result[key] = value
+            i += 1
+        return result
+    
+    def readAsciiString(self, length):
+        """
+        Private method to read an ASCII encoded string.
+        
+        @param length length of the string (integer)
+        @return ASCII encoded string
+        """
+        result = str(unpack("!{0}s".format(length),
+            self.contents[self.currentOffset:self.currentOffset+length])[0],
+            encoding="ascii")
+        self.currentOffset += length
+        return result
+    
+    def readUnicode(self, length):
+        """
+        Private method to read an Unicode encoded string.
+        
+        @param length length of the string (integer)
+        @return unicode encoded string
+        """
+        actual_length = length*2
+        data = self.contents[self.currentOffset:self.currentOffset+actual_length]
+        # unpack not needed?!! data = unpack(">%ds" % (actual_length), data)[0]
+        self.currentOffset += actual_length
+        return data.decode('utf_16_be')
+    
+    def readDate(self):
+        """
+        Private method to read a date.
+        
+        @return date object (datetime.datetime)
+        """
+        global apple_reference_date_offset
+        result = unpack(">d", self.contents[self.currentOffset:self.currentOffset+8])[0]
+        result = datetime.datetime.utcfromtimestamp(result + apple_reference_date_offset)
+        self.currentOffset += 8
+        return result
+    
+    def readData(self, length):
+        """
+        Private method to read some bytes.
+        
+        @param length number of bytes to read (integer)
+        @return Data object
+        """
+        result = self.contents[self.currentOffset:self.currentOffset+length]
+        self.currentOffset += length
+        return Data(result)
+    
+    def readUid(self, length):
+        """
+        Private method to read a UID.
+        
+        @param length length of the UID (integer)
+        @return Uid object
+        """
+        return Uid(self.readInteger(length+1))
+    
+    def getSizedInteger(self, data, bytes):
+        """
+        Private method to read an integer of a specific size.
+        
+        @param data data to extract the integer from (bytes)
+        @param bytes length of the integer (integer)
+        """
+        result = 0
+        # 1, 2, and 4 byte integers are unsigned
+        if bytes == 1:
+            result = unpack('>B', data)[0]
+        elif bytes == 2:
+            result = unpack('>H', data)[0]
+        elif bytes == 4:
+            result = unpack('>L', data)[0]
+        elif bytes == 8:
+            result = unpack('>q', data)[0]
+        else:
+            raise InvalidPlistException("Encountered integer longer than 8 bytes.")
+        return result
+
+class HashableWrapper(object):
+    """
+    Class wrapping a hashable value.
+    """
+    def __init__(self, value):
+        self.value = value
+    def __repr__(self):
+        return "<HashableWrapper: %s>" % [self.value]
+
+class BoolWrapper(object):
+    """
+    Class wrapping a boolean value.
+    """
+    def __init__(self, value):
+        self.value = value
+    def __repr__(self):
+        return "<BoolWrapper: %s>" % self.value
+
+class PlistWriter(object):
+    """
+    Class implementing the plist writer.
+    """
+    header = b'bplist00bybiplist1.0'
+    file = None
+    byteCounts = None
+    trailer = None
+    computedUniques = None
+    writtenReferences = None
+    referencePositions = None
+    wrappedTrue = None
+    wrappedFalse = None
+    
+    def __init__(self, file):
+        """
+        Constructor
+        
+        @param file file to write the plist data to (file object)
+        """
+        self.reset()
+        self.file = file
+        self.wrappedTrue = BoolWrapper(True)
+        self.wrappedFalse = BoolWrapper(False)
+
+    def reset(self):
+        """
+        Private method to reset the instance object.
+        """
+        self.byteCounts = PlistByteCounts(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
+        self.trailer = PlistTrailer(0, 0, 0, 0, 0)
+        
+        # A set of all the uniques which have been computed.
+        self.computedUniques = set()
+        # A list of all the uniques which have been written.
+        self.writtenReferences = {}
+        # A dict of the positions of the written uniques.
+        self.referencePositions = {}
+        
+    def positionOfObjectReference(self, obj):
+        """
+        Private method to get the position of an object.
+        
+        If the given object has been written already, return its
+        position in the offset table. Otherwise, return None.
+        
+        @return position of the object (integer)
+        """
+        return self.writtenReferences.get(obj)
+        
+    def writeRoot(self, root):
+        """
+        Public method to write an object to a plist file.
+        
+        Strategy is:
+        <ul>
+        <li>write header</li>
+        <li>wrap root object so everything is hashable</li>
+        <li>compute size of objects which will be written
+          <ul>
+          <li>need to do this in order to know how large the object refs
+            will be in the list/dict/set reference lists</li>
+        </ul></li>
+        <li>write objects
+          <ul>
+          <li>keep objects in writtenReferences</li>
+          <li>keep positions of object references in referencePositions</li>
+          <li>write object references with the length computed previously</li>
+        </ul></li>
+        <li>computer object reference length</li>
+        <li>write object reference positions</li>
+        <li>write trailer</li>
+        </ul>
+        
+        @param root reference to the object to be written
+        """
+        output = self.header
+        wrapped_root = self.wrapRoot(root)
+        should_reference_root = True#not isinstance(wrapped_root, HashableWrapper)
+        self.computeOffsets(wrapped_root, asReference=should_reference_root, isRoot=True)
+        self.trailer = self.trailer._replace(
+            **{'objectRefSize':self.intSize(len(self.computedUniques))})
+        (_, output) = self.writeObjectReference(wrapped_root, output)
+        output = self.writeObject(wrapped_root, output, setReferencePosition=True)
+        
+        # output size at this point is an upper bound on how big the
+        # object reference offsets need to be.
+        self.trailer = self.trailer._replace(**{
+            'offsetSize':self.intSize(len(output)),
+            'offsetCount':len(self.computedUniques),
+            'offsetTableOffset':len(output),
+            'topLevelObjectNumber':0
+            })
+        
+        output = self.writeOffsetTable(output)
+        output += pack('!xxxxxxBBQQQ', *self.trailer)
+        self.file.write(output)
+    
+    def wrapRoot(self, root):
+        """
+        Private method to generate object wrappers.
+        
+        @param root object to be wrapped
+        @return wrapped object
+        """
+        if isinstance(root, bool):
+            if root is True:
+                return self.wrappedTrue
+            else:
+                return self.wrappedFalse
+        elif isinstance(root, set):
+            n = set()
+            for value in root:
+                n.add(self.wrapRoot(value))
+            return HashableWrapper(n)
+        elif isinstance(root, dict):
+            n = {}
+            for key, value in root.items():
+                n[self.wrapRoot(key)] = self.wrapRoot(value)
+            return HashableWrapper(n)
+        elif isinstance(root, list):
+            n = []
+            for value in root:
+                n.append(self.wrapRoot(value))
+            return HashableWrapper(n)
+        elif isinstance(root, tuple):
+            n = tuple([self.wrapRoot(value) for value in root])
+            return HashableWrapper(n)
+        else:
+            return root
+
+    def incrementByteCount(self, field, incr=1):
+        self.byteCounts = self.byteCounts._replace(
+            **{field:self.byteCounts.__getattribute__(field) + incr})
+
+    def computeOffsets(self, obj, asReference=False, isRoot=False):
+        def check_key(key):
+            if key is None:
+                raise InvalidPlistException('Dictionary keys cannot be null in plists.')
+            elif isinstance(key, Data):
+                raise InvalidPlistException('Data cannot be dictionary keys in plists.')
+            elif not isinstance(key, str):
+                raise InvalidPlistException('Keys must be strings.')
+        
+        def proc_size(size):
+            if size > 0b1110:
+                size += self.intSize(size)
+            return size
+        # If this should be a reference, then we keep a record of it in the
+        # uniques table.
+        if asReference:
+            if obj in self.computedUniques:
+                return
+            else:
+                self.computedUniques.add(obj)
+        
+        if obj is None:
+            self.incrementByteCount('nullBytes')
+        elif isinstance(obj, BoolWrapper):
+            self.incrementByteCount('boolBytes')
+        elif isinstance(obj, Uid):
+            size = self.intSize(obj)
+            self.incrementByteCount('uidBytes', incr=1+size)
+        elif isinstance(obj, int):
+            size = self.intSize(obj)
+            self.incrementByteCount('intBytes', incr=1+size)
+        elif isinstance(obj, (float)):
+            size = self.realSize(obj)
+            self.incrementByteCount('realBytes', incr=1+size)
+        elif isinstance(obj, datetime.datetime):    
+            self.incrementByteCount('dateBytes', incr=2)
+        elif isinstance(obj, Data):
+            size = proc_size(len(obj))
+            self.incrementByteCount('dataBytes', incr=1+size)
+        elif isinstance(obj, str):
+            size = proc_size(len(obj))
+            self.incrementByteCount('stringBytes', incr=1+size)
+        elif isinstance(obj, HashableWrapper):
+            obj = obj.value
+            if isinstance(obj, set):
+                size = proc_size(len(obj))
+                self.incrementByteCount('setBytes', incr=1+size)
+                for value in obj:
+                    self.computeOffsets(value, asReference=True)
+            elif isinstance(obj, (list, tuple)):
+                size = proc_size(len(obj))
+                self.incrementByteCount('arrayBytes', incr=1+size)
+                for value in obj:
+                    self.computeOffsets(value, asReference=True)
+            elif isinstance(obj, dict):
+                size = proc_size(len(obj))
+                self.incrementByteCount('dictBytes', incr=1+size)
+                for key, value in obj.items():
+                    check_key(key)
+                    self.computeOffsets(key, asReference=True)
+                    self.computeOffsets(value, asReference=True)
+        else:
+            raise InvalidPlistException("Unknown object type.")
+
+    def writeObjectReference(self, obj, output):
+        """
+        Private method to write an object reference.
+        
+        Tries to write an object reference, adding it to the references
+        table. Does not write the actual object bytes or set the reference
+        position. Returns a tuple of whether the object was a new reference
+        (True if it was, False if it already was in the reference table)
+        and the new output.
+        
+        @param obj object to be written
+        @param output output stream to append the object to
+        @return flag indicating a new reference and the new output
+        """
+        position = self.positionOfObjectReference(obj)
+        if position is None:
+            self.writtenReferences[obj] = len(self.writtenReferences)
+            output += self.binaryInt(len(self.writtenReferences) - 1,
+                                      bytes=self.trailer.objectRefSize)
+            return (True, output)
+        else:
+            output += self.binaryInt(position, bytes=self.trailer.objectRefSize)
+            return (False, output)
+
+    def writeObject(self, obj, output, setReferencePosition=False):
+        """
+        Private method to serialize the given object to the output.
+        
+        @param obj object to be serialized
+        @param output output to be serialized to (bytes)
+        @param setReferencePosition flag indicating, that the reference
+            position the object was written to shall be recorded (boolean)
+        @return new output
+        """
+        def proc_variable_length(format, length):
+            result = ''
+            if length > 0b1110:
+                result += pack('!B', (format << 4) | 0b1111)
+                result = self.writeObject(length, result)
+            else:
+                result += pack('!B', (format << 4) | length)
+            return result
+        
+        if setReferencePosition:
+            self.referencePositions[obj] = len(output)
+        
+        if obj is None:
+            output += pack('!B', 0b00000000)
+        elif isinstance(obj, BoolWrapper):
+            if obj.value is False:
+                output += pack('!B', 0b00001000)
+            else:
+                output += pack('!B', 0b00001001)
+        elif isinstance(obj, Uid):
+            size = self.intSize(obj)
+            output += pack('!B', (0b1000 << 4) | size - 1)
+            output += self.binaryInt(obj)
+        elif isinstance(obj, int):
+            bytes = self.intSize(obj)
+            root = math.log(bytes, 2)
+            output += pack('!B', (0b0001 << 4) | int(root))
+            output += self.binaryInt(obj)
+        elif isinstance(obj, float):
+            # just use doubles
+            output += pack('!B', (0b0010 << 4) | 3)
+            output += self.binaryReal(obj)
+        elif isinstance(obj, datetime.datetime):
+            timestamp = calendar.timegm(obj.utctimetuple())
+            timestamp -= apple_reference_date_offset
+            output += pack('!B', 0b00110011)
+            output += pack('!d', float(timestamp))
+        elif isinstance(obj, Data):
+            output += proc_variable_length(0b0100, len(obj))
+            output += obj
+        elif isinstance(obj, str):
+            # Python 3 uses unicode strings only
+            bytes = obj.encode('utf_16_be')
+            output += proc_variable_length(0b0110, len(bytes)/2)
+            output += bytes
+        elif isinstance(obj, HashableWrapper):
+            obj = obj.value
+            if isinstance(obj, (set, list, tuple)):
+                if isinstance(obj, set):
+                    output += proc_variable_length(0b1100, len(obj))
+                else:
+                    output += proc_variable_length(0b1010, len(obj))
+            
+                objectsToWrite = []
+                for objRef in obj:
+                    (isNew, output) = self.writeObjectReference(objRef, output)
+                    if isNew:
+                        objectsToWrite.append(objRef)
+                for objRef in objectsToWrite:
+                    output = self.writeObject(objRef, output, setReferencePosition=True)
+            elif isinstance(obj, dict):
+                output += proc_variable_length(0b1101, len(obj))
+                keys = []
+                values = []
+                objectsToWrite = []
+                for key, value in obj.items():
+                    keys.append(key)
+                    values.append(value)
+                for key in keys:
+                    (isNew, output) = self.writeObjectReference(key, output)
+                    if isNew:
+                        objectsToWrite.append(key)
+                for value in values:
+                    (isNew, output) = self.writeObjectReference(value, output)
+                    if isNew:
+                        objectsToWrite.append(value)
+                for objRef in objectsToWrite:
+                    output = self.writeObject(objRef, output, setReferencePosition=True)
+        return output
+    
+    def writeOffsetTable(self, output):
+        """
+        Private method to write all of the object reference offsets.
+        
+        @param output current output (bytes)
+        @return new output (bytes)
+        """
+        all_positions = []
+        writtenReferences = list(self.writtenReferences.items())
+        writtenReferences.sort(key=lambda x: x[1])
+        for obj,order in writtenReferences:
+            position = self.referencePositions.get(obj)
+            if position is None:
+                raise InvalidPlistException(
+                    "Error while writing offsets table. Object not found. {0}"
+                    .format(obj))
+            output += self.binaryInt(position, self.trailer.offsetSize)
+            all_positions.append(position)
+        return output
+    
+    def binaryReal(self, obj):
+        """
+        Private method to pack a real object.
+        
+        @param obj real to be packed
+        @return serialized object (bytes)
+        """
+        # just use doubles
+        result = pack('>d', obj)
+        return result
+    
+    def binaryInt(self, obj, bytes=None):
+        """
+        Private method to pack an integer object.
+        
+        @param obj integer to be packed
+        @param bytes length the integer should be packed into (integer)
+        @return serialized object (bytes)
+        """
+        result = ''
+        if bytes is None:
+            bytes = self.intSize(obj)
+        if bytes == 1:
+            result += pack('>B', obj)
+        elif bytes == 2:
+            result += pack('>H', obj)
+        elif bytes == 4:
+            result += pack('>L', obj)
+        elif bytes == 8:
+            result += pack('>q', obj)
+        else:
+            raise InvalidPlistException(
+                "Core Foundation can't handle integers with size greater than 8 bytes.")
+        return result
+    
+    def intSize(self, obj):
+        """
+        Private method to determine the number of bytes necessary to store the
+        given integer.
+        
+        @param obj integer object
+        @return number of bytes required (integer)
+        """
+        # SIGNED
+        if obj < 0: # Signed integer, always 8 bytes
+            return 8
+        # UNSIGNED
+        elif obj <= 0xFF: # 1 byte
+            return 1
+        elif obj <= 0xFFFF: # 2 bytes
+            return 2
+        elif obj <= 0xFFFFFFFF: # 4 bytes
+            return 4
+        # SIGNED
+        # 0x7FFFFFFFFFFFFFFF is the max.
+        elif obj <= 0x7FFFFFFFFFFFFFFF: # 8 bytes
+            return 8
+        else:
+            raise InvalidPlistException(
+                "Core Foundation can't handle integers with size greater than 8 bytes.")
+    
+    def realSize(self, obj):
+        """
+        Private method to determine the number of bytes necessary to store the
+        given real.
+        
+        @param obj real object
+        @return number of bytes required (integer)
+        """
+        return 8

eric ide

mercurial