attr_file.c 24.7 KB
Newer Older
1 2 3 4 5 6 7
/*
 * 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.
 */

8 9
#include "attr_file.h"

10
#include "repository.h"
11
#include "filebuf.h"
12
#include "attrcache.h"
13 14
#include "git2/blob.h"
#include "git2/tree.h"
15
#include "blob.h"
16
#include "index.h"
17
#include "wildmatch.h"
18 19
#include <ctype.h>

20 21
static void attr_file_free(git_attr_file *file)
{
22 23
	bool unlock = !git_mutex_lock(&file->lock);
	git_attr_file__clear_rules(file, false);
24
	git_pool_clear(&file->pool);
25 26 27 28
	if (unlock)
		git_mutex_unlock(&file->lock);
	git_mutex_free(&file->lock);

29 30 31
	git__memzero(file, sizeof(*file));
	git__free(file);
}
32

33
int git_attr_file__new(
34
	git_attr_file **out,
35
	git_attr_file_entry *entry,
36
	git_attr_file_source *source)
37
{
38
	git_attr_file *attrs = git__calloc(1, sizeof(git_attr_file));
39
	GIT_ERROR_CHECK_ALLOC(attrs);
40

41
	if (git_mutex_init(&attrs->lock) < 0) {
42
		git_error_set(GIT_ERROR_OS, "failed to initialize lock");
43
		goto on_error;
44 45
	}

46 47 48
	if (git_pool_init(&attrs->pool, 1) < 0)
		goto on_error;

49
	GIT_REFCOUNT_INC(attrs);
50
	attrs->entry = entry;
51
	memcpy(&attrs->source, source, sizeof(git_attr_file_source));
52 53
	*out = attrs;
	return 0;
54 55 56 57

on_error:
	git__free(attrs);
	return -1;
58
}
59

60
int git_attr_file__clear_rules(git_attr_file *file, bool need_lock)
61 62 63
{
	unsigned int i;
	git_attr_rule *rule;
64

65
	if (need_lock && git_mutex_lock(&file->lock) < 0) {
66
		git_error_set(GIT_ERROR_OS, "failed to lock attribute file");
67 68 69
		return -1;
	}

70 71 72
	git_vector_foreach(&file->rules, i, rule)
		git_attr_rule__free(rule);
	git_vector_free(&file->rules);
73 74 75 76 77

	if (need_lock)
		git_mutex_unlock(&file->lock);

	return 0;
78
}
79

80 81 82 83 84 85
void git_attr_file__free(git_attr_file *file)
{
	if (!file)
		return;
	GIT_REFCOUNT_DEC(file, attr_file_free);
}
86

87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
static int attr_file_oid_from_index(
	git_oid *oid, git_repository *repo, const char *path)
{
	int error;
	git_index *idx;
	size_t pos;
	const git_index_entry *entry;

	if ((error = git_repository_index__weakptr(&idx, repo)) < 0 ||
		(error = git_index__find_pos(&pos, idx, path, 0, 0)) < 0)
		return error;

	if (!(entry = git_index_get_byindex(idx, pos)))
		return GIT_ENOTFOUND;

	*oid = entry->id;
103
	return 0;
104 105 106 107 108
}

int git_attr_file__load(
	git_attr_file **out,
	git_repository *repo,
109
	git_attr_session *attr_session,
110
	git_attr_file_entry *entry,
111
	git_attr_file_source *source,
112 113
	git_attr_file_parser parser,
	bool allow_macros)
