/* * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. */ #include "common.h" #include "fileops.h" #include "hash.h" #include "filter.h" #include "repository.h" #include "global.h" #include "git2/sys/filter.h" #include "git2/config.h" #include "blob.h" #include "attr_file.h" #include "array.h" struct git_filter_source { git_repository *repo; const char *path; git_oid oid; /* zero if unknown (which is likely) */ uint16_t filemode; /* zero if unknown */ git_filter_mode_t mode; }; typedef struct { git_filter *filter; void *payload; } git_filter_entry; struct git_filter_list { git_array_t(git_filter_entry) filters; git_filter_source source; char path[GIT_FLEX_ARRAY]; }; typedef struct { const char *filter_name; git_filter *filter; int priority; int initialized; size_t nattrs, nmatches; char *attrdata; const char *attrs[GIT_FLEX_ARRAY]; } git_filter_def; static int filter_def_priority_cmp(const void *a, const void *b) { int pa = ((const git_filter_def *)a)->priority; int pb = ((const git_filter_def *)b)->priority; return (pa < pb) ? -1 : (pa > pb) ? 1 : 0; } struct filter_registry { git_vector filters; }; static struct filter_registry *git__filter_registry = NULL; static void filter_registry_shutdown(void) { struct filter_registry *reg = NULL; size_t pos; git_filter_def *fdef; if ((reg = git__swap(git__filter_registry, NULL)) == NULL) return; git_vector_foreach(®->filters, pos, fdef) { if (fdef->initialized && fdef->filter && fdef->filter->shutdown) { fdef->filter->shutdown(fdef->filter); fdef->initialized = false; } git__free(fdef->attrdata); git__free(fdef); } git_vector_free(®->filters); git__free(reg); } static int filter_registry_initialize(void) { int error = 0; struct filter_registry *reg; if (git__filter_registry) return 0; reg = git__calloc(1, sizeof(struct filter_registry)); GITERR_CHECK_ALLOC(reg); if ((error = git_vector_init( ®->filters, 2, filter_def_priority_cmp)) < 0) goto cleanup; reg = git__compare_and_swap(&git__filter_registry, NULL, reg); if (reg != NULL) goto cleanup; git__on_shutdown(filter_registry_shutdown); /* try to register both default filters */ { git_filter *crlf = git_crlf_filter_new(); git_filter *ident = git_ident_filter_new(); if (crlf && git_filter_register( GIT_FILTER_CRLF, crlf, GIT_FILTER_CRLF_PRIORITY) < 0) crlf = NULL; if (ident && git_filter_register( GIT_FILTER_IDENT, ident, GIT_FILTER_IDENT_PRIORITY) < 0) ident = NULL; if (!crlf || !ident) return -1; } return 0; cleanup: git_vector_free(®->filters); git__free(reg); return error; } static int filter_def_scan_attrs( git_buf *attrs, size_t *nattr, size_t *nmatch, const char *attr_str) { const char *start, *scan = attr_str; int has_eq; *nattr = *nmatch = 0; if (!scan) return 0; while (*scan) { while (git__isspace(*scan)) scan++; for (start = scan, has_eq = 0; *scan && !git__isspace(*scan); ++scan) { if (*scan == '=') has_eq = 1; } if (scan > start) { (*nattr)++; if (has_eq || *start == '-' || *start == '+' || *start == '!') (*nmatch)++; if (has_eq) git_buf_putc(attrs, '='); git_buf_put(attrs, start, scan - start); git_buf_putc(attrs, '\0'); } } return 0; } static void filter_def_set_attrs(git_filter_def *fdef) { char *scan = fdef->attrdata; size_t i; for (i = 0; i < fdef->nattrs; ++i) { const char *name, *value; switch (*scan) { case '=': name = scan + 1; for (scan++; *scan != '='; scan++) /* find '=' */; *scan++ = '\0'; value = scan; break; case '-': name = scan + 1; value = git_attr__false; break; case '+': name = scan + 1; value = git_attr__true; break; case '!': name = scan + 1; value = git_attr__unset; break; default: name = scan; value = NULL; break; } fdef->attrs[i] = name; fdef->attrs[i + fdef->nattrs] = value; scan += strlen(scan) + 1; } } static int filter_def_name_key_check(const void *key, const void *fdef) { const char *name = fdef ? ((const git_filter_def *)fdef)->filter_name : NULL; return name ? git__strcmp(key, name) : -1; } static int filter_def_filter_key_check(const void *key, const void *fdef) { const void *filter = fdef ? ((const git_filter_def *)fdef)->filter : NULL; return (key == filter) ? 0 : -1; } static int filter_registry_find(size_t *pos, const char *name) { return git_vector_search2( pos, &git__filter_registry->filters, filter_def_name_key_check, name); } static git_filter_def *filter_registry_lookup(size_t *pos, const char *name) { git_filter_def *fdef = NULL; if (!filter_registry_find(pos, name)) fdef = git_vector_get(&git__filter_registry->filters, *pos); return fdef; } int git_filter_register( const char *name, git_filter *filter, int priority) { git_filter_def *fdef; size_t nattr = 0, nmatch = 0; git_buf attrs = GIT_BUF_INIT; if (filter_registry_initialize() < 0) return -1; if (!filter_registry_find(NULL, name)) { giterr_set( GITERR_FILTER, "Attempt to reregister existing filter '%s'", name); return GIT_EEXISTS; } if (filter_def_scan_attrs(&attrs, &nattr, &nmatch, filter->attributes) < 0) return -1; fdef = git__calloc( sizeof(git_filter_def) + 2 * nattr * sizeof(char *), 1); GITERR_CHECK_ALLOC(fdef); fdef->filter_name = name; fdef->filter = filter; fdef->priority = priority; fdef->nattrs = nattr; fdef->nmatches = nmatch; fdef->attrdata = git_buf_detach(&attrs); filter_def_set_attrs(fdef); if (git_vector_insert(&git__filter_registry->filters, fdef) < 0) { git__free(fdef->attrdata); git__free(fdef); return -1; } git_vector_sort(&git__filter_registry->filters); return 0; } int git_filter_unregister(const char *name) { size_t pos; git_filter_def *fdef; /* cannot unregister default filters */ if (!strcmp(GIT_FILTER_CRLF, name) || !strcmp(GIT_FILTER_IDENT, name)) { giterr_set(GITERR_FILTER, "Cannot unregister filter '%s'", name); return -1; } if ((fdef = filter_registry_lookup(&pos, name)) == NULL) { giterr_set(GITERR_FILTER, "Cannot find filter '%s' to unregister", name); return GIT_ENOTFOUND; } (void)git_vector_remove(&git__filter_registry->filters, pos); if (fdef->initialized && fdef->filter && fdef->filter->shutdown) { fdef->filter->shutdown(fdef->filter); fdef->initialized = false; } git__free(fdef->attrdata); git__free(fdef); return 0; } static int filter_initialize(git_filter_def *fdef) { int error = 0; if (!fdef->initialized && fdef->filter && fdef->filter->initialize && (error = fdef->filter->initialize(fdef->filter)) < 0) { /* auto-unregister if initialize fails */ git_filter_unregister(fdef->filter_name); return error; } fdef->initialized = true; return 0; } git_filter *git_filter_lookup(const char *name) { size_t pos; git_filter_def *fdef; if (filter_registry_initialize() < 0) return NULL; if ((fdef = filter_registry_lookup(&pos, name)) == NULL) return NULL; if (!fdef->initialized && filter_initialize(fdef) < 0) return NULL; return fdef->filter; } void git_filter_free(git_filter *filter) { git__free(filter); } git_repository *git_filter_source_repo(const git_filter_source *src) { return src->repo; } const char *git_filter_source_path(const git_filter_source *src) { return src->path; } uint16_t git_filter_source_filemode(const git_filter_source *src) { return src->filemode; } const git_oid *git_filter_source_id(const git_filter_source *src) { return git_oid_iszero(&src->oid) ? NULL : &src->oid; } git_filter_mode_t git_filter_source_mode(const git_filter_source *src) { return src->mode; } static int filter_list_new( git_filter_list **out, const git_filter_source *src) { git_filter_list *fl = NULL; size_t pathlen = src->path ? strlen(src->path) : 0; fl = git__calloc(1, sizeof(git_filter_list) + pathlen + 1); GITERR_CHECK_ALLOC(fl); if (src->path) memcpy(fl->path, src->path, pathlen); fl->source.repo = src->repo; fl->source.path = fl->path; fl->source.mode = src->mode; *out = fl; return 0; } static int filter_list_check_attributes( const char ***out, git_filter_def *fdef, const git_filter_source *src) { int error; size_t i; const char **strs = git__calloc(fdef->nattrs, sizeof(const char *)); GITERR_CHECK_ALLOC(strs); error = git_attr_get_many( strs, src->repo, 0, src->path, fdef->nattrs, fdef->attrs); /* if no values were found but no matches are needed, it's okay! */ if (error == GIT_ENOTFOUND && !fdef->nmatches) { giterr_clear(); git__free((void *)strs); return 0; } for (i = 0; !error && i < fdef->nattrs; ++i) { const char *want = fdef->attrs[fdef->nattrs + i]; git_attr_t want_type, found_type; if (!want) continue; want_type = git_attr_value(want); found_type = git_attr_value(strs[i]); if (want_type != found_type || (want_type == GIT_ATTR_VALUE_T && strcmp(want, strs[i]))) error = GIT_ENOTFOUND; } if (error) git__free((void *)strs); else *out = strs; return error; } int git_filter_list_new( git_filter_list **out, git_repository *repo, git_filter_mode_t mode) { git_filter_source src = { 0 }; src.repo = repo; src.path = NULL; src.mode = mode; return filter_list_new(out, &src); } int git_filter_list_load( git_filter_list **filters, git_repository *repo, git_blob *blob, /* can be NULL */ const char *path, git_filter_mode_t mode) { int error = 0; git_filter_list *fl = NULL; git_filter_source src = { 0 }; git_filter_entry *fe; size_t idx; git_filter_def *fdef; if (filter_registry_initialize() < 0) return -1; src.repo = repo; src.path = path; src.mode = mode; if (blob) git_oid_cpy(&src.oid, git_blob_id(blob)); git_vector_foreach(&git__filter_registry->filters, idx, fdef) { const char **values = NULL; void *payload = NULL; if (!fdef || !fdef->filter) continue; if (fdef->nattrs > 0) { error = filter_list_check_attributes(&values, fdef, &src); if (error == GIT_ENOTFOUND) { error = 0; continue; } else if (error < 0) break; } if (!fdef->initialized && (error = filter_initialize(fdef)) < 0) break; if (fdef->filter->check) error = fdef->filter->check( fdef->filter, &payload, &src, values); git__free((void *)values); if (error == GIT_PASSTHROUGH) error = 0; else if (error < 0) break; else { if (!fl && (error = filter_list_new(&fl, &src)) < 0) return error; fe = git_array_alloc(fl->filters); GITERR_CHECK_ALLOC(fe); fe->filter = fdef->filter; fe->payload = payload; } } if (error && fl != NULL) { git_array_clear(fl->filters); git__free(fl); fl = NULL; } *filters = fl; return error; } void git_filter_list_free(git_filter_list *fl) { uint32_t i; if (!fl) return; for (i = 0; i < git_array_size(fl->filters); ++i) { git_filter_entry *fe = git_array_get(fl->filters, i); if (fe->filter->cleanup) fe->filter->cleanup(fe->filter, fe->payload); } git_array_clear(fl->filters); git__free(fl); } int git_filter_list_push( git_filter_list *fl, git_filter *filter, void *payload) { int error = 0; size_t pos; git_filter_def *fdef; git_filter_entry *fe; assert(fl && filter); if (git_vector_search2( &pos, &git__filter_registry->filters, filter_def_filter_key_check, filter) < 0) { giterr_set(GITERR_FILTER, "Cannot use an unregistered filter"); return -1; } fdef = git_vector_get(&git__filter_registry->filters, pos); if (!fdef->initialized && (error = filter_initialize(fdef)) < 0) return error; fe = git_array_alloc(fl->filters); GITERR_CHECK_ALLOC(fe); fe->filter = filter; fe->payload = payload; return 0; } size_t git_filter_list_length(const git_filter_list *fl) { return fl ? git_array_size(fl->filters) : 0; } static int filter_list_out_buffer_from_raw( git_buf *out, const void *ptr, size_t size) { if (git_buf_is_allocated(out)) git_buf_free(out); if (!size) { git_buf_init(out, 0); } else { out->ptr = (char *)ptr; out->asize = 0; out->size = size; } return 0; } int git_filter_list_apply_to_data( git_buf *tgt, git_filter_list *fl, git_buf *src) { int error = 0; uint32_t i; git_buf *dbuffer[2], local = GIT_BUF_INIT; unsigned int si = 0; if (!fl) return filter_list_out_buffer_from_raw(tgt, src->ptr, src->size); dbuffer[0] = src; dbuffer[1] = tgt; /* if `src` buffer is reallocable, then use it, otherwise copy it */ if (!git_buf_is_allocated(src)) { if (git_buf_set(&local, src->ptr, src->size) < 0) return -1; dbuffer[0] = &local; } for (i = 0; i < git_array_size(fl->filters); ++i) { unsigned int di = 1 - si; uint32_t fidx = (fl->source.mode == GIT_FILTER_TO_WORKTREE) ? i : git_array_size(fl->filters) - 1 - i; git_filter_entry *fe = git_array_get(fl->filters, fidx); dbuffer[di]->size = 0; /* Apply the filter from dbuffer[src] to the other buffer; * if the filtering is canceled by the user mid-filter, * we skip to the next filter without changing the source * of the double buffering (so that the text goes through * cleanly). */ error = fe->filter->apply( fe->filter, &fe->payload, dbuffer[di], dbuffer[si], &fl->source); if (error == GIT_PASSTHROUGH) { /* PASSTHROUGH means filter decided not to process the buffer */ error = 0; } else if (!error) { git_buf_shorten(dbuffer[di], 0); /* force NUL termination */ si = di; /* swap buffers */ } else { tgt->size = 0; return error; } } /* Ensure that the output ends up in dbuffer[1] (i.e. the dest) */ if (si != 1) git_buf_swap(dbuffer[0], dbuffer[1]); git_buf_free(&local); /* don't leak if we allocated locally */ return 0; } int git_filter_list_apply_to_file( git_buf *out, git_filter_list *filters, git_repository *repo, const char *path) { int error; const char *base = repo ? git_repository_workdir(repo) : NULL; git_buf abspath = GIT_BUF_INIT, raw = GIT_BUF_INIT; if (!(error = git_path_join_unrooted(&abspath, path, base, NULL)) && !(error = git_futils_readbuffer(&raw, abspath.ptr))) { error = git_filter_list_apply_to_data(out, filters, &raw); git_buf_free(&raw); } git_buf_free(&abspath); return error; } int git_filter_list_apply_to_blob( git_buf *out, git_filter_list *filters, git_blob *blob) { git_buf in = GIT_BUF_INIT; git_off_t rawsize = git_blob_rawsize(blob); if (!git__is_sizet(rawsize)) { giterr_set(GITERR_OS, "Blob is too large to filter"); return -1; } in.ptr = (char *)git_blob_rawcontent(blob); in.asize = 0; in.size = (size_t)rawsize; if (filters) git_oid_cpy(&filters->source.oid, git_blob_id(blob)); return git_filter_list_apply_to_data(out, filters, &in); }