File indexing completed on 2024-05-12 05:46:49

0001 /*
0002    This file is part of the KDE libraries
0003    Copyright (C) 2000 David Smith <dsmith@algonet.se>
0004    Copyright (C) 2004 Scott Wheeler <wheeler@kde.org>
0005 
0006    This class was inspired by a previous KUrlCompletion by
0007    Henner Zeller <zeller@think.de>
0008 
0009    This library is free software; you can redistribute it and/or
0010    modify it under the terms of the GNU Library General Public
0011    License as published by the Free Software Foundation; either
0012    version 2 of the License, or (at your option) any later version.
0013 
0014    This library is distributed in the hope that it will be useful,
0015    but WITHOUT ANY WARRANTY; without even the implied warranty of
0016    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
0017    Library General Public License for more details.
0018 
0019    You should have received a copy of the GNU Library General Public License
0020    along with this library; see the file COPYING.LIB.   If not, write to
0021    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
0022    Boston, MA 02110-1301, USA.
0023 */
0024 
0025 #include "kurlcompletion.h"
0026 #include "../pathhelpers_p.h"
0027 
0028 #include <stdlib.h>
0029 #include <assert.h>
0030 #include <limits.h>
0031 
0032 #include <QMutex>
0033 #include <QRegExp>
0034 #include <QDir>
0035 #include <QDirIterator>
0036 #include <QFile>
0037 #include <QThread>
0038 #include <QUrl>
0039 #include <QDebug>
0040 #include <QMimeDatabase>
0041 #include <QProcessEnvironment>
0042 #include <qplatformdefs.h> // QT_LSTAT, QT_STAT, QT_STATBUF
0043 
0044 #include <kurlauthorized.h>
0045 #include <kio/job.h>
0046 #include <kprotocolmanager.h>
0047 #include <kconfig.h>
0048 #include <ksharedconfig.h>
0049 #include <kcompletion.h>
0050 #include <kioglobal_p.h>
0051 #include <KUser>
0052 #include <kio_widgets_debug.h>
0053 
0054 #include <time.h>
0055 #include <kconfiggroup.h>
0056 
0057 #ifdef Q_OS_WIN
0058 #include <qt_windows.h>
0059 #else
0060 #include <sys/param.h>
0061 #include <pwd.h>
0062 #endif
0063 
0064 static bool expandTilde(QString &);
0065 static bool expandEnv(QString &);
0066 
0067 static QString unescape(const QString &text);
0068 
0069 // Permission mask for files that are executable by
0070 // user, group or other
0071 #define MODE_EXE (S_IXUSR | S_IXGRP | S_IXOTH)
0072 
0073 // Constants for types of completion
0074 enum ComplType {CTNone = 0, CTEnv, CTUser, CTMan, CTExe, CTFile, CTUrl, CTInfo};
0075 
0076 class CompletionThread;
0077 
0078 // Ensure that we don't end up with "//".
0079 static QUrl addPathToUrl(const QUrl &url, const QString &relPath)
0080 {
0081     QUrl u(url);
0082     u.setPath(concatPaths(url.path(), relPath));
0083     return u;
0084 }
0085 
0086 
0087 static QBasicAtomicInt s_waitDuration = Q_BASIC_ATOMIC_INITIALIZER(-1);
0088 
0089 static int initialWaitDuration()
0090 {
0091 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
0092     if (s_waitDuration.load() == -1) {
0093 #else
0094     if (s_waitDuration.loadRelaxed() == -1) {
0095 #endif
0096         const QByteArray envVar = qgetenv("KURLCOMPLETION_WAIT");
0097         if (envVar.isEmpty()) {
0098             s_waitDuration = 200; // default: 200 ms
0099         } else {
0100             s_waitDuration = envVar.toInt();
0101         }
0102     }
0103     return s_waitDuration;
0104 }
0105 
0106 ///////////////////////////////////////////////////////
0107 ///////////////////////////////////////////////////////
0108 // KUrlCompletionPrivate
0109 //
0110 class KUrlCompletionPrivate
0111 {
0112 public:
0113     explicit KUrlCompletionPrivate(KUrlCompletion *parent)
0114         : q(parent),
0115           url_auto_completion(true),
0116           userListThread(nullptr),
0117           dirListThread(nullptr)
0118     {
0119     }
0120 
0121     ~KUrlCompletionPrivate();
0122 
0123     void _k_slotEntries(KIO::Job *, const KIO::UDSEntryList &);
0124     void _k_slotIOFinished(KJob *);
0125     void slotCompletionThreadDone(QThread *thread, const QStringList &matches);
0126 
0127     class MyURL;
0128     bool userCompletion(const MyURL &url, QString *match);
0129     bool envCompletion(const MyURL &url, QString *match);
0130     bool exeCompletion(const MyURL &url, QString *match);
0131     bool fileCompletion(const MyURL &url, QString *match);
0132     bool urlCompletion(const MyURL &url, QString *match);
0133 
0134     bool isAutoCompletion();
0135 
0136     // List the next dir in m_dirs
0137     QString listDirectories(const QStringList &,
0138                             const QString &,
0139                             bool only_exe = false,
0140                             bool only_dir = false,
0141                             bool no_hidden = false,
0142                             bool stat_files = true);
0143 
0144     void listUrls(const QList<QUrl> &urls,
0145                   const QString &filter = QString(),
0146                   bool only_exe = false,
0147                   bool no_hidden = false);
0148 
0149     void addMatches(const QStringList &);
0150     QString finished();
0151 
0152     void init();
0153 
0154     void setListedUrl(ComplType compl_type,
0155                       const QString &dir = QString(),
0156                       const QString &filter = QString(),
0157                       bool no_hidden = false);
0158 
0159     bool isListedUrl(ComplType compl_type,
0160                      const QString &dir = QString(),
0161                      const QString &filter = QString(),
0162                      bool no_hidden = false);
0163 
0164     KUrlCompletion * const q;
0165     QList<QUrl> list_urls;
0166 
0167     bool onlyLocalProto;
0168 
0169     // urlCompletion() in Auto/Popup mode?
0170     bool url_auto_completion;
0171 
0172     // Append '/' to directories in Popup mode?
0173     // Doing that stat's all files and is slower
0174     bool popup_append_slash;
0175 
0176     // Keep track of currently listed files to avoid reading them again
0177     bool last_no_hidden;
0178     QString last_path_listed;
0179     QString last_file_listed;
0180     QString last_prepend;
0181     ComplType last_compl_type;
0182 
0183     QUrl cwd; // "current directory" = base dir for completion
0184 
0185     KUrlCompletion::Mode mode; // ExeCompletion, FileCompletion, DirCompletion
0186     bool replace_env;
0187     bool replace_home;
0188     bool complete_url; // if true completing a URL (i.e. 'prepend' is a URL), otherwise a path
0189 
0190     KIO::ListJob *list_job; // kio job to list directories
0191 
0192     QString prepend; // text to prepend to listed items
0193     QString compl_text; // text to pass on to KCompletion
0194 
0195     // Filters for files read with  kio
0196     bool list_urls_only_exe; // true = only list executables
0197     bool list_urls_no_hidden;
0198     QString list_urls_filter; // filter for listed files
0199 
0200     CompletionThread *userListThread;
0201     CompletionThread *dirListThread;
0202 
0203     QStringList mimeTypeFilters;
0204 };
0205 
0206 class CompletionThread : public QThread
0207 {
0208     Q_OBJECT
0209 protected:
0210     CompletionThread(KUrlCompletionPrivate *receiver) :
0211         QThread(),
0212         m_prepend(receiver->prepend),
0213         m_complete_url(receiver->complete_url),
0214         m_terminationRequested(false)
0215     {}
0216 
0217 public:
0218     void requestTermination()
0219     {
0220         if (!isFinished()) {
0221             qCDebug(KIO_WIDGETS) << "stopping thread" << this;
0222         }
0223 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
0224         m_terminationRequested.store(true);
0225 #else
0226         m_terminationRequested.storeRelaxed(true);
0227 #endif
0228         wait();
0229     }
0230 
0231     QStringList matches() const
0232     {
0233         QMutexLocker locker(&m_mutex);
0234         return m_matches;
0235     }
0236 
0237 Q_SIGNALS:
0238     void completionThreadDone(QThread *thread, const QStringList &matches);
0239 
0240 protected:
0241     void addMatch(const QString &match)
0242     {
0243         QMutexLocker locker(&m_mutex);
0244         m_matches.append(match);
0245     }
0246     bool terminationRequested() const
0247     {
0248 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
0249         return m_terminationRequested.load();
0250 #else
0251         return m_terminationRequested.loadRelaxed();
0252 #endif
0253     }
0254     void done()
0255     {
0256         if (!terminationRequested()) {
0257             qCDebug(KIO_WIDGETS) << "done, emitting signal with" << m_matches.count() << "matches";
0258             emit completionThreadDone(this, m_matches);
0259         }
0260     }
0261 
0262     const QString m_prepend;
0263     const bool m_complete_url; // if true completing a URL (i.e. 'm_prepend' is a URL), otherwise a path
0264 
0265 private:
0266     mutable QMutex m_mutex; // protects m_matches
0267     QStringList m_matches; // written by secondary thread, read by the matches() method
0268     QAtomicInt m_terminationRequested; // used as a bool
0269 };
0270 
0271 /**
0272  * A simple thread that fetches a list of tilde-completions and returns this
0273  * to the caller via the completionThreadDone signal.
0274  */
0275 
0276 class UserListThread : public CompletionThread
0277 {
0278     Q_OBJECT
0279 public:
0280     UserListThread(KUrlCompletionPrivate *receiver) :
0281         CompletionThread(receiver)
0282     {}
0283 
0284 protected:
0285     void run() override
0286     {
0287 #ifndef Q_OS_ANDROID
0288         const QChar tilde = QLatin1Char('~');
0289 
0290         // we don't need to handle prepend here, right? ~user is always at pos 0
0291         assert(m_prepend.isEmpty());
0292 #pragma message("TODO: add KUser::allUserNames() with a std::function<bool()> shouldTerminate parameter")
0293 #ifndef Q_OS_WIN
0294         struct passwd *pw;
0295         ::setpwent();
0296         while ((pw = ::getpwent()) && !terminationRequested()) {
0297             addMatch(tilde + QString::fromLocal8Bit(pw->pw_name));
0298         }
0299         ::endpwent();
0300 #else
0301         //currently terminationRequested is ignored on Windows
0302         const QStringList allUsers = KUser::allUserNames();
0303         for (const QString& s : allUsers) {
0304             addMatch(tilde + s);
0305         }
0306 #endif
0307         addMatch(QString(tilde));
0308 #endif
0309         done();
0310     }
0311 };
0312 
0313 class DirectoryListThread : public CompletionThread
0314 {
0315     Q_OBJECT
0316 public:
0317     DirectoryListThread(KUrlCompletionPrivate *receiver,
0318                         const QStringList &dirList,
0319                         const QString &filter,
0320                         const QStringList &mimeTypeFilters,
0321                         bool onlyExe,
0322                         bool onlyDir,
0323                         bool noHidden,
0324                         bool appendSlashToDir) :
0325         CompletionThread(receiver),
0326         m_dirList(dirList),
0327         m_filter(filter),
0328         m_mimeTypeFilters(mimeTypeFilters),
0329         m_onlyExe(onlyExe),
0330         m_onlyDir(onlyDir),
0331         m_noHidden(noHidden),
0332         m_appendSlashToDir(appendSlashToDir)
0333     {}
0334 
0335     void run() override;
0336 
0337 private:
0338     QStringList m_dirList;
0339     QString m_filter;
0340     QStringList m_mimeTypeFilters;
0341     bool m_onlyExe;
0342     bool m_onlyDir;
0343     bool m_noHidden;
0344     bool m_appendSlashToDir;
0345 };
0346 
0347 void DirectoryListThread::run()
0348 {
0349     //qDebug() << "Entered DirectoryListThread::run(), m_filter=" << m_filter << ", m_onlyExe=" << m_onlyExe << ", m_onlyDir=" << m_onlyDir << ", m_appendSlashToDir=" << m_appendSlashToDir << ", m_dirList.size()=" << m_dirList.size();
0350 
0351     QDir::Filters iterator_filter = (m_noHidden ? QDir::Filter(0) : QDir::Hidden) | QDir::Readable | QDir::NoDotAndDotDot;
0352     if (m_onlyExe) {
0353         iterator_filter |= (QDir::Dirs | QDir::Files | QDir::Executable);
0354     } else if (m_onlyDir) {
0355         iterator_filter |= QDir::Dirs;
0356     } else {
0357         iterator_filter |= (QDir::Dirs | QDir::Files);
0358     }
0359 
0360     QMimeDatabase mimeTypes;
0361 
0362     const QStringList::const_iterator end = m_dirList.constEnd();
0363     for (QStringList::const_iterator it = m_dirList.constBegin();
0364             it != end && !terminationRequested();
0365             ++it) {
0366         //qDebug() << "Scanning directory" << *it;
0367 
0368         QDirIterator current_dir_iterator(*it, iterator_filter);
0369 
0370         while (current_dir_iterator.hasNext() && !terminationRequested()) {
0371             current_dir_iterator.next();
0372 
0373             QFileInfo file_info = current_dir_iterator.fileInfo();
0374             const QString file_name = file_info.fileName();
0375 
0376             //qDebug() << "Found" << file_name;
0377 
0378             if (!m_filter.isEmpty() && !file_name.startsWith(m_filter)) {
0379                 continue;
0380             }
0381 
0382             if (!m_mimeTypeFilters.isEmpty() && !file_info.isDir()) {
0383                 auto mimeType = mimeTypes.mimeTypeForFile(file_info);
0384                 if (!m_mimeTypeFilters.contains(mimeType.name())) {
0385                     continue;
0386                 }
0387             }
0388 
0389             QString toAppend = file_name;
0390             // Add '/' to directories
0391             if (m_appendSlashToDir && file_info.isDir()) {
0392                 toAppend.append(QLatin1Char('/'));
0393             }
0394 
0395             if (m_complete_url) {
0396                 QUrl info(m_prepend);
0397                 info = addPathToUrl(info, toAppend);
0398                 addMatch(info.toDisplayString());
0399             } else {
0400                 addMatch(m_prepend + toAppend);
0401             }
0402         }
0403     }
0404 
0405     done();
0406 }
0407 
0408 KUrlCompletionPrivate::~KUrlCompletionPrivate()
0409 {
0410 }
0411 
0412 ///////////////////////////////////////////////////////
0413 ///////////////////////////////////////////////////////
0414 // MyURL - wrapper for QUrl with some different functionality
0415 //
0416 
0417 class KUrlCompletionPrivate::MyURL
0418 {
0419 public:
0420     MyURL(const QString &url, const QUrl &cwd);
0421     MyURL(const MyURL &url);
0422     ~MyURL();
0423 
0424     QUrl kurl() const
0425     {
0426         return m_kurl;
0427     }
0428 
0429     bool isLocalFile() const
0430     {
0431         return m_kurl.isLocalFile();
0432     }
0433     QString scheme() const
0434     {
0435         return m_kurl.scheme();
0436     }
0437     // The directory with a trailing '/'
0438     QString dir() const
0439     {
0440         return m_kurl.adjusted(QUrl::RemoveFilename).path();
0441     }
0442     QString file() const
0443     {
0444         return m_kurl.fileName();
0445     }
0446 
0447     // The initial, unparsed, url, as a string.
0448     QString url() const
0449     {
0450         return m_url;
0451     }
0452 
0453     // Is the initial string a URL, or just a path (whether absolute or relative)
0454     bool isURL() const
0455     {
0456         return m_isURL;
0457     }
0458 
0459     void filter(bool replace_user_dir, bool replace_env);
0460 
0461 private:
0462     void init(const QString &url, const QUrl &cwd);
0463 
0464     QUrl m_kurl;
0465     QString m_url;
0466     bool m_isURL;
0467 };
0468 
0469 KUrlCompletionPrivate::MyURL::MyURL(const QString &_url, const QUrl &cwd)
0470 {
0471     init(_url, cwd);
0472 }
0473 
0474 KUrlCompletionPrivate::MyURL::MyURL(const MyURL &_url)
0475     : m_kurl(_url.m_kurl)
0476 {
0477     m_url = _url.m_url;
0478     m_isURL = _url.m_isURL;
0479 }
0480 
0481 void KUrlCompletionPrivate::MyURL::init(const QString &_url, const QUrl &cwd)
0482 {
0483     // Save the original text
0484     m_url = _url;
0485 
0486     // Non-const copy
0487     QString url_copy = _url;
0488 
0489     // Special shortcuts for "man:" and "info:"
0490     if (url_copy.startsWith(QLatin1Char('#'))) {
0491         if (url_copy.length() > 1 && url_copy.at(1) == QLatin1Char('#')) {
0492             url_copy.replace(0, 2, QStringLiteral("info:"));
0493         } else {
0494             url_copy.replace(0, 1, QStringLiteral("man:"));
0495         }
0496     }
0497 
0498     // Look for a protocol in 'url'
0499     QRegExp protocol_regex = QRegExp(QStringLiteral("^(?![A-Za-z]:)[^/\\s\\\\]*:"));
0500 
0501     // Assume "file:" or whatever is given by 'cwd' if there is
0502     // no protocol.  (QUrl does this only for absolute paths)
0503     if (protocol_regex.indexIn(url_copy) == 0) {
0504         m_kurl = QUrl(url_copy);
0505         m_isURL = true;
0506     } else { // relative path or ~ or $something
0507         m_isURL = false;
0508         if (!QDir::isRelativePath(url_copy) ||
0509                 url_copy.startsWith(QLatin1Char('~')) ||
0510                 url_copy.startsWith(QLatin1Char('$'))) {
0511             m_kurl = QUrl::fromLocalFile(url_copy);
0512         } else {
0513             // Relative path
0514             if (cwd.isEmpty()) {
0515                 m_kurl = QUrl(url_copy);
0516             } else {
0517                 m_kurl = cwd;
0518                 m_kurl.setPath(concatPaths(m_kurl.path(), url_copy));
0519             }
0520         }
0521     }
0522 }
0523 
0524 KUrlCompletionPrivate::MyURL::~MyURL()
0525 {
0526 }
0527 
0528 void KUrlCompletionPrivate::MyURL::filter(bool replace_user_dir, bool replace_env)
0529 {
0530     QString d = dir() + file();
0531     if (replace_user_dir) {
0532         expandTilde(d);
0533     }
0534     if (replace_env) {
0535         expandEnv(d);
0536     }
0537     m_kurl.setPath(d);
0538 }
0539 
0540 ///////////////////////////////////////////////////////
0541 ///////////////////////////////////////////////////////
0542 // KUrlCompletion
0543 //
0544 
0545 KUrlCompletion::KUrlCompletion() : KCompletion(), d(new KUrlCompletionPrivate(this))
0546 {
0547     d->init();
0548 }
0549 
0550 KUrlCompletion::KUrlCompletion(Mode _mode)
0551     : KCompletion(),
0552       d(new KUrlCompletionPrivate(this))
0553 {
0554     d->init();
0555     setMode(_mode);
0556 }
0557 
0558 KUrlCompletion::~KUrlCompletion()
0559 {
0560     stop();
0561     delete d;
0562 }
0563 
0564 void KUrlCompletionPrivate::init()
0565 {
0566     cwd = QUrl::fromLocalFile(QDir::homePath());
0567 
0568     replace_home = true;
0569     replace_env = true;
0570     last_no_hidden = false;
0571     last_compl_type = CTNone;
0572     list_job = nullptr;
0573     mode = KUrlCompletion::FileCompletion;
0574 
0575     // Read settings
0576     KConfigGroup cg(KSharedConfig::openConfig(), "URLCompletion");
0577 
0578     url_auto_completion = cg.readEntry("alwaysAutoComplete", true);
0579     popup_append_slash = cg.readEntry("popupAppendSlash", true);
0580     onlyLocalProto = cg.readEntry("LocalProtocolsOnly", false);
0581 
0582     q->setIgnoreCase(true);
0583 }
0584 
0585 void KUrlCompletion::setDir(const QUrl &dir)
0586 {
0587     d->cwd = dir;
0588 }
0589 
0590 QUrl KUrlCompletion::dir() const
0591 {
0592     return d->cwd;
0593 }
0594 
0595 KUrlCompletion::Mode KUrlCompletion::mode() const
0596 {
0597     return d->mode;
0598 }
0599 
0600 void KUrlCompletion::setMode(Mode _mode)
0601 {
0602     d->mode = _mode;
0603 }
0604 
0605 bool KUrlCompletion::replaceEnv() const
0606 {
0607     return d->replace_env;
0608 }
0609 
0610 void KUrlCompletion::setReplaceEnv(bool replace)
0611 {
0612     d->replace_env = replace;
0613 }
0614 
0615 bool KUrlCompletion::replaceHome() const
0616 {
0617     return d->replace_home;
0618 }
0619 
0620 void KUrlCompletion::setReplaceHome(bool replace)
0621 {
0622     d->replace_home = replace;
0623 }
0624 
0625 /*
0626  * makeCompletion()
0627  *
0628  * Entry point for file name completion
0629  */
0630 QString KUrlCompletion::makeCompletion(const QString &text)
0631 {
0632     qCDebug(KIO_WIDGETS) << text << "d->cwd=" << d->cwd;
0633 
0634     KUrlCompletionPrivate::MyURL url(text, d->cwd);
0635 
0636     d->compl_text = text;
0637 
0638     // Set d->prepend to the original URL, with the filename [and ref/query] stripped.
0639     // This is what gets prepended to the directory-listing matches.
0640     if (url.isURL()) {
0641         QUrl directoryUrl(url.kurl());
0642         directoryUrl.setQuery(QString());
0643         directoryUrl.setFragment(QString());
0644         directoryUrl.setPath(url.dir());
0645         d->prepend = directoryUrl.toString();
0646     } else {
0647         d->prepend = text.left(text.length() - url.file().length());
0648     }
0649 
0650     d->complete_url = url.isURL();
0651 
0652     QString aMatch;
0653 
0654     // Environment variables
0655     //
0656     if (d->replace_env && d->envCompletion(url, &aMatch)) {
0657         return aMatch;
0658     }
0659 
0660     // User directories
0661     //
0662     if (d->replace_home && d->userCompletion(url, &aMatch)) {
0663         return aMatch;
0664     }
0665 
0666     // Replace user directories and variables
0667     url.filter(d->replace_home, d->replace_env);
0668 
0669     //qDebug() << "Filtered: proto=" << url.scheme()
0670     //          << ", dir=" << url.dir()
0671     //          << ", file=" << url.file()
0672     //          << ", kurl url=" << *url.kurl();
0673 
0674     if (d->mode == ExeCompletion) {
0675         // Executables
0676         //
0677         if (d->exeCompletion(url, &aMatch)) {
0678             return aMatch;
0679         }
0680 
0681         // KRun can run "man:" and "info:" etc. so why not treat them
0682         // as executables...
0683 
0684         if (d->urlCompletion(url, &aMatch)) {
0685             return aMatch;
0686         }
0687     } else {
0688         // Local files, directories
0689         //
0690         if (d->fileCompletion(url, &aMatch)) {
0691             return aMatch;
0692         }
0693 
0694         // All other...
0695         //
0696         if (d->urlCompletion(url, &aMatch)) {
0697             return aMatch;
0698         }
0699     }
0700 
0701     d->setListedUrl(CTNone);
0702     stop();
0703 
0704     return QString();
0705 }
0706 
0707 /*
0708  * finished
0709  *
0710  * Go on and call KCompletion.
0711  * Called when all matches have been added
0712  */
0713 QString KUrlCompletionPrivate::finished()
0714 {
0715     if (last_compl_type == CTInfo) {
0716         return q->KCompletion::makeCompletion(compl_text.toLower());
0717     } else {
0718         return q->KCompletion::makeCompletion(compl_text);
0719     }
0720 }
0721 
0722 /*
0723  * isRunning
0724  *
0725  * Return true if either a KIO job or a thread is running
0726  */
0727 bool KUrlCompletion::isRunning() const
0728 {
0729     return d->list_job || (d->dirListThread && !d->dirListThread->isFinished()) || (d->userListThread && !d->userListThread->isFinished());
0730 }
0731 
0732 /*
0733  * stop
0734  *
0735  * Stop and delete a running KIO job or the DirLister
0736  */
0737 void KUrlCompletion::stop()
0738 {
0739     if (d->list_job) {
0740         d->list_job->kill();
0741         d->list_job = nullptr;
0742     }
0743 
0744     if (d->dirListThread) {
0745         d->dirListThread->requestTermination();
0746         delete d->dirListThread;
0747         d->dirListThread = nullptr;
0748     }
0749 
0750     if (d->userListThread) {
0751         d->userListThread->requestTermination();
0752         delete d->userListThread;
0753         d->userListThread = nullptr;
0754     }
0755 }
0756 
0757 /*
0758  * Keep track of the last listed directory
0759  */
0760 void KUrlCompletionPrivate::setListedUrl(ComplType complType,
0761         const QString &directory,
0762         const QString &filter,
0763         bool no_hidden)
0764 {
0765     last_compl_type = complType;
0766     last_path_listed = directory;
0767     last_file_listed = filter;
0768     last_no_hidden = no_hidden;
0769     last_prepend = prepend;
0770 }
0771 
0772 bool KUrlCompletionPrivate::isListedUrl(ComplType complType,
0773                                         const QString &directory,
0774                                         const QString &filter,
0775                                         bool no_hidden)
0776 {
0777     return  last_compl_type == complType
0778             && (last_path_listed == directory
0779                 || (directory.isEmpty() && last_path_listed.isEmpty()))
0780             && (filter.startsWith(last_file_listed)
0781                 || (filter.isEmpty() && last_file_listed.isEmpty()))
0782             && last_no_hidden == no_hidden
0783             && last_prepend == prepend; // e.g. relative path vs absolute
0784 }
0785 
0786 /*
0787  * isAutoCompletion
0788  *
0789  * Returns true if completion mode is Auto or Popup
0790  */
0791 bool KUrlCompletionPrivate::isAutoCompletion()
0792 {
0793     return q->completionMode() == KCompletion::CompletionAuto
0794            || q->completionMode() == KCompletion::CompletionPopup
0795            || q->completionMode() == KCompletion::CompletionMan
0796            || q->completionMode() == KCompletion::CompletionPopupAuto;
0797 }
0798 //////////////////////////////////////////////////
0799 //////////////////////////////////////////////////
0800 // User directories
0801 //
0802 
0803 bool KUrlCompletionPrivate::userCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch)
0804 {
0805     if (url.scheme() != QLatin1String("file")
0806             || !url.dir().isEmpty()
0807             || !url.file().startsWith(QLatin1Char('~'))
0808             || !prepend.isEmpty()) {
0809         return false;
0810     }
0811 
0812     if (!isListedUrl(CTUser)) {
0813         q->stop();
0814         q->clear();
0815         setListedUrl(CTUser);
0816 
0817         Q_ASSERT(!userListThread); // caller called stop()
0818         userListThread = new UserListThread(this);
0819         QObject::connect(userListThread, &CompletionThread::completionThreadDone,
0820                 q, [this](QThread *thread, const QStringList &matches){ slotCompletionThreadDone(thread, matches); });
0821         userListThread->start();
0822 
0823         // If the thread finishes quickly make sure that the results
0824         // are added to the first matching case.
0825 
0826         userListThread->wait(initialWaitDuration());
0827         const QStringList l = userListThread->matches();
0828         addMatches(l);
0829     }
0830     *pMatch = finished();
0831     return true;
0832 }
0833 
0834 /////////////////////////////////////////////////////
0835 /////////////////////////////////////////////////////
0836 // Environment variables
0837 //
0838 
0839 bool KUrlCompletionPrivate::envCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch)
0840 {
0841     if (url.file().isEmpty() || url.file().at(0) != QLatin1Char('$')) {
0842         return false;
0843     }
0844 
0845     if (!isListedUrl(CTEnv)) {
0846         q->stop();
0847         q->clear();
0848 
0849         QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
0850         const QStringList keys = env.keys();
0851 
0852         QStringList l;
0853         l.reserve(keys.size());
0854         for (const QString &key : keys) {
0855             l.append(prepend + QLatin1Char('$') + key);
0856         }
0857 
0858         addMatches(l);
0859     }
0860 
0861     setListedUrl(CTEnv);
0862 
0863     *pMatch = finished();
0864     return true;
0865 }
0866 
0867 //////////////////////////////////////////////////
0868 //////////////////////////////////////////////////
0869 // Executables
0870 //
0871 
0872 bool KUrlCompletionPrivate::exeCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch)
0873 {
0874     if (!url.isLocalFile()) {
0875         return false;
0876     }
0877 
0878     QString directory = unescape(url.dir());  // remove escapes
0879 
0880     // Find directories to search for completions, either
0881     //
0882     // 1. complete path given in url
0883     // 2. current directory (d->cwd)
0884     // 3. $PATH
0885     // 4. no directory at all
0886 
0887     QStringList dirList;
0888 
0889     if (!url.file().isEmpty()) {
0890         // $PATH
0891         dirList = QString::fromLocal8Bit(qgetenv("PATH")).split(
0892                       QDir::listSeparator(), QString::SkipEmptyParts);
0893 
0894         QStringList::Iterator it = dirList.begin();
0895 
0896         for (; it != dirList.end(); ++it) {
0897             it->append(QLatin1Char('/'));
0898         }
0899     } else if (!QDir::isRelativePath(directory)) {
0900         // complete path in url
0901         dirList.append(directory);
0902     } else if (!directory.isEmpty() && !cwd.isEmpty()) {
0903         // current directory
0904         dirList.append(cwd.toLocalFile() + QLatin1Char('/') + directory);
0905     }
0906 
0907     // No hidden files unless the user types "."
0908     bool no_hidden_files = url.file().isEmpty() || url.file().at(0) != QLatin1Char('.');
0909 
0910     // List files if needed
0911     //
0912     if (!isListedUrl(CTExe, directory, url.file(), no_hidden_files)) {
0913         q->stop();
0914         q->clear();
0915 
0916         setListedUrl(CTExe, directory, url.file(), no_hidden_files);
0917 
0918         *pMatch = listDirectories(dirList, url.file(), true, false, no_hidden_files);
0919     } else {
0920         *pMatch = finished();
0921     }
0922 
0923     return true;
0924 }
0925 
0926 //////////////////////////////////////////////////
0927 //////////////////////////////////////////////////
0928 // Local files
0929 //
0930 
0931 bool KUrlCompletionPrivate::fileCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch)
0932 {
0933     if (!url.isLocalFile()) {
0934         return false;
0935     }
0936 
0937     QString directory = unescape(url.dir());
0938 
0939     if (url.url() == QLatin1String("..")) {
0940         *pMatch = QStringLiteral("..");
0941         return true;
0942     }
0943 
0944     //qDebug() << "fileCompletion" << url << "dir=" << dir;
0945 
0946     // Find directories to search for completions, either
0947     //
0948     // 1. complete path given in url
0949     // 2. current directory (d->cwd)
0950     // 3. no directory at all
0951 
0952     QStringList dirList;
0953 
0954     if (!QDir::isRelativePath(directory)) {
0955         // complete path in url
0956         dirList.append(directory);
0957     } else if (!cwd.isEmpty()) {
0958         // current directory
0959         QString dirToAdd = cwd.toLocalFile();
0960         if (!directory.isEmpty()) {
0961             if (!dirToAdd.endsWith(QLatin1Char('/'))) {
0962                 dirToAdd.append(QLatin1Char('/'));
0963             }
0964             dirToAdd.append(directory);
0965         }
0966         dirList.append(dirToAdd);
0967     }
0968 
0969     // No hidden files unless the user types "."
0970     bool no_hidden_files = !url.file().startsWith(QLatin1Char('.'));
0971 
0972     // List files if needed
0973     //
0974     if (!isListedUrl(CTFile, directory, QString(), no_hidden_files)) {
0975         q->stop();
0976         q->clear();
0977 
0978         setListedUrl(CTFile, directory, QString(), no_hidden_files);
0979 
0980         // Append '/' to directories in Popup mode?
0981         bool append_slash = (popup_append_slash
0982                              && (q->completionMode() == KCompletion::CompletionPopup ||
0983                                  q->completionMode() == KCompletion::CompletionPopupAuto));
0984 
0985         bool only_dir = (mode == KUrlCompletion::DirCompletion);
0986 
0987         *pMatch = listDirectories(dirList, QString(), false, only_dir, no_hidden_files,
0988                                   append_slash);
0989     } else {
0990         *pMatch = finished();
0991     }
0992 
0993     return true;
0994 }
0995 
0996 //////////////////////////////////////////////////
0997 //////////////////////////////////////////////////
0998 // URLs not handled elsewhere...
0999 //
1000 
1001 static bool isLocalProtocol(const QString &protocol)
1002 {
1003     return (KProtocolInfo::protocolClass(protocol) == QLatin1String(":local"));
1004 }
1005 
1006 bool KUrlCompletionPrivate::urlCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch)
1007 {
1008     //qDebug() << *url.kurl();
1009     if (onlyLocalProto && isLocalProtocol(url.scheme())) {
1010         return false;
1011     }
1012 
1013     // Use d->cwd as base url in case url is not absolute
1014     QUrl url_dir = url.kurl();
1015     if (url_dir.isRelative() && !cwd.isEmpty()) {
1016         // Create an URL with the directory to be listed
1017         url_dir = cwd.resolved(url_dir);
1018     }
1019 
1020     // url is malformed
1021     if (!url_dir.isValid() || url.scheme().isEmpty()) {
1022         return false;
1023     }
1024 
1025     // non local urls
1026     if (!isLocalProtocol(url.scheme())) {
1027         // url does not specify host
1028         if (url_dir.host().isEmpty()) {
1029             return false;
1030         }
1031 
1032         // url does not specify a valid directory
1033         if (url_dir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path().isEmpty()) {
1034             return false;
1035         }
1036 
1037         // automatic completion is disabled
1038         if (isAutoCompletion() && !url_auto_completion) {
1039             return false;
1040         }
1041     }
1042 
1043     // url handler doesn't support listing
1044     if (!KProtocolManager::supportsListing(url_dir)) {
1045         return false;
1046     }
1047 
1048     // Remove escapes
1049     const QString directory = unescape(url_dir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path());
1050     url_dir.setPath(directory);
1051 
1052     // List files if needed
1053     //
1054     if (!isListedUrl(CTUrl, directory, url.file())) {
1055         q->stop();
1056         q->clear();
1057 
1058         setListedUrl(CTUrl, directory, QString());
1059 
1060         QList<QUrl> url_list;
1061         url_list.append(url_dir);
1062 
1063         listUrls(url_list, QString(), false);
1064 
1065         pMatch->clear();
1066     } else if (!q->isRunning()) {
1067         *pMatch = finished();
1068     } else {
1069         pMatch->clear();
1070     }
1071 
1072     return true;
1073 }
1074 
1075 //////////////////////////////////////////////////
1076 //////////////////////////////////////////////////
1077 // Directory and URL listing
1078 //
1079 
1080 /*
1081  * addMatches
1082  *
1083  * Called to add matches to KCompletion
1084  */
1085 void KUrlCompletionPrivate::addMatches(const QStringList &matchList)
1086 {
1087     q->insertItems(matchList);
1088 }
1089 
1090 /*
1091  * listDirectories
1092  *
1093  * List files starting with 'filter' in the given directories,
1094  * either using DirLister or listURLs()
1095  *
1096  * In either case, addMatches() is called with the listed
1097  * files, and eventually finished() when the listing is done
1098  *
1099  * Returns the match if available, or QString() if
1100  * DirLister timed out or using kio
1101  */
1102 QString KUrlCompletionPrivate::listDirectories(
1103     const QStringList &dirList,
1104     const QString &filter,
1105     bool only_exe,
1106     bool only_dir,
1107     bool no_hidden,
1108     bool append_slash_to_dir)
1109 {
1110     assert(!q->isRunning());
1111 
1112     if (qEnvironmentVariableIsEmpty("KURLCOMPLETION_LOCAL_KIO")) {
1113 
1114         qCDebug(KIO_WIDGETS) << "Listing directories:" << dirList << "with filter=" << filter << "using thread";
1115 
1116         // Don't use KIO
1117 
1118         QStringList dirs;
1119 
1120         QStringList::ConstIterator end = dirList.constEnd();
1121         for (QStringList::ConstIterator it = dirList.constBegin();
1122                 it != end;
1123                 ++it) {
1124             QUrl url = QUrl::fromLocalFile(*it);
1125             if (KUrlAuthorized::authorizeUrlAction(QStringLiteral("list"), QUrl(), url)) {
1126                 dirs.append(*it);
1127             }
1128         }
1129 
1130         Q_ASSERT(!dirListThread); // caller called stop()
1131         dirListThread = new DirectoryListThread(this, dirs, filter, mimeTypeFilters,
1132                                                 only_exe, only_dir, no_hidden,
1133                                                 append_slash_to_dir);
1134         QObject::connect(dirListThread, &CompletionThread::completionThreadDone,
1135                 q, [this](QThread *thread, const QStringList &matches){ slotCompletionThreadDone(thread, matches); });
1136         dirListThread->start();
1137         dirListThread->wait(initialWaitDuration());
1138         qCDebug(KIO_WIDGETS) << "Adding initial matches:" << dirListThread->matches();
1139         addMatches(dirListThread->matches());
1140 
1141         return finished();
1142     }
1143 
1144     // Use KIO
1145     //qDebug() << "Listing (listDirectories):" << dirList << "with KIO";
1146 
1147     QList<QUrl> url_list;
1148 
1149     QStringList::ConstIterator it = dirList.constBegin();
1150     QStringList::ConstIterator end = dirList.constEnd();
1151 
1152     url_list.reserve(dirList.size());
1153     for (; it != end; ++it) {
1154         url_list.append(QUrl(*it));
1155     }
1156 
1157     listUrls(url_list, filter, only_exe, no_hidden);
1158     // Will call addMatches() and finished()
1159 
1160     return QString();
1161 }
1162 
1163 /*
1164  * listURLs
1165  *
1166  * Use KIO to list the given urls
1167  *
1168  * addMatches() is called with the listed files
1169  * finished() is called when the listing is done
1170  */
1171 void KUrlCompletionPrivate::listUrls(
1172     const QList<QUrl> &urls,
1173     const QString &filter,
1174     bool only_exe,
1175     bool no_hidden)
1176 {
1177     assert(list_urls.isEmpty());
1178     assert(list_job == nullptr);
1179 
1180     list_urls = urls;
1181     list_urls_filter = filter;
1182     list_urls_only_exe = only_exe;
1183     list_urls_no_hidden = no_hidden;
1184 
1185     //qDebug() << "Listing URLs:" << *urls[0] << ",...";
1186 
1187     // Start it off by calling _k_slotIOFinished
1188     //
1189     // This will start a new list job as long as there
1190     // are urls in d->list_urls
1191     //
1192     _k_slotIOFinished(nullptr);
1193 }
1194 
1195 /*
1196  * _k_slotEntries
1197  *
1198  * Receive files listed by KIO and call addMatches()
1199  */
1200 void KUrlCompletionPrivate::_k_slotEntries(KIO::Job *, const KIO::UDSEntryList &entries)
1201 {
1202     QStringList matchList;
1203 
1204     KIO::UDSEntryList::ConstIterator it = entries.constBegin();
1205     const KIO::UDSEntryList::ConstIterator end = entries.constEnd();
1206 
1207     QString filter = list_urls_filter;
1208 
1209     int filter_len = filter.length();
1210 
1211     // Iterate over all files
1212     //
1213     for (; it != end; ++it) {
1214         const KIO::UDSEntry &entry = *it;
1215         const QString url = entry.stringValue(KIO::UDSEntry::UDS_URL);
1216 
1217         QString entry_name;
1218         if (!url.isEmpty()) {
1219             //qDebug() << "url:" << url;
1220             entry_name = QUrl(url).fileName();
1221         } else {
1222             entry_name = entry.stringValue(KIO::UDSEntry::UDS_NAME);
1223         }
1224 
1225         //qDebug() << "name:" << name;
1226 
1227         if ((!entry_name.isEmpty() && entry_name.at(0) == QLatin1Char('.')) &&
1228                 (list_urls_no_hidden ||
1229                  entry_name.length() == 1 ||
1230                  (entry_name.length() == 2 && entry_name.at(1) == QLatin1Char('.')))) {
1231             continue;
1232         }
1233 
1234         const bool isDir = entry.isDir();
1235 
1236         if (mode == KUrlCompletion::DirCompletion && !isDir) {
1237             continue;
1238         }
1239 
1240         if (filter_len != 0 && entry_name.leftRef(filter_len) != filter) {
1241             continue;
1242         }
1243 
1244         if (!mimeTypeFilters.isEmpty() && !isDir &&
1245             !mimeTypeFilters.contains(entry.stringValue(KIO::UDSEntry::UDS_MIME_TYPE))) {
1246             continue;
1247         }
1248 
1249         QString toAppend = entry_name;
1250 
1251         if (isDir) {
1252             toAppend.append(QLatin1Char('/'));
1253         }
1254 
1255         if (!list_urls_only_exe ||
1256                 (entry.numberValue(KIO::UDSEntry::UDS_ACCESS) & MODE_EXE)  // true if executable
1257             ) {
1258             if (complete_url) {
1259                 QUrl url(prepend);
1260                 url = addPathToUrl(url, toAppend);
1261                 matchList.append(url.toDisplayString());
1262             } else {
1263                 matchList.append(prepend + toAppend);
1264             }
1265         }
1266 
1267     }
1268 
1269     addMatches(matchList);
1270 }
1271 
1272 /*
1273  * _k_slotIOFinished
1274  *
1275  * Called when a KIO job is finished.
1276  *
1277  * Start a new list job if there are still urls in
1278  * list_urls, otherwise call finished()
1279  */
1280 void KUrlCompletionPrivate::_k_slotIOFinished(KJob *job)
1281 {
1282     assert(job == list_job); Q_UNUSED(job)
1283 
1284     if (list_urls.isEmpty()) {
1285 
1286         list_job = nullptr;
1287 
1288         finished(); // will call KCompletion::makeCompletion()
1289 
1290     } else {
1291 
1292         QUrl kurl(list_urls.takeFirst());
1293 
1294 //      list_urls.removeAll( kurl );
1295 
1296 //qDebug() << "Start KIO::listDir" << kurl;
1297 
1298         list_job = KIO::listDir(kurl, KIO::HideProgressInfo);
1299         list_job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
1300 
1301         assert(list_job);
1302 
1303         q->connect(list_job,
1304                    SIGNAL(result(KJob*)),
1305                    SLOT(_k_slotIOFinished(KJob*)));
1306 
1307         q->connect(list_job,
1308                    SIGNAL(entries(KIO::Job*,KIO::UDSEntryList)),
1309                    SLOT(_k_slotEntries(KIO::Job*,KIO::UDSEntryList)));
1310     }
1311 }
1312 
1313 ///////////////////////////////////////////////////
1314 ///////////////////////////////////////////////////
1315 
1316 /*
1317  * postProcessMatch, postProcessMatches
1318  *
1319  * Called by KCompletion before emitting match() and matches()
1320  *
1321  * Append '/' to directories for file completion. This is
1322  * done here to avoid stat()'ing a lot of files
1323  */
1324 void KUrlCompletion::postProcessMatch(QString *pMatch) const
1325 {
1326 //qDebug() << *pMatch;
1327 
1328     if (!pMatch->isEmpty() && pMatch->startsWith(QLatin1String("file:"))) {
1329 
1330         // Add '/' to directories in file completion mode
1331         // unless it has already been done
1332         if (d->last_compl_type == CTFile
1333                 && pMatch->at(pMatch->length() - 1) != QLatin1Char('/')) {
1334             QString copy = QUrl(*pMatch).toLocalFile();
1335             expandTilde(copy);
1336             expandEnv(copy);
1337             if (QDir::isRelativePath(copy)) {
1338                 copy.prepend(d->cwd.toLocalFile() + QLatin1Char('/'));
1339             }
1340 
1341             //qDebug() << "stat'ing" << copy;
1342 
1343             QByteArray file = QFile::encodeName(copy);
1344 
1345             QT_STATBUF sbuff;
1346             if (QT_STAT(file.constData(), &sbuff) == 0) {
1347                 if ((sbuff.st_mode & QT_STAT_MASK) == QT_STAT_DIR) {
1348                     pMatch->append(QLatin1Char('/'));
1349                 }
1350             } else {
1351                 //qDebug() << "Could not stat file" << copy;
1352             }
1353         }
1354     }
1355 }
1356 
1357 void KUrlCompletion::postProcessMatches(QStringList * /*matches*/) const
1358 {
1359     // Maybe '/' should be added to directories here as in
1360     // postProcessMatch() but it would slow things down
1361     // when there are a lot of matches...
1362 }
1363 
1364 void KUrlCompletion::postProcessMatches(KCompletionMatches * /*matches*/) const
1365 {
1366     // Maybe '/' should be added to directories here as in
1367     // postProcessMatch() but it would slow things down
1368     // when there are a lot of matches...
1369 }
1370 
1371 // no longer used, KF6 TODO: remove this method
1372 void KUrlCompletion::customEvent(QEvent *e)
1373 {
1374     KCompletion::customEvent(e);
1375 }
1376 
1377 void KUrlCompletionPrivate::slotCompletionThreadDone(QThread *thread, const QStringList &matches)
1378 {
1379     if (thread != userListThread && thread != dirListThread) {
1380         qCDebug(KIO_WIDGETS) << "got" << matches.count() << "outdated matches";
1381         return;
1382     }
1383 
1384     qCDebug(KIO_WIDGETS) << "got" << matches.count() << "matches at end of thread";
1385     q->setItems(matches);
1386 
1387     if (userListThread == thread) {
1388         thread->wait();
1389         delete thread;
1390         userListThread = nullptr;
1391     }
1392 
1393     if (dirListThread == thread) {
1394         thread->wait();
1395         delete thread;
1396         dirListThread = nullptr;
1397     }
1398     finished(); // will call KCompletion::makeCompletion()
1399 }
1400 
1401 // static
1402 QString KUrlCompletion::replacedPath(const QString &text, bool replaceHome, bool replaceEnv)
1403 {
1404     if (text.isEmpty()) {
1405         return text;
1406     }
1407 
1408     KUrlCompletionPrivate::MyURL url(text, QUrl());  // no need to replace something of our current cwd
1409     if (!url.kurl().isLocalFile()) {
1410         return text;
1411     }
1412 
1413     url.filter(replaceHome, replaceEnv);
1414     return url.dir() + url.file();
1415 }
1416 
1417 QString KUrlCompletion::replacedPath(const QString &text) const
1418 {
1419     return replacedPath(text, d->replace_home, d->replace_env);
1420 }
1421 
1422 void KUrlCompletion::setMimeTypeFilters(const QStringList &mimeTypeFilters)
1423 {
1424     d->mimeTypeFilters = mimeTypeFilters;
1425 }
1426 
1427 QStringList KUrlCompletion::mimeTypeFilters() const
1428 {
1429     return d->mimeTypeFilters;
1430 }
1431 
1432 /////////////////////////////////////////////////////////
1433 /////////////////////////////////////////////////////////
1434 // Static functions
1435 
1436 /*
1437  * expandEnv
1438  *
1439  * Expand environment variables in text. Escaped '$' are ignored.
1440  * Return true if expansion was made.
1441  */
1442 static bool expandEnv(QString &text)
1443 {
1444     // Find all environment variables beginning with '$'
1445     //
1446     int pos = 0;
1447 
1448     bool expanded = false;
1449 
1450     while ((pos = text.indexOf(QLatin1Char('$'), pos)) != -1) {
1451 
1452         // Skip escaped '$'
1453         //
1454         if (pos > 0 && text.at(pos - 1) == QLatin1Char('\\')) {
1455             pos++;
1456         }
1457         // Variable found => expand
1458         //
1459         else {
1460             // Find the end of the variable = next '/' or ' '
1461             //
1462             int pos2 = text.indexOf(QLatin1Char(' '), pos + 1);
1463             int pos_tmp = text.indexOf(QLatin1Char('/'), pos + 1);
1464 
1465             if (pos2 == -1 || (pos_tmp != -1 && pos_tmp < pos2)) {
1466                 pos2 = pos_tmp;
1467             }
1468 
1469             if (pos2 == -1) {
1470                 pos2 = text.length();
1471             }
1472 
1473             // Replace if the variable is terminated by '/' or ' '
1474             // and defined
1475             //
1476             if (pos2 >= 0) {
1477                 int len = pos2 - pos;
1478                 const QStringRef key = text.midRef(pos + 1, len - 1);
1479                 QString value =
1480                     QString::fromLocal8Bit(qgetenv(key.toLocal8Bit().constData()));
1481 
1482                 if (!value.isEmpty()) {
1483                     expanded = true;
1484                     text.replace(pos, len, value);
1485                     pos = pos + value.length();
1486                 } else {
1487                     pos = pos2;
1488                 }
1489             }
1490         }
1491     }
1492 
1493     return expanded;
1494 }
1495 
1496 /*
1497  * expandTilde
1498  *
1499  * Replace "~user" with the users home directory
1500  * Return true if expansion was made.
1501  */
1502 static bool expandTilde(QString &text)
1503 {
1504     if (text.isEmpty() || (text.at(0) != QLatin1Char('~'))) {
1505         return false;
1506     }
1507 
1508     bool expanded = false;
1509 
1510     // Find the end of the user name = next '/' or ' '
1511     //
1512     int pos2 = text.indexOf(QLatin1Char(' '), 1);
1513     int pos_tmp = text.indexOf(QLatin1Char('/'), 1);
1514 
1515     if (pos2 == -1 || (pos_tmp != -1 && pos_tmp < pos2)) {
1516         pos2 = pos_tmp;
1517     }
1518 
1519     if (pos2 == -1) {
1520         pos2 = text.length();
1521     }
1522 
1523     // Replace ~user if the user name is terminated by '/' or ' '
1524     //
1525     if (pos2 >= 0) {
1526 
1527         QString userName = text.mid(1, pos2 - 1);
1528         QString dir;
1529 
1530         // A single ~ is replaced with $HOME
1531         //
1532         if (userName.isEmpty()) {
1533             dir = QDir::homePath();
1534         }
1535         // ~user is replaced with the dir from passwd
1536         //
1537         else {
1538             KUser user(userName);
1539             dir = user.homeDir();
1540         }
1541 
1542         if (!dir.isEmpty()) {
1543             expanded = true;
1544             text.replace(0, pos2, dir);
1545         }
1546     }
1547 
1548     return expanded;
1549 }
1550 
1551 /*
1552  * unescape
1553  *
1554  * Remove escapes and return the result in a new string
1555  *
1556  */
1557 static QString unescape(const QString &text)
1558 {
1559     QString result;
1560     result.reserve(text.size());
1561 
1562     for (const QChar ch : text) {
1563         if (ch != QLatin1Char('\\')) {
1564             result.append(ch);
1565         }
1566     }
1567 
1568     return result;
1569 }
1570 
1571 #include "moc_kurlcompletion.cpp"
1572 #include "kurlcompletion.moc"