114 115
{
	int error = 0;
116
	git_commit *commit = NULL;
117 118
	git_tree *tree = NULL;
	git_tree_entry *tree_entry = NULL;
119
	git_blob *blob = NULL;
120
	git_str content = GIT_STR_INIT;
121
	const char *content_str;
122
	git_attr_file *file;
123
	struct stat st;
124
	bool nonexistent = false;
125
	int bom_offset;
126
	git_str_bom_t bom;
127
	git_oid id;
128
	git_object_size_t blobsize;
129 130 131

	*out = NULL;

132
	switch (source->type) {
133
	case GIT_ATTR_FILE_SOURCE_MEMORY:
134 135
		/* in-memory attribute file doesn't need data */
		break;
136
	case GIT_ATTR_FILE_SOURCE_INDEX: {
137
		if ((error = attr_file_oid_from_index(&id, repo, entry->path)) < 0 ||
Edward Thomson committed
138
		    (error = git_blob_lookup(&blob, repo, &id)) < 0)
139 140
			return error;

141 142
		/* Do not assume that data straight from the ODB is NULL-terminated;
		 * copy the contents of a file to a buffer to work on */
143 144 145
		blobsize = git_blob_rawsize(blob);

		GIT_ERROR_CHECK_BLOBSIZE(blobsize);
146
		git_str_put(&content, git_blob_rawcontent(blob), (size_t)blobsize);
147 148
		break;
	}
149
	case GIT_ATTR_FILE_SOURCE_FILE: {
150
		int fd = -1;
151

152
		/* For open or read errors, pretend that we got ENOTFOUND. */
153 154
		/* TODO: issue warning when warning API is available */

155 156 157 158 159
		if (p_stat(entry->fullpath, &st) < 0 ||
			S_ISDIR(st.st_mode) ||
			(fd = git_futils_open_ro(entry->fullpath)) < 0 ||
			(error = git_futils_readbuffer_fd(&content, fd, (size_t)st.st_size)) < 0)
			nonexistent = true;
160 161

		if (fd >= 0)
162
			p_close(fd);
163 164 165

		break;
	}
166
	case GIT_ATTR_FILE_SOURCE_HEAD:
167
	case GIT_ATTR_FILE_SOURCE_COMMIT: {
168
		if (source->type == GIT_ATTR_FILE_SOURCE_COMMIT) {
169 170 171 172 173 174 175
			if ((error = git_commit_lookup(&commit, repo, source->commit_id)) < 0 ||
			    (error = git_commit_tree(&tree, commit)) < 0)
				goto cleanup;
		} else {
			if ((error = git_repository_head_tree(&tree, repo)) < 0)
				goto cleanup;
		}
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191

		if ((error = git_tree_entry_bypath(&tree_entry, tree, entry->path)) < 0) {
			/*
			 * If the attributes file does not exist, we can
			 * cache an empty file for this commit to prevent
			 * needless future lookups.
			 */
			if (error == GIT_ENOTFOUND) {
				error = 0;
				break;
			}

			goto cleanup;
		}

		if ((error = git_blob_lookup(&blob, repo, git_tree_entry_id(tree_entry))) < 0)
192 193 194 195 196 197 198 199 200
			goto cleanup;

		/*
		 * Do not assume that data straight from the ODB is NULL-terminated;
		 * copy the contents of a file to a buffer to work on.
		 */
		blobsize = git_blob_rawsize(blob);

		GIT_ERROR_CHECK_BLOBSIZE(blobsize);
201
		if ((error = git_str_put(&content,
202 203 204 205 206
			git_blob_rawcontent(blob), (size_t)blobsize)) < 0)
			goto cleanup;

		break;
	}
207
	default:
208
		git_error_set(GIT_ERROR_INVALID, "unknown file source %d", source->type);
209
		return -1;
210 211
	}

212
	if ((error = git_attr_file__new(&file, entry, source)) < 0)
213 214
		goto cleanup;

215
	/* advance over a UTF8 BOM */
216 217
	content_str = git_str_cstr(&content);
	bom_offset = git_str_detect_bom(&bom, &content);
218

219
	if (bom == GIT_STR_BOM_UTF8)
220 221
		content_str += bom_offset;

222 223 224 225 226 227
	/* store the key of the attr_reader; don't bother with cache
	 * invalidation during the same attr reader session.
	 */
	if (attr_session)
		file->session_key = attr_session->key;

228
	if (parser && (error = parser(repo, file, content_str, allow_macros)) < 0) {
229
		git_attr_file__free(file);
230 231 232
		goto cleanup;
	}

233 234 235
	/* write cache breakers */
	if (nonexistent)
		file->nonexistent = 1;
236
	else if (source->type == GIT_ATTR_FILE_SOURCE_INDEX)
237
		git_oid_cpy(&file->cache_data.oid, git_blob_id(blob));
238 239
	else if (source->type == GIT_ATTR_FILE_SOURCE_HEAD)
		git_oid_cpy(&file->cache_data.oid, git_tree_id(tree));
240
	else if (source->type == GIT_ATTR_FILE_SOURCE_COMMIT)
241
		git_oid_cpy(&file->cache_data.oid, git_tree_id(tree));
242
	else if (source->type == GIT_ATTR_FILE_SOURCE_FILE)
243 244 245 246
		git_futils_filestamp_set_from_stat(&file->cache_data.stamp, &st);
	/* else always cacheable */

	*out = file;
247 248 249

cleanup:
	git_blob_free(blob);
250 251
	git_tree_entry_free(tree_entry);
	git_tree_free(tree);
252
	git_commit_free(commit);
253
	git_str_dispose(&content);
254 255

	return error;
256 257
}

