diff options
Diffstat (limited to 'src/config_file.c')
-rw-r--r-- | src/config_file.c | 1110 |
1 files changed, 598 insertions, 512 deletions
diff --git a/src/config_file.c b/src/config_file.c index 481c593f4..cbc48bcd9 100644 --- a/src/config_file.c +++ b/src/config_file.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2011 the libgit2 contributors + * Copyright (C) 2009-2012 the libgit2 contributors * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. @@ -12,14 +12,17 @@ #include "buffer.h" #include "git2/config.h" #include "git2/types.h" - +#include "strmap.h" #include <ctype.h> +#include <sys/types.h> +#include <regex.h> + +GIT__USE_STRMAP; typedef struct cvar_t { struct cvar_t *next; - char *section; - char *name; + char *key; /* TODO: we might be able to get rid of this */ char *value; } cvar_t; @@ -69,10 +72,10 @@ typedef struct { typedef struct { git_config_file parent; - cvar_t_list var_list; + git_strmap *values; struct { - git_fbuffer buffer; + git_buf buffer; char *read_ptr; int line_number; int eof; @@ -83,344 +86,379 @@ typedef struct { static int config_parse(diskfile_backend *cfg_file); static int parse_variable(diskfile_backend *cfg, char **var_name, char **var_value); -static int config_write(diskfile_backend *cfg, cvar_t *var); +static int config_write(diskfile_backend *cfg, const char *key, const regex_t *preg, const char *value); + +static void set_parse_error(diskfile_backend *backend, int col, const char *error_str) +{ + giterr_set(GITERR_CONFIG, "Failed to parse config file: %s (in %s:%d, column %d)", + error_str, backend->file_path, backend->reader.line_number, col); +} static void cvar_free(cvar_t *var) { if (var == NULL) return; - git__free(var->section); - git__free(var->name); + git__free(var->key); git__free(var->value); git__free(var); } -static void cvar_list_free(cvar_t_list *list) +/* Take something the user gave us and make it nice for our hash function */ +static int normalize_name(const char *in, char **out) { - cvar_t *cur; + char *name, *fdot, *ldot; + + assert(in && out); + + name = git__strdup(in); + GITERR_CHECK_ALLOC(name); + + fdot = strchr(name, '.'); + ldot = strrchr(name, '.'); - while (!CVAR_LIST_EMPTY(list)) { - cur = CVAR_LIST_HEAD(list); - CVAR_LIST_REMOVE_HEAD(list); - cvar_free(cur); + if (fdot == NULL || ldot == NULL) { + git__free(name); + giterr_set(GITERR_CONFIG, + "Invalid variable name: '%s'", in); + return -1; } + + /* Downcase up to the first dot and after the last one */ + git__strntolower(name, fdot - name); + git__strtolower(ldot); + + *out = name; + return 0; } -/* - * Compare according to the git rules. Section contains the section as - * it's stored internally. query is the full name as would be given to - * 'git config'. - */ -static int cvar_match_section(const char *section, const char *query) +static void free_vars(git_strmap *values) { - const char *sdot, *qdot, *qsub; - size_t section_len; + cvar_t *var = NULL; - sdot = strchr(section, '.'); + if (values == NULL) + return; - /* If the section doesn't have any dots, it's easy */ - if (sdot == NULL) - return !strncasecmp(section, query, strlen(section)); + git_strmap_foreach_value(values, var, + while (var != NULL) { + cvar_t *next = CVAR_LIST_NEXT(var); + cvar_free(var); + var = next; + }); - /* - * If it does have dots, compare the sections - * case-insensitively. The comparison includes the dots. - */ - section_len = sdot - section + 1; - if (strncasecmp(section, query, sdot - section)) - return 0; + git_strmap_free(values); +} + +static int config_open(git_config_file *cfg) +{ + int res; + diskfile_backend *b = (diskfile_backend *)cfg; + + b->values = git_strmap_alloc(); + GITERR_CHECK_ALLOC(b->values); + + git_buf_init(&b->reader.buffer, 0); + res = git_futils_readbuffer(&b->reader.buffer, b->file_path); - qsub = query + section_len; - qdot = strchr(qsub, '.'); - /* Make sure the subsections are the same length */ - if (strlen(sdot + 1) != (size_t) (qdot - qsub)) + /* It's fine if the file doesn't exist */ + if (res == GIT_ENOTFOUND) return 0; - /* The subsection is case-sensitive */ - return !strncmp(sdot + 1, qsub, strlen(sdot + 1)); + if (res < 0 || config_parse(b) < 0) { + free_vars(b->values); + b->values = NULL; + git_buf_free(&b->reader.buffer); + return -1; + } + + git_buf_free(&b->reader.buffer); + return 0; } -static int cvar_match_name(const cvar_t *var, const char *str) +static void backend_free(git_config_file *_backend) { - const char *name_start; + diskfile_backend *backend = (diskfile_backend *)_backend; - if (!cvar_match_section(var->section, str)) { - return 0; - } - /* Early exit if the lengths are different */ - name_start = strrchr(str, '.') + 1; - if (strlen(var->name) != strlen(name_start)) - return 0; + if (backend == NULL) + return; - return !strcasecmp(var->name, name_start); + git__free(backend->file_path); + free_vars(backend->values); + git__free(backend); } -static cvar_t *cvar_list_find(cvar_t_list *list, const char *name) +static int file_foreach(git_config_file *backend, int (*fn)(const char *, const char *, void *), void *data) { - cvar_t *iter; + diskfile_backend *b = (diskfile_backend *)backend; + cvar_t *var; + const char *key; - CVAR_LIST_FOREACH (list, iter) { - if (cvar_match_name(iter, name)) - return iter; - } + if (!b->values) + return 0; + + git_strmap_foreach(b->values, key, var, + do { + if (fn(key, var->value, data) < 0) + break; - return NULL; + var = CVAR_LIST_NEXT(var); + } while (var != NULL); + ); + + return 0; } -static int cvar_normalize_name(cvar_t *var, char **output) +static int config_set(git_config_file *cfg, const char *name, const char *value) { - char *section_sp = strchr(var->section, ' '); - char *quote, *name; - size_t len; - int ret; + cvar_t *var = NULL, *old_var; + diskfile_backend *b = (diskfile_backend *)cfg; + char *key; + khiter_t pos; + int rval; + + if (normalize_name(name, &key) < 0) + return -1; /* - * The final string is going to be at most one char longer than - * the input + * Try to find it in the existing values and update it if it + * only has one value. */ - len = strlen(var->section) + strlen(var->name) + 1; - name = git__malloc(len + 1); - if (name == NULL) - return GIT_ENOMEM; - - /* If there aren't any spaces in the section, it's easy */ - if (section_sp == NULL) { - ret = p_snprintf(name, len + 1, "%s.%s", var->section, var->name); - if (ret < 0) { - git__free(name); - return git__throw(GIT_EOSERR, "Failed to normalize name. OS err: %s", strerror(errno)); + pos = git_strmap_lookup_index(b->values, key); + if (git_strmap_valid_index(b->values, pos)) { + cvar_t *existing = git_strmap_value_at(b->values, pos); + char *tmp = NULL; + + git__free(key); + if (existing->next != NULL) { + giterr_set(GITERR_CONFIG, "Multivar incompatible with simple set"); + return -1; + } + + if (value) { + tmp = git__strdup(value); + GITERR_CHECK_ALLOC(tmp); } - *output = name; - return GIT_SUCCESS; + git__free(existing->value); + existing->value = tmp; + + return config_write(b, existing->key, NULL, value); } - /* - * If there are spaces, we replace the space by a dot, move - * section name so it overwrites the first quotation mark and - * replace the last quotation mark by a dot. We then append the - * variable name. - */ - strcpy(name, var->section); - section_sp = strchr(name, ' '); - *section_sp = '.'; - /* Remove first quote */ - quote = strchr(name, '"'); - memmove(quote, quote+1, strlen(quote+1)); - /* Remove second quote */ - quote = strchr(name, '"'); - *quote = '.'; - strcpy(quote+1, var->name); - - *output = name; - return GIT_SUCCESS; -} + var = git__malloc(sizeof(cvar_t)); + GITERR_CHECK_ALLOC(var); -static char *interiorize_section(const char *orig) -{ - char *dot, *last_dot, *section, *ret; - size_t len; + memset(var, 0x0, sizeof(cvar_t)); - dot = strchr(orig, '.'); - last_dot = strrchr(orig, '.'); - len = last_dot - orig; + var->key = key; + var->value = NULL; - /* No subsection, this is easy */ - if (last_dot == dot) - return git__strndup(orig, dot - orig); + if (value) { + var->value = git__strdup(value); + GITERR_CHECK_ALLOC(var->value); + } - section = git__strndup(orig, len); - if (section == NULL) - return NULL; + if (config_write(b, key, NULL, value) < 0) { + cvar_free(var); + return -1; + } - ret = section; - len = dot - orig; - git__strntolower(section, len); - return ret; + git_strmap_insert2(b->values, key, var, old_var, rval); + if (rval < 0) + return -1; + if (old_var != NULL) + cvar_free(old_var); + + return 0; } -static int config_open(git_config_file *cfg) +/* + * Internal function that actually gets the value in string form + */ +static int config_get(git_config_file *cfg, const char *name, const char **out) { - int error; diskfile_backend *b = (diskfile_backend *)cfg; + char *key; + khiter_t pos; - error = git_futils_readbuffer(&b->reader.buffer, b->file_path); - - /* It's fine if the file doesn't exist */ - if (error == GIT_ENOTFOUND) - return GIT_SUCCESS; - - if (error < GIT_SUCCESS) - goto cleanup; + if (normalize_name(name, &key) < 0) + return -1; - error = config_parse(b); - if (error < GIT_SUCCESS) - goto cleanup; + pos = git_strmap_lookup_index(b->values, key); + git__free(key); - git_futils_freebuffer(&b->reader.buffer); + /* no error message; the config system will write one */ + if (!git_strmap_valid_index(b->values, pos)) + return GIT_ENOTFOUND; - return GIT_SUCCESS; + *out = ((cvar_t *)git_strmap_value_at(b->values, pos))->value; - cleanup: - cvar_list_free(&b->var_list); - git_futils_freebuffer(&b->reader.buffer); - - return git__rethrow(error, "Failed to open config"); + return 0; } -static void backend_free(git_config_file *_backend) +static int config_get_multivar( + git_config_file *cfg, + const char *name, + const char *regex_str, + int (*fn)(const char *, void *), + void *data) { - diskfile_backend *backend = (diskfile_backend *)_backend; + cvar_t *var; + diskfile_backend *b = (diskfile_backend *)cfg; + char *key; + khiter_t pos; - if (backend == NULL) - return; + if (normalize_name(name, &key) < 0) + return -1; - git__free(backend->file_path); - cvar_list_free(&backend->var_list); + pos = git_strmap_lookup_index(b->values, key); + git__free(key); - git__free(backend); -} + if (!git_strmap_valid_index(b->values, pos)) + return GIT_ENOTFOUND; -static int file_foreach(git_config_file *backend, int (*fn)(const char *, const char *, void *), void *data) -{ - int ret = GIT_SUCCESS; - cvar_t *var; - diskfile_backend *b = (diskfile_backend *)backend; + var = git_strmap_value_at(b->values, pos); - CVAR_LIST_FOREACH(&b->var_list, var) { - char *normalized = NULL; + if (regex_str != NULL) { + regex_t regex; + int result; - ret = cvar_normalize_name(var, &normalized); - if (ret < GIT_SUCCESS) - return ret; + /* regex matching; build the regex */ + result = regcomp(®ex, regex_str, REG_EXTENDED); + if (result < 0) { + giterr_set_regex(®ex, result); + return -1; + } - ret = fn(normalized, var->value, data); - git__free(normalized); - if (ret) - break; + /* and throw the callback only on the variables that + * match the regex */ + do { + if (regexec(®ex, var->value, 0, NULL, 0) == 0) { + /* early termination by the user is not an error; + * just break and return successfully */ + if (fn(var->value, data) < 0) + break; + } + + var = var->next; + } while (var != NULL); + regfree(®ex); + } else { + /* no regex; go through all the variables */ + do { + /* early termination by the user is not an error; + * just break and return successfully */ + if (fn(var->value, data) < 0) + break; + + var = var->next; + } while (var != NULL); } - return ret; + return 0; } -static int config_set(git_config_file *cfg, const char *name, const char *value) +static int config_set_multivar( + git_config_file *cfg, const char *name, const char *regexp, const char *value) { - cvar_t *var = NULL; - cvar_t *existing = NULL; - int error = GIT_SUCCESS; - const char *last_dot; + int replaced = 0; + cvar_t *var, *newvar; diskfile_backend *b = (diskfile_backend *)cfg; + char *key; + regex_t preg; + int result; + khiter_t pos; - /* - * If it already exists, we just need to update its value. - */ - existing = cvar_list_find(&b->var_list, name); - if (existing != NULL) { - char *tmp = value ? git__strdup(value) : NULL; - if (tmp == NULL && value != NULL) - return GIT_ENOMEM; + assert(regexp); - git__free(existing->value); - existing->value = tmp; + if (normalize_name(name, &key) < 0) + return -1; - return config_write(b, existing); + pos = git_strmap_lookup_index(b->values, key); + if (!git_strmap_valid_index(b->values, pos)) { + git__free(key); + return GIT_ENOTFOUND; } - /* - * Otherwise, create it and stick it at the end of the queue. If - * value is NULL, we return an error, because you can't delete a - * variable that doesn't exist. - */ + var = git_strmap_value_at(b->values, pos); - if (value == NULL) - return git__throw(GIT_ENOTFOUND, "Can't delete non-exitent variable"); - - last_dot = strrchr(name, '.'); - if (last_dot == NULL) { - return git__throw(GIT_EINVALIDTYPE, "Variables without section aren't allowed"); + result = regcomp(&preg, regexp, REG_EXTENDED); + if (result < 0) { + git__free(key); + giterr_set_regex(&preg, result); + return -1; } - var = git__malloc(sizeof(cvar_t)); - if (var == NULL) - return GIT_ENOMEM; + for (;;) { + if (regexec(&preg, var->value, 0, NULL, 0) == 0) { + char *tmp = git__strdup(value); + GITERR_CHECK_ALLOC(tmp); - memset(var, 0x0, sizeof(cvar_t)); + git__free(var->value); + var->value = tmp; + replaced = 1; + } - var->section = interiorize_section(name); - if (var->section == NULL) { - error = GIT_ENOMEM; - goto out; - } + if (var->next == NULL) + break; - var->name = git__strdup(last_dot + 1); - if (var->name == NULL) { - error = GIT_ENOMEM; - goto out; + var = var->next; } - var->value = value ? git__strdup(value) : NULL; - if (var->value == NULL && value != NULL) { - error = GIT_ENOMEM; - goto out; + /* If we've reached the end of the variables and we haven't found it yet, we need to append it */ + if (!replaced) { + newvar = git__malloc(sizeof(cvar_t)); + GITERR_CHECK_ALLOC(newvar); + + memset(newvar, 0x0, sizeof(cvar_t)); + + newvar->key = git__strdup(var->key); + GITERR_CHECK_ALLOC(newvar->key); + + newvar->value = git__strdup(value); + GITERR_CHECK_ALLOC(newvar->value); + + var->next = newvar; } - CVAR_LIST_APPEND(&b->var_list, var); - error = config_write(b, var); + result = config_write(b, key, &preg, value); - out: - if (error < GIT_SUCCESS) - cvar_free(var); + git__free(key); + regfree(&preg); - return error == GIT_SUCCESS ? GIT_SUCCESS : git__rethrow(error, "Failed to set config value"); + return result; } -/* - * Internal function that actually gets the value in string form - */ -static int config_get(git_config_file *cfg, const char *name, const char **out) +static int config_delete(git_config_file *cfg, const char *name) { cvar_t *var; - int error = GIT_SUCCESS; diskfile_backend *b = (diskfile_backend *)cfg; + char *key; + int result; + khiter_t pos; - var = cvar_list_find(&b->var_list, name); - - if (var == NULL) - return git__throw(GIT_ENOTFOUND, "Variable '%s' not found", name); + if (normalize_name(name, &key) < 0) + return -1; - *out = var->value; + pos = git_strmap_lookup_index(b->values, key); + git__free(key); - return error == GIT_SUCCESS ? GIT_SUCCESS : git__rethrow(error, "Failed to get config value for %s", name); -} + if (!git_strmap_valid_index(b->values, pos)) + return GIT_ENOTFOUND; -static int config_delete(git_config_file *cfg, const char *name) -{ - int error; - cvar_t *iter, *prev = NULL; - diskfile_backend *b = (diskfile_backend *)cfg; + var = git_strmap_value_at(b->values, pos); - CVAR_LIST_FOREACH (&b->var_list, iter) { - /* This is a bit hacky because we use a singly-linked list */ - if (cvar_match_name(iter, name)) { - if (CVAR_LIST_HEAD(&b->var_list) == iter) - CVAR_LIST_HEAD(&b->var_list) = CVAR_LIST_NEXT(iter); - else - CVAR_LIST_REMOVE_AFTER(prev); - - git__free(iter->value); - iter->value = NULL; - error = config_write(b, iter); - cvar_free(iter); - return error == GIT_SUCCESS ? - GIT_SUCCESS : - git__rethrow(error, "Failed to update config file"); - } - /* Store it for the next round */ - prev = iter; + if (var->next != NULL) { + giterr_set(GITERR_CONFIG, "Cannot delete multivar with a single delete"); + return -1; } - return git__throw(GIT_ENOTFOUND, "Variable '%s' not found", name); + git_strmap_delete_at(b->values, pos); + + result = config_write(b, var->key, NULL, NULL); + + cvar_free(var); + return result; } int git_config_file__ondisk(git_config_file **out, const char *path) @@ -428,27 +466,25 @@ int git_config_file__ondisk(git_config_file **out, const char *path) diskfile_backend *backend; backend = git__malloc(sizeof(diskfile_backend)); - if (backend == NULL) - return GIT_ENOMEM; + GITERR_CHECK_ALLOC(backend); memset(backend, 0x0, sizeof(diskfile_backend)); backend->file_path = git__strdup(path); - if (backend->file_path == NULL) { - git__free(backend); - return GIT_ENOMEM; - } + GITERR_CHECK_ALLOC(backend->file_path); backend->parent.open = config_open; backend->parent.get = config_get; + backend->parent.get_multivar = config_get_multivar; backend->parent.set = config_set; + backend->parent.set_multivar = config_set_multivar; backend->parent.del = config_delete; backend->parent.foreach = file_foreach; backend->parent.free = backend_free; *out = (git_config_file *)backend; - return GIT_SUCCESS; + return 0; } static int cfg_getchar_raw(diskfile_backend *cfg) @@ -489,7 +525,7 @@ static int cfg_getchar(diskfile_backend *cfg_file, int flags) assert(cfg_file->reader.read_ptr); do c = cfg_getchar_raw(cfg_file); - while (skip_whitespace && isspace(c) && + while (skip_whitespace && git__isspace(c) && !cfg_file->reader.eof); if (skip_comments && (c == '#' || c == ';')) { @@ -527,7 +563,7 @@ static int cfg_peek(diskfile_backend *cfg, int flags) /* * Read and consume a line, returning it in newly-allocated memory. */ -static char *cfg_readline(diskfile_backend *cfg) +static char *cfg_readline(diskfile_backend *cfg, bool skip_whitespace) { char *line = NULL; char *line_src, *line_end; @@ -535,9 +571,11 @@ static char *cfg_readline(diskfile_backend *cfg) line_src = cfg->reader.read_ptr; - /* Skip empty empty lines */ - while (isspace(*line_src)) - ++line_src; + if (skip_whitespace) { + /* Skip empty empty lines */ + while (git__isspace(*line_src)) + ++line_src; + } line_end = strchr(line_src, '\n'); @@ -554,7 +592,7 @@ static char *cfg_readline(diskfile_backend *cfg) memcpy(line, line_src, line_len); do line[line_len] = '\0'; - while (line_len-- > 0 && isspace(line[line_len])); + while (line_len-- > 0 && git__isspace(line[line_len])); if (*line_end == '\n') line_end++; @@ -597,12 +635,11 @@ GIT_INLINE(int) config_keychar(int c) return isalnum(c) || c == '-'; } -static int parse_section_header_ext(const char *line, const char *base_name, char **section_name) +static int parse_section_header_ext(diskfile_backend *cfg, const char *line, const char *base_name, char **section_name) { int c, rpos; char *first_quote, *last_quote; git_buf buf = GIT_BUF_INIT; - int error = GIT_SUCCESS; int quote_marks; /* * base_name is what came before the space. We should be at the @@ -613,8 +650,10 @@ static int parse_section_header_ext(const char *line, const char *base_name, cha first_quote = strchr(line, '"'); last_quote = strrchr(line, '"'); - if (last_quote - first_quote == 0) - return git__throw(GIT_EOBJCORRUPTED, "Failed to parse ext header. There is no final quotation mark"); + if (last_quote - first_quote == 0) { + set_parse_error(cfg, 0, "Missing closing quotation mark in section header"); + return -1; + } git_buf_grow(&buf, strlen(base_name) + last_quote - first_quote + 2); git_buf_printf(&buf, "%s.", base_name); @@ -631,26 +670,30 @@ static int parse_section_header_ext(const char *line, const char *base_name, cha */ do { if (quote_marks == 2) { - error = git__throw(GIT_EOBJCORRUPTED, "Falied to parse ext header. Text after closing quote"); - goto out; - + set_parse_error(cfg, rpos, "Unexpected text after closing quotes"); + git_buf_free(&buf); + return -1; } switch (c) { case '"': ++quote_marks; continue; + case '\\': c = line[rpos++]; + switch (c) { case '"': case '\\': break; + default: - error = git__throw(GIT_EOBJCORRUPTED, "Failed to parse ext header. Unsupported escape char \\%c", c); - goto out; + set_parse_error(cfg, rpos, "Unsupported escape sequence"); + git_buf_free(&buf); + return -1; } - break; + default: break; } @@ -658,61 +701,53 @@ static int parse_section_header_ext(const char *line, const char *base_name, cha git_buf_putc(&buf, c); } while ((c = line[rpos++]) != ']'); - *section_name = git__strdup(git_buf_cstr(&buf)); - out: - git_buf_free(&buf); - - return error; + *section_name = git_buf_detach(&buf); + return 0; } static int parse_section_header(diskfile_backend *cfg, char **section_out) { char *name, *name_end; int name_length, c, pos; - int error = GIT_SUCCESS; + int result; char *line; - line = cfg_readline(cfg); + line = cfg_readline(cfg, true); if (line == NULL) - return GIT_ENOMEM; + return -1; /* find the end of the variable's name */ name_end = strchr(line, ']'); if (name_end == NULL) { git__free(line); - return git__throw(GIT_EOBJCORRUPTED, "Failed to parse header. Can't find header name end"); + set_parse_error(cfg, 0, "Missing ']' in section header"); + return -1; } name = (char *)git__malloc((size_t)(name_end - line) + 1); - if (name == NULL) { - git__free(line); - return GIT_ENOMEM; - } + GITERR_CHECK_ALLOC(name); name_length = 0; pos = 0; /* Make sure we were given a section header */ c = line[pos++]; - if (c != '[') { - error = git__throw(GIT_ERROR, "Failed to parse header. Didn't get section header. This is a bug"); - goto error; - } + assert(c == '['); c = line[pos++]; do { - if (isspace(c)){ + if (git__isspace(c)){ name[name_length] = '\0'; - error = parse_section_header_ext(line, name, section_out); + result = parse_section_header_ext(cfg, line, name, section_out); git__free(line); git__free(name); - return error == GIT_SUCCESS ? GIT_SUCCESS : git__rethrow(error, "Failed to parse header"); + return result; } if (!config_keychar(c) && c != '.') { - error = git__throw(GIT_EOBJCORRUPTED, "Failed to parse header. Wrong format on header"); - goto error; + set_parse_error(cfg, pos, "Unexpected character in header"); + goto fail_parse; } name[name_length++] = (char) tolower(c); @@ -720,28 +755,29 @@ static int parse_section_header(diskfile_backend *cfg, char **section_out) } while ((c = line[pos++]) != ']'); if (line[pos - 1] != ']') { - error = git__throw(GIT_EOBJCORRUPTED, "Failed to parse header. Config file ended unexpectedly"); - goto error; + set_parse_error(cfg, pos, "Unexpected end of file"); + goto fail_parse; } - name[name_length] = 0; git__free(line); - git__strtolower(name); + + name[name_length] = 0; *section_out = name; - return GIT_SUCCESS; -error: + return 0; + +fail_parse: git__free(line); git__free(name); - return error; + return -1; } static int skip_bom(diskfile_backend *cfg) { - static const char *utf8_bom = "\xef\xbb\xbf"; + static const char utf8_bom[] = "\xef\xbb\xbf"; - if (cfg->reader.buffer.len < sizeof(utf8_bom)) - return GIT_SUCCESS; + if (cfg->reader.buffer.size < sizeof(utf8_bom)) + return 0; if (memcmp(cfg->reader.read_ptr, utf8_bom, sizeof(utf8_bom)) == 0) cfg->reader.read_ptr += sizeof(utf8_bom); @@ -750,7 +786,7 @@ static int skip_bom(diskfile_backend *cfg) shit with the BoM */ - return GIT_SUCCESS; + return 0; } /* @@ -792,9 +828,9 @@ static int skip_bom(diskfile_backend *cfg) boolean_false = "no" | "0" | "false" | "off" */ -static void strip_comments(char *line) +static int strip_comments(char *line, int in_quotes) { - int quote_count = 0; + int quote_count = in_quotes; char *ptr; for (ptr = line; *ptr; ++ptr) { @@ -807,30 +843,37 @@ static void strip_comments(char *line) } } - if (isspace(ptr[-1])) { - /* TODO skip whitespace */ + /* skip any space at the end */ + if (git__isspace(ptr[-1])) { + ptr--; } + ptr[0] = '\0'; + + return quote_count; } static int config_parse(diskfile_backend *cfg_file) { - int error = GIT_SUCCESS, c; + int c; char *current_section = NULL; char *var_name; char *var_value; - cvar_t *var; + cvar_t *var, *existing; + git_buf buf = GIT_BUF_INIT; + int result = 0; + khiter_t pos; /* Initialize the reading position */ - cfg_file->reader.read_ptr = cfg_file->reader.buffer.data; + cfg_file->reader.read_ptr = cfg_file->reader.buffer.ptr; cfg_file->reader.eof = 0; /* If the file is empty, there's nothing for us to do */ if (*cfg_file->reader.read_ptr == '\0') - return GIT_SUCCESS; + return 0; skip_bom(cfg_file); - while (error == GIT_SUCCESS && !cfg_file->reader.eof) { + while (result == 0 && !cfg_file->reader.eof) { c = cfg_peek(cfg_file, SKIP_WHITESPACE); @@ -842,7 +885,7 @@ static int config_parse(diskfile_backend *cfg_file) case '[': /* section header, new section begins */ git__free(current_section); current_section = NULL; - error = parse_section_header(cfg_file, ¤t_section); + result = parse_section_header(cfg_file, ¤t_section); break; case ';': @@ -851,100 +894,119 @@ static int config_parse(diskfile_backend *cfg_file) break; default: /* assume variable declaration */ - error = parse_variable(cfg_file, &var_name, &var_value); - - if (error < GIT_SUCCESS) + result = parse_variable(cfg_file, &var_name, &var_value); + if (result < 0) break; var = git__malloc(sizeof(cvar_t)); - if (var == NULL) { - error = GIT_ENOMEM; - break; - } + GITERR_CHECK_ALLOC(var); memset(var, 0x0, sizeof(cvar_t)); - var->section = git__strdup(current_section); - if (var->section == NULL) { - error = GIT_ENOMEM; - git__free(var); - break; - } + git__strtolower(var_name); + git_buf_printf(&buf, "%s.%s", current_section, var_name); + git__free(var_name); - var->name = var_name; + if (git_buf_oom(&buf)) + return -1; + + var->key = git_buf_detach(&buf); var->value = var_value; - git__strtolower(var->name); - CVAR_LIST_APPEND(&cfg_file->var_list, var); + /* Add or append the new config option */ + pos = git_strmap_lookup_index(cfg_file->values, var->key); + if (!git_strmap_valid_index(cfg_file->values, pos)) { + git_strmap_insert(cfg_file->values, var->key, var, result); + if (result < 0) + break; + result = 0; + } else { + existing = git_strmap_value_at(cfg_file->values, pos); + while (existing->next != NULL) { + existing = existing->next; + } + existing->next = var; + } break; } } git__free(current_section); - - return error == GIT_SUCCESS ? GIT_SUCCESS : git__rethrow(error, "Failed to parse config"); + return result; } -static int write_section(git_filebuf *file, cvar_t *var) +static int write_section(git_filebuf *file, const char *key) { - int error; + int result; + const char *dot; + git_buf buf = GIT_BUF_INIT; - error = git_filebuf_printf(file, "[%s]\n", var->section); - if (error < GIT_SUCCESS) - return error; + /* All of this just for [section "subsection"] */ + dot = strchr(key, '.'); + git_buf_putc(&buf, '['); + if (dot == NULL) { + git_buf_puts(&buf, key); + } else { + git_buf_put(&buf, key, dot - key); + /* TODO: escape */ + git_buf_printf(&buf, " \"%s\"", dot + 1); + } + git_buf_puts(&buf, "]\n"); + + if (git_buf_oom(&buf)) + return -1; + + result = git_filebuf_write(file, git_buf_cstr(&buf), buf.size); + git_buf_free(&buf); - error = git_filebuf_printf(file, " %s = %s\n", var->name, var->value); - return error; + return result; } /* * This is pretty much the parsing, except we write out anything we don't have */ -static int config_write(diskfile_backend *cfg, cvar_t *var) +static int config_write(diskfile_backend *cfg, const char *key, const regex_t *preg, const char* value) { - int error = GIT_SUCCESS, c; - int section_matches = 0, last_section_matched = 0; - char *current_section = NULL; - char *var_name, *var_value, *data_start; + int result, c; + int section_matches = 0, last_section_matched = 0, preg_replaced = 0, write_trailer = 0; + const char *pre_end = NULL, *post_start = NULL, *data_start; + char *current_section = NULL, *section, *name, *ldot; git_filebuf file = GIT_FILEBUF_INIT; - const char *pre_end = NULL, *post_start = NULL; /* We need to read in our own config file */ - error = git_futils_readbuffer(&cfg->reader.buffer, cfg->file_path); - if (error < GIT_SUCCESS && error != GIT_ENOTFOUND) { - return git__rethrow(error, "Failed to read existing config file %s", cfg->file_path); - } + result = git_futils_readbuffer(&cfg->reader.buffer, cfg->file_path); /* Initialise the reading position */ - if (error == GIT_ENOTFOUND) { - error = GIT_SUCCESS; + if (result == GIT_ENOTFOUND) { cfg->reader.read_ptr = NULL; cfg->reader.eof = 1; data_start = NULL; - cfg->reader.buffer.len = 0; - cfg->reader.buffer.data = NULL; - } else { - cfg->reader.read_ptr = cfg->reader.buffer.data; + git_buf_clear(&cfg->reader.buffer); + } else if (result == 0) { + cfg->reader.read_ptr = cfg->reader.buffer.ptr; cfg->reader.eof = 0; data_start = cfg->reader.read_ptr; + } else { + return -1; /* OS error when reading the file */ } /* Lock the file */ - error = git_filebuf_open(&file, cfg->file_path, 0); - if (error < GIT_SUCCESS) - return git__rethrow(error, "Failed to lock config file"); + if (git_filebuf_open(&file, cfg->file_path, 0) < 0) + return -1; skip_bom(cfg); + ldot = strrchr(key, '.'); + name = ldot + 1; + section = git__strndup(key, ldot - key); - while (error == GIT_SUCCESS && !cfg->reader.eof) { + while (!cfg->reader.eof) { c = cfg_peek(cfg, SKIP_WHITESPACE); - switch (c) { - case '\0': /* We've arrived at the end of the file */ + if (c == '\0') { /* We've arrived at the end of the file */ break; - case '[': /* section header, new section begins */ + } else if (c == '[') { /* section header, new section begins */ /* * We set both positions to the current one in case we * need to add a variable to the end of a section. In that @@ -953,23 +1015,22 @@ static int config_write(diskfile_backend *cfg, cvar_t *var) * default case will take care of updating them. */ pre_end = post_start = cfg->reader.read_ptr; - if (current_section) - git__free(current_section); - error = parse_section_header(cfg, ¤t_section); - if (error < GIT_SUCCESS) - break; + + git__free(current_section); + current_section = NULL; + if (parse_section_header(cfg, ¤t_section) < 0) + goto rewrite_fail; /* Keep track of when it stops matching */ last_section_matched = section_matches; - section_matches = !strcmp(current_section, var->section); - break; + section_matches = !strcmp(current_section, section); + } - case ';': - case '#': + else if (c == ';' || c == '#') { cfg_consume_line(cfg); - break; + } - default: + else { /* * If the section doesn't match, but the last section did, * it means we need to add a variable (so skip the line @@ -983,58 +1044,54 @@ static int config_write(diskfile_backend *cfg, cvar_t *var) if (!section_matches) { if (!last_section_matched) { cfg_consume_line(cfg); - break; + continue; } } else { - int cmp = -1; + int has_matched = 0; + char *var_name, *var_value; pre_end = cfg->reader.read_ptr; - if ((error = parse_variable(cfg, &var_name, &var_value)) == GIT_SUCCESS) - cmp = strcasecmp(var->name, var_name); + if (parse_variable(cfg, &var_name, &var_value) < 0) + goto rewrite_fail; + + /* First try to match the name of the variable */ + if (strcasecmp(name, var_name) == 0) + has_matched = 1; + + /* If the name matches, and we have a regex to match the + * value, try to match it */ + if (has_matched && preg != NULL) + has_matched = (regexec(preg, var_value, 0, NULL, 0) == 0); git__free(var_name); git__free(var_value); - if (cmp != 0) - break; + /* if there is no match, keep going */ + if (!has_matched) + continue; post_start = cfg->reader.read_ptr; } - /* - * We've found the variable we wanted to change, so - * write anything up to it - */ - error = git_filebuf_write(&file, data_start, pre_end - data_start); - if (error < GIT_SUCCESS) { - git__rethrow(error, "Failed to write the first part of the file"); - break; - } + /* We've found the variable we wanted to change, so + * write anything up to it */ + git_filebuf_write(&file, data_start, pre_end - data_start); + preg_replaced = 1; - /* - * Then replace the variable. If the value is NULL, it - * means we want to delete it, so pretend everything went - * fine - */ - if (var->value == NULL) - error = GIT_SUCCESS; - else - error = git_filebuf_printf(&file, "\t%s = %s\n", var->name, var->value); - if (error < GIT_SUCCESS) { - git__rethrow(error, "Failed to overwrite the variable"); - break; + /* Then replace the variable. If the value is NULL, it + * means we want to delete it, so don't write anything. */ + if (value != NULL) { + git_filebuf_printf(&file, "\t%s = %s\n", name, value); } - /* And then the write out rest of the file */ - error = git_filebuf_write(&file, post_start, - cfg->reader.buffer.len - (post_start - data_start)); - - if (error < GIT_SUCCESS) { - git__rethrow(error, "Failed to write the rest of the file"); - break; + /* multiline variable? we need to keep reading lines to match */ + if (preg != NULL) { + data_start = post_start; + continue; } - goto cleanup; + write_trailer = 1; + break; /* break from the loop */ } } @@ -1047,127 +1104,166 @@ static int config_write(diskfile_backend *cfg, cvar_t *var) * 2) we didn't find a section for us so we need to create it * ourselves. * - * Either way we need to write out the whole file. + * 3) we're setting a multivar with a regex, which means we + * continue to search for matching values + * + * In the last case, if we've already replaced a value, we + * want to write the rest of the file. Otherwise we need to write + * out the whole file and then the new variable. */ + if (write_trailer) { + /* Write out rest of the file */ + git_filebuf_write(&file, post_start, cfg->reader.buffer.size - (post_start - data_start)); + } else { + if (preg_replaced) { + git_filebuf_printf(&file, "\n%s", data_start); + } else { + git_filebuf_write(&file, cfg->reader.buffer.ptr, cfg->reader.buffer.size); + + /* And now if we just need to add a variable */ + if (!section_matches && write_section(&file, section) < 0) + goto rewrite_fail; + + /* Sanity check: if we are here, and value is NULL, that means that somebody + * touched the config file after our intial read. We should probably assert() + * this, but instead we'll handle it gracefully with an error. */ + if (value == NULL) { + giterr_set(GITERR_CONFIG, + "Race condition when writing a config file (a cvar has been removed)"); + goto rewrite_fail; + } - error = git_filebuf_write(&file, cfg->reader.buffer.data, cfg->reader.buffer.len); - if (error < GIT_SUCCESS) { - git__rethrow(error, "Failed to write original config content"); - goto cleanup; + git_filebuf_printf(&file, "\t%s = %s\n", name, value); + } } - /* And now if we just need to add a variable */ - if (section_matches) { - error = git_filebuf_printf(&file, "\t%s = %s\n", var->name, var->value); - goto cleanup; - } + git__free(section); + git__free(current_section); - /* Or maybe we need to write out a whole section */ - error = write_section(&file, var); - if (error < GIT_SUCCESS) - git__rethrow(error, "Failed to write new section"); + result = git_filebuf_commit(&file, GIT_CONFIG_FILE_MODE); + git_buf_free(&cfg->reader.buffer); + return result; - cleanup: +rewrite_fail: + git__free(section); git__free(current_section); - if (error < GIT_SUCCESS) - git_filebuf_cleanup(&file); - else - error = git_filebuf_commit(&file, GIT_CONFIG_FILE_MODE); - - git_futils_freebuffer(&cfg->reader.buffer); - return error; + git_filebuf_cleanup(&file); + git_buf_free(&cfg->reader.buffer); + return -1; } -static int is_multiline_var(const char *str) +/* '\"' -> '"' etc */ +static char *fixup_line(const char *ptr, int quote_count) { - char *end = strrchr(str, '\0') - 1; + char *str = git__malloc(strlen(ptr) + 1); + char *out = str, *esc; + const char *escapes = "ntb\"\\"; + const char *escaped = "\n\t\b\"\\"; - while (isspace(*end)) - --end; + if (str == NULL) + return NULL; - return *end == '\\'; + while (*ptr != '\0') { + if (*ptr == '"') { + quote_count++; + } else if (*ptr != '\\') { + *out++ = *ptr; + } else { + /* backslash, check the next char */ + ptr++; + /* if we're at the end, it's a multiline, so keep the backslash */ + if (*ptr == '\0') { + *out++ = '\\'; + goto out; + } + if ((esc = strchr(escapes, *ptr)) != NULL) { + *out++ = escaped[esc - escapes]; + } else { + git__free(str); + giterr_set(GITERR_CONFIG, "Invalid escape at %s", ptr); + return NULL; + } + } + ptr++; + } + +out: + *out = '\0'; + + return str; } -static int parse_multiline_variable(diskfile_backend *cfg, const char *first, char **out) +static int is_multiline_var(const char *str) { - char *line = NULL, *end; - int error = GIT_SUCCESS, ret; - size_t len; - char *buf; + const char *end = str + strlen(str); + return (end > str) && (end[-1] == '\\'); +} + +static int parse_multiline_variable(diskfile_backend *cfg, git_buf *value, int in_quotes) +{ + char *line = NULL, *proc_line = NULL; + int quote_count; /* Check that the next line exists */ - line = cfg_readline(cfg); + line = cfg_readline(cfg, false); if (line == NULL) - return GIT_ENOMEM; + return -1; /* We've reached the end of the file, there is input missing */ if (line[0] == '\0') { - error = git__throw(GIT_EOBJCORRUPTED, "Failed to parse multiline var. File ended unexpectedly"); - goto out; + set_parse_error(cfg, 0, "Unexpected end of file while parsing multine var"); + git__free(line); + return -1; } - strip_comments(line); + quote_count = strip_comments(line, !!in_quotes); /* If it was just a comment, pretend it didn't exist */ if (line[0] == '\0') { - error = parse_multiline_variable(cfg, first, out); - goto out; + git__free(line); + return parse_multiline_variable(cfg, value, quote_count); + /* TODO: unbounded recursion. This **could** be exploitable */ } - /* Find the continuation character '\' and strip the whitespace */ - end = strrchr(first, '\\'); - while (isspace(end[-1])) - --end; - - *end = '\0'; /* Terminate the string here */ - - len = strlen(first) + strlen(line) + 2; - buf = git__malloc(len); - if (buf == NULL) { - error = GIT_ENOMEM; - goto out; - } + /* Drop the continuation character '\': to closely follow the UNIX + * standard, this character **has** to be last one in the buf, with + * no whitespace after it */ + assert(is_multiline_var(value->ptr)); + git_buf_truncate(value, git_buf_len(value) - 1); - ret = p_snprintf(buf, len, "%s %s", first, line); - if (ret < 0) { - error = git__throw(GIT_EOSERR, "Failed to parse multiline var. Failed to put together two lines. OS err: %s", strerror(errno)); - git__free(buf); - goto out; + proc_line = fixup_line(line, in_quotes); + if (proc_line == NULL) { + git__free(line); + return -1; } + /* add this line to the multiline var */ + git_buf_puts(value, proc_line); + git__free(line); + git__free(proc_line); /* - * If we need to continue reading the next line, pretend - * everything we've read up to now was in one line and call - * ourselves. + * If we need to continue reading the next line, let's just + * keep putting stuff in the buffer */ - if (is_multiline_var(buf)) { - char *final_val; - error = parse_multiline_variable(cfg, buf, &final_val); - git__free(buf); - buf = final_val; - } - - *out = buf; + if (is_multiline_var(value->ptr)) + return parse_multiline_variable(cfg, value, quote_count); - out: - git__free(line); - return error; + return 0; } static int parse_variable(diskfile_backend *cfg, char **var_name, char **var_value) { - char *tmp; - int error = GIT_SUCCESS; const char *var_end = NULL; const char *value_start = NULL; char *line; + int quote_count; - line = cfg_readline(cfg); + line = cfg_readline(cfg, true); if (line == NULL) - return GIT_ENOMEM; + return -1; - strip_comments(line); + quote_count = strip_comments(line, 0); var_end = strchr(line, '='); @@ -1176,57 +1272,47 @@ static int parse_variable(diskfile_backend *cfg, char **var_name, char **var_val else value_start = var_end + 1; - if (isspace(var_end[-1])) { + if (git__isspace(var_end[-1])) { do var_end--; - while (isspace(var_end[0])); + while (git__isspace(var_end[0])); } - tmp = git__strndup(line, var_end - line + 1); - if (tmp == NULL) { - error = GIT_ENOMEM; - goto out; - } + *var_name = git__strndup(line, var_end - line + 1); + GITERR_CHECK_ALLOC(*var_name); - *var_name = tmp; + /* If there is no value, boolean true is assumed */ + *var_value = NULL; /* * Now, let's try to parse the value */ if (value_start != NULL) { - - while (isspace(value_start[0])) + while (git__isspace(value_start[0])) value_start++; - if (value_start[0] == '\0') { - *var_value = NULL; - goto out; - } - if (is_multiline_var(value_start)) { - error = parse_multiline_variable(cfg, value_start, var_value); - if (error != GIT_SUCCESS) - { - *var_value = NULL; + git_buf multi_value = GIT_BUF_INIT; + char *proc_line = fixup_line(value_start, 0); + GITERR_CHECK_ALLOC(proc_line); + git_buf_puts(&multi_value, proc_line); + git__free(proc_line); + if (parse_multiline_variable(cfg, &multi_value, quote_count) < 0 || git_buf_oom(&multi_value)) { git__free(*var_name); + git__free(line); + git_buf_free(&multi_value); + return -1; } - goto out; - } - tmp = git__strdup(value_start); - if (tmp == NULL) { - git__free(*var_name); - *var_value = NULL; - error = GIT_ENOMEM; - goto out; + *var_value = git_buf_detach(&multi_value); + + } + else if (value_start[0] != '\0') { + *var_value = fixup_line(value_start, 0); + GITERR_CHECK_ALLOC(*var_value); } - *var_value = tmp; - } else { - /* If there is no value, boolean true is assumed */ - *var_value = NULL; } - out: git__free(line); - return error; + return 0; } |