diff --git a/include/git2/config.h b/include/git2/config.h index abf59fa9e..3ebbe64de 100644 --- a/include/git2/config.h +++ b/include/git2/config.h @@ -37,13 +37,23 @@ */ GIT_BEGIN_DECL +/** + * Allocate a new configuration + */ +GIT_EXTERN(int) git_config_new(git_config **out); + /** * Open a configuration file * * @param cfg_out pointer to the configuration data * @param path where to load the confiration from */ -GIT_EXTERN(int) git_config_open(git_config **cfg_out, const char *path); +GIT_EXTERN(int) git_config_open_bare(git_config **cfg_out, const char *path); + +/** + * + */ +GIT_EXTERN(int) git_config_add_backend(git_config *cfg, git_config_backend *backend, int priority); /** * Free the configuration and its associated memory diff --git a/include/git2/config_backend.h b/include/git2/config_backend.h new file mode 100644 index 000000000..427cd95dd --- /dev/null +++ b/include/git2/config_backend.h @@ -0,0 +1,57 @@ +/* + * 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. + */ + +#ifndef INCLUDE_git_config_backend_h__ +#define INCLUDE_git_config_backend_h__ + +#include "common.h" +#include "types.h" +#include "config.h" + +GIT_BEGIN_DECL + +struct git_config; + +struct git_config_backend { + struct git_config *cfg; + /* Open means open the file/database and parse if necessary */ + int (*open)(struct git_config_backend *); + int (* get)(struct git_config_backend *, const char *key, const char **value); + int (* set)(struct git_config_backend *, const char *key, const char *value); + int (*foreach)(struct git_config_backend *, int (*fn)(const char *, void *), void *data); + void (*free)(struct git_config_backend *); +}; + +/** + * Create a file-backed configuration backend + * + * @param out the new backend + * @path where the config file is located + */ +GIT_EXTERN(int) git_config_backend_file(struct git_config_backend **out, const char *path); + +GIT_END_DECL + +#endif diff --git a/include/git2/types.h b/include/git2/types.h index 84e3d5b7f..49e80dd95 100644 --- a/include/git2/types.h +++ b/include/git2/types.h @@ -133,6 +133,9 @@ typedef struct git_index git_index; /** Memory representation of a config file */ typedef struct git_config git_config; +/** A specific implementation of a config backend */ +typedef struct git_config_backend git_config_backend; + /** Memory representation of a config variable */ typedef struct git_cvar git_cvar; diff --git a/src/config.c b/src/config.c index 654b70b3e..d839e3892 100644 --- a/src/config.c +++ b/src/config.c @@ -27,144 +27,15 @@ #include "fileops.h" #include "hashtable.h" #include "config.h" +#include "git2/config_backend.h" +#include "vector.h" #include -/********************** - * Forward declarations - ***********************/ -static int config_parse(git_config *cfg_file); -static int parse_variable(git_config *cfg, char **var_name, char **var_value); -void git_config_free(git_config *cfg); - -static void cvar_free(git_cvar *var) -{ - if (var == NULL) - return; - - free(var->section); - free(var->name); - free(var->value); - free(var); -} - -static void cvar_list_free(git_cvar_list *list) -{ - git_cvar *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, *last_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, '.'); - last_dot = strrchr(input, '.'); - comparison_len = strlen(local_sp + 2) - 1; - - if (last_dot == first_dot || last_dot - first_dot - 1 != comparison_len) - return 0; - - return !strncmp(local_sp + 2, first_dot + 1, comparison_len); -} - -static int cvar_match_name(const git_cvar *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 git_cvar *cvar_list_find(git_cvar_list *list, const char *name) -{ - git_cvar *iter; - - CVAR_LIST_FOREACH (list, iter) { - if (cvar_match_name(iter, name)) - return iter; - } - - return NULL; -} - -static int cvar_name_normalize(const char *input, char **output) -{ - char *input_sp = strchr(input, ' '); - char *quote, *str; - int i; - - /* We need to make a copy anyway */ - str = git__strdup(input); - if (str == NULL) - return GIT_ENOMEM; - - *output = str; - - /* If there aren't any spaces, we don't need to do anything */ - if (input_sp == NULL) - return GIT_SUCCESS; - - /* - * If there are spaces, we replace the space by a dot, move the - * variable name so that the dot before it replaces the last - * quotation mark and repeat so that the first quotation mark - * disappears. - */ - str[input_sp - input] = '.'; - - for (i = 0; i < 2; ++i) { - quote = strrchr(str, '"'); - memmove(quote, quote + 1, strlen(quote)); - } - - return GIT_SUCCESS; -} +typedef struct { + git_config_backend *backend; + int priority; +} backend_internal; void git__strntolower(char *str, int len) { @@ -180,12 +51,65 @@ void git__strtolower(char *str) git__strntolower(str, strlen(str)); } -int git_config_open(git_config **cfg_out, const char *path) +int git_config_open_bare(git_config **out, const char *path) { - git_config *cfg; + git_config_backend *backend = NULL; + git_config *cfg = NULL; int error = GIT_SUCCESS; - assert(cfg_out && path); + error = git_config_new(&cfg); + if (error < GIT_SUCCESS) + goto error; + + error = git_config_backend_file(&backend, path); + if (error < GIT_SUCCESS) + goto error; + + git_config_add_backend(cfg, backend, 1); + + error = backend->open(backend); + if (error < GIT_SUCCESS) + goto error; + + *out = cfg; + + return error; + + error: + if(backend) + backend->free(backend); + + return error; +} + +void git_config_free(git_config *cfg) +{ + unsigned int i; + git_config_backend *backend; + backend_internal *internal; + + for(i = 0; i < cfg->backends.length; ++i){ + internal = git_vector_get(&cfg->backends, i); + backend = internal->backend; + backend->free(backend); + free(internal); + } + + git_vector_free(&cfg->backends); + free(cfg); +} + +static int config_backend_cmp(const void *a, const void *b) +{ + const backend_internal *bk_a = *(const backend_internal **)(a); + const backend_internal *bk_b = *(const backend_internal **)(b); + + return bk_b->priority - bk_a->priority; +} + +int git_config_new(git_config **out) +{ + git_config *cfg; cfg = git__malloc(sizeof(git_config)); if (cfg == NULL) @@ -193,45 +117,38 @@ int git_config_open(git_config **cfg_out, const char *path) memset(cfg, 0x0, sizeof(git_config)); - cfg->file_path = git__strdup(path); - if (cfg->file_path == NULL) { - error = GIT_ENOMEM; - goto cleanup; + if (git_vector_init(&cfg->backends, 3, config_backend_cmp) < 0) { + free(cfg); + return GIT_ENOMEM; } - error = gitfo_read_file(&cfg->reader.buffer, cfg->file_path); - if(error < GIT_SUCCESS) - goto cleanup; + *out = cfg; - error = config_parse(cfg); - if (error < GIT_SUCCESS) - goto cleanup; - else - *cfg_out = cfg; - - gitfo_free_buf(&cfg->reader.buffer); - - return error; - - cleanup: - cvar_list_free(&cfg->var_list); - if (cfg->file_path) - free(cfg->file_path); - gitfo_free_buf(&cfg->reader.buffer); - free(cfg); - - return error; + return GIT_SUCCESS; } -void git_config_free(git_config *cfg) +int git_config_add_backend(git_config *cfg, git_config_backend *backend, int priority) { - if (cfg == NULL) - return; + backend_internal *internal; - free(cfg->file_path); - cvar_list_free(&cfg->var_list); + assert(cfg && backend); - free(cfg); + internal = git__malloc(sizeof(backend_internal)); + if (internal == NULL) + return GIT_ENOMEM; + + internal->backend = backend; + internal->priority = priority; + + if (git_vector_insert(&cfg->backends, internal) < 0) { + free(internal); + return GIT_ENOMEM; + } + + git_vector_sort(&cfg->backends); + internal->backend->cfg = cfg; + + return GIT_SUCCESS; } /* @@ -241,23 +158,20 @@ void git_config_free(git_config *cfg) int git_config_foreach(git_config *cfg, int (*fn)(const char *, void *), void *data) { int ret = GIT_SUCCESS; - git_cvar *var; - char *normalized; + unsigned int i; + backend_internal *internal; + git_config_backend *backend; - CVAR_LIST_FOREACH(&cfg->var_list, var) { - ret = cvar_name_normalize(var->name, &normalized); - if (ret < GIT_SUCCESS) - return ret; - - ret = fn(normalized, data); - free(normalized); - if (ret) - break; + for(i = 0; i < cfg->backends.length && ret == 0; ++i) { + internal = git_vector_get(&cfg->backends, i); + backend = internal->backend; + ret = backend->foreach(backend, fn, data); } return ret; } + /************** * Setters **************/ @@ -265,70 +179,6 @@ int git_config_foreach(git_config *cfg, int (*fn)(const char *, void *), void *d /* * Internal function to actually set the string value of a variable */ -static int config_set(git_config *cfg, const char *name, const char *value) -{ - git_cvar *var = NULL; - git_cvar *existing = NULL; - int error = GIT_SUCCESS; - const char *last_dot; - - /* - * If it already exists, we just need to update its value. - */ - existing = cvar_list_find(&cfg->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 GIT_SUCCESS; - } - - /* - * Otherwise, create it and stick it at the end of the queue. - */ - - var = git__malloc(sizeof(git_cvar)); - if (var == NULL) - return GIT_ENOMEM; - - memset(var, 0x0, sizeof(git_cvar)); - - last_dot = strrchr(name, '.'); - if (last_dot == NULL) { - error = GIT_ERROR; - goto out; - } - - var->section = git__strndup(name, last_dot - 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(&cfg->var_list, var); - - out: - if (error < GIT_SUCCESS) - cvar_free(var); - - return error; -} int git_config_set_long(git_config *cfg, const char *name, long int value) { @@ -341,10 +191,10 @@ int git_config_set_long(git_config *cfg, const char *name, long int value) buf_len = ret + 1; help_buf = git__malloc(buf_len); snprintf(help_buf, buf_len, "%ld", value); - ret = config_set(cfg, name, help_buf); + ret = git_config_set_string(cfg, name, help_buf); free(help_buf); } else { - ret = config_set(cfg, name, str_value); + ret = git_config_set_string(cfg, name, str_value); } return ret; @@ -364,43 +214,33 @@ int git_config_set_bool(git_config *cfg, const char *name, int value) else str_value = "true"; - return config_set(cfg, name, str_value); + return git_config_set_string(cfg, name, str_value); } int git_config_set_string(git_config *cfg, const char *name, const char *value) { - return config_set(cfg, name, value); + backend_internal *internal; + git_config_backend *backend; + + assert(cfg->backends.length > 0); + + internal = git_vector_get(&cfg->backends, 0); + backend = internal->backend; + + return backend->set(backend, name, value); } /*********** * Getters ***********/ -/* - * Internal function that actually gets the value in string form - */ -static int config_get(git_config *cfg, const char *name, const char **out) -{ - git_cvar *var; - int error = GIT_SUCCESS; - - var = cvar_list_find(&cfg->var_list, name); - - if (var == NULL) - return GIT_ENOTFOUND; - - *out = var->value; - - return error; -} - int git_config_get_long(git_config *cfg, const char *name, long int *out) { const char *value, *num_end; int ret; long int num; - ret = config_get(cfg, name, &value); + ret = git_config_get_string(cfg, name, &value); if (ret < GIT_SUCCESS) return ret; @@ -449,7 +289,7 @@ int git_config_get_bool(git_config *cfg, const char *name, int *out) const char *value; int error = GIT_SUCCESS; - error = config_get(cfg, name, &value); + error = git_config_get_string(cfg, name, &value); if (error < GIT_SUCCESS) return error; @@ -482,618 +322,14 @@ int git_config_get_bool(git_config *cfg, const char *name, int *out) int git_config_get_string(git_config *cfg, const char *name, const char **out) { - return config_get(cfg, name, out); + backend_internal *internal; + git_config_backend *backend; + + assert(cfg->backends.length > 0); + + internal = git_vector_get(&cfg->backends, 0); + backend = internal->backend; + + return backend->get(backend, name, out); } -static int cfg_getchar_raw(git_config *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(git_config *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(git_config *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; -} - -static const char *LINEBREAK_UNIX = "\\\n"; -static const char *LINEBREAK_WIN32 = "\\\r\n"; - -static int is_linebreak(const char *pos) -{ - return memcmp(pos - 1, LINEBREAK_UNIX, sizeof(LINEBREAK_UNIX)) == 0 || - memcmp(pos - 2, LINEBREAK_WIN32, sizeof(LINEBREAK_WIN32)) == 0; -} - -/* - * Read and consume a line, returning it in newly-allocated memory. - */ -static char *cfg_readline(git_config *cfg) -{ - char *line = NULL; - char *line_src, *line_end; - int line_len; - - line_src = cfg->reader.read_ptr; - line_end = strchr(line_src, '\n'); - - /* no newline at EOF */ - if (line_end == NULL) - line_end = strchr(line_src, 0); - else - while (is_linebreak(line_end)) - line_end = strchr(line_end + 1, '\n'); - - - while (line_src < line_end && isspace(*line_src)) - line_src++; - - line = (char *)git__malloc((size_t)(line_end - line_src) + 1); - if (line == NULL) - return NULL; - - line_len = 0; - while (line_src < line_end) { - - if (memcmp(line_src, LINEBREAK_UNIX, sizeof(LINEBREAK_UNIX)) == 0) { - line_src += sizeof(LINEBREAK_UNIX); - continue; - } - - if (memcmp(line_src, LINEBREAK_WIN32, sizeof(LINEBREAK_WIN32)) == 0) { - line_src += sizeof(LINEBREAK_WIN32); - continue; - } - - line[line_len++] = *line_src++; - } - - 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(git_config *cfg) -{ - char *line_start, *line_end; - int len; - - 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'); - } - - len = line_end - line_start; - - if (*line_end == '\n') - line_end++; - - if (*line_end == '\0') - cfg->reader.eof = 1; - - cfg->reader.line_number++; - cfg->reader.read_ptr = line_end; -} - -static 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_EOBJCORRUPTED; - - 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 { - switch (c) { - case '"': - if (quote_marks++ >= 2) - return GIT_EOBJCORRUPTED; - break; - case '\\': - c = line[rpos++]; - switch (c) { - case '"': - case '\\': - break; - default: - error = GIT_EOBJCORRUPTED; - goto out; - } - default: - break; - } - - subsection[pos++] = 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_ERROR; - goto out; - } else if (ret < 0) { - error = GIT_EOSERR; - goto out; - } - - git__strntolower(*section_name, strchr(*section_name, ' ') - *section_name); - - out: - free(subsection); - - return error; -} - -static int parse_section_header(git_config *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_EOBJCORRUPTED; - - name = (char *)git__malloc((size_t)(name_end - line) + 1); - if (name == NULL) - return GIT_EOBJCORRUPTED; - - name_length = 0; - pos = 0; - - /* Make sure we were given a section header */ - c = line[pos++]; - if (c != '[') { - error = GIT_EOBJCORRUPTED; - goto error; - } - - c = line[pos++]; - - do { - if (cfg->reader.eof){ - error = GIT_EOBJCORRUPTED; - goto error; - } - - if (isspace(c)){ - name[name_length] = '\0'; - error = parse_section_header_ext(line, name, section_out); - free(line); - free(name); - return error; - } - - if (!config_keychar(c) && c != '.') { - error = GIT_EOBJCORRUPTED; - goto error; - } - - name[name_length++] = 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(git_config *cfg) -{ - static const unsigned 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(git_config *cfg_file) -{ - int error = GIT_SUCCESS, c; - char *current_section = NULL; - char *var_name; - char *var_value; - git_cvar *var; - - /* Initialise the reading position */ - cfg_file->reader.read_ptr = cfg_file->reader.buffer.data; - cfg_file->reader.eof = 0; - - 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); - 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(git_cvar)); - if (var == NULL) { - error = GIT_ENOMEM; - break; - } - - memset(var, 0x0, sizeof(git_cvar)); - - 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; - } - } - - if (current_section) - free(current_section); - - 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(git_config *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_EOBJCORRUPTED; - 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_EOSERR; - 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(git_config *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 = 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 thre is no value, boolean true is assumed */ - *var_value = NULL; - } - - out: - free(line); - return error; -} diff --git a/src/config.h b/src/config.h index 82a66bfbf..e786e8a49 100644 --- a/src/config.h +++ b/src/config.h @@ -1,72 +1,14 @@ #ifndef INCLUDE_config_h__ #define INCLUDE_config_h__ +#include "git2.h" #include "git2/config.h" - -typedef struct { - git_cvar *head; - git_cvar *tail; -} git_cvar_list; +#include "vector.h" struct git_config { - git_cvar_list var_list; - - struct { - gitfo_buf buffer; - char *read_ptr; - int line_number; - int eof; - } reader; - - char *file_path; + git_vector backends; }; -struct git_cvar { - git_cvar *next; - char *section; - char *name; - char *value; -}; - -#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)) - - void git__strtolower(char *str); void git__strntolower(char *str, int len); diff --git a/src/config_file.c b/src/config_file.c new file mode 100644 index 000000000..05a57cd2b --- /dev/null +++ b/src/config_file.c @@ -0,0 +1,1012 @@ +/* + * 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 "git2/config_backend.h" +#include "git2/types.h" + +#include + +struct git_cvar { + git_cvar *next; + char *section; + char *name; + char *value; +}; + +typedef struct { + git_cvar *head; + git_cvar *tail; +} git_cvar_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_backend parent; + + git_cvar_list var_list; + + struct { + gitfo_buf buffer; + char *read_ptr; + int line_number; + int eof; + } reader; + + char *file_path; +} file_backend; + +static int config_parse(file_backend *cfg_file); +static int parse_variable(file_backend *cfg, char **var_name, char **var_value); + +static void cvar_free(git_cvar *var) +{ + if (var == NULL) + return; + + free(var->section); + free(var->name); + free(var->value); + free(var); +} + +static void cvar_list_free(git_cvar_list *list) +{ + git_cvar *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, *last_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, '.'); + last_dot = strrchr(input, '.'); + comparison_len = strlen(local_sp + 2) - 1; + + if (last_dot == first_dot || last_dot - first_dot - 1 != comparison_len) + return 0; + + return !strncmp(local_sp + 2, first_dot + 1, comparison_len); +} + +static int cvar_match_name(const git_cvar *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 git_cvar *cvar_list_find(git_cvar_list *list, const char *name) +{ + git_cvar *iter; + + CVAR_LIST_FOREACH (list, iter) { + if (cvar_match_name(iter, name)) + return iter; + } + + return NULL; +} + +static int cvar_name_normalize(const char *input, char **output) +{ + char *input_sp = strchr(input, ' '); + char *quote, *str; + int i; + + /* We need to make a copy anyway */ + str = git__strdup(input); + if (str == NULL) + return GIT_ENOMEM; + + *output = str; + + /* If there aren't any spaces, we don't need to do anything */ + if (input_sp == NULL) + return GIT_SUCCESS; + + /* + * If there are spaces, we replace the space by a dot, move the + * variable name so that the dot before it replaces the last + * quotation mark and repeat so that the first quotation mark + * disappears. + */ + str[input_sp - input] = '.'; + + for (i = 0; i < 2; ++i) { + quote = strrchr(str, '"'); + memmove(quote, quote + 1, strlen(quote)); + } + + return GIT_SUCCESS; +} + +static int config_open(git_config_backend *cfg) +{ + int error; + file_backend *b = (file_backend *)cfg; + + error = gitfo_read_file(&b->reader.buffer, b->file_path); + if(error < GIT_SUCCESS) + goto cleanup; + + error = config_parse(b); + if (error < GIT_SUCCESS) + goto cleanup; + + gitfo_free_buf(&b->reader.buffer); + + return error; + + cleanup: + cvar_list_free(&b->var_list); + gitfo_free_buf(&b->reader.buffer); + free(cfg); + + return error; +} + +static void backend_free(git_config_backend *_backend) +{ + file_backend *backend = (file_backend *)_backend; + + if (backend == NULL) + return; + + free(backend->file_path); + cvar_list_free(&backend->var_list); + + free(backend); +} + +static int file_foreach(git_config_backend *backend, int (*fn)(const char *, void *), void *data) +{ + int ret; + git_cvar *var; + char *normalized; + file_backend *b = (file_backend *)backend; + + CVAR_LIST_FOREACH(&b->var_list, var) { + ret = cvar_name_normalize(var->name, &normalized); + if (ret < GIT_SUCCESS) + return ret; + + ret = fn(normalized, data); + free(normalized); + if (ret) + break; + } + + return ret; +} + +static int config_set(git_config_backend *cfg, const char *name, const char *value) +{ + git_cvar *var = NULL; + git_cvar *existing = NULL; + int error = GIT_SUCCESS; + const char *last_dot; + file_backend *b = (file_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 GIT_SUCCESS; + } + + /* + * Otherwise, create it and stick it at the end of the queue. + */ + + var = git__malloc(sizeof(git_cvar)); + if (var == NULL) + return GIT_ENOMEM; + + memset(var, 0x0, sizeof(git_cvar)); + + last_dot = strrchr(name, '.'); + if (last_dot == NULL) { + error = GIT_ERROR; + goto out; + } + + var->section = git__strndup(name, last_dot - 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); + + out: + if (error < GIT_SUCCESS) + cvar_free(var); + + return error; +} + +/* + * Internal function that actually gets the value in string form + */ +static int config_get(git_config_backend *cfg, const char *name, const char **out) +{ + git_cvar *var; + int error = GIT_SUCCESS; + file_backend *b = (file_backend *)cfg; + + var = cvar_list_find(&b->var_list, name); + + if (var == NULL) + return GIT_ENOTFOUND; + + *out = var->value; + + return error; +} + +int git_config_backend_file(git_config_backend **out, const char *path) +{ + file_backend *backend; + + backend = git__malloc(sizeof(file_backend)); + if (backend == NULL) + return GIT_ENOMEM; + + memset(backend, 0x0, sizeof(file_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_backend *)backend; + + return GIT_SUCCESS; +} + +static int cfg_getchar_raw(file_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(file_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(file_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; +} + +static const char *LINEBREAK_UNIX = "\\\n"; +static const char *LINEBREAK_WIN32 = "\\\r\n"; + +static int is_linebreak(const char *pos) +{ + return memcmp(pos - 1, LINEBREAK_UNIX, sizeof(LINEBREAK_UNIX)) == 0 || + memcmp(pos - 2, LINEBREAK_WIN32, sizeof(LINEBREAK_WIN32)) == 0; +} + +/* + * Read and consume a line, returning it in newly-allocated memory. + */ +static char *cfg_readline(file_backend *cfg) +{ + char *line = NULL; + char *line_src, *line_end; + int line_len; + + line_src = cfg->reader.read_ptr; + line_end = strchr(line_src, '\n'); + + /* no newline at EOF */ + if (line_end == NULL) + line_end = strchr(line_src, 0); + else + while (is_linebreak(line_end)) + line_end = strchr(line_end + 1, '\n'); + + + while (line_src < line_end && isspace(*line_src)) + line_src++; + + line = (char *)git__malloc((size_t)(line_end - line_src) + 1); + if (line == NULL) + return NULL; + + line_len = 0; + while (line_src < line_end) { + + if (memcmp(line_src, LINEBREAK_UNIX, sizeof(LINEBREAK_UNIX)) == 0) { + line_src += sizeof(LINEBREAK_UNIX); + continue; + } + + if (memcmp(line_src, LINEBREAK_WIN32, sizeof(LINEBREAK_WIN32)) == 0) { + line_src += sizeof(LINEBREAK_WIN32); + continue; + } + + line[line_len++] = *line_src++; + } + + 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(file_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; +} + +static 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_EOBJCORRUPTED; + + 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 { + switch (c) { + case '"': + if (quote_marks++ >= 2) + return GIT_EOBJCORRUPTED; + break; + case '\\': + c = line[rpos++]; + switch (c) { + case '"': + case '\\': + break; + default: + error = GIT_EOBJCORRUPTED; + goto out; + } + default: + break; + } + + subsection[pos++] = 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_ERROR; + goto out; + } else if (ret < 0) { + error = GIT_EOSERR; + goto out; + } + + git__strntolower(*section_name, strchr(*section_name, ' ') - *section_name); + + out: + free(subsection); + + return error; +} + +static int parse_section_header(file_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_EOBJCORRUPTED; + + name = (char *)git__malloc((size_t)(name_end - line) + 1); + if (name == NULL) + return GIT_EOBJCORRUPTED; + + name_length = 0; + pos = 0; + + /* Make sure we were given a section header */ + c = line[pos++]; + if (c != '[') { + error = GIT_EOBJCORRUPTED; + goto error; + } + + c = line[pos++]; + + do { + if (cfg->reader.eof){ + error = GIT_EOBJCORRUPTED; + goto error; + } + + if (isspace(c)){ + name[name_length] = '\0'; + error = parse_section_header_ext(line, name, section_out); + free(line); + free(name); + return error; + } + + if (!config_keychar(c) && c != '.') { + error = GIT_EOBJCORRUPTED; + goto error; + } + + name[name_length++] = 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(file_backend *cfg) +{ + static const unsigned 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(file_backend *cfg_file) +{ + int error = GIT_SUCCESS, c; + char *current_section = NULL; + char *var_name; + char *var_value; + git_cvar *var; + + /* Initialise the reading position */ + cfg_file->reader.read_ptr = cfg_file->reader.buffer.data; + cfg_file->reader.eof = 0; + + 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); + 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(git_cvar)); + if (var == NULL) { + error = GIT_ENOMEM; + break; + } + + memset(var, 0x0, sizeof(git_cvar)); + + 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; + } + } + + if (current_section) + free(current_section); + + 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(file_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_EOBJCORRUPTED; + 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_EOSERR; + 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(file_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 = 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 thre is no value, boolean true is assumed */ + *var_value = NULL; + } + + out: + free(line); + return error; +} diff --git a/tests/t15-config.c b/tests/t15-config.c index 8e0d3215c..1b971bd74 100644 --- a/tests/t15-config.c +++ b/tests/t15-config.c @@ -36,7 +36,7 @@ BEGIN_TEST(config0, "read a simple configuration") git_config *cfg; int i; - must_pass(git_config_open(&cfg, CONFIG_BASE "/config0")); + must_pass(git_config_open_bare(&cfg, CONFIG_BASE "/config0")); must_pass(git_config_get_int(cfg, "core.repositoryformatversion", &i)); must_be_true(i == 0); must_pass(git_config_get_bool(cfg, "core.filemode", &i)); @@ -58,7 +58,7 @@ BEGIN_TEST(config1, "case sensitivity") int i; const char *str; - must_pass(git_config_open(&cfg, CONFIG_BASE "/config1")); + must_pass(git_config_open_bare(&cfg, CONFIG_BASE "/config1")); must_pass(git_config_get_string(cfg, "this.that.other", &str)); must_be_true(!strcmp(str, "true")); @@ -84,7 +84,7 @@ BEGIN_TEST(config2, "parse a multiline value") git_config *cfg; const char *str; - must_pass(git_config_open(&cfg, CONFIG_BASE "/config2")); + must_pass(git_config_open_bare(&cfg, CONFIG_BASE "/config2")); must_pass(git_config_get_string(cfg, "this.That.and", &str)); must_be_true(!strcmp(str, "one one one two two three three")); @@ -99,7 +99,7 @@ BEGIN_TEST(config3, "parse a [section.subsection] header") git_config *cfg; const char *str; - must_pass(git_config_open(&cfg, CONFIG_BASE "/config3")); + must_pass(git_config_open_bare(&cfg, CONFIG_BASE "/config3")); must_pass(git_config_get_string(cfg, "section.subsection.var", &str)); must_be_true(!strcmp(str, "hello")); @@ -117,7 +117,7 @@ BEGIN_TEST(config4, "a variable name on its own is valid") const char *str; int i; - must_pass(git_config_open(&cfg, CONFIG_BASE "/config4")); + must_pass(git_config_open_bare(&cfg, CONFIG_BASE "/config4")); must_pass(git_config_get_string(cfg, "some.section.variable", &str)); must_be_true(str == NULL); @@ -134,7 +134,7 @@ BEGIN_TEST(config5, "test number suffixes") const char *str; long int i; - must_pass(git_config_open(&cfg, CONFIG_BASE "/config5")); + must_pass(git_config_open_bare(&cfg, CONFIG_BASE "/config5")); must_pass(git_config_get_long(cfg, "number.simple", &i)); must_be_true(i == 1);