1533 lines
39 KiB
C
1533 lines
39 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))
|
|
|
|
// --- 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);
|
|
}
|
|
|
|
// --- 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;
|
|
}
|
|
|
|
// --- 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);
|
|
}
|
|
|
|
// --- 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_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 (self->type)
|
|
&& config_item_type_is_string (source->type))
|
|
source->type = self->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_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:
|
|
{
|
|
char indent[self->indent + 1];
|
|
memset (indent, '\t', self->indent);
|
|
indent[self->indent] = 0;
|
|
|
|
str_append (self->output, "{\n");
|
|
|
|
self->indent++;
|
|
config_item_write_object_innards (self, value);
|
|
self->indent--;
|
|
|
|
str_append_printf (self->output, "%s}", indent);
|
|
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");
|
|
}
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
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;
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
// XXX: this thing is ugly in concept
|
|
|
|
static void
|
|
config_schema_fix_value
|
|
(struct config_schema *schema, struct config_item_ *object)
|
|
{
|
|
struct config_item_ *item =
|
|
str_map_find (&object->value.object, schema->name);
|
|
|
|
bool replace = true;
|
|
if (item)
|
|
{
|
|
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 (&object->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;
|
|
if (schema->on_change)
|
|
schema->on_change (item);
|
|
}
|
|
|
|
static void
|
|
config_schema_apply_to_object
|
|
(struct config_schema *schema_array, struct config_item_ *object)
|
|
{
|
|
hard_assert (object->type == CONFIG_ITEM_OBJECT);
|
|
while (schema_array->name)
|
|
config_schema_fix_value (schema_array++, object);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
// 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);
|
|
}
|
|
module->loader (subtree, module->user_data);
|
|
}
|
|
}
|