|
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 |