diff --git a/autotests/usermetadatawritertest.cpp b/autotests/usermetadatawritertest.cpp --- a/autotests/usermetadatawritertest.cpp +++ b/autotests/usermetadatawritertest.cpp @@ -51,14 +51,23 @@ // Tags md.setTags(QStringList() << QStringLiteral("this/is/a/test/tag")); QCOMPARE(md.tags().at(0), QStringLiteral("this/is/a/test/tag")); + QVERIFY(md.queryAttributes(UserMetaData::Attribute::Any) & UserMetaData::Attribute::Tags); + QVERIFY(md.queryAttributes(UserMetaData::Attribute::All) & UserMetaData::Attribute::Tags); + QVERIFY(md.queryAttributes(UserMetaData::Attribute::Tags) & UserMetaData::Attribute::Tags); + QVERIFY(!(md.queryAttributes(UserMetaData::Attribute::Rating) & UserMetaData::Attribute::Tags)); md.setTags(QStringList()); QVERIFY(!md.hasAttribute(QStringLiteral("xdg.tags"))); + QVERIFY(!(md.queryAttributes(UserMetaData::Attribute::Tags) & UserMetaData::Attribute::Tags)); // Rating md.setRating(3); QCOMPARE(md.rating(), 3); + QVERIFY(md.queryAttributes(UserMetaData::Attribute::All) & UserMetaData::Attribute::Rating); + QVERIFY(md.queryAttributes(UserMetaData::Attribute::Rating) & UserMetaData::Attribute::Rating); md.setRating(0); QVERIFY(!md.hasAttribute(QStringLiteral("baloo.rating"))); + QVERIFY(!(md.queryAttributes(UserMetaData::Attribute::All) & UserMetaData::Attribute::Rating)); + QVERIFY(!(md.queryAttributes(UserMetaData::Attribute::Rating) & UserMetaData::Attribute::Rating)); // Comment md.setUserComment(QStringLiteral("this is a test comment")); @@ -93,8 +102,16 @@ // Attribute md.setAttribute(QStringLiteral("test.attribute"), QStringLiteral("attribute")); QCOMPARE(md.attribute(QStringLiteral("test.attribute")), QStringLiteral("attribute")); + md.setAttribute(QStringLiteral("test.attribute2"), QStringLiteral("attribute2")); + QCOMPARE(md.attribute(QStringLiteral("test.attribute2")), QStringLiteral("attribute2")); + QVERIFY(md.queryAttributes(UserMetaData::Attribute::All) & UserMetaData::Attribute::Other); + QVERIFY(md.queryAttributes(UserMetaData::Attribute::Other) & UserMetaData::Attribute::Other); md.setAttribute(QStringLiteral("test.attribute"), QString()); QVERIFY(!md.hasAttribute(QStringLiteral("test.attribute"))); + QVERIFY(md.queryAttributes(UserMetaData::Attribute::All) & UserMetaData::Attribute::Other); + QVERIFY(md.queryAttributes(UserMetaData::Attribute::Other) & UserMetaData::Attribute::Other); + md.setAttribute(QStringLiteral("test.attribute2"), QString()); + QVERIFY(!md.hasAttribute(QStringLiteral("test.attribute2"))); // Check for side effects of calling sequence QVERIFY(!md.hasAttribute(QStringLiteral("test.check_contains"))); diff --git a/src/usermetadata.h b/src/usermetadata.h --- a/src/usermetadata.h +++ b/src/usermetadata.h @@ -22,6 +22,7 @@ #define KFILEMETADATA_USERMETADATA_H #include "kfilemetadata_export.h" +#include #include #include @@ -39,6 +40,22 @@ enum Error { NoError = 0 }; + + enum Attribute : uint32_t { + None = 0x0, + Any = None, + Tags = 0x1, + Rating = 0x2, + Comment = 0x4, + OriginUrl = 0x8, + OriginEmailSubject = 0x10, + OriginEmailSender = 0x20, + OriginEmailMessageId = 0x40, + Other = 0xffffff80, + All = 0xffffffff, + }; + Q_DECLARE_FLAGS(Attributes, Attribute) + const UserMetaData& operator =(const UserMetaData& rhs); QString filePath() const; @@ -68,11 +85,28 @@ QString attribute(const QString& name); Error setAttribute(const QString& name, const QString& value); bool hasAttribute(const QString& name); + + /** + * Query list of available attributes + * + * Checks for the availability of the given \p attributes. May return + * a superset of the input value when the file has attributes set + * beyond the requested ones. + * + * If the input attribute mask is Attribute::Any, either Attribute::None + * (the file has no user attributes) or Attribute::All (the file has at + * least one attribute set) is returned. + * + * \since 5.60 + */ + Attributes queryAttributes(Attributes attributes = Attribute::Any) const; + private: class Private; Private *d; }; +Q_DECLARE_OPERATORS_FOR_FLAGS(UserMetaData::Attributes) } #endif // KFILEMETADATA_USERMETADATA_H diff --git a/src/usermetadata.cpp b/src/usermetadata.cpp --- a/src/usermetadata.cpp +++ b/src/usermetadata.cpp @@ -215,3 +215,8 @@ { return k_isSupported(d->filePath); } + +UserMetaData::Attributes UserMetaData::queryAttributes(UserMetaData::Attributes attributes) const +{ + return k_queryAttributes(d->filePath, attributes); +} diff --git a/src/xattr_p.h b/src/xattr_p.h --- a/src/xattr_p.h +++ b/src/xattr_p.h @@ -160,6 +160,133 @@ return (ret >= 0) || (errno != ENOTSUP); } + +static KFileMetaData::UserMetaData::Attribute _mapAttribute(const QByteArray& key) +{ + using KFileMetaData::UserMetaData; + if (key == "user.xdg.tags") { + return UserMetaData::Attribute::Tags; + } + if (key == "user.baloo.rating") { + return UserMetaData::Attribute::Rating; + } + if (key == "user.xdg.comment") { + return UserMetaData::Attribute::Comment; + } + if (key == "user.xdg.origin.url") { + return UserMetaData::Attribute::OriginUrl; + } + if (key == "user.xdg.origin.email.subject") { + return UserMetaData::Attribute::OriginEmailSubject; + } + if (key == "user.xdg.origin.email.sender") { + return UserMetaData::Attribute::OriginEmailSender; + } + if (key == "user.xdg.origin.email.message-id") { + return UserMetaData::Attribute::OriginEmailMessageId; + } + return UserMetaData::Attribute::Other; +} + +#if defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD) +static QList _split_length_value(QByteArray data) +{ + int pos = 0; + QList entries; + + while (pos < data.size()) { + unsigned char len = data[pos]; + if (pos + 1 + len <= data.size()) { + auto value = data.mid(pos + 1, len); + entries.append(value); + } + pos += 1 + len; + } + return entries; +} +#endif + +KFileMetaData::UserMetaData::Attributes k_queryAttributes(const QString& path, + KFileMetaData::UserMetaData::Attributes attributes) +{ + using KFileMetaData::UserMetaData; + + const QByteArray p = QFile::encodeName(path); + const char* encodedPath = p.constData(); + + #if defined(Q_OS_LINUX) + const ssize_t size = listxattr(encodedPath, nullptr, 0); + #elif defined(Q_OS_MAC) + const ssize_t size = listxattr(encodedPath, nullptr, 0, 0); + #elif defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD) + const ssize_t size = extattr_list_file(encodedPath, EXTATTR_NAMESPACE_USER, nullptr, 0); + #endif + + if (size == 0) { + return UserMetaData::Attribute::None; + } + + if (size == -1 && errno == ENOTSUP) { + return UserMetaData::Attribute::None; + } + + if (size == -1 && errno == E2BIG) { + return UserMetaData::Attribute::All; + } + + if (size > 0 && attributes == UserMetaData::Attribute::Any) { + return UserMetaData::Attribute::All; + } + + QByteArray data(size, Qt::Uninitialized); + + while (true) { + #if defined(Q_OS_LINUX) + const ssize_t r = listxattr(encodedPath, data.data(), data.size()); + #elif defined(Q_OS_MAC) + const ssize_t r = listxattr(encodedPath, data.data(), data.size(), 0); + #elif defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD) + const ssize_t r = extattr_list_file(encodedPath, EXTATTR_NAMESPACE_USER, data.data(), data.size()); + #endif + + if (r == 0) { + return UserMetaData::Attribute::None; + } + + if (r < 0 && errno != ERANGE) { + return UserMetaData::Attribute::None; + } + + if (r > 0) { + data.resize(r); + break; + } else { + data.resize(data.size() * 2); + } + } + + UserMetaData::Attributes fileAttributes = UserMetaData::Attribute::None; + QByteArray prefix = QByteArray::fromRawData("user.", 5); + #if defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD) + const auto entries = _split_length_value(data); + #else + const auto entries = data.split('\0'); + #endif + for (const auto entry : entries) { + if (!entry.startsWith(prefix)) { + continue; + } + fileAttributes |= _mapAttribute(entry); + fileAttributes &= attributes; + + if (fileAttributes == attributes) { + break; + } + } + + return fileAttributes; +} + #elif defined(Q_OS_WIN) inline ssize_t k_getxattr(const QString& path, const QString& name, QString* value) @@ -278,6 +405,20 @@ return ((dwVolFlags & FILE_NAMED_STREAMS) && _wcsicmp(szFSName, L"NTFS") == 0); } +KFileMetaData::UserMetaData::Attributes k_queryAttributes(const QString& path, + KFileMetaData::UserMetaData::Attributes attributes) +{ + if (!k_isSupported(path)) { + return UserMetaData::Attribute::None; + } + + // TODO - this is mostly a stub, streams should be enumerated, see k_hasAttribute above + if (attributes == UserMetaData::Attribute::Any) { + return UserMetaData::Attribute::All; + } + return attributes; +} + #else inline ssize_t k_getxattr(const QString&, const QString&, QString*) { @@ -303,6 +444,13 @@ { return false; } + +KFileMetaData::UserMetaData::Attributes k_queryAttributes(const QString&, + KFileMetaData::UserMetaData::Attributes attributes) +{ + return UserMetaData::Attribute::None; +} + #endif #endif // KFILEMETADATA_XATTR_P_H