/*
 * 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
 */

typedef struct {
	git_config_iterator parent;
	git_config_iterator *current;
	const git_config *cfg;
	regex_t regex;
	int has_regex;
	size_t i;
} all_iter;

static int find_next_backend(size_t *out, const git_config *cfg, size_t i)
{
	file_internal *internal;

	for (; i > 0; --i) {
		internal = git_vector_get(&cfg->files, i - 1);
		if (!internal || !internal->file)
			continue;

		*out = i;
		return 0;
	}

	return -1;
}

static int all_iter_next(git_config_entry **entry, git_config_iterator *_iter)
{
	all_iter *iter = (all_iter *) _iter;
	file_internal *internal;
	git_config_backend *backend;
	size_t i;
	int error = 0;

	if (iter->current != NULL &&
	    (error = iter->current->next(entry, iter->current)) == 0) {
		return 0;
	}

	if (error < 0 && error != GIT_ITEROVER)
		return error;

	do {
		if (find_next_backend(&i, iter->cfg, iter->i) < 0)
			return GIT_ITEROVER;

		internal = git_vector_get(&iter->cfg->files, i - 1);
		backend = internal->file;
		iter->i = i - 1;

		if (iter->current)
			iter->current->free(iter->current);

		iter->current = NULL;
		error = backend->iterator(&iter->current, backend);
		if (error == GIT_ENOTFOUND)
			continue;

		if (error < 0)
			return error;

		error = iter->current->next(entry, iter->current);
		/* If this backend is empty, then keep going */
		if (error == GIT_ITEROVER)
			continue;

		return error;

	} while(1);

	return GIT_ITEROVER;
}

static int all_iter_glob_next(git_config_entry **entry, git_config_iterator *_iter)
{
	int error;
	all_iter *iter = (all_iter *) _iter;

	/*
	 * We use the "normal" function to grab the next one across
	 * backends and then apply the regex
	 */
	while ((error = all_iter_next(entry, _iter)) == 0) {
		/* skip non-matching keys if regexp was provided */
		if (regexec(&iter->regex, (*entry)->name, 0, NULL, 0) != 0)
			continue;

		/* and simply return if we like the entry's name */
		return 0;
	}

	return error;
}

static void all_iter_free(git_config_iterator *_iter)
{
	all_iter *iter = (all_iter *) _iter;

	if (iter->current)
		iter->current->free(iter->current);

	git__free(iter);
}

static void all_iter_glob_free(git_config_iterator *_iter)
{
	all_iter *iter = (all_iter *) _iter;

	regfree(&iter->regex);
	all_iter_free(_iter);
}

int git_config_iterator_new(git_config_iterator **out, const git_config *cfg)
{
	all_iter *iter;

	iter = git__calloc(1, sizeof(all_iter));
	GITERR_CHECK_ALLOC(iter);

	iter->parent.free = all_iter_free;
	iter->parent.next = all_iter_next;

	iter->i = cfg->files.length;
	iter->cfg = cfg;

	*out = (git_config_iterator *) iter;

	return 0;
}

int git_config_iterator_glob_new(git_config_iterator **out, const git_config *cfg, const char *regexp)
{
	all_iter *iter;
	int result;

	if (regexp == NULL)
		return git_config_iterator_new(out, cfg);

	iter = git__calloc(1, sizeof(all_iter));
	GITERR_CHECK_ALLOC(iter);

	if ((result = regcomp(&iter->regex, regexp, REG_EXTENDED)) < 0) {
		giterr_set_regex(&iter->regex, result);
		regfree(&iter->regex);
		return -1;
	}

	iter->parent.next = all_iter_glob_next;
	iter->parent.free = all_iter_glob_free;
	iter->i = cfg->files.length;
	iter->cfg = cfg;

	*out = (git_config_iterator *) iter;

	return 0;
}

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_backend_foreach_match(
	git_config_backend *backend,
	const char *regexp,
	int (*fn)(const git_config_entry *, void *),
	void *data)
{
	git_config_entry *entry;
	git_config_iterator* iter;
	regex_t regex;
	int result = 0;

	if (regexp != NULL) {
		if ((result = regcomp(&regex, regexp, REG_EXTENDED)) < 0) {
			giterr_set_regex(&regex, result);
			regfree(&regex);
			return -1;
		}
	}

	if ((result = backend->iterator(&iter, backend)) < 0) {
		iter = NULL;
		return -1;
	}

	while(!(iter->next(&entry, iter) < 0)) {
		/* skip non-matching keys if regexp was provided */
		if (regexp && regexec(&regex, entry->name, 0, NULL, 0) != 0)
			continue;

		/* abort iterator on non-zero return value */
		if (fn(entry, data)) {
			giterr_clear();
			result = GIT_EUSER;
			goto cleanup;
		}
	}

cleanup:
	if (regexp != NULL)
		regfree(&regex);

	iter->free(iter);

	return result;
}

