From: Alan T. DeKok Date: Tue, 16 Nov 2021 21:06:04 +0000 (-0500) Subject: start of calculation and comparison API X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0bc2a7639a234393d05a362c2425d5ec370175e6;p=thirdparty%2Ffreeradius-server.git start of calculation and comparison API --- diff --git a/src/bin/unit_test_attribute.c b/src/bin/unit_test_attribute.c index d2bacd6581..97c8972801 100644 --- a/src/bin/unit_test_attribute.c +++ b/src/bin/unit_test_attribute.c @@ -44,6 +44,7 @@ typedef struct request_s request_t; #include #include #include +#include #include #include #include @@ -1003,9 +1004,14 @@ static size_t parse_typed_value(command_result_t *result, fr_value_box_t *box, c fr_skip_whitespace(p); *out = p; + /* + * As a hack, allow most things to be inside + * double-quoted strings. This is really only for dates, + * which are space-delimited. + */ if (*p == '"'){ p++; - slen = fr_value_box_from_substr(box, box, type, NULL, + slen = fr_value_box_from_substr(box, box, FR_TYPE_STRING, NULL, &FR_SBUFF_IN(p, strlen(p)), &value_parse_rules_double_quoted, false); @@ -1020,6 +1026,12 @@ static size_t parse_typed_value(command_result_t *result, fr_value_box_t *box, c p++; slen += 2; + if (type != FR_TYPE_STRING) { + if (fr_value_box_cast_in_place(box, box, type, NULL) < 0) { + RETURN_PARSE_ERROR(0); + } + } + } else { slen = fr_value_box_from_substr(box, box, type, NULL, &FR_SBUFF_IN(p, strlen(p)), @@ -1152,6 +1164,79 @@ static size_t command_normalise_attribute(command_result_t *result, command_file RETURN_OK(slen); } +static const fr_token_t token2op[UINT8_MAX + 1] = { + [ '+' ] = T_OP_ADD, + [ '-' ] = T_OP_SUB, + [ '^' ] = T_OP_PREPEND, + [ '.' ] = T_OP_ADD, +}; + +/** Perform calculations + * + */ +static size_t command_calc(command_result_t *result, command_file_ctx_t *cc, + char *data, UNUSED size_t data_used, char *in, size_t inlen) +{ + fr_value_box_t *a, *b, *out; + size_t match_len; + fr_type_t type; + fr_token_t op; + char const *p, *value, *end; + size_t slen; + + a = talloc_zero(cc->tmp_ctx, fr_value_box_t); + b = talloc_zero(cc->tmp_ctx, fr_value_box_t); + out = talloc_zero(cc->tmp_ctx, fr_value_box_t); + + p = in; + end = in + inlen; + + match_len = parse_typed_value(result, a, &value, p, end - p); + if (match_len == 0) return 0; /* errors have already been updated */ + + p += match_len; + fr_skip_whitespace(p); + + op = fr_table_value_by_longest_prefix(&match_len, fr_tokens_table, p, end - p, T_INVALID); + if (op != T_INVALID) { + p += match_len; + } else { + op = token2op[(uint8_t) p[0]]; + if (op == T_INVALID) RETURN_PARSE_ERROR(0); + p++; + } + fr_skip_whitespace(p); + + match_len = parse_typed_value(result, b, &value, p, end - p); + if (match_len == 0) return 0; + + p += match_len; + fr_skip_whitespace(p); + + /* + * If there's no output data type, then the code tries to + * figure one out automatically. + */ + if (!*p) { + type = FR_TYPE_NULL; + } else { + if (strncmp(p, "->", 2) != 0) RETURN_PARSE_ERROR(0); + p += 2; + fr_skip_whitespace(p); + + type = fr_table_value_by_longest_prefix(&match_len, fr_value_box_type_table, p, end - p, FR_TYPE_MAX); + if (type == FR_TYPE_MAX) RETURN_PARSE_ERROR(0); + fr_value_box_init(out, type, NULL, false); + } + + if (fr_value_calc(cc->tmp_ctx, out, type, a, op, b) < 0) RETURN_PARSE_ERROR(0); + + slen = fr_value_box_print(&FR_SBUFF_OUT(data, COMMAND_OUTPUT_MAX), out, NULL); + if (slen <= 0) RETURN_OK_WITH_ERROR(); + + RETURN_OK(slen); +} + /** Change the working directory * */ @@ -2583,6 +2668,11 @@ static fr_table_ptr_sorted_t commands[] = { .usage = "attribute = ", .description = "Parse and reprint an attribute value pair, writing \"ok\" to the data buffer on success" }}, + { L("calc "), &(command_entry_t){ + .func = command_calc, + .usage = "calc -> ", + .description = "Perform calculations on value boxes", + }}, { L("cd "), &(command_entry_t){ .func = command_cd, .usage = "cd ", diff --git a/src/lib/util/calc.c b/src/lib/util/calc.c new file mode 100644 index 0000000000..f73925680c --- /dev/null +++ b/src/lib/util/calc.c @@ -0,0 +1,470 @@ +/* + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/** + * $Id$ + * + * @file src/lib/util/calc.c + * @brief Functions to perform calculations on leaf values + * + * @copyright 2021 Network RADIUS SAS (legal@networkradius.com) + */ + +RCSID("$Id$") + +#include +#include "calc.h" + +#define swap(_a, _b) do { __typeof__ (a) _tmp = _a; _a = _b; _b = _tmp; } while (0) + +#define OVERFLOW (-3) +#define INVALID (-2) + +static int calc_date(TALLOC_CTX *ctx, fr_value_box_t *dst, fr_value_box_t const *a, fr_token_t op, fr_value_box_t const *b) +{ + fr_value_box_t one, two; + bool overflow; + + fr_assert(dst->type == FR_TYPE_DATE); + + if ((a->type == FR_TYPE_DATE) && (b->type == FR_TYPE_DATE)) { + fr_strerror_const("Cannot perform operation on two dates"); + return -1; + } + + fr_assert(!dst->enumv); /* unix time is always seconds */ + + /* + * Cast dates to time delta, do the conversions. + */ + if (a->type != FR_TYPE_TIME_DELTA) { + if (fr_value_box_cast(ctx, &one, FR_TYPE_TIME_DELTA, NULL, a) < 0) return -1; + a = &one; + } + + if (b->type != FR_TYPE_TIME_DELTA) { + if (fr_value_box_cast(ctx, &two, FR_TYPE_TIME_DELTA, NULL, b) < 0) return -1; + b = &two; + } + + switch (op) { + case T_OP_ADD: + dst->vb_date = fr_unix_time_from_integer(&overflow, + fr_time_delta_unwrap(a->vb_time_delta) + fr_time_delta_unwrap(b->vb_time_delta), + FR_TIME_RES_NSEC); + if (overflow) return OVERFLOW; /* overflow */ + break; + + case T_OP_SUB: + dst->vb_date = fr_unix_time_from_integer(&overflow, + fr_time_delta_unwrap(a->vb_time_delta) - fr_time_delta_unwrap(b->vb_time_delta), + FR_TIME_RES_NSEC); + if (overflow) return OVERFLOW; /* overflow */ + break; + + default: + return INVALID; /* invalid operator */ + } + + return 0; +} + +static int calc_time_delta(TALLOC_CTX *ctx, fr_value_box_t *dst, fr_value_box_t const *a, fr_token_t op, fr_value_box_t const *b) +{ + fr_value_box_t one, two; + + fr_assert(dst->type == FR_TYPE_TIME_DELTA); + + /* + * We can subtract two dates to get a time delta, but we + * cannot add two dates to get a time delta. + */ + if ((a->type == FR_TYPE_DATE) && (b->type == FR_TYPE_DATE)) { + if (op != T_OP_SUB) { + fr_strerror_const("Cannot perform operation on two dates"); + return -1; + } + } + + /* + * Unix times are always converted 1-1 to our internal + * TIME_DELTA. + */ + if (a->type == FR_TYPE_DATE) { + if (fr_value_box_cast(ctx, &one, FR_TYPE_TIME_DELTA, NULL, a) < 0) return -1; + a = &one; + } + + if (b->type == FR_TYPE_DATE) { + if (fr_value_box_cast(ctx, &two, FR_TYPE_TIME_DELTA, NULL, b) < 0) return -1; + b = &two; + } + + /* + * We cast the inputs based on the destination time resolution. So "5ms + 5" = "10ms". + */ + if (a->type != FR_TYPE_TIME_DELTA) { + if (fr_value_box_cast(ctx, &one, FR_TYPE_TIME_DELTA, dst->enumv, a) < 0) return -1; + a = &one; + } + + if (b->type != FR_TYPE_TIME_DELTA) { + if (fr_value_box_cast(ctx, &two, FR_TYPE_TIME_DELTA, dst->enumv, b) < 0) return -1; + b = &two; + } + + switch (op) { + case T_OP_ADD: + dst->vb_time_delta = fr_time_delta_wrap(fr_time_delta_unwrap(a->vb_time_delta) + fr_time_delta_unwrap(b->vb_time_delta)); + break; + + case T_OP_SUB: + dst->vb_time_delta = fr_time_delta_wrap(fr_time_delta_unwrap(a->vb_time_delta) - fr_time_delta_unwrap(b->vb_time_delta)); + break; + + default: + return INVALID; /* invalid operator */ + } + + return 0; + +} + +static int calc_octets(TALLOC_CTX *ctx, fr_value_box_t *dst, fr_value_box_t const *a, fr_token_t op, fr_value_box_t const *b) +{ + uint8_t *buf; + size_t len; + fr_value_box_t one, two; + + fr_assert(dst->type == FR_TYPE_OCTETS); + + if (a->type != FR_TYPE_OCTETS) { + if (fr_value_box_cast(ctx, &one, FR_TYPE_OCTETS, dst->enumv, a) < 0) return -1; + a = &one; + } + + if (b->type != FR_TYPE_OCTETS) { + if (fr_value_box_cast(ctx, &two, FR_TYPE_OCTETS, dst->enumv, b) < 0) return -1; + b = &two; + } + + len = a->length + b->length; + + switch (op) { + case T_OP_PREPEND: /* dst = b . a */ + buf = talloc_array(ctx, uint8_t, len); + if (!buf) { + oom: + fr_strerror_const("Out of memory"); + return -1; + } + + memcpy(buf, b->vb_octets, b->vb_length); + memcpy(buf + b->vb_length, a->vb_octets, a->vb_length); + + fr_value_box_clear_value(dst); + fr_value_box_memdup_shallow(dst, dst->enumv, buf, len, a->tainted | b->tainted); + break; + + case T_OP_ADD: /* dst = a . b */ + buf = talloc_array(ctx, uint8_t, len); + if (!buf) goto oom; + + memcpy(buf, a->vb_octets, a->vb_length); + memcpy(buf + a->vb_length, b->vb_octets, b->vb_length); + + fr_value_box_clear_value(dst); + fr_value_box_memdup_shallow(dst, dst->enumv, buf, len, a->tainted | b->tainted); + break; + + default: + return INVALID; /* invalid operator */ + } + + if (a != &one) fr_value_box_clear(&one); + if (b != &two) fr_value_box_clear(&two); + + return 0; +} + +static int calc_string(TALLOC_CTX *ctx, fr_value_box_t *dst, fr_value_box_t const *a, fr_token_t op, fr_value_box_t const *b) +{ + char *buf; + size_t len; + fr_value_box_t one, two; + + fr_assert(dst->type == FR_TYPE_STRING); + + if (a->type != FR_TYPE_STRING) { + if (fr_value_box_cast(ctx, &one, FR_TYPE_STRING, dst->enumv, a) < 0) return -1; + a = &one; + } + + if (b->type != FR_TYPE_OCTETS) { + if (fr_value_box_cast(ctx, &two, FR_TYPE_STRING, dst->enumv, b) < 0) return -1; + b = &two; + } + + len = a->length + b->length; + + switch (op) { + case T_OP_PREPEND: /* dst = b . a */ + buf = talloc_array(ctx, char, len + 1); + if (!buf) { + oom: + fr_strerror_const("Out of memory"); + return -1; + } + + memcpy(buf, b->vb_strvalue, b->vb_length); + memcpy(buf + b->vb_length, a->vb_strvalue, a->vb_length); + buf[a->vb_length + b->vb_length] = '\0'; + + fr_value_box_clear_value(dst); + fr_value_box_strdup_shallow(dst, dst->enumv, buf, a->tainted | b->tainted); + break; + + case T_OP_ADD: + buf = talloc_array(ctx, char, len + 1); + if (!buf) goto oom; + + memcpy(buf, a->vb_strvalue, a->vb_length); + memcpy(buf + a->vb_length, b->vb_strvalue, b->vb_length); + buf[a->vb_length + b->vb_length] = '\0'; + + fr_value_box_clear_value(dst); + fr_value_box_strdup_shallow(dst, dst->enumv, buf, a->tainted | b->tainted); + break; + + default: + return INVALID; /* invalid operator */ + } + + if (a != &one) fr_value_box_clear(&one); + if (b != &two) fr_value_box_clear(&two); + + return 0; +} + +static int cast_ipv4_addr(fr_value_box_t *out, fr_value_box_t const *in, fr_dict_attr_t const *enumv) +{ + switch (in->type) { + default: + return -1; + + case FR_TYPE_STRING: + if (fr_value_box_from_str(NULL, out, FR_TYPE_IPV4_ADDR, enumv, + in->vb_strvalue, in->vb_length, + NULL, in->tainted) < 0) return -1; + break; + + case FR_TYPE_IPV6_ADDR: + if (fr_value_box_cast(NULL, out, FR_TYPE_IPV4_ADDR, NULL, in) < 0) return -1; + break; + + case FR_TYPE_IPV6_PREFIX: + if (fr_value_box_cast(NULL, out, FR_TYPE_IPV4_PREFIX, NULL, in) < 0) return -1; + break; + + case FR_TYPE_BOOL: + case FR_TYPE_INT8: + case FR_TYPE_INT16: + case FR_TYPE_INT32: + case FR_TYPE_INT64: + case FR_TYPE_FLOAT32: + case FR_TYPE_FLOAT64: + case FR_TYPE_UINT8: + case FR_TYPE_UINT16: + case FR_TYPE_UINT32: + case FR_TYPE_UINT64: + case FR_TYPE_SIZE: + fr_value_box_copy(NULL, out, in); + break; + } + + return 0; +} + +static int calc_ipv4_addr(TALLOC_CTX *ctx, fr_value_box_t *dst, fr_value_box_t const *in1, fr_token_t op, fr_value_box_t const *in2) +{ + uint32_t num; + fr_value_box_t one, two; + fr_value_box_t *a, *b; + + fr_assert(dst->type == FR_TYPE_IPV4_ADDR); + + if (cast_ipv4_addr(&one, in1, dst->enumv) < 0) return -1; + a = &one; + + if (cast_ipv4_addr(&two, in2, dst->enumv) < 0) return -1; + b = &two; + + switch (op) { + case T_OP_ADD: + /* + * For simplicity, make sure that the prefix is first. + */ + if (b->type == FR_TYPE_IPV4_PREFIX) swap(a,b); + + /* + * We can only add something to a prefix, and + * that something has to be a number. + */ + if (a->type != FR_TYPE_IPV4_PREFIX) return INVALID; + + switch (b->type) { + case FR_TYPE_INT8: + case FR_TYPE_INT16: + case FR_TYPE_INT64: + case FR_TYPE_FLOAT32: + case FR_TYPE_FLOAT64: + if (fr_value_box_cast_in_place(ctx, b, FR_TYPE_INT32, NULL) < 0) return -1; + FALL_THROUGH; + + case FR_TYPE_INT32: + if (b->vb_int32 < 0) return OVERFLOW; + + num = b->vb_int32; + break; + + case FR_TYPE_BOOL: + case FR_TYPE_UINT8: + case FR_TYPE_UINT16: + case FR_TYPE_UINT64: + case FR_TYPE_SIZE: + if (fr_value_box_cast_in_place(ctx, b, FR_TYPE_UINT32, NULL) < 0) return -1; + FALL_THROUGH; + + case FR_TYPE_UINT32: + num = b->vb_uint32; + break; + + default: + /* + * Can't add an IP address to a prefix. + */ + return INVALID; + } + + /* + * Trying to add a number outside of the given prefix. That's not allowed. + */ + if (num >= (((uint32_t) 1) << a->vb_ip.prefix)) return OVERFLOW; + + dst->vb_ip.addr.v4.s_addr = a->vb_ip.addr.v4.s_addr + num; + dst->vb_ip.prefix = 0; + break; + + default: + return INVALID; + } + + return 0; +} + +typedef int (*fr_binary_op_t)(TALLOC_CTX *ctx, fr_value_box_t *dst, fr_value_box_t const *a, fr_token_t op, fr_value_box_t const *b); + +static const fr_binary_op_t calc_type[FR_TYPE_MAX] = { + [FR_TYPE_OCTETS] = calc_octets, + [FR_TYPE_STRING] = calc_string, + + [FR_TYPE_DATE] = calc_date, + [FR_TYPE_TIME_DELTA] = calc_time_delta, + + [FR_TYPE_IPV4_ADDR] = calc_ipv4_addr, +}; + +int fr_value_calc(TALLOC_CTX *ctx, fr_value_box_t *dst, fr_type_t hint, fr_value_box_t const *a, fr_token_t op, fr_value_box_t const *b) +{ + int rcode; + + if (fr_type_is_structural(dst->type)) { + fr_strerror_const("Cannot calculate results for structural types"); + return -1; + } + + switch (op) { + case T_OP_CMP_EQ: + case T_OP_NE: + case T_OP_GE: + case T_OP_GT: + case T_OP_LE: + case T_OP_LT: + if ((hint != FR_TYPE_NULL) && (hint != FR_TYPE_BOOL)) { + fr_strerror_const("Invalid destination type for comparison operator"); + return -1; + } + + fr_value_box_clear_value(dst); + fr_value_box_init(dst, FR_TYPE_BOOL, NULL, false); // just force it... + + rcode = fr_value_box_cmp_op(op, a, b); + if (rcode < 0) { + fr_strerror_const("Failed doing comparison"); + return -1; + } + + dst->vb_bool = (rcode > 0); + return 0; + + case T_OP_ADD: + case T_OP_SUB: + case T_OP_PREPEND: + if (hint == FR_TYPE_NULL) { + if (a->type != b->type) { + fr_strerror_const("not yet implemented"); + return -1; + } + + fr_value_box_init(dst, hint, NULL, false); + + } else if ((dst != a) && (dst != b)) { + /* + * It's OK to use one of the inputs as + * the output. But if we don't, ensure + * that the output value box is + * initialized. + */ + fr_value_box_init(dst, hint, NULL, false); + } + + if (!calc_type[dst->type]) { + fr_strerror_printf("No handler has been implemented for leaf type %s", + fr_table_str_by_value(fr_value_box_type_table, dst->type, "")); + rcode = -1; + break; + } + + rcode = calc_type[dst->type](ctx, dst, a, op, b); + break; + + default: + rcode = INVALID; + break; + } + + if (rcode == OVERFLOW) { + fr_strerror_printf("Value overflows/underflows when calculating answer for %s", + fr_table_str_by_value(fr_value_box_type_table, dst->type, "")); + + } else if (rcode == INVALID) { + fr_strerror_printf("Invalid operator %s for destination type %s", + fr_tokens[op], + fr_table_str_by_value(fr_value_box_type_table, dst->type, "")); + } + + return rcode; +} diff --git a/src/lib/util/calc.h b/src/lib/util/calc.h new file mode 100644 index 0000000000..c4867e7989 --- /dev/null +++ b/src/lib/util/calc.h @@ -0,0 +1,38 @@ +#pragma once +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/** + * $Id$ + * + * @file lib/util/edit.h + * @brief Structures and prototypes for editing lists. + * + * @copyright 2021 Network RADIUS SAS (legal@networkradius.com) + */ +RCSIDH(calc_h, "$Id$") + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int fr_value_calc(TALLOC_CTX *ctx, fr_value_box_t *dst, fr_type_t hint, fr_value_box_t const *a, fr_token_t op, fr_value_box_t const *b); + +#ifdef __cplusplus +} +#endif diff --git a/src/lib/util/libfreeradius-util.mk b/src/lib/util/libfreeradius-util.mk index b2ca3303da..1ccfa4a0a8 100644 --- a/src/lib/util/libfreeradius-util.mk +++ b/src/lib/util/libfreeradius-util.mk @@ -10,6 +10,7 @@ SOURCES := \ base16.c \ base32.c \ base64.c \ + calc.c \ cap.c \ cursor.c \ dbuff.c \ diff --git a/src/tests/unit/calc.txt b/src/tests/unit/calc.txt new file mode 100644 index 0000000000..d6593dd0aa --- /dev/null +++ b/src/tests/unit/calc.txt @@ -0,0 +1,35 @@ +# +# Calculate various thingies. +# + +# string append +calc string "a" . string "b" -> string +match ab + +# string prepend +calc string "a" ^ string "b" -> string +match ba + +# time deltas +calc time_delta 1 + time_delta 2 -> time_delta +match 3 + +# dates can be subtracted, but not added. +calc date "Jan 11 1970 00:00:00 UTC" - date "Jan 1 1970 00:00:00 UTC" -> time_delta +match 864000 + +# One day earlier +calc date "Jan 11 1970 00:00:00 UTC" - time_delta 86400 -> date +match Jan 10 1970 00:00:00 UTC + +calc date "Jan 11 1970 00:00:00 UTC" - time_delta 1d -> date +match Jan 10 1970 00:00:00 UTC + +# +# Comparisons don't need output data types +# +calc date "Jan 11 1970 00:00:00 UTC" < date "Jan 1 1970 00:00:00 UTC" +match no + +count +match 14