| 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 | |