diff options
Diffstat (limited to 'src/config_file.c')
-rw-r--r-- | src/config_file.c | 695 |
1 files changed, 413 insertions, 282 deletions
diff --git a/src/config_file.c b/src/config_file.c index dec952115..15c8de49c 100644 --- a/src/config_file.c +++ b/src/config_file.c @@ -15,6 +15,7 @@ #include "git2/sys/config.h" #include "git2/types.h" #include "strmap.h" +#include "array.h" #include <ctype.h> #include <sys/types.h> @@ -25,8 +26,18 @@ GIT__USE_STRMAP; typedef struct cvar_t { struct cvar_t *next; git_config_entry *entry; + int included; /* whether this is part of [include] */ } cvar_t; +typedef struct git_config_file_iter { + git_config_iterator parent; + git_strmap_iter iter; + cvar_t* next_var; +} git_config_file_iter; + +/* Max depth for [include] directives */ +#define MAX_INCLUDE_DEPTH 10 + #define CVAR_LIST_HEAD(list) ((list)->head) #define CVAR_LIST_TAIL(list) ((list)->tail) @@ -65,34 +76,37 @@ typedef struct cvar_t { (iter) && (((tmp) = CVAR_LIST_NEXT(iter) || 1));\ (iter) = (tmp)) +struct reader { + time_t file_mtime; + size_t file_size; + char *file_path; + git_buf buffer; + char *read_ptr; + int line_number; + int eof; +}; + typedef struct { git_config_backend parent; git_strmap *values; - struct { - git_buf buffer; - char *read_ptr; - int line_number; - int eof; - } reader; + git_array_t(struct reader) readers; char *file_path; - time_t file_mtime; - size_t file_size; git_config_level_t level; } diskfile_backend; -static int config_parse(diskfile_backend *cfg_file, git_config_level_t level); -static int parse_variable(diskfile_backend *cfg, char **var_name, char **var_value); +static int config_parse(diskfile_backend *cfg_file, struct reader *reader, git_config_level_t level, int depth); +static int parse_variable(struct reader *reader, char **var_name, char **var_value); static int config_write(diskfile_backend *cfg, const char *key, const regex_t *preg, const char *value); static char *escape_value(const char *ptr); -static void set_parse_error(diskfile_backend *backend, int col, const char *error_str) +static void set_parse_error(struct reader *reader, 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); + error_str, reader->file_path, reader->line_number, col); } static void cvar_free(cvar_t *var) @@ -106,6 +120,18 @@ static void cvar_free(cvar_t *var) git__free(var); } +static int cvar_length(cvar_t *var) +{ + int length = 0; + + while (var) { + length++; + var = var->next; + } + + return length; +} + int git_config_file_normalize_section(char *start, char *end) { char *scan; @@ -118,7 +144,7 @@ int git_config_file_normalize_section(char *start, char *end) if (end && scan >= end) break; if (isalnum(*scan)) - *scan = tolower(*scan); + *scan = (char)tolower(*scan); else if (*scan != '-' || scan == start) return GIT_EINVALIDSPEC; } @@ -129,41 +155,6 @@ int git_config_file_normalize_section(char *start, char *end) return 0; } -/* Take something the user gave us and make it nice for our hash function */ -static int normalize_name(const char *in, char **out) -{ - char *name, *fdot, *ldot; - - assert(in && out); - - name = git__strdup(in); - GITERR_CHECK_ALLOC(name); - - fdot = strchr(name, '.'); - ldot = strrchr(name, '.'); - - if (fdot == NULL || fdot == name || ldot == NULL || !ldot[1]) - goto invalid; - - /* Validate and downcase up to first dot and after last dot */ - if (git_config_file_normalize_section(name, fdot) < 0 || - git_config_file_normalize_section(ldot + 1, NULL) < 0) - goto invalid; - - /* If there is a middle range, make sure it doesn't have newlines */ - while (fdot < ldot) - if (*fdot++ == '\n') - goto invalid; - - *out = name; - return 0; - -invalid: - git__free(name); - giterr_set(GITERR_CONFIG, "Invalid config item name '%s'", in); - return GIT_EINVALIDSPEC; -} - static void free_vars(git_strmap *values) { cvar_t *var = NULL; @@ -184,6 +175,7 @@ static void free_vars(git_strmap *values) static int config_open(git_config_backend *cfg, git_config_level_t level) { int res; + struct reader *reader; diskfile_backend *b = (diskfile_backend *)cfg; b->level = level; @@ -191,32 +183,52 @@ static int config_open(git_config_backend *cfg, git_config_level_t level) b->values = git_strmap_alloc(); GITERR_CHECK_ALLOC(b->values); - git_buf_init(&b->reader.buffer, 0); + git_array_init(b->readers); + reader = git_array_alloc(b->readers); + memset(reader, 0, sizeof(struct reader)); + + reader->file_path = git__strdup(b->file_path); + GITERR_CHECK_ALLOC(reader->file_path); + + git_buf_init(&reader->buffer, 0); res = git_futils_readbuffer_updated( - &b->reader.buffer, b->file_path, &b->file_mtime, &b->file_size, NULL); + &reader->buffer, b->file_path, &reader->file_mtime, &reader->file_size, NULL); /* It's fine if the file doesn't exist */ if (res == GIT_ENOTFOUND) return 0; - if (res < 0 || (res = config_parse(b, level)) < 0) { + if (res < 0 || (res = config_parse(b, reader, level, 0)) < 0) { free_vars(b->values); b->values = NULL; } - git_buf_free(&b->reader.buffer); + reader = git_array_get(b->readers, 0); + git_buf_free(&reader->buffer); return res; } static int config_refresh(git_config_backend *cfg) { - int res, updated = 0; + int res = 0, updated = 0, any_updated = 0; diskfile_backend *b = (diskfile_backend *)cfg; git_strmap *old_values; + struct reader *reader; + uint32_t i; - res = git_futils_readbuffer_updated( - &b->reader.buffer, b->file_path, &b->file_mtime, &b->file_size, &updated); - if (res < 0 || !updated) + for (i = 0; i < git_array_size(b->readers); i++) { + reader = git_array_get(b->readers, i); + res = git_futils_readbuffer_updated( + &reader->buffer, reader->file_path, &reader->file_mtime, &reader->file_size, &updated); + + if (res < 0) + return (res == GIT_ENOTFOUND) ? 0 : res; + + if (updated) + any_updated = 1; + } + + if (!any_updated) return (res == GIT_ENOTFOUND) ? 0 : res; /* need to reload - store old values and prep for reload */ @@ -224,74 +236,88 @@ static int config_refresh(git_config_backend *cfg) b->values = git_strmap_alloc(); GITERR_CHECK_ALLOC(b->values); - if ((res = config_parse(b, b->level)) < 0) { + if ((res = config_parse(b, reader, b->level, 0)) < 0) { free_vars(b->values); b->values = old_values; } else { free_vars(old_values); } - git_buf_free(&b->reader.buffer); + git_buf_free(&reader->buffer); return res; } static void backend_free(git_config_backend *_backend) { diskfile_backend *backend = (diskfile_backend *)_backend; + uint32_t i; if (backend == NULL) return; + for (i = 0; i < git_array_size(backend->readers); i++) { + struct reader *r = git_array_get(backend->readers, i); + git__free(r->file_path); + } + git_array_clear(backend->readers); + git__free(backend->file_path); free_vars(backend->values); git__free(backend); } -static int file_foreach( - git_config_backend *backend, - const char *regexp, - int (*fn)(const git_config_entry *, void *), - void *data) +static void config_iterator_free( + git_config_iterator* iter) { - diskfile_backend *b = (diskfile_backend *)backend; - cvar_t *var, *next_var; - const char *key; - regex_t regex; - int result = 0; + git__free(iter); +} - if (!b->values) - return 0; +static int config_iterator_next( + git_config_entry **entry, + git_config_iterator *iter) +{ + git_config_file_iter *it = (git_config_file_iter *) iter; + diskfile_backend *b = (diskfile_backend *) it->parent.backend; + int err = 0; + cvar_t * var; - if (regexp != NULL) { - if ((result = regcomp(®ex, regexp, REG_EXTENDED)) < 0) { - giterr_set_regex(®ex, result); - regfree(®ex); - return -1; - } + if (it->next_var == NULL) { + err = git_strmap_next((void**) &var, &(it->iter), b->values); + } else { + var = it->next_var; } - git_strmap_foreach(b->values, key, var, - for (; var != NULL; var = next_var) { - next_var = CVAR_LIST_NEXT(var); + if (err < 0) { + it->next_var = NULL; + return err; + } - /* skip non-matching keys if regexp was provided */ - if (regexp && regexec(®ex, key, 0, NULL, 0) != 0) - continue; + *entry = var->entry; + it->next_var = CVAR_LIST_NEXT(var); - /* abort iterator on non-zero return value */ - if (fn(var->entry, data)) { - giterr_clear(); - result = GIT_EUSER; - goto cleanup; - } - } - ); + return 0; +} -cleanup: - if (regexp != NULL) - regfree(®ex); +static int config_iterator_new( + git_config_iterator **iter, + struct git_config_backend* backend) +{ + diskfile_backend *b = (diskfile_backend *)backend; + git_config_file_iter *it = git__calloc(1, sizeof(git_config_file_iter)); - return result; + GIT_UNUSED(b); + + GITERR_CHECK_ALLOC(it); + + it->parent.backend = backend; + it->iter = git_strmap_begin(b->values); + it->next_var = NULL; + + it->parent.next = config_iterator_next; + it->parent.free = config_iterator_free; + *iter = (git_config_iterator *) it; + + return 0; } static int config_set(git_config_backend *cfg, const char *name, const char *value) @@ -302,7 +328,7 @@ static int config_set(git_config_backend *cfg, const char *name, const char *val khiter_t pos; int rval, ret; - if ((rval = normalize_name(name, &key)) < 0) + if ((rval = git_config__normalize_name(name, &key)) < 0) return rval; /* @@ -359,10 +385,10 @@ static int config_set(git_config_backend *cfg, const char *name, const char *val GITERR_CHECK_ALLOC(esc_value); } - if (config_write(b, key, NULL, esc_value) < 0) { + if ((ret = config_write(b, key, NULL, esc_value)) < 0) { git__free(esc_value); cvar_free(var); - return -1; + return ret; } git__free(esc_value); @@ -384,82 +410,23 @@ static int config_get(const git_config_backend *cfg, const char *name, const git char *key; khiter_t pos; int error; - - if ((error = normalize_name(name, &key)) < 0) - return error; - - pos = git_strmap_lookup_index(b->values, key); - git__free(key); - - /* no error message; the config system will write one */ - if (!git_strmap_valid_index(b->values, pos)) - return GIT_ENOTFOUND; - - *out = ((cvar_t *)git_strmap_value_at(b->values, pos))->entry; - - return 0; -} - -static int config_get_multivar( - git_config_backend *cfg, - const char *name, - const char *regex_str, - int (*fn)(const git_config_entry *, void *), - void *data) -{ cvar_t *var; - diskfile_backend *b = (diskfile_backend *)cfg; - char *key; - khiter_t pos; - int error; - if ((error = normalize_name(name, &key)) < 0) + if ((error = git_config__normalize_name(name, &key)) < 0) return error; pos = git_strmap_lookup_index(b->values, key); git__free(key); + /* no error message; the config system will write one */ if (!git_strmap_valid_index(b->values, pos)) return GIT_ENOTFOUND; var = git_strmap_value_at(b->values, pos); + while (var->next) + var = var->next; - if (regex_str != NULL) { - regex_t regex; - int result; - - /* regex matching; build the regex */ - result = regcomp(®ex, regex_str, REG_EXTENDED); - if (result < 0) { - giterr_set_regex(®ex, result); - regfree(®ex); - return -1; - } - - /* and throw the callback only on the variables that - * match the regex */ - do { - if (regexec(®ex, var->entry->value, 0, NULL, 0) == 0) { - /* early termination by the user is not an error; - * just break and return successfully */ - if (fn(var->entry, 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->entry, data) < 0) - break; - - var = var->next; - } while (var != NULL); - } + *out = var->entry; return 0; } @@ -477,7 +444,7 @@ static int config_set_multivar( assert(regexp); - if ((result = normalize_name(name, &key)) < 0) + if ((result = git_config__normalize_name(name, &key)) < 0) return result; pos = git_strmap_lookup_index(b->values, key); @@ -550,7 +517,7 @@ static int config_delete(git_config_backend *cfg, const char *name) int result; khiter_t pos; - if ((result = normalize_name(name, &key)) < 0) + if ((result = git_config__normalize_name(name, &key)) < 0) return result; pos = git_strmap_lookup_index(b->values, key); @@ -576,6 +543,80 @@ static int config_delete(git_config_backend *cfg, const char *name) return result; } +static int config_delete_multivar(git_config_backend *cfg, const char *name, const char *regexp) +{ + cvar_t *var, *prev = NULL, *new_head = NULL; + cvar_t **to_delete; + int to_delete_idx; + diskfile_backend *b = (diskfile_backend *)cfg; + char *key; + regex_t preg; + int result; + khiter_t pos; + + if ((result = git_config__normalize_name(name, &key)) < 0) + return result; + + pos = git_strmap_lookup_index(b->values, key); + + if (!git_strmap_valid_index(b->values, pos)) { + giterr_set(GITERR_CONFIG, "Could not find key '%s' to delete", name); + git__free(key); + return GIT_ENOTFOUND; + } + + var = git_strmap_value_at(b->values, pos); + + result = regcomp(&preg, regexp, REG_EXTENDED); + if (result < 0) { + git__free(key); + giterr_set_regex(&preg, result); + regfree(&preg); + return -1; + } + + to_delete = git__calloc(cvar_length(var), sizeof(cvar_t *)); + GITERR_CHECK_ALLOC(to_delete); + to_delete_idx = 0; + + while (var != NULL) { + cvar_t *next = var->next; + + if (regexec(&preg, var->entry->value, 0, NULL, 0) == 0) { + // If we are past the head, reattach previous node to next one, + // otherwise set the new head for the strmap. + if (prev != NULL) { + prev->next = next; + } else { + new_head = next; + } + + to_delete[to_delete_idx++] = var; + } else { + prev = var; + } + + var = next; + } + + if (new_head != NULL) { + git_strmap_set_value_at(b->values, pos, new_head); + } else { + git_strmap_delete_at(b->values, pos); + } + + if (to_delete_idx > 0) + result = config_write(b, key, &preg, NULL); + + while (to_delete_idx-- > 0) + cvar_free(to_delete[to_delete_idx]); + + git__free(key); + git__free(to_delete); + regfree(&preg); + return result; +} + int git_config_file__ondisk(git_config_backend **out, const char *path) { diskfile_backend *backend; @@ -590,11 +631,11 @@ int git_config_file__ondisk(git_config_backend **out, const char *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.del_multivar = config_delete_multivar; + backend->parent.iterator = config_iterator_new; backend->parent.refresh = config_refresh; backend->parent.free = backend_free; @@ -603,26 +644,26 @@ int git_config_file__ondisk(git_config_backend **out, const char *path) return 0; } -static int cfg_getchar_raw(diskfile_backend *cfg) +static int reader_getchar_raw(struct reader *reader) { int c; - c = *cfg->reader.read_ptr++; + c = *reader->read_ptr++; /* Win 32 line breaks: if we find a \r\n sequence, return only the \n as a newline */ - if (c == '\r' && *cfg->reader.read_ptr == '\n') { - cfg->reader.read_ptr++; + if (c == '\r' && *reader->read_ptr == '\n') { + reader->read_ptr++; c = '\n'; } if (c == '\n') - cfg->reader.line_number++; + reader->line_number++; if (c == 0) { - cfg->reader.eof = 1; + reader->eof = 1; c = '\n'; } @@ -632,21 +673,23 @@ static int cfg_getchar_raw(diskfile_backend *cfg) #define SKIP_WHITESPACE (1 << 1) #define SKIP_COMMENTS (1 << 2) -static int cfg_getchar(diskfile_backend *cfg_file, int flags) +static int reader_getchar(struct reader *reader, int flags) { const int skip_whitespace = (flags & SKIP_WHITESPACE); const int skip_comments = (flags & SKIP_COMMENTS); int c; - assert(cfg_file->reader.read_ptr); + assert(reader->read_ptr); - do c = cfg_getchar_raw(cfg_file); - while (skip_whitespace && git__isspace(c) && - !cfg_file->reader.eof); + do { + c = reader_getchar_raw(reader); + } while (skip_whitespace && git__isspace(c) && + !reader->eof); if (skip_comments && (c == '#' || c == ';')) { - do c = cfg_getchar_raw(cfg_file); - while (c != '\n'); + do { + c = reader_getchar_raw(reader); + } while (c != '\n'); } return c; @@ -655,23 +698,23 @@ static int cfg_getchar(diskfile_backend *cfg_file, int flags) /* * Read the next char, but don't move the reading pointer. */ -static int cfg_peek(diskfile_backend *cfg, int flags) +static int reader_peek(struct reader *reader, int flags) { void *old_read_ptr; int old_lineno, old_eof; int ret; - assert(cfg->reader.read_ptr); + assert(reader->read_ptr); - old_read_ptr = cfg->reader.read_ptr; - old_lineno = cfg->reader.line_number; - old_eof = cfg->reader.eof; + old_read_ptr = reader->read_ptr; + old_lineno = reader->line_number; + old_eof = reader->eof; - ret = cfg_getchar(cfg, flags); + ret = reader_getchar(reader, flags); - cfg->reader.read_ptr = old_read_ptr; - cfg->reader.line_number = old_lineno; - cfg->reader.eof = old_eof; + reader->read_ptr = old_read_ptr; + reader->line_number = old_lineno; + reader->eof = old_eof; return ret; } @@ -679,13 +722,13 @@ 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, bool skip_whitespace) +static char *reader_readline(struct reader *reader, bool skip_whitespace) { char *line = NULL; char *line_src, *line_end; size_t line_len; - line_src = cfg->reader.read_ptr; + line_src = reader->read_ptr; if (skip_whitespace) { /* Skip empty empty lines */ @@ -714,10 +757,10 @@ static char *cfg_readline(diskfile_backend *cfg, bool skip_whitespace) line_end++; if (*line_end == '\0') - cfg->reader.eof = 1; + reader->eof = 1; - cfg->reader.line_number++; - cfg->reader.read_ptr = line_end; + reader->line_number++; + reader->read_ptr = line_end; return line; } @@ -725,11 +768,11 @@ static char *cfg_readline(diskfile_backend *cfg, bool skip_whitespace) /* * Consume a line, without storing it anywhere */ -static void cfg_consume_line(diskfile_backend *cfg) +static void reader_consume_line(struct reader *reader) { char *line_start, *line_end; - line_start = cfg->reader.read_ptr; + line_start = reader->read_ptr; line_end = strchr(line_start, '\n'); /* No newline at EOF */ if(line_end == NULL){ @@ -740,10 +783,10 @@ static void cfg_consume_line(diskfile_backend *cfg) line_end++; if (*line_end == '\0') - cfg->reader.eof = 1; + reader->eof = 1; - cfg->reader.line_number++; - cfg->reader.read_ptr = line_end; + reader->line_number++; + reader->read_ptr = line_end; } GIT_INLINE(int) config_keychar(int c) @@ -751,12 +794,11 @@ GIT_INLINE(int) config_keychar(int c) return isalnum(c) || c == '-'; } -static int parse_section_header_ext(diskfile_backend *cfg, const char *line, const char *base_name, char **section_name) +static int parse_section_header_ext(struct reader *reader, 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 quote_marks; /* * base_name is what came before the space. We should be at the * first quotation mark, except for now, line isn't being kept in @@ -767,7 +809,7 @@ static int parse_section_header_ext(diskfile_backend *cfg, const char *line, con last_quote = strrchr(line, '"'); if (last_quote - first_quote == 0) { - set_parse_error(cfg, 0, "Missing closing quotation mark in section header"); + set_parse_error(reader, 0, "Missing closing quotation mark in section header"); return -1; } @@ -775,37 +817,30 @@ static int parse_section_header_ext(diskfile_backend *cfg, const char *line, con git_buf_printf(&buf, "%s.", base_name); rpos = 0; - quote_marks = 0; line = first_quote; - c = line[rpos++]; + c = line[++rpos]; /* * At the end of each iteration, whatever is stored in c will be * added to the string. In case of error, jump to out */ do { - if (quote_marks == 2) { - set_parse_error(cfg, rpos, "Unexpected text after closing quotes"); + + switch (c) { + case 0: + set_parse_error(reader, 0, "Unexpected end-of-line in section header"); git_buf_free(&buf); return -1; - } - switch (c) { case '"': - ++quote_marks; - continue; + goto end_parse; case '\\': - c = line[rpos++]; - - switch (c) { - case '"': - case '\\': - break; + c = line[++rpos]; - default: - set_parse_error(cfg, rpos, "Unsupported escape sequence"); + if (c == 0) { + set_parse_error(reader, rpos, "Unexpected end-of-line in section header"); git_buf_free(&buf); return -1; } @@ -814,29 +849,37 @@ static int parse_section_header_ext(diskfile_backend *cfg, const char *line, con break; } - git_buf_putc(&buf, c); - } while ((c = line[rpos++]) != ']'); + git_buf_putc(&buf, (char)c); + c = line[++rpos]; + } while (line + rpos < last_quote); + +end_parse: + if (line[rpos] != '"' || line[rpos + 1] != ']') { + set_parse_error(reader, rpos, "Unexpected text after closing quotes"); + git_buf_free(&buf); + return -1; + } *section_name = git_buf_detach(&buf); return 0; } -static int parse_section_header(diskfile_backend *cfg, char **section_out) +static int parse_section_header(struct reader *reader, char **section_out) { char *name, *name_end; int name_length, c, pos; int result; char *line; - line = cfg_readline(cfg, true); + line = reader_readline(reader, true); if (line == NULL) return -1; /* find the end of the variable's name */ - name_end = strchr(line, ']'); + name_end = strrchr(line, ']'); if (name_end == NULL) { git__free(line); - set_parse_error(cfg, 0, "Missing ']' in section header"); + set_parse_error(reader, 0, "Missing ']' in section header"); return -1; } @@ -855,14 +898,14 @@ static int parse_section_header(diskfile_backend *cfg, char **section_out) do { if (git__isspace(c)){ name[name_length] = '\0'; - result = parse_section_header_ext(cfg, line, name, section_out); + result = parse_section_header_ext(reader, line, name, section_out); git__free(line); git__free(name); return result; } if (!config_keychar(c) && c != '.') { - set_parse_error(cfg, pos, "Unexpected character in header"); + set_parse_error(reader, pos, "Unexpected character in header"); goto fail_parse; } @@ -871,7 +914,7 @@ static int parse_section_header(diskfile_backend *cfg, char **section_out) } while ((c = line[pos++]) != ']'); if (line[pos - 1] != ']') { - set_parse_error(cfg, pos, "Unexpected end of file"); + set_parse_error(reader, pos, "Unexpected end of file"); goto fail_parse; } @@ -888,14 +931,14 @@ fail_parse: return -1; } -static int skip_bom(diskfile_backend *cfg) +static int skip_bom(struct reader *reader) { git_bom_t bom; int bom_offset = git_buf_text_detect_bom(&bom, - &cfg->reader.buffer, cfg->reader.read_ptr - cfg->reader.buffer.ptr); + &reader->buffer, reader->read_ptr - reader->buffer.ptr); if (bom == GIT_BOM_UTF8) - cfg->reader.read_ptr += bom_offset; + reader->read_ptr += bom_offset; /* TODO: reference implementation is pretty stupid with BoM */ @@ -965,7 +1008,16 @@ static int strip_comments(char *line, int in_quotes) return quote_count; } -static int config_parse(diskfile_backend *cfg_file, git_config_level_t level) +static int included_path(git_buf *out, const char *dir, const char *path) +{ + /* From the user's home */ + if (path[0] == '~' && path[1] == '/') + return git_futils_find_global_file(out, &path[1]); + + return git_path_join_unrooted(out, path, dir, NULL); +} + +static int config_parse(diskfile_backend *cfg_file, struct reader *reader, git_config_level_t level, int depth) { int c; char *current_section = NULL; @@ -975,39 +1027,46 @@ static int config_parse(diskfile_backend *cfg_file, git_config_level_t level) git_buf buf = GIT_BUF_INIT; int result = 0; khiter_t pos; + uint32_t reader_idx; + + if (depth >= MAX_INCLUDE_DEPTH) { + giterr_set(GITERR_CONFIG, "Maximum config include depth reached"); + return -1; + } + reader_idx = git_array_size(cfg_file->readers) - 1; /* Initialize the reading position */ - cfg_file->reader.read_ptr = cfg_file->reader.buffer.ptr; - cfg_file->reader.eof = 0; + reader->read_ptr = reader->buffer.ptr; + reader->eof = 0; /* If the file is empty, there's nothing for us to do */ - if (*cfg_file->reader.read_ptr == '\0') + if (*reader->read_ptr == '\0') return 0; - skip_bom(cfg_file); + skip_bom(reader); - while (result == 0 && !cfg_file->reader.eof) { + while (result == 0 && !reader->eof) { - c = cfg_peek(cfg_file, SKIP_WHITESPACE); + c = reader_peek(reader, SKIP_WHITESPACE); switch (c) { case '\n': /* EOF when peeking, set EOF in the reader to exit the loop */ - cfg_file->reader.eof = 1; + reader->eof = 1; break; case '[': /* section header, new section begins */ git__free(current_section); current_section = NULL; - result = parse_section_header(cfg_file, ¤t_section); + result = parse_section_header(reader, ¤t_section); break; case ';': case '#': - cfg_consume_line(cfg_file); + reader_consume_line(reader); break; default: /* assume variable declaration */ - result = parse_variable(cfg_file, &var_name, &var_value); + result = parse_variable(reader, &var_name, &var_value); if (result < 0) break; @@ -1028,6 +1087,7 @@ static int config_parse(diskfile_backend *cfg_file, git_config_level_t level) var->entry->name = git_buf_detach(&buf); var->entry->value = var_value; var->entry->level = level; + var->included = !!depth; /* Add or append the new config option */ pos = git_strmap_lookup_index(cfg_file->values, var->entry->name); @@ -1044,6 +1104,42 @@ static int config_parse(diskfile_backend *cfg_file, git_config_level_t level) existing->next = var; } + if (!git__strcmp(var->entry->name, "include.path")) { + struct reader *r; + git_buf path = GIT_BUF_INIT; + char *dir; + uint32_t index; + + r = git_array_alloc(cfg_file->readers); + /* The reader may have been reallocated */ + reader = git_array_get(cfg_file->readers, reader_idx); + memset(r, 0, sizeof(struct reader)); + if ((result = git_path_dirname_r(&path, reader->file_path)) < 0) + break; + + /* We need to know out index in the array, as the next config_parse call may realloc */ + index = git_array_size(cfg_file->readers) - 1; + dir = git_buf_detach(&path); + result = included_path(&path, dir, var->entry->value); + git__free(dir); + + if (result < 0) + break; + + r->file_path = git_buf_detach(&path); + git_buf_init(&r->buffer, 0); + if ((result = git_futils_readbuffer_updated(&r->buffer, r->file_path, &r->file_mtime, + &r->file_size, NULL)) < 0) + break; + + result = config_parse(cfg_file, r, level, depth+1); + r = git_array_get(cfg_file->readers, index); + git_buf_free(&r->buffer); + + if (result < 0) + break; + } + break; } } @@ -1082,6 +1178,24 @@ static int write_section(git_filebuf *file, const char *key) return result; } +static const char *quotes_for_value(const char *value) +{ + const char *ptr; + + if (value[0] == ' ' || value[0] == '\0') + return "\""; + + for (ptr = value; *ptr; ++ptr) { + if (*ptr == ';' || *ptr == '#') + return "\""; + } + + if (ptr[-1] == ' ') + return "\""; + + return ""; +} + /* * This is pretty much the parsing, except we write out anything we don't have */ @@ -1089,38 +1203,44 @@ static int config_write(diskfile_backend *cfg, const char *key, const regex_t *p { 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; + const char *pre_end = NULL, *post_start = NULL, *data_start, *write_start; char *current_section = NULL, *section, *name, *ldot; git_filebuf file = GIT_FILEBUF_INIT; + struct reader *reader = git_array_get(cfg->readers, 0); /* We need to read in our own config file */ - result = git_futils_readbuffer(&cfg->reader.buffer, cfg->file_path); + result = git_futils_readbuffer(&reader->buffer, cfg->file_path); /* Initialise the reading position */ if (result == GIT_ENOTFOUND) { - cfg->reader.read_ptr = NULL; - cfg->reader.eof = 1; + reader->read_ptr = NULL; + reader->eof = 1; data_start = NULL; - git_buf_clear(&cfg->reader.buffer); + git_buf_clear(&reader->buffer); } else if (result == 0) { - cfg->reader.read_ptr = cfg->reader.buffer.ptr; - cfg->reader.eof = 0; - data_start = cfg->reader.read_ptr; + reader->read_ptr = reader->buffer.ptr; + reader->eof = 0; + data_start = reader->read_ptr; } else { return -1; /* OS error when reading the file */ } + write_start = data_start; + /* Lock the file */ - if (git_filebuf_open(&file, cfg->file_path, 0) < 0) - return -1; + if ((result = git_filebuf_open( + &file, cfg->file_path, 0, GIT_CONFIG_FILE_MODE)) < 0) { + git_buf_free(&reader->buffer); + return result; + } - skip_bom(cfg); + skip_bom(reader); ldot = strrchr(key, '.'); name = ldot + 1; section = git__strndup(key, ldot - key); - while (!cfg->reader.eof) { - c = cfg_peek(cfg, SKIP_WHITESPACE); + while (!reader->eof) { + c = reader_peek(reader, SKIP_WHITESPACE); if (c == '\0') { /* We've arrived at the end of the file */ break; @@ -1133,11 +1253,11 @@ static int config_write(diskfile_backend *cfg, const char *key, const regex_t *p * new section. If we actually want to replace it, the * default case will take care of updating them. */ - pre_end = post_start = cfg->reader.read_ptr; + pre_end = post_start = reader->read_ptr; git__free(current_section); current_section = NULL; - if (parse_section_header(cfg, ¤t_section) < 0) + if (parse_section_header(reader, ¤t_section) < 0) goto rewrite_fail; /* Keep track of when it stops matching */ @@ -1146,7 +1266,7 @@ static int config_write(diskfile_backend *cfg, const char *key, const regex_t *p } else if (c == ';' || c == '#') { - cfg_consume_line(cfg); + reader_consume_line(reader); } else { @@ -1162,15 +1282,15 @@ static int config_write(diskfile_backend *cfg, const char *key, const regex_t *p */ if (!section_matches) { if (!last_section_matched) { - cfg_consume_line(cfg); + reader_consume_line(reader); continue; } } else { int has_matched = 0; char *var_name, *var_value; - pre_end = cfg->reader.read_ptr; - if (parse_variable(cfg, &var_name, &var_value) < 0) + pre_end = reader->read_ptr; + if (parse_variable(reader, &var_name, &var_value) < 0) goto rewrite_fail; /* First try to match the name of the variable */ @@ -1189,23 +1309,28 @@ static int config_write(diskfile_backend *cfg, const char *key, const regex_t *p if (!has_matched) continue; - post_start = cfg->reader.read_ptr; + post_start = reader->read_ptr; } /* 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); + git_filebuf_write(&file, write_start, pre_end - write_start); preg_replaced = 1; /* 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); + const char *q = quotes_for_value(value); + git_filebuf_printf(&file, "\t%s = %s%s%s\n", name, q, value, q); } - /* multiline variable? we need to keep reading lines to match */ - if (preg != NULL) { - data_start = post_start; + /* + * If we have a multivar, we should keep looking for entries, + * but only if we're in the right section. Otherwise we'll end up + * looping on the edge of a matching and a non-matching section. + */ + if (section_matches && preg != NULL) { + write_start = post_start; continue; } @@ -1232,12 +1357,14 @@ static int config_write(diskfile_backend *cfg, const char *key, const regex_t *p */ if (write_trailer) { /* Write out rest of the file */ - git_filebuf_write(&file, post_start, cfg->reader.buffer.size - (post_start - data_start)); + git_filebuf_write(&file, post_start, reader->buffer.size - (post_start - data_start)); } else { if (preg_replaced) { - git_filebuf_printf(&file, "\n%s", data_start); + git_filebuf_printf(&file, "\n%s", write_start); } else { - git_filebuf_write(&file, cfg->reader.buffer.ptr, cfg->reader.buffer.size); + const char *q; + + git_filebuf_write(&file, reader->buffer.ptr, reader->buffer.size); /* And now if we just need to add a variable */ if (!section_matches && write_section(&file, section) < 0) @@ -1253,10 +1380,11 @@ static int config_write(diskfile_backend *cfg, const char *key, const regex_t *p } /* If we are here, there is at least a section line */ - if (cfg->reader.buffer.size > 0 && *(cfg->reader.buffer.ptr + cfg->reader.buffer.size - 1) != '\n') + if (reader->buffer.size > 0 && *(reader->buffer.ptr + reader->buffer.size - 1) != '\n') git_filebuf_write(&file, "\n", 1); - git_filebuf_printf(&file, "\t%s = %s\n", name, value); + q = quotes_for_value(value); + git_filebuf_printf(&file, "\t%s = %s%s%s\n", name, q, value, q); } } @@ -1264,10 +1392,10 @@ static int config_write(diskfile_backend *cfg, const char *key, const regex_t *p git__free(current_section); /* refresh stats - if this errors, then commit will error too */ - (void)git_filebuf_stats(&cfg->file_mtime, &cfg->file_size, &file); + (void)git_filebuf_stats(&reader->file_mtime, &reader->file_size, &file); - result = git_filebuf_commit(&file, GIT_CONFIG_FILE_MODE); - git_buf_free(&cfg->reader.buffer); + result = git_filebuf_commit(&file); + git_buf_free(&reader->buffer); return result; @@ -1276,7 +1404,7 @@ rewrite_fail: git__free(current_section); git_filebuf_cleanup(&file); - git_buf_free(&cfg->reader.buffer); + git_buf_free(&reader->buffer); return -1; } @@ -1293,6 +1421,9 @@ static char *escape_value(const char *ptr) assert(ptr); len = strlen(ptr); + if (!len) + return git__calloc(1, sizeof(char)); + git_buf_grow(&buf, len); while (*ptr != '\0') { @@ -1365,19 +1496,19 @@ static int is_multiline_var(const char *str) return (end > str) && (count & 1); } -static int parse_multiline_variable(diskfile_backend *cfg, git_buf *value, int in_quotes) +static int parse_multiline_variable(struct reader *reader, 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, false); + line = reader_readline(reader, false); if (line == NULL) return -1; /* We've reached the end of the file, there is input missing */ if (line[0] == '\0') { - set_parse_error(cfg, 0, "Unexpected end of file while parsing multine var"); + set_parse_error(reader, 0, "Unexpected end of file while parsing multine var"); git__free(line); return -1; } @@ -1387,7 +1518,7 @@ static int parse_multiline_variable(diskfile_backend *cfg, git_buf *value, int i /* If it was just a comment, pretend it didn't exist */ if (line[0] == '\0') { git__free(line); - return parse_multiline_variable(cfg, value, quote_count); + return parse_multiline_variable(reader, value, quote_count); /* TODO: unbounded recursion. This **could** be exploitable */ } @@ -1395,7 +1526,7 @@ static int parse_multiline_variable(diskfile_backend *cfg, git_buf *value, int i * 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); + git_buf_shorten(value, 1); proc_line = fixup_line(line, in_quotes); if (proc_line == NULL) { @@ -1412,19 +1543,19 @@ static int parse_multiline_variable(diskfile_backend *cfg, git_buf *value, int i * keep putting stuff in the buffer */ if (is_multiline_var(value->ptr)) - return parse_multiline_variable(cfg, value, quote_count); + return parse_multiline_variable(reader, value, quote_count); return 0; } -static int parse_variable(diskfile_backend *cfg, char **var_name, char **var_value) +static int parse_variable(struct reader *reader, char **var_name, char **var_value) { const char *var_end = NULL; const char *value_start = NULL; char *line; int quote_count; - line = cfg_readline(cfg, true); + line = reader_readline(reader, true); if (line == NULL) return -1; @@ -1459,7 +1590,7 @@ static int parse_variable(diskfile_backend *cfg, char **var_name, char **var_val 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)) { + if (parse_multiline_variable(reader, &multi_value, quote_count) < 0 || git_buf_oom(&multi_value)) { git__free(*var_name); git__free(line); git_buf_free(&multi_value); |