Start writing a new configuration system
For degesch but in the long term for the rest as well.
This commit is contained in:
parent
452f42d96e
commit
6bf57d3450
596
common.c
596
common.c
@ -31,6 +31,7 @@
|
||||
#endif // WANT_SYSLOG_LOGGING
|
||||
|
||||
#include "liberty/liberty.c"
|
||||
#include <setjmp.h>
|
||||
#include <arpa/inet.h>
|
||||
|
||||
// --- 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
|
||||
}
|
||||
|
12
degesch.c
12
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 \
|
||||
|
Loading…
Reference in New Issue
Block a user