/* * 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" 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; } }