| /* |
| * Copyright (C) the libgit2 contributors. All rights reserved. |
| * |
| * This file is part of libgit2, distributed under the GNU GPL v2 with |
| * a Linking Exception. For full terms see the included COPYING file. |
| */ |
| |
| #include "common.h" |
| #include "fileops.h" |
| #include "config.h" |
| #include "git2/config.h" |
| #include "git2/sys/config.h" |
| #include "vector.h" |
| #include "buf_text.h" |
| #include "config_file.h" |
| #if GIT_WIN32 |
| # include <windows.h> |
| #endif |
| |
| #include <ctype.h> |
| |
| typedef struct { |
| git_refcount rc; |
| |
| git_config_backend *file; |
| git_config_level_t level; |
| } file_internal; |
| |
| static void file_internal_free(file_internal *internal) |
| { |
| git_config_backend *file; |
| |
| file = internal->file; |
| file->free(file); |
| git__free(internal); |
| } |
| |
| static void config_free(git_config *cfg) |
| { |
| size_t i; |
| file_internal *internal; |
| |
| for (i = 0; i < cfg->files.length; ++i) { |
| internal = git_vector_get(&cfg->files, i); |
| GIT_REFCOUNT_DEC(internal, file_internal_free); |
| } |
| |
| git_vector_free(&cfg->files); |
| |
| git__memzero(cfg, sizeof(*cfg)); |
| git__free(cfg); |
| } |
| |
| void git_config_free(git_config *cfg) |
| { |
| if (cfg == NULL) |
| return; |
| |
| GIT_REFCOUNT_DEC(cfg, config_free); |
| } |
| |
| static int config_backend_cmp(const void *a, const void *b) |
| { |
| const file_internal *bk_a = (const file_internal *)(a); |
| const file_internal *bk_b = (const file_internal *)(b); |
| |
| return bk_b->level - bk_a->level; |
| } |
| |
| int git_config_new(git_config **out) |
| { |
| git_config *cfg; |
| |
| cfg = git__malloc(sizeof(git_config)); |
| GITERR_CHECK_ALLOC(cfg); |
| |
| memset(cfg, 0x0, sizeof(git_config)); |
| |
| if (git_vector_init(&cfg->files, 3, config_backend_cmp) < 0) { |
| git__free(cfg); |
| return -1; |
| } |
| |
| *out = cfg; |
| GIT_REFCOUNT_INC(cfg); |
| return 0; |
| } |
| |
| int git_config_add_file_ondisk( |
| git_config *cfg, |
| const char *path, |
| git_config_level_t level, |
| int force) |
| { |
| git_config_backend *file = NULL; |
| struct stat st; |
| int res; |
| |
| assert(cfg && path); |
| |
| res = p_stat(path, &st); |
| if (res < 0 && errno != ENOENT) { |
| giterr_set(GITERR_CONFIG, "Error stat'ing config file '%s'", path); |
| return -1; |
| } |
| |
| if (git_config_file__ondisk(&file, path) < 0) |
| return -1; |
| |
| if ((res = git_config_add_backend(cfg, file, level, force)) < 0) { |
| /* |
| * free manually; the file is not owned by the config |
| * instance yet and will not be freed on cleanup |
| */ |
| file->free(file); |
| return res; |
| } |
| |
| return 0; |
| } |
| |
| int git_config_open_ondisk(git_config **out, const char *path) |
| { |
| int error; |
| git_config *config; |
| |
| *out = NULL; |
| |
| if (git_config_new(&config) < 0) |
| return -1; |
| |
| if ((error = git_config_add_file_ondisk(config, path, GIT_CONFIG_LEVEL_LOCAL, 0)) < 0) |
| git_config_free(config); |
| else |
| *out = config; |
| |
| return error; |
| } |
| |
| static int find_internal_file_by_level( |
| file_internal **internal_out, |
| const git_config *cfg, |
| git_config_level_t level) |
| { |
| int pos = -1; |
| file_internal *internal; |
| size_t i; |
| |
| /* when passing GIT_CONFIG_HIGHEST_LEVEL, the idea is to get the config file |
| * which has the highest level. As config files are stored in a vector |
| * sorted by decreasing order of level, getting the file at position 0 |
| * will do the job. |
| */ |
| if (level == GIT_CONFIG_HIGHEST_LEVEL) { |
| pos = 0; |
| } else { |
| git_vector_foreach(&cfg->files, i, internal) { |
| if (internal->level == level) |
| pos = (int)i; |
| } |
| } |
| |
| if (pos == -1) { |
| giterr_set(GITERR_CONFIG, |
| "No config file exists for the given level '%i'", (int)level); |
| return GIT_ENOTFOUND; |
| } |
| |
| *internal_out = git_vector_get(&cfg->files, pos); |
| |
| return 0; |
| } |
| |
| static int duplicate_level(void **old_raw, void *new_raw) |
| { |
| file_internal **old = (file_internal **)old_raw; |
| |
| GIT_UNUSED(new_raw); |
| |
| giterr_set(GITERR_CONFIG, "A file with the same level (%i) has already been added to the config", (int)(*old)->level); |
| return GIT_EEXISTS; |
| } |
| |
| static void try_remove_existing_file_internal( |
| git_config *cfg, |
| git_config_level_t level) |
| { |
| int pos = -1; |
| file_internal *internal; |
| size_t i; |
| |
| git_vector_foreach(&cfg->files, i, internal) { |
| if (internal->level == level) |
| pos = (int)i; |
| } |
| |
| if (pos == -1) |
| return; |
| |
| internal = git_vector_get(&cfg->files, pos); |
| |
| if (git_vector_remove(&cfg->files, pos) < 0) |
| return; |
| |
| GIT_REFCOUNT_DEC(internal, file_internal_free); |
| } |
| |
| static int git_config__add_internal( |
| git_config *cfg, |
| file_internal *internal, |
| git_config_level_t level, |
| int force) |
| { |
| int result; |
| |
| /* delete existing config file for level if it exists */ |
| if (force) |
| try_remove_existing_file_internal(cfg, level); |
| |
| if ((result = git_vector_insert_sorted(&cfg->files, |
| internal, &duplicate_level)) < 0) |
| return result; |
| |
| git_vector_sort(&cfg->files); |
| internal->file->cfg = cfg; |
| |
| GIT_REFCOUNT_INC(internal); |
| |
| return 0; |
| } |
| |
| int git_config_open_global(git_config **cfg_out, git_config *cfg) |
| { |
| if (!git_config_open_level(cfg_out, cfg, GIT_CONFIG_LEVEL_XDG)) |
| return 0; |
| |
| return git_config_open_level(cfg_out, cfg, GIT_CONFIG_LEVEL_GLOBAL); |
| } |
| |
| int git_config_open_level( |
| git_config **cfg_out, |
| const git_config *cfg_parent, |
| git_config_level_t level) |
| { |
| git_config *cfg; |
| file_internal *internal; |
| int res; |
| |
| if ((res = find_internal_file_by_level(&internal, cfg_parent, level)) < 0) |
| return res; |
| |
| if ((res = git_config_new(&cfg)) < 0) |
| return res; |
| |
| if ((res = git_config__add_internal(cfg, internal, level, true)) < 0) { |
| git_config_free(cfg); |
| return res; |
| } |
| |
| *cfg_out = cfg; |
| |
| return 0; |
| } |
| |
| int git_config_add_backend( |
| git_config *cfg, |
| git_config_backend *file, |
| git_config_level_t level, |
| int force) |
| { |
| file_internal *internal; |
| int result; |
| |
| assert(cfg && file); |
| |
| GITERR_CHECK_VERSION(file, GIT_CONFIG_BACKEND_VERSION, "git_config_backend"); |
| |
| if ((result = file->open(file, level)) < 0) |
| return result; |
| |
| internal = git__malloc(sizeof(file_internal)); |
| GITERR_CHECK_ALLOC(internal); |
| |
| memset(internal, 0x0, sizeof(file_internal)); |
| |
| internal->file = file; |
| internal->level = level; |
| |
| if ((result = git_config__add_internal(cfg, internal, level, force)) < 0) { |
| git__free(internal); |
| return result; |
| } |
| |
| return 0; |
| } |
| |
| int git_config_refresh(git_config *cfg) |
| { |
| int error = 0; |
| size_t i; |
| |
| for (i = 0; i < cfg->files.length && !error; ++i) { |
| file_internal *internal = git_vector_get(&cfg->files, i); |
| git_config_backend *file = internal->file; |
| error = file->refresh(file); |
| } |
| |
| if (!error && GIT_REFCOUNT_OWNER(cfg) != NULL) |
| git_repository__cvar_cache_clear(GIT_REFCOUNT_OWNER(cfg)); |
| |
| return error; |
| } |
| |
| /* |
| * Loop over all the variables |
| */ |
| |
| int git_config_foreach( |
| const git_config *cfg, git_config_foreach_cb cb, void *payload) |
| { |
| return git_config_foreach_match(cfg, NULL, cb, payload); |
| } |
| |
| int git_config_foreach_match( |
| const git_config *cfg, |
| const char *regexp, |
| git_config_foreach_cb cb, |
| void *payload) |
| { |
| int ret = 0; |
| size_t i; |
| file_internal *internal; |
| git_config_backend *file; |
| |
| for (i = 0; i < cfg->files.length && ret == 0; ++i) { |
| internal = git_vector_get(&cfg->files, i); |
| file = internal->file; |
| ret = file->foreach(file, regexp, cb, payload); |
| } |
| |
| return ret; |
| } |
| |
| /************** |
| * Setters |
| **************/ |
| |
| static int config_error_nofiles(const char *name) |
| { |
| giterr_set(GITERR_CONFIG, |
| "Cannot set value for '%s' when no config files exist", name); |
| return GIT_ENOTFOUND; |
| } |
| |
| int git_config_delete_entry(git_config *cfg, const char *name) |
| { |
| git_config_backend *file; |
| file_internal *internal; |
| |
| internal = git_vector_get(&cfg->files, 0); |
| if (!internal || !internal->file) |
| return config_error_nofiles(name); |
| file = internal->file; |
| |
| return file->del(file, name); |
| } |
| |
| int git_config_set_int64(git_config *cfg, const char *name, int64_t value) |
| { |
| char str_value[32]; /* All numbers should fit in here */ |
| p_snprintf(str_value, sizeof(str_value), "%" PRId64, value); |
| return git_config_set_string(cfg, name, str_value); |
| } |
| |
| int git_config_set_int32(git_config *cfg, const char *name, int32_t value) |
| { |
| return git_config_set_int64(cfg, name, (int64_t)value); |
| } |
| |
| int git_config_set_bool(git_config *cfg, const char *name, int value) |
| { |
| return git_config_set_string(cfg, name, value ? "true" : "false"); |
| } |
| |
| int git_config_set_string(git_config *cfg, const char *name, const char *value) |
| { |
| int error; |
| git_config_backend *file; |
| file_internal *internal; |
| |
| if (!value) { |
| giterr_set(GITERR_CONFIG, "The value to set cannot be NULL"); |
| return -1; |
| } |
| |
| internal = git_vector_get(&cfg->files, 0); |
| if (!internal || !internal->file) |
| return config_error_nofiles(name); |
| file = internal->file; |
| |
| error = file->set(file, name, value); |
| |
| if (!error && GIT_REFCOUNT_OWNER(cfg) != NULL) |
| git_repository__cvar_cache_clear(GIT_REFCOUNT_OWNER(cfg)); |
| |
| return error; |
| } |
| |
| /*********** |
| * Getters |
| ***********/ |
| int git_config_get_mapped( |
| int *out, |
| const git_config *cfg, |
| const char *name, |
| const git_cvar_map *maps, |
| size_t map_n) |
| { |
| const char *value; |
| int ret; |
| |
| if ((ret = git_config_get_string(&value, cfg, name)) < 0) |
| return ret; |
| |
| return git_config_lookup_map_value(out, maps, map_n, value); |
| } |
| |
| int git_config_get_int64(int64_t *out, const git_config *cfg, const char *name) |
| { |
| const char *value; |
| int ret; |
| |
| if ((ret = git_config_get_string(&value, cfg, name)) < 0) |
| return ret; |
| |
| return git_config_parse_int64(out, value); |
| } |
| |
| int git_config_get_int32(int32_t *out, const git_config *cfg, const char *name) |
| { |
| const char *value; |
| int ret; |
| |
| if ((ret = git_config_get_string(&value, cfg, name)) < 0) |
| return ret; |
| |
| return git_config_parse_int32(out, value); |
| } |
| |
| static int get_string_at_file(const char **out, const git_config_backend *file, const char *name) |
| { |
| const git_config_entry *entry; |
| int res; |
| |
| res = file->get(file, name, &entry); |
| if (!res) |
| *out = entry->value; |
| |
| return res; |
| } |
| |
| static int config_error_notfound(const char *name) |
| { |
| giterr_set(GITERR_CONFIG, "Config value '%s' was not found", name); |
| return GIT_ENOTFOUND; |
| } |
| |
| static int get_string(const char **out, const git_config *cfg, const char *name) |
| { |
| file_internal *internal; |
| unsigned int i; |
| int res; |
| |
| git_vector_foreach(&cfg->files, i, internal) { |
| if (!internal || !internal->file) |
| continue; |
| |
| res = get_string_at_file(out, internal->file, name); |
| if (res != GIT_ENOTFOUND) |
| return res; |
| } |
| |
| return config_error_notfound(name); |
| } |
| |
| int git_config_get_bool(int *out, const git_config *cfg, const char *name) |
| { |
| const char *value = NULL; |
| int ret; |
| |
| if ((ret = get_string(&value, cfg, name)) < 0) |
| return ret; |
| |
| return git_config_parse_bool(out, value); |
| } |
| |
| int git_config_get_string(const char **out, const git_config *cfg, const char *name) |
| { |
| int ret; |
| const char *str = NULL; |
| |
| if ((ret = get_string(&str, cfg, name)) < 0) |
| return ret; |
| |
| *out = str == NULL ? "" : str; |
| return 0; |
| } |
| |
| int git_config_get_entry(const git_config_entry **out, const git_config *cfg, const char *name) |
| { |
| file_internal *internal; |
| unsigned int i; |
| git_config_backend *file; |
| int ret; |
| |
| *out = NULL; |
| |
| git_vector_foreach(&cfg->files, i, internal) { |
| if (!internal || !internal->file) |
| continue; |
| file = internal->file; |
| |
| ret = file->get(file, name, out); |
| if (ret != GIT_ENOTFOUND) |
| return ret; |
| } |
| |
| return config_error_notfound(name); |
| } |
| |
| int git_config_get_multivar( |
| const git_config *cfg, const char *name, const char *regexp, |
| git_config_foreach_cb cb, void *payload) |
| { |
| file_internal *internal; |
| git_config_backend *file; |
| int ret = GIT_ENOTFOUND; |
| size_t i; |
| |
| /* |
| * This loop runs the "wrong" way 'round because we need to |
| * look at every value from the most general to most specific |
| */ |
| for (i = cfg->files.length; i > 0; --i) { |
| internal = git_vector_get(&cfg->files, i - 1); |
| if (!internal || !internal->file) |
| continue; |
| file = internal->file; |
| |
| ret = file->get_multivar(file, name, regexp, cb, payload); |
| if (ret < 0 && ret != GIT_ENOTFOUND) |
| return ret; |
| } |
| |
| return (ret == GIT_ENOTFOUND) ? config_error_notfound(name) : 0; |
| } |
| |
| int git_config_set_multivar(git_config *cfg, const char *name, const char *regexp, const char *value) |
| { |
| git_config_backend *file; |
| file_internal *internal; |
| |
| internal = git_vector_get(&cfg->files, 0); |
| if (!internal || !internal->file) |
| return config_error_nofiles(name); |
| file = internal->file; |
| |
| return file->set_multivar(file, name, regexp, value); |
| } |
| |
| static int git_config__find_file_to_path( |
| char *out, size_t outlen, int (*find)(git_buf *buf)) |
| { |
| int error = 0; |
| git_buf path = GIT_BUF_INIT; |
| |
| if ((error = find(&path)) < 0) |
| goto done; |
| |
| if (path.size >= outlen) { |
| giterr_set(GITERR_NOMEMORY, "Buffer is too short for the path"); |
| error = GIT_EBUFS; |
| goto done; |
| } |
| |
| git_buf_copy_cstr(out, outlen, &path); |
| |
| done: |
| git_buf_free(&path); |
| return error; |
| } |
| |
| int git_config_find_global_r(git_buf *path) |
| { |
| return git_futils_find_global_file(path, GIT_CONFIG_FILENAME_GLOBAL); |
| } |
| |
| int git_config_find_global(char *global_config_path, size_t length) |
| { |
| return git_config__find_file_to_path( |
| global_config_path, length, git_config_find_global_r); |
| } |
| |
| int git_config_find_xdg_r(git_buf *path) |
| { |
| return git_futils_find_xdg_file(path, GIT_CONFIG_FILENAME_XDG); |
| } |
| |
| int git_config_find_xdg(char *xdg_config_path, size_t length) |
| { |
| return git_config__find_file_to_path( |
| xdg_config_path, length, git_config_find_xdg_r); |
| } |
| |
| int git_config_find_system_r(git_buf *path) |
| { |
| return git_futils_find_system_file(path, GIT_CONFIG_FILENAME_SYSTEM); |
| } |
| |
| int git_config_find_system(char *system_config_path, size_t length) |
| { |
| return git_config__find_file_to_path( |
| system_config_path, length, git_config_find_system_r); |
| } |
| |
| int git_config__global_location(git_buf *buf) |
| { |
| const git_buf *paths; |
| const char *sep, *start; |
| size_t len; |
| |
| if (git_futils_dirs_get(&paths, GIT_FUTILS_DIR_GLOBAL) < 0) |
| return -1; |
| |
| /* no paths, so give up */ |
| if (git_buf_len(paths) == 0) |
| return -1; |
| |
| start = git_buf_cstr(paths); |
| sep = strchr(start, GIT_PATH_LIST_SEPARATOR); |
| |
| if (sep) |
| len = sep - start; |
| else |
| len = paths->size; |
| |
| if (git_buf_set(buf, start, len) < 0) |
| return -1; |
| |
| return git_buf_joinpath(buf, buf->ptr, GIT_CONFIG_FILENAME_GLOBAL); |
| } |
| |
| int git_config_open_default(git_config **out) |
| { |
| int error; |
| git_config *cfg = NULL; |
| git_buf buf = GIT_BUF_INIT; |
| |
| if ((error = git_config_new(&cfg)) < 0) |
| return error; |
| |
| if (!git_config_find_global_r(&buf) || !git_config__global_location(&buf)) { |
| error = git_config_add_file_ondisk(cfg, buf.ptr, |
| GIT_CONFIG_LEVEL_GLOBAL, 0); |
| } |
| |
| if (!error && !git_config_find_xdg_r(&buf)) |
| error = git_config_add_file_ondisk(cfg, buf.ptr, |
| GIT_CONFIG_LEVEL_XDG, 0); |
| |
| if (!error && !git_config_find_system_r(&buf)) |
| error = git_config_add_file_ondisk(cfg, buf.ptr, |
| GIT_CONFIG_LEVEL_SYSTEM, 0); |
| |
| git_buf_free(&buf); |
| |
| if (error) { |
| git_config_free(cfg); |
| cfg = NULL; |
| } |
| |
| *out = cfg; |
| |
| return error; |
| } |
| |
| /*********** |
| * Parsers |
| ***********/ |
| |
| int git_config_lookup_map_value( |
| int *out, |
| const git_cvar_map *maps, |
| size_t map_n, |
| const char *value) |
| { |
| size_t i; |
| |
| if (!value) |
| goto fail_parse; |
| |
| for (i = 0; i < map_n; ++i) { |
| const git_cvar_map *m = maps + i; |
| |
| switch (m->cvar_type) { |
| case GIT_CVAR_FALSE: |
| case GIT_CVAR_TRUE: { |
| int bool_val; |
| |
| if (git__parse_bool(&bool_val, value) == 0 && |
| bool_val == (int)m->cvar_type) { |
| *out = m->map_value; |
| return 0; |
| } |
| break; |
| } |
| |
| case GIT_CVAR_INT32: |
| if (git_config_parse_int32(out, value) == 0) |
| return 0; |
| break; |
| |
| case GIT_CVAR_STRING: |
| if (strcasecmp(value, m->str_match) == 0) { |
| *out = m->map_value; |
| return 0; |
| } |
| break; |
| } |
| } |
| |
| fail_parse: |
| giterr_set(GITERR_CONFIG, "Failed to map '%s'", value); |
| return -1; |
| } |
| |
| int git_config_parse_bool(int *out, const char *value) |
| { |
| if (git__parse_bool(out, value) == 0) |
| return 0; |
| |
| if (git_config_parse_int32(out, value) == 0) { |
| *out = !!(*out); |
| return 0; |
| } |
| |
| giterr_set(GITERR_CONFIG, "Failed to parse '%s' as a boolean value", value); |
| return -1; |
| } |
| |
| int git_config_parse_int64(int64_t *out, const char *value) |
| { |
| const char *num_end; |
| int64_t num; |
| |
| if (git__strtol64(&num, value, &num_end, 0) < 0) |
| goto fail_parse; |
| |
| switch (*num_end) { |
| case 'g': |
| case 'G': |
| num *= 1024; |
| /* fallthrough */ |
| |
| case 'm': |
| case 'M': |
| num *= 1024; |
| /* fallthrough */ |
| |
| case 'k': |
| case 'K': |
| num *= 1024; |
| |
| /* check that that there are no more characters after the |
| * given modifier suffix */ |
| if (num_end[1] != '\0') |
| return -1; |
| |
| /* fallthrough */ |
| |
| case '\0': |
| *out = num; |
| return 0; |
| |
| default: |
| goto fail_parse; |
| } |
| |
| fail_parse: |
| giterr_set(GITERR_CONFIG, "Failed to parse '%s' as an integer", value); |
| return -1; |
| } |
| |
| int git_config_parse_int32(int32_t *out, const char *value) |
| { |
| int64_t tmp; |
| int32_t truncate; |
| |
| if (git_config_parse_int64(&tmp, value) < 0) |
| goto fail_parse; |
| |
| truncate = tmp & 0xFFFFFFFF; |
| if (truncate != tmp) |
| goto fail_parse; |
| |
| *out = truncate; |
| return 0; |
| |
| fail_parse: |
| giterr_set(GITERR_CONFIG, "Failed to parse '%s' as a 32-bit integer", value); |
| return -1; |
| } |
| |
| struct rename_data { |
| git_config *config; |
| git_buf *name; |
| size_t old_len; |
| int actual_error; |
| }; |
| |
| static int rename_config_entries_cb( |
| const git_config_entry *entry, |
| void *payload) |
| { |
| int error = 0; |
| struct rename_data *data = (struct rename_data *)payload; |
| size_t base_len = git_buf_len(data->name); |
| |
| if (base_len > 0 && |
| !(error = git_buf_puts(data->name, entry->name + data->old_len))) |
| { |
| error = git_config_set_string( |
| data->config, git_buf_cstr(data->name), entry->value); |
| |
| git_buf_truncate(data->name, base_len); |
| } |
| |
| if (!error) |
| error = git_config_delete_entry(data->config, entry->name); |
| |
| data->actual_error = error; /* preserve actual error code */ |
| |
| return error; |
| } |
| |
| int git_config_rename_section( |
| git_repository *repo, |
| const char *old_section_name, |
| const char *new_section_name) |
| { |
| git_config *config; |
| git_buf pattern = GIT_BUF_INIT, replace = GIT_BUF_INIT; |
| int error = 0; |
| struct rename_data data; |
| |
| git_buf_text_puts_escape_regex(&pattern, old_section_name); |
| |
| if ((error = git_buf_puts(&pattern, "\\..+")) < 0) |
| goto cleanup; |
| |
| if ((error = git_repository_config__weakptr(&config, repo)) < 0) |
| goto cleanup; |
| |
| data.config = config; |
| data.name = &replace; |
| data.old_len = strlen(old_section_name) + 1; |
| data.actual_error = 0; |
| |
| if ((error = git_buf_join(&replace, '.', new_section_name, "")) < 0) |
| goto cleanup; |
| |
| if (new_section_name != NULL && |
| (error = git_config_file_normalize_section( |
| replace.ptr, strchr(replace.ptr, '.'))) < 0) |
| { |
| giterr_set( |
| GITERR_CONFIG, "Invalid config section '%s'", new_section_name); |
| goto cleanup; |
| } |
| |
| error = git_config_foreach_match( |
| config, git_buf_cstr(&pattern), rename_config_entries_cb, &data); |
| |
| if (error == GIT_EUSER) |
| error = data.actual_error; |
| |
| cleanup: |
| git_buf_free(&pattern); |
| git_buf_free(&replace); |
| |
| return error; |
| } |