复合文档解析

时间:2022-04-23 05:42:24
第一章    仓库与流


复合文档的原理就像一个文件系统(文件系统:如FAT与NTFS)。复合文档将数据分成许多流(Streams),这些流又存储在不同的仓库(Storages)里。将复合文档想象成你的D盘,D盘用的是NTFS(NT File System)格式,流就相当于D盘里的文件,仓库就相当于D盘里的文件夹。


流和仓库的命名规则与文件系统相似,同一个仓库下的流及仓库不能重名,不同仓库下可以有同名的流。每个复合文档都有一个根仓库(root storage)。
 




第二章    扇区与扇区链


2.1 扇区与扇区标识
 
所有的流又分成更小的数据块,叫做数据扇区(sectors)。Sectors 可能包含控制数据或用户数据。


整个文件由一个头(Header)结构以及其后的所有Sectors组成。Sectors的大小在头中确定,且每个Sectors的大小都相同。


以下为示意图:


HEADER


SECTOR 0


SECTOR 1


SECTOR 2


SECTOR 3


SECTOR 4


SECTOR 5


SECTOR 6


Sectors 简单的以其在文件中的顺序列举,一个扇区的索引(从0开始)叫做扇区标识(SID:sector identifier)。SID是一个有符号的32位的整型值。
如果一个SID的值非负,就表示真正存在的那个Sector;如果为负,就表示特殊的含义。下表给出有效的特殊SID:
SID   Name                   Meaning                                           


–1   Free SID             空闲sector,可存在于文件中,但不是任何流的组成部分。


–2    End Of Chain SID      SID链的结束标记 (见2.2节)


–3   SAT SID                此Sector用于存放扇区配置表(SAT)(见4.2节)


–4   MSAT SID          此Sector用于存放主扇区配置表(MSAT)(见4.1节)




2.2 扇区链与扇区标识链


用于存储流数据的所有Sectors的列表叫做扇区链(Sector Chain)。这些Sectors可以是无序的。因此用于指定一个流的Sectors的顺序的SID数组就称为SID chain。一个SID chain总是以End Of Chain SID(-2)为结束标记。


例:一个流由4个Sector组成,其SID链为[1, 6, 3, 5, –2]。




流的SID链是通过扇区配置表构建的(见4.2节),但短流和以下两种内部流除外:


1.主扇区配置表,其从自身构建SID链(每个扇区包含下一个扇区的SID)。


2.扇区配置表,其通过主扇区配置表构建SID链。


 第三章    复合文档头


3.1 复合文档头的内容


 


复合文档头在文件的开始,且其大小必定为512字节。这意味着第一个Sector的开始相对文件的偏移量为512字节。


复合文档头的结构如下:


Offset    Size        Contents                                           


0            8            复合文档文件标识:D0H CFH 11H E0H A1H B1H 1AH E1H


8            16          此文件的唯一标识(不重要, 可全部为0)


24          2            文件格式修订号 (一般为003EH)


26          2            文件格式版本号(一般为0003H)


28          2            字节顺序规则标识(见3.2)::FEH FFH = Little-Endian


FFH FEH = Big-Endian


30          2            复合文档中sector的大小(ssz),以2的幂形式存储, sector实际大小为s_size


= 2ssz 字节(一般为9即512字节, 最小值为7即128字节)


32          2            short-sector的大小(见5.1),以2的幂形式存储, short-sector实际大


                            小为s_s_size = 2sssz 字节(一般为6即64字节,最大为sector的大小)


34          10          Not used


44          4            用于存放扇区配置表(SAT)的sector总数


48          4            用于存放目录流的第一个sector的SID (见6)


52          4            Not used


56          4            标准流的最小大小(一般为4096 bytes), 小于此值的流即为短流。


60          4            用于存放短扇区配置表(SSAT)的第一个sector的SID (见5.2),


或为–2 (End Of Chain SID)如不存在。


64          4            用于存放短扇区配置表(SSAT)的sector总数


68          4            用于存放主扇区配置表(MSAT)的第一个sector的SID (见4.1),


