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