]> git.proxmox.com Git - libgit2.git/blob - src/attr_file.c
status: fix handling of filenames with special prefixes
[libgit2.git] / src / attr_file.c
1 #include "common.h"
2 #include "repository.h"
3 #include "filebuf.h"
4 #include "attr.h"
5 #include "git2/blob.h"
6 #include "git2/tree.h"
7 #include <ctype.h>
8
9 static int sort_by_hash_and_name(const void *a_raw, const void *b_raw);
10 static void git_attr_rule__clear(git_attr_rule *rule);
11 static bool parse_optimized_patterns(
12 git_attr_fnmatch *spec,
13 git_pool *pool,
14 const char *pattern);
15
16 int git_attr_file__new(
17 git_attr_file **attrs_ptr,
18 git_attr_file_source from,
19 const char *path,
20 git_pool *pool)
21 {
22 git_attr_file *attrs = NULL;
23
24 attrs = git__calloc(1, sizeof(git_attr_file));
25 GITERR_CHECK_ALLOC(attrs);
26
27 if (pool)
28 attrs->pool = pool;
29 else {
30 attrs->pool = git__calloc(1, sizeof(git_pool));
31 if (!attrs->pool || git_pool_init(attrs->pool, 1, 0) < 0)
32 goto fail;
33 attrs->pool_is_allocated = true;
34 }
35
36 if (path) {
37 size_t len = strlen(path);
38
39 attrs->key = git_pool_malloc(attrs->pool, (uint32_t)len + 3);
40 GITERR_CHECK_ALLOC(attrs->key);
41
42 attrs->key[0] = '0' + from;
43 attrs->key[1] = '#';
44 memcpy(&attrs->key[2], path, len);
45 attrs->key[len + 2] = '\0';
46 }
47
48 if (git_vector_init(&attrs->rules, 4, NULL) < 0)
49 goto fail;
50
51 *attrs_ptr = attrs;
52 return 0;
53
54 fail:
55 git_attr_file__free(attrs);
56 attrs_ptr = NULL;
57 return -1;
58 }
59
60 int git_attr_file__parse_buffer(
61 git_repository *repo, void *parsedata, const char *buffer, git_attr_file *attrs)
62 {
63 int error = 0;
64 const char *scan = NULL;
65 char *context = NULL;
66 git_attr_rule *rule = NULL;
67
68 GIT_UNUSED(parsedata);
69
70 assert(buffer && attrs);
71
72 scan = buffer;
73
74 /* if subdir file path, convert context for file paths */
75 if (attrs->key && git__suffixcmp(attrs->key, "/" GIT_ATTR_FILE) == 0) {
76 context = attrs->key + 2;
77 context[strlen(context) - strlen(GIT_ATTR_FILE)] = '\0';
78 }
79
80 while (!error && *scan) {
81 /* allocate rule if needed */
82 if (!rule && !(rule = git__calloc(1, sizeof(git_attr_rule)))) {
83 error = -1;
84 break;
85 }
86
87 /* parse the next "pattern attr attr attr" line */
88 if (!(error = git_attr_fnmatch__parse_gitattr_format(
89 &rule->match, attrs->pool, context, &scan)) &&
90 !(error = git_attr_assignment__parse(
91 repo, attrs->pool, &rule->assigns, &scan)))
92 {
93 if (rule->match.flags & GIT_ATTR_FNMATCH_MACRO)
94 /* should generate error/warning if this is coming from any
95 * file other than .gitattributes at repo root.
96 */
97 error = git_attr_cache__insert_macro(repo, rule);
98 else
99 error = git_vector_insert(&attrs->rules, rule);
100 }
101
102 /* if the rule wasn't a pattern, on to the next */
103 if (error < 0) {
104 git_attr_rule__clear(rule); /* reset rule contents */
105 if (error == GIT_ENOTFOUND)
106 error = 0;
107 } else {
108 rule = NULL; /* vector now "owns" the rule */
109 }
110 }
111
112 git_attr_rule__free(rule);
113
114 /* restore file path used for context */
115 if (context)
116 context[strlen(context)] = '.'; /* first char of GIT_ATTR_FILE */
117
118 return error;
119 }
120
121 int git_attr_file__new_and_load(
122 git_attr_file **attrs_ptr,
123 const char *path)
124 {
125 int error;
126 git_buf content = GIT_BUF_INIT;
127
128 if ((error = git_attr_file__new(attrs_ptr, 0, path, NULL)) < 0)
129 return error;
130
131 if (!(error = git_futils_readbuffer(&content, path)))
132 error = git_attr_file__parse_buffer(
133 NULL, NULL, git_buf_cstr(&content), *attrs_ptr);
134
135 git_buf_free(&content);
136
137 if (error) {
138 git_attr_file__free(*attrs_ptr);
139 *attrs_ptr = NULL;
140 }
141
142 return error;
143 }
144
145 void git_attr_file__clear_rules(git_attr_file *file)
146 {
147 unsigned int i;
148 git_attr_rule *rule;
149
150 git_vector_foreach(&file->rules, i, rule)
151 git_attr_rule__free(rule);
152
153 git_vector_free(&file->rules);
154 }
155
156 void git_attr_file__free(git_attr_file *file)
157 {
158 if (!file)
159 return;
160
161 git_attr_file__clear_rules(file);
162
163 if (file->pool_is_allocated) {
164 git_pool_clear(file->pool);
165 git__free(file->pool);
166 }
167 file->pool = NULL;
168
169 git__free(file);
170 }
171
172 uint32_t git_attr_file__name_hash(const char *name)
173 {
174 uint32_t h = 5381;
175 int c;
176 assert(name);
177 while ((c = (int)*name++) != 0)
178 h = ((h << 5) + h) + c;
179 return h;
180 }
181
182
183 int git_attr_file__lookup_one(
184 git_attr_file *file,
185 const git_attr_path *path,
186 const char *attr,
187 const char **value)
188 {
189 size_t i;
190 git_attr_name name;
191 git_attr_rule *rule;
192
193 *value = NULL;
194
195 name.name = attr;
196 name.name_hash = git_attr_file__name_hash(attr);
197
198 git_attr_file__foreach_matching_rule(file, path, i, rule) {
199 size_t pos;
200
201 if (!git_vector_bsearch(&pos, &rule->assigns, &name)) {
202 *value = ((git_attr_assignment *)
203 git_vector_get(&rule->assigns, pos))->value;
204 break;
205 }
206 }
207
208 return 0;
209 }
210
211
212 bool git_attr_fnmatch__match(
213 git_attr_fnmatch *match,
214 const git_attr_path *path)
215 {
216 int fnm;
217 int icase_flags = (match->flags & GIT_ATTR_FNMATCH_ICASE) ? FNM_CASEFOLD : 0;
218
219 if (match->flags & GIT_ATTR_FNMATCH_DIRECTORY && !path->is_dir)
220 return false;
221
222 if (match->flags & GIT_ATTR_FNMATCH_FULLPATH)
223 fnm = p_fnmatch(match->pattern, path->path, FNM_PATHNAME | icase_flags);
224 else if (path->is_dir)
225 fnm = p_fnmatch(match->pattern, path->basename, FNM_LEADING_DIR | icase_flags);
226 else
227 fnm = p_fnmatch(match->pattern, path->basename, icase_flags);
228
229 return (fnm == FNM_NOMATCH) ? false : true;
230 }
231
232 bool git_attr_rule__match(
233 git_attr_rule *rule,
234 const git_attr_path *path)
235 {
236 bool matched = git_attr_fnmatch__match(&rule->match, path);
237
238 if (rule->match.flags & GIT_ATTR_FNMATCH_NEGATIVE)
239 matched = !matched;
240
241 return matched;
242 }
243
244
245 git_attr_assignment *git_attr_rule__lookup_assignment(
246 git_attr_rule *rule, const char *name)
247 {
248 size_t pos;
249 git_attr_name key;
250 key.name = name;
251 key.name_hash = git_attr_file__name_hash(name);
252
253 if (git_vector_bsearch(&pos, &rule->assigns, &key))
254 return NULL;
255
256 return git_vector_get(&rule->assigns, pos);
257 }
258
259 int git_attr_path__init(
260 git_attr_path *info, const char *path, const char *base)
261 {
262 ssize_t root;
263
264 /* build full path as best we can */
265 git_buf_init(&info->full, 0);
266
267 if (git_path_join_unrooted(&info->full, path, base, &root) < 0)
268 return -1;
269
270 info->path = info->full.ptr + root;
271
272 /* remove trailing slashes */
273 while (info->full.size > 0) {
274 if (info->full.ptr[info->full.size - 1] != '/')
275 break;
276 info->full.size--;
277 }
278 info->full.ptr[info->full.size] = '\0';
279
280 /* skip leading slashes in path */
281 while (*info->path == '/')
282 info->path++;
283
284 /* find trailing basename component */
285 info->basename = strrchr(info->path, '/');
286 if (info->basename)
287 info->basename++;
288 if (!info->basename || !*info->basename)
289 info->basename = info->path;
290
291 info->is_dir = (int)git_path_isdir(info->full.ptr);
292
293 return 0;
294 }
295
296 void git_attr_path__free(git_attr_path *info)
297 {
298 git_buf_free(&info->full);
299 info->path = NULL;
300 info->basename = NULL;
301 }
302
303 /*
304 * From gitattributes(5):
305 *
306 * Patterns have the following format:
307 *
308 * - A blank line matches no files, so it can serve as a separator for
309 * readability.
310 *
311 * - A line starting with # serves as a comment.
312 *
313 * - An optional prefix ! which negates the pattern; any matching file
314 * excluded by a previous pattern will become included again. If a negated
315 * pattern matches, this will override lower precedence patterns sources.
316 *
317 * - If the pattern ends with a slash, it is removed for the purpose of the
318 * following description, but it would only find a match with a directory. In
319 * other words, foo/ will match a directory foo and paths underneath it, but
320 * will not match a regular file or a symbolic link foo (this is consistent
321 * with the way how pathspec works in general in git).
322 *
323 * - If the pattern does not contain a slash /, git treats it as a shell glob
324 * pattern and checks for a match against the pathname without leading
325 * directories.
326 *
327 * - Otherwise, git treats the pattern as a shell glob suitable for consumption
328 * by fnmatch(3) with the FNM_PATHNAME flag: wildcards in the pattern will
329 * not match a / in the pathname. For example, "Documentation/\*.html" matches
330 * "Documentation/git.html" but not "Documentation/ppc/ppc.html". A leading
331 * slash matches the beginning of the pathname; for example, "/\*.c" matches
332 * "cat-file.c" but not "mozilla-sha1/sha1.c".
333 */
334
335 /*
336 * This will return 0 if the spec was filled out,
337 * GIT_ENOTFOUND if the fnmatch does not require matching, or
338 * another error code there was an actual problem.
339 */
340 int git_attr_fnmatch__parse_gitattr_format(
341 git_attr_fnmatch *spec,
342 git_pool *pool,
343 const char *source,
344 const char **base)
345 {
346 const char *pattern;
347
348 assert(spec && base && *base);
349
350 pattern = *base;
351
352 while (git__isspace(*pattern)) pattern++;
353 if (!*pattern || *pattern == '#') {
354 *base = git__next_line(pattern);
355 return GIT_ENOTFOUND;
356 }
357
358 if (*pattern == '[') {
359 if (strncmp(pattern, "[attr]", 6) == 0) {
360 spec->flags = spec->flags | GIT_ATTR_FNMATCH_MACRO;
361 pattern += 6;
362 }
363 /* else a character range like [a-e]* which is accepted */
364 }
365
366 if (*pattern == '!') {
367 spec->flags = spec->flags | GIT_ATTR_FNMATCH_NEGATIVE;
368 pattern++;
369 }
370
371 if (git_attr_fnmatch__parse_shellglob_format(spec, pool,
372 source, &pattern) < 0)
373 return -1;
374
375 *base = pattern;
376
377 return 0;
378 }
379
380 /*
381 * Fills a spec for the purpose of pure pathspec matching, not
382 * related to a gitattribute file parsing.
383 *
384 * This will return 0 if the spec was filled out, or
385 * another error code there was an actual problem.
386 */
387 int git_attr_fnmatch__parse_shellglob_format(
388 git_attr_fnmatch *spec,
389 git_pool *pool,
390 const char *source,
391 const char **base)
392 {
393 const char *pattern, *scan;
394 int slash_count, allow_space;
395
396 assert(spec && base && *base);
397
398 if (parse_optimized_patterns(spec, pool, *base))
399 return 0;
400
401 allow_space = (spec->flags & GIT_ATTR_FNMATCH_ALLOWSPACE) != 0;
402 pattern = *base;
403
404 slash_count = 0;
405 for (scan = pattern; *scan != '\0'; ++scan) {
406 /* scan until (non-escaped) white space */
407 if (git__isspace(*scan) && *(scan - 1) != '\\') {
408 if (!allow_space || (*scan != ' ' && *scan != '\t'))
409 break;
410 }
411
412 if (*scan == '/') {
413 spec->flags = spec->flags | GIT_ATTR_FNMATCH_FULLPATH;
414 slash_count++;
415 if (pattern == scan)
416 pattern++;
417 }
418 /* remember if we see an unescaped wildcard in pattern */
419 else if (git__iswildcard(*scan) &&
420 (scan == pattern || (*(scan - 1) != '\\')))
421 spec->flags = spec->flags | GIT_ATTR_FNMATCH_HASWILD;
422 }
423
424 *base = scan;
425
426 spec->length = scan - pattern;
427
428 if (pattern[spec->length - 1] == '/') {
429 spec->length--;
430 spec->flags = spec->flags | GIT_ATTR_FNMATCH_DIRECTORY;
431 if (--slash_count <= 0)
432 spec->flags = spec->flags & ~GIT_ATTR_FNMATCH_FULLPATH;
433 }
434
435 if ((spec->flags & GIT_ATTR_FNMATCH_FULLPATH) != 0 &&
436 source != NULL && git_path_root(pattern) < 0)
437 {
438 size_t sourcelen = strlen(source);
439 /* given an unrooted fullpath match from a file inside a repo,
440 * prefix the pattern with the relative directory of the source file
441 */
442 spec->pattern = git_pool_malloc(
443 pool, (uint32_t)(sourcelen + spec->length + 1));
444 if (spec->pattern) {
445 memcpy(spec->pattern, source, sourcelen);
446 memcpy(spec->pattern + sourcelen, pattern, spec->length);
447 spec->length += sourcelen;
448 spec->pattern[spec->length] = '\0';
449 }
450 } else {
451 spec->pattern = git_pool_strndup(pool, pattern, spec->length);
452 }
453
454 if (!spec->pattern) {
455 *base = git__next_line(pattern);
456 return -1;
457 } else {
458 /* strip '\' that might have be used for internal whitespace */
459 spec->length = git__unescape(spec->pattern);
460 }
461
462 return 0;
463 }
464
465 static bool parse_optimized_patterns(
466 git_attr_fnmatch *spec,
467 git_pool *pool,
468 const char *pattern)
469 {
470 if (!pattern[1] && (pattern[0] == '*' || pattern[0] == '.')) {
471 spec->flags = GIT_ATTR_FNMATCH_MATCH_ALL;
472 spec->pattern = git_pool_strndup(pool, pattern, 1);
473 spec->length = 1;
474
475 return true;
476 }
477
478 return false;
479 }
480
481 static int sort_by_hash_and_name(const void *a_raw, const void *b_raw)
482 {
483 const git_attr_name *a = a_raw;
484 const git_attr_name *b = b_raw;
485
486 if (b->name_hash < a->name_hash)
487 return 1;
488 else if (b->name_hash > a->name_hash)
489 return -1;
490 else
491 return strcmp(b->name, a->name);
492 }
493
494 static void git_attr_assignment__free(git_attr_assignment *assign)
495 {
496 /* name and value are stored in a git_pool associated with the
497 * git_attr_file, so they do not need to be freed here
498 */
499 assign->name = NULL;
500 assign->value = NULL;
501 git__free(assign);
502 }
503
504 static int merge_assignments(void **old_raw, void *new_raw)
505 {
506 git_attr_assignment **old = (git_attr_assignment **)old_raw;
507 git_attr_assignment *new = (git_attr_assignment *)new_raw;
508
509 GIT_REFCOUNT_DEC(*old, git_attr_assignment__free);
510 *old = new;
511 return GIT_EEXISTS;
512 }
513
514 int git_attr_assignment__parse(
515 git_repository *repo,
516 git_pool *pool,
517 git_vector *assigns,
518 const char **base)
519 {
520 int error;
521 const char *scan = *base;
522 git_attr_assignment *assign = NULL;
523
524 assert(assigns && !assigns->length);
525
526 assigns->_cmp = sort_by_hash_and_name;
527
528 while (*scan && *scan != '\n') {
529 const char *name_start, *value_start;
530
531 /* skip leading blanks */
532 while (git__isspace(*scan) && *scan != '\n') scan++;
533
534 /* allocate assign if needed */
535 if (!assign) {
536 assign = git__calloc(1, sizeof(git_attr_assignment));
537 GITERR_CHECK_ALLOC(assign);
538 GIT_REFCOUNT_INC(assign);
539 }
540
541 assign->name_hash = 5381;
542 assign->value = git_attr__true;
543
544 /* look for magic name prefixes */
545 if (*scan == '-') {
546 assign->value = git_attr__false;
547 scan++;
548 } else if (*scan == '!') {
549 assign->value = git_attr__unset; /* explicit unspecified state */
550 scan++;
551 } else if (*scan == '#') /* comment rest of line */
552 break;
553
554 /* find the name */
555 name_start = scan;
556 while (*scan && !git__isspace(*scan) && *scan != '=') {
557 assign->name_hash =
558 ((assign->name_hash << 5) + assign->name_hash) + *scan;
559 scan++;
560 }
561 if (scan == name_start) {
562 /* must have found lone prefix (" - ") or leading = ("=foo")
563 * or end of buffer -- advance until whitespace and continue
564 */
565 while (*scan && !git__isspace(*scan)) scan++;
566 continue;
567 }
568
569 /* allocate permanent storage for name */
570 assign->name = git_pool_strndup(pool, name_start, scan - name_start);
571 GITERR_CHECK_ALLOC(assign->name);
572
573 /* if there is an equals sign, find the value */
574 if (*scan == '=') {
575 for (value_start = ++scan; *scan && !git__isspace(*scan); ++scan);
576
577 /* if we found a value, allocate permanent storage for it */
578 if (scan > value_start) {
579 assign->value = git_pool_strndup(pool, value_start, scan - value_start);
580 GITERR_CHECK_ALLOC(assign->value);
581 }
582 }
583
584 /* expand macros (if given a repo with a macro cache) */
585 if (repo != NULL && assign->value == git_attr__true) {
586 git_attr_rule *macro =
587 git_attr_cache__lookup_macro(repo, assign->name);
588
589 if (macro != NULL) {
590 unsigned int i;
591 git_attr_assignment *massign;
592
593 git_vector_foreach(&macro->assigns, i, massign) {
594 GIT_REFCOUNT_INC(massign);
595
596 error = git_vector_insert_sorted(
597 assigns, massign, &merge_assignments);
598 if (error < 0 && error != GIT_EEXISTS)
599 return error;
600 }
601 }
602 }
603
604 /* insert allocated assign into vector */
605 error = git_vector_insert_sorted(assigns, assign, &merge_assignments);
606 if (error < 0 && error != GIT_EEXISTS)
607 return error;
608
609 /* clear assign since it is now "owned" by the vector */
610 assign = NULL;
611 }
612
613 if (assign != NULL)
614 git_attr_assignment__free(assign);
615
616 *base = git__next_line(scan);
617
618 return (assigns->length == 0) ? GIT_ENOTFOUND : 0;
619 }
620
621 static void git_attr_rule__clear(git_attr_rule *rule)
622 {
623 unsigned int i;
624 git_attr_assignment *assign;
625
626 if (!rule)
627 return;
628
629 if (!(rule->match.flags & GIT_ATTR_FNMATCH_IGNORE)) {
630 git_vector_foreach(&rule->assigns, i, assign)
631 GIT_REFCOUNT_DEC(assign, git_attr_assignment__free);
632 git_vector_free(&rule->assigns);
633 }
634
635 /* match.pattern is stored in a git_pool, so no need to free */
636 rule->match.pattern = NULL;
637 rule->match.length = 0;
638 rule->match.flags = 0;
639 }
640
641 void git_attr_rule__free(git_attr_rule *rule)
642 {
643 git_attr_rule__clear(rule);
644 git__free(rule);
645 }
646