1 /*
2     This file is part of the KDE project
3     SPDX-FileCopyrightText: 2004 David Faure <faure@kde.org>
4 
5     SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7 
8 #include "trashimpl.h"
9 #include "discspaceutil.h"
10 #include "kiotrashdebug.h"
11 #include "trashsizecache.h"
12 
13 #include <kdirnotify.h>
14 #include <kfileitem.h>
15 #include <kio/chmodjob.h>
16 #include <kio/copyjob.h>
17 #include <kio/deletejob.h>
18 #include <kmountpoint.h>
19 
20 #include <KConfigGroup>
21 #include <KFileUtils>
22 #include <KJobUiDelegate>
23 #include <KLocalizedString>
24 #include <KSharedConfig>
25 #include <solid/block.h>
26 #include <solid/device.h>
27 #include <solid/networkshare.h>
28 #include <solid/storageaccess.h>
29 
30 #include <QCoreApplication>
31 #include <QDebug>
32 #include <QDir>
33 #include <QEventLoop>
34 #include <QFile>
35 #include <QLockFile>
36 #include <QStandardPaths>
37 #include <QUrl>
38 
39 #include <cerrno>
40 #include <dirent.h>
41 #include <fcntl.h>
42 #include <stdlib.h>
43 #include <sys/param.h>
44 #include <sys/stat.h>
45 #include <sys/types.h>
46 #include <unistd.h>
47 
TrashImpl()48 TrashImpl::TrashImpl()
49     : QObject()
50     , m_lastErrorCode(0)
51     , m_initStatus(InitToBeDone)
52     , m_homeDevice(0)
53     , m_trashDirectoriesScanned(false)
54     ,
55     // not using kio_trashrc since KIO uses that one already for kio_trash
56     // so better have a separate one, for faster parsing by e.g. kmimetype.cpp
57     m_config(QStringLiteral("trashrc"), KConfig::SimpleConfig)
58 {
59     QT_STATBUF buff;
60     if (QT_LSTAT(QFile::encodeName(QDir::homePath()).constData(), &buff) == 0) {
61         m_homeDevice = buff.st_dev;
62     } else {
63         qCWarning(KIO_TRASH) << "Should never happen: couldn't stat $HOME" << strerror(errno);
64     }
65 }
66 
67 /**
68  * Test if a directory exists, create otherwise
69  * @param _name full path of the directory
70  * @return errorcode, or 0 if the dir was created or existed already
71  * Warning, don't use return value like a bool
72  */
testDir(const QString & _name) const73 int TrashImpl::testDir(const QString &_name) const
74 {
75     DIR *dp = ::opendir(QFile::encodeName(_name).constData());
76     if (!dp) {
77         QString name = _name;
78         if (name.endsWith(QLatin1Char('/'))) {
79             name.chop(1);
80         }
81 
82         bool ok = QDir().mkdir(name);
83         if (!ok && QFile::exists(name)) {
84             QString new_name = name;
85             name.append(QStringLiteral(".orig"));
86             if (QFile::rename(name, new_name)) {
87                 ok = QDir().mkdir(name);
88             } else { // foo.orig existed already. How likely is that?
89                 ok = false;
90             }
91             if (!ok) {
92                 return KIO::ERR_DIR_ALREADY_EXIST;
93             }
94         }
95         if (!ok) {
96             // KMessageBox::sorry( 0, i18n( "Could not create directory %1. Check for permissions." ).arg( name ) );
97             qCWarning(KIO_TRASH) << "could not create" << name;
98             return KIO::ERR_CANNOT_MKDIR;
99         } else {
100             // qCDebug(KIO_TRASH) << name << "created.";
101         }
102     } else { // exists already
103         closedir(dp);
104     }
105     return 0; // success
106 }
107 
deleteEmptyTrashInfrastructure()108 void TrashImpl::deleteEmptyTrashInfrastructure()
109 {
110 #ifdef Q_OS_OSX
111     // For each known trash directory...
112     if (!m_trashDirectoriesScanned) {
113         scanTrashDirectories();
114     }
115 
116     for (auto it = m_trashDirectories.cbegin(); it != m_trashDirectories.cend(); ++it) {
117         const QString trashPath = it.value();
118         QString infoPath = trashPath + QLatin1String("/info");
119 
120         // qCDebug(KIO_TRASH) << "empty Trash" << trashPath << "; removing infrastructure";
121         synchronousDel(infoPath, false, true);
122         synchronousDel(trashPath + QLatin1String("/files"), false, true);
123         if (trashPath.endsWith(QLatin1String("/KDE.trash"))) {
124             synchronousDel(trashPath, false, true);
125         }
126     }
127 #endif
128 }
129 
createTrashInfrastructure(int trashId,const QString & path)130 bool TrashImpl::createTrashInfrastructure(int trashId, const QString &path)
131 {
132     const QString trashDir = path.isEmpty() ? trashDirectoryPath(trashId) : path;
133     if (const int err = testDir(trashDir)) {
134         error(err, trashDir);
135         return false;
136     }
137 
138     const QString infoDir = trashDir + QLatin1String("/info");
139     if (const int err = testDir(infoDir)) {
140         error(err, infoDir);
141         return false;
142     }
143 
144     const QString filesDir = trashDir + QLatin1String("/files");
145     if (const int err = testDir(filesDir)) {
146         error(err, filesDir);
147         return false;
148     }
149 
150     return true;
151 }
152 
init()153 bool TrashImpl::init()
154 {
155     if (m_initStatus == InitOK) {
156         return true;
157     }
158     if (m_initStatus == InitError) {
159         return false;
160     }
161 
162     // Check the trash directory and its info and files subdirs
163     // see also kdesktop/init.cc for first time initialization
164     m_initStatus = InitError;
165 #ifndef Q_OS_OSX
166     // $XDG_DATA_HOME/Trash, i.e. ~/.local/share/Trash by default.
167     const QString xdgDataDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/');
168     if (!QDir().mkpath(xdgDataDir)) {
169         qCWarning(KIO_TRASH) << "failed to create" << xdgDataDir;
170         return false;
171     }
172 
173     const QString trashDir = xdgDataDir + QLatin1String("Trash");
174     if (!createTrashInfrastructure(0, trashDir)) {
175         return false;
176     }
177 #else
178     // we DO NOT create ~/.Trash on OS X, that's the operating system's privilege
179     QString trashDir = QDir::homePath() + QLatin1String("/.Trash");
180     if (!QFileInfo(trashDir).isDir()) {
181         error(KIO::ERR_DOES_NOT_EXIST, trashDir);
182         return false;
183     }
184     trashDir += QLatin1String("/KDE.trash");
185     // we don't have to call createTrashInfrastructure() here because it'll be called when needed.
186 #endif
187     m_trashDirectories.insert(0, trashDir);
188     m_initStatus = InitOK;
189     // qCDebug(KIO_TRASH) << "initialization OK, home trash dir:" << trashDir;
190     return true;
191 }
192 
migrateOldTrash()193 void TrashImpl::migrateOldTrash()
194 {
195     qCDebug(KIO_TRASH);
196 
197     KConfigGroup g(KSharedConfig::openConfig(), "Paths");
198     const QString oldTrashDir = g.readPathEntry("Trash", QString());
199 
200     if (oldTrashDir.isEmpty()) {
201         return;
202     }
203 
204     const QStringList entries = listDir(oldTrashDir);
205     bool allOK = true;
206     for (QString srcPath : entries) {
207         if (srcPath == QLatin1Char('.') || srcPath == QLatin1String("..") || srcPath == QLatin1String(".directory")) {
208             continue;
209         }
210         srcPath.prepend(oldTrashDir); // make absolute
211         int trashId;
212         QString fileId;
213         if (!createInfo(srcPath, trashId, fileId)) {
214             qCWarning(KIO_TRASH) << "Trash migration: failed to create info for" << srcPath;
215             allOK = false;
216         } else {
217             bool ok = moveToTrash(srcPath, trashId, fileId);
218             if (!ok) {
219                 (void)deleteInfo(trashId, fileId);
220                 qCWarning(KIO_TRASH) << "Trash migration: failed to create info for" << srcPath;
221                 allOK = false;
222             } else {
223                 qCDebug(KIO_TRASH) << "Trash migration: moved" << srcPath;
224             }
225         }
226     }
227     if (allOK) {
228         // We need to remove the old one, otherwise the desktop will have two trashcans...
229         qCDebug(KIO_TRASH) << "Trash migration: all OK, removing old trash directory";
230         synchronousDel(oldTrashDir, false, true);
231     }
232 }
233 
createInfo(const QString & origPath,int & trashId,QString & fileId)234 bool TrashImpl::createInfo(const QString &origPath, int &trashId, QString &fileId)
235 {
236     // off_t should be 64bit on Unix systems to have large file support
237     // FIXME: on windows this gets disabled until trash gets integrated
238     // BUG: 165449
239 #ifndef Q_OS_WIN
240     Q_STATIC_ASSERT(sizeof(off_t) >= 8);
241 #endif
242 
243     // qCDebug(KIO_TRASH) << origPath;
244     // Check source
245     QT_STATBUF buff_src;
246     if (QT_LSTAT(QFile::encodeName(origPath).constData(), &buff_src) == -1) {
247         if (errno == EACCES) {
248             error(KIO::ERR_ACCESS_DENIED, origPath);
249         } else {
250             error(KIO::ERR_DOES_NOT_EXIST, origPath);
251         }
252         return false;
253     }
254 
255     // Choose destination trash
256     trashId = findTrashDirectory(origPath);
257     if (trashId < 0) {
258         qCWarning(KIO_TRASH) << "OUCH - internal error, TrashImpl::findTrashDirectory returned" << trashId;
259         return false; // ### error() needed?
260     }
261     // qCDebug(KIO_TRASH) << "trashing to" << trashId;
262 
263     // Grab original filename
264     auto url = QUrl::fromLocalFile(origPath);
265     url = url.adjusted(QUrl::StripTrailingSlash);
266     const QString origFileName = url.fileName();
267 
268     // Make destination file in info/
269 #ifdef Q_OS_OSX
270     createTrashInfrastructure(trashId);
271 #endif
272     url.setPath(infoPath(trashId, origFileName)); // we first try with origFileName
273     QUrl baseDirectory = QUrl::fromLocalFile(url.path());
274     // Here we need to use O_EXCL to avoid race conditions with other kioslave processes
275     int fd = 0;
276     QString fileName;
277     do {
278         // qCDebug(KIO_TRASH) << "trying to create" << url.path();
279         fd = ::open(QFile::encodeName(url.path()).constData(), O_WRONLY | O_CREAT | O_EXCL, 0600);
280         if (fd < 0) {
281             if (errno == EEXIST) {
282                 fileName = url.fileName();
283                 url = url.adjusted(QUrl::RemoveFilename);
284                 url.setPath(url.path() + KFileUtils::suggestName(baseDirectory, fileName));
285                 // and try again on the next iteration
286             } else {
287                 error(KIO::ERR_CANNOT_WRITE, url.path());
288                 return false;
289             }
290         }
291     } while (fd < 0);
292     const QString infoPath = url.path();
293     fileId = url.fileName();
294     Q_ASSERT(fileId.endsWith(QLatin1String(".trashinfo")));
295     fileId.chop(10); // remove .trashinfo from fileId
296 
297     FILE *file = ::fdopen(fd, "w");
298     if (!file) { // can't see how this would happen
299         error(KIO::ERR_CANNOT_WRITE, infoPath);
300         return false;
301     }
302 
303     // Contents of the info file. We could use KSimpleConfig, but that would
304     // mean closing and reopening fd, i.e. opening a race condition...
305     QByteArray info = "[Trash Info]\n";
306     info += "Path=";
307     // Escape filenames according to the way they are encoded on the filesystem
308     // All this to basically get back to the raw 8-bit representation of the filename...
309     if (trashId == 0) { // home trash: absolute path
310         info += QUrl::toPercentEncoding(origPath, "/");
311     } else {
312         info += QUrl::toPercentEncoding(makeRelativePath(topDirectoryPath(trashId), origPath), "/");
313     }
314     info += '\n';
315     info += "DeletionDate=" + QDateTime::currentDateTime().toString(Qt::ISODate).toLatin1() + '\n';
316     size_t sz = info.size();
317 
318     size_t written = ::fwrite(info.data(), 1, sz, file);
319     if (written != sz) {
320         ::fclose(file);
321         QFile::remove(infoPath);
322         error(KIO::ERR_DISK_FULL, infoPath);
323         return false;
324     }
325 
326     ::fclose(file);
327 
328     // qCDebug(KIO_TRASH) << "info file created in trashId=" << trashId << ":" << fileId;
329     return true;
330 }
331 
makeRelativePath(const QString & topdir,const QString & path)332 QString TrashImpl::makeRelativePath(const QString &topdir, const QString &path)
333 {
334     QString realPath = QFileInfo(path).canonicalFilePath();
335     if (realPath.isEmpty()) { // shouldn't happen
336         realPath = path;
337     }
338     // topdir ends with '/'
339 #ifndef Q_OS_WIN
340     if (realPath.startsWith(topdir)) {
341 #else
342     if (realPath.startsWith(topdir, Qt::CaseInsensitive)) {
343 #endif
344         const QString rel = realPath.mid(topdir.length());
345         Q_ASSERT(rel[0] != QLatin1Char('/'));
346         return rel;
347     } else { // shouldn't happen...
348         qCWarning(KIO_TRASH) << "Couldn't make relative path for" << realPath << "(" << path << "), with topdir=" << topdir;
349         return realPath;
350     }
351 }
352 
353 void TrashImpl::enterLoop()
354 {
355     QEventLoop eventLoop;
356     connect(this, &TrashImpl::leaveModality, &eventLoop, &QEventLoop::quit);
357     eventLoop.exec(QEventLoop::ExcludeUserInputEvents);
358 }
359 
360 QString TrashImpl::infoPath(int trashId, const QString &fileId) const
361 {
362     const QString trashPath = trashDirectoryPath(trashId) + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo");
363     return trashPath;
364 }
365 
366 QString TrashImpl::filesPath(int trashId, const QString &fileId) const
367 {
368     const QString trashPath = trashDirectoryPath(trashId) + QLatin1String("/files/") + fileId;
369     return trashPath;
370 }
371 
372 bool TrashImpl::deleteInfo(int trashId, const QString &fileId)
373 {
374 #ifdef Q_OS_OSX
375     createTrashInfrastructure(trashId);
376 #endif
377 
378     if (QFile::remove(infoPath(trashId, fileId))) {
379         fileRemoved();
380         return true;
381     }
382 
383     return false;
384 }
385 
386 bool TrashImpl::moveToTrash(const QString &origPath, int trashId, const QString &fileId)
387 {
388     // qCDebug(KIO_TRASH) << "Trashing" << origPath << trashId << fileId;
389     if (!adaptTrashSize(origPath, trashId)) {
390         return false;
391     }
392 
393 #ifdef Q_OS_OSX
394     createTrashInfrastructure(trashId);
395 #endif
396     const QString dest = filesPath(trashId, fileId);
397     if (!move(origPath, dest)) {
398         // Maybe the move failed due to no permissions to delete source.
399         // In that case, delete dest to keep things consistent, since KIO doesn't do it.
400         if (QFileInfo(dest).isFile()) {
401             QFile::remove(dest);
402         } else {
403             synchronousDel(dest, false, true);
404         }
405         return false;
406     }
407 
408     if (QFileInfo(dest).isDir()) {
409         TrashSizeCache trashSize(trashDirectoryPath(trashId));
410         const qint64 pathSize = DiscSpaceUtil::sizeOfPath(dest);
411         trashSize.add(fileId, pathSize);
412     }
413 
414     fileAdded();
415     return true;
416 }
417 
418 bool TrashImpl::moveFromTrash(const QString &dest, int trashId, const QString &fileId, const QString &relativePath)
419 {
420     QString src = filesPath(trashId, fileId);
421     if (!relativePath.isEmpty()) {
422         src += QLatin1Char('/') + relativePath;
423     }
424     if (!move(src, dest)) {
425         return false;
426     }
427 
428     TrashSizeCache trashSize(trashDirectoryPath(trashId));
429     trashSize.remove(fileId);
430 
431     return true;
432 }
433 
434 bool TrashImpl::move(const QString &src, const QString &dest)
435 {
436     if (directRename(src, dest)) {
437         // This notification is done by KIO::moveAs when using the code below
438         // But if we do a direct rename we need to do the notification ourselves
439         org::kde::KDirNotify::emitFilesAdded(QUrl::fromLocalFile(dest));
440         return true;
441     }
442     if (m_lastErrorCode != KIO::ERR_UNSUPPORTED_ACTION) {
443         return false;
444     }
445 
446     const auto urlSrc = QUrl::fromLocalFile(src);
447     const auto urlDest = QUrl::fromLocalFile(dest);
448 
449     // qCDebug(KIO_TRASH) << urlSrc << "->" << urlDest;
450     KIO::CopyJob *job = KIO::moveAs(urlSrc, urlDest, KIO::HideProgressInfo);
451     job->setUiDelegate(nullptr);
452     connect(job, &KJob::result, this, &TrashImpl::jobFinished);
453     enterLoop();
454 
455     return m_lastErrorCode == 0;
456 }
457 
458 void TrashImpl::jobFinished(KJob *job)
459 {
460     // qCDebug(KIO_TRASH) << "error=" << job->error() << job->errorText();
461     error(job->error(), job->errorText());
462 
463     Q_EMIT leaveModality();
464 }
465 
466 bool TrashImpl::copyToTrash(const QString &origPath, int trashId, const QString &fileId)
467 {
468     // qCDebug(KIO_TRASH);
469     if (!adaptTrashSize(origPath, trashId)) {
470         return false;
471     }
472 
473 #ifdef Q_OS_OSX
474     createTrashInfrastructure(trashId);
475 #endif
476     const QString dest = filesPath(trashId, fileId);
477     if (!copy(origPath, dest)) {
478         return false;
479     }
480 
481     if (QFileInfo(dest).isDir()) {
482         TrashSizeCache trashSize(trashDirectoryPath(trashId));
483         const qint64 pathSize = DiscSpaceUtil::sizeOfPath(dest);
484         trashSize.add(fileId, pathSize);
485     }
486 
487     fileAdded();
488     return true;
489 }
490 
491 bool TrashImpl::copyFromTrash(const QString &dest, int trashId, const QString &fileId, const QString &relativePath)
492 {
493     const QString src = physicalPath(trashId, fileId, relativePath);
494     return copy(src, dest);
495 }
496 
497 bool TrashImpl::copy(const QString &src, const QString &dest)
498 {
499     // kio_file's copy() method is quite complex (in order to be fast), let's just call it...
500     m_lastErrorCode = 0;
501     const auto urlSrc = QUrl::fromLocalFile(src);
502     const auto urlDest = QUrl::fromLocalFile(dest);
503     // qCDebug(KIO_TRASH) << "copying" << src << "to" << dest;
504     KIO::CopyJob *job = KIO::copyAs(urlSrc, urlDest, KIO::HideProgressInfo);
505     job->setUiDelegate(nullptr);
506     connect(job, &KJob::result, this, &TrashImpl::jobFinished);
507     enterLoop();
508 
509     return m_lastErrorCode == 0;
510 }
511 
512 bool TrashImpl::directRename(const QString &src, const QString &dest)
513 {
514     // qCDebug(KIO_TRASH) << src << "->" << dest;
515     // Do not use QFile::rename here, we need to be able to move broken symlinks too
516     // (and we need to make sure errno is set)
517     if (::rename(QFile::encodeName(src).constData(), QFile::encodeName(dest).constData()) != 0) {
518         if (errno == EXDEV) {
519             error(KIO::ERR_UNSUPPORTED_ACTION, QStringLiteral("rename"));
520         } else {
521             if ((errno == EACCES) || (errno == EPERM)) {
522                 error(KIO::ERR_ACCESS_DENIED, dest);
523             } else if (errno == EROFS) { // The file is on a read-only filesystem
524                 error(KIO::ERR_CANNOT_DELETE, src);
525             } else if (errno == ENOENT) {
526                 const QString marker(QStringLiteral("Trash/files/"));
527                 const int idx = src.lastIndexOf(marker) + marker.size();
528                 const QString displayName = QLatin1String("trash:/") + src.mid(idx);
529                 error(KIO::ERR_DOES_NOT_EXIST, displayName);
530             } else {
531                 error(KIO::ERR_CANNOT_RENAME, src);
532             }
533         }
534         return false;
535     }
536     return true;
537 }
538 
539 bool TrashImpl::moveInTrash(int trashId, const QString &oldFileId, const QString &newFileId)
540 {
541     m_lastErrorCode = 0;
542 
543     const QString oldInfo = infoPath(trashId, oldFileId);
544     const QString oldFile = filesPath(trashId, oldFileId);
545     const QString newInfo = infoPath(trashId, newFileId);
546     const QString newFile = filesPath(trashId, newFileId);
547 
548     if (directRename(oldInfo, newInfo)) {
549         if (directRename(oldFile, newFile)) {
550             // success
551 
552             if (QFileInfo(newFile).isDir()) {
553                 TrashSizeCache trashSize(trashDirectoryPath(trashId));
554                 trashSize.rename(oldFileId, newFileId);
555             }
556             return true;
557         } else {
558             // rollback
559             directRename(newInfo, oldInfo);
560         }
561     }
562     return false;
563 }
564 
565 bool TrashImpl::del(int trashId, const QString &fileId)
566 {
567 #ifdef Q_OS_OSX
568     createTrashInfrastructure(trashId);
569 #endif
570 
571     const QString info = infoPath(trashId, fileId);
572     const QString file = filesPath(trashId, fileId);
573 
574     QT_STATBUF buff;
575     if (QT_LSTAT(QFile::encodeName(info).constData(), &buff) == -1) {
576         if (errno == EACCES) {
577             error(KIO::ERR_ACCESS_DENIED, file);
578         } else {
579             error(KIO::ERR_DOES_NOT_EXIST, file);
580         }
581         return false;
582     }
583 
584     const bool isDir = QFileInfo(file).isDir();
585     if (!synchronousDel(file, true, isDir)) {
586         return false;
587     }
588 
589     if (isDir) {
590         TrashSizeCache trashSize(trashDirectoryPath(trashId));
591         trashSize.remove(fileId);
592     }
593 
594     QFile::remove(info);
595     fileRemoved();
596     return true;
597 }
598 
599 bool TrashImpl::synchronousDel(const QString &path, bool setLastErrorCode, bool isDir)
600 {
601     const int oldErrorCode = m_lastErrorCode;
602     const QString oldErrorMsg = m_lastErrorMessage;
603     const auto url = QUrl::fromLocalFile(path);
604     // First ensure that all dirs have u+w permissions,
605     // otherwise we won't be able to delete files in them (#130780).
606     if (isDir) {
607         //         qCDebug(KIO_TRASH) << "chmod'ing" << url;
608         KFileItem fileItem(url, QStringLiteral("inode/directory"), KFileItem::Unknown);
609         KFileItemList fileItemList;
610         fileItemList.append(fileItem);
611         KIO::ChmodJob *chmodJob = KIO::chmod(fileItemList, 0200, 0200, QString(), QString(), true /*recursive*/, KIO::HideProgressInfo);
612         connect(chmodJob, &KJob::result, this, &TrashImpl::jobFinished);
613         enterLoop();
614     }
615 
616     KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo);
617     connect(job, &KJob::result, this, &TrashImpl::jobFinished);
618     enterLoop();
619     bool ok = m_lastErrorCode == 0;
620     if (!setLastErrorCode) {
621         m_lastErrorCode = oldErrorCode;
622         m_lastErrorMessage = oldErrorMsg;
623     }
624     return ok;
625 }
626 
627 bool TrashImpl::emptyTrash()
628 {
629     // qCDebug(KIO_TRASH);
630     // The naive implementation "delete info and files in every trash directory"
631     // breaks when deleted directories contain files owned by other users.
632     // We need to ensure that the .trashinfo file is only removed when the
633     // corresponding files could indeed be removed (#116371)
634 
635     // On the other hand, we certainly want to remove any file that has no associated
636     // .trashinfo file for some reason (#167051)
637 
638     QSet<QString> unremovableFiles;
639 
640     int myErrorCode = 0;
641     QString myErrorMsg;
642     const TrashedFileInfoList fileInfoList = list();
643     for (const auto &info : fileInfoList) {
644         const QString filesPath = info.physicalPath;
645         if (synchronousDel(filesPath, true, true) || m_lastErrorCode == KIO::ERR_DOES_NOT_EXIST) {
646             QFile::remove(infoPath(info.trashId, info.fileId));
647         } else {
648             // error code is set by synchronousDel, let's remember it
649             // (so that successfully removing another file doesn't erase the error)
650             myErrorCode = m_lastErrorCode;
651             myErrorMsg = m_lastErrorMessage;
652             // and remember not to remove this file
653             unremovableFiles.insert(filesPath);
654             qCDebug(KIO_TRASH) << "Unremovable:" << filesPath;
655         }
656 
657         TrashSizeCache trashSize(trashDirectoryPath(info.trashId));
658         trashSize.clear();
659     }
660 
661     // Now do the orphaned-files cleanup
662     for (auto trit = m_trashDirectories.cbegin(); trit != m_trashDirectories.cend(); ++trit) {
663         // const int trashId = trit.key();
664         const QString filesDir = trit.value() + QLatin1String("/files");
665         const QStringList list = listDir(filesDir);
666         for (const QString &fileName : list) {
667             if (fileName == QLatin1Char('.') || fileName == QLatin1String("..")) {
668                 continue;
669             }
670             const QString filePath = filesDir + QLatin1Char('/') + fileName;
671             if (!unremovableFiles.contains(filePath)) {
672                 qCWarning(KIO_TRASH) << "Removing orphaned file" << filePath;
673                 QFile::remove(filePath);
674             }
675         }
676     }
677 
678     m_lastErrorCode = myErrorCode;
679     m_lastErrorMessage = myErrorMsg;
680 
681     fileRemoved();
682 
683     return m_lastErrorCode == 0;
684 }
685 
686 TrashImpl::TrashedFileInfoList TrashImpl::list()
687 {
688     // Here we scan for trash directories unconditionally. This allows
689     // noticing plugged-in [e.g. removable] devices, or new mounts etc.
690     scanTrashDirectories();
691 
692     TrashedFileInfoList lst;
693     // For each known trash directory...
694     for (auto it = m_trashDirectories.cbegin(); it != m_trashDirectories.cend(); ++it) {
695         const int trashId = it.key();
696         QString infoPath = it.value();
697         infoPath += QLatin1String("/info");
698         // Code taken from kio_file
699         const QStringList entryNames = listDir(infoPath);
700         // char path_buffer[PATH_MAX];
701         // getcwd(path_buffer, PATH_MAX - 1);
702         // if ( chdir( infoPathEnc ) )
703         //    continue;
704 
705         const QLatin1String tail(".trashinfo");
706         const int tailLength = tail.size();
707         for (const QString &fileName : entryNames) {
708             if (fileName == QLatin1Char('.') || fileName == QLatin1String("..")) {
709                 continue;
710             }
711             if (!fileName.endsWith(tail)) {
712                 qCWarning(KIO_TRASH) << "Invalid info file found in" << infoPath << ":" << fileName;
713                 continue;
714             }
715 
716             TrashedFileInfo info;
717             if (infoForFile(trashId, fileName.chopped(tailLength), info)) {
718                 lst << info;
719             }
720         }
721     }
722     return lst;
723 }
724 
725 // Returns the entries in a given directory - including "." and ".."
726 QStringList TrashImpl::listDir(const QString &physicalPath)
727 {
728     return QDir(physicalPath).entryList(QDir::Dirs | QDir::Files | QDir::Hidden | QDir::System);
729 }
730 
731 bool TrashImpl::infoForFile(int trashId, const QString &fileId, TrashedFileInfo &info)
732 {
733     // qCDebug(KIO_TRASH) << trashId << fileId;
734     info.trashId = trashId; // easy :)
735     info.fileId = fileId; // equally easy
736     info.physicalPath = filesPath(trashId, fileId);
737     return readInfoFile(infoPath(trashId, fileId), info, trashId);
738 }
739 
740 bool TrashImpl::trashSpaceInfo(const QString &path, TrashSpaceInfo &info)
741 {
742     const int trashId = findTrashDirectory(path);
743     if (trashId < 0) {
744         qCWarning(KIO_TRASH) << "No trash directory found! TrashImpl::findTrashDirectory returned" << trashId;
745         return false;
746     }
747 
748     const KConfig config(QStringLiteral("ktrashrc"));
749 
750     const QString trashPath = trashDirectoryPath(trashId);
751     const auto group = config.group(trashPath);
752 
753     const bool useSizeLimit = group.readEntry("UseSizeLimit", true);
754     const double percent = group.readEntry("Percent", 10.0);
755 
756     DiscSpaceUtil util(trashPath + QLatin1String("/files/"));
757     qint64 total = util.size();
758     if (useSizeLimit) {
759         total *= percent / 100.0;
760     }
761 
762     TrashSizeCache trashSize(trashPath);
763     const qint64 used = trashSize.calculateSize();
764 
765     info.totalSize = total;
766     info.availableSize = total - used;
767 
768     return true;
769 }
770 
771 bool TrashImpl::readInfoFile(const QString &infoPath, TrashedFileInfo &info, int trashId)
772 {
773     KConfig cfg(infoPath, KConfig::SimpleConfig);
774     if (!cfg.hasGroup("Trash Info")) {
775         error(KIO::ERR_CANNOT_OPEN_FOR_READING, infoPath);
776         return false;
777     }
778     const KConfigGroup group = cfg.group("Trash Info");
779     info.origPath = QUrl::fromPercentEncoding(group.readEntry("Path").toLatin1());
780     if (info.origPath.isEmpty()) {
781         return false; // path is mandatory...
782     }
783     if (trashId == 0) {
784         Q_ASSERT(info.origPath[0] == QLatin1Char('/'));
785     } else {
786         const QString topdir = topDirectoryPath(trashId); // includes trailing slash
787         info.origPath.prepend(topdir);
788     }
789     const QString line = group.readEntry("DeletionDate");
790     if (!line.isEmpty()) {
791         info.deletionDate = QDateTime::fromString(line, Qt::ISODate);
792     }
793     return true;
794 }
795 
796 QString TrashImpl::physicalPath(int trashId, const QString &fileId, const QString &relativePath)
797 {
798     QString filePath = filesPath(trashId, fileId);
799     if (!relativePath.isEmpty()) {
800         filePath += QLatin1Char('/') + relativePath;
801     }
802     return filePath;
803 }
804 
805 void TrashImpl::error(int e, const QString &s)
806 {
807     if (e) {
808         qCDebug(KIO_TRASH) << e << s;
809     }
810     m_lastErrorCode = e;
811     m_lastErrorMessage = s;
812 }
813 
814 bool TrashImpl::isEmpty() const
815 {
816     // For each known trash directory...
817     if (!m_trashDirectoriesScanned) {
818         scanTrashDirectories();
819     }
820 
821     for (auto it = m_trashDirectories.cbegin(); it != m_trashDirectories.cend(); ++it) {
822         const QString infoPath = it.value() + QLatin1String("/info");
823 
824         DIR *dp = ::opendir(QFile::encodeName(infoPath).constData());
825         if (dp) {
826             struct dirent *ep;
827             ep = readdir(dp);
828             ep = readdir(dp); // ignore '.' and '..' dirent
829             ep = readdir(dp); // look for third file
830             closedir(dp);
831             if (ep != nullptr) {
832                 // qCDebug(KIO_TRASH) << ep->d_name << "in" << infoPath << "-> not empty";
833                 return false; // not empty
834             }
835         }
836     }
837     return true;
838 }
839 
840 void TrashImpl::fileAdded()
841 {
842     m_config.reparseConfiguration();
843     KConfigGroup group = m_config.group("Status");
844     if (group.readEntry("Empty", true) == true) {
845         group.writeEntry("Empty", false);
846         m_config.sync();
847     }
848     // The apps showing the trash (e.g. kdesktop) will be notified
849     // of this change when KDirNotify::FilesAdded("trash:/") is emitted,
850     // which will be done by the job soon after this.
851 }
852 
853 void TrashImpl::fileRemoved()
854 {
855     if (isEmpty()) {
856         deleteEmptyTrashInfrastructure();
857         KConfigGroup group = m_config.group("Status");
858         group.writeEntry("Empty", true);
859         m_config.sync();
860         org::kde::KDirNotify::emitFilesChanged({QUrl::fromEncoded("trash:/")});
861     }
862     // The apps showing the trash (e.g. kdesktop) will be notified
863     // of this change when KDirNotify::FilesRemoved(...) is emitted,
864     // which will be done by the job soon after this.
865 }
866 
867 #ifdef Q_OS_OSX
868 #include <CoreFoundation/CoreFoundation.h>
869 #include <DiskArbitration/DiskArbitration.h>
870 #include <sys/mount.h>
871 
872 int TrashImpl::idForMountPoint(const QString &mountPoint) const
873 {
874     DADiskRef disk;
875     CFDictionaryRef descDict;
876     DASessionRef session = DASessionCreate(NULL);
877     int devId = -1;
878     if (session) {
879         QByteArray mp = QFile::encodeName(mountPoint);
880         struct statfs statFS;
881         statfs(mp.constData(), &statFS);
882         disk = DADiskCreateFromBSDName(kCFAllocatorDefault, session, statFS.f_mntfromname);
883         if (disk) {
884             descDict = DADiskCopyDescription(disk);
885             if (descDict) {
886                 CFNumberRef cfMajor = (CFNumberRef)CFDictionaryGetValue(descDict, kDADiskDescriptionMediaBSDMajorKey);
887                 CFNumberRef cfMinor = (CFNumberRef)CFDictionaryGetValue(descDict, kDADiskDescriptionMediaBSDMinorKey);
888                 int major, minor;
889                 if (CFNumberGetValue(cfMajor, kCFNumberIntType, &major) && CFNumberGetValue(cfMinor, kCFNumberIntType, &minor)) {
890                     qCWarning(KIO_TRASH) << "major=" << major << " minor=" << minor;
891                     devId = 1000 * major + minor;
892                 }
893                 CFRelease(cfMajor);
894                 CFRelease(cfMinor);
895             } else {
896                 qCWarning(KIO_TRASH) << "couldn't get DADiskCopyDescription from" << disk;
897             }
898             CFRelease(disk);
899         } else {
900             qCWarning(KIO_TRASH) << "DADiskCreateFromBSDName failed on statfs from" << mp;
901         }
902         CFRelease(session);
903     } else {
904         qCWarning(KIO_TRASH) << "couldn't create DASession";
905     }
906     return devId;
907 }
908 
909 #else
910 
911 int TrashImpl::idForDevice(const Solid::Device &device) const
912 {
913     const Solid::Block *block = device.as<Solid::Block>();
914     if (block) {
915         // qCDebug(KIO_TRASH) << "major=" << block->deviceMajor() << "minor=" << block->deviceMinor();
916         return block->deviceMajor() * 1000 + block->deviceMinor();
917     } else {
918         const Solid::NetworkShare *netshare = device.as<Solid::NetworkShare>();
919 
920         if (netshare) {
921             QString url = netshare->url().url();
922 
923             QLockFile configLock(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/trashrc.nextid.lock"));
924 
925             if (!configLock.lock()) {
926                 return -1;
927             }
928 
929             m_config.reparseConfiguration();
930             KConfigGroup group = m_config.group("NetworkShares");
931             int id = group.readEntry(url, -1);
932 
933             if (id == -1) {
934                 id = group.readEntry("NextID", 0);
935                 // qCDebug(KIO_TRASH) << "new share=" << url << " id=" << id;
936 
937                 group.writeEntry(url, id);
938                 group.writeEntry("NextID", id + 1);
939                 group.sync();
940             }
941 
942             return 6000000 + id;
943         }
944 
945         // Not a block device nor a network share
946         return -1;
947     }
948 }
949 
950 void TrashImpl::refreshDevices() const
951 {
952     // this is needed because Solid's fstab backend uses QSocketNotifier
953     // to get notifications about changes to mtab
954     // otherwise we risk getting old device list
955     qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
956 }
957 #endif
958 
959 void TrashImpl::insertTrashDir(int id, const QString &trashDir, const QString &topdir) const
960 {
961     m_trashDirectories.insert(id, trashDir);
962     qCDebug(KIO_TRASH) << "found" << trashDir << "gave it id" << id;
963     m_topDirectories.insert(id, !topdir.endsWith(QLatin1Char('/')) ? topdir + QLatin1Char('/') : topdir);
964 }
965 
966 int TrashImpl::findTrashDirectory(const QString &origPath)
967 {
968     // qCDebug(KIO_TRASH) << origPath;
969     // Check if it's on the same device as $HOME
970     QT_STATBUF buff;
971     if (QT_LSTAT(QFile::encodeName(origPath).constData(), &buff) == 0 && buff.st_dev == m_homeDevice) {
972         return 0;
973     }
974 
975     KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(origPath);
976     if (!mp) {
977         // qCDebug(KIO_TRASH) << "KMountPoint found no mount point for" << origPath;
978         return 0;
979     }
980 
981     QString mountPoint = mp->mountPoint();
982     const QString trashDir = trashForMountPoint(mountPoint, true);
983     // qCDebug(KIO_TRASH) << "mountPoint=" << mountPoint << "trashDir=" << trashDir;
984 
985 #ifndef Q_OS_OSX
986     if (trashDir.isEmpty()) {
987         return 0; // no trash available on partition
988     }
989 #endif
990 
991     int id = idForTrashDirectory(trashDir);
992     if (id > -1) {
993         qCDebug(KIO_TRASH) << "Found Trash dir" << trashDir << "with id" << id;
994         return id;
995     }
996 
997 #ifdef Q_OS_OSX
998     id = idForMountPoint(mountPoint);
999 #else
1000     refreshDevices();
1001     const QString query = QLatin1String("[StorageAccess.accessible == true AND StorageAccess.filePath == '%1']").arg(mountPoint);
1002     const QList<Solid::Device> lst = Solid::Device::listFromQuery(query);
1003     qCDebug(KIO_TRASH) << "Queried Solid with" << query << "got" << lst.count() << "devices";
1004     if (lst.isEmpty()) { // not a device. Maybe some tmpfs mount for instance.
1005         return 0;
1006     }
1007 
1008     // Pretend we got exactly one...
1009     const Solid::Device device = lst.at(0);
1010     id = idForDevice(device);
1011 #endif
1012     if (id == -1) {
1013         return 0;
1014     }
1015 
1016     // New trash dir found, register it
1017     insertTrashDir(id, trashDir, mountPoint);
1018     return id;
1019 }
1020 
1021 KIO::UDSEntry TrashImpl::trashUDSEntry(KIO::StatDetails details)
1022 {
1023     KIO::UDSEntry entry;
1024     if (details & KIO::StatRecursiveSize) {
1025         KIO::filesize_t size = 0;
1026         long latestModifiedDate = 0;
1027 
1028         for (const QString &trashPath : std::as_const(m_trashDirectories)) {
1029             TrashSizeCache trashSize(trashPath);
1030             TrashSizeCache::SizeAndModTime res = trashSize.calculateSizeAndLatestModDate();
1031             size += res.size;
1032 
1033             // Find latest modification date
1034             if (res.mtime > latestModifiedDate) {
1035                 latestModifiedDate = res.mtime;
1036             }
1037         }
1038 
1039         entry.reserve(3);
1040         entry.fastInsert(KIO::UDSEntry::UDS_RECURSIVE_SIZE, static_cast<long long>(size));
1041 
1042         entry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, latestModifiedDate / 1000);
1043         // access date is unreliable for the trash folder, use the modified date instead
1044         entry.fastInsert(KIO::UDSEntry::UDS_ACCESS_TIME, latestModifiedDate / 1000);
1045     }
1046     return entry;
1047 }
1048 
1049 void TrashImpl::scanTrashDirectories() const
1050 {
1051 #ifndef Q_OS_OSX
1052     refreshDevices();
1053 #endif
1054 
1055     const QList<Solid::Device> lst = Solid::Device::listFromQuery(QStringLiteral("StorageAccess.accessible == true"));
1056     for (const Solid::Device &device : lst) {
1057         QString topdir = device.as<Solid::StorageAccess>()->filePath();
1058         QString trashDir = trashForMountPoint(topdir, false);
1059         if (!trashDir.isEmpty()) {
1060             // OK, trashDir is a valid trash directory. Ensure it's registered.
1061             int trashId = idForTrashDirectory(trashDir);
1062             if (trashId == -1) {
1063                 // new trash dir found, register it
1064 #ifdef Q_OS_OSX
1065                 trashId = idForMountPoint(topdir);
1066 #else
1067                 trashId = idForDevice(device);
1068 #endif
1069                 if (trashId == -1) {
1070                     continue;
1071                 }
1072 
1073                 insertTrashDir(trashId, trashDir, topdir);
1074             }
1075         }
1076     }
1077     m_trashDirectoriesScanned = true;
1078 }
1079 
1080 TrashImpl::TrashDirMap TrashImpl::trashDirectories() const
1081 {
1082     if (!m_trashDirectoriesScanned) {
1083         scanTrashDirectories();
1084     }
1085     return m_trashDirectories;
1086 }
1087 
1088 TrashImpl::TrashDirMap TrashImpl::topDirectories() const
1089 {
1090     if (!m_trashDirectoriesScanned) {
1091         scanTrashDirectories();
1092     }
1093     return m_topDirectories;
1094 }
1095 
1096 QString TrashImpl::trashForMountPoint(const QString &topdir, bool createIfNeeded) const
1097 {
1098     // (1) Administrator-created $topdir/.Trash directory
1099 
1100 #ifndef Q_OS_OSX
1101     const QString rootTrashDir = topdir + QLatin1String("/.Trash");
1102 #else
1103     const QString rootTrashDir = topdir + QLatin1String("/.Trashes");
1104 #endif
1105     const QByteArray rootTrashDir_c = QFile::encodeName(rootTrashDir);
1106     // Can't use QFileInfo here since we need to test for the sticky bit
1107     uid_t uid = getuid();
1108     QT_STATBUF buff;
1109     const unsigned int requiredBits = S_ISVTX; // Sticky bit required
1110     if (QT_LSTAT(rootTrashDir_c.constData(), &buff) == 0) {
1111         if ((S_ISDIR(buff.st_mode)) // must be a dir
1112             && (!S_ISLNK(buff.st_mode)) // not a symlink
1113             && ((buff.st_mode & requiredBits) == requiredBits) //
1114             && (::access(rootTrashDir_c.constData(), W_OK) == 0) // must be user-writable
1115         ) {
1116             if (buff.st_dev == m_homeDevice) // bind mount, maybe
1117                 return QString();
1118 #ifndef Q_OS_OSX
1119             const QString trashDir = rootTrashDir + QLatin1Char('/') + QString::number(uid);
1120 #else
1121             QString trashDir = rootTrashDir + QLatin1Char('/') + QString::number(uid);
1122 #endif
1123             const QByteArray trashDir_c = QFile::encodeName(trashDir);
1124             if (QT_LSTAT(trashDir_c.constData(), &buff) == 0) {
1125                 if ((buff.st_uid == uid) // must be owned by user
1126                     && (S_ISDIR(buff.st_mode)) // must be a dir
1127                     && (!S_ISLNK(buff.st_mode)) // not a symlink
1128                     && (buff.st_mode & 0777) == 0700) { // rwx for user
1129 #ifdef Q_OS_OSX
1130                     trashDir += QStringLiteral("/KDE.trash");
1131 #endif
1132                     return trashDir;
1133                 }
1134                 qCWarning(KIO_TRASH) << "Directory" << trashDir << "exists but didn't pass the security checks, can't use it";
1135             } else if (createIfNeeded && initTrashDirectory(trashDir_c)) {
1136                 return trashDir;
1137             }
1138         } else {
1139             qCWarning(KIO_TRASH) << "Root trash dir" << rootTrashDir << "exists but didn't pass the security checks, can't use it";
1140         }
1141     }
1142 
1143 #ifndef Q_OS_OSX
1144     // (2) $topdir/.Trash-$uid
1145     const QString trashDir = topdir + QLatin1String("/.Trash-") + QString::number(uid);
1146     const QByteArray trashDir_c = QFile::encodeName(trashDir);
1147     if (QT_LSTAT(trashDir_c.constData(), &buff) == 0) {
1148         if ((buff.st_uid == uid) // must be owned by user
1149             && S_ISDIR(buff.st_mode) // must be a dir
1150             && !S_ISLNK(buff.st_mode) // not a symlink
1151             && ((buff.st_mode & 0700) == 0700)) { // and we need write access to it
1152 
1153             if (buff.st_dev == m_homeDevice) // bind mount, maybe
1154                 return QString();
1155             if (checkTrashSubdirs(trashDir_c)) {
1156                 return trashDir;
1157             }
1158         }
1159         qCWarning(KIO_TRASH) << "Directory" << trashDir << "exists but didn't pass the security checks, can't use it";
1160         // Exists, but not usable
1161         return QString();
1162     }
1163     if (createIfNeeded && initTrashDirectory(trashDir_c)) {
1164         return trashDir;
1165     }
1166 #endif
1167     return QString();
1168 }
1169 
1170 int TrashImpl::idForTrashDirectory(const QString &trashDir) const
1171 {
1172     // If this is too slow we can always use a reverse map...
1173     for (auto it = m_trashDirectories.cbegin(); it != m_trashDirectories.cend(); ++it) {
1174         if (it.value() == trashDir) {
1175             return it.key();
1176         }
1177     }
1178     return -1;
1179 }
1180 
1181 bool TrashImpl::initTrashDirectory(const QByteArray &trashDir_c) const
1182 {
1183     if (mkdir(trashDir_c.constData(), 0700) != 0) {
1184         return false;
1185     }
1186     return checkTrashSubdirs(trashDir_c);
1187 }
1188 
1189 bool TrashImpl::checkTrashSubdirs(const QByteArray &trashDir_c) const
1190 {
1191     const QString trashDir = QFile::decodeName(trashDir_c);
1192     const QString info = trashDir + QLatin1String("/info");
1193     const QString files = trashDir + QLatin1String("/files");
1194     return testDir(info) == 0 && testDir(files) == 0;
1195 }
1196 
1197 QString TrashImpl::trashDirectoryPath(int trashId) const
1198 {
1199     // Never scanned for trash dirs? (This can happen after killing kio_trash
1200     // and reusing a directory listing from the earlier instance.)
1201     if (!m_trashDirectoriesScanned) {
1202         scanTrashDirectories();
1203     }
1204     Q_ASSERT(m_trashDirectories.contains(trashId));
1205     return m_trashDirectories[trashId];
1206 }
1207 
1208 QString TrashImpl::topDirectoryPath(int trashId) const
1209 {
1210     if (!m_trashDirectoriesScanned) {
1211         scanTrashDirectories();
1212     }
1213     assert(trashId != 0);
1214     Q_ASSERT(m_topDirectories.contains(trashId));
1215     return m_topDirectories[trashId];
1216 }
1217 
1218 // Helper method. Creates a URL with the format trash:/trashid-fileid or
1219 // trash:/trashid-fileid/relativePath/To/File for a file inside a trashed directory.
1220 QUrl TrashImpl::makeURL(int trashId, const QString &fileId, const QString &relativePath)
1221 {
1222     QUrl url;
1223     url.setScheme(QStringLiteral("trash"));
1224     QString path = QLatin1Char('/') + QString::number(trashId) + QLatin1Char('-') + fileId;
1225     if (!relativePath.isEmpty()) {
1226         path += QLatin1Char('/') + relativePath;
1227     }
1228     url.setPath(path);
1229     return url;
1230 }
1231 
1232 // Helper method. Parses a trash URL with the URL scheme defined in makeURL.
1233 // The trash:/ URL itself isn't parsed here, must be caught by the caller before hand.
1234 bool TrashImpl::parseURL(const QUrl &url, int &trashId, QString &fileId, QString &relativePath)
1235 {
1236     if (url.scheme() != QLatin1String("trash")) {
1237         return false;
1238     }
1239     const QString path = url.path();
1240     if (path.isEmpty()) {
1241         return false;
1242     }
1243     int start = 0;
1244     if (path[0] == QLatin1Char('/')) { // always true I hope
1245         start = 1;
1246     }
1247     int slashPos = path.indexOf(QLatin1Char('-'), 0); // don't match leading slash
1248     if (slashPos <= 0) {
1249         return false;
1250     }
1251     bool ok = false;
1252 
1253     // QStringView::toInt() implementation in Qt5 uses toString(), which
1254     // defeats the point of "not-allocating"
1255 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
1256     trashId = QStringView(path).mid(start, slashPos - start).toInt(&ok);
1257 #else
1258     trashId = path.midRef(start, slashPos - start).toInt(&ok);
1259 #endif
1260 
1261     Q_ASSERT(ok);
1262     if (!ok) {
1263         return false;
1264     }
1265     start = slashPos + 1;
1266     slashPos = path.indexOf(QLatin1Char('/'), start);
1267     if (slashPos <= 0) {
1268         fileId = path.mid(start);
1269         relativePath.clear();
1270         return true;
1271     }
1272     fileId = path.mid(start, slashPos - start);
1273     relativePath = path.mid(slashPos + 1);
1274     return true;
1275 }
1276 
1277 bool TrashImpl::adaptTrashSize(const QString &origPath, int trashId)
1278 {
1279     KConfig config(QStringLiteral("ktrashrc"));
1280 
1281     const QString trashPath = trashDirectoryPath(trashId);
1282     KConfigGroup group = config.group(trashPath);
1283 
1284     const bool useTimeLimit = group.readEntry("UseTimeLimit", false);
1285     const bool useSizeLimit = group.readEntry("UseSizeLimit", true);
1286     const double percent = group.readEntry("Percent", 10.0);
1287     const int actionType = group.readEntry("LimitReachedAction", 0);
1288 
1289     if (useTimeLimit) { // delete all files in trash older than X days
1290         const int maxDays = group.readEntry("Days", 7);
1291         const QDateTime currentDate = QDateTime::currentDateTime();
1292 
1293         const TrashedFileInfoList trashedFiles = list();
1294         for (const auto &info : trashedFiles) {
1295             if (info.trashId != trashId) {
1296                 continue;
1297             }
1298 
1299             if (info.deletionDate.daysTo(currentDate) > maxDays) {
1300                 del(info.trashId, info.fileId);
1301             }
1302         }
1303     }
1304 
1305     if (!useSizeLimit) { // check if size limit exceeded
1306         return true;
1307     }
1308 
1309     // calculate size of the files to be put into the trash
1310     const qint64 additionalSize = DiscSpaceUtil::sizeOfPath(origPath);
1311 
1312 #ifdef Q_OS_OSX
1313     createTrashInfrastructure(trashId);
1314 #endif
1315     TrashSizeCache trashSize(trashPath);
1316     DiscSpaceUtil util(trashPath + QLatin1String("/files/"));
1317     if (util.usage(trashSize.calculateSize() + additionalSize) >= percent) {
1318         // before we start to remove any files from the trash,
1319         // check whether the new file will fit into the trash
1320         // at all...
1321         const qint64 partitionSize = util.size();
1322 
1323         if (((static_cast<double>(additionalSize) / static_cast<double>(partitionSize)) * 100) >= percent) {
1324             m_lastErrorCode = KIO::ERR_SLAVE_DEFINED;
1325             m_lastErrorMessage = i18n("The file is too large to be trashed.");
1326             return false;
1327         }
1328 
1329         if (actionType == 0) { // warn the user only
1330             m_lastErrorCode = KIO::ERR_SLAVE_DEFINED;
1331             m_lastErrorMessage = i18n("The trash is full. Empty it or remove items manually.");
1332             return false;
1333         }
1334 
1335         // Start removing some other files from the trash
1336 
1337         QDir::SortFlags sortFlags;
1338         if (actionType == 1) {
1339             sortFlags = QDir::Time | QDir::Reversed; // Delete oldest files first
1340         } else if (actionType == 2) {
1341             sortFlags = QDir::Size; // Delete biggest files first
1342         } else {
1343             qWarning() << "Called with actionType" << actionType << ", which theoretically should never happen!";
1344             return false; // Bail out
1345         }
1346 
1347         constexpr QDir::Filters dirFilters = QDir::Files | QDir::AllDirs | QDir::NoDotAndDotDot;
1348         const QFileInfoList infoList = QDir(trashPath + QLatin1String("/files")).entryInfoList(dirFilters, sortFlags);
1349         for (const auto &info : infoList) {
1350             del(trashId, info.fileName()); // delete trashed file
1351 
1352             TrashSizeCache trashSize(trashPath);
1353             if (util.usage(trashSize.calculateSize() + additionalSize) < percent) { // check whether we have enough space now
1354                 return true;
1355             }
1356         }
1357     }
1358 
1359     return true;
1360 }
1361 
1362 #include "moc_trashimpl.cpp"
1363