config_file.c 33.5 KB
Newer Older
1
/*
Edward Thomson committed
2
 * Copyright (C) the libgit2 contributors. All rights reserved.
3
 *
Vicent Marti committed
4 5
 * This file is part of libgit2, distributed under the GNU GPL v2 with
 * a Linking Exception. For full terms see the included COPYING file.
6 7 8 9 10
 */

#include "common.h"
#include "config.h"
#include "fileops.h"
11
#include "filebuf.h"
12
#include "buffer.h"
13
#include "buf_text.h"
14
#include "git2/config.h"
15
#include "git2/types.h"
16
#include "strmap.h"
17

18
#include <ctype.h>
19 20
#include <sys/types.h>
#include <regex.h>
21

22
GIT__USE_STRMAP;
23

24 25
typedef struct cvar_t {
	struct cvar_t *next;
26
	git_config_entry *entry;
27
} cvar_t;
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67

#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 {
Ben Straub committed
68
	git_config_backend parent;
69

70
	git_strmap *values;
71 72

	struct {
73
		git_buf buffer;
74 75 76 77 78
		char *read_ptr;
		int line_number;
		int eof;
	} reader;

79 80 81 82 83
	char  *file_path;
	time_t file_mtime;
	size_t file_size;

	unsigned int level;
84
} diskfile_backend;
85

86
static int config_parse(diskfile_backend *cfg_file, unsigned int level);
87
static int parse_variable(diskfile_backend *cfg, char **var_name, char **var_value);
88
static int config_write(diskfile_backend *cfg, const char *key, const regex_t *preg, const char *value);
89
static char *escape_value(const char *ptr);
90

91 92 93 94 95 96
static void set_parse_error(diskfile_backend *backend, int col, const char *error_str)
{
	giterr_set(GITERR_CONFIG, "Failed to parse config file: %s (in %s:%d, column %d)",
		error_str, backend->file_path, backend->reader.line_number, col);
}

97
static void cvar_free(cvar_t *var)
98 99 100 101
{
	if (var == NULL)
		return;

102 103 104
	git__free((char*)var->entry->name);
	git__free((char *)var->entry->value);
	git__free(var->entry);
105
	git__free(var);
106 107
}

108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
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 = tolower(*scan);
		else if (*scan != '-' || scan == start)
			return GIT_EINVALIDSPEC;
	}

	if (scan == start)
		return GIT_EINVALIDSPEC;

	return 0;
}

