上一篇在这里 7z文件格式及其源码的分析(五)
这一篇主要介绍7z的流式压缩和解压原理.
对于流式处理来讲, 主要问题是要处理好文件头的问题. 因为是流式的, 所以,不可能走回头路. 也就不可能说压缩完了之后回到开头来修改头的信息.
7z文件的文件头其实是分为两部分的, 这两部分文件头在前面的几篇分析中都有介绍:
-
头Header
, 就是写在文件前面的 Header. 这个头比较小, 主要记录了文件的版本信息, 以及尾header的位置偏移和校验和等等. 其主要作用是文件类型识别和查找到尾Header -
尾Header
, 就是写在文件末尾的 Header. 这里记录了 7z 文件的全部压缩信息, 解压主要就靠这里面的信息了.
7z 常见的压缩过程是: 先把 头Header
的一些位置保留, 然后压缩数据. 压缩完之后, 尾Header
写完之后, 再把 尾Header
的大小,偏移,校验和写回到 头Header
中去.
在流式环境下, 最后一步回头写 头Header
的过程显然不可能完成. 因为已经流过了.
那 7z 是怎么处理这个问题的呢? 7z 支持流式压缩吗?
7z 的文档里,并没有明确的回答这个问题. 我们还是让它的代码回答吧.
我们找到 7z 的解压代码, 7zIn.cpp
文件. 找到 HRESULT CInArchive::ReadDatabase2()
函数. 大概在 1136
行.
这个函数就是在读取 头Header
信息, 并查找 尾Header
位置.
我们截取它开头的一部分代码片段:
HRESULT CInArchive::ReadDatabase2(
DECL_EXTERNAL_CODECS_LOC_VARS
CArchiveDatabaseEx &db
#ifndef _NO_CRYPTO
, ICryptoGetTextPassword *getTextPassword, bool &passwordIsDefined
#endif
)
{
db.Clear();
db.ArchiveInfo.StartPosition = _arhiveBeginStreamPosition;
db.ArchiveInfo.Version.Major = _header[6];
db.ArchiveInfo.Version.Minor = _header[7];
if (db.ArchiveInfo.Version.Major != kMajorVersion)
ThrowUnsupportedVersion();
UInt32 crcFromArchive = Get32(_header + 8);
UInt64 nextHeaderOffset = Get64(_header + 0xC);
UInt64 nextHeaderSize = Get64(_header + 0x14);
UInt32 nextHeaderCRC = Get32(_header + 0x1C);
UInt32 crc = CrcCalc(_header + 0xC, 20);
#ifdef FORMAT_7Z_RECOVERY
if (crcFromArchive == 0 && nextHeaderOffset == 0 && nextHeaderSize == 0 && nextHeaderCRC == 0)
{
UInt64 cur, cur2;
RINOK(_stream->Seek(0, STREAM_SEEK_CUR, &cur));
const int kCheckSize = 500;
Byte buf[kCheckSize];
RINOK(_stream->Seek(0, STREAM_SEEK_END, &cur2));
int checkSize = kCheckSize;
if (cur2 - cur < kCheckSize)
checkSize = (int)(cur2 - cur);
RINOK(_stream->Seek(-checkSize, STREAM_SEEK_END, &cur2));
RINOK(ReadStream_FALSE(_stream, buf, (size_t)checkSize));
int i;
for (i = (int)checkSize - 2; i >= 0; i--)
if (buf[i] == 0x17 && buf[i + 1] == 0x6 || buf[i] == 0x01 && buf[i + 1] == 0x04)
break;
if (i < 0)
return S_FALSE;
nextHeaderSize = checkSize - i;
nextHeaderOffset = cur2 - cur + i;
nextHeaderCRC = CrcCalc(buf + i, (size_t)nextHeaderSize);
RINOK(_stream->Seek(cur, STREAM_SEEK_SET, NULL));
}
else
#endif
{
if (crc != crcFromArchive)
ThrowIncorrect();
}
db.ArchiveInfo.StartPositionAfterHeader = _arhiveBeginStreamPosition + kHeaderSize;
(...)
}
可以看到, 下面这几句是在从 头Header
上读取 尾Header
信息.
UInt32 crcFromArchive = Get32(_header + 8);
UInt64 nextHeaderOffset = Get64(_header + 0xC);
UInt64 nextHeaderSize = Get64(_header + 0x14);
UInt32 nextHeaderCRC = Get32(_header + 0x1C);
UInt32 crc = CrcCalc(_header + 0xC, 20);
紧接着, 后面又有一句判断:
if (crcFromArchive == 0 && nextHeaderOffset == 0 && nextHeaderSize == 0 && nextHeaderCRC == 0)
如果这几个都读不到怎么办.
看代码, 接下来, 它会从文件末尾开始, 倒着向前查找特征字符:
if (buf[i] == 0x17 && buf[i + 1] == 0x6 || buf[i] == 0x01 && buf[i + 1] == 0x04)
这就是在找 0x17 0x06
或者 0x01 0x04
这两个特征串.
代码已经很明确了:
如果找到这其中之一, 就算是找到 尾Header
了.
为什么能这么做呢, 这两个串代表什么呢?
通过查看7z的文档(事实上, 前面讲的 尾Header
的结构的时候也能发现)知道.
0x17
是代表压缩Header的标记kEncodedHeader
. 后面的0x06
是 packstream 的标记kPackInfo
.0x01
是代表普通Header的标记kHeader
. 而0x04
是 mainstream 的标记kMainStreamsInfo
.
这正是 7z 的两种 尾Header
的格式.
到这里, 可以总结一下了:
7z 流式压缩
压缩时可以把头Header
中的 偏移量和 crc 和等 留空.
在解压时, 如果检测到这些留空了, 则会从文件末尾开始查找尾Header
特征串.
这种方法可以一定程度实现 流式压缩. 虽然 7z 的文件结构本身,限制它不适合流式压缩的.
欢迎大家讨论交流: Neil 的博客