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