diff options
author | Edward Thomson <ethomson@microsoft.com> | 2014-04-21 23:32:31 -0400 |
---|---|---|
committer | Philip Kelley <phkelley@hotmail.com> | 2014-04-22 00:28:27 -0400 |
commit | 65477db1660273c453c590b8e3b97a4f7c41df61 (patch) | |
tree | 667f296f7e4ac27ba31fbf3e96446792e7ca5f45 /src/win32 | |
parent | c2c8161541e54689926ec1f463569d5d1b975503 (diff) | |
download | libgit2-65477db1660273c453c590b8e3b97a4f7c41df61.tar.gz |
Handle win32 reparse points properly
Diffstat (limited to 'src/win32')
-rw-r--r-- | src/win32/posix.h | 2 | ||||
-rw-r--r-- | src/win32/posix_w32.c | 333 | ||||
-rw-r--r-- | src/win32/reparse.h | 57 | ||||
-rw-r--r-- | src/win32/w32_util.c | 70 | ||||
-rw-r--r-- | src/win32/w32_util.h | 23 |
5 files changed, 353 insertions, 132 deletions
diff --git a/src/win32/posix.h b/src/win32/posix.h index e5a32b510..7f9d57cc3 100644 --- a/src/win32/posix.h +++ b/src/win32/posix.h @@ -30,7 +30,7 @@ GIT_INLINE(int) p_link(const char *old, const char *new) extern int p_mkdir(const char *path, mode_t mode); extern int p_unlink(const char *path); extern int p_lstat(const char *file_name, struct stat *buf); -extern int p_readlink(const char *link, char *target, size_t target_len); +extern int p_readlink(const char *path, char *buf, size_t bufsiz); extern int p_symlink(const char *old, const char *new); extern char *p_realpath(const char *orig_path, char *buffer); extern int p_vsnprintf(char *buffer, size_t count, const char *format, va_list argptr); diff --git a/src/win32/posix_w32.c b/src/win32/posix_w32.c index 868be6017..bef65a354 100644 --- a/src/win32/posix_w32.c +++ b/src/win32/posix_w32.c @@ -9,12 +9,13 @@ #include "path.h" #include "utf-conv.h" #include "repository.h" +#include "reparse.h" #include <errno.h> #include <io.h> #include <fcntl.h> #include <ws2tcpip.h> -#if defined(__MINGW32__) +#ifndef FILE_NAME_NORMALIZED # define FILE_NAME_NORMALIZED 0 #endif @@ -100,29 +101,79 @@ GIT_INLINE(time_t) filetime_to_time_t(const FILETIME *ft) return (time_t)winTime; } -#define WIN32_IS_WSEP(CH) ((CH) == L'/' || (CH) == L'\\') - -static int do_lstat( - const char *file_name, struct stat *buf, int posix_enotdir) +/* On success, returns the length, in characters, of the path stored in dest. + * On failure, returns a negative value. */ +static int readlink_w( + git_win32_path dest, + const git_win32_path path) { - WIN32_FILE_ATTRIBUTE_DATA fdata; - git_win32_path fbuf; - wchar_t lastch; - int flen; + 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; - if ((flen = utf8_to_16_with_errno(fbuf, file_name)) < 0) + 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 (INVALID_HANDLE_VALUE == handle) { + errno = ENOENT; return -1; + } - /* truncate trailing slashes */ - for (; flen > 0; --flen) { - lastch = fbuf[flen - 1]; - if (WIN32_IS_WSEP(lastch)) - fbuf[flen - 1] = L'\0'; - else if (lastch != L'\0') - break; + if (!DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, NULL, 0, + reparse_buf, sizeof(buf), &ioctl_ret, NULL)) { + errno = EINVAL; + goto on_error; } - if (GetFileAttributesExW(fbuf, GetFileExInfoStandard, &fdata)) { + switch (reparse_buf->ReparseTag) { + case IO_REPARSE_TAG_SYMLINK: + target = reparse_buf->SymbolicLinkReparseBuffer.PathBuffer + + (reparse_buf->SymbolicLinkReparseBuffer.SubstituteNameOffset / sizeof(WCHAR)); + target_len = reparse_buf->SymbolicLinkReparseBuffer.SubstituteNameLength / sizeof(WCHAR); + break; + case IO_REPARSE_TAG_MOUNT_POINT: + target = reparse_buf->MountPointReparseBuffer.PathBuffer + + (reparse_buf->MountPointReparseBuffer.SubstituteNameOffset / sizeof(WCHAR)); + target_len = reparse_buf->MountPointReparseBuffer.SubstituteNameLength / sizeof(WCHAR); + break; + default: + errno = EINVAL; + goto on_error; + } + + if (target_len) { + /* The path may need to have a prefix removed. */ + target_len = git_win32__to_dos(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; +} + +#define WIN32_IS_WSEP(CH) ((CH) == L'/' || (CH) == L'\\') + +static int lstat_w( + wchar_t *path, + struct stat *buf, + bool posix_enotdir) +{ + WIN32_FILE_ATTRIBUTE_DATA fdata; + + if (GetFileAttributesExW(path, GetFileExInfoStandard, &fdata)) { int fMode = S_IREAD; if (!buf) @@ -136,12 +187,6 @@ static int do_lstat( if (!(fdata.dwFileAttributes & FILE_ATTRIBUTE_READONLY)) fMode |= S_IWRITE; - if (fdata.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) - fMode |= S_IFLNK; - - if ((fMode & (S_IFDIR | S_IFLNK)) == (S_IFDIR | S_IFLNK)) // junction - fMode ^= S_IFLNK; - buf->st_ino = 0; buf->st_gid = 0; buf->st_uid = 0; @@ -153,16 +198,17 @@ static int do_lstat( buf->st_mtime = filetime_to_time_t(&(fdata.ftLastWriteTime)); buf->st_ctime = filetime_to_time_t(&(fdata.ftCreationTime)); - /* Windows symlinks have zero file size, call readlink to determine - * the length of the path pointed to, which we expect everywhere else - */ - if (S_ISLNK(fMode)) { - git_win32_utf8_path target; + if (fdata.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) { + git_win32_path target; - if (p_readlink(file_name, target, GIT_WIN_PATH_UTF8) == -1) - return -1; + if (readlink_w(target, path) >= 0) { + buf->st_mode = (buf->st_mode & ~S_IFMT) | S_IFLNK; - buf->st_size = strlen(target); + /* st_size gets the UTF-8 length of the target name, in bytes, + * not counting the NULL terminator */ + if ((buf->st_size = git__utf16_to_8(NULL, 0, target)) < 0) + return -1; + } } return 0; @@ -174,18 +220,23 @@ static int do_lstat( * file path is a regular file, otherwise set ENOENT. */ if (posix_enotdir) { + size_t path_len = wcslen(path); + /* scan up path until we find an existing item */ while (1) { + DWORD attrs; + /* remove last directory component */ - for (--flen; flen > 0 && !WIN32_IS_WSEP(fbuf[flen]); --flen); + for (path_len--; path_len > 0 && !WIN32_IS_WSEP(path[path_len]); path_len--); - if (flen <= 0) + if (path_len <= 0) break; - fbuf[flen] = L'\0'; + path[path_len] = L'\0'; + attrs = GetFileAttributesW(path); - if (GetFileAttributesExW(fbuf, GetFileExInfoStandard, &fdata)) { - if (!(fdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) + if (INVALID_FILE_ATTRIBUTES != attrs) { + if (!(attrs & FILE_ATTRIBUTE_DIRECTORY)) errno = ENOTDIR; break; } @@ -195,105 +246,51 @@ static int do_lstat( return -1; } -int p_lstat(const char *filename, struct stat *buf) +static int do_lstat(const char *path, struct stat *buf, bool posixly_correct) { - return do_lstat(filename, buf, 0); + git_win32_path path_w; + int len; + + if ((len = utf8_to_16_with_errno(path_w, path)) < 0) + return -1; + + git_win32__path_trim_end(path_w, len); + + return lstat_w(path_w, buf, posixly_correct); } -int p_lstat_posixly(const char *filename, struct stat *buf) +int p_lstat(const char *filename, struct stat *buf) { - return do_lstat(filename, buf, 1); + return do_lstat(filename, buf, false); } -/* - * Returns the address of the GetFinalPathNameByHandleW function. - * This function is available on Windows Vista and higher. - */ -static PFGetFinalPathNameByHandleW get_fpnbyhandle(void) +int p_lstat_posixly(const char *filename, struct stat *buf) { - static PFGetFinalPathNameByHandleW pFunc = NULL; - PFGetFinalPathNameByHandleW toReturn = pFunc; - - if (!toReturn) { - HMODULE hModule = GetModuleHandleW(L"kernel32"); - - if (hModule) - toReturn = (PFGetFinalPathNameByHandleW)GetProcAddress(hModule, "GetFinalPathNameByHandleW"); - - pFunc = toReturn; - } - - assert(toReturn); - - return toReturn; + return do_lstat(filename, buf, true); } -/* - * Parts of the The p_readlink function are heavily inspired by the php - * readlink function in link_win32.c - * - * Copyright (c) 1999 - 2012 The PHP Group. All rights reserved. - * - * For details of the PHP license see http://www.php.net/license/3_01.txt - */ -int p_readlink(const char *link, char *target, size_t target_len) +int p_readlink(const char *path, char *buf, size_t bufsiz) { - static const wchar_t prefix[] = L"\\\\?\\"; - PFGetFinalPathNameByHandleW pgfp = get_fpnbyhandle(); - HANDLE hFile = NULL; - wchar_t *target_w = NULL; - bool trim_prefix; - git_win32_path link_w; - DWORD dwChars, dwLastError; - int error = -1; - - /* Check that we found the function, and convert to UTF-16 */ - if (!pgfp || utf8_to_16_with_errno(link_w, link) < 0) - goto on_error; - - /* Use FILE_FLAG_BACKUP_SEMANTICS so we can open a directory. Do not - * specify FILE_FLAG_OPEN_REPARSE_POINT; we want to open a handle to the - * target of the link. */ - hFile = CreateFileW(link_w, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, - NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); - - if (hFile == INVALID_HANDLE_VALUE) - goto on_error; - - /* Find out how large the buffer should be to hold the result */ - if (!(dwChars = pgfp(hFile, NULL, 0, FILE_NAME_NORMALIZED))) - goto on_error; - - if (!(target_w = git__malloc(dwChars * sizeof(wchar_t)))) - goto on_error; - - /* Call a second time */ - dwChars = pgfp(hFile, target_w, dwChars, FILE_NAME_NORMALIZED); - - if (!dwChars) - goto on_error; - - /* Do we need to trim off a \\?\ from the start of the path? */ - trim_prefix = (dwChars >= ARRAY_SIZE(prefix)) && - !wcsncmp(prefix, target_w, ARRAY_SIZE(prefix)); - - /* Convert the result to UTF-8 */ - if (git__utf16_to_8(target, target_len, trim_prefix ? target_w + 4 : target_w) < 0) - goto on_error; + git_win32_path path_w, target_w; + git_win32_utf8_path target; + int len; - error = 0; + /* readlink(2) does not NULL-terminate the string written + * to the target buffer. Furthermore, the target buffer need + * not be large enough to hold the entire result. A truncated + * result should be written in this case. Since this truncation + * could occur in the middle of the encoding of a code point, + * we need to buffer the result on the stack. */ -on_error: - dwLastError = GetLastError(); - - if (hFile && INVALID_HANDLE_VALUE != hFile) - CloseHandle(hFile); + if (utf8_to_16_with_errno(path_w, path) < 0 || + readlink_w(target_w, path_w) < 0 || + (len = git_win32_path_to_utf8(target, target_w)) < 0) + return -1; - if (target_w) - git__free(target_w); + bufsiz = min((size_t)len, bufsiz); + memcpy(buf, target, bufsiz); - SetLastError(dwLastError); - return error; + return (int)bufsiz; } int p_symlink(const char *old, const char *new) @@ -356,20 +353,94 @@ int p_getcwd(char *buffer_out, size_t size) return 0; } +/* + * Returns the address of the GetFinalPathNameByHandleW function. + * This function is available on Windows Vista and higher. + */ +static PFGetFinalPathNameByHandleW get_fpnbyhandle(void) +{ + static PFGetFinalPathNameByHandleW pFunc = NULL; + PFGetFinalPathNameByHandleW toReturn = pFunc; + + if (!toReturn) { + HMODULE hModule = GetModuleHandleW(L"kernel32"); + + if (hModule) + toReturn = (PFGetFinalPathNameByHandleW)GetProcAddress(hModule, "GetFinalPathNameByHandleW"); + + pFunc = toReturn; + } + + assert(toReturn); + + return toReturn; +} + +static int getfinalpath_w( + git_win32_path dest, + const wchar_t *path) +{ + PFGetFinalPathNameByHandleW pgfp = get_fpnbyhandle(); + HANDLE hFile; + DWORD dwChars; + + if (!pgfp) + return -1; + + /* Use FILE_FLAG_BACKUP_SEMANTICS so we can open a directory. Do not + * specify FILE_FLAG_OPEN_REPARSE_POINT; we want to open a handle to the + * target of the link. */ + hFile = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); + + if (hFile == INVALID_HANDLE_VALUE) + return -1; + + /* Call GetFinalPathNameByHandle */ + dwChars = pgfp(hFile, dest, GIT_WIN_PATH_UTF16, FILE_NAME_NORMALIZED); + + if (!dwChars || dwChars >= GIT_WIN_PATH_UTF16) { + DWORD error = GetLastError(); + CloseHandle(hFile); + SetLastError(error); + return -1; + } + + CloseHandle(hFile); + + /* The path may be delivered to us with a prefix; canonicalize */ + return (int)git_win32__to_dos(dest, dwChars); +} + +static int follow_and_lstat_link(git_win32_path path, struct stat* buf) +{ + git_win32_path target_w; + + if (getfinalpath_w(target_w, path) < 0) + return -1; + + return lstat_w(target_w, buf, false); +} + int p_stat(const char* path, struct stat* buf) { - git_win32_utf8_path target; - int error = 0; + git_win32_path path_w; + int len; - error = do_lstat(path, buf, 0); + if ((len = utf8_to_16_with_errno(path_w, path)) < 0) + return -1; - /* We need not do this in a loop to unwind chains of symlinks since - * p_readlink calls GetFinalPathNameByHandle which does it for us. */ - if (error >= 0 && S_ISLNK(buf->st_mode) && - (error = p_readlink(path, target, GIT_WIN_PATH_UTF8)) >= 0) - error = do_lstat(target, buf, 0); + git_win32__path_trim_end(path_w, len); - return error; + if (lstat_w(path_w, buf, false) < 0) + return -1; + + /* The item is a symbolic link or mount point. No need to iterate + * to follow multiple links; use GetFinalPathNameFromHandle. */ + if (S_ISLNK(buf->st_mode)) + return follow_and_lstat_link(path_w, buf); + + return 0; } int p_chdir(const char* path) diff --git a/src/win32/reparse.h b/src/win32/reparse.h new file mode 100644 index 000000000..4f56ed055 --- /dev/null +++ b/src/win32/reparse.h @@ -0,0 +1,57 @@ +/* +* 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_git_win32_reparse_h__ +#define INCLUDE_git_win32_reparse_h__ + +/* This structure is defined on MSDN at +* http://msdn.microsoft.com/en-us/library/windows/hardware/ff552012(v=vs.85).aspx +* +* It was formerly included in the Windows 2000 SDK and remains defined in +* MinGW, so we must define it with a silly name to avoid conflicting. +*/ +typedef struct _GIT_REPARSE_DATA_BUFFER { + ULONG ReparseTag; + USHORT ReparseDataLength; + USHORT Reserved; + union { + struct { + USHORT SubstituteNameOffset; + USHORT SubstituteNameLength; + USHORT PrintNameOffset; + USHORT PrintNameLength; + ULONG Flags; + WCHAR PathBuffer[1]; + } SymbolicLinkReparseBuffer; + struct { + USHORT SubstituteNameOffset; + USHORT SubstituteNameLength; + USHORT PrintNameOffset; + USHORT PrintNameLength; + WCHAR PathBuffer[1]; + } MountPointReparseBuffer; + struct { + UCHAR DataBuffer[1]; + } GenericReparseBuffer; + }; +} GIT_REPARSE_DATA_BUFFER; + +#define REPARSE_DATA_HEADER_SIZE 8 +#define REPARSE_DATA_MOUNTPOINT_HEADER_SIZE 8 +#define REPARSE_DATA_UNION_SIZE 12 + +/* Missing in MinGW */ +#ifndef FSCTL_GET_REPARSE_POINT +# define FSCTL_GET_REPARSE_POINT 0x000900a8 +#endif + +/* Missing in MinGW */ +#ifndef FSCTL_SET_REPARSE_POINT +# define FSCTL_SET_REPARSE_POINT 0x000900a4 +#endif + +#endif
\ No newline at end of file diff --git a/src/win32/w32_util.c b/src/win32/w32_util.c index 246b30a6b..50b85a334 100644 --- a/src/win32/w32_util.c +++ b/src/win32/w32_util.c @@ -7,6 +7,8 @@ #include "w32_util.h" +#define CONST_STRLEN(x) ((sizeof(x)/sizeof(x[0])) - 1) + /** * Creates a FindFirstFile(Ex) filter string from a UTF-8 path. * The filter string enumerates all items in the directory. @@ -67,3 +69,71 @@ int git_win32__sethidden(const char *path) return 0; } + +/** + * 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 (3 == len && 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__to_dos(wchar_t *str, size_t len) +{ + static const wchar_t dosdevices_prefix[] = L"\\\?\?\\"; + static const wchar_t nt_prefix[] = L"\\\\?\\"; + static const wchar_t unc_prefix[] = L"UNC\\"; + size_t to_advance = 0; + + /* "\??\" -- DOS Devices prefix */ + if (len >= CONST_STRLEN(dosdevices_prefix) && + !wcsncmp(str, dosdevices_prefix, CONST_STRLEN(dosdevices_prefix))) { + to_advance += CONST_STRLEN(dosdevices_prefix); + len -= CONST_STRLEN(dosdevices_prefix); + } + /* "\\?\" -- NT namespace prefix */ + else if (len >= CONST_STRLEN(nt_prefix) && + !wcsncmp(str, nt_prefix, CONST_STRLEN(nt_prefix))) { + to_advance += CONST_STRLEN(nt_prefix); + len -= CONST_STRLEN(nt_prefix); + } + + /* "\??\UNC\", "\\?\UNC\" -- UNC prefix */ + if (to_advance && len >= CONST_STRLEN(unc_prefix) && + !wcsncmp(str + to_advance, unc_prefix, CONST_STRLEN(unc_prefix))) { + to_advance += CONST_STRLEN(unc_prefix); + len -= CONST_STRLEN(unc_prefix); + } + + if (to_advance) { + memmove(str, str + to_advance, len * sizeof(wchar_t)); + str[len] = L'\0'; + } + + return git_win32__path_trim_end(str, len); +}
\ No newline at end of file diff --git a/src/win32/w32_util.h b/src/win32/w32_util.h index cd02bd7b2..acdee3d69 100644 --- a/src/win32/w32_util.h +++ b/src/win32/w32_util.h @@ -10,6 +10,11 @@ #include "utf-conv.h" +GIT_INLINE(bool) git_win32__isalpha(wchar_t c) +{ + return ((c >= L'A' && c <= L'Z') || (c >= L'a' && c <= L'z')); +} + /** * Creates a FindFirstFile(Ex) filter string from a UTF-8 path. * The filter string enumerates all items in the directory. @@ -28,4 +33,22 @@ bool git_win32__findfirstfile_filter(git_win32_path dest, const char *src); */ int git_win32__sethidden(const char *path); +/** + * 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); + +/** + * 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__to_dos(wchar_t *str, size_t len); + #endif |