From f923dedc3f99a53656fbdc5126859faa94e61961 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 12 Apr 2022 15:28:07 +0200 Subject: [PATCH] [libcalamares] Add a word-expander This is a variant on KMacroExpander, which allows for reporting of errors after expansion. --- src/libcalamares/CMakeLists.txt | 3 +- src/libcalamares/utils/StringExpander.cpp | 82 +++++++++++++++++++++++ src/libcalamares/utils/StringExpander.h | 66 ++++++++++++++++++ src/libcalamares/utils/Tests.cpp | 59 ++++++++++++++++ 4 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 src/libcalamares/utils/StringExpander.cpp create mode 100644 src/libcalamares/utils/StringExpander.h diff --git a/src/libcalamares/CMakeLists.txt b/src/libcalamares/CMakeLists.txt index 407dc5f61..2baa3366a 100644 --- a/src/libcalamares/CMakeLists.txt +++ b/src/libcalamares/CMakeLists.txt @@ -69,6 +69,7 @@ set(libSources utils/Retranslator.cpp utils/Runner.cpp utils/String.cpp + utils/StringExpander.cpp utils/UMask.cpp utils/Variant.cpp utils/Yaml.cpp @@ -146,7 +147,7 @@ calamares_automoc( calamares ) target_link_libraries( calamares LINK_PRIVATE ${OPTIONAL_PRIVATE_LIBRARIES} - LINK_PUBLIC yamlcpp::yamlcpp Qt5::Core ${OPTIONAL_PUBLIC_LIBRARIES} + LINK_PUBLIC yamlcpp::yamlcpp Qt5::Core KF5::CoreAddons ${OPTIONAL_PUBLIC_LIBRARIES} ) add_library(Calamares::calamares ALIAS calamares) diff --git a/src/libcalamares/utils/StringExpander.cpp b/src/libcalamares/utils/StringExpander.cpp new file mode 100644 index 000000000..044e3aee1 --- /dev/null +++ b/src/libcalamares/utils/StringExpander.cpp @@ -0,0 +1,82 @@ +/* === This file is part of Calamares - === + * + * SPDX-FileCopyrightText: 2022 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Calamares is Free Software: see the License-Identifier above. + * + * + */ + +#include "StringExpander.h" +#include "Logger.h" + +namespace Calamares +{ +namespace String +{ + +struct DictionaryExpander::Private +{ + QHash< QString, QString > dictionary; + QStringList missing; +}; + +DictionaryExpander::DictionaryExpander() + : KWordMacroExpander( '$' ) + , d( std::make_unique< Private >() ) +{ +} + +DictionaryExpander::~DictionaryExpander() {} + + +void +DictionaryExpander::insert( const QString& key, const QString& value ) +{ + d->dictionary.insert( key, value ); +} + +void +DictionaryExpander::clearErrors() +{ + d->missing.clear(); +} + +bool +DictionaryExpander::hasErrors() const +{ + return !d->missing.isEmpty(); +} + +QStringList +DictionaryExpander::errorNames() const +{ + return d->missing; +} + +QString +DictionaryExpander::expand( QString s ) +{ + clearErrors(); + expandMacros( s ); + return s; +} + +bool +DictionaryExpander::expandMacro( const QString& str, QStringList& ret ) +{ + if ( d->dictionary.contains( str ) ) + { + ret << d->dictionary[ str ]; + return true; + } + else + { + d->missing << str; + return false; + } +} + +} // namespace String +} // namespace Calamares diff --git a/src/libcalamares/utils/StringExpander.h b/src/libcalamares/utils/StringExpander.h new file mode 100644 index 000000000..afcd03ef7 --- /dev/null +++ b/src/libcalamares/utils/StringExpander.h @@ -0,0 +1,66 @@ +/* === This file is part of Calamares - === + * + * SPDX-FileCopyrightText: 2022 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Calamares is Free Software: see the License-Identifier above. + * + * + */ + +#ifndef UTILS_STRINGEXPANDER_H +#define UTILS_STRINGEXPANDER_H + +#include "DllMacro.h" + +#include + +#include +#include + +namespace Calamares +{ +namespace String +{ + +/** @brief Expand variables in a string against a dictionary. + * + * This class provides a convenience API for building up a dictionary + * and using it to expand strings. Use the `expand()` method to + * do standard word-based expansion with `$` as macro-symbol. + * + * Unlike straight-up `KMacroExpander::expandMacros()`, this + * provides an API to find out which variables were missing + * from the dictionary during expansion. Use `hasErrors()` and + * `errorNames()` to find out which variables those were. + * + * Call `clearErrors()` to reset the stored errors. Calling + * `expand()` implicitly clears the errors before starting + * a new expansion, as well. + */ +class DictionaryExpander : public KWordMacroExpander +{ +public: + DictionaryExpander(); + virtual ~DictionaryExpander() override; + + void insert( const QString& key, const QString& value ); + + void clearErrors(); + bool hasErrors() const; + QStringList errorNames() const; + + QString expand( QString s ); + +protected: + virtual bool expandMacro( const QString& str, QStringList& ret ) override; + +private: + struct Private; + std::unique_ptr< Private > d; +}; + +} // namespace String +} // namespace Calamares + +#endif diff --git a/src/libcalamares/utils/Tests.cpp b/src/libcalamares/utils/Tests.cpp index b4b2251cc..49f1b1ed8 100644 --- a/src/libcalamares/utils/Tests.cpp +++ b/src/libcalamares/utils/Tests.cpp @@ -15,6 +15,7 @@ #include "RAII.h" #include "Runner.h" #include "String.h" +#include "StringExpander.h" #include "Traits.h" #include "UMask.h" #include "Variant.h" @@ -75,6 +76,10 @@ private Q_SLOTS: void testStringRemoveTrailing_data(); void testStringRemoveTrailing(); + /** @section Test String expansion. */ + void testStringMacroExpander_data(); + void testStringMacroExpander(); // The KF5::CoreAddons bits + /** @section Test Runner directory-manipulation. */ void testRunnerDirs(); void testCalculateWorkingDirectory(); @@ -817,6 +822,60 @@ LibCalamaresTests::testStringRemoveTrailing() QCOMPARE( string, result ); } +void +LibCalamaresTests::testStringMacroExpander_data() +{ + QTest::addColumn< QString >( "source" ); + QTest::addColumn< QString >( "result" ); + QTest::addColumn< QStringList >( "errors" ); + + QTest::newRow( "empty " ) << QString() << QString() << QStringList {}; + QTest::newRow( "constant" ) << QStringLiteral( "bunnies!" ) << QStringLiteral( "bunnies!" ) << QStringList {}; + QTest::newRow( "escaped " ) << QStringLiteral( "$$bun" ) << QStringLiteral( "$bun" ) + << QStringList {}; // Double $$ is an escaped $ + QTest::newRow( "whole " ) << QStringLiteral( "${ROOT}" ) << QStringLiteral( "wortel" ) << QStringList {}; + QTest::newRow( "unbraced" ) << QStringLiteral( "$ROOT" ) << QStringLiteral( "wortel" ) + << QStringList {}; // Does not need {} + QTest::newRow( "bad-var1" ) << QStringLiteral( "${ROOF}" ) << QStringLiteral( "${ROOF}" ) + << QStringList { QStringLiteral( "ROOF" ) }; // Not replaced + QTest::newRow( "twice " ) << QStringLiteral( "${ROOT}x${ROOT}" ) << QStringLiteral( "wortelxwortel" ) + << QStringList {}; + QTest::newRow( "bad-var2" ) << QStringLiteral( "${ROOT}x${ROPE}" ) << QStringLiteral( "wortelx${ROPE}" ) + << QStringList { QStringLiteral( "ROPE" ) }; // Not replaced + // This is a borked string with a "nested" variable. The variable-name- + // scanner goes from ${ to the next } and tries to match that. + QTest::newRow( "confuse1" ) << QStringLiteral( "${RO${ROOT}" ) << QStringLiteral( "${ROwortel" ) + << QStringList { "RO${ROOT" }; + // This one doesn't have a { for the first name to match with + QTest::newRow( "confuse2" ) << QStringLiteral( "$RO${ROOT}" ) << QStringLiteral( "$ROwortel" ) + << QStringList { "RO" }; + // Here we see it just doesn't nest + QTest::newRow( "confuse3" ) << QStringLiteral( "${RO${ROOT}}" ) << QStringLiteral( "${ROwortel}" ) + << QStringList { "RO${ROOT" }; +} + +void +LibCalamaresTests::testStringMacroExpander() +{ + QHash< QString, QString > dict; + dict.insert( QStringLiteral( "ROOT" ), QStringLiteral( "wortel" ) ); + + Calamares::String::DictionaryExpander d; + d.insert( QStringLiteral( "ROOT" ), QStringLiteral( "wortel" ) ); + + QFETCH( QString, source ); + QFETCH( QString, result ); + QFETCH( QStringList, errors ); + + QString km_expanded = KMacroExpander::expandMacros( source, dict, '$' ); + QCOMPARE( km_expanded, result ); + + QString de_expanded = d.expand( source ); + QCOMPARE( de_expanded, result ); + QCOMPARE( d.errorNames(), errors ); + QCOMPARE( d.hasErrors(), !errors.isEmpty() ); +} + static QString dirname( const QTemporaryDir& d ) {