262 lines
11 KiB
Diff
262 lines
11 KiB
Diff
From 0eb17f745b4b23bd32e6c3896812bea8d1ed0dfc Mon Sep 17 00:00:00 2001
|
|
From: Evan Goode <mail@evangoo.de>
|
|
Date: Wed, 20 Sep 2023 20:05:29 +0000
|
|
Subject: [PATCH 01/11] [conf] Add limited shell-style variable expansion
|
|
|
|
Ported from the DNF 5 implementation here:
|
|
https://github.com/rpm-software-management/dnf5/pull/800.
|
|
|
|
Adds support for ${variable:-word} and ${variable:+word}
|
|
POSIX-shell-like expansions of vars.
|
|
|
|
${variable:-word} means if `variable` is unset or empty, the expansion
|
|
of `word` is substituted. Otherwise, the value of `variable` is
|
|
substituted.
|
|
|
|
${variable:+word} means if `variable` is unset or empty, nothing is
|
|
substituted. Otherwise, the expansion of `word` is substituted.
|
|
|
|
Zypper supports these expansions, see here:
|
|
https://doc.opensuse.org/projects/libzypp/HEAD/structzypp_1_1repo_1_1RepoVarExpand.html
|
|
|
|
For: https://bugzilla.redhat.com/show_bug.cgi?id=1789346
|
|
---
|
|
libdnf/conf/ConfigParser.cpp | 189 ++++++++++++++++++++++++++++++-----
|
|
libdnf/conf/ConfigParser.hpp | 12 +++
|
|
2 files changed, 175 insertions(+), 26 deletions(-)
|
|
|
|
diff --git a/libdnf/conf/ConfigParser.cpp b/libdnf/conf/ConfigParser.cpp
|
|
index 0755c656..e5d6b3b7 100644
|
|
--- a/libdnf/conf/ConfigParser.cpp
|
|
+++ b/libdnf/conf/ConfigParser.cpp
|
|
@@ -29,40 +29,177 @@ namespace libdnf {
|
|
void ConfigParser::substitute(std::string & text,
|
|
const std::map<std::string, std::string> & substitutions)
|
|
{
|
|
- auto start = text.find_first_of("$");
|
|
- while (start != text.npos)
|
|
- {
|
|
- auto variable = start + 1;
|
|
- if (variable >= text.length())
|
|
- break;
|
|
- bool bracket;
|
|
- if (text[variable] == '{') {
|
|
- bracket = true;
|
|
- if (++variable >= text.length())
|
|
+ text = ConfigParser::substitute_expression(text, substitutions, 0).first;
|
|
+}
|
|
+
|
|
+const unsigned int MAXIMUM_EXPRESSION_DEPTH = 32;
|
|
+
|
|
+std::pair<std::string, size_t> ConfigParser::substitute_expression(const std::string & text,
|
|
+ const std::map<std::string, std::string> & substitutions,
|
|
+ unsigned int depth) {
|
|
+ if (depth > MAXIMUM_EXPRESSION_DEPTH) {
|
|
+ return std::make_pair(std::string(text), text.length());
|
|
+ }
|
|
+ std::string res{text};
|
|
+
|
|
+ // The total number of characters read in the replacee
|
|
+ size_t total_scanned = 0;
|
|
+
|
|
+ size_t pos = 0;
|
|
+ while (pos < res.length()) {
|
|
+ if (res[pos] == '}' && depth > 0) {
|
|
+ return std::make_pair(res.substr(0, pos), total_scanned);
|
|
+ }
|
|
+
|
|
+ if (res[pos] == '\\') {
|
|
+ // Escape the next character (if there is one)
|
|
+ if (pos + 1 >= res.length()) {
|
|
break;
|
|
- } else
|
|
- bracket = false;
|
|
- auto it = std::find_if_not(text.begin()+variable, text.end(),
|
|
- [](char c){return std::isalnum(c) || c=='_';});
|
|
- if (bracket && it == text.end())
|
|
- break;
|
|
- auto pastVariable = std::distance(text.begin(), it);
|
|
- if (bracket && *it != '}') {
|
|
- start = text.find_first_of("$", pastVariable);
|
|
+ }
|
|
+ res.erase(pos, 1);
|
|
+ total_scanned += 2;
|
|
+ pos += 1;
|
|
continue;
|
|
}
|
|
- auto subst = substitutions.find(text.substr(variable, pastVariable - variable));
|
|
- if (subst != substitutions.end()) {
|
|
- if (bracket)
|
|
- ++pastVariable;
|
|
- text.replace(start, pastVariable - start, subst->second);
|
|
- start = text.find_first_of("$", start + subst->second.length());
|
|
+ if (res[pos] == '$') {
|
|
+ // variable expression starts after the $ and includes the braces
|
|
+ // ${variable:-word}
|
|
+ // ^-- pos_variable_expression
|
|
+ size_t pos_variable_expression = pos + 1;
|
|
+ if (pos_variable_expression >= res.length()) {
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ // Does the variable expression use braces? If so, the variable name
|
|
+ // starts one character after the start of the variable_expression
|
|
+ bool has_braces;
|
|
+ size_t pos_variable;
|
|
+ if (res[pos_variable_expression] == '{') {
|
|
+ has_braces = true;
|
|
+ pos_variable = pos_variable_expression + 1;
|
|
+ if (pos_variable >= res.length()) {
|
|
+ break;
|
|
+ }
|
|
+ } else {
|
|
+ has_braces = false;
|
|
+ pos_variable = pos_variable_expression;
|
|
+ }
|
|
+
|
|
+ // Find the end of the variable name
|
|
+ auto it = std::find_if_not(res.begin() + static_cast<long>(pos_variable), res.end(), [](char c) {
|
|
+ return std::isalnum(c) != 0 || c == '_';
|
|
+ });
|
|
+ auto pos_after_variable = static_cast<size_t>(std::distance(res.begin(), it));
|
|
+
|
|
+ // Find the substituting string and the end of the variable expression
|
|
+ auto variable_mapping = substitutions.find(res.substr(pos_variable, pos_after_variable - pos_variable));
|
|
+ const std::string * subst_str = nullptr;
|
|
+
|
|
+ size_t pos_after_variable_expression;
|
|
+
|
|
+ if (has_braces) {
|
|
+ if (pos_after_variable >= res.length()) {
|
|
+ break;
|
|
+ }
|
|
+ if (res[pos_after_variable] == ':') {
|
|
+ if (pos_after_variable + 1 >= res.length()) {
|
|
+ break;
|
|
+ }
|
|
+ char expansion_mode = res[pos_after_variable + 1];
|
|
+ size_t pos_word = pos_after_variable + 2;
|
|
+ if (pos_word >= res.length()) {
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ // Expand the default/alternate expression
|
|
+ auto word_str = res.substr(pos_word);
|
|
+ auto word_substitution = substitute_expression(word_str, substitutions, depth + 1);
|
|
+ auto expanded_word = word_substitution.first;
|
|
+ auto scanned = word_substitution.second;
|
|
+ auto pos_after_word = pos_word + scanned;
|
|
+ if (pos_after_word >= res.length()) {
|
|
+ break;
|
|
+ }
|
|
+ if (res[pos_after_word] != '}') {
|
|
+ // The variable expression doesn't end in a '}',
|
|
+ // continue after the word and don't expand it
|
|
+ total_scanned += pos_after_word - pos;
|
|
+ pos = pos_after_word;
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if (expansion_mode == '-') {
|
|
+ // ${variable:-word} (default value)
|
|
+ // If variable is unset or empty, the expansion of word is
|
|
+ // substituted. Otherwise, the value of variable is
|
|
+ // substituted.
|
|
+ if (variable_mapping == substitutions.end() || variable_mapping->second.empty()) {
|
|
+ subst_str = &expanded_word;
|
|
+ } else {
|
|
+ subst_str = &variable_mapping->second;
|
|
+ }
|
|
+ } else if (expansion_mode == '+') {
|
|
+ // ${variable:+word} (alternate value)
|
|
+ // If variable is unset or empty nothing is substituted.
|
|
+ // Otherwise, the expansion of word is substituted.
|
|
+ if (variable_mapping == substitutions.end() || variable_mapping->second.empty()) {
|
|
+ const std::string empty{};
|
|
+ subst_str = ∅
|
|
+ } else {
|
|
+ subst_str = &expanded_word;
|
|
+ }
|
|
+ } else {
|
|
+ // Unknown expansion mode, continue after the ':'
|
|
+ pos = pos_after_variable + 1;
|
|
+ continue;
|
|
+ }
|
|
+ pos_after_variable_expression = pos_after_word + 1;
|
|
+ } else if (res[pos_after_variable] == '}') {
|
|
+ // ${variable}
|
|
+ if (variable_mapping != substitutions.end()) {
|
|
+ subst_str = &variable_mapping->second;
|
|
+ }
|
|
+ // Move past the closing '}'
|
|
+ pos_after_variable_expression = pos_after_variable + 1;
|
|
+ } else {
|
|
+ // Variable expression doesn't end in a '}', continue after the variable
|
|
+ pos = pos_after_variable;
|
|
+ continue;
|
|
+ }
|
|
+ } else {
|
|
+ // No braces, we have a $variable
|
|
+ if (variable_mapping != substitutions.end()) {
|
|
+ subst_str = &variable_mapping->second;
|
|
+ }
|
|
+ pos_after_variable_expression = pos_after_variable;
|
|
+ }
|
|
+
|
|
+ // If there is no substitution to make, move past the variable expression and continue.
|
|
+ if (subst_str == nullptr) {
|
|
+ total_scanned += pos_after_variable_expression - pos;
|
|
+ pos = pos_after_variable_expression;
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ res.replace(pos, pos_after_variable_expression - pos, *subst_str);
|
|
+ total_scanned += pos_after_variable_expression - pos;
|
|
+ pos += subst_str->length();
|
|
} else {
|
|
- start = text.find_first_of("$", pastVariable);
|
|
+ total_scanned += 1;
|
|
+ pos += 1;
|
|
}
|
|
}
|
|
+
|
|
+ // We have reached the end of the text
|
|
+ if (depth > 0) {
|
|
+ // If we are in a subexpression and we didn't find a closing '}', make no substitutions.
|
|
+ return std::make_pair(std::string{text}, text.length());
|
|
+ }
|
|
+
|
|
+ return std::make_pair(res, text.length());
|
|
}
|
|
|
|
+
|
|
static void read(ConfigParser & cfgParser, IniParser & parser)
|
|
{
|
|
IniParser::ItemType readedType;
|
|
diff --git a/libdnf/conf/ConfigParser.hpp b/libdnf/conf/ConfigParser.hpp
|
|
index a0d81837..f3d10061 100644
|
|
--- a/libdnf/conf/ConfigParser.hpp
|
|
+++ b/libdnf/conf/ConfigParser.hpp
|
|
@@ -150,6 +150,18 @@ private:
|
|
int itemNumber{0};
|
|
std::string header;
|
|
std::map<std::string, std::string> rawItems;
|
|
+
|
|
+ /**
|
|
+ * @brief Expand variables in a subexpression
|
|
+ *
|
|
+ * @param text String with variable expressions
|
|
+ * @param substitutions Substitution map
|
|
+ * @param depth The recursive depth
|
|
+ * @return Pair of the resulting string and the number of characters scanned in `text`
|
|
+ */
|
|
+ static std::pair<std::string, size_t> substitute_expression(const std::string & text,
|
|
+ const std::map<std::string, std::string> & substitutions,
|
|
+ unsigned int depth);
|
|
};
|
|
|
|
inline void ConfigParser::setSubstitutions(const std::map<std::string, std::string> & substitutions)
|
|
--
|
|
2.49.0
|
|
|