Blame src/win32/path_w32.c

Packit ae9e2a
/*
Packit ae9e2a
 * Copyright (C) the libgit2 contributors. All rights reserved.
Packit ae9e2a
 *
Packit ae9e2a
 * This file is part of libgit2, distributed under the GNU GPL v2 with
Packit ae9e2a
 * a Linking Exception. For full terms see the included COPYING file.
Packit ae9e2a
 */
Packit ae9e2a
Packit ae9e2a
#include "common.h"
Packit ae9e2a
#include "path.h"
Packit ae9e2a
#include "path_w32.h"
Packit ae9e2a
#include "utf-conv.h"
Packit ae9e2a
#include "posix.h"
Packit ae9e2a
#include "reparse.h"
Packit ae9e2a
#include "dir.h"
Packit ae9e2a
Packit ae9e2a
#define PATH__NT_NAMESPACE     L"\\\\?\\"
Packit ae9e2a
#define PATH__NT_NAMESPACE_LEN 4
Packit ae9e2a
Packit ae9e2a
#define PATH__ABSOLUTE_LEN     3
Packit ae9e2a
Packit ae9e2a
#define path__is_dirsep(p) ((p) == '/' || (p) == '\\')
Packit ae9e2a
Packit ae9e2a
#define path__is_absolute(p) \
Packit ae9e2a
	(git__isalpha((p)[0]) && (p)[1] == ':' && ((p)[2] == '\\' || (p)[2] == '/'))
Packit ae9e2a
Packit ae9e2a
#define path__is_nt_namespace(p) \
Packit ae9e2a
	(((p)[0] == '\\' && (p)[1] == '\\' && (p)[2] == '?' && (p)[3] == '\\') || \
Packit ae9e2a
	 ((p)[0] == '/' && (p)[1] == '/' && (p)[2] == '?' && (p)[3] == '/'))
Packit ae9e2a
Packit ae9e2a
#define path__is_unc(p) \
Packit ae9e2a
	(((p)[0] == '\\' && (p)[1] == '\\') || ((p)[0] == '/' && (p)[1] == '/'))