131 132
/* Take something the user gave us and make it nice for our hash function */
static int normalize_name(const char *in, char **out)
133
{
134
	char *name, *fdot, *ldot;
135

136
	assert(in && out);
137

138
	name = git__strdup(in);
139
	GITERR_CHECK_ALLOC(name);
140

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

144 145 146 147 148 149 150
	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;
151

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

	*out = name;
158
	return 0;
159 160 161 162 163

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

166
static void free_vars(git_strmap *values)
167
{
168
	cvar_t *var = NULL;
169

170 171
	if (values == NULL)
		return;
172

173
	git_strmap_foreach_value(values, var,
174 175 176 177 178
		while (var != NULL) {
			cvar_t *next = CVAR_LIST_NEXT(var);
			cvar_free(var);
			var = next;
		});
179

180
	git_strmap_free(values);
181 182
}

Ben Straub committed
183
static int config_open(git_config_backend *cfg, unsigned int level)
184
{
185
	int res;
186
	diskfile_backend *b = (diskfile_backend *)cfg;
187

188 189
	b->level = level;

190
	b->values = git_strmap_alloc();
191
	GITERR_CHECK_ALLOC(b->values);
192

193
	git_buf_init(&b->reader.buffer, 0);
194 195
	res = git_futils_readbuffer_updated(
		&b->reader.buffer, b->file_path, &b->file_mtime, &b->file_size, NULL);
196

197
	/* It's fine if the file doesn't exist */
198 199 200
	if (res == GIT_ENOTFOUND)
		return 0;

201
	if (res < 0 || (res = config_parse(b, level)) < 0) {
202 203 204
		free_vars(b->values);
		b->values = NULL;
	}
205

206
	git_buf_free(&b->reader.buffer);
207 208 209
	return res;
}

Ben Straub committed
210
static int config_refresh(git_config_backend *cfg)
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
{
	int res, updated = 0;
	diskfile_backend *b = (diskfile_backend *)cfg;
	git_strmap *old_values;

	res = git_futils_readbuffer_updated(
		&b->reader.buffer, b->file_path, &b->file_mtime, &b->file_size, &updated);
	if (res < 0 || !updated)
		return (res == GIT_ENOTFOUND) ? 0 : res;

	/* need to reload - store old values and prep for reload */
	old_values = b->values;
	b->values = git_strmap_alloc();
	GITERR_CHECK_ALLOC(b->values);

	if ((res = config_parse(b, b->level)) < 0) {
		free_vars(b->values);
		b->values = old_values;
	} else {
		free_vars(old_values);
	}

	git_buf_free(&b->reader.buffer);
	return res;
235 236
}

Ben Straub committed
237
static void backend_free(git_config_backend *_backend)
238
{
239
	diskfile_backend *backend = (diskfile_backend *)_backend;
240 241 242 243

	if (backend == NULL)
		return;

244
	git__free(backend->file_path);
245
	free_vars(backend->values);
246
	git__free(backend);
247 248
}

249
static int file_foreach(
Ben Straub committed
250
	git_config_backend *backend,
251
	const char *regexp,
252
	int (*fn)(const git_config_entry *, void *),
253
	void *data)
254
{
255
	diskfile_backend *b = (diskfile_backend *)backend;
256
	cvar_t *var, *next_var;
257
	const char *key;
258 259
	regex_t regex;
	int result = 0;
260

261 262
	if (!b->values)
		return 0;
263

264 265 266 267 268 269 270
	if (regexp != NULL) {
		if ((result = regcomp(&regex, regexp, REG_EXTENDED)) < 0) {
			giterr_set_regex(&regex, result);
			regfree(&regex);
			return -1;
		}
	}
271

272
	git_strmap_foreach(b->values, key, var,
273 274 275
		for (; var != NULL; var = next_var) {
			next_var = CVAR_LIST_NEXT(var);

276 277 278 279 280
			/* skip non-matching keys if regexp was provided */
			if (regexp && regexec(&regex, key, 0, NULL, 0) != 0)
				continue;

			/* abort iterator on non-zero return value */
281
			if (fn(var->entry, data)) {
Russell Belfer committed
282
				giterr_clear();
283
				result = GIT_EUSER;
284
				goto cleanup;
285
			}
286
		}
287
	);
288

289 290 291 292 293
cleanup:
	if (regexp != NULL)
		regfree(&regex);

	return result;
294 295
}

Ben Straub committed
296
static int config_set(git_config_backend *cfg, const char *name, const char *value)
297
{
298
	cvar_t *var = NULL, *old_var;
299
	diskfile_backend *b = (diskfile_backend *)cfg;
300
	char *key, *esc_value = NULL;
301
	khiter_t pos;
302
	int rval, ret;
303

304 305
	if ((rval = normalize_name(name, &key)) < 0)
		return rval;
306 307

	/*
308 309
	 * Try to find it in the existing values and update it if it
	 * only has one value.
310
	 */
311 312 313
	pos = git_strmap_lookup_index(b->values, key);
	if (git_strmap_valid_index(b->values, pos)) {
		cvar_t *existing = git_strmap_value_at(b->values, pos);
314
		char *tmp = NULL;
315 316

		git__free(key);
Russell Belfer committed
317

318 319 320 321
		if (existing->next != NULL) {
			giterr_set(GITERR_CONFIG, "Multivar incompatible with simple set");
			return -1;
		}
322

Russell Belfer committed
323
		/* don't update if old and new values already match */
324 325
		if ((!existing->entry->value && !value) ||
			(existing->entry->value && value && !strcmp(existing->entry->value, value)))
Russell Belfer committed
326 327
			return 0;

328 329 330
		if (value) {
			tmp = git__strdup(value);
			GITERR_CHECK_ALLOC(tmp);
331 332
			esc_value = escape_value(value);
			GITERR_CHECK_ALLOC(esc_value);
333
		}
334

335 336
		git__free((void *)existing->entry->value);
		existing->entry->value = tmp;
337

338
		ret = config_write(b, existing->entry->name, NULL, esc_value);
339 340 341

		git__free(esc_value);
		return ret;
342 343
	}

344
	var = git__malloc(sizeof(cvar_t));
345
	GITERR_CHECK_ALLOC(var);
346
	memset(var, 0x0, sizeof(cvar_t));
347 348 349
	var->entry = git__malloc(sizeof(git_config_entry));
	GITERR_CHECK_ALLOC(var->entry);
	memset(var->entry, 0x0, sizeof(git_config_entry));
350

351 352
	var->entry->name = key;
	var->entry->value = NULL;
353

354
	if (value) {
355 356
		var->entry->value = git__strdup(value);
		GITERR_CHECK_ALLOC(var->entry->value);
357 358
		esc_value = escape_value(value);
		GITERR_CHECK_ALLOC(esc_value);
359 360
	}

361 362
	if (config_write(b, key, NULL, esc_value) < 0) {
		git__free(esc_value);
363 364 365 366
		cvar_free(var);
		return -1;
	}

367
	git__free(esc_value);
368
	git_strmap_insert2(b->values, key, var, old_var, rval);
369
	if (rval < 0)
370
		return -1;
371 372
	if (old_var != NULL)
		cvar_free(old_var);
373

374
	return 0;
375 376 377 378 379
}

/*
 * Internal function that actually gets the value in string form
 */
Ben Straub committed
380
static int config_get(const git_config_backend *cfg, const char *name, const git_config_entry **out)
381
{
382
	diskfile_backend *b = (diskfile_backend *)cfg;
383
	char *key;
384
	khiter_t pos;
385
	int error;
386

387 388
	if ((error = normalize_name(name, &key)) < 0)
		return error;
389

390
	pos = git_strmap_lookup_index(b->values, key);
391
	git__free(key);
392

393
	/* no error message; the config system will write one */
394
	if (!git_strmap_valid_index(b->values, pos))
395
		return GIT_ENOTFOUND;
396

397
	*out = ((cvar_t *)git_strmap_value_at(b->values, pos))->entry;
398

399
	return 0;
400 401
}

402
static int config_get_multivar(
Ben Straub committed
403
	git_config_backend *cfg,
404 405
	const char *name,
	const char *regex_str,
406
	int (*fn)(const git_config_entry *, void *),
407
	void *data)
408 409 410 411
{
	cvar_t *var;
	diskfile_backend *b = (diskfile_backend *)cfg;
	char *key;
412
	khiter_t pos;
413
	int error;
414

415 416
	if ((error = normalize_name(name, &key)) < 0)
		return error;
417

418
	pos = git_strmap_lookup_index(b->values, key);
419 420
	git__free(key);

421
	if (!git_strmap_valid_index(b->values, pos))
422
		return GIT_ENOTFOUND;
423

424
	var = git_strmap_value_at(b->values, pos);
425

426 427 428
	if (regex_str != NULL) {
		regex_t regex;
		int result;
429

430 431 432 433
		/* regex matching; build the regex */
		result = regcomp(&regex, regex_str, REG_EXTENDED);
		if (result < 0) {
			giterr_set_regex(&regex, result);
434
			regfree(&regex);
435
			return -1;
436 437
		}

438 439 440
		/* and throw the callback only on the variables that
		 * match the regex */
		do {
441
			if (regexec(&regex, var->entry->value, 0, NULL, 0) == 0) {
442 443
				/* early termination by the user is not an error;
				 * just break and return successfully */
444
				if (fn(var->entry, data) < 0)
445 446
					break;
			}
447

448 449
			var = var->next;
		} while (var != NULL);
450
		regfree(&regex);
451 452 453 454 455
	} else {
		/* no regex; go through all the variables */
		do {
			/* early termination by the user is not an error;
			 * just break and return successfully */
456
			if (fn(var->entry, data) < 0)
457 458 459 460 461
				break;

			var = var->next;
		} while (var != NULL);
	}
462

463
	return 0;
464 465
}

466
static int config_set_multivar(
Ben Straub committed
467
	git_config_backend *cfg, const char *name, const char *regexp, const char *value)
468
{
469
	int replaced = 0;
470
	cvar_t *var, *newvar;
471 472 473
	diskfile_backend *b = (diskfile_backend *)cfg;
	char *key;
	regex_t preg;
474
	int result;
475
	khiter_t pos;
476

477
	assert(regexp);
478

479 480
	if ((result = normalize_name(name, &key)) < 0)
		return result;
481

482 483
	pos = git_strmap_lookup_index(b->values, key);
	if (!git_strmap_valid_index(b->values, pos)) {
484
		git__free(key);
485
		return GIT_ENOTFOUND;
486
	}
487

488
	var = git_strmap_value_at(b->values, pos);
489

490 491
	result = regcomp(&preg, regexp, REG_EXTENDED);
	if (result < 0) {
492
		git__free(key);
493
		giterr_set_regex(&preg, result);
494
		regfree(&preg);
495
		return -1;
496
	}
497

498
	for (;;) {
499
		if (regexec(&preg, var->entry->value, 0, NULL, 0) == 0) {
500
			char *tmp = git__strdup(value);
501
			GITERR_CHECK_ALLOC(tmp);
502

503 504
			git__free((void *)var->entry->value);
			var->entry->value = tmp;
505 506 507
			replaced = 1;
		}

508
		if (var->next == NULL)
509
			break;
510 511 512

		var = var->next;
	}
513 514 515 516

	/* If we've reached the end of the variables and we haven't found it yet, we need to append it */
	if (!replaced) {
		newvar = git__malloc(sizeof(cvar_t));
517
		GITERR_CHECK_ALLOC(newvar);
518
		memset(newvar, 0x0, sizeof(cvar_t));
519 520 521 522 523 524
		newvar->entry = git__malloc(sizeof(git_config_entry));
		GITERR_CHECK_ALLOC(newvar->entry);
		memset(newvar->entry, 0x0, sizeof(git_config_entry));

		newvar->entry->name = git__strdup(var->entry->name);
		GITERR_CHECK_ALLOC(newvar->entry->name);
525

526 527
		newvar->entry->value = git__strdup(value);
		GITERR_CHECK_ALLOC(newvar->entry->value);
528

529
		newvar->entry->level = var->entry->level;
530 531 532 533

		var->next = newvar;
	}

534
	result = config_write(b, key, &preg, value);
535

536
	git__free(key);
537
	regfree(&preg);
538 539

	return result;
540 541
}

Ben Straub committed
542
static int config_delete(git_config_backend *cfg, const char *name)
543
{
544
	cvar_t *var;
545
	diskfile_backend *b = (diskfile_backend *)cfg;
546
	char *key;
547
	int result;
548
	khiter_t pos;
549

550 551
	if ((result = normalize_name(name, &key)) < 0)
		return result;
552

553
	pos = git_strmap_lookup_index(b->values, key);
554
	git__free(key);
555

556 557
	if (!git_strmap_valid_index(b->values, pos)) {
		giterr_set(GITERR_CONFIG, "Could not find key '%s' to delete", name);
558
		return GIT_ENOTFOUND;
559
	}
560

561
	var = git_strmap_value_at(b->values, pos);
562

563 564 565 566
	if (var->next != NULL) {
		giterr_set(GITERR_CONFIG, "Cannot delete multivar with a single delete");
		return -1;
	}
567

568
	git_strmap_delete_at(b->values, pos);
569

570
	result = config_write(b, var->entry->name, NULL, NULL);
571

572 573
	cvar_free(var);
	return result;
574 575
}

Ben Straub committed
576
int git_config_file__ondisk(git_config_backend **out, const char *path)
577
{
578
	diskfile_backend *backend;
579

580
	backend = git__calloc(1, sizeof(diskfile_backend));
581
	GITERR_CHECK_ALLOC(backend);
582

583
	backend->parent.version = GIT_CONFIG_BACKEND_VERSION;
584 585

	backend->file_path = git__strdup(path);
586
	GITERR_CHECK_ALLOC(backend->file_path);
587 588 589

	backend->parent.open = config_open;
	backend->parent.get = config_get;
590
	backend->parent.get_multivar = config_get_multivar;
591
	backend->parent.set = config_set;
592
	backend->parent.set_multivar = config_set_multivar;
593
	backend->parent.del = config_delete;
594
	backend->parent.foreach = file_foreach;
595
	backend->parent.refresh = config_refresh;
596 597
	backend->parent.free = backend_free;

Ben Straub committed
598
	*out = (git_config_backend *)backend;
599

600
	return 0;
601 602
}

603
static int cfg_getchar_raw(diskfile_backend *cfg)
604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631
{
	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)

632
static int cfg_getchar(diskfile_backend *cfg_file, int flags)
633 634 635 636 637 638 639 640
{
	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);
641
	while (skip_whitespace && git__isspace(c) &&
642
	       !cfg_file->reader.eof);
643 644 645 646 647 648 649 650 651 652 653 654

	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.
 */
655
static int cfg_peek(diskfile_backend *cfg, int flags)
656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678
{
	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.
 */
679
static char *cfg_readline(diskfile_backend *cfg, bool skip_whitespace)
680 681 682
{
	char *line = NULL;
	char *line_src, *line_end;
683
	size_t line_len;
684 685

	line_src = cfg->reader.read_ptr;
686

687 688
	if (skip_whitespace) {
		/* Skip empty empty lines */
689
		while (git__isspace(*line_src))
690 691
			++line_src;
	}
692

693
	line_end = strchr(line_src, '\n');
694

695
	/* no newline at EOF */
696 697 698
	if (line_end == NULL)
		line_end = strchr(line_src, 0);

699
	line_len = line_end - line_src;
700

701
	line = git__malloc(line_len + 1);
702 703 704
	if (line == NULL)
		return NULL;

705
	memcpy(line, line_src, line_len);
706

707
	do line[line_len] = '\0';
708
	while (line_len-- > 0 && git__isspace(line[line_len]));
709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724

	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
 */
725
static void cfg_consume_line(diskfile_backend *cfg)
726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745
{
	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;
}

746
GIT_INLINE(int) config_keychar(int c)
747 748 749 750
{
	return isalnum(c) || c == '-';
}

751
static int parse_section_header_ext(diskfile_backend *cfg, const char *line, const char *base_name, char **section_name)
752
{
753 754 755
	int c, rpos;
	char *first_quote, *last_quote;
	git_buf buf = GIT_BUF_INIT;
756 757 758 759 760 761 762 763 764 765
	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, '"');

766 767 768 769
	if (last_quote - first_quote == 0) {
		set_parse_error(cfg, 0, "Missing closing quotation mark in section header");
		return -1;
	}
770

771 772
	git_buf_grow(&buf, strlen(base_name) + last_quote - first_quote + 2);
	git_buf_printf(&buf, "%s.", base_name);
773 774 775 776 777 778 779 780 781 782 783 784

	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 {
785
		if (quote_marks == 2) {
786 787 788
			set_parse_error(cfg, rpos, "Unexpected text after closing quotes");
			git_buf_free(&buf);
			return -1;
789 790
		}

791 792
		switch (c) {
		case '"':
793
			++quote_marks;
794
			continue;
795

796 797
		case '\\':
			c = line[rpos++];
798

799 800 801 802
			switch (c) {
			case '"':
			case '\\':
				break;
803

804
			default:
805 806 807
				set_parse_error(cfg, rpos, "Unsupported escape sequence");
				git_buf_free(&buf);
				return -1;
808
			}
809

810 811 812 813
		default:
			break;
		}

814
		git_buf_putc(&buf, c);
815 816
	} while ((c = line[rpos++]) != ']');

817 818
	*section_name = git_buf_detach(&buf);
	return 0;
819 820
}

821
static int parse_section_header(diskfile_backend *cfg, char **section_out)
822 823 824
{
	char *name, *name_end;
	int name_length, c, pos;
825
	int result;
826 827
	char *line;

828
	line = cfg_readline(cfg, true);
829
	if (line == NULL)
830
		return -1;
831 832 833

	/* find the end of the variable's name */
	name_end = strchr(line, ']');
schu committed
834
	if (name_end == NULL) {
835
		git__free(line);
836 837
		set_parse_error(cfg, 0, "Missing ']' in section header");
		return -1;
schu committed
838
	}
839 840

	name = (char *)git__malloc((size_t)(name_end - line) + 1);
841
	GITERR_CHECK_ALLOC(name);
842 843 844 845 846 847

	name_length = 0;
	pos = 0;

	/* Make sure we were given a section header */
	c = line[pos++];
848
	assert(c == '[');
849 850 851 852

	c = line[pos++];

	do {
853
		if (git__isspace(c)){
854
			name[name_length] = '\0';
855
			result = parse_section_header_ext(cfg, line, name, section_out);
856 857
			git__free(line);
			git__free(name);
858
			return result;
859 860 861
		}

		if (!config_keychar(c) && c != '.') {
862 863
			set_parse_error(cfg, pos, "Unexpected character in header");
			goto fail_parse;
864 865
		}

866
		name[name_length++] = (char) tolower(c);
867 868 869

	} while ((c = line[pos++]) != ']');

schu committed
870
	if (line[pos - 1] != ']') {
871 872
		set_parse_error(cfg, pos, "Unexpected end of file");
		goto fail_parse;
schu committed
873
	}
874

875
	git__free(line);
876 877

	name[name_length] = 0;
878 879
	*section_out = name;

880 881 882
	return 0;

fail_parse:
883 884
	git__free(line);
	git__free(name);
885
	return -1;
886 887
}

888
static int skip_bom(diskfile_backend *cfg)
889
{
890 891 892
	git_bom_t bom;
	int bom_offset = git_buf_text_detect_bom(&bom,
		&cfg->reader.buffer, cfg->reader.read_ptr - cfg->reader.buffer.ptr);
893

894 895
	if (bom == GIT_BOM_UTF8)
		cfg->reader.read_ptr += bom_offset;
896

897
	/* TODO: reference implementation is pretty stupid with BoM */
898

899
	return 0;
900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940
}

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

941
static int strip_comments(char *line, int in_quotes)
942
{
943
	int quote_count = in_quotes;
944 945 946 947 948 949 950 951 952 953 954 955
	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;
		}
	}

956
	/* skip any space at the end */
957
	if (ptr > line && git__isspace(ptr[-1])) {
958
		ptr--;
959
	}
960 961 962
	ptr[0] = '\0';

	return quote_count;
963 964
}

