diff --git a/libpromises/evalfunction.c b/libpromises/evalfunction.c index 44774e8a4c..26992bfeb7 100644 --- a/libpromises/evalfunction.c +++ b/libpromises/evalfunction.c @@ -22,6 +22,7 @@ included file COSL.txt. */ +#include #include #include @@ -35,6 +36,8 @@ #include #include #include +#include +#include #include #include #include @@ -7777,6 +7780,190 @@ static int JsonPrimitiveComparator(JsonElement const *left_obj, return StringSafeCompare(left, right); } +static bool ClassFilterDataArrayOfArrays( + EvalContext *ctx, + const char *fn_name, + JsonElement *json_array, + const char *class_expr_index, + bool *remove) +{ + errno = 0; /* to detect error */ + assert(SIZE_MAX >= ULONG_MAX); /* make sure returned value can fit in size_t */ + char *endptr; + size_t index = strtoul(class_expr_index, &endptr, 10); + if (!StringEqual(endptr, "")) /* check that the whole string was consumed */ + { + Log(LOG_LEVEL_VERBOSE, + "Function %s(): Bad class expression index '%s': Not a valid integer", + fn_name, class_expr_index); + return false; + } + if (errno != 0) + { + Log(LOG_LEVEL_VERBOSE, + "Function %s(): Bad class expression index '%s': %s", + fn_name, class_expr_index, GetErrorStr()); + return false; + } + + size_t length = JsonLength(json_array); + if (index >= length) + { + Log(LOG_LEVEL_VERBOSE, + "Function %s(): Bad class expression index '%s': Index out of bounds (%zu >= %zu)", + fn_name, class_expr_index, index, length); + return false; + } + + JsonElement *json_child = JsonArrayGet(json_array, index); + if (JsonGetType(json_child) != JSON_TYPE_STRING) + { + Log(LOG_LEVEL_VERBOSE, + "Function %s(): Bad class expression at index '%zu': Expected type string", + fn_name, index); + return false; + } + + const char *class_expr = JsonPrimitiveGetAsString(json_child); + assert(class_expr != NULL); + + *remove = !IsDefinedClass(ctx, class_expr); + return true; +} + +static bool ClassFilterDataArrayOfObjects( + EvalContext *ctx, + const char *fn_name, + JsonElement *json_object, + const char *class_expr_key, + bool *remove) +{ + JsonElement *json_child = JsonObjectGet(json_object, class_expr_key); + if (json_child == NULL) + { + Log(LOG_LEVEL_VERBOSE, + "Function %s(): Bad class expression key '%s': Key not found", + fn_name, class_expr_key); + return false; + } + + if (JsonGetType(json_child) != JSON_TYPE_STRING) + { + Log(LOG_LEVEL_VERBOSE, + "Function %s(): Bad class expression at key '%s': Expected type string", + fn_name, class_expr_key); + return false; + } + + const char *class_expr = JsonPrimitiveGetAsString(json_child); + assert(class_expr != NULL); + + *remove = !IsDefinedClass(ctx, class_expr); + return true; +} + +static bool ClassFilterDataArray( + EvalContext *ctx, + const char *fn_name, + const char *data_structure, + const char *key_or_index, + JsonElement *child, + bool *remove) +{ + switch (JsonGetType(child)) + { + case JSON_TYPE_ARRAY: + if (StringEqual(data_structure, "auto") || + StringEqual(data_structure, "array_of_arrays")) + { + return ClassFilterDataArrayOfArrays( + ctx, fn_name, child, key_or_index, remove); + } + Log(LOG_LEVEL_VERBOSE, + "Function %s(): Expected child element to be of container type array", + fn_name); + break; + + case JSON_TYPE_OBJECT: + if (StringEqual(data_structure, "auto") || + StringEqual(data_structure, "array_of_objects")) + { + return ClassFilterDataArrayOfObjects( + ctx, fn_name, child, key_or_index, remove); + } + Log(LOG_LEVEL_VERBOSE, + "Function %s(): Expected child element to be of container type object", + fn_name); + break; + + default: + Log(LOG_LEVEL_VERBOSE, + "Function %s(): Expected child element to be of container type", + fn_name); + break; + } + + return false; +} + +static FnCallResult FnCallClassFilterData( + EvalContext *ctx, + ARG_UNUSED Policy const *policy, + FnCall const *fp, + Rlist const *args) +{ + assert(ctx != NULL); + assert(fp != NULL); + assert(args != NULL); + assert(args->next != NULL); + assert(args->next->next != NULL); + + bool allocated = false; + JsonElement *parent = VarNameOrInlineToJson(ctx, fp, args, false, &allocated); + if (parent == NULL) + { + Log(LOG_LEVEL_VERBOSE, + "Function %s(): Expected parent element to be of container type array", + fp->name); + return FnFailure(); + } + + /* Currently only parent type array is supported */ + if (JsonGetType(parent) != JSON_TYPE_ARRAY) + { + JsonDestroyMaybe(parent, allocated); + return FnFailure(); + } + assert(allocated); /* Non-primitives are always allocated */ + + const char *data_structure = RlistScalarValue(args->next); + const char *key_or_index = RlistScalarValue(args->next->next); + + /* Iterate through array backwards so we can avoid having to compute index + * offsets for each removed element */ + for (size_t i = JsonLength(parent); i > 0; i--) + { + size_t index = i - 1; + JsonElement *child = JsonArrayGet(parent, index); + assert(child != NULL); + + bool remove; + if (!ClassFilterDataArray(ctx, fp->name, data_structure, key_or_index, child, &remove)) + { + /* Error is already logged */ + JsonDestroy(parent); + return FnFailure(); + } + + if (remove) + { + JsonArrayRemoveRange(parent, index, index); + } + } + + return FnReturnContainerNoCopy(parent); +} + static FnCallResult FnCallClassFilterCsv(EvalContext *ctx, ARG_UNUSED Policy const *policy, FnCall const *fp, @@ -10371,6 +10558,14 @@ static const FnCallArg VALIDJSON_ARGS[] = {NULL, CF_DATA_TYPE_NONE, NULL} }; +static const FnCallArg CLASSFILTERDATA_ARGS[] = +{ + {CF_ANYSTRING, CF_DATA_TYPE_STRING, "CFEngine variable identifier or inline JSON"}, + {"array_of_arrays,array_of_objects,auto", CF_DATA_TYPE_OPTION, "Specify type of data structure"}, + {CF_ANYSTRING, CF_DATA_TYPE_STRING, "Key or index of class expressions"}, + {NULL, CF_DATA_TYPE_NONE, NULL} +}; + static const FnCallArg CLASSFILTERCSV_ARGS[] = { {CF_ABSPATHRANGE, CF_DATA_TYPE_STRING, "File name"}, @@ -10907,6 +11102,8 @@ const FnCallType CF_FNCALL_TYPES[] = FNCALL_OPTION_VARARG, FNCALL_CATEGORY_UTILS, SYNTAX_STATUS_NORMAL), FnCallTypeNew("classesmatching", CF_DATA_TYPE_STRING_LIST, CLASSMATCH_ARGS, &FnCallClassesMatching, "List the defined classes matching regex arg1 and tag regexes arg2,arg3,...", FNCALL_OPTION_VARARG, FNCALL_CATEGORY_UTILS, SYNTAX_STATUS_NORMAL), + FnCallTypeNew("classfilterdata", CF_DATA_TYPE_CONTAINER, CLASSFILTERDATA_ARGS, &FnCallClassFilterData, "Filter data container by defined classes", + FNCALL_OPTION_VARARG, FNCALL_CATEGORY_IO, SYNTAX_STATUS_NORMAL), FnCallTypeNew("classfiltercsv", CF_DATA_TYPE_CONTAINER, CLASSFILTERCSV_ARGS, &FnCallClassFilterCsv, "Parse a CSV file and create data container filtered by defined classes", FNCALL_OPTION_VARARG, FNCALL_CATEGORY_IO, SYNTAX_STATUS_NORMAL), FnCallTypeNew("countclassesmatching", CF_DATA_TYPE_INT, CLASSMATCH_ARGS, &FnCallClassesMatching, "Count the number of defined classes matching regex arg1", diff --git a/tests/acceptance/01_vars/02_functions/classfilterdata_array_of_arrays_foo.cf b/tests/acceptance/01_vars/02_functions/classfilterdata_array_of_arrays_foo.cf new file mode 100644 index 0000000000..43052db129 --- /dev/null +++ b/tests/acceptance/01_vars/02_functions/classfilterdata_array_of_arrays_foo.cf @@ -0,0 +1,78 @@ +body common control +{ + bundlesequence => { "check" }; +} + +bundle agent test(context, index) +{ + meta: + "description" -> { "ENT-6193", "CFE-3421" } + string => "Test for expected results from policy function classfilterdata() with array of arrays using context '$(with)' and index $(index)", + with => join("', '", "context"); + + vars: + "test" + data => '[ + [ "foo", "!foo", "foo&bar", "foo|bar" ], + [ "bar", "!bar", "bar&baz", "bar|baz" ], + [ "baz", "!baz", "foo&baz", "foo|baz" ], + ]'; + + "actual" + data => classfilterdata("@(test)", "array_of_arrays", "$(index)"); + + classes: + "$(context)"; + + reports: + "$(with)" + with => storejson("@(actual)"), + bundle_return_value_index => "$(index)"; +} + +bundle agent check +{ + vars: + "num_indices" string => "3"; + "context" slist => { "foo" }; + "range" slist => expandrange("[0-$(num_indices)]", "1"); + + "expected[0]" + string => storejson('[ + [ "foo", "!foo", "foo&bar", "foo|bar" ], + ]'); + "expected[1]" + string => storejson('[ + [ "bar", "!bar", "bar&baz", "bar|baz" ], + [ "baz", "!baz", "foo&baz", "foo|baz" ], + ]'); + "expected[2]" + string => storejson('[ + ]'); + "expected[3]" + string => storejson('[ + [ "foo", "!foo", "foo&bar", "foo|bar" ], + [ "baz", "!baz", "foo&baz", "foo|baz" ], + ]'); + + classes: + "ok_$(range)" + expression => strcmp("$(expected[$(range)])", + "$(actual[$(range)])"); + "ok" + expression => and(expandrange("ok_[0-$(num_indices)]", "1")); + + methods: + "context: '$(with)' and index: $(range)" + usebundle => test("@(context)", "$(range)"), + useresult => "actual", + with => join(", ", "context"); + + reports: + "Context '$(with)'; index $(range); expected '$(expected[$(range)])'; actual '$(actual[$(range)])'" + with => join("', '", "context"); + ok:: + "$(this.promise_filename) Pass"; + !ok:: + "$(this.promise_filename) FAIL"; +} diff --git a/tests/acceptance/01_vars/02_functions/classfilterdata_array_of_arrays_foo_bar.cf b/tests/acceptance/01_vars/02_functions/classfilterdata_array_of_arrays_foo_bar.cf new file mode 100644 index 0000000000..0d2130fdf6 --- /dev/null +++ b/tests/acceptance/01_vars/02_functions/classfilterdata_array_of_arrays_foo_bar.cf @@ -0,0 +1,80 @@ +body common control +{ + bundlesequence => { "check" }; +} + +bundle agent test(context, index) +{ + meta: + "description" -> { "ENT-6193", "CFE-3421" } + string => "Test for expected results from policy function classfilterdata() with array of arrays using context '$(with)' and index $(index)", + with => join("', '", "context"); + + vars: + "test" + data => '[ + [ "foo", "!foo", "foo&bar", "foo|bar" ], + [ "bar", "!bar", "bar&baz", "bar|baz" ], + [ "baz", "!baz", "foo&baz", "foo|baz" ], + ]'; + + "actual" + data => classfilterdata("@(test)", "array_of_arrays", "$(index)"); + + classes: + "$(context)"; + + reports: + "$(with)" + with => storejson("@(actual)"), + bundle_return_value_index => "$(index)"; +} + +bundle agent check +{ + vars: + "num_indices" string => "3"; + "context" slist => { "foo", "bar" }; + "range" slist => expandrange("[0-$(num_indices)]", "1"); + + "expected[0]" + string => storejson('[ + [ "foo", "!foo", "foo&bar", "foo|bar" ], + [ "bar", "!bar", "bar&baz", "bar|baz" ], + ]'); + "expected[1]" + string => storejson('[ + [ "baz", "!baz", "foo&baz", "foo|baz" ], + ]'); + "expected[2]" + string => storejson('[ + [ "foo", "!foo", "foo&bar", "foo|bar" ], + ]'); + "expected[3]" + string => storejson('[ + [ "foo", "!foo", "foo&bar", "foo|bar" ], + [ "bar", "!bar", "bar&baz", "bar|baz" ], + [ "baz", "!baz", "foo&baz", "foo|baz" ], + ]'); + + classes: + "ok_$(range)" + expression => strcmp("$(expected[$(range)])", + "$(actual[$(range)])"); + "ok" + expression => and(expandrange("ok_[0-$(num_indices)]", "1")); + + methods: + "context: '$(with)' and index: $(range)" + usebundle => test("@(context)", "$(range)"), + useresult => "actual", + with => join(", ", "context"); + + reports: + "Context '$(with)'; index $(range); expected '$(expected[$(range)])'; actual '$(actual[$(range)])'" + with => join("', '", "context"); + ok:: + "$(this.promise_filename) Pass"; + !ok:: + "$(this.promise_filename) FAIL"; +} diff --git a/tests/acceptance/01_vars/02_functions/classfilterdata_array_of_objects_foo.cf b/tests/acceptance/01_vars/02_functions/classfilterdata_array_of_objects_foo.cf new file mode 100644 index 0000000000..5e424e8952 --- /dev/null +++ b/tests/acceptance/01_vars/02_functions/classfilterdata_array_of_objects_foo.cf @@ -0,0 +1,78 @@ +body common control +{ + bundlesequence => { "check" }; +} + +bundle agent test(context, index) +{ + meta: + "description" -> { "ENT-6193", "CFE-3421" } + string => "Test for expected results from policy function classfilterdata() with array of objects using context '$(with)' and key 'key_$(index)'", + with => join("', '", "context"); + + vars: + "test" + data => '[ + { "key_0": "foo", "key_1": "!foo", "key_2": "foo&bar", "key_3": "foo|bar" }, + { "key_0": "bar", "key_1": "!bar", "key_2": "bar&baz", "key_3": "bar|baz" }, + { "key_0": "baz", "key_1": "!baz", "key_2": "foo&baz", "key_3": "foo|baz" }, + ]'; + + "actual" + data => classfilterdata("@(test)", "array_of_objects", "key_$(index)"); + + classes: + "$(context)"; + + reports: + "$(with)" + with => storejson("@(actual)"), + bundle_return_value_index => "$(index)"; +} + +bundle agent check +{ + vars: + "num_indices" string => "3"; + "context" slist => { "foo" }; + "range" slist => expandrange("[0-$(num_indices)]", "1"); + + "expected[0]" + string => storejson('[ + { "key_0": "foo", "key_1": "!foo", "key_2": "foo&bar", "key_3": "foo|bar" }, + ]'); + "expected[1]" + string => storejson('[ + { "key_0": "bar", "key_1": "!bar", "key_2": "bar&baz", "key_3": "bar|baz" }, + { "key_0": "baz", "key_1": "!baz", "key_2": "foo&baz", "key_3": "foo|baz" }, + ]'); + "expected[2]" + string => storejson('[ + ]'); + "expected[3]" + string => storejson('[ + { "key_0": "foo", "key_1": "!foo", "key_2": "foo&bar", "key_3": "foo|bar" }, + { "key_0": "baz", "key_1": "!baz", "key_2": "foo&baz", "key_3": "foo|baz" }, + ]'); + + classes: + "ok_$(range)" + expression => strcmp("$(expected[$(range)])", + "$(actual[$(range)])"); + "ok" + expression => and(expandrange("ok_[0-$(num_indices)]", "1")); + + methods: + "context: '$(with)' and index: $(range)" + usebundle => test("@(context)", "$(range)"), + useresult => "actual", + with => join(", ", "context"); + + reports: + "Context '$(with)'; index $(range); expected '$(expected[$(range)])'; actual '$(actual[$(range)])'" + with => join("', '", "context"); + ok:: + "$(this.promise_filename) Pass"; + !ok:: + "$(this.promise_filename) FAIL"; +} diff --git a/tests/acceptance/01_vars/02_functions/classfilterdata_array_of_objects_foo_bar.cf b/tests/acceptance/01_vars/02_functions/classfilterdata_array_of_objects_foo_bar.cf new file mode 100644 index 0000000000..42af249d78 --- /dev/null +++ b/tests/acceptance/01_vars/02_functions/classfilterdata_array_of_objects_foo_bar.cf @@ -0,0 +1,80 @@ +body common control +{ + bundlesequence => { "check" }; +} + +bundle agent test(context, index) +{ + meta: + "description" -> { "ENT-6193", "CFE-3421" } + string => "Test for expected results from policy function classfilterdata() with array of objects using context '$(with)' and key 'key_$(index)'", + with => join("', '", "context"); + + vars: + "test" + data => '[ + { "key_0": "foo", "key_1": "!foo", "key_2": "foo&bar", "key_3": "foo|bar" }, + { "key_0": "bar", "key_1": "!bar", "key_2": "bar&baz", "key_3": "bar|baz" }, + { "key_0": "baz", "key_1": "!baz", "key_2": "foo&baz", "key_3": "foo|baz" }, + ]'; + + "actual" + data => classfilterdata("@(test)", "array_of_objects", "key_$(index)"); + + classes: + "$(context)"; + + reports: + "$(with)" + with => storejson("@(actual)"), + bundle_return_value_index => "$(index)"; +} + +bundle agent check +{ + vars: + "num_indices" string => "3"; + "context" slist => { "foo", "bar" }; + "range" slist => expandrange("[0-$(num_indices)]", "1"); + + "expected[0]" + string => storejson('[ + { "key_0": "foo", "key_1": "!foo", "key_2": "foo&bar", "key_3": "foo|bar" }, + { "key_0": "bar", "key_1": "!bar", "key_2": "bar&baz", "key_3": "bar|baz" }, + ]'); + "expected[1]" + string => storejson('[ + { "key_0": "baz", "key_1": "!baz", "key_2": "foo&baz", "key_3": "foo|baz" }, + ]'); + "expected[2]" + string => storejson('[ + { "key_0": "foo", "key_1": "!foo", "key_2": "foo&bar", "key_3": "foo|bar" }, + ]'); + "expected[3]" + string => storejson('[ + { "key_0": "foo", "key_1": "!foo", "key_2": "foo&bar", "key_3": "foo|bar" }, + { "key_0": "bar", "key_1": "!bar", "key_2": "bar&baz", "key_3": "bar|baz" }, + { "key_0": "baz", "key_1": "!baz", "key_2": "foo&baz", "key_3": "foo|baz" }, + ]'); + + classes: + "ok_$(range)" + expression => strcmp("$(expected[$(range)])", + "$(actual[$(range)])"); + "ok" + expression => and(expandrange("ok_[0-$(num_indices)]", "1")); + + methods: + "context: '$(with)' and index: $(range)" + usebundle => test("@(context)", "$(range)"), + useresult => "actual", + with => join(", ", "context"); + + reports: + "Context '$(with)'; index $(range); expected '$(expected[$(range)])'; actual '$(actual[$(range)])'" + with => join("', '", "context"); + ok:: + "$(this.promise_filename) Pass"; + !ok:: + "$(this.promise_filename) FAIL"; +}