复合二进制文档 - 基本概念(上篇)

136 阅读9分钟

复合二进制文档 - 基本概念(上篇)

系列文章分为上中下三篇,教你怎么使用纯js解析复合二进制文档(doc,ppt,msi)

什么是COM结构化存储详解

COM结构化存储(Structured Storage)是微软提供的一种复合文档存储技术,它允许将多个独立的数据流和存储对象组织在一个文件中,就像文件系统中的文件和目录一样。

核心概念

存储(storage)
  • 类似于文件系统中的目录
  • 可以包含其他存储对象和流对象
  • 用于组织数据的层次结构
流 (stream)
  • 类似于文件系统中的文件
  • 包含实际的数据内容
  • 是存储的基本单元
技术特点
  • 事务支持:支持事务性操作,可以回滚更改
  • 增量保存:只保存修改的部分,提高效率

复合二进制文档(CFB)

复合二进制文档是COM结构化存储的一种实现,常用的文件如:.doc,.xls,.msi都是标准的复合二级制文档格式;

  • tips: doc和docx的区别:两者是两种不同的数据格式,doc是复合二进制格式,docx是基于xml的二进制格式;
扇区

扇区(sectors)是流的组成单元,一个流由多个扇区按照一定顺序组成。组成一个流的多个扇区叫做扇区链。 扇区的大小是固定的,而且任意扇区的大小都是一样的; 扇区按简单顺序列举,一个扇区的索引(从0开始)叫做扇区标识(SID:sector identifier)。SID是一个有符号的32位(4字节)的整型值。 扇区索引值存在一些特殊值,表示不同的含义:

  • -1 : 空闲扇区,可存在于文件中,但不是任何流的组成部分。
  • -2 : 扇区链的结束标记。
  • -3 : 此扇区用于存放扇区配置表(SAT)。
  • -4 : 此扇区用于存放主扇区配置表(MSAT)。
扇区链与扇区标识链

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

流的扇区标识是通过扇区配置表构建的,但短流和以下两种内部流除外: 1.主扇区配置表,其从自身构建SID链(每个扇区包含下一个扇区的SID)。 2.扇区配置表,其通过主扇区配置表构建SID链

扇区配置表

扇区配置表(SAT)是一个SID数组,记录了所有流的SID链及SID排列顺序。 SAT的大小(SID个数)就等于复合文档中所存在的sector的个数,也就是说扇区个数可以这样计算:(表示扇区配置表的扇区数量 * 扇区大小 )/ 4,表示扇区配置表的扇区数量和SID可以从主扇区配置表中得到。

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

SAT可能在任意位置的值为-1(未被引用),这些扇区将不被流使用。如果该位置的值是-2表示一个流的结束。如果扇区用于存放SAT则为SAT SID(-3),同样用于存放MSAT则为MSAT SID(-4)。

一个SID链的起点从用户流的目录入口 或头文档 或目录流本身获得。

主扇区配置表

主扇区配置表(MSAT)是一个扇区标识数组,存放了所有用于存放扇区配置表(SAT)的扇区的SID。 MSAT的前109个SID也存放于文档头(Header)中,在头文档的offset=76开始(前文已经说明,明个SID占4字节,所以109个SID的大小为436字节) 如果一个MSAT的SID数多余109个,那么多出来的SID将存放于sector中,文档头中已经指明了用于存放MSAT的第一个sector的SID。

存放MSAT的扇区的内容(假定扇区大小为 n 字节,则可以存放(n-4/4)个SID),那么:

offsetsizecontent
0n-4SID数组(数量为(n-4)/4)
n-44下一个扇区的SID

复合文档头

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

复合文档头包含了一些特定数据:

