]> git.proxmox.com Git - libgit2.git/blobdiff - src/attr_file.c
New upstream version 1.1.0+dfsg.1
[libgit2.git] / src / attr_file.c
index 11d149358daa2aab0d33cc0daf44175a9480d1e0..3f69b5ffcb3ebe9558aa8d033069bd2d8ad47366 100644 (file)
@@ -1,11 +1,21 @@
-#include "common.h"
+/*
+ * 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 "attr_file.h"
+
 #include "repository.h"
 #include "filebuf.h"
-#include "attr_file.h"
 #include "attrcache.h"
+#include "buf_text.h"
 #include "git2/blob.h"
 #include "git2/tree.h"
+#include "blob.h"
 #include "index.h"
+#include "wildmatch.h"
 #include <ctype.h>
 
 static void attr_file_free(git_attr_file *file)
@@ -27,20 +37,25 @@ int git_attr_file__new(
        git_attr_file_source source)
 {
        git_attr_file *attrs = git__calloc(1, sizeof(git_attr_file));
-       GITERR_CHECK_ALLOC(attrs);
+       GIT_ERROR_CHECK_ALLOC(attrs);
 
        if (git_mutex_init(&attrs->lock) < 0) {
-               giterr_set(GITERR_OS, "Failed to initialize lock");
-               git__free(attrs);
-               return -1;
+               git_error_set(GIT_ERROR_OS, "failed to initialize lock");
+               goto on_error;
        }
 
-       git_pool_init(&attrs->pool, 1);
+       if (git_pool_init(&attrs->pool, 1) < 0)
+               goto on_error;
+
        GIT_REFCOUNT_INC(attrs);
        attrs->entry  = entry;
        attrs->source = source;
        *out = attrs;
        return 0;
+
+on_error:
+       git__free(attrs);
+       return -1;
 }
 
 int git_attr_file__clear_rules(git_attr_file *file, bool need_lock)
@@ -49,7 +64,7 @@ int git_attr_file__clear_rules(git_attr_file *file, bool need_lock)
        git_attr_rule *rule;
 
        if (need_lock && git_mutex_lock(&file->lock) < 0) {
-               giterr_set(GITERR_OS, "Failed to lock attribute file");
+               git_error_set(GIT_ERROR_OS, "failed to lock attribute file");
                return -1;
        }
 
@@ -95,14 +110,22 @@ int git_attr_file__load(
        git_attr_session *attr_session,
        git_attr_file_entry *entry,
        git_attr_file_source source,
-       git_attr_file_parser parser)
+       git_attr_file_parser parser,
+       bool allow_macros)
 {
        int error = 0;
+       git_tree *tree = NULL;
+       git_tree_entry *tree_entry = NULL;
        git_blob *blob = NULL;
        git_buf content = GIT_BUF_INIT;
+       const char *content_str;
        git_attr_file *file;
        struct stat st;
        bool nonexistent = false;
+       int bom_offset;
+       git_bom_t bom;
+       git_oid id;
+       git_object_size_t blobsize;
 
        *out = NULL;
 
@@ -111,15 +134,16 @@ int git_attr_file__load(
                /* in-memory attribute file doesn't need data */
                break;
        case GIT_ATTR_FILE__FROM_INDEX: {
-               git_oid id;
-
                if ((error = attr_file_oid_from_index(&id, repo, entry->path)) < 0 ||
                        (error = git_blob_lookup(&blob, repo, &id)) < 0)
                        return error;
 
                /* Do not assume that data straight from the ODB is NULL-terminated;
                 * copy the contents of a file to a buffer to work on */
-               git_buf_put(&content, git_blob_rawcontent(blob), git_blob_rawsize(blob));
+               blobsize = git_blob_rawsize(blob);
+
+               GIT_ERROR_CHECK_BLOBSIZE(blobsize);
+               git_buf_put(&content, git_blob_rawcontent(blob), (size_t)blobsize);
                break;
        }
        case GIT_ATTR_FILE__FROM_FILE: {
@@ -139,21 +163,47 @@ int git_attr_file__load(
 
                break;
        }
+       case GIT_ATTR_FILE__FROM_HEAD: {
+               if ((error = git_repository_head_tree(&tree, repo)) < 0 ||
+                   (error = git_tree_entry_bypath(&tree_entry, tree, entry->path)) < 0 ||
+                   (error = git_blob_lookup(&blob, repo, git_tree_entry_id(tree_entry))) < 0)
+                       goto cleanup;
+
+               /*
+                * Do not assume that data straight from the ODB is NULL-terminated;
+                * copy the contents of a file to a buffer to work on.
+                */
+               blobsize = git_blob_rawsize(blob);
+
+               GIT_ERROR_CHECK_BLOBSIZE(blobsize);
+               if ((error = git_buf_put(&content,
+                       git_blob_rawcontent(blob), (size_t)blobsize)) < 0)
+                       goto cleanup;
+
+               break;
+       }
        default:
-               giterr_set(GITERR_INVALID, "Unknown file source %d", source);
+               git_error_set(GIT_ERROR_INVALID, "unknown file source %d", source);
                return -1;
        }
 
        if ((error = git_attr_file__new(&file, entry, source)) < 0)
                goto cleanup;
 
