diff --git a/autotests/karchivetest.cpp b/autotests/karchivetest.cpp index 085aaa9..bdaa269 100644 --- a/autotests/karchivetest.cpp +++ b/autotests/karchivetest.cpp @@ -1,1459 +1,1477 @@ /* This file is part of the KDE project Copyright (C) 2006, 2010 David Faure Copyright (C) 2012 Mario Bensi This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "karchivetest.h" #include #include #include #include #include #include #include #include #ifndef Q_OS_WIN #include // symlink #include #endif #ifdef Q_OS_WIN #include #include #else #include #include #endif QTEST_MAIN(KArchiveTest) void initLocale() { qputenv("LC_ALL", "en_US.UTF-8"); // KArchive uses QFile::decodeName, and our tests use utf8 encoding for filenames } Q_CONSTRUCTOR_FUNCTION(initLocale) static const int SIZE1 = 100; /** * Writes test fileset specified archive * @param archive archive */ static void writeTestFilesToArchive(KArchive *archive) { QVERIFY(archive->writeFile("empty", "", 0100644, "weis", "users")); QVERIFY(archive->writeFile("test1", QByteArray("Hallo"), 0100440, QString("weis"), QString("users"))); // Now let's try with the prepareWriting/writeData/finishWriting API QVERIFY(archive->prepareWriting("test2", "weis", "users", 8)); QVERIFY(archive->writeData("Hallo ", 6)); QVERIFY(archive->writeData("Du", 2)); QVERIFY(archive->finishWriting(8)); // Add local file QFile localFile(QStringLiteral("test3")); QVERIFY(localFile.open(QIODevice::WriteOnly)); QVERIFY(localFile.write("Noch so einer", 13) == 13); localFile.close(); QVERIFY(archive->addLocalFile("test3", "z/test3")); // writeFile API QVERIFY(archive->writeFile("my/dir/test3", "I do not speak German\nDavid.", 0100644, "dfaure", "hackers")); // Now a medium file : 100 null bytes char medium[SIZE1]; memset(medium, 0, SIZE1); QVERIFY(archive->writeFile("mediumfile", QByteArray(medium, SIZE1))); // Another one, with an absolute path QVERIFY(archive->writeFile("/dir/subdir/mediumfile2", QByteArray(medium, SIZE1))); // Now a huge file : 20000 null bytes int n = 20000; char *huge = new char[n]; memset(huge, 0, n); QVERIFY(archive->writeFile("hugefile", QByteArray(huge, n))); delete [] huge; // Now an empty directory QVERIFY(archive->writeDir("aaaemptydir")); #ifndef Q_OS_WIN // Add local symlink QVERIFY(archive->addLocalFile("test3_symlink", "z/test3_symlink")); #endif // Add executable QVERIFY(archive->writeFile("executableAll", "#!/bin/sh\necho hi", 0100755)); } static QString getCurrentUserName() { #if defined(Q_OS_UNIX) struct passwd *pw = getpwuid(getuid()); return pw ? QFile::decodeName(pw->pw_name) : QString::number(getuid()); #elif defined(Q_OS_WIN) wchar_t buffer[255]; DWORD size = 255; bool ok = GetUserNameW(buffer, &size); if (!ok) { return QString(); } return QString::fromWCharArray(buffer); #else return QString(); #endif } static QString getCurrentGroupName() { #if defined(Q_OS_UNIX) struct group *grp = getgrgid(getgid()); return grp ? QFile::decodeName(grp->gr_name) : QString::number(getgid()); #elif defined(Q_OS_WIN) return QString(); #else return QString(); #endif } enum ListingFlags { WithUserGroup = 1, WithTime = 0x02 }; // ListingFlags static QStringList recursiveListEntries(const KArchiveDirectory *dir, const QString &path, int listingFlags) { QStringList ret; QStringList l = dir->entries(); l.sort(); for (const QString &it : qAsConst(l)) { const KArchiveEntry *entry = dir->entry(it); QString descr; descr += QStringLiteral("mode=") + QString::number(entry->permissions(), 8) + ' '; if (listingFlags & WithUserGroup) { descr += QStringLiteral("user=") + entry->user() + ' '; descr += QStringLiteral("group=") + entry->group() + ' '; } descr += QStringLiteral("path=") + path + (it) + ' '; descr += QStringLiteral("type=") + (entry->isDirectory() ? "dir" : "file"); if (entry->isFile()) { descr += QStringLiteral(" size=") + QString::number(static_cast(entry)->size()); } if (!entry->symLinkTarget().isEmpty()) { descr += QStringLiteral(" symlink=") + entry->symLinkTarget(); } if (listingFlags & WithTime) { descr += QStringLiteral(" time=") + entry->date().toString(QStringLiteral("dd.MM.yyyy hh:mm:ss")); } //qDebug() << descr; ret.append(descr); if (entry->isDirectory()) { ret += recursiveListEntries((KArchiveDirectory *)entry, path + it + '/', listingFlags); } } return ret; } /** * Verifies contents of specified archive against test fileset * @param archive archive */ static void testFileData(KArchive *archive) { const KArchiveDirectory *dir = archive->directory(); const KArchiveFile *f = dir->file(QStringLiteral("z/test3")); QByteArray arr(f->data()); QCOMPARE(arr.size(), 13); QCOMPARE(arr, QByteArray("Noch so einer")); // Now test using createDevice() QIODevice *dev = f->createDevice(); QByteArray contents = dev->readAll(); QCOMPARE(contents, arr); delete dev; dev = f->createDevice(); contents = dev->read(5); // test reading in two chunks QCOMPARE(contents.size(), 5); contents += dev->read(50); QCOMPARE(contents.size(), 13); QCOMPARE(QString::fromLatin1(contents.constData()), QString::fromLatin1(arr.constData())); delete dev; // test read/seek/peek work fine f = dir->file(QStringLiteral("test1")); dev = f->createDevice(); contents = dev->peek(4); QCOMPARE(contents, QByteArray("Hall")); contents = dev->peek(2); QCOMPARE(contents, QByteArray("Ha")); dev->seek(2); contents = dev->peek(2); QCOMPARE(contents, QByteArray("ll")); dev->seek(0); contents = dev->read(2); QCOMPARE(contents, QByteArray("Ha")); contents = dev->peek(2); QCOMPARE(contents, QByteArray("ll")); dev->seek(1); contents = dev->read(1); QCOMPARE(contents, QByteArray("a")); dev->seek(4); contents = dev->read(1); QCOMPARE(contents, QByteArray("o")); const KArchiveEntry *e = dir->entry(QStringLiteral("mediumfile")); QVERIFY(e && e->isFile()); f = (KArchiveFile *)e; QCOMPARE(f->data().size(), SIZE1); f = dir->file(QStringLiteral("hugefile")); QCOMPARE(f->data().size(), 20000); e = dir->entry(QStringLiteral("aaaemptydir")); QVERIFY(e && e->isDirectory()); QVERIFY(!dir->file("aaaemptydir")); e = dir->entry(QStringLiteral("my/dir/test3")); QVERIFY(e && e->isFile()); f = (KArchiveFile *)e; dev = f->createDevice(); QByteArray firstLine = dev->readLine(); QCOMPARE(QString::fromLatin1(firstLine.constData()), QString::fromLatin1("I do not speak German\n")); QByteArray secondLine = dev->read(100); QCOMPARE(QString::fromLatin1(secondLine.constData()), QString::fromLatin1("David.")); delete dev; #ifndef Q_OS_WIN e = dir->entry(QStringLiteral("z/test3_symlink")); QVERIFY(e); QVERIFY(e->isFile()); QCOMPARE(e->symLinkTarget(), QString("test3")); #endif // Test "./" prefix for KOffice (xlink:href="./ObjectReplacements/Object 1") e = dir->entry(QStringLiteral("./hugefile")); QVERIFY(e && e->isFile()); e = dir->entry(QStringLiteral("./my/dir/test3")); QVERIFY(e && e->isFile()); // Test directory entries e = dir->entry(QStringLiteral("my")); QVERIFY(e && e->isDirectory()); e = dir->entry(QStringLiteral("my/")); QVERIFY(e && e->isDirectory()); e = dir->entry(QStringLiteral("./my/")); QVERIFY(e && e->isDirectory()); } static void testReadWrite(KArchive *archive) { QVERIFY(archive->writeFile("newfile", "New File", 0100440, "dfaure", "users")); } #ifdef Q_OS_WIN extern Q_CORE_EXPORT int qt_ntfs_permission_lookup; #endif static void testCopyTo(KArchive *archive) { const KArchiveDirectory *dir = archive->directory(); QTemporaryDir tmpDir; const QString dirName = tmpDir.path() + '/'; QVERIFY(dir->copyTo(dirName)); QVERIFY(QFile::exists(dirName + "dir")); QVERIFY(QFileInfo(dirName + "dir").isDir()); QFileInfo fileInfo1(dirName + "dir/subdir/mediumfile2"); QVERIFY(fileInfo1.exists()); QVERIFY(fileInfo1.isFile()); QCOMPARE(fileInfo1.size(), Q_INT64_C(100)); QFileInfo fileInfo2(dirName + "hugefile"); QVERIFY(fileInfo2.exists()); QVERIFY(fileInfo2.isFile()); QCOMPARE(fileInfo2.size(), Q_INT64_C(20000)); QFileInfo fileInfo3(dirName + "mediumfile"); QVERIFY(fileInfo3.exists()); QVERIFY(fileInfo3.isFile()); QCOMPARE(fileInfo3.size(), Q_INT64_C(100)); QFileInfo fileInfo4(dirName + "my/dir/test3"); QVERIFY(fileInfo4.exists()); QVERIFY(fileInfo4.isFile()); QCOMPARE(fileInfo4.size(), Q_INT64_C(28)); #ifndef Q_OS_WIN const QString fileName = dirName + "z/test3_symlink"; const QFileInfo fileInfo5(fileName); QVERIFY(fileInfo5.exists()); QVERIFY(fileInfo5.isFile()); // Do not use fileInfo.readLink() for unix symlinks // It returns the -full- path to the target, while we want the target string "as is". QString symLinkTarget; const QByteArray encodedFileName = QFile::encodeName(fileName); QByteArray s; #if defined(PATH_MAX) s.resize(PATH_MAX + 1); #else int path_max = pathconf(encodedFileName.data(), _PC_PATH_MAX); if (path_max <= 0) { path_max = 4096; } s.resize(path_max); #endif int len = readlink(encodedFileName.data(), s.data(), s.size() - 1); if (len >= 0) { s[len] = '\0'; symLinkTarget = QFile::decodeName(s.constData()); } QCOMPARE(symLinkTarget, QString("test3")); #endif #ifdef Q_OS_WIN QScopedValueRollback ntfsMode(qt_ntfs_permission_lookup); qt_ntfs_permission_lookup++; #endif QVERIFY(QFileInfo(dirName + "executableAll").permissions() & (QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther)); } /** * Prepares dataset for archive filter tests */ void KArchiveTest::setupData() { QTest::addColumn("fileName"); QTest::addColumn("mimeType"); QTest::newRow(".tar.gz") << "karchivetest.tar.gz" << "application/x-gzip"; #if HAVE_BZIP2_SUPPORT QTest::newRow(".tar.bz2") << "karchivetest.tar.bz2" << "application/x-bzip"; #endif #if HAVE_XZ_SUPPORT QTest::newRow(".tar.lzma") << "karchivetest.tar.lzma" << "application/x-lzma"; QTest::newRow(".tar.xz") << "karchivetest.tar.xz" << "application/x-xz"; #endif } /** * @see QTest::initTestCase() */ void KArchiveTest::initTestCase() { #ifndef Q_OS_WIN // Prepare local symlink QFile::remove(QStringLiteral("test3_symlink")); if (::symlink("test3", "test3_symlink") != 0) { qDebug() << errno; QVERIFY(false); } #endif } void KArchiveTest::testEmptyFilename() { QTest::ignoreMessage(QtWarningMsg, "KArchive: No file name specified"); KTar tar(QLatin1String("")); QVERIFY(!tar.open(QIODevice::ReadOnly)); QCOMPARE(tar.errorString(), tr("No filename or device was specified")); } void KArchiveTest::testNullDevice() { QIODevice *nil = nullptr; QTest::ignoreMessage(QtWarningMsg, "KArchive: Null device specified"); KTar tar(nil); QVERIFY(!tar.open(QIODevice::ReadOnly)); QCOMPARE(tar.errorString(), tr("No filename or device was specified")); } void KArchiveTest::testNonExistentFile() { KTar tar(QStringLiteral("nonexistent.tar.gz")); QVERIFY(!tar.open(QIODevice::ReadOnly)); QCOMPARE(tar.errorString(), tr("File %1 does not exist").arg("nonexistent.tar.gz")); } void KArchiveTest::testCreateTar_data() { QTest::addColumn("fileName"); QTest::newRow(".tar") << "karchivetest.tar"; } /** * @dataProvider testCreateTar_data */ void KArchiveTest::testCreateTar() { QFETCH(QString, fileName); // With tempfile: 0.7-0.8 ms, 994236 instr. loads // Without tempfile: 0.81 ms, 992541 instr. loads // Note: use ./karchivetest 2>&1 | grep ms // to avoid being slowed down by the kDebugs. QBENCHMARK { KTar tar(fileName); QVERIFY(tar.open(QIODevice::WriteOnly)); writeTestFilesToArchive(&tar); QVERIFY(tar.close()); QFileInfo fileInfo(QFile::encodeName(fileName)); QVERIFY(fileInfo.exists()); // We can't check for an exact size because of the addLocalFile, whose data is system-dependent QVERIFY(fileInfo.size() > 450); } // NOTE The only .tar test, cleanup here //QFile::remove(fileName); } /** * @dataProvider setupData */ void KArchiveTest::testCreateTarXXX() { QFETCH(QString, fileName); // With tempfile: 1.3-1.7 ms, 2555089 instr. loads // Without tempfile: 0.87 ms, 987915 instr. loads QBENCHMARK { KTar tar(fileName); QVERIFY(tar.open(QIODevice::WriteOnly)); writeTestFilesToArchive(&tar); QVERIFY(tar.close()); QFileInfo fileInfo(QFile::encodeName(fileName)); QVERIFY(fileInfo.exists()); // We can't check for an exact size because of the addLocalFile, whose data is system-dependent QVERIFY(fileInfo.size() > 350); } } //static void compareEntryWithTimestamp(const QString &dateString, const QString &expectedDateString, const QDateTime &expectedDateTime) // Made it a macro so that line numbers are meaningful on failure #define compareEntryWithTimestamp(dateString, expectedDateString, expectedDateTime) \ { \ /* Take the time from the listing and chop it off */ \ const QDateTime dt = QDateTime::fromString(dateString.right(19), "dd.MM.yyyy hh:mm:ss"); \ QString _str(dateString); \ _str.chop(25); \ QCOMPARE(_str, expectedDateString); \ \ /* Compare the times separately with allowed 2 sec diversion */ \ if (dt.secsTo(expectedDateTime) > 2) { qWarning() << dt << "is too different from" << expectedDateTime; } \ QVERIFY(dt.secsTo(expectedDateTime) <= 2); \ } /** * @dataProvider setupData */ void KArchiveTest::testReadTar() // testCreateTarGz must have been run first. { QFETCH(QString, fileName); QFileInfo localFileData(QStringLiteral("test3")); const QString systemUserName = getCurrentUserName(); const QString systemGroupName = getCurrentGroupName(); const QString owner = localFileData.owner(); const QString group = localFileData.group(); const QString emptyTime = QDateTime().toString(QStringLiteral("dd.MM.yyyy hh:mm:ss")); const QDateTime creationTime = QFileInfo(fileName).created(); // 1.6-1.7 ms per interaction, 2908428 instruction loads // After the "no tempfile when writing fix" this went down // to 0.9-1.0 ms, 1689059 instruction loads. // I guess it finds the data in the kernel cache now that no KTempFile is // used when writing. QBENCHMARK { KTar tar(fileName); QVERIFY(tar.open(QIODevice::ReadOnly)); const KArchiveDirectory *dir = tar.directory(); QVERIFY(dir != nullptr); const QStringList listing = recursiveListEntries(dir, QLatin1String(""), WithUserGroup | WithTime); #ifndef Q_OS_WIN const int expectedCount = 16; #else const int expectedCount = 15; #endif if (listing.count() != expectedCount) { qWarning() << listing; } QCOMPARE(listing.count(), expectedCount); compareEntryWithTimestamp(listing[0], QString("mode=40755 user= group= path=aaaemptydir type=dir"), creationTime); QCOMPARE(listing[1], QString("mode=40777 user=%1 group=%2 path=dir type=dir time=%3").arg(systemUserName).arg(systemGroupName).arg(emptyTime)); QCOMPARE(listing[2], QString("mode=40777 user=%1 group=%2 path=dir/subdir type=dir time=%3").arg(systemUserName).arg(systemGroupName).arg(emptyTime)); compareEntryWithTimestamp(listing[3], QString("mode=100644 user= group= path=dir/subdir/mediumfile2 type=file size=100"), creationTime); compareEntryWithTimestamp(listing[4], QString("mode=100644 user=weis group=users path=empty type=file size=0"), creationTime); compareEntryWithTimestamp(listing[5], QString("mode=100755 user= group= path=executableAll type=file size=17"), creationTime); compareEntryWithTimestamp(listing[6], QString("mode=100644 user= group= path=hugefile type=file size=20000"), creationTime); compareEntryWithTimestamp(listing[7], QString("mode=100644 user= group= path=mediumfile type=file size=100"), creationTime); QCOMPARE(listing[8], QString("mode=40777 user=%1 group=%2 path=my type=dir time=").arg(systemUserName).arg(systemGroupName)); QCOMPARE(listing[9], QString("mode=40777 user=%1 group=%2 path=my/dir type=dir time=").arg(systemUserName).arg(systemGroupName)); compareEntryWithTimestamp(listing[10], QString("mode=100644 user=dfaure group=hackers path=my/dir/test3 type=file size=28"), creationTime); compareEntryWithTimestamp(listing[11], QString("mode=100440 user=weis group=users path=test1 type=file size=5"), creationTime); compareEntryWithTimestamp(listing[12], QString("mode=100644 user=weis group=users path=test2 type=file size=8"), creationTime); QCOMPARE(listing[13], QString("mode=40777 user=%1 group=%2 path=z type=dir time=").arg(systemUserName).arg(systemGroupName)); // This one was added with addLocalFile, so ignore mode. QString str = listing[14]; str.replace(QRegExp(QStringLiteral("mode.*user=")), QStringLiteral("user=")); compareEntryWithTimestamp(str, QString("user=%1 group=%2 path=z/test3 type=file size=13").arg(owner).arg(group), creationTime); #ifndef Q_OS_WIN str = listing[15]; str.replace(QRegExp(QStringLiteral("mode.*path=")), QStringLiteral("path=")); compareEntryWithTimestamp(str, QString("path=z/test3_symlink type=file size=0 symlink=test3"), creationTime); #endif QVERIFY(tar.close()); } } /** * This tests the decompression using kfilterdev, basically. * To debug KTarPrivate::fillTempFile(). * * @dataProvider setupData */ void KArchiveTest::testUncompress() { QFETCH(QString, fileName); QFETCH(QString, mimeType); // testCreateTar must have been run first. QVERIFY(QFile::exists(fileName)); KFilterDev filterDev(fileName); QByteArray buffer; buffer.resize(8 * 1024); //qDebug() << "buffer.size()=" << buffer.size(); QVERIFY(filterDev.open(QIODevice::ReadOnly)); qint64 totalSize = 0; qint64 len = -1; while (!filterDev.atEnd() && len != 0) { len = filterDev.read(buffer.data(), buffer.size()); QVERIFY(len >= 0); totalSize += len; // qDebug() << "read len=" << len << " totalSize=" << totalSize; } filterDev.close(); // qDebug() << "totalSize=" << totalSize; QVERIFY(totalSize > 26000); // 27648 here when using gunzip } /** * @dataProvider setupData */ void KArchiveTest::testTarFileData() { QFETCH(QString, fileName); // testCreateTar must have been run first. KTar tar(fileName); QVERIFY(tar.open(QIODevice::ReadOnly)); testFileData(&tar); QVERIFY(tar.close()); } /** * @dataProvider setupData */ void KArchiveTest::testTarCopyTo() { QFETCH(QString, fileName); // testCreateTar must have been run first. KTar tar(fileName); QVERIFY(tar.open(QIODevice::ReadOnly)); testCopyTo(&tar); QVERIFY(tar.close()); } /** * @dataProvider setupData */ void KArchiveTest::testTarReadWrite() { QFETCH(QString, fileName); // testCreateTar must have been run first. KTar tar(fileName); QVERIFY(tar.open(QIODevice::ReadWrite)); testReadWrite(&tar); testFileData(&tar); QVERIFY(tar.close()); // Reopen it and check it { KTar tar(fileName); QVERIFY(tar.open(QIODevice::ReadOnly)); testFileData(&tar); const KArchiveDirectory *dir = tar.directory(); const KArchiveEntry *e = dir->entry(QStringLiteral("newfile")); QVERIFY(e && e->isFile()); const KArchiveFile *f = (KArchiveFile *)e; QCOMPARE(f->data().size(), 8); } // NOTE This is the last test for this dataset. so cleanup here QFile::remove(fileName); } void KArchiveTest::testTarMaxLength_data() { QTest::addColumn("fileName"); QTest::newRow("maxlength.tar.gz") << "karchivetest-maxlength.tar.gz"; } /** * @dataProvider testTarMaxLength_data */ void KArchiveTest::testTarMaxLength() { QFETCH(QString, fileName); KTar tar(fileName); QVERIFY(tar.open(QIODevice::WriteOnly)); // Generate long filenames of each possible length bigger than 98... // Also exceed 512 byte block size limit to see how well the ././@LongLink // implementation fares for (int i = 98; i < 514; i++) { QString str, num; str.fill('a', i - 10); num.setNum(i); num = num.rightJustified(10, '0'); tar.writeFile(str + num, "hum"); } // Result of this test : works perfectly now (failed at 482 formerly and // before that at 154). QVERIFY(tar.close()); QVERIFY(tar.open(QIODevice::ReadOnly)); const KArchiveDirectory *dir = tar.directory(); QVERIFY(dir != nullptr); const QStringList listing = recursiveListEntries(dir, QLatin1String(""), WithUserGroup); QCOMPARE(listing[0], QString("mode=100644 user= group= path=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000098 type=file size=3")); QCOMPARE(listing[3], QString("mode=100644 user= group= path=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000101 type=file size=3")); QCOMPARE(listing[4], QString("mode=100644 user= group= path=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000102 type=file size=3")); QCOMPARE(listing.count(), 416); QVERIFY(tar.close()); // NOTE Cleanup here QFile::remove(fileName); } void KArchiveTest::testTarGlobalHeader() { KTar tar(QFINDTESTDATA(QLatin1String("global_header_test.tar.gz"))); QVERIFY2(tar.open(QIODevice::ReadOnly), "global_header_test.tar.gz"); const KArchiveDirectory *dir = tar.directory(); QVERIFY(dir != nullptr); const QStringList listing = recursiveListEntries(dir, QLatin1String(""), WithUserGroup); QCOMPARE(listing.count(), 2); QCOMPARE(listing[0], QString("mode=40775 user=root group=root path=Test type=dir")); QCOMPARE(listing[1], QString("mode=664 user=root group=root path=Test/test.txt type=file size=0")); QVERIFY(tar.close()); } void KArchiveTest::testTarPrefix() { KTar tar(QFINDTESTDATA(QLatin1String("tar_prefix_test.tar.gz"))); QVERIFY2(tar.open(QIODevice::ReadOnly), "tar_prefix_test.tar.gz"); const KArchiveDirectory *dir = tar.directory(); QVERIFY(dir != nullptr); const QStringList listing = recursiveListEntries(dir, QLatin1String(""), WithUserGroup); QCOMPARE(listing[0], QString("mode=40775 user=root group=root path=Test type=dir")); QCOMPARE(listing[1], QString("mode=40775 user=root group=root path=Test/qt-jambi-qtjambi-4_7 type=dir")); QCOMPARE(listing[2], QString("mode=40775 user=root group=root path=Test/qt-jambi-qtjambi-4_7/examples type=dir")); QCOMPARE(listing[3], QString("mode=40775 user=root group=root path=Test/qt-jambi-qtjambi-4_7/examples/generator type=dir")); QCOMPARE(listing[4], QString("mode=40775 user=root group=root path=Test/qt-jambi-qtjambi-4_7/examples/generator/trolltech_original type=dir")); QCOMPARE(listing[5], QString("mode=40775 user=root group=root path=Test/qt-jambi-qtjambi-4_7/examples/generator/trolltech_original/java type=dir")); QCOMPARE(listing[6], QString("mode=40775 user=root group=root path=Test/qt-jambi-qtjambi-4_7/examples/generator/trolltech_original/java/com type=dir")); QCOMPARE(listing[7], QString("mode=40775 user=root group=root path=Test/qt-jambi-qtjambi-4_7/examples/generator/trolltech_original/java/com/trolltech type=dir")); QCOMPARE(listing[8], QString("mode=40775 user=root group=root path=Test/qt-jambi-qtjambi-4_7/examples/generator/trolltech_original/java/com/trolltech/examples type=dir")); QCOMPARE(listing[9], QString("mode=664 user=root group=root path=Test/qt-jambi-qtjambi-4_7/examples/generator/trolltech_original/java/com/trolltech/examples/GeneratorExample.html type=file size=43086")); QCOMPARE(listing.count(), 10); QVERIFY(tar.close()); } void KArchiveTest::testTarDirectoryForgotten() { KTar tar(QFINDTESTDATA(QLatin1String("tar_directory_forgotten.tar.gz"))); QVERIFY2(tar.open(QIODevice::ReadOnly), "tar_directory_forgotten.tar.gz"); const KArchiveDirectory *dir = tar.directory(); QVERIFY(dir != nullptr); const QStringList listing = recursiveListEntries(dir, QLatin1String(""), WithUserGroup); QVERIFY(listing[9].contains("trolltech/examples/generator")); QVERIFY(listing[10].contains("trolltech/examples/generator/GeneratorExample.html")); QCOMPARE(listing.count(), 11); QVERIFY(tar.close()); } +void KArchiveTest::testTarEmptyFileMissingDir() +{ + KTar tar(QFINDTESTDATA(QLatin1String("tar_emptyfile_missingdir.tar.gz"))); + QVERIFY(tar.open(QIODevice::ReadOnly)); + + const KArchiveDirectory *dir = tar.directory(); + QVERIFY(dir != nullptr); + + const QStringList listing = recursiveListEntries(dir, QLatin1String(""), 0); + + QCOMPARE(listing[0], QString("mode=40777 path=dir type=dir")); + QCOMPARE(listing[1], QString("mode=40777 path=dir/foo type=dir")); + QCOMPARE(listing[2], QString("mode=644 path=dir/foo/file type=file size=0")); + QCOMPARE(listing.count(), 3); + + QVERIFY(tar.close()); +} + void KArchiveTest::testTarRootDir() // bug 309463 { KTar tar(QFINDTESTDATA(QLatin1String("tar_rootdir.tar.gz"))); QVERIFY2(tar.open(QIODevice::ReadOnly), qPrintable(tar.fileName())); const KArchiveDirectory *dir = tar.directory(); QVERIFY(dir != nullptr); const QStringList listing = recursiveListEntries(dir, QLatin1String(""), WithUserGroup); //qDebug() << listing.join("\n"); QVERIFY(listing[0].contains("%{APPNAME}.cpp")); QVERIFY(listing[1].contains("%{APPNAME}.h")); QVERIFY(listing[5].contains("main.cpp")); QCOMPARE(listing.count(), 10); } void KArchiveTest::testTarLongNonASCIINames() // bug 266141 { const QString tarName = QString("karchive-long-non-ascii-names.tar"); const QString longName = QString("раз-два-три-четыре-пять-вышел-зайчик-погулять-вдруг-охотник-" "выбегает-прямо-в-зайчика.txt"); { KTar tar(tarName); QVERIFY(tar.open(QIODevice::WriteOnly)); QVERIFY(tar.writeFile(longName, "", 0644, "user", "users")); QVERIFY(tar.close()); } { KTar tar(tarName); QVERIFY(tar.open(QIODevice::ReadOnly)); const KArchiveDirectory *dir = tar.directory(); QVERIFY(dir != nullptr); const QStringList listing = recursiveListEntries(dir, QString(""), 0); const QString expectedListingEntry = QString("mode=644 path=") + longName + QString(" type=file size=0"); QCOMPARE(listing.count(), 1); QCOMPARE(listing[0], expectedListingEntry); QVERIFY(tar.close()); } } void KArchiveTest::testTarShortNonASCIINames() // bug 266141 { KTar tar(QFINDTESTDATA(QString("tar_non_ascii_file_name.tar.gz"))); QVERIFY(tar.open(QIODevice::ReadOnly)); const KArchiveDirectory *dir = tar.directory(); QVERIFY(dir != nullptr); const QStringList listing = recursiveListEntries(dir, QString(""), 0); QCOMPARE(listing.count(), 1); QCOMPARE(listing[0], QString("mode=644 path=абвгдеёжзийклмнопрстуфхцчшщъыьэюя.txt" " type=file size=0")); QVERIFY(tar.close()); } void KArchiveTest::testTarDirectoryTwice() // bug 206994 { KTar tar(QFINDTESTDATA(QLatin1String("tar_directory_twice.tar.gz"))); QVERIFY(tar.open(QIODevice::ReadOnly)); const KArchiveDirectory *dir = tar.directory(); QVERIFY(dir != nullptr); const QStringList listing = recursiveListEntries(dir, QLatin1String(""), WithUserGroup); //qDebug() << listing.join("\n"); QVERIFY(listing[0].contains("path=d")); QVERIFY(listing[1].contains("path=d/f1.txt")); QVERIFY(listing[2].contains("path=d/f2.txt")); QCOMPARE(listing.count(), 3); } void KArchiveTest::testTarIgnoreRelativePathOutsideArchive() { #if HAVE_BZIP2_SUPPORT // This test extracts a Tar archive that contains a relative path "../foo" pointing // outside of the archive directory. For security reasons extractions should only // be allowed within the extracted directory as long as not specifically asked. KTar tar(QFINDTESTDATA(QLatin1String("tar_relative_path_outside_archive.tar.bz2"))); QVERIFY(tar.open(QIODevice::ReadOnly)); const KArchiveDirectory *dir = tar.directory(); QTemporaryDir tmpDir; const QString dirName = tmpDir.path() + "/subdir"; // use a subdir so /tmp/foo doesn't break the test :) QDir().mkdir(dirName); QVERIFY(dir->copyTo(dirName)); QVERIFY(!QFile::exists(dirName + "../foo")); QVERIFY(QFile::exists(dirName + "/foo")); #else QSKIP("Test data is in bz2 format and karchive is built without bzip2 format"); #endif } /// static const char s_zipFileName[] = "karchivetest.zip"; static const char s_zipMaxLengthFileName[] = "karchivetest-maxlength.zip"; static const char s_zipLocaleFileName[] = "karchivetest-locale.zip"; static const char s_zipMimeType[] = "application/vnd.oasis.opendocument.text"; void KArchiveTest::testCreateZip() { KZip zip(s_zipFileName); QVERIFY(zip.open(QIODevice::WriteOnly)); zip.setExtraField(KZip::NoExtraField); zip.setCompression(KZip::NoCompression); QByteArray zipMimeType(s_zipMimeType); zip.writeFile(QStringLiteral("mimetype"), zipMimeType); zip.setCompression(KZip::DeflateCompression); writeTestFilesToArchive(&zip); QVERIFY(zip.close()); QFile zipFile(QFile::encodeName(s_zipFileName)); QFileInfo fileInfo(zipFile); QVERIFY(fileInfo.exists()); QVERIFY(fileInfo.size() > 300); // Check that the header with no-compression and no-extrafield worked. // (This is for the "magic" for koffice documents) QVERIFY(zipFile.open(QIODevice::ReadOnly)); QByteArray arr = zipFile.read(4); QCOMPARE(arr, QByteArray("PK\003\004")); QVERIFY(zipFile.seek(30)); arr = zipFile.read(8); QCOMPARE(arr, QByteArray("mimetype")); arr = zipFile.read(zipMimeType.size()); QCOMPARE(arr, zipMimeType); } void KArchiveTest::testCreateZipError() { // Giving a directory name to kzip must give an error case in close(), see #136630. // Otherwise we just lose data. KZip zip(QDir::currentPath()); QVERIFY(!zip.open(QIODevice::WriteOnly)); QCOMPARE( zip.errorString(), tr("QSaveFile creation for %1 failed: Filename refers to a directory") .arg(QDir::currentPath())); } void KArchiveTest::testReadZipError() { QFile brokenZip(QStringLiteral("broken.zip")); QVERIFY(brokenZip.open(QIODevice::WriteOnly)); // incomplete magic brokenZip.write(QByteArray("PK\003")); brokenZip.close(); { KZip zip(QStringLiteral("broken.zip")); QVERIFY(!zip.open(QIODevice::ReadOnly)); QCOMPARE( zip.errorString(), tr("Invalid ZIP file. Unexpected end of file. (Error code: %1)").arg(1)); QVERIFY(brokenZip.open(QIODevice::WriteOnly | QIODevice::Append)); // add rest of magic, but still incomplete header brokenZip.write(QByteArray("\004\000\000\000\000")); brokenZip.close(); QVERIFY(!zip.open(QIODevice::ReadOnly)); QCOMPARE( zip.errorString(), tr("Invalid ZIP file. Unexpected end of file. (Error code: %1)").arg(4)); } QVERIFY(brokenZip.remove()); } void KArchiveTest::testReadZip() { // testCreateZip must have been run first. KZip zip(s_zipFileName); QVERIFY(zip.open(QIODevice::ReadOnly)); const KArchiveDirectory *dir = zip.directory(); QVERIFY(dir != nullptr); // ZIP has no support for per-file user/group, so omit them from the listing const QStringList listing = recursiveListEntries(dir, QLatin1String(""), 0); #ifndef Q_OS_WIN QCOMPARE(listing.count(), 17); #else QCOMPARE(listing.count(), 16); #endif QCOMPARE(listing[0], QString("mode=40755 path=aaaemptydir type=dir")); QCOMPARE(listing[1], QString("mode=40777 path=dir type=dir")); QCOMPARE(listing[2], QString("mode=40777 path=dir/subdir type=dir")); QCOMPARE(listing[3], QString("mode=100644 path=dir/subdir/mediumfile2 type=file size=100")); QCOMPARE(listing[4], QString("mode=100644 path=empty type=file size=0")); QCOMPARE(listing[5], QString("mode=100755 path=executableAll type=file size=17")); QCOMPARE(listing[6], QString("mode=100644 path=hugefile type=file size=20000")); QCOMPARE(listing[7], QString("mode=100644 path=mediumfile type=file size=100")); QCOMPARE(listing[8], QString("mode=100644 path=mimetype type=file size=%1").arg(strlen(s_zipMimeType))); QCOMPARE(listing[9], QString("mode=40777 path=my type=dir")); QCOMPARE(listing[10], QString("mode=40777 path=my/dir type=dir")); QCOMPARE(listing[11], QString("mode=100644 path=my/dir/test3 type=file size=28")); QCOMPARE(listing[12], QString("mode=100440 path=test1 type=file size=5")); QCOMPARE(listing[13], QString("mode=100644 path=test2 type=file size=8")); QCOMPARE(listing[14], QString("mode=40777 path=z type=dir")); // This one was added with addLocalFile, so ignore mode QString str = listing[15]; str.replace(QRegExp(QStringLiteral("mode.*path=")), QStringLiteral("path=")); QCOMPARE(str, QString("path=z/test3 type=file size=13")); #ifndef Q_OS_WIN str = listing[16]; str.replace(QRegExp(QStringLiteral("mode.*path=")), QStringLiteral("path=")); QCOMPARE(str, QString("path=z/test3_symlink type=file size=5 symlink=test3")); #endif QVERIFY(zip.close()); } void KArchiveTest::testZipFileData() { // testCreateZip must have been run first. KZip zip(s_zipFileName); QVERIFY(zip.open(QIODevice::ReadOnly)); testFileData(&zip); QVERIFY(zip.close()); } void KArchiveTest::testZipCopyTo() { // testCreateZip must have been run first. KZip zip(s_zipFileName); QVERIFY(zip.open(QIODevice::ReadOnly)); testCopyTo(&zip); QVERIFY(zip.close()); } void KArchiveTest::testZipMaxLength() { KZip zip(s_zipMaxLengthFileName); QVERIFY(zip.open(QIODevice::WriteOnly)); // Similar to testTarMaxLength just to make sure, but of course zip doesn't have // those limitations in the first place. for (int i = 98; i < 514; i++) { QString str, num; str.fill('a', i - 10); num.setNum(i); num = num.rightJustified(10, '0'); zip.writeFile(str + num, "hum"); } QVERIFY(zip.close()); QVERIFY(zip.open(QIODevice::ReadOnly)); const KArchiveDirectory *dir = zip.directory(); QVERIFY(dir != nullptr); const QStringList listing = recursiveListEntries(dir, QLatin1String(""), 0); QCOMPARE(listing[0], QString("mode=100644 path=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000098 type=file size=3")); QCOMPARE(listing[3], QString("mode=100644 path=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000101 type=file size=3")); QCOMPARE(listing[4], QString("mode=100644 path=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000102 type=file size=3")); QCOMPARE(listing.count(), 514 - 98); QVERIFY(zip.close()); } void KArchiveTest::testZipWithNonLatinFileNames() { KZip zip(s_zipLocaleFileName); QVERIFY(zip.open(QIODevice::WriteOnly)); const QByteArray fileData("Test of data with a russian file name"); const QString fileName = QStringLiteral("Архитектура.okular"); const QString recodedFileName = QFile::decodeName(QFile::encodeName(fileName)); QVERIFY(zip.writeFile(fileName, fileData)); QVERIFY(zip.close()); QVERIFY(zip.open(QIODevice::ReadOnly)); const KArchiveDirectory *dir = zip.directory(); QVERIFY(dir != nullptr); const QStringList listing = recursiveListEntries(dir, QLatin1String(""), 0); QCOMPARE(listing.count(), 1); QCOMPARE(listing[0], QString::fromUtf8("mode=100644 path=%1 type=file size=%2").arg(recodedFileName).arg(fileData.size())); const KArchiveFile *fileEntry = static_cast< const KArchiveFile *>(dir->entry(dir->entries()[0])); QCOMPARE(fileEntry->data(), fileData); } void KArchiveTest::testZipWithOverwrittenFileName() { KZip zip(s_zipFileName); QVERIFY(zip.open(QIODevice::WriteOnly)); const QByteArray fileData1("There could be a fire, if there is smoke."); const QString fileName = QStringLiteral("wisdom"); QVERIFY(zip.writeFile(fileName, fileData1, 0100644, "konqi", "dragons")); // now overwrite it const QByteArray fileData2("If there is smoke, there could be a fire."); QVERIFY(zip.writeFile(fileName, fileData2, 0100644, "konqi", "dragons")); QVERIFY(zip.close()); QVERIFY(zip.open(QIODevice::ReadOnly)); const KArchiveDirectory *dir = zip.directory(); QVERIFY(dir != nullptr); const QStringList listing = recursiveListEntries(dir, QLatin1String(""), 0); QCOMPARE(listing.count(), 1); QCOMPARE(listing[0], QString::fromUtf8("mode=100644 path=%1 type=file size=%2").arg(fileName).arg(fileData2.size())); const KArchiveFile *fileEntry = static_cast< const KArchiveFile *>(dir->entry(dir->entries()[0])); QCOMPARE(fileEntry->data(), fileData2); } static bool writeFile(const QString &dirName, const QString &fileName, const QByteArray &data) { Q_ASSERT(dirName.endsWith('/')); QFile file(dirName + fileName); if (!file.open(QIODevice::WriteOnly)) { return false; } file.write(data); return true; } void KArchiveTest::testZipAddLocalDirectory() { // Prepare local dir QTemporaryDir tmpDir; const QString dirName = tmpDir.path() + '/'; const QByteArray file1Data = "Hello Shantanu"; const QString file1 = QStringLiteral("file1"); QVERIFY(writeFile(dirName, file1, file1Data)); { KZip zip(s_zipFileName); QVERIFY(zip.open(QIODevice::WriteOnly)); QVERIFY(zip.addLocalDirectory(dirName, ".")); QVERIFY(zip.close()); } { KZip zip(s_zipFileName); QVERIFY(zip.open(QIODevice::ReadOnly)); const KArchiveDirectory *dir = zip.directory(); QVERIFY(dir != nullptr); const KArchiveEntry *e = dir->entry(file1); QVERIFY(e && e->isFile()); const KArchiveFile *f = (KArchiveFile *)e; QCOMPARE(f->data(), file1Data); } } void KArchiveTest::testZipReadRedundantDataDescriptor_data() { QTest::addColumn("fileName"); QTest::newRow("noSignature") << "data/redundantDataDescriptorsNoSignature.zip"; QTest::newRow("withSignature") << "data/redundantDataDescriptorsWithSignature.zip"; } /** * @dataProvider testZipReadRedundantDataDescriptor_data */ void KArchiveTest::testZipReadRedundantDataDescriptor() { QFETCH(QString, fileName); const QString redundantDataDescriptorZipFileName = QFINDTESTDATA(fileName); QVERIFY(!redundantDataDescriptorZipFileName.isEmpty()); KZip zip(redundantDataDescriptorZipFileName); QVERIFY(zip.open(QIODevice::ReadOnly)); const KArchiveDirectory *dir = zip.directory(); QVERIFY(dir != nullptr); const QByteArray fileData("aaaaaaaaaaaaaaa"); // ZIP has no support for per-file user/group, so omit them from the listing const QStringList listing = recursiveListEntries(dir, QLatin1String(""), 0); QCOMPARE(listing.count(), 2); QCOMPARE(listing[0], QString::fromUtf8("mode=100644 path=compressed type=file size=%2").arg(fileData.size())); QCOMPARE(listing[1], QString::fromUtf8("mode=100644 path=uncompressed type=file size=%2").arg(fileData.size())); const KArchiveFile *fileEntry = static_cast< const KArchiveFile *>(dir->entry(dir->entries()[0])); QCOMPARE(fileEntry->data(), fileData); fileEntry = static_cast< const KArchiveFile *>(dir->entry(dir->entries()[1])); QCOMPARE(fileEntry->data(), fileData); } void KArchiveTest::testZipDirectoryPermissions() { QString fileName = QFINDTESTDATA("data/dirpermissions.zip"); QVERIFY(!fileName.isEmpty()); KZip zip(fileName); QVERIFY(zip.open(QIODevice::ReadOnly)); const KArchiveDirectory *dir = zip.directory(); const QStringList listing = recursiveListEntries(dir, QString(), 0); QCOMPARE(listing.join(' '), QString::fromUtf8("mode=40700 path=700 type=dir mode=40750 path=750 type=dir mode=40755 path=755 type=dir")); } void KArchiveTest::testZipUnusualButValid() { QString fileName = QFINDTESTDATA("data/unusual_but_valid_364071.zip"); QVERIFY(!fileName.isEmpty()); KZip zip(fileName); QVERIFY(zip.open(QIODevice::ReadOnly)); const KArchiveDirectory *dir = zip.directory(); const QStringList listing = recursiveListEntries(dir, QString(), 0); QCOMPARE(listing.join(' '), QLatin1String("mode=40744 path=test type=dir mode=744 path=test/os-release type=file size=199")); } void KArchiveTest::testZipDuplicateNames() { QString fileName = QFINDTESTDATA("data/out.epub"); QVERIFY(!fileName.isEmpty()); KZip zip(fileName); QVERIFY(zip.open(QIODevice::ReadOnly)); int metaInfCount = 0; for (const QString &entryName : zip.directory()->entries()) { if (entryName.startsWith("META-INF")) { metaInfCount++; } } QVERIFY2(metaInfCount == 1, "Archive root directory contains duplicates"); } void KArchiveTest::testRcc() { const QString rccFile = QFINDTESTDATA("runtime_resource.rcc"); // was copied from qtbase/tests/auto/corelib/io/qresourceengine QVERIFY(!rccFile.isEmpty()); KRcc rcc(rccFile); QVERIFY(rcc.open(QIODevice::ReadOnly)); const KArchiveDirectory *rootDir = rcc.directory(); QVERIFY(rootDir != nullptr); const KArchiveEntry *rrEntry = rootDir->entry(QStringLiteral("runtime_resource")); QVERIFY(rrEntry && rrEntry->isDirectory()); const KArchiveDirectory *rrDir = static_cast(rrEntry); const KArchiveEntry *fileEntry = rrDir->entry(QStringLiteral("search_file.txt")); QVERIFY(fileEntry && fileEntry->isFile()); const KArchiveFile *searchFile = static_cast(fileEntry); const QByteArray fileData = searchFile->data(); QCOMPARE(QString::fromLatin1(fileData), QString::fromLatin1("root\n")); } /** * @see QTest::cleanupTestCase() */ void KArchiveTest::cleanupTestCase() { QFile::remove(s_zipMaxLengthFileName); QFile::remove(s_zipFileName); QFile::remove(s_zipLocaleFileName); #ifndef Q_OS_WIN QFile::remove(QStringLiteral("test3_symlink")); #endif } /// #if HAVE_XZ_SUPPORT /** * Prepares dataset for archive filter tests */ void KArchiveTest::setup7ZipData() { QTest::addColumn("fileName"); QTest::newRow(".7z") << "karchivetest.7z"; } /** * @dataProvider testCreate7Zip_data */ void KArchiveTest::testCreate7Zip() { QFETCH(QString, fileName); QBENCHMARK { K7Zip k7zip(fileName); QVERIFY(k7zip.open(QIODevice::WriteOnly)); writeTestFilesToArchive(&k7zip); QVERIFY(k7zip.close()); QFileInfo fileInfo(QFile::encodeName(fileName)); QVERIFY(fileInfo.exists()); //qDebug() << "fileInfo.size()" << fileInfo.size(); // We can't check for an exact size because of the addLocalFile, whose data is system-dependent QVERIFY(fileInfo.size() > 390); } } /** * @dataProvider setupData */ void KArchiveTest::testRead7Zip() // testCreate7Zip must have been run first. { QFETCH(QString, fileName); QBENCHMARK { K7Zip k7zip(fileName); QVERIFY(k7zip.open(QIODevice::ReadOnly)); const KArchiveDirectory *dir = k7zip.directory(); QVERIFY(dir != nullptr); const QStringList listing = recursiveListEntries(dir, QLatin1String(""), 0); #ifndef Q_OS_WIN QCOMPARE(listing.count(), 16); #else QCOMPARE(listing.count(), 15); #endif QCOMPARE(listing[0], QString("mode=40755 path=aaaemptydir type=dir")); QCOMPARE(listing[1], QString("mode=40777 path=dir type=dir")); QCOMPARE(listing[2], QString("mode=40777 path=dir/subdir type=dir")); QCOMPARE(listing[3], QString("mode=100644 path=dir/subdir/mediumfile2 type=file size=100")); QCOMPARE(listing[4], QString("mode=100644 path=empty type=file size=0")); QCOMPARE(listing[5], QString("mode=100755 path=executableAll type=file size=17")); QCOMPARE(listing[6], QString("mode=100644 path=hugefile type=file size=20000")); QCOMPARE(listing[7], QString("mode=100644 path=mediumfile type=file size=100")); QCOMPARE(listing[8], QString("mode=40777 path=my type=dir")); QCOMPARE(listing[9], QString("mode=40777 path=my/dir type=dir")); QCOMPARE(listing[10], QString("mode=100644 path=my/dir/test3 type=file size=28")); QCOMPARE(listing[11], QString("mode=100440 path=test1 type=file size=5")); QCOMPARE(listing[12], QString("mode=100644 path=test2 type=file size=8")); QCOMPARE(listing[13], QString("mode=40777 path=z type=dir")); // This one was added with addLocalFile, so ignore mode/user/group. QString str = listing[14]; str.replace(QRegExp(QStringLiteral("mode.*path=")), QStringLiteral("path=")); QCOMPARE(str, QString("path=z/test3 type=file size=13")); #ifndef Q_OS_WIN str = listing[15]; str.replace(QRegExp(QStringLiteral("mode.*path=")), QStringLiteral("path=")); QCOMPARE(str, QString("path=z/test3_symlink type=file size=0 symlink=test3")); #endif QVERIFY(k7zip.close()); } } /** * @dataProvider setupData */ void KArchiveTest::test7ZipFileData() { QFETCH(QString, fileName); // testCreate7Zip must have been run first. K7Zip k7zip(fileName); QVERIFY(k7zip.open(QIODevice::ReadOnly)); testFileData(&k7zip); QVERIFY(k7zip.close()); } /** * @dataProvider setupData */ void KArchiveTest::test7ZipCopyTo() { QFETCH(QString, fileName); // testCreateTar must have been run first. K7Zip k7zip(fileName); QVERIFY(k7zip.open(QIODevice::ReadOnly)); testCopyTo(&k7zip); QVERIFY(k7zip.close()); } /** * @dataProvider setupData */ void KArchiveTest::test7ZipReadWrite() { QFETCH(QString, fileName); // testCreate7zip must have been run first. K7Zip k7zip(fileName); QVERIFY(k7zip.open(QIODevice::ReadWrite)); testReadWrite(&k7zip); testFileData(&k7zip); QVERIFY(k7zip.close()); // Reopen it and check it { K7Zip k7zip(fileName); QVERIFY(k7zip.open(QIODevice::ReadOnly)); testFileData(&k7zip); const KArchiveDirectory *dir = k7zip.directory(); const KArchiveEntry *e = dir->entry(QStringLiteral("newfile")); QVERIFY(e && e->isFile()); const KArchiveFile *f = (KArchiveFile *)e; QCOMPARE(f->data().size(), 8); } // NOTE This is the last test for this dataset. so cleanup here QFile::remove(fileName); } /** * @dataProvider test7ZipMaxLength_data */ void KArchiveTest::test7ZipMaxLength() { QFETCH(QString, fileName); K7Zip k7zip(fileName); QVERIFY(k7zip.open(QIODevice::WriteOnly)); // Generate long filenames of each possible length bigger than 98... for (int i = 98; i < 514; i++) { QString str, num; str.fill('a', i - 10); num.setNum(i); num = num.rightJustified(10, '0'); k7zip.writeFile(str + num, "hum"); } QVERIFY(k7zip.close()); QVERIFY(k7zip.open(QIODevice::ReadOnly)); const KArchiveDirectory *dir = k7zip.directory(); QVERIFY(dir != nullptr); const QStringList listing = recursiveListEntries(dir, QLatin1String(""), 0); QCOMPARE(listing[0], QString("mode=100644 path=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000098 type=file size=3")); QCOMPARE(listing[3], QString("mode=100644 path=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000101 type=file size=3")); QCOMPARE(listing[4], QString("mode=100644 path=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000102 type=file size=3")); QCOMPARE(listing.count(), 416); QVERIFY(k7zip.close()); // NOTE Cleanup here QFile::remove(fileName); } #endif diff --git a/autotests/karchivetest.h b/autotests/karchivetest.h index 8dc0f98..079cfca 100644 --- a/autotests/karchivetest.h +++ b/autotests/karchivetest.h @@ -1,137 +1,138 @@ /* This file is part of the KDE project Copyright (C) 2006 David Faure Copyright (C) 2012 Mario Bensi This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KARCHIVETEST_H #define KARCHIVETEST_H #include #include class KArchiveTest : public QObject { Q_OBJECT void setupData(); void setup7ZipData(); private Q_SLOTS: void initTestCase(); void testEmptyFilename(); void testNullDevice(); void testNonExistentFile(); void testCreateTar_data(); void testCreateTar(); void testCreateTarXXX_data() { setupData(); } void testCreateTarXXX(); void testReadTar_data() { setupData(); } void testReadTar(); void testUncompress_data() { setupData(); } void testUncompress(); void testTarFileData_data() { setupData(); } void testTarFileData(); void testTarCopyTo_data() { setupData(); } void testTarCopyTo(); void testTarReadWrite_data() { setupData(); } void testTarReadWrite(); void testTarMaxLength_data(); void testTarMaxLength(); void testTarGlobalHeader(); void testTarPrefix(); void testTarDirectoryForgotten(); + void testTarEmptyFileMissingDir(); void testTarRootDir(); void testTarDirectoryTwice(); void testTarIgnoreRelativePathOutsideArchive(); void testTarLongNonASCIINames(); void testTarShortNonASCIINames(); void testCreateZip(); void testCreateZipError(); void testReadZipError(); void testReadZip(); void testZipFileData(); void testZipCopyTo(); void testZipMaxLength(); void testZipWithNonLatinFileNames(); void testZipWithOverwrittenFileName(); void testZipAddLocalDirectory(); void testZipReadRedundantDataDescriptor_data(); void testZipReadRedundantDataDescriptor(); void testZipDirectoryPermissions(); void testZipUnusualButValid(); void testZipDuplicateNames(); void testRcc(); #if HAVE_XZ_SUPPORT void testCreate7Zip_data() { setup7ZipData(); } void testCreate7Zip(); void testRead7Zip_data() { setup7ZipData(); } void testRead7Zip(); void test7ZipFileData_data() { setup7ZipData(); } void test7ZipFileData(); void test7ZipCopyTo_data() { setup7ZipData(); } void test7ZipCopyTo(); void test7ZipReadWrite_data() { setup7ZipData(); } void test7ZipReadWrite(); void test7ZipMaxLength_data() { setup7ZipData(); } void test7ZipMaxLength(); #endif void cleanupTestCase(); }; #endif diff --git a/autotests/tar_emptyfile_missingdir.tar.gz b/autotests/tar_emptyfile_missingdir.tar.gz new file mode 100644 index 0000000..12aac6e Binary files /dev/null and b/autotests/tar_emptyfile_missingdir.tar.gz differ diff --git a/src/karchive.cpp b/src/karchive.cpp index e83eaf5..5bf0af3 100644 --- a/src/karchive.cpp +++ b/src/karchive.cpp @@ -1,1007 +1,1028 @@ /* This file is part of the KDE libraries Copyright (C) 2000-2005 David Faure Copyright (C) 2003 Leo Savernik Moved from ktar.cpp by Roberto Teixeira This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "karchive.h" #include "karchive_p.h" #include "klimitediodevice_p.h" #include "loggingcategory.h" #include // QT_STATBUF, QT_LSTAT #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_UNIX #include #include #include // PATH_MAX #include #endif #ifdef Q_OS_WIN #include // DWORD, GetUserNameW #endif // Q_OS_WIN +//////////////////////////////////////////////////////////////////////// +/////////////////// KArchiveDirectoryPrivate /////////////////////////// +//////////////////////////////////////////////////////////////////////// + +class KArchiveDirectoryPrivate +{ +public: + KArchiveDirectoryPrivate(KArchiveDirectory *parent) : q(parent) + { + } + + ~KArchiveDirectoryPrivate() + { + qDeleteAll(entries); + } + + KArchiveDirectoryPrivate(const KArchiveDirectoryPrivate &) = delete; + KArchiveDirectoryPrivate &operator=(const KArchiveDirectoryPrivate &) = delete; + + static KArchiveDirectoryPrivate *get(KArchiveDirectory *directory) + { + return directory->d; + } + + // Returns in containingDirectory the directory that actually contains the returned entry + const KArchiveEntry *entry(const QString &_name, KArchiveDirectory **containingDirectory) const + { + *containingDirectory = q; + + QString name = QDir::cleanPath(_name); + int pos = name.indexOf(QLatin1Char('/')); + if (pos == 0) { // ouch absolute path (see also KArchive::findOrCreate) + if (name.length() > 1) { + name = name.mid(1); // remove leading slash + pos = name.indexOf(QLatin1Char('/')); // look again + } else { // "/" + return q; + } + } + // trailing slash ? -> remove + if (pos != -1 && pos == name.length() - 1) { + name = name.left(pos); + pos = name.indexOf(QLatin1Char('/')); // look again + } + if (pos != -1) { + const QString left = name.left(pos); + const QString right = name.mid(pos + 1); + + //qCDebug(KArchiveLog) << "left=" << left << "right=" << right; + + KArchiveEntry *e = entries.value(left); + if (!e || !e->isDirectory()) { + return nullptr; + } + *containingDirectory = static_cast(e); + return (*containingDirectory)->d->entry(right, containingDirectory); + } + + return entries.value(name); + } + + KArchiveDirectory *q; + QHash entries; +}; + //////////////////////////////////////////////////////////////////////// /////////////////////////// KArchive /////////////////////////////////// //////////////////////////////////////////////////////////////////////// KArchive::KArchive(const QString &fileName) : d(new KArchivePrivate(this)) { if (fileName.isEmpty()) { qCWarning(KArchiveLog) << "KArchive: No file name specified"; } d->fileName = fileName; // This constructor leaves the device set to 0. // This is for the use of QSaveFile, see open(). } KArchive::KArchive(QIODevice *dev) : d(new KArchivePrivate(this)) { if (!dev) { qCWarning(KArchiveLog) << "KArchive: Null device specified"; } d->dev = dev; } KArchive::~KArchive() { Q_ASSERT(!isOpen()); // the derived class destructor must have closed already delete d; } bool KArchive::open(QIODevice::OpenMode mode) { Q_ASSERT(mode != QIODevice::NotOpen); if (isOpen()) { close(); } if (!d->fileName.isEmpty()) { Q_ASSERT(!d->dev); if (!createDevice(mode)) { return false; } } if (!d->dev) { setErrorString(tr("No filename or device was specified")); return false; } if (!d->dev->isOpen() && !d->dev->open(mode)) { setErrorString(tr("Could not set device mode to %1").arg(mode)); return false; } d->mode = mode; Q_ASSERT(!d->rootDir); d->rootDir = nullptr; return openArchive(mode); } bool KArchive::createDevice(QIODevice::OpenMode mode) { switch (mode) { case QIODevice::WriteOnly: if (!d->fileName.isEmpty()) { // The use of QSaveFile can't be done in the ctor (no mode known yet) //qCDebug(KArchiveLog) << "Writing to a file using QSaveFile"; d->saveFile = new QSaveFile(d->fileName); if (!d->saveFile->open(QIODevice::WriteOnly)) { setErrorString( tr("QSaveFile creation for %1 failed: %2") .arg(d->fileName, d->saveFile->errorString())); delete d->saveFile; d->saveFile = nullptr; return false; } d->dev = d->saveFile; Q_ASSERT(d->dev); } break; case QIODevice::ReadOnly: case QIODevice::ReadWrite: // ReadWrite mode still uses QFile for now; we'd need to copy to the tempfile, in fact. if (!d->fileName.isEmpty()) { d->dev = new QFile(d->fileName); d->deviceOwned = true; } break; // continued below default: setErrorString(tr("Unsupported mode %1").arg(d->mode)); return false; } return true; } bool KArchive::close() { if (!isOpen()) { setErrorString(tr("Archive already closed")); return false; // already closed (return false or true? arguable...) } // moved by holger to allow kzip to write the zip central dir // to the file in closeArchive() // DF: added d->dev so that we skip closeArchive if saving aborted. bool closeSucceeded = true; if (d->dev) { closeSucceeded = closeArchive(); if (d->mode == QIODevice::WriteOnly && !closeSucceeded) { d->abortWriting(); } } if (d->dev && d->dev != d->saveFile) { d->dev->close(); } // if d->saveFile is not null then it is equal to d->dev. if (d->saveFile) { closeSucceeded = d->saveFile->commit(); delete d->saveFile; d->saveFile = nullptr; } if (d->deviceOwned) { delete d->dev; // we created it ourselves in open() } delete d->rootDir; d->rootDir = nullptr; d->mode = QIODevice::NotOpen; d->dev = nullptr; return closeSucceeded; } QString KArchive::errorString() const { return d->errorStr; } const KArchiveDirectory *KArchive::directory() const { // rootDir isn't const so that parsing-on-demand is possible return const_cast(this)->rootDir(); } bool KArchive::addLocalFile(const QString &fileName, const QString &destName) { QFileInfo fileInfo(fileName); if (!fileInfo.isFile() && !fileInfo.isSymLink()) { setErrorString( tr("%1 doesn't exist or is not a regular file.") .arg(fileName)); return false; } #if defined(Q_OS_UNIX) #define STAT_METHOD QT_LSTAT #else #define STAT_METHOD QT_STAT #endif QT_STATBUF fi; if (STAT_METHOD(QFile::encodeName(fileName).constData(), &fi) == -1) { setErrorString( tr("Failed accessing the file %1 for adding to the archive. The error was: %2") .arg(fileName) .arg(QLatin1Literal{strerror(errno)})); return false; } if (fileInfo.isSymLink()) { QString symLinkTarget; // Do NOT use fileInfo.readLink() for unix symlinks! // It returns the -full- path to the target, while we want the target string "as is". #if defined(Q_OS_UNIX) && !defined(Q_OS_OS2EMX) const QByteArray encodedFileName = QFile::encodeName(fileName); QByteArray s; #if defined(PATH_MAX) s.resize(PATH_MAX + 1); #else int path_max = pathconf(encodedFileName.data(), _PC_PATH_MAX); if (path_max <= 0) { path_max = 4096; } s.resize(path_max); #endif int len = readlink(encodedFileName.data(), s.data(), s.size() - 1); if (len >= 0) { s[len] = '\0'; symLinkTarget = QFile::decodeName(s.constData()); } #endif if (symLinkTarget.isEmpty()) { // Mac or Windows symLinkTarget = fileInfo.symLinkTarget(); } return writeSymLink(destName, symLinkTarget, fileInfo.owner(), fileInfo.group(), fi.st_mode, fileInfo.lastRead(), fileInfo.lastModified(), fileInfo.created()); }/*end if*/ qint64 size = fileInfo.size(); // the file must be opened before prepareWriting is called, otherwise // if the opening fails, no content will follow the already written // header and the tar file is effectively f*cked up QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { setErrorString( tr("Couldn't open file %1: %2") .arg(fileName, file.errorString())); return false; } if (!prepareWriting(destName, fileInfo.owner(), fileInfo.group(), size, fi.st_mode, fileInfo.lastRead(), fileInfo.lastModified(), fileInfo.created())) { //qCWarning(KArchiveLog) << " prepareWriting" << destName << "failed"; return false; } // Read and write data in chunks to minimize memory usage QByteArray array; array.resize(int(qMin(qint64(1024 * 1024), size))); qint64 n; qint64 total = 0; while ((n = file.read(array.data(), array.size())) > 0) { if (!writeData(array.data(), n)) { //qCWarning(KArchiveLog) << "writeData failed"; return false; } total += n; } Q_ASSERT(total == size); if (!finishWriting(size)) { //qCWarning(KArchiveLog) << "finishWriting failed"; return false; } return true; } bool KArchive::addLocalDirectory(const QString &path, const QString &destName) { QDir dir(path); if (!dir.exists()) { setErrorString( tr("Directory %1 does not exist") .arg(path)); return false; } dir.setFilter(dir.filter() | QDir::Hidden); const QStringList files = dir.entryList(); for (QStringList::ConstIterator it = files.begin(); it != files.end(); ++it) { if (*it != QLatin1String(".") && *it != QLatin1String("..")) { QString fileName = path + QLatin1Char('/') + *it; // qCDebug(KArchiveLog) << "storing " << fileName; QString dest = destName.isEmpty() ? *it : (destName + QLatin1Char('/') + *it); QFileInfo fileInfo(fileName); if (fileInfo.isFile() || fileInfo.isSymLink()) { addLocalFile(fileName, dest); } else if (fileInfo.isDir()) { addLocalDirectory(fileName, dest); } // We omit sockets } } return true; } bool KArchive::writeFile(const QString &name, const QByteArray &data, mode_t perm, const QString &user, const QString &group, const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) { const qint64 size = data.size(); if (!prepareWriting(name, user, group, size, perm, atime, mtime, ctime)) { //qCWarning(KArchiveLog) << "prepareWriting failed"; return false; } // Write data // Note: if data is null, don't call write, it would terminate the KCompressionDevice if (data.constData() && size && !writeData(data.constData(), size)) { //qCWarning(KArchiveLog) << "writeData failed"; return false; } if (!finishWriting(size)) { //qCWarning(KArchiveLog) << "finishWriting failed"; return false; } return true; } bool KArchive::writeData(const char *data, qint64 size) { bool ok = device()->write(data, size) == size; if (!ok) { setErrorString( tr("Writing failed: %1") .arg(device()->errorString())); d->abortWriting(); } return ok; } // The writeDir -> doWriteDir pattern allows to avoid propagating the default // values into all virtual methods of subclasses, and it allows more extensibility: // if a new argument is needed, we can add a writeDir overload which stores the // additional argument in the d pointer, and doWriteDir reimplementations can fetch // it from there. bool KArchive::writeDir(const QString &name, const QString &user, const QString &group, mode_t perm, const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) { return doWriteDir(name, user, group, perm | 040000, atime, mtime, ctime); } bool KArchive::writeSymLink(const QString &name, const QString &target, const QString &user, const QString &group, mode_t perm, const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) { return doWriteSymLink(name, target, user, group, perm, atime, mtime, ctime); } bool KArchive::prepareWriting(const QString &name, const QString &user, const QString &group, qint64 size, mode_t perm, const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) { bool ok = doPrepareWriting(name, user, group, size, perm, atime, mtime, ctime); if (!ok) { d->abortWriting(); } return ok; } bool KArchive::finishWriting(qint64 size) { return doFinishWriting(size); } void KArchive::setErrorString(const QString &errorStr) { d->errorStr = errorStr; } static QString getCurrentUserName() { #if defined(Q_OS_UNIX) struct passwd *pw = getpwuid(getuid()); return pw ? QFile::decodeName(pw->pw_name) : QString::number(getuid()); #elif defined(Q_OS_WIN) wchar_t buffer[255]; DWORD size = 255; bool ok = GetUserNameW(buffer, &size); if (!ok) { return QString(); } return QString::fromWCharArray(buffer); #else return QString(); #endif } static QString getCurrentGroupName() { #if defined(Q_OS_UNIX) struct group *grp = getgrgid(getgid()); return grp ? QFile::decodeName(grp->gr_name) : QString::number(getgid()); #elif defined(Q_OS_WIN) return QString(); #else return QString(); #endif } KArchiveDirectory *KArchive::rootDir() { if (!d->rootDir) { //qCDebug(KArchiveLog) << "Making root dir "; QString username = ::getCurrentUserName(); QString groupname = ::getCurrentGroupName(); d->rootDir = new KArchiveDirectory(this, QStringLiteral("/"), int(0777 + S_IFDIR), QDateTime(), username, groupname, QString()); } return d->rootDir; } KArchiveDirectory *KArchive::findOrCreate(const QString &path) { return d->findOrCreate(path, 0 /*recursionCounter*/); } KArchiveDirectory *KArchivePrivate::findOrCreate(const QString &path, int recursionCounter) { // Check we're not in a path that is ultra deep, this is most probably fine since PATH_MAX on Linux // is defined as 4096, so even on /a/a/a/a/a/a 2500 recursions puts us over that limit // an ultra deep recursion will makes us crash due to not enough stack. Tests show that 1MB stack // (default on Linux seems to be 8MB) gives us up to around 4000 recursions if (recursionCounter > 2500) { qCWarning(KArchiveLog) << "path recursion limit exceeded, bailing out"; return nullptr; } //qCDebug(KArchiveLog) << path; if (path.isEmpty() || path == QLatin1String("/") || path == QLatin1String(".")) { // root dir => found //qCDebug(KArchiveLog) << "returning rootdir"; return q->rootDir(); } // Important note : for tar files containing absolute paths // (i.e. beginning with "/"), this means the leading "/" will // be removed (no KDirectory for it), which is exactly the way // the "tar" program works (though it displays a warning about it) // See also KArchiveDirectory::entry(). // Already created ? => found - const KArchiveEntry *ent = q->rootDir()->entry(path); - if (ent) { - if (ent->isDirectory()) + KArchiveDirectory *existingEntryParentDirectory; + const KArchiveEntry *existingEntry = KArchiveDirectoryPrivate::get(q->rootDir())->entry(path, &existingEntryParentDirectory); + if (existingEntry) { + if (existingEntry->isDirectory()) //qCDebug(KArchiveLog) << "found it"; { - const KArchiveDirectory *dir = static_cast(ent); + const KArchiveDirectory *dir = static_cast(existingEntry); return const_cast(dir); } else { - const KArchiveFile *file = static_cast(ent); + const KArchiveFile *file = static_cast(existingEntry); if (file->size() > 0) { qCWarning(KArchiveLog) << path << "is normal file, but there are file paths in the archive assuming it is a directory, bailing out"; return nullptr; } qCDebug(KArchiveLog) << path << " is an empty file, assuming it is actually a directory and replacing"; - KArchiveEntry *myEntry = const_cast(ent); - q->rootDir()->removeEntry(myEntry); + KArchiveEntry *myEntry = const_cast(existingEntry); + existingEntryParentDirectory->removeEntry(myEntry); delete myEntry; } } // Otherwise go up and try again int pos = path.lastIndexOf(QLatin1Char('/')); KArchiveDirectory *parent; QString dirname; if (pos == -1) { // no more slash => create in root dir parent = q->rootDir(); dirname = path; } else { QString left = path.left(pos); dirname = path.mid(pos + 1); parent = findOrCreate(left, recursionCounter + 1); // recursive call... until we find an existing dir. } if (!parent) { return nullptr; } //qCDebug(KArchiveLog) << "found parent " << parent->name() << " adding " << dirname << " to ensure " << path; // Found -> add the missing piece KArchiveDirectory *e = new KArchiveDirectory(q, dirname, rootDir->permissions(), rootDir->date(), rootDir->user(), rootDir->group(), QString()); if (parent->addEntryV2(e)) { return e; // now a directory to exists } else { return nullptr; } } void KArchive::setDevice(QIODevice *dev) { if (d->deviceOwned) { delete d->dev; } d->dev = dev; d->deviceOwned = false; } void KArchive::setRootDir(KArchiveDirectory *rootDir) { Q_ASSERT(!d->rootDir); // Call setRootDir only once during parsing please ;) d->rootDir = rootDir; } QIODevice::OpenMode KArchive::mode() const { return d->mode; } QIODevice *KArchive::device() const { return d->dev; } bool KArchive::isOpen() const { return d->mode != QIODevice::NotOpen; } QString KArchive::fileName() const { return d->fileName; } void KArchivePrivate::abortWriting() { if (saveFile) { saveFile->cancelWriting(); delete saveFile; saveFile = nullptr; dev = nullptr; } } // this is a hacky wrapper to check if time_t value is invalid QDateTime KArchivePrivate::time_tToDateTime(uint time_t) { if (time_t == uint(-1)) { return QDateTime(); } return QDateTime::fromTime_t(time_t); } //////////////////////////////////////////////////////////////////////// /////////////////////// KArchiveEntry ////////////////////////////////// //////////////////////////////////////////////////////////////////////// class KArchiveEntryPrivate { public: KArchiveEntryPrivate(KArchive *_archive, const QString &_name, int _access, const QDateTime &_date, const QString &_user, const QString &_group, const QString &_symlink) : name(_name) , date(_date) , access(_access) , user(_user) , group(_group) , symlink(_symlink) , archive(_archive) { } QString name; QDateTime date; mode_t access; QString user; QString group; QString symlink; KArchive *archive; }; KArchiveEntry::KArchiveEntry(KArchive *t, const QString &name, int access, const QDateTime &date, const QString &user, const QString &group, const QString &symlink) : d(new KArchiveEntryPrivate(t, name, access, date, user, group, symlink)) { } KArchiveEntry::~KArchiveEntry() { delete d; } QDateTime KArchiveEntry::date() const { return d->date; } QString KArchiveEntry::name() const { return d->name; } mode_t KArchiveEntry::permissions() const { return d->access; } QString KArchiveEntry::user() const { return d->user; } QString KArchiveEntry::group() const { return d->group; } QString KArchiveEntry::symLinkTarget() const { return d->symlink; } bool KArchiveEntry::isFile() const { return false; } bool KArchiveEntry::isDirectory() const { return false; } KArchive *KArchiveEntry::archive() const { return d->archive; } //////////////////////////////////////////////////////////////////////// /////////////////////// KArchiveFile /////////////////////////////////// //////////////////////////////////////////////////////////////////////// class KArchiveFilePrivate { public: KArchiveFilePrivate(qint64 _pos, qint64 _size) : pos(_pos) , size(_size) { } qint64 pos; qint64 size; }; KArchiveFile::KArchiveFile(KArchive *t, const QString &name, int access, const QDateTime &date, const QString &user, const QString &group, const QString &symlink, qint64 pos, qint64 size) : KArchiveEntry(t, name, access, date, user, group, symlink) , d(new KArchiveFilePrivate(pos, size)) { } KArchiveFile::~KArchiveFile() { delete d; } qint64 KArchiveFile::position() const { return d->pos; } qint64 KArchiveFile::size() const { return d->size; } void KArchiveFile::setSize(qint64 s) { d->size = s; } QByteArray KArchiveFile::data() const { bool ok = archive()->device()->seek(d->pos); if (!ok) { //qCWarning(KArchiveLog) << "Failed to sync to" << d->pos << "to read" << name(); } // Read content QByteArray arr; if (d->size) { arr = archive()->device()->read(d->size); Q_ASSERT(arr.size() == d->size); } return arr; } QIODevice *KArchiveFile::createDevice() const { return new KLimitedIODevice(archive()->device(), d->pos, d->size); } bool KArchiveFile::isFile() const { return true; } static QFileDevice::Permissions withExecutablePerms( QFileDevice::Permissions filePerms, mode_t perms) { if (perms & 01) filePerms |= QFileDevice::ExeOther; if (perms & 010) filePerms |= QFileDevice::ExeGroup; if (perms & 0100) filePerms |= QFileDevice::ExeOwner; return filePerms; } bool KArchiveFile::copyTo(const QString &dest) const { QFile f(dest + QLatin1Char('/') + name()); if (f.open(QIODevice::ReadWrite | QIODevice::Truncate)) { QIODevice *inputDev = createDevice(); // Read and write data in chunks to minimize memory usage const qint64 chunkSize = 1024 * 1024; qint64 remainingSize = d->size; QByteArray array; array.resize(int(qMin(chunkSize, remainingSize))); while (remainingSize > 0) { const qint64 currentChunkSize = qMin(chunkSize, remainingSize); const qint64 n = inputDev->read(array.data(), currentChunkSize); Q_UNUSED(n) // except in Q_ASSERT Q_ASSERT(n == currentChunkSize); f.write(array.data(), currentChunkSize); remainingSize -= currentChunkSize; } f.setPermissions(withExecutablePerms(f.permissions(), permissions())); f.close(); delete inputDev; return true; } return false; } //////////////////////////////////////////////////////////////////////// //////////////////////// KArchiveDirectory ///////////////////////////////// //////////////////////////////////////////////////////////////////////// -class KArchiveDirectoryPrivate -{ -public: - KArchiveDirectoryPrivate() - { - } - - ~KArchiveDirectoryPrivate() - { - qDeleteAll(entries); - } - - KArchiveDirectoryPrivate(const KArchiveDirectoryPrivate &) = delete; - KArchiveDirectoryPrivate &operator=(const KArchiveDirectoryPrivate &) = delete; - - QHash entries; -}; - KArchiveDirectory::KArchiveDirectory(KArchive *t, const QString &name, int access, const QDateTime &date, const QString &user, const QString &group, const QString &symlink) : KArchiveEntry(t, name, access, date, user, group, symlink) - , d(new KArchiveDirectoryPrivate) + , d(new KArchiveDirectoryPrivate(this)) { } KArchiveDirectory::~KArchiveDirectory() { delete d; } QStringList KArchiveDirectory::entries() const { return d->entries.keys(); } const KArchiveEntry *KArchiveDirectory::entry(const QString &_name) const { - QString name = QDir::cleanPath(_name); - int pos = name.indexOf(QLatin1Char('/')); - if (pos == 0) { // ouch absolute path (see also KArchive::findOrCreate) - if (name.length() > 1) { - name = name.mid(1); // remove leading slash - pos = name.indexOf(QLatin1Char('/')); // look again - } else { // "/" - return this; - } - } - // trailing slash ? -> remove - if (pos != -1 && pos == name.length() - 1) { - name = name.left(pos); - pos = name.indexOf(QLatin1Char('/')); // look again - } - if (pos != -1) { - const QString left = name.left(pos); - const QString right = name.mid(pos + 1); - - //qCDebug(KArchiveLog) << "left=" << left << "right=" << right; - - const KArchiveEntry *e = d->entries.value(left); - if (!e || !e->isDirectory()) { - return nullptr; - } - return static_cast(e)->entry(right); - } - - return d->entries.value(name); + KArchiveDirectory *dummy; + return d->entry(_name, &dummy); } const KArchiveFile *KArchiveDirectory::file(const QString &name) const { const KArchiveEntry *e = entry(name); if (e && e->isFile()) { return static_cast(e); } return nullptr; } void KArchiveDirectory::addEntry(KArchiveEntry *entry) { addEntryV2(entry); } bool KArchiveDirectory::addEntryV2(KArchiveEntry *entry) { if (d->entries.value(entry->name())) { qCWarning(KArchiveLog) << "directory " << name() << "has entry" << entry->name() << "already"; delete entry; return false; } d->entries.insert(entry->name(), entry); return true; } void KArchiveDirectory::removeEntry(KArchiveEntry *entry) { if (!entry) { return; } QHash::Iterator it = d->entries.find(entry->name()); // nothing removed? if (it == d->entries.end()) { qCWarning(KArchiveLog) << "directory " << name() << "has no entry with name " << entry->name(); return; } if (it.value() != entry) { qCWarning(KArchiveLog) << "directory " << name() << "has another entry for name " << entry->name(); return; } d->entries.erase(it); } bool KArchiveDirectory::isDirectory() const { return true; } static bool sortByPosition(const KArchiveFile *file1, const KArchiveFile *file2) { return file1->position() < file2->position(); } bool KArchiveDirectory::copyTo(const QString &dest, bool recursiveCopy) const { QDir root; const QString destDir(QDir(dest).absolutePath()); // get directory path without any "." or ".." QList fileList; QMap fileToDir; // placeholders for iterated items QStack dirStack; QStack dirNameStack; dirStack.push(this); // init stack at current directory dirNameStack.push(destDir); // ... with given path do { const KArchiveDirectory *curDir = dirStack.pop(); // extract only to specified folder if it is located within archive's extraction folder // otherwise put file under root position in extraction folder QString curDirName = dirNameStack.pop(); if (!QDir(curDirName).absolutePath().startsWith(destDir)) { qCWarning(KArchiveLog) << "Attempted export into folder" << curDirName << "which is outside of the extraction root folder" << destDir << "." << "Changing export of contained files to extraction root folder."; curDirName = destDir; } if (!root.mkpath(curDirName)) { return false; } const QStringList dirEntries = curDir->entries(); for (QStringList::const_iterator it = dirEntries.begin(); it != dirEntries.end(); ++it) { const KArchiveEntry *curEntry = curDir->entry(*it); if (!curEntry->symLinkTarget().isEmpty()) { QString linkName = curDirName + QLatin1Char('/') + curEntry->name(); // To create a valid link on Windows, linkName must have a .lnk file extension. #ifdef Q_OS_WIN if (!linkName.endsWith(QLatin1String(".lnk"))) { linkName += QLatin1String(".lnk"); } #endif QFile symLinkTarget(curEntry->symLinkTarget()); if (!symLinkTarget.link(linkName)) { //qCDebug(KArchiveLog) << "symlink(" << curEntry->symLinkTarget() << ',' << linkName << ") failed:" << strerror(errno); } } else { if (curEntry->isFile()) { const KArchiveFile *curFile = dynamic_cast(curEntry); if (curFile) { fileList.append(curFile); fileToDir.insert(curFile->position(), curDirName); } } if (curEntry->isDirectory() && recursiveCopy) { const KArchiveDirectory *ad = dynamic_cast(curEntry); if (ad) { dirStack.push(ad); dirNameStack.push(curDirName + QLatin1Char('/') + curEntry->name()); } } } } } while (!dirStack.isEmpty()); std::sort(fileList.begin(), fileList.end(), sortByPosition); // sort on d->pos, so we have a linear access for (QList::const_iterator it = fileList.constBegin(), end = fileList.constEnd(); it != end; ++it) { const KArchiveFile *f = *it; qint64 pos = f->position(); if (!f->copyTo(fileToDir[pos])) { return false; } } return true; } void KArchive::virtual_hook(int, void *) { /*BASE::virtual_hook( id, data )*/; } void KArchiveEntry::virtual_hook(int, void *) { /*BASE::virtual_hook( id, data );*/ } void KArchiveFile::virtual_hook(int id, void *data) { KArchiveEntry::virtual_hook(id, data); } void KArchiveDirectory::virtual_hook(int id, void *data) { KArchiveEntry::virtual_hook(id, data); } diff --git a/src/karchivedirectory.h b/src/karchivedirectory.h index 25c7dbd..96d471f 100644 --- a/src/karchivedirectory.h +++ b/src/karchivedirectory.h @@ -1,133 +1,134 @@ /* This file is part of the KDE libraries Copyright (C) 2000-2005 David Faure Copyright (C) 2003 Leo Savernik Moved from ktar.h by Roberto Teixeira This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KARCHIVEDIRECTORY_H #define KARCHIVEDIRECTORY_H #include #include #include #include #include #include class KArchiveDirectoryPrivate; class KArchiveFile; /** * @class KArchiveDirectory karchivedirectory.h KArchiveDirectory * * Represents a directory entry in a KArchive. * @short A directory in an archive. * * @see KArchive * @see KArchiveFile */ class KARCHIVE_EXPORT KArchiveDirectory : public KArchiveEntry { public: /** * Creates a new directory entry. * @param archive the entries archive * @param name the name of the entry * @param access the permissions in unix format * @param date the date (in seconds since 1970) * @param user the user that owns the entry * @param group the group that owns the entry * @param symlink the symlink, or QString() */ KArchiveDirectory(KArchive *archive, const QString &name, int access, const QDateTime &date, const QString &user, const QString &group, const QString &symlink); virtual ~KArchiveDirectory(); /** * Returns a list of sub-entries. * Note that the list is not sorted, it's even in random order (due to using a hashtable). * Use sort() on the result to sort the list by filename. * * @return the names of all entries in this directory (filenames, no path). */ QStringList entries() const; /** * Returns the entry in the archive with the given name. * The entry could be a file or a directory, use isFile() to find out which one it is. * @param name may be "test1", "mydir/test3", "mydir/mysubdir/test3", etc. * @return a pointer to the entry in the directory, or a null pointer if there is no such entry. */ const KArchiveEntry *entry(const QString &name) const; /** * Returns the file entry in the archive with the given name. * If the entry exists and is a file, a KArchiveFile is returned. * Otherwise, a null pointer is returned. * This is a convenience method for entry(), when we know the entry is expected to be a file. * * @param name may be "test1", "mydir/test3", "mydir/mysubdir/test3", etc. * @return a pointer to the file entry in the directory, or a null pointer if there is no such file entry. * @since 5.3 */ const KArchiveFile *file(const QString &name) const; /** * @internal * Adds a new entry to the directory. * Note: this can delete the entry if another one with the same name is already present */ void addEntry(KArchiveEntry *); // KF6 TODO: return bool /** * @internal * Adds a new entry to the directory. * @return whether the entry was added or not. Non added entries are deleted */ bool addEntryV2(KArchiveEntry *); // KF6 TODO: merge with the one above /** * @internal * Removes an entry from the directory. */ void removeEntry(KArchiveEntry *); // KF6 TODO: return bool since it can fail /** * Checks whether this entry is a directory. * @return true, since this entry is a directory */ bool isDirectory() const override; /** * Extracts all entries in this archive directory to the directory * @p dest. * @param dest the directory to extract to * @param recursive if set to true, subdirectories are extracted as well * @return true on success, false if the directory (dest + '/' + name()) couldn't be created */ bool copyTo(const QString &dest, bool recursive = true) const; protected: void virtual_hook(int id, void *data) override; private: + friend class KArchiveDirectoryPrivate; KArchiveDirectoryPrivate *const d; }; #endif