或为–2 (End Of Chain SID) 若无附加的sectors。


72          4            用于存放主扇区配置表(MSAT)的sector总数


76          436        存放主扇区配置表(MSAT)的第一部分,包含109个SID。


3.2 字节顺序(Byte Order)


 


文件数据的二进制存储有两种方法Little-Endian 和 Big-Endian,但实际应用中只使用Little-Endian方法即:低位8字节存放在地址的低位,高位8字节存放在地址的高位。


例:一个32位的整数13579BDFH(转为十进制即324508639),以Little-Endian存放为DFH 9BH 57H13H,以Big-Endian 存放为 13H 57H 9BH DFH。(H下标表示十六进制)


 


3.3 扇区偏移量


       从头中的信息可以计算出一个sector的偏移量(offset),公式为:


sec_pos(SID) = 512 + SID ∙ s_size = 512 + SID ∙ 2 ssz


例:ssz = 10 and SID = 5:


sec_pos(SID) = 512 + SID ∙ 2 ssz = 512 + 5 ∙ 210 = 512 + 5 ∙ 1024 = 5632.


 


第四章    扇区配置


4.1 主扇区配置表


 


主扇区配置表(MSAT:master sector allocation table)是一个SID数组,指明了所有用于存放扇区配置表(SAT:sector allocation table)的sector的SID。MSAT的大小(SID个数)就等于存放SAT的sector数,在头中指明。


MSAT的前109个SID也存放于头中,如果一个MSAT的SID数多余109个,那么多出来的SID将存放于sector中,头中已经指明了用于存放MSAT的第一个sector的SID。在用于存放MSAT的sector中的最后一个SID指向下一个用于存放MSAT的sector,如果没有下一个则为End Of Chain SID(-2)。


存放MSAT的sector的内容:(s_size表示sector的大小)


Offset            Size                   Contents                         


0                   s_size-4       MSAT的(s_size-4) / 4个SID的数组


s_size-4       4                   下一个用于存放MSAT的sector的SID,或-2(已为最后一个)


 


最后一个存放MSAT的sector可能未被完全填满,空闲的地方将被填上Free SID(-1)。


 


例:一个复合文档需要300个sector用于存放SAT,头中指定sector的大小为512字节,这说明一个sector可存放128个SID。MAST有300个SID,前109个放于头中,其余的191个将要占用2个sector来存放。此例假定第一个存放MSAT的sector为sector 1,则sector 1包含127个SID。第128个SID指向一个用于存放MSAT的sector,假定为sector 6,则sector 6包含剩下的64个SID(最后一个SID为-2,其他的值为-1)。


 


4.2 扇区配置表


 


 


       扇区配置表(SAT:sector allocation table)是一个SID数组,包含所有用户流(短流除外)和内部控制流(the short-stream container stream, 见5.1, the short-sector allocation table, 见5.2, and the directory, 见7)的SID链。SAT的大小(SID个数)就等于复合文档中所存在的sector的个数。


       SAT的建立就是通过按顺序读取MSAT中指定的sector中的内容。


存放SAT的sector的内容:(s_size表示sector的大小)


Offset            Size               Contents                             


0                   s_size            SAT的s_size / 4个SID的数组


                                                                 


       当通过SAT为一个流创建SID链时,SAT数组的当前位置(array index)表示的就是当前的sector,而该位置存放的SID则指向下一个sector。


       SAT可能在任意位置包含Free SID(-1),这些sector将不被流使用。如果该位置包含End Of Chain SID(-2)表示一个流的结束。如果sector用于存放SAT则为SAT SID(-3),同样用于存放MSAT则为MSAT SID(-4)。


       一个SID链的起点从用户流的目录入口(directory entry,见6.2节)或头(内部控制流)或目录流本身获得。


例:一个复合文档包含一个用于存放SAT的sector(sector 1)和2个流。


Sector 1的内容如下图:


 






 


在位置1其值为-3,表明Sector 1是SAT的一部分。


