""" http://github.com/farcaller/bplist-python/blob/master/bplist.py """ import struct import plistlib from datetime import datetime, timedelta class BPListWriter(object): def __init__(self, objects): self.bplist = "" self.objects = objects def binary(self): '''binary -> string Generates bplist ''' self.data = 'bplist00' # TODO: flatten objects and count max length size # TODO: write objects and save offsets # TODO: write offsets # TODO: write metadata return self.data def write(self, filename): ''' Writes bplist to file ''' if self.bplist != "": pass # TODO: save self.bplist to file else: raise Exception('BPlist not yet generated') class BPlistReader(object): def __init__(self, s): self.data = s self.objects = [] self.resolved = {} def __unpackIntStruct(self, sz, s): '''__unpackIntStruct(size, string) -> int Unpacks the integer of given size (1, 2 or 4 bytes) from string ''' if sz == 1: ot = '!B' elif sz == 2: ot = '!H' elif sz == 4: ot = '!I' elif sz == 8: ot = '!Q' else: raise Exception('int unpack size '+str(sz)+' unsupported') return struct.unpack(ot, s)[0] def __unpackInt(self, offset): '''__unpackInt(offset) -> int Unpacks int field from plist at given offset ''' return self.__unpackIntMeta(offset)[1] def __unpackIntMeta(self, offset): '''__unpackIntMeta(offset) -> (size, int) Unpacks int field from plist at given offset and returns its size and value ''' obj_header = struct.unpack('!B', self.data[offset])[0] obj_type, obj_info = (obj_header & 0xF0), (obj_header & 0x0F) int_sz = 2**obj_info return int_sz, self.__unpackIntStruct(int_sz, self.data[offset+1:offset+1+int_sz]) def __resolveIntSize(self, obj_info, offset): '''__resolveIntSize(obj_info, offset) -> (count, offset) Calculates count of objref* array entries and returns count and offset to first element ''' if obj_info == 0x0F: ofs, obj_count = self.__unpackIntMeta(offset+1) objref = offset+2+ofs else: obj_count = obj_info objref = offset+1 return obj_count, objref def __unpackFloatStruct(self, sz, s): '''__unpackFloatStruct(size, string) -> float Unpacks the float of given size (4 or 8 bytes) from string ''' if sz == 4: ot = '!f' elif sz == 8: ot = '!d' else: raise Exception('float unpack size '+str(sz)+' unsupported') return struct.unpack(ot, s)[0] def __unpackFloat(self, offset): '''__unpackFloat(offset) -> float Unpacks float field from plist at given offset ''' obj_header = struct.unpack('!B', self.data[offset])[0] obj_type, obj_info = (obj_header & 0xF0), (obj_header & 0x0F) int_sz = 2**obj_info return int_sz, self.__unpackFloatStruct(int_sz, self.data[offset+1:offset+1+int_sz]) def __unpackDate(self, offset): td = int(struct.unpack(">d", self.data[offset+1:offset+9])[0]) return datetime(year=2001,month=1,day=1) + timedelta(seconds=td) def __unpackItem(self, offset): '''__unpackItem(offset) Unpacks and returns an item from plist ''' obj_header = struct.unpack('!B', self.data[offset])[0] obj_type, obj_info = (obj_header & 0xF0), (obj_header & 0x0F) if obj_type == 0x00: if obj_info == 0x00: # null 0000 0000 return None elif obj_info == 0x08: # bool 0000 1000 // false return False elif obj_info == 0x09: # bool 0000 1001 // true return True elif obj_info == 0x0F: # fill 0000 1111 // fill byte raise Exception("0x0F Not Implemented") # this is really pad byte, FIXME else: raise Exception('unpack item type '+str(obj_header)+' at '+str(offset)+ 'failed') elif obj_type == 0x10: # int 0001 nnnn ... // # of bytes is 2^nnnn, big-endian bytes return self.__unpackInt(offset) elif obj_type == 0x20: # real 0010 nnnn ... // # of bytes is 2^nnnn, big-endian bytes return self.__unpackFloat(offset) elif obj_type == 0x30: # date 0011 0011 ... // 8 byte float follows, big-endian bytes return self.__unpackDate(offset) elif obj_type == 0x40: # data 0100 nnnn [int] ... // nnnn is number of bytes unless 1111 then int count follows, followed by bytes obj_count, objref = self.__resolveIntSize(obj_info, offset) return plistlib.Data(self.data[objref:objref+obj_count]) # XXX: we return data as str elif obj_type == 0x50: # string 0101 nnnn [int] ... // ASCII string, nnnn is # of chars, else 1111 then int count, then bytes obj_count, objref = self.__resolveIntSize(obj_info, offset) return self.data[objref:objref+obj_count] elif obj_type == 0x60: # string 0110 nnnn [int] ... // Unicode string, nnnn is # of chars, else 1111 then int count, then big-endian 2-byte uint16_t obj_count, objref = self.__resolveIntSize(obj_info, offset) return self.data[objref:objref+obj_count*2].decode('utf-16be') elif obj_type == 0x80: # uid 1000 nnnn ... // nnnn+1 is # of bytes # FIXME: Accept as a string for now obj_count, objref = self.__resolveIntSize(obj_info, offset) return plistlib.Data(self.data[objref:objref+obj_count]) elif obj_type == 0xA0: # array 1010 nnnn [int] objref* // nnnn is count, unless '1111', then int count follows obj_count, objref = self.__resolveIntSize(obj_info, offset) arr = [] for i in range(obj_count): arr.append(self.__unpackIntStruct(self.object_ref_size, self.data[objref+i*self.object_ref_size:objref+i*self.object_ref_size+self.object_ref_size])) return arr elif obj_type == 0xC0: # set 1100 nnnn [int] objref* // nnnn is count, unless '1111', then int count follows # XXX: not serializable via apple implementation raise Exception("0xC0 Not Implemented") # FIXME: implement elif obj_type == 0xD0: # dict 1101 nnnn [int] keyref* objref* // nnnn is count, unless '1111', then int count follows obj_count, objref = self.__resolveIntSize(obj_info, offset) keys = [] for i in range(obj_count): keys.append(self.__unpackIntStruct(self.object_ref_size, self.data[objref+i*self.object_ref_size:objref+i*self.object_ref_size+self.object_ref_size])) values = [] objref += obj_count*self.object_ref_size for i in range(obj_count): values.append(self.__unpackIntStruct(self.object_ref_size, self.data[objref+i*self.object_ref_size:objref+i*self.object_ref_size+self.object_ref_size])) dic = {} for i in range(obj_count): dic[keys[i]] = values[i] return dic else: raise Exception('don\'t know how to unpack obj type '+hex(obj_type)+' at '+str(offset)) def __resolveObject(self, idx): try: return self.resolved[idx] except KeyError: obj = self.objects[idx] if type(obj) == list: newArr = [] for i in obj: newArr.append(self.__resolveObject(i)) self.resolved[idx] = newArr return newArr if type(obj) == dict: newDic = {} for k,v in obj.iteritems(): rk = self.__resolveObject(k) rv = self.__resolveObject(v) newDic[rk] = rv self.resolved[idx] = newDic return newDic else: self.resolved[idx] = obj return obj def parse(self): # read header if self.data[:8] != 'bplist00': raise Exception('Bad magic') # read trailer self.offset_size, self.object_ref_size, self.number_of_objects, self.top_object, self.table_offset = struct.unpack('!6xBB4xI4xI4xI', self.data[-32:]) #print "** plist offset_size:",self.offset_size,"objref_size:",self.object_ref_size,"num_objs:",self.number_of_objects,"top:",self.top_object,"table_ofs:",self.table_offset # read offset table self.offset_table = self.data[self.table_offset:-32] self.offsets = [] ot = self.offset_table for i in xrange(self.number_of_objects): offset_entry = ot[:self.offset_size] ot = ot[self.offset_size:] self.offsets.append(self.__unpackIntStruct(self.offset_size, offset_entry)) #print "** plist offsets:",self.offsets # read object table self.objects = [] k = 0 for i in self.offsets: obj = self.__unpackItem(i) #print "** plist unpacked",k,type(obj),obj,"at",i k += 1 self.objects.append(obj) # rebuild object tree #for i in range(len(self.objects)): # self.__resolveObject(i) # return root object return self.__resolveObject(self.top_object) @classmethod def plistWithString(cls, s): parser = cls(s) return parser.parse() @classmethod def plistWithFile(cls, f): file = open(f,"rb") parser = cls(file.read()) file.close() return parser.parse()