summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEdward Thomson <ethomson@edwardthomson.com>2020-04-08 11:20:45 +0100
committerEdward Thomson <ethomson@edwardthomson.com>2020-04-08 11:20:45 +0100
commit6540600bb554d2bd49650174fc2cd45317fee287 (patch)
tree04cb2e84f276f3235a0ee6d1604356ae3ce15615
parent768e19297b502808eeca681dd102e602a2ba7adb (diff)
downloadlibgit2-6540600bb554d2bd49650174fc2cd45317fee287.tar.gz
path: split repository-specific functions
Make path.[ch] contain pure utility functions that do not know about public-facing libgit2 objects, including `git_repository`.
-rw-r--r--src/checkout.c1
-rw-r--r--src/index.c1
-rw-r--r--src/path.c405
-rw-r--r--src/path.h54
-rw-r--r--src/refdb_fs.c1
-rw-r--r--src/repo_path.c415
-rw-r--r--src/repo_path.h66
-rw-r--r--src/submodule.c1
-rw-r--r--src/tree.c1
-rw-r--r--tests/path/core.c5
-rw-r--r--tests/path/dotgit.c1
11 files changed, 490 insertions, 461 deletions
diff --git a/src/checkout.c b/src/checkout.c
index 5cfa7280b..c36c90525 100644
--- a/src/checkout.c
+++ b/src/checkout.c
@@ -20,6 +20,7 @@
#include "refs.h"
#include "repository.h"
+#include "repo_path.h"
#include "index.h"
#include "filter.h"
#include "blob.h"
diff --git a/src/index.c b/src/index.c
index 907bd6d93..c0795af9f 100644
--- a/src/index.c
+++ b/src/index.c
@@ -10,6 +10,7 @@
#include <stddef.h>
#include "repository.h"
+#include "repo_path.h"
#include "tree.h"
#include "tree-cache.h"
#include "hash.h"
diff --git a/src/path.c b/src/path.c
index 625b95c0d..f97cceddd 100644
--- a/src/path.c
+++ b/src/path.c
@@ -1536,373 +1536,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);
}
-/* Reject paths like AUX or COM1, or those versions that end in a dot or
- * colon. ("AUX." or "AUX:")
- */
-GIT_INLINE(bool) verify_dospath(
- const char *component,
- size_t len,
- const char dospath[3],
- bool trailing_num)
-{
- size_t last = trailing_num ? 4 : 3;
-
- if (len < last || git__strncasecmp(component, dospath, 3) != 0)
- return true;
-
- if (trailing_num && (component[3] < '1' || component[3] > '9'))
- return true;
-
- return (len > last &&
- component[last] != '.' &&
- component[last] != ':');
-}
-
-static int32_t next_hfs_char(const char **in, size_t *len)
-{
- while (*len) {
- int32_t codepoint;
- int cp_len = git__utf8_iterate((const uint8_t *)(*in), (int)(*len), &codepoint);
- if (cp_len < 0)
- return -1;
-
- (*in) += cp_len;
- (*len) -= cp_len;
-
- /* these code points are ignored completely */
- switch (codepoint) {
- case 0x200c: /* ZERO WIDTH NON-JOINER */
- case 0x200d: /* ZERO WIDTH JOINER */
- case 0x200e: /* LEFT-TO-RIGHT MARK */
- case 0x200f: /* RIGHT-TO-LEFT MARK */
- case 0x202a: /* LEFT-TO-RIGHT EMBEDDING */
- case 0x202b: /* RIGHT-TO-LEFT EMBEDDING */
- case 0x202c: /* POP DIRECTIONAL FORMATTING */
- case 0x202d: /* LEFT-TO-RIGHT OVERRIDE */
- case 0x202e: /* RIGHT-TO-LEFT OVERRIDE */
- case 0x206a: /* INHIBIT SYMMETRIC SWAPPING */
- case 0x206b: /* ACTIVATE SYMMETRIC SWAPPING */
- case 0x206c: /* INHIBIT ARABIC FORM SHAPING */
- case 0x206d: /* ACTIVATE ARABIC FORM SHAPING */
- case 0x206e: /* NATIONAL DIGIT SHAPES */
- case 0x206f: /* NOMINAL DIGIT SHAPES */
- case 0xfeff: /* ZERO WIDTH NO-BREAK SPACE */
- continue;
- }
-
- /* fold into lowercase -- this will only fold characters in
- * the ASCII range, which is perfectly fine, because the
- * git folder name can only be composed of ascii characters
- */
- return git__tolower(codepoint);
- }
- return 0; /* NULL byte -- end of string */
-}
-
-static bool verify_dotgit_hfs_generic(const char *path, size_t len, const char *needle, size_t needle_len)
-{
- size_t i;
- char c;
-
- if (next_hfs_char(&path, &len) != '.')
- return true;
-
- for (i = 0; i < needle_len; i++) {
- c = next_hfs_char(&path, &len);
- if (c != needle[i])
- return true;
- }
-
- if (next_hfs_char(&path, &len) != '\0')
- return true;
-
- return false;
-}
-
-static bool verify_dotgit_hfs(const char *path, size_t len)
-{
- return verify_dotgit_hfs_generic(path, len, "git", CONST_STRLEN("git"));
-}
-
-GIT_INLINE(bool) verify_dotgit_ntfs(git_repository *repo, const char *path, size_t len)
-{
- git_buf *reserved = git_repository__reserved_names_win32;
- size_t reserved_len = git_repository__reserved_names_win32_len;
- size_t start = 0, i;
-
- if (repo)
- git_repository__reserved_names(&reserved, &reserved_len, repo, true);
-
- for (i = 0; i < reserved_len; i++) {
- git_buf *r = &reserved[i];
-
- if (len >= r->size &&
- strncasecmp(path, r->ptr, r->size) == 0) {
- start = r->size;
- break;
- }
- }
-
- if (!start)
- return true;
-
- /*
- * Reject paths that start with Windows-style directory separators
- * (".git\") or NTFS alternate streams (".git:") and could be used
- * to write to the ".git" directory on Windows platforms.
- */
- if (path[start] == '\\' || path[start] == ':')
- return false;
-
- /* Reject paths like '.git ' or '.git.' */
- for (i = start; i < len; i++) {
- if (path[i] != ' ' && path[i] != '.')
- return true;
- }
-
- return false;
-}
-
-/*
- * Windows paths that end with spaces and/or dots are elided to the
- * path without them for backward compatibility. That is to say
- * that opening file "foo ", "foo." or even "foo . . ." will all
- * map to a filename of "foo". This function identifies spaces and
- * dots at the end of a filename, whether the proper end of the
- * filename (end of string) or a colon (which would indicate a
- * Windows alternate data stream.)
- */
-GIT_INLINE(bool) ntfs_end_of_filename(const char *path)
-{
- const char *c = path;
-
- for (;; c++) {
- if (*c == '\0' || *c == ':')
- return true;
- if (*c != ' ' && *c != '.')
- return false;
- }
-
- return true;
-}
-
-GIT_INLINE(bool) verify_dotgit_ntfs_generic(const char *name, size_t len, const char *dotgit_name, size_t dotgit_len, const char *shortname_pfix)
-{
- int i, saw_tilde;
-
- if (name[0] == '.' && len >= dotgit_len &&
- !strncasecmp(name + 1, dotgit_name, dotgit_len)) {
- return !ntfs_end_of_filename(name + dotgit_len + 1);
- }
-
- /* Detect the basic NTFS shortname with the first six chars */
- if (!strncasecmp(name, dotgit_name, 6) && name[6] == '~' &&
- name[7] >= '1' && name[7] <= '4')
- return !ntfs_end_of_filename(name + 8);
-
- /* Catch fallback names */
- for (i = 0, saw_tilde = 0; i < 8; i++) {
- if (name[i] == '\0') {
- return true;
- } else if (saw_tilde) {
- if (name[i] < '0' || name[i] > '9')
- return true;
- } else if (name[i] == '~') {
- if (name[i+1] < '1' || name[i+1] > '9')
- return true;
- saw_tilde = 1;
- } else if (i >= 6) {
- return true;
- } else if ((unsigned char)name[i] > 127) {
- return true;
- } else if (git__tolower(name[i]) != shortname_pfix[i]) {
- return true;
- }
- }
-
- return !ntfs_end_of_filename(name + i);
-}
-
-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;
-
- switch (c) {
- case '<':
- case '>':
- case ':':
- case '"':
- case '|':
- case '?':
- case '*':
- return false;
- }
- }
-
- return true;
-}
-
-/*
- * Return the length of the common prefix between str and prefix, comparing them
- * case-insensitively (must be ASCII to match).
- */
-GIT_INLINE(size_t) common_prefix_icase(const char *str, size_t len, const char *prefix)
-{
- size_t count = 0;
-
- while (len >0 && tolower(*str) == tolower(*prefix)) {
- count++;
- str++;
- prefix++;
- len--;
- }
-
- return count;
-}
-
-/*
- * We fundamentally don't like some paths when dealing with user-inputted
- * strings (in checkout or ref names): we don't want dot or dot-dot
- * anywhere, we want to avoid writing weird paths on Windows that can't
- * be handled by tools that use the non-\\?\ APIs, we don't want slashes
- * or double slashes at the end of paths that can make them ambiguous.
- *
- * For checkout, we don't want to recurse into ".git" either.
- */
-static bool verify_component(
- git_repository *repo,
- const char *component,
- size_t len,
- uint16_t mode,
- unsigned int flags)
-{
- if (len == 0)
- return false;
-
- if ((flags & GIT_PATH_REJECT_TRAVERSAL) &&
- len == 1 && component[0] == '.')
- return false;
-
- if ((flags & GIT_PATH_REJECT_TRAVERSAL) &&
- len == 2 && component[0] == '.' && component[1] == '.')
- return false;
-
- if ((flags & GIT_PATH_REJECT_TRAILING_DOT) && component[len-1] == '.')
- return false;
-
- if ((flags & GIT_PATH_REJECT_TRAILING_SPACE) && component[len-1] == ' ')
- return false;
-
- if ((flags & GIT_PATH_REJECT_TRAILING_COLON) && component[len-1] == ':')
- return false;
-
- if (flags & GIT_PATH_REJECT_DOS_PATHS) {
- if (!verify_dospath(component, len, "CON", false) ||
- !verify_dospath(component, len, "PRN", false) ||
- !verify_dospath(component, len, "AUX", false) ||
- !verify_dospath(component, len, "NUL", false) ||
- !verify_dospath(component, len, "COM", true) ||
- !verify_dospath(component, len, "LPT", true))
- return false;
- }
-
- if (flags & GIT_PATH_REJECT_DOT_GIT_HFS) {
- if (!verify_dotgit_hfs(component, len))
- return false;
- if (S_ISLNK(mode) && git_path_is_gitfile(component, len, GIT_PATH_GITFILE_GITMODULES, GIT_PATH_FS_HFS))
- return false;
- }
-
- if (flags & GIT_PATH_REJECT_DOT_GIT_NTFS) {
- if (!verify_dotgit_ntfs(repo, component, len))
- return false;
- if (S_ISLNK(mode) && git_path_is_gitfile(component, len, GIT_PATH_GITFILE_GITMODULES, GIT_PATH_FS_NTFS))
- return false;
- }
-
- /* don't bother rerunning the `.git` test if we ran the HFS or NTFS
- * specific tests, they would have already rejected `.git`.
- */
- if ((flags & GIT_PATH_REJECT_DOT_GIT_HFS) == 0 &&
- (flags & GIT_PATH_REJECT_DOT_GIT_NTFS) == 0 &&
- (flags & GIT_PATH_REJECT_DOT_GIT_LITERAL)) {
- if (len >= 4 &&
- component[0] == '.' &&
- (component[1] == 'g' || component[1] == 'G') &&
- (component[2] == 'i' || component[2] == 'I') &&
- (component[3] == 't' || component[3] == 'T')) {
- if (len == 4)
- return false;
-
- if (S_ISLNK(mode) && common_prefix_icase(component, len, ".gitmodules") == len)
- return false;
- }
- }
-
- return true;
-}
-
-GIT_INLINE(unsigned int) dotgit_flags(
- git_repository *repo,
- unsigned int flags)
-{
- int protectHFS = 0, protectNTFS = 1;
- int error = 0;
-
- flags |= GIT_PATH_REJECT_DOT_GIT_LITERAL;
-
-#ifdef __APPLE__
- protectHFS = 1;
-#endif
-
- if (repo && !protectHFS)
- error = git_repository__configmap_lookup(&protectHFS, repo, GIT_CONFIGMAP_PROTECTHFS);
- if (!error && protectHFS)
- flags |= GIT_PATH_REJECT_DOT_GIT_HFS;
-
- if (repo)
- error = git_repository__configmap_lookup(&protectNTFS, repo, GIT_CONFIGMAP_PROTECTNTFS);
- if (!error && protectNTFS)
- flags |= GIT_PATH_REJECT_DOT_GIT_NTFS;
-
- return flags;
-}
-
-bool git_path_isvalid(
- git_repository *repo,
- const char *path,
- uint16_t mode,
- unsigned int flags)
-{
- 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;
-
- if (*c == '/') {
- if (!verify_component(repo, start, (c - start), mode, flags))
- return false;
-
- start = c+1;
- }
- }
-
- return verify_component(repo, start, (c - start), mode, flags);
-}
-
int git_path_normalize_slashes(git_buf *out, const char *path)
{
int error;
@@ -1919,44 +1552,6 @@ int git_path_normalize_slashes(git_buf *out, const char *path)
return 0;
}
-static const struct {
- const char *file;
- const char *hash;
- size_t filelen;
-} gitfiles[] = {
- { "gitignore", "gi250a", CONST_STRLEN("gitignore") },
- { "gitmodules", "gi7eba", CONST_STRLEN("gitmodules") },
- { "gitattributes", "gi7d29", CONST_STRLEN("gitattributes") }
-};
-
-extern int git_path_is_gitfile(const char *path, size_t pathlen, git_path_gitfile gitfile, git_path_fs fs)
-{
- const char *file, *hash;
- size_t filelen;
-
- if (!(gitfile >= GIT_PATH_GITFILE_GITIGNORE && gitfile < ARRAY_SIZE(gitfiles))) {
- git_error_set(GIT_ERROR_OS, "invalid gitfile for path validation");
- return -1;
- }
-
- file = gitfiles[gitfile].file;
- filelen = gitfiles[gitfile].filelen;
- hash = gitfiles[gitfile].hash;
-
- switch (fs) {
- case GIT_PATH_FS_GENERIC:
- return !verify_dotgit_ntfs_generic(path, pathlen, file, filelen, hash) ||
- !verify_dotgit_hfs_generic(path, pathlen, file, filelen);
- case GIT_PATH_FS_NTFS:
- return !verify_dotgit_ntfs_generic(path, pathlen, file, filelen, hash);
- case GIT_PATH_FS_HFS:
- return !verify_dotgit_hfs_generic(path, pathlen, file, filelen);
- default:
- git_error_set(GIT_ERROR_OS, "invalid filesystem for path validation");
- return -1;
- }
-}
-
bool git_path_supports_symlinks(const char *dir)
{
git_buf path = GIT_BUF_INIT;
diff --git a/src/path.h b/src/path.h
index ed6b93574..5e8590dee 100644
--- a/src/path.h
+++ b/src/path.h
@@ -588,60 +588,6 @@ extern int git_path_dirload(
extern bool git_path_is_local_file_url(const char *file_url);
extern int git_path_from_url_or_path(git_buf *local_path_out, const char *url_or_path);
-/* 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_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_LITERAL (1 << 9)
-#define GIT_PATH_REJECT_DOT_GIT_HFS (1 << 10)
-#define GIT_PATH_REJECT_DOT_GIT_NTFS (1 << 11)
-
-/* 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_FILESYSTEM_DEFAULTS \
- GIT_PATH_REJECT_TRAVERSAL | \
- GIT_PATH_REJECT_BACKSLASH | \
- GIT_PATH_REJECT_TRAILING_DOT | \
- GIT_PATH_REJECT_TRAILING_SPACE | \
- GIT_PATH_REJECT_TRAILING_COLON | \
- GIT_PATH_REJECT_DOS_PATHS | \
- GIT_PATH_REJECT_NT_CHARS
-#else
-# define GIT_PATH_REJECT_FILESYSTEM_DEFAULTS \
- GIT_PATH_REJECT_TRAVERSAL
-#endif
-
- /* Paths that should never be written into the working directory. */
-#define GIT_PATH_REJECT_WORKDIR_DEFAULTS \
- GIT_PATH_REJECT_FILESYSTEM_DEFAULTS | GIT_PATH_REJECT_DOT_GIT
-
-/* Paths that should never be written to the index. */
-#define GIT_PATH_REJECT_INDEX_DEFAULTS \
- GIT_PATH_REJECT_TRAVERSAL | GIT_PATH_REJECT_DOT_GIT
-
-/*
- * Determine whether a path is a valid git path or not - this must not contain
- * a '.' or '..' component, or a component that is ".git" (in any case).
- *
- * `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".
- */
-extern bool git_path_isvalid(
- git_repository *repo,
- const char *path,
- uint16_t mode,
- unsigned int flags);
-
/**
* Convert any backslashes into slashes
*/
diff --git a/src/refdb_fs.c b/src/refdb_fs.c
index 653cc1b97..bfa5d7353 100644
--- a/src/refdb_fs.c
+++ b/src/refdb_fs.c
@@ -8,6 +8,7 @@
#include "refs.h"
#include "hash.h"
#include "repository.h"
+#include "repo_path.h"
#include "futils.h"
#include "filebuf.h"
#include "pack.h"
diff --git a/src/repo_path.c b/src/repo_path.c
new file mode 100644
index 000000000..0fd3ddce4
--- /dev/null
+++ b/src/repo_path.c
@@ -0,0 +1,415 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+
+#include "repository.h"
+#include "repo_path.h"
+#include "path.h"
+
+static const struct {
+ const char *file;
+ const char *hash;
+ size_t filelen;
+} gitfiles[] = {
+ { "gitignore", "gi250a", CONST_STRLEN("gitignore") },
+ { "gitmodules", "gi7eba", CONST_STRLEN("gitmodules") },
+ { "gitattributes", "gi7d29", CONST_STRLEN("gitattributes") }
+};
+
+/* Reject paths like AUX or COM1, or those versions that end in a dot or
+ * colon. ("AUX." or "AUX:")
+ */
+GIT_INLINE(bool) verify_dospath(
+ const char *component,
+ size_t len,
+ const char dospath[3],
+ bool trailing_num)
+{
+ size_t last = trailing_num ? 4 : 3;
+
+ if (len < last || git__strncasecmp(component, dospath, 3) != 0)
+ return true;
+
+ if (trailing_num && (component[3] < '1' || component[3] > '9'))
+ return true;
+
+ return (len > last &&
+ component[last] != '.' &&
+ component[last] != ':');
+}
+
+static int32_t next_hfs_char(const char **in, size_t *len)
+{
+ while (*len) {
+ int32_t codepoint;
+ int cp_len = git__utf8_iterate((const uint8_t *)(*in), (int)(*len), &codepoint);
+ if (cp_len < 0)
+ return -1;
+
+ (*in) += cp_len;
+ (*len) -= cp_len;
+
+ /* these code points are ignored completely */
+ switch (codepoint) {
+ case 0x200c: /* ZERO WIDTH NON-JOINER */
+ case 0x200d: /* ZERO WIDTH JOINER */
+ case 0x200e: /* LEFT-TO-RIGHT MARK */
+ case 0x200f: /* RIGHT-TO-LEFT MARK */
+ case 0x202a: /* LEFT-TO-RIGHT EMBEDDING */
+ case 0x202b: /* RIGHT-TO-LEFT EMBEDDING */
+ case 0x202c: /* POP DIRECTIONAL FORMATTING */
+ case 0x202d: /* LEFT-TO-RIGHT OVERRIDE */
+ case 0x202e: /* RIGHT-TO-LEFT OVERRIDE */
+ case 0x206a: /* INHIBIT SYMMETRIC SWAPPING */
+ case 0x206b: /* ACTIVATE SYMMETRIC SWAPPING */
+ case 0x206c: /* INHIBIT ARABIC FORM SHAPING */
+ case 0x206d: /* ACTIVATE ARABIC FORM SHAPING */
+ case 0x206e: /* NATIONAL DIGIT SHAPES */
+ case 0x206f: /* NOMINAL DIGIT SHAPES */
+ case 0xfeff: /* ZERO WIDTH NO-BREAK SPACE */
+ continue;
+ }
+
+ /* fold into lowercase -- this will only fold characters in
+ * the ASCII range, which is perfectly fine, because the
+ * git folder name can only be composed of ascii characters
+ */
+ return git__tolower(codepoint);
+ }
+ return 0; /* NULL byte -- end of string */
+}
+
+static bool verify_dotgit_hfs_generic(const char *path, size_t len, const char *needle, size_t needle_len)
+{
+ size_t i;
+ char c;
+
+ if (next_hfs_char(&path, &len) != '.')
+ return true;
+
+ for (i = 0; i < needle_len; i++) {
+ c = next_hfs_char(&path, &len);
+ if (c != needle[i])
+ return true;
+ }
+
+ if (next_hfs_char(&path, &len) != '\0')
+ return true;
+
+ return false;
+}
+
+static bool verify_dotgit_hfs(const char *path, size_t len)
+{
+ return verify_dotgit_hfs_generic(path, len, "git", CONST_STRLEN("git"));
+}
+
+GIT_INLINE(bool) verify_dotgit_ntfs(git_repository *repo, const char *path, size_t len)
+{
+ git_buf *reserved = git_repository__reserved_names_win32;
+ size_t reserved_len = git_repository__reserved_names_win32_len;
+ size_t start = 0, i;
+
+ if (repo)
+ git_repository__reserved_names(&reserved, &reserved_len, repo, true);
+
+ for (i = 0; i < reserved_len; i++) {
+ git_buf *r = &reserved[i];
+
+ if (len >= r->size &&
+ strncasecmp(path, r->ptr, r->size) == 0) {
+ start = r->size;
+ break;
+ }
+ }
+
+ if (!start)
+ return true;
+
+ /*
+ * Reject paths that start with Windows-style directory separators
+ * (".git\") or NTFS alternate streams (".git:") and could be used
+ * to write to the ".git" directory on Windows platforms.
+ */
+ if (path[start] == '\\' || path[start] == ':')
+ return false;
+
+ /* Reject paths like '.git ' or '.git.' */
+ for (i = start; i < len; i++) {
+ if (path[i] != ' ' && path[i] != '.')
+ return true;
+ }
+
+ return false;
+}
+
+/*
+ * Windows paths that end with spaces and/or dots are elided to the
+ * path without them for backward compatibility. That is to say
+ * that opening file "foo ", "foo." or even "foo . . ." will all
+ * map to a filename of "foo". This function identifies spaces and
+ * dots at the end of a filename, whether the proper end of the
+ * filename (end of string) or a colon (which would indicate a
+ * Windows alternate data stream.)
+ */
+GIT_INLINE(bool) ntfs_end_of_filename(const char *path)
+{
+ const char *c = path;
+
+ for (;; c++) {
+ if (*c == '\0' || *c == ':')
+ return true;
+ if (*c != ' ' && *c != '.')
+ return false;
+ }
+
+ return true;
+}
+
+GIT_INLINE(bool) verify_dotgit_ntfs_generic(const char *name, size_t len, const char *dotgit_name, size_t dotgit_len, const char *shortname_pfix)
+{
+ int i, saw_tilde;
+
+ if (name[0] == '.' && len >= dotgit_len &&
+ !strncasecmp(name + 1, dotgit_name, dotgit_len)) {
+ return !ntfs_end_of_filename(name + dotgit_len + 1);
+ }
+
+ /* Detect the basic NTFS shortname with the first six chars */
+ if (!strncasecmp(name, dotgit_name, 6) && name[6] == '~' &&
+ name[7] >= '1' && name[7] <= '4')
+ return !ntfs_end_of_filename(name + 8);
+
+ /* Catch fallback names */
+ for (i = 0, saw_tilde = 0; i < 8; i++) {
+ if (name[i] == '\0') {
+ return true;
+ } else if (saw_tilde) {
+ if (name[i] < '0' || name[i] > '9')
+ return true;
+ } else if (name[i] == '~') {
+ if (name[i+1] < '1' || name[i+1] > '9')
+ return true;
+ saw_tilde = 1;
+ } else if (i >= 6) {
+ return true;
+ } else if ((unsigned char)name[i] > 127) {
+ return true;
+ } else if (git__tolower(name[i]) != shortname_pfix[i]) {
+ return true;
+ }
+ }
+
+ return !ntfs_end_of_filename(name + i);
+}
+
+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;
+
+ switch (c) {
+ case '<':
+ case '>':
+ case ':':
+ case '"':
+ case '|':
+ case '?':
+ case '*':
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/*
+ * Return the length of the common prefix between str and prefix, comparing them
+ * case-insensitively (must be ASCII to match).
+ */
+GIT_INLINE(size_t) common_prefix_icase(const char *str, size_t len, const char *prefix)
+{
+ size_t count = 0;
+
+ while (len >0 && tolower(*str) == tolower(*prefix)) {
+ count++;
+ str++;
+ prefix++;
+ len--;
+ }
+
+ return count;
+}
+
+int git_path_is_gitfile(const char *path, size_t pathlen, git_path_gitfile gitfile, git_path_fs fs)
+{
+ const char *file, *hash;
+ size_t filelen;
+
+ if (!(gitfile >= GIT_PATH_GITFILE_GITIGNORE && gitfile < ARRAY_SIZE(gitfiles))) {
+ git_error_set(GIT_ERROR_OS, "invalid gitfile for path validation");
+ return -1;
+ }
+
+ file = gitfiles[gitfile].file;
+ filelen = gitfiles[gitfile].filelen;
+ hash = gitfiles[gitfile].hash;
+
+ switch (fs) {
+ case GIT_PATH_FS_GENERIC:
+ return !verify_dotgit_ntfs_generic(path, pathlen, file, filelen, hash) ||
+ !verify_dotgit_hfs_generic(path, pathlen, file, filelen);
+ case GIT_PATH_FS_NTFS:
+ return !verify_dotgit_ntfs_generic(path, pathlen, file, filelen, hash);
+ case GIT_PATH_FS_HFS:
+ return !verify_dotgit_hfs_generic(path, pathlen, file, filelen);
+ default:
+ git_error_set(GIT_ERROR_OS, "invalid filesystem for path validation");
+ return -1;
+ }
+}
+
+/*
+ * We fundamentally don't like some paths when dealing with user-inputted
+ * strings (in checkout or ref names): we don't want dot or dot-dot
+ * anywhere, we want to avoid writing weird paths on Windows that can't
+ * be handled by tools that use the non-\\?\ APIs, we don't want slashes
+ * or double slashes at the end of paths that can make them ambiguous.
+ *
+ * For checkout, we don't want to recurse into ".git" either.
+ */
+static bool verify_component(
+ git_repository *repo,
+ const char *component,
+ size_t len,
+ uint16_t mode,
+ unsigned int flags)
+{
+ if (len == 0)
+ return false;
+
+ if ((flags & GIT_PATH_REJECT_TRAVERSAL) &&
+ len == 1 && component[0] == '.')
+ return false;
+
+ if ((flags & GIT_PATH_REJECT_TRAVERSAL) &&
+ len == 2 && component[0] == '.' && component[1] == '.')
+ return false;
+
+ if ((flags & GIT_PATH_REJECT_TRAILING_DOT) && component[len-1] == '.')
+ return false;
+
+ if ((flags & GIT_PATH_REJECT_TRAILING_SPACE) && component[len-1] == ' ')
+ return false;
+
+ if ((flags & GIT_PATH_REJECT_TRAILING_COLON) && component[len-1] == ':')
+ return false;
+
+ if (flags & GIT_PATH_REJECT_DOS_PATHS) {
+ if (!verify_dospath(component, len, "CON", false) ||
+ !verify_dospath(component, len, "PRN", false) ||
+ !verify_dospath(component, len, "AUX", false) ||
+ !verify_dospath(component, len, "NUL", false) ||
+ !verify_dospath(component, len, "COM", true) ||
+ !verify_dospath(component, len, "LPT", true))
+ return false;
+ }
+
+ if (flags & GIT_PATH_REJECT_DOT_GIT_HFS) {
+ if (!verify_dotgit_hfs(component, len))
+ return false;
+ if (S_ISLNK(mode) && git_path_is_gitfile(component, len, GIT_PATH_GITFILE_GITMODULES, GIT_PATH_FS_HFS))
+ return false;
+ }
+
+ if (flags & GIT_PATH_REJECT_DOT_GIT_NTFS) {
+ if (!verify_dotgit_ntfs(repo, component, len))
+ return false;
+ if (S_ISLNK(mode) && git_path_is_gitfile(component, len, GIT_PATH_GITFILE_GITMODULES, GIT_PATH_FS_NTFS))
+ return false;
+ }
+
+ /* don't bother rerunning the `.git` test if we ran the HFS or NTFS
+ * specific tests, they would have already rejected `.git`.
+ */
+ if ((flags & GIT_PATH_REJECT_DOT_GIT_HFS) == 0 &&
+ (flags & GIT_PATH_REJECT_DOT_GIT_NTFS) == 0 &&
+ (flags & GIT_PATH_REJECT_DOT_GIT_LITERAL)) {
+ if (len >= 4 &&
+ component[0] == '.' &&
+ (component[1] == 'g' || component[1] == 'G') &&
+ (component[2] == 'i' || component[2] == 'I') &&
+ (component[3] == 't' || component[3] == 'T')) {
+ if (len == 4)
+ return false;
+
+ if (S_ISLNK(mode) && common_prefix_icase(component, len, ".gitmodules") == len)
+ return false;
+ }
+ }
+
+ return true;
+}
+
+GIT_INLINE(unsigned int) dotgit_flags(
+ git_repository *repo,
+ unsigned int flags)
+{
+ int protectHFS = 0, protectNTFS = 1;
+ int error = 0;
+
+ flags |= GIT_PATH_REJECT_DOT_GIT_LITERAL;
+
+#ifdef __APPLE__
+ protectHFS = 1;
+#endif
+
+ if (repo && !protectHFS)
+ error = git_repository__configmap_lookup(&protectHFS, repo, GIT_CONFIGMAP_PROTECTHFS);
+ if (!error && protectHFS)
+ flags |= GIT_PATH_REJECT_DOT_GIT_HFS;
+
+ if (repo)
+ error = git_repository__configmap_lookup(&protectNTFS, repo, GIT_CONFIGMAP_PROTECTNTFS);
+ if (!error && protectNTFS)
+ flags |= GIT_PATH_REJECT_DOT_GIT_NTFS;
+
+ return flags;
+}
+
+bool git_path_isvalid(
+ git_repository *repo,
+ const char *path,
+ uint16_t mode,
+ unsigned int flags)
+{
+ 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;
+
+ if (*c == '/') {
+ if (!verify_component(repo, start, (c - start), mode, flags))
+ return false;
+
+ start = c+1;
+ }
+ }
+
+ return verify_component(repo, start, (c - start), mode, flags);
+}
diff --git a/src/repo_path.h b/src/repo_path.h
new file mode 100644
index 000000000..f539fdc2f
--- /dev/null
+++ b/src/repo_path.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+#ifndef INCLUDE_repo_path_h__
+#define INCLUDE_repo_path_h__
+
+#include "common.h"
+
+/* 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_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_LITERAL (1 << 9)
+#define GIT_PATH_REJECT_DOT_GIT_HFS (1 << 10)
+#define GIT_PATH_REJECT_DOT_GIT_NTFS (1 << 11)
+
+/* 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_FILESYSTEM_DEFAULTS \
+ GIT_PATH_REJECT_TRAVERSAL | \
+ GIT_PATH_REJECT_BACKSLASH | \
+ GIT_PATH_REJECT_TRAILING_DOT | \
+ GIT_PATH_REJECT_TRAILING_SPACE | \
+ GIT_PATH_REJECT_TRAILING_COLON | \
+ GIT_PATH_REJECT_DOS_PATHS | \
+ GIT_PATH_REJECT_NT_CHARS
+#else
+# define GIT_PATH_REJECT_FILESYSTEM_DEFAULTS \
+ GIT_PATH_REJECT_TRAVERSAL
+#endif
+
+ /* Paths that should never be written into the working directory. */
+#define GIT_PATH_REJECT_WORKDIR_DEFAULTS \
+ GIT_PATH_REJECT_FILESYSTEM_DEFAULTS | GIT_PATH_REJECT_DOT_GIT
+
+/* Paths that should never be written to the index. */
+#define GIT_PATH_REJECT_INDEX_DEFAULTS \
+ GIT_PATH_REJECT_TRAVERSAL | GIT_PATH_REJECT_DOT_GIT
+
+/*
+ * Determine whether a path is a valid git path or not - this must not contain
+ * a '.' or '..' component, or a component that is ".git" (in any case).
+ *
+ * `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".
+ */
+extern bool git_path_isvalid(
+ git_repository *repo,
+ const char *path,
+ uint16_t mode,
+ unsigned int flags);
+
+#endif
diff --git a/src/submodule.c b/src/submodule.c
index 1690e08f8..123f058ba 100644
--- a/src/submodule.c
+++ b/src/submodule.c
@@ -18,6 +18,7 @@
#include "config_backend.h"
#include "config.h"
#include "repository.h"
+#include "repo_path.h"
#include "tree.h"
#include "iterator.h"
#include "path.h"
diff --git a/src/tree.c b/src/tree.c
index 48468dff6..66e94b02e 100644
--- a/src/tree.c
+++ b/src/tree.c
@@ -13,6 +13,7 @@
#include "futils.h"
#include "tree-cache.h"
#include "index.h"
+#include "repo_path.h"
#define DEFAULT_TREE_SIZE 16
#define MAX_FILEMODE_BYTES 6
diff --git a/tests/path/core.c b/tests/path/core.c
index 48b518c8d..ebf604792 100644
--- a/tests/path/core.c
+++ b/tests/path/core.c
@@ -1,5 +1,6 @@
#include "clar_libgit2.h"
#include "path.h"
+#include "repo_path.h"
static void test_make_relative(
const char *expected_path,
@@ -44,10 +45,10 @@ void test_path_core__make_relative(void)
test_make_relative("/path/to/foo.c", "/path/to/foo.c", "d:/path/to", GIT_ENOTFOUND);
test_make_relative("d:/path/to/foo.c", "d:/path/to/foo.c", "/path/to", GIT_ENOTFOUND);
-
+
test_make_relative("/path/to/foo.c", "/path/to/foo.c", "not-a-rooted-path", GIT_ENOTFOUND);
test_make_relative("not-a-rooted-path", "not-a-rooted-path", "/path/to", GIT_ENOTFOUND);
-
+
test_make_relative("/path", "/path", "pathtofoo", GIT_ENOTFOUND);
test_make_relative("path", "path", "pathtofoo", GIT_ENOTFOUND);
}
diff --git a/tests/path/dotgit.c b/tests/path/dotgit.c
index ceb7330d2..d0e99290c 100644
--- a/tests/path/dotgit.c
+++ b/tests/path/dotgit.c
@@ -1,5 +1,6 @@
#include "clar_libgit2.h"
#include "path.h"
+#include "repo_path.h"
static char *gitmodules_altnames[] = {
".gitmodules",