1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the tools applications of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
21 | ** included in the packaging of this file. Please review the following |
22 | ** information to ensure the GNU General Public License requirements will |
23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
24 | ** |
25 | ** $QT_END_LICENSE$ |
26 | ** |
27 | ****************************************************************************/ |
28 | |
29 | #include "scanner.h" |
30 | #include "logging.h" |
31 | |
32 | #include <QtCore/qdir.h> |
33 | #include <QtCore/qjsonarray.h> |
34 | #include <QtCore/qjsondocument.h> |
35 | #include <QtCore/qjsonobject.h> |
36 | #include <QtCore/qregexp.h> |
37 | #include <QtCore/qtextstream.h> |
38 | #include <QtCore/qvariant.h> |
39 | |
40 | #include <iostream> |
41 | |
42 | namespace Scanner { |
43 | |
44 | static void missingPropertyWarning(const QString &filePath, const QString &property) |
45 | { |
46 | std::cerr << qPrintable(tr("File %1: Missing mandatory property '%2'." ).arg( |
47 | QDir::toNativeSeparators(filePath), property)) << std::endl; |
48 | } |
49 | |
50 | static void validatePackage(Package &p, const QString &filePath, LogLevel logLevel) |
51 | { |
52 | if (p.qtParts.isEmpty()) |
53 | p.qtParts << QStringLiteral("libs" ); |
54 | |
55 | if (logLevel != SilentLog) { |
56 | if (p.name.isEmpty()) { |
57 | if (p.id.startsWith(s: QLatin1String("chromium-" ))) // Ignore invalid README.chromium files |
58 | return; |
59 | |
60 | missingPropertyWarning(filePath, QStringLiteral("Name" )); |
61 | } |
62 | |
63 | if (p.id.isEmpty()) |
64 | missingPropertyWarning(filePath, QStringLiteral("Id" )); |
65 | if (p.license.isEmpty()) |
66 | missingPropertyWarning(filePath, QStringLiteral("License" )); |
67 | |
68 | for (const QString &part : qAsConst(t&: p.qtParts)) { |
69 | if (part != QLatin1String("examples" ) |
70 | && part != QLatin1String("tests" ) |
71 | && part != QLatin1String("tools" ) |
72 | && part != QLatin1String("libs" ) |
73 | && logLevel != SilentLog) { |
74 | std::cerr << qPrintable(tr("File %1: Property 'QtPart' contains unknown element " |
75 | "'%2'. Valid entries are 'examples', 'tests', 'tools' " |
76 | "and 'libs'." ).arg( |
77 | QDir::toNativeSeparators(filePath), part)) |
78 | << std::endl; |
79 | } |
80 | } |
81 | } |
82 | } |
83 | |
84 | // Transforms a JSON object into a Package object |
85 | static Package readPackage(const QJsonObject &object, const QString &filePath, LogLevel logLevel) |
86 | { |
87 | Package p; |
88 | const QString directory = QFileInfo(filePath).absolutePath(); |
89 | p.path = directory; |
90 | |
91 | for (auto iter = object.constBegin(); iter != object.constEnd(); ++iter) { |
92 | const QString key = iter.key(); |
93 | |
94 | if (!iter.value().isString() && key != QLatin1String("QtParts" )) { |
95 | if (logLevel != SilentLog) |
96 | std::cerr << qPrintable(tr("File %1: Expected JSON string as value of %2." ).arg( |
97 | QDir::toNativeSeparators(filePath), key)) << std::endl; |
98 | continue; |
99 | } |
100 | const QString value = iter.value().toString(); |
101 | if (key == QLatin1String("Name" )) { |
102 | p.name = value; |
103 | } else if (key == QLatin1String("Path" )) { |
104 | p.path = QDir(directory).absoluteFilePath(fileName: value); |
105 | } else if (key == QLatin1String("Files" )) { |
106 | p.files = value.split(sep: QRegExp(QStringLiteral("\\s" )), behavior: Qt::SkipEmptyParts); |
107 | } else if (key == QLatin1String("Id" )) { |
108 | p.id = value; |
109 | } else if (key == QLatin1String("Homepage" )) { |
110 | p.homepage = value; |
111 | } else if (key == QLatin1String("Version" )) { |
112 | p.version = value; |
113 | } else if (key == QLatin1String("DownloadLocation" )) { |
114 | p.downloadLocation = value; |
115 | } else if (key == QLatin1String("License" )) { |
116 | p.license = value; |
117 | } else if (key == QLatin1String("LicenseId" )) { |
118 | p.licenseId = value; |
119 | } else if (key == QLatin1String("LicenseFile" )) { |
120 | p.licenseFile = QDir(directory).absoluteFilePath(fileName: value); |
121 | } else if (key == QLatin1String("Copyright" )) { |
122 | p.copyright = value; |
123 | } else if (key == QLatin1String("PackageComment" )) { |
124 | p.packageComment = value; |
125 | } else if (key == QLatin1String("QDocModule" )) { |
126 | p.qdocModule = value; |
127 | } else if (key == QLatin1String("Description" )) { |
128 | p.description = value; |
129 | } else if (key == QLatin1String("QtUsage" )) { |
130 | p.qtUsage = value; |
131 | } else if (key == QLatin1String("QtParts" )) { |
132 | const QVariantList variantList = iter.value().toArray().toVariantList(); |
133 | for (const QVariant &v: variantList) { |
134 | if (v.type() != QVariant::String && logLevel != SilentLog) { |
135 | std::cerr << qPrintable(tr("File %1: Expected JSON string in array of %2." ).arg( |
136 | QDir::toNativeSeparators(filePath), key)) |
137 | << std::endl; |
138 | } |
139 | p.qtParts.append(t: v.toString()); |
140 | } |
141 | } else { |
142 | if (logLevel != SilentLog) |
143 | std::cerr << qPrintable(tr("File %1: Unknown key %2." ).arg( |
144 | QDir::toNativeSeparators(filePath), key)) << std::endl; |
145 | } |
146 | } |
147 | |
148 | validatePackage(p, filePath, logLevel); |
149 | |
150 | return p; |
151 | } |
152 | |
153 | // Parses a package's details from a README.chromium file |
154 | static Package parseChromiumFile(QFile &file, const QString &filePath, LogLevel logLevel) |
155 | { |
156 | const QString directory = QFileInfo(filePath).absolutePath(); |
157 | |
158 | // Parse the fields in the file |
159 | QHash<QString, QString> fields; |
160 | |
161 | QTextStream in(&file); |
162 | while (!in.atEnd()) { |
163 | QString line = in.readLine().trimmed(); |
164 | QStringList parts = line.split(QStringLiteral(":" )); |
165 | |
166 | if (parts.count() < 2) |
167 | continue; |
168 | |
169 | QString key = parts.at(i: 0); |
170 | parts.removeFirst(); |
171 | QString value = parts.join(sep: QString()).trimmed(); |
172 | |
173 | fields[key] = value; |
174 | |
175 | if (line == QLatin1String("Description:" )) { // special field : should handle multi-lines values |
176 | while (!in.atEnd()) { |
177 | QString line = in.readLine().trimmed(); |
178 | |
179 | if (line.startsWith(s: QLatin1String("Local Modifications:" ))) // Don't include this part |
180 | break; |
181 | |
182 | fields[key] += line + QStringLiteral("\n" ); |
183 | } |
184 | |
185 | break; |
186 | } |
187 | } |
188 | |
189 | // Construct the Package object |
190 | Package p; |
191 | |
192 | QString shortName = fields.contains(akey: QLatin1String("Short Name" )) |
193 | ? fields[QLatin1String("Short Name" )] |
194 | : fields[QLatin1String("Name" )]; |
195 | QString version = fields[QStringLiteral("Version" )]; |
196 | |
197 | p.id = QStringLiteral("chromium-" ) + shortName.toLower().replace(c: QChar::Space, QStringLiteral("-" )); |
198 | p.name = fields[QStringLiteral("Name" )]; |
199 | if (version != QLatin1Char('0')) // "0" : not applicable |
200 | p.version = version; |
201 | p.license = fields[QStringLiteral("License" )]; |
202 | p.homepage = fields[QStringLiteral("URL" )]; |
203 | p.qdocModule = QStringLiteral("qtwebengine" ); |
204 | p.qtUsage = QStringLiteral("Used in Qt WebEngine" ); |
205 | p.description = fields[QStringLiteral("Description" )].trimmed(); |
206 | p.path = directory; |
207 | |
208 | QString licenseFile = fields[QStringLiteral("License File" )]; |
209 | if (licenseFile != QString() && licenseFile != QLatin1String("NOT_SHIPPED" )) { |
210 | p.licenseFile = QDir(directory).absoluteFilePath(fileName: licenseFile); |
211 | } else { |
212 | // Look for a LICENSE or COPYING file as a fallback |
213 | QDir dir = directory; |
214 | |
215 | dir.setNameFilters({ QStringLiteral("LICENSE" ), QStringLiteral("COPYING" ) }); |
216 | dir.setFilter(QDir::Files | QDir::NoDotAndDotDot); |
217 | |
218 | const QFileInfoList entries = dir.entryInfoList(); |
219 | if (!entries.empty()) |
220 | p.licenseFile = entries.at(i: 0).absoluteFilePath(); |
221 | } |
222 | |
223 | validatePackage(p, filePath, logLevel); |
224 | |
225 | return p; |
226 | } |
227 | |
228 | QVector<Package> readFile(const QString &filePath, LogLevel logLevel) |
229 | { |
230 | QVector<Package> packages; |
231 | |
232 | if (logLevel == VerboseLog) { |
233 | std::cerr << qPrintable(tr("Reading file %1..." ).arg( |
234 | QDir::toNativeSeparators(filePath))) << std::endl; |
235 | } |
236 | QFile file(filePath); |
237 | if (!file.open(flags: QIODevice::ReadOnly | QIODevice::Text)) { |
238 | if (logLevel != SilentLog) |
239 | std::cerr << qPrintable(tr("Could not open file %1." ).arg( |
240 | QDir::toNativeSeparators(file.fileName()))) << std::endl; |
241 | return QVector<Package>(); |
242 | } |
243 | |
244 | if (filePath.endsWith(s: QLatin1String(".json" ))) { |
245 | QJsonParseError jsonParseError; |
246 | const QJsonDocument document = QJsonDocument::fromJson(json: file.readAll(), error: &jsonParseError); |
247 | if (document.isNull()) { |
248 | if (logLevel != SilentLog) |
249 | std::cerr << qPrintable(tr("Could not parse file %1: %2" ).arg( |
250 | QDir::toNativeSeparators(file.fileName()), |
251 | jsonParseError.errorString())) |
252 | << std::endl; |
253 | return QVector<Package>(); |
254 | } |
255 | |
256 | if (document.isObject()) { |
257 | packages << readPackage(object: document.object(), filePath: file.fileName(), logLevel); |
258 | } else if (document.isArray()) { |
259 | QJsonArray array = document.array(); |
260 | for (int i = 0, size = array.size(); i < size; ++i) { |
261 | QJsonValue value = array.at(i); |
262 | if (value.isObject()) { |
263 | packages << readPackage(object: value.toObject(), filePath: file.fileName(), logLevel); |
264 | } else { |
265 | if (logLevel != SilentLog) |
266 | std::cerr << qPrintable(tr("File %1: Expecting JSON object in array." ) |
267 | .arg(QDir::toNativeSeparators(file.fileName()))) |
268 | << std::endl; |
269 | } |
270 | } |
271 | } else { |
272 | if (logLevel != SilentLog) |
273 | std::cerr << qPrintable(tr("File %1: Expecting JSON object in array." ).arg( |
274 | QDir::toNativeSeparators(file.fileName()))) << std::endl; |
275 | } |
276 | } else if (filePath.endsWith(s: QLatin1String(".chromium" ))) { |
277 | Package chromiumPackage = parseChromiumFile(file, filePath, logLevel); |
278 | if (!chromiumPackage.name.isEmpty()) // Skip invalid README.chromium files |
279 | packages << chromiumPackage; |
280 | } else { |
281 | if (logLevel != SilentLog) |
282 | std::cerr << qPrintable(tr("File %1: Unsupported file type." ) |
283 | .arg(QDir::toNativeSeparators(file.fileName()))) |
284 | << std::endl; |
285 | } |
286 | |
287 | return packages; |
288 | } |
289 | |
290 | QVector<Package> scanDirectory(const QString &directory, InputFormats inputFormats, LogLevel logLevel) |
291 | { |
292 | QDir dir(directory); |
293 | QVector<Package> packages; |
294 | |
295 | QStringList nameFilters = QStringList(); |
296 | if (inputFormats & InputFormat::QtAttributions) |
297 | nameFilters << QStringLiteral("qt_attribution.json" ); |
298 | if (inputFormats & InputFormat::ChromiumAttributions) |
299 | nameFilters << QStringLiteral("README.chromium" ); |
300 | if (qEnvironmentVariableIsSet(varName: "QT_ATTRIBUTIONSSCANNER_TEST" )) { |
301 | nameFilters |
302 | << QStringLiteral("qt_attribution_test.json" ) |
303 | << QStringLiteral("README_test.chromium" ); |
304 | } |
305 | |
306 | dir.setNameFilters(nameFilters); |
307 | dir.setFilter(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::Files); |
308 | |
309 | const QFileInfoList entries = dir.entryInfoList(); |
310 | for (const QFileInfo &info : entries) { |
311 | if (info.isDir()) { |
312 | packages += scanDirectory(directory: info.filePath(), inputFormats, logLevel); |
313 | } else { |
314 | packages += readFile(filePath: info.filePath(), logLevel); |
315 | } |
316 | } |
317 | |
318 | return packages; |
319 | } |
320 | |
321 | } // namespace Scanner |
322 | |