258 259 260
int git_attr_file__out_of_date(
	git_repository *repo,
	git_attr_session *attr_session,
261 262
	git_attr_file *file,
	git_attr_file_source *source)
263 264 265 266
{
	if (!file)
		return 1;

267 268 269 270 271 272 273 274
	/* we are never out of date if we just created this data in the same
	 * attr_session; otherwise, nonexistent files must be invalidated
	 */
	if (attr_session && attr_session->key == file->session_key)
		return 0;
	else if (file->nonexistent)
		return 1;

275
	switch (file->source.type) {
276
	case GIT_ATTR_FILE_SOURCE_MEMORY:
277 278
		return 0;

279
	case GIT_ATTR_FILE_SOURCE_FILE:
280 281 282
		return git_futils_filestamp_check(
			&file->cache_data.stamp, file->entry->fullpath);

283
	case GIT_ATTR_FILE_SOURCE_INDEX: {
284 285 286
		int error;
		git_oid id;

287 288
		if ((error = attr_file_oid_from_index(
				&id, repo, file->entry->path)) < 0)
289 290 291 292 293
			return error;

		return (git_oid__cmp(&file->cache_data.oid, &id) != 0);
	}

294
	case GIT_ATTR_FILE_SOURCE_HEAD: {
295
		git_tree *tree = NULL;
296
		int error = git_repository_head_tree(&tree, repo);
297

298 299
		if (error < 0)
			return error;
300

301
		error = (git_oid__cmp(&file->cache_data.oid, git_tree_id(tree)) != 0);
302

303 304 305
		git_tree_free(tree);
		return error;
	}
306

307 308 309 310 311 312 313 314 315 316
	case GIT_ATTR_FILE_SOURCE_COMMIT: {
		git_commit *commit = NULL;
		git_tree *tree = NULL;
		int error;

		if ((error = git_commit_lookup(&commit, repo, source->commit_id)) < 0)
			return error;

		error = git_commit_tree(&tree, commit);
		git_commit_free(commit);
317 318

		if (error < 0)
319 320
			return error;

321
		error = (git_oid__cmp(&file->cache_data.oid, git_tree_id(tree)) != 0);
322 323 324 325 326

		git_tree_free(tree);
		return error;
	}

327
	default:
328
		git_error_set(GIT_ERROR_INVALID, "invalid file type %d", file->source.type);
329 330
		return -1;
	}
331 332 333 334 335 336 337 338 339
}

static int sort_by_hash_and_name(const void *a_raw, const void *b_raw);
static void git_attr_rule__clear(git_attr_rule *rule);
static bool parse_optimized_patterns(
	git_attr_fnmatch *spec,
	git_pool *pool,
	const char *pattern);

340
int git_attr_file__parse_buffer(
341
	git_repository *repo, git_attr_file *attrs, const char *data, bool allow_macros)
342
{
343
	const char *scan = data, *context = NULL;
344
	git_attr_rule *rule = NULL;
345
	int error = 0;
346

347
	/* If subdir file path, convert context for file paths */
348
	if (attrs->entry && git_fs_path_root(attrs->entry->path) < 0 &&
349
	    !git__suffixcmp(attrs->entry->path, "/" GIT_ATTR_FILE))
350
		context = attrs->entry->path;
351

352
	if (git_mutex_lock(&attrs->lock) < 0) {
353
		git_error_set(GIT_ERROR_OS, "failed to lock attribute file");
354 355 356
		return -1;
	}

357
	while (!error && *scan) {
358 359 360 361 362 363 364 365 366 367 368 369
		/* Allocate rule if needed, otherwise re-use previous rule */
		if (!rule) {
			rule = git__calloc(1, sizeof(*rule));
			GIT_ERROR_CHECK_ALLOC(rule);
		} else
			git_attr_rule__clear(rule);

		rule->match.flags = GIT_ATTR_FNMATCH_ALLOWNEG | GIT_ATTR_FNMATCH_ALLOWMACRO;

		/* Parse the next "pattern attr attr attr" line */
		if ((error = git_attr_fnmatch__parse(&rule->match, &attrs->pool, context, &scan)) < 0 ||
		    (error = git_attr_assignment__parse(repo, &attrs->pool, &rule->assigns, &scan)) < 0)
370
		{
371 372 373 374
			if (error != GIT_ENOTFOUND)
				goto out;
			error = 0;
			continue;
375
		}
376

377 378
		if (rule->match.flags & GIT_ATTR_FNMATCH_MACRO) {
			/* TODO: warning if macro found in file below repo root */
379 380
			if (!allow_macros)
				continue;
381 382 383 384 385 386
			if ((error = git_attr_cache__insert_macro(repo, rule)) < 0)
				goto out;
		} else if ((error = git_vector_insert(&attrs->rules, rule)) < 0)
			goto out;

		rule = NULL;
387 388
	}

389
out:
390
	git_mutex_unlock(&attrs->lock);
391
	git_attr_rule__free(rule);
392

393 394 395
	return error;
}

