☰
C++文件夹加密压缩:minizip+zlib与AES-256实战解析
2026/10/14 1:17:42 网站建设 项目流程

简介:多级文件夹在传输和备份时往往需要加密压缩,这份资源基于minizip与zlib,在VS2019环境下演示如何将多级目录加密打包为zip文件,同时提供对应的zip解压缩实现,生成的标准zip可被WinRAR、360zip等常用工具解密解压。资源主要面向已经接触过zlib库编译、希望进一步封装文件备份或加密传输模块的C++开发者,也可作为学习minizip接口调用的参考工程。压缩包共19个文件,整体约167KB,包括7个头文件、3个c文件、2个cpp文件,并配有json配置、Visual Studio工程文件(sln、vcxproj)和依赖的lib静态库;其中头文件与c/cpp源文件构成minizip核心及工具类封装,工程文件与配置文件便于直接打开调试。已有806人学习,资源虽小但胜在完整:从递归压缩多级文件夹、设置加密口令,到解压还原目录结构,关键代码均有体现,还附有测试目录和示例入口,适合快速移植到实际项目中。

1. C++ minizip+zlib 文件夹加密压缩:需求、场景与边界

文件夹加密压缩,听起来就是“把一堆文件装进 zip,再挂个密码”,但真正落到 C++ 工程里,你会发现这条路没有想象中直:调系统命令不跨平台,用重型压缩库又太重。minizip + zlib 是 zlib 官方自带、很多人容易忽略的组合——minizip 负责 zip 容器读写,zlib 负责 Deflate 压缩,再配上社区维护的 AES 扩展,就能做到不依赖外部命令的文件夹级加密压缩。它能解决一类很典型的落地需求:程序内部把整个目录连同子目录一起加密打包,交付给下游后用同一套口令解开还原。适合做数据备份、离线交付、日志包加密上传这些场景。这篇就按实际动手的顺序讲:选哪个 minizip、库怎么编译出来、递归压缩解压怎么写、参数怎么调,最后把最容易翻车的中文路径、加密标识不生效、密码校验失败这些坑挨个排查一遍。源码级的东西,照着调就行。

2. minizip 选型与编译准备:基础版和 AES 增强版差在哪

2.1 基础版 minizip 为什么不能直接用于加密场景

很多人第一次接触 minizip,是从 zlib 源码包里的 contrib/minizip 目录发现的。这个基础版的文件构成是 zip.c、unzip.c、ioapi.c 这一组源码,配套 zip.h、unzip.h 两个头文件,功能定位很明确:把 zip 容器读写和 zlib 的 Deflate 压缩串起来。它能创建 zip、追加文件、解压条目、读 zip 中央目录,但加密能力几乎可以忽略。基础版在写入文件成员时虽然也接受一个 password 参数,但走的是老掉牙的 ZipCrypto 方案,而且不同发行版对 ZipCrypto 的完整程度参差不齐,有的版本根本不会真正加密,只是把密码标记写进去。

这里要说明一个关键区别:zip 加密和文件加密不是一回事。ZipCrypto 是 PKZIP 2.0 时代定义的流加密,密钥从一个口令用简单哈希展开,加密强度在今天的算力面前很弱;WinZip AES 则是后来 WinZip 提出的标准,基于 AES 块密码和 PBKDF2 口令派生,业界共识是把 AES-256 作为默认选项。标题里的“加密压缩”,如果只是传个 password 给基础版 minizip,得到的很可能是 ZipCrypto,安全级别不及格。所以我的选型结论很直接:凡是需求里写着“加密”,就别在基础版上浪费时间,直接找支持 AES 的增强版 minizip。

怎么判断你拿到的是不是增强版?看 zip.h 里有没有 AES_METHOD 这个宏,以及有没有 zipOpenNewFileInZip4_64 这样的函数声明。基础版的最高接口是 zipOpenNewFileInZip3_64,参数止于 zip64 标志;增强版会把 AES 版本号、AES 密钥长度、密码校验值长度三个参数追加在后面,这是最有辨识度的一处差异。拿到支持 AES 的版本后,压缩包里的加密条目才会真正带 WinZip AES 标记,主流解压工具都能认。

