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() << "Xatr command not foud."; + } + } + } #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); } @@ -597,7 +710,7 @@ QVERIFY(QT_LSTAT(QFile::encodeName(src).constData(), &buf) == 0); QUrl u = QUrl::fromLocalFile(src); QUrl d = QUrl::fromLocalFile(dest); - + // copy the symlink KIO::Job *job = KIO::copy(u, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); @@ -616,6 +729,11 @@ const QString filePath = homeTmpDir() + "fileFromHome"; const QString dest = homeTmpDir() + "fileFromHome_copied"; createTestFile(filePath); + checkXattrFsSupport(filePath); + if (!m_SkipXattr) { + setXattr(filePath); + } + setXattr(filePath); copyLocalFile(filePath, dest); } @@ -646,6 +764,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); } @@ -667,6 +790,7 @@ const QString dest = homeTmpDir() + "testlink_copied"; createTestSymlink(filePath, "relative"); copyLocalSymlink(filePath, dest, QStringLiteral("relative")); + QFile::remove(filePath); #endif } @@ -678,8 +802,10 @@ #else const QString filePath = homeTmpDir() + "testlink"; const QString dest = otherTmpDir() + "testlink_copied"; + createTestSymlink(filePath, QFile::encodeName(homeTmpDir())); copyLocalSymlink(filePath, dest, homeTmpDir()); + QFile::remove(filePath); #endif } 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,14 @@ #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 +135,76 @@ return PrivilegeOperationReturnValue::failure(errcode); } + +bool FileProtocol::copyXattrs(const int src_fd, const int dest_fd) +{ +#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) + 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 + ssize_t listlen = extattr_list_file(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; + } + QByteArray keylist(listlen, Qt::Uninitialized); +#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) + listlen = flistxattr(src_fd, keylist.data(), listlen); +#elif defined(Q_OS_MAC) + listlen = flistxattr(src_fd, keylist.data(), listlen, 0); +#elif HAVE_SYS_EXTATTR + listlen = extattr_list_file(src_fd, EXTATTR_NAMESPACE_USER, keylist.data(), listlen); +#endif + QList m_keyList = keylist.split('\0'); + if (m_keyList.last().isEmpty()) m_keyList.removeLast(); // the last item may be empty + // for each key + for (const auto & key : qAsConst(m_keyList)) { + // get the size of value +#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) + 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 + ssize_t valuelen = extattr_get_file(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; + } + QByteArray value(valuelen + 1, Qt::Uninitialized); + // get the value for key +#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) + valuelen = fgetxattr(src_fd, key.constData(), value.data(), valuelen); +#elif defined(Q_OS_MAC) + vallen = fgetxattr(src_fd, key.constData(), value.data(), valuelen, 0, 0); +#elif HAVE_SYS_EXTATTR + vallen = extattr_get_file(src_fd, EXTATTR_NAMESPACE_USER, key.constData(), value.data(), valuelen); +#endif + //write key:value pair on dest file +#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) + 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 defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD) + ssize_t destlen = extattr_set_file(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"; + } else { + qCWarning(KIO_FILE) << "failed to write a xattr on destination file"; + } + } + } + return true; +} + void FileProtocol::copy(const QUrl &srcUrl, const QUrl &destUrl, int _mode, JobFlags _flags) { @@ -247,6 +322,7 @@ acl = nullptr; } #endif + totalSize(buff_src.st_size); KIO::filesize_t processed_size = 0; @@ -329,6 +405,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();