965
static int config_parse(diskfile_backend *cfg_file, unsigned int level)
966
{
967
	int c;
968 969 970
	char *current_section = NULL;
	char *var_name;
	char *var_value;
971
	cvar_t *var, *existing;
972
	git_buf buf = GIT_BUF_INIT;
973
	int result = 0;
974
	khiter_t pos;
975

976
	/* Initialize the reading position */
977
	cfg_file->reader.read_ptr = cfg_file->reader.buffer.ptr;
978 979
	cfg_file->reader.eof = 0;

980 981
	/* If the file is empty, there's nothing for us to do */
	if (*cfg_file->reader.read_ptr == '\0')
982
		return 0;
983

984 985
	skip_bom(cfg_file);

986
	while (result == 0 && !cfg_file->reader.eof) {
987 988 989 990

		c = cfg_peek(cfg_file, SKIP_WHITESPACE);

		switch (c) {
991 992
		case '\n': /* EOF when peeking, set EOF in the reader to exit the loop */
			cfg_file->reader.eof = 1;
993 994 995
			break;

		case '[': /* section header, new section begins */
996
			git__free(current_section);
997
			current_section = NULL;
998
			result = parse_section_header(cfg_file, &current_section);
999 1000 1001 1002 1003 1004 1005 1006
			break;

		case ';':
		case '#':
			cfg_consume_line(cfg_file);
			break;

		default: /* assume variable declaration */
1007 1008
			result = parse_variable(cfg_file, &var_name, &var_value);
			if (result < 0)
1009 1010
				break;

1011
			var = git__malloc(sizeof(cvar_t));
1012
			GITERR_CHECK_ALLOC(var);
1013
			memset(var, 0x0, sizeof(cvar_t));
1014 1015 1016
			var->entry = git__malloc(sizeof(git_config_entry));
			GITERR_CHECK_ALLOC(var->entry);
			memset(var->entry, 0x0, sizeof(git_config_entry));
1017

1018 1019 1020 1021
			git__strtolower(var_name);
			git_buf_printf(&buf, "%s.%s", current_section, var_name);
			git__free(var_name);

1022 1023
			if (git_buf_oom(&buf))
				return -1;
1024

1025 1026 1027
			var->entry->name = git_buf_detach(&buf);
			var->entry->value = var_value;
			var->entry->level = level;
1028

1029
			/* Add or append the new config option */
1030
			pos = git_strmap_lookup_index(cfg_file->values, var->entry->name);
1031
			if (!git_strmap_valid_index(cfg_file->values, pos)) {
1032
				git_strmap_insert(cfg_file->values, var->entry->name, var, result);
1033 1034 1035
				if (result < 0)
					break;
				result = 0;
1036
			} else {
1037
				existing = git_strmap_value_at(cfg_file->values, pos);
1038 1039 1040 1041 1042
				while (existing->next != NULL) {
					existing = existing->next;
				}
				existing->next = var;
			}
1043 1044 1045 1046 1047

			break;
		}
	}

1048
	git__free(current_section);
1049
	return result;
1050 1051
}

