From 6bf57d345050d40ef5bd4f9a30b184ba13aad886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?= Date: Fri, 1 May 2015 15:24:42 +0200 Subject: [PATCH] Start writing a new configuration system For degesch but in the long term for the rest as well. --- common.c | 596 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ degesch.c | 12 -- 2 files changed, 596 insertions(+), 12 deletions(-) diff --git a/common.c b/common.c index 759c452..5f86396 100644 --- a/common.c +++ b/common.c @@ -31,6 +31,7 @@ #endif // WANT_SYSLOG_LOGGING #include "liberty/liberty.c" +#include #include // --- Logging ----------------------------------------------------------------- @@ -458,3 +459,598 @@ socks_connect (const char *socks_host, const char *socks_port, fail: return result; } + +// --- To be moved to liberty -------------------------------------------------- + +static bool +isalpha_ascii (int c) +{ + c &= ~32; + return c >= 'A' && c <= 'Z'; +} + +static bool +isdigit_ascii (int c) +{ + return c >= '0' && c <= '9'; +} + +static bool +isalnum_ascii (int c) +{ + return isalpha_ascii (c) || isdigit_ascii (c); +} + +static int +toupper_ascii (int c) +{ + return c >= 'A' && c <= 'Z' ? c : c - ('a' - 'A'); +} + +// --- Advanced configuration -------------------------------------------------- + +// This is a new configuration format, superseding the one currently present +// in liberty. It's just a lot more complicated and allows key-value maps. +// We need it in degesch to provide non-sucking user experience. + +enum config_item_type +{ + CONFIG_ITEM_NULL, ///< No value + CONFIG_ITEM_OBJECT, ///< Key-value map + CONFIG_ITEM_BOOLEAN, ///< Truth value + CONFIG_ITEM_INTEGER, ///< Integer + CONFIG_ITEM_STRING, ///< Arbitrary string of characters + CONFIG_ITEM_STRING_ARRAY ///< Comma-separated list of strings +}; + +struct config_item_ +{ + enum config_item_type type; ///< Type of the item + union + { + struct str_map object; ///< Key-value data + bool boolean; ///< Boolean data + int64_t integer; ///< Integer data + struct str string; ///< String data + } + value; ///< The value of this item + + struct config_schema *schema; ///< Schema describing this value + void *user_data; ///< User value attached by schema owner +}; + +struct config_schema +{ + const char *name; ///< Name of the item + const char *comment; ///< User-readable description + + enum config_item_type type; ///< Required type + bool is_nullable; ///< Can be null? + const char *default_; ///< Default as a configuration snippet + + /// Check if the new value can be accepted. + /// If this is not defined, only "type" and "is_nullable" is considered. + bool (*validate) (struct config_item_ *, const struct config_item_ *); + + /// The value has changed. Only appliable to objects. + bool (*on_changed) (struct config_item_ *); + + /// Free any resources located in "item->user_data" + void (*on_destroy) (struct config_item_ *item); +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +config_item_destroy (struct config_item_ *self) +{ + if (self->schema && self->schema->on_destroy) + self->schema->on_destroy (self); + + switch (self->type) + { + case CONFIG_ITEM_STRING: + case CONFIG_ITEM_STRING_ARRAY: + str_free (&self->value.string); + break; + case CONFIG_ITEM_OBJECT: + str_map_free (&self->value.object); + default: + break; + } + free (self); +} + +static struct config_item_ * +config_item_new (enum config_item_type type) +{ + struct config_item_ *self = xcalloc (1, sizeof *self); + self->type = type; + return self; +} + +static struct config_item_ * +config_item_null (void) +{ + return config_item_new (CONFIG_ITEM_NULL); +} + +static struct config_item_ * +config_item_boolean (bool b) +{ + struct config_item_ *self = config_item_new (CONFIG_ITEM_BOOLEAN); + self->value.boolean = b; + return self; +} + +static struct config_item_ * +config_item_integer (int64_t i) +{ + struct config_item_ *self = config_item_new (CONFIG_ITEM_INTEGER); + self->value.integer = i; + return self; +} + +static struct config_item_ * +config_item_string (const struct str *s) +{ + struct config_item_ *self = config_item_new (CONFIG_ITEM_STRING); + str_init (&self->value.string); + if (s) str_append_str (&self->value.string, s); + return self; +} + +static struct config_item_ * +config_item_string_array (const struct str *s) +{ + struct config_item_ *self = config_item_string (s); + self->type = CONFIG_ITEM_STRING_ARRAY; + return self; +} + +static struct config_item_ * +config_item_object (void) +{ + struct config_item_ *self = config_item_new (CONFIG_ITEM_BOOLEAN); + str_map_init (&self->value.object); + self->value.object.free = (void (*)(void *)) config_item_destroy; + return self; +} + +/// Doesn't do any validations or such, only moves source data to the item +static void +config_item_move (struct config_item_ *self, struct config_item_ *source) +{ + // TODO +} + +static bool +config_item_set_from (struct config_item_ *self, + struct config_item_ *source, struct error **e) +{ + hard_assert (self->type == CONFIG_ITEM_OBJECT); + // TODO +} + +static struct config_item_ * +config_item_get (struct config_item_ *self, const char *path) +{ + hard_assert (self->type == CONFIG_ITEM_OBJECT); + // TODO +} + +static void +config_item_write (struct config_item_ *root, struct str *output) +{ + // TODO +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +config_schema_apply_to_object + (struct config_schema *schema_array, struct config_item_ *object) +{ + hard_assert (object->type == CONFIG_ITEM_OBJECT); + // TODO +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +enum config_token +{ + CONFIG_T_ABORT, ///< EOF or error + + CONFIG_T_WORD, ///< [a-zA-Z0-9_]+ + CONFIG_T_EQUALS, ///< Equal sign + CONFIG_T_LBRACE, ///< Left curly bracket + CONFIG_T_RBRACE, ///< Right curly bracket + CONFIG_T_NEWLINE, ///< New line + + CONFIG_T_NULL, ///< CONFIG_ITEM_NULL + CONFIG_T_BOOLEAN, ///< CONFIG_ITEM_BOOLEAN + CONFIG_T_INTEGER, ///< CONFIG_ITEM_INTEGER + CONFIG_T_STRING ///< CONFIG_ITEM_STRING{,_LIST} +}; + +struct config_tokenizer +{ + const char *p; + size_t len; + + unsigned line; + unsigned column; + + int64_t integer; + struct str string; +}; + +/// Input has to be null-terminated anyway +static void +config_tokenizer_init (struct config_tokenizer *self, const char *p, size_t len) +{ + memset (self, 0, sizeof *self); + self->p = p; + self->len = len; + str_init (&self->string); +} + +static void +config_tokenizer_free (struct config_tokenizer *self) +{ + str_free (&self->string); +} + +static bool +config_tokenizer_is_word_char (int c) +{ + return isalnum_ascii (c) || c == '_'; +} + +static int +config_tokenizer_advance (struct config_tokenizer *self) +{ + int c = *self->p++; + if (c == '\n') + { + self->column = 0; + self->line++; + } + else + self->column++; + + self->len--; + return c; +} + +static void +config_tokenizer_error (struct config_tokenizer *self, + struct error **e, const char *description) +{ + // FIXME: we don't always want to specify the line + error_set (e, "near line %u, column %u: %s", + self->line + 1, self->column + 1, description); +} + +static enum config_token +config_tokenizer_next (struct config_tokenizer *self, struct error **e) +{ + // Skip over any whitespace between tokens + while (self->len && isspace_ascii (*self->p) && *self->p != '\n') + config_tokenizer_advance (self); + if (!self->len) + return CONFIG_T_ABORT; + + switch (*self->p) + { + case '\n': config_tokenizer_advance (self); return CONFIG_T_NEWLINE; + case '=': config_tokenizer_advance (self); return CONFIG_T_EQUALS; + case '{': config_tokenizer_advance (self); return CONFIG_T_LBRACE; + case '}': config_tokenizer_advance (self); return CONFIG_T_RBRACE; + + case '#': + // Comments go until newline + while (self->len) + if (config_tokenizer_advance (self) == '\n') + return CONFIG_T_NEWLINE; + return CONFIG_T_ABORT; + + case '"': + // TODO: string, validate as UTF-8 + break; + } + + bool is_word = false; + while (config_tokenizer_is_word_char (*self->p)) + { + is_word = true; + str_reset (&self->string); + str_append_c (&self->string, config_tokenizer_advance (self)); + } + if (is_word) + { + if (!strcmp (self->string.str, "null")) + return CONFIG_T_NULL; + + bool boolean; + if (!set_boolean_if_valid (&boolean, self->string.str)) + return CONFIG_T_WORD; + + self->integer = boolean; + return CONFIG_T_BOOLEAN; + } + + char *end; + errno = 0; + self->integer = strtoll (self->p, &end, 10); + if (errno == ERANGE) + { + config_tokenizer_error (self, e, "integer out of range"); + return CONFIG_T_ABORT; + } + if (end != self->p) + { + self->len -= end - self->p; + self->p = end; + return CONFIG_T_INTEGER; + } + + config_tokenizer_error (self, e, "invalid input"); + return CONFIG_T_ABORT; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct config_parser +{ + struct config_tokenizer tokenizer; ///< Tokenizer + + struct error *error; ///< Tokenizer error + enum config_token token; ///< Current token in the tokenizer + bool replace_token; ///< Replace the token +}; + +static void +config_parser_init (struct config_parser *self, const char *script, size_t len) +{ + memset (self, 0, sizeof *self); + config_tokenizer_init (&self->tokenizer, script, len); + + // As reading in tokens may cause exceptions, we wait for the first peek() + // to replace the initial CONFIG_T_ABORT. + self->replace_token = true; +} + +static void +config_parser_free (struct config_parser *self) +{ + config_tokenizer_free (&self->tokenizer); + if (self->error) + error_free (self->error); +} + +static enum config_token +config_parser_peek (struct config_parser *self, jmp_buf out) +{ + if (self->replace_token) + { + self->token = config_tokenizer_next (&self->tokenizer, &self->error); + if (self->error) + longjmp (out, 1); + self->replace_token = false; + } + return self->token; +} + +static bool +config_parser_accept + (struct config_parser *self, enum config_token token, jmp_buf out) +{ + return self->replace_token = (config_parser_peek (self, out) == token); +} + +static void +config_parser_expect + (struct config_parser *self, enum config_token token, jmp_buf out) +{ + if (config_parser_accept (self, token, out)) + return; + + // TODO: fill in "X" and "Y" + config_tokenizer_error (&self->tokenizer, &self->error, + "unexpected X, expected Y"); + longjmp (out, 1); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +// We don't need no generator, but a few macros will come in handy. +// From time to time C just doesn't have the right features. + +#define PEEK() config_parser_peek (self, err) +#define ACCEPT(token) config_parser_accept (self, token, err) +#define EXPECT(token) config_parser_expect (self, token, err) +#define SKIP_NL() do {} while (ACCEPT (CONFIG_T_NEWLINE)) + +static struct config_item_ *config_parser_parse_object + (struct config_parser *self, jmp_buf out); + +static struct config_item_ * +config_parser_parse_value (struct config_parser *self, jmp_buf out) +{ + struct config_item_ *volatile result = NULL; + jmp_buf err; + + if (setjmp (err)) + { + if (result) + config_item_destroy (result); + longjmp (out, 1); + } + + if (ACCEPT (CONFIG_T_LBRACE)) + { + result = config_parser_parse_object (self, out); + EXPECT (CONFIG_T_RBRACE); + return result; + } + if (ACCEPT (CONFIG_T_NULL)) + return config_item_null (); + if (ACCEPT (CONFIG_T_BOOLEAN)) + return config_item_boolean (self->tokenizer.integer); + if (ACCEPT (CONFIG_T_INTEGER)) + return config_item_integer (self->tokenizer.integer); + if (ACCEPT (CONFIG_T_STRING)) + return config_item_string (&self->tokenizer.string); + + // TODO: fill in "X" as the token name + config_tokenizer_error (&self->tokenizer, &self->error, + "unexpected X, expected a value"); + longjmp (out, 1); +} + +/// Parse a single "key = value" assignment into @a object +static bool +config_parser_parse_kv_pair (struct config_parser *self, + struct config_item_ *object, jmp_buf out) +{ + char *volatile key = NULL; + jmp_buf err; + + if (setjmp (err)) + { + free (key); + longjmp (out, 1); + } + + SKIP_NL (); + + // Either this object's closing right brace if called recursively, + // or end of file when called on a whole configuration file + if (PEEK () == CONFIG_T_RBRACE + || PEEK () == CONFIG_T_ABORT) + return false; + + EXPECT (CONFIG_T_WORD); + key = xstrdup (self->tokenizer.string.str); + SKIP_NL (); + + EXPECT (CONFIG_T_EQUALS); + SKIP_NL (); + + str_map_set (&object->value.object, key, + config_parser_parse_value (self, err)); + + free (key); + key = NULL; + + if (PEEK () == CONFIG_T_RBRACE + || PEEK () == CONFIG_T_ABORT) + return false; + + EXPECT (CONFIG_T_NEWLINE); + return true; +} + +/// Parse the inside of an object definition +static struct config_item_ * +config_parser_parse_object (struct config_parser *self, jmp_buf out) +{ + struct config_item_ *volatile object = config_item_object (); + jmp_buf err; + + if (setjmp (err)) + { + config_item_destroy (object); + longjmp (out, 1); + } + + while (config_parser_parse_kv_pair (self, object, err)) + ; + return object; +} + +#undef PEEK +#undef ACCEPT +#undef EXPECT +#undef SKIP_NL + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +/// Parse a configuration snippet either as an object or a bare value. +/// If it's the latter (@a single_value_only), no newlines may follow. +static struct config_item_ * +config_item_parse (const char *script, size_t len, + bool single_value_only, struct error **e) +{ + struct config_parser parser; + config_parser_init (&parser, script, len); + + struct config_item_ *volatile object = NULL; + jmp_buf err; + + if (setjmp (err)) + { + if (object) + { + config_item_destroy (object); + object = NULL; + } + + error_propagate (e, parser.error); + parser.error = NULL; + goto end; + } + + if (single_value_only) + object = config_parser_parse_value (&parser, err); + else + object = config_parser_parse_object (&parser, err); + config_parser_expect (&parser, CONFIG_T_ABORT, err); +end: + config_parser_free (&parser); + return object; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +typedef void (*config_module_load_fn) + (struct config_item_ *subtree, void *user_data); + +struct config_module +{ + char *name; ///< Name of the subtree + config_module_load_fn loader; ///< Module config subtree loader + void *user_data; ///< User data +}; + +struct config +{ + struct str_map modules; ///< Toplevel modules + struct config_item_ *root; ///< CONFIG_ITEM_OBJECT +}; + +static void +config_init (struct config *self) +{ + // TODO +} + +static void +config_free (struct config *self) +{ + // TODO +} + +static bool +config_register_module (const char *name, + config_module_load_fn loader, void *user_data) +{ + // TODO +} + +static bool +config_load (struct config_item_ *root, struct error **e) +{ + // TODO +} diff --git a/degesch.c b/degesch.c index 9912977..8e41a1a 100644 --- a/degesch.c +++ b/degesch.c @@ -108,18 +108,6 @@ static struct config_item g_config_table[] = // All text stored in our data structures is encoded in UTF-8. // Or at least should be. The exception is IRC identifiers. -static bool -isdigit_ascii (int c) -{ - return c >= '0' && c <= '9'; -} - -static int -toupper_ascii (int c) -{ - return c >= 'A' && c <= 'Z' ? c : c - ('a' - 'A'); -} - /// Shorthand to set an error and return failure from the function #define FAIL(...) \ BLOCK_START \