2.2 编译带 AES 的增强版 minizip:依赖、选项与链接

增强版 minizip 的外部依赖主要是两个:zlib 本身,以及它自带的 crypt 对 AES 的实现。常见的构建方式是把增强版源码整个目录拿下来,里面通常自带 CMakeLists.txt,顶层选项会有 minizip 相关开关。我一般建议静态编译,部署时少一层动态库依赖。Linux 和 macOS 下的大致流程:

mkdir -p build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release cmake --build . -j4 # 查找产物,确认 minizip 静态库和 crypt 库是否生成 find . -name "*minizip*" -o -name "*crypt*" | sort

这段命令的作用是把增强版源码用 CMake 配置成 Release 构建,然后编译并在 build 目录里搜索产物。cmake 输出里的静态库一般会以 libminizip.a、libcrypt.a 这种名字出现;如果你在工程里会同时用到 zlib 的函数,还需要提前把 zlib 编好,或者在根 CMakeLists 里开启 zlib 子目录选项。参数怎么改:如果只想在本地验证功能,不关心部署,可以把 CMAKE_BUILD_TYPE 换成 Debug;如果嫌静态库占体积,可以改成 BUILD_SHARED_LIBS=ON 生成动态库,但后续拷贝程序时记得带上 so 或者 dll。

Windows 上用 MSVC 时有一点不同,CMake 需要显式指定架构:

mkdir build && cd build cmake .. -A x64 -DCMAKE_INSTALL_PREFIX=./out cmake --build . --config Release cmake --install .

这里的 -A x64 是指定 64 位工具链,避免默认生成 32 位库;-DCMAKE_INSTALL_PREFIX 让安装产物统一落到 out 目录,后面集成到自己的 C++ 工程时直接引用 out/include 和 out/lib 就行。Windows 下最容易遇到的编译报错是 crypt 相关文件里用的某些 POSIX 写法在 MSVC 下不兼容,比如 unistd.h 缺失。常见做法是给工程添加一个兼容头文件,或者在预处理里定义 WIN32_LEAN_AND_MEAN、_CRT_SECURE_NO_WARNINGS 再重编。

链接阶段,你自己的目标文件需要同时链接 minizip、crypt、zlib 三个部分,顺序尽量按照“目标文件 → minizip → crypt → zlib”来,避免符号解析顺序问题:

g++ main.cpp -o ziptool \ -I./build/out/include \ -L./build/out/lib \ -lminizip -lcrypt -lz -lpthread

-I 和 -L 分别指向头文件与库文件目录,-lminizip -lcrypt -lz 依次引入三个库。静态链接时如果遇到 undefined reference,先检查是不是顺序反了,再把 -lz 挪到末尾重试。这一步过完,你手里就有一个真正带 AES 能力的 minizip 库了。

2.3 编译成功之后做一次接口探测

库编出来只能说明代码能编译,不能说明 AES 接口真的可用。我习惯在动手写业务代码之前,先用一组小命令探一下头文件,把“黑匣子”提前打开。

grep -n "AES_METHOD" build/out/include/zip.h grep -n "zipOpenNewFileInZip4_64" build/out/include/zip.h grep -n "UNZ_BADPASSWORD" build/out/include/unzip.h

如果三个 grep 都有输出,说明你拿到的是增强版,且加密和解密的错误码约定都在。AES_METHOD 是压缩条目加密算法的标记值,zipOpenNewFileInZip4_64 是开启 AES 写入的入口,UNZ_BADPASSWORD 是解压时口令错误的返回码。任何一个没搜到,都说明头文件版本不对,继续往下写压缩代码是浪费时间。这一步是血泪经验换来的,之前我直接拿基础版写业务,结果压缩出来的包在主流解压软件里根本没有加密项,排查了整整一天。

顺带把编译期检查写进代码里,防止以后有人误换头文件导致库和声明不匹配:

#include "zip.h" #ifndef AES_METHOD #error "当前 minizip 头文件缺少 AES_METHOD,请确认使用的是支持 AES 的增强版" #endif

