diff --git a/autotests/jobtest.h b/autotests/jobtest.h --- a/autotests/jobtest.h +++ b/autotests/jobtest.h @@ -127,6 +127,9 @@ void enterLoop(); enum { AlreadyExists = 1 }; void copyLocalFile(const QString &src, const QString &dest); + void checkXattrFsSupport(const QString &writeTest); + void setXattr(const QString &src); + void compareXattr(const QString &src, const QString &dest); void copyLocalDirectory(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); @@ -141,6 +144,9 @@ QStringList m_names; int m_dataReqCount; QString m_mimetype; + QString m_setXattrCmd; + QString m_getXattrCmd; + bool m_SkipXattr = false; }; #endif 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 @@ -94,6 +95,30 @@ qFatal("couldn't create %s", qPrintable(otherTmpDir())); } } + + /***** + * Set platform xattr related commands. + * Linux commands: setfattr, getfattr + * BSD commands: setextattr, getextattr + * MacOS commands: xattr -w, xattr -p + ****/ + m_getXattrCmd = QStandardPaths::findExecutable("getfattr").split("/").last(); + if (m_getXattrCmd == "getfattr") { + m_setXattrCmd = "setfattr"; + } else { + m_getXattrCmd = QStandardPaths::findExecutable("getextattr").split("/").last(); + if (m_getXattrCmd == "getextattr") { + m_setXattrCmd = "setextattr"; + } else { + m_getXattrCmd = QStandardPaths::findExecutable("xattr").split("/").last(); + if (m_getXattrCmd == "xattr") { + m_getXattrCmd += " -p"; + m_setXattrCmd = "xattr -w"; + } else { + qWarning() << "Neither getfattr, getextattr nor xattr was found."; + } + } + } #if 0 if (KProtocolInfo::isKnownProtocol("system")) { if (!QFile::exists(realSystemPath())) { @@ -443,6 +468,89 @@ } //// +static QHash getSampleXattrs() +{ + 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["user.name with space"] = "value with spaces"; + return attrs; +} + +void JobTest::checkXattrFsSupport(const QString &dest) +{ + QStringList path = dest.split("/"); + path.removeLast(); + QString writeTest = path.join("/") + "/fsXattrTestFile"; + createTestFile(writeTest); + setXattr(writeTest); + QFile::remove(writeTest); +} + +void JobTest::setXattr(const QString &src) +{ + // TODO: Linux (setfattr) only + if (m_setXattrCmd != "setfattr") { + QSKIP("Linux only test"); + } + + QProcess xattrWriter; + xattrWriter.setProcessChannelMode(QProcess::MergedChannels); + // arguments 0:"-n" 1:"name" 2:"-v", 3:"value" 4:src + QStringList arguments = {"-n", "", "-v", "", "-h", src}; + QHash attrs = getSampleXattrs(); + QHashIterator i(attrs); + while (i.hasNext()) { + i.next(); + arguments.replace(1, i.key()); + arguments.replace(3, i.value()); + xattrWriter.start(m_setXattrCmd, arguments); + QVERIFY(xattrWriter.waitForStarted()); + QCOMPARE(xattrWriter.state(), QProcess::Running); + QVERIFY(xattrWriter.waitForFinished(-1)); + QCOMPARE(xattrWriter.exitStatus(), QProcess::NormalExit); + QList resultdest = xattrWriter.readAllStandardOutput().split('\n'); + if (!resultdest[0].isEmpty()) { + QWARN("Error writing user xattr. Xattr copy tests will be disabled."); + qDebug() << resultdest; + m_SkipXattr = true; + } + } +} + +void JobTest::compareXattr(const QString &src, const QString &dest) +{ + // TODO: Linux (setfattr) only + if (m_setXattrCmd != "setfattr") { + QSKIP("Linux only test"); + } + + QProcess xattrReader; + xattrReader.setProcessChannelMode(QProcess::MergedChannels); + QStringList arguments = {"-d", src}; + xattrReader.start(m_getXattrCmd, arguments); + QVERIFY(xattrReader.waitForStarted()); + QCOMPARE(xattrReader.state(), QProcess::Running); + QVERIFY(xattrReader.waitForFinished(-1)); + QCOMPARE(xattrReader.exitStatus(), QProcess::NormalExit); + QList resultSrc = xattrReader.readAllStandardOutput().split('\n'); + // Line 1 is the file name + resultSrc.removeAt(1); + + arguments.replace(1, dest); + xattrReader.start(m_getXattrCmd, arguments); + QVERIFY(xattrReader.waitForStarted()); + QCOMPARE(xattrReader.state(), QProcess::Running); + QVERIFY(xattrReader.waitForFinished(-1)); + QCOMPARE(xattrReader.exitStatus(), QProcess::NormalExit); + QList resultDest = xattrReader.readAllStandardOutput().split('\n'); + resultDest.removeAt(1); + + QCOMPARE(resultDest, resultSrc); +} void JobTest::copyLocalFile(const QString &src, const QString &dest) { @@ -458,6 +566,7 @@ QVERIFY(QFile::exists(dest)); QVERIFY(QFile::exists(src)); // still there QCOMPARE(int(QFileInfo(dest).permissions()), int(0x6666)); + compareXattr(src, dest); { // check that the timestamp is the same (#24443) @@ -484,6 +593,7 @@ QVERIFY(ok); QVERIFY(QFile::exists(dest)); QVERIFY(QFile::exists(src)); // still there + compareXattr(src, dest); { // check that the timestamp is the same (#24443) QFileInfo srcInfo(src); @@ -506,22 +616,25 @@ QVERIFY(job->exec()); QVERIFY(QFile::exists(dest)); QVERIFY(QFile::exists(src)); // still there + compareXattr(src, dest); // Do it again, with Overwrite. job = KIO::copyAs(u, d, KIO::Overwrite | KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY(job->exec()); QVERIFY(QFile::exists(dest)); QVERIFY(QFile::exists(src)); // still there + compareXattr(src, dest); // Do it again, without Overwrite (should fail). job = KIO::copyAs(u, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY(!job->exec()); // Clean up + QFile::remove(src); QFile::remove(dest); } @@ -616,6 +729,10 @@ const QString filePath = homeTmpDir() + "fileFromHome"; const QString dest = homeTmpDir() + "fileFromHome_copied"; createTestFile(filePath); + checkXattrFsSupport(filePath); + if (!m_SkipXattr) { + setXattr(filePath); + } copyLocalFile(filePath, dest); } @@ -646,6 +763,11 @@ const QString filePath = homeTmpDir() + "fileFromHome"; const QString dest = otherTmpDir() + "fileFromHome_copied"; createTestFile(filePath); + checkXattrFsSupport(filePath); + checkXattrFsSupport(dest); + if (!m_SkipXattr) { + setXattr(filePath); + } copyLocalFile(filePath, dest); } diff --git a/src/ioslaves/file/ConfigureChecks.cmake b/src/ioslaves/file/ConfigureChecks.cmake --- a/src/ioslaves/file/ConfigureChecks.cmake +++ b/src/ioslaves/file/ConfigureChecks.cmake @@ -7,6 +7,9 @@ check_include_files(sys/time.h HAVE_SYS_TIME_H) check_include_files(string.h HAVE_STRING_H) check_include_files(limits.h HAVE_LIMITS_H) +check_include_files(sys/xattr.h HAVE_SYS_XATTR_H) +check_include_files(sys/extattr.h HAVE_SYS_EXTATTR_H) + check_function_exists(sendfile HAVE_SENDFILE) check_function_exists(posix_fadvise HAVE_FADVISE) # kioslave diff --git a/src/ioslaves/file/file.h b/src/ioslaves/file/file.h --- a/src/ioslaves/file/file.h +++ b/src/ioslaves/file/file.h @@ -72,6 +72,7 @@ void read(KIO::filesize_t size) override; void write(const QByteArray &data) override; void seek(KIO::filesize_t offset) override; + bool copyXattrs(const int src_fd, const int dest_fd); void close() override; /** diff --git a/src/ioslaves/file/file_unix.cpp b/src/ioslaves/file/file_unix.cpp --- a/src/ioslaves/file/file_unix.cpp +++ b/src/ioslaves/file/file_unix.cpp @@ -38,9 +38,6 @@ #include #include -#if HAVE_SYS_XATTR_H -#include -#endif #include #include @@ -57,6 +54,13 @@ #include #endif +#if HAVE_SYS_XATTR_H +#include +//BSD uses a different include +#elif HAVE_SYS_EXTATTR_H +#include +#endif + using namespace KIO; #define MAX_IPC_SIZE (1024*32) @@ -130,6 +134,106 @@ return PrivilegeOperationReturnValue::failure(errcode); } + +bool FileProtocol::copyXattrs(const int src_fd, const int dest_fd) +{ + // Get the size of the list of keys from source file +#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) && !defined(Q_OS_MAC) + ssize_t listlen = flistxattr(src_fd, nullptr, 0); +#elif defined(Q_OS_MAC) + ssize_t listlen = flistxattr(src_fd, nullptr, 0, 0); +#elif HAVE_SYS_EXTATTR_H + ssize_t listlen = extattr_list_fd(src_fd, EXTATTR_NAMESPACE_USER, nullptr, 0); +#endif + if (listlen == -1) { + qCDebug(KIO_FILE) << "libc failed to extract list of xattr from file"; + return false; + } + if (listlen == 0) { + qCDebug(KIO_FILE) << "file" << src_fd << "don't have any xattr"; + return false; + } + + // Get the list of keys + QByteArray keylist(listlen, Qt::Uninitialized); +#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) && !defined(Q_OS_MAC) + listlen = flistxattr(src_fd, keylist.data(), listlen); +#elif defined(Q_OS_MAC) + listlen = flistxattr(src_fd, keylist.data(), listlen, 0); +#elif HAVE_SYS_EXTATTR_H + listlen = extattr_list_fd(src_fd, EXTATTR_NAMESPACE_USER, keylist.data(), listlen); +#endif + // Linux and MacOS return = list of null terminated string, each string = [data,'\0'] + // BSD return = list of items, each item prepended of 1 byte size = [size, data] + + QByteArray::const_iterator keyPtr = keylist.cbegin(); + size_t keyLen; + QByteArray key; + QByteArray value; + + // For each key + while (keyPtr != keylist.cend()) { + // Get the size of key +#if HAVE_SYS_XATTR_H + keyLen = strlen(keyPtr); +#elif HAVE_SYS_EXTATTR_H + keyLen = static_cast(*keyPtr); + keyPtr++; +#endif + QByteArray key(keyPtr, keyLen); + + // Get size of value +#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) && !defined(Q_OS_MAC) + ssize_t valuelen = fgetxattr(src_fd, key.constData(), nullptr, 0); +#elif defined(Q_OS_MAC) + ssize_t valuelen = fgetxattr(src_fd, key.constData(), nullptr, 0, 0, 0); +#elif HAVE_SYS_EXTATTR_H + ssize_t valuelen = extattr_get_fd(src_fd, EXTATTR_NAMESPACE_USER, key.constData(), nullptr, 0); +#endif + if (valuelen == -1) { + qCWarning(KIO_FILE) << "libc failed to extract a xattr value from file"; + continue; + } +#if HAVE_SYS_XATTR_H + value.resize(valuelen +1); +#elif HAVE_SYS_EXTATTR_H + value.resize(valuelen); +#endif + // Get the value for key +#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) && !defined(Q_OS_MAC) + valuelen = fgetxattr(src_fd, key.constData(), value.data(), valuelen); +#elif defined(Q_OS_MAC) + valuelen = fgetxattr(src_fd, key.constData(), value.data(), valuelen, 0, 0); +#elif HAVE_SYS_EXTATTR_H + valuelen = extattr_get_fd(src_fd, EXTATTR_NAMESPACE_USER, key.constData(), value.data(), valuelen); +#endif + + // Write key:value pair on destination +#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) && !defined(Q_OS_MAC) + ssize_t destlen = fsetxattr(dest_fd, key.constData(), value.data(), valuelen, 0); +#elif defined(Q_OS_MAC) + ssize_t destlen = fsetxattr(dest_fd, key.constData(), value.data(), valuelen, 0, 0); +#elif HAVE_SYS_EXTATTR_H + ssize_t destlen = extattr_set_fd(dest_fd, EXTATTR_NAMESPACE_USER, key.constData(), value.data(), valuelen); +#endif + if (destlen == -1) { + if (errno == ENOTSUP) { + qCWarning(KIO_FILE) << "destination filesystem don't support xattrs"; + return false; + } else { + qCWarning(KIO_FILE) << "failed to write a xattr on destination file"; + } + } + +#if HAVE_SYS_XATTR_H + keyPtr += keyLen + 1; +#elif HAVE_SYS_EXTATTR_H + keyPtr += keyLen; +#endif + } + return true; +} + void FileProtocol::copy(const QUrl &srcUrl, const QUrl &destUrl, int _mode, JobFlags _flags) { @@ -329,6 +433,13 @@ processedSize(processed_size); } + // Copy Extended attributes +#if HAVE_SYS_XATTR_H || HAVE_SYS_EXTATTR_H + if (!copyXattrs(src_file.handle(), dest_file.handle())) { + qCDebug(KIO_FILE) << "cant copy Extended attributes"; + } +#endif + src_file.close(); dest_file.close();