From: Aki Tuomi Date: Sat, 21 Dec 2024 21:35:47 +0000 (+0200) Subject: lib-var-expand: Add ability to export and import programs X-Git-Tag: 2.4.1~329 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=72d91d9039766e7669f367c1025e5d5d593d88c0;p=thirdparty%2Fdovecot%2Fcore.git lib-var-expand: Add ability to export and import programs This adds functions for exporting and importing var-expand programs into strings. --- diff --git a/src/lib-var-expand/expansion-program.c b/src/lib-var-expand/expansion-program.c index 3f1c36e5d1..0b761e541a 100644 --- a/src/lib-var-expand/expansion-program.c +++ b/src/lib-var-expand/expansion-program.c @@ -3,6 +3,7 @@ #include "lib.h" #include "array.h" #include "str.h" +#include "strescape.h" #include "hex-binary.h" #include "var-expand-private.h" #include "var-expand-parser-private.h" @@ -179,3 +180,455 @@ void var_expand_program_free(struct var_expand_program **_program) pool_unref(&program->pool); } + +/* Export code */ + +/* Encodes numbers in 7-bit bytes, using 8th to indicate + that the number continues. Uses little endian encoding + to allow this. */ +static void export_number(string_t *dest, intmax_t number) +{ + unsigned char b; + + /* fast path - store any non-negative number that's smaller than + 127 as number + 1, including 0. */ + if (number >= 0 && number < 0x7f) { + b = number + 1; + str_append_c(dest, b); + return; + } + + /* Store sign with 0x80, so we can differentiate + from fast path. */ + if (number < 0) { + str_append_c(dest, 0x80 | '-'); + number = -number; + } else + str_append_c(dest, 0x80 | '+'); + + /* Store the number in 7 byte chunks + so we can use the 8th bit for indicating + whether the number continues. */ + while (number > 0) { + if (number > 0x7f) + b = 0x80; + else + b = 0x0; + b |= number & 0x7f; + number >>= 7; + str_append_c(dest, b); + } +} + +static void var_expand_program_export_one(const struct var_expand_program *program, + string_t *dest) +{ + const struct var_expand_statement *stmt = program->first; + while (stmt != NULL) { + str_append(dest, stmt->function); + str_append_c(dest, '\1'); + const struct var_expand_parameter *param = stmt->params; + param = stmt->params; + while (param != NULL) { + if (param->key != NULL) + str_append(dest, param->key); + str_append_c(dest, '\1'); + switch (param->value_type) { + case VAR_EXPAND_PARAMETER_VALUE_TYPE_STRING: + str_append_c(dest, 's'); + str_append_tabescaped(dest, param->value.str); + str_append_c(dest, '\r'); + break; + case VAR_EXPAND_PARAMETER_VALUE_TYPE_INT: + str_append_c(dest, 'i'); + export_number(dest, param->value.num); + break; + case VAR_EXPAND_PARAMETER_VALUE_TYPE_VARIABLE: + str_append_c(dest, 'v'); + str_append_tabescaped(dest, param->value.str); + str_append_c(dest, '\r'); + break; + default: + i_unreached(); + } + param = param->next; + if (param != NULL) + str_append_c(dest, '\1'); + } + str_append_c(dest, '\t'); + stmt = stmt->next; + if (stmt != NULL) + str_append_c(dest, '\1'); + else + str_append_c(dest, '\t'); + } + const char *const *vars = program->variables; + + for (; vars != NULL && *vars != NULL; vars++) { + /* ensure variable has no \1 in name */ + i_assert(strchr(*vars, '\1') == NULL); + str_append(dest, *vars); + str_append_c(dest, '\1'); + } + str_append_c(dest, '\t'); +} + +void var_expand_program_export_append(string_t *dest, + const struct var_expand_program *program) +{ + i_assert(program != NULL); + i_assert(dest != NULL); + + while (program != NULL) { + if (program->only_literal) { + i_assert(program->first->params->value_type == + VAR_EXPAND_PARAMETER_VALUE_TYPE_STRING); + str_append_c(dest, '\1'); + str_append_tabescaped(dest, program->first->params->value.str); + str_append_c(dest, '\r'); + } else { + str_append_c(dest, '\2'); + var_expand_program_export_one(program, dest); + } + + program = program->next; + } +} + +const char *var_expand_program_export(const struct var_expand_program *program) +{ + string_t *dest = t_str_new(64); + var_expand_program_export_append(dest, program); + return str_c(dest); +} + +/* Import code */ + +static int extract_name(char *data, size_t size, + const char **value_r, const char **error_r) +{ + char *ptr = memchr(data, '\1', size); + if (ptr == NULL) { + *error_r = "Missing end of name"; + return -1; + } + size_t len = ptr - data; + if (len == 0) { + *value_r = NULL; + return 1; + } + *value_r = data; + *ptr = '\0'; + return len + 1; + +} + +static int extract_value(char *data, size_t size, + const char **value_r, const char **error_r) +{ + char *ptr = memchr(data, '\r', size); + if (ptr == NULL) { + *error_r = "Missing end of string"; + return -1; + } + size_t len = ptr - data; + *ptr = '\0'; + *value_r = str_tabunescape(data); + /* make sure we end up in right place. */ + return len + 1; +} + +static int extract_number(const char *data, size_t size, intmax_t *value_r, + const char **error_r) +{ + const unsigned char *ptr = (const unsigned char*)data; + bool negative; + size_t len = 1; + + if ((*ptr & 0x80) == 0) { + /* fast path for small positive number */ + intmax_t number = *ptr; + *value_r = number - 1; + return 1; + } + + const char sign = *ptr - 0x80; + if (sign == '+') { + negative = FALSE; + } else if (sign == '-') { + negative = TRUE; + } else { + *error_r = "Unknown number"; + return -1; + } + ptr++; + + intmax_t value = 0; + intmax_t shift = 0; + + /* a number can be at most 9 bytes */ + for (size_t i = 0; i < I_MIN(size, 9); i++) { + len++; + value |= ((*(ptr) & 0x7fLL) << shift); + /* if high byte is set, the number continues */ + if ((*ptr & 0x80) == 0) + break; + shift += 7; + ptr++; + } + + if ((*ptr & 0x80) != 0) { + *error_r = "Unfinished number"; + return -1; + } + + if (negative) + value = -value; + + *value_r = value; + + return len; +} + +#define ADVANCE_INPUT(count) \ + if (unlikely(size < (size_t)count)) { \ + *error_r = "Premature end of data"; \ + return -1; \ + }\ + data = data + (count); \ + size = size - (size_t)(count); + +static int var_expand_program_import_stmt(char *data, size_t size, + struct var_expand_program *program, + const char **error_r) +{ + const char *name; + const char *value; + size_t orig_size = size; + + /* normal program, starts with filter name */ + int ret = extract_name(data, size, &name, error_r); + if (ret < 0) + return -1; + if (name == NULL) { + *error_r = "missing function name"; + return -1; + } + ADVANCE_INPUT(ret); + + struct var_expand_statement *stmt = + p_new(program->pool, struct var_expand_statement, 1); + + if (program->first == NULL) + program->first = stmt; + else { + struct var_expand_statement *ptr = program->first; + while (ptr->next != NULL) ptr = ptr->next; + ptr->next = stmt; + } + + stmt->function = name; + struct var_expand_parameter *prev = NULL; + int idx = -1; + + while (size > 0 && *data != '\t') { + struct var_expand_parameter *param = + p_new(program->pool, struct var_expand_parameter, 1); + /* check if it's named parameter */ + if (*data == '\1') { + param->idx = ++idx; + ADVANCE_INPUT(1); + } else { + ret = extract_name(data, size, + &name, error_r); + if (ret < 0) + return -1; + ADVANCE_INPUT(ret); + param->key = name; + } + + /* check the parameter type */ + switch (*data) { + case 's': + param->value_type = + VAR_EXPAND_PARAMETER_VALUE_TYPE_STRING; + break; + case 'i': + param->value_type = + VAR_EXPAND_PARAMETER_VALUE_TYPE_INT; + break; + case 'v': + param->value_type = + VAR_EXPAND_PARAMETER_VALUE_TYPE_VARIABLE; + break; + default: + *error_r = "Unsupported parameter type"; + return -1; + } + ADVANCE_INPUT(1); + + if (param->value_type == VAR_EXPAND_PARAMETER_VALUE_TYPE_STRING || + param->value_type == VAR_EXPAND_PARAMETER_VALUE_TYPE_VARIABLE) { + ret = extract_value(data, size, + &value, error_r); + if (ret < 0) + return -1; + ADVANCE_INPUT(ret); + param->value.str = value; + } else if (param->value_type == VAR_EXPAND_PARAMETER_VALUE_TYPE_INT) { + ret = extract_number(data, size, ¶m->value.num, + error_r); + if (ret < 0) + return -1; + + ADVANCE_INPUT(ret); + } else { + *error_r = "Unsupported value type"; + return -1; + } + + if (prev == NULL) + stmt->params = param; + else + prev->next = param; + prev = param; + + if (*data == '\t') { + break; + } else if (*data == '\1') { + ADVANCE_INPUT(1); + } else { + *error_r = "Missing parameter end"; + return -1; + } + } + + if (*data != '\t') + *error_r = "Missing parameter statement end"; + + ADVANCE_INPUT(1); + + return orig_size - size; +} + +static int var_expand_program_import_one(char **_data, size_t *_size, + struct var_expand_program *program, + const char **error_r) +{ + char *data = *_data; + size_t size = *_size; + const char *value; + int ret; + + /* Only literal */ + if (*data == '\1') { + ADVANCE_INPUT(1); + ret = extract_value(data, size, &value, error_r); + if (ret < 0) + return -1; + ADVANCE_INPUT(ret); + + /* just literal data */ + struct var_expand_statement *stmt = + p_new(program->pool, struct var_expand_statement, 1); + struct var_expand_parameter *param = + p_new(program->pool, struct var_expand_parameter, 1); + param->idx = 0; + param->value_type = VAR_EXPAND_PARAMETER_VALUE_TYPE_STRING; + param->value.str = value; + stmt->params = param; + stmt->function = "literal"; + program->first = stmt; + program->only_literal = TRUE; + /* A full program */ + } else if (*data == '\2') { + ADVANCE_INPUT(1); + while (*data != '\t' && size > 0) { + int ret = var_expand_program_import_stmt(data, size, program, error_r); + if (ret < 0) + return -1; + ADVANCE_INPUT(ret); + if (*data == '\t') { + ADVANCE_INPUT(1); + break; + } else if (*data != '\1') { + *error_r = "Missing statement end"; + return -1; + } + ADVANCE_INPUT(1); + } + /* And finally there should be variables */ + if (*data != '\t') { + const char *ptr = memchr(data, '\t', size); + if (ptr == NULL) { + *error_r = "Missing variables end"; + return -1; + } + size_t len = ptr - data; + program->variables = (const char *const *) + p_strsplit(program->pool, data, "\1"); + ADVANCE_INPUT(len + 1); + } else { + ADVANCE_INPUT(1); + } + } else { + *error_r = "Unknown input"; + return -1; + } + *_data = data; + *_size = size; + + return 0; +} + +int var_expand_program_import_sized(const char *data, size_t size, + struct var_expand_program **program_r, + const char **error_r) +{ + i_assert(data != NULL); + + /* The absolute minimum program is \2 \t or \1 \r. */ + if (size < 2) { + *error_r = "Too short"; + return -1; + } + + pool_t pool = pool_alloconly_create(MEMPOOL_GROWING"var expand program", size); + struct var_expand_program *prev = NULL; + struct var_expand_program *first = NULL; + int ret; + char *copy_data = p_strndup(pool, data, size); + + while (size > 0) { + struct var_expand_program *program = + p_new(pool, struct var_expand_program, 1); + program->pool = pool; + T_BEGIN { + ret = var_expand_program_import_one(©_data, &size, + program, error_r); + } T_END; + if (ret < 0) + break; + if (first == NULL) + first = program; + if (prev != NULL) + prev->next = program; + prev = program; + } + + if (ret < 0) + pool_unref(&pool); + else + *program_r = first; + + return ret; +} + +int var_expand_program_import(const char *data, + struct var_expand_program **program_r, + const char **error_r) +{ + i_assert(data != NULL); + return var_expand_program_import_sized(data, strlen(data), program_r, + error_r); +} diff --git a/src/lib-var-expand/test-var-expand.c b/src/lib-var-expand/test-var-expand.c index 4c8582b68d..e7238c23a4 100644 --- a/src/lib-var-expand/test-var-expand.c +++ b/src/lib-var-expand/test-var-expand.c @@ -15,6 +15,7 @@ #endif #include +#include struct var_expand_test { const char *in; @@ -22,6 +23,9 @@ struct var_expand_test { int ret; }; +/* Run with -b to set TRUE */ +static bool do_bench = FALSE; + static void run_var_expand_tests(const struct var_expand_params *params, const struct var_expand_test tests[], size_t test_count) @@ -959,7 +963,219 @@ static void test_var_expand_generate(void) test_end(); } -int main(void) +static void test_var_expand_export_import(void) +{ + test_begin("var_expand(export/import)"); + + const struct var_expand_params params = { + .table = (const struct var_expand_table[]) { + { .key = "variable", .value = "1234567890" }, + { .key = "this", .value = "isht" }, + { .key = "a", .value = "b" }, + { .key = "test", .value = "tset" }, + VAR_EXPAND_TABLE_END + }, + }; + + const struct test_case { + const char *prog_in; + const char *export; + } test_cases[] = { + { "", "\x02\t" }, + { "literal", "\x01literal\r" }, + { "\x01\x02\r\t", "\x01\x01""1\x02\x01r\x01t\r" }, + { "%{variable}", "\x02variable\x01\t\tvariable\x01\t" }, + { "%{lookup('variable')}", "\x02lookup\x01\x01svariable\r\t\t\t" }, + { + "%{this} is %{a} simple %{test}", + "\x02this\x01\t\ta\x01test\x01this" + "\x01\t\x01 is \r\x02" + "a\x01\t\t\t\x01 simple \r\x02" + "test\x01\t\t\t" + }, + { + "%{variable | substr(0,1) % 32}", + "\x02variable\x01\t\x01substr\x01\x01i\x01\x01\x01i\x02" + "\t\x01""calculate\x01\x01i\x05\x01\x01i!\t\tvariable" + "\x01\t" + }, + { + "%{variable | substr(0,1) % 32} / %{variable | substr(1,1) % 32}", + "\x02variable\x01\t\x01substr\x01\x01i\x01\x01\x01i\x02" + "\t\x01""calculate\x01\x01i\x05\x01\x01i!\t\tvariable" + "\x01\t\x01 / \r\x02variable\x01\t\x01substr\x01\x01i" + "\x02\x01\x01i\x02\t\x01""calculate\x01\x01i\x05\x01" + "\x01i!\t\t\t" + }, +#if UINT32_MAX < INTMAX_MAX + { + "%{variable + 4294967296}", + "\x02variable\x01\t\x01""calculate\x01\x01i\x01\x01\x01" + "i\xab\x80\x80\x80\x80\x10\t\tvariable\x01\t" + }, +#endif + { + "%{variable + -100}", + "\x02variable\x01\t\x01""calculate\x01\x01i\x01\x01\x01" + "i\xad""d\t\tvariable\x01\t" + }, + { + "%{variable + 126}", + "\x02variable\x01\t\x01""calculate\x01\x01i\x01\x01\x01i" + "\x7f\t\tvariable\x01\t" + }, + { + "%{variable + 127}", + "\x02variable\x01\t\x01""calculate\x01\x01i\x01\x01\x01i" + "\xab\x7f\t\tvariable\x01\t" + }, + }; + + string_t *dest = t_str_new(64); + + string_t *result_a = t_str_new(64); + string_t *result_b = t_str_new(64); + + for(size_t i = 0; i < N_ELEMENTS(test_cases); i++) { + const char *error; + const struct test_case *t = &test_cases[i]; + struct var_expand_program *prog; + str_truncate(dest, 0); + str_truncate(result_a, 0); + str_truncate(result_b, 0); + + /* We test two things, that we can export & import the program + and that the result of the imported program matches the + original program. */ + if (var_expand_program_create(t->prog_in, &prog, &error) <0) + i_error("var_expand_program_create(): %s", error); + if (var_expand_program_execute(result_a, prog, ¶ms, &error) < 0) + i_error("var_expand_program_execute(a): %s", error); + var_expand_program_dump(prog, dest); + str_truncate(dest, 0); + var_expand_program_export_append(dest, prog); + var_expand_program_free(&prog); + test_assert_strcmp_idx(str_c(dest), t->export, i); + if (var_expand_program_import(str_c(dest), &prog, &error) < 0) + i_error("var_expand_program_import(): %s", error); + if (var_expand_program_execute(result_b, prog, ¶ms, &error) < 0) + i_error("var_expand_program_execute(b): %s", error); + test_assert_strcmp_idx(str_c(result_a), str_c(result_b), i); + str_truncate(dest, 0); + var_expand_program_dump(prog, dest); + var_expand_program_free(&prog); + } + + const struct test_case_err { + const char *input; + const char *error; + } test_cases_err[] = { + { "", "Too short" }, + { "\x01literal", "Missing end of string" }, + { "\x03literal", "Unknown input" }, + { "\x02literal\x01", "Premature end of data" }, + { "\x02literal\x01text\x01", "Unsupported parameter type" }, + { "\x02literal\x01\x01stext\t", "Missing end of string" }, + { "\x02literal\x01\x01i\xa1", "Unknown number" }, + { "\x02literal\x01\x01i\xab\xf0\t", "Missing parameter end" }, + { + "\x02literal\x01\x01i\xab\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0", + "Unfinished number" + }, + { "\x02literal\x01\x01stext\r", "Missing parameter end" }, + { "\x02literal\x01\x01stext\r\t", "Missing statement end" }, + { "\x02literal\x01\x01stext\r\t\t", "Missing variables end" }, + }; + + for(size_t i = 0; i < N_ELEMENTS(test_cases_err); i++) { + struct var_expand_program *prog; + const char *error; + const struct test_case_err *t = &test_cases_err[i]; + int ret = var_expand_program_import(t->input, &prog, &error); + test_assert_cmp(ret, ==, -1); + if (ret == 0) { + var_expand_program_free(&prog); + continue; + } + + test_assert_strcmp(error, t->error); + } + + test_end(); +} + +#define BENCH_ROUNDS 200000 +static void test_var_expand_bench(void) +{ + if (!do_bench) + return; + struct test_cases { + const char *program; + const char *exported; + } test_cases[] = { + { "literal", NULL }, + { "%{variable}", NULL }, + { "%{lookup('variable')}", NULL }, + { "%{this} is %{a} simple %{test}", NULL }, + { "%{variable | substr(0,1) % 32}", NULL }, + { "%{variable | substr(0,1) % 32} / %{variable | substr(1,1) % 32}", NULL }, + { "%{variable + 4294967296}", NULL }, + }; + test_begin("var_expand(export benchmark)"); + + /* prepare exports */ + for (size_t i = 0; i < N_ELEMENTS(test_cases); i++) { + const char *error ATTR_UNUSED; + struct var_expand_program *prog; + if (var_expand_program_create(test_cases[i].program, &prog, + &error) < 0) + i_error("%s", error); + test_cases[i].exported = var_expand_program_export(prog); + var_expand_program_free(&prog); + } + + struct timespec ts0, ts1; + int ret; + for (size_t i = 0; i < N_ELEMENTS(test_cases); i++) { + i_debug("%s", test_cases[i].program); + /* do speedtest */ + ret = clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts0); + i_assert(ret == 0); + + for (int rounds = 0; rounds < BENCH_ROUNDS; rounds++) { + const char *error ATTR_UNUSED; + struct var_expand_program *prog; + if (var_expand_program_create(test_cases[i].program, + &prog, &error) < 0) + i_error("%s", error); + var_expand_program_free(&prog); + } + ret = clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts1); + i_assert(ret == 0); + unsigned long long diff = (ts1.tv_sec - ts0.tv_sec) * 1000000000 + (ts1.tv_nsec - ts0.tv_nsec); + i_debug("var_expand_program_create: %llu ns total, %llu ns / program", + diff, diff / BENCH_ROUNDS); + + ret = clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts0); + i_assert(ret == 0); + for (int rounds = 0; rounds < BENCH_ROUNDS; rounds++) { + const char *error ATTR_UNUSED; + struct var_expand_program *prog; + if (var_expand_program_import(test_cases[i].exported, + &prog, &error) < 0) + i_error("%s", error); + var_expand_program_free(&prog); + } + ret = clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts1); + i_assert(ret == 0); + diff = (ts1.tv_sec - ts0.tv_sec) * 1000000000 + (ts1.tv_nsec - ts0.tv_nsec); + i_debug("var_expand_program_import: %llu ns total, %llu ns / program", + diff, diff / BENCH_ROUNDS); + } + test_end(); +} + +int main(int argc, char *const argv[]) { void (*const tests[])(void) = { test_var_expand_merge_tables, @@ -977,8 +1193,18 @@ int main(void) test_var_expand_perc, test_var_expand_set_copy, test_var_expand_generate, + test_var_expand_export_import, + test_var_expand_bench, NULL }; + char opt; + while ((opt = getopt(argc, argv, "b")) != -1) { + if (opt == 'b') + do_bench = TRUE; + else + i_fatal("Usage: %s [-b]", argv[0]); + } + return test_run(tests); } diff --git a/src/lib-var-expand/var-expand.h b/src/lib-var-expand/var-expand.h index f8c71a24ef..58aea4ef03 100644 --- a/src/lib-var-expand/var-expand.h +++ b/src/lib-var-expand/var-expand.h @@ -167,6 +167,41 @@ static inline void var_expand_table_copy(struct var_expand_table *table, entry_b->func = entry_a->func; } +/* Export variable expand program to a portable string. + + Output format is a list of programs. Each program is catenated after + the previous program. + If there is only literal: \x01 + : tab-escaped string \r + Otherwise: + : \x02 \x01 \t \t + : string + : \x01 \x01 .. + Note that last parameter has no \x01 at the end. + : \x01 + : string + : s = string, i = intmax, v = variable + : | tab-escaped string \r + : + The number is expressed in 7-bit bytes and 8th bit indicates the + presence of next byte. The number is in little-endian ordering. + : \x01 \x01 .. + Note that last variable has no \x01 at the end. +*/ + +const char *var_expand_program_export(const struct var_expand_program *program); +void var_expand_program_export_append(string_t *dest, + const struct var_expand_program *program); + +/* Imports a variable expansion program exported by var_expand_program_export(). */ + +int var_expand_program_import(const char *data, + struct var_expand_program **program_r, + const char **error_r); +int var_expand_program_import_sized(const char *data, size_t size, + struct var_expand_program **program_r, + const char **error_r); + void var_expand_crypt_load(void); #endif