这段代码的意思是:如果头文件里没有 AES_METHOD 宏,编译期直接报错,而不是等到运行时才发现加密失效。参数不用调,它只是个哨兵检查。到这里,环境准备已经完备,下一步可以写文件夹压缩的逻辑了。

3. 文件夹加密压缩实现:递归遍历与 AES-256 写入

3.1 目录遍历:收集相对路径时的三个注意点

加密压缩文件夹的第一步是把目录结构“拍扁”成 zip 条目列表。用 C++17 的 std::filesystem 做递归遍历是最省事的方案,但直接上手会踩几个细节坑。首先是路径分隔符,Windows 上 std::filesystem 给出的路径默认带反斜杠,而 zip 规范内部统一用正斜杠,所以条目路径必须做一次 generic_string 转换。其次是相对路径基准,条目名必须相对于压缩根目录计算,否则解压时会出现一层多余的根目录。第三是符号链接和特殊文件,recursive_directory_iterator 默认会跟随目录符号链接,搞不好会把你磁盘上的整个目录链卷进来。

#include <filesystem> #include <vector> #include <string> namespace fs = std::filesystem; // 递归收集根目录下所有普通文件,输出 zip 内部相对路径 static void collectFiles(const fs::path& root, std::vector<std::string>& out) { fs::recursive_directory_iterator it( root, fs::directory_options::skip_permission_denied); fs::recursive_directory_iterator end; for (; it != end; ++it) { // 只处理常规文件,符号链接和目录由循环继续处理 if (it->is_regular_file()) { std::string rel = it->path().lexically_relative(root).generic_string(); out.push_back(rel); } } }

这段函数是把 root 目录下所有普通文件的相对路径收集到 vector 里。lexically_relative 负责计算相对路径,generic_string 把分隔符统一成 /。参数说明:root 是压缩根目录,建议传入绝对路径,否则相对路径的基准会随当前工作目录变化;out 是收集结果的输出参数。如果想跳过隐藏目录,可以在循环里判断文件名首字符是否为 .,然后用 it.disable_recursion_pending() 阻止进入该目录。

3.2 写入 ZIP:加密参数、压缩级别与文件属性

收集完文件列表后,真正的核心工作是逐个打开文件、写入 zip。关键点是调用 zipOpenNewFileInZip4_64 时把 AES 参数填对,以及用分块方式写文件内容,避免小内存机器压缩大文件时把进程拖垮。下面是一段可以放进你的工程里直接改写的实现:

#include <cstdio> #include <string> #include "zip.h" // 往打开的 zip 包里追加一个文件成员 int addFileToZip(zipFile zf, const std::string& arcName, // zip 内部路径,必须是正斜杠 const std::string& diskPath, // 磁盘上的实际文件路径 const std::string& password, // 口令,空串表示不加密 int level) { // 压缩级别 0-9,6 是常用值 FILE* fp = fopen(diskPath.c_str(), "rb"); if (!fp) return ZIP_ERRNO; // AES 版本标记,增强版 minizip 用它区分 AES 与传统 ZipCrypto static const unsigned char aes_version[] = { 'A', 'E', 'S' }; // 关键调用:注意最后的三个 AES 参数 int ret = zipOpenNewFileInZip4_64( zf, arcName.c_str(), nullptr, // 无额外时间信息 nullptr, 0, nullptr, 0, nullptr, Z_DEFLATED, // 压缩方法用 Deflate level, // 压缩级别 0, // raw 为 0,走标准压缩流 -MAX_WBITS, // windowBits,与 zlib 约定保持一致 DEF_MEM_LEVEL, // 内存分配级别 Z_DEFAULT_STRATEGY, password.empty() ? nullptr : password.c_str(), 0, 0, 0, 0, // CRC 与尺寸填 0,由库内部计算 1, // 开 zip64,避免大文件超限 password.empty() ? nullptr : aes_version, // 空指针则无 AES 加密 password.empty() ? 0 : 32, // 32 表示 AES-256 password.empty() ? 0 : 10); // 密码校验值长度 10 if (ret != ZIP_OK) { fclose(fp); return ret; } // 分块读入文件内容并写入 zip,64KB 是性能与内存的折中 unsigned char buf[64 * 1024]; size_t n; while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) { ret = zipWriteInFileInZip(zf, buf, (unsigned int)n); if (ret != ZIP_OK) { fclose(fp); return ret; } } fclose(fp); // 关闭当前条目,内部会补全 CRC 与尺寸信息 return zipCloseFileInFileInZip(zf); }

