summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/path.c69
-rw-r--r--src/path.h23
-rw-r--r--tests-clar/core/path.c51
3 files changed, 140 insertions, 3 deletions
diff --git a/src/path.c b/src/path.c
index 9c88240e0..e9bc4871c 100644
--- a/src/path.c
+++ b/src/path.c
@@ -17,9 +17,7 @@
#include <stdio.h>
#include <ctype.h>
-#ifdef GIT_WIN32
#define LOOKS_LIKE_DRIVE_PREFIX(S) (git__isalpha((S)[0]) && (S)[1] == ':')
-#endif
/*
* Based on the Android implementation, BSD licensed.
@@ -172,11 +170,11 @@ int git_path_root(const char *path)
{
int offset = 0;
-#ifdef GIT_WIN32
/* Does the root of the path look like a windows drive ? */
if (LOOKS_LIKE_DRIVE_PREFIX(path))
offset += 2;
+#ifdef GIT_WIN32
/* Are we dealing with a windows network path? */
else if ((path[0] == '/' && path[1] == '/') ||
(path[0] == '\\' && path[1] == '\\'))
@@ -464,6 +462,71 @@ int git_path_find_dir(git_buf *dir, const char *path, const char *base)
return error;
}
+int git_path_resolve_relative(git_buf *path, size_t ceiling)
+{
+ char *base, *to, *from, *next;
+ size_t len;
+
+ if (!path || git_buf_oom(path))
+ return -1;
+
+ if (ceiling > path->size)
+ ceiling = path->size;
+
+ /* recognize drive prefixes, etc. that should not be backed over */
+ if (ceiling == 0)
+ ceiling = git_path_root(path->ptr) + 1;
+
+ /* recognize URL prefixes that should not be backed over */
+ if (ceiling == 0) {
+ for (next = path->ptr; *next && git__isalpha(*next); ++next);
+ if (next[0] == ':' && next[1] == '/' && next[2] == '/')
+ ceiling = (next + 3) - path->ptr;
+ }
+
+ base = to = from = path->ptr + ceiling;
+
+ while (*from) {
+ for (next = from; *next && *next != '/'; ++next);
+
+ len = next - from;
+
+ if (len == 1 && from[0] == '.')
+ /* do nothing with singleton dot */;
+
+ else if (len == 2 && from[0] == '.' && from[1] == '.') {
+ while (to > base && to[-1] == '/') to--;
+ while (to > base && to[-1] != '/') to--;
+ }
+
+ else {
+ if (*next == '/')
+ len++;
+
+ if (to != from)
+ memmove(to, from, len);
+
+ to += len;
+ }
+
+ from += len;
+
+ while (*from == '/') from++;
+ }
+
+ *to = '\0';
+
+ path->size = to - path->ptr;
+
+ return 0;
+}
+
+int git_path_apply_relative(git_buf *target, const char *relpath)
+{
+ git_buf_joinpath(target, git_buf_cstr(target), relpath);
+ return git_path_resolve_relative(target, 0);
+}
+
int git_path_cmp(
const char *name1, size_t len1, int isdir1,
const char *name2, size_t len2, int isdir2)
diff --git a/src/path.h b/src/path.h
index fd76805e5..d68393b3d 100644
--- a/src/path.h
+++ b/src/path.h
@@ -186,6 +186,29 @@ extern int git_path_prettify_dir(git_buf *path_out, const char *path, const char
extern int git_path_find_dir(git_buf *dir, const char *path, const char *base);
/**
+ * Resolve relative references within a path.
+ *
+ * This eliminates "./" and "../" relative references inside a path,
+ * as well as condensing multiple slashes into single ones. It will
+ * not touch the path before the "ceiling" length.
+ *
+ * Additionally, this will recognize an "c:/" drive prefix or a "xyz://" URL
+ * prefix and not touch that part of the path.
+ */
+extern int git_path_resolve_relative(git_buf *path, size_t ceiling);
+
+/**
+ * Apply a relative path to base path.
+ *
+ * Note that the base path could be a filename or a URL and this
+ * should still work. The relative path is walked segment by segment
+ * with three rules: series of slashes will be condensed to a single
+ * slash, "." will be eaten with no change, and ".." will remove a
+ * segment from the base path.
+ */
+extern int git_path_apply_relative(git_buf *target, const char *relpath);
+
+/**
* Walk each directory entry, except '.' and '..', calling fn(state).
*
* @param pathbuf buffer the function reads the initial directory
diff --git a/tests-clar/core/path.c b/tests-clar/core/path.c
index d826612ac..864393b70 100644
--- a/tests-clar/core/path.c
+++ b/tests-clar/core/path.c
@@ -418,3 +418,54 @@ void test_core_path__13_cannot_prettify_a_non_existing_file(void)
git_buf_free(&p);
}
+
+void test_core_path__14_apply_relative(void)
+{
+ git_buf p = GIT_BUF_INIT;
+
+ cl_git_pass(git_buf_sets(&p, "/this/is/a/base"));
+
+ cl_git_pass(git_path_apply_relative(&p, "../test"));
+ cl_assert_equal_s("/this/is/a/test", p.ptr);
+
+ cl_git_pass(git_path_apply_relative(&p, "../../the/./end"));
+ cl_assert_equal_s("/this/is/the/end", p.ptr);
+
+ cl_git_pass(git_path_apply_relative(&p, "./of/this/../the/string"));
+ cl_assert_equal_s("/this/is/the/end/of/the/string", p.ptr);
+
+ cl_git_pass(git_path_apply_relative(&p, "../../../../../.."));
+ cl_assert_equal_s("/this/", p.ptr);
+
+ cl_git_pass(git_path_apply_relative(&p, "../../../../../"));
+ cl_assert_equal_s("/", p.ptr);
+
+ cl_git_pass(git_path_apply_relative(&p, "../../../../.."));
+ cl_assert_equal_s("/", p.ptr);
+
+
+ cl_git_pass(git_buf_sets(&p, "d:/another/test"));
+
+ cl_git_pass(git_path_apply_relative(&p, "../../../../.."));
+ cl_assert_equal_s("d:/", p.ptr);
+
+ cl_git_pass(git_path_apply_relative(&p, "from/here/to/../and/./back/."));
+ cl_assert_equal_s("d:/from/here/and/back/", p.ptr);
+
+
+ cl_git_pass(git_buf_sets(&p, "https://my.url.com/test.git"));
+
+ cl_git_pass(git_path_apply_relative(&p, "../another.git"));
+ cl_assert_equal_s("https://my.url.com/another.git", p.ptr);
+
+ cl_git_pass(git_path_apply_relative(&p, "../full/path/url.patch"));
+ cl_assert_equal_s("https://my.url.com/full/path/url.patch", p.ptr);
+
+ cl_git_pass(git_path_apply_relative(&p, ".."));
+ cl_assert_equal_s("https://my.url.com/full/path/", p.ptr);
+
+ cl_git_pass(git_path_apply_relative(&p, "../../../../../"));
+ cl_assert_equal_s("https://", p.ptr);
+
+ git_buf_free(&p);
+}