From ac335da5352f8c6119f342bd643133aa49ee8e9c Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Mon, 6 Jul 2026 11:57:00 +0200 Subject: [PATCH 1/2] Extended string_replace with list arguments Changelog: Title Ticket: CFE-4694 Signed-off-by: Victor Moene --- libpromises/evalfunction.c | 121 ++++++++++++++++++++++++++++++++----- 1 file changed, 106 insertions(+), 15 deletions(-) diff --git a/libpromises/evalfunction.c b/libpromises/evalfunction.c index 922149a359..33bdd34464 100644 --- a/libpromises/evalfunction.c +++ b/libpromises/evalfunction.c @@ -8974,11 +8974,13 @@ static FnCallResult FnCallStringSplit(ARG_UNUSED EvalContext *ctx, /*********************************************************************/ -static FnCallResult FnCallStringReplace(ARG_UNUSED EvalContext *ctx, +static FnCallResult FnCallStringReplace(EvalContext *ctx, ARG_UNUSED Policy const *policy, - ARG_UNUSED FnCall const *fp, - Rlist const *finalargs) + const FnCall *fp, + const Rlist *finalargs) { + assert(fp != NULL); + assert(finalargs != NULL); if (finalargs->next == NULL || finalargs->next->next == NULL) { Log(LOG_LEVEL_WARNING, @@ -8987,21 +8989,109 @@ static FnCallResult FnCallStringReplace(ARG_UNUSED EvalContext *ctx, return FnFailure(); } - char *string = RlistScalarValue(finalargs); - char *match = RlistScalarValue(finalargs->next); - char *substitute = RlistScalarValue(finalargs->next->next); + char *input_string = RlistScalarValue(finalargs); + char *ret = NULL; - char *ret = SearchAndReplace(string, match, substitute); + char *option = "strings"; + if (finalargs->next->next->next != NULL) + { + option = RlistScalarValue(finalargs->next->next->next); + } - if (ret == NULL) + if (StringEqual(option, "strings")) { - Log(LOG_LEVEL_WARNING, - "Failed to replace with function '%s', string: '%s', match: '%s', " - "substitute: '%s'", - fp->name, string, match, substitute); + char *match_string = RlistScalarValue(finalargs->next); + char *substitute_string = RlistScalarValue(finalargs->next->next); + + ret = SearchAndReplace(input_string, match_string, substitute_string); + if (ret == NULL) + { + Log(LOG_LEVEL_WARNING, + "Failed to replace with function '%s', string: '%s', " + "match: '%s', substitute: '%s'", + fp->name, input_string, match_string, substitute_string); + return FnFailure(); + } + return FnReturnNoCopy(ret); + } + + assert(StringEqual(option, "lists")); + bool match_allocated = false; + JsonElement *match = VarNameOrInlineToJson(ctx, fp, finalargs->next, true, &match_allocated); + + if (match == NULL) + { + Log(LOG_LEVEL_ERR, "Failed to replace with function '%s' in '%s': " + "Couldn't resolve match pattern '%s' to an array" + , fp->name, input_string, RlistScalarValue(finalargs->next)); return FnFailure(); } + if (JsonGetType(match) != JSON_TYPE_ARRAY) + { + Log(LOG_LEVEL_ERR, "Failed to replace with function '%s' in '%s': " + "Match pattern expected 'array' but got '%s'" + , fp->name, input_string, JsonGetTypeAsString(match)); + goto cleanup_match; + } + + bool substitute_allocated = false; + JsonElement *substitute = VarNameOrInlineToJson(ctx, fp, finalargs->next->next, true, &substitute_allocated); + if (substitute == NULL) + { + Log(LOG_LEVEL_ERR, "Failed to replace with function '%s' in '%s': " + "Couldn't resolve substitute pattern '%s' to an array" + , fp->name, input_string, RlistScalarValue(finalargs->next->next)); + goto cleanup_match; + } + + if (JsonGetType(substitute) != JSON_TYPE_ARRAY) + { + Log(LOG_LEVEL_ERR, "Failed to replace with function '%s' in '%s': " + "Substitute pattern expected 'array' but got '%s'" + , fp->name, input_string, JsonGetTypeAsString(substitute)); + goto cleanup_substitute; + } + + if (JsonLength(match) != JsonLength(substitute)) + { + Log(LOG_LEVEL_ERR, "Failed to replace with function '%s' in '%s': " + "Match and substitute pattern lists must have equal lengths", + fp->name, input_string); + goto cleanup_substitute; + } + + char *working = SafeStringDuplicate(input_string); + for (size_t i = 0; i < JsonLength(match); i++) + { + const char *m = JsonPrimitiveGetAsString(JsonAt(match, i)); + const char *s = JsonPrimitiveGetAsString(JsonAt(substitute, i)); + + char *next = SearchAndReplace(working, m, s); + free(working); + + if (next == NULL) + { + Log(LOG_LEVEL_WARNING, + "Failed to replace with function '%s', string: '%s', " + "match: '%s', substitute: '%s'", + fp->name, input_string, m, s); + working = NULL; + break; + } + working = next; + } + ret = working; + +cleanup_substitute: + JsonDestroyMaybe(substitute, substitute_allocated); +cleanup_match: + JsonDestroyMaybe(match, match_allocated); + + if (ret == NULL) + { + return FnFailure(); + } return FnReturnNoCopy(ret); } @@ -11272,8 +11362,9 @@ static const FnCallArg STRFTIME_ARGS[] = static const FnCallArg STRING_REPLACE_ARGS[] = { {CF_ANYSTRING, CF_DATA_TYPE_STRING, "Source string"}, - {CF_ANYSTRING, CF_DATA_TYPE_STRING, "String to replace"}, - {CF_ANYSTRING, CF_DATA_TYPE_STRING, "Replacement string"}, + {CF_ANYSTRING, CF_DATA_TYPE_STRING, "String or list of strings to replace"}, + {CF_ANYSTRING, CF_DATA_TYPE_STRING, "Replacement string or list of strings"}, + {"strings,lists", CF_DATA_TYPE_OPTION, "Whether to expect strings or list of strings"}, {NULL, CF_DATA_TYPE_NONE, NULL} }; @@ -11873,7 +11964,7 @@ const FnCallType CF_FNCALL_TYPES[] = FnCallTypeNew("string_split", CF_DATA_TYPE_STRING_LIST, SPLITSTRING_ARGS, &FnCallStringSplit, "Convert a string in arg1 into a list of at most arg3 strings by splitting on a regular expression in arg2", FNCALL_OPTION_NONE, FNCALL_CATEGORY_DATA, SYNTAX_STATUS_NORMAL, DEFAULT_ARGC), FnCallTypeNew("string_replace", CF_DATA_TYPE_STRING, STRING_REPLACE_ARGS, &FnCallStringReplace, "Search through arg1, replacing occurences of arg2 with arg3.", - FNCALL_OPTION_NONE, FNCALL_CATEGORY_DATA, SYNTAX_STATUS_NORMAL, DEFAULT_ARGC), + FNCALL_OPTION_COLLECTING | FNCALL_OPTION_VARARG, FNCALL_CATEGORY_DATA, SYNTAX_STATUS_NORMAL, ARGC(3, 4)), FnCallTypeNew("string_trim", CF_DATA_TYPE_STRING, STRING_TRIM_ARGS, &FnCallStringTrim, "Trim whitespace from beginning and end of string", FNCALL_OPTION_NONE, FNCALL_CATEGORY_DATA, SYNTAX_STATUS_NORMAL, DEFAULT_ARGC), FnCallTypeNew("regex_replace", CF_DATA_TYPE_STRING, REGEX_REPLACE_ARGS, &FnCallRegReplace, "Replace occurrences of arg1 in arg2 with arg3, allowing backreferences. Perl-style options accepted in arg4.", From 3c3892f6132f4cf2aedcda99dad42e4e70895300 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Mon, 6 Jul 2026 13:44:41 +0200 Subject: [PATCH 2/2] Updated tests for string_replace with lists Signed-off-by: Victor Moene --- examples/string_replace.cf | 14 ++++++++++++++ .../01_vars/02_functions/string_replace.cf | 14 ++++++++++++++ .../02_functions/string_replace.cf.expected.json | 12 ++++++++++++ 3 files changed, 40 insertions(+) diff --git a/examples/string_replace.cf b/examples/string_replace.cf index dc31afd938..4c9340d2ca 100644 --- a/examples/string_replace.cf +++ b/examples/string_replace.cf @@ -30,6 +30,16 @@ bundle agent main # replace ambiguous order "replace_ambiguous" string => string_replace("aaaaa", "aaa", "b"); + "matches" slist => { "e", "r" }; + "subs" slist => { "E", "R" }; + + "replace_list" + string => string_replace("Hello World", "@(matches)", "@(subs)", "lists"); + + "replace_list_json" + string => string_replace( + "Hello World", '["World", "o"]', '["Everyone", "0"]', "lists" + ); reports: # in order, the above... @@ -37,6 +47,8 @@ bundle agent main "replace_several = '$(replace_several)'"; "replace_none = '$(replace_none)'"; "replace_ambiguous = '$(replace_ambiguous)'"; + "replace_list = '$(replace_list)'"; + "replace_list_json = '$(replace_list_json)'"; } #+end_src @@ -47,5 +59,7 @@ bundle agent main #@ R: replace_several = 'Thos os a strong' #@ R: replace_none = 'This is a string' #@ R: replace_ambiguous = 'baa' +#@ R: replace_list = 'HEllo WoRld' +#@ R: replace_list_json = 'Hell0 Every0ne' #@ ``` #+end_src diff --git a/tests/acceptance/01_vars/02_functions/string_replace.cf b/tests/acceptance/01_vars/02_functions/string_replace.cf index 4aba179966..88c0d8552e 100644 --- a/tests/acceptance/01_vars/02_functions/string_replace.cf +++ b/tests/acceptance/01_vars/02_functions/string_replace.cf @@ -16,6 +16,8 @@ bundle agent test vars: "test" string => "abcdefghij\t\n"; "test2" string => "(){}[].*?"; + "match_list" slist => { "a", "b", "d" }; + "sub_list" slist => { "b", "c", "e" }; # normal tests "match_once" string => string_replace("abcd", "abc", "ABC"); @@ -35,6 +37,18 @@ bundle agent test # empty cases "empty" string => string_replace("", "abc", "ABC"); "empty2" string => string_replace("", "abc", ""); + + # list + "list_replace" + string => string_replace("abcd", "@(match_list)", "@(sub_list)", "lists"); + + "list_replace_inline_json" + string => string_replace( + "Hello World", + '["World", "o"]', + parsejson('["Everyone", "0"]'), + "lists" + ); } ####################################################### diff --git a/tests/acceptance/01_vars/02_functions/string_replace.cf.expected.json b/tests/acceptance/01_vars/02_functions/string_replace.cf.expected.json index ff9ba7584a..5187e69c28 100644 --- a/tests/acceptance/01_vars/02_functions/string_replace.cf.expected.json +++ b/tests/acceptance/01_vars/02_functions/string_replace.cf.expected.json @@ -1,6 +1,13 @@ { "empty": "", "empty2": "", + "list_replace": "ccce", + "list_replace_inline_json": "Hell0 Every0ne", + "match_list": [ + "a", + "b", + "d" + ], "match_none": "abdc", "match_none2": "abcdefghij\\t\\n", "match_once": "ABCd", @@ -12,6 +19,11 @@ "special": "1{}[].*?", "special2": "()\\1[].*?", "special3": "(){}[]\\2", + "sub_list": [ + "b", + "c", + "e" + ], "test": "abcdefghij\\t\\n", "test2": "(){}[].*?" }