diff options
Diffstat (limited to 'src/qtattributionsscanner/scanner.cpp')
-rw-r--r-- | src/qtattributionsscanner/scanner.cpp | 474 |
1 files changed, 353 insertions, 121 deletions
diff --git a/src/qtattributionsscanner/scanner.cpp b/src/qtattributionsscanner/scanner.cpp index 06abc2310..879ce7c80 100644 --- a/src/qtattributionsscanner/scanner.cpp +++ b/src/qtattributionsscanner/scanner.cpp @@ -1,35 +1,11 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the tools applications of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "scanner.h" #include "logging.h" #include <QtCore/qdir.h> +#include <QtCore/qhash.h> #include <QtCore/qjsonarray.h> #include <QtCore/qjsondocument.h> #include <QtCore/qjsonobject.h> @@ -37,7 +13,8 @@ #include <QtCore/qvariant.h> #include <iostream> -#include <optional> + +using namespace Qt::Literals::StringLiterals; namespace Scanner { @@ -47,45 +24,91 @@ static void missingPropertyWarning(const QString &filePath, const QString &prope QDir::toNativeSeparators(filePath), property)) << std::endl; } -static void validatePackage(Package &p, const QString &filePath, LogLevel logLevel) +static bool validatePackage(Package &p, const QString &filePath, Checks checks, LogLevel logLevel) { + bool validPackage = true; + if (p.qtParts.isEmpty()) - p.qtParts << QStringLiteral("libs"); + p.qtParts << u"libs"_s; - if (logLevel != SilentLog) { - if (p.name.isEmpty()) { - if (p.id.startsWith(QLatin1String("chromium-"))) // Ignore invalid README.chromium files - return; + if (p.name.isEmpty()) { + if (p.id.startsWith("chromium-"_L1)) // Ignore invalid README.chromium files + return false; - missingPropertyWarning(filePath, QStringLiteral("Name")); - } + if (logLevel != SilentLog) + missingPropertyWarning(filePath, u"Name"_s); + validPackage = false; + } - if (p.id.isEmpty()) - missingPropertyWarning(filePath, QStringLiteral("Id")); - if (p.license.isEmpty()) - missingPropertyWarning(filePath, QStringLiteral("License")); + if (p.id.isEmpty()) { + if (logLevel != SilentLog) + missingPropertyWarning(filePath, u"Id"_s); + validPackage = false; + } + if (p.license.isEmpty()) { + if (logLevel != SilentLog) + missingPropertyWarning(filePath, u"License"_s); + validPackage = false; + } - if (!p.copyright.isEmpty() && !p.copyrightFile.isEmpty()) { + if (!p.copyright.isEmpty() && !p.copyrightFile.isEmpty()) { + if (logLevel != SilentLog) { std::cerr << qPrintable(tr("File %1: Properties 'Copyright' and 'CopyrightFile' are " "mutually exclusive.") .arg(QDir::toNativeSeparators(filePath))) << std::endl; } + validPackage = false; + } - for (const QString &part : qAsConst(p.qtParts)) { - if (part != QLatin1String("examples") - && part != QLatin1String("tests") - && part != QLatin1String("tools") - && part != QLatin1String("libs") - && logLevel != SilentLog) { + if (p.securityCritical && p.downloadLocation.isEmpty()) { + if (logLevel != SilentLog) + missingPropertyWarning(filePath, u"DownloadLocation"_s); + validPackage = false; + } + + for (const QString &part : std::as_const(p.qtParts)) { + if (part != "examples"_L1 && part != "tests"_L1 + && part != "tools"_L1 && part != "libs"_L1) { + + if (logLevel != SilentLog) { std::cerr << qPrintable(tr("File %1: Property 'QtPart' contains unknown element " "'%2'. Valid entries are 'examples', 'tests', 'tools' " "and 'libs'.").arg( QDir::toNativeSeparators(filePath), part)) << std::endl; } + validPackage = false; } } + + if (!(checks & Check::Paths)) + return validPackage; + + const QDir dir = p.path; + if (!dir.exists()) { + std::cerr << qPrintable( + tr("File %1: Directory '%2' does not exist.") + .arg(QDir::toNativeSeparators(filePath), QDir::toNativeSeparators(p.path))) + << std::endl; + validPackage = false; + } else { + for (const QString &file : std::as_const(p.files)) { + if (!dir.exists(file)) { + if (logLevel != SilentLog) { + std::cerr << qPrintable( + tr("File %1: Path '%2' does not exist in directory '%3'.") + .arg(QDir::toNativeSeparators(filePath), + QDir::toNativeSeparators(file), + QDir::toNativeSeparators(p.path))) + << std::endl; + } + validPackage = false; + } + } + } + + return validPackage; } static std::optional<QStringList> toStringList(const QJsonValue &value) @@ -93,7 +116,7 @@ static std::optional<QStringList> toStringList(const QJsonValue &value) if (!value.isArray()) return std::nullopt; QStringList result; - for (auto iter : value.toArray()) { + for (const auto &iter : value.toArray()) { if (iter.type() != QJsonValue::String) return std::nullopt; result.push_back(iter.toString()); @@ -101,80 +124,257 @@ static std::optional<QStringList> toStringList(const QJsonValue &value) return result; } +static std::optional<QString> arrayToMultiLineString(const QJsonValue &value) +{ + if (!value.isArray()) + return std::nullopt; + QString result; + for (const auto &iter : value.toArray()) { + if (iter.type() != QJsonValue::String) + return std::nullopt; + result.append(iter.toString()); + result.append(QLatin1StringView("\n")); + } + return result; +} + +// Extracts SPDX license ids from a SPDX license expression. +// For "(BSD-3-Clause AND BeerWare)" this function returns { "BSD-3-Clause", "BeerWare" }. +static QStringList extractLicenseIdsFromSPDXExpression(QString expression) +{ + const QStringList spdxOperators = { + u"AND"_s, + u"OR"_s, + u"WITH"_s + }; + + // Replace parentheses with spaces. We're not interested in grouping. + const QRegularExpression parensRegex(u"[()]"_s); + expression.replace(parensRegex, u" "_s); + + // Split the string at space boundaries to extract tokens. + QStringList result; + for (const QString &token : expression.split(QLatin1Char(' '), Qt::SkipEmptyParts)) { + if (spdxOperators.contains(token)) + continue; + + // Remove the unary + operator, if present. + if (token.endsWith(QLatin1Char('+'))) + result.append(token.mid(0, token.size() - 1)); + else + result.append(token); + } + return result; +} + +// Starting at packageDir, look for a LICENSES subdirectory in the directory hierarchy upwards. +// Return a default-constructed QString if the directory was not found. +static QString locateLicensesDir(const QString &packageDir) +{ + static const QString licensesSubDir = u"LICENSES"_s; + QDir dir(packageDir); + while (true) { + if (!dir.exists()) + break; + if (dir.cd(licensesSubDir)) + return dir.path(); + if (dir.isRoot() || !dir.cdUp()) + break; + } + return {}; +} + +// Locates the license files that belong to the licenses mentioned in LicenseId and stores them in +// the specified package object. +static bool autoDetectLicenseFiles(Package &p) +{ + const QString licensesDirPath = locateLicensesDir(p.path); + const QStringList licenseIds = extractLicenseIdsFromSPDXExpression(p.licenseId); + if (!licenseIds.isEmpty() && licensesDirPath.isEmpty()) { + std::cerr << qPrintable(tr("LICENSES directory could not be located.")) << std::endl; + return false; + } + + bool success = true; + QDir licensesDir(licensesDirPath); + for (const QString &id : licenseIds) { + QString fileName = id + u".txt"; + if (licensesDir.exists(fileName)) { + p.licenseFiles.append(licensesDir.filePath(fileName)); + } else { + std::cerr << qPrintable(tr("Expected license file not found: %1").arg( + QDir::toNativeSeparators(licensesDir.filePath(fileName)))) + << std::endl; + success = false; + } + } + + return success; +} + // Transforms a JSON object into a Package object -static Package readPackage(const QJsonObject &object, const QString &filePath, LogLevel logLevel) +static std::optional<Package> readPackage(const QJsonObject &object, const QString &filePath, + Checks checks, LogLevel logLevel) { Package p; + bool validPackage = true; const QString directory = QFileInfo(filePath).absolutePath(); p.path = directory; for (auto iter = object.constBegin(); iter != object.constEnd(); ++iter) { const QString key = iter.key(); - if (!iter.value().isString() && key != QLatin1String("QtParts") - && key != QLatin1String("LicenseFiles")) { + if (!iter.value().isString() && key != "QtParts"_L1 && key != "SecurityCritical"_L1 + && key != "Files"_L1 && key != "LicenseFiles"_L1 && key != "Comment"_L1 + && key != "Copyright"_L1) { if (logLevel != SilentLog) std::cerr << qPrintable(tr("File %1: Expected JSON string as value of %2.").arg( QDir::toNativeSeparators(filePath), key)) << std::endl; + validPackage = false; continue; } const QString value = iter.value().toString(); - if (key == QLatin1String("Name")) { + if (key == "Name"_L1) { p.name = value; - } else if (key == QLatin1String("Path")) { + } else if (key == "Path"_L1) { p.path = QDir(directory).absoluteFilePath(value); - } else if (key == QLatin1String("Files")) { - p.files = value.simplified().split(QLatin1Char(' '), Qt::SkipEmptyParts); - } else if (key == QLatin1String("Id")) { + } else if (key == "Files"_L1) { + QJsonValueConstRef jsonValue = iter.value(); + if (jsonValue.isArray()) { + auto maybeStringList = toStringList(jsonValue); + if (maybeStringList) + p.files = maybeStringList.value(); + } else if (jsonValue.isString()) { + // Legacy format: multiple values separated by space in one string. + p.files = value.simplified().split(QLatin1Char(' '), Qt::SkipEmptyParts); + } else { + if (logLevel != SilentLog) { + std::cerr << qPrintable(tr("File %1: Expected JSON array of strings as value " + "of Files.")); + validPackage = false; + continue; + } + } + } else if (key == "Comment"_L1) { + // Accepted purely to record details of potential interest doing + // updates in future. Value is an arbitrary object. Any number of + // Comment entries may be present: JSON doesn't require names to be + // unique, albeit some linters may kvetch. + } else if (key == "Id"_L1) { p.id = value; - } else if (key == QLatin1String("Homepage")) { + } else if (key == "Homepage"_L1) { p.homepage = value; - } else if (key == QLatin1String("Version")) { + } else if (key == "Version"_L1) { p.version = value; - } else if (key == QLatin1String("DownloadLocation")) { + } else if (key == "DownloadLocation"_L1) { p.downloadLocation = value; - } else if (key == QLatin1String("License")) { + } else if (key == "License"_L1) { p.license = value; - } else if (key == QLatin1String("LicenseId")) { + } else if (key == "LicenseId"_L1) { p.licenseId = value; - } else if (key == QLatin1String("LicenseFile")) { + } else if (key == "LicenseFile"_L1) { p.licenseFiles = QStringList(QDir(directory).absoluteFilePath(value)); - } else if (key == QLatin1String("LicenseFiles")) { + } else if (key == "LicenseFiles"_L1) { auto strings = toStringList(iter.value()); - if (!strings && (logLevel != SilentLog)) - std::cerr << qPrintable(tr("File %1: Expected JSON array of strings in %2.") - .arg(QDir::toNativeSeparators(filePath), key)) - << std::endl; + if (!strings) { + if (logLevel != SilentLog) + std::cerr << qPrintable(tr("File %1: Expected JSON array of strings in %2.") + .arg(QDir::toNativeSeparators(filePath), key)) + << std::endl; + validPackage = false; + continue; + } const QDir dir(directory); - for (auto iter : strings.value()) + for (const auto &iter : std::as_const(strings.value())) p.licenseFiles.push_back(dir.absoluteFilePath(iter)); - } else if (key == QLatin1String("Copyright")) { - p.copyright = value; - } else if (key == QLatin1String("CopyrightFile")) { + } else if (key == "Copyright"_L1) { + QJsonValueConstRef jsonValue = iter.value(); + if (jsonValue.isArray()) { + // Array joined with new lines + auto maybeString = arrayToMultiLineString(jsonValue); + if (maybeString) + p.copyright = maybeString.value(); + } else if (jsonValue.isString()) { + // Legacy format: multiple values separated by space in one string. + p.copyright = value; + } else { + if (logLevel != SilentLog) { + std::cerr << qPrintable(tr("File %1: Expected JSON array of string or" + "string as value of %2.").arg( + QDir::toNativeSeparators(filePath), key)) << std::endl; + validPackage = false; + continue; + } + } + } else if (key == "CopyrightFile"_L1) { p.copyrightFile = QDir(directory).absoluteFilePath(value); - } else if (key == QLatin1String("PackageComment")) { + } else if (key == "PackageComment"_L1) { p.packageComment = value; - } else if (key == QLatin1String("QDocModule")) { + } else if (key == "QDocModule"_L1) { p.qdocModule = value; - } else if (key == QLatin1String("Description")) { + } else if (key == "Description"_L1) { p.description = value; - } else if (key == QLatin1String("QtUsage")) { + } else if (key == "QtUsage"_L1) { p.qtUsage = value; - } else if (key == QLatin1String("QtParts")) { - auto parts = toStringList(iter.value()); - if (!parts && (logLevel != SilentLog)) - std::cerr << qPrintable(tr("File %1: Expected JSON array of strings in %2.") + } else if (key == "SecurityCritical"_L1) { + if (!iter.value().isBool()) { + std::cerr << qPrintable(tr("File %1: Expected JSON boolean in %2.") .arg(QDir::toNativeSeparators(filePath), key)) << std::endl; + validPackage = false; + continue; + } + p.securityCritical = iter.value().toBool(); + } else if (key == "QtParts"_L1) { + auto parts = toStringList(iter.value()); + if (!parts) { + if (logLevel != SilentLog) { + std::cerr << qPrintable(tr("File %1: Expected JSON array of strings in %2.") + .arg(QDir::toNativeSeparators(filePath), key)) + << std::endl; + } + validPackage = false; + continue; + } p.qtParts = parts.value(); } else { - if (logLevel != SilentLog) + if (logLevel != SilentLog) { std::cerr << qPrintable(tr("File %1: Unknown key %2.").arg( QDir::toNativeSeparators(filePath), key)) << std::endl; + } + validPackage = false; + } + } + + if (!p.copyrightFile.isEmpty()) { + QFile file(p.copyrightFile); + if (!file.open(QIODevice::ReadOnly)) { + std::cerr << qPrintable(tr("File %1: Cannot open 'CopyrightFile' %2.\n") + .arg(QDir::toNativeSeparators(filePath), + QDir::toNativeSeparators(p.copyrightFile))); + validPackage = false; + } + p.copyrightFileContents = QString::fromUtf8(file.readAll()); + } + + if (p.licenseFiles.isEmpty() && !autoDetectLicenseFiles(p)) + return std::nullopt; + + for (const QString &licenseFile : std::as_const(p.licenseFiles)) { + QFile file(licenseFile); + if (!file.open(QIODevice::ReadOnly)) { + if (logLevel != SilentLog) { + std::cerr << qPrintable(tr("File %1: Cannot open 'LicenseFile' %2.\n") + .arg(QDir::toNativeSeparators(filePath), + QDir::toNativeSeparators(licenseFile))); + } + validPackage = false; } + p.licenseFilesContents << QString::fromUtf8(file.readAll()).trimmed(); } - validatePackage(p, filePath, logLevel); + if (!validatePackage(p, filePath, checks, logLevel) || !validPackage) + return std::nullopt; return p; } @@ -190,9 +390,9 @@ static Package parseChromiumFile(QFile &file, const QString &filePath, LogLevel QTextStream in(&file); while (!in.atEnd()) { QString line = in.readLine().trimmed(); - QStringList parts = line.split(QStringLiteral(":")); + QStringList parts = line.split(u":"_s); - if (parts.count() < 2) + if (parts.size() < 2) continue; QString key = parts.at(0); @@ -201,14 +401,14 @@ static Package parseChromiumFile(QFile &file, const QString &filePath, LogLevel fields[key] = value; - if (line == QLatin1String("Description:")) { // special field : should handle multi-lines values + if (line == "Description:"_L1) { // special field : should handle multi-lines values while (!in.atEnd()) { QString line = in.readLine().trimmed(); - if (line.startsWith(QLatin1String("Local Modifications:"))) // Don't include this part + if (line.startsWith("Local Modifications:"_L1)) // Don't include this part break; - fields[key] += line + QStringLiteral("\n"); + fields[key] += line + u"\n"_s; } break; @@ -218,30 +418,30 @@ static Package parseChromiumFile(QFile &file, const QString &filePath, LogLevel // Construct the Package object Package p; - QString shortName = fields.contains(QLatin1String("Short Name")) - ? fields[QLatin1String("Short Name")] - : fields[QLatin1String("Name")]; - QString version = fields[QStringLiteral("Version")]; + QString shortName = fields.contains("Short Name"_L1) + ? fields["Short Name"_L1] + : fields["Name"_L1]; + QString version = fields[u"Version"_s]; - p.id = QStringLiteral("chromium-") + shortName.toLower().replace(QChar::Space, QStringLiteral("-")); - p.name = fields[QStringLiteral("Name")]; + p.id = u"chromium-"_s + shortName.toLower().replace(QChar::Space, u"-"_s); + p.name = fields[u"Name"_s]; if (version != QLatin1Char('0')) // "0" : not applicable p.version = version; - p.license = fields[QStringLiteral("License")]; - p.homepage = fields[QStringLiteral("URL")]; - p.qdocModule = QStringLiteral("qtwebengine"); - p.qtUsage = QStringLiteral("Used in Qt WebEngine"); - p.description = fields[QStringLiteral("Description")].trimmed(); + p.license = fields[u"License"_s]; + p.homepage = fields[u"URL"_s]; + p.qdocModule = u"qtwebengine"_s; + p.qtUsage = u"Used in Qt WebEngine"_s; + p.description = fields[u"Description"_s].trimmed(); p.path = directory; - QString licenseFile = fields[QStringLiteral("License File")]; - if (licenseFile != QString() && licenseFile != QLatin1String("NOT_SHIPPED")) { + QString licenseFile = fields[u"License File"_s]; + if (licenseFile != QString() && licenseFile != "NOT_SHIPPED"_L1) { p.licenseFiles = QStringList(QDir(directory).absoluteFilePath(licenseFile)); } else { // Look for a LICENSE or COPYING file as a fallback QDir dir = directory; - dir.setNameFilters({ QStringLiteral("LICENSE"), QStringLiteral("COPYING") }); + dir.setNameFilters({ u"LICENSE"_s, u"COPYING"_s }); dir.setFilter(QDir::Files | QDir::NoDotAndDotDot); const QFileInfoList entries = dir.entryInfoList(); @@ -249,14 +449,16 @@ static Package parseChromiumFile(QFile &file, const QString &filePath, LogLevel p.licenseFiles = QStringList(entries.at(0).absoluteFilePath()); } - validatePackage(p, filePath, logLevel); + // let's ignore warnings regarding Chromium files for now + Q_UNUSED(validatePackage(p, filePath, {}, logLevel)); return p; } -QList<Package> readFile(const QString &filePath, LogLevel logLevel) +std::optional<QList<Package>> readFile(const QString &filePath, Checks checks, LogLevel logLevel) { QList<Package> packages; + bool errorsFound = false; if (logLevel == VerboseLog) { std::cerr << qPrintable(tr("Reading file %1...").arg( @@ -267,10 +469,10 @@ QList<Package> readFile(const QString &filePath, LogLevel logLevel) if (logLevel != SilentLog) std::cerr << qPrintable(tr("Could not open file %1.").arg( QDir::toNativeSeparators(file.fileName()))) << std::endl; - return QList<Package>(); + return std::nullopt; } - if (filePath.endsWith(QLatin1String(".json"))) { + if (filePath.endsWith(".json"_L1)) { QJsonParseError jsonParseError; const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &jsonParseError); if (document.isNull()) { @@ -279,58 +481,77 @@ QList<Package> readFile(const QString &filePath, LogLevel logLevel) QDir::toNativeSeparators(file.fileName()), jsonParseError.errorString())) << std::endl; - return QList<Package>(); + return std::nullopt; } if (document.isObject()) { - packages << readPackage(document.object(), file.fileName(), logLevel); + std::optional<Package> p = + readPackage(document.object(), file.fileName(), checks, logLevel); + if (p) { + packages << *p; + } else { + errorsFound = true; + } } else if (document.isArray()) { QJsonArray array = document.array(); for (int i = 0, size = array.size(); i < size; ++i) { QJsonValue value = array.at(i); if (value.isObject()) { - packages << readPackage(value.toObject(), file.fileName(), logLevel); + std::optional<Package> p = + readPackage(value.toObject(), file.fileName(), checks, logLevel); + if (p) { + packages << *p; + } else { + errorsFound = true; + } } else { - if (logLevel != SilentLog) + if (logLevel != SilentLog) { std::cerr << qPrintable(tr("File %1: Expecting JSON object in array.") .arg(QDir::toNativeSeparators(file.fileName()))) << std::endl; + } + errorsFound = true; } } } else { - if (logLevel != SilentLog) + if (logLevel != SilentLog) { std::cerr << qPrintable(tr("File %1: Expecting JSON object in array.").arg( QDir::toNativeSeparators(file.fileName()))) << std::endl; + } + errorsFound = true; } - } else if (filePath.endsWith(QLatin1String(".chromium"))) { + } else if (filePath.endsWith(".chromium"_L1)) { Package chromiumPackage = parseChromiumFile(file, filePath, logLevel); if (!chromiumPackage.name.isEmpty()) // Skip invalid README.chromium files packages << chromiumPackage; } else { - if (logLevel != SilentLog) + if (logLevel != SilentLog) { std::cerr << qPrintable(tr("File %1: Unsupported file type.") .arg(QDir::toNativeSeparators(file.fileName()))) << std::endl; + } + errorsFound = true; } + if (errorsFound) + return std::nullopt; return packages; } -QList<Package> scanDirectory(const QString &directory, InputFormats inputFormats, LogLevel logLevel) +std::optional<QList<Package>> scanDirectory(const QString &directory, InputFormats inputFormats, + Checks checks, LogLevel logLevel) { QDir dir(directory); QList<Package> packages; + bool errorsFound = false; QStringList nameFilters = QStringList(); if (inputFormats & InputFormat::QtAttributions) - nameFilters << QStringLiteral("qt_attribution.json"); + nameFilters << u"qt_attribution.json"_s; if (inputFormats & InputFormat::ChromiumAttributions) - nameFilters << QStringLiteral("README.chromium"); - if (qEnvironmentVariableIsSet("QT_ATTRIBUTIONSSCANNER_TEST")) { - nameFilters - << QStringLiteral("qt_attribution_test.json") - << QStringLiteral("README_test.chromium"); - } + nameFilters << u"README.chromium"_s; + if (qEnvironmentVariableIsSet("QT_ATTRIBUTIONSSCANNER_TEST")) + nameFilters << u"qt_attribution_test.json"_s << u"README_test.chromium"_s; dir.setNameFilters(nameFilters); dir.setFilter(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::Files); @@ -338,12 +559,23 @@ QList<Package> scanDirectory(const QString &directory, InputFormats inputFormats const QFileInfoList entries = dir.entryInfoList(); for (const QFileInfo &info : entries) { if (info.isDir()) { - packages += scanDirectory(info.filePath(), inputFormats, logLevel); + std::optional<QList<Package>> ps = + scanDirectory(info.filePath(), inputFormats, checks, logLevel); + if (!ps) + errorsFound = true; + else + packages += *ps; } else { - packages += readFile(info.filePath(), logLevel); + std::optional p = readFile(info.filePath(), checks, logLevel); + if (!p) + errorsFound = true; + else + packages += *p; } } + if (errorsFound) + return std::nullopt; return packages; } |