X-Git-Url: https://git.sur5r.net/?p=i3%2Fi3;a=blobdiff_plain;f=src%2Fconfig_parser.c;h=9f972fed8f80b4510931cac6edb52cef431c832a;hp=ea00412d78b2d2533c5dd0b87486aaa3da7e9f4e;hb=HEAD;hpb=96e1b80371b985d4f67b36e6cb48e61b5fb83995 diff --git a/src/config_parser.c b/src/config_parser.c index ea00412d..9f972fed 100644 --- a/src/config_parser.c +++ b/src/config_parser.c @@ -1,5 +1,3 @@ -#undef I3__FILE__ -#define I3__FILE__ "config_parser.c" /* * vim:ts=4:sw=4:expandtab * @@ -25,6 +23,8 @@ * nearest token. * */ +#include "all.h" + #include #include #include @@ -35,13 +35,14 @@ #include #include #include - -#include "all.h" +#include // Macros to make the YAJL API a bit easier to use. #define y(x, ...) yajl_gen_##x(command_output.json_gen, ##__VA_ARGS__) #define ystr(str) yajl_gen_string(command_output.json_gen, (unsigned char *)str, strlen(str)) +xcb_xrm_database_t *database = NULL; + #ifndef TEST_PARSER pid_t config_error_nagbar_pid = -1; static struct context *context; @@ -122,7 +123,7 @@ static void push_string(const char *identifier, const char *str) { /* When we arrive here, the stack is full. This should not happen and * means there’s either a bug in this parser or the specification * contains a command with more than 10 identified tokens. */ - fprintf(stderr, "BUG: commands_parser stack full. This means either a bug " + fprintf(stderr, "BUG: config_parser stack full. This means either a bug " "in the code, or a new command which contains more than " "10 identified tokens.\n"); exit(1); @@ -142,7 +143,7 @@ static void push_long(const char *identifier, long num) { /* When we arrive here, the stack is full. This should not happen and * means there’s either a bug in this parser or the specification * contains a command with more than 10 identified tokens. */ - fprintf(stderr, "BUG: commands_parser stack full. This means either a bug " + fprintf(stderr, "BUG: config_parser stack full. This means either a bug " "in the code, or a new command which contains more than " "10 identified tokens.\n"); exit(1); @@ -170,7 +171,7 @@ static long get_long(const char *identifier) { static void clear_stack(void) { for (int c = 0; c < 10; c++) { - if (stack[c].type == STACK_STR && stack[c].val.str != NULL) + if (stack[c].type == STACK_STR) free(stack[c].val.str); stack[c].identifier = NULL; stack[c].val.str = NULL; @@ -178,53 +179,6 @@ static void clear_stack(void) { } } -// TODO: remove this if it turns out we don’t need it for testing. -#if 0 -/******************************************************************************* - * A dynamically growing linked list which holds the criteria for the current - * command. - ******************************************************************************/ - -typedef struct criterion { - char *type; - char *value; - - TAILQ_ENTRY(criterion) criteria; -} criterion; - -static TAILQ_HEAD(criteria_head, criterion) criteria = - TAILQ_HEAD_INITIALIZER(criteria); - -/* - * Stores the given type/value in the list of criteria. - * Accepts a pointer as first argument, since it is 'call'ed by the parser. - * - */ -static void push_criterion(void *unused_criteria, const char *type, - const char *value) { - struct criterion *criterion = smalloc(sizeof(struct criterion)); - criterion->type = sstrdup(type); - criterion->value = sstrdup(value); - TAILQ_INSERT_TAIL(&criteria, criterion, criteria); -} - -/* - * Clears the criteria linked list. - * Accepts a pointer as first argument, since it is 'call'ed by the parser. - * - */ -static void clear_criteria(void *unused_criteria) { - struct criterion *criterion; - while (!TAILQ_EMPTY(&criteria)) { - criterion = TAILQ_FIRST(&criteria); - free(criterion->type); - free(criterion->value); - TAILQ_REMOVE(&criteria, criterion, criteria); - free(criterion); - } -} -#endif - /******************************************************************************* * The parser itself. ******************************************************************************/ @@ -281,7 +235,7 @@ static void next_state(const cmdp_token *token) { * */ static const char *start_of_line(const char *walk, const char *beginning) { - while (*walk != '\n' && *walk != '\r' && walk >= beginning) { + while (walk >= beginning && *walk != '\n' && *walk != '\r') { walk--; } @@ -789,7 +743,7 @@ static char *migrate_config(char *input, off_t size) { /* read the script’s output */ int conv_size = 65535; - char *converted = smalloc(conv_size); + char *converted = scalloc(conv_size, 1); int read_bytes = 0, ret; do { if (read_bytes == conv_size) { @@ -810,6 +764,7 @@ static char *migrate_config(char *input, off_t size) { wait(&status); if (!WIFEXITED(status)) { fprintf(stderr, "Child did not terminate normally, using old config file (will lead to broken behaviour)\n"); + FREE(converted); return NULL; } @@ -824,24 +779,115 @@ static char *migrate_config(char *input, off_t size) { fprintf(stderr, "# i3 config file (v4)\n"); /* TODO: nag the user with a message to include a hint for i3 in their config file */ } + FREE(converted); return NULL; } return converted; } +/** + * Launch nagbar to indicate errors in the configuration file. + */ +void start_config_error_nagbar(const char *configpath, bool has_errors) { + char *editaction, *pageraction; + sasprintf(&editaction, "i3-sensible-editor \"%s\" && i3-msg reload\n", configpath); + sasprintf(&pageraction, "i3-sensible-pager \"%s\"\n", errorfilename); + char *argv[] = { + NULL, /* will be replaced by the executable path */ + "-f", + (config.font.pattern ? config.font.pattern : "fixed"), + "-t", + (has_errors ? "error" : "warning"), + "-m", + (has_errors ? "You have an error in your i3 config file!" : "Your config is outdated. Please fix the warnings to make sure everything works."), + "-b", + "edit config", + editaction, + (errorfilename ? "-b" : NULL), + (has_errors ? "show errors" : "show warnings"), + pageraction, + NULL}; + + start_nagbar(&config_error_nagbar_pid, argv); + free(editaction); + free(pageraction); +} + +/* + * Inserts or updates a variable assignment depending on whether it already exists. + * + */ +static void upsert_variable(struct variables_head *variables, char *key, char *value) { + struct Variable *current; + SLIST_FOREACH(current, variables, variables) { + if (strcmp(current->key, key) != 0) { + continue; + } + + DLOG("Updated variable: %s = %s -> %s\n", key, current->value, value); + FREE(current->value); + current->value = sstrdup(value); + return; + } + + DLOG("Defined new variable: %s = %s\n", key, value); + struct Variable *new = scalloc(1, sizeof(struct Variable)); + struct Variable *test = NULL, *loc = NULL; + new->key = sstrdup(key); + new->value = sstrdup(value); + /* ensure that the correct variable is matched in case of one being + * the prefix of another */ + SLIST_FOREACH(test, variables, variables) { + if (strlen(new->key) >= strlen(test->key)) + break; + loc = test; + } + + if (loc == NULL) { + SLIST_INSERT_HEAD(variables, new, variables); + } else { + SLIST_INSERT_AFTER(loc, new, variables); + } +} + +static char *get_resource(char *name) { + if (conn == NULL) { + return NULL; + } + + /* Load the resource database lazily. */ + if (database == NULL) { + database = xcb_xrm_database_from_default(conn); + + if (database == NULL) { + ELOG("Failed to open the resource database.\n"); + + /* Load an empty database so we don't keep trying to load the + * default database over and over again. */ + database = xcb_xrm_database_from_string(""); + + return NULL; + } + } + + char *resource; + xcb_xrm_resource_get_string(database, name, NULL, &resource); + return resource; +} + /* * Parses the given file by first replacing the variables, then calling * parse_config and possibly launching i3-nagbar. * */ bool parse_file(const char *f, bool use_nagbar) { - SLIST_HEAD(variables_head, Variable) variables = SLIST_HEAD_INITIALIZER(&variables); + struct variables_head variables = SLIST_HEAD_INITIALIZER(&variables); int fd; struct stat stbuf; char *buf; FILE *fstr; - char buffer[4096], key[512], value[512], *continuation = NULL; + char buffer[4096], key[512], value[4096], *continuation = NULL; if ((fd = open(f, O_RDONLY)) == -1) die("Could not open configuration file: %s\n", strerror(errno)); @@ -854,6 +900,15 @@ bool parse_file(const char *f, bool use_nagbar) { if ((fstr = fdopen(fd, "r")) == NULL) die("Could not fdopen: %s\n", strerror(errno)); + FREE(current_config); + current_config = scalloc(stbuf.st_size + 1, 1); + if ((ssize_t)fread(current_config, 1, stbuf.st_size, fstr) != stbuf.st_size) { + die("Could not fread: %s\n", strerror(errno)); + } + rewind(fstr); + + bool invalid_sets = false; + while (!feof(fstr)) { if (!continuation) continuation = buffer; @@ -862,51 +917,94 @@ bool parse_file(const char *f, bool use_nagbar) { break; die("Could not read configuration file\n"); } - if (buffer[strlen(buffer) - 1] != '\n') { + if (buffer[strlen(buffer) - 1] != '\n' && !feof(fstr)) { ELOG("Your line continuation is too long, it exceeds %zd bytes\n", sizeof(buffer)); } + + /* sscanf implicitly strips whitespace. */ + value[0] = '\0'; + const bool skip_line = (sscanf(buffer, "%511s %4095[^\n]", key, value) < 1 || strlen(key) < 3); + const bool comment = (key[0] == '#'); + value[4095] = '\n'; + continuation = strstr(buffer, "\\\n"); if (continuation) { - continue; + if (!comment) { + continue; + } + DLOG("line continuation in comment is ignored: \"%.*s\"\n", (int)strlen(buffer) - 1, buffer); + continuation = NULL; } strncpy(buf + strlen(buf), buffer, strlen(buffer) + 1); - /* sscanf implicitly strips whitespace. Also, we skip comments and empty lines. */ - if (sscanf(buffer, "%511s %511[^\n]", key, value) < 1 || - key[0] == '#' || strlen(key) < 3) + /* Skip comments and empty lines. */ + if (skip_line || comment) { continue; + } - if (strcasecmp(key, "set") == 0) { - if (value[0] != '$') { + if (strcasecmp(key, "set") == 0 && *value != '\0') { + char v_key[512]; + char v_value[4096] = {'\0'}; + + if (sscanf(value, "%511s %4095[^\n]", v_key, v_value) < 1) { + ELOG("Failed to parse variable specification '%s', skipping it.\n", value); + invalid_sets = true; + continue; + } + + if (v_key[0] != '$') { ELOG("Malformed variable assignment, name has to start with $\n"); + invalid_sets = true; continue; } - /* get key/value for this variable */ - char *v_key = value, *v_value; - if (strstr(value, " ") == NULL && strstr(value, "\t") == NULL) { - ELOG("Malformed variable assignment, need a value\n"); + upsert_variable(&variables, v_key, v_value); + continue; + } else if (strcasecmp(key, "set_from_resource") == 0) { + char res_name[512] = {'\0'}; + char v_key[512]; + char fallback[4096] = {'\0'}; + + /* Ensure that this string is terminated. For example, a user might + * want a variable to be empty if the resource can't be found and + * uses + * set_from_resource $foo i3wm.foo + * Without explicitly terminating the string first, sscanf() will + * leave it uninitialized, causing garbage in the config.*/ + fallback[0] = '\0'; + + if (sscanf(value, "%511s %511s %4095[^\n]", v_key, res_name, fallback) < 1) { + ELOG("Failed to parse resource specification '%s', skipping it.\n", value); + invalid_sets = true; continue; } - if (!(v_value = strstr(value, " "))) - v_value = strstr(value, "\t"); + if (v_key[0] != '$') { + ELOG("Malformed variable assignment, name has to start with $\n"); + invalid_sets = true; + continue; + } - *(v_value++) = '\0'; - while (*v_value == '\t' || *v_value == ' ') - v_value++; + char *res_value = get_resource(res_name); + if (res_value == NULL) { + DLOG("Could not get resource '%s', using fallback '%s'.\n", res_name, fallback); + res_value = sstrdup(fallback); + } - struct Variable *new = scalloc(1, sizeof(struct Variable)); - new->key = sstrdup(v_key); - new->value = sstrdup(v_value); - SLIST_INSERT_HEAD(&variables, new, variables); - DLOG("Got new variable %s = %s\n", v_key, v_value); + upsert_variable(&variables, v_key, res_value); + FREE(res_value); continue; } } fclose(fstr); + if (database != NULL) { + xcb_xrm_database_free(database); + /* Explicitly set the database to NULL again in case the config gets reloaded. */ + database = NULL; + } + /* For every custom variable, see how often it occurs in the file and * how much extra bytes it requires when replaced. */ struct Variable *current, *nearest; @@ -920,7 +1018,7 @@ bool parse_file(const char *f, bool use_nagbar) { char *next; for (next = bufcopy; next < (bufcopy + stbuf.st_size) && - (next = strcasestr(next, current->key)) != NULL; + (next = strcasestr(next, current->key)) != NULL; next += strlen(current->key)) { *next = '_'; extra_bytes += extra; @@ -929,9 +1027,9 @@ bool parse_file(const char *f, bool use_nagbar) { FREE(bufcopy); /* Then, allocate a new buffer and copy the file over to the new one, - * but replace occurences of our variables */ + * but replace occurrences of our variables */ char *walk = buf, *destwalk; - char *new = smalloc(stbuf.st_size + extra_bytes + 1); + char *new = scalloc(stbuf.st_size + extra_bytes + 1, 1); destwalk = new; while (walk < (buf + stbuf.st_size)) { /* Find the next variable */ @@ -967,7 +1065,7 @@ bool parse_file(const char *f, bool use_nagbar) { int version = detect_version(buf); if (version == 3) { /* We need to convert this v3 configuration */ - char *converted = migrate_config(new, stbuf.st_size); + char *converted = migrate_config(new, strlen(new)); if (converted != NULL) { ELOG("\n"); ELOG("****************************************************************\n"); @@ -1000,34 +1098,12 @@ bool parse_file(const char *f, bool use_nagbar) { check_for_duplicate_bindings(context); reorder_bindings(); - if (use_nagbar && (context->has_errors || context->has_warnings)) { + if (use_nagbar && (context->has_errors || context->has_warnings || invalid_sets)) { ELOG("FYI: You are using i3 version %s\n", i3_version); if (version == 3) ELOG("Please convert your configfile first, then fix any remaining errors (see above).\n"); - char *editaction, - *pageraction; - sasprintf(&editaction, "i3-sensible-editor \"%s\" && i3-msg reload\n", f); - sasprintf(&pageraction, "i3-sensible-pager \"%s\"\n", errorfilename); - char *argv[] = { - NULL, /* will be replaced by the executable path */ - "-f", - (config.font.pattern ? config.font.pattern : "fixed"), - "-t", - (context->has_errors ? "error" : "warning"), - "-m", - (context->has_errors ? "You have an error in your i3 config file!" : "Your config is outdated. Please fix the warnings to make sure everything works."), - "-b", - "edit config", - editaction, - (errorfilename ? "-b" : NULL), - (context->has_errors ? "show errors" : "show warnings"), - pageraction, - NULL}; - - start_nagbar(&config_error_nagbar_pid, argv); - free(editaction); - free(pageraction); + start_config_error_nagbar(f, context->has_errors || invalid_sets); } bool has_errors = context->has_errors;