1/*
2 This file is part of the KDE Baloo Project
3 SPDX-FileCopyrightText: 2014 Raphael Kubo da Costa <rakuco@FreeBSD.org>
4
5 SPDX-License-Identifier: LGPL-2.1-or-later
6*/
7
8#ifndef KFILEMETADATA_XATTR_P_H
9#define KFILEMETADATA_XATTR_P_H
10
11#include <QByteArray>
12#include <QFile>
13#include <QString>
14#include <QDebug>
15
16#if defined(Q_OS_LINUX) || defined(__GLIBC__)
17#include <sys/types.h>
18#include <sys/xattr.h>
19
20#if defined(Q_OS_ANDROID) || defined(Q_OS_LINUX)
21// attr/xattr.h is not available in the Android NDK so we are defining ENOATTR ourself
22#ifndef ENOATTR
23# define ENOATTR ENODATA /* No such attribute */
24#endif
25#endif
26
27#include <errno.h>
28#elif defined(Q_OS_MAC)
29#include <sys/types.h>
30#include <sys/xattr.h>
31#include <errno.h>
32#elif defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD)
33#include <sys/types.h>
34#include <sys/extattr.h>
35#include <errno.h>
36#elif defined(Q_OS_OPENBSD)
37#include <errno.h>
38#elif defined(Q_OS_WIN)
39#include <QFileInfo>
40#include <windows.h>
41#define ssize_t SSIZE_T
42#endif
43#include <chrono>
44
45static KFileMetaData::UserMetaData::Attribute _mapAttribute(QByteArrayView key)
46{
47 using KFileMetaData::UserMetaData;
48 if (key == "xdg.tags") {
49 return UserMetaData::Attribute::Tags;
50 }
51 if (key == "baloo.rating") {
52 return UserMetaData::Attribute::Rating;
53 }
54 if (key == "xdg.comment") {
55 return UserMetaData::Attribute::Comment;
56 }
57 if (key == "xdg.origin.url") {
58 return UserMetaData::Attribute::OriginUrl;
59 }
60 if (key == "xdg.origin.email.subject") {
61 return UserMetaData::Attribute::OriginEmailSubject;
62 }
63 if (key == "xdg.origin.email.sender") {
64 return UserMetaData::Attribute::OriginEmailSender;
65 }
66 if (key == "xdg.origin.email.message-id") {
67 return UserMetaData::Attribute::OriginEmailMessageId;
68 }
69 return UserMetaData::Attribute::Other;
70}
71
72static KFileMetaData::UserMetaData::Attributes attributesFromEntries(const QList<QByteArray> &entries, const QByteArrayView &prefix, KFileMetaData::UserMetaData::Attributes attributes)
73{
74 using KFileMetaData::UserMetaData;
75 UserMetaData::Attributes fileAttributes = UserMetaData::Attribute::None;
76 for (const auto &entry : entries) {
77 if (!entry.startsWith(bv: prefix)) {
78 continue;
79 }
80 fileAttributes |= _mapAttribute(key: QByteArrayView(entry).sliced(pos: prefix.size()));
81 fileAttributes &= attributes;
82 if (fileAttributes == attributes) {
83 break;
84 }
85 }
86 return fileAttributes;
87}
88
89#if defined(Q_OS_LINUX) || defined(Q_OS_MAC) || defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD)
90inline ssize_t k_getxattr(const QString& path, const QString& name, QString* value)
91{
92 const QByteArray p = QFile::encodeName(fileName: path);
93 const char* encodedPath = p.constData();
94
95#if defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD)
96 const QByteArray n = name.toUtf8();
97#else
98 const QByteArray n = QByteArrayView("user.") + name.toUtf8();
99#endif
100 const char* attributeName = n.constData();
101
102 // First get the size of the data we are going to get to reserve the right amount of space.
103#if defined(Q_OS_LINUX) || (defined(__GLIBC__) && !defined(__stub_getxattr))
104 const ssize_t size = getxattr(path: encodedPath, name: attributeName, value: nullptr, size: 0);
105#elif defined(Q_OS_MAC)
106 const ssize_t size = getxattr(encodedPath, attributeName, NULL, 0, 0, 0);
107#elif defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD)
108 const ssize_t size = extattr_get_file(encodedPath, EXTATTR_NAMESPACE_USER, attributeName, NULL, 0);
109#endif
110
111 if (!value) {
112 return size;
113 }
114
115 if (size <= 0) {
116 value->clear();
117 return size;
118 }
119
120 QByteArray data(size, Qt::Uninitialized);
121
122 while (true) {
123#if defined(Q_OS_LINUX) || (defined(__GLIBC__) && !defined(__stub_getxattr))
124 const ssize_t r = getxattr(path: encodedPath, name: attributeName, value: data.data(), size: data.size());
125#elif defined(Q_OS_MAC)
126 const ssize_t r = getxattr(encodedPath, attributeName, data.data(), data.size(), 0, 0);
127#elif defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD)
128 const ssize_t r = extattr_get_file(encodedPath, EXTATTR_NAMESPACE_USER, attributeName, data.data(), data.size());
129#endif
130
131 if (r < 0 && errno != ERANGE) {
132 value->clear();
133 return r;
134 }
135
136 if (r >= 0) {
137 data.resize(size: r);
138 *value = QString::fromUtf8(ba: data);
139 return size;
140 } else {
141 // ERANGE
142 data.resize(size: data.size() * 2);
143 }
144 }
145}
146
147inline int k_setxattr(const QString& path, const QString& name, const QString& value)
148{
149 const QByteArray p = QFile::encodeName(fileName: path);
150 const char* encodedPath = p.constData();
151
152#if defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD)
153 const QByteArray n = name.toUtf8();
154#else
155 const QByteArray n = QByteArrayView("user.") + name.toUtf8();
156#endif
157 const char* attributeName = n.constData();
158
159 const QByteArray v = value.toUtf8();
160 const void* attributeValue = v.constData();
161
162 const size_t valueSize = v.size();
163
164#if defined(Q_OS_LINUX) || (defined(__GLIBC__) && !defined(__stub_setxattr))
165 int result = setxattr(path: encodedPath, name: attributeName, value: attributeValue, size: valueSize, flags: 0);
166 return result == -1 ? errno : 0;
167#elif defined(Q_OS_MAC)
168 int count = setxattr(encodedPath, attributeName, attributeValue, valueSize, 0, 0);
169 return count == -1 ? errno : 0;
170#elif defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD)
171 const ssize_t count = extattr_set_file(encodedPath, EXTATTR_NAMESPACE_USER, attributeName, attributeValue, valueSize);
172 return count == -1 ? errno : 0;
173#endif
174}
175
176
177inline int k_removexattr(const QString& path, const QString& name)
178{
179 const QByteArray p = QFile::encodeName(fileName: path);
180 const char* encodedPath = p.constData();
181
182#if defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD)
183 const QByteArray n = name.toUtf8();
184#else
185 const QByteArray n = QByteArrayView("user.") + name.toUtf8();
186#endif
187 const char* attributeName = n.constData();
188
189 #if defined(Q_OS_LINUX) || (defined(__GLIBC__) && !defined(__stub_removexattr))
190 int result = removexattr(path: encodedPath, name: attributeName);
191 return result == -1 ? errno : 0;
192 #elif defined(Q_OS_MAC)
193 int result = removexattr(encodedPath, attributeName, XATTR_NOFOLLOW );
194 return result == -1 ? errno : 0;
195 #elif defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD)
196 int result = extattr_delete_file (encodedPath, EXTATTR_NAMESPACE_USER, attributeName);
197 return result == -1 ? errno : 0;
198 #endif
199}
200
201inline bool k_hasAttribute(const QString& path, const QString& name)
202{
203 auto ret = k_getxattr(path, name, value: nullptr);
204 return (ret >= 0);
205}
206
207inline bool k_isSupported(const QString& path)
208{
209 auto ret = k_getxattr(path, QStringLiteral("test"), value: nullptr);
210 return (ret >= 0) || (errno != ENOTSUP);
211}
212
213#if defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD)
214static QList<QByteArray> _split_length_value(QByteArray data)
215{
216 int pos = 0;
217 QList<QByteArray> entries;
218
219 while (pos < data.size()) {
220 unsigned char len = data[pos];
221 if (pos + 1 + len <= data.size()) {
222 auto value = data.mid(pos + 1, len);
223 entries.append(value);
224 }
225 pos += 1 + len;
226 }
227 return entries;
228}
229#endif
230
231KFileMetaData::UserMetaData::Attributes k_queryAttributes(const QString& path,
232 KFileMetaData::UserMetaData::Attributes attributes)
233{
234 using KFileMetaData::UserMetaData;
235
236 const QByteArray p = QFile::encodeName(fileName: path);
237 const char* encodedPath = p.constData();
238
239 #if defined(Q_OS_LINUX)
240 const ssize_t size = listxattr(path: encodedPath, list: nullptr, size: 0);
241 #elif defined(Q_OS_MAC)
242 const ssize_t size = listxattr(encodedPath, nullptr, 0, 0);
243 #elif defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD)
244 const ssize_t size = extattr_list_file(encodedPath, EXTATTR_NAMESPACE_USER, nullptr, 0);
245 #endif
246
247 if (size == 0) {
248 return UserMetaData::Attribute::None;
249 }
250
251 if (size < 0) {
252 if (errno == E2BIG) {
253 return UserMetaData::Attribute::All;
254 }
255
256 return UserMetaData::Attribute::None;
257 }
258
259 if (attributes == UserMetaData::Attribute::Any) {
260 return UserMetaData::Attribute::All;
261 }
262
263 QByteArray data(size, Qt::Uninitialized);
264
265 while (true) {
266 #if defined(Q_OS_LINUX)
267 const ssize_t r = listxattr(path: encodedPath, list: data.data(), size: data.size());
268 #elif defined(Q_OS_MAC)
269 const ssize_t r = listxattr(encodedPath, data.data(), data.size(), 0);
270 #elif defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD)
271 const ssize_t r = extattr_list_file(encodedPath, EXTATTR_NAMESPACE_USER, data.data(), data.size());
272 #endif
273
274 if (r == 0) {
275 return UserMetaData::Attribute::None;
276 }
277
278 if (r < 0 && errno != ERANGE) {
279 return UserMetaData::Attribute::None;
280 }
281
282 if (r > 0) {
283 data.resize(size: r);
284 break;
285 } else {
286 data.resize(size: data.size() * 2);
287 }
288 }
289
290#if defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD)
291 const QByteArrayView prefix;
292 const auto entries = _split_length_value(data);
293#else
294 const QByteArrayView prefix("user.");
295 const auto entries = data.split(sep: '\0');
296#endif
297
298 return attributesFromEntries(entries, prefix, attributes);
299}
300
301#elif defined(Q_OS_WIN)
302
303inline ssize_t k_getxattr(const QString& path, const QString& name, QString* value)
304{
305 const QString fullADSName = path +
306 QLatin1String(":user.") + name;
307 HANDLE hFile = ::CreateFileW(reinterpret_cast<const WCHAR*>(fullADSName.utf16()), GENERIC_READ, FILE_SHARE_READ, NULL,
308 OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL);
309
310 if (hFile == INVALID_HANDLE_VALUE) {
311 DWORD error = ::GetLastError();
312 std::string message = std::system_category().message(error);
313 qWarning() << "failed to open ADS:" << message << fullADSName;
314 return -1;
315 }
316
317 LARGE_INTEGER lsize;
318 BOOL ret = GetFileSizeEx(hFile, &lsize);
319
320 if (!ret || lsize.QuadPart > 0x7fffffff || lsize.QuadPart == 0) {
321 CloseHandle(hFile);
322 value->clear();
323 return lsize.QuadPart == 0 ? 0 : -1;
324 }
325
326 DWORD r = 0;
327 QByteArray data(lsize.QuadPart, Qt::Uninitialized);
328 // should we care about attributes longer than 2GiB? - unix xattr are restricted to much lower values
329 ret = ::ReadFile(hFile, data.data(), data.size(), &r, NULL);
330 CloseHandle(hFile);
331
332 if (!ret) {
333 DWORD error = ::GetLastError();
334 std::string message = std::system_category().message(error);
335 qWarning() << "failed to open ADS:" << message << fullADSName;
336 return -1;
337 }
338
339 if (r == 0) {
340 value->clear();
341 return 0;
342 }
343
344 data.resize(r);
345
346 *value = QString::fromUtf8(data);
347 return r;
348}
349
350inline int k_setxattr(const QString& path, const QString& name, const QString& value)
351{
352 const QString fullADSName = path + QLatin1String(":user.") + name;
353 if (fullADSName.size() > MAX_PATH) {
354 // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation
355 // We could handle longer file names but it would require special casing per-windows version
356 return ERROR_FILENAME_EXCED_RANGE;
357 }
358
359 HANDLE hFile = ::CreateFileW(reinterpret_cast<const WCHAR*>(fullADSName.utf16()), GENERIC_WRITE, 0, NULL,
360 CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL);
361
362 if (hFile == INVALID_HANDLE_VALUE) {
363 DWORD error = ::GetLastError();
364 std::string message = std::system_category().message(error);
365 qWarning() << "failed to open file to write to ADS:" << message << error << fullADSName;
366 return -1; // unknown error
367 }
368
369 DWORD count = 0;
370
371 const QByteArray v = value.toUtf8();
372 if(!::WriteFile(hFile, v.constData(), v.size(), &count, NULL)) {
373 DWORD error = ::GetLastError();
374 std::string message = std::system_category().message(error);
375 qWarning() << "failed to write to ADS:" << message << error << fullADSName;
376
377 CloseHandle(hFile);
378 return -1; // unknown error
379 }
380
381 CloseHandle(hFile);
382 return 0; // Success
383}
384
385inline bool k_hasAttribute(const QString& path, const QString& name)
386{
387 QString fullpath = path + QLatin1String(":user.") + name;
388 HANDLE hFile = CreateFileW(reinterpret_cast<const WCHAR*>(fullpath.utf16()), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
389
390 if (hFile == INVALID_HANDLE_VALUE) {
391 // ERROR_FILE_NOT_FOUND supposedly
392 return false;
393 }
394 // ADS exists
395 CloseHandle(hFile);
396 return true;
397}
398
399inline int k_removexattr(const QString& path, const QString& name)
400{
401 const QString fullADSName = path + QLatin1String(":user.") + name;
402 int ret = (DeleteFileW(reinterpret_cast<const WCHAR*>(fullADSName.utf16()))) ? 0 : -1;
403 return ret;
404}
405
406inline bool k_isSupported(const QString& path)
407{
408 QFileInfo f(path);
409 const QString drive = QString(f.absolutePath().left(2)) + QStringLiteral("\\");
410 WCHAR szFSName[MAX_PATH];
411 DWORD dwVolFlags;
412 ::GetVolumeInformationW(reinterpret_cast<const WCHAR*>(drive.utf16()), NULL, 0, NULL, NULL, &dwVolFlags, szFSName, MAX_PATH);
413 return ((dwVolFlags & FILE_NAMED_STREAMS) && _wcsicmp(szFSName, L"NTFS") == 0);
414}
415
416KFileMetaData::UserMetaData::Attributes k_queryAttributes(const QString& path,
417 KFileMetaData::UserMetaData::Attributes attributes)
418{
419 using KFileMetaData::UserMetaData;
420
421 if (!k_isSupported(path)) {
422 return UserMetaData::Attribute::None;
423 }
424
425 HANDLE hFile= ::CreateFile(reinterpret_cast<const WCHAR*>(path.utf16()), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE , NULL,
426 OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
427
428 if (hFile == INVALID_HANDLE_VALUE) {
429 DWORD error = ::GetLastError();
430 std::string message = std::system_category().message(error);
431 qWarning() << "failed CreateFile:" << message << path << error;
432 return UserMetaData::Attribute::None;
433 }
434
435 QList<QByteArray> entries;
436 QString entry;
437 FILE_STREAM_INFO* fi = new FILE_STREAM_INFO[256];
438 if (::GetFileInformationByHandleEx(hFile, FileStreamInfo, fi, 256 * sizeof(FILE_STREAM_INFO))) {
439 // ignore first entry it is "::$DATA"
440 FILE_STREAM_INFO* p = fi;
441 while (p->NextEntryOffset != NULL) {
442 p = (FILE_STREAM_INFO*) ((char*)p + p->NextEntryOffset);
443 entry = QString::fromUtf16((char16_t*)p->StreamName, p->StreamNameLength / sizeof(char16_t));
444 // entries are of the form ":user.key:$DATA"
445 entry.chop(6);
446 entries.append(entry.sliced(1).toLocal8Bit());
447 }
448 } else {
449 DWORD error = ::GetLastError();
450 if (error != ERROR_HANDLE_EOF) {
451 std::string message = std::system_category().message(error);
452 qWarning() << "failed GetFileInformationByHandleEx:" << message << path << hFile;
453 }
454 }
455 delete[] fi;
456 CloseHandle(hFile);
457
458 if (entries.size() == 0) {
459 return UserMetaData::Attribute::None;
460 }
461
462 if (entries.size() > 0 && attributes == UserMetaData::Attribute::Any) {
463 return UserMetaData::Attribute::All;
464 }
465
466 const QByteArrayView prefix("user.");
467 return attributesFromEntries(entries, prefix, attributes);
468}
469
470#else
471inline ssize_t k_getxattr(const QString&, const QString&, QString*)
472{
473 return 0;
474}
475
476inline int k_setxattr(const QString&, const QString&, const QString&)
477{
478 return -1;
479}
480
481inline int k_removexattr(const QString&, const QString&)
482{
483 return -1;
484}
485
486inline bool k_hasAttribute(const QString&, const QString&)
487{
488 return false;
489}
490
491inline bool k_isSupported(const QString&)
492{
493 return false;
494}
495
496KFileMetaData::UserMetaData::Attributes k_queryAttributes(const QString&,
497 KFileMetaData::UserMetaData::Attributes attributes)
498{
499 return KFileMetaData::UserMetaData::Attribute::None;
500}
501
502#endif
503
504#endif // KFILEMETADATA_XATTR_P_H
505

source code of kfilemetadata/src/xattr_p.h