Commit dc1ba018289aa40e1948a5fbb673652f86170677

Edward Thomson 2021-03-20T13:01:00

path: introduce ondisk and workdir path validation Introduce `git_path_validate_filesystem` which validates (absolute) on-disk paths and `git_path_validate_workdir` to perform validations on (absolute) working directory paths. These functions are useful as there may be system limitations on on-disk paths, particularly on Windows (for example, enforcing MAX_PATH). For working directory paths, these limitations may be per-repository, based on the `core.longpaths` configuration setting.

diff --git a/src/config_cache.c b/src/config_cache.c
index 37617af..4bb91f5 100644
--- a/src/config_cache.c
+++ b/src/config_cache.c
@@ -86,6 +86,7 @@ static struct map_data _configmaps[] = {
 	{"core.protecthfs", NULL, 0, GIT_PROTECTHFS_DEFAULT },
 	{"core.protectntfs", NULL, 0, GIT_PROTECTNTFS_DEFAULT },
 	{"core.fsyncobjectfiles", NULL, 0, GIT_FSYNCOBJECTFILES_DEFAULT },
+	{"core.longpaths", NULL, 0, GIT_LONGPATHS_DEFAULT },
 };
 
 int git_config__configmap_lookup(int *out, git_config *config, git_configmap_item item)
diff --git a/src/path.c b/src/path.c
index b4b1566..a4c289e 100644
--- a/src/path.c
+++ b/src/path.c
@@ -1904,6 +1904,46 @@ bool git_path_validate(
 	return verify_component(repo, start, (c - start), mode, flags);
 }
 
