libdnf/0023-conf-Add-limited-shell-style-variable-expansion.patch
2025-06-30 19:42:02 +00:00

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 = &empty;
+ } 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