1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "stringutils.h"
27 
28 #include "hostosinfo.h"
29 
30 #include <utils/algorithm.h>
31 #include <utils/qtcassert.h>
32 
33 #include <QCoreApplication>
34 #include <QDir>
35 #include <QJsonArray>
36 #include <QJsonValue>
37 #include <QRegularExpression>
38 #include <QSet>
39 #include <QTime>
40 
41 #include <limits.h>
42 
43 namespace Utils {
44 
settingsKey(const QString & category)45 QTCREATOR_UTILS_EXPORT QString settingsKey(const QString &category)
46 {
47     QString rc(category);
48     const QChar underscore = '_';
49     // Remove the sort category "X.Category" -> "Category"
50     if (rc.size() > 2 && rc.at(0).isLetter() && rc.at(1) == '.')
51         rc.remove(0, 2);
52     // Replace special characters
53     const int size = rc.size();
54     for (int i = 0; i < size; i++) {
55         const QChar c = rc.at(i);
56         if (!c.isLetterOrNumber() && c != underscore)
57             rc[i] = underscore;
58     }
59     return rc;
60 }
61 
62 // Figure out length of common start of string ("C:\a", "c:\b"  -> "c:\"
commonPartSize(const QString & s1,const QString & s2)63 static inline int commonPartSize(const QString &s1, const QString &s2)
64 {
65     const int size = qMin(s1.size(), s2.size());
66     for (int i = 0; i < size; i++)
67         if (s1.at(i) != s2.at(i))
68             return i;
69     return size;
70 }
71 
commonPrefix(const QStringList & strings)72 QTCREATOR_UTILS_EXPORT QString commonPrefix(const QStringList &strings)
73 {
74     switch (strings.size()) {
75     case 0:
76         return QString();
77     case 1:
78         return strings.front();
79     default:
80         break;
81     }
82     // Figure out common string part: "C:\foo\bar1" "C:\foo\bar2"  -> "C:\foo\bar"
83     int commonLength = INT_MAX;
84     const int last = strings.size() - 1;
85     for (int i = 0; i < last; i++)
86         commonLength = qMin(commonLength, commonPartSize(strings.at(i), strings.at(i + 1)));
87     if (!commonLength)
88         return QString();
89     return strings.at(0).left(commonLength);
90 }
91 
commonPath(const QStringList & files)92 QTCREATOR_UTILS_EXPORT QString commonPath(const QStringList &files)
93 {
94     QStringList appendedSlashes = Utils::transform(files, [](const QString &file) -> QString {
95         if (!file.endsWith('/'))
96             return QString(file + '/');
97         return file;
98     });
99     QString common = commonPrefix(appendedSlashes);
100     // Find common directory part: "C:\foo\bar" -> "C:\foo"
101     int lastSeparatorPos = common.lastIndexOf('/');
102     if (lastSeparatorPos == -1)
103         lastSeparatorPos = common.lastIndexOf('\\');
104     if (lastSeparatorPos == -1)
105         return QString();
106     if (HostOsInfo::isAnyUnixHost() && lastSeparatorPos == 0) // Unix: "/a", "/b" -> '/'
107         lastSeparatorPos = 1;
108     common.truncate(lastSeparatorPos);
109     return common;
110 }
111 
withTildeHomePath(const QString & path)112 QTCREATOR_UTILS_EXPORT QString withTildeHomePath(const QString &path)
113 {
114     if (HostOsInfo::isWindowsHost())
115         return path;
116 
117     static const QString homePath = QDir::homePath();
118 
119     QFileInfo fi(QDir::cleanPath(path));
120     QString outPath = fi.absoluteFilePath();
121     if (outPath.startsWith(homePath))
122         outPath = '~' + outPath.mid(homePath.size());
123     else
124         outPath = path;
125     return outPath;
126 }
127 
validateVarName(const QString & varName)128 static bool validateVarName(const QString &varName)
129 {
130     return !varName.startsWith("JS:");
131 }
132 
expandNestedMacros(const QString & str,int * pos,QString * ret)133 bool AbstractMacroExpander::expandNestedMacros(const QString &str, int *pos, QString *ret)
134 {
135     QString varName;
136     QString pattern, replace;
137     QString defaultValue;
138     QString *currArg = &varName;
139     QChar prev;
140     QChar c;
141     QChar replacementChar;
142     bool replaceAll = false;
143 
144     int i = *pos;
145     int strLen = str.length();
146     varName.reserve(strLen - i);
147     for (; i < strLen; prev = c) {
148         c = str.at(i++);
149         if (c == '\\' && i < strLen) {
150             c = str.at(i++);
151             // For the replacement, do not skip the escape sequence when followed by a digit.
152             // This is needed for enabling convenient capture group replacement,
153             // like %{var/(.)(.)/\2\1}, without escaping the placeholders.
154             if (currArg == &replace && c.isDigit())
155                 *currArg += '\\';
156             *currArg += c;
157         } else if (c == '}') {
158             if (varName.isEmpty()) { // replace "%{}" with "%"
159                 *ret = QString('%');
160                 *pos = i;
161                 return true;
162             }
163             QSet<AbstractMacroExpander*> seen;
164             if (resolveMacro(varName, ret, seen)) {
165                 *pos = i;
166                 if (!pattern.isEmpty() && currArg == &replace) {
167                     const QRegularExpression regexp(pattern);
168                     if (regexp.isValid()) {
169                         if (replaceAll) {
170                             ret->replace(regexp, replace);
171                         } else {
172                             // There isn't an API for replacing once...
173                             const QRegularExpressionMatch match = regexp.match(*ret);
174                             if (match.hasMatch()) {
175                                 *ret = ret->left(match.capturedStart(0))
176                                         + match.captured(0).replace(regexp, replace)
177                                         + ret->mid(match.capturedEnd(0));
178                             }
179                         }
180                     }
181                 }
182                 return true;
183             }
184             if (!defaultValue.isEmpty()) {
185                 *pos = i;
186                 *ret = defaultValue;
187                 return true;
188             }
189             return false;
190         } else if (c == '{' && prev == '%') {
191             if (!expandNestedMacros(str, &i, ret))
192                 return false;
193             varName.chop(1);
194             varName += *ret;
195         } else if (currArg == &varName && c == '-' && prev == ':' && validateVarName(varName)) {
196             varName.chop(1);
197             currArg = &defaultValue;
198         } else if (currArg == &varName && (c == '/' || c == '#') && validateVarName(varName)) {
199             replacementChar = c;
200             currArg = &pattern;
201             if (i < strLen && str.at(i) == replacementChar) {
202                 ++i;
203                 replaceAll = true;
204             }
205         } else if (currArg == &pattern && c == replacementChar) {
206             currArg = &replace;
207         } else {
208             *currArg += c;
209         }
210     }
211     return false;
212 }
213 
findMacro(const QString & str,int * pos,QString * ret)214 int AbstractMacroExpander::findMacro(const QString &str, int *pos, QString *ret)
215 {
216     forever {
217         int openPos = str.indexOf("%{", *pos);
218         if (openPos < 0)
219             return 0;
220         int varPos = openPos + 2;
221         if (expandNestedMacros(str, &varPos, ret)) {
222             *pos = openPos;
223             return varPos - openPos;
224         }
225         // An actual expansion may be nested into a "false" one,
226         // so we continue right after the last %{.
227         *pos = openPos + 2;
228     }
229 }
230 
expandMacros(QString * str,AbstractMacroExpander * mx)231 QTCREATOR_UTILS_EXPORT void expandMacros(QString *str, AbstractMacroExpander *mx)
232 {
233     QString rsts;
234 
235     for (int pos = 0; int len = mx->findMacro(*str, &pos, &rsts); ) {
236         str->replace(pos, len, rsts);
237         pos += rsts.length();
238     }
239 }
240 
expandMacros(const QString & str,AbstractMacroExpander * mx)241 QTCREATOR_UTILS_EXPORT QString expandMacros(const QString &str, AbstractMacroExpander *mx)
242 {
243     QString ret = str;
244     expandMacros(&ret, mx);
245     return ret;
246 }
247 
stripAccelerator(const QString & text)248 QTCREATOR_UTILS_EXPORT QString stripAccelerator(const QString &text)
249 {
250     QString res = text;
251     for (int index = res.indexOf('&'); index != -1; index = res.indexOf('&', index + 1))
252         res.remove(index, 1);
253     return res;
254 }
255 
readMultiLineString(const QJsonValue & value,QString * out)256 QTCREATOR_UTILS_EXPORT bool readMultiLineString(const QJsonValue &value, QString *out)
257 {
258     QTC_ASSERT(out, return false);
259     if (value.isString()) {
260         *out = value.toString();
261     } else if (value.isArray()) {
262         QJsonArray array = value.toArray();
263         QStringList lines;
264         for (const QJsonValue &v : array) {
265             if (!v.isString())
266                 return false;
267             lines.append(v.toString());
268         }
269         *out = lines.join(QLatin1Char('\n'));
270     } else {
271         return false;
272     }
273     return true;
274 }
275 
parseUsedPortFromNetstatOutput(const QByteArray & line)276 QTCREATOR_UTILS_EXPORT int parseUsedPortFromNetstatOutput(const QByteArray &line)
277 {
278     const QByteArray trimmed = line.trimmed();
279     int base = 0;
280     QByteArray portString;
281 
282     if (trimmed.startsWith("TCP") || trimmed.startsWith("UDP")) {
283         // Windows.  Expected output is something like
284         //
285         // Active Connections
286         //
287         //   Proto  Local Address          Foreign Address        State
288         //   TCP    0.0.0.0:80             0.0.0.0:0              LISTENING
289         //   TCP    0.0.0.0:113            0.0.0.0:0              LISTENING
290         // [...]
291         //   TCP    10.9.78.4:14714       0.0.0.0:0              LISTENING
292         //   TCP    10.9.78.4:50233       12.13.135.180:993      ESTABLISHED
293         // [...]
294         //   TCP    [::]:445               [::]:0                 LISTENING
295         //   TCP    192.168.0.80:51905     169.55.74.50:443       ESTABLISHED
296         //   UDP    [fe80::880a:2932:8dff:a858%6]:1900  *:*
297         const int firstBracketPos = trimmed.indexOf('[');
298         int colonPos = -1;
299         if (firstBracketPos == -1) {
300             colonPos = trimmed.indexOf(':');  // IPv4
301         } else  {
302             // jump over host part
303             const int secondBracketPos = trimmed.indexOf(']', firstBracketPos + 1);
304             colonPos = trimmed.indexOf(':', secondBracketPos);
305         }
306         const int firstDigitPos = colonPos + 1;
307         const int spacePos = trimmed.indexOf(' ', firstDigitPos);
308         if (spacePos < 0)
309             return -1;
310         const int len = spacePos - firstDigitPos;
311         base = 10;
312         portString = trimmed.mid(firstDigitPos, len);
313     } else if (trimmed.startsWith("tcp") || trimmed.startsWith("udp")) {
314         // macOS. Expected output is something like
315         //
316         // Active Internet connections (including servers)
317         // Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
318         // tcp4       0      0  192.168.1.12.55687     88.198.14.66.443       ESTABLISHED
319         // tcp6       0      0  2a01:e34:ee42:d0.55684 2a02:26f0:ff::5c.443   ESTABLISHED
320         // [...]
321         // tcp4       0      0  *.631                  *.*                    LISTEN
322         // tcp6       0      0  *.631                  *.*                    LISTEN
323         // [...]
324         // udp4       0      0  192.168.79.1.123       *.*
325         // udp4       0      0  192.168.8.1.123        *.*
326         int firstDigitPos = -1;
327         int spacePos = -1;
328         if (trimmed[3] == '6') {
329             // IPV6
330             firstDigitPos = trimmed.indexOf('.') + 1;
331             spacePos = trimmed.indexOf(' ', firstDigitPos);
332         } else {
333             // IPV4
334             firstDigitPos = trimmed.indexOf('.') + 1;
335             spacePos = trimmed.indexOf(' ', firstDigitPos);
336             firstDigitPos = trimmed.lastIndexOf('.', spacePos) + 1;
337         }
338         if (spacePos < 0)
339             return -1;
340         base = 10;
341         portString = trimmed.mid(firstDigitPos, spacePos - firstDigitPos);
342         if (portString == "*")
343             return -1;
344     } else {
345         // Expected output on Linux something like
346         //
347         //   sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt ...
348         //   0: 00000000:2805 00000000:0000 0A 00000000:00000000 00:00000000 00000000  ...
349         //
350         const int firstColonPos = trimmed.indexOf(':');
351         if (firstColonPos < 0)
352             return -1;
353         const int secondColonPos = trimmed.indexOf(':', firstColonPos + 1);
354         if (secondColonPos < 0)
355             return -1;
356         const int spacePos = trimmed.indexOf(' ', secondColonPos + 1);
357         if (spacePos < 0)
358             return -1;
359         const int len = spacePos - secondColonPos - 1;
360         base = 16;
361         portString = trimmed.mid(secondColonPos + 1, len);
362     }
363 
364     bool ok = true;
365     const int port = portString.toInt(&ok, base);
366     if (!ok) {
367         qWarning("%s: Unexpected string '%s' is not a port. Tried to read from '%s'",
368                 Q_FUNC_INFO, line.data(), portString.data());
369         return -1;
370     }
371     return port;
372 }
373 
caseFriendlyCompare(const QString & a,const QString & b)374 int caseFriendlyCompare(const QString &a, const QString &b)
375 {
376     int result = a.compare(b, Qt::CaseInsensitive);
377     if (result != 0)
378         return result;
379     return a.compare(b, Qt::CaseSensitive);
380 }
381 
quoteAmpersands(const QString & text)382 QString quoteAmpersands(const QString &text)
383 {
384     QString result = text;
385     return result.replace("&", "&&");
386 }
387 
formatElapsedTime(qint64 elapsed)388 QString formatElapsedTime(qint64 elapsed)
389 {
390     elapsed += 500; // round up
391     const QString format = QString::fromLatin1(elapsed >= 3600000 ? "h:mm:ss" : "mm:ss");
392     const QString time = QTime(0, 0).addMSecs(elapsed).toString(format);
393     return QCoreApplication::translate("StringUtils", "Elapsed time: %1.").arg(time);
394 }
395 
396 /*
397  * Basically QRegularExpression::wildcardToRegularExpression(), but let wildcards match
398  * path separators as well
399  */
wildcardToRegularExpression(const QString & original)400 QString wildcardToRegularExpression(const QString &original)
401 {
402 #if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
403     using qsizetype = int;
404 #endif
405     const qsizetype wclen = original.size();
406     QString rx;
407     rx.reserve(wclen + wclen / 16);
408     qsizetype i = 0;
409     const QChar *wc = original.data();
410 
411     const QLatin1String starEscape(".*");
412     const QLatin1String questionMarkEscape(".");
413 
414     while (i < wclen) {
415         const QChar c = wc[i++];
416         switch (c.unicode()) {
417         case '*':
418             rx += starEscape;
419             break;
420         case '?':
421             rx += questionMarkEscape;
422             break;
423         case '\\':
424         case '$':
425         case '(':
426         case ')':
427         case '+':
428         case '.':
429         case '^':
430         case '{':
431         case '|':
432         case '}':
433             rx += QLatin1Char('\\');
434             rx += c;
435             break;
436         case '[':
437             rx += c;
438             // Support for the [!abc] or [!a-c] syntax
439             if (i < wclen) {
440                 if (wc[i] == QLatin1Char('!')) {
441                     rx += QLatin1Char('^');
442                     ++i;
443                 }
444 
445                 if (i < wclen && wc[i] == QLatin1Char(']'))
446                     rx += wc[i++];
447 
448                 while (i < wclen && wc[i] != QLatin1Char(']')) {
449                     if (wc[i] == QLatin1Char('\\'))
450                         rx += QLatin1Char('\\');
451                     rx += wc[i++];
452                 }
453             }
454             break;
455         default:
456             rx += c;
457             break;
458         }
459     }
460 
461 #if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
462     return QRegularExpression::anchoredPattern(rx);
463 #else
464     return "\\A" + rx + "\\z";
465 #endif
466 }
467 } // namespace Utils
468