| /* |
| * This file is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License, version 2, |
| * as published by the Free Software Foundation. |
| * |
| * In addition to the permissions in the GNU General Public License, |
| * the authors give you unlimited permission to link the compiled |
| * version of this file into combinations with other programs, |
| * and to distribute those combinations without any restriction |
| * coming from the use of this file. (The General Public License |
| * restrictions do apply in other respects; for example, they cover |
| * modification of the file, and distribution when not linked into |
| * a combined executable.) |
| * |
| * This file is distributed in the hope that it will be useful, but |
| * WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; see the file COPYING. If not, write to |
| * the Free Software Foundation, 51 Franklin Street, Fifth Floor, |
| * Boston, MA 02110-1301, USA. |
| */ |
| |
| #include "common.h" |
| #include "config.h" |
| #include "fileops.h" |
| #include "filebuf.h" |
| #include "git2/config.h" |
| #include "git2/types.h" |
| |
| |
| #include <ctype.h> |
| |
| typedef struct cvar_t { |
| struct cvar_t *next; |
| char *section; |
| char *name; |
| char *value; |
| } cvar_t; |
| |
| typedef struct { |
| struct cvar_t *head; |
| struct cvar_t *tail; |
| } cvar_t_list; |
| |
| #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_file parent; |
| |
| cvar_t_list var_list; |
| |
| struct { |
| git_fbuffer buffer; |
| char *read_ptr; |
| int line_number; |
| int eof; |
| } reader; |
| |
| char *file_path; |
| } diskfile_backend; |
| |
| 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 void cvar_free(cvar_t *var) |
| { |
| if (var == NULL) |
| return; |
| |
| free(var->section); |
| free(var->name); |
| free(var->value); |
| free(var); |
| } |
| |
| static void cvar_list_free(cvar_t_list *list) |
| { |
| cvar_t *cur; |
| |
| while (!CVAR_LIST_EMPTY(list)) { |
| cur = CVAR_LIST_HEAD(list); |
| CVAR_LIST_REMOVE_HEAD(list); |
| cvar_free(cur); |
| } |
| } |
| |
| /* |
| * Compare two strings according to the git section-subsection |
| * rules. The order of the strings is important because local is |
| * assumed to have the internal format (only the section name and with |
| * case information) and input the normalized one (only dots, no case |
| * information). |
| */ |
| static int cvar_match_section(const char *local, const char *input) |
| { |
| char *first_dot; |
| char *local_sp = strchr(local, ' '); |
| int comparison_len; |
| |
| /* |
| * If the local section name doesn't contain a space, then we can |
| * just do a case-insensitive compare. |
| */ |
| if (local_sp == NULL) |
| return !strncasecmp(local, input, strlen(local)); |
| |
| /* |
| * From here onwards, there is a space diving the section and the |
| * subsection. Anything before the space in local is |
| * case-insensitive. |
| */ |
| if (strncasecmp(local, input, local_sp - local)) |
| return 0; |
| |
| /* |
| * We compare starting from the first character after the |
| * quotation marks, which is two characters beyond the space. For |
| * the input, we start one character beyond the dot. If the names |
| * have different lengths, then we can fail early, as we know they |
| * can't be the same. |
| * The length is given by the length between the quotation marks. |
| */ |
| |
| first_dot = strchr(input, '.'); |
| comparison_len = strlen(local_sp + 2) - 1; |
| |
| return !strncmp(local_sp + 2, first_dot + 1, comparison_len); |
| } |
| |
| static int cvar_match_name(const cvar_t *var, const char *str) |
| { |
| const char *name_start; |
| |
| 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; |
| |
| return !strcasecmp(var->name, name_start); |
| } |
| |
| static cvar_t *cvar_list_find(cvar_t_list *list, const char *name) |
| { |
| cvar_t *iter; |
| |
| CVAR_LIST_FOREACH (list, iter) { |
| if (cvar_match_name(iter, name)) |
| return iter; |
| } |
| |
| return NULL; |
| } |
| |
| static int cvar_normalize_name(cvar_t *var, char **output) |
| { |
| char *section_sp = strchr(var->section, ' '); |
| char *quote, *name; |
| int len, ret; |
| |
| /* |
| * The final string is going to be at most one char longer than |
| * the input |
| */ |
| 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 = snprintf(name, len + 1, "%s.%s", var->section, var->name); |
| if (ret < 0) |
| return git__throw(GIT_EOSERR, "Failed to normalize name. OS err: %s", strerror(errno)); |
| |
| *output = name; |
| return GIT_SUCCESS; |
| } |
| |
| /* |
| * 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; |
| } |
| |
| static char *interiorize_section(const char *orig) |
| { |
| char *dot, *last_dot, *section, *ret; |
| int len; |
| |
| dot = strchr(orig, '.'); |
| last_dot = strrchr(orig, '.'); |
| len = last_dot - orig; |
| |
| /* No subsection, this is easy */ |
| if (last_dot == dot) |
| return git__strndup(orig, dot - orig); |
| |
| section = git__malloc(len + 4); |
| if (section == NULL) |
| return NULL; |
| |
| memset(section, 0x0, len + 4); |
| ret = section; |
| len = dot - orig; |
| memcpy(section, orig, len); |
| section += len; |
| len = STRLEN(" \""); |
| memcpy(section, " \"", len); |
| section += len; |
| len = last_dot - dot - 1; |
| memcpy(section, dot + 1, len); |
| section += len; |
| *section = '"'; |
| |
| return ret; |
| } |
| |
| static int config_open(git_config_file *cfg) |
| { |
| int error; |
| diskfile_backend *b = (diskfile_backend *)cfg; |
| |
| error = git_futils_readbuffer(&b->reader.buffer, b->file_path); |
| if(error < GIT_SUCCESS) |
| goto cleanup; |
| |
| error = config_parse(b); |
| if (error < GIT_SUCCESS) |
| goto cleanup; |
| |
| git_futils_freebuffer(&b->reader.buffer); |
| |
| return error; |
| |
| cleanup: |
| cvar_list_free(&b->var_list); |
| git_futils_freebuffer(&b->reader.buffer); |
| |
| return git__rethrow(error, "Failed to open config"); |
| } |
| |
| static void backend_free(git_config_file *_backend) |
| { |
| diskfile_backend *backend = (diskfile_backend *)_backend; |
| |
| if (backend == NULL) |
| return; |
| |
| free(backend->file_path); |
| cvar_list_free(&backend->var_list); |
| |
| free(backend); |
| } |
| |
| 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; |
| |
| CVAR_LIST_FOREACH(&b->var_list, var) { |
| char *normalized = NULL; |
| |
| ret = cvar_normalize_name(var, &normalized); |
| if (ret < GIT_SUCCESS) |
| return ret; |
| |
| ret = fn(normalized, var->value, data); |
| free(normalized); |
| if (ret) |
| break; |
| } |
| |
| return ret; |
| } |
| |
| static int config_set(git_config_file *cfg, const char *name, const char *value) |
| { |
| cvar_t *var = NULL; |
| cvar_t *existing = NULL; |
| int error = GIT_SUCCESS; |
| const char *last_dot; |
| diskfile_backend *b = (diskfile_backend *)cfg; |
| |
| /* |
| * 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; |
| |
| free(existing->value); |
| existing->value = tmp; |
| |
| return config_write(b, existing); |
| } |
| |
| /* |
| * Otherwise, create it and stick it at the end of the queue. |
| */ |
| |
| last_dot = strrchr(name, '.'); |
| if (last_dot == NULL) { |
| return git__throw(GIT_EINVALIDTYPE, "Variables without section aren't allowed"); |
| } |
| |
| var = git__malloc(sizeof(cvar_t)); |
| if (var == NULL) |
| return GIT_ENOMEM; |
| |
| memset(var, 0x0, sizeof(cvar_t)); |
| |
| var->section = interiorize_section(name); |
| if (var->section == NULL) { |
| error = GIT_ENOMEM; |
| goto out; |
| } |
| |
| var->name = git__strdup(last_dot + 1); |
| if (var->name == NULL) { |
| error = GIT_ENOMEM; |
| goto out; |
| } |
| |
| var->value = value ? git__strdup(value) : NULL; |
| if (var->value == NULL && value != NULL) { |
| error = GIT_ENOMEM; |
| goto out; |
| } |
| |
| CVAR_LIST_APPEND(&b->var_list, var); |
| error = config_write(b, var); |
| |
| out: |
| if (error < GIT_SUCCESS) |
| cvar_free(var); |
| |
| return error == GIT_SUCCESS ? GIT_SUCCESS : git__rethrow(error, "Failed to set config value"); |
| } |
| |
| /* |
| * Internal function that actually gets the value in string form |
| */ |
| static int config_get(git_config_file *cfg, const char *name, const char **out) |
| { |
| cvar_t *var; |
| int error = GIT_SUCCESS; |
| diskfile_backend *b = (diskfile_backend *)cfg; |
| |
| var = cvar_list_find(&b->var_list, name); |
| |
| if (var == NULL) |
| return git__throw(GIT_ENOTFOUND, "Variable '%s' not found", name); |
| |
| *out = var->value; |
| |
| return error == GIT_SUCCESS ? GIT_SUCCESS : git__rethrow(error, "Failed to get config value for %s", name); |
| } |
| |
| 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; |
| |
| memset(backend, 0x0, sizeof(diskfile_backend)); |
| |
| backend->file_path = git__strdup(path); |
| if (backend->file_path == NULL) { |
| free(backend); |
| return GIT_ENOMEM; |
| } |
| |
| backend->parent.open = config_open; |
| backend->parent.get = config_get; |
| backend->parent.set = config_set; |
| backend->parent.foreach = file_foreach; |
| backend->parent.free = backend_free; |
| |
| *out = (git_config_file *)backend; |
| |
| return GIT_SUCCESS; |
| } |
| |
| 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 && isspace(c)); |
| |
| 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) |
| { |
| char *line = NULL; |
| char *line_src, *line_end; |
| int line_len; |
| |
| line_src = cfg->reader.read_ptr; |
| |
| /* Skip empty empty lines */ |
| while (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); |
| |
| line[line_len] = '\0'; |
| |
| while (--line_len >= 0 && isspace(line[line_len])) |
| line[line_len] = '\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; |
| |
| return line; |
| } |
| |
| /* |
| * Consume a line, without storing it anywhere |
| */ |
| 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(const char *line, const char *base_name, char **section_name) |
| { |
| int buf_len, total_len, pos, rpos; |
| int c, ret; |
| char *subsection, *first_quote, *last_quote; |
| int error = GIT_SUCCESS; |
| 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) |
| return git__throw(GIT_EOBJCORRUPTED, "Failed to parse ext header. There is no final quotation mark"); |
| |
| buf_len = last_quote - first_quote + 2; |
| |
| subsection = git__malloc(buf_len + 2); |
| if (subsection == NULL) |
| return GIT_ENOMEM; |
| |
| pos = 0; |
| 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) { |
| error = git__throw(GIT_EOBJCORRUPTED, "Falied to parse ext header. Text after closing quote"); |
| goto out; |
| |
| } |
| |
| switch (c) { |
| case '"': |
| ++quote_marks; |
| break; |
| 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; |
| } |
| break; |
| default: |
| break; |
| } |
| |
| subsection[pos++] = (char) c; |
| } while ((c = line[rpos++]) != ']'); |
| |
| subsection[pos] = '\0'; |
| |
| total_len = strlen(base_name) + strlen(subsection) + 2; |
| *section_name = git__malloc(total_len); |
| if (*section_name == NULL) { |
| error = GIT_ENOMEM; |
| goto out; |
| } |
| |
| ret = snprintf(*section_name, total_len, "%s %s", base_name, subsection); |
| if (ret >= total_len) { |
| /* If this fails, we've checked the length wrong */ |
| error = git__throw(GIT_ERROR, "Failed to parse ext header. Wrong total length calculation"); |
| goto out; |
| } else if (ret < 0) { |
| error = git__throw(GIT_EOSERR, "Failed to parse ext header. OS error: %s", strerror(errno)); |
| goto out; |
| } |
| |
| git__strntolower(*section_name, strchr(*section_name, ' ') - *section_name); |
| |
| out: |
| free(subsection); |
| |
| return error; |
| } |
| |
| static int parse_section_header(diskfile_backend *cfg, char **section_out) |
| { |
| char *name, *name_end; |
| int name_length, c, pos; |
| int error = GIT_SUCCESS; |
| char *line; |
| |
| line = cfg_readline(cfg); |
| if (line == NULL) |
| return GIT_ENOMEM; |
| |
| /* find the end of the variable's name */ |
| name_end = strchr(line, ']'); |
| if (name_end == NULL) |
| return git__throw(GIT_EOBJCORRUPTED, "Failed to parse header. Can't find header name end"); |
| |
| name = (char *)git__malloc((size_t)(name_end - line) + 1); |
| if (name == NULL) |
| return GIT_ENOMEM; |
| |
| 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; |
| } |
| |
| c = line[pos++]; |
| |
| do { |
| if (cfg->reader.eof){ |
| error = git__throw(GIT_EOBJCORRUPTED, "Failed to parse header. Config file ended unexpectedly"); |
| goto error; |
| } |
| |
| if (isspace(c)){ |
| name[name_length] = '\0'; |
| error = parse_section_header_ext(line, name, section_out); |
| free(line); |
| free(name); |
| return error == GIT_SUCCESS ? GIT_SUCCESS : git__rethrow(error, "Failed to parse header"); |
| } |
| |
| if (!config_keychar(c) && c != '.') { |
| error = git__throw(GIT_EOBJCORRUPTED, "Failed to parse header. Wrong format on header"); |
| goto error; |
| } |
| |
| name[name_length++] = (char) tolower(c); |
| |
| } while ((c = line[pos++]) != ']'); |
| |
| name[name_length] = 0; |
| free(line); |
| git__strtolower(name); |
| *section_out = name; |
| return GIT_SUCCESS; |
| |
| error: |
| free(line); |
| free(name); |
| return error; |
| } |
| |
| static int skip_bom(diskfile_backend *cfg) |
| { |
| static const char *utf8_bom = "\xef\xbb\xbf"; |
| |
| if (memcmp(cfg->reader.read_ptr, utf8_bom, sizeof(utf8_bom)) == 0) |
| cfg->reader.read_ptr += sizeof(utf8_bom); |
| |
| /* TODO: the reference implementation does pretty stupid |
| shit with the BoM |
| */ |
| |
| return GIT_SUCCESS; |
| } |
| |
| /* |
| (* 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 void strip_comments(char *line) |
| { |
| int quote_count = 0; |
| 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; |
| } |
| } |
| |
| if (isspace(ptr[-1])) { |
| /* TODO skip whitespace */ |
| } |
| } |
| |
| static int config_parse(diskfile_backend *cfg_file) |
| { |
| int error = GIT_SUCCESS, c; |
| char *current_section = NULL; |
| char *var_name; |
| char *var_value; |
| cvar_t *var; |
| |
| /* Initialize the reading position */ |
| cfg_file->reader.read_ptr = cfg_file->reader.buffer.data; |
| 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; |
| |
| skip_bom(cfg_file); |
| |
| while (error == GIT_SUCCESS && !cfg_file->reader.eof) { |
| |
| c = cfg_peek(cfg_file, SKIP_WHITESPACE); |
| |
| switch (c) { |
| case '\0': /* We've arrived at the end of the file */ |
| break; |
| |
| case '[': /* section header, new section begins */ |
| free(current_section); |
| current_section = NULL; |
| error = parse_section_header(cfg_file, ¤t_section); |
| break; |
| |
| case ';': |
| case '#': |
| cfg_consume_line(cfg_file); |
| break; |
| |
| default: /* assume variable declaration */ |
| error = parse_variable(cfg_file, &var_name, &var_value); |
| |
| if (error < GIT_SUCCESS) |
| break; |
| |
| var = malloc(sizeof(cvar_t)); |
| if (var == NULL) { |
| error = GIT_ENOMEM; |
| break; |
| } |
| |
| memset(var, 0x0, sizeof(cvar_t)); |
| |
| var->section = git__strdup(current_section); |
| if (var->section == NULL) { |
| error = GIT_ENOMEM; |
| free(var); |
| break; |
| } |
| |
| var->name = var_name; |
| var->value = var_value; |
| git__strtolower(var->name); |
| |
| CVAR_LIST_APPEND(&cfg_file->var_list, var); |
| |
| break; |
| } |
| } |
| |
| free(current_section); |
| |
| return error == GIT_SUCCESS ? GIT_SUCCESS : git__rethrow(error, "Failed to parse config"); |
| } |
| |
| static int write_section(git_filebuf *file, cvar_t *var) |
| { |
| int error; |
| |
| error = git_filebuf_printf(file, "[%s]\n", var->section); |
| if (error < GIT_SUCCESS) |
| return error; |
| |
| error = git_filebuf_printf(file, " %s = %s\n", var->name, var->value); |
| return error; |
| } |
| |
| /* |
| * 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) |
| { |
| int error = GIT_SUCCESS, c; |
| int section_matches = 0, last_section_matched = 0; |
| char *current_section = NULL; |
| char *var_name, *var_value, *data_start; |
| git_filebuf file; |
| 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) { |
| return git__rethrow(error, "Failed to read existing config file %s", cfg->file_path); |
| } |
| |
| /* Initialise the reading position */ |
| cfg->reader.read_ptr = cfg->reader.buffer.data; |
| cfg->reader.eof = 0; |
| data_start = cfg->reader.read_ptr; |
| |
| /* 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"); |
| |
| skip_bom(cfg); |
| |
| while (error == GIT_SUCCESS && !cfg->reader.eof) { |
| c = cfg_peek(cfg, SKIP_WHITESPACE); |
| |
| switch (c) { |
| case '\0': /* We've arrived at the end of the file */ |
| break; |
| |
| case '[': /* 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; |
| free(current_section); |
| error = parse_section_header(cfg, ¤t_section); |
| if (error < GIT_SUCCESS) |
| break; |
| |
| /* Keep track of when it stops matching */ |
| last_section_matched = section_matches; |
| section_matches = !strcmp(current_section, var->section); |
| break; |
| |
| case ';': |
| case '#': |
| cfg_consume_line(cfg); |
| break; |
| |
| default: |
| /* |
| * 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); |
| break; |
| } |
| } else { |
| int cmp = -1; |
| |
| pre_end = cfg->reader.read_ptr; |
| if ((error = parse_variable(cfg, &var_name, &var_value)) == GIT_SUCCESS) |
| cmp = strcasecmp(var->name, var_name); |
| |
| free(var_name); |
| free(var_value); |
| |
| if (cmp != 0) |
| break; |
| |
| 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; |
| } |
| |
| /* Then replace the variable */ |
| 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; |
| } |
| |
| /* 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; |
| } |
| |
| goto cleanup; |
| } |
| } |
| |
| /* |
| * 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. |
| * |
| * Either way we need to write out the whole file. |
| */ |
| |
| 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; |
| } |
| |
| /* 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; |
| } |
| |
| /* 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"); |
| |
| cleanup: |
| free(current_section); |
| |
| if (error < GIT_SUCCESS) |
| git_filebuf_cleanup(&file); |
| else |
| error = git_filebuf_commit(&file); |
| |
| git_futils_freebuffer(&cfg->reader.buffer); |
| return error; |
| } |
| |
| static int is_multiline_var(const char *str) |
| { |
| char *end = strrchr(str, '\0') - 1; |
| |
| while (isspace(*end)) |
| --end; |
| |
| return *end == '\\'; |
| } |
| |
| static int parse_multiline_variable(diskfile_backend *cfg, const char *first, char **out) |
| { |
| char *line = NULL, *end; |
| int error = GIT_SUCCESS, len, ret; |
| char *buf; |
| |
| /* Check that the next line exists */ |
| line = cfg_readline(cfg); |
| if (line == NULL) |
| return GIT_ENOMEM; |
| |
| /* 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; |
| } |
| |
| strip_comments(line); |
| |
| /* If it was just a comment, pretend it didn't exist */ |
| if (line[0] == '\0') { |
| error = parse_multiline_variable(cfg, first, out); |
| goto out; |
| } |
| |
| /* 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; |
| } |
| |
| ret = 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)); |
| free(buf); |
| goto out; |
| } |
| |
| /* |
| * 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 (is_multiline_var(buf)) { |
| char *final_val; |
| error = parse_multiline_variable(cfg, buf, &final_val); |
| free(buf); |
| buf = final_val; |
| } |
| |
| *out = buf; |
| |
| out: |
| free(line); |
| return error; |
| } |
| |
| 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; |
| |
| line = cfg_readline(cfg); |
| if (line == NULL) |
| return GIT_ENOMEM; |
| |
| strip_comments(line); |
| |
| var_end = strchr(line, '='); |
| |
| if (var_end == NULL) |
| var_end = strchr(line, '\0'); |
| else |
| value_start = var_end + 1; |
| |
| if (isspace(var_end[-1])) { |
| do var_end--; |
| while (isspace(var_end[0])); |
| } |
| |
| tmp = git__strndup(line, var_end - line + 1); |
| if (tmp == NULL) { |
| error = GIT_ENOMEM; |
| goto out; |
| } |
| |
| *var_name = tmp; |
| |
| /* |
| * Now, let's try to parse the value |
| */ |
| if (value_start != NULL) { |
| |
| while (isspace(value_start[0])) |
| value_start++; |
| |
| if (value_start[0] == '\0') |
| goto out; |
| |
| if (is_multiline_var(value_start)) { |
| error = parse_multiline_variable(cfg, value_start, var_value); |
| if (error < GIT_SUCCESS) |
| free(*var_name); |
| goto out; |
| } |
| |
| tmp = strdup(value_start); |
| if (tmp == NULL) { |
| free(*var_name); |
| error = GIT_ENOMEM; |
| goto out; |
| } |
| |
| *var_value = tmp; |
| } else { |
| /* If there is no value, boolean true is assumed */ |
| *var_value = NULL; |
| } |
| |
| out: |
| free(line); |
| return error; |
| } |