+       /* advance over a UTF8 BOM */
+       content_str = git_buf_cstr(&content);
+       bom_offset = git_buf_text_detect_bom(&bom, &content);
+
+       if (bom == GIT_BOM_UTF8)
+               content_str += bom_offset;
+
        /* store the key of the attr_reader; don't bother with cache
         * invalidation during the same attr reader session.
         */
        if (attr_session)
                file->session_key = attr_session->key;
 
-       if (parser && (error = parser(repo, file, git_buf_cstr(&content))) < 0) {
+       if (parser && (error = parser(repo, file, content_str, allow_macros)) < 0) {
                git_attr_file__free(file);
                goto cleanup;
        }
@@ -163,6 +213,8 @@ int git_attr_file__load(
                file->nonexistent = 1;
        else if (source == GIT_ATTR_FILE__FROM_INDEX)
                git_oid_cpy(&file->cache_data.oid, git_blob_id(blob));
+       else if (source == GIT_ATTR_FILE__FROM_HEAD)
+               git_oid_cpy(&file->cache_data.oid, git_tree_id(tree));
        else if (source == GIT_ATTR_FILE__FROM_FILE)
                git_futils_filestamp_set_from_stat(&file->cache_data.stamp, &st);
        /* else always cacheable */
@@ -171,7 +223,9 @@ int git_attr_file__load(
 
 cleanup:
        git_blob_free(blob);
-       git_buf_free(&content);
+       git_tree_entry_free(tree_entry);
+       git_tree_free(tree);
+       git_buf_dispose(&content);
 
        return error;
 }
@@ -211,8 +265,21 @@ int git_attr_file__out_of_date(
                return (git_oid__cmp(&file->cache_data.oid, &id) != 0);
        }
 
+       case GIT_ATTR_FILE__FROM_HEAD: {
+               git_tree *tree;
+               int error;
+
+               if ((error = git_repository_head_tree(&tree, repo)) < 0)
+                       return error;
+
+               error = git_oid__cmp(&file->cache_data.oid, git_tree_id(tree));
+
+               git_tree_free(tree);
+               return error;
+       }
+
        default:
-               giterr_set(GITERR_INVALID, "Invalid file type %d", file->source);
+               git_error_set(GIT_ERROR_INVALID, "invalid file type %d", file->source);
                return -1;
        }
 }
