From b0fe11292219f5d63f2159e0b0eb24ff21d66b10 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Tue, 10 Jul 2012 15:13:30 -0700 Subject: [PATCH] Add path utilities to resolve relative paths This makes it easy to take a buffer containing a path with relative references (i.e. .. or . path segments) and resolve all of those into a clean path. This can be applied to URLs as well as file paths which can be useful. As part of this, I made the drive-letter detection apply on all platforms, not just windows. If you give a path that looks like "c:/..." on any platform, it seems like we might as well detect that as a rooted path. I suppose if you create a directory named "x:" on another platform and want to use that as the beginning of a relative path under the root directory of your repo, this could cause a problem, but then it seems like you're asking for trouble. --- src/path.c | 69 ++++++++++++++++++++++++++++++++++++++++-- src/path.h | 23 ++++++++++++++ tests-clar/core/path.c | 51 +++++++++++++++++++++++++++++++ 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 #include -#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 @@ -185,6 +185,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). * 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); +}