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