+#ifdef GIT_WIN32
+GIT_INLINE(bool) should_validate_longpaths(git_repository *repo)
+{
+	int longpaths = 0;
+
+	if (repo &&
+	    git_repository__configmap_lookup(&longpaths, repo, GIT_CONFIGMAP_LONGPATHS) < 0)
+		longpaths = 0;
+
+	return (longpaths == 0);
+}
+
+#else
+# define should_validate_longpaths(repo) (GIT_UNUSED(repo), false)
+#endif
+
+int git_path_validate_workdir(git_repository *repo, const char *path)
+{
+	if (should_validate_longpaths(repo))
+		return git_path_validate_ondisk(path, strlen(path));
+
+	return 0;
+}
+
+int git_path_validate_workdir_with_len(
+	git_repository *repo,
+	const char *path,
+	size_t path_len)
+{
+	if (should_validate_longpaths(repo))
+		return git_path_validate_ondisk(path, path_len);
+
+	return 0;
+}
+
+int git_path_validate_workdir_buf(git_repository *repo, git_buf *path)
+{
+	return git_path_validate_workdir_with_len(repo, path->ptr, path->size);
+}
+
 int git_path_normalize_slashes(git_buf *out, const char *path)
 {
 	int error;
diff --git a/src/path.h b/src/path.h
index 1ff15df..0cf2dbc 100644
--- a/src/path.h
+++ b/src/path.h
@@ -635,6 +635,10 @@ extern int git_path_from_url_or_path(git_buf *local_path_out, const char *url_or
  * This will ensure that a git path does not contain any "unsafe" components,
  * a '.' or '..' component, or a component that is ".git" (in any case).
  *
+ * (Note: if you take or construct an on-disk path -- a workdir path,
+ * a path to a git repository or a reference name that could be a loose
+ * ref -- you should _also_ validate that with `git_path_validate_workdir`.)
+ *
  * `repo` is optional.  If specified, it will be used to determine the short
  * path name to reject (if `GIT_PATH_REJECT_DOS_SHORTNAME` is specified),
  * in addition to the default of "git~1".
@@ -646,6 +650,72 @@ extern bool git_path_validate(
 	unsigned int flags);
 
 /**
+ * Validate an on-disk path, taking into account that it will have a
+ * suffix appended (eg, `.lock`).
+ */
+GIT_INLINE(int) git_path_validate_filesystem_with_suffix(
+	const char *path,
+	size_t path_len,
+	size_t suffix_len)
+{
+#ifdef GIT_WIN32
+	size_t path_chars, total_chars;
+
+	path_chars = git_utf8_char_length(path, path_len);
+
+	if (GIT_ADD_SIZET_OVERFLOW(&total_chars, path_chars, suffix_len) ||
+	    total_chars > MAX_PATH) {
+		git_error_set(GIT_ERROR_FILESYSTEM, "path too long: '%s'", path);
+		return -1;
+	}
+	return 0;
+#else
+	GIT_UNUSED(path);
+	GIT_UNUSED(path_len);
+	GIT_UNUSED(suffix_len);
+	return 0;
+#endif
+}
+
+/**
+ * Validate an path on the filesystem.  This ensures that the given
+ * path is valid for the operating system/platform; for example, this
+ * will ensure that the given absolute path is smaller than MAX_PATH on
+ * Windows.
+ *
+ * For paths within the working directory, you should use ensure that
+ * `core.longpaths` is obeyed.  Use `git_path_validate_workdir`.
+ */
+GIT_INLINE(int) git_path_validate_filesystem(
+	const char *path,
+	size_t path_len)
+{
+	return git_path_validate_filesystem_with_suffix(path, path_len, 0);
+}
+
+/**
+ * Validate a path relative to the repo's worktree.  This ensures that
+ * the given working tree path is valid for the operating system/platform.
+ * This will ensure that an absolute path is smaller than MAX_PATH on
+ * Windows, while keeping `core.longpaths` configuration settings in mind.
+ *
+ * This should be checked by mechamisms like `git_checkout` after
+ * contructing on-disk paths and before trying to write them.
+ *
+ * If the repository is null, no repository configuration is applied.
+ */
+extern int git_path_validate_workdir(
+	git_repository *repo,
+	const char *path);
+extern int git_path_validate_workdir_with_len(
+	git_repository *repo,
+	const char *path,
+	size_t path_len);
+extern int git_path_validate_workdir_buf(
+	git_repository *repo,
+	git_buf *buf);
+
+/**
  * Convert any backslashes into slashes
  */
 int git_path_normalize_slashes(git_buf *out, const char *path);
diff --git a/src/repository.h b/src/repository.h
index c0a2824..4df8500 100644
--- a/src/repository.h
+++ b/src/repository.h
@@ -51,6 +51,7 @@ typedef enum {
 	GIT_CONFIGMAP_PROTECTHFS,       /* core.protectHFS */
 	GIT_CONFIGMAP_PROTECTNTFS,      /* core.protectNTFS */
 	GIT_CONFIGMAP_FSYNCOBJECTFILES, /* core.fsyncObjectFiles */
+	GIT_CONFIGMAP_LONGPATHS,        /* core.longpaths */
 	GIT_CONFIGMAP_CACHE_MAX
 } git_configmap_item;
 
@@ -116,6 +117,8 @@ typedef enum {
 	GIT_PROTECTNTFS_DEFAULT = GIT_CONFIGMAP_TRUE,
 	/* core.fsyncObjectFiles */
 	GIT_FSYNCOBJECTFILES_DEFAULT = GIT_CONFIGMAP_FALSE,
+	/* core.longpaths */
+	GIT_LONGPATHS_DEFAULT = GIT_CONFIGMAP_FALSE,
 } git_configmap_value;
 
 /* internal repository init flags */
diff --git a/tests/path/core.c b/tests/path/core.c
index 8294213..6723216 100644
--- a/tests/path/core.c
+++ b/tests/path/core.c
@@ -1,6 +1,11 @@
 #include "clar_libgit2.h"
 #include "path.h"
 
+void test_path_core__cleanup(void)
+{
+	cl_git_sandbox_cleanup();
+}
+
 static void test_make_relative(
 	const char *expected_path,
 	const char *path,
@@ -306,6 +311,59 @@ void test_path_core__isvalid_dotgit_with_hfs_ignorables(void)
 	cl_assert_equal_b(true, git_path_validate(NULL, ".git\xe2\xab\x81", 0, GIT_PATH_REJECT_DOT_GIT_HFS));
 }
 
+void test_path_core__validate_workdir(void)
+{
+	cl_must_pass(git_path_validate_workdir(NULL, "/foo/bar"));
+	cl_must_pass(git_path_validate_workdir(NULL, "C:\\Foo\\Bar"));
+	cl_must_pass(git_path_validate_workdir(NULL, "\\\\?\\C:\\Foo\\Bar"));
+	cl_must_pass(git_path_validate_workdir(NULL, "\\\\?\\C:\\Foo\\Bar"));
+	cl_must_pass(git_path_validate_workdir(NULL, "\\\\?\\UNC\\server\\C$\\folder"));
+
+#ifdef GIT_WIN32
+	/*
+	 * In the absense of a repo configuration, 259 character paths
+	 * succeed. >= 260 character paths fail.
+	 */
+	cl_must_pass(git_path_validate_workdir(NULL, "C:\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\ok.txt"));
+	cl_must_pass(git_path_validate_workdir(NULL, "C:\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\260.txt"));
+	cl_must_fail(git_path_validate_workdir(NULL, "C:\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\longer_than_260.txt"));
+
+	/* count characters, not bytes */
+	cl_must_pass(git_path_validate_workdir(NULL, "C:\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\\260.txt"));
+	cl_must_fail(git_path_validate_workdir(NULL, "C:\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\\long.txt"));
+#else
+	cl_must_pass(git_path_validate_workdir(NULL, "/c/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/ok.txt"));
+	cl_must_pass(git_path_validate_workdir(NULL, "/c/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/260.txt"));
+	cl_must_pass(git_path_validate_workdir(NULL, "/c/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/longer_than_260.txt"));
+	cl_must_pass(git_path_validate_workdir(NULL, "C:\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\\260.txt"));
+	cl_must_pass(git_path_validate_workdir(NULL, "C:\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\aaaaaaaaa\\\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\xc2\xa2\\long.txt"));
+#endif
+}
+
+void test_path_core__validate_workdir_with_core_longpath(void)
+{
+#ifdef GIT_WIN32
+	git_repository *repo;
+	git_config *config;
+
+	repo = cl_git_sandbox_init("empty_bare.git");
+
+	cl_git_pass(git_repository_open(&repo, "empty_bare.git"));
+	cl_git_pass(git_repository_config(&config, repo));
+
+	/* fail by default */
+	cl_must_fail(git_path_validate_workdir(repo, "/c/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/longer_than_260.txt"));
+
+	/* set core.longpaths explicitly on */
+	cl_git_pass(git_config_set_bool(config, "core.longpaths", 1));
+	cl_must_pass(git_path_validate_workdir(repo, "/c/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/longer_than_260.txt"));
+
+	/* set core.longpaths explicitly off */
+	cl_git_pass(git_config_set_bool(config, "core.longpaths", 0));
+	cl_must_fail(git_path_validate_workdir(repo, "/c/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/aaaaaaaaa/longer_than_260.txt"));
+#endif
+}
+
 static void test_join_unrooted(
 	const char *expected_result,
 	ssize_t expected_rootlen,