File indexing completed on 2024-12-01 03:41:19

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