/* SPDX-FileCopyrightText: 2019-2020 Fabian Vogt SPDX-FileCopyrightText: 2019-2020 Alexander Saoutkin SPDX-License-Identifier: GPL-3.0-or-later */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kiofuse_interface.h" #include "kiofuseprivate_interface.h" class FileOpsTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void testDBusErrorReply(); void testLocalPathToRemoteUrl(); void testLocalFileOps(); void testLocalDirOps(); void testReaddirOps(); void testCreationOps(); void testRenameOps(); void testDeletionOps(); void testArchiveOps(); void testManWorkaround(); void testKioErrorMapping(); void testRootLookup(); void testFilenameEscaping(); void testDirRefresh(); void testFileRefresh(); void testSymlinkRefresh(); void testTypeRefresh(); void testDirSymlink(); void testSymlinkRewrite(); #ifdef WASTE_DISK_SPACE void testReadWrite4GBFile(); #endif // WASTE_DISK_SPACE private: QDateTime roundDownToSecond(QDateTime dt); bool forceNodeTimeout(); /** Unlike QFileInfo::symLinkTarget, which returns absolute paths only, * this returns the raw link content. On failure or truncation, a null * QString is returned instead. */ QString readlink(QString symlink); org::kde::KIOFuse::VFS m_kiofuse_iface{QStringLiteral("org.kde.KIOFuse"), QStringLiteral("/org/kde/KIOFuse"), QDBusConnection::sessionBus()}; org::kde::KIOFuse::Private m_kiofuseprivate_iface{QStringLiteral("org.kde.KIOFuse"), QStringLiteral("/org/kde/KIOFuse"), QDBusConnection::sessionBus()}; QTemporaryDir m_mountDir; }; void FileOpsTest::initTestCase() { // QTemporaryDir would otherwise rm -rf on destruction, // which is fatal if umount fails while something is mounted inside m_mountDir.setAutoRemove(false); QString programpath = QFINDTESTDATA("kio-fuse"); QProcess kiofuseProcess; kiofuseProcess.setProgram(programpath); #ifdef TEST_CACHE_BASED_IO kiofuseProcess.setArguments(QStringList() << m_mountDir.path() << QStringLiteral("--disable-filejob-io")); #else kiofuseProcess.setArguments({m_mountDir.path()}); #endif kiofuseProcess.setProcessChannelMode(QProcess::ForwardedChannels); kiofuseProcess.start(); QVERIFY(kiofuseProcess.waitForFinished()); QCOMPARE(kiofuseProcess.exitStatus(), QProcess::NormalExit); QCOMPARE(kiofuseProcess.exitCode(), 0); } void FileOpsTest::cleanupTestCase() { QProcess unmountProcess; #ifdef Q_OS_FREEBSD // No fusermount on FreeBSD, use umount directly instead unmountProcess.start(QStringLiteral("umount"), {m_mountDir.path()}); #else unmountProcess.start(QStringLiteral("fusermount3"), {QStringLiteral("-u"), m_mountDir.path()}); #endif QVERIFY(unmountProcess.waitForFinished()); QCOMPARE(unmountProcess.exitStatus(), QProcess::NormalExit); QCOMPARE(unmountProcess.exitCode(), 0); // Remove only after umounting suceeded m_mountDir.remove(); } void FileOpsTest::testDBusErrorReply() { QDBusPendingReply reply = m_kiofuse_iface.mountUrl(QStringLiteral("invalid URL")); reply.waitForFinished(); QVERIFY(reply.isError()); QCOMPARE(reply.error().name(), QStringLiteral("org.kde.KIOFuse.VFS.Error.CannotMount")); reply = m_kiofuse_iface.mountUrl(QStringLiteral("http://www.kde.org")); reply.waitForFinished(); QVERIFY(reply.isError()); QCOMPARE(reply.error().name(), QStringLiteral("org.kde.KIOFuse.VFS.Error.SchemeNotSupported")); } void FileOpsTest::testLocalPathToRemoteUrl() { QDBusPendingReply errorReply; // mtp:/ -> Remote URL can't possibly be location of KIOFuse mount. // / -> Root can't possibly be location of KIOFuse mount. // m_mountDir -> Whilst this is in the KIOFuse mount, no remote URL exists for it for(auto url : {QStringLiteral("mtp:/"), QStringLiteral("/"), m_mountDir.path()}) { errorReply = m_kiofuse_iface.remoteUrl(url); errorReply.waitForFinished(); QVERIFY2(errorReply.isError(), qPrintable(url)); QCOMPARE(errorReply.error().name(), QStringLiteral("org.kde.KIOFuse.VFS.Error.RemoteURLNotFound")); } QTemporaryFile localFile; QVERIFY(localFile.open()); localFile.close(); // Force creation of file to avoid empty fileName() QString remoteUrl = QStringLiteral("file://%1").arg(localFile.fileName()); QString reply = m_kiofuse_iface.mountUrl(remoteUrl).value(); QVERIFY(!reply.isEmpty()); QString calculatedRemoteUrl = m_kiofuse_iface.remoteUrl(reply).value(); QCOMPARE(remoteUrl, calculatedRemoteUrl); } void FileOpsTest::testLocalFileOps() { QTemporaryFile localFile; QVERIFY(localFile.open()); QCOMPARE(localFile.write("teststring"), 10); QVERIFY(localFile.flush()); // Mount the temporary file QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localFile.fileName())).value(); QVERIFY(!reply.isEmpty()); // Doing the same again should work just fine reply = m_kiofuse_iface.mountUrl(localFile.fileName()).value(); QVERIFY(!reply.isEmpty()); QFile mirroredFile(reply); QVERIFY(mirroredFile.exists()); QCOMPARE(mirroredFile.size(), localFile.size()); // Compare file metadata QFileInfo localFileInfo(localFile), mirroredFileInfo(mirroredFile); QCOMPARE(mirroredFileInfo.size(), localFileInfo.size()); QCOMPARE(mirroredFileInfo.ownerId(), localFileInfo.ownerId()); QCOMPARE(mirroredFileInfo.groupId(), localFileInfo.groupId()); // Not supported by KIO // QCOMPARE(mirroredFileInfo.metadataChangeTime(), localFileInfo.metadataChangeTime()); // KIO does not expose times with sub-second precision QCOMPARE(mirroredFileInfo.lastModified(), roundDownToSecond(localFileInfo.lastModified())); QCOMPARE(mirroredFileInfo.lastRead(), roundDownToSecond(localFileInfo.lastRead())); QVERIFY(mirroredFile.open(QIODevice::ReadWrite)); // Test touching the file struct timespec times[2] = {{time_t(localFileInfo.lastModified().toSecsSinceEpoch()) + 42, 0}, {time_t(localFileInfo.lastRead().toSecsSinceEpoch()) + 1, 0}}; QCOMPARE(futimens(mirroredFile.handle(), times), 0); localFileInfo.refresh(); mirroredFileInfo.refresh(); QCOMPARE(mirroredFileInfo.lastModified().toSecsSinceEpoch(), times[1].tv_sec); QCOMPARE(localFileInfo.lastModified().toSecsSinceEpoch(), times[1].tv_sec); // Access time not supported on the remote side, so only check in the mirror QCOMPARE(mirroredFileInfo.lastRead().toSecsSinceEpoch(), times[0].tv_sec); //QCOMPARE(localFileInfo.lastRead().toSecsSinceEpoch(), times[0].tv_sec); // Compare the content QVERIFY(localFile.seek(0)); QCOMPARE(localFile.readAll(), mirroredFile.readAll()); // Try again QVERIFY(localFile.seek(0)); QVERIFY(mirroredFile.seek(0)); QCOMPARE(localFile.readAll(), mirroredFile.readAll()); // Again, but at an offset QVERIFY(localFile.seek(1)); QVERIFY(mirroredFile.seek(1)); QCOMPARE(localFile.readAll(), mirroredFile.readAll()); // Write new data QVERIFY(mirroredFile.seek(0)); QCOMPARE(mirroredFile.write(QStringLiteral("newteststring!").toUtf8()), 14); QVERIFY(mirroredFile.flush()); // Flush the written contents into the backend QCOMPARE(fsync(mirroredFile.handle()), 0); // Currently, kio-fuse uses KIO::put and not KIO::write, so the file was replaced // instead of changed. So reopen the file. QFile localFile2(localFile.fileName()); QVERIFY(localFile2.open(QIODevice::ReadOnly)); // Compare the content QVERIFY(localFile2.seek(0)); QVERIFY(mirroredFile.seek(0)); QCOMPARE(localFile2.readAll(), mirroredFile.readAll()); // Write new data, but close the file instead of flushing QVERIFY(mirroredFile.seek(0)); QCOMPARE(mirroredFile.write(QStringLiteral("differentteststring").toUtf8()), 19); mirroredFile.close(); localFile2.close(); QVERIFY(localFile2.open(QIODevice::ReadOnly)); QVERIFY(localFile2.seek(0)); QVERIFY(mirroredFile.open(QIODevice::ReadWrite)); QVERIFY(mirroredFile.seek(0)); QCOMPARE(localFile2.readAll(), QStringLiteral("differentteststring").toUtf8()); // Test truncation at open mirroredFile.close(); QVERIFY(mirroredFile.open(QIODevice::WriteOnly | QIODevice::Truncate)); QCOMPARE(mirroredFile.write(QStringLiteral("tststrng").toUtf8()), 8); QVERIFY(mirroredFile.flush()); QCOMPARE(fsync(mirroredFile.handle()), 0); // Flush the written contents into the backend localFile2.close(); // Reopen the file, see above. QVERIFY(localFile2.open(QIODevice::ReadOnly)); QCOMPARE(localFile2.readAll(), QStringLiteral("tststrng").toUtf8()); // Compare the content // Test manual truncation QCOMPARE(ftruncate(mirroredFile.handle(), 3), 0); QCOMPARE(fsync(mirroredFile.handle()), 0); // Flush the written contents into the backend localFile2.close(); // Reopen the file, see above. QVERIFY(localFile2.open(QIODevice::ReadOnly)); QCOMPARE(localFile2.readAll(), QStringLiteral("tst").toUtf8()); // Compare the content // Test chown by not changing anything (no CAP_CHOWN...) QCOMPARE(chown(mirroredFile.fileName().toUtf8().data(), getuid(), getgid()), 0); localFileInfo.refresh(); QCOMPARE(localFileInfo.ownerId(), getuid()); QCOMPARE(localFileInfo.groupId(), getgid()); // Should not be allowed QCOMPARE(chown(mirroredFile.fileName().toUtf8().data(), getuid(), 0), -1); QCOMPARE(chown(mirroredFile.fileName().toUtf8().data(), 0, getgid()), -1); // Test chmod QCOMPARE(chmod(mirroredFile.fileName().toUtf8().data(), 0054), 0); struct stat attr; QCOMPARE(stat(localFile.fileName().toUtf8().data(), &attr), 0); QCOMPARE(attr.st_mode, S_IFREG | 0054); QCOMPARE(chmod(mirroredFile.fileName().toUtf8().data(), 0600), 0); QCOMPARE(stat(localFile.fileName().toUtf8().data(), &attr), 0); QCOMPARE(attr.st_mode, S_IFREG | 0600); // Mount the data path QString dataPath = QFINDTESTDATA(QStringLiteral("data")); QVERIFY(!dataPath.isEmpty()); reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(dataPath)).value(); QVERIFY(!reply.isEmpty()); QString mirrordataPath = reply; // Verify the symlink inside is correct QFile symlink(QDir(mirrordataPath).filePath(QStringLiteral("symlink"))); QVERIFY(symlink.open(QIODevice::ReadOnly)); QCOMPARE(symlink.readAll(), QStringLiteral("symlinktargetcontent").toUtf8()); QCOMPARE(symlink.symLinkTarget(), QDir(mirrordataPath).filePath(QStringLiteral("symlinktarget"))); // Verify that we adhere to O_APPEND flag as kernel doesn't handle this for us. QTemporaryFile appendFile; QVERIFY(appendFile.open()); QCOMPARE(appendFile.write("teststring"), 10); QVERIFY(appendFile.flush()); appendFile.close(); // Mount the temp file reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(appendFile.fileName())).value(); QVERIFY(!reply.isEmpty()); QFile appendMirror(reply); QVERIFY(appendMirror.exists()); QVERIFY(appendMirror.open(QIODevice::Append | QIODevice::ReadWrite)); // even if we set seek to 0 kio-fuse should change it back to the end of the file. QVERIFY(appendMirror.seek(0)); QCOMPARE(appendMirror.write("APPENDME"), 8); // Pass changes from mirror to local. QVERIFY(appendMirror.flush()); QCOMPARE(fsync(appendMirror.handle()), 0); // Currently, kio-fuse uses KIO::put and not KIO::write, so the file was replaced // instead of changed. So reopen the file. QFile appendFile2(appendFile.fileName()); QVERIFY(appendFile2.open(QIODevice::ReadOnly)); QVERIFY(appendMirror.seek(0)); QVERIFY(appendFile2.seek(0)); // If we don't adhere to O_APPEND flag we'd get "APPENDMEng" instead... QCOMPARE(appendMirror.readAll(), QStringLiteral("teststringAPPENDME").toUtf8()); QVERIFY(appendMirror.seek(0)); QVERIFY(appendFile2.seek(0)); QCOMPARE(appendMirror.readAll(), appendFile2.readAll()); } void FileOpsTest::testLocalDirOps() { QTemporaryDir localDir; QVERIFY(localDir.isValid()); // Mount the temporary dir QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value(); QVERIFY(!reply.isEmpty()); QDir mirrorDir(reply); QVERIFY(mirrorDir.exists()); // Create a folder inside QVERIFY(mirrorDir.mkdir(QStringLiteral("directory"))); QVERIFY(QFile::exists(localDir.filePath(QStringLiteral("directory")))); // Compare file metadata QFileInfo localDirInfo(localDir.path()), mirrorDirInfo(mirrorDir.path()); QCOMPARE(mirrorDirInfo.ownerId(), localDirInfo.ownerId()); QCOMPARE(mirrorDirInfo.groupId(), localDirInfo.groupId()); // Not supported by KIO // QCOMPARE(mirroredFileInfo.metadataChangeTime(), localFileInfo.metadataChangeTime()); // KIO does not expose times with sub-second precision QCOMPARE(mirrorDirInfo.lastModified(), roundDownToSecond(localDirInfo.lastModified())); QCOMPARE(mirrorDirInfo.lastRead(), roundDownToSecond(localDirInfo.lastRead())); // Test touching the file struct timespec times[2] = {{time_t(localDirInfo.lastModified().toSecsSinceEpoch()) + 42, 0}, {time_t(localDirInfo.lastRead().toSecsSinceEpoch()) + 1, 0}}; QCOMPARE(utimensat(AT_FDCWD, mirrorDir.path().toUtf8().data(), times, 0), 0); localDirInfo.refresh(); mirrorDirInfo.refresh(); QCOMPARE(mirrorDirInfo.lastModified().toSecsSinceEpoch(), times[1].tv_sec); QCOMPARE(localDirInfo.lastModified().toSecsSinceEpoch(), times[1].tv_sec); // Access time not supported on the remote side, so only check in the mirror QCOMPARE(mirrorDirInfo.lastRead().toSecsSinceEpoch(), times[0].tv_sec); //QCOMPARE(localDirInfo.lastRead().toSecsSinceEpoch(), times[0].tv_sec); // Test chown by not changing anything (no CAP_CHOWN...) QCOMPARE(chown(mirrorDir.path().toUtf8().data(), getuid(), getgid()), 0); localDirInfo.refresh(); QCOMPARE(localDirInfo.ownerId(), getuid()); QCOMPARE(localDirInfo.groupId(), getgid()); // Should not be allowed QCOMPARE(chown(mirrorDir.path().toUtf8().data(), getuid(), 0), -1); QCOMPARE(chown(mirrorDir.path().toUtf8().data(), 0, getgid()), -1); // Test chmod QCOMPARE(chmod(mirrorDir.path().toUtf8().data(), 0054), 0); struct stat attr; QCOMPARE(stat(localDir.path().toUtf8().data(), &attr), 0); QCOMPARE(attr.st_mode, S_IFDIR | 0054); QCOMPARE(chmod(mirrorDir.path().toUtf8().data(), 0700), 0); QCOMPARE(stat(localDir.path().toUtf8().data(), &attr), 0); QCOMPARE(attr.st_mode, S_IFDIR | 0700); // Mount the data path and compare the directory content QString dataPath = QFINDTESTDATA(QStringLiteral("data")); QVERIFY(!dataPath.isEmpty()); reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(dataPath)).value(); QVERIFY(!reply.isEmpty()); QString mirrordataPath = reply; auto sourceEntryList = QDir(dataPath).entryList(QDir::NoFilter, QDir::Name); auto mirrorEntryList = QDir(mirrordataPath).entryList(QDir::NoFilter, QDir::Name); QCOMPARE(mirrorEntryList, sourceEntryList); // Make sure dirlisting file:/// works sourceEntryList = QDir(QStringLiteral("/")).entryList(QDir::NoFilter, QDir::Name); reply = m_kiofuse_iface.mountUrl(QStringLiteral("file:///")).value(); QVERIFY(!reply.isEmpty()); mirrorEntryList = QDir(reply).entryList(QDir::NoFilter, QDir::Name); QCOMPARE(mirrorEntryList, sourceEntryList); } void FileOpsTest::testReaddirOps() { QTemporaryDir localDir; QVERIFY(localDir.isValid()); // Mount the temporary dir QString testDirPath = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value(); QVERIFY(!testDirPath.isEmpty()); // Fill the directory with some files for(unsigned int i=0; i<=10; i++) { QVERIFY(QFile(QStringLiteral("%1/tmpFile%2").arg(testDirPath).arg(i)).open(QIODevice::WriteOnly)); } DIR *testDir = opendir(qPrintable(testDirPath)); QVERIFY(testDir); auto opendirCleanup = qScopeGuard([&](){ closedir(testDir); }); QStringList testDirEntryList; QStringList testDirEntryListUpdated; // Get the initial entry list to compare with struct dirent *pDirent = nullptr; while((pDirent = readdir(testDir)) != nullptr) testDirEntryList.push_back(QString::fromUtf8(pDirent->d_name)); testDirEntryList.sort(); // Verify that entries remain same even if we add new or remove existing entries QVERIFY(QFile::remove(testDirPath + QStringLiteral("/tmpFile1"))); QVERIFY(QFile(testDirPath + QStringLiteral("/addCaseFile")).open(QIODevice::WriteOnly)); rewinddir(testDir); while((pDirent = readdir(testDir)) != nullptr) testDirEntryListUpdated.push_back(QString::fromUtf8(pDirent->d_name)); testDirEntryListUpdated.sort(); QCOMPARE(testDirEntryListUpdated, testDirEntryList); // Verify that entries remain same even if entries are modified while iterating testDirEntryListUpdated.clear(); rewinddir(testDir); unsigned int count = 1; while((pDirent = readdir(testDir)) != nullptr) { QVERIFY(QFile(QStringLiteral("%1/iterCaseFile%2").arg(testDirPath).arg(count)).open(QIODevice::WriteOnly)); testDirEntryListUpdated.push_back(QString::fromUtf8(pDirent->d_name)); count++; } testDirEntryListUpdated.sort(); QCOMPARE(testDirEntryListUpdated, testDirEntryList); } void FileOpsTest::testCreationOps() { QTemporaryDir localDir; QVERIFY(localDir.isValid()); // Mount the temporary dir QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value(); QVERIFY(!reply.isEmpty()); QDir mirrorDir(reply); QVERIFY(mirrorDir.exists()); // Create a symlink QCOMPARE(symlink("target", mirrorDir.filePath(QStringLiteral("symlink")).toUtf8().data()), 0); QCOMPARE(QFileInfo(localDir.filePath(QStringLiteral("symlink"))).symLinkTarget(), localDir.filePath(QStringLiteral("target"))); // Create a regular file QFile newFile(mirrorDir.filePath(QStringLiteral("newFile"))); QVERIFY(newFile.open(QIODevice::ReadWrite)); QFile newFileLocal(localDir.filePath(QStringLiteral("newFile"))); QVERIFY(newFileLocal.exists()); QCOMPARE(newFileLocal.size(), 0); QVERIFY(newFile.write(QStringLiteral("someweirdstring").toUtf8())); QVERIFY(newFile.flush()); QCOMPARE(fsync(newFile.handle()), 0); // Reopen the file (see above in testLocalFileOps) newFileLocal.close(); QVERIFY(newFileLocal.open(QIODevice::ReadOnly)); QCOMPARE(newFileLocal.readAll(), QStringLiteral("someweirdstring").toUtf8()); } void FileOpsTest::testRenameOps() { QTemporaryDir localDir; QVERIFY(localDir.isValid()); // Mount the temporary dir QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value(); QVERIFY(!reply.isEmpty()); QDir mirrorDir(reply); QVERIFY(mirrorDir.exists()); // Create a directory QVERIFY(QDir(mirrorDir.path()).mkdir(QStringLiteral("dira"))); QDir dir(mirrorDir.filePath(QStringLiteral("dira"))); // And a file inside QFile file(dir.filePath(QStringLiteral("filea"))); QVERIFY(file.open(QIODevice::ReadWrite)); QVERIFY(file.write(QStringLiteral("someweirdstring").toUtf8())); // Note: QFile::rename copies and unlinks if the rename syscall fails, // so use the libc function directly // Rename the file QCOMPARE(rename(dir.filePath(QStringLiteral("filea")).toUtf8().data(), dir.filePath(QStringLiteral("fileb")).toUtf8().data()), 0); QVERIFY(!QFile::exists(dir.filePath(QStringLiteral("filea")))); QVERIFY(QFile::exists(dir.filePath(QStringLiteral("fileb")))); QVERIFY(!QFile::exists(localDir.filePath(QStringLiteral("dira/filea")))); QVERIFY(QFile::exists(localDir.filePath(QStringLiteral("dira/fileb")))); // Rename the directory QCOMPARE(rename(mirrorDir.filePath(QStringLiteral("dira")).toUtf8().data(), mirrorDir.filePath(QStringLiteral("dirb")).toUtf8().data()), 0); QVERIFY(!QFile::exists(mirrorDir.filePath(QStringLiteral("dira")))); QVERIFY(QFile::exists(mirrorDir.filePath(QStringLiteral("dirb")))); QVERIFY(!QFile::exists(mirrorDir.filePath(QStringLiteral("dira")))); QVERIFY(QFile::exists(mirrorDir.filePath(QStringLiteral("dirb")))); QVERIFY(!QFile::exists(mirrorDir.filePath(QStringLiteral("dirb/filea")))); QVERIFY(QFile::exists(mirrorDir.filePath(QStringLiteral("dirb/fileb")))); // Verify that the file is still open and "connected" QVERIFY(file.write(QStringLiteral("!").toUtf8())); QVERIFY(file.flush()); QCOMPARE(fsync(file.handle()), 0); QFile localFile(localDir.filePath(QStringLiteral("dirb/fileb"))); QVERIFY(localFile.open(QIODevice::ReadOnly)); QCOMPARE(localFile.readAll(), QStringLiteral("someweirdstring!").toUtf8()); // Try the same, but overwriting an existing file QFile overwrittenFile(mirrorDir.filePath(QStringLiteral("dirb/filec"))); QVERIFY(overwrittenFile.open(QIODevice::ReadWrite)); QCOMPARE(overwrittenFile.write(QStringLiteral("data").toUtf8()), 4); QVERIFY(overwrittenFile.flush()); #ifdef RENAME_NOREPLACE QCOMPARE(renameat2(AT_FDCWD, mirrorDir.filePath(QStringLiteral("dirb/fileb")).toUtf8().data(), AT_FDCWD, mirrorDir.filePath(QStringLiteral("dirb/filec")).toUtf8().data(), RENAME_NOREPLACE), -1); QCOMPARE(errno, EEXIST); #endif QCOMPARE(rename(mirrorDir.filePath(QStringLiteral("dirb/fileb")).toUtf8().data(), mirrorDir.filePath(QStringLiteral("dirb/filec")).toUtf8().data()), 0); QVERIFY(!QFile::exists(localDir.filePath(QStringLiteral("dirb/fileb")))); QVERIFY(QFile::exists(localDir.filePath(QStringLiteral("dirb/filec")))); QVERIFY(!QFile::exists(mirrorDir.filePath(QStringLiteral("dirb/fileb")))); QVERIFY(QFile::exists(mirrorDir.filePath(QStringLiteral("dirb/filec")))); QVERIFY(overwrittenFile.seek(0)); #ifdef TEST_CACHE_BASED_IO // Both handles must still be valid QCOMPARE(overwrittenFile.readAll(), QStringLiteral("data").toUtf8()); #else // Doesn't apply to FileJob (KIO::open) I/O #endif localFile.close(); localFile.setFileName(localDir.filePath(QStringLiteral("dirb/filec"))); QVERIFY(localFile.open(QIODevice::ReadOnly)); QCOMPARE(localFile.readAll(), QStringLiteral("someweirdstring!").toUtf8()); } void FileOpsTest::testDeletionOps() { QTemporaryDir localDir; QVERIFY(localDir.isValid()); // Mount the temporary dir QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value(); QVERIFY(!reply.isEmpty()); QDir mirrorDir(reply); QVERIFY(mirrorDir.exists()); // Create a directory QVERIFY(QDir(mirrorDir.path()).mkdir(QStringLiteral("dir"))); QDir dir(mirrorDir.filePath(QStringLiteral("dir"))); // And a file inside QFile file(dir.filePath(QStringLiteral("file"))); QVERIFY(file.open(QIODevice::ReadWrite)); QVERIFY(file.write(QStringLiteral("someweirdstring").toUtf8())); QVERIFY(file.flush()); // Try to delete the directory QCOMPARE(unlink(dir.path().toUtf8().data()), -1); #ifdef Q_OS_LINUX QCOMPARE(errno, EISDIR); #else QCOMPARE(errno, EPERM); #endif QCOMPARE(rmdir(dir.path().toUtf8().data()), -1); QCOMPARE(errno, ENOTEMPTY); #ifdef TEST_CACHE_BASED_IO // Delete the file QCOMPARE(rmdir(file.fileName().toUtf8().data()), -1); QCOMPARE(errno, ENOTDIR); QCOMPARE(unlink(file.fileName().toUtf8().data()), 0); QVERIFY(!file.exists()); QVERIFY(!QFile::exists(localDir.filePath(QStringLiteral("dir/file")))); // Make sure it's still open QVERIFY(file.seek(0)); QCOMPARE(file.readAll(), QStringLiteral("someweirdstring").toUtf8()); // Delete the now empty directory QCOMPARE(rmdir(dir.path().toUtf8().data()), 0); QVERIFY(!dir.exists()); QVERIFY(!QFile::exists(localDir.filePath(QStringLiteral("dir")))); // Make sure the file is still open QVERIFY(file.seek(0)); QCOMPARE(file.readAll(), QStringLiteral("someweirdstring").toUtf8()); #else // FileJob-based nodes only unlink if the file isn't open QCOMPARE(rmdir(file.fileName().toUtf8().data()), -1); QCOMPARE(errno, ENOTDIR); QCOMPARE(unlink(file.fileName().toUtf8().data()), -1); file.close(); QCOMPARE(unlink(file.fileName().toUtf8().data()), 0); QVERIFY(!file.exists()); QVERIFY(!QFile::exists(localDir.filePath(QStringLiteral("dir/file")))); #endif // Not implemented: Link the file back into the tree, if possible // QCOMPARE(link(QStringLiteral("/proc/self/fd/%1").arg(file.handle()).toUtf8().data(), // mirrorDir.filePath(QStringLiteral("deletedFile")).toUtf8().data()), 0); // ... test that the file is still open and connected. } void FileOpsTest::testArchiveOps() { if (!KProtocolInfo::isKnownProtocol(QStringLiteral("tar"))) QSKIP("Test requires tar protocol to be supported. See README for packages required."); QString outerpath = QFINDTESTDATA(QStringLiteral("data/outerarchive.tar.gz")); // Mount a file inside the archive QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("tar://%1/outerarchive/outerfile").arg(outerpath)).value(); QVERIFY(!reply.isEmpty()); // And verify its content QString outerfilepath = reply; QFile outerfile(outerfilepath); QVERIFY(outerfile.open(QIODevice::ReadOnly)); QCOMPARE(outerfile.readAll(), QStringLiteral("outercontent").toUtf8()); reply = m_kiofuse_iface.mountUrl(QStringLiteral("tar://%1/outerarchive/innerarchive.tar.gz").arg(outerpath)).value(); QVERIFY(!reply.isEmpty()); QString innerpath = reply; // Unfortunately kio_archive is not reentrant, so a direct access would deadlock. // As a workaround, cache the file to avoid a 2nd call into kio_archive. QFile innerarchiveFile(innerpath); QVERIFY(innerarchiveFile.open(QIODevice::ReadOnly)); QVERIFY(!innerarchiveFile.readAll().isEmpty()); // Next, mount an archive inside - this uses kio-fuse recursively reply = m_kiofuse_iface.mountUrl(QStringLiteral("tar://%1").arg(innerpath)).value(); QVERIFY(!reply.isEmpty()); QFile innerfile(QStringLiteral("%1/innerarchive/innerfile").arg(reply)); QVERIFY(innerfile.open(QIODevice::ReadOnly)); QCOMPARE(innerfile.readAll(), QStringLiteral("innercontent").toUtf8()); innerfile.close(); } void FileOpsTest::testManWorkaround() { // The man ioslave has "hybrid" directories which stat as regular files but also support // listDir. This behaviour is not supported and mounting has to fail. if (!KProtocolInfo::isKnownProtocol(QStringLiteral("man"))) QSKIP("Test requires man protocol to be supported. See README for packages required."); QDBusPendingReply reply = m_kiofuse_iface.mountUrl(QStringLiteral("man:foo")); reply.waitForFinished(); QVERIFY(reply.isError()); QCOMPARE(reply.error().name(), QStringLiteral("org.kde.KIOFuse.VFS.Error.CannotMount")); reply = m_kiofuse_iface.mountUrl(QStringLiteral("man:/")); reply.waitForFinished(); QVERIFY(reply.isError()); QCOMPARE(reply.error().name(), QStringLiteral("org.kde.KIOFuse.VFS.Error.CannotMount")); } void FileOpsTest::testKioErrorMapping() { QTemporaryFile localFile; QVERIFY(localFile.open()); // Mount the temporary file QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localFile.fileName())).value(); QVERIFY(!reply.isEmpty()); QFile mirroredFile(reply); QVERIFY(mirroredFile.exists()); QVERIFY(mirroredFile.open(QIODevice::ReadWrite)); QCOMPARE(mirroredFile.size(), localFile.size()); // No permission to chown to root/root (unless running with CAP_CHOWN or being root) QCOMPARE(chown(mirroredFile.fileName().toUtf8().data(), 0, 0), -1); QCOMPARE(errno, EPERM); } void FileOpsTest::testRootLookup() { struct stat st; // Verify that it does not exist... QCOMPARE(stat(qPrintable(QStringLiteral("%1/invalid").arg(m_mountDir.path())), &st), -1); // ... and set errno correctly QCOMPARE(errno, ENOENT); } void FileOpsTest::testFilenameEscaping() { QTemporaryDir localDir; QVERIFY(localDir.isValid()); // Mount the temporary dir QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value(); QVERIFY(!reply.isEmpty()); QDir mirrorDir(reply); QVERIFY(mirrorDir.exists()); // Create a file in localDir with an "unusual" filename for(const QString &name : {QStringLiteral("file0?name"), QStringLiteral("file1#name"), QStringLiteral("file2%20name"), QStringLiteral("file2 \nname?asdf&foo#bar")}) { QFile localFile(localDir.filePath(name)); QVERIFY(localFile.open(QFile::WriteOnly)); QCOMPARE(localFile.write("teststring", 10), 10); localFile.close(); QFile mirrorFile(mirrorDir.filePath(name)); QVERIFY2(mirrorFile.open(QFile::ReadOnly), name.toUtf8().data()); QCOMPARE(mirrorFile.readAll(), QStringLiteral("teststring").toUtf8()); } } void FileOpsTest::testDirRefresh() { QTemporaryDir localDir; QVERIFY(localDir.isValid()); // Mount the temporary dir QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value(); QVERIFY(!reply.isEmpty()); QDir mirrorDir(reply); QVERIFY(mirrorDir.exists()); // readdir must not have any content yet QCOMPARE(mirrorDir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot).count(), 0); QFile newFile(localDir.filePath(QStringLiteral("newFile"))); QVERIFY(newFile.open(QFile::ReadWrite)); // Verify that the file is part of a dirlist after refresh QCOMPARE(mirrorDir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot).count(), 0); QVERIFY(forceNodeTimeout()); QCOMPARE(mirrorDir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot), QStringList{QStringLiteral("newFile")}); // Delete the file newFile.close(); QVERIFY(newFile.remove()); // Verify that it disappears from the dirlist after refresh QCOMPARE(mirrorDir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot), QStringList{QStringLiteral("newFile")}); QVERIFY(forceNodeTimeout()); QCOMPARE(mirrorDir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot).count(), 0); // Recreate the file QVERIFY(newFile.open(QFile::ReadWrite)); // Verify that access is immediately possible again (lookup is "optimistic") QVERIFY(QFile::exists(mirrorDir.filePath(QStringLiteral("newFile")))); // Delete the file again newFile.close(); QVERIFY(newFile.remove()); // Verify that after a refresh it's dropped QVERIFY(forceNodeTimeout()); QVERIFY(!QFile::exists(mirrorDir.filePath(QStringLiteral("newFile")))); } void FileOpsTest::testFileRefresh() { QTemporaryDir localDir; QVERIFY(localDir.isValid()); // Mount the temporary dir QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value(); QVERIFY(!reply.isEmpty()); QDir mirrorDir(reply); QVERIFY(mirrorDir.exists()); // readdir must not have any content yet QCOMPARE(mirrorDir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot).count(), 0); QFile localFile(localDir.filePath(QStringLiteral("newFile"))); QVERIFY(localFile.open(QFile::ReadWrite)); QFile mirrorFile(mirrorDir.filePath(QStringLiteral("newFile"))); QVERIFY(mirrorFile.open(QFile::ReadOnly)); QCOMPARE(mirrorFile.size(), 0); // File is empty QCOMPARE(mirrorFile.readAll(), QByteArray{}); QVERIFY(mirrorFile.permissions() & QFile::ReadOther); // Has default perms QCOMPARE(localFile.write("teststring", 10), 10); // Write some data QVERIFY(localFile.flush()); QVERIFY(localFile.setPermissions(localFile.permissions() & ~QFile::ReadOther)); // Change perms QCOMPARE(mirrorFile.size(), 0); // File is still empty QVERIFY(forceNodeTimeout()); // Without reopening, it has the new content and perms now QCOMPARE(mirrorFile.size(), 10); QCOMPARE(mirrorFile.readAll(), QStringLiteral("teststring").toUtf8()); QCOMPARE(mirrorFile.permissions() & QFile::ReadOther, 0); // Has changed perms } void FileOpsTest::testSymlinkRefresh() { QTemporaryDir localDir; QVERIFY(localDir.isValid()); // Mount the temporary dir QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value(); QVERIFY(!reply.isEmpty()); QDir mirrorDir(reply); QVERIFY(mirrorDir.exists()); // Create a symlink QCOMPARE(symlink("oldtarget", localDir.filePath(QStringLiteral("symlink")).toUtf8().data()), 0); QCOMPARE(readlink(mirrorDir.filePath(QStringLiteral("symlink"))), QStringLiteral("oldtarget")); // Change the symlink QVERIFY(QFile::remove(localDir.filePath((QStringLiteral("symlink"))))); QCOMPARE(symlink("newtarget", localDir.filePath(QStringLiteral("symlink")).toUtf8().data()), 0); QVERIFY(forceNodeTimeout()); QCOMPARE(readlink(mirrorDir.filePath(QStringLiteral("symlink"))), QStringLiteral("newtarget")); } void FileOpsTest::testTypeRefresh() { QTemporaryDir localDir; QVERIFY(localDir.isValid()); // Mount the temporary dir QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value(); QVERIFY(!reply.isEmpty()); QDir mirrorDir(reply); QVERIFY(mirrorDir.exists()); // Create a file and directory QFile localFile(localDir.filePath(QStringLiteral("changingtodir"))); QVERIFY(localFile.open(QFile::ReadWrite)); QVERIFY(QDir(localDir.path()).mkdir(QStringLiteral("changingtofile"))); // Open it on the mirror QFile changingMirrorFile(mirrorDir.filePath(QStringLiteral("changingtodir"))); QVERIFY(changingMirrorFile.open(QFile::ReadOnly)); // Replace the file locally with a directory QVERIFY(localFile.remove()); QVERIFY(QDir(localDir.path()).mkdir(QStringLiteral("changingtodir"))); QVERIFY(forceNodeTimeout()); // Verify that it's a directory now struct stat st; QCOMPARE(stat(qPrintable(changingMirrorFile.fileName()), &st), 0); QCOMPARE(st.st_mode & S_IFMT, S_IFDIR); // The opened file still refers to the (now deleted) file QCOMPARE(fstat(changingMirrorFile.handle(), &st), 0); QCOMPARE(st.st_mode & S_IFMT, S_IFREG); } void FileOpsTest::testDirSymlink() { QTemporaryDir localTmpDir; QVERIFY(localTmpDir.isValid()); QDir localDir(localTmpDir.path()); // Mount the temporary dir QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value(); QVERIFY(!reply.isEmpty()); QDir mirrorDir(reply); QVERIFY(mirrorDir.exists()); // Create a directory (with a dir inside) and a symlink to the parent QVERIFY(mirrorDir.mkpath(QStringLiteral("realdir/child"))); QCOMPARE(symlink("realdir/../realdir", qPrintable(mirrorDir.filePath(QStringLiteral("linktodir")))), 0); // Verify that the link is saved as-is QCOMPARE(readlink(mirrorDir.filePath(QStringLiteral("linktodir"))), QStringLiteral("realdir/../realdir")); // Verify that it was correctly created everywhere QVERIFY(mirrorDir.exists(QStringLiteral("linktodir/child"))); QVERIFY(localDir.exists(QStringLiteral("linktodir/child"))); QVERIFY(localDir.exists(QStringLiteral("realdir/child"))); // Verify that remoteUrl contains the exact path auto remoteUrlReply = m_kiofuse_iface.remoteUrl(mirrorDir.filePath(QStringLiteral("linktodir/child"))); remoteUrlReply.waitForFinished(); QVERIFY(!remoteUrlReply.isError()); QCOMPARE(QUrl{remoteUrlReply.value()}, QUrl::fromLocalFile(localDir.filePath(QStringLiteral("linktodir/child")))); // Verify that the child can be mounted through linktodir auto mountReply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.filePath(QStringLiteral("linktodir/child")))); mountReply.waitForFinished(); QVERIFY(!mountReply.isError()); } void FileOpsTest::testSymlinkRewrite() { QTemporaryDir localTmpDir; QVERIFY(localTmpDir.isValid()); QDir localDir(localTmpDir.path()); // Mount the temporary dir QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localDir.path())).value(); QVERIFY(!reply.isEmpty()); QDir mirrorDir(reply); QVERIFY(mirrorDir.exists()); // Create a symlink /mnt/file/.../symlink -> /mnt/file/.../somedir/../somefile. // This is to test that even if the target does not exist and is some convoluted // path, it is still rewritten correctly. QCOMPARE(symlink(qPrintable(mirrorDir.filePath(QStringLiteral("somedir/../somefile"))), qPrintable(mirrorDir.filePath(QStringLiteral("symlink")))), 0); // Verify that it can be read back as-is on the mount QCOMPARE(readlink(mirrorDir.filePath(QStringLiteral("symlink"))), mirrorDir.filePath(QStringLiteral("somedir/../somefile"))); // Verify that it's absolute on the local side QCOMPARE(readlink(localDir.filePath(QStringLiteral("symlink"))), localDir.filePath(QStringLiteral("somedir/../somefile"))); if (!KProtocolInfo::isKnownProtocol(QStringLiteral("tar"))) QSKIP("Test requires tar protocol to be supported. See README for packages required."); // Mount something with a different origin QString outerpath = QFINDTESTDATA(QStringLiteral("data/outerarchive.tar.gz")); reply = m_kiofuse_iface.mountUrl(QStringLiteral("tar://%1/outerarchive/").arg(outerpath)).value(); QVERIFY(!reply.isEmpty()); QDir archiveDir(reply); QVERIFY(archiveDir.exists()); // Create a symlink /mnt/file/.../symlink -> /mnt/tar/.../outerarchive/somewhere // which can't be rewritten properly QCOMPARE(symlink(qPrintable(archiveDir.filePath(QStringLiteral("somewhere"))), qPrintable(mirrorDir.filePath(QStringLiteral("symlink2")))), 0); // Verify that it didn't get rewritten during write QCOMPARE(readlink(localDir.filePath(QStringLiteral("symlink2"))), archiveDir.filePath(QStringLiteral("somewhere"))); // If rewriting fails, it'll keep it as-is, but the next read will rewrite // it in the other direction again, i.e. /mnt/file/mnt/tar/... reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(archiveDir.path())).value(); QVERIFY(!reply.isEmpty()); QCOMPARE(readlink(mirrorDir.filePath(QStringLiteral("symlink2"))), QDir(reply).filePath(QStringLiteral("somewhere"))); } #ifdef WASTE_DISK_SPACE void FileOpsTest::testReadWrite4GBFile() { QTemporaryFile localFile; QVERIFY(localFile.open()); // Mount the temporary file QString reply = m_kiofuse_iface.mountUrl(QStringLiteral("file://%1").arg(localFile.fileName())).value(); QVERIFY(!reply.isEmpty()); QFile mirroredFile(reply); QVERIFY(mirroredFile.exists()); QVERIFY(mirroredFile.open(QIODevice::ReadWrite)); // Write new data at a 2^32 offset QVERIFY(mirroredFile.seek(qint64(4096)*1024*1024)); QCOMPARE(mirroredFile.write(QStringLiteral("newteststring!").toUtf8()), 14); QVERIFY(mirroredFile.flush()); // Flush the written contents into the backend QCOMPARE(fsync(mirroredFile.handle()), 0); // Currently, kio-fuse uses KIO::put and not KIO::write, so the file was replaced // instead of changed. So reopen the file. QFile localFile2(localFile.fileName()); QVERIFY(localFile2.open(QIODevice::ReadOnly));; // Compare the content QVERIFY(localFile2.seek(qint64(4096)*1024*1024-6)); QCOMPARE(localFile2.read(20), QByteArray("\x00\x00\x00\x00\x00\x00newteststring!", 20)); QVERIFY(localFile2.seek(qint64(4096)*4096*1024-6)); QCOMPARE(localFile2.read(20), QByteArray()); QVERIFY(localFile2.seek(qint64(4096)*1024*1024-6)); QVERIFY(mirroredFile.seek(qint64(4096)*1024*1024-6)); QCOMPARE(localFile2.read(20), mirroredFile.read(20)); } #endif // WASTE_DISK_SPACE QDateTime FileOpsTest::roundDownToSecond(QDateTime dt) { return QDateTime::fromTime_t(dt.toTime_t()); } bool FileOpsTest::forceNodeTimeout() { auto reply = m_kiofuseprivate_iface.forceNodeTimeout(); reply.waitForFinished(); return !reply.isError(); } QString FileOpsTest::readlink(QString symlink) { char buf[PATH_MAX]; int len = ::readlink(qPrintable(symlink), buf, sizeof(buf)); if(len < 0 || len >= int(sizeof(buf))) // Failed or truncated? return {}; // Return a null QString return QString::fromLocal8Bit(buf, len); } QTEST_GUILESS_MAIN(FileOpsTest) #include "fileopstest.moc"