/* silcconfig.c Author: Johnny Mnemonic Copyright (C) 1997 - 2002 Pekka Riikonen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. */ /* $Id$ */ #include "silcincludes.h" /* limit debug logging verbosity */ #if 0 #define SILC_CONFIG_DEBUG(fmt) SILC_LOG_DEBUG(fmt) #else #define SILC_CONFIG_DEBUG(fmt) #endif /* this is the option struct and currently it is only used internally to * the module and other structs. */ typedef struct SilcConfigOptionStruct { char *name; /* *lowercase* name of the option */ SilcConfigType type; /* important: the type of the returned value */ SilcConfigCallback cb; /* the value handler */ const SilcConfigTable *subtable; /* used if type is SILC_CONFIG_ARG_BLOCK */ void *context; /* context passed to the callback function */ struct SilcConfigOptionStruct *next; } SilcConfigOption; /* unique for each config file (and included ones) */ struct SilcConfigFileObject { char *filename; /* the original filename opened */ int level; /* parsing level, how many nested silc_config_main we have */ char *base; /* this is a fixed pointer to the base location */ char *p; /* the Parser poitner */ SilcUInt32 len; /* fixed length of the whole file */ SilcUInt32 line; /* current parsing line, strictly linked to p */ bool included; /* wether this file is main or included */ }; /* We need the entity to base our block-style parsing on */ struct SilcConfigEntityObject { SilcConfigOption *opts; /* known options list */ SilcConfigFile *file; /* parsing file object */ }; /* access error descriptions only with silc_config_strerror() */ static char *errorstrs[] = { "-OK", /* SILC_CONFIG_OK */ "-SILENT", /* SILC_CONFIG_ESILENT */ "-PRINTLINE", /* SILC_CONFIG_EPRINTLINE */ "Invalid syntax", /* SILC_CONFIG_EGENERIC */ "Internal error! Please report this bug", /* SILC_CONFIG_EINTERNAL */ "Can't open specified file", /* SILC_CONFIG_ECANTOPEN */ "Expected open-brace '{'", /* SILC_CONFIG_EOPENBRACE */ "Missing close-brace '}'", /* SILC_CONFIG_ECLOSEBRACE */ "Invalid data type", /* SILC_CONFIG_ETYPE */ "Unknown option", /* SILC_CONFIG_EBADOPTION */ "Invalid text", /* SILC_CONFIG_EINVALIDTEXT */ "Double option specification", /* SILC_CONFIG_EDOUBLE */ "Expected data but not found", /* SILC_CONFIG_EEXPECTED */ "Expected '='", /* SILC_CONFIG_EEXPECTEDEQUAL */ "Unexpected data", /* SILC_CONFIG_EUNEXPECTED */ "Missing mandatory fields", /* SILC_CONFIG_EMISSFIELDS */ "Missing ';'", /* SILC_CONFIG_EMISSCOLON */ }; /* return string describing SilcConfig's error code */ char *silc_config_strerror(int errnum) { if ((errnum < 0) || (errnum >= sizeof(errorstrs)/sizeof(*errorstrs)) || (errorstrs[errnum] == NULL)) { char *defret = "-INVALIDERROR"; return defret; } return errorstrs[errnum]; } /* Begin of internal SilcConfig's text util functions */ /* Points the first non-space character */ static void my_trim_spaces(SilcConfigFile *file) { register char *r = file->p; while (isspace(*r)) if (*r++ == '\n') file->line++; file->p = r; } /* Skips the current line until newline (lf or cr) */ static void my_skip_line(SilcConfigFile *file) { register char *r = file->p; while (*r && (*r != '\n') && (*r != '\r')) r++; file->p = (*r ? r + 1 : r); file->line++; } /* Obtains a text token from the current position until first separator. * a separator is any non alphanumeric character nor "_" or "-" */ static char *my_next_token(SilcConfigFile *file, char *to) { register char *o; my_trim_spaces(file); o = file->p; while (isalnum(*o) || (*o == '_') || (*o == '-')) *to++ = *o++; *to = '\0'; file->p = o; return to; } /* Obtains a string from the current position. The only difference from * next_token() is that quoted-strings are also accepted */ static char *my_get_string(SilcConfigFile *file, char *to) { char *o; my_trim_spaces(file); o = file->p; if (*o == '"') { char *quot = strchr(++o, '"'); int len = quot - o; if (!quot) { /* XXX FIXME: gotta do something here */ printf("Bullshit, missing matching \""); exit(1); } if (len <= 0) *to = '\0'; else { strncpy(to, o, len); to[len] = '\0'; } /* update stream pointer */ file->p = quot + 1; return to; } /* we don't need quote parsing, fall-back to token extractor */ my_next_token(file, to); return to; }; /* Skips all comment lines and spaces lines until first useful character */ static void my_skip_comments(SilcConfigFile *file) { while (1) { my_trim_spaces(file); if (*file->p != '#') return; my_skip_line(file); } } /* End of internal text functions * Next section contains SilcConfig internal config utils */ /* find an option in the list by name and returns its pointer */ static SilcConfigOption *silc_config_find_option(SilcConfigEntity ent, const char *name) { SilcConfigOption *tmp; for (tmp = ent->opts; tmp; tmp = tmp->next) { if (!strcasecmp(tmp->name, name)) return tmp; } return NULL; } /* ... */ static void *silc_config_marshall(SilcConfigType type, const char *val) { void *pt; int val_int; bool val_bool; char *val_tmp; SilcUInt32 val_size; switch (type) { case SILC_CONFIG_ARG_TOGGLE: if (!strcasecmp(val, "yes") || !strcasecmp(val, "true") || !strcasecmp(val, "on") || !strcasecmp(val, "1")) { val_bool = TRUE; } else if (!strcasecmp(val, "no") || !strcasecmp(val, "false") || !strcasecmp(val, "off") || !strcasecmp(val, "0")) { val_bool = FALSE; } else return NULL; pt = silc_calloc(1, sizeof(val_bool)); *(bool *)pt = (bool) val_bool; return pt; case SILC_CONFIG_ARG_INT: val_int = (int) strtol(val, &val_tmp, 0); if (*val_tmp) /* error converting string */ return NULL; pt = silc_calloc(1, sizeof(val_int)); *(int *)pt = val_int; return pt; case SILC_CONFIG_ARG_SIZE: val_size = (SilcUInt32) strtol(val, &val_tmp, 0); if (val == val_tmp) return NULL; /* really wrong, there must be at least one digit */ /* Search for a designator */ switch (tolower(val_tmp[0])) { case '\0': /* None */ break; case 'k': /* Kilobytes */ val_size *= (SilcUInt32) 1024; break; case 'm': /* Megabytes */ val_size *= (SilcUInt32) (1024 * 1024); break; case 'g': val_size *= (SilcUInt32) (1024 * 1024 * 1024); break; default: return NULL; } /* the string must die here */ if (val_tmp[1]) return NULL; pt = silc_calloc(1, sizeof(val_size)); *(SilcUInt32 *)pt = val_size; return pt; case SILC_CONFIG_ARG_STR: /* the only difference between STR and STRE is */ if (!val[0]) /* that STR cannot be empty, while STRE can. */ return NULL; case SILC_CONFIG_ARG_STRE: pt = (void *) strdup(val); return pt; /* following types are not supposed to have a return value */ case SILC_CONFIG_ARG_BLOCK: case SILC_CONFIG_ARG_NONE: return NULL; default: return NULL; } return NULL; } /* End of internal functions */ /* Tries to open the config file and returns a valid SilcConfigFile object * or NULL if failed */ SilcConfigFile *silc_config_open(const char *configfile) { char *buffer; SilcUInt32 filelen; SilcConfigFile *ret; if (!(buffer = silc_file_readfile(configfile, &filelen))) return NULL; ret = silc_calloc(1, sizeof(*ret)); ret->filename = strdup(configfile); ret->base = ret->p = buffer; ret->len = filelen; ret->line = 1; /* line count, start from first line */ return ret; } /* Frees a file object */ void silc_config_close(SilcConfigFile *file) { if (file) { silc_free(file->filename); memset(file->base, 'F', file->len); silc_free(file->base); memset(file, 'F', sizeof(*file)); silc_free(file); } } /* initializes a SilcConfigEntity pointer allocation */ SilcConfigEntity silc_config_init(SilcConfigFile *file) { SilcConfigEntity ret; if (!file) return NULL; SILC_CONFIG_DEBUG(("Allocating new config entity")); ret = silc_calloc(1, sizeof(*ret)); ret->file = file; return ret; }; /* Returns the original filename of the object file */ char *silc_config_get_filename(SilcConfigFile *file) { if (file) return file->filename; return NULL; } /* Returns the current line that file parsing arrived at */ SilcUInt32 silc_config_get_line(SilcConfigFile *file) { if (file) return file->line; return 0; } /* Returns a pointer to the beginning of the requested line. If the line * was not found, NULL is returned */ char *silc_config_read_line(SilcConfigFile *file, SilcUInt32 line) { register char *p; int len; char *ret = NULL, *endbuf; if (!file || (line <= 0)) return NULL; for (p = file->base; *p && (*p != EOF); p++) { if (line <= 1) goto found; if (*p == '\n') line--; } return NULL; found: if ((endbuf = strchr(p, '\n'))) { len = endbuf - p; if (len > 0) ret = silc_memdup(p, len); } else { ret = silc_memdup(p, strlen(p)); } return ret; } /* Convenience function to read the current parsed line */ char *silc_config_read_current_line(SilcConfigFile *file) { return silc_config_read_line(file, file->line); } /* (Private) destroy a SilcConfigEntity */ static void silc_config_destroy(SilcConfigEntity ent, bool destroy_opts) { SilcConfigOption *oldopt, *nextopt; SILC_CONFIG_DEBUG(("Freeing config entity [ent=0x%x] [opts=0x%x]", (SilcUInt32) ent, (SilcUInt32) ent->opts)); /* if she wants to preserve options just free the object struct */ if (!destroy_opts) goto skip_sect; for (oldopt = ent->opts; oldopt; oldopt = nextopt) { nextopt = oldopt->next; memset(oldopt->name, 'F', strlen(oldopt->name) + 1); silc_free(oldopt->name); memset(oldopt, 'F', sizeof(*oldopt)); silc_free(oldopt); } skip_sect: memset(ent, 'F', sizeof(*ent)); silc_free(ent); } /* Registers a new option in the specified entity. * Returns TRUE on success, FALSE if already registered. */ bool silc_config_register(SilcConfigEntity ent, const char *name, SilcConfigType type, SilcConfigCallback cb, const SilcConfigTable *subtable, void *context) { SilcConfigOption *newopt; SILC_CONFIG_DEBUG(("Register new option=\"%s\" " "type=%u cb=0x%08x context=0x%08x", name, type, (SilcUInt32) cb, (SilcUInt32) context)); /* if we are registering a block, make sure there is a specified sub-table */ if (!ent || !name || ((type == SILC_CONFIG_ARG_BLOCK) && !subtable)) return FALSE; /* don't register a reserved tag */ if (!strcasecmp(name, "include")) return FALSE; /* check if an option was previously registered */ if (silc_config_find_option(ent, name)) { SILC_LOG_DEBUG(("Error: Can't register \"%s\" twice.", name)); return FALSE; } /* allocate and append the new option */ newopt = silc_calloc(1, sizeof(*newopt)); newopt->name = strdup(name); newopt->type = type; newopt->cb = cb; newopt->subtable = subtable; newopt->context = context; /* append this option to the list */ if (!ent->opts) ent->opts = newopt; else { SilcConfigOption *tmp; for (tmp = ent->opts; tmp->next; tmp = tmp->next); tmp->next = newopt; } return TRUE; } /* Register a new option table in the specified config entity */ bool silc_config_register_table(SilcConfigEntity ent, const SilcConfigTable table[], void *context) { int i; if (!ent || !table) return FALSE; SILC_CONFIG_DEBUG(("Registering table")); /* XXX FIXME: some potability checks needed - really? */ for (i = 0; table[i].name; i++) { if (!silc_config_register(ent, table[i].name, table[i].type, table[i].callback, table[i].subtable, context)) return FALSE; } return TRUE; } /* ... */ static int silc_config_main_internal(SilcConfigEntity ent) { SilcConfigFile *file = ent->file; char **p = &file->p; /* loop throught statements */ while (1) { char buf[255]; SilcConfigOption *thisopt; /* makes it pointing to the next interesting char */ my_skip_comments(file); /* got eof? */ if (**p == '\0' || **p == EOF) { if (file->level > 1) /* cannot get eof in a sub-level! */ return SILC_CONFIG_EEXPECTED; goto finish; } /* check if we completed this (sub) section (it doesn't matter if this * is the main section) */ if (**p == '}') { if (file->level < 2) /* can't be! must be at least one sub-block */ return SILC_CONFIG_EUNEXPECTED; (*p)++; goto finish; } //SILC_LOG_HEXDUMP(("Preparing lookup at line=%lu", file->line), *p, 16); /* obtain the keyword */ my_next_token(file, buf); SILC_CONFIG_DEBUG(("Looking up keyword=\"%s\" [line=%lu]", buf, file->line)); /* handle special directive */ if (!strcasecmp(buf, "include")) { int ret; SilcConfigFile *inc_file; SilcConfigEntity inc_ent; my_trim_spaces(file); /* prepare next char */ /* Now trying to include the specified file. The included file will * be allowed to include sub-files but it will preserve the block-level * of the including block. Note that the included file won't be allowed * to raise the block level of the including block. */ my_get_string(file, buf); /* get the filename */ SILC_LOG_DEBUG(("Including file \"%s\"", buf)); /* before getting on, check if this row is REALLY complete */ if (*(*p)++ != ';') return SILC_CONFIG_EMISSCOLON; /* open the file and start the parsing */ inc_file = silc_config_open(buf); if (!inc_file) /* does it point a valid filename? */ return SILC_CONFIG_ECANTOPEN; inc_file->included = TRUE; /* create a new entity and hack it to use the same options */ inc_ent = silc_config_init(inc_file); inc_ent->opts = ent->opts; ret = silc_config_main(inc_ent); /* Cleanup. * If the included file returned an error, the application will probably * want to output some kind of error message. Because of this, we can't * destroy THIS file object. The hack works this way: The application * expects to destroy the originally created object file, so we'll swap * the original file with the included file. */ if (ret) { SilcConfigFile tmp_file; SILC_CONFIG_DEBUG(("SWAPPING FILE OBJECTS")); memcpy(&tmp_file, inc_file, sizeof(tmp_file)); memcpy(inc_file, file, sizeof(tmp_file)); silc_config_close(inc_file); memcpy(file, &tmp_file, sizeof(tmp_file)); return ret; } /* otherwise if no errors encoured, continue normally */ silc_config_close(inc_file); continue; /* this one is handled */ } /* we have a registered option (it can also be a sub-block) */ thisopt = silc_config_find_option(ent, buf); if (!thisopt) return SILC_CONFIG_EBADOPTION; my_trim_spaces(file); /* prepare next char */ /* option type is a block? */ if (thisopt->type == SILC_CONFIG_ARG_BLOCK) { int ret; SilcConfigEntity sub_ent; SILC_CONFIG_DEBUG(("Entering sub-block")); if (*(*p)++ != '{') return SILC_CONFIG_EOPENBRACE; /* build the new entity for this sub-block */ sub_ent = silc_config_init(ent->file); /* use the previous specified table to describe this block's options */ silc_config_register_table(sub_ent, thisopt->subtable, thisopt->context); /* run this block! */ ret = silc_config_main(sub_ent); SILC_CONFIG_DEBUG(("Returned from sub-block [ret=%d]", ret)); if (ret) /* now check the result */ return ret; /* now call block clean-up callback (if any) */ if (thisopt->cb) { int ret; SILC_CONFIG_DEBUG(("Now calling clean-up callback")); ret = thisopt->cb(thisopt->type, thisopt->name, file->line, NULL, thisopt->context); if (ret) { SILC_CONFIG_DEBUG(("Callback refused the value [ret=%d]", ret)); return ret; } } /* Do we want ';' to be mandatory after close brace? */ if (*(*p)++ != ';') return SILC_CONFIG_EMISSCOLON; } else if (thisopt->type == SILC_CONFIG_ARG_NONE) { /* before getting on, check if this row is REALLY complete */ if (*(*p)++ != ';') return SILC_CONFIG_EMISSCOLON; SILC_CONFIG_DEBUG(("Triggering callback for none")); if (thisopt->cb) { thisopt->cb(thisopt->type, thisopt->name, file->line, NULL, thisopt->context); } } else { void *pt; int ret; if (*(*p)++ != '=') return SILC_CONFIG_EEXPECTEDEQUAL; my_get_string(file, buf); /* get the option argument */ SILC_CONFIG_DEBUG(("With argument=\"%s\"", buf)); /* before getting on, check if this row is REALLY complete */ if (*(*p)++ != ';') return SILC_CONFIG_EMISSCOLON; /* convert the option argument to the right format */ pt = silc_config_marshall(thisopt->type, buf); if (!pt) return SILC_CONFIG_EINVALIDTEXT; if (thisopt->cb) { ret = thisopt->cb(thisopt->type, thisopt->name, file->line, pt, thisopt->context); if (ret) { SILC_CONFIG_DEBUG(("Callback refused the value [ret=%d]", ret)); return ret; } } silc_free(pt); } continue; finish: break; } return SILC_CONFIG_OK; } /* ... */ int silc_config_main(SilcConfigEntity ent) { SilcConfigFile *file = ent->file; int ret; /* don't silently accept a NULL entity */ if (!ent) { ret = SILC_CONFIG_EGENERIC; goto main_cleanup; } /* call the real main and store the result */ file->level++; SILC_CONFIG_DEBUG(("[Lev=%d] Entering config parsing core", file->level)); ret = silc_config_main_internal(ent); SILC_CONFIG_DEBUG(("[Lev=%d] Quitting main [ret=%d]", file->level, ret)); if (!file->level) /* when swap happens, we could close a file twice */ goto main_end; file->level--; /* If this file was included don't destroy the options set because it is * the same of the including block. Although if this entity is in a * sub-block created inside the included file, this options set must be * destroyed. */ main_cleanup: if ((file->level != 0) || (file->included != TRUE)) silc_config_destroy(ent, TRUE); else silc_config_destroy(ent, FALSE); main_end: return ret; }