2 * Copyright (c) 2013, Olivier MATZ <zer0@droids-corp.org>
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are met:
7 * * Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * * Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 * * Neither the name of the University of California, Berkeley nor the
13 * names of its contributors may be used to endorse or promote products
14 * derived from this software without specific prior written permission.
16 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
17 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
20 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35 #include <sys/param.h>
36 #include <sys/queue.h>
39 #include "cfzy_string.h"
40 #include "cfzy_expr.h"
41 #include "cfzy_htable.h"
42 #include "cfzy_conftree.h"
43 #include "cfzy_confnode.h"
44 #include "cfzy_conftree_parser.h"
46 #define LOG(level, fmt, args...) \
47 CFZY_LOG("conftree_parser", level, fmt, ##args)
50 * Structure that stores the state of the configuration tree parser.
52 struct conftree_parse_status {
53 const char *filename; /* name of file beeing parsed */
54 FILE *f; /* pointer to file beeing parsed */
55 int linenum; /* current line number in file */
56 char *curline; /* pointer to current line, or
57 NULL if no current line */
58 struct cfzy_token_list *tklist; /* pointer to current token
59 list of NULL if no current line */
60 struct cfzy_confnode *parent; /* current parent node */
63 static int __cfzy_conftree_parse(const char *filename,
64 struct cfzy_confnode **pparent);
66 /* return true if line is empty or contains only spaces/comments */
67 static int line_is_empty(const char *buf)
69 while (*buf != '\0' && *buf != '#') {
78 * Add a token in the tklist structure given as argument.
80 * 'buf': pointer to the beginning of line buffer.
81 * 'offset': offset in the line where start parsing
83 * Return the number of consumed bytes if a token is found
84 * Return 0 if there is no more token
88 append_token(struct cfzy_token_list *tklist, const char *buf, int offset)
90 struct cfzy_token *tok;
92 unsigned len, space_len = 0;
97 while (*s != '\0' && isspace(*s))
99 if (*s == '\0' || *s == '#')
102 tok = malloc(sizeof(struct cfzy_token));
104 LOG(ERR, "not enough memory\n");
108 space_len = s - buf - offset;
109 tok->offset = s - buf;
111 /* if it's a quote, unquote the string */
112 if (*s == '"' || *s == '\'') {
113 retbuf = cfzy_string_unquote(s, &len);
117 /* else wait a space */
120 if (retbuf == NULL) {
121 LOG(ERR, "not enough memory\n");
126 s[len] != '\0' && !isspace(s[len]) && s[len] != '#';
133 /* fill token structure and append it in tklist */
135 TAILQ_INSERT_TAIL(&tklist->list, tok, next);
138 return len + space_len;
145 /* free a token list previously allocated with tokenize() */
146 static void free_token_list(struct cfzy_token_list *tklist)
148 struct cfzy_token *tok;
150 while ((tok = TAILQ_FIRST(&tklist->list)) != NULL) {
151 TAILQ_REMOVE(&tklist->list, tok, next);
159 * Parse the line contained in buffer 'linebuf'. This function fills a
160 * tklist structure, composed of several tokens.
161 * Return the tklist pointer on success or NULL on error.
163 static struct cfzy_token_list *tokenize(const char *linebuf)
165 struct cfzy_token_list *tklist;
168 tklist = malloc(sizeof(struct cfzy_token_list));
169 if (tklist == NULL) {
170 LOG(ERR, "not enough memory\n");
174 memset(tklist, 0, sizeof(struct cfzy_token_list));
175 TAILQ_INIT(&tklist->list);
177 /* read tokens from buf and append it to tklist structure */
179 ret = append_token(tklist, linebuf, offset);
181 free_token_list(tklist);
187 tklist->linebuf = linebuf;
191 /* duplicate buf, replacing variables like $(VAR) by their values */
192 char *replace_variables(const char *buf)
194 char *s, *buf_dup, *var;
199 buf_dup = strdup(buf);
200 if (buf_dup == NULL) {
201 LOG(ERR, "not enough memory\n");
207 for ( ; s[0] != '\0' ; s++) {
212 for ( ; s[0] != '\0' && (s[0] != '$' || s[1] != '('); s++);
215 if (cfzy_string_asprintf(&out, "%s", start) < 0)
224 for ( ; s[0] != '\0' && s[0] != ')'; s++);
233 if (cfzy_string_asprintf(&out, var) < 0)
250 * Fill state structure with current line. If state->curline is
251 * already set, the function does nothing, else it will read a new
252 * line in the file and tokenize it. Return 0 on success, and -1 on
253 * error. If state->curline is still NULL after a call to this
254 * function, we reached the end of the file.
256 static int get_curline(struct conftree_parse_status *state)
263 if (state->curline != NULL)
266 state->curline = fgets(buf, sizeof(buf), state->f);
269 if (state->curline == NULL) {
275 line_len = strlen(state->curline);
277 /* if line ends with '\', get another line */
278 while (line_len >= 2 &&
279 state->curline[line_len - 1] == '\n' &&
280 state->curline[line_len - 2] == '\\') {
281 if (line_len >= (int)sizeof(buf) - 1)
284 line = fgets(buf + line_len - 2,
285 sizeof(buf) - line_len - 2, state->f);
286 if (line == NULL) /* EOF */
289 line_len = strlen(state->curline);
293 state->curline = replace_variables(buf);
294 if (state->curline == NULL) {
295 LOG(ERR, "Cannot eval variables\n");
299 state->tklist = tokenize(state->curline);
300 if (state->tklist == NULL) {
301 LOG(ERR, "Cannot parse line\n");
302 free(state->curline);
310 * This function is called when we processed the current line. It will
311 * free allocated memory (token list) and set state->curline to NULL.
313 static void eat_curline(struct conftree_parse_status *state)
315 if (state->curline != NULL) {
316 free(state->curline);
317 state->curline = NULL;
319 if (state->tklist != NULL) {
320 free_token_list(state->tklist);
321 state->tklist = NULL;
325 /* Try to parse node help. Return NO_MATCH if the current line does
326 * not match the 'help' token, SUCCESS if help is successfully parsed,
327 * or ERROR on error. */
328 static enum cfzy_parse_return
329 parse_help(struct cfzy_confnode *n, struct conftree_parse_status *state)
331 struct cfzy_token_list *tklist;
332 struct cfzy_token *tok;
335 if (get_curline(state) < 0)
338 buf = state->curline;
344 /* we want 1 token */
345 tklist = state->tklist;
346 if (tklist->n_token != 1)
349 /* we want a "help" token */
350 tok = TAILQ_FIRST(&tklist->list);
351 if (strcmp(tok->str, "help") && strcmp(tok->str, "---help---"))
354 LOG(INFO, "parse help\n");
357 /* parse help content */
361 if (get_curline(state) < 0)
364 buf = state->curline;
370 /* skip empty lines */
371 if (line_is_empty(buf)) {
376 /* if line does not start with space, this is this end
381 /* append string in buffer, stripping starting spaces */
382 tok = TAILQ_FIRST(&state->tklist->list);
383 if (cfzy_string_asprintf(&n->help, "%s",
384 buf + tok->offset) < 0)
391 /* Try to parse the "source" command. Return NO_MATCH if the current
392 * line does not match the "source" token, SUCCESS if the file is
393 * successfully sourced, or ERROR on error. */
394 static enum cfzy_parse_return source_file(struct conftree_parse_status *state)
396 struct cfzy_token_list *tklist;
397 struct cfzy_token *tok;
399 if (get_curline(state) < 0)
403 if (state->curline == NULL)
406 tklist = state->tklist;
407 tok = TAILQ_FIRST(&tklist->list);
409 /* syntax is: source FILE_NAME */
410 if (tklist->n_token != 2 || strcmp(tok->str, "source"))
413 tok = TAILQ_NEXT(tok, next);
414 LOG(INFO, "source <%s>\n", tok->str);
417 if (__cfzy_conftree_parse(tok->str, &state->parent) < 0) {
418 LOG(ERR, "error in sourced file\n");
426 /* Try to create a new node command ("config", "menuconfig", "choice",
427 * "if", ...). Return NO_MATCH if the current line is not a new node
428 * command, SUCCESS if a new node is created, or ERROR on error. This
429 * function does not parse the attributes of the node but only the
431 static enum cfzy_parse_return
432 new_node(struct conftree_parse_status *state, struct cfzy_confnode **nodep)
434 struct cfzy_token_list *tklist;
435 enum cfzy_parse_return ret;
437 if (get_curline(state) < 0)
441 if (state->curline == NULL)
444 tklist = state->tklist;
446 *nodep = cfzy_confnode_alloc(state->parent->conftree);
447 if (*nodep == NULL) {
448 LOG(ERR, "cannot allocate node\n");
451 (*nodep)->parent = state->parent;
453 ret = cfzy_confnode_choice_new(*nodep, tklist);
456 ret = cfzy_confnode_choiceconfig_new(*nodep, tklist);
459 ret = cfzy_confnode_comment_new(*nodep, tklist);
462 ret = cfzy_confnode_config_new(*nodep, tklist);
465 ret = cfzy_confnode_if_new(*nodep, tklist);
468 ret = cfzy_confnode_intconfig_new(*nodep, tklist);
471 ret = cfzy_confnode_menu_new(*nodep, tklist);
474 ret = cfzy_confnode_menuconfig_new(*nodep, tklist);
477 ret = cfzy_confnode_strconfig_new(*nodep, tklist);
481 cfzy_confnode_free(*nodep);
485 /* Try to create a new node with all its attributes and help. Return
486 * NO_MATCH if the current line is not a new node command, SUCCESS if
487 * a new node is created, or ERROR on error. */
488 static enum cfzy_parse_return
489 parse_confnode(struct conftree_parse_status *state)
492 struct cfzy_confnode *n;
493 enum cfzy_parse_return ret;
494 struct cfzy_token_list *tklist;
496 if (get_curline(state) < 0)
499 buf = state->curline;
505 /* line must not start with a space */
509 /* if it's a new node command */
510 ret = new_node(state, &n);
511 if (ret == ERROR || ret == NO_MATCH)
514 /* save parsing infos */
515 n->filename = strdup(state->filename);
516 if (n->filename == NULL) {
517 cfzy_confnode_free(n);
520 n->linenum = state->linenum;
522 TAILQ_INSERT_TAIL(&state->parent->children, n, child_next);
525 /* parse node attributes */
528 /* fill state structure with current or next line */
529 if (get_curline(state) < 0)
532 buf = state->curline;
533 tklist = state->tklist;
539 /* skip empty lines */
540 if (line_is_empty(buf)) {
545 /* if line does not start with space, this node is
550 /* check if it's a node attribute */
551 ret = cfzy_confnode_add_attr(n, tklist);
554 if (ret == SUCCESS) {
559 /* check if it's a help indication */
560 ret = parse_help(n, state);
566 /* the attribute is invalid */
567 LOG(ERR, "invalid node attribute\n");
571 /* enter in a new node */
572 if (n->flags & CFZY_F_IS_DIR)
578 /* Try to match the command that closes the current parent node, if
579 * it's a directory. Return NO_MATCH if the line does not match this
580 * command, SUCCESS if the parent node is closed (in this case,
581 * state->parent is updated), or ERROR on error. */
582 static enum cfzy_parse_return
583 close_dir(struct conftree_parse_status *state)
585 struct cfzy_token_list *tklist;
586 struct cfzy_token *tok;
587 struct cfzy_confnode *n;
590 if ((n->flags & CFZY_F_IS_DIR) == 0)
593 /* fill state structure with current or next line */
594 if (get_curline(state) < 0)
598 if (state->curline == NULL)
601 tklist = state->tklist;
602 if (tklist->n_token != 1)
605 /* token must be "endchoice", "endmenu", "endif", ... */
606 tok = TAILQ_FIRST(&tklist->list);
607 if (strncmp(tok->str, "end", 3))
609 if (strcmp(tok->str + 3, cfzy_confnode_get_type_str(n)))
611 if (cfzy_confnode_close(n) < 0) {
612 LOG(ERR, "cannot close node\n");
616 state->parent = n->parent;
621 * Parse the configuration tree by reading the file stored in the
622 * 'state' structure. Return SUCCESS or ERROR.
624 static enum cfzy_parse_return parse_conftree(struct conftree_parse_status *state)
626 enum cfzy_parse_return ret;
629 /* browse all lines of the file */
632 /* fill state structure with current or next line */
633 if (get_curline(state) < 0)
636 buf = state->curline;
642 LOG(DEBUG, "parent=%s: %s",
643 cfzy_confnode_name(state->parent), buf);
645 /* skip empty / comments lines */
646 if (line_is_empty(buf)) {
651 /* if the line does not start with a space, syntax error */
653 LOG(ERR, "line starts with space\n");
657 /* if it's a source command, parse the new file */
658 ret = source_file(state);
664 /* if the command closes a menu, choice, ... */
665 ret = close_dir(state);
668 if (ret == SUCCESS) {
674 ret = parse_confnode(state);
680 /* invalid command */
681 LOG(ERR, "invalid command\n");
689 * Parse the configuration tree by reading "filename", knowing that
690 * the current parent node is pointed by pparent (which can be
691 * modified at the end of the function). This function will fill open
692 * the file, and fill a state structure for the parser, then call
693 * parse_conftree(). Return 0 on success and -1 on error.
696 __cfzy_conftree_parse(const char *filename, struct cfzy_confnode **pparent)
698 struct conftree_parse_status state;
700 char old_cwd_buf[PATH_MAX];
701 char *cwd_buf = NULL, *cwd_ptr = NULL, *old_cwd = NULL;
704 memset(&state, 0, sizeof(state));
706 old_cwd = getcwd(old_cwd_buf, sizeof(old_cwd_buf));
707 if (old_cwd == NULL) {
708 LOG(ERR, "getcwd failed: %s\n", strerror(errno));
712 f = fopen(filename, "r");
714 LOG(ERR, "cannot find file <%s>\n", filename);
718 cwd_buf = strdup(filename);
719 if (cwd_buf == NULL) {
720 LOG(ERR, "not enough memory\n");
723 cwd_ptr = dirname(cwd_buf);
724 ret = chdir(cwd_ptr);
726 LOG(ERR, "chdir failed: %s\n", strerror(errno));
730 state.filename = filename;
733 state.parent = *pparent;
734 state.curline = NULL;
736 /* browse all lines of the file */
737 ret = parse_conftree(&state);
738 if (ret != SUCCESS) {
739 /* node will be freed by caller (cfzy_conftree_new) */
740 LOG(ERR, "parse error at %s:%d\n", filename, state.linenum);
741 LOG(ERR, ">>> %s", state.curline);
745 eat_curline(&state); /* will free token_list */
748 if (chdir(old_cwd) < 0) {
749 LOG(ERR, "chdir back to <%s> failed: %s\n",
750 old_cwd, strerror(errno));
754 *pparent = state.parent;
759 eat_curline(&state); /* will free token_list */
765 if (chdir(old_cwd) < 0)
766 LOG(ERR, "chdir back to <%s> failed: %s\n",
767 old_cwd, strerror(errno));
772 /* External api for parsing a new configuration tree file. Return 0 on
773 * success and -1 on error. */
774 int cfzy_conftree_parse(const char *filename, struct cfzy_conftree *conftree)
777 struct cfzy_confnode *parent = conftree->root;
779 ret = __cfzy_conftree_parse(filename, &parent);
783 if (parent != conftree->root) {
784 LOG(ERR, "Node <%s> not closed properly\n",
785 cfzy_confnode_name(parent));