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.h"
9 
10 #include "repository.h"
11 #include "sysdir.h"
12 #include "config.h"
13 #include "attr_file.h"
14 #include "ignore.h"
15 #include "git2/oid.h"
16 #include <ctype.h>
17 
18 const char *git_attr__true  = "[internal]__TRUE__";
19 const char *git_attr__false = "[internal]__FALSE__";
20 const char *git_attr__unset = "[internal]__UNSET__";
21 
git_attr_value(const char * attr)22 git_attr_value_t git_attr_value(const char *attr)
23 {
24 	if (attr == NULL || attr == git_attr__unset)
25 		return GIT_ATTR_VALUE_UNSPECIFIED;
26 
27 	if (attr == git_attr__true)
28 		return GIT_ATTR_VALUE_TRUE;
29 
30 	if (attr == git_attr__false)
31 		return GIT_ATTR_VALUE_FALSE;
32 
33 	return GIT_ATTR_VALUE_STRING;
34 }
35 
36 static int collect_attr_files(
37 	git_repository *repo,
38 	git_attr_session *attr_session,
39 	uint32_t flags,
40 	const char *path,
41 	git_vector *files);
42 
43 static void release_attr_files(git_vector *files);
44 
git_attr_get(const char ** value,git_repository * repo,uint32_t flags,const char * pathname,const char * name)45 int git_attr_get(
46 	const char **value,
47 	git_repository *repo,
48 	uint32_t flags,
49 	const char *pathname,
50 	const char *name)
51 {
52 	int error;
53 	git_attr_path path;
54 	git_vector files = GIT_VECTOR_INIT;
55 	size_t i, j;
56 	git_attr_file *file;
57 	git_attr_name attr;
58 	git_attr_rule *rule;
59 	git_dir_flag dir_flag = GIT_DIR_FLAG_UNKNOWN;
60 
61 	GIT_ASSERT_ARG(value);
62 	GIT_ASSERT_ARG(repo);
63 	GIT_ASSERT_ARG(name);
64 
65 	*value = NULL;
66 
67 	if (git_repository_is_bare(repo))
68 		dir_flag = GIT_DIR_FLAG_FALSE;
69 
70 	if (git_attr_path__init(&path, pathname, git_repository_workdir(repo), dir_flag) < 0)
71 		return -1;
72 
73 	if ((error = collect_attr_files(repo, NULL, flags, pathname, &files)) < 0)
74 		goto cleanup;
75 
76 	memset(&attr, 0, sizeof(attr));
77 	attr.name = name;
78 	attr.name_hash = git_attr_file__name_hash(name);
79 
80 	git_vector_foreach(&files, i, file) {
81 
82 		git_attr_file__foreach_matching_rule(file, &path, j, rule) {
83 			size_t pos;
84 
85 			if (!git_vector_bsearch(&pos, &rule->assigns, &attr)) {
86 				*value = ((git_attr_assignment *)git_vector_get(
87 							  &rule->assigns, pos))->value;
88 				goto cleanup;
89 			}
90 		}
91 	}
92 
93 cleanup:
94 	release_attr_files(&files);
95 	git_attr_path__free(&path);
96 
97 	return error;
98 }
99 
100 
101 typedef struct {
102 	git_attr_name name;
103 	git_attr_assignment *found;
104 } attr_get_many_info;
105 
git_attr_get_many_with_session(const char ** values,git_repository * repo,git_attr_session * attr_session,uint32_t flags,const char * pathname,size_t num_attr,const char ** names)106 int git_attr_get_many_with_session(
107 	const char **values,
108 	git_repository *repo,
109 	git_attr_session *attr_session,
110 	uint32_t flags,
111 	const char *pathname,
112 	size_t num_attr,
113 	const char **names)
114 {
115 	int error;
116 	git_attr_path path;
117 	git_vector files = GIT_VECTOR_INIT;
118 	size_t i, j, k;
119 	git_attr_file *file;
120 	git_attr_rule *rule;
121 	attr_get_many_info *info = NULL;
122 	size_t num_found = 0;
123 	git_dir_flag dir_flag = GIT_DIR_FLAG_UNKNOWN;
124 
125 	if (!num_attr)
126 		return 0;
127 
128 	GIT_ASSERT_ARG(values);
129 	GIT_ASSERT_ARG(repo);
130 	GIT_ASSERT_ARG(pathname);
131 	GIT_ASSERT_ARG(names);
132 
133 	if (git_repository_is_bare(repo))
134 		dir_flag = GIT_DIR_FLAG_FALSE;
135 
136 	if (git_attr_path__init(&path, pathname, git_repository_workdir(repo), dir_flag) < 0)
137 		return -1;
138 
139 	if ((error = collect_attr_files(repo, attr_session, flags, pathname, &files)) < 0)
140 		goto cleanup;
141 
142 	info = git__calloc(num_attr, sizeof(attr_get_many_info));
143 	GIT_ERROR_CHECK_ALLOC(info);
144 
145 	git_vector_foreach(&files, i, file) {
146 
147 		git_attr_file__foreach_matching_rule(file, &path, j, rule) {
148 
149 			for (k = 0; k < num_attr; k++) {
150 				size_t pos;
151 
152 				if (info[k].found != NULL) /* already found assignment */
153 					continue;
154 
155 				if (!info[k].name.name) {
156 					info[k].name.name = names[k];
157 					info[k].name.name_hash = git_attr_file__name_hash(names[k]);
158 				}
159 
160 				if (!git_vector_bsearch(&pos, &rule->assigns, &info[k].name)) {
161 					info[k].found = (git_attr_assignment *)
162 						git_vector_get(&rule->assigns, pos);
163 					values[k] = info[k].found->value;
164 
165 					if (++num_found == num_attr)
166 						goto cleanup;
167 				}
168 			}
169 		}
170 	}
171 
172 	for (k = 0; k < num_attr; k++) {
173 		if (!info[k].found)
174 			values[k] = NULL;
175 	}
176 
177 cleanup:
178 	release_attr_files(&files);
179 	git_attr_path__free(&path);
180 	git__free(info);
181 
182 	return error;
183 }
184 
git_attr_get_many(const char ** values,git_repository * repo,uint32_t flags,const char * pathname,size_t num_attr,const char ** names)185 int git_attr_get_many(
186 	const char **values,
187 	git_repository *repo,
188 	uint32_t flags,
189 	const char *pathname,
190 	size_t num_attr,
191 	const char **names)
192 {
193 	return git_attr_get_many_with_session(
194 		values, repo, NULL, flags, pathname, num_attr, names);
195 }
196 
git_attr_foreach(git_repository * repo,uint32_t flags,const char * pathname,int (* callback)(const char * name,const char * value,void * payload),void * payload)197 int git_attr_foreach(
198 	git_repository *repo,
199 	uint32_t flags,
200 	const char *pathname,
201 	int (*callback)(const char *name, const char *value, void *payload),
202 	void *payload)
203 {
204 	int error;
205 	git_attr_path path;
206 	git_vector files = GIT_VECTOR_INIT;
207 	size_t i, j, k;
208 	git_attr_file *file;
209 	git_attr_rule *rule;
210 	git_attr_assignment *assign;
211 	git_strmap *seen = NULL;
212 	git_dir_flag dir_flag = GIT_DIR_FLAG_UNKNOWN;
213 
214 	GIT_ASSERT_ARG(repo);
215 	GIT_ASSERT_ARG(callback);
216 
217 	if (git_repository_is_bare(repo))
218 		dir_flag = GIT_DIR_FLAG_FALSE;
219 
220 	if (git_attr_path__init(&path, pathname, git_repository_workdir(repo), dir_flag) < 0)
221 		return -1;
222 
223 	if ((error = collect_attr_files(repo, NULL, flags, pathname, &files)) < 0 ||
224 	    (error = git_strmap_new(&seen)) < 0)
225 		goto cleanup;
226 
227 	git_vector_foreach(&files, i, file) {
228 
229 		git_attr_file__foreach_matching_rule(file, &path, j, rule) {
230 
231 			git_vector_foreach(&rule->assigns, k, assign) {
232 				/* skip if higher priority assignment was already seen */
233 				if (git_strmap_exists(seen, assign->name))
234 					continue;
235 
236 				if ((error = git_strmap_set(seen, assign->name, assign)) < 0)
237 					goto cleanup;
238 
239 				error = callback(assign->name, assign->value, payload);
240 				if (error) {
241 					git_error_set_after_callback(error);
242 					goto cleanup;
243 				}
244 			}
245 		}
246 	}
247 
248 cleanup:
249 	git_strmap_free(seen);
250 	release_attr_files(&files);
251 	git_attr_path__free(&path);
252 
253 	return error;
254 }
255 
preload_attr_file(git_repository * repo,git_attr_session * attr_session,git_attr_file_source source,const char * base,const char * file,bool allow_macros)256 static int preload_attr_file(
257 	git_repository *repo,
258 	git_attr_session *attr_session,
259 	git_attr_file_source source,
260 	const char *base,
261 	const char *file,
262 	bool allow_macros)
263 {
264 	int error;
265 	git_attr_file *preload = NULL;
266 
267 	if (!file)
268 		return 0;
269 	if (!(error = git_attr_cache__get(&preload, repo, attr_session, source, base, file,
270 					  git_attr_file__parse_buffer, allow_macros)))
271 		git_attr_file__free(preload);
272 
273 	return error;
274 }
275 
system_attr_file(git_buf * out,git_attr_session * attr_session)276 static int system_attr_file(
277 	git_buf *out,
278 	git_attr_session *attr_session)
279 {
280 	int error;
281 
282 	if (!attr_session) {
283 		error = git_sysdir_find_system_file(out, GIT_ATTR_FILE_SYSTEM);
284 
285 		if (error == GIT_ENOTFOUND)
286 			git_error_clear();
287 
288 		return error;
289 	}
290 
291 	if (!attr_session->init_sysdir) {
292 		error = git_sysdir_find_system_file(&attr_session->sysdir, GIT_ATTR_FILE_SYSTEM);
293 
294 		if (error == GIT_ENOTFOUND)
295 			git_error_clear();
296 		else if (error)
297 			return error;
298 
299 		attr_session->init_sysdir = 1;
300 	}
301 
302 	if (attr_session->sysdir.size == 0)
303 		return GIT_ENOTFOUND;
304 
305 	/* We can safely provide a git_buf with no allocation (asize == 0) to
306 	 * a consumer. This allows them to treat this as a regular `git_buf`,
307 	 * but their call to `git_buf_dispose` will not attempt to free it.
308 	 */
309 	git_buf_attach_notowned(
310 		out, attr_session->sysdir.ptr, attr_session->sysdir.size);
311 	return 0;
312 }
313 
attr_setup(git_repository * repo,git_attr_session * attr_session,uint32_t flags)314 static int attr_setup(
315 	git_repository *repo,
316 	git_attr_session *attr_session,
317 	uint32_t flags)
318 {
319 	git_buf path = GIT_BUF_INIT;
320 	git_index *idx = NULL;
321 	const char *workdir;
322 	int error = 0;
323 
324 	if (attr_session && attr_session->init_setup)
325 		return 0;
326 
327 	if ((error = git_attr_cache__init(repo)) < 0)
328 		return error;
329 
330 	/*
331 	 * Preload attribute files that could contain macros so the
332 	 * definitions will be available for later file parsing.
333 	 */
334 
335 	if ((error = system_attr_file(&path, attr_session)) < 0 ||
336 	    (error = preload_attr_file(repo, attr_session, GIT_ATTR_FILE__FROM_FILE,
337 				       NULL, path.ptr, true)) < 0) {
338 		if (error != GIT_ENOTFOUND)
339 			goto out;
340 	}
341 
342 	if ((error = preload_attr_file(repo, attr_session, GIT_ATTR_FILE__FROM_FILE,
343 				       NULL, git_repository_attr_cache(repo)->cfg_attr_file, true)) < 0)
344 		goto out;
345 
346 	git_buf_clear(&path); /* git_repository_item_path expects an empty buffer, because it uses git_buf_set */
347 	if ((error = git_repository_item_path(&path, repo, GIT_REPOSITORY_ITEM_INFO)) < 0 ||
348 	    (error = preload_attr_file(repo, attr_session, GIT_ATTR_FILE__FROM_FILE,
349 				       path.ptr, GIT_ATTR_FILE_INREPO, true)) < 0) {
350 		if (error != GIT_ENOTFOUND)
351 			goto out;
352 	}
353 
354 	if ((workdir = git_repository_workdir(repo)) != NULL &&
355 	    (error = preload_attr_file(repo, attr_session, GIT_ATTR_FILE__FROM_FILE,
356 				       workdir, GIT_ATTR_FILE, true)) < 0)
357 			goto out;
358 
359 	if ((error = git_repository_index__weakptr(&idx, repo)) < 0 ||
360 	    (error = preload_attr_file(repo, attr_session, GIT_ATTR_FILE__FROM_INDEX,
361 				       NULL, GIT_ATTR_FILE, true)) < 0)
362 			goto out;
363 
364 	if ((flags & GIT_ATTR_CHECK_INCLUDE_HEAD) != 0 &&
365 	    (error = preload_attr_file(repo, attr_session, GIT_ATTR_FILE__FROM_HEAD,
366 				       NULL, GIT_ATTR_FILE, true)) < 0)
367 		goto out;
368 
369 	if (attr_session)
370 		attr_session->init_setup = 1;
371 
372 out:
373 	git_buf_dispose(&path);
374 
375 	return error;
376 }
377 
git_attr_add_macro(git_repository * repo,const char * name,const char * values)378 int git_attr_add_macro(
379 	git_repository *repo,
380 	const char *name,
381 	const char *values)
382 {
383 	int error;
384 	git_attr_rule *macro = NULL;
385 	git_pool *pool;
386 
387 	GIT_ASSERT_ARG(repo);
388 	GIT_ASSERT_ARG(name);
389 
390 	if ((error = git_attr_cache__init(repo)) < 0)
391 		return error;
392 
393 	macro = git__calloc(1, sizeof(git_attr_rule));
394 	GIT_ERROR_CHECK_ALLOC(macro);
395 
396 	pool = &git_repository_attr_cache(repo)->pool;
397 
398 	macro->match.pattern = git_pool_strdup(pool, name);
399 	GIT_ERROR_CHECK_ALLOC(macro->match.pattern);
400 
401 	macro->match.length = strlen(macro->match.pattern);
402 	macro->match.flags = GIT_ATTR_FNMATCH_MACRO;
403 
404 	error = git_attr_assignment__parse(repo, pool, &macro->assigns, &values);
405 
406 	if (!error)
407 		error = git_attr_cache__insert_macro(repo, macro);
408 
409 	if (error < 0)
410 		git_attr_rule__free(macro);
411 
412 	return error;
413 }
414 
415 typedef struct {
416 	git_repository *repo;
417 	git_attr_session *attr_session;
418 	uint32_t flags;
419 	const char *workdir;
420 	git_index *index;
421 	git_vector *files;
422 } attr_walk_up_info;
423 
attr_decide_sources(uint32_t flags,bool has_wd,bool has_index,git_attr_file_source * srcs)424 static int attr_decide_sources(
425 	uint32_t flags, bool has_wd, bool has_index, git_attr_file_source *srcs)
426 {
427 	int count = 0;
428 
429 	switch (flags & 0x03) {
430 	case GIT_ATTR_CHECK_FILE_THEN_INDEX:
431 		if (has_wd)
432 			srcs[count++] = GIT_ATTR_FILE__FROM_FILE;
433 		if (has_index)
434 			srcs[count++] = GIT_ATTR_FILE__FROM_INDEX;
435 		break;
436 	case GIT_ATTR_CHECK_INDEX_THEN_FILE:
437 		if (has_index)
438 			srcs[count++] = GIT_ATTR_FILE__FROM_INDEX;
439 		if (has_wd)
440 			srcs[count++] = GIT_ATTR_FILE__FROM_FILE;
441 		break;
442 	case GIT_ATTR_CHECK_INDEX_ONLY:
443 		if (has_index)
444 			srcs[count++] = GIT_ATTR_FILE__FROM_INDEX;
445 		break;
446 	}
447 
448 	if ((flags & GIT_ATTR_CHECK_INCLUDE_HEAD) != 0)
449 		srcs[count++] = GIT_ATTR_FILE__FROM_HEAD;
450 
451 	return count;
452 }
453 
push_attr_file(git_repository * repo,git_attr_session * attr_session,git_vector * list,git_attr_file_source source,const char * base,const char * filename,bool allow_macros)454 static int push_attr_file(
455 	git_repository *repo,
456 	git_attr_session *attr_session,
457 	git_vector *list,
458 	git_attr_file_source source,
459 	const char *base,
460 	const char *filename,
461 	bool allow_macros)
462 {
463 	int error = 0;
464 	git_attr_file *file = NULL;
465 
466 	error = git_attr_cache__get(&file, repo, attr_session,
467 		source, base, filename, git_attr_file__parse_buffer, allow_macros);
468 
469 	if (error < 0)
470 		return error;
471 
472 	if (file != NULL) {
473 		if ((error = git_vector_insert(list, file)) < 0)
474 			git_attr_file__free(file);
475 	}
476 
477 	return error;
478 }
479 
push_one_attr(void * ref,const char * path)480 static int push_one_attr(void *ref, const char *path)
481 {
482 	attr_walk_up_info *info = (attr_walk_up_info *)ref;
483 	git_attr_file_source src[GIT_ATTR_FILE_NUM_SOURCES];
484 	int error = 0, n_src, i;
485 	bool allow_macros;
486 
487 	n_src = attr_decide_sources(
488 		info->flags, info->workdir != NULL, info->index != NULL, src);
489 	allow_macros = info->workdir ? !strcmp(info->workdir, path) : false;
490 
491 	for (i = 0; !error && i < n_src; ++i)
492 		error = push_attr_file(info->repo, info->attr_session, info->files,
493 				       src[i], path, GIT_ATTR_FILE, allow_macros);
494 
495 	return error;
496 }
497 
release_attr_files(git_vector * files)498 static void release_attr_files(git_vector *files)
499 {
500 	size_t i;
501 	git_attr_file *file;
502 
503 	git_vector_foreach(files, i, file) {
504 		git_attr_file__free(file);
505 		files->contents[i] = NULL;
506 	}
507 	git_vector_free(files);
508 }
509 
collect_attr_files(git_repository * repo,git_attr_session * attr_session,uint32_t flags,const char * path,git_vector * files)510 static int collect_attr_files(
511 	git_repository *repo,
512 	git_attr_session *attr_session,
513 	uint32_t flags,
514 	const char *path,
515 	git_vector *files)
516 {
517 	int error = 0;
518 	git_buf dir = GIT_BUF_INIT, attrfile = GIT_BUF_INIT;
519 	const char *workdir = git_repository_workdir(repo);
520 	attr_walk_up_info info = { NULL };
521 
522 	if ((error = attr_setup(repo, attr_session, flags)) < 0)
523 		return error;
524 
525 	/* Resolve path in a non-bare repo */
526 	if (workdir != NULL)
527 		error = git_path_find_dir(&dir, path, workdir);
528 	else
529 		error = git_path_dirname_r(&dir, path);
530 	if (error < 0)
531 		goto cleanup;
532 
533 	/* in precendence order highest to lowest:
534 	 * - $GIT_DIR/info/attributes
535 	 * - path components with .gitattributes
536 	 * - config core.attributesfile
537 	 * - $GIT_PREFIX/etc/gitattributes
538 	 */
539 
540 	if ((error = git_repository_item_path(&attrfile, repo, GIT_REPOSITORY_ITEM_INFO)) < 0 ||
541 	    (error = push_attr_file(repo, attr_session, files, GIT_ATTR_FILE__FROM_FILE,
542 				    attrfile.ptr, GIT_ATTR_FILE_INREPO, true)) < 0) {
543 		if (error != GIT_ENOTFOUND)
544 			goto cleanup;
545 	}
546 
547 	info.repo = repo;
548 	info.attr_session = attr_session;
549 	info.flags = flags;
550 	info.workdir = workdir;
551 	if (git_repository_index__weakptr(&info.index, repo) < 0)
552 		git_error_clear(); /* no error even if there is no index */
553 	info.files = files;
554 
555 	if (!strcmp(dir.ptr, "."))
556 		error = push_one_attr(&info, "");
557 	else
558 		error = git_path_walk_up(&dir, workdir, push_one_attr, &info);
559 
560 	if (error < 0)
561 		goto cleanup;
562 
563 	if (git_repository_attr_cache(repo)->cfg_attr_file != NULL) {
564 		error = push_attr_file(repo, attr_session, files, GIT_ATTR_FILE__FROM_FILE,
565 				       NULL, git_repository_attr_cache(repo)->cfg_attr_file, true);
566 		if (error < 0)
567 			goto cleanup;
568 	}
569 
570 	if ((flags & GIT_ATTR_CHECK_NO_SYSTEM) == 0) {
571 		error = system_attr_file(&dir, attr_session);
572 
573 		if (!error)
574 			error = push_attr_file(repo, attr_session, files, GIT_ATTR_FILE__FROM_FILE,
575 					       NULL, dir.ptr, true);
576 		else if (error == GIT_ENOTFOUND)
577 			error = 0;
578 	}
579 
580  cleanup:
581 	if (error < 0)
582 		release_attr_files(files);
583 	git_buf_dispose(&attrfile);
584 	git_buf_dispose(&dir);
585 
586 	return error;
587 }
588