396
uint32_t git_attr_file__name_hash(const char *name)
397
{
398
	uint32_t h = 5381;
399
	int c;
400 401 402

	GIT_ASSERT_ARG(name);

403 404 405 406 407 408 409
	while ((c = (int)*name++) != 0)
		h = ((h << 5) + h) + c;
	return h;
}

int git_attr_file__lookup_one(
	git_attr_file *file,
410
	git_attr_path *path,
411 412 413
	const char *attr,
	const char **value)
{
414
	size_t i;
415 416 417 418 419 420 421 422 423
	git_attr_name name;
	git_attr_rule *rule;

	*value = NULL;

	name.name = attr;
	name.name_hash = git_attr_file__name_hash(attr);

	git_attr_file__foreach_matching_rule(file, path, i, rule) {
424
		size_t pos;
425

426
		if (!git_vector_bsearch(&pos, &rule->assigns, &name)) {
427 428 429 430 431 432
			*value = ((git_attr_assignment *)
					  git_vector_get(&rule->assigns, pos))->value;
			break;
		}
	}

433
	return 0;
434 435
}

436
int git_attr_file__load_standalone(git_attr_file **out, const char *path)
437
{
438
	git_str content = GIT_STR_INIT;
439
	git_attr_file_source source = { GIT_ATTR_FILE_SOURCE_FILE };
440 441
	git_attr_file *file = NULL;
	int error;
442

443 444
	if ((error = git_futils_readbuffer(&content, path)) < 0)
		goto out;
445

446 447
	/*
	 * Because the cache entry is allocated from the file's own pool, we
448 449 450
	 * don't have to free it - freeing file+pool will free cache entry, too.
	 */

451
	if ((error = git_attr_file__new(&file, NULL, &source)) < 0 ||
452
	    (error = git_attr_file__parse_buffer(NULL, file, content.ptr, true)) < 0 ||
453
	    (error = git_attr_cache__alloc_file_entry(&file->entry, NULL, NULL, path, &file->pool)) < 0)
454
		goto out;
455

456 457
	*out = file;
out:
458 459
	if (error < 0)
		git_attr_file__free(file);
460
	git_str_dispose(&content);
461 462 463

	return error;
}
464

465
bool git_attr_fnmatch__match(
466
	git_attr_fnmatch *match,
467
	git_attr_path *path)
468
{
469
	const char *relpath = path->path;
470 471
	const char *filename;
	int flags = 0;
472

473 474 475 476 477 478 479 480 481 482 483 484 485
	/*
	 * If the rule was generated in a subdirectory, we must only
	 * use it for paths inside that directory. We can thus return
	 * a non-match if the prefixes don't match.
	 */
	if (match->containing_dir) {
		if (match->flags & GIT_ATTR_FNMATCH_ICASE) {
			if (git__strncasecmp(path->path, match->containing_dir, match->containing_dir_length))
				return 0;
		} else {
			if (git__prefixcmp(path->path, match->containing_dir))
				return 0;
		}
486 487

		relpath += match->containing_dir_length;
488 489
	}

490
	if (match->flags & GIT_ATTR_FNMATCH_ICASE)
491
		flags |= WM_CASEFOLD;
492 493

	if (match->flags & GIT_ATTR_FNMATCH_FULLPATH) {
494
		filename = relpath;
495
		flags |= WM_PATHNAME;
496 497 498
	} else {
		filename = path->basename;
	}
499

500
	if ((match->flags & GIT_ATTR_FNMATCH_DIRECTORY) && !path->is_dir) {
501 502
		bool samename;

503 504 505 506 507
		/*
		 * for attribute checks or checks at the root of this match's
		 * containing_dir (or root of the repository if no containing_dir),
		 * do not match.
		 */
508
		if (!(match->flags & GIT_ATTR_FNMATCH_IGNORE) ||
509
			path->basename == relpath)
510 511
			return false;

512
		/* fail match if this is a file with same name as ignored folder */
513
		samename = (match->flags & GIT_ATTR_FNMATCH_ICASE) ?
514 515
			!strcasecmp(match->pattern, relpath) :
			!strcmp(match->pattern, relpath);
516 517 518 519

		if (samename)
			return false;

520
		return (wildmatch(match->pattern, relpath, flags) == WM_MATCH);
521 522
	}

523
	return (wildmatch(match->pattern, filename, flags) == WM_MATCH);
524 525
}

