diff --git a/autotests/usermetadatawritertest.cpp b/autotests/usermetadatawritertest.cpp index 3f28139..a1bc9fe 100644 --- a/autotests/usermetadatawritertest.cpp +++ b/autotests/usermetadatawritertest.cpp @@ -1,127 +1,138 @@ /* * Copyright (C) 2017 James D. Smith * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ #include "usermetadatawritertest.h" #include "indexerextractortestsconfig.h" #include "usermetadata.h" #include #include #define TEST_FILENAME "writertest.txt" +#define TEST_SYMLINK "dangling_symlink" using namespace KFileMetaData; QString UserMetaDataWriterTest::testFilePath(const QString& fileName) const { return QLatin1String(INDEXER_TESTS_SAMPLE_FILES_PATH) + QLatin1Char('/') + fileName; } void UserMetaDataWriterTest::initTestCase() { QFile testFile(testFilePath("plain_text_file.txt")); QFile writerTestFile(testFilePath(TEST_FILENAME)); QFile::copy(testFilePath("plain_text_file.txt"), testFilePath(TEST_FILENAME)); + + QFile::link(testFilePath("invalid_target"), testFilePath(TEST_SYMLINK)); } void UserMetaDataWriterTest::test() { KFileMetaData::UserMetaData md(testFilePath(TEST_FILENAME)); QVERIFY(md.isSupported()); // 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")); QCOMPARE(md.userComment(), QStringLiteral("this is a test comment")); md.setUserComment(QString()); QVERIFY(!md.hasAttribute(QStringLiteral("xdg.comment"))); // Origin url md.setOriginUrl(QUrl("http://this.is.a.test.website.local")); QCOMPARE(md.originUrl(), QUrl("http://this.is.a.test.website.local")); md.setOriginUrl(QUrl()); QVERIFY(!md.hasAttribute(QStringLiteral("xdg.origin.url"))); // Origin e-mail subject md.setOriginEmailSubject(QStringLiteral("this is a test e-mail subject")); QCOMPARE(md.originEmailSubject(), QStringLiteral("this is a test e-mail subject")); md.setOriginEmailSubject(QString()); QVERIFY(!md.hasAttribute(QStringLiteral("xdg.origin.email.subject"))); // Origin e-mail sender md.setOriginEmailSender(QStringLiteral("Blue Bear")); QCOMPARE(md.originEmailSender(), QStringLiteral("Blue Bear")); md.setOriginEmailSender(QString()); QVERIFY(!md.hasAttribute(QStringLiteral("xdg.origin.email.sender"))); // Origin e-mail message id md.setOriginEmailMessageId(QStringLiteral("19991231235959.52234.24C26516HHBTF1C4")); QCOMPARE(md.originEmailMessageId(), QStringLiteral("19991231235959.52234.24C26516HHBTF1C4")); md.setOriginEmailMessageId(QString()); QVERIFY(!md.hasAttribute(QStringLiteral("xdg.origin.email.message-id"))); // 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"))); md.setAttribute(QStringLiteral("test.check_contains"), QStringLiteral("dummy")); QVERIFY(md.hasAttribute(QStringLiteral("test.check_contains"))); md.setAttribute(QStringLiteral("test.check_contains"), QString()); QVERIFY(!md.hasAttribute(QStringLiteral("test.check_contains"))); } + +void UserMetaDataWriterTest::testDanglingSymlink() +{ + KFileMetaData::UserMetaData md(testFilePath(TEST_SYMLINK)); + QVERIFY(md.queryAttributes(UserMetaData::Attribute::All) == UserMetaData::Attribute::None); +} + void UserMetaDataWriterTest::cleanupTestCase() { QFile::remove(testFilePath(TEST_FILENAME)); + QFile::remove(testFilePath(TEST_SYMLINK)); } QTEST_GUILESS_MAIN(UserMetaDataWriterTest) diff --git a/autotests/usermetadatawritertest.h b/autotests/usermetadatawritertest.h index dfa6656..2faa136 100644 --- a/autotests/usermetadatawritertest.h +++ b/autotests/usermetadatawritertest.h @@ -1,37 +1,38 @@ /* * Copyright (C) 2017 James D. Smith * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ #ifndef USERMETADATAWRITERTEST_H #define USERMETADATAWRITERTEST_H #include class UserMetaDataWriterTest : public QObject { Q_OBJECT private: QString testFilePath(const QString& fileName) const; private Q_SLOTS: void initTestCase(); void test(); + void testDanglingSymlink(); void cleanupTestCase(); }; #endif // USERMETADATAWRITERTEST_H diff --git a/src/xattr_p.h b/src/xattr_p.h index d431504..419e812 100644 --- a/src/xattr_p.h +++ b/src/xattr_p.h @@ -1,457 +1,457 @@ /* * This file is part of the KDE Baloo Project * Copyright (C) 2014 Raphael Kubo da Costa * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ #ifndef KFILEMETADATA_XATTR_P_H #define KFILEMETADATA_XATTR_P_H #include #include #include #include #include #if defined(Q_OS_LINUX) || defined(__GLIBC__) #include #include #if defined(Q_OS_ANDROID) || defined(Q_OS_LINUX) // attr/xattr.h is not available in the Android NDK so we are defining ENOATTR ourself #ifndef ENOATTR # define ENOATTR ENODATA /* No such attribute */ #endif #endif #include #elif defined(Q_OS_MAC) #include #include #include #elif defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD) #include #include #include #elif defined(Q_OS_WIN) #include #define ssize_t SSIZE_T #endif #if defined(Q_OS_LINUX) || defined(Q_OS_MAC) || defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD) inline ssize_t k_getxattr(const QString& path, const QString& name, QString* value) { const QByteArray p = QFile::encodeName(path); const char* encodedPath = p.constData(); const QByteArray n = name.toUtf8(); const char* attributeName = n.constData(); // First get the size of the data we are going to get to reserve the right amount of space. #if defined(Q_OS_LINUX) || (defined(__GLIBC__) && !defined(__stub_getxattr)) const ssize_t size = getxattr(encodedPath, attributeName, nullptr, 0); #elif defined(Q_OS_MAC) const ssize_t size = getxattr(encodedPath, attributeName, NULL, 0, 0, 0); #elif defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD) const ssize_t size = extattr_get_file(encodedPath, EXTATTR_NAMESPACE_USER, attributeName, NULL, 0); #endif if (!value) { return size; } if (size <= 0) { value->clear(); return size; } QByteArray data(size, Qt::Uninitialized); while (true) { #if defined(Q_OS_LINUX) || (defined(__GLIBC__) && !defined(__stub_getxattr)) const ssize_t r = getxattr(encodedPath, attributeName, data.data(), data.size()); #elif defined(Q_OS_MAC) const ssize_t r = getxattr(encodedPath, attributeName, data.data(), data.size(), 0, 0); #elif defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD) const ssize_t r = extattr_get_file(encodedPath, EXTATTR_NAMESPACE_USER, attributeName, data.data(), data.size()); #endif if (r < 0 && errno != ERANGE) { value->clear(); return r; } if (r >= 0) { data.resize(r); *value = QString::fromUtf8(data); return size; } else { // ERANGE data.resize(data.size() * 2); } } } inline int k_setxattr(const QString& path, const QString& name, const QString& value) { const QByteArray p = QFile::encodeName(path); const char* encodedPath = p.constData(); const QByteArray n = name.toUtf8(); const char* attributeName = n.constData(); const QByteArray v = value.toUtf8(); const void* attributeValue = v.constData(); const size_t valueSize = v.size(); #if defined(Q_OS_LINUX) || (defined(__GLIBC__) && !defined(__stub_setxattr)) return setxattr(encodedPath, attributeName, attributeValue, valueSize, 0); #elif defined(Q_OS_MAC) return setxattr(encodedPath, attributeName, attributeValue, valueSize, 0, 0); #elif defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD) const ssize_t count = extattr_set_file(encodedPath, EXTATTR_NAMESPACE_USER, attributeName, attributeValue, valueSize); return count == -1 ? -1 : 0; #endif } inline int k_removexattr(const QString& path, const QString& name) { const QByteArray p = QFile::encodeName(path); const char* encodedPath = p.constData(); const QByteArray n = name.toUtf8(); const char* attributeName = n.constData(); #if defined(Q_OS_LINUX) || (defined(__GLIBC__) && !defined(__stub_removexattr)) return removexattr(encodedPath, attributeName); #elif defined(Q_OS_MAC) return removexattr(encodedPath, attributeName, XATTR_NOFOLLOW ); #elif defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD) return extattr_delete_file (encodedPath, EXTATTR_NAMESPACE_USER, attributeName); #endif } inline bool k_hasAttribute(const QString& path, const QString& name) { auto ret = k_getxattr(path, name, nullptr); return (ret >= 0); } inline bool k_isSupported(const QString& path) { auto ret = k_getxattr(path, QStringLiteral("user.test"), nullptr); 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 < 0) { + if (errno == E2BIG) { + return UserMetaData::Attribute::All; + } - if (size == -1 && errno == E2BIG) { - return UserMetaData::Attribute::All; + return UserMetaData::Attribute::None; } - if (size > 0 && attributes == UserMetaData::Attribute::Any) { + if (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) { const QString fullADSName = path + QLatin1Char(':') + name; HANDLE hFile = ::CreateFileW(reinterpret_cast(fullADSName.utf16()), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL); if(!hFile) return 0; LARGE_INTEGER lsize; BOOL ret = GetFileSizeEx(hFile, &lsize); if (ret || lsize.QuadPart > 0x7fffffff || lsize.QuadPart == 0) { CloseHandle(hFile); value->clear(); return lsize.QuadPart == 0 ? 0 : -1; } DWORD r = 0; QByteArray data(lsize.QuadPart, Qt::Uninitialized); // should we care about attributes longer than 2GiB? - unix xattr are restricted to much lower values ret = ::ReadFile(hFile, data.data(), data.size(), &r, NULL); CloseHandle(hFile); if (ret || r == 0) { value->clear(); return r == 0 ? 0 : -1; } data.resize(r); *value = QString::fromUtf8(data); return r; } inline int k_setxattr(const QString& path, const QString& name, const QString& value) { const QByteArray v = value.toUtf8(); const QString fullADSName = path + QLatin1Char(':') + name; HANDLE hFile = ::CreateFileW(reinterpret_cast(fullADSName.utf16()), GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL); if(!hFile) return -1; DWORD count = 0; if(!::WriteFile(hFile, v.constData(), v.size(), &count, NULL)) { DWORD dw = GetLastError(); TCHAR msg[1024]; FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &msg, 0, NULL ); qWarning() << "failed to write to ADS:" << msg; CloseHandle(hFile); return -1; } CloseHandle(hFile); return count; } inline bool k_hasAttribute(const QString& path, const QString& name) { // enumerate all streams: const QString streamName = QStringLiteral(":") + name + QStringLiteral(":$DATA"); HANDLE hFile = ::CreateFileW(reinterpret_cast(path.utf16()), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL); if(!hFile) { return false; } FILE_STREAM_INFO* fi = new FILE_STREAM_INFO[256]; if(GetFileInformationByHandleEx(hFile, FileStreamInfo, fi, 256 * sizeof(FILE_STREAM_INFO))) { if(QString::fromUtf16((ushort*)fi->StreamName, fi->StreamNameLength / sizeof(ushort)) == streamName) { delete[] fi; CloseHandle(hFile); return true; } FILE_STREAM_INFO* p = fi; do { p = (FILE_STREAM_INFO*) ((char*)p + p->NextEntryOffset); if(QString::fromUtf16((ushort*)p->StreamName, p->StreamNameLength / sizeof(ushort)) == streamName) { delete[] fi; CloseHandle(hFile); return true; } } while(p->NextEntryOffset != NULL); } delete[] fi; CloseHandle(hFile); return false; } inline int k_removexattr(const QString& path, const QString& name) { const QString fullADSName = path + QLatin1Char(':') + name; int ret = (DeleteFileW(reinterpret_cast(fullADSName.utf16()))) ? 0 : -1; return ret; } inline bool k_isSupported(const QString& path) { QFileInfo f(path); const QString drive = QString(f.absolutePath().left(2)) + QStringLiteral("\\"); WCHAR szFSName[MAX_PATH]; DWORD dwVolFlags; ::GetVolumeInformationW(reinterpret_cast(drive.utf16()), NULL, 0, NULL, NULL, &dwVolFlags, szFSName, MAX_PATH); return ((dwVolFlags & FILE_NAMED_STREAMS) && _wcsicmp(szFSName, L"NTFS") == 0); } KFileMetaData::UserMetaData::Attributes k_queryAttributes(const QString& path, KFileMetaData::UserMetaData::Attributes attributes) { using KFileMetaData::UserMetaData; 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*) { return 0; } inline int k_setxattr(const QString&, const QString&, const QString&) { return -1; } inline int k_removexattr(const QString&, const QString&) { return -1; } inline bool k_hasAttribute(const QString&, const QString&) { return false; } inline bool k_isSupported(const QString&) { return false; } KFileMetaData::UserMetaData::Attributes k_queryAttributes(const QString&, KFileMetaData::UserMetaData::Attributes attributes) { return KFileMetaData::UserMetaData::Attribute::None; } #endif #endif // KFILEMETADATA_XATTR_P_H