diff --git a/3rdparty/CMakeLists.txt b/3rdparty/CMakeLists.txt index 42150e0ab..265a7255b 100644 --- a/3rdparty/CMakeLists.txt +++ b/3rdparty/CMakeLists.txt @@ -16,6 +16,15 @@ add_subdirectory(libzipplugin) add_subdirectory(libminizipplugin) +# clipzipplugin(仅 x86/ARM 环境启用,依赖 src/pzip) +# pzip 高性能并行压缩插件,通过调用 pzip CLI 实现多线程并行压缩 +if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm|x86_64|amd64") + message(STATUS "x86/ARM 环境,启用 pzip 高性能压缩插件") + add_subdirectory(clipzipplugin) +else() + message(STATUS "非 x86/ARM 环境,使用默认 libzip 插件") +endif() + ##add_subdirectory(cliunarchiverplugin) #不可用 ##add_subdirectory(clizipplugin) #不可用 diff --git a/3rdparty/clipzipplugin/CMakeLists.txt b/3rdparty/clipzipplugin/CMakeLists.txt new file mode 100644 index 000000000..933088994 --- /dev/null +++ b/3rdparty/clipzipplugin/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.9.5) + +set(LIB_NAME clipzipplugin) +project(${LIB_NAME}) + +find_package(PkgConfig REQUIRED) +find_package(Qt5 REQUIRED COMPONENTS Widgets) +find_package(KF5Codecs REQUIRED) + +include(FindPkgConfig) + +include_directories(${PROJECT_SOURCE_DIR}) +include_directories(${HFILES_OUTPUT_PATH}) + +link_directories(${LIBRARY_OUTPUT_PATH}) + +file(GLOB_RECURSE c_files RELATIVE ${PROJECT_SOURCE_DIR} *.cpp) +file(GLOB_RECURSE h_files RELATIVE ${PROJECT_SOURCE_DIR} *.h) +file(GLOB_RECURSE json_files RELATIVE ${PROJECT_SOURCE_DIR} *.json) + +add_library(${LIB_NAME} SHARED ${c_files} ${json_files} ${h_files}) + +target_link_libraries(${LIB_NAME} + Qt5::Widgets + KF5::Codecs + compressor-interface +) diff --git a/3rdparty/clipzipplugin/clipzipplugin.cpp b/3rdparty/clipzipplugin/clipzipplugin.cpp new file mode 100644 index 000000000..06e035d6a --- /dev/null +++ b/3rdparty/clipzipplugin/clipzipplugin.cpp @@ -0,0 +1,603 @@ +// 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 "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"); + +CliPzipPluginFactory::CliPzipPluginFactory() +{ + registerPlugin(); +} + +CliPzipPluginFactory::~CliPzipPluginFactory() +{ +} + +CliPzipPlugin::CliPzipPlugin(QObject *parent, const QVariantList &args) + : ReadWriteArchiveInterface(parent, args) +{ + setWaitForFinishedSignal(true); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + if (QMetaType::type("QProcess::ExitStatus") == 0) { +#else + if (!QMetaType::fromName("QProcess::ExitStatus").isValid()) { +#endif + qRegisterMetaType("QProcess::ExitStatus"); + } + + m_ePlugintype = PT_Libzip; // 复用 Libzip 类型,因为都是 ZIP 格式 + m_timer = new QTimer(this); + connect(m_timer, &QTimer::timeout, this, [=]() { + 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(); + if (m_timer) { + m_timer->stop(); + } +} + +QString CliPzipPlugin::getPzipPath() const +{ + // 优先使用安装路径 + if (QFileInfo::exists(PZIP_INSTALL_PATH)) { + return PZIP_INSTALL_PATH; + } + + // 开发环境:尝试从应用程序目录查找 + QString appDir = QCoreApplication::applicationDirPath(); + QString devPath = appDir + "/../src/pzip/pzip"; + if (QFileInfo::exists(devPath)) { + return devPath; + } + + // 尝试构建目录 + devPath = appDir + "/pzip"; + if (QFileInfo::exists(devPath)) { + return devPath; + } + + // 最后尝试 PATH 中的 pzip + return QStandardPaths::findExecutable("pzip"); +} + +PluginFinishType CliPzipPlugin::list() +{ + // pzip 目前不支持列表功能,返回错误让其他插件处理 + // 或者可以用 unzip -l 来实现 + return PFT_Error; +} + +PluginFinishType CliPzipPlugin::testArchive() +{ + // pzip 仅支持压缩,不支持测试功能,返回错误让其他插件处理 + return PFT_Error; +} + +PluginFinishType CliPzipPlugin::extractFiles(const QList &files, const ExtractionOptions &options) +{ + Q_UNUSED(files); + Q_UNUSED(options); + + // pzip 仅支持压缩,不构建 punzip 解压工具(避免 symlink race 安全漏洞,PMS BUG-382553) + // 解压操作由其他插件(libarchive/cli7z 等)处理 + m_eErrorType = ET_PluginError; + return PFT_Error; +} + +PluginFinishType CliPzipPlugin::addFiles(const QList &files, const CompressOptions &options) +{ + 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()) { + qWarning() << "pzip not found!"; + m_eErrorType = ET_PluginError; + return PFT_Error; + } + + m_process = new KPtyProcess; + m_process->setPtyChannels(KPtyProcess::StdinChannel); + m_process->setOutputChannelMode(KProcess::MergedChannels); + 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) { + level = 1; + } + arguments << "-l" << QString::number(level); + + // 线程数:只有大于1时才指定,否则让 pzip 自动使用全部 CPU 核心 + if (options.iCPUTheadNum > 1) { + arguments << "-c" << QString::number(options.iCPUTheadNum); + } + + // 输出文件 + arguments << outArchiveName; + + // 添加所有源文件/目录 + for (const FileEntry &file : files) { + QString filePath = file.strFullPath; + // 移除末尾的 '/' + if (filePath.endsWith('/')) { + filePath.chop(1); + } + arguments << filePath; + } + + qInfo() << "Running pzip:" << pzipPath << arguments; + + m_process->setProgram(pzipPath, arguments); + + connect(m_process, &QProcess::readyReadStandardOutput, this, [=] { + readStdout(); + }); + + connect(m_process, &QProcess::readyReadStandardError, this, [=] { + QByteArray errorOutput = m_process->readAllStandardError(); + qDebug() << "pzip stderr:" << QString::fromLocal8Bit(errorOutput); + }); + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + connect(m_process, QOverload::of(&QProcess::finished), + this, &CliPzipPlugin::processFinished); +#else + connect(m_process, &QProcess::finished, this, &CliPzipPlugin::processFinished); +#endif + + m_process->start(); + + if (m_process->waitForStarted()) { + m_childProcessId.clear(); + 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; +} + +PluginFinishType CliPzipPlugin::moveFiles(const QList &files, const CompressOptions &options) +{ + Q_UNUSED(files); + Q_UNUSED(options); + return PFT_Error; // 不支持 +} + +PluginFinishType CliPzipPlugin::copyFiles(const QList &files, const CompressOptions &options) +{ + Q_UNUSED(files); + Q_UNUSED(options); + return PFT_Error; // 不支持 +} + +PluginFinishType CliPzipPlugin::deleteFiles(const QList &files) +{ + Q_UNUSED(files); + return PFT_Error; // 不支持 +} + +PluginFinishType CliPzipPlugin::renameFiles(const QList &files) +{ + Q_UNUSED(files); + return PFT_Error; // 不支持 +} + +PluginFinishType CliPzipPlugin::addComment(const QString &comment) +{ + Q_UNUSED(comment); + return PFT_Error; // 不支持 +} + +PluginFinishType CliPzipPlugin::updateArchiveData(const UpdateOptions &options) +{ + Q_UNUSED(options); + return PFT_Error; // 不支持 +} + +void CliPzipPlugin::pauseOperation() +{ + if (!m_childProcessId.empty()) { + for (int i = m_childProcessId.size() - 1; i >= 0; i--) { + if (m_childProcessId[i] > 0) { + kill(static_cast<__pid_t>(m_childProcessId[i]), SIGSTOP); + } + } + } + + if (m_processId > 0) { + kill(static_cast<__pid_t>(m_processId), SIGSTOP); + } +} + +void CliPzipPlugin::continueOperation() +{ + if (!m_childProcessId.empty()) { + for (int i = m_childProcessId.size() - 1; i >= 0; i--) { + if (m_childProcessId[i] > 0) { + kill(static_cast<__pid_t>(m_childProcessId[i]), SIGCONT); + } + } + } + + if (m_processId > 0) { + kill(static_cast<__pid_t>(m_processId), SIGCONT); + } +} + +bool CliPzipPlugin::doKill() +{ + if (m_process) { + killProcess(false); + m_timer->stop(); + return true; + } + return false; +} + +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; + } + + if (line.contains(QLatin1String("error")) || line.contains(QLatin1String("Error"))) { + qWarning() << "pzip error:" << line; + // 不一定是致命错误,继续处理 + } + + emitProgressIfArchiveGrew(); + return true; +} + +void CliPzipPlugin::killProcess(bool emitFinished) +{ + Q_UNUSED(emitFinished); + + if (!m_process) { + return; + } + + if (!m_childProcessId.empty()) { + for (int i = m_childProcessId.size() - 1; i >= 0; i--) { + if (m_childProcessId[i] > 0) { + kill(static_cast<__pid_t>(m_childProcessId[i]), SIGKILL); + } + } + } + + qint64 processID = m_process->processId(); + if (processID > 0) { + kill(static_cast<__pid_t>(processID), SIGCONT); + kill(static_cast<__pid_t>(processID), SIGTERM); + } + + m_isProcessKilled = true; +} + +void CliPzipPlugin::deleteProcess() +{ + m_passwordFile.reset(); + if (m_process) { + readStdout(true); + m_process->blockSignals(true); + delete m_process; + m_process = nullptr; + } +} + +void CliPzipPlugin::getChildProcessId(qint64 processId, const QStringList &listKey, QVector &childprocessid) +{ + childprocessid.clear(); + + if (0 >= processId || 0 == listKey.count()) { + return; + } + + QString strProcessId = QString::number(processId); + QProcess p; + p.setProgram("pstree"); + p.setArguments(QStringList() << "-np" << strProcessId); + p.start(); + + if (p.waitForReadyRead()) { + QByteArray dd = p.readAllStandardOutput(); + QList lines = dd.split('\n'); + + if (lines.count() > 0 && lines[0].contains(strProcessId.toUtf8())) { + for (const QByteArray &line : lines) { + for (const QString &strKey : listKey) { + QString str = QString("-%1(").arg(strKey); + QByteArray ba = str.toUtf8(); + int iCount = line.count(ba); + int iIndex = 0; + for (int i = 0; i < iCount; ++i) { + int iStartIndex = line.indexOf(ba, iIndex); + int iEndIndex = line.indexOf(")", iStartIndex); + if (0 < iStartIndex && 0 < iEndIndex) { + childprocessid.append(line.mid(iStartIndex + str.length(), + iEndIndex - iStartIndex - str.length()).toInt()); + } + iIndex = iStartIndex + 1; + } + } + } + } + } +} + +void CliPzipPlugin::readStdout(bool handleAll) +{ + Q_UNUSED(handleAll); + + if (m_isProcessKilled) { + return; + } + + Q_ASSERT(m_process); + + if (!m_process->bytesAvailable()) { + return; + } + + QByteArray dd = m_process->readAllStandardOutput(); + m_stdOutData += dd; + + QList lines = m_stdOutData.split('\n'); + m_stdOutData = lines.takeLast(); + + for (const QByteArray &line : lines) { + if (!handleLine(QString::fromLocal8Bit(line))) { + killProcess(); + return; + } + } +} + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +void CliPzipPlugin::processFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + qInfo() << "pzip process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; +#else +void CliPzipPlugin::processFinished(int exitCode) +{ + QProcess::ExitStatus exitStatus = m_process ? m_process->exitStatus() : QProcess::NormalExit; + qInfo() << "pzip process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; +#endif + + deleteProcess(); + m_timer->stop(); + + PluginFinishType eFinishType; + + if (0 == exitCode && exitStatus == QProcess::NormalExit) { + 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; + } + + emit signalprogress(100); + emit signalFinished(eFinishType); +} + diff --git a/3rdparty/clipzipplugin/clipzipplugin.h b/3rdparty/clipzipplugin/clipzipplugin.h new file mode 100644 index 000000000..6ab09a374 --- /dev/null +++ b/3rdparty/clipzipplugin/clipzipplugin.h @@ -0,0 +1,122 @@ +// 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 + +#ifndef CLIPZIPPLUGIN_H +#define CLIPZIPPLUGIN_H + +#include "archiveinterface.h" +#include "kpluginfactory.h" +#include "kptyprocess.h" + +#include +#include +#include +#include +#include +#include // for QT_VERSION_CHECK + +class CliPzipPluginFactory : public KPluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.kde.KPluginFactory" FILE "kerfuffle_clipzip.json") + Q_INTERFACES(KPluginFactory) + +public: + explicit CliPzipPluginFactory(); + ~CliPzipPluginFactory(); +}; + +/** + * @brief CliPzipPlugin - 使用 pzip 进行高性能并行 ZIP 压缩的插件 + * + * 该插件调用内置的 pzip 命令行工具进行压缩,支持多线程并行压缩, + * 在 ARM 平台上性能提升显著。 + */ +class CliPzipPlugin : public ReadWriteArchiveInterface +{ + Q_OBJECT + +public: + explicit CliPzipPlugin(QObject *parent, const QVariantList &args); + ~CliPzipPlugin() override; + + // ReadOnlyArchiveInterface interface + PluginFinishType list() override; + PluginFinishType testArchive() override; + PluginFinishType extractFiles(const QList &files, const ExtractionOptions &options) override; + + // ReadWriteArchiveInterface interface + PluginFinishType addFiles(const QList &files, const CompressOptions &options) override; + PluginFinishType moveFiles(const QList &files, const CompressOptions &options) override; + PluginFinishType copyFiles(const QList &files, const CompressOptions &options) override; + PluginFinishType deleteFiles(const QList &files) override; + PluginFinishType renameFiles(const QList &files) override; + PluginFinishType addComment(const QString &comment) override; + PluginFinishType updateArchiveData(const UpdateOptions &options) override; + + void pauseOperation() override; + void continueOperation() override; + bool doKill() override; + +private: + /** + * @brief getPzipPath 获取 pzip 可执行文件路径 + */ + QString getPzipPath() const; + + /** + * @brief handleLine 处理命令行输出 + */ + bool handleLine(const QString &line); + + /** + * @brief killProcess 结束进程 + */ + void killProcess(bool emitFinished = true); + + /** + * @brief deleteProcess 删除进程 + */ + void deleteProcess(); + + /** + * @brief getChildProcessId 获取子进程 ID + */ + 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) + void processFinished(int exitCode, QProcess::ExitStatus exitStatus); +#else + void processFinished(int exitCode); +#endif + +private: + KPtyProcess *m_process = nullptr; + QByteArray m_stdOutData; + bool m_isProcessKilled = false; + qint64 m_processId = 0; + 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; + int m_extractedCount = 0; + int m_totalFilesCount = 0; +}; + +#endif // CLIPZIPPLUGIN_H + diff --git a/3rdparty/clipzipplugin/kerfuffle_clipzip.json b/3rdparty/clipzipplugin/kerfuffle_clipzip.json new file mode 100644 index 000000000..efe6279d0 --- /dev/null +++ b/3rdparty/clipzipplugin/kerfuffle_clipzip.json @@ -0,0 +1,30 @@ +{ + "KPlugin": { + "Description": "High-performance parallel ZIP compression using pzip", + "Description[zh_CN]": "使用 pzip 进行高性能并行 ZIP 压缩", + "Id": "kerfuffle_clipzip", + "MimeTypes": [ + "application/zip" + ], + "Name": "pzip plugin", + "Name[zh_CN]": "pzip 高性能压缩插件", + "ServiceTypes": [ + "Kerfuffle/Plugin" + ], + "Version": "1.0" + }, + "X-KDE-Priority": 200, + "X-KDE-Kerfuffle-ReadWrite": true, + "application/zip": { + "CompressionLevelDefault": 1, + "CompressionLevelMax": 9, + "CompressionLevelMin": 1, + "CompressionMethodDefault": "Deflate", + "CompressionMethods": { + "Deflate": "Deflate", + "Store": "Store" + }, + "SupportsTesting": false, + "SupportsWriteComment": false + } +} diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e288aa4b..30d2e958c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -117,3 +117,8 @@ install(TARGETS bz2plugin DESTINATION ${COMPRESSOR_PLUGIN_PATH}) install(TARGETS gzplugin DESTINATION ${COMPRESSOR_PLUGIN_PATH}) install(TARGETS xzplugin DESTINATION ${COMPRESSOR_PLUGIN_PATH}) +# pzip 高性能压缩插件(仅 x86/ARM 环境安装) +if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm|x86_64|amd64") + install(TARGETS clipzipplugin DESTINATION ${COMPRESSOR_PLUGIN_PATH}) +endif() + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 630830ed3..d8da05965 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -116,3 +116,9 @@ install(DIRECTORY assets/deepin-compressor DESTINATION ${CMAKE_INSTALL_DATADIR}/ # 支持压缩的格式 #install(FILES conf/compressor-singlecompress.conf DESTINATION ${CMAKE_INSTALL_DATADIR}/deepin-compressor/compressedformat/) + +# pzip 高性能并行压缩工具(仅 x86/ARM 环境启用) +# pzip 仅用于压缩(write-only),不构建 punzip 解压工具(安全原因:symlink race) +if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm|x86_64|amd64") + add_subdirectory(pzip) +endif() diff --git a/src/pzip/CMakeLists.txt b/src/pzip/CMakeLists.txt new file mode 100644 index 000000000..741ac3827 --- /dev/null +++ b/src/pzip/CMakeLists.txt @@ -0,0 +1,60 @@ +# pzip - 高性能并行 ZIP 压缩工具 +# 安全说明:仅构建 pzip(压缩),不构建 punzip(解压)。 +# punzip 的解压路径存在符号链接竞态(symlink race)安全漏洞(PMS BUG-382553), +# 已从 master 分支构建目标中移除,本移植保持一致——pzip 为 write-only 工具。 +cmake_minimum_required(VERSION 3.9.5) + +set(PZIP_NAME pzip-tool) +project(${PZIP_NAME}) + +set(CMAKE_AUTOMOC OFF) +set(CMAKE_AUTORCC OFF) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +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}) + +set(PZIP_SOURCES + src/archiver.cpp + src/file_task.cpp + src/zip_writer.cpp + src/zip_reader.cpp + 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 + OpenSSL::Crypto +) + +target_compile_features(pzip_core_lib PUBLIC cxx_std_17) + +if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm") + target_compile_options(pzip_core_lib PRIVATE -O3 -funroll-loops -march=armv8-a+crc) +else() + target_compile_options(pzip_core_lib PRIVATE -O3 -mtune=generic -funroll-loops) +endif() + +add_executable(pzip-bin cmd/pzip_main.cpp) +target_link_libraries(pzip-bin pzip_core_lib) +set_target_properties(pzip-bin PROPERTIES OUTPUT_NAME "pzip") + +install(TARGETS pzip-bin + RUNTIME DESTINATION lib/deepin-compressor +) + diff --git a/src/pzip/cmd/punzip_main.cpp b/src/pzip/cmd/punzip_main.cpp new file mode 100644 index 000000000..1230d1ace --- /dev/null +++ b/src/pzip/cmd/punzip_main.cpp @@ -0,0 +1,196 @@ +// 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 + +/** + * @file punzip_main.cpp + * @brief punzip 命令行工具 - 并行 ZIP 解压 + * + * 用法: punzip [选项] [-d <输出目录>] + */ + +#include "pzip/pzip.h" +#include +#include +#include +#include + +void printUsage(const char* progName) { + std::cout << "punzip - Parallel ZIP Extractor v" << pzip::version() << "\n\n" + << "用法: " << progName << " [选项] \n\n" + << "选项:\n" + << " -d, --directory 解压到指定目录(默认: 当前目录)\n" + << " -c, --concurrency 设置并发线程数(默认: CPU 核心数)\n" + << " -o, --overwrite 覆盖已存在的文件\n" + << " -n, --no-overwrite 不覆盖已存在的文件\n" + << " -v, --verbose 显示详细信息\n" + << " -q, --quiet 静默模式\n" + << " -l, --list 仅列出文件,不解压\n" + << " -h, --help 显示帮助信息\n" + << "\n" + << "示例:\n" + << " " << progName << " archive.zip\n" + << " " << progName << " -d /tmp/output archive.zip\n" + << " " << progName << " -c 4 -v archive.zip\n"; +} + +void listArchive(const std::string& archivePath) { + pzip::ZipReader reader(archivePath); + pzip::Error err = reader.open(); + + if (err) { + std::cerr << "错误: " << err.message << "\n"; + return; + } + + const auto& entries = reader.entries(); + + std::cout << " 长度 日期 时间 名称\n"; + std::cout << "-------- ---------- ----- ----\n"; + + uint64_t totalSize = 0; + for (const auto& entry : entries) { + // 格式化时间 + time_t modTime = 0; // TODO: 从 header 获取 + struct tm* tm = localtime(&modTime); + + char dateStr[32] = "0000-00-00 00:00"; + if (tm) { + snprintf(dateStr, sizeof(dateStr), "%04d-%02d-%02d %02d:%02d", + tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, + tm->tm_hour, tm->tm_min); + } + + std::cout << std::setw(8) << entry.header.uncompressedSize << " " + << dateStr << " " + << entry.header.name << "\n"; + + totalSize += entry.header.uncompressedSize; + } + + std::cout << "-------- -------\n"; + std::cout << std::setw(8) << totalSize << " " + << entries.size() << " 个文件\n"; +} + +int main(int argc, char* argv[]) { + // 默认选项 + pzip::ExtractorOptions options; + std::string outputDir = "."; + bool verbose = false; + bool quiet = false; + bool listOnly = false; + + // 命令行选项定义 + static struct option longOptions[] = { + {"directory", required_argument, nullptr, 'd'}, + {"concurrency", required_argument, nullptr, 'c'}, + {"overwrite", no_argument, nullptr, 'o'}, + {"no-overwrite", no_argument, nullptr, 'n'}, + {"verbose", no_argument, nullptr, 'v'}, + {"quiet", no_argument, nullptr, 'q'}, + {"list", no_argument, nullptr, 'l'}, + {"help", no_argument, nullptr, 'h'}, + {nullptr, 0, nullptr, 0} + }; + + // 解析命令行选项 + int opt; + while ((opt = getopt_long(argc, argv, "d:c:onvqlh", longOptions, nullptr)) != -1) { + switch (opt) { + case 'd': + outputDir = optarg; + break; + case 'c': + options.concurrency = std::stoul(optarg); + break; + case 'o': + options.overwrite = true; + break; + case 'n': + options.overwrite = false; + break; + case 'v': + verbose = true; + break; + case 'q': + quiet = true; + break; + case 'l': + listOnly = true; + break; + case 'h': + printUsage(argv[0]); + return 0; + default: + printUsage(argv[0]); + return 1; + } + } + + // 检查参数数量 + if (argc - optind < 1) { + std::cerr << "错误: 需要指定 ZIP 文件\n"; + printUsage(argv[0]); + return 1; + } + + std::string archivePath = argv[optind]; + + // 验证文件存在 + if (!pzip::fs::exists(archivePath)) { + std::cerr << "错误: 文件不存在: " << archivePath << "\n"; + return 1; + } + + // 仅列出模式 + if (listOnly) { + listArchive(archivePath); + return 0; + } + + // 设置进度回调 + if (!quiet) { + options.progress = [verbose](size_t current, size_t total) { + if (verbose) { + std::cout << "\r解压进度: " << current << "/" << total + << " (" << (total > 0 ? current * 100 / total : 0) << "%)" + << std::flush; + } + }; + } + + // 开始计时 + auto startTime = std::chrono::high_resolution_clock::now(); + + if (!quiet) { + std::cout << "解压文件: " << archivePath << "\n"; + std::cout << "输出目录: " << outputDir << "\n"; + if (verbose) { + std::cout << "并发线程数: " << (options.concurrency > 0 ? options.concurrency : std::thread::hardware_concurrency()) << "\n"; + } + } + + // 执行解压 + pzip::Error err = pzip::decompress(archivePath, outputDir, options); + + // 结束计时 + auto endTime = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(endTime - startTime); + double seconds = duration.count() / 1000.0; + + if (err) { + std::cerr << "\n错误: " << err.message << "\n"; + return 1; + } + + if (!quiet) { + std::cout << "\n完成!\n"; + std::cout << "耗时: " << pzip::utils::formatTime(seconds) << "\n"; + } + + return 0; +} + + diff --git a/src/pzip/cmd/pzip_main.cpp b/src/pzip/cmd/pzip_main.cpp new file mode 100644 index 000000000..d2dcc8d57 --- /dev/null +++ b/src/pzip/cmd/pzip_main.cpp @@ -0,0 +1,334 @@ +// 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 + +/** + * @file pzip_main.cpp + * @brief pzip 命令行工具 - 并行 ZIP 压缩 + * + * 用法: pzip [选项] <文件或目录...> + */ + +#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 <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" + << "\n" + << "示例:\n" + << " " << progName << " archive.zip file1.txt file2.txt\n" + << " " << progName << " archive.zip directory/\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[]) { + // 默认选项 + 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:p:P:e:vqh", longOptions, nullptr)) != -1) { + switch (opt) { + case 'c': + options.concurrency = std::stoul(optarg); + break; + case 'l': + options.compressionLevel = std::stoi(optarg); + if (options.compressionLevel < 0 || options.compressionLevel > 9) { + std::cerr << "错误: 压缩级别必须在 0-9 之间\n"; + 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; + case 'q': + quiet = true; + break; + case 'h': + printUsage(argv[0]); + return 0; + default: + printUsage(argv[0]); + 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) { + std::cerr << "错误: 需要指定输出文件和输入文件/目录\n"; + printUsage(argv[0]); + return 1; + } + + // 获取输出文件和输入文件列表 + std::string archivePath = argv[optind]; + std::vector inputPaths; + for (int i = optind + 1; i < argc; ++i) { + inputPaths.emplace_back(argv[i]); + } + + // 验证输入文件存在 + for (const auto& path : inputPaths) { + if (!pzip::fs::exists(path)) { + std::cerr << "错误: 文件不存在: " << path << "\n"; + return 1; + } + } + + // 设置进度回调 + if (!quiet) { + options.progress = [verbose](size_t current, size_t total) { + if (verbose) { + std::cout << "\r压缩进度: " << current << "/" << total + << " (" << (total > 0 ? current * 100 / total : 0) << "%)" + << std::flush; + } + }; + } + + // 开始计时 + auto startTime = std::chrono::high_resolution_clock::now(); + + if (!quiet) { + std::cout << "创建压缩文件: " << archivePath << "\n"; + 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(); + auto duration = std::chrono::duration_cast(endTime - startTime); + double seconds = duration.count() / 1000.0; + + if (err) { + std::cerr << "\n错误: " << err.message << "\n"; + return 1; + } + + if (!quiet) { + std::cout << "\n"; + + // 显示统计信息 + std::error_code ec; + auto archiveSize = pzip::fs::file_size(archivePath, ec); + + uint64_t totalInputSize = 0; + for (const auto& path : inputPaths) { + if (pzip::fs::is_directory(path, ec)) { + for (const auto& entry : pzip::fs::recursive_directory_iterator(path, ec)) { + if (entry.is_regular_file()) { + totalInputSize += entry.file_size(); + } + } + } else { + totalInputSize += pzip::fs::file_size(path, ec); + } + } + + std::cout << "完成!\n"; + std::cout << "原始大小: " << pzip::utils::formatSize(totalInputSize) << "\n"; + std::cout << "压缩大小: " << pzip::utils::formatSize(archiveSize) << "\n"; + + if (totalInputSize > 0) { + double ratio = 100.0 * (1.0 - static_cast(archiveSize) / totalInputSize); + std::cout << "压缩率: " << std::fixed << std::setprecision(1) << ratio << "%\n"; + } + + std::cout << "耗时: " << pzip::utils::formatTime(seconds) << "\n"; + + if (seconds > 0 && totalInputSize > 0) { + double speed = totalInputSize / seconds / (1024 * 1024); + std::cout << "速度: " << std::fixed << std::setprecision(1) << speed << " MB/s\n"; + } + } + + return 0; +} + + diff --git a/src/pzip/include/pzip/archiver.h b/src/pzip/include/pzip/archiver.h new file mode 100644 index 000000000..3bc1e0e2f --- /dev/null +++ b/src/pzip/include/pzip/archiver.h @@ -0,0 +1,134 @@ +// 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 "common.h" +#include "worker_pool.h" +#include "file_task.h" +#include "zip_writer.h" +#include + +namespace pzip { + +/** + * @brief 压缩器选项 + */ +struct ArchiverOptions { + size_t concurrency = 0; // 并发数(0 = 自动使用全部 CPU 核心) + 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; + } +}; + +/** + * @brief 并行 ZIP 压缩器 + * + * 核心实现类,对应 Go 版 pzip 的 archiver + * + * 架构: + * - fileProcessPool: 多线程并行压缩文件 + * - fileWriterPool: 单线程顺序写入 ZIP(因为 ZIP 格式要求顺序写入) + */ +class Archiver { +public: + /** + * @brief 构造函数 + * @param archive ZIP 文件路径 + * @param options 选项 + */ + explicit Archiver(const fs::path& archive, const ArchiverOptions& options = {}); + ~Archiver(); + + // 禁止拷贝 + Archiver(const Archiver&) = delete; + Archiver& operator=(const Archiver&) = delete; + + /** + * @brief 压缩文件或目录 + * @param paths 要压缩的文件/目录列表 + * @return 错误信息 + */ + Error archive(const std::vector& paths); + + /** + * @brief 取消压缩 + */ + void cancel(); + + /** + * @brief 关闭压缩器 + * @return 错误信息 + */ + Error close(); + + /** + * @brief 设置进度回调 + */ + void setProgressCallback(ProgressCallback callback); + + /** + * @brief 获取已处理的文件数 + */ + size_t processedFiles() const { return processedFiles_; } + + /** + * @brief 获取总文件数 + */ + size_t totalFiles() const { return totalFiles_; } + +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); + + // 设置相对路径名 + void setRelativeName(FileTask* task, const fs::path& relativeTo); + + fs::path archivePath_; + fs::path absoluteArchivePath_; + ArchiverOptions options_; + + std::unique_ptr writer_; + std::unique_ptr> fileProcessPool_; + std::unique_ptr> fileWriterPool_; + + fs::path chroot_; // 当前根目录 + + std::atomic processedFiles_{0}; + std::atomic totalFiles_{0}; + std::atomic cancelled_{false}; + +}; + +} // namespace pzip + + diff --git a/src/pzip/include/pzip/common.h b/src/pzip/include/pzip/common.h new file mode 100644 index 000000000..1ed7f8b6b --- /dev/null +++ b/src/pzip/include/pzip/common.h @@ -0,0 +1,135 @@ +// 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 +#include +#include +#include +#include +#include +#include +#include + +namespace pzip { + +// 常量定义 +constexpr size_t DEFAULT_BUFFER_SIZE = 2 * 1024 * 1024; // 2MB 缓冲区 +constexpr size_t READ_BUFFER_SIZE = 32 * 1024; // 32KB 读取缓冲 +constexpr int DEFAULT_COMPRESSION_LEVEL = -1; // zlib 默认压缩级别 +constexpr uint16_t ZIP_VERSION_20 = 20; + +// ZIP 压缩方法 +constexpr uint16_t ZIP_METHOD_STORE = 0; +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 { + OK = 0, + FILE_NOT_FOUND, + FILE_OPEN_ERROR, + FILE_READ_ERROR, + FILE_WRITE_ERROR, + COMPRESSION_ERROR, + DECOMPRESSION_ERROR, + INVALID_ARCHIVE, + MEMORY_ERROR, + CANCELLED, + 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; + std::string message; + + Error() : code(ErrorCode::OK) {} + Error(ErrorCode c, const std::string& msg = "") : code(c), message(msg) {} + + explicit operator bool() const { return code != ErrorCode::OK; } +}; + +// 进度回调 +using ProgressCallback = std::function; + +// 前向声明 +class FileTask; +template class WorkerPool; +class Archiver; +class Extractor; +class ZipWriter; +class ZipReader; +struct ExtractTask; + +} // namespace pzip + 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/extractor.h b/src/pzip/include/pzip/extractor.h new file mode 100644 index 000000000..4837a6b10 --- /dev/null +++ b/src/pzip/include/pzip/extractor.h @@ -0,0 +1,116 @@ +// 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 "common.h" +#include "worker_pool.h" +#include "zip_reader.h" + +namespace pzip { + +/** + * @brief 解压器选项 + */ +struct ExtractorOptions { + size_t concurrency = 0; // 并发数(0 = CPU 核心数) + bool preservePermissions = true; // 保留文件权限 + bool overwrite = true; // 是否覆盖已存在的文件 + ProgressCallback progress; // 进度回调 +}; + +/** + * @brief 解压任务 + */ +struct ExtractTask { + const ZipEntry* entry; + fs::path relativePath; + bool success = false; +}; + +/** + * @brief 并行 ZIP 解压器 + * + * 对应 Go 版 pzip 的 extractor + */ +class Extractor { +public: + /** + * @brief 构造函数 + * @param outputDir 解压目标目录 + * @param options 选项 + */ + explicit Extractor(const fs::path& outputDir, const ExtractorOptions& options = {}); + ~Extractor(); + + // 禁止拷贝 + Extractor(const Extractor&) = delete; + Extractor& operator=(const Extractor&) = delete; + + /** + * @brief 解压 ZIP 文件 + * @param archivePath ZIP 文件路径 + * @return 错误信息 + */ + Error extract(const fs::path& archivePath); + + /** + * @brief 取消解压 + */ + void cancel(); + + /** + * @brief 关闭解压器 + * @return 错误信息 + */ + Error close(); + + /** + * @brief 设置进度回调 + */ + void setProgressCallback(ProgressCallback callback); + + /** + * @brief 获取已处理的文件数 + */ + size_t processedFiles() const { return processedFiles_; } + + /** + * @brief 获取总文件数 + */ + size_t totalFiles() const { return totalFiles_; } + +private: + // 解压单个文件 + Error extractFile(ExtractTask* task); + + // 写入目录 + Error writeDirectory(const fs::path& relativePath, const ZipEntry& entry); + + // 写入文件 + Error writeFile(const fs::path& relativePath, const ZipEntry& entry); + + // 设置文件权限 + Error setPermissions(int fileDescriptor, const ZipEntry& entry); + + // 校验并规范化 ZIP 条目路径 + fs::path outputPath(const std::string& name) const; + + fs::path outputDir_; + fs::path absoluteOutputDir_; + fs::path canonicalOutputDir_; + ExtractorOptions options_; + + std::unique_ptr reader_; + std::unique_ptr> fileWorkerPool_; + + std::atomic processedFiles_{0}; + std::atomic totalFiles_{0}; + std::atomic cancelled_{false}; + int outputRootFd_ = -1; +}; + +} // namespace pzip + diff --git a/src/pzip/include/pzip/fast_deflate.h b/src/pzip/include/pzip/fast_deflate.h new file mode 100644 index 000000000..d5f4b5737 --- /dev/null +++ b/src/pzip/include/pzip/fast_deflate.h @@ -0,0 +1,521 @@ +// 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 + +/** + * @file fast_deflate.hpp + * @brief 高性能 DEFLATE 压缩器 + * + * 参考 klauspost/compress (Go) 官方实现 + * - level1.go: 快速单哈希表匹配算法 + * - level4.go: 双哈希表匹配算法 + * - huffman_bit_writer.go: 高效霍夫曼编码输出 + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__GNUC__) || defined(__clang__) +#define PZIP_FORCE_INLINE __attribute__((always_inline)) inline +#define PZIP_HOT __attribute__((hot)) +#define PZIP_LIKELY(x) __builtin_expect(!!(x), 1) +#define PZIP_UNLIKELY(x) __builtin_expect(!!(x), 0) +#else +#define PZIP_FORCE_INLINE inline +#define PZIP_HOT +#define PZIP_LIKELY(x) (x) +#define PZIP_UNLIKELY(x) (x) +#endif + +namespace pzip { + +// 压缩级别 +enum class CompressionLevel { + NoCompression = 0, + BestSpeed = 1, + Level2 = 2, + Level3 = 3, + Level4 = 4, + Level5 = 5, + DefaultCompression = 6, + Level7 = 7, + Level8 = 8, + BestCompression = 9 +}; + +// ============================================================================ +// 常量定义 - 来自 klauspost/compress/flate +// ============================================================================ + +constexpr int TABLE_BITS = 15; +constexpr size_t TABLE_SIZE = 1 << TABLE_BITS; +constexpr int TABLE_SHIFT = 32 - TABLE_BITS; + +constexpr size_t BASE_MATCH_LENGTH = 3; +constexpr size_t MAX_MATCH_LENGTH = 258; +constexpr int32_t MAX_MATCH_OFFSET = 1 << 15; + +constexpr size_t MAX_STORE_BLOCK_SIZE = 65535; +constexpr size_t ALLOC_HISTORY = MAX_STORE_BLOCK_SIZE * 5; + +constexpr uint32_t PRIME_4_BYTES = 2654435761U; +constexpr uint64_t PRIME_5_BYTES = 889523592379ULL; +constexpr uint64_t PRIME_7_BYTES = 58295818150454627ULL; + +constexpr int OFFSET_CODE_COUNT = 30; +constexpr int END_BLOCK_MARKER = 256; +constexpr int LENGTH_CODES_START = 257; +constexpr int LITERAL_COUNT = 286; +constexpr int BUFFER_FLUSH_SIZE = 246; + +constexpr int LENGTH_SHIFT = 22; +constexpr uint32_t OFFSET_MASK = (1 << LENGTH_SHIFT) - 1; +constexpr uint32_t MATCH_TYPE = 1U << 30; + +// ============================================================================ +// 静态查找表 - 内联以提高性能 +// ============================================================================ + +// lengthCodes1 表 - 从 klauspost/compress +alignas(64) constexpr uint8_t lengthCodes1[256] = { + 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 10, 10, 11, 11, 12, 12, + 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, + 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, + 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, + 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 29, +}; + +// offsetCodes 表 +alignas(64) constexpr uint32_t offsetCodes[256] = { + 0, 1, 2, 3, 4, 4, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, + 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, +}; + +// offsetCodes14 表 (用于 offset >= 256) +alignas(64) constexpr uint32_t offsetCodes14[256] = { + 14, 15, 16, 17, 18, 18, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, + 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, + 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, + 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, + 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, + 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, +}; + +// ============================================================================ +// 内联辅助函数 +// ============================================================================ + +PZIP_FORCE_INLINE uint32_t load32(const uint8_t* b, size_t i) { + uint32_t v; + std::memcpy(&v, b + i, 4); + return v; +} + +PZIP_FORCE_INLINE uint64_t load64(const uint8_t* b, size_t i) { + uint64_t v; + std::memcpy(&v, b + i, 8); + return v; +} + +PZIP_FORCE_INLINE void store64(uint8_t* b, size_t i, uint64_t v) { + std::memcpy(b + i, &v, 8); +} + +// 4字节哈希 +PZIP_FORCE_INLINE uint32_t hash4(uint32_t u) { + return (u * PRIME_4_BYTES) >> TABLE_SHIFT; +} + +// 5字节哈希 (level1 使用) +PZIP_FORCE_INLINE uint32_t hash5(uint64_t u) { + return static_cast(((u << (64 - 40)) * PRIME_5_BYTES) >> (64 - TABLE_BITS)); +} + +// 7字节哈希 (level4 使用) +PZIP_FORCE_INLINE uint32_t hash7(uint64_t u) { + return static_cast(((u << (64 - 56)) * PRIME_7_BYTES) >> (64 - TABLE_BITS)); +} + +// offsetCode - 内联以避免函数调用 +PZIP_FORCE_INLINE uint32_t offsetCode(uint32_t off) { + if (off < 256) return offsetCodes[off]; + return offsetCodes14[(off >> 7) & 0xFF]; +} + +// ============================================================================ +// Token 类型 +// ============================================================================ + +using Token = uint32_t; + +PZIP_FORCE_INLINE Token makeLiteralToken(uint8_t lit) { + return static_cast(lit); +} + +PZIP_FORCE_INLINE Token makeMatchToken(uint32_t xlength, uint32_t xoffset) { + return MATCH_TYPE | (xlength << LENGTH_SHIFT) | xoffset; +} + +// ============================================================================ +// Tokens 容器 - 使用固定大小数组,所有方法内联 +// ============================================================================ + +struct Tokens { + std::array extraHist; + std::array offHist; + std::array litHist; + uint16_t n = 0; + std::array tokens; + + PZIP_FORCE_INLINE void reset() { + n = 0; + litHist.fill(0); + extraHist.fill(0); + offHist.fill(0); + } + + PZIP_FORCE_INLINE void addLiteral(uint8_t lit) { + tokens[n] = makeLiteralToken(lit); + litHist[lit]++; + n++; + } + + // addMatch - 短匹配 (<=258) + PZIP_FORCE_INLINE void addMatch(uint32_t xlength, uint32_t xoffset) { + uint32_t oCode = offsetCode(xoffset); + xoffset |= oCode << 16; + extraHist[lengthCodes1[static_cast(xlength)]]++; + offHist[oCode & 31]++; + tokens[n++] = MATCH_TYPE | (xlength << LENGTH_SHIFT) | xoffset; + } + + // addMatchLong - 长匹配,完全内联 + PZIP_FORCE_INLINE void addMatchLong(int32_t xlength, uint32_t xoffset) { + uint32_t oc = offsetCode(xoffset); + xoffset |= oc << 16; + + while (xlength > 0) { + int32_t xl = xlength; + if (xl > 258) { + xl = (xl > 258 + 3) ? 258 : (258 - 3); + } + xlength -= xl; + xl -= 3; // BASE_MATCH_LENGTH + + extraHist[lengthCodes1[static_cast(xl)]]++; + offHist[oc & 31]++; + tokens[n++] = MATCH_TYPE | (static_cast(xl) << LENGTH_SHIFT) | xoffset; + } + } + + PZIP_FORCE_INLINE void addEOB() { + tokens[n++] = END_BLOCK_MARKER; + } +}; + +// ============================================================================ +// hcode - 霍夫曼编码 +// ============================================================================ + +struct HCode { + uint32_t value = 0; + + HCode() = default; + HCode(uint16_t code, uint8_t len) : value(len | (static_cast(code) << 8)) {} + + PZIP_FORCE_INLINE uint8_t len() const { return static_cast(value); } + PZIP_FORCE_INLINE uint64_t code64() const { return static_cast(value >> 8); } + PZIP_FORCE_INLINE bool zero() const { return value == 0; } + + void set(uint16_t code, uint8_t len) { + value = len | (static_cast(code) << 8); + } +}; + +// ============================================================================ +// HuffmanEncoder +// ============================================================================ + +class HuffmanEncoder { +public: + std::vector codes; + std::array bitCount; + + explicit HuffmanEncoder(int size); + void generate(const uint16_t* freq, int numSymbols, int maxBits); + int bitLength(const uint16_t* freq, int numSymbols) const; + +private: + struct LiteralNode { + uint16_t literal; + uint16_t freq; + }; + std::vector freqCache; + void bitCounts(std::vector& list, int maxBits); + void assignEncodingAndSize(const int32_t* bitCount, std::vector& list); +}; + +// ============================================================================ +// HuffmanBitWriter - 高效位输出 +// ============================================================================ + +class HuffmanBitWriter { +public: + explicit HuffmanBitWriter(); + ~HuffmanBitWriter() = default; + + HuffmanBitWriter(const HuffmanBitWriter&) = delete; + HuffmanBitWriter& operator=(const HuffmanBitWriter&) = delete; + + HuffmanBitWriter(HuffmanBitWriter&&) = default; + HuffmanBitWriter& operator=(HuffmanBitWriter&&) = default; + + void reset(); + void flush(); + + PZIP_FORCE_INLINE void writeBits(int32_t b, uint8_t nb) { + bits_ |= static_cast(b) << (nbits_ & 63); + nbits_ += nb; + if (PZIP_UNLIKELY(nbits_ >= 48)) { + writeOutBits(); + } + } + + PZIP_FORCE_INLINE void writeCode(HCode c) { + bits_ |= c.code64() << (nbits_ & 63); + nbits_ += c.len(); + if (PZIP_UNLIKELY(nbits_ >= 48)) { + writeOutBits(); + } + } + + void writeBytes(const uint8_t* bytes, size_t len); + void writeStoredHeader(int length, bool isEof); + void writeFixedHeader(bool isEof); + + void writeBlock(Tokens* tokens, bool eof, const uint8_t* input, size_t inputLen); + void writeBlockDynamic(Tokens* tokens, bool eof, const uint8_t* input, size_t inputLen, bool sync); + void writeBlockHuff(bool eof, const uint8_t* input, size_t inputLen, bool sync); + + void writeTokens(const Token* tokens, size_t n, const HCode* leCodes, const HCode* oeCodes); + + const std::vector& data() const { return output_; } + std::vector& data() { return output_; } + + void setLogNewTablePenalty(int penalty) { logNewTablePenalty_ = penalty; } + +private: + void writeOutBits(); + void indexTokens(Tokens* t, bool alwaysEOB); + void generate(); + int extraBitSize(); + int fixedSize(int extraBits); + int storedSize(const uint8_t* input, size_t len, bool* storable); + void histogram(const uint8_t* input, size_t len); + std::pair headerSize(); + void generateCodegen(int numLiterals, int numOffsets, HuffmanEncoder* litEnc, HuffmanEncoder* offEnc); + int codegens(); + void writeDynamicHeader(int numLiterals, int numOffsets, int numCodegens, bool isEof); + + std::vector output_; + uint64_t bits_ = 0; + uint8_t nbits_ = 0; + uint8_t nbytes_ = 0; + int lastHeader_ = 0; + bool lastHuffMan_ = false; + int logNewTablePenalty_ = 7; + + std::array bytes_; + std::array literalFreq_; + std::array offsetFreq_; + std::array codegenFreq_; + std::array codegen_; + + std::unique_ptr literalEncoding_; + std::unique_ptr offsetEncoding_; + std::unique_ptr tmpLitEncoding_; + std::unique_ptr codegenEncoding_; +}; + +// ============================================================================ +// TableEntry +// ============================================================================ + +struct TableEntry { + int32_t offset = 0; +}; + +// ============================================================================ +// FastGen - 快速编码器基类 +// ============================================================================ + +class FastGen { +public: + FastGen() : cur_(MAX_STORE_BLOCK_SIZE) { + hist_.reserve(ALLOC_HISTORY); + } + virtual ~FastGen() = default; + + int32_t addBlock(const uint8_t* src, size_t len); + virtual void reset(); + + // 纯虚函数,由子类实现 + virtual void encode(Tokens* dst, const uint8_t* src, size_t len) = 0; + +protected: + std::vector hist_; + int32_t cur_; +}; + +// ============================================================================ +// FastEncL1 - Level 1 编码器 (最快) +// ============================================================================ + +class FastEncL1 : public FastGen { +public: + FastEncL1() { table_.fill({}); } + + void encode(Tokens* dst, const uint8_t* src, size_t len) override; + void reset() override; + +private: + std::array table_; +}; + +// ============================================================================ +// FastEncL4 - Level 4 编码器 (平衡) +// ============================================================================ + +class FastEncL4 : public FastGen { +public: + FastEncL4() { + table_.fill({}); + bTable_.fill({}); + } + + void encode(Tokens* dst, const uint8_t* src, size_t len) override; + void reset() override; + +private: + std::array table_; + std::array bTable_; +}; + +// ============================================================================ +// FastDeflate - 高性能压缩器 +// ============================================================================ + +class FastDeflate { +public: + explicit FastDeflate(CompressionLevel level = CompressionLevel::DefaultCompression); + ~FastDeflate() = default; + + size_t compress(const uint8_t* input, size_t inputSize, std::vector& output); + void reset(); + +private: + CompressionLevel level_; + std::unique_ptr encoderL1_; + std::unique_ptr encoderL4_; + std::unique_ptr writer_; + Tokens tokens_; + bool useL1_; +}; + +// ============================================================================ +// 便捷函数 +// ============================================================================ + +size_t deflateCompress(const uint8_t* input, size_t inputSize, + std::vector& output, + CompressionLevel level = CompressionLevel::DefaultCompression); + +// ============================================================================ +// FlateWriter - 流式压缩器(参照 Go klauspost/compress flate.Writer) +// ============================================================================ + +// 输出回调类型(参照 Go io.Writer) +using WriteFunc = std::function; + +class FlateWriter { +public: + // 接收输出目标(参照 Go flate.NewWriter(w io.Writer, level int)) + explicit FlateWriter(WriteFunc output, CompressionLevel level = CompressionLevel::BestSpeed); + ~FlateWriter() = default; + + // 流式写入数据(参照 Go compressor.write) + size_t write(const uint8_t* data, size_t size); + + // 完成压缩(参照 Go compressor.Close) + void close(); + + // 重置并设置新的输出目标(参照 Go compressor.Reset) + void reset(WriteFunc output); + +private: + void storeFast(); + size_t fillBlock(const uint8_t* data, size_t size); + void flushOutput(); + void forceFlush(); + + // 获取当前使用的编码器 + FastGen* encoder() const { return useL1_ ? static_cast(encoderL1_.get()) + : static_cast(encoderL4_.get()); } + + WriteFunc output_; + std::vector window_; + size_t windowEnd_ = 0; + + CompressionLevel level_; + bool useL1_ = true; // Level 1-3 使用 L1,Level 4+ 使用 L4 + std::unique_ptr encoderL1_; + std::unique_ptr encoderL4_; + std::unique_ptr writer_; + Tokens tokens_; +}; + +} // namespace pzip diff --git a/src/pzip/include/pzip/file_task.h b/src/pzip/include/pzip/file_task.h new file mode 100644 index 000000000..7f60e9146 --- /dev/null +++ b/src/pzip/include/pzip/file_task.h @@ -0,0 +1,161 @@ +// 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 "common.h" +#include +#include +#include +#include +#include + +namespace pzip { + +namespace fs = std::filesystem; + +/** + * @brief ZIP 文件头信息 + */ +// ZIP64 阈值常量 (对应 Go 的 uint32max) +constexpr uint32_t ZIP_UINT32_MAX = 0xFFFFFFFF; +constexpr uint16_t ZIP_UINT16_MAX = 0xFFFF; +constexpr uint16_t ZIP_VERSION_45 = 45; // ZIP64 需要版本 4.5 + +struct ZipFileHeader { + std::string name; // 文件名(相对路径) + uint16_t versionMadeBy = 0; + uint16_t versionNeeded = ZIP_VERSION_20; + uint16_t flags = 0; + uint16_t method = ZIP_METHOD_DEFLATE; + uint16_t modTime = 0; + uint16_t modDate = 0; + uint32_t crc32 = 0; + uint64_t compressedSize = 0; + 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; + } +}; + +/** + * @brief 文件任务类 + * + * 对应 Go 版 pzip 的 pool.File 结构 + * 管理文件的压缩数据缓冲区 + */ +class FileTask { +public: + FileTask(); + ~FileTask(); + + // 禁止拷贝,允许移动 + FileTask(const FileTask&) = delete; + FileTask& operator=(const FileTask&) = delete; + FileTask(FileTask&&) noexcept; + FileTask& operator=(FileTask&&) noexcept; + + /** + * @brief 重置任务,准备处理新文件 + * @param path 文件完整路径 + * @param relativeTo 相对路径基准目录 + * @return 错误信息 + */ + Error reset(const fs::path& path, const fs::path& relativeTo = ""); + + /** + * @brief 写入压缩数据 + * @param data 数据指针 + * @param size 数据大小 + * @return 实际写入的字节数 + */ + size_t write(const uint8_t* data, size_t size); + + /** + * @brief 获取已写入的总字节数 + */ + size_t written() const { return written_; } + + /** + * @brief 是否有溢出数据(写入了临时文件) + */ + bool overflowed() const { return overflow_ != nullptr; } + + /** + * @brief 获取压缩数据缓冲区 + */ + const std::vector& compressedData() const { return buffer_; } + + /** + * @brief 获取压缩数据的指针和大小(包括溢出部分) + * @param callback 回调函数,接收数据块 + */ + void readCompressedData(std::function callback); + + // 公共成员 + fs::path path; // 文件完整路径 + fs::file_status status; // 文件状态 + uintmax_t fileSize = 0; // 原始文件大小 + ZipFileHeader header; // ZIP 头信息 + 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; + +private: + std::vector buffer_; // 内存缓冲区 + size_t bufferUsed_ = 0; // 已使用的缓冲区大小 + std::unique_ptr overflow_; // 溢出临时文件 + fs::path overflowPath_; // 临时文件路径 + size_t written_ = 0; // 总写入字节数 +}; + +/** + * @brief 文件任务对象池 + * + * 类似于 Go 的 sync.Pool,复用 FileTask 对象减少内存分配 + */ +class FileTaskPool { +public: + static FileTaskPool& instance(); + + std::unique_ptr acquire(); + void release(std::unique_ptr task); + +private: + FileTaskPool() = default; + + std::mutex mutex_; + std::vector> pool_; +}; + +} // namespace pzip + diff --git a/src/pzip/include/pzip/pzip.h b/src/pzip/include/pzip/pzip.h new file mode 100644 index 000000000..779d38bcc --- /dev/null +++ b/src/pzip/include/pzip/pzip.h @@ -0,0 +1,63 @@ +// 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 + +/** + * @file pzip.hpp + * @brief pzip 主头文件 + * + * 包含所有公共 API + */ + +#include "common.h" +#include "archiver.h" +#include "extractor.h" +#include "utils.h" + +namespace pzip { + +/** + * @brief 获取版本号 + */ +inline const char* version() { + return "1.0.0"; +} + +/** + * @brief 快速压缩文件/目录 + * @param archivePath 输出 ZIP 文件路径 + * @param paths 要压缩的文件/目录列表 + * @param options 压缩选项 + * @return 错误信息 + */ +inline Error compress(const fs::path& archivePath, + const std::vector& paths, + const ArchiverOptions& options = {}) { + Archiver archiver(archivePath, options); + Error err = archiver.archive(paths); + if (err) return err; + return archiver.close(); +} + +/** + * @brief 快速解压 ZIP 文件 + * @param archivePath ZIP 文件路径 + * @param outputDir 输出目录 + * @param options 解压选项 + * @return 错误信息 + */ +inline Error decompress(const fs::path& archivePath, + const fs::path& outputDir, + const ExtractorOptions& options = {}) { + Extractor extractor(outputDir, options); + Error err = extractor.extract(archivePath); + if (err) return err; + return extractor.close(); +} + +} // namespace pzip + + diff --git a/src/pzip/include/pzip/utils.h b/src/pzip/include/pzip/utils.h new file mode 100644 index 000000000..3cd015be7 --- /dev/null +++ b/src/pzip/include/pzip/utils.h @@ -0,0 +1,113 @@ +// 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 "common.h" +#include +#include + +namespace pzip { + +namespace fs = std::filesystem; + +/** + * @brief 工具函数集合 + */ +namespace utils { + +/** + * @brief 检测字符串是否为有效 UTF-8 + * @param s 输入字符串 + * @return {是否有效, 是否需要 UTF-8 标志} + */ +std::pair detectUTF8(const std::string& s); + +/** + * @brief 将路径转换为 ZIP 格式(使用 /) + * @param path 文件路径 + * @return ZIP 格式路径 + */ +std::string toZipPath(const fs::path& path); + +/** + * @brief 从 ZIP 路径转换为本地路径 + * @param zipPath ZIP 格式路径 + * @return 本地路径 + */ +fs::path fromZipPath(const std::string& zipPath); + +/** + * @brief 计算 CRC32 + * @param data 数据指针 + * @param size 数据大小 + * @return CRC32 值 + */ +uint32_t crc32(const uint8_t* data, size_t size); + +/** + * @brief 更新 CRC32 + * @param crc 当前 CRC32 值 + * @param data 数据指针 + * @param size 数据大小 + * @return 更新后的 CRC32 值 + */ +uint32_t crc32Update(uint32_t crc, const uint8_t* data, size_t size); + +/** + * @brief 获取文件修改时间 + * @param path 文件路径 + * @return Unix 时间戳 + */ +time_t getModTime(const fs::path& path); + +/** + * @brief 设置文件修改时间 + * @param path 文件路径 + * @param modTime Unix 时间戳 + * @return 是否成功 + */ +bool setModTime(const fs::path& path, time_t modTime); + +/** + * @brief 从 Unix 权限获取 ZIP 外部属性 + * @param mode Unix 文件模式 + * @return ZIP 外部属性 + */ +uint32_t modeToZipAttr(mode_t mode); + +/** + * @brief 从 ZIP 外部属性获取 Unix 权限 + * @param attr ZIP 外部属性 + * @return Unix 文件模式 + */ +mode_t zipAttrToMode(uint32_t attr); + +/** + * @brief 创建临时文件 + * @param prefix 文件名前缀 + * @return 临时文件路径 + */ +fs::path createTempFile(const std::string& prefix = "pzip"); + +/** + * @brief 格式化文件大小 + * @param size 字节数 + * @return 格式化的字符串(如 "1.5 MB") + */ +std::string formatSize(uint64_t size); + +/** + * @brief 格式化时间 + * @param seconds 秒数 + * @return 格式化的字符串(如 "1m 30s") + */ +std::string formatTime(double seconds); + +} // namespace utils + +} // namespace pzip + + diff --git a/src/pzip/include/pzip/worker_pool.h b/src/pzip/include/pzip/worker_pool.h new file mode 100644 index 000000000..63394eed2 --- /dev/null +++ b/src/pzip/include/pzip/worker_pool.h @@ -0,0 +1,227 @@ +// 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 "common.h" +#include +#include +#include + +namespace pzip { + +/** + * @brief 通用线程池模板类 + * + * 类似于 Go 版 pzip 的 FileWorkerPool,支持任务入队和并行处理 + * + * @tparam T 任务类型 + */ +template +class WorkerPool { +public: + using Executor = std::function; + + /** + * @brief 构造函数 + * @param executor 任务执行函数 + * @param concurrency 并发数(0 表示使用 CPU 核心数) + * @param capacity 队列容量 + */ + WorkerPool(Executor executor, size_t concurrency = 0, size_t capacity = 1); + + ~WorkerPool(); + + // 禁止拷贝 + WorkerPool(const WorkerPool&) = delete; + WorkerPool& operator=(const WorkerPool&) = delete; + + /** + * @brief 启动工作线程 + */ + void start(); + + /** + * @brief 入队任务 + * @param task 任务指针 + */ + void enqueue(T* task); + + /** + * @brief 关闭线程池,等待所有任务完成 + * @return 第一个遇到的错误(如果有) + */ + Error close(); + + /** + * @brief 取消所有待处理任务 + */ + void cancel(); + + /** + * @brief 获取待处理任务数量 + */ + size_t pendingTasks() const; + + /** + * @brief 是否正在运行 + */ + bool isRunning() const { return running_; } + +private: + void workerThread(); + + Executor executor_; + size_t concurrency_; + size_t capacity_; + + std::vector workers_; + std::queue tasks_; + + mutable std::mutex mutex_; + std::condition_variable taskAvailable_; + std::condition_variable spaceAvailable_; + + std::atomic running_{false}; + std::atomic shutdown_{false}; + std::atomic cancelled_{false}; + + Error firstError_; + std::mutex errorMutex_; +}; + +// ============================================================================ +// 模板实现 +// ============================================================================ + +template +WorkerPool::WorkerPool(Executor executor, size_t concurrency, size_t capacity) + : executor_(std::move(executor)) + , concurrency_(concurrency == 0 ? std::thread::hardware_concurrency() : concurrency) + , capacity_(capacity) +{ +} + +template +WorkerPool::~WorkerPool() { + if (running_) { + cancel(); + close(); + } +} + +template +void WorkerPool::start() { + if (running_) return; + + running_ = true; + shutdown_ = false; + cancelled_ = false; + firstError_ = Error(); + + workers_.reserve(concurrency_); + for (size_t i = 0; i < concurrency_; ++i) { + workers_.emplace_back(&WorkerPool::workerThread, this); + } +} + +template +void WorkerPool::enqueue(T* task) { + std::unique_lock lock(mutex_); + + // 等待队列有空间 + spaceAvailable_.wait(lock, [this] { + return tasks_.size() < capacity_ || shutdown_ || cancelled_; + }); + + if (shutdown_ || cancelled_) return; + + tasks_.push(task); + taskAvailable_.notify_one(); +} + +template +Error WorkerPool::close() { + { + std::lock_guard lock(mutex_); + shutdown_ = true; + } + taskAvailable_.notify_all(); + spaceAvailable_.notify_all(); + + for (auto& worker : workers_) { + if (worker.joinable()) { + worker.join(); + } + } + + workers_.clear(); + running_ = false; + + std::lock_guard lock(errorMutex_); + return firstError_; +} + +template +void WorkerPool::cancel() { + { + std::lock_guard lock(mutex_); + cancelled_ = true; + + // 清空队列 + while (!tasks_.empty()) { + tasks_.pop(); + } + } + taskAvailable_.notify_all(); + spaceAvailable_.notify_all(); +} + +template +size_t WorkerPool::pendingTasks() const { + std::lock_guard lock(mutex_); + return tasks_.size(); +} + +template +void WorkerPool::workerThread() { + while (true) { + T* task = nullptr; + + { + std::unique_lock lock(mutex_); + + taskAvailable_.wait(lock, [this] { + return !tasks_.empty() || shutdown_ || cancelled_; + }); + + if (cancelled_ || (shutdown_ && tasks_.empty())) { + return; + } + + if (!tasks_.empty()) { + task = tasks_.front(); + tasks_.pop(); + spaceAvailable_.notify_one(); + } + } + + if (task && !cancelled_) { + Error err = executor_(task); + + if (err) { + std::lock_guard lock(errorMutex_); + if (!firstError_) { + firstError_ = err; + } + cancelled_ = true; + taskAvailable_.notify_all(); + } + } + } +} + +} // namespace pzip + diff --git a/src/pzip/include/pzip/zip_reader.h b/src/pzip/include/pzip/zip_reader.h new file mode 100644 index 000000000..081f4565a --- /dev/null +++ b/src/pzip/include/pzip/zip_reader.h @@ -0,0 +1,101 @@ +// 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 "common.h" +#include "file_task.h" +#include + +namespace pzip { + +/** + * @brief ZIP 文件条目 + */ +struct ZipEntry { + ZipFileHeader header; + uint64_t dataOffset; // 压缩数据在文件中的偏移 + uint64_t localHeaderOffset; // 本地文件头偏移 + + bool isDirectory() const { return header.isDirectory(); } +}; + +/** + * @brief ZIP 文件读取器 + * + * 类似于 Go archive/zip 的 Reader + */ +class ZipReader { +public: + explicit ZipReader(const fs::path& path); + ~ZipReader(); + + // 禁止拷贝 + ZipReader(const ZipReader&) = delete; + ZipReader& operator=(const ZipReader&) = delete; + + /** + * @brief 打开并读取 ZIP 文件目录 + * @return 错误信息 + */ + Error open(); + + /** + * @brief 关闭文件 + */ + void close(); + + /** + * @brief 获取所有条目 + */ + const std::vector& entries() const { return entries_; } + + /** + * @brief 读取条目的压缩数据 + * @param entry 条目 + * @param buffer 输出缓冲区 + * @return 错误信息 + */ + Error readCompressed(const ZipEntry& entry, std::vector& buffer); + + /** + * @brief 解压并读取条目数据 + * @param entry 条目 + * @param buffer 输出缓冲区 + * @return 错误信息 + */ + Error readDecompressed(const ZipEntry& entry, std::vector& buffer); + + /** + * @brief 获取注释 + */ + const std::string& comment() const { return comment_; } + + /** + * @brief 是否已打开 + */ + bool isOpen() const { return file_.is_open(); } + + // DOS 时间转换 + static time_t dosToTime(uint16_t date, uint16_t time); + +private: + Error readEndOfCentralDirectory(); + Error readCentralDirectory(); + Error readLocalFileHeader(ZipEntry& entry); + + fs::path path_; + std::ifstream file_; + std::vector entries_; + std::string comment_; + + uint64_t centralDirOffset_ = 0; + uint64_t centralDirSize_ = 0; + uint32_t totalEntries_ = 0; +}; + +} // namespace pzip + + diff --git a/src/pzip/include/pzip/zip_writer.h b/src/pzip/include/pzip/zip_writer.h new file mode 100644 index 000000000..b066dd38f --- /dev/null +++ b/src/pzip/include/pzip/zip_writer.h @@ -0,0 +1,134 @@ +// 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 "common.h" +#include "file_task.h" +#include "pzip/crypto/winzip_aes_extra.h" +#include + +namespace pzip { + +/** + * @brief Extended Timestamp Extra Field (0x5455) + * + * 用于存储 UTC 时间戳 + */ +struct ExtendedTimestamp { + time_t modTime; + + std::vector encode() const; + static ExtendedTimestamp decode(const uint8_t* data, size_t size); +}; + +/** + * @brief ZIP 文件写入器 + * + * 支持 CreateRaw 方式写入预压缩数据 + * 类似于 Go archive/zip 的 Writer + */ +class ZipWriter { +public: + explicit ZipWriter(const fs::path& path); + ~ZipWriter(); + + // 禁止拷贝 + ZipWriter(const ZipWriter&) = delete; + ZipWriter& operator=(const ZipWriter&) = delete; + + /** + * @brief 打开文件准备写入 + * @return 错误信息 + */ + Error open(); + + /** + * @brief 写入预压缩的数据(CreateRaw 风格) + * @param header ZIP 文件头 + * @param compressedData 压缩后的数据 + * @param compressedSize 压缩数据大小 + * @return 错误信息 + */ + 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 文件头 + * @param data 原始数据 + * @param size 数据大小 + * @return 错误信息 + */ + Error create(const ZipFileHeader& header, const uint8_t* data, size_t size); + + /** + * @brief 写入目录条目 + * @param header ZIP 文件头(name 应以 / 结尾) + * @return 错误信息 + */ + Error createDirectory(const ZipFileHeader& header); + + /** + * @brief 关闭并写入中央目录 + * @return 错误信息 + */ + Error close(); + + /** + * @brief 是否已打开 + */ + bool isOpen() const { return file_.is_open(); } + + /** + * @brief 设置注释 + */ + void setComment(const std::string& comment) { comment_ = comment; } + +private: + // 中央目录条目 + struct CentralDirEntry { + ZipFileHeader header; + uint64_t localHeaderOffset; + }; + + Error writeLocalFileHeader(const ZipFileHeader& header); + Error writeDataDescriptor(const ZipFileHeader& header); + Error writeCentralDirectory(); + Error writeEndOfCentralDirectory(uint64_t centralDirOffset, uint64_t centralDirSize); + + // DOS 时间转换 + static void timeToDos(time_t t, uint16_t& date, uint16_t& time); + + fs::path path_; + std::ofstream file_; + std::vector centralDir_; + std::string comment_; + uint64_t currentOffset_ = 0; + std::mutex writeMutex_; + + // 大缓冲区减少系统调用 + static constexpr size_t WRITE_BUFFER_SIZE = 256 * 1024; + std::vector writeBuffer_; +}; + +} // namespace pzip + + diff --git a/src/pzip/src/archiver.cpp b/src/pzip/src/archiver.cpp new file mode 100644 index 000000000..f607cad28 --- /dev/null +++ b/src/pzip/src/archiver.cpp @@ -0,0 +1,459 @@ +// 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/archiver.h" +#include "pzip/fast_deflate.h" +#include "pzip/utils.h" +#include "pzip/crypto/aes_encryptor.h" +#include +#include +#include +#include +#include + +#ifdef USE_LIBDEFLATE +#include +#endif + +namespace pzip { + +// ============================================================================ +// Archiver 实现 +// ============================================================================ + +Archiver::Archiver(const fs::path& archive, const ArchiverOptions& options) + : archivePath_(archive) + , options_(options) +{ + std::error_code ec; + absoluteArchivePath_ = fs::absolute(archive, ec); + if (ec) { + absoluteArchivePath_ = archive; + } + + // 创建 ZIP 写入器 + writer_ = std::make_unique(archivePath_); + + // 确定并发数 + size_t concurrency = options_.concurrency; + if (concurrency == 0) { + concurrency = std::thread::hardware_concurrency(); + } + + // 创建文件处理线程池(并行压缩) + auto processExecutor = [this](FileTask* task) -> Error { + return compressFile(task); + }; + fileProcessPool_ = std::make_unique>( + processExecutor, concurrency, concurrency * 2 + ); + + // 创建文件写入线程池(顺序写入,concurrency = 1) + auto writeExecutor = [this](FileTask* task) -> Error { + return archiveFile(task); + }; + fileWriterPool_ = std::make_unique>( + writeExecutor, 1, concurrency * 2 + ); +} + +Archiver::~Archiver() { + // 资源由智能指针自动管理 +} + +Error Archiver::archive(const std::vector& paths) { + // 打开 ZIP 文件 + Error err = writer_->open(); + if (err) return err; + + // 启动线程池 + fileProcessPool_->start(); + fileWriterPool_->start(); + + // 遍历所有路径 + for (const auto& path : paths) { + if (cancelled_) break; + + std::error_code ec; + auto status = fs::status(path, ec); + if (ec) { + return Error(ErrorCode::FILE_NOT_FOUND, "Cannot stat: " + path.string()); + } + + if (fs::is_directory(status)) { + err = walkDirectory(path); + if (err) return err; + } else { + // 单个文件 + chroot_.clear(); + + auto task = FileTaskPool::instance().acquire(); + err = task->reset(path); + if (err) { + FileTaskPool::instance().release(std::move(task)); + return err; + } + + // 跳过输出文件本身 + if (fs::equivalent(task->path, absoluteArchivePath_, ec)) { + FileTaskPool::instance().release(std::move(task)); + continue; + } + + totalFiles_++; + fileProcessPool_->enqueue(task.release()); + } + } + + // 等待处理完成 + err = fileProcessPool_->close(); + if (err) return err; + + err = fileWriterPool_->close(); + if (err) return err; + + return Error(); +} + +Error Archiver::walkDirectory(const fs::path& root) { + std::error_code ec; + chroot_ = fs::absolute(root, ec); + if (ec) { + return Error(ErrorCode::FILE_NOT_FOUND, "Cannot get absolute path: " + root.string()); + } + + for (auto it = fs::recursive_directory_iterator(chroot_, ec); + it != fs::recursive_directory_iterator(); + ++it) { + if (cancelled_) break; + if (ec) { + return Error(ErrorCode::FILE_READ_ERROR, "Directory iteration error: " + ec.message()); + } + + const auto& entry = *it; + + // 跳过输出文件本身 + if (fs::equivalent(entry.path(), absoluteArchivePath_, ec)) { + continue; + } + + auto task = FileTaskPool::instance().acquire(); + Error err = task->reset(entry.path(), chroot_.parent_path()); + if (err) { + FileTaskPool::instance().release(std::move(task)); + continue; // 跳过无法处理的文件 + } + + totalFiles_++; + fileProcessPool_->enqueue(task.release()); + } + + // 也要添加根目录本身 + auto task = FileTaskPool::instance().acquire(); + Error err = task->reset(chroot_, chroot_.parent_path()); + if (!err) { + totalFiles_++; + fileProcessPool_->enqueue(task.release()); + } else { + FileTaskPool::instance().release(std::move(task)); + } + + return Error(); +} + +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(); +} + +Error Archiver::compress(FileTask* task) { + if (fs::is_directory(task->status)) { + return Error(); + } + + if (task->isSymlink) { + const auto& target = task->symlinkTarget; + task->write(reinterpret_cast(target.data()), target.size()); + task->header.crc32 = ::crc32(0, reinterpret_cast(target.data()), target.size()); + return Error(); + } + + std::ifstream file(task->path, std::ios::binary); + if (!file.is_open()) { + return Error(ErrorCode::FILE_OPEN_ERROR, "Cannot open file: " + task->path.string()); + } + + const bool useStore = (options_.compressionLevel == 0); + + constexpr size_t BUFFER_SIZE = 32 * 1024; + 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 + file.close(); + task->streamFromSource = true; + return Error(); + } + + // Deflate 模式:使用 FlateWriter 按指定等级压缩 + auto level = static_cast( + std::clamp(options_.compressionLevel, 1, 9)); + + FlateWriter writer([task](const uint8_t* data, size_t size) { + task->write(data, size); + }, level); + + while (file.good() && !file.eof()) { + file.read(reinterpret_cast(buf.data()), buf.size()); + auto bytesRead = file.gcount(); + if (bytesRead > 0) { + 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; + } + } + } + } + + writer.close(); + + if (file.bad()) { + file.close(); + return Error(ErrorCode::FILE_READ_ERROR, "I/O error reading file: " + task->path.string()); + } + + if (totalBytesRead != task->fileSize) { + file.close(); + return Error(ErrorCode::FILE_READ_ERROR, + "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(); + + task->header.crc32 = crc; + + return Error(); +} + +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) { + h.modTime = ((tm->tm_hour & 0x1F) << 11) | + ((tm->tm_min & 0x3F) << 5) | + ((tm->tm_sec / 2) & 0x1F); + h.modDate = (((tm->tm_year - 80) & 0x7F) << 9) | + (((tm->tm_mon + 1) & 0x0F) << 5) | + (tm->tm_mday & 0x1F); + } + + // 目录处理 + if (fs::is_directory(task->status)) { + if (!h.name.empty() && h.name.back() != '/') { + h.name += '/'; + } + h.method = ZIP_METHOD_STORE; + h.flags &= ~ZIP_FLAG_DATA_DESCRIPTOR; + 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; + h.flags &= ~ZIP_FLAG_DATA_DESCRIPTOR; + h.uncompressedSize = task->symlinkTarget.size(); + 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; + h.flags |= ZIP_FLAG_DATA_DESCRIPTOR; + h.uncompressedSize = task->fileSize; + h.compressedSize = task->fileSize; + } else { + h.method = ZIP_METHOD_DEFLATE; + h.flags |= ZIP_FLAG_DATA_DESCRIPTOR; + h.uncompressedSize = task->fileSize; + h.compressedSize = task->written(); + } +} + +Error Archiver::archiveFile(FileTask* task) { + if (cancelled_) { + FileTaskPool::instance().release(std::unique_ptr(task)); + return Error(ErrorCode::CANCELLED, "Operation cancelled"); + } + + 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; +} + +void Archiver::cancel() { + cancelled_ = true; + if (fileProcessPool_) { + fileProcessPool_->cancel(); + } + if (fileWriterPool_) { + fileWriterPool_->cancel(); + } +} + +Error Archiver::close() { + if (writer_ && writer_->isOpen()) { + return writer_->close(); + } + return Error(); +} + +void Archiver::setProgressCallback(ProgressCallback callback) { + options_.progress = std::move(callback); +} + +} // namespace pzip 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/extractor.cpp b/src/pzip/src/extractor.cpp new file mode 100644 index 000000000..a623f8286 --- /dev/null +++ b/src/pzip/src/extractor.cpp @@ -0,0 +1,418 @@ +// 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/extractor.h" +#include "pzip/utils.h" +#include +#include +#include +#include +#include + +namespace pzip { + +namespace { + +class ScopedFd { +public: + ScopedFd() = default; + explicit ScopedFd(int descriptor) : descriptor_(descriptor) {} + + ~ScopedFd() { reset(); } + + ScopedFd(const ScopedFd&) = delete; + ScopedFd& operator=(const ScopedFd&) = delete; + + ScopedFd(ScopedFd&& other) noexcept : descriptor_(other.release()) {} + + ScopedFd& operator=(ScopedFd&& other) noexcept { + if (this != &other) + reset(other.release()); + return *this; + } + + int get() const { return descriptor_; } + + int release() { + int descriptor = descriptor_; + descriptor_ = -1; + return descriptor; + } + + void reset(int descriptor = -1) { + if (descriptor_ >= 0) + ::close(descriptor_); + descriptor_ = descriptor; + } + +private: + int descriptor_ = -1; +}; + +std::string descriptorError(const std::string& action, const fs::path& path, int errorNumber) { + return action + " " + path.string() + ": " + std::strerror(errorNumber); +} + +int openDirectoryBeneath(int baseDescriptor, const fs::path& relativePath, bool create, + std::string& errorMessage) { + ScopedFd current(::openat(baseDescriptor, ".", O_RDONLY | O_DIRECTORY | O_CLOEXEC)); + if (current.get() < 0) { + errorMessage = descriptorError("Cannot open extraction directory", relativePath, errno); + return -1; + } + + for (const auto& component : relativePath) { + const std::string name = component.string(); + if (name.empty() || name == ".") + continue; + if (name == "..") { + errorMessage = "Unsafe path component in archive: " + relativePath.string(); + return -1; + } + + ScopedFd next(::openat(current.get(), name.c_str(), + O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW)); + if (next.get() < 0 && errno == ENOENT && create) { + if (::mkdirat(current.get(), name.c_str(), 0777) < 0 && errno != EEXIST) { + errorMessage = descriptorError("Cannot create directory", relativePath, errno); + return -1; + } + next.reset(::openat(current.get(), name.c_str(), + O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW)); + } + + if (next.get() < 0) { + errorMessage = descriptorError("Cannot open parent directory", relativePath, errno); + return -1; + } + current = std::move(next); + } + + return current.release(); +} + +bool writeAll(int descriptor, const std::vector& data, int& errorNumber) { + size_t written = 0; + while (written < data.size()) { + const ssize_t result = ::write(descriptor, data.data() + written, data.size() - written); + if (result < 0) { + if (errno == EINTR) + continue; + errorNumber = errno; + return false; + } + if (result == 0) { + errorNumber = EIO; + return false; + } + written += static_cast(result); + } + return true; +} + +void setModificationTime(int descriptor, const ZipEntry& entry) { + const time_t modificationTime = ZipReader::dosToTime(entry.header.modDate, entry.header.modTime); + const timespec times[2] = { + {modificationTime, 0}, + {modificationTime, 0} + }; + ::futimens(descriptor, times); +} + +} // namespace + +// ============================================================================ +// Extractor 实现 +// ============================================================================ + +Extractor::Extractor(const fs::path& outputDir, const ExtractorOptions& options) + : outputDir_(outputDir) + , options_(options) +{ + std::error_code ec; + absoluteOutputDir_ = fs::absolute(outputDir, ec); + if (ec) { + absoluteOutputDir_ = outputDir; + } + + // 确定并发数 + size_t concurrency = options_.concurrency; + if (concurrency == 0) { + concurrency = std::thread::hardware_concurrency(); + } + + // 创建解压线程池 + auto executor = [this](ExtractTask* task) -> Error { + return extractFile(task); + }; + fileWorkerPool_ = std::make_unique>( + executor, concurrency, concurrency * 2 + ); +} + +Extractor::~Extractor() { + close(); +} + +Error Extractor::extract(const fs::path& archivePath) { + // 创建读取器 + reader_ = std::make_unique(archivePath); + + // 打开 ZIP 文件 + Error err = reader_->open(); + if (err) return err; + + // 确保输出目录存在 + std::error_code ec; + fs::create_directories(absoluteOutputDir_, ec); + if (ec) { + return Error(ErrorCode::FILE_WRITE_ERROR, "Cannot create output directory"); + } + + canonicalOutputDir_ = fs::weakly_canonical(absoluteOutputDir_, ec); + if (ec) { + return Error(ErrorCode::FILE_WRITE_ERROR, + "Cannot resolve output directory: " + absoluteOutputDir_.string()); + } + + std::string rootError; + ScopedFd filesystemRoot(::open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC)); + if (filesystemRoot.get() < 0) { + return Error(ErrorCode::FILE_WRITE_ERROR, + descriptorError("Cannot open filesystem root", "/", errno)); + } + outputRootFd_ = openDirectoryBeneath(filesystemRoot.get(), + canonicalOutputDir_.relative_path(), + false, rootError); + if (outputRootFd_ < 0) { + return Error(ErrorCode::FILE_WRITE_ERROR, rootError); + } + + // 获取所有条目 + const auto& entries = reader_->entries(); + totalFiles_ = entries.size(); + + // 创建任务 + std::vector> tasks; + tasks.reserve(entries.size()); + + for (const auto& entry : entries) { + const fs::path output = outputPath(entry.header.name); + if (output.empty()) { + ::close(outputRootFd_); + outputRootFd_ = -1; + return Error(ErrorCode::INVALID_ARCHIVE, "Unsafe path in archive: " + entry.header.name); + } + + auto task = std::make_unique(); + task->entry = &entry; + task->relativePath = output; + tasks.push_back(std::move(task)); + } + + // 启动线程池 + fileWorkerPool_->start(); + + // 入队任务 + for (auto& task : tasks) { + if (cancelled_) break; + fileWorkerPool_->enqueue(task.release()); + } + + // 等待完成 + err = fileWorkerPool_->close(); + + ::close(outputRootFd_); + outputRootFd_ = -1; + + return err; +} + +Error Extractor::extractFile(ExtractTask* task) { + if (cancelled_) { + delete task; + return Error(ErrorCode::CANCELLED, "Operation cancelled"); + } + + const auto& entry = *task->entry; + const auto& relativePath = task->relativePath; + + Error err; + + if (entry.isDirectory()) { + err = writeDirectory(relativePath, entry); + } else { + err = writeFile(relativePath, entry); + } + + if (!err) { + task->success = true; + } + + // 更新进度 + processedFiles_++; + if (options_.progress) { + options_.progress(processedFiles_, totalFiles_); + } + + delete task; + return err; +} + +Error Extractor::writeDirectory(const fs::path& relativePath, const ZipEntry& entry) { + if (relativePath == ".") { + return Error(); + } + + std::string openError; + ScopedFd parent(openDirectoryBeneath(outputRootFd_, relativePath.parent_path(), + true, openError)); + if (parent.get() < 0) { + return Error(ErrorCode::FILE_WRITE_ERROR, openError); + } + + const std::string name = relativePath.filename().string(); + if (name.empty() || name == "." || name == "..") { + return Error(ErrorCode::INVALID_ARCHIVE, "Invalid directory path: " + relativePath.string()); + } + + if (::mkdirat(parent.get(), name.c_str(), 0777) < 0 && errno != EEXIST) { + return Error(ErrorCode::FILE_WRITE_ERROR, + descriptorError("Cannot create directory", relativePath, errno)); + } + + ScopedFd directory(::openat(parent.get(), name.c_str(), + O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW)); + if (directory.get() < 0) { + if (!options_.overwrite) + return Error(); + + if (::unlinkat(parent.get(), name.c_str(), 0) < 0) { + return Error(ErrorCode::FILE_WRITE_ERROR, + descriptorError("Cannot replace directory entry", relativePath, errno)); + } + if (::mkdirat(parent.get(), name.c_str(), 0777) < 0) { + return Error(ErrorCode::FILE_WRITE_ERROR, + descriptorError("Cannot create directory", relativePath, errno)); + } + directory.reset(::openat(parent.get(), name.c_str(), + O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW)); + if (directory.get() < 0) { + return Error(ErrorCode::FILE_WRITE_ERROR, + descriptorError("Cannot open created directory", relativePath, errno)); + } + } + + setPermissions(directory.get(), entry); + return Error(); +} + +Error Extractor::writeFile(const fs::path& relativePath, const ZipEntry& entry) { + std::vector data; + Error err = reader_->readDecompressed(entry, data); + if (err) + return err; + + std::string openError; + ScopedFd parent(openDirectoryBeneath(outputRootFd_, relativePath.parent_path(), + true, openError)); + if (parent.get() < 0) { + return Error(ErrorCode::FILE_WRITE_ERROR, openError); + } + + const std::string name = relativePath.filename().string(); + if (name.empty() || name == "." || name == "..") { + return Error(ErrorCode::INVALID_ARCHIVE, "Invalid file path: " + relativePath.string()); + } + + int flags = O_WRONLY | O_CREAT | O_CLOEXEC | O_NOFOLLOW; + flags |= options_.overwrite ? O_TRUNC : O_EXCL; + + ScopedFd output(::openat(parent.get(), name.c_str(), flags, 0666)); + if (output.get() < 0) { + if (!options_.overwrite && errno == EEXIST) + return Error(); + return Error(ErrorCode::FILE_WRITE_ERROR, + descriptorError("Cannot create file", relativePath, errno)); + } + + int writeError = 0; + if (!writeAll(output.get(), data, writeError)) { + return Error(ErrorCode::FILE_WRITE_ERROR, + descriptorError("Cannot write file", relativePath, writeError)); + } + + setPermissions(output.get(), entry); + setModificationTime(output.get(), entry); + return Error(); +} + +Error Extractor::setPermissions(int fileDescriptor, const ZipEntry& entry) { + if (!options_.preservePermissions) { + return Error(); + } + + const mode_t mode = utils::zipAttrToMode(entry.header.externalAttr); + if (mode != 0) { + ::fchmod(fileDescriptor, mode); + } + + return Error(); +} + +fs::path Extractor::outputPath(const std::string& name) const { + if (name.find('\0') != std::string::npos) + return {}; + + const fs::path localPath = utils::fromZipPath(name); + if (localPath.empty()) + return {}; + + // 拒绝绝对路径和包含 ".." 的路径分量,防止跳出解压目录。 + if (localPath.is_absolute() || localPath.has_root_name() || localPath.has_root_directory()) + return {}; + for (const auto& part : localPath) { + if (part == "..") + return {}; + } + + fs::path normalizedPath = localPath.lexically_normal(); + if (!normalizedPath.empty() && normalizedPath.filename().empty()) + normalizedPath = normalizedPath.parent_path(); + if (normalizedPath.empty() || normalizedPath.is_absolute() + || normalizedPath.has_root_name() || normalizedPath.has_root_directory()) { + return {}; + } + for (const auto& part : normalizedPath) { + if (part == "..") + return {}; + } + return normalizedPath; +} + +void Extractor::cancel() { + cancelled_ = true; + if (fileWorkerPool_) { + fileWorkerPool_->cancel(); + } +} + +Error Extractor::close() { + if (outputRootFd_ >= 0) { + ::close(outputRootFd_); + outputRootFd_ = -1; + } + if (reader_) { + reader_->close(); + } + return Error(); +} + +void Extractor::setProgressCallback(ProgressCallback callback) { + options_.progress = std::move(callback); +} + +} // namespace pzip + diff --git a/src/pzip/src/fast_deflate.cpp b/src/pzip/src/fast_deflate.cpp new file mode 100644 index 000000000..941480a4f --- /dev/null +++ b/src/pzip/src/fast_deflate.cpp @@ -0,0 +1,1263 @@ +// 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 + +/** + * @file fast_deflate.cpp + * @brief 高性能 DEFLATE 压缩器实现 + * + * 直接参考 klauspost/compress (Go) 官方代码翻译 + * 优化版本:添加 Level1 快速编码器 + */ + +#include "pzip/fast_deflate.h" +#include +#include +#include + +namespace pzip { + +// C++17 兼容性 +#if defined(__GNUC__) || defined(__clang__) +#define CTZ64(x) __builtin_ctzll(x) +#define CLZ32(x) __builtin_clz(x) +#else +inline int CTZ64(uint64_t x) { + if (x == 0) return 64; + int n = 0; + if ((x & 0xFFFFFFFF) == 0) { n += 32; x >>= 32; } + if ((x & 0xFFFF) == 0) { n += 16; x >>= 16; } + if ((x & 0xFF) == 0) { n += 8; x >>= 8; } + if ((x & 0xF) == 0) { n += 4; x >>= 4; } + if ((x & 0x3) == 0) { n += 2; x >>= 2; } + if ((x & 0x1) == 0) { n += 1; } + return n; +} +inline int CLZ32(uint32_t x) { + if (x == 0) return 32; + int n = 0; + if ((x & 0xFFFF0000) == 0) { n += 16; x <<= 16; } + if ((x & 0xFF000000) == 0) { n += 8; x <<= 8; } + if ((x & 0xF0000000) == 0) { n += 4; x <<= 4; } + if ((x & 0xC0000000) == 0) { n += 2; x <<= 2; } + if ((x & 0x80000000) == 0) { n += 1; } + return n; +} +#endif + +// ============================================================================ +// 静态表 - 仅用于 HuffmanBitWriter +// ============================================================================ + +static const uint8_t lengthExtraBits[32] = { + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, + 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0, 0, +}; + +static const uint8_t lengthBase[32] = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24, 28, + 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 255, 0, 0, 0, +}; + +static const int8_t offsetExtraBits[32] = { + 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, + 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, +}; + +static const uint32_t offsetBase[32] = { + 0x000000, 0x000001, 0x000002, 0x000003, 0x000004, + 0x000006, 0x000008, 0x00000c, 0x000010, 0x000018, + 0x000020, 0x000030, 0x000040, 0x000060, 0x000080, + 0x0000c0, 0x000100, 0x000180, 0x000200, 0x000300, + 0x000400, 0x000600, 0x000800, 0x000c00, 0x001000, + 0x001800, 0x002000, 0x003000, 0x004000, 0x006000, + 0x008000, 0x00c000, +}; + +static uint32_t offsetCombined[32] = {}; + +static struct StaticInit { + StaticInit() { + for (int i = 0; i < 32; ++i) { + if (offsetExtraBits[i] == 0 || offsetBase[i] > 0x006000) continue; + offsetCombined[i] = static_cast(offsetExtraBits[i]) | (offsetBase[i] << 8); + } + } +} staticInit; + +// ============================================================================ +// 内联函数 +// ============================================================================ + +PZIP_FORCE_INLINE uint16_t reverseBits(uint16_t number, uint8_t bitLength) { + uint16_t result = 0; + for (int i = 0; i < bitLength; ++i) { + result = (result << 1) | (number & 1); + number >>= 1; + } + return result; +} + +// 快速字符串匹配 - 使用 64 位比较 +PZIP_FORCE_INLINE PZIP_HOT int matchLen(const uint8_t* a, const uint8_t* b, size_t maxLen) { + int n = 0; + size_t left = maxLen; + + // 8 字节对齐快速比较 + while (left >= 8) { + uint64_t va = load64(a, n); + uint64_t vb = load64(b, n); + uint64_t diff = va ^ vb; + if (PZIP_UNLIKELY(diff != 0)) { + return n + CTZ64(diff) / 8; + } + n += 8; + left -= 8; + } + + // 处理剩余字节 + while (n < static_cast(maxLen) && a[n] == b[n]) { + n++; + } + return n; +} + +// ============================================================================ +// FastGen 实现 +// ============================================================================ + +int32_t FastGen::addBlock(const uint8_t* src, size_t len) { + if (hist_.size() + len > hist_.capacity()) { + if (hist_.capacity() == 0) { + hist_.reserve(ALLOC_HISTORY); + } else { + int32_t offset = static_cast(hist_.size()) - MAX_MATCH_OFFSET; + if (offset > 0) { + std::memmove(hist_.data(), hist_.data() + offset, MAX_MATCH_OFFSET); + cur_ += offset; + hist_.resize(MAX_MATCH_OFFSET); + } + } + } + int32_t s = static_cast(hist_.size()); + hist_.insert(hist_.end(), src, src + len); + return s; +} + +void FastGen::reset() { + if (hist_.capacity() < ALLOC_HISTORY) { + hist_.reserve(ALLOC_HISTORY); + } + cur_ += MAX_MATCH_OFFSET + static_cast(hist_.size()); + hist_.clear(); +} + +// ============================================================================ +// FastEncL1 实现 - Level 1 快速编码器 +// ============================================================================ + +void FastEncL1::reset() { + FastGen::reset(); + table_.fill({}); +} + +void PZIP_HOT FastEncL1::encode(Tokens* dst, const uint8_t* src, size_t len) { + constexpr int inputMargin = 11; + constexpr int minNonLiteralBlockSize = 13; + constexpr int skipLog = 5; + constexpr int doEvery = 2; + + // 防止溢出 + constexpr int32_t bufferReset = 0x7FFFFFFF - static_cast(ALLOC_HISTORY) - static_cast(MAX_STORE_BLOCK_SIZE) - 1; + if (cur_ >= bufferReset) { + if (hist_.empty()) { + table_.fill({}); + cur_ = MAX_MATCH_OFFSET; + } else { + int32_t minOff = cur_ + static_cast(hist_.size()) - MAX_MATCH_OFFSET; + for (auto& e : table_) { + e.offset = (e.offset <= minOff) ? 0 : (e.offset - cur_ + MAX_MATCH_OFFSET); + } + cur_ = MAX_MATCH_OFFSET; + } + } + + int32_t s = addBlock(src, len); + + if (static_cast(len) < minNonLiteralBlockSize) { + dst->n = static_cast(len); + return; + } + + const uint8_t* data = hist_.data(); + int32_t nextEmit = s; + int32_t sLimit = static_cast(hist_.size()) - inputMargin; + + uint64_t cv = load64(data, s); + + while (true) { + int32_t nextS = s; + int32_t t; + + // 查找匹配 + while (true) { + uint32_t nextHash = hash5(cv); + TableEntry candidate = table_[nextHash]; + nextS = s + doEvery + (s - nextEmit) / (1 << skipLog); + + if (PZIP_UNLIKELY(nextS > sLimit)) { + goto emitRemainder; + } + + uint64_t now = load64(data, nextS); + table_[nextHash] = {s + cur_}; + uint32_t nextHash2 = hash5(now); + t = candidate.offset - cur_; + + if (s - t < MAX_MATCH_OFFSET && static_cast(cv) == load32(data, t)) { + table_[nextHash2] = {nextS + cur_}; + break; + } + + // 再试一次 + cv = now; + s = nextS; + nextS++; + candidate = table_[nextHash2]; + now >>= 8; + table_[nextHash2] = {s + cur_}; + + t = candidate.offset - cur_; + if (s - t < MAX_MATCH_OFFSET && static_cast(cv) == load32(data, t)) { + table_[hash5(now)] = {nextS + cur_}; + break; + } + cv = now; + s = nextS; + } + + // 内层匹配循环 - 持续处理连续的匹配 + for (;;) { + size_t maxLen = std::min(hist_.size() - s - 4, MAX_MATCH_LENGTH - 4); + int32_t l = matchLen(data + s + 4, data + t + 4, maxLen) + 4; + + // 向后扩展 + while (t > 0 && s > nextEmit && data[t - 1] == data[s - 1]) { + s--; t--; l++; + } + + // 输出字面量 - 内联版本 + for (int32_t i = nextEmit; i < s; ++i) { + dst->tokens[dst->n] = makeLiteralToken(data[i]); + dst->litHist[data[i]]++; + dst->n++; + } + + // 输出匹配 - 内联版本 + dst->addMatchLong(l, static_cast(s - t - 1)); + s += l; + nextEmit = s; + + if (nextS >= s) s = nextS + 1; + + if (PZIP_UNLIKELY(s >= sLimit)) { + if (s + 8 < static_cast(hist_.size())) { + cv = load64(data, s); + table_[hash5(cv)] = {s + cur_}; + } + goto emitRemainder; + } + + // 更新哈希表并查找下一个匹配 + uint64_t x = load64(data, s - 2); + int32_t o = cur_ + s - 2; + table_[hash5(x)] = {o}; + x >>= 16; + TableEntry candidate = table_[hash5(x)]; + table_[hash5(x)] = {o + 2}; + + t = candidate.offset - cur_; + if (s - t > MAX_MATCH_OFFSET || static_cast(x) != load32(data, t)) { + // 没有找到下一个有效匹配,退出内层循环 + cv = x >> 8; + s++; + break; + } + // 找到了有效匹配,继续内层循环处理 + } + } + +emitRemainder: + if (nextEmit < static_cast(hist_.size())) { + if (dst->n == 0) return; + for (int32_t i = nextEmit; i < static_cast(hist_.size()); ++i) { + dst->tokens[dst->n] = makeLiteralToken(data[i]); + dst->litHist[data[i]]++; + dst->n++; + } + } +} + +// ============================================================================ +// FastEncL4 实现 - Level 4 编码器 +// ============================================================================ + +void FastEncL4::reset() { + FastGen::reset(); + table_.fill({}); + bTable_.fill({}); +} + +void PZIP_HOT FastEncL4::encode(Tokens* dst, const uint8_t* src, size_t len) { + constexpr int inputMargin = 11; + constexpr int minNonLiteralBlockSize = 13; + constexpr int skipLog = 6; + constexpr int doEvery = 1; + + constexpr int32_t bufferReset = 0x7FFFFFFF - static_cast(ALLOC_HISTORY) - static_cast(MAX_STORE_BLOCK_SIZE) - 1; + if (cur_ >= bufferReset) { + if (hist_.empty()) { + table_.fill({}); + bTable_.fill({}); + cur_ = MAX_MATCH_OFFSET; + } else { + int32_t minOff = cur_ + static_cast(hist_.size()) - MAX_MATCH_OFFSET; + for (auto& e : table_) { + e.offset = (e.offset <= minOff) ? 0 : (e.offset - cur_ + MAX_MATCH_OFFSET); + } + for (auto& e : bTable_) { + e.offset = (e.offset <= minOff) ? 0 : (e.offset - cur_ + MAX_MATCH_OFFSET); + } + cur_ = MAX_MATCH_OFFSET; + } + } + + int32_t s = addBlock(src, len); + + if (static_cast(len) < minNonLiteralBlockSize) { + dst->n = static_cast(len); + return; + } + + const uint8_t* data = hist_.data(); + int32_t nextEmit = s; + int32_t sLimit = static_cast(hist_.size()) - inputMargin; + + uint64_t cv = load64(data, s); + + while (true) { + int32_t nextS = s; + int32_t t; + + while (true) { + uint32_t nextHashS = hash4(static_cast(cv)); + uint32_t nextHashL = hash7(cv); + + s = nextS; + nextS = s + doEvery + (s - nextEmit) / (1 << skipLog); + if (PZIP_UNLIKELY(nextS > sLimit)) goto emitRemainder; + + TableEntry sCandidate = table_[nextHashS]; + TableEntry lCandidate = bTable_[nextHashL]; + uint64_t next = load64(data, nextS); + TableEntry entry = {s + cur_}; + table_[nextHashS] = entry; + bTable_[nextHashL] = entry; + + t = lCandidate.offset - cur_; + if (s - t < MAX_MATCH_OFFSET && static_cast(cv) == load32(data, t)) { + break; + } + + t = sCandidate.offset - cur_; + if (s - t < MAX_MATCH_OFFSET && static_cast(cv) == load32(data, t)) { + TableEntry lCand2 = bTable_[hash7(next)]; + int32_t lOff = lCand2.offset - cur_; + if (nextS - lOff < MAX_MATCH_OFFSET && load32(data, lOff) == static_cast(next)) { + size_t max1 = std::min(hist_.size() - s - 4, MAX_MATCH_LENGTH - 4); + size_t max2 = std::min(hist_.size() - nextS - 4, MAX_MATCH_LENGTH - 4); + int l1 = matchLen(data + s + 4, data + t + 4, max1); + int l2 = matchLen(data + nextS + 4, data + nextS - lOff + 4, max2); + if (l2 > l1) { + s = nextS; + t = lCand2.offset - cur_; + } + } + break; + } + cv = next; + } + + { + size_t maxLen = std::min(hist_.size() - s - 4, MAX_MATCH_LENGTH - 4); + int32_t l = matchLen(data + s + 4, data + t + 4, maxLen) + 4; + + while (t > 0 && s > nextEmit && data[t - 1] == data[s - 1]) { + s--; t--; l++; + } + + // 输出字面量 - 内联版本 + for (int32_t i = nextEmit; i < s; ++i) { + dst->tokens[dst->n] = makeLiteralToken(data[i]); + dst->litHist[data[i]]++; + dst->n++; + } + + // 输出匹配 + dst->addMatchLong(l, static_cast(s - t - 1)); + s += l; + nextEmit = s; + + if (nextS >= s) s = nextS + 1; + + if (PZIP_UNLIKELY(s >= sLimit)) { + if (s + 8 < static_cast(hist_.size())) { + cv = load64(data, s); + table_[hash4(static_cast(cv))] = {s + cur_}; + bTable_[hash7(cv)] = {s + cur_}; + } + goto emitRemainder; + } + + // 更新哈希表 + int32_t i = nextS; + if (i < s - 1) { + cv = load64(data, i); + TableEntry te = {i + cur_}; + TableEntry te2 = {te.offset + 1}; + bTable_[hash7(cv)] = te; + bTable_[hash7(cv >> 8)] = te2; + table_[hash4(static_cast(cv >> 8))] = te2; + + for (i += 3; i < s - 1; i += 3) { + cv = load64(data, i); + te = {i + cur_}; + te2 = {te.offset + 1}; + bTable_[hash7(cv)] = te; + bTable_[hash7(cv >> 8)] = te2; + table_[hash4(static_cast(cv >> 8))] = te2; + } + } + + uint64_t x = load64(data, s - 1); + int32_t o = cur_ + s - 1; + table_[hash4(static_cast(x))] = {o}; + bTable_[hash7(x)] = {o}; + cv = x >> 8; + } + } + +emitRemainder: + if (nextEmit < static_cast(hist_.size())) { + if (dst->n == 0) return; + for (int32_t i = nextEmit; i < static_cast(hist_.size()); ++i) { + dst->tokens[dst->n] = makeLiteralToken(data[i]); + dst->litHist[data[i]]++; + dst->n++; + } + } +} + +// ============================================================================ +// HuffmanEncoder 实现 +// ============================================================================ + +HuffmanEncoder::HuffmanEncoder(int size) { + size_t cap = 1ULL << (32 - CLZ32(static_cast(size - 1))); + codes.resize(cap); + freqCache.resize(size + 1); +} + +void HuffmanEncoder::generate(const uint16_t* freq, int numSymbols, int maxBits) { + std::vector list; + list.reserve(numSymbols + 1); + + int count = 0; + for (int i = 0; i < numSymbols; ++i) { + if (freq[i] != 0) { + list.push_back({static_cast(i), freq[i]}); + count++; + } else { + codes[i] = HCode(); + } + } + + if (count <= 2) { + for (int i = 0; i < count; ++i) { + codes[list[i].literal].set(static_cast(i), 1); + } + return; + } + + std::sort(list.begin(), list.end(), [](const LiteralNode& a, const LiteralNode& b) { + return a.freq < b.freq; + }); + + bitCounts(list, maxBits); + assignEncodingAndSize(bitCount.data(), list); +} + +void HuffmanEncoder::bitCounts(std::vector& list, int maxBits) { + int n = static_cast(list.size()); + if (maxBits > n - 1) maxBits = n - 1; + + bitCount.fill(0); + int bitsRemaining = 1 << maxBits; + + for (int i = n - 1; i >= 0; --i) { + int bits = 1; + int needed = 1; + while (bits < maxBits && needed <= bitsRemaining / 2) { + bits++; + needed <<= 1; + } + bitCount[bits]++; + bitsRemaining -= (1 << (maxBits - bits)); + } +} + +void HuffmanEncoder::assignEncodingAndSize(const int32_t* bc, std::vector& list) { + uint16_t code = 0; + int listIdx = static_cast(list.size()) - 1; + + for (int bits = 1; bits <= 15 && listIdx >= 0; ++bits) { + code <<= 1; + int cnt = bc[bits]; + + std::vector symbols; + for (int i = 0; i < cnt && listIdx >= 0; ++i) { + symbols.push_back(list[listIdx--].literal); + } + std::sort(symbols.begin(), symbols.end()); + + for (uint16_t sym : symbols) { + codes[sym].set(reverseBits(code++, bits), bits); + } + } +} + +int HuffmanEncoder::bitLength(const uint16_t* freq, int numSymbols) const { + int total = 0; + for (int i = 0; i < numSymbols && i < static_cast(codes.size()); ++i) { + if (freq[i] != 0) { + total += static_cast(freq[i]) * codes[i].len(); + } + } + return total; +} + +// ============================================================================ +// 固定霍夫曼编码表 +// ============================================================================ + +static std::unique_ptr createFixedLiteralEncoding() { + auto h = std::make_unique(LITERAL_COUNT); + for (int ch = 0; ch < LITERAL_COUNT; ++ch) { + uint16_t bits; + uint8_t size; + if (ch < 144) { bits = ch + 48; size = 8; } + else if (ch < 256) { bits = ch + 400 - 144; size = 9; } + else if (ch < 280) { bits = ch - 256; size = 7; } + else { bits = ch + 192 - 280; size = 8; } + h->codes[ch].set(reverseBits(bits, size), size); + } + return h; +} + +static std::unique_ptr createFixedOffsetEncoding() { + auto h = std::make_unique(30); + for (int ch = 0; ch < 30; ++ch) { + h->codes[ch].set(reverseBits(static_cast(ch), 5), 5); + } + return h; +} + +static std::unique_ptr fixedLiteralEncoding = createFixedLiteralEncoding(); +static std::unique_ptr fixedOffsetEncoding = createFixedOffsetEncoding(); + +// ============================================================================ +// HuffmanBitWriter 实现 +// ============================================================================ + +HuffmanBitWriter::HuffmanBitWriter() { + output_.reserve(256 * 1024); + literalEncoding_ = std::make_unique(LITERAL_COUNT); + offsetEncoding_ = std::make_unique(OFFSET_CODE_COUNT); + tmpLitEncoding_ = std::make_unique(LITERAL_COUNT); + codegenEncoding_ = std::make_unique(19); + reset(); +} + +void HuffmanBitWriter::reset() { + output_.clear(); + bits_ = 0; + nbits_ = 0; + nbytes_ = 0; + lastHeader_ = 0; + lastHuffMan_ = false; +} + +void HuffmanBitWriter::writeOutBits() { + store64(bytes_.data(), nbytes_, bits_); + bits_ >>= 48; + nbits_ -= 48; + nbytes_ += 6; + + if (PZIP_UNLIKELY(nbytes_ >= BUFFER_FLUSH_SIZE)) { + output_.insert(output_.end(), bytes_.begin(), bytes_.begin() + nbytes_); + nbytes_ = 0; + } +} + +void HuffmanBitWriter::flush() { + if (lastHeader_ > 0) { + writeCode(literalEncoding_->codes[END_BLOCK_MARKER]); + lastHeader_ = 0; + } + + uint8_t n = nbytes_; + while (nbits_ != 0) { + bytes_[n++] = static_cast(bits_); + bits_ >>= 8; + nbits_ = (nbits_ > 8) ? (nbits_ - 8) : 0; + } + bits_ = 0; + + if (n > 0) { + output_.insert(output_.end(), bytes_.begin(), bytes_.begin() + n); + } + nbytes_ = 0; +} + +void HuffmanBitWriter::writeBytes(const uint8_t* bytes, size_t len) { + uint8_t n = nbytes_; + while (nbits_ != 0) { + bytes_[n++] = static_cast(bits_); + bits_ >>= 8; + nbits_ -= 8; + } + if (n != 0) { + output_.insert(output_.end(), bytes_.begin(), bytes_.begin() + n); + } + nbytes_ = 0; + output_.insert(output_.end(), bytes, bytes + len); +} + +void HuffmanBitWriter::writeStoredHeader(int length, bool isEof) { + if (lastHeader_ > 0) { + writeCode(literalEncoding_->codes[END_BLOCK_MARKER]); + lastHeader_ = 0; + } + + if (length == 0 && isEof) { + writeFixedHeader(isEof); + writeBits(0, 7); + flush(); + return; + } + + writeBits(isEof ? 1 : 0, 3); + flush(); + writeBits(length, 16); + writeBits(~length & 0xFFFF, 16); +} + +void HuffmanBitWriter::writeFixedHeader(bool isEof) { + if (lastHeader_ > 0) { + writeCode(literalEncoding_->codes[END_BLOCK_MARKER]); + lastHeader_ = 0; + } + writeBits(isEof ? 3 : 2, 3); +} + +void HuffmanBitWriter::indexTokens(Tokens* t, bool alwaysEOB) { + std::memcpy(literalFreq_.data(), t->litHist.data(), 256 * sizeof(uint16_t)); + std::memcpy(literalFreq_.data() + 256, t->extraHist.data(), 32 * sizeof(uint16_t)); + offsetFreq_ = t->offHist; + if (t->n != 0 && alwaysEOB) { + literalFreq_[END_BLOCK_MARKER] = 1; + } +} + +void HuffmanBitWriter::generate() { + literalEncoding_->generate(literalFreq_.data(), LITERAL_COUNT, 15); + offsetEncoding_->generate(offsetFreq_.data(), OFFSET_CODE_COUNT, 15); +} + +int HuffmanBitWriter::extraBitSize() { + int total = 0; + for (int i = 0; i < LITERAL_COUNT - 257; ++i) { + total += static_cast(literalFreq_[257 + i]) * lengthExtraBits[i & 31]; + } + for (int i = 0; i < OFFSET_CODE_COUNT; ++i) { + total += static_cast(offsetFreq_[i]) * offsetExtraBits[i & 31]; + } + return total; +} + +int HuffmanBitWriter::fixedSize(int extraBits) { + return 3 + fixedLiteralEncoding->bitLength(literalFreq_.data(), LITERAL_COUNT) + + fixedOffsetEncoding->bitLength(offsetFreq_.data(), OFFSET_CODE_COUNT) + extraBits; +} + +int HuffmanBitWriter::storedSize(const uint8_t* input, size_t len, bool* storable) { + *storable = (input != nullptr && len <= MAX_STORE_BLOCK_SIZE); + return *storable ? (static_cast(len) + 5) * 8 : 0; +} + +void PZIP_HOT HuffmanBitWriter::writeTokens(const Token* tokens, size_t n, + const HCode* leCodes, const HCode* oeCodes) { + if (n == 0) return; + + const HCode* lits = leCodes; + const HCode* offs = oeCodes; + const HCode* lengths = leCodes + LENGTH_CODES_START; + + uint64_t bits = bits_; + uint8_t nbits = nbits_; + uint8_t nbytes = nbytes_; + + bool deferEOB = (tokens[n - 1] == static_cast(END_BLOCK_MARKER)); + if (deferEOB) n--; + + for (size_t i = 0; i < n; ++i) { + Token t = tokens[i]; + + if (PZIP_LIKELY(t < 256)) { + HCode c = lits[t]; + bits |= c.code64() << (nbits & 63); + nbits += c.len(); + if (PZIP_UNLIKELY(nbits >= 48)) { + store64(bytes_.data(), nbytes, bits); + bits >>= 48; + nbits -= 48; + nbytes += 6; + if (nbytes >= BUFFER_FLUSH_SIZE) { + output_.insert(output_.end(), bytes_.begin(), bytes_.begin() + nbytes); + nbytes = 0; + } + } + continue; + } + + uint32_t length = (t >> LENGTH_SHIFT) & 0xFF; + uint8_t lengthCode = lengthCodes1[length] - 1; + + HCode c = lengths[lengthCode]; + bits |= c.code64() << (nbits & 63); + nbits += c.len(); + if (nbits >= 48) { + store64(bytes_.data(), nbytes, bits); + bits >>= 48; + nbits -= 48; + nbytes += 6; + if (nbytes >= BUFFER_FLUSH_SIZE) { + output_.insert(output_.end(), bytes_.begin(), bytes_.begin() + nbytes); + nbytes = 0; + } + } + + if (lengthCode >= 8) { + uint8_t extraLengthBits = lengthExtraBits[lengthCode]; + int32_t extraLength = static_cast(length) - lengthBase[lengthCode]; + bits |= static_cast(extraLength) << (nbits & 63); + nbits += extraLengthBits; + if (nbits >= 48) { + store64(bytes_.data(), nbytes, bits); + bits >>= 48; + nbits -= 48; + nbytes += 6; + if (nbytes >= BUFFER_FLUSH_SIZE) { + output_.insert(output_.end(), bytes_.begin(), bytes_.begin() + nbytes); + nbytes = 0; + } + } + } + + uint32_t offset = t & OFFSET_MASK; + uint32_t offCode = (offset >> 16) & 31; + offset &= 0xFFFF; + + c = offs[offCode]; + bits |= c.code64() << (nbits & 63); + nbits += c.len(); + if (nbits >= 48) { + store64(bytes_.data(), nbytes, bits); + bits >>= 48; + nbits -= 48; + nbytes += 6; + if (nbytes >= BUFFER_FLUSH_SIZE) { + output_.insert(output_.end(), bytes_.begin(), bytes_.begin() + nbytes); + nbytes = 0; + } + } + + if (offCode >= 4) { + uint32_t offsetComb = offsetCombined[offCode]; + bits |= static_cast((offset - (offsetComb >> 8)) & 0xFFFF) << (nbits & 63); + nbits += static_cast(offsetComb); + if (nbits >= 48) { + store64(bytes_.data(), nbytes, bits); + bits >>= 48; + nbits -= 48; + nbytes += 6; + if (nbytes >= BUFFER_FLUSH_SIZE) { + output_.insert(output_.end(), bytes_.begin(), bytes_.begin() + nbytes); + nbytes = 0; + } + } + } + } + + bits_ = bits; + nbits_ = nbits; + nbytes_ = nbytes; + + if (deferEOB) { + writeCode(leCodes[END_BLOCK_MARKER]); + } +} + +void HuffmanBitWriter::writeBlock(Tokens* tokens, bool eof, + const uint8_t* input, size_t inputLen) { + tokens->addEOB(); + + if (lastHeader_ > 0) { + writeCode(literalEncoding_->codes[END_BLOCK_MARKER]); + lastHeader_ = 0; + } + + indexTokens(tokens, false); + generate(); + + bool storable; + int ssize = storedSize(input, inputLen, &storable); + int extraBits = storable ? extraBitSize() : 0; + int size = fixedSize(extraBits); + + if (storable && ssize <= size) { + writeStoredHeader(static_cast(inputLen), eof); + writeBytes(input, inputLen); + return; + } + + writeFixedHeader(eof); + writeTokens(tokens->tokens.data(), tokens->n, + fixedLiteralEncoding->codes.data(), fixedOffsetEncoding->codes.data()); +} + +void HuffmanBitWriter::writeBlockDynamic(Tokens* tokens, bool eof, + const uint8_t* input, size_t inputLen, bool sync) { + // 每个块都需要 EOB 标记 + tokens->addEOB(); + + if (lastHeader_ > 0) { + writeCode(literalEncoding_->codes[END_BLOCK_MARKER]); + lastHeader_ = 0; + } + + indexTokens(tokens, true); + + bool storable; + int ssize = storedSize(input, inputLen, &storable); + int extraBits = storable ? extraBitSize() : 0; + + generate(); + int size = fixedSize(extraBits); + + if (storable && ssize <= size) { + writeStoredHeader(static_cast(inputLen), eof); + writeBytes(input, inputLen); + return; + } + + writeFixedHeader(eof); + writeTokens(tokens->tokens.data(), tokens->n, + fixedLiteralEncoding->codes.data(), fixedOffsetEncoding->codes.data()); +} + +void HuffmanBitWriter::histogram(const uint8_t* input, size_t len) { + for (size_t i = 0; i < len; i++) { + literalFreq_[input[i]]++; + } +} + +// codegenOrder 表 - 来自 RFC 1951 +static const uint8_t codegenOrder[19] = { + 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 +}; + +std::pair HuffmanBitWriter::headerSize() { + int numCodegens = 19; + while (numCodegens > 4 && codegenFreq_[codegenOrder[numCodegens - 1]] == 0) { + numCodegens--; + } + + int size = 3 + 5 + 5 + 4 + (3 * numCodegens) + + codegenEncoding_->bitLength(codegenFreq_.data(), 19) + + static_cast(codegenFreq_[16]) * 2 + + static_cast(codegenFreq_[17]) * 3 + + static_cast(codegenFreq_[18]) * 7; + + return {size, numCodegens}; +} + +void HuffmanBitWriter::generateCodegen(int numLiterals, int numOffsets, + HuffmanEncoder* litEnc, HuffmanEncoder* offEnc) { + codegenFreq_.fill(0); + + size_t n = 0; + // 写入字面量码长 + for (int i = 0; i < numLiterals; i++) { + uint8_t bits = litEnc->codes[i].len(); + codegen_[n++] = bits; + codegenFreq_[bits]++; + } + // 写入偏移量码长 + for (int i = 0; i < numOffsets; i++) { + uint8_t bits = offEnc->codes[i].len(); + codegen_[n++] = bits; + codegenFreq_[bits]++; + } +} + +int HuffmanBitWriter::codegens() { + // 返回需要编码的 codegen 数量 + int n = 19; + while (n > 4 && codegenFreq_[codegenOrder[n - 1]] == 0) { + n--; + } + return n; +} + +void HuffmanBitWriter::writeDynamicHeader(int numLiterals, int numOffsets, int numCodegens, bool isEof) { + // BFINAL + BTYPE + writeBits(isEof ? 5 : 4, 3); // BTYPE = 10 (dynamic) + + // HLIT, HDIST, HCLEN + writeBits(numLiterals - 257, 5); + writeBits(numOffsets - 1, 5); + writeBits(numCodegens - 4, 4); + + // 写入 codegen 码长 + for (int i = 0; i < numCodegens; i++) { + writeBits(codegenEncoding_->codes[codegenOrder[i]].len(), 3); + } + + // 写入字面量和偏移量码长 + for (int i = 0; i < numLiterals + numOffsets; i++) { + writeCode(codegenEncoding_->codes[codegen_[i]]); + } +} + +void HuffmanBitWriter::writeBlockHuff(bool eof, const uint8_t* input, size_t inputLen, bool sync) { + // 清空频率表 + literalFreq_.fill(0); + if (!lastHuffMan_) { + offsetFreq_.fill(0); + } + + constexpr int numLiterals = END_BLOCK_MARKER + 1; + constexpr int numOffsets = 1; + constexpr int guessHeaderSizeBits = 70 * 8; + + // 统计字面量频率 + histogram(input, inputLen); + + bool storable; + int ssize = storedSize(input, inputLen, &storable); + + // 快速检测不可压缩内容 + if (storable && inputLen > 1024) { + double abs_val = 0; + double avg = static_cast(inputLen) / 256.0; + double max_val = static_cast(inputLen * 2); + + for (int i = 0; i < 256; i++) { + double diff = static_cast(literalFreq_[i]) - avg; + abs_val += diff * diff; + if (abs_val > max_val) { + break; + } + } + + if (abs_val < max_val) { + // 数据分布均匀,不可压缩 + writeStoredHeader(static_cast(inputLen), eof); + writeBytes(input, inputLen); + return; + } + } + + literalFreq_[END_BLOCK_MARKER] = 1; + tmpLitEncoding_->generate(literalFreq_.data(), numLiterals, 15); + int estBits = tmpLitEncoding_->bitLength(literalFreq_.data(), numLiterals); + + if (estBits < INT32_MAX) { + estBits += lastHeader_; + if (lastHeader_ == 0) { + estBits += guessHeaderSizeBits; + } + estBits += estBits >> logNewTablePenalty_; + } + + // 如果 stored 更好,使用 stored + if (storable && ssize <= estBits) { + writeStoredHeader(static_cast(inputLen), eof); + writeBytes(input, inputLen); + return; + } + + // 检查是否可以复用上一个编码表 + if (lastHeader_ > 0) { + // 必须包含 END_BLOCK_MARKER (256) 以保持与 estBits 比较的一致性 + int reuseSize = literalEncoding_->bitLength(literalFreq_.data(), numLiterals); + + if (estBits < reuseSize) { + // 使用新表 + writeCode(literalEncoding_->codes[END_BLOCK_MARKER]); + lastHeader_ = 0; + } + } + + if (lastHeader_ == 0) { + // 使用临时编码器 + std::swap(literalEncoding_, tmpLitEncoding_); + + // 生成 codegen + // 使用预定义的静态距离编码表(Huffman-only 模式不使用距离码,但 DEFLATE 要求至少有一个有效距离符号) + generateCodegen(numLiterals, numOffsets, literalEncoding_.get(), fixedOffsetEncoding.get()); + codegenEncoding_->generate(codegenFreq_.data(), 19, 7); + int numCodegens = codegens(); + + // 写入动态头 + writeDynamicHeader(numLiterals, numOffsets, numCodegens, eof); + lastHuffMan_ = true; + lastHeader_ = headerSize().first; + } + + // 写入字面量 + const auto& encoding = literalEncoding_->codes; + for (size_t i = 0; i < inputLen; i++) { + writeCode(encoding[input[i]]); + } + + // 写入 EOB(如果需要) + if (eof || sync) { + writeCode(literalEncoding_->codes[END_BLOCK_MARKER]); + lastHeader_ = 0; + lastHuffMan_ = false; + } +} + +// ============================================================================ +// FastDeflate 实现 +// ============================================================================ + +FastDeflate::FastDeflate(CompressionLevel level) + : level_(level) + , encoderL1_(std::make_unique()) + , encoderL4_(std::make_unique()) + , writer_(std::make_unique()) { + // Level 1-3 使用 L1 编码器(最快) + // Level 4+ 使用 L4 编码器(更好压缩率) + useL1_ = (static_cast(level) <= 3); +} + +void FastDeflate::reset() { + encoderL1_->reset(); + encoderL4_->reset(); + writer_->reset(); + tokens_.reset(); +} + +size_t FastDeflate::compress(const uint8_t* input, size_t inputSize, + std::vector& output) { + reset(); + + if (inputSize == 0) { + writer_->writeStoredHeader(0, true); + writer_->flush(); + output = std::move(writer_->data()); + return output.size(); + } + + size_t pos = 0; + while (pos < inputSize) { + size_t blockSize = std::min(inputSize - pos, MAX_STORE_BLOCK_SIZE); + bool isLast = (pos + blockSize >= inputSize); + + // 参照 Go klauspost/compress storeFast: + // 先 encode,再根据 tokens.n 决定使用哪种模式 + tokens_.reset(); + + if (useL1_) { + encoderL1_->encode(&tokens_, input + pos, blockSize); + } else { + encoderL4_->encode(&tokens_, input + pos, blockSize); + } + + // 三级判断(参照 klauspost/compress storeFast deflate.go:738-748) + if (tokens_.n == 0) { + // If we made zero matches, store the block as is. + writer_->writeStoredHeader(static_cast(blockSize), isLast); + writer_->writeBytes(input + pos, blockSize); + } else if (static_cast(tokens_.n) > blockSize - (blockSize >> 4)) { + // If we removed less than 1/16th, huffman compress the block. + writer_->writeBlockHuff(isLast, input + pos, blockSize, isLast); + } else { + writer_->writeBlockDynamic(&tokens_, isLast, input + pos, blockSize, isLast); + } + + pos += blockSize; + } + + writer_->flush(); + output = std::move(writer_->data()); + return output.size(); +} + +// ============================================================================ +// 便捷函数 +// ============================================================================ + +size_t deflateCompress(const uint8_t* input, size_t inputSize, + std::vector& output, + CompressionLevel level) { + FastDeflate deflate(level); + return deflate.compress(input, inputSize, output); +} + +// ============================================================================ +// FlateWriter 实现(参照 Go klauspost/compress flate compressor) +// ============================================================================ + +FlateWriter::FlateWriter(WriteFunc output, CompressionLevel level) + : output_(std::move(output)) + , window_(MAX_STORE_BLOCK_SIZE) // 64KB window + , level_(level) + , useL1_(static_cast(level) <= 3) + , encoderL1_(std::make_unique()) + , encoderL4_(std::make_unique()) + , writer_(std::make_unique()) { + // 根据压缩级别调整新表惩罚因子 + // Level 1-3: 更高惩罚(减少表切换,更快) + // Level 4-6: 中等惩罚 + // Level 7-9: 更低惩罚(允许更多表切换,更好压缩率) + int penalty = 7; // 默认值 + int lvl = static_cast(level); + if (lvl <= 3) { + penalty = 8; + } else if (lvl >= 7) { + penalty = 6; + } + writer_->setLogNewTablePenalty(penalty); +} + +void FlateWriter::reset(WriteFunc output) { + output_ = std::move(output); + windowEnd_ = 0; + encoder()->reset(); + writer_->reset(); + tokens_.reset(); +} + +size_t FlateWriter::fillBlock(const uint8_t* data, size_t size) { + size_t n = std::min(size, window_.size() - windowEnd_); + std::memcpy(window_.data() + windowEnd_, data, n); + windowEnd_ += n; + return n; +} + +// 将 writer_ 中的数据刷新到输出目标(当缓冲区足够大时) +void FlateWriter::flushOutput() { + auto& buf = writer_->data(); + // 只在缓冲区超过 256KB 时才刷新,减少系统调用次数 + if (buf.size() >= 256 * 1024 && output_) { + output_(buf.data(), buf.size()); + buf.clear(); + } +} + +// 强制刷新所有数据 +void FlateWriter::forceFlush() { + auto& buf = writer_->data(); + if (!buf.empty() && output_) { + output_(buf.data(), buf.size()); + buf.clear(); + } +} + +// 参照 Go compressor.storeFast +void FlateWriter::storeFast() { + if (windowEnd_ == 0) { + return; + } + + if (windowEnd_ < 128) { + if (windowEnd_ <= 32) { + writer_->writeStoredHeader(static_cast(windowEnd_), false); + writer_->writeBytes(window_.data(), windowEnd_); + } else { + writer_->writeBlockHuff(false, window_.data(), windowEnd_, false); + } + tokens_.reset(); + windowEnd_ = 0; + encoder()->reset(); + flushOutput(); // 立即刷新到输出 + return; + } + + encoder()->encode(&tokens_, window_.data(), windowEnd_); + + if (tokens_.n == 0) { + writer_->writeStoredHeader(static_cast(windowEnd_), false); + writer_->writeBytes(window_.data(), windowEnd_); + } else if (static_cast(tokens_.n) > windowEnd_ - (windowEnd_ >> 4)) { + writer_->writeBlockHuff(false, window_.data(), windowEnd_, false); + } else { + writer_->writeBlockDynamic(&tokens_, false, window_.data(), windowEnd_, false); + } + + tokens_.reset(); + windowEnd_ = 0; + flushOutput(); +} + +// 参照 Go compressor.write +size_t FlateWriter::write(const uint8_t* data, size_t size) { + size_t total = size; + while (size > 0) { + if (windowEnd_ == window_.size()) { + storeFast(); + } + size_t n = fillBlock(data, size); + data += n; + size -= n; + } + return total; +} + +// 参照 Go compressor.Close +void FlateWriter::close() { + if (windowEnd_ > 0) { + encoder()->encode(&tokens_, window_.data(), windowEnd_); + + if (tokens_.n == 0) { + writer_->writeStoredHeader(static_cast(windowEnd_), true); + writer_->writeBytes(window_.data(), windowEnd_); + } else if (static_cast(tokens_.n) > windowEnd_ - (windowEnd_ >> 4)) { + writer_->writeBlockHuff(true, window_.data(), windowEnd_, true); + } else { + writer_->writeBlockDynamic(&tokens_, true, window_.data(), windowEnd_, true); + } + } else { + writer_->writeStoredHeader(0, true); + } + + writer_->flush(); + forceFlush(); // 强制刷新所有剩余数据 +} + +} // namespace pzip diff --git a/src/pzip/src/file_task.cpp b/src/pzip/src/file_task.cpp new file mode 100644 index 000000000..b44453e55 --- /dev/null +++ b/src/pzip/src/file_task.cpp @@ -0,0 +1,307 @@ +// 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/file_task.h" +#include "pzip/utils.h" +#include +#include + +namespace pzip { + +// ============================================================================ +// FileTask 实现 +// ============================================================================ + +FileTask::FileTask() { + buffer_.resize(DEFAULT_BUFFER_SIZE); +} + +FileTask::~FileTask() { + // 清理临时文件 + if (overflow_) { + overflow_->close(); + if (!overflowPath_.empty() && fs::exists(overflowPath_)) { + std::error_code ec; + fs::remove(overflowPath_, ec); + } + } + + // 清理压缩器(由外部管理,这里不释放) +} + +FileTask::FileTask(FileTask&& other) noexcept + : path(std::move(other.path)) + , status(other.status) + , fileSize(other.fileSize) + , header(std::move(other.header)) + , isSymlink(other.isSymlink) + , symlinkTarget(std::move(other.symlinkTarget)) + , streamFromSource(other.streamFromSource) + , compressor(other.compressor) + , buffer_(std::move(other.buffer_)) + , bufferUsed_(other.bufferUsed_) + , 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 { + if (this != &other) { + path = std::move(other.path); + status = other.status; + fileSize = other.fileSize; + header = std::move(other.header); + isSymlink = other.isSymlink; + symlinkTarget = std::move(other.symlinkTarget); + streamFromSource = other.streamFromSource; + compressor = other.compressor; + buffer_ = std::move(other.buffer_); + bufferUsed_ = other.bufferUsed_; + 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; +} + +Error FileTask::reset(const fs::path& filePath, const fs::path& relativeTo) { + // 清理之前的状态 + if (overflow_) { + overflow_->close(); + if (!overflowPath_.empty() && fs::exists(overflowPath_)) { + std::error_code ec; + fs::remove(overflowPath_, ec); + } + overflow_.reset(); + overflowPath_.clear(); + } + + bufferUsed_ = 0; + written_ = 0; + isSymlink = false; + symlinkTarget.clear(); + streamFromSource = false; + encryptedData.clear(); + salt.clear(); + passwordVerification = 0; + authCode.fill(0); + + // 设置新文件信息 + path = filePath; + + std::error_code ec; + // 使用 symlink_status 不跟随符号链接 + status = fs::symlink_status(path, ec); + if (ec) { + return Error(ErrorCode::FILE_NOT_FOUND, "Cannot stat file: " + path.string()); + } + + // 检测符号链接 + isSymlink = fs::is_symlink(status); + if (isSymlink) { + // 读取符号链接目标 + symlinkTarget = fs::read_symlink(path, ec).string(); + if (ec) { + return Error(ErrorCode::FILE_READ_ERROR, "Cannot read symlink target: " + path.string()); + } + fileSize = symlinkTarget.size(); + } else if (fs::is_regular_file(status)) { + fileSize = fs::file_size(path, ec); + if (ec) { + return Error(ErrorCode::FILE_READ_ERROR, "Cannot get file size: " + path.string()); + } + } else { + fileSize = 0; + } + + // 初始化头信息 + header = ZipFileHeader(); + + // 设置相对路径名 + // 注意:fs::relative() 会解析符号链接,所以使用 path 迭代器手动计算 + if (!relativeTo.empty()) { + // 使用 path 迭代器跳过共同前缀 + auto pathIt = path.begin(); + auto relIt = relativeTo.begin(); + + while (pathIt != path.end() && relIt != relativeTo.end() && *pathIt == *relIt) { + ++pathIt; + ++relIt; + } + + // 构建相对路径 + fs::path relPath; + for (; pathIt != path.end(); ++pathIt) { + relPath /= *pathIt; + } + + if (!relPath.empty()) { + header.name = utils::toZipPath(relPath); + } else { + header.name = utils::toZipPath(path.filename()); + } + } else { + header.name = utils::toZipPath(path.filename()); + } + + // 如果是目录,确保名称以 / 结尾 + if (fs::is_directory(status) && !header.name.empty() && header.name.back() != '/') { + header.name += '/'; + } + + // 设置修改时间 + auto modTime = utils::getModTime(path); + + // 设置文件权限(Unix) + struct stat st; + if (stat(path.c_str(), &st) == 0) { + header.externalAttr = utils::modeToZipAttr(st.st_mode); + } + + return Error(); +} + +size_t FileTask::write(const uint8_t* data, size_t size) { + size_t totalWritten = 0; + + // 先写入内存缓冲区 + size_t available = buffer_.size() - bufferUsed_; + if (available > 0) { + size_t toWrite = std::min(available, size); + std::memcpy(buffer_.data() + bufferUsed_, data, toWrite); + bufferUsed_ += toWrite; + totalWritten += toWrite; + data += toWrite; + size -= toWrite; + } + + // 剩余数据写入临时文件 + if (size > 0) { + if (!overflow_) { + overflowPath_ = utils::createTempFile("pzip-overflow"); + overflow_ = std::make_unique( + overflowPath_, + std::ios::in | std::ios::out | std::ios::binary | std::ios::trunc + ); + + if (!overflow_->is_open()) { + overflow_.reset(); + return totalWritten; + } + } + + if (overflow_ && overflow_->is_open()) { + overflow_->write(reinterpret_cast(data), size); + if (overflow_->good()) { + totalWritten += size; + } + } + } + + written_ += totalWritten; + return totalWritten; +} + +void FileTask::readCompressedData(std::function callback) { + // Store 模式:直接从源文件流式读取,边读边算 CRC + if (streamFromSource) { + std::ifstream file(path, std::ios::binary); + if (!file.is_open()) return; + + uint32_t crc = 0; + std::vector readBuf(READ_BUFFER_SIZE); + while (file.good() && !file.eof()) { + file.read(reinterpret_cast(readBuf.data()), readBuf.size()); + auto bytesRead = file.gcount(); + if (bytesRead > 0) { + crc = utils::crc32Update(crc, readBuf.data(), bytesRead); + callback(readBuf.data(), bytesRead); + } + } + header.crc32 = crc; + return; + } + + // 先读取内存缓冲区 + if (bufferUsed_ > 0) { + callback(buffer_.data(), bufferUsed_); + } + + // 再读取溢出文件 + if (overflow_ && overflow_->is_open()) { + overflow_->flush(); + overflow_->clear(); + overflow_->seekg(0, std::ios::beg); + + std::vector readBuf(READ_BUFFER_SIZE); + while (overflow_->good() && !overflow_->eof()) { + overflow_->read(reinterpret_cast(readBuf.data()), readBuf.size()); + auto bytesRead = overflow_->gcount(); + if (bytesRead > 0) { + callback(readBuf.data(), bytesRead); + } + } + } +} + +// ============================================================================ +// FileTaskPool 实现 +// ============================================================================ + +FileTaskPool& FileTaskPool::instance() { + static FileTaskPool pool; + return pool; +} + +std::unique_ptr FileTaskPool::acquire() { + std::lock_guard lock(mutex_); + + if (!pool_.empty()) { + auto task = std::move(pool_.back()); + pool_.pop_back(); + return task; + } + + return std::make_unique(); +} + +void FileTaskPool::release(std::unique_ptr task) { + if (!task) return; + + std::lock_guard lock(mutex_); + + // 限制池大小 + if (pool_.size() < 32) { + pool_.push_back(std::move(task)); + } +} + +} // namespace pzip + + diff --git a/src/pzip/src/utils.cpp b/src/pzip/src/utils.cpp new file mode 100644 index 000000000..65e6a7d59 --- /dev/null +++ b/src/pzip/src/utils.cpp @@ -0,0 +1,283 @@ +// 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/utils.h" +#include +#include +#include +#include +#include +#include +#include + +#if defined(__aarch64__) +#include +#include +#ifdef __ARM_FEATURE_CRC32 +#include +#define PZIP_HAS_ARM_CRC32_COMPILE 1 +#else +#define PZIP_HAS_ARM_CRC32_COMPILE 0 +#endif +#endif + +namespace pzip { +namespace utils { + +std::pair detectUTF8(const std::string& s) { + bool valid = true; + bool require = false; + + size_t i = 0; + while (i < s.size()) { + unsigned char c = s[i]; + + if (c < 0x20 || c > 0x7d || c == 0x5c) { + // 需要检查是否是有效的多字节 UTF-8 + if (c < 0x80) { + // ASCII 控制字符或反斜杠 + require = true; + } else if ((c & 0xE0) == 0xC0) { + // 2 字节序列 + if (i + 1 >= s.size() || (s[i+1] & 0xC0) != 0x80) { + valid = false; + break; + } + i += 1; + require = true; + } else if ((c & 0xF0) == 0xE0) { + // 3 字节序列 + if (i + 2 >= s.size() || + (s[i+1] & 0xC0) != 0x80 || + (s[i+2] & 0xC0) != 0x80) { + valid = false; + break; + } + i += 2; + require = true; + } else if ((c & 0xF8) == 0xF0) { + // 4 字节序列 + if (i + 3 >= s.size() || + (s[i+1] & 0xC0) != 0x80 || + (s[i+2] & 0xC0) != 0x80 || + (s[i+3] & 0xC0) != 0x80) { + valid = false; + break; + } + i += 3; + require = true; + } else { + valid = false; + break; + } + } + i++; + } + + return {valid, require}; +} + +std::string toZipPath(const fs::path& path) { + std::string result = path.generic_string(); + + // 确保使用正斜杠 + for (char& c : result) { + if (c == '\\') c = '/'; + } + + // 移除开头的斜杠(防止归档条目名为绝对路径) + while (!result.empty() && result[0] == '/') { + result = result.substr(1); + } + + // 安全加固:移除路径遍历组件(".."),防止 Zip Slip 攻击 + // 归档条目名中不应包含 "../",否则解压时可能写入归档目录之外的位置 + std::string sanitized; + size_t pos = 0; + while (pos < result.size()) { + size_t slashPos = result.find('/', pos); + std::string component = (slashPos == std::string::npos) + ? result.substr(pos) + : result.substr(pos, slashPos - pos); + + if (component != ".." && !component.empty()) { + if (!sanitized.empty()) { + sanitized += '/'; + } + sanitized += component; + } + // 空组件(连续斜杠)和 ".." 组件被跳过 + + if (slashPos == std::string::npos) { + break; + } + pos = slashPos + 1; + } + + return sanitized; +} + +fs::path fromZipPath(const std::string& zipPath) { + std::string result = zipPath; + + // 在 Windows 上转换斜杠 +#ifdef _WIN32 + for (char& c : result) { + if (c == '/') c = '\\'; + } +#endif + + return fs::path(result); +} + +#if defined(__aarch64__) && PZIP_HAS_ARM_CRC32_COMPILE +static uint32_t crc32_arm_hw(uint32_t crc, const uint8_t* data, size_t size) { + uint32_t c = crc ^ 0xFFFFFFFF; + + // 逐字节对齐到 8 字节边界 + while (size > 0 && (reinterpret_cast(data) & 7)) { + c = __crc32b(c, *data++); + size--; + } + + // 每次处理 8 字节 + while (size >= 64) { + c = __crc32d(c, *reinterpret_cast(data)); + c = __crc32d(c, *reinterpret_cast(data + 8)); + c = __crc32d(c, *reinterpret_cast(data + 16)); + c = __crc32d(c, *reinterpret_cast(data + 24)); + c = __crc32d(c, *reinterpret_cast(data + 32)); + c = __crc32d(c, *reinterpret_cast(data + 40)); + c = __crc32d(c, *reinterpret_cast(data + 48)); + c = __crc32d(c, *reinterpret_cast(data + 56)); + data += 64; + size -= 64; + } + + while (size >= 8) { + c = __crc32d(c, *reinterpret_cast(data)); + data += 8; + size -= 8; + } + + while (size > 0) { + c = __crc32b(c, *data++); + size--; + } + + return c ^ 0xFFFFFFFF; +} + +static bool hasArmCrc32() { + static bool checked = false; + static bool supported = false; + if (!checked) { + supported = (getauxval(AT_HWCAP) & HWCAP_CRC32) != 0; + checked = true; + } + return supported; +} +#endif + +uint32_t crc32(const uint8_t* data, size_t size) { +#if defined(__aarch64__) && PZIP_HAS_ARM_CRC32_COMPILE + if (hasArmCrc32()) { + return crc32_arm_hw(0, data, size); + } +#endif + return ::crc32(0L, data, size); +} + +uint32_t crc32Update(uint32_t crc, const uint8_t* data, size_t size) { +#if defined(__aarch64__) && PZIP_HAS_ARM_CRC32_COMPILE + if (hasArmCrc32()) { + return crc32_arm_hw(crc, data, size); + } +#endif + return ::crc32(crc, data, size); +} + +time_t getModTime(const fs::path& path) { + struct stat st; + if (stat(path.c_str(), &st) == 0) { + return st.st_mtime; + } + return time(nullptr); +} + +bool setModTime(const fs::path& path, time_t modTime) { + struct utimbuf times; + times.actime = modTime; + times.modtime = modTime; + return utime(path.c_str(), ×) == 0; +} + +uint32_t modeToZipAttr(mode_t mode) { + // ZIP 外部属性:高 16 位是 Unix 模式,低 16 位是 DOS 属性 + return static_cast(mode) << 16; +} + +mode_t zipAttrToMode(uint32_t attr) { + // 提取高 16 位作为 Unix 模式 + return static_cast(attr >> 16); +} + +fs::path createTempFile(const std::string& prefix) { + // 获取临时目录 + fs::path tempDir = fs::temp_directory_path(); + + // 生成随机文件名 + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> dis(0, 999999); + + std::ostringstream oss; + oss << prefix << "-" << std::setfill('0') << std::setw(6) << dis(gen); + + return tempDir / oss.str(); +} + +std::string formatSize(uint64_t size) { + const char* units[] = {"B", "KB", "MB", "GB", "TB"}; + int unit = 0; + double dsize = static_cast(size); + + while (dsize >= 1024.0 && unit < 4) { + dsize /= 1024.0; + unit++; + } + + std::ostringstream oss; + if (unit == 0) { + oss << size << " " << units[unit]; + } else { + oss << std::fixed << std::setprecision(1) << dsize << " " << units[unit]; + } + + return oss.str(); +} + +std::string formatTime(double seconds) { + std::ostringstream oss; + + if (seconds < 60) { + oss << std::fixed << std::setprecision(1) << seconds << "s"; + } else if (seconds < 3600) { + int mins = static_cast(seconds) / 60; + int secs = static_cast(seconds) % 60; + oss << mins << "m " << secs << "s"; + } else { + int hours = static_cast(seconds) / 3600; + int mins = (static_cast(seconds) % 3600) / 60; + oss << hours << "h " << mins << "m"; + } + + return oss.str(); +} + +} // namespace utils +} // namespace pzip + + diff --git a/src/pzip/src/worker_pool.cpp b/src/pzip/src/worker_pool.cpp new file mode 100644 index 000000000..e4c989088 --- /dev/null +++ b/src/pzip/src/worker_pool.cpp @@ -0,0 +1,16 @@ +// 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/worker_pool.h" +#include "pzip/file_task.h" +#include "pzip/extractor.h" + +namespace pzip { + +template class WorkerPool; +template class WorkerPool; + +} // namespace pzip + diff --git a/src/pzip/src/zip_reader.cpp b/src/pzip/src/zip_reader.cpp new file mode 100644 index 000000000..d0be4b58e --- /dev/null +++ b/src/pzip/src/zip_reader.cpp @@ -0,0 +1,288 @@ +// 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/zip_reader.h" +#include "pzip/utils.h" +#include +#include +#include + +namespace pzip { + +// ZIP 签名常量 +constexpr uint32_t LOCAL_FILE_HEADER_SIG = 0x04034b50; +constexpr uint32_t CENTRAL_DIR_HEADER_SIG = 0x02014b50; +constexpr uint32_t END_OF_CENTRAL_DIR_SIG = 0x06054b50; + +// ============================================================================ +// ZipReader 实现 +// ============================================================================ + +ZipReader::ZipReader(const fs::path& path) : path_(path) {} + +ZipReader::~ZipReader() { + close(); +} + +time_t ZipReader::dosToTime(uint16_t date, uint16_t time) { + struct tm tm = {}; + + tm.tm_sec = (time & 0x1F) * 2; + tm.tm_min = (time >> 5) & 0x3F; + tm.tm_hour = (time >> 11) & 0x1F; + tm.tm_mday = date & 0x1F; + tm.tm_mon = ((date >> 5) & 0x0F) - 1; + tm.tm_year = ((date >> 9) & 0x7F) + 80; + + return mktime(&tm); +} + +Error ZipReader::open() { + if (file_.is_open()) { + return Error(ErrorCode::FILE_OPEN_ERROR, "File already open"); + } + + file_.open(path_, std::ios::binary | std::ios::in); + if (!file_.is_open()) { + return Error(ErrorCode::FILE_OPEN_ERROR, "Cannot open file: " + path_.string()); + } + + // 读取结束记录以获取中央目录位置 + Error err = readEndOfCentralDirectory(); + if (err) { + file_.close(); + return err; + } + + // 读取中央目录 + err = readCentralDirectory(); + if (err) { + file_.close(); + return err; + } + + return Error(); +} + +void ZipReader::close() { + if (file_.is_open()) { + file_.close(); + } + entries_.clear(); +} + +Error ZipReader::readEndOfCentralDirectory() { + // 从文件末尾向前搜索结束签名 + file_.seekg(0, std::ios::end); + auto fileSize = file_.tellg(); + + // 最大搜索范围(包括可能的注释) + const size_t maxSearch = std::min(static_cast(fileSize), size_t(65536 + 22)); + + std::vector buf(maxSearch); + file_.seekg(-static_cast(maxSearch), std::ios::end); + file_.read(reinterpret_cast(buf.data()), maxSearch); + + // 从后向前搜索签名 + int64_t sigOffset = -1; + for (int64_t i = maxSearch - 22; i >= 0; --i) { + if (buf[i] == 0x50 && buf[i+1] == 0x4b && + buf[i+2] == 0x05 && buf[i+3] == 0x06) { + sigOffset = i; + break; + } + } + + if (sigOffset < 0) { + return Error(ErrorCode::INVALID_ARCHIVE, "Cannot find end of central directory"); + } + + // 解析结束记录 + const uint8_t* p = buf.data() + sigOffset + 4; + + auto read16 = [&p]() -> uint16_t { + uint16_t v = p[0] | (p[1] << 8); + p += 2; + return v; + }; + + auto read32 = [&p]() -> uint32_t { + uint32_t v = p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24); + p += 4; + return v; + }; + + /* diskNumber = */ read16(); + /* diskWithCD = */ read16(); + /* entriesOnDisk = */ read16(); + totalEntries_ = read16(); + centralDirSize_ = read32(); + centralDirOffset_ = read32(); + uint16_t commentLen = read16(); + + if (commentLen > 0 && sigOffset + 22 + commentLen <= maxSearch) { + comment_.assign(reinterpret_cast(p), commentLen); + } + + return Error(); +} + +Error ZipReader::readCentralDirectory() { + entries_.clear(); + entries_.reserve(totalEntries_); + + file_.seekg(centralDirOffset_, std::ios::beg); + + for (uint32_t i = 0; i < totalEntries_; ++i) { + // 读取中央目录头 + uint8_t header[46]; + file_.read(reinterpret_cast(header), 46); + + if (!file_.good()) { + return Error(ErrorCode::FILE_READ_ERROR, "Failed to read central directory"); + } + + // 验证签名 + uint32_t sig = header[0] | (header[1] << 8) | (header[2] << 16) | (header[3] << 24); + if (sig != CENTRAL_DIR_HEADER_SIG) { + return Error(ErrorCode::INVALID_ARCHIVE, "Invalid central directory signature"); + } + + ZipEntry entry; + auto& h = entry.header; + + h.versionMadeBy = header[4] | (header[5] << 8); + h.versionNeeded = header[6] | (header[7] << 8); + h.flags = header[8] | (header[9] << 8); + h.method = header[10] | (header[11] << 8); + h.modTime = header[12] | (header[13] << 8); + h.modDate = header[14] | (header[15] << 8); + h.crc32 = header[16] | (header[17] << 8) | (header[18] << 16) | (header[19] << 24); + h.compressedSize = header[20] | (header[21] << 8) | (header[22] << 16) | (header[23] << 24); + h.uncompressedSize = header[24] | (header[25] << 8) | (header[26] << 16) | (header[27] << 24); + + uint16_t nameLen = header[28] | (header[29] << 8); + uint16_t extraLen = header[30] | (header[31] << 8); + uint16_t commentLen = header[32] | (header[33] << 8); + + h.externalAttr = header[38] | (header[39] << 8) | (header[40] << 16) | (header[41] << 24); + entry.localHeaderOffset = header[42] | (header[43] << 8) | (header[44] << 16) | (header[45] << 24); + + // 读取文件名 + if (nameLen > 0) { + std::vector nameBuf(nameLen); + file_.read(nameBuf.data(), nameLen); + h.name.assign(nameBuf.data(), nameLen); + } + + // 读取扩展字段 + if (extraLen > 0) { + h.extra.resize(extraLen); + file_.read(reinterpret_cast(h.extra.data()), extraLen); + } + + // 跳过注释 + if (commentLen > 0) { + file_.seekg(commentLen, std::ios::cur); + } + + entries_.push_back(entry); + } + + // 读取每个条目的本地文件头以获取数据偏移 + for (auto& entry : entries_) { + Error err = readLocalFileHeader(entry); + if (err) return err; + } + + return Error(); +} + +Error ZipReader::readLocalFileHeader(ZipEntry& entry) { + file_.seekg(entry.localHeaderOffset, std::ios::beg); + + uint8_t header[30]; + file_.read(reinterpret_cast(header), 30); + + if (!file_.good()) { + return Error(ErrorCode::FILE_READ_ERROR, "Failed to read local file header"); + } + + // 验证签名 + uint32_t sig = header[0] | (header[1] << 8) | (header[2] << 16) | (header[3] << 24); + if (sig != LOCAL_FILE_HEADER_SIG) { + return Error(ErrorCode::INVALID_ARCHIVE, "Invalid local file header signature"); + } + + uint16_t nameLen = header[26] | (header[27] << 8); + uint16_t extraLen = header[28] | (header[29] << 8); + + // 数据偏移 = 本地头偏移 + 30 + 文件名长度 + 扩展字段长度 + entry.dataOffset = entry.localHeaderOffset + 30 + nameLen + extraLen; + + return Error(); +} + +Error ZipReader::readCompressed(const ZipEntry& entry, std::vector& buffer) { + buffer.resize(entry.header.compressedSize); + + file_.seekg(entry.dataOffset, std::ios::beg); + file_.read(reinterpret_cast(buffer.data()), entry.header.compressedSize); + + if (!file_.good()) { + return Error(ErrorCode::FILE_READ_ERROR, "Failed to read compressed data"); + } + + return Error(); +} + +Error ZipReader::readDecompressed(const ZipEntry& entry, std::vector& buffer) { + if (entry.header.method == ZIP_METHOD_STORE) { + // 存储方式,直接读取 + return readCompressed(entry, buffer); + } + + if (entry.header.method != ZIP_METHOD_DEFLATE) { + return Error(ErrorCode::DECOMPRESSION_ERROR, + "Unsupported compression method: " + std::to_string(entry.header.method)); + } + + // 读取压缩数据 + std::vector compressed; + Error err = readCompressed(entry, compressed); + if (err) return err; + + // 解压 + buffer.resize(entry.header.uncompressedSize); + + z_stream strm = {}; + strm.next_in = compressed.data(); + strm.avail_in = compressed.size(); + strm.next_out = buffer.data(); + strm.avail_out = buffer.size(); + + // -MAX_WBITS 表示 raw deflate(无 zlib 头) + if (inflateInit2(&strm, -MAX_WBITS) != Z_OK) { + return Error(ErrorCode::DECOMPRESSION_ERROR, "Failed to initialize decompressor"); + } + + int ret = inflate(&strm, Z_FINISH); + inflateEnd(&strm); + + if (ret != Z_STREAM_END) { + return Error(ErrorCode::DECOMPRESSION_ERROR, "Decompression failed"); + } + + // 验证 CRC32 + uint32_t crc = utils::crc32(buffer.data(), buffer.size()); + if (crc != entry.header.crc32) { + return Error(ErrorCode::DECOMPRESSION_ERROR, "CRC32 mismatch"); + } + + return Error(); +} + +} // namespace pzip + diff --git a/src/pzip/src/zip_writer.cpp b/src/pzip/src/zip_writer.cpp new file mode 100644 index 000000000..8b5ade787 --- /dev/null +++ b/src/pzip/src/zip_writer.cpp @@ -0,0 +1,769 @@ +// 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/zip_writer.h" +#include "pzip/utils.h" +#include +#include + +namespace pzip { + +// ZIP 签名常量 +constexpr uint32_t LOCAL_FILE_HEADER_SIG = 0x04034b50; +constexpr uint32_t DATA_DESCRIPTOR_SIG = 0x08074b50; +constexpr uint32_t CENTRAL_DIR_HEADER_SIG = 0x02014b50; +constexpr uint32_t END_OF_CENTRAL_DIR_SIG = 0x06054b50; +constexpr uint32_t ZIP64_END_OF_CENTRAL_DIR_SIG = 0x06064b50; +constexpr uint32_t ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIG = 0x07064b50; + +// Extra field IDs +constexpr uint16_t EXTRA_ID_ZIP64 = 0x0001; +constexpr uint16_t EXTRA_ID_EXTENDED_TIMESTAMP = 0x5455; + +// ============================================================================ +// ExtendedTimestamp 实现 +// ============================================================================ + +std::vector ExtendedTimestamp::encode() const { + std::vector data; + data.reserve(9); + + // Header ID + data.push_back(EXTRA_ID_EXTENDED_TIMESTAMP & 0xFF); + data.push_back((EXTRA_ID_EXTENDED_TIMESTAMP >> 8) & 0xFF); + + // Data size (1 byte flags + 4 bytes mtime) + data.push_back(5); + data.push_back(0); + + // Flags (bit 0 = mtime present) + data.push_back(0x01); + + // Modification time (4 bytes, little endian) + uint32_t t = static_cast(modTime); + data.push_back(t & 0xFF); + data.push_back((t >> 8) & 0xFF); + data.push_back((t >> 16) & 0xFF); + data.push_back((t >> 24) & 0xFF); + + return data; +} + +ExtendedTimestamp ExtendedTimestamp::decode(const uint8_t* data, size_t size) { + ExtendedTimestamp ts; + ts.modTime = 0; + + if (size >= 5 && (data[0] & 0x01)) { + ts.modTime = data[1] | (data[2] << 8) | (data[3] << 16) | (data[4] << 24); + } + + return ts; +} + +// ============================================================================ +// ZipWriter 实现 +// ============================================================================ + +ZipWriter::ZipWriter(const fs::path& path) : path_(path) {} + +ZipWriter::~ZipWriter() { + if (file_.is_open()) { + close(); + } +} + +Error ZipWriter::open() { + if (file_.is_open()) { + return Error(ErrorCode::FILE_OPEN_ERROR, "File already open"); + } + + // 设置大缓冲区减少系统调用(必须在 open 之前) + writeBuffer_.resize(WRITE_BUFFER_SIZE); + file_.rdbuf()->pubsetbuf(writeBuffer_.data(), writeBuffer_.size()); + + file_.open(path_, std::ios::binary | std::ios::out | std::ios::trunc); + if (!file_.is_open()) { + return Error(ErrorCode::FILE_OPEN_ERROR, "Cannot create file: " + path_.string()); + } + + currentOffset_ = 0; + centralDir_.clear(); + + return Error(); +} + +void ZipWriter::timeToDos(time_t t, uint16_t& date, uint16_t& dosTime) { + struct tm* tm = localtime(&t); + if (!tm) { + date = 0; + dosTime = 0; + return; + } + + // DOS time: bits 0-4 = seconds/2, bits 5-10 = minute, bits 11-15 = hour + dosTime = ((tm->tm_hour & 0x1F) << 11) | + ((tm->tm_min & 0x3F) << 5) | + ((tm->tm_sec / 2) & 0x1F); + + // DOS date: bits 0-4 = day, bits 5-8 = month, bits 9-15 = year - 1980 + date = (((tm->tm_year - 80) & 0x7F) << 9) | + (((tm->tm_mon + 1) & 0x0F) << 5) | + (tm->tm_mday & 0x1F); +} + +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, 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 (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 + // 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 + // 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(extra.size())); + + // Filename + buf.insert(buf.end(), header.name.begin(), header.name.end()); + + // Extra field + 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(); +} + +Error ZipWriter::writeDataDescriptor(const ZipFileHeader& header) { + // 对应 Go archive/zip 的 writeDataDescriptor + // 如果 isZip64(),使用 24 字节(4+4+8+8),否则使用 16 字节(4+4+4+4) + std::vector buf; + + 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); + }; + + auto write64 = [&buf](uint64_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); + buf.push_back((v >> 32) & 0xFF); + buf.push_back((v >> 40) & 0xFF); + buf.push_back((v >> 48) & 0xFF); + buf.push_back((v >> 56) & 0xFF); + }; + + if (header.isZip64()) { + buf.reserve(24); // dataDescriptor64Len + } else { + buf.reserve(16); // dataDescriptorLen + } + + // Signature (de-facto standard, required by OS X) + write32(DATA_DESCRIPTOR_SIG); + + // CRC32 + write32(header.crc32); + + if (header.isZip64()) { + // 64 位大小 + write64(header.compressedSize); + write64(header.uncompressedSize); + } else { + // 32 位大小 + write32(static_cast(header.compressedSize)); + write32(static_cast(header.uncompressedSize)); + } + + file_.write(reinterpret_cast(buf.data()), buf.size()); + + if (!file_.good()) { + return Error(ErrorCode::FILE_WRITE_ERROR, "Failed to write data descriptor"); + } + + currentOffset_ += buf.size(); + return Error(); +} + +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; + entry.localHeaderOffset = localHeaderOffset; + centralDir_.push_back(entry); + 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; + h.method = ZIP_METHOD_STORE; + h.compressedSize = size; + h.uncompressedSize = size; + h.crc32 = utils::crc32(data, size); + h.flags &= ~ZIP_FLAG_DATA_DESCRIPTOR; + + return createRaw(h, [data, size](std::function writer) { + writer(data, size); + }); +} + +Error ZipWriter::createDirectory(const ZipFileHeader& header) { + ZipFileHeader h = header; + + // 确保名称以 / 结尾 + if (!h.name.empty() && h.name.back() != '/') { + h.name += '/'; + } + + h.method = ZIP_METHOD_STORE; + h.compressedSize = 0; + h.uncompressedSize = 0; + h.crc32 = 0; + h.flags &= ~ZIP_FLAG_DATA_DESCRIPTOR; + + return createRaw(h, [](std::function) { + // 目录没有数据 + }); +} + +Error ZipWriter::writeCentralDirectory() { + // 对应 Go archive/zip 的 Close() 中写入中央目录的逻辑 + std::vector buf; + buf.reserve(46); // directoryHeaderLen + + 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); + }; + + auto write64 = [&buf](uint64_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); + buf.push_back((v >> 32) & 0xFF); + buf.push_back((v >> 40) & 0xFF); + buf.push_back((v >> 48) & 0xFF); + buf.push_back((v >> 56) & 0xFF); + }; + + 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 (加密时设置加密标志位) + 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 (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 + // zip64 extra header should be used." + write32(ZIP_UINT32_MAX); // compressed size + write32(ZIP_UINT32_MAX); // uncompressed size + } else { + 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) { + // 28 bytes: 2x uint16 + 3x uint64 + // Header ID + zip64Extra.push_back(EXTRA_ID_ZIP64 & 0xFF); + zip64Extra.push_back((EXTRA_ID_ZIP64 >> 8) & 0xFF); + // Data size = 24 (3x uint64) + zip64Extra.push_back(24); + zip64Extra.push_back(0); + // Uncompressed size + for (int i = 0; i < 8; i++) { + zip64Extra.push_back((h.uncompressedSize >> (i * 8)) & 0xFF); + } + // Compressed size + for (int i = 0; i < 8; i++) { + zip64Extra.push_back((h.compressedSize >> (i * 8)) & 0xFF); + } + // Local header offset + for (int i = 0; i < 8; i++) { + zip64Extra.push_back((entry.localHeaderOffset >> (i * 8)) & 0xFF); + } + } + + // 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()); + currentOffset_ += zip64Extra.size(); + } + } + + if (!file_.good()) { + return Error(ErrorCode::FILE_WRITE_ERROR, "Failed to write central directory"); + } + + return Error(); +} + +Error ZipWriter::writeEndOfCentralDirectory(uint64_t centralDirOffset, uint64_t centralDirSize) { + // 对应 Go archive/zip 的 Close() 中写入 EOCD 的逻辑 + + uint64_t records = centralDir_.size(); + uint64_t size = centralDirSize; + uint64_t offset = centralDirOffset; + + std::vector buf; + + 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); + }; + + auto write64 = [&buf](uint64_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); + buf.push_back((v >> 32) & 0xFF); + buf.push_back((v >> 40) & 0xFF); + buf.push_back((v >> 48) & 0xFF); + buf.push_back((v >> 56) & 0xFF); + }; + + // 检查是否需要 ZIP64 EOCD + // 对应 Go: if records >= uint16max || size >= uint32max || offset >= uint32max + bool needZip64 = (records >= ZIP_UINT16_MAX || + size >= ZIP_UINT32_MAX || + offset >= ZIP_UINT32_MAX); + + if (needZip64) { + // 写入 ZIP64 End of Central Directory Record (56 bytes) + // directory64EndLen = 56 + buf.clear(); + buf.reserve(56 + 20); // directory64EndLen + directory64LocLen + + // ZIP64 EOCD signature + write32(ZIP64_END_OF_CENTRAL_DIR_SIG); + + // Size of ZIP64 EOCD record (excluding signature and this field) + // = 56 - 12 = 44 + write64(44); + + // Version made by + write16(ZIP_VERSION_45); + + // Version needed to extract + write16(ZIP_VERSION_45); + + // Number of this disk + write32(0); + + // Number of the disk with the start of the central directory + write32(0); + + // Total number of entries in the central directory on this disk + write64(records); + + // Total number of entries in the central directory + write64(records); + + // Size of the central directory + write64(size); + + // Offset of start of central directory + write64(offset); + + // 写入 ZIP64 End of Central Directory Locator (20 bytes) + // directory64LocLen = 20 + + // ZIP64 EOCD Locator signature + write32(ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIG); + + // Number of the disk with the start of the zip64 end of central directory + write32(0); + + // Relative offset of the zip64 end of central directory record + // (currentOffset_ 已经指向 central directory 结束位置) + write64(currentOffset_); + + // Total number of disks + write32(1); + + file_.write(reinterpret_cast(buf.data()), buf.size()); + currentOffset_ += buf.size(); + + // 在普通 EOCD 中使用 max 值表示应使用 ZIP64 值 + records = ZIP_UINT16_MAX; + size = ZIP_UINT32_MAX; + offset = ZIP_UINT32_MAX; + } + + // 写入普通 End of Central Directory Record (22 bytes) + // directoryEndLen = 22 + buf.clear(); + buf.reserve(22 + comment_.size()); + + // Signature + write32(END_OF_CENTRAL_DIR_SIG); + + // Number of this disk + write16(0); + + // Disk where central directory starts + write16(0); + + // Number of central directory records on this disk + write16(static_cast(records)); + + // Total number of central directory records + write16(static_cast(records)); + + // Size of central directory + write32(static_cast(size)); + + // Offset of start of central directory + write32(static_cast(offset)); + + // Comment length + write16(static_cast(comment_.size())); + + file_.write(reinterpret_cast(buf.data()), buf.size()); + + // Comment + if (!comment_.empty()) { + file_.write(comment_.c_str(), comment_.size()); + } + + if (!file_.good()) { + return Error(ErrorCode::FILE_WRITE_ERROR, "Failed to write end of central directory"); + } + + return Error(); +} + +Error ZipWriter::close() { + if (!file_.is_open()) { + return Error(); + } + + // 记录中央目录开始位置 (对应 Go 的 start := w.cw.count) + uint64_t centralDirOffset = currentOffset_; + + // 写入中央目录 + Error err = writeCentralDirectory(); + if (err) return err; + + // 计算中央目录大小 (对应 Go 的 size := uint64(end - start)) + uint64_t centralDirSize = currentOffset_ - centralDirOffset; + + // 写入结束记录 + err = writeEndOfCentralDirectory(centralDirOffset, centralDirSize); + if (err) return err; + + file_.close(); + return Error(); +} + +} // namespace pzip + + diff --git a/src/source/archivemanager/archivemanager.cpp b/src/source/archivemanager/archivemanager.cpp index 0f09990da..9ff6df5d0 100644 --- a/src/source/archivemanager/archivemanager.cpp +++ b/src/source/archivemanager/archivemanager.cpp @@ -1,5 +1,5 @@ // Copyright (C) 2019 ~ 2020 Uniontech Software Technology Co.,Ltd. -// SPDX-FileCopyrightText: 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2022-2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -130,7 +130,13 @@ bool ArchiveManager::addFiles(const QString &strArchiveFullPath, const QListmetaData().name().contains("pzip", Qt::CaseInsensitive)) { + continue; + } + switch (eType) { // 自动识别 case APT_Auto: + case APT_AutoWithoutPzip: pIface = createInterface(fileName, mimeType, plugin); break; // cli7zplugin diff --git a/src/source/common/uitools.h b/src/source/common/uitools.h index 40ff41703..9493cc06b 100644 --- a/src/source/common/uitools.h +++ b/src/source/common/uitools.h @@ -1,5 +1,5 @@ // Copyright (C) 2019 ~ 2020 Uniontech Software Technology Co.,Ltd. -// SPDX-FileCopyrightText: 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2022-2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -50,6 +50,7 @@ class UiTools : public QObject enum AssignPluginType { APT_Auto, // 自动识别 + APT_AutoWithoutPzip,// 自动识别,但排除 pzip 插件 APT_Cli7z, // cli7zplugin APT_Libarchive, // libarchive APT_Libzip, // libzipplugin diff --git a/src/source/mainwindow.cpp b/src/source/mainwindow.cpp index fe0b3d4cd..cfc125d2c 100644 --- a/src/source/mainwindow.cpp +++ b/src/source/mainwindow.cpp @@ -1,5 +1,5 @@ // Copyright (C) 2019 ~ 2020 Uniontech Software Technology Co.,Ltd. -// SPDX-FileCopyrightText: 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2022-2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -1085,9 +1085,12 @@ void MainWindow::slotCompress(const QVariant &val) options.iCPUTheadNum = m_stCompressParameter.iCPUTheadNum; bool bUseLibarchive = false; -#ifdef __aarch64__ // 华为arm平台 zip压缩 性能提升. 在多线程场景下使用7z,单线程场景下使用libarchive - double maxFileSizeProportion = static_cast(maxFileSize_) / static_cast(m_stCompressParameter.qSize); - bUseLibarchive = maxFileSizeProportion > 0.6; +#ifdef __aarch64__ // 华为arm平台 zip压缩 性能提升 + // pzip 高性能并行压缩插件已移植到 release/eagle(Qt5/KF5), + // ARM64 平台 ZIP 压缩默认走 pzip(APT_Auto 自动选择,pzip 优先级最高), + // 不再根据大文件占比强制使用 libarchive。 + // 分卷 ZIP 不支持 pzip,由下方 bSplit 分支走 cli7z。 + bUseLibarchive = false; #else bUseLibarchive = false; #endif @@ -1104,6 +1107,9 @@ void MainWindow::slotCompress(const QVariant &val) if (true == zipPasswordIsChinese) { // 对zip的中文加密使用libzip插件 eType = UiTools::APT_Libzip; + } else if (options.bSplit && "application/zip" == m_stCompressParameter.strMimeType) { + // 分卷 ZIP 不支持 pzip,使用 cli7z + eType = UiTools::APT_Cli7z; } else if ((false == options.bSplit) && true == bUseLibarchive && "application/zip" == m_stCompressParameter.strMimeType) { // 考虑到华为arm平台 zip压缩 性能提升,只针对zip类型的压缩才会考虑到是否特殊处理arm平台,分卷情况不做此处理 eType = UiTools::APT_Libarchive;