diff --git a/autotests/jobtest.h b/autotests/jobtest.h --- a/autotests/jobtest.h +++ b/autotests/jobtest.h @@ -127,7 +127,11 @@ void enterLoop(); enum { AlreadyExists = 1 }; void copyLocalFile(const QString &src, const QString &dest); + void setXattr(const QString &src, const QString &command); + void compareXattr(const QString &src, const QString &dest, const QString &command); + void copyLocalFileWithXattr(const QString &src, const QString &dest); void copyLocalDirectory(const QString &src, const QString &dest, int flags = 0); + void copyLocalDirectoryWithXattr(const QString &src, const QString &_dest, int flags = 0); void moveLocalFile(const QString &src, const QString &dest); void moveLocalDirectory(const QString &src, const QString &dest); //void copyFileToSystem( bool resolve_local_urls ); diff --git a/autotests/jobtest.cpp b/autotests/jobtest.cpp --- a/autotests/jobtest.cpp +++ b/autotests/jobtest.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include @@ -525,6 +526,185 @@ QFile::remove(dest); } +void JobTest::setXattr(const QString &src, const QString &command) +{ + QProcess xattrwriter; + + QStringList arguments = {"-n", "user.fnoValue", src}; + xattrwriter.start(command, arguments); + xattrwriter.waitForFinished(-1); + + QHash attrs; + attrs["user.name with space"] = "value with spaces"; + attrs["user.baloo.rating"] = "1"; + attrs["user.fnewLine"] = "line1\\nline2"; + attrs["user.flistNull"] = "item1\\0item2"; + attrs["user.fattr.with.a.lot.of.namespaces"] = "true"; + attrs["root.fnot.allowed"] = "forbidden"; + attrs["user.name with space"] = "value with spaces"; + + arguments.insert(2, "-v"); + arguments.insert(3, "value"); + // arguments 0:"-n" 1:"name" 2:"-v", 3:"value" 4:src + QHashIterator i(attrs); + while (i.hasNext()) { + i.next(); + arguments.replace(1, i.key()); + arguments.replace(3, i.value()); + xattrwriter.start(command, arguments); + xattrwriter.waitForStarted(); + QVERIFY(xattrwriter.state() == QProcess::Running); + xattrwriter.waitForFinished(-1); + QVERIFY(xattrwriter.exitStatus() == QProcess::NormalExit); + } +} + +void JobTest::compareXattr(const QString &src, const QString &dest, const QString &command) +{ + QHash commands; + commands["setfattr"] = "getfattr"; + commands["setextattr"] = "getextatr"; + commands["xattr"] = "xattr"; + + QProcess xattrreader; + xattrreader.setProcessChannelMode(QProcess::MergedChannels); + + //test destination to see if filesystem supports xattr + xattrreader.start(command, QStringList{"-n", "user.test", dest}); + xattrreader.waitForFinished(-1); + QString testfs = xattrreader.readAllStandardOutput(); + if (testfs.section(':', 2, 2) == " Operation not supported\n") { + return; + } else { + //clean up + xattrreader.start(commands[command], QStringList{"-x", "user.test", dest}); + xattrreader.waitForFinished(-1); + } + + QHash attrs; + attrs["user.fnoValue"] = ""; + attrs["user.name with space"] = "value with spaces"; + attrs["user.baloo.rating"] = "1"; + attrs["user.fnewLine"] = "line1\\nline2"; + attrs["user.flistNull"] = "item1\\0item2"; + attrs["user.fattr.with.a.lot.of.namespaces"] = "true"; + attrs["root.fnot.allowed"] = "forbidden"; + attrs["user.name with space"] = "value with spaces"; + + QStringList arguments = {"-n", "name", src}; + QHashIterator i(attrs); + while (i.hasNext()) { + i.next(); + arguments.replace(1, i.key()); + xattrreader.start(commands[command], arguments); + xattrreader.waitForStarted(); + QVERIFY(xattrreader.state() == QProcess::Running); + xattrreader.waitForFinished(-1); + QVERIFY(xattrreader.exitStatus() == QProcess::NormalExit); + QString resultsrc = xattrreader.readAllStandardOutput(); + /****** + * We need to chop the filename from command output to compare + * 0:"getfattr: Removing leading '/' from absolute path names\n + * 1:# file: home/user/.qttest/share/kio/jobtest/kiotests/fileFromHome\n + * 2:user.baloo.rating=\"1\"\n + * 3:\n" + * ****/ + resultsrc = resultsrc.section('\n', 2, 2); + + arguments.replace(2, dest); + xattrreader.start(commands[command], arguments); + xattrreader.waitForStarted(); + QVERIFY(xattrreader.state() == QProcess::Running); + xattrreader.waitForFinished(-1); + QVERIFY(xattrreader.exitStatus() == QProcess::NormalExit); + QString resultdest = xattrreader.readAllStandardOutput(); + resultdest = resultdest.section('\n', 2, 2); + + QCOMPARE(resultdest, resultsrc); + } +} + +void JobTest::copyLocalFileWithXattr(const QString &src, const QString &dest) +{ + /***** + * Find if the platform have support for xattr. + * Linux commands: setfattr, getfattr + * BSD commands: setextattr, getextattr + * MacOS commands: xattr -w, xattr -p + ****/ + QHash commands; + commands["setfattr"] = "getfattr"; + commands["setextattr"] = "getextatr"; + commands["xattr"] = "xattr"; + + QString command; + QHashIterator i(commands); + while (i.hasNext()) { + i.next(); + if (!QStandardPaths::findExecutable(i.key()).isNull()) { + command = i.key(); + } + } + + // TODO: The tests are linux only for now. + if (command.isNull() || command != "setfattr") { + return; + } + + setXattr(src, command); + + // Simplified version of tests on copyLocalFile with a added compareXattr call + + const QUrl u = QUrl::fromLocalFile(src); + const QUrl d = QUrl::fromLocalFile(dest); + + // copy the file with file_copy + // This test is failing right now + /* + const int perms = 0666; + KIO::Job *job = KIO::file_copy(u, d, perms, KIO::HideProgressInfo); + job->setUiDelegate(nullptr); + bool ok = job->exec(); + QVERIFY(ok); + compareXattr(src, dest, command); // Our test + */ + + // cleanup and retry with KIO::copy() + QFile::remove(dest); + KIO::Job *job = KIO::copy(u, d, KIO::HideProgressInfo); + QSignalSpy spyCopyingDone(job, SIGNAL(copyingDone(KIO::Job*,QUrl,QUrl,QDateTime,bool,bool))); + job->setUiDelegate(nullptr); + job->setUiDelegateExtension(nullptr); + bool ok = job->exec(); + QVERIFY(ok); + compareXattr(src, dest, command); // Our test + + // cleanup and retry with KIO::copyAs() + QFile::remove(dest); + job = KIO::copyAs(u, d, KIO::HideProgressInfo); + job->setUiDelegate(nullptr); + job->setUiDelegateExtension(nullptr); + QVERIFY(job->exec()); + compareXattr(src, dest, command); // Our test + + // Do it again, with Overwrite. + job = KIO::copyAs(u, d, KIO::Overwrite | KIO::HideProgressInfo); + job->setUiDelegate(nullptr); + job->setUiDelegateExtension(nullptr); + QVERIFY(job->exec()); + compareXattr(src, dest, command); // Our test + + // Do it again, without Overwrite (should fail). + job = KIO::copyAs(u, d, KIO::HideProgressInfo); + job->setUiDelegate(nullptr); + job->setUiDelegateExtension(nullptr); + QVERIFY(!job->exec()); + compareXattr(src, dest, command); // Our test + + // Clean up + QFile::remove(dest); +} + void JobTest::copyLocalDirectory(const QString &src, const QString &_dest, int flags) { QVERIFY(QFileInfo(src).isDir()); @@ -577,6 +757,73 @@ job->setUiDelegateExtension(nullptr); ok = job->exec(); QVERIFY(!ok); + + // clean up + QDir(dest).removeRecursively(); +} + +void JobTest::copyLocalDirectoryWithXattr(const QString &src, const QString &_dest, int flags) +{ + /***** + * Find if the platform have support for xattr. + * Linux commands: setfattr, getfattr + * BSD commands: setextattr, getextattr + * MacOS commands: xattr -w, xattr -p + ****/ + QHash commands; + commands["setfattr"] = "getfattr"; + commands["setextattr"] = "getextatr"; + commands["xattr"] = "xattr"; + + QString command; + QHashIterator i(commands); + while (i.hasNext()) { + i.next(); + if (!QStandardPaths::findExecutable(i.key()).isNull()) { + command = i.key(); + } + } + + // TODO: The tests are linux only for now. + if (command.isNull() || command != "setfattr") { + return; + } + + setXattr(src, command); + + // Simplified version of tests on copyLocalDirectory with a added compareXattr call + + QUrl u = QUrl::fromLocalFile(src); + QString dest(_dest); + QUrl d = QUrl::fromLocalFile(dest); + + KIO::Job *job = KIO::copy(u, d, KIO::HideProgressInfo); + job->setUiDelegate(nullptr); + job->setUiDelegateExtension(nullptr); + bool ok = job->exec(); + QVERIFY(ok); + compareXattr(src, dest, command); // Our test + + if (flags & AlreadyExists) { + dest += '/' + u.fileName(); + //qDebug() << "Expecting dest=" << dest; + } + + // Do it again, with Overwrite. + // Use copyAs, we don't want a subdir inside d. + job = KIO::copyAs(u, d, KIO::HideProgressInfo | KIO::Overwrite); + job->setUiDelegate(nullptr); + job->setUiDelegateExtension(nullptr); + ok = job->exec(); + // currently fail by desing + compareXattr(src, dest, command); // Our test + + // Do it again, without Overwrite (should fail). + job = KIO::copyAs(u, d, KIO::HideProgressInfo); + job->setUiDelegate(nullptr); + job->setUiDelegateExtension(nullptr); + ok = job->exec(); + compareXattr(src, dest, command); // Our test } #ifndef Q_OS_WIN @@ -617,6 +864,7 @@ const QString dest = homeTmpDir() + "fileFromHome_copied"; createTestFile(filePath); copyLocalFile(filePath, dest); + copyLocalFileWithXattr(filePath, dest); } void JobTest::copyDirectoryToSamePartition() @@ -626,6 +874,7 @@ const QString dest = homeTmpDir() + "dirFromHome_copied"; createTestDirectory(src); copyLocalDirectory(src, dest); + copyLocalDirectoryWithXattr(src, dest); } void JobTest::copyDirectoryToExistingDirectory() @@ -638,6 +887,7 @@ createTestDirectory(src); createTestDirectory(dest); copyLocalDirectory(src, dest, AlreadyExists); + copyLocalDirectoryWithXattr(src, dest, AlreadyExists); } void JobTest::copyFileToOtherPartition() @@ -647,6 +897,7 @@ const QString dest = otherTmpDir() + "fileFromHome_copied"; createTestFile(filePath); copyLocalFile(filePath, dest); + copyLocalFileWithXattr(filePath, dest); } void JobTest::copyDirectoryToOtherPartition() @@ -656,6 +907,7 @@ const QString dest = otherTmpDir() + "dirFromHome_copied"; createTestDirectory(src); copyLocalDirectory(src, dest); + copyLocalDirectoryWithXattr(src, dest); } void JobTest::copyRelativeSymlinkToSamePartition() // #352927 diff --git a/src/core/ConfigureChecks.cmake b/src/core/ConfigureChecks.cmake --- a/src/core/ConfigureChecks.cmake +++ b/src/core/ConfigureChecks.cmake @@ -26,6 +26,8 @@ check_include_files(sys/types.h HAVE_SYS_TYPES_H) check_include_files(fstab.h HAVE_FSTAB_H) check_include_files(sys/param.h HAVE_SYS_PARAM_H) +check_include_files(sys/xattr.h HAVE_SYS_XATTR_H) +check_include_files(sys/extattr.h HAVE_SYS_EXTATTR_H) check_library_exists(volmgt volmgt_running "" HAVE_VOLMGT) diff --git a/src/core/config-kiocore.h.cmake b/src/core/config-kiocore.h.cmake --- a/src/core/config-kiocore.h.cmake +++ b/src/core/config-kiocore.h.cmake @@ -4,6 +4,9 @@ #cmakedefine01 HAVE_POSIX_ACL /* Defined if acl/libacl.h exists */ #cmakedefine01 HAVE_ACL_LIBACL_H +/* Defined if system has extended file attributes support. */ +#cmakedefine01 HAVE_SYS_XATTR_H +#cmakedefine01 HAVE_SYS_EXTATTR_H #define CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "${CMAKE_INSTALL_FULL_LIBEXECDIR_KF5}" diff --git a/src/core/copyjob.cpp b/src/core/copyjob.cpp --- a/src/core/copyjob.cpp +++ b/src/core/copyjob.cpp @@ -19,9 +19,16 @@ Boston, MA 02110-1301, USA. */ +#include + #include "copyjob.h" #include "kiocoredebug.h" #include +#if HAVE_SYS_XATTR_H +#include +#elif HAVE_SYS_EXTATTR_H +#include +#endif #include "kcoredirlister.h" #include "kfileitem.h" #include "job.h" // buildErrorString @@ -234,6 +241,7 @@ void slotResultRenaming(KJob *job); void slotResultSettingDirAttributes(KJob *job); void setNextDirAttribute(); + void copyXattrs(const QUrl &source, const QUrl &dest); void startRenameJob(const QUrl &slave_url); bool shouldOverwriteDir(const QString &path) const; @@ -1115,6 +1123,7 @@ } } else { // no error : remove from list, to move on to next dir //this is required for the undo feature + copyXattrs((*it).uSource, (*it).uDest); emit q->copyingDone(q, (*it).uSource, finalDestUrl((*it).uSource, (*it).uDest), (*it).mtime, true, false); m_directoriesCopied.append(*it); dirs.erase(it); @@ -1358,6 +1367,7 @@ emit q->copyingLinkDone(q, (*it).uSource, target, finalUrl); } else { //required for the undo feature + copyXattrs((*it).uSource, (*it).uDest); emit q->copyingDone(q, (*it).uSource, finalUrl, (*it).mtime, false, false); if (m_mode == CopyJob::Move) { org::kde::KDirNotify::emitFileMoved((*it).uSource, finalUrl); @@ -2173,6 +2183,87 @@ } } +// copy xattr to new file or dir +void CopyJobPrivate::copyXattrs(const QUrl &source, const QUrl &dest) +{ +#if HAVE_SYS_XATTR_H || HAVE_SYS_EXTATTR_H + // check if both source and dest exists, as KIO can copy from data, accees coud be lost, etc. + if (!QFileInfo::exists(source.toLocalFile()) && !QFileInfo::exists(dest.toLocalFile())) { + qCWarning(KIO_COPYJOB_DEBUG) << "failed to open source and destination"; + return; + } + const QByteArray sourcearray = QFile::encodeName(source.toLocalFile()); + const char *xattrsrc = sourcearray.constData(); + // get size of key list +#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) + ssize_t listlen = listxattr(xattrsrc, nullptr, 0); +#elif defined(Q_OS_MAC) + ssize_t listlen = listxattr(xattrsrc, nullptr, 0, 0); +#elif HAVE_SYS_EXTATTR + ssize_t listlen = extattr_list_file(xattr_src, EXTATTR_NAMESPACE_USER, nullptr, 0); +#endif + switch (listlen) { + case -1 : qCWarning(KIO_COPYJOB_DEBUG) << "libc failed to extract xattr from " << xattrsrc; + return; + case 0 : qCDebug(KIO_COPYJOB_DEBUG) << "file " << xattrsrc << " don't have any xattr"; + return; + } + QByteArray keylist(listlen, Qt::Uninitialized); + // get the key list +#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) + listlen = listxattr(xattrsrc, keylist.data(), listlen); +#elif defined(Q_OS_MAC) + listlen = listxattr(xattrsrc, keylist.data(), listlen, 0); +#elif HAVE_SYS_EXTATTR + listlen = extattr_list_file(xattrsrc, EXTATTR_NAMESPACE_USER, keylist.data(), listlen); +#endif + QList xattrkeys = keylist.split('\0'); + xattrkeys.removeLast(); // the last item is alwys empty + + for (const auto & xattrkey : xattrkeys) { + // get the size of value for key +#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) + ssize_t valuelen = getxattr(xattrsrc, xattrkey.constData(), nullptr, 0); +#elif defined(Q_OS_MAC) + ssize_t valuelen = listxattr(xattrsrc, xattrkey.constData(), nullptr, 0, 0, 0); +#elif HAVE_SYS_EXTATTR + ssize_t valuelen = extattr_get_file(xattrsrc, EXTATTR_NAMESPACE_USER, xattrkey.constData(), nullptr, 0); +#endif + if (valuelen < 0) { + qCWarning(KIO_COPYJOB_DEBUG) << "libc failed to extract value for " << xattrkey.constData() << \ + " key of " << xattrsrc << " file"; + continue; + } + QByteArray xattrval(valuelen + 1, Qt::Uninitialized); + //get the value of the key +#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) + valuelen = getxattr(xattrsrc, xattrkey.constData(), xattrval.data(), valuelen); +#elif defined(Q_OS_MAC) + vallen = getxattr(xattrsrc, xattrkey.constData(), xattrval.data(), valuelen, 0, 0); +#elif HAVE_SYS_EXTATTR + vallen = extattr_get_file(xattrsrc, EXTATTR_NAMESPACE_USER, xattrkey.constData(), xattrval.data(), valuelen); +#endif + //write key:value pair on dest file + const QByteArray destarray = QFile::encodeName(dest.toLocalFile()); + const char *xattrdest = destarray.constData(); +#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) + ssize_t destlen = setxattr(xattrdest, xattrkey.constData(), xattrval.data(), valuelen, 0); +#elif defined(Q_OS_MAC) + ssize_t destlen = setxattr(xattrdest, xattrkey.constData(), xattrval.data(), valuelen, 0, 0); +#elif defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD) + ssize_t destlen = extattr_set_file(xattrdest, EXTATTR_NAMESPACE_USER, xattrkey.constData(), xattrval.data(), valuelen); +#endif + if (destlen < 0) { + if (errno == ENOTSUP) { + qCDebug(KIO_COPYJOB_DEBUG) << "destination filesystem don't support xattrs"; + } else { + qCWarning(KIO_COPYJOB_DEBUG) << "libc failed to write xattr on a file"; + } + } + } +#endif +} + void KIO::CopyJob::setDefaultPermissions(bool b) { d_func()->m_defaultPermissions = b;