File indexing completed on 2024-04-21 16:30:21

0001 /*
0002    SPDX-FileCopyrightText: 2019-2020 Fabian Vogt <fabian@ritter-vogt.de>
0003    SPDX-FileCopyrightText: 2019-2020 Alexander Saoutkin <a.saoutkin@gmail.com>
0004    SPDX-License-Identifier: GPL-3.0-or-later
0005 */
0006 
0007 #include <fcntl.h>
0008 #include <sys/stat.h>
0009 #include <unistd.h>
0010 #include <errno.h>
0011 #include <dirent.h>
0012 
0013 #include <QProcess>
0014 #include <QStandardPaths>
0015 #include <QTemporaryDir>
0016 #include <QTemporaryFile>
0017 #include <QTest>
0018 #include <QtDBus/QDBusConnection>
0019 #include <QtDBus/QDBusReply>
0020 #include <QDebug>
0021 
0022 #include <KProtocolInfo>
0023 
0024 #include "kiofuse_interface.h"
0025 #include "kiofuseprivate_interface.h"
0026 
0027 class FileOpsTest : public QObject
0028 {
0029     Q_OBJECT
0030 
0031 private Q_SLOTS:
0032     void initTestCase();
0033     void cleanupTestCase();
0034 
0035     void testDBusErrorReply();
0036     void testLocalPathToRemoteUrl();
0037     void testLocalFileOps();
0038     void testLocalDirOps();
0039     void testReaddirOps();
0040     void testCreationOps();
0041     void testRenameOps();
0042     void testDeletionOps();
0043     void testArchiveOps();
0044     void testManWorkaround();
0045     void testKioErrorMapping();
0046     void testRootLookup();
0047     void testFilenameEscaping();
0048     void testDirRefresh();
0049     void testFileRefresh();
0050     void testSymlinkRefresh();
0051     void testTypeRefresh();
0052     void testDirSymlink();
0053     void testSymlinkRewrite();
0054 #ifdef WASTE_DISK_SPACE
0055     void testReadWrite4GBFile();
0056 #endif // WASTE_DISK_SPACE
0057 
0058 private:
0059     QDateTime roundDownToSecond(const QDateTime &dt);
0060     bool forceNodeTimeout();
0061     /** Unlike QFileInfo::symLinkTarget, which returns absolute paths only,
0062       * this returns the raw link content. On failure or truncation, a null
0063       * QString is returned instead. */
0064     QString readlink(const QString &symlink);
0065 
0066     org::kde::KIOFuse::VFS m_kiofuse_iface{QStringLiteral("org.kde.KIOFuse"),
0067                                            QStringLiteral("/org/kde/KIOFuse"),
0068                                            QDBusConnection::sessionBus()};
0069     org::kde::KIOFuse::Private m_kiofuseprivate_iface{QStringLiteral("org.kde.KIOFuse"),
0070                                                       QStringLiteral("/org/kde/KIOFuse"),
0071                                                       QDBusConnection::sessionBus()};
0072     QTemporaryDir m_mountDir;
0073 };
0074 
0075 void FileOpsTest::initTestCase()
0076 {
0077     // QTemporaryDir would otherwise rm -rf on destruction,
0078     // which is fatal if umount fails while something is mounted inside
0079     m_mountDir.setAutoRemove(false);
0080     QString programpath = QFINDTESTDATA("kio-fuse");
0081 
0082     QProcess kiofuseProcess;
0083     kiofuseProcess.setProgram(programpath);
0084 #ifdef TEST_CACHE_BASED_IO
0085     kiofuseProcess.setArguments(QStringList() << m_mountDir.path() << QStringLiteral("--disable-filejob-io"));
0086 #else
0087     kiofuseProcess.setArguments({m_mountDir.path()});
0088 #endif
0089     kiofuseProcess.setProcessChannelMode(QProcess::ForwardedChannels);
0090 
0091     kiofuseProcess.start();
0092     QVERIFY(kiofuseProcess.waitForFinished());
0093     QCOMPARE(kiofuseProcess.exitStatus(),  QProcess::NormalExit);
0094     QCOMPARE(kiofuseProcess.exitCode(), 0);
0095 }
0096 
0097 void FileOpsTest::cleanupTestCase()
0098 {
0099     QProcess unmountProcess;
0100     #ifdef Q_OS_FREEBSD
0101         // No fusermount on FreeBSD, use umount directly instead
0102         unmountProcess.start(QStringLiteral("umount"), {m_mountDir.path()});
0103     #else
0104         unmountProcess.start(QStringLiteral("fusermount3"), {QStringLiteral("-u"), m_mountDir.path()});
0105     #endif
0106 
0107     QVERIFY(unmountProcess.waitForFinished());
0108     QCOMPARE(unmountProcess.exitStatus(), QProcess::NormalExit);
0109     QCOMPARE(unmountProcess.exitCode(), 0);
0110 
0111     // Remove only after umounting suceeded
0112     m_mountDir.remove();
0113 }
0114 
0115 void FileOpsTest::testDBusErrorReply()
0116 {
0117     QDBusPendingReply<QString> reply = m_kiofuse_iface.mountUrl(QStringLiteral("invalid URL"));
0118     reply.waitForFinished();
0119     QVERIFY(reply.isError());
0120     QCOMPARE(reply.error().name(), QStringLiteral("org.kde.KIOFuse.VFS.Error.CannotMount"));
0121 
0122     reply = m_kiofuse_iface.mountUrl(QStringLiteral("http://www.kde.org"));
0123     reply.waitForFinished();
0124     QVERIFY(reply.isError());
0125     QCOMPARE(reply.error().name(), QStringLiteral("org.kde.KIOFuse.VFS.Error.SchemeNotSupported"));
0126 }
0127 
0128 void FileOpsTest::testLocalPathToRemoteUrl()
0129 {
0130     QDBusPendingReply<QString> errorReply;
0131     // mtp:/ -> Remote URL can't possibly be location of KIOFuse mount.
0132     // / -> Root can't possibly be location of KIOFuse mount.
0133     // m_mountDir -> Whilst this is in the KIOFuse mount, no remote URL exists for it
0134     for(const auto &url : {QStringLiteral("mtp:/"), QStringLiteral("/"), m_mountDir.path()})
0135     {
0136         errorReply = m_kiofuse_iface.remoteUrl(url);
0137         errorReply.waitForFinished();
0138         QVERIFY2(errorReply.isError(), qPrintable(url));
0139         QCOMPARE(errorReply.error().name(), QStringLiteral("org.kde.KIOFuse.VFS.Error.RemoteURLNotFound"));
0140     }
0141 
0142     QTemporaryFile localFile;
0143     QVERIFY(localFile.open());
0144     localFile.close(); // Force creation of file to avoid empty fileName()
0145     QString remoteUrl = QStringLiteral("file://%1").arg(localFile.fileName());
0146     QString reply = m_kiofuse_iface.mountUrl(remoteUrl).value();
0147     QVERIFY(!reply.isEmpty());
0148     QString calculatedRemoteUrl = m_kiofuse_iface.remoteUrl(reply).value();
0149     QCOMPARE(remoteUrl, calculatedRemoteUrl);
0150 }
0151 
0152 void FileOpsTest::testLocalFileOps()
0153 {
0154     QTemporaryFile localFile;
0155     QVERIFY(localFile.open());
0156 
0157     QCOMPARE(localFile.write("teststring"), 10);
0158     QVERIFY(localFile.flush());
0159 
0160     // Mount the temporary file
0161     QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localFile.fileName())).value();
0162     QVERIFY(!reply.isEmpty());
0163 
0164     // Doing the same again should work just fine
0165     reply = m_kiofuse_iface.mountUrl(localFile.fileName()).value();
0166     QVERIFY(!reply.isEmpty());
0167 
0168     QFile mirroredFile(reply);
0169     QVERIFY(mirroredFile.exists());
0170     QCOMPARE(mirroredFile.size(), localFile.size());
0171 
0172     // Compare file metadata
0173     QFileInfo localFileInfo(localFile),
0174               mirroredFileInfo(mirroredFile);
0175 
0176     QCOMPARE(mirroredFileInfo.size(), localFileInfo.size());
0177     QCOMPARE(mirroredFileInfo.ownerId(), localFileInfo.ownerId());
0178     QCOMPARE(mirroredFileInfo.groupId(), localFileInfo.groupId());
0179     // Not supported by KIO
0180     // QCOMPARE(mirroredFileInfo.metadataChangeTime(), localFileInfo.metadataChangeTime());
0181     // KIO does not expose times with sub-second precision
0182     QCOMPARE(mirroredFileInfo.lastModified(), roundDownToSecond(localFileInfo.lastModified()));
0183     QCOMPARE(mirroredFileInfo.lastRead(), roundDownToSecond(localFileInfo.lastRead()));
0184 
0185     QVERIFY(mirroredFile.open(QIODevice::ReadWrite));
0186     // Test touching the file
0187     struct timespec times[2] = {{time_t(localFileInfo.lastModified().toSecsSinceEpoch()) + 42, 0},
0188                                 {time_t(localFileInfo.lastRead().toSecsSinceEpoch()) + 1, 0}};
0189     QCOMPARE(futimens(mirroredFile.handle(), times), 0);
0190     localFileInfo.refresh();
0191     mirroredFileInfo.refresh();
0192     QCOMPARE(mirroredFileInfo.lastModified().toSecsSinceEpoch(), times[1].tv_sec);
0193     QCOMPARE(localFileInfo.lastModified().toSecsSinceEpoch(), times[1].tv_sec);
0194     // Access time not supported on the remote side, so only check in the mirror
0195     QCOMPARE(mirroredFileInfo.lastRead().toSecsSinceEpoch(), times[0].tv_sec);
0196     //QCOMPARE(localFileInfo.lastRead().toSecsSinceEpoch(), times[0].tv_sec);
0197 
0198     // Compare the content
0199     QVERIFY(localFile.seek(0));
0200     QCOMPARE(localFile.readAll(), mirroredFile.readAll());
0201 
0202     // Try again
0203     QVERIFY(localFile.seek(0));
0204     QVERIFY(mirroredFile.seek(0));
0205     QCOMPARE(localFile.readAll(), mirroredFile.readAll());
0206 
0207     // Again, but at an offset
0208     QVERIFY(localFile.seek(1));
0209     QVERIFY(mirroredFile.seek(1));
0210     QCOMPARE(localFile.readAll(), mirroredFile.readAll());
0211 
0212     // Write new data
0213     QVERIFY(mirroredFile.seek(0));
0214     QCOMPARE(mirroredFile.write(QStringLiteral("newteststring!").toUtf8()), 14);
0215     QVERIFY(mirroredFile.flush());
0216     // Flush the written contents into the backend
0217     QCOMPARE(fsync(mirroredFile.handle()), 0);
0218 
0219     // Currently, kio-fuse uses KIO::put and not KIO::write, so the file was replaced
0220     // instead of changed. So reopen the file.
0221     QFile localFile2(localFile.fileName());
0222     QVERIFY(localFile2.open(QIODevice::ReadOnly));
0223 
0224     // Compare the content
0225     QVERIFY(localFile2.seek(0));
0226     QVERIFY(mirroredFile.seek(0));
0227     QCOMPARE(localFile2.readAll(), mirroredFile.readAll());
0228 
0229     // Write new data, but close the file instead of flushing
0230     QVERIFY(mirroredFile.seek(0));
0231     QCOMPARE(mirroredFile.write(QStringLiteral("differentteststring").toUtf8()), 19);
0232     mirroredFile.close();
0233     localFile2.close();
0234     QVERIFY(localFile2.open(QIODevice::ReadOnly));
0235     QVERIFY(localFile2.seek(0));
0236     QVERIFY(mirroredFile.open(QIODevice::ReadWrite));
0237     QVERIFY(mirroredFile.seek(0));
0238     QCOMPARE(localFile2.readAll(), QStringLiteral("differentteststring").toUtf8());
0239 
0240     // Test truncation at open
0241     mirroredFile.close();
0242     QVERIFY(mirroredFile.open(QIODevice::WriteOnly | QIODevice::Truncate));
0243     QCOMPARE(mirroredFile.write(QStringLiteral("tststrng").toUtf8()), 8);
0244     QVERIFY(mirroredFile.flush());
0245     QCOMPARE(fsync(mirroredFile.handle()), 0); // Flush the written contents into the backend
0246 
0247     localFile2.close(); // Reopen the file, see above.
0248     QVERIFY(localFile2.open(QIODevice::ReadOnly));
0249     QCOMPARE(localFile2.readAll(), QStringLiteral("tststrng").toUtf8()); // Compare the content
0250 
0251     // Test manual truncation
0252     QCOMPARE(ftruncate(mirroredFile.handle(), 3), 0);
0253     QCOMPARE(fsync(mirroredFile.handle()), 0); // Flush the written contents into the backend
0254 
0255     localFile2.close(); // Reopen the file, see above.
0256     QVERIFY(localFile2.open(QIODevice::ReadOnly));
0257     QCOMPARE(localFile2.readAll(), QStringLiteral("tst").toUtf8()); // Compare the content
0258 
0259     // Test chown by not changing anything (no CAP_CHOWN...)
0260     QCOMPARE(chown(mirroredFile.fileName().toUtf8().data(), getuid(), getgid()), 0);
0261     localFileInfo.refresh();
0262     QCOMPARE(localFileInfo.ownerId(), getuid());
0263     QCOMPARE(localFileInfo.groupId(), getgid());
0264     // Should not be allowed
0265     QCOMPARE(chown(mirroredFile.fileName().toUtf8().data(), getuid(), 0), -1);
0266     QCOMPARE(chown(mirroredFile.fileName().toUtf8().data(), 0, getgid()), -1);
0267 
0268     // Test chmod
0269     QCOMPARE(chmod(mirroredFile.fileName().toUtf8().data(), 0054), 0);
0270     struct stat attr;
0271     QCOMPARE(stat(localFile.fileName().toUtf8().data(), &attr), 0);
0272     QCOMPARE(attr.st_mode, S_IFREG | 0054);
0273     QCOMPARE(chmod(mirroredFile.fileName().toUtf8().data(), 0600), 0);
0274     QCOMPARE(stat(localFile.fileName().toUtf8().data(), &attr), 0);
0275     QCOMPARE(attr.st_mode, S_IFREG | 0600);
0276 
0277     // Mount the data path
0278     QString dataPath = QFINDTESTDATA(QStringLiteral("data"));
0279     QVERIFY(!dataPath.isEmpty());
0280     reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(dataPath)).value();
0281     QVERIFY(!reply.isEmpty());
0282     QString mirrordataPath = reply;
0283 
0284     // Verify the symlink inside is correct
0285     QFile symlink(QDir(mirrordataPath).filePath(QStringLiteral("symlink")));
0286 
0287     QVERIFY(symlink.open(QIODevice::ReadOnly));
0288     QCOMPARE(symlink.readAll(), QStringLiteral("symlinktargetcontent").toUtf8());
0289     QCOMPARE(symlink.symLinkTarget(), QDir(mirrordataPath).filePath(QStringLiteral("symlinktarget")));
0290     
0291     // Verify that we adhere to O_APPEND flag as kernel doesn't handle this for us.
0292     QTemporaryFile appendFile;
0293     QVERIFY(appendFile.open());
0294     QCOMPARE(appendFile.write("teststring"), 10);
0295     QVERIFY(appendFile.flush());
0296     appendFile.close();
0297     // Mount the temp file
0298     reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(appendFile.fileName())).value();
0299     QVERIFY(!reply.isEmpty());
0300 
0301     QFile appendMirror(reply);
0302     QVERIFY(appendMirror.exists());
0303     QVERIFY(appendMirror.open(QIODevice::Append | QIODevice::ReadWrite));
0304     // even if we set seek to 0 kio-fuse should change it back to the end of the file.
0305     QVERIFY(appendMirror.seek(0));
0306     QCOMPARE(appendMirror.write("APPENDME"), 8);
0307     // Pass changes from mirror to local.
0308     QVERIFY(appendMirror.flush());
0309     QCOMPARE(fsync(appendMirror.handle()), 0);
0310     
0311     // Currently, kio-fuse uses KIO::put and not KIO::write, so the file was replaced
0312     // instead of changed. So reopen the file.
0313     QFile appendFile2(appendFile.fileName());
0314     QVERIFY(appendFile2.open(QIODevice::ReadOnly));
0315     QVERIFY(appendMirror.seek(0));
0316     QVERIFY(appendFile2.seek(0));
0317     // If we don't adhere to O_APPEND flag we'd get "APPENDMEng" instead...
0318     QCOMPARE(appendMirror.readAll(), QStringLiteral("teststringAPPENDME").toUtf8());
0319     QVERIFY(appendMirror.seek(0));
0320     QVERIFY(appendFile2.seek(0));
0321     QCOMPARE(appendMirror.readAll(), appendFile2.readAll());
0322 }
0323 
0324 void FileOpsTest::testLocalDirOps()
0325 {
0326     QTemporaryDir localDir;
0327     QVERIFY(localDir.isValid());
0328 
0329     // Mount the temporary dir
0330     QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value();
0331     QVERIFY(!reply.isEmpty());
0332 
0333     QDir mirrorDir(reply);
0334     QVERIFY(mirrorDir.exists());
0335 
0336     // Create a folder inside
0337     QVERIFY(mirrorDir.mkdir(QStringLiteral("directory")));
0338     QVERIFY(QFile::exists(localDir.filePath(QStringLiteral("directory"))));
0339 
0340     // Compare file metadata
0341     QFileInfo localDirInfo(localDir.path()),
0342               mirrorDirInfo(mirrorDir.path());
0343 
0344     QCOMPARE(mirrorDirInfo.ownerId(), localDirInfo.ownerId());
0345     QCOMPARE(mirrorDirInfo.groupId(), localDirInfo.groupId());
0346     // Not supported by KIO
0347     // QCOMPARE(mirroredFileInfo.metadataChangeTime(), localFileInfo.metadataChangeTime());
0348     // KIO does not expose times with sub-second precision
0349     QCOMPARE(mirrorDirInfo.lastModified(), roundDownToSecond(localDirInfo.lastModified()));
0350     QCOMPARE(mirrorDirInfo.lastRead(), roundDownToSecond(localDirInfo.lastRead()));
0351 
0352     // Test touching the file
0353     struct timespec times[2] = {{time_t(localDirInfo.lastModified().toSecsSinceEpoch()) + 42, 0},
0354                                 {time_t(localDirInfo.lastRead().toSecsSinceEpoch()) + 1, 0}};
0355     QCOMPARE(utimensat(AT_FDCWD, mirrorDir.path().toUtf8().data(), times, 0), 0);
0356     localDirInfo.refresh();
0357     mirrorDirInfo.refresh();
0358     QCOMPARE(mirrorDirInfo.lastModified().toSecsSinceEpoch(), times[1].tv_sec);
0359     QCOMPARE(localDirInfo.lastModified().toSecsSinceEpoch(), times[1].tv_sec);
0360     // Access time not supported on the remote side, so only check in the mirror
0361     QCOMPARE(mirrorDirInfo.lastRead().toSecsSinceEpoch(), times[0].tv_sec);
0362     //QCOMPARE(localDirInfo.lastRead().toSecsSinceEpoch(), times[0].tv_sec);
0363 
0364     // Test chown by not changing anything (no CAP_CHOWN...)
0365     QCOMPARE(chown(mirrorDir.path().toUtf8().data(), getuid(), getgid()), 0);
0366     localDirInfo.refresh();
0367     QCOMPARE(localDirInfo.ownerId(), getuid());
0368     QCOMPARE(localDirInfo.groupId(), getgid());
0369     // Should not be allowed
0370     QCOMPARE(chown(mirrorDir.path().toUtf8().data(), getuid(), 0), -1);
0371     QCOMPARE(chown(mirrorDir.path().toUtf8().data(), 0, getgid()), -1);
0372 
0373     // Test chmod
0374     QCOMPARE(chmod(mirrorDir.path().toUtf8().data(), 0054), 0);
0375     struct stat attr;
0376     QCOMPARE(stat(localDir.path().toUtf8().data(), &attr), 0);
0377     QCOMPARE(attr.st_mode, S_IFDIR | 0054);
0378     QCOMPARE(chmod(mirrorDir.path().toUtf8().data(), 0700), 0);
0379     QCOMPARE(stat(localDir.path().toUtf8().data(), &attr), 0);
0380     QCOMPARE(attr.st_mode, S_IFDIR | 0700);
0381 
0382     // Mount the data path and compare the directory content
0383     QString dataPath = QFINDTESTDATA(QStringLiteral("data"));
0384     QVERIFY(!dataPath.isEmpty());
0385     reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(dataPath)).value();
0386     QVERIFY(!reply.isEmpty());
0387     QString mirrordataPath = reply;
0388 
0389     auto sourceEntryList = QDir(dataPath).entryList(QDir::NoFilter, QDir::Name);
0390     auto mirrorEntryList = QDir(mirrordataPath).entryList(QDir::NoFilter, QDir::Name);
0391 
0392     QCOMPARE(mirrorEntryList, sourceEntryList);
0393 
0394     // Make sure dirlisting file:/// works
0395     sourceEntryList = QDir(QStringLiteral("/")).entryList(QDir::NoFilter, QDir::Name);
0396     reply = m_kiofuse_iface.mountUrl(QStringLiteral("file:///")).value();
0397     QVERIFY(!reply.isEmpty());
0398     mirrorEntryList = QDir(reply).entryList(QDir::NoFilter, QDir::Name);
0399 
0400     QCOMPARE(mirrorEntryList, sourceEntryList);
0401 }
0402 
0403 void FileOpsTest::testReaddirOps()
0404 {
0405     QTemporaryDir localDir;
0406     QVERIFY(localDir.isValid());
0407 
0408     // Mount the temporary dir
0409     QString testDirPath = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value();
0410     QVERIFY(!testDirPath.isEmpty());
0411 
0412     // Fill the directory with some files
0413     for(unsigned int i=0; i<=10; i++)
0414     {
0415         QVERIFY(QFile(QStringLiteral("%1/tmpFile%2").arg(testDirPath).arg(i)).open(QIODevice::WriteOnly));
0416     }
0417 
0418     DIR *testDir = opendir(qPrintable(testDirPath));
0419     QVERIFY(testDir);
0420 
0421     auto opendirCleanup = qScopeGuard([&](){ closedir(testDir); });
0422 
0423     QStringList testDirEntryList;
0424     QStringList testDirEntryListUpdated;
0425 
0426     // Get the initial entry list to compare with
0427     struct dirent *pDirent = nullptr; 
0428     while((pDirent = readdir(testDir)) != nullptr)
0429         testDirEntryList.push_back(QString::fromUtf8(pDirent->d_name));
0430     testDirEntryList.sort();
0431     
0432     // Verify that entries remain same even if we add new or remove existing entries
0433     QVERIFY(QFile::remove(testDirPath + QStringLiteral("/tmpFile1")));
0434     QVERIFY(QFile(testDirPath + QStringLiteral("/addCaseFile")).open(QIODevice::WriteOnly));
0435 
0436     rewinddir(testDir);
0437     while((pDirent = readdir(testDir)) != nullptr)
0438         testDirEntryListUpdated.push_back(QString::fromUtf8(pDirent->d_name));
0439     testDirEntryListUpdated.sort();
0440 
0441     QCOMPARE(testDirEntryListUpdated, testDirEntryList);
0442 
0443     // Verify that entries remain same even if entries are modified while iterating
0444     testDirEntryListUpdated.clear();
0445     rewinddir(testDir);
0446 
0447     unsigned int count = 1;
0448     while((pDirent = readdir(testDir)) != nullptr)
0449     {
0450         QVERIFY(QFile(QStringLiteral("%1/iterCaseFile%2").arg(testDirPath).arg(count)).open(QIODevice::WriteOnly));
0451             
0452         testDirEntryListUpdated.push_back(QString::fromUtf8(pDirent->d_name));
0453         count++;
0454     }
0455     testDirEntryListUpdated.sort();
0456 
0457     QCOMPARE(testDirEntryListUpdated, testDirEntryList);
0458 }
0459 
0460 void FileOpsTest::testCreationOps()
0461 {
0462     QTemporaryDir localDir;
0463     QVERIFY(localDir.isValid());
0464 
0465     // Mount the temporary dir
0466     QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value();
0467     QVERIFY(!reply.isEmpty());
0468 
0469     QDir mirrorDir(reply);
0470     QVERIFY(mirrorDir.exists());
0471 
0472     // Create a symlink
0473     QCOMPARE(symlink("target", mirrorDir.filePath(QStringLiteral("symlink")).toUtf8().data()), 0);
0474     QCOMPARE(QFileInfo(localDir.filePath(QStringLiteral("symlink"))).symLinkTarget(), localDir.filePath(QStringLiteral("target")));
0475 
0476     // Create a regular file
0477     QFile newFile(mirrorDir.filePath(QStringLiteral("newFile")));
0478     QVERIFY(newFile.open(QIODevice::ReadWrite));
0479 
0480     QFile newFileLocal(localDir.filePath(QStringLiteral("newFile")));
0481     QVERIFY(newFileLocal.exists());
0482     QCOMPARE(newFileLocal.size(), 0);
0483 
0484     QVERIFY(newFile.write(QStringLiteral("someweirdstring").toUtf8()));
0485     QVERIFY(newFile.flush());
0486     QCOMPARE(fsync(newFile.handle()), 0);
0487 
0488     // Reopen the file (see above in testLocalFileOps)
0489     newFileLocal.close();
0490     QVERIFY(newFileLocal.open(QIODevice::ReadOnly));
0491     QCOMPARE(newFileLocal.readAll(), QStringLiteral("someweirdstring").toUtf8());
0492 }
0493 
0494 void FileOpsTest::testRenameOps()
0495 {
0496     QTemporaryDir localDir;
0497     QVERIFY(localDir.isValid());
0498 
0499     // Mount the temporary dir
0500     QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value();
0501     QVERIFY(!reply.isEmpty());
0502 
0503     QDir mirrorDir(reply);
0504     QVERIFY(mirrorDir.exists());
0505 
0506     // Create a directory
0507     QVERIFY(QDir(mirrorDir.path()).mkdir(QStringLiteral("dira")));
0508     QDir dir(mirrorDir.filePath(QStringLiteral("dira")));
0509 
0510     // And a file inside
0511     QFile file(dir.filePath(QStringLiteral("filea")));
0512     QVERIFY(file.open(QIODevice::ReadWrite));
0513     QVERIFY(file.write(QStringLiteral("someweirdstring").toUtf8()));
0514 
0515     // Note: QFile::rename copies and unlinks if the rename syscall fails,
0516     // so use the libc function directly
0517 
0518     // Rename the file
0519     QCOMPARE(rename(dir.filePath(QStringLiteral("filea")).toUtf8().data(),
0520                     dir.filePath(QStringLiteral("fileb")).toUtf8().data()), 0);
0521     QVERIFY(!QFile::exists(dir.filePath(QStringLiteral("filea"))));
0522     QVERIFY(QFile::exists(dir.filePath(QStringLiteral("fileb"))));
0523     QVERIFY(!QFile::exists(localDir.filePath(QStringLiteral("dira/filea"))));
0524     QVERIFY(QFile::exists(localDir.filePath(QStringLiteral("dira/fileb"))));
0525 
0526     // Rename the directory
0527     QCOMPARE(rename(mirrorDir.filePath(QStringLiteral("dira")).toUtf8().data(),
0528                     mirrorDir.filePath(QStringLiteral("dirb")).toUtf8().data()), 0);
0529     QVERIFY(!QFile::exists(mirrorDir.filePath(QStringLiteral("dira"))));
0530     QVERIFY(QFile::exists(mirrorDir.filePath(QStringLiteral("dirb"))));
0531     QVERIFY(!QFile::exists(mirrorDir.filePath(QStringLiteral("dira"))));
0532     QVERIFY(QFile::exists(mirrorDir.filePath(QStringLiteral("dirb"))));
0533     QVERIFY(!QFile::exists(mirrorDir.filePath(QStringLiteral("dirb/filea"))));
0534     QVERIFY(QFile::exists(mirrorDir.filePath(QStringLiteral("dirb/fileb"))));
0535 
0536     // Verify that the file is still open and "connected"
0537     QVERIFY(file.write(QStringLiteral("!").toUtf8()));
0538     QVERIFY(file.flush());
0539     QCOMPARE(fsync(file.handle()), 0);
0540     QFile localFile(localDir.filePath(QStringLiteral("dirb/fileb")));
0541     QVERIFY(localFile.open(QIODevice::ReadOnly));
0542     QCOMPARE(localFile.readAll(), QStringLiteral("someweirdstring!").toUtf8());
0543 
0544     // Try the same, but overwriting an existing file
0545     QFile overwrittenFile(mirrorDir.filePath(QStringLiteral("dirb/filec")));
0546     QVERIFY(overwrittenFile.open(QIODevice::ReadWrite));
0547     QCOMPARE(overwrittenFile.write(QStringLiteral("data").toUtf8()), 4);
0548     QVERIFY(overwrittenFile.flush());
0549 #ifdef RENAME_NOREPLACE
0550     QCOMPARE(renameat2(AT_FDCWD, mirrorDir.filePath(QStringLiteral("dirb/fileb")).toUtf8().data(),
0551                        AT_FDCWD, mirrorDir.filePath(QStringLiteral("dirb/filec")).toUtf8().data(),
0552                        RENAME_NOREPLACE), -1);
0553     QCOMPARE(errno, EEXIST);
0554 #endif
0555 
0556     QCOMPARE(rename(mirrorDir.filePath(QStringLiteral("dirb/fileb")).toUtf8().data(),
0557                     mirrorDir.filePath(QStringLiteral("dirb/filec")).toUtf8().data()), 0);
0558     QVERIFY(!QFile::exists(localDir.filePath(QStringLiteral("dirb/fileb"))));
0559     QVERIFY(QFile::exists(localDir.filePath(QStringLiteral("dirb/filec"))));
0560     QVERIFY(!QFile::exists(mirrorDir.filePath(QStringLiteral("dirb/fileb"))));
0561     QVERIFY(QFile::exists(mirrorDir.filePath(QStringLiteral("dirb/filec"))));
0562 
0563     QVERIFY(overwrittenFile.seek(0));
0564 #ifdef TEST_CACHE_BASED_IO
0565     // Both handles must still be valid
0566     QCOMPARE(overwrittenFile.readAll(), QStringLiteral("data").toUtf8());
0567 #else
0568     // Doesn't apply to FileJob (KIO::open) I/O
0569 #endif
0570 
0571     localFile.close();
0572     localFile.setFileName(localDir.filePath(QStringLiteral("dirb/filec")));
0573     QVERIFY(localFile.open(QIODevice::ReadOnly));
0574     QCOMPARE(localFile.readAll(), QStringLiteral("someweirdstring!").toUtf8());
0575 }
0576 
0577 void FileOpsTest::testDeletionOps()
0578 {
0579     QTemporaryDir localDir;
0580     QVERIFY(localDir.isValid());
0581 
0582     // Mount the temporary dir
0583     QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value();
0584     QVERIFY(!reply.isEmpty());
0585 
0586     QDir mirrorDir(reply);
0587     QVERIFY(mirrorDir.exists());
0588 
0589     // Create a directory
0590     QVERIFY(QDir(mirrorDir.path()).mkdir(QStringLiteral("dir")));
0591     QDir dir(mirrorDir.filePath(QStringLiteral("dir")));
0592 
0593     // And a file inside
0594     QFile file(dir.filePath(QStringLiteral("file")));
0595     QVERIFY(file.open(QIODevice::ReadWrite));
0596     QVERIFY(file.write(QStringLiteral("someweirdstring").toUtf8()));
0597     QVERIFY(file.flush());
0598 
0599     // Try to delete the directory
0600     QCOMPARE(unlink(dir.path().toUtf8().data()), -1);
0601     #ifdef Q_OS_LINUX
0602         QCOMPARE(errno, EISDIR);
0603     #else
0604         QCOMPARE(errno, EPERM);
0605     #endif
0606     QCOMPARE(rmdir(dir.path().toUtf8().data()), -1);
0607     QCOMPARE(errno, ENOTEMPTY);
0608 
0609 #ifdef TEST_CACHE_BASED_IO
0610     // Delete the file
0611     QCOMPARE(rmdir(file.fileName().toUtf8().data()), -1);
0612     QCOMPARE(errno, ENOTDIR);
0613     QCOMPARE(unlink(file.fileName().toUtf8().data()), 0);
0614     QVERIFY(!file.exists());
0615     QVERIFY(!QFile::exists(localDir.filePath(QStringLiteral("dir/file"))));
0616 
0617     // Make sure it's still open
0618     QVERIFY(file.seek(0));
0619     QCOMPARE(file.readAll(), QStringLiteral("someweirdstring").toUtf8());
0620 
0621     // Delete the now empty directory
0622     QCOMPARE(rmdir(dir.path().toUtf8().data()), 0);
0623     QVERIFY(!dir.exists());
0624     QVERIFY(!QFile::exists(localDir.filePath(QStringLiteral("dir"))));
0625 
0626     // Make sure the file is still open
0627     QVERIFY(file.seek(0));
0628     QCOMPARE(file.readAll(), QStringLiteral("someweirdstring").toUtf8());
0629 #else
0630     // FileJob-based nodes only unlink if the file isn't open
0631     QCOMPARE(rmdir(file.fileName().toUtf8().data()), -1);
0632     QCOMPARE(errno, ENOTDIR);
0633     QCOMPARE(unlink(file.fileName().toUtf8().data()), -1);
0634     file.close();
0635     QCOMPARE(unlink(file.fileName().toUtf8().data()), 0);
0636     QVERIFY(!file.exists());
0637     QVERIFY(!QFile::exists(localDir.filePath(QStringLiteral("dir/file"))));
0638 #endif
0639 
0640 
0641     // Not implemented: Link the file back into the tree, if possible
0642     // QCOMPARE(link(QStringLiteral("/proc/self/fd/%1").arg(file.handle()).toUtf8().data(),
0643     //              mirrorDir.filePath(QStringLiteral("deletedFile")).toUtf8().data()), 0);
0644     // ... test that the file is still open and connected.
0645 }
0646 
0647 void FileOpsTest::testArchiveOps()
0648 {
0649     if (!KProtocolInfo::isKnownProtocol(QStringLiteral("tar")))
0650         QSKIP("Test requires tar protocol to be supported. See README for packages required.");
0651 
0652     QString outerpath = QFINDTESTDATA(QStringLiteral("data/outerarchive.tar.gz"));
0653 
0654     // Mount a file inside the archive
0655     QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("tar://%1/outerarchive/outerfile").arg(outerpath)).value();
0656     QVERIFY(!reply.isEmpty());
0657 
0658     // And verify its content
0659     QString outerfilepath = reply;
0660     QFile outerfile(outerfilepath);
0661     QVERIFY(outerfile.open(QIODevice::ReadOnly));
0662     QCOMPARE(outerfile.readAll(), QStringLiteral("outercontent").toUtf8());
0663 
0664     reply = m_kiofuse_iface.mountUrl(QStringLiteral("tar://%1/outerarchive/innerarchive.tar.gz").arg(outerpath)).value();
0665     QVERIFY(!reply.isEmpty());
0666     QString innerpath = reply;
0667 
0668     // Unfortunately kio_archive is not reentrant, so a direct access would deadlock.
0669     // As a workaround, cache the file to avoid a 2nd call into kio_archive.
0670     QFile innerarchiveFile(innerpath);
0671     QVERIFY(innerarchiveFile.open(QIODevice::ReadOnly));
0672     QVERIFY(!innerarchiveFile.readAll().isEmpty());
0673 
0674     // Next, mount an archive inside - this uses kio-fuse recursively
0675     reply = m_kiofuse_iface.mountUrl(QStringLiteral("tar://%1").arg(innerpath)).value();
0676     QVERIFY(!reply.isEmpty());
0677 
0678     QFile innerfile(QStringLiteral("%1/innerarchive/innerfile").arg(reply));
0679     QVERIFY(innerfile.open(QIODevice::ReadOnly));
0680     QCOMPARE(innerfile.readAll(), QStringLiteral("innercontent").toUtf8());
0681     innerfile.close();
0682 }
0683 
0684 void FileOpsTest::testManWorkaround()
0685 {
0686     // The man ioworker has "hybrid" directories which stat as regular files but also support
0687     // listDir. This behaviour is not supported and mounting has to fail.
0688 
0689     if (!KProtocolInfo::isKnownProtocol(QStringLiteral("man")))
0690         QSKIP("Test requires man protocol to be supported. See README for packages required.");
0691 
0692     QDBusPendingReply<QString> reply = m_kiofuse_iface.mountUrl(QStringLiteral("man:foo"));
0693     reply.waitForFinished();
0694     QVERIFY(reply.isError());
0695     QCOMPARE(reply.error().name(), QStringLiteral("org.kde.KIOFuse.VFS.Error.CannotMount"));
0696 
0697     reply = m_kiofuse_iface.mountUrl(QStringLiteral("man:/"));
0698     reply.waitForFinished();
0699     QVERIFY(reply.isError());
0700     QCOMPARE(reply.error().name(), QStringLiteral("org.kde.KIOFuse.VFS.Error.CannotMount"));
0701 }
0702 
0703 void FileOpsTest::testKioErrorMapping()
0704 {
0705     QTemporaryFile localFile;
0706     QVERIFY(localFile.open());
0707     
0708     // Mount the temporary file
0709     QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localFile.fileName())).value();
0710     QVERIFY(!reply.isEmpty());
0711     
0712     QFile mirroredFile(reply);
0713     QVERIFY(mirroredFile.exists());
0714     QVERIFY(mirroredFile.open(QIODevice::ReadWrite));
0715     QCOMPARE(mirroredFile.size(), localFile.size());
0716     // No permission to chown to root/root (unless running with CAP_CHOWN or being root)
0717     QCOMPARE(chown(mirroredFile.fileName().toUtf8().data(), 0, 0), -1);
0718     QCOMPARE(errno, EPERM);
0719 }
0720 
0721 void FileOpsTest::testRootLookup()
0722 {
0723     struct stat st;
0724     // Verify that it does not exist...
0725     QCOMPARE(stat(qPrintable(QStringLiteral("%1/invalid").arg(m_mountDir.path())), &st), -1);
0726     // ... and set errno correctly
0727     QCOMPARE(errno, ENOENT);
0728 }
0729 
0730 void FileOpsTest::testFilenameEscaping()
0731 {
0732     QTemporaryDir localDir;
0733     QVERIFY(localDir.isValid());
0734 
0735     // Mount the temporary dir
0736     QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value();
0737     QVERIFY(!reply.isEmpty());
0738 
0739     QDir mirrorDir(reply);
0740     QVERIFY(mirrorDir.exists());
0741 
0742     // Create a file in localDir with an "unusual" filename
0743     for(const QString &name : {QStringLiteral("file0?name"),
0744         QStringLiteral("file1#name"), QStringLiteral("file2%20name"),
0745         QStringLiteral("file2 \nname?asdf&foo#bar")})
0746     {
0747         QFile localFile(localDir.filePath(name));
0748         QVERIFY(localFile.open(QFile::WriteOnly));
0749         QCOMPARE(localFile.write("teststring", 10), 10);
0750         localFile.close();
0751 
0752         QFile mirrorFile(mirrorDir.filePath(name));
0753         QVERIFY2(mirrorFile.open(QFile::ReadOnly), name.toUtf8().data());
0754         QCOMPARE(mirrorFile.readAll(), QStringLiteral("teststring").toUtf8());
0755     }
0756 }
0757 
0758 void FileOpsTest::testDirRefresh()
0759 {
0760     QTemporaryDir localDir;
0761     QVERIFY(localDir.isValid());
0762 
0763     // Mount the temporary dir
0764     QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value();
0765     QVERIFY(!reply.isEmpty());
0766 
0767     QDir mirrorDir(reply);
0768     QVERIFY(mirrorDir.exists());
0769 
0770     // readdir must not have any content yet
0771     QCOMPARE(mirrorDir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot).count(), 0);
0772 
0773     QFile newFile(localDir.filePath(QStringLiteral("newFile")));
0774     QVERIFY(newFile.open(QFile::ReadWrite));
0775 
0776     // Verify that the file is part of a dirlist after refresh
0777     QCOMPARE(mirrorDir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot).count(), 0);
0778     QVERIFY(forceNodeTimeout());
0779     QCOMPARE(mirrorDir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot),
0780              QStringList{QStringLiteral("newFile")});
0781 
0782     // Delete the file
0783     newFile.close();
0784     QVERIFY(newFile.remove());
0785 
0786     // Verify that it disappears from the dirlist after refresh
0787     QCOMPARE(mirrorDir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot), QStringList{QStringLiteral("newFile")});
0788     QVERIFY(forceNodeTimeout());
0789     QCOMPARE(mirrorDir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot).count(), 0);
0790 
0791     // Recreate the file
0792     QVERIFY(newFile.open(QFile::ReadWrite));
0793 
0794     // Verify that access is immediately possible again (lookup is "optimistic")
0795     QVERIFY(QFile::exists(mirrorDir.filePath(QStringLiteral("newFile"))));
0796 
0797     // Delete the file again
0798     newFile.close();
0799     QVERIFY(newFile.remove());
0800 
0801     // Verify that after a refresh it's dropped
0802     QVERIFY(forceNodeTimeout());
0803     QVERIFY(!QFile::exists(mirrorDir.filePath(QStringLiteral("newFile"))));
0804 }
0805 
0806 void FileOpsTest::testFileRefresh()
0807 {
0808     QTemporaryDir localDir;
0809     QVERIFY(localDir.isValid());
0810 
0811     // Mount the temporary dir
0812     QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value();
0813     QVERIFY(!reply.isEmpty());
0814 
0815     QDir mirrorDir(reply);
0816     QVERIFY(mirrorDir.exists());
0817 
0818     // readdir must not have any content yet
0819     QCOMPARE(mirrorDir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot).count(), 0);
0820 
0821     QFile localFile(localDir.filePath(QStringLiteral("newFile")));
0822     QVERIFY(localFile.open(QFile::ReadWrite));
0823 
0824     QFile mirrorFile(mirrorDir.filePath(QStringLiteral("newFile")));
0825     QVERIFY(mirrorFile.open(QFile::ReadOnly));
0826     QCOMPARE(mirrorFile.size(), 0); // File is empty
0827     QCOMPARE(mirrorFile.readAll(), QByteArray{});
0828     QVERIFY(mirrorFile.permissions() & QFile::ReadOther); // Has default perms
0829 
0830     QCOMPARE(localFile.write("teststring", 10), 10); // Write some data
0831     QVERIFY(localFile.flush());
0832     QVERIFY(localFile.setPermissions(localFile.permissions() & ~QFile::ReadOther)); // Change perms
0833     QCOMPARE(mirrorFile.size(), 0); // File is still empty
0834     QVERIFY(forceNodeTimeout());
0835 
0836     // Without reopening, it has the new content and perms now
0837     QCOMPARE(mirrorFile.size(), 10);
0838     QCOMPARE(mirrorFile.readAll(), QStringLiteral("teststring").toUtf8());
0839     QCOMPARE(mirrorFile.permissions() & QFile::ReadOther, 0); // Has changed perms
0840 }
0841 
0842 void FileOpsTest::testSymlinkRefresh()
0843 {
0844     QTemporaryDir localDir;
0845     QVERIFY(localDir.isValid());
0846 
0847     // Mount the temporary dir
0848     QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value();
0849     QVERIFY(!reply.isEmpty());
0850 
0851     QDir mirrorDir(reply);
0852     QVERIFY(mirrorDir.exists());
0853 
0854     // Create a symlink
0855     QCOMPARE(symlink("oldtarget", localDir.filePath(QStringLiteral("symlink")).toUtf8().data()), 0);
0856     QCOMPARE(readlink(mirrorDir.filePath(QStringLiteral("symlink"))), QStringLiteral("oldtarget"));
0857 
0858     // Change the symlink
0859     QVERIFY(QFile::remove(localDir.filePath((QStringLiteral("symlink")))));
0860     QCOMPARE(symlink("newtarget", localDir.filePath(QStringLiteral("symlink")).toUtf8().data()), 0);
0861 
0862     QVERIFY(forceNodeTimeout());
0863 
0864     QCOMPARE(readlink(mirrorDir.filePath(QStringLiteral("symlink"))), QStringLiteral("newtarget"));
0865 }
0866 
0867 void FileOpsTest::testTypeRefresh()
0868 {
0869     QTemporaryDir localDir;
0870     QVERIFY(localDir.isValid());
0871 
0872     // Mount the temporary dir
0873     QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value();
0874     QVERIFY(!reply.isEmpty());
0875 
0876     QDir mirrorDir(reply);
0877     QVERIFY(mirrorDir.exists());
0878 
0879     // Create a file and directory
0880     QFile localFile(localDir.filePath(QStringLiteral("changingtodir")));
0881     QVERIFY(localFile.open(QFile::ReadWrite));
0882     QVERIFY(QDir(localDir.path()).mkdir(QStringLiteral("changingtofile")));
0883 
0884     // Open it on the mirror
0885     QFile changingMirrorFile(mirrorDir.filePath(QStringLiteral("changingtodir")));
0886     QVERIFY(changingMirrorFile.open(QFile::ReadOnly));
0887 
0888     // Replace the file locally  with a directory
0889     QVERIFY(localFile.remove());
0890     QVERIFY(QDir(localDir.path()).mkdir(QStringLiteral("changingtodir")));
0891 
0892     QVERIFY(forceNodeTimeout());
0893 
0894     // Verify that it's a directory now
0895     struct stat st;
0896     QCOMPARE(stat(qPrintable(changingMirrorFile.fileName()), &st), 0);
0897     QCOMPARE(st.st_mode & S_IFMT, S_IFDIR);
0898 
0899     // The opened file still refers to the (now deleted) file
0900     QCOMPARE(fstat(changingMirrorFile.handle(), &st), 0);
0901     QCOMPARE(st.st_mode & S_IFMT, S_IFREG);
0902 }
0903 
0904 void FileOpsTest::testDirSymlink()
0905 {
0906     QTemporaryDir localTmpDir;
0907     QVERIFY(localTmpDir.isValid());
0908     QDir localDir(localTmpDir.path());
0909 
0910     // Mount the temporary dir
0911     QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value();
0912     QVERIFY(!reply.isEmpty());
0913 
0914     QDir mirrorDir(reply);
0915     QVERIFY(mirrorDir.exists());
0916 
0917     // Create a directory (with a dir inside) and a symlink to the parent
0918     QVERIFY(mirrorDir.mkpath(QStringLiteral("realdir/child")));
0919     QCOMPARE(symlink("realdir/../realdir",
0920                      qPrintable(mirrorDir.filePath(QStringLiteral("linktodir")))), 0);
0921     // Verify that the link is saved as-is
0922     QCOMPARE(readlink(mirrorDir.filePath(QStringLiteral("linktodir"))),
0923              QStringLiteral("realdir/../realdir"));
0924 
0925     // Verify that it was correctly created everywhere
0926     QVERIFY(mirrorDir.exists(QStringLiteral("linktodir/child")));
0927     QVERIFY(localDir.exists(QStringLiteral("linktodir/child")));
0928     QVERIFY(localDir.exists(QStringLiteral("realdir/child")));
0929 
0930     // Verify that remoteUrl contains the exact path
0931     auto remoteUrlReply = m_kiofuse_iface.remoteUrl(mirrorDir.filePath(QStringLiteral("linktodir/child")));
0932     remoteUrlReply.waitForFinished();
0933     QVERIFY(!remoteUrlReply.isError());
0934     QCOMPARE(QUrl{remoteUrlReply.value()}, QUrl::fromLocalFile(localDir.filePath(QStringLiteral("linktodir/child"))));
0935 
0936     // Verify that the child can be mounted through linktodir
0937     auto mountReply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.filePath(QStringLiteral("linktodir/child"))));
0938     mountReply.waitForFinished();
0939     QVERIFY(!mountReply.isError());
0940 }
0941 
0942 void FileOpsTest::testSymlinkRewrite()
0943 {
0944     QTemporaryDir localTmpDir;
0945     QVERIFY(localTmpDir.isValid());
0946     QDir localDir(localTmpDir.path());
0947 
0948     // Mount the temporary dir
0949     QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value();
0950     QVERIFY(!reply.isEmpty());
0951 
0952     QDir mirrorDir(reply);
0953     QVERIFY(mirrorDir.exists());
0954 
0955     // Create a symlink /mnt/file/.../symlink -> /mnt/file/.../somedir/../somefile.
0956     // This is to test that even if the target does not exist and is some convoluted
0957     // path, it is still rewritten correctly.
0958     QCOMPARE(symlink(qPrintable(mirrorDir.filePath(QStringLiteral("somedir/../somefile"))),
0959                      qPrintable(mirrorDir.filePath(QStringLiteral("symlink")))), 0);
0960     // Verify that it can be read back as-is on the mount
0961     QCOMPARE(readlink(mirrorDir.filePath(QStringLiteral("symlink"))),
0962              mirrorDir.filePath(QStringLiteral("somedir/../somefile")));
0963 
0964     // Verify that it's absolute on the local side
0965     QCOMPARE(readlink(localDir.filePath(QStringLiteral("symlink"))),
0966              localDir.filePath(QStringLiteral("somedir/../somefile")));
0967 
0968     if (!KProtocolInfo::isKnownProtocol(QStringLiteral("tar")))
0969         QSKIP("Test requires tar protocol to be supported. See README for packages required.");
0970 
0971     // Mount something with a different origin
0972     QString outerpath = QFINDTESTDATA(QStringLiteral("data/outerarchive.tar.gz"));
0973     reply = m_kiofuse_iface.mountUrl(QStringLiteral("tar://%1/outerarchive/").arg(outerpath)).value();
0974     QVERIFY(!reply.isEmpty());
0975     QDir archiveDir(reply);
0976     QVERIFY(archiveDir.exists());
0977 
0978     // Create a symlink /mnt/file/.../symlink -> /mnt/tar/.../outerarchive/somewhere
0979     // which can't be rewritten properly
0980     QCOMPARE(symlink(qPrintable(archiveDir.filePath(QStringLiteral("somewhere"))),
0981                      qPrintable(mirrorDir.filePath(QStringLiteral("symlink2")))), 0);
0982     // Verify that it didn't get rewritten during write
0983     QCOMPARE(readlink(localDir.filePath(QStringLiteral("symlink2"))),
0984              archiveDir.filePath(QStringLiteral("somewhere")));
0985     // If rewriting fails, it'll keep it as-is, but the next read will rewrite
0986     // it in the other direction again, i.e. /mnt/file/mnt/tar/...
0987     reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(archiveDir.path())).value();
0988     QVERIFY(!reply.isEmpty());
0989 
0990     QCOMPARE(readlink(mirrorDir.filePath(QStringLiteral("symlink2"))),
0991              QDir(reply).filePath(QStringLiteral("somewhere")));
0992 }
0993 
0994 #ifdef WASTE_DISK_SPACE
0995 void FileOpsTest::testReadWrite4GBFile()
0996 {
0997     QTemporaryFile localFile;
0998     QVERIFY(localFile.open());
0999 
1000     // Mount the temporary file
1001     QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localFile.fileName())).value();
1002     QVERIFY(!reply.isEmpty());
1003 
1004     QFile mirroredFile(reply);
1005     QVERIFY(mirroredFile.exists());
1006     QVERIFY(mirroredFile.open(QIODevice::ReadWrite));
1007 
1008     // Write new data at a 2^32 offset
1009     QVERIFY(mirroredFile.seek(qint64(4096)*1024*1024));
1010     QCOMPARE(mirroredFile.write(QStringLiteral("newteststring!").toUtf8()), 14);
1011 
1012     QVERIFY(mirroredFile.flush());
1013     // Flush the written contents into the backend
1014     QCOMPARE(fsync(mirroredFile.handle()), 0);
1015 
1016     // Currently, kio-fuse uses KIO::put and not KIO::write, so the file was replaced
1017     // instead of changed. So reopen the file.
1018     QFile localFile2(localFile.fileName());
1019     QVERIFY(localFile2.open(QIODevice::ReadOnly));;
1020 
1021     // Compare the content
1022     QVERIFY(localFile2.seek(qint64(4096)*1024*1024-6));
1023     QCOMPARE(localFile2.read(20), QByteArray("\x00\x00\x00\x00\x00\x00newteststring!", 20));
1024     QVERIFY(localFile2.seek(qint64(4096)*4096*1024-6));
1025     QCOMPARE(localFile2.read(20), QByteArray());
1026     QVERIFY(localFile2.seek(qint64(4096)*1024*1024-6));
1027     QVERIFY(mirroredFile.seek(qint64(4096)*1024*1024-6));
1028     QCOMPARE(localFile2.read(20), mirroredFile.read(20));
1029 }
1030 #endif // WASTE_DISK_SPACE
1031 
1032 QDateTime FileOpsTest::roundDownToSecond(const QDateTime &dt)
1033 {
1034     return dt.addMSecs(-dt.time().msec());
1035 }
1036 
1037 bool FileOpsTest::forceNodeTimeout()
1038 {
1039     auto reply = m_kiofuseprivate_iface.forceNodeTimeout();
1040     reply.waitForFinished();
1041     return !reply.isError();
1042 }
1043 
1044 QString FileOpsTest::readlink(const QString &symlink)
1045 {
1046     char buf[PATH_MAX];
1047     int len = ::readlink(qPrintable(symlink), buf, sizeof(buf));
1048     if(len < 0 || len >= int(sizeof(buf))) // Failed or truncated?
1049         return {}; // Return a null QString
1050 
1051     return QString::fromLocal8Bit(buf, len);
1052 }
1053 
1054 QTEST_GUILESS_MAIN(FileOpsTest)
1055 
1056 #include "fileopstest.moc"