From 91fca5cb054fc95fe2e7ac090cac2c3a302565ca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Fri, 11 Dec 2015 02:46:34 +0100
Subject: [PATCH] Import configuration module from uirc3
It seems to be mature enough, and it's just a bit too useful.
---
liberty.c | 1118 +++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 1118 insertions(+)
diff --git a/liberty.c b/liberty.c
index 1b136ed..ba5a64d 100644
--- a/liberty.c
+++ b/liberty.c
@@ -29,9 +29,11 @@
#include
#include
#include
+#include
#include
#include
#include
+#include
#include
#include
@@ -2451,6 +2453,18 @@ base64_encode (const void *data, size_t len, struct str *output)
// --- Utilities ---------------------------------------------------------------
+static void
+cstr_split (const char *s, const char *delimiters, struct str_vector *out)
+{
+ const char *begin = s, *end;
+ while ((end = strpbrk (begin, delimiters)))
+ {
+ str_vector_add_owned (out, xstrndup (begin, end - begin));
+ begin = ++end;
+ }
+ str_vector_add (out, begin);
+}
+
static void
cstr_split_ignore_empty (const char *s, char delimiter, struct str_vector *out)
{
@@ -3714,6 +3728,1110 @@ connector_add_target (struct connector *self,
#endif // LIBERTY_WANT_POLLER
+// --- Advanced configuration --------------------------------------------------
+
+// This is a more powerful configuration format, adding key-value maps and
+// simplifying item validation and dynamic handling of changes. All strings
+// must be encoded in UTF-8.
+
+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
+ const char *default_; ///< Default as a configuration snippet
+
+ /// Check if the new value can be accepted.
+ /// In addition to this, "type" and having a default is considered.
+ bool (*validate) (const struct config_item *, struct error **e);
+
+ /// The value has changed
+ void (*on_change) (struct config_item *);
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static const char *
+config_item_type_name (enum config_item_type type)
+{
+ switch (type)
+ {
+ case CONFIG_ITEM_NULL: return "null";
+ case CONFIG_ITEM_BOOLEAN: return "boolean";
+ case CONFIG_ITEM_INTEGER: return "integer";
+ case CONFIG_ITEM_STRING: return "string";
+ case CONFIG_ITEM_STRING_ARRAY: return "string array";
+
+ default:
+ hard_assert (!"invalid config item type value");
+ return NULL;
+ }
+}
+
+static bool
+config_item_type_is_string (enum config_item_type type)
+{
+ return type == CONFIG_ITEM_STRING
+ || type == CONFIG_ITEM_STRING_ARRAY;
+}
+
+static void
+config_item_free (struct config_item *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;
+ }
+}
+
+static void
+config_item_destroy (struct config_item *self)
+{
+ config_item_free (self);
+ free (self);
+}
+
+/// Doesn't do any validations or handle schemas, just moves source data
+/// to the target item and destroys the source item
+static void
+config_item_move (struct config_item *self, struct config_item *source)
+{
+ // Not quite sure how to handle that
+ hard_assert (!source->schema);
+
+ config_item_free (self);
+ self->type = source->type;
+ memcpy (&self->value, &source->value, sizeof source->value);
+ free (source);
+}
+
+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);
+ hard_assert (utf8_validate
+ (self->value.string.str, self->value.string.len));
+ if (s) str_append_str (&self->value.string, s);
+ return self;
+}
+
+static struct config_item *
+config_item_string_from_cstr (const char *s)
+{
+ struct str tmp;
+ str_init (&tmp);
+ str_append (&tmp, s);
+ struct config_item *self = config_item_string (&tmp);
+ str_free (&tmp);
+ 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_OBJECT);
+ str_map_init (&self->value.object);
+ self->value.object.free = (void (*)(void *)) config_item_destroy;
+ return self;
+}
+
+static bool
+config_schema_accepts_type
+ (struct config_schema *self, enum config_item_type type)
+{
+ if (self->type == type)
+ return true;
+ // This is a bit messy but it has its purpose
+ if (config_item_type_is_string (self->type)
+ && config_item_type_is_string (type))
+ return true;
+ return !self->default_ && type == CONFIG_ITEM_NULL;
+}
+
+static bool
+config_item_validate_by_schema (struct config_item *self,
+ struct config_schema *schema, struct error **e)
+{
+ struct error *error = NULL;
+ if (!config_schema_accepts_type (schema, self->type))
+ error_set (e, "invalid type of value, expected: %s%s",
+ config_item_type_name (schema->type),
+ !schema->default_ ? " (or null)" : "");
+ else if (schema->validate && !schema->validate (self, &error))
+ {
+ error_set (e, "%s: %s", "invalid value", error->message);
+ error_free (error);
+ }
+ else
+ return true;
+ return false;
+}
+
+static bool
+config_item_set_from (struct config_item *self, struct config_item *source,
+ struct error **e)
+{
+ struct config_schema *schema = self->schema;
+ if (!schema)
+ {
+ // Easy, we don't know what this item is
+ config_item_move (self, source);
+ return true;
+ }
+
+ if (!config_item_validate_by_schema (source, schema, e))
+ return false;
+
+ // Make sure the string subtype fits the schema
+ if (config_item_type_is_string (source->type)
+ && config_item_type_is_string (schema->type))
+ source->type = schema->type;
+
+ config_item_move (self, source);
+
+ // Notify owner about the change so that they can apply it
+ if (schema->on_change)
+ schema->on_change (self);
+ return true;
+}
+
+static struct config_item *
+config_item_get (struct config_item *self, const char *path, struct error **e)
+{
+ hard_assert (self->type == CONFIG_ITEM_OBJECT);
+
+ struct str_vector v;
+ str_vector_init (&v);
+ cstr_split (path, ".", &v);
+
+ struct config_item *result = NULL;
+ size_t i = 0;
+ while (true)
+ {
+ const char *key = v.vector[i];
+ if (!*key)
+ error_set (e, "empty path element");
+ else if (!(self = str_map_find (&self->value.object, key)))
+ error_set (e, "`%s' not found in object", key);
+ else if (++i == v.len)
+ result = self;
+ else if (self->type != CONFIG_ITEM_OBJECT)
+ error_set (e, "`%s' is not an object", key);
+ else
+ continue;
+ break;
+ }
+ str_vector_free (&v);
+ return result;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct config_writer
+{
+ struct str *output;
+ unsigned indent;
+};
+
+static void config_item_write_object_innards
+ (struct config_writer *self, struct config_item *object);
+
+static void
+config_item_write_string (struct str *output, const struct str *s)
+{
+ str_append_c (output, '"');
+ for (size_t i = 0; i < s->len; i++)
+ {
+ unsigned char c = s->str[i];
+ if (c == '\n') str_append (output, "\\n");
+ else if (c == '\r') str_append (output, "\\r");
+ else if (c == '\t') str_append (output, "\\t");
+ else if (c == '\\') str_append (output, "\\\\");
+ else if (c == '"') str_append (output, "\\\"");
+ else if (c < 32) str_append_printf (output, "\\x%02x", c);
+ else str_append_c (output, c);
+ }
+ str_append_c (output, '"');
+}
+
+static void
+config_item_write_object
+ (struct config_writer *self, struct config_item *value)
+{
+ char indent[self->indent + 1];
+ memset (indent, '\t', self->indent);
+ indent[self->indent] = 0;
+
+ str_append_c (self->output, '{');
+ if (value->value.object.len)
+ {
+ self->indent++;
+ str_append_c (self->output, '\n');
+ config_item_write_object_innards (self, value);
+ self->indent--;
+ str_append (self->output, indent);
+ }
+ str_append_c (self->output, '}');
+}
+
+static void
+config_item_write_value (struct config_writer *self, struct config_item *value)
+{
+ switch (value->type)
+ {
+ case CONFIG_ITEM_NULL:
+ str_append (self->output, "null");
+ break;
+ case CONFIG_ITEM_BOOLEAN:
+ str_append (self->output, value->value.boolean ? "on" : "off");
+ break;
+ case CONFIG_ITEM_INTEGER:
+ str_append_printf (self->output, "%" PRIi64, value->value.integer);
+ break;
+ case CONFIG_ITEM_STRING:
+ case CONFIG_ITEM_STRING_ARRAY:
+ config_item_write_string (self->output, &value->value.string);
+ break;
+ case CONFIG_ITEM_OBJECT:
+ config_item_write_object (self, value);
+ break;
+ default:
+ hard_assert (!"invalid item type");
+ }
+}
+
+static void
+config_item_write_kv_pair (struct config_writer *self,
+ const char *key, struct config_item *value)
+{
+ char indent[self->indent + 1];
+ memset (indent, '\t', self->indent);
+ indent[self->indent] = 0;
+
+ if (value->schema && value->schema->comment)
+ str_append_printf (self->output,
+ "%s# %s\n", indent, value->schema->comment);
+
+ str_append_printf (self->output, "%s%s = ", indent, key);
+ config_item_write_value (self, value);
+ str_append_c (self->output, '\n');
+}
+
+static void
+config_item_write_object_innards
+ (struct config_writer *self, struct config_item *object)
+{
+ hard_assert (object->type == CONFIG_ITEM_OBJECT);
+
+ struct str_map_iter iter;
+ str_map_iter_init (&iter, &object->value.object);
+
+ struct config_item *value;
+ while ((value = str_map_iter_next (&iter)))
+ config_item_write_kv_pair (self, iter.link->key, value);
+}
+
+static void
+config_item_write (struct config_item *value,
+ bool object_innards, struct str *output)
+{
+ struct config_writer writer = { .output = output, .indent = 0 };
+ if (object_innards)
+ config_item_write_object_innards (&writer, value);
+ else
+ config_item_write_value (&writer, value);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+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}
+};
+
+static const char *
+config_token_name (enum config_token token)
+{
+ switch (token)
+ {
+ case CONFIG_T_ABORT: return "end of input";
+
+ case CONFIG_T_WORD: return "word";
+ case CONFIG_T_EQUALS: return "equal sign";
+ case CONFIG_T_LBRACE: return "left brace";
+ case CONFIG_T_RBRACE: return "right brace";
+ case CONFIG_T_NEWLINE: return "newline";
+
+ case CONFIG_T_NULL: return "null value";
+ case CONFIG_T_BOOLEAN: return "boolean";
+ case CONFIG_T_INTEGER: return "integer";
+ case CONFIG_T_STRING: return "string";
+
+ default:
+ hard_assert (!"invalid token value");
+ return NULL;
+ }
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct config_tokenizer
+{
+ const char *p; ///< Current position in input
+ size_t len; ///< How many bytes of input are left
+
+ bool report_line; ///< Whether to count lines at all
+ unsigned line; ///< Current line
+ unsigned column; ///< Current column
+
+ int64_t integer; ///< Parsed boolean or integer value
+ struct str string; ///< Parsed string value
+};
+
+/// 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;
+ self->report_line = true;
+ 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->report_line)
+ {
+ 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 *format, ...) ATTRIBUTE_PRINTF (3, 4);
+
+static void
+config_tokenizer_error (struct config_tokenizer *self,
+ struct error **e, const char *format, ...)
+{
+ struct str description;
+ str_init (&description);
+
+ va_list ap;
+ va_start (ap, format);
+ str_append_vprintf (&description, format, ap);
+ va_end (ap);
+
+ if (self->report_line)
+ error_set (e, "near line %u, column %u: %s",
+ self->line + 1, self->column + 1, description.str);
+ else if (self->len)
+ error_set (e, "near character %u: %s",
+ self->column + 1, description.str);
+ else
+ error_set (e, "near end: %s", description.str);
+
+ str_free (&description);
+}
+
+static bool
+config_tokenizer_hexa_escape (struct config_tokenizer *self, struct str *output)
+{
+ int i;
+ unsigned char code = 0;
+
+ for (i = 0; self->len && i < 2; i++)
+ {
+ unsigned char c = tolower_ascii (*self->p);
+ if (c >= '0' && c <= '9')
+ code = (code << 4) | (c - '0');
+ else if (c >= 'a' && c <= 'f')
+ code = (code << 4) | (c - 'a' + 10);
+ else
+ break;
+
+ config_tokenizer_advance (self);
+ }
+
+ if (!i)
+ return false;
+
+ str_append_c (output, code);
+ return true;
+}
+
+static bool
+config_tokenizer_octal_escape
+ (struct config_tokenizer *self, struct str *output)
+{
+ int i;
+ unsigned char code = 0;
+
+ for (i = 0; self->len && i < 3; i++)
+ {
+ unsigned char c = *self->p;
+ if (c >= '0' && c <= '7')
+ code = (code << 3) | (c - '0');
+ else
+ break;
+
+ config_tokenizer_advance (self);
+ }
+
+ if (!i)
+ return false;
+
+ str_append_c (output, code);
+ return true;
+}
+
+static bool
+config_tokenizer_escape_sequence
+ (struct config_tokenizer *self, struct str *output, struct error **e)
+{
+ if (!self->len)
+ {
+ config_tokenizer_error (self, e, "premature end of escape sequence");
+ return false;
+ }
+
+ unsigned char c;
+ switch ((c = *self->p))
+ {
+ case '"': break;
+ case '\\': break;
+ case 'a': c = '\a'; break;
+ case 'b': c = '\b'; break;
+ case 'f': c = '\f'; break;
+ case 'n': c = '\n'; break;
+ case 'r': c = '\r'; break;
+ case 't': c = '\t'; break;
+ case 'v': c = '\v'; break;
+
+ case 'x':
+ case 'X':
+ config_tokenizer_advance (self);
+ if (config_tokenizer_hexa_escape (self, output))
+ return true;
+
+ config_tokenizer_error (self, e, "invalid hexadecimal escape");
+ return false;
+
+ default:
+ if (config_tokenizer_octal_escape (self, output))
+ return true;
+
+ config_tokenizer_error (self, e, "unknown escape sequence");
+ return false;
+ }
+
+ str_append_c (output, c);
+ config_tokenizer_advance (self);
+ return true;
+}
+
+static bool
+config_tokenizer_string
+ (struct config_tokenizer *self, struct str *output, struct error **e)
+{
+ unsigned char c;
+ while (self->len)
+ {
+ if ((c = config_tokenizer_advance (self)) == '"')
+ return true;
+ if (c != '\\')
+ str_append_c (output, c);
+ else if (!config_tokenizer_escape_sequence (self, output, e))
+ return false;
+ }
+ config_tokenizer_error (self, e, "premature end of string");
+ return false;
+}
+
+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 '"':
+ config_tokenizer_advance (self);
+ str_reset (&self->string);
+ if (!config_tokenizer_string (self, &self->string, e))
+ return CONFIG_T_ABORT;
+ if (!utf8_validate (self->string.str, self->string.len))
+ {
+ config_tokenizer_error (self, e, "not a valid UTF-8 string");
+ return CONFIG_T_ABORT;
+ }
+ return CONFIG_T_STRING;
+ }
+
+ 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;
+ }
+
+ if (!config_tokenizer_is_word_char (*self->p))
+ {
+ config_tokenizer_error (self, e, "invalid input");
+ return CONFIG_T_ABORT;
+ }
+
+ str_reset (&self->string);
+ do
+ str_append_c (&self->string, config_tokenizer_advance (self));
+ while (config_tokenizer_is_word_char (*self->p));
+
+ 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;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+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;
+
+ config_tokenizer_error (&self->tokenizer, &self->error,
+ "unexpected `%s', expected `%s'",
+ config_token_name (self->token),
+ config_token_name (token));
+ 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);
+ SKIP_NL ();
+ 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);
+
+ config_tokenizer_error (&self->tokenizer, &self->error,
+ "unexpected `%s', expected a value",
+ config_token_name (self->token));
+ 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)
+ {
+ // This is really only intended for in-program configuration
+ // and telling the line number would look awkward
+ parser.tokenizer.report_line = false;
+ 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;
+}
+
+/// Clone an item. Schema assignments aren't retained.
+struct config_item *
+config_item_clone (struct config_item *self)
+{
+ // Oh well, it saves code
+ struct str tmp;
+ str_init (&tmp);
+ config_item_write (self, false, &tmp);
+ struct config_item *result =
+ config_item_parse (tmp.str, tmp.len, true, NULL);
+ str_free (&tmp);
+ return result;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static struct config_item *
+config_schema_initialize_item (struct config_schema *schema,
+ struct config_item *parent, struct error **warning, struct error **e)
+{
+ hard_assert (parent->type == CONFIG_ITEM_OBJECT);
+ struct config_item *item =
+ str_map_find (&parent->value.object, schema->name);
+
+ struct error *error = NULL;
+ if (item && config_item_validate_by_schema (item, schema, &error))
+ goto keep_current;
+
+ if (error)
+ {
+ error_set (warning, "resetting configuration item "
+ "`%s' to default: %s", schema->name, error->message);
+ error_free (error);
+ error = NULL;
+ }
+
+ if (schema->default_)
+ item = config_item_parse
+ (schema->default_, strlen (schema->default_), true, &error);
+ else
+ item = config_item_null ();
+
+ if (error || !config_item_validate_by_schema (item, schema, &error))
+ {
+ error_set (e, "invalid default for configuration item `%s': %s",
+ schema->name, error->message);
+ error_free (error);
+
+ config_item_destroy (item);
+ return NULL;
+ }
+
+ // This will free the old item if there was any
+ str_map_set (&parent->value.object, schema->name, item);
+
+keep_current:
+ // Make sure the string subtype fits the schema
+ if (config_item_type_is_string (item->type)
+ && config_item_type_is_string (schema->type))
+ item->type = schema->type;
+
+ item->schema = schema;
+ return item;
+}
+
+/// Assign schemas and user_data to multiple items at once;
+/// feel free to copy over and modify to suit your particular needs
+static void
+config_schema_apply_to_object (struct config_schema *schema_array,
+ struct config_item *object, void *user_data)
+{
+ while (schema_array->name)
+ {
+ struct error *warning = NULL, *e = NULL;
+ struct config_item *item = config_schema_initialize_item
+ (schema_array++, object, &warning, &e);
+
+ if (warning)
+ {
+ print_warning ("%s", warning->message);
+ error_free (warning);
+ }
+ if (e)
+ print_fatal ("%s", e->message);
+
+ item->user_data = user_data;
+ }
+}
+
+static void
+config_schema_call_changed (struct config_item *item)
+{
+ if (item->type == CONFIG_ITEM_OBJECT)
+ {
+ struct str_map_iter iter;
+ str_map_iter_init (&iter, &item->value.object);
+
+ struct config_item *child;
+ while ((child = str_map_iter_next (&iter)))
+ config_schema_call_changed (child);
+ }
+ else if (item->schema && item->schema->on_change)
+ item->schema->on_change (item);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// XXX: the callbacks may be overdesigned and of little to no practical use
+
+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
+};
+
+static void
+config_module_destroy (struct config_module *self)
+{
+ free (self->name);
+ free (self);
+}
+
+struct config
+{
+ struct str_map modules; ///< Toplevel modules
+ struct config_item *root; ///< CONFIG_ITEM_OBJECT
+};
+
+static void
+config_init (struct config *self)
+{
+ memset (self, 0, sizeof *self);
+ str_map_init (&self->modules);
+ self->modules.free = (str_map_free_fn) config_module_destroy;
+}
+
+static void
+config_free (struct config *self)
+{
+ str_map_free (&self->modules);
+ if (self->root)
+ config_item_destroy (self->root);
+}
+
+static void
+config_register_module (struct config *self,
+ const char *name, config_module_load_fn loader, void *user_data)
+{
+ struct config_module *module = xcalloc (1, sizeof *module);
+ module->name = xstrdup (name);
+ module->loader = loader;
+ module->user_data = user_data;
+
+ str_map_set (&self->modules, name, module);
+}
+
+static void
+config_load (struct config *self, struct config_item *root)
+{
+ hard_assert (root->type == CONFIG_ITEM_OBJECT);
+ if (self->root)
+ config_item_destroy (self->root);
+ self->root = root;
+
+ struct str_map_iter iter;
+ str_map_iter_init (&iter, &self->modules);
+
+ struct config_module *module;
+ while ((module = str_map_iter_next (&iter)))
+ {
+ struct config_item *subtree = str_map_find
+ (&root->value.object, module->name);
+ // Silently fix inputs that only a lunatic user could create
+ if (!subtree || subtree->type != CONFIG_ITEM_OBJECT)
+ {
+ subtree = config_item_object ();
+ str_map_set (&root->value.object, module->name, subtree);
+ }
+ if (module->loader)
+ module->loader (subtree, module->user_data);
+ }
+}
+
// --- Protocol modules --------------------------------------------------------
#include "liberty-proto.c"