""" Apple/NeXT Binary Property List (BPLIST) parser. Also includes a .createXML() function which produces an XML representation of the object. Note that it will discard unknown objects, nulls and fill values, but should work for most files. Documents: - CFBinaryPList.c http://src.gnu-darwin.org/DarwinSourceArchive/expanded/CF/CF-299/Parsing.subproj/CFBinaryPList.c - ForFoundationOnly.h (for structure formats) http://src.gnu-darwin.org/DarwinSourceArchive/expanded/CF/CF-299/Base.subproj/ForFoundationOnly.h - XML <-> BPList converter http://scw.us/iPhone/plutil/plutil.pl Author: Robert Xiao Created: 2008-09-21 """ from hachoir.parser import HachoirParser from hachoir.field import (RootSeekableFieldSet, FieldSet, Enum, Bits, GenericInteger, Float32, Float64, UInt8, UInt64, Bytes, NullBytes, RawBytes, String) from hachoir.core.endian import BIG_ENDIAN from hachoir.core.text_handler import displayHandler from hachoir.core.tools import humanDatetime from datetime import datetime, timedelta class BPListTrailer(FieldSet): def createFields(self): yield NullBytes(self, "unused", 6) yield UInt8(self, "offsetIntSize", "Size (in bytes) of offsets in the offset table") yield UInt8(self, "objectRefSize", "Size (in bytes) of object numbers in object references") yield UInt64(self, "numObjects", "Number of objects in this file") yield UInt64(self, "topObject", "Top-level object reference") yield UInt64(self, "offsetTableOffset", "File offset to the offset table") def createDescription(self): return "Binary PList trailer" class BPListOffsetTable(FieldSet): def createFields(self): size = self["../trailer/offsetIntSize"].value * 8 for i in range(self["../trailer/numObjects"].value): yield Bits(self, "offset[]", size) class BPListSize(FieldSet): def createFields(self): yield Bits(self, "size", 4) if self['size'].value == 0xF: yield BPListObject(self, "fullsize") def createValue(self): if 'fullsize' in self: return self['fullsize'].value else: return self['size'].value class BPListObjectRef(GenericInteger): def __init__(self, parent, name, description=None): size = parent['/trailer/objectRefSize'].value * 8 GenericInteger.__init__(self, parent, name, False, size, description) def getRef(self): return self.parent['/object[' + str(self.value) + ']'] def createDisplay(self): return self.getRef().display def createXML(self, prefix=''): return self.getRef().createXML(prefix) class BPListArray(FieldSet): def __init__(self, parent, name, size, description=None): FieldSet.__init__(self, parent, name, description=description) self.numels = size def createFields(self): for i in range(self.numels): yield BPListObjectRef(self, "ref[]") def createValue(self): return self.array('ref') def createDisplay(self): return '[' + ', '.join([x.display for x in self.value]) + ']' def createXML(self, prefix=''): return prefix + '<array>\n' + ''.join([x.createXML(prefix + '\t') + '\n' for x in self.value]) + prefix + '</array>' class BPListDict(FieldSet): def __init__(self, parent, name, size, description=None): FieldSet.__init__(self, parent, name, description=description) self.numels = size def createFields(self): for i in range(self.numels): yield BPListObjectRef(self, "keyref[]") for i in range(self.numels): yield BPListObjectRef(self, "valref[]") def createValue(self): return list(zip(self.array('keyref'), self.array('valref'))) def createDisplay(self): return '{' + ', '.join(['%s: %s' % (k.display, v.display) for k, v in self.value]) + '}' def createXML(self, prefix=''): return prefix + '<dict>\n' + ''.join(['%s\t<key>%s</key>\n%s\n' % (prefix, k.getRef().value.encode('utf-8'), v.createXML(prefix + '\t')) for k, v in self.value]) + prefix + '</dict>' class BPListObject(FieldSet): def createFields(self): yield Enum(Bits(self, "marker_type", 4), {0: "Simple", 1: "Int", 2: "Real", 3: "Date", 4: "Data", 5: "ASCII String", 6: "UTF-16-BE String", 8: "UID", 10: "Array", 13: "Dict", }) markertype = self['marker_type'].value if markertype == 0: # Simple (Null) yield Enum(Bits(self, "value", 4), {0: "Null", 8: "False", 9: "True", 15: "Fill Byte", }) if self['value'].display == "False": self.xml = lambda prefix: prefix + "<false/>" elif self['value'].display == "True": self.xml = lambda prefix: prefix + "<true/>" else: self.xml = lambda prefix: prefix + "" elif markertype == 1: # Int yield Bits(self, "size", 4, "log2 of number of bytes") size = self['size'].value # 8-bit (size=0), 16-bit (size=1) and 32-bit (size=2) numbers are unsigned # 64-bit (size=3) numbers are signed yield GenericInteger(self, "value", (size >= 3), (2**size) * 8) self.xml = lambda prefix: prefix + \ "<integer>%s</integer>" % self['value'].value elif markertype == 2: # Real yield Bits(self, "size", 4, "log2 of number of bytes") if self['size'].value == 2: # 2**2 = 4 byte float yield Float32(self, "value") elif self['size'].value == 3: # 2**3 = 8 byte float yield Float64(self, "value") else: # FIXME: What is the format of the real? yield Bits(self, "value", (2**self['size'].value) * 8) self.xml = lambda prefix: prefix + \ "<real>%s</real>" % self['value'].value elif markertype == 3: # Date yield Bits(self, "extra", 4, "Extra value, should be 3") # Use a heuristic to determine which epoch to use def cvt_time(v): v = timedelta(seconds=v) epoch2001 = datetime(2001, 1, 1) epoch1970 = datetime(1970, 1, 1) if (epoch2001 + v - datetime.today()).days > 5 * 365: return epoch1970 + v return epoch2001 + v yield displayHandler(Float64(self, "value"), lambda x: humanDatetime(cvt_time(x))) self.xml = lambda prefix: prefix + \ "<date>%sZ</date>" % ( cvt_time(self['value'].value).isoformat()) elif markertype == 4: # Data yield BPListSize(self, "size") if self['size'].value: yield Bytes(self, "value", self['size'].value) self.xml = lambda prefix: prefix + \ "<data>\n%s\n%s</data>" % ( self['value'].value.encode('base64').strip(), prefix) else: self.xml = lambda prefix: prefix + '<data></data>' elif markertype == 5: # ASCII String yield BPListSize(self, "size") if self['size'].value: yield String(self, "value", self['size'].value, charset="ASCII") self.xml = lambda prefix: prefix + \ "<string>%s</string>" % (self['value'].value.replace( '&', '&').encode('iso-8859-1')) else: self.xml = lambda prefix: prefix + '<string></string>' elif markertype == 6: # UTF-16-BE String yield BPListSize(self, "size") if self['size'].value: yield String(self, "value", self['size'].value * 2, charset="UTF-16-BE") self.xml = lambda prefix: prefix + \ "<string>%s</string>" % ( self['value'].value.replace('&', '&').encode('utf-8')) else: self.xml = lambda prefix: prefix + '<string></string>' elif markertype == 8: # UID yield Bits(self, "size", 4, "Number of bytes minus 1") yield GenericInteger(self, "value", False, (self['size'].value + 1) * 8) self.xml = lambda prefix: prefix + "" # no equivalent? elif markertype == 10: # Array yield BPListSize(self, "size") size = self['size'].value if size: yield BPListArray(self, "value", size) self.xml = lambda prefix: self['value'].createXML(prefix) elif markertype == 13: # Dict yield BPListSize(self, "size") yield BPListDict(self, "value", self['size'].value) self.xml = lambda prefix: self['value'].createXML(prefix) else: yield Bits(self, "value", 4) self.xml = lambda prefix: '' def createValue(self): if 'value' in self: return self['value'].value elif self['marker_type'].value in [4, 5, 6]: return '' else: return None def createDisplay(self): if 'value' in self: return str(self['value'].display) elif self['marker_type'].value in [4, 5, 6]: return '' else: return None def createXML(self, prefix=''): if 'value' in self: try: return self.xml(prefix) except AttributeError: return '' return '' def getFieldType(self): return '%s<%s>' % (FieldSet.getFieldType(self), self['marker_type'].display) class BPList(HachoirParser, RootSeekableFieldSet): endian = BIG_ENDIAN MAGIC = b"bplist00" PARSER_TAGS = { "id": "bplist", "category": "misc", "file_ext": ("plist",), "magic": ((MAGIC, 0),), "min_size": 8 + 32, # bplist00 + 32-byte trailer "description": "Apple/NeXT Binary Property List", } def __init__(self, stream, **args): RootSeekableFieldSet.__init__( self, None, "root", stream, None, stream.askSize(self)) HachoirParser.__init__(self, stream, **args) def validate(self): if self.stream.readBytes(0, len(self.MAGIC)) != self.MAGIC: return "Invalid magic" return True def createFields(self): yield Bytes(self, "magic", 8, "File magic (bplist00)") if self.size: self.seekByte(self.size // 8 - 32, True) else: # FIXME: UNTESTED while True: try: self.seekByte(1024) except Exception: break self.seekByte(self.size // 8 - 32) yield BPListTrailer(self, "trailer") self.seekByte(self['trailer/offsetTableOffset'].value) yield BPListOffsetTable(self, "offset_table") for i in self.array("offset_table/offset"): if self.current_size > i.value * 8: self.seekByte(i.value) elif self.current_size < i.value * 8: # try to detect files with gaps or unparsed content yield RawBytes(self, "padding[]", i.value - self.current_size // 8) yield BPListObject(self, "object[]") def createXML(self, prefix=''): return '''<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> ''' + self['/object[' + str(self['/trailer/topObject'].value) + ']'].createXML(prefix) + ''' </plist>'''