Commit e995f74e8822456f97a2000da5a8d6803b88f643

Edward Thomson 2019-12-06T15:39:08

net: introduce git_net_url_joinpath Provide a mechanism to add a path and query string to an existing url so that we can easily append `/info/refs?...` type url segments to a url given to us by a user.

diff --git a/src/net.c b/src/net.c
index bb76643..c4a351a 100644
--- a/src/net.c
+++ b/src/net.c
@@ -153,6 +153,76 @@ done:
 	return error;
 }
 
+int git_net_url_joinpath(
+	git_net_url *out,
+	git_net_url *one,
+	const char *two)
+{
+	git_buf path = GIT_BUF_INIT;
+	const char *query;
+	size_t one_len, two_len;
+
+	git_net_url_dispose(out);
+
+	if ((query = strchr(two, '?')) != NULL) {
+		two_len = query - two;
+
+		if (*(++query) != '\0') {
+			out->query = git__strdup(query);
+			GIT_ERROR_CHECK_ALLOC(out->query);
+		}
+	} else {
+		two_len = strlen(two);
+	}
+
+	/* Strip all trailing `/`s from the first path */
+	one_len = one->path ? strlen(one->path) : 0;
+	while (one_len && one->path[one_len - 1] == '/')
+		one_len--;
+
+	/* Strip all leading `/`s from the second path */
+	while (*two == '/') {
+		two++;
+		two_len--;
+	}
+
+	git_buf_put(&path, one->path, one_len);
+	git_buf_putc(&path, '/');
+	git_buf_put(&path, two, two_len);
+
+	if (git_buf_oom(&path))
+		return -1;
+
+	out->path = git_buf_detach(&path);
+
+	if (one->scheme) {
+		out->scheme = git__strdup(one->scheme);
+		GIT_ERROR_CHECK_ALLOC(out->scheme);
+	}
+
+	if (one->host) {
+		out->host = git__strdup(one->host);
+		GIT_ERROR_CHECK_ALLOC(out->host);
+	}
+
+	if (one->port) {
+		out->port = git__strdup(one->port);
+		GIT_ERROR_CHECK_ALLOC(out->port);
+	}
+
+	if (one->username) {
+		out->username = git__strdup(one->username);
+		GIT_ERROR_CHECK_ALLOC(out->username);
+	}
+
+	if (one->password) {
+		out->password = git__strdup(one->password);
+		GIT_ERROR_CHECK_ALLOC(out->password);
+	}
+
+	return 0;
+}
+
 /*
  * Some servers strip the query parameters from the Location header
  * when sending a redirect. Others leave it in place.
diff --git a/src/net.h b/src/net.h
index 31fff97..089d9ae 100644
--- a/src/net.h
+++ b/src/net.h
@@ -24,6 +24,12 @@ typedef struct git_net_url {
 /** Parses a string containing a URL into a structure.  */
 extern int git_net_url_parse(git_net_url *url, const char *str);
 
+/** Appends a path and/or query string to the given URL */
+extern int git_net_url_joinpath(
+	git_net_url *out,
+	git_net_url *in,
+	const char *path);
+
 /** Ensures that a URL is minimally valid (contains a host, port and path) */
 extern bool git_net_url_valid(git_net_url *url);
 
