CHM格式有一个初始化头,占38H字节,后面是header section和到正文 段的偏移量。加在一起,这些被称为文件头。
header section一共有两个section,一个是文件目录,另一个包含着文件长度和一些未知信息。
初始化头:
前四个字节为ITSF,第二个双字为版本信息,第三双字是文件头的总长度,第四双字值为1,第五双字是一个时间记录,(第一个字节是MSB,第二个字节是fractional seconds(second byte),第三个字节可并不确定,第四个字节仅能知道其符号位是确定的。)第六双字是windows语言ID标识,后面16个字节是两个连续的组ID,分别为{7C01FD10-7BAA-11D0-9E0C-00A0-C922-E6EC}
和{7C01FD11-7BAA-11D0-9E0C-00A0-C922-E6EC}
后面是header section的表,其中有两项,每项占16个字节,记录着从文件头开始的偏移量和section的长度,各占8个字节。
后面还有8个字节的信息,这些在版本2里是没有的。
header section 0:
第一双字:0x01fe
第三双字为文件大小
共占5个双字,其余双字均为0
header section 1(directory header)
开始的四个字节为ITSP,
后面的双字为版本号,
第三双字为本section长度,
第四双字信息未知,
第五双字值为0x1000,是目录块的大小,
第六双字是quickref section的“密度”,一般是2
第七双字是索引树的深度,1表示没有索引,2表示有一层的PMGI数据块。
第八双字表示根索引的块号,如果没有索引为-1
第九双字是第一个PMGL(listing)的块号
第十双字是最后一个PMGL的块号
第十一双字是-1
第十二双字是目录块的块数
第十三双字是windows语言ID标识
从这里开始有16个字节的GUID{5D02926A-212E-11D0-9DF9-00A0C922E6EC}
然后四个双字不知道是什么东西
本段共84个字节
从这里开始往后都是数据块,分为两种,一种是列表块(listing chunks),一种是索引块(index chunks)其中列表块的格式如下:
开始是四个字节PMGL
然后的四个字节是目录块尾部的空白区的长度或是quickref区域的长度
第三双字恒为0
第四双字是前一个列表块的块号,如果这是第一个块,该值为-1
第五双字是后一个列表块的块号,如果这是最后一块,该值为-1
从这里开始是目录列表项,按文件名排序,并且大小写不分
quickref区是从数据块的后面向前写,每隔n个项出现一个quickref,且n的值为1+(1<<“密度”),其格式从后至前为
第一个字:整个数据块中的项数
第二个字:从第0项到第n项之间的偏移量
第三个字:从第0项到第2n项之间的偏移量
以此类推
目录列表的每一项的格式如下:
encint型名字长度,后面是UTF-8编码的名称,encint型正文段,encint型偏移量,encint型长度,其中偏移量是从解压缩之后的正文段的开始来计算的,同样长度也是表示解压缩之后的长度。
在目录中存在两种文件,用户数据文件和格式信息文件,格式信息文件以两个连续的冒号“::”开头,用户数据文件以“/”开头。
索引块:
前四个字节为PMGI
后面四个字节是块尾部的quickref或是空白区的长度。
从这里开始是目录索引项的开始,每一个目录索引项的结构如下:
encint型的名称长度,UFT-8编码的名称,以此名称开始的列表块的块号。
quickref的格式和排列与列表块中相同
当有索引块的层次较多时,将不再存储数据块号而是存储下一层的索引号。
解释一下encint型变量的编码规则:
一种可变长度的整型变量,第一个字节只使用低7位,最高位为1表示该字节之后的下一字节的低7位要接在这7位的尾部组成一个数,这样通过移位相加的运算,直到遇到最高位为0的字节,可以组和成一个长度可调节的整数。
正文:在版本3中,正文一般紧跟着文件头,而且在文件头表之后有一个双字用来指定其位置。在版本2中,正文部分紧跟着文件头,而且所有此文件夹中的正文部分的第0段放在都放在这个益上,其它的正文段都within content section 0
名称列表文件:
放在content section 0中,文件名为"::DataSpace/NameList",其中包含着所有正文段的名称,其格式如下:
第一个字:以字计数的文件长度
第二个字:文件中的entry数
对于每一个entry格式为:
第一个字:以字计数的名字长度,不包括最后的NULL结尾符
以word 0表示所有entry的结束。
名称的编码类似于UFT-16。
段的名称目前为止只有两种,Uncompressed和MSCompressed,分别表示自解释文件和Microsoft LZX压缩算法压缩的文件。
section data:
对于段号不为0的段,还有一个文件为::DataSpace/Storage/<Section Name>/Content,里面存放着该段的压缩信息,所以,当解析非0段时,需要两步工作,第一步,取得第0段并将其解圧,取得段名,第二步才能利用段名找到相应的段
其余与格式相关的文件:
::DataSpace/Storage/<SectionName>/ControlData
共0x20个字节,存储关于压缩的信息
第一个双字为在“LZXC”串后的双字个数,在版本2中,此值必为6
第二个双字为“LZXC”
第三个双字为版本信息,必须大于2
第四个双字为LZX reset interval
第五个双字为窗口大小
第六个双字为缓存大小
第七个双字为0,未知信息。
::DataSpace/Storage/<SectionName>/SpanInfo
存放着未解压的段的长度信息。
::DataSpace/Storage/<SectionName>/Transform/List
存放GUID列表用于解压缩
压缩段:
这一段用LZX压缩,要进行解压缩,先要读取::DataSpace/Storage/<SectionName>/Transform/{7FC28940-9D31-11D0-9B27-00A0C91E9C7C}/InstanceData/ResetTable,其格式如下:
第一个双字为2,估计是版本信息
第二个双字是reset table中的entry数
第三个双字是8,每一个entry的大小
第四个双字是表头长度
16个字节的压缩前长度
16个字节的压缩后长度
16个字节的0x8000 block size for locations below
16个字节的0
16个字节的第一个非压缩数据块的边界在压缩数据块中的位置信息
注意:
There is one change from LZX as defined by Microsoft: After each LZX reset interval (defined in the ControlData file, but in practice equal to the window size) of compressed data is processed, the LZX state is fully reset, as if an entirely new file was being encoded. This allows semi-random access to the compressed data; you can start reading on any reset interval boundary using the reset interval size and the reset table.