资讯动态

C++文件夹加密压缩:minizip+zlib实战指南

发布时间:2026/10/14 3:48:45 来源:尧图企业网站定制
简介面向需要在Windows平台上实现文件夹加密压缩与解压的C开发者一整套可直接对照的minizipzlib示例代码可帮助快速上手。它演示了多级文件夹整体加密压缩成zip以及后续解压还原的完整流程并兼容WinRAR、360zip等第三方工具解密解压。压缩包共19个文件以7个h、3个c、2个cpp和3个json为主另有lib静态库、VS工程文件等其中头文件和C源码对应minizip核心逻辑cpp与json分别承担调用示例和项目配置lib为编译好的静态依赖能减少手动配置成本。资源约167KB便于快速下载和迁移目前已有 806 人学习下载。通过该示例可以掌握ZipUtil的封装思路、多级目录递归压缩的实现方式以及加密zip可被常见压缩工具正常识别的技术要点对想在产品中集成加密压缩功能的人很有参考价值尤其是目录层级较深的项目也能直接复用。1. 文件夹加密压缩这个需求为什么绕不开 minizipzlibC 里做文件夹的加密压缩和解压缩最被低估的可靠组合是老牌的 minizip zlib。很多人一开始会直接调 zlib 的compress/uncompress接口或者自己拼文件头写加密逻辑结果一碰到多文件、目录结构、密码校验、跨平台路径就翻车。minizip 恰好补上了 zlib 不管的两块事ZIP 容器格式的封装和条目级加密。它不引入庞大的第三方运行时编译干净和 zlib 同一套数据流适合做桌面工具、备份程序、嵌入式 Linux 上的文件打包和传输。这篇文章要把这套组合从原理拆到可跑的代码再落到文件夹递归、中文路径、大文件、AES 加密这些真正的坑上。新手能照着跑通熟手能直接抄设计和参数。2. minizip 和 zlib 的分工压缩核心、格式封装与加密入口2.1 zlib 只负责 DEFLATE 流不负责文件组织zlib 是一套压缩库核心是 DEFLATE 算法对外提供的是流式接口deflate把输入缓冲压成压缩流inflate把压缩流还原成原始数据。它不关心你有几个文件、文件名是什么、目录结构长什么样更不关心加密。它的任务就是把一段字节序列变短或者还原。直接用 zlib 做文件夹压缩意味着你得自己设计容器格式哪个字节记录文件名、哪个字段存文件大小、怎么表示目录结束、怎么拼校验值。自己做出来的格式没人能解自己写解压代码要无限兼容自己写的压缩逻辑。这套东西一旦涉及二进制布局、扩展字段、跨平台换行符就是无底洞。zlib 的定位就是不做这些它很纯粹恰恰因为纯粹它不适合单独承担文件级任务。2.2 minizip 补上 ZIP 容器和加密这两块拼图minizip 是伴随 zlib 一起维护的一组封装层包含zip.c和unzip.c。它负责把多个文件组织成标准 ZIP 结构本地文件头、中央目录、条目注释、压缩方法和时间戳。你要做的只是按文件名一个个打开条目、写入数据、关闭条目它会把 ZIP 格式该有的字段都填好。加密是另一个关键点。ZIP 格式里有两代加密方案老式 ZipCrypto 和 AES。ZipCrypto 是一个流密码计算量小但历史上有已知明文攻击的漏洞密码学上早已不推荐。新版 minizip 在编译时定义HAVE_AES并链接配套的 AES 源码后可以写出使用 AES-256 加密的 ZIP 条目。这个加密是逐条目进行的同一个压缩包里某些文件加密、某些文件不加密完全可控。minizip 还帮你处理了中央目录和 local file header 的一致性包括 CRC32 的计算和回填。如果你自己写格式这些细节要手动管理出错概率极高。minizip 把 ZIP 的复杂性封装成十几个函数这是它最大的价值。2.3 选型边界什么时候该用这套组合什么时候换方案minizip zlib 不是唯一方案但它在很多场景里是最合适的。如果你的程序是 C/C 写的不需要跨语言调用不想引入动辄几 MB 的压缩库又必须输出别人用普通解压工具能打开的 ZIP 文件这套组合几乎是最短路径。如果需求是单文件压缩、不关心格式兼容性可以直接用 zlib 的gzFile接口。如果要压缩出来的归档必须被特定商业软件读取或者需要 7z 格式的 LZMA 压缩率那应该用 7z 的 SDK。如果要处理 ZIP 但希望带着完整文档和现代 C 接口libzip 也是一个选择代价是依赖更重、交叉编译更麻烦。我一般这样判断目标平台是 Linux 或裸机交叉编译输出必须是普通 ZIP加密是锦上添花的功能就用 minizip zlib。目标平台是桌面 Windows/macOS团队对代码可维护性要求高能接受 CMake 拉依赖可以考虑 libzip。至于 7z SDK除非你要做的是非标压缩率优先的工具否则没必要给自己找那个复杂度。对比维度minizip zliblibzip裸写 zlib 自造格式ZIP 兼容性标准 ZIP常见解压工具可开标准 ZIP只有自己能解加密条目级支持 ZipCrypto 和 AES支持 AES要自己实现依赖重量轻单 C 文件加大循环中等带加密后端最轻但功能缺失交叉编译成本低中等低维护成本低接口稳定中需要跟着版本走高格式维护很重3. 最小可编译骨架先跑通单文件加密压缩与解压3.1 获取和编译 minizip 的常见方式minizip 的源码通常位于 zlib 发行包的contrib/minizip/目录里也可以单独拉取。编译时不需要把整个 zlib 编译成动态库直接把zip.c、unzip.c、ioapi.c、你本地的 zlib 源文件以及加密所需的 AES 源文件一起编进工程即可。工程里必须定义HAVE_AES才会启用 AES 加密路径否则传入密码时会退化为 ZipCrypto。如果你用 CMake一个能用的片段长这样add_library(minizip STATIC zip.c unzip.c ioapi.c # 下面是 AES 支持需要的源文件 aes/aescrypt.c aes/aeskey.c aes/aes_impl.c aes/aes.h aes/fileenc.c aes/hmac.c aes/prng.c aes/pwd2key.c aes/sha1.c ) target_include_directories(minizip PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/aes ${ZLIB_INCLUDE_DIR} ) target_compile_definitions(minizip PUBLIC HAVE_AES) target_link_libraries(minizip PUBLIC zlib)这里的关键点是HAVE_AES和 AES 源文件必须同时到位只定义宏不编 AES 源码会链接失败。如果你不需要 AES只要传统 ZipCrypto可以去掉这些文件但我不建议在生产里开 ZipCrypto后文会讲原因。3.2 加密压缩的代码骨架单文件加密压缩是整套逻辑的最小单元。下面的代码把任意一个文件写入 zip 包条目名可以自己指定并设置密码#include minizip/zip.h #include fstream #include vector #include string bool ZipEncryptFile(const std::string zipPath, const std::string filePath, const std::string entryName, const std::string password) { zipFile zf zipOpen(zipPath.c_str(), APPEND_STATUS_CREATE); if (!zf) return false; std::ifstream in(filePath, std::ios::binary); if (!in.is_open()) { zipClose(zf, nullptr); return false; } zip_fileinfo zfi {0}; int err zipOpenNewFileInZip4_64( zf, entryName.c_str(), // ZIP 内的条目名统一用正斜杠 zfi, // 文件时间信息传 0 表示使用默认时间 nullptr, 0, // 本地额外字段 nullptr, 0, // 全局额外字段 nullptr, // 条目注释 Z_DEFLATED, // 压缩方法 Z_DEFAULT_COMPRESSION, // 压缩级别 0-9 0, // raw 模式1 表示不压缩 -MAX_WBITS, // windowBits-MAX_WBITS 表示 raw deflate 8, // memLevel1-9 Z_DEFAULT_STRATEGY, // 压缩策略 password.c_str(), // 密码传 nullptr 表示不加密 0, // crcForCrypting传 0 让库自己算 0, // versionMadeBy ZIP_UTF8_FLAG, // 文件名 UTF-8 标记 0 // dosDate传 0 表示按当前时间 ); if (err ! ZIP_OK) { in.close(); zipClose(zf, nullptr); return false; } std::vectorchar buf(1 16); while (in.read(buf.data(), buf.size()) || in.gcount() 0) { err zipWriteInFileInZip(zf, buf.data(), static_castunsigned(in.gcount())); if (err ! ZIP_OK) { in.close(); zipClose(zf, nullptr); return false; } } in.close(); zipCloseFileInZipRaw64(zf, 0, 0); zipClose(zf, nullptr); return true; }这段代码里最值得注意的参数是-MAX_WBITS。zlib 在 deflate 时如果直接用MAX_WBITS会产生 zlib 头而 ZIP 条目里存的必须是裸的 deflate 流所以传负值去掉 zlib 容器的头尾。这个细节很多人漏掉导致解压出来的数据开头多出 2 字节 zlib 头数据本身完整但文件头错位。zipCloseFileInZipRaw64的第二个和第三个参数传 0是在告诉 minizip 自己完成大小和 CRC 的回填。如果你提前知道大小和 CRC也可以传进去省一次统计但正常场景下传 0 最省事。3.3 解压回读的代码骨架解压单文件需要先定位条目再用密码打开#include minizip/unzip.h #include fstream #include vector #include string bool UnzipEncryptFile(const std::string zipPath, const std::string entryName, const std::string outputPath, const std::string password) { unzFile uf unzOpen(zipPath.c_str()); if (!uf) return false; if (unzLocateFile(uf, entryName.c_str(), 1) ! UNZ_OK) { unzClose(uf); return false; } unz_file_info64 info; char name[1024]; unzGetCurrentFileInfo64(uf, info, name, sizeof(name), nullptr, 0, nullptr, 0); int openErr password.empty() ? unzOpenCurrentFile(uf) : unzOpenCurrentFilePassword(uf, password.c_str()); if (openErr ! UNZ_OK) { unzClose(uf); return false; } std::ofstream out(outputPath, std::ios::binary); if (!out.is_open()) { unzCloseCurrentFile(uf); unzClose(uf); return false; } std::vectorchar buf(1 16); int readLen 0; while ((readLen unzReadCurrentFile(uf, buf.data(), buf.size())) 0) { out.write(buf.data(), readLen); } out.close(); unzCloseCurrentFile(uf); unzClose(uf); return readLen 0; }注意unzReadCurrentFile返回 0 是正常读到文件尾返回负数才是错误。很多人习惯性认为返回值小于 0 才失败实际上还要把 0 单独拎出来判断。unzOpenCurrentFilePassword在密码错误时会返回UNZ_BADPASSWORD这也是判断密码对错的最直接途径。3.4 代码运行失败时先看哪里第一次跑这套代码没有一次成功的这不是玄学是几个固定位置容易出错。编译期最常见的错误是zipOpenNewFileInZip4_64声明找不到这通常是因为 minizip 源码版本太老没有4_64系列接口。解决办法是换新一点的最小化源码或者在编译宏里启用ZIP64支持。运行期最常见的失败点是打开 zip 后写入失败却看不到任何日志。我的习惯是在每个返回值之后打印行号加错误码比如zipCloseFileInZipRaw64返回ZIP_ERRNO时查errno多半是磁盘满了或者文件句柄耗尽。另一个隐蔽问题是密码里的空字符如果密码字符串里包含\0minizip 只取到空字符之前的部分这时候用普通压缩工具能打开用你自己代码解压却说密码错误。4. 把文件夹整体干进去递归遍历、目录项与进度回传4.1 用 std::filesystem 递归收集条目路径统一为正斜杠压缩文件夹的第一件事是收集所有文件和目录。C17 的std::filesystem::recursive_directory_iterator是最直接的方案。收集阶段要做三件事算出相对于根目录的条目名、区分目录和普通文件、统计总字节数用于进度计算。条目名里必须使用正斜杠。Windows 上std::filesystem::path::generic_string()会把反斜杠转成正斜杠这正是 ZIP 标准要求的。目录条目以/结尾这是 ZIP 格式里区分目录和文件的关键标记少这个斜杠解压工具会把目录当成文件。另一种常见做法是手动FindFirstFile递归在 Windows 下效率略高但代码量翻倍。我倾向于先用std::filesystem把逻辑跑通等确认整个链路没有问题、确实有性能瓶颈时再替换为平台 API别过早优化。4.2 压缩文件夹的完整流程下面代码把整个目录递归压入加密 ZIP并支持进度回调和取消#include filesystem #include fstream #include functional #include vector #include minizip/zip.h namespace fs std::filesystem; bool ZipEncryptFolder(const fs::path rootDir, const fs::path zipPath, const std::string password, const std::functionbool(int64_t total, int64_t done) progress) { struct Entry { fs::path path; std::string entryName; int64_t size; }; std::vectorEntry files; int64_t totalBytes 0; for (const auto it : fs::recursive_directory_iterator(rootDir)) { fs::path rel fs::relative(it.path(), rootDir); std::string entryName rel.generic_string(); if (it.is_directory()) { entryName /; } if (it.is_regular_file()) { int64_t sz it.file_size(); totalBytes sz; files.push_back({it.path(), entryName, sz}); } } zipFile zf zipOpen(zipPath.c_str(), APPEND_STATUS_CREATE); if (!zf) return false; zip_fileinfo zfi {0}; int64_t doneBytes 0; for (const auto f : files) { int err zipOpenNewFileInZip4_64( zf, f.entryName.c_str(), zfi, nullptr, 0, nullptr, 0, nullptr, Z_DEFLATED, Z_DEFAULT_COMPRESSION, 0, -MAX_WBITS, 8, Z_DEFAULT_STRATEGY, password.c_str(), 0, 0, ZIP_UTF8_FLAG, 0); if (err ! ZIP_OK) { zipClose(zf, nullptr); return false; } std::ifstream in(f.path, std::ios::binary); std::vectorchar buf(1 16); while (in.read(buf.data(), buf.size()) || in.gcount() 0) { zipWriteInFileInZip(zf, buf.data(), static_castunsigned(in.gcount())); doneBytes in.gcount(); if (progress !progress(totalBytes, doneBytes)) { in.close(); zipClose(zf, nullptr); return false; } } zipCloseFileInZipRaw64(zf, 0, 0); } zipClose(zf, nullptr); return true; }进度回调里返回false表示请求取消。我把取消检查放在每次分块写入之后而不是文件之间这样大文件也能响应取消。目录项我只是为它写了条目名和斜杠但没有实际数据写入这是标准做法。注意fs::recursive_directory_iterator默认会跟着符号链接走可能造成循环或重复打包。如果不需要跟随符号链接可以在迭代器构造时传directory_options::skip_permission_denied并在循环里用it.is_symlink()跳过。4.3 解压到文件夹重建目录与路径穿越防护解压文件夹比压缩更麻烦因为要面对不可信的 ZIP 文件路径。下面代码按条目逐一遍历自动创建目录并检查路径穿越#include minizip/unzip.h bool UnzipEncryptFolder(const fs::path zipPath, const fs::path outRoot, const std::string password) { unzFile uf unzOpen(zipPath.c_str()); if (!uf) return false; if (unzGoToFirstFile(uf) ! UNZ_OK) { unzClose(uf); return false; } fs::path normalizedRoot fs::absolute(outRoot).lexically_normal(); do { unz_file_info64 info; char name[4096]; unzGetCurrentFileInfo64(uf, info, name, sizeof(name), nullptr, 0, nullptr, 0); std::string entryName name; // 把条目名中的反斜杠统一转成正斜杠 std::replace(entryName.begin(), entryName.end(), \\, /); if (entryName.empty()) continue; fs::path entryPath fs::path(entryName).lexically_normal(); fs::path outPath normalizedRoot / entryPath; // 路径穿越防护 fs::path normalizedOut outPath.lexically_normal(); if (normalizedOut.string().find(normalizedRoot.string()) ! 0) { unzClose(uf); return false; } if (entryName.back() /) { fs::create_directories(outPath); } else { fs::create_directories(outPath.parent_path()); std::ofstream out(outPath, std::ios::binary); if (!out.is_open()) { unzClose(uf); return false; } if (unzOpenCurrentFilePassword(uf, password.c_str()) ! UNZ_OK) { out.close(); unzClose(uf); return false; } std::vectorchar buf(1 16); int r; while ((r unzReadCurrentFile(uf, buf.data(), buf.size())) 0) { out.write(buf.data(), r); } out.close(); unzCloseCurrentFile(uf); } } while (unzGoToNextFile(uf) UNZ_OK); unzClose(uf); return true; }路径穿越防护的原理是把最终输出路径规范化之后检查它是否以输出根目录为前缀。..经过lexically_normal()会被解析掉如果entryName是../../etc/passwd规范化后的路径前缀必然不再是输出根目录此时直接中断处理。这个检查必须放在创建任何目录之前。某些恶意压缩包会先写一个..开头的目录条目再写一个常规文件名利用后续文件的真实路径覆盖系统文件。提前拦截所有越界路径是最稳妥的策略。4.4 进度回传和取消控制的设计压缩端需要统计总字节数解压端同样需要。解压进度的计算有一个难点一个条目解压完成前你无法知道当前条目真实大小但unz_file_info64里已经有uncompressed_size字段可以在unzOpenCurrentFilePassword之前取到。进度回调的签名建议设计成std::functionbool(int64_t current, int64_t total)返回false表示请求中止。在压缩端我已经实现了这个模式解压端同样在每个分块之后调用回调然后清空当前条目的写入状态并退出循环。这比压完一个文件再检查一次更能处理大文件场景。还有一个细节回调里应尽量少做重活。压缩循环跑在调用线程上如果回调里做磁盘扫描或网络请求会明显拖慢压缩速度。我一般只在回调里更新 UI 或写入日志不做同步 IO。5. 常见问题与避坑中文名、分隔符、大文件与 AES 加密的坑5.1 中文文件名解压后乱码现象用 minizip 压缩的中文文件名用系统自带的解压工具打开是乱码用你自己的代码解压出来也是乱码。原因ZIP 标准早期文件名默认编码是 CP437后来各家工具各搞一套。minizip 里如果你没有在flagBase传ZIP_UTF8_FLAG写入的条目名会被当作本地编码按字节写入解压端拿到字节后按自身 locale 解读中文环境就炸了。解决写入条目时flagBase必须设为ZIP_UTF8_FLAG并且条目名本身必须是 UTF-8 编码。我的代码里已经这么做了。如果是从 Windows API 拿到的宽字符文件名要先转成 UTF-8 再写入。解压端读条目名时如果看到ZIP_UTF8_FLAG标记它自己会按 UTF-8 处理。这个标记存在unz_file_info64的flag字段里读出来之后判断即可。5.2 路径分隔符导致文件夹层级丢失现象在 Windows 上压缩的包在 Linux 上解压后所有文件挤在同一个目录里文件夹层级全部丢失。原因写入条目名时用了std::string直接拼接path.string()Windows 的反斜杠\没有被转成正斜杠。部分解压工具把\当作普通字符不做目录切分于是一个sub\file.txt就变成了名为sub\file.txt的单个文件。解决所有条目名在写入前统一调用path.generic_string()并额外做一次replace(\\, /)。我给出的代码里已经覆盖这一点。解压端也要做同样的事因为从 zip 里读出来的名字无法保证一定是正斜杠。5.3 大文件内存暴涨与 32 位偏移溢出现象压一个 4GB 以上的文件程序内存飙升到几个 GB或者压缩过程中直接崩溃解压也遇到同样的问题。原因两个叠加的坑。第一个是有人把整个文件读入内存再交给 zip 接口文件多大内存多大。第二个更隐蔽老版本 minizip 的内部偏移量用的是 32 位有符号整数超过 2GB 就溢出本地文件头和中央目录对不上整个包损坏。解决压缩和解压都必须用分块读写我前面代码里的 64KB 缓冲就是干这个的。偏移量问题需要确保使用的是带64后缀的接口比如zipOpenNewFileInZip4_64、zipCloseFileInZipRaw64、unzGetCurrentFileInfo64。编译时还要确认没有定义降级到 32 位的宏。检查方法很简单把 zip 包里的单个文件压到超过 4GB再用你自己的代码完整解压一次比对逐字节哈希一致。5.4 AES 加密文件在第三方工具里打不开现象你用 minizip 开了 AES 加密生成的 zip 包在自己程序里能解压但发给别人用普通解压工具打开提示「不支持的压缩方法」或直接报错。原因ZIP 的 AES 加密不是标准 ZIP 格式的一部分。它是一个事实上的行业扩展把压缩方法字段标注为 99实际算法是 AES。老牌工具和部分系统内置解压器不认这个字段。解决分情况考虑。如果目标用户大概率用系统内置工具放弃 AES退回 ZipCrypto或者不加密。如果目标用户用的是现代第三方解压工具AES-256 是可以接受的但你要在文档里写明要求对方使用支持 AES 的工具。实际操作中我倾向于同时提供两个配置项AES-256和ZipCrypto让调用方按场景选择。密码学上 ZipCrypto 有弱点但它兼容性极好适合内网传输不太敏感的文件。所谓血泪经验就是别假设所有解压工具都懂 AES发布前拿两种工具实测一遍。6. 进阶把这套封装成可复用的文件夹加密压缩库6.1 接口设计回调、参数和返回值把前面散落的函数收拢成一个类能少踩很多重复的坑。接口层面我会留出四个可控点压缩级别、加密算法、进度回调、路径前缀处理。头文件大致长这样class ZipEncryptor { public: struct Options { int compressionLevel Z_DEFAULT_COMPRESSION; enum class EncryptMode { None, ZipCrypto, Aes256 } encryptMode EncryptMode::Aes256; std::string password; bool utf8EntryName true; }; using ProgressCallback std::functionbool(int64_t done, int64_t total); bool CreateArchive(const std::string zipPath, const std::string srcDir, const Options opt, ProgressCallback progress); bool ExtractArchive(const std::string zipPath, const std::string dstDir, const std::string password, ProgressCallback progress); };加密算法的选择不能在构造函数里写死要在Options里独立配置。这样同一套代码既能输出兼容性优先的 ZipCrypto 包也能输出安全优先的 AES 包。返回值只保留成功失败两个状态具体的错误码通过std::error_code或一个 out 参数暴露方便上层日志记录。6.2 功能验证与性能观测方法做完封装之后别急着收工。我每次改完这套代码都至少做三组验证第一组用常见桌面解压工具开包确认对方能正常列出目录树、解压中文名。第二组用密码错误的测试用例解压确认返回明确的错误码而不是崩溃或静默吐出垃圾数据。第三组压一个包含大量小文件加几个大文件的混合目录记录耗时、压缩率和内存峰值确认分块缓冲区在合理范围内。性能观测我一般用最简单的手段压缩前后做两次totalBytes统计压缩耗时用std::chrono::steady_clock内存峰值在 Linux 下看/usr/bin/time -v。如果发现压缩率异常低先检查是不是源文件本身已经是压缩格式如果发现内存高先怀疑是否有人把进度回调里的数据拷贝到容器里。这套代码我已经在几个项目里复用最大的教训是不要为了让接口看起来简洁而把加密模式隐藏成默认参数半年后你自己也记不清默认值是什么。把选项显式暴露给调用方比什么都重要。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