Files
qelectrotech-source-mirror/tests/qttest/tst_qetzip.cpp
T
ispyisail c240e4e6f7 Fix the dates of the files inside a .qetz project
miniz is built with MINIZ_NO_TIME so that saving the same project twice
gives the same bytes. Without a clock it writes 0 as each entry's DOS
date: day 0 of month 0, 1980, which is not a date. Unzip tools show it
as 30 Nov 1979, others fail the conversion and show 13 Dec 1901
(reported on PR #1462).

Write 1 Jan 1980 00:00, the first valid DOS date, into the local and
central header of every entry once the archive is built. The bytes stay
reproducible, and miniz stays unmodified. Using the save time instead
was rejected: two saves would no longer give the same file, and miniz
converts through the local time zone, so even a fixed time_t would
differ between machines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:49:34 +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-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()));
}
// 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"