2 |
2 |
3 # Copyright (c) 2012 - 2013 Detlev Offenbach <detlev@die-offenbachs.de> |
3 # Copyright (c) 2012 - 2013 Detlev Offenbach <detlev@die-offenbachs.de> |
4 # |
4 # |
5 |
5 |
6 """ |
6 """ |
7 Module implementing a library for reading and writing binary property list files. |
7 Module implementing a library for reading and writing binary property list |
|
8 files. |
8 |
9 |
9 Binary Property List (plist) files provide a faster and smaller serialization |
10 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 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 plists which can be read by OS X, iOS, or other clients. |
12 |
13 |
71 # used to endorse or promote products derived from this software without |
72 # used to endorse or promote products derived from this software without |
72 # specific prior written permission. |
73 # specific prior written permission. |
73 # |
74 # |
74 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
75 # 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 # 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 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
77 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
78 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE |
78 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
79 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
79 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
80 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
80 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
81 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
81 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
82 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
82 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
83 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
83 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
84 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
|
85 # POSSIBILITY OF SUCH DAMAGE. |
84 # |
86 # |
85 |
87 |
86 from collections import namedtuple |
88 from collections import namedtuple |
87 from io import BytesIO |
89 from io import BytesIO |
88 import calendar |
90 import calendar |
137 |
140 |
138 def readPlist(pathOrFile): |
141 def readPlist(pathOrFile): |
139 """ |
142 """ |
140 Module function to read a plist file. |
143 Module function to read a plist file. |
141 |
144 |
142 @param pathOrFile name of the plist file (string) or an open file (file object) |
145 @param pathOrFile name of the plist file (string) or an open file |
|
146 (file object) |
143 @return reference to the read object |
147 @return reference to the read object |
144 @exception InvalidPlistException raised to signal an invalid plist file |
148 @exception InvalidPlistException raised to signal an invalid plist file |
145 """ |
149 """ |
146 didOpen = False |
150 didOpen = False |
147 result = None |
151 result = None |
165 def writePlist(rootObject, pathOrFile, binary=True): |
169 def writePlist(rootObject, pathOrFile, binary=True): |
166 """ |
170 """ |
167 Module function to write a plist file. |
171 Module function to write a plist file. |
168 |
172 |
169 @param rootObject reference to the object to be written |
173 @param rootObject reference to the object to be written |
170 @param pathOrFile name of the plist file (string) or an open file (file object) |
174 @param pathOrFile name of the plist file (string) or an open file |
171 @param binary flag indicating the generation of a binary plist file (boolean) |
175 (file object) |
|
176 @param binary flag indicating the generation of a binary plist file |
|
177 (boolean) |
172 """ |
178 """ |
173 if not binary: |
179 if not binary: |
174 plistlib.writePlist(rootObject, pathOrFile) |
180 plistlib.writePlist(rootObject, pathOrFile) |
175 return |
181 return |
176 else: |
182 else: |
226 return True |
232 return True |
227 else: |
233 else: |
228 return False |
234 return False |
229 |
235 |
230 PlistTrailer = namedtuple('PlistTrailer', |
236 PlistTrailer = namedtuple('PlistTrailer', |
231 'offsetSize, objectRefSize, offsetCount, topLevelObjectNumber, offsetTableOffset') |
237 'offsetSize, objectRefSize, offsetCount, topLevelObjectNumber,' |
|
238 ' offsetTableOffset') |
232 PlistByteCounts = namedtuple('PlistByteCounts', |
239 PlistByteCounts = namedtuple('PlistByteCounts', |
233 'nullBytes, boolBytes, intBytes, realBytes, dateBytes, dataBytes, stringBytes, ' |
240 'nullBytes, boolBytes, intBytes, realBytes, dateBytes, dataBytes,' |
234 'uidBytes, arrayBytes, setBytes, dictBytes') |
241 ' stringBytes, uidBytes, arrayBytes, setBytes, dictBytes') |
235 |
242 |
236 |
243 |
237 class PlistReader(object): |
244 class PlistReader(object): |
238 """ |
245 """ |
239 Class implementing the plist reader. |
246 Class implementing the plist reader. |
287 self.contents = self.file.read() |
294 self.contents = self.file.read() |
288 if len(self.contents) < 32: |
295 if len(self.contents) < 32: |
289 raise InvalidPlistException("File is too short.") |
296 raise InvalidPlistException("File is too short.") |
290 trailerContents = self.contents[-32:] |
297 trailerContents = self.contents[-32:] |
291 try: |
298 try: |
292 self.trailer = PlistTrailer._make(unpack("!xxxxxxBBQQQ", trailerContents)) |
299 self.trailer = PlistTrailer._make( |
|
300 unpack("!xxxxxxBBQQQ", trailerContents)) |
293 offset_size = self.trailer.offsetSize * self.trailer.offsetCount |
301 offset_size = self.trailer.offsetSize * self.trailer.offsetCount |
294 offset = self.trailer.offsetTableOffset |
302 offset = self.trailer.offsetTableOffset |
295 offset_contents = self.contents[offset:offset + offset_size] |
303 offset_contents = self.contents[offset:offset + offset_size] |
296 offset_i = 0 |
304 offset_i = 0 |
297 while offset_i < self.trailer.offsetCount: |
305 while offset_i < self.trailer.offsetCount: |
298 begin = self.trailer.offsetSize * offset_i |
306 begin = self.trailer.offsetSize * offset_i |
299 tmp_contents = offset_contents[begin:begin + self.trailer.offsetSize] |
307 tmp_contents = offset_contents[ |
300 tmp_sized = self.getSizedInteger(tmp_contents, self.trailer.offsetSize) |
308 begin:begin + self.trailer.offsetSize] |
|
309 tmp_sized = self.getSizedInteger( |
|
310 tmp_contents, self.trailer.offsetSize) |
301 self.offsets.append(tmp_sized) |
311 self.offsets.append(tmp_sized) |
302 offset_i += 1 |
312 offset_i += 1 |
303 self.setCurrentOffsetToObjectNumber(self.trailer.topLevelObjectNumber) |
313 self.setCurrentOffsetToObjectNumber( |
|
314 self.trailer.topLevelObjectNumber) |
304 result = self.readObject() |
315 result = self.readObject() |
305 except TypeError as e: |
316 except TypeError as e: |
306 raise InvalidPlistException(e) |
317 raise InvalidPlistException(e) |
307 return result |
318 return result |
308 |
319 |
345 result = True |
356 result = True |
346 elif extra == 0b1111: |
357 elif extra == 0b1111: |
347 pass # fill byte |
358 pass # fill byte |
348 else: |
359 else: |
349 raise InvalidPlistException( |
360 raise InvalidPlistException( |
350 "Invalid object found at offset: {0}".format(self.currentOffset - 1)) |
361 "Invalid object found at offset: {0}".format( |
|
362 self.currentOffset - 1)) |
351 # int |
363 # int |
352 elif format == 0b0001: |
364 elif format == 0b0001: |
353 extra = proc_extra(extra) |
365 extra = proc_extra(extra) |
354 result = self.readInteger(pow(2, extra)) |
366 result = self.readInteger(pow(2, extra)) |
355 # real |
367 # real |
501 |
514 |
502 @param length length of the string (integer) |
515 @param length length of the string (integer) |
503 @return unicode encoded string |
516 @return unicode encoded string |
504 """ |
517 """ |
505 actual_length = length * 2 |
518 actual_length = length * 2 |
506 data = self.contents[self.currentOffset:self.currentOffset + actual_length] |
519 data = self.contents[ |
|
520 self.currentOffset:self.currentOffset + actual_length] |
507 # unpack not needed?!! data = unpack(">%ds" % (actual_length), data)[0] |
521 # unpack not needed?!! data = unpack(">%ds" % (actual_length), data)[0] |
508 self.currentOffset += actual_length |
522 self.currentOffset += actual_length |
509 return data.decode('utf_16_be') |
523 return data.decode('utf_16_be') |
510 |
524 |
511 def readDate(self): |
525 def readDate(self): |
513 Private method to read a date. |
527 Private method to read a date. |
514 |
528 |
515 @return date object (datetime.datetime) |
529 @return date object (datetime.datetime) |
516 """ |
530 """ |
517 global apple_reference_date_offset |
531 global apple_reference_date_offset |
518 result = unpack(">d", self.contents[self.currentOffset:self.currentOffset + 8])[0] |
532 result = unpack(">d", |
519 result = datetime.datetime.utcfromtimestamp(result + apple_reference_date_offset) |
533 self.contents[self.currentOffset:self.currentOffset + 8])[0] |
|
534 result = datetime.datetime.utcfromtimestamp( |
|
535 result + apple_reference_date_offset) |
520 self.currentOffset += 8 |
536 self.currentOffset += 8 |
521 return result |
537 return result |
522 |
538 |
523 def readData(self, length): |
539 def readData(self, length): |
524 """ |
540 """ |
559 elif bytes == 4: |
575 elif bytes == 4: |
560 result = unpack('>L', data)[0] |
576 result = unpack('>L', data)[0] |
561 elif bytes == 8: |
577 elif bytes == 8: |
562 result = unpack('>q', data)[0] |
578 result = unpack('>q', data)[0] |
563 else: |
579 else: |
564 raise InvalidPlistException("Encountered integer longer than 8 bytes.") |
580 raise InvalidPlistException( |
|
581 "Encountered integer longer than 8 bytes.") |
565 return result |
582 return result |
566 |
583 |
567 |
584 |
568 class HashableWrapper(object): |
585 class HashableWrapper(object): |
569 """ |
586 """ |
684 |
701 |
685 @param root reference to the object to be written |
702 @param root reference to the object to be written |
686 """ |
703 """ |
687 output = self.header |
704 output = self.header |
688 wrapped_root = self.wrapRoot(root) |
705 wrapped_root = self.wrapRoot(root) |
689 should_reference_root = True # not isinstance(wrapped_root, HashableWrapper) |
706 should_reference_root = True |
690 self.computeOffsets(wrapped_root, asReference=should_reference_root, isRoot=True) |
707 self.computeOffsets( |
|
708 wrapped_root, asReference=should_reference_root, isRoot=True) |
691 self.trailer = self.trailer._replace( |
709 self.trailer = self.trailer._replace( |
692 **{'objectRefSize': self.intSize(len(self.computedUniques))}) |
710 **{'objectRefSize': self.intSize(len(self.computedUniques))}) |
693 (_, output) = self.writeObjectReference(wrapped_root, output) |
711 (_, output) = self.writeObjectReference(wrapped_root, output) |
694 output = self.writeObject(wrapped_root, output, setReferencePosition=True) |
712 output = self.writeObject( |
|
713 wrapped_root, output, setReferencePosition=True) |
695 |
714 |
696 # output size at this point is an upper bound on how big the |
715 # output size at this point is an upper bound on how big the |
697 # object reference offsets need to be. |
716 # object reference offsets need to be. |
698 self.trailer = self.trailer._replace(**{ |
717 self.trailer = self.trailer._replace(**{ |
699 'offsetSize': self.intSize(len(output)), |
718 'offsetSize': self.intSize(len(output)), |
759 @exception InvalidPlistException raised to indicate an invalid |
778 @exception InvalidPlistException raised to indicate an invalid |
760 plist file |
779 plist file |
761 """ # __IGNORE_WARNING__ |
780 """ # __IGNORE_WARNING__ |
762 def check_key(key): |
781 def check_key(key): |
763 if key is None: |
782 if key is None: |
764 raise InvalidPlistException('Dictionary keys cannot be null in plists.') |
783 raise InvalidPlistException( |
|
784 'Dictionary keys cannot be null in plists.') |
765 elif isinstance(key, Data): |
785 elif isinstance(key, Data): |
766 raise InvalidPlistException('Data cannot be dictionary keys in plists.') |
786 raise InvalidPlistException( |
|
787 'Data cannot be dictionary keys in plists.') |
767 elif not isinstance(key, str): |
788 elif not isinstance(key, str): |
768 raise InvalidPlistException('Keys must be strings.') |
789 raise InvalidPlistException('Keys must be strings.') |
769 |
790 |
770 def proc_size(size): |
791 def proc_size(size): |
771 if size > 0b1110: |
792 if size > 0b1110: |
842 self.writtenReferences[obj] = len(self.writtenReferences) |
863 self.writtenReferences[obj] = len(self.writtenReferences) |
843 output += self.binaryInt(len(self.writtenReferences) - 1, |
864 output += self.binaryInt(len(self.writtenReferences) - 1, |
844 bytes=self.trailer.objectRefSize) |
865 bytes=self.trailer.objectRefSize) |
845 return (True, output) |
866 return (True, output) |
846 else: |
867 else: |
847 output += self.binaryInt(position, bytes=self.trailer.objectRefSize) |
868 output += self.binaryInt( |
|
869 position, bytes=self.trailer.objectRefSize) |
848 return (False, output) |
870 return (False, output) |
849 |
871 |
850 def writeObject(self, obj, output, setReferencePosition=False): |
872 def writeObject(self, obj, output, setReferencePosition=False): |
851 """ |
873 """ |
852 Private method to serialize the given object to the output. |
874 Private method to serialize the given object to the output. |
914 for objRef in obj: |
936 for objRef in obj: |
915 (isNew, output) = self.writeObjectReference(objRef, output) |
937 (isNew, output) = self.writeObjectReference(objRef, output) |
916 if isNew: |
938 if isNew: |
917 objectsToWrite.append(objRef) |
939 objectsToWrite.append(objRef) |
918 for objRef in objectsToWrite: |
940 for objRef in objectsToWrite: |
919 output = self.writeObject(objRef, output, setReferencePosition=True) |
941 output = self.writeObject( |
|
942 objRef, output, setReferencePosition=True) |
920 elif isinstance(obj, dict): |
943 elif isinstance(obj, dict): |
921 output += proc_variable_length(0b1101, len(obj)) |
944 output += proc_variable_length(0b1101, len(obj)) |
922 keys = [] |
945 keys = [] |
923 values = [] |
946 values = [] |
924 objectsToWrite = [] |
947 objectsToWrite = [] |
932 for value in values: |
955 for value in values: |
933 (isNew, output) = self.writeObjectReference(value, output) |
956 (isNew, output) = self.writeObjectReference(value, output) |
934 if isNew: |
957 if isNew: |
935 objectsToWrite.append(value) |
958 objectsToWrite.append(value) |
936 for objRef in objectsToWrite: |
959 for objRef in objectsToWrite: |
937 output = self.writeObject(objRef, output, setReferencePosition=True) |
960 output = self.writeObject( |
|
961 objRef, output, setReferencePosition=True) |
938 return output |
962 return output |
939 |
963 |
940 def writeOffsetTable(self, output): |
964 def writeOffsetTable(self, output): |
941 """ |
965 """ |
942 Private method to write all of the object reference offsets. |
966 Private method to write all of the object reference offsets. |
991 result += pack('>L', obj) |
1015 result += pack('>L', obj) |
992 elif bytes == 8: |
1016 elif bytes == 8: |
993 result += pack('>q', obj) |
1017 result += pack('>q', obj) |
994 else: |
1018 else: |
995 raise InvalidPlistException( |
1019 raise InvalidPlistException( |
996 "Core Foundation can't handle integers with size greater than 8 bytes.") |
1020 "Core Foundation can't handle integers with size greater" |
|
1021 " than 8 bytes.") |
997 return result |
1022 return result |
998 |
1023 |
999 def intSize(self, obj): |
1024 def intSize(self, obj): |
1000 """ |
1025 """ |
1001 Private method to determine the number of bytes necessary to store the |
1026 Private method to determine the number of bytes necessary to store the |
1020 # 0x7FFFFFFFFFFFFFFF is the max. |
1045 # 0x7FFFFFFFFFFFFFFF is the max. |
1021 elif obj <= 0x7FFFFFFFFFFFFFFF: # 8 bytes |
1046 elif obj <= 0x7FFFFFFFFFFFFFFF: # 8 bytes |
1022 return 8 |
1047 return 8 |
1023 else: |
1048 else: |
1024 raise InvalidPlistException( |
1049 raise InvalidPlistException( |
1025 "Core Foundation can't handle integers with size greater than 8 bytes.") |
1050 "Core Foundation can't handle integers with size greater" |
|
1051 " than 8 bytes.") |
1026 |
1052 |
1027 def realSize(self, obj): |
1053 def realSize(self, obj): |
1028 """ |
1054 """ |
1029 Private method to determine the number of bytes necessary to store the |
1055 Private method to determine the number of bytes necessary to store the |
1030 given real. |
1056 given real. |