Commit 7c791f3dfcc00b892bcea9a1df63b36737d6eb1a

Edward Thomson 2018-10-20T20:25:51

Merge pull request #4852 from libgit2/ethomson/unc_paths Win32 path canonicalization refactoring

diff --git a/src/win32/path_w32.c b/src/win32/path_w32.c
index 5e24260..b955b02 100644
--- a/src/win32/path_w32.c
+++ b/src/win32/path_w32.c
@@ -220,7 +220,7 @@ int git_win32_path_from_utf8(git_win32_path out, const char *src)
 			goto on_error;
 		}
 
-		/* Skip the drive letter specification ("C:") */	
+		/* Skip the drive letter specification ("C:") */
 		if (git__utf8_to_16(dest + 2, MAX_PATH - 2, src) < 0)
 			goto on_error;
 	}
@@ -315,7 +315,7 @@ static bool path_is_volume(wchar_t *target, size_t target_len)
 }
 
 /* On success, returns the length, in characters, of the path stored in dest.
-* On failure, returns a negative value. */
+ * On failure, returns a negative value. */
 int git_win32_path_readlink_w(git_win32_path dest, const git_win32_path path)
 {
 	BYTE buf[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
@@ -360,16 +360,16 @@ int git_win32_path_readlink_w(git_win32_path dest, const git_win32_path path)
 
 	if (path_is_volume(target, target_len)) {
 		/* This path is a reparse point that represents another volume mounted
-		* at this location, it is not a symbolic link our input was canonical.
-		*/
+		 * at this location, it is not a symbolic link our input was canonical.
+		 */
 		errno = EINVAL;
 		error = -1;
 	} else if (target_len) {
-		/* The path may need to have a prefix removed. */
-		target_len = git_win32__canonicalize_path(target, target_len);
+		/* The path may need to have a namespace prefix removed. */
+		target_len = git_win32_path_remove_namespace(target, target_len);
 
 		/* Need one additional character in the target buffer
-		* for the terminating NULL. */
+		 * for the terminating NULL. */
 		if (GIT_WIN_PATH_UTF16 > target_len) {
 			wcscpy(dest, target);
 			error = (int)target_len;
@@ -380,3 +380,97 @@ on_error:
 	CloseHandle(handle);
 	return error;
 }
+
+/**
+ * Removes any trailing backslashes from a path, except in the case of a drive
+ * letter path (C:\, D:\, etc.). This function cannot fail.
+ *
+ * @param path The path which should be trimmed.
+ * @return The length of the modified string (<= the input length)
+ */
+size_t git_win32_path_trim_end(wchar_t *str, size_t len)
+{
+	while (1) {
+		if (!len || str[len - 1] != L'\\')
+			break;
+
+		/*
+		 * Don't trim backslashes from drive letter paths, which
+		 * are 3 characters long and of the form C:\, D:\, etc.
+		 */
+		if (len == 3 && git_win32__isalpha(str[0]) && str[1] == ':')
+			break;
+
+		len--;
+	}
+
+	str[len] = L'\0';
+
+	return len;
+}
+
+/**
+ * Removes any of the following namespace prefixes from a path,
+ * if found: "\??\", "\\?\", "\\?\UNC\". This function cannot fail.
+ *
+ * @param path The path which should be converted.
+ * @return The length of the modified string (<= the input length)
+ */
+size_t git_win32_path_remove_namespace(wchar_t *str, size_t len)
+{
+	static const wchar_t dosdevices_namespace[] = L"\\\?\?\\";
+	static const wchar_t nt_namespace[] = L"\\\\?\\";
+	static const wchar_t unc_namespace_remainder[] = L"UNC\\";
+	static const wchar_t unc_prefix[] = L"\\\\";
+
+	const wchar_t *prefix = NULL, *remainder = NULL;
+	size_t prefix_len = 0, remainder_len = 0;
+
+	/* "\??\" -- DOS Devices prefix */
+	if (len >= CONST_STRLEN(dosdevices_namespace) &&
+		!wcsncmp(str, dosdevices_namespace, CONST_STRLEN(dosdevices_namespace))) {
+		remainder = str + CONST_STRLEN(dosdevices_namespace);
+		remainder_len = len - CONST_STRLEN(dosdevices_namespace);
+	}
+	/* "\\?\" -- NT namespace prefix */
+	else if (len >= CONST_STRLEN(nt_namespace) &&
+		!wcsncmp(str, nt_namespace, CONST_STRLEN(nt_namespace))) {
+		remainder = str + CONST_STRLEN(nt_namespace);
+		remainder_len = len - CONST_STRLEN(nt_namespace);
+	}
+
+	/* "\??\UNC\", "\\?\UNC\" -- UNC prefix */
+	if (remainder_len >= CONST_STRLEN(unc_namespace_remainder) &&
+		!wcsncmp(remainder, unc_namespace_remainder, CONST_STRLEN(unc_namespace_remainder))) {
+
+		/*
+		 * The proper Win32 path for a UNC share has "\\" at beginning of it
+		 * and looks like "\\server\share\<folderStructure>".  So remove the
+		 * UNC namespace and add a prefix of "\\" in its place.
+		 */
+		remainder += CONST_STRLEN(unc_namespace_remainder);
+		remainder_len -= CONST_STRLEN(unc_namespace_remainder);
+
+		prefix = unc_prefix;
+		prefix_len = CONST_STRLEN(unc_prefix);
+	}
+
+	if (remainder) {
+		/*
+		 * Sanity check that the new string isn't longer than the old one.
+		 * (This could only happen due to programmer error introducing a
+		 * prefix longer than the namespace it replaces.)
+		 */
+		assert(len >= remainder_len + prefix_len);
+
+		if (prefix)
+			memmove(str, prefix, prefix_len * sizeof(wchar_t));
+
+		memmove(str + prefix_len, remainder, remainder_len * sizeof(wchar_t));
+
+		len = remainder_len + prefix_len;
+		str[len] = L'\0';
+	}
+
+	return git_win32_path_trim_end(str, len);
+}
diff --git a/src/win32/path_w32.h b/src/win32/path_w32.h
index 83ffd1f..facbced 100644
--- a/src/win32/path_w32.h
+++ b/src/win32/path_w32.h
@@ -83,4 +83,22 @@ extern char *git_win32_path_8dot3_name(const char *path);
 
 extern int git_win32_path_readlink_w(git_win32_path dest, const git_win32_path path);
 
+/**
+ * Removes any trailing backslashes from a path, except in the case of a drive
+ * letter path (C:\, D:\, etc.). This function cannot fail.
+ *
+ * @param path The path which should be trimmed.
+ * @return The length of the modified string (<= the input length)
+ */
+size_t git_win32_path_trim_end(wchar_t *str, size_t len);
+
+/**
+ * Removes any of the following namespace prefixes from a path,
+ * if found: "\??\", "\\?\", "\\?\UNC\". This function cannot fail.
+ *
+ * @param path The path which should be converted.
+ * @return The length of the modified string (<= the input length)
+ */
+size_t git_win32_path_remove_namespace(wchar_t *str, size_t len);
+
 #endif
diff --git a/src/win32/posix_w32.c b/src/win32/posix_w32.c
index 8617e45..8c321ef 100644
--- a/src/win32/posix_w32.c
+++ b/src/win32/posix_w32.c
@@ -354,7 +354,7 @@ static int do_lstat(const char *path, struct stat *buf, bool posixly_correct)
 	if ((len = git_win32_path_from_utf8(path_w, path)) < 0)
 		return -1;
 
-	git_win32__path_trim_end(path_w, len);
+	git_win32_path_trim_end(path_w, len);
 
 	return lstat_w(path_w, buf, posixly_correct);
 }
@@ -648,8 +648,8 @@ static int getfinalpath_w(
 	if (!dwChars || dwChars >= GIT_WIN_PATH_UTF16)
 		return -1;
 
-	/* The path may be delivered to us with a prefix; canonicalize */
-	return (int)git_win32__canonicalize_path(dest, dwChars);
+	/* The path may be delivered to us with a namespace prefix; remove */
+	return (int)git_win32_path_remove_namespace(dest, dwChars);
 }
 
 static int follow_and_lstat_link(git_win32_path path, struct stat* buf)
diff --git a/src/win32/w32_util.c b/src/win32/w32_util.c
index b7b1ffa..5996c9f 100644
--- a/src/win32/w32_util.c
+++ b/src/win32/w32_util.c
@@ -93,71 +93,3 @@ int git_win32__hidden(bool *out, const char *path)
 	*out = (attrs & FILE_ATTRIBUTE_HIDDEN) ? true : false;
 	return 0;
 }
-
-/**
- * Removes any trailing backslashes from a path, except in the case of a drive
- * letter path (C:\, D:\, etc.). This function cannot fail.
- *
- * @param path The path which should be trimmed.
- * @return The length of the modified string (<= the input length)
- */
-size_t git_win32__path_trim_end(wchar_t *str, size_t len)
-{
-	while (1) {
-		if (!len || str[len - 1] != L'\\')
-			break;
-
-		/* Don't trim backslashes from drive letter paths, which
-		 * are 3 characters long and of the form C:\, D:\, etc. */
-		if (len == 3 && git_win32__isalpha(str[0]) && str[1] == ':')
-			break;
-
-		len--;
-	}
-
-	str[len] = L'\0';
-
-	return len;
-}
-
-/**
- * Removes any of the following namespace prefixes from a path,
- * if found: "\??\", "\\?\", "\\?\UNC\". This function cannot fail.
- *
- * @param path The path which should be converted.
- * @return The length of the modified string (<= the input length)
- */
-size_t git_win32__canonicalize_path(wchar_t *str, size_t len)
-{
-	static const wchar_t dosdevices_prefix[] = L"\\\?\?\\";
-	static const wchar_t nt_prefix[] = L"\\\\?\\";
-	static const wchar_t unc_prefix[] = L"UNC\\";
-	size_t to_advance = 0;
-
-	/* "\??\" -- DOS Devices prefix */
-	if (len >= CONST_STRLEN(dosdevices_prefix) &&
-		!wcsncmp(str, dosdevices_prefix, CONST_STRLEN(dosdevices_prefix))) {
-		to_advance += CONST_STRLEN(dosdevices_prefix);
-		len -= CONST_STRLEN(dosdevices_prefix);
-	}
-	/* "\\?\" -- NT namespace prefix */
-	else if (len >= CONST_STRLEN(nt_prefix) &&
-		!wcsncmp(str, nt_prefix, CONST_STRLEN(nt_prefix))) {
-		to_advance += CONST_STRLEN(nt_prefix);
-		len -= CONST_STRLEN(nt_prefix);
-	}
-
-	/* "\??\UNC\", "\\?\UNC\" -- UNC prefix */
-	if (to_advance && len >= CONST_STRLEN(unc_prefix) &&
-		!wcsncmp(str + to_advance, unc_prefix, CONST_STRLEN(unc_prefix))) {
-		to_advance += CONST_STRLEN(unc_prefix);
-		len -= CONST_STRLEN(unc_prefix);
-	}
-
-	if (to_advance) {
-		memmove(str, str + to_advance, len * sizeof(wchar_t));
-		str[len] = L'\0';
-	}
-
-	return git_win32__path_trim_end(str, len);
-}
diff --git a/src/win32/w32_util.h b/src/win32/w32_util.h
index 6531f47..5216a13 100644
--- a/src/win32/w32_util.h
+++ b/src/win32/w32_util.h
@@ -60,24 +60,6 @@ extern int git_win32__set_hidden(const char *path, bool hidden);
 extern int git_win32__hidden(bool *hidden, const char *path);
 
 /**
- * Removes any trailing backslashes from a path, except in the case of a drive
- * letter path (C:\, D:\, etc.). This function cannot fail.
- *
- * @param path The path which should be trimmed.
- * @return The length of the modified string (<= the input length)
- */
-size_t git_win32__path_trim_end(wchar_t *str, size_t len);
-
-/**
- * Removes any of the following namespace prefixes from a path,
- * if found: "\??\", "\\?\", "\\?\UNC\". This function cannot fail.
- *
- * @param path The path which should be converted.
- * @return The length of the modified string (<= the input length)
- */
-size_t git_win32__canonicalize_path(wchar_t *str, size_t len);
-
-/**
  * Converts a FILETIME structure to a struct timespec.
  *
  * @param FILETIME A pointer to a FILETIME
diff --git a/tests/path/win32.c b/tests/path/win32.c
index 4ff0397..a5413c7 100644
--- a/tests/path/win32.c
+++ b/tests/path/win32.c
@@ -129,7 +129,7 @@ void test_path_win32__absolute_from_relative(void)
 #endif
 }
 
-void test_canonicalize(const wchar_t *in, const wchar_t *expected)
+static void test_canonicalize(const wchar_t *in, const wchar_t *expected)
 {
 #ifdef GIT_WIN32
 	git_win32_path canonical;
@@ -145,6 +145,55 @@ void test_canonicalize(const wchar_t *in, const wchar_t *expected)
 #endif
 }
 
+static void test_remove_namespace(const wchar_t *in, const wchar_t *expected)
+{
+#ifdef GIT_WIN32
+	git_win32_path canonical;
+
+	cl_assert(wcslen(in) < MAX_PATH);
+	wcscpy(canonical, in);
+
+	cl_must_pass(git_win32_path_remove_namespace(canonical, wcslen(in)));
+	cl_assert_equal_wcs(expected, canonical);
+#else
+	GIT_UNUSED(in);
+	GIT_UNUSED(expected);
+#endif
+}
+
+void test_path_win32__remove_namespace(void)
+{
+	test_remove_namespace(L"\\\\?\\C:\\Temp\\Foo", L"C:\\Temp\\Foo");
+	test_remove_namespace(L"\\\\?\\C:\\", L"C:\\");
+	test_remove_namespace(L"\\\\?\\", L"");
+
+	test_remove_namespace(L"\\??\\C:\\Temp\\Foo", L"C:\\Temp\\Foo");
+	test_remove_namespace(L"\\??\\C:\\", L"C:\\");
+	test_remove_namespace(L"\\??\\", L"");
+
+	test_remove_namespace(L"\\\\?\\UNC\\server\\C$\\folder", L"\\\\server\\C$\\folder");
+	test_remove_namespace(L"\\\\?\\UNC\\server\\C$\\folder", L"\\\\server\\C$\\folder");
+	test_remove_namespace(L"\\\\?\\UNC\\server\\C$", L"\\\\server\\C$");
+	test_remove_namespace(L"\\\\?\\UNC\\server\\", L"\\\\server");
+	test_remove_namespace(L"\\\\?\\UNC\\server", L"\\\\server");
+
+	test_remove_namespace(L"\\??\\UNC\\server\\C$\\folder", L"\\\\server\\C$\\folder");
+	test_remove_namespace(L"\\??\\UNC\\server\\C$\\folder", L"\\\\server\\C$\\folder");
+	test_remove_namespace(L"\\??\\UNC\\server\\C$", L"\\\\server\\C$");
+	test_remove_namespace(L"\\??\\UNC\\server\\", L"\\\\server");
+	test_remove_namespace(L"\\??\\UNC\\server", L"\\\\server");
+
+	test_remove_namespace(L"\\\\server\\C$\\folder", L"\\\\server\\C$\\folder");
+	test_remove_namespace(L"\\\\server\\C$", L"\\\\server\\C$");
+	test_remove_namespace(L"\\\\server\\", L"\\\\server");
+	test_remove_namespace(L"\\\\server", L"\\\\server");
+
+	test_remove_namespace(L"C:\\Foo\\Bar", L"C:\\Foo\\Bar");
+	test_remove_namespace(L"C:\\", L"C:\\");
+	test_remove_namespace(L"", L"");
+
+}
+
 void test_path_win32__canonicalize(void)
 {
 #ifdef GIT_WIN32