File indexing completed on 2024-05-19 15:15:35

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