1052
static int write_section(git_filebuf *file, const char *key)
1053
{
1054
	int result;
1055
	const char *dot;
1056
	git_buf buf = GIT_BUF_INIT;
1057

1058
	/* All of this just for [section "subsection"] */
1059
	dot = strchr(key, '.');
1060
	git_buf_putc(&buf, '[');
1061
	if (dot == NULL) {
1062
		git_buf_puts(&buf, key);
1063
	} else {
1064
		char *escaped;
1065
		git_buf_put(&buf, key, dot - key);
1066 1067 1068 1069
		escaped = escape_value(dot + 1);
		GITERR_CHECK_ALLOC(escaped);
		git_buf_printf(&buf, " \"%s\"", escaped);
		git__free(escaped);
1070 1071
	}
	git_buf_puts(&buf, "]\n");
1072

1073
	if (git_buf_oom(&buf))
1074
		return -1;
1075

1076
	result = git_filebuf_write(file, git_buf_cstr(&buf), buf.size);
1077
	git_buf_free(&buf);
1078

1079
	return result;
1080 1081 1082 1083 1084
}

/*
 * This is pretty much the parsing, except we write out anything we don't have
 */
1085
static int config_write(diskfile_backend *cfg, const char *key, const regex_t *preg, const char* value)
1086
{
1087 1088 1089
	int result, c;
	int section_matches = 0, last_section_matched = 0, preg_replaced = 0, write_trailer = 0;
	const char *pre_end = NULL, *post_start = NULL, *data_start;
1090
	char *current_section = NULL, *section, *name, *ldot;
1091
	git_filebuf file = GIT_FILEBUF_INIT;
1092 1093

	/* We need to read in our own config file */
1094
	result = git_futils_readbuffer(&cfg->reader.buffer, cfg->file_path);
1095 1096

	/* Initialise the reading position */
1097
	if (result == GIT_ENOTFOUND) {
1098 1099 1100
		cfg->reader.read_ptr = NULL;
		cfg->reader.eof = 1;
		data_start = NULL;
1101
		git_buf_clear(&cfg->reader.buffer);
1102
	} else if (result == 0) {
1103
		cfg->reader.read_ptr = cfg->reader.buffer.ptr;
1104 1105
		cfg->reader.eof = 0;
		data_start = cfg->reader.read_ptr;
1106 1107
	} else {
		return -1; /* OS error when reading the file */
1108
	}
1109 1110

	/* Lock the file */
1111 1112
	if (git_filebuf_open(&file, cfg->file_path, 0) < 0)
		return -1;
1113 1114

	skip_bom(cfg);
1115 1116 1117
	ldot = strrchr(key, '.');
	name = ldot + 1;
	section = git__strndup(key, ldot - key);
1118

1119
	while (!cfg->reader.eof) {
1120 1121
		c = cfg_peek(cfg, SKIP_WHITESPACE);

1122
		if (c == '\0') { /* We've arrived at the end of the file */
1123 1124
			break;

1125
		} else if (c == '[') { /* section header, new section begins */
1126 1127 1128 1129 1130 1131 1132 1133
			/*
			 * We set both positions to the current one in case we
			 * need to add a variable to the end of a section. In that
			 * case, we want both variables to point just before the
			 * new section. If we actually want to replace it, the
			 * default case will take care of updating them.
			 */
			pre_end = post_start = cfg->reader.read_ptr;
1134 1135

			git__free(current_section);
1136
			current_section = NULL;
1137 1138
			if (parse_section_header(cfg, &current_section) < 0)
				goto rewrite_fail;
1139 1140 1141

			/* Keep track of when it stops matching */
			last_section_matched = section_matches;
1142
			section_matches = !strcmp(current_section, section);
1143
		}
1144

1145
		else if (c == ';' || c == '#') {
1146
			cfg_consume_line(cfg);
1147
		}
1148

1149
		else {
1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162
			/*
			 * If the section doesn't match, but the last section did,
			 * it means we need to add a variable (so skip the line
			 * otherwise). If both the section and name match, we need
			 * to overwrite the variable (so skip the line
			 * otherwise). pre_end needs to be updated each time so we
			 * don't loose that information, but we only need to
			 * update post_start if we're going to use it in this
			 * iteration.
			 */
			if (!section_matches) {
				if (!last_section_matched) {
					cfg_consume_line(cfg);
1163
					continue;
1164 1165
				}
			} else {
1166 1167
				int has_matched = 0;
				char *var_name, *var_value;
1168

1169
				pre_end = cfg->reader.read_ptr;
1170 1171
				if (parse_variable(cfg, &var_name, &var_value) < 0)
					goto rewrite_fail;
1172

1173 1174 1175
				/* First try to match the name of the variable */
				if (strcasecmp(name, var_name) == 0)
					has_matched = 1;
1176

1177 1178 1179 1180
				/* If the name matches, and we have a regex to match the
				 * value, try to match it */
				if (has_matched && preg != NULL)
					has_matched = (regexec(preg, var_value, 0, NULL, 0) == 0);
1181

1182 1183
				git__free(var_name);
				git__free(var_value);
1184

1185 1186 1187
				/* if there is no match, keep going */
				if (!has_matched)
					continue;
1188

1189 1190 1191
				post_start = cfg->reader.read_ptr;
			}

1192 1193 1194
			/* We've found the variable we wanted to change, so
			 * write anything up to it */
			git_filebuf_write(&file, data_start, pre_end - data_start);
1195
			preg_replaced = 1;
1196

1197 1198 1199 1200
			/* Then replace the variable. If the value is NULL, it
			 * means we want to delete it, so don't write anything. */
			if (value != NULL) {
				git_filebuf_printf(&file, "\t%s = %s\n", name, value);
1201 1202
			}

1203
			/* multiline variable? we need to keep reading lines to match */
1204 1205 1206 1207 1208
			if (preg != NULL) {
				data_start = post_start;
				continue;
			}

1209 1210
			write_trailer = 1;
			break; /* break from the loop */
1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222
		}
	}

	/*
	 * Being here can mean that
	 *
	 * 1) our section is the last one in the file and we're
	 * adding a variable
	 *
	 * 2) we didn't find a section for us so we need to create it
	 * ourselves.
	 *
1223 1224 1225 1226 1227 1228
	 * 3) we're setting a multivar with a regex, which means we
	 * continue to search for matching values
	 *
	 * In the last case, if we've already replaced a value, we
	 * want to write the rest of the file. Otherwise we need to write
	 * out the whole file and then the new variable.
1229
	 */
1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250
	if (write_trailer) {
		/* Write out rest of the file */
		git_filebuf_write(&file, post_start, cfg->reader.buffer.size - (post_start - data_start));
	} else {
		if (preg_replaced) {
			git_filebuf_printf(&file, "\n%s", data_start);
		} else {
			git_filebuf_write(&file, cfg->reader.buffer.ptr, cfg->reader.buffer.size);

			/* And now if we just need to add a variable */
			if (!section_matches && write_section(&file, section) < 0)
				goto rewrite_fail;

			/* Sanity check: if we are here, and value is NULL, that means that somebody
			 * touched the config file after our intial read. We should probably assert()
			 * this, but instead we'll handle it gracefully with an error. */
			if (value == NULL) {
				giterr_set(GITERR_CONFIG,
					"Race condition when writing a config file (a cvar has been removed)");
				goto rewrite_fail;
			}
1251

1252
			/* If we are here, there is at least a section line */
1253
			if (cfg->reader.buffer.size > 0 && *(cfg->reader.buffer.ptr + cfg->reader.buffer.size - 1) != '\n')
1254 1255
				git_filebuf_write(&file, "\n", 1);

1256 1257
			git_filebuf_printf(&file, "\t%s = %s\n", name, value);
		}
1258 1259
	}

1260 1261
	git__free(section);
	git__free(current_section);
1262

1263 1264 1265
	/* refresh stats - if this errors, then commit will error too */
	(void)git_filebuf_stats(&cfg->file_mtime, &cfg->file_size, &file);

1266 1267
	result = git_filebuf_commit(&file, GIT_CONFIG_FILE_MODE);
	git_buf_free(&cfg->reader.buffer);
1268

1269
	return result;
1270

1271
rewrite_fail:
1272
	git__free(section);
1273
	git__free(current_section);
1274

1275
	git_filebuf_cleanup(&file);
1276
	git_buf_free(&cfg->reader.buffer);
1277
	return -1;
1278 1279
}

