Ticket #145: chm.2.diff

File chm.2.diff, 9.9 kB (added by nneonneo <nneonneo@gmail.com>, 1 year ago)

Changed to use SFS; when the directory appears AFTER the data, we have little choice...

  • chm.py

    old new  
    66  http://www.wotsit.org (search "chm") 
    77- chmlib library 
    88  http://www.jedrea.com/chmlib/ 
     9- Unofficial CHM Spec 
     10  http://savannah.nongnu.org/projects/chmspec 
     11- Microsoft's HTML Help (.chm) format 
     12  http://www.speakeasy.org/~russotto/chm/chmformat.html 
    913 
    1014Author: Victor Stinner 
    1115Creation date: 2007-03-04 
    1216""" 
    1317 
    1418from hachoir_parser import Parser 
    15 from hachoir_core.field import (Field, FieldSet, ParserError, 
    16     Int32, UInt32, UInt64, 
     19from hachoir_core.field import (Field, FieldSet, ParserError, RootSeekableFieldSet, 
     20    Int32, UInt16, UInt32, UInt64, 
    1721    RawBytes, PaddingBytes, 
    1822    Enum, String) 
    1923from hachoir_core.endian import LITTLE_ENDIAN 
     24from hachoir_parser import HachoirParser 
    2025from hachoir_parser.common.win32 import GUID 
    2126from hachoir_parser.common.win32_lang_id import LANGUAGE_ID 
    2227from hachoir_core.text_handler import textHandler, hexadecimal, filesizeHandler 
     
    4247                raise ParserError("CHM: CWord is limited to 64 bits") 
    4348            addr += 8 
    4449            byte = stream.readBits(addr, 8, endian) 
     50        value <<= 7 
    4551        value += byte 
    4652        self.createValue = lambda: value 
    4753 
     
    99105    def createFields(self): 
    100106        yield CWord(self, "name_len") 
    101107        yield String(self, "name", self["name_len"].value, charset="UTF-8") 
    102         yield CWord(self, "space") 
    103         yield CWord(self, "start"
    104         yield filesizeHandler(CWord(self, "length")) 
     108        yield CWord(self, "section", "Section number that the entry data is in.") 
     109        yield CWord(self, "start", "Start offset of the data"
     110        yield filesizeHandler(CWord(self, "length", "Length of the data")) 
    105111 
    106112    def createDescription(self): 
    107113        return "%s (%s)" % (self["name"].value, self["length"].display) 
     
    118124 
    119125        # Entries 
    120126        stop = self.size - self["free_space"].value * 8 
     127        entry_count = 0 
    121128        while self.current_size < stop: 
    122129            yield PMGL_Entry(self, "entry[]") 
     130            entry_count+=1 
    123131 
    124132        # Padding 
    125         padding = (self.size - self.current_size) // 8 
     133        quickref_frequency = 1 + (1 << self["/dir/itsp/density"].value) 
     134        num_quickref = (entry_count // quickref_frequency) 
     135        if entry_count % quickref_frequency == 0: 
     136            num_quickref -= 1 
     137        print self.current_size//8, quickref_frequency, num_quickref 
     138        padding = (self["free_space"].value - (num_quickref*2+2)) 
    126139        if padding: 
    127140            yield PaddingBytes(self, "padding", padding) 
     141        for i in range(num_quickref*quickref_frequency, 0, -quickref_frequency): 
     142            yield UInt16(self, "quickref[%i]"%i) 
     143        yield UInt16(self, "entry_count") 
    128144 
    129145class PMGI_Entry(FieldSet): 
    130146    def createFields(self): 
     
    164180        if self.current_size < self.size: 
    165181            yield PMGI(self, "pmgi", size=block_size) 
    166182 
    167 class ChmFile(Parser): 
     183class NameList(FieldSet): 
     184    def createFields(self): 
     185        yield UInt16(self, "length", "Length of name list in 2-byte blocks") 
     186        yield UInt16(self, "count", "Number of entries in name list") 
     187        for index in range(self["count"].value): 
     188            length=UInt16(self, "name_len[]", "Length of name in 2-byte blocks, excluding terminating null") 
     189            yield length 
     190            yield String(self, "name[]", length.value*2+2, charset="UTF-16-LE") 
     191 
     192class ControlData(FieldSet): 
     193    def createFields(self): 
     194        yield UInt32(self, "count", "Number of DWORDS in this struct") 
     195        yield String(self, "type", 4, "Type of compression") 
     196        if self["type"].value!='LZXC': return 
     197        yield UInt32(self, "version", "Compression version") 
     198        version=self["version"].value 
     199        if version==1: block='bytes' 
     200        else: block='32KB blocks' 
     201        yield UInt32(self, "reset_interval", "LZX: Reset interval in %s"%block) 
     202        yield UInt32(self, "window_size", "LZX: Window size in %s"%block) 
     203        yield UInt32(self, "cache_size", "LZX: Cache size in %s"%block) 
     204        yield UInt32(self, "unknown[]") 
     205 
     206class ResetTable(FieldSet): 
     207    def createFields(self): 
     208        yield UInt32(self, "unknown[]", "Version number?") 
     209        yield UInt32(self, "count", "Number of entries") 
     210        yield UInt32(self, "entry_size", "Size of each entry") 
     211        yield UInt32(self, "header_size", "Size of this header") 
     212        yield UInt64(self, "uncompressed_size") 
     213        yield UInt64(self, "compressed_size") 
     214        yield UInt64(self, "block_size", "Block size in bytes") 
     215        for i in xrange(self["count"].value): 
     216            yield UInt64(self, "block_location[]", "location in compressed data of 1st block boundary in uncompressed data") 
     217 
     218class SystemEntry(FieldSet): 
     219    ENTRY_TYPE={0:"HHP: [OPTIONS]: Contents File", 
     220                1:"HHP: [OPTIONS]: Index File", 
     221                2:"HHP: [OPTIONS]: Default Topic", 
     222                3:"HHP: [OPTIONS]: Title", 
     223                4:"File Metadata", 
     224                5:"HHP: [OPTIONS]: Default Window", 
     225                6:"HHP: [OPTIONS]: Compiled file", 
     226                # 7 present only in files with Binary Index; unknown function 
     227                # 8 unknown function 
     228                9: "Version", 
     229                10: "Timestamp", 
     230                # 11 only in Binary TOC files 
     231                12: "Number of Info Types", 
     232                13: "#IDXHDR file", 
     233                # 14 unknown function 
     234                # 15 checksum?? 
     235                16:"HHP: [OPTIONS]: Default Font", 
     236    } 
     237    def createFields(self): 
     238        yield Enum(UInt16(self, "type", "Type of entry"),self.ENTRY_TYPE) 
     239        yield UInt16(self, "length", "Length of entry") 
     240        yield RawBytes(self, "data", self["length"].value) 
     241    def createDescription(self): 
     242        return '#SYSTEM Entry, Type %s'%self["type"].display 
     243         
     244class SystemFile(FieldSet): 
     245    def createFields(self): 
     246        yield UInt32(self, "version", "Either 2 or 3") 
     247        while self.current_size < self.size: 
     248            yield SystemEntry(self, "entry[]") 
     249 
     250class ChmFile(HachoirParser, RootSeekableFieldSet): 
    168251    PARSER_TAGS = { 
    169252        "id": "chm", 
    170253        "category": "misc", 
     
    175258    } 
    176259    endian = LITTLE_ENDIAN 
    177260 
     261    def __init__(self, stream, **args): 
     262        RootSeekableFieldSet.__init__(self, None, "root", stream, None, stream.askSize(self)) 
     263        HachoirParser.__init__(self, stream, **args) 
     264 
    178265    def validate(self): 
    179         if self.stream.readBytes(0, 4) != "ITSF": 
     266        if self["itsf/magic"].value != "ITSF": 
    180267            return "Invalid magic" 
    181268        if self["itsf/version"].value != 3: 
    182269            return "Invalid version" 
     
    186273        yield ITSF(self, "itsf") 
    187274        yield Filesize_Header(self, "file_size", size=self["itsf/filesize_len"].value*8) 
    188275 
    189         padding = self.seekByte(self["itsf/dir_offset"].value) 
    190         if padding: 
    191             yield padding 
    192         yield Directory(self, "dir", size=self["itsf/dir_len"].value*8) 
     276        self.seekByte(self["itsf/dir_offset"].value) 
     277        directory=Directory(self, "dir", size=self["itsf/dir_len"].value*8) 
     278        yield directory 
    193279 
    194         size = (self.size - self.current_size) // 8 
    195         if size: 
    196             yield RawBytes(self, "raw_end", size) 
     280        otherentries = {} 
     281        for pmgl in directory.array("pmgl"): 
     282            for entry in pmgl.array("entry"): 
     283                if entry["section"].value != 0: 
     284                    otherentries.setdefault(entry["section"].value,[]).append(entry) 
     285                    continue 
     286                if entry["length"].value == 0: 
     287                    continue 
     288                self.seekByte(self["itsf/data_offset"].value+entry["start"].value) 
     289                name = entry["name"].value 
     290                if name == "::DataSpace/NameList": 
     291                    yield NameList(self, "name_list") 
     292                elif name.startswith('::DataSpace/Storage/'): 
     293                    sectname = str(name.split('/')[2]) 
     294                    if name.endswith('/SpanInfo'): 
     295                        yield UInt64(self, "%s_spaninfo"%sectname, "Size of uncompressed data in the %s section"%sectname) 
     296                    elif name.endswith('/ControlData'): 
     297                        yield ControlData(self, "%s_controldata"%sectname, "Data about the compression scheme", size=entry["length"].value*8) 
     298                    elif name.endswith('/Transform/List'): 
     299                        yield String(self, "%s_transform_list"%sectname, 38, description="Transform/List element", charset="UTF-16-LE") 
     300                    elif name.endswith('/Transform/{7FC28940-9D31-11D0-9B27-00A0C91E9C7C}/InstanceData/ResetTable'): 
     301                        yield ResetTable(self, "%s_reset_table"%sectname, "LZX Reset Table", size=entry["length"].value*8) 
     302                    elif name.endswith('/Content'): 
     303                        # eventually, a LZX wrapper will appear here, we hope! 
     304                        yield RawBytes(self, "%s_content"%sectname, entry["length"].value, "Content for the %s section"%sectname) 
     305                    else: 
     306                        yield RawBytes(self, "entry_data[]", entry["length"].value, name) 
     307                elif name=="/#SYSTEM": 
     308                    yield SystemFile(self, "system_file", size=entry["length"].value*8) 
     309                else: 
     310                    yield RawBytes(self, "entry_data[]", entry["length"].value, name) 
    197311 
     312    def getFile(self, filename): 
     313        page=0 
     314        if 'pmgi' in self['/dir']: 
     315            for entry in self['/dir/pmgi'].array('entry'): 
     316                if entry['name'].value <= filename: 
     317                    page=entry['page'].value 
     318        pmgl=self['/dir/pmgl[%i]'%page] 
     319        for entry in pmgl.array('entry'): 
     320            if entry['name'].value == filename: 
     321                return entry 
     322        raise ParserError("File '%s' not found!"%filename) 
     323 
    198324    def createContentSize(self): 
    199325        return self["file_size/file_size"].value * 8 
    200326