mirror of
https://github.com/SickGear/SickGear.git
synced 2024-12-26 20:53:38 +00:00
326 lines
12 KiB
Python
326 lines
12 KiB
Python
|
"""
|
||
|
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>'''
|