526
bool git_attr_rule__match(
527
	git_attr_rule *rule,
528
	git_attr_path *path)
529
{
530
	bool matched = git_attr_fnmatch__match(&rule->match, path);
531

532
	if (rule->match.flags & GIT_ATTR_FNMATCH_NEGATIVE)
533
		matched = !matched;
534 535 536 537 538 539 540

	return matched;
}

git_attr_assignment *git_attr_rule__lookup_assignment(
	git_attr_rule *rule, const char *name)
{
541
	size_t pos;
542 543 544 545
	git_attr_name key;
	key.name = name;
	key.name_hash = git_attr_file__name_hash(name);

546 547
	if (git_vector_bsearch(&pos, &rule->assigns, &key))
		return NULL;
548

549
	return git_vector_get(&rule->assigns, pos);
550 551 552
}

int git_attr_path__init(
553 554 555 556
	git_attr_path *info,
	const char *path,
	const char *base,
	git_dir_flag dir_flag)
557
{
558 559
	ssize_t root;

560
	/* build full path as best we can */
561
	git_str_init(&info->full, 0);
562

563
	if (git_fs_path_join_unrooted(&info->full, path, base, &root) < 0)
564 565 566
		return -1;

	info->path = info->full.ptr + root;
567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586

	/* remove trailing slashes */
	while (info->full.size > 0) {
		if (info->full.ptr[info->full.size - 1] != '/')
			break;
		info->full.size--;
	}
	info->full.ptr[info->full.size] = '\0';

	/* skip leading slashes in path */
	while (*info->path == '/')
		info->path++;

	/* find trailing basename component */
	info->basename = strrchr(info->path, '/');
	if (info->basename)
		info->basename++;
	if (!info->basename || !*info->basename)
		info->basename = info->path;

587 588 589 590 591 592 593 594 595 596 597 598
	switch (dir_flag)
	{
	case GIT_DIR_FLAG_FALSE:
		info->is_dir = 0;
		break;

	case GIT_DIR_FLAG_TRUE:
		info->is_dir = 1;
		break;

	case GIT_DIR_FLAG_UNKNOWN:
	default:
599
		info->is_dir = (int)git_fs_path_isdir(info->full.ptr);
600 601
		break;
	}
602

603
	return 0;
604 605
}

606 607
void git_attr_path__free(git_attr_path *info)
{
608
	git_str_dispose(&info->full);
609 610 611 612
	info->path = NULL;
	info->basename = NULL;
}

613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645
/*
 * From gitattributes(5):
 *
 * Patterns have the following format:
 *
 * - A blank line matches no files, so it can serve as a separator for
 *   readability.
 *
 * - A line starting with # serves as a comment.
 *
 * - An optional prefix ! which negates the pattern; any matching file
 *   excluded by a previous pattern will become included again. If a negated
 *   pattern matches, this will override lower precedence patterns sources.
 *
 * - If the pattern ends with a slash, it is removed for the purpose of the
 *   following description, but it would only find a match with a directory. In
 *   other words, foo/ will match a directory foo and paths underneath it, but
 *   will not match a regular file or a symbolic link foo (this is consistent
 *   with the way how pathspec works in general in git).
 *
 * - If the pattern does not contain a slash /, git treats it as a shell glob
 *   pattern and checks for a match against the pathname without leading
 *   directories.
 *
 * - Otherwise, git treats the pattern as a shell glob suitable for consumption
 *   by fnmatch(3) with the FNM_PATHNAME flag: wildcards in the pattern will
 *   not match a / in the pathname. For example, "Documentation/\*.html" matches
 *   "Documentation/git.html" but not "Documentation/ppc/ppc.html". A leading
 *   slash matches the beginning of the pathname; for example, "/\*.c" matches
 *   "cat-file.c" but not "mozilla-sha1/sha1.c".
 */

