(一)
前言
原创:弄潮小狮
一直想要给自己的项目加上将报表所见即所得的转存为本地电子表格(Excel),从前只是想让自己的系统看起来华丽一点,就没有多大的动力去做,其实也找过一些资料,就是懒的做。
最近,终于迫于客户需求,不得不做了……
关于这个功能,网络上有不少可以直接“拿来”的,有第三方控件,也有开源代码(譬如:DW2Excel等等)。
由来,小狮我对于学习代码这种事情是相当头疼的,更主要的是,干这活属于商业行为,直接“拿来”恐怕有点问题,所以还是决定自己动手,还有一点,自己的代码,维护起来也还不至于太被动。
于是高尚一下,叫做“崇尚原创”。
在PB里面,数据窗口本来提供了Saveas方法,可以直接导出数据到Excel文件,但仅限于数据窗口的数据源,要做到“所见即所得”,那就相当困难,甚至不可能了。
在PB里面,也可以通过DDE或者OLE的方法(这一段就不抄了),直接通过Excel编写文件,达到“所见即所得”,但这又得依赖于运行环境,如果相关的机器没有安装Excel,就无法实现。
而且,我自己也用过用OLE的方法实现相关功能的系统,感受之下,对这种方法的稳定性及可操作性很不放心。
基于以上原因,为了让自己的系统更具独立性,决定从零开始,自己来。
那首先得找点资料……
网络上,有一篇流传颇广的叫做《Excel文件格式》的资料,是英文的,老实说,这篇资料对我关于“Excel文件格式”的理解,起到启蒙的作用,但是,进一步老实说,这篇资料对于我的最终工作结果,几乎起不到任何作用。
这篇被广泛如获至宝收藏的资料,是基于BIFF2的,BIFF的意思是Binary Interchange File Format(二进制交换文件格式),BIFF2版本的Excel文件并不支持“合并单元格”,这对于我的工作是个致命的打击,无法“合并单元格”,谈何“所见即所得”?
所以,我怀疑那些收藏的人,有没有认真的用过这篇资料!
当然包括我,我最早得到这篇资料至今少说也有5年了,也是到了最近实在不得不用的时候,才发现了这个问题。
这也许是人之常情,被我怀疑的同学们不必为了这个跟我急!
既然这样,那就得继续找资料了……
于是找到一篇《excelfileformat》,也是英文的,这一篇很好,从BIFF2-BIFF8,非常详尽的阐述了Excel的文件格式,老外们在这方面的态度还是比较认真的,值得学习。
但是问题又来了,仅BIFF8版本的Excel文件支持“合并单元格”!
这个问题似乎很好解决,按照BIFF8的格式来做不就行了?
当然行,是的,可以按照BIFF8的格式来做。做出来的文件也可以被Excel读取,但必须通过两次崩溃、修复后才能使用。
为什么?!
微软有一个“复合文档格式”,Office从97到2003都采用“复合文档格式”来组织文档,Word、Excel、PowerPoint等等,都采用这种格式来保存。
据说微软最新又推出另一种格式,但是问题不大,于我的工作,“复合文档”已经够用了。
“裸”的BIFF8版本的Excel文件,用Excel来打开,可能导致读写错误,进而导致崩溃,同时Excel的修复功能能够把它修复成“复合文档”。
这是我的理解。
有这样的问题,显然是不能提交给客户的。那么就必须给Excel文件“穿上”“复合文档”的外衣,要搞定Excel文件,必须先搞定复合文档。
因此,“复合文档”成了通向我的工作的第一只拦路虎(为什么说“第一只”?因为后面还有)!必须解决掉。
还得找资料……
于是又找到一篇《compdocfileformat》,与前面提到的《excelfileformat》一同出自一个叫做“OpenOffice”的组织。我想我应该谢谢他们。
值得一提的是,网上有人说这些文档的结构,微软官方并不公开,是一些黑客通过跟踪、分析等等整理出来,不知是真是假。反正很佩服这些人的毅力。
由于我的英文实在太差,有了这些英文的文档,学习起来还是很吃力,就想,会不会有中文资料?
找来找去,找到一份叫做《复合文档格式研究》的资料,作者声称是为了另一篇正在创作的《Excel文件格式研究》准备的。
我为什么没有先搜到这份并不奇怪,因为我在找“Excel文件格式”并不知道“复合文档”……
作者声称这份中文资料为其“创作”,但扫一眼就看出来这是《compdocfileformat》的翻译,而且翻译的水平跟“金山词霸”有得一拼,被人追问“记得保存过类似的资料,所以想问一下,这是你原创还是翻译?”之下,于是退缩为“说原创是不可能的,我既不是微软的工程师,也不是世界顶尖黑客。关于excel内部结构,微软是不会公开的,有关的资料也是国外一些黑客用反编译的方法推测出来的。本文是在翻译的基础上,加了一些其他的内容,但都是自己写的。”
这世道……
这篇《复合文档格式研究》倒是对我的工作起到了很大的帮助作用,我还是得谢谢作者,虽然作者对原文做了大量的删节而不是“加了一些其他的内容”,连章节引用都没改,可见确实“都是自己写的”。
资料齐了,可以动手了。
不知道是因为东西方的文化差异造成的,还是我的基础实在太差,很多地方很难懂,做的过程相当痛苦。
从开始找资料到第一个Excel文件的输出,整整花了三周的时间,折腾之下,也就有了一点心得,于是决定记录下来,那得有个地方记,于是决定重开博客。
对于复合文档,由于并非小狮原创,也谈不上“研究”,于是叫做《复合文档学习笔记》。
一向,我主张不管学术本身如何晦涩,写出来,也要尽量让毫无基础的人能够看懂。
而且我也是个很啰嗦的人,写成文字可能就相当“冗余”。
但我自己又是一个很善于遗忘的人,既然是笔记,那也要预着将来自己把相关知识忘个一干二净以后再来看能看得懂,有利于自己再学习,于是就要一如既往的“啰嗦”了。
本文基本上按照我自己的学习经过来写,重点在我认为难懂的地方阐述自己的见解。
当然,你也可以认为我是在“翻译一些资料”,但我实在不敢用“翻译”二字,问题就因为“我的英文实在太差”,英文原文可能已经很详尽了,我的这一篇也可能是“添足之笔”,但我保证这是我“自己写的”。
原文《compdocfileformat》和《excelfileformat》都是老外牛人的心血,不敢窃为己有,提供链接:
复合文档格式http://sc.openoffice.org/compdocfileformat.pdf、
Excel文件格式http://sc.openoffice.org/excelfileformat.pdf。
如果你只是想了解这些结构,而且基础及英文还行,看原文就可以了。
如果你想要看中文的,而且受得了我的啰嗦,那就看我的。
当然还要有耐心,这是第一篇,我很忙,第二篇什么时候写就不一定了。
但我想我会在忘了这些之前把它写完。
(二)
头部
原创:弄潮小狮
从前文谈及的两份来之OpenOffice的文档看,《compdocfileformat》(复合文档文件格式)只有区区200来KB,《excelfileformat》(Excel文件格式)却超过1MB,看起来,Excel文件的结构似乎比复合文档复杂5倍?实际上,Excel文件格式相当简单,复合文档的文件格式却要复杂的多。
本文很多说法来源或翻译于《compdocfileformat》一文,若无特别说明,本文所指“英文原文”即指该文。
下文提及的“复合文档”均指“微软复合文档”。
摆弄文件格式,于小狮来讲,历史已经不短了,如果也可以说是资历的,那可谓“老”了。
最早应该在15年前,为了在Dos下用TC显示一幅BMP图片,连猜带蒙,搞定了BMP的文件格式,如果那时候知道这样做就能够成为黑客的话,那……
遐想中……
接下来就是几年前学习了一下PE,后来又搞了反汇编,顺带把com文件的格式也摸索了,熟悉我的朋友应该知道,做这些事为了一个“PB神盾”,也只是应用在自己的一些产品上,有朋友跟我说这个东东商机大大的……
又遐想……
摆弄文件结构,需要一些知识准备:字节、字、整数、浮点数、结构体等等,还有数制转换、ANIS、Unicode等等……
我相信很多人对这些知识已经滚瓜烂熟了,嗤之以鼻、不屑一谈。
曾经有牛人由于太精通PB,以至于把硬盘文件分为“二进制文件”(或者流文件)及“文本文件”,有鉴于此,为了准备让基础薄弱如小狮者能够理解本文,是有必要就有关知识啰嗦啰嗦的。
但本文又不是要讲这些,只能有机会另外撰文了,本文就先假设读者已经对上述知识滚瓜烂熟了……
鉴于有人称微软未公开复合文档等的文件结构,手头资料又并非微软官方文档,那么就意味官方术语可能并不存在,本文中有关术语、定义或者说法,或小狮自己的理解,或人云亦云、以讹传讹,或来源于对有关文档的翻译,小狮的翻译水平又确实有限,难免谬误,深表歉意之余,欢迎指正。
但谢绝引为学术依据,否则后果自负,这算是免责声明。
为了形象的表示复合文档的结构,以便下一步工作的校验,小狮专门为此用Delphi编写了一个名叫《复合文档查看器》的小程序,若无特别说明,本文插图均为该程序的运行结果。
复合文档的头部存放于文件开头的512(200h)个字节,这是固定的,头部存放的信息,基本上反映了整个文件的总体概貌。
小狮注:作为头部,大多数文件都用固定的字节数存放在文件的开头。但这并非固定的标准,有些头部是可变长度的,有些文件的头部则存放于文件的末尾,可见,“头部”仅仅是个称谓,并不等于“开头部分”,这不是翻译的问题,英文原文写做“Header”,大约是有来由的。如果“Header”确实又有“尾部”的意思,那就是小狮翻译水平的问题了。但复合文档的“头部”确实位于文件开头的的512个字节,这就使得中华儿女们对此的理解不会有歧义,这很好!
还有一句废话得讲讲,本文中涉及数字后面带个“h”的,说明是个十六进制数,如前文的200h,换算为十进制就是512。有些地方直接用十六进制而没有相应的换算为十进制,其一是小狮偷懒了,其二有些数值用十进制表示,于业内人士而言,反而不好表达。当然,如果读者认为我不懂换算,那我也管不了……
废话讲多了,该入正题了,先看图1:
图1是一个示例文件的头部,为了更能说明问题,我选择了一个16.4 MB那么大的文件来作为示例,为什么这么做?后面会讲到。
再啰嗦一下,在计算机科学中,计数一般从0开始,本文也这么做,其一是为了更像那么回事,其二在Delphi编程方便。
如图1所示:
头部从0字节开始的8个字节,是复合文档的标识,8个字节的值依次是D0h、CFh、11h、E0h、A1h、B1h、1Ah、E1h。如果这8个字节不是这些值,那么这个文件就不是复合文件,当然,刚好是这些值,那也不能说就是……
8至23一共16个字节,英文原文称“Unique identifier (UID) of this file (not of interest in the following, may be all 0)”,大意是“文件唯一标识,可能全部是0”,老实说,我没搞明白这个标识有什么用,手头用来做实验的文件也没找到一个不是全0的,自己生成的文件里面添上全0也没发生过什么事故,因此个人认为,这个当它全0就行了,不用管它,因此《复合文档查看器》也没有把它显示出来。
24(18h)至25两个字节,是一个16位整数,固定值为003Eh,文件格式的修订号。
26(1Ah)至27两个字节,是一个16位整数,固定值为0003h,文件格式的版本号。
28(1Ch)至29两个字节,是一个16位整数,值为FFFEh或FEFFh,字节排序方式。解释这个必须依赖WinHex,图2是WinHex打开示例文件的截图:
图2(WinHex截图)
图中:28(1Ch)字节的值是FEh,29(1Dh)字节的值是FFh,合起来,作为一个16位(2个字节)整数,它的值是FFFEh,这就是说,低字节在前,高字节在后。其实,这个在英文原文中有举例说明(第9页,4.2 Byte Order),其中说明很详细,我就不照译了,但这对于初涉其中的同学很容易混淆,所以专门说一下。
另外就是,按照原文的说法,FFFEh表示“Little-Endian”(这个词我不知道怎么翻译,意即低字节在前),FEFFh表示“Big-Endian”(这个词我不知道怎么翻译,意即高字节在前),那么,对于本字段而言,不管采用何种方法进行储存,其储存结果都如图2所示,这就很麻烦(其实小狮也完全搞不懂),所幸,英文原文又称“在现实应用中,只采用前者”,这就没什么问题了,我们直接把这个值固定为FFFEh,而且“Little-Endian”符合内存读写规律,省去很多转换。幸甚!幸甚!
30(1Eh)至31两个字节,是一个16位整数,值通常为9,表示一个扇区的大小,是2的幂,值为9表示2^9,即512个字节,这个值不是固定的,但通常是9。
扇区:英文“sector”,我不知道原文是否有“扇形区域”的意思,但“扇区”的叫法本身有点意思,这个有机会再写,在本文中,只需要把它理解为一个“存储块”就行了。
32(20h)至33两个字节,是一个16位整数,值通常为6,表示一个短扇区的大小,是2的幂,值为6表示2^6,即64个字节,这个值不是固定的,但通常是6。
短扇区:英文“short-sector”,这个概念我也是第一次接触,英文原文有解释(第12页,6.1 Short-Stream Container Stream),就不翻译了,我的理解是它“集装”(原文用了“Container”这个词)于扇区中。
由此,对这两个字段用2的幂来表示就很好理解,也只有这样,只要后者不大于前者,一个扇区就刚好能够“装下”整数个短扇区。
接下来从34(22h)至43共10个字节,英文原文称“Not used”,也就是不使用,这与其他文档的表述有点区别(别的文档称“保留”),我不知道是否是一样的意思,也不知道这10个字节是否真的不被使用,实际上这10个字节的值总是0。《复合文档查看器》也没有把它显示出来。
44(2Ch)至47四个字节,是一个32位整数,表示分区表扇区的总数,这个值跟文件大小有关。
分区表:英文原文“sector allocation table”(缩写SAT),意为“扇区分配表”,我把它称为“分区表”,其一是我记得磁盘系统有这种叫法,其二是为了下本提及时少打几个字。
在复合文档中:除了头部,其余存储空间按扇区大小划分成若干个扇区(你可以发现一个复合文档的字节总数总是512的整数倍),用分区表进行管理,而分区表本身也存储于扇区,这个字段表示用来存储分区表的扇区的总数。事实上,这种空间分配机制跟微软赖以起家的磁盘系统的分配机制极其类似。
大家都知道格式化磁盘的时候有一种“快速格式化”,其实就是重建一个磁盘分区表,仅此而已。
48(30h)至51四个字节,是一个32位整数,表示目录流第一个扇区的ID,ID这个词,用途太广泛了,在这里是编号的意思,小狮习惯称之为“编码”。
扇区编码:前面讲过,在复合文档中,除了头部,其余存储空间按扇区大小划分成若干个扇区,紧跟着头部的那个扇区就是第一个扇区,编号为0(从0开始编码),这样,从扇区编码就可以很容易的计算出扇区相对于文件开始位置的偏移量,示例中:目录流第一个扇区的ID为1,则偏移量=头部大小(512)+扇区编码(1)×扇区大小(512)=1024(400h)。
接下来52(34h)至55四个字节又是“Not used”,不使用。《复合文档查看器》也没有把它显示出来。
56(38h)至59四个字节,是一个32位整数,表示最小标准流尺寸,值通常为4096(1000h),当一个流的大小不小于这个值时,采用扇区(标准流)储存,否则采用短扇区(短流)储存,我不知道这种机制的好处在哪里,但人家这样做,我想自有道理吧?在文件处理的时候,时空开销上应该能够得到优化,但我不确定。
流:先贤们从西方文献中直译过来的,原文“stream”我不知道是否有其他译法,但“流”已经成为计算机科学中的一个基本术语,意思大约是“连续的数据信息”,其实我很难表述这个概念,也解释不好。便于理解起见,你可以将它当作文件系统中的一个文件来看待。
60(3Ch)至63四个字节,是一个32位整数,表示短分区表(缩写SSAT)的第一个扇区的ID,类似分区表,短分区表是用来管理短扇区的。值为FFFFFFFFh(-1)时,表示短分区表不存在。如前所述,如果复合文档的所有“流”的大小均不小于“最小标准流尺寸”,则没有采用短扇区来储存的“流”,当然短扇区就不存在,也不需要所谓的短扇区分区表来管理。
64(40h)至67四个字节,是一个32位整数,表示短分区表扇区总数,类似分区表,短分区表也需要扇区来储存。
68(44h)至71四个字节,是一个32位整数,表示主分区表(缩写MSAT)的第一个扇区的ID。前面讲到“分区表(SAT)”、“短分区表(SSAT)”,现在又冒出了一个“主分区表(MSAT)”,后面会有一篇专门来认识它们。要理解这个字段,还得借助接下来的两个字段,先接着说……
72(48h)至75四个字节,是一个32位整数,表示主分区表的扇区总数。
从76(4C)开始到头部结束,一共436个字节,为109个32位整数(可以看作数组),为主分区表的开头109个记录,每个记录为分区表扇区的编码(即ID)。当分区表扇区的个数(44(2Ch)至47)大于109个时,就需要另外的扇区来存储,前一个字段“主分区表的扇区总数”表示的就是“另外的扇区”的个数,而再前一个字段“主分区表的第一个扇区的ID”就表示这“第一个另外的扇区”的编码。当分区表扇区的个数不大于109个时,整个主分区表就存储于头部,不再需要“另外的扇区”。那么,“主分区表的第一个扇区的ID”的值为FFFFFFFFh(-1),“主分区表的扇区总数”的值为0。
当需要两个以上的“另外的扇区”来储存主分区表时,接下来的扇区如何寻址,下一篇再讲……
拗口!
早就说了,小狮是个极为啰嗦的人,一个头部就写了这么多,后面还有不少内容,不知什么时候才是个头。
(三)
分区表
原创:弄潮小狮
先整理一些概念:
扇区:在复合文档中:除了头部,其余存储空间被划分成大小相等的存储区域,这些区域就叫做“扇区”。
短扇区:短扇区应该是复合文档中特有的一个概念(恕我孤陋寡闻,在其他格式文件中,未见过这个概念),根据有关资料介绍及小狮本人的实践,在复合文件中,特定的一些扇区又被划分成大小相等的更小(相对于扇区)存储区域,这些区域就叫做“短扇区”。这个概念有点难理解,我在学习过程中有这样的感觉,希望通过下文的描述,可以更清晰的给读者朋友勾勒出来。
扇区编码(ID):在复合文档中,扇区按顺序进行编码,紧跟着头部的那个扇区就是第一个扇区,编号为0(从0开始编码)。
扇区偏移量:扇区第一个字节在复合文档中的绝对位置。
短扇区偏移量:短扇区第一个字节在复合文档中的绝对位置。
分区表:是一个32位整数的数组,按下标与扇区一一对应,数组的每成员的值表示在流中紧接相对应的扇区的下一个扇区(即该扇区的后续扇区)的编号,这就形成了一个链表,将相关的扇区“串接”起来,形成一个完整的“流”。有几个特殊的值:FFFFFFFFh(-1),表示该扇区为自由扇区(未被使用);FFFFFFFEh(-2),表示链表结束(没有后续扇区);FFFFFFFDh(-3),表示该扇区被分区表使用;FFFFFFFCh(-4),表示该扇区被主分区表使用。英文原文有更详细的描述(第7页,3.2 Sector Chains and SecID Chains)。
短分区表:类似分区表,不同的是短分区表用来管理短扇区。通常,数组成员的特殊值只有FFFFFFFFh(-1)和FFFFFFFEh(-2),因为分区表及主分区表不会使用短扇区来储存。
分区表扇区:用来储存分区表的扇区,就叫做“分区表扇区”。
短分区表扇区:用来储存短分区表的扇区,就叫做“短分区表扇区”。
主分区表:主分区表主要是用来管理分区表扇区的,它与前述两种分区表有个重要的区别:前两者是“链表”的形式,主分区表是一个彻头彻尾的数组,它的每一个成员的值直接指向对应的扇区,特殊值只有FFFFFFFFh(-1)。“主分区表”这个词直接翻译于英文原文,或许这个词不够恰当,但我想不出另一个更有利于表述的词来。
主分区表扇区:用来储存主分区表的扇区,就叫“主分区表扇区”。
流:如前文所述,先贤们从西方文献中直译过来的,原文“stream”我不知道是否有其他译法,但“流”已经成为计算机科学中的一个基本术语,意思大约是“连续的数据信息”,其实我很难表述这个概念,也解释不好。便于理解起见,你可以将它当作文件系统中的一个文件来看待。
短流:流尺寸小于特定数值的流,在复合文档中,这个值通常为4096(1000h)。
目录流:用来存储目录的流,在复合文档中,这是一个特殊的流,它由复合文档直接管理,而不由目录系统管理。原因应该是它本身描绘了目录的结构,目录记录了流的入口,“由目录管理目录流的入口”,显然是行不通的。
这些概念,几乎都有个“区”字,到底哪个本身就是“区”,哪个用来管理“区”的,很容易混淆。其实英文更容易混淆,都有“sector”这个词。
如果你现在还是很难理解分区表的概念,你可以将分区表理解成一份标注复合文档存储空间的“地图”。
在复合文档中:
主分区表始于头部第76(48h)个字节,为一个32位整数的数组,至头部结束,共436个字节,可存放109个分区表扇区编码,当分区表扇区的个数不止109个的时候,就需要额外的扇区进行存放,这些“额外的扇区”就叫做“主分区表扇区”。
通常,一个扇区的大小为512个字节,那么,一个主分区表扇区就能够储存128个分区表扇区编码?实际上,每个主分区表扇区只储存127个分区表扇区编码,因为每一个主分区表扇区的最后4个字节(一个32位整数)用来存放下一个主分区表扇区的编码。这样,当分区表很大的时候,才能够将主分区表按顺序“串”起来,主分区表已经是最顶级“地图”了,它只能用这种方式来进行“串接”。
事实上,按照《复合文档学习笔记(二)-头部》中的示例(图1),在这个文件中,分区表扇区的个数有263个,头部储存109个,还有154个扇区编码需要储存,那就需要“额外的扇区”,一个主分区表扇区储存127个扇区编码,154个扇区编码就需要2个扇区来存储,即2个主分区表扇区。
有一点我没搞清楚的是,前面讲过“每一个主分区表扇区的最后4个字节(一个32位整数)用来存放下一个主分区表扇区的编码”,那么,如果分配到最后一个扇区剩下128个扇区编码,剩下一个又需要一个扇区来储存,这显然很浪费,这时候,这4个字节是否直接存放的最后一个分区表山区编码?这种情况英文原文没有提及,小狮也没有去做测试,你知道,有找一个(或者专门做一个)这样的文件,是一件痛苦的事情。
在主分区表中,每一个成员的值表示一个扇区编码,FFFFFFFFh(-1)表示该扇区是空闲扇区。
按照主分区表的顺序,将这些扇区“串起来”,逻辑上就形成一个连续的数据串(扇区偏移量的计算方法见《复合文档学习笔记(二)-头部》),这个“数据串”就是分区表。
分区表也是一个32位整数的数组,但其用法与主分区表是截然不同的,分区表按照数组下标,对应扇区,譬如:分区表的第0个成员,就对应0扇区。
每个成员的值表示紧接该扇区(下一个扇区)的扇区编码(扇区偏移量计算方法同“主分区表”),这就形成一个链表,这在英文原文中也有例子说明,这里我也简单据一个例子。
譬如:分区表的第0个成员,就对应0扇区,其值为1,就表示,在流中,紧接0扇区的下一茬数据存放在1扇区中。
链表在何处结束?当一个分区表成员的值为FFFFFFFEh(-2)时,表示“下面没有了”,链表到此结束。
还有两种特殊的扇区:值为FFFFFFFDh(-3)时,表示这个扇区被分区表占用(即分区表扇区);值为FFFFFFFCh(-4)时,表示这个扇区被主分区表占用(即主分区表扇区)。
还有一个值:FFFFFFFFh(-1),表示这个扇区是空闲扇区,这通常出现在分区表的末尾。如前例:分区表的最后100个成员,其值都是FFFFFFFFh(-1)。
短分区表也是一个32位整数的数组。第一个短分区表扇区编码由头部获得,短扇区分区表由分区表管理,获得第一个短分区表扇区编码后,通过分区表“串接”,就可获得完整的短扇区分区表。其用法与分区表类似,但短扇区偏移量寻址有点麻烦,这个将在下一篇结合“目录和目录流”进行讲述。
本篇讲起来已经很绕了,为了方便阅读,小狮将可能引起混淆的有关词汇加蓝,并下划线表示,并非URL,转载时请保留原貌,谢谢!
(四)
目录及目录流
原创:弄潮小狮
通过前面的讲述,我们已经知道了复合文档如何对存储空间进行管理,这是远远不够的,我们不仅要知道复合文档中的数据,更要知道这些数据表达什么?又如何管理?
这就是本篇要讲述的“目录及目录流”,这也需要先整理几个概念:
目录:如果说,分区表是一份复合文档的物理地图的话,那么,目录就是一份“逻辑地图”,它将告诉我们,在符合文档中的流是如何被组织、被使用的,以及流的存储位置、大小等等。
目录流:前面讲过,目录流是用来储存目录的流,由复合文档直接管理。
目录树:从DOS时代过来的朋友对“目录树”应该不会陌生,没见过DOS的朋友,也有比较直观的学习途径,在Windows下面,有“文件夹”(其实就是DOS的“目录”),文件夹里面又包含文件夹、文件,这就形成了一个树形结构,从“根”到“枝”、“叶”。
红黑树:事实上,我不记得上学的时候学过这个概念(或者说“数据结构”),在此之前连听都没听说过,资料显示,“红黑树”早在1978年已经面世,也许是我上学不认真所致,惭愧!因此,在学习复合文档的时候,我又不得不花了不少时间“恶补”一课(这是我碰到的另一只拦路虎)。要详细讲述这个概念以及实现方法等等,可能会需要很长的篇幅,但简单地说,红黑树是二叉树的一种,有兴趣(或者有需要)的朋友可以自己找资料了解,在网上,这方面的资料不少。
目录入口:英文原文“Directory Entry”,一个说明目录及其相关信息的结构。
既然目录流由复合文档直接管理,那么,如何读取目录流?
根据前面几节的描述,在头部可以找到目录流的第一个扇区的编码(ID)(见:《复合文档学习笔记(二)》图1),根据该编码,从分区表(SAT)中可以读取目录流的扇区链,见图3:
图3
按照目录流的扇区链的顺序,读取每个扇区的数据,“串起来”就是目录流了。
每个目录入口有128(80h)个字节,一个复合文档至少必须有一个目录(即“根入口”,Root Entry),在默认情况下,它大于短扇区尺寸(2^6),因此,目录流总是以扇区来存储。
实际上,不管短扇区尺寸如何设置,目录流不可能以短扇区来存储,这个问题本文最后会谈到。
示例中读出的目录流数据见图4:
图4(WinHex截图)
从图4可以看到,目录流按每128个字节一个目录入口按顺序存放,以第一个目录入口为例,在128个字节的目录信息中:
0(00h)至63(3Fh)共64个字节,存放目录入口名称,采用Unicode字符,是一个0值字符终止的字符串。
64(40h)至65(41h)共2个字节,是一个16位整数,表示目录入口名称的长度(包括终止符0)。
第66(42h)字节,是一个8位整数,表示目录入口的类型:
1(01h)表示目录是一个仓(这个“仓”有点怪,英文原文为“storage”,我想可以理解为相当于一个文件夹);
2(02h)表示目录是一个流;
5(05h)表示目录是根,在复合文档,通常有且仅有一个这样的目录,并且总是第一个,我不知道有没有例外,在我学习的过程中未确实碰到例外的情况。
其他值,英文原文语焉不详,我在学习过程中也没碰到过,这里不做描述,以免以讹传讹。
第67(43h)字节,是一个8位整数,表示目录入口节点在红黑树中的颜色:0(00h)表示红色,1(01h)表示黑色。
68(44h)至79(4Fh)共12个字节,是三个32位整数,这三个整数的前两个被应用于红黑树中,按顺序分别表示左孩子的目录编码和右孩子的目录编码,值为-1(FFFFFFFFh)时表示该节点为“叶子”,这个请借助红黑树相关理论进行理解,这里不再赘述。第三个整数表示子目录红黑树的根节点,值为-1(FFFFFFFFh)时表示没有子目录(通常流目录入口这个值为-1),这个有点绕,后面专门讲一下。
80(50h)至99(63h)共20个字节,英文原文未做详细解释,并且可以全0,在生成复合文档时,我将其全部填0,未出现异常,权且当作它们是保留字节。
100(64h)至115(73h)共16个字节,是两个8个字节的时间戳,前者表示目录创建的时间,后者表示目录最后修改的时间。关于时间戳,英文原文有详细解释,并有生动示例,这里不再赘述。
116(74h)至119(77h)共4个字节,是一个32位整数,表示目录入口所表示的流的第一个扇区编码(ID),0(00000000h)表示没有。对于根目录,这个值有特殊的含义,本文末尾会有进一步的解释。
120(78h)至123(7Bh)共4个字节,是一个32位整数,表示目录入口所表示的流的尺寸,通过将这个尺寸与短扇区尺寸进行比较,可以确定该流是是以扇区还是短扇区进行存储。
124(7Ch)至127(7Fh)共4个字节,未使用。
示例中的目录入口列表如图5:
图5
通过上述说明,我们可以整理出示例文件中的目录逻辑结构,见图6:
图6
在这一节中,出现了两种树(红黑树和目录树)及两种根(根目录和根节点),在英文原文中,相关描述语焉不详(或许是小狮的英文实在太烂),在理解上费了不少周折(也是一只不小的拦路虎)。
从图6可以看出,在前面的描述中,红黑树并非目录树的本身的组织形式,目录树只有一棵,它本身就是复合文档中流的组织结构,而红黑树主要用于构造(或查找),每一个仓目录下的子目录独立构成一棵红黑树,上文目录入口中的“子目录红黑树的根节点”说法就变得很好理解了。
查找或遍历目录时,从根目录(Root Entry)开始,从一棵红黑树进入另一棵红黑树,如此往复……
图7是示例文件的目录结构。
另外,从图5可以看到,根目录的“入口扇区”(即“目录入口所表示的流的第一个扇区编码”)有一个值,从前面叙述中,我们已经知道,根目录只是一个特殊的“仓目录”并非是一个“流目录”,它怎么有一个“入口扇区”呢?
这个问题提得好!
在《复合文档学习笔记(三)》中,我介绍了扇区及短扇区,我们必须知道短扇区储存在哪些扇区中,否则无法读取有关数据……
你猜对了,根目录的“入口扇区”就是短扇区所在的扇区的第一个扇区编码,如果一个复合文档中不存在短扇区,那么该值0(00000000h)。
可见,短扇区的偏移量依靠根目录的“入口扇区”来确定,这也是前面讲过“目录流不可能以短扇区来存储”的原因了。
本文很绕舌,因为要讲清楚的目录流跟搞清楚复合文件的目录流一样困难,或许小狮的学习能力及表述能力均有待提高。
学习时脑痛,讲述时也脑痛,但本节终于写完了。
看官就凑合吧!
(五)
两个流
原创:弄潮小狮
复合文档本身有两个流,表述了复合文档的相关信息,这就是“<05H>SummaryInformation”(与文件类型无关)和“<05H>DocumentSummaryInformation”(与文件类型有关)。
这两个流在英文原文里面没有做进一步介绍,网上资料也非常有限。因此,本篇专门介绍这两个流。
在Windows下面,右击一个Office文件(Word、Excel、PPT或其他),从弹出菜单选择“属性”,查看属性窗口的“摘要”选项页,如图8:
图8(操作系统截图)
可以查看文件的有关摘要信息,这些信息就存放在复合文档的“<05H>SummaryInformation”流中。
事实上,不管你的系统是否安装Office,或者你查看的文件是否具备正确的扩展名(即文件名后缀,如:“.xls”),只要查看的文件是一个正确的复合文档,只要你的操作系统是Windows(未考证过版本限制,xp以上的版本就没问题)就可以查看到该信息(图8显示的是一个Excel文件,其他文件有些差别)。
这就说明,微软的操作系统本身直接支持复合文档的文件格式,而不管该复合文档作何用途。
示例文件中“<05H>SummaryInformation”流的数据见图9:
图9
“<05H>SummaryInformation”流的格式:
0(00h)至27(1Bh)共28个字节,是“<05H>SummaryInformation”流的头部,在头部中:
0(00h)至1(01h)共2个字节,是一个16位整数,表示字节排序方式。这个在《复合文档学习笔记(二)》中已经讲过,不再赘述。
2(02h)至3(03h)共2个字节,是一个16位整数,用于流验证。事实上,我不清楚这个字段是做什么用的,这个值总是0.
4(04h)至5(05h)共2个字节,表示操作系统的版本号,前一个字节表示主版本号,后一个字节表示次版本号。对于Windows xp,这两个字节的值为“05 01”。
6(06h)至7(07h)共2个字节,是一个16位整数,表示操作系统,取值可以是:2(Win32)、1(Mac)或0 (Win16)。
8(08h)至23(17h)共16个字节,是一个流的类编码(the stream's class id),格式为“dword + 2 words + 2 bytes + 6 bytes”(dword为32位整数、word为16位整数、byte为8位整数),值为“00000000-0000-0000-0000-000000000000”。
24(18h)至27(1Bh)共4个字节,是一个32位整数,表示流中节(section)的个数,通常“<05H>SummaryInformation”流只有1节,该值总是1。
紧接着头部的是节声明,共20个字节,2个字段:
28(1Ch)至43(2Bh)共16个字节,是一个节的类编码(section's class id),对于“<05H>SummaryInformation”流,该值为“f29f85e0-4ff9-1068-ab91-08002b27b3d9”。
44(2Ch)至47(2F)共4个字节,是一个32位整数,表示节数据相对于流起始位置的偏移量。值通常为48(30h)。
节头部,在示例中,节的偏移量为48(30h),节头部共8个字节,有2个字段,在节中:
0(00h)至3(03h)(示例中,对应流偏移量为48(30h)至51(33h),下同)共4个字节,是一个32位整数,表示节尺寸。
24(18h)至27(1Bh)共4个字节,是一个32位整数,表示属性个数。
紧接着节头部,是若干(按属性个数,示例中为8个)个属性声明,属性声明由两个32位整数构成,前者为属性编码(ID),后者为属性数据相对于节起始位置的偏移量。
对于“<05H>SummaryInformation”流,属性编码(ID)及其含义如下:
属性编码(ID) | 含义 | 属性类型 |
1(01h) | 代码页 | 2(02h) |
2(02h) | 标题 | 30(1Eh) |
3(03h) | 主题 | 30(1Eh) |
4(04h) | 作者 | 30(1Eh) |
5(05h) | 关键字 | 30(1Eh) |
6(06h) | 备注 | 30(1Eh) |
7(07h) | 模板 | 30(1Eh) |
8(08h) | 最后保存 | 30(1Eh) |
9(09h) | 修正号 | 30(1Eh) |
10(0Ah) | 总编辑时间 | 64(40h) |
11(0Bh) | 最后打印时间 | 64(40h) |
12(0Ch) | 创建时间 | 64(40h) |
13(0Dh) | 最后保存时间 | 64(40h) |
14(0Eh) | 页数 | 3(03h) |
15(0Fh) | 字数 | 3(03h) |
16(10h) | 字符数 | 3(03h) |
17(11h) | 缩略图 | 71(47h) |
18(12h) | 创建文档的程序名称 | 30(1Eh) |
19(13h) | 安全 | 3(03h) |
属性数据的长度不是固定的,它跟属性类型有关,属性数据的第一个32位整数表示属性类型,紧接其后的就是属性的值,属性类型及其含义如下:
属性类型 | 含义 |
2(02h) | 16位带符号整数 |
3(03h) | 32位带符号整数 |
19(13h) | 32位无符号整数 |
30(1Eh) | 字符串(格式:32位整数表示长度+0结尾的字符串) |
64(40h) | 时间戳 |
71(47h) | 剪贴板格式 |
打开一个Office文件(Word或者Excel等),从菜单项“文件|属性”可以打开如图10所示的属性窗口:
图10(Excel截图)
这些属性就储存于“<05H>DocumentSummaryInformation”流中,其结构与“<05H>SummaryInformation”流类似,所不同的是节的类编码(section's class id)的值为“d5cdd502-2e9c-101b-9397-08002b2cf9ae”。
“<05H>DocumentSummaryInformation”流中主要储存与文件类型有关的属性,因此,根据不同类型的文件,在属性编码及属性类型上做了一些扩展,这里就不再一一介绍了。
另外,图8中有一个“公司”的属性,也储存于“<05H>DocumentSummaryInformation”流中,属性编码(ID)为15(0Fh),属性类型为30(1Eh)。因此,在操作系统中查看一个没有扩展名的复合文档的属性,是没有该项属性的。
很长时间没有写过技术类的文章了,越写越累!
全文完。
另:本文发表以来,常有网友留言索要“复合文档查看器”,现有网友提供空间http://download.csdn.net/source/3272856,有需要的可直接下载。
声明:本工具仅为方便学习而编写,本身不是很稳定,也常报错。也请勿用于商业用途。