Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support core.longpaths on Windows #5347

Closed
wants to merge 10 commits into from
8 changes: 8 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,14 @@ jobs:
CMAKE_OPTIONS: -A x64 -DWIN32_LEAKCHECK=ON -DDEPRECATE_HARD=ON
SKIP_SSH_TESTS: true
SKIP_NEGOTIATE_TESTS: true
- # Windows amd64 Visual Studio Longpaths
os: windows-2019
env:
ARCH: amd64
CMAKE_GENERATOR: Visual Studio 16 2019
CMAKE_OPTIONS: -A x64 -DWIN32_LEAKCHECK=ON -DDEPRECATE_HARD=ON -DTEST_LONGPATHS=ON
SKIP_SSH_TESTS: true
SKIP_NEGOTIATE_TESTS: true
- # Windows x86 Visual Studio
os: windows-2019
env:
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ jobs:
CMAKE_OPTIONS: -A x64 -DDEPRECATE_HARD=ON
SKIP_SSH_TESTS: true
SKIP_NEGOTIATE_TESTS: true
- # Windows amd64 Visual Studio Longpaths
os: windows-2019
env:
ARCH: amd64
CMAKE_GENERATOR: Visual Studio 16 2019
CMAKE_OPTIONS: -A x64 -DWIN32_LEAKCHECK=ON -DDEPRECATE_HARD=ON -DTEST_LONGPATHS=ON
SKIP_SSH_TESTS: true
SKIP_NEGOTIATE_TESTS: true
- # Windows x86 Visual Studio
os: windows-2019
env:
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ IF(WIN32)
# By default, libgit2 is built with WinHTTP. To use the built-in
# HTTP transport, invoke CMake with the "-DWINHTTP=OFF" argument.
OPTION(WINHTTP "Use Win32 WinHTTP routines" ON)
OPTION(TEST_LONGPATHS "Build tests with longpaths support" OFF)
ENDIF()

IF(MSVC)
Expand Down
10 changes: 9 additions & 1 deletion include/git2/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,9 @@ typedef enum {
GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS,
GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE,
GIT_OPT_GET_MWINDOW_FILE_LIMIT,
GIT_OPT_SET_MWINDOW_FILE_LIMIT
GIT_OPT_SET_MWINDOW_FILE_LIMIT,
GIT_OPT_SET_WINDOWS_LONGPATHS,
GIT_OPT_GET_WINDOWS_LONGPATHS
} git_libgit2_opt_t;

