Wed, 27 Nov 2024 23:00:07 +0100
add TODO to use a future ucx feature
/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2020 Olaf Wintermann. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include "serverconfig.h" #include "conf.h" #include "logging.h" #include <stdio.h> #include <stdlib.h> #include <string.h> #include <ctype.h> #include <errno.h> #include <cx/buffer.h> #include <cx/utils.h> ServerConfig* serverconfig_load(const char *file) { CxMempool *mp = cxBasicMempoolCreate(512); if(!mp) { return NULL; } ConfigParser2 parser; memset(&parser, 0, sizeof(ConfigParser2)); parser.mp = mp; parser.filename = file; parser.allow_hierarchy = true; parser.delim = ""; ConfigNode *root = serverconfig_load_file(&parser, file); if(!root) { cxMempoolDestroy(mp); return NULL; } ServerConfig *scfg = cxMalloc(mp->allocator, sizeof(ServerConfig)); if(!scfg) { cxMempoolDestroy(mp); return NULL; } scfg->root = root; scfg->mp = mp; scfg->tab = cx_str("\t"); return scfg; } ConfigNode* serverconfig_load_file(ConfigParser2 *parser, const char *file) { FILE *in = fopen(file, "r"); if(in == NULL) { parser->error = CONFIG_PARSER_IO_ERROR; parser->io_errno = errno; return NULL; } // temporary buffer to store the file content CxBuffer buf; if(cxBufferInit(&buf, NULL, 16384, cxDefaultAllocator, CX_BUFFER_AUTO_EXTEND|CX_BUFFER_FREE_CONTENTS)) { fclose(in); parser->error = CONFIG_PARSER_OOM; return NULL; } //ucx_stream_copy(in, buf, (read_func)fread, (write_func)ucx_buffer_write); char readbuf[2048]; size_t r; while((r = fread(readbuf, 1, 2048, in)) > 0) { cxBufferWrite(readbuf, 1, r, &buf); } fclose(in); ConfigNode *root = serverconfig_parse(parser, cx_strn(buf.space, buf.size)); cxBufferDestroy(&buf); return root; } static int scfg_char_is_delim(ConfigParser2 *parser, char c) { size_t len = strlen(parser->delim); for(int i=0;i<len;i++) { if(c == parser->delim[i]) { return 1; } } return 0; } static CFGToken get_next_token(ConfigParser2 *parser, cxstring content, int *pos) { CFGToken token = { {NULL, 0}, CFG_NO_TOKEN }; CFGTokenType type = CFG_TOKEN; int start = *pos; int token_begin = -1; int token_end = content.length-1; int quote = 0; int comment = 0; int i; char prev = 0; for(i=start;i<content.length;i++) { char c = content.ptr[i]; if(c == '\n') { if(quote) { *pos = i; ws_cfg_log( LOG_FAILURE, "cfgparser: file %s:%d:%d: error: %s", parser->filename, parser->linenum, parser->linepos+2, "missing '\"' character before end of line"); return token; // error } else if(start == i) { // single newline char token type = CFG_TOKEN_NEWLINE; token_begin = i; token_end = i+1; parser->linenum++; parser->linepos = 0; break; } token_end = i; if(token_begin < 0) { // only space/comment token token_begin = start; type = comment ? CFG_TOKEN_COMMENT : CFG_TOKEN_SPACE; } // make sure next run will return current newline char as token i--; break; } else if(quote) { if(c == '"' && prev != '\\') { quote = 0; } } else if(comment) { // ignore if(c == '\n') { comment = 0; } } else if(c == '#') { comment = 1; } else if(isspace(c)) { if(token_begin >= 0) { token_end = i; break; } } else if(c == '"') { quote = 1; if(token_begin < 0) { token_begin = i; } } else if(scfg_char_is_delim(parser, c)) { if(token_begin >= 0) { token_end = i; i--; break; } else { token_begin = i; token_end = i+1; break; } } else if(token_begin < 0) { token_begin = i; } prev = c; parser->linepos++; } *pos = i + 1; if(token_begin < 0) { return token; // error or EOF } token.type = type; token.content = cx_strsubsl(content, token_begin, token_end - token_begin); return token; } /* static void test_print_config(ConfigNode *parent) { UCX_FOREACH(elm, parent->children) { ConfigNode *node = elm->data; if(node->type == CONFIG_NODE_SPACE) { printf("sp: %s", node->text_begin.ptr); } else if(node->type == CONFIG_NODE_COMMENT) { printf("cm: %s", node->text_begin.ptr); } else if(node->type == CONFIG_NODE_OBJECT) { printf("o{: %s : %s", node->name.ptr, node->text_begin.ptr); test_print_config(node); printf("o}: %s", node->text_end.ptr); } else if(node->type == CONFIG_NODE_DIRECTIVE) { printf("di: %s", node->text_begin.ptr); } else { printf("fk: %s", node->text_begin.ptr); } } } */ static void config_arg_set_value(CxAllocator *a, ConfigParam *arg, CFGToken token) { cxstring nv = (cxstring){NULL,0}; WSBool quotes = token.content.ptr[0] == '\"'; if(!quotes) { nv = cx_strchr(token.content, '='); } if(!nv.ptr) { if(quotes) { // remove quote token.content.ptr++; token.content.length -= 2; } arg->value = cx_strdup_a(a, token.content); } else { intptr_t eq = (intptr_t)(nv.ptr - token.content.ptr); cxstring name = token.content; name.length = (size_t)eq; cxstring value = nv; value.ptr++; value.length--; if(value.length > 1 && value.ptr[0] == '"' && value.ptr[value.length-1] == '"') { value.ptr++; value.length -= 2; // remove quote } arg->name = cx_strdup_a(a, name); arg->value = cx_strdup_a(a, value); } } static int nodestack_prepend(CxAllocator *a, ConfigNodeStack **stack, ConfigNode *node) { ConfigNodeStack *elm = cxMalloc(a, sizeof(ConfigNodeStack)); if(!elm) return 1; elm->node = node; elm->next = NULL; cx_linked_list_prepend((void**)stack, NULL, -1, offsetof(ConfigNodeStack, next), elm); return 0; } ConfigNode* serverconfig_parse(ConfigParser2 *parser, cxstring content) { CxMempool *mp = parser->mp; CxAllocator *a = (CxAllocator*)mp->allocator; parser->linenum = 1; parser->linepos = 1; if(!parser->delim) { parser->delim = ""; } // PARSE: // first non space/comment token is directive/object name // following tokens are arguments // newline starts new directive // '{' converts directive to object and following directives will // be placed into the object int pos = 0; // needed for tokenizer CFGToken token; ConfigNode *root_obj = cxCalloc(a, 1, sizeof(ConfigNode)); root_obj->type = CONFIG_NODE_OBJECT; ConfigNodeStack *node_stack = cxMalloc(a, sizeof(ConfigNodeStack)); node_stack->node = root_obj; node_stack->next = NULL; ConfigNode *current = cxCalloc(a, 1, sizeof(ConfigNode)); current->type = CONFIG_NODE_SPACE; ConfigNode *obj = NULL; int obj_closed = 0; int text_start = 0; int err = 0; while((token = get_next_token(parser, content, &pos)).type != CFG_NO_TOKEN) { //printf("[%.*s]\n", (int)token.content.length, token.content.ptr); fflush(stdout); switch(token.type) { case CFG_NO_TOKEN: break; case CFG_TOKEN_COMMENT: { if(current->type == CONFIG_NODE_SPACE) { current->type = CONFIG_NODE_COMMENT; } break; } case CFG_TOKEN_SPACE: break; case CFG_TOKEN_NEWLINE: { cxstring line = cx_strsubsl(content, text_start, pos - text_start); text_start = pos; cxmutstr line_cp = cx_strdup_a(a, line); ConfigNode *parent = node_stack->node; if(current->type == CONFIG_NODE_CLOSE_OBJECT) { // this is a newline after a object is closed with '}' // the line containing "}\n" should be added to the object parent->text_end = line_cp; // validate if(parser->validateObjEnd && parser->validateObjEnd(parser, parent)) { // validation failed err = 1; break; } // done with this object, remove it from the stack ConfigNodeStack *remove_item = node_stack; node_stack = node_stack->next; cxFree(a, remove_item); } else if(current->type == CONFIG_NODE_OPEN_OBJECT) { // newline after a object is opened with '{' // append '{' to the object text cxmutstr new_textbegin = cx_strcat_a(a, 2, obj->text_begin, line_cp); cxFree(a, obj->text_begin.ptr); cxFree(a, line_cp.ptr); obj->text_begin = new_textbegin; } else { // normal line containing a directive, space or comment // add it to parent node current->text_begin = line_cp; CFG_NODE_ADD(&parent->children_begin, &parent->children_end, current); // validate after CFG_NODE_ADD, because now current->prev is set if(current->type == CONFIG_NODE_DIRECTIVE && parser->validateDirective && parser->validateDirective(parser, current)) { err = 1; break; } } // obj points to the previous node that started as a directive // the type is set to CONFIG_NODE_OBECT if it was followed by // a '{' character if(obj && obj->type == CONFIG_NODE_OBJECT) { // new object started if(parser->validateObjBegin && parser->validateObjBegin(parser, obj)) { // validation callback failed err = 1; break; } // add it to the stack nodestack_prepend(a, &node_stack, obj); obj = NULL; } current = cxCalloc(a, 1, sizeof(ConfigNode)); current->type = CONFIG_NODE_SPACE; obj_closed = 0; break; } case CFG_TOKEN: { // normal text token // either a directive/obj name, parameter or { } if(!cx_strcmp(token.content, cx_str("{"))) { // check if the parser allows an object hierarchy if(!parser->allow_hierarchy) { parser->error = CONFIG_PARSER_SYNTAX_ERROR; err = 1; break; } // obj is pointing to the previous node that started // a directive if(!obj) { err = 1; break; } obj->type = CONFIG_NODE_OBJECT; if(current != obj) { current->type = CONFIG_NODE_OPEN_OBJECT; } } else if(!cx_strcmp(token.content, cx_str("}"))) { // check if the parser allows an object hierarchy if(!parser->allow_hierarchy) { parser->error = CONFIG_PARSER_SYNTAX_ERROR; err = 1; break; } obj_closed = 1; // force newline before next directive obj = NULL; current->type = CONFIG_NODE_CLOSE_OBJECT; } else { if(obj_closed) { err = 1; break; } if(!current->name.ptr) { // currently this could be a directive or object current->name = cx_strdup_a(a, token.content); current->type = CONFIG_NODE_DIRECTIVE; obj = current; // potential object } else { // name already set, therefore this token must // be a parameter ConfigParam *arg = cxCalloc(a, 1, sizeof(ConfigParam)); config_arg_set_value(a, arg, token); CFG_PARAM_ADD(¤t->args, NULL, arg); } } break; } } if(err) { break; } } if(pos < content.length || err) { // content not fully parsed because of an error return NULL; } //test_print_config(&root_obj); return root_obj; } void serverconfig_free(ServerConfig *cfg) { cxMempoolDestroy(cfg->mp); } ConfigNode* serverconfig_get_node(ConfigNode *parent, ConfigNodeType type, cxstring name) { for(ConfigNode *node=parent->children_begin;node;node=node->next) { if(node->type == type && !cx_strcasecmp(cx_strcast(node->name), name)) { return node; } } return NULL; } CxList* serverconfig_get_node_list(ConfigNode *parent, ConfigNodeType type, cxstring name) { CxList *nodes = cxLinkedListCreate(cxDefaultAllocator, NULL, CX_STORE_POINTERS); for(ConfigNode *node=parent->children_begin;node;node=node->next) { if(node->type == type && !cx_strcasecmp(cx_strcast(node->name), name)) { cxListAdd(nodes, node); } } return nodes; } cxstring serverconfig_object_directive_value(ConfigNode *obj, cxstring name) { ConfigNode *node = serverconfig_get_node(obj, CONFIG_NODE_DIRECTIVE, name); if(node && CFG_NUM_PARAMS(node->args) == 1) { ConfigParam *arg = node->args; return (cxstring){ arg->value.ptr, arg->value.length }; } return (cxstring){ NULL, 0 }; } cxstring serverconfig_directive_get_arg(ConfigNode *directive, cxstring arg_name) { cxstring ret = (cxstring){ NULL, 0 }; ConfigParam *arg = directive->args; while(arg) { if(!cx_strcmp(arg_name, cx_strcast(arg->name))) { ret = cx_strcast(arg->value); break; } arg = arg->next; } return ret; } /* -------------------------- utility functions -------------------------- */ int serverconfig_validate_directive_name( ConfigNode *directive, const char *names[], size_t numnames, size_t *nameindex) { for(size_t i=0;i<numnames;i++) { if(!cx_strcmp(cx_strcast(directive->name), cx_str(names[i]))) { *nameindex = i; return 0; } } return 1; } int serverconfig_check_param_names(ConfigNode *directive, ConfigParam **err) { ConfigParam *arg = directive->args; while(arg) { if(arg->name.length == 0) { *err = arg; return 1; } arg = arg->next; } return 0; } ConfigNode* serverconfig_previous_dir_or_obj(ConfigNode *node) { node = node->prev; while(node) { if(node->type == CONFIG_NODE_DIRECTIVE || node->type == CONFIG_NODE_OBJECT) { return node; } node = node->prev; } return NULL; } size_t serverconfig_children_count(ConfigNode *node, ConfigNodeType type) { size_t count = 0; node = node->children_begin; while(node) { if(node->type == type) { count++; } node = node->next; } return count; }