一. 仓库与流
1. 什么是复合文档?
复合文档(Compound Document) 是一种不仅包含文本而且包括图形、电子表格数据、声音、视频图象以及其它信息的文档。可以把复合文档想象成一个所有者,它装着文本、图形以及多媒体信息如 声音和图象。目前建立复合文档的趋势是使用面向对象技术,在这里,非标准信息如图像和声音可以作为独立的、自包含式对象包含在文档中。Microsoft Windows就是使用这种技术,叫做“OLE2 storage file format”或“Microsoft Office compatible storage file format”。
当然Excel、Word等都是用这种格式存储的。本文主要研究复合文档的二进制结构。
2. 仓库与流
复合文档的原理就像一个文件系统(文件系统:如FAT与NTFS)。复合文档将数据分成许多流(Streams),这些流又存储在不同的仓库(Storages)里。将复合文档想象成你的D盘,D盘用的是NTFS(NT File System)格式,流就相当于D盘里的文件,仓库就相当于D盘里的文件夹。
关系如下图:
二. 扇区与扇区链
1. 扇区与扇区标识
所有的流又分成更小的数据块,叫做数据扇区(sectors)。Sectors 可能包含控制数据或用户数据。
整个文件由一个头(Header)结构以及其后的所有Sectors组成。Sectors的大小在头中确定,且每个Sectors的大小都相同。
大概是这种格式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17HEADER (包含下面Sector的个数,大小信息)
SECTOR 0 (以下的所有sector大小都一致)
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链的结束标记 (见”扇区链与扇区标识链”) |
–3 | SAT SID | 此Sector用于存放扇区配置表(SAT)(见”扇区配置表”) |
–4 | MSAT SID | 此Sector用于存放主扇区配置表(MSAT)(见”主扇区配置表”) |
2. 扇区链与扇区标识链
用于存储流数据的所有Sectors的列表叫做扇区链(Sector Chain)。这些Sectors可以是无序的。因此用于指定一个流的Sectors的顺序的SID数组就称为SID chain。一个SID chain总是以End Of Chain SID(-2)为结束标记。
例:一个流由4个Sector组成,其SID链为[1, 6, 3, 5, –2]
三. 复合文档头
1. 复合文档头(Header)
复合文档头在文件的开始,且其大小必定为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 | 字节顺序规则标识::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的大小,以2的幂形式存储, short-sector实际大 小为s_s_size = 2sssz 字节(一般为6即64字节,最大为sector的大小) |
34 | 10 | Not used |
44 | 4 | 用于存放扇区配置表(SAT)的sector总数 |
48 | 4 | 用于存放目录流的第一个sector的SID |
52 | 4 | Not used |
56 | 4 | 标准流的最小大小(一般为4096 bytes), 小于此值的流即为短流。 |
60 | 4 | 用于存放短扇区配置表(SSAT)的第一个sector的SID,或为–2 (End Of Chain SID)如不存在。 |
64 | 4 | 用于存放短扇区配置表(SSAT)的sector总数 |
68 | 4 | 用于存放主扇区配置表(MSAT)的第一个sector的SID,或为–2 (End Of Chain SID) 若无附加的sectors。 |
72 | 4 | 用于存放主扇区配置表(MSAT)的sector总数 |
76 | 436 | 存放主扇区配置表(MSAT)的第一部分,包含109个SID。 |
2. 扇区偏移量计算方法
- sector_pos: 扇区的位置
- SID: 扇区的索引
- ssz: header中的每个sector大小
1 | sector_pos[SID] = 9^2 + SID * ssz^2 |
例:ssz = 10 and SID = 5:
1
2
3sector_pos[SID] = 9^2 + 5 * 10^2
等于:
sector_pos[SID] = 512 + 5 * 1024
四. 扇区配置
1. 主扇区配置表
- 主扇区配置表(MSAT:master sector allocation table)是一个SID数组,指明了所有用于存放扇区配置表(SAT:sector allocation table)的sector的SID。MSAT的大小(SID个数)就等于存放SAT的sector数,在头中指明。
MSAT的前109个SID也存放于复合文档头(Header)中,如果一个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)。
2. 扇区配置表
扇区配置表(SAT:sector allocation table)是一个SID数组,包含所有用户流(短流除外)和内部控制流(the short-stream container stream, the short-sector allocation table, and the directory)的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节)或头(内部控制流)或目录流本身获得。
五. 目录
1. 目录结构
目录(directory)是一种内部控制流,由一系列目录入口(directory entry)组成。每一个目录入口都指向复合文档的一个仓库或流。目录入口以其在目录流中出现的顺序被列举,一个以0开始的目录入口索引称为目录入口标识(DID: directory entry identifier)。
如下图所示:
1
2
3
4
5
6
7
8
9DIRECTORY ENTRY 0
DIRECTORY ENTRY 1
DIRECTORY ENTRY 2
DIRECTORY ENTRY 3
...目录入口的位置不因其指向的仓库或流的存在与否而改变。如果一个仓库或流被删除了,其相应的目录入口就标记为空。在目录的开始有一个特殊的目录入口,叫做根仓库入口(root storage entry),其指向根仓库。
目录将每个仓库的直接成员(仓库或流)放在一个独立的红黑树(red-black tree)中。红黑树是一种树状的数据结构,本文仅简单介绍一下,详细情况请参考有关资料。
建构一个Red-Black tree的规则:
每个节点(node)的颜色属性不是红就是黑。
根节点一定是黑的。
如果某个节点是红的,那它的子节点一定是黑的。
从根节点到每个叶节点的路径(path)必须有相同数目的黑节点。
例:以第一章中的图为例
- 根仓库入口描述根仓库,它不是任何仓库入口的成员,因此无需构建红黑树。
- 根仓库的所有直接成员(“Storage1”, “Storage2”, “Stream1”, “Stream2”, “Stream3”, 和 “Stream4”)将组成一棵红黑树,其根节点的DID存放于根仓库入口中。
- 仓库Storage1只有一个成员Stream1,Stream1构成一棵红黑树,此树只有一个节点。Storage1的目录入口包含Stream1的DID。
- 仓库Storage2包含3个成员“Stream21”, “Stream22”, 和“Stream23”。这3个成员将构建一棵红黑树,其根节点的DID存放于Storage2的目录入口中。
这种存放规则将导致每个目录入口都包含3个DID:
- 在包含此目录入口的红黑树中,此目录入口的左节点的DID。
- 在包含此目录入口的红黑树中,此目录入口的右节点的DID。
- 若此目录入口表示一个仓库,则还包含此仓库的直接成员所组成的另一颗红黑树的根节点的DID。
在构建红黑树的过程中,一个节点究竟作为左还是右,是通过比较其名字来判断的。一个节点比另一个小是指其名字的长度更短,如长度一样,则逐字符比较。
规定:左节点<根节点<右节点。
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 |