#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"
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);
+}
#endif
#include <time.h>
+#include <unistd.h>
struct var_expand_test {
const char *in;
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)
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,
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);
}