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

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