From aa2120e9da45017c6fe0126d6e9b1ee20ff40037 Mon Sep 17 00:00:00 2001 From: nulltoken Date: Thu, 10 Feb 2011 15:08:00 +0100 Subject: [PATCH] Added git_reference__normalize_name() along with tests. --- src/common.h | 1 + src/git2/types.h | 1 + src/refs.c | 104 +++++++++++++++++++++++++++++++++++++++++++++-- src/refs.h | 2 + tests/t10-refs.c | 61 +++++++++++++++++++++++++++ 5 files changed, 165 insertions(+), 4 deletions(-) diff --git a/src/common.h b/src/common.h index 1ca00471b..1aede7367 100644 --- a/src/common.h +++ b/src/common.h @@ -53,5 +53,6 @@ typedef SSIZE_T ssize_t; #include "bswap.h" #define GIT_PATH_MAX 4096 +#define GIT_FILELOCK_EXTENSION ".lock\0" #endif /* INCLUDE_common_h__ */ diff --git a/src/git2/types.h b/src/git2/types.h index 4f66742f0..7bf4d189e 100644 --- a/src/git2/types.h +++ b/src/git2/types.h @@ -140,6 +140,7 @@ typedef struct git_reference git_reference; /** Basic type of any Git reference. */ typedef enum { + GIT_REF_ANY = -2, /** Reference can be an object id reference or a symbolic reference */ GIT_REF_INVALID = -1, /** Invalid reference */ GIT_REF_OID = 1, /** A reference which points at an object id */ GIT_REF_SYMBOLIC = 2, /** A reference which points at another reference */ diff --git a/src/refs.c b/src/refs.c index b95ec70cf..46589e04d 100644 --- a/src/refs.c +++ b/src/refs.c @@ -571,12 +571,13 @@ error_cleanup: int git_repository_lookup_ref(git_reference **ref_out, git_repository *repo, const char *name) { int error; + char normalized_name[GIT_PATH_MAX]; assert(ref_out && repo && name); *ref_out = NULL; - error = check_refname(name); + error = git_reference__normalize_name(normalized_name, name, GIT_REF_ANY); if (error < GIT_SUCCESS) return error; @@ -584,7 +585,7 @@ int git_repository_lookup_ref(git_reference **ref_out, git_repository *repo, con * First, check if the reference is on the local cache; * references on the cache are assured to be up-to-date */ - *ref_out = git_hashtable_lookup(repo->references.cache, name); + *ref_out = git_hashtable_lookup(repo->references.cache, normalized_name); if (*ref_out != NULL) return GIT_SUCCESS; @@ -593,7 +594,7 @@ int git_repository_lookup_ref(git_reference **ref_out, git_repository *repo, con * If the file exists, we parse it and store it on the * cache. */ - error = lookup_loose_ref(ref_out, repo, name); + error = lookup_loose_ref(ref_out, repo, normalized_name); if (error == GIT_SUCCESS) return GIT_SUCCESS; @@ -618,7 +619,7 @@ int git_repository_lookup_ref(git_reference **ref_out, git_repository *repo, con return error; /* check the cache again -- hopefully the reference will be there */ - *ref_out = git_hashtable_lookup(repo->references.cache, name); + *ref_out = git_hashtable_lookup(repo->references.cache, normalized_name); if (*ref_out != NULL) return GIT_SUCCESS; } @@ -653,4 +654,99 @@ void git_repository__refcache_free(git_refcache *refs) git_hashtable_free(refs->cache); } +static int check_valid_ref_char(char ch) +{ + if (ch <= ' ') + return GIT_ERROR; + + switch (ch) { + case '~': + case '^': + case ':': + case '\\': + case '?': + case '[': + return GIT_ERROR; + break; + + default: + return GIT_SUCCESS; + } +} + +int git_reference__normalize_name(char *buffer_out, const char *name, git_rtype type) +{ + int error = GIT_SUCCESS; + const char *name_end, *buffer_out_start; + char *current; + int contains_a_slash = 0; + + assert(name && buffer_out); + + buffer_out_start = buffer_out; + current = (char *)name; + name_end = name + strlen(name); + + if (type == GIT_REF_INVALID) + return GIT_EINVALIDTYPE; + + /* A refname can not be empty */ + if (name_end == name) + return GIT_EINVALIDREFNAME; + + /* A refname can not end with a dot or a slash */ + if (*(name_end - 1) == '.' || *(name_end - 1) == '/') + return GIT_EINVALIDREFNAME; + + while (current < name_end) { + if (check_valid_ref_char(*current)) + return GIT_EINVALIDREFNAME; + + if (buffer_out > buffer_out_start) { + char prev = *(buffer_out - 1); + + /* A refname can not start with a dot nor contain a double dot */ + if (*current == '.' && ((prev == '.') || (prev == '/'))) + return GIT_EINVALIDREFNAME; + + /* '@{' is forbidden within a refname */ + if (*current == '{' && prev == '@') + return GIT_EINVALIDREFNAME; + + /* Prevent multiple slashes from being added to the output */ + if (*current == '/' && prev == '/') { + current++; + continue; + } + } + + if (*current == '/') { + /* Slashes are not authorized in symbolic reference name */ + if (type == GIT_REF_SYMBOLIC) { + return GIT_EINVALIDREFNAME; + } + + contains_a_slash = 1; + } + + *buffer_out++ = *current++; + } + + /* Object id refname have to contain at least one slash */ + if (type == GIT_REF_OID && !contains_a_slash) + return GIT_EINVALIDREFNAME; + + /* A refname can not end with ".lock" */ + if (!git__suffixcmp(name, GIT_FILELOCK_EXTENSION)) + return GIT_EINVALIDREFNAME; + + *buffer_out = '\0'; + + /* For object id references, name has to start with refs/(heads|tags|remotes) */ + if (type == GIT_REF_OID && !(!git__prefixcmp(buffer_out_start, GIT_REFS_HEADS_DIR) || + !git__prefixcmp(buffer_out_start, GIT_REFS_TAGS_DIR) || !git__prefixcmp(buffer_out_start, GIT_REFS_REMOTES_DIR))) + return GIT_EINVALIDREFNAME; + + return error; +} diff --git a/src/refs.h b/src/refs.h index 70196aa95..2e9f340b8 100644 --- a/src/refs.h +++ b/src/refs.h @@ -9,6 +9,7 @@ #define GIT_REFS_DIR "refs/" #define GIT_REFS_HEADS_DIR GIT_REFS_DIR "heads/" #define GIT_REFS_TAGS_DIR GIT_REFS_DIR "tags/" +#define GIT_REFS_REMOTES_DIR GIT_REFS_DIR "remotes/" #define GIT_SYMREF "ref: " #define GIT_PACKEDREFS_FILE "packed-refs" @@ -37,5 +38,6 @@ typedef struct { void git_repository__refcache_free(git_refcache *refs); int git_repository__refcache_init(git_refcache *refs); +int git_reference__normalize_name(char *buffer_out, const char *name, git_rtype type); #endif diff --git a/tests/t10-refs.c b/tests/t10-refs.c index 2d055c978..45ee09a79 100644 --- a/tests/t10-refs.c +++ b/tests/t10-refs.c @@ -279,6 +279,63 @@ BEGIN_TEST("createref", create_new_object_id_ref) must_pass(gitfo_unlink(ref_path)); /* TODO: replace with git_reference_delete() when available */ END_TEST +static int ensure_refname_normalized(git_rtype ref_type, const char *input_refname, const char *expected_refname) +{ + int error = GIT_SUCCESS; + char buffer_out[GIT_PATH_MAX]; + + error = git_reference__normalize_name(buffer_out, input_refname, ref_type); + if (error < GIT_SUCCESS) + return error; + + if (expected_refname == NULL) + return error; + + if (strcmp(buffer_out, expected_refname)) + error = GIT_ERROR; + + return error; +} + +BEGIN_TEST("normalizeref", normalize_unknown_ref_type) + must_fail(ensure_refname_normalized(GIT_REF_INVALID, "a", NULL)); +END_TEST + +BEGIN_TEST("normalizeref", normalize_object_id_ref) + must_fail(ensure_refname_normalized(GIT_REF_OID, "a", NULL)); + must_fail(ensure_refname_normalized(GIT_REF_OID, "", NULL)); + must_fail(ensure_refname_normalized(GIT_REF_OID, "refs/heads/a/", NULL)); + must_fail(ensure_refname_normalized(GIT_REF_OID, "refs/heads/a.", NULL)); + must_fail(ensure_refname_normalized(GIT_REF_OID, "refs/heads/a.lock", NULL)); + must_fail(ensure_refname_normalized(GIT_REF_OID, "refs/dummy/a", NULL)); + must_pass(ensure_refname_normalized(GIT_REF_OID, "refs/tags/a", "refs/tags/a")); + must_pass(ensure_refname_normalized(GIT_REF_OID, "refs/heads/a/b", "refs/heads/a/b")); + must_pass(ensure_refname_normalized(GIT_REF_OID, "refs/heads/a./b", "refs/heads/a./b")); + must_fail(ensure_refname_normalized(GIT_REF_OID, "refs/heads/foo?bar", NULL)); + must_fail(ensure_refname_normalized(GIT_REF_OID, "refs/heads\foo", NULL)); + must_pass(ensure_refname_normalized(GIT_REF_OID, "refs/heads/v@ation", "refs/heads/v@ation")); + must_pass(ensure_refname_normalized(GIT_REF_OID, "refs///heads///a", "refs/heads/a")); + must_fail(ensure_refname_normalized(GIT_REF_OID, "refs/heads/.a/b", NULL)); + must_fail(ensure_refname_normalized(GIT_REF_OID, "refs/heads/foo/../bar", NULL)); + must_fail(ensure_refname_normalized(GIT_REF_OID, "refs/heads/foo..bar", NULL)); + must_fail(ensure_refname_normalized(GIT_REF_OID, "refs/heads/./foo", NULL)); + must_fail(ensure_refname_normalized(GIT_REF_OID, "refs/heads/v@{ation", NULL)); +END_TEST + +BEGIN_TEST("normalizeref", normalize_symbolic_ref) + must_pass(ensure_refname_normalized(GIT_REF_SYMBOLIC, "a", "a")); + must_fail(ensure_refname_normalized(GIT_REF_SYMBOLIC, "", NULL)); + must_fail(ensure_refname_normalized(GIT_REF_SYMBOLIC, "a/b", NULL)); + must_fail(ensure_refname_normalized(GIT_REF_SYMBOLIC, "heads\foo", NULL)); +END_TEST + + +BEGIN_TEST("normalizeref", normalize_any_ref) /* Slash related rules do not apply, neither do 'refs' prefix related rules */ + must_pass(ensure_refname_normalized(GIT_REF_ANY, "a", "a")); + must_pass(ensure_refname_normalized(GIT_REF_ANY, "a/b", "a/b")); + must_pass(ensure_refname_normalized(GIT_REF_ANY, "refs///heads///a", "refs/heads/a")); +END_TEST + git_testsuite *libgit2_suite_refs(void) { git_testsuite *suite = git_testsuite_new("References"); @@ -293,6 +350,10 @@ git_testsuite *libgit2_suite_refs(void) ADD_TEST(suite, "readpackedref", packed_exists_but_more_recent_loose_reference_is_retrieved); ADD_TEST(suite, "createref", create_new_symbolic_ref); ADD_TEST(suite, "createref", create_new_object_id_ref); + ADD_TEST(suite, "normalizeref", normalize_unknown_ref_type); + ADD_TEST(suite, "normalizeref", normalize_object_id_ref); + ADD_TEST(suite, "normalizeref", normalize_symbolic_ref); + ADD_TEST(suite, "normalizeref", normalize_any_ref); return suite; }