Split $releasever_{major,minor}, shell-style variable expansion

Resolves: RHEL-95006
This commit is contained in:
Evan Goode 2025-06-30 19:26:17 +00:00
parent 9bb30a22ba
commit 03cea2d0b5
12 changed files with 1320 additions and 1 deletions

View File

@ -0,0 +1,261 @@
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

View File

@ -0,0 +1,148 @@
From 89053c30a56da51849ffd5f4323ba3ef04eb8fcd Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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<std::string, size_t> ConfigParser::substitute_expression(const std::st
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 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<std::string, size_t> 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 = &empty;
} else {
@@ -156,9 +176,7 @@ std::pair<std::string, size_t> 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<std::string, size_t> 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<std::string, size_t> ConfigParser::substitute_expression(const std::st
return std::make_pair(res, text.length());
}
+std::tuple<std::string, std::string> 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<std::string, size_t> substitute_expression(const std::string & text,
const std::map<std::string, std::string> & substitutions,
unsigned int depth);
+
+ static std::tuple<std::string, std::string> split_releasever(const std::string & releasever);
};
inline void ConfigParser::setSubstitutions(const std::map<std::string, std::string> & 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

View File

@ -0,0 +1,91 @@
From 734a573408643c90faf7ff57ea33fec5fb467553 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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<std::string, std::string> 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<std::string, std::string> 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 <cppunit/TestCase.h>
+#include <cppunit/extensions/HelperMacros.h>
+
+#include <libdnf/conf/ConfigParser.hpp>
+
+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

View File

@ -0,0 +1,124 @@
From ff0911502ae7a71f9a872d93f91c395f2f04de4a Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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<std::string, size_t> 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<std::string, size_t> 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<std::string, size_t> 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 = &empty;
+ 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<std::string, size_t> 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<std::string, size_t> 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

View File

@ -0,0 +1,38 @@
From 89b931d8036499af18697332e16fe66955fdcee0 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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<std::string, std::string> 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

View File

@ -0,0 +1,156 @@
From a131207df220a5f81e57f1a61931b0fdcba3325c Mon Sep 17 00:00:00 2001
From: Diego Herrera <dherrera@redhat.com>
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

View File

@ -0,0 +1,78 @@
From 15f1efa3d3b60e2de34f130e9f4d190a92a5a4ed Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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<std::string, std::string> 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<std::string, size_t> 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<std::string, size_t> ConfigParser::substitute_expression(const std::st
return std::make_pair(res, text.length());
}
-std::tuple<std::string, std::string> ConfigParser::split_releasever(const std::string & releasever)
+std::pair<std::string, std::string> 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<std::string, std::string> 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<std::string, std::string> splitReleasever(const std::string & releasever);
private:
std::map<std::string, std::string> substitutions;
@@ -162,8 +163,6 @@ private:
static std::pair<std::string, size_t> substitute_expression(const std::string & text,
const std::map<std::string, std::string> & substitutions,
unsigned int depth);
-
- static std::tuple<std::string, std::string> split_releasever(const std::string & releasever);
};
inline void ConfigParser::setSubstitutions(const std::map<std::string, std::string> & substitutions)
--
2.49.0

View File

@ -0,0 +1,147 @@
From beff5b9e184de2df12be744f8e776846dda49459 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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

View File

@ -0,0 +1,121 @@
From 80f6ccc3f3f28ddc48e1f175e7948b0c1a663337 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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

View File

@ -0,0 +1,60 @@
From 39d5473950b5ea0a3c75c30ad729538143fcb017 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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 <glib/gstdio.h>
#include "hy-util.h"
#include <librepo/librepo.h>
+#include <librepo/url_substitution.h>
#include <rpm/rpmts.h>
#include <librepo/yum.h>
@@ -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<std::string, std::string> substitutions;
+ for (LrUrlVars *elem = priv->urlvars; elem; elem = g_slist_next(elem)) {
+ const auto * pair = static_cast<LrVar*>(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

View File

@ -0,0 +1,80 @@
From bb6638eafa4c82cbbc672645dbf30575e7453b15 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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

View File

@ -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 <egoode@redhat.com> - 0.69.0-16
- Introduce $releasever_major, $releasever_minor variables, shell-style
variable substitution (RHEL-95006)
* Thu Jun 26 2025 Evan Goode <egoode@redhat.com> - 0.69.0-15
- history DB: Add "persistence" column (RHEL-100623)
- conf: Add bootc_unsafe_paths (RHEL-100622)