diff --git a/sources/container/qetzip.cpp b/sources/container/qetzip.cpp index 7a0be3c26..dc9e2c9f7 100644 --- a/sources/container/qetzip.cpp +++ b/sources/container/qetzip.cpp @@ -22,6 +22,7 @@ #include #include #include +#include namespace { void setError(QString *error, const QString &text) @@ -44,6 +45,42 @@ bool safeName(const QString &name) } return true; } + + //Built with MINIZ_NO_TIME, miniz leaves each entry's DOS date at 0, + //day 0 of month 0, which is no date: unzip shows 30 Nov 1979, other + //tools 13 Dec 1901. Give every entry 1 Jan 1980 00:00 instead, the + //first valid DOS date, so the bytes still do not depend on the time. +constexpr quint16 EntryDosDate = (1 << 5) | 1; + +bool setEntryDates(QByteArray *bytes) +{ + char *data = bytes->data(); + mz_zip_archive zip; + mz_zip_zero_struct(&zip); + if (!mz_zip_reader_init_mem(&zip, data, size_t(bytes->size()), 0)) return false; + + bool ok = true; + const mz_uint count = mz_zip_reader_get_num_files(&zip); + for (mz_uint i = 0 ; i < count && ok ; ++i) + { + mz_zip_archive_file_stat stat; + if (!mz_zip_reader_file_stat(&zip, i, &stat)) { + ok = false; + break; + } + //date at offset 12 of the local header, 14 of the central one + const quint64 local = stat.m_local_header_ofs + 12; + const quint64 central = zip.m_central_directory_file_ofs + stat.m_central_dir_ofs + 14; + if (local + 2 > quint64(bytes->size()) || central + 2 > quint64(bytes->size())) { + ok = false; + break; + } + qToLittleEndian(EntryDosDate, data + local); + qToLittleEndian(EntryDosDate, data + central); + } + mz_zip_reader_end(&zip); + return ok; +} } /** @@ -87,8 +124,12 @@ QByteArray QetZip::toBytes(const QList &entries, QString *error) mz_zip_writer_end(&zip); return {}; } - const QByteArray bytes(static_cast(buffer), qsizetype(size)); + QByteArray bytes(static_cast(buffer), qsizetype(size)); mz_zip_writer_end(&zip); //frees buffer + if (!setEntryDates(&bytes)) { + setError(error, QStringLiteral("cannot date the zip entries")); + return {}; + } return bytes; } diff --git a/sources/container/qetzip.h b/sources/container/qetzip.h index 9238f6ebf..808a861ce 100644 --- a/sources/container/qetzip.h +++ b/sources/container/qetzip.h @@ -30,8 +30,8 @@ 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. + Every entry is dated 1 Jan 1980 00:00, not the time of saving: saving + the same project twice gives the same bytes. */ namespace QetZip { diff --git a/tests/qttest/tst_qetzip.cpp b/tests/qttest/tst_qetzip.cpp index 4c69afef3..156cb8fb6 100644 --- a/tests/qttest/tst_qetzip.cpp +++ b/tests/qttest/tst_qetzip.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "container/qetzip.h" #include "container/miniz/miniz.h" @@ -72,6 +73,33 @@ private slots: 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(local), quint16(0x50 | 0x4b << 8)); // "PK" + QCOMPARE(qFromLittleEndian(central), quint16(0x50 | 0x4b << 8)); + // 1 Jan 1980: day 1, month 1, year 0 since 1980; time 00:00:00 + QCOMPARE(qFromLittleEndian(local + 12), quint16(0x21)); + QCOMPARE(qFromLittleEndian(central + 14), quint16(0x21)); + QCOMPARE(qFromLittleEndian(local + 10), quint16(0)); + QCOMPARE(qFromLittleEndian(central + 12), quint16(0)); + } + mz_zip_reader_end(&archive); + } + void refusesUnsafeNamesOnWrite_data() { QTest::addColumn("name");