/* * Quick'n'Dirty config-file parser (2007-10-16) * Copyright (c) 2007 Daniel Mealha Cabrita * Licensed under GNU GPL v2 or later version */ #include #include /* FIXME: _GNU_SOURCE has no effect at this point * let's just build strndup() for all OSes to be safe */ // #ifdef __GLIBC__ // #define _GNU_SOURCE // #else #define QP_BUILD_STRNDUP // #endif #include #include "qparser.h" /* applied to t_qp_bool */ #define QP_INVALID -1 #define t_qp_datatype enum enum_qp_datatype enum enum_qp_datatype {QP_DATATYPE_STRING, QP_DATATYPE_INT_FLOAT, QP_DATATYPE_BOOL, QP_DATATYPE_ARRAY, QP_DATATYPE_INVALID, QP_DATATYPE_ANY, QP_DATATYPE_EMPTY }; static t_qp_initflags qp_globalflags; #ifdef QP_BUILD_STRNDUP char *strndup(const char *s, size_t n) { char *newstr = NULL; if ((newstr = calloc (n + 1, sizeof (char))) != NULL) { strncpy (newstr, s, n); *(newstr + n) = '\0'; } return (newstr); } #endif t_qp_datatype return_datatype (const char *src); char *remove_quotes (char *src, t_qp_datatype datatype); void log_error (t_qp_configfile *conf_handler, const char *conf_key, t_qp_error error_code); char *array_get_element (char *src, int position); int strcmp_x (const char *s1, const char *s2) { if (qp_globalflags & QP_INITFLAG_IGNORECASE) return (strcasecmp (s1, s2)); return (strcmp (s1, s2)); } int strncmp_x (const char *s1, const char *s2, size_t n) { if (qp_globalflags & QP_INITFLAG_IGNORECASE) return (strncasecmp (s1, s2, n)); return (strncmp (s1, s2, n)); } /* converts CRLF and CR to that standard and closes dst with '\0' * it also replaces '\0' in the middle of the text with spaces * return: size of resulting text (may be <= srclen because of CR suppression, for example) */ /* *dst may be the same pointer as *src, but the buffer must be at least srclen+1 */ int fix_linebreaks_qp (const unsigned char *src, int srclen, unsigned char *dst) { const unsigned char *rpos = src; unsigned char *wpos = dst; unsigned char prevchar = '\0'; unsigned char curchar; int prevchar_was_cr_or_lf = 0; int curchar_is_cr_or_lf; int count = srclen; int dstlen = 0; while (count--) { curchar = *(rpos++); curchar_is_cr_or_lf = 0; if ((curchar == '\n') || (curchar == '\r')) { curchar_is_cr_or_lf = 1; if ((!prevchar_was_cr_or_lf) || ((prevchar_was_cr_or_lf) && (prevchar == curchar))) { *(wpos++) = '\n'; dstlen++; } } else if (curchar != '\0') { *(wpos++) = curchar; dstlen++; } else { *(wpos++) = ' '; dstlen++; } prevchar_was_cr_or_lf = curchar_is_cr_or_lf; prevchar = curchar; } *wpos = '\0'; return (dstlen); } /* returns null if unsuccessful * pointed buffer must be freed after usage * returns file size into *filesize, if filesize != NULL * the returned data consists of the file data itself, with an * appended '\0' to it (thus, it's a valid C string) */ /* FIXME: this routine does not expect IO errors */ char *load_textfile_to_memory (const char *given_filename) { int my_filesize; char *my_buff; FILE *my_file; if ((my_file = fopen (given_filename, "rb")) != NULL) { /* determine file size */ fseek (my_file, 0, SEEK_END); my_filesize = ftell (my_file); fseek (my_file, 0, SEEK_SET); if ((my_buff = malloc (my_filesize + 1)) != NULL) { fread (my_buff, my_filesize, 1, my_file); fclose (my_file); *(my_buff + my_filesize) = '\0'; return (my_buff); } else { fclose (my_file); } } return(NULL); } /* equivalent to strlen, but instead of stopping at '\0', stops at '\n' (or '\0') */ /* returns line len (including the trailing '\n') */ int get_line_len (const char *src) { int linelen = 0; while ((*src != '\0') && (*src != '\n')) { src++; linelen++; } if (*src == '\n') linelen++; return (linelen); } /* remove comments and empty lines, leave only useful config data */ /* 'src' may be the same as 'dst' */ void remove_useless_data (const char *src, char *dst) { int linelen, counter; int emptyline; int inside_grouping; const char *readpos, *readpos2; char *writepos, *writepos2; /* remove comments */ readpos = src; writepos = dst; while ((linelen = get_line_len (readpos)) != 0) { counter = linelen; while (counter) { if (*readpos == '#') { /* ignore the rest of the line */ if (*(readpos + counter - 1) == '\n') *(writepos++) = '\n'; readpos += counter; counter = 0; } else if (*readpos == '"') { *(writepos++) = *(readpos++); counter--; /* this may seem like a possible memory invasion case, * but the text as whole has a trailing '\0' still. * this is just an ugly shortchut */ while (counter && (*readpos != '"')) { *(writepos++) = *(readpos++); counter--; } if (counter && (*readpos == '"')) { *(writepos++) = *(readpos++); counter--; } } else { *(writepos++) = *(readpos++); counter--; } } } *writepos = '\0'; /* remove empty lines (just spaces/tabs/etc or simply empty) */ readpos = dst; writepos = dst; while ((linelen = get_line_len (readpos)) != 0) { counter = linelen; emptyline = 1; readpos2 = readpos; writepos2 = writepos; while (counter--) { if ((*readpos2 > ' ') && (*readpos2 != '\n')) emptyline = 0; readpos2++; } if (emptyline == 0) { counter = linelen; while (counter--) *(writepos++) = *(readpos++); } else { readpos += linelen; } } *writepos = '\0'; /* remove unneccessary spaces */ readpos = dst; writepos = dst; while ((linelen = get_line_len (readpos)) != 0) { counter = linelen; while (counter) { if ((*readpos <= ' ') && (*readpos != '\n')) { readpos++; } else if (*readpos == '\\') { *(writepos++) = *(readpos++); if (counter > 1) { *(writepos++) = *(readpos++); counter--; } } else if (*readpos == '"') { *(writepos++) = *(readpos++); counter--; /* this may seem like a possible memory invasion case, * but the text as whole has a trailing '\0' still. * this is just an ugly shortchut */ while (counter && (*readpos != '"')) { *(writepos++) = *(readpos++); counter--; } if (counter && (*readpos == '"')) { *(writepos++) = *(readpos++); counter--; } counter++; } else { *(writepos++) = *(readpos++); } counter--; } } *writepos = '\0'; /* join lines from the same entry into just one * typically lines in the {} context */ readpos = dst; writepos = dst; counter = strlen (readpos); inside_grouping = 0; while (counter) { switch (*readpos) { case '"': *(writepos++) = *(readpos++); counter--; /* this may seem like a possible memory invasion case, * but the text as whole has a trailing '\0' still. * this is just an ugly shortchut */ while (counter && (*readpos != '"')) { *(writepos++) = *(readpos++); counter--; } if (counter && (*readpos == '"')) { *(writepos++) = *(readpos++); counter--; } break; case '\\': *(writepos++) = *(readpos++); counter--; if (counter) { *(writepos++) = *(readpos++); counter--; } break; case '{': inside_grouping = 1; *(writepos++) = *(readpos++); counter--; break; case '}': inside_grouping = 0; *(writepos++) = *(readpos++); counter--; break; case '\n': if (inside_grouping == 0) *(writepos++) = *(readpos++); else readpos++; counter--; break; default: *(writepos++) = *(readpos++); counter--; } } *writepos = '\0'; } /* * check whether the key matches or not * returns: 0:does not 1:matches -1:syntax error */ int check_conf_key (const char *src, int src_len, const char *conf_key) { int loop = src_len; int equal_pos = -1; const char *readpos; /* where is the (first) '=' ? */ readpos = src; while (loop) { if (*(readpos++) == '=') { equal_pos = src_len - loop; loop = 0; } else { loop--; } } if (equal_pos <= 0) { return (-1); } if (strlen (conf_key) == equal_pos) { if (! strncmp_x (src, conf_key, equal_pos)) return (1); } return (0); } /* * get entry from config file as string * (if it's supposed to be int, should be converted later from string) * returns: string with value, or NULL if not found * NOTE: the returned pointer, if != NULL, should be freed manually */ char *get_single_val (t_qp_configfile *conf_handler, const char *conf_key) { char *readpos; int linelen; int conf_key_len; char *new_str; char *ret_data = NULL; int data_len; int line_number = 0; conf_key_len = strlen (conf_key); readpos = conf_handler->filedata; while ((linelen = get_line_len (readpos)) != 0) { if (check_conf_key (readpos, linelen, conf_key) == 1) { if (ret_data != NULL) log_error (conf_handler, conf_key, QP_ERROR_DUPLICATED); conf_handler->requested_conf [line_number] = QP_TRUE; data_len = linelen - (conf_key_len + 1); new_str = strndup (readpos + conf_key_len + 1, data_len); /* remove trailing \n if present */ if (*(new_str + (data_len - 1)) == '\n') *(new_str + (data_len - 1)) = '\0'; ret_data = new_str; } else { /* TODO: syntax error in this line */ } readpos += linelen; line_number++; } return (ret_data); } /* returns: !=0: sane, ==0: syntax error */ int check_datatype_sanity (t_qp_configfile *conf_handler, const char *conf_key, char *src, t_qp_datatype datatype) { char *ret_data; ret_data = strdup (src); if (return_datatype (ret_data) == datatype) { if ((datatype == QP_DATATYPE_STRING) || (datatype == QP_DATATYPE_ARRAY)) { if (remove_quotes (ret_data, datatype) == NULL) { /* syntax error related to " or {} */ log_error (conf_handler, conf_key, QP_ERROR_SYNTAX); free (ret_data); return (0); } } free (ret_data); return (1); } else { /* syntax error (invalid datatype) */ log_error (conf_handler, conf_key, QP_ERROR_SYNTAX); free (ret_data); return (0); } free (ret_data); return (1); } /* * same as get_single_val(), but: * - also verifies whether the readen value is coherent with datatype * - add error messages to log when appropriate * - remove quotes " from string, removes {} from arrays */ char *get_single_val_typed_adjusted (t_qp_configfile *conf_handler, const char *conf_key, t_qp_datatype datatype, const t_qp_flags qp_flags) { char *ret_data; if ((ret_data = get_single_val (conf_handler, conf_key)) != NULL) { if (check_datatype_sanity (conf_handler, conf_key, ret_data, datatype)) { if ((datatype == QP_DATATYPE_STRING) || (datatype == QP_DATATYPE_ARRAY)) remove_quotes (ret_data, datatype); return (ret_data); } } else { /* not found */ if (qp_flags & QP_FLAG_REQUIRED) log_error (conf_handler, conf_key, QP_ERROR_MISSING_CONFKEY); } return (NULL); } /* * similar to get_single_val_typed_adjusted() but used for array data instead * - also verifies whether the readen value is coherent with datatype * - add error messages to log when appropriate * - remove quotes " from string * - won't report error if data addressed by position does not exist * (since it may be used to determine the array length) */ char *get_array_val_typed_adjusted (t_qp_configfile *conf_handler, const char *conf_key, int position, t_qp_datatype datatype, const t_qp_flags qp_flags) { char *ret_data; char *element_data; if ((ret_data = get_single_val_typed_adjusted (conf_handler, conf_key, QP_DATATYPE_ARRAY, qp_flags)) != NULL) { element_data = array_get_element (ret_data, position); free (ret_data); if (element_data == NULL) return (NULL); if (datatype == QP_DATATYPE_ANY) return (element_data); if (check_datatype_sanity (conf_handler, conf_key, element_data, datatype)) { if (datatype == QP_DATATYPE_STRING) remove_quotes (element_data, datatype); return (element_data); } } else { /* not found */ if (qp_flags & QP_FLAG_REQUIRED) log_error (conf_handler, conf_key, QP_ERROR_MISSING_CONFKEY); } return (NULL); } /* * src contains boolean value, this routine interprets its value. * returns: QP_TRUE, QP_FALSE or QP_INVALID (syntax error) */ t_qp_bool collect_bool_from_str (const char *src) { int counter; const char tab_true [][5] = {"TRUE", "T", "Y", "YES", ""}; const char tab_false [][6] = {"FALSE", "F", "N", "NO", ""}; /* check if true */ counter = 0; while (tab_true [counter][0] != '\0') { if (! strcmp_x (src, tab_true [counter])) return (QP_TRUE); counter++; } /* check if false */ counter = 0; while (tab_false [counter][0] != '\0') { if (! strcmp_x (src, tab_false [counter])) return (QP_FALSE); counter++; } /* neither, error */ return (QP_INVALID); } /* return what kind of data is the one pointed by 'src' */ t_qp_datatype return_datatype (const char *src) { switch (*src) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': case '-': return (QP_DATATYPE_INT_FLOAT); case '{': return (QP_DATATYPE_ARRAY); case '\0': return (QP_DATATYPE_EMPTY); case '"': return (QP_DATATYPE_STRING); default: if (collect_bool_from_str (src) != QP_INVALID) return (QP_DATATYPE_BOOL); return (QP_DATATYPE_INVALID); } } /* * remove surrounding [ " ] or [ { and } ] from string depending on its type * if everything ok, returns 'src' * if syntax error (braces not closed, for example) returns NULL * applies only to QP_DATATYPE_ARRAY and QP_DATATYPE_STRING */ char *remove_quotes (char *src, t_qp_datatype datatype) { int srclen; srclen = strlen (src); if (srclen < 2) return (NULL); /* check if there's syntax error */ switch (datatype) { case QP_DATATYPE_ARRAY: if (*src == '{') { if (*(src + (srclen - 1)) != '}') return (NULL); } break; case QP_DATATYPE_STRING: if (*src == '"') { if (*(src + (srclen - 1)) != '"') { return (NULL); } } break; default: return (NULL); } strncpy (src, src + 1, srclen - 2); *(src + (srclen - 2)) = '\0'; return (src); } void log_error (t_qp_configfile *conf_handler, const char *conf_key, t_qp_error error_code) { int counter = 0; /* check whether the error is not logged already */ while (counter < conf_handler->errorlog_total) { if (! strcmp_x (conf_handler->errorlog [counter].conf_key, conf_key)) { if (conf_handler->errorlog [counter].error_code == error_code) { /* already logged */ return; } } counter++; } /* return if cannot add more error entries */ if (conf_handler->errorlog_total == QC_ERRORLOG_MAX_ERRORS) return; /* the last error is reserved to report 'more errors, but not displayed' (error table overflow) */ if (conf_handler->errorlog_total == (QC_ERRORLOG_MAX_ERRORS - 1)) { conf_handler->errorlog [conf_handler->errorlog_total].conf_key = strdup ("(...)"); conf_handler->errorlog [conf_handler->errorlog_total].error_code = QP_ERROR_MORE_ERRORS; } else { conf_handler->errorlog [conf_handler->errorlog_total].conf_key = strdup (conf_key); conf_handler->errorlog [conf_handler->errorlog_total].error_code = error_code; } (conf_handler->errorlog_total)++; } /* get str len of a element from within {} array * delimiter is ',' (if outside quotes) or '\0' (regardless) * returns: element size, _not_ including the following ',' if present */ int array_get_element_size (const char *src) { const char *readpos = src; int element_size = 0; int inside_quote = 0; while (*readpos != '\0') { switch (*readpos) { case '"': inside_quote ^= 1; readpos++; element_size++; break; case '\\': if (*readpos != '\0') { readpos += 2; element_size += 2;; } else { readpos++; element_size++; } break; case ',': if (inside_quote) { readpos++; element_size++; } else { return (element_size); } break; default: element_size++; readpos++; } } return (element_size); } /* get the a element from the array. * src must be in the following format: * element1,element2,element3 * returns: pointer to a string containing that element (that string must be freeded manually) * or NULL, if element not found */ char *array_get_element (char *src, int position) { int element_size; element_size = array_get_element_size (src); while (position) { src += element_size; if (*src == '\0') return (NULL); src++; element_size = array_get_element_size (src); position--; } return ((char *) strndup (src, element_size)); } /* * API (public) functions start here */ t_qp_configfile *qp_init (const char *conf_filename, t_qp_initflags qp_initflags) { t_qp_configfile *conf_handler; int conf_len; char *readpos; int counter; conf_handler = malloc (sizeof (t_qp_configfile)); if (conf_handler == NULL) { /* unable to allocate mem */ return (NULL); } conf_handler->filename = conf_filename; conf_handler->filedata = load_textfile_to_memory (conf_handler->filename); if (conf_handler->filedata == NULL) { /* unable to load file into memory */ free (conf_handler); return (NULL); } conf_len = strlen (conf_handler->filedata); fix_linebreaks_qp (conf_handler->filedata, conf_len, conf_handler->filedata); remove_useless_data (conf_handler->filedata, conf_handler->filedata); conf_handler->errorlog_total = 0; qp_globalflags = qp_initflags; /* create and initialize array for requested_conf */ conf_len = 0; readpos = conf_handler->filedata; conf_handler->total_lines = 0; while (*readpos != '\0') { if (*readpos == '\n') conf_handler->total_lines++; readpos++; conf_len++; } if ((*(conf_handler->filedata + (conf_len - 1)) != '\n') && (*(conf_handler->filedata + (conf_len - 1)) != '\0')) conf_handler->total_lines++; if ((conf_handler->requested_conf = calloc (conf_handler->total_lines, sizeof (t_qp_bool))) == NULL) { free (conf_handler); return (NULL); } counter = 0; while (counter < conf_handler->total_lines) { conf_handler->requested_conf [counter] = QP_FALSE; counter++; } return (conf_handler); } void qp_end (t_qp_configfile *conf_handler) { /* TODO: free strdup()s from error log */ free (conf_handler->requested_conf); free (conf_handler); } /* used to see errors while parsing for later processing (warning the user, etc) * error_num starts from 0 * returns !=0 if there's a error at error_num, ==0 if there's none */ int qp_get_error (t_qp_configfile *conf_handler, int error_num, const char **conf_key, t_qp_error *error_code) { if (conf_handler->errorlog_total < (error_num + 1)) return (0); *conf_key = conf_handler->errorlog [error_num].conf_key; *error_code = conf_handler->errorlog [error_num].error_code; return (1); } /* accepts dst == NULL, when wanting to check the configuration option presence but not updating any variable */ void qp_getconf_float (t_qp_configfile *conf_handler, const char *conf_key, float *dst, const t_qp_flags qp_flags) { char *ret_data; float pre_dst; if ((ret_data = get_single_val_typed_adjusted (conf_handler, conf_key, QP_DATATYPE_INT_FLOAT, qp_flags)) != NULL) { if (! sscanf (ret_data, "%f", &pre_dst)) { /* syntax error */ log_error (conf_handler, conf_key, QP_ERROR_SYNTAX); } if (dst != NULL) *dst = pre_dst; free (ret_data); } return; } /* accepts dst == NULL, when wanting to check the configuration option presence but not updating any variable */ void qp_getconf_int (t_qp_configfile *conf_handler, const char *conf_key, int *dst, const t_qp_flags qp_flags) { char *ret_data; int pre_dst; if ((ret_data = get_single_val_typed_adjusted (conf_handler, conf_key, QP_DATATYPE_INT_FLOAT, qp_flags)) != NULL) { if (! sscanf (ret_data, "%d", &pre_dst)) { /* syntax error */ log_error (conf_handler, conf_key, QP_ERROR_SYNTAX); } if (dst != NULL) *dst = pre_dst; free (ret_data); } return; } /* accepts dst == NULL, when wanting to check the configuration option presence but not updating any variable * neither allocating memory for the result string */ void qp_getconf_str (t_qp_configfile *conf_handler, const char *conf_key, char **dst, const t_qp_flags qp_flags) { char *ret_data; if ((ret_data = get_single_val_typed_adjusted (conf_handler, conf_key, QP_DATATYPE_STRING, qp_flags)) != NULL) { if (dst == NULL) { free (ret_data); } else { *dst = ret_data; /* TODO: add ret_data to free() pool */ } } return; } /* accepts dst == NULL, when wanting to check the configuration option presence but not updating any variable */ void qp_getconf_bool (t_qp_configfile *conf_handler, const char *conf_key, t_qp_bool *dst, const t_qp_flags qp_flags) { char *ret_data; t_qp_bool pre_dst; if ((ret_data = get_single_val_typed_adjusted (conf_handler, conf_key, QP_DATATYPE_BOOL, qp_flags)) != NULL) { switch (collect_bool_from_str (ret_data)) { case QP_TRUE: pre_dst = QP_TRUE; break; case QP_FALSE: pre_dst = QP_FALSE; break; default: /* invalid boolean, syntax error */ log_error (conf_handler, conf_key, QP_ERROR_SYNTAX); } if (dst != NULL) *dst = pre_dst; free (ret_data); } return; } /* accepts dst == NULL, when wanting to check the configuration option presence but not updating any variable */ void qp_getconf_array_bool (t_qp_configfile *conf_handler, const char *conf_key, int position, t_qp_bool *dst, const t_qp_flags qp_flags) { char *ret_data; t_qp_bool pre_dst; if ((ret_data = get_array_val_typed_adjusted (conf_handler, conf_key, position, QP_DATATYPE_BOOL, qp_flags)) != NULL) { switch (collect_bool_from_str (ret_data)) { case QP_TRUE: pre_dst = QP_TRUE; break; case QP_FALSE: pre_dst = QP_FALSE; break; default: /* invalid boolean, syntax error */ log_error (conf_handler, conf_key, QP_ERROR_SYNTAX); } if (dst != NULL) *dst = pre_dst; free (ret_data); } return; } /* accepts dst == NULL, when wanting to check the configuration option presence but not updating any variable * neither allocating memory for the result string */ void qp_getconf_array_str (t_qp_configfile *conf_handler, const char *conf_key, int position, char **dst, const t_qp_flags qp_flags) { char *ret_data; if ((ret_data = get_array_val_typed_adjusted (conf_handler, conf_key, position, QP_DATATYPE_STRING, qp_flags)) != NULL) { if (dst == NULL) { free (ret_data); } else { *dst = ret_data; // TODO: add element_data to free() pool */ } } return; } /* accepts dst == NULL, when wanting to check the configuration option presence but not updating any variable */ void qp_getconf_array_int (t_qp_configfile *conf_handler, const char *conf_key, int position, int *dst, const t_qp_flags qp_flags) { char *ret_data; int pre_dst; if ((ret_data = get_array_val_typed_adjusted (conf_handler, conf_key, position, QP_DATATYPE_INT_FLOAT, qp_flags)) != NULL) { if (! sscanf (ret_data, "%d", &pre_dst)) { /* syntax error */ log_error (conf_handler, conf_key, QP_ERROR_SYNTAX); } if (dst != NULL) *dst = pre_dst; free (ret_data); } return; } /* accepts dst == NULL, when wanting to check the configuration option presence but not updating any variable */ void qp_getconf_array_float (t_qp_configfile *conf_handler, const char *conf_key, int position, float *dst, const t_qp_flags qp_flags) { char *ret_data; float pre_dst; if ((ret_data = get_array_val_typed_adjusted (conf_handler, conf_key, position, QP_DATATYPE_INT_FLOAT, qp_flags)) != NULL) { if (! sscanf (ret_data, "%f", &pre_dst)) { /* syntax error */ log_error (conf_handler, conf_key, QP_ERROR_SYNTAX); } if (dst != NULL) *dst = pre_dst; free (ret_data); } return; } int qp_get_array_size (t_qp_configfile *conf_handler, const char *conf_key) { char *ret_data; int array_size = 0; while ((ret_data = get_array_val_typed_adjusted (conf_handler, conf_key, array_size, QP_DATATYPE_ANY, QP_FLAG_NONE)) != NULL) { free (ret_data); array_size++; } return (array_size); } /* add to the errorlist pool options which were not requested (using qp_getconf_* funtions) * up to the point this function is called * use this if you do _not_ want the parser to simply ignore unrecognized options */ void qp_fail_unrecognized_conf (t_qp_configfile *conf_handler) { int counter = 0; int linelen; const char *readpos = conf_handler->filedata; char *new_str; if (conf_handler->total_lines == 0) return; while (counter < conf_handler->total_lines) { linelen = get_line_len (readpos); if (conf_handler->requested_conf [counter] == QP_FALSE) { new_str = (char *) strndup (readpos, linelen); if (*(new_str + (linelen - 1)) == '\n') *(new_str + (linelen - 1)) = '\0'; log_error (conf_handler, new_str, QP_ERROR_INVALID_PARM); } counter++; readpos += linelen; } } /* ends here */