1 | // Copyright (C) 2021 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 |
3 | |
4 | #include <QtCore/qprocess.h> |
5 | #include <QCryptographicHash> |
6 | #include "location.h" |
7 | #include "utilities.h" |
8 | |
9 | QT_BEGIN_NAMESPACE |
10 | |
11 | Q_LOGGING_CATEGORY(lcQdoc, "qt.qdoc" ) |
12 | Q_LOGGING_CATEGORY(lcQdocClang, "qt.qdoc.clang" ) |
13 | |
14 | /*! |
15 | \namespace Utilities |
16 | \internal |
17 | \brief This namespace holds QDoc-internal utility methods. |
18 | */ |
19 | namespace Utilities { |
20 | static inline void setDebugEnabled(bool value) |
21 | { |
22 | const_cast<QLoggingCategory &>(lcQdoc()).setEnabled(type: QtDebugMsg, enable: value); |
23 | const_cast<QLoggingCategory &>(lcQdocClang()).setEnabled(type: QtDebugMsg, enable: value); |
24 | } |
25 | |
26 | void startDebugging(const QString &message) |
27 | { |
28 | setDebugEnabled(true); |
29 | qCDebug(lcQdoc, "START DEBUGGING: %ls" , qUtf16Printable(message)); |
30 | } |
31 | |
32 | void stopDebugging(const QString &message) |
33 | { |
34 | qCDebug(lcQdoc, "STOP DEBUGGING: %ls" , qUtf16Printable(message)); |
35 | setDebugEnabled(false); |
36 | } |
37 | |
38 | bool debugging() |
39 | { |
40 | return lcQdoc().isEnabled(type: QtDebugMsg); |
41 | } |
42 | |
43 | /*! |
44 | \internal |
45 | Convenience method that's used to get the correct punctuation character for |
46 | the words at \a wordPosition in a list of \a numberOfWords length. |
47 | For the last position in the list, returns "." (full stop). For any other |
48 | word, this method calls comma(). |
49 | |
50 | \sa comma() |
51 | */ |
52 | QString separator(qsizetype wordPosition, qsizetype numberOfWords) |
53 | { |
54 | static QString terminator = QStringLiteral("." ); |
55 | if (wordPosition == numberOfWords - 1) |
56 | return terminator; |
57 | else |
58 | return comma(wordPosition, numberOfWords); |
59 | } |
60 | |
61 | /*! |
62 | \internal |
63 | Convenience method that's used to get the correct punctuation character for |
64 | the words at \a wordPosition in a list of \a numberOfWords length. |
65 | |
66 | For a list of length one, returns an empty QString. For a list of length |
67 | two, returns the string " and ". For any length beyond two, returns the |
68 | string ", " until the last element, which returns ", and ". |
69 | |
70 | \sa comma() |
71 | */ |
72 | QString comma(qsizetype wordPosition, qsizetype numberOfWords) |
73 | { |
74 | if (wordPosition == numberOfWords - 1) |
75 | return QString(); |
76 | if (numberOfWords == 2) |
77 | return QStringLiteral(" and " ); |
78 | if (wordPosition == 0 || wordPosition < numberOfWords - 2) |
79 | return QStringLiteral(", " ); |
80 | return QStringLiteral(", and " ); |
81 | } |
82 | |
83 | /*! |
84 | \brief Returns an ascii-printable representation of \a str. |
85 | |
86 | Replace non-ascii-printable characters in \a str from a subset of such |
87 | characters. The subset includes alphanumeric (alnum) characters |
88 | ([a-zA-Z0-9]), space, punctuation characters, and common symbols. Non-alnum |
89 | characters in this subset are replaced by a single hyphen. Leading, |
90 | trailing, and consecutive hyphens are removed, such that the resulting |
91 | string does not start or end with a hyphen. All characters are converted to |
92 | lowercase. |
93 | |
94 | If any character in \a str is non-latin, or latin and not found in the |
95 | aforementioned subset (e.g. 'ß', 'å', or 'ö'), a hash of \a str is appended |
96 | to the final string. |
97 | |
98 | Returns a string that is normalized for use where ascii-printable strings |
99 | are required, such as file names or fragment identifiers in URLs. |
100 | |
101 | The implementation is equivalent to: |
102 | |
103 | \code |
104 | name.replace(QRegularExpression("[^A-Za-z0-9]+"), " "); |
105 | name = name.simplified(); |
106 | name.replace(QLatin1Char(' '), QLatin1Char('-')); |
107 | name = name.toLower(); |
108 | \endcode |
109 | |
110 | However, it has been measured to be approximately four times faster. |
111 | */ |
112 | QString asAsciiPrintable(const QString &str) |
113 | { |
114 | auto legal_ascii = [](const uint value) { |
115 | const uint start_ascii_subset{ 32 }; |
116 | const uint end_ascii_subset{ 126 }; |
117 | |
118 | return value >= start_ascii_subset && value <= end_ascii_subset; |
119 | }; |
120 | |
121 | QString result; |
122 | bool begun = false; |
123 | bool has_non_alnum_content{ false }; |
124 | |
125 | for (const auto &c : str) { |
126 | char16_t u = c.unicode(); |
127 | if (!legal_ascii(u)) |
128 | has_non_alnum_content = true; |
129 | if (u >= 'A' && u <= 'Z') |
130 | u += 'a' - 'A'; |
131 | if ((u >= 'a' && u <= 'z') || (u >= '0' && u <= '9')) { |
132 | result += QLatin1Char(u); |
133 | begun = true; |
134 | } else if (begun) { |
135 | result += QLatin1Char('-'); |
136 | begun = false; |
137 | } |
138 | } |
139 | if (result.endsWith(c: QLatin1Char('-'))) |
140 | result.chop(n: 1); |
141 | |
142 | if (has_non_alnum_content) { |
143 | auto title_hash = QString::fromLocal8Bit( |
144 | ba: QCryptographicHash::hash(data: str.toUtf8(), method: QCryptographicHash::Md5).toHex()); |
145 | title_hash.truncate(pos: 8); |
146 | if (!result.isEmpty()) |
147 | result.append(c: QLatin1Char('-')); |
148 | result.append(s: title_hash); |
149 | } |
150 | |
151 | return result; |
152 | } |
153 | |
154 | /*! |
155 | \internal |
156 | */ |
157 | static bool runProcess(const QString &program, const QStringList &arguments, |
158 | QByteArray *stdOutIn, QByteArray *stdErrIn) |
159 | { |
160 | QProcess process; |
161 | process.start(program, arguments, mode: QProcess::ReadWrite); |
162 | if (!process.waitForStarted()) { |
163 | qCDebug(lcQdoc).nospace() << "Unable to start " << process.program() |
164 | << ": " << process.errorString(); |
165 | return false; |
166 | } |
167 | process.closeWriteChannel(); |
168 | const bool finished = process.waitForFinished(); |
169 | const QByteArray stdErr = process.readAllStandardError(); |
170 | if (stdErrIn) |
171 | *stdErrIn = stdErr; |
172 | if (stdOutIn) |
173 | *stdOutIn = process.readAllStandardOutput(); |
174 | |
175 | if (!finished) { |
176 | qCDebug(lcQdoc).nospace() << process.program() << " timed out: " << stdErr; |
177 | process.kill(); |
178 | return false; |
179 | } |
180 | |
181 | if (process.exitStatus() != QProcess::NormalExit) { |
182 | qCDebug(lcQdoc).nospace() << process.program() << " crashed: " << stdErr; |
183 | return false; |
184 | } |
185 | |
186 | if (process.exitCode() != 0) { |
187 | qCDebug(lcQdoc).nospace() << process.program() << " exited with " |
188 | << process.exitCode() << ": " << stdErr; |
189 | return false; |
190 | } |
191 | |
192 | return true; |
193 | } |
194 | |
195 | /*! |
196 | \internal |
197 | */ |
198 | static QByteArray frameworkSuffix() { |
199 | return QByteArrayLiteral(" (framework directory)" ); |
200 | } |
201 | |
202 | /*! |
203 | \internal |
204 | Determine the compiler's internal include paths from the output of |
205 | |
206 | \badcode |
207 | [clang++|g++] -E -x c++ - -v </dev/null |
208 | \endcode |
209 | |
210 | Output looks like: |
211 | |
212 | \badcode |
213 | #include <...> search starts here: |
214 | /usr/local/include |
215 | /System/Library/Frameworks (framework directory) |
216 | End of search list. |
217 | \endcode |
218 | */ |
219 | QStringList getInternalIncludePaths(const QString &compiler) |
220 | { |
221 | QStringList result; |
222 | QStringList arguments; |
223 | arguments << QStringLiteral("-E" ) << QStringLiteral("-x" ) << QStringLiteral("c++" ) |
224 | << QStringLiteral("-" ) << QStringLiteral("-v" ); |
225 | QByteArray stdOut; |
226 | QByteArray stdErr; |
227 | if (!runProcess(program: compiler, arguments, stdOutIn: &stdOut, stdErrIn: &stdErr)) |
228 | return result; |
229 | const QByteArrayList stdErrLines = stdErr.split(sep: '\n'); |
230 | bool isIncludeDir = false; |
231 | for (const QByteArray &line : stdErrLines) { |
232 | if (isIncludeDir) { |
233 | if (line.startsWith(QByteArrayLiteral("End of search list" ))) { |
234 | isIncludeDir = false; |
235 | } else { |
236 | QByteArray prefix("-I" ); |
237 | QByteArray {line.trimmed()}; |
238 | if (headerPath.endsWith(bv: frameworkSuffix())) { |
239 | headerPath.truncate(pos: headerPath.size() - frameworkSuffix().size()); |
240 | prefix = QByteArrayLiteral("-F" ); |
241 | } |
242 | result.append(t: QString::fromLocal8Bit(ba: prefix + headerPath)); |
243 | } |
244 | } else if (line.startsWith(QByteArrayLiteral("#include <...> search starts here" ))) { |
245 | isIncludeDir = true; |
246 | } |
247 | } |
248 | |
249 | return result; |
250 | } |
251 | |
252 | } // namespace Utilities |
253 | |
254 | QT_END_NAMESPACE |
255 | |