xK/common.c

1983 lines
50 KiB
C

/*
* common.c: common functionality
*
* Copyright (c) 2014 - 2015, Přemysl Janouch <p.janouch@gmail.com>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#define LIBERTY_WANT_SSL
#define LIBERTY_WANT_POLLER
#define LIBERTY_WANT_PROTO_IRC
#ifdef WANT_SYSLOG_LOGGING
#define print_fatal_data ((void *) LOG_ERR)
#define print_error_data ((void *) LOG_ERR)
#define print_warning_data ((void *) LOG_WARNING)
#define print_status_data ((void *) LOG_INFO)
#define print_debug_data ((void *) LOG_DEBUG)
#endif // WANT_SYSLOG_LOGGING
#include "liberty/liberty.c"
#include <setjmp.h>
#include <inttypes.h>
#include <arpa/inet.h>
/// Shorthand to set an error and return failure from the function
#define FAIL(...) \
BLOCK_START \
error_set (e, __VA_ARGS__); \
return false; \
BLOCK_END
// A few other debugging shorthands
#define LOG_FUNC_FAILURE(name, desc) \
print_debug ("%s: %s: %s", __func__, (name), (desc))
#define LOG_LIBC_FAILURE(name) \
print_debug ("%s: %s: %s", __func__, (name), strerror (errno))
// --- To be moved to liberty --------------------------------------------------
static void
split_str (const char *s, char delimiter, struct str_vector *out)
{
const char *begin = s, *end;
while ((end = strchr (begin, delimiter)))
{
str_vector_add_owned (out, xstrndup (begin, end - begin));
begin = ++end;
}
str_vector_add (out, begin);
}
static ssize_t
str_vector_find (const struct str_vector *v, const char *s)
{
for (size_t i = 0; i < v->len; i++)
if (!strcmp (v->vector[i], s))
return i;
return -1;
}
// --- Logging -----------------------------------------------------------------
static void
log_message_syslog (void *user_data, const char *quote, const char *fmt,
va_list ap)
{
int prio = (int) (intptr_t) user_data;
va_list va;
va_copy (va, ap);
int size = vsnprintf (NULL, 0, fmt, va);
va_end (va);
if (size < 0)
return;
char buf[size + 1];
if (vsnprintf (buf, sizeof buf, fmt, ap) >= 0)
syslog (prio, "%s%s", quote, buf);
}
// --- Connector ---------------------------------------------------------------
// This is a helper that tries to establish a connection with any address on
// a given list. Sadly it also introduces a bit of a callback hell.
struct connector_target
{
LIST_HEADER (struct connector_target)
char *hostname; ///< Target hostname or address
char *service; ///< Target service name or port
struct addrinfo *results; ///< Resolved target
struct addrinfo *iter; ///< Current endpoint
};
static struct connector_target *
connector_target_new (void)
{
struct connector_target *self = xmalloc (sizeof *self);
return self;
}
static void
connector_target_destroy (struct connector_target *self)
{
free (self->hostname);
free (self->service);
freeaddrinfo (self->results);
free (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct connector
{
int socket; ///< Socket FD for the connection
struct poller_fd connected_event; ///< We've connected or failed
struct connector_target *targets; ///< Targets
struct connector_target *targets_t; ///< Tail of targets
void *user_data; ///< User data for callbacks
// You may destroy the connector object in these two main callbacks:
/// Connection has been successfully established
void (*on_connected) (void *user_data, int socket);
/// Failed to establish a connection to either target
void (*on_failure) (void *user_data);
// Optional:
/// Connecting to a new address
void (*on_connecting) (void *user_data, const char *address);
/// Connecting to the last address has failed
void (*on_error) (void *user_data, const char *error);
};
static void
connector_notify_connecting (struct connector *self,
struct connector_target *target, struct addrinfo *gai_iter)
{
if (!self->on_connecting)
return;
const char *real_host = target->hostname;
// We don't really need this, so we can let it quietly fail
char buf[NI_MAXHOST];
int err = getnameinfo (gai_iter->ai_addr, gai_iter->ai_addrlen,
buf, sizeof buf, NULL, 0, NI_NUMERICHOST);
if (err)
LOG_FUNC_FAILURE ("getnameinfo", gai_strerror (err));
else
real_host = buf;
char *address = format_host_port_pair (real_host, target->service);
self->on_connecting (self->user_data, address);
free (address);
}
static void
connector_notify_error (struct connector *self, const char *error)
{
if (self->on_error)
self->on_error (self->user_data, error);
}
static void
connector_prepare_next (struct connector *self)
{
struct connector_target *target = self->targets;
if (!(target->iter = target->iter->ai_next))
{
LIST_UNLINK_WITH_TAIL (self->targets, self->targets_t, target);
connector_target_destroy (target);
}
}
static void
connector_step (struct connector *self)
{
struct connector_target *target = self->targets;
if (!target)
{
// Total failure, none of the targets has succeeded
self->on_failure (self->user_data);
return;
}
struct addrinfo *gai_iter = target->iter;
hard_assert (gai_iter != NULL);
connector_notify_connecting (self, target, gai_iter);
int fd = self->socket = socket (gai_iter->ai_family,
gai_iter->ai_socktype, gai_iter->ai_protocol);
if (fd == -1)
{
connector_notify_error (self, strerror (errno));
connector_prepare_next (self);
connector_step (self);
return;
}
set_cloexec (fd);
set_blocking (fd, false);
int yes = 1;
soft_assert (setsockopt (fd, SOL_SOCKET, SO_KEEPALIVE,
&yes, sizeof yes) != -1);
if (!connect (fd, gai_iter->ai_addr, gai_iter->ai_addrlen))
{
set_blocking (fd, true);
self->on_connected (self->user_data, fd);
return;
}
else if (errno != EINPROGRESS)
{
connector_notify_error (self, strerror (errno));
xclose (fd);
connector_prepare_next (self);
connector_step (self);
return;
}
self->connected_event.fd = self->socket = fd;
poller_fd_set (&self->connected_event, POLLOUT);
connector_prepare_next (self);
}
static void
connector_on_ready (const struct pollfd *pfd, struct connector *self)
{
// See http://cr.yp.to/docs/connect.html if this doesn't work.
// The second connect() method doesn't work with DragonflyBSD.
int error = 0;
socklen_t error_len = sizeof error;
hard_assert (!getsockopt (pfd->fd,
SOL_SOCKET, SO_ERROR, &error, &error_len));
if (error)
{
connector_notify_error (self, strerror (error));
poller_fd_reset (&self->connected_event);
xclose (self->socket);
self->socket = -1;
connector_step (self);
}
else
{
poller_fd_reset (&self->connected_event);
self->socket = -1;
set_blocking (pfd->fd, true);
self->on_connected (self->user_data, pfd->fd);
}
}
static void
connector_init (struct connector *self, struct poller *poller)
{
memset (self, 0, sizeof *self);
self->socket = -1;
poller_fd_init (&self->connected_event, poller, self->socket);
self->connected_event.user_data = self;
self->connected_event.dispatcher = (poller_fd_fn) connector_on_ready;
}
static void
connector_free (struct connector *self)
{
poller_fd_reset (&self->connected_event);
if (self->socket != -1)
xclose (self->socket);
LIST_FOR_EACH (struct connector_target, iter, self->targets)
connector_target_destroy (iter);
}
static bool
connector_add_target (struct connector *self,
const char *hostname, const char *service, struct error **e)
{
struct addrinfo hints, *results;
memset (&hints, 0, sizeof hints);
hints.ai_socktype = SOCK_STREAM;
// TODO: even this should be done asynchronously, most likely in
// a thread pool, similarly to how libuv does it
int err = getaddrinfo (hostname, service, &hints, &results);
if (err)
{
error_set (e, "%s: %s", "getaddrinfo", gai_strerror (err));
return false;
}
struct connector_target *target = connector_target_new ();
target->hostname = xstrdup (hostname);
target->service = xstrdup (service);
target->results = results;
target->iter = target->results;
LIST_APPEND_WITH_TAIL (self->targets, self->targets_t, target);
return true;
}
// --- SOCKS 5/4a (blocking implementation) ------------------------------------
// These are awkward protocols. Note that the `username' is used differently
// in SOCKS 4a and 5. In the former version, it is the username that you can
// get ident'ed against. In the latter version, it forms a pair with the
// password field and doesn't need to be an actual user on your machine.
// TODO: make a non-blocking poller-based version of this;
// either use c-ares or (even better) start another thread to do resolution
struct socks_addr
{
enum socks_addr_type
{
SOCKS_IPV4 = 1, ///< IPv4 address
SOCKS_DOMAIN = 3, ///< Domain name to be resolved
SOCKS_IPV6 = 4 ///< IPv6 address
}
type; ///< The type of this address
union
{
uint8_t ipv4[4]; ///< IPv4 address, network octet order
const char *domain; ///< Domain name
uint8_t ipv6[16]; ///< IPv6 address, network octet order
}
data; ///< The address itself
};
struct socks_data
{
struct socks_addr address; ///< Target address
uint16_t port; ///< Target port
const char *username; ///< Authentication username
const char *password; ///< Authentication password
struct socks_addr bound_address; ///< Bound address at the server
uint16_t bound_port; ///< Bound port at the server
};
static bool
socks_get_socket (struct addrinfo *addresses, int *fd, struct error **e)
{
int sockfd;
for (; addresses; addresses = addresses->ai_next)
{
sockfd = socket (addresses->ai_family,
addresses->ai_socktype, addresses->ai_protocol);
if (sockfd == -1)
continue;
set_cloexec (sockfd);
int yes = 1;
soft_assert (setsockopt (sockfd, SOL_SOCKET, SO_KEEPALIVE,
&yes, sizeof yes) != -1);
if (!connect (sockfd, addresses->ai_addr, addresses->ai_addrlen))
break;
xclose (sockfd);
}
if (!addresses)
{
error_set (e, "couldn't connect to the SOCKS server");
return false;
}
*fd = sockfd;
return true;
}
#define SOCKS_FAIL(...) \
BLOCK_START \
error_set (e, __VA_ARGS__); \
goto fail; \
BLOCK_END
#define SOCKS_RECV(buf, len) \
BLOCK_START \
if ((n = recv (sockfd, (buf), (len), 0)) == -1) \
SOCKS_FAIL ("%s: %s", "recv", strerror (errno)); \
if (n != (len)) \
SOCKS_FAIL ("%s: %s", "protocol error", "unexpected EOF"); \
BLOCK_END
static bool
socks_4a_connect (struct addrinfo *addresses, struct socks_data *data,
int *fd, struct error **e)
{
int sockfd;
if (!socks_get_socket (addresses, &sockfd, e))
return false;
const void *dest_ipv4 = "\x00\x00\x00\x01";
const char *dest_domain = NULL;
char buf[INET6_ADDRSTRLEN];
switch (data->address.type)
{
case SOCKS_IPV4:
dest_ipv4 = data->address.data.ipv4;
break;
case SOCKS_IPV6:
// About the best thing we can do, not sure if it works anywhere at all
if (!inet_ntop (AF_INET6, &data->address.data.ipv6, buf, sizeof buf))
SOCKS_FAIL ("%s: %s", "inet_ntop", strerror (errno));
dest_domain = buf;
break;
case SOCKS_DOMAIN:
dest_domain = data->address.data.domain;
}
struct str req;
str_init (&req);
str_append_c (&req, 4); // version
str_append_c (&req, 1); // connect
str_append_c (&req, data->port >> 8); // higher bits of port
str_append_c (&req, data->port); // lower bits of port
str_append_data (&req, dest_ipv4, 4); // destination address
if (data->username)
str_append (&req, data->username);
str_append_c (&req, '\0');
if (dest_domain)
{
str_append (&req, dest_domain);
str_append_c (&req, '\0');
}
ssize_t n = send (sockfd, req.str, req.len, 0);
str_free (&req);
if (n == -1)
SOCKS_FAIL ("%s: %s", "send", strerror (errno));
uint8_t resp[8];
SOCKS_RECV (resp, sizeof resp);
if (resp[0] != 0)
SOCKS_FAIL ("protocol error");
switch (resp[1])
{
case 90:
break;
case 91:
SOCKS_FAIL ("request rejected or failed");
case 92:
SOCKS_FAIL ("%s: %s", "request rejected",
"SOCKS server cannot connect to identd on the client");
case 93:
SOCKS_FAIL ("%s: %s", "request rejected",
"identd reports different user-id");
default:
SOCKS_FAIL ("protocol error");
}
*fd = sockfd;
return true;
fail:
xclose (sockfd);
return false;
}
#undef SOCKS_FAIL
#define SOCKS_FAIL(...) \
BLOCK_START \
error_set (e, __VA_ARGS__); \
return false; \
BLOCK_END
static bool
socks_5_userpass_auth (int sockfd, struct socks_data *data, struct error **e)
{
size_t ulen = strlen (data->username);
if (ulen > 255)
ulen = 255;
size_t plen = strlen (data->password);
if (plen > 255)
plen = 255;
uint8_t req[3 + ulen + plen], *p = req;
*p++ = 0x01; // version
*p++ = ulen; // username length
memcpy (p, data->username, ulen);
p += ulen;
*p++ = plen; // password length
memcpy (p, data->password, plen);
p += plen;
ssize_t n = send (sockfd, req, p - req, 0);
if (n == -1)
SOCKS_FAIL ("%s: %s", "send", strerror (errno));
uint8_t resp[2];
SOCKS_RECV (resp, sizeof resp);
if (resp[0] != 0x01)
SOCKS_FAIL ("protocol error");
if (resp[1] != 0x00)
SOCKS_FAIL ("authentication failure");
return true;
}
static bool
socks_5_auth (int sockfd, struct socks_data *data, struct error **e)
{
bool can_auth = data->username && data->password;
uint8_t hello[4];
hello[0] = 0x05; // version
hello[1] = 1 + can_auth; // number of authentication methods
hello[2] = 0x00; // no authentication required
hello[3] = 0x02; // username/password
ssize_t n = send (sockfd, hello, 3 + can_auth, 0);
if (n == -1)
SOCKS_FAIL ("%s: %s", "send", strerror (errno));
uint8_t resp[2];
SOCKS_RECV (resp, sizeof resp);
if (resp[0] != 0x05)
SOCKS_FAIL ("protocol error");
switch (resp[1])
{
case 0x02:
if (!can_auth)
SOCKS_FAIL ("protocol error");
if (!socks_5_userpass_auth (sockfd, data, e))
return false;
case 0x00:
break;
case 0xFF:
SOCKS_FAIL ("no acceptable authentication methods");
default:
SOCKS_FAIL ("protocol error");
}
return true;
}
static bool
socks_5_send_req (int sockfd, struct socks_data *data, struct error **e)
{
uint8_t req[4 + 256 + 2], *p = req;
*p++ = 0x05; // version
*p++ = 0x01; // connect
*p++ = 0x00; // reserved
*p++ = data->address.type;
switch (data->address.type)
{
case SOCKS_IPV4:
memcpy (p, data->address.data.ipv4, sizeof data->address.data.ipv4);
p += sizeof data->address.data.ipv4;
break;
case SOCKS_DOMAIN:
{
size_t dlen = strlen (data->address.data.domain);
if (dlen > 255)
dlen = 255;
*p++ = dlen;
memcpy (p, data->address.data.domain, dlen);
p += dlen;
break;
}
case SOCKS_IPV6:
memcpy (p, data->address.data.ipv6, sizeof data->address.data.ipv6);
p += sizeof data->address.data.ipv6;
break;
}
*p++ = data->port >> 8;
*p++ = data->port;
if (send (sockfd, req, p - req, 0) == -1)
SOCKS_FAIL ("%s: %s", "send", strerror (errno));
return true;
}
static bool
socks_5_process_resp (int sockfd, struct socks_data *data, struct error **e)
{
uint8_t resp_header[4];
ssize_t n;
SOCKS_RECV (resp_header, sizeof resp_header);
if (resp_header[0] != 0x05)
SOCKS_FAIL ("protocol error");
switch (resp_header[1])
{
case 0x00:
break;
case 0x01: SOCKS_FAIL ("general SOCKS server failure");
case 0x02: SOCKS_FAIL ("connection not allowed by ruleset");
case 0x03: SOCKS_FAIL ("network unreachable");
case 0x04: SOCKS_FAIL ("host unreachable");
case 0x05: SOCKS_FAIL ("connection refused");
case 0x06: SOCKS_FAIL ("TTL expired");
case 0x07: SOCKS_FAIL ("command not supported");
case 0x08: SOCKS_FAIL ("address type not supported");
default: SOCKS_FAIL ("protocol error");
}
switch ((data->bound_address.type = resp_header[3]))
{
case SOCKS_IPV4:
SOCKS_RECV (data->bound_address.data.ipv4,
sizeof data->bound_address.data.ipv4);
break;
case SOCKS_IPV6:
SOCKS_RECV (data->bound_address.data.ipv6,
sizeof data->bound_address.data.ipv6);
break;
case SOCKS_DOMAIN:
{
uint8_t len;
SOCKS_RECV (&len, sizeof len);
char domain[len + 1];
SOCKS_RECV (domain, len);
domain[len] = '\0';
data->bound_address.data.domain = xstrdup (domain);
break;
}
default:
SOCKS_FAIL ("protocol error");
}
uint16_t port;
SOCKS_RECV (&port, sizeof port);
data->bound_port = ntohs (port);
return true;
}
#undef SOCKS_FAIL
#undef SOCKS_RECV
static bool
socks_5_connect (struct addrinfo *addresses, struct socks_data *data,
int *fd, struct error **e)
{
int sockfd;
if (!socks_get_socket (addresses, &sockfd, e))
return false;
if (!socks_5_auth (sockfd, data, e)
|| !socks_5_send_req (sockfd, data, e)
|| !socks_5_process_resp (sockfd, data, e))
{
xclose (sockfd);
return false;
}
*fd = sockfd;
return true;
}
static int
socks_connect (const char *socks_host, const char *socks_port,
const char *host, const char *port,
const char *username, const char *password, struct error **e)
{
int result = -1;
struct addrinfo gai_hints, *gai_result;
memset (&gai_hints, 0, sizeof gai_hints);
gai_hints.ai_socktype = SOCK_STREAM;
unsigned long port_no;
const struct servent *serv;
if ((serv = getservbyname (port, "tcp")))
port_no = (uint16_t) ntohs (serv->s_port);
else if (!xstrtoul (&port_no, port, 10) || !port_no || port_no > UINT16_MAX)
{
error_set (e, "invalid port number");
goto fail;
}
int err = getaddrinfo (socks_host, socks_port, &gai_hints, &gai_result);
if (err)
{
error_set (e, "%s: %s", "getaddrinfo", gai_strerror (err));
goto fail;
}
struct socks_data data =
{ .username = username, .password = password, .port = port_no };
if (inet_pton (AF_INET, host, &data.address.data.ipv4) == 1)
data.address.type = SOCKS_IPV4;
else if (inet_pton (AF_INET6, host, &data.address.data.ipv6) == 1)
data.address.type = SOCKS_IPV6;
else
{
data.address.type = SOCKS_DOMAIN;
data.address.data.domain = host;
}
if (!socks_5_connect (gai_result, &data, &result, NULL))
socks_4a_connect (gai_result, &data, &result, e);
if (data.bound_address.type == SOCKS_DOMAIN)
free ((char *) data.bound_address.data.domain);
freeaddrinfo (gai_result);
fail:
return result;
}
// --- CTCP decoding -----------------------------------------------------------
#define CTCP_M_QUOTE '\020'
#define CTCP_X_DELIM '\001'
#define CTCP_X_QUOTE '\\'
struct ctcp_chunk
{
LIST_HEADER (struct ctcp_chunk)
bool is_extended; ///< Is this a tagged extended message?
struct str tag; ///< The tag, if any
struct str text; ///< Message contents
};
static struct ctcp_chunk *
ctcp_chunk_new (void)
{
struct ctcp_chunk *self = xcalloc (1, sizeof *self);
str_init (&self->tag);
str_init (&self->text);
return self;
}
static void
ctcp_chunk_destroy (struct ctcp_chunk *self)
{
str_free (&self->tag);
str_free (&self->text);
free (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
ctcp_low_level_decode (const char *message, struct str *output)
{
bool escape = false;
for (const char *p = message; *p; p++)
{
if (escape)
{
switch (*p)
{
case '0': str_append_c (output, '\0'); break;
case 'r': str_append_c (output, '\r'); break;
case 'n': str_append_c (output, '\n'); break;
default: str_append_c (output, *p);
}
escape = false;
}
else if (*p == CTCP_M_QUOTE)
escape = true;
else
str_append_c (output, *p);
}
}
static void
ctcp_intra_decode (const char *chunk, size_t len, struct str *output)
{
bool escape = false;
for (size_t i = 0; i < len; i++)
{
char c = chunk[i];
if (escape)
{
if (c == 'a')
str_append_c (output, CTCP_X_DELIM);
else
str_append_c (output, c);
escape = false;
}
else if (c == CTCP_X_QUOTE)
escape = true;
else
str_append_c (output, c);
}
}
static void
ctcp_parse_tagged (const char *chunk, size_t len, struct ctcp_chunk *output)
{
// We may search for the space before doing the higher level decoding,
// as it doesn't concern space characters at all
size_t tag_end = len;
for (size_t i = 0; i < len; i++)
if (chunk[i] == ' ')
{
tag_end = i;
break;
}
output->is_extended = true;
ctcp_intra_decode (chunk, tag_end, &output->tag);
if (tag_end++ != len)
ctcp_intra_decode (chunk + tag_end, len - tag_end, &output->text);
}
static struct ctcp_chunk *
ctcp_parse (const char *message)
{
struct str m;
str_init (&m);
ctcp_low_level_decode (message, &m);
struct ctcp_chunk *result = NULL, *result_tail = NULL;
size_t start = 0;
bool in_ctcp = false;
for (size_t i = 0; i < m.len; i++)
{
char c = m.str[i];
if (c != CTCP_X_DELIM)
continue;
// Remember the current state
size_t my_start = start;
bool my_is_ctcp = in_ctcp;
start = i + 1;
in_ctcp = !in_ctcp;
// Skip empty chunks
if (my_start == i)
continue;
struct ctcp_chunk *chunk = ctcp_chunk_new ();
if (my_is_ctcp)
ctcp_parse_tagged (m.str + my_start, i - my_start, chunk);
else
str_append_data (&chunk->text, m.str + my_start, i - my_start);
LIST_APPEND_WITH_TAIL (result, result_tail, chunk);
}
// Finish the last text part. We ignore unended tagged chunks.
// TODO: don't ignore them, e.g. a /me may get cut off
if (!in_ctcp && start != m.len)
{
struct ctcp_chunk *chunk = ctcp_chunk_new ();
// According to the original CTCP specification we should use
// ctcp_intra_decode() but no one seems to use that and it breaks
// normal text with backslashes
str_append_data (&chunk->text, m.str + start, m.len - start);
LIST_APPEND_WITH_TAIL (result, result_tail, chunk);
}
str_free (&m);
return result;
}
static void
ctcp_destroy (struct ctcp_chunk *list)
{
LIST_FOR_EACH (struct ctcp_chunk, iter, list)
ctcp_chunk_destroy (iter);
}
// --- 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
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);
split_str (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 void
config_schema_initialize_item (struct config_schema *schema,
struct config_item_ *parent, void *user_data)
{
struct config_item_ *item =
str_map_find (&parent->value.object, schema->name);
bool replace = true;
if (item)
{
// FIXME: either do this silently or tell about it via a callback
// or just store it in an output vector; don't print it directly
struct error *e = NULL;
replace = !config_item_validate_by_schema (item, schema, &e);
if (e)
{
print_error ("resetting configuration item "
"`%s' to default: %s", schema->name, e->message);
error_free (e);
}
}
if (replace)
{
struct error *e = NULL;
if (schema->default_)
item = config_item_parse
(schema->default_, strlen (schema->default_), true, &e);
else
item = config_item_null ();
if (e || !config_item_validate_by_schema (item, schema, &e))
exit_fatal ("invalid default for `%s': %s",
schema->name, e->message);
// This will free the old item if there was any
str_map_set (&parent->value.object, schema->name, item);
}
// 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;
item->user_data = user_data;
}
static void
config_schema_apply_to_object (struct config_schema *schema_array,
struct config_item_ *object, void *user_data)
{
hard_assert (object->type == CONFIG_ITEM_OBJECT);
while (schema_array->name)
config_schema_initialize_item (schema_array++, object, 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: this doesn't necessarily have to be well designed at all
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 struct config_module *
config_module_new ()
{
struct config_module *self = xcalloc (1, sizeof *self);
return self;
}
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 = (void (*) (void *)) 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 = config_module_new ();
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);
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);
}
}