mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-13 16:34:14 +02:00
Add a zip reader and writer for .qetz projects (bundled miniz)
First stage of the zipped project file discussed in #1440 (and #865): the zip itself, nothing reads or writes a project with it yet. miniz 3.1.2 (MIT, one .c and one .h, no zlib) is bundled unmodified in sources/container/miniz, as the LZMA SDK is for .edz files, so builds need nothing new. QetZip wraps it: plain ZIP with Deflate, entries stored where asked (pictures, the mimetype entry), written through QSaveFile so a failed save keeps the previous file, without timestamps so that the same project gives the same bytes. Reading refuses unsafe or repeated entry names, encrypted entries and archives over 2 GB once unpacked. Test: tst_qetzip (round trip, same bytes twice, names refused both ways, truncated and non-zip input). A written archive also passes unzip -t and Python's zipfile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Wga9B92BNurDEtxxtCDxf
This commit is contained in:
@@ -102,6 +102,13 @@ else()
|
||||
message(STATUS "Qt Qml module not available: JavaScript scripting (--run) disabled")
|
||||
endif()
|
||||
|
||||
# The zip container of a .qetz project is read and written with the bundled
|
||||
# miniz (sources/container/miniz): in memory only, and without timestamps so
|
||||
# that saving the same project twice gives the same bytes. The names must not
|
||||
# clash with the zlib Qt links. Every target including miniz.h needs the same
|
||||
# three, hence directory-wide.
|
||||
add_compile_definitions(MINIZ_NO_TIME MINIZ_NO_STDIO MINIZ_NO_ZLIB_COMPATIBLE_NAMES)
|
||||
|
||||
# The crash handler writes a backtrace into the dump. Detecting this with
|
||||
# __has_include(<execinfo.h>) is not enough: the header is present on FreeBSD
|
||||
# too, but backtrace() lives in a separate libexecinfo there, so the compile
|
||||
|
||||
@@ -165,6 +165,10 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/import/edz/lzma/LzmaDec.c
|
||||
${QET_DIR}/sources/import/edz/lzma/Ppmd7.c
|
||||
${QET_DIR}/sources/import/edz/lzma/Ppmd7Dec.c
|
||||
${QET_DIR}/sources/container/miniz/miniz.c
|
||||
${QET_DIR}/sources/container/miniz/miniz.h
|
||||
${QET_DIR}/sources/container/qetzip.cpp
|
||||
${QET_DIR}/sources/container/qetzip.h
|
||||
${QET_DIR}/sources/borderproperties.cpp
|
||||
${QET_DIR}/sources/borderproperties.h
|
||||
${QET_DIR}/sources/bordertitleblock.cpp
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
Copyright 2013-2014 RAD Game Tools and Valve Software
|
||||
Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC
|
||||
|
||||
All Rights Reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,5 @@
|
||||
miniz 3.1.2 (https://github.com/richgel999/miniz, release asset
|
||||
miniz-3.1.2.zip, sha256 f0446d863f9c19926ad9483c523fdc42e42b8d4a6a431d27e09d49c79a140d9a),
|
||||
MIT licence (LICENSE). Unmodified. Reads and writes the zip container of a
|
||||
.qetz project (sources/container/qetzip.*). Built with MINIZ_NO_TIME,
|
||||
MINIZ_NO_STDIO and MINIZ_NO_ZLIB_COMPATIBLE_NAMES (see qetzip.h).
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "qetzip.h"
|
||||
|
||||
#include "miniz/miniz.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QSaveFile>
|
||||
#include <QSet>
|
||||
|
||||
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<Entry> &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<QString> 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<const char *>(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<Entry> &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<Entry> *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<QString> 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<const char *>(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<Entry> *entries, QString *error)
|
||||
{
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
setError(error, file.errorString());
|
||||
return false;
|
||||
}
|
||||
return fromBytes(file.readAll(), entries, error);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef QETZIP_H
|
||||
#define QETZIP_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
/**
|
||||
@brief The QetZip namespace
|
||||
Reads and writes the zip file a .qetz project is stored in: plain ZIP,
|
||||
Deflate, as ODF and OOXML documents are, so any unzip tool opens it.
|
||||
A whole archive is read into, or written from, memory -- projects are
|
||||
megabytes, not gigabytes -- and written through QSaveFile, so a failed
|
||||
save leaves the previous file as it was.
|
||||
|
||||
Entries carry no timestamp: saving the same project twice gives the
|
||||
same bytes.
|
||||
*/
|
||||
namespace QetZip
|
||||
{
|
||||
struct Entry
|
||||
{
|
||||
QString name; ///< path inside the archive, '/'-separated
|
||||
QByteArray data;
|
||||
bool compress = true; ///< false: stored (pictures, the mimetype entry)
|
||||
};
|
||||
|
||||
/// Largest archive read(): uncompressed total and entry count
|
||||
constexpr qint64 MaxUncompressedSize = qint64(2) * 1024 * 1024 * 1024;
|
||||
constexpr int MaxEntries = 100000;
|
||||
|
||||
bool write(const QString &path, const QList<Entry> &entries, QString *error = nullptr);
|
||||
QByteArray toBytes(const QList<Entry> &entries, QString *error = nullptr);
|
||||
bool read(const QString &path, QList<Entry> *entries, QString *error = nullptr);
|
||||
bool fromBytes(const QByteArray &zip, QList<Entry> *entries, QString *error = nullptr);
|
||||
}
|
||||
|
||||
#endif // QETZIP_H
|
||||
@@ -1162,3 +1162,14 @@ if(Qt6_VERSION VERSION_LESS 6.5.0)
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
# The zip a .qetz project is stored in: round trip, same bytes twice,
|
||||
# unsafe entry names refused (sources/container/qetzip.*, bundled miniz).
|
||||
add_executable(
|
||||
tst_qetzip
|
||||
tst_qetzip.cpp
|
||||
${QET_DIR}/sources/container/qetzip.cpp
|
||||
${QET_DIR}/sources/container/miniz/miniz.c)
|
||||
add_test(NAME tst_qetzip COMMAND tst_qetzip)
|
||||
target_include_directories(tst_qetzip PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_qetzip PRIVATE Qt::Test)
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include <QTemporaryDir>
|
||||
|
||||
#include "container/qetzip.h"
|
||||
#include "container/miniz/miniz.h"
|
||||
|
||||
// The zip a .qetz project is stored in (sources/container/qetzip.*):
|
||||
// entries come back as written, in order, stored ones stored; the same
|
||||
// entries give the same bytes; unsafe names are refused both ways.
|
||||
class tst_qetzip : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
static QList<QetZip::Entry> sample()
|
||||
{
|
||||
QList<QetZip::Entry> entries;
|
||||
entries.append({QStringLiteral("mimetype"), QByteArray("application/x-qelectrotech-project+zip"), false});
|
||||
QByteArray xml("<project>");
|
||||
for (int i = 0 ; i < 2000 ; ++i) xml += "<element name=\"K1\"/>";
|
||||
xml += "</project>";
|
||||
entries.append({QStringLiteral("project.xml"), xml, true});
|
||||
QByteArray picture;
|
||||
for (int i = 0 ; i < 4096 ; ++i) picture += char((i * 7919) & 0xff);
|
||||
entries.append({QStringLiteral("images/00.png"), picture, false});
|
||||
entries.append({QStringLiteral("folios/é ü.xml"), QByteArray("<diagram/>"), true});
|
||||
entries.append({QStringLiteral("empty"), QByteArray(), true});
|
||||
return entries;
|
||||
}
|
||||
|
||||
// a zip holding one entry named @p name, written past QetZip's checks
|
||||
static QByteArray rawZip(const char *name)
|
||||
{
|
||||
mz_zip_archive zip;
|
||||
mz_zip_zero_struct(&zip);
|
||||
mz_zip_writer_init_heap(&zip, 0, 0);
|
||||
mz_zip_writer_add_mem(&zip, name, "x", 1, MZ_DEFAULT_LEVEL);
|
||||
void *buffer = nullptr;
|
||||
size_t size = 0;
|
||||
mz_zip_writer_finalize_heap_archive(&zip, &buffer, &size);
|
||||
const QByteArray bytes(static_cast<const char *>(buffer), qsizetype(size));
|
||||
mz_zip_writer_end(&zip);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void roundTrip()
|
||||
{
|
||||
const QList<QetZip::Entry> in = sample();
|
||||
QString error;
|
||||
const QByteArray zip = QetZip::toBytes(in, &error);
|
||||
QVERIFY2(!zip.isEmpty(), qPrintable(error));
|
||||
QVERIFY(zip.startsWith("PK"));
|
||||
QVERIFY2(zip.size() < in.at(1).data.size() / 4, "the XML entry was not compressed");
|
||||
|
||||
QList<QetZip::Entry> out;
|
||||
QVERIFY2(QetZip::fromBytes(zip, &out, &error), qPrintable(error));
|
||||
QCOMPARE(out.size(), in.size());
|
||||
for (int i = 0 ; i < in.size() ; ++i) {
|
||||
QCOMPARE(out.at(i).name, in.at(i).name);
|
||||
QCOMPARE(out.at(i).data, in.at(i).data);
|
||||
if (!in.at(i).data.isEmpty())
|
||||
QCOMPARE(out.at(i).compress, in.at(i).compress);
|
||||
}
|
||||
// mimetype first and stored: its bytes appear as is after the header
|
||||
QCOMPARE(zip.indexOf("application/x-qelectrotech-project+zip"), 30 + int(qstrlen("mimetype")));
|
||||
}
|
||||
|
||||
void sameBytesTwice()
|
||||
{
|
||||
QCOMPARE(QetZip::toBytes(sample()), QetZip::toBytes(sample()));
|
||||
}
|
||||
|
||||
void refusesUnsafeNamesOnWrite_data()
|
||||
{
|
||||
QTest::addColumn<QString>("name");
|
||||
QTest::newRow("parent") << QStringLiteral("../x.xml");
|
||||
QTest::newRow("inner parent") << QStringLiteral("folios/../../x.xml");
|
||||
QTest::newRow("absolute") << QStringLiteral("/etc/x");
|
||||
QTest::newRow("drive") << QStringLiteral("C:/x");
|
||||
QTest::newRow("backslash") << QStringLiteral("folios\\x.xml");
|
||||
QTest::newRow("empty") << QString();
|
||||
}
|
||||
void refusesUnsafeNamesOnWrite()
|
||||
{
|
||||
QFETCH(QString, name);
|
||||
QString error;
|
||||
QVERIFY(QetZip::toBytes({{name, QByteArray("x"), true}}, &error).isEmpty());
|
||||
QVERIFY(!error.isEmpty());
|
||||
}
|
||||
|
||||
void refusesRepeatedNames()
|
||||
{
|
||||
QString error;
|
||||
QVERIFY(QetZip::toBytes({{QStringLiteral("a"), "1", true}, {QStringLiteral("a"), "2", true}}, &error).isEmpty());
|
||||
}
|
||||
|
||||
void refusesUnsafeNamesOnRead()
|
||||
{
|
||||
QList<QetZip::Entry> out;
|
||||
QString error;
|
||||
QVERIFY(QetZip::fromBytes(rawZip("ok/name.xml"), &out, &error));
|
||||
QVERIFY(!QetZip::fromBytes(rawZip("../evil.xml"), &out, &error));
|
||||
QVERIFY(out.isEmpty());
|
||||
// miniz will not write a name starting with '/': write "Xabs.xml"
|
||||
// and turn the X into '/' in both of its headers
|
||||
QByteArray absolute = rawZip("Xabs.xml");
|
||||
QCOMPARE(absolute.count("Xabs.xml"), 2);
|
||||
absolute.replace("Xabs.xml", "/abs.xml");
|
||||
QVERIFY(!QetZip::fromBytes(absolute, &out, &error));
|
||||
}
|
||||
|
||||
void refusesGarbage()
|
||||
{
|
||||
QList<QetZip::Entry> out;
|
||||
QString error;
|
||||
QVERIFY(!QetZip::fromBytes(QByteArray("not a zip at all"), &out, &error));
|
||||
QVERIFY(!error.isEmpty());
|
||||
QByteArray cut = QetZip::toBytes(sample());
|
||||
cut.chop(cut.size() / 2);
|
||||
QVERIFY(!QetZip::fromBytes(cut, &out, &error));
|
||||
}
|
||||
|
||||
void writeAndRead()
|
||||
{
|
||||
QTemporaryDir dir;
|
||||
QVERIFY(dir.isValid());
|
||||
const QString path = dir.filePath(QStringLiteral("p.qetz"));
|
||||
QString error;
|
||||
QVERIFY2(QetZip::write(path, sample(), &error), qPrintable(error));
|
||||
QList<QetZip::Entry> out;
|
||||
QVERIFY2(QetZip::read(path, &out, &error), qPrintable(error));
|
||||
QCOMPARE(out.size(), sample().size());
|
||||
QCOMPARE(out.at(1).data, sample().at(1).data);
|
||||
// a refused write leaves the file as it was
|
||||
QVERIFY(!QetZip::write(path, {{QStringLiteral("../x"), "x", true}}, &error));
|
||||
QVERIFY(QetZip::read(path, &out, &error));
|
||||
QCOMPARE(out.size(), sample().size());
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_qetzip)
|
||||
|
||||
#include "tst_qetzip.moc"
|
||||
Reference in New Issue
Block a user