diff --git a/3rdparty/cli7zplugin/cli7zplugin.cpp b/3rdparty/cli7zplugin/cli7zplugin.cpp index 82cff7b82..dcf99eafc 100644 --- a/3rdparty/cli7zplugin/cli7zplugin.cpp +++ b/3rdparty/cli7zplugin/cli7zplugin.cpp @@ -193,7 +193,7 @@ bool Cli7zPlugin::readListLine(const QString &line) return false; } - const QRegularExpression rxVersionLine(QStringLiteral("^p7zip Version ([\\d\\.]+) .*$")); + const QRegularExpression rxVersionLine(QStringLiteral("^(?:p7zip Version|7-Zip) ([\\d\\.]+).*$")); QRegularExpressionMatch matchVersion; switch (m_parseState) { diff --git a/3rdparty/clipzipplugin/CMakeLists.txt b/3rdparty/clipzipplugin/CMakeLists.txt index 666f79c0e..504779adb 100644 --- a/3rdparty/clipzipplugin/CMakeLists.txt +++ b/3rdparty/clipzipplugin/CMakeLists.txt @@ -4,7 +4,7 @@ set(LIB_NAME clipzipplugin) project(${LIB_NAME}) find_package(PkgConfig REQUIRED) -find_package(Qt${QT_DESIRED_VERSION} REQUIRED COMPONENTS Widgets) +find_package(Qt${QT_DESIRED_VERSION} REQUIRED COMPONENTS Widgets Core5Compat) find_package(KF${KF_VERSION_MAJOR}Codecs REQUIRED) include(FindPkgConfig) @@ -22,6 +22,7 @@ add_library(${LIB_NAME} SHARED ${c_files} ${json_files} ${h_files}) target_link_libraries(${LIB_NAME} Qt${QT_DESIRED_VERSION}::Widgets + Qt${QT_DESIRED_VERSION}::Core5Compat KF${KF_VERSION_MAJOR}::Codecs compressor-interface ) diff --git a/3rdparty/clipzipplugin/clipzipplugin.cpp b/3rdparty/clipzipplugin/clipzipplugin.cpp index ab3627127..44f13a778 100644 --- a/3rdparty/clipzipplugin/clipzipplugin.cpp +++ b/3rdparty/clipzipplugin/clipzipplugin.cpp @@ -5,16 +5,71 @@ #include "clipzipplugin.h" #include "datamanager.h" +#include "common.h" #include #include #include #include #include +#include +#include #include #include +#include +#include + + +namespace { + +constexpr auto kUiEncAes128 = "AES128"; +constexpr auto kUiEncAes192 = "AES192"; +constexpr auto kUiEncAes256 = "AES256"; + +constexpr auto kCliEncAes128 = "aes128"; +constexpr auto kCliEncAes192 = "aes192"; +constexpr auto kCliEncAes256 = "aes256"; + +// 与 LibzipPlugin::passwordUnicode(str, 0) 对 .zip 的逻辑一致,保证与 libzip 产包互解 +static QByteArray passwordBytesLikeLibzipForZip(const QString &strPassword, const QString &archiveName) +{ + if (!archiveName.endsWith(QLatin1String(".zip"), Qt::CaseInsensitive)) { + return strPassword.toUtf8(); + } + const int nCount = strPassword.count(); + bool hasHan = false; + for (int i = 0; i < nCount; ++i) { + const ushort uni = strPassword.at(i).unicode(); + if (uni >= 0x4E00 && uni <= 0x9FA5) { + hasHan = true; + break; + } + } + if (hasHan) { + QTextCodec *utf8 = QTextCodec::codecForName("UTF-8"); + QTextCodec *dst = QTextCodec::codecForName("UTF-8"); + if (!utf8 || !dst) { + return strPassword.toUtf8(); + } + const QString strUnicode = utf8->toUnicode(strPassword.toUtf8().constData()); + return dst->fromUnicode(strUnicode); + } + return strPassword.toUtf8(); +} + +static QString encryptionCliArgFromUi(const QString &ui) +{ + if (ui == QLatin1String(kUiEncAes128)) return QString::fromLatin1(kCliEncAes128); + if (ui == QLatin1String(kUiEncAes192)) return QString::fromLatin1(kCliEncAes192); + if (ui == QLatin1String(kUiEncAes256)) return QString::fromLatin1(kCliEncAes256); + return QString::fromLatin1(kCliEncAes256); +} + +} // namespace + +// pzip // pzip 安装路径 static const QString PZIP_INSTALL_PATH = QStringLiteral("/usr/lib/deepin-compressor/pzip"); static const QString PUNZIP_INSTALL_PATH = QStringLiteral("/usr/lib/deepin-compressor/punzip"); @@ -43,13 +98,31 @@ CliPzipPlugin::CliPzipPlugin(QObject *parent, const QVariantList &args) m_ePlugintype = PT_Libzip; // 复用 Libzip 类型,因为都是 ZIP 格式 m_timer = new QTimer(this); connect(m_timer, &QTimer::timeout, this, [=]() { - QFileInfo info(m_strArchiveName); - if (m_qTotalSize > 0) { - emit signalprogress(static_cast(info.size()) / m_qTotalSize * 100); - } + emitProgressIfArchiveGrew(); }); } +void CliPzipPlugin::emitProgressIfArchiveGrew() +{ + if (m_qTotalSize <= 0) { + return; + } + + QFileInfo info(m_progressArchiveName.isEmpty() ? m_strArchiveName : m_progressArchiveName); + const qint64 curSize = info.size(); + if (curSize < 0) { + return; + } + + // 避免重复发送相同进度:会导致 UI 速度=0,从而“剩余时间”消失 + if (curSize == m_lastProgressArchiveSize) { + return; + } + m_lastProgressArchiveSize = curSize; + + emit signalprogress(static_cast(curSize) / m_qTotalSize * 100); +} + CliPzipPlugin::~CliPzipPlugin() { deleteProcess(); @@ -172,6 +245,11 @@ PluginFinishType CliPzipPlugin::addFiles(const QList &files, const Co m_qTotalSize = options.qTotalSize; m_stdOutData.clear(); m_isProcessKilled = false; + m_tempArchiveDir.reset(); + m_tempArchiveName.clear(); + m_progressArchiveName.clear(); + m_lastProgressArchiveSize = -1; + m_lastUiBytesProgress = -1.0; QString pzipPath = getPzipPath(); if (pzipPath.isEmpty()) { @@ -186,10 +264,74 @@ PluginFinishType CliPzipPlugin::addFiles(const QList &files, const Co m_process->setNextOpenMode(QIODevice::ReadWrite | QIODevice::Unbuffered | QIODevice::Text); QStringList arguments; - + + m_passwordFile.reset(); + // 静默模式 arguments << "-q"; + arguments << "--ui-events"; + + // MTP 挂载目录不一定支持 seek/rename 等操作,先在临时目录生成,再 mv 回目标路径 + QString outArchiveName = m_strArchiveName; + if (IsMtpFileOrDirectory(m_strArchiveName)) { + m_tempArchiveDir = std::make_unique(); + m_tempArchiveDir->setAutoRemove(true); + m_tempArchiveName = m_tempArchiveDir->path() + QDir::separator() + QFileInfo(m_strArchiveName).fileName(); + outArchiveName = m_tempArchiveName; + qInfo() << "[PZIP_ROUTE]" << "clipzip: mtp target detected, staging archive at:" << outArchiveName + << "final:" << m_strArchiveName; + } + m_progressArchiveName = outArchiveName; + + // pzip 静默模式不会输出“当前文件名”,为了避免进度页一直显示“计算中”,先上报一个可展示的文件名 + if (!files.isEmpty()) { + const FileEntry &f0 = files.first(); + const QString display = !f0.strAlias.isEmpty() ? f0.strAlias + : (!f0.strFileName.isEmpty() ? f0.strFileName : f0.strFullPath); + emit signalCurFileName(display); + } + + // 兜底:部分场景上层未计算总大小(qTotalSize=0),会导致进度条永远不刷新 + if (m_qTotalSize <= 0) { + qint64 sum = 0; + for (const FileEntry &f : files) { + QFileInfo fi(f.strFullPath); + if (!fi.exists()) continue; + if (fi.isFile()) { + sum += fi.size(); + continue; + } + if (fi.isDir()) { + QDirIterator it(fi.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories); + while (it.hasNext()) { + it.next(); + sum += it.fileInfo().size(); + } + } + } + m_qTotalSize = sum; + qInfo() << "[PZIP_ROUTE]" << "clipzip: fallback total size computed:" << m_qTotalSize; + } + + if (options.bEncryption && !options.strPassword.isEmpty()) { + m_passwordFile = std::make_unique(); + if (!m_passwordFile->open()) { + qWarning() << "Failed to create temporary file for password"; + m_eErrorType = ET_PluginError; + return PFT_Error; + } + const QByteArray pwBytes = passwordBytesLikeLibzipForZip(options.strPassword, m_strArchiveName); + if (m_passwordFile->write(pwBytes) != pwBytes.size()) { + qWarning() << "Failed to write password bytes"; + m_eErrorType = ET_PluginError; + return PFT_Error; + } + m_passwordFile->flush(); + arguments << "--password-file" << m_passwordFile->fileName(); + arguments << "-e" << encryptionCliArgFromUi(options.strEncryptionMethod); + } + // 压缩等级:0=Store(不压缩),1-9=deflate 压缩级别 int level = options.iCompressionLevel; if (level < 0 || level > 9) { @@ -203,7 +345,7 @@ PluginFinishType CliPzipPlugin::addFiles(const QList &files, const Co } // 输出文件 - arguments << m_strArchiveName; + arguments << outArchiveName; // 添加所有源文件/目录 for (const FileEntry &file : files) { @@ -242,6 +384,11 @@ PluginFinishType CliPzipPlugin::addFiles(const QList &files, const Co m_processId = m_process->processId(); getChildProcessId(m_processId, QStringList() << "pzip", m_childProcessId); m_timer->start(500); // 每500ms更新一次进度 + + // 让 UI 立即脱离“计算中”(速度/剩余时间会在首次 setProgress 后更新) + if (m_qTotalSize > 0) { + emit signalprogress(1); + } } return PFT_Nomral; @@ -327,6 +474,36 @@ bool CliPzipPlugin::doKill() bool CliPzipPlugin::handleLine(const QString &line) { + static const QLatin1String kUiPrefix("[PZIP_UI] entry "); + if (line.startsWith(kUiPrefix)) { + const QString nameInArchive = line.mid(kUiPrefix.size()); + if (!nameInArchive.isEmpty()) { + emit signalCurFileName(nameInArchive); + } + emitProgressIfArchiveGrew(); + return true; + } + + static const QLatin1String kUiBytesPrefix("[PZIP_UI] inbytes "); + if (line.startsWith(kUiBytesPrefix)) { + if (m_qTotalSize > 0) { + bool ok = false; + const qint64 inBytes = line.mid(kUiBytesPrefix.size()).trimmed().toLongLong(&ok); + if (ok && inBytes >= 0) { + double p = static_cast(inBytes) / static_cast(m_qTotalSize) * 100.0; + if (p > 99.9) p = 99.9; // 结束由 processFinished 统一补 100 + if (p < 0.0) p = 0.0; + + // 只要有实际变化就推一把,让 UI 能稳定算速度/剩余时间 + if (m_lastUiBytesProgress < 0.0 || std::abs(p - m_lastUiBytesProgress) >= 0.05) { + m_lastUiBytesProgress = p; + emit signalprogress(p); + } + } + } + return true; + } + if (line.contains(QLatin1String("No space left on device"))) { m_eErrorType = ET_InsufficientDiskSpace; return false; @@ -337,13 +514,7 @@ bool CliPzipPlugin::handleLine(const QString &line) // 不一定是致命错误,继续处理 } - // 更新进度 - if (m_qTotalSize > 0) { - QFileInfo info(m_strArchiveName); - emit signalprogress(static_cast(info.size()) / m_qTotalSize * 100); - } - - emit signalCurFileName(line); + emitProgressIfArchiveGrew(); return true; } @@ -374,6 +545,7 @@ void CliPzipPlugin::killProcess(bool emitFinished) void CliPzipPlugin::deleteProcess() { + m_passwordFile.reset(); if (m_process) { readStdout(true); m_process->blockSignals(true); @@ -467,7 +639,22 @@ void CliPzipPlugin::processFinished(int exitCode) PluginFinishType eFinishType; if (0 == exitCode && exitStatus == QProcess::NormalExit) { - eFinishType = PFT_Nomral; + bool ok = true; + + // 若走了 MTP staging,成功后将临时文件移动到最终目标路径 + if (!m_tempArchiveName.isEmpty() && m_tempArchiveDir) { + QProcess mover; + const QStringList args = {m_tempArchiveName, m_strArchiveName}; + int ret = mover.execute(QStringLiteral("mv"), args); + ok = (ret == 0) && (mover.exitStatus() == QProcess::NormalExit) && (mover.exitCode() == 0); + qInfo() << "[PZIP_ROUTE]" << "clipzip: mv staged archive to final, ok=" << ok + << "from" << m_tempArchiveName << "to" << m_strArchiveName; + if (!ok) { + m_eErrorType = ET_FileWriteError; + } + } + + eFinishType = ok ? PFT_Nomral : PFT_Error; } else { eFinishType = PFT_Error; } diff --git a/3rdparty/clipzipplugin/clipzipplugin.h b/3rdparty/clipzipplugin/clipzipplugin.h index 8b3189b8f..f9ef48c47 100644 --- a/3rdparty/clipzipplugin/clipzipplugin.h +++ b/3rdparty/clipzipplugin/clipzipplugin.h @@ -12,6 +12,9 @@ #include #include +#include +#include +#include #include // for QT_VERSION_CHECK class CliPzipPluginFactory : public KPluginFactory @@ -88,6 +91,8 @@ class CliPzipPlugin : public ReadWriteArchiveInterface */ void getChildProcessId(qint64 processId, const QStringList &listKey, QVector &childprocessid); + void emitProgressIfArchiveGrew(); + private slots: void readStdout(bool handleAll = false); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) @@ -104,6 +109,13 @@ private slots: QVector m_childProcessId; qint64 m_qTotalSize = 0; QTimer *m_timer = nullptr; + qint64 m_lastProgressArchiveSize = -1; + double m_lastUiBytesProgress = -1.0; + + std::unique_ptr m_passwordFile; + std::unique_ptr m_tempArchiveDir; + QString m_tempArchiveName; + QString m_progressArchiveName; // 解压相关 QString m_extractDestPath; diff --git a/src/pzip/CMakeLists.txt b/src/pzip/CMakeLists.txt index 95787bb36..e5455633d 100644 --- a/src/pzip/CMakeLists.txt +++ b/src/pzip/CMakeLists.txt @@ -14,6 +14,8 @@ find_package(PkgConfig REQUIRED) find_package(ZLIB REQUIRED) find_package(Threads REQUIRED) +find_package(OpenSSL REQUIRED) + include_directories(${PROJECT_SOURCE_DIR}/include) include_directories(${ZLIB_INCLUDE_DIRS}) @@ -26,11 +28,17 @@ set(PZIP_SOURCES src/fast_deflate.cpp src/utils.cpp src/worker_pool.cpp + src/crypto/aes_encryptor.cpp + src/crypto/winzip_aes_extra.cpp ) add_library(pzip_core_lib STATIC ${PZIP_SOURCES}) target_include_directories(pzip_core_lib PUBLIC ${PROJECT_SOURCE_DIR}/include) -target_link_libraries(pzip_core_lib ${ZLIB_LIBRARIES} Threads::Threads) +target_link_libraries(pzip_core_lib + ${ZLIB_LIBRARIES} + Threads::Threads + OpenSSL::Crypto +) target_compile_features(pzip_core_lib PUBLIC cxx_std_17) diff --git a/src/pzip/cmd/pzip_main.cpp b/src/pzip/cmd/pzip_main.cpp index 666fb4676..d2dcc8d57 100644 --- a/src/pzip/cmd/pzip_main.cpp +++ b/src/pzip/cmd/pzip_main.cpp @@ -12,17 +12,29 @@ #include "pzip/pzip.h" #include +#include #include #include #include #include +#include +#include +#include +#include +#include +#include +#include void printUsage(const char* progName) { std::cout << "pzip - Parallel ZIP Archiver v" << pzip::version() << "\n\n" << "用法: " << progName << " [选项] <文件或目录...>\n\n" << "选项:\n" << " -c, --concurrency 设置并发线程数(默认: 全部 CPU 核心)\n" - << " -l, --level <1-9> 设置压缩级别(默认: 1,最快)\n" + << " -l, --level <0-9> 设置压缩级别(默认: 1,最快;0=不压缩)\n" + << " -p, --password <密码> 设置加密密码(UTF-8/终端字节)\n" + << " -P, --password-file <路径> 从文件读取密码字节(与 libzip 插件编码对齐时由 GUI 写入)\n" + << " -e, --encryption <方法> 加密方法: aes128, aes192, aes256(默认: aes256)\n" + << " --ui-events 输出给 GUI 解析的事件行(例如当前文件名),格式稳定,适合配合 -q 使用\n" << " -v, --verbose 显示详细信息\n" << " -q, --quiet 静默模式\n" << " -h, --help 显示帮助信息\n" @@ -30,7 +42,9 @@ void printUsage(const char* progName) { << "示例:\n" << " " << progName << " archive.zip file1.txt file2.txt\n" << " " << progName << " archive.zip directory/\n" - << " " << progName << " -c 4 -l 9 archive.zip files/\n"; + << " " << progName << " -c 4 -l 9 archive.zip files/\n" + << " " << progName << " -p mypassword archive.zip files/\n" + << " " << progName << " -p mypassword -e aes256 archive.zip files/\n"; } int main(int argc, char* argv[]) { @@ -38,20 +52,26 @@ int main(int argc, char* argv[]) { pzip::ArchiverOptions options; bool verbose = false; bool quiet = false; - + bool uiEvents = false; + std::string encryptionMethodStr; + // 命令行选项定义 static struct option longOptions[] = { {"concurrency", required_argument, nullptr, 'c'}, {"level", required_argument, nullptr, 'l'}, + {"password", required_argument, nullptr, 'p'}, + {"password-file", required_argument, nullptr, 'P'}, + {"encryption", required_argument, nullptr, 'e'}, + {"ui-events", no_argument, nullptr, 1 }, {"verbose", no_argument, nullptr, 'v'}, {"quiet", no_argument, nullptr, 'q'}, {"help", no_argument, nullptr, 'h'}, {nullptr, 0, nullptr, 0} }; - + // 解析命令行选项 int opt; - while ((opt = getopt_long(argc, argv, "c:l:vqh", longOptions, nullptr)) != -1) { + while ((opt = getopt_long(argc, argv, "c:l:p:P:e:vqh", longOptions, nullptr)) != -1) { switch (opt) { case 'c': options.concurrency = std::stoul(optarg); @@ -63,6 +83,24 @@ int main(int argc, char* argv[]) { return 1; } break; + case 'p': + options.password = optarg; + break; + case 'P': { + std::ifstream pf(optarg, std::ios::binary); + if (!pf) { + std::cerr << "错误: 无法读取密码文件: " << optarg << "\n"; + return 1; + } + options.password.assign(std::istreambuf_iterator(pf), std::istreambuf_iterator()); + break; + } + case 'e': + encryptionMethodStr = optarg; + break; + case 1: + uiEvents = true; + break; case 'v': verbose = true; break; @@ -77,6 +115,100 @@ int main(int argc, char* argv[]) { return 1; } } + + // 解析加密方法 + if (!options.password.empty()) { + if (encryptionMethodStr.empty() || encryptionMethodStr == "aes256") { + options.encryptionMethod = pzip::EncryptionMethod::AES256; + } else if (encryptionMethodStr == "aes192") { + options.encryptionMethod = pzip::EncryptionMethod::AES192; + } else if (encryptionMethodStr == "aes128") { + options.encryptionMethod = pzip::EncryptionMethod::AES128; + } else { + std::cerr << "错误: 不支持的加密方法: " << encryptionMethodStr << "\n"; + std::cerr << "支持的加密方法: aes128, aes192, aes256\n"; + return 1; + } + } + + // --ui-events 可能被 GUI 打开用于“当前文件名 + 进度/速率/剩余时间”显示; + // 如果直接在压缩线程里打印(尤其是大量小文件),会被 stdout/pty 吞吐拖慢。 + // 因此这里做“异步合并输出”: + // - 压缩线程只上报 name / bytes 到内存(极轻量) + // - 单独线程限频输出:[PZIP_UI] entry / [PZIP_UI] inbytes + struct UiEventEmitter { + std::mutex mu; + std::condition_variable cv; + std::optional pendingEntry; + bool stop = false; + std::atomic inBytes{0}; + + void pushEntry(std::string s) { + { + std::lock_guard lk(mu); + pendingEntry = std::move(s); // 合并:只保留最新 + } + cv.notify_one(); + } + + void addBytes(uint64_t delta) { + inBytes.fetch_add(delta, std::memory_order_relaxed); + cv.notify_one(); + } + + void requestStop() { + { + std::lock_guard lk(mu); + stop = true; + } + cv.notify_one(); + } + }; + std::unique_ptr ui; + std::thread uiThread; + if (uiEvents) { + ui = std::make_unique(); + + // 输出线程:打印最新 entry;同时周期性输出累计输入字节(用于 GUI 计算速度/剩余时间) + uiThread = std::thread([em = ui.get()] { + std::string lastPrinted; + uint64_t lastInBytes = 0; + while (true) { + std::optional curEntry; + { + std::unique_lock lk(em->mu); + em->cv.wait_for(lk, std::chrono::milliseconds(100), [&] { + return em->stop || em->pendingEntry.has_value(); + }); + if (em->stop && !em->pendingEntry.has_value()) { + break; + } + curEntry = std::move(em->pendingEntry); + em->pendingEntry.reset(); + } + + if (curEntry && *curEntry != lastPrinted) { + std::cout << "[PZIP_UI] entry " << *curEntry << "\n"; + lastPrinted = *curEntry; + } + + const uint64_t curInBytes = em->inBytes.load(std::memory_order_relaxed); + if (curInBytes != lastInBytes) { + std::cout << "[PZIP_UI] inbytes " << curInBytes << "\n"; + lastInBytes = curInBytes; + } + } + }); + + options.onEntryStart = [em = ui.get()](const std::string& nameInArchive) { + em->pushEntry(nameInArchive); + }; + options.onBytesRead = [em = ui.get()](uint64_t delta) { + if (delta > 0) { + em->addBytes(delta); + } + }; + } // 检查参数数量 if (argc - optind < 2) { @@ -119,11 +251,35 @@ int main(int argc, char* argv[]) { if (verbose) { std::cout << "并发线程数: " << (options.concurrency > 0 ? options.concurrency : std::thread::hardware_concurrency()) << "\n"; std::cout << "压缩级别: " << options.compressionLevel << "\n"; + if (options.isEncrypted()) { + std::cout << "加密: 是\n"; + std::cout << "加密方法: "; + switch (options.encryptionMethod) { + case pzip::EncryptionMethod::AES128: + std::cout << "AES-128\n"; + break; + case pzip::EncryptionMethod::AES192: + std::cout << "AES-192\n"; + break; + case pzip::EncryptionMethod::AES256: + std::cout << "AES-256\n"; + break; + default: + std::cout << "未知\n"; + } + } } } // 执行压缩 pzip::Error err = pzip::compress(archivePath, inputPaths, options); + + if (ui) { + ui->requestStop(); + if (uiThread.joinable()) { + uiThread.join(); + } + } // 结束计时 auto endTime = std::chrono::high_resolution_clock::now(); diff --git a/src/pzip/include/pzip/archiver.h b/src/pzip/include/pzip/archiver.h index 42e0014a3..3bc1e0e2f 100644 --- a/src/pzip/include/pzip/archiver.h +++ b/src/pzip/include/pzip/archiver.h @@ -9,6 +9,7 @@ #include "worker_pool.h" #include "file_task.h" #include "zip_writer.h" +#include namespace pzip { @@ -20,6 +21,18 @@ struct ArchiverOptions { int compressionLevel = 1; // 压缩级别(1 = 最快,默认值) bool preservePermissions = true; // 保留文件权限 ProgressCallback progress; // 进度回调 + std::function onEntryStart; // 当前处理的条目(用于 UI 事件) + std::function onBytesRead; // 输入字节增量(用于 UI 进度/速率) + + // 加密选项 + std::string password; // 加密密码 + EncryptionMethod encryptionMethod = EncryptionMethod::None; + bool useAE2 = true; // 使用 AE-2 格式 + + // 判断是否启用加密 + bool isEncrypted() const { + return !password.empty() && encryptionMethod != EncryptionMethod::None; + } }; /** @@ -81,16 +94,19 @@ class Archiver { private: // 压缩单个文件 Error compressFile(FileTask* task); - + + // 加密文件 + Error encryptFile(FileTask* task); + // 写入单个文件到 ZIP Error archiveFile(FileTask* task); - + // 遍历目录 Error walkDirectory(const fs::path& root); - + // 压缩文件内容 Error compress(FileTask* task); - + // 填充 ZIP 文件头 void populateHeader(FileTask* task); diff --git a/src/pzip/include/pzip/common.h b/src/pzip/include/pzip/common.h index 4957ad7d8..1ed7f8b6b 100644 --- a/src/pzip/include/pzip/common.h +++ b/src/pzip/include/pzip/common.h @@ -29,6 +29,19 @@ constexpr uint16_t ZIP_METHOD_DEFLATE = 8; // ZIP 标志位 constexpr uint16_t ZIP_FLAG_DATA_DESCRIPTOR = 0x0008; constexpr uint16_t ZIP_FLAG_UTF8 = 0x0800; +constexpr uint16_t ZIP_FLAG_ENCRYPTED = 0x0001; + +// ZIP 加密方法 +constexpr uint16_t ZIP_ENCRYPTION_NONE = 0; +constexpr uint16_t ZIP_ENCRYPTION_WINZIP_AES = 99; // WinZip AES 标识 + +// WinZip AES 参数 +constexpr uint16_t WINZIP_AES_EXTRA_ID = 0x9901; // WinZip AES Extra Field ID +constexpr uint16_t WINZIP_AES_VERSION_1 = 0x0001; // AE-1 +constexpr uint16_t WINZIP_AES_VERSION_2 = 0x0002; // AE-2 +constexpr size_t WINZIP_AES_AUTH_CODE_SIZE = 10; // HMAC-SHA1 认证码大小 +constexpr size_t WINZIP_AES_PV_SIZE = 2; // 密码验证值大小 +constexpr uint32_t PBKDF2_ITERATIONS = 1000; // PBKDF2 迭代次数 // 错误码 enum class ErrorCode { @@ -42,9 +55,59 @@ enum class ErrorCode { INVALID_ARCHIVE, MEMORY_ERROR, CANCELLED, - UNKNOWN_ERROR + UNKNOWN_ERROR, + + // 加密相关错误码 + ENCRYPTION_NOT_SUPPORTED, // 不支持的加密方法 + ENCRYPTION_KEY_DERIVATION_ERROR,// 密钥派生失败 + ENCRYPTION_ERROR, // 加密失败 + DECRYPTION_ERROR, // 解密失败 + WRONG_PASSWORD, // 密码错误 + MISSING_PASSWORD, // 缺少密码 + AUTHENTICATION_FAILED // 认证码校验失败 +}; + +// 加密方法枚举 +enum class EncryptionMethod { + None = 0, + AES128 = 1, + AES192 = 2, + AES256 = 3 }; +constexpr size_t AES128_KEY_SIZE = 16; +constexpr size_t AES192_KEY_SIZE = 24; +constexpr size_t AES256_KEY_SIZE = 32; + +constexpr size_t AES128_SALT_SIZE = 8; +constexpr size_t AES192_SALT_SIZE = 12; +constexpr size_t AES256_SALT_SIZE = 16; + +// 获取加密方法对应的密钥大小 +inline size_t encryptionKeySize(EncryptionMethod method) { + switch (method) { + case EncryptionMethod::AES128: return AES128_KEY_SIZE; + case EncryptionMethod::AES192: return AES192_KEY_SIZE; + case EncryptionMethod::AES256: return AES256_KEY_SIZE; + default: return 0; + } +} + +// 获取加密方法对应的盐值大小 +inline size_t encryptionSaltSize(EncryptionMethod method) { + switch (method) { + case EncryptionMethod::AES128: return AES128_SALT_SIZE; + case EncryptionMethod::AES192: return AES192_SALT_SIZE; + case EncryptionMethod::AES256: return AES256_SALT_SIZE; + default: return 0; + } +} + +// 获取加密方法对应的 AES 强度标识 +inline uint8_t encryptionStrength(EncryptionMethod method) { + return static_cast(method); +} + // 错误信息 struct Error { ErrorCode code; diff --git a/src/pzip/include/pzip/crypto/aes_encryptor.h b/src/pzip/include/pzip/crypto/aes_encryptor.h new file mode 100644 index 000000000..120902758 --- /dev/null +++ b/src/pzip/include/pzip/crypto/aes_encryptor.h @@ -0,0 +1,131 @@ +// Copyright (C) 2025 ~ 2026 Uniontech Software Technology Co.,Ltd. +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "pzip/common.h" +#include +#include + +typedef struct evp_cipher_ctx_st EVP_CIPHER_CTX; +typedef struct hmac_ctx_st HMAC_CTX; + +namespace pzip { + +// AES 块大小 +constexpr size_t AES_BLOCK_SIZE = 16; + +/** + * @brief WinZip AES 加密器 + * + * 实现 WinZip AES 加密规范: + * - PBKDF2-HMAC-SHA1 密钥派生 + * - AES-CTR 模式加密(手动实现,counter 小端序递增) + * - HMAC-SHA1 认证码 + * + * 线程安全:每个 FileTask 应创建独立的 AESEncryptor 实例 + */ +class AESEncryptor { +public: + AESEncryptor(const std::string& password, EncryptionMethod method); + ~AESEncryptor(); + + AESEncryptor(const AESEncryptor&) = delete; + AESEncryptor& operator=(const AESEncryptor&) = delete; + + AESEncryptor(AESEncryptor&& other) noexcept; + AESEncryptor& operator=(AESEncryptor&& other) noexcept; + + const std::vector& salt() const { return salt_; } + uint16_t passwordVerification() const { return passwordVerification_; } + + size_t encrypt(const uint8_t* input, size_t inputSize, uint8_t* output); + std::vector encrypt(const std::vector& input); + + std::array authenticationCode(); + + EncryptionMethod method() const { return method_; } + bool isValid() const { return valid_; } + const std::string& lastError() const { return lastError_; } + + size_t encryptedSize(size_t originalSize) const; + +private: + bool generateSalt(); + bool deriveKeys(const std::string& password); + bool initCipher(); + bool initHMAC(); + void incrementCounter(); + bool aesCrypt(uint8_t* data, size_t length); + + EncryptionMethod method_; + std::vector salt_; + std::vector aesKey_; + std::vector hmacKey_; + uint16_t passwordVerification_; + + EVP_CIPHER_CTX* aesCtx_; + HMAC_CTX* hmacCtx_; + + bool valid_; + std::string lastError_; + + std::vector counterBlock_; + std::vector pad_; + size_t padOffset_; + uint64_t counter_; +}; + +/** + * @brief WinZip AES 解密器 + */ +class AESDecryptor { +public: + AESDecryptor(const std::string& password, EncryptionMethod method, + const std::vector& salt, uint16_t passwordVerification); + ~AESDecryptor(); + + AESDecryptor(const AESDecryptor&) = delete; + AESDecryptor& operator=(const AESDecryptor&) = delete; + + AESDecryptor(AESDecryptor&& other) noexcept; + AESDecryptor& operator=(AESDecryptor&& other) noexcept; + + bool verifyPassword() const { return passwordVerified_; } + + size_t decrypt(const uint8_t* input, size_t inputSize, uint8_t* output); + std::vector decrypt(const std::vector& input); + + bool verifyAuthenticationCode(const std::array& authCode); + + bool isValid() const { return valid_; } + const std::string& lastError() const { return lastError_; } + +private: + bool deriveKeys(const std::string& password); + bool initCipher(); + void incrementCounter(); + bool aesCrypt(uint8_t* data, size_t length); + + EncryptionMethod method_; + std::vector salt_; + uint16_t expectedPasswordVerification_; + uint16_t actualPasswordVerification_; + std::vector aesKey_; + std::vector hmacKey_; + + EVP_CIPHER_CTX* aesCtx_; + HMAC_CTX* hmacCtx_; + + bool valid_; + bool passwordVerified_; + std::string lastError_; + + std::vector counterBlock_; + std::vector pad_; + size_t padOffset_; +}; + +} // namespace pzip diff --git a/src/pzip/include/pzip/crypto/winzip_aes_extra.h b/src/pzip/include/pzip/crypto/winzip_aes_extra.h new file mode 100644 index 000000000..13b94055e --- /dev/null +++ b/src/pzip/include/pzip/crypto/winzip_aes_extra.h @@ -0,0 +1,33 @@ +// Copyright (C) 2025 ~ 2026 Uniontech Software Technology Co.,Ltd. +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "pzip/common.h" + +#include +#include + +namespace pzip { + +// WinZip AES extra 的 Vendor ID:ASCII "AE"(按 ZIP 小端存储为 0x4541) +constexpr uint16_t WINZIP_AES_VENDOR_ID_AE = 0x4541; + +/** + * @brief WinZip AES Extra Field (0x9901) 编码/解码 + * + * 与 ZIP Local/CD Header 中的 extra 字段布局对应;供 ZipWriter 等使用。 + */ +struct WinZipAESExtra { + uint16_t vendorVersion = WINZIP_AES_VERSION_2; // AE-1 / AE-2 + uint16_t vendorId = WINZIP_AES_VENDOR_ID_AE; // "AE" + uint8_t aesStrength = 0; // 1=AES-128, 2=AES-192, 3=AES-256 + uint16_t actualCompressionMethod = ZIP_METHOD_STORE; + + std::vector encode() const; + static WinZipAESExtra decode(const uint8_t* data, size_t size); +}; + +} // namespace pzip diff --git a/src/pzip/include/pzip/file_task.h b/src/pzip/include/pzip/file_task.h index 10994aee2..7f60e9146 100644 --- a/src/pzip/include/pzip/file_task.h +++ b/src/pzip/include/pzip/file_task.h @@ -9,6 +9,7 @@ #include #include #include +#include #include namespace pzip { @@ -36,15 +37,25 @@ struct ZipFileHeader { uint64_t uncompressedSize = 0; uint32_t externalAttr = 0; // Unix 权限等 std::vector extra; // 扩展字段 - + + // 加密相关字段 + uint16_t encryptionMethod = 0; // 加密方法 (0=无, 99=WinZip AES) + uint8_t aesStrength = 0; // AES 强度 (1/2/3 对应 AES-128/192/256) + uint16_t actualCompressionMethod = ZIP_METHOD_DEFLATE; // 实际压缩方法(WinZip AES 时) + bool isDirectory() const { return !name.empty() && name.back() == '/'; } - + // 对应 Go 的 isZip64() 方法 bool isZip64() const { return compressedSize >= ZIP_UINT32_MAX || uncompressedSize >= ZIP_UINT32_MAX; } + + // 是否加密 + bool isEncrypted() const { + return encryptionMethod == ZIP_ENCRYPTION_WINZIP_AES; + } }; /** @@ -109,7 +120,13 @@ class FileTask { bool isSymlink = false; // 是否是符号链接 std::string symlinkTarget; // 符号链接目标路径 bool streamFromSource = false; // Store 模式:写入时直接从源文件读取,跳过缓冲 - + + // 加密相关 + std::vector encryptedData; // 加密后的数据(包含 salt + pv + encrypted + authCode) + std::vector salt; // 盐值 + uint16_t passwordVerification = 0; // 密码验证值 + std::array authCode{}; // 认证码 + // 压缩器(由 Archiver 管理) z_stream* compressor = nullptr; diff --git a/src/pzip/include/pzip/zip_writer.h b/src/pzip/include/pzip/zip_writer.h index 97f299275..b066dd38f 100644 --- a/src/pzip/include/pzip/zip_writer.h +++ b/src/pzip/include/pzip/zip_writer.h @@ -7,6 +7,7 @@ #include "common.h" #include "file_task.h" +#include "pzip/crypto/winzip_aes_extra.h" #include namespace pzip { @@ -51,9 +52,24 @@ class ZipWriter { * @param compressedSize 压缩数据大小 * @return 错误信息 */ - Error createRaw(const ZipFileHeader& header, + Error createRaw(const ZipFileHeader& header, std::function)> dataProvider); + /** + * @brief 写入加密数据(WinZip AES) + * @param header ZIP 文件头 + * @param salt 盐值 + * @param passwordVerification 密码验证值 + * @param encryptedData 加密后的数据 + * @param authCode 认证码 + * @return 错误信息 + */ + Error createEncrypted(const ZipFileHeader& header, + const std::vector& salt, + uint16_t passwordVerification, + const std::vector& encryptedData, + const std::array& authCode); + /** * @brief 写入文件(会自动压缩) * @param header ZIP 文件头 diff --git a/src/pzip/src/archiver.cpp b/src/pzip/src/archiver.cpp index 8afadaf13..f607cad28 100644 --- a/src/pzip/src/archiver.cpp +++ b/src/pzip/src/archiver.cpp @@ -6,6 +6,7 @@ #include "pzip/archiver.h" #include "pzip/fast_deflate.h" #include "pzip/utils.h" +#include "pzip/crypto/aes_encryptor.h" #include #include #include @@ -166,17 +167,71 @@ Error Archiver::compressFile(FileTask* task) { if (cancelled_) { return Error(ErrorCode::CANCELLED, "Operation cancelled"); } - - // 压缩文件内容 + + if (options_.onEntryStart) { + options_.onEntryStart(task->header.name); + } + + // 先按与无加密相同的路径做 Deflate/Store,再对压缩流做 WinZip AES(与 libzip 语义一致) Error err = compress(task); if (err) return err; - - // 填充头信息 + + if (options_.isEncrypted() && !fs::is_directory(task->status)) { + err = encryptFile(task); + if (err) return err; + } + populateHeader(task); - - // 送入写入队列 + fileWriterPool_->enqueue(task); - + + return Error(); +} + + +Error Archiver::encryptFile(FileTask* task) { + // 创建加密器 + AESEncryptor encryptor(options_.password, options_.encryptionMethod); + if (!encryptor.isValid()) { + return Error(ErrorCode::ENCRYPTION_ERROR, "Failed to initialize encryptor: " + encryptor.lastError()); + } + + // 保存盐值和密码验证值 + task->salt = encryptor.salt(); + task->passwordVerification = encryptor.passwordVerification(); + + // 收集压缩后的数据 + std::vector compressedData; + task->readCompressedData([&compressedData](const uint8_t* data, size_t size) { + compressedData.insert(compressedData.end(), data, data + size); + }); + + // 加密压缩后的数据 + if (!compressedData.empty()) { + task->encryptedData = encryptor.encrypt(compressedData); + if (task->encryptedData.empty() && !compressedData.empty()) { + return Error(ErrorCode::ENCRYPTION_ERROR, "Encryption failed: " + encryptor.lastError()); + } + } + + // 获取认证码 + task->authCode = encryptor.authenticationCode(); + + // 内层压缩方式须与加密前数据一致(Store / Deflate) + uint16_t innerMethod = ZIP_METHOD_STORE; + if (!task->isSymlink && !task->streamFromSource) { + innerMethod = ZIP_METHOD_DEFLATE; + } + task->header.method = innerMethod; + task->header.encryptionMethod = ZIP_ENCRYPTION_WINZIP_AES; + task->header.aesStrength = encryptionStrength(options_.encryptionMethod); + task->header.actualCompressionMethod = innerMethod; + + // 计算加密后的大小:salt + pv + encrypted data + authCode + size_t saltSize = task->salt.size(); + size_t encryptedDataSize = task->encryptedData.size(); + task->header.compressedSize = saltSize + WINZIP_AES_PV_SIZE + encryptedDataSize + WINZIP_AES_AUTH_CODE_SIZE; + return Error(); } @@ -203,6 +258,8 @@ Error Archiver::compress(FileTask* task) { std::vector buf(BUFFER_SIZE); uint32_t crc = 0; uint64_t totalBytesRead = 0; + uint64_t reportedBytesRead = 0; + constexpr uint64_t REPORT_GRANULARITY = 512 * 1024; // 限制回调频率,避免拖慢压缩线程 if (useStore) { // Store 模式:跳过读文件,写入 ZIP 时直接从源文件流式读取并计算 CRC @@ -226,6 +283,14 @@ Error Archiver::compress(FileTask* task) { crc = ::crc32(crc, buf.data(), bytesRead); writer.write(buf.data(), bytesRead); totalBytesRead += bytesRead; + + if (options_.onBytesRead) { + const uint64_t deltaSinceLast = totalBytesRead - reportedBytesRead; + if (deltaSinceLast >= REPORT_GRANULARITY) { + options_.onBytesRead(deltaSinceLast); + reportedBytesRead = totalBytesRead; + } + } } } @@ -242,6 +307,13 @@ Error Archiver::compress(FileTask* task) { "Short read: expected " + std::to_string(task->fileSize) + " bytes, got " + std::to_string(totalBytesRead) + " for: " + task->path.string()); } + + if (options_.onBytesRead) { + const uint64_t tail = totalBytesRead - reportedBytesRead; + if (tail > 0) { + options_.onBytesRead(tail); + } + } file.close(); @@ -252,24 +324,24 @@ Error Archiver::compress(FileTask* task) { void Archiver::populateHeader(FileTask* task) { auto& h = task->header; - + // UTF-8 检测 auto [validUtf8, requireUtf8] = utils::detectUTF8(h.name); if (requireUtf8 && validUtf8) { h.flags |= ZIP_FLAG_UTF8; } - + // 版本信息 h.versionMadeBy = (3 << 8) | ZIP_VERSION_20; // Unix + ZIP 2.0 h.versionNeeded = ZIP_VERSION_20; - + // 修改时间 time_t modTime = utils::getModTime(task->path); ExtendedTimestamp ext; ext.modTime = modTime; auto extData = ext.encode(); h.extra.insert(h.extra.end(), extData.begin(), extData.end()); - + // DOS 时间 struct tm* tm = localtime(&modTime); if (tm) { @@ -280,7 +352,7 @@ void Archiver::populateHeader(FileTask* task) { (((tm->tm_mon + 1) & 0x0F) << 5) | (tm->tm_mday & 0x1F); } - + // 目录处理 if (fs::is_directory(task->status)) { if (!h.name.empty() && h.name.back() != '/') { @@ -291,6 +363,9 @@ void Archiver::populateHeader(FileTask* task) { h.uncompressedSize = 0; h.compressedSize = 0; h.crc32 = 0; + // 目录不加密 + h.encryptionMethod = ZIP_ENCRYPTION_NONE; + h.aesStrength = 0; } else if (task->isSymlink) { // 符号链接:存储链接目标 h.method = ZIP_METHOD_STORE; @@ -299,6 +374,16 @@ void Archiver::populateHeader(FileTask* task) { h.compressedSize = task->symlinkTarget.size(); // 设置 Unix 符号链接属性 h.externalAttr = static_cast(S_IFLNK | 0777) << 16; + } else if (h.isEncrypted()) { + // 加密文件:不使用数据描述符,直接写入实际大小 + // 注意:DATA_DESCRIPTOR 与 WinZip AES 加密不兼容 + h.flags |= ZIP_FLAG_ENCRYPTED; + h.flags &= ~ZIP_FLAG_DATA_DESCRIPTOR; + // CRC32 在 AE-2 格式中为 0 + h.crc32 = 0; + // 设置未压缩大小(原始文件大小) + h.uncompressedSize = task->fileSize; + // compressedSize 已经在 encryptFile 中设置 } else if (options_.compressionLevel == 0) { // Store 模式:不压缩,CRC 在写入阶段边读边算,用 DATA_DESCRIPTOR 后置 h.method = ZIP_METHOD_STORE; @@ -318,22 +403,35 @@ Error Archiver::archiveFile(FileTask* task) { FileTaskPool::instance().release(std::unique_ptr(task)); return Error(ErrorCode::CANCELLED, "Operation cancelled"); } - - // 写入 ZIP - Error err = writer_->createRaw(task->header, - [task](std::function writer) { - task->readCompressedData(writer); - }); - + + Error err; + + // 如果是加密文件,使用加密写入方式 + if (task->header.isEncrypted()) { + err = writer_->createEncrypted( + task->header, + task->salt, + task->passwordVerification, + task->encryptedData, + task->authCode + ); + } else { + // 普通写入 + err = writer_->createRaw(task->header, + [task](std::function writer) { + task->readCompressedData(writer); + }); + } + // 更新进度 processedFiles_++; if (options_.progress) { options_.progress(processedFiles_, totalFiles_); } - + // 释放任务 FileTaskPool::instance().release(std::unique_ptr(task)); - + return err; } diff --git a/src/pzip/src/crypto/aes_encryptor.cpp b/src/pzip/src/crypto/aes_encryptor.cpp new file mode 100644 index 000000000..3cf9f5afd --- /dev/null +++ b/src/pzip/src/crypto/aes_encryptor.cpp @@ -0,0 +1,580 @@ +// Copyright (C) 2025 ~ 2026 Uniontech Software Technology Co.,Ltd. +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "pzip/crypto/aes_encryptor.h" + +#include +#include +#include +#include + +#include +#include + +namespace pzip { + +//============================================================================= +// AESEncryptor 实现 +//============================================================================= + +AESEncryptor::AESEncryptor(const std::string& password, EncryptionMethod method) + : method_(method) + , aesCtx_(nullptr) + , hmacCtx_(nullptr) + , valid_(false) + , counter_(0) + , padOffset_(AES_BLOCK_SIZE) +{ + if (method == EncryptionMethod::None || password.empty()) { + lastError_ = "Invalid encryption method or empty password"; + return; + } + + size_t keySize = encryptionKeySize(method); + size_t saltSize = encryptionSaltSize(method); + + if (keySize == 0 || saltSize == 0) { + lastError_ = "Unsupported encryption method"; + return; + } + + salt_.resize(saltSize); + aesKey_.resize(keySize); + hmacKey_.resize(keySize); + counterBlock_.resize(AES_BLOCK_SIZE, 0); + pad_.resize(AES_BLOCK_SIZE, 0); + + if (!generateSalt()) { + lastError_ = "CSPRNG failed: RAND_bytes could not generate salt"; + return; + } + + if (!deriveKeys(password)) { + return; + } + + if (!initCipher()) { + return; + } + + if (!initHMAC()) { + return; + } + + valid_ = true; +} + +AESEncryptor::~AESEncryptor() { + if (aesCtx_) { + EVP_CIPHER_CTX_free(aesCtx_); + aesCtx_ = nullptr; + } + if (hmacCtx_) { + HMAC_CTX_free(hmacCtx_); + hmacCtx_ = nullptr; + } + std::fill(aesKey_.begin(), aesKey_.end(), 0); + std::fill(hmacKey_.begin(), hmacKey_.end(), 0); + std::fill(counterBlock_.begin(), counterBlock_.end(), 0); + std::fill(pad_.begin(), pad_.end(), 0); +} + +AESEncryptor::AESEncryptor(AESEncryptor&& other) noexcept + : method_(other.method_) + , salt_(std::move(other.salt_)) + , aesKey_(std::move(other.aesKey_)) + , hmacKey_(std::move(other.hmacKey_)) + , passwordVerification_(other.passwordVerification_) + , aesCtx_(other.aesCtx_) + , hmacCtx_(other.hmacCtx_) + , valid_(other.valid_) + , lastError_(std::move(other.lastError_)) + , counterBlock_(std::move(other.counterBlock_)) + , pad_(std::move(other.pad_)) + , padOffset_(other.padOffset_) + , counter_(other.counter_) +{ + other.aesCtx_ = nullptr; + other.hmacCtx_ = nullptr; + other.valid_ = false; +} + +AESEncryptor& AESEncryptor::operator=(AESEncryptor&& other) noexcept { + if (this != &other) { + if (aesCtx_) { + EVP_CIPHER_CTX_free(aesCtx_); + } + if (hmacCtx_) { + HMAC_CTX_free(hmacCtx_); + } + + method_ = other.method_; + salt_ = std::move(other.salt_); + aesKey_ = std::move(other.aesKey_); + hmacKey_ = std::move(other.hmacKey_); + passwordVerification_ = other.passwordVerification_; + aesCtx_ = other.aesCtx_; + hmacCtx_ = other.hmacCtx_; + valid_ = other.valid_; + lastError_ = std::move(other.lastError_); + counterBlock_ = std::move(other.counterBlock_); + pad_ = std::move(other.pad_); + padOffset_ = other.padOffset_; + counter_ = other.counter_; + + other.aesCtx_ = nullptr; + other.hmacCtx_ = nullptr; + other.valid_ = false; + } + return *this; +} + +bool AESEncryptor::generateSalt() { + return RAND_bytes(salt_.data(), static_cast(salt_.size())) == 1; +} + +bool AESEncryptor::deriveKeys(const std::string& password) { + size_t derivedKeyLen = aesKey_.size() + hmacKey_.size() + WINZIP_AES_PV_SIZE; + std::vector derivedKey(derivedKeyLen); + + int result = PKCS5_PBKDF2_HMAC( + password.c_str(), + static_cast(password.size()), + salt_.data(), + static_cast(salt_.size()), + PBKDF2_ITERATIONS, + EVP_sha1(), + static_cast(derivedKeyLen), + derivedKey.data() + ); + + if (result != 1) { + lastError_ = "PBKDF2 key derivation failed"; + return false; + } + + size_t offset = 0; + std::copy(derivedKey.begin() + offset, + derivedKey.begin() + offset + aesKey_.size(), + aesKey_.begin()); + offset += aesKey_.size(); + + std::copy(derivedKey.begin() + offset, + derivedKey.begin() + offset + hmacKey_.size(), + hmacKey_.begin()); + offset += hmacKey_.size(); + + passwordVerification_ = static_cast(derivedKey[offset]) | + (static_cast(derivedKey[offset + 1]) << 8); + + std::fill(derivedKey.begin(), derivedKey.end(), 0); + return true; +} + +bool AESEncryptor::initCipher() { + aesCtx_ = EVP_CIPHER_CTX_new(); + if (!aesCtx_) { + lastError_ = "Failed to create cipher context"; + return false; + } + + const EVP_CIPHER* cipher = nullptr; + switch (method_) { + case EncryptionMethod::AES128: + cipher = EVP_aes_128_ecb(); + break; + case EncryptionMethod::AES192: + cipher = EVP_aes_192_ecb(); + break; + case EncryptionMethod::AES256: + cipher = EVP_aes_256_ecb(); + break; + default: + lastError_ = "Unsupported encryption method"; + return false; + } + + if (EVP_EncryptInit_ex(aesCtx_, cipher, nullptr, aesKey_.data(), nullptr) != 1) { + lastError_ = "Failed to initialize cipher"; + return false; + } + EVP_CIPHER_CTX_set_padding(aesCtx_, 0); + + return true; +} + +bool AESEncryptor::initHMAC() { + hmacCtx_ = HMAC_CTX_new(); + if (!hmacCtx_) { + lastError_ = "Failed to create HMAC context"; + return false; + } + + if (HMAC_Init_ex(hmacCtx_, hmacKey_.data(), static_cast(hmacKey_.size()), + EVP_sha1(), nullptr) != 1) { + lastError_ = "Failed to initialize HMAC"; + return false; + } + + return true; +} + +void AESEncryptor::incrementCounter() { + for (int j = 0; j < 8; j++) { + counterBlock_[j]++; + if (counterBlock_[j] != 0) { + break; + } + } +} + +bool AESEncryptor::aesCrypt(uint8_t* data, size_t length) { + for (size_t i = 0; i < length; i++) { + if (padOffset_ == AES_BLOCK_SIZE) { + incrementCounter(); + + int outLen = 0; + if (EVP_EncryptUpdate(aesCtx_, pad_.data(), &outLen, + counterBlock_.data(), AES_BLOCK_SIZE) != 1) { + lastError_ = "AES encryption failed"; + return false; + } + padOffset_ = 0; + } + data[i] ^= pad_[padOffset_++]; + } + return true; +} + +size_t AESEncryptor::encrypt(const uint8_t* input, size_t inputSize, uint8_t* output) { + if (!valid_ || !aesCtx_ || !hmacCtx_) { + return 0; + } + + std::memcpy(output, input, inputSize); + + if (!aesCrypt(output, inputSize)) { + return 0; + } + + if (HMAC_Update(hmacCtx_, output, inputSize) != 1) { + lastError_ = "HMAC update failed"; + return 0; + } + + return inputSize; +} + +std::vector AESEncryptor::encrypt(const std::vector& input) { + std::vector output(input.size()); + size_t outLen = encrypt(input.data(), input.size(), output.data()); + if (outLen == 0) { + return {}; + } + output.resize(outLen); + return output; +} + +std::array AESEncryptor::authenticationCode() { + std::array authCode{}; + + if (!valid_ || !hmacCtx_) { + return authCode; + } + + unsigned int macLen = SHA_DIGEST_LENGTH; + uint8_t mac[SHA_DIGEST_LENGTH]; + + if (HMAC_Final(hmacCtx_, mac, &macLen) != 1) { + lastError_ = "HMAC final failed"; + return authCode; + } + + std::copy(mac, mac + WINZIP_AES_AUTH_CODE_SIZE, authCode.begin()); + + return authCode; +} + +size_t AESEncryptor::encryptedSize(size_t originalSize) const { + size_t saltSz = encryptionSaltSize(method_); + return saltSz + WINZIP_AES_PV_SIZE + originalSize + WINZIP_AES_AUTH_CODE_SIZE; +} + +//============================================================================= +// AESDecryptor 实现 +//============================================================================= + +AESDecryptor::AESDecryptor(const std::string& password, EncryptionMethod method, + const std::vector& salt, uint16_t passwordVerification) + : method_(method) + , salt_(salt) + , expectedPasswordVerification_(passwordVerification) + , aesCtx_(nullptr) + , hmacCtx_(nullptr) + , valid_(false) + , passwordVerified_(false) + , padOffset_(AES_BLOCK_SIZE) +{ + if (method == EncryptionMethod::None || password.empty() || salt.empty()) { + lastError_ = "Invalid decryption parameters"; + return; + } + + size_t keySize = encryptionKeySize(method); + if (keySize == 0) { + lastError_ = "Unsupported encryption method"; + return; + } + + aesKey_.resize(keySize); + hmacKey_.resize(keySize); + counterBlock_.resize(AES_BLOCK_SIZE, 0); + pad_.resize(AES_BLOCK_SIZE, 0); + + if (!deriveKeys(password)) { + return; + } + + passwordVerified_ = (actualPasswordVerification_ == expectedPasswordVerification_); + if (!passwordVerified_) { + lastError_ = "Wrong password"; + return; + } + + if (!initCipher()) { + return; + } + + hmacCtx_ = HMAC_CTX_new(); + if (!hmacCtx_) { + lastError_ = "Failed to create HMAC context"; + return; + } + + if (HMAC_Init_ex(hmacCtx_, hmacKey_.data(), static_cast(hmacKey_.size()), + EVP_sha1(), nullptr) != 1) { + lastError_ = "Failed to initialize HMAC"; + return; + } + + valid_ = true; +} + +AESDecryptor::~AESDecryptor() { + if (aesCtx_) { + EVP_CIPHER_CTX_free(aesCtx_); + aesCtx_ = nullptr; + } + if (hmacCtx_) { + HMAC_CTX_free(hmacCtx_); + hmacCtx_ = nullptr; + } + std::fill(aesKey_.begin(), aesKey_.end(), 0); + std::fill(hmacKey_.begin(), hmacKey_.end(), 0); + std::fill(counterBlock_.begin(), counterBlock_.end(), 0); + std::fill(pad_.begin(), pad_.end(), 0); +} + +AESDecryptor::AESDecryptor(AESDecryptor&& other) noexcept + : method_(other.method_) + , salt_(std::move(other.salt_)) + , expectedPasswordVerification_(other.expectedPasswordVerification_) + , actualPasswordVerification_(other.actualPasswordVerification_) + , aesKey_(std::move(other.aesKey_)) + , hmacKey_(std::move(other.hmacKey_)) + , aesCtx_(other.aesCtx_) + , hmacCtx_(other.hmacCtx_) + , valid_(other.valid_) + , passwordVerified_(other.passwordVerified_) + , lastError_(std::move(other.lastError_)) + , counterBlock_(std::move(other.counterBlock_)) + , pad_(std::move(other.pad_)) + , padOffset_(other.padOffset_) +{ + other.aesCtx_ = nullptr; + other.hmacCtx_ = nullptr; + other.valid_ = false; +} + +AESDecryptor& AESDecryptor::operator=(AESDecryptor&& other) noexcept { + if (this != &other) { + if (aesCtx_) { + EVP_CIPHER_CTX_free(aesCtx_); + } + if (hmacCtx_) { + HMAC_CTX_free(hmacCtx_); + } + + method_ = other.method_; + salt_ = std::move(other.salt_); + expectedPasswordVerification_ = other.expectedPasswordVerification_; + actualPasswordVerification_ = other.actualPasswordVerification_; + aesKey_ = std::move(other.aesKey_); + hmacKey_ = std::move(other.hmacKey_); + aesCtx_ = other.aesCtx_; + hmacCtx_ = other.hmacCtx_; + valid_ = other.valid_; + passwordVerified_ = other.passwordVerified_; + lastError_ = std::move(other.lastError_); + counterBlock_ = std::move(other.counterBlock_); + pad_ = std::move(other.pad_); + padOffset_ = other.padOffset_; + + other.aesCtx_ = nullptr; + other.hmacCtx_ = nullptr; + other.valid_ = false; + } + return *this; +} + +bool AESDecryptor::deriveKeys(const std::string& password) { + size_t keySize = aesKey_.size(); + size_t derivedKeyLen = keySize + keySize + WINZIP_AES_PV_SIZE; + std::vector derivedKey(derivedKeyLen); + + int result = PKCS5_PBKDF2_HMAC( + password.c_str(), + static_cast(password.size()), + salt_.data(), + static_cast(salt_.size()), + PBKDF2_ITERATIONS, + EVP_sha1(), + static_cast(derivedKeyLen), + derivedKey.data() + ); + + if (result != 1) { + lastError_ = "PBKDF2 key derivation failed"; + return false; + } + + size_t offset = 0; + std::copy(derivedKey.begin() + offset, + derivedKey.begin() + offset + keySize, + aesKey_.begin()); + offset += keySize; + + std::copy(derivedKey.begin() + offset, + derivedKey.begin() + offset + keySize, + hmacKey_.begin()); + offset += keySize; + + actualPasswordVerification_ = static_cast(derivedKey[offset]) | + (static_cast(derivedKey[offset + 1]) << 8); + + std::fill(derivedKey.begin(), derivedKey.end(), 0); + return true; +} + +bool AESDecryptor::initCipher() { + aesCtx_ = EVP_CIPHER_CTX_new(); + if (!aesCtx_) { + lastError_ = "Failed to create cipher context"; + return false; + } + + const EVP_CIPHER* cipher = nullptr; + switch (method_) { + case EncryptionMethod::AES128: + cipher = EVP_aes_128_ecb(); + break; + case EncryptionMethod::AES192: + cipher = EVP_aes_192_ecb(); + break; + case EncryptionMethod::AES256: + cipher = EVP_aes_256_ecb(); + break; + default: + lastError_ = "Unsupported encryption method"; + return false; + } + + if (EVP_EncryptInit_ex(aesCtx_, cipher, nullptr, aesKey_.data(), nullptr) != 1) { + lastError_ = "Failed to initialize cipher"; + return false; + } + EVP_CIPHER_CTX_set_padding(aesCtx_, 0); + + return true; +} + +void AESDecryptor::incrementCounter() { + for (int j = 0; j < 8; j++) { + counterBlock_[j]++; + if (counterBlock_[j] != 0) { + break; + } + } +} + +bool AESDecryptor::aesCrypt(uint8_t* data, size_t length) { + for (size_t i = 0; i < length; i++) { + if (padOffset_ == AES_BLOCK_SIZE) { + incrementCounter(); + + int outLen = 0; + if (EVP_EncryptUpdate(aesCtx_, pad_.data(), &outLen, + counterBlock_.data(), AES_BLOCK_SIZE) != 1) { + lastError_ = "AES encryption failed"; + return false; + } + padOffset_ = 0; + } + data[i] ^= pad_[padOffset_++]; + } + return true; +} + +size_t AESDecryptor::decrypt(const uint8_t* input, size_t inputSize, uint8_t* output) { + if (!valid_ || !aesCtx_ || !hmacCtx_) { + return 0; + } + + if (HMAC_Update(hmacCtx_, input, inputSize) != 1) { + lastError_ = "HMAC update failed"; + return 0; + } + + std::memcpy(output, input, inputSize); + + if (!aesCrypt(output, inputSize)) { + return 0; + } + + return inputSize; +} + +std::vector AESDecryptor::decrypt(const std::vector& input) { + std::vector output(input.size()); + size_t outLen = decrypt(input.data(), input.size(), output.data()); + if (outLen == 0) { + return {}; + } + output.resize(outLen); + return output; +} + +bool AESDecryptor::verifyAuthenticationCode(const std::array& authCode) { + if (!valid_ || !hmacCtx_) { + return false; + } + + unsigned int macLen = SHA_DIGEST_LENGTH; + uint8_t mac[SHA_DIGEST_LENGTH]; + + if (HMAC_Final(hmacCtx_, mac, &macLen) != 1) { + lastError_ = "HMAC final failed"; + return false; + } + + uint8_t diff = 0; + for (size_t i = 0; i < WINZIP_AES_AUTH_CODE_SIZE; ++i) { + diff |= mac[i] ^ authCode[i]; + } + + return diff == 0; +} + +} // namespace pzip diff --git a/src/pzip/src/crypto/winzip_aes_extra.cpp b/src/pzip/src/crypto/winzip_aes_extra.cpp new file mode 100644 index 000000000..35478d2a1 --- /dev/null +++ b/src/pzip/src/crypto/winzip_aes_extra.cpp @@ -0,0 +1,75 @@ +// Copyright (C) 2025 ~ 2026 Uniontech Software Technology Co.,Ltd. +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "pzip/crypto/winzip_aes_extra.h" + +namespace pzip { + +namespace { +constexpr size_t WINZIP_AES_EXTRA_TOTAL_SIZE = 11; +constexpr uint16_t WINZIP_AES_EXTRA_PAYLOAD_SIZE = 7; +constexpr uint16_t BYTE_MASK_U16 = 0x00FF; +constexpr int BYTE_SHIFT = 8; + +inline uint8_t u16Lo(uint16_t v) { return static_cast(v & BYTE_MASK_U16); } +inline uint8_t u16Hi(uint16_t v) { return static_cast((v >> BYTE_SHIFT) & BYTE_MASK_U16); } +inline uint16_t le16At(const uint8_t* p) { return static_cast(p[0] | (static_cast(p[1]) << BYTE_SHIFT)); } + +// WinZip AES extra (0x9901) layout (total 11 bytes): +// [0..1] headerId (0x9901) +// [2..3] dataSize (7) +// [4..5] vendorVersion (AE-1/AE-2) +// [6..7] vendorId ("AE" => 0x4541 little-endian) +// [8] aesStrength (1/2/3) +// [9..10] actualCompressionMethod (Store/Deflate/...) +constexpr size_t WINZIP_AES_EXTRA_OFF_VENDOR_VERSION = 4; +constexpr size_t WINZIP_AES_EXTRA_OFF_VENDOR_ID = 6; +constexpr size_t WINZIP_AES_EXTRA_OFF_AES_STRENGTH = 8; +constexpr size_t WINZIP_AES_EXTRA_OFF_ACTUAL_METHOD = 9; +} + +std::vector WinZipAESExtra::encode() const { + std::vector data; + data.reserve(WINZIP_AES_EXTRA_TOTAL_SIZE); + + data.push_back(u16Lo(WINZIP_AES_EXTRA_ID)); + data.push_back(u16Hi(WINZIP_AES_EXTRA_ID)); + + data.push_back(u16Lo(WINZIP_AES_EXTRA_PAYLOAD_SIZE)); + data.push_back(u16Hi(WINZIP_AES_EXTRA_PAYLOAD_SIZE)); + + data.push_back(u16Lo(vendorVersion)); + data.push_back(u16Hi(vendorVersion)); + + data.push_back(u16Lo(vendorId)); + data.push_back(u16Hi(vendorId)); + + data.push_back(aesStrength); + + data.push_back(u16Lo(actualCompressionMethod)); + data.push_back(u16Hi(actualCompressionMethod)); + + return data; +} + +WinZipAESExtra WinZipAESExtra::decode(const uint8_t* data, size_t size) { + WinZipAESExtra extra; + extra.vendorVersion = 0; + extra.vendorId = WINZIP_AES_VENDOR_ID_AE; + extra.aesStrength = 0; + extra.actualCompressionMethod = ZIP_METHOD_STORE; + + // Full WinZip AES extra record is 11 bytes: 2 id + 2 size + 2 ver + 2 vendor + 1 strength + 2 method + if (size >= WINZIP_AES_EXTRA_TOTAL_SIZE) { + extra.vendorVersion = le16At(data + WINZIP_AES_EXTRA_OFF_VENDOR_VERSION); + extra.vendorId = le16At(data + WINZIP_AES_EXTRA_OFF_VENDOR_ID); + extra.aesStrength = data[WINZIP_AES_EXTRA_OFF_AES_STRENGTH]; + extra.actualCompressionMethod = le16At(data + WINZIP_AES_EXTRA_OFF_ACTUAL_METHOD); + } + + return extra; +} + +} // namespace pzip diff --git a/src/pzip/src/file_task.cpp b/src/pzip/src/file_task.cpp index fdbbee523..b44453e55 100644 --- a/src/pzip/src/file_task.cpp +++ b/src/pzip/src/file_task.cpp @@ -45,12 +45,18 @@ FileTask::FileTask(FileTask&& other) noexcept , overflow_(std::move(other.overflow_)) , overflowPath_(std::move(other.overflowPath_)) , written_(other.written_) + , encryptedData(std::move(other.encryptedData)) + , salt(std::move(other.salt)) + , passwordVerification(other.passwordVerification) + , authCode(other.authCode) { other.compressor = nullptr; other.isSymlink = false; other.streamFromSource = false; other.bufferUsed_ = 0; other.written_ = 0; + other.passwordVerification = 0; + other.authCode.fill(0); } FileTask& FileTask::operator=(FileTask&& other) noexcept { @@ -68,12 +74,18 @@ FileTask& FileTask::operator=(FileTask&& other) noexcept { overflow_ = std::move(other.overflow_); overflowPath_ = std::move(other.overflowPath_); written_ = other.written_; + encryptedData = std::move(other.encryptedData); + salt = std::move(other.salt); + passwordVerification = other.passwordVerification; + authCode = other.authCode; other.compressor = nullptr; other.isSymlink = false; other.streamFromSource = false; other.bufferUsed_ = 0; other.written_ = 0; + other.passwordVerification = 0; + other.authCode.fill(0); } return *this; } @@ -95,6 +107,10 @@ Error FileTask::reset(const fs::path& filePath, const fs::path& relativeTo) { isSymlink = false; symlinkTarget.clear(); streamFromSource = false; + encryptedData.clear(); + salt.clear(); + passwordVerification = 0; + authCode.fill(0); // 设置新文件信息 path = filePath; diff --git a/src/pzip/src/zip_writer.cpp b/src/pzip/src/zip_writer.cpp index 4c42ce4ae..8b5ade787 100644 --- a/src/pzip/src/zip_writer.cpp +++ b/src/pzip/src/zip_writer.cpp @@ -117,79 +117,118 @@ Error ZipWriter::writeLocalFileHeader(const ZipFileHeader& header) { // 对应 Go archive/zip 的 CreateRaw/CreateHeader 写入本地文件头的逻辑 std::vector buf; buf.reserve(30 + header.name.size() + header.extra.size()); - + auto write16 = [&buf](uint16_t v) { buf.push_back(v & 0xFF); buf.push_back((v >> 8) & 0xFF); }; - + auto write32 = [&buf](uint32_t v) { buf.push_back(v & 0xFF); buf.push_back((v >> 8) & 0xFF); buf.push_back((v >> 16) & 0xFF); buf.push_back((v >> 24) & 0xFF); }; - + // Signature write32(LOCAL_FILE_HEADER_SIG); - - // Version needed (ZIP64 需要版本 4.5) - // 对应 Go: if fh.isZip64() { fh.ReaderVersion = zipVersion45 } - write16(header.isZip64() ? ZIP_VERSION_45 : header.versionNeeded); - - // Flags - write16(header.flags); - - // Compression method - write16(header.method); - + + // Version needed (ZIP64 需要版本 4.5, WinZip AES 需要版本 2.0) + uint16_t versionNeeded = header.versionNeeded; + if (header.isZip64()) { + versionNeeded = ZIP_VERSION_45; + } + write16(versionNeeded); + + // Flags (加密时设置加密标志位) + uint16_t flags = header.flags; + if (header.isEncrypted()) { + flags |= ZIP_FLAG_ENCRYPTED; + } + write16(flags); + + // Compression method (WinZip AES 使用 99) + uint16_t method = header.method; + if (header.isEncrypted()) { + method = ZIP_ENCRYPTION_WINZIP_AES; + } + write16(method); + // Modification time and date write16(header.modTime); write16(header.modDate); - - // CRC32 (0 if using data descriptor) - if (header.flags & ZIP_FLAG_DATA_DESCRIPTOR) { + + // CRC32 (AE-2 格式使用 0,AE-1 格式使用实际 CRC32) + // 对于加密文件,直接写入 0 + if (header.isEncrypted()) { + // AE-2 格式: CRC32 存储为 0 + write32(0); + } else if (header.flags & ZIP_FLAG_DATA_DESCRIPTOR) { write32(0); } else { write32(header.crc32); } - - // Compressed size (0 if using data descriptor) - if (header.flags & ZIP_FLAG_DATA_DESCRIPTOR) { + + // Compressed size + // WinZip AES 加密文件不使用 DATA_DESCRIPTOR,直接写入实际大小 + if (header.isEncrypted()) { + write32(static_cast(header.compressedSize)); + } else if (header.flags & ZIP_FLAG_DATA_DESCRIPTOR) { write32(0); } else if (header.isZip64()) { write32(ZIP_UINT32_MAX); } else { write32(static_cast(header.compressedSize)); } - - // Uncompressed size (0 if using data descriptor) - if (header.flags & ZIP_FLAG_DATA_DESCRIPTOR) { + + // Uncompressed size + // WinZip AES 加密文件直接写入实际大小 + if (header.isEncrypted()) { + write32(static_cast(header.uncompressedSize)); + } else if (header.flags & ZIP_FLAG_DATA_DESCRIPTOR) { write32(0); } else if (header.isZip64()) { write32(ZIP_UINT32_MAX); } else { write32(static_cast(header.uncompressedSize)); } - + + // 构建 extra 字段(WinZip AES extra 必须放在第一位) + std::vector extra; + + // 如果是 WinZip AES 加密,先添加 WinZip AES Extra Field(必须在其他 extra 之前) + if (header.isEncrypted()) { + WinZipAESExtra aesExtra; + aesExtra.vendorVersion = WINZIP_AES_VERSION_2; // 使用 AE-2 + aesExtra.vendorId = WINZIP_AES_VENDOR_ID_AE; // "AE" + aesExtra.aesStrength = header.aesStrength; + aesExtra.actualCompressionMethod = header.actualCompressionMethod; + + std::vector aesExtraData = aesExtra.encode(); + extra.insert(extra.end(), aesExtraData.begin(), aesExtraData.end()); + } + + // 然后添加其他 extra 字段(如 Extended Timestamp) + extra.insert(extra.end(), header.extra.begin(), header.extra.end()); + // Filename length write16(static_cast(header.name.size())); - + // Extra field length - write16(static_cast(header.extra.size())); - + write16(static_cast(extra.size())); + // Filename buf.insert(buf.end(), header.name.begin(), header.name.end()); - + // Extra field - buf.insert(buf.end(), header.extra.begin(), header.extra.end()); - + buf.insert(buf.end(), extra.begin(), extra.end()); + file_.write(reinterpret_cast(buf.data()), buf.size()); - + if (!file_.good()) { return Error(ErrorCode::FILE_WRITE_ERROR, "Failed to write local file header"); } - + currentOffset_ += buf.size(); return Error(); } @@ -252,34 +291,34 @@ Error ZipWriter::writeDataDescriptor(const ZipFileHeader& header) { Error ZipWriter::createRaw(const ZipFileHeader& header, std::function)> dataProvider) { std::lock_guard lock(writeMutex_); - + if (!file_.is_open()) { return Error(ErrorCode::FILE_OPEN_ERROR, "File not open"); } - + // 记录本地文件头偏移 uint64_t localHeaderOffset = currentOffset_; - + // 写入本地文件头 Error err = writeLocalFileHeader(header); if (err) return err; - + // 写入压缩数据(dataProvider 可能会更新 header 中的 CRC 等字段) dataProvider([this](const uint8_t* data, size_t size) { file_.write(reinterpret_cast(data), size); currentOffset_ += size; }); - + if (!file_.good()) { return Error(ErrorCode::FILE_WRITE_ERROR, "Failed to write compressed data"); } - + // 如果使用数据描述符,写入它(此时 header.crc32 已在 dataProvider 中更新) if (header.flags & ZIP_FLAG_DATA_DESCRIPTOR) { err = writeDataDescriptor(header); if (err) return err; } - + // dataProvider 运行后再拷贝 header,确保 CRC 等字段是最新值 CentralDirEntry entry; entry.header = header; @@ -288,6 +327,65 @@ Error ZipWriter::createRaw(const ZipFileHeader& header, return Error(); } +Error ZipWriter::createEncrypted(const ZipFileHeader& header, + const std::vector& salt, + uint16_t passwordVerification, + const std::vector& encryptedData, + const std::array& authCode) { + std::lock_guard lock(writeMutex_); + + if (!file_.is_open()) { + return Error(ErrorCode::FILE_OPEN_ERROR, "File not open"); + } + + // 记录本地文件头偏移 + uint64_t localHeaderOffset = currentOffset_; + + // 写入本地文件头(会自动处理 WinZip AES Extra Field) + Error err = writeLocalFileHeader(header); + if (err) return err; + + // 写入加密数据格式:salt + passwordVerification + encryptedData + authCode + + // 1. 写入 salt + file_.write(reinterpret_cast(salt.data()), salt.size()); + currentOffset_ += salt.size(); + + // 2. 写入 password verification (2 bytes, little endian) + uint8_t pv[WINZIP_AES_PV_SIZE] = { + static_cast(passwordVerification & 0xFF), + static_cast((passwordVerification >> 8) & 0xFF) + }; + file_.write(reinterpret_cast(pv), WINZIP_AES_PV_SIZE); + currentOffset_ += WINZIP_AES_PV_SIZE; + + // 3. 写入加密数据 + if (!encryptedData.empty()) { + file_.write(reinterpret_cast(encryptedData.data()), encryptedData.size()); + currentOffset_ += encryptedData.size(); + } + + // 4. 写入 authentication code (10 bytes) + file_.write(reinterpret_cast(authCode.data()), WINZIP_AES_AUTH_CODE_SIZE); + currentOffset_ += WINZIP_AES_AUTH_CODE_SIZE; + + if (!file_.good()) { + return Error(ErrorCode::FILE_WRITE_ERROR, "Failed to write encrypted data"); + } + + // 不使用数据描述符 - WinZip AES 格式直接在本地文件头中写入实际大小 + // 7z 和其他工具期望加密文件有正确的本地文件头大小 + + // 添加到中央目录 + CentralDirEntry entry; + entry.header = header; + entry.header.crc32 = 0; // AE-2 格式 + entry.localHeaderOffset = localHeaderOffset; + centralDir_.push_back(entry); + + return Error(); +} + Error ZipWriter::create(const ZipFileHeader& header, const uint8_t* data, size_t size) { // 这个简化实现直接存储,实际应该压缩 ZipFileHeader h = header; @@ -352,32 +450,44 @@ Error ZipWriter::writeCentralDirectory() { for (auto& entry : centralDir_) { auto& h = entry.header; buf.clear(); - + // 检查是否需要 ZIP64 extra 字段 bool needZip64 = h.isZip64() || entry.localHeaderOffset >= ZIP_UINT32_MAX; - + // Signature write32(CENTRAL_DIR_HEADER_SIG); - + // Version made by write16(h.versionMadeBy); - + // Version needed (ZIP64 需要版本 4.5) write16(needZip64 ? ZIP_VERSION_45 : h.versionNeeded); - - // Flags - write16(h.flags); - - // Compression method - write16(h.method); - + + // Flags (加密时设置加密标志位) + uint16_t flags = h.flags; + if (h.isEncrypted()) { + flags |= ZIP_FLAG_ENCRYPTED; + } + write16(flags); + + // Compression method (WinZip AES 使用 99) + uint16_t method = h.method; + if (h.isEncrypted()) { + method = ZIP_ENCRYPTION_WINZIP_AES; + } + write16(method); + // Modification time and date write16(h.modTime); write16(h.modDate); - - // CRC32 - write32(h.crc32); - + + // CRC32 (AE-2 格式为 0) + uint32_t crc32 = h.crc32; + if (h.isEncrypted()) { + crc32 = 0; // AE-2 格式 + } + write32(crc32); + if (needZip64) { // 对应 Go: "the file needs a zip64 header. store maxint in both // 32 bit size fields (and offset later) to signal that the @@ -388,10 +498,25 @@ Error ZipWriter::writeCentralDirectory() { write32(static_cast(h.compressedSize)); write32(static_cast(h.uncompressedSize)); } - + // Filename length write16(static_cast(h.name.size())); - + + // 构建 extra 字段 + std::vector extraFields; + + // 如果是 WinZip AES 加密,添加 WinZip AES Extra Field + if (h.isEncrypted()) { + WinZipAESExtra aesExtra; + aesExtra.vendorVersion = WINZIP_AES_VERSION_2; // 使用 AE-2 + aesExtra.vendorId = WINZIP_AES_VENDOR_ID_AE; // "AE" + aesExtra.aesStrength = h.aesStrength; + aesExtra.actualCompressionMethod = h.actualCompressionMethod; + + std::vector aesExtraData = aesExtra.encode(); + extraFields.insert(extraFields.end(), aesExtraData.begin(), aesExtraData.end()); + } + // 构建 ZIP64 extra 字段 std::vector zip64Extra; if (needZip64) { @@ -415,44 +540,50 @@ Error ZipWriter::writeCentralDirectory() { zip64Extra.push_back((entry.localHeaderOffset >> (i * 8)) & 0xFF); } } - - // Extra field length (原有 extra + ZIP64 extra) - size_t extraLen = h.extra.size() + zip64Extra.size(); + + // Extra field length (原有 extra + WinZip AES extra + ZIP64 extra) + size_t extraLen = h.extra.size() + extraFields.size() + zip64Extra.size(); write16(static_cast(extraLen)); - + // Comment length write16(0); - + // Disk number start write16(0); - + // Internal file attributes write16(0); - + // External file attributes write32(h.externalAttr); - + // Relative offset of local header if (entry.localHeaderOffset > ZIP_UINT32_MAX) { write32(ZIP_UINT32_MAX); } else { write32(static_cast(entry.localHeaderOffset)); } - + // 写入固定部分 file_.write(reinterpret_cast(buf.data()), buf.size()); currentOffset_ += buf.size(); - + // Filename file_.write(h.name.c_str(), h.name.size()); currentOffset_ += h.name.size(); - + // 原有 Extra field if (!h.extra.empty()) { file_.write(reinterpret_cast(h.extra.data()), h.extra.size()); currentOffset_ += h.extra.size(); } - + + // WinZip AES Extra Field + if (!extraFields.empty()) { + file_.write(reinterpret_cast(extraFields.data()), extraFields.size()); + currentOffset_ += extraFields.size(); + } + // ZIP64 extra field if (!zip64Extra.empty()) { file_.write(reinterpret_cast(zip64Extra.data()), zip64Extra.size()); diff --git a/src/source/mainwindow.cpp b/src/source/mainwindow.cpp index ed628b88f..43519e78b 100644 --- a/src/source/mainwindow.cpp +++ b/src/source/mainwindow.cpp @@ -1270,12 +1270,16 @@ void MainWindow::slotCompress(const QVariant &val) bUseLibarchive = false; #endif + // 旧逻辑:ZIP 只要有密码就强制使用 libzip(历史原因:pzip 早期不支持加密)。 + // 现状:Qt6 环境已支持通过 pzip(clipzipplugin 调用 pzip-bin)写入 WinZip AES(AE-2), + // 因此不应再因为“有密码”而强行切回 libzip,否则加密路径永远走不到 pzip。 bool useLibzipForPassword = false; - if ("application/zip" == m_stCompressParameter.strMimeType) { - if (!m_stCompressParameter.strPassword.isEmpty()) { - useLibzipForPassword = true; - } +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + if ("application/zip" == m_stCompressParameter.strMimeType + && !m_stCompressParameter.strPassword.isEmpty()) { + useLibzipForPassword = true; } +#endif UiTools::AssignPluginType eType = UiTools::APT_Auto; // 默认自动选择插件 if (true == useLibzipForPassword) { diff --git a/src/source/page/progresspage.cpp b/src/source/page/progresspage.cpp index 91442cae3..1e646e4b0 100644 --- a/src/source/page/progresspage.cpp +++ b/src/source/page/progresspage.cpp @@ -96,17 +96,28 @@ void ProgressPage::setProgress(double dPercent) return; } - int iPercent = qRound(dPercent); - if (m_iPerent >= iPercent) { - qDebug() << "Progress not increased, current:" << m_iPerent << "new:" << iPercent; - return ; + if (dPercent < 0) { + dPercent = 0; } - qInfo() << "Updating progress:" << iPercent << "%"; - m_iPerent = iPercent; - m_pProgressBar->setValue(m_iPerent); // 进度条刷新值 - m_pProgressBar->update(); + // 原先用「整数百分比是否变大」过滤更新,会导致子百分比进度(如 1.1%、1.2%)全部被丢弃, + // 从而永远不调用 calSpeedAndRemainingTime,速度与剩余时间一直停在「计算中」。 + // 这里改为:进度必须单调递增(浮点),进度条仍可按四舍五入刷新。 + if (dPercent <= m_dProgressPercent) { + qDebug() << "Progress not increased, current:" << m_dProgressPercent << "new:" << dPercent; + return; + } + + m_dProgressPercent = dPercent; + + const int iPercent = qMin(100, qRound(dPercent)); + if (iPercent != m_iPerent) { + m_iPerent = iPercent; + m_pProgressBar->setValue(m_iPerent); // 进度条刷新值 + m_pProgressBar->update(); + } + qInfo() << "Updating progress:" << dPercent << "% (bar" << m_iPerent << "%)"; // 刷新界面显示 double dSpeed = 0.0; qint64 qRemainingTime = 0; @@ -158,12 +169,14 @@ void ProgressPage::resetProgress() m_timer.elapsed(); m_iPerent = 0; + m_dProgressPercent = -1.0; m_qConsumeTime = 0; } void ProgressPage::restartTimer() { qDebug() << "restartTimer"; + m_qConsumeTime = 0; m_timer.restart(); } @@ -271,12 +284,17 @@ void ProgressPage::initConnections() void ProgressPage::calSpeedAndRemainingTime(double &dSpeed, qint64 &qRemainingTime) { qDebug() << "calSpeedAndRemainingTime"; - if (m_qConsumeTime < 0) { - qDebug() << "Consume time is negative, restart timer"; + // QElapsedTimer 未启动时 elapsed() 会返回 -1,会导致 m_qConsumeTime 变成负数, + // 然后速度/剩余时间被重置为 0(你日志里的 warning 就是这个原因)。 + if (!m_timer.isValid()) { m_timer.start(); + m_qConsumeTime = 0; } - m_qConsumeTime += m_timer.elapsed(); + const qint64 delta = m_timer.elapsed(); + if (delta > 0) { + m_qConsumeTime += delta; + } if (m_qConsumeTime < 0) { qWarning() << "Consume time is negative, reset speed and remaining time"; @@ -285,6 +303,8 @@ void ProgressPage::calSpeedAndRemainingTime(double &dSpeed, qint64 &qRemainingTi return; } + const double p = qBound(0.0, m_dProgressPercent < 0 ? 0.0 : m_dProgressPercent, 100.0); + // 计算速度 if (0 == m_qConsumeTime) { qWarning() << "Consume time is zero, set speed to 0"; @@ -292,10 +312,10 @@ void ProgressPage::calSpeedAndRemainingTime(double &dSpeed, qint64 &qRemainingTi } else { if (PT_Convert == m_eType) { qDebug() << "Calculate speed for convert type"; - dSpeed = 2 * (m_qTotalSize / 1024.0 * m_iPerent / 100) / m_qConsumeTime * 1000; + dSpeed = 2 * (m_qTotalSize / 1024.0 * p / 100) / m_qConsumeTime * 1000; } else { qDebug() << "Calculate speed for other types"; - dSpeed = (m_qTotalSize / 1024.0 * m_iPerent / 100) / m_qConsumeTime * 1000; + dSpeed = (m_qTotalSize / 1024.0 * p / 100) / m_qConsumeTime * 1000; } } @@ -303,10 +323,10 @@ void ProgressPage::calSpeedAndRemainingTime(double &dSpeed, qint64 &qRemainingTi double sizeLeft = 0; if (PT_Convert == m_eType) { qDebug() << "Calculate remaining size for convert type"; - sizeLeft = (m_qTotalSize * 2 / 1024.0) * (100 - m_iPerent) / 100; //剩余大小 + sizeLeft = (m_qTotalSize * 2 / 1024.0) * (100 - p) / 100; //剩余大小 } else { qDebug() << "Calculate remaining size for other types"; - sizeLeft = (m_qTotalSize / 1024.0) * (100 - m_iPerent) / 100; //剩余大小 + sizeLeft = (m_qTotalSize / 1024.0) * (100 - p) / 100; //剩余大小 } if (dSpeed != 0.0) { diff --git a/src/source/page/progresspage.h b/src/source/page/progresspage.h index 595769fe7..3fd65e276 100644 --- a/src/source/page/progresspage.h +++ b/src/source/page/progresspage.h @@ -145,6 +145,8 @@ private Q_SLOTS: Progress_Type m_eType = PT_None; // 进度类型 qint64 m_qTotalSize = 0; // 文件大小kB int m_iPerent = 0; // 进度值 + /** 最近一次收到的进度 0~100;-1 表示尚未收到有效进度(用于允许首包从 0% 开始) */ + double m_dProgressPercent = -1.0; QElapsedTimer m_timer; //计时器 qint64 m_qConsumeTime = 0; //消耗时间