偏移量(Offset)大小(Size)内容(Contents)
08复合文档文件标识:D0H CFH 11H E0H A1H B1H 1AH E1H
816此文件的唯一标识(不重要, 可全部为0)
242文件格式修订号 (一般为003EH)
262文件格式版本号(一般为0003H)
282字节顺序规则标识(大端与小端):FEH FFH = Little-Endian,FFH FEH = Big-Endian
302复合文档中sector的大小(ssz),以2的幂形式存储
322short-sector的大小,以2的幂形式存储
3410Not used
444用于存放扇区配置表(SAT)的sector总数
484用于存放目录流的第一个sector的SID
524Not used
564标准流的最小大小(一般为4096 bytes),小于此值的流即为短流。
604用于存放短扇区配置表(SSAT)的第一个sector的SID,或为–2 (End Of Chain SID)如不存在。
644用于存放短扇区配置表(SSAT)的sector总数
684用于存放主扇区配置表(MSAT)的第一个sector的SID,或为–2 (End Of Chain SID) 若无附加的sectors。
724用于存放主扇区配置表(MSAT)的sector总数
76436存放主扇区配置表(MSAT)的第一部分,包含109个SID。

短流

当一个流的大小小于某个阈值(在复合文档头中指定),则把这个流成为短流;

短流并不是使用常规数据扇区来存储,而是集中存放在一个特殊的短流存放容器中,这个容器本身是一个常规的扇区。

短流的存储结构

短流数据存放在短流池; 所有短流的数据都连续或链式存储在一个专门的短流池中。 短流池本身是一个常规流(大小不受 4096 字节限制),但内部存储的是多个短流数据块。

短流使用短扇区(Short Sector)存储: 短流的存储单元是短扇区(Short Sector),通常大小为 64 字节,在复合文档头中指定。

短流的管理方式: 短扇区分配表(SSAT):记录短流数据的存储位置,类似于 FAT(文件分配表),但仅管理短流。 短流目录项(Directory Entry) 中会标记该流是否为短流(DIR_ENTRY::_mse = STGTY_STREAM + 大小 < 阈值)。

短流的读取流程

检查目录项(Directory Entry): 如果 streamSize < 4096(或其他阈值),则标记为短流。

定位短流池(Short Stream Container): 从根存储找到短流池的位置(通常是一个常规流)。

查询短扇区分配表(SSAT): 根据 SSAT 找到短流在短流池中的存储位置。

读取短扇区链: 短流可能占用多个短扇区,通过 SSAT 链式读取。

目录(storage)

目录(directory)是一种内部控制流,由一系列目录入口(directory entry)组成。

目录结构
目录入口

一个目录入口的大小严格地为128字节,计算其相对目录流的偏移量的公式为:dir_entry_pos(DID) = DID ∙ 128。目录入口的内容; 它的二进制结构如下:

偏移量 (Offset)大小 (Size)内容 (Contents)
064入口名称,一般为16位的Unicode字符,以0结束
642用于存放名字的区域的大小,包括结尾的0
661入口类型:unknown = 0,storage = 1,stream = 2,rootStorage = 5,
671此入口的节点颜色:00H = Red,01H = Black
684其左节点的DID(若此入口为一个 storage或stream),若没有左节点就为-1。
724其右节点的DID(若此入口为一个 storage或stream),若没有右节点就为-1。
764其成员红黑树的根节点的DID(若此入口为storage),其他为-1。
8016唯一标识符(若为storage)(不重要,可能全为0)
964用户标记
1008创建此入口的时间标记。大多数情况都不写。
1088最后修改此入口的时间标记。大多数情况都不写。
1164若此为流的入口,指定流的第一个扇区或短扇区的SID;若此为根storage入口,指定短流存放流的第一个sector的SID;其他情况,为0。
1204若此为流的入口,指定流的大小(字节);若此为根仓库入口,指定短流存放流的大小(字节);其他情况,为0。
1244Not used

总结

行文至此,做一个小总结:

  • 复合二进制文档的二进制结构可以笼统的分为两个部分:复合文档头(固定512字节)和各种扇区(每个扇区的大小一致);
  • 复合文档头指定了各种特定数据,如:扇区大小,短扇区大小,字节顺序规则标识,主扇区配置的钱109个扇区标识;
  • 主扇区配置是一个扇区标识数组,扇区标识指定的扇区专门保存扇区配置,前109保存在文档头中;当超出后则保存在专门的扇区中;保存主扇区配置的扇区格式前文已经讲解了;
  • 扇区配置表也是一个扇区标识数组array;数组下标i代表当前扇区的扇区标识,数组值array[i]标识扇区链的下一个扇区;当值为-2时表示尾结点;
  • 目录则记录各个目录和流间的层级关系;

参考文档

CFBF 描述:sc.openoffice.org/compdocfile…