From e0f72cdcf7dcf66c469a7ff3f03ef22a52aece3e Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Wed, 20 Sep 2023 20:05:29 +0000 Subject: [PATCH] [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 4a461f02ab..64ba6da646 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 & 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 ConfigParser::substitute_expression(const std::string & text, + const std::map & 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(pos_variable), res.end(), [](char c) { + return std::isalnum(c) != 0 || c == '_'; + }); + auto pos_after_variable = static_cast(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 f43ea3fac0..2caaa291b1 100644 --- a/libdnf/conf/ConfigParser.hpp +++ b/libdnf/conf/ConfigParser.hpp @@ -147,6 +147,18 @@ struct ConfigParser { int itemNumber{0}; std::string header; std::map 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 substitute_expression(const std::string & text, + const std::map & substitutions, + unsigned int depth); }; inline void ConfigParser::setSubstitutions(const std::map & substitutions)