Utilities/binplistlib.py

changeset 2997
7f0ef975da9e
parent 2969
0e1af1313b8b
child 3030
4a0a82ddd9d2
child 3057
10516539f238
equal deleted inserted replaced
2996:c6f16f1b9958 2997:7f0ef975da9e
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
114 return "Uid(%d)" % self 116 return "Uid(%d)" % self
115 117
116 118
117 class Data(bytes): 119 class Data(bytes):
118 """ 120 """
119 Class implementing a wrapper around bytes types for representing Data values. 121 Class implementing a wrapper around bytes types for representing Data
122 values.
120 """ 123 """
121 pass 124 pass
122 125
123 126
124 class InvalidPlistException(Exception): 127 class InvalidPlistException(Exception):
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
436 """ 448 """
437 refs = [] 449 refs = []
438 i = 0 450 i = 0
439 while i < count: 451 while i < count:
440 fragment = self.contents[ 452 fragment = self.contents[
441 self.currentOffset:self.currentOffset + self.trailer.objectRefSize] 453 self.currentOffset:
454 self.currentOffset + self.trailer.objectRefSize]
442 ref = self.getSizedInteger(fragment, len(fragment)) 455 ref = self.getSizedInteger(fragment, len(fragment))
443 refs.append(ref) 456 refs.append(ref)
444 self.currentOffset += self.trailer.objectRefSize 457 self.currentOffset += self.trailer.objectRefSize
445 i += 1 458 i += 1
446 return refs 459 return refs
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.

eric ide

mercurial