Tighten refspec processing

This changes the pattern matching code to not store the required final
/ before the *, and then to require each side to be a valid ref (or
empty). In particular, any refspec that looks like it should be a
pattern but doesn't quite meet the requirements will be found to be
invalid as a fallback non-pattern.

This was cherry picked from commit ef00d15 (Tighten refspec processing,
2008-03-17), and two fix-up commits 46220ca (remote.c: Fix overtight
refspec validation, 2008-03-20) and 7d19da4 (refspec: allow colon-less
wildcard "refs/category/*", 2008-03-25) squashed in.

Signed-off-by: Daniel Barkalow <barkalow@iabervon.org>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
diff --git a/builtin-fetch.c b/builtin-fetch.c
index 9a6ddce..6fd006f 100644
--- a/builtin-fetch.c
+++ b/builtin-fetch.c
@@ -630,5 +630,6 @@
 
 	signal(SIGINT, unlock_pack_on_signal);
 	atexit(unlock_pack);
-	return do_fetch(transport, parse_ref_spec(ref_nr, refs), ref_nr);
+	return do_fetch(transport,
+			parse_fetch_refspec(ref_nr, refs), ref_nr);
 }
diff --git a/builtin-send-pack.c b/builtin-send-pack.c
index 8afb1d0..98c54d9 100644
--- a/builtin-send-pack.c
+++ b/builtin-send-pack.c
@@ -536,7 +536,7 @@
 	int i;
 
 	for (i = 0; i < nr_heads; i++) {
-		const char *remote = strchr(heads[i], ':');
+		const char *remote = strrchr(heads[i], ':');
 
 		remote = remote ? (remote + 1) : heads[i];
 		switch (check_ref_format(remote)) {
diff --git a/remote.c b/remote.c
index 6b56473..95d65a0 100644
--- a/remote.c
+++ b/remote.c
@@ -305,42 +305,127 @@
 	git_config(handle_config);
 }
 
-struct refspec *parse_ref_spec(int nr_refspec, const char **refspec)
+static struct refspec *parse_refspec_internal(int nr_refspec, const char **refspec, int fetch)
 {
 	int i;
+	int st;
 	struct refspec *rs = xcalloc(sizeof(*rs), nr_refspec);
+
 	for (i = 0; i < nr_refspec; i++) {
-		const char *sp, *ep, *gp;
-		sp = refspec[i];
-		if (*sp == '+') {
+		size_t llen, rlen;
+		int is_glob;
+		const char *lhs, *rhs;
+
+		llen = rlen = is_glob = 0;
+
+		lhs = refspec[i];
+		if (*lhs == '+') {
 			rs[i].force = 1;
-			sp++;
+			lhs++;
 		}
-		gp = strchr(sp, '*');
-		ep = strchr(sp, ':');
-		if (gp && ep && gp > ep)
-			gp = NULL;
-		if (ep) {
-			if (ep[1]) {
-				const char *glob = strchr(ep + 1, '*');
-				if (!glob)
-					gp = NULL;
-				if (gp)
-					rs[i].dst = xstrndup(ep + 1,
-							     glob - ep - 1);
-				else
-					rs[i].dst = xstrdup(ep + 1);
+
+		rhs = strrchr(lhs, ':');
+		if (rhs) {
+			rhs++;
+			rlen = strlen(rhs);
+			is_glob = (2 <= rlen && !strcmp(rhs + rlen - 2, "/*"));
+			if (is_glob)
+				rlen -= 2;
+			rs[i].dst = xstrndup(rhs, rlen);
+		}
+
+		llen = (rhs ? (rhs - lhs - 1) : strlen(lhs));
+		if (2 <= llen && !memcmp(lhs + llen - 2, "/*", 2)) {
+			if ((rhs && !is_glob) || (!rhs && fetch))
+				goto invalid;
+			is_glob = 1;
+			llen -= 2;
+		} else if (rhs && is_glob) {
+			goto invalid;
+		}
+
+		rs[i].pattern = is_glob;
+		rs[i].src = xstrndup(lhs, llen);
+
+		if (fetch) {
+			/*
+			 * LHS
+			 * - empty is allowed; it means HEAD.
+			 * - otherwise it must be a valid looking ref.
+			 */
+			if (!*rs[i].src)
+				; /* empty is ok */
+			else {
+				st = check_ref_format(rs[i].src);
+				if (st && st != CHECK_REF_FORMAT_ONELEVEL)
+					goto invalid;
+			}
+			/*
+			 * RHS
+			 * - missing is ok, and is same as empty.
+			 * - empty is ok; it means not to store.
+			 * - otherwise it must be a valid looking ref.
+			 */
+			if (!rs[i].dst) {
+				; /* ok */
+			} else if (!*rs[i].dst) {
+				; /* ok */
+			} else {
+				st = check_ref_format(rs[i].dst);
+				if (st && st != CHECK_REF_FORMAT_ONELEVEL)
+					goto invalid;
 			}
 		} else {
-			ep = sp + strlen(sp);
+			/*
+			 * LHS
+			 * - empty is allowed; it means delete.
+			 * - when wildcarded, it must be a valid looking ref.
+			 * - otherwise, it must be an extended SHA-1, but
+			 *   there is no existing way to validate this.
+			 */
+			if (!*rs[i].src)
+				; /* empty is ok */
+			else if (is_glob) {
+				st = check_ref_format(rs[i].src);
+				if (st && st != CHECK_REF_FORMAT_ONELEVEL)
+					goto invalid;
+			}
+			else
+				; /* anything goes, for now */
+			/*
+			 * RHS
+			 * - missing is allowed, but LHS then must be a
+			 *   valid looking ref.
+			 * - empty is not allowed.
+			 * - otherwise it must be a valid looking ref.
+			 */
+			if (!rs[i].dst) {
+				st = check_ref_format(rs[i].src);
+				if (st && st != CHECK_REF_FORMAT_ONELEVEL)
+					goto invalid;
+			} else if (!*rs[i].dst) {
+				goto invalid;
+			} else {
+				st = check_ref_format(rs[i].dst);
+				if (st && st != CHECK_REF_FORMAT_ONELEVEL)
+					goto invalid;
+			}
 		}
-		if (gp) {
-			rs[i].pattern = 1;
-			ep = gp;
-		}
-		rs[i].src = xstrndup(sp, ep - sp);
 	}
 	return rs;
+
+ invalid:
+	die("Invalid refspec '%s'", refspec[i]);
+}
+
+struct refspec *parse_fetch_refspec(int nr_refspec, const char **refspec)
+{
+	return parse_refspec_internal(nr_refspec, refspec, 1);
+}
+
+struct refspec *parse_push_refspec(int nr_refspec, const char **refspec)
+{
+	return parse_refspec_internal(nr_refspec, refspec, 0);
 }
 
 static int valid_remote_nick(const char *name)
@@ -371,8 +456,8 @@
 		add_url(ret, name);
 	if (!ret->url)
 		return NULL;
-	ret->fetch = parse_ref_spec(ret->fetch_refspec_nr, ret->fetch_refspec);
-	ret->push = parse_ref_spec(ret->push_refspec_nr, ret->push_refspec);
+	ret->fetch = parse_fetch_refspec(ret->fetch_refspec_nr, ret->fetch_refspec);
+	ret->push = parse_push_refspec(ret->push_refspec_nr, ret->push_refspec);
 	return ret;
 }
 
@@ -385,11 +470,11 @@
 		if (!r)
 			continue;
 		if (!r->fetch)
-			r->fetch = parse_ref_spec(r->fetch_refspec_nr,
-					r->fetch_refspec);
+			r->fetch = parse_fetch_refspec(r->fetch_refspec_nr,
+						       r->fetch_refspec);
 		if (!r->push)
-			r->push = parse_ref_spec(r->push_refspec_nr,
-					r->push_refspec);
+			r->push = parse_push_refspec(r->push_refspec_nr,
+						     r->push_refspec);
 		result = fn(r, priv);
 	}
 	return result;
@@ -455,7 +540,8 @@
 		if (!fetch->dst)
 			continue;
 		if (fetch->pattern) {
-			if (!prefixcmp(needle, key)) {
+			if (!prefixcmp(needle, key) &&
+			    needle[strlen(key)] == '/') {
 				*result = xmalloc(strlen(value) +
 						  strlen(needle) -
 						  strlen(key) + 1);
@@ -695,7 +781,9 @@
 {
 	int i;
 	for (i = 0; i < rs_nr; i++) {
-		if (rs[i].pattern && !prefixcmp(src->name, rs[i].src))
+		if (rs[i].pattern &&
+		    !prefixcmp(src->name, rs[i].src) &&
+		    src->name[strlen(rs[i].src)] == '/')
 			return rs + i;
 	}
 	return NULL;
@@ -710,7 +798,7 @@
 	       int nr_refspec, const char **refspec, int flags)
 {
 	struct refspec *rs =
-		parse_ref_spec(nr_refspec, (const char **) refspec);
+		parse_push_refspec(nr_refspec, (const char **) refspec);
 	int send_all = flags & MATCH_REFS_ALL;
 	int send_mirror = flags & MATCH_REFS_MIRROR;
 
@@ -894,7 +982,7 @@
 		  struct ref ***tail,
 		  int missing_ok)
 {
-	struct ref *ref_map, *rm;
+	struct ref *ref_map, **rmp;
 
 	if (refspec->pattern) {
 		ref_map = get_expanded_map(remote_refs, refspec);
@@ -911,10 +999,20 @@
 		}
 	}
 
-	for (rm = ref_map; rm; rm = rm->next) {
-		if (rm->peer_ref && check_ref_format(rm->peer_ref->name + 5))
-			die("* refusing to create funny ref '%s' locally",
-			    rm->peer_ref->name);
+	for (rmp = &ref_map; *rmp; ) {
+		if ((*rmp)->peer_ref) {
+			int st = check_ref_format((*rmp)->peer_ref->name + 5);
+			if (st && st != CHECK_REF_FORMAT_ONELEVEL) {
+				struct ref *ignore = *rmp;
+				error("* Ignoring funny ref '%s' locally",
+				      (*rmp)->peer_ref->name);
+				*rmp = (*rmp)->next;
+				free(ignore->peer_ref);
+				free(ignore);
+				continue;
+			}
+		}
+		rmp = &((*rmp)->next);
 	}
 
 	if (ref_map)
diff --git a/remote.h b/remote.h
index 86e036d..6b8c32c 100644
--- a/remote.h
+++ b/remote.h
@@ -63,7 +63,8 @@
  */
 void ref_remove_duplicates(struct ref *ref_map);
 
-struct refspec *parse_ref_spec(int nr_refspec, const char **refspec);
+struct refspec *parse_fetch_refspec(int nr_refspec, const char **refspec);
+struct refspec *parse_push_refspec(int nr_refspec, const char **refspec);
 
 int match_refs(struct ref *src, struct ref *dst, struct ref ***dst_tail,
 	       int nr_refspec, const char **refspec, int all);
diff --git a/t/t5511-refspec.sh b/t/t5511-refspec.sh
new file mode 100755
index 0000000..670a8f1
--- /dev/null
+++ b/t/t5511-refspec.sh
@@ -0,0 +1,72 @@
+#!/bin/sh
+
+test_description='refspec parsing'
+
+. ./test-lib.sh
+
+test_refspec () {
+
+	kind=$1 refspec=$2 expect=$3
+	git config remote.frotz.url "." &&
+	git config --remove-section remote.frotz &&
+	git config remote.frotz.url "." &&
+	git config "remote.frotz.$kind" "$refspec" &&
+	if test "$expect" != invalid
+	then
+		title="$kind $refspec"
+		test='git ls-remote frotz'
+	else
+		title="$kind $refspec (invalid)"
+		test='test_must_fail git ls-remote frotz'
+	fi
+	test_expect_success "$title" "$test"
+}
+
+test_refspec push ''						invalid
+test_refspec push ':'						invalid
+
+test_refspec fetch ''
+test_refspec fetch ':'
+
+test_refspec push 'refs/heads/*:refs/remotes/frotz/*'
+test_refspec push 'refs/heads/*:refs/remotes/frotz'		invalid
+test_refspec push 'refs/heads:refs/remotes/frotz/*'		invalid
+test_refspec push 'refs/heads/master:refs/remotes/frotz/xyzzy'
+
+
+# These have invalid LHS, but we do not have a formal "valid sha-1
+# expression syntax checker" so they are not checked with the current
+# code.  They will be caught downstream anyway, but we may want to
+# have tighter check later...
+
+: test_refspec push 'refs/heads/master::refs/remotes/frotz/xyzzy'	invalid
+: test_refspec push 'refs/heads/maste :refs/remotes/frotz/xyzzy'	invalid
+
+test_refspec fetch 'refs/heads/*:refs/remotes/frotz/*'
+test_refspec fetch 'refs/heads/*:refs/remotes/frotz'		invalid
+test_refspec fetch 'refs/heads:refs/remotes/frotz/*'		invalid
+test_refspec fetch 'refs/heads/master:refs/remotes/frotz/xyzzy'
+test_refspec fetch 'refs/heads/master::refs/remotes/frotz/xyzzy'	invalid
+test_refspec fetch 'refs/heads/maste :refs/remotes/frotz/xyzzy'	invalid
+
+test_refspec push 'master~1:refs/remotes/frotz/backup'
+test_refspec fetch 'master~1:refs/remotes/frotz/backup'		invalid
+test_refspec push 'HEAD~4:refs/remotes/frotz/new'
+test_refspec fetch 'HEAD~4:refs/remotes/frotz/new'		invalid
+
+test_refspec push 'HEAD'
+test_refspec fetch 'HEAD'
+test_refspec push 'refs/heads/ nitfol'				invalid
+test_refspec fetch 'refs/heads/ nitfol'				invalid
+
+test_refspec push 'HEAD:'					invalid
+test_refspec fetch 'HEAD:'
+test_refspec push 'refs/heads/ nitfol:'				invalid
+test_refspec fetch 'refs/heads/ nitfol:'			invalid
+
+test_refspec push ':refs/remotes/frotz/deleteme'
+test_refspec fetch ':refs/remotes/frotz/HEAD-to-me'
+test_refspec push ':refs/remotes/frotz/delete me'		invalid
+test_refspec fetch ':refs/remotes/frotz/HEAD to me'		invalid
+
+test_done