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 | |
45 | static 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 | |
72 | static 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) |
90 | inline 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 | |
147 | inline 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 | |
177 | inline 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 | |
201 | inline 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 | |
207 | inline 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) |
214 | static 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 | |
231 | KFileMetaData::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 | |
303 | inline 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 | |
350 | inline 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 | |
385 | inline 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 | |
399 | inline 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 | |
406 | inline 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 | |
416 | KFileMetaData::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 |
471 | inline ssize_t k_getxattr(const QString&, const QString&, QString*) |
472 | { |
473 | return 0; |
474 | } |
475 | |
476 | inline int k_setxattr(const QString&, const QString&, const QString&) |
477 | { |
478 | return -1; |
479 | } |
480 | |
481 | inline int k_removexattr(const QString&, const QString&) |
482 | { |
483 | return -1; |
484 | } |
485 | |
486 | inline bool k_hasAttribute(const QString&, const QString&) |
487 | { |
488 | return false; |
489 | } |
490 | |
491 | inline bool k_isSupported(const QString&) |
492 | { |
493 | return false; |
494 | } |
495 | |
496 | KFileMetaData::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 | |