diff options
author | Edward Thomson <ethomson@edwardthomson.com> | 2020-04-08 11:20:45 +0100 |
---|---|---|
committer | Edward Thomson <ethomson@edwardthomson.com> | 2020-04-08 11:20:45 +0100 |
commit | 6540600bb554d2bd49650174fc2cd45317fee287 (patch) | |
tree | 04cb2e84f276f3235a0ee6d1604356ae3ce15615 | |
parent | 768e19297b502808eeca681dd102e602a2ba7adb (diff) | |
download | libgit2-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.c | 1 | ||||
-rw-r--r-- | src/index.c | 1 | ||||
-rw-r--r-- | src/path.c | 405 | ||||
-rw-r--r-- | src/path.h | 54 | ||||
-rw-r--r-- | src/refdb_fs.c | 1 | ||||
-rw-r--r-- | src/repo_path.c | 415 | ||||
-rw-r--r-- | src/repo_path.h | 66 | ||||
-rw-r--r-- | src/submodule.c | 1 | ||||
-rw-r--r-- | src/tree.c | 1 | ||||
-rw-r--r-- | tests/path/core.c | 5 | ||||
-rw-r--r-- | tests/path/dotgit.c | 1 |
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", |