这段代码做的事是把磁盘文件的数据流式写入 zip。要改的参数有三个地方:password 传空串就是明文压缩,传非空且 aes_version 非空就是 AES-256;level 传 0 表示不压缩只存储,适合已经压缩过的文件,传 9 压缩率最高但 CPU 开销大,我一般对混合内容用 6;aes_version 传 nullptr 且 password 非空时,会退回传统 ZipCrypto,不建议用于敏感数据。注释里已经标清楚了每个参数的含义,实际使用时按自己的编译环境核对一下 zip.h 里的函数签名,少数版本对宏参数名有改动。

3.3 在线验证:压缩完立即用 unzList 核对加密标识

写完压缩逻辑别急着收工,我每次都会在同一个程序里加一段验证逻辑:可以是一个命令行参数,也可以是一个内部函数,把刚生成的 zip 重新打开,遍历所有条目并打印加密标识。这一步能立刻暴露“传了密码但加密没生效”这类问题。

#include "unzip.h" // 列出 zip 全部条目,并标记哪些是加密项 static void listEntries(const char* zipPath) { unzFile uf = unzOpen(zipPath); if (!uf) return; unz_global_info gi; unzGetGlobalInfo(uf, &gi); char name[256]; for (unsigned long i = 0; i < gi.number_entry; ++i) { unz_file_info64 fi; int ret = unzGetCurrentFileInfo64( uf, &fi, name, sizeof(name), nullptr, 0, nullptr, 0); if (ret != UNZ_OK) break; // flag 的 bit0 置位表示该条目有加密 bool encrypted = (fi.flag & 1) != 0; printf("%s [%s]\n", name, encrypted ? "encrypted" : "plain"); if (i + 1 < gi.number_entry) { unzGoToNextFile(uf); } } unzClose(uf); }

这段代码遍历 zip 中央目录,把每个成员的名字和加密状态打印出来。fi.flag 来自 zip 压缩包里的通用位标志,bit0 置位就代表条目被加密。验证时你应该看到每个文件的输出都带着 encrypted 标记;如果出现 plain,说明你的写入路径没走到带密码的分支。到这一步,加密压缩的写入侧就算站稳了。

4. 加密压缩包的解压实现:口令校验与目录重建

4.1 解压入口:先判加密标志,再决定走哪条 API

解压侧的入口和压缩侧对称,但有个容易忽略的细节:打开当前条目的 API 需要根据条目是否加密来选择。未加密条目用 unzOpenCurrentFile,加密条目用 unzOpenCurrentFilePassword。增强版实现里,unzOpenCurrentFilePassword 内部会根据条目的加密方法自动区分 AES 和 ZipCrypto,调用方不需要手动指定算法,但前提是你传入的口令得对。

#include <cstdio> #include <string> #include "unzip.h" // 解压当前条目到输出目录,返回 UNZ_OK 表示成功 int extractCurrentFile(unzFile uf, const std::string& outRoot, const std::string& password) { unz_file_info64 fi; char name[1024]; int ret = unzGetCurrentFileInfo64(uf, &fi, name, sizeof(name), nullptr, 0, nullptr, 0); if (ret != UNZ_OK) return ret; // 目录条目的名字以 / 结尾,这里由上层调用处理 size_t len = strlen(name); if (len > 0 && name[len - 1] == '/') { return UNZ_OK; } // 根据加密标志选择打开口令的 API if (fi.flag & 1) { ret = unzOpenCurrentFilePassword(uf, password.c_str()); } else { ret = unzOpenCurrentFile(uf); } if (ret != UNZ_OK) { return ret; // 口令错误时这里返回 UNZ_BADPASSWORD } // 构造安全路径,防止条目里的 ../ 逃逸出输出目录 // safeJoin 的实现见 4.2,这里先省略 FILE* fp = fopen(outRoot.c_str(), "wb"); if (!fp) { unzCloseCurrentFile(uf); return UNZ_ERRNO; } unsigned char buf[64 * 1024]; for (;;) { int n = unzReadCurrentFile(uf, buf, sizeof(buf)); if (n < 0) { ret = n; break; } if (n == 0) break; fwrite(buf, 1, (size_t)n, fp); } fclose(fp); // 关闭条目,这一步会做 CRC 校验并释放内部状态 int closeRet = unzCloseCurrentFile(uf); if (closeRet != UNZ_OK) return closeRet; return ret; }

