From 844cb03550841ac83d2afa07e20a817e634491b2 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Fri, 10 Jul 2026 11:24:49 +0200 Subject: [PATCH 1/2] Made format function collect data container Ticket: CFE-3105 Changelog: Title Signed-off-by: Victor Moene --- libpromises/evalfunction.c | 128 ++++++++---------- .../acceptance/01_vars/02_functions/format.cf | 4 + 2 files changed, 57 insertions(+), 75 deletions(-) diff --git a/libpromises/evalfunction.c b/libpromises/evalfunction.c index 8dfcdc8e42..dde4c4d30c 100644 --- a/libpromises/evalfunction.c +++ b/libpromises/evalfunction.c @@ -118,7 +118,6 @@ static bool ExecModule(EvalContext *ctx, char *command); static bool CheckIDChar(const char ch); static bool CheckID(const char *id); -static const Rlist *GetListReferenceArgument(const EvalContext *ctx, const FnCall *fp, const char *lval_str, DataType *datatype_out); static char *CfReadFile(const char *filename, size_t maxsize); /*******************************************************************/ @@ -5486,41 +5485,6 @@ static FnCallResult FnCallFilter(EvalContext *ctx, ARG_UNUSED const Policy *poli /*********************************************************************/ -static const Rlist *GetListReferenceArgument(const EvalContext *ctx, const FnCall *fp, const char *lval_str, DataType *datatype_out) -{ - VarRef *ref = VarRefParse(lval_str); - DataType value_type; - const Rlist *value = EvalContextVariableGet(ctx, ref, &value_type); - VarRefDestroy(ref); - - /* Error 1: variable not found. */ - if (value_type == CF_DATA_TYPE_NONE) - { - Log(LOG_LEVEL_VERBOSE, - "Could not resolve expected list variable '%s' in function '%s'", - lval_str, fp->name); - assert(value == NULL); - } - /* Error 2: variable is not a list. */ - else if (DataTypeToRvalType(value_type) != RVAL_TYPE_LIST) - { - Log(LOG_LEVEL_ERR, "Function '%s' expected a list variable," - " got variable of type '%s'", - fp->name, DataTypeToString(value_type)); - - value = NULL; - value_type = CF_DATA_TYPE_NONE; - } - - if (datatype_out) - { - *datatype_out = value_type; - } - return value; -} - -/*********************************************************************/ - static FnCallResult FilterInternal(EvalContext *ctx, const FnCall *fp, const char *regex, @@ -6248,6 +6212,25 @@ static FnCallResult FnCallSort(EvalContext *ctx, ARG_UNUSED const Policy *policy /*********************************************************************/ +static bool CanFormatAsStringList(EvalContext *ctx, const Rlist *arg, const Rlist **out) +{ + assert(arg != NULL); + + if (arg->val.type != RVAL_TYPE_SCALAR) + { + *out = NULL; + return false; + } + + const char* const varname = RlistScalarValue(arg); + VarRef *ref = VarRefParse(varname); + DataType type; + *out = EvalContextVariableGet(ctx, ref, &type); + VarRefDestroy(ref); + + return type == CF_DATA_TYPE_STRING_LIST; +} + static FnCallResult FnCallFormat(EvalContext *ctx, ARG_UNUSED const Policy *policy, const FnCall *fp, const Rlist *finalargs) { const char *const id = "built-in FnCall format-arg"; @@ -6293,7 +6276,7 @@ static FnCallResult FnCallFormat(EvalContext *ctx, ARG_UNUSED const Policy *poli { const char *format_piece = BufferData(SeqAt(s, 1)); bool percent = StringEqualN(format_piece, "%%", 2); - char *data = NULL; + const Rlist *arg = NULL; if (percent) { @@ -6301,10 +6284,10 @@ static FnCallResult FnCallFormat(EvalContext *ctx, ARG_UNUSED const Policy *poli } else if (rp != NULL) { - data = RlistScalarValue(rp); + arg = rp; rp = rp->next; } - else // not %% and no data + else // not %% and no arg { Log(LOG_LEVEL_ERR, "format() didn't have enough parameters"); BufferDestroy(buf); @@ -6333,7 +6316,7 @@ static FnCallResult FnCallFormat(EvalContext *ctx, ARG_UNUSED const Policy *poli if (strrchr(format_piece, 'd') != NULL || strrchr(format_piece, 'o') != NULL || strrchr(format_piece, 'x') != NULL) { long x = 0; - sscanf(data, "%ld", &x); + sscanf(RlistScalarValue(arg), "%ld", &x); snprintf(piece, CF_BUFSIZE, format_piece, x); BufferAppend(buf, piece, strlen(piece)); } @@ -6345,13 +6328,13 @@ static FnCallResult FnCallFormat(EvalContext *ctx, ARG_UNUSED const Policy *poli else if (strrchr(format_piece, 'f') != NULL) { double x = 0; - sscanf(data, "%lf", &x); + sscanf(RlistScalarValue(arg), "%lf", &x); snprintf(piece, CF_BUFSIZE, format_piece, x); BufferAppend(buf, piece, strlen(piece)); } else if (strrchr(format_piece, 's') != NULL) { - BufferAppendF(buf, format_piece, data); + BufferAppendF(buf, format_piece, RlistScalarValue(arg)); } else if (strrchr(format_piece, 'S') != NULL) { @@ -6370,51 +6353,46 @@ static FnCallResult FnCallFormat(EvalContext *ctx, ARG_UNUSED const Policy *poli ProgrammingError("Couldn't find the expected S format spec in %s", format_piece); } - const char* const varname = data; - VarRef *ref = VarRefParse(varname); - DataType type; - const void *value = EvalContextVariableGet(ctx, ref, &type); - VarRefDestroy(ref); - - if (type == CF_DATA_TYPE_CONTAINER) + const Rlist *list; + if (CanFormatAsStringList(ctx, arg, &list)) { Writer *w = StringWriter(); - JsonWriteCompact(w, value); + WriterWrite(w, "{ "); + for (const Rlist *tmp = list; tmp; tmp = tmp->next) + { + char *escaped = EscapeCharCopy(RlistScalarValue(tmp), '"', '\\'); + WriterWriteF(w, "\"%s\"", escaped); + free(escaped); + + if (tmp != NULL && tmp->next != NULL) + { + WriterWrite(w, ", "); + } + } + WriterWrite(w, " }"); + BufferAppendF(buf, format_rewrite, StringWriterData(w)); WriterClose(w); } - else // it might be a list reference + else { - DataType data_type; - const Rlist *list = GetListReferenceArgument(ctx, fp, varname, &data_type); - if (data_type == CF_DATA_TYPE_STRING_LIST) - { - Writer *w = StringWriter(); - WriterWrite(w, "{ "); - for (const Rlist *rp = list; rp; rp = rp->next) - { - char *escaped = EscapeCharCopy(RlistScalarValue(rp), '"', '\\'); - WriterWriteF(w, "\"%s\"", escaped); - free(escaped); - - if (rp != NULL && rp->next != NULL) - { - WriterWrite(w, ", "); - } - } - WriterWrite(w, " }"); + bool allocated = false; + JsonElement *element = VarNameOrInlineToJson(ctx, fp, arg, false, &allocated); - BufferAppendF(buf, format_rewrite, StringWriterData(w)); - WriterClose(w); - } - else // whatever this is, it's not a list reference or a data container + if (element == NULL) { Log(LOG_LEVEL_VERBOSE, "format() with %%S specifier needs a data container or a list instead of '%s'.", - varname); + RlistScalarValue(arg)); BufferDestroy(buf); SeqDestroy(s); return FnFailure(); } + + Writer *w = StringWriter(); + JsonWriteCompact(w, element); + BufferAppendF(buf, format_rewrite, StringWriterData(w)); + WriterClose(w); + JsonDestroyMaybe(element, allocated); } } else @@ -11716,7 +11694,7 @@ const FnCallType CF_FNCALL_TYPES[] = FnCallTypeNew("findprocesses", CF_DATA_TYPE_CONTAINER, PROCESSEXISTS_ARGS, &FnCallProcessExists, "Returns data container of processes matching the regular expression", FNCALL_OPTION_CACHED, FNCALL_CATEGORY_SYSTEM, SYNTAX_STATUS_NORMAL, DEFAULT_ARGC), FnCallTypeNew("format", CF_DATA_TYPE_STRING, FORMAT_ARGS, &FnCallFormat, "Applies a list of string values in arg2,arg3... to a string format in arg1 with sprintf() rules", - FNCALL_OPTION_VARARG, FNCALL_CATEGORY_DATA, SYNTAX_STATUS_NORMAL, ARGC(1, -1)), + FNCALL_OPTION_VARARG | FNCALL_OPTION_COLLECTING, FNCALL_CATEGORY_DATA, SYNTAX_STATUS_NORMAL, ARGC(1, -1)), FnCallTypeNew("getclassmetatags", CF_DATA_TYPE_STRING_LIST, GETCLASSMETATAGS_ARGS, &FnCallGetMetaTags, "Collect the class arg1's meta tags into an slist, optionally collecting only tag key arg2", FNCALL_OPTION_VARARG, FNCALL_CATEGORY_UTILS, SYNTAX_STATUS_NORMAL, ARGC(1, -1)), FnCallTypeNew("getenv", CF_DATA_TYPE_STRING, GETENV_ARGS, &FnCallGetEnv, "Return the environment variable named arg1, truncated at arg2 characters", diff --git a/tests/acceptance/01_vars/02_functions/format.cf b/tests/acceptance/01_vars/02_functions/format.cf index a9cf432a9b..1dc41e5693 100644 --- a/tests/acceptance/01_vars/02_functions/format.cf +++ b/tests/acceptance/01_vars/02_functions/format.cf @@ -46,6 +46,8 @@ bundle edit_line init_insert "key='S_123_list' value='{ \"one\", \"two\", \"\\\"three\\\"\" }'"; "key='S_container_1' value='[null]'"; "key='S_container_2' value='[{\"x\":123},\"yz\"]'"; + "key='S_function' value='{\"hello\":\"world\"}'"; + "key='S_inline_json' value='{\"hello\":\"world\"}'"; } ####################################################### @@ -82,6 +84,8 @@ bundle agent test "array[S_container_1]" string => format("%S", mycontainer1); "array[S_container_2]" string => format("%S", mycontainer2); "formatted" slist => maparray("key='$(this.k)' value='$(this.v)'", "array"); + "array[S_function]" string => format("%S", parsejson('{"hello":"world"}')); + "array[S_inline_json]" string => format("%S", '{"hello":"world"}'); files: "$(G.testfile).actual" From ee666b24d19b430d61a866e7123f87b47358f173 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Mon, 10 Aug 2026 16:41:35 +0200 Subject: [PATCH 2/2] Sorted keys in nth_datacontainer test From the docs: "The list which getindices returns is not guaranteed to be in any specific order." The tests failed for me but it didnt' seem related to format() Signed-off-by: Victor Moene --- tests/acceptance/01_vars/02_functions/nth_datacontainer.cf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/acceptance/01_vars/02_functions/nth_datacontainer.cf b/tests/acceptance/01_vars/02_functions/nth_datacontainer.cf index ea599ed3e9..e6090255ee 100644 --- a/tests/acceptance/01_vars/02_functions/nth_datacontainer.cf +++ b/tests/acceptance/01_vars/02_functions/nth_datacontainer.cf @@ -28,9 +28,9 @@ bundle common test_common "numbers" ilist => { "-100", "-1", 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1000 }; - "keys" slist => getindices(data); - "object_keys" slist => getindices("data[object]"); - "list_keys" slist => getindices("data[list]"); + "keys" slist => sort(getindices(data), "lex"); + "object_keys" slist => sort(getindices("data[object]"), "lex"); + "list_keys" slist => sort(getindices("data[list]"), "lex"); "all_keys" slist => {