1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312
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);
	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);
}

1313 1314
/* '\"' -> '"' etc */
static char *fixup_line(const char *ptr, int quote_count)
1315
{
1316 1317 1318 1319 1320
	char *str = git__malloc(strlen(ptr) + 1);
	char *out = str, *esc;

	if (str == NULL)
		return NULL;
1321

1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350
	while (*ptr != '\0') {
		if (*ptr == '"') {
			quote_count++;
		} else if (*ptr != '\\') {
			*out++ = *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') {
				*out++ = '\\';
				goto out;
			}
			if ((esc = strchr(escapes, *ptr)) != NULL) {
				*out++ = escaped[esc - escapes];
			} else {
				git__free(str);
				giterr_set(GITERR_CONFIG, "Invalid escape at %s", ptr);
				return NULL;
			}
		}
		ptr++;
	}

out:
	*out = '\0';

	return str;
}
1351 1352 1353

static int is_multiline_var(const char *str)
{
1354
	int count = 0;
1355
	const char *end = str + strlen(str);
1356 1357 1358 1359 1360 1361 1362
	while (end > str && end[-1] == '\\') {
		count++;
		end--;
	}

	/* An odd number means last backslash wasn't escaped, so it's multiline */
	return (end > str) && (count & 1);
1363 1364
}

