Commit ec74b40ceef3dc3892c7d84bb4f5d99bab504ba4

Edward Thomson 2014-12-16T18:53:55

Introduce core.protectHFS and core.protectNTFS Validate HFS ignored char ".git" paths when `core.protectHFS` is specified. Validate NTFS invalid ".git" paths when `core.protectNTFS` is specified.

diff --git a/src/config_cache.c b/src/config_cache.c
index 45c39ce..d397a4b 100644
--- a/src/config_cache.c
+++ b/src/config_cache.c
@@ -76,6 +76,8 @@ static struct map_data _cvar_maps[] = {
 	{"core.precomposeunicode", NULL, 0, GIT_PRECOMPOSE_DEFAULT },
 	{"core.safecrlf", _cvar_map_safecrlf, ARRAY_SIZE(_cvar_map_safecrlf), GIT_SAFE_CRLF_DEFAULT},
 	{"core.logallrefupdates", NULL, 0, GIT_LOGALLREFUPDATES_DEFAULT },
+	{"core.protecthfs", NULL, 0, GIT_PROTECTHFS_DEFAULT },
+	{"core.protectntfs", NULL, 0, GIT_PROTECTNTFS_DEFAULT },
 };
 
 int git_config__cvar(int *out, git_config *config, git_cvar_cached cvar)
diff --git a/src/path.c b/src/path.c
index b9c9729..768a7e1 100644
--- a/src/path.c
+++ b/src/path.c
@@ -1240,26 +1240,6 @@ int git_path_from_url_or_path(git_buf *local_path_out, const char *url_or_path)
 		return git_buf_sets(local_path_out, url_or_path);
 }
 
-GIT_INLINE(bool) verify_shortname(
-	git_repository *repo,
-	const char *component,
-	size_t len)
-{
-	const char *shortname_repo;
-
-	if (len == git_repository__8dot3_default_len &&
-		strncasecmp(git_repository__8dot3_default, component, len) == 0)
-		return false;
-
-	if (repo &&
-		(shortname_repo = git_repository__8dot3_name(repo)) &&
-		shortname_repo != git_repository__8dot3_default &&
-		git__prefixncmp_icase(component, len, shortname_repo) == 0)
-		return false;
-
-	return true;
-}
-
 /* Reject paths like AUX or COM1, or those versions that end in a dot or
  * colon.  ("AUX." or "AUX:")
  */
@@ -1335,11 +1315,48 @@ static bool verify_dotgit_hfs(const char *path, size_t len)
 	return false;
 }
 
