/* This file is part of the KDE project SPDX-FileCopyrightText: 2004 David Faure SPDX-License-Identifier: LGPL-2.0-or-later */ #include "testtrash.h" #include #include "../../../utils_p.h" #include "filecopyjob.h" #include "kio_trash.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // There are two ways to test encoding things: // * with utf8 filenames // * with latin1 filenames -- not sure this still works. // #define UTF8TEST 1 int initLocale() { #ifdef UTF8TEST // Assume utf8 system setenv("LC_ALL", "C.utf-8", 1); setenv("LANGUAGE", "en", 1); setenv("KDE_UTF8_FILENAMES", "true", 1); #else // Ensure a known QFile::encodeName behavior for trashUtf8FileFromHome // However this assume your $HOME doesn't use characters from other locales... setenv("LC_ALL", "en_US.ISO-8859-1", 1); unsetenv("KDE_UTF8_FILENAMES"); #endif setenv("KIOWORKER_ENABLE_TESTMODE", "1", 1); // ensure the KIO workers call QStandardPaths::setTestModeEnabled(true) too setenv("KDE_SKIP_KDERC", "1", 1); unsetenv("KDE_COLOR_DEBUG"); return 0; } Q_CONSTRUCTOR_FUNCTION(initLocale) QString TestTrash::homeTmpDir() const { return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/testtrash/"); } QString TestTrash::readOnlyDirPath() const { return homeTmpDir() + QLatin1String("readonly"); } QString TestTrash::otherTmpDir() const { // This one needs to be on another partition for the test to be meaningful return Utils::slashAppended(m_tempDir.path()); } QString TestTrash::utf8FileName() const { return QLatin1String("test") + QChar(0x2153); // "1/3" character, not part of latin1 } QString TestTrash::umlautFileName() const { return QLatin1String("umlaut") + QChar(0xEB); } static void removeFile(const QString &trashDir, const QString &fileName) { QDir dir; dir.remove(trashDir + fileName); QVERIFY(!QDir(trashDir + fileName).exists()); } static void removeDir(const QString &trashDir, const QString &dirName) { QDir dir; dir.rmdir(trashDir + dirName); QVERIFY(!QDir(trashDir + dirName).exists()); } static void removeDirRecursive(const QString &dir) { if (QFile::exists(dir)) { // Make it work even with readonly dirs, like trashReadOnlyDirFromHome() creates QUrl u = QUrl::fromLocalFile(dir); // qDebug() << "chmod +0200 on" << u; KFileItem fileItem(u, QStringLiteral("inode/directory"), KFileItem::Unknown); KFileItemList fileItemList; fileItemList.append(fileItem); KIO::ChmodJob *chmodJob = KIO::chmod(fileItemList, 0200, 0200, QString(), QString(), true /*recursive*/, KIO::HideProgressInfo); chmodJob->exec(); KIO::Job *delJob = KIO::del(u, KIO::HideProgressInfo); if (!delJob->exec()) { qFatal("Couldn't delete %s", qPrintable(dir)); } } } void TestTrash::initTestCase() { QStandardPaths::setTestModeEnabled(true); QVERIFY(m_tempDir.isValid()); #ifndef Q_OS_OSX m_trashDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/Trash"); qDebug() << "setup: using trash directory " << m_trashDir; #endif // Look for another writable partition than $HOME (not mandatory) TrashImpl impl; impl.init(); TrashImpl::TrashDirMap trashDirs = impl.trashDirectories(); #ifdef Q_OS_OSX QVERIFY(trashDirs.contains(0)); m_trashDir = trashDirs.value(0); qDebug() << "setup: using trash directory " << m_trashDir; #endif TrashImpl::TrashDirMap topDirs = impl.topDirectories(); bool foundTrashDir = false; m_otherPartitionId = 0; m_tmpIsWritablePartition = false; m_tmpTrashId = -1; QList writableTopDirs; for (TrashImpl::TrashDirMap::ConstIterator it = trashDirs.constBegin(); it != trashDirs.constEnd(); ++it) { if (it.key() == 0) { QCOMPARE(it.value(), m_trashDir); QVERIFY(topDirs.find(0) == topDirs.end()); foundTrashDir = true; } else { QVERIFY(topDirs.find(it.key()) != topDirs.end()); const QString topdir = topDirs[it.key()]; if (QFileInfo(topdir).isWritable()) { writableTopDirs.append(it.key()); if (topdir == QLatin1String("/tmp/")) { m_tmpIsWritablePartition = true; m_tmpTrashId = it.key(); qDebug() << "/tmp is on its own partition (trashid=" << m_tmpTrashId << "), some tests will be skipped"; removeFile(it.value(), QStringLiteral("/info/fileFromOther.trashinfo")); removeFile(it.value(), QStringLiteral("/files/fileFromOther")); removeFile(it.value(), QStringLiteral("/info/symlinkFromOther.trashinfo")); removeFile(it.value(), QStringLiteral("/files/symlinkFromOther")); removeFile(it.value(), QStringLiteral("/info/trashDirFromOther.trashinfo")); removeFile(it.value(), QStringLiteral("/files/trashDirFromOther/testfile")); removeDir(it.value(), QStringLiteral("/files/trashDirFromOther")); } } } } for (auto it = writableTopDirs.constBegin(); it != writableTopDirs.constEnd(); ++it) { const QString topdir = topDirs[*it]; const QString trashdir = trashDirs[*it]; QVERIFY(!topdir.isEmpty()); QVERIFY(!trashDirs.isEmpty()); if (topdir != QLatin1String("/tmp/") || // we'd prefer not to use /tmp here, to separate the tests (writableTopDirs.count() > 1)) { // but well, if we have no choice, take it m_otherPartitionTopDir = topdir; m_otherPartitionTrashDir = trashdir; m_otherPartitionId = *it; qDebug() << "OK, found another writable partition: topDir=" << m_otherPartitionTopDir << " trashDir=" << m_otherPartitionTrashDir << " id=" << m_otherPartitionId; break; } } // Check that m_trashDir got listed QVERIFY(foundTrashDir); if (m_otherPartitionTrashDir.isEmpty()) { qWarning() << "No writable partition other than $HOME found, some tests will be skipped"; } // Start with a clean base dir qDebug() << "initial cleanup"; removeDirRecursive(homeTmpDir()); QDir dir; // TT: why not a static method? bool ok = dir.mkdir(homeTmpDir()); if (!ok) { qFatal("Couldn't create directory: %s", qPrintable(homeTmpDir())); } QVERIFY(QFileInfo(otherTmpDir()).isDir()); // Start with a clean trash too qDebug() << "removing trash dir"; removeDirRecursive(m_trashDir); } void TestTrash::cleanupTestCase() { // Clean up removeDirRecursive(homeTmpDir()); removeDirRecursive(otherTmpDir()); removeDirRecursive(m_trashDir); } void TestTrash::urlTestFile() { const QUrl url = TrashImpl::makeURL(1, QStringLiteral("fileId"), QString()); QCOMPARE(url.url(), QStringLiteral("trash:/1-fileId")); int trashId; QString fileId; QString relativePath; bool ok = TrashImpl::parseURL(url, trashId, fileId, relativePath); QVERIFY(ok); QCOMPARE(QString::number(trashId), QStringLiteral("1")); QCOMPARE(fileId, QStringLiteral("fileId")); QCOMPARE(relativePath, QString()); } void TestTrash::urlTestDirectory() { const QUrl url = TrashImpl::makeURL(1, QStringLiteral("fileId"), QStringLiteral("subfile")); QCOMPARE(url.url(), QStringLiteral("trash:/1-fileId/subfile")); int trashId; QString fileId; QString relativePath; bool ok = TrashImpl::parseURL(url, trashId, fileId, relativePath); QVERIFY(ok); QCOMPARE(trashId, 1); QCOMPARE(fileId, QStringLiteral("fileId")); QCOMPARE(relativePath, QStringLiteral("subfile")); } void TestTrash::urlTestSubDirectory() { const QUrl url = TrashImpl::makeURL(1, QStringLiteral("fileId"), QStringLiteral("subfile/foobar")); QCOMPARE(url.url(), QStringLiteral("trash:/1-fileId/subfile/foobar")); int trashId; QString fileId; QString relativePath; bool ok = TrashImpl::parseURL(url, trashId, fileId, relativePath); QVERIFY(ok); QCOMPARE(trashId, 1); QCOMPARE(fileId, QStringLiteral("fileId")); QCOMPARE(relativePath, QStringLiteral("subfile/foobar")); } static void checkInfoFile(const QString &infoPath, const QString &origFilePath) { qDebug() << infoPath; QFileInfo info(infoPath); QVERIFY2(info.exists(), qPrintable(infoPath)); QVERIFY(info.isFile()); KConfig infoFile(info.absoluteFilePath()); KConfigGroup group = infoFile.group(QStringLiteral("Trash Info")); if (!group.exists()) { qFatal("no Trash Info group in %s", qPrintable(info.absoluteFilePath())); } const QString origPath = group.readEntry("Path"); QVERIFY(!origPath.isEmpty()); QCOMPARE(origPath.toUtf8(), QUrl::toPercentEncoding(origFilePath, "/")); if (origFilePath.contains(QChar(0x2153)) || origFilePath.contains(QLatin1Char('%')) || origFilePath.contains(QLatin1String("umlaut"))) { QVERIFY(origPath.contains(QLatin1Char('%'))); } else { QVERIFY(!origPath.contains(QLatin1Char('%'))); } const QString date = group.readEntry("DeletionDate"); QVERIFY(!date.isEmpty()); QVERIFY(date.contains(QLatin1String("T"))); } static void createTestFile(const QString &path) { QFile f(path); if (!f.open(QIODevice::WriteOnly)) { qFatal("Can't create %s", qPrintable(path)); } f.write("Hello world\n", 12); f.close(); QVERIFY(QFile::exists(path)); } void TestTrash::trashFile(const QString &origFilePath, const QString &fileId) { // setup if (!QFile::exists(origFilePath)) { createTestFile(origFilePath); } QUrl u = QUrl::fromLocalFile(origFilePath); // test KIO::Job *job = KIO::move(u, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo); bool ok = job->exec(); if (!ok) { qCritical() << "moving " << u << " to trash failed with error " << job->error() << " " << job->errorString(); } QVERIFY(ok); if (origFilePath.startsWith(QLatin1String("/tmp")) && m_tmpIsWritablePartition) { qDebug() << " TESTS SKIPPED"; } else { checkInfoFile(m_trashDir + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo"), origFilePath); QFileInfo fileInTrash(m_trashDir + QLatin1String("/files/") + fileId); QVERIFY(fileInTrash.isFile()); QCOMPARE(fileInTrash.size(), 12); } // coolo suggests testing that the original file is actually gone, too :) QVERIFY(!QFile::exists(origFilePath)); QMap metaData = job->metaData(); QVERIFY(!metaData.isEmpty()); bool found = false; QMap::ConstIterator it = metaData.constBegin(); for (; it != metaData.constEnd(); ++it) { if (it.key().startsWith(QLatin1String("trashURL"))) { QUrl trashURL(it.value()); qDebug() << trashURL; QVERIFY(!trashURL.isEmpty()); QCOMPARE(trashURL.scheme(), QLatin1String("trash")); int trashId = 0; if (origFilePath.startsWith(QLatin1String("/tmp")) && m_tmpIsWritablePartition) { trashId = m_tmpTrashId; } QCOMPARE(trashURL.path(), QString(QStringLiteral("/") + QString::number(trashId) + QLatin1Char('-') + fileId)); found = true; } } QVERIFY(found); } void TestTrash::trashFileFromHome() { const QString fileName = QStringLiteral("fileFromHome"); trashFile(homeTmpDir() + fileName, fileName); // Do it again, check that we got a different id trashFile(homeTmpDir() + fileName, fileName + QLatin1String(" (1)")); } void TestTrash::trashPercentFileFromHome() { const QString fileName = QStringLiteral("file%2f"); trashFile(homeTmpDir() + fileName, fileName); } void TestTrash::trashUtf8FileFromHome() { #ifdef UTF8TEST const QString fileName = utf8FileName(); trashFile(homeTmpDir() + fileName, fileName); #endif } void TestTrash::trashUmlautFileFromHome() { const QString fileName = umlautFileName(); trashFile(homeTmpDir() + fileName, fileName); } void TestTrash::testTrashNotEmpty() { KConfig cfg(QStringLiteral("trashrc"), KConfig::SimpleConfig); const KConfigGroup group = cfg.group(QStringLiteral("Status")); QVERIFY(group.exists()); QCOMPARE(group.readEntry("Empty", true), false); } void TestTrash::trashFileFromOther() { const QString fileName = QStringLiteral("fileFromOther"); trashFile(otherTmpDir() + fileName, fileName); } void TestTrash::trashFileIntoOtherPartition() { if (m_otherPartitionTrashDir.isEmpty()) { qDebug() << " - SKIPPED"; return; } const QString fileName = QStringLiteral("testtrash-file"); const QString origFilePath = m_otherPartitionTopDir + fileName; const QString &fileId = fileName; // cleanup QFile::remove(m_otherPartitionTrashDir + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo")); QFile::remove(m_otherPartitionTrashDir + QLatin1String("/files/") + fileId); // setup if (!QFile::exists(origFilePath)) { createTestFile(origFilePath); } QUrl u = QUrl::fromLocalFile(origFilePath); // test KIO::Job *job = KIO::move(u, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY(ok); QMap metaData = job->metaData(); // Note that the Path stored in the info file is relative, on other partitions (#95652) checkInfoFile(m_otherPartitionTrashDir + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo"), fileName); QFileInfo files(m_otherPartitionTrashDir + QLatin1String("/files/") + fileId); QVERIFY(files.isFile()); QCOMPARE(files.size(), 12); // coolo suggests testing that the original file is actually gone, too :) QVERIFY(!QFile::exists(origFilePath)); QVERIFY(!metaData.isEmpty()); bool found = false; QMap::ConstIterator it = metaData.constBegin(); for (; it != metaData.constEnd(); ++it) { if (it.key().startsWith(QLatin1String("trashURL"))) { QUrl trashURL(it.value()); qDebug() << trashURL; QVERIFY(!trashURL.isEmpty()); QCOMPARE(trashURL.scheme(), QLatin1String("trash")); QCOMPARE(trashURL.path(), QStringLiteral("/%1-%2").arg(m_otherPartitionId).arg(fileId)); found = true; } } QVERIFY(found); } void TestTrash::trashFileOwnedByRoot() { QUrl u(QStringLiteral("file:///etc/passwd")); const QString fileId = QStringLiteral("passwd"); if (geteuid() == 0 || QFileInfo(u.toLocalFile()).isWritable()) { QSKIP("Test must not be run by root."); } KIO::CopyJob *job = KIO::move(u, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo); job->setUiDelegate(nullptr); // no skip dialog, thanks bool ok = job->exec(); QVERIFY(!ok); QCOMPARE(job->error(), KIO::ERR_ACCESS_DENIED); const QString infoPath(m_trashDir + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo")); QVERIFY(!QFile::exists(infoPath)); QFileInfo files(m_trashDir + QLatin1String("/files/") + fileId); QVERIFY(!files.exists()); QVERIFY(QFile::exists(u.path())); } void TestTrash::trashSymlink(const QString &origFilePath, const QString &fileId, bool broken) { // setup const char *target = broken ? "/nonexistent" : "/tmp"; bool ok = ::symlink(target, QFile::encodeName(origFilePath).constData()) == 0; QVERIFY(ok); QUrl u = QUrl::fromLocalFile(origFilePath); // test KIO::Job *job = KIO::move(u, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo); ok = job->exec(); QVERIFY(ok); if (origFilePath.startsWith(QLatin1String("/tmp")) && m_tmpIsWritablePartition) { qDebug() << " TESTS SKIPPED"; return; } checkInfoFile(m_trashDir + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo"), origFilePath); QFileInfo files(m_trashDir + QLatin1String("/files/") + fileId); QVERIFY(files.isSymLink()); QCOMPARE(files.symLinkTarget(), QFile::decodeName(target)); QVERIFY(!QFile::exists(origFilePath)); } void TestTrash::trashSymlinkFromHome() { const QString fileName = QStringLiteral("symlinkFromHome"); trashSymlink(homeTmpDir() + fileName, fileName, false); } void TestTrash::trashSymlinkFromOther() { const QString fileName = QStringLiteral("symlinkFromOther"); trashSymlink(otherTmpDir() + fileName, fileName, false); } void TestTrash::trashBrokenSymlinkFromHome() { const QString fileName = QStringLiteral("brokenSymlinkFromHome"); trashSymlink(homeTmpDir() + fileName, fileName, true); } void TestTrash::trashDirectory(const QString &origPath, const QString &fileId) { qDebug() << fileId; // setup if (!QFileInfo::exists(origPath)) { QDir dir; bool ok = dir.mkdir(origPath); QVERIFY(ok); } createTestFile(origPath + QLatin1String("/testfile")); QVERIFY(QDir().mkdir(origPath + QStringLiteral("/subdir"))); createTestFile(origPath + QLatin1String("/subdir/subfile")); QUrl u = QUrl::fromLocalFile(origPath); // test KIO::Job *job = KIO::move(u, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo); QVERIFY(job->exec()); if (origPath.startsWith(QLatin1String("/tmp")) && m_tmpIsWritablePartition) { qDebug() << " TESTS SKIPPED"; return; } checkInfoFile(m_trashDir + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo"), origPath); QFileInfo filesDir(m_trashDir + QLatin1String("/files/") + fileId); QVERIFY(filesDir.isDir()); QFileInfo files(m_trashDir + QLatin1String("/files/") + fileId + QLatin1String("/testfile")); QVERIFY(files.exists()); QVERIFY(files.isFile()); QCOMPARE(files.size(), 12); QVERIFY(!QFile::exists(origPath)); QVERIFY(QFile::exists(m_trashDir + QStringLiteral("/files/") + fileId + QStringLiteral("/subdir/subfile"))); QFile dirCache(m_trashDir + QLatin1String("/directorysizes")); QVERIFY2(dirCache.open(QIODevice::ReadOnly), qPrintable(dirCache.fileName())); QByteArray lines; bool found = false; while (!dirCache.atEnd()) { const QByteArray line = dirCache.readLine(); if (line.endsWith(QByteArray(' ' + QFile::encodeName(fileId).toPercentEncoding() + '\n'))) { QVERIFY(!found); // should be there only once! found = true; } lines += line; } QVERIFY2(found, lines.constData()); // qDebug() << lines; checkDirCacheValidity(); } void TestTrash::checkDirCacheValidity() { QFile dirCache(m_trashDir + QLatin1String("/directorysizes")); QVERIFY2(dirCache.open(QIODevice::ReadOnly), qPrintable(dirCache.fileName())); QSet seenDirs; while (!dirCache.atEnd()) { QByteArray line = dirCache.readLine(); QVERIFY(line.endsWith('\n')); line.chop(1); qDebug() << "LINE" << line; const auto exploded = line.split(' '); QCOMPARE(exploded.size(), 3); bool succeeded = false; const int size = exploded.at(0).toInt(&succeeded); QVERIFY(succeeded); QVERIFY(size > 0); const int mtime = exploded.at(0).toInt(&succeeded); QVERIFY(succeeded); QVERIFY(mtime > 0); QVERIFY(QDateTime::fromMSecsSinceEpoch(mtime).isValid()); const QByteArray dir = QByteArray::fromPercentEncoding(exploded.at(2)); QVERIFY2(!seenDirs.contains(dir), dir.constData()); seenDirs.insert(dir); const QString localDir = m_trashDir + QLatin1String("/files/") + QFile::decodeName(dir); QVERIFY2(QFile::exists(localDir), qPrintable(localDir)); QVERIFY(QFileInfo(localDir).isDir()); } } void TestTrash::trashDirectoryFromHome() { QString dirName = QStringLiteral("trashDirFromHome"); trashDirectory(homeTmpDir() + dirName, dirName); checkDirCacheValidity(); // Do it again, check that we got a different id trashDirectory(homeTmpDir() + dirName, dirName + QLatin1String(" (1)")); } void TestTrash::trashDotDirectory() { QString dirName = QStringLiteral(".dotTrashDirFromHome"); trashDirectory(homeTmpDir() + dirName, dirName); // Do it again, check that we got a different id // TODO trashDirectory(homeTmpDir() + dirName, dirName + QString::fromLatin1(" (1)")); } void TestTrash::trashReadOnlyDirFromHome() { const QString dirName = readOnlyDirPath(); QDir dir; bool ok = dir.mkdir(dirName); QVERIFY(ok); // #130780 const QString subDirPath = dirName + QLatin1String("/readonly_subdir"); ok = dir.mkdir(subDirPath); QVERIFY(ok); createTestFile(subDirPath + QLatin1String("/testfile_in_subdir")); ::chmod(QFile::encodeName(subDirPath).constData(), 0500); trashDirectory(dirName, QStringLiteral("readonly")); } void TestTrash::trashDirectoryFromOther() { QString dirName = QStringLiteral("trashDirFromOther"); trashDirectory(otherTmpDir() + dirName, dirName); } void TestTrash::trashDirectoryWithTrailingSlash() { QString dirName = QStringLiteral("dirwithslash/"); trashDirectory(homeTmpDir() + dirName, QStringLiteral("dirwithslash")); } void TestTrash::trashBrokenSymlinkIntoSubdir() { QString origPath = homeTmpDir() + QStringLiteral("subDirBrokenSymlink"); if (!QFileInfo::exists(origPath)) { QDir dir; bool ok = dir.mkdir(origPath); QVERIFY(ok); } bool ok = ::symlink("/nonexistent", QFile::encodeName(origPath + QStringLiteral("/link")).constData()) == 0; QVERIFY(ok); trashDirectory(origPath, QStringLiteral("subDirBrokenSymlink")); } void TestTrash::testRemoveStaleInfofile() { const QString fileName = QStringLiteral("disappearingFileInTrash"); const QString filePath = homeTmpDir() + fileName; createTestFile(filePath); trashFile(filePath, fileName); const QString pathInTrash = m_trashDir + QLatin1String("/files/") + QLatin1String("disappearingFileInTrash"); // remove the file without using KIO QVERIFY(QFile::remove(pathInTrash)); // .trashinfo file still exists const QString infoPath = m_trashDir + QLatin1String("/info/disappearingFileInTrash.trashinfo"); QVERIFY(QFile(infoPath).exists()); KIO::ListJob *job = KIO::listDir(QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo); connect(job, &KIO::ListJob::entries, this, &TestTrash::slotEntries); QVERIFY(job->exec()); // during the list job, kio_trash should have deleted the .trashinfo file since it // references a trashed file that doesn't exist any more QVERIFY(!QFile(infoPath).exists()); } void TestTrash::delRootFile() { // test deleting a trashed file KIO::Job *delJob = KIO::del(QUrl(QStringLiteral("trash:/0-fileFromHome")), KIO::HideProgressInfo); bool ok = delJob->exec(); QVERIFY(ok); QFileInfo file(m_trashDir + QLatin1String("/files/fileFromHome")); QVERIFY(!file.exists()); QFileInfo info(m_trashDir + QLatin1String("/info/fileFromHome.trashinfo")); QVERIFY(!info.exists()); // trash it again, we might need it later const QString fileName = QStringLiteral("fileFromHome"); trashFile(homeTmpDir() + fileName, fileName); } void TestTrash::delFileInDirectory() { // test deleting a file inside a trashed directory -> not allowed KIO::Job *delJob = KIO::del(QUrl(QStringLiteral("trash:/0-trashDirFromHome/testfile")), KIO::HideProgressInfo); bool ok = delJob->exec(); QVERIFY(!ok); QCOMPARE(delJob->error(), KIO::ERR_ACCESS_DENIED); QFileInfo dir(m_trashDir + QLatin1String("/files/trashDirFromHome")); QVERIFY(dir.exists()); QFileInfo file(m_trashDir + QLatin1String("/files/trashDirFromHome/testfile")); QVERIFY(file.exists()); QFileInfo info(m_trashDir + QLatin1String("/info/trashDirFromHome.trashinfo")); QVERIFY(info.exists()); } void TestTrash::delDirectory() { // test deleting a trashed directory KIO::Job *delJob = KIO::del(QUrl(QStringLiteral("trash:/0-trashDirFromHome")), KIO::HideProgressInfo); bool ok = delJob->exec(); QVERIFY(ok); QFileInfo dir(m_trashDir + QLatin1String("/files/trashDirFromHome")); QVERIFY(!dir.exists()); QFileInfo file(m_trashDir + QLatin1String("/files/trashDirFromHome/testfile")); QVERIFY(!file.exists()); QFileInfo info(m_trashDir + QLatin1String("/info/trashDirFromHome.trashinfo")); QVERIFY(!info.exists()); checkDirCacheValidity(); // trash it again, we'll need it later QString dirName = QStringLiteral("trashDirFromHome"); trashDirectory(homeTmpDir() + dirName, dirName); } static bool MyNetAccess_stat(const QUrl &url, KIO::UDSEntry &entry) { KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo); bool ok = statJob->exec(); if (ok) { entry = statJob->statResult(); } return ok; } static bool MyNetAccess_exists(const QUrl &url) { KIO::UDSEntry dummy; return MyNetAccess_stat(url, dummy); } void TestTrash::mostLocalUrlTest() { const QStringList trashFiles = QDir(m_trashDir + QLatin1String("/files/")).entryList(); for (const QString &file : trashFiles) { if (file == QLatin1Char('.') || file == QLatin1String("..")) { continue; } QUrl url; url.setScheme(QStringLiteral("trash")); url.setPath(QLatin1String("0-") + file); KIO::StatJob *statJob = KIO::mostLocalUrl(url, KIO::HideProgressInfo); QVERIFY(statJob->exec()); QCOMPARE(QUrl::fromLocalFile(m_trashDir + QStringLiteral("/files/") + file), statJob->mostLocalUrl()); } } void TestTrash::statRoot() { QUrl url(QStringLiteral("trash:/")); KIO::UDSEntry entry; bool ok = MyNetAccess_stat(url, entry); QVERIFY(ok); KFileItem item(entry, url); QVERIFY(item.isDir()); QVERIFY(!item.isLink()); QVERIFY(item.isReadable()); QVERIFY(item.isWritable()); QVERIFY(!item.isHidden()); QCOMPARE(item.name(), QStringLiteral(".")); } void TestTrash::statFileInRoot() { QUrl url(QStringLiteral("trash:/0-fileFromHome")); KIO::UDSEntry entry; bool ok = MyNetAccess_stat(url, entry); QVERIFY(ok); KFileItem item(entry, url); QVERIFY(item.isFile()); QVERIFY(!item.isDir()); QVERIFY(!item.isLink()); QVERIFY(item.isReadable()); QVERIFY(!item.isWritable()); QVERIFY(!item.isHidden()); QCOMPARE(item.text(), QStringLiteral("fileFromHome")); } void TestTrash::statDirectoryInRoot() { QUrl url(QStringLiteral("trash:/0-trashDirFromHome")); KIO::UDSEntry entry; bool ok = MyNetAccess_stat(url, entry); QVERIFY(ok); KFileItem item(entry, url); QVERIFY(item.isDir()); QVERIFY(!item.isLink()); QVERIFY(item.isReadable()); QVERIFY(!item.isWritable()); QVERIFY(!item.isHidden()); QCOMPARE(item.text(), QStringLiteral("trashDirFromHome")); } void TestTrash::statSymlinkInRoot() { QUrl url(QStringLiteral("trash:/0-symlinkFromHome")); KIO::UDSEntry entry; bool ok = MyNetAccess_stat(url, entry); QVERIFY(ok); KFileItem item(entry, url); QVERIFY(item.isLink()); QCOMPARE(item.linkDest(), QStringLiteral("/tmp")); QVERIFY(item.isReadable()); QVERIFY(!item.isWritable()); QVERIFY(!item.isHidden()); QCOMPARE(item.text(), QStringLiteral("symlinkFromHome")); } void TestTrash::statFileInDirectory() { QUrl url(QStringLiteral("trash:/0-trashDirFromHome/testfile")); KIO::UDSEntry entry; bool ok = MyNetAccess_stat(url, entry); QVERIFY(ok); KFileItem item(entry, url); QVERIFY(item.isFile()); QVERIFY(!item.isLink()); QVERIFY(item.isReadable()); QVERIFY(!item.isWritable()); QVERIFY(!item.isHidden()); QCOMPARE(item.text(), QStringLiteral("testfile")); } void TestTrash::statBrokenSymlinkInSubdir() { QUrl url(QStringLiteral("trash:/0-subDirBrokenSymlink/link")); KIO::UDSEntry entry; bool ok = MyNetAccess_stat(url, entry); QVERIFY(ok); KFileItem item(entry, url); QVERIFY(item.isLink()); QVERIFY(item.isReadable()); QVERIFY(!item.isWritable()); QVERIFY(!item.isHidden()); QCOMPARE(item.linkDest(), QLatin1String("/nonexistent")); } void TestTrash::copyFromTrash(const QString &fileId, const QString &destPath, const QString &relativePath) { QUrl src(QLatin1String("trash:/0-") + fileId); if (!relativePath.isEmpty()) { src.setPath(Utils::concatPaths(src.path(), relativePath)); } QUrl dest = QUrl::fromLocalFile(destPath); QVERIFY(MyNetAccess_exists(src)); // A dnd would use copy(), but we use copyAs to ensure the final filename // qDebug() << "copyAs:" << src << " -> " << dest; KIO::Job *job = KIO::copyAs(src, dest, KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY2(ok, qPrintable(job->errorString())); QString infoFile(m_trashDir + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo")); QVERIFY(QFile::exists(infoFile)); QFileInfo filesItem(m_trashDir + QLatin1String("/files/") + fileId); QVERIFY(filesItem.exists()); QVERIFY(QFile::exists(destPath)); } void TestTrash::copyFileFromTrash() { // To test case of already-existing destination, uncomment this. // This brings up the "rename" dialog though, so it can't be fully automated #if 0 const QString destPath = otherTmpDir() + QString::fromLatin1("fileFromHome_copied"); copyFromTrash("fileFromHome", destPath); QVERIFY(QFileInfo(destPath).isFile()); QCOMPARE(QFileInfo(destPath).size(), 12); #endif } void TestTrash::copyFileInDirectoryFromTrash() { const QString destPath = otherTmpDir() + QLatin1String("testfile_copied"); copyFromTrash(QStringLiteral("trashDirFromHome"), destPath, QStringLiteral("testfile")); QVERIFY(QFileInfo(destPath).isFile()); QCOMPARE(QFileInfo(destPath).size(), 12); QVERIFY(QFileInfo(destPath).isWritable()); } void TestTrash::copyDirectoryFromTrash() { const QString destPath = otherTmpDir() + QLatin1String("trashDirFromHome_copied"); copyFromTrash(QStringLiteral("trashDirFromHome"), destPath); QVERIFY(QFileInfo(destPath).isDir()); QVERIFY(QFile::exists(destPath + QStringLiteral("/testfile"))); QVERIFY(QFile::exists(destPath + QStringLiteral("/subdir/subfile"))); } void TestTrash::copySymlinkFromTrash() // relies on trashSymlinkFromHome() being called first { const QString destPath = otherTmpDir() + QLatin1String("symlinkFromHome_copied"); copyFromTrash(QStringLiteral("symlinkFromHome"), destPath); QVERIFY(QFileInfo(destPath).isSymLink()); } void TestTrash::moveInTrash(const QString &fileId, const QString &destFileId) { const QUrl src(QLatin1String("trash:/0-") + fileId); const QUrl dest(QLatin1String("trash:/") + destFileId); QVERIFY(MyNetAccess_exists(src)); QVERIFY(!MyNetAccess_exists(dest)); KIO::Job *job = KIO::moveAs(src, dest, KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY2(ok, qPrintable(job->errorString())); // Check old doesn't exist anymore QString infoFile(m_trashDir + QStringLiteral("/info/") + fileId + QStringLiteral(".trashinfo")); QVERIFY(!QFile::exists(infoFile)); QFileInfo filesItem(m_trashDir + QStringLiteral("/files/") + fileId); QVERIFY(!filesItem.exists()); // Check new exists now QString newInfoFile(m_trashDir + QStringLiteral("/info/") + destFileId + QStringLiteral(".trashinfo")); QVERIFY(QFile::exists(newInfoFile)); QFileInfo newFilesItem(m_trashDir + QStringLiteral("/files/") + destFileId); QVERIFY(newFilesItem.exists()); } void TestTrash::renameFileInTrash() { const QString fileName = QStringLiteral("renameFileInTrash"); const QString filePath = homeTmpDir() + fileName; createTestFile(filePath); trashFile(filePath, fileName); const QString destFileName = QStringLiteral("fileRenamed"); moveInTrash(fileName, destFileName); // cleanup KIO::Job *delJob = KIO::del(QUrl(QStringLiteral("trash:/0-fileRenamed")), KIO::HideProgressInfo); bool ok = delJob->exec(); QVERIFY2(ok, qPrintable(delJob->errorString())); } void TestTrash::renameDirInTrash() { const QString dirName = QStringLiteral("trashDirFromHome"); const QString destDirName = QStringLiteral("dirRenamed"); moveInTrash(dirName, destDirName); moveInTrash(destDirName, dirName); } void TestTrash::moveFromTrash(const QString &fileId, const QString &destPath, const QString &relativePath) { QUrl src(QLatin1String("trash:/0-") + fileId); if (!relativePath.isEmpty()) { src.setPath(Utils::concatPaths(src.path(), relativePath)); } QUrl dest = QUrl::fromLocalFile(destPath); QVERIFY(MyNetAccess_exists(src)); // A dnd would use move(), but we use moveAs to ensure the final filename KIO::Job *job = KIO::moveAs(src, dest, KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY2(ok, qPrintable(job->errorString())); QString infoFile(m_trashDir + QStringLiteral("/info/") + fileId + QStringLiteral(".trashinfo")); QVERIFY(!QFile::exists(infoFile)); QFileInfo filesItem(m_trashDir + QStringLiteral("/files/") + fileId); QVERIFY(!filesItem.exists()); QVERIFY(QFile::exists(destPath)); QVERIFY(QFileInfo(destPath).isWritable()); } void TestTrash::moveFileFromTrash() { const QString fileName = QStringLiteral("moveFileFromTrash"); const QString filePath = homeTmpDir() + fileName; createTestFile(filePath); const QFile::Permissions origPerms = QFileInfo(filePath).permissions(); trashFile(filePath, fileName); const QString destPath = otherTmpDir() + QStringLiteral("fileFromTrash_restored"); moveFromTrash(fileName, destPath); const QFileInfo destInfo(destPath); QVERIFY(destInfo.isFile()); QCOMPARE(destInfo.size(), 12); QVERIFY(destInfo.isWritable()); QCOMPARE(int(destInfo.permissions()), int(origPerms)); QVERIFY(QFile::remove(destPath)); } void TestTrash::moveFileFromTrashToDir_data() { QTest::addColumn("destDir"); QTest::newRow("home_partition") << homeTmpDir(); // this will trigger a direct renaming QTest::newRow("other_partition") << otherTmpDir(); // this will require a real move } void TestTrash::moveFileFromTrashToDir() { // Given a file in the trash const QString fileName = QStringLiteral("moveFileFromTrashToDir"); const QString filePath = homeTmpDir() + fileName; createTestFile(filePath); const QFile::Permissions origPerms = QFileInfo(filePath).permissions(); trashFile(filePath, fileName); QVERIFY(!QFile::exists(filePath)); // When moving it out to a dir QFETCH(QString, destDir); const auto fileId = QStringLiteral("moveFileFromTrashToDir"); const QString destPath = destDir + fileId; const QUrl src(QLatin1String("trash:/0-") + fileName); const QUrl dest(QUrl::fromLocalFile(destDir)); KIO::Job *job = KIO::move(src, dest, KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY2(ok, qPrintable(job->errorString())); // Then it should move ;) const QFileInfo destInfo(destPath); QVERIFY(destInfo.isFile()); QCOMPARE(destInfo.size(), 12); QVERIFY(destInfo.isWritable()); QCOMPARE(int(destInfo.permissions()), int(origPerms)); // trashinfo should be removed const QString trashInfoPath(m_trashDir + QStringLiteral("/info/") + fileId + QStringLiteral(".trashinfo")); QVERIFY(!QFile::exists(trashInfoPath)); QVERIFY(QFile::remove(destPath)); } void TestTrash::moveFileInDirectoryFromTrash() { const QString destPath = otherTmpDir() + QStringLiteral("testfile_restored"); copyFromTrash(QStringLiteral("trashDirFromHome"), destPath, QStringLiteral("testfile")); QVERIFY(QFileInfo(destPath).isFile()); QCOMPARE(QFileInfo(destPath).size(), 12); } void TestTrash::moveDirectoryFromTrash() { const QString destPath = otherTmpDir() + QStringLiteral("trashDirFromHome_restored"); moveFromTrash(QStringLiteral("trashDirFromHome"), destPath); QVERIFY(QFileInfo(destPath).isDir()); checkDirCacheValidity(); // trash it again, we'll need it later QString dirName = QStringLiteral("trashDirFromHome"); trashDirectory(homeTmpDir() + dirName, dirName); } void TestTrash::trashDirectoryOwnedByRoot() { QUrl u(QStringLiteral("file:///")); ; if (QFile::exists(QStringLiteral("/etc/cups"))) { u.setPath(QStringLiteral("/etc/cups")); } else if (QFile::exists(QStringLiteral("/boot"))) { u.setPath(QStringLiteral("/boot")); } else { u.setPath(QStringLiteral("/etc")); } const QString fileId = u.path(); qDebug() << "fileId=" << fileId; if (geteuid() == 0 || QFileInfo(u.toLocalFile()).isWritable()) { QSKIP("Test must not be run by root."); } KIO::CopyJob *job = KIO::move(u, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo); job->setUiDelegate(nullptr); // no skip dialog, thanks bool ok = job->exec(); QVERIFY(!ok); const int err = job->error(); QVERIFY(err == KIO::ERR_ACCESS_DENIED || err == KIO::ERR_CANNOT_OPEN_FOR_READING); const QString infoPath(m_trashDir + QStringLiteral("/info/") + fileId + QStringLiteral(".trashinfo")); QVERIFY(!QFile::exists(infoPath)); QFileInfo files(m_trashDir + QStringLiteral("/files/") + fileId); QVERIFY(!files.exists()); QVERIFY(QFile::exists(u.path())); } void TestTrash::moveSymlinkFromTrash() { const QString destPath = otherTmpDir() + QStringLiteral("symlinkFromHome_restored"); moveFromTrash(QStringLiteral("symlinkFromHome"), destPath); QVERIFY(QFileInfo(destPath).isSymLink()); } void TestTrash::testMoveNonExistingFile() { const QUrl dest = QUrl::fromLocalFile(homeTmpDir() + QLatin1String("DoesNotExist")); KIO::Job *job = KIO::file_move(QUrl(QStringLiteral("trash:/0-DoesNotExist")), dest, -1, KIO::HideProgressInfo); QVERIFY(!job->exec()); QCOMPARE(job->error(), KIO::ERR_DOES_NOT_EXIST); QCOMPARE(job->errorString(), QStringLiteral("The file or folder trash:/DoesNotExist does not exist.")); } void TestTrash::getFile() { const QString fileId = QStringLiteral("fileFromHome (1)"); const QUrl url = TrashImpl::makeURL(0, fileId, QString()); QTemporaryFile tmpFile; QVERIFY(tmpFile.open()); const QString tmpFilePath = tmpFile.fileName(); KIO::Job *getJob = KIO::file_copy(url, QUrl::fromLocalFile(tmpFilePath), -1, KIO::Overwrite | KIO::HideProgressInfo); bool ok = getJob->exec(); QVERIFY2(ok, qPrintable(getJob->errorString())); // Don't use tmpFile.close()+tmpFile.open() here, the size would still be 0 in the QTemporaryFile object // (due to the use of fstat on the old fd). Arguably a bug (I even have a testcase), but probably // not fixable without breaking the security of QTemporaryFile... QFile reader(tmpFilePath); QVERIFY(reader.open(QIODevice::ReadOnly)); QByteArray str = reader.readAll(); QCOMPARE(str, QByteArray("Hello world\n")); } void TestTrash::restoreFile() { const QString fileId = QStringLiteral("fileFromHome (1)"); const QUrl url = TrashImpl::makeURL(0, fileId, QString()); const QString infoFile(m_trashDir + QStringLiteral("/info/") + fileId + QStringLiteral(".trashinfo")); const QString filesItem(m_trashDir + QStringLiteral("/files/") + fileId); QVERIFY(QFile::exists(infoFile)); QVERIFY(QFile::exists(filesItem)); QByteArray packedArgs; QDataStream stream(&packedArgs, QIODevice::WriteOnly); stream << (int)3 << url; KIO::Job *job = KIO::special(url, packedArgs, KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY(ok); QVERIFY(!QFile::exists(infoFile)); QVERIFY(!QFile::exists(filesItem)); const QString destPath = homeTmpDir() + QStringLiteral("fileFromHome"); QVERIFY(QFile::exists(destPath)); } void TestTrash::restoreFileFromSubDir() { const QString fileId = QStringLiteral("trashDirFromHome (1)/testfile"); QVERIFY(!QFile::exists(homeTmpDir() + QStringLiteral("trashDirFromHome (1)"))); const QUrl url = TrashImpl::makeURL(0, fileId, QString()); const QString infoFile(m_trashDir + QStringLiteral("/info/trashDirFromHome (1).trashinfo")); const QString filesItem(m_trashDir + QStringLiteral("/files/trashDirFromHome (1)/testfile")); QVERIFY(QFile::exists(infoFile)); QVERIFY(QFile::exists(filesItem)); QByteArray packedArgs; QDataStream stream(&packedArgs, QIODevice::WriteOnly); stream << (int)3 << url; KIO::Job *job = KIO::special(url, packedArgs, KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY(!ok); // dest dir doesn't exist -> error message QCOMPARE(job->error(), KIO::ERR_WORKER_DEFINED); // check that nothing happened QVERIFY(QFile::exists(infoFile)); QVERIFY(QFile::exists(filesItem)); QVERIFY(!QFile::exists(homeTmpDir() + QStringLiteral("trashDirFromHome (1)"))); } void TestTrash::restoreFileToDeletedDirectory() { // Ensure we'll get "fileFromHome" as fileId removeFile(m_trashDir, QStringLiteral("/info/fileFromHome.trashinfo")); removeFile(m_trashDir, QStringLiteral("/files/fileFromHome")); trashFileFromHome(); // Delete orig dir KIO::Job *delJob = KIO::del(QUrl::fromLocalFile(homeTmpDir()), KIO::HideProgressInfo); bool delOK = delJob->exec(); QVERIFY(delOK); const QString fileId = QStringLiteral("fileFromHome"); const QUrl url = TrashImpl::makeURL(0, fileId, QString()); const QString infoFile(m_trashDir + QStringLiteral("/info/") + fileId + QStringLiteral(".trashinfo")); const QString filesItem(m_trashDir + QStringLiteral("/files/") + fileId); QVERIFY(QFile::exists(infoFile)); QVERIFY(QFile::exists(filesItem)); QByteArray packedArgs; QDataStream stream(&packedArgs, QIODevice::WriteOnly); stream << (int)3 << url; KIO::Job *job = KIO::special(url, packedArgs, KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY(!ok); // dest dir doesn't exist -> error message QCOMPARE(job->error(), KIO::ERR_WORKER_DEFINED); // check that nothing happened QVERIFY(QFile::exists(infoFile)); QVERIFY(QFile::exists(filesItem)); const QString destPath = homeTmpDir() + QStringLiteral("fileFromHome"); QVERIFY(!QFile::exists(destPath)); } void TestTrash::listRootDir() { m_entryCount = 0; m_listResult.clear(); m_displayNameListResult.clear(); KIO::ListJob *job = KIO::listDir(QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo); connect(job, &KIO::ListJob::entries, this, &TestTrash::slotEntries); bool ok = job->exec(); QVERIFY(ok); qDebug() << "listDir done - m_entryCount=" << m_entryCount; QVERIFY(m_entryCount > 1); // qDebug() << m_listResult; // qDebug() << m_displayNameListResult; QCOMPARE(m_listResult.count(QStringLiteral(".")), 1); // found it, and only once QCOMPARE(m_displayNameListResult.count(QStringLiteral("fileFromHome")), 1); QCOMPARE(m_displayNameListResult.count(QStringLiteral("fileFromHome (1)")), 1); } void TestTrash::listRecursiveRootDir() { m_entryCount = 0; m_listResult.clear(); m_displayNameListResult.clear(); KIO::ListJob *job = KIO::listRecursive(QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo); connect(job, &KIO::ListJob::entries, this, &TestTrash::slotEntries); bool ok = job->exec(); QVERIFY(ok); qDebug() << "listDir done - m_entryCount=" << m_entryCount; QVERIFY(m_entryCount > 1); qDebug() << m_listResult; qDebug() << m_displayNameListResult; QCOMPARE(m_listResult.count(QStringLiteral(".")), 1); // found it, and only once QCOMPARE(m_listResult.count(QStringLiteral("0-fileFromHome")), 1); QCOMPARE(m_listResult.count(QStringLiteral("0-fileFromHome (1)")), 1); QCOMPARE(m_listResult.count(QStringLiteral("0-trashDirFromHome/testfile")), 1); QCOMPARE(m_listResult.count(QStringLiteral("0-readonly/readonly_subdir/testfile_in_subdir")), 1); QCOMPARE(m_listResult.count(QStringLiteral("0-subDirBrokenSymlink/link")), 1); QCOMPARE(m_displayNameListResult.count(QStringLiteral("fileFromHome")), 1); QCOMPARE(m_displayNameListResult.count(QStringLiteral("fileFromHome (1)")), 1); QCOMPARE(m_displayNameListResult.count(QStringLiteral("trashDirFromHome/testfile")), 1); QCOMPARE(m_displayNameListResult.count(QStringLiteral("readonly/readonly_subdir/testfile_in_subdir")), 1); QCOMPARE(m_displayNameListResult.count(QStringLiteral("subDirBrokenSymlink/link")), 1); } void TestTrash::listSubDir() { m_entryCount = 0; m_listResult.clear(); m_displayNameListResult.clear(); KIO::ListJob *job = KIO::listDir(QUrl(QStringLiteral("trash:/0-trashDirFromHome")), KIO::HideProgressInfo); connect(job, &KIO::ListJob::entries, this, &TestTrash::slotEntries); bool ok = job->exec(); QVERIFY(ok); qDebug() << "listDir done - m_entryCount=" << m_entryCount; QCOMPARE(m_entryCount, 3); // qDebug() << m_listResult; // qDebug() << m_displayNameListResult; QCOMPARE(m_listResult.count(QStringLiteral(".")), 1); // found it, and only once QCOMPARE(m_listResult.count(QStringLiteral("testfile")), 1); // found it, and only once QCOMPARE(m_listResult.count(QStringLiteral("subdir")), 1); QCOMPARE(m_displayNameListResult.count(QStringLiteral("testfile")), 1); QCOMPARE(m_displayNameListResult.count(QStringLiteral("subdir")), 1); } void TestTrash::slotEntries(KIO::Job *, const KIO::UDSEntryList &lst) { for (const KIO::UDSEntry &entry : lst) { QString name = entry.stringValue(KIO::UDSEntry::UDS_NAME); QString displayName = entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME); QUrl url(entry.stringValue(KIO::UDSEntry::UDS_URL)); qDebug() << "name" << name << "displayName" << displayName << " UDS_URL=" << url; if (!url.isEmpty()) { QCOMPARE(url.scheme(), QStringLiteral("trash")); } m_listResult << name; m_displayNameListResult << displayName; } m_entryCount += lst.count(); } void TestTrash::emptyTrash() { // ## Even though we use a custom XDG_DATA_HOME value, emptying the // trash would still empty the other trash directories in other partitions. // So we can't activate this test by default. #if 0 // To make this test standalone trashFileFromHome(); // #167051: orphaned files createTestFile(m_trashDir + "/files/testfile_nometadata"); QByteArray packedArgs; QDataStream stream(&packedArgs, QIODevice::WriteOnly); stream << (int)1; KIO::Job *job = KIO::special(QUrl("trash:/"), packedArgs, KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY(ok); KConfig cfg("trashrc", KConfig::SimpleConfig); QVERIFY(cfg.hasGroup("Status")); QVERIFY(cfg.group("Status").readEntry("Empty", false) == true); QVERIFY(!QFile::exists(m_trashDir + "/files/fileFromHome")); QVERIFY(!QFile::exists(m_trashDir + "/files/readonly")); QVERIFY(!QFile::exists(m_trashDir + "/info/readonly.trashinfo")); QVERIFY(QDir(m_trashDir + "/info").entryList(QDir::NoDotAndDotDot | QDir::AllEntries).isEmpty()); QVERIFY(QDir(m_trashDir + "/files").entryList(QDir::NoDotAndDotDot | QDir::AllEntries).isEmpty()); #else qDebug() << " : SKIPPED"; #endif } static bool isTrashEmpty() { KConfig cfg(QStringLiteral("trashrc"), KConfig::SimpleConfig); const KConfigGroup group = cfg.group(QStringLiteral("Status")); return group.readEntry("Empty", true); } void TestTrash::testEmptyTrashSize() { KIO::DirectorySizeJob *job = KIO::directorySize(QUrl(QStringLiteral("trash:/"))); QVERIFY(job->exec()); if (isTrashEmpty()) { QCOMPARE(job->totalSize(), 0ULL); } else { QVERIFY(job->totalSize() < 1000000000 /*1GB*/); // #157023 } } static void checkIcon(const QUrl &url, const QString &expectedIcon) { QString icon = KIO::iconNameForUrl(url); // #100321 QCOMPARE(icon, expectedIcon); } void TestTrash::testIcons() { // The JSON file says "user-trash-full" in all cases, whether the trash is full or not QCOMPARE(KProtocolInfo::icon(QStringLiteral("trash")), QStringLiteral("user-trash-full")); // #100321 if (isTrashEmpty()) { checkIcon(QUrl(QStringLiteral("trash:/")), QStringLiteral("user-trash")); } else { checkIcon(QUrl(QStringLiteral("trash:/")), QStringLiteral("user-trash-full")); } checkIcon(QUrl(QStringLiteral("trash:/foo/")), QStringLiteral("inode-directory")); } QTEST_GUILESS_MAIN(TestTrash) #include "moc_testtrash.cpp"