summaryrefslogtreecommitdiff
path: root/src/win32
diff options
context:
space:
mode:
authorEdward Thomson <ethomson@microsoft.com>2014-04-21 23:32:31 -0400
committerPhilip Kelley <phkelley@hotmail.com>2014-04-22 00:28:27 -0400
commit65477db1660273c453c590b8e3b97a4f7c41df61 (patch)
tree667f296f7e4ac27ba31fbf3e96446792e7ca5f45 /src/win32
parentc2c8161541e54689926ec1f463569d5d1b975503 (diff)
downloadlibgit2-65477db1660273c453c590b8e3b97a4f7c41df61.tar.gz
Handle win32 reparse points properly
Diffstat (limited to 'src/win32')
-rw-r--r--src/win32/posix.h2
-rw-r--r--src/win32/posix_w32.c333
-rw-r--r--src/win32/reparse.h57
-rw-r--r--src/win32/w32_util.c70
-rw-r--r--src/win32/w32_util.h23
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