]> git.proxmox.com Git - libgit2.git/blob - src/attr_file.c
New upstream version 1.1.0+dfsg.1
[libgit2.git] / src / attr_file.c
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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 */
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
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 */
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
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
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
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
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
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
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
949 void git_attr_rule__free(git_attr_rule *rule)
950 {
951 git_attr_rule__clear(rule);
952 git__free(rule);
953 }
954
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
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 }