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