File indexing completed on 2024-05-05 03:56:09

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2004 David Faure <faure@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "trashimpl.h"
0009 #include "discspaceutil.h"
0010 #include "kiotrashdebug.h"
0011 #include "trashsizecache.h"
0012 
0013 #include "../utils_p.h"
0014 #include <kdirnotify.h>
0015 #include <kfileitem.h>
0016 #include <kio/chmodjob.h>
0017 #include <kio/copyjob.h>
0018 #include <kio/deletejob.h>
0019 #include <kmountpoint.h>
0020 
0021 #include <KConfigGroup>
0022 #include <KFileUtils>
0023 #include <KJobUiDelegate>
0024 #include <KLocalizedString>
0025 #include <KSharedConfig>
0026 #include <solid/block.h>
0027 #include <solid/device.h>
0028 #include <solid/networkshare.h>
0029 #include <solid/storageaccess.h>
0030 
0031 #include <QCoreApplication>
0032 #include <QDebug>
0033 #include <QDir>
0034 #include <QEventLoop>
0035 #include <QFile>
0036 #include <QLockFile>
0037 #include <QStandardPaths>
0038 #include <QUrl>
0039 
0040 #include <cerrno>
0041 #include <dirent.h>
0042 #include <fcntl.h>
0043 #include <stdlib.h>
0044 #include <sys/param.h>
0045 #include <sys/stat.h>
0046 #include <sys/types.h>
0047 #include <unistd.h>
0048 
0049 TrashImpl::TrashImpl()
0050     : QObject()
0051     , m_lastErrorCode(0)
0052     , m_initStatus(InitToBeDone)
0053     , m_homeDevice(0)
0054     , m_trashDirectoriesScanned(false)
0055     ,
0056     // not using kio_trashrc since KIO uses that one already for kio_trash
0057     // so better have a separate one, for faster parsing by e.g. kmimetype.cpp
0058     m_config(QStringLiteral("trashrc"), KConfig::SimpleConfig)
0059 {
0060     QT_STATBUF buff;
0061     if (QT_LSTAT(QFile::encodeName(QDir::homePath()).constData(), &buff) == 0) {
0062         m_homeDevice = buff.st_dev;
0063     } else {
0064         qCWarning(KIO_TRASH) << "Should never happen: couldn't stat $HOME" << strerror(errno);
0065     }
0066 }
0067 
0068 /**
0069  * Test if a directory exists, create otherwise
0070  * @param _name full path of the directory
0071  * @return errorcode, or 0 if the dir was created or existed already
0072  * Warning, don't use return value like a bool
0073  */
0074 int TrashImpl::testDir(const QString &_name) const
0075 {
0076     DIR *dp = ::opendir(QFile::encodeName(_name).constData());
0077     if (!dp) {
0078         QString name = Utils::trailingSlashRemoved(_name);
0079 
0080         bool ok = QDir().mkdir(name);
0081         if (!ok && QFile::exists(name)) {
0082             QString new_name = name;
0083             name.append(QStringLiteral(".orig"));
0084             if (QFile::rename(name, new_name)) {
0085                 ok = QDir().mkdir(name);
0086             } else { // foo.orig existed already. How likely is that?
0087                 ok = false;
0088             }
0089             if (!ok) {
0090                 return KIO::ERR_DIR_ALREADY_EXIST;
0091             }
0092         }
0093         if (!ok) {
0094             // KMessageBox::sorry( 0, i18n( "Could not create directory %1. Check for permissions." ).arg( name ) );
0095             qCWarning(KIO_TRASH) << "could not create" << name;
0096             return KIO::ERR_CANNOT_MKDIR;
0097         } else {
0098             // qCDebug(KIO_TRASH) << name << "created.";
0099         }
0100     } else { // exists already
0101         closedir(dp);
0102     }
0103     return 0; // success
0104 }
0105 
0106 void TrashImpl::deleteEmptyTrashInfrastructure()
0107 {
0108 #ifdef Q_OS_OSX
0109     // For each known trash directory...
0110     if (!m_trashDirectoriesScanned) {
0111         scanTrashDirectories();
0112     }
0113 
0114     for (auto it = m_trashDirectories.cbegin(); it != m_trashDirectories.cend(); ++it) {
0115         const QString trashPath = it.value();
0116         QString infoPath = trashPath + QLatin1String("/info");
0117 
0118         // qCDebug(KIO_TRASH) << "empty Trash" << trashPath << "; removing infrastructure";
0119         synchronousDel(infoPath, false, true);
0120         synchronousDel(trashPath + QLatin1String("/files"), false, true);
0121         if (trashPath.endsWith(QLatin1String("/KDE.trash"))) {
0122             synchronousDel(trashPath, false, true);
0123         }
0124     }
0125 #endif
0126 }
0127 
0128 bool TrashImpl::createTrashInfrastructure(int trashId, const QString &path)
0129 {
0130     const QString trashDir = path.isEmpty() ? trashDirectoryPath(trashId) : path;
0131     if (const int err = testDir(trashDir)) {
0132         error(err, trashDir);
0133         return false;
0134     }
0135 
0136     const QString infoDir = trashDir + QLatin1String("/info");
0137     if (const int err = testDir(infoDir)) {
0138         error(err, infoDir);
0139         return false;
0140     }
0141 
0142     const QString filesDir = trashDir + QLatin1String("/files");
0143     if (const int err = testDir(filesDir)) {
0144         error(err, filesDir);
0145         return false;
0146     }
0147 
0148     return true;
0149 }
0150 
0151 bool TrashImpl::init()
0152 {
0153     if (m_initStatus == InitOK) {
0154         return true;
0155     }
0156     if (m_initStatus == InitError) {
0157         return false;
0158     }
0159 
0160     // Check the trash directory and its info and files subdirs
0161     // see also kdesktop/init.cc for first time initialization
0162     m_initStatus = InitError;
0163 #ifndef Q_OS_OSX
0164     // $XDG_DATA_HOME/Trash, i.e. ~/.local/share/Trash by default.
0165     const QString xdgDataDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/');
0166     if (!QDir().mkpath(xdgDataDir)) {
0167         qCWarning(KIO_TRASH) << "failed to create" << xdgDataDir;
0168         return false;
0169     }
0170 
0171     const QString trashDir = xdgDataDir + QLatin1String("Trash");
0172     if (!createTrashInfrastructure(0, trashDir)) {
0173         return false;
0174     }
0175 #else
0176     // we DO NOT create ~/.Trash on OS X, that's the operating system's privilege
0177     QString trashDir = QDir::homePath() + QLatin1String("/.Trash");
0178     if (!QFileInfo(trashDir).isDir()) {
0179         error(KIO::ERR_DOES_NOT_EXIST, trashDir);
0180         return false;
0181     }
0182     trashDir += QLatin1String("/KDE.trash");
0183     // we don't have to call createTrashInfrastructure() here because it'll be called when needed.
0184 #endif
0185     m_trashDirectories.insert(0, trashDir);
0186     m_initStatus = InitOK;
0187     // qCDebug(KIO_TRASH) << "initialization OK, home trash dir:" << trashDir;
0188     return true;
0189 }
0190 
0191 void TrashImpl::migrateOldTrash()
0192 {
0193     qCDebug(KIO_TRASH);
0194 
0195     KConfigGroup g(KSharedConfig::openConfig(), QStringLiteral("Paths"));
0196     const QString oldTrashDir = g.readPathEntry("Trash", QString());
0197 
0198     if (oldTrashDir.isEmpty()) {
0199         return;
0200     }
0201 
0202     const QStringList entries = listDir(oldTrashDir);
0203     bool allOK = true;
0204     for (QString srcPath : entries) {
0205         if (srcPath == QLatin1Char('.') || srcPath == QLatin1String("..") || srcPath == QLatin1String(".directory")) {
0206             continue;
0207         }
0208         srcPath.prepend(oldTrashDir); // make absolute
0209         int trashId;
0210         QString fileId;
0211         if (!createInfo(srcPath, trashId, fileId)) {
0212             qCWarning(KIO_TRASH) << "Trash migration: failed to create info for" << srcPath;
0213             allOK = false;
0214         } else {
0215             bool ok = moveToTrash(srcPath, trashId, fileId);
0216             if (!ok) {
0217                 (void)deleteInfo(trashId, fileId);
0218                 qCWarning(KIO_TRASH) << "Trash migration: failed to create info for" << srcPath;
0219                 allOK = false;
0220             } else {
0221                 qCDebug(KIO_TRASH) << "Trash migration: moved" << srcPath;
0222             }
0223         }
0224     }
0225     if (allOK) {
0226         // We need to remove the old one, otherwise the desktop will have two trashcans...
0227         qCDebug(KIO_TRASH) << "Trash migration: all OK, removing old trash directory";
0228         synchronousDel(oldTrashDir, false, true);
0229     }
0230 }
0231 
0232 bool TrashImpl::createInfo(const QString &origPath, int &trashId, QString &fileId)
0233 {
0234     // off_t should be 64bit on Unix systems to have large file support
0235     // FIXME: on windows this gets disabled until trash gets integrated
0236     // BUG: 165449
0237 #ifndef Q_OS_WIN
0238     Q_STATIC_ASSERT(sizeof(off_t) >= 8);
0239 #endif
0240 
0241     // qCDebug(KIO_TRASH) << origPath;
0242     // Check source
0243     QT_STATBUF buff_src;
0244     if (QT_LSTAT(QFile::encodeName(origPath).constData(), &buff_src) == -1) {
0245         if (errno == EACCES) {
0246             error(KIO::ERR_ACCESS_DENIED, origPath);
0247         } else {
0248             error(KIO::ERR_DOES_NOT_EXIST, origPath);
0249         }
0250         return false;
0251     }
0252 
0253     // Choose destination trash
0254     trashId = findTrashDirectory(origPath);
0255     if (trashId < 0) {
0256         qCWarning(KIO_TRASH) << "OUCH - internal error, TrashImpl::findTrashDirectory returned" << trashId;
0257         return false; // ### error() needed?
0258     }
0259     // qCDebug(KIO_TRASH) << "trashing to" << trashId;
0260 
0261     // Grab original filename
0262     auto url = QUrl::fromLocalFile(origPath);
0263     url = url.adjusted(QUrl::StripTrailingSlash);
0264     const QString origFileName = url.fileName();
0265 
0266     // Make destination file in info/
0267 #ifdef Q_OS_OSX
0268     createTrashInfrastructure(trashId);
0269 #endif
0270     url.setPath(infoPath(trashId, origFileName)); // we first try with origFileName
0271     QUrl baseDirectory = QUrl::fromLocalFile(url.path());
0272     // Here we need to use O_EXCL to avoid race conditions with other kioworker processes
0273     int fd = 0;
0274     QString fileName;
0275     do {
0276         // qCDebug(KIO_TRASH) << "trying to create" << url.path();
0277         fd = ::open(QFile::encodeName(url.path()).constData(), O_WRONLY | O_CREAT | O_EXCL, 0600);
0278         if (fd < 0) {
0279             if (errno == EEXIST) {
0280                 fileName = url.fileName();
0281                 url = url.adjusted(QUrl::RemoveFilename);
0282                 url.setPath(url.path() + KFileUtils::suggestName(baseDirectory, fileName));
0283                 // and try again on the next iteration
0284             } else {
0285                 error(KIO::ERR_CANNOT_WRITE, url.path());
0286                 return false;
0287             }
0288         }
0289     } while (fd < 0);
0290     const QString infoPath = url.path();
0291     fileId = url.fileName();
0292     Q_ASSERT(fileId.endsWith(QLatin1String(".trashinfo")));
0293     fileId.chop(10); // remove .trashinfo from fileId
0294 
0295     FILE *file = ::fdopen(fd, "w");
0296     if (!file) { // can't see how this would happen
0297         error(KIO::ERR_CANNOT_WRITE, infoPath);
0298         return false;
0299     }
0300 
0301     // Contents of the info file. We could use KSimpleConfig, but that would
0302     // mean closing and reopening fd, i.e. opening a race condition...
0303     QByteArray info = "[Trash Info]\n";
0304     info += "Path=";
0305     // Escape filenames according to the way they are encoded on the filesystem
0306     // All this to basically get back to the raw 8-bit representation of the filename...
0307     if (trashId == 0) { // home trash: absolute path
0308         info += QUrl::toPercentEncoding(origPath, "/");
0309     } else {
0310         info += QUrl::toPercentEncoding(makeRelativePath(topDirectoryPath(trashId), origPath), "/");
0311     }
0312     info += '\n';
0313     info += "DeletionDate=" + QDateTime::currentDateTime().toString(Qt::ISODate).toLatin1() + '\n';
0314     size_t sz = info.size();
0315 
0316     size_t written = ::fwrite(info.data(), 1, sz, file);
0317     if (written != sz) {
0318         ::fclose(file);
0319         QFile::remove(infoPath);
0320         error(KIO::ERR_DISK_FULL, infoPath);
0321         return false;
0322     }
0323 
0324     ::fclose(file);
0325 
0326     // qCDebug(KIO_TRASH) << "info file created in trashId=" << trashId << ":" << fileId;
0327     return true;
0328 }
0329 
0330 QString TrashImpl::makeRelativePath(const QString &topdir, const QString &path)
0331 {
0332     QString realPath = QFileInfo(path).canonicalFilePath();
0333     if (realPath.isEmpty()) { // shouldn't happen
0334         realPath = path;
0335     }
0336     // topdir ends with '/'
0337 #ifndef Q_OS_WIN
0338     if (realPath.startsWith(topdir)) {
0339 #else
0340     if (realPath.startsWith(topdir, Qt::CaseInsensitive)) {
0341 #endif
0342         const QString rel = realPath.mid(topdir.length());
0343         Q_ASSERT(rel[0] != QLatin1Char('/'));
0344         return rel;
0345     } else { // shouldn't happen...
0346         qCWarning(KIO_TRASH) << "Couldn't make relative path for" << realPath << "(" << path << "), with topdir=" << topdir;
0347         return realPath;
0348     }
0349 }
0350 
0351 void TrashImpl::enterLoop()
0352 {
0353     QEventLoop eventLoop;
0354     connect(this, &TrashImpl::leaveModality, &eventLoop, &QEventLoop::quit);
0355     eventLoop.exec(QEventLoop::ExcludeUserInputEvents);
0356 }
0357 
0358 QString TrashImpl::infoPath(int trashId, const QString &fileId) const
0359 {
0360     const QString trashPath = trashDirectoryPath(trashId) + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo");
0361     return trashPath;
0362 }
0363 
0364 QString TrashImpl::filesPath(int trashId, const QString &fileId) const
0365 {
0366     const QString trashPath = trashDirectoryPath(trashId) + QLatin1String("/files/") + fileId;
0367     return trashPath;
0368 }
0369 
0370 bool TrashImpl::deleteInfo(int trashId, const QString &fileId)
0371 {
0372 #ifdef Q_OS_OSX
0373     createTrashInfrastructure(trashId);
0374 #endif
0375 
0376     if (QFile::remove(infoPath(trashId, fileId))) {
0377         fileRemoved();
0378         return true;
0379     }
0380 
0381     return false;
0382 }
0383 
0384 bool TrashImpl::moveToTrash(const QString &origPath, int trashId, const QString &fileId)
0385 {
0386     // qCDebug(KIO_TRASH) << "Trashing" << origPath << trashId << fileId;
0387     if (!adaptTrashSize(origPath, trashId)) {
0388         return false;
0389     }
0390 
0391 #ifdef Q_OS_OSX
0392     createTrashInfrastructure(trashId);
0393 #endif
0394     const QString dest = filesPath(trashId, fileId);
0395     if (!move(origPath, dest)) {
0396         // Maybe the move failed due to no permissions to delete source.
0397         // In that case, delete dest to keep things consistent, since KIO doesn't do it.
0398         if (QFileInfo(dest).isFile()) {
0399             QFile::remove(dest);
0400         } else {
0401             synchronousDel(dest, false, true);
0402         }
0403         return false;
0404     }
0405 
0406     if (QFileInfo(dest).isDir()) {
0407         TrashSizeCache trashSize(trashDirectoryPath(trashId));
0408         const qint64 pathSize = DiscSpaceUtil::sizeOfPath(dest);
0409         trashSize.add(fileId, pathSize);
0410     }
0411 
0412     fileAdded();
0413     return true;
0414 }
0415 
0416 bool TrashImpl::moveFromTrash(const QString &dest, int trashId, const QString &fileId, const QString &relativePath)
0417 {
0418     QString src = filesPath(trashId, fileId);
0419     if (!relativePath.isEmpty()) {
0420         src += QLatin1Char('/') + relativePath;
0421     }
0422     if (!move(src, dest)) {
0423         return false;
0424     }
0425 
0426     TrashSizeCache trashSize(trashDirectoryPath(trashId));
0427     trashSize.remove(fileId);
0428 
0429     return true;
0430 }
0431 
0432 bool TrashImpl::move(const QString &src, const QString &dest)
0433 {
0434     if (directRename(src, dest)) {
0435         // This notification is done by KIO::moveAs when using the code below
0436         // But if we do a direct rename we need to do the notification ourselves
0437         org::kde::KDirNotify::emitFilesAdded(QUrl::fromLocalFile(dest));
0438         return true;
0439     }
0440     if (m_lastErrorCode != KIO::ERR_UNSUPPORTED_ACTION) {
0441         return false;
0442     }
0443 
0444     const auto urlSrc = QUrl::fromLocalFile(src);
0445     const auto urlDest = QUrl::fromLocalFile(dest);
0446 
0447     // qCDebug(KIO_TRASH) << urlSrc << "->" << urlDest;
0448     KIO::CopyJob *job = KIO::moveAs(urlSrc, urlDest, KIO::HideProgressInfo);
0449     job->setUiDelegate(nullptr);
0450     connect(job, &KJob::result, this, &TrashImpl::jobFinished);
0451     enterLoop();
0452 
0453     return m_lastErrorCode == 0;
0454 }
0455 
0456 void TrashImpl::jobFinished(KJob *job)
0457 {
0458     // qCDebug(KIO_TRASH) << "error=" << job->error() << job->errorText();
0459     error(job->error(), job->errorText());
0460 
0461     Q_EMIT leaveModality();
0462 }
0463 
0464 bool TrashImpl::copyToTrash(const QString &origPath, int trashId, const QString &fileId)
0465 {
0466     // qCDebug(KIO_TRASH);
0467     if (!adaptTrashSize(origPath, trashId)) {
0468         return false;
0469     }
0470 
0471 #ifdef Q_OS_OSX
0472     createTrashInfrastructure(trashId);
0473 #endif
0474     const QString dest = filesPath(trashId, fileId);
0475     if (!copy(origPath, dest)) {
0476         return false;
0477     }
0478 
0479     if (QFileInfo(dest).isDir()) {
0480         TrashSizeCache trashSize(trashDirectoryPath(trashId));
0481         const qint64 pathSize = DiscSpaceUtil::sizeOfPath(dest);
0482         trashSize.add(fileId, pathSize);
0483     }
0484 
0485     fileAdded();
0486     return true;
0487 }
0488 
0489 bool TrashImpl::copyFromTrash(const QString &dest, int trashId, const QString &fileId, const QString &relativePath)
0490 {
0491     const QString src = physicalPath(trashId, fileId, relativePath);
0492     return copy(src, dest);
0493 }
0494 
0495 bool TrashImpl::copy(const QString &src, const QString &dest)
0496 {
0497     // kio_file's copy() method is quite complex (in order to be fast), let's just call it...
0498     m_lastErrorCode = 0;
0499     const auto urlSrc = QUrl::fromLocalFile(src);
0500     const auto urlDest = QUrl::fromLocalFile(dest);
0501     // qCDebug(KIO_TRASH) << "copying" << src << "to" << dest;
0502     KIO::CopyJob *job = KIO::copyAs(urlSrc, urlDest, KIO::HideProgressInfo);
0503     job->setUiDelegate(nullptr);
0504     connect(job, &KJob::result, this, &TrashImpl::jobFinished);
0505     enterLoop();
0506 
0507     return m_lastErrorCode == 0;
0508 }
0509 
0510 bool TrashImpl::directRename(const QString &src, const QString &dest)
0511 {
0512     // qCDebug(KIO_TRASH) << src << "->" << dest;
0513     // Do not use QFile::rename here, we need to be able to move broken symlinks too
0514     // (and we need to make sure errno is set)
0515     if (::rename(QFile::encodeName(src).constData(), QFile::encodeName(dest).constData()) != 0) {
0516         if (errno == EXDEV) {
0517             error(KIO::ERR_UNSUPPORTED_ACTION, QStringLiteral("rename"));
0518         } else {
0519             if ((errno == EACCES) || (errno == EPERM)) {
0520                 error(KIO::ERR_ACCESS_DENIED, dest);
0521             } else if (errno == EROFS) { // The file is on a read-only filesystem
0522                 error(KIO::ERR_CANNOT_DELETE, src);
0523             } else if (errno == ENOENT) {
0524                 const QString marker(QStringLiteral("Trash/files/"));
0525                 const int idx = src.lastIndexOf(marker) + marker.size();
0526                 const QString displayName = QLatin1String("trash:/") + src.mid(idx);
0527                 error(KIO::ERR_DOES_NOT_EXIST, displayName);
0528             } else {
0529                 error(KIO::ERR_CANNOT_RENAME, src);
0530             }
0531         }
0532         return false;
0533     }
0534     return true;
0535 }
0536 
0537 bool TrashImpl::moveInTrash(int trashId, const QString &oldFileId, const QString &newFileId)
0538 {
0539     m_lastErrorCode = 0;
0540 
0541     const QString oldInfo = infoPath(trashId, oldFileId);
0542     const QString oldFile = filesPath(trashId, oldFileId);
0543     const QString newInfo = infoPath(trashId, newFileId);
0544     const QString newFile = filesPath(trashId, newFileId);
0545 
0546     if (directRename(oldInfo, newInfo)) {
0547         if (directRename(oldFile, newFile)) {
0548             // success
0549 
0550             if (QFileInfo(newFile).isDir()) {
0551                 TrashSizeCache trashSize(trashDirectoryPath(trashId));
0552                 trashSize.rename(oldFileId, newFileId);
0553             }
0554             return true;
0555         } else {
0556             // rollback
0557             directRename(newInfo, oldInfo);
0558         }
0559     }
0560     return false;
0561 }
0562 
0563 bool TrashImpl::del(int trashId, const QString &fileId)
0564 {
0565 #ifdef Q_OS_OSX
0566     createTrashInfrastructure(trashId);
0567 #endif
0568 
0569     const QString info = infoPath(trashId, fileId);
0570     const QString file = filesPath(trashId, fileId);
0571 
0572     QT_STATBUF buff;
0573     if (QT_LSTAT(QFile::encodeName(info).constData(), &buff) == -1) {
0574         if (errno == EACCES) {
0575             error(KIO::ERR_ACCESS_DENIED, file);
0576         } else {
0577             error(KIO::ERR_DOES_NOT_EXIST, file);
0578         }
0579         return false;
0580     }
0581 
0582     const bool isDir = QFileInfo(file).isDir();
0583     if (!synchronousDel(file, true, isDir)) {
0584         return false;
0585     }
0586 
0587     if (isDir) {
0588         TrashSizeCache trashSize(trashDirectoryPath(trashId));
0589         trashSize.remove(fileId);
0590     }
0591 
0592     QFile::remove(info);
0593     fileRemoved();
0594     return true;
0595 }
0596 
0597 bool TrashImpl::synchronousDel(const QString &path, bool setLastErrorCode, bool isDir)
0598 {
0599     const int oldErrorCode = m_lastErrorCode;
0600     const QString oldErrorMsg = m_lastErrorMessage;
0601     const auto url = QUrl::fromLocalFile(path);
0602     // First ensure that all dirs have u+w permissions,
0603     // otherwise we won't be able to delete files in them (#130780).
0604     if (isDir) {
0605         //         qCDebug(KIO_TRASH) << "chmod'ing" << url;
0606         KFileItem fileItem(url, QStringLiteral("inode/directory"), KFileItem::Unknown);
0607         KFileItemList fileItemList;
0608         fileItemList.append(fileItem);
0609         KIO::ChmodJob *chmodJob = KIO::chmod(fileItemList, 0200, 0200, QString(), QString(), true /*recursive*/, KIO::HideProgressInfo);
0610         connect(chmodJob, &KJob::result, this, &TrashImpl::jobFinished);
0611         enterLoop();
0612     }
0613 
0614     KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo);
0615     connect(job, &KJob::result, this, &TrashImpl::jobFinished);
0616     enterLoop();
0617     bool ok = m_lastErrorCode == 0;
0618     if (!setLastErrorCode) {
0619         m_lastErrorCode = oldErrorCode;
0620         m_lastErrorMessage = oldErrorMsg;
0621     }
0622     return ok;
0623 }
0624 
0625 bool TrashImpl::emptyTrash()
0626 {
0627     // qCDebug(KIO_TRASH);
0628     // The naive implementation "delete info and files in every trash directory"
0629     // breaks when deleted directories contain files owned by other users.
0630     // We need to ensure that the .trashinfo file is only removed when the
0631     // corresponding files could indeed be removed (#116371)
0632 
0633     // On the other hand, we certainly want to remove any file that has no associated
0634     // .trashinfo file for some reason (#167051)
0635 
0636     QSet<QString> unremovableFiles;
0637 
0638     int myErrorCode = 0;
0639     QString myErrorMsg;
0640     const TrashedFileInfoList fileInfoList = list();
0641     for (const auto &info : fileInfoList) {
0642         const QString filesPath = info.physicalPath;
0643         if (synchronousDel(filesPath, true, true) || m_lastErrorCode == KIO::ERR_DOES_NOT_EXIST) {
0644             QFile::remove(infoPath(info.trashId, info.fileId));
0645         } else {
0646             // error code is set by synchronousDel, let's remember it
0647             // (so that successfully removing another file doesn't erase the error)
0648             myErrorCode = m_lastErrorCode;
0649             myErrorMsg = m_lastErrorMessage;
0650             // and remember not to remove this file
0651             unremovableFiles.insert(filesPath);
0652             qCDebug(KIO_TRASH) << "Unremovable:" << filesPath;
0653         }
0654 
0655         TrashSizeCache trashSize(trashDirectoryPath(info.trashId));
0656         trashSize.clear();
0657     }
0658 
0659     // Now do the orphaned-files cleanup
0660     for (auto trit = m_trashDirectories.cbegin(); trit != m_trashDirectories.cend(); ++trit) {
0661         // const int trashId = trit.key();
0662         const QString filesDir = trit.value() + QLatin1String("/files");
0663         const QStringList list = listDir(filesDir);
0664         for (const QString &fileName : list) {
0665             if (fileName == QLatin1Char('.') || fileName == QLatin1String("..")) {
0666                 continue;
0667             }
0668             const QString filePath = filesDir + QLatin1Char('/') + fileName;
0669             if (!unremovableFiles.contains(filePath)) {
0670                 qCWarning(KIO_TRASH) << "Removing orphaned file" << filePath;
0671                 QFile::remove(filePath);
0672             }
0673         }
0674     }
0675 
0676     m_lastErrorCode = myErrorCode;
0677     m_lastErrorMessage = myErrorMsg;
0678 
0679     fileRemoved();
0680 
0681     return m_lastErrorCode == 0;
0682 }
0683 
0684 TrashImpl::TrashedFileInfoList TrashImpl::list()
0685 {
0686     // Here we scan for trash directories unconditionally. This allows
0687     // noticing plugged-in [e.g. removable] devices, or new mounts etc.
0688     scanTrashDirectories();
0689 
0690     TrashedFileInfoList lst;
0691     // For each known trash directory...
0692     for (auto it = m_trashDirectories.cbegin(); it != m_trashDirectories.cend(); ++it) {
0693         const int trashId = it.key();
0694         QString infoPath = it.value();
0695         infoPath += QLatin1String("/info");
0696         // Code taken from kio_file
0697         const QStringList entryNames = listDir(infoPath);
0698         // char path_buffer[PATH_MAX];
0699         // getcwd(path_buffer, PATH_MAX - 1);
0700         // if ( chdir( infoPathEnc ) )
0701         //    continue;
0702 
0703         const QLatin1String tail(".trashinfo");
0704         const int tailLength = tail.size();
0705         for (const QString &fileName : entryNames) {
0706             if (fileName == QLatin1Char('.') || fileName == QLatin1String("..")) {
0707                 continue;
0708             }
0709             if (!fileName.endsWith(tail)) {
0710                 qCWarning(KIO_TRASH) << "Invalid info file found in" << infoPath << ":" << fileName;
0711                 continue;
0712             }
0713 
0714             TrashedFileInfo info;
0715             if (infoForFile(trashId, fileName.chopped(tailLength), info)) {
0716                 lst << info;
0717             }
0718         }
0719     }
0720     return lst;
0721 }
0722 
0723 // Returns the entries in a given directory - including "." and ".."
0724 QStringList TrashImpl::listDir(const QString &physicalPath)
0725 {
0726     return QDir(physicalPath).entryList(QDir::Dirs | QDir::Files | QDir::Hidden | QDir::System);
0727 }
0728 
0729 bool TrashImpl::infoForFile(int trashId, const QString &fileId, TrashedFileInfo &info)
0730 {
0731     // qCDebug(KIO_TRASH) << trashId << fileId;
0732     info.trashId = trashId; // easy :)
0733     info.fileId = fileId; // equally easy
0734     info.physicalPath = filesPath(trashId, fileId);
0735     return readInfoFile(infoPath(trashId, fileId), info, trashId);
0736 }
0737 
0738 bool TrashImpl::trashSpaceInfo(const QString &path, TrashSpaceInfo &info)
0739 {
0740     const int trashId = findTrashDirectory(path);
0741     if (trashId < 0) {
0742         qCWarning(KIO_TRASH) << "No trash directory found! TrashImpl::findTrashDirectory returned" << trashId;
0743         return false;
0744     }
0745 
0746     const KConfig config(QStringLiteral("ktrashrc"));
0747 
0748     const QString trashPath = trashDirectoryPath(trashId);
0749     const auto group = config.group(trashPath);
0750 
0751     const bool useSizeLimit = group.readEntry("UseSizeLimit", true);
0752     const double percent = group.readEntry("Percent", 10.0);
0753 
0754     DiscSpaceUtil util(trashPath + QLatin1String("/files/"));
0755     qint64 total = util.size();
0756     if (useSizeLimit) {
0757         total *= percent / 100.0;
0758     }
0759 
0760     TrashSizeCache trashSize(trashPath);
0761     const qint64 used = trashSize.calculateSize();
0762 
0763     info.totalSize = total;
0764     info.availableSize = total - used;
0765 
0766     return true;
0767 }
0768 
0769 bool TrashImpl::readInfoFile(const QString &infoPath, TrashedFileInfo &info, int trashId)
0770 {
0771     KConfig cfg(infoPath, KConfig::SimpleConfig);
0772     if (!cfg.hasGroup(QStringLiteral("Trash Info"))) {
0773         error(KIO::ERR_CANNOT_OPEN_FOR_READING, infoPath);
0774         return false;
0775     }
0776     const KConfigGroup group = cfg.group(QStringLiteral("Trash Info"));
0777     info.origPath = QUrl::fromPercentEncoding(group.readEntry("Path").toLatin1());
0778     if (info.origPath.isEmpty()) {
0779         return false; // path is mandatory...
0780     }
0781     if (trashId == 0) {
0782         Q_ASSERT(info.origPath[0] == QLatin1Char('/'));
0783     } else {
0784         const QString topdir = topDirectoryPath(trashId); // includes trailing slash
0785         info.origPath.prepend(topdir);
0786     }
0787     const QString line = group.readEntry("DeletionDate");
0788     if (!line.isEmpty()) {
0789         info.deletionDate = QDateTime::fromString(line, Qt::ISODate);
0790     }
0791     return true;
0792 }
0793 
0794 QString TrashImpl::physicalPath(int trashId, const QString &fileId, const QString &relativePath)
0795 {
0796     QString filePath = filesPath(trashId, fileId);
0797     if (!relativePath.isEmpty()) {
0798         filePath += QLatin1Char('/') + relativePath;
0799     }
0800     return filePath;
0801 }
0802 
0803 void TrashImpl::error(int e, const QString &s)
0804 {
0805     if (e) {
0806         qCDebug(KIO_TRASH) << e << s;
0807     }
0808     m_lastErrorCode = e;
0809     m_lastErrorMessage = s;
0810 }
0811 
0812 bool TrashImpl::isEmpty() const
0813 {
0814     // For each known trash directory...
0815     if (!m_trashDirectoriesScanned) {
0816         scanTrashDirectories();
0817     }
0818 
0819     for (auto it = m_trashDirectories.cbegin(); it != m_trashDirectories.cend(); ++it) {
0820         const QString infoPath = it.value() + QLatin1String("/info");
0821 
0822         DIR *dp = ::opendir(QFile::encodeName(infoPath).constData());
0823         if (dp) {
0824             struct dirent *ep;
0825             ep = readdir(dp);
0826             ep = readdir(dp); // ignore '.' and '..' dirent
0827             ep = readdir(dp); // look for third file
0828             closedir(dp);
0829             if (ep != nullptr) {
0830                 // qCDebug(KIO_TRASH) << ep->d_name << "in" << infoPath << "-> not empty";
0831                 return false; // not empty
0832             }
0833         }
0834     }
0835     return true;
0836 }
0837 
0838 void TrashImpl::fileAdded()
0839 {
0840     m_config.reparseConfiguration();
0841     KConfigGroup group = m_config.group(QStringLiteral("Status"));
0842     if (group.readEntry("Empty", true) == true) {
0843         group.writeEntry("Empty", false);
0844         m_config.sync();
0845     }
0846     // The apps showing the trash (e.g. kdesktop) will be notified
0847     // of this change when KDirNotify::FilesAdded("trash:/") is emitted,
0848     // which will be done by the job soon after this.
0849 }
0850 
0851 void TrashImpl::fileRemoved()
0852 {
0853     if (isEmpty()) {
0854         deleteEmptyTrashInfrastructure();
0855         KConfigGroup group = m_config.group(QStringLiteral("Status"));
0856         group.writeEntry("Empty", true);
0857         m_config.sync();
0858         org::kde::KDirNotify::emitFilesChanged({QUrl::fromEncoded("trash:/")});
0859     }
0860     // The apps showing the trash (e.g. kdesktop) will be notified
0861     // of this change when KDirNotify::FilesRemoved(...) is emitted,
0862     // which will be done by the job soon after this.
0863 }
0864 
0865 #ifdef Q_OS_OSX
0866 #include <CoreFoundation/CoreFoundation.h>
0867 #include <DiskArbitration/DiskArbitration.h>
0868 #include <sys/mount.h>
0869 
0870 int TrashImpl::idForMountPoint(const QString &mountPoint) const
0871 {
0872     DADiskRef disk;
0873     CFDictionaryRef descDict;
0874     DASessionRef session = DASessionCreate(NULL);
0875     int devId = -1;
0876     if (session) {
0877         QByteArray mp = QFile::encodeName(mountPoint);
0878         struct statfs statFS;
0879         statfs(mp.constData(), &statFS);
0880         disk = DADiskCreateFromBSDName(kCFAllocatorDefault, session, statFS.f_mntfromname);
0881         if (disk) {
0882             descDict = DADiskCopyDescription(disk);
0883             if (descDict) {
0884                 CFNumberRef cfMajor = (CFNumberRef)CFDictionaryGetValue(descDict, kDADiskDescriptionMediaBSDMajorKey);
0885                 CFNumberRef cfMinor = (CFNumberRef)CFDictionaryGetValue(descDict, kDADiskDescriptionMediaBSDMinorKey);
0886                 int major, minor;
0887                 if (CFNumberGetValue(cfMajor, kCFNumberIntType, &major) && CFNumberGetValue(cfMinor, kCFNumberIntType, &minor)) {
0888                     qCWarning(KIO_TRASH) << "major=" << major << " minor=" << minor;
0889                     devId = 1000 * major + minor;
0890                 }
0891                 CFRelease(cfMajor);
0892                 CFRelease(cfMinor);
0893             } else {
0894                 qCWarning(KIO_TRASH) << "couldn't get DADiskCopyDescription from" << disk;
0895             }
0896             CFRelease(disk);
0897         } else {
0898             qCWarning(KIO_TRASH) << "DADiskCreateFromBSDName failed on statfs from" << mp;
0899         }
0900         CFRelease(session);
0901     } else {
0902         qCWarning(KIO_TRASH) << "couldn't create DASession";
0903     }
0904     return devId;
0905 }
0906 
0907 #else
0908 
0909 int TrashImpl::idForDevice(const Solid::Device &device) const
0910 {
0911     const Solid::Block *block = device.as<Solid::Block>();
0912     if (block) {
0913         // qCDebug(KIO_TRASH) << "major=" << block->deviceMajor() << "minor=" << block->deviceMinor();
0914         return block->deviceMajor() * 1000 + block->deviceMinor();
0915     } else {
0916         const Solid::NetworkShare *netshare = device.as<Solid::NetworkShare>();
0917 
0918         if (netshare) {
0919             QString url = netshare->url().url();
0920 
0921             QLockFile configLock(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/trashrc.nextid.lock"));
0922 
0923             if (!configLock.lock()) {
0924                 return -1;
0925             }
0926 
0927             m_config.reparseConfiguration();
0928             KConfigGroup group = m_config.group(QStringLiteral("NetworkShares"));
0929             int id = group.readEntry(url, -1);
0930 
0931             if (id == -1) {
0932                 id = group.readEntry("NextID", 0);
0933                 // qCDebug(KIO_TRASH) << "new share=" << url << " id=" << id;
0934 
0935                 group.writeEntry(url, id);
0936                 group.writeEntry("NextID", id + 1);
0937                 group.sync();
0938             }
0939 
0940             return 6000000 + id;
0941         }
0942 
0943         // Not a block device nor a network share
0944         return -1;
0945     }
0946 }
0947 
0948 void TrashImpl::refreshDevices() const
0949 {
0950     // this is needed because Solid's fstab backend uses QSocketNotifier
0951     // to get notifications about changes to mtab
0952     // otherwise we risk getting old device list
0953     qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
0954 }
0955 #endif
0956 
0957 void TrashImpl::insertTrashDir(int id, const QString &trashDir, const QString &topdir) const
0958 {
0959     m_trashDirectories.insert(id, trashDir);
0960     qCDebug(KIO_TRASH) << "found" << trashDir << "gave it id" << id;
0961     m_topDirectories.insert(id, Utils::slashAppended(topdir));
0962 }
0963 
0964 int TrashImpl::findTrashDirectory(const QString &origPath)
0965 {
0966     // qCDebug(KIO_TRASH) << origPath;
0967     // Check if it's on the same device as $HOME
0968     QT_STATBUF buff;
0969     if (QT_LSTAT(QFile::encodeName(origPath).constData(), &buff) == 0 && buff.st_dev == m_homeDevice) {
0970         return 0;
0971     }
0972 
0973     KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(origPath);
0974     if (!mp) {
0975         // qCDebug(KIO_TRASH) << "KMountPoint found no mount point for" << origPath;
0976         return 0;
0977     }
0978 
0979     QString mountPoint = mp->mountPoint();
0980     const QString trashDir = trashForMountPoint(mountPoint, true);
0981     // qCDebug(KIO_TRASH) << "mountPoint=" << mountPoint << "trashDir=" << trashDir;
0982 
0983 #ifndef Q_OS_OSX
0984     if (trashDir.isEmpty()) {
0985         return 0; // no trash available on partition
0986     }
0987 #endif
0988 
0989     int id = idForTrashDirectory(trashDir);
0990     if (id > -1) {
0991         qCDebug(KIO_TRASH) << "Found Trash dir" << trashDir << "with id" << id;
0992         return id;
0993     }
0994 
0995 #ifdef Q_OS_OSX
0996     id = idForMountPoint(mountPoint);
0997 #else
0998     refreshDevices();
0999     const QString query = QLatin1String("[StorageAccess.accessible == true AND StorageAccess.filePath == '%1']").arg(mountPoint);
1000     const QList<Solid::Device> lst = Solid::Device::listFromQuery(query);
1001     qCDebug(KIO_TRASH) << "Queried Solid with" << query << "got" << lst.count() << "devices";
1002     if (lst.isEmpty()) { // not a device. Maybe some tmpfs mount for instance.
1003         return 0;
1004     }
1005 
1006     // Pretend we got exactly one...
1007     const Solid::Device device = lst.at(0);
1008     id = idForDevice(device);
1009 #endif
1010     if (id == -1) {
1011         return 0;
1012     }
1013 
1014     // New trash dir found, register it
1015     insertTrashDir(id, trashDir, mountPoint);
1016     return id;
1017 }
1018 
1019 KIO::UDSEntry TrashImpl::trashUDSEntry(KIO::StatDetails details)
1020 {
1021     KIO::UDSEntry entry;
1022     if (details & KIO::StatRecursiveSize) {
1023         KIO::filesize_t size = 0;
1024         long latestModifiedDate = 0;
1025 
1026         for (const QString &trashPath : std::as_const(m_trashDirectories)) {
1027             TrashSizeCache trashSize(trashPath);
1028             TrashSizeCache::SizeAndModTime res = trashSize.calculateSizeAndLatestModDate();
1029             size += res.size;
1030 
1031             // Find latest modification date
1032             if (res.mtime > latestModifiedDate) {
1033                 latestModifiedDate = res.mtime;
1034             }
1035         }
1036 
1037         entry.reserve(3);
1038         entry.fastInsert(KIO::UDSEntry::UDS_RECURSIVE_SIZE, static_cast<long long>(size));
1039 
1040         entry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, latestModifiedDate / 1000);
1041         // access date is unreliable for the trash folder, use the modified date instead
1042         entry.fastInsert(KIO::UDSEntry::UDS_ACCESS_TIME, latestModifiedDate / 1000);
1043     }
1044     return entry;
1045 }
1046 
1047 void TrashImpl::scanTrashDirectories() const
1048 {
1049 #ifndef Q_OS_OSX
1050     refreshDevices();
1051 #endif
1052 
1053     const QList<Solid::Device> lst = Solid::Device::listFromQuery(QStringLiteral("StorageAccess.accessible == true"));
1054     for (const Solid::Device &device : lst) {
1055         QString topdir = device.as<Solid::StorageAccess>()->filePath();
1056         QString trashDir = trashForMountPoint(topdir, false);
1057         if (!trashDir.isEmpty()) {
1058             // OK, trashDir is a valid trash directory. Ensure it's registered.
1059             int trashId = idForTrashDirectory(trashDir);
1060             if (trashId == -1) {
1061                 // new trash dir found, register it
1062 #ifdef Q_OS_OSX
1063                 trashId = idForMountPoint(topdir);
1064 #else
1065                 trashId = idForDevice(device);
1066 #endif
1067                 if (trashId == -1) {
1068                     continue;
1069                 }
1070 
1071                 insertTrashDir(trashId, trashDir, topdir);
1072             }
1073         }
1074     }
1075     m_trashDirectoriesScanned = true;
1076 }
1077 
1078 TrashImpl::TrashDirMap TrashImpl::trashDirectories() const
1079 {
1080     if (!m_trashDirectoriesScanned) {
1081         scanTrashDirectories();
1082     }
1083     return m_trashDirectories;
1084 }
1085 
1086 TrashImpl::TrashDirMap TrashImpl::topDirectories() const
1087 {
1088     if (!m_trashDirectoriesScanned) {
1089         scanTrashDirectories();
1090     }
1091     return m_topDirectories;
1092 }
1093 
1094 QString TrashImpl::trashForMountPoint(const QString &topdir, bool createIfNeeded) const
1095 {
1096     // (1) Administrator-created $topdir/.Trash directory
1097 
1098 #ifndef Q_OS_OSX
1099     const QString rootTrashDir = topdir + QLatin1String("/.Trash");
1100 #else
1101     const QString rootTrashDir = topdir + QLatin1String("/.Trashes");
1102 #endif
1103     const QByteArray rootTrashDir_c = QFile::encodeName(rootTrashDir);
1104     // Can't use QFileInfo here since we need to test for the sticky bit
1105     uid_t uid = getuid();
1106     QT_STATBUF buff;
1107     const unsigned int requiredBits = S_ISVTX; // Sticky bit required
1108     if (QT_LSTAT(rootTrashDir_c.constData(), &buff) == 0) {
1109         if ((S_ISDIR(buff.st_mode)) // must be a dir
1110             && (!S_ISLNK(buff.st_mode)) // not a symlink
1111             && ((buff.st_mode & requiredBits) == requiredBits) //
1112             && (::access(rootTrashDir_c.constData(), W_OK) == 0) // must be user-writable
1113         ) {
1114             if (buff.st_dev == m_homeDevice) // bind mount, maybe
1115                 return QString();
1116 #ifndef Q_OS_OSX
1117             const QString trashDir = rootTrashDir + QLatin1Char('/') + QString::number(uid);
1118 #else
1119             QString trashDir = rootTrashDir + QLatin1Char('/') + QString::number(uid);
1120 #endif
1121             const QByteArray trashDir_c = QFile::encodeName(trashDir);
1122             if (QT_LSTAT(trashDir_c.constData(), &buff) == 0) {
1123                 if ((buff.st_uid == uid) // must be owned by user
1124                     && (S_ISDIR(buff.st_mode)) // must be a dir
1125                     && (!S_ISLNK(buff.st_mode)) // not a symlink
1126                     && (buff.st_mode & 0777) == 0700) { // rwx for user
1127 #ifdef Q_OS_OSX
1128                     trashDir += QStringLiteral("/KDE.trash");
1129 #endif
1130                     return trashDir;
1131                 }
1132                 qCWarning(KIO_TRASH) << "Directory" << trashDir << "exists but didn't pass the security checks, can't use it";
1133             } else if (createIfNeeded && initTrashDirectory(trashDir_c)) {
1134                 return trashDir;
1135             }
1136         } else {
1137             qCWarning(KIO_TRASH) << "Root trash dir" << rootTrashDir << "exists but didn't pass the security checks, can't use it";
1138         }
1139     }
1140 
1141 #ifndef Q_OS_OSX
1142     // (2) $topdir/.Trash-$uid
1143     const QString trashDir = topdir + QLatin1String("/.Trash-") + QString::number(uid);
1144     const QByteArray trashDir_c = QFile::encodeName(trashDir);
1145     if (QT_LSTAT(trashDir_c.constData(), &buff) == 0) {
1146         if ((buff.st_uid == uid) // must be owned by user
1147             && S_ISDIR(buff.st_mode) // must be a dir
1148             && !S_ISLNK(buff.st_mode) // not a symlink
1149             && ((buff.st_mode & 0700) == 0700)) { // and we need write access to it
1150 
1151             if (buff.st_dev == m_homeDevice) // bind mount, maybe
1152                 return QString();
1153             if (checkTrashSubdirs(trashDir_c)) {
1154                 return trashDir;
1155             }
1156         }
1157         qCWarning(KIO_TRASH) << "Directory" << trashDir << "exists but didn't pass the security checks, can't use it";
1158         // Exists, but not usable
1159         return QString();
1160     }
1161     if (createIfNeeded && initTrashDirectory(trashDir_c)) {
1162         return trashDir;
1163     }
1164 #endif
1165     return QString();
1166 }
1167 
1168 int TrashImpl::idForTrashDirectory(const QString &trashDir) const
1169 {
1170     // If this is too slow we can always use a reverse map...
1171     for (auto it = m_trashDirectories.cbegin(); it != m_trashDirectories.cend(); ++it) {
1172         if (it.value() == trashDir) {
1173             return it.key();
1174         }
1175     }
1176     return -1;
1177 }
1178 
1179 bool TrashImpl::initTrashDirectory(const QByteArray &trashDir_c) const
1180 {
1181     if (mkdir(trashDir_c.constData(), 0700) != 0) {
1182         return false;
1183     }
1184     return checkTrashSubdirs(trashDir_c);
1185 }
1186 
1187 bool TrashImpl::checkTrashSubdirs(const QByteArray &trashDir_c) const
1188 {
1189     const QString trashDir = QFile::decodeName(trashDir_c);
1190     const QString info = trashDir + QLatin1String("/info");
1191     const QString files = trashDir + QLatin1String("/files");
1192     return testDir(info) == 0 && testDir(files) == 0;
1193 }
1194 
1195 QString TrashImpl::trashDirectoryPath(int trashId) const
1196 {
1197     // Never scanned for trash dirs? (This can happen after killing kio_trash
1198     // and reusing a directory listing from the earlier instance.)
1199     if (!m_trashDirectoriesScanned) {
1200         scanTrashDirectories();
1201     }
1202     Q_ASSERT(m_trashDirectories.contains(trashId));
1203     return m_trashDirectories[trashId];
1204 }
1205 
1206 QString TrashImpl::topDirectoryPath(int trashId) const
1207 {
1208     if (!m_trashDirectoriesScanned) {
1209         scanTrashDirectories();
1210     }
1211     assert(trashId != 0);
1212     Q_ASSERT(m_topDirectories.contains(trashId));
1213     return m_topDirectories[trashId];
1214 }
1215 
1216 // Helper method. Creates a URL with the format trash:/trashid-fileid or
1217 // trash:/trashid-fileid/relativePath/To/File for a file inside a trashed directory.
1218 QUrl TrashImpl::makeURL(int trashId, const QString &fileId, const QString &relativePath)
1219 {
1220     QUrl url;
1221     url.setScheme(QStringLiteral("trash"));
1222     QString path = QLatin1Char('/') + QString::number(trashId) + QLatin1Char('-') + fileId;
1223     if (!relativePath.isEmpty()) {
1224         path += QLatin1Char('/') + relativePath;
1225     }
1226     url.setPath(path);
1227     return url;
1228 }
1229 
1230 // Helper method. Parses a trash URL with the URL scheme defined in makeURL.
1231 // The trash:/ URL itself isn't parsed here, must be caught by the caller before hand.
1232 bool TrashImpl::parseURL(const QUrl &url, int &trashId, QString &fileId, QString &relativePath)
1233 {
1234     if (url.scheme() != QLatin1String("trash")) {
1235         return false;
1236     }
1237     const QString path = url.path();
1238     if (path.isEmpty()) {
1239         return false;
1240     }
1241     int start = 0;
1242     if (path[0] == QLatin1Char('/')) { // always true I hope
1243         start = 1;
1244     }
1245     int slashPos = path.indexOf(QLatin1Char('-'), 0); // don't match leading slash
1246     if (slashPos <= 0) {
1247         return false;
1248     }
1249     bool ok = false;
1250 
1251     trashId = QStringView(path).mid(start, slashPos - start).toInt(&ok);
1252 
1253     Q_ASSERT_X(ok, Q_FUNC_INFO, qUtf8Printable(url.toString()));
1254     if (!ok) {
1255         return false;
1256     }
1257     start = slashPos + 1;
1258     slashPos = path.indexOf(QLatin1Char('/'), start);
1259     if (slashPos <= 0) {
1260         fileId = path.mid(start);
1261         relativePath.clear();
1262         return true;
1263     }
1264     fileId = path.mid(start, slashPos - start);
1265     relativePath = path.mid(slashPos + 1);
1266     return true;
1267 }
1268 
1269 bool TrashImpl::adaptTrashSize(const QString &origPath, int trashId)
1270 {
1271     KConfig config(QStringLiteral("ktrashrc"));
1272 
1273     const QString trashPath = trashDirectoryPath(trashId);
1274     KConfigGroup group = config.group(trashPath);
1275 
1276     const bool useTimeLimit = group.readEntry("UseTimeLimit", false);
1277     const bool useSizeLimit = group.readEntry("UseSizeLimit", true);
1278     const double percent = group.readEntry("Percent", 10.0);
1279     const int actionType = group.readEntry("LimitReachedAction", 0);
1280 
1281     if (useTimeLimit) { // delete all files in trash older than X days
1282         const int maxDays = group.readEntry("Days", 7);
1283         const QDateTime currentDate = QDateTime::currentDateTime();
1284 
1285         const TrashedFileInfoList trashedFiles = list();
1286         for (const auto &info : trashedFiles) {
1287             if (info.trashId != trashId) {
1288                 continue;
1289             }
1290 
1291             if (info.deletionDate.daysTo(currentDate) > maxDays) {
1292                 del(info.trashId, info.fileId);
1293             }
1294         }
1295     }
1296 
1297     if (!useSizeLimit) { // check if size limit exceeded
1298         return true;
1299     }
1300 
1301     // calculate size of the files to be put into the trash
1302     const qint64 additionalSize = DiscSpaceUtil::sizeOfPath(origPath);
1303 
1304 #ifdef Q_OS_OSX
1305     createTrashInfrastructure(trashId);
1306 #endif
1307     DiscSpaceUtil util(trashPath + QLatin1String("/files/"));
1308     auto cache = TrashSizeCache(trashPath);
1309     auto trashSize = cache.calculateSize();
1310 
1311     if (util.usage(trashSize + additionalSize) < percent) {
1312         return true;
1313     }
1314 
1315     // before we start to remove any files from the trash,
1316     // check whether the new file will fit into the trash
1317     // at all...
1318     const qint64 partitionSize = util.size();
1319 
1320     if ((util.usage(partitionSize + additionalSize)) >= percent) {
1321         m_lastErrorCode = KIO::ERR_TRASH_FILE_TOO_LARGE;
1322         m_lastErrorMessage = KIO::buildErrorString(m_lastErrorCode, {});
1323         return false;
1324     }
1325 
1326     if (actionType == 0) { // warn the user only
1327         m_lastErrorCode = KIO::ERR_WORKER_DEFINED;
1328         m_lastErrorMessage = i18n("The trash is full. Empty it or remove items manually.");
1329         return false;
1330     }
1331 
1332     // Start removing some other files from the trash
1333 
1334     QDir::SortFlags sortFlags;
1335     if (actionType == 1) {
1336         sortFlags = QDir::Time | QDir::Reversed; // Delete oldest files first
1337     } else if (actionType == 2) {
1338         sortFlags = QDir::Size; // Delete biggest files first
1339     } else {
1340         qWarning() << "Called with actionType" << actionType << ", which theoretically should never happen!";
1341         return false; // Bail out
1342     }
1343 
1344     const auto dirCache = cache.readDirCache();
1345     constexpr QDir::Filters dirFilters = QDir::Files | QDir::AllDirs | QDir::NoDotAndDotDot;
1346     const QFileInfoList infoList = QDir(trashPath + QLatin1String("/files")).entryInfoList(dirFilters, sortFlags);
1347     for (const auto &info : infoList) {
1348         auto fileSizeFreed = info.size();
1349         if (info.isDir()) {
1350             fileSizeFreed = dirCache.constFind(info.path().toUtf8())->size;
1351         }
1352 
1353         del(trashId, info.fileName()); // delete trashed file
1354         trashSize -= fileSizeFreed;
1355 
1356         if (util.usage(trashSize + additionalSize) < percent) { // check whether we have enough space now
1357             return true;
1358         }
1359     }
1360 
1361     return true;
1362 }
1363 
1364 #include "moc_trashimpl.cpp"