/* * 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_w32.h" #include "fs_path.h" #include "utf-conv.h" #include "posix.h" #include "reparse.h" #include "dir.h" #define PATH__NT_NAMESPACE L"\\\\?\\" #define PATH__NT_NAMESPACE_LEN 4 #define PATH__ABSOLUTE_LEN 3 #define path__is_nt_namespace(p) \ (((p)[0] == '\\' && (p)[1] == '\\' && (p)[2] == '?' && (p)[3] == '\\') || \ ((p)[0] == '/' && (p)[1] == '/' && (p)[2] == '?' && (p)[3] == '/')) #define path__is_unc(p) \ (((p)[0] == '\\' && (p)[1] == '\\') || ((p)[0] == '/' && (p)[1] == '/')) #define path__startswith_slash(p) \ ((p)[0] == '\\' || (p)[0] == '/') GIT_INLINE(int) path__cwd(wchar_t *path, int size) { int len; if ((len = GetCurrentDirectoryW(size, path)) == 0) { errno = GetLastError() == ERROR_ACCESS_DENIED ? EACCES : ENOENT; return -1; } else if (len > size) { errno = ENAMETOOLONG; return -1; } /* The Win32 APIs may return "\\?\" once you've used it first. * But it may not. What a gloriously predictable API! */ if (wcsncmp(path, PATH__NT_NAMESPACE, PATH__NT_NAMESPACE_LEN)) return len; len -= PATH__NT_NAMESPACE_LEN; memmove(path, path + PATH__NT_NAMESPACE_LEN, sizeof(wchar_t) * len); return len; } static wchar_t *path__skip_server(wchar_t *path) { wchar_t *c; for (c = path; *c; c++) { if (git_fs_path_is_dirsep(*c)) return c + 1; } return c; } static wchar_t *path__skip_prefix(wchar_t *path) { if (path__is_nt_namespace(path)) { path += PATH__NT_NAMESPACE_LEN; if (wcsncmp(path, L"UNC\\", 4) == 0) path = path__skip_server(path + 4); else if (git_fs_path_is_absolute(path)) path += PATH__ABSOLUTE_LEN; } else if (git_fs_path_is_absolute(path)) { path += PATH__ABSOLUTE_LEN; } else if (path__is_unc(path)) { path = path__skip_server(path + 2); } return path; } int git_win32_path_canonicalize(git_win32_path path) { wchar_t *base, *from, *to, *next; size_t len; base = to = path__skip_prefix(path); /* Unposixify if the prefix */ for (from = path; from < to; from++) { if (*from == L'/') *from = L'\\'; } while (*from) { for (next = from; *next; ++next) { if (*next == L'/') { *next = L'\\'; break; } if (*next == L'\\') break; } len = next - from; if (len == 1 && from[0] == L'.') /* do nothing with singleton dot */; else if (len == 2 && from[0] == L'.' && from[1] == L'.') { if (to == base) { /* no more path segments to strip, eat the "../" */ if (*next == L'\\') len++; base = to; } else { /* back up a path segment */ while (to > base && to[-1] == L'\\') to--; while (to > base && to[-1] != L'\\') to--; } } else { if (*next == L'\\' && *from != L'\\') len++; if (to != from) memmove(to, from, sizeof(wchar_t) * len); to += len; } from += len; while (*from == L'\\') from++; } /* Strip trailing backslashes */ while (to > base && to[-1] == L'\\') to--; *to = L'\0'; if ((to - path) > INT_MAX) { SetLastError(ERROR_FILENAME_EXCED_RANGE); return -1; } return (int)(to - path); } static int git_win32_path_join( git_win32_path dest, const wchar_t *one, size_t one_len, const wchar_t *two, size_t two_len) { size_t backslash = 0; if (one_len && two_len && one[one_len - 1] != L'\\') backslash = 1; if (one_len + two_len + backslash > MAX_PATH) { git_error_set(GIT_ERROR_INVALID, "path too long"); return -1; } memmove(dest, one, one_len * sizeof(wchar_t)); if (backslash) dest[one_len] = L'\\'; memcpy(dest + one_len + backslash, two, two_len * sizeof(wchar_t)); dest[one_len + backslash + two_len] = L'\0'; return 0; } struct win32_path_iter { wchar_t *env; const wchar_t *current_dir; }; static int win32_path_iter_init(struct win32_path_iter *iter) { DWORD len = GetEnvironmentVariableW(L"PATH", NULL, 0); if (!len && GetLastError() == ERROR_ENVVAR_NOT_FOUND) { iter->env = NULL; iter->current_dir = NULL; return 0; } else if (!len) { git_error_set(GIT_ERROR_OS, "could not load PATH"); return -1; } iter->env = git__malloc(len * sizeof(wchar_t)); GIT_ERROR_CHECK_ALLOC(iter->env); len = GetEnvironmentVariableW(L"PATH", iter->env, len); if (len == 0) { git_error_set(GIT_ERROR_OS, "could not load PATH"); return -1; } iter->current_dir = iter->env; return 0; } static int win32_path_iter_next( const wchar_t **out, size_t *out_len, struct win32_path_iter *iter) { const wchar_t *start; wchar_t term; size_t len = 0; if (!iter->current_dir || !*iter->current_dir) return GIT_ITEROVER; term = (*iter->current_dir == L'"') ? *iter->current_dir++ : L';'; start = iter->current_dir; while (*iter->current_dir && *iter->current_dir != term) { iter->current_dir++; len++; } *out = start; *out_len = len; if (term == L'"' && *iter->current_dir) iter->current_dir++; while (*iter->current_dir == L';') iter->current_dir++; return 0; } static void win32_path_iter_dispose(struct win32_path_iter *iter) { if (!iter) return; git__free(iter->env); iter->env = NULL; iter->current_dir = NULL; } int git_win32_path_find_executable(git_win32_path fullpath, wchar_t *exe) { struct win32_path_iter path_iter; const wchar_t *dir; size_t dir_len, exe_len = wcslen(exe); bool found = false; if (win32_path_iter_init(&path_iter) < 0) return -1; while (win32_path_iter_next(&dir, &dir_len, &path_iter) != GIT_ITEROVER) { if (git_win32_path_join(fullpath, dir, dir_len, exe, exe_len) < 0) continue; if (_waccess(fullpath, 0) == 0) { found = true; break; } } win32_path_iter_dispose(&path_iter); if (found) return 0; fullpath[0] = L'\0'; return GIT_ENOTFOUND; } static int win32_path_cwd(wchar_t *out, size_t len) { int cwd_len; if (len > INT_MAX) { errno = ENAMETOOLONG; return -1; } if ((cwd_len = path__cwd(out, (int)len)) < 0) return -1; /* UNC paths */ if (wcsncmp(L"\\\\", out, 2) == 0) { /* Our buffer must be at least 5 characters larger than the * current working directory: we swallow one of the leading * '\'s, but we we add a 'UNC' specifier to the path, plus * a trailing directory separator, plus a NUL. */ if (cwd_len > GIT_WIN_PATH_MAX - 4) { errno = ENAMETOOLONG; return -1; } memmove(out+2, out, sizeof(wchar_t) * cwd_len); out[0] = L'U'; out[1] = L'N'; out[2] = L'C'; cwd_len += 2; } /* Our buffer must be at least 2 characters larger than the current * working directory. (One character for the directory separator, * one for the null. */ else if (cwd_len > GIT_WIN_PATH_MAX - 2) { errno = ENAMETOOLONG; return -1; } return cwd_len; } int git_win32_path_from_utf8(git_win32_path out, const char *src) { wchar_t *dest = out; /* All win32 paths are in NT-prefixed format, beginning with "\\?\". */ memcpy(dest, PATH__NT_NAMESPACE, sizeof(wchar_t) * PATH__NT_NAMESPACE_LEN); dest += PATH__NT_NAMESPACE_LEN; /* See if this is an absolute path (beginning with a drive letter) */ if (git_fs_path_is_absolute(src)) { if (git__utf8_to_16(dest, GIT_WIN_PATH_MAX, src) < 0) goto on_error; } /* File-prefixed NT-style paths beginning with \\?\ */ else if (path__is_nt_namespace(src)) { /* Skip the NT prefix, the destination already contains it */ if (git__utf8_to_16(dest, GIT_WIN_PATH_MAX, src + PATH__NT_NAMESPACE_LEN) < 0) goto on_error; } /* UNC paths */ else if (path__is_unc(src)) { memcpy(dest, L"UNC\\", sizeof(wchar_t) * 4); dest += 4; /* Skip the leading "\\" */ if (git__utf8_to_16(dest, GIT_WIN_PATH_MAX - 2, src + 2) < 0) goto on_error; } /* Absolute paths omitting the drive letter */ else if (path__startswith_slash(src)) { if (path__cwd(dest, GIT_WIN_PATH_MAX) < 0) goto on_error; if (!git_fs_path_is_absolute(dest)) { errno = ENOENT; goto on_error; } /* Skip the drive letter specification ("C:") */ if (git__utf8_to_16(dest + 2, GIT_WIN_PATH_MAX - 2, src) < 0) goto on_error; } /* Relative paths */ else { int cwd_len; if ((cwd_len = win32_path_cwd(dest, GIT_WIN_PATH_MAX)) < 0) goto on_error; dest[cwd_len++] = L'\\'; if (git__utf8_to_16(dest + cwd_len, GIT_WIN_PATH_MAX - cwd_len, src) < 0) goto on_error; } return git_win32_path_canonicalize(out); on_error: /* set windows error code so we can use its error message */ if (errno == ENAMETOOLONG) SetLastError(ERROR_FILENAME_EXCED_RANGE); return -1; } int git_win32_path_relative_from_utf8(git_win32_path out, const char *src) { wchar_t *dest = out, *p; int len; /* Handle absolute paths */ if (git_fs_path_is_absolute(src) || path__is_nt_namespace(src) || path__is_unc(src) || path__startswith_slash(src)) { return git_win32_path_from_utf8(out, src); } if ((len = git__utf8_to_16(dest, GIT_WIN_PATH_MAX, src)) < 0) return -1; for (p = dest; p < (dest + len); p++) { if (*p == L'/') *p = L'\\'; } return len; } int git_win32_path_to_utf8(git_win32_utf8_path dest, const wchar_t *src) { char *out = dest; int len; /* Strip NT namespacing "\\?\" */ if (path__is_nt_namespace(src)) { src += 4; /* "\\?\UNC\server\share" -> "\\server\share" */ if (wcsncmp(src, L"UNC\\", 4) == 0) { src += 4; memcpy(dest, "\\\\", 2); out = dest + 2; } } if ((len = git__utf16_to_8(out, GIT_WIN_PATH_UTF8, src)) < 0) return len; git_fs_path_mkposix(dest); return len; } char *git_win32_path_8dot3_name(const char *path) { git_win32_path longpath, shortpath; wchar_t *start; char *shortname; int len, namelen = 1; if (git_win32_path_from_utf8(longpath, path) < 0) return NULL; len = GetShortPathNameW(longpath, shortpath, GIT_WIN_PATH_UTF16); while (len && shortpath[len-1] == L'\\') shortpath[--len] = L'\0'; if (len == 0 || len >= GIT_WIN_PATH_UTF16) return NULL; for (start = shortpath + (len - 1); start > shortpath && *(start-1) != '/' && *(start-1) != '\\'; start--) namelen++; /* We may not have actually been given a short name. But if we have, * it will be in the ASCII byte range, so we don't need to worry about * multi-byte sequences and can allocate naively. */ if (namelen > 12 || (shortname = git__malloc(namelen + 1)) == NULL) return NULL; if ((len = git__utf16_to_8(shortname, namelen + 1, start)) < 0) return NULL; return shortname; } static bool path_is_volume(wchar_t *target, size_t target_len) { return (target_len && wcsncmp(target, L"\\??\\Volume{", 11) == 0); } /* On success, returns the length, in characters, of the path stored in dest. * On failure, returns a negative value. */ int git_win32_path_readlink_w(git_win32_path dest, const git_win32_path path) { BYTE buf[MAXIMUM_REPARSE_DATA_BUFFER_SIZE]; GIT_REPARSE_DATA_BUFFER *reparse_buf = (GIT_REPARSE_DATA_BUFFER *)buf; HANDLE handle = NULL; DWORD ioctl_ret; wchar_t *target; size_t target_len; int error = -1; handle = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, NULL); if (handle == INVALID_HANDLE_VALUE) { errno = ENOENT; return -1; } if (!DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, NULL, 0, reparse_buf, sizeof(buf), &ioctl_ret, NULL)) { errno = EINVAL; goto on_error; } switch (reparse_buf->ReparseTag) { case IO_REPARSE_TAG_SYMLINK: target = reparse_buf->ReparseBuffer.SymbolicLink.PathBuffer + (reparse_buf->ReparseBuffer.SymbolicLink.SubstituteNameOffset / sizeof(WCHAR)); target_len = reparse_buf->ReparseBuffer.SymbolicLink.SubstituteNameLength / sizeof(WCHAR); break; case IO_REPARSE_TAG_MOUNT_POINT: target = reparse_buf->ReparseBuffer.MountPoint.PathBuffer + (reparse_buf->ReparseBuffer.MountPoint.SubstituteNameOffset / sizeof(WCHAR)); target_len = reparse_buf->ReparseBuffer.MountPoint.SubstituteNameLength / sizeof(WCHAR); break; default: errno = EINVAL; goto on_error; } if (path_is_volume(target, target_len)) { /* This path is a reparse point that represents another volume mounted * at this location, it is not a symbolic link our input was canonical. */ errno = EINVAL; error = -1; } else if (target_len) { /* The path may need to have a namespace prefix removed. */ target_len = git_win32_path_remove_namespace(target, target_len); /* Need one additional character in the target buffer * for the terminating NULL. */ if (GIT_WIN_PATH_UTF16 > target_len) { wcscpy(dest, target); error = (int)target_len; } } on_error: CloseHandle(handle); return error; } /** * Removes any trailing backslashes from a path, except in the case of a drive * letter path (C:\, D:\, etc.). This function cannot fail. * * @param path The path which should be trimmed. * @return The length of the modified string (<= the input length) */ size_t git_win32_path_trim_end(wchar_t *str, size_t len) { while (1) { if (!len || str[len - 1] != L'\\') break; /* * Don't trim backslashes from drive letter paths, which * are 3 characters long and of the form C:\, D:\, etc. */ if (len == 3 && git_win32__isalpha(str[0]) && str[1] == ':') break; len--; } str[len] = L'\0'; return len; } /** * Removes any of the following namespace prefixes from a path, * if found: "\??\", "\\?\", "\\?\UNC\". This function cannot fail. * * @param path The path which should be converted. * @return The length of the modified string (<= the input length) */ size_t git_win32_path_remove_namespace(wchar_t *str, size_t len) { static const wchar_t dosdevices_namespace[] = L"\\\?\?\\"; static const wchar_t nt_namespace[] = L"\\\\?\\"; static const wchar_t unc_namespace_remainder[] = L"UNC\\"; static const wchar_t unc_prefix[] = L"\\\\"; const wchar_t *prefix = NULL, *remainder = NULL; size_t prefix_len = 0, remainder_len = 0; /* "\??\" -- DOS Devices prefix */ if (len >= CONST_STRLEN(dosdevices_namespace) && !wcsncmp(str, dosdevices_namespace, CONST_STRLEN(dosdevices_namespace))) { remainder = str + CONST_STRLEN(dosdevices_namespace); remainder_len = len - CONST_STRLEN(dosdevices_namespace); } /* "\\?\" -- NT namespace prefix */ else if (len >= CONST_STRLEN(nt_namespace) && !wcsncmp(str, nt_namespace, CONST_STRLEN(nt_namespace))) { remainder = str + CONST_STRLEN(nt_namespace); remainder_len = len - CONST_STRLEN(nt_namespace); } /* "\??\UNC\", "\\?\UNC\" -- UNC prefix */ if (remainder_len >= CONST_STRLEN(unc_namespace_remainder) && !wcsncmp(remainder, unc_namespace_remainder, CONST_STRLEN(unc_namespace_remainder))) { /* * The proper Win32 path for a UNC share has "\\" at beginning of it * and looks like "\\server\share\". So remove the * UNC namespace and add a prefix of "\\" in its place. */ remainder += CONST_STRLEN(unc_namespace_remainder); remainder_len -= CONST_STRLEN(unc_namespace_remainder); prefix = unc_prefix; prefix_len = CONST_STRLEN(unc_prefix); } /* * Sanity check that the new string isn't longer than the old one. * (This could only happen due to programmer error introducing a * prefix longer than the namespace it replaces.) */ if (remainder && len >= remainder_len + prefix_len) { if (prefix) memmove(str, prefix, prefix_len * sizeof(wchar_t)); memmove(str + prefix_len, remainder, remainder_len * sizeof(wchar_t)); len = remainder_len + prefix_len; str[len] = L'\0'; } return git_win32_path_trim_end(str, len); }