int git_config_foreach_match(
	const git_config *cfg,
	const char *regexp,
	git_config_foreach_cb cb,
	void *payload)
{
	int error;
	git_config_iterator *iter;
	git_config_entry *entry;

	if ((error = git_config_iterator_glob_new(&iter, cfg, regexp)) < 0)
		return error;

	while ((error = git_config_next(&entry, iter)) == 0) {
		if(cb(entry, payload)) {
			giterr_clear();
			error = GIT_EUSER;
			break;
		}
	}

	git_config_iterator_free(iter);

	if (error == GIT_ITEROVER)
		error = 0;

	return error;
}

/**************
 * 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_foreach(
	const git_config *cfg, const char *name, const char *regexp,
	git_config_foreach_cb cb, void *payload)
{
	int err, found;
	git_config_iterator *iter;
	git_config_entry *entry;

	if ((err = git_config_multivar_iterator_new(&iter, cfg, name, regexp)) < 0)
		return err;

	found = 0;
	while ((err = iter->next(&entry, iter)) == 0) {
		found = 1;
		if(cb(entry, payload)) {
			iter->free(iter);
			return GIT_EUSER;
		}
	}

	iter->free(iter);
	if (err == GIT_ITEROVER)
		err = 0;

	if (found == 0 && err == 0)
		err = config_error_notfound(name);

	return err;
}

typedef struct {
	git_config_iterator parent;
	git_config_iterator *iter;
	char *name;
	regex_t regex;
	int have_regex;
} multivar_iter;

static int multivar_iter_next(git_config_entry **entry, git_config_iterator *_iter)
{
	multivar_iter *iter = (multivar_iter *) _iter;
	int error = 0;

	while ((error = iter->iter->next(entry, iter->iter)) == 0) {
		if (git__strcmp(iter->name, (*entry)->name))
			continue;

		if (!iter->have_regex)
			return 0;

		if (regexec(&iter->regex, (*entry)->value, 0, NULL, 0) == 0)
			return 0;
	}

	return error;
}

void multivar_iter_free(git_config_iterator *_iter)
{
	multivar_iter *iter = (multivar_iter *) _iter;

	iter->iter->free(iter->iter);

	git__free(iter->name);
	regfree(&iter->regex);
	git__free(iter);
}

int git_config_multivar_iterator_new(git_config_iterator **out, const git_config *cfg, const char *name, const char *regexp)
{
	multivar_iter *iter = NULL;
	git_config_iterator *inner = NULL;
	int error;

	if ((error = git_config_iterator_new(&inner, cfg)) < 0)
		return error;

	iter = git__calloc(1, sizeof(multivar_iter));
	GITERR_CHECK_ALLOC(iter);

	if ((error = git_config__normalize_name(name, &iter->name)) < 0)
		goto on_error;

	if (regexp != NULL) {
		error = regcomp(&iter->regex, regexp, REG_EXTENDED);
		if (error < 0) {
			giterr_set_regex(&iter->regex, error);
			error = -1;
			regfree(&iter->regex);
			goto on_error;
		}

		iter->have_regex = 1;
	}

	iter->iter = inner;
	iter->parent.free = multivar_iter_free;
	iter->parent.next = multivar_iter_next;

	*out = (git_config_iterator *) iter;

	return 0;

on_error:

	inner->free(inner);
	git__free(iter);
	return error;
}

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);
}

int git_config_next(git_config_entry **entry, git_config_iterator *iter)
{
	return iter->next(entry, iter);
}

void git_config_iterator_free(git_config_iterator *iter)
{
	iter->free(iter);
}

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;
}

/* Take something the user gave us and make it nice for our hash function */
int git_config__normalize_name(const char *in, char **out)
{
	char *name, *fdot, *ldot;

	assert(in && out);

	name = git__strdup(in);
	GITERR_CHECK_ALLOC(name);

	fdot = strchr(name, '.');
	ldot = strrchr(name, '.');

	if (fdot == NULL || fdot == name || ldot == NULL || !ldot[1])
		goto invalid;

	/* Validate and downcase up to first dot and after last dot */
	if (git_config_file_normalize_section(name, fdot) < 0 ||
		git_config_file_normalize_section(ldot + 1, NULL) < 0)
		goto invalid;

	/* If there is a middle range, make sure it doesn't have newlines */
	while (fdot < ldot)
		if (*fdot++ == '\n')
			goto invalid;

	*out = name;
	return 0;

invalid:
	git__free(name);
	giterr_set(GITERR_CONFIG, "Invalid config item name '%s'", in);
	return GIT_EINVALIDSPEC;
}

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;
}
