diff --git a/include/git2.h b/include/git2.h index d44c3f8df..0d0e54898 100644 --- a/include/git2.h +++ b/include/git2.h @@ -52,5 +52,6 @@ #include "git2/tree.h" #include "git2/index.h" +#include "git2/config.h" #endif diff --git a/include/git2/config.h b/include/git2/config.h new file mode 100644 index 000000000..3ebbe64de --- /dev/null +++ b/include/git2/config.h @@ -0,0 +1,171 @@ +/* + * 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_h__ +#define INCLUDE_git_config_h__ + +#include "common.h" +#include "types.h" + +/** + * @file git2/config.h + * @brief Git config management routines + * @defgroup git_config Git config management routines + * @ingroup Git + * @{ + */ +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_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 + * + * @param cfg the configuration to free + */ +GIT_EXTERN(void) git_config_free(git_config *cfg); + +/** + * Get the value of an integer config variable. + * + * @param cfg where to look for the variable + * @param name the variable's name + * @param out pointer to the variable where the value should be stored + * @return GIT_SUCCESS on success; error code otherwise + */ +GIT_EXTERN(int) git_config_get_int(git_config *cfg, const char *name, int *out); + +/** + * Get the value of a long integer config variable. + * + * @param cfg where to look for the variable + * @param name the variable's name + * @param out pointer to the variable where the value should be stored + * @return GIT_SUCCESS on success; error code otherwise + */ +GIT_EXTERN(int) git_config_get_long(git_config *cfg, const char *name, long int *out); + +/** + * Get the value of a boolean config variable. + * + * This function uses the usual C convention of 0 being false and + * anything else true. + * + * @param cfg where to look for the variable + * @param name the variable's name + * @param out pointer to the variable where the value should be stored + * @return GIT_SUCCESS on success; error code otherwise + */ +GIT_EXTERN(int) git_config_get_bool(git_config *cfg, const char *name, int *out); + +/** + * Get the value of a string config variable. + * + * The string is owned by the variable and should not be freed by the + * user. + * + * @param cfg where to look for the variable + * @param name the variable's name + * @param out pointer to the variable's value + * @return GIT_SUCCESS on success; error code otherwise + */ +GIT_EXTERN(int) git_config_get_string(git_config *cfg, const char *name, const char **out); + +/** + * Set the value of an integer config variable. + * + * @param cfg where to look for the variable + * @param name the variable's name + * @param out pointer to the variable where the value should be stored + * @return GIT_SUCCESS on success; error code otherwise + */ +GIT_EXTERN(int) git_config_set_int(git_config *cfg, const char *name, int value); + +/** + * Set the value of a long integer config variable. + * + * @param cfg where to look for the variable + * @param name the variable's name + * @param out pointer to the variable where the value should be stored + * @return GIT_SUCCESS on success; error code otherwise + */ +GIT_EXTERN(int) git_config_set_long(git_config *cfg, const char *name, long int value); + +/** + * Set the value of a boolean config variable. + * + * @param cfg where to look for the variable + * @param name the variable's name + * @param value the value to store + * @return GIT_SUCCESS on success; error code otherwise + */ +GIT_EXTERN(int) git_config_set_bool(git_config *cfg, const char *name, int value); + +/** + * Set the value of a string config variable. + * + * A copy of the string is made and the user is free to use it + * afterwards. + * + * @param cfg where to look for the variable + * @param name the variable's name + * @param value the string to store. + * @return GIT_SUCCESS on success; error code otherwise + */ +GIT_EXTERN(int) git_config_set_string(git_config *cfg, const char *name, const char *value); + +/** + * Perform an operation on each config variable. + * + * The callback is passed a pointer to a config variable name and the + * data pointer passed to this function. As soon as one of the + * callback functions returns something other than 0, this function + * returns that value. + * + * @param cfg where to get the variables from + * @param callback the function to call on each variable + * @param data the data to pass to the callback + * @return GIT_SUCCESS or the return value of the callback which didn't return 0 + */ +GIT_EXTERN(int) git_config_foreach(git_config *cfg, int (*callback)(const char *, void *data), void *data); + +/** @} */ +GIT_END_DECL +#endif 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 6123abc82..49e80dd95 100644 --- a/include/git2/types.h +++ b/include/git2/types.h @@ -130,6 +130,15 @@ typedef struct git_treebuilder git_treebuilder; /** Memory representation of an index file. */ 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; + /** Time in a signature */ typedef struct git_time { git_time_t time; /** time in seconds from epoch */ diff --git a/src/config.c b/src/config.c new file mode 100644 index 000000000..234c5303f --- /dev/null +++ b/src/config.c @@ -0,0 +1,337 @@ +/* + * 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 "fileops.h" +#include "hashtable.h" +#include "config.h" +#include "git2/config_backend.h" +#include "vector.h" + +#include + +typedef struct { + git_config_backend *backend; + int priority; +} backend_internal; + +void git__strntolower(char *str, int len) +{ + int i; + + for (i = 0; i < len; ++i) { + str[i] = tolower(str[i]); + } +} + +void git__strtolower(char *str) +{ + git__strntolower(str, strlen(str)); +} + +int git_config_open_bare(git_config **out, const char *path) +{ + git_config_backend *backend = NULL; + git_config *cfg = NULL; + int error = GIT_SUCCESS; + + error = git_config_new(&cfg); + if (error < GIT_SUCCESS) + goto error; + + error = git_config_backend_file(&backend, path); + if (error < GIT_SUCCESS) + goto error; + + error = git_config_add_backend(cfg, backend, 1); + if (error < GIT_SUCCESS) + goto error; + + 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) + return GIT_ENOMEM; + + memset(cfg, 0x0, sizeof(git_config)); + + if (git_vector_init(&cfg->backends, 3, config_backend_cmp) < 0) { + free(cfg); + return GIT_ENOMEM; + } + + *out = cfg; + + return GIT_SUCCESS; +} + +int git_config_add_backend(git_config *cfg, git_config_backend *backend, int priority) +{ + backend_internal *internal; + + assert(cfg && backend); + + 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; +} + +/* + * Loop over all the variables + */ + +int git_config_foreach(git_config *cfg, int (*fn)(const char *, void *), void *data) +{ + int ret = GIT_SUCCESS; + unsigned int i; + backend_internal *internal; + git_config_backend *backend; + + 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 + **************/ + +/* + * Internal function to actually set the string value of a variable + */ + +int git_config_set_long(git_config *cfg, const char *name, long int value) +{ + char str_value[5]; /* Most numbers should fit in here */ + int buf_len = sizeof(str_value), ret; + char *help_buf = NULL; + + if ((ret = snprintf(str_value, buf_len, "%ld", value)) >= buf_len - 1){ + /* The number is too large, we need to allocate more memory */ + buf_len = ret + 1; + help_buf = git__malloc(buf_len); + snprintf(help_buf, buf_len, "%ld", value); + ret = git_config_set_string(cfg, name, help_buf); + free(help_buf); + } else { + ret = git_config_set_string(cfg, name, str_value); + } + + return ret; +} + +int git_config_set_int(git_config *cfg, const char *name, int value) +{ + return git_config_set_long(cfg, name, value); +} + +int git_config_set_bool(git_config *cfg, const char *name, int value) +{ + const char *str_value; + + if (value == 0) + str_value = "false"; + else + str_value = "true"; + + return git_config_set_string(cfg, name, str_value); +} + +int git_config_set_string(git_config *cfg, const char *name, const char *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 + ***********/ + +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 = git_config_get_string(cfg, name, &value); + if (ret < GIT_SUCCESS) + return ret; + + ret = git__strtol32(&num, value, &num_end, 0); + if (ret < GIT_SUCCESS) + return ret; + + switch (*num_end) { + case '\0': + break; + case 'k': + case 'K': + num *= 1024; + break; + case 'm': + case 'M': + num *= 1024 * 1024; + break; + case 'g': + case 'G': + num *= 1024 * 1024 * 1024; + break; + default: + return GIT_EINVALIDTYPE; + } + + *out = num; + + return GIT_SUCCESS; +} + +int git_config_get_int(git_config *cfg, const char *name, int *out) +{ + long int tmp; + int ret; + + ret = git_config_get_long(cfg, name, &tmp); + + *out = (int) tmp; + + return ret; +} + +int git_config_get_bool(git_config *cfg, const char *name, int *out) +{ + const char *value; + int error = GIT_SUCCESS; + + error = git_config_get_string(cfg, name, &value); + if (error < GIT_SUCCESS) + return error; + + /* A missing value means true */ + if (value == NULL) { + *out = 1; + return GIT_SUCCESS; + } + + if (!strcasecmp(value, "true") || + !strcasecmp(value, "yes") || + !strcasecmp(value, "on")) { + *out = 1; + return GIT_SUCCESS; + } + if (!strcasecmp(value, "false") || + !strcasecmp(value, "no") || + !strcasecmp(value, "off")) { + *out = 0; + return GIT_SUCCESS; + } + + /* Try to parse it as an integer */ + error = git_config_get_int(cfg, name, out); + if (error == GIT_SUCCESS) + *out = !!(*out); + + return error; +} + +int git_config_get_string(git_config *cfg, const char *name, const char **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); +} + diff --git a/src/config.h b/src/config.h new file mode 100644 index 000000000..e786e8a49 --- /dev/null +++ b/src/config.h @@ -0,0 +1,15 @@ +#ifndef INCLUDE_config_h__ +#define INCLUDE_config_h__ + +#include "git2.h" +#include "git2/config.h" +#include "vector.h" + +struct git_config { + git_vector backends; +}; + +void git__strtolower(char *str); +void git__strntolower(char *str, int len); + +#endif diff --git a/src/config_file.c b/src/config_file.c new file mode 100644 index 000000000..d66dd20e3 --- /dev/null +++ b/src/config_file.c @@ -0,0 +1,1011 @@ +/* + * 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. + */ + + last_dot = strrchr(name, '.'); + if (last_dot == NULL) { + return git__throw(GIT_EINVALIDTYPE, "Variables without section aren't allowed"); + } + + var = git__malloc(sizeof(git_cvar)); + if (var == NULL) + return GIT_ENOMEM; + + memset(var, 0x0, sizeof(git_cvar)); + + 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__throw(GIT_ENOTFOUND, "Variable '%s' not found", name); + + *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__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 { + switch (c) { + case '"': + if (quote_marks++ >= 2) + return git__throw(GIT_EOBJCORRUPTED, "Failed to parse ext header. Too many quotes"); + 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; + } + 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__thow(GIT_ERROR, "Failed to parse ext header. Wrong total lenght 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(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__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; + } + + if (!config_keychar(c) && c != '.') { + error = git__throw(GIT_EOBJCORRUPTED, "Failed to parse header. Wrong format on header"); + 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__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(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/src/errors.c b/src/errors.c index bf3810174..1fb68ee65 100644 --- a/src/errors.c +++ b/src/errors.c @@ -42,7 +42,7 @@ static struct { {GIT_EOBJTYPE, "The specified object is of invalid type"}, {GIT_EOBJCORRUPTED, "The specified object has its data corrupted"}, {GIT_ENOTAREPO, "The specified repository is invalid"}, - {GIT_EINVALIDTYPE, "The object type is invalid or doesn't match"}, + {GIT_EINVALIDTYPE, "The object or config variable type is invalid or doesn't match"}, {GIT_EMISSINGOBJDATA, "The object cannot be written that because it's missing internal data"}, {GIT_EPACKCORRUPTED, "The packfile for the ODB is corrupted"}, {GIT_EFLOCKFAIL, "Failed to adquire or release a file lock"}, diff --git a/tests/resources/config/config0 b/tests/resources/config/config0 new file mode 100644 index 000000000..85235c501 Binary files /dev/null and b/tests/resources/config/config0 differ diff --git a/tests/resources/config/config1 b/tests/resources/config/config1 new file mode 100644 index 000000000..211dc9e7d Binary files /dev/null and b/tests/resources/config/config1 differ diff --git a/tests/resources/config/config2 b/tests/resources/config/config2 new file mode 100644 index 000000000..60a389827 Binary files /dev/null and b/tests/resources/config/config2 differ diff --git a/tests/resources/config/config3 b/tests/resources/config/config3 new file mode 100644 index 000000000..44a5e50ea Binary files /dev/null and b/tests/resources/config/config3 differ diff --git a/tests/resources/config/config4 b/tests/resources/config/config4 new file mode 100644 index 000000000..741fa0ffd Binary files /dev/null and b/tests/resources/config/config4 differ diff --git a/tests/resources/config/config5 b/tests/resources/config/config5 new file mode 100644 index 000000000..8ab60ccec Binary files /dev/null and b/tests/resources/config/config5 differ diff --git a/tests/t15-config.c b/tests/t15-config.c new file mode 100644 index 000000000..1b971bd74 --- /dev/null +++ b/tests/t15-config.c @@ -0,0 +1,171 @@ +/* + * 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 "test_lib.h" +#include "test_helpers.h" + +#include + +#define CONFIG_BASE TEST_RESOURCES "/config" + +/* + * This one is so we know the code isn't completely broken + */ +BEGIN_TEST(config0, "read a simple configuration") + git_config *cfg; + int i; + + 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)); + must_be_true(i == 1); + must_pass(git_config_get_bool(cfg, "core.bare", &i)); + must_be_true(i == 0); + must_pass(git_config_get_bool(cfg, "core.logallrefupdates", &i)); + must_be_true(i == 1); + + git_config_free(cfg); +END_TEST + +/* + * [this "that"] and [this "That] are different namespaces. Make sure + * each returns the correct one. + */ +BEGIN_TEST(config1, "case sensitivity") + git_config *cfg; + int i; + const char *str; + + 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")); + must_pass(git_config_get_string(cfg, "this.That.other", &str)); + must_be_true(!strcmp(str, "yes")); + + must_pass(git_config_get_bool(cfg, "this.that.other", &i)); + must_be_true(i == 1); + must_pass(git_config_get_bool(cfg, "this.That.other", &i)); + must_be_true(i == 1); + + /* This one doesn't exist */ + must_fail(git_config_get_bool(cfg, "this.thaT.other", &i)); + + git_config_free(cfg); +END_TEST + +/* + * If \ is the last non-space character on the line, we read the next + * one, separating each line with SP. + */ +BEGIN_TEST(config2, "parse a multiline value") + git_config *cfg; + const char *str; + + 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")); + + git_config_free(cfg); +END_TEST + +/* + * This kind of subsection declaration is case-insensitive + */ +BEGIN_TEST(config3, "parse a [section.subsection] header") + git_config *cfg; + const char *str; + + 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")); + + /* Avoid a false positive */ + str = "nohello"; + must_pass(git_config_get_string(cfg, "section.subSectIon.var", &str)); + must_be_true(!strcmp(str, "hello")); + + git_config_free(cfg); +END_TEST + +BEGIN_TEST(config4, "a variable name on its own is valid") + git_config *cfg; +const char *str; +int i; + + 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); + + must_pass(git_config_get_bool(cfg, "some.section.variable", &i)); + must_be_true(i == 1); + + + git_config_free(cfg); +END_TEST + +BEGIN_TEST(config5, "test number suffixes") + git_config *cfg; + const char *str; + long int i; + + 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); + + must_pass(git_config_get_long(cfg, "number.k", &i)); + must_be_true(i == 1 * 1024); + + must_pass(git_config_get_long(cfg, "number.kk", &i)); + must_be_true(i == 1 * 1024); + + must_pass(git_config_get_long(cfg, "number.m", &i)); + must_be_true(i == 1 * 1024 * 1024); + + must_pass(git_config_get_long(cfg, "number.mm", &i)); + must_be_true(i == 1 * 1024 * 1024); + + must_pass(git_config_get_long(cfg, "number.g", &i)); + must_be_true(i == 1 * 1024 * 1024 * 1024); + + must_pass(git_config_get_long(cfg, "number.gg", &i)); + must_be_true(i == 1 * 1024 * 1024 * 1024); + + git_config_free(cfg); +END_TEST + + +BEGIN_SUITE(config) + ADD_TEST(config0); + ADD_TEST(config1); + ADD_TEST(config2); + ADD_TEST(config3); + ADD_TEST(config4); + ADD_TEST(config5); +END_SUITE diff --git a/tests/test_main.c b/tests/test_main.c index 102688ce1..0f5f16a26 100644 --- a/tests/test_main.c +++ b/tests/test_main.c @@ -44,6 +44,7 @@ DECLARE_SUITE(sqlite); DECLARE_SUITE(hiredis); DECLARE_SUITE(repository); DECLARE_SUITE(threads); +DECLARE_SUITE(config); static libgit2_suite suite_methods[]= { SUITE_NAME(core), @@ -60,7 +61,8 @@ static libgit2_suite suite_methods[]= { SUITE_NAME(sqlite), SUITE_NAME(repository), SUITE_NAME(threads), - SUITE_NAME(hiredis) + SUITE_NAME(hiredis), + SUITE_NAME(config), }; #define GIT_SUITE_COUNT (ARRAY_SIZE(suite_methods))