/*
646 647
 * Determine the length of trailing spaces. Escaped spaces do not count as
 * trailing whitespace.
648 649 650
 */
static size_t trailing_space_length(const char *p, size_t len)
{
651
	size_t n, i;
652
	for (n = len; n; n--) {
653 654 655 656 657 658 659 660 661 662 663 664 665 666
		if (p[n-1] != ' ' && p[n-1] != '\t')
			break;

		/*
		 * Count escape-characters before space. In case where it's an
		 * even number of escape characters, then the escape char itself
		 * is escaped and the whitespace is an unescaped whitespace.
		 * Otherwise, the last escape char is not escaped and the
		 * whitespace in an escaped whitespace.
		 */
		i = n;
		while (i > 1 && p[i-2] == '\\')
			i--;
		if ((n - i) % 2)
667 668 669 670 671
			break;
	}
	return len - n;
}

672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699
static size_t unescape_spaces(char *str)
{
	char *scan, *pos = str;
	bool escaped = false;

	if (!str)
		return 0;

	for (scan = str; *scan; scan++) {
		if (!escaped && *scan == '\\') {
			escaped = true;
			continue;
		}

		/* Only insert the escape character for escaped non-spaces */
		if (escaped && !git__isspace(*scan))
			*pos++ = '\\';

		*pos++ = *scan;
		escaped = false;
	}

	if (pos != scan)
		*pos = '\0';

	return (pos - str);
}

700
/*
701
 * This will return 0 if the spec was filled out,
702 703 704
 * GIT_ENOTFOUND if the fnmatch does not require matching, or
 * another error code there was an actual problem.
 */
705
int git_attr_fnmatch__parse(
706
	git_attr_fnmatch *spec,
707
	git_pool *pool,
708
	const char *context,
709 710
	const char **base)
{
711 712
	const char *pattern, *scan;
	int slash_count, allow_space;
713
	bool escaped;
714

715 716
	GIT_ASSERT_ARG(spec);
	GIT_ASSERT_ARG(base && *base);
717

718 719 720 721 722 723
	if (parse_optimized_patterns(spec, pool, *base))
		return 0;

	spec->flags = (spec->flags & GIT_ATTR_FNMATCH__INCOMING);
	allow_space = ((spec->flags & GIT_ATTR_FNMATCH_ALLOWSPACE) != 0);

724 725
	pattern = *base;

726 727 728 729 730
	while (!allow_space && git__isspace(*pattern))
		pattern++;

	if (!*pattern || *pattern == '#' || *pattern == '\n' ||
	    (*pattern == '\r' && *(pattern + 1) == '\n')) {
731 732
		*base = git__next_line(pattern);
		return GIT_ENOTFOUND;
733 734
	}

735
	if (*pattern == '[' && (spec->flags & GIT_ATTR_FNMATCH_ALLOWMACRO) != 0) {
736 737 738 739
		if (strncmp(pattern, "[attr]", 6) == 0) {
			spec->flags = spec->flags | GIT_ATTR_FNMATCH_MACRO;
			pattern += 6;
		}
740
		/* else a character range like [a-e]* which is accepted */
741 742
	}

743
	if (*pattern == '!' && (spec->flags & GIT_ATTR_FNMATCH_ALLOWNEG) != 0) {
744
		spec->flags = spec->flags | GIT_ATTR_FNMATCH_NEGATIVE;
745 746 747 748
		pattern++;
	}

	slash_count = 0;
749 750
	escaped = false;
	/* Scan until a non-escaped whitespace. */
751
	for (scan = pattern; *scan != '\0'; ++scan) {
752
		char c = *scan;
753

754 755 756 757 758 759 760
		if (c == '\\' && !escaped) {
			escaped = true;
			continue;
		} else if (git__isspace(c) && !escaped) {
			if (!allow_space || (c != ' ' && c != '\t' && c != '\r'))
				break;
		} else if (c == '/') {
761
			spec->flags = spec->flags | GIT_ATTR_FNMATCH_FULLPATH;
762
			slash_count++;
763 764

			if (slash_count == 1 && pattern == scan)
765
				pattern++;
766 767
		} else if (git__iswildcard(c) && !escaped) {
			/* remember if we see an unescaped wildcard in pattern */
768
			spec->flags = spec->flags | GIT_ATTR_FNMATCH_HASWILD;
769 770 771
		}

		escaped = false;
772 773 774
	}

	*base = scan;
775

776 777
	if ((spec->length = scan - pattern) == 0)
		return GIT_ENOTFOUND;
778

779 780 781 782 783 784 785 786 787
	/*
	 * Remove one trailing \r in case this is a CRLF delimited
	 * file, in the case of Icon\r\r\n, we still leave the first
	 * \r there to match against.
	 */
	if (pattern[spec->length - 1] == '\r')
		if (--spec->length == 0)
			return GIT_ENOTFOUND;

788
	/* Remove trailing spaces. */
789 790 791 792
	spec->length -= trailing_space_length(pattern, spec->length);

	if (spec->length == 0)
		return GIT_ENOTFOUND;
793

794 795
	if (pattern[spec->length - 1] == '/') {
		spec->length--;
796
		spec->flags = spec->flags | GIT_ATTR_FNMATCH_DIRECTORY;
797
		if (--slash_count <= 0)
798
			spec->flags = spec->flags & ~GIT_ATTR_FNMATCH_FULLPATH;
799 800
	}

801
	if (context) {
802
		char *slash = strrchr(context, '/');
803 804 805 806 807 808 809 810 811
		size_t len;
		if (slash) {
			/* include the slash for easier matching */
			len = slash - context + 1;
			spec->containing_dir = git_pool_strndup(pool, context, len);
			spec->containing_dir_length = len;
		}
	}

812
	spec->pattern = git_pool_strndup(pool, pattern, spec->length);
813 814 815

	if (!spec->pattern) {
		*base = git__next_line(pattern);
816
		return -1;
817
	} else {
818 819
		/* strip '\' that might have been used for internal whitespace */
		spec->length = unescape_spaces(spec->pattern);
820 821
	}

822
	return 0;
823 824
}

