| /* |
| * Copyright (C) the libgit2 contributors. All rights reserved. |
| * |
| * This file is part of libgit2, distributed under the GNU GPL v2 with |
| * a Linking Exception. For full terms see the included COPYING file. |
| */ |
| |
| #include "common.h" |
| #include "config.h" |
| #include "filebuf.h" |
| #include "sysdir.h" |
| #include "buffer.h" |
| #include "buf_text.h" |
| #include "git2/config.h" |
| #include "git2/sys/config.h" |
| #include "git2/types.h" |
| #include "strmap.h" |
| #include "array.h" |
| |
| #include <ctype.h> |
| #include <sys/types.h> |
| #include <regex.h> |
| |
| typedef struct cvar_t { |
| struct cvar_t *next; |
| git_config_entry *entry; |
| bool included; /* whether this is part of [include] */ |
| } cvar_t; |
| |
| typedef struct git_config_file_iter { |
| git_config_iterator parent; |
| git_strmap_iter iter; |
| cvar_t* next_var; |
| } git_config_file_iter; |
| |
| /* Max depth for [include] directives */ |
| #define MAX_INCLUDE_DEPTH 10 |
| |
| #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)) |
| |
| struct reader { |
| git_oid checksum; |
| char *file_path; |
| git_buf buffer; |
| char *read_ptr; |
| int line_number; |
| int eof; |
| }; |
| |
| typedef struct { |
| git_atomic refcount; |
| git_strmap *values; |
| } refcounted_strmap; |
| |
| typedef struct { |
| git_config_backend parent; |
| /* mutex to coordinate accessing the values */ |
| git_mutex values_mutex; |
| refcounted_strmap *values; |
| } diskfile_header; |
| |
| typedef struct { |
| diskfile_header header; |
| |
| git_config_level_t level; |
| |
| git_array_t(struct reader) readers; |
| |
| bool locked; |
| git_filebuf locked_buf; |
| git_buf locked_content; |
| |
| char *file_path; |
| } diskfile_backend; |
| |
| typedef struct { |
| diskfile_header header; |
| |
| diskfile_backend *snapshot_from; |
| } diskfile_readonly_backend; |
| |
| static int config_read(git_strmap *values, diskfile_backend *cfg_file, struct reader *reader, git_config_level_t level, int depth); |
| static int config_write(diskfile_backend *cfg, const char *key, const regex_t *preg, const char *value); |
| static char *escape_value(const char *ptr); |
| |
| int git_config_file__snapshot(git_config_backend **out, diskfile_backend *in); |
| static int config_snapshot(git_config_backend **out, git_config_backend *in); |
| |
| static void set_parse_error(struct reader *reader, int col, const char *error_str) |
| { |
| giterr_set(GITERR_CONFIG, "failed to parse config file: %s (in %s:%d, column %d)", |
| error_str, reader->file_path, reader->line_number, col); |
| } |
| |
| static int config_error_readonly(void) |
| { |
| giterr_set(GITERR_CONFIG, "this backend is read-only"); |
| return -1; |
| } |
| |
| static void cvar_free(cvar_t *var) |
| { |
| if (var == NULL) |
| return; |
| |
| git__free((char*)var->entry->name); |
| git__free((char *)var->entry->value); |
| git__free(var->entry); |
| git__free(var); |
| } |
| |
| int git_config_file_normalize_section(char *start, char *end) |
| { |
| char *scan; |
| |
| if (start == end) |
| return GIT_EINVALIDSPEC; |
| |
| /* Validate and downcase range */ |
| for (scan = start; *scan; ++scan) { |
| if (end && scan >= end) |
| break; |
| if (isalnum(*scan)) |
| *scan = (char)git__tolower(*scan); |
| else if (*scan != '-' || scan == start) |
| return GIT_EINVALIDSPEC; |
| } |
| |
| if (scan == start) |
| return GIT_EINVALIDSPEC; |
| |
| return 0; |
| } |
| |
| /* Add or append the new config option */ |
| static int append_entry(git_strmap *values, cvar_t *var) |
| { |
| git_strmap_iter pos; |
| cvar_t *existing; |
| int error = 0; |
| |
| pos = git_strmap_lookup_index(values, var->entry->name); |
| if (!git_strmap_valid_index(values, pos)) { |
| git_strmap_insert(values, var->entry->name, var, &error); |
| } else { |
| existing = git_strmap_value_at(values, pos); |
| while (existing->next != NULL) { |
| existing = existing->next; |
| } |
| existing->next = var; |
| } |
| |
| if (error > 0) |
| error = 0; |
| |
| return error; |
| } |
| |
| static void free_vars(git_strmap *values) |
| { |
| cvar_t *var = NULL; |
| |
| if (values == NULL) |
| return; |
| |
| git_strmap_foreach_value(values, var, |
| while (var != NULL) { |
| cvar_t *next = CVAR_LIST_NEXT(var); |
| cvar_free(var); |
| var = next; |
| }); |
| |
| git_strmap_free(values); |
| } |
| |
| static void refcounted_strmap_free(refcounted_strmap *map) |
| { |
| if (!map) |
| return; |
| |
| if (git_atomic_dec(&map->refcount) != 0) |
| return; |
| |
| free_vars(map->values); |
| git__free(map); |
| } |
| |
| /** |
| * Take the current values map from the backend and increase its |
| * refcount. This is its own function to make sure we use the mutex to |
| * avoid the map pointer from changing under us. |
| */ |
| static refcounted_strmap *refcounted_strmap_take(diskfile_header *h) |
| { |
| refcounted_strmap *map; |
| |
| if (git_mutex_lock(&h->values_mutex) < 0) { |
| giterr_set(GITERR_OS, "failed to lock config backend"); |
| return NULL; |
| } |
| |
| map = h->values; |
| git_atomic_inc(&map->refcount); |
| |
| git_mutex_unlock(&h->values_mutex); |
| |
| return map; |
| } |
| |
| static int refcounted_strmap_alloc(refcounted_strmap **out) |
| { |
| refcounted_strmap *map; |
| int error; |
| |
| map = git__calloc(1, sizeof(refcounted_strmap)); |
| GITERR_CHECK_ALLOC(map); |
| |
| git_atomic_set(&map->refcount, 1); |
| |
| if ((error = git_strmap_alloc(&map->values)) < 0) |
| git__free(map); |
| else |
| *out = map; |
| |
| return error; |
| } |
| |
| static int config_open(git_config_backend *cfg, git_config_level_t level) |
| { |
| int res; |
| struct reader *reader; |
| diskfile_backend *b = (diskfile_backend *)cfg; |
| |
| b->level = level; |
| |
| if ((res = refcounted_strmap_alloc(&b->header.values)) < 0) |
| return res; |
| |
| git_array_init(b->readers); |
| reader = git_array_alloc(b->readers); |
| if (!reader) { |
| refcounted_strmap_free(b->header.values); |
| return -1; |
| } |
| memset(reader, 0, sizeof(struct reader)); |
| |
| reader->file_path = git__strdup(b->file_path); |
| GITERR_CHECK_ALLOC(reader->file_path); |
| |
| git_buf_init(&reader->buffer, 0); |
| res = git_futils_readbuffer_updated( |
| &reader->buffer, b->file_path, &reader->checksum, NULL); |
| |
| /* It's fine if the file doesn't exist */ |
| if (res == GIT_ENOTFOUND) |
| return 0; |
| |
| if (res < 0 || (res = config_read(b->header.values->values, b, reader, level, 0)) < 0) { |
| refcounted_strmap_free(b->header.values); |
| b->header.values = NULL; |
| } |
| |
| reader = git_array_get(b->readers, 0); |
| git_buf_free(&reader->buffer); |
| |
| return res; |
| } |
| |
| /* The meat of the refresh, as we want to use it in different places */ |
| static int config__refresh(git_config_backend *cfg) |
| { |
| refcounted_strmap *values = NULL, *tmp; |
| diskfile_backend *b = (diskfile_backend *)cfg; |
| struct reader *reader = NULL; |
| int error = 0; |
| |
| if ((error = refcounted_strmap_alloc(&values)) < 0) |
| goto out; |
| |
| reader = git_array_get(b->readers, git_array_size(b->readers) - 1); |
| GITERR_CHECK_ALLOC(reader); |
| |
| if ((error = config_read(values->values, b, reader, b->level, 0)) < 0) |
| goto out; |
| |
| if ((error = git_mutex_lock(&b->header.values_mutex)) < 0) { |
| giterr_set(GITERR_OS, "failed to lock config backend"); |
| goto out; |
| } |
| |
| tmp = b->header.values; |
| b->header.values = values; |
| values = tmp; |
| |
| git_mutex_unlock(&b->header.values_mutex); |
| |
| out: |
| refcounted_strmap_free(values); |
| if (reader) |
| git_buf_free(&reader->buffer); |
| return error; |
| } |
| |
| static int config_refresh(git_config_backend *cfg) |
| { |
| int error = 0, updated = 0, any_updated = 0; |
| diskfile_backend *b = (diskfile_backend *)cfg; |
| struct reader *reader = NULL; |
| uint32_t i; |
| |
| for (i = 0; i < git_array_size(b->readers); i++) { |
| reader = git_array_get(b->readers, i); |
| error = git_futils_readbuffer_updated( |
| &reader->buffer, reader->file_path, |
| &reader->checksum, &updated); |
| |
| if (error < 0 && error != GIT_ENOTFOUND) |
| return error; |
| |
| if (updated) |
| any_updated = 1; |
| } |
| |
| if (!any_updated) |
| return (error == GIT_ENOTFOUND) ? 0 : error; |
| |
| return config__refresh(cfg); |
| } |
| |
| static void backend_free(git_config_backend *_backend) |
| { |
| diskfile_backend *backend = (diskfile_backend *)_backend; |
| uint32_t i; |
| |
| if (backend == NULL) |
| return; |
| |
| for (i = 0; i < git_array_size(backend->readers); i++) { |
| struct reader *r = git_array_get(backend->readers, i); |
| git__free(r->file_path); |
| } |
| git_array_clear(backend->readers); |
| |
| git__free(backend->file_path); |
| refcounted_strmap_free(backend->header.values); |
| git_mutex_free(&backend->header.values_mutex); |
| git__free(backend); |
| } |
| |
| static void config_iterator_free( |
| git_config_iterator* iter) |
| { |
| iter->backend->free(iter->backend); |
| git__free(iter); |
| } |
| |
| static int config_iterator_next( |
| git_config_entry **entry, |
| git_config_iterator *iter) |
| { |
| git_config_file_iter *it = (git_config_file_iter *) iter; |
| diskfile_header *h = (diskfile_header *) it->parent.backend; |
| git_strmap *values = h->values->values; |
| int err = 0; |
| cvar_t * var; |
| |
| if (it->next_var == NULL) { |
| err = git_strmap_next((void**) &var, &(it->iter), values); |
| } else { |
| var = it->next_var; |
| } |
| |
| if (err < 0) { |
| it->next_var = NULL; |
| return err; |
| } |
| |
| *entry = var->entry; |
| it->next_var = CVAR_LIST_NEXT(var); |
| |
| return 0; |
| } |
| |
| static int config_iterator_new( |
| git_config_iterator **iter, |
| struct git_config_backend* backend) |
| { |
| diskfile_header *h; |
| git_config_file_iter *it; |
| git_config_backend *snapshot; |
| diskfile_backend *b = (diskfile_backend *) backend; |
| int error; |
| |
| if ((error = config_snapshot(&snapshot, backend)) < 0) |
| return error; |
| |
| if ((error = snapshot->open(snapshot, b->level)) < 0) |
| return error; |
| |
| it = git__calloc(1, sizeof(git_config_file_iter)); |
| GITERR_CHECK_ALLOC(it); |
| |
| h = (diskfile_header *)snapshot; |
| |
| /* strmap_begin() is currently a macro returning 0 */ |
| GIT_UNUSED(h); |
| |
| it->parent.backend = snapshot; |
| it->iter = git_strmap_begin(h->values); |
| it->next_var = NULL; |
| |
| it->parent.next = config_iterator_next; |
| it->parent.free = config_iterator_free; |
| *iter = (git_config_iterator *) it; |
| |
| return 0; |
| } |
| |
| static int config_set(git_config_backend *cfg, const char *name, const char *value) |
| { |
| diskfile_backend *b = (diskfile_backend *)cfg; |
| refcounted_strmap *map; |
| git_strmap *values; |
| char *key, *esc_value = NULL; |
| khiter_t pos; |
| int rval, ret; |
| |
| if ((rval = git_config__normalize_name(name, &key)) < 0) |
| return rval; |
| |
| if ((map = refcounted_strmap_take(&b->header)) == NULL) |
| return -1; |
| values = map->values; |
| |
| /* |
| * Try to find it in the existing values and update it if it |
| * only has one value. |
| */ |
| pos = git_strmap_lookup_index(values, key); |
| if (git_strmap_valid_index(values, pos)) { |
| cvar_t *existing = git_strmap_value_at(values, pos); |
| |
| if (existing->next != NULL) { |
| giterr_set(GITERR_CONFIG, "multivar incompatible with simple set"); |
| ret = -1; |
| goto out; |
| } |
| |
| /* don't update if old and new values already match */ |
| if ((!existing->entry->value && !value) || |
| (existing->entry->value && value && |
| !strcmp(existing->entry->value, value))) { |
| ret = 0; |
| goto out; |
| } |
| } |
| |
| /* No early returns due to sanity checks, let's write it out and refresh */ |
| |
| if (value) { |
| esc_value = escape_value(value); |
| GITERR_CHECK_ALLOC(esc_value); |
| } |
| |
| if ((ret = config_write(b, key, NULL, esc_value)) < 0) |
| goto out; |
| |
| ret = config_refresh(cfg); |
| |
| out: |
| refcounted_strmap_free(map); |
| git__free(esc_value); |
| git__free(key); |
| return ret; |
| } |
| |
| /* release the map containing the entry as an equivalent to freeing it */ |
| static void release_map(git_config_entry *entry) |
| { |
| refcounted_strmap *map = (refcounted_strmap *) entry->payload; |
| refcounted_strmap_free(map); |
| } |
| |
| /* |
| * Internal function that actually gets the value in string form |
| */ |
| static int config_get(git_config_backend *cfg, const char *key, git_config_entry **out) |
| { |
| diskfile_header *h = (diskfile_header *)cfg; |
| refcounted_strmap *map; |
| git_strmap *values; |
| khiter_t pos; |
| cvar_t *var; |
| int error = 0; |
| |
| if (!h->parent.readonly && ((error = config_refresh(cfg)) < 0)) |
| return error; |
| |
| if ((map = refcounted_strmap_take(h)) == NULL) |
| return -1; |
| values = map->values; |
| |
| pos = git_strmap_lookup_index(values, key); |
| |
| /* no error message; the config system will write one */ |
| if (!git_strmap_valid_index(values, pos)) { |
| refcounted_strmap_free(map); |
| return GIT_ENOTFOUND; |
| } |
| |
| var = git_strmap_value_at(values, pos); |
| while (var->next) |
| var = var->next; |
| |
| *out = var->entry; |
| (*out)->free = release_map; |
| (*out)->payload = map; |
| |
| return error; |
| } |
| |
| static int config_set_multivar( |
| git_config_backend *cfg, const char *name, const char *regexp, const char *value) |
| { |
| diskfile_backend *b = (diskfile_backend *)cfg; |
| char *key; |
| regex_t preg; |
| int result; |
| |
| assert(regexp); |
| |
| if ((result = git_config__normalize_name(name, &key)) < 0) |
| return result; |
| |
| result = p_regcomp(&preg, regexp, REG_EXTENDED); |
| if (result != 0) { |
| giterr_set_regex(&preg, result); |
| result = -1; |
| goto out; |
| } |
| |
| /* If we do have it, set call config_write() and reload */ |
| if ((result = config_write(b, key, &preg, value)) < 0) |
| goto out; |
| |
| result = config_refresh(cfg); |
| |
| out: |
| git__free(key); |
| regfree(&preg); |
| |
| return result; |
| } |
| |
| static int config_delete(git_config_backend *cfg, const char *name) |
| { |
| cvar_t *var; |
| diskfile_backend *b = (diskfile_backend *)cfg; |
| refcounted_strmap *map; git_strmap *values; |
| char *key; |
| int result; |
| khiter_t pos; |
| |
| if ((result = git_config__normalize_name(name, &key)) < 0) |
| return result; |
| |
| if ((map = refcounted_strmap_take(&b->header)) == NULL) |
| return -1; |
| values = b->header.values->values; |
| |
| pos = git_strmap_lookup_index(values, key); |
| git__free(key); |
| |
| if (!git_strmap_valid_index(values, pos)) { |
| refcounted_strmap_free(map); |
| giterr_set(GITERR_CONFIG, "could not find key '%s' to delete", name); |
| return GIT_ENOTFOUND; |
| } |
| |
| var = git_strmap_value_at(values, pos); |
| refcounted_strmap_free(map); |
| |
| if (var->next != NULL) { |
| giterr_set(GITERR_CONFIG, "cannot delete multivar with a single delete"); |
| return -1; |
| } |
| |
| if ((result = config_write(b, var->entry->name, NULL, NULL)) < 0) |
| return result; |
| |
| return config_refresh(cfg); |
| } |
| |
| static int config_delete_multivar(git_config_backend *cfg, const char *name, const char *regexp) |
| { |
| diskfile_backend *b = (diskfile_backend *)cfg; |
| refcounted_strmap *map; |
| git_strmap *values; |
| char *key; |
| regex_t preg; |
| int result; |
| khiter_t pos; |
| |
| if ((result = git_config__normalize_name(name, &key)) < 0) |
| return result; |
| |
| if ((map = refcounted_strmap_take(&b->header)) == NULL) |
| return -1; |
| values = b->header.values->values; |
| |
| pos = git_strmap_lookup_index(values, key); |
| |
| if (!git_strmap_valid_index(values, pos)) { |
| refcounted_strmap_free(map); |
| git__free(key); |
| giterr_set(GITERR_CONFIG, "could not find key '%s' to delete", name); |
| return GIT_ENOTFOUND; |
| } |
| |
| refcounted_strmap_free(map); |
| |
| result = p_regcomp(&preg, regexp, REG_EXTENDED); |
| if (result != 0) { |
| giterr_set_regex(&preg, result); |
| result = -1; |
| goto out; |
| } |
| |
| if ((result = config_write(b, key, &preg, NULL)) < 0) |
| goto out; |
| |
| result = config_refresh(cfg); |
| |
| out: |
| git__free(key); |
| regfree(&preg); |
| return result; |
| } |
| |
| static int config_snapshot(git_config_backend **out, git_config_backend *in) |
| { |
| diskfile_backend *b = (diskfile_backend *) in; |
| |
| return git_config_file__snapshot(out, b); |
| } |
| |
| static int config_lock(git_config_backend *_cfg) |
| { |
| diskfile_backend *cfg = (diskfile_backend *) _cfg; |
| int error; |
| |
| if ((error = git_filebuf_open(&cfg->locked_buf, cfg->file_path, 0, GIT_CONFIG_FILE_MODE)) < 0) |
| return error; |
| |
| error = git_futils_readbuffer(&cfg->locked_content, cfg->file_path); |
| if (error < 0 && error != GIT_ENOTFOUND) { |
| git_filebuf_cleanup(&cfg->locked_buf); |
| return error; |
| } |
| |
| cfg->locked = true; |
| return 0; |
| |
| } |
| |
| static int config_unlock(git_config_backend *_cfg, int success) |
| { |
| diskfile_backend *cfg = (diskfile_backend *) _cfg; |
| int error = 0; |
| |
| if (success) { |
| git_filebuf_write(&cfg->locked_buf, cfg->locked_content.ptr, cfg->locked_content.size); |
| error = git_filebuf_commit(&cfg->locked_buf); |
| } |
| |
| git_filebuf_cleanup(&cfg->locked_buf); |
| git_buf_free(&cfg->locked_content); |
| cfg->locked = false; |
| |
| return error; |
| } |
| |
| int git_config_file__ondisk(git_config_backend **out, const char *path) |
| { |
| diskfile_backend *backend; |
| |
| backend = git__calloc(1, sizeof(diskfile_backend)); |
| GITERR_CHECK_ALLOC(backend); |
| |
| backend->header.parent.version = GIT_CONFIG_BACKEND_VERSION; |
| git_mutex_init(&backend->header.values_mutex); |
| |
| backend->file_path = git__strdup(path); |
| GITERR_CHECK_ALLOC(backend->file_path); |
| |
| backend->header.parent.open = config_open; |
| backend->header.parent.get = config_get; |
| backend->header.parent.set = config_set; |
| backend->header.parent.set_multivar = config_set_multivar; |
| backend->header.parent.del = config_delete; |
| backend->header.parent.del_multivar = config_delete_multivar; |
| backend->header.parent.iterator = config_iterator_new; |
| backend->header.parent.snapshot = config_snapshot; |
| backend->header.parent.lock = config_lock; |
| backend->header.parent.unlock = config_unlock; |
| backend->header.parent.free = backend_free; |
| |
| *out = (git_config_backend *)backend; |
| |
| return 0; |
| } |
| |
| static int config_set_readonly(git_config_backend *cfg, const char *name, const char *value) |
| { |
| GIT_UNUSED(cfg); |
| GIT_UNUSED(name); |
| GIT_UNUSED(value); |
| |
| return config_error_readonly(); |
| } |
| |
| static int config_set_multivar_readonly( |
| git_config_backend *cfg, const char *name, const char *regexp, const char *value) |
| { |
| GIT_UNUSED(cfg); |
| GIT_UNUSED(name); |
| GIT_UNUSED(regexp); |
| GIT_UNUSED(value); |
| |
| return config_error_readonly(); |
| } |
| |
| static int config_delete_multivar_readonly(git_config_backend *cfg, const char *name, const char *regexp) |
| { |
| GIT_UNUSED(cfg); |
| GIT_UNUSED(name); |
| GIT_UNUSED(regexp); |
| |
| return config_error_readonly(); |
| } |
| |
| static int config_delete_readonly(git_config_backend *cfg, const char *name) |
| { |
| GIT_UNUSED(cfg); |
| GIT_UNUSED(name); |
| |
| return config_error_readonly(); |
| } |
| |
| static int config_lock_readonly(git_config_backend *_cfg) |
| { |
| GIT_UNUSED(_cfg); |
| |
| return config_error_readonly(); |
| } |
| |
| static int config_unlock_readonly(git_config_backend *_cfg, int success) |
| { |
| GIT_UNUSED(_cfg); |
| GIT_UNUSED(success); |
| |
| return config_error_readonly(); |
| } |
| |
| static void backend_readonly_free(git_config_backend *_backend) |
| { |
| diskfile_backend *backend = (diskfile_backend *)_backend; |
| |
| if (backend == NULL) |
| return; |
| |
| refcounted_strmap_free(backend->header.values); |
| git_mutex_free(&backend->header.values_mutex); |
| git__free(backend); |
| } |
| |
| static int config_readonly_open(git_config_backend *cfg, git_config_level_t level) |
| { |
| diskfile_readonly_backend *b = (diskfile_readonly_backend *) cfg; |
| diskfile_backend *src = b->snapshot_from; |
| diskfile_header *src_header = &src->header; |
| refcounted_strmap *src_map; |
| int error; |
| |
| if (!src_header->parent.readonly && (error = config_refresh(&src_header->parent)) < 0) |
| return error; |
| |
| /* We're just copying data, don't care about the level */ |
| GIT_UNUSED(level); |
| |
| if ((src_map = refcounted_strmap_take(src_header)) == NULL) |
| return -1; |
| b->header.values = src_map; |
| |
| return 0; |
| } |
| |
| int git_config_file__snapshot(git_config_backend **out, diskfile_backend *in) |
| { |
| diskfile_readonly_backend *backend; |
| |
| backend = git__calloc(1, sizeof(diskfile_readonly_backend)); |
| GITERR_CHECK_ALLOC(backend); |
| |
| backend->header.parent.version = GIT_CONFIG_BACKEND_VERSION; |
| git_mutex_init(&backend->header.values_mutex); |
| |
| backend->snapshot_from = in; |
| |
| backend->header.parent.readonly = 1; |
| backend->header.parent.version = GIT_CONFIG_BACKEND_VERSION; |
| backend->header.parent.open = config_readonly_open; |
| backend->header.parent.get = config_get; |
| backend->header.parent.set = config_set_readonly; |
| backend->header.parent.set_multivar = config_set_multivar_readonly; |
| backend->header.parent.del = config_delete_readonly; |
| backend->header.parent.del_multivar = config_delete_multivar_readonly; |
| backend->header.parent.iterator = config_iterator_new; |
| backend->header.parent.lock = config_lock_readonly; |
| backend->header.parent.unlock = config_unlock_readonly; |
| backend->header.parent.free = backend_readonly_free; |
| |
| *out = (git_config_backend *)backend; |
| |
| return 0; |
| } |
| |
| static int reader_getchar_raw(struct reader *reader) |
| { |
| int c; |
| |
| c = *reader->read_ptr++; |
| |
| /* |
| Win 32 line breaks: if we find a \r\n sequence, |
| return only the \n as a newline |
| */ |
| if (c == '\r' && *reader->read_ptr == '\n') { |
| reader->read_ptr++; |
| c = '\n'; |
| } |
| |
| if (c == '\n') |
| reader->line_number++; |
| |
| if (c == 0) { |
| reader->eof = 1; |
| c = '\0'; |
| } |
| |
| return c; |
| } |
| |
| #define SKIP_WHITESPACE (1 << 1) |
| #define SKIP_COMMENTS (1 << 2) |
| |
| static int reader_getchar(struct reader *reader, int flags) |
| { |
| const int skip_whitespace = (flags & SKIP_WHITESPACE); |
| const int skip_comments = (flags & SKIP_COMMENTS); |
| int c; |
| |
| assert(reader->read_ptr); |
| |
| do { |
| c = reader_getchar_raw(reader); |
| } while (c != '\n' && c != '\0' && skip_whitespace && git__isspace(c)); |
| |
| if (skip_comments && (c == '#' || c == ';')) { |
| do { |
| c = reader_getchar_raw(reader); |
| } while (c != '\n' && c != '\0'); |
| } |
| |
| return c; |
| } |
| |
| /* |
| * Read the next char, but don't move the reading pointer. |
| */ |
| static int reader_peek(struct reader *reader, int flags) |
| { |
| void *old_read_ptr; |
| int old_lineno, old_eof; |
| int ret; |
| |
| assert(reader->read_ptr); |
| |
| old_read_ptr = reader->read_ptr; |
| old_lineno = reader->line_number; |
| old_eof = reader->eof; |
| |
| ret = reader_getchar(reader, flags); |
| |
| reader->read_ptr = old_read_ptr; |
| reader->line_number = old_lineno; |
| reader->eof = old_eof; |
| |
| return ret; |
| } |
| |
| /* |
| * Read and consume a line, returning it in newly-allocated memory. |
| */ |
| static char *reader_readline(struct reader *reader, bool skip_whitespace) |
| { |
| char *line = NULL; |
| char *line_src, *line_end; |
| size_t line_len, alloc_len; |
| |
| line_src = reader->read_ptr; |
| |
| if (skip_whitespace) { |
| /* Skip empty empty lines */ |
| while (git__isspace(*line_src)) |
| ++line_src; |
| } |
| |
| line_end = strchr(line_src, '\n'); |
| |
| /* no newline at EOF */ |
| if (line_end == NULL) |
| line_end = strchr(line_src, 0); |
| |
| line_len = line_end - line_src; |
| |
| if (GIT_ADD_SIZET_OVERFLOW(&alloc_len, line_len, 1) || |
| (line = git__malloc(alloc_len)) == NULL) { |
| return NULL; |
| } |
| |
| memcpy(line, line_src, line_len); |
| |
| do line[line_len] = '\0'; |
| while (line_len-- > 0 && git__isspace(line[line_len])); |
| |
| if (*line_end == '\n') |
| line_end++; |
| |
| if (*line_end == '\0') |
| reader->eof = 1; |
| |
| reader->line_number++; |
| reader->read_ptr = line_end; |
| |
| return line; |
| } |
| |
| /* |
| * Consume a line, without storing it anywhere |
| */ |
| static void reader_consume_line(struct reader *reader) |
| { |
| char *line_start, *line_end; |
| |
| line_start = 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') |
| reader->eof = 1; |
| |
| reader->line_number++; |
| reader->read_ptr = line_end; |
| } |
| |
| GIT_INLINE(int) config_keychar(int c) |
| { |
| return isalnum(c) || c == '-'; |
| } |
| |
| static int parse_section_header_ext(struct reader *reader, const char *line, const char *base_name, char **section_name) |
| { |
| int c, rpos; |
| char *first_quote, *last_quote; |
| git_buf buf = GIT_BUF_INIT; |
| size_t quoted_len, alloc_len, base_name_len = strlen(base_name); |
| |
| /* |
| * 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, '"'); |
| if (first_quote == NULL) { |
| set_parse_error(reader, 0, "Missing quotation marks in section header"); |
| return -1; |
| } |
| |
| last_quote = strrchr(line, '"'); |
| quoted_len = last_quote - first_quote; |
| |
| if (quoted_len == 0) { |
| set_parse_error(reader, 0, "Missing closing quotation mark in section header"); |
| return -1; |
| } |
| |
| GITERR_CHECK_ALLOC_ADD(&alloc_len, base_name_len, quoted_len); |
| GITERR_CHECK_ALLOC_ADD(&alloc_len, alloc_len, 2); |
| |
| git_buf_grow(&buf, alloc_len); |
| git_buf_printf(&buf, "%s.", base_name); |
| |
| rpos = 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 0: |
| set_parse_error(reader, 0, "Unexpected end-of-line in section header"); |
| git_buf_free(&buf); |
| return -1; |
| |
| case '"': |
| goto end_parse; |
| |
| case '\\': |
| c = line[++rpos]; |
| |
| if (c == 0) { |
| set_parse_error(reader, rpos, "Unexpected end-of-line in section header"); |
| git_buf_free(&buf); |
| return -1; |
| } |
| |
| default: |
| break; |
| } |
| |
| git_buf_putc(&buf, (char)c); |
| c = line[++rpos]; |
| } while (line + rpos < last_quote); |
| |
| end_parse: |
| if (line[rpos] != '"' || line[rpos + 1] != ']') { |
| set_parse_error(reader, rpos, "Unexpected text after closing quotes"); |
| git_buf_free(&buf); |
| return -1; |
| } |
| |
| *section_name = git_buf_detach(&buf); |
| return 0; |
| } |
| |
| static int parse_section_header(struct reader *reader, char **section_out) |
| { |
| char *name, *name_end; |
| int name_length, c, pos; |
| int result; |
| char *line; |
| size_t line_len; |
| |
| line = reader_readline(reader, true); |
| if (line == NULL) |
| return -1; |
| |
| /* find the end of the variable's name */ |
| name_end = strrchr(line, ']'); |
| if (name_end == NULL) { |
| git__free(line); |
| set_parse_error(reader, 0, "Missing ']' in section header"); |
| return -1; |
| } |
| |
| GITERR_CHECK_ALLOC_ADD(&line_len, (size_t)(name_end - line), 1); |
| name = git__malloc(line_len); |
| GITERR_CHECK_ALLOC(name); |
| |
| name_length = 0; |
| pos = 0; |
| |
| /* Make sure we were given a section header */ |
| c = line[pos++]; |
| assert(c == '['); |
| |
| c = line[pos++]; |
| |
| do { |
| if (git__isspace(c)){ |
| name[name_length] = '\0'; |
| result = parse_section_header_ext(reader, line, name, section_out); |
| git__free(line); |
| git__free(name); |
| return result; |
| } |
| |
| if (!config_keychar(c) && c != '.') { |
| set_parse_error(reader, pos, "Unexpected character in header"); |
| goto fail_parse; |
| } |
| |
| name[name_length++] = (char)git__tolower(c); |
| |
| } while ((c = line[pos++]) != ']'); |
| |
| if (line[pos - 1] != ']') { |
| set_parse_error(reader, pos, "Unexpected end of file"); |
| goto fail_parse; |
| } |
| |
| git__free(line); |
| |
| name[name_length] = 0; |
| *section_out = name; |
| |
| return 0; |
| |
| fail_parse: |
| git__free(line); |
| git__free(name); |
| return -1; |
| } |
| |
| static int skip_bom(struct reader *reader) |
| { |
| git_bom_t bom; |
| int bom_offset = git_buf_text_detect_bom(&bom, |
| &reader->buffer, reader->read_ptr - reader->buffer.ptr); |
| |
| if (bom == GIT_BOM_UTF8) |
| reader->read_ptr += bom_offset; |
| |
| /* TODO: reference implementation is pretty stupid with BoM */ |
| |
| return 0; |
| } |
| |
| /* |
| (* basic types *) |
| digit = "0".."9" |
| integer = digit { digit } |
| alphabet = "a".."z" + "A" .. "Z" |
| |
| section_char = alphabet | "." | "-" |
| extension_char = (* any character except newline *) |
| any_char = (* any character *) |
| variable_char = "alphabet" | "-" |
| |
| |
| (* actual grammar *) |
| config = { section } |
| |
| section = header { definition } |
| |
| header = "[" section [subsection | subsection_ext] "]" |
| |
| subsection = "." section |
| subsection_ext = "\"" extension "\"" |
| |
| section = section_char { section_char } |
| extension = extension_char { extension_char } |
| |
| definition = variable_name ["=" variable_value] "\n" |
| |
| variable_name = variable_char { variable_char } |
| variable_value = string | boolean | integer |
| |
| string = quoted_string | plain_string |
| quoted_string = "\"" plain_string "\"" |
| plain_string = { any_char } |
| |
| boolean = boolean_true | boolean_false |
| boolean_true = "yes" | "1" | "true" | "on" |
| boolean_false = "no" | "0" | "false" | "off" |
| */ |
| |
| static int strip_comments(char *line, int in_quotes) |
| { |
| int quote_count = in_quotes, backslash_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 && |
| (backslash_count % 2) == 0) { |
| ptr[0] = '\0'; |
| break; |
| } |
| |
| if (ptr[0] == '\\') |
| backslash_count++; |
| else |
| backslash_count = 0; |
| } |
| |
| /* skip any space at the end */ |
| while (ptr > line && git__isspace(ptr[-1])) { |
| ptr--; |
| } |
| ptr[0] = '\0'; |
| |
| return quote_count; |
| } |
| |
| static int included_path(git_buf *out, const char *dir, const char *path) |
| { |
| /* From the user's home */ |
| if (path[0] == '~' && path[1] == '/') |
| return git_sysdir_find_global_file(out, &path[1]); |
| |
| return git_path_join_unrooted(out, path, dir, NULL); |
| } |
| |
| static const char *escapes = "ntb\"\\"; |
| static const char *escaped = "\n\t\b\"\\"; |
| |
| /* Escape the values to write them to the file */ |
| static char *escape_value(const char *ptr) |
| { |
| git_buf buf = GIT_BUF_INIT; |
| size_t len; |
| const char *esc; |
| |
| assert(ptr); |
| |
| len = strlen(ptr); |
| if (!len) |
| return git__calloc(1, sizeof(char)); |
| |
| git_buf_grow(&buf, len); |
| |
| while (*ptr != '\0') { |
| if ((esc = strchr(escaped, *ptr)) != NULL) { |
| git_buf_putc(&buf, '\\'); |
| git_buf_putc(&buf, escapes[esc - escaped]); |
| } else { |
| git_buf_putc(&buf, *ptr); |
| } |
| ptr++; |
| } |
| |
| if (git_buf_oom(&buf)) { |
| git_buf_free(&buf); |
| return NULL; |
| } |
| |
| return git_buf_detach(&buf); |
| } |
| |
| /* '\"' -> '"' etc */ |
| static int unescape_line( |
| char **out, bool *is_multi, const char *ptr, int quote_count) |
| { |
| char *str, *fixed, *esc; |
| size_t ptr_len = strlen(ptr), alloc_len; |
| |
| *is_multi = false; |
| |
| if (GIT_ADD_SIZET_OVERFLOW(&alloc_len, ptr_len, 1) || |
| (str = git__malloc(alloc_len)) == NULL) { |
| return -1; |
| } |
| |
| fixed = str; |
| |
| while (*ptr != '\0') { |
| if (*ptr == '"') { |
| quote_count++; |
| } else if (*ptr != '\\') { |
| *fixed++ = *ptr; |
| } else { |
| /* backslash, check the next char */ |
| ptr++; |
| /* if we're at the end, it's a multiline, so keep the backslash */ |
| if (*ptr == '\0') { |
| *is_multi = true; |
| goto done; |
| } |
| if ((esc = strchr(escapes, *ptr)) != NULL) { |
| *fixed++ = escaped[esc - escapes]; |
| } else { |
| git__free(str); |
| giterr_set(GITERR_CONFIG, "invalid escape at %s", ptr); |
| return -1; |
| } |
| } |
| ptr++; |
| } |
| |
| done: |
| *fixed = '\0'; |
| *out = str; |
| |
| return 0; |
| } |
| |
| static int parse_multiline_variable(struct reader *reader, git_buf *value, int in_quotes) |
| { |
| char *line = NULL, *proc_line = NULL; |
| int quote_count; |
| bool multiline; |
| |
| /* Check that the next line exists */ |
| line = reader_readline(reader, false); |
| if (line == NULL) |
| return -1; |
| |
| /* We've reached the end of the file, there is no continuation. |
| * (this is not an error). |
| */ |
| if (line[0] == '\0') { |
| git__free(line); |
| return 0; |
| } |
| |
| quote_count = strip_comments(line, !!in_quotes); |
| |
| /* If it was just a comment, pretend it didn't exist */ |
| if (line[0] == '\0') { |
| git__free(line); |
| return parse_multiline_variable(reader, value, quote_count); |
| /* TODO: unbounded recursion. This **could** be exploitable */ |
| } |
| |
| if (unescape_line(&proc_line, &multiline, line, in_quotes) < 0) { |
| git__free(line); |
| return -1; |
| } |
| /* add this line to the multiline var */ |
| |
| git_buf_puts(value, proc_line); |
| git__free(line); |
| git__free(proc_line); |
| |
| /* |
| * If we need to continue reading the next line, let's just |
| * keep putting stuff in the buffer |
| */ |
| if (multiline) |
| return parse_multiline_variable(reader, value, quote_count); |
| |
| return 0; |
| } |
| |
| GIT_INLINE(bool) is_namechar(char c) |
| { |
| return isalnum(c) || c == '-'; |
| } |
| |
| static int parse_name( |
| char **name, const char **value, struct reader *reader, const char *line) |
| { |
| const char *name_end = line, *value_start; |
| |
| *name = NULL; |
| *value = NULL; |
| |
| while (*name_end && is_namechar(*name_end)) |
| name_end++; |
| |
| if (line == name_end) { |
| set_parse_error(reader, 0, "Invalid configuration key"); |
| return -1; |
| } |
| |
| value_start = name_end; |
| |
| while (*value_start && git__isspace(*value_start)) |
| value_start++; |
| |
| if (*value_start == '=') { |
| *value = value_start + 1; |
| } else if (*value_start) { |
| set_parse_error(reader, 0, "Invalid configuration key"); |
| return -1; |
| } |
| |
| if ((*name = git__strndup(line, name_end - line)) == NULL) |
| return -1; |
| |
| return 0; |
| } |
| |
| static int parse_variable(struct reader *reader, char **var_name, char **var_value) |
| { |
| const char *value_start = NULL; |
| char *line; |
| int quote_count; |
| bool multiline; |
| |
| line = reader_readline(reader, true); |
| if (line == NULL) |
| return -1; |
| |
| quote_count = strip_comments(line, 0); |
| |
| /* If there is no value, boolean true is assumed */ |
| *var_value = NULL; |
| |
| if (parse_name(var_name, &value_start, reader, line) < 0) |
| goto on_error; |
| |
| /* |
| * Now, let's try to parse the value |
| */ |
| if (value_start != NULL) { |
| while (git__isspace(value_start[0])) |
| value_start++; |
| |
| if (unescape_line(var_value, &multiline, value_start, 0) < 0) |
| goto on_error; |
| |
| if (multiline) { |
| git_buf multi_value = GIT_BUF_INIT; |
| git_buf_attach(&multi_value, *var_value, 0); |
| |
| if (parse_multiline_variable(reader, &multi_value, quote_count) < 0 || |
| git_buf_oom(&multi_value)) { |
| git_buf_free(&multi_value); |
| goto on_error; |
| } |
| |
| *var_value = git_buf_detach(&multi_value); |
| } |
| } |
| |
| git__free(line); |
| return 0; |
| |
| on_error: |
| git__free(*var_name); |
| git__free(line); |
| return -1; |
| } |
| |
| static int config_parse( |
| struct reader *reader, |
| int (*on_section)(struct reader **reader, const char *current_section, const char *line, size_t line_len, void *data), |
| int (*on_variable)(struct reader **reader, const char *current_section, char *var_name, char *var_value, const char *line, size_t line_len, void *data), |
| int (*on_comment)(struct reader **reader, const char *line, size_t line_len, void *data), |
| int (*on_eof)(struct reader **reader, const char *current_section, void *data), |
| void *data) |
| { |
| char *current_section = NULL, *var_name, *var_value, *line_start; |
| char c; |
| size_t line_len; |
| int result = 0; |
| |
| skip_bom(reader); |
| |
| while (result == 0 && !reader->eof) { |
| line_start = reader->read_ptr; |
| |
| c = reader_peek(reader, SKIP_WHITESPACE); |
| |
| switch (c) { |
| case '\0': /* EOF when peeking, set EOF in the reader to exit the loop */ |
| reader->eof = 1; |
| break; |
| |
| case '[': /* section header, new section begins */ |
| git__free(current_section); |
| current_section = NULL; |
| |
| if ((result = parse_section_header(reader, ¤t_section)) == 0 && on_section) { |
| line_len = reader->read_ptr - line_start; |
| result = on_section(&reader, current_section, line_start, line_len, data); |
| } |
| break; |
| |
| case '\n': /* comment or whitespace-only */ |
| case ';': |
| case '#': |
| reader_consume_line(reader); |
| |
| if (on_comment) { |
| line_len = reader->read_ptr - line_start; |
| result = on_comment(&reader, line_start, line_len, data); |
| } |
| break; |
| |
| default: /* assume variable declaration */ |
| if ((result = parse_variable(reader, &var_name, &var_value)) == 0 && on_variable) { |
| line_len = reader->read_ptr - line_start; |
| result = on_variable(&reader, current_section, var_name, var_value, line_start, line_len, data); |
| } |
| break; |
| } |
| } |
| |
| if (on_eof) |
| result = on_eof(&reader, current_section, data); |
| |
| git__free(current_section); |
| return result; |
| } |
| |
| struct parse_data { |
| git_strmap *values; |
| diskfile_backend *cfg_file; |
| uint32_t reader_idx; |
| git_config_level_t level; |
| int depth; |
| }; |
| |
| static int read_on_variable( |
| struct reader **reader, |
| const char *current_section, |
| char *var_name, |
| char *var_value, |
| const char *line, |
| size_t line_len, |
| void *data) |
| { |
| struct parse_data *parse_data = (struct parse_data *)data; |
| git_buf buf = GIT_BUF_INIT; |
| cvar_t *var; |
| int result = 0; |
| |
| GIT_UNUSED(line); |
| GIT_UNUSED(line_len); |
| |
| git__strtolower(var_name); |
| git_buf_printf(&buf, "%s.%s", current_section, var_name); |
| git__free(var_name); |
| |
| if (git_buf_oom(&buf)) { |
| git__free(var_value); |
| return -1; |
| } |
| |
| var = git__calloc(1, sizeof(cvar_t)); |
| GITERR_CHECK_ALLOC(var); |
| var->entry = git__calloc(1, sizeof(git_config_entry)); |
| GITERR_CHECK_ALLOC(var->entry); |
| |
| var->entry->name = git_buf_detach(&buf); |
| var->entry->value = var_value; |
| var->entry->level = parse_data->level; |
| var->included = !!parse_data->depth; |
| |
| if ((result = append_entry(parse_data->values, var)) < 0) |
| return result; |
| |
| result = 0; |
| |
| /* Add or append the new config option */ |
| if (!git__strcmp(var->entry->name, "include.path")) { |
| struct reader *r; |
| git_buf path = GIT_BUF_INIT; |
| char *dir; |
| uint32_t index; |
| |
| r = git_array_alloc(parse_data->cfg_file->readers); |
| /* The reader may have been reallocated */ |
| *reader = git_array_get(parse_data->cfg_file->readers, parse_data->reader_idx); |
| memset(r, 0, sizeof(struct reader)); |
| |
| if ((result = git_path_dirname_r(&path, (*reader)->file_path)) < 0) |
| return result; |
| |
| /* We need to know our index in the array, as the next config_parse call may realloc */ |
| index = git_array_size(parse_data->cfg_file->readers) - 1; |
| dir = git_buf_detach(&path); |
| result = included_path(&path, dir, var->entry->value); |
| git__free(dir); |
| |
| if (result < 0) |
| return result; |
| |
| r->file_path = git_buf_detach(&path); |
| git_buf_init(&r->buffer, 0); |
| |
| result = git_futils_readbuffer_updated( |
| &r->buffer, r->file_path, &r->checksum, NULL); |
| |
| if (result == 0) { |
| result = config_read(parse_data->values, parse_data->cfg_file, r, parse_data->level, parse_data->depth+1); |
| r = git_array_get(parse_data->cfg_file->readers, index); |
| *reader = git_array_get(parse_data->cfg_file->readers, parse_data->reader_idx); |
| } else if (result == GIT_ENOTFOUND) { |
| giterr_clear(); |
| result = 0; |
| } |
| |
| git_buf_free(&r->buffer); |
| } |
| |
| return result; |
| } |
| |
| static int config_read(git_strmap *values, diskfile_backend *cfg_file, struct reader *reader, git_config_level_t level, int depth) |
| { |
| struct parse_data parse_data; |
| |
| if (depth >= MAX_INCLUDE_DEPTH) { |
| giterr_set(GITERR_CONFIG, "maximum config include depth reached"); |
| return -1; |
| } |
| |
| /* Initialize the reading position */ |
| reader->read_ptr = reader->buffer.ptr; |
| reader->eof = 0; |
| |
| /* If the file is empty, there's nothing for us to do */ |
| if (*reader->read_ptr == '\0') |
| return 0; |
| |
| parse_data.values = values; |
| parse_data.cfg_file = cfg_file; |
| parse_data.reader_idx = git_array_size(cfg_file->readers) - 1; |
| parse_data.level = level; |
| parse_data.depth = depth; |
| |
| return config_parse(reader, NULL, read_on_variable, NULL, NULL, &parse_data); |
| } |
| |
| static int write_section(git_buf *fbuf, const char *key) |
| { |
| int result; |
| const char *dot; |
| git_buf buf = GIT_BUF_INIT; |
| |
| /* All of this just for [section "subsection"] */ |
| dot = strchr(key, '.'); |
| git_buf_putc(&buf, '['); |
| if (dot == NULL) { |
| git_buf_puts(&buf, key); |
| } else { |
| char *escaped; |
| git_buf_put(&buf, key, dot - key); |
| escaped = escape_value(dot + 1); |
| GITERR_CHECK_ALLOC(escaped); |
| git_buf_printf(&buf, " \"%s\"", escaped); |
| git__free(escaped); |
| } |
| git_buf_puts(&buf, "]\n"); |
| |
| if (git_buf_oom(&buf)) |
| return -1; |
| |
| result = git_buf_put(fbuf, git_buf_cstr(&buf), buf.size); |
| git_buf_free(&buf); |
| |
| return result; |
| } |
| |
| static const char *quotes_for_value(const char *value) |
| { |
| const char *ptr; |
| |
| if (value[0] == ' ' || value[0] == '\0') |
| return "\""; |
| |
| for (ptr = value; *ptr; ++ptr) { |
| if (*ptr == ';' || *ptr == '#') |
| return "\""; |
| } |
| |
| if (ptr[-1] == ' ') |
| return "\""; |
| |
| return ""; |
| } |
| |
| struct write_data { |
| git_buf *buf; |
| git_buf buffered_comment; |
| unsigned int in_section : 1, |
| preg_replaced : 1; |
| const char *section; |
| const char *name; |
| const regex_t *preg; |
| const char *value; |
| }; |
| |
| static int write_line_to(git_buf *buf, const char *line, size_t line_len) |
| { |
| int result = git_buf_put(buf, line, line_len); |
| |
| if (!result && line_len && line[line_len-1] != '\n') |
| result = git_buf_printf(buf, "\n"); |
| |
| return result; |
| } |
| |
| static int write_line(struct write_data *write_data, const char *line, size_t line_len) |
| { |
| return write_line_to(write_data->buf, line, line_len); |
| } |
| |
| static int write_value(struct write_data *write_data) |
| { |
| const char *q; |
| int result; |
| |
| q = quotes_for_value(write_data->value); |
| result = git_buf_printf(write_data->buf, |
| "\t%s = %s%s%s\n", write_data->name, q, write_data->value, q); |
| |
| /* If we are updating a single name/value, we're done. Setting `value` |
| * to `NULL` will prevent us from trying to write it again later (in |
| * `write_on_section`) if we see the same section repeated. |
| */ |
| if (!write_data->preg) |
| write_data->value = NULL; |
| |
| return result; |
| } |
| |
| static int write_on_section( |
| struct reader **reader, |
| const char *current_section, |
| const char *line, |
| size_t line_len, |
| void *data) |
| { |
| struct write_data *write_data = (struct write_data *)data; |
| int result = 0; |
| |
| GIT_UNUSED(reader); |
| |
| /* If we were previously in the correct section (but aren't anymore) |
| * and haven't written our value (for a simple name/value set, not |
| * a multivar), then append it to the end of the section before writing |
| * the new one. |
| */ |
| if (write_data->in_section && !write_data->preg && write_data->value) |
| result = write_value(write_data); |
| |
| write_data->in_section = strcmp(current_section, write_data->section) == 0; |
| |
| /* |
| * If there were comments just before this section, dump them as well. |
| */ |
| if (!result) { |
| result = git_buf_put(write_data->buf, write_data->buffered_comment.ptr, write_data->buffered_comment.size); |
| git_buf_clear(&write_data->buffered_comment); |
| } |
| |
| if (!result) |
| result = write_line(write_data, line, line_len); |
| |
| return result; |
| } |
| |
| static int write_on_variable( |
| struct reader **reader, |
| const char *current_section, |
| char *var_name, |
| char *var_value, |
| const char *line, |
| size_t line_len, |
| void *data) |
| { |
| struct write_data *write_data = (struct write_data *)data; |
| bool has_matched = false; |
| int error; |
| |
| GIT_UNUSED(reader); |
| GIT_UNUSED(current_section); |
| |
| /* |
| * If there were comments just before this variable, let's dump them as well. |
| */ |
| if ((error = git_buf_put(write_data->buf, write_data->buffered_comment.ptr, write_data->buffered_comment.size)) < 0) |
| return error; |
| |
| git_buf_clear(&write_data->buffered_comment); |
| |
| /* See if we are to update this name/value pair; first examine name */ |
| if (write_data->in_section && |
| strcasecmp(write_data->name, var_name) == 0) |
| has_matched = true; |
| |
| /* If we have a regex to match the value, see if it matches */ |
| if (has_matched && write_data->preg != NULL) |
| has_matched = (regexec(write_data->preg, var_value, 0, NULL, 0) == 0); |
| |
| git__free(var_name); |
| git__free(var_value); |
| |
| /* If this isn't the name/value we're looking for, simply dump the |
| * existing data back out and continue on. |
| */ |
| if (!has_matched) |
| return write_line(write_data, line, line_len); |
| |
| write_data->preg_replaced = 1; |
| |
| /* If value is NULL, we are deleting this value; write nothing. */ |
| if (!write_data->value) |
| return 0; |
| |
| return write_value(write_data); |
| } |
| |
| static int write_on_comment(struct reader **reader, const char *line, size_t line_len, void *data) |
| { |
| struct write_data *write_data; |
| |
| GIT_UNUSED(reader); |
| |
| write_data = (struct write_data *)data; |
| return write_line_to(&write_data->buffered_comment, line, line_len); |
| } |
| |
| static int write_on_eof( |
| struct reader **reader, const char *current_section, void *data) |
| { |
| struct write_data *write_data = (struct write_data *)data; |
| int result = 0; |
| |
| GIT_UNUSED(reader); |
| |
| /* |
| * If we've buffered comments when reaching EOF, make sure to dump them. |
| */ |
| if ((result = git_buf_put(write_data->buf, write_data->buffered_comment.ptr, write_data->buffered_comment.size)) < 0) |
| return result; |
| |
| /* If we are at the EOF and have not written our value (again, for a |
| * simple name/value set, not a multivar) then we have never seen the |
| * section in question and should create a new section and write the |
| * value. |
| */ |
| if ((!write_data->preg || !write_data->preg_replaced) && write_data->value) { |
| /* write the section header unless we're already in it */ |
| if (!current_section || strcmp(current_section, write_data->section)) |
| result = write_section(write_data->buf, write_data->section); |
| |
| if (!result) |
| result = write_value(write_data); |
| } |
| |
| return result; |
| } |
| |
| /* |
| * This is pretty much the parsing, except we write out anything we don't have |
| */ |
| static int config_write(diskfile_backend *cfg, const char *key, const regex_t *preg, const char* value) |
| { |
| int result; |
| char *section, *name, *ldot; |
| git_filebuf file = GIT_FILEBUF_INIT; |
| git_buf buf = GIT_BUF_INIT; |
| struct reader *reader = git_array_get(cfg->readers, 0); |
| struct write_data write_data; |
| |
| if (cfg->locked) { |
| result = git_buf_puts(&reader->buffer, git_buf_cstr(&cfg->locked_content)); |
| } else { |
| /* Lock the file */ |
| if ((result = git_filebuf_open( |
| &file, cfg->file_path, GIT_FILEBUF_HASH_CONTENTS, GIT_CONFIG_FILE_MODE)) < 0) { |
| git_buf_free(&reader->buffer); |
| return result; |
| } |
| |
| /* We need to read in our own config file */ |
| result = git_futils_readbuffer(&reader->buffer, cfg->file_path); |
| } |
| |
| /* Initialise the reading position */ |
| if (result == GIT_ENOTFOUND) { |
| reader->read_ptr = NULL; |
| reader->eof = 1; |
| git_buf_clear(&reader->buffer); |
| } else if (result == 0) { |
| reader->read_ptr = reader->buffer.ptr; |
| reader->eof = 0; |
| } else { |
| git_filebuf_cleanup(&file); |
| return -1; /* OS error when reading the file */ |
| } |
| |
| ldot = strrchr(key, '.'); |
| name = ldot + 1; |
| section = git__strndup(key, ldot - key); |
| |
| write_data.buf = &buf; |
| git_buf_init(&write_data.buffered_comment, 0); |
| write_data.section = section; |
| write_data.in_section = 0; |
| write_data.preg_replaced = 0; |
| write_data.name = name; |
| write_data.preg = preg; |
| write_data.value = value; |
| |
| result = config_parse(reader, write_on_section, write_on_variable, write_on_comment, write_on_eof, &write_data); |
| git__free(section); |
| git_buf_free(&write_data.buffered_comment); |
| |
| if (result < 0) { |
| git_filebuf_cleanup(&file); |
| goto done; |
| } |
| |
| if (cfg->locked) { |
| size_t len = buf.asize; |
| /* Update our copy with the modified contents */ |
| git_buf_free(&cfg->locked_content); |
| git_buf_attach(&cfg->locked_content, git_buf_detach(&buf), len); |
| } else { |
| git_filebuf_write(&file, git_buf_cstr(&buf), git_buf_len(&buf)); |
| result = git_filebuf_commit(&file); |
| } |
| |
| done: |
| git_buf_free(&buf); |
| git_buf_free(&reader->buffer); |
| return result; |
| } |