您的位置:首页 > 其它

关于类似EXCEL文档存储方式的读书心得

2012-06-21 11:56 288 查看
近几天在网上查阅了一些关于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的话),依此循环取得所有目录
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: