Commit aa2120e9da45017c6fe0126d6e9b1ee20ff40037

nulltoken 2011-02-10T15:08:00

Added git_reference__normalize_name() along with tests.

diff --git a/src/common.h b/src/common.h
index 1ca0047..1aede73 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 4f66742..7bf4d18 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 b95ec70..46589e0 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 70196aa..2e9f340 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 2d055c9..45ee09a 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;
 }