Skip to content

ENT-6193, CFE-3421: Added policy function classfilterdata() #5836

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 197 additions & 0 deletions libpromises/evalfunction.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
included file COSL.txt.
*/

#include <limits.h>
#include <platform.h>
#include <evalfunction.h>

Expand All @@ -35,6 +36,8 @@
#include <files_names.h>
#include <files_interfaces.h>
#include <hash.h>
#include <stddef.h>
#include <stdint.h>
#include <vars.h>
#include <addr_lib.h>
#include <syntax.h>
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"},
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
}
Original file line number Diff line number Diff line change
@@ -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";
}
Loading
Loading