/* * Copyright (c) 2010 Philippe Pepiot * Copyright (c) 2011 Martin Duquesnoy * * Permission to use, copy, modify, and 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. */ #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #ifndef _BSD_SOURCE #define _BSD_SOURCE #endif #include #include #include #include #include #include #include #include #include #include #include #include #include "parse.h" #include "util.h" extern char *__progname; enum keyword_t { SEC_START, SEC_END, INCLUDE, WORD, EQUAL, LIST_START, LIST_END, NONE }; #ifdef DEBUG static const struct { const char *name; enum keyword_t type; } kw_t_name[] = { {"SEC_START", SEC_START}, {"SEC_END", SEC_END}, {"INCLUDE", INCLUDE}, {"WORD", WORD}, {"EQUAL", EQUAL}, {"LIST_START", LIST_START}, {"LIST_END", LIST_END}, {"NONE", NONE}, }; #endif struct files { char *name; struct files *parent; }; struct keyword { enum keyword_t type; /* if WORD */ int line; struct files *file; char *name; struct keyword *next; }; struct state { bool quote; bool comment; char quote_char; }; /* TO REMOVE (use a identifier for config and fallback XDG in api functions) */ TAILQ_HEAD(, conf_sec) config; static struct keyword *keywords = NULL; static struct keyword * push_keyword(struct keyword *tail, enum keyword_t type, char *buf, size_t *offset, struct files *file, int line) { struct keyword *kw; #ifdef DEBUG int i = 0; #endif if(type == WORD && *offset == 0) return tail; kw = xcalloc(1, sizeof(*kw)); kw->type = type; kw->line = line; kw->file = file; kw->next = NULL; if(*offset) { buf[*offset] = '\0'; if(!strcmp(buf, INCLUDE_CMD)) kw->type = INCLUDE; else kw->name = strdup(buf); *offset = 0; } else kw->name = NULL; if(tail) tail->next = kw; #ifdef DEBUG for(i = 0; kw_t_name[i].type != NONE; ++i) if(kw_t_name[i].type == kw->type) warnxl("%s %s %s:%d\n", kw_t_name[i].name, (kw->name) ? kw->name : "", kw->file->name, kw->line); #endif return kw; } static void syntax(struct keyword *kw, const char *fmt, ...) { va_list args; fprintf(stderr, "%s:", __progname); if(kw && kw->file && kw->file->name) fprintf(stderr, "%s:%d", kw->file->name, kw->line); if(kw && kw->name) fprintf(stderr, ", near '%s'", kw->name); fprintf(stderr, ": "); va_start(args, fmt); vfprintf(stderr, fmt, args); va_end(args); fprintf(stderr, "\n"); } #define PUSH_KEYWORD(type) tail = push_keyword(tail, type, bufname, &j, file, line) static struct keyword * parse_keywords(const char *filename) { int fd; struct stat st; char *buf; struct keyword *head = NULL; struct keyword *tail = NULL; struct files *file; enum keyword_t type; /* keyword type to push */ struct state s = { false, false, '\0'}; char *bufname; char path[PATH_MAX]; size_t i, j; int line; bool error = false; if(stat(filename, &st) == -1 || (fd = open(filename, O_RDONLY)) == -1) { warnxl("%s", filename); return NULL; } if(!st.st_size) { warnxl("%s: empty file", filename); close(fd); return NULL; } if(!realpath(filename, path)) { warnxl("%s", filename); close(fd); return NULL; } buf = xmalloc(1, st.st_size + 1); if(read(fd, buf, st.st_size) == -1) { warnxl("%s", filename); free(buf); close(fd); return NULL; } buf[st.st_size] = '\0'; file = xcalloc(1, sizeof(*file)); bufname = xcalloc(BUFSIZ, sizeof(*bufname)); file->name = strdup(path); file->parent = NULL; for(i = j = 0, line = 1; i < (size_t)st.st_size; ++i) { if(!head && tail) head = tail; if(buf[i] == '\n' && s.comment) { line++; s.comment = false; continue; } if(buf[i] == '#' && !s.quote) { s.comment = true; continue; } if(s.comment) continue; /* end of quotted string */ if(s.quote && buf[i] == s.quote_char) { PUSH_KEYWORD(WORD); s.quote = false; continue; } if(!s.quote) { if((buf[i] == '"' || buf[i] == '\'')) { PUSH_KEYWORD(WORD); /* begin quotted string */ s.quote_char = buf[i]; s.quote = true; continue; } if(buf[i] == '[') { PUSH_KEYWORD(WORD); if(buf[i + 1] == '/') { i += 2; type = SEC_END; } else { ++i; type = SEC_START; } /* get section name */ while(buf[i] != ']') { if(i >= ((size_t)st.st_size-1) || j >= (BUFSIZ-1)) { bufname[j] = '\0'; syntax(NULL, "word too long in %s:%d near '%s'", file->name, line, bufname); error = true; break; } bufname[j++] = buf[i++]; } PUSH_KEYWORD(type); continue; } if(buf[i] == '{') { PUSH_KEYWORD(WORD); PUSH_KEYWORD(LIST_START); continue; } if(buf[i] == '}') { PUSH_KEYWORD(WORD); PUSH_KEYWORD(LIST_END); continue; } if(buf[i] == ',') { PUSH_KEYWORD(WORD); continue; } if(buf[i] == '=') { PUSH_KEYWORD(WORD); PUSH_KEYWORD(EQUAL); continue; } if(strchr("\t\n ", buf[i])) { PUSH_KEYWORD(WORD); if(buf[i] == '\n') ++line; continue; } } if(j >= (BUFSIZ - 1)) { bufname[j] = '\0'; syntax(NULL, "word too long in %s:%d near '%s'", file->name, line, bufname); error = true; break; } bufname[j++] = buf[i]; } free(buf); free(bufname); close(fd); warnxl("%s read", file->name); return (error ? NULL: head); } /* * return NULL on failure and head->next if * no config found (of file doesn't exist) * NOTE to devs: head->name is the file to include */ static struct keyword * include(struct keyword *head) { struct keyword *kw; struct keyword *tail; struct files *file; struct passwd *user; char *filename = NULL; char *base = NULL; head = head->next; if(!head || head->type != WORD) { syntax(head, "missing filename to include"); return NULL; } /* replace ~ by user directory */ if(head->name && head->name[0] == '~') { if((user = getpwuid(getuid())) && user->pw_dir) xasprintf(&filename, "%s%s", user->pw_dir, head->name + 1); else if(getenv("HOME")) xasprintf(&filename, "%s%s", getenv("HOME"), head->name + 1); else /* to warning ? */ filename = head->name; } /* relative path from parent file */ else if(head->name && head->name[0] != '/') { base = strdup(head->file->name); xasprintf(&filename, "%s/%s", dirname(base), head->name); free(base); } else filename = head->name; if(!(kw = parse_keywords(filename))) { warnxl("no config found in include file %s", head->name); if(filename != head->name) free(filename); return NULL; } kw->file->parent = head->file; /* detect circular include */ for(file = kw->file->parent; file != NULL; file = file->parent) if(!strcmp(file->name, kw->file->name)) { syntax(kw, "circular include of %s", kw->file->name); if(filename != head->name) free(filename); return NULL; } if(filename != head->name) free(filename); head = head->next; if(kw) { for(tail = kw; tail->next; tail = tail->next); tail->next = head; } return kw; } static void * free_opt(struct conf_opt *o) { free(o); return NULL; } static struct conf_opt * get_option(struct keyword **head) { struct conf_opt *o; size_t j = 0; struct keyword *kw = *head; o = xcalloc(1, sizeof(*o)); o->name = kw->name; o->used = false; o->line = kw->line; o->filename = kw->file->name; kw = kw->next; if(kw->type != EQUAL) { syntax(kw, "missing '=' here"); return free_opt(o); } if(!(kw = kw->next)) { syntax(kw, "missing value"); return free_opt(o); } switch(kw->type) { case INCLUDE: if(!(kw = include(kw))) return free_opt(o); break; case WORD: o->val[0] = kw->name; o->val[1] = NULL; kw = kw->next; break; case LIST_START: kw = kw->next; while(kw && kw->type != LIST_END) { switch(kw->type) { case WORD: if(j >= (PARSE_MAX_LIST - 1)) { syntax(kw, "too much values in list"); return free_opt(o); } o->val[j++] = kw->name; kw = kw->next; break; case INCLUDE: if(!(kw = include(kw))) return free_opt(o); break; default: syntax(kw, "declaration into a list"); return free_opt(o); break; } } if(!kw) { syntax(kw, "list unclosed"); return free_opt(o); } kw = kw->next; break; default: syntax(kw, "missing value"); return free_opt(o); break; } *head = kw; return o; } static void * free_sec(struct conf_sec *sec) { struct conf_opt *o; struct conf_sec *s; if(sec) { while(!SLIST_EMPTY(&sec->optlist)) { o = SLIST_FIRST(&sec->optlist); SLIST_REMOVE_HEAD(&sec->optlist, entry); free_opt(o); } while(!TAILQ_EMPTY(&sec->sub)) { s = TAILQ_FIRST(&sec->sub); TAILQ_REMOVE(&sec->sub, s, entry); free_sec(s); } free(sec); } return NULL; } static struct conf_sec * get_section(struct keyword **head) { struct conf_sec *s; struct conf_opt *o; struct conf_sec *sub; struct keyword *kw = *head; s = xcalloc(1, sizeof(*s)); s->name = kw->name; TAILQ_INIT(&s->sub); SLIST_INIT(&s->optlist); kw = kw->next; while(kw && kw->type != SEC_END) { switch(kw->type) { case INCLUDE: if(!(kw = include(kw))) return free_sec(s); break; case SEC_START: if(!(sub = get_section(&kw))) return free_sec(s); TAILQ_INSERT_TAIL(&s->sub, sub, entry); ++s->nsub; break; case WORD: if(!(o = get_option(&kw))) return free_sec(s); SLIST_INSERT_HEAD(&s->optlist, o, entry); ++s->nopt; break; default: syntax(kw, "syntax error"); return free_sec(s); break; } } if(!kw || strcmp(kw->name, s->name)) { syntax(kw, "missing end section %s", s->name); return free_sec(s); } kw = kw->next; *head = kw; return s; } int free_conf(void) { struct conf_sec *s; struct keyword *kw, *nkw; struct files **f = NULL; int i, nf = 0; while(!TAILQ_EMPTY(&config)) { s = TAILQ_FIRST(&config); TAILQ_REMOVE(&config, s, entry); free_sec(s); } kw = keywords; while(kw) { nkw = kw->next; free(kw->name); for(i = 0; i < nf; ++i) if(f[i] == kw->file) { if(!(f = realloc(f, sizeof(*f) * (++i)))) errl(EXIT_FAILURE, "realloc"); f[i - 1] = kw->file; } kw = nkw; } if(nf > 0) { for(i = 0; i < nf; ++i) { free(f[i]->name); free(f[i]); } free(f); } return -1; } int get_conf(const char *filename) { struct conf_sec *s; struct keyword *head, *kw; head = kw = parse_keywords(filename); if(!head) return -1; /* TODO ERREUR */ keywords = head; TAILQ_INIT(&config); while(kw) { switch(kw->type) { case INCLUDE: if(!(kw = include(kw))) return free_conf(); break; case SEC_START: if(!(s = get_section(&kw))) return free_conf(); TAILQ_INSERT_TAIL(&config, s, entry); break; default: syntax(kw, "out of any section"); return free_conf(); break; } } return 0; }