其中一个流为内部目录流,假定头中指定其开始为Sector 0,SAT中位置0的值为2,位置2的值为3,位置3 的值为-2。因此目录流的SID链为[0, 2, 3, –2],即此目录流存放于3个sector中。


目录中包含一个用户流的入口假定为sector 10,从图中可看出此流的SID链为[10, 6, 7, 8, 9, –2]。


 


第五章    短流


5.1 短流存放流


 


当一个流的大小小于指定的值(在头中指定),就称为短流(short-stream)。


短流并不是直接使用sector存放数据,而是内含在一种特殊的内部控制流——短流存放流(short-stream container stream)中。


       短流存放流象其他的用户流一样:先从目录中的根仓库入口(root storage entry)获得第一个使用的sector,其SID链从SAT中获得。然后此流将其所占用的sectors分成short-sector,以便用来存放短流。此处也许较难理解,我们来打个比方:既然流组成符合文档,而短流组成短流存放流,这两者是相似的。把短流存放流当作复合文档,那么短流对应流,short-sector对应sector,唯一的不同是复合文档有一个头结构,而短流存放流没有。short-sector的大小在头中已经指定,因此可根据SID计算short-sector相对于短流存放流的偏移量(offset)。公式为:


short_s_pos(SID) = SID ∙ short_s_size = SID ∙ 2 sssz


例:sssz = 6 and SID = 5:


short_s_pos(SID) = SID ∙ 2 sssz = 5 ∙ 26 = 5 ∙ 64 = 320.


 


5.2 短扇区配置表


 


       短扇区配置表(SSAT:short-sector allocation table)是一个SID数组,包含所有短流的SID链。与SAT很相似。


       用于存放SSAT的第一个sector的SID在头中指定,其余的SID链从SAT中获得。


存放SSAT的sector的内容:(s_size表示sector的大小)


Offset           Size               Contents                                   


0                   s_size            SSAT的s_size / 4个SID的数组


                                                                


 


SSAT的用法与SAT类似,不同的是其SID链引用的是short-sector。


 


第六章    目录


 


 


6.1 目录结构


 


 


目录(directory)是一种内部控制流,由一系列目录入口(directory entry)组成。每一个目录入口都指向复合文档的一个仓库或流。目录入口以其在目录流中出现的顺序被列举,一个以0开始的目录入口索引称为目录入口标识(DID: directory entry identifier)。


如下图所示:


DIRECTORY ENTRY 0


DIRECTORY ENTRY 1


DIRECTORY ENTRY 2


DIRECTORY ENTRY 3





       目录入口的位置不因其指向的仓库或流的存在与否而改变。如果一个仓库或流被删除了,其相应的目录入口就标记为空。在目录的开始有一个特殊的目录入口,叫做根仓库入口(root storage entry),其指向根仓库。


       目录将每个仓库的直接成员(仓库或流)放在一个独立的红黑树(red-black tree)中。红黑树是一种树状的数据结构,本文仅简单介绍一下,详细情况请参考有关资料。


建构一个Red-Black tree的规则:


1. 每个节点(node)的颜色属性不是红就是黑。


2. 根节点一定是黑的。


3. 如果某个节点是红的,那它的子节点一定是黑的。


4. 从根节点到每个叶节点的路径(path)必须有相同数目的黑节点。


  ex:                 B               (用图形来解说第4点,从根节点


                   /     /             到最底层的node,你会发现每个


                  B       B            path都恰好有3个black node)


                 / /    /   /        


                B   B  R     B


               /      / /   / /


              R      B   B R   R


                    / /


                   R   R


注意并不总是执行上述规则。安全的方法是忽略节点的颜色。


例:以第一章中的图为例


1.根仓库入口描述根仓库,它不是任何仓库入口的成员,因此无需构建红黑树。


2.根仓库的所有直接成员(“Storage1”, “Storage2”, “Stream1”, “Stream2”, “Stream3”, 和 “Stream4”)将组成一棵红黑树,其根节点的DID存放于根仓库入口中。


