1 /*
2  * Copyright (C) the libgit2 contributors. All rights reserved.
3  *
4  * This file is part of libgit2, distributed under the GNU GPL v2 with
5  * a Linking Exception. For full terms see the included COPYING file.
6  */
7 
8 #include "attr_file.h"
9 
10 #include "repository.h"
11 #include "filebuf.h"
12 #include "attrcache.h"
13 #include "buf_text.h"
14 #include "git2/blob.h"
15 #include "git2/tree.h"
16 #include "blob.h"
17 #include "index.h"
18 #include "wildmatch.h"
19 #include <ctype.h>
20 
attr_file_free(git_attr_file * file)21 static void attr_file_free(git_attr_file *file)
22 {
23 	bool unlock = !git_mutex_lock(&file->lock);
24 	git_attr_file__clear_rules(file, false);
25 	git_pool_clear(&file->pool);
26 	if (unlock)
27 		git_mutex_unlock(&file->lock);
28 	git_mutex_free(&file->lock);
29 
30 	git__memzero(file, sizeof(*file));
31 	git__free(file);
32 }
33 
git_attr_file__new(git_attr_file ** out,git_attr_file_entry * entry,git_attr_file_source source)34 int git_attr_file__new(
35 	git_attr_file **out,
36 	git_attr_file_entry *entry,
37 	git_attr_file_source source)
38 {
39 	git_attr_file *attrs = git__calloc(1, sizeof(git_attr_file));
40 	GIT_ERROR_CHECK_ALLOC(attrs);
41 
42 	if (git_mutex_init(&attrs->lock) < 0) {
43 		git_error_set(GIT_ERROR_OS, "failed to initialize lock");
44 		goto on_error;
45 	}
46 
47 	if (git_pool_init(&attrs->pool, 1) < 0)
48 		goto on_error;
49 
50 	GIT_REFCOUNT_INC(attrs);
51 	attrs->entry  = entry;
52 	attrs->source = source;
53 	*out = attrs;
54 	return 0;
55 
56 on_error:
57 	git__free(attrs);
58 	return -1;
59 }
60 
git_attr_file__clear_rules(git_attr_file * file,bool need_lock)61 int git_attr_file__clear_rules(git_attr_file *file, bool need_lock)
62 {
63 	unsigned int i;
64 	git_attr_rule *rule;
65 
66 	if (need_lock && git_mutex_lock(&file->lock) < 0) {
67 		git_error_set(GIT_ERROR_OS, "failed to lock attribute file");
68 		return -1;
69 	}
70 
71 	git_vector_foreach(&file->rules, i, rule)
72 		git_attr_rule__free(rule);
73 	git_vector_free(&file->rules);
74 
75 	if (need_lock)
76 		git_mutex_unlock(&file->lock);
77 
78 	return 0;
79 }
80 
git_attr_file__free(git_attr_file * file)81 void git_attr_file__free(git_attr_file *file)
82 {
83 	if (!file)
84 		return;
85 	GIT_REFCOUNT_DEC(file, attr_file_free);
86 }
87 
attr_file_oid_from_index(git_oid * oid,git_repository * repo,const char * path)88 static int attr_file_oid_from_index(
89 	git_oid *oid, git_repository *repo, const char *path)
90 {
91 	int error;
92 	git_index *idx;
93 	size_t pos;
94 	const git_index_entry *entry;
95 
96 	if ((error = git_repository_index__weakptr(&idx, repo)) < 0 ||
97 		(error = git_index__find_pos(&pos, idx, path, 0, 0)) < 0)
98 		return error;
99 
100 	if (!(entry = git_index_get_byindex(idx, pos)))
101 		return GIT_ENOTFOUND;
102 
103 	*oid = entry->id;
104 	return 0;
105 }
106 
git_attr_file__load(git_attr_file ** out,git_repository * repo,git_attr_session * attr_session,git_attr_file_entry * entry,git_attr_file_source source,git_attr_file_parser parser,bool allow_macros)107 int git_attr_file__load(
108 	git_attr_file **out,
109 	git_repository *repo,
110 	git_attr_session *attr_session,
111 	git_attr_file_entry *entry,
112 	git_attr_file_source source,
113 	git_attr_file_parser parser,
114 	bool allow_macros)
115 {
116 	int error = 0;
117 	git_tree *tree = NULL;
118 	git_tree_entry *tree_entry = NULL;
119 	git_blob *blob = NULL;
120 	git_buf content = GIT_BUF_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_bom_t bom;
127 	git_oid id;
128 	git_object_size_t blobsize;
129 
130 	*out = NULL;
131 
132 	switch (source) {
133 	case GIT_ATTR_FILE__IN_MEMORY:
134 		/* in-memory attribute file doesn't need data */
135 		break;
136 	case GIT_ATTR_FILE__FROM_INDEX: {
137 		if ((error = attr_file_oid_from_index(&id, repo, entry->path)) < 0 ||
138 			(error = git_blob_lookup(&blob, repo, &id)) < 0)
139 			return error;
140 
141 		/* Do not assume that data straight from the ODB is NULL-terminated;
142 		 * copy the contents of a file to a buffer to work on */
143 		blobsize = git_blob_rawsize(blob);
144 
145 		GIT_ERROR_CHECK_BLOBSIZE(blobsize);
146 		git_buf_put(&content, git_blob_rawcontent(blob), (size_t)blobsize);
147 		break;
148 	}
149 	case GIT_ATTR_FILE__FROM_FILE: {
150 		int fd = -1;
151 
152 		/* For open or read errors, pretend that we got ENOTFOUND. */
153 		/* TODO: issue warning when warning API is available */
154 
155 		if (p_stat(entry->fullpath, &st) < 0 ||
156 			S_ISDIR(st.st_mode) ||
157 			(fd = git_futils_open_ro(entry->fullpath)) < 0 ||
158 			(error = git_futils_readbuffer_fd(&content, fd, (size_t)st.st_size)) < 0)
159 			nonexistent = true;
160 
161 		if (fd >= 0)
162 			p_close(fd);
163 
164 		break;
165 	}
166 	case GIT_ATTR_FILE__FROM_HEAD: {
167 		if ((error = git_repository_head_tree(&tree, repo)) < 0 ||
168 		    (error = git_tree_entry_bypath(&tree_entry, tree, entry->path)) < 0 ||
169 		    (error = git_blob_lookup(&blob, repo, git_tree_entry_id(tree_entry))) < 0)
170 			goto cleanup;
171 
172 		/*
173 		 * Do not assume that data straight from the ODB is NULL-terminated;
174 		 * copy the contents of a file to a buffer to work on.
175 		 */
176 		blobsize = git_blob_rawsize(blob);
177 
178 		GIT_ERROR_CHECK_BLOBSIZE(blobsize);
179 		if ((error = git_buf_put(&content,
180 			git_blob_rawcontent(blob), (size_t)blobsize)) < 0)
181 			goto cleanup;
182 
183 		break;
184 	}
185 	default:
186 		git_error_set(GIT_ERROR_INVALID, "unknown file source %d", source);
187 		return -1;
188 	}
189 
190 	if ((error = git_attr_file__new(&file, entry, source)) < 0)
191 		goto cleanup;
192 
193 	/* advance over a UTF8 BOM */
194 	content_str = git_buf_cstr(&content);
195 	bom_offset = git_buf_text_detect_bom(&bom, &content);
196 
197 	if (bom == GIT_BOM_UTF8)
198 		content_str += bom_offset;
199 
200 	/* store the key of the attr_reader; don't bother with cache
201 	 * invalidation during the same attr reader session.
202 	 */
203 	if (attr_session)
204 		file->session_key = attr_session->key;
205 
206 	if (parser && (error = parser(repo, file, content_str, allow_macros)) < 0) {
207 		git_attr_file__free(file);
208 		goto cleanup;
209 	}
210 
211 	/* write cache breakers */
212 	if (nonexistent)
213 		file->nonexistent = 1;
214 	else if (source == GIT_ATTR_FILE__FROM_INDEX)
215 		git_oid_cpy(&file->cache_data.oid, git_blob_id(blob));
216 	else if (source == GIT_ATTR_FILE__FROM_HEAD)
217 		git_oid_cpy(&file->cache_data.oid, git_tree_id(tree));
218 	else if (source == GIT_ATTR_FILE__FROM_FILE)
219 		git_futils_filestamp_set_from_stat(&file->cache_data.stamp, &st);
220 	/* else always cacheable */
221 
222 	*out = file;
223 
224 cleanup:
225 	git_blob_free(blob);
226 	git_tree_entry_free(tree_entry);
227 	git_tree_free(tree);
228 	git_buf_dispose(&content);
229 
230 	return error;
231 }
232 
git_attr_file__out_of_date(git_repository * repo,git_attr_session * attr_session,git_attr_file * file)233 int git_attr_file__out_of_date(
234 	git_repository *repo,
235 	git_attr_session *attr_session,
236 	git_attr_file *file)
237 {
238 	if (!file)
239 		return 1;
240 
241 	/* we are never out of date if we just created this data in the same
242 	 * attr_session; otherwise, nonexistent files must be invalidated
243 	 */
244 	if (attr_session && attr_session->key == file->session_key)
245 		return 0;
246 	else if (file->nonexistent)
247 		return 1;
248 
249 	switch (file->source) {
250 	case GIT_ATTR_FILE__IN_MEMORY:
251 		return 0;
252 
253 	case GIT_ATTR_FILE__FROM_FILE:
254 		return git_futils_filestamp_check(
255 			&file->cache_data.stamp, file->entry->fullpath);
256 
257 	case GIT_ATTR_FILE__FROM_INDEX: {
258 		int error;
259 		git_oid id;
260 
261 		if ((error = attr_file_oid_from_index(
262 				&id, repo, file->entry->path)) < 0)
263 			return error;
264 
265 		return (git_oid__cmp(&file->cache_data.oid, &id) != 0);
266 	}
267 
268 	case GIT_ATTR_FILE__FROM_HEAD: {
269 		git_tree *tree;
270 		int error;
271 
272 		if ((error = git_repository_head_tree(&tree, repo)) < 0)
273 			return error;
274 
275 		error = git_oid__cmp(&file->cache_data.oid, git_tree_id(tree));
276 
277 		git_tree_free(tree);
278 		return error;
279 	}
280 
281 	default:
282 		git_error_set(GIT_ERROR_INVALID, "invalid file type %d", file->source);
283 		return -1;
284 	}
285 }
286 
287 static int sort_by_hash_and_name(const void *a_raw, const void *b_raw);
288 static void git_attr_rule__clear(git_attr_rule *rule);
289 static bool parse_optimized_patterns(
290 	git_attr_fnmatch *spec,
291 	git_pool *pool,
292 	const char *pattern);
293 
git_attr_file__parse_buffer(git_repository * repo,git_attr_file * attrs,const char * data,bool allow_macros)294 int git_attr_file__parse_buffer(
295 	git_repository *repo, git_attr_file *attrs, const char *data, bool allow_macros)
296 {
297 	const char *scan = data, *context = NULL;
298 	git_attr_rule *rule = NULL;
299 	int error = 0;
300 
301 	/* If subdir file path, convert context for file paths */
302 	if (attrs->entry && git_path_root(attrs->entry->path) < 0 &&
303 	    !git__suffixcmp(attrs->entry->path, "/" GIT_ATTR_FILE))
304 		context = attrs->entry->path;
305 
306 	if (git_mutex_lock(&attrs->lock) < 0) {
307 		git_error_set(GIT_ERROR_OS, "failed to lock attribute file");
308 		return -1;
309 	}
310 
311 	while (!error && *scan) {
312 		/* Allocate rule if needed, otherwise re-use previous rule */
313 		if (!rule) {
314 			rule = git__calloc(1, sizeof(*rule));
315 			GIT_ERROR_CHECK_ALLOC(rule);
316 		} else
317 			git_attr_rule__clear(rule);
318 
319 		rule->match.flags = GIT_ATTR_FNMATCH_ALLOWNEG | GIT_ATTR_FNMATCH_ALLOWMACRO;
320 
321 		/* Parse the next "pattern attr attr attr" line */
322 		if ((error = git_attr_fnmatch__parse(&rule->match, &attrs->pool, context, &scan)) < 0 ||
323 		    (error = git_attr_assignment__parse(repo, &attrs->pool, &rule->assigns, &scan)) < 0)
324 		{
325 			if (error != GIT_ENOTFOUND)
326 				goto out;
327 			error = 0;
328 			continue;
329 		}
330 
331 		if (rule->match.flags & GIT_ATTR_FNMATCH_MACRO) {
332 			/* TODO: warning if macro found in file below repo root */
333 			if (!allow_macros)
334 				continue;
335 			if ((error = git_attr_cache__insert_macro(repo, rule)) < 0)
336 				goto out;
337 		} else if ((error = git_vector_insert(&attrs->rules, rule)) < 0)
338 			goto out;
339 
340 		rule = NULL;
341 	}
342 
343 out:
344 	git_mutex_unlock(&attrs->lock);
345 	git_attr_rule__free(rule);
346 
347 	return error;
348 }
349 
git_attr_file__name_hash(const char * name)350 uint32_t git_attr_file__name_hash(const char *name)
351 {
352 	uint32_t h = 5381;
353 	int c;
354 	assert(name);
355 	while ((c = (int)*name++) != 0)
356 		h = ((h << 5) + h) + c;
357 	return h;
358 }
359 
git_attr_file__lookup_one(git_attr_file * file,git_attr_path * path,const char * attr,const char ** value)360 int git_attr_file__lookup_one(
361 	git_attr_file *file,
362 	git_attr_path *path,
363 	const char *attr,
364 	const char **value)
365 {
366 	size_t i;
367 	git_attr_name name;
368 	git_attr_rule *rule;
369 
370 	*value = NULL;
371 
372 	name.name = attr;
373 	name.name_hash = git_attr_file__name_hash(attr);
374 
375 	git_attr_file__foreach_matching_rule(file, path, i, rule) {
376 		size_t pos;
377 
378 		if (!git_vector_bsearch(&pos, &rule->assigns, &name)) {
379 			*value = ((git_attr_assignment *)
380 					  git_vector_get(&rule->assigns, pos))->value;
381 			break;
382 		}
383 	}
384 
385 	return 0;
386 }
387 
git_attr_file__load_standalone(git_attr_file ** out,const char * path)388 int git_attr_file__load_standalone(git_attr_file **out, const char *path)
389 {
390 	git_buf content = GIT_BUF_INIT;
391 	git_attr_file *file = NULL;
392 	int error;
393 
394 	if ((error = git_futils_readbuffer(&content, path)) < 0)
395 		goto out;
396 
397 	/*
398 	 * Because the cache entry is allocated from the file's own pool, we
399 	 * don't have to free it - freeing file+pool will free cache entry, too.
400 	 */
401 
402 	if ((error = git_attr_file__new(&file, NULL, GIT_ATTR_FILE__FROM_FILE)) < 0 ||
403 	    (error = git_attr_file__parse_buffer(NULL, file, content.ptr, true)) < 0 ||
404 	    (error = git_attr_cache__alloc_file_entry(&file->entry, NULL, path, &file->pool)) < 0)
405 		goto out;
406 
407 	*out = file;
408 out:
409 	if (error < 0)
410 		git_attr_file__free(file);
411 	git_buf_dispose(&content);
412 
413 	return error;
414 }
415 
git_attr_fnmatch__match(git_attr_fnmatch * match,git_attr_path * path)416 bool git_attr_fnmatch__match(
417 	git_attr_fnmatch *match,
418 	git_attr_path *path)
419 {
420 	const char *relpath = path->path;
421 	const char *filename;
422 	int flags = 0;
423 
424 	/*
425 	 * If the rule was generated in a subdirectory, we must only
426 	 * use it for paths inside that directory. We can thus return
427 	 * a non-match if the prefixes don't match.
428 	 */
429 	if (match->containing_dir) {
430 		if (match->flags & GIT_ATTR_FNMATCH_ICASE) {
431 			if (git__strncasecmp(path->path, match->containing_dir, match->containing_dir_length))
432 				return 0;
433 		} else {
434 			if (git__prefixcmp(path->path, match->containing_dir))
435 				return 0;
436 		}
437 
438 		relpath += match->containing_dir_length;
439 	}
440 
441 	if (match->flags & GIT_ATTR_FNMATCH_ICASE)
442 		flags |= WM_CASEFOLD;
443 
444 	if (match->flags & GIT_ATTR_FNMATCH_FULLPATH) {
445 		filename = relpath;
446 		flags |= WM_PATHNAME;
447 	} else {
448 		filename = path->basename;
449 	}
450 
451 	if ((match->flags & GIT_ATTR_FNMATCH_DIRECTORY) && !path->is_dir) {
452 		bool samename;
453 
454 		/*
455 		 * for attribute checks or checks at the root of this match's
456 		 * containing_dir (or root of the repository if no containing_dir),
457 		 * do not match.
458 		 */
459 		if (!(match->flags & GIT_ATTR_FNMATCH_IGNORE) ||
460 			path->basename == relpath)
461 			return false;
462 
463 		/* fail match if this is a file with same name as ignored folder */
464 		samename = (match->flags & GIT_ATTR_FNMATCH_ICASE) ?
465 			!strcasecmp(match->pattern, relpath) :
466 			!strcmp(match->pattern, relpath);
467 
468 		if (samename)
469 			return false;
470 
471 		return (wildmatch(match->pattern, relpath, flags) == WM_MATCH);
472 	}
473 
474 	return (wildmatch(match->pattern, filename, flags) == WM_MATCH);
475 }
476 
git_attr_rule__match(git_attr_rule * rule,git_attr_path * path)477 bool git_attr_rule__match(
478 	git_attr_rule *rule,
479 	git_attr_path *path)
480 {
481 	bool matched = git_attr_fnmatch__match(&rule->match, path);
482 
483 	if (rule->match.flags & GIT_ATTR_FNMATCH_NEGATIVE)
484 		matched = !matched;
485 
486 	return matched;
487 }
488 
git_attr_rule__lookup_assignment(git_attr_rule * rule,const char * name)489 git_attr_assignment *git_attr_rule__lookup_assignment(
490 	git_attr_rule *rule, const char *name)
491 {
492 	size_t pos;
493 	git_attr_name key;
494 	key.name = name;
495 	key.name_hash = git_attr_file__name_hash(name);
496 
497 	if (git_vector_bsearch(&pos, &rule->assigns, &key))
498 		return NULL;
499 
500 	return git_vector_get(&rule->assigns, pos);
501 }
502 
git_attr_path__init(git_attr_path * info,const char * path,const char * base,git_dir_flag dir_flag)503 int git_attr_path__init(
504 	git_attr_path *info, const char *path, const char *base, git_dir_flag dir_flag)
505 {
506 	ssize_t root;
507 
508 	/* build full path as best we can */
509 	git_buf_init(&info->full, 0);
510 
511 	if (git_path_join_unrooted(&info->full, path, base, &root) < 0)
512 		return -1;
513 
514 	info->path = info->full.ptr + root;
515 
516 	/* remove trailing slashes */
517 	while (info->full.size > 0) {
518 		if (info->full.ptr[info->full.size - 1] != '/')
519 			break;
520 		info->full.size--;
521 	}
522 	info->full.ptr[info->full.size] = '\0';
523 
524 	/* skip leading slashes in path */
525 	while (*info->path == '/')
526 		info->path++;
527 
528 	/* find trailing basename component */
529 	info->basename = strrchr(info->path, '/');
530 	if (info->basename)
531 		info->basename++;
532 	if (!info->basename || !*info->basename)
533 		info->basename = info->path;
534 
535 	switch (dir_flag)
536 	{
537 	case GIT_DIR_FLAG_FALSE:
538 		info->is_dir = 0;
539 		break;
540 
541 	case GIT_DIR_FLAG_TRUE:
542 		info->is_dir = 1;
543 		break;
544 
545 	case GIT_DIR_FLAG_UNKNOWN:
546 	default:
547 		info->is_dir = (int)git_path_isdir(info->full.ptr);
548 		break;
549 	}
550 
551 	return 0;
552 }
553 
git_attr_path__free(git_attr_path * info)554 void git_attr_path__free(git_attr_path *info)
555 {
556 	git_buf_dispose(&info->full);
557 	info->path = NULL;
558 	info->basename = NULL;
559 }
560 
561 /*
562  * From gitattributes(5):
563  *
564  * Patterns have the following format:
565  *
566  * - A blank line matches no files, so it can serve as a separator for
567  *   readability.
568  *
569  * - A line starting with # serves as a comment.
570  *
571  * - An optional prefix ! which negates the pattern; any matching file
572  *   excluded by a previous pattern will become included again. If a negated
573  *   pattern matches, this will override lower precedence patterns sources.
574  *
575  * - If the pattern ends with a slash, it is removed for the purpose of the
576  *   following description, but it would only find a match with a directory. In
577  *   other words, foo/ will match a directory foo and paths underneath it, but
578  *   will not match a regular file or a symbolic link foo (this is consistent
579  *   with the way how pathspec works in general in git).
580  *
581  * - If the pattern does not contain a slash /, git treats it as a shell glob
582  *   pattern and checks for a match against the pathname without leading
583  *   directories.
584  *
585  * - Otherwise, git treats the pattern as a shell glob suitable for consumption
586  *   by fnmatch(3) with the FNM_PATHNAME flag: wildcards in the pattern will
587  *   not match a / in the pathname. For example, "Documentation/\*.html" matches
588  *   "Documentation/git.html" but not "Documentation/ppc/ppc.html". A leading
589  *   slash matches the beginning of the pathname; for example, "/\*.c" matches
590  *   "cat-file.c" but not "mozilla-sha1/sha1.c".
591  */
592 
593 /*
594  * Determine the length of trailing spaces. Escaped spaces do not count as
595  * trailing whitespace.
596  */
trailing_space_length(const char * p,size_t len)597 static size_t trailing_space_length(const char *p, size_t len)
598 {
599 	size_t n, i;
600 	for (n = len; n; n--) {
601 		if (p[n-1] != ' ' && p[n-1] != '\t')
602 			break;
603 
604 		/*
605 		 * Count escape-characters before space. In case where it's an
606 		 * even number of escape characters, then the escape char itself
607 		 * is escaped and the whitespace is an unescaped whitespace.
608 		 * Otherwise, the last escape char is not escaped and the
609 		 * whitespace in an escaped whitespace.
610 		 */
611 		i = n;
612 		while (i > 1 && p[i-2] == '\\')
613 			i--;
614 		if ((n - i) % 2)
615 			break;
616 	}
617 	return len - n;
618 }
619 
unescape_spaces(char * str)620 static size_t unescape_spaces(char *str)
621 {
622 	char *scan, *pos = str;
623 	bool escaped = false;
624 
625 	if (!str)
626 		return 0;
627 
628 	for (scan = str; *scan; scan++) {
629 		if (!escaped && *scan == '\\') {
630 			escaped = true;
631 			continue;
632 		}
633 
634 		/* Only insert the escape character for escaped non-spaces */
635 		if (escaped && !git__isspace(*scan))
636 			*pos++ = '\\';
637 
638 		*pos++ = *scan;
639 		escaped = false;
640 	}
641 
642 	if (pos != scan)
643 		*pos = '\0';
644 
645 	return (pos - str);
646 }
647 
648 /*
649  * This will return 0 if the spec was filled out,
650  * GIT_ENOTFOUND if the fnmatch does not require matching, or
651  * another error code there was an actual problem.
652  */
git_attr_fnmatch__parse(git_attr_fnmatch * spec,git_pool * pool,const char * context,const char ** base)653 int git_attr_fnmatch__parse(
654 	git_attr_fnmatch *spec,
655 	git_pool *pool,
656 	const char *context,
657 	const char **base)
658 {
659 	const char *pattern, *scan;
660 	int slash_count, allow_space;
661 	bool escaped;
662 
663 	assert(spec && base && *base);
664 
665 	if (parse_optimized_patterns(spec, pool, *base))
666 		return 0;
667 
668 	spec->flags = (spec->flags & GIT_ATTR_FNMATCH__INCOMING);
669 	allow_space = ((spec->flags & GIT_ATTR_FNMATCH_ALLOWSPACE) != 0);
670 
671 	pattern = *base;
672 
673 	while (!allow_space && git__isspace(*pattern))
674 		pattern++;
675 
676 	if (!*pattern || *pattern == '#' || *pattern == '\n' ||
677 	    (*pattern == '\r' && *(pattern + 1) == '\n')) {
678 		*base = git__next_line(pattern);
679 		return GIT_ENOTFOUND;
680 	}
681 
682 	if (*pattern == '[' && (spec->flags & GIT_ATTR_FNMATCH_ALLOWMACRO) != 0) {
683 		if (strncmp(pattern, "[attr]", 6) == 0) {
684 			spec->flags = spec->flags | GIT_ATTR_FNMATCH_MACRO;
685 			pattern += 6;
686 		}
687 		/* else a character range like [a-e]* which is accepted */
688 	}
689 
690 	if (*pattern == '!' && (spec->flags & GIT_ATTR_FNMATCH_ALLOWNEG) != 0) {
691 		spec->flags = spec->flags | GIT_ATTR_FNMATCH_NEGATIVE;
692 		pattern++;
693 	}
694 
695 	slash_count = 0;
696 	escaped = false;
697 	/* Scan until a non-escaped whitespace. */
698 	for (scan = pattern; *scan != '\0'; ++scan) {
699 		char c = *scan;
700 
701 		if (c == '\\' && !escaped) {
702 			escaped = true;
703 			continue;
704 		} else if (git__isspace(c) && !escaped) {
705 			if (!allow_space || (c != ' ' && c != '\t' && c != '\r'))
706 				break;
707 		} else if (c == '/') {
708 			spec->flags = spec->flags | GIT_ATTR_FNMATCH_FULLPATH;
709 			slash_count++;
710 
711 			if (slash_count == 1 && pattern == scan)
712 				pattern++;
713 		} else if (git__iswildcard(c) && !escaped) {
714 			/* remember if we see an unescaped wildcard in pattern */
715 			spec->flags = spec->flags | GIT_ATTR_FNMATCH_HASWILD;
716 		}
717 
718 		escaped = false;
719 	}
720 
721 	*base = scan;
722 
723 	if ((spec->length = scan - pattern) == 0)
724 		return GIT_ENOTFOUND;
725 
726 	/*
727 	 * Remove one trailing \r in case this is a CRLF delimited
728 	 * file, in the case of Icon\r\r\n, we still leave the first
729 	 * \r there to match against.
730 	 */
731 	if (pattern[spec->length - 1] == '\r')
732 		if (--spec->length == 0)
733 			return GIT_ENOTFOUND;
734 
735 	/* Remove trailing spaces. */
736 	spec->length -= trailing_space_length(pattern, spec->length);
737 
738 	if (spec->length == 0)
739 		return GIT_ENOTFOUND;
740 
741 	if (pattern[spec->length - 1] == '/') {
742 		spec->length--;
743 		spec->flags = spec->flags | GIT_ATTR_FNMATCH_DIRECTORY;
744 		if (--slash_count <= 0)
745 			spec->flags = spec->flags & ~GIT_ATTR_FNMATCH_FULLPATH;
746 	}
747 
748 	if (context) {
749 		char *slash = strrchr(context, '/');
750 		size_t len;
751 		if (slash) {
752 			/* include the slash for easier matching */
753 			len = slash - context + 1;
754 			spec->containing_dir = git_pool_strndup(pool, context, len);
755 			spec->containing_dir_length = len;
756 		}
757 	}
758 
759 	spec->pattern = git_pool_strndup(pool, pattern, spec->length);
760 
761 	if (!spec->pattern) {
762 		*base = git__next_line(pattern);
763 		return -1;
764 	} else {
765 		/* strip '\' that might have been used for internal whitespace */
766 		spec->length = unescape_spaces(spec->pattern);
767 	}
768 
769 	return 0;
770 }
771 
parse_optimized_patterns(git_attr_fnmatch * spec,git_pool * pool,const char * pattern)772 static bool parse_optimized_patterns(
773 	git_attr_fnmatch *spec,
774 	git_pool *pool,
775 	const char *pattern)
776 {
777 	if (!pattern[1] && (pattern[0] == '*' || pattern[0] == '.')) {
778 		spec->flags = GIT_ATTR_FNMATCH_MATCH_ALL;
779 		spec->pattern = git_pool_strndup(pool, pattern, 1);
780 		spec->length = 1;
781 
782 		return true;
783 	}
784 
785 	return false;
786 }
787 
sort_by_hash_and_name(const void * a_raw,const void * b_raw)788 static int sort_by_hash_and_name(const void *a_raw, const void *b_raw)
789 {
790 	const git_attr_name *a = a_raw;
791 	const git_attr_name *b = b_raw;
792 
793 	if (b->name_hash < a->name_hash)
794 		return 1;
795 	else if (b->name_hash > a->name_hash)
796 		return -1;
797 	else
798 		return strcmp(b->name, a->name);
799 }
800 
git_attr_assignment__free(git_attr_assignment * assign)801 static void git_attr_assignment__free(git_attr_assignment *assign)
802 {
803 	/* name and value are stored in a git_pool associated with the
804 	 * git_attr_file, so they do not need to be freed here
805 	 */
806 	assign->name = NULL;
807 	assign->value = NULL;
808 	git__free(assign);
809 }
810 
merge_assignments(void ** old_raw,void * new_raw)811 static int merge_assignments(void **old_raw, void *new_raw)
812 {
813 	git_attr_assignment **old = (git_attr_assignment **)old_raw;
814 	git_attr_assignment *new = (git_attr_assignment *)new_raw;
815 
816 	GIT_REFCOUNT_DEC(*old, git_attr_assignment__free);
817 	*old = new;
818 	return GIT_EEXISTS;
819 }
820 
git_attr_assignment__parse(git_repository * repo,git_pool * pool,git_vector * assigns,const char ** base)821 int git_attr_assignment__parse(
822 	git_repository *repo,
823 	git_pool *pool,
824 	git_vector *assigns,
825 	const char **base)
826 {
827 	int error;
828 	const char *scan = *base;
829 	git_attr_assignment *assign = NULL;
830 
831 	assert(assigns && !assigns->length);
832 
833 	git_vector_set_cmp(assigns, sort_by_hash_and_name);
834 
835 	while (*scan && *scan != '\n') {
836 		const char *name_start, *value_start;
837 
838 		/* skip leading blanks */
839 		while (git__isspace(*scan) && *scan != '\n') scan++;
840 
841 		/* allocate assign if needed */
842 		if (!assign) {
843 			assign = git__calloc(1, sizeof(git_attr_assignment));
844 			GIT_ERROR_CHECK_ALLOC(assign);
845 			GIT_REFCOUNT_INC(assign);
846 		}
847 
848 		assign->name_hash = 5381;
849 		assign->value = git_attr__true;
850 
851 		/* look for magic name prefixes */
852 		if (*scan == '-') {
853 			assign->value = git_attr__false;
854 			scan++;
855 		} else if (*scan == '!') {
856 			assign->value = git_attr__unset; /* explicit unspecified state */
857 			scan++;
858 		} else if (*scan == '#') /* comment rest of line */
859 			break;
860 
861 		/* find the name */
862 		name_start = scan;
863 		while (*scan && !git__isspace(*scan) && *scan != '=') {
864 			assign->name_hash =
865 				((assign->name_hash << 5) + assign->name_hash) + *scan;
866 			scan++;
867 		}
868 		if (scan == name_start) {
869 			/* must have found lone prefix (" - ") or leading = ("=foo")
870 			 * or end of buffer -- advance until whitespace and continue
871 			 */
872 			while (*scan && !git__isspace(*scan)) scan++;
873 			continue;
874 		}
875 
876 		/* allocate permanent storage for name */
877 		assign->name = git_pool_strndup(pool, name_start, scan - name_start);
878 		GIT_ERROR_CHECK_ALLOC(assign->name);
879 
880 		/* if there is an equals sign, find the value */
881 		if (*scan == '=') {
882 			for (value_start = ++scan; *scan && !git__isspace(*scan); ++scan);
883 
884 			/* if we found a value, allocate permanent storage for it */
885 			if (scan > value_start) {
886 				assign->value = git_pool_strndup(pool, value_start, scan - value_start);
887 				GIT_ERROR_CHECK_ALLOC(assign->value);
888 			}
889 		}
890 
891 		/* expand macros (if given a repo with a macro cache) */
892 		if (repo != NULL && assign->value == git_attr__true) {
893 			git_attr_rule *macro =
894 				git_attr_cache__lookup_macro(repo, assign->name);
895 
896 			if (macro != NULL) {
897 				unsigned int i;
898 				git_attr_assignment *massign;
899 
900 				git_vector_foreach(&macro->assigns, i, massign) {
901 					GIT_REFCOUNT_INC(massign);
902 
903 					error = git_vector_insert_sorted(
904 						assigns, massign, &merge_assignments);
905 					if (error < 0 && error != GIT_EEXISTS) {
906 						git_attr_assignment__free(assign);
907 						return error;
908 					}
909 				}
910 			}
911 		}
912 
913 		/* insert allocated assign into vector */
914 		error = git_vector_insert_sorted(assigns, assign, &merge_assignments);
915 		if (error < 0 && error != GIT_EEXISTS)
916 			return error;
917 
918 		/* clear assign since it is now "owned" by the vector */
919 		assign = NULL;
920 	}
921 
922 	if (assign != NULL)
923 		git_attr_assignment__free(assign);
924 
925 	*base = git__next_line(scan);
926 
927 	return (assigns->length == 0) ? GIT_ENOTFOUND : 0;
928 }
929 
git_attr_rule__clear(git_attr_rule * rule)930 static void git_attr_rule__clear(git_attr_rule *rule)
931 {
932 	unsigned int i;
933 	git_attr_assignment *assign;
934 
935 	if (!rule)
936 		return;
937 
938 	if (!(rule->match.flags & GIT_ATTR_FNMATCH_IGNORE)) {
939 		git_vector_foreach(&rule->assigns, i, assign)
940 			GIT_REFCOUNT_DEC(assign, git_attr_assignment__free);
941 		git_vector_free(&rule->assigns);
942 	}
943 
944 	/* match.pattern is stored in a git_pool, so no need to free */
945 	rule->match.pattern = NULL;
946 	rule->match.length = 0;
947 }
948 
git_attr_rule__free(git_attr_rule * rule)949 void git_attr_rule__free(git_attr_rule *rule)
950 {
951 	git_attr_rule__clear(rule);
952 	git__free(rule);
953 }
954 
git_attr_session__init(git_attr_session * session,git_repository * repo)955 int git_attr_session__init(git_attr_session *session, git_repository *repo)
956 {
957 	assert(repo);
958 
959 	memset(session, 0, sizeof(*session));
960 	session->key = git_atomic_inc(&repo->attr_session_key);
961 
962 	return 0;
963 }
964 
git_attr_session__free(git_attr_session * session)965 void git_attr_session__free(git_attr_session *session)
966 {
967 	if (!session)
968 		return;
969 
970 	git_buf_dispose(&session->sysdir);
971 	git_buf_dispose(&session->tmp);
972 
973 	memset(session, 0, sizeof(git_attr_session));
974 }
975