From 03cea2d0b596b367e21c1016de5959ce1a12394b Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Mon, 30 Jun 2025 19:26:17 +0000 Subject: [PATCH] Split $releasever_{major,minor}, shell-style variable expansion Resolves: RHEL-95006 --- ...mited-shell-style-variable-expansion.patch | 261 ++++++++++++++++++ ...sever-to-releasever_major-and-releas.patch | 148 ++++++++++ ...or-releasever_major-releasever_minor.patch | 91 ++++++ ...figParser-fix-use-out-of-scope-leaks.patch | 124 +++++++++ ...s-for-shell-style-variable-expansion.patch | 38 +++ ...-to-releasever_major-and-releasever_.patch | 156 +++++++++++ ...igParser-make-splitReleasever-public.patch | 78 ++++++ ...easever_major-releasever_minor-from-.patch | 147 ++++++++++ ...ever_-major-minor-from-context-inste.patch | 121 ++++++++ ...rt-shell-style-variable-substitution.patch | 60 ++++ ...est-shell-style-variable-expressions.patch | 80 ++++++ libdnf.spec | 17 +- 12 files changed, 1320 insertions(+), 1 deletion(-) create mode 100644 0023-conf-Add-limited-shell-style-variable-expansion.patch create mode 100644 0024-conf-split-releasever-to-releasever_major-and-releas.patch create mode 100644 0025-Test-for-releasever_major-releasever_minor.patch create mode 100644 0026-ConfigParser-fix-use-out-of-scope-leaks.patch create mode 100644 0027-Add-tests-for-shell-style-variable-expansion.patch create mode 100644 0028-Split-releasever-to-releasever_major-and-releasever_.patch create mode 100644 0029-ConfigParser-make-splitReleasever-public.patch create mode 100644 0030-C-API-Detect-releasever_major-releasever_minor-from-.patch create mode 100644 0031-C-API-Use-releasever_-major-minor-from-context-inste.patch create mode 100644 0032-C-API-support-shell-style-variable-substitution.patch create mode 100644 0033-C-API-test-shell-style-variable-expressions.patch diff --git a/0023-conf-Add-limited-shell-style-variable-expansion.patch b/0023-conf-Add-limited-shell-style-variable-expansion.patch new file mode 100644 index 0000000..f53d383 --- /dev/null +++ b/0023-conf-Add-limited-shell-style-variable-expansion.patch @@ -0,0 +1,261 @@ +From 0eb17f745b4b23bd32e6c3896812bea8d1ed0dfc Mon Sep 17 00:00:00 2001 +From: Evan Goode +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 & 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 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 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) +-- +2.49.0 + diff --git a/0024-conf-split-releasever-to-releasever_major-and-releas.patch b/0024-conf-split-releasever-to-releasever_major-and-releas.patch new file mode 100644 index 0000000..72eea5e --- /dev/null +++ b/0024-conf-split-releasever-to-releasever_major-and-releas.patch @@ -0,0 +1,148 @@ +From 89053c30a56da51849ffd5f4323ba3ef04eb8fcd Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Mon, 9 Oct 2023 21:16:57 +0000 +Subject: [PATCH 02/11] [conf] split $releasever to $releasever_major and + $releasever_minor + +This functionality is also implemented in DNF 4, but direct consumers of +libdnf (PackageKit, microdnf) also need it. + +DNF 4 PR: https://github.com/rpm-software-management/dnf/pull/1989 + +For https://bugzilla.redhat.com/show_bug.cgi?id=1789346 +--- + libdnf/conf/ConfigParser.cpp | 50 +++++++++++++++++++++++++------- + libdnf/conf/ConfigParser.hpp | 2 ++ + tests/libdnf/conf/CMakeLists.txt | 11 +++++++ + 3 files changed, 53 insertions(+), 10 deletions(-) + create mode 100644 tests/libdnf/conf/CMakeLists.txt + +diff --git a/libdnf/conf/ConfigParser.cpp b/libdnf/conf/ConfigParser.cpp +index e5d6b3b7..a89fd8bf 100644 +--- a/libdnf/conf/ConfigParser.cpp ++++ b/libdnf/conf/ConfigParser.cpp +@@ -92,7 +92,27 @@ std::pair ConfigParser::substitute_expression(const std::st + 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 auto & variable_key = res.substr(pos_variable, pos_after_variable - pos_variable); ++ const auto variable_mapping = substitutions.find(variable_key); ++ ++ const std::string * variable_value = nullptr; ++ ++ if (variable_mapping == substitutions.end()) { ++ if (variable_key == "releasever_major" || variable_key == "releasever_minor") { ++ const auto releasever_mapping = substitutions.find("releasever"); ++ if (releasever_mapping != substitutions.end()) { ++ const auto & releasever_split = ConfigParser::split_releasever(releasever_mapping->second); ++ if (variable_key == "releasever_major") { ++ variable_value = &std::get<0>(releasever_split); ++ } else { ++ variable_value = &std::get<1>(releasever_split); ++ } ++ } ++ } ++ } else { ++ variable_value = &variable_mapping->second; ++ } ++ + const std::string * subst_str = nullptr; + + size_t pos_after_variable_expression; +@@ -133,16 +153,16 @@ std::pair ConfigParser::substitute_expression(const std::st + // 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()) { ++ if (variable_value == nullptr || variable_value->empty()) { + subst_str = &expanded_word; + } else { +- subst_str = &variable_mapping->second; ++ subst_str = variable_value; + } + } 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()) { ++ if (variable_value == nullptr || variable_value->empty()) { + const std::string empty{}; + subst_str = ∅ + } else { +@@ -156,9 +176,7 @@ std::pair ConfigParser::substitute_expression(const std::st + 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; +- } ++ subst_str = variable_value; + // Move past the closing '}' + pos_after_variable_expression = pos_after_variable + 1; + } else { +@@ -168,9 +186,7 @@ std::pair ConfigParser::substitute_expression(const std::st + } + } else { + // No braces, we have a $variable +- if (variable_mapping != substitutions.end()) { +- subst_str = &variable_mapping->second; +- } ++ subst_str = variable_value; + pos_after_variable_expression = pos_after_variable; + } + +@@ -199,6 +215,20 @@ std::pair ConfigParser::substitute_expression(const std::st + return std::make_pair(res, text.length()); + } + ++std::tuple ConfigParser::split_releasever(const std::string & releasever) ++{ ++ // Uses the same logic as DNF 5 and as splitReleaseverTo in libzypp ++ std::string releasever_major; ++ std::string releasever_minor; ++ const auto pos = releasever.find('.'); ++ if (pos == std::string::npos) { ++ releasever_major = releasever; ++ } else { ++ releasever_major = releasever.substr(0, pos); ++ releasever_minor = releasever.substr(pos + 1); ++ } ++ return std::make_tuple(releasever_major, releasever_minor); ++} + + static void read(ConfigParser & cfgParser, IniParser & parser) + { +diff --git a/libdnf/conf/ConfigParser.hpp b/libdnf/conf/ConfigParser.hpp +index f3d10061..f30dd4a4 100644 +--- a/libdnf/conf/ConfigParser.hpp ++++ b/libdnf/conf/ConfigParser.hpp +@@ -162,6 +162,8 @@ private: + static std::pair substitute_expression(const std::string & text, + const std::map & substitutions, + unsigned int depth); ++ ++ static std::tuple split_releasever(const std::string & releasever); + }; + + inline void ConfigParser::setSubstitutions(const std::map & substitutions) +diff --git a/tests/libdnf/conf/CMakeLists.txt b/tests/libdnf/conf/CMakeLists.txt +new file mode 100644 +index 00000000..05058367 +--- /dev/null ++++ b/tests/libdnf/conf/CMakeLists.txt +@@ -0,0 +1,11 @@ ++set(LIBDNF_TEST_SOURCES ++ ${LIBDNF_TEST_SOURCES} ++ ${CMAKE_CURRENT_SOURCE_DIR}/ConfigParserTest.cpp ++ PARENT_SCOPE ++) ++ ++set(LIBDNF_TEST_HEADERS ++ ${LIBDNF_TEST_HEADERS} ++ ${CMAKE_CURRENT_SOURCE_DIR}/ConfigParserTest.hpp ++ PARENT_SCOPE ++) +-- +2.49.0 + diff --git a/0025-Test-for-releasever_major-releasever_minor.patch b/0025-Test-for-releasever_major-releasever_minor.patch new file mode 100644 index 0000000..765d841 --- /dev/null +++ b/0025-Test-for-releasever_major-releasever_minor.patch @@ -0,0 +1,91 @@ +From 734a573408643c90faf7ff57ea33fec5fb467553 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Mon, 9 Oct 2023 21:54:08 +0000 +Subject: [PATCH 03/11] Test for $releasever_major, $releasever_minor + +--- + tests/CMakeLists.txt | 1 + + tests/libdnf/conf/ConfigParserTest.cpp | 33 ++++++++++++++++++++++++++ + tests/libdnf/conf/ConfigParserTest.hpp | 21 ++++++++++++++++ + 3 files changed, 55 insertions(+) + create mode 100644 tests/libdnf/conf/ConfigParserTest.cpp + create mode 100644 tests/libdnf/conf/ConfigParserTest.hpp + +diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt +index 85b47ca1..e4909682 100644 +--- a/tests/CMakeLists.txt ++++ b/tests/CMakeLists.txt +@@ -1,3 +1,4 @@ ++add_subdirectory(libdnf/conf) + add_subdirectory(libdnf/module/modulemd) + add_subdirectory(libdnf/module) + add_subdirectory(libdnf/repo) +diff --git a/tests/libdnf/conf/ConfigParserTest.cpp b/tests/libdnf/conf/ConfigParserTest.cpp +new file mode 100644 +index 00000000..70278196 +--- /dev/null ++++ b/tests/libdnf/conf/ConfigParserTest.cpp +@@ -0,0 +1,33 @@ ++#include "ConfigParserTest.hpp" ++ ++CPPUNIT_TEST_SUITE_REGISTRATION(ConfigParserTest); ++ ++void ConfigParserTest::setUp() ++{} ++ ++void ConfigParserTest::testConfigParserReleasever() ++{ ++ { ++ // Test $releasever_major, $releasever_minor ++ std::map substitutions = { ++ {"releasever", "1.23"}, ++ }; ++ ++ std::string text = "major: $releasever_major, minor: $releasever_minor"; ++ libdnf::ConfigParser::substitute(text, substitutions); ++ CPPUNIT_ASSERT(text == "major: 1, minor: 23"); ++ ++ text = "full releasever: $releasever"; ++ libdnf::ConfigParser::substitute(text, substitutions); ++ CPPUNIT_ASSERT(text == "full releasever: 1.23"); ++ } ++ { ++ // Test with empty $releasever, should set empty $releasever_major, $releasever_minor ++ std::map substitutions = { ++ {"releasever", ""}, ++ }; ++ std::string text = "major: $releasever_major, minor: $releasever_minor"; ++ libdnf::ConfigParser::substitute(text, substitutions); ++ CPPUNIT_ASSERT(text == "major: , minor: "); ++ } ++} +diff --git a/tests/libdnf/conf/ConfigParserTest.hpp b/tests/libdnf/conf/ConfigParserTest.hpp +new file mode 100644 +index 00000000..7f1faf47 +--- /dev/null ++++ b/tests/libdnf/conf/ConfigParserTest.hpp +@@ -0,0 +1,21 @@ ++#ifndef LIBDNF_CONFIGPARSERTEST_HPP ++#define LIBDNF_CONFIGPARSERTEST_HPP ++ ++#include ++#include ++ ++#include ++ ++class ConfigParserTest : public CppUnit::TestCase ++{ ++ CPPUNIT_TEST_SUITE(ConfigParserTest); ++ CPPUNIT_TEST(testConfigParserReleasever); ++ CPPUNIT_TEST_SUITE_END(); ++ ++public: ++ void setUp() override; ++ void testConfigParserReleasever(); ++ ++}; ++ ++#endif // LIBDNF_CONFIGPARSERTEST_HPP +-- +2.49.0 + diff --git a/0026-ConfigParser-fix-use-out-of-scope-leaks.patch b/0026-ConfigParser-fix-use-out-of-scope-leaks.patch new file mode 100644 index 0000000..f8a0fa7 --- /dev/null +++ b/0026-ConfigParser-fix-use-out-of-scope-leaks.patch @@ -0,0 +1,124 @@ +From ff0911502ae7a71f9a872d93f91c395f2f04de4a Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Tue, 7 May 2024 16:33:03 +0000 +Subject: [PATCH 04/11] ConfigParser: fix use-out-of-scope leaks + +--- + libdnf/conf/ConfigParser.cpp | 48 ++++++++++++++++++++++++------------ + 1 file changed, 32 insertions(+), 16 deletions(-) + +diff --git a/libdnf/conf/ConfigParser.cpp b/libdnf/conf/ConfigParser.cpp +index a89fd8bf..18e5a195 100644 +--- a/libdnf/conf/ConfigParser.cpp ++++ b/libdnf/conf/ConfigParser.cpp +@@ -95,7 +95,9 @@ std::pair ConfigParser::substitute_expression(const std::st + const auto & variable_key = res.substr(pos_variable, pos_after_variable - pos_variable); + const auto variable_mapping = substitutions.find(variable_key); + +- const std::string * variable_value = nullptr; ++ // No std::optional here. ++ bool variable_value_has_value{false}; ++ std::string variable_value{""}; + + if (variable_mapping == substitutions.end()) { + if (variable_key == "releasever_major" || variable_key == "releasever_minor") { +@@ -103,17 +105,22 @@ std::pair ConfigParser::substitute_expression(const std::st + if (releasever_mapping != substitutions.end()) { + const auto & releasever_split = ConfigParser::split_releasever(releasever_mapping->second); + if (variable_key == "releasever_major") { +- variable_value = &std::get<0>(releasever_split); ++ variable_value = std::get<0>(releasever_split); ++ variable_value_has_value = true; + } else { +- variable_value = &std::get<1>(releasever_split); ++ variable_value = std::get<1>(releasever_split); ++ variable_value_has_value = true; + } + } + } + } else { +- variable_value = &variable_mapping->second; ++ variable_value = variable_mapping->second; ++ variable_value_has_value = true; + } + +- const std::string * subst_str = nullptr; ++ // No std::optional here ++ std::string subst_str{""}; ++ bool subst_str_has_value{false}; + + size_t pos_after_variable_expression; + +@@ -153,20 +160,23 @@ std::pair ConfigParser::substitute_expression(const std::st + // If variable is unset or empty, the expansion of word is + // substituted. Otherwise, the value of variable is + // substituted. +- if (variable_value == nullptr || variable_value->empty()) { +- subst_str = &expanded_word; ++ if (!variable_value_has_value || variable_value.empty()) { ++ subst_str = expanded_word; ++ subst_str_has_value = true; + } else { + subst_str = variable_value; ++ subst_str_has_value = true; + } + } 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_value == nullptr || variable_value->empty()) { +- const std::string empty{}; +- subst_str = ∅ ++ if (!variable_value_has_value || variable_value.empty()) { ++ subst_str = ""; ++ subst_str_has_value = true; + } else { +- subst_str = &expanded_word; ++ subst_str = expanded_word; ++ subst_str_has_value = true; + } + } else { + // Unknown expansion mode, continue after the ':' +@@ -176,7 +186,10 @@ std::pair ConfigParser::substitute_expression(const std::st + pos_after_variable_expression = pos_after_word + 1; + } else if (res[pos_after_variable] == '}') { + // ${variable} +- subst_str = variable_value; ++ if (variable_value_has_value) { ++ subst_str = variable_value; ++ subst_str_has_value = true; ++ } + // Move past the closing '}' + pos_after_variable_expression = pos_after_variable + 1; + } else { +@@ -186,20 +199,23 @@ std::pair ConfigParser::substitute_expression(const std::st + } + } else { + // No braces, we have a $variable +- subst_str = variable_value; ++ if (variable_value_has_value) { ++ subst_str = variable_value; ++ subst_str_has_value = true; ++ } + 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) { ++ if (!subst_str_has_value) { + total_scanned += pos_after_variable_expression - pos; + pos = pos_after_variable_expression; + continue; + } + +- res.replace(pos, pos_after_variable_expression - pos, *subst_str); ++ res.replace(pos, pos_after_variable_expression - pos, subst_str); + total_scanned += pos_after_variable_expression - pos; +- pos += subst_str->length(); ++ pos += subst_str.length(); + } else { + total_scanned += 1; + pos += 1; +-- +2.49.0 + diff --git a/0027-Add-tests-for-shell-style-variable-expansion.patch b/0027-Add-tests-for-shell-style-variable-expansion.patch new file mode 100644 index 0000000..94e9637 --- /dev/null +++ b/0027-Add-tests-for-shell-style-variable-expansion.patch @@ -0,0 +1,38 @@ +From 89b931d8036499af18697332e16fe66955fdcee0 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Tue, 7 May 2024 16:28:59 +0000 +Subject: [PATCH 05/11] Add tests for shell-style variable expansion + +--- + tests/libdnf/conf/ConfigParserTest.cpp | 17 +++++++++++++++++ + 1 file changed, 17 insertions(+) + +diff --git a/tests/libdnf/conf/ConfigParserTest.cpp b/tests/libdnf/conf/ConfigParserTest.cpp +index 70278196..1448d8d3 100644 +--- a/tests/libdnf/conf/ConfigParserTest.cpp ++++ b/tests/libdnf/conf/ConfigParserTest.cpp +@@ -30,4 +30,21 @@ void ConfigParserTest::testConfigParserReleasever() + libdnf::ConfigParser::substitute(text, substitutions); + CPPUNIT_ASSERT(text == "major: , minor: "); + } ++ { ++ std::map substitutions = { ++ {"var1", "value123"}, ++ {"var2", "456"}, ++ }; ++ std::string text = "foo$var1-bar"; ++ libdnf::ConfigParser::substitute(text, substitutions); ++ CPPUNIT_ASSERT(text == "foovalue123-bar"); ++ ++ text = "${var1:+alternate}-${unset:-default}-${nn:+n${nn:-${nnn:}"; ++ libdnf::ConfigParser::substitute(text, substitutions); ++ CPPUNIT_ASSERT(text == "alternate-default-${nn:+n${nn:-${nnn:}"); ++ ++ text = "${unset:-${var1:+${var2:+$var2}}}"; ++ libdnf::ConfigParser::substitute(text, substitutions); ++ CPPUNIT_ASSERT(text == "456"); ++ } + } +-- +2.49.0 + diff --git a/0028-Split-releasever-to-releasever_major-and-releasever_.patch b/0028-Split-releasever-to-releasever_major-and-releasever_.patch new file mode 100644 index 0000000..8cb5aa2 --- /dev/null +++ b/0028-Split-releasever-to-releasever_major-and-releasever_.patch @@ -0,0 +1,156 @@ +From a131207df220a5f81e57f1a61931b0fdcba3325c Mon Sep 17 00:00:00 2001 +From: Diego Herrera +Date: Thu, 16 Jan 2025 18:29:40 -0300 +Subject: [PATCH 06/11] Split $releasever to $releasever_major and + $releasever_minor in c api + +--- + libdnf/dnf-repo.cpp | 6 ++++- + libdnf/dnf-utils.cpp | 44 ++++++++++++++++++++++++++++++++++++ + libdnf/dnf-utils.h | 3 +++ + tests/libdnf/dnf-self-test.c | 27 ++++++++++++++++++++++ + 4 files changed, 79 insertions(+), 1 deletion(-) + +diff --git a/libdnf/dnf-repo.cpp b/libdnf/dnf-repo.cpp +index 48434fd9..17aa04be 100644 +--- a/libdnf/dnf-repo.cpp ++++ b/libdnf/dnf-repo.cpp +@@ -1191,6 +1191,8 @@ dnf_repo_setup(DnfRepo *repo, GError **error) try + DnfRepoEnabled enabled = DNF_REPO_ENABLED_NONE; + g_autofree gchar *basearch = NULL; + g_autofree gchar *release = NULL; ++ g_autofree gchar *major = NULL; ++ g_autofree gchar *minor = NULL; + + basearch = g_key_file_get_string(priv->keyfile, "general", "arch", NULL); + if (basearch == NULL) +@@ -1218,9 +1220,11 @@ dnf_repo_setup(DnfRepo *repo, GError **error) try + return FALSE; + if (!lr_handle_setopt(priv->repo_handle, error, LRO_INTERRUPTIBLE, 0L)) + return FALSE; ++ dnf_split_releasever(release, &major, &minor); + priv->urlvars = lr_urlvars_set(priv->urlvars, "releasever", release); ++ priv->urlvars = lr_urlvars_set(priv->urlvars, "releasever_major", major); ++ priv->urlvars = lr_urlvars_set(priv->urlvars, "releasever_minor", minor); + priv->urlvars = lr_urlvars_set(priv->urlvars, "basearch", basearch); +- + /* Call libdnf::dnf_context_load_vars(priv->context); only when values not in cache. + * But what about if variables on disk change during long running programs (PackageKit daemon)? + * if (!libdnf::dnf_context_get_vars_cached(priv->context)) +diff --git a/libdnf/dnf-utils.cpp b/libdnf/dnf-utils.cpp +index 874282cf..43c84b82 100644 +--- a/libdnf/dnf-utils.cpp ++++ b/libdnf/dnf-utils.cpp +@@ -84,6 +84,50 @@ dnf_realpath(const gchar *path) + return real; + } + ++/** ++ * dnf_split_releasever: ++ * @releasever: A releasever string ++ * @releasever_major: Output string, or %NULL ++ * @releasever_minor: Output string, or %NULL ++ * ++ * Splits a releasever string into mayor and minor ++ * using the same logic as DNF 5 and as splitReleaseverTo in libzypp. ++ **/ ++void ++dnf_split_releasever(const gchar *releasever, ++ gchar **releasever_major, ++ gchar **releasever_minor) ++{ ++ g_autofree gchar** result = NULL; ++ ++ // Uses the same logic as DNF 5 and as splitReleaseverTo in libzypp ++ result = g_strsplit(releasever, ".", 2); ++ ++ if(result[0] == NULL) { ++ if(releasever_major != NULL) ++ *releasever_major = g_strdup(""); ++ if(releasever_minor != NULL) ++ *releasever_minor = g_strdup(""); ++ return; ++ } ++ else { ++ if(releasever_major != NULL) ++ *releasever_major = result[0]; ++ else ++ g_free(result[0]); ++ } ++ ++ if(result[1] == NULL) { ++ if(releasever_minor != NULL) ++ *releasever_minor = g_strdup(""); ++ } else { ++ if(releasever_minor != NULL) ++ *releasever_minor = result[1]; ++ else ++ g_free(result[1]); ++ } ++} ++ + /** + * dnf_remove_recursive: + * @directory: A directory path +diff --git a/libdnf/dnf-utils.h b/libdnf/dnf-utils.h +index 6b711918..c10dd53f 100644 +--- a/libdnf/dnf-utils.h ++++ b/libdnf/dnf-utils.h +@@ -53,6 +53,9 @@ extern "C" { + #endif + + gchar *dnf_realpath (const gchar *path); ++void dnf_split_releasever (const gchar *releasever, ++ gchar **releasever_major, ++ gchar **releasever_minor); + gboolean dnf_remove_recursive (const gchar *directory, + GError **error); + gboolean dnf_ensure_file_unlinked (const gchar *src_path, +diff --git a/tests/libdnf/dnf-self-test.c b/tests/libdnf/dnf-self-test.c +index 1e2bfac3..2a0371c9 100644 +--- a/tests/libdnf/dnf-self-test.c ++++ b/tests/libdnf/dnf-self-test.c +@@ -188,6 +188,32 @@ dnf_lock_threads_func(void) + g_object_unref(lock); + } + ++static void ++dnf_split_releasever_func(void) ++{ ++ gchar *major, *minor; ++ dnf_split_releasever("1.23.45", &major, &minor); ++ g_assert_cmpstr(major, ==, "1"); ++ g_assert_cmpstr(minor, ==, "23.45"); ++ g_free(major); ++ g_free(minor); ++ dnf_split_releasever("6.789", &major, &minor); ++ g_assert_cmpstr(major, ==, "6"); ++ g_assert_cmpstr(minor, ==, "789"); ++ g_free(major); ++ g_free(minor); ++ dnf_split_releasever("10", &major, &minor); ++ g_assert_cmpstr(major, ==, "10"); ++ g_assert_cmpstr(minor, ==, ""); ++ g_free(major); ++ g_free(minor); ++ dnf_split_releasever("", &major, &minor); ++ g_assert_cmpstr(major, ==, ""); ++ g_assert_cmpstr(minor, ==, ""); ++ g_free(major); ++ g_free(minor); ++} ++ + static void + ch_test_repo_func(void) + { +@@ -1239,6 +1265,7 @@ main(int argc, char **argv) + g_test_add_func("/libdnf/context{cache-clean-check}", dnf_context_cache_clean_check_func); + g_test_add_func("/libdnf/lock", dnf_lock_func); + g_test_add_func("/libdnf/lock[threads]", dnf_lock_threads_func); ++ g_test_add_func("/libdnf/split_releasever", dnf_split_releasever_func); + g_test_add_func("/libdnf/repo", ch_test_repo_func); + g_test_add_func("/libdnf/repo_empty_keyfile", dnf_repo_setup_with_empty_keyfile); + g_test_add_func("/libdnf/state", dnf_state_func); +-- +2.49.0 + diff --git a/0029-ConfigParser-make-splitReleasever-public.patch b/0029-ConfigParser-make-splitReleasever-public.patch new file mode 100644 index 0000000..add797f --- /dev/null +++ b/0029-ConfigParser-make-splitReleasever-public.patch @@ -0,0 +1,78 @@ +From 15f1efa3d3b60e2de34f130e9f4d190a92a5a4ed Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Tue, 21 Jan 2025 19:19:06 +0000 +Subject: [PATCH 07/11] ConfigParser: make splitReleasever public + +--- + bindings/swig/conf.i | 1 + + libdnf/conf/ConfigParser.cpp | 6 +++--- + libdnf/conf/ConfigParser.hpp | 3 +-- + 3 files changed, 5 insertions(+), 5 deletions(-) + +diff --git a/bindings/swig/conf.i b/bindings/swig/conf.i +index b6a0ce88..c42d5673 100644 +--- a/bindings/swig/conf.i ++++ b/bindings/swig/conf.i +@@ -199,6 +199,7 @@ public: + std::string & getHeader() noexcept; + const Container & getData() const noexcept; + Container & getData() noexcept; ++ static std::pair splitReleasever(const std::string & releasever); + }; + } + %clear std::string & text; +diff --git a/libdnf/conf/ConfigParser.cpp b/libdnf/conf/ConfigParser.cpp +index 18e5a195..6ff110a7 100644 +--- a/libdnf/conf/ConfigParser.cpp ++++ b/libdnf/conf/ConfigParser.cpp +@@ -103,7 +103,7 @@ std::pair ConfigParser::substitute_expression(const std::st + if (variable_key == "releasever_major" || variable_key == "releasever_minor") { + const auto releasever_mapping = substitutions.find("releasever"); + if (releasever_mapping != substitutions.end()) { +- const auto & releasever_split = ConfigParser::split_releasever(releasever_mapping->second); ++ const auto & releasever_split = ConfigParser::splitReleasever(releasever_mapping->second); + if (variable_key == "releasever_major") { + variable_value = std::get<0>(releasever_split); + variable_value_has_value = true; +@@ -231,7 +231,7 @@ std::pair ConfigParser::substitute_expression(const std::st + return std::make_pair(res, text.length()); + } + +-std::tuple ConfigParser::split_releasever(const std::string & releasever) ++std::pair ConfigParser::splitReleasever(const std::string & releasever) + { + // Uses the same logic as DNF 5 and as splitReleaseverTo in libzypp + std::string releasever_major; +@@ -243,7 +243,7 @@ std::tuple ConfigParser::split_releasever(const std::s + releasever_major = releasever.substr(0, pos); + releasever_minor = releasever.substr(pos + 1); + } +- return std::make_tuple(releasever_major, releasever_minor); ++ return std::make_pair(releasever_major, releasever_minor); + } + + static void read(ConfigParser & cfgParser, IniParser & parser) +diff --git a/libdnf/conf/ConfigParser.hpp b/libdnf/conf/ConfigParser.hpp +index f30dd4a4..2d269147 100644 +--- a/libdnf/conf/ConfigParser.hpp ++++ b/libdnf/conf/ConfigParser.hpp +@@ -143,6 +143,7 @@ public: + std::string & getHeader() noexcept; + const Container & getData() const noexcept; + Container & getData() noexcept; ++ static std::pair splitReleasever(const std::string & releasever); + + private: + std::map substitutions; +@@ -162,8 +163,6 @@ private: + static std::pair substitute_expression(const std::string & text, + const std::map & substitutions, + unsigned int depth); +- +- static std::tuple split_releasever(const std::string & releasever); + }; + + inline void ConfigParser::setSubstitutions(const std::map & substitutions) +-- +2.49.0 + diff --git a/0030-C-API-Detect-releasever_major-releasever_minor-from-.patch b/0030-C-API-Detect-releasever_major-releasever_minor-from-.patch new file mode 100644 index 0000000..81fe57a --- /dev/null +++ b/0030-C-API-Detect-releasever_major-releasever_minor-from-.patch @@ -0,0 +1,147 @@ +From beff5b9e184de2df12be744f8e776846dda49459 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Mon, 3 Feb 2025 20:31:12 +0000 +Subject: [PATCH 08/11] C API: Detect releasever_major, releasever_minor from + provides + +releasever_major and releasever_minor can now be overridden by virtual +provides on the system-release package (any of `DISTROVERPKG`). The +detection of releasever is unchanged. releasever_major and +releasever_minor are specified by the versions of the +`system-release-major` and `system-release-minor` provides, +respectively. + +Introduces dnf_context_set_release_ver_major and +dnf_context_set_release_ver_minor. +--- + libdnf/dnf-context.cpp | 65 ++++++++++++++++++++++++++++++++++++++++-- + libdnf/dnf-context.h | 4 +++ + 2 files changed, 66 insertions(+), 3 deletions(-) + +diff --git a/libdnf/dnf-context.cpp b/libdnf/dnf-context.cpp +index 97d1c599..d51cb08c 100644 +--- a/libdnf/dnf-context.cpp ++++ b/libdnf/dnf-context.cpp +@@ -85,6 +85,8 @@ + #define MAX_NATIVE_ARCHES 12 + + #define RELEASEVER_PROV "system-release(releasever)" ++#define RELEASEVER_MAJOR_PROV "system-release(releasever_major)" ++#define RELEASEVER_MINOR_PROV "system-release(releasever_minor)" + + /* data taken from https://github.com/rpm-software-management/dnf/blob/master/dnf/arch.py */ + static const struct { +@@ -142,6 +144,8 @@ typedef struct + gchar **installonlypkgs; + gchar *base_arch; + gchar *release_ver; ++ gchar *release_ver_major; ++ gchar *release_ver_minor; + gchar *platform_module; + gchar *cache_dir; + gchar *solv_dir; +@@ -1263,7 +1267,9 @@ dnf_context_set_vars_dir(DnfContext *context, const gchar * const *vars_dir) + * @context: a #DnfContext instance. + * @release_ver: the release version, e.g. "20" + * +- * Sets the release version. ++ * Sets the release version. Sets the major and minor release version by splitting `release_ver` on ++ * the first ".". The derived major and minor versions can later be overridden by calling ++ *`dnf_context_set_release_ver_major` and `dnf_context_set_release_ver_minor`, respectively. + * + * Since: 0.1.0 + **/ +@@ -1273,6 +1279,46 @@ dnf_context_set_release_ver(DnfContext *context, const gchar *release_ver) + DnfContextPrivate *priv = GET_PRIVATE(context); + g_free(priv->release_ver); + priv->release_ver = g_strdup(release_ver); ++ ++ g_free(priv->release_ver_major); ++ g_free(priv->release_ver_minor); ++ dnf_split_releasever(release_ver, &priv->release_ver_major, &priv->release_ver_minor); ++} ++ ++/** ++ * dnf_context_set_release_ver_major: ++ * @context: a #DnfContext instance. ++ * @release_ver_major: the release major version, e.g. "10" ++ * ++ * Sets the release major version, which is usually derived by splitting releasever on the first ++ * ".". This setter does not update the value of $releasever. ++ * ++ * Since: 0.74.0 ++ **/ ++void ++dnf_context_set_release_ver_major(DnfContext *context, const gchar *release_ver_major) ++{ ++ DnfContextPrivate *priv = GET_PRIVATE(context); ++ g_free(priv->release_ver_major); ++ priv->release_ver_major = g_strdup(release_ver_major); ++} ++ ++/** ++ * dnf_context_set_release_ver_minor: ++ * @context: a #DnfContext instance. ++ * @release_ver_minor: the release minor version, e.g. "1" ++ * ++ * Sets the release minor version, which is usually derived by splitting releasever on the first ++ * ".". This setter does not update the value of $releasever. ++ * ++ * Since: 0.74.0 ++ **/ ++void ++dnf_context_set_release_ver_minor(DnfContext *context, const gchar *release_ver_minor) ++{ ++ DnfContextPrivate *priv = GET_PRIVATE(context); ++ g_free(priv->release_ver_minor); ++ priv->release_ver_minor = g_strdup(release_ver_minor); + } + + /** +@@ -1649,13 +1695,26 @@ dnf_context_set_os_release(DnfContext *context, GError **error) try + Header hdr; + while ((hdr = rpmdbNextIterator (mi)) != NULL) { + const char *v = headerGetString (hdr, RPMTAG_VERSION); ++ const char *v_major = nullptr; ++ const char *v_minor = nullptr; + rpmds ds = rpmdsNew (hdr, RPMTAG_PROVIDENAME, 0); + while (rpmdsNext (ds) >= 0) { +- if (strcmp (rpmdsN (ds), RELEASEVER_PROV) == 0 && rpmdsFlags (ds) == RPMSENSE_EQUAL) ++ if (strcmp (rpmdsN (ds), RELEASEVER_PROV) == 0 && rpmdsFlags (ds) == RPMSENSE_EQUAL) { + v = rpmdsEVR (ds); ++ } else if (strcmp (rpmdsN (ds), RELEASEVER_MAJOR_PROV) == 0 && rpmdsFlags (ds) == RPMSENSE_EQUAL) { ++ v_major = rpmdsEVR(ds); ++ } else if (strcmp (rpmdsN (ds), RELEASEVER_MINOR_PROV) == 0 && rpmdsFlags (ds) == RPMSENSE_EQUAL) { ++ v_minor = rpmdsEVR(ds); ++ } + } + found_in_rpmdb = TRUE; +- dnf_context_set_release_ver (context, v); ++ dnf_context_set_release_ver(context, v); ++ if (v_major != nullptr) { ++ dnf_context_set_release_ver_major(context, v_major); ++ } ++ if (v_minor != nullptr) { ++ dnf_context_set_release_ver_minor(context, v_minor); ++ } + rpmdsFree (ds); + break; + } +diff --git a/libdnf/dnf-context.h b/libdnf/dnf-context.h +index cb00a29b..4d8481b2 100644 +--- a/libdnf/dnf-context.h ++++ b/libdnf/dnf-context.h +@@ -164,6 +164,10 @@ void dnf_context_set_vars_dir (DnfContext *context + const gchar * const *vars_dir); + void dnf_context_set_release_ver (DnfContext *context, + const gchar *release_ver); ++void dnf_context_set_release_ver_major (DnfContext *context, ++ const gchar *release_ver_major); ++void dnf_context_set_release_ver_minor (DnfContext *context, ++ const gchar *release_ver_minor); + void dnf_context_set_platform_module (DnfContext *context, + const gchar *platform_module); + void dnf_context_set_cache_dir (DnfContext *context, +-- +2.49.0 + diff --git a/0031-C-API-Use-releasever_-major-minor-from-context-inste.patch b/0031-C-API-Use-releasever_-major-minor-from-context-inste.patch new file mode 100644 index 0000000..9b1e8d1 --- /dev/null +++ b/0031-C-API-Use-releasever_-major-minor-from-context-inste.patch @@ -0,0 +1,121 @@ +From 80f6ccc3f3f28ddc48e1f175e7948b0c1a663337 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Mon, 3 Feb 2025 16:06:35 -0500 +Subject: [PATCH 09/11] C API: Use releasever_{major,minor} from context + instead of always splitting + +Introduces dnf_context_get_release_ver_major and +dnf_context_get_release_ver_minor. +--- + libdnf/dnf-context.cpp | 36 ++++++++++++++++++++++++++++++++++++ + libdnf/dnf-context.h | 2 ++ + libdnf/dnf-repo.cpp | 17 +++++++++++------ + 3 files changed, 49 insertions(+), 6 deletions(-) + +diff --git a/libdnf/dnf-context.cpp b/libdnf/dnf-context.cpp +index d51cb08c..ffd6b662 100644 +--- a/libdnf/dnf-context.cpp ++++ b/libdnf/dnf-context.cpp +@@ -620,6 +620,42 @@ dnf_context_get_release_ver(DnfContext *context) + return priv->release_ver; + } + ++/** ++ * dnf_context_get_release_ver_major: ++ * @context: a #DnfContext instance. ++ * ++ * Gets the release major version. Usually derived by taking the substring of releasever before the ++ * first ".", but can be overridden by the distribution. ++ * ++ * Returns: the release major version, e.g. "10" ++ * ++ * Since: 0.74.0 ++ **/ ++const gchar * ++dnf_context_get_release_ver_major(DnfContext *context) ++{ ++ DnfContextPrivate *priv = GET_PRIVATE(context); ++ return priv->release_ver_major; ++} ++ ++/** ++ * dnf_context_get_release_ver_minor: ++ * @context: a #DnfContext instance. ++ * ++ * Gets the release minor version. Usually derived by taking the substring of releasever after the ++ * first ".", but can be overridden by the distribution. ++ * ++ * Returns: the release minor version, e.g. "1" ++ * ++ * Since: 0.74.0 ++ **/ ++const gchar * ++dnf_context_get_release_ver_minor(DnfContext *context) ++{ ++ DnfContextPrivate *priv = GET_PRIVATE(context); ++ return priv->release_ver_minor; ++} ++ + /** + * dnf_context_get_platform_module: + * @context: a #DnfContext instance. +diff --git a/libdnf/dnf-context.h b/libdnf/dnf-context.h +index 4d8481b2..8e1c948e 100644 +--- a/libdnf/dnf-context.h ++++ b/libdnf/dnf-context.h +@@ -120,6 +120,8 @@ const gchar *dnf_context_get_base_arch (DnfContext *context + const gchar *dnf_context_get_os_info (DnfContext *context); + const gchar *dnf_context_get_arch_info (DnfContext *context); + const gchar *dnf_context_get_release_ver (DnfContext *context); ++const gchar *dnf_context_get_release_ver_major (DnfContext *context); ++const gchar *dnf_context_get_release_ver_minor (DnfContext *context); + const gchar *dnf_context_get_platform_module (DnfContext *context); + const gchar *dnf_context_get_cache_dir (DnfContext *context); + const gchar *dnf_context_get_arch (DnfContext *context); +diff --git a/libdnf/dnf-repo.cpp b/libdnf/dnf-repo.cpp +index 17aa04be..00555ada 100644 +--- a/libdnf/dnf-repo.cpp ++++ b/libdnf/dnf-repo.cpp +@@ -1191,8 +1191,8 @@ dnf_repo_setup(DnfRepo *repo, GError **error) try + DnfRepoEnabled enabled = DNF_REPO_ENABLED_NONE; + g_autofree gchar *basearch = NULL; + g_autofree gchar *release = NULL; +- g_autofree gchar *major = NULL; +- g_autofree gchar *minor = NULL; ++ g_autofree gchar *release_major = NULL; ++ g_autofree gchar *release_minor = NULL; + + basearch = g_key_file_get_string(priv->keyfile, "general", "arch", NULL); + if (basearch == NULL) +@@ -1205,8 +1205,14 @@ dnf_repo_setup(DnfRepo *repo, GError **error) try + return FALSE; + } + release = g_key_file_get_string(priv->keyfile, "general", "version", NULL); +- if (release == NULL) ++ if (release == NULL) { + release = g_strdup(dnf_context_get_release_ver(priv->context)); ++ release_major = g_strdup(dnf_context_get_release_ver_major(priv->context)); ++ release_minor = g_strdup(dnf_context_get_release_ver_minor(priv->context)); ++ } else { ++ dnf_split_releasever(release, &release_major, &release_minor); ++ } ++ + if (release == NULL) { + g_set_error_literal(error, + DNF_ERROR, +@@ -1220,10 +1226,9 @@ dnf_repo_setup(DnfRepo *repo, GError **error) try + return FALSE; + if (!lr_handle_setopt(priv->repo_handle, error, LRO_INTERRUPTIBLE, 0L)) + return FALSE; +- dnf_split_releasever(release, &major, &minor); + priv->urlvars = lr_urlvars_set(priv->urlvars, "releasever", release); +- priv->urlvars = lr_urlvars_set(priv->urlvars, "releasever_major", major); +- priv->urlvars = lr_urlvars_set(priv->urlvars, "releasever_minor", minor); ++ priv->urlvars = lr_urlvars_set(priv->urlvars, "releasever_major", release_major); ++ priv->urlvars = lr_urlvars_set(priv->urlvars, "releasever_minor", release_minor); + priv->urlvars = lr_urlvars_set(priv->urlvars, "basearch", basearch); + /* Call libdnf::dnf_context_load_vars(priv->context); only when values not in cache. + * But what about if variables on disk change during long running programs (PackageKit daemon)? +-- +2.49.0 + diff --git a/0032-C-API-support-shell-style-variable-substitution.patch b/0032-C-API-support-shell-style-variable-substitution.patch new file mode 100644 index 0000000..d712854 --- /dev/null +++ b/0032-C-API-support-shell-style-variable-substitution.patch @@ -0,0 +1,60 @@ +From 39d5473950b5ea0a3c75c30ad729538143fcb017 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Fri, 7 Feb 2025 18:02:20 +0000 +Subject: [PATCH 10/11] C API: support shell-style variable substitution + +Rework `dnf_repo_substitute` to call the C++ API's +ConfigParser::substitute instead of librepo's lr_url_substitute. + +Resolves https://github.com/rpm-software-management/libdnf/issues/1690 +--- + libdnf/dnf-repo.cpp | 17 +++++++++++------ + 1 file changed, 11 insertions(+), 6 deletions(-) + +diff --git a/libdnf/dnf-repo.cpp b/libdnf/dnf-repo.cpp +index 00555ada..86c31be0 100644 +--- a/libdnf/dnf-repo.cpp ++++ b/libdnf/dnf-repo.cpp +@@ -34,6 +34,7 @@ + */ + + #include "conf/OptionBool.hpp" ++#include "conf/ConfigParser.hpp" + + #include "dnf-context.hpp" + #include "hy-repo-private.hpp" +@@ -45,6 +46,7 @@ + #include + #include "hy-util.h" + #include ++#include + #include + #include + +@@ -242,14 +244,17 @@ static gchar * + dnf_repo_substitute(DnfRepo *repo, const gchar *url) + { + DnfRepoPrivate *priv = GET_PRIVATE(repo); +- char *tmp; +- gchar *substituted; + +- /* do a little dance so we can use g_free() rather than lr_free() */ +- tmp = lr_url_substitute(url, priv->urlvars); +- substituted = g_strdup(tmp); +- lr_free(tmp); ++ std::map substitutions; ++ for (LrUrlVars *elem = priv->urlvars; elem; elem = g_slist_next(elem)) { ++ const auto * pair = static_cast(elem->data); ++ substitutions.insert({std::string{pair->var}, std::string{pair->val}}); ++ } ++ ++ std::string tmp{url}; ++ libdnf::ConfigParser::substitute(tmp, substitutions); + ++ auto * substituted = g_strdup(tmp.c_str()); + return substituted; + } + +-- +2.49.0 + diff --git a/0033-C-API-test-shell-style-variable-expressions.patch b/0033-C-API-test-shell-style-variable-expressions.patch new file mode 100644 index 0000000..1b9acde --- /dev/null +++ b/0033-C-API-test-shell-style-variable-expressions.patch @@ -0,0 +1,80 @@ +From bb6638eafa4c82cbbc672645dbf30575e7453b15 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Fri, 7 Feb 2025 18:42:43 +0000 +Subject: [PATCH 11/11] C API: test shell-style variable expressions + +--- + data/tests/vars/var1 | 1 + + data/tests/vars/var2 | 1 + + data/tests/yum.repos.d/shell-expansion.repo | 5 +++++ + tests/libdnf/dnf-self-test.c | 10 ++++++++++ + 4 files changed, 17 insertions(+) + create mode 100644 data/tests/vars/var1 + create mode 100644 data/tests/vars/var2 + create mode 100644 data/tests/yum.repos.d/shell-expansion.repo + +diff --git a/data/tests/vars/var1 b/data/tests/vars/var1 +new file mode 100644 +index 00000000..a9f37252 +--- /dev/null ++++ b/data/tests/vars/var1 +@@ -0,0 +1 @@ ++value123 +diff --git a/data/tests/vars/var2 b/data/tests/vars/var2 +new file mode 100644 +index 00000000..8d38505c +--- /dev/null ++++ b/data/tests/vars/var2 +@@ -0,0 +1 @@ ++456 +diff --git a/data/tests/yum.repos.d/shell-expansion.repo b/data/tests/yum.repos.d/shell-expansion.repo +new file mode 100644 +index 00000000..3bd4c1ec +--- /dev/null ++++ b/data/tests/yum.repos.d/shell-expansion.repo +@@ -0,0 +1,5 @@ ++[shell-expansion] ++name=${unset:-${var1:+${var2:+$var2}}} ++baseurl=https://${unset:-${var1:+${var2:+$var2}}} ++enabled=1 ++gpgcheck=0 +diff --git a/tests/libdnf/dnf-self-test.c b/tests/libdnf/dnf-self-test.c +index 2a0371c9..1596870e 100644 +--- a/tests/libdnf/dnf-self-test.c ++++ b/tests/libdnf/dnf-self-test.c +@@ -842,6 +842,7 @@ dnf_repo_loader_func(void) + DnfState *state; + gboolean ret; + g_autofree gchar *repos_dir = NULL; ++ g_autofree gchar *vars_dir = NULL; + g_autoptr(DnfContext) ctx = NULL; + g_autoptr(DnfRepoLoader) repo_loader = NULL; + guint metadata_expire; +@@ -849,8 +850,10 @@ dnf_repo_loader_func(void) + /* set up local context */ + ctx = dnf_context_new(); + repos_dir = dnf_test_get_filename("yum.repos.d"); ++ vars_dir = dnf_test_get_filename("vars"); + dnf_context_set_repo_dir(ctx, repos_dir); + dnf_context_set_solv_dir(ctx, "/tmp"); ++ dnf_context_set_vars_dir(ctx, (const gchar *[]){vars_dir, NULL}); + ret = dnf_context_setup(ctx, NULL, &error); + g_assert_no_error(error); + g_assert(ret); +@@ -906,6 +909,13 @@ dnf_repo_loader_func(void) + g_assert_error(error, DNF_ERROR, DNF_ERROR_REPO_NOT_AVAILABLE); + g_assert(!ret); + g_clear_error(&error); ++ ++ /* check that shell-style variable expressions are correctly expanded in repo values */ ++ dnf_state_reset(state); ++ repo = dnf_repo_loader_get_repo_by_id(repo_loader, "shell-expansion", &error); ++ g_assert_no_error(error); ++ g_assert(repo != NULL); ++ g_assert_cmpstr(dnf_repo_get_description(repo), ==, "456"); + } + + static void +-- +2.49.0 + diff --git a/libdnf.spec b/libdnf.spec index c74c115..946d5e6 100644 --- a/libdnf.spec +++ b/libdnf.spec @@ -58,7 +58,7 @@ Name: libdnf Version: %{libdnf_major_version}.%{libdnf_minor_version}.%{libdnf_micro_version} -Release: 15%{?dist} +Release: 16%{?dist} Summary: Library providing simplified C and Python API to libsolv License: LGPLv2+ URL: https://github.com/rpm-software-management/libdnf @@ -85,6 +85,17 @@ Patch19: 0019-module-Warn-if-module-config-file-is-inaccessible.patch Patch20: 0020-history-DB-Add-persistence-column.patch Patch21: 0021-MergedTransaction-listPersistences.patch Patch22: 0022-conf-Add-usr_drift_protected_paths.patch +Patch23: 0023-conf-Add-limited-shell-style-variable-expansion.patch +Patch24: 0024-conf-split-releasever-to-releasever_major-and-releas.patch +Patch25: 0025-Test-for-releasever_major-releasever_minor.patch +Patch26: 0026-ConfigParser-fix-use-out-of-scope-leaks.patch +Patch27: 0027-Add-tests-for-shell-style-variable-expansion.patch +Patch28: 0028-Split-releasever-to-releasever_major-and-releasever_.patch +Patch29: 0029-ConfigParser-make-splitReleasever-public.patch +Patch30: 0030-C-API-Detect-releasever_major-releasever_minor-from-.patch +Patch31: 0031-C-API-Use-releasever_-major-minor-from-context-inste.patch +Patch32: 0032-C-API-support-shell-style-variable-substitution.patch +Patch33: 0033-C-API-test-shell-style-variable-expressions.patch BuildRequires: cmake @@ -334,6 +345,10 @@ popd %endif %changelog +* Mon Jun 30 2025 Evan Goode - 0.69.0-16 +- Introduce $releasever_major, $releasever_minor variables, shell-style + variable substitution (RHEL-95006) + * Thu Jun 26 2025 Evan Goode - 0.69.0-15 - history DB: Add "persistence" column (RHEL-100623) - conf: Add bootc_unsafe_paths (RHEL-100622)