diff --git a/tests/network/joinpath.c b/tests/network/joinpath.c
new file mode 100644
index 0000000..da8393b
--- /dev/null
+++ b/tests/network/joinpath.c
@@ -0,0 +1,194 @@
+#include "clar_libgit2.h"
+#include "net.h"
+#include "netops.h"
+
+static git_net_url source, target;
+
+void test_network_joinpath__initialize(void)
+{
+	memset(&source, 0, sizeof(source));
+	memset(&target, 0, sizeof(target));
+}
+
+void test_network_joinpath__cleanup(void)
+{
+	git_net_url_dispose(&source);
+	git_net_url_dispose(&target);
+}
+
+void test_network_joinpath__target_paths_and_queries(void)
+{
+	cl_git_pass(git_net_url_parse(&source, "http://example.com/a/b"));
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "/c/d"));
+	cl_assert_equal_s(target.path, "/a/b/c/d");
+	cl_assert_equal_p(target.query, NULL);
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "/c/d?foo"));
+	cl_assert_equal_s(target.path, "/a/b/c/d");
+	cl_assert_equal_s(target.query, "foo");
+	git_net_url_dispose(&target);
+}
+
+void test_network_joinpath__source_query_removed(void)
+{
+	cl_git_pass(git_net_url_parse(&source, "http://example.com/a/b?query&one&two"));
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "/c/d"));
+	cl_assert_equal_s(target.path, "/a/b/c/d");
+	cl_assert_equal_p(target.query, NULL);
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "/c/d?foo"));
+	cl_assert_equal_s(target.path, "/a/b/c/d");
+	cl_assert_equal_s(target.query, "foo");
+	git_net_url_dispose(&target);
+}
+
+void test_network_joinpath__source_lacks_path(void)
+{
+	cl_git_pass(git_net_url_parse(&source, "http://example.com"));
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "/"));
+	cl_assert_equal_s(target.path, "/");
+	cl_assert_equal_p(target.query, NULL);
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, ""));
+	cl_assert_equal_s(target.path, "/");
+	cl_assert_equal_p(target.query, NULL);
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "asdf"));
+	cl_assert_equal_s(target.path, "/asdf");
+	cl_assert_equal_p(target.query, NULL);
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "/asdf"));
+	cl_assert_equal_s(target.path, "/asdf");
+	cl_assert_equal_p(target.query, NULL);
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "/foo/bar"));
+	cl_assert_equal_s(target.path, "/foo/bar");
+	cl_assert_equal_p(target.query, NULL);
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "asdf?hello"));
+	cl_assert_equal_s(target.path, "/asdf");
+	cl_assert_equal_s(target.query, "hello");
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "/asdf?hello"));
+	cl_assert_equal_s(target.path, "/asdf");
+	cl_assert_equal_s(target.query, "hello");
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "/foo/bar?hello"));
+	cl_assert_equal_s(target.path, "/foo/bar");
+	cl_assert_equal_s(target.query, "hello");
+	git_net_url_dispose(&target);
+}
+
+void test_network_joinpath__source_is_slash(void)
+{
+	cl_git_pass(git_net_url_parse(&source, "http://example.com/"));
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "/"));
+	cl_assert_equal_s(target.path, "/");
+	cl_assert_equal_p(target.query, NULL);
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, ""));
+	cl_assert_equal_s(target.path, "/");
+	cl_assert_equal_p(target.query, NULL);
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "asdf"));
+	cl_assert_equal_s(target.path, "/asdf");
+	cl_assert_equal_p(target.query, NULL);
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "/asdf"));
+	cl_assert_equal_s(target.path, "/asdf");
+	cl_assert_equal_p(target.query, NULL);
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "/foo/bar"));
+	cl_assert_equal_s(target.path, "/foo/bar");
+	cl_assert_equal_p(target.query, NULL);
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "asdf?hello"));
+	cl_assert_equal_s(target.path, "/asdf");
+	cl_assert_equal_s(target.query, "hello");
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "/asdf?hello"));
+	cl_assert_equal_s(target.path, "/asdf");
+	cl_assert_equal_s(target.query, "hello");
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "/foo/bar?hello"));
+	cl_assert_equal_s(target.path, "/foo/bar");
+	cl_assert_equal_s(target.query, "hello");
+	git_net_url_dispose(&target);
+}
+
+
+void test_network_joinpath__source_has_query(void)
+{
+	cl_git_pass(git_net_url_parse(&source, "http://example.com?query"));
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "/"));
+	cl_assert_equal_s(target.path, "/");
+	cl_assert_equal_p(target.query, NULL);
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, ""));
+	cl_assert_equal_s(target.path, "/");
+	cl_assert_equal_p(target.query, NULL);
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "asdf"));
+	cl_assert_equal_s(target.path, "/asdf");
+	cl_assert_equal_p(target.query, NULL);
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "/asdf"));
+	cl_assert_equal_s(target.path, "/asdf");
+	cl_assert_equal_p(target.query, NULL);
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "/foo/bar"));
+	cl_assert_equal_s(target.path, "/foo/bar");
+	cl_assert_equal_p(target.query, NULL);
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "asdf?hello"));
+	cl_assert_equal_s(target.path, "/asdf");
+	cl_assert_equal_s(target.query, "hello");
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "/asdf?hello"));
+	cl_assert_equal_s(target.path, "/asdf");
+	cl_assert_equal_s(target.query, "hello");
+	git_net_url_dispose(&target);
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "/foo/bar?hello"));
+	cl_assert_equal_s(target.path, "/foo/bar");
+	cl_assert_equal_s(target.query, "hello");
+	git_net_url_dispose(&target);
+}
+
+
+void test_network_joinpath__empty_query_ignored(void)
+{
+	cl_git_pass(git_net_url_parse(&source, "http://example.com/foo"));
+
+	cl_git_pass(git_net_url_joinpath(&target, &source, "/bar/baz?"));
+	cl_assert_equal_s(target.path, "/foo/bar/baz");
+	cl_assert_equal_p(target.query, NULL);
+	git_net_url_dispose(&target);
+}