Работа с файлами и директориями в Qt

Архивация и распаковка ZIP в Qt: обзор подходов и библиотек

Введение

Qt Core не предоставляет встроенного публичного API для работы с ZIP-архивами в полном объёме — это удивляет многих разработчиков, ожидающих найти QZipFile рядом с QFile. Тем не менее, существует несколько рабочих путей: приватный класс Qt, модуль KArchive, сторонняя библиотека QuaZip и интеграция с системными утилитами. Разберём каждый вариант.

Концепция

  1. QZipReader/QZipWriter — приватные классы, входящие в состав Qt (используются, например, для распаковки .qm-файлов и пакетов помощи). Они доступны через приватный заголовок <private/qzipreader_p.h> и <private/qzipwriter_p.h>, но Qt не гарантирует стабильность их API между версиями — использовать в продакшене не рекомендуется.
  1. QuaZip — самая популярная сторонняя библиотека с открытым исходным кодом, построенная на zlib и предоставляющая Qt-дружественный интерфейс (QuaZip, QuaZipFile), совместимый с QIODevice.
  1. KArchive (KDE Frameworks) — мощная библиотека, поддерживающая ZIP, TAR, 7z и другие форматы, но тянет зависимость от KDE Frameworks.
  1. Вызов внешних утилит через QProcess (zip/unzip, 7z) — простой, но платформо-зависимый способ.

Рассмотрим практический пример на QuaZip, поскольку это наиболее распространённое и поддерживаемое Qt-ориентированное решение.

Пример кода: Архивация и распаковка ZIP в Qt

#include <QuaZip/quazip.h>
#include <QuaZip/quazipfile.h>
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QDebug>

bool addFileToZip(QuaZip &zip, const QString &filePath, const QString &zipEntryName)
{
    QFile inFile(filePath);
    if (!inFile.open(QIODevice::ReadOnly))
        return false;

    QuaZipFile outFile(&zip);
    QuaZipNewInfo newInfo(zipEntryName, filePath);

    if (!outFile.open(QIODevice::WriteOnly, newInfo)) {
        inFile.close();
        return false;
    }

    outFile.write(inFile.readAll());

    outFile.close();
    inFile.close();
    return outFile.getZipError() == UNZ_OK;
}

bool createZipArchive(const QString &zipPath, const QStringList &sourceFiles)
{
    QuaZip zip(zipPath);
    if (!zip.open(QuaZip::mdCreate)) {
        qWarning() << "Не удалось создать архив:" << zipPath;
        return false;
    }

    for (const QString &filePath : sourceFiles) {
        QFileInfo info(filePath);
        if (!addFileToZip(zip, filePath, info.fileName())) {
            qWarning() << "Ошибка добавления файла:" << filePath;
            zip.close();
            return false;
        }
    }

    zip.close();
    return zip.getZipError() == UNZ_OK;
}

bool extractZipArchive(const QString &zipPath, const QString &outputDir)
{
    QuaZip zip(zipPath);
    if (!zip.open(QuaZip::mdUnzip)) {
        qWarning() << "Не удалось открыть архив:" << zipPath;
        return false;
    }

    QDir().mkpath(outputDir);

    for (bool more = zip.goToFirstFile(); more; more = zip.goToNextFile()) {
        QuaZipFile inFile(&zip);
        if (!inFile.open(QIODevice::ReadOnly))
            continue;

        QString entryName = zip.getCurrentFileName();
        QString outPath = outputDir + "/" + entryName;
        QFileInfo outInfo(outPath);
        QDir().mkpath(outInfo.absolutePath());

        QFile outFile(outPath);
        if (outFile.open(QIODevice::WriteOnly)) {
            outFile.write(inFile.readAll());
            outFile.close();
        }
        inFile.close();
    }

    zip.close();
    return true;
}

Подключение в .pro-файле (qmake) обычно выглядит так:

QT += core
CONFIG += link_pkgconfig
PKGCONFIG += quazip1-qt6

Либо через CMake и find_package(QuaZip-Qt6 REQUIRED) с последующим target_link_libraries(myapp PRIVATE QuaZip::QuaZip).

Пояснения к коду

QuaZip представляет сам архив, а QuaZipFile — отдельную запись внутри него, реализующую интерфейс QIODevice, что позволяет читать и писать данные так же, как с обычным QFile. Режим QuaZip::mdCreate создаёт новый архив, mdUnzip — открывает для чтения существующий.

При распаковке метод goToFirstFile()/goToNextFile() итерируют по записям архива, а getCurrentFileName() возвращает относительный путь внутри ZIP, который нужно аккуратно подставить относительно целевой директории распаковки.

Подводные камни

  1. Атаки через Zip Slip. При распаковке нельзя слепо доверять путям внутри архива — запись может содержать ../../etc/passwd. Перед записью обязательно нормализуйте путь и проверяйте, что результат остаётся внутри целевой директории.
  1. Кодировки имён файлов. Старые ZIP-архивы могут хранить имена в кодировках, отличных от UTF-8 (например, CP866 для архивов, созданных на Windows с русской локалью). QuaZip поддерживает указание кодировки, но это нужно делать явно.
  1. Лицензирование и сборка. QuaZip и KArchive — внешние зависимости, которые нужно собрать или установить через пакетный менеджер (vcpkg, conan, apt). Это усложняет процесс сборки на CI по сравнению с использованием только Qt Core.
  1. Производительность для очень больших архивов. Чтение всего файла через readAll() при добавлении в архив (как в примере) подходит для небольших файлов; для крупных стоит писать блоками через read()/write() в цикле.
  1. Приватные классы Qt нестабильны. Использование QZipReader/QZipWriter напрямую может привести к поломке сборки при обновлении версии Qt, так как это не публичный API.

Для большинства приложений на Qt оптимальный баланс между удобством, лицензией (LGPL) и стабильностью API даёт QuaZip — она остаётся стандартом де-факто для ZIP-операций в Qt-сообществе.

================================================================================