mirror of
https://github.com/SickGear/SickGear.git
synced 2024-12-01 00:43:37 +00:00
980e05cc99
Backported 400 revisions from rev 1de4961-8897c5b (2018-2014). Move core/benchmark, core/cmd_line, core/memory, core/profiler and core/timeout to core/optional/* Remove metadata/qt* PORT: Version 2.0a3 (inline with 3.0a3 @ f80c7d5). Basic Support for XMP Packets. tga: improvements to adhere more closely to the spec. pdf: slightly improved parsing. rar: fix TypeError on unknown block types. Add MacRoman win32 codepage. tiff/exif: support SubIFDs and tiled images. Add method to export metadata in dictionary. mpeg_video: don't attempt to parse Stream past length. mpeg_video: parse ESCR correctly, add SCR value. Change centralise CustomFragments. field: don't set parser class if class is None, to enable autodetect. field: add value/display for CustomFragment. parser: inline warning to enable tracebacks in debug mode. Fix empty bytestrings in makePrintable. Fix contentSize in jpeg.py to account for image_data blocks. Fix the ELF parser. Enhance the AR archive parser. elf parser: fix wrong wrong fields order in parsing little endian section flags. elf parser: add s390 as a machine type. Flesh out mp4 parser. PORT: Version 2.0a1 (inline with 3.0a1). Major refactoring and PEP8. Fix ResourceWarning warnings on files. Add a close() method and support for the context manager protocol ("with obj: ...") to parsers, input and output streams. metadata: get comment from ZIP. Support for InputIOStream.read(0). Fix sizeGe when size is None. Remove unused new_seekable_field_set file. Remove parser Mapsforge .map. Remove parser Parallel Realities Starfighter .pak files. sevenzip: fix for newer archives. java: update access flags and modifiers for Java 1.7 and update description text for most recent Java. Support ustar prefix field in tar archives. Remove file_system* parsers. Remove misc parsers 3d0, 3ds, gnome_keyring, msoffice*, mstask, ole*, word*. Remove program parsers macho, nds, prc. Support non-8bit Character subclasses. Python parser supports Python 3.7. Enhance mpeg_ts parser to support MTS/M2TS. Support for creation date in tiff. Change don't hardcode errno constant. PORT: 1.9.1 Internal Only: The following are legacy reference to upstream commit messages. Relevant changes up to b0a115f8. Use integer division. Replace HACHOIR_ERRORS with Exception. Fix metadata.Data: make it sortable. Import fixes from e7de492. PORT: Version 2.0a1 (inline with 3.0a1 @ e9f8fad). Replace hachoir.core.field with hachoir.field Replace hachoir.core.stream with hachoir.stream Remove the compatibility module for PY1.5 to PY2.5. metadata: support TIFF picture. metadata: fix string normalization. metadata: fix datetime regex Fix hachoir bug #57. FileFromInputStream: fix comparison between None and an int. InputIOStream: open the file in binary mode.
320 lines
12 KiB
Python
320 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 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 u''
|
|
else:
|
|
return None
|
|
|
|
def createDisplay(self):
|
|
if 'value' in self:
|
|
return unicode(self['value'].display)
|
|
elif self['marker_type'].value in [4, 5, 6]:
|
|
return u''
|
|
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 = "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>'''
|