File indexing completed on 2024-04-14 03:52:40

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2004-2006 David Faure <faure@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "jobtest.h"
0009 #include "mockcoredelegateextensions.h"
0010 
0011 #include "kio/job.h"
0012 #include "kiotesthelper.h" // createTestFile etc.
0013 #include <kio/chmodjob.h>
0014 #include <kio/copyjob.h>
0015 #include <kio/deletejob.h>
0016 #include <kio/directorysizejob.h>
0017 #include <kio/filecopyjob.h>
0018 #include <kio/listjob.h>
0019 #include <kio/mimetypejob.h>
0020 #include <kio/statjob.h>
0021 #include <kio/storedtransferjob.h>
0022 #include <kmountpoint.h>
0023 #include <kprotocolinfo.h>
0024 
0025 #include <KJobUiDelegate>
0026 #include <KLocalizedString>
0027 
0028 #include <QBuffer>
0029 #include <QDebug>
0030 #include <QDir>
0031 #include <QElapsedTimer>
0032 #include <QEventLoop>
0033 #include <QFileInfo>
0034 #include <QHash>
0035 #include <QPointer>
0036 #include <QProcess>
0037 #include <QSignalSpy>
0038 #include <QTemporaryFile>
0039 #include <QTest>
0040 #include <QTimer>
0041 #include <QUrl>
0042 #include <QVariant>
0043 
0044 #ifndef Q_OS_WIN
0045 #include <unistd.h> // for readlink
0046 #endif
0047 
0048 QTEST_MAIN(JobTest)
0049 
0050 // The code comes partly from kdebase/kioslave/trash/testtrash.cpp
0051 
0052 static QString otherTmpDir()
0053 {
0054 #ifdef Q_OS_WIN
0055     return QDir::tempPath() + "/jobtest/";
0056 #else
0057     // This one needs to be on another partition, but we can't guarantee that it is
0058     // On CI, it typically isn't...
0059     return QStringLiteral("/tmp/jobtest/");
0060 #endif
0061 }
0062 
0063 static bool otherTmpDirIsOnSamePartition() // true on CI because it's a LXC container
0064 {
0065     KMountPoint::Ptr srcMountPoint = KMountPoint::currentMountPoints().findByPath(homeTmpDir());
0066     KMountPoint::Ptr destMountPoint = KMountPoint::currentMountPoints().findByPath(otherTmpDir());
0067     Q_ASSERT(srcMountPoint);
0068     Q_ASSERT(destMountPoint);
0069     return srcMountPoint->mountedFrom() == destMountPoint->mountedFrom();
0070 }
0071 
0072 void JobTest::initTestCase()
0073 {
0074     QStandardPaths::setTestModeEnabled(true);
0075     QCoreApplication::instance()->setApplicationName("kio/jobtest"); // testing for #357499
0076 
0077     // to make sure io is not too fast
0078     qputenv("KIOWORKER_FILE_ENABLE_TESTMODE", "1");
0079 
0080     s_referenceTimeStamp = QDateTime::currentDateTime().addSecs(-30); // 30 seconds ago
0081 
0082     // Start with a clean base dir
0083     cleanupTestCase();
0084     homeTmpDir(); // create it
0085     if (!QFile::exists(otherTmpDir())) {
0086         bool ok = QDir().mkdir(otherTmpDir());
0087         if (!ok) {
0088             qFatal("couldn't create %s", qPrintable(otherTmpDir()));
0089         }
0090     }
0091 
0092     /*****
0093      * Set platform xattr related commands.
0094      * Linux commands: setfattr, getfattr
0095      * BSD commands: setextattr, getextattr
0096      * MacOS commands: xattr -w, xattr -p
0097      ****/
0098     m_getXattrCmd = QStandardPaths::findExecutable("getfattr");
0099     if (m_getXattrCmd.endsWith("getfattr")) {
0100         m_setXattrCmd = QStandardPaths::findExecutable("setfattr");
0101         m_setXattrFormatArgs = [](const QString &attrName, const QString &value, const QString &fileName) {
0102             return QStringList{QLatin1String("-n"), attrName, QLatin1String("-v"), value, fileName};
0103         };
0104     } else {
0105         // On BSD there is lsextattr to list all xattrs and getextattr to get a value
0106         // for specific xattr. For test purposes lsextattr is more suitable to be used
0107         // as m_getXattrCmd, so search for it instead of getextattr.
0108         m_getXattrCmd = QStandardPaths::findExecutable("lsextattr");
0109         if (m_getXattrCmd.endsWith("lsextattr")) {
0110             m_setXattrCmd = QStandardPaths::findExecutable("setextattr");
0111             m_setXattrFormatArgs = [](const QString &attrName, const QString &value, const QString &fileName) {
0112                 return QStringList{QLatin1String("user"), attrName, value, fileName};
0113             };
0114         } else {
0115             m_getXattrCmd = QStandardPaths::findExecutable("xattr");
0116             m_setXattrFormatArgs = [](const QString &attrName, const QString &value, const QString &fileName) {
0117                 return QStringList{QLatin1String("-w"), attrName, value, fileName};
0118             };
0119             if (!m_getXattrCmd.endsWith("xattr")) {
0120                 qWarning() << "Neither getfattr, getextattr nor xattr was found.";
0121             }
0122         }
0123     }
0124 
0125     qRegisterMetaType<KJob *>("KJob*");
0126     qRegisterMetaType<KIO::Job *>("KIO::Job*");
0127     qRegisterMetaType<QDateTime>("QDateTime");
0128 }
0129 
0130 void JobTest::cleanupTestCase()
0131 {
0132     QDir(homeTmpDir()).removeRecursively();
0133     QDir(otherTmpDir()).removeRecursively();
0134 }
0135 
0136 struct ScopedCleaner {
0137     using Func = std::function<void()>;
0138     explicit ScopedCleaner(Func f)
0139         : m_f(std::move(f))
0140     {
0141     }
0142     ~ScopedCleaner()
0143     {
0144         m_f();
0145     }
0146 
0147 private:
0148     const Func m_f;
0149 };
0150 
0151 void JobTest::enterLoop()
0152 {
0153     QEventLoop eventLoop;
0154     connect(this, &JobTest::exitLoop, &eventLoop, &QEventLoop::quit);
0155     eventLoop.exec(QEventLoop::ExcludeUserInputEvents);
0156 }
0157 
0158 void JobTest::storedGet()
0159 {
0160     // qDebug();
0161     const QString filePath = homeTmpDir() + "fileFromHome";
0162     createTestFile(filePath);
0163     QUrl u = QUrl::fromLocalFile(filePath);
0164     m_result = -1;
0165 
0166     KIO::StoredTransferJob *job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo);
0167 
0168     QSignalSpy spyPercent(job, &KJob::percentChanged);
0169     QVERIFY(spyPercent.isValid());
0170     job->setUiDelegate(nullptr);
0171     connect(job, &KJob::result, this, &JobTest::slotGetResult);
0172     enterLoop();
0173     QCOMPARE(m_result, 0); // no error
0174     QCOMPARE(m_data, QByteArray("Hello\0world", 11));
0175     QCOMPARE(m_data.size(), 11);
0176     QVERIFY(!spyPercent.isEmpty());
0177 }
0178 
0179 void JobTest::slotGetResult(KJob *job)
0180 {
0181     m_result = job->error();
0182     m_data = static_cast<KIO::StoredTransferJob *>(job)->data();
0183     Q_EMIT exitLoop();
0184 }
0185 
0186 void JobTest::put()
0187 {
0188     const QString filePath = homeTmpDir() + "fileFromHome";
0189     QUrl u = QUrl::fromLocalFile(filePath);
0190     KIO::TransferJob *job = KIO::put(u, 0600, KIO::Overwrite | KIO::HideProgressInfo);
0191     quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems
0192     QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago
0193     job->setModificationTime(mtime);
0194     job->setUiDelegate(nullptr);
0195     connect(job, &KJob::result, this, &JobTest::slotResult);
0196     connect(job, &KIO::TransferJob::dataReq, this, &JobTest::slotDataReq);
0197     m_result = -1;
0198     m_dataReqCount = 0;
0199     enterLoop();
0200     QVERIFY(m_result == 0); // no error
0201 
0202     QFileInfo fileInfo(filePath);
0203     QVERIFY(fileInfo.exists());
0204     QCOMPARE(fileInfo.size(), 30LL); // "This is a test for KIO::put()\n"
0205     QCOMPARE(fileInfo.permissions(), QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser);
0206     QCOMPARE(fileInfo.lastModified(), mtime);
0207 }
0208 
0209 void JobTest::putPermissionKept()
0210 {
0211     const QString filePath = homeTmpDir() + "fileFromHome";
0212     QVERIFY2(::chmod(filePath.toUtf8(), 0644) == 0, strerror(errno));
0213 
0214     QUrl u = QUrl::fromLocalFile(filePath);
0215     KIO::TransferJob *job = KIO::put(u, -1, KIO::Overwrite | KIO::HideProgressInfo);
0216     quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems
0217     QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago
0218     job->setModificationTime(mtime);
0219     job->setUiDelegate(nullptr);
0220     connect(job, &KJob::result, this, &JobTest::slotResult);
0221     connect(job, &KIO::TransferJob::dataReq, this, &JobTest::slotDataReq);
0222     m_result = -1;
0223     m_dataReqCount = 0;
0224     enterLoop();
0225     QVERIFY(m_result == 0); // no error
0226 
0227     QFileInfo fileInfo(filePath);
0228     QVERIFY(fileInfo.exists());
0229     QCOMPARE(fileInfo.size(), 30LL); // "This is a test for KIO::put()\n"
0230     QCOMPARE(fileInfo.permissions(), QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser | QFile::ReadGroup | QFile::ReadOther /* 644 */);
0231     QCOMPARE(fileInfo.lastModified(), mtime);
0232 }
0233 
0234 void JobTest::slotDataReq(KIO::Job *, QByteArray &data)
0235 {
0236     // Really not the way you'd write a slotDataReq usually :)
0237     switch (m_dataReqCount++) {
0238     case 0:
0239         data = "This is a test for ";
0240         break;
0241     case 1:
0242         data = "KIO::put()\n";
0243         break;
0244     case 2:
0245         data = QByteArray();
0246         break;
0247     }
0248 }
0249 
0250 void JobTest::slotResult(KJob *job)
0251 {
0252     m_result = job->error();
0253     Q_EMIT exitLoop();
0254 }
0255 
0256 void JobTest::storedPut()
0257 {
0258     const QString filePath = homeTmpDir() + "fileFromHome";
0259     QUrl u = QUrl::fromLocalFile(filePath);
0260     QByteArray putData = "This is the put data";
0261     KIO::TransferJob *job = KIO::storedPut(putData, u, 0600, KIO::Overwrite | KIO::HideProgressInfo);
0262 
0263     QSignalSpy spyPercent(job, &KJob::percentChanged);
0264     QVERIFY(spyPercent.isValid());
0265     quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems
0266     QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago
0267     job->setModificationTime(mtime);
0268     job->setUiDelegate(nullptr);
0269     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0270     QFileInfo fileInfo(filePath);
0271     QVERIFY(fileInfo.exists());
0272     QCOMPARE(fileInfo.size(), (long long)putData.size());
0273     QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser));
0274     QCOMPARE(fileInfo.lastModified(), mtime);
0275     QVERIFY(!spyPercent.isEmpty());
0276 }
0277 
0278 void JobTest::storedPutIODevice()
0279 {
0280     const QString filePath = homeTmpDir() + "fileFromHome";
0281     QBuffer putData;
0282     putData.setData("This is the put data");
0283     QVERIFY(putData.open(QIODevice::ReadOnly));
0284     KIO::TransferJob *job = KIO::storedPut(&putData, QUrl::fromLocalFile(filePath), 0600, KIO::Overwrite | KIO::HideProgressInfo);
0285 
0286     QSignalSpy spyPercent(job, &KJob::percentChanged);
0287     QVERIFY(spyPercent.isValid());
0288     quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems
0289     QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago
0290     job->setModificationTime(mtime);
0291     job->setUiDelegate(nullptr);
0292     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0293     QFileInfo fileInfo(filePath);
0294     QVERIFY(fileInfo.exists());
0295     QCOMPARE(fileInfo.size(), (long long)putData.size());
0296     QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser));
0297     QCOMPARE(fileInfo.lastModified(), mtime);
0298     QVERIFY(!spyPercent.isEmpty());
0299 }
0300 
0301 void JobTest::storedPutIODeviceFile()
0302 {
0303     // Given a source file and a destination file
0304     const QString src = homeTmpDir() + "fileFromHome";
0305     createTestFile(src);
0306     QVERIFY(QFile::exists(src));
0307     QFile srcFile(src);
0308     QVERIFY(srcFile.open(QIODevice::ReadOnly));
0309     const QString dest = homeTmpDir() + "fileFromHome_copied";
0310     QFile::remove(dest);
0311     const QUrl destUrl = QUrl::fromLocalFile(dest);
0312 
0313     // When using storedPut with the QFile as argument
0314     KIO::StoredTransferJob *job = KIO::storedPut(&srcFile, destUrl, 0600, KIO::Overwrite | KIO::HideProgressInfo);
0315 
0316     // Then the copy should succeed and the dest file exist
0317     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0318     QVERIFY(QFile::exists(dest));
0319     QCOMPARE(QFileInfo(src).size(), QFileInfo(dest).size());
0320     QFile::remove(dest);
0321 }
0322 
0323 void JobTest::storedPutIODeviceTempFile()
0324 {
0325     // Create a temp file in the current dir.
0326     QTemporaryFile tempFile(QStringLiteral("jobtest-tmp"));
0327     QVERIFY(tempFile.open());
0328 
0329     // Write something into the file.
0330     QTextStream stream(&tempFile);
0331     stream << QStringLiteral("This is the put data");
0332     stream.flush();
0333     QVERIFY(QFileInfo(tempFile).size() > 0);
0334 
0335     const QString dest = homeTmpDir() + QLatin1String("tmpfile-dest");
0336     const QUrl destUrl = QUrl::fromLocalFile(dest);
0337 
0338     // QTemporaryFiles are open in ReadWrite mode,
0339     // so we don't need to close and reopen,
0340     // but we need to rewind to the beginning.
0341     tempFile.seek(0);
0342     auto job = KIO::storedPut(&tempFile, destUrl, -1);
0343 
0344     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0345     QVERIFY(QFileInfo::exists(dest));
0346     QCOMPARE(QFileInfo(dest).size(), QFileInfo(tempFile).size());
0347     QVERIFY(QFile::remove(dest));
0348 }
0349 
0350 void JobTest::storedPutIODeviceFastDevice()
0351 {
0352     const QString filePath = homeTmpDir() + "fileFromHome";
0353     const QUrl u = QUrl::fromLocalFile(filePath);
0354     const QByteArray putDataContents = "This is the put data";
0355     QBuffer putDataBuffer;
0356     QVERIFY(putDataBuffer.open(QIODevice::ReadWrite));
0357 
0358     KIO::StoredTransferJob *job = KIO::storedPut(&putDataBuffer, u, 0600, KIO::Overwrite | KIO::HideProgressInfo);
0359 
0360     QSignalSpy spyPercent(job, &KJob::percentChanged);
0361     QVERIFY(spyPercent.isValid());
0362     quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems
0363     QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago
0364     job->setModificationTime(mtime);
0365     job->setTotalSize(putDataContents.size());
0366     job->setUiDelegate(nullptr);
0367     job->setAsyncDataEnabled(true);
0368 
0369     // Emit the readChannelFinished even before the job has had time to start
0370     const auto pos = putDataBuffer.pos();
0371     int size = putDataBuffer.write(putDataContents);
0372     putDataBuffer.seek(pos);
0373     Q_EMIT putDataBuffer.readChannelFinished();
0374 
0375     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0376     QCOMPARE(size, putDataContents.size());
0377     QCOMPARE(putDataBuffer.bytesAvailable(), 0);
0378 
0379     QFileInfo fileInfo(filePath);
0380     QVERIFY(fileInfo.exists());
0381     QCOMPARE(fileInfo.size(), (long long)putDataContents.size());
0382     QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser));
0383     QCOMPARE(fileInfo.lastModified(), mtime);
0384     QVERIFY(!spyPercent.isEmpty());
0385 }
0386 
0387 void JobTest::storedPutIODeviceSlowDevice()
0388 {
0389     const QString filePath = homeTmpDir() + "fileFromHome";
0390     const QUrl u = QUrl::fromLocalFile(filePath);
0391     const QByteArray putDataContents = "This is the put data";
0392     QBuffer putDataBuffer;
0393     QVERIFY(putDataBuffer.open(QIODevice::ReadWrite));
0394 
0395     KIO::StoredTransferJob *job = KIO::storedPut(&putDataBuffer, u, 0600, KIO::Overwrite | KIO::HideProgressInfo);
0396 
0397     QSignalSpy spyPercent(job, &KJob::percentChanged);
0398     QVERIFY(spyPercent.isValid());
0399     quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems
0400     QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago
0401     job->setModificationTime(mtime);
0402     job->setTotalSize(putDataContents.size());
0403     job->setUiDelegate(nullptr);
0404     job->setAsyncDataEnabled(true);
0405 
0406     int size = 0;
0407     const auto writeOnce = [&putDataBuffer, &size, putDataContents]() {
0408         const auto pos = putDataBuffer.pos();
0409         size += putDataBuffer.write(putDataContents);
0410         putDataBuffer.seek(pos);
0411         //         qDebug() << "written" << size;
0412     };
0413 
0414     QTimer::singleShot(200, this, writeOnce);
0415     QTimer::singleShot(400, this, writeOnce);
0416     // Simulate the transfer is done
0417     QTimer::singleShot(450, this, [&putDataBuffer]() {
0418         Q_EMIT putDataBuffer.readChannelFinished();
0419     });
0420 
0421     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0422     QCOMPARE(size, putDataContents.size() * 2);
0423     QCOMPARE(putDataBuffer.bytesAvailable(), 0);
0424 
0425     QFileInfo fileInfo(filePath);
0426     QVERIFY(fileInfo.exists());
0427     QCOMPARE(fileInfo.size(), (long long)putDataContents.size() * 2);
0428     QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser));
0429     QCOMPARE(fileInfo.lastModified(), mtime);
0430     QVERIFY(!spyPercent.isEmpty());
0431 }
0432 
0433 void JobTest::storedPutIODeviceSlowDeviceBigChunk()
0434 {
0435     const QString filePath = homeTmpDir() + "fileFromHome";
0436     const QUrl u = QUrl::fromLocalFile(filePath);
0437     const QByteArray putDataContents(300000, 'K'); // Make sure the 300000 is bigger than MAX_READ_BUF_SIZE
0438     QBuffer putDataBuffer;
0439     QVERIFY(putDataBuffer.open(QIODevice::ReadWrite));
0440 
0441     KIO::StoredTransferJob *job = KIO::storedPut(&putDataBuffer, u, 0600, KIO::Overwrite | KIO::HideProgressInfo);
0442 
0443     QSignalSpy spyPercent(job, &KJob::percentChanged);
0444     QVERIFY(spyPercent.isValid());
0445     quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems
0446     QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago
0447     job->setModificationTime(mtime);
0448     job->setTotalSize(putDataContents.size());
0449     job->setUiDelegate(nullptr);
0450     job->setAsyncDataEnabled(true);
0451 
0452     int size = 0;
0453     const auto writeOnce = [&putDataBuffer, &size, putDataContents]() {
0454         const auto pos = putDataBuffer.pos();
0455         size += putDataBuffer.write(putDataContents);
0456         putDataBuffer.seek(pos);
0457         //         qDebug() << "written" << size;
0458     };
0459 
0460     QTimer::singleShot(200, this, writeOnce);
0461     // Simulate the transfer is done
0462     QTimer::singleShot(450, this, [&putDataBuffer]() {
0463         Q_EMIT putDataBuffer.readChannelFinished();
0464     });
0465 
0466     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0467     QCOMPARE(size, putDataContents.size());
0468     QCOMPARE(putDataBuffer.bytesAvailable(), 0);
0469 
0470     QFileInfo fileInfo(filePath);
0471     QVERIFY(fileInfo.exists());
0472     QCOMPARE(fileInfo.size(), (long long)putDataContents.size());
0473     QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser));
0474     QCOMPARE(fileInfo.lastModified(), mtime);
0475     QVERIFY(!spyPercent.isEmpty());
0476 }
0477 
0478 void JobTest::asyncStoredPutReadyReadAfterFinish()
0479 {
0480     const QString filePath = homeTmpDir() + "fileFromHome";
0481     const QUrl u = QUrl::fromLocalFile(filePath);
0482 
0483     QBuffer putDataBuffer;
0484     QVERIFY(putDataBuffer.open(QIODevice::ReadWrite));
0485 
0486     KIO::StoredTransferJob *job = KIO::storedPut(&putDataBuffer, u, 0600, KIO::Overwrite | KIO::HideProgressInfo);
0487     job->setAsyncDataEnabled(true);
0488 
0489     bool jobFinished = false;
0490 
0491     connect(job, &KJob::finished, this, [&jobFinished, &putDataBuffer] {
0492         putDataBuffer.readyRead();
0493         jobFinished = true;
0494     });
0495 
0496     QTimer::singleShot(200, this, [job]() {
0497         job->kill();
0498     });
0499 
0500     QTRY_VERIFY(jobFinished);
0501 }
0502 
0503 static QHash<QString, QString> getSampleXattrs()
0504 {
0505     QHash<QString, QString> attrs;
0506     attrs["user.name with space"] = "value with spaces";
0507     attrs["user.baloo.rating"] = "1";
0508     attrs["user.fnewLine"] = "line1\\nline2";
0509     attrs["user.flistNull"] = "item1\\0item2";
0510     attrs["user.fattr.with.a.lot.of.namespaces"] = "true";
0511     attrs["user.fempty"] = "";
0512     return attrs;
0513 }
0514 
0515 bool JobTest::checkXattrFsSupport(const QString &dir)
0516 {
0517     const QString writeTest = dir + "/fsXattrTestFile";
0518     createTestFile(writeTest);
0519     bool ret = setXattr(writeTest);
0520     QFile::remove(writeTest);
0521     return ret;
0522 }
0523 
0524 bool JobTest::setXattr(const QString &dest)
0525 {
0526     QProcess xattrWriter;
0527     xattrWriter.setProcessChannelMode(QProcess::MergedChannels);
0528 
0529     QHash<QString, QString> attrs = getSampleXattrs();
0530     QHashIterator<QString, QString> i(attrs);
0531     while (i.hasNext()) {
0532         i.next();
0533         QStringList arguments = m_setXattrFormatArgs(i.key(), i.value(), dest);
0534         xattrWriter.start(m_setXattrCmd, arguments);
0535         xattrWriter.waitForStarted();
0536         xattrWriter.waitForFinished(-1);
0537         if (xattrWriter.exitStatus() != QProcess::NormalExit) {
0538             return false;
0539         }
0540         QList<QByteArray> resultdest = xattrWriter.readAllStandardOutput().split('\n');
0541         if (!resultdest[0].isEmpty()) {
0542             qWarning() << "Error writing user xattr. Xattr copy tests will be disabled.";
0543             qDebug() << resultdest;
0544             return false;
0545         }
0546     }
0547 
0548     return true;
0549 }
0550 
0551 QList<QByteArray> JobTest::readXattr(const QString &src)
0552 {
0553     QProcess xattrReader;
0554     xattrReader.setProcessChannelMode(QProcess::MergedChannels);
0555 
0556     QStringList arguments;
0557     char outputSeparator = '\n';
0558     // Linux
0559     if (m_getXattrCmd.endsWith("getfattr")) {
0560         arguments = QStringList{"-d", src};
0561     }
0562     // BSD
0563     else if (m_getXattrCmd.endsWith("lsextattr")) {
0564         arguments = QStringList{"-q", "user", src};
0565         outputSeparator = '\t';
0566     }
0567     // MacOS
0568     else {
0569         arguments = QStringList{"-l", src};
0570     }
0571 
0572     xattrReader.start(m_getXattrCmd, arguments);
0573     xattrReader.waitForFinished();
0574     QList<QByteArray> result = xattrReader.readAllStandardOutput().split(outputSeparator);
0575     if (m_getXattrCmd.endsWith("getfattr")) {
0576         if (result.size() > 1) {
0577             // Line 1 is the file name
0578             result.removeAt(1);
0579         }
0580     } else if (m_getXattrCmd.endsWith("lsextattr")) {
0581         // cut off trailing \n
0582         result.last().chop(1);
0583         // lsextattr does not sort its output
0584         std::sort(result.begin(), result.end());
0585     }
0586 
0587     return result;
0588 }
0589 
0590 void JobTest::compareXattr(const QString &src, const QString &dest)
0591 {
0592     auto srcAttrs = readXattr(src);
0593     auto dstAttrs = readXattr(dest);
0594     QCOMPARE(dstAttrs, srcAttrs);
0595 }
0596 
0597 void JobTest::copyLocalFile(const QString &src, const QString &dest)
0598 {
0599     const QUrl u = QUrl::fromLocalFile(src);
0600     const QUrl d = QUrl::fromLocalFile(dest);
0601 
0602     const int perms = 0666;
0603     // copy the file with file_copy
0604     KIO::Job *job = KIO::file_copy(u, d, perms, KIO::HideProgressInfo);
0605     job->setUiDelegate(nullptr);
0606     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0607     QVERIFY(QFile::exists(dest));
0608     QVERIFY(QFile::exists(src)); // still there
0609     QCOMPARE(int(QFileInfo(dest).permissions()), int(0x6666));
0610     compareXattr(src, dest);
0611 
0612     {
0613         // check that the timestamp is the same (#24443)
0614         // Note: this only works because of copy() in kio_file.
0615         // The datapump solution ignores mtime, the app has to call FileCopyJob::setModificationTime()
0616         QFileInfo srcInfo(src);
0617         QFileInfo destInfo(dest);
0618 #ifdef Q_OS_WIN
0619         // win32 time may differs in msec part
0620         QCOMPARE(srcInfo.lastModified().toString("dd.MM.yyyy hh:mm"), destInfo.lastModified().toString("dd.MM.yyyy hh:mm"));
0621 #else
0622         QCOMPARE(srcInfo.lastModified(), destInfo.lastModified());
0623 #endif
0624     }
0625 
0626     // cleanup and retry with KIO::copy()
0627     QFile::remove(dest);
0628     auto *copyjob = KIO::copy(u, d, KIO::HideProgressInfo);
0629     QSignalSpy spyCopyingDone(copyjob, &KIO::CopyJob::copyingDone);
0630     copyjob->setUiDelegate(nullptr);
0631     copyjob->setUiDelegateExtension(nullptr);
0632     QVERIFY2(copyjob->exec(), qPrintable(copyjob->errorString()));
0633     QVERIFY(QFile::exists(dest));
0634     QVERIFY(QFile::exists(src)); // still there
0635     compareXattr(src, dest);
0636     {
0637         // check that the timestamp is the same (#24443)
0638         QFileInfo srcInfo(src);
0639         QFileInfo destInfo(dest);
0640 #ifdef Q_OS_WIN
0641         // win32 time may differs in msec part
0642         QCOMPARE(srcInfo.lastModified().toString("dd.MM.yyyy hh:mm"), destInfo.lastModified().toString("dd.MM.yyyy hh:mm"));
0643 #else
0644         QCOMPARE(srcInfo.lastModified(), destInfo.lastModified());
0645 #endif
0646     }
0647     QCOMPARE(spyCopyingDone.count(), 1);
0648 
0649     QCOMPARE(copyjob->totalAmount(KJob::Files), 1);
0650     QCOMPARE(copyjob->totalAmount(KJob::Directories), 0);
0651     QCOMPARE(copyjob->processedAmount(KJob::Files), 1);
0652     QCOMPARE(copyjob->processedAmount(KJob::Directories), 0);
0653     QCOMPARE(copyjob->percent(), 100);
0654 
0655     // cleanup and retry with KIO::copyAs()
0656     QFile::remove(dest);
0657     job = KIO::copyAs(u, d, KIO::HideProgressInfo);
0658     job->setUiDelegate(nullptr);
0659     job->setUiDelegateExtension(nullptr);
0660     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0661     QVERIFY(QFile::exists(dest));
0662     QVERIFY(QFile::exists(src)); // still there
0663     compareXattr(src, dest);
0664 
0665     // Do it again, with Overwrite.
0666     job = KIO::copyAs(u, d, KIO::Overwrite | KIO::HideProgressInfo);
0667     job->setUiDelegate(nullptr);
0668     job->setUiDelegateExtension(nullptr);
0669     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0670     QVERIFY(QFile::exists(dest));
0671     QVERIFY(QFile::exists(src)); // still there
0672     compareXattr(src, dest);
0673 
0674     // Do it again, without Overwrite (should fail).
0675     job = KIO::copyAs(u, d, KIO::HideProgressInfo);
0676     job->setUiDelegate(nullptr);
0677     job->setUiDelegateExtension(nullptr);
0678     QVERIFY(!job->exec());
0679 
0680     // Clean up
0681     QFile::remove(src);
0682     QFile::remove(dest);
0683 }
0684 
0685 void JobTest::copyLocalDirectory(const QString &src, const QString &_dest, int flags)
0686 {
0687     QVERIFY(QFileInfo(src).isDir());
0688     QVERIFY(QFileInfo(src + "/testfile").isFile());
0689     QUrl u = QUrl::fromLocalFile(src);
0690     QString dest(_dest);
0691     QUrl d = QUrl::fromLocalFile(dest);
0692     if (flags & AlreadyExists) {
0693         QVERIFY(QFile::exists(dest));
0694     } else {
0695         QVERIFY(!QFile::exists(dest));
0696     }
0697 
0698     KIO::Job *job = KIO::copy(u, d, KIO::HideProgressInfo);
0699     job->setUiDelegate(nullptr);
0700     job->setUiDelegateExtension(nullptr);
0701     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0702     QVERIFY(QFile::exists(dest));
0703     QVERIFY(QFileInfo(dest).isDir());
0704     QVERIFY(QFileInfo(dest + "/testfile").isFile());
0705     QVERIFY(QFile::exists(src)); // still there
0706 
0707     if (flags & AlreadyExists) {
0708         dest += '/' + u.fileName();
0709         // qDebug() << "Expecting dest=" << dest;
0710     }
0711 
0712     // CopyJob::setNextDirAttribute isn't implemented for Windows currently.
0713 #ifndef Q_OS_WIN
0714     {
0715         // Check that the timestamp is the same (#24443)
0716         QFileInfo srcInfo(src);
0717         QFileInfo destInfo(dest);
0718         QCOMPARE(srcInfo.lastModified(), destInfo.lastModified());
0719     }
0720 #endif
0721 
0722     QCOMPARE(job->totalAmount(KJob::Files), 2); // testfile and testlink
0723     QCOMPARE(job->totalAmount(KJob::Directories), 1);
0724     QCOMPARE(job->processedAmount(KJob::Files), 2);
0725     QCOMPARE(job->processedAmount(KJob::Directories), 1);
0726     QCOMPARE(job->percent(), 100);
0727 
0728     // Do it again, with Overwrite.
0729     // Use copyAs, we don't want a subdir inside d.
0730     job = KIO::copyAs(u, d, KIO::HideProgressInfo | KIO::Overwrite);
0731     job->setUiDelegate(nullptr);
0732     job->setUiDelegateExtension(nullptr);
0733     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0734 
0735     QCOMPARE(job->totalAmount(KJob::Files), 2); // testfile and testlink
0736     QCOMPARE(job->totalAmount(KJob::Directories), 1);
0737     QCOMPARE(job->processedAmount(KJob::Files), 2);
0738     QCOMPARE(job->processedAmount(KJob::Directories), 1);
0739     QCOMPARE(job->percent(), 100);
0740 
0741     // Do it again, without Overwrite (should fail).
0742     job = KIO::copyAs(u, d, KIO::HideProgressInfo);
0743     job->setUiDelegate(nullptr);
0744     job->setUiDelegateExtension(nullptr);
0745     QVERIFY(!job->exec());
0746 }
0747 
0748 #ifndef Q_OS_WIN
0749 static QString linkTarget(const QString &path)
0750 {
0751     // Use readlink on Unix because symLinkTarget turns relative targets into absolute (#352927)
0752     char linkTargetBuffer[4096];
0753     const int n = readlink(QFile::encodeName(path).constData(), linkTargetBuffer, sizeof(linkTargetBuffer) - 1);
0754     if (n != -1) {
0755         linkTargetBuffer[n] = 0;
0756     }
0757     return QFile::decodeName(linkTargetBuffer);
0758 }
0759 
0760 static void copyLocalSymlink(const QString &src, const QString &dest, const QString &expectedLinkTarget)
0761 {
0762     QT_STATBUF buf;
0763     QVERIFY(QT_LSTAT(QFile::encodeName(src).constData(), &buf) == 0);
0764     QUrl u = QUrl::fromLocalFile(src);
0765     QUrl d = QUrl::fromLocalFile(dest);
0766 
0767     // copy the symlink
0768     KIO::Job *job = KIO::copy(u, d, KIO::HideProgressInfo);
0769     job->setUiDelegate(nullptr);
0770     job->setUiDelegateExtension(nullptr);
0771     QVERIFY2(job->exec(), qPrintable(QString::number(job->error())));
0772     QVERIFY(QT_LSTAT(QFile::encodeName(dest).constData(), &buf) == 0); // dest exists
0773     QCOMPARE(linkTarget(dest), expectedLinkTarget);
0774 
0775     // cleanup
0776     QFile::remove(dest);
0777 }
0778 #endif
0779 
0780 void JobTest::copyFileToSamePartition()
0781 {
0782     const QString homeDir = homeTmpDir();
0783     const QString filePath = homeDir + "fileFromHome";
0784     const QString dest = homeDir + "fileFromHome_copied";
0785     createTestFile(filePath);
0786     if (checkXattrFsSupport(homeDir)) {
0787         setXattr(filePath);
0788     }
0789     copyLocalFile(filePath, dest);
0790 }
0791 
0792 void JobTest::copyDirectoryToSamePartition()
0793 {
0794     // qDebug();
0795     const QString src = homeTmpDir() + "dirFromHome";
0796     const QString dest = homeTmpDir() + "dirFromHome_copied";
0797     createTestDirectory(src);
0798     copyLocalDirectory(src, dest);
0799 }
0800 
0801 void JobTest::copyDirectoryToExistingDirectory()
0802 {
0803     // qDebug();
0804     // just the same as copyDirectoryToSamePartition, but this time dest exists.
0805     // So we get a subdir, "dirFromHome_copy/dirFromHome"
0806     const QString src = homeTmpDir() + "dirFromHome";
0807     const QString dest = homeTmpDir() + "dirFromHome_copied";
0808     createTestDirectory(src);
0809     createTestDirectory(dest);
0810     copyLocalDirectory(src, dest, AlreadyExists);
0811 }
0812 
0813 void JobTest::copyDirectoryToExistingSymlinkedDirectory()
0814 {
0815     // qDebug();
0816     // just the same as copyDirectoryToSamePartition, but this time dest is a symlink.
0817     // So we get a file in the symlink dir, "dirFromHome_symlink/dirFromHome" and
0818     // "dirFromHome_symOrigin/dirFromHome"
0819     const QString src = homeTmpDir() + "dirFromHome";
0820     const QString origSymlink = homeTmpDir() + "dirFromHome_symOrigin";
0821     const QString targetSymlink = homeTmpDir() + "dirFromHome_symlink";
0822     createTestDirectory(src);
0823     createTestDirectory(origSymlink);
0824 
0825     bool ok = KIOPrivate::createSymlink(origSymlink, targetSymlink);
0826     if (!ok) {
0827         qFatal("couldn't create symlink: %s", strerror(errno));
0828     }
0829     QVERIFY(QFileInfo(targetSymlink).isSymLink());
0830     QVERIFY(QFileInfo(targetSymlink).isDir());
0831 
0832     KIO::Job *job = KIO::copy(QUrl::fromLocalFile(src), QUrl::fromLocalFile(targetSymlink), KIO::HideProgressInfo);
0833     job->setUiDelegate(nullptr);
0834     job->setUiDelegateExtension(nullptr);
0835     QVERIFY2(job->exec(), qPrintable(job->errorString()));
0836     QVERIFY(QFile::exists(src)); // still there
0837 
0838     // file is visible in both places due to symlink
0839     QVERIFY(QFileInfo(origSymlink + "/dirFromHome").isDir());
0840     ;
0841     QVERIFY(QFileInfo(targetSymlink + "/dirFromHome").isDir());
0842     QVERIFY(QDir(origSymlink).removeRecursively());
0843     QVERIFY(QFile::remove(targetSymlink));
0844 }
0845 
0846 void JobTest::copyFileToOtherPartition()
0847 {
0848     // qDebug();
0849     const QString homeDir = homeTmpDir();
0850     const QString otherHomeDir = otherTmpDir();
0851     const QString filePath = homeDir + "fileFromHome";
0852     const QString dest = otherHomeDir + "fileFromHome_copied";
0853     bool canRead = checkXattrFsSupport(homeDir);
0854     bool canWrite = checkXattrFsSupport(otherHomeDir);
0855     createTestFile(filePath);
0856     if (canRead && canWrite) {
0857         setXattr(filePath);
0858     }
0859     copyLocalFile(filePath, dest);
0860 }
0861 
0862 // Same partition doesn't matter as much as copying to the same filesystem type
0863 // to be sure it supports the same permissions
0864 void JobTest::testCopyFilePermissionsToSamePartition()
0865 {
0866 #if defined(Q_OS_UNIX)
0867     const QString homeDir = homeTmpDir();
0868     const QString src = homeDir + "fileFromHome";
0869     const QUrl u = QUrl::fromLocalFile(src);
0870     createTestFile(src);
0871 
0872     const QByteArray src_c = QFile::encodeName(src).constData();
0873     QT_STATBUF src_buff;
0874     QCOMPARE(QT_LSTAT(src_c.constData(), &src_buff), 0); // Exists
0875     // Default system permissions for newly created files, umask et al.
0876     const mode_t systemDefaultPerms = src_buff.st_mode;
0877 
0878     const QString dest = homeDir + "fileFromHome_copied";
0879     const QUrl d = QUrl::fromLocalFile(dest);
0880 
0881     const QByteArray dest_c = QFile::encodeName(dest).constData();
0882     QT_STATBUF dest_buff;
0883 
0884     // Copy the file, with -1 permissions (i.e. don't touch dest permissions)
0885     auto copyStat = [&](const mode_t perms) {
0886         KIO::Job *job = KIO::file_copy(u, d, perms, KIO::HideProgressInfo);
0887         job->setUiDelegate(nullptr);
0888         QVERIFY2(job->exec(), qPrintable(job->errorString()));
0889         QCOMPARE(QT_LSTAT(dest_c.constData(), &dest_buff), 0);
0890     };
0891 
0892     copyStat(-1);
0893     // dest should have system default permissions
0894     QCOMPARE(dest_buff.st_mode, systemDefaultPerms);
0895 
0896     QVERIFY(QFile::remove(dest));
0897     // Change src permissions to non-default
0898     QCOMPARE(::chmod(src_c.constData(), S_IRUSR), 0);
0899     // Copy the file again, permissions -1 (i.e. don't touch dest permissions)
0900     copyStat(-1);
0901     // dest should have system default permissions, not src's ones
0902     QCOMPARE(dest_buff.st_mode, systemDefaultPerms);
0903 
0904     QVERIFY(QFile::remove(dest));
0905     // Copy the file again, explicitly setting the permissions to the src ones
0906     copyStat(src_buff.st_mode);
0907     // dest should have same permissions as src
0908     QCOMPARE(dest_buff.st_mode, dest_buff.st_mode);
0909 
0910     QVERIFY(QFile::remove(dest));
0911     // Copy the file again, explicitly setting some other permissions
0912     copyStat(S_IWUSR);
0913     // dest should have S_IWUSR
0914     QCOMPARE(dest_buff.st_mode & 0777, S_IWUSR);
0915 
0916     // Clean up, the weird permissions used above mess with the next
0917     // unit tests
0918     QVERIFY(QFile::remove(dest));
0919     QVERIFY(QFile::remove(src));
0920 #endif
0921 }
0922 
0923 void JobTest::copyDirectoryToOtherPartition()
0924 {
0925     // qDebug();
0926     const QString src = homeTmpDir() + "dirFromHome";
0927     const QString dest = otherTmpDir() + "dirFromHome_copied";
0928     createTestDirectory(src);
0929     copyLocalDirectory(src, dest);
0930 }
0931 
0932 void JobTest::copyRelativeSymlinkToSamePartition() // #352927
0933 {
0934 #ifdef Q_OS_WIN
0935     QSKIP("Skipping symlink test on Windows");
0936 #else
0937     const QString filePath = homeTmpDir() + "testlink";
0938     const QString dest = homeTmpDir() + "testlink_copied";
0939     createTestSymlink(filePath, "relative");
0940     copyLocalSymlink(filePath, dest, QStringLiteral("relative"));
0941     QFile::remove(filePath);
0942 #endif
0943 }
0944 
0945 void JobTest::copyAbsoluteSymlinkToOtherPartition()
0946 {
0947 #ifdef Q_OS_WIN
0948     QSKIP("Skipping symlink test on Windows");
0949 #else
0950     const QString filePath = homeTmpDir() + "testlink";
0951     const QString dest = otherTmpDir() + "testlink_copied";
0952     createTestSymlink(filePath, QFile::encodeName(homeTmpDir()));
0953     copyLocalSymlink(filePath, dest, homeTmpDir());
0954     QFile::remove(filePath);
0955 #endif
0956 }
0957 
0958 void JobTest::copyFolderWithUnaccessibleSubfolder()
0959 {
0960 #ifdef Q_OS_WIN
0961     QSKIP("Skipping unaccessible folder test on Windows, cannot remove all permissions from a folder");
0962 #endif
0963     QTemporaryDir dir(homeTmpDir() + "UnaccessibleSubfolderTest");
0964     QVERIFY(dir.isValid());
0965     const QString src_dir = dir.path() + "srcHome";
0966     const QString dst_dir = dir.path() + "dstHome";
0967 
0968     QDir().remove(src_dir);
0969     QDir().remove(dst_dir);
0970 
0971     createTestDirectory(src_dir);
0972     createTestDirectory(src_dir + "/folder1");
0973     QString inaccessible = src_dir + "/folder1/inaccessible";
0974 
0975     createTestDirectory(inaccessible);
0976 
0977     QFile(inaccessible).setPermissions(QFile::Permissions()); // Make it inaccessible
0978     // Copying should throw some warnings, as it cannot access some folders
0979 
0980     ScopedCleaner cleaner([&] {
0981         QFile(inaccessible).setPermissions(QFile::Permissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner));
0982 
0983         qDebug() << "Cleaning up" << src_dir << "and" << dst_dir;
0984         KIO::DeleteJob *deljob1 = KIO::del(QUrl::fromLocalFile(src_dir), KIO::HideProgressInfo);
0985         deljob1->setUiDelegate(nullptr); // no skip dialog, thanks
0986         const bool job1OK = deljob1->exec();
0987         QVERIFY(job1OK);
0988 
0989         KIO::DeleteJob *deljob2 = KIO::del(QUrl::fromLocalFile(dst_dir), KIO::HideProgressInfo);
0990         deljob2->setUiDelegate(nullptr); // no skip dialog, thanks
0991         const bool job2OK = deljob2->exec();
0992         QVERIFY(job2OK);
0993 
0994         qDebug() << "Result:" << job1OK << job2OK;
0995     });
0996 
0997     KIO::CopyJob *job = KIO::copy(QUrl::fromLocalFile(src_dir), QUrl::fromLocalFile(dst_dir), KIO::HideProgressInfo);
0998 
0999     QSignalSpy spy(job, &KJob::warning);
1000     job->setUiDelegate(nullptr); // no skip dialog, thanks
1001     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1002 
1003     QCOMPARE(job->totalAmount(KJob::Files), 4); // testfile, testlink, folder1/testlink, folder1/testfile
1004     QCOMPARE(job->totalAmount(KJob::Directories), 3); // srcHome, srcHome/folder1, srcHome/folder1/inaccessible
1005     QCOMPARE(job->processedAmount(KJob::Files), 4);
1006     QCOMPARE(job->processedAmount(KJob::Directories), 3);
1007     QCOMPARE(job->percent(), 100);
1008 
1009     QCOMPARE(spy.count(), 1); // one warning should be emitted by the copy job
1010 }
1011 
1012 void JobTest::copyDataUrl()
1013 {
1014     // GIVEN
1015     const QString dst_dir = homeTmpDir();
1016     QVERIFY(!QFileInfo::exists(dst_dir + "/data"));
1017     // WHEN
1018     KIO::CopyJob *job = KIO::copy(QUrl("data:,Hello%2C%20World!"), QUrl::fromLocalFile(dst_dir), KIO::HideProgressInfo);
1019     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1020     // THEN
1021     const QFileInfo fileInfo(dst_dir + "/data");
1022     QVERIFY(fileInfo.isFile());
1023     QCOMPARE(fileInfo.size(), 13);
1024     QFile::remove(dst_dir + "/data");
1025 }
1026 
1027 void JobTest::suspendFileCopy()
1028 {
1029     const QString filePath = homeTmpDir() + "fileFromHome";
1030     const QString dest = homeTmpDir() + "fileFromHome_copied";
1031     createTestFile(filePath);
1032 
1033     const QUrl u = QUrl::fromLocalFile(filePath);
1034     const QUrl d = QUrl::fromLocalFile(dest);
1035     KIO::Job *job = KIO::file_copy(u, d, KIO::HideProgressInfo);
1036     QSignalSpy spyResult(job, &KJob::result);
1037     job->setUiDelegate(nullptr);
1038     job->setUiDelegateExtension(nullptr);
1039     QVERIFY(job->suspend());
1040     QVERIFY(!spyResult.wait(300));
1041     QVERIFY(job->resume());
1042     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1043     QVERIFY(QFile::exists(dest));
1044     QFile::remove(dest);
1045 }
1046 
1047 void JobTest::suspendCopy()
1048 {
1049     const QString filePath = homeTmpDir() + "fileFromHome";
1050     const QString dest = homeTmpDir() + "fileFromHome_copied";
1051     createTestFile(filePath);
1052 
1053     const QUrl u = QUrl::fromLocalFile(filePath);
1054     const QUrl d = QUrl::fromLocalFile(dest);
1055     KIO::Job *job = KIO::copy(u, d, KIO::HideProgressInfo);
1056     QSignalSpy spyResult(job, &KJob::result);
1057     job->setUiDelegate(nullptr);
1058     job->setUiDelegateExtension(nullptr);
1059     QVERIFY(job->suspend());
1060     QVERIFY(!spyResult.wait(300));
1061     QVERIFY(job->resume());
1062     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1063     QVERIFY(QFile::exists(dest));
1064     QFile::remove(dest);
1065 }
1066 
1067 void JobTest::moveLocalFile(const QString &src, const QString &dest)
1068 {
1069     QVERIFY(QFile::exists(src));
1070     QUrl u = QUrl::fromLocalFile(src);
1071     QUrl d = QUrl::fromLocalFile(dest);
1072 
1073     // move the file with file_move
1074     KIO::Job *job = KIO::file_move(u, d, 0666, KIO::HideProgressInfo);
1075     job->setUiDelegate(nullptr);
1076     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1077     QVERIFY(QFile::exists(dest));
1078     QVERIFY(!QFile::exists(src)); // not there anymore
1079     QCOMPARE(int(QFileInfo(dest).permissions()), int(0x6666));
1080 
1081     // move it back with KIO::move()
1082     job = KIO::move(d, u, KIO::HideProgressInfo);
1083     job->setUiDelegate(nullptr);
1084     job->setUiDelegateExtension(nullptr);
1085     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1086     QVERIFY(!QFile::exists(dest));
1087     QVERIFY(QFile::exists(src)); // it's back
1088 }
1089 
1090 static void moveLocalSymlink(const QString &src, const QString &dest)
1091 {
1092     QT_STATBUF buf;
1093     QVERIFY(QT_LSTAT(QFile::encodeName(src).constData(), &buf) == 0);
1094     QUrl u = QUrl::fromLocalFile(src);
1095     QUrl d = QUrl::fromLocalFile(dest);
1096 
1097     // move the symlink with move, NOT with file_move
1098     KIO::Job *job = KIO::move(u, d, KIO::HideProgressInfo);
1099     job->setUiDelegate(nullptr);
1100     job->setUiDelegateExtension(nullptr);
1101     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1102     QVERIFY(QT_LSTAT(QFile::encodeName(dest).constData(), &buf) == 0);
1103     QVERIFY(!QFile::exists(src)); // not there anymore
1104 
1105     // move it back with KIO::move()
1106     job = KIO::move(d, u, KIO::HideProgressInfo);
1107     job->setUiDelegate(nullptr);
1108     job->setUiDelegateExtension(nullptr);
1109     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1110     QVERIFY(QT_LSTAT(QFile::encodeName(dest).constData(), &buf) != 0); // doesn't exist anymore
1111     QVERIFY(QT_LSTAT(QFile::encodeName(src).constData(), &buf) == 0); // it's back
1112 }
1113 
1114 void JobTest::moveLocalDirectory(const QString &src, const QString &dest)
1115 {
1116     qDebug() << src << " " << dest;
1117     QVERIFY(QFile::exists(src));
1118     QVERIFY(QFileInfo(src).isDir());
1119     QVERIFY(QFileInfo(src + "/testfile").isFile());
1120 #ifndef Q_OS_WIN
1121     QVERIFY(QFileInfo(src + "/testlink").isSymLink());
1122 #endif
1123     QUrl u = QUrl::fromLocalFile(src);
1124     QUrl d = QUrl::fromLocalFile(dest);
1125 
1126     KIO::Job *job = KIO::move(u, d, KIO::HideProgressInfo);
1127     job->setUiDelegate(nullptr);
1128     job->setUiDelegateExtension(nullptr);
1129     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1130     QVERIFY(QFile::exists(dest));
1131     QVERIFY(QFileInfo(dest).isDir());
1132     QVERIFY(QFileInfo(dest + "/testfile").isFile());
1133     QVERIFY(!QFile::exists(src)); // not there anymore
1134 #ifndef Q_OS_WIN
1135     QVERIFY(QFileInfo(dest + "/testlink").isSymLink());
1136 #endif
1137 }
1138 
1139 void JobTest::moveFileToSamePartition()
1140 {
1141     qDebug();
1142     const QString filePath = homeTmpDir() + "fileFromHome";
1143     const QString dest = homeTmpDir() + "fileFromHome_moved";
1144     createTestFile(filePath);
1145     moveLocalFile(filePath, dest);
1146 }
1147 
1148 void JobTest::moveDirectoryToSamePartition()
1149 {
1150     qDebug();
1151     const QString src = homeTmpDir() + "dirFromHome";
1152     const QString dest = homeTmpDir() + "dirFromHome_moved";
1153     createTestDirectory(src);
1154     moveLocalDirectory(src, dest);
1155 }
1156 
1157 void JobTest::moveDirectoryIntoItself()
1158 {
1159     qDebug();
1160     const QString src = homeTmpDir() + "dirFromHome";
1161     const QString dest = src + "/foo";
1162     createTestDirectory(src);
1163     QVERIFY(QFile::exists(src));
1164     QUrl u = QUrl::fromLocalFile(src);
1165     QUrl d = QUrl::fromLocalFile(dest);
1166     KIO::CopyJob *job = KIO::move(u, d);
1167     QVERIFY(!job->exec());
1168     QCOMPARE(job->error(), (int)KIO::ERR_CANNOT_MOVE_INTO_ITSELF);
1169     QCOMPARE(job->errorString(), i18n("A folder cannot be moved into itself"));
1170     QDir(dest).removeRecursively();
1171 }
1172 
1173 void JobTest::moveFileToOtherPartition()
1174 {
1175     qDebug();
1176     const QString filePath = homeTmpDir() + "fileFromHome";
1177     const QString dest = otherTmpDir() + "fileFromHome_moved";
1178     createTestFile(filePath);
1179     moveLocalFile(filePath, dest);
1180 }
1181 
1182 void JobTest::moveSymlinkToOtherPartition()
1183 {
1184 #ifndef Q_OS_WIN
1185     qDebug();
1186     const QString filePath = homeTmpDir() + "testlink";
1187     const QString dest = otherTmpDir() + "testlink_moved";
1188     createTestSymlink(filePath);
1189     moveLocalSymlink(filePath, dest);
1190 #endif
1191 }
1192 
1193 void JobTest::moveDirectoryToOtherPartition()
1194 {
1195     qDebug();
1196 #ifndef Q_OS_WIN
1197     const QString src = homeTmpDir() + "dirFromHome";
1198     const QString dest = otherTmpDir() + "dirFromHome_moved";
1199     createTestDirectory(src);
1200     moveLocalDirectory(src, dest);
1201 #endif
1202 }
1203 
1204 struct CleanupInaccessibleSubdir {
1205     explicit CleanupInaccessibleSubdir(const QString &subdir)
1206         : subdir(subdir)
1207     {
1208     }
1209     ~CleanupInaccessibleSubdir()
1210     {
1211         QVERIFY(QFile(subdir).setPermissions(QFile::Permissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner)));
1212         QVERIFY(QDir(subdir).removeRecursively());
1213     }
1214 
1215 private:
1216     const QString subdir;
1217 };
1218 
1219 void JobTest::moveFileNoPermissions()
1220 {
1221 #ifdef Q_OS_WIN
1222     QSKIP("Skipping unaccessible folder test on Windows, cannot remove all permissions from a folder");
1223 #endif
1224     // Given a file that cannot be moved (subdir has no permissions)
1225     const QString subdir = homeTmpDir() + "subdir";
1226     QVERIFY(QDir().mkpath(subdir));
1227     const QString src = subdir + "/thefile";
1228     createTestFile(src);
1229     QVERIFY(QFile(subdir).setPermissions(QFile::Permissions())); // Make it inaccessible
1230     CleanupInaccessibleSubdir c(subdir);
1231 
1232     // When trying to move it
1233     const QString dest = homeTmpDir() + "dest";
1234     KIO::CopyJob *job = KIO::move(QUrl::fromLocalFile(src), QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
1235     job->setUiDelegate(nullptr);
1236     job->setUiDelegateExtension(nullptr); // no skip dialog, thanks
1237 
1238     // The job should fail with "access denied"
1239     QVERIFY(!job->exec());
1240     QCOMPARE(job->error(), (int)KIO::ERR_ACCESS_DENIED);
1241     // Note that, just like mv(1), KIO's behavior depends on whether
1242     // a direct rename(2) was used, or a full copy+del. In the first case
1243     // there is no destination file created, but in the second case the
1244     // destination file remains.
1245     // In this test it's the same partition, so no dest created.
1246     QVERIFY(!QFile::exists(dest));
1247 
1248     QCOMPARE(job->totalAmount(KJob::Files), 1);
1249     QCOMPARE(job->totalAmount(KJob::Directories), 0);
1250     QCOMPARE(job->processedAmount(KJob::Files), 0);
1251     QCOMPARE(job->processedAmount(KJob::Directories), 0);
1252     QCOMPARE(job->percent(), 0);
1253 }
1254 
1255 void JobTest::moveDirectoryNoPermissions()
1256 {
1257 #ifdef Q_OS_WIN
1258     QSKIP("Skipping unaccessible folder test on Windows, cannot remove all permissions from a folder");
1259 #endif
1260     // Given a dir that cannot be moved (parent dir has no permissions)
1261     const QString subdir = homeTmpDir() + "subdir";
1262     const QString src = subdir + "/thedir";
1263     QVERIFY(QDir().mkpath(src));
1264     QVERIFY(QFileInfo(src).isDir());
1265     QVERIFY(QFile(subdir).setPermissions(QFile::Permissions())); // Make it inaccessible
1266     CleanupInaccessibleSubdir c(subdir);
1267 
1268     // When trying to move it
1269     const QString dest = homeTmpDir() + "mdnp";
1270     KIO::CopyJob *job = KIO::move(QUrl::fromLocalFile(src), QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
1271     job->setUiDelegate(nullptr);
1272     job->setUiDelegateExtension(nullptr); // no skip dialog, thanks
1273 
1274     // The job should fail with "access denied"
1275     QVERIFY(!job->exec());
1276     QCOMPARE(job->error(), (int)KIO::ERR_ACCESS_DENIED);
1277 
1278     QVERIFY(!QFile::exists(dest));
1279 
1280     QCOMPARE(job->totalAmount(KJob::Files), 1);
1281     QCOMPARE(job->totalAmount(KJob::Directories), 0);
1282     QCOMPARE(job->processedAmount(KJob::Files), 0);
1283     QCOMPARE(job->processedAmount(KJob::Directories), 0);
1284     QCOMPARE(job->percent(), 0);
1285 }
1286 
1287 void JobTest::moveDirectoryToReadonlyFilesystem_data()
1288 {
1289     QTest::addColumn<QList<QUrl>>("sources");
1290     QTest::addColumn<int>("expectedErrorCode");
1291 
1292     const QString srcFileHomePath = homeTmpDir() + "srcFileHome";
1293     const QUrl srcFileHome = QUrl::fromLocalFile(srcFileHomePath);
1294     createTestFile(srcFileHomePath);
1295 
1296     const QString srcFileOtherPath = otherTmpDir() + "srcFileOther";
1297     const QUrl srcFileOther = QUrl::fromLocalFile(srcFileOtherPath);
1298     createTestFile(srcFileOtherPath);
1299 
1300     const QString srcDirHomePath = homeTmpDir() + "srcDirHome";
1301     const QUrl srcDirHome = QUrl::fromLocalFile(srcDirHomePath);
1302     createTestDirectory(srcDirHomePath);
1303 
1304     const QString srcDirHome2Path = homeTmpDir() + "srcDirHome2";
1305     const QUrl srcDirHome2 = QUrl::fromLocalFile(srcDirHome2Path);
1306     createTestDirectory(srcDirHome2Path);
1307 
1308     const QString srcDirOtherPath = otherTmpDir() + "srcDirOther";
1309     const QUrl srcDirOther = QUrl::fromLocalFile(srcDirOtherPath);
1310     createTestDirectory(srcDirOtherPath);
1311 
1312     QTest::newRow("file_same_partition") << QList<QUrl>{srcFileHome} << int(KIO::ERR_WRITE_ACCESS_DENIED);
1313     QTest::newRow("file_other_partition") << QList<QUrl>{srcFileOther} << int(KIO::ERR_WRITE_ACCESS_DENIED);
1314     QTest::newRow("one_dir_same_partition") << QList<QUrl>{srcDirHome} << int(KIO::ERR_WRITE_ACCESS_DENIED);
1315     QTest::newRow("one_dir_other_partition") << QList<QUrl>{srcDirOther} << int(KIO::ERR_WRITE_ACCESS_DENIED);
1316     QTest::newRow("dirs_same_partition") << QList<QUrl>{srcDirHome, srcDirHome2} << int(KIO::ERR_WRITE_ACCESS_DENIED);
1317     QTest::newRow("dirs_both_partitions") << QList<QUrl>{srcDirOther, srcDirHome} << int(KIO::ERR_WRITE_ACCESS_DENIED);
1318 }
1319 
1320 void JobTest::moveDirectoryToReadonlyFilesystem()
1321 {
1322     QFETCH(QList<QUrl>, sources);
1323     QFETCH(int, expectedErrorCode);
1324 
1325     const QString dst_dir = homeTmpDir() + "readonlyDest";
1326     const QUrl dst = QUrl::fromLocalFile(dst_dir);
1327     QVERIFY2(QDir().mkdir(dst_dir), qPrintable(dst_dir));
1328     QFile(dst_dir).setPermissions(QFile::Permissions(QFile::ReadOwner | QFile::ExeOwner)); // Make it readonly, moving should throw some errors
1329 
1330     ScopedCleaner cleaner([&] {
1331         QVERIFY(QFile(dst_dir).setPermissions(QFile::Permissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner)));
1332         QVERIFY(QDir(dst_dir).removeRecursively());
1333     });
1334 
1335     KIO::CopyJob *job = KIO::move(sources, dst, KIO::HideProgressInfo | KIO::NoPrivilegeExecution);
1336     job->setUiDelegate(nullptr);
1337     QVERIFY(!job->exec());
1338     QCOMPARE(job->error(), expectedErrorCode);
1339     for (const QUrl &srcUrl : std::as_const(sources)) {
1340         QVERIFY(QFileInfo::exists(srcUrl.toLocalFile())); // no moving happened
1341     }
1342 
1343     KIO::CopyJob *job2 = KIO::move(sources, dst, KIO::HideProgressInfo);
1344     job2->setUiDelegate(nullptr);
1345     QVERIFY(!job2->exec());
1346     if (job2->error() != KIO::ERR_CANNOT_MKDIR) { // This can happen when moving between partitions, but on CI it's the same partition so allow both
1347         QCOMPARE(job2->error(), expectedErrorCode);
1348     }
1349     for (const QUrl &srcUrl : std::as_const(sources)) {
1350         QVERIFY(QFileInfo::exists(srcUrl.toLocalFile())); // no moving happened
1351     }
1352 }
1353 
1354 static QByteArray expectedListRecursiveOutput()
1355 {
1356     return QByteArray(
1357         ".,..,"
1358         "dirFromHome,dirFromHome/testfile,"
1359         "dirFromHome/testlink," // exists on Windows too, see createTestDirectory
1360         "dirFromHome_copied,"
1361         "dirFromHome_copied/dirFromHome,dirFromHome_copied/dirFromHome/testfile,"
1362         "dirFromHome_copied/dirFromHome/testlink,"
1363         "dirFromHome_copied/testfile,"
1364         "dirFromHome_copied/testlink,"
1365 #ifndef Q_OS_WIN
1366         "dirFromHome_link,"
1367 #endif
1368         "fileFromHome");
1369 }
1370 
1371 void JobTest::listRecursive()
1372 {
1373     // Note: many other tests must have been run before since we rely on the files they created
1374 
1375     const QString src = homeTmpDir();
1376 #ifndef Q_OS_WIN
1377     // Add a symlink to a dir, to make sure we don't recurse into those
1378     bool symlinkOk = symlink("dirFromHome", QFile::encodeName(src + "/dirFromHome_link").constData()) == 0;
1379     QVERIFY(symlinkOk);
1380 #endif
1381     m_names.clear();
1382     KIO::ListJob *job = KIO::listRecursive(QUrl::fromLocalFile(src), KIO::HideProgressInfo);
1383     job->setUiDelegate(nullptr);
1384     connect(job, &KIO::ListJob::entries, this, &JobTest::slotEntries);
1385     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1386     m_names.sort();
1387     const QByteArray ref_names = expectedListRecursiveOutput();
1388     const QString joinedNames = m_names.join(QLatin1Char(','));
1389     if (joinedNames.toLatin1() != ref_names) {
1390         qDebug("%s", qPrintable(joinedNames));
1391         qDebug("%s", ref_names.data());
1392     }
1393     QCOMPARE(joinedNames.toLatin1(), ref_names);
1394 }
1395 
1396 void JobTest::multipleListRecursive()
1397 {
1398     // Note: listRecursive() must have been run first
1399     const QString src = homeTmpDir();
1400     m_names.clear();
1401     QList<KIO::ListJob *> jobs;
1402     for (int i = 0; i < 100; ++i) {
1403         KIO::ListJob *job = KIO::listRecursive(QUrl::fromLocalFile(src), KIO::HideProgressInfo);
1404         job->setUiDelegate(nullptr);
1405         if (i == 6) {
1406             connect(job, &KIO::ListJob::entries, this, &JobTest::slotEntries);
1407         }
1408         connect(job, &KJob::result, this, [&jobs, job]() {
1409             jobs.removeOne(job);
1410         });
1411         jobs.push_back(job);
1412     }
1413     QTRY_VERIFY(jobs.isEmpty());
1414 
1415     m_names.sort();
1416     const QByteArray ref_names = expectedListRecursiveOutput();
1417     const QString joinedNames = m_names.join(QLatin1Char(','));
1418     if (joinedNames.toLatin1() != ref_names) {
1419         qDebug("%s", qPrintable(joinedNames));
1420         qDebug("%s", ref_names.data());
1421     }
1422     QCOMPARE(joinedNames.toLatin1(), ref_names);
1423 }
1424 
1425 void JobTest::listFile()
1426 {
1427     const QString filePath = homeTmpDir() + "fileFromHome";
1428     createTestFile(filePath);
1429     KIO::ListJob *job = KIO::listDir(QUrl::fromLocalFile(filePath), KIO::HideProgressInfo);
1430     job->setUiDelegate(nullptr);
1431     QVERIFY(!job->exec());
1432     QCOMPARE(job->error(), static_cast<int>(KIO::ERR_IS_FILE));
1433 
1434     // And list something that doesn't exist
1435     const QString path = homeTmpDir() + "fileFromHomeDoesNotExist";
1436     job = KIO::listDir(QUrl::fromLocalFile(path), KIO::HideProgressInfo);
1437     job->setUiDelegate(nullptr);
1438     QVERIFY(!job->exec());
1439     QCOMPARE(job->error(), static_cast<int>(KIO::ERR_DOES_NOT_EXIST));
1440 }
1441 
1442 void JobTest::killJob()
1443 {
1444     const QString src = homeTmpDir();
1445     KIO::ListJob *job = KIO::listDir(QUrl::fromLocalFile(src), KIO::HideProgressInfo);
1446     QVERIFY(job->isAutoDelete());
1447     QPointer<KIO::ListJob> ptr(job);
1448     job->setUiDelegate(nullptr);
1449     qApp->processEvents(); // let the job start, it's no fun otherwise
1450     job->kill();
1451     qApp->sendPostedEvents(nullptr, QEvent::DeferredDelete); // process the deferred delete of the job
1452     QVERIFY(ptr.isNull());
1453 }
1454 
1455 void JobTest::killJobBeforeStart()
1456 {
1457     const QString src = homeTmpDir();
1458     KIO::Job *job = KIO::stat(QUrl::fromLocalFile(src), KIO::HideProgressInfo);
1459     QVERIFY(job->isAutoDelete());
1460     QPointer<KIO::Job> ptr(job);
1461     job->setUiDelegate(nullptr);
1462     job->kill();
1463     qApp->sendPostedEvents(nullptr, QEvent::DeferredDelete); // process the deferred delete of the job
1464     QVERIFY(ptr.isNull());
1465     qApp->processEvents(); // does KIO scheduler crash here? nope.
1466 }
1467 
1468 void JobTest::deleteJobBeforeStart() // #163171
1469 {
1470     const QString src = homeTmpDir();
1471     KIO::Job *job = KIO::stat(QUrl::fromLocalFile(src), KIO::HideProgressInfo);
1472     QVERIFY(job->isAutoDelete());
1473     job->setUiDelegate(nullptr);
1474     delete job;
1475     qApp->processEvents(); // does KIO scheduler crash here?
1476 }
1477 
1478 void JobTest::directorySize()
1479 {
1480     // Note: many other tests must have been run before since we rely on the files they created
1481 
1482     const QString src = homeTmpDir();
1483 
1484     KIO::DirectorySizeJob *job = KIO::directorySize(QUrl::fromLocalFile(src));
1485     job->setUiDelegate(nullptr);
1486     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1487     qDebug() << "totalSize: " << job->totalSize();
1488     qDebug() << "totalFiles: " << job->totalFiles();
1489     qDebug() << "totalSubdirs: " << job->totalSubdirs();
1490 #ifdef Q_OS_WIN
1491     QCOMPARE(job->totalFiles(), 5ULL); // see expected result in listRecursive() above
1492     QCOMPARE(job->totalSubdirs(), 3ULL); // see expected result in listRecursive() above
1493     QVERIFY(job->totalSize() > 54);
1494 #else
1495     QCOMPARE(job->totalFiles(), 7ULL); // see expected result in listRecursive() above
1496     QCOMPARE(job->totalSubdirs(), 4ULL); // see expected result in listRecursive() above
1497     QVERIFY2(job->totalSize() >= 60,
1498              qPrintable(QString("totalSize was %1").arg(job->totalSize()))); // size of subdir entries is filesystem dependent. E.g. this is 16428 with ext4 but
1499                                                                              // only 272 with xfs, and 63 on FreeBSD
1500 #endif
1501 
1502     qApp->sendPostedEvents(nullptr, QEvent::DeferredDelete);
1503 }
1504 
1505 void JobTest::directorySizeError()
1506 {
1507     KIO::DirectorySizeJob *job = KIO::directorySize(QUrl::fromLocalFile(QStringLiteral("/I/Dont/Exist")));
1508     job->setUiDelegate(nullptr);
1509     QVERIFY(!job->exec());
1510     qApp->sendPostedEvents(nullptr, QEvent::DeferredDelete);
1511 }
1512 
1513 void JobTest::slotEntries(KIO::Job *, const KIO::UDSEntryList &lst)
1514 {
1515     for (KIO::UDSEntryList::ConstIterator it = lst.begin(); it != lst.end(); ++it) {
1516         QString displayName = (*it).stringValue(KIO::UDSEntry::UDS_NAME);
1517         // QUrl url = (*it).stringValue( KIO::UDSEntry::UDS_URL );
1518         m_names.append(displayName);
1519     }
1520 }
1521 
1522 void JobTest::calculateRemainingSeconds()
1523 {
1524     unsigned int seconds = KIO::calculateRemainingSeconds(2 * 86400 - 60, 0, 1);
1525     QCOMPARE(seconds, static_cast<unsigned int>(2 * 86400 - 60));
1526     QString text = KIO::convertSeconds(seconds);
1527     QCOMPARE(text, i18n("1 day 23:59:00"));
1528 
1529     seconds = KIO::calculateRemainingSeconds(520, 20, 10);
1530     QCOMPARE(seconds, static_cast<unsigned int>(50));
1531     text = KIO::convertSeconds(seconds);
1532     QCOMPARE(text, i18n("00:00:50"));
1533 }
1534 
1535 void JobTest::getInvalidUrl()
1536 {
1537     QUrl url(QStringLiteral("http://strange<hostname>/"));
1538     QVERIFY(!url.isValid());
1539 
1540     KIO::SimpleJob *job = KIO::get(url, KIO::NoReload, KIO::HideProgressInfo);
1541     QVERIFY(job != nullptr);
1542     job->setUiDelegate(nullptr);
1543 
1544     QVERIFY(!job->exec()); // it should fail :)
1545 }
1546 
1547 void JobTest::slotMimetype(KIO::Job *job, const QString &type)
1548 {
1549     QVERIFY(job != nullptr);
1550     m_mimetype = type;
1551 }
1552 
1553 void JobTest::deleteFile()
1554 {
1555     const QString dest = otherTmpDir() + "fileFromHome_copied";
1556     createTestFile(dest);
1557     KIO::Job *job = KIO::del(QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
1558     job->setUiDelegate(nullptr);
1559     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1560     QVERIFY(!QFile::exists(dest));
1561 }
1562 
1563 void JobTest::deleteDirectory()
1564 {
1565     const QString dest = otherTmpDir() + "dirFromHome_copied";
1566     if (!QFile::exists(dest)) {
1567         createTestDirectory(dest);
1568     }
1569     // Let's put a few things in there to see if the recursive deletion works correctly
1570     // A hidden file:
1571     createTestFile(dest + "/.hidden");
1572 #ifndef Q_OS_WIN
1573     // A broken symlink:
1574     createTestSymlink(dest + "/broken_symlink");
1575     // A symlink to a dir:
1576     const auto srcFileName = QFile::encodeName(QFileInfo(QFINDTESTDATA("jobtest.cpp")).absolutePath()).constData();
1577     const auto symLinkFileName = QFile::encodeName(dest + "/symlink_to_dir").constData();
1578     if (symlink(srcFileName, symLinkFileName) != 0) {
1579         qFatal("couldn't create symlink: %s", strerror(errno));
1580     }
1581 #endif
1582 
1583     KIO::Job *job = KIO::del(QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
1584     job->setUiDelegate(nullptr);
1585     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1586     QVERIFY(!QFile::exists(dest));
1587 }
1588 
1589 void JobTest::deleteSymlink(bool using_fast_path)
1590 {
1591     extern KIOCORE_EXPORT bool kio_resolve_local_urls;
1592     kio_resolve_local_urls = !using_fast_path;
1593 
1594 #ifndef Q_OS_WIN
1595     const QString src = homeTmpDir() + "dirFromHome";
1596     createTestDirectory(src);
1597     QVERIFY(QFile::exists(src));
1598     const QString dest = homeTmpDir() + "/dirFromHome_link";
1599     if (!QFile::exists(dest)) {
1600         // Add a symlink to a dir, to make sure we don't recurse into those
1601         bool symlinkOk = symlink(QFile::encodeName(src).constData(), QFile::encodeName(dest).constData()) == 0;
1602         QVERIFY(symlinkOk);
1603         QVERIFY(QFile::exists(dest));
1604     }
1605     KIO::Job *job = KIO::del(QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
1606     job->setUiDelegate(nullptr);
1607     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1608     QVERIFY(!QFile::exists(dest));
1609     QVERIFY(QFile::exists(src));
1610 #endif
1611 
1612     kio_resolve_local_urls = true;
1613 }
1614 
1615 void JobTest::deleteSymlink()
1616 {
1617 #ifndef Q_OS_WIN
1618     deleteSymlink(true);
1619     deleteSymlink(false);
1620 #endif
1621 }
1622 
1623 void JobTest::deleteManyDirs(bool using_fast_path)
1624 {
1625     extern KIOCORE_EXPORT bool kio_resolve_local_urls;
1626     kio_resolve_local_urls = !using_fast_path;
1627 
1628     const int numDirs = 50;
1629     QList<QUrl> dirs;
1630     for (int i = 0; i < numDirs; ++i) {
1631         const QString dir = homeTmpDir() + "dir" + QString::number(i);
1632         createTestDirectory(dir);
1633         dirs << QUrl::fromLocalFile(dir);
1634     }
1635     QElapsedTimer dt;
1636     dt.start();
1637     KIO::Job *job = KIO::del(dirs, KIO::HideProgressInfo);
1638     job->setUiDelegate(nullptr);
1639     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1640     for (const QUrl &dir : std::as_const(dirs)) {
1641         QVERIFY(!QFile::exists(dir.toLocalFile()));
1642     }
1643 
1644     qDebug() << "Deleted" << numDirs << "dirs in" << dt.elapsed() << "milliseconds";
1645     kio_resolve_local_urls = true;
1646 }
1647 
1648 void JobTest::deleteManyDirs()
1649 {
1650     deleteManyDirs(true);
1651     deleteManyDirs(false);
1652 }
1653 
1654 static QList<QUrl> createManyFiles(const QString &baseDir, int numFiles)
1655 {
1656     QList<QUrl> ret;
1657     ret.reserve(numFiles);
1658     for (int i = 0; i < numFiles; ++i) {
1659         // create empty file
1660         const QString file = baseDir + QString::number(i);
1661         QFile f(file);
1662         bool ok = f.open(QIODevice::WriteOnly);
1663         if (ok) {
1664             f.write("Hello");
1665             ret.append(QUrl::fromLocalFile(file));
1666         }
1667     }
1668     return ret;
1669 }
1670 
1671 void JobTest::deleteManyFilesIndependently()
1672 {
1673     QElapsedTimer dt;
1674     dt.start();
1675     const int numFiles = 100; // Use 1000 for performance testing
1676     const QString baseDir = homeTmpDir();
1677     const QList<QUrl> urls = createManyFiles(baseDir, numFiles);
1678     QCOMPARE(urls.count(), numFiles);
1679     for (int i = 0; i < numFiles; ++i) {
1680         // delete each file independently. lots of jobs. this stress-tests kio scheduling.
1681         const QUrl url = urls.at(i);
1682         const QString file = url.toLocalFile();
1683         QVERIFY(QFile::exists(file));
1684         // qDebug() << file;
1685         KIO::Job *job = KIO::del(url, KIO::HideProgressInfo);
1686         job->setUiDelegate(nullptr);
1687         QVERIFY2(job->exec(), qPrintable(job->errorString()));
1688         QVERIFY(!QFile::exists(file));
1689     }
1690     qDebug() << "Deleted" << numFiles << "files in" << dt.elapsed() << "milliseconds";
1691 }
1692 
1693 void JobTest::deleteManyFilesTogether(bool using_fast_path)
1694 {
1695     extern KIOCORE_EXPORT bool kio_resolve_local_urls;
1696     kio_resolve_local_urls = !using_fast_path;
1697 
1698     QElapsedTimer dt;
1699     dt.start();
1700     const int numFiles = 100; // Use 1000 for performance testing
1701     const QString baseDir = homeTmpDir();
1702     const QList<QUrl> urls = createManyFiles(baseDir, numFiles);
1703     QCOMPARE(urls.count(), numFiles);
1704 
1705     // qDebug() << file;
1706     KIO::Job *job = KIO::del(urls, KIO::HideProgressInfo);
1707     job->setUiDelegate(nullptr);
1708     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1709     qDebug() << "Deleted" << numFiles << "files in" << dt.elapsed() << "milliseconds";
1710 
1711     kio_resolve_local_urls = true;
1712 }
1713 
1714 void JobTest::deleteManyFilesTogether()
1715 {
1716     deleteManyFilesTogether(true);
1717     deleteManyFilesTogether(false);
1718 }
1719 
1720 void JobTest::rmdirEmpty()
1721 {
1722     const QString dir = homeTmpDir() + "dir";
1723     QDir().mkdir(dir);
1724     QVERIFY(QFile::exists(dir));
1725     KIO::Job *job = KIO::rmdir(QUrl::fromLocalFile(dir));
1726     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1727     QVERIFY(!QFile::exists(dir));
1728 }
1729 
1730 void JobTest::rmdirNotEmpty()
1731 {
1732     const QString dir = homeTmpDir() + "dir";
1733     createTestDirectory(dir);
1734     createTestDirectory(dir + "/subdir");
1735     KIO::Job *job = KIO::rmdir(QUrl::fromLocalFile(dir));
1736     QVERIFY(!job->exec());
1737     QVERIFY(QFile::exists(dir));
1738 }
1739 
1740 void JobTest::stat()
1741 {
1742 #if 1
1743     const QString filePath = homeTmpDir() + "fileFromHome";
1744     createTestFile(filePath);
1745     const QUrl url(QUrl::fromLocalFile(filePath));
1746     KIO::StatJob *job = KIO::stat(url, KIO::HideProgressInfo);
1747     QVERIFY(job);
1748     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1749     // TODO set setSide
1750     const KIO::UDSEntry &entry = job->statResult();
1751 
1752     // we only get filename, access, type, size, uid, gid, btime, mtime, atime
1753     QVERIFY(entry.contains(KIO::UDSEntry::UDS_NAME));
1754     QVERIFY(entry.contains(KIO::UDSEntry::UDS_ACCESS));
1755     QVERIFY(entry.contains(KIO::UDSEntry::UDS_SIZE));
1756     QVERIFY(entry.contains(KIO::UDSEntry::UDS_FILE_TYPE));
1757     QVERIFY(entry.contains(KIO::UDSEntry::UDS_LOCAL_USER_ID));
1758     QVERIFY(entry.contains(KIO::UDSEntry::UDS_LOCAL_GROUP_ID));
1759     QVERIFY(!entry.contains(KIO::UDSEntry::UDS_USER));
1760     QVERIFY(!entry.contains(KIO::UDSEntry::UDS_GROUP));
1761     // QVERIFY(entry.contains(KIO::UDSEntry::UDS_CREATION_TIME)); // only true if st_birthtime or statx is used
1762     QVERIFY(entry.contains(KIO::UDSEntry::UDS_MODIFICATION_TIME));
1763     QVERIFY(entry.contains(KIO::UDSEntry::UDS_ACCESS_TIME));
1764     QCOMPARE(entry.count(), 8 + (entry.contains(KIO::UDSEntry::UDS_CREATION_TIME) ? 1 : 0));
1765 
1766     QVERIFY(!entry.isDir());
1767     QVERIFY(!entry.isLink());
1768     QCOMPARE(entry.stringValue(KIO::UDSEntry::UDS_NAME), QStringLiteral("fileFromHome"));
1769 
1770     // Compare what we get via kio_file and what we get when KFileItem stat()s directly
1771     const KFileItem kioItem(entry, url);
1772     const KFileItem fileItem(url);
1773     QCOMPARE(kioItem.name(), fileItem.name());
1774     QCOMPARE(kioItem.url(), fileItem.url());
1775     QCOMPARE(kioItem.size(), fileItem.size());
1776     QCOMPARE(kioItem.user(), fileItem.user());
1777     QCOMPARE(kioItem.group(), fileItem.group());
1778     QCOMPARE(kioItem.mimetype(), fileItem.mimetype());
1779     QCOMPARE(kioItem.permissions(), fileItem.permissions());
1780     QCOMPARE(kioItem.time(KFileItem::ModificationTime), fileItem.time(KFileItem::ModificationTime));
1781     QCOMPARE(kioItem.time(KFileItem::AccessTime), fileItem.time(KFileItem::AccessTime));
1782 
1783 #else
1784     // Testing stat over HTTP
1785     KIO::StatJob *job = KIO::stat(QUrl("http://www.kde.org"), KIO::HideProgressInfo);
1786     QVERIFY(job);
1787     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1788     // TODO set setSide, setDetails
1789     const KIO::UDSEntry &entry = job->statResult();
1790     QVERIFY(!entry.isDir());
1791     QVERIFY(!entry.isLink());
1792     QCOMPARE(entry.stringValue(KIO::UDSEntry::UDS_NAME), QString());
1793 #endif
1794 }
1795 
1796 void JobTest::statDetailsBasic()
1797 {
1798     const QString filePath = homeTmpDir() + "fileFromHome";
1799     createTestFile(filePath);
1800     const QUrl url(QUrl::fromLocalFile(filePath));
1801     KIO::StatJob *job = KIO::stat(url, KIO::StatJob::StatSide::SourceSide, KIO::StatBasic, KIO::HideProgressInfo);
1802     QVERIFY(job);
1803     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1804     // TODO set setSide
1805     const KIO::UDSEntry &entry = job->statResult();
1806 
1807     // we only get filename, access, type, size, (no linkdest)
1808     QVERIFY(entry.contains(KIO::UDSEntry::UDS_NAME));
1809     QVERIFY(entry.contains(KIO::UDSEntry::UDS_ACCESS));
1810     QVERIFY(entry.contains(KIO::UDSEntry::UDS_SIZE));
1811     QVERIFY(entry.contains(KIO::UDSEntry::UDS_FILE_TYPE));
1812     QCOMPARE(entry.count(), 4);
1813 
1814     QVERIFY(!entry.isDir());
1815     QVERIFY(!entry.isLink());
1816     QVERIFY(entry.numberValue(KIO::UDSEntry::UDS_ACCESS) > 0);
1817     QCOMPARE(entry.stringValue(KIO::UDSEntry::UDS_NAME), QStringLiteral("fileFromHome"));
1818 
1819     // Compare what we get via kio_file and what we get when KFileItem stat()s directly
1820     // for the requested fields
1821     const KFileItem kioItem(entry, url);
1822     const KFileItem fileItem(url);
1823     QCOMPARE(kioItem.name(), fileItem.name());
1824     QCOMPARE(kioItem.url(), fileItem.url());
1825     QCOMPARE(kioItem.size(), fileItem.size());
1826     QCOMPARE(kioItem.user(), "");
1827     QCOMPARE(kioItem.group(), "");
1828     QCOMPARE(kioItem.mimetype(), "application/octet-stream");
1829     QCOMPARE(kioItem.permissions(), 438);
1830     QCOMPARE(kioItem.time(KFileItem::ModificationTime), QDateTime());
1831     QCOMPARE(kioItem.time(KFileItem::AccessTime), QDateTime());
1832 }
1833 
1834 void JobTest::statDetailsBasicSetDetails()
1835 {
1836     const QString filePath = homeTmpDir() + "fileFromHome";
1837     createTestFile(filePath);
1838     const QUrl url(QUrl::fromLocalFile(filePath));
1839     KIO::StatJob *job = KIO::stat(url);
1840     job->setDetails(KIO::StatBasic);
1841     QVERIFY(job);
1842     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1843     // TODO set setSide
1844     const KIO::UDSEntry &entry = job->statResult();
1845 
1846     // we only get filename, access, type, size, (no linkdest)
1847     QVERIFY(entry.contains(KIO::UDSEntry::UDS_NAME));
1848     QVERIFY(entry.contains(KIO::UDSEntry::UDS_ACCESS));
1849     QVERIFY(entry.contains(KIO::UDSEntry::UDS_SIZE));
1850     QVERIFY(entry.contains(KIO::UDSEntry::UDS_FILE_TYPE));
1851     QCOMPARE(entry.count(), 4);
1852 
1853     QVERIFY(!entry.isDir());
1854     QVERIFY(!entry.isLink());
1855     QVERIFY(entry.numberValue(KIO::UDSEntry::UDS_ACCESS) > 0);
1856     QCOMPARE(entry.stringValue(KIO::UDSEntry::UDS_NAME), QStringLiteral("fileFromHome"));
1857 
1858     // Compare what we get via kio_file and what we get when KFileItem stat()s directly
1859     // for the requested fields
1860     const KFileItem kioItem(entry, url);
1861     const KFileItem fileItem(url);
1862     QCOMPARE(kioItem.name(), fileItem.name());
1863     QCOMPARE(kioItem.url(), fileItem.url());
1864     QCOMPARE(kioItem.size(), fileItem.size());
1865     QCOMPARE(kioItem.user(), "");
1866     QCOMPARE(kioItem.group(), "");
1867     QCOMPARE(kioItem.mimetype(), "application/octet-stream");
1868     QCOMPARE(kioItem.permissions(), 438);
1869     QCOMPARE(kioItem.time(KFileItem::ModificationTime), QDateTime());
1870     QCOMPARE(kioItem.time(KFileItem::AccessTime), QDateTime());
1871 }
1872 
1873 void JobTest::statWithInode()
1874 {
1875     const QString filePath = homeTmpDir() + "fileFromHome";
1876     createTestFile(filePath);
1877     const QUrl url(QUrl::fromLocalFile(filePath));
1878     KIO::StatJob *job = KIO::stat(url, KIO::StatJob::SourceSide, KIO::StatInode);
1879     QVERIFY(job);
1880     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1881 
1882     const KIO::UDSEntry entry = job->statResult();
1883     QVERIFY(entry.contains(KIO::UDSEntry::UDS_DEVICE_ID));
1884     QVERIFY(entry.contains(KIO::UDSEntry::UDS_INODE));
1885     QCOMPARE(entry.count(), 2);
1886 
1887     const QString path = otherTmpDir() + "otherFile";
1888     createTestFile(path);
1889     const QUrl otherUrl(QUrl::fromLocalFile(path));
1890     KIO::StatJob *otherJob = KIO::stat(otherUrl, KIO::StatJob::SourceSide, KIO::StatInode);
1891     QVERIFY(otherJob);
1892     QVERIFY2(otherJob->exec(), qPrintable(otherJob->errorString()));
1893 
1894     const KIO::UDSEntry otherEntry = otherJob->statResult();
1895     QVERIFY(otherEntry.contains(KIO::UDSEntry::UDS_DEVICE_ID));
1896     QVERIFY(otherEntry.contains(KIO::UDSEntry::UDS_INODE));
1897     QCOMPARE(otherEntry.count(), 2);
1898 
1899     const int device = entry.numberValue(KIO::UDSEntry::UDS_DEVICE_ID);
1900     const int otherDevice = otherEntry.numberValue(KIO::UDSEntry::UDS_DEVICE_ID);
1901 
1902     // this test doesn't make sense on the CI as it's an LXC container with one partition
1903     if (otherTmpDirIsOnSamePartition()) {
1904         // On the CI where the two tmp dirs are on the only partition available
1905         // in the LXC container, the device ID's would be identical
1906         QCOMPARE(device, otherDevice);
1907     } else {
1908         QVERIFY(device != otherDevice);
1909     }
1910 }
1911 
1912 #ifndef Q_OS_WIN
1913 void JobTest::statSymlink()
1914 {
1915     const QString filePath = homeTmpDir() + "fileFromHome";
1916     createTestFile(filePath);
1917     const QString symlink = otherTmpDir() + "link";
1918     QVERIFY(QFile(filePath).link(symlink));
1919     QVERIFY(QFile::exists(symlink));
1920     setTimeStamp(symlink, QDateTime::currentDateTime().addSecs(-20)); // differentiate link time and source file time
1921 
1922     const QUrl url(QUrl::fromLocalFile(symlink));
1923     KIO::StatJob *job =
1924         KIO::stat(url, KIO::StatJob::StatSide::SourceSide, KIO::StatBasic | KIO::StatResolveSymlink | KIO::StatUser | KIO::StatTime, KIO::HideProgressInfo);
1925     QVERIFY(job);
1926     QVERIFY2(job->exec(), qPrintable(job->errorString()));
1927     // TODO set setSide, setDetails
1928     const KIO::UDSEntry &entry = job->statResult();
1929 
1930     // we only get filename, access, type, size, linkdest, uid, gid, btime, mtime, atime
1931     QVERIFY(entry.contains(KIO::UDSEntry::UDS_NAME));
1932     QVERIFY(entry.contains(KIO::UDSEntry::UDS_ACCESS));
1933     QVERIFY(entry.contains(KIO::UDSEntry::UDS_SIZE));
1934     QVERIFY(entry.contains(KIO::UDSEntry::UDS_FILE_TYPE));
1935     QVERIFY(entry.contains(KIO::UDSEntry::UDS_LINK_DEST));
1936     QVERIFY(entry.contains(KIO::UDSEntry::UDS_LOCAL_USER_ID));
1937     QVERIFY(entry.contains(KIO::UDSEntry::UDS_LOCAL_GROUP_ID));
1938     QVERIFY(!entry.contains(KIO::UDSEntry::UDS_USER));
1939     QVERIFY(!entry.contains(KIO::UDSEntry::UDS_GROUP));
1940     // QVERIFY(entry.contains(KIO::UDSEntry::UDS_CREATION_TIME)); // only true if st_birthtime or statx is used
1941     QVERIFY(entry.contains(KIO::UDSEntry::UDS_MODIFICATION_TIME));
1942     QVERIFY(entry.contains(KIO::UDSEntry::UDS_ACCESS_TIME));
1943     QCOMPARE(entry.count(), 9 + (entry.contains(KIO::UDSEntry::UDS_CREATION_TIME) ? 1 : 0));
1944 
1945     QVERIFY(!entry.isDir());
1946     QVERIFY(entry.isLink());
1947     QVERIFY(entry.numberValue(KIO::UDSEntry::UDS_ACCESS) > 0);
1948     QCOMPARE(entry.stringValue(KIO::UDSEntry::UDS_NAME), QStringLiteral("link"));
1949 
1950     // Compare what we get via kio_file and what we get when KFileItem stat()s directly
1951     const KFileItem kioItem(entry, url);
1952     const KFileItem fileItem(url);
1953     QCOMPARE(kioItem.name(), fileItem.name());
1954     QCOMPARE(kioItem.url(), fileItem.url());
1955     QVERIFY(kioItem.isLink());
1956     QVERIFY(fileItem.isLink());
1957     QCOMPARE(kioItem.linkDest(), fileItem.linkDest());
1958     QCOMPARE(kioItem.size(), fileItem.size());
1959     QCOMPARE(kioItem.user(), fileItem.user());
1960     QCOMPARE(kioItem.group(), fileItem.group());
1961     QCOMPARE(kioItem.mimetype(), fileItem.mimetype());
1962     QCOMPARE(kioItem.permissions(), fileItem.permissions());
1963     QCOMPARE(kioItem.time(KFileItem::ModificationTime), fileItem.time(KFileItem::ModificationTime));
1964     QCOMPARE(kioItem.time(KFileItem::AccessTime), fileItem.time(KFileItem::AccessTime));
1965 }
1966 
1967 /* Check that the underlying system, and Qt, support
1968  * millisecond timestamp resolution.
1969  */
1970 void JobTest::statTimeResolution()
1971 {
1972     const QString filePath = homeTmpDir() + "statFile";
1973     const QDateTime early70sDate = QDateTime::fromMSecsSinceEpoch(107780520123L);
1974     const time_t early70sTime = 107780520; // Seconds for January 6 1973, 12:02
1975 
1976     createTestFile(filePath);
1977 
1978     QFile dest_file(filePath);
1979     QVERIFY(dest_file.open(QIODevice::ReadOnly));
1980 #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
1981     // with nano secs precision
1982     struct timespec ut[2];
1983     ut[0].tv_sec = early70sTime;
1984     ut[0].tv_nsec = 123000000L; // 123 ms
1985     ut[1] = ut[0];
1986     // need to do this with the dest file still opened, or this fails
1987     QCOMPARE(::futimens(dest_file.handle(), ut), 0);
1988 #else
1989     struct timeval ut[2];
1990     ut[0].tv_sec = early70sTime;
1991     ut[0].tv_usec = 123000;
1992     ut[1] = ut[0];
1993     QCOMPARE(::futimes(dest_file.handle(), ut), 0);
1994 #endif
1995     dest_file.close();
1996 
1997     // Check that the modification time is set with millisecond precision
1998     dest_file.setFileName(filePath);
1999     QDateTime d = dest_file.fileTime(QFileDevice::FileModificationTime);
2000     QCOMPARE(d, early70sDate);
2001     QCOMPARE(d.time().msec(), 123);
2002 
2003 #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
2004     QT_STATBUF buff_dest;
2005     QCOMPARE(QT_STAT(filePath.toLocal8Bit().data(), &buff_dest), 0);
2006     QCOMPARE(buff_dest.st_mtim.tv_sec, early70sTime);
2007     QCOMPARE(buff_dest.st_mtim.tv_nsec, 123000000L);
2008 #endif
2009 
2010     QCOMPARE(QFileInfo(filePath).lastModified(), early70sDate);
2011 }
2012 #endif
2013 
2014 void JobTest::mostLocalUrl()
2015 {
2016     const QString filePath = homeTmpDir() + "fileFromHome";
2017     createTestFile(filePath);
2018     KIO::StatJob *job = KIO::mostLocalUrl(QUrl::fromLocalFile(filePath), KIO::HideProgressInfo);
2019     QVERIFY(job);
2020     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2021     QCOMPARE(job->mostLocalUrl().toLocalFile(), filePath);
2022 }
2023 
2024 void JobTest::mostLocalUrlHttp()
2025 {
2026     // the url is returned as-is, as an http url can't have a mostLocalUrl
2027     const QUrl url("http://www.google.com");
2028     KIO::StatJob *httpStat = KIO::mostLocalUrl(url, KIO::HideProgressInfo);
2029     QVERIFY(httpStat);
2030     QVERIFY2(httpStat->exec(), qPrintable(httpStat->errorString()));
2031     QCOMPARE(httpStat->mostLocalUrl(), url);
2032 }
2033 
2034 void JobTest::chmodFile()
2035 {
2036     const QString filePath = homeTmpDir() + "fileForChmod";
2037     createTestFile(filePath);
2038     KFileItem item(QUrl::fromLocalFile(filePath));
2039     const mode_t origPerm = item.permissions();
2040     mode_t newPerm = origPerm ^ S_IWGRP;
2041     QVERIFY(newPerm != origPerm);
2042     KFileItemList items;
2043     items << item;
2044     KIO::Job *job = KIO::chmod(items, newPerm, S_IWGRP /*TODO: QFile::WriteGroup*/, QString(), QString(), false, KIO::HideProgressInfo);
2045     job->setUiDelegate(nullptr);
2046     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2047 
2048     KFileItem newItem(QUrl::fromLocalFile(filePath));
2049     QCOMPARE(QString::number(newItem.permissions(), 8), QString::number(newPerm, 8));
2050     QFile::remove(filePath);
2051 }
2052 
2053 #ifdef Q_OS_UNIX
2054 void JobTest::chmodSticky()
2055 {
2056     const QString dirPath = homeTmpDir() + "dirForChmodSticky";
2057     QDir().mkpath(dirPath);
2058     KFileItem item(QUrl::fromLocalFile(dirPath));
2059     const mode_t origPerm = item.permissions();
2060     mode_t newPerm = origPerm ^ S_ISVTX;
2061     QVERIFY(newPerm != origPerm);
2062     KFileItemList items({item});
2063     KIO::Job *job = KIO::chmod(items, newPerm, S_ISVTX, QString(), QString(), false, KIO::HideProgressInfo);
2064     job->setUiDelegate(nullptr);
2065     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2066 
2067     KFileItem newItem(QUrl::fromLocalFile(dirPath));
2068     QCOMPARE(QString::number(newItem.permissions(), 8), QString::number(newPerm, 8));
2069     QVERIFY(QDir().rmdir(dirPath));
2070 }
2071 #endif
2072 
2073 void JobTest::chmodFileError()
2074 {
2075     // chown(root) should fail
2076     const QString filePath = homeTmpDir() + "fileForChmod";
2077     createTestFile(filePath);
2078     KFileItem item(QUrl::fromLocalFile(filePath));
2079     const mode_t origPerm = item.permissions();
2080     mode_t newPerm = origPerm ^ S_IWGRP;
2081     QVERIFY(newPerm != origPerm);
2082     KFileItemList items;
2083     items << item;
2084     KIO::Job *job = KIO::chmod(items, newPerm, S_IWGRP /*TODO: QFile::WriteGroup*/, QStringLiteral("root"), QString(), false, KIO::HideProgressInfo);
2085     // Simulate the user pressing "Skip" in the dialog.
2086     job->setUiDelegate(new KJobUiDelegate);
2087     auto *askUser = new MockAskUserInterface(job->uiDelegate());
2088 
2089     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2090 
2091     QCOMPARE(askUser->m_askUserSkipCalled, 1);
2092     KFileItem newItem(QUrl::fromLocalFile(filePath));
2093     // We skipped, so the chmod didn't happen.
2094     QCOMPARE(QString::number(newItem.permissions(), 8), QString::number(origPerm, 8));
2095     QFile::remove(filePath);
2096 }
2097 
2098 void JobTest::mimeType()
2099 {
2100 #if 1
2101     const QString filePath = homeTmpDir() + "fileFromHome";
2102     createTestFile(filePath);
2103     KIO::MimetypeJob *job = KIO::mimetype(QUrl::fromLocalFile(filePath), KIO::HideProgressInfo);
2104     QVERIFY(job);
2105     QSignalSpy spyMimeTypeFound(job, &KIO::TransferJob::mimeTypeFound);
2106     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2107 
2108     QCOMPARE(spyMimeTypeFound.count(), 1);
2109     QCOMPARE(spyMimeTypeFound[0][0], QVariant::fromValue(static_cast<KIO::Job *>(job)));
2110     QCOMPARE(spyMimeTypeFound[0][1].toString(), QStringLiteral("application/octet-stream"));
2111 #else
2112     // Testing mimetype over HTTP
2113     KIO::MimetypeJob *job = KIO::mimetype(QUrl("http://www.kde.org"), KIO::HideProgressInfo);
2114     QVERIFY(job);
2115     QSignalSpy spyMimeTypeFound(job, &KIO::TransferJob::mimeTypeFound);
2116     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2117     QCOMPARE(spyMimeTypeFound.count(), 1);
2118     QCOMPARE(spyMimeTypeFound[0][0], QVariant::fromValue(static_cast<KIO::Job *>(job)));
2119     QCOMPARE(spyMimeTypeFound[0][1].toString(), QString("text/html"));
2120 #endif
2121 }
2122 
2123 void JobTest::mimeTypeError()
2124 {
2125     // KIO::mimetype() on a file that doesn't exist
2126     const QString filePath = homeTmpDir() + "doesNotExist";
2127     KIO::MimetypeJob *job = KIO::mimetype(QUrl::fromLocalFile(filePath), KIO::HideProgressInfo);
2128     QVERIFY(job);
2129     QSignalSpy spyMimeTypeFound(job, &KIO::TransferJob::mimeTypeFound);
2130     QSignalSpy spyResult(job, &KJob::result);
2131     QVERIFY(!job->exec());
2132     QCOMPARE(spyMimeTypeFound.count(), 0);
2133     QCOMPARE(spyResult.count(), 1);
2134 }
2135 
2136 void JobTest::moveFileDestAlreadyExists_data()
2137 {
2138     QTest::addColumn<bool>("autoSkip");
2139 
2140     QTest::newRow("autoSkip") << true;
2141     QTest::newRow("manualSkip") << false;
2142 }
2143 
2144 void JobTest::moveFileDestAlreadyExists() // #157601
2145 {
2146     QFETCH(bool, autoSkip);
2147 
2148     const QString file1 = homeTmpDir() + "fileFromHome";
2149     createTestFile(file1);
2150     const QString file2 = homeTmpDir() + "fileFromHome2";
2151     createTestFile(file2);
2152     const QString file3 = homeTmpDir() + "anotherFile";
2153     createTestFile(file3);
2154     const QString existingDest = otherTmpDir() + "fileFromHome";
2155     createTestFile(existingDest);
2156     const QString existingDest2 = otherTmpDir() + "fileFromHome2";
2157     createTestFile(existingDest2);
2158 
2159     ScopedCleaner cleaner([] {
2160         QFile::remove(otherTmpDir() + "anotherFile");
2161     });
2162 
2163     const QList<QUrl> urls{QUrl::fromLocalFile(file1), QUrl::fromLocalFile(file2), QUrl::fromLocalFile(file3)};
2164     KIO::CopyJob *job = KIO::move(urls, QUrl::fromLocalFile(otherTmpDir()), KIO::HideProgressInfo);
2165     MockAskUserInterface *askUserHandler = nullptr;
2166     if (autoSkip) {
2167         job->setUiDelegate(nullptr);
2168         job->setAutoSkip(true);
2169     } else {
2170         // Simulate the user pressing "Skip" in the dialog.
2171         job->setUiDelegate(new KJobUiDelegate);
2172         askUserHandler = new MockAskUserInterface(job->uiDelegate());
2173         askUserHandler->m_renameResult = KIO::Result_Skip;
2174     }
2175     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2176 
2177     if (askUserHandler) {
2178         QCOMPARE(askUserHandler->m_askUserRenameCalled, 2);
2179         QCOMPARE(askUserHandler->m_askUserSkipCalled, 0);
2180     }
2181     QVERIFY(QFile::exists(file1)); // it was skipped
2182     QVERIFY(QFile::exists(file2)); // it was skipped
2183     QVERIFY(!QFile::exists(file3)); // it was moved
2184 
2185     QCOMPARE(job->totalAmount(KJob::Files), 3);
2186     QCOMPARE(job->totalAmount(KJob::Directories), 0);
2187     QCOMPARE(job->processedAmount(KJob::Files), 1);
2188     QCOMPARE(job->processedAmount(KJob::Directories), 0);
2189     QCOMPARE(job->percent(), 100);
2190 }
2191 
2192 void JobTest::copyFileDestAlreadyExists_data()
2193 {
2194     QTest::addColumn<bool>("autoSkip");
2195 
2196     QTest::newRow("autoSkip") << true;
2197     QTest::newRow("manualSkip") << false;
2198 }
2199 
2200 static void simulatePressingSkip(KJob *job)
2201 {
2202     // Simulate the user pressing "Skip" in the dialog.
2203     job->setUiDelegate(new KJobUiDelegate);
2204     auto *askUserHandler = new MockAskUserInterface(job->uiDelegate());
2205     askUserHandler->m_skipResult = KIO::Result_Skip;
2206 }
2207 
2208 void JobTest::copyFileDestAlreadyExists() // to test skipping when copying
2209 {
2210     QFETCH(bool, autoSkip);
2211     const QString file1 = homeTmpDir() + "fileFromHome";
2212     createTestFile(file1);
2213     const QString file2 = homeTmpDir() + "anotherFile";
2214     createTestFile(file2);
2215     const QString existingDest = otherTmpDir() + "fileFromHome";
2216     createTestFile(existingDest);
2217 
2218     ScopedCleaner cleaner([] {
2219         QFile::remove(otherTmpDir() + "anotherFile");
2220     });
2221 
2222     const QList<QUrl> urls{QUrl::fromLocalFile(file1), QUrl::fromLocalFile(file2)};
2223     KIO::CopyJob *job = KIO::copy(urls, QUrl::fromLocalFile(otherTmpDir()), KIO::HideProgressInfo);
2224     if (autoSkip) {
2225         job->setUiDelegate(nullptr);
2226         job->setAutoSkip(true);
2227     } else {
2228         simulatePressingSkip(job);
2229     }
2230     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2231     QVERIFY(QFile::exists(otherTmpDir() + "anotherFile"));
2232 
2233     QCOMPARE(job->totalAmount(KJob::Files), 2); // file1, file2
2234     QCOMPARE(job->totalAmount(KJob::Directories), 0);
2235     QCOMPARE(job->processedAmount(KJob::Files), 1);
2236     QCOMPARE(job->processedAmount(KJob::Directories), 0);
2237     QCOMPARE(job->percent(), 100);
2238 }
2239 
2240 void JobTest::moveDestAlreadyExistsAutoRename_data()
2241 {
2242     QTest::addColumn<bool>("samePartition");
2243     QTest::addColumn<bool>("moveDirs");
2244 
2245     QTest::newRow("files_same_partition") << true << false;
2246     QTest::newRow("files_other_partition") << false << false;
2247     QTest::newRow("dirs_same_partition") << true << true;
2248     QTest::newRow("dirs_other_partition") << false << true;
2249 }
2250 
2251 void JobTest::moveDestAlreadyExistsAutoRename()
2252 {
2253     QFETCH(bool, samePartition);
2254     QFETCH(bool, moveDirs);
2255 
2256     QString dir;
2257     if (samePartition) {
2258         dir = homeTmpDir() + "dir/";
2259         QVERIFY(QDir(dir).exists() || QDir().mkdir(dir));
2260     } else {
2261         dir = otherTmpDir();
2262     }
2263     moveDestAlreadyExistsAutoRename(dir, moveDirs);
2264 
2265     if (samePartition) {
2266         // cleanup
2267         KIO::Job *job = KIO::del(QUrl::fromLocalFile(dir), KIO::HideProgressInfo);
2268         QVERIFY2(job->exec(), qPrintable(job->errorString()));
2269         QVERIFY(!QFile::exists(dir));
2270     }
2271 }
2272 
2273 void JobTest::moveDestAlreadyExistsAutoRename(const QString &destDir, bool moveDirs) // #256650
2274 {
2275     const QString prefix = moveDirs ? QStringLiteral("dir ") : QStringLiteral("file ");
2276 
2277     const QString file1 = homeTmpDir() + prefix + "(1)";
2278     const QString file2 = homeTmpDir() + prefix + "(2)";
2279     const QString existingDest1 = destDir + prefix + "(1)";
2280     const QString existingDest2 = destDir + prefix + "(2)";
2281     const QStringList sources = QStringList{file1, file2, existingDest1, existingDest2};
2282     for (const QString &source : sources) {
2283         if (moveDirs) {
2284             QVERIFY(QDir().mkdir(source));
2285             createTestFile(source + "/innerfile");
2286             createTestFile(source + "/innerfile2");
2287         } else {
2288             createTestFile(source);
2289         }
2290     }
2291     const QString file3 = destDir + prefix + "(3)";
2292     const QString file4 = destDir + prefix + "(4)";
2293 
2294     ScopedCleaner cleaner([&]() {
2295         if (moveDirs) {
2296             QDir().rmdir(file1);
2297             QDir().rmdir(file2);
2298             QDir().rmdir(file3);
2299             QDir().rmdir(file4);
2300         } else {
2301             QFile::remove(file1);
2302             QFile::remove(file2);
2303             QFile::remove(file3);
2304             QFile::remove(file4);
2305         }
2306     });
2307 
2308     const QList<QUrl> urls = {QUrl::fromLocalFile(file1), QUrl::fromLocalFile(file2)};
2309     KIO::CopyJob *job = KIO::move(urls, QUrl::fromLocalFile(destDir), KIO::HideProgressInfo);
2310     job->setUiDelegate(nullptr);
2311     job->setUiDelegateExtension(nullptr);
2312     job->setAutoRename(true);
2313 
2314     QSignalSpy spyRenamed(job, &KIO::CopyJob::renamed);
2315 
2316     // qDebug() << QDir(destDir).entryList();
2317 
2318     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2319 
2320     // qDebug() << QDir(destDir).entryList();
2321     QVERIFY(!QFile::exists(file1)); // it was moved
2322     QVERIFY(!QFile::exists(file2)); // it was moved
2323 
2324     QVERIFY(QFile::exists(existingDest1));
2325     QVERIFY(QFile::exists(existingDest2));
2326     QVERIFY(QFile::exists(file3));
2327     QVERIFY(QFile::exists(file4));
2328 
2329     QVERIFY(!spyRenamed.isEmpty());
2330 
2331     auto list = spyRenamed.takeFirst();
2332     QCOMPARE(list.at(1).toUrl(), QUrl::fromLocalFile(destDir + prefix + "(1)"));
2333     QCOMPARE(list.at(2).toUrl(), QUrl::fromLocalFile(file3));
2334 
2335     bool samePartition = false;
2336     // Normally we'd see renamed(1, 3) and renamed(2, 4)
2337     // But across partitions, direct rename fails, and we end up with a task list of
2338     // 1->3, 2->3 since renaming 1 to 3 didn't happen yet.
2339     // so renamed(2, 3) is emitted, as if the user had chosen that.
2340     // And when that fails, we then get (3, 4)
2341     if (spyRenamed.count() == 1) {
2342         // It was indeed on the same partition
2343         samePartition = true;
2344         list = spyRenamed.takeFirst();
2345         QCOMPARE(list.at(1).toUrl(), QUrl::fromLocalFile(destDir + prefix + "(2)"));
2346         QCOMPARE(list.at(2).toUrl(), QUrl::fromLocalFile(file4));
2347     } else {
2348         // Remove all renamed signals about innerfiles
2349         spyRenamed.erase(std::remove_if(spyRenamed.begin(),
2350                                         spyRenamed.end(),
2351                                         [](const QList<QVariant> &spy) {
2352                                             return spy.at(1).toUrl().path().contains("innerfile");
2353                                         }),
2354                          spyRenamed.end());
2355 
2356         list = spyRenamed.takeFirst();
2357         QCOMPARE(list.at(1).toUrl(), QUrl::fromLocalFile(destDir + prefix + "(2)"));
2358         QCOMPARE(list.at(2).toUrl(), QUrl::fromLocalFile(file3));
2359 
2360         list = spyRenamed.takeFirst();
2361         QCOMPARE(list.at(1).toUrl(), QUrl::fromLocalFile(file3));
2362         QCOMPARE(list.at(2).toUrl(), QUrl::fromLocalFile(file4));
2363     }
2364 
2365     if (samePartition) {
2366         QCOMPARE(job->totalAmount(KJob::Files), 2); // direct-renamed, so counted as files
2367         QCOMPARE(job->totalAmount(KJob::Directories), 0);
2368         QCOMPARE(job->processedAmount(KJob::Files), 2);
2369         QCOMPARE(job->processedAmount(KJob::Directories), 0);
2370     } else {
2371         if (moveDirs) {
2372             QCOMPARE(job->totalAmount(KJob::Directories), 2);
2373             QCOMPARE(job->totalAmount(KJob::Files), 4); // innerfiles
2374             QCOMPARE(job->processedAmount(KJob::Directories), 2);
2375             QCOMPARE(job->processedAmount(KJob::Files), 4);
2376         } else {
2377             QCOMPARE(job->totalAmount(KJob::Files), 2);
2378             QCOMPARE(job->totalAmount(KJob::Directories), 0);
2379             QCOMPARE(job->processedAmount(KJob::Files), 2);
2380             QCOMPARE(job->processedAmount(KJob::Directories), 0);
2381         }
2382     }
2383 
2384     QCOMPARE(job->percent(), 100);
2385 }
2386 
2387 void JobTest::copyDirectoryAlreadyExistsSkip()
2388 {
2389     // when copying a directory (which contains at least one file) to some location, and then
2390     // copying the same dir to the same location again, and clicking "Skip" there should be no
2391     // segmentation fault, bug 408350
2392 
2393     const QString src = homeTmpDir() + "a";
2394     createTestDirectory(src);
2395     const QString dest = homeTmpDir() + "dest";
2396     createTestDirectory(dest);
2397 
2398     QUrl u = QUrl::fromLocalFile(src);
2399     QUrl d = QUrl::fromLocalFile(dest);
2400 
2401     KIO::Job *job = KIO::copy(u, d, KIO::HideProgressInfo);
2402     job->setUiDelegate(nullptr);
2403     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2404     QVERIFY(QFile::exists(dest + QStringLiteral("/a/testfile")));
2405 
2406     job = KIO::copy(u, d, KIO::HideProgressInfo);
2407 
2408     simulatePressingSkip(job);
2409 
2410     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2411     QVERIFY(QFile::exists(dest + QStringLiteral("/a/testfile")));
2412 
2413     QDir(src).removeRecursively();
2414     QDir(dest).removeRecursively();
2415 
2416     QCOMPARE(job->totalAmount(KJob::Files), 2); // testfile, testlink
2417     QCOMPARE(job->totalAmount(KJob::Directories), 1);
2418     QCOMPARE(job->processedAmount(KJob::Files), 0);
2419     QCOMPARE(job->processedAmount(KJob::Directories), 1);
2420     QCOMPARE(job->percent(), 0);
2421 }
2422 
2423 void JobTest::copyFileAlreadyExistsRename()
2424 {
2425     const QString sourceFile = homeTmpDir() + "file";
2426     const QString dest = homeTmpDir() + "dest/";
2427     const QString alreadyExisting = dest + "file";
2428     const QString renamedFile = dest + "file-renamed";
2429 
2430     createTestFile(sourceFile);
2431     createTestFile(alreadyExisting);
2432     QVERIFY(QFile::exists(sourceFile));
2433     QVERIFY(QFile::exists(alreadyExisting));
2434 
2435     createTestDirectory(dest);
2436 
2437     ScopedCleaner cleaner([&] {
2438         QVERIFY(QFile(sourceFile).remove());
2439         QVERIFY(QDir(dest).removeRecursively());
2440     });
2441 
2442     QUrl s = QUrl::fromLocalFile(sourceFile);
2443     QUrl d = QUrl::fromLocalFile(dest);
2444 
2445     KIO::CopyJob *job = KIO::copy(s, d, KIO::HideProgressInfo);
2446     // Simulate the user pressing "Rename" in the dialog and choosing another destination.
2447     job->setUiDelegate(new KJobUiDelegate);
2448     auto *askUserHandler = new MockAskUserInterface(job->uiDelegate());
2449     askUserHandler->m_renameResult = KIO::Result_Rename;
2450     askUserHandler->m_newDestUrl = QUrl::fromLocalFile(renamedFile);
2451 
2452     QSignalSpy spyRenamed(job, &KIO::CopyJob::renamed);
2453 
2454     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2455     QVERIFY(QFile::exists(renamedFile));
2456 
2457     QCOMPARE(spyRenamed.count(), 1);
2458     auto list = spyRenamed.takeFirst();
2459     QCOMPARE(list.at(1).toUrl(), QUrl::fromLocalFile(alreadyExisting));
2460     QCOMPARE(list.at(2).toUrl(), QUrl::fromLocalFile(renamedFile));
2461 }
2462 
2463 void JobTest::safeOverwrite_data()
2464 {
2465     QTest::addColumn<bool>("destFileExists");
2466 
2467     QTest::newRow("dest_file_exists") << true;
2468     QTest::newRow("dest_file_does_not_exist") << false;
2469 }
2470 
2471 void JobTest::safeOverwrite()
2472 {
2473 #ifdef Q_OS_WIN
2474     QSKIP("Test skipped on Windows");
2475 #endif
2476 
2477     QFETCH(bool, destFileExists);
2478     const QString srcDir = homeTmpDir() + "overwrite";
2479     const QString srcFile = srcDir + "/testfile";
2480     const QString destDir = otherTmpDir() + "overwrite_other";
2481     const QString destFile = destDir + "/testfile";
2482     const QString destPartFile = destFile + ".part";
2483 
2484     createTestDirectory(srcDir);
2485     createTestDirectory(destDir);
2486 
2487     ScopedCleaner cleaner([&] {
2488         QDir(srcDir).removeRecursively();
2489         QDir(destDir).removeRecursively();
2490     });
2491 
2492     const int srcSize = 1000000; // ~1MB
2493     QVERIFY(QFile::resize(srcFile, srcSize));
2494     if (!destFileExists) {
2495         QVERIFY(QFile::remove(destFile));
2496     } else {
2497         QVERIFY(QFile::exists(destFile));
2498     }
2499     QVERIFY(!QFile::exists(destPartFile));
2500 
2501     if (otherTmpDirIsOnSamePartition()) {
2502         QSKIP(qPrintable(QStringLiteral("This test requires %1 and %2 to be on different partitions").arg(srcDir, destDir)));
2503     }
2504 
2505     KIO::FileCopyJob *job = KIO::file_move(QUrl::fromLocalFile(srcFile), QUrl::fromLocalFile(destFile), -1, KIO::HideProgressInfo | KIO::Overwrite);
2506     job->setUiDelegate(nullptr);
2507     QSignalSpy spyTotalSize(job, &KIO::FileCopyJob::totalSize);
2508     connect(job, &KIO::FileCopyJob::processedSize, this, [&](KJob *job, qulonglong size) {
2509         Q_UNUSED(job);
2510         if (size > 0 && size < srcSize) {
2511             // To avoid overwriting dest, we want the KIO worker to use dest.part
2512             QCOMPARE(QFileInfo::exists(destPartFile), destFileExists);
2513         }
2514     });
2515     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2516     QVERIFY(QFile::exists(destFile));
2517     QVERIFY(!QFile::exists(srcFile));
2518     QVERIFY(!QFile::exists(destPartFile));
2519     QCOMPARE(spyTotalSize.count(), 1);
2520 }
2521 
2522 void JobTest::overwriteOlderFiles_data()
2523 {
2524     QTest::addColumn<bool>("destFileOlder");
2525     QTest::addColumn<bool>("moving");
2526 
2527     QTest::newRow("dest_file_older_copying") << true << false;
2528     QTest::newRow("dest_file_older_moving") << true << true;
2529     QTest::newRow("dest_file_younger_copying") << false << false;
2530     QTest::newRow("dest_file_younger_moving") << false << true;
2531 }
2532 
2533 void JobTest::overwriteOlderFiles()
2534 {
2535     QFETCH(bool, destFileOlder);
2536     QFETCH(bool, moving);
2537     const QString srcDir = homeTmpDir() + "overwrite";
2538     const QString srcFile = srcDir + "/testfile";
2539     const QString srcFile2 = srcDir + "/testfile2";
2540     const QString srcFile3 = srcDir + "/testfile3";
2541     const QString destDir = otherTmpDir() + "overwrite_other";
2542     const QString destFile = destDir + "/testfile";
2543     const QString destFile2 = destDir + "/testfile2";
2544     const QString destFile3 = destDir + "/testfile3";
2545     const QString destPartFile = destFile + ".part";
2546 
2547     createTestDirectory(srcDir);
2548     createTestDirectory(destDir);
2549     createTestFile(srcFile2);
2550     createTestFile(srcFile3);
2551     createTestFile(destFile2);
2552     createTestFile(destFile3);
2553     QVERIFY(!QFile::exists(destPartFile));
2554 
2555     const int srcSize = 1000; // ~1KB
2556     QVERIFY(QFile::resize(srcFile, srcSize));
2557     QVERIFY(QFile::resize(srcFile2, srcSize));
2558     QVERIFY(QFile::resize(srcFile3, srcSize));
2559     if (destFileOlder) {
2560         setTimeStamp(destFile, QFile(srcFile).fileTime(QFileDevice::FileModificationTime).addSecs(-2));
2561         setTimeStamp(destFile2, QFile(srcFile2).fileTime(QFileDevice::FileModificationTime).addSecs(-2));
2562 
2563         QVERIFY(QFile(destFile).fileTime(QFileDevice::FileModificationTime) <= QFile(srcFile).fileTime(QFileDevice::FileModificationTime));
2564         QVERIFY(QFile(destFile2).fileTime(QFileDevice::FileModificationTime) <= QFile(srcFile2).fileTime(QFileDevice::FileModificationTime));
2565     } else {
2566         setTimeStamp(destFile, QFile(srcFile).fileTime(QFileDevice::FileModificationTime).addSecs(2));
2567         setTimeStamp(destFile2, QFile(srcFile2).fileTime(QFileDevice::FileModificationTime).addSecs(2));
2568 
2569         QVERIFY(QFile(destFile).fileTime(QFileDevice::FileModificationTime) >= QFile(srcFile).fileTime(QFileDevice::FileModificationTime));
2570         QVERIFY(QFile(destFile2).fileTime(QFileDevice::FileModificationTime) >= QFile(srcFile2).fileTime(QFileDevice::FileModificationTime));
2571     }
2572     // to have an always skipped file
2573     setTimeStamp(destFile3, QFile(srcFile3).fileTime(QFileDevice::FileModificationTime).addSecs(2));
2574 
2575     KIO::CopyJob *job;
2576     if (moving) {
2577         job = KIO::move({QUrl::fromLocalFile(srcFile), QUrl::fromLocalFile(srcFile2), QUrl::fromLocalFile(srcFile3)},
2578                         QUrl::fromLocalFile(destDir),
2579                         KIO::HideProgressInfo);
2580     } else {
2581         job = KIO::copy({QUrl::fromLocalFile(srcFile), QUrl::fromLocalFile(srcFile2), QUrl::fromLocalFile(srcFile3)},
2582                         QUrl::fromLocalFile(destDir),
2583                         KIO::HideProgressInfo);
2584     }
2585 
2586     job->setUiDelegate(new KJobUiDelegate);
2587     auto *askUserHandler = new MockAskUserInterface(job->uiDelegate());
2588     askUserHandler->m_renameResult = KIO::Result_OverwriteWhenOlder;
2589 
2590     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2591     QCOMPARE(askUserHandler->m_askUserRenameCalled, 1);
2592     QVERIFY(!QFile::exists(destPartFile));
2593     // QCOMPARE(spyTotalSize.count(), 1);
2594 
2595     // skipped file whose dest is always newer
2596     QVERIFY(QFile::exists(srcFile3)); // it was skipped
2597     QCOMPARE(QFile(destFile3).size(), 11);
2598 
2599     if (destFileOlder) {
2600         // files were overwritten
2601         QCOMPARE(QFile(destFile).size(), 1000);
2602         QCOMPARE(QFile(destFile2).size(), 1000);
2603 
2604         // files were overwritten
2605         QCOMPARE(job->processedAmount(KJob::Files), 2);
2606         QCOMPARE(job->processedAmount(KJob::Directories), 0);
2607 
2608         if (moving) {
2609             QVERIFY(!QFile::exists(srcFile)); // it was moved
2610             QVERIFY(!QFile::exists(srcFile2)); // it was moved
2611         } else {
2612             QVERIFY(QFile::exists(srcFile)); // it was copied
2613             QVERIFY(QFile::exists(srcFile2)); // it was copied
2614 
2615             QCOMPARE(QFile(destFile).fileTime(QFileDevice::FileModificationTime), QFile(srcFile).fileTime(QFileDevice::FileModificationTime));
2616             QCOMPARE(QFile(destFile2).fileTime(QFileDevice::FileModificationTime), QFile(srcFile2).fileTime(QFileDevice::FileModificationTime));
2617         }
2618     } else {
2619         // files were skipped
2620         QCOMPARE(job->processedAmount(KJob::Files), 0);
2621         QCOMPARE(job->processedAmount(KJob::Directories), 0);
2622 
2623         QCOMPARE(QFile(destFile).size(), 11);
2624         QCOMPARE(QFile(destFile2).size(), 11);
2625 
2626         QVERIFY(QFile::exists(srcFile));
2627         QVERIFY(QFile::exists(srcFile2));
2628     }
2629 
2630     QDir(srcDir).removeRecursively();
2631     QDir(destDir).removeRecursively();
2632 }
2633 
2634 void JobTest::moveAndOverwrite()
2635 {
2636     const QString sourceFile = homeTmpDir() + "fileFromHome";
2637     createTestFile(sourceFile);
2638     QString existingDest = otherTmpDir() + "fileFromHome";
2639     createTestFile(existingDest);
2640 
2641     KIO::FileCopyJob *job = KIO::file_move(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(existingDest), -1, KIO::HideProgressInfo | KIO::Overwrite);
2642     job->setUiDelegate(nullptr);
2643     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2644     QVERIFY(!QFile::exists(sourceFile)); // it was moved
2645 
2646 #ifndef Q_OS_WIN
2647     // Now same thing when the target is a symlink to the source
2648     createTestFile(sourceFile);
2649     createTestSymlink(existingDest, QFile::encodeName(sourceFile));
2650     QVERIFY(QFile::exists(existingDest));
2651     job = KIO::file_move(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(existingDest), -1, KIO::HideProgressInfo | KIO::Overwrite);
2652     job->setUiDelegate(nullptr);
2653     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2654     QVERIFY(!QFile::exists(sourceFile)); // it was moved
2655 
2656     // Now same thing when the target is a symlink to another file
2657     createTestFile(sourceFile);
2658     createTestFile(sourceFile + QLatin1Char('2'));
2659     createTestSymlink(existingDest, QFile::encodeName(sourceFile + QLatin1Char('2')));
2660     QVERIFY(QFile::exists(existingDest));
2661     job = KIO::file_move(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(existingDest), -1, KIO::HideProgressInfo | KIO::Overwrite);
2662     job->setUiDelegate(nullptr);
2663     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2664     QVERIFY(!QFile::exists(sourceFile)); // it was moved
2665 
2666     // Now same thing when the target is a _broken_ symlink
2667     createTestFile(sourceFile);
2668     createTestSymlink(existingDest);
2669     QVERIFY(!QFile::exists(existingDest)); // it exists, but it's broken...
2670     job = KIO::file_move(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(existingDest), -1, KIO::HideProgressInfo | KIO::Overwrite);
2671     job->setUiDelegate(nullptr);
2672     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2673     QVERIFY(!QFile::exists(sourceFile)); // it was moved
2674 #endif
2675 }
2676 
2677 void JobTest::moveOverSymlinkToSelf() // #169547
2678 {
2679 #ifndef Q_OS_WIN
2680     const QString sourceFile = homeTmpDir() + "fileFromHome";
2681     createTestFile(sourceFile);
2682     const QString existingDest = homeTmpDir() + "testlink";
2683     createTestSymlink(existingDest, QFile::encodeName(sourceFile));
2684     QVERIFY(QFile::exists(existingDest));
2685 
2686     KIO::CopyJob *job = KIO::move(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(existingDest), KIO::HideProgressInfo);
2687     job->setUiDelegate(nullptr);
2688     QVERIFY(!job->exec());
2689     QCOMPARE(job->error(), (int)KIO::ERR_FILE_ALREADY_EXIST); // and not ERR_IDENTICAL_FILES!
2690     QVERIFY(QFile::exists(sourceFile)); // it not moved
2691 #endif
2692 }
2693 
2694 void JobTest::createSymlink()
2695 {
2696 #ifdef Q_OS_WIN
2697     QSKIP("Test skipped on Windows");
2698 #endif
2699     const QString sourceFile = homeTmpDir() + "fileFromHome";
2700     createTestFile(sourceFile);
2701     const QString destDir = homeTmpDir() + "dest";
2702     QVERIFY(QDir().mkpath(destDir));
2703 
2704     ScopedCleaner cleaner([&] {
2705         QDir(destDir).removeRecursively();
2706     });
2707 
2708     // With KIO::link (high-level)
2709     KIO::CopyJob *job = KIO::link(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(destDir), KIO::HideProgressInfo);
2710     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2711     QVERIFY(QFileInfo::exists(sourceFile));
2712     const QString dest = destDir + "/fileFromHome";
2713     QVERIFY(QFileInfo(dest).isSymLink());
2714     QCOMPARE(QFileInfo(dest).symLinkTarget(), sourceFile);
2715     QFile::remove(dest);
2716 
2717     // With KIO::symlink (low-level)
2718     const QString linkPath = destDir + "/link";
2719     KIO::Job *symlinkJob = KIO::symlink(sourceFile, QUrl::fromLocalFile(linkPath), KIO::HideProgressInfo);
2720     QVERIFY2(symlinkJob->exec(), qPrintable(symlinkJob->errorString()));
2721     QVERIFY(QFileInfo::exists(sourceFile));
2722     QVERIFY(QFileInfo(linkPath).isSymLink());
2723     QCOMPARE(QFileInfo(linkPath).symLinkTarget(), sourceFile);
2724 }
2725 
2726 void JobTest::createSymlinkTargetDirDoesntExist()
2727 {
2728 #ifdef Q_OS_WIN
2729     QSKIP("Test skipped on Windows");
2730 #endif
2731     const QString sourceFile = homeTmpDir() + "fileFromHome";
2732     createTestFile(sourceFile);
2733     const QString destDir = homeTmpDir() + "dest/does/not/exist";
2734 
2735     KIO::CopyJob *job = KIO::link(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(destDir), KIO::HideProgressInfo);
2736     QVERIFY(!job->exec());
2737     QCOMPARE(job->error(), static_cast<int>(KIO::ERR_CANNOT_SYMLINK));
2738 }
2739 
2740 void JobTest::createSymlinkAsShouldSucceed()
2741 {
2742 #ifdef Q_OS_WIN
2743     QSKIP("Test skipped on Windows");
2744 #endif
2745     const QString sourceFile = homeTmpDir() + "fileFromHome";
2746     createTestFile(sourceFile);
2747     const QString dest = homeTmpDir() + "testlink";
2748     QFile::remove(dest); // just in case
2749 
2750     ScopedCleaner cleaner([&] {
2751         QVERIFY(QFile::remove(dest));
2752     });
2753 
2754     KIO::CopyJob *job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
2755     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2756     QVERIFY(QFileInfo::exists(sourceFile));
2757     QVERIFY(QFileInfo(dest).isSymLink());
2758 }
2759 
2760 void JobTest::createSymlinkAsShouldFailDirectoryExists()
2761 {
2762 #ifdef Q_OS_WIN
2763     QSKIP("Test skipped on Windows");
2764 #endif
2765     const QString sourceFile = homeTmpDir() + "fileFromHome";
2766     createTestFile(sourceFile);
2767     const QString dest = homeTmpDir() + "dest";
2768     QVERIFY(QDir().mkpath(dest)); // dest exists as a directory
2769 
2770     ScopedCleaner cleaner([&] {
2771         QVERIFY(QDir().rmdir(dest));
2772     });
2773 
2774     // With KIO::link (high-level)
2775     KIO::CopyJob *job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
2776     QVERIFY(!job->exec());
2777     QCOMPARE(job->error(), (int)KIO::ERR_DIR_ALREADY_EXIST);
2778     QVERIFY(QFileInfo::exists(sourceFile));
2779     QVERIFY(!QFileInfo::exists(dest + "/fileFromHome"));
2780 
2781     // With KIO::symlink (low-level)
2782     KIO::Job *symlinkJob = KIO::symlink(sourceFile, QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
2783     QVERIFY(!symlinkJob->exec());
2784     QCOMPARE(symlinkJob->error(), (int)KIO::ERR_DIR_ALREADY_EXIST);
2785     QVERIFY(QFileInfo::exists(sourceFile));
2786 }
2787 
2788 void JobTest::createSymlinkAsShouldFailFileExists()
2789 {
2790 #ifdef Q_OS_WIN
2791     QSKIP("Test skipped on Windows");
2792 #endif
2793     const QString sourceFile = homeTmpDir() + "fileFromHome";
2794     createTestFile(sourceFile);
2795     const QString dest = homeTmpDir() + "testlink";
2796     QFile::remove(dest); // just in case
2797 
2798     ScopedCleaner cleaner([&] {
2799         QVERIFY(QFile::remove(sourceFile));
2800         QVERIFY(QFile::remove(dest));
2801     });
2802 
2803     // First time works
2804     KIO::CopyJob *job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
2805     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2806     QVERIFY(QFileInfo(dest).isSymLink());
2807 
2808     // Second time fails (already exists)
2809     job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
2810     QVERIFY(!job->exec());
2811     QCOMPARE(job->error(), (int)KIO::ERR_FILE_ALREADY_EXIST);
2812 
2813     // KIO::symlink fails too
2814     KIO::Job *symlinkJob = KIO::symlink(sourceFile, QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
2815     QVERIFY(!symlinkJob->exec());
2816     QCOMPARE(symlinkJob->error(), (int)KIO::ERR_FILE_ALREADY_EXIST);
2817 }
2818 
2819 void JobTest::createSymlinkWithOverwriteShouldWork()
2820 {
2821 #ifdef Q_OS_WIN
2822     QSKIP("Test skipped on Windows");
2823 #endif
2824     const QString sourceFile = homeTmpDir() + "fileFromHome";
2825     createTestFile(sourceFile);
2826     const QString dest = homeTmpDir() + "testlink";
2827     QFile::remove(dest); // just in case
2828 
2829     ScopedCleaner cleaner([&] {
2830         QVERIFY(QFile::remove(sourceFile));
2831         QVERIFY(QFile::remove(dest));
2832     });
2833 
2834     // First time works
2835     KIO::CopyJob *job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
2836     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2837     QVERIFY(QFileInfo(dest).isSymLink());
2838 
2839     // Changing the link target, with overwrite, works
2840     job = KIO::linkAs(QUrl::fromLocalFile(sourceFile + QLatin1Char('2')), QUrl::fromLocalFile(dest), KIO::Overwrite | KIO::HideProgressInfo);
2841     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2842     QVERIFY(QFileInfo(dest).isSymLink());
2843     QCOMPARE(QFileInfo(dest).symLinkTarget(), QString(sourceFile + QLatin1Char('2')));
2844 
2845     // Changing the link target using KIO::symlink, with overwrite, works
2846     KIO::Job *symlinkJob = KIO::symlink(sourceFile + QLatin1Char('3'), QUrl::fromLocalFile(dest), KIO::Overwrite | KIO::HideProgressInfo);
2847     QVERIFY2(symlinkJob->exec(), qPrintable(symlinkJob->errorString()));
2848     QVERIFY(QFileInfo(dest).isSymLink());
2849     QCOMPARE(QFileInfo(dest).symLinkTarget(), QString(sourceFile + QLatin1Char('3')));
2850 }
2851 
2852 void JobTest::createBrokenSymlink()
2853 {
2854 #ifdef Q_OS_WIN
2855     QSKIP("Test skipped on Windows");
2856 #endif
2857     const QString sourceFile = "/does/not/exist";
2858     const QString dest = homeTmpDir() + "testlink";
2859     QFile::remove(dest); // just in case
2860     KIO::CopyJob *job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
2861     QVERIFY2(job->exec(), qPrintable(job->errorString()));
2862     QVERIFY(QFileInfo(dest).isSymLink());
2863 
2864     // Second time fails (already exists)
2865     job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
2866     QVERIFY(!job->exec());
2867     QCOMPARE(job->error(), (int)KIO::ERR_FILE_ALREADY_EXIST);
2868     QVERIFY(QFile::remove(dest));
2869 }
2870 
2871 void JobTest::cancelCopyAndCleanDest_data()
2872 {
2873     QTest::addColumn<bool>("suspend");
2874     QTest::addColumn<bool>("overwrite");
2875 
2876     QTest::newRow("suspend_no_overwrite") << true << false;
2877     QTest::newRow("no_suspend_no_overwrite") << false << false;
2878 
2879 #ifndef Q_OS_WIN
2880     QTest::newRow("suspend_with_overwrite") << true << true;
2881     QTest::newRow("no_suspend_with_overwrite") << false << true;
2882 #endif
2883 }
2884 
2885 void JobTest::cancelCopyAndCleanDest()
2886 {
2887     QFETCH(bool, suspend);
2888     QFETCH(bool, overwrite);
2889 
2890     const QString baseDir = homeTmpDir();
2891     const QString srcTemplate = baseDir + QStringLiteral("testfile_XXXXXX");
2892     const QString destFile = baseDir + QStringLiteral("testfile_copy_slow_") + QString::fromLatin1(QTest::currentDataTag());
2893 
2894     QTemporaryFile f(srcTemplate);
2895     if (!f.open()) {
2896         qFatal("Couldn't open %s", qPrintable(f.fileName()));
2897     }
2898 
2899     const int sz = 4000000; //~4MB
2900     f.seek(sz - 1);
2901     f.write("0");
2902     f.close();
2903     QCOMPARE(f.size(), sz);
2904 
2905     if (overwrite) {
2906         createTestFile(destFile);
2907     }
2908     const QString destToCheck = (overwrite) ? destFile + QStringLiteral(".part") : destFile;
2909 
2910     KIO::JobFlag m_overwriteFlag = overwrite ? KIO::Overwrite : KIO::DefaultFlags;
2911     KIO::FileCopyJob *copyJob = KIO::file_copy(QUrl::fromLocalFile(f.fileName()), QUrl::fromLocalFile(destFile), -1, KIO::HideProgressInfo | m_overwriteFlag);
2912     copyJob->setUiDelegate(nullptr);
2913     QSignalSpy spyProcessedSize(copyJob, &KIO::Job::processedSize);
2914     QSignalSpy spyFinished(copyJob, &KIO::Job::finished);
2915     connect(copyJob, &KIO::Job::processedSize, this, [destFile, suspend, destToCheck](KJob *job, qulonglong processedSize) {
2916         if (processedSize > 0) {
2917             QVERIFY2(QFile::exists(destToCheck), qPrintable(destToCheck));
2918             qDebug() << "processedSize=" << processedSize << "file size" << QFileInfo(destToCheck).size();
2919             if (suspend) {
2920                 job->suspend();
2921             }
2922             QVERIFY(job->kill());
2923         }
2924     });
2925 
2926     QVERIFY(!copyJob->exec());
2927     QCOMPARE(spyProcessedSize.count(), 1);
2928     QCOMPARE(spyFinished.count(), 1);
2929     QCOMPARE(copyJob->error(), KIO::ERR_USER_CANCELED);
2930 
2931     // the destination file actual deletion happens after finished() is emitted
2932     // we need to give some time to the KIO worker to finish the file cleaning
2933     QTRY_VERIFY2(!QFile::exists(destToCheck), qPrintable(destToCheck));
2934 }
2935 
2936 #include "moc_jobtest.cpp"