1/*
2 SPDX-FileCopyrightText: 2000-2002 Stephan Kulow <coolo@kde.org>
3 SPDX-FileCopyrightText: 2000-2002 David Faure <faure@kde.org>
4 SPDX-FileCopyrightText: 2000-2002 Waldo Bastian <bastian@kde.org>
5 SPDX-FileCopyrightText: 2006 Allan Sandfeld Jensen <sandfeld@kde.org>
6 SPDX-FileCopyrightText: 2007 Thiago Macieira <thiago@kde.org>
7 SPDX-FileCopyrightText: 2007 Christian Ehrlicher <ch.ehrlicher@gmx.de>
8 SPDX-FileCopyrightText: 2021-2024 Méven Car <meven@kde.org>
9
10 SPDX-License-Identifier: LGPL-2.0-or-later
11*/
12
13#include "file.h"
14#include "stat_unix.h"
15
16#include "config-kioworker-file.h"
17
18#include "../utils_p.h"
19
20#if HAVE_POSIX_ACL
21#include <../../aclhelpers_p.h>
22#endif
23
24#include <QDir>
25#include <QFile>
26#include <QMimeDatabase>
27#include <QStandardPaths>
28#include <QThread>
29#include <qplatformdefs.h>
30
31#include <KConfigGroup>
32#include <KFileSystemType>
33#include <KLocalizedString>
34#include <QDebug>
35#include <kmountpoint.h>
36
37#include <array>
38#include <cerrno>
39#include <stdint.h>
40#include <utime.h>
41
42#include <KAuth/Action>
43#include <KAuth/ExecuteJob>
44#include <KRandom>
45
46#include "fdreceiver.h"
47
48#ifdef Q_OS_LINUX
49
50#include <linux/fs.h>
51#include <sys/ioctl.h>
52#include <unistd.h>
53
54#endif // Q_OS_LINUX
55
56#if HAVE_COPY_FILE_RANGE
57// sys/types.h must be included before unistd.h,
58// and it needs to be included explicitly for FreeBSD
59#include <sys/types.h>
60#include <unistd.h>
61#endif
62
63#if HAVE_SYS_XATTR_H
64#include <sys/xattr.h>
65// BSD uses a different include
66#elif HAVE_SYS_EXTATTR_H
67#include <sys/types.h> // For FreeBSD, this must be before sys/extattr.h
68
69#include <sys/extattr.h>
70#endif
71
72using namespace KIO;
73
74/* 512 kB */
75static constexpr int s_maxIPCSize = 1024 * 512;
76
77static bool same_inode(const QT_STATBUF &src, const QT_STATBUF &dest)
78{
79 if (src.st_ino == dest.st_ino && src.st_dev == dest.st_dev) {
80 return true;
81 }
82
83 return false;
84}
85
86static const QString socketPath()
87{
88 const QString runtimeDir = QStandardPaths::writableLocation(type: QStandardPaths::RuntimeLocation);
89 return QStringLiteral("%1/filehelper%2%3").arg(args: runtimeDir, args: KRandom::randomString(length: 6)).arg(a: qlonglong(QThread::currentThreadId()));
90}
91
92static QString actionDetails(ActionType actionType, const QVariantList &args)
93{
94 QString action;
95 QString detail;
96 switch (actionType) {
97 case CHMOD:
98 action = i18n("Change File Permissions");
99 detail = i18n("New Permissions: %1", args[1].toInt());
100 break;
101 case CHOWN:
102 action = i18n("Change File Owner");
103 detail = i18n("New Owner: UID=%1, GID=%2", args[1].toInt(), args[2].toInt());
104 break;
105 case DEL:
106 action = i18n("Remove File");
107 break;
108 case RMDIR:
109 action = i18n("Remove Directory");
110 break;
111 case MKDIR:
112 action = i18n("Create Directory");
113 detail = i18n("Directory Permissions: %1", args[1].toInt());
114 break;
115 case OPEN:
116 action = i18n("Open File");
117 break;
118 case OPENDIR:
119 action = i18n("Open Directory");
120 break;
121 case RENAME:
122 action = i18n("Rename");
123 detail = i18n("New Filename: %1", args[1].toString());
124 break;
125 case SYMLINK:
126 action = i18n("Create Symlink");
127 detail = i18n("Target: %1", args[1].toString());
128 break;
129 case UTIME:
130 action = i18n("Change Timestamp");
131 break;
132 case COPY:
133 action = i18n("Copy");
134 detail = i18n("From: %1, To: %2", args[0].toString(), args[1].toString());
135 break;
136 default:
137 action = i18n("Unknown Action");
138 break;
139 }
140
141 const QString metadata = i18n(
142 "Action: %1\n"
143 "Source: %2\n"
144 "%3",
145 action,
146 args[0].toString(),
147 detail);
148 return metadata;
149}
150
151bool FileProtocol::privilegeOperationUnitTestMode()
152{
153 return (metaData(QStringLiteral("UnitTesting")) == QLatin1String("true"))
154 && (requestPrivilegeOperation(QStringLiteral("Test Call")) == KIO::OperationAllowed);
155}
156
157#if HAVE_POSIX_ACL
158bool FileProtocol::isExtendedACL(acl_t acl)
159{
160 return (ACLPortability::acl_equiv_mode(acl, nullptr) != 0);
161}
162#endif
163
164static bool isOnCifsMount(const QString &filePath)
165{
166 const auto mount = KMountPoint::currentMountPoints().findByPath(path: filePath);
167 if (!mount) {
168 return false;
169 }
170 return mount->mountType() == QStringLiteral("cifs") || mount->mountType() == QStringLiteral("smb3");
171}
172
173static bool createUDSEntry(const QString &filename, const QByteArray &path, UDSEntry &entry, KIO::StatDetails details, const QString &fullPath)
174{
175 assert(entry.count() == 0); // by contract :-)
176 int entries = 0;
177 if (details & KIO::StatBasic) {
178 // filename, access, type, size, linkdest
179 entries += 5;
180 }
181 if (details & KIO::StatUser) {
182 // uid, gid
183 entries += 2;
184 }
185 if (details & KIO::StatTime) {
186 // atime, mtime, btime
187 entries += 3;
188 }
189 if (details & KIO::StatAcl) {
190 // acl data
191 entries += 3;
192 }
193 if (details & KIO::StatInode) {
194 // dev, inode
195 entries += 2;
196 }
197 if (details & KIO::StatMimeType) {
198 // mimetype
199 entries += 1;
200 }
201 entry.reserve(size: entries);
202
203 if (details & KIO::StatBasic) {
204 entry.fastInsert(field: KIO::UDSEntry::UDS_NAME, value: filename);
205 }
206
207 bool isBrokenSymLink = false;
208#if HAVE_POSIX_ACL
209 QByteArray targetPath = path;
210#endif
211
212#if HAVE_STATX
213 // statx syscall is available
214 struct statx buff;
215#else
216 QT_STATBUF buff;
217#endif
218
219 if (LSTAT(path: path.data(), buff: &buff, details) == 0) {
220 if (Utils::isLinkMask(mode: stat_mode(buf: buff))) {
221 QByteArray linkTargetBuffer;
222 if (details & (KIO::StatBasic | KIO::StatResolveSymlink)) {
223// Use readlink on Unix because symLinkTarget turns relative targets into absolute (#352927)
224#if HAVE_STATX
225 size_t lowerBound = 256;
226 size_t higherBound = 1024;
227 uint64_t s = stat_size(buff);
228 if (s > SIZE_MAX) {
229 qCWarning(KIO_FILE) << "file size bigger than SIZE_MAX, too big for readlink use!" << path;
230 return false;
231 }
232 size_t size = static_cast<size_t>(s);
233 using SizeType = size_t;
234#else
235 off_t lowerBound = 256;
236 off_t higherBound = 1024;
237 off_t size = stat_size(buf: buff);
238 using SizeType = off_t;
239#endif
240 SizeType bufferSize = qBound(min: lowerBound, val: size + 1, max: higherBound);
241 linkTargetBuffer.resize(size: bufferSize);
242 while (true) {
243 ssize_t n = readlink(path: path.constData(), buf: linkTargetBuffer.data(), len: bufferSize);
244 if (n < 0 && errno != ERANGE) {
245 qCWarning(KIO_FILE) << "readlink failed!" << path;
246 return false;
247 } else if (n > 0 && static_cast<SizeType>(n) != bufferSize) {
248 // the buffer was not filled in the last iteration
249 // we are finished reading, break the loop
250 linkTargetBuffer.truncate(pos: n);
251 break;
252 }
253 bufferSize *= 2;
254 linkTargetBuffer.resize(size: bufferSize);
255 }
256 const QString linkTarget = QFile::decodeName(localFileName: linkTargetBuffer);
257 entry.fastInsert(field: KIO::UDSEntry::UDS_LINK_DEST, value: linkTarget);
258 }
259
260 // A symlink
261 if (details & KIO::StatResolveSymlink) {
262 if (STAT(path: path.constData(), buff: &buff, details) == -1) {
263 isBrokenSymLink = true;
264 } else {
265#if HAVE_POSIX_ACL
266 if (details & KIO::StatAcl) {
267 // valid symlink, will get the ACLs of the destination
268 targetPath = linkTargetBuffer;
269 }
270#endif
271 }
272 }
273 }
274 } else {
275 // qCWarning(KIO_FILE) << "lstat didn't work on " << path.data();
276 return false;
277 }
278
279 mode_t type = 0;
280 if (details & (KIO::StatBasic | KIO::StatAcl)) {
281 mode_t access;
282 signed long long size;
283 if (isBrokenSymLink) {
284 // It is a link pointing to nowhere
285 type = S_IFMT - 1;
286 access = S_IRWXU | S_IRWXG | S_IRWXO;
287 size = 0LL;
288 } else {
289 type = stat_mode(buf: buff) & S_IFMT; // extract file type
290 access = stat_mode(buf: buff) & 07777; // extract permissions
291 size = stat_size(buf: buff);
292 }
293
294 if (details & KIO::StatBasic) {
295 entry.fastInsert(field: KIO::UDSEntry::UDS_FILE_TYPE, l: type);
296 entry.fastInsert(field: KIO::UDSEntry::UDS_ACCESS, l: access);
297 entry.fastInsert(field: KIO::UDSEntry::UDS_SIZE, l: size);
298 }
299
300#if HAVE_POSIX_ACL
301 if (details & KIO::StatAcl) {
302 /* Append an atom indicating whether the file has extended acl information
303 * and if withACL is specified also one with the acl itself. If it's a directory
304 * and it has a default ACL, also append that. */
305 appendACLAtoms(targetPath, entry, type);
306 }
307#endif
308 }
309
310 if (details & KIO::StatUser) {
311 const auto uid = stat_uid(buf: buff);
312 const auto gid = stat_gid(buf: buff);
313 entry.fastInsert(field: KIO::UDSEntry::UDS_LOCAL_USER_ID, l: uid);
314 entry.fastInsert(field: KIO::UDSEntry::UDS_LOCAL_GROUP_ID, l: gid);
315 }
316
317 if (details & KIO::StatTime) {
318 entry.fastInsert(field: KIO::UDSEntry::UDS_MODIFICATION_TIME, l: stat_mtime(buf: buff));
319 entry.fastInsert(field: KIO::UDSEntry::UDS_ACCESS_TIME, l: stat_atime(buf: buff));
320
321#ifdef st_birthtime
322 /* For example FreeBSD's and NetBSD's stat contains a field for
323 * the inode birth time: st_birthtime
324 * This however only works on UFS and ZFS, and not, on say, NFS.
325 * Instead of setting a bogus fallback like st_mtime, only use
326 * it if it is greater than 0. */
327 if (buff.st_birthtime > 0) {
328 entry.fastInsert(KIO::UDSEntry::UDS_CREATION_TIME, buff.st_birthtime);
329 }
330#elif defined __st_birthtime
331 /* As above, but OpenBSD calls it slightly differently. */
332 if (buff.__st_birthtime > 0) {
333 entry.fastInsert(KIO::UDSEntry::UDS_CREATION_TIME, buff.__st_birthtime);
334 }
335#elif HAVE_STATX
336 /* And linux version using statx syscall */
337 if (buff.stx_mask & STATX_BTIME) {
338 entry.fastInsert(KIO::UDSEntry::UDS_CREATION_TIME, buff.stx_btime.tv_sec);
339 }
340#endif
341 }
342
343 if (details & KIO::StatInode) {
344 entry.fastInsert(field: KIO::UDSEntry::UDS_DEVICE_ID, l: stat_dev(buf: buff));
345 entry.fastInsert(field: KIO::UDSEntry::UDS_INODE, l: stat_ino(buf: buff));
346 }
347
348 if (details & KIO::StatMimeType) {
349 QMimeDatabase db;
350 entry.fastInsert(field: KIO::UDSEntry::UDS_MIME_TYPE, value: db.mimeTypeForFile(fileName: fullPath).name());
351 }
352
353 return true;
354}
355
356WorkerResult FileProtocol::tryOpen(QFile &f, const QByteArray &path, int flags, int mode, int errcode)
357{
358 const QString sockPath = socketPath();
359 FdReceiver fdRecv(QFile::encodeName(fileName: sockPath).toStdString());
360 if (!fdRecv.isListening()) {
361 return WorkerResult::fail(error: errcode);
362 }
363
364 QIODevice::OpenMode openMode;
365 if (flags & O_RDONLY) {
366 openMode |= QIODevice::ReadOnly;
367 }
368 if (flags & O_WRONLY || flags & O_CREAT) {
369 openMode |= QIODevice::WriteOnly;
370 }
371 if (flags & O_RDWR) {
372 openMode |= QIODevice::ReadWrite;
373 }
374 if (flags & O_TRUNC) {
375 openMode |= QIODevice::Truncate;
376 }
377 if (flags & O_APPEND) {
378 openMode |= QIODevice::Append;
379 }
380
381 auto result = execWithElevatedPrivilege(action: OPEN, args: {path, flags, mode, sockPath}, errcode);
382 if (!result.success()) {
383 return result;
384 } else {
385 int fd = fdRecv.fileDescriptor();
386 if (fd < 3 || !f.open(fd, ioFlags: openMode, handleFlags: QFileDevice::AutoCloseHandle)) {
387 return WorkerResult::fail(error: errcode);
388 }
389 }
390 return WorkerResult::pass();
391}
392
393WorkerResult FileProtocol::tryChangeFileAttr(ActionType action, const QVariantList &args, int errcode)
394{
395 KAuth::Action execAction(QStringLiteral("org.kde.kio.file.exec"));
396 execAction.setHelperId(QStringLiteral("org.kde.kio.file"));
397 if (execAction.status() == KAuth::Action::AuthorizedStatus) {
398 return execWithElevatedPrivilege(action, args, errcode);
399 }
400 return WorkerResult::fail(error: errcode);
401}
402
403#if HAVE_SYS_XATTR_H || HAVE_SYS_EXTATTR_H
404bool FileProtocol::copyXattrs(const int src_fd, const int dest_fd)
405{
406 // Get the list of keys
407 ssize_t listlen = 0;
408 QByteArray keylist;
409 while (true) {
410 keylist.resize(size: listlen);
411#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) && !defined(Q_OS_MAC)
412 listlen = flistxattr(fd: src_fd, list: keylist.data(), size: listlen);
413#elif defined(Q_OS_MAC)
414 listlen = flistxattr(src_fd, keylist.data(), listlen, 0);
415#elif HAVE_SYS_EXTATTR_H
416 listlen = extattr_list_fd(src_fd, EXTATTR_NAMESPACE_USER, listlen == 0 ? nullptr : keylist.data(), listlen);
417#endif
418 if (listlen > 0 && keylist.size() == 0) {
419 continue;
420 }
421 if (listlen > 0 && keylist.size() > 0) {
422 break;
423 }
424 if (listlen == -1 && errno == ERANGE) {
425 listlen = 0;
426 continue;
427 }
428 if (listlen == 0) {
429 // qCDebug(KIO_FILE) << "the file doesn't have any xattr";
430 return true;
431 }
432 Q_ASSERT_X(listlen == -1, "copyXattrs", "unexpected return value from listxattr");
433 if (listlen == -1 && errno == ENOTSUP) {
434 qCDebug(KIO_FILE) << "source filesystem does not support xattrs";
435 }
436 return false;
437 }
438
439 keylist.resize(size: listlen);
440
441 // Linux and MacOS return a list of null terminated strings, each string = [data,'\0']
442 // BSDs return a list of items, each item consisting of the size byte
443 // prepended to the key = [size, data]
444 auto keyPtr = keylist.cbegin();
445 size_t keyLen;
446 QByteArray value;
447
448 // For each key
449 while (keyPtr != keylist.cend()) {
450 // Get size of the key
451#if HAVE_SYS_XATTR_H
452 keyLen = strlen(s: keyPtr);
453 auto next_key = [&]() {
454 keyPtr += keyLen + 1;
455 };
456#elif HAVE_SYS_EXTATTR_H
457 keyLen = static_cast<unsigned char>(*keyPtr);
458 keyPtr++;
459 auto next_key = [&]() {
460 keyPtr += keyLen;
461 };
462#endif
463 QByteArray key(keyPtr, keyLen);
464
465 // Get the value for key
466 ssize_t valuelen = 0;
467 do {
468 value.resize(size: valuelen);
469#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) && !defined(Q_OS_MAC)
470 valuelen = fgetxattr(fd: src_fd, name: key.constData(), value: value.data(), size: valuelen);
471#elif defined(Q_OS_MAC)
472 valuelen = fgetxattr(src_fd, key.constData(), value.data(), valuelen, 0, 0);
473#elif HAVE_SYS_EXTATTR_H
474 valuelen = extattr_get_fd(src_fd, EXTATTR_NAMESPACE_USER, key.constData(), valuelen == 0 ? nullptr : value.data(), valuelen);
475#endif
476 if (valuelen > 0 && value.size() == 0) {
477 continue;
478 }
479 if (valuelen > 0 && value.size() > 0) {
480 break;
481 }
482 if (valuelen == -1 && errno == ERANGE) {
483 valuelen = 0;
484 continue;
485 }
486 // happens when attr value is an empty string
487 if (valuelen == 0) {
488 break;
489 }
490 Q_ASSERT_X(valuelen == -1, "copyXattrs", "unexpected return value from getxattr");
491 // Some other error, skip to the next attribute, most notably
492 // - ENOTSUP: invalid (inaccassible) attribute namespace, e.g. with SELINUX
493 break;
494 } while (true);
495
496 if (valuelen < 0) {
497 // Skip to next attribute.
498 next_key();
499 continue;
500 }
501
502 // Write key:value pair on destination
503#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) && !defined(Q_OS_MAC)
504 ssize_t destlen = fsetxattr(fd: dest_fd, name: key.constData(), value: value.constData(), size: valuelen, flags: 0);
505#elif defined(Q_OS_MAC)
506 ssize_t destlen = fsetxattr(dest_fd, key.constData(), value.constData(), valuelen, 0, 0);
507#elif HAVE_SYS_EXTATTR_H
508 ssize_t destlen = extattr_set_fd(dest_fd, EXTATTR_NAMESPACE_USER, key.constData(), value.constData(), valuelen);
509#endif
510 if (destlen == -1 && errno == ENOTSUP) {
511 qCDebug(KIO_FILE) << "Destination filesystem does not support xattrs";
512 return false;
513 }
514 if (destlen == -1 && (errno == ENOSPC || errno == EDQUOT)) {
515 return false;
516 }
517
518 next_key();
519 }
520 return true;
521}
522#endif // HAVE_SYS_XATTR_H || HAVE_SYS_EXTATTR_H
523
524WorkerResult FileProtocol::copy(const QUrl &srcUrl, const QUrl &destUrl, int _mode, JobFlags _flags)
525{
526 if (privilegeOperationUnitTestMode()) {
527 return WorkerResult::pass();
528 }
529
530 qCDebug(KIO_FILE) << "copy()" << srcUrl << "to" << destUrl << "mode=" << _mode;
531
532 const QString src = srcUrl.toLocalFile();
533 QString dest = destUrl.toLocalFile();
534 QByteArray _src(QFile::encodeName(fileName: src));
535 QByteArray _dest(QFile::encodeName(fileName: dest));
536 QByteArray _destBackup;
537
538 QT_STATBUF buffSrc;
539 if (QT_STAT(file: _src.data(), buf: &buffSrc) == -1) {
540 if (errno == EACCES) {
541 return WorkerResult::fail(error: KIO::ERR_ACCESS_DENIED, errorString: src);
542 } else {
543 return WorkerResult::fail(error: KIO::ERR_DOES_NOT_EXIST, errorString: src);
544 }
545 }
546
547 if (S_ISDIR(buffSrc.st_mode)) {
548 return WorkerResult::fail(error: KIO::ERR_IS_DIRECTORY, errorString: src);
549 }
550 if (S_ISFIFO(buffSrc.st_mode) || S_ISSOCK(buffSrc.st_mode)) {
551 return WorkerResult::fail(error: KIO::ERR_CANNOT_OPEN_FOR_READING, errorString: src);
552 }
553
554 QT_STATBUF buffDest;
555 bool dest_exists = (QT_LSTAT(file: _dest.data(), buf: &buffDest) != -1);
556 if (dest_exists) {
557 if (same_inode(src: buffDest, dest: buffSrc)) {
558 return WorkerResult::fail(error: KIO::ERR_IDENTICAL_FILES, errorString: dest);
559 }
560
561 if (S_ISDIR(buffDest.st_mode)) {
562 return WorkerResult::fail(error: KIO::ERR_DIR_ALREADY_EXIST, errorString: dest);
563 }
564
565 if (_flags & KIO::Overwrite) {
566 // If the destination is a symlink and overwrite is TRUE,
567 // remove the symlink first to prevent the scenario where
568 // the symlink actually points to current source!
569 if (S_ISLNK(buffDest.st_mode)) {
570 // qDebug() << "copy(): LINK DESTINATION";
571 if (!QFile::remove(fileName: dest)) {
572 auto result = execWithElevatedPrivilege(action: DEL, args: {_dest}, errno);
573 if (!result.success()) {
574 if (!resultWasCancelled(result)) {
575 return WorkerResult::fail(error: KIO::ERR_CANNOT_DELETE_ORIGINAL, errorString: dest);
576 }
577 return result;
578 }
579 }
580 } else if (S_ISREG(buffDest.st_mode) && !isOnCifsMount(filePath: dest)) {
581 _destBackup = _dest;
582 dest.append(QStringLiteral(".part"));
583 _dest = QFile::encodeName(fileName: dest);
584 }
585 } else {
586 return WorkerResult::fail(error: KIO::ERR_FILE_ALREADY_EXIST, errorString: dest);
587 }
588 }
589
590 QFile srcFile(src);
591 if (!srcFile.open(flags: QIODevice::ReadOnly)) {
592 auto result = tryOpen(f&: srcFile, path: _src, O_RDONLY, S_IRUSR, errno);
593 if (!result.success()) {
594 if (!resultWasCancelled(result)) {
595 return WorkerResult::fail(error: KIO::ERR_CANNOT_OPEN_FOR_READING, errorString: src);
596 }
597 return result;
598 }
599 }
600
601#if HAVE_FADVISE
602 posix_fadvise(fd: srcFile.handle(), offset: 0, len: 0, POSIX_FADV_SEQUENTIAL);
603#endif
604
605 QFile destFile(dest);
606 if (!destFile.open(flags: QIODevice::Truncate | QIODevice::WriteOnly)) {
607 auto result = tryOpen(f&: destFile, path: _dest, O_WRONLY | O_TRUNC | O_CREAT, S_IRUSR | S_IWUSR, errno);
608 if (!result.success()) {
609 int err = result.error();
610 if (!resultWasCancelled(result)) {
611 // qDebug() << "###### COULD NOT WRITE " << dest;
612 if (err == EACCES) {
613 return WorkerResult::fail(error: KIO::ERR_WRITE_ACCESS_DENIED, errorString: dest);
614 } else {
615 return WorkerResult::fail(error: KIO::ERR_CANNOT_OPEN_FOR_WRITING, errorString: dest);
616 }
617 }
618 return result;
619 }
620 return WorkerResult::pass();
621 }
622
623 // _mode == -1 means don't touch dest permissions, leave it with the system default ones
624 if (_mode != -1) {
625 if (::chmod(file: _dest.constData(), mode: _mode) == -1) {
626 const int errCode = errno;
627 KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(path: dest);
628 // Eat the error if the filesystem apparently doesn't support chmod.
629 // This test isn't fullproof though, vboxsf (VirtualBox shared folder) supports
630 // chmod if the host is Linux, and doesn't if the host is Windows. Hard to detect.
631 if (mp && mp->testFileSystemFlag(flag: KMountPoint::SupportsChmod)) {
632 if (!tryChangeFileAttr(action: CHMOD, args: {_dest, _mode}, errcode: errCode).success()) {
633 qCWarning(KIO_FILE) << "Could not change permissions for" << dest;
634 }
635 }
636 }
637 }
638
639#if HAVE_FADVISE
640 posix_fadvise(fd: destFile.handle(), offset: 0, len: 0, POSIX_FADV_SEQUENTIAL);
641#endif
642
643 const auto srcSize = buffSrc.st_size;
644 totalSize(bytes: srcSize);
645
646 off_t sizeProcessed = 0;
647
648#ifdef FICLONE
649 // Share data blocks ("reflink") on supporting filesystems, like brfs and XFS
650 int ret = ::ioctl(fd: destFile.handle(), FICLONE, srcFile.handle());
651 if (ret != -1) {
652 sizeProcessed = srcSize;
653 processedSize(bytes: srcSize);
654 }
655 // if fs does not support reflinking, files are on different devices...
656#endif
657
658 bool existingDestDeleteAttempted = false;
659
660 processedSize(bytes: sizeProcessed);
661
662#if HAVE_COPY_FILE_RANGE
663 while (!wasKilled() && sizeProcessed < srcSize) {
664 if (testMode && destFile.fileName().contains(s: QLatin1String("slow"))) {
665 QThread::msleep(50);
666 }
667
668 const ssize_t copiedBytes = ::copy_file_range(infd: srcFile.handle(), pinoff: nullptr, outfd: destFile.handle(), poutoff: nullptr, length: s_maxIPCSize, flags: 0);
669
670 if (copiedBytes == -1) {
671 // ENOENT is returned on cifs in some cases, probably a kernel bug
672 // (s.a. https://git.savannah.gnu.org/cgit/coreutils.git/commit/?id=7fc84d1c0f6b35231b0b4577b70aaa26bf548a7c)
673 if (errno == EINVAL || errno == EXDEV || errno == ENOENT) {
674 break; // will continue with next copy mechanism
675 }
676
677 if (errno == EINTR) { // Interrupted
678 continue;
679 }
680
681 if (errno == ENOSPC) { // disk full
682 // attempt to free disk space occupied by file being overwritten
683 if (!_destBackup.isEmpty() && !existingDestDeleteAttempted) {
684 ::unlink(name: _destBackup.constData());
685 existingDestDeleteAttempted = true;
686 continue;
687 }
688
689 if (!QFile::remove(fileName: dest)) { // don't keep partly copied file
690 auto result = execWithElevatedPrivilege(action: DEL, args: {_dest}, errno);
691 if (!result.success()) {
692 return result;
693 }
694 }
695
696 return WorkerResult::fail(error: KIO::ERR_DISK_FULL, errorString: dest);
697 }
698
699 if (!QFile::remove(fileName: dest)) { // don't keep partly copied file
700 auto result = execWithElevatedPrivilege(action: DEL, args: {_dest}, errno);
701 if (!result.success()) {
702 return result;
703 }
704 }
705
706 return WorkerResult::fail(error: KIO::ERR_WORKER_DEFINED, i18n("Cannot copy file from %1 to %2. (Errno: %3)", src, dest, errno));
707 }
708
709 sizeProcessed += copiedBytes;
710 processedSize(bytes: sizeProcessed);
711 }
712#endif
713
714 /* standard read/write fallback */
715 if (sizeProcessed < srcSize) {
716 std::array<char, s_maxIPCSize> buffer;
717 while (!wasKilled() && sizeProcessed < srcSize) {
718 if (testMode && destFile.fileName().contains(s: QLatin1String("slow"))) {
719 QThread::msleep(50);
720 }
721
722 const ssize_t readBytes = ::read(fd: srcFile.handle(), buf: &buffer, nbytes: s_maxIPCSize);
723
724 if (readBytes == -1) {
725 if (errno == EINTR) { // Interrupted
726 continue;
727 } else {
728 qCWarning(KIO_FILE) << "Couldn't read[2]. Error:" << srcFile.errorString();
729 }
730
731 if (!QFile::remove(fileName: dest)) { // don't keep partly copied file
732 auto result = execWithElevatedPrivilege(action: DEL, args: {_dest}, errno);
733 if (!result.success()) {
734 return result;
735 }
736 }
737 return WorkerResult::fail(error: KIO::ERR_CANNOT_READ, errorString: src);
738 }
739
740 if (destFile.write(data: buffer.data(), len: readBytes) != readBytes) {
741 int error = KIO::ERR_CANNOT_WRITE;
742 if (destFile.error() == QFileDevice::ResourceError) { // disk full
743 // attempt to free disk space occupied by file being overwritten
744 if (!_destBackup.isEmpty() && !existingDestDeleteAttempted) {
745 ::unlink(name: _destBackup.constData());
746 existingDestDeleteAttempted = true;
747 if (destFile.write(data: buffer.data(), len: readBytes) == readBytes) { // retry
748 continue;
749 }
750 }
751 error = KIO::ERR_DISK_FULL;
752 } else {
753 qCWarning(KIO_FILE) << "Couldn't write[2]. Error:" << destFile.errorString();
754 }
755
756 if (!QFile::remove(fileName: dest)) { // don't keep partly copied file
757 auto result = execWithElevatedPrivilege(action: DEL, args: {_dest}, errno);
758 if (!result.success()) {
759 return result;
760 }
761 }
762 return WorkerResult::fail(error: error, errorString: dest);
763 }
764 sizeProcessed += readBytes;
765 processedSize(bytes: sizeProcessed);
766 }
767 }
768
769 // Copy Extended attributes
770#if HAVE_SYS_XATTR_H || HAVE_SYS_EXTATTR_H
771 if (!copyXattrs(src_fd: srcFile.handle(), dest_fd: destFile.handle())) {
772 qCDebug(KIO_FILE) << "can't copy Extended attributes";
773 }
774#endif
775
776 srcFile.close();
777
778 destFile.flush(); // so the write() happens before futimes()
779
780 // copy access and modification time
781 if (!wasKilled()) {
782#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
783 // with nano secs precision
784 struct timespec ut[2];
785 ut[0] = buffSrc.st_atim;
786 ut[1] = buffSrc.st_mtim;
787 // need to do this with the dest file still opened, or this fails
788 if (::futimens(fd: destFile.handle(), times: ut) != 0) {
789#else
790 struct timeval ut[2];
791 ut[0].tv_sec = buffSrc.st_atime;
792 ut[0].tv_usec = 0;
793 ut[1].tv_sec = buffSrc.st_mtime;
794 ut[1].tv_usec = 0;
795 if (::futimes(destFile.handle(), ut) != 0) {
796#endif
797 if (!tryChangeFileAttr(action: UTIME, args: {_dest, qint64(buffSrc.st_atime), qint64(buffSrc.st_mtime)}, errno).success()) {
798 qCWarning(KIO_FILE) << "Couldn't preserve access and modification time for" << dest;
799 }
800 }
801 }
802
803 destFile.close();
804
805 if (wasKilled()) {
806 qCDebug(KIO_FILE) << "Clean dest file after KIO worker was killed:" << dest;
807 if (!QFile::remove(fileName: dest)) { // don't keep partly copied file
808 execWithElevatedPrivilege(action: DEL, args: {_dest}, errno);
809 }
810 return WorkerResult::fail(error: KIO::ERR_USER_CANCELED, errorString: dest);
811 }
812
813 if (destFile.error() != QFile::NoError) {
814 qCWarning(KIO_FILE) << "Error when closing file descriptor[2]:" << destFile.errorString();
815
816 if (!QFile::remove(fileName: dest)) { // don't keep partly copied file
817 execWithElevatedPrivilege(action: DEL, args: {_dest}, errno);
818 }
819
820 return WorkerResult::fail(error: KIO::ERR_CANNOT_WRITE, errorString: dest);
821 }
822
823#if HAVE_POSIX_ACL
824 // If no special mode is given, preserve the ACL attributes from the source file
825 if (_mode == -1) {
826 acl_t acl = acl_get_fd(srcFile.handle());
827 if (acl && acl_set_file(_dest.data(), ACL_TYPE_ACCESS, acl) != 0) {
828 qCWarning(KIO_FILE) << "Could not set ACL permissions for" << dest;
829 }
830 }
831#endif
832
833 // preserve ownership
834 if (_mode != -1) {
835 if (::chown(file: _dest.data(), owner: -1 /*keep user*/, group: buffSrc.st_gid) == 0) {
836 // as we are the owner of the new file, we can always change the group, but
837 // we might not be allowed to change the owner
838 (void)::chown(file: _dest.data(), owner: buffSrc.st_uid, group: -1 /*keep group*/);
839 } else {
840 if (!tryChangeFileAttr(action: CHOWN, args: {_dest, buffSrc.st_uid, buffSrc.st_gid}, errno).success()) {
841 qCWarning(KIO_FILE) << "Couldn't preserve group for" << dest;
842 }
843 }
844 }
845
846 if (!_destBackup.isEmpty()) { // Overwrite final dest file with new file
847 if (::unlink(name: _destBackup.constData()) == -1) {
848 qCWarning(KIO_FILE) << "Couldn't remove original dest" << _destBackup << "(" << strerror(errno) << ")";
849 }
850
851 if (::rename(old: _dest.constData(), new: _destBackup.constData()) == -1) {
852 qCWarning(KIO_FILE) << "Couldn't rename" << _dest << "to" << _destBackup << "(" << strerror(errno) << ")";
853 }
854 }
855
856 processedSize(bytes: srcSize);
857 return WorkerResult::pass();
858}
859
860static bool isLocalFileSameHost(const QUrl &url)
861{
862 if (!url.isLocalFile()) {
863 return false;
864 }
865
866 if (url.host().isEmpty() || (url.host() == QLatin1String("localhost"))) {
867 return true;
868 }
869
870 char hostname[256];
871 hostname[0] = '\0';
872 if (!gethostname(name: hostname, len: 255)) {
873 hostname[sizeof(hostname) - 1] = '\0';
874 }
875
876 return (QString::compare(s1: url.host(), s2: QLatin1String(hostname), cs: Qt::CaseInsensitive) == 0);
877}
878
879#if HAVE_SYS_XATTR_H
880static bool isNtfsHidden(const QString &filename)
881{
882 constexpr auto attrName = "system.ntfs_attrib_be";
883 const auto filenameEncoded = QFile::encodeName(fileName: filename);
884
885 uint32_t intAttr = 0;
886 constexpr size_t xattr_size = sizeof(intAttr);
887 char strAttr[xattr_size];
888#ifdef Q_OS_MACOS
889 auto length = getxattr(filenameEncoded.data(), attrName, strAttr, xattr_size, 0, XATTR_NOFOLLOW);
890#else
891 auto length = getxattr(path: filenameEncoded.data(), name: attrName, value: strAttr, size: xattr_size);
892#endif
893 if (length <= 0) {
894 return false;
895 }
896
897 char *c = strAttr;
898 for (decltype(length) n = 0; n < length; ++n, ++c) {
899 intAttr <<= 8;
900 intAttr |= static_cast<uchar>(*c);
901 }
902
903 constexpr auto FILE_ATTRIBUTE_HIDDEN = 0x2u;
904 return static_cast<bool>(intAttr & FILE_ATTRIBUTE_HIDDEN);
905}
906#endif
907
908WorkerResult FileProtocol::listDir(const QUrl &url)
909{
910 if (!isLocalFileSameHost(url)) {
911 QUrl redir(url);
912 redir.setScheme(configValue(QStringLiteral("DefaultRemoteProtocol"), QStringLiteral("smb")));
913 redirection(url: redir);
914 // qDebug() << "redirecting to " << redir;
915 return WorkerResult::pass();
916 }
917 const QString path(url.toLocalFile());
918 const QByteArray _path(QFile::encodeName(fileName: path));
919 DIR *dp = opendir(name: _path.data());
920 if (dp == nullptr) {
921 switch (errno) {
922 case ENOENT:
923 return WorkerResult::fail(error: KIO::ERR_DOES_NOT_EXIST, errorString: path);
924 case ENOTDIR:
925 return WorkerResult::fail(error: KIO::ERR_IS_FILE, errorString: path);
926#ifdef ENOMEDIUM
927 case ENOMEDIUM:
928 return WorkerResult::fail(error: ERR_WORKER_DEFINED, i18n("No media in device for %1", path));
929#endif
930 default:
931 return WorkerResult::fail(error: KIO::ERR_CANNOT_ENTER_DIRECTORY, errorString: path);
932 break;
933 }
934 }
935
936 const QByteArray encodedBasePath = _path + '/';
937
938 const KIO::StatDetails details = getStatDetails();
939 // qDebug() << "========= LIST " << url << "details=" << details << " =========";
940 UDSEntry entry;
941
942#ifndef HAVE_DIRENT_D_TYPE
943 QT_STATBUF st;
944#endif
945 QT_DIRENT *ep;
946 while ((ep = QT_READDIR(dirp: dp)) != nullptr) {
947 entry.clear();
948
949 const QString filename = QFile::decodeName(localFileName: ep->d_name);
950
951 /*
952 * details == 0 (if statement) is the fast code path.
953 * We only get the file name and type. After that we emit
954 * the result.
955 *
956 * The else statement is the slow path that requests all
957 * file information in file.cpp. It executes a stat call
958 * for every entry thus becoming slower.
959 *
960 */
961 if (details == KIO::StatBasic) {
962 entry.fastInsert(field: KIO::UDSEntry::UDS_NAME, value: filename);
963#ifdef HAVE_DIRENT_D_TYPE
964 entry.fastInsert(field: KIO::UDSEntry::UDS_FILE_TYPE, l: (ep->d_type == DT_DIR) ? S_IFDIR : S_IFREG);
965 const bool isSymLink = (ep->d_type == DT_LNK);
966#else
967 // oops, no fast way, we need to stat (e.g. on Solaris)
968 if (QT_LSTAT(ep->d_name, &st) == -1) {
969 continue; // how can stat fail?
970 }
971 entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_ISDIR(st.st_mode) ? S_IFDIR : S_IFREG);
972 const bool isSymLink = S_ISLNK(st.st_mode);
973#endif
974 if (isSymLink) {
975 // for symlinks obey the UDSEntry contract and provide UDS_LINK_DEST
976 // even if we don't know the link dest (and DeleteJob doesn't care...)
977 entry.fastInsert(field: KIO::UDSEntry::UDS_LINK_DEST, QStringLiteral("Dummy Link Target"));
978 }
979 listEntry(entry);
980
981 } else {
982 QString fullPath = Utils::slashAppended(s: path);
983 fullPath += filename;
984
985 if (createUDSEntry(filename, path: encodedBasePath + QByteArray(ep->d_name), entry, details, fullPath)) {
986#if HAVE_SYS_XATTR_H
987 if (isNtfsHidden(filename)) {
988 bool ntfsHidden = true;
989
990 // Bug 392913: NTFS root volume is always "hidden", ignore this
991 if (ep->d_type == DT_DIR || ep->d_type == DT_UNKNOWN || ep->d_type == DT_LNK) {
992 const QString fullFilePath = QDir(filename).canonicalPath();
993 auto mountPoint = KMountPoint::currentMountPoints().findByPath(path: fullFilePath);
994 if (mountPoint && mountPoint->mountPoint() == fullFilePath) {
995 ntfsHidden = false;
996 }
997 }
998
999 if (ntfsHidden) {
1000 entry.fastInsert(field: KIO::UDSEntry::UDS_HIDDEN, l: 1);
1001 }
1002 }
1003#endif
1004 listEntry(entry);
1005 }
1006 }
1007 }
1008
1009 closedir(dirp: dp);
1010
1011 return WorkerResult::pass();
1012}
1013
1014WorkerResult FileProtocol::rename(const QUrl &srcUrl, const QUrl &destUrl, KIO::JobFlags _flags)
1015{
1016 char off_t_should_be_64_bits[sizeof(off_t) >= 8 ? 1 : -1];
1017 (void)off_t_should_be_64_bits;
1018 const QString src = srcUrl.toLocalFile();
1019 const QString dest = destUrl.toLocalFile();
1020 const QByteArray _src(QFile::encodeName(fileName: src));
1021 const QByteArray _dest(QFile::encodeName(fileName: dest));
1022 QT_STATBUF buff_src;
1023 if (QT_LSTAT(file: _src.data(), buf: &buff_src) == -1) {
1024 if (errno == EACCES) {
1025 return WorkerResult::fail(error: KIO::ERR_ACCESS_DENIED, errorString: src);
1026 } else {
1027 return WorkerResult::fail(error: KIO::ERR_DOES_NOT_EXIST, errorString: src);
1028 }
1029 }
1030
1031 QT_STATBUF buff_dest;
1032 // stat symlinks here (lstat, not stat), to avoid ERR_IDENTICAL_FILES when replacing symlink
1033 // with its target (#169547)
1034 bool dest_exists = (QT_LSTAT(file: _dest.data(), buf: &buff_dest) != -1);
1035 if (dest_exists) {
1036 // Try QFile::rename(), this can help when renaming 'a' to 'A' on a case-insensitive
1037 // filesystem, e.g. FAT32/VFAT.
1038 if (src != dest && QString::compare(s1: src, s2: dest, cs: Qt::CaseInsensitive) == 0) {
1039 qCDebug(KIO_FILE) << "Dest already exists; detected special case of lower/uppercase renaming"
1040 << "in same dir on a case-insensitive filesystem, try with QFile::rename()"
1041 << "(which uses 2 rename calls)";
1042 if (QFile::rename(oldName: src, newName: dest)) {
1043 return WorkerResult::pass();
1044 }
1045 }
1046
1047 if (same_inode(src: buff_dest, dest: buff_src)) {
1048 return WorkerResult::fail(error: KIO::ERR_IDENTICAL_FILES, errorString: dest);
1049 }
1050
1051 if (S_ISDIR(buff_dest.st_mode)) {
1052 return WorkerResult::fail(error: KIO::ERR_DIR_ALREADY_EXIST, errorString: dest);
1053 }
1054
1055 if (!(_flags & KIO::Overwrite)) {
1056 return WorkerResult::fail(error: KIO::ERR_FILE_ALREADY_EXIST, errorString: dest);
1057 }
1058 }
1059
1060 if (::rename(old: _src.data(), new: _dest.data()) == -1) {
1061 auto result = execWithElevatedPrivilege(action: RENAME, args: {_src, _dest}, errno);
1062 if (!result.success()) {
1063 if (!resultWasCancelled(result)) {
1064 int err = result.error();
1065 if ((err == EACCES) || (err == EPERM)) {
1066 return WorkerResult::fail(error: KIO::ERR_WRITE_ACCESS_DENIED, errorString: dest);
1067 } else if (err == EXDEV) {
1068 return WorkerResult::fail(error: KIO::ERR_UNSUPPORTED_ACTION, QStringLiteral("rename"));
1069 } else if (err == EROFS) { // The file is on a read-only filesystem
1070 return WorkerResult::fail(error: KIO::ERR_CANNOT_DELETE, errorString: src);
1071 } else {
1072 return WorkerResult::fail(error: KIO::ERR_CANNOT_RENAME, errorString: src);
1073 }
1074 }
1075 }
1076 return result;
1077 }
1078
1079 return WorkerResult::pass();
1080}
1081
1082WorkerResult FileProtocol::symlink(const QString &target, const QUrl &destUrl, KIO::JobFlags flags)
1083{
1084 // Assume dest is local too (wouldn't be here otherwise)
1085 const QString dest = destUrl.toLocalFile();
1086 const QByteArray dest_c = QFile::encodeName(fileName: dest);
1087
1088 if (::symlink(from: QFile::encodeName(fileName: target).constData(), to: dest_c.constData()) == 0) {
1089 return WorkerResult::pass();
1090 }
1091
1092 // Does the destination already exist ?
1093 if (errno == EEXIST) {
1094 if (flags & KIO::Overwrite) {
1095 // Try to delete the destination
1096 if (unlink(name: dest_c.constData()) != 0) {
1097 auto result = execWithElevatedPrivilege(action: DEL, args: {dest}, errno);
1098 if (!result.success()) {
1099 if (!resultWasCancelled(result)) {
1100 return WorkerResult::fail(error: KIO::ERR_CANNOT_DELETE, errorString: dest);
1101 }
1102
1103 return result;
1104 }
1105 }
1106
1107 // Try again - this won't loop forever since unlink succeeded
1108 return symlink(target, destUrl, flags);
1109 } else {
1110 if (QT_STATBUF buff_dest; QT_LSTAT(file: dest_c.constData(), buf: &buff_dest) == 0) {
1111 return WorkerResult::fail(S_ISDIR(buff_dest.st_mode) ? KIO::ERR_DIR_ALREADY_EXIST : KIO::ERR_FILE_ALREADY_EXIST, errorString: dest);
1112 } else { // Can't happen, we already know "dest" exists
1113 return WorkerResult::fail(error: KIO::ERR_CANNOT_SYMLINK, errorString: dest);
1114 }
1115
1116 return WorkerResult::pass();
1117 }
1118 }
1119
1120 // Permission error, could be that the filesystem doesn't support symlinks
1121 if (errno == EPERM) {
1122 // "dest" doesn't exist, get the filesystem type of the parent dir
1123 const QString parentDir = destUrl.adjusted(options: QUrl::StripTrailingSlash | QUrl::RemoveFilename).toLocalFile();
1124 const KFileSystemType::Type fsType = KFileSystemType::fileSystemType(path: parentDir);
1125
1126 if (fsType == KFileSystemType::Fat || fsType == KFileSystemType::Exfat) {
1127 const QString msg = i18nc(
1128 "The first arg is the path to the symlink that couldn't be created, the second"
1129 "arg is the filesystem type (e.g. vfat, exfat)",
1130 "Could not create symlink \"%1\".\n"
1131 "The destination filesystem (%2) doesn't support symlinks.",
1132 dest,
1133 KFileSystemType::fileSystemName(fsType));
1134
1135 return WorkerResult::fail(error: KIO::ERR_WORKER_DEFINED, errorString: msg);
1136 }
1137 }
1138
1139 auto result = execWithElevatedPrivilege(action: SYMLINK, args: {dest, target}, errno);
1140 if (!result.success()) {
1141 if (!resultWasCancelled(result)) {
1142 // Some error occurred while we tried to symlink
1143 return WorkerResult::fail(error: KIO::ERR_CANNOT_SYMLINK, errorString: dest);
1144 }
1145 return result;
1146 }
1147 return WorkerResult::pass();
1148}
1149
1150WorkerResult FileProtocol::del(const QUrl &url, bool isfile)
1151{
1152 const QString path = url.toLocalFile();
1153 const QByteArray _path(QFile::encodeName(fileName: path));
1154 /*****
1155 * Delete files
1156 *****/
1157
1158 if (isfile) {
1159 // qDebug() << "Deleting file "<< url;
1160
1161 if (unlink(name: _path.data()) == -1) {
1162 auto result = execWithElevatedPrivilege(action: DEL, args: {_path}, errno);
1163 if (!result.success()) {
1164 auto err = result.error();
1165 if (!resultWasCancelled(result)) {
1166 if ((err == EACCES) || (err == EPERM)) {
1167 return WorkerResult::fail(error: KIO::ERR_ACCESS_DENIED, errorString: path);
1168 } else if (err == EISDIR) {
1169 return WorkerResult::fail(error: KIO::ERR_IS_DIRECTORY, errorString: path);
1170 } else {
1171 return WorkerResult::fail(error: KIO::ERR_CANNOT_DELETE, errorString: path);
1172 }
1173 }
1174 return result;
1175 }
1176 return WorkerResult::pass();
1177 }
1178 } else {
1179 /*****
1180 * Delete empty directory
1181 *****/
1182
1183 // qDebug() << "Deleting directory " << url;
1184 if (metaData(QStringLiteral("recurse")) == QLatin1String("true")) {
1185 auto result = deleteRecursive(path);
1186 if (!result.success()) {
1187 return result;
1188 }
1189 }
1190 if (QT_RMDIR(path: _path.data()) == -1) {
1191 auto result = execWithElevatedPrivilege(action: RMDIR, args: {_path}, errno);
1192 if (!result.success()) {
1193 if (!resultWasCancelled(result)) {
1194 if ((result.error() == EACCES) || (result.error() == EPERM)) {
1195 return WorkerResult::fail(error: KIO::ERR_ACCESS_DENIED, errorString: path);
1196 } else {
1197 // qDebug() << "could not rmdir " << perror;
1198 return WorkerResult::fail(error: KIO::ERR_CANNOT_RMDIR, errorString: path);
1199 }
1200 }
1201 return result;
1202 }
1203 }
1204 }
1205 return WorkerResult::pass();
1206}
1207
1208WorkerResult FileProtocol::chown(const QUrl &url, const QString &owner, const QString &group)
1209{
1210 const QString path = url.toLocalFile();
1211 const QByteArray _path(QFile::encodeName(fileName: path));
1212 uid_t uid;
1213 gid_t gid;
1214
1215 // get uid from given owner
1216 {
1217 struct passwd *p = ::getpwnam(name: owner.toLocal8Bit().constData());
1218
1219 if (!p) {
1220 return WorkerResult::fail(error: KIO::ERR_WORKER_DEFINED, i18n("Could not get user id for given user name %1", owner));
1221 }
1222
1223 uid = p->pw_uid;
1224 }
1225
1226 // get gid from given group
1227 {
1228 struct group *p = ::getgrnam(name: group.toLocal8Bit().constData());
1229
1230 if (!p) {
1231 return WorkerResult::fail(error: KIO::ERR_WORKER_DEFINED, i18n("Could not get group id for given group name %1", group));
1232 }
1233
1234 gid = p->gr_gid;
1235 }
1236
1237 if (::chown(file: _path.constData(), owner: uid, group: gid) == -1) {
1238 auto result = execWithElevatedPrivilege(action: CHOWN, args: {_path, uid, gid}, errno);
1239 if (!result.success()) {
1240 if (!resultWasCancelled(result)) {
1241 switch (result.error()) {
1242 case EPERM:
1243 case EACCES:
1244 return WorkerResult::fail(error: KIO::ERR_ACCESS_DENIED, errorString: path);
1245 break;
1246 case ENOSPC:
1247 return WorkerResult::fail(error: KIO::ERR_DISK_FULL, errorString: path);
1248 break;
1249 default:
1250 return WorkerResult::fail(error: KIO::ERR_CANNOT_CHOWN, errorString: path);
1251 }
1252 }
1253 }
1254 }
1255
1256 return WorkerResult::pass();
1257}
1258
1259WorkerResult FileProtocol::stat(const QUrl &url)
1260{
1261 if (!isLocalFileSameHost(url)) {
1262 return redirect(url);
1263 }
1264
1265 /* directories may not have a slash at the end if
1266 * we want to stat() them; it requires that we
1267 * change into it .. which may not be allowed
1268 * stat("/is/unaccessible") -> rwx------
1269 * stat("/is/unaccessible/") -> EPERM H.Z.
1270 * This is the reason for the -1
1271 */
1272 const QString path(url.adjusted(options: QUrl::StripTrailingSlash).toLocalFile());
1273 const QByteArray _path(QFile::encodeName(fileName: path));
1274
1275 const KIO::StatDetails details = getStatDetails();
1276
1277 UDSEntry entry;
1278 if (!createUDSEntry(filename: url.fileName(), path: _path, entry, details, fullPath: path)) {
1279 return WorkerResult::fail(error: KIO::ERR_DOES_NOT_EXIST, errorString: path);
1280 }
1281 statEntry(entry: entry);
1282
1283 return WorkerResult::pass();
1284}
1285
1286WorkerResult FileProtocol::execWithElevatedPrivilege(ActionType action, const QVariantList &args, int errcode)
1287{
1288 if (privilegeOperationUnitTestMode()) {
1289 return WorkerResult::pass();
1290 }
1291
1292 // temporarily disable privilege execution
1293 if (true) {
1294 return WorkerResult::fail(error: errcode);
1295 }
1296
1297 if (!(errcode == EACCES || errcode == EPERM)) {
1298 return WorkerResult::fail(error: errcode);
1299 }
1300
1301 const QString operationDetails = actionDetails(actionType: action, args);
1302 KIO::PrivilegeOperationStatus opStatus = requestPrivilegeOperation(operationDetails);
1303 if (opStatus != KIO::OperationAllowed) {
1304 if (opStatus == KIO::OperationCanceled) {
1305 return WorkerResult::fail(error: KIO::ERR_USER_CANCELED, errorString: QString());
1306 }
1307 return WorkerResult::fail(error: errcode);
1308 }
1309
1310 const QUrl targetUrl = QUrl::fromLocalFile(localfile: args.first().toString()); // target is always the first item.
1311 const bool useParent = action != CHOWN && action != CHMOD && action != UTIME;
1312 const QString targetPath = useParent ? targetUrl.adjusted(options: QUrl::RemoveFilename).toLocalFile() : targetUrl.toLocalFile();
1313 bool userIsOwner = QFileInfo(targetPath).ownerId() == getuid();
1314 if (action == RENAME) { // for rename check src and dest owner
1315 QString dest = QUrl(args[1].toString()).toLocalFile();
1316 userIsOwner = userIsOwner && QFileInfo(dest).ownerId() == getuid();
1317 }
1318 if (userIsOwner) {
1319 return WorkerResult::fail(error: KIO::ERR_PRIVILEGE_NOT_REQUIRED, errorString: targetPath);
1320 }
1321
1322 QByteArray helperArgs;
1323 QDataStream out(&helperArgs, QIODevice::WriteOnly);
1324 out << action;
1325 for (const QVariant &arg : args) {
1326 out << arg;
1327 }
1328
1329 const QString actionId = QStringLiteral("org.kde.kio.file.exec");
1330 KAuth::Action execAction(actionId);
1331 execAction.setHelperId(QStringLiteral("org.kde.kio.file"));
1332
1333 QVariantMap argv;
1334 argv.insert(QStringLiteral("arguments"), value: helperArgs);
1335 execAction.setArguments(argv);
1336
1337 auto reply = execAction.execute();
1338 if (reply->exec()) {
1339 addTemporaryAuthorization(action: actionId);
1340 return WorkerResult::pass();
1341 }
1342
1343 return WorkerResult::fail(error: KIO::ERR_ACCESS_DENIED);
1344}
1345
1346int FileProtocol::setACL(const char *path, mode_t perm, bool directoryDefault)
1347{
1348 int ret = 0;
1349#if HAVE_POSIX_ACL
1350
1351 const QString ACLString = metaData(QStringLiteral("ACL_STRING"));
1352 const QString defaultACLString = metaData(QStringLiteral("DEFAULT_ACL_STRING"));
1353 // Empty strings mean leave as is
1354 if (!ACLString.isEmpty()) {
1355 acl_t acl = nullptr;
1356 if (ACLString == QLatin1String("ACL_DELETE")) {
1357 // user told us to delete the extended ACL, so let's write only
1358 // the minimal (UNIX permission bits) part
1359 acl = ACLPortability::acl_from_mode(perm);
1360 }
1361 acl = acl_from_text(ACLString.toLatin1().constData());
1362 if (acl_valid(acl) == 0) { // let's be safe
1363 ret = acl_set_file(path, ACL_TYPE_ACCESS, acl);
1364 // qDebug() << "Set ACL on:" << path << "to:" << aclToText(acl);
1365 }
1366 acl_free(acl);
1367 if (ret != 0) {
1368 return ret; // better stop trying right away
1369 }
1370 }
1371
1372 if (directoryDefault && !defaultACLString.isEmpty()) {
1373 if (defaultACLString == QLatin1String("ACL_DELETE")) {
1374 // user told us to delete the default ACL, do so
1375 ret += acl_delete_def_file(path);
1376 } else {
1377 acl_t acl = acl_from_text(defaultACLString.toLatin1().constData());
1378 if (acl_valid(acl) == 0) { // let's be safe
1379 ret += acl_set_file(path, ACL_TYPE_DEFAULT, acl);
1380 // qDebug() << "Set Default ACL on:" << path << "to:" << aclToText(acl);
1381 }
1382 acl_free(acl);
1383 }
1384 }
1385#else
1386 Q_UNUSED(path);
1387 Q_UNUSED(perm);
1388 Q_UNUSED(directoryDefault);
1389#endif
1390 return ret;
1391}
1392

source code of kio/src/kioworkers/file/file_unix.cpp