File indexing completed on 2025-03-16 03:43:38
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"