mirror of
https://git.proxmox.com/git/libgit2
synced 2025-05-05 22:55:47 +00:00

Also free the subsection if we find too many quotes Signed-off-by: Carlos Martín Nieto <cmn@elego.de>
1002 lines
22 KiB
C
1002 lines
22 KiB
C
/*
|
|
* This file is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License, version 2,
|
|
* as published by the Free Software Foundation.
|
|
*
|
|
* In addition to the permissions in the GNU General Public License,
|
|
* the authors give you unlimited permission to link the compiled
|
|
* version of this file into combinations with other programs,
|
|
* and to distribute those combinations without any restriction
|
|
* coming from the use of this file. (The General Public License
|
|
* restrictions do apply in other respects; for example, they cover
|
|
* modification of the file, and distribution when not linked into
|
|
* a combined executable.)
|
|
*
|
|
* This file is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; see the file COPYING. If not, write to
|
|
* the Free Software Foundation, 51 Franklin Street, Fifth Floor,
|
|
* Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
#include "common.h"
|
|
#include "config.h"
|
|
#include "fileops.h"
|
|
#include "git2/config.h"
|
|
#include "git2/types.h"
|
|
|
|
#include <ctype.h>
|
|
|
|
typedef struct cvar_t {
|
|
struct cvar_t *next;
|
|
char *section;
|
|
char *name;
|
|
char *value;
|
|
} cvar_t;
|
|
|
|
typedef struct {
|
|
struct cvar_t *head;
|
|
struct cvar_t *tail;
|
|
} cvar_t_list;
|
|
|
|
#define CVAR_LIST_HEAD(list) ((list)->head)
|
|
|
|
#define CVAR_LIST_TAIL(list) ((list)->tail)
|
|
|
|
#define CVAR_LIST_NEXT(var) ((var)->next)
|
|
|
|
#define CVAR_LIST_EMPTY(list) ((list)->head == NULL)
|
|
|
|
#define CVAR_LIST_APPEND(list, var) do {\
|
|
if (CVAR_LIST_EMPTY(list)) {\
|
|
CVAR_LIST_HEAD(list) = CVAR_LIST_TAIL(list) = var;\
|
|
} else {\
|
|
CVAR_LIST_NEXT(CVAR_LIST_TAIL(list)) = var;\
|
|
CVAR_LIST_TAIL(list) = var;\
|
|
}\
|
|
} while(0)
|
|
|
|
#define CVAR_LIST_REMOVE_HEAD(list) do {\
|
|
CVAR_LIST_HEAD(list) = CVAR_LIST_NEXT(CVAR_LIST_HEAD(list));\
|
|
} while(0)
|
|
|
|
#define CVAR_LIST_REMOVE_AFTER(var) do {\
|
|
CVAR_LIST_NEXT(var) = CVAR_LIST_NEXT(CVAR_LIST_NEXT(var));\
|
|
} while(0)
|
|
|
|
#define CVAR_LIST_FOREACH(list, iter)\
|
|
for ((iter) = CVAR_LIST_HEAD(list);\
|
|
(iter) != NULL;\
|
|
(iter) = CVAR_LIST_NEXT(iter))
|
|
|
|
/*
|
|
* Inspired by the FreeBSD functions
|
|
*/
|
|
#define CVAR_LIST_FOREACH_SAFE(start, iter, tmp)\
|
|
for ((iter) = CVAR_LIST_HEAD(vars);\
|
|
(iter) && (((tmp) = CVAR_LIST_NEXT(iter) || 1));\
|
|
(iter) = (tmp))
|
|
|
|
typedef struct {
|
|
git_config_file parent;
|
|
|
|
cvar_t_list var_list;
|
|
|
|
struct {
|
|
gitfo_buf buffer;
|
|
char *read_ptr;
|
|
int line_number;
|
|
int eof;
|
|
} reader;
|
|
|
|
char *file_path;
|
|
} diskfile_backend;
|
|
|
|
static int config_parse(diskfile_backend *cfg_file);
|
|
static int parse_variable(diskfile_backend *cfg, char **var_name, char **var_value);
|
|
|
|
static void cvar_free(cvar_t *var)
|
|
{
|
|
if (var == NULL)
|
|
return;
|
|
|
|
free(var->section);
|
|
free(var->name);
|
|
free(var->value);
|
|
free(var);
|
|
}
|
|
|
|
static void cvar_list_free(cvar_t_list *list)
|
|
{
|
|
cvar_t *cur;
|
|
|
|
while (!CVAR_LIST_EMPTY(list)) {
|
|
cur = CVAR_LIST_HEAD(list);
|
|
CVAR_LIST_REMOVE_HEAD(list);
|
|
cvar_free(cur);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Compare two strings according to the git section-subsection
|
|
* rules. The order of the strings is important because local is
|
|
* assumed to have the internal format (only the section name and with
|
|
* case information) and input the normalized one (only dots, no case
|
|
* information).
|
|
*/
|
|
static int cvar_match_section(const char *local, const char *input)
|
|
{
|
|
char *first_dot, *last_dot;
|
|
char *local_sp = strchr(local, ' ');
|
|
int comparison_len;
|
|
|
|
/*
|
|
* If the local section name doesn't contain a space, then we can
|
|
* just do a case-insensitive compare.
|
|
*/
|
|
if (local_sp == NULL)
|
|
return !strncasecmp(local, input, strlen(local));
|
|
|
|
/*
|
|
* From here onwards, there is a space diving the section and the
|
|
* subsection. Anything before the space in local is
|
|
* case-insensitive.
|
|
*/
|
|
if (strncasecmp(local, input, local_sp - local))
|
|
return 0;
|
|
|
|
/*
|
|
* We compare starting from the first character after the
|
|
* quotation marks, which is two characters beyond the space. For
|
|
* the input, we start one character beyond the dot. If the names
|
|
* have different lengths, then we can fail early, as we know they
|
|
* can't be the same.
|
|
* The length is given by the length between the quotation marks.
|
|
*/
|
|
|
|
first_dot = strchr(input, '.');
|
|
last_dot = strrchr(input, '.');
|
|
comparison_len = strlen(local_sp + 2) - 1;
|
|
|
|
if (last_dot == first_dot || last_dot - first_dot - 1 != comparison_len)
|
|
return 0;
|
|
|
|
return !strncmp(local_sp + 2, first_dot + 1, comparison_len);
|
|
}
|
|
|
|
static int cvar_match_name(const cvar_t *var, const char *str)
|
|
{
|
|
const char *name_start;
|
|
|
|
if (!cvar_match_section(var->section, str)) {
|
|
return 0;
|
|
}
|
|
/* Early exit if the lengths are different */
|
|
name_start = strrchr(str, '.') + 1;
|
|
if (strlen(var->name) != strlen(name_start))
|
|
return 0;
|
|
|
|
return !strcasecmp(var->name, name_start);
|
|
}
|
|
|
|
static cvar_t *cvar_list_find(cvar_t_list *list, const char *name)
|
|
{
|
|
cvar_t *iter;
|
|
|
|
CVAR_LIST_FOREACH (list, iter) {
|
|
if (cvar_match_name(iter, name))
|
|
return iter;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static int cvar_normalize_name(cvar_t *var, char **output)
|
|
{
|
|
char *section_sp = strchr(var->section, ' ');
|
|
char *quote, *name;
|
|
int len, ret;
|
|
|
|
/*
|
|
* The final string is going to be at most one char longer than
|
|
* the input
|
|
*/
|
|
len = strlen(var->section) + strlen(var->name) + 1;
|
|
name = git__malloc(len + 1);
|
|
if (name == NULL)
|
|
return GIT_ENOMEM;
|
|
|
|
/* If there aren't any spaces in the section, it's easy */
|
|
if (section_sp == NULL) {
|
|
ret = snprintf(name, len + 1, "%s.%s", var->section, var->name);
|
|
if (ret < 0)
|
|
return git__throw(GIT_EOSERR, "Failed to normalize name. OS err: %s", strerror(errno));
|
|
|
|
*output = name;
|
|
return GIT_SUCCESS;
|
|
}
|
|
|
|
/*
|
|
* If there are spaces, we replace the space by a dot, move
|
|
* section name so it overwrites the first quotation mark and
|
|
* replace the last quotation mark by a dot. We then append the
|
|
* variable name.
|
|
*/
|
|
strcpy(name, var->section);
|
|
section_sp = strchr(name, ' ');
|
|
*section_sp = '.';
|
|
/* Remove first quote */
|
|
quote = strchr(name, '"');
|
|
memmove(quote, quote+1, strlen(quote+1));
|
|
/* Remove second quote */
|
|
quote = strchr(name, '"');
|
|
*quote = '.';
|
|
strcpy(quote+1, var->name);
|
|
|
|
*output = name;
|
|
return GIT_SUCCESS;
|
|
}
|
|
|
|
static int config_open(git_config_file *cfg)
|
|
{
|
|
int error;
|
|
diskfile_backend *b = (diskfile_backend *)cfg;
|
|
|
|
error = gitfo_read_file(&b->reader.buffer, b->file_path);
|
|
if(error < GIT_SUCCESS)
|
|
goto cleanup;
|
|
|
|
error = config_parse(b);
|
|
if (error < GIT_SUCCESS)
|
|
goto cleanup;
|
|
|
|
gitfo_free_buf(&b->reader.buffer);
|
|
|
|
return error;
|
|
|
|
cleanup:
|
|
cvar_list_free(&b->var_list);
|
|
gitfo_free_buf(&b->reader.buffer);
|
|
|
|
return git__rethrow(error, "Failed to open config");
|
|
}
|
|
|
|
static void backend_free(git_config_file *_backend)
|
|
{
|
|
diskfile_backend *backend = (diskfile_backend *)_backend;
|
|
|
|
if (backend == NULL)
|
|
return;
|
|
|
|
free(backend->file_path);
|
|
cvar_list_free(&backend->var_list);
|
|
|
|
free(backend);
|
|
}
|
|
|
|
static int file_foreach(git_config_file *backend, int (*fn)(const char *, void *), void *data)
|
|
{
|
|
int ret = GIT_SUCCESS;
|
|
cvar_t *var;
|
|
diskfile_backend *b = (diskfile_backend *)backend;
|
|
|
|
CVAR_LIST_FOREACH(&b->var_list, var) {
|
|
char *normalized = NULL;
|
|
|
|
ret = cvar_normalize_name(var, &normalized);
|
|
if (ret < GIT_SUCCESS)
|
|
return ret;
|
|
|
|
ret = fn(normalized, data);
|
|
free(normalized);
|
|
if (ret)
|
|
break;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int config_set(git_config_file *cfg, const char *name, const char *value)
|
|
{
|
|
cvar_t *var = NULL;
|
|
cvar_t *existing = NULL;
|
|
int error = GIT_SUCCESS;
|
|
const char *last_dot;
|
|
diskfile_backend *b = (diskfile_backend *)cfg;
|
|
|
|
/*
|
|
* If it already exists, we just need to update its value.
|
|
*/
|
|
existing = cvar_list_find(&b->var_list, name);
|
|
if (existing != NULL) {
|
|
char *tmp = value ? git__strdup(value) : NULL;
|
|
if (tmp == NULL && value != NULL)
|
|
return GIT_ENOMEM;
|
|
|
|
free(existing->value);
|
|
existing->value = tmp;
|
|
|
|
return GIT_SUCCESS;
|
|
}
|
|
|
|
/*
|
|
* Otherwise, create it and stick it at the end of the queue.
|
|
*/
|
|
|
|
last_dot = strrchr(name, '.');
|
|
if (last_dot == NULL) {
|
|
return git__throw(GIT_EINVALIDTYPE, "Variables without section aren't allowed");
|
|
}
|
|
|
|
var = git__malloc(sizeof(cvar_t));
|
|
if (var == NULL)
|
|
return GIT_ENOMEM;
|
|
|
|
memset(var, 0x0, sizeof(cvar_t));
|
|
|
|
var->section = git__strndup(name, last_dot - name);
|
|
if (var->section == NULL) {
|
|
error = GIT_ENOMEM;
|
|
goto out;
|
|
}
|
|
|
|
var->name = git__strdup(last_dot + 1);
|
|
if (var->name == NULL) {
|
|
error = GIT_ENOMEM;
|
|
goto out;
|
|
}
|
|
|
|
var->value = value ? git__strdup(value) : NULL;
|
|
if (var->value == NULL && value != NULL) {
|
|
error = GIT_ENOMEM;
|
|
goto out;
|
|
}
|
|
|
|
CVAR_LIST_APPEND(&b->var_list, var);
|
|
|
|
out:
|
|
if (error < GIT_SUCCESS)
|
|
cvar_free(var);
|
|
|
|
return error == GIT_SUCCESS ? GIT_SUCCESS : git__rethrow(error, "Failed to set config value");
|
|
}
|
|
|
|
/*
|
|
* Internal function that actually gets the value in string form
|
|
*/
|
|
static int config_get(git_config_file *cfg, const char *name, const char **out)
|
|
{
|
|
cvar_t *var;
|
|
int error = GIT_SUCCESS;
|
|
diskfile_backend *b = (diskfile_backend *)cfg;
|
|
|
|
var = cvar_list_find(&b->var_list, name);
|
|
|
|
if (var == NULL)
|
|
return git__throw(GIT_ENOTFOUND, "Variable '%s' not found", name);
|
|
|
|
*out = var->value;
|
|
|
|
return error == GIT_SUCCESS ? GIT_SUCCESS : git__rethrow(error, "Failed to get config value for %s", name);
|
|
}
|
|
|
|
int git_config_file__ondisk(git_config_file **out, const char *path)
|
|
{
|
|
diskfile_backend *backend;
|
|
|
|
backend = git__malloc(sizeof(diskfile_backend));
|
|
if (backend == NULL)
|
|
return GIT_ENOMEM;
|
|
|
|
memset(backend, 0x0, sizeof(diskfile_backend));
|
|
|
|
backend->file_path = git__strdup(path);
|
|
if (backend->file_path == NULL) {
|
|
free(backend);
|
|
return GIT_ENOMEM;
|
|
}
|
|
|
|
backend->parent.open = config_open;
|
|
backend->parent.get = config_get;
|
|
backend->parent.set = config_set;
|
|
backend->parent.foreach = file_foreach;
|
|
backend->parent.free = backend_free;
|
|
|
|
*out = (git_config_file *)backend;
|
|
|
|
return GIT_SUCCESS;
|
|
}
|
|
|
|
static int cfg_getchar_raw(diskfile_backend *cfg)
|
|
{
|
|
int c;
|
|
|
|
c = *cfg->reader.read_ptr++;
|
|
|
|
/*
|
|
Win 32 line breaks: if we find a \r\n sequence,
|
|
return only the \n as a newline
|
|
*/
|
|
if (c == '\r' && *cfg->reader.read_ptr == '\n') {
|
|
cfg->reader.read_ptr++;
|
|
c = '\n';
|
|
}
|
|
|
|
if (c == '\n')
|
|
cfg->reader.line_number++;
|
|
|
|
if (c == 0) {
|
|
cfg->reader.eof = 1;
|
|
c = '\n';
|
|
}
|
|
|
|
return c;
|
|
}
|
|
|
|
#define SKIP_WHITESPACE (1 << 1)
|
|
#define SKIP_COMMENTS (1 << 2)
|
|
|
|
static int cfg_getchar(diskfile_backend *cfg_file, int flags)
|
|
{
|
|
const int skip_whitespace = (flags & SKIP_WHITESPACE);
|
|
const int skip_comments = (flags & SKIP_COMMENTS);
|
|
int c;
|
|
|
|
assert(cfg_file->reader.read_ptr);
|
|
|
|
do c = cfg_getchar_raw(cfg_file);
|
|
while (skip_whitespace && isspace(c));
|
|
|
|
if (skip_comments && (c == '#' || c == ';')) {
|
|
do c = cfg_getchar_raw(cfg_file);
|
|
while (c != '\n');
|
|
}
|
|
|
|
return c;
|
|
}
|
|
|
|
/*
|
|
* Read the next char, but don't move the reading pointer.
|
|
*/
|
|
static int cfg_peek(diskfile_backend *cfg, int flags)
|
|
{
|
|
void *old_read_ptr;
|
|
int old_lineno, old_eof;
|
|
int ret;
|
|
|
|
assert(cfg->reader.read_ptr);
|
|
|
|
old_read_ptr = cfg->reader.read_ptr;
|
|
old_lineno = cfg->reader.line_number;
|
|
old_eof = cfg->reader.eof;
|
|
|
|
ret = cfg_getchar(cfg, flags);
|
|
|
|
cfg->reader.read_ptr = old_read_ptr;
|
|
cfg->reader.line_number = old_lineno;
|
|
cfg->reader.eof = old_eof;
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Read and consume a line, returning it in newly-allocated memory.
|
|
*/
|
|
static char *cfg_readline(diskfile_backend *cfg)
|
|
{
|
|
char *line = NULL;
|
|
char *line_src, *line_end;
|
|
int line_len;
|
|
|
|
line_src = cfg->reader.read_ptr;
|
|
|
|
/* Skip empty empty lines */
|
|
while (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;
|
|
|
|
line = git__malloc(line_len + 1);
|
|
if (line == NULL)
|
|
return NULL;
|
|
|
|
memcpy(line, line_src, line_len);
|
|
|
|
line[line_len] = '\0';
|
|
|
|
while (--line_len >= 0 && isspace(line[line_len]))
|
|
line[line_len] = '\0';
|
|
|
|
if (*line_end == '\n')
|
|
line_end++;
|
|
|
|
if (*line_end == '\0')
|
|
cfg->reader.eof = 1;
|
|
|
|
cfg->reader.line_number++;
|
|
cfg->reader.read_ptr = line_end;
|
|
|
|
return line;
|
|
}
|
|
|
|
/*
|
|
* Consume a line, without storing it anywhere
|
|
*/
|
|
void cfg_consume_line(diskfile_backend *cfg)
|
|
{
|
|
char *line_start, *line_end;
|
|
|
|
line_start = cfg->reader.read_ptr;
|
|
line_end = strchr(line_start, '\n');
|
|
/* No newline at EOF */
|
|
if(line_end == NULL){
|
|
line_end = strchr(line_start, '\0');
|
|
}
|
|
|
|
if (*line_end == '\n')
|
|
line_end++;
|
|
|
|
if (*line_end == '\0')
|
|
cfg->reader.eof = 1;
|
|
|
|
cfg->reader.line_number++;
|
|
cfg->reader.read_ptr = line_end;
|
|
}
|
|
|
|
GIT_INLINE(int) config_keychar(int c)
|
|
{
|
|
return isalnum(c) || c == '-';
|
|
}
|
|
|
|
static int parse_section_header_ext(const char *line, const char *base_name, char **section_name)
|
|
{
|
|
int buf_len, total_len, pos, rpos;
|
|
int c, ret;
|
|
char *subsection, *first_quote, *last_quote;
|
|
int error = GIT_SUCCESS;
|
|
int quote_marks;
|
|
/*
|
|
* base_name is what came before the space. We should be at the
|
|
* first quotation mark, except for now, line isn't being kept in
|
|
* sync so we only really use it to calculate the length.
|
|
*/
|
|
|
|
first_quote = strchr(line, '"');
|
|
last_quote = strrchr(line, '"');
|
|
|
|
if (last_quote - first_quote == 0)
|
|
return git__throw(GIT_EOBJCORRUPTED, "Failed to parse ext header. There is no final quotation mark");
|
|
|
|
buf_len = last_quote - first_quote + 2;
|
|
|
|
subsection = git__malloc(buf_len + 2);
|
|
if (subsection == NULL)
|
|
return GIT_ENOMEM;
|
|
|
|
pos = 0;
|
|
rpos = 0;
|
|
quote_marks = 0;
|
|
|
|
line = first_quote;
|
|
c = line[rpos++];
|
|
|
|
/*
|
|
* At the end of each iteration, whatever is stored in c will be
|
|
* added to the string. In case of error, jump to out
|
|
*/
|
|
do {
|
|
switch (c) {
|
|
case '"':
|
|
if (quote_marks++ >= 2) {
|
|
error = git__throw(GIT_EOBJCORRUPTED, "Failed to parse ext header. Too many quotes");
|
|
goto out;
|
|
}
|
|
break;
|
|
case '\\':
|
|
c = line[rpos++];
|
|
switch (c) {
|
|
case '"':
|
|
case '\\':
|
|
break;
|
|
default:
|
|
error = git__throw(GIT_EOBJCORRUPTED, "Failed to parse ext header. Unsupported escape char \\%c", c);
|
|
goto out;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
subsection[pos++] = (char) c;
|
|
} while ((c = line[rpos++]) != ']');
|
|
|
|
subsection[pos] = '\0';
|
|
|
|
total_len = strlen(base_name) + strlen(subsection) + 2;
|
|
*section_name = git__malloc(total_len);
|
|
if (*section_name == NULL) {
|
|
error = GIT_ENOMEM;
|
|
goto out;
|
|
}
|
|
|
|
ret = snprintf(*section_name, total_len, "%s %s", base_name, subsection);
|
|
if (ret >= total_len) {
|
|
/* If this fails, we've checked the length wrong */
|
|
error = git__throw(GIT_ERROR, "Failed to parse ext header. Wrong total length calculation");
|
|
goto out;
|
|
} else if (ret < 0) {
|
|
error = git__throw(GIT_EOSERR, "Failed to parse ext header. OS error: %s", strerror(errno));
|
|
goto out;
|
|
}
|
|
|
|
git__strntolower(*section_name, strchr(*section_name, ' ') - *section_name);
|
|
|
|
out:
|
|
free(subsection);
|
|
|
|
return error;
|
|
}
|
|
|
|
static int parse_section_header(diskfile_backend *cfg, char **section_out)
|
|
{
|
|
char *name, *name_end;
|
|
int name_length, c, pos;
|
|
int error = GIT_SUCCESS;
|
|
char *line;
|
|
|
|
line = cfg_readline(cfg);
|
|
if (line == NULL)
|
|
return GIT_ENOMEM;
|
|
|
|
/* find the end of the variable's name */
|
|
name_end = strchr(line, ']');
|
|
if (name_end == NULL)
|
|
return git__throw(GIT_EOBJCORRUPTED, "Failed to parse header. Can't find header name end");
|
|
|
|
name = (char *)git__malloc((size_t)(name_end - line) + 1);
|
|
if (name == NULL)
|
|
return GIT_ENOMEM;
|
|
|
|
name_length = 0;
|
|
pos = 0;
|
|
|
|
/* Make sure we were given a section header */
|
|
c = line[pos++];
|
|
if (c != '[') {
|
|
error = git__throw(GIT_ERROR, "Failed to parse header. Didn't get section header. This is a bug");
|
|
goto error;
|
|
}
|
|
|
|
c = line[pos++];
|
|
|
|
do {
|
|
if (cfg->reader.eof){
|
|
error = git__throw(GIT_EOBJCORRUPTED, "Failed to parse header. Config file ended unexpectedly");
|
|
goto error;
|
|
}
|
|
|
|
if (isspace(c)){
|
|
name[name_length] = '\0';
|
|
error = parse_section_header_ext(line, name, section_out);
|
|
free(line);
|
|
free(name);
|
|
return error == GIT_SUCCESS ? GIT_SUCCESS : git__rethrow(error, "Failed to parse header");
|
|
}
|
|
|
|
if (!config_keychar(c) && c != '.') {
|
|
error = git__throw(GIT_EOBJCORRUPTED, "Failed to parse header. Wrong format on header");
|
|
goto error;
|
|
}
|
|
|
|
name[name_length++] = (char) tolower(c);
|
|
|
|
} while ((c = line[pos++]) != ']');
|
|
|
|
name[name_length] = 0;
|
|
free(line);
|
|
git__strtolower(name);
|
|
*section_out = name;
|
|
return GIT_SUCCESS;
|
|
|
|
error:
|
|
free(line);
|
|
free(name);
|
|
return error;
|
|
}
|
|
|
|
static int skip_bom(diskfile_backend *cfg)
|
|
{
|
|
static const char *utf8_bom = "\xef\xbb\xbf";
|
|
|
|
if (memcmp(cfg->reader.read_ptr, utf8_bom, sizeof(utf8_bom)) == 0)
|
|
cfg->reader.read_ptr += sizeof(utf8_bom);
|
|
|
|
/* TODO: the reference implementation does pretty stupid
|
|
shit with the BoM
|
|
*/
|
|
|
|
return GIT_SUCCESS;
|
|
}
|
|
|
|
/*
|
|
(* basic types *)
|
|
digit = "0".."9"
|
|
integer = digit { digit }
|
|
alphabet = "a".."z" + "A" .. "Z"
|
|
|
|
section_char = alphabet | "." | "-"
|
|
extension_char = (* any character except newline *)
|
|
any_char = (* any character *)
|
|
variable_char = "alphabet" | "-"
|
|
|
|
|
|
(* actual grammar *)
|
|
config = { section }
|
|
|
|
section = header { definition }
|
|
|
|
header = "[" section [subsection | subsection_ext] "]"
|
|
|
|
subsection = "." section
|
|
subsection_ext = "\"" extension "\""
|
|
|
|
section = section_char { section_char }
|
|
extension = extension_char { extension_char }
|
|
|
|
definition = variable_name ["=" variable_value] "\n"
|
|
|
|
variable_name = variable_char { variable_char }
|
|
variable_value = string | boolean | integer
|
|
|
|
string = quoted_string | plain_string
|
|
quoted_string = "\"" plain_string "\""
|
|
plain_string = { any_char }
|
|
|
|
boolean = boolean_true | boolean_false
|
|
boolean_true = "yes" | "1" | "true" | "on"
|
|
boolean_false = "no" | "0" | "false" | "off"
|
|
*/
|
|
|
|
static void strip_comments(char *line)
|
|
{
|
|
int quote_count = 0;
|
|
char *ptr;
|
|
|
|
for (ptr = line; *ptr; ++ptr) {
|
|
if (ptr[0] == '"' && ptr > line && ptr[-1] != '\\')
|
|
quote_count++;
|
|
|
|
if ((ptr[0] == ';' || ptr[0] == '#') && (quote_count % 2) == 0) {
|
|
ptr[0] = '\0';
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (isspace(ptr[-1])) {
|
|
/* TODO skip whitespace */
|
|
}
|
|
}
|
|
|
|
static int config_parse(diskfile_backend *cfg_file)
|
|
{
|
|
int error = GIT_SUCCESS, c;
|
|
char *current_section = NULL;
|
|
char *var_name;
|
|
char *var_value;
|
|
cvar_t *var;
|
|
|
|
/* Initialize the reading position */
|
|
cfg_file->reader.read_ptr = cfg_file->reader.buffer.data;
|
|
cfg_file->reader.eof = 0;
|
|
|
|
skip_bom(cfg_file);
|
|
|
|
while (error == GIT_SUCCESS && !cfg_file->reader.eof) {
|
|
|
|
c = cfg_peek(cfg_file, SKIP_WHITESPACE);
|
|
|
|
switch (c) {
|
|
case '\0': /* We've arrived at the end of the file */
|
|
break;
|
|
|
|
case '[': /* section header, new section begins */
|
|
free(current_section);
|
|
current_section = NULL;
|
|
error = parse_section_header(cfg_file, ¤t_section);
|
|
break;
|
|
|
|
case ';':
|
|
case '#':
|
|
cfg_consume_line(cfg_file);
|
|
break;
|
|
|
|
default: /* assume variable declaration */
|
|
error = parse_variable(cfg_file, &var_name, &var_value);
|
|
|
|
if (error < GIT_SUCCESS)
|
|
break;
|
|
|
|
var = malloc(sizeof(cvar_t));
|
|
if (var == NULL) {
|
|
error = GIT_ENOMEM;
|
|
break;
|
|
}
|
|
|
|
memset(var, 0x0, sizeof(cvar_t));
|
|
|
|
var->section = git__strdup(current_section);
|
|
if (var->section == NULL) {
|
|
error = GIT_ENOMEM;
|
|
free(var);
|
|
break;
|
|
}
|
|
|
|
var->name = var_name;
|
|
var->value = var_value;
|
|
git__strtolower(var->name);
|
|
|
|
CVAR_LIST_APPEND(&cfg_file->var_list, var);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
free(current_section);
|
|
|
|
return error == GIT_SUCCESS ? GIT_SUCCESS : git__rethrow(error, "Failed to parse config");
|
|
}
|
|
|
|
static int is_multiline_var(const char *str)
|
|
{
|
|
char *end = strrchr(str, '\0') - 1;
|
|
|
|
while (isspace(*end))
|
|
--end;
|
|
|
|
return *end == '\\';
|
|
}
|
|
|
|
static int parse_multiline_variable(diskfile_backend *cfg, const char *first, char **out)
|
|
{
|
|
char *line = NULL, *end;
|
|
int error = GIT_SUCCESS, len, ret;
|
|
char *buf;
|
|
|
|
/* Check that the next line exists */
|
|
line = cfg_readline(cfg);
|
|
if (line == NULL)
|
|
return GIT_ENOMEM;
|
|
|
|
/* We've reached the end of the file, there is input missing */
|
|
if (line[0] == '\0') {
|
|
error = git__throw(GIT_EOBJCORRUPTED, "Failed to parse multiline var. File ended unexpectedly");
|
|
goto out;
|
|
}
|
|
|
|
strip_comments(line);
|
|
|
|
/* If it was just a comment, pretend it didn't exist */
|
|
if (line[0] == '\0') {
|
|
error = parse_multiline_variable(cfg, first, out);
|
|
goto out;
|
|
}
|
|
|
|
/* Find the continuation character '\' and strip the whitespace */
|
|
end = strrchr(first, '\\');
|
|
while (isspace(end[-1]))
|
|
--end;
|
|
|
|
*end = '\0'; /* Terminate the string here */
|
|
|
|
len = strlen(first) + strlen(line) + 2;
|
|
buf = git__malloc(len);
|
|
if (buf == NULL) {
|
|
error = GIT_ENOMEM;
|
|
goto out;
|
|
}
|
|
|
|
ret = snprintf(buf, len, "%s %s", first, line);
|
|
if (ret < 0) {
|
|
error = git__throw(GIT_EOSERR, "Failed to parse multiline var. Failed to put together two lines. OS err: %s", strerror(errno));
|
|
free(buf);
|
|
goto out;
|
|
}
|
|
|
|
/*
|
|
* If we need to continue reading the next line, pretend
|
|
* everything we've read up to now was in one line and call
|
|
* ourselves.
|
|
*/
|
|
if (is_multiline_var(buf)) {
|
|
char *final_val;
|
|
error = parse_multiline_variable(cfg, buf, &final_val);
|
|
free(buf);
|
|
buf = final_val;
|
|
}
|
|
|
|
*out = buf;
|
|
|
|
out:
|
|
free(line);
|
|
return error;
|
|
}
|
|
|
|
static int parse_variable(diskfile_backend *cfg, char **var_name, char **var_value)
|
|
{
|
|
char *tmp;
|
|
int error = GIT_SUCCESS;
|
|
const char *var_end = NULL;
|
|
const char *value_start = NULL;
|
|
char *line;
|
|
|
|
line = cfg_readline(cfg);
|
|
if (line == NULL)
|
|
return GIT_ENOMEM;
|
|
|
|
strip_comments(line);
|
|
|
|
var_end = strchr(line, '=');
|
|
|
|
if (var_end == NULL)
|
|
var_end = strchr(line, '\0');
|
|
else
|
|
value_start = var_end + 1;
|
|
|
|
if (isspace(var_end[-1])) {
|
|
do var_end--;
|
|
while (isspace(var_end[0]));
|
|
}
|
|
|
|
tmp = git__strndup(line, var_end - line + 1);
|
|
if (tmp == NULL) {
|
|
error = GIT_ENOMEM;
|
|
goto out;
|
|
}
|
|
|
|
*var_name = tmp;
|
|
|
|
/*
|
|
* Now, let's try to parse the value
|
|
*/
|
|
if (value_start != NULL) {
|
|
|
|
while (isspace(value_start[0]))
|
|
value_start++;
|
|
|
|
if (value_start[0] == '\0')
|
|
goto out;
|
|
|
|
if (is_multiline_var(value_start)) {
|
|
error = parse_multiline_variable(cfg, value_start, var_value);
|
|
if (error < GIT_SUCCESS)
|
|
free(*var_name);
|
|
goto out;
|
|
}
|
|
|
|
tmp = strdup(value_start);
|
|
if (tmp == NULL) {
|
|
free(*var_name);
|
|
error = GIT_ENOMEM;
|
|
goto out;
|
|
}
|
|
|
|
*var_value = tmp;
|
|
} else {
|
|
/* If there is no value, boolean true is assumed */
|
|
*var_value = NULL;
|
|
}
|
|
|
|
out:
|
|
free(line);
|
|
return error;
|
|
}
|