Utilities/binplistlib.py

changeset 1720
201622cf8a01
child 1965
96f5a76e1845
equal deleted inserted replaced
1719:c65aefefa2ff 1720:201622cf8a01
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2012 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a library for reading and writing binary property list files.
8
9 Binary Property List (plist) files provide a faster and smaller serialization
10 format for property lists on OS X. This is a library for generating binary
11 plists which can be read by OS X, iOS, or other clients.
12
13 The API models the plistlib API, and will call through to plistlib when
14 XML serialization or deserialization is required.
15
16 To generate plists with UID values, wrap the values with the Uid object. The
17 value must be an int.
18
19 To generate plists with NSData/CFData values, wrap the values with the
20 Data object. The value must be a bytes object.
21
22 Date values can only be datetime.datetime objects.
23
24 The exceptions InvalidPlistException and NotBinaryPlistException may be
25 thrown to indicate that the data cannot be serialized or deserialized as
26 a binary plist.
27
28 Plist generation example:
29 <pre>
30 from binplistlib import *
31 from datetime import datetime
32 plist = {'aKey':'aValue',
33 '0':1.322,
34 'now':datetime.now(),
35 'list':[1,2,3],
36 'tuple':('a','b','c')
37 }
38 try:
39 writePlist(plist, "example.plist")
40 except (InvalidPlistException, NotBinaryPlistException) as e:
41 print("Something bad happened:", e)
42 </pre>
43 Plist parsing example:
44 <pre>
45 from binplistlib import *
46 try:
47 plist = readPlist("example.plist")
48 print(plist)
49 except (InvalidPlistException, NotBinaryPlistException) as e:
50 print("Not a plist:", e)
51 </pre>
52 """
53
54 #
55 # Ported from the Python 2 biplist.py script.
56 #
57 # Original License:
58 #
59 # Copyright (c) 2010, Andrew Wooster
60 # All rights reserved.
61 #
62 # Redistribution and use in source and binary forms, with or without
63 # modification, are permitted provided that the following conditions are met:
64 #
65 # * Redistributions of source code must retain the above copyright notice,
66 # this list of conditions and the following disclaimer.
67 # * Redistributions in binary form must reproduce the above copyright
68 # notice, this list of conditions and the following disclaimer in the
69 # documentation and/or other materials provided with the distribution.
70 # * Neither the name of biplist nor the names of its contributors may be
71 # used to endorse or promote products derived from this software without
72 # specific prior written permission.
73 #
74 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
75 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
76 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
77 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
78 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
79 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
80 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
81 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
82 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
83 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
84 #
85
86 from collections import namedtuple
87 from io import BytesIO
88 import calendar
89 import datetime
90 import math
91 import plistlib
92 from struct import pack, unpack
93
94 __all__ = [
95 'Uid', 'Data', 'readPlist', 'writePlist', 'readPlistFromBytes',
96 'writePlistToBytes', 'InvalidPlistException', 'NotBinaryPlistException'
97 ]
98
99 apple_reference_date_offset = 978307200
100
101
102 class Uid(int):
103 """
104 Class implementing a wrapper around integers for representing UID values.
105
106 This is used in keyed archiving.
107 """
108 def __repr__(self):
109 return "Uid(%d)" % self
110
111
112 class Data(bytes):
113 """
114 Class implementing a wrapper around bytes types for representing Data values.
115 """
116 pass
117
118
119 class InvalidPlistException(Exception):
120 """
121 Exception raised when the plist is incorrectly formatted.
122 """
123 pass
124
125
126 class NotBinaryPlistException(Exception):
127 """
128 Exception raised when a binary plist was expected but not encountered.
129 """
130 pass
131
132
133 def readPlist(pathOrFile):
134 """
135 Module function to read a plist file.
136
137 @param pathOrFile name of the plist file (string) or an open file (file object)
138 @return reference to the read object
139 @exception InvalidPlistException raised to signal an invalid plist file
140 """
141 didOpen = False
142 result = None
143 if isinstance(pathOrFile, str):
144 pathOrFile = open(pathOrFile, 'rb')
145 didOpen = True
146 try:
147 reader = PlistReader(pathOrFile)
148 result = reader.parse()
149 except NotBinaryPlistException as e:
150 try:
151 pathOrFile.seek(0)
152 result = plistlib.readPlist(pathOrFile)
153 except Exception as e:
154 raise InvalidPlistException(e)
155 if didOpen:
156 pathOrFile.close()
157 return result
158
159 def writePlist(rootObject, pathOrFile, binary=True):
160 """
161 Module function to write a plist file.
162
163 @param rootObject reference to the object to be written
164 @param pathOrFile name of the plist file (string) or an open file (file object)
165 @param binary flag indicating the generation of a binary plist file (boolean)
166 """
167 if not binary:
168 plistlib.writePlist(rootObject, pathOrFile)
169 return
170 else:
171 didOpen = False
172 if isinstance(pathOrFile, str):
173 pathOrFile = open(pathOrFile, 'wb')
174 didOpen = True
175 writer = PlistWriter(pathOrFile)
176 writer.writeRoot(rootObject)
177 if didOpen:
178 pathOrFile.close()
179 return
180
181 def readPlistFromBytes(data):
182 """
183 Module function to read from a plist bytes object.
184
185 @param data plist data (bytes)
186 @return reference to the read object
187 @exception InvalidPlistException raised to signal an invalid plist file
188 """
189 return readPlist(BytesIO(data))
190
191 def writePlistToBytes(rootObject, binary=True):
192 """
193 Module function to write a plist bytes object.
194
195 @param rootObject reference to the object to be written
196 @param binary flag indicating the generation of a binary plist bytes object (boolean)
197 """
198 if not binary:
199 return plistlib.writePlistToBytes(rootObject)
200 else:
201 io = BytesIO()
202 writer = PlistWriter(io)
203 writer.writeRoot(rootObject)
204 return io.getvalue()
205
206 def is_stream_binary_plist(stream):
207 """
208 Module function to check, if the stream is a binary plist.
209
210 @param stream plist stream (file object)
211 @return flag indicating a binary plist (boolean)
212 """
213 stream.seek(0)
214 header = stream.read(7)
215 if header == b'bplist0':
216 return True
217 else:
218 return False
219
220 PlistTrailer = namedtuple('PlistTrailer',
221 'offsetSize, objectRefSize, offsetCount, topLevelObjectNumber, offsetTableOffset')
222 PlistByteCounts = namedtuple('PlistByteCounts',
223 'nullBytes, boolBytes, intBytes, realBytes, dateBytes, dataBytes, stringBytes, '
224 'uidBytes, arrayBytes, setBytes, dictBytes')
225
226 class PlistReader(object):
227 """
228 Class implementing the plist reader.
229 """
230 file = None
231 contents = b''
232 offsets = None
233 trailer = None
234 currentOffset = 0
235
236 def __init__(self, fileOrStream):
237 """
238 Constructor
239
240 @param fileOrStream open file containing the plist data (file object)
241 """
242 self.reset()
243 self.file = fileOrStream
244
245 def parse(self):
246 """
247 Public method to parse the plist data.
248
249 @return unpickled object
250 """
251 return self.readRoot()
252
253 def reset(self):
254 """
255 Private method to reset the instance object.
256 """
257 self.trailer = None
258 self.contents = b''
259 self.offsets = []
260 self.currentOffset = 0
261
262 def readRoot(self):
263 """
264 Private method to read the root object.
265
266 @return unpickled object
267 """
268 result = None
269 self.reset()
270 # Get the header, make sure it's a valid file.
271 if not is_stream_binary_plist(self.file):
272 raise NotBinaryPlistException()
273 self.file.seek(0)
274 self.contents = self.file.read()
275 if len(self.contents) < 32:
276 raise InvalidPlistException("File is too short.")
277 trailerContents = self.contents[-32:]
278 try:
279 self.trailer = PlistTrailer._make(unpack("!xxxxxxBBQQQ", trailerContents))
280 offset_size = self.trailer.offsetSize * self.trailer.offsetCount
281 offset = self.trailer.offsetTableOffset
282 offset_contents = self.contents[offset:offset+offset_size]
283 offset_i = 0
284 while offset_i < self.trailer.offsetCount:
285 begin = self.trailer.offsetSize*offset_i
286 tmp_contents = offset_contents[begin:begin+self.trailer.offsetSize]
287 tmp_sized = self.getSizedInteger(tmp_contents, self.trailer.offsetSize)
288 self.offsets.append(tmp_sized)
289 offset_i += 1
290 self.setCurrentOffsetToObjectNumber(self.trailer.topLevelObjectNumber)
291 result = self.readObject()
292 except TypeError as e:
293 raise InvalidPlistException(e)
294 return result
295
296 def setCurrentOffsetToObjectNumber(self, objectNumber):
297 """
298 Private method to set the current offset.
299
300 @param objectNumber number of the object (integer)
301 """
302 self.currentOffset = self.offsets[objectNumber]
303
304 def readObject(self):
305 """
306 Private method to read the object data.
307
308 @return unpickled object
309 """
310 result = None
311 tmp_byte = self.contents[self.currentOffset:self.currentOffset+1]
312 marker_byte = unpack("!B", tmp_byte)[0]
313 format = (marker_byte >> 4) & 0x0f
314 extra = marker_byte & 0x0f
315 self.currentOffset += 1
316
317 def proc_extra(extra):
318 if extra == 0b1111:
319 #self.currentOffset += 1
320 extra = self.readObject()
321 return extra
322
323 # bool, null, or fill byte
324 if format == 0b0000:
325 if extra == 0b0000:
326 result = None
327 elif extra == 0b1000:
328 result = False
329 elif extra == 0b1001:
330 result = True
331 elif extra == 0b1111:
332 pass # fill byte
333 else:
334 raise InvalidPlistException(
335 "Invalid object found at offset: {0}".format(self.currentOffset - 1))
336 # int
337 elif format == 0b0001:
338 extra = proc_extra(extra)
339 result = self.readInteger(pow(2, extra))
340 # real
341 elif format == 0b0010:
342 extra = proc_extra(extra)
343 result = self.readReal(extra)
344 # date
345 elif format == 0b0011 and extra == 0b0011:
346 result = self.readDate()
347 # data
348 elif format == 0b0100:
349 extra = proc_extra(extra)
350 result = self.readData(extra)
351 # ascii string
352 elif format == 0b0101:
353 extra = proc_extra(extra)
354 result = self.readAsciiString(extra)
355 # Unicode string
356 elif format == 0b0110:
357 extra = proc_extra(extra)
358 result = self.readUnicode(extra)
359 # uid
360 elif format == 0b1000:
361 result = self.readUid(extra)
362 # array
363 elif format == 0b1010:
364 extra = proc_extra(extra)
365 result = self.readArray(extra)
366 # set
367 elif format == 0b1100:
368 extra = proc_extra(extra)
369 result = set(self.readArray(extra))
370 # dict
371 elif format == 0b1101:
372 extra = proc_extra(extra)
373 result = self.readDict(extra)
374 else:
375 raise InvalidPlistException(
376 "Invalid object found: {{format: {0}, extra: {1}}}".format(
377 bin(format), bin(extra)))
378 return result
379
380 def readInteger(self, bytes):
381 """
382 Private method to read an Integer object.
383
384 @param bytes length of the object (integer)
385 @return integer object
386 """
387 result = 0
388 original_offset = self.currentOffset
389 data = self.contents[self.currentOffset:self.currentOffset+bytes]
390 result = self.getSizedInteger(data, bytes)
391 self.currentOffset = original_offset + bytes
392 return result
393
394 def readReal(self, length):
395 """
396 Private method to read a Real object.
397
398 @param length length of the object (integer)
399 @return float object
400 """
401 result = 0.0
402 to_read = pow(2, length)
403 data = self.contents[self.currentOffset:self.currentOffset+to_read]
404 if length == 2: # 4 bytes
405 result = unpack('>f', data)[0]
406 elif length == 3: # 8 bytes
407 result = unpack('>d', data)[0]
408 else:
409 raise InvalidPlistException(
410 "Unknown real of length {0} bytes".format(to_read))
411 return result
412
413 def readRefs(self, count):
414 """
415 Private method to read References.
416
417 @param count amount of the references (integer)
418 @return list of references (list of integers)
419 """
420 refs = []
421 i = 0
422 while i < count:
423 fragment = self.contents[
424 self.currentOffset:self.currentOffset+self.trailer.objectRefSize]
425 ref = self.getSizedInteger(fragment, len(fragment))
426 refs.append(ref)
427 self.currentOffset += self.trailer.objectRefSize
428 i += 1
429 return refs
430
431 def readArray(self, count):
432 """
433 Private method to read an Array object.
434
435 @param count number of array elements (integer)
436 @return list of unpickled objects
437 """
438 result = []
439 values = self.readRefs(count)
440 i = 0
441 while i < len(values):
442 self.setCurrentOffsetToObjectNumber(values[i])
443 value = self.readObject()
444 result.append(value)
445 i += 1
446 return result
447
448 def readDict(self, count):
449 """
450 Private method to read a Dictionary object.
451
452 @param count number of dictionary elements (integer)
453 @return dictionary of unpickled objects
454 """
455 result = {}
456 keys = self.readRefs(count)
457 values = self.readRefs(count)
458 i = 0
459 while i < len(keys):
460 self.setCurrentOffsetToObjectNumber(keys[i])
461 key = self.readObject()
462 self.setCurrentOffsetToObjectNumber(values[i])
463 value = self.readObject()
464 result[key] = value
465 i += 1
466 return result
467
468 def readAsciiString(self, length):
469 """
470 Private method to read an ASCII encoded string.
471
472 @param length length of the string (integer)
473 @return ASCII encoded string
474 """
475 result = str(unpack("!{0}s".format(length),
476 self.contents[self.currentOffset:self.currentOffset+length])[0],
477 encoding="ascii")
478 self.currentOffset += length
479 return result
480
481 def readUnicode(self, length):
482 """
483 Private method to read an Unicode encoded string.
484
485 @param length length of the string (integer)
486 @return unicode encoded string
487 """
488 actual_length = length*2
489 data = self.contents[self.currentOffset:self.currentOffset+actual_length]
490 # unpack not needed?!! data = unpack(">%ds" % (actual_length), data)[0]
491 self.currentOffset += actual_length
492 return data.decode('utf_16_be')
493
494 def readDate(self):
495 """
496 Private method to read a date.
497
498 @return date object (datetime.datetime)
499 """
500 global apple_reference_date_offset
501 result = unpack(">d", self.contents[self.currentOffset:self.currentOffset+8])[0]
502 result = datetime.datetime.utcfromtimestamp(result + apple_reference_date_offset)
503 self.currentOffset += 8
504 return result
505
506 def readData(self, length):
507 """
508 Private method to read some bytes.
509
510 @param length number of bytes to read (integer)
511 @return Data object
512 """
513 result = self.contents[self.currentOffset:self.currentOffset+length]
514 self.currentOffset += length
515 return Data(result)
516
517 def readUid(self, length):
518 """
519 Private method to read a UID.
520
521 @param length length of the UID (integer)
522 @return Uid object
523 """
524 return Uid(self.readInteger(length+1))
525
526 def getSizedInteger(self, data, bytes):
527 """
528 Private method to read an integer of a specific size.
529
530 @param data data to extract the integer from (bytes)
531 @param bytes length of the integer (integer)
532 """
533 result = 0
534 # 1, 2, and 4 byte integers are unsigned
535 if bytes == 1:
536 result = unpack('>B', data)[0]
537 elif bytes == 2:
538 result = unpack('>H', data)[0]
539 elif bytes == 4:
540 result = unpack('>L', data)[0]
541 elif bytes == 8:
542 result = unpack('>q', data)[0]
543 else:
544 raise InvalidPlistException("Encountered integer longer than 8 bytes.")
545 return result
546
547 class HashableWrapper(object):
548 """
549 Class wrapping a hashable value.
550 """
551 def __init__(self, value):
552 self.value = value
553 def __repr__(self):
554 return "<HashableWrapper: %s>" % [self.value]
555
556 class BoolWrapper(object):
557 """
558 Class wrapping a boolean value.
559 """
560 def __init__(self, value):
561 self.value = value
562 def __repr__(self):
563 return "<BoolWrapper: %s>" % self.value
564
565 class PlistWriter(object):
566 """
567 Class implementing the plist writer.
568 """
569 header = b'bplist00bybiplist1.0'
570 file = None
571 byteCounts = None
572 trailer = None
573 computedUniques = None
574 writtenReferences = None
575 referencePositions = None
576 wrappedTrue = None
577 wrappedFalse = None
578
579 def __init__(self, file):
580 """
581 Constructor
582
583 @param file file to write the plist data to (file object)
584 """
585 self.reset()
586 self.file = file
587 self.wrappedTrue = BoolWrapper(True)
588 self.wrappedFalse = BoolWrapper(False)
589
590 def reset(self):
591 """
592 Private method to reset the instance object.
593 """
594 self.byteCounts = PlistByteCounts(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
595 self.trailer = PlistTrailer(0, 0, 0, 0, 0)
596
597 # A set of all the uniques which have been computed.
598 self.computedUniques = set()
599 # A list of all the uniques which have been written.
600 self.writtenReferences = {}
601 # A dict of the positions of the written uniques.
602 self.referencePositions = {}
603
604 def positionOfObjectReference(self, obj):
605 """
606 Private method to get the position of an object.
607
608 If the given object has been written already, return its
609 position in the offset table. Otherwise, return None.
610
611 @return position of the object (integer)
612 """
613 return self.writtenReferences.get(obj)
614
615 def writeRoot(self, root):
616 """
617 Public method to write an object to a plist file.
618
619 Strategy is:
620 <ul>
621 <li>write header</li>
622 <li>wrap root object so everything is hashable</li>
623 <li>compute size of objects which will be written
624 <ul>
625 <li>need to do this in order to know how large the object refs
626 will be in the list/dict/set reference lists</li>
627 </ul></li>
628 <li>write objects
629 <ul>
630 <li>keep objects in writtenReferences</li>
631 <li>keep positions of object references in referencePositions</li>
632 <li>write object references with the length computed previously</li>
633 </ul></li>
634 <li>computer object reference length</li>
635 <li>write object reference positions</li>
636 <li>write trailer</li>
637 </ul>
638
639 @param root reference to the object to be written
640 """
641 output = self.header
642 wrapped_root = self.wrapRoot(root)
643 should_reference_root = True#not isinstance(wrapped_root, HashableWrapper)
644 self.computeOffsets(wrapped_root, asReference=should_reference_root, isRoot=True)
645 self.trailer = self.trailer._replace(
646 **{'objectRefSize':self.intSize(len(self.computedUniques))})
647 (_, output) = self.writeObjectReference(wrapped_root, output)
648 output = self.writeObject(wrapped_root, output, setReferencePosition=True)
649
650 # output size at this point is an upper bound on how big the
651 # object reference offsets need to be.
652 self.trailer = self.trailer._replace(**{
653 'offsetSize':self.intSize(len(output)),
654 'offsetCount':len(self.computedUniques),
655 'offsetTableOffset':len(output),
656 'topLevelObjectNumber':0
657 })
658
659 output = self.writeOffsetTable(output)
660 output += pack('!xxxxxxBBQQQ', *self.trailer)
661 self.file.write(output)
662
663 def wrapRoot(self, root):
664 """
665 Private method to generate object wrappers.
666
667 @param root object to be wrapped
668 @return wrapped object
669 """
670 if isinstance(root, bool):
671 if root is True:
672 return self.wrappedTrue
673 else:
674 return self.wrappedFalse
675 elif isinstance(root, set):
676 n = set()
677 for value in root:
678 n.add(self.wrapRoot(value))
679 return HashableWrapper(n)
680 elif isinstance(root, dict):
681 n = {}
682 for key, value in root.items():
683 n[self.wrapRoot(key)] = self.wrapRoot(value)
684 return HashableWrapper(n)
685 elif isinstance(root, list):
686 n = []
687 for value in root:
688 n.append(self.wrapRoot(value))
689 return HashableWrapper(n)
690 elif isinstance(root, tuple):
691 n = tuple([self.wrapRoot(value) for value in root])
692 return HashableWrapper(n)
693 else:
694 return root
695
696 def incrementByteCount(self, field, incr=1):
697 self.byteCounts = self.byteCounts._replace(
698 **{field:self.byteCounts.__getattribute__(field) + incr})
699
700 def computeOffsets(self, obj, asReference=False, isRoot=False):
701 def check_key(key):
702 if key is None:
703 raise InvalidPlistException('Dictionary keys cannot be null in plists.')
704 elif isinstance(key, Data):
705 raise InvalidPlistException('Data cannot be dictionary keys in plists.')
706 elif not isinstance(key, str):
707 raise InvalidPlistException('Keys must be strings.')
708
709 def proc_size(size):
710 if size > 0b1110:
711 size += self.intSize(size)
712 return size
713 # If this should be a reference, then we keep a record of it in the
714 # uniques table.
715 if asReference:
716 if obj in self.computedUniques:
717 return
718 else:
719 self.computedUniques.add(obj)
720
721 if obj is None:
722 self.incrementByteCount('nullBytes')
723 elif isinstance(obj, BoolWrapper):
724 self.incrementByteCount('boolBytes')
725 elif isinstance(obj, Uid):
726 size = self.intSize(obj)
727 self.incrementByteCount('uidBytes', incr=1+size)
728 elif isinstance(obj, int):
729 size = self.intSize(obj)
730 self.incrementByteCount('intBytes', incr=1+size)
731 elif isinstance(obj, (float)):
732 size = self.realSize(obj)
733 self.incrementByteCount('realBytes', incr=1+size)
734 elif isinstance(obj, datetime.datetime):
735 self.incrementByteCount('dateBytes', incr=2)
736 elif isinstance(obj, Data):
737 size = proc_size(len(obj))
738 self.incrementByteCount('dataBytes', incr=1+size)
739 elif isinstance(obj, str):
740 size = proc_size(len(obj))
741 self.incrementByteCount('stringBytes', incr=1+size)
742 elif isinstance(obj, HashableWrapper):
743 obj = obj.value
744 if isinstance(obj, set):
745 size = proc_size(len(obj))
746 self.incrementByteCount('setBytes', incr=1+size)
747 for value in obj:
748 self.computeOffsets(value, asReference=True)
749 elif isinstance(obj, (list, tuple)):
750 size = proc_size(len(obj))
751 self.incrementByteCount('arrayBytes', incr=1+size)
752 for value in obj:
753 self.computeOffsets(value, asReference=True)
754 elif isinstance(obj, dict):
755 size = proc_size(len(obj))
756 self.incrementByteCount('dictBytes', incr=1+size)
757 for key, value in obj.items():
758 check_key(key)
759 self.computeOffsets(key, asReference=True)
760 self.computeOffsets(value, asReference=True)
761 else:
762 raise InvalidPlistException("Unknown object type.")
763
764 def writeObjectReference(self, obj, output):
765 """
766 Private method to write an object reference.
767
768 Tries to write an object reference, adding it to the references
769 table. Does not write the actual object bytes or set the reference
770 position. Returns a tuple of whether the object was a new reference
771 (True if it was, False if it already was in the reference table)
772 and the new output.
773
774 @param obj object to be written
775 @param output output stream to append the object to
776 @return flag indicating a new reference and the new output
777 """
778 position = self.positionOfObjectReference(obj)
779 if position is None:
780 self.writtenReferences[obj] = len(self.writtenReferences)
781 output += self.binaryInt(len(self.writtenReferences) - 1,
782 bytes=self.trailer.objectRefSize)
783 return (True, output)
784 else:
785 output += self.binaryInt(position, bytes=self.trailer.objectRefSize)
786 return (False, output)
787
788 def writeObject(self, obj, output, setReferencePosition=False):
789 """
790 Private method to serialize the given object to the output.
791
792 @param obj object to be serialized
793 @param output output to be serialized to (bytes)
794 @param setReferencePosition flag indicating, that the reference
795 position the object was written to shall be recorded (boolean)
796 @return new output
797 """
798 def proc_variable_length(format, length):
799 result = ''
800 if length > 0b1110:
801 result += pack('!B', (format << 4) | 0b1111)
802 result = self.writeObject(length, result)
803 else:
804 result += pack('!B', (format << 4) | length)
805 return result
806
807 if setReferencePosition:
808 self.referencePositions[obj] = len(output)
809
810 if obj is None:
811 output += pack('!B', 0b00000000)
812 elif isinstance(obj, BoolWrapper):
813 if obj.value is False:
814 output += pack('!B', 0b00001000)
815 else:
816 output += pack('!B', 0b00001001)
817 elif isinstance(obj, Uid):
818 size = self.intSize(obj)
819 output += pack('!B', (0b1000 << 4) | size - 1)
820 output += self.binaryInt(obj)
821 elif isinstance(obj, int):
822 bytes = self.intSize(obj)
823 root = math.log(bytes, 2)
824 output += pack('!B', (0b0001 << 4) | int(root))
825 output += self.binaryInt(obj)
826 elif isinstance(obj, float):
827 # just use doubles
828 output += pack('!B', (0b0010 << 4) | 3)
829 output += self.binaryReal(obj)
830 elif isinstance(obj, datetime.datetime):
831 timestamp = calendar.timegm(obj.utctimetuple())
832 timestamp -= apple_reference_date_offset
833 output += pack('!B', 0b00110011)
834 output += pack('!d', float(timestamp))
835 elif isinstance(obj, Data):
836 output += proc_variable_length(0b0100, len(obj))
837 output += obj
838 elif isinstance(obj, str):
839 # Python 3 uses unicode strings only
840 bytes = obj.encode('utf_16_be')
841 output += proc_variable_length(0b0110, len(bytes)/2)
842 output += bytes
843 elif isinstance(obj, HashableWrapper):
844 obj = obj.value
845 if isinstance(obj, (set, list, tuple)):
846 if isinstance(obj, set):
847 output += proc_variable_length(0b1100, len(obj))
848 else:
849 output += proc_variable_length(0b1010, len(obj))
850
851 objectsToWrite = []
852 for objRef in obj:
853 (isNew, output) = self.writeObjectReference(objRef, output)
854 if isNew:
855 objectsToWrite.append(objRef)
856 for objRef in objectsToWrite:
857 output = self.writeObject(objRef, output, setReferencePosition=True)
858 elif isinstance(obj, dict):
859 output += proc_variable_length(0b1101, len(obj))
860 keys = []
861 values = []
862 objectsToWrite = []
863 for key, value in obj.items():
864 keys.append(key)
865 values.append(value)
866 for key in keys:
867 (isNew, output) = self.writeObjectReference(key, output)
868 if isNew:
869 objectsToWrite.append(key)
870 for value in values:
871 (isNew, output) = self.writeObjectReference(value, output)
872 if isNew:
873 objectsToWrite.append(value)
874 for objRef in objectsToWrite:
875 output = self.writeObject(objRef, output, setReferencePosition=True)
876 return output
877
878 def writeOffsetTable(self, output):
879 """
880 Private method to write all of the object reference offsets.
881
882 @param output current output (bytes)
883 @return new output (bytes)
884 """
885 all_positions = []
886 writtenReferences = list(self.writtenReferences.items())
887 writtenReferences.sort(key=lambda x: x[1])
888 for obj,order in writtenReferences:
889 position = self.referencePositions.get(obj)
890 if position is None:
891 raise InvalidPlistException(
892 "Error while writing offsets table. Object not found. {0}"
893 .format(obj))
894 output += self.binaryInt(position, self.trailer.offsetSize)
895 all_positions.append(position)
896 return output
897
898 def binaryReal(self, obj):
899 """
900 Private method to pack a real object.
901
902 @param obj real to be packed
903 @return serialized object (bytes)
904 """
905 # just use doubles
906 result = pack('>d', obj)
907 return result
908
909 def binaryInt(self, obj, bytes=None):
910 """
911 Private method to pack an integer object.
912
913 @param obj integer to be packed
914 @param bytes length the integer should be packed into (integer)
915 @return serialized object (bytes)
916 """
917 result = ''
918 if bytes is None:
919 bytes = self.intSize(obj)
920 if bytes == 1:
921 result += pack('>B', obj)
922 elif bytes == 2:
923 result += pack('>H', obj)
924 elif bytes == 4:
925 result += pack('>L', obj)
926 elif bytes == 8:
927 result += pack('>q', obj)
928 else:
929 raise InvalidPlistException(
930 "Core Foundation can't handle integers with size greater than 8 bytes.")
931 return result
932
933 def intSize(self, obj):
934 """
935 Private method to determine the number of bytes necessary to store the
936 given integer.
937
938 @param obj integer object
939 @return number of bytes required (integer)
940 """
941 # SIGNED
942 if obj < 0: # Signed integer, always 8 bytes
943 return 8
944 # UNSIGNED
945 elif obj <= 0xFF: # 1 byte
946 return 1
947 elif obj <= 0xFFFF: # 2 bytes
948 return 2
949 elif obj <= 0xFFFFFFFF: # 4 bytes
950 return 4
951 # SIGNED
952 # 0x7FFFFFFFFFFFFFFF is the max.
953 elif obj <= 0x7FFFFFFFFFFFFFFF: # 8 bytes
954 return 8
955 else:
956 raise InvalidPlistException(
957 "Core Foundation can't handle integers with size greater than 8 bytes.")
958
959 def realSize(self, obj):
960 """
961 Private method to determine the number of bytes necessary to store the
962 given real.
963
964 @param obj real object
965 @return number of bytes required (integer)
966 """
967 return 8

eric ide

mercurial