/**
Expand Down Expand Up @@ -416,6 +418,12 @@ typedef enum {
* > authentication, use expect/continue when POSTing data.
* > This option is not available on Windows.
*
* opts(GIT_OPT_SET_WINDOWS_LONGPATHS, int enabled)
* > Set whether longpaths (paths > 260) are allowed on Windows.
*
* opts(GIT_OPT_GET_WINDOWS_LONGPATHS, int *enabled)
* > Get whether longpaths (paths > 260) are allowed on Windows.
*
* @param option Option key
* @param ... value to set the option
* @return 0 on success, <0 on failure
Expand Down
13 changes: 13 additions & 0 deletions src/libgit2.c
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ extern size_t git_mwindow__mapped_limit;
extern size_t git_mwindow__file_limit;
extern size_t git_indexer__max_objects;
extern bool git_disable_pack_keep_file_checks;
extern bool git_win32_longpaths_support;

char *git__user_agent;
char *git__ssl_ciphers;
Expand Down Expand Up @@ -368,6 +369,18 @@ int git_libgit2_opts(int key, ...)
git_http__expect_continue = (va_arg(ap, int) != 0);
break;

case GIT_OPT_SET_WINDOWS_LONGPATHS:
#ifdef GIT_WIN32
git_win32_longpaths_support = (va_arg(ap, int) != 0);
#endif
break;

case GIT_OPT_GET_WINDOWS_LONGPATHS:
#ifdef GIT_WIN32
*(va_arg(ap, int *)) = git_win32_longpaths_support;
#endif
break;

default:
git_error_set(GIT_ERROR_INVALID, "invalid option key");
error = -1;
Expand Down
8 changes: 6 additions & 2 deletions src/path.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
#include <stdio.h>
#include <ctype.h>

extern bool git_win32_longpaths_support;

static int dos_drive_prefix_length(const char *path)
{
int i;
Expand Down Expand Up @@ -1244,14 +1246,16 @@ int git_path_diriter_init(
static int diriter_update_paths(git_path_diriter *diriter)
{
size_t filename_len, path_len;

size_t max_path_utf16_length = git_win32_longpaths_support
? GIT_WIN_PATH_UTF16
: GIT_WIN_SHORT_PATH_UTF16;
filename_len = wcslen(diriter->current.cFileName);

if (GIT_ADD_SIZET_OVERFLOW(&path_len, diriter->parent_len, filename_len) ||
GIT_ADD_SIZET_OVERFLOW(&path_len, path_len, 2))
return -1;

if (path_len > GIT_WIN_PATH_UTF16) {
if (path_len > max_path_utf16_length) {
git_error_set(GIT_ERROR_FILESYSTEM,
"invalid path '%.*ls\\%ls' (path too long)",
diriter->parent_len, diriter->path, diriter->current.cFileName);
Expand Down
44 changes: 30 additions & 14 deletions src/win32/path_w32.c
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
#define path__startswith_slash(p) \
((p)[0] == '\\' || (p)[0] == '/')

bool git_win32_longpaths_support = false;

GIT_INLINE(int) path__cwd(wchar_t *path, int size)
{
int len;
Expand Down Expand Up @@ -142,7 +144,6 @@ int git_win32_path_canonicalize(git_win32_path path)
while (to > base && to[-1] == L'\\') to--;

*to = L'\0';

if ((to - path) > INT_MAX) {
SetLastError(ERROR_FILENAME_EXCED_RANGE);
return -1;
Expand All @@ -154,6 +155,9 @@ int git_win32_path_canonicalize(git_win32_path path)
static int win32_path_cwd(wchar_t *out, size_t len)
{
int cwd_len;
int max_path_length = git_win32_longpaths_support
? WIN_GIT_PATH_MAX
: WIN_GIT_SHORT_PATH_MAX;

if (len > INT_MAX) {
errno = ENAMETOOLONG;
Expand All @@ -170,7 +174,7 @@ static int win32_path_cwd(wchar_t *out, size_t len)
* '\'s, but we we add a 'UNC' specifier to the path, plus
* a trailing directory separator, plus a NUL.
*/
if (cwd_len > MAX_PATH - 4) {
if (cwd_len > max_path_length - 4) {
errno = ENAMETOOLONG;
return -1;
}
Expand All @@ -187,7 +191,7 @@ static int win32_path_cwd(wchar_t *out, size_t len)
* working directory. (One character for the directory separator,
* one for the null.
*/
else if (cwd_len > MAX_PATH - 2) {
else if (cwd_len > max_path_length - 2) {
errno = ENAMETOOLONG;
return -1;
}
Expand All @@ -198,20 +202,23 @@ static int win32_path_cwd(wchar_t *out, size_t len)
int git_win32_path_from_utf8(git_win32_path out, const char *src)
{
wchar_t *dest = out;
size_t max_path_length = git_win32_longpaths_support
? WIN_GIT_PATH_MAX
: WIN_GIT_SHORT_PATH_MAX;

/* 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_path_is_absolute(src)) {
if (git__utf8_to_16(dest, MAX_PATH, src) < 0)
if (git__utf8_to_16(dest, max_path_length, 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, MAX_PATH, src + PATH__NT_NAMESPACE_LEN) < 0)
if (git__utf8_to_16(dest, max_path_length, src + PATH__NT_NAMESPACE_LEN) < 0)
goto on_error;
}
/* UNC paths */
Expand All @@ -220,12 +227,12 @@ int git_win32_path_from_utf8(git_win32_path out, const char *src)
dest += 4;

/* Skip the leading "\\" */
if (git__utf8_to_16(dest, MAX_PATH - 2, src + 2) < 0)
if (git__utf8_to_16(dest, max_path_length - 2, src + 2) < 0)
goto on_error;
}
/* Absolute paths omitting the drive letter */
else if (path__startswith_slash(src)) {
if (path__cwd(dest, MAX_PATH) < 0)
if (path__cwd(dest, (int)max_path_length) < 0)
goto on_error;

if (!git_path_is_absolute(dest)) {
Expand All @@ -234,19 +241,19 @@ int git_win32_path_from_utf8(git_win32_path out, const char *src)
}

/* Skip the drive letter specification ("C:") */
if (git__utf8_to_16(dest + 2, MAX_PATH - 2, src) < 0)
if (git__utf8_to_16(dest + 2, max_path_length - 2, src) < 0)
goto on_error;
}
/* Relative paths */
else {
int cwd_len;

if ((cwd_len = win32_path_cwd(dest, MAX_PATH)) < 0)
if ((cwd_len = win32_path_cwd(dest, max_path_length)) < 0)
goto on_error;

dest[cwd_len++] = L'\\';

if (git__utf8_to_16(dest + cwd_len, MAX_PATH - cwd_len, src) < 0)
if (git__utf8_to_16(dest + cwd_len, max_path_length - cwd_len, src) < 0)
goto on_error;
}

Expand All @@ -264,6 +271,9 @@ int git_win32_path_relative_from_utf8(git_win32_path out, const char *src)
{
wchar_t *dest = out, *p;
int len;
size_t max_path_length = git_win32_longpaths_support
? WIN_GIT_PATH_MAX
: WIN_GIT_SHORT_PATH_MAX;

/* Handle absolute paths */
if (git_path_is_absolute(src) ||
Expand All @@ -273,7 +283,7 @@ int git_win32_path_relative_from_utf8(git_win32_path out, const char *src)
return git_win32_path_from_utf8(out, src);
}

if ((len = git__utf8_to_16(dest, MAX_PATH, src)) < 0)
if ((len = git__utf8_to_16(dest, max_path_length, src)) < 0)
return -1;

for (p = dest; p < (dest + len); p++) {
Expand Down Expand Up @@ -316,16 +326,19 @@ char *git_win32_path_8dot3_name(const char *path)
wchar_t *start;
char *shortname;
int len, namelen = 1;
int max_path_utf16_length = git_win32_longpaths_support
? GIT_WIN_PATH_UTF16
: GIT_WIN_SHORT_PATH_UTF16;

if (git_win32_path_from_utf8(longpath, path) < 0)
return NULL;

len = GetShortPathNameW(longpath, shortpath, GIT_WIN_PATH_UTF16);
len = GetShortPathNameW(longpath, shortpath, max_path_utf16_length);

while (len && shortpath[len-1] == L'\\')
shortpath[--len] = L'\0';

if (len == 0 || len >= GIT_WIN_PATH_UTF16)
if (len == 0 || len >= max_path_utf16_length)
return NULL;

for (start = shortpath + (len - 1);
Expand Down Expand Up @@ -361,6 +374,9 @@ int git_win32_path_readlink_w(git_win32_path dest, const git_win32_path path)
DWORD ioctl_ret;
wchar_t *target;
size_t target_len;
size_t max_path_utf16_length = git_win32_longpaths_support
? GIT_WIN_PATH_UTF16
: GIT_WIN_SHORT_PATH_UTF16;

int error = -1;

Expand Down Expand Up @@ -407,7 +423,7 @@ int git_win32_path_readlink_w(git_win32_path dest, const git_win32_path path)

/* Need one additional character in the target buffer
* for the terminating NULL. */
if (GIT_WIN_PATH_UTF16 > target_len) {
if (max_path_utf16_length > target_len) {
wcscpy(dest, target);
error = (int)target_len;
}
Expand Down
18 changes: 14 additions & 4 deletions src/win32/posix_w32.c
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
unsigned long git_win32__createfile_sharemode =
FILE_SHARE_READ | FILE_SHARE_WRITE;
int git_win32__retries = 10;
extern bool git_win32_longpaths_support;

GIT_INLINE(void) set_errno(void)
{
Expand Down Expand Up @@ -636,7 +637,10 @@ int p_futimes(int fd, const struct p_timeval times[2])
int p_getcwd(char *buffer_out, size_t size)
{
git_win32_path buf;
wchar_t *cwd = _wgetcwd(buf, GIT_WIN_PATH_UTF16);
int max_path_utf16_length = git_win32_longpaths_support
? GIT_WIN_PATH_UTF16
: GIT_WIN_SHORT_PATH_UTF16;
wchar_t *cwd = _wgetcwd(buf, max_path_utf16_length);

if (!cwd)
return -1;
Expand All @@ -662,6 +666,9 @@ static int getfinalpath_w(
{
HANDLE hFile;
DWORD dwChars;
DWORD max_path_utf16_length = git_win32_longpaths_support
? GIT_WIN_PATH_UTF16
: GIT_WIN_SHORT_PATH_UTF16;

/* 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
Expand All @@ -673,10 +680,10 @@ static int getfinalpath_w(
return -1;

/* Call GetFinalPathNameByHandle */
dwChars = GetFinalPathNameByHandleW(hFile, dest, GIT_WIN_PATH_UTF16, FILE_NAME_NORMALIZED);
dwChars = GetFinalPathNameByHandleW(hFile, dest, max_path_utf16_length, FILE_NAME_NORMALIZED);
CloseHandle(hFile);

if (!dwChars || dwChars >= GIT_WIN_PATH_UTF16)
if (!dwChars || dwChars >= max_path_utf16_length)
return -1;

/* The path may be delivered to us with a namespace prefix; remove */
Expand Down Expand Up @@ -779,14 +786,17 @@ int p_rmdir(const char* path)
char *p_realpath(const char *orig_path, char *buffer)
{
git_win32_path orig_path_w, buffer_w;
DWORD max_path_utf16_length = git_win32_longpaths_support
? GIT_WIN_PATH_UTF16
: GIT_WIN_SHORT_PATH_UTF16;

if (git_win32_path_from_utf8(orig_path_w, orig_path) < 0)
return NULL;

/* Note that if the path provided is a relative path, then the current directory
* is used to resolve the path -- which is a concurrency issue because the current
* directory is a process-wide variable. */
if (!GetFullPathNameW(orig_path_w, GIT_WIN_PATH_UTF16, buffer_w, NULL)) {
if (!GetFullPathNameW(orig_path_w, max_path_utf16_length, buffer_w, NULL)) {
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
errno = ENAMETOOLONG;
else
Expand Down
29 changes: 20 additions & 9 deletions src/win32/w32_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,35 @@
#ifndef INCLUDE_win32_w32_common_h__
#define INCLUDE_win32_w32_common_h__

#include <git2/common.h>

/*
* 4096 is the max allowed Git path. `MAX_PATH` (260) is the typical max allowed
* Windows path length, however win32 Unicode APIs generally allow up to 32,767
* if prefixed with "\\?\" (i.e. converted to an NT-style name).
*/
#define WIN_GIT_PATH_MAX GIT_PATH_MAX
#define WIN_GIT_SHORT_PATH_MAX MAX_PATH

/*
* Provides a large enough buffer to support Windows paths: MAX_PATH is
* 260, corresponding to a maximum path length of 259 characters plus a
* NULL terminator. Prefixing with "\\?\" adds 4 characters, but if the
* original was a UNC path, then we turn "\\server\share" into
* Provides a large enough buffer to support Windows Git paths:
* WIN_GIT_PATH_MAX is 4096, corresponding to a maximum path length of 4095
* characters plus a NULL terminator. Prefixing with "\\?\" adds 4 characters,
* but if the original was a UNC path, then we turn "\\server\share" into
* "\\?\UNC\server\share". So we replace the first two characters with
* 8 characters, a net gain of 6, so the maximum length is MAX_PATH+6.
* 8 characters, a net gain of 6, so the maximum length is WIN_GIT_PATH_MAX + 6.
*/
#define GIT_WIN_PATH_UTF16 MAX_PATH+6
#define GIT_WIN_PATH_UTF16 WIN_GIT_PATH_MAX + 6
#define GIT_WIN_SHORT_PATH_UTF16 WIN_GIT_SHORT_PATH_MAX + 6

/* Maximum size of a UTF-8 Win32 path. We remove the "\\?\" or "\\?\UNC\"
* prefixes for presentation, bringing us back to 259 (non-NULL)
/* Maximum size of a UTF-8 Win32 Git path. We remove the "\\?\" or "\\?\UNC\"
* prefixes for presentation, bringing us back to 4095 (non-NULL)
* characters. UTF-8 does have 4-byte sequences, but they are encoded in
* UTF-16 using surrogate pairs, which takes up the space of two characters.
* Two characters in the range U+0800 -> U+FFFF take up more space in UTF-8
* (6 bytes) than one surrogate pair (4 bytes).
*/
#define GIT_WIN_PATH_UTF8 (259 * 3 + 1)
#define GIT_WIN_PATH_UTF8 ((WIN_GIT_PATH_MAX - 1) * 3 + 1)

/*
* The length of a Windows "shortname", for 8.3 compatibility.
Expand Down