825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840
static bool parse_optimized_patterns(
	git_attr_fnmatch *spec,
	git_pool *pool,
	const char *pattern)
{
	if (!pattern[1] && (pattern[0] == '*' || pattern[0] == '.')) {
		spec->flags = GIT_ATTR_FNMATCH_MATCH_ALL;
		spec->pattern = git_pool_strndup(pool, pattern, 1);
		spec->length = 1;

		return true;
	}

	return false;
}

841 842 843 844 845 846 847 848 849 850 851 852 853
static int sort_by_hash_and_name(const void *a_raw, const void *b_raw)
{
	const git_attr_name *a = a_raw;
	const git_attr_name *b = b_raw;

	if (b->name_hash < a->name_hash)
		return 1;
	else if (b->name_hash > a->name_hash)
		return -1;
	else
		return strcmp(b->name, a->name);
}

854
static void git_attr_assignment__free(git_attr_assignment *assign)
855
{
856 857 858
	/* name and value are stored in a git_pool associated with the
	 * git_attr_file, so they do not need to be freed here
	 */
859
	assign->name = NULL;
860
	assign->value = NULL;
861 862 863
	git__free(assign);
}

864 865 866 867 868 869 870 871 872 873
static int merge_assignments(void **old_raw, void *new_raw)
{
	git_attr_assignment **old = (git_attr_assignment **)old_raw;
	git_attr_assignment *new = (git_attr_assignment *)new_raw;

	GIT_REFCOUNT_DEC(*old, git_attr_assignment__free);
	*old = new;
	return GIT_EEXISTS;
}

