File indexing completed on 2024-06-16 06:56:56

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2000 David Smith <dsmith@algonet.se>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "kshellcompletion.h"
0009 
0010 #include <KCompletion>
0011 #include <KCompletionMatches>
0012 #include <stdlib.h>
0013 
0014 class KShellCompletionPrivate
0015 {
0016 public:
0017     KShellCompletionPrivate()
0018         : m_word_break_char(QLatin1Char(' '))
0019         , m_quote_char1(QLatin1Char('\"'))
0020         , m_quote_char2(QLatin1Char('\''))
0021         , m_escape_char(QLatin1Char('\\'))
0022     {
0023     }
0024 
0025     void splitText(const QString &text, QString &text_start, QString &text_compl) const;
0026     bool quoteText(QString *text, bool force, bool skip_last) const;
0027     QString unquote(const QString &text) const;
0028 
0029     QString m_text_start; // part of the text that was not completed
0030     QString m_text_compl; // part of the text that was completed (unchanged)
0031 
0032     QChar m_word_break_char;
0033     QChar m_quote_char1;
0034     QChar m_quote_char2;
0035     QChar m_escape_char;
0036 };
0037 
0038 KShellCompletion::KShellCompletion()
0039     : KUrlCompletion()
0040     , d(new KShellCompletionPrivate)
0041 {
0042 }
0043 
0044 KShellCompletion::~KShellCompletion() = default;
0045 
0046 /*
0047  * makeCompletion()
0048  *
0049  * Entry point for file name completion
0050  */
0051 QString KShellCompletion::makeCompletion(const QString &text)
0052 {
0053     // Split text at the last unquoted space
0054     //
0055     d->splitText(text, d->m_text_start, d->m_text_compl);
0056 
0057     // Remove quotes from the text to be completed
0058     //
0059     QString tmp = d->unquote(d->m_text_compl);
0060     d->m_text_compl = tmp;
0061 
0062     // Do exe-completion if there was no unquoted space
0063     //
0064     const bool is_exe_completion = !d->m_text_start.contains(d->m_word_break_char);
0065 
0066     setMode(is_exe_completion ? ExeCompletion : FileCompletion);
0067 
0068     // Make completion on the last part of text
0069     //
0070     return KUrlCompletion::makeCompletion(d->m_text_compl);
0071 }
0072 
0073 /*
0074  * postProcessMatch, postProcessMatches
0075  *
0076  * Called by KCompletion before emitting match() and matches()
0077  *
0078  * Add add the part of the text that was not completed
0079  * Add quotes when needed
0080  */
0081 void KShellCompletion::postProcessMatch(QString *match) const
0082 {
0083     KUrlCompletion::postProcessMatch(match);
0084 
0085     if (match->isNull()) {
0086         return;
0087     }
0088 
0089     if (match->endsWith(QLatin1Char('/'))) {
0090         d->quoteText(match, false, true); // don't quote the trailing '/'
0091     } else {
0092         d->quoteText(match, false, false); // quote the whole text
0093     }
0094 
0095     match->prepend(d->m_text_start);
0096 }
0097 
0098 void KShellCompletion::postProcessMatches(QStringList *matches) const
0099 {
0100     KUrlCompletion::postProcessMatches(matches);
0101 
0102     for (QString &match : *matches) {
0103         if (!match.isNull()) {
0104             if (match.endsWith(QLatin1Char('/'))) {
0105                 d->quoteText(&match, false, true); // don't quote trailing '/'
0106             } else {
0107                 d->quoteText(&match, false, false); // quote the whole text
0108             }
0109 
0110             match.prepend(d->m_text_start);
0111         }
0112     }
0113 }
0114 
0115 void KShellCompletion::postProcessMatches(KCompletionMatches *matches) const
0116 {
0117     KUrlCompletion::postProcessMatches(matches);
0118 
0119     for (auto &match : *matches) {
0120         QString &matchString = match.value();
0121         if (!matchString.isNull()) {
0122             if (matchString.endsWith(QLatin1Char('/'))) {
0123                 d->quoteText(&matchString, false, true); // don't quote trailing '/'
0124             } else {
0125                 d->quoteText(&matchString, false, false); // quote the whole text
0126             }
0127 
0128             matchString.prepend(d->m_text_start);
0129         }
0130     }
0131 }
0132 
0133 /*
0134  * splitText
0135  *
0136  * Split text at the last unquoted space
0137  *
0138  * text_start = [out] text at the left, including the space
0139  * text_compl = [out] text at the right
0140  */
0141 void KShellCompletionPrivate::splitText(const QString &text, QString &text_start, QString &text_compl) const
0142 {
0143     bool in_quote = false;
0144     bool escaped = false;
0145     QChar p_last_quote_char;
0146     int last_unquoted_space = -1;
0147 
0148     for (int pos = 0; pos < text.length(); pos++) {
0149         int end_space_len = 0;
0150 
0151         if (escaped) {
0152             escaped = false;
0153         } else if (in_quote && text[pos] == p_last_quote_char) {
0154             in_quote = false;
0155         } else if (!in_quote && text[pos] == m_quote_char1) {
0156             p_last_quote_char = m_quote_char1;
0157             in_quote = true;
0158         } else if (!in_quote && text[pos] == m_quote_char2) {
0159             p_last_quote_char = m_quote_char2;
0160             in_quote = true;
0161         } else if (text[pos] == m_escape_char) {
0162             escaped = true;
0163         } else if (!in_quote && text[pos] == m_word_break_char) {
0164             end_space_len = 1;
0165 
0166             while (pos + 1 < text.length() && text[pos + 1] == m_word_break_char) {
0167                 end_space_len++;
0168                 pos++;
0169             }
0170 
0171             if (pos + 1 == text.length()) {
0172                 break;
0173             }
0174 
0175             last_unquoted_space = pos;
0176         }
0177     }
0178 
0179     text_start = text.left(last_unquoted_space + 1);
0180 
0181     // the last part without trailing blanks
0182     text_compl = text.mid(last_unquoted_space + 1);
0183 }
0184 
0185 /*
0186  * quoteText()
0187  *
0188  * Add quotations to 'text' if needed or if 'force' = true
0189  * Returns true if quotes were added
0190  *
0191  * skip_last => ignore the last character (we add a space or '/' to all filenames)
0192  */
0193 bool KShellCompletionPrivate::quoteText(QString *text, bool force, bool skip_last) const
0194 {
0195     int pos = 0;
0196 
0197     if (!force) {
0198         pos = text->indexOf(m_word_break_char);
0199         if (skip_last && (pos == (int)(text->length()) - 1)) {
0200             pos = -1;
0201         }
0202     }
0203 
0204     if (!force && pos == -1) {
0205         pos = text->indexOf(m_quote_char1);
0206         if (skip_last && (pos == (int)(text->length()) - 1)) {
0207             pos = -1;
0208         }
0209     }
0210 
0211     if (!force && pos == -1) {
0212         pos = text->indexOf(m_quote_char2);
0213         if (skip_last && (pos == (int)(text->length()) - 1)) {
0214             pos = -1;
0215         }
0216     }
0217 
0218     if (!force && pos == -1) {
0219         pos = text->indexOf(m_escape_char);
0220         if (skip_last && (pos == (int)(text->length()) - 1)) {
0221             pos = -1;
0222         }
0223     }
0224 
0225     if (force || (pos >= 0)) {
0226         // Escape \ in the string
0227         text->replace(m_escape_char, QString(m_escape_char) + m_escape_char);
0228 
0229         // Escape " in the string
0230         text->replace(m_quote_char1, QString(m_escape_char) + m_quote_char1);
0231 
0232         // " at the beginning
0233         text->insert(0, m_quote_char1);
0234 
0235         // " at the end
0236         if (skip_last) {
0237             text->insert(text->length() - 1, m_quote_char1);
0238         } else {
0239             text->insert(text->length(), m_quote_char1);
0240         }
0241 
0242         return true;
0243     }
0244 
0245     return false;
0246 }
0247 
0248 /*
0249  * unquote
0250  *
0251  * Remove quotes and return the result in a new string
0252  *
0253  */
0254 QString KShellCompletionPrivate::unquote(const QString &text) const
0255 {
0256     bool in_quote = false;
0257     bool escaped = false;
0258     QChar p_last_quote_char;
0259     QString result;
0260 
0261     for (const QChar ch : text) {
0262         if (escaped) {
0263             escaped = false;
0264             result.insert(result.length(), ch);
0265         } else if (in_quote && ch == p_last_quote_char) {
0266             in_quote = false;
0267         } else if (!in_quote && ch == m_quote_char1) {
0268             p_last_quote_char = m_quote_char1;
0269             in_quote = true;
0270         } else if (!in_quote && ch == m_quote_char2) {
0271             p_last_quote_char = m_quote_char2;
0272             in_quote = true;
0273         } else if (ch == m_escape_char) {
0274             escaped = true;
0275             result.insert(result.length(), ch);
0276         } else {
0277             result.insert(result.length(), ch);
0278         }
0279     }
0280 
0281     return result;
0282 }
0283 
0284 #include "moc_kshellcompletion.cpp"