Packit ae9e2a
Packit ae9e2a
GIT_INLINE(int) path__cwd(wchar_t *path, int size)
Packit ae9e2a
{
Packit ae9e2a
	int len;
Packit ae9e2a
Packit ae9e2a
	if ((len = GetCurrentDirectoryW(size, path)) == 0) {
Packit ae9e2a
		errno = GetLastError() == ERROR_ACCESS_DENIED ? EACCES : ENOENT;
Packit ae9e2a
		return -1;
Packit ae9e2a
	} else if (len > size) {
Packit ae9e2a
		errno = ENAMETOOLONG;
Packit ae9e2a
		return -1;
Packit ae9e2a
	}
Packit ae9e2a
Packit ae9e2a
	/* The Win32 APIs may return "\\?\" once you've used it first.
Packit ae9e2a
	 * But it may not.  What a gloriously predictible API!
Packit ae9e2a
	 */
Packit ae9e2a
	if (wcsncmp(path, PATH__NT_NAMESPACE, PATH__NT_NAMESPACE_LEN))
Packit ae9e2a
		return len;
Packit ae9e2a
Packit ae9e2a
	len -= PATH__NT_NAMESPACE_LEN;
Packit ae9e2a
Packit ae9e2a
	memmove(path, path + PATH__NT_NAMESPACE_LEN, sizeof(wchar_t) * len);
Packit ae9e2a
	return len;
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
static wchar_t *path__skip_server(wchar_t *path)
Packit ae9e2a
{
Packit ae9e2a
	wchar_t *c;
Packit ae9e2a
Packit ae9e2a
	for (c = path; *c; c++) {
Packit ae9e2a
		if (path__is_dirsep(*c))
Packit ae9e2a
			return c + 1;
Packit ae9e2a
	}
Packit ae9e2a
Packit ae9e2a
	return c;
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
static wchar_t *path__skip_prefix(wchar_t *path)
Packit ae9e2a
{
Packit ae9e2a
	if (path__is_nt_namespace(path)) {
Packit ae9e2a
		path += PATH__NT_NAMESPACE_LEN;
Packit ae9e2a
Packit ae9e2a
		if (wcsncmp(path, L"UNC\\", 4) == 0)
Packit ae9e2a
			path = path__skip_server(path + 4);
Packit ae9e2a
		else if (path__is_absolute(path))
Packit ae9e2a
			path += PATH__ABSOLUTE_LEN;
Packit ae9e2a
	} else if (path__is_absolute(path)) {
Packit ae9e2a
		path += PATH__ABSOLUTE_LEN;
Packit ae9e2a
	} else if (path__is_unc(path)) {
Packit ae9e2a
		path = path__skip_server(path + 2);
Packit ae9e2a
	}
Packit ae9e2a
Packit ae9e2a
	return path;
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
int git_win32_path_canonicalize(git_win32_path path)
Packit ae9e2a
{
Packit ae9e2a
	wchar_t *base, *from, *to, *next;
Packit ae9e2a
	size_t len;
Packit ae9e2a
Packit ae9e2a
	base = to = path__skip_prefix(path);
Packit ae9e2a
Packit ae9e2a
	/* Unposixify if the prefix */
Packit ae9e2a
	for (from = path; from < to; from++) {
Packit ae9e2a
		if (*from == L'/')
Packit ae9e2a
			*from = L'\\';
Packit ae9e2a
	}
Packit ae9e2a
Packit ae9e2a
	while (*from) {
Packit ae9e2a
		for (next = from; *next; ++next) {
Packit ae9e2a
			if (*next == L'/') {
Packit ae9e2a
				*next = L'\\';
Packit ae9e2a
				break;
Packit ae9e2a
			}
Packit ae9e2a
Packit ae9e2a
			if (*next == L'\\')
Packit ae9e2a
				break;
Packit ae9e2a
		}
Packit ae9e2a
Packit ae9e2a
		len = next - from;
Packit ae9e2a
Packit ae9e2a
		if (len == 1 && from[0] == L'.')
Packit ae9e2a
			/* do nothing with singleton dot */;
Packit ae9e2a
Packit ae9e2a
		else if (len == 2 && from[0] == L'.' && from[1] == L'.') {
Packit ae9e2a
			if (to == base) {
Packit ae9e2a
				/* no more path segments to strip, eat the "../" */
Packit ae9e2a
				if (*next == L'\\')
Packit ae9e2a
					len++;
Packit ae9e2a
Packit ae9e2a
				base = to;
Packit ae9e2a
			} else {
Packit ae9e2a
				/* back up a path segment */
Packit ae9e2a
				while (to > base && to[-1] == L'\\') to--;
Packit ae9e2a
				while (to > base && to[-1] != L'\\') to--;
Packit ae9e2a
			}
Packit ae9e2a
		} else {
Packit ae9e2a
			if (*next == L'\\' && *from != L'\\')
Packit ae9e2a
				len++;
Packit ae9e2a
Packit ae9e2a
			if (to != from)
Packit ae9e2a
				memmove(to, from, sizeof(wchar_t) * len);
Packit ae9e2a
Packit ae9e2a
			to += len;
Packit ae9e2a
		}
Packit ae9e2a
Packit ae9e2a
		from += len;
Packit ae9e2a
Packit ae9e2a
		while (*from == L'\\') from++;
Packit ae9e2a
	}
Packit ae9e2a
Packit ae9e2a
	/* Strip trailing backslashes */
Packit ae9e2a
	while (to > base && to[-1] == L'\\') to--;
Packit ae9e2a
Packit ae9e2a
	*to = L'\0';
Packit ae9e2a
Packit ae9e2a
	return (to - path);
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
int git_win32_path__cwd(wchar_t *out, size_t len)
Packit ae9e2a
{
Packit ae9e2a
	int cwd_len;
Packit ae9e2a
Packit ae9e2a
	if ((cwd_len = path__cwd(out, len)) < 0)
Packit ae9e2a
		return -1;
Packit ae9e2a
Packit ae9e2a
	/* UNC paths */
Packit ae9e2a
	if (wcsncmp(L"\\\\", out, 2) == 0) {
Packit ae9e2a
		/* Our buffer must be at least 5 characters larger than the
Packit ae9e2a
		 * current working directory:  we swallow one of the leading
Packit ae9e2a
		 * '\'s, but we we add a 'UNC' specifier to the path, plus
Packit ae9e2a
		 * a trailing directory separator, plus a NUL.
Packit ae9e2a
		 */
Packit ae9e2a
		if (cwd_len > MAX_PATH - 4) {
Packit ae9e2a
			errno = ENAMETOOLONG;
Packit ae9e2a
			return -1;
Packit ae9e2a
		}
Packit ae9e2a
Packit ae9e2a
		memmove(out+2, out, sizeof(wchar_t) * cwd_len);
Packit ae9e2a
		out[0] = L'U';
Packit ae9e2a
		out[1] = L'N';
Packit ae9e2a
		out[2] = L'C';
Packit ae9e2a
Packit ae9e2a
		cwd_len += 2;
Packit ae9e2a
	}
Packit ae9e2a
Packit ae9e2a
	/* Our buffer must be at least 2 characters larger than the current
Packit ae9e2a
	 * working directory.  (One character for the directory separator,
Packit ae9e2a
	 * one for the null.
Packit ae9e2a
	 */
Packit ae9e2a
	else if (cwd_len > MAX_PATH - 2) {
Packit ae9e2a
		errno = ENAMETOOLONG;
Packit ae9e2a
		return -1;
Packit ae9e2a
	}
Packit ae9e2a
Packit ae9e2a
	return cwd_len;
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
int git_win32_path_from_utf8(git_win32_path out, const char *src)
Packit ae9e2a
{
Packit ae9e2a
	wchar_t *dest = out;
Packit ae9e2a
Packit ae9e2a
	/* All win32 paths are in NT-prefixed format, beginning with "\\?\". */
Packit ae9e2a
	memcpy(dest, PATH__NT_NAMESPACE, sizeof(wchar_t) * PATH__NT_NAMESPACE_LEN);
Packit ae9e2a
	dest += PATH__NT_NAMESPACE_LEN;
Packit ae9e2a
Packit ae9e2a
	/* See if this is an absolute path (beginning with a drive letter) */
Packit ae9e2a
	if (path__is_absolute(src)) {
Packit ae9e2a
		if (git__utf8_to_16(dest, MAX_PATH, src) < 0)
Packit ae9e2a
			goto on_error;
Packit ae9e2a
	}
Packit ae9e2a
	/* File-prefixed NT-style paths beginning with \\?\ */
Packit ae9e2a
	else if (path__is_nt_namespace(src)) {
Packit ae9e2a
		/* Skip the NT prefix, the destination already contains it */
Packit ae9e2a
		if (git__utf8_to_16(dest, MAX_PATH, src + PATH__NT_NAMESPACE_LEN) < 0)
Packit ae9e2a
			goto on_error;
Packit ae9e2a
	}
Packit ae9e2a
	/* UNC paths */
Packit ae9e2a
	else if (path__is_unc(src)) {
Packit ae9e2a
		memcpy(dest, L"UNC\\", sizeof(wchar_t) * 4);
Packit ae9e2a
		dest += 4;
Packit ae9e2a
Packit ae9e2a
		/* Skip the leading "\\" */
Packit ae9e2a
		if (git__utf8_to_16(dest, MAX_PATH - 2, src + 2) < 0)
Packit ae9e2a
			goto on_error;
Packit ae9e2a
	}
Packit ae9e2a
	/* Absolute paths omitting the drive letter */
Packit ae9e2a
	else if (src[0] == '\\' || src[0] == '/') {
Packit ae9e2a
		if (path__cwd(dest, MAX_PATH) < 0)
Packit ae9e2a
			goto on_error;
Packit ae9e2a
Packit ae9e2a
		if (!path__is_absolute(dest)) {
Packit ae9e2a
			errno = ENOENT;
Packit ae9e2a
			goto on_error;
Packit ae9e2a
		}
Packit ae9e2a
Packit ae9e2a
		/* Skip the drive letter specification ("C:") */	
Packit ae9e2a
		if (git__utf8_to_16(dest + 2, MAX_PATH - 2, src) < 0)
Packit ae9e2a
			goto on_error;
Packit ae9e2a
	}
Packit ae9e2a
	/* Relative paths */
Packit ae9e2a
	else {
Packit ae9e2a
		int cwd_len;
Packit ae9e2a
Packit ae9e2a
		if ((cwd_len = git_win32_path__cwd(dest, MAX_PATH)) < 0)
Packit ae9e2a
			goto on_error;
Packit ae9e2a
Packit ae9e2a
		dest[cwd_len++] = L'\\';
Packit ae9e2a
Packit ae9e2a
		if (git__utf8_to_16(dest + cwd_len, MAX_PATH - cwd_len, src) < 0)
Packit ae9e2a
			goto on_error;
Packit ae9e2a
	}
Packit ae9e2a
Packit ae9e2a
	return git_win32_path_canonicalize(out);
Packit ae9e2a
Packit ae9e2a
on_error:
Packit ae9e2a
	/* set windows error code so we can use its error message */
Packit ae9e2a
	if (errno == ENAMETOOLONG)
Packit ae9e2a
		SetLastError(ERROR_FILENAME_EXCED_RANGE);
Packit ae9e2a
Packit ae9e2a
	return -1;
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
int git_win32_path_to_utf8(git_win32_utf8_path dest, const wchar_t *src)
Packit ae9e2a
{
Packit ae9e2a
	char *out = dest;
Packit ae9e2a
	int len;
Packit ae9e2a
Packit ae9e2a
	/* Strip NT namespacing "\\?\" */
Packit ae9e2a
	if (path__is_nt_namespace(src)) {
Packit ae9e2a
		src += 4;
Packit ae9e2a
Packit ae9e2a
		/* "\\?\UNC\server\share" -> "\\server\share" */
Packit ae9e2a
		if (wcsncmp(src, L"UNC\\", 4) == 0) {
Packit ae9e2a
			src += 4;
Packit ae9e2a
Packit ae9e2a
			memcpy(dest, "\\\\", 2);
Packit ae9e2a
			out = dest + 2;
Packit ae9e2a
		}
Packit ae9e2a
	}
Packit ae9e2a
Packit ae9e2a
	if ((len = git__utf16_to_8(out, GIT_WIN_PATH_UTF8, src)) < 0)
Packit ae9e2a
		return len;
Packit ae9e2a
Packit ae9e2a
	git_path_mkposix(dest);
Packit ae9e2a
Packit ae9e2a
	return len;
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
char *git_win32_path_8dot3_name(const char *path)
Packit ae9e2a
{
Packit ae9e2a
	git_win32_path longpath, shortpath;
Packit ae9e2a
	wchar_t *start;
Packit ae9e2a
	char *shortname;
Packit ae9e2a
	int len, namelen = 1;
Packit ae9e2a
Packit ae9e2a
	if (git_win32_path_from_utf8(longpath, path) < 0)
Packit ae9e2a
		return NULL;
Packit ae9e2a
Packit ae9e2a
	len = GetShortPathNameW(longpath, shortpath, GIT_WIN_PATH_UTF16);
Packit ae9e2a
Packit ae9e2a
	while (len && shortpath[len-1] == L'\\')
Packit ae9e2a
		shortpath[--len] = L'\0';
Packit ae9e2a
Packit ae9e2a
	if (len == 0 || len >= GIT_WIN_PATH_UTF16)
Packit ae9e2a
		return NULL;
Packit ae9e2a
Packit ae9e2a
	for (start = shortpath + (len - 1);
Packit ae9e2a
		start > shortpath && *(start-1) != '/' && *(start-1) != '\\';
Packit ae9e2a
		start--)
Packit ae9e2a
		namelen++;
Packit ae9e2a
Packit ae9e2a
	/* We may not have actually been given a short name.  But if we have,
Packit ae9e2a
	 * it will be in the ASCII byte range, so we don't need to worry about
Packit ae9e2a
	 * multi-byte sequences and can allocate naively.
Packit ae9e2a
	 */
Packit ae9e2a
	if (namelen > 12 || (shortname = git__malloc(namelen + 1)) == NULL)
Packit ae9e2a
		return NULL;
Packit ae9e2a
Packit ae9e2a
	if ((len = git__utf16_to_8(shortname, namelen + 1, start)) < 0)
Packit ae9e2a
		return NULL;
Packit ae9e2a
Packit ae9e2a
	return shortname;
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
static bool path_is_volume(wchar_t *target, size_t target_len)
Packit ae9e2a
{
Packit ae9e2a
	return (target_len && wcsncmp(target, L"\\??\\Volume{", 11) == 0);
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
/* On success, returns the length, in characters, of the path stored in dest.
Packit ae9e2a
* On failure, returns a negative value. */
Packit ae9e2a
int git_win32_path_readlink_w(git_win32_path dest, const git_win32_path path)
Packit ae9e2a
{
Packit ae9e2a
	BYTE buf[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
Packit ae9e2a
	GIT_REPARSE_DATA_BUFFER *reparse_buf = (GIT_REPARSE_DATA_BUFFER *)buf;
Packit ae9e2a
	HANDLE handle = NULL;
Packit ae9e2a
	DWORD ioctl_ret;
Packit ae9e2a
	wchar_t *target;
Packit ae9e2a
	size_t target_len;
Packit ae9e2a
Packit ae9e2a
	int error = -1;
Packit ae9e2a
Packit ae9e2a
	handle = CreateFileW(path, GENERIC_READ,
Packit ae9e2a
		FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
Packit ae9e2a
		FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, NULL);
Packit ae9e2a
Packit ae9e2a
	if (handle == INVALID_HANDLE_VALUE) {
Packit ae9e2a
		errno = ENOENT;
Packit ae9e2a
		return -1;
Packit ae9e2a
	}
Packit ae9e2a
Packit ae9e2a
	if (!DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, NULL, 0,
Packit ae9e2a
		reparse_buf, sizeof(buf), &ioctl_ret, NULL)) {
Packit ae9e2a
		errno = EINVAL;
Packit ae9e2a
		goto on_error;
Packit ae9e2a
	}
Packit ae9e2a
Packit ae9e2a
	switch (reparse_buf->ReparseTag) {
Packit ae9e2a
	case IO_REPARSE_TAG_SYMLINK:
Packit ae9e2a
		target = reparse_buf->SymbolicLinkReparseBuffer.PathBuffer +
Packit ae9e2a
			(reparse_buf->SymbolicLinkReparseBuffer.SubstituteNameOffset / sizeof(WCHAR));
Packit ae9e2a
		target_len = reparse_buf->SymbolicLinkReparseBuffer.SubstituteNameLength / sizeof(WCHAR);
Packit ae9e2a
	break;
Packit ae9e2a
	case IO_REPARSE_TAG_MOUNT_POINT:
Packit ae9e2a
		target = reparse_buf->MountPointReparseBuffer.PathBuffer +
Packit ae9e2a
			(reparse_buf->MountPointReparseBuffer.SubstituteNameOffset / sizeof(WCHAR));
Packit ae9e2a
		target_len = reparse_buf->MountPointReparseBuffer.SubstituteNameLength / sizeof(WCHAR);
Packit ae9e2a
	break;
Packit ae9e2a
	default:
Packit ae9e2a
		errno = EINVAL;
Packit ae9e2a
		goto on_error;
Packit ae9e2a
	}
Packit ae9e2a
Packit ae9e2a
	if (path_is_volume(target, target_len)) {
Packit ae9e2a
		/* This path is a reparse point that represents another volume mounted
Packit ae9e2a
		* at this location, it is not a symbolic link our input was canonical.
Packit ae9e2a
		*/
Packit ae9e2a
		errno = EINVAL;
Packit ae9e2a
		error = -1;
Packit ae9e2a
	} else if (target_len) {
Packit ae9e2a
		/* The path may need to have a prefix removed. */
Packit ae9e2a
		target_len = git_win32__canonicalize_path(target, target_len);
Packit ae9e2a
Packit ae9e2a
		/* Need one additional character in the target buffer
Packit ae9e2a
		* for the terminating NULL. */
Packit ae9e2a
		if (GIT_WIN_PATH_UTF16 > target_len) {
Packit ae9e2a
			wcscpy(dest, target);
Packit ae9e2a
			error = (int)target_len;
Packit ae9e2a
		}
Packit ae9e2a
	}
Packit ae9e2a
Packit ae9e2a
on_error:
Packit ae9e2a
	CloseHandle(handle);
Packit ae9e2a
	return error;
Packit ae9e2a
}