近几天在网上查阅了一些关于EXCEL文档的存储格式的文章,总结出该类文档存储的组织方式,对于文件内的流(如Workboot,sheet)等的存储格式(BIFF格式)还在研究中,现仅将该类文档的流组织心得贴出来.请大家探讨一下:
复合文档(Compound Document)存储的数据结构
一、文件存储结构
该文件存储组织方式是:起始有512字节的文件头,然后是固定长度的n个扇区。
文件头 扇区(SID=0) 扇区(SID=1) 扇区(SID=2) 扇区(SID=3)………扇区(SID=n-1) 扇区(SID=n)
文件头包含了一些重要的信息,如扇区大小、扇区分配等等信息。因此该文件的长度总是512+扇区*扇区大小。
二、文件头结构(总长度=512字节)
public struct CD_Header
{
/// <summary>
/// 8字节,存储复合文档文件标识:D0H CFH 11H E0H A1H B1H 1AH E1H
/// </summary>
public long CD_Flag;
/// <summary>
/// 8字节,此文件的唯一标识(不重要, 可全部为0)
/// 同FileID2一起使用
/// </summary>
public long FileID;
/// <summary>
/// 8字节,此文件的唯一标识(不重要, 可全部为0)
/// 同FileID一起使用
/// </summary>
public long FileID2;
/// <summary>
/// 2字节,文件格式修订号 (一般为003EH)
/// </summary>
public short MinorVersion;
/// <summary>
/// 2字节,文件格式版本号(一般为0003H)
/// </summary>
public short MajorVersion;
/// <summary>
/// 2字节,字节顺序规则标识:FEH FFH = Little-Endian,FFH FEH = Big-Endian
/// </summary>
public short ByteOrder;
/// <summary>
/// 2字节,复合文档中扇区(sector)的大小(ssz),以2的幂形式存储, sector实际大小为s_size
/// = 2^ssz 字节(一般为9即512字节, 最小值为7即128字节)
/// </summary>
public short SectorSize;
/// <summary>
/// 2字节,short-sector的大小,以2的幂形式存储, short-sector实际大
/// 小为s_s_size = 2^sssz 字节(一般为6即64字节,最大为sector的大小)
/// </summary>
public short ShortSectorSize;
/// <summary>
/// 以下10字节未使用,设为0
/// Not used
/// </summary>
public short NotUsed1;
/// <summary>
/// Not used
/// </summary>
public short NotUsed2;
/// <summary>
/// Not used
/// </summary>
public short NotUsed3;
/// <summary>
/// Not used
/// </summary>
public short NotUsed4;
/// <summary>
/// Not used
/// </summary>
public short NotUsed5;
/// <summary>
/// 4字节 用于存放扇区配置表(SAT:sector allocation table)的sector总数
/// </summary>
public int SAT_SectorCount;
/// <summary>
/// 4字节,用于存放目录流的第一个sector的SID
/// </summary>
public int DirStreamFisrtSectorID;
/// <summary>
/// Not used
/// 4字节
/// </summary>
public int NotUsed6;
/// <summary>
/// 4字节,标准流的最小大小(一般为4096 bytes), 小于此值的流即为短流
/// </summary>
public int StandStreamSize;
/// <summary>
/// 4字节,用于存放短扇区配置表(SSAT)的第一个sector的SID ,
/// 或为–2 (End Of Chain SID)如不存在
/// 其SID链从SAT表中获取
/// </summary>
public int SSAT_FirstSectorID;
/// <summary>
/// 4字节,用于存放短扇区配置表(SSAT)的sector总数
/// </summary>
public int SSAT_SectorCount;
/// <summary>
/// 4字节,用于存放主扇区配置表(MSAT)的第一个sector的SID
/// 或为–2 (End Of Chain SID) 若无附加的sectors
/// 因为在头中已存在109个存储SAT表的SID,因此该字段可能为-2,
/// 除非SAT表的数量大于109个
/// </summary>
public int MSAT_FirstSectorID;
/// <summary>
/// 4字节,用于存放主扇区配置表(MSAT)的sector总数
/// 如果MSAT_FirstSectorID=-2则该字段为0
/// </summary>
public int MSAT_SectorCount;
/// <summary>
/// 109*4字节(436字节),存储扇区配置表(MSAT)的sector ID
/// </summary>
[MarshalAs(UnmanagedType.SafeArray, SizeConst = 436,
SafeArraySubType = VarEnum.VT_INT)]
public int[] SAT_SectorID;
}
三、数据存储的组织方式
该文件的所有数据都存储在一系列的扇区内,而存储使用到的扇区可以是不连续的,使用到的扇区通过SAT(扇区配置表,sector allocation table)查找SID链,扇区配置表也存储在指定的扇区中,它的SID通过主扇区配置表(MSAT:master sector allocation table)表查找,文件头中就存储部份MSAT的内容,如果SAT的个数大于109个,则其它SAT的SID则要通过文件头中的MSAT_FirstSectorID字段来查找MSAT表而获得。
主扇区配置表(MSAT):存储了一系列存有扇区配置表(SAT)的扇区ID,该表以4字节为分段,每分段存储了一个SID,而该SID指向的扇区存储有一个扇区配置表(SAT),该表的前面109个分段存储在文件头中的SAT_SectorID数组中。如果SAT表的个数超过了109个,则在文件头中MSAT_FirstSectorID存储有指向MSAT表的扇区ID,该表可存的SAT表的个数=(扇区大小/4)-1,最后一个分段用于存储下一个MSAT表的SID,可用-2指示结束。
扇区配置表(SAT):一个扇区配置表可以存储的SID个数=扇区大小/4,存储的SID是指向下一个扇区的ID,多个SAT表组成全局SAT表,即一个SID数组,数组的索引代表了当前的SID,而其内容则指向下一个SID,这样就组成了一个SID链,该链中SID所引用的扇区组成一个存储流来存储数据内容。
SID的值=–1 Free SID 空闲sector,可存在于文件中,但不是任何流的组成部分
–2 End Of Chain SID SID链的结束标记
–3 SAT SID 此Sector用于存放扇区配置表(SAT)
–4 MSAT SID 此Sector用于存放主扇区配置表(MSAT)
>=0 SID 此扇区的索引
四、扇区配置表及其SID链的构造
1、扇区配置表(SAT)的SID链
从文件头中的SAT_SectorID数组中依序加载SID,当碰到Free SID时停止加载,如果加载结果等于109,则转到MSAT_FirstSectorID(SID)指向的扇区(如果该SID>=0)以4字节为一个SID分段继续加载,同样以碰到Free SID时停止加载,对于最后一个分段判断其是否为End Of Chain SID,如果是则停止加载,否则转到该SID继续加载。其具体流程祥见扇区配置表的SID链获取流程图.
2、扇区配置表(SAT)
由(1)中获取的SID链所引用的扇区中存储的内容就是扇区配置表(SAT),该表存储了一系列的SID链,这些SID链构成了数据存储区所使用的扇区的情况,该表以4字节为一个分段,每个分段代表一个从零开始的扇区索引(SID),而每个分段的内容则存储了另一个SID,它指明该分段代表的扇区的指向的下一个扇区的SID,用End Of Chain SID指示没有下一个扇区,用Free SID指示该分段代表的扇区未使用。整个SAT应该可以用一个SID数组来表示:SID_Array,数组的大小指明所有扇区的个数,数组的索引即是该扇区的SID,扇区的字节大小由文件头的SectorSize给出,而数组的内容含义如下:
>=0 该扇区指向的下一个扇区的SID,由此可以看出,指定一个SID,即可组成该SID所代表的所有扇区的一个SID链。
=–1 Free SID 空闲sector,可存在于文件中,但不是任何流的组成部分
=–2 End Of Chain SID SID链的结束标记
=–3 SAT SID 此Sector用于存放扇区配置表(SAT)
=–4 MSAT SID 此Sector用于存放主扇区配置表(MSAT)
3、扇区偏移量的计算
给定一个扇区的SID可以计算出该扇区在整个文件中的偏移量
Sector_POS(SID)=HeaderSize+SID*SectorSize。
HeaderSize:文件头的长度(512字节)。
SectorSize:扇区长度(由文件头的SectorSize给出)
五、短扇区配置表(SSAT)及短流存储区
短流存储区是这样定义的:存储所有流长度小于文件头中StandStreamSize字段的流,它的存储区域是由一个SID并从SAT表中查到的SID链所引用的扇区组成的流,然后它将该流分成ShortSectorSize大小的扇区,再由SSAT表构建每个短流的SID链。短流存储区的扇区长度由头文件中的ShortSectorSize字段给出,短流存储区的SID链是由从根目录获得的短流存储区的其始SID并查找SAT表获得的。
从文件头的字段SSAT_FirstSectorID获得起始SID,然后查询SAT表构成SSAT表的SID链,由该SID链所引用的扇区组成的流即存储了SSAT的内容,它的含义同SAT表的含义类似,只不过其中所指的SID是SSID,即短流区的扇区ID。
六、目录
目录(directory)是一种内部控制流,由一系列目录入口(directory entry)组成。每一个目录入口都指向复合文档的一个仓库或流。目录入口以其在目录流中出现的顺序被列举,一个以0开始的目录入口索引称为目录入口标识(DID: directory entry identifier)。
目录入口的位置不因其指向的仓库或流的存在与否而改变。如果一个仓库或流被删除了,其相应的目录入口就标记为空。在目录的开始有一个特殊的目录入口,叫做根仓库入口(root storage entry),其指向根仓库。
目录将每个仓库的直接成员(仓库或流)放在一个独立的红黑树(red-black tree)中。红黑树是一种树状的数据结构。
一个目录入口的大小严格地为128字节,计算其相对目录流的偏移量的公式为:dir_entry_pos(DID) = DID * 128。
目录的SID链:由文件头中的DirStreamFisrtSectorID字段获得起始SID,查询SAT表获得存储目录的SID链。由该链所引用的扇区组成存储目录的流。
目录入口结构如下:
public struct CD_DirectoryEntry
{
/// <summary>
/// 64字节 此入口的名字(字符数组), 一般为16位的Unicode字符,
/// 以0结束。(因此最大长度为31个字符)
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
public string DirectoryName;
/// <summary>
/// 2字节 用于存放名字的区域的大小,包括结尾的0
/// </summary>
public short DirectoryNameSize;
/// <summary>
/// 1字节 入口类型:
/// 00H = Empty
/// 01H = User storage
/// 02H = User stream
/// 03H = LockBytes (unknown)
/// 04H = Property (unknown)
/// 05H = Root storage
/// </summary>
public byte DirectoryEntryType;
/// <summary>
/// 1字节 此入口的节点颜色: 00H = Red 01H = Black
/// </summary>
public byte RBT;
/// <summary>
/// 4字节 其左节点的DID (若此入口为一个user storage or stream)
/// 若没有左节点就为-1。
/// </summary>
public int LeftDID;
/// <summary>
/// 4字节 其右节点的DID (若此入口为一个user storage or stream),
/// 若没有右节点就为-1。
/// </summary>
public int RightDID;
/// <summary>
/// 4字节 其成员红黑树的根节点的DID (若此入口为storage), 其他为-1。
/// </summary>
public int RootDID;
/// <summary>
/// 16字节 组成唯一标识符(若为storage)(不重要, 可能全为0)
/// </summary>
public Guid StorageID;
/// <summary>
/// 4字节 用户标记(不重要, 可能全为0)
/// </summary>
public int UserFlag;
/// <summary>
/// 8字节 创建此入口的时间标记。大多数情况都不写
/// </summary>
public DateTime CreateTime;
/// <summary>
/// 8字节 最后修改此入口的时间标记。大多数情况都不写
/// </summary>
public DateTime LastChangedTime;
/// <summary>
/// 4字节 若此为流的入口,指定流的第一个sector或short-sector的SID,
/// 若此为根仓库入口,指定短流存放流的第一个sector的SID,其他情况,为0
/// </summary>
public int FirstStreamSID;
/// <summary>
/// 4字节 若此为流的入口,指定流的大小(字节)
/// 若此为根仓库入口,指定短流存放流的大小(字节)其他情况,为0。
/// </summary>
public int StreamLength;
/// <summary>
/// 4字节 Not used
/// </summary>
public int NotUsed;
}
第一个目录入口(DID=0)是根入口,该入口RootDID指明了目录入口,根据RootDID计算出其在目录流中的偏移量后取取该目录,再由LeftDID或RightDID取出左右目录(如果不为-1的话),依此循环取得所有目录