/* Copyright 2006-2026 The QElectroTech Team This file is part of QElectroTech. QElectroTech is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. QElectroTech is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with QElectroTech. If not, see . */ #include "qetzip.h" #include "miniz/miniz.h" #include #include #include namespace { void setError(QString *error, const QString &text) { if (error) *error = text; } //A name read from an archive is only ever a key, but refuse the ones a //careless caller could turn into a path outside a folder: absolute, //with a drive, with "..", or not valid UTF-8. bool safeName(const QString &name) { if (name.isEmpty() || name.startsWith(QLatin1Char('/')) || name.contains(QLatin1Char('\\')) || name.contains(QLatin1Char(':')) || name.contains(QChar::ReplacementCharacter)) { return false; } for (const QString &part : name.split(QLatin1Char('/'))) { if (part == QLatin1String("..")) return false; } return true; } } /** @brief QetZip::toBytes @return the zip holding @p entries in their order, or an empty array with @p error set */ QByteArray QetZip::toBytes(const QList &entries, QString *error) { mz_zip_archive zip; mz_zip_zero_struct(&zip); if (!mz_zip_writer_init_heap(&zip, 0, 0)) { setError(error, QStringLiteral("cannot start a zip archive")); return {}; } QSet names; for (const Entry &entry : entries) { if (!safeName(entry.name) || names.contains(entry.name)) { setError(error, QStringLiteral("invalid or repeated entry name \"%1\"").arg(entry.name)); mz_zip_writer_end(&zip); return {}; } names.insert(entry.name); const QByteArray name = entry.name.toUtf8(); const mz_uint level = entry.compress ? MZ_DEFAULT_LEVEL : MZ_NO_COMPRESSION; if (!mz_zip_writer_add_mem(&zip, name.constData(), entry.data.constData(), size_t(entry.data.size()), level)) { setError(error, QStringLiteral("cannot add \"%1\": %2") .arg(entry.name, QString::fromLatin1(mz_zip_get_error_string(mz_zip_get_last_error(&zip))))); mz_zip_writer_end(&zip); return {}; } } void *buffer = nullptr; size_t size = 0; if (!mz_zip_writer_finalize_heap_archive(&zip, &buffer, &size)) { setError(error, QStringLiteral("cannot finish the zip archive")); mz_zip_writer_end(&zip); return {}; } const QByteArray bytes(static_cast(buffer), qsizetype(size)); mz_zip_writer_end(&zip); //frees buffer return bytes; } /** @brief QetZip::write Write @p entries to @p path, replacing it only once the whole archive is written. */ bool QetZip::write(const QString &path, const QList &entries, QString *error) { const QByteArray bytes = toBytes(entries, error); if (bytes.isEmpty()) return false; QSaveFile file(path); if (!file.open(QIODevice::WriteOnly)) { setError(error, file.errorString()); return false; } if (file.write(bytes) != bytes.size() || !file.commit()) { setError(error, file.errorString()); return false; } return true; } /** @brief QetZip::fromBytes Read every file entry of the zip @p zip into @p entries, in archive order. Folders are skipped. An encrypted entry, an unsafe or repeated name, a method miniz cannot read, or more than MaxEntries entries or MaxUncompressedSize bytes refuse the whole archive. */ bool QetZip::fromBytes(const QByteArray &zip_bytes, QList *entries, QString *error) { if (!entries) return false; entries->clear(); mz_zip_archive zip; mz_zip_zero_struct(&zip); if (!mz_zip_reader_init_mem(&zip, zip_bytes.constData(), size_t(zip_bytes.size()), 0)) { setError(error, QStringLiteral("not a zip archive: %1") .arg(QString::fromLatin1(mz_zip_get_error_string(mz_zip_get_last_error(&zip))))); return false; } const mz_uint count = mz_zip_reader_get_num_files(&zip); if (count > mz_uint(MaxEntries)) { setError(error, QStringLiteral("too many entries (%1)").arg(count)); mz_zip_reader_end(&zip); return false; } qint64 total = 0; QSet names; for (mz_uint i = 0 ; i < count ; ++i) { mz_zip_archive_file_stat stat; if (!mz_zip_reader_file_stat(&zip, i, &stat)) { setError(error, QStringLiteral("unreadable entry %1").arg(i)); mz_zip_reader_end(&zip); return false; } if (stat.m_is_directory) continue; const QString name = QString::fromUtf8(stat.m_filename); if (stat.m_is_encrypted || !stat.m_is_supported || !safeName(name) || names.contains(name)) { setError(error, QStringLiteral("entry \"%1\" cannot be read").arg(name)); mz_zip_reader_end(&zip); return false; } total += qint64(stat.m_uncomp_size); if (total > MaxUncompressedSize) { setError(error, QStringLiteral("archive too large once unpacked")); mz_zip_reader_end(&zip); return false; } size_t size = 0; void *data = mz_zip_reader_extract_to_heap(&zip, i, &size, 0); if (!data) { setError(error, QStringLiteral("cannot unpack \"%1\": %2") .arg(name, QString::fromLatin1(mz_zip_get_error_string(mz_zip_get_last_error(&zip))))); mz_zip_reader_end(&zip); return false; } Entry entry; entry.name = name; entry.data = QByteArray(static_cast(data), qsizetype(size)); entry.compress = stat.m_method != 0; mz_free(data); names.insert(name); entries->append(entry); } mz_zip_reader_end(&zip); return true; } /** @brief QetZip::read fromBytes() on the file at @p path */ bool QetZip::read(const QString &path, QList *entries, QString *error) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) { setError(error, file.errorString()); return false; } return fromBytes(file.readAll(), entries, error); }