这段代码是解压循环里的核心动作:读取当前条目的元信息,判断加密与否,按分支打开,然后分块读出数据并写入磁盘。注意 unzReadCurrentFile 返回 0 表示读到了文件尾,返回负数才是错误,所以循环条件里不能把 n == 0 和错误混在一起。参数说明:password 必须是压缩时使用的同一口令;outRoot 参数在示例里写的是文件名,实际工程中应该替换为 4.2 的 safeJoin 结果。

4.2 目录重建与文件写出:不要忽略空目录

zip 格式里,空目录也是一个独立条目,名字以 / 结尾,数据区长度为 0。压缩端如果遇到空目录,通常也会写入这样的条目。解压端必须识别它并调用 mkdir;同时,非空文件条目的父目录可能尚未创建,也要先创建父目录再写文件。另一个安全问题是路径穿越:恶意构造的 zip 里可以有 ../ 前缀,直接拼接路径会把文件写到输出目录之外,这是必须堵住的洞。

#include <filesystem> #include <string> namespace fs = std::filesystem; // 安全拼接:保证 arcName 生成的路径仍位于 root 内部 bool safeJoin(const std::string& root, const std::string& arcName, std::string& outPath) { fs::path rootP = fs::absolute(root); fs::path child = (rootP / arcName).lexically_normal(); std::string rootStr = rootP.string(); std::string childStr = child.string(); // childStr 必须以 rootStr 开头,且后续字符是分隔符或结尾 if (childStr.rfind(rootStr, 0) != 0) return false; if (childStr.size() > rootStr.size() && childStr[rootStr.size()] != '/' && childStr[rootStr.size()] != '\\') { return false; } outPath = childStr; return true; } // 为输出路径创建父目录 void ensureParent(const std::string& filePath) { fs::path p(filePath); if (p.has_parent_path() && !fs::exists(p.parent_path())) { fs::create_directories(p.parent_path()); } }

safeJoin 的思路是把 root 变成绝对路径,再把 arcName 拼上去并做 lexically_normal 规范化,最后用前缀比较确认结果仍在 root 内部。这个方法能挡住 ../ 逃逸,但要注意 root 本身最好也是绝对路径,否则 fs::absolute 的行为依赖当前工作目录,容易判断失准。ensureParent 则负责在写文件前补齐父目录。参数说明:root 传解压输出根目录,arcName 传 zip 条目名,outPath 是返回的安全路径。如果 safeJoin 返回 false,直接跳过该条目,不要继续解压。

4.3 口令错误的判别:unzOpenCurrentFilePassword 返回什么

口令错误是加密解压里最常见的失败路径,但它的报错时机很有迷惑性。有些条目在 unzOpenCurrentFilePassword 这一步就返回 UNZ_BADPASSWORD,因为 WinZip AES 格式里包含一个口令校验值,可以在读取正文前做预检;但传统 ZipCrypto 和一些实现并不会预检,它们会等 unzReadCurrentFile 读到尾部做 CRC 校验时,才抛出一个看起来像数据损坏的错误。这个差异让很多人误判成“压缩包坏了”。

所以判别逻辑不能只看一次返回值。我一般这样处理:unzOpenCurrentFilePassword 返回非 UNZ_OK 时,先记录错误码,如果是 UNZ_BADPASSWORD 就直接提示口令错误;如果打开成功但 unzCloseCurrentFile 返回 CRC 错误,再结合文件大小和修改时间判断,优先怀疑口令,其次才是磁盘损坏。

