Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
scanner.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 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 "scanner.h"
5#include "logging.h"
6
7#include <QtCore/qdir.h>
8#include <QtCore/qhash.h>
9#include <QtCore/qjsonarray.h>
10#include <QtCore/qjsondocument.h>
11#include <QtCore/qjsonobject.h>
12#include <QtCore/qtextstream.h>
13#include <QtCore/qvariant.h>
14
15#include <iostream>
16
17using namespace Qt::Literals::StringLiterals;
18
19namespace Scanner {
20
21static void missingPropertyWarning(const QString &filePath, const QString &property)
22{
23 std::cerr << qPrintable(tr("File %1: Missing mandatory property '%2'.").arg(
24 QDir::toNativeSeparators(filePath), property)) << std::endl;
25}
26
27bool validatePackage(Package &p, Checks checks, LogLevel logLevel)
28{
29 const auto &filePath = p.filePath;
30 bool validPackage = true;
31
32 if (filePath.isEmpty()) {
33 std::cerr << qPrintable(tr("The origin file of package '%1' was not recorded.")
34 .arg(p.id));
35 return false;
36 }
37
38 if (p.qtParts.isEmpty())
39 p.qtParts << u"libs"_s;
40
41 if (p.name.isEmpty()) {
42 if (p.id.startsWith("chromium-"_L1)) // Ignore invalid README.chromium files
43 return false;
44
45 if (logLevel != SilentLog)
46 missingPropertyWarning(filePath, u"Name"_s);
47 validPackage = false;
48 }
49
50 if (p.id.isEmpty()) {
51 if (logLevel != SilentLog)
52 missingPropertyWarning(filePath, u"Id"_s);
53 validPackage = false;
54 } else if (!p.id.isLower() || p.id.contains(' '_L1)) {
55 if (logLevel != SilentLog)
56 std::cerr << qPrintable(tr("File %1: Value of 'Id' must be in lowercase and without spaces.")
57 .arg(QDir::toNativeSeparators(filePath))) << std::endl;
58 validPackage = false;
59 }
60
61 if (p.license.isEmpty()) {
62 if (logLevel != SilentLog)
63 missingPropertyWarning(filePath, u"License"_s);
64 validPackage = false;
65 }
66
67 if (!p.copyright.isEmpty() && !p.copyrightFile.isEmpty()) {
68 if (logLevel != SilentLog) {
69 std::cerr << qPrintable(tr("File %1: Properties 'Copyright' and 'CopyrightFile' are "
70 "mutually exclusive.")
71 .arg(QDir::toNativeSeparators(filePath)))
72 << std::endl;
73 }
74 validPackage = false;
75 }
76
77 if (p.securityCritical && p.downloadLocation.isEmpty()) {
78 if (logLevel != SilentLog)
79 missingPropertyWarning(filePath, u"DownloadLocation"_s);
80 validPackage = false;
81 }
82
83 for (const QString &part : std::as_const(p.qtParts)) {
84 if (part != "examples"_L1 && part != "tests"_L1
85 && part != "tools"_L1 && part != "libs"_L1) {
86
87 if (logLevel != SilentLog) {
88 std::cerr << qPrintable(tr("File %1: Property 'QtPart' contains unknown element "
89 "'%2'. Valid entries are 'examples', 'tests', 'tools' "
90 "and 'libs'.").arg(
91 QDir::toNativeSeparators(filePath), part))
92 << std::endl;
93 }
94 validPackage = false;
95 }
96 }
97
98 if (!(checks & Check::Paths))
99 return validPackage;
100
101 const QDir dir = p.path;
102 if (!dir.exists()) {
103 std::cerr << qPrintable(
104 tr("File %1: Directory '%2' does not exist.")
105 .arg(QDir::toNativeSeparators(filePath), QDir::toNativeSeparators(p.path)))
106 << std::endl;
107 validPackage = false;
108 } else {
109 for (const QString &file : std::as_const(p.files)) {
110 if (!dir.exists(file)) {
111 if (logLevel != SilentLog) {
112 std::cerr << qPrintable(
113 tr("File %1: Path '%2' does not exist in directory '%3'.")
114 .arg(QDir::toNativeSeparators(filePath),
115 QDir::toNativeSeparators(file),
116 QDir::toNativeSeparators(p.path)))
117 << std::endl;
118 }
119 validPackage = false;
120 }
121 }
122 }
123
124 return validPackage;
125}
126
127static std::optional<QStringList> toStringList(const QJsonValue &value)
128{
129 if (!value.isArray())
130 return std::nullopt;
131 QStringList result;
132 for (const auto &iter : value.toArray()) {
133 if (iter.type() != QJsonValue::String)
134 return std::nullopt;
135 result.push_back(iter.toString());
136 }
137 return result;
138}
139
140static std::optional<QString> arrayToMultiLineString(const QJsonValue &value)
141{
142 if (!value.isArray())
143 return std::nullopt;
144 QString result;
145 for (const auto &iter : value.toArray()) {
146 if (iter.type() != QJsonValue::String)
147 return std::nullopt;
148 result.append(iter.toString());
149 result.append(QLatin1StringView("\n"));
150 }
151 return result;
152}
153
154// Extracts SPDX license ids from a SPDX license expression.
155// For "(BSD-3-Clause AND BeerWare)" this function returns { "BSD-3-Clause", "BeerWare" }.
157{
158 const QStringList spdxOperators = {
159 u"AND"_s,
160 u"OR"_s,
161 u"WITH"_s
162 };
163
164 // Replace parentheses with spaces. We're not interested in grouping.
165 const QRegularExpression parensRegex(u"[()]"_s);
166 expression.replace(parensRegex, u" "_s);
167
168 // Split the string at space boundaries to extract tokens.
169 QStringList result;
170 for (const QString &token : expression.split(QLatin1Char(' '), Qt::SkipEmptyParts)) {
171 if (spdxOperators.contains(token))
172 continue;
173
174 // Remove the unary + operator, if present.
175 if (token.endsWith(QLatin1Char('+')))
176 result.append(token.mid(0, token.size() - 1));
177 else
178 result.append(token);
179 }
180 return result;
181}
182
183// Starting at packageDir, look for a LICENSES subdirectory in the directory hierarchy upwards.
184// Return a default-constructed QString if the directory was not found.
185static QString locateLicensesDir(const QString &packageDir)
186{
187 static const QString licensesSubDir = u"LICENSES"_s;
188 QDir dir(packageDir);
189 while (true) {
190 if (!dir.exists())
191 break;
192 if (dir.cd(licensesSubDir))
193 return dir.path();
194 if (dir.isRoot() || !dir.cdUp())
195 break;
196 }
197 return {};
198}
199
200// Locates the license files that belong to the licenses mentioned in LicenseId and stores them in
201// the specified package object.
203{
204 const QString licensesDirPath = locateLicensesDir(p.path);
205 const QStringList licenseIds = extractLicenseIdsFromSPDXExpression(p.licenseId);
206
207 bool success = true;
208 QDir licensesDir(licensesDirPath);
209 QDir licensesDirLocal = p.path;
210 for (const QString &id : licenseIds) {
211 QString fileName = id + u".txt";
212 QString fileNameLocal = u"LICENSE." + id + u".txt";
213
214 if (licensesDirLocal.exists(fileNameLocal)) {
215 p.licenseFiles.append(licensesDirLocal.filePath(fileNameLocal));
216 } else if (licensesDir.exists(fileName)) {
217 p.licenseFiles.append(licensesDir.filePath(fileName));
218 } else {
219 std::cerr << qPrintable(tr("Missing expected license file:")) << std::endl;
220 std::cerr << qPrintable(QDir::toNativeSeparators(licensesDirLocal.filePath(fileNameLocal)))
221 << std::endl;
222 if (!licensesDirPath.isEmpty()) {
223 std::cerr << qPrintable(tr("or\n %1").arg(
224 QDir::toNativeSeparators(licensesDir.filePath(fileName))))
225 << std::endl;
226 }
227 success = false;
228 }
229 }
230
231 return success;
232}
233
234// Tries to interpret a json value either as a string or an array of strings, and assigns the
235// result to outList. Returns true on success, false on failure. On failure, it also conditionally
236// prints an error.
237static bool handleStringOrStringArrayJsonKey(QStringList &outList, const QString &key,
238 QJsonValueConstRef jsonValue, const QString &filePath,
239 LogLevel logLevel)
240{
241 if (jsonValue.isArray()) {
242 auto maybeStringList = toStringList(jsonValue);
243 if (maybeStringList)
244 outList = maybeStringList.value();
245 } else if (jsonValue.isString()) {
246 outList.append(jsonValue.toString());
247 } else {
248 if (logLevel != SilentLog) {
249 std::cerr << qPrintable(tr("File %1: Expected JSON array of strings or "
250 "string as value of %2.").arg(
251 QDir::toNativeSeparators(filePath), key))
252 << std::endl;
253 }
254 return false;
255 }
256 return true;
257}
258
259// Transforms a JSON object into a Package object
260static std::optional<Package> readPackage(const QJsonObject &object, const QString &filePath,
261 LogLevel logLevel)
262{
263 Package p;
264 bool validPackage = true;
265 const QString directory = QFileInfo(filePath).absolutePath();
266 p.filePath = QFileInfo(filePath).absoluteFilePath();
267 p.path = directory;
268
269 for (auto iter = object.constBegin(); iter != object.constEnd(); ++iter) {
270 const QString key = iter.key();
271
272 if (!iter.value().isString() && key != "QtParts"_L1 && key != "SecurityCritical"_L1
273 && key != "Files"_L1 && key != "LicenseFiles"_L1 && key != "Comment"_L1
274 && key != "Copyright"_L1 && key != "CPE"_L1 && key != "PURL"_L1) {
275 if (logLevel != SilentLog)
276 std::cerr << qPrintable(tr("File %1: Expected JSON string as value of %2.").arg(
277 QDir::toNativeSeparators(filePath), key)) << std::endl;
278 validPackage = false;
279 continue;
280 }
281 const QString value = iter.value().toString();
282 if (key == "Name"_L1) {
283 p.name = value;
284 } else if (key == "Path"_L1) {
285 p.path = QDir(directory).absoluteFilePath(value);
286 } else if (key == "Files"_L1) {
287 QJsonValueConstRef jsonValue = iter.value();
288 if (jsonValue.isArray()) {
289 auto maybeStringList = toStringList(jsonValue);
290 if (maybeStringList)
291 p.files = maybeStringList.value();
292 } else if (jsonValue.isString()) {
293 // Legacy format: multiple values separated by space in one string.
294 p.files = value.simplified().split(QLatin1Char(' '), Qt::SkipEmptyParts);
295 } else {
296 if (logLevel != SilentLog) {
297 std::cerr << qPrintable(tr("File %1: Expected JSON array of strings as value "
298 "of Files.").arg(QDir::toNativeSeparators(filePath)))
299 << std::endl;
300 validPackage = false;
301 continue;
302 }
303 }
304 } else if (key == "Comment"_L1) {
305 // Accepted purely to record details of potential interest doing
306 // updates in future. Value is an arbitrary object. Any number of
307 // Comment entries may be present: JSON doesn't require names to be
308 // unique, albeit some linters may kvetch.
309 } else if (key == "Id"_L1) {
310 p.id = value;
311 } else if (key == "Homepage"_L1) {
312 p.homepage = value;
313 } else if (key == "Version"_L1) {
314 p.version = value;
315 } else if (key == "DownloadLocation"_L1) {
316 p.downloadLocation = value;
317 } else if (key == "License"_L1) {
318 p.license = value;
319 } else if (key == "LicenseId"_L1) {
320 p.licenseId = value;
321 } else if (key == "LicenseFile"_L1) {
322 p.licenseFiles = QStringList(QDir(directory).absoluteFilePath(value));
323 } else if (key == "LicenseFiles"_L1) {
324 auto strings = toStringList(iter.value());
325 if (!strings) {
326 if (logLevel != SilentLog)
327 std::cerr << qPrintable(tr("File %1: Expected JSON array of strings in %2.")
328 .arg(QDir::toNativeSeparators(filePath), key))
329 << std::endl;
330 validPackage = false;
331 continue;
332 }
333 const QDir dir(directory);
334 for (const auto &iter : std::as_const(strings.value()))
335 p.licenseFiles.push_back(dir.absoluteFilePath(iter));
336 } else if (key == "Copyright"_L1) {
337 QJsonValueConstRef jsonValue = iter.value();
338 if (jsonValue.isArray()) {
339 // Array joined with new lines
340 auto maybeString = arrayToMultiLineString(jsonValue);
341 if (maybeString)
342 p.copyright = maybeString.value();
343 } else if (jsonValue.isString()) {
344 // Legacy format: multiple values separated by space in one string.
345 p.copyright = value;
346 } else {
347 if (logLevel != SilentLog) {
348 std::cerr << qPrintable(tr("File %1: Expected JSON array of strings or "
349 "string as value of %2.").arg(
350 QDir::toNativeSeparators(filePath), key)) << std::endl;
351 validPackage = false;
352 continue;
353 }
354 }
355 } else if (key == "CPE"_L1) {
356 const QJsonValueConstRef jsonValue = iter.value();
357 if (!handleStringOrStringArrayJsonKey(p.cpeList, key, jsonValue, filePath, logLevel)) {
358 validPackage = false;
359 continue;
360 }
361 } else if (key == "PURL"_L1) {
362 const QJsonValueConstRef jsonValue = iter.value();
363 if (!handleStringOrStringArrayJsonKey(p.purlList, key, jsonValue, filePath, logLevel)) {
364 validPackage = false;
365 continue;
366 }
367 } else if (key == "CopyrightFile"_L1) {
368 p.copyrightFile = QDir(directory).absoluteFilePath(value);
369 } else if (key == "PackageComment"_L1) {
370 p.packageComment = value;
371 } else if (key == "QDocModule"_L1) {
372 p.qdocModule = value;
373 } else if (key == "Description"_L1) {
374 p.description = value;
375 } else if (key == "QtUsage"_L1) {
376 p.qtUsage = value;
377 } else if (key == "SecurityCritical"_L1) {
378 if (!iter.value().isBool()) {
379 std::cerr << qPrintable(tr("File %1: Expected JSON boolean in %2.")
380 .arg(QDir::toNativeSeparators(filePath), key))
381 << std::endl;
382 validPackage = false;
383 continue;
384 }
385 p.securityCritical = iter.value().toBool();
386 } else if (key == "QtParts"_L1) {
387 auto parts = toStringList(iter.value());
388 if (!parts) {
389 if (logLevel != SilentLog) {
390 std::cerr << qPrintable(tr("File %1: Expected JSON array of strings in %2.")
391 .arg(QDir::toNativeSeparators(filePath), key))
392 << std::endl;
393 }
394 validPackage = false;
395 continue;
396 }
397 p.qtParts = parts.value();
398 } else {
399 if (logLevel != SilentLog) {
400 std::cerr << qPrintable(tr("File %1: Unknown key %2.").arg(
401 QDir::toNativeSeparators(filePath), key)) << std::endl;
402 }
403 validPackage = false;
404 }
405 }
406
407 if (!p.copyrightFile.isEmpty()) {
408 QFile file(p.copyrightFile);
409 if (!file.open(QIODevice::ReadOnly)) {
410 std::cerr << qPrintable(tr("File %1: Cannot open 'CopyrightFile' %2.\n")
411 .arg(QDir::toNativeSeparators(filePath),
412 QDir::toNativeSeparators(p.copyrightFile)));
413 validPackage = false;
414 }
415 p.copyrightFileContents = QString::fromUtf8(file.readAll());
416 }
417
418 if (p.licenseFiles.isEmpty() && !autoDetectLicenseFiles(p))
419 return std::nullopt;
420
421 for (const QString &licenseFile : std::as_const(p.licenseFiles)) {
422 QFile file(licenseFile);
423 if (!file.open(QIODevice::ReadOnly)) {
424 if (logLevel != SilentLog) {
425 std::cerr << qPrintable(tr("File %1: Cannot open 'LicenseFile' %2.\n")
426 .arg(QDir::toNativeSeparators(filePath),
427 QDir::toNativeSeparators(licenseFile)));
428 }
429 validPackage = false;
430 }
431 p.licenseFilesContents << QString::fromUtf8(file.readAll()).trimmed();
432 }
433
434 if (!validPackage)
435 return std::nullopt;
436
437 return p;
438}
439
440// Parses a package's details from a README.chromium file
441static Package parseChromiumFile(QFile &file, const QString &filePath, LogLevel logLevel)
442{
443 const QString directory = QFileInfo(filePath).absolutePath();
444
445 // Parse the fields in the file
446 QHash<QString, QString> fields;
447
448 QTextStream in(&file);
449 while (!in.atEnd()) {
450 QString line = in.readLine().trimmed();
451 QStringList parts = line.split(u":"_s);
452
453 if (parts.size() < 2)
454 continue;
455
456 QString key = parts.at(0);
457 parts.removeFirst();
458 QString value = parts.join(QString()).trimmed();
459
460 fields[key] = value;
461
462 if (line == "Description:"_L1) { // special field : should handle multi-lines values
463 while (!in.atEnd()) {
464 QString line = in.readLine().trimmed();
465
466 if (line.startsWith("Local Modifications:"_L1)) // Don't include this part
467 break;
468
469 fields[key] += line + u"\n"_s;
470 }
471
472 break;
473 }
474 }
475
476 // Construct the Package object
477 Package p;
478
479 QString shortName = fields.contains("Short Name"_L1)
480 ? fields["Short Name"_L1]
481 : fields["Name"_L1];
482 QString version = fields[u"Version"_s];
483
484 p.filePath = QFileInfo(filePath).absoluteFilePath();
485 p.id = u"chromium-"_s + shortName.toLower().replace(QChar::Space, u"-"_s);
486 p.name = fields[u"Name"_s];
487 if (version != QLatin1Char('0')) // "0" : not applicable
488 p.version = version;
489 p.license = fields[u"License"_s];
490 p.homepage = fields[u"URL"_s];
491 p.qdocModule = u"qtwebengine"_s;
492 p.qtUsage = u"Used in Qt WebEngine"_s;
493 p.description = fields[u"Description"_s].trimmed();
494 p.path = directory;
495
496 QString licenseFile = fields[u"License File"_s];
497 if (licenseFile != QString() && licenseFile != "NOT_SHIPPED"_L1) {
498 p.licenseFiles = QStringList(QDir(directory).absoluteFilePath(licenseFile));
499 } else {
500 // Look for a LICENSE or COPYING file as a fallback
501 QDir dir = directory;
502
503 dir.setNameFilters({ u"LICENSE"_s, u"COPYING"_s });
504 dir.setFilter(QDir::Files | QDir::NoDotAndDotDot);
505
506 const QFileInfoList entries = dir.entryInfoList();
507 if (!entries.empty())
508 p.licenseFiles = QStringList(entries.at(0).absoluteFilePath());
509 }
510
511 // let's ignore warnings regarding Chromium files for now
512 Q_UNUSED(validatePackage(p, {}, logLevel));
513
514 return p;
515}
516
518{
519 int line = -1;
520 int column = -1;
521};
522
523static CursorPosition mapFromOffset(const QByteArray &content, int offset)
524{
525 CursorPosition pos{ 1, 1 };
526 for (int i = 0; i < content.size(); ++i) {
527 if (i == offset)
528 return pos;
529
530 if (content[i] == '\n') {
531 pos.line++;
532 pos.column = 1;
533 } else {
534 pos.column++;
535 }
536 }
537 return CursorPosition();
538}
539
540std::optional<QList<Package>> readFile(const QString &filePath, LogLevel logLevel)
541{
542 QList<Package> packages;
543 bool errorsFound = false;
544
545 if (logLevel == VerboseLog) {
546 std::cerr << qPrintable(tr("Reading file %1...").arg(
547 QDir::toNativeSeparators(filePath))) << std::endl;
548 }
549 QFile file(filePath);
550 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
551 if (logLevel != SilentLog)
552 std::cerr << qPrintable(tr("Could not open file %1.").arg(
553 QDir::toNativeSeparators(file.fileName()))) << std::endl;
554 return std::nullopt;
555 }
556
557 if (filePath.endsWith(".json"_L1)) {
558 QJsonParseError jsonParseError;
559 const QByteArray content = file.readAll();
560 const QJsonDocument document = QJsonDocument::fromJson(content, &jsonParseError);
561 if (document.isNull()) {
562 if (logLevel != SilentLog) {
563 const CursorPosition pos = mapFromOffset(content, jsonParseError.offset);
564 std::cerr << qPrintable(tr("Could not parse file %1: %2 at line %3, column %4")
565 .arg(QDir::toNativeSeparators(file.fileName()),
566 jsonParseError.errorString(),
567 QString::number(pos.line),
568 QString::number(pos.column)))
569 << std::endl;
570 }
571 return std::nullopt;
572 }
573
574 if (document.isObject()) {
575 std::optional<Package> p =
576 readPackage(document.object(), file.fileName(), logLevel);
577 if (p) {
578 packages << *p;
579 } else {
580 errorsFound = true;
581 }
582 } else if (document.isArray()) {
583 QJsonArray array = document.array();
584 for (int i = 0, size = array.size(); i < size; ++i) {
585 QJsonValue value = array.at(i);
586 if (value.isObject()) {
587 std::optional<Package> p =
588 readPackage(value.toObject(), file.fileName(), logLevel);
589 if (p) {
590 packages << *p;
591 } else {
592 errorsFound = true;
593 }
594 } else {
595 if (logLevel != SilentLog) {
596 std::cerr << qPrintable(tr("File %1: Expecting JSON object in array.")
597 .arg(QDir::toNativeSeparators(file.fileName())))
598 << std::endl;
599 }
600 errorsFound = true;
601 }
602 }
603 } else {
604 if (logLevel != SilentLog) {
605 std::cerr << qPrintable(tr("File %1: Expecting JSON object in array.").arg(
606 QDir::toNativeSeparators(file.fileName()))) << std::endl;
607 }
608 errorsFound = true;
609 }
610 } else if (filePath.endsWith(".chromium"_L1)) {
611 Package chromiumPackage = parseChromiumFile(file, filePath, logLevel);
612 if (!chromiumPackage.name.isEmpty()) // Skip invalid README.chromium files
613 packages << chromiumPackage;
614 } else {
615 if (logLevel != SilentLog) {
616 std::cerr << qPrintable(tr("File %1: Unsupported file type.")
617 .arg(QDir::toNativeSeparators(file.fileName())))
618 << std::endl;
619 }
620 errorsFound = true;
621 }
622
623 if (errorsFound)
624 return std::nullopt;
625 return packages;
626}
627
628std::optional<QList<Package>> scanDirectory(const QString &directory, InputFormats inputFormats,
629 LogLevel logLevel)
630{
631 QDir dir(directory);
632 QList<Package> packages;
633 bool errorsFound = false;
634
635 QStringList nameFilters = QStringList();
636 if (inputFormats & InputFormat::QtAttributions)
637 nameFilters << u"qt_attribution.json"_s;
638 if (inputFormats & InputFormat::ChromiumAttributions)
639 nameFilters << u"README.chromium"_s;
640 if (qEnvironmentVariableIsSet("QT_ATTRIBUTIONSSCANNER_TEST"))
641 nameFilters << u"qt_attribution_test.json"_s << u"README_test.chromium"_s;
642
643 dir.setNameFilters(nameFilters);
644 dir.setFilter(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::Files);
645
646 const QFileInfoList entries = dir.entryInfoList();
647 for (const QFileInfo &info : entries) {
648 if (info.isDir()) {
649 std::optional<QList<Package>> ps =
650 scanDirectory(info.filePath(), inputFormats, logLevel);
651 if (!ps)
652 errorsFound = true;
653 else
654 packages += *ps;
655 } else {
656 std::optional p = readFile(info.filePath(), logLevel);
657 if (!p)
658 errorsFound = true;
659 else
660 packages += *p;
661 }
662 }
663
664 if (errorsFound)
665 return std::nullopt;
666 return packages;
667}
668
669} // namespace Scanner
LogLevel
Definition logging.h:9
@ SilentLog
Definition logging.h:12
@ VerboseLog
Definition logging.h:10
static QStringList extractLicenseIdsFromSPDXExpression(QString expression)
Definition scanner.cpp:156
std::optional< QList< Package > > scanDirectory(const QString &directory, InputFormats inputFormats, LogLevel logLevel)
Definition scanner.cpp:628
static CursorPosition mapFromOffset(const QByteArray &content, int offset)
Definition scanner.cpp:523
static void missingPropertyWarning(const QString &filePath, const QString &property)
Definition scanner.cpp:21
static QString locateLicensesDir(const QString &packageDir)
Definition scanner.cpp:185
static bool handleStringOrStringArrayJsonKey(QStringList &outList, const QString &key, QJsonValueConstRef jsonValue, const QString &filePath, LogLevel logLevel)
Definition scanner.cpp:237
static Package parseChromiumFile(QFile &file, const QString &filePath, LogLevel logLevel)
Definition scanner.cpp:441
static std::optional< QStringList > toStringList(const QJsonValue &value)
Definition scanner.cpp:127
bool validatePackage(Package &p, Checks checks, LogLevel logLevel)
Definition scanner.cpp:27
static std::optional< Package > readPackage(const QJsonObject &object, const QString &filePath, LogLevel logLevel)
Definition scanner.cpp:260
static bool autoDetectLicenseFiles(Package &p)
Definition scanner.cpp:202
static std::optional< QString > arrayToMultiLineString(const QJsonValue &value)
Definition scanner.cpp:140
bool securityCritical
Definition package.h:18