如何用自解压文件 (SFX) 编写简单的打包器/解包器





4.00/5 (15投票s)
2003年9月22日
5分钟阅读

133199

3186
一个使用打包和解包例程编写自解压文件的例子。
引言
在这篇文章中,我将展示如何编写一个文件打包器/解包器,以及如何创建一个自解压版本的存档(SFX)。
请注意,本文档和代码是为学习目的而编写的,并非用于复杂的功能,因此存在以下限制:
- 仅支持文件打包(将它们合并成一个文件),不支持压缩
- 打包器不打包子目录中的文件
- 打包器头部并未真正优化——仅够我们使用
- 此处展示的所有代码都可作为控制台应用程序编译,不提供 GUI 版本
存档文件格式
想法是构建一个结构/格式,允许我们将文件列表和文件内容保存在一个文件中,以便我们能够将文件恢复到原始状态。
因此,打包头部设计如下:
-
Signature
- 偏移量 0x02/DWORD
这将占据头部的最初 4 个字节。它将包含一个简单的签名,允许我们识别我们的打包文件。 -
NumOfFiles
- 偏移量 0x04/DWORD
此处我们存储一个DWORD
,其中包含主题中文件的数量。 -
FilesInfo
- 偏移量 0x08/sizeof(packdata_t)
此处我们开始按定义的顺序存储文件信息,数组为packdata_t FileInfo[NumOfFiles]
。packdata_t
结构定义如下:struct packdata_t { char FileName[MAX_PATH]; long filesize; }
正如您所注意到的,我们只保存了文件的大小和名称。
packdata_t
结构不是存储文件名或信息的最佳方式,因为我们可以使用可变长度的packdata_t
结构,定义为:struct packdata_t { long filesize; // Other file info, such as creation date , attributes, ... char filenameLength; char FileName[1]; }
但是,管理这个最后的结构当然超出了本文的范围。
在打包头部之后,我们按顺序存储了文件的内容。所以整个存档文件的格式看起来像这样:
签名 |
NumOfFiles |
packdata_t Files[NumOfFiles] |
File1 内容 |
File2 内容 |
. |
. |
. |
File(NumOfFiles) 内容 |
编写打包器
为了使代码更具可扩展性,我定义了一个结构,它将保存从打包器/解包器例程内部触发的回调函数。这些回调用于视觉通知和更新。
回调结构定义如下:
typedef struct
{
void (*newfile)(char *name, long size);
void (*fileprogress)(long pos);
} packcallbacks_t;
每当打包器/解包器遇到或处理新文件时,都会调用 newfile()
回调。它将接收文件的名称和大小。
当操作正在进行时,会调用 fileprogress()
回调。它将接收打包器/解包器当前正在处理的当前位置。
现在,让我们定义 packfiles 函数的原型:
int packfilesEx(char *path, char *mask, char *archive,
packcallbacks_t * pcb = NULL);
- 我们需要一个
path
来指定源目录。 mask
,它将告诉我们搜索和打包哪些文件。archive
,它将保存存档文件名。- 可选的
pcb
,它将保存用于视觉通知的回调列表。
在进入代码之前,这是 packfilesEx()
的代码流程:
- 构建要打包的所有文件的
packdata_t
数组(存储它们的名称和大小) - 创建存档文件并在其中写入
Signature
和文件计数 - 将
packdata_t
数组写入存档 - 开始读取每个文件并将其内容写入存档
- 循环(4)直到所有文件都存储完毕
- 关闭存档文件
此操作足以将所有文件打包到一个存档文件中。现在我们直接进入代码。
int packfilesEx(char *path, char *mask, char *archive, packcallbacks_t *pcb) { TCHAR szCurDir[MAX_PATH]; // define a vector that will hold the packdata_t array. // STL Vectors are stored in contiquous memory. std::vector<packdata_t> filesList; // make sure the current source directory is valid // and change working directory to it if so. // save current directory GetCurrentDirectory(MAX_PATH, szCurDir); // go to new working directory if (!SetCurrentDirectory(path)) return packerrorPath; WIN32_FIND_DATA fd; HANDLE findHandle; packdata_t pdata; findHandle = FindFirstFile(mask, &fd); if (findHandle == INVALID_HANDLE_VALUE) return packerrorNoFiles; long lTemp; // this loop is for storing file's headers only // directories are omitted do { // skip directory entries if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) continue; // clear record memset(&pdata, 0, sizeof(pdata)); // fill packdata entry strcpy(pdata.filename, fd.cFileName); pdata.filesize = fd.nFileSizeLow; // save entry filesList.push_back(pdata); } while(FindNextFile(findHandle, &fd)); FindClose(findHandle); FILE *fpArchive = fopen(archive, "wb"); if (!fpArchive) return packerrorCannotCreateArchive; // write signature lTemp = 'KCPL'; // lallous pack! (L-PCK) fwrite(&lTemp, sizeof(lTemp), 1, fpArchive); // write entries count lTemp = filesList.size(); fwrite(&lTemp, sizeof(lTemp), 1, fpArchive); // store files entries (since std::vector stores elements // in a linear manner) fwrite(&filesList[0], sizeof(pdata), filesList.size(), fpArchive); // process all files to copy for (unsigned int cnt=0;cnt<filesList.size();cnt++) { FILE *inFile = fopen(filesList[cnt].filename, "rb"); long size = filesList[cnt].filesize; // if callback assigned then trigger it if (pcb && pcb->newfile) pcb->newfile(filesList[cnt].filename, size); // copy file name long pos = 0; while (size > 0) { char buffer[4096]; long toread = size > sizeof(buffer) ? sizeof(buffer) : size; fread(buffer, toread, 1, inFile); fwrite(buffer, toread, 1, fpArchive); pos += toread; size -= toread; if (pcb && pcb->fileprogress) pcb->fileprogress(pos); } fclose(inFile); } // close archive and restore working directory fclose(fpArchive); SetCurrentDirectory(szCurDir); return packerrorSuccess; }
编写解包器
由于打包过程已详细解释,解包部分变得更加直观;因此,只展示代码流程。
- 打开存档文件
- 读取打包头部
- 验证签名——如果无效,则报告并退出
- 读取打包头部(
Signature
、NumOfFiles
、packdata_t
数组)后,开始提取文件 - 创建一个名为
packdata_t[idx].FileName
的新文件,并从存档文件中写入其内容 - 处理下一个文件
- 关闭存档文件并退出
int unpackfileEx(char *archive, char *dest, packcallbacks_t * pcb, long startPos) { FILE *fpArchive = fopen(archive, "rb"); // failed to open archive? if (!fpArchive) return packerrorCouldNotOpenArchive; long nFiles; if (startPos) fseek(fpArchive, startPos, SEEK_SET); // read signature fread(&nFiles, sizeof(nFiles), 1, fpArchive); if (nFiles != 'KCPL') return (fclose(fpArchive), packerrorNotAPackedFile); // read files entries count fread(&nFiles, sizeof(nFiles), 1, fpArchive); // no files? if (!nFiles) return (fclose(fpArchive), packerrorNoFiles); // read all files entries std::vector<packdata_t> filesList(nFiles); fread(&filesList[0], sizeof(packdata_t), nFiles, fpArchive); // loop in all files for (unsigned int i=0;i<filesList.size();i++) { FILE *fpOut; char Buffer[4096]; packdata_t *pdata = &filesList[i]; // trigger callback if (pcb && pcb->newfile) pcb->newfile(pdata->filename, pdata->filesize); strcpy(Buffer, dest); strcat(Buffer, pdata->filename); fpOut = fopen(Buffer, "wb"); if (!fpOut) return (fclose(fpArchive), packerrorExtractError); // how many chunks of Buffer_Size is there is in filesize? long size = pdata->filesize; long pos = 0; while (size > 0) { long toread = size > sizeof(Buffer) ? sizeof(Buffer) : size; fread(Buffer, toread, 1, fpArchive); fwrite(Buffer, toread, 1, fpOut); pos += toread; size -= toread; if (pcb && pcb->fileprogress) pcb->fileprogress(pos); } fclose(fpOut); nFiles--; } fclose(fpArchive); return packerrorSuccess; }
编写自解压文件 (SFX)
SFX 只是解包器的一个特殊版本(我们将称之为 UnpackerStub),它不是通过命令行接收存档文件,而是查找嵌入其中的存档文件。
如果你是数学迷,你可以认为 SFX 是“UnpackerStub.exe + Archive.bin = UnpackerArchive.exe”。
那么如何将存档文件嵌入解包器中形成 SFX 呢?
为此,我们需要在 UnpackerStub 中写入一些信息,以帮助它定位 Archive.bin 的主体。
为此,我使用了 IMAGE_DOS_HEADER
中的 e_res2
字段来存储解包器存根中存档数据的指针。
每个可执行文件都有一个经过良好文档记录和定义的格式,它将指示和告诉操作系统如何加载/运行它。IMAGE_DOS_HEADER
(定义在 WINNT.H 中)位于每个可执行文件的偏移量零处,并具有以下字段:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
我将存档文件地址的指针存储在足够容纳 DWORD 的 e_res2
字段中。存储存档指针后,我确保将存档内容附加到 UnpackerStub 中该指针的位置。
已编写两个函数来获取/存储存档数据的指针:
int SfxSetInsertPos(char *filename, long pos) { FILE *fp = fopen(filename, "rb+"); if (fp == NULL) return packerrorCouldNotOpenArchive; IMAGE_DOS_HEADER idh; // read dos header fread((void *)&idh, sizeof(idh), 1, fp); // adjust position value in an unused MZ field *(long *)&idh.e_res2[0] = pos; // update header rewind(fp); fwrite((void *)&idh, sizeof(idh), 1, fp); fclose(fp); return packerrorSuccess; }
此函数将存储指针。首先读取头部,更新 e_res2
字段,然后再次写回头部。
int SfxGetInsertPos(char *filename, long *pos) { FILE *fp = fopen(filename, "rb"); if (fp == NULL) return packerrorCouldNotOpenArchive; IMAGE_DOS_HEADER idh; fread((void *)&idh, sizeof(idh), 1, fp); fclose(fp); *pos = *(long *)&idh.e_res2[0]; return packerrorSuccess; }
此函数将读取头部并从 e_res2 字段中提取值。
简而言之,解包器存根的工作原理如下:
- 调用
SfxGetInsertPos()
获取存档文件的位置 - 调用
UnpackFilesEx()
,同时传递存档文件的位置(嵌入的 archive.bin 的起始位置)以及存档文件名(通过调用GetModuleFileName(NULL, ...)
计算得出)。
现在我继续描述打包器如何构建 SFX。
// check if unpackerstub.exe exists if (GetFileAttributes(sfxStubFile) == (DWORD)-1) { printf("SFX stub file not found!"); return 1; } // open archive file FILE *fpArc = fopen(argv[3], "rb"); if (!fpArc) { printf("Failed to open archive!\n"); return 1; } // get archive size fseek(fpArc, 0, SEEK_END); long arcSize = ftell(fpArc); rewind(fpArc); // form output sfx file name char sfxName[MAX_PATH]; strcpy(sfxName, argv[3]); strcat(sfxName, ".sfx.exe"); // take a copy from SFX if (!CopyFile(sfxStubFile, sfxName, FALSE)) { fclose(fpArc); printf("Could not create SFX file!\n"); return 1; } // append data to SFX FILE *fpSfx = fopen(sfxName, "rb+"); fseek(fpSfx, 0, SEEK_END); // get SFX size before archive appending long sfxSize = ftell(fpSfx); // start appending from archive file to the end of SFX file char Buffer[4096 * 2]; while (arcSize > 0) { long rw = arcSize > sizeof(Buffer) ? sizeof(Buffer) : arcSize; fread(Buffer, rw, 1, fpArc); fwrite(Buffer, rw, 1, fpSfx); arcSize -= rw; } fclose(fpArc); fclose(fpSfx); // mark archive data position inside SFX SfxSetInsertPos(sfxName, sfxSize); // delete archive file while keeping only the SFX DeleteFile(argv[3]); printf("SFX created: %s\n", sfxName);
就是这样!
使用代码和二进制文件
本文档附带 Packer.cpp 和 Unpacker.cpp,这两个示例演示了如何使用打包和解包功能。
Packer.exe 用法
您应始终指定 **完整路径**,因为目前不支持相对路径。
c:>packer e:\temp\bc *.* e:\test.bin
这将把 e:\temp\bc\*.* 的内容打包到 e:\test.bin(存档)中。
如果您添加 'sfx' 作为
c:>packer e:\temp\bc *.* e:\test.bin sfx
将创建一个名为 e:\test.bin.sfx.exe 的 SFX。
Unpacker.exe 用法
确保指定有效的输出目录。
c:\>unpacker e:\test.bin e:\out
这将把 e:\test.bin 的内容解压到 e:\out\。
Sfx.exe 用法
sfx 只接受一个参数,即目标目录。
c:\>sfx.exe e:\out
这将解压到 e:\out\。
最终注释
希望您喜欢阅读本文并学到了一些新东西。