diff options
Diffstat (limited to 'src/libgit2/path.c')
-rw-r--r-- | src/libgit2/path.c | 375 |
1 files changed, 375 insertions, 0 deletions
diff --git a/src/libgit2/path.c b/src/libgit2/path.c new file mode 100644 index 000000000..a19340efe --- /dev/null +++ b/src/libgit2/path.c @@ -0,0 +1,375 @@ +/* + * 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 "path.h" + +#include "repository.h" +#include "fs_path.h" +#include "utf8.h" + +typedef struct { + git_repository *repo; + uint16_t file_mode; + unsigned int flags; +} repository_path_validate_data; + +static int32_t next_hfs_char(const char **in, size_t *len) +{ + while (*len) { + uint32_t codepoint; + int cp_len = git_utf8_iterate(&codepoint, *in, *len); + 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((int)codepoint); + } + return 0; /* NULL byte -- end of string */ +} + +static bool validate_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 validate_dotgit_hfs(const char *path, size_t len) +{ + return validate_dotgit_hfs_generic(path, len, "git", CONST_STRLEN("git")); +} + +GIT_INLINE(bool) validate_dotgit_ntfs( + git_repository *repo, + const char *path, + size_t len) +{ + git_str *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_str *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) validate_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); +} + +/* + * 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; +} + +static bool validate_repo_component( + const char *component, + size_t len, + void *payload) +{ + repository_path_validate_data *data = (repository_path_validate_data *)payload; + + if (data->flags & GIT_PATH_REJECT_DOT_GIT_HFS) { + if (!validate_dotgit_hfs(component, len)) + return false; + + if (S_ISLNK(data->file_mode) && + git_path_is_gitfile(component, len, GIT_PATH_GITFILE_GITMODULES, GIT_PATH_FS_HFS)) + return false; + } + + if (data->flags & GIT_PATH_REJECT_DOT_GIT_NTFS) { + if (!validate_dotgit_ntfs(data->repo, component, len)) + return false; + + if (S_ISLNK(data->file_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 ((data->flags & GIT_PATH_REJECT_DOT_GIT_HFS) == 0 && + (data->flags & GIT_PATH_REJECT_DOT_GIT_NTFS) == 0 && + (data->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(data->file_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; +} + +GIT_INLINE(unsigned int) length_flags( + git_repository *repo, + unsigned int flags) +{ +#ifdef GIT_WIN32 + int allow = 0; + + if (repo && + git_repository__configmap_lookup(&allow, repo, GIT_CONFIGMAP_LONGPATHS) < 0) + allow = 0; + + if (allow) + flags &= ~GIT_FS_PATH_REJECT_LONG_PATHS; + +#else + GIT_UNUSED(repo); + flags &= ~GIT_FS_PATH_REJECT_LONG_PATHS; +#endif + + return flags; +} + +bool git_path_str_is_valid( + git_repository *repo, + const git_str *path, + uint16_t file_mode, + unsigned int flags) +{ + repository_path_validate_data data = {0}; + + /* Upgrade the ".git" checks based on platform */ + if ((flags & GIT_PATH_REJECT_DOT_GIT)) + flags = dotgit_flags(repo, flags); + + /* Update the length checks based on platform */ + if ((flags & GIT_FS_PATH_REJECT_LONG_PATHS)) + flags = length_flags(repo, flags); + + data.repo = repo; + data.file_mode = file_mode; + data.flags = flags; + + return git_fs_path_str_is_valid_ext(path, flags, NULL, validate_repo_component, NULL, &data); +} + +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 !validate_dotgit_ntfs_generic(path, pathlen, file, filelen, hash) || + !validate_dotgit_hfs_generic(path, pathlen, file, filelen); + case GIT_PATH_FS_NTFS: + return !validate_dotgit_ntfs_generic(path, pathlen, file, filelen, hash); + case GIT_PATH_FS_HFS: + return !validate_dotgit_hfs_generic(path, pathlen, file, filelen); + default: + git_error_set(GIT_ERROR_OS, "invalid filesystem for path validation"); + return -1; + } +} + |