3.仓库Storage1只有一个成员Stream1,Stream1构成一棵红黑树,此树只有一个节点。Storage1的目录入口包含Stream1的DID。


4. 仓库Storage2包含3个成员“Stream21”, “Stream22”, 和“Stream23”。这3个成员将构建一棵红黑树,其根节点的DID存放于Storage2的目录入口中。


 


这种存放规则将导致每个目录入口都包含3个DID:


1.在包含此目录入口的红黑树中,此目录入口的左节点的DID。


2.在包含此目录入口的红黑树中,此目录入口的右节点的DID。


3.若此目录入口表示一个仓库,则还包含此仓库的直接成员所组成的另一颗红黑树的根节点的DID。


在构建红黑树的过程中,一个节点究竟作为左还是右,是通过比较其名字来判断的。一个节点比另一个小是指其名字的长度更短,如长度一样,则逐字符比较。


规定:左节点<根节点<右节点。


 


6.2 目录入口


 


       一个目录入口的大小严格地为128字节,计算其相对目录流的偏移量的公式为:dir_entry_pos(DID) = DID ∙ 128。


目录入口的内容:


Offset    Size        Contents                                                  


0            64          此入口的名字(字符数组), 一般为16位的Unicode字符,


以0结束。(因此最大长度为31个字符)


64          2            用于存放名字的区域的大小,包括结尾的0。


(如:一个名字右5个字符则此值为(5+1)∙2 = 12)


66          1            入口类型: 00H = Empty       03H = LockBytes (unknown)


01H = User storage       04H = Property (unknown)         02H = User stream 05H = Root storage


67          1            此入口的节点颜色: 00H = Red    01H = Black


68          4            其左节点的DID (若此入口为一个user storage or stream)    若没有左节点就为-1。


72          4            其右节点的DID (若此入口为一个user storage or stream),    若没有右节点就为-1。


76          4            其成员红黑树的根节点的DID (若此入口为storage), 其他为-1。


80          16          唯一标识符(若为storage)(不重要, 可能全为0)


 


96          4            用户标记(不重要, 可能全为0)


100        8            创建此入口的时间标记。大多数情况都不写。


108        8            最后修改此入口的时间标记。大多数情况都不写。


116            4            若此为流的入口,指定流的第一个sector或short-sector的SID,若此为根仓库入口,指定短流存放流的第一个sector的SID,其他情况,为0。  120        4             若此为流的入口,指定流的大小(字节)若此为根仓库入口,指定短流存放流的大小(字节)其他情况,为0。


124        4            Not used


时间标记(time stamp) 是一个符号的64位的整数,表示从1601-01-01 00:00:00开始的时间值。此值的单位为10-7秒。


当计算时间标记是要注意闰年。


例:时间标记值为01AE408B10149C00H


计算步骤                           公式                                  结果                  


转为十进制                                 t0 =  121,105,206,000,000,000


化成秒的余数                     rfrac = t0 mod 107              rfrac = 0


化成秒的整数                    t1 = t0 / 107                        t1 =  12,110,520,600


化成分的余数                     rsec = t1 mod 60                 rsec = 0


化成秒的整数                     t2 = t1 / 60                         t2 =  201,842,010


化成小时的余数           rmin = t2 mod 60                rmin = 30


化成小时的整数          t3 = t2 / 60                         t3 =  3,364,033


化成天的余数                     rhour = t3 mod 24               rhour = 1


化成天的整数                     t4 = t3 / 24                         t4 =  140,168


距1601-01-01的整年 ryear = 1601 + t4含的年     ryear = 1601 + 383 = 1984


到1984年还剩的天数  t5 = t4 – (1601-01-01           t5 = 140,168 – 139,887 = 281


到1984-01-01的天数)


距1984-01-01的月数 rmonth = 1 + t5含的月数    rmonth = 1 + 9 = 10


到10月还剩的天数             t6 = t5 – (1984-01-01           t6 = 281 – 274 = 7


到1984-10-01的天数)


10月最终天数            rday = 1 + t6                       rday = 1 + 7 = 8