File indexing completed on 2024-05-12 03:56:44

0001 /*
0002     SPDX-FileCopyrightText: 2000-2002 Stephan Kulow <coolo@kde.org>
0003     SPDX-FileCopyrightText: 2000-2002 David Faure <faure@kde.org>
0004     SPDX-FileCopyrightText: 2000-2002 Waldo Bastian <bastian@kde.org>
0005     SPDX-FileCopyrightText: 2006 Allan Sandfeld Jensen <sandfeld@kde.org>
0006     SPDX-FileCopyrightText: 2007 Thiago Macieira <thiago@kde.org>
0007     SPDX-FileCopyrightText: 2007 Christian Ehrlicher <ch.ehrlicher@gmx.de>
0008 
0009     SPDX-License-Identifier: LGPL-2.0-or-later
0010 */
0011 
0012 #include "file.h"
0013 
0014 #include "config-kioworker-file.h"
0015 
0016 #include "../utils_p.h"
0017 
0018 #if HAVE_POSIX_ACL
0019 #include <../../aclhelpers_p.h>
0020 #endif
0021 
0022 #include <QDir>
0023 #include <QFile>
0024 #include <QMimeDatabase>
0025 #include <QStandardPaths>
0026 #include <QThread>
0027 #include <qplatformdefs.h>
0028 
0029 #include <KConfigGroup>
0030 #include <KFileSystemType>
0031 #include <KLocalizedString>
0032 #include <QDebug>
0033 #include <kmountpoint.h>
0034 
0035 #include <array>
0036 #include <cerrno>
0037 #include <stdint.h>
0038 #include <utime.h>
0039 
0040 #include <KAuth/Action>
0041 #include <KAuth/ExecuteJob>
0042 #include <KRandom>
0043 
0044 #include "fdreceiver.h"
0045 
0046 #ifdef Q_OS_LINUX
0047 
0048 #include <linux/fs.h>
0049 #include <sys/ioctl.h>
0050 #include <unistd.h>
0051 
0052 #endif // Q_OS_LINUX
0053 
0054 #if HAVE_STATX
0055 #include <sys/stat.h>
0056 #include <sys/sysmacros.h> // for makedev()
0057 #endif
0058 
0059 #if HAVE_COPY_FILE_RANGE
0060 // sys/types.h must be included before unistd.h,
0061 // and it needs to be included explicitly for FreeBSD
0062 #include <sys/types.h>
0063 #include <unistd.h>
0064 #endif
0065 
0066 #if HAVE_SYS_XATTR_H
0067 #include <sys/xattr.h>
0068 // BSD uses a different include
0069 #elif HAVE_SYS_EXTATTR_H
0070 #include <sys/types.h> // For FreeBSD, this must be before sys/extattr.h
0071 
0072 #include <sys/extattr.h>
0073 #endif
0074 
0075 using namespace KIO;
0076 
0077 /* 512 kB */
0078 static constexpr int s_maxIPCSize = 1024 * 512;
0079 
0080 static bool same_inode(const QT_STATBUF &src, const QT_STATBUF &dest)
0081 {
0082     if (src.st_ino == dest.st_ino && src.st_dev == dest.st_dev) {
0083         return true;
0084     }
0085 
0086     return false;
0087 }
0088 
0089 static const QString socketPath()
0090 {
0091     const QString runtimeDir = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation);
0092     return QStringLiteral("%1/filehelper%2%3").arg(runtimeDir, KRandom::randomString(6)).arg(qlonglong(QThread::currentThreadId()));
0093 }
0094 
0095 static QString actionDetails(ActionType actionType, const QVariantList &args)
0096 {
0097     QString action;
0098     QString detail;
0099     switch (actionType) {
0100     case CHMOD:
0101         action = i18n("Change File Permissions");
0102         detail = i18n("New Permissions: %1", args[1].toInt());
0103         break;
0104     case CHOWN:
0105         action = i18n("Change File Owner");
0106         detail = i18n("New Owner: UID=%1, GID=%2", args[1].toInt(), args[2].toInt());
0107         break;
0108     case DEL:
0109         action = i18n("Remove File");
0110         break;
0111     case RMDIR:
0112         action = i18n("Remove Directory");
0113         break;
0114     case MKDIR:
0115         action = i18n("Create Directory");
0116         detail = i18n("Directory Permissions: %1", args[1].toInt());
0117         break;
0118     case OPEN:
0119         action = i18n("Open File");
0120         break;
0121     case OPENDIR:
0122         action = i18n("Open Directory");
0123         break;
0124     case RENAME:
0125         action = i18n("Rename");
0126         detail = i18n("New Filename: %1", args[1].toString());
0127         break;
0128     case SYMLINK:
0129         action = i18n("Create Symlink");
0130         detail = i18n("Target: %1", args[1].toString());
0131         break;
0132     case UTIME:
0133         action = i18n("Change Timestamp");
0134         break;
0135     case COPY:
0136         action = i18n("Copy");
0137         detail = i18n("From: %1, To: %2", args[0].toString(), args[1].toString());
0138         break;
0139     default:
0140         action = i18n("Unknown Action");
0141         break;
0142     }
0143 
0144     const QString metadata = i18n(
0145         "Action: %1\n"
0146         "Source: %2\n"
0147         "%3",
0148         action,
0149         args[0].toString(),
0150         detail);
0151     return metadata;
0152 }
0153 
0154 bool FileProtocol::privilegeOperationUnitTestMode()
0155 {
0156     return (metaData(QStringLiteral("UnitTesting")) == QLatin1String("true"))
0157         && (requestPrivilegeOperation(QStringLiteral("Test Call")) == KIO::OperationAllowed);
0158 }
0159 
0160 #if HAVE_POSIX_ACL
0161 bool FileProtocol::isExtendedACL(acl_t acl)
0162 {
0163     return (ACLPortability::acl_equiv_mode(acl, nullptr) != 0);
0164 }
0165 #endif
0166 
0167 #if HAVE_STATX
0168 // statx syscall is available
0169 inline int LSTAT(const char *path, struct statx *buff, KIO::StatDetails details)
0170 {
0171     uint32_t mask = 0;
0172     if (details & KIO::StatBasic) {
0173         // filename, access, type, size, linkdest
0174         mask |= STATX_SIZE | STATX_TYPE;
0175     }
0176     if (details & KIO::StatUser) {
0177         // uid, gid
0178         mask |= STATX_UID | STATX_GID;
0179     }
0180     if (details & KIO::StatTime) {
0181         // atime, mtime, btime
0182         mask |= STATX_ATIME | STATX_MTIME | STATX_BTIME;
0183     }
0184     if (details & KIO::StatInode) {
0185         // dev, inode
0186         mask |= STATX_INO;
0187     }
0188     return statx(AT_FDCWD, path, AT_SYMLINK_NOFOLLOW, mask, buff);
0189 }
0190 inline int STAT(const char *path, struct statx *buff, const KIO::StatDetails &details)
0191 {
0192     uint32_t mask = 0;
0193     // KIO::StatAcl needs type
0194     if (details & (KIO::StatBasic | KIO::StatAcl | KIO::StatResolveSymlink)) {
0195         // filename, access, type
0196         mask |= STATX_TYPE;
0197     }
0198     if (details & (KIO::StatBasic | KIO::StatResolveSymlink)) {
0199         // size, linkdest
0200         mask |= STATX_SIZE;
0201     }
0202     if (details & KIO::StatUser) {
0203         // uid, gid
0204         mask |= STATX_UID | STATX_GID;
0205     }
0206     if (details & KIO::StatTime) {
0207         // atime, mtime, btime
0208         mask |= STATX_ATIME | STATX_MTIME | STATX_BTIME;
0209     }
0210     // KIO::Inode is ignored as when STAT is called, the entry inode field has already been filled
0211     return statx(AT_FDCWD, path, AT_STATX_SYNC_AS_STAT, mask, buff);
0212 }
0213 inline static uint16_t stat_mode(const struct statx &buf)
0214 {
0215     return buf.stx_mode;
0216 }
0217 inline static dev_t stat_dev(const struct statx &buf)
0218 {
0219     return makedev(buf.stx_dev_major, buf.stx_dev_minor);
0220 }
0221 inline static uint64_t stat_ino(const struct statx &buf)
0222 {
0223     return buf.stx_ino;
0224 }
0225 inline static uint64_t stat_size(const struct statx &buf)
0226 {
0227     return buf.stx_size;
0228 }
0229 inline static uint32_t stat_uid(const struct statx &buf)
0230 {
0231     return buf.stx_uid;
0232 }
0233 inline static uint32_t stat_gid(const struct statx &buf)
0234 {
0235     return buf.stx_gid;
0236 }
0237 inline static int64_t stat_atime(const struct statx &buf)
0238 {
0239     return buf.stx_atime.tv_sec;
0240 }
0241 inline static int64_t stat_mtime(const struct statx &buf)
0242 {
0243     return buf.stx_mtime.tv_sec;
0244 }
0245 #else
0246 // regular stat struct
0247 inline int LSTAT(const char *path, QT_STATBUF *buff, KIO::StatDetails details)
0248 {
0249     Q_UNUSED(details)
0250     return QT_LSTAT(path, buff);
0251 }
0252 inline int STAT(const char *path, QT_STATBUF *buff, KIO::StatDetails details)
0253 {
0254     Q_UNUSED(details)
0255     return QT_STAT(path, buff);
0256 }
0257 inline static mode_t stat_mode(const QT_STATBUF &buf)
0258 {
0259     return buf.st_mode;
0260 }
0261 inline static dev_t stat_dev(const QT_STATBUF &buf)
0262 {
0263     return buf.st_dev;
0264 }
0265 inline static ino_t stat_ino(const QT_STATBUF &buf)
0266 {
0267     return buf.st_ino;
0268 }
0269 inline static off_t stat_size(const QT_STATBUF &buf)
0270 {
0271     return buf.st_size;
0272 }
0273 inline static uid_t stat_uid(const QT_STATBUF &buf)
0274 {
0275     return buf.st_uid;
0276 }
0277 inline static gid_t stat_gid(const QT_STATBUF &buf)
0278 {
0279     return buf.st_gid;
0280 }
0281 inline static time_t stat_atime(const QT_STATBUF &buf)
0282 {
0283     return buf.st_atime;
0284 }
0285 inline static time_t stat_mtime(const QT_STATBUF &buf)
0286 {
0287     return buf.st_mtime;
0288 }
0289 #endif
0290 
0291 static bool isOnCifsMount(const QString &filePath)
0292 {
0293     const auto mount = KMountPoint::currentMountPoints().findByPath(filePath);
0294     if (!mount) {
0295         return false;
0296     }
0297     return mount->mountType() == QStringLiteral("cifs") || mount->mountType() == QStringLiteral("smb3");
0298 }
0299 
0300 static bool createUDSEntry(const QString &filename, const QByteArray &path, UDSEntry &entry, KIO::StatDetails details, const QString &fullPath)
0301 {
0302     assert(entry.count() == 0); // by contract :-)
0303     int entries = 0;
0304     if (details & KIO::StatBasic) {
0305         // filename, access, type, size, linkdest
0306         entries += 5;
0307     }
0308     if (details & KIO::StatUser) {
0309         // uid, gid
0310         entries += 2;
0311     }
0312     if (details & KIO::StatTime) {
0313         // atime, mtime, btime
0314         entries += 3;
0315     }
0316     if (details & KIO::StatAcl) {
0317         // acl data
0318         entries += 3;
0319     }
0320     if (details & KIO::StatInode) {
0321         // dev, inode
0322         entries += 2;
0323     }
0324     if (details & KIO::StatMimeType) {
0325         // mimetype
0326         entries += 1;
0327     }
0328     entry.reserve(entries);
0329 
0330     if (details & KIO::StatBasic) {
0331         entry.fastInsert(KIO::UDSEntry::UDS_NAME, filename);
0332     }
0333 
0334     bool isBrokenSymLink = false;
0335 #if HAVE_POSIX_ACL
0336     QByteArray targetPath = path;
0337 #endif
0338 
0339 #if HAVE_STATX
0340     // statx syscall is available
0341     struct statx buff;
0342 #else
0343     QT_STATBUF buff;
0344 #endif
0345 
0346     if (LSTAT(path.data(), &buff, details) == 0) {
0347         if (Utils::isLinkMask(stat_mode(buff))) {
0348             QByteArray linkTargetBuffer;
0349             if (details & (KIO::StatBasic | KIO::StatResolveSymlink)) {
0350 // Use readlink on Unix because symLinkTarget turns relative targets into absolute (#352927)
0351 #if HAVE_STATX
0352                 size_t lowerBound = 256;
0353                 size_t higherBound = 1024;
0354                 uint64_t s = stat_size(buff);
0355                 if (s > SIZE_MAX) {
0356                     qCWarning(KIO_FILE) << "file size bigger than SIZE_MAX, too big for readlink use!" << path;
0357                     return false;
0358                 }
0359                 size_t size = static_cast<size_t>(s);
0360                 using SizeType = size_t;
0361 #else
0362                 off_t lowerBound = 256;
0363                 off_t higherBound = 1024;
0364                 off_t size = stat_size(buff);
0365                 using SizeType = off_t;
0366 #endif
0367                 SizeType bufferSize = qBound(lowerBound, size + 1, higherBound);
0368                 linkTargetBuffer.resize(bufferSize);
0369                 while (true) {
0370                     ssize_t n = readlink(path.constData(), linkTargetBuffer.data(), bufferSize);
0371                     if (n < 0 && errno != ERANGE) {
0372                         qCWarning(KIO_FILE) << "readlink failed!" << path;
0373                         return false;
0374                     } else if (n > 0 && static_cast<SizeType>(n) != bufferSize) {
0375                         // the buffer was not filled in the last iteration
0376                         // we are finished reading, break the loop
0377                         linkTargetBuffer.truncate(n);
0378                         break;
0379                     }
0380                     bufferSize *= 2;
0381                     linkTargetBuffer.resize(bufferSize);
0382                 }
0383                 const QString linkTarget = QFile::decodeName(linkTargetBuffer);
0384                 entry.fastInsert(KIO::UDSEntry::UDS_LINK_DEST, linkTarget);
0385             }
0386 
0387             // A symlink
0388             if (details & KIO::StatResolveSymlink) {
0389                 if (STAT(path.constData(), &buff, details) == -1) {
0390                     isBrokenSymLink = true;
0391                 } else {
0392 #if HAVE_POSIX_ACL
0393                     if (details & KIO::StatAcl) {
0394                         // valid symlink, will get the ACLs of the destination
0395                         targetPath = linkTargetBuffer;
0396                     }
0397 #endif
0398                 }
0399             }
0400         }
0401     } else {
0402         // qCWarning(KIO_FILE) << "lstat didn't work on " << path.data();
0403         return false;
0404     }
0405 
0406     mode_t type = 0;
0407     if (details & (KIO::StatBasic | KIO::StatAcl)) {
0408         mode_t access;
0409         signed long long size;
0410         if (isBrokenSymLink) {
0411             // It is a link pointing to nowhere
0412             type = S_IFMT - 1;
0413             access = S_IRWXU | S_IRWXG | S_IRWXO;
0414             size = 0LL;
0415         } else {
0416             type = stat_mode(buff) & S_IFMT; // extract file type
0417             access = stat_mode(buff) & 07777; // extract permissions
0418             size = stat_size(buff);
0419         }
0420 
0421         if (details & KIO::StatBasic) {
0422             entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, type);
0423             entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, access);
0424             entry.fastInsert(KIO::UDSEntry::UDS_SIZE, size);
0425         }
0426 
0427 #if HAVE_POSIX_ACL
0428         if (details & KIO::StatAcl) {
0429             /* Append an atom indicating whether the file has extended acl information
0430              * and if withACL is specified also one with the acl itself. If it's a directory
0431              * and it has a default ACL, also append that. */
0432             appendACLAtoms(targetPath, entry, type);
0433         }
0434 #endif
0435     }
0436 
0437     if (details & KIO::StatUser) {
0438         const auto uid = stat_uid(buff);
0439         const auto gid = stat_gid(buff);
0440         entry.fastInsert(KIO::UDSEntry::UDS_LOCAL_USER_ID, uid);
0441         entry.fastInsert(KIO::UDSEntry::UDS_LOCAL_GROUP_ID, gid);
0442     }
0443 
0444     if (details & KIO::StatTime) {
0445         entry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, stat_mtime(buff));
0446         entry.fastInsert(KIO::UDSEntry::UDS_ACCESS_TIME, stat_atime(buff));
0447 
0448 #ifdef st_birthtime
0449         /* For example FreeBSD's and NetBSD's stat contains a field for
0450          * the inode birth time: st_birthtime
0451          * This however only works on UFS and ZFS, and not, on say, NFS.
0452          * Instead of setting a bogus fallback like st_mtime, only use
0453          * it if it is greater than 0. */
0454         if (buff.st_birthtime > 0) {
0455             entry.fastInsert(KIO::UDSEntry::UDS_CREATION_TIME, buff.st_birthtime);
0456         }
0457 #elif defined __st_birthtime
0458         /* As above, but OpenBSD calls it slightly differently. */
0459         if (buff.__st_birthtime > 0) {
0460             entry.fastInsert(KIO::UDSEntry::UDS_CREATION_TIME, buff.__st_birthtime);
0461         }
0462 #elif HAVE_STATX
0463         /* And linux version using statx syscall */
0464         if (buff.stx_mask & STATX_BTIME) {
0465             entry.fastInsert(KIO::UDSEntry::UDS_CREATION_TIME, buff.stx_btime.tv_sec);
0466         }
0467 #endif
0468     }
0469 
0470     if (details & KIO::StatInode) {
0471         entry.fastInsert(KIO::UDSEntry::UDS_DEVICE_ID, stat_dev(buff));
0472         entry.fastInsert(KIO::UDSEntry::UDS_INODE, stat_ino(buff));
0473     }
0474 
0475     if (details & KIO::StatMimeType) {
0476         QMimeDatabase db;
0477         entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, db.mimeTypeForFile(fullPath).name());
0478     }
0479 
0480     return true;
0481 }
0482 
0483 WorkerResult FileProtocol::tryOpen(QFile &f, const QByteArray &path, int flags, int mode, int errcode)
0484 {
0485     const QString sockPath = socketPath();
0486     FdReceiver fdRecv(QFile::encodeName(sockPath).toStdString());
0487     if (!fdRecv.isListening()) {
0488         return WorkerResult::fail(errcode);
0489     }
0490 
0491     QIODevice::OpenMode openMode;
0492     if (flags & O_RDONLY) {
0493         openMode |= QIODevice::ReadOnly;
0494     }
0495     if (flags & O_WRONLY || flags & O_CREAT) {
0496         openMode |= QIODevice::WriteOnly;
0497     }
0498     if (flags & O_RDWR) {
0499         openMode |= QIODevice::ReadWrite;
0500     }
0501     if (flags & O_TRUNC) {
0502         openMode |= QIODevice::Truncate;
0503     }
0504     if (flags & O_APPEND) {
0505         openMode |= QIODevice::Append;
0506     }
0507 
0508     auto result = execWithElevatedPrivilege(OPEN, {path, flags, mode, sockPath}, errcode);
0509     if (!result.success()) {
0510         return result;
0511     } else {
0512         int fd = fdRecv.fileDescriptor();
0513         if (fd < 3 || !f.open(fd, openMode, QFileDevice::AutoCloseHandle)) {
0514             return WorkerResult::fail(errcode);
0515         }
0516     }
0517     return WorkerResult::pass();
0518 }
0519 
0520 WorkerResult FileProtocol::tryChangeFileAttr(ActionType action, const QVariantList &args, int errcode)
0521 {
0522     KAuth::Action execAction(QStringLiteral("org.kde.kio.file.exec"));
0523     execAction.setHelperId(QStringLiteral("org.kde.kio.file"));
0524     if (execAction.status() == KAuth::Action::AuthorizedStatus) {
0525         return execWithElevatedPrivilege(action, args, errcode);
0526     }
0527     return WorkerResult::fail(errcode);
0528 }
0529 
0530 #if HAVE_SYS_XATTR_H || HAVE_SYS_EXTATTR_H
0531 bool FileProtocol::copyXattrs(const int src_fd, const int dest_fd)
0532 {
0533     // Get the list of keys
0534     ssize_t listlen = 0;
0535     QByteArray keylist;
0536     while (true) {
0537         keylist.resize(listlen);
0538 #if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) && !defined(Q_OS_MAC)
0539         listlen = flistxattr(src_fd, keylist.data(), listlen);
0540 #elif defined(Q_OS_MAC)
0541         listlen = flistxattr(src_fd, keylist.data(), listlen, 0);
0542 #elif HAVE_SYS_EXTATTR_H
0543         listlen = extattr_list_fd(src_fd, EXTATTR_NAMESPACE_USER, listlen == 0 ? nullptr : keylist.data(), listlen);
0544 #endif
0545         if (listlen > 0 && keylist.size() == 0) {
0546             continue;
0547         }
0548         if (listlen > 0 && keylist.size() > 0) {
0549             break;
0550         }
0551         if (listlen == -1 && errno == ERANGE) {
0552             listlen = 0;
0553             continue;
0554         }
0555         if (listlen == 0) {
0556             // qCDebug(KIO_FILE) << "the file doesn't have any xattr";
0557             return true;
0558         }
0559         Q_ASSERT_X(listlen == -1, "copyXattrs", "unexpected return value from listxattr");
0560         if (listlen == -1 && errno == ENOTSUP) {
0561             qCDebug(KIO_FILE) << "source filesystem does not support xattrs";
0562         }
0563         return false;
0564     }
0565 
0566     keylist.resize(listlen);
0567 
0568     // Linux and MacOS return a list of null terminated strings, each string = [data,'\0']
0569     // BSDs return a list of items, each item consisting of the size byte
0570     // prepended to the key = [size, data]
0571     auto keyPtr = keylist.cbegin();
0572     size_t keyLen;
0573     QByteArray value;
0574 
0575     // For each key
0576     while (keyPtr != keylist.cend()) {
0577         // Get size of the key
0578 #if HAVE_SYS_XATTR_H
0579         keyLen = strlen(keyPtr);
0580         auto next_key = [&]() {
0581             keyPtr += keyLen + 1;
0582         };
0583 #elif HAVE_SYS_EXTATTR_H
0584         keyLen = static_cast<unsigned char>(*keyPtr);
0585         keyPtr++;
0586         auto next_key = [&]() {
0587             keyPtr += keyLen;
0588         };
0589 #endif
0590         QByteArray key(keyPtr, keyLen);
0591 
0592         // Get the value for key
0593         ssize_t valuelen = 0;
0594         do {
0595             value.resize(valuelen);
0596 #if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) && !defined(Q_OS_MAC)
0597             valuelen = fgetxattr(src_fd, key.constData(), value.data(), valuelen);
0598 #elif defined(Q_OS_MAC)
0599             valuelen = fgetxattr(src_fd, key.constData(), value.data(), valuelen, 0, 0);
0600 #elif HAVE_SYS_EXTATTR_H
0601             valuelen = extattr_get_fd(src_fd, EXTATTR_NAMESPACE_USER, key.constData(), valuelen == 0 ? nullptr : value.data(), valuelen);
0602 #endif
0603             if (valuelen > 0 && value.size() == 0) {
0604                 continue;
0605             }
0606             if (valuelen > 0 && value.size() > 0) {
0607                 break;
0608             }
0609             if (valuelen == -1 && errno == ERANGE) {
0610                 valuelen = 0;
0611                 continue;
0612             }
0613             // happens when attr value is an empty string
0614             if (valuelen == 0) {
0615                 break;
0616             }
0617             Q_ASSERT_X(valuelen == -1, "copyXattrs", "unexpected return value from getxattr");
0618             // Some other error, skip to the next attribute, most notably
0619             // - ENOTSUP: invalid (inaccassible) attribute namespace, e.g. with SELINUX
0620             break;
0621         } while (true);
0622 
0623         if (valuelen < 0) {
0624             // Skip to next attribute.
0625             next_key();
0626             continue;
0627         }
0628 
0629         // Write key:value pair on destination
0630 #if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) && !defined(Q_OS_MAC)
0631         ssize_t destlen = fsetxattr(dest_fd, key.constData(), value.constData(), valuelen, 0);
0632 #elif defined(Q_OS_MAC)
0633         ssize_t destlen = fsetxattr(dest_fd, key.constData(), value.constData(), valuelen, 0, 0);
0634 #elif HAVE_SYS_EXTATTR_H
0635         ssize_t destlen = extattr_set_fd(dest_fd, EXTATTR_NAMESPACE_USER, key.constData(), value.constData(), valuelen);
0636 #endif
0637         if (destlen == -1 && errno == ENOTSUP) {
0638             qCDebug(KIO_FILE) << "Destination filesystem does not support xattrs";
0639             return false;
0640         }
0641         if (destlen == -1 && (errno == ENOSPC || errno == EDQUOT)) {
0642             return false;
0643         }
0644 
0645         next_key();
0646     }
0647     return true;
0648 }
0649 #endif // HAVE_SYS_XATTR_H || HAVE_SYS_EXTATTR_H
0650 
0651 WorkerResult FileProtocol::copy(const QUrl &srcUrl, const QUrl &destUrl, int _mode, JobFlags _flags)
0652 {
0653     if (privilegeOperationUnitTestMode()) {
0654         return WorkerResult::pass();
0655     }
0656 
0657     qCDebug(KIO_FILE) << "copy()" << srcUrl << "to" << destUrl << "mode=" << _mode;
0658 
0659     const QString src = srcUrl.toLocalFile();
0660     QString dest = destUrl.toLocalFile();
0661     QByteArray _src(QFile::encodeName(src));
0662     QByteArray _dest(QFile::encodeName(dest));
0663     QByteArray _destBackup;
0664 
0665     QT_STATBUF buffSrc;
0666     if (QT_STAT(_src.data(), &buffSrc) == -1) {
0667         if (errno == EACCES) {
0668             return WorkerResult::fail(KIO::ERR_ACCESS_DENIED, src);
0669         } else {
0670             return WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, src);
0671         }
0672     }
0673 
0674     if (S_ISDIR(buffSrc.st_mode)) {
0675         return WorkerResult::fail(KIO::ERR_IS_DIRECTORY, src);
0676     }
0677     if (S_ISFIFO(buffSrc.st_mode) || S_ISSOCK(buffSrc.st_mode)) {
0678         return WorkerResult::fail(KIO::ERR_CANNOT_OPEN_FOR_READING, src);
0679     }
0680 
0681     QT_STATBUF buffDest;
0682     bool dest_exists = (QT_LSTAT(_dest.data(), &buffDest) != -1);
0683     if (dest_exists) {
0684         if (same_inode(buffDest, buffSrc)) {
0685             return WorkerResult::fail(KIO::ERR_IDENTICAL_FILES, dest);
0686         }
0687 
0688         if (S_ISDIR(buffDest.st_mode)) {
0689             return WorkerResult::fail(KIO::ERR_DIR_ALREADY_EXIST, dest);
0690         }
0691 
0692         if (_flags & KIO::Overwrite) {
0693             // If the destination is a symlink and overwrite is TRUE,
0694             // remove the symlink first to prevent the scenario where
0695             // the symlink actually points to current source!
0696             if (S_ISLNK(buffDest.st_mode)) {
0697                 // qDebug() << "copy(): LINK DESTINATION";
0698                 if (!QFile::remove(dest)) {
0699                     auto result = execWithElevatedPrivilege(DEL, {_dest}, errno);
0700                     if (!result.success()) {
0701                         if (!resultWasCancelled(result)) {
0702                             return WorkerResult::fail(KIO::ERR_CANNOT_DELETE_ORIGINAL, dest);
0703                         }
0704                         return result;
0705                     }
0706                 }
0707             } else if (S_ISREG(buffDest.st_mode) && !isOnCifsMount(dest)) {
0708                 _destBackup = _dest;
0709                 dest.append(QStringLiteral(".part"));
0710                 _dest = QFile::encodeName(dest);
0711             }
0712         } else {
0713             return WorkerResult::fail(KIO::ERR_FILE_ALREADY_EXIST, dest);
0714         }
0715     }
0716 
0717     QFile srcFile(src);
0718     if (!srcFile.open(QIODevice::ReadOnly)) {
0719         auto result = tryOpen(srcFile, _src, O_RDONLY, S_IRUSR, errno);
0720         if (!result.success()) {
0721             if (!resultWasCancelled(result)) {
0722                 return WorkerResult::fail(KIO::ERR_CANNOT_OPEN_FOR_READING, src);
0723             }
0724             return result;
0725         }
0726     }
0727 
0728 #if HAVE_FADVISE
0729     posix_fadvise(srcFile.handle(), 0, 0, POSIX_FADV_SEQUENTIAL);
0730 #endif
0731 
0732     QFile destFile(dest);
0733     if (!destFile.open(QIODevice::Truncate | QIODevice::WriteOnly)) {
0734         auto result = tryOpen(destFile, _dest, O_WRONLY | O_TRUNC | O_CREAT, S_IRUSR | S_IWUSR, errno);
0735         if (!result.success()) {
0736             int err = result.error();
0737             if (!resultWasCancelled(result)) {
0738                 // qDebug() << "###### COULD NOT WRITE " << dest;
0739                 if (err == EACCES) {
0740                     return WorkerResult::fail(KIO::ERR_WRITE_ACCESS_DENIED, dest);
0741                 } else {
0742                     return WorkerResult::fail(KIO::ERR_CANNOT_OPEN_FOR_WRITING, dest);
0743                 }
0744             }
0745             return result;
0746         }
0747         return WorkerResult::pass();
0748     }
0749 
0750     // _mode == -1 means don't touch dest permissions, leave it with the system default ones
0751     if (_mode != -1) {
0752         if (::chmod(_dest.constData(), _mode) == -1) {
0753             const int errCode = errno;
0754             KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(dest);
0755             // Eat the error if the filesystem apparently doesn't support chmod.
0756             // This test isn't fullproof though, vboxsf (VirtualBox shared folder) supports
0757             // chmod if the host is Linux, and doesn't if the host is Windows. Hard to detect.
0758             if (mp && mp->testFileSystemFlag(KMountPoint::SupportsChmod)) {
0759                 if (!tryChangeFileAttr(CHMOD, {_dest, _mode}, errCode).success()) {
0760                     qCWarning(KIO_FILE) << "Could not change permissions for" << dest;
0761                 }
0762             }
0763         }
0764     }
0765 
0766 #if HAVE_FADVISE
0767     posix_fadvise(destFile.handle(), 0, 0, POSIX_FADV_SEQUENTIAL);
0768 #endif
0769 
0770     const auto srcSize = buffSrc.st_size;
0771     totalSize(srcSize);
0772 
0773     off_t sizeProcessed = 0;
0774 
0775 #ifdef FICLONE
0776     // Share data blocks ("reflink") on supporting filesystems, like brfs and XFS
0777     int ret = ::ioctl(destFile.handle(), FICLONE, srcFile.handle());
0778     if (ret != -1) {
0779         sizeProcessed = srcSize;
0780         processedSize(srcSize);
0781     }
0782     // if fs does not support reflinking, files are on different devices...
0783 #endif
0784 
0785     bool existingDestDeleteAttempted = false;
0786 
0787     processedSize(sizeProcessed);
0788 
0789 #if HAVE_COPY_FILE_RANGE
0790     while (!wasKilled() && sizeProcessed < srcSize) {
0791         if (testMode && destFile.fileName().contains(QLatin1String("slow"))) {
0792             QThread::msleep(50);
0793         }
0794 
0795         const ssize_t copiedBytes = ::copy_file_range(srcFile.handle(), nullptr, destFile.handle(), nullptr, s_maxIPCSize, 0);
0796 
0797         if (copiedBytes == -1) {
0798             // ENOENT is returned on cifs in some cases, probably a kernel bug
0799             // (s.a. https://git.savannah.gnu.org/cgit/coreutils.git/commit/?id=7fc84d1c0f6b35231b0b4577b70aaa26bf548a7c)
0800             if (errno == EINVAL || errno == EXDEV || errno == ENOENT) {
0801                 break; // will continue with next copy mechanism
0802             }
0803 
0804             if (errno == EINTR) { // Interrupted
0805                 continue;
0806             }
0807 
0808             if (errno == ENOSPC) { // disk full
0809                 // attempt to free disk space occupied by file being overwritten
0810                 if (!_destBackup.isEmpty() && !existingDestDeleteAttempted) {
0811                     ::unlink(_destBackup.constData());
0812                     existingDestDeleteAttempted = true;
0813                     continue;
0814                 }
0815 
0816                 if (!QFile::remove(dest)) { // don't keep partly copied file
0817                     auto result = execWithElevatedPrivilege(DEL, {_dest}, errno);
0818                     if (!result.success()) {
0819                         return result;
0820                     }
0821                 }
0822 
0823                 return WorkerResult::fail(KIO::ERR_DISK_FULL, dest);
0824             }
0825 
0826             if (!QFile::remove(dest)) { // don't keep partly copied file
0827                 auto result = execWithElevatedPrivilege(DEL, {_dest}, errno);
0828                 if (!result.success()) {
0829                     return result;
0830                 }
0831             }
0832 
0833             return WorkerResult::fail(KIO::ERR_WORKER_DEFINED, i18n("Cannot copy file from %1 to %2. (Errno: %3)", src, dest, errno));
0834         }
0835 
0836         sizeProcessed += copiedBytes;
0837         processedSize(sizeProcessed);
0838     }
0839 #endif
0840 
0841     /* standard read/write fallback */
0842     if (sizeProcessed < srcSize) {
0843         std::array<char, s_maxIPCSize> buffer;
0844         while (!wasKilled() && sizeProcessed < srcSize) {
0845             if (testMode && destFile.fileName().contains(QLatin1String("slow"))) {
0846                 QThread::msleep(50);
0847             }
0848 
0849             const ssize_t readBytes = ::read(srcFile.handle(), &buffer, s_maxIPCSize);
0850 
0851             if (readBytes == -1) {
0852                 if (errno == EINTR) { // Interrupted
0853                     continue;
0854                 } else {
0855                     qCWarning(KIO_FILE) << "Couldn't read[2]. Error:" << srcFile.errorString();
0856                 }
0857 
0858                 if (!QFile::remove(dest)) { // don't keep partly copied file
0859                     auto result = execWithElevatedPrivilege(DEL, {_dest}, errno);
0860                     if (!result.success()) {
0861                         return result;
0862                     }
0863                 }
0864                 return WorkerResult::fail(KIO::ERR_CANNOT_READ, src);
0865             }
0866 
0867             if (destFile.write(buffer.data(), readBytes) != readBytes) {
0868                 int error = KIO::ERR_CANNOT_WRITE;
0869                 if (destFile.error() == QFileDevice::ResourceError) { // disk full
0870                     // attempt to free disk space occupied by file being overwritten
0871                     if (!_destBackup.isEmpty() && !existingDestDeleteAttempted) {
0872                         ::unlink(_destBackup.constData());
0873                         existingDestDeleteAttempted = true;
0874                         if (destFile.write(buffer.data(), readBytes) == readBytes) { // retry
0875                             continue;
0876                         }
0877                     }
0878                     error = KIO::ERR_DISK_FULL;
0879                 } else {
0880                     qCWarning(KIO_FILE) << "Couldn't write[2]. Error:" << destFile.errorString();
0881                 }
0882 
0883                 if (!QFile::remove(dest)) { // don't keep partly copied file
0884                     auto result = execWithElevatedPrivilege(DEL, {_dest}, errno);
0885                     if (!result.success()) {
0886                         return result;
0887                     }
0888                 }
0889                 return WorkerResult::fail(error, dest);
0890             }
0891             sizeProcessed += readBytes;
0892             processedSize(sizeProcessed);
0893         }
0894     }
0895 
0896     // Copy Extended attributes
0897 #if HAVE_SYS_XATTR_H || HAVE_SYS_EXTATTR_H
0898     if (!copyXattrs(srcFile.handle(), destFile.handle())) {
0899         qCDebug(KIO_FILE) << "can't copy Extended attributes";
0900     }
0901 #endif
0902 
0903     srcFile.close();
0904 
0905     destFile.flush(); // so the write() happens before futimes()
0906 
0907     // copy access and modification time
0908     if (!wasKilled()) {
0909 #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
0910         // with nano secs precision
0911         struct timespec ut[2];
0912         ut[0] = buffSrc.st_atim;
0913         ut[1] = buffSrc.st_mtim;
0914         // need to do this with the dest file still opened, or this fails
0915         if (::futimens(destFile.handle(), ut) != 0) {
0916 #else
0917         struct timeval ut[2];
0918         ut[0].tv_sec = buffSrc.st_atime;
0919         ut[0].tv_usec = 0;
0920         ut[1].tv_sec = buffSrc.st_mtime;
0921         ut[1].tv_usec = 0;
0922         if (::futimes(destFile.handle(), ut) != 0) {
0923 #endif
0924             if (!tryChangeFileAttr(UTIME, {_dest, qint64(buffSrc.st_atime), qint64(buffSrc.st_mtime)}, errno).success()) {
0925                 qCWarning(KIO_FILE) << "Couldn't preserve access and modification time for" << dest;
0926             }
0927         }
0928     }
0929 
0930     destFile.close();
0931 
0932     if (wasKilled()) {
0933         qCDebug(KIO_FILE) << "Clean dest file after KIO worker was killed:" << dest;
0934         if (!QFile::remove(dest)) { // don't keep partly copied file
0935             execWithElevatedPrivilege(DEL, {_dest}, errno);
0936         }
0937         return WorkerResult::fail(KIO::ERR_USER_CANCELED, dest);
0938     }
0939 
0940     if (destFile.error() != QFile::NoError) {
0941         qCWarning(KIO_FILE) << "Error when closing file descriptor[2]:" << destFile.errorString();
0942 
0943         if (!QFile::remove(dest)) { // don't keep partly copied file
0944             execWithElevatedPrivilege(DEL, {_dest}, errno);
0945         }
0946 
0947         return WorkerResult::fail(KIO::ERR_CANNOT_WRITE, dest);
0948     }
0949 
0950 #if HAVE_POSIX_ACL
0951     // If no special mode is given, preserve the ACL attributes from the source file
0952     if (_mode == -1) {
0953         acl_t acl = acl_get_fd(srcFile.handle());
0954         if (acl && acl_set_file(_dest.data(), ACL_TYPE_ACCESS, acl) != 0) {
0955             qCWarning(KIO_FILE) << "Could not set ACL permissions for" << dest;
0956         }
0957     }
0958 #endif
0959 
0960     // preserve ownership
0961     if (_mode != -1) {
0962         if (::chown(_dest.data(), -1 /*keep user*/, buffSrc.st_gid) == 0) {
0963             // as we are the owner of the new file, we can always change the group, but
0964             // we might not be allowed to change the owner
0965             (void)::chown(_dest.data(), buffSrc.st_uid, -1 /*keep group*/);
0966         } else {
0967             if (!tryChangeFileAttr(CHOWN, {_dest, buffSrc.st_uid, buffSrc.st_gid}, errno).success()) {
0968                 qCWarning(KIO_FILE) << "Couldn't preserve group for" << dest;
0969             }
0970         }
0971     }
0972 
0973     if (!_destBackup.isEmpty()) { // Overwrite final dest file with new file
0974         if (::unlink(_destBackup.constData()) == -1) {
0975             qCWarning(KIO_FILE) << "Couldn't remove original dest" << _destBackup << "(" << strerror(errno) << ")";
0976         }
0977 
0978         if (::rename(_dest.constData(), _destBackup.constData()) == -1) {
0979             qCWarning(KIO_FILE) << "Couldn't rename" << _dest << "to" << _destBackup << "(" << strerror(errno) << ")";
0980         }
0981     }
0982 
0983     processedSize(srcSize);
0984     return WorkerResult::pass();
0985 }
0986 
0987 static bool isLocalFileSameHost(const QUrl &url)
0988 {
0989     if (!url.isLocalFile()) {
0990         return false;
0991     }
0992 
0993     if (url.host().isEmpty() || (url.host() == QLatin1String("localhost"))) {
0994         return true;
0995     }
0996 
0997     char hostname[256];
0998     hostname[0] = '\0';
0999     if (!gethostname(hostname, 255)) {
1000         hostname[sizeof(hostname) - 1] = '\0';
1001     }
1002 
1003     return (QString::compare(url.host(), QLatin1String(hostname), Qt::CaseInsensitive) == 0);
1004 }
1005 
1006 #if HAVE_SYS_XATTR_H
1007 static bool isNtfsHidden(const QString &filename)
1008 {
1009     constexpr auto attrName = "system.ntfs_attrib_be";
1010     const auto filenameEncoded = QFile::encodeName(filename);
1011 
1012     uint32_t intAttr = 0;
1013     constexpr size_t xattr_size = sizeof(intAttr);
1014     char strAttr[xattr_size];
1015 #ifdef Q_OS_MACOS
1016     auto length = getxattr(filenameEncoded.data(), attrName, strAttr, xattr_size, 0, XATTR_NOFOLLOW);
1017 #else
1018     auto length = getxattr(filenameEncoded.data(), attrName, strAttr, xattr_size);
1019 #endif
1020     if (length <= 0) {
1021         return false;
1022     }
1023 
1024     char *c = strAttr;
1025     for (decltype(length) n = 0; n < length; ++n, ++c) {
1026         intAttr <<= 8;
1027         intAttr |= static_cast<uchar>(*c);
1028     }
1029 
1030     constexpr auto FILE_ATTRIBUTE_HIDDEN = 0x2u;
1031     return static_cast<bool>(intAttr & FILE_ATTRIBUTE_HIDDEN);
1032 }
1033 #endif
1034 
1035 WorkerResult FileProtocol::listDir(const QUrl &url)
1036 {
1037     if (!isLocalFileSameHost(url)) {
1038         QUrl redir(url);
1039         redir.setScheme(configValue(QStringLiteral("DefaultRemoteProtocol"), QStringLiteral("smb")));
1040         redirection(redir);
1041         // qDebug() << "redirecting to " << redir;
1042         return WorkerResult::pass();
1043     }
1044     const QString path(url.toLocalFile());
1045     const QByteArray _path(QFile::encodeName(path));
1046     DIR *dp = opendir(_path.data());
1047     if (dp == nullptr) {
1048         switch (errno) {
1049         case ENOENT:
1050             return WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, path);
1051         case ENOTDIR:
1052             return WorkerResult::fail(KIO::ERR_IS_FILE, path);
1053 #ifdef ENOMEDIUM
1054         case ENOMEDIUM:
1055             return WorkerResult::fail(ERR_WORKER_DEFINED, i18n("No media in device for %1", path));
1056 #endif
1057         default:
1058             return WorkerResult::fail(KIO::ERR_CANNOT_ENTER_DIRECTORY, path);
1059             break;
1060         }
1061     }
1062 
1063     const QByteArray encodedBasePath = _path + '/';
1064 
1065     const KIO::StatDetails details = getStatDetails();
1066     // qDebug() << "========= LIST " << url << "details=" << details << " =========";
1067     UDSEntry entry;
1068 
1069 #ifndef HAVE_DIRENT_D_TYPE
1070     QT_STATBUF st;
1071 #endif
1072     QT_DIRENT *ep;
1073     while ((ep = QT_READDIR(dp)) != nullptr) {
1074         entry.clear();
1075 
1076         const QString filename = QFile::decodeName(ep->d_name);
1077 
1078         /*
1079          * details == 0 (if statement) is the fast code path.
1080          * We only get the file name and type. After that we emit
1081          * the result.
1082          *
1083          * The else statement is the slow path that requests all
1084          * file information in file.cpp. It executes a stat call
1085          * for every entry thus becoming slower.
1086          *
1087          */
1088         if (details == KIO::StatBasic) {
1089             entry.fastInsert(KIO::UDSEntry::UDS_NAME, filename);
1090 #ifdef HAVE_DIRENT_D_TYPE
1091             entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, (ep->d_type == DT_DIR) ? S_IFDIR : S_IFREG);
1092             const bool isSymLink = (ep->d_type == DT_LNK);
1093 #else
1094             // oops, no fast way, we need to stat (e.g. on Solaris)
1095             if (QT_LSTAT(ep->d_name, &st) == -1) {
1096                 continue; // how can stat fail?
1097             }
1098             entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_ISDIR(st.st_mode) ? S_IFDIR : S_IFREG);
1099             const bool isSymLink = S_ISLNK(st.st_mode);
1100 #endif
1101             if (isSymLink) {
1102                 // for symlinks obey the UDSEntry contract and provide UDS_LINK_DEST
1103                 // even if we don't know the link dest (and DeleteJob doesn't care...)
1104                 entry.fastInsert(KIO::UDSEntry::UDS_LINK_DEST, QStringLiteral("Dummy Link Target"));
1105             }
1106             listEntry(entry);
1107 
1108         } else {
1109             QString fullPath = Utils::slashAppended(path);
1110             fullPath += filename;
1111 
1112             if (createUDSEntry(filename, encodedBasePath + QByteArray(ep->d_name), entry, details, fullPath)) {
1113 #if HAVE_SYS_XATTR_H
1114                 if (isNtfsHidden(filename)) {
1115                     bool ntfsHidden = true;
1116 
1117                     // Bug 392913: NTFS root volume is always "hidden", ignore this
1118                     if (ep->d_type == DT_DIR || ep->d_type == DT_UNKNOWN || ep->d_type == DT_LNK) {
1119                         const QString fullFilePath = QDir(filename).canonicalPath();
1120                         auto mountPoint = KMountPoint::currentMountPoints().findByPath(fullFilePath);
1121                         if (mountPoint && mountPoint->mountPoint() == fullFilePath) {
1122                             ntfsHidden = false;
1123                         }
1124                     }
1125 
1126                     if (ntfsHidden) {
1127                         entry.fastInsert(KIO::UDSEntry::UDS_HIDDEN, 1);
1128                     }
1129                 }
1130 #endif
1131                 listEntry(entry);
1132             }
1133         }
1134     }
1135 
1136     closedir(dp);
1137 
1138     return WorkerResult::pass();
1139 }
1140 
1141 WorkerResult FileProtocol::rename(const QUrl &srcUrl, const QUrl &destUrl, KIO::JobFlags _flags)
1142 {
1143     char off_t_should_be_64_bits[sizeof(off_t) >= 8 ? 1 : -1];
1144     (void)off_t_should_be_64_bits;
1145     const QString src = srcUrl.toLocalFile();
1146     const QString dest = destUrl.toLocalFile();
1147     const QByteArray _src(QFile::encodeName(src));
1148     const QByteArray _dest(QFile::encodeName(dest));
1149     QT_STATBUF buff_src;
1150     if (QT_LSTAT(_src.data(), &buff_src) == -1) {
1151         if (errno == EACCES) {
1152             return WorkerResult::fail(KIO::ERR_ACCESS_DENIED, src);
1153         } else {
1154             return WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, src);
1155         }
1156     }
1157 
1158     QT_STATBUF buff_dest;
1159     // stat symlinks here (lstat, not stat), to avoid ERR_IDENTICAL_FILES when replacing symlink
1160     // with its target (#169547)
1161     bool dest_exists = (QT_LSTAT(_dest.data(), &buff_dest) != -1);
1162     if (dest_exists) {
1163         // Try QFile::rename(), this can help when renaming 'a' to 'A' on a case-insensitive
1164         // filesystem, e.g. FAT32/VFAT.
1165         if (src != dest && QString::compare(src, dest, Qt::CaseInsensitive) == 0) {
1166             qCDebug(KIO_FILE) << "Dest already exists; detected special case of lower/uppercase renaming"
1167                               << "in same dir on a case-insensitive filesystem, try with QFile::rename()"
1168                               << "(which uses 2 rename calls)";
1169             if (QFile::rename(src, dest)) {
1170                 return WorkerResult::pass();
1171             }
1172         }
1173 
1174         if (same_inode(buff_dest, buff_src)) {
1175             return WorkerResult::fail(KIO::ERR_IDENTICAL_FILES, dest);
1176         }
1177 
1178         if (S_ISDIR(buff_dest.st_mode)) {
1179             return WorkerResult::fail(KIO::ERR_DIR_ALREADY_EXIST, dest);
1180         }
1181 
1182         if (!(_flags & KIO::Overwrite)) {
1183             return WorkerResult::fail(KIO::ERR_FILE_ALREADY_EXIST, dest);
1184         }
1185     }
1186 
1187     if (::rename(_src.data(), _dest.data()) == -1) {
1188         auto result = execWithElevatedPrivilege(RENAME, {_src, _dest}, errno);
1189         if (!result.success()) {
1190             if (!resultWasCancelled(result)) {
1191                 int err = result.error();
1192                 if ((err == EACCES) || (err == EPERM)) {
1193                     return WorkerResult::fail(KIO::ERR_WRITE_ACCESS_DENIED, dest);
1194                 } else if (err == EXDEV) {
1195                     return WorkerResult::fail(KIO::ERR_UNSUPPORTED_ACTION, QStringLiteral("rename"));
1196                 } else if (err == EROFS) { // The file is on a read-only filesystem
1197                     return WorkerResult::fail(KIO::ERR_CANNOT_DELETE, src);
1198                 } else {
1199                     return WorkerResult::fail(KIO::ERR_CANNOT_RENAME, src);
1200                 }
1201             }
1202         }
1203         return result;
1204     }
1205 
1206     return WorkerResult::pass();
1207 }
1208 
1209 WorkerResult FileProtocol::symlink(const QString &target, const QUrl &destUrl, KIO::JobFlags flags)
1210 {
1211     // Assume dest is local too (wouldn't be here otherwise)
1212     const QString dest = destUrl.toLocalFile();
1213     const QByteArray dest_c = QFile::encodeName(dest);
1214 
1215     if (::symlink(QFile::encodeName(target).constData(), dest_c.constData()) == 0) {
1216         return WorkerResult::pass();
1217     }
1218 
1219     // Does the destination already exist ?
1220     if (errno == EEXIST) {
1221         if (flags & KIO::Overwrite) {
1222             // Try to delete the destination
1223             if (unlink(dest_c.constData()) != 0) {
1224                 auto result = execWithElevatedPrivilege(DEL, {dest}, errno);
1225                 if (!result.success()) {
1226                     if (!resultWasCancelled(result)) {
1227                         return WorkerResult::fail(KIO::ERR_CANNOT_DELETE, dest);
1228                     }
1229 
1230                     return result;
1231                 }
1232             }
1233 
1234             // Try again - this won't loop forever since unlink succeeded
1235             return symlink(target, destUrl, flags);
1236         } else {
1237             if (QT_STATBUF buff_dest; QT_LSTAT(dest_c.constData(), &buff_dest) == 0) {
1238                 return WorkerResult::fail(S_ISDIR(buff_dest.st_mode) ? KIO::ERR_DIR_ALREADY_EXIST : KIO::ERR_FILE_ALREADY_EXIST, dest);
1239             } else { // Can't happen, we already know "dest" exists
1240                 return WorkerResult::fail(KIO::ERR_CANNOT_SYMLINK, dest);
1241             }
1242 
1243             return WorkerResult::pass();
1244         }
1245     }
1246 
1247     // Permission error, could be that the filesystem doesn't support symlinks
1248     if (errno == EPERM) {
1249         // "dest" doesn't exist, get the filesystem type of the parent dir
1250         const QString parentDir = destUrl.adjusted(QUrl::StripTrailingSlash | QUrl::RemoveFilename).toLocalFile();
1251         const KFileSystemType::Type fsType = KFileSystemType::fileSystemType(parentDir);
1252 
1253         if (fsType == KFileSystemType::Fat || fsType == KFileSystemType::Exfat) {
1254             const QString msg = i18nc(
1255                 "The first arg is the path to the symlink that couldn't be created, the second"
1256                 "arg is the filesystem type (e.g. vfat, exfat)",
1257                 "Could not create symlink \"%1\".\n"
1258                 "The destination filesystem (%2) doesn't support symlinks.",
1259                 dest,
1260                 KFileSystemType::fileSystemName(fsType));
1261 
1262             return WorkerResult::fail(KIO::ERR_WORKER_DEFINED, msg);
1263         }
1264     }
1265 
1266     auto result = execWithElevatedPrivilege(SYMLINK, {dest, target}, errno);
1267     if (!result.success()) {
1268         if (!resultWasCancelled(result)) {
1269             // Some error occurred while we tried to symlink
1270             return WorkerResult::fail(KIO::ERR_CANNOT_SYMLINK, dest);
1271         }
1272         return result;
1273     }
1274     return WorkerResult::pass();
1275 }
1276 
1277 WorkerResult FileProtocol::del(const QUrl &url, bool isfile)
1278 {
1279     const QString path = url.toLocalFile();
1280     const QByteArray _path(QFile::encodeName(path));
1281     /*****
1282      * Delete files
1283      *****/
1284 
1285     if (isfile) {
1286         // qDebug() << "Deleting file "<< url;
1287 
1288         if (unlink(_path.data()) == -1) {
1289             auto result = execWithElevatedPrivilege(DEL, {_path}, errno);
1290             if (!result.success()) {
1291                 auto err = result.error();
1292                 if (!resultWasCancelled(result)) {
1293                     if ((err == EACCES) || (err == EPERM)) {
1294                         return WorkerResult::fail(KIO::ERR_ACCESS_DENIED, path);
1295                     } else if (err == EISDIR) {
1296                         return WorkerResult::fail(KIO::ERR_IS_DIRECTORY, path);
1297                     } else {
1298                         return WorkerResult::fail(KIO::ERR_CANNOT_DELETE, path);
1299                     }
1300                 }
1301                 return result;
1302             }
1303             return WorkerResult::pass();
1304         }
1305     } else {
1306         /*****
1307          * Delete empty directory
1308          *****/
1309 
1310         // qDebug() << "Deleting directory " << url;
1311         if (metaData(QStringLiteral("recurse")) == QLatin1String("true")) {
1312             auto result = deleteRecursive(path);
1313             if (!result.success()) {
1314                 return result;
1315             }
1316         }
1317         if (QT_RMDIR(_path.data()) == -1) {
1318             auto result = execWithElevatedPrivilege(RMDIR, {_path}, errno);
1319             if (!result.success()) {
1320                 if (!resultWasCancelled(result)) {
1321                     if ((result.error() == EACCES) || (result.error() == EPERM)) {
1322                         return WorkerResult::fail(KIO::ERR_ACCESS_DENIED, path);
1323                     } else {
1324                         // qDebug() << "could not rmdir " << perror;
1325                         return WorkerResult::fail(KIO::ERR_CANNOT_RMDIR, path);
1326                     }
1327                 }
1328                 return result;
1329             }
1330         }
1331     }
1332     return WorkerResult::pass();
1333 }
1334 
1335 WorkerResult FileProtocol::chown(const QUrl &url, const QString &owner, const QString &group)
1336 {
1337     const QString path = url.toLocalFile();
1338     const QByteArray _path(QFile::encodeName(path));
1339     uid_t uid;
1340     gid_t gid;
1341 
1342     // get uid from given owner
1343     {
1344         struct passwd *p = ::getpwnam(owner.toLocal8Bit().constData());
1345 
1346         if (!p) {
1347             return WorkerResult::fail(KIO::ERR_WORKER_DEFINED, i18n("Could not get user id for given user name %1", owner));
1348         }
1349 
1350         uid = p->pw_uid;
1351     }
1352 
1353     // get gid from given group
1354     {
1355         struct group *p = ::getgrnam(group.toLocal8Bit().constData());
1356 
1357         if (!p) {
1358             return WorkerResult::fail(KIO::ERR_WORKER_DEFINED, i18n("Could not get group id for given group name %1", group));
1359         }
1360 
1361         gid = p->gr_gid;
1362     }
1363 
1364     if (::chown(_path.constData(), uid, gid) == -1) {
1365         auto result = execWithElevatedPrivilege(CHOWN, {_path, uid, gid}, errno);
1366         if (!result.success()) {
1367             if (!resultWasCancelled(result)) {
1368                 switch (result.error()) {
1369                 case EPERM:
1370                 case EACCES:
1371                     return WorkerResult::fail(KIO::ERR_ACCESS_DENIED, path);
1372                     break;
1373                 case ENOSPC:
1374                     return WorkerResult::fail(KIO::ERR_DISK_FULL, path);
1375                     break;
1376                 default:
1377                     return WorkerResult::fail(KIO::ERR_CANNOT_CHOWN, path);
1378                 }
1379             }
1380         }
1381     }
1382 
1383     return WorkerResult::pass();
1384 }
1385 
1386 WorkerResult FileProtocol::stat(const QUrl &url)
1387 {
1388     if (!isLocalFileSameHost(url)) {
1389         return redirect(url);
1390     }
1391 
1392     /* directories may not have a slash at the end if
1393      * we want to stat() them; it requires that we
1394      * change into it .. which may not be allowed
1395      * stat("/is/unaccessible")  -> rwx------
1396      * stat("/is/unaccessible/") -> EPERM            H.Z.
1397      * This is the reason for the -1
1398      */
1399     const QString path(url.adjusted(QUrl::StripTrailingSlash).toLocalFile());
1400     const QByteArray _path(QFile::encodeName(path));
1401 
1402     const KIO::StatDetails details = getStatDetails();
1403 
1404     UDSEntry entry;
1405     if (!createUDSEntry(url.fileName(), _path, entry, details, path)) {
1406         return WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, path);
1407     }
1408     statEntry(entry);
1409 
1410     return WorkerResult::pass();
1411 }
1412 
1413 WorkerResult FileProtocol::execWithElevatedPrivilege(ActionType action, const QVariantList &args, int errcode)
1414 {
1415     if (privilegeOperationUnitTestMode()) {
1416         return WorkerResult::pass();
1417     }
1418 
1419     // temporarily disable privilege execution
1420     if (true) {
1421         return WorkerResult::fail(errcode);
1422     }
1423 
1424     if (!(errcode == EACCES || errcode == EPERM)) {
1425         return WorkerResult::fail(errcode);
1426     }
1427 
1428     const QString operationDetails = actionDetails(action, args);
1429     KIO::PrivilegeOperationStatus opStatus = requestPrivilegeOperation(operationDetails);
1430     if (opStatus != KIO::OperationAllowed) {
1431         if (opStatus == KIO::OperationCanceled) {
1432             return WorkerResult::fail(KIO::ERR_USER_CANCELED, QString());
1433         }
1434         return WorkerResult::fail(errcode);
1435     }
1436 
1437     const QUrl targetUrl = QUrl::fromLocalFile(args.first().toString()); // target is always the first item.
1438     const bool useParent = action != CHOWN && action != CHMOD && action != UTIME;
1439     const QString targetPath = useParent ? targetUrl.adjusted(QUrl::RemoveFilename).toLocalFile() : targetUrl.toLocalFile();
1440     bool userIsOwner = QFileInfo(targetPath).ownerId() == getuid();
1441     if (action == RENAME) { // for rename check src and dest owner
1442         QString dest = QUrl(args[1].toString()).toLocalFile();
1443         userIsOwner = userIsOwner && QFileInfo(dest).ownerId() == getuid();
1444     }
1445     if (userIsOwner) {
1446         return WorkerResult::fail(KIO::ERR_PRIVILEGE_NOT_REQUIRED, targetPath);
1447     }
1448 
1449     QByteArray helperArgs;
1450     QDataStream out(&helperArgs, QIODevice::WriteOnly);
1451     out << action;
1452     for (const QVariant &arg : args) {
1453         out << arg;
1454     }
1455 
1456     const QString actionId = QStringLiteral("org.kde.kio.file.exec");
1457     KAuth::Action execAction(actionId);
1458     execAction.setHelperId(QStringLiteral("org.kde.kio.file"));
1459 
1460     QVariantMap argv;
1461     argv.insert(QStringLiteral("arguments"), helperArgs);
1462     execAction.setArguments(argv);
1463 
1464     auto reply = execAction.execute();
1465     if (reply->exec()) {
1466         addTemporaryAuthorization(actionId);
1467         return WorkerResult::pass();
1468     }
1469 
1470     return WorkerResult::fail(KIO::ERR_ACCESS_DENIED);
1471 }
1472 
1473 int FileProtocol::setACL(const char *path, mode_t perm, bool directoryDefault)
1474 {
1475     int ret = 0;
1476 #if HAVE_POSIX_ACL
1477 
1478     const QString ACLString = metaData(QStringLiteral("ACL_STRING"));
1479     const QString defaultACLString = metaData(QStringLiteral("DEFAULT_ACL_STRING"));
1480     // Empty strings mean leave as is
1481     if (!ACLString.isEmpty()) {
1482         acl_t acl = nullptr;
1483         if (ACLString == QLatin1String("ACL_DELETE")) {
1484             // user told us to delete the extended ACL, so let's write only
1485             // the minimal (UNIX permission bits) part
1486             acl = ACLPortability::acl_from_mode(perm);
1487         }
1488         acl = acl_from_text(ACLString.toLatin1().constData());
1489         if (acl_valid(acl) == 0) { // let's be safe
1490             ret = acl_set_file(path, ACL_TYPE_ACCESS, acl);
1491             // qDebug() << "Set ACL on:" << path << "to:" << aclToText(acl);
1492         }
1493         acl_free(acl);
1494         if (ret != 0) {
1495             return ret; // better stop trying right away
1496         }
1497     }
1498 
1499     if (directoryDefault && !defaultACLString.isEmpty()) {
1500         if (defaultACLString == QLatin1String("ACL_DELETE")) {
1501             // user told us to delete the default ACL, do so
1502             ret += acl_delete_def_file(path);
1503         } else {
1504             acl_t acl = acl_from_text(defaultACLString.toLatin1().constData());
1505             if (acl_valid(acl) == 0) { // let's be safe
1506                 ret += acl_set_file(path, ACL_TYPE_DEFAULT, acl);
1507                 // qDebug() << "Set Default ACL on:" << path << "to:" << aclToText(acl);
1508             }
1509             acl_free(acl);
1510         }
1511     }
1512 #else
1513     Q_UNUSED(path);
1514     Q_UNUSED(perm);
1515     Q_UNUSED(directoryDefault);
1516 #endif
1517     return ret;
1518 }