]> git.proxmox.com Git - libgit2.git/blob - src/attr_file.c
Vector improvements and their fallout
[libgit2.git] / src / attr_file.c
1 #include "common.h"
2 #include "repository.h"
3 #include "filebuf.h"
4 #include "git2/blob.h"
5 #include "git2/tree.h"
6 #include <ctype.h>
7
8 static int sort_by_hash_and_name(const void *a_raw, const void *b_raw);
9 static void git_attr_rule__clear(git_attr_rule *rule);
10
11 int git_attr_file__new(
12 git_attr_file **attrs_ptr,
13 git_attr_file_source from,
14 const char *path,
15 git_pool *pool)
16 {
17 git_attr_file *attrs = NULL;
18
19 attrs = git__calloc(1, sizeof(git_attr_file));
20 GITERR_CHECK_ALLOC(attrs);
21
22 if (pool)
23 attrs->pool = pool;
24 else {
25 attrs->pool = git__calloc(1, sizeof(git_pool));
26 if (!attrs->pool || git_pool_init(attrs->pool, 1, 0) < 0)
27 goto fail;
28 attrs->pool_is_allocated = true;
29 }
30
31 if (path) {
32 size_t len = strlen(path);
33
34 attrs->key = git_pool_malloc(attrs->pool, (uint32_t)len + 3);
35 GITERR_CHECK_ALLOC(attrs->key);
36
37 attrs->key[0] = '0' + from;
38 attrs->key[1] = '#';
39 memcpy(&attrs->key[2], path, len);
40 attrs->key[len + 2] = '\0';
41 }
42
43 if (git_vector_init(&attrs->rules, 4, NULL) < 0)
44 goto fail;
45
46 *attrs_ptr = attrs;
47 return 0;
48
49 fail:
50 git_attr_file__free(attrs);
51 attrs_ptr = NULL;
52 return -1;
53 }
54
55 int git_attr_file__parse_buffer(
56 git_repository *repo, void *parsedata, const char *buffer, git_attr_file *attrs)
57 {
58 int error = 0;
59 const char *scan = NULL;
60 char *context = NULL;
61 git_attr_rule *rule = NULL;
62
63 GIT_UNUSED(parsedata);
64
65 assert(buffer && attrs);
66
67 scan = buffer;
68
69 /* if subdir file path, convert context for file paths */
70 if (attrs->key && git__suffixcmp(attrs->key, "/" GIT_ATTR_FILE) == 0) {
71 context = attrs->key + 2;
72 context[strlen(context) - strlen(GIT_ATTR_FILE)] = '\0';
73 }
74
75 while (!error && *scan) {
76 /* allocate rule if needed */
77 if (!rule && !(rule = git__calloc(1, sizeof(git_attr_rule)))) {
78 error = -1;
79 break;
80 }
81
82 /* parse the next "pattern attr attr attr" line */
83 if (!(error = git_attr_fnmatch__parse(
84 &rule->match, attrs->pool, context, &scan)) &&
85 !(error = git_attr_assignment__parse(
86 repo, attrs->pool, &rule->assigns, &scan)))
87 {
88 if (rule->match.flags & GIT_ATTR_FNMATCH_MACRO)
89 /* should generate error/warning if this is coming from any
90 * file other than .gitattributes at repo root.
91 */
92 error = git_attr_cache__insert_macro(repo, rule);
93 else
94 error = git_vector_insert(&attrs->rules, rule);
95 }
96
97 /* if the rule wasn't a pattern, on to the next */
98 if (error < 0) {
99 git_attr_rule__clear(rule); /* reset rule contents */
100 if (error == GIT_ENOTFOUND)
101 error = 0;
102 } else {
103 rule = NULL; /* vector now "owns" the rule */
104 }
105 }
106
107 git_attr_rule__free(rule);
108
109 /* restore file path used for context */
110 if (context)
111 context[strlen(context)] = '.'; /* first char of GIT_ATTR_FILE */
112
113 return error;
114 }
115
116 int git_attr_file__new_and_load(
117 git_attr_file **attrs_ptr,
118 const char *path)
119 {
120 int error;
121 git_buf content = GIT_BUF_INIT;
122
123 if ((error = git_attr_file__new(attrs_ptr, 0, path, NULL)) < 0)
124 return error;
125
126 if (!(error = git_futils_readbuffer(&content, path)))
127 error = git_attr_file__parse_buffer(
128 NULL, NULL, git_buf_cstr(&content), *attrs_ptr);
129
130 git_buf_free(&content);
131
132 if (error) {
133 git_attr_file__free(*attrs_ptr);
134 *attrs_ptr = NULL;
135 }
136
137 return error;
138 }
139
140 void git_attr_file__clear_rules(git_attr_file *file)
141 {
142 unsigned int i;
143 git_attr_rule *rule;
144
145 git_vector_foreach(&file->rules, i, rule)
146 git_attr_rule__free(rule);
147
148 git_vector_free(&file->rules);
149 }
150
151 void git_attr_file__free(git_attr_file *file)
152 {
153 if (!file)
154 return;
155
156 git_attr_file__clear_rules(file);
157
158 if (file->pool_is_allocated) {
159 git_pool_clear(file->pool);
160 git__free(file->pool);
161 }
162 file->pool = NULL;
163
164 git__free(file);
165 }
166
167 uint32_t git_attr_file__name_hash(const char *name)
168 {
169 uint32_t h = 5381;
170 int c;
171 assert(name);
172 while ((c = (int)*name++) != 0)
173 h = ((h << 5) + h) + c;
174 return h;
175 }
176
177
178 int git_attr_file__lookup_one(
179 git_attr_file *file,
180 const git_attr_path *path,
181 const char *attr,
182 const char **value)
183 {
184 size_t i;
185 git_attr_name name;
186 git_attr_rule *rule;
187
188 *value = NULL;
189
190 name.name = attr;
191 name.name_hash = git_attr_file__name_hash(attr);
192
193 git_attr_file__foreach_matching_rule(file, path, i, rule) {
194 size_t pos;
195
196 if (!git_vector_bsearch(&pos, &rule->assigns, &name)) {
197 *value = ((git_attr_assignment *)
198 git_vector_get(&rule->assigns, pos))->value;
199 break;
200 }
201 }
202
203 return 0;
204 }
205
206
207 bool git_attr_fnmatch__match(
208 git_attr_fnmatch *match,
209 const git_attr_path *path)
210 {
211 int fnm;
212 int icase_flags = (match->flags & GIT_ATTR_FNMATCH_ICASE) ? FNM_CASEFOLD : 0;
213
214 if (match->flags & GIT_ATTR_FNMATCH_DIRECTORY && !path->is_dir)
215 return false;
216
217 if (match->flags & GIT_ATTR_FNMATCH_FULLPATH)
218 fnm = p_fnmatch(match->pattern, path->path, FNM_PATHNAME | icase_flags);
219 else if (path->is_dir)
220 fnm = p_fnmatch(match->pattern, path->basename, FNM_LEADING_DIR | icase_flags);
221 else
222 fnm = p_fnmatch(match->pattern, path->basename, icase_flags);
223
224 return (fnm == FNM_NOMATCH) ? false : true;
225 }
226
227 bool git_attr_rule__match(
228 git_attr_rule *rule,
229 const git_attr_path *path)
230 {
231 bool matched = git_attr_fnmatch__match(&rule->match, path);
232
233 if (rule->match.flags & GIT_ATTR_FNMATCH_NEGATIVE)
234 matched = !matched;
235
236 return matched;
237 }
238
239
240 git_attr_assignment *git_attr_rule__lookup_assignment(
241 git_attr_rule *rule, const char *name)
242 {
243 size_t pos;
244 git_attr_name key;
245 key.name = name;
246 key.name_hash = git_attr_file__name_hash(name);
247
248 if (git_vector_bsearch(&pos, &rule->assigns, &key))
249 return NULL;
250
251 return git_vector_get(&rule->assigns, pos);
252 }
253
254 int git_attr_path__init(
255 git_attr_path *info, const char *path, const char *base)
256 {
257 ssize_t root;
258
259 /* build full path as best we can */
260 git_buf_init(&info->full, 0);
261
262 if (git_path_join_unrooted(&info->full, path, base, &root) < 0)
263 return -1;
264
265 info->path = info->full.ptr + root;
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, allow_space;
344
345 assert(spec && base && *base);
346
347 spec->flags = (spec->flags & GIT_ATTR_FNMATCH_ALLOWSPACE);
348 allow_space = (spec->flags != 0);
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 slash_count = 0;
372 for (scan = pattern; *scan != '\0'; ++scan) {
373 /* scan until (non-escaped) white space */
374 if (git__isspace(*scan) && *(scan - 1) != '\\') {
375 if (!allow_space || (*scan != ' ' && *scan != '\t'))
376 break;
377 }
378
379 if (*scan == '/') {
380 spec->flags = spec->flags | GIT_ATTR_FNMATCH_FULLPATH;
381 slash_count++;
382 if (pattern == scan)
383 pattern++;
384 }
385 /* remember if we see an unescaped wildcard in pattern */
386 else if (git__iswildcard(*scan) &&
387 (scan == pattern || (*(scan - 1) != '\\')))
388 spec->flags = spec->flags | GIT_ATTR_FNMATCH_HASWILD;
389 }
390
391 *base = scan;
392
393 spec->length = scan - pattern;
394
395 if (pattern[spec->length - 1] == '/') {
396 spec->length--;
397 spec->flags = spec->flags | GIT_ATTR_FNMATCH_DIRECTORY;
398 if (--slash_count <= 0)
399 spec->flags = spec->flags & ~GIT_ATTR_FNMATCH_FULLPATH;
400 }
401
402 if ((spec->flags & GIT_ATTR_FNMATCH_FULLPATH) != 0 &&
403 source != NULL && git_path_root(pattern) < 0)
404 {
405 size_t sourcelen = strlen(source);
406 /* given an unrooted fullpath match from a file inside a repo,
407 * prefix the pattern with the relative directory of the source file
408 */
409 spec->pattern = git_pool_malloc(
410 pool, (uint32_t)(sourcelen + spec->length + 1));
411 if (spec->pattern) {
412 memcpy(spec->pattern, source, sourcelen);
413 memcpy(spec->pattern + sourcelen, pattern, spec->length);
414 spec->length += sourcelen;
415 spec->pattern[spec->length] = '\0';
416 }
417 } else {
418 spec->pattern = git_pool_strndup(pool, pattern, spec->length);
419 }
420
421 if (!spec->pattern) {
422 *base = git__next_line(pattern);
423 return -1;
424 } else {
425 /* strip '\' that might have be used for internal whitespace */
426 spec->length = git__unescape(spec->pattern);
427 }
428
429 return 0;
430 }
431
432 static int sort_by_hash_and_name(const void *a_raw, const void *b_raw)
433 {
434 const git_attr_name *a = a_raw;
435 const git_attr_name *b = b_raw;
436
437 if (b->name_hash < a->name_hash)
438 return 1;
439 else if (b->name_hash > a->name_hash)
440 return -1;
441 else
442 return strcmp(b->name, a->name);
443 }
444
445 static void git_attr_assignment__free(git_attr_assignment *assign)
446 {
447 /* name and value are stored in a git_pool associated with the
448 * git_attr_file, so they do not need to be freed here
449 */
450 assign->name = NULL;
451 assign->value = NULL;
452 git__free(assign);
453 }
454
455 static int merge_assignments(void **old_raw, void *new_raw)
456 {
457 git_attr_assignment **old = (git_attr_assignment **)old_raw;
458 git_attr_assignment *new = (git_attr_assignment *)new_raw;
459
460 GIT_REFCOUNT_DEC(*old, git_attr_assignment__free);
461 *old = new;
462 return GIT_EEXISTS;
463 }
464
465 int git_attr_assignment__parse(
466 git_repository *repo,
467 git_pool *pool,
468 git_vector *assigns,
469 const char **base)
470 {
471 int error;
472 const char *scan = *base;
473 git_attr_assignment *assign = NULL;
474
475 assert(assigns && !assigns->length);
476
477 assigns->_cmp = sort_by_hash_and_name;
478
479 while (*scan && *scan != '\n') {
480 const char *name_start, *value_start;
481
482 /* skip leading blanks */
483 while (git__isspace(*scan) && *scan != '\n') scan++;
484
485 /* allocate assign if needed */
486 if (!assign) {
487 assign = git__calloc(1, sizeof(git_attr_assignment));
488 GITERR_CHECK_ALLOC(assign);
489 GIT_REFCOUNT_INC(assign);
490 }
491
492 assign->name_hash = 5381;
493 assign->value = git_attr__true;
494
495 /* look for magic name prefixes */
496 if (*scan == '-') {
497 assign->value = git_attr__false;
498 scan++;
499 } else if (*scan == '!') {
500 assign->value = git_attr__unset; /* explicit unspecified state */
501 scan++;
502 } else if (*scan == '#') /* comment rest of line */
503 break;
504
505 /* find the name */
506 name_start = scan;
507 while (*scan && !git__isspace(*scan) && *scan != '=') {
508 assign->name_hash =
509 ((assign->name_hash << 5) + assign->name_hash) + *scan;
510 scan++;
511 }
512 if (scan == name_start) {
513 /* must have found lone prefix (" - ") or leading = ("=foo")
514 * or end of buffer -- advance until whitespace and continue
515 */
516 while (*scan && !git__isspace(*scan)) scan++;
517 continue;
518 }
519
520 /* allocate permanent storage for name */
521 assign->name = git_pool_strndup(pool, name_start, scan - name_start);
522 GITERR_CHECK_ALLOC(assign->name);
523
524 /* if there is an equals sign, find the value */
525 if (*scan == '=') {
526 for (value_start = ++scan; *scan && !git__isspace(*scan); ++scan);
527
528 /* if we found a value, allocate permanent storage for it */
529 if (scan > value_start) {
530 assign->value = git_pool_strndup(pool, value_start, scan - value_start);
531 GITERR_CHECK_ALLOC(assign->value);
532 }
533 }
534
535 /* expand macros (if given a repo with a macro cache) */
536 if (repo != NULL && assign->value == git_attr__true) {
537 git_attr_rule *macro =
538 git_attr_cache__lookup_macro(repo, assign->name);
539
540 if (macro != NULL) {
541 unsigned int i;
542 git_attr_assignment *massign;
543
544 git_vector_foreach(&macro->assigns, i, massign) {
545 GIT_REFCOUNT_INC(massign);
546
547 error = git_vector_insert_sorted(
548 assigns, massign, &merge_assignments);
549 if (error < 0 && error != GIT_EEXISTS)
550 return error;
551 }
552 }
553 }
554
555 /* insert allocated assign into vector */
556 error = git_vector_insert_sorted(assigns, assign, &merge_assignments);
557 if (error < 0 && error != GIT_EEXISTS)
558 return error;
559
560 /* clear assign since it is now "owned" by the vector */
561 assign = NULL;
562 }
563
564 if (assign != NULL)
565 git_attr_assignment__free(assign);
566
567 *base = git__next_line(scan);
568
569 return (assigns->length == 0) ? GIT_ENOTFOUND : 0;
570 }
571
572 static void git_attr_rule__clear(git_attr_rule *rule)
573 {
574 unsigned int i;
575 git_attr_assignment *assign;
576
577 if (!rule)
578 return;
579
580 if (!(rule->match.flags & GIT_ATTR_FNMATCH_IGNORE)) {
581 git_vector_foreach(&rule->assigns, i, assign)
582 GIT_REFCOUNT_DEC(assign, git_attr_assignment__free);
583 git_vector_free(&rule->assigns);
584 }
585
586 /* match.pattern is stored in a git_pool, so no need to free */
587 rule->match.pattern = NULL;
588 rule->match.length = 0;
589 }
590
591 void git_attr_rule__free(git_attr_rule *rule)
592 {
593 git_attr_rule__clear(rule);
594 git__free(rule);
595 }
596