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"