Files
qelectrotech-source-mirror/tests/qttest/tst_qetzip.cpp
T
ispyisail cd3a726e26 Name the .qetz MIME type application/x-qet-project+zip
Every other QElectroTech file type is application/x-qet-*; the zipped
project was application/x-qelectrotech-project+zip. No release has
shipped it yet, so it is renamed now.

Files written with the old name still open: the reader accepts both, and
the shared-mime-info file keeps the old name as an alias and matches
either on content.

Review of #1472 (scorpio810, point 2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 06:20:57 +13:00

174 lines
6.1 KiB
C++

// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QTemporaryDir>
#include <QtEndian>
#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-qet-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-qet-project+zip"), 30 + int(qstrlen("mimetype")));
}
void sameBytesTwice()
{
QCOMPARE(QetZip::toBytes(sample()), QetZip::toBytes(sample()));
}
// every entry carries a valid date in both of its headers, not the
// 0 miniz writes without a clock (shown as 1979 or 1901 by unzip tools)
void entriesHaveValidDate()
{
const QByteArray zip = QetZip::toBytes(sample());
QVERIFY(!zip.isEmpty());
mz_zip_archive archive;
mz_zip_zero_struct(&archive);
QVERIFY(mz_zip_reader_init_mem(&archive, zip.constData(), size_t(zip.size()), 0));
const mz_uint count = mz_zip_reader_get_num_files(&archive);
QCOMPARE(int(count), sample().size());
for (mz_uint i = 0 ; i < count ; ++i) {
mz_zip_archive_file_stat stat;
QVERIFY(mz_zip_reader_file_stat(&archive, i, &stat));
const char *local = zip.constData() + stat.m_local_header_ofs;
const char *central = zip.constData() + archive.m_central_directory_file_ofs + stat.m_central_dir_ofs;
QCOMPARE(qFromLittleEndian<quint16>(local), quint16(0x50 | 0x4b << 8)); // "PK"
QCOMPARE(qFromLittleEndian<quint16>(central), quint16(0x50 | 0x4b << 8));
// 1 Jan 1980: day 1, month 1, year 0 since 1980; time 00:00:00
QCOMPARE(qFromLittleEndian<quint16>(local + 12), quint16(0x21));
QCOMPARE(qFromLittleEndian<quint16>(central + 14), quint16(0x21));
QCOMPARE(qFromLittleEndian<quint16>(local + 10), quint16(0));
QCOMPARE(qFromLittleEndian<quint16>(central + 12), quint16(0));
}
mz_zip_reader_end(&archive);
}
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"