7z作为开源的解压缩项目,支持多种格式的解压缩,由 Igor Pavlov 开发,最新的版本为 19.00 版。
源码下载位置:https://www.7-zip.org/a/7z1900-src.7z
1、源码结构
源码解压之后,是这样的结构:
| 路径 | 备注 |
|---|---|
| Asm | 包含主要算法的汇编实现,直接使用汇编可以提高执行效率。但是却对跨平台移植造成了一些困难 |
| C | 主要是算法的代码,由C语言实现 |
| CPP | 相关COM接口的实现,界面,工程文件等 |
| DOC | 相关文档 |
对于我们编译项目来说,最主要的就是 CPP 文件夹,编译的项目文件在 CPP\7zip\Bundles\ 中可以找到。
| 路径 | 备注 |
|---|---|
| Alone | 独立的可执行程序,支持的解压格式仅包括7z, cab, tar, zip这几种。 |
| Alone7z | 独立的可执行程序,仅支持7z格式。FM文件管理器(File Manager),通过加载7z.dll的导出函数进行解压。 |
| Format7z | 7za.dll 7z Standalone Plugin,7z 独立插件(仅7z格式) |
| Format7zF | 7z.dll 7z Plugin, 7z插件,包含各种格式。 |
| SFXCon | 自解压(控制台程序)。 |
| SFXSetup | 自解压程序(安装包)。 |
| SFXWin | 自解压程序(Windows界面)。 |
2、代码的编译
打开CPP\7zip\Bundles\Format7zF\Format7z.dsw,即可打开7z.dll工程。
我使用的编译器是vs2008,打开dsw文件提示升级,转换后可生成sln文件和对应的vcproj文件。
直接编译。第一次编译,报错:1>LINK : 无法创建 .ILK 文件的映射;正在非增量链接
1>LINK : fatal error LNK1104: 无法打开文件“C:\Program Files\7-Zip\7z.dll”
解决方法:文件占用,编译器权限不够,不能对此文件进行修改,修改生成目标地址,生成到其他地方就可以了。
第二次编译,报错:
1>正在链接...
1>.\Debug\7zCrcOpt.obj : fatal error LNK1107: 文件无效或损坏: 无法在 0x276 处读取
解决方法:删除工程中的asm文件,改由对应的c文件实现,记得将这些c文件的预编译头选项改为“不使用预编译头”。
第三次编译,成功。
3、内部接口
7z.dll 中的每一种支持的格式被称作 Archive,代码位于CPP\7zip\Archive中。
每一种Archive包含一个Handler,Handler里包含处理每一种Archive的接口。
每一种Archive包含一个Register,用于向全局对象注册,只有注册后的Handler才会被调用。
Handler必须继承IInArchive接口,表示可读,用于解压。(必选)
Handler可以继承IOutArchive接口,表示可写,用于压缩文档。(可选)
根据官方说明:
Packing / unpacking: 7z, XZ, BZIP2, GZIP, TAR, ZIP and WIM
仅这几种Archive是可以被压缩的,因此这些Archive的Handler要继承IOutArchive接口。
如果代码只用于解压,而不用于压缩,可定义EXTRACT_ONLY宏,可不生成IOutArchive,可以减少文件体积。
| 文件 | 接口 | 说明 |
|---|---|---|
| ICoder.h | ICompressProgressInfo | 设置进度,用于向外部展示进度条 |
| ICompressCoder | 解码 | |
| ICompressCoder2 | 同上,传出多个Stream对象 | |
| ICompressSetCoderPropertiesOpt | 设置属性 | |
| ICompressSetCoderProperties | 设置属性 | |
| ICompressSetDecoderProperties2 | 设置属性 | |
| ICompressWriteCoderProperties | 将属性写入到Stream | |
| ICompressGetInStreamProcessedSize | 获取已经处理的大小 | |
| ICompressSetCoderMt | 设置进程数 | |
| ICompressSetFinishMode | 设置结束标志 | |
| ICompressGetInStreamProcessedSize2 | 获取已经处理的大小 | |
| ICompressSetMemLimit | 设置内存限制 | |
| ICompressGetSubStreamSize | 获取内部流文件大小 | |
| ICompressSetInStream | 设置压缩传入的InStream流对象 | |
| ICompressSetInStreamSize | 设置压缩传入的InStream流对象大小 | |
| ICompressSetOutStreamSize | 设置压缩传出的InStream流对象 | |
| ICompressSetBufSize | 设置缓冲区大小 | |
| ICompressInitEncoder | 初始化编码器 | |
| ICompressSetInStream2 | 设置输入流 | |
| ICompressSetOutStream2 | 设置输出流 | |
| ICompressSetInStreamSize2 | 设置输入流大小 | |
| ICompressFilter | 设置过滤器,只处理小于等于size的文档 | |
| ICompressCodecsInfo | 获取压缩解码器信息 | |
| ISetCompressCodecsInfo | 设置压缩编码器信息 | |
| ICryptoProperties | 加密属性 | |
| ICryptoResetInitVector | 加密,重置InitVector | |
| ICryptoSetPassword | 设置密码,用户处理加密文档。 | |
| ICryptoSetCRC | 设置CRC,用于处理加密文档的。 | |
| IHasher | 计算哈希接口 | |
| IHashers | 哈希管理器 | |
| IStream.h | ISequentialInStream | 顺序可读文件流 |
| ISequentialOutStream | 顺序可写文件流 | |
| IInStream | 随机可读文件流(在ISequentialInStream基础上增加Seek函数) | |
| IOutStream | 随机可写文件流(在ISequentialOutStream基础上增加Seek/SetSize函数) | |
| IStreamGetSize | 获取文件流大小 | |
| IOutStreamFinish | 为可写文件流设置结束状态 | |
| IStreamGetProps | 获取文件流的属性 | |
| IStreamGetProps2 | 获取文件流的属性 | |
| IArchive.h | IInArchive | 可读文档(用于输入) |
| IArchiveGetRawProps | 文档属性 | |
| IArchiveGetRootProps | 根文档属性 | |
| IArchiveOpenSeq | 将顺序流打开为文档 | |
| IArchiveUpdateCallback | 设置文档更新回调函数 | |
| IArchiveUpdateCallback2 | 设置文档更新回调函数 | |
| IArchiveUpdateCallbackFile | 设置文档更新回调函数(到输出流) | |
| IOutArchive | 可写文档(用于输出) | |
| ISetProperties | 设置属性 | |
| IArchiveKeepModeForNextOpen | 下次打开时保持相同模式 | |
| IArchiveAllowTail | 允许尾部数据 |
4、外部接口
调用通过IDA打开7z.dll可发现其导出函数。
| 函数 | 说明 |
|---|---|
| CreateDecoder | 创建解码器 |
| CreateEncoder | 创建编码器 |
| CreateObject | 创建对象 |
| GetHandlerProperty2 | 获取Handler属性 |
| GetHandlerProperty | 获取Handler属性 |
| GetHashers | 获取IHasher对象 |
| GetIsArc | 获取IsArc函数地址 |
| GetMethodProperty | 获取解码器属性。传入codecIndex和PROPID,传出PROPVARIANT* |
| GetNumberOfFormats | 获取文件格式的数量。(指:7z,zip,rar等文件格式) |
| GetNumberOfMethods | 获取解码器的数量。(指:BCJ2,LZMA,Deflate等格式编码) |
| SetCaseSensitive | 设置当前文件系统是否大小写敏感,WINDOWS默认不敏感,其他系统默认敏感。 |
| SetCodecs | 传入ICompressCodecsInfo对象,设置外部解码器。 |
| SetLargePageMode | 设置大内存页模式,这种模式可申请更多的内存。 |
5、解码器
解码器通过【注册】的方式,注册到全局变量g_Arcs中。
@ CPP\7zip\UI\Common\LoadCodecs.cpp
static const unsigned kNumArcsMax = 64;
static unsigned g_NumArcs = 0;
static const CArcInfo *g_Arcs[kNumArcsMax];
根据定义g_Arcs最多可以容纳64种不同的解码器。
CArcInfo的定义如下:
struct CArcInfo
{
UInt16 Flags;
Byte Id;
Byte SignatureSize;
UInt16 SignatureOffset;
const Byte *Signature;
const char *Name;
const char *Ext;
const char *AddExt;
Func_CreateInArchive CreateInArchive;
Func_CreateOutArchive CreateOutArchive;
Func_IsArc IsArc;
bool IsMultiSignature() const
{ return (Flags & NArcInfoFlags::kMultiSignature) != 0; }
};
CArcInfo各主要成员的含义:
| 成员 | 说明 |
|---|---|
CArcInfo::Flags | 定义在@CPP\7zip\Archive\IArchive.h中,NArcInfoFlags有详细说明。 |
CArcInfo::Id | Archive的ID标识符,例如:7z=7, Rar=3。 |
CArcInfo::SignatureSize | 解码器标识的长度。 |
CArcInfo::Signature | 解码器标识符,例如:zip={0x50, 0x4B, 0x03, 0x04},7z={'7' + 1, 'z', 0xBC, 0xAF, 0x27, 0x1C}。等。 |
CArcInfo::Name | 解码器名称。 |
CArcInfo::Ext | 解码器扩展名。 |
CArcInfo::CreateInArchive | 函数指针,创建解码器InArchive对象,用于打开文件用于解压。 |
CArcInfo::CreateOutArchive | 函数指针,创建解码器OutArchive对象,用于创建文件用于压缩。 |
CArcInfo::IsArc | 函数指针,判断文件格式是否合法。 |
CArcInfo::IsMultiSignature | 判断是否有多个Signature。 |
各解码器通过RegisterArc.h中封装的宏进行注册。
如:ZIP 的注册代码位于CPP\7zip\Archive\Zip\ZipRegister.cpp中,我将代码贴出来。
#include "StdAfx.h"
#include "../../Common/RegisterArc.h"
#include "ZipHandler.h"
namespace NArchive {
namespace NZip {
static const Byte k_Signature[] = {
4, 0x50, 0x4B, 0x03, 0x04, // Local
4, 0x50, 0x4B, 0x05, 0x06, // Ecd
4, 0x50, 0x4B, 0x06, 0x06, // Ecd64
6, 0x50, 0x4B, 0x07, 0x08, 0x50, 0x4B, // Span / Descriptor
6, 0x50, 0x4B, 0x30, 0x30, 0x50, 0x4B }; // NoSpan
REGISTER_ARC_IO(
"zip", "zip z01 zipx jar xpi odt ods docx xlsx epub ipa apk appx", 0, 1,
k_Signature,
0,
NArcInfoFlags::kFindSignature |
NArcInfoFlags::kMultiSignature |
NArcInfoFlags::kUseGlobalOffset,
IsArc_Zip)
}}
6、相关宏开关
| 宏 | 作用 |
|---|---|
_7ZIP_ST | Single-Thread单线程,默认未定义,开启后将不编译多线程相关逻辑 |
_7ZIP_LARGE_PAGES | 开启大内存页,默认未开启 |
_SZ_ALLOC_DEBUG | 默认不开启,开启后可输出内存申请与释放的Log |
USE_MIXER_MT | Multiple_Thread 多线程解码器,不可与USE_MIXER_ST同时开启。 |
USE_MIXER_ST | Single_Thread 单线程解码器,不可与USE_MIXER_MT同时开启。 |
EXTRACT_ONLY | 开启后只包含解压逻辑,不包含压缩逻辑。 |
NSIS_SCRIPT | 是否将NSIS脚本解压,默认关闭,需要手动修改CPP\7zip\Archive\Nsis\Nsis.h开启 |
EXTERNAL_CODECS | 是否使用外部解码器,全局变量g_ExternalCodecs负责加载外部解码器。CPP\7zip\UI\Agent\Agent.cpp@LoadGlobalCodecs()中包含g_ExternalCodecs相关初始化逻辑。 |
NEW_FOLDER_INTERFACE | 使用新文件夹操作接口:IFolderOperations和IFolderSetFlatMode |
NO_READ_FROM_CODER | 禁止从解码器读取数据,默认未定义 |
USE_WIN_FILE | 默认开启,开启后使用Windows API处理文件(CreateFile/CloseHandle等),否则使用C函数处理文件(如open,close等)。 |
7、接口的调用
1. 模块加载
使用LoadLibrary/GetProcAddress(Windows)或dlopen/dlsym(Linux)获取函数地址。
2. 获取文件格式数量
DWORD dwFormat;
HRESULT hr = GetNumberOfFormats(&dwFormat);
3. 获取每种格式的GUID
for(DWORD i=0;i<dwFormat;++i)
{
PROPVARIANT propvar;
propvar.vt = VT_EMPTY;
HRESULT hr = GetHandlerProperty2(i, NArchive::NHandlerPropID::kClassID, &propvar)
GUID clsid = *(const GUID *)propvar.bstrVal;
SysFreeString(propvar.bstrVal);
// todo
......
}
在GetHandlerProperty2内部,是通过SetPropGUID()函数将classid的值传递给PROPVARIANT的。
内部调用了SysAllocStringByteLen(),为避免内存泄漏,获取成功后应当调用SysFreeString()释放
static inline HRESULT SetPropStrFromBin(const char *s, unsigned size, PROPVARIANT *value)
{
if ((value->bstrVal = ::SysAllocStringByteLen(s, size)) != 0)
value->vt = VT_BSTR;
return S_OK;
}
static inline HRESULT SetPropGUID(const GUID &guid, PROPVARIANT *value)
{
return SetPropStrFromBin((const char *)&guid, sizeof(guid), value);
}
注:文档CPP\7zip\Archive\Guid.txt中有一段描述,是关于格式classid的。
返回的classid都应当符合这种格式{23170F69-40C1-278A-1000-000110xx0000}
中间两位xx在下表中可以找到对应关系,如果只是希望打开指定格式的文档,直接指定classid即可,不需要通过GetHandlerProperty2来获取。
Handler GUIDs:
{23170F69-40C1-278A-1000-000110xx0000}
01 Zip
02 BZip2
03 Rar
04 Arj
05 Z
06 Lzh
07 7z
08 Cab
09 Nsis
0A lzma
0B lzma86
0C xz
0D ppmd
......
4. 创建IInArchive对象
CMyComPtr<IInArchive> parc;
HRESULt hr = CreateObject(&clsid, &IID_IInArchive, (void**)&parc));
注:我在Linux版本调试时,遇到了崩溃的问题,调试之后发现,7z中的IUnknown接口在Linux中使用了虚析构函数。
这与我代码中已有的IUnknown定义不一致,创建的对象会有虚表地址,因此调用的函数地址错位导致崩溃。
DEFINE_GUID(IID_IUnknown,
0x00000000, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46);
struct IUnknown
{
STDMETHOD(QueryInterface) (REFIID iid, void **outObject) PURE;
STDMETHOD_(ULONG, AddRef)() PURE;
STDMETHOD_(ULONG, Release)() PURE;
#ifndef _WIN32
virtual ~IUnknown() {}
#endif
};
5. 获取包中文件数量
在IInArchive->Open返回成功之后。可通过调用IInArchive->GetNumberOfItems()获取文档中包含的文件数量。
UInt32 dwItems;
HRESULT hr = parc->GetNumberOfItems(&dwItems);
6. 解压
IInArchive->Extract负责文档的解压。
DWORD dw; // dw 为要解压的文档ID,从0开始
C7zArchiveOpenCB opencb(); // 自定义一个CallBack类,继承自IArchiveExtractCallBack接口
CMyComPtr<IArchiveExtractCallback> pcb = &opencb();
HRESULT hr = parc->Extract(&dw, 1, 0/*TestMode*/, pcb));
CMyComPtr<ISequentialOutStream> pSeqOutStm;
hr = pcb->GetStream(0, &pSeqOutStm, 0); // pSeqOutStm 为解压文件流,只支持顺序写入
// 需要调用者对pSeqOutStm进行包装,使其支持Read/Seek/Tell等操作
7. 关闭IInArchive
解压完毕后,调用IInArchive->Close关闭IInArchive对象,以便回收内存防止泄漏。
parc->Close();
------先写这么多,后续更------
本文详细介绍了7z源码的结构、编译过程、内部与外部接口,以及解码器的注册和接口调用方法。在编译过程中,遇到的错误及解决办法也一并分享。内容涵盖从源码结构到实际解压操作的完整流程。
1540





