/* * 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 // --- 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 bool isalpha_ascii (int c) { c &= ~32; return c >= 'A' && c <= 'Z'; } static bool isdigit_ascii (int c) { return c >= '0' && c <= '9'; } static bool isalnum_ascii (int c) { return isalpha_ascii (c) || isdigit_ascii (c); } static int toupper_ascii (int c) { return c >= 'A' && c <= 'Z' ? c : c - ('a' - 'A'); } 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 bool is_nullable; ///< Can be null? const char *default_; ///< Default as a configuration snippet /// Check if the new value can be accepted. /// If this is not defined, only "type" and "is_nullable" is considered. bool (*validate) (struct config_item_ *, const struct config_item_ *); /// The value has changed. Only appliable to objects. void (*on_changed) (struct config_item_ *); /// Free any resources located in "item->user_data" void (*on_destroy) (struct config_item_ *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) { if (self->schema && self->schema->on_destroy) self->schema->on_destroy (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); if (s) str_append_str (&self->value.string, s); return self; } static struct config_item_ * config_item_string_array (const struct str *s) { struct config_item_ *self = config_item_string (s); self->type = CONFIG_ITEM_STRING_ARRAY; return self; } static struct config_item_ * config_item_object (void) { struct config_item_ *self = config_item_new (CONFIG_ITEM_BOOLEAN); str_map_init (&self->value.object); self->value.object.free = (void (*)(void *)) config_item_destroy; return self; } 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->is_nullable && type == CONFIG_ITEM_NULL; } 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; } // Otherwise we check the type and validate the item if (!config_schema_accepts_type (schema, source->type)) { error_set (e, "invalid type of value, expected: %s%s", config_item_type_name (schema->type), schema->is_nullable ? " (or null)" : ""); return false; } if (schema->validate && !schema->validate (self, source)) { // XXX: perhaps "schema->validate" could provide a message for us? error_set (e, "invalid value"); 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_changed) schema->on_changed (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_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: // TODO 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); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void config_schema_apply_to_object (struct config_schema *schema_array, struct config_item_ *object) { hard_assert (object->type == CONFIG_ITEM_OBJECT); // TODO } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - enum config_token { CONFIG_T_ABORT, ///< EOF or error CONFIG_T_WORD, ///< [a-zA-Z0-9_]+ CONFIG_T_EQUALS, ///< Equal sign CONFIG_T_LBRACE, ///< Left curly bracket CONFIG_T_RBRACE, ///< Right curly bracket CONFIG_T_NEWLINE, ///< New line CONFIG_T_NULL, ///< CONFIG_ITEM_NULL CONFIG_T_BOOLEAN, ///< CONFIG_ITEM_BOOLEAN CONFIG_T_INTEGER, ///< CONFIG_ITEM_INTEGER CONFIG_T_STRING ///< CONFIG_ITEM_STRING{,_LIST} }; 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 error_set (e, "near character %u: %s", self->column + 1, description.str); str_free (&description); } static enum config_token config_tokenizer_next (struct config_tokenizer *self, struct error **e) { // Skip over any whitespace between tokens while (self->len && isspace_ascii (*self->p) && *self->p != '\n') config_tokenizer_advance (self); if (!self->len) return CONFIG_T_ABORT; switch (*self->p) { case '\n': config_tokenizer_advance (self); return CONFIG_T_NEWLINE; case '=': config_tokenizer_advance (self); return CONFIG_T_EQUALS; case '{': config_tokenizer_advance (self); return CONFIG_T_LBRACE; case '}': config_tokenizer_advance (self); return CONFIG_T_RBRACE; case '#': // Comments go until newline while (self->len) if (config_tokenizer_advance (self) == '\n') return CONFIG_T_NEWLINE; return CONFIG_T_ABORT; case '"': // TODO: string, validate as UTF-8 break; } bool is_word = false; while (config_tokenizer_is_word_char (*self->p)) { is_word = true; str_reset (&self->string); str_append_c (&self->string, config_tokenizer_advance (self)); } if (is_word) { if (!strcmp (self->string.str, "null")) return CONFIG_T_NULL; bool boolean; if (!set_boolean_if_valid (&boolean, self->string.str)) return CONFIG_T_WORD; self->integer = boolean; return CONFIG_T_BOOLEAN; } char *end; errno = 0; self->integer = strtoll (self->p, &end, 10); if (errno == ERANGE) { config_tokenizer_error (self, e, "integer out of range"); return CONFIG_T_ABORT; } if (end != self->p) { self->len -= end - self->p; self->p = end; return CONFIG_T_INTEGER; } config_tokenizer_error (self, e, "invalid input"); return CONFIG_T_ABORT; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct config_parser { struct config_tokenizer tokenizer; ///< Tokenizer struct error *error; ///< Tokenizer error enum config_token token; ///< Current token in the tokenizer bool replace_token; ///< Replace the token }; static void config_parser_init (struct config_parser *self, const char *script, size_t len) { memset (self, 0, sizeof *self); config_tokenizer_init (&self->tokenizer, script, len); // As reading in tokens may cause exceptions, we wait for the first peek() // to replace the initial CONFIG_T_ABORT. self->replace_token = true; } static void config_parser_free (struct config_parser *self) { config_tokenizer_free (&self->tokenizer); if (self->error) error_free (self->error); } static enum config_token config_parser_peek (struct config_parser *self, jmp_buf out) { if (self->replace_token) { self->token = config_tokenizer_next (&self->tokenizer, &self->error); if (self->error) longjmp (out, 1); self->replace_token = false; } return self->token; } static bool config_parser_accept (struct config_parser *self, enum config_token token, jmp_buf out) { return self->replace_token = (config_parser_peek (self, out) == token); } static void config_parser_expect (struct config_parser *self, enum config_token token, jmp_buf out) { if (config_parser_accept (self, token, out)) return; 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; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - typedef void (*config_module_load_fn) (struct config_item_ *subtree, void *user_data); struct config_module { char *name; ///< Name of the subtree config_module_load_fn loader; ///< Module config subtree loader void *user_data; ///< User data }; struct config { struct str_map modules; ///< Toplevel modules struct config_item_ *root; ///< CONFIG_ITEM_OBJECT }; static void config_init (struct config *self) { // TODO } static void config_free (struct config *self) { // TODO } static bool config_register_module (const char *name, config_module_load_fn loader, void *user_data) { // TODO } static bool config_load (struct config_item_ *root, struct error **e) { // TODO }