65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (15投票s)

2003年9月22日

5分钟阅读

viewsIcon

133199

downloadIcon

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() 的代码流程:

  1. 构建要打包的所有文件的 packdata_t 数组(存储它们的名称和大小)
  2. 创建存档文件并在其中写入 Signature 和文件计数
  3. packdata_t 数组写入存档
  4. 开始读取每个文件并将其内容写入存档
  5. 循环(4)直到所有文件都存储完毕
  6. 关闭存档文件

此操作足以将所有文件打包到一个存档文件中。现在我们直接进入代码。

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;
}

编写解包器

由于打包过程已详细解释,解包部分变得更加直观;因此,只展示代码流程。

  1. 打开存档文件
  2. 读取打包头部
  3. 验证签名——如果无效,则报告并退出
  4. 读取打包头部(SignatureNumOfFilespackdata_t 数组)后,开始提取文件
  5. 创建一个名为 packdata_t[idx].FileName 的新文件,并从存档文件中写入其内容
  6. 处理下一个文件
  7. 关闭存档文件并退出
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 字段中提取值。

简而言之,解包器存根的工作原理如下:

  1. 调用 SfxGetInsertPos() 获取存档文件的位置
  2. 调用 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.cppUnpacker.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\

最终注释

希望您喜欢阅读本文并学到了一些新东西。

© . All rights reserved.