@@ -225,56 +292,55 @@ static bool parse_optimized_patterns(
        const char *pattern);
 
 int git_attr_file__parse_buffer(
-       git_repository *repo, git_attr_file *attrs, const char *data)
+       git_repository *repo, git_attr_file *attrs, const char *data, bool allow_macros)
 {
-       int error = 0;
        const char *scan = data, *context = NULL;
        git_attr_rule *rule = NULL;
+       int error = 0;
 
-       /* if subdir file path, convert context for file paths */
-       if (attrs->entry &&
-               git_path_root(attrs->entry->path) < 0 &&
-               !git__suffixcmp(attrs->entry->path, "/" GIT_ATTR_FILE))
+       /* If subdir file path, convert context for file paths */
+       if (attrs->entry && git_path_root(attrs->entry->path) < 0 &&
+           !git__suffixcmp(attrs->entry->path, "/" GIT_ATTR_FILE))
                context = attrs->entry->path;
 
        if (git_mutex_lock(&attrs->lock) < 0) {
-               giterr_set(GITERR_OS, "Failed to lock attribute file");
+               git_error_set(GIT_ERROR_OS, "failed to lock attribute file");
                return -1;
        }
 
        while (!error && *scan) {
-               /* allocate rule if needed */
-               if (!rule && !(rule = git__calloc(1, sizeof(*rule)))) {
-                       error = -1;
-                       break;
-               }
-
-               rule->match.flags =
-                       GIT_ATTR_FNMATCH_ALLOWNEG | GIT_ATTR_FNMATCH_ALLOWMACRO;
-
-               /* parse the next "pattern attr attr attr" line */
-               if (!(error = git_attr_fnmatch__parse(
-                               &rule->match, &attrs->pool, context, &scan)) &&
-                       !(error = git_attr_assignment__parse(
-                               repo, &attrs->pool, &rule->assigns, &scan)))
+               /* Allocate rule if needed, otherwise re-use previous rule */
+               if (!rule) {
+                       rule = git__calloc(1, sizeof(*rule));
+                       GIT_ERROR_CHECK_ALLOC(rule);
+               } else
+                       git_attr_rule__clear(rule);
+
+               rule->match.flags = GIT_ATTR_FNMATCH_ALLOWNEG | GIT_ATTR_FNMATCH_ALLOWMACRO;
+
+               /* Parse the next "pattern attr attr attr" line */
+               if ((error = git_attr_fnmatch__parse(&rule->match, &attrs->pool, context, &scan)) < 0 ||
+                   (error = git_attr_assignment__parse(repo, &attrs->pool, &rule->assigns, &scan)) < 0)
                {
-                       if (rule->match.flags & GIT_ATTR_FNMATCH_MACRO)
-                               /* TODO: warning if macro found in file below repo root */
-                               error = git_attr_cache__insert_macro(repo, rule);
-                       else
-                               error = git_vector_insert(&attrs->rules, rule);
+                       if (error != GIT_ENOTFOUND)
+                               goto out;
+                       error = 0;
+                       continue;
                }
 
-               /* if the rule wasn't a pattern, on to the next */
-               if (error < 0) {
-                       git_attr_rule__clear(rule); /* reset rule contents */
-                       if (error == GIT_ENOTFOUND)
-                               error = 0;
-               } else {
-                       rule = NULL; /* vector now "owns" the rule */
-               }
+               if (rule->match.flags & GIT_ATTR_FNMATCH_MACRO) {
+                       /* TODO: warning if macro found in file below repo root */
+                       if (!allow_macros)
+                               continue;
+                       if ((error = git_attr_cache__insert_macro(repo, rule)) < 0)
+                               goto out;
+               } else if ((error = git_vector_insert(&attrs->rules, rule)) < 0)
+                       goto out;
+
+               rule = NULL;
        }
 
+out:
        git_mutex_unlock(&attrs->lock);
        git_attr_rule__free(rule);
 
@@ -321,33 +387,28 @@ int git_attr_file__lookup_one(
 
 int git_attr_file__load_standalone(git_attr_file **out, const char *path)
 {
-       int error;
-       git_attr_file *file;
        git_buf content = GIT_BUF_INIT;
+       git_attr_file *file = NULL;
+       int error;
 
-       error = git_attr_file__new(&file, NULL, GIT_ATTR_FILE__FROM_FILE);
-       if (error < 0)
-               return error;
+       if ((error = git_futils_readbuffer(&content, path)) < 0)
+               goto out;
 
-       error = git_attr_cache__alloc_file_entry(
-               &file->entry, NULL, path, &file->pool);
-       if (error < 0) {
-               git_attr_file__free(file);
-               return error;
-       }
-       /* because the cache entry is allocated from the file's own pool, we
+       /*
+        * Because the cache entry is allocated from the file's own pool, we
         * don't have to free it - freeing file+pool will free cache entry, too.
         */
 
-       if (!(error = git_futils_readbuffer(&content, path))) {
-               error = git_attr_file__parse_buffer(NULL, file, content.ptr);
-               git_buf_free(&content);
-       }
+       if ((error = git_attr_file__new(&file, NULL, GIT_ATTR_FILE__FROM_FILE)) < 0 ||
+           (error = git_attr_file__parse_buffer(NULL, file, content.ptr, true)) < 0 ||
+           (error = git_attr_cache__alloc_file_entry(&file->entry, NULL, path, &file->pool)) < 0)
+               goto out;
 
+       *out = file;
+out:
        if (error < 0)
                git_attr_file__free(file);
-       else
-               *out = file;
+       git_buf_dispose(&content);
 
        return error;
 }
@@ -378,30 +439,27 @@ bool git_attr_fnmatch__match(
        }
 
        if (match->flags & GIT_ATTR_FNMATCH_ICASE)
-               flags |= FNM_CASEFOLD;
-       if (match->flags & GIT_ATTR_FNMATCH_LEADINGDIR)
-               flags |= FNM_LEADING_DIR;
+               flags |= WM_CASEFOLD;
 
        if (match->flags & GIT_ATTR_FNMATCH_FULLPATH) {
                filename = relpath;
-               flags |= FNM_PATHNAME;
+               flags |= WM_PATHNAME;
        } else {
                filename = path->basename;
-
-               if (path->is_dir)
-                       flags |= FNM_LEADING_DIR;
        }
 
        if ((match->flags & GIT_ATTR_FNMATCH_DIRECTORY) && !path->is_dir) {
                bool samename;
 
-               /* for attribute checks or root ignore checks, fail match */
+               /*
+                * for attribute checks or checks at the root of this match's
+                * containing_dir (or root of the repository if no containing_dir),
+                * do not match.
+                */
                if (!(match->flags & GIT_ATTR_FNMATCH_IGNORE) ||
-                       path->basename == path->path)
+                       path->basename == relpath)
                        return false;
 
-               flags |= FNM_LEADING_DIR;
-
                /* fail match if this is a file with same name as ignored folder */
                samename = (match->flags & GIT_ATTR_FNMATCH_ICASE) ?
                        !strcasecmp(match->pattern, relpath) :
@@ -410,22 +468,10 @@ bool git_attr_fnmatch__match(
                if (samename)
                        return false;
 
-               return (p_fnmatch(match->pattern, relpath, flags) != FNM_NOMATCH);
+               return (wildmatch(match->pattern, relpath, flags) == WM_MATCH);
        }
 
-       /* if path is a directory prefix of a negated pattern, then match */
-       if ((match->flags & GIT_ATTR_FNMATCH_NEGATIVE) && path->is_dir) {
-               size_t pathlen = strlen(relpath);
-               bool prefixed = (pathlen <= match->length) &&
-                       ((match->flags & GIT_ATTR_FNMATCH_ICASE) ?
-                       !strncasecmp(match->pattern, relpath, pathlen) :
-                       !strncmp(match->pattern, relpath, pathlen));
-
-               if (prefixed && git_path_at_end_of_segment(&match->pattern[pathlen]))
-                       return true;
-       }
-
-       return (p_fnmatch(match->pattern, filename, flags) != FNM_NOMATCH);
+       return (wildmatch(match->pattern, filename, flags) == WM_MATCH);
 }
 
 bool git_attr_rule__match(
@@ -507,7 +553,7 @@ int git_attr_path__init(
 
 void git_attr_path__free(git_attr_path *info)
 {
-       git_buf_free(&info->full);
+       git_buf_dispose(&info->full);
        info->path = NULL;
        info->basename = NULL;
 }
@@ -544,6 +590,61 @@ void git_attr_path__free(git_attr_path *info)
  *   "cat-file.c" but not "mozilla-sha1/sha1.c".
  */
 
+/*
+ * Determine the length of trailing spaces. Escaped spaces do not count as
+ * trailing whitespace.
+ */
+static size_t trailing_space_length(const char *p, size_t len)
+{
+       size_t n, i;
+       for (n = len; n; n--) {
+               if (p[n-1] != ' ' && p[n-1] != '\t')
+                       break;
+
+               /*
+                * Count escape-characters before space. In case where it's an
+                * even number of escape characters, then the escape char itself
+                * is escaped and the whitespace is an unescaped whitespace.
+                * Otherwise, the last escape char is not escaped and the
+                * whitespace in an escaped whitespace.
+                */
+               i = n;
+               while (i > 1 && p[i-2] == '\\')
+                       i--;
+               if ((n - i) % 2)
+                       break;
+       }
+       return len - n;
+}
+
+static size_t unescape_spaces(char *str)
+{
+       char *scan, *pos = str;
+       bool escaped = false;
+
+       if (!str)
+               return 0;
+
+       for (scan = str; *scan; scan++) {
+               if (!escaped && *scan == '\\') {
+                       escaped = true;
+                       continue;
+               }
+
+               /* Only insert the escape character for escaped non-spaces */
+               if (escaped && !git__isspace(*scan))
+                       *pos++ = '\\';
+
+               *pos++ = *scan;
+               escaped = false;
+       }
+
+       if (pos != scan)
+               *pos = '\0';
+
+       return (pos - str);
+}
+
 /*
  * This will return 0 if the spec was filled out,
  * GIT_ENOTFOUND if the fnmatch does not require matching, or
@@ -557,6 +658,7 @@ int git_attr_fnmatch__parse(
 {
        const char *pattern, *scan;
        int slash_count, allow_space;
+       bool escaped;
 
        assert(spec && base && *base);
 
@@ -568,8 +670,11 @@ int git_attr_fnmatch__parse(
 
        pattern = *base;
 
-       while (git__isspace(*pattern)) pattern++;
-       if (!*pattern || *pattern == '#') {
+       while (!allow_space && git__isspace(*pattern))
+               pattern++;
+
+       if (!*pattern || *pattern == '#' || *pattern == '\n' ||
+           (*pattern == '\r' && *(pattern + 1) == '\n')) {
                *base = git__next_line(pattern);
                return GIT_ENOTFOUND;
        }
@@ -583,29 +688,34 @@ int git_attr_fnmatch__parse(
        }
 
        if (*pattern == '!' && (spec->flags & GIT_ATTR_FNMATCH_ALLOWNEG) != 0) {
-               spec->flags = spec->flags |
-                       GIT_ATTR_FNMATCH_NEGATIVE | GIT_ATTR_FNMATCH_LEADINGDIR;
+               spec->flags = spec->flags | GIT_ATTR_FNMATCH_NEGATIVE;
                pattern++;
        }
 
        slash_count = 0;
+       escaped = false;
+       /* Scan until a non-escaped whitespace. */
        for (scan = pattern; *scan != '\0'; ++scan) {
-               /* scan until (non-escaped) white space */
-               if (git__isspace(*scan) && *(scan - 1) != '\\') {
-                       if (!allow_space || (*scan != ' ' && *scan != '\t' && *scan != '\r'))
-                               break;
-               }
+               char c = *scan;
 
-               if (*scan == '/') {
+               if (c == '\\' && !escaped) {
+                       escaped = true;
+                       continue;
+               } else if (git__isspace(c) && !escaped) {
+                       if (!allow_space || (c != ' ' && c != '\t' && c != '\r'))
+                               break;
+               } else if (c == '/') {
                        spec->flags = spec->flags | GIT_ATTR_FNMATCH_FULLPATH;
                        slash_count++;
-                       if (pattern == scan)
+
+                       if (slash_count == 1 && pattern == scan)
                                pattern++;
-               }
-               /* remember if we see an unescaped wildcard in pattern */
-               else if (git__iswildcard(*scan) &&
-                       (scan == pattern || (*(scan - 1) != '\\')))
+               } else if (git__iswildcard(c) && !escaped) {
+                       /* remember if we see an unescaped wildcard in pattern */
                        spec->flags = spec->flags | GIT_ATTR_FNMATCH_HASWILD;
+               }
+
+               escaped = false;
        }
 
        *base = scan;
@@ -622,20 +732,18 @@ int git_attr_fnmatch__parse(
                if (--spec->length == 0)
                        return GIT_ENOTFOUND;
 
+       /* Remove trailing spaces. */
+       spec->length -= trailing_space_length(pattern, spec->length);
+
+       if (spec->length == 0)
+               return GIT_ENOTFOUND;
+
        if (pattern[spec->length - 1] == '/') {
                spec->length--;
                spec->flags = spec->flags | GIT_ATTR_FNMATCH_DIRECTORY;
                if (--slash_count <= 0)
                        spec->flags = spec->flags & ~GIT_ATTR_FNMATCH_FULLPATH;
        }
-       if ((spec->flags & GIT_ATTR_FNMATCH_NOLEADINGDIR) == 0 &&
-               spec->length >= 2 &&
-               pattern[spec->length - 1] == '*' &&
-               pattern[spec->length - 2] == '/') {
-               spec->length -= 2;
-               spec->flags = spec->flags | GIT_ATTR_FNMATCH_LEADINGDIR;
-               /* leave FULLPATH match on, however */
-       }
 
        if (context) {
                char *slash = strrchr(context, '/');
@@ -654,9 +762,8 @@ int git_attr_fnmatch__parse(
                *base = git__next_line(pattern);
                return -1;
        } else {
-               /* strip '\' that might have be used for internal whitespace */
-               spec->length = git__unescape(spec->pattern);
-               /* TODO: convert remaining '\' into '/' for POSIX ??? */
+               /* strip '\' that might have been used for internal whitespace */
+               spec->length = unescape_spaces(spec->pattern);
        }
 
        return 0;
@@ -734,7 +841,7 @@ int git_attr_assignment__parse(
                /* allocate assign if needed */
                if (!assign) {
                        assign = git__calloc(1, sizeof(git_attr_assignment));
-                       GITERR_CHECK_ALLOC(assign);
+                       GIT_ERROR_CHECK_ALLOC(assign);
                        GIT_REFCOUNT_INC(assign);
                }
 
@@ -768,7 +875,7 @@ int git_attr_assignment__parse(
 
                /* allocate permanent storage for name */
                assign->name = git_pool_strndup(pool, name_start, scan - name_start);
-               GITERR_CHECK_ALLOC(assign->name);
+               GIT_ERROR_CHECK_ALLOC(assign->name);
 
                /* if there is an equals sign, find the value */
                if (*scan == '=') {
@@ -777,7 +884,7 @@ int git_attr_assignment__parse(
                        /* if we found a value, allocate permanent storage for it */
                        if (scan > value_start) {
                                assign->value = git_pool_strndup(pool, value_start, scan - value_start);
-                               GITERR_CHECK_ALLOC(assign->value);
+                               GIT_ERROR_CHECK_ALLOC(assign->value);
                        }
                }
 
@@ -849,6 +956,7 @@ int git_attr_session__init(git_attr_session *session, git_repository *repo)
 {
        assert(repo);
 
+       memset(session, 0, sizeof(*session));
        session->key = git_atomic_inc(&repo->attr_session_key);
 
        return 0;
@@ -859,8 +967,8 @@ void git_attr_session__free(git_attr_session *session)
        if (!session)
                return;
 
-       git_buf_free(&session->sysdir);
-       git_buf_free(&session->tmp);
+       git_buf_dispose(&session->sysdir);
+       git_buf_dispose(&session->tmp);
 
        memset(session, 0, sizeof(git_attr_session));
 }