File indexing completed on 2024-05-19 03:58:12

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2004 David Faure <faure@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "testtrash.h"
0009 
0010 #include <QTest>
0011 
0012 #include "../../../utils_p.h"
0013 #include "filecopyjob.h"
0014 #include "kio_trash.h"
0015 
0016 #include <kprotocolinfo.h>
0017 
0018 #include <KConfigGroup>
0019 #include <kfileitem.h>
0020 #include <kio/chmodjob.h>
0021 #include <kio/copyjob.h>
0022 #include <kio/deletejob.h>
0023 #include <kio/directorysizejob.h>
0024 #include <kio/listjob.h>
0025 #include <kio/statjob.h>
0026 
0027 #include <KJobUiDelegate>
0028 
0029 #include <QDataStream>
0030 #include <QDebug>
0031 #include <QDir>
0032 #include <QFileInfo>
0033 #include <QList>
0034 #include <QStandardPaths>
0035 #include <QTemporaryFile>
0036 #include <QUrl>
0037 
0038 #include <unistd.h>
0039 
0040 // There are two ways to test encoding things:
0041 // * with utf8 filenames
0042 // * with latin1 filenames -- not sure this still works.
0043 //
0044 #define UTF8TEST 1
0045 
0046 int initLocale()
0047 {
0048 #ifdef UTF8TEST
0049     // Assume utf8 system
0050     setenv("LC_ALL", "C.utf-8", 1);
0051     setenv("KDE_UTF8_FILENAMES", "true", 1);
0052 #else
0053     // Ensure a known QFile::encodeName behavior for trashUtf8FileFromHome
0054     // However this assume your $HOME doesn't use characters from other locales...
0055     setenv("LC_ALL", "en_US.ISO-8859-1", 1);
0056     unsetenv("KDE_UTF8_FILENAMES");
0057 #endif
0058     setenv("KIOWORKER_ENABLE_TESTMODE", "1", 1); // ensure the KIO workers call QStandardPaths::setTestModeEnabled(true) too
0059     setenv("KDE_SKIP_KDERC", "1", 1);
0060     unsetenv("KDE_COLOR_DEBUG");
0061     return 0;
0062 }
0063 Q_CONSTRUCTOR_FUNCTION(initLocale)
0064 
0065 QString TestTrash::homeTmpDir() const
0066 {
0067     return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/testtrash/");
0068 }
0069 
0070 QString TestTrash::readOnlyDirPath() const
0071 {
0072     return homeTmpDir() + QLatin1String("readonly");
0073 }
0074 
0075 QString TestTrash::otherTmpDir() const
0076 {
0077     // This one needs to be on another partition for the test to be meaningful
0078     return Utils::slashAppended(m_tempDir.path());
0079 }
0080 
0081 QString TestTrash::utf8FileName() const
0082 {
0083     return QLatin1String("test") + QChar(0x2153); // "1/3" character, not part of latin1
0084 }
0085 
0086 QString TestTrash::umlautFileName() const
0087 {
0088     return QLatin1String("umlaut") + QChar(0xEB);
0089 }
0090 
0091 static void removeFile(const QString &trashDir, const QString &fileName)
0092 {
0093     QDir dir;
0094     dir.remove(trashDir + fileName);
0095     QVERIFY(!QDir(trashDir + fileName).exists());
0096 }
0097 
0098 static void removeDir(const QString &trashDir, const QString &dirName)
0099 {
0100     QDir dir;
0101     dir.rmdir(trashDir + dirName);
0102     QVERIFY(!QDir(trashDir + dirName).exists());
0103 }
0104 
0105 static void removeDirRecursive(const QString &dir)
0106 {
0107     if (QFile::exists(dir)) {
0108         // Make it work even with readonly dirs, like trashReadOnlyDirFromHome() creates
0109         QUrl u = QUrl::fromLocalFile(dir);
0110         // qDebug() << "chmod +0200 on" << u;
0111         KFileItem fileItem(u, QStringLiteral("inode/directory"), KFileItem::Unknown);
0112         KFileItemList fileItemList;
0113         fileItemList.append(fileItem);
0114         KIO::ChmodJob *chmodJob = KIO::chmod(fileItemList, 0200, 0200, QString(), QString(), true /*recursive*/, KIO::HideProgressInfo);
0115         chmodJob->exec();
0116 
0117         KIO::Job *delJob = KIO::del(u, KIO::HideProgressInfo);
0118         if (!delJob->exec()) {
0119             qFatal("Couldn't delete %s", qPrintable(dir));
0120         }
0121     }
0122 }
0123 
0124 void TestTrash::initTestCase()
0125 {
0126     QStandardPaths::setTestModeEnabled(true);
0127 
0128     QVERIFY(m_tempDir.isValid());
0129 
0130 #ifndef Q_OS_OSX
0131     m_trashDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/Trash");
0132     qDebug() << "setup: using trash directory " << m_trashDir;
0133 #endif
0134 
0135     // Look for another writable partition than $HOME (not mandatory)
0136     TrashImpl impl;
0137     impl.init();
0138 
0139     TrashImpl::TrashDirMap trashDirs = impl.trashDirectories();
0140 #ifdef Q_OS_OSX
0141     QVERIFY(trashDirs.contains(0));
0142     m_trashDir = trashDirs.value(0);
0143     qDebug() << "setup: using trash directory " << m_trashDir;
0144 #endif
0145 
0146     TrashImpl::TrashDirMap topDirs = impl.topDirectories();
0147     bool foundTrashDir = false;
0148     m_otherPartitionId = 0;
0149     m_tmpIsWritablePartition = false;
0150     m_tmpTrashId = -1;
0151     QList<int> writableTopDirs;
0152     for (TrashImpl::TrashDirMap::ConstIterator it = trashDirs.constBegin(); it != trashDirs.constEnd(); ++it) {
0153         if (it.key() == 0) {
0154             QCOMPARE(it.value(), m_trashDir);
0155             QVERIFY(topDirs.find(0) == topDirs.end());
0156             foundTrashDir = true;
0157         } else {
0158             QVERIFY(topDirs.find(it.key()) != topDirs.end());
0159             const QString topdir = topDirs[it.key()];
0160             if (QFileInfo(topdir).isWritable()) {
0161                 writableTopDirs.append(it.key());
0162                 if (topdir == QLatin1String("/tmp/")) {
0163                     m_tmpIsWritablePartition = true;
0164                     m_tmpTrashId = it.key();
0165                     qDebug() << "/tmp is on its own partition (trashid=" << m_tmpTrashId << "), some tests will be skipped";
0166                     removeFile(it.value(), QStringLiteral("/info/fileFromOther.trashinfo"));
0167                     removeFile(it.value(), QStringLiteral("/files/fileFromOther"));
0168                     removeFile(it.value(), QStringLiteral("/info/symlinkFromOther.trashinfo"));
0169                     removeFile(it.value(), QStringLiteral("/files/symlinkFromOther"));
0170                     removeFile(it.value(), QStringLiteral("/info/trashDirFromOther.trashinfo"));
0171                     removeFile(it.value(), QStringLiteral("/files/trashDirFromOther/testfile"));
0172                     removeDir(it.value(), QStringLiteral("/files/trashDirFromOther"));
0173                 }
0174             }
0175         }
0176     }
0177     for (auto it = writableTopDirs.constBegin(); it != writableTopDirs.constEnd(); ++it) {
0178         const QString topdir = topDirs[*it];
0179         const QString trashdir = trashDirs[*it];
0180         QVERIFY(!topdir.isEmpty());
0181         QVERIFY(!trashDirs.isEmpty());
0182         if (topdir != QLatin1String("/tmp/") || // we'd prefer not to use /tmp here, to separate the tests
0183             (writableTopDirs.count() > 1)) { // but well, if we have no choice, take it
0184             m_otherPartitionTopDir = topdir;
0185             m_otherPartitionTrashDir = trashdir;
0186             m_otherPartitionId = *it;
0187             qDebug() << "OK, found another writable partition: topDir=" << m_otherPartitionTopDir << " trashDir=" << m_otherPartitionTrashDir
0188                      << " id=" << m_otherPartitionId;
0189             break;
0190         }
0191     }
0192     // Check that m_trashDir got listed
0193     QVERIFY(foundTrashDir);
0194     if (m_otherPartitionTrashDir.isEmpty()) {
0195         qWarning() << "No writable partition other than $HOME found, some tests will be skipped";
0196     }
0197 
0198     // Start with a clean base dir
0199     qDebug() << "initial cleanup";
0200     removeDirRecursive(homeTmpDir());
0201 
0202     QDir dir; // TT: why not a static method?
0203     bool ok = dir.mkdir(homeTmpDir());
0204     if (!ok) {
0205         qFatal("Couldn't create directory: %s", qPrintable(homeTmpDir()));
0206     }
0207     QVERIFY(QFileInfo(otherTmpDir()).isDir());
0208 
0209     // Start with a clean trash too
0210     qDebug() << "removing trash dir";
0211     removeDirRecursive(m_trashDir);
0212 }
0213 
0214 void TestTrash::cleanupTestCase()
0215 {
0216     // Clean up
0217     removeDirRecursive(homeTmpDir());
0218     removeDirRecursive(otherTmpDir());
0219     removeDirRecursive(m_trashDir);
0220 }
0221 
0222 void TestTrash::urlTestFile()
0223 {
0224     const QUrl url = TrashImpl::makeURL(1, QStringLiteral("fileId"), QString());
0225     QCOMPARE(url.url(), QStringLiteral("trash:/1-fileId"));
0226 
0227     int trashId;
0228     QString fileId;
0229     QString relativePath;
0230     bool ok = TrashImpl::parseURL(url, trashId, fileId, relativePath);
0231     QVERIFY(ok);
0232     QCOMPARE(QString::number(trashId), QStringLiteral("1"));
0233     QCOMPARE(fileId, QStringLiteral("fileId"));
0234     QCOMPARE(relativePath, QString());
0235 }
0236 
0237 void TestTrash::urlTestDirectory()
0238 {
0239     const QUrl url = TrashImpl::makeURL(1, QStringLiteral("fileId"), QStringLiteral("subfile"));
0240     QCOMPARE(url.url(), QStringLiteral("trash:/1-fileId/subfile"));
0241 
0242     int trashId;
0243     QString fileId;
0244     QString relativePath;
0245     bool ok = TrashImpl::parseURL(url, trashId, fileId, relativePath);
0246     QVERIFY(ok);
0247     QCOMPARE(trashId, 1);
0248     QCOMPARE(fileId, QStringLiteral("fileId"));
0249     QCOMPARE(relativePath, QStringLiteral("subfile"));
0250 }
0251 
0252 void TestTrash::urlTestSubDirectory()
0253 {
0254     const QUrl url = TrashImpl::makeURL(1, QStringLiteral("fileId"), QStringLiteral("subfile/foobar"));
0255     QCOMPARE(url.url(), QStringLiteral("trash:/1-fileId/subfile/foobar"));
0256 
0257     int trashId;
0258     QString fileId;
0259     QString relativePath;
0260     bool ok = TrashImpl::parseURL(url, trashId, fileId, relativePath);
0261     QVERIFY(ok);
0262     QCOMPARE(trashId, 1);
0263     QCOMPARE(fileId, QStringLiteral("fileId"));
0264     QCOMPARE(relativePath, QStringLiteral("subfile/foobar"));
0265 }
0266 
0267 static void checkInfoFile(const QString &infoPath, const QString &origFilePath)
0268 {
0269     qDebug() << infoPath;
0270     QFileInfo info(infoPath);
0271     QVERIFY2(info.exists(), qPrintable(infoPath));
0272     QVERIFY(info.isFile());
0273     KConfig infoFile(info.absoluteFilePath());
0274     KConfigGroup group = infoFile.group(QStringLiteral("Trash Info"));
0275     if (!group.exists()) {
0276         qFatal("no Trash Info group in %s", qPrintable(info.absoluteFilePath()));
0277     }
0278     const QString origPath = group.readEntry("Path");
0279     QVERIFY(!origPath.isEmpty());
0280     QCOMPARE(origPath.toUtf8(), QUrl::toPercentEncoding(origFilePath, "/"));
0281     if (origFilePath.contains(QChar(0x2153)) || origFilePath.contains(QLatin1Char('%')) || origFilePath.contains(QLatin1String("umlaut"))) {
0282         QVERIFY(origPath.contains(QLatin1Char('%')));
0283     } else {
0284         QVERIFY(!origPath.contains(QLatin1Char('%')));
0285     }
0286     const QString date = group.readEntry("DeletionDate");
0287     QVERIFY(!date.isEmpty());
0288     QVERIFY(date.contains(QLatin1String("T")));
0289 }
0290 
0291 static void createTestFile(const QString &path)
0292 {
0293     QFile f(path);
0294     if (!f.open(QIODevice::WriteOnly)) {
0295         qFatal("Can't create %s", qPrintable(path));
0296     }
0297     f.write("Hello world\n", 12);
0298     f.close();
0299     QVERIFY(QFile::exists(path));
0300 }
0301 
0302 void TestTrash::trashFile(const QString &origFilePath, const QString &fileId)
0303 {
0304     // setup
0305     if (!QFile::exists(origFilePath)) {
0306         createTestFile(origFilePath);
0307     }
0308     QUrl u = QUrl::fromLocalFile(origFilePath);
0309 
0310     // test
0311     KIO::Job *job = KIO::move(u, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo);
0312     bool ok = job->exec();
0313     if (!ok) {
0314         qCritical() << "moving " << u << " to trash failed with error " << job->error() << " " << job->errorString();
0315     }
0316     QVERIFY(ok);
0317     if (origFilePath.startsWith(QLatin1String("/tmp")) && m_tmpIsWritablePartition) {
0318         qDebug() << " TESTS SKIPPED";
0319     } else {
0320         checkInfoFile(m_trashDir + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo"), origFilePath);
0321 
0322         QFileInfo fileInTrash(m_trashDir + QLatin1String("/files/") + fileId);
0323         QVERIFY(fileInTrash.isFile());
0324         QCOMPARE(fileInTrash.size(), 12);
0325     }
0326 
0327     // coolo suggests testing that the original file is actually gone, too :)
0328     QVERIFY(!QFile::exists(origFilePath));
0329 
0330     QMap<QString, QString> metaData = job->metaData();
0331     QVERIFY(!metaData.isEmpty());
0332     bool found = false;
0333     QMap<QString, QString>::ConstIterator it = metaData.constBegin();
0334     for (; it != metaData.constEnd(); ++it) {
0335         if (it.key().startsWith(QLatin1String("trashURL"))) {
0336             QUrl trashURL(it.value());
0337             qDebug() << trashURL;
0338             QVERIFY(!trashURL.isEmpty());
0339             QCOMPARE(trashURL.scheme(), QLatin1String("trash"));
0340             int trashId = 0;
0341             if (origFilePath.startsWith(QLatin1String("/tmp")) && m_tmpIsWritablePartition) {
0342                 trashId = m_tmpTrashId;
0343             }
0344             QCOMPARE(trashURL.path(), QString(QStringLiteral("/") + QString::number(trashId) + QLatin1Char('-') + fileId));
0345             found = true;
0346         }
0347     }
0348     QVERIFY(found);
0349 }
0350 
0351 void TestTrash::trashFileFromHome()
0352 {
0353     const QString fileName = QStringLiteral("fileFromHome");
0354     trashFile(homeTmpDir() + fileName, fileName);
0355 
0356     // Do it again, check that we got a different id
0357     trashFile(homeTmpDir() + fileName, fileName + QLatin1String(" (1)"));
0358 }
0359 
0360 void TestTrash::trashPercentFileFromHome()
0361 {
0362     const QString fileName = QStringLiteral("file%2f");
0363     trashFile(homeTmpDir() + fileName, fileName);
0364 }
0365 
0366 void TestTrash::trashUtf8FileFromHome()
0367 {
0368 #ifdef UTF8TEST
0369     const QString fileName = utf8FileName();
0370     trashFile(homeTmpDir() + fileName, fileName);
0371 #endif
0372 }
0373 
0374 void TestTrash::trashUmlautFileFromHome()
0375 {
0376     const QString fileName = umlautFileName();
0377     trashFile(homeTmpDir() + fileName, fileName);
0378 }
0379 
0380 void TestTrash::testTrashNotEmpty()
0381 {
0382     KConfig cfg(QStringLiteral("trashrc"), KConfig::SimpleConfig);
0383     const KConfigGroup group = cfg.group(QStringLiteral("Status"));
0384     QVERIFY(group.exists());
0385     QCOMPARE(group.readEntry("Empty", true), false);
0386 }
0387 
0388 void TestTrash::trashFileFromOther()
0389 {
0390     const QString fileName = QStringLiteral("fileFromOther");
0391     trashFile(otherTmpDir() + fileName, fileName);
0392 }
0393 
0394 void TestTrash::trashFileIntoOtherPartition()
0395 {
0396     if (m_otherPartitionTrashDir.isEmpty()) {
0397         qDebug() << " - SKIPPED";
0398         return;
0399     }
0400     const QString fileName = QStringLiteral("testtrash-file");
0401     const QString origFilePath = m_otherPartitionTopDir + fileName;
0402     const QString &fileId = fileName;
0403     // cleanup
0404     QFile::remove(m_otherPartitionTrashDir + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo"));
0405     QFile::remove(m_otherPartitionTrashDir + QLatin1String("/files/") + fileId);
0406 
0407     // setup
0408     if (!QFile::exists(origFilePath)) {
0409         createTestFile(origFilePath);
0410     }
0411     QUrl u = QUrl::fromLocalFile(origFilePath);
0412 
0413     // test
0414     KIO::Job *job = KIO::move(u, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo);
0415     bool ok = job->exec();
0416     QVERIFY(ok);
0417     QMap<QString, QString> metaData = job->metaData();
0418     // Note that the Path stored in the info file is relative, on other partitions (#95652)
0419     checkInfoFile(m_otherPartitionTrashDir + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo"), fileName);
0420 
0421     QFileInfo files(m_otherPartitionTrashDir + QLatin1String("/files/") + fileId);
0422     QVERIFY(files.isFile());
0423     QCOMPARE(files.size(), 12);
0424 
0425     // coolo suggests testing that the original file is actually gone, too :)
0426     QVERIFY(!QFile::exists(origFilePath));
0427 
0428     QVERIFY(!metaData.isEmpty());
0429     bool found = false;
0430     QMap<QString, QString>::ConstIterator it = metaData.constBegin();
0431     for (; it != metaData.constEnd(); ++it) {
0432         if (it.key().startsWith(QLatin1String("trashURL"))) {
0433             QUrl trashURL(it.value());
0434             qDebug() << trashURL;
0435             QVERIFY(!trashURL.isEmpty());
0436             QCOMPARE(trashURL.scheme(), QLatin1String("trash"));
0437             QCOMPARE(trashURL.path(), QStringLiteral("/%1-%2").arg(m_otherPartitionId).arg(fileId));
0438             found = true;
0439         }
0440     }
0441     QVERIFY(found);
0442 }
0443 
0444 void TestTrash::trashFileOwnedByRoot()
0445 {
0446     QUrl u(QStringLiteral("file:///etc/passwd"));
0447     const QString fileId = QStringLiteral("passwd");
0448 
0449     if (geteuid() == 0 || QFileInfo(u.toLocalFile()).isWritable()) {
0450         QSKIP("Test must not be run by root.");
0451     }
0452 
0453     KIO::CopyJob *job = KIO::move(u, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo);
0454     job->setUiDelegate(nullptr); // no skip dialog, thanks
0455     bool ok = job->exec();
0456     QVERIFY(!ok);
0457 
0458     QCOMPARE(job->error(), KIO::ERR_ACCESS_DENIED);
0459     const QString infoPath(m_trashDir + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo"));
0460     QVERIFY(!QFile::exists(infoPath));
0461 
0462     QFileInfo files(m_trashDir + QLatin1String("/files/") + fileId);
0463     QVERIFY(!files.exists());
0464 
0465     QVERIFY(QFile::exists(u.path()));
0466 }
0467 
0468 void TestTrash::trashSymlink(const QString &origFilePath, const QString &fileId, bool broken)
0469 {
0470     // setup
0471     const char *target = broken ? "/nonexistent" : "/tmp";
0472     bool ok = ::symlink(target, QFile::encodeName(origFilePath).constData()) == 0;
0473     QVERIFY(ok);
0474     QUrl u = QUrl::fromLocalFile(origFilePath);
0475 
0476     // test
0477     KIO::Job *job = KIO::move(u, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo);
0478     ok = job->exec();
0479     QVERIFY(ok);
0480     if (origFilePath.startsWith(QLatin1String("/tmp")) && m_tmpIsWritablePartition) {
0481         qDebug() << " TESTS SKIPPED";
0482         return;
0483     }
0484     checkInfoFile(m_trashDir + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo"), origFilePath);
0485 
0486     QFileInfo files(m_trashDir + QLatin1String("/files/") + fileId);
0487     QVERIFY(files.isSymLink());
0488     QCOMPARE(files.symLinkTarget(), QFile::decodeName(target));
0489     QVERIFY(!QFile::exists(origFilePath));
0490 }
0491 
0492 void TestTrash::trashSymlinkFromHome()
0493 {
0494     const QString fileName = QStringLiteral("symlinkFromHome");
0495     trashSymlink(homeTmpDir() + fileName, fileName, false);
0496 }
0497 
0498 void TestTrash::trashSymlinkFromOther()
0499 {
0500     const QString fileName = QStringLiteral("symlinkFromOther");
0501     trashSymlink(otherTmpDir() + fileName, fileName, false);
0502 }
0503 
0504 void TestTrash::trashBrokenSymlinkFromHome()
0505 {
0506     const QString fileName = QStringLiteral("brokenSymlinkFromHome");
0507     trashSymlink(homeTmpDir() + fileName, fileName, true);
0508 }
0509 
0510 void TestTrash::trashDirectory(const QString &origPath, const QString &fileId)
0511 {
0512     qDebug() << fileId;
0513     // setup
0514     if (!QFileInfo::exists(origPath)) {
0515         QDir dir;
0516         bool ok = dir.mkdir(origPath);
0517         QVERIFY(ok);
0518     }
0519     createTestFile(origPath + QLatin1String("/testfile"));
0520     QVERIFY(QDir().mkdir(origPath + QStringLiteral("/subdir")));
0521     createTestFile(origPath + QLatin1String("/subdir/subfile"));
0522     QUrl u = QUrl::fromLocalFile(origPath);
0523 
0524     // test
0525     KIO::Job *job = KIO::move(u, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo);
0526     QVERIFY(job->exec());
0527     if (origPath.startsWith(QLatin1String("/tmp")) && m_tmpIsWritablePartition) {
0528         qDebug() << " TESTS SKIPPED";
0529         return;
0530     }
0531     checkInfoFile(m_trashDir + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo"), origPath);
0532 
0533     QFileInfo filesDir(m_trashDir + QLatin1String("/files/") + fileId);
0534     QVERIFY(filesDir.isDir());
0535     QFileInfo files(m_trashDir + QLatin1String("/files/") + fileId + QLatin1String("/testfile"));
0536     QVERIFY(files.exists());
0537     QVERIFY(files.isFile());
0538     QCOMPARE(files.size(), 12);
0539     QVERIFY(!QFile::exists(origPath));
0540     QVERIFY(QFile::exists(m_trashDir + QStringLiteral("/files/") + fileId + QStringLiteral("/subdir/subfile")));
0541 
0542     QFile dirCache(m_trashDir + QLatin1String("/directorysizes"));
0543     QVERIFY2(dirCache.open(QIODevice::ReadOnly), qPrintable(dirCache.fileName()));
0544     QByteArray lines;
0545     bool found = false;
0546     while (!dirCache.atEnd()) {
0547         const QByteArray line = dirCache.readLine();
0548         if (line.endsWith(QByteArray(' ' + QFile::encodeName(fileId).toPercentEncoding() + '\n'))) {
0549             QVERIFY(!found); // should be there only once!
0550             found = true;
0551         }
0552         lines += line;
0553     }
0554     QVERIFY2(found, lines.constData());
0555     // qDebug() << lines;
0556 
0557     checkDirCacheValidity();
0558 }
0559 
0560 void TestTrash::checkDirCacheValidity()
0561 {
0562     QFile dirCache(m_trashDir + QLatin1String("/directorysizes"));
0563     QVERIFY2(dirCache.open(QIODevice::ReadOnly), qPrintable(dirCache.fileName()));
0564     QSet<QByteArray> seenDirs;
0565     while (!dirCache.atEnd()) {
0566         QByteArray line = dirCache.readLine();
0567         QVERIFY(line.endsWith('\n'));
0568         line.chop(1);
0569         qDebug() << "LINE" << line;
0570 
0571         const auto exploded = line.split(' ');
0572         QCOMPARE(exploded.size(), 3);
0573 
0574         bool succeeded = false;
0575         const int size = exploded.at(0).toInt(&succeeded);
0576         QVERIFY(succeeded);
0577         QVERIFY(size > 0);
0578 
0579         const int mtime = exploded.at(0).toInt(&succeeded);
0580         QVERIFY(succeeded);
0581         QVERIFY(mtime > 0);
0582         QVERIFY(QDateTime::fromMSecsSinceEpoch(mtime).isValid());
0583 
0584         const QByteArray dir = QByteArray::fromPercentEncoding(exploded.at(2));
0585         QVERIFY2(!seenDirs.contains(dir), dir.constData());
0586         seenDirs.insert(dir);
0587         const QString localDir = m_trashDir + QLatin1String("/files/") + QFile::decodeName(dir);
0588         QVERIFY2(QFile::exists(localDir), qPrintable(localDir));
0589         QVERIFY(QFileInfo(localDir).isDir());
0590     }
0591 }
0592 
0593 void TestTrash::trashDirectoryFromHome()
0594 {
0595     QString dirName = QStringLiteral("trashDirFromHome");
0596     trashDirectory(homeTmpDir() + dirName, dirName);
0597     checkDirCacheValidity();
0598     // Do it again, check that we got a different id
0599     trashDirectory(homeTmpDir() + dirName, dirName + QLatin1String(" (1)"));
0600 }
0601 
0602 void TestTrash::trashDotDirectory()
0603 {
0604     QString dirName = QStringLiteral(".dotTrashDirFromHome");
0605     trashDirectory(homeTmpDir() + dirName, dirName);
0606     // Do it again, check that we got a different id
0607     // TODO trashDirectory(homeTmpDir() + dirName, dirName + QString::fromLatin1(" (1)"));
0608 }
0609 
0610 void TestTrash::trashReadOnlyDirFromHome()
0611 {
0612     const QString dirName = readOnlyDirPath();
0613     QDir dir;
0614     bool ok = dir.mkdir(dirName);
0615     QVERIFY(ok);
0616     // #130780
0617     const QString subDirPath = dirName + QLatin1String("/readonly_subdir");
0618     ok = dir.mkdir(subDirPath);
0619     QVERIFY(ok);
0620     createTestFile(subDirPath + QLatin1String("/testfile_in_subdir"));
0621     ::chmod(QFile::encodeName(subDirPath).constData(), 0500);
0622 
0623     trashDirectory(dirName, QStringLiteral("readonly"));
0624 }
0625 
0626 void TestTrash::trashDirectoryFromOther()
0627 {
0628     QString dirName = QStringLiteral("trashDirFromOther");
0629     trashDirectory(otherTmpDir() + dirName, dirName);
0630 }
0631 
0632 void TestTrash::trashDirectoryWithTrailingSlash()
0633 {
0634     QString dirName = QStringLiteral("dirwithslash/");
0635     trashDirectory(homeTmpDir() + dirName, QStringLiteral("dirwithslash"));
0636 }
0637 
0638 void TestTrash::trashBrokenSymlinkIntoSubdir()
0639 {
0640     QString origPath = homeTmpDir() + QStringLiteral("subDirBrokenSymlink");
0641 
0642     if (!QFileInfo::exists(origPath)) {
0643         QDir dir;
0644         bool ok = dir.mkdir(origPath);
0645         QVERIFY(ok);
0646     }
0647     bool ok = ::symlink("/nonexistent", QFile::encodeName(origPath + QStringLiteral("/link")).constData()) == 0;
0648     QVERIFY(ok);
0649 
0650     trashDirectory(origPath, QStringLiteral("subDirBrokenSymlink"));
0651 }
0652 
0653 void TestTrash::testRemoveStaleInfofile()
0654 {
0655     const QString fileName = QStringLiteral("disappearingFileInTrash");
0656     const QString filePath = homeTmpDir() + fileName;
0657     createTestFile(filePath);
0658     trashFile(filePath, fileName);
0659 
0660     const QString pathInTrash = m_trashDir + QLatin1String("/files/") + QLatin1String("disappearingFileInTrash");
0661     // remove the file without using KIO
0662     QVERIFY(QFile::remove(pathInTrash));
0663 
0664     // .trashinfo file still exists
0665     const QString infoPath = m_trashDir + QLatin1String("/info/disappearingFileInTrash.trashinfo");
0666     QVERIFY(QFile(infoPath).exists());
0667 
0668     KIO::ListJob *job = KIO::listDir(QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo);
0669     connect(job, &KIO::ListJob::entries, this, &TestTrash::slotEntries);
0670     QVERIFY(job->exec());
0671 
0672     // during the list job, kio_trash should have deleted the .trashinfo file since it
0673     // references a trashed file that doesn't exist any more
0674     QVERIFY(!QFile(infoPath).exists());
0675 }
0676 
0677 void TestTrash::delRootFile()
0678 {
0679     // test deleting a trashed file
0680     KIO::Job *delJob = KIO::del(QUrl(QStringLiteral("trash:/0-fileFromHome")), KIO::HideProgressInfo);
0681     bool ok = delJob->exec();
0682     QVERIFY(ok);
0683 
0684     QFileInfo file(m_trashDir + QLatin1String("/files/fileFromHome"));
0685     QVERIFY(!file.exists());
0686     QFileInfo info(m_trashDir + QLatin1String("/info/fileFromHome.trashinfo"));
0687     QVERIFY(!info.exists());
0688 
0689     // trash it again, we might need it later
0690     const QString fileName = QStringLiteral("fileFromHome");
0691     trashFile(homeTmpDir() + fileName, fileName);
0692 }
0693 
0694 void TestTrash::delFileInDirectory()
0695 {
0696     // test deleting a file inside a trashed directory -> not allowed
0697     KIO::Job *delJob = KIO::del(QUrl(QStringLiteral("trash:/0-trashDirFromHome/testfile")), KIO::HideProgressInfo);
0698     bool ok = delJob->exec();
0699     QVERIFY(!ok);
0700     QCOMPARE(delJob->error(), KIO::ERR_ACCESS_DENIED);
0701 
0702     QFileInfo dir(m_trashDir + QLatin1String("/files/trashDirFromHome"));
0703     QVERIFY(dir.exists());
0704     QFileInfo file(m_trashDir + QLatin1String("/files/trashDirFromHome/testfile"));
0705     QVERIFY(file.exists());
0706     QFileInfo info(m_trashDir + QLatin1String("/info/trashDirFromHome.trashinfo"));
0707     QVERIFY(info.exists());
0708 }
0709 
0710 void TestTrash::delDirectory()
0711 {
0712     // test deleting a trashed directory
0713     KIO::Job *delJob = KIO::del(QUrl(QStringLiteral("trash:/0-trashDirFromHome")), KIO::HideProgressInfo);
0714     bool ok = delJob->exec();
0715     QVERIFY(ok);
0716 
0717     QFileInfo dir(m_trashDir + QLatin1String("/files/trashDirFromHome"));
0718     QVERIFY(!dir.exists());
0719     QFileInfo file(m_trashDir + QLatin1String("/files/trashDirFromHome/testfile"));
0720     QVERIFY(!file.exists());
0721     QFileInfo info(m_trashDir + QLatin1String("/info/trashDirFromHome.trashinfo"));
0722     QVERIFY(!info.exists());
0723 
0724     checkDirCacheValidity();
0725 
0726     // trash it again, we'll need it later
0727     QString dirName = QStringLiteral("trashDirFromHome");
0728     trashDirectory(homeTmpDir() + dirName, dirName);
0729 }
0730 
0731 static bool MyNetAccess_stat(const QUrl &url, KIO::UDSEntry &entry)
0732 {
0733     KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo);
0734     bool ok = statJob->exec();
0735     if (ok) {
0736         entry = statJob->statResult();
0737     }
0738     return ok;
0739 }
0740 static bool MyNetAccess_exists(const QUrl &url)
0741 {
0742     KIO::UDSEntry dummy;
0743     return MyNetAccess_stat(url, dummy);
0744 }
0745 
0746 void TestTrash::mostLocalUrlTest()
0747 {
0748     const QStringList trashFiles = QDir(m_trashDir + QLatin1String("/files/")).entryList();
0749     for (const QString &file : trashFiles) {
0750         if (file == QLatin1Char('.') || file == QLatin1String("..")) {
0751             continue;
0752         }
0753         QUrl url;
0754         url.setScheme(QStringLiteral("trash"));
0755         url.setPath(QLatin1String("0-") + file);
0756         KIO::StatJob *statJob = KIO::mostLocalUrl(url, KIO::HideProgressInfo);
0757         QVERIFY(statJob->exec());
0758         QCOMPARE(QUrl::fromLocalFile(m_trashDir + QStringLiteral("/files/") + file), statJob->mostLocalUrl());
0759     }
0760 }
0761 
0762 void TestTrash::statRoot()
0763 {
0764     QUrl url(QStringLiteral("trash:/"));
0765     KIO::UDSEntry entry;
0766     bool ok = MyNetAccess_stat(url, entry);
0767     QVERIFY(ok);
0768     KFileItem item(entry, url);
0769     QVERIFY(item.isDir());
0770     QVERIFY(!item.isLink());
0771     QVERIFY(item.isReadable());
0772     QVERIFY(item.isWritable());
0773     QVERIFY(!item.isHidden());
0774     QCOMPARE(item.name(), QStringLiteral("."));
0775 }
0776 
0777 void TestTrash::statFileInRoot()
0778 {
0779     QUrl url(QStringLiteral("trash:/0-fileFromHome"));
0780     KIO::UDSEntry entry;
0781     bool ok = MyNetAccess_stat(url, entry);
0782     QVERIFY(ok);
0783     KFileItem item(entry, url);
0784     QVERIFY(item.isFile());
0785     QVERIFY(!item.isDir());
0786     QVERIFY(!item.isLink());
0787     QVERIFY(item.isReadable());
0788     QVERIFY(!item.isWritable());
0789     QVERIFY(!item.isHidden());
0790     QCOMPARE(item.text(), QStringLiteral("fileFromHome"));
0791 }
0792 
0793 void TestTrash::statDirectoryInRoot()
0794 {
0795     QUrl url(QStringLiteral("trash:/0-trashDirFromHome"));
0796     KIO::UDSEntry entry;
0797     bool ok = MyNetAccess_stat(url, entry);
0798     QVERIFY(ok);
0799     KFileItem item(entry, url);
0800     QVERIFY(item.isDir());
0801     QVERIFY(!item.isLink());
0802     QVERIFY(item.isReadable());
0803     QVERIFY(!item.isWritable());
0804     QVERIFY(!item.isHidden());
0805     QCOMPARE(item.text(), QStringLiteral("trashDirFromHome"));
0806 }
0807 
0808 void TestTrash::statSymlinkInRoot()
0809 {
0810     QUrl url(QStringLiteral("trash:/0-symlinkFromHome"));
0811     KIO::UDSEntry entry;
0812     bool ok = MyNetAccess_stat(url, entry);
0813     QVERIFY(ok);
0814     KFileItem item(entry, url);
0815     QVERIFY(item.isLink());
0816     QCOMPARE(item.linkDest(), QStringLiteral("/tmp"));
0817     QVERIFY(item.isReadable());
0818     QVERIFY(!item.isWritable());
0819     QVERIFY(!item.isHidden());
0820     QCOMPARE(item.text(), QStringLiteral("symlinkFromHome"));
0821 }
0822 
0823 void TestTrash::statFileInDirectory()
0824 {
0825     QUrl url(QStringLiteral("trash:/0-trashDirFromHome/testfile"));
0826     KIO::UDSEntry entry;
0827     bool ok = MyNetAccess_stat(url, entry);
0828     QVERIFY(ok);
0829     KFileItem item(entry, url);
0830     QVERIFY(item.isFile());
0831     QVERIFY(!item.isLink());
0832     QVERIFY(item.isReadable());
0833     QVERIFY(!item.isWritable());
0834     QVERIFY(!item.isHidden());
0835     QCOMPARE(item.text(), QStringLiteral("testfile"));
0836 }
0837 
0838 void TestTrash::statBrokenSymlinkInSubdir()
0839 {
0840     QUrl url(QStringLiteral("trash:/0-subDirBrokenSymlink/link"));
0841     KIO::UDSEntry entry;
0842     bool ok = MyNetAccess_stat(url, entry);
0843     QVERIFY(ok);
0844     KFileItem item(entry, url);
0845     QVERIFY(item.isLink());
0846     QVERIFY(item.isReadable());
0847     QVERIFY(!item.isWritable());
0848     QVERIFY(!item.isHidden());
0849     QCOMPARE(item.linkDest(), QLatin1String("/nonexistent"));
0850 }
0851 
0852 void TestTrash::copyFromTrash(const QString &fileId, const QString &destPath, const QString &relativePath)
0853 {
0854     QUrl src(QLatin1String("trash:/0-") + fileId);
0855     if (!relativePath.isEmpty()) {
0856         src.setPath(Utils::concatPaths(src.path(), relativePath));
0857     }
0858     QUrl dest = QUrl::fromLocalFile(destPath);
0859 
0860     QVERIFY(MyNetAccess_exists(src));
0861 
0862     // A dnd would use copy(), but we use copyAs to ensure the final filename
0863     // qDebug() << "copyAs:" << src << " -> " << dest;
0864     KIO::Job *job = KIO::copyAs(src, dest, KIO::HideProgressInfo);
0865     bool ok = job->exec();
0866     QVERIFY2(ok, qPrintable(job->errorString()));
0867     QString infoFile(m_trashDir + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo"));
0868     QVERIFY(QFile::exists(infoFile));
0869 
0870     QFileInfo filesItem(m_trashDir + QLatin1String("/files/") + fileId);
0871     QVERIFY(filesItem.exists());
0872 
0873     QVERIFY(QFile::exists(destPath));
0874 }
0875 
0876 void TestTrash::copyFileFromTrash()
0877 {
0878 // To test case of already-existing destination, uncomment this.
0879 // This brings up the "rename" dialog though, so it can't be fully automated
0880 #if 0
0881     const QString destPath = otherTmpDir() + QString::fromLatin1("fileFromHome_copied");
0882     copyFromTrash("fileFromHome", destPath);
0883     QVERIFY(QFileInfo(destPath).isFile());
0884     QCOMPARE(QFileInfo(destPath).size(), 12);
0885 #endif
0886 }
0887 
0888 void TestTrash::copyFileInDirectoryFromTrash()
0889 {
0890     const QString destPath = otherTmpDir() + QLatin1String("testfile_copied");
0891     copyFromTrash(QStringLiteral("trashDirFromHome"), destPath, QStringLiteral("testfile"));
0892     QVERIFY(QFileInfo(destPath).isFile());
0893     QCOMPARE(QFileInfo(destPath).size(), 12);
0894     QVERIFY(QFileInfo(destPath).isWritable());
0895 }
0896 
0897 void TestTrash::copyDirectoryFromTrash()
0898 {
0899     const QString destPath = otherTmpDir() + QLatin1String("trashDirFromHome_copied");
0900     copyFromTrash(QStringLiteral("trashDirFromHome"), destPath);
0901     QVERIFY(QFileInfo(destPath).isDir());
0902     QVERIFY(QFile::exists(destPath + QStringLiteral("/testfile")));
0903     QVERIFY(QFile::exists(destPath + QStringLiteral("/subdir/subfile")));
0904 }
0905 
0906 void TestTrash::copySymlinkFromTrash() // relies on trashSymlinkFromHome() being called first
0907 {
0908     const QString destPath = otherTmpDir() + QLatin1String("symlinkFromHome_copied");
0909     copyFromTrash(QStringLiteral("symlinkFromHome"), destPath);
0910     QVERIFY(QFileInfo(destPath).isSymLink());
0911 }
0912 
0913 void TestTrash::moveInTrash(const QString &fileId, const QString &destFileId)
0914 {
0915     const QUrl src(QLatin1String("trash:/0-") + fileId);
0916     const QUrl dest(QLatin1String("trash:/") + destFileId);
0917 
0918     QVERIFY(MyNetAccess_exists(src));
0919     QVERIFY(!MyNetAccess_exists(dest));
0920 
0921     KIO::Job *job = KIO::moveAs(src, dest, KIO::HideProgressInfo);
0922     bool ok = job->exec();
0923     QVERIFY2(ok, qPrintable(job->errorString()));
0924 
0925     // Check old doesn't exist anymore
0926     QString infoFile(m_trashDir + QStringLiteral("/info/") + fileId + QStringLiteral(".trashinfo"));
0927     QVERIFY(!QFile::exists(infoFile));
0928     QFileInfo filesItem(m_trashDir + QStringLiteral("/files/") + fileId);
0929     QVERIFY(!filesItem.exists());
0930 
0931     // Check new exists now
0932     QString newInfoFile(m_trashDir + QStringLiteral("/info/") + destFileId + QStringLiteral(".trashinfo"));
0933     QVERIFY(QFile::exists(newInfoFile));
0934     QFileInfo newFilesItem(m_trashDir + QStringLiteral("/files/") + destFileId);
0935     QVERIFY(newFilesItem.exists());
0936 }
0937 
0938 void TestTrash::renameFileInTrash()
0939 {
0940     const QString fileName = QStringLiteral("renameFileInTrash");
0941     const QString filePath = homeTmpDir() + fileName;
0942     createTestFile(filePath);
0943     trashFile(filePath, fileName);
0944 
0945     const QString destFileName = QStringLiteral("fileRenamed");
0946     moveInTrash(fileName, destFileName);
0947 
0948     // cleanup
0949     KIO::Job *delJob = KIO::del(QUrl(QStringLiteral("trash:/0-fileRenamed")), KIO::HideProgressInfo);
0950     bool ok = delJob->exec();
0951     QVERIFY2(ok, qPrintable(delJob->errorString()));
0952 }
0953 
0954 void TestTrash::renameDirInTrash()
0955 {
0956     const QString dirName = QStringLiteral("trashDirFromHome");
0957     const QString destDirName = QStringLiteral("dirRenamed");
0958     moveInTrash(dirName, destDirName);
0959     moveInTrash(destDirName, dirName);
0960 }
0961 
0962 void TestTrash::moveFromTrash(const QString &fileId, const QString &destPath, const QString &relativePath)
0963 {
0964     QUrl src(QLatin1String("trash:/0-") + fileId);
0965     if (!relativePath.isEmpty()) {
0966         src.setPath(Utils::concatPaths(src.path(), relativePath));
0967     }
0968     QUrl dest = QUrl::fromLocalFile(destPath);
0969 
0970     QVERIFY(MyNetAccess_exists(src));
0971 
0972     // A dnd would use move(), but we use moveAs to ensure the final filename
0973     KIO::Job *job = KIO::moveAs(src, dest, KIO::HideProgressInfo);
0974     bool ok = job->exec();
0975     QVERIFY2(ok, qPrintable(job->errorString()));
0976     QString infoFile(m_trashDir + QStringLiteral("/info/") + fileId + QStringLiteral(".trashinfo"));
0977     QVERIFY(!QFile::exists(infoFile));
0978 
0979     QFileInfo filesItem(m_trashDir + QStringLiteral("/files/") + fileId);
0980     QVERIFY(!filesItem.exists());
0981 
0982     QVERIFY(QFile::exists(destPath));
0983     QVERIFY(QFileInfo(destPath).isWritable());
0984 }
0985 
0986 void TestTrash::moveFileFromTrash()
0987 {
0988     const QString fileName = QStringLiteral("moveFileFromTrash");
0989     const QString filePath = homeTmpDir() + fileName;
0990     createTestFile(filePath);
0991     const QFile::Permissions origPerms = QFileInfo(filePath).permissions();
0992     trashFile(filePath, fileName);
0993 
0994     const QString destPath = otherTmpDir() + QStringLiteral("fileFromTrash_restored");
0995     moveFromTrash(fileName, destPath);
0996     const QFileInfo destInfo(destPath);
0997     QVERIFY(destInfo.isFile());
0998     QCOMPARE(destInfo.size(), 12);
0999     QVERIFY(destInfo.isWritable());
1000     QCOMPARE(int(destInfo.permissions()), int(origPerms));
1001 
1002     QVERIFY(QFile::remove(destPath));
1003 }
1004 
1005 void TestTrash::moveFileFromTrashToDir_data()
1006 {
1007     QTest::addColumn<QString>("destDir");
1008 
1009     QTest::newRow("home_partition") << homeTmpDir(); // this will trigger a direct renaming
1010     QTest::newRow("other_partition") << otherTmpDir(); // this will require a real move
1011 }
1012 
1013 void TestTrash::moveFileFromTrashToDir()
1014 {
1015     // Given a file in the trash
1016     const QString fileName = QStringLiteral("moveFileFromTrashToDir");
1017     const QString filePath = homeTmpDir() + fileName;
1018     createTestFile(filePath);
1019     const QFile::Permissions origPerms = QFileInfo(filePath).permissions();
1020     trashFile(filePath, fileName);
1021     QVERIFY(!QFile::exists(filePath));
1022 
1023     // When moving it out to a dir
1024     QFETCH(QString, destDir);
1025     const auto fileId = QStringLiteral("moveFileFromTrashToDir");
1026     const QString destPath = destDir + fileId;
1027 
1028     const QUrl src(QLatin1String("trash:/0-") + fileName);
1029     const QUrl dest(QUrl::fromLocalFile(destDir));
1030     KIO::Job *job = KIO::move(src, dest, KIO::HideProgressInfo);
1031     bool ok = job->exec();
1032     QVERIFY2(ok, qPrintable(job->errorString()));
1033 
1034     // Then it should move ;)
1035     const QFileInfo destInfo(destPath);
1036     QVERIFY(destInfo.isFile());
1037     QCOMPARE(destInfo.size(), 12);
1038     QVERIFY(destInfo.isWritable());
1039     QCOMPARE(int(destInfo.permissions()), int(origPerms));
1040 
1041     // trashinfo should be removed
1042     const QString trashInfoPath(m_trashDir + QStringLiteral("/info/") + fileId + QStringLiteral(".trashinfo"));
1043     QVERIFY(!QFile::exists(trashInfoPath));
1044 
1045     QVERIFY(QFile::remove(destPath));
1046 }
1047 
1048 void TestTrash::moveFileInDirectoryFromTrash()
1049 {
1050     const QString destPath = otherTmpDir() + QStringLiteral("testfile_restored");
1051     copyFromTrash(QStringLiteral("trashDirFromHome"), destPath, QStringLiteral("testfile"));
1052     QVERIFY(QFileInfo(destPath).isFile());
1053     QCOMPARE(QFileInfo(destPath).size(), 12);
1054 }
1055 
1056 void TestTrash::moveDirectoryFromTrash()
1057 {
1058     const QString destPath = otherTmpDir() + QStringLiteral("trashDirFromHome_restored");
1059     moveFromTrash(QStringLiteral("trashDirFromHome"), destPath);
1060     QVERIFY(QFileInfo(destPath).isDir());
1061     checkDirCacheValidity();
1062 
1063     // trash it again, we'll need it later
1064     QString dirName = QStringLiteral("trashDirFromHome");
1065     trashDirectory(homeTmpDir() + dirName, dirName);
1066 }
1067 
1068 void TestTrash::trashDirectoryOwnedByRoot()
1069 {
1070     QUrl u(QStringLiteral("file:///"));
1071     ;
1072     if (QFile::exists(QStringLiteral("/etc/cups"))) {
1073         u.setPath(QStringLiteral("/etc/cups"));
1074     } else if (QFile::exists(QStringLiteral("/boot"))) {
1075         u.setPath(QStringLiteral("/boot"));
1076     } else {
1077         u.setPath(QStringLiteral("/etc"));
1078     }
1079     const QString fileId = u.path();
1080     qDebug() << "fileId=" << fileId;
1081 
1082     if (geteuid() == 0 || QFileInfo(u.toLocalFile()).isWritable()) {
1083         QSKIP("Test must not be run by root.");
1084     }
1085 
1086     KIO::CopyJob *job = KIO::move(u, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo);
1087     job->setUiDelegate(nullptr); // no skip dialog, thanks
1088     bool ok = job->exec();
1089     QVERIFY(!ok);
1090     const int err = job->error();
1091     QVERIFY(err == KIO::ERR_ACCESS_DENIED || err == KIO::ERR_CANNOT_OPEN_FOR_READING);
1092 
1093     const QString infoPath(m_trashDir + QStringLiteral("/info/") + fileId + QStringLiteral(".trashinfo"));
1094     QVERIFY(!QFile::exists(infoPath));
1095 
1096     QFileInfo files(m_trashDir + QStringLiteral("/files/") + fileId);
1097     QVERIFY(!files.exists());
1098 
1099     QVERIFY(QFile::exists(u.path()));
1100 }
1101 
1102 void TestTrash::moveSymlinkFromTrash()
1103 {
1104     const QString destPath = otherTmpDir() + QStringLiteral("symlinkFromHome_restored");
1105     moveFromTrash(QStringLiteral("symlinkFromHome"), destPath);
1106     QVERIFY(QFileInfo(destPath).isSymLink());
1107 }
1108 
1109 void TestTrash::testMoveNonExistingFile()
1110 {
1111     const QUrl dest = QUrl::fromLocalFile(homeTmpDir() + QLatin1String("DoesNotExist"));
1112     KIO::Job *job = KIO::file_move(QUrl(QStringLiteral("trash:/0-DoesNotExist")), dest, -1, KIO::HideProgressInfo);
1113 
1114     QVERIFY(!job->exec());
1115     QCOMPARE(job->error(), KIO::ERR_DOES_NOT_EXIST);
1116     QCOMPARE(job->errorString(), QStringLiteral("The file or folder trash:/DoesNotExist does not exist."));
1117 }
1118 
1119 void TestTrash::getFile()
1120 {
1121     const QString fileId = QStringLiteral("fileFromHome (1)");
1122     const QUrl url = TrashImpl::makeURL(0, fileId, QString());
1123 
1124     QTemporaryFile tmpFile;
1125     QVERIFY(tmpFile.open());
1126     const QString tmpFilePath = tmpFile.fileName();
1127 
1128     KIO::Job *getJob = KIO::file_copy(url, QUrl::fromLocalFile(tmpFilePath), -1, KIO::Overwrite | KIO::HideProgressInfo);
1129     bool ok = getJob->exec();
1130     QVERIFY2(ok, qPrintable(getJob->errorString()));
1131     // Don't use tmpFile.close()+tmpFile.open() here, the size would still be 0 in the QTemporaryFile object
1132     // (due to the use of fstat on the old fd). Arguably a bug (I even have a testcase), but probably
1133     // not fixable without breaking the security of QTemporaryFile...
1134     QFile reader(tmpFilePath);
1135     QVERIFY(reader.open(QIODevice::ReadOnly));
1136     QByteArray str = reader.readAll();
1137     QCOMPARE(str, QByteArray("Hello world\n"));
1138 }
1139 
1140 void TestTrash::restoreFile()
1141 {
1142     const QString fileId = QStringLiteral("fileFromHome (1)");
1143     const QUrl url = TrashImpl::makeURL(0, fileId, QString());
1144     const QString infoFile(m_trashDir + QStringLiteral("/info/") + fileId + QStringLiteral(".trashinfo"));
1145     const QString filesItem(m_trashDir + QStringLiteral("/files/") + fileId);
1146 
1147     QVERIFY(QFile::exists(infoFile));
1148     QVERIFY(QFile::exists(filesItem));
1149 
1150     QByteArray packedArgs;
1151     QDataStream stream(&packedArgs, QIODevice::WriteOnly);
1152     stream << (int)3 << url;
1153     KIO::Job *job = KIO::special(url, packedArgs, KIO::HideProgressInfo);
1154     bool ok = job->exec();
1155     QVERIFY(ok);
1156 
1157     QVERIFY(!QFile::exists(infoFile));
1158     QVERIFY(!QFile::exists(filesItem));
1159 
1160     const QString destPath = homeTmpDir() + QStringLiteral("fileFromHome");
1161     QVERIFY(QFile::exists(destPath));
1162 }
1163 
1164 void TestTrash::restoreFileFromSubDir()
1165 {
1166     const QString fileId = QStringLiteral("trashDirFromHome (1)/testfile");
1167     QVERIFY(!QFile::exists(homeTmpDir() + QStringLiteral("trashDirFromHome (1)")));
1168 
1169     const QUrl url = TrashImpl::makeURL(0, fileId, QString());
1170     const QString infoFile(m_trashDir + QStringLiteral("/info/trashDirFromHome (1).trashinfo"));
1171     const QString filesItem(m_trashDir + QStringLiteral("/files/trashDirFromHome (1)/testfile"));
1172 
1173     QVERIFY(QFile::exists(infoFile));
1174     QVERIFY(QFile::exists(filesItem));
1175 
1176     QByteArray packedArgs;
1177     QDataStream stream(&packedArgs, QIODevice::WriteOnly);
1178     stream << (int)3 << url;
1179     KIO::Job *job = KIO::special(url, packedArgs, KIO::HideProgressInfo);
1180     bool ok = job->exec();
1181     QVERIFY(!ok);
1182     // dest dir doesn't exist -> error message
1183     QCOMPARE(job->error(), KIO::ERR_WORKER_DEFINED);
1184 
1185     // check that nothing happened
1186     QVERIFY(QFile::exists(infoFile));
1187     QVERIFY(QFile::exists(filesItem));
1188     QVERIFY(!QFile::exists(homeTmpDir() + QStringLiteral("trashDirFromHome (1)")));
1189 }
1190 
1191 void TestTrash::restoreFileToDeletedDirectory()
1192 {
1193     // Ensure we'll get "fileFromHome" as fileId
1194     removeFile(m_trashDir, QStringLiteral("/info/fileFromHome.trashinfo"));
1195     removeFile(m_trashDir, QStringLiteral("/files/fileFromHome"));
1196     trashFileFromHome();
1197     // Delete orig dir
1198     KIO::Job *delJob = KIO::del(QUrl::fromLocalFile(homeTmpDir()), KIO::HideProgressInfo);
1199     bool delOK = delJob->exec();
1200     QVERIFY(delOK);
1201 
1202     const QString fileId = QStringLiteral("fileFromHome");
1203     const QUrl url = TrashImpl::makeURL(0, fileId, QString());
1204     const QString infoFile(m_trashDir + QStringLiteral("/info/") + fileId + QStringLiteral(".trashinfo"));
1205     const QString filesItem(m_trashDir + QStringLiteral("/files/") + fileId);
1206 
1207     QVERIFY(QFile::exists(infoFile));
1208     QVERIFY(QFile::exists(filesItem));
1209 
1210     QByteArray packedArgs;
1211     QDataStream stream(&packedArgs, QIODevice::WriteOnly);
1212     stream << (int)3 << url;
1213     KIO::Job *job = KIO::special(url, packedArgs, KIO::HideProgressInfo);
1214     bool ok = job->exec();
1215     QVERIFY(!ok);
1216     // dest dir doesn't exist -> error message
1217     QCOMPARE(job->error(), KIO::ERR_WORKER_DEFINED);
1218 
1219     // check that nothing happened
1220     QVERIFY(QFile::exists(infoFile));
1221     QVERIFY(QFile::exists(filesItem));
1222 
1223     const QString destPath = homeTmpDir() + QStringLiteral("fileFromHome");
1224     QVERIFY(!QFile::exists(destPath));
1225 }
1226 
1227 void TestTrash::listRootDir()
1228 {
1229     m_entryCount = 0;
1230     m_listResult.clear();
1231     m_displayNameListResult.clear();
1232     KIO::ListJob *job = KIO::listDir(QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo);
1233     connect(job, &KIO::ListJob::entries, this, &TestTrash::slotEntries);
1234     bool ok = job->exec();
1235     QVERIFY(ok);
1236     qDebug() << "listDir done - m_entryCount=" << m_entryCount;
1237     QVERIFY(m_entryCount > 1);
1238 
1239     // qDebug() << m_listResult;
1240     // qDebug() << m_displayNameListResult;
1241     QCOMPARE(m_listResult.count(QStringLiteral(".")), 1); // found it, and only once
1242     QCOMPARE(m_displayNameListResult.count(QStringLiteral("fileFromHome")), 1);
1243     QCOMPARE(m_displayNameListResult.count(QStringLiteral("fileFromHome (1)")), 1);
1244 }
1245 
1246 void TestTrash::listRecursiveRootDir()
1247 {
1248     m_entryCount = 0;
1249     m_listResult.clear();
1250     m_displayNameListResult.clear();
1251     KIO::ListJob *job = KIO::listRecursive(QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo);
1252     connect(job, &KIO::ListJob::entries, this, &TestTrash::slotEntries);
1253     bool ok = job->exec();
1254     QVERIFY(ok);
1255     qDebug() << "listDir done - m_entryCount=" << m_entryCount;
1256     QVERIFY(m_entryCount > 1);
1257 
1258     qDebug() << m_listResult;
1259     qDebug() << m_displayNameListResult;
1260     QCOMPARE(m_listResult.count(QStringLiteral(".")), 1); // found it, and only once
1261     QCOMPARE(m_listResult.count(QStringLiteral("0-fileFromHome")), 1);
1262     QCOMPARE(m_listResult.count(QStringLiteral("0-fileFromHome (1)")), 1);
1263     QCOMPARE(m_listResult.count(QStringLiteral("0-trashDirFromHome/testfile")), 1);
1264     QCOMPARE(m_listResult.count(QStringLiteral("0-readonly/readonly_subdir/testfile_in_subdir")), 1);
1265     QCOMPARE(m_listResult.count(QStringLiteral("0-subDirBrokenSymlink/link")), 1);
1266     QCOMPARE(m_displayNameListResult.count(QStringLiteral("fileFromHome")), 1);
1267     QCOMPARE(m_displayNameListResult.count(QStringLiteral("fileFromHome (1)")), 1);
1268     QCOMPARE(m_displayNameListResult.count(QStringLiteral("trashDirFromHome/testfile")), 1);
1269     QCOMPARE(m_displayNameListResult.count(QStringLiteral("readonly/readonly_subdir/testfile_in_subdir")), 1);
1270     QCOMPARE(m_displayNameListResult.count(QStringLiteral("subDirBrokenSymlink/link")), 1);
1271 }
1272 
1273 void TestTrash::listSubDir()
1274 {
1275     m_entryCount = 0;
1276     m_listResult.clear();
1277     m_displayNameListResult.clear();
1278     KIO::ListJob *job = KIO::listDir(QUrl(QStringLiteral("trash:/0-trashDirFromHome")), KIO::HideProgressInfo);
1279     connect(job, &KIO::ListJob::entries, this, &TestTrash::slotEntries);
1280     bool ok = job->exec();
1281     QVERIFY(ok);
1282     qDebug() << "listDir done - m_entryCount=" << m_entryCount;
1283     QCOMPARE(m_entryCount, 3);
1284 
1285     // qDebug() << m_listResult;
1286     // qDebug() << m_displayNameListResult;
1287     QCOMPARE(m_listResult.count(QStringLiteral(".")), 1); // found it, and only once
1288     QCOMPARE(m_listResult.count(QStringLiteral("testfile")), 1); // found it, and only once
1289     QCOMPARE(m_listResult.count(QStringLiteral("subdir")), 1);
1290     QCOMPARE(m_displayNameListResult.count(QStringLiteral("testfile")), 1);
1291     QCOMPARE(m_displayNameListResult.count(QStringLiteral("subdir")), 1);
1292 }
1293 
1294 void TestTrash::slotEntries(KIO::Job *, const KIO::UDSEntryList &lst)
1295 {
1296     for (const KIO::UDSEntry &entry : lst) {
1297         QString name = entry.stringValue(KIO::UDSEntry::UDS_NAME);
1298         QString displayName = entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME);
1299         QUrl url(entry.stringValue(KIO::UDSEntry::UDS_URL));
1300         qDebug() << "name" << name << "displayName" << displayName << " UDS_URL=" << url;
1301         if (!url.isEmpty()) {
1302             QCOMPARE(url.scheme(), QStringLiteral("trash"));
1303         }
1304         m_listResult << name;
1305         m_displayNameListResult << displayName;
1306     }
1307     m_entryCount += lst.count();
1308 }
1309 
1310 void TestTrash::emptyTrash()
1311 {
1312     // ## Even though we use a custom XDG_DATA_HOME value, emptying the
1313     // trash would still empty the other trash directories in other partitions.
1314     // So we can't activate this test by default.
1315 #if 0
1316 
1317     // To make this test standalone
1318     trashFileFromHome();
1319 
1320     // #167051: orphaned files
1321     createTestFile(m_trashDir + "/files/testfile_nometadata");
1322 
1323     QByteArray packedArgs;
1324     QDataStream stream(&packedArgs, QIODevice::WriteOnly);
1325     stream << (int)1;
1326     KIO::Job *job = KIO::special(QUrl("trash:/"), packedArgs, KIO::HideProgressInfo);
1327     bool ok = job->exec();
1328     QVERIFY(ok);
1329 
1330     KConfig cfg("trashrc", KConfig::SimpleConfig);
1331     QVERIFY(cfg.hasGroup("Status"));
1332     QVERIFY(cfg.group("Status").readEntry("Empty", false) == true);
1333 
1334     QVERIFY(!QFile::exists(m_trashDir + "/files/fileFromHome"));
1335     QVERIFY(!QFile::exists(m_trashDir + "/files/readonly"));
1336     QVERIFY(!QFile::exists(m_trashDir + "/info/readonly.trashinfo"));
1337     QVERIFY(QDir(m_trashDir + "/info").entryList(QDir::NoDotAndDotDot | QDir::AllEntries).isEmpty());
1338     QVERIFY(QDir(m_trashDir + "/files").entryList(QDir::NoDotAndDotDot | QDir::AllEntries).isEmpty());
1339 
1340 #else
1341     qDebug() << " : SKIPPED";
1342 #endif
1343 }
1344 
1345 static bool isTrashEmpty()
1346 {
1347     KConfig cfg(QStringLiteral("trashrc"), KConfig::SimpleConfig);
1348     const KConfigGroup group = cfg.group(QStringLiteral("Status"));
1349     return group.readEntry("Empty", true);
1350 }
1351 
1352 void TestTrash::testEmptyTrashSize()
1353 {
1354     KIO::DirectorySizeJob *job = KIO::directorySize(QUrl(QStringLiteral("trash:/")));
1355     QVERIFY(job->exec());
1356     if (isTrashEmpty()) {
1357         QCOMPARE(job->totalSize(), 0ULL);
1358     } else {
1359         QVERIFY(job->totalSize() < 1000000000 /*1GB*/); // #157023
1360     }
1361 }
1362 
1363 static void checkIcon(const QUrl &url, const QString &expectedIcon)
1364 {
1365     QString icon = KIO::iconNameForUrl(url); // #100321
1366     QCOMPARE(icon, expectedIcon);
1367 }
1368 
1369 void TestTrash::testIcons()
1370 {
1371     // The JSON file says "user-trash-full" in all cases, whether the trash is full or not
1372     QCOMPARE(KProtocolInfo::icon(QStringLiteral("trash")), QStringLiteral("user-trash-full")); // #100321
1373 
1374     if (isTrashEmpty()) {
1375         checkIcon(QUrl(QStringLiteral("trash:/")), QStringLiteral("user-trash"));
1376     } else {
1377         checkIcon(QUrl(QStringLiteral("trash:/")), QStringLiteral("user-trash-full"));
1378     }
1379 
1380     checkIcon(QUrl(QStringLiteral("trash:/foo/")), QStringLiteral("inode-directory"));
1381 }
1382 
1383 QTEST_GUILESS_MAIN(TestTrash)
1384 
1385 #include "moc_testtrash.cpp"