Unverified Commit a5ddae68 by Edward Thomson Committed by GitHub

Merge pull request #5097 from pks-t/pks/ignore-escapes

gitignore with escapes
parents e277ff4d 3b517351
...@@ -565,15 +565,55 @@ void git_attr_path__free(git_attr_path *info) ...@@ -565,15 +565,55 @@ void git_attr_path__free(git_attr_path *info)
*/ */
static size_t trailing_space_length(const char *p, size_t len) static size_t trailing_space_length(const char *p, size_t len)
{ {
size_t n; size_t n, i;
for (n = len; n; n--) { for (n = len; n; n--) {
if ((p[n-1] != ' ' && p[n-1] != '\t') || if (p[n-1] != ' ' && p[n-1] != '\t')
(n > 1 && p[n-2] == '\\')) break;
/*
* Count escape-characters before space. In case where it's an
* even number of escape characters, then the escape char itself
* is escaped and the whitespace is an unescaped whitespace.
* Otherwise, the last escape char is not escaped and the
* whitespace in an escaped whitespace.
*/
i = n;
while (i > 1 && p[i-2] == '\\')
i--;
if ((n - i) % 2)
break; break;
} }
return len - n; return len - n;
} }
static size_t unescape_spaces(char *str)
{
char *scan, *pos = str;
bool escaped = false;
if (!str)
return 0;
for (scan = str; *scan; scan++) {
if (!escaped && *scan == '\\') {
escaped = true;
continue;
}
/* Only insert the escape character for escaped non-spaces */
if (escaped && !git__isspace(*scan))
*pos++ = '\\';
*pos++ = *scan;
escaped = false;
}
if (pos != scan)
*pos = '\0';
return (pos - str);
}
/* /*
* This will return 0 if the spec was filled out, * This will return 0 if the spec was filled out,
* GIT_ENOTFOUND if the fnmatch does not require matching, or * GIT_ENOTFOUND if the fnmatch does not require matching, or
...@@ -587,6 +627,7 @@ int git_attr_fnmatch__parse( ...@@ -587,6 +627,7 @@ int git_attr_fnmatch__parse(
{ {
const char *pattern, *scan; const char *pattern, *scan;
int slash_count, allow_space; int slash_count, allow_space;
bool escaped;
assert(spec && base && *base); assert(spec && base && *base);
...@@ -623,28 +664,29 @@ int git_attr_fnmatch__parse( ...@@ -623,28 +664,29 @@ int git_attr_fnmatch__parse(
} }
slash_count = 0; slash_count = 0;
escaped = false;
/* Scan until a non-escaped whitespace. */
for (scan = pattern; *scan != '\0'; ++scan) { for (scan = pattern; *scan != '\0'; ++scan) {
/* char c = *scan;
* Scan until a non-escaped whitespace: find a whitespace, then look
* one char backward to ensure that it's not prefixed by a `\`.
* Only look backward if we're not at the first position (`pattern`).
*/
if (git__isspace(*scan) && scan > pattern && *(scan - 1) != '\\') {
if (!allow_space || (*scan != ' ' && *scan != '\t' && *scan != '\r'))
break;
}
if (*scan == '/') { if (c == '\\' && !escaped) {
escaped = true;
continue;
} else if (git__isspace(c) && !escaped) {
if (!allow_space || (c != ' ' && c != '\t' && c != '\r'))
break;
} else if (c == '/') {
spec->flags = spec->flags | GIT_ATTR_FNMATCH_FULLPATH; spec->flags = spec->flags | GIT_ATTR_FNMATCH_FULLPATH;
slash_count++; slash_count++;
if (slash_count == 1 && pattern == scan) if (slash_count == 1 && pattern == scan)
pattern++; pattern++;
} } else if (git__iswildcard(c) && !escaped) {
/* remember if we see an unescaped wildcard in pattern */ /* remember if we see an unescaped wildcard in pattern */
else if (git__iswildcard(*scan) &&
(scan == pattern || (*(scan - 1) != '\\')))
spec->flags = spec->flags | GIT_ATTR_FNMATCH_HASWILD; spec->flags = spec->flags | GIT_ATTR_FNMATCH_HASWILD;
}
escaped = false;
} }
*base = scan; *base = scan;
...@@ -699,9 +741,8 @@ int git_attr_fnmatch__parse( ...@@ -699,9 +741,8 @@ int git_attr_fnmatch__parse(
*base = git__next_line(pattern); *base = git__next_line(pattern);
return -1; return -1;
} else { } else {
/* strip '\' that might have be used for internal whitespace */ /* strip '\' that might have been used for internal whitespace */
spec->length = git__unescape(spec->pattern); spec->length = unescape_spaces(spec->pattern);
/* TODO: convert remaining '\' into '/' for POSIX ??? */
} }
return 0; return 0;
......
...@@ -276,9 +276,12 @@ int git_path_root(const char *path) ...@@ -276,9 +276,12 @@ int git_path_root(const char *path)
while (path[offset] && path[offset] != '/' && path[offset] != '\\') while (path[offset] && path[offset] != '/' && path[offset] != '\\')
offset++; offset++;
} }
if (path[offset] == '\\')
return offset;
#endif #endif
if (path[offset] == '/' || path[offset] == '\\') if (path[offset] == '/')
return offset; return offset;
return -1; /* Not a real error - signals that path is not rooted */ return -1; /* Not a real error - signals that path is not rooted */
......
...@@ -5,12 +5,12 @@ ...@@ -5,12 +5,12 @@
static git_repository *g_repo = NULL; static git_repository *g_repo = NULL;
void test_attr_ignore__initialize(void) void test_ignore_path__initialize(void)
{ {
g_repo = cl_git_sandbox_init("attr"); g_repo = cl_git_sandbox_init("attr");
} }
void test_attr_ignore__cleanup(void) void test_ignore_path__cleanup(void)
{ {
cl_git_sandbox_cleanup(); cl_git_sandbox_cleanup();
g_repo = NULL; g_repo = NULL;
...@@ -31,7 +31,7 @@ static void assert_is_ignored_( ...@@ -31,7 +31,7 @@ static void assert_is_ignored_(
#define assert_is_ignored(expected, filepath) \ #define assert_is_ignored(expected, filepath) \
assert_is_ignored_(expected, filepath, __FILE__, __LINE__) assert_is_ignored_(expected, filepath, __FILE__, __LINE__)
void test_attr_ignore__honor_temporary_rules(void) void test_ignore_path__honor_temporary_rules(void)
{ {
cl_git_rewritefile("attr/.gitignore", "/NewFolder\n/NewFolder/NewFolder"); cl_git_rewritefile("attr/.gitignore", "/NewFolder\n/NewFolder/NewFolder");
...@@ -41,7 +41,7 @@ void test_attr_ignore__honor_temporary_rules(void) ...@@ -41,7 +41,7 @@ void test_attr_ignore__honor_temporary_rules(void)
assert_is_ignored(true, "NewFolder/NewFolder/File.txt"); assert_is_ignored(true, "NewFolder/NewFolder/File.txt");
} }
void test_attr_ignore__allow_root(void) void test_ignore_path__allow_root(void)
{ {
cl_git_rewritefile("attr/.gitignore", "/"); cl_git_rewritefile("attr/.gitignore", "/");
...@@ -51,7 +51,7 @@ void test_attr_ignore__allow_root(void) ...@@ -51,7 +51,7 @@ void test_attr_ignore__allow_root(void)
assert_is_ignored(false, "NewFolder/NewFolder/File.txt"); assert_is_ignored(false, "NewFolder/NewFolder/File.txt");
} }
void test_attr_ignore__ignore_space(void) void test_ignore_path__ignore_space(void)
{ {
cl_git_rewritefile("attr/.gitignore", "/\n\n/NewFolder \n/NewFolder/NewFolder"); cl_git_rewritefile("attr/.gitignore", "/\n\n/NewFolder \n/NewFolder/NewFolder");
...@@ -61,7 +61,7 @@ void test_attr_ignore__ignore_space(void) ...@@ -61,7 +61,7 @@ void test_attr_ignore__ignore_space(void)
assert_is_ignored(true, "NewFolder/NewFolder/File.txt"); assert_is_ignored(true, "NewFolder/NewFolder/File.txt");
} }
void test_attr_ignore__intermittent_space(void) void test_ignore_path__intermittent_space(void)
{ {
cl_git_rewritefile("attr/.gitignore", "foo bar\n"); cl_git_rewritefile("attr/.gitignore", "foo bar\n");
...@@ -70,7 +70,7 @@ void test_attr_ignore__intermittent_space(void) ...@@ -70,7 +70,7 @@ void test_attr_ignore__intermittent_space(void)
assert_is_ignored(true, "foo bar"); assert_is_ignored(true, "foo bar");
} }
void test_attr_ignore__trailing_space(void) void test_ignore_path__trailing_space(void)
{ {
cl_git_rewritefile( cl_git_rewritefile(
"attr/.gitignore", "attr/.gitignore",
...@@ -85,7 +85,7 @@ void test_attr_ignore__trailing_space(void) ...@@ -85,7 +85,7 @@ void test_attr_ignore__trailing_space(void)
assert_is_ignored(false, "bar "); assert_is_ignored(false, "bar ");
} }
void test_attr_ignore__escaped_trailing_spaces(void) void test_ignore_path__escaped_trailing_spaces(void)
{ {
cl_git_rewritefile( cl_git_rewritefile(
"attr/.gitignore", "attr/.gitignore",
...@@ -107,7 +107,7 @@ void test_attr_ignore__escaped_trailing_spaces(void) ...@@ -107,7 +107,7 @@ void test_attr_ignore__escaped_trailing_spaces(void)
assert_is_ignored(false, "qux "); assert_is_ignored(false, "qux ");
} }
void test_attr_ignore__ignore_dir(void) void test_ignore_path__ignore_dir(void)
{ {
cl_git_rewritefile("attr/.gitignore", "dir/\n"); cl_git_rewritefile("attr/.gitignore", "dir/\n");
...@@ -115,7 +115,7 @@ void test_attr_ignore__ignore_dir(void) ...@@ -115,7 +115,7 @@ void test_attr_ignore__ignore_dir(void)
assert_is_ignored(true, "dir/file"); assert_is_ignored(true, "dir/file");
} }
void test_attr_ignore__ignore_dir_with_trailing_space(void) void test_ignore_path__ignore_dir_with_trailing_space(void)
{ {
cl_git_rewritefile("attr/.gitignore", "dir/ \n"); cl_git_rewritefile("attr/.gitignore", "dir/ \n");
...@@ -123,7 +123,7 @@ void test_attr_ignore__ignore_dir_with_trailing_space(void) ...@@ -123,7 +123,7 @@ void test_attr_ignore__ignore_dir_with_trailing_space(void)
assert_is_ignored(true, "dir/file"); assert_is_ignored(true, "dir/file");
} }
void test_attr_ignore__ignore_root(void) void test_ignore_path__ignore_root(void)
{ {
cl_git_rewritefile("attr/.gitignore", "/\n\n/NewFolder\n/NewFolder/NewFolder"); cl_git_rewritefile("attr/.gitignore", "/\n\n/NewFolder\n/NewFolder/NewFolder");
...@@ -133,7 +133,7 @@ void test_attr_ignore__ignore_root(void) ...@@ -133,7 +133,7 @@ void test_attr_ignore__ignore_root(void)
assert_is_ignored(true, "NewFolder/NewFolder/File.txt"); assert_is_ignored(true, "NewFolder/NewFolder/File.txt");
} }
void test_attr_ignore__full_paths(void) void test_ignore_path__full_paths(void)
{ {
cl_git_rewritefile("attr/.gitignore", "Folder/*/Contained"); cl_git_rewritefile("attr/.gitignore", "Folder/*/Contained");
...@@ -153,7 +153,7 @@ void test_attr_ignore__full_paths(void) ...@@ -153,7 +153,7 @@ void test_attr_ignore__full_paths(void)
assert_is_ignored(false, "Folder/Middle/More/More/Contained/Not/Happy/Child"); assert_is_ignored(false, "Folder/Middle/More/More/Contained/Not/Happy/Child");
} }
void test_attr_ignore__more_starstar_cases(void) void test_ignore_path__more_starstar_cases(void)
{ {
cl_must_pass(p_unlink("attr/.gitignore")); cl_must_pass(p_unlink("attr/.gitignore"));
cl_git_mkfile( cl_git_mkfile(
...@@ -171,7 +171,7 @@ void test_attr_ignore__more_starstar_cases(void) ...@@ -171,7 +171,7 @@ void test_attr_ignore__more_starstar_cases(void)
assert_is_ignored(false, "sub/sub2/aaa.html"); assert_is_ignored(false, "sub/sub2/aaa.html");
} }
void test_attr_ignore__leading_stars(void) void test_ignore_path__leading_stars(void)
{ {
cl_git_rewritefile( cl_git_rewritefile(
"attr/.gitignore", "attr/.gitignore",
...@@ -204,7 +204,7 @@ void test_attr_ignore__leading_stars(void) ...@@ -204,7 +204,7 @@ void test_attr_ignore__leading_stars(void)
assert_is_ignored(false, "dir1/kid2/file"); assert_is_ignored(false, "dir1/kid2/file");
} }
void test_attr_ignore__globs_and_path_delimiters(void) void test_ignore_path__globs_and_path_delimiters(void)
{ {
cl_git_rewritefile("attr/.gitignore", "foo/bar/**"); cl_git_rewritefile("attr/.gitignore", "foo/bar/**");
assert_is_ignored(true, "foo/bar/baz"); assert_is_ignored(true, "foo/bar/baz");
...@@ -230,7 +230,7 @@ void test_attr_ignore__globs_and_path_delimiters(void) ...@@ -230,7 +230,7 @@ void test_attr_ignore__globs_and_path_delimiters(void)
assert_is_ignored(false, "_test/foo/bar/code/file"); assert_is_ignored(false, "_test/foo/bar/code/file");
} }
void test_attr_ignore__skip_gitignore_directory(void) void test_ignore_path__skip_gitignore_directory(void)
{ {
cl_git_rewritefile("attr/.git/info/exclude", "/NewFolder\n/NewFolder/NewFolder"); cl_git_rewritefile("attr/.git/info/exclude", "/NewFolder\n/NewFolder/NewFolder");
p_unlink("attr/.gitignore"); p_unlink("attr/.gitignore");
...@@ -244,7 +244,7 @@ void test_attr_ignore__skip_gitignore_directory(void) ...@@ -244,7 +244,7 @@ void test_attr_ignore__skip_gitignore_directory(void)
assert_is_ignored(true, "NewFolder/NewFolder/File.txt"); assert_is_ignored(true, "NewFolder/NewFolder/File.txt");
} }
void test_attr_ignore__subdirectory_gitignore(void) void test_ignore_path__subdirectory_gitignore(void)
{ {
p_unlink("attr/.gitignore"); p_unlink("attr/.gitignore");
cl_assert(!git_path_exists("attr/.gitignore")); cl_assert(!git_path_exists("attr/.gitignore"));
...@@ -262,7 +262,7 @@ void test_attr_ignore__subdirectory_gitignore(void) ...@@ -262,7 +262,7 @@ void test_attr_ignore__subdirectory_gitignore(void)
assert_is_ignored(false, "dir/file3"); assert_is_ignored(false, "dir/file3");
} }
void test_attr_ignore__expand_tilde_to_homedir(void) void test_ignore_path__expand_tilde_to_homedir(void)
{ {
git_config *cfg; git_config *cfg;
...@@ -292,7 +292,7 @@ void test_attr_ignore__expand_tilde_to_homedir(void) ...@@ -292,7 +292,7 @@ void test_attr_ignore__expand_tilde_to_homedir(void)
/* Ensure that the .gitignore in the subdirectory only affects /* Ensure that the .gitignore in the subdirectory only affects
* items in the subdirectory. */ * items in the subdirectory. */
void test_attr_ignore__gitignore_in_subdir(void) void test_ignore_path__gitignore_in_subdir(void)
{ {
cl_git_rmfile("attr/.gitignore"); cl_git_rmfile("attr/.gitignore");
...@@ -320,7 +320,7 @@ void test_attr_ignore__gitignore_in_subdir(void) ...@@ -320,7 +320,7 @@ void test_attr_ignore__gitignore_in_subdir(void)
} }
/* Ensure that files do not match folder cases */ /* Ensure that files do not match folder cases */
void test_attr_ignore__dont_ignore_files_for_folder(void) void test_ignore_path__dont_ignore_files_for_folder(void)
{ {
cl_git_rmfile("attr/.gitignore"); cl_git_rmfile("attr/.gitignore");
...@@ -351,7 +351,7 @@ void test_attr_ignore__dont_ignore_files_for_folder(void) ...@@ -351,7 +351,7 @@ void test_attr_ignore__dont_ignore_files_for_folder(void)
assert_is_ignored(false, "dir/TeSt"); assert_is_ignored(false, "dir/TeSt");
} }
void test_attr_ignore__symlink_to_outside(void) void test_ignore_path__symlink_to_outside(void)
{ {
#ifdef GIT_WIN32 #ifdef GIT_WIN32
cl_skip(); cl_skip();
...@@ -364,7 +364,7 @@ void test_attr_ignore__symlink_to_outside(void) ...@@ -364,7 +364,7 @@ void test_attr_ignore__symlink_to_outside(void)
assert_is_ignored(true, "lala/../symlink"); assert_is_ignored(true, "lala/../symlink");
} }
void test_attr_ignore__test(void) void test_ignore_path__test(void)
{ {
cl_git_rewritefile("attr/.gitignore", cl_git_rewritefile("attr/.gitignore",
"/*/\n" "/*/\n"
...@@ -376,7 +376,7 @@ void test_attr_ignore__test(void) ...@@ -376,7 +376,7 @@ void test_attr_ignore__test(void)
assert_is_ignored(true, "bin/foo"); assert_is_ignored(true, "bin/foo");
} }
void test_attr_ignore__unignore_dir_succeeds(void) void test_ignore_path__unignore_dir_succeeds(void)
{ {
cl_git_rewritefile("attr/.gitignore", cl_git_rewritefile("attr/.gitignore",
"*.c\n" "*.c\n"
...@@ -385,7 +385,7 @@ void test_attr_ignore__unignore_dir_succeeds(void) ...@@ -385,7 +385,7 @@ void test_attr_ignore__unignore_dir_succeeds(void)
assert_is_ignored(true, "src/foo/foo.c"); assert_is_ignored(true, "src/foo/foo.c");
} }
void test_attr_ignore__case_insensitive_unignores_previous_rule(void) void test_ignore_path__case_insensitive_unignores_previous_rule(void)
{ {
git_config *cfg; git_config *cfg;
...@@ -402,7 +402,7 @@ void test_attr_ignore__case_insensitive_unignores_previous_rule(void) ...@@ -402,7 +402,7 @@ void test_attr_ignore__case_insensitive_unignores_previous_rule(void)
assert_is_ignored(false, "case/file"); assert_is_ignored(false, "case/file");
} }
void test_attr_ignore__case_sensitive_unignore_does_nothing(void) void test_ignore_path__case_sensitive_unignore_does_nothing(void)
{ {
git_config *cfg; git_config *cfg;
...@@ -419,7 +419,7 @@ void test_attr_ignore__case_sensitive_unignore_does_nothing(void) ...@@ -419,7 +419,7 @@ void test_attr_ignore__case_sensitive_unignore_does_nothing(void)
assert_is_ignored(true, "case/file"); assert_is_ignored(true, "case/file");
} }
void test_attr_ignore__ignored_subdirfiles_with_subdir_rule(void) void test_ignore_path__ignored_subdirfiles_with_subdir_rule(void)
{ {
cl_git_rewritefile( cl_git_rewritefile(
"attr/.gitignore", "attr/.gitignore",
...@@ -431,7 +431,7 @@ void test_attr_ignore__ignored_subdirfiles_with_subdir_rule(void) ...@@ -431,7 +431,7 @@ void test_attr_ignore__ignored_subdirfiles_with_subdir_rule(void)
assert_is_ignored(true, "dir/sub1/sub2"); assert_is_ignored(true, "dir/sub1/sub2");
} }
void test_attr_ignore__ignored_subdirfiles_with_negations(void) void test_ignore_path__ignored_subdirfiles_with_negations(void)
{ {
cl_git_rewritefile( cl_git_rewritefile(
"attr/.gitignore", "attr/.gitignore",
...@@ -443,7 +443,7 @@ void test_attr_ignore__ignored_subdirfiles_with_negations(void) ...@@ -443,7 +443,7 @@ void test_attr_ignore__ignored_subdirfiles_with_negations(void)
assert_is_ignored(true, "dir/sub1/c.test"); assert_is_ignored(true, "dir/sub1/c.test");
} }
void test_attr_ignore__negative_directory_rules_only_match_directories(void) void test_ignore_path__negative_directory_rules_only_match_directories(void)
{ {
cl_git_rewritefile( cl_git_rewritefile(
"attr/.gitignore", "attr/.gitignore",
...@@ -459,3 +459,82 @@ void test_attr_ignore__negative_directory_rules_only_match_directories(void) ...@@ -459,3 +459,82 @@ void test_attr_ignore__negative_directory_rules_only_match_directories(void)
assert_is_ignored(false, "src/A.keep"); assert_is_ignored(false, "src/A.keep");
assert_is_ignored(false, ".gitignore"); assert_is_ignored(false, ".gitignore");
} }
void test_ignore_path__escaped_character(void)
{
cl_git_rewritefile("attr/.gitignore", "\\c\n");
assert_is_ignored(true, "c");
assert_is_ignored(false, "\\c");
}
void test_ignore_path__escaped_newline(void)
{
cl_git_rewritefile(
"attr/.gitignore",
"\\\nnewline\n"
);
assert_is_ignored(true, "\nnewline");
}
void test_ignore_path__escaped_glob(void)
{
cl_git_rewritefile("attr/.gitignore", "\\*\n");
assert_is_ignored(true, "*");
assert_is_ignored(false, "foo");
}
void test_ignore_path__escaped_comments(void)
{
cl_git_rewritefile(
"attr/.gitignore",
"#foo\n"
"\\#bar\n"
"\\##baz\n"
"\\#\\\\#qux\n"
);
assert_is_ignored(false, "#foo");
assert_is_ignored(true, "#bar");
assert_is_ignored(false, "\\#bar");
assert_is_ignored(true, "##baz");
assert_is_ignored(false, "\\##baz");
assert_is_ignored(true, "#\\#qux");
assert_is_ignored(false, "##qux");
assert_is_ignored(false, "\\##qux");
}
void test_ignore_path__escaped_slash(void)
{
cl_git_rewritefile(
"attr/.gitignore",
"\\\\\n"
"\\\\preceding\n"
"inter\\\\mittent\n"
"trailing\\\\\n"
);
#ifndef GIT_WIN32
assert_is_ignored(true, "\\");
assert_is_ignored(true, "\\preceding");
#endif
assert_is_ignored(true, "inter\\mittent");
assert_is_ignored(true, "trailing\\");
}
void test_ignore_path__escaped_space(void)
{
cl_git_rewritefile(
"attr/.gitignore",
"foo\\\\ \n"
"bar\\\\\\ \n");
assert_is_ignored(true, "foo\\");
assert_is_ignored(false, "foo\\ ");
assert_is_ignored(false, "foo\\\\ ");
assert_is_ignored(false, "foo\\\\");
assert_is_ignored(true, "bar\\ ");
assert_is_ignored(false, "bar\\\\");
assert_is_ignored(false, "bar\\\\ ");
assert_is_ignored(false, "bar\\\\\\");
assert_is_ignored(false, "bar\\\\\\ ");
}
...@@ -343,6 +343,16 @@ void test_path_core__join_unrooted(void) ...@@ -343,6 +343,16 @@ void test_path_core__join_unrooted(void)
test_join_unrooted("c:/foo", 2, "c:/foo", "c:/asdf"); test_join_unrooted("c:/foo", 2, "c:/foo", "c:/asdf");
test_join_unrooted("c:/foo/bar", 2, "c:/foo/bar", "c:/asdf"); test_join_unrooted("c:/foo/bar", 2, "c:/foo/bar", "c:/asdf");
#ifdef GIT_WIN32
/* Paths starting with '\\' are absolute */
test_join_unrooted("\\bar", 0, "\\bar", "c:/foo/");
test_join_unrooted("\\\\network\\bar", 9, "\\\\network\\bar", "c:/foo/");
#else
/* Paths starting with '\\' are not absolute on non-Windows systems */
test_join_unrooted("/foo/\\bar", 4, "\\bar", "/foo");
test_join_unrooted("c:/foo/\\bar", 7, "\\bar", "c:/foo/");
#endif
/* Base is returned when it's provided and is the prefix */ /* Base is returned when it's provided and is the prefix */
test_join_unrooted("c:/foo/bar/foobar", 6, "c:/foo/bar/foobar", "c:/foo"); test_join_unrooted("c:/foo/bar/foobar", 6, "c:/foo/bar/foobar", "c:/foo");
test_join_unrooted("c:/foo/bar/foobar", 10, "c:/foo/bar/foobar", "c:/foo/bar"); test_join_unrooted("c:/foo/bar/foobar", 10, "c:/foo/bar/foobar", "c:/foo/bar");
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment