本文是一个系列. 主要是分享我最近一年做7z文件开发的经验. 主要包括7z官方源码的结构分析, 以及7z文件格式的分析. 其中涉及到7z源码结构的各个细节, 以及7z文件格式的具体细节. 本文适合对象: 想要了解学习7z源码的开发人员, 想要了解7z文件格式细节, 做7z文件压缩器和解压器的开发人员, 以及其他压缩文件爱好者等等.
目前7z的最新稳定版是9.20, 而9.30版本还在alpha版本. 所以本文是基于其9.20版本. 我将尽可能详细的描述所有细节, 但到目前为止我了解到的细节大概能到八成到九成的样子. 也不是百分百. 希望能和大家共同讨论学习. 这些信息足以开发一个工业级别的高兼容性的7z压缩器和解压器了.
本文首发在我的个人独立博客 http://byNeil.com 以及博客园. 欢迎大家浏览我的独立博客, 留言交流. 转载请注明出处链接. 本人保留一切权利. 但本人不对使用本文提供的代码造成的损失负责.
好了, 憋死我了, 前面的客套话终于说完了. 有人可能要问, 为什么要分析7z文件格式? 这个不是开放的吗? 为什么我前面说我只了解到八,九成, 而不是百分百呢?
我先来解释这几个问题, 这能说明本文的必要性, 以及看下去的必要性. 首先7z的确是开放的. 但他的格式说明非常简单, 简直是简陋. 简单的一个txt文件,寥寥几十行算不上说明的符号, 对复杂的7z格式来说, 简直就是天书.( 后面分析7z源码的时候会详细说道这一点. ) 所以如果你接到老板的一个通知:"我们要做7z兼容的压缩解压器" 的时候, 你就要崩溃了. 因为相对于zip这些经过标准化的文件格式来说, 7z几乎就是一个黑洞.(zip有标准化的 spec, 里面有详细到字节级别的说明). 所以我分析7z格式可以说有一半是通过阅读源码逆向推测出文件格式的, 再经过反复和源码对比测试证明其正确性的. 所以我只能说了解到了八九成, 而不是百分百.
先分享一些有用的链接:
7z的官方主页: http://www.7-zip.org/ (它托管在sourceforge的虚拟机上, 本来能打开的, 但是前一段时间开始被墙了, 我实在想不到墙它的理由, 可能是被sourceforge主机上其他的网站拖累的吧,你懂的).
这是sourceforge上7z的论坛: http://sourceforge.net/p/sevenzip/discussion/45797 . 论坛比较活跃, 每天都有新帖. 而且7z作者也在上面每天亲自回帖. 由于作者是俄罗斯人, 所以时区跟我们差的不多. 一般留言4个小时左右, 作者就能回复吧. 我问过几个问题, 都是这样的. 当然也有些帖子作者懒得回, 那就另当别论了. 呵呵.
7z的官方中文镜像: http://sparanoid.com/lab/7z/ 这个不需要FQ. 是官方给出的中文版链接. 值得信赖. 上面的内容基本和官方主页保持一致.
我们先从几个概念开始吧 : 7z软件 和7z文件. 7z文件指的是一种后缀名为.7z的文件. 7z软件指的是作者开发的生成和操作7z文件的工具. 但是现在7z软件已经发展到不光支持7z格式的文件了, 还能额外的生成和编辑另外几十种文件格式了. 其中包括我们熟知的zip格式, rar格式, iso格式等等, 也包括一些不可思议的格式, 比如: CAB, CHM, CPIO, CramFS, DEB, DMG, FAT . 看眼花了吧. 好吧, 本文主要讨论 7z文件格式, 以及7z软件中主要和7z文件相关的功能. 当然也会粗略的涉及到其他的一些有用的功能, 比如如何给7z软件写插件, 让他支持我们自己定义的文件格式.
说到压缩文件, 一般会遇到两个概念, 一个是 "Compress"(压缩) 和"Archive" (存档). "存档" 这个词个人觉得不好理解. 我个人把 "Archive" 叫做 "打包".
举个例子, 比如有一堆毛巾, 每一条毛巾代表一个文件. "Compress"(压缩)的意思就是把毛巾里面的水拧干, 达到压缩的目的. 而"Archive"(打包) 的意思就是把这些毛巾包成一个大包, 便于运输. 所以你可能想到了, 要运输一堆这样的毛巾可能有两种方法: 第一种: 先把每条毛巾拧干并折成方块(压缩), 再把这些方块组合成一个大包(打包). 第二种, 先把这些毛巾包成一个大包(打包), 然后再把这个大包拧干. 凭经验你可能已经意识到, 前一种方法比较好处理, 而且压缩比比较大. 后一种方法就弱一点了.
对了, 前一种就是先压缩后打包的方式. 7z, zip 和rar等就是用的这种方法. 后一种就是先打包, 后压缩的方法. 常见的tar.gz的文件就是用这种方法的. 先用tar打包, 在用gz压缩.
对于7z 我们再具体解释一下 "压缩" 和 "打包" 的概念.
7z 文件格式严格的说是一种"打包"的格式, 它规定了打包的方法. 而"压缩"的任务是由不同的算法完成的. "压缩算法"负责把毛巾里的水拧干. 具体来讲就是把一个文件按照一定规则变小. 压缩算法一般只能处理单个文件, 对于多个文件, 只能单个文件压缩, 然后再打包.
(当然,前面也说到了, 第二种方法就是先把文件打包, 然后再压缩. tar.gz就是这样的. tar工具就是简单的把所有的文件首尾相接, 变成一个大文件, 然后再用压缩算法一次压缩的. 这个不是本文的讨论对象, 所以以后就不提了.)
7z默认使用的是lzma压缩算法. 单个文件会先用lzma压缩算法压缩, 然后7z负责把压缩过后的文件打包. (这个跟zip类似, zip文件也是打包格式, 它默认使用deflate算法压缩.) 7z还支持封装其他几种压缩算法, 比如: Lzma2, ppmd, bzip2和deflate.
这些压缩算法在7z里面都有源码, 也不是本文要讨论的重点. 我们将重点讨论7z的打包格式. 就是介绍7z具体是怎样把压缩过后的文件封装打包的. 期间也会夹杂介绍其中一些算法. 后面如果有时间, 我会再单独介绍这些算法的具体实现.
打开源码的 "DOC" 目录. 这里面就是官方所有的文档了. 其中只有二个文档跟结构相关:
1. 7zFormat.txt, 这是我们的主角, 里面介绍了7z文件的大体结构.
2. Methods.txt, 这里面介绍了7z压缩算法id的编码规则, 以后会用到.
我们从7zFormat.txt文件开始.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
Archive structure ~~~~~~~~~~~~~~~~~ SignatureHeader [PackedStreams] [PackedStreamsForHeaders] [ Header or { Packed Header HeaderInfo } ] |
上面就是7z文件的总体结构了. 我来稍微解释一下. 上面的代码中, 从波浪线往后开始算. 7z的文件结构基本上分为三部分:
1. 前文件头(就是最前面的header).
2. 压缩数据.
3. 尾文件头(就是放在文件末尾的header).
一, 前文件头就是上图中的 "SignatureHeader". 它是32个字节定长的. 前文件头其实记录的信息很少, 它的主要目的是记录尾文件头的位置, 压缩的主要结构都是存在尾文件头中.
它的结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
SignatureHeader ~~~~~~~~~~~~~~~ BYTE kSignature[ 6 ] = { ‘7‘ , ‘z‘ , 0xBC , 0xAF , 0x27 , 0x1C }; ArchiveVersion { BYTE Major; // now = 0 BYTE Minor; // now = 2 }; UINT32 StartHeaderCRC; StartHeader { REAL_UINT64 NextHeaderOffset REAL_UINT64 NextHeaderSize UINT32 NextHeaderCRC } |
先是固定的6个字节的值, 前两个字节的值是字母 ‘7‘ 和‘z‘ 的ascii值. 后面四个字节是固定的: 0xbc, 0xaf, 0x27, 0x1c
然后是两个字节的版本号, 注意主版本号在前面, 次版本号在后面. 目前的版本号是: 0.2, 注意这是7z文件格式的版本号, 不是7z软件的版本号.
然后是四个字节的 UINT32 的值, (注意, 7z的所有数据都是采用小端在前的存储, 所以要注意这四个字节的实际存储顺序是低位字节在前面, 高位字节在后. 后面的所有数据都是这种结构, 所以以后就不再强调了. ) . 这4个字节的值是做什么的呢? 先抛开这四个字节本身, 前文件头的32个字节中, 已经用去了 6 + 2 +4 =12 个, 还剩下20个字节. 对了, 这四个字节就是剩下的20个字节的CRC校验值. 具体的CRC算法源码, 在源码中的 "C" 文件夹下的 ‘7zCrc.c‘ 和 ‘7zCrc.h‘.
最后这20个字节要一起介绍了. 先是8个字节的UINT64的值, 它记录的是尾文件头(上图中的NextHeader)与前文件头的距离, 这个距离是不算前面这32个字节头的, 也就是抛开前面32个字节开始计数的(解压器通过读取这个值,然后从第33个字节开始直接跳过这个距离, 就可以找到尾文件头了). 然后是8个字节的值, 记录了尾文件头的大小(解压的时候, 通过这个值就能读出尾文件头的长度了). 最后还有4个字节的值, 它也是一个Crc校验值, 是整个尾文件头的校验值.
这里需要注意的是, 上图中用的是 "REAL_UINT64" 这个表达方式, 它的意思就是我们通常理解的占8个字节的UInT64的值(当然是小端存储的啦). 这里用了"real", 真. 那是不是还有"假"的InT64呢. 答案是肯定的. 7z为了兼容压缩大文件(大于4G),这个问题曾一度是zip文件的噩梦, 早期的zip只能压缩小于4G的文件, 并且压缩后的总文件大小也不能超过4G, 后来专门做了标准升级. 好了扯远了. 7z一早设计就考虑到了大文件的问题, 所以很多地方都必须用int64来表达, 这样也会带来一个问题, 就是绝大多数case下, 都不可能超过4G(试问一下,你平时有多少压缩文件超过4G 呢), 所以呢, 就会造成8个字节的int64根本用不上, 多余的字节浪费了. 尤其在小文件压缩的时候, 很影响压缩比. 所以呢, 7z采取了一种巧妙的方法. 就是int64并不是都用8个字节存储, 它用一种简单的编码方式,进行变长存储. 在这个文件中也有描述:
1
2
3
4
5
6
7
8
9
10
11
12
|
REAL_UINT64 means real UINT64. UINT64 means real UINT64 encoded with the following scheme: Size of encoding sequence depends from first byte: First_Byte Extra_Bytes Value (binary) 0xxxxxxx : ( xxxxxxx ) 10xxxxxx BYTE y[ 1 ] : ( xxxxxx << ( 8 * 1 )) + y 110xxxxx BYTE y[ 2 ] : ( xxxxx << ( 8 * 2 )) + y ... 1111110x BYTE y[ 6 ] : ( x << ( 8 * 6 )) + y 11111110 BYTE y[ 7 ] : y 11111111 BYTE y[ 8 ] : y |
上面就是编码方式: 就是根据第一个字节的内容来判断后面还有多少个字节.
如果第一个字节的最高位是 0, 那后面就没有字节了. 范围在 0-127.
如果第一个字节的最高两位是 1, 0, 表示它后面还有一个字节. 读取方式是: ( xxxxxx << (8 * 1)) + y
依次类推, 不再详细介绍了.
它的写入方法在: CPP7zipArchive7z7zOut.cpp 文件的 第204行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
void COutArchive::WriteNumber(UInt64 value) { Byte firstByte = 0; Byte mask = 0x80; int i; for (i = 0; i < 8; i++) { if (value < ((UInt64(1) << ( 7 * (i + 1))))) { firstByte |= Byte(value >> (8 * i)); break ; } firstByte |= mask; mask >>= 1; } WriteByte(firstByte); for (;i > 0; i--) { WriteByte((Byte)value); value >>= 8; } } |
它的读取方法在: 7zIn.cpp 的第210行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
UInt64 CInByte2::ReadNumber() { if (_pos >= _size) ThrowEndOfData(); Byte firstByte = _buffer[_pos++]; Byte mask = 0x80; UInt64 value = 0; for ( int i = 0; i < 8; i++) { if ((firstByte & mask) == 0) { UInt64 highPart = firstByte & (mask - 1); value += (highPart << (i * 8)); return value; } if (_pos >= _size) ThrowEndOfData(); value |= ((UInt64)_buffer[_pos++] << (8 * i)); mask >>= 1; } return value; } |
这里贴出来给大家参考一下. 其实, 后面提到的Uint64如果没有特别说明是8个字节, 那它都是采用这种压缩方式存储的. 但是注意UInt32 无论何时都是占4个字节的, 没有采用压缩.
二, 第二部分比较简单, 它会比较大, 简单的说, 它就是文件压缩后的压缩数据存放地点. 结构如下:
1
2
|
[PackedStreams] [PackedStreamsForHeaders] |
简单的说, 7z会把文件压缩成若干个"Pack", 就是包的意思, 这里就是按顺序存储这些pack的. 每个pack的位置和大小信息都会记录在尾header中, 解压的时候就会从这里读出pack,然后解压出来. 这里都是简单的排布压缩后的数据, 所以没有多少细节需要介绍的.
三, 真正复杂的主角出场了, 尾文件头, 就是7z中所谓的 nextHeader.
Header structure
~~~~~~~~~~~~~~~~
{
ArchiveProperties
AdditionalStreams
{
PackInfo
{
PackPos
NumPackStreams
Sizes[NumPackStreams]
CRCs[NumPackStreams]
}
CodersInfo
{
NumFolders
Folders[NumFolders]
{
NumCoders
CodersInfo[NumCoders]
{
ID
NumInStreams;
NumOutStreams;
PropertiesSize
Properties[PropertiesSize]
}
NumBindPairs
BindPairsInfo[NumBindPairs]
{
InIndex;
OutIndex;
}
PackedIndices
}
UnPackSize[Folders][Folders.NumOutstreams]
CRCs[NumFolders]
}
SubStreamsInfo
{
NumUnPackStreamsInFolders[NumFolders];
UnPackSizes[]
CRCs[]
}
}
MainStreamsInfo
{
(Same as in AdditionalStreams)
}
FilesInfo
{
NumFiles
Properties[]
{
ID
Size
Data
}
}
}
尾header的结构非常复杂, 里面有很多压缩概念, 如若没有理解压缩过程, 单独的纯字节层面介绍是没有意义的.
我们下一篇开始介绍详细的7z压缩流程, 介绍7z是如何把一系列的文件, 压缩成一个大文件的, 怎样利用压缩算放, 怎样排布文件结构. 同时我们再一边来介绍这个尾header的结构.