874 875
int git_attr_assignment__parse(
	git_repository *repo,
876
	git_pool *pool,
877 878 879
	git_vector *assigns,
	const char **base)
{
880
	int error;
881 882 883
	const char *scan = *base;
	git_attr_assignment *assign = NULL;

884
	GIT_ASSERT_ARG(assigns && !assigns->length);
885

886
	git_vector_set_cmp(assigns, sort_by_hash_and_name);
887

888
	while (*scan && *scan != '\n') {
889 890 891
		const char *name_start, *value_start;

		/* skip leading blanks */
892
		while (git__isspace(*scan) && *scan != '\n') scan++;
893 894 895 896

		/* allocate assign if needed */
		if (!assign) {
			assign = git__calloc(1, sizeof(git_attr_assignment));
897
			GIT_ERROR_CHECK_ALLOC(assign);
898
			GIT_REFCOUNT_INC(assign);
899 900 901
		}

		assign->name_hash = 5381;
902
		assign->value = git_attr__true;
903 904 905

		/* look for magic name prefixes */
		if (*scan == '-') {
906
			assign->value = git_attr__false;
907 908
			scan++;
		} else if (*scan == '!') {
909
			assign->value = git_attr__unset; /* explicit unspecified state */
910 911 912 913 914 915
			scan++;
		} else if (*scan == '#') /* comment rest of line */
			break;

		/* find the name */
		name_start = scan;
916
		while (*scan && !git__isspace(*scan) && *scan != '=') {
917 918 919 920
			assign->name_hash =
				((assign->name_hash << 5) + assign->name_hash) + *scan;
			scan++;
		}
921
		if (scan == name_start) {
922 923 924
			/* must have found lone prefix (" - ") or leading = ("=foo")
			 * or end of buffer -- advance until whitespace and continue
			 */
925
			while (*scan && !git__isspace(*scan)) scan++;
926 927 928
			continue;
		}

929
		/* allocate permanent storage for name */
930
		assign->name = git_pool_strndup(pool, name_start, scan - name_start);
931
		GIT_ERROR_CHECK_ALLOC(assign->name);
932

933 934
		/* if there is an equals sign, find the value */
		if (*scan == '=') {
935
			for (value_start = ++scan; *scan && !git__isspace(*scan); ++scan);
936 937 938

			/* if we found a value, allocate permanent storage for it */
			if (scan > value_start) {
939
				assign->value = git_pool_strndup(pool, value_start, scan - value_start);
940
				GIT_ERROR_CHECK_ALLOC(assign->value);
941 942 943
			}
		}

944
		/* expand macros (if given a repo with a macro cache) */
945
		if (repo != NULL && assign->value == git_attr__true) {
946 947
			git_attr_rule *macro =
				git_attr_cache__lookup_macro(repo, assign->name);
948 949 950 951 952

			if (macro != NULL) {
				unsigned int i;
				git_attr_assignment *massign;

953 954
				git_vector_foreach(&macro->assigns, i, massign) {
					GIT_REFCOUNT_INC(massign);
955

956 957
					error = git_vector_insert_sorted(
						assigns, massign, &merge_assignments);
Jacques Germishuys committed
958 959
					if (error < 0 && error != GIT_EEXISTS) {
						git_attr_assignment__free(assign);
960
						return error;
Jacques Germishuys committed
961
					}
962 963
				}
			}
964 965 966
		}

		/* insert allocated assign into vector */
967
		error = git_vector_insert_sorted(assigns, assign, &merge_assignments);
968 969
		if (error < 0 && error != GIT_EEXISTS)
			return error;
970 971 972 973 974

		/* clear assign since it is now "owned" by the vector */
		assign = NULL;
	}

975
	if (assign != NULL)
976
		git_attr_assignment__free(assign);
977

978
	*base = git__next_line(scan);
979

980
	return (assigns->length == 0) ? GIT_ENOTFOUND : 0;
981 982
}

Russell Belfer committed
983
static void git_attr_rule__clear(git_attr_rule *rule)
984 985 986 987 988
{
	unsigned int i;
	git_attr_assignment *assign;

	if (!rule)
989
		return;
990

991 992 993 994 995 996
	if (!(rule->match.flags & GIT_ATTR_FNMATCH_IGNORE)) {
		git_vector_foreach(&rule->assigns, i, assign)
			GIT_REFCOUNT_DEC(assign, git_attr_assignment__free);
		git_vector_free(&rule->assigns);
	}

997
	/* match.pattern is stored in a git_pool, so no need to free */
998 999 1000
	rule->match.pattern = NULL;
	rule->match.length = 0;
}
Russell Belfer committed
1001 1002 1003 1004 1005 1006 1007

void git_attr_rule__free(git_attr_rule *rule)
{
	git_attr_rule__clear(rule);
	git__free(rule);
}

1008 1009
int git_attr_session__init(git_attr_session *session, git_repository *repo)
{
1010
	GIT_ASSERT_ARG(repo);
1011

1012
	memset(session, 0, sizeof(*session));
1013
	session->key = git_atomic32_inc(&repo->attr_session_key);
1014 1015 1016

	return 0;
}
1017 1018 1019 1020 1021 1022

void git_attr_session__free(git_attr_session *session)
{
	if (!session)
		return;

1023 1024
	git_str_dispose(&session->sysdir);
	git_str_dispose(&session->tmp);
1025 1026 1027

	memset(session, 0, sizeof(git_attr_session));
}