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

   近几天在网上查阅了一些关于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的话),依此循环取得所有目录

posted on 2012-06-21 11:56  悠竹客  阅读(1508)  评论(0编辑  收藏  举报

导航