From 6bf57d345050d40ef5bd4f9a30b184ba13aad886 Mon Sep 17 00:00:00 2001
From: Přemysl Janouch
Date: Fri, 1 May 2015 15:24:42 +0200
Subject: 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 \
--
cgit v1.2.3-70-g09d2