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