/* * common.c: common functionality * * Copyright (c) 2014 - 2015, Přemysl Janouch * * 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 #include #include /// Shorthand to set an error and return failure from the function #define FAIL(...) \ BLOCK_START \ error_set (e, __VA_ARGS__); \ return 0; \ BLOCK_END #define CONTAINER_OF(pointer, type, member) \ (type *) ((char *) pointer - offsetof (type, member)) // --- To be moved to liberty -------------------------------------------------- static void split_str (const char *s, const char *delimiters, struct str_vector *out) { const char *begin = s, *end; while ((end = strpbrk (begin, delimiters))) { str_vector_add_owned (out, xstrndup (begin, end - begin)); begin = ++end; } str_vector_add (out, begin); } static 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); } // --- SOCKS 5/4a -------------------------------------------------------------- // Asynchronous SOCKS connector. Adds more stuff on top of the regular one. // 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. 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 char *domain; ///< Domain name uint8_t ipv6[16]; ///< IPv6 address, network octet order } data; ///< The address itself }; static void socks_addr_free (struct socks_addr *self) { if (self->type == SOCKS_DOMAIN) free (self->data.domain); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct socks_target { LIST_HEADER (struct socks_target) char *address_str; ///< Target address as a string struct socks_addr address; ///< Target address uint16_t port; ///< Target service port }; enum socks_protocol { SOCKS_5, ///< SOCKS5 SOCKS_4A, ///< SOCKS4A SOCKS_MAX ///< End of protocol }; static inline const char * socks_protocol_to_string (enum socks_protocol self) { switch (self) { case SOCKS_5: return "SOCKS5"; case SOCKS_4A: return "SOCKS4A"; default: return NULL; } } struct socks_connector { struct connector *connector; ///< Proxy server iterator (effectively) enum socks_protocol protocol_iter; ///< Protocol iterator struct socks_target *targets_iter; ///< Targets iterator // Negotiation: struct poller_timer timeout; ///< Timeout timer int socket_fd; ///< Current socket file descriptor struct poller_fd socket_event; ///< Socket can be read from/written to struct str read_buffer; ///< Read buffer struct str write_buffer; ///< Write buffer bool done; ///< Tunnel succesfully established uint8_t bound_address_len; ///< Length of domain name size_t data_needed; ///< How much data "on_data" needs /// Process incoming data if there's enough of it available bool (*on_data) (struct socks_connector *, struct msg_unpacker *); // Configuration: char *hostname; ///< SOCKS server hostname char *service; ///< SOCKS server service name or port char *username; ///< Username for authentication char *password; ///< Password for authentication struct socks_target *targets; ///< Targets struct socks_target *targets_tail; ///< Tail of targets void *user_data; ///< User data for callbacks // Additional results: struct socks_addr bound_address; ///< Bound address at the server uint16_t bound_port; ///< Bound port at the server // 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, const char *via, const char *version); /// Connecting to the last address has failed void (*on_error) (void *user_data, const char *error); }; // I've tried to make the actual protocol handlers as simple as possible #define SOCKS_FAIL(...) \ BLOCK_START \ char *error = xstrdup_printf (__VA_ARGS__); \ if (self->on_error) \ self->on_error (self->user_data, error); \ free (error); \ return false; \ BLOCK_END #define SOCKS_DATA_CB(name) static bool name \ (struct socks_connector *self, struct msg_unpacker *unpacker) #define SOCKS_GO(name, data_needed_) \ self->on_data = name; \ self->data_needed = data_needed_; \ return true // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - SOCKS_DATA_CB (socks_4a_finish) { uint8_t null, status; hard_assert (msg_unpacker_u8 (unpacker, &null)); hard_assert (msg_unpacker_u8 (unpacker, &status)); if (null != 0) SOCKS_FAIL ("protocol error"); switch (status) { case 90: self->done = true; return false; 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"); } } static bool socks_4a_start (struct socks_connector *self) { struct socks_target *target = self->targets_iter; const void *dest_ipv4 = "\x00\x00\x00\x01"; const char *dest_domain = NULL; char buf[INET6_ADDRSTRLEN]; switch (target->address.type) { case SOCKS_IPV4: dest_ipv4 = target->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, &target->address.data.ipv6, buf, sizeof buf)) SOCKS_FAIL ("%s: %s", "inet_ntop", strerror (errno)); dest_domain = buf; break; case SOCKS_DOMAIN: dest_domain = target->address.data.domain; } struct str *wb = &self->write_buffer; str_init (wb); str_pack_u8 (wb, 4); // version str_pack_u8 (wb, 1); // connect str_pack_u16 (wb, target->port); // port str_append_data (wb, dest_ipv4, 4); // destination address if (self->username) str_append (wb, self->username); str_append_c (wb, '\0'); if (dest_domain) { str_append (wb, dest_domain); str_append_c (wb, '\0'); } SOCKS_GO (socks_4a_finish, 8); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - SOCKS_DATA_CB (socks_5_request_port) { hard_assert (msg_unpacker_u16 (unpacker, &self->bound_port)); self->done = true; return false; } SOCKS_DATA_CB (socks_5_request_ipv4) { memcpy (self->bound_address.data.ipv4, unpacker->data, unpacker->len); SOCKS_GO (socks_5_request_port, 2); } SOCKS_DATA_CB (socks_5_request_ipv6) { memcpy (self->bound_address.data.ipv6, unpacker->data, unpacker->len); SOCKS_GO (socks_5_request_port, 2); } SOCKS_DATA_CB (socks_5_request_domain_data) { self->bound_address.data.domain = xstrndup (unpacker->data, unpacker->len); SOCKS_GO (socks_5_request_port, 2); } SOCKS_DATA_CB (socks_5_request_domain) { hard_assert (msg_unpacker_u8 (unpacker, &self->bound_address_len)); SOCKS_GO (socks_5_request_domain_data, self->bound_address_len); } SOCKS_DATA_CB (socks_5_request_finish) { uint8_t version, status, reserved, type; hard_assert (msg_unpacker_u8 (unpacker, &version)); hard_assert (msg_unpacker_u8 (unpacker, &status)); hard_assert (msg_unpacker_u8 (unpacker, &reserved)); hard_assert (msg_unpacker_u8 (unpacker, &type)); if (version != 0x05) SOCKS_FAIL ("protocol error"); switch (status) { 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 ((self->bound_address.type = type)) { case SOCKS_IPV4: SOCKS_GO (socks_5_request_ipv4, sizeof self->bound_address.data.ipv4); case SOCKS_IPV6: SOCKS_GO (socks_5_request_ipv6, sizeof self->bound_address.data.ipv6); case SOCKS_DOMAIN: SOCKS_GO (socks_5_request_domain, 1); default: SOCKS_FAIL ("protocol error"); } } static bool socks_5_request_start (struct socks_connector *self) { struct socks_target *target = self->targets_iter; struct str *wb = &self->write_buffer; str_pack_u8 (wb, 0x05); // version str_pack_u8 (wb, 0x01); // connect str_pack_u8 (wb, 0x00); // reserved str_pack_u8 (wb, target->address.type); switch (target->address.type) { case SOCKS_IPV4: str_append_data (wb, target->address.data.ipv4, sizeof target->address.data.ipv4); break; case SOCKS_DOMAIN: { size_t dlen = strlen (target->address.data.domain); if (dlen > 255) dlen = 255; str_pack_u8 (wb, dlen); str_append_data (wb, target->address.data.domain, dlen); break; } case SOCKS_IPV6: str_append_data (wb, target->address.data.ipv6, sizeof target->address.data.ipv6); break; } str_pack_u16 (wb, target->port); SOCKS_GO (socks_5_request_finish, 4); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - SOCKS_DATA_CB (socks_5_userpass_finish) { uint8_t version, status; hard_assert (msg_unpacker_u8 (unpacker, &version)); hard_assert (msg_unpacker_u8 (unpacker, &status)); if (version != 0x01) SOCKS_FAIL ("protocol error"); if (status != 0x00) SOCKS_FAIL ("authentication failure"); return socks_5_request_start (self); } static bool socks_5_userpass_start (struct socks_connector *self) { size_t ulen = strlen (self->username); if (ulen > 255) ulen = 255; size_t plen = strlen (self->password); if (plen > 255) plen = 255; struct str *wb = &self->write_buffer; str_pack_u8 (wb, 0x01); // version str_pack_u8 (wb, ulen); // username length str_append_data (wb, self->username, ulen); str_pack_u8 (wb, plen); // password length str_append_data (wb, self->password, plen); SOCKS_GO (socks_5_userpass_finish, 2); } SOCKS_DATA_CB (socks_5_auth_finish) { uint8_t version, method; hard_assert (msg_unpacker_u8 (unpacker, &version)); hard_assert (msg_unpacker_u8 (unpacker, &method)); if (version != 0x05) SOCKS_FAIL ("protocol error"); bool can_auth = self->username && self->password; switch (method) { case 0x02: if (!can_auth) SOCKS_FAIL ("protocol error"); return socks_5_userpass_start (self); case 0x00: return socks_5_request_start (self); case 0xFF: SOCKS_FAIL ("no acceptable authentication methods"); default: SOCKS_FAIL ("protocol error"); } } static bool socks_5_auth_start (struct socks_connector *self) { bool can_auth = self->username && self->password; struct str *wb = &self->write_buffer; str_pack_u8 (wb, 0x05); // version str_pack_u8 (wb, 1 + can_auth); // number of authentication methods str_pack_u8 (wb, 0x00); // no authentication required if (can_auth) str_pack_u8 (wb, 0x02); // username/password SOCKS_GO (socks_5_auth_finish, 2); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void socks_connector_start (struct socks_connector *self); static void socks_connector_destroy_connector (struct socks_connector *self) { if (self->connector) { connector_free (self->connector); free (self->connector); self->connector = NULL; } } static void socks_connector_cancel_events (struct socks_connector *self) { // Before calling the final callbacks, we should cancel events that // could potentially fire; caller should destroy us immediately, though poller_fd_reset (&self->socket_event); poller_timer_reset (&self->timeout); } static void socks_connector_fail (struct socks_connector *self) { socks_connector_cancel_events (self); self->on_failure (self->user_data); } static bool socks_connector_step_iterators (struct socks_connector *self) { // At the lowest level we iterate over all addresses for the SOCKS server // and just try to connect; this is done automatically by the connector // Then we iterate over available protocols if (++self->protocol_iter != SOCKS_MAX) return true; // At the highest level we iterate over possible targets self->protocol_iter = 0; if (self->targets_iter && (self->targets_iter = self->targets_iter->next)) return true; return false; } static void socks_connector_step (struct socks_connector *self) { if (self->socket_fd != -1) { poller_fd_reset (&self->socket_event); xclose (self->socket_fd); self->socket_fd = -1; } socks_connector_destroy_connector (self); if (socks_connector_step_iterators (self)) socks_connector_start (self); else socks_connector_fail (self); } static void socks_connector_on_timeout (struct socks_connector *self) { if (self->on_error) self->on_error (self->user_data, "timeout"); socks_connector_destroy_connector (self); socks_connector_fail (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void socks_connector_on_connected (void *user_data, int socket_fd) { set_blocking (socket_fd, false); struct socks_connector *self = user_data; self->socket_fd = socket_fd; self->socket_event.fd = socket_fd; poller_fd_set (&self->socket_event, POLLIN | POLLOUT); str_reset (&self->read_buffer); str_reset (&self->write_buffer); if (!(self->protocol_iter == SOCKS_5 && socks_5_auth_start (self)) && !(self->protocol_iter == SOCKS_4A && socks_4a_start (self))) socks_connector_fail (self); } static void socks_connector_on_failure (void *user_data) { struct socks_connector *self = user_data; // TODO: skip SOCKS server on connection failure socks_connector_step (self); } static void socks_connector_on_connecting (void *user_data, const char *via) { struct socks_connector *self = user_data; if (!self->on_connecting) return; struct socks_target *target = self->targets_iter; char *port = xstrdup_printf ("%u", target->port); char *address = format_host_port_pair (target->address_str, port); free (port); self->on_connecting (self->user_data, address, via, socks_protocol_to_string (self->protocol_iter)); free (address); } static void socks_connector_on_error (void *user_data, const char *error) { struct socks_connector *self = user_data; // TODO: skip protocol on protocol failure if (self->on_error) self->on_error (self->user_data, error); } static void socks_connector_start (struct socks_connector *self) { hard_assert (!self->connector); struct connector *connector = self->connector = xcalloc (1, sizeof *connector); connector_init (connector, self->socket_event.poller); connector->user_data = self; connector->on_connected = socks_connector_on_connected; connector->on_connecting = socks_connector_on_connecting; connector->on_error = socks_connector_on_error; connector->on_failure = socks_connector_on_failure; struct error *e = NULL; if (!connector_add_target (connector, self->hostname, self->service, &e)) { if (self->on_error) self->on_error (self->user_data, e->message); error_free (e); socks_connector_destroy_connector (self); socks_connector_fail (self); return; } poller_timer_set (&self->timeout, 60 * 1000); connector_step (connector); self->done = false; self->bound_port = 0; socks_addr_free (&self->bound_address); memset (&self->bound_address, 0, sizeof self->bound_address); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static bool socks_try_fill_read_buffer (struct socks_connector *self, size_t n) { ssize_t remains = (ssize_t) n - (ssize_t) self->read_buffer.len; if (remains <= 0) return true; ssize_t received; str_ensure_space (&self->read_buffer, remains); do received = recv (self->socket_fd, self->read_buffer.str + self->read_buffer.len, remains, 0); while ((received == -1) && errno == EINTR); if (received == 0) SOCKS_FAIL ("%s: %s", "protocol error", "unexpected EOF"); if (received == -1 && errno != EAGAIN) SOCKS_FAIL ("%s: %s", "recv", strerror (errno)); if (received > 0) self->read_buffer.len += received; return true; } static bool socks_call_on_data (struct socks_connector *self) { size_t to_consume = self->data_needed; if (!socks_try_fill_read_buffer (self, to_consume)) return false; if (self->read_buffer.len < to_consume) return true; struct msg_unpacker unpacker; msg_unpacker_init (&unpacker, self->read_buffer.str, self->read_buffer.len); bool result = self->on_data (self, &unpacker); str_remove_slice (&self->read_buffer, 0, to_consume); return result; } static bool socks_try_flush_write_buffer (struct socks_connector *self) { struct str *wb = &self->write_buffer; ssize_t n_written; while (wb->len) { n_written = send (self->socket_fd, wb->str, wb->len, 0); if (n_written >= 0) { str_remove_slice (wb, 0, n_written); continue; } if (errno == EAGAIN) break; if (errno == EINTR) continue; SOCKS_FAIL ("%s: %s", "send", strerror (errno)); } return true; } static void socks_connector_on_ready (const struct pollfd *pfd, struct socks_connector *self) { (void) pfd; if (socks_call_on_data (self) && socks_try_flush_write_buffer (self)) { poller_fd_set (&self->socket_event, self->write_buffer.len ? (POLLIN | POLLOUT) : POLLIN); } else if (self->done) { socks_connector_cancel_events (self); int fd = self->socket_fd; self->socket_fd = -1; set_blocking (fd, true); self->on_connected (self->user_data, fd); } else // We've failed this target, let's try to move on socks_connector_step (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void socks_connector_init (struct socks_connector *self, struct poller *poller) { memset (self, 0, sizeof *self); poller_fd_init (&self->socket_event, poller, (self->socket_fd = -1)); self->socket_event.dispatcher = (poller_fd_fn) socks_connector_on_ready; self->socket_event.user_data = self; poller_timer_init (&self->timeout, poller); self->timeout.dispatcher = (poller_timer_fn) socks_connector_on_timeout; self->timeout.user_data = self; str_init (&self->read_buffer); str_init (&self->write_buffer); } static void socks_connector_free (struct socks_connector *self) { socks_connector_destroy_connector (self); socks_connector_cancel_events (self); if (self->socket_fd != -1) xclose (self->socket_fd); str_free (&self->read_buffer); str_free (&self->write_buffer); free (self->hostname); free (self->service); free (self->username); free (self->password); LIST_FOR_EACH (struct socks_target, iter, self->targets) { socks_addr_free (&iter->address); free (iter->address_str); free (iter); } socks_addr_free (&self->bound_address); } static bool socks_connector_add_target (struct socks_connector *self, const char *host, const char *service, struct error **e) { unsigned long port; const struct servent *serv; if ((serv = getservbyname (service, "tcp"))) port = (uint16_t) ntohs (serv->s_port); else if (!xstrtoul (&port, service, 10) || !port || port > UINT16_MAX) { error_set (e, "invalid port number"); return false; } struct socks_target *target = xcalloc (1, sizeof *target); if (inet_pton (AF_INET, host, &target->address.data.ipv4) == 1) target->address.type = SOCKS_IPV4; else if (inet_pton (AF_INET6, host, &target->address.data.ipv6) == 1) target->address.type = SOCKS_IPV6; else { target->address.type = SOCKS_DOMAIN; target->address.data.domain = xstrdup (host); } target->port = port; target->address_str = xstrdup (host); LIST_APPEND_WITH_TAIL (self->targets, self->targets_tail, target); return true; } static void socks_connector_run (struct socks_connector *self, const char *host, const char *service, const char *username, const char *password) { hard_assert (self->targets); hard_assert (host && service); self->hostname = xstrdup (host); self->service = xstrdup (service); if (username) self->username = xstrdup (username); if (password) self->password = xstrdup (password); self->targets_iter = self->targets; self->protocol_iter = 0; // XXX: this can fail immediately from an error creating the connector socks_connector_start (self); } // --- 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? bool is_partial; ///< Unterminated 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; // According to the original CTCP specification we should use // ctcp_intra_decode() on all parts, however no one seems to // use that and it breaks normal text with backslashes 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 part. Unended tagged chunks are marked as such. if (start != m.len) { struct ctcp_chunk *chunk = ctcp_chunk_new (); if (in_ctcp) { ctcp_parse_tagged (m.str + start, m.len - start, chunk); chunk->is_partial = true; } else 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); } }