/* * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. */ #include "common.h" #include "config.h" #include "fileops.h" #include "filebuf.h" #include "buffer.h" #include "buf_text.h" #include "git2/config.h" #include "git2/types.h" #include "strmap.h" #include #include #include GIT__USE_STRMAP; typedef struct cvar_t { struct cvar_t *next; git_config_entry *entry; } cvar_t; #define CVAR_LIST_HEAD(list) ((list)->head) #define CVAR_LIST_TAIL(list) ((list)->tail) #define CVAR_LIST_NEXT(var) ((var)->next) #define CVAR_LIST_EMPTY(list) ((list)->head == NULL) #define CVAR_LIST_APPEND(list, var) do {\ if (CVAR_LIST_EMPTY(list)) {\ CVAR_LIST_HEAD(list) = CVAR_LIST_TAIL(list) = var;\ } else {\ CVAR_LIST_NEXT(CVAR_LIST_TAIL(list)) = var;\ CVAR_LIST_TAIL(list) = var;\ }\ } while(0) #define CVAR_LIST_REMOVE_HEAD(list) do {\ CVAR_LIST_HEAD(list) = CVAR_LIST_NEXT(CVAR_LIST_HEAD(list));\ } while(0) #define CVAR_LIST_REMOVE_AFTER(var) do {\ CVAR_LIST_NEXT(var) = CVAR_LIST_NEXT(CVAR_LIST_NEXT(var));\ } while(0) #define CVAR_LIST_FOREACH(list, iter)\ for ((iter) = CVAR_LIST_HEAD(list);\ (iter) != NULL;\ (iter) = CVAR_LIST_NEXT(iter)) /* * Inspired by the FreeBSD functions */ #define CVAR_LIST_FOREACH_SAFE(start, iter, tmp)\ for ((iter) = CVAR_LIST_HEAD(vars);\ (iter) && (((tmp) = CVAR_LIST_NEXT(iter) || 1));\ (iter) = (tmp)) typedef struct { git_config_backend parent; git_strmap *values; struct { git_buf buffer; char *read_ptr; int line_number; int eof; } reader; char *file_path; time_t file_mtime; size_t file_size; unsigned int level; } diskfile_backend; static int config_parse(diskfile_backend *cfg_file, unsigned int level); static int parse_variable(diskfile_backend *cfg, 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) { 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((char*)var->entry->name); git__free((char *)var->entry->value); git__free(var->entry); git__free(var); } int git_config_file_normalize_section(char *start, char *end) { char *scan; if (start == end) return GIT_EINVALIDSPEC; /* Validate and downcase range */ for (scan = start; *scan; ++scan) { if (end && scan >= end) break; if (isalnum(*scan)) *scan = tolower(*scan); else if (*scan != '-' || scan == start) return GIT_EINVALIDSPEC; } if (scan == start) return GIT_EINVALIDSPEC; 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; if (values == NULL) return; git_strmap_foreach_value(values, var, while (var != NULL) { cvar_t *next = CVAR_LIST_NEXT(var); cvar_free(var); var = next; }); git_strmap_free(values); } static int config_open(git_config_backend *cfg, unsigned int level) { int res; diskfile_backend *b = (diskfile_backend *)cfg; b->level = level; b->values = git_strmap_alloc(); GITERR_CHECK_ALLOC(b->values); git_buf_init(&b->reader.buffer, 0); res = git_futils_readbuffer_updated( &b->reader.buffer, b->file_path, &b->file_mtime, &b->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) { free_vars(b->values); b->values = NULL; } git_buf_free(&b->reader.buffer); return res; } static int config_refresh(git_config_backend *cfg) { int res, updated = 0; diskfile_backend *b = (diskfile_backend *)cfg; git_strmap *old_values; res = git_futils_readbuffer_updated( &b->reader.buffer, b->file_path, &b->file_mtime, &b->file_size, &updated); if (res < 0 || !updated) return (res == GIT_ENOTFOUND) ? 0 : res; /* need to reload - store old values and prep for reload */ old_values = b->values; b->values = git_strmap_alloc(); GITERR_CHECK_ALLOC(b->values); if ((res = config_parse(b, b->level)) < 0) { free_vars(b->values); b->values = old_values; } else { free_vars(old_values); } git_buf_free(&b->reader.buffer); return res; } static void backend_free(git_config_backend *_backend) { diskfile_backend *backend = (diskfile_backend *)_backend; if (backend == NULL) return; 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) { diskfile_backend *b = (diskfile_backend *)backend; cvar_t *var, *next_var; const char *key; regex_t regex; int result = 0; if (!b->values) return 0; if (regexp != NULL) { if ((result = regcomp(®ex, regexp, REG_EXTENDED)) < 0) { giterr_set_regex(®ex, result); regfree(®ex); return -1; } } git_strmap_foreach(b->values, key, var, for (; var != NULL; var = next_var) { next_var = CVAR_LIST_NEXT(var); /* skip non-matching keys if regexp was provided */ if (regexp && regexec(®ex, key, 0, NULL, 0) != 0) continue; /* abort iterator on non-zero return value */ if (fn(var->entry, data)) { giterr_clear(); result = GIT_EUSER; goto cleanup; } } ); cleanup: if (regexp != NULL) regfree(®ex); return result; } static int config_set(git_config_backend *cfg, const char *name, const char *value) { cvar_t *var = NULL, *old_var; diskfile_backend *b = (diskfile_backend *)cfg; char *key, *esc_value = NULL; khiter_t pos; int rval, ret; if ((rval = normalize_name(name, &key)) < 0) return rval; /* * Try to find it in the existing values and update it if it * only has one value. */ 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; } /* don't update if old and new values already match */ if ((!existing->entry->value && !value) || (existing->entry->value && value && !strcmp(existing->entry->value, value))) return 0; if (value) { tmp = git__strdup(value); GITERR_CHECK_ALLOC(tmp); esc_value = escape_value(value); GITERR_CHECK_ALLOC(esc_value); } git__free((void *)existing->entry->value); existing->entry->value = tmp; ret = config_write(b, existing->entry->name, NULL, esc_value); git__free(esc_value); return ret; } var = git__malloc(sizeof(cvar_t)); GITERR_CHECK_ALLOC(var); memset(var, 0x0, sizeof(cvar_t)); var->entry = git__malloc(sizeof(git_config_entry)); GITERR_CHECK_ALLOC(var->entry); memset(var->entry, 0x0, sizeof(git_config_entry)); var->entry->name = key; var->entry->value = NULL; if (value) { var->entry->value = git__strdup(value); GITERR_CHECK_ALLOC(var->entry->value); esc_value = escape_value(value); GITERR_CHECK_ALLOC(esc_value); } if (config_write(b, key, NULL, esc_value) < 0) { git__free(esc_value); cvar_free(var); return -1; } git__free(esc_value); 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; } /* * Internal function that actually gets the value in string form */ static int config_get(const git_config_backend *cfg, const char *name, const git_config_entry **out) { diskfile_backend *b = (diskfile_backend *)cfg; 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) return error; pos = git_strmap_lookup_index(b->values, key); git__free(key); if (!git_strmap_valid_index(b->values, pos)) return GIT_ENOTFOUND; var = git_strmap_value_at(b->values, pos); 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); } return 0; } static int config_set_multivar( git_config_backend *cfg, const char *name, const char *regexp, const char *value) { int replaced = 0; cvar_t *var, *newvar; diskfile_backend *b = (diskfile_backend *)cfg; char *key; regex_t preg; int result; khiter_t pos; assert(regexp); if ((result = normalize_name(name, &key)) < 0) return result; pos = git_strmap_lookup_index(b->values, key); if (!git_strmap_valid_index(b->values, pos)) { 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; } for (;;) { if (regexec(&preg, var->entry->value, 0, NULL, 0) == 0) { char *tmp = git__strdup(value); GITERR_CHECK_ALLOC(tmp); git__free((void *)var->entry->value); var->entry->value = tmp; replaced = 1; } if (var->next == NULL) break; var = var->next; } /* 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->entry = git__malloc(sizeof(git_config_entry)); GITERR_CHECK_ALLOC(newvar->entry); memset(newvar->entry, 0x0, sizeof(git_config_entry)); newvar->entry->name = git__strdup(var->entry->name); GITERR_CHECK_ALLOC(newvar->entry->name); newvar->entry->value = git__strdup(value); GITERR_CHECK_ALLOC(newvar->entry->value); newvar->entry->level = var->entry->level; var->next = newvar; } result = config_write(b, key, &preg, value); git__free(key); regfree(&preg); return result; } static int config_delete(git_config_backend *cfg, const char *name) { cvar_t *var; diskfile_backend *b = (diskfile_backend *)cfg; char *key; int result; khiter_t pos; if ((result = normalize_name(name, &key)) < 0) return result; pos = git_strmap_lookup_index(b->values, key); git__free(key); if (!git_strmap_valid_index(b->values, pos)) { giterr_set(GITERR_CONFIG, "Could not find key '%s' to delete", name); return GIT_ENOTFOUND; } var = git_strmap_value_at(b->values, pos); if (var->next != NULL) { giterr_set(GITERR_CONFIG, "Cannot delete multivar with a single delete"); return -1; } git_strmap_delete_at(b->values, pos); result = config_write(b, var->entry->name, NULL, NULL); cvar_free(var); return result; } int git_config_file__ondisk(git_config_backend **out, const char *path) { diskfile_backend *backend; backend = git__calloc(1, sizeof(diskfile_backend)); GITERR_CHECK_ALLOC(backend); backend->parent.version = GIT_CONFIG_BACKEND_VERSION; backend->file_path = git__strdup(path); 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.refresh = config_refresh; backend->parent.free = backend_free; *out = (git_config_backend *)backend; return 0; } static int cfg_getchar_raw(diskfile_backend *cfg) { int c; c = *cfg->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++; c = '\n'; } if (c == '\n') cfg->reader.line_number++; if (c == 0) { cfg->reader.eof = 1; c = '\n'; } return c; } #define SKIP_WHITESPACE (1 << 1) #define SKIP_COMMENTS (1 << 2) static int cfg_getchar(diskfile_backend *cfg_file, int flags) { const int skip_whitespace = (flags & SKIP_WHITESPACE); const int skip_comments = (flags & SKIP_COMMENTS); int c; assert(cfg_file->reader.read_ptr); do c = cfg_getchar_raw(cfg_file); while (skip_whitespace && git__isspace(c) && !cfg_file->reader.eof); if (skip_comments && (c == '#' || c == ';')) { do c = cfg_getchar_raw(cfg_file); while (c != '\n'); } return c; } /* * Read the next char, but don't move the reading pointer. */ static int cfg_peek(diskfile_backend *cfg, int flags) { void *old_read_ptr; int old_lineno, old_eof; int ret; assert(cfg->reader.read_ptr); old_read_ptr = cfg->reader.read_ptr; old_lineno = cfg->reader.line_number; old_eof = cfg->reader.eof; ret = cfg_getchar(cfg, flags); cfg->reader.read_ptr = old_read_ptr; cfg->reader.line_number = old_lineno; cfg->reader.eof = old_eof; return ret; } /* * Read and consume a line, returning it in newly-allocated memory. */ static char *cfg_readline(diskfile_backend *cfg, bool skip_whitespace) { char *line = NULL; char *line_src, *line_end; size_t line_len; line_src = cfg->reader.read_ptr; if (skip_whitespace) { /* Skip empty empty lines */ while (git__isspace(*line_src)) ++line_src; } line_end = strchr(line_src, '\n'); /* no newline at EOF */ if (line_end == NULL) line_end = strchr(line_src, 0); line_len = line_end - line_src; line = git__malloc(line_len + 1); if (line == NULL) return NULL; memcpy(line, line_src, line_len); do line[line_len] = '\0'; while (line_len-- > 0 && git__isspace(line[line_len])); if (*line_end == '\n') line_end++; if (*line_end == '\0') cfg->reader.eof = 1; cfg->reader.line_number++; cfg->reader.read_ptr = line_end; return line; } /* * Consume a line, without storing it anywhere */ static void cfg_consume_line(diskfile_backend *cfg) { char *line_start, *line_end; line_start = cfg->reader.read_ptr; line_end = strchr(line_start, '\n'); /* No newline at EOF */ if(line_end == NULL){ line_end = strchr(line_start, '\0'); } if (*line_end == '\n') line_end++; if (*line_end == '\0') cfg->reader.eof = 1; cfg->reader.line_number++; cfg->reader.read_ptr = line_end; } 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) { 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 * sync so we only really use it to calculate the length. */ first_quote = strchr(line, '"'); last_quote = strrchr(line, '"'); 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); rpos = 0; quote_marks = 0; line = first_quote; 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"); git_buf_free(&buf); return -1; } switch (c) { case '"': ++quote_marks; continue; case '\\': c = line[rpos++]; switch (c) { case '"': case '\\': break; default: set_parse_error(cfg, rpos, "Unsupported escape sequence"); git_buf_free(&buf); return -1; } default: break; } git_buf_putc(&buf, c); } while ((c = line[rpos++]) != ']'); *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 result; char *line; line = cfg_readline(cfg, true); if (line == NULL) return -1; /* find the end of the variable's name */ name_end = strchr(line, ']'); if (name_end == NULL) { git__free(line); set_parse_error(cfg, 0, "Missing ']' in section header"); return -1; } name = (char *)git__malloc((size_t)(name_end - line) + 1); GITERR_CHECK_ALLOC(name); name_length = 0; pos = 0; /* Make sure we were given a section header */ c = line[pos++]; assert(c == '['); c = line[pos++]; do { if (git__isspace(c)){ name[name_length] = '\0'; result = parse_section_header_ext(cfg, 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"); goto fail_parse; } name[name_length++] = (char) tolower(c); } while ((c = line[pos++]) != ']'); if (line[pos - 1] != ']') { set_parse_error(cfg, pos, "Unexpected end of file"); goto fail_parse; } git__free(line); name[name_length] = 0; *section_out = name; return 0; fail_parse: git__free(line); git__free(name); return -1; } static int skip_bom(diskfile_backend *cfg) { git_bom_t bom; int bom_offset = git_buf_text_detect_bom(&bom, &cfg->reader.buffer, cfg->reader.read_ptr - cfg->reader.buffer.ptr); if (bom == GIT_BOM_UTF8) cfg->reader.read_ptr += bom_offset; /* TODO: reference implementation is pretty stupid with BoM */ return 0; } /* (* basic types *) digit = "0".."9" integer = digit { digit } alphabet = "a".."z" + "A" .. "Z" section_char = alphabet | "." | "-" extension_char = (* any character except newline *) any_char = (* any character *) variable_char = "alphabet" | "-" (* actual grammar *) config = { section } section = header { definition } header = "[" section [subsection | subsection_ext] "]" subsection = "." section subsection_ext = "\"" extension "\"" section = section_char { section_char } extension = extension_char { extension_char } definition = variable_name ["=" variable_value] "\n" variable_name = variable_char { variable_char } variable_value = string | boolean | integer string = quoted_string | plain_string quoted_string = "\"" plain_string "\"" plain_string = { any_char } boolean = boolean_true | boolean_false boolean_true = "yes" | "1" | "true" | "on" boolean_false = "no" | "0" | "false" | "off" */ static int strip_comments(char *line, int in_quotes) { int quote_count = in_quotes; char *ptr; for (ptr = line; *ptr; ++ptr) { if (ptr[0] == '"' && ptr > line && ptr[-1] != '\\') quote_count++; if ((ptr[0] == ';' || ptr[0] == '#') && (quote_count % 2) == 0) { ptr[0] = '\0'; break; } } /* skip any space at the end */ if (ptr > line && git__isspace(ptr[-1])) { ptr--; } ptr[0] = '\0'; return quote_count; } static int config_parse(diskfile_backend *cfg_file, unsigned int level) { int c; char *current_section = NULL; char *var_name; char *var_value; 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.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 0; skip_bom(cfg_file); while (result == 0 && !cfg_file->reader.eof) { c = cfg_peek(cfg_file, SKIP_WHITESPACE); switch (c) { case '\n': /* EOF when peeking, set EOF in the reader to exit the loop */ cfg_file->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); break; case ';': case '#': cfg_consume_line(cfg_file); break; default: /* assume variable declaration */ result = parse_variable(cfg_file, &var_name, &var_value); if (result < 0) break; var = git__malloc(sizeof(cvar_t)); GITERR_CHECK_ALLOC(var); memset(var, 0x0, sizeof(cvar_t)); var->entry = git__malloc(sizeof(git_config_entry)); GITERR_CHECK_ALLOC(var->entry); memset(var->entry, 0x0, sizeof(git_config_entry)); git__strtolower(var_name); git_buf_printf(&buf, "%s.%s", current_section, var_name); git__free(var_name); if (git_buf_oom(&buf)) return -1; var->entry->name = git_buf_detach(&buf); var->entry->value = var_value; var->entry->level = level; /* Add or append the new config option */ pos = git_strmap_lookup_index(cfg_file->values, var->entry->name); if (!git_strmap_valid_index(cfg_file->values, pos)) { git_strmap_insert(cfg_file->values, var->entry->name, 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 result; } static int write_section(git_filebuf *file, const char *key) { int result; const char *dot; git_buf buf = GIT_BUF_INIT; /* All of this just for [section "subsection"] */ dot = strchr(key, '.'); git_buf_putc(&buf, '['); if (dot == NULL) { git_buf_puts(&buf, key); } else { char *escaped; git_buf_put(&buf, key, dot - key); escaped = escape_value(dot + 1); GITERR_CHECK_ALLOC(escaped); git_buf_printf(&buf, " \"%s\"", escaped); git__free(escaped); } 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); return result; } /* * This is pretty much the parsing, except we write out anything we don't have */ static int config_write(diskfile_backend *cfg, const char *key, const regex_t *preg, const char* value) { 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; /* We need to read in our own config file */ result = git_futils_readbuffer(&cfg->reader.buffer, cfg->file_path); /* Initialise the reading position */ if (result == GIT_ENOTFOUND) { cfg->reader.read_ptr = NULL; cfg->reader.eof = 1; data_start = NULL; 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 */ 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 (!cfg->reader.eof) { c = cfg_peek(cfg, SKIP_WHITESPACE); if (c == '\0') { /* We've arrived at the end of the file */ break; } 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 * case, we want both variables to point just before the * 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; 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, section); } else if (c == ';' || c == '#') { cfg_consume_line(cfg); } else { /* * If the section doesn't match, but the last section did, * it means we need to add a variable (so skip the line * otherwise). If both the section and name match, we need * to overwrite the variable (so skip the line * otherwise). pre_end needs to be updated each time so we * don't loose that information, but we only need to * update post_start if we're going to use it in this * iteration. */ if (!section_matches) { if (!last_section_matched) { cfg_consume_line(cfg); 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) 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 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 */ 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 don't write anything. */ if (value != NULL) { git_filebuf_printf(&file, "\t%s = %s\n", name, value); } /* multiline variable? we need to keep reading lines to match */ if (preg != NULL) { data_start = post_start; continue; } write_trailer = 1; break; /* break from the loop */ } } /* * Being here can mean that * * 1) our section is the last one in the file and we're * adding a variable * * 2) we didn't find a section for us so we need to create it * ourselves. * * 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; } /* 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') git_filebuf_write(&file, "\n", 1); git_filebuf_printf(&file, "\t%s = %s\n", name, value); } } git__free(section); 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); result = git_filebuf_commit(&file, GIT_CONFIG_FILE_MODE); git_buf_free(&cfg->reader.buffer); return result; rewrite_fail: git__free(section); git__free(current_section); git_filebuf_cleanup(&file); git_buf_free(&cfg->reader.buffer); return -1; } static const char *escapes = "ntb\"\\"; static const char *escaped = "\n\t\b\"\\"; /* Escape the values to write them to the file */ static char *escape_value(const char *ptr) { git_buf buf = GIT_BUF_INIT; size_t len; const char *esc; assert(ptr); len = strlen(ptr); git_buf_grow(&buf, len); while (*ptr != '\0') { if ((esc = strchr(escaped, *ptr)) != NULL) { git_buf_putc(&buf, '\\'); git_buf_putc(&buf, escapes[esc - escaped]); } else { git_buf_putc(&buf, *ptr); } ptr++; } if (git_buf_oom(&buf)) { git_buf_free(&buf); return NULL; } return git_buf_detach(&buf); } /* '\"' -> '"' etc */ static char *fixup_line(const char *ptr, int quote_count) { char *str = git__malloc(strlen(ptr) + 1); char *out = str, *esc; if (str == NULL) return NULL; 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 is_multiline_var(const char *str) { int count = 0; const char *end = str + strlen(str); while (end > str && end[-1] == '\\') { count++; end--; } /* An odd number means last backslash wasn't escaped, so it's multiline */ return (end > str) && (count & 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, 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"); git__free(line); return -1; } quote_count = strip_comments(line, !!in_quotes); /* 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); /* TODO: unbounded recursion. This **could** be exploitable */ } /* 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); 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, let's just * keep putting stuff in the buffer */ if (is_multiline_var(value->ptr)) return parse_multiline_variable(cfg, value, quote_count); return 0; } static int parse_variable(diskfile_backend *cfg, 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); if (line == NULL) return -1; quote_count = strip_comments(line, 0); var_end = strchr(line, '='); if (var_end == NULL) var_end = strchr(line, '\0'); else value_start = var_end + 1; do var_end--; while (var_end>line && git__isspace(*var_end)); *var_name = git__strndup(line, var_end - line + 1); GITERR_CHECK_ALLOC(*var_name); /* 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 (git__isspace(value_start[0])) value_start++; if (is_multiline_var(value_start)) { 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; } *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); } else { /* equals sign but missing rhs */ *var_value = git__strdup(""); GITERR_CHECK_ALLOC(*var_value); } } git__free(line); return 0; }