+GIT_INLINE(bool) verify_dotgit_ntfs(git_repository *repo, const char *path, size_t len)
+{
+	const char *shortname = NULL;
+	size_t i, start, shortname_len = 0;
+
+	/* See if the repo has a custom shortname (not "GIT~1") */
+	if (repo &&
+		(shortname = git_repository__8dot3_name(repo)) &&
+		shortname != git_repository__8dot3_default)
+		shortname_len = strlen(shortname);
+
+	if (len >= 4 && strncasecmp(path, ".git", 4) == 0)
+		start = 4;
+	else if (len >= git_repository__8dot3_default_len &&
+		strncasecmp(path, git_repository__8dot3_default, git_repository__8dot3_default_len) == 0)
+		start = git_repository__8dot3_default_len;
+	else if (shortname_len && len >= shortname_len &&
+		strncasecmp(path, shortname, shortname_len) == 0)
+		start = shortname_len;
+	else
+		return true;
+
+	/* Reject paths beginning with ".git\" */
+	if (path[start] == '\\')
+		return false;
+
+	for (i = start; i < len; i++) {
+		if (path[i] != ' ' && path[i] != '.')
+			return true;
+	}
+
+	return false;
+}
+
 GIT_INLINE(bool) verify_char(unsigned char c, unsigned int flags)
 {
 	if ((flags & GIT_PATH_REJECT_BACKSLASH) && c == '\\')
 		return false;
 
+	if ((flags & GIT_PATH_REJECT_SLASH) && c == '/')
+		return false;
+
 	if (flags & GIT_PATH_REJECT_NT_CHARS) {
 		if (c < 32)
 			return false;
@@ -1385,13 +1402,6 @@ static bool verify_component(
 		len == 2 && component[0] == '.' && component[1] == '.')
 		return false;
 
-	if ((flags & GIT_PATH_REJECT_DOT_GIT) && len == 4 &&
-		component[0] == '.' &&
-		(component[1] == 'g' || component[1] == 'G') &&
-		(component[2] == 'i' || component[2] == 'I') &&
-		(component[3] == 't' || component[3] == 'T'))
-		return false;
-
 	if ((flags & GIT_PATH_REJECT_TRAILING_DOT) && component[len-1] == '.')
 		return false;
 
@@ -1401,10 +1411,6 @@ static bool verify_component(
 	if ((flags & GIT_PATH_REJECT_TRAILING_COLON) && component[len-1] == ':')
 		return false;
 
-	if ((flags & GIT_PATH_REJECT_DOS_GIT_SHORTNAME) &&
-		!verify_shortname(repo, component, len))
-		return false;
-
 	if (flags & GIT_PATH_REJECT_DOS_PATHS) {
 		if (!verify_dospath(component, len, "CON", false) ||
 			!verify_dospath(component, len, "PRN", false) ||
@@ -1419,9 +1425,50 @@ static bool verify_component(
 		!verify_dotgit_hfs(component, len))
 		return false;
 
+	if (flags & GIT_PATH_REJECT_DOT_GIT_NTFS &&
+		!verify_dotgit_ntfs(repo, component, len))
+		return false;
+
+	if ((flags & GIT_PATH_REJECT_DOT_GIT_HFS) == 0 &&
+		(flags & GIT_PATH_REJECT_DOT_GIT_NTFS) == 0 &&
+		(flags & GIT_PATH_REJECT_DOT_GIT) &&
+		len == 4 &&
+		component[0] == '.' &&
+		(component[1] == 'g' || component[1] == 'G') &&
+		(component[2] == 'i' || component[2] == 'I') &&
+		(component[3] == 't' || component[3] == 'T'))
+		return false;
+
 	return true;
 }
 
+GIT_INLINE(unsigned int) dotgit_flags(
+	git_repository *repo,
+	unsigned int flags)
+{
+	int protectHFS = 0, protectNTFS = 0;
+
+#ifdef __APPLE__
+	protectHFS = 1;
+#endif
+
+#ifdef GIT_WIN32
+	protectNTFS = 1;
+#endif
+
+	if (repo && !protectHFS)
+		git_repository__cvar(&protectHFS, repo, GIT_CVAR_PROTECTHFS);
+	if (protectHFS)
+		flags |= GIT_PATH_REJECT_DOT_GIT_HFS;
+
+	if (repo && !protectNTFS)
+		git_repository__cvar(&protectNTFS, repo, GIT_CVAR_PROTECTNTFS);
+	if (protectNTFS)
+		flags |= GIT_PATH_REJECT_DOT_GIT_NTFS;
+
+	return flags;
+}
+
 bool git_path_isvalid(
 	git_repository *repo,
 	const char *path,
@@ -1429,6 +1476,10 @@ bool git_path_isvalid(
 {
 	const char *start, *c;
 
+	/* Upgrade the ".git" checks based on platform */
+	if ((flags & GIT_PATH_REJECT_DOT_GIT))
+		flags = dotgit_flags(repo, flags);
+
 	for (start = c = path; *c; c++) {
 		if (!verify_char(*c, flags))
 			return false;
diff --git a/src/path.h b/src/path.h
index 4e25d93..b753140 100644
--- a/src/path.h
+++ b/src/path.h
@@ -465,15 +465,20 @@ extern int git_path_from_url_or_path(git_buf *local_path_out, const char *url_or
 /* Flags to determine path validity in `git_path_isvalid` */
 #define GIT_PATH_REJECT_TRAVERSAL          (1 << 0)
 #define GIT_PATH_REJECT_DOT_GIT            (1 << 1)
-#define GIT_PATH_REJECT_BACKSLASH          (1 << 2)
-#define GIT_PATH_REJECT_TRAILING_DOT       (1 << 3)
-#define GIT_PATH_REJECT_TRAILING_SPACE     (1 << 4)
-#define GIT_PATH_REJECT_TRAILING_COLON     (1 << 5)
-#define GIT_PATH_REJECT_DOS_GIT_SHORTNAME  (1 << 6)
+#define GIT_PATH_REJECT_SLASH              (1 << 2)
+#define GIT_PATH_REJECT_BACKSLASH          (1 << 3)
+#define GIT_PATH_REJECT_TRAILING_DOT       (1 << 4)
+#define GIT_PATH_REJECT_TRAILING_SPACE     (1 << 5)
+#define GIT_PATH_REJECT_TRAILING_COLON     (1 << 6)
 #define GIT_PATH_REJECT_DOS_PATHS          (1 << 7)
 #define GIT_PATH_REJECT_NT_CHARS           (1 << 8)
 #define GIT_PATH_REJECT_DOT_GIT_HFS        (1 << 9)
+#define GIT_PATH_REJECT_DOT_GIT_NTFS       (1 << 10)
 
+/* Default path safety for writing files to disk: since we use the
+ * Win32 "File Namespace" APIs ("\\?\") we need to protect from
+ * paths that the normal Win32 APIs would not write.
+ */
 #ifdef GIT_WIN32
 # define GIT_PATH_REJECT_DEFAULTS \
 	GIT_PATH_REJECT_TRAVERSAL | \
@@ -481,13 +486,8 @@ extern int git_path_from_url_or_path(git_buf *local_path_out, const char *url_or
 	GIT_PATH_REJECT_TRAILING_DOT | \
 	GIT_PATH_REJECT_TRAILING_SPACE | \
 	GIT_PATH_REJECT_TRAILING_COLON | \
-	GIT_PATH_REJECT_DOS_GIT_SHORTNAME | \
 	GIT_PATH_REJECT_DOS_PATHS | \
 	GIT_PATH_REJECT_NT_CHARS
-#elif __APPLE__
-# define GIT_PATH_REJECT_DEFAULTS \
-	GIT_PATH_REJECT_TRAVERSAL | \
-	GIT_PATH_REJECT_DOT_GIT_HFS
 #else
 # define GIT_PATH_REJECT_DEFAULTS GIT_PATH_REJECT_TRAVERSAL
 #endif
diff --git a/src/repository.h b/src/repository.h
index d9b950a..6da8c28 100644
--- a/src/repository.h
+++ b/src/repository.h
@@ -40,6 +40,8 @@ typedef enum {
 	GIT_CVAR_PRECOMPOSE,    /* core.precomposeunicode */
 	GIT_CVAR_SAFE_CRLF,		/* core.safecrlf */
 	GIT_CVAR_LOGALLREFUPDATES, /* core.logallrefupdates */
+	GIT_CVAR_PROTECTHFS,    /* core.protectHFS */
+	GIT_CVAR_PROTECTNTFS,   /* core.protectNTFS */
 	GIT_CVAR_CACHE_MAX
 } git_cvar_cached;
 
@@ -96,6 +98,10 @@ typedef enum {
 	/* core.logallrefupdates */
 	GIT_LOGALLREFUPDATES_UNSET = 2,
 	GIT_LOGALLREFUPDATES_DEFAULT = GIT_LOGALLREFUPDATES_UNSET,
+	/* core.protectHFS */
+	GIT_PROTECTHFS_DEFAULT = GIT_CVAR_FALSE,
+	/* core.protectNTFS */
+	GIT_PROTECTNTFS_DEFAULT = GIT_CVAR_FALSE,
 } git_cvar_value;
 
 /* internal repository init flags */
diff --git a/tests/checkout/nasty.c b/tests/checkout/nasty.c
index a667dcd..c07d938 100644
--- a/tests/checkout/nasty.c
+++ b/tests/checkout/nasty.c
@@ -291,3 +291,35 @@ void test_checkout_nasty__dot_git_hfs_ignorable(void)
 	test_checkout_fails("refs/heads/dotgit_hfs_ignorable_16", ".git/foobar");
 #endif
 }
+
+void test_checkout_nasty__honors_core_protecthfs(void)
+{
+	cl_repo_set_bool(repo, "core.protectHFS", true);
+
+	test_checkout_fails("refs/heads/dotgit_hfs_ignorable_1", ".git/foobar");
+	test_checkout_fails("refs/heads/dotgit_hfs_ignorable_2", ".git/foobar");
+	test_checkout_fails("refs/heads/dotgit_hfs_ignorable_3", ".git/foobar");
+	test_checkout_fails("refs/heads/dotgit_hfs_ignorable_4", ".git/foobar");
+	test_checkout_fails("refs/heads/dotgit_hfs_ignorable_5", ".git/foobar");
+	test_checkout_fails("refs/heads/dotgit_hfs_ignorable_6", ".git/foobar");
+	test_checkout_fails("refs/heads/dotgit_hfs_ignorable_7", ".git/foobar");
+	test_checkout_fails("refs/heads/dotgit_hfs_ignorable_8", ".git/foobar");
+	test_checkout_fails("refs/heads/dotgit_hfs_ignorable_9", ".git/foobar");
+	test_checkout_fails("refs/heads/dotgit_hfs_ignorable_10", ".git/foobar");
+	test_checkout_fails("refs/heads/dotgit_hfs_ignorable_11", ".git/foobar");
+	test_checkout_fails("refs/heads/dotgit_hfs_ignorable_12", ".git/foobar");
+	test_checkout_fails("refs/heads/dotgit_hfs_ignorable_13", ".git/foobar");
+	test_checkout_fails("refs/heads/dotgit_hfs_ignorable_14", ".git/foobar");
+	test_checkout_fails("refs/heads/dotgit_hfs_ignorable_15", ".git/foobar");
+	test_checkout_fails("refs/heads/dotgit_hfs_ignorable_16", ".git/foobar");
+}
+
+void test_checkout_nasty__honors_core_protectntfs(void)
+{
+	cl_repo_set_bool(repo, "core.protectNTFS", true);
+
+	test_checkout_fails("refs/heads/dotgit_backslash_path", ".git/foobar");
+	test_checkout_fails("refs/heads/dotcapitalgit_backslash_path", ".GIT/foobar");
+	test_checkout_fails("refs/heads/dot_git_dot", ".git/foobar");
+	test_checkout_fails("refs/heads/git_tilde1", ".git/foobar");
+}
diff --git a/tests/path/core.c b/tests/path/core.c
index 528108b..85fee82 100644
--- a/tests/path/core.c
+++ b/tests/path/core.c
@@ -172,11 +172,27 @@ void test_path_core__isvalid_trailing_colon(void)
 	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo:/bar", GIT_PATH_REJECT_TRAILING_COLON));
 }
 
-void test_path_core__isvalid_dos_git_shortname(void)
+void test_path_core__isvalid_dotgit_ntfs(void)
 {
-	cl_assert_equal_b(true, git_path_isvalid(NULL, "git~1", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".git", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".git ", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".git.", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".git.. .", 0));
 
-	cl_assert_equal_b(false, git_path_isvalid(NULL, "git~1", GIT_PATH_REJECT_DOS_GIT_SHORTNAME));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "git~1", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "git~1 ", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "git~1.", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "git~1.. .", 0));
+
+	cl_assert_equal_b(false, git_path_isvalid(NULL, ".git", GIT_PATH_REJECT_DOT_GIT_NTFS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, ".git ", GIT_PATH_REJECT_DOT_GIT_NTFS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, ".git.", GIT_PATH_REJECT_DOT_GIT_NTFS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, ".git.. .", GIT_PATH_REJECT_DOT_GIT_NTFS));
+
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "git~1", GIT_PATH_REJECT_DOT_GIT_NTFS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "git~1 ", GIT_PATH_REJECT_DOT_GIT_NTFS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "git~1.", GIT_PATH_REJECT_DOT_GIT_NTFS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "git~1.. .", GIT_PATH_REJECT_DOT_GIT_NTFS));
 }
 
 void test_path_core__isvalid_dos_paths(void)