1365
static int parse_multiline_variable(diskfile_backend *cfg, git_buf *value, int in_quotes)
1366
{
1367 1368
	char *line = NULL, *proc_line = NULL;
	int quote_count;
1369 1370

	/* Check that the next line exists */
1371
	line = cfg_readline(cfg, false);
1372
	if (line == NULL)
1373
		return -1;
1374 1375 1376

	/* We've reached the end of the file, there is input missing */
	if (line[0] == '\0') {
1377 1378 1379
		set_parse_error(cfg, 0, "Unexpected end of file while parsing multine var");
		git__free(line);
		return -1;
1380 1381
	}

1382
	quote_count = strip_comments(line, !!in_quotes);
1383 1384 1385

	/* If it was just a comment, pretend it didn't exist */
	if (line[0] == '\0') {
1386
		git__free(line);
1387
		return parse_multiline_variable(cfg, value, quote_count);
1388
		/* TODO: unbounded recursion. This **could** be exploitable */
1389 1390
	}

1391 1392 1393 1394
	/* Drop the continuation character '\': to closely follow the UNIX
	 * standard, this character **has** to be last one in the buf, with
	 * no whitespace after it */
	assert(is_multiline_var(value->ptr));
nulltoken committed
1395
	git_buf_truncate(value, git_buf_len(value) - 1);
1396

1397 1398 1399 1400
	proc_line = fixup_line(line, in_quotes);
	if (proc_line == NULL) {
		git__free(line);
		return -1;
1401
	}
1402
	/* add this line to the multiline var */
1403
	git_buf_puts(value, proc_line);
1404
	git__free(line);
1405
	git__free(proc_line);
1406 1407

	/*
1408 1409
	 * If we need to continue reading the next line, let's just
	 * keep putting stuff in the buffer
1410
	 */
1411
	if (is_multiline_var(value->ptr))
1412
		return parse_multiline_variable(cfg, value, quote_count);
1413

1414
	return 0;
1415 1416
}

