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