int ret = unzOpenCurrentFilePassword(uf, password.c_str()); if (ret == UNZ_BADPASSWORD) { // 口令在预检阶段就被拒绝,不需要继续读数据 unzCloseCurrentFile(uf); return -1; } if (ret != UNZ_OK) { // 其他错误按流的正确性处理,日志记录错误码 return ret; }

这段代码的目的是把口令错误和普通 IO 错误区分开。参数说明:ret 是 unzOpenCurrentFilePassword 的返回值,UNZ_BADPASSWORD 是增强版 unzip.h 里定义的错误码宏,具体数值不同版本可能不同,但用宏名判断就具备可移植性。推荐在解压循环里对每个条目记录错误码和文件名,这样即使出现“中途 CRC 失败”,也能快速定位是哪一个文件,避免大海捞针。

5. 加密压缩的常见坑与排查:翻车最多的地方都在这里

5.1 压缩包能打开但看不出加密

现象:用压缩软件打开刚生成的 zip,文件名能看到,但条目旁边没有锁图标,或者属性里找不到加密标记。

原因:最常见的是写入时只传了 password,没有把 aes_version 参数传进去。基础版 minizip 或者某些封装层会拿 password 的非空与否来判断加密,结果走的是传统 ZipCrypto,而一部分压缩软件对 ZipCrypto 的展示并不明显;另一种情况是密码参数传了,但 zipOpenNewFileInZip4_64 没被调用,走了老接口。

解决:回到 2.3 的接口探测,确认头文件里有 AES_METHOD;检查压缩代码是否真的调用了 zipOpenNewFileInZip4_64,最后的 aes_version 参数必须指向非空数组,aes_key_size 传 32。压缩完后用 3.3 的 listEntries 打印加密标识,这一步能立刻验证。

5.2 中文文件名解压后乱码

现象:压缩时文件名正常,换一台机器或换一个压缩工具解压,中文变成一整串乱码,有时还会出现“文件名不存在”的报错。

原因:zip 规范没有强制规定文件名编码。Windows 下多数国产压缩工具默认按本地代码页写 GBK,而很多基于 minizip 的实现在写入时直接拷贝字符串,如果你在 Linux 上按 UTF-8 存路径,Windows 上就会按 GBK 解读,两边对不上。这是中文环境下最容易翻车的点,比加密参数问题更难排查。

解决:在项目里统一约定——zip 内部文件名一律 UTF-8。压缩前把宽字符路径转成 UTF-8 再写入,解压后把 UTF-8 转回当前系统的本地编码用于 fopen。Windows 上用 WideCharToMultiByte 和 MultiByteToWideChar 完成转换;Linux 下直接用 UTF-8 原串即可。如果压缩端和解压端都是同一个程序,这个约定只要保持一致就不会乱码;如果要交付给第三方工具,最好额外测试两个不同系统的解压表现。

5.3 AES 加密包在某些解压软件里提示口令错误

现象:同一个压缩包,在 A 软件里能正常解压,换到 B 软件里输同一口令却提示“密码错误”或“无法解密”。

原因:AES 加密条目里 WinZip AES 的额外字段(extra field)记录着算法版本和校验值长度,如果写入端填错了校验值长度,或者把 AES-128 写成了 AES-256,某些解压端会据此用错误的密钥长度派生口令。另外,系统自带的 zip 解压功能只支持传统 ZipCrypto,不支持 WinZip AES,用它打开 AES 加密包会直接判定无效。

解决:固定用 AES-256,不要用 AES-128 和 AES-192;aes_crc_size 参数按增强版源码的推荐值 10 填,不要随意改成其他数字。交付前至少用两种主流压缩工具各测一次,特别是别拿系统自带解压功能当兼容性标准,它不支持 AES 是正常现象,不是你的 bug。

5.4 大文件压缩到一半中断,或解压时提示数据损坏

现象:小文件全部正常,某个超过 4GB 的文件压缩到一半就失败,或者解压到后半段报出 CRC 错误。

原因:zip 经典格式里单文件大小和整个包大小都有 4GB 上限,超过就必须启用 ZIP64。很多增强版实现已经默认支持 ZIP64,但如果你调用的接口没有把 zip64 参数设为 1,或者底层 zlib 的 windowBits 配置不对,大文件就会在写满 4GB 边界时出错。另外一种情况是压缩过程中文件还在被其他进程写,边压边变,导致写入的尺寸和 CRC 对不上,解压时自然校验失败。

解决:调用 zipOpenNewFileInZip4_64 时把 zip64 参数固定为 1;压缩前先记录文件大小和修改时间,压缩前再核对一次,发现文件在变就跳过或重试;解压端如果出现 CRC 错误,单独对该文件做一次重试,重试仍失败再报错。缓冲区用 64KB 左右即可,不要为了追求速度把缓冲区开到 10MB,反而增加内存压力。

5.5 解压出来文件个数对,但个别文件是 0 字节或内容残缺

现象:解压流程没有报错,文件数量也对,但某些文件大小是 0,或者打开后内容明显缺少一段。

原因:多半是压缩时写空目录条目和解压时目录重建的逻辑弄混了。空目录条目被当成普通文件打开,写入了一个 0 字节文件;或者解压循环里遇到目录条目时没有跳过,直接按文件处理。另一种可能是在读取循环里把 unzReadCurrentFile 返回 0 当成错误处理,导致文件尾部提前截断。

解决:解压循环开始时先判断条目名是否以 / 结尾,是目录就调 ensureParent 创建目录然后 unzGoToNextFile 继续;读取循环里把返回 0 当作正常结束,只有负数才是错误。压缩侧空目录也要用单独的目录条目写入,不要硬塞文件内容。

6. 更顺手的落地方式:流式压缩与回归自检

6.1 压缩 → 解压 → 比对 的自检脚本

加密压缩这种功能一旦上线,最怕的不是代码写不出来,而是改了某个参数后压缩和解压悄悄不一致。所以我会在工程里保留一个回归自检脚本,每次改完代码先跑一遍,确认从压缩到解压再到逐字节比对全链路是通的。这个脚本本质上就是对同一份数据做“往返测试”。

#!/usr/bin/env bash # 回归自检:加密压缩 → 解压 → 逐字节比对 set -euo pipefail WORK="selfcheck_data" ZIP="selfcheck.zip" PW="selfcheck_pw_2024" rm -rf "$WORK" "$ZIP" restored mkdir -p "$WORK/sub/empty_dir" echo "hello minizip" > "$WORK/hello.txt" head -c 10485760 /dev/urandom > "$WORK/rand.bin" : > "$WORK/sub/empty_file" ./ziptool -c -p "$PW" "$WORK" "$ZIP" ./ziptool -x -p "$PW" "$ZIP" restored diff -r "$WORK" restored echo "selfcheck pass"

这个脚本构造了一个包含普通文本、大随机文件、空文件和空目录的测试目录,然后执行压缩和解压,最后用 diff -r 对整个目录树做比对。diff -r 对二进制文件会逐字节比较,判等时没有输出,所以最后的 echo 只在全部一致时才会执行。参数说明:ziptool 是编译出来的命令行程序,-c 表示压缩,-x 表示解压,-p 后面跟口令。如果未来有人把加密参数改成 AES-128,只要压缩和解压仍然自洽,脚本可能依旧通过,所以这一步只能防回归,不能验证算法强度。

6.2 我保留的压缩习惯与参数基准

这套方案做下来,我给自己定了几条执行标准:文件夹加密压缩统一用 AES-256,不再开 ZipCrypto 的入口;压缩级别固定 6,兼顾速度和体积;文件名写入前强制转 UTF-8,解压后转本地编码;每次交付前跑一遍上面的自检脚本。遇到大规模文件夹,我会先把文件按扩展名分组,再决定哪些用 level 9 硬压缩,哪些直接 level 0 存储,而不是一刀切。多线程写同一个 zip 包属于自找麻烦,增强版 minizip 的内部状态不是线程安全的,我宁愿单线程按顺序写,把“多线程”留给“多个目录分别压缩再合并”这种场景。另外,zip 的口令不适合硬编码在二进制里,程序里做一个“读取外部配置或环境变量”的入口,避免每次发布都改密码。这些习惯可能不是最优解,但至少让加密压缩这个功能踩坑概率降到最低。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询