1417
static int parse_variable(diskfile_backend *cfg, char **var_name, char **var_value)
1418 1419 1420 1421
{
	const char *var_end = NULL;
	const char *value_start = NULL;
	char *line;
1422
	int quote_count;
1423

1424
	line = cfg_readline(cfg, true);
1425
	if (line == NULL)
1426
		return -1;
1427

1428
	quote_count = strip_comments(line, 0);
1429 1430 1431 1432 1433 1434 1435 1436

	var_end = strchr(line, '=');

	if (var_end == NULL)
		var_end = strchr(line, '\0');
	else
		value_start = var_end + 1;

Russell Belfer committed
1437
	do var_end--;
1438
	while (var_end>line && git__isspace(*var_end));
1439

1440 1441
	*var_name = git__strndup(line, var_end - line + 1);
	GITERR_CHECK_ALLOC(*var_name);
1442

1443 1444
	/* If there is no value, boolean true is assumed */
	*var_value = NULL;
1445 1446 1447 1448 1449

	/*
	 * Now, let's try to parse the value
	 */
	if (value_start != NULL) {
1450
		while (git__isspace(value_start[0]))
1451 1452 1453
			value_start++;

		if (is_multiline_var(value_start)) {
1454
			git_buf multi_value = GIT_BUF_INIT;
1455 1456 1457
			char *proc_line = fixup_line(value_start, 0);
			GITERR_CHECK_ALLOC(proc_line);
			git_buf_puts(&multi_value, proc_line);
1458
			git__free(proc_line);
1459
			if (parse_multiline_variable(cfg, &multi_value, quote_count) < 0 || git_buf_oom(&multi_value)) {
1460
				git__free(*var_name);
1461 1462 1463
				git__free(line);
				git_buf_free(&multi_value);
				return -1;
1464
			}
1465

1466
			*var_value = git_buf_detach(&multi_value);
1467

1468 1469
		}
		else if (value_start[0] != '\0') {
1470
			*var_value = fixup_line(value_start, 0);
1471
			GITERR_CHECK_ALLOC(*var_value);
1472 1473 1474
		} else { /* equals sign but missing rhs */
			*var_value = git__strdup("");
			GITERR_CHECK_ALLOC(*var_value);
1475 1476 1477
		}
	}

1478
	git__free(line);
1479
	return 0;
1480
}