Skip to content

Commit 3b04488

Browse files
kbleesGit for Windows Build Agent
authored andcommitted
mingw: support long paths
Windows paths are typically limited to MAX_PATH = 260 characters, even though the underlying NTFS file system supports paths up to 32,767 chars. This limitation is also evident in Windows Explorer, cmd.exe and many other applications (including IDEs). Particularly annoying is that most Windows APIs return bogus error codes if a relative path only barely exceeds MAX_PATH in conjunction with the current directory, e.g. ERROR_PATH_NOT_FOUND / ENOENT instead of the infinitely more helpful ERROR_FILENAME_EXCED_RANGE / ENAMETOOLONG. Many Windows wide char APIs support longer than MAX_PATH paths through the file namespace prefix ('\\?\' or '\\?\UNC\') followed by an absolute path. Notable exceptions include functions dealing with executables and the current directory (CreateProcess, LoadLibrary, Get/SetCurrentDirectory) as well as the entire shell API (ShellExecute, SHGetSpecialFolderPath...). Introduce a handle_long_path function to check the length of a specified path properly (and fail with ENAMETOOLONG), and to optionally expand long paths using the '\\?\' file namespace prefix. Short paths will not be modified, so we don't need to worry about device names (NUL, CON, AUX). Contrary to MSDN docs, the GetFullPathNameW function doesn't seem to be limited to MAX_PATH (at least not on Win7), so we can use it to do the heavy lifting of the conversion (translate '/' to '\', eliminate '.' and '..', and make an absolute path). Add long path error checking to xutftowcs_path for APIs with hard MAX_PATH limit. Add a new MAX_LONG_PATH constant and xutftowcs_long_path function for APIs that support long paths. While improved error checking is always active, long paths support must be explicitly enabled via 'core.longpaths' option. This is to prevent end users to shoot themselves in the foot by checking out files that Windows Explorer, cmd/bash or their favorite IDE cannot handle. Test suite: Test the case is when the full pathname length of a dir is close to 260 (MAX_PATH). Bug report and an original reproducer by Andrey Rogozhnikov: msysgit#122 (comment) [jes: adjusted test number to avoid conflicts, added support for chdir(), etc] Thanks-to: Martin W. Kirst <[email protected]> Thanks-to: Doug Kelly <[email protected]> Original-test-by: Andrey Rogozhnikov <[email protected]> Signed-off-by: Karsten Blees <[email protected]> Signed-off-by: Stepan Kasal <[email protected]> Signed-off-by: Johannes Schindelin <[email protected]>
1 parent 3924467 commit 3b04488

File tree

8 files changed

+349
-67
lines changed

8 files changed

+349
-67
lines changed

Documentation/config/core.adoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,13 @@ core.fscache::
696696
Git for Windows uses this to bulk-read and cache lstat data of entire
697697
directories (instead of doing lstat file by file).
698698

699+
core.longpaths::
700+
Enable long path (> 260) support for builtin commands in Git for
701+
Windows. This is disabled by default, as long paths are not supported
702+
by Windows Explorer, cmd.exe and the Git for Windows tool chain
703+
(msys, bash, tcl, perl...). Only enable this if you know what you're
704+
doing and are prepared to live with a few quirks.
705+
699706
core.unsetenvvars::
700707
Windows-only: comma-separated list of environment variables'
701708
names that need to be unset before spawning any other process.

compat/mingw.c

Lines changed: 137 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,27 @@ static enum hide_dotfiles_type hide_dotfiles = HIDE_DOTFILES_DOTGITONLY;
251251
static char *unset_environment_variables;
252252
int core_fscache;
253253

254+
int are_long_paths_enabled(void)
255+
{
256+
/* default to `false` during initialization */
257+
static const int fallback = 0;
258+
259+
static int enabled = -1;
260+
261+
if (enabled < 0) {
262+
/* avoid infinite recursion */
263+
if (!the_repository)
264+
return fallback;
265+
266+
if (the_repository->config &&
267+
the_repository->config->hash_initialized &&
268+
git_config_get_bool("core.longpaths", &enabled) < 0)
269+
enabled = 0;
270+
}
271+
272+
return enabled < 0 ? fallback : enabled;
273+
}
274+
254275
int mingw_core_config(const char *var, const char *value,
255276
const struct config_context *ctx UNUSED,
256277
void *cb UNUSED)
@@ -307,8 +328,8 @@ static wchar_t *normalize_ntpath(wchar_t *wbuf)
307328
int mingw_unlink(const char *pathname, int handle_in_use_error)
308329
{
309330
int ret, tries = 0;
310-
wchar_t wpathname[MAX_PATH];
311-
if (xutftowcs_path(wpathname, pathname) < 0)
331+
wchar_t wpathname[MAX_LONG_PATH];
332+
if (xutftowcs_long_path(wpathname, pathname) < 0)
312333
return -1;
313334

314335
if (DeleteFileW(wpathname))
@@ -343,7 +364,7 @@ static int is_dir_empty(const wchar_t *wpath)
343364
{
344365
WIN32_FIND_DATAW findbuf;
345366
HANDLE handle;
346-
wchar_t wbuf[MAX_PATH + 2];
367+
wchar_t wbuf[MAX_LONG_PATH + 2];
347368
wcscpy(wbuf, wpath);
348369
wcscat(wbuf, L"\\*");
349370
handle = FindFirstFileW(wbuf, &findbuf);
@@ -364,7 +385,7 @@ static int is_dir_empty(const wchar_t *wpath)
364385
int mingw_rmdir(const char *pathname)
365386
{
366387
int ret, tries = 0;
367-
wchar_t wpathname[MAX_PATH];
388+
wchar_t wpathname[MAX_LONG_PATH];
368389
struct stat st;
369390

370391
/*
@@ -386,7 +407,7 @@ int mingw_rmdir(const char *pathname)
386407
return -1;
387408
}
388409

389-
if (xutftowcs_path(wpathname, pathname) < 0)
410+
if (xutftowcs_long_path(wpathname, pathname) < 0)
390411
return -1;
391412

392413
while ((ret = _wrmdir(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {
@@ -465,15 +486,18 @@ static int set_hidden_flag(const wchar_t *path, int set)
465486
int mingw_mkdir(const char *path, int mode UNUSED)
466487
{
467488
int ret;
468-
wchar_t wpath[MAX_PATH];
489+
wchar_t wpath[MAX_LONG_PATH];
469490

470491
if (!is_valid_win32_path(path, 0)) {
471492
errno = EINVAL;
472493
return -1;
473494
}
474495

475-
if (xutftowcs_path(wpath, path) < 0)
496+
/* CreateDirectoryW path limit is 248 (MAX_PATH - 8.3 file name) */
497+
if (xutftowcs_path_ex(wpath, path, MAX_LONG_PATH, -1, 248,
498+
are_long_paths_enabled()) < 0)
476499
return -1;
500+
477501
ret = _wmkdir(wpath);
478502
if (!ret && needs_hiding(path))
479503
return set_hidden_flag(wpath, 1);
@@ -637,7 +661,7 @@ int mingw_open (const char *filename, int oflags, ...)
637661
va_list args;
638662
unsigned mode;
639663
int fd, create = (oflags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL);
640-
wchar_t wfilename[MAX_PATH];
664+
wchar_t wfilename[MAX_LONG_PATH];
641665
open_fn_t open_fn;
642666

643667
DECLARE_PROC_ADDR(ntdll.dll, NTSTATUS, NTAPI, RtlGetLastNtStatus, void);
@@ -669,7 +693,7 @@ int mingw_open (const char *filename, int oflags, ...)
669693

670694
if (filename && !strcmp(filename, "/dev/null"))
671695
wcscpy(wfilename, L"nul");
672-
else if (xutftowcs_path(wfilename, filename) < 0)
696+
else if (xutftowcs_long_path(wfilename, filename) < 0)
673697
return -1;
674698

675699
fd = open_fn(wfilename, oflags, mode);
@@ -742,14 +766,14 @@ FILE *mingw_fopen (const char *filename, const char *otype)
742766
{
743767
int hide = needs_hiding(filename);
744768
FILE *file;
745-
wchar_t wfilename[MAX_PATH], wotype[4];
769+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
746770
if (filename && !strcmp(filename, "/dev/null"))
747771
wcscpy(wfilename, L"nul");
748772
else if (!is_valid_win32_path(filename, 1)) {
749773
int create = otype && strchr(otype, 'w');
750774
errno = create ? EINVAL : ENOENT;
751775
return NULL;
752-
} else if (xutftowcs_path(wfilename, filename) < 0)
776+
} else if (xutftowcs_long_path(wfilename, filename) < 0)
753777
return NULL;
754778

755779
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -771,14 +795,14 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
771795
{
772796
int hide = needs_hiding(filename);
773797
FILE *file;
774-
wchar_t wfilename[MAX_PATH], wotype[4];
798+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
775799
if (filename && !strcmp(filename, "/dev/null"))
776800
wcscpy(wfilename, L"nul");
777801
else if (!is_valid_win32_path(filename, 1)) {
778802
int create = otype && strchr(otype, 'w');
779803
errno = create ? EINVAL : ENOENT;
780804
return NULL;
781-
} else if (xutftowcs_path(wfilename, filename) < 0)
805+
} else if (xutftowcs_long_path(wfilename, filename) < 0)
782806
return NULL;
783807

784808
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -828,7 +852,7 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
828852
HANDLE h = (HANDLE) _get_osfhandle(fd);
829853
if (GetFileType(h) != FILE_TYPE_PIPE) {
830854
if (orig == EINVAL) {
831-
wchar_t path[MAX_PATH];
855+
wchar_t path[MAX_LONG_PATH];
832856
DWORD ret = GetFinalPathNameByHandleW(h, path,
833857
ARRAY_SIZE(path), 0);
834858
UINT drive_type = ret > 0 && ret < ARRAY_SIZE(path) ?
@@ -865,27 +889,33 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
865889

866890
int mingw_access(const char *filename, int mode)
867891
{
868-
wchar_t wfilename[MAX_PATH];
892+
wchar_t wfilename[MAX_LONG_PATH];
869893
if (!strcmp("nul", filename) || !strcmp("/dev/null", filename))
870894
return 0;
871-
if (xutftowcs_path(wfilename, filename) < 0)
895+
if (xutftowcs_long_path(wfilename, filename) < 0)
872896
return -1;
873897
/* X_OK is not supported by the MSVCRT version */
874898
return _waccess(wfilename, mode & ~X_OK);
875899
}
876900

901+
/* cached length of current directory for handle_long_path */
902+
static int current_directory_len = 0;
903+
877904
int mingw_chdir(const char *dirname)
878905
{
879-
wchar_t wdirname[MAX_PATH];
880-
if (xutftowcs_path(wdirname, dirname) < 0)
906+
int result;
907+
wchar_t wdirname[MAX_LONG_PATH];
908+
if (xutftowcs_long_path(wdirname, dirname) < 0)
881909
return -1;
882-
return _wchdir(wdirname);
910+
result = _wchdir(wdirname);
911+
current_directory_len = GetCurrentDirectoryW(0, NULL);
912+
return result;
883913
}
884914

885915
int mingw_chmod(const char *filename, int mode)
886916
{
887-
wchar_t wfilename[MAX_PATH];
888-
if (xutftowcs_path(wfilename, filename) < 0)
917+
wchar_t wfilename[MAX_LONG_PATH];
918+
if (xutftowcs_long_path(wfilename, filename) < 0)
889919
return -1;
890920
return _wchmod(wfilename, mode);
891921
}
@@ -933,8 +963,8 @@ static int has_valid_directory_prefix(wchar_t *wfilename)
933963
static int do_lstat(int follow, const char *file_name, struct stat *buf)
934964
{
935965
WIN32_FILE_ATTRIBUTE_DATA fdata;
936-
wchar_t wfilename[MAX_PATH];
937-
if (xutftowcs_path(wfilename, file_name) < 0)
966+
wchar_t wfilename[MAX_LONG_PATH];
967+
if (xutftowcs_long_path(wfilename, file_name) < 0)
938968
return -1;
939969

940970
if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
@@ -1105,10 +1135,10 @@ int mingw_utime (const char *file_name, const struct utimbuf *times)
11051135
FILETIME mft, aft;
11061136
int rc;
11071137
DWORD attrs;
1108-
wchar_t wfilename[MAX_PATH];
1138+
wchar_t wfilename[MAX_LONG_PATH];
11091139
HANDLE osfilehandle;
11101140

1111-
if (xutftowcs_path(wfilename, file_name) < 0)
1141+
if (xutftowcs_long_path(wfilename, file_name) < 0)
11121142
return -1;
11131143

11141144
/* must have write permission */
@@ -1191,6 +1221,7 @@ char *mingw_mktemp(char *template)
11911221
wchar_t wtemplate[MAX_PATH];
11921222
int offset = 0;
11931223

1224+
/* we need to return the path, thus no long paths here! */
11941225
if (xutftowcs_path(wtemplate, template) < 0)
11951226
return NULL;
11961227

@@ -1832,6 +1863,10 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen
18321863

18331864
if (*argv && !strcmp(cmd, *argv))
18341865
wcmd[0] = L'\0';
1866+
/*
1867+
* Paths to executables and to the current directory do not support
1868+
* long paths, therefore we cannot use xutftowcs_long_path() here.
1869+
*/
18351870
else if (xutftowcs_path(wcmd, cmd) < 0)
18361871
return -1;
18371872
if (dir && xutftowcs_path(wdir, dir) < 0)
@@ -2521,12 +2556,12 @@ int mingw_rename(const char *pold, const char *pnew)
25212556
static int supports_file_rename_info_ex = 1;
25222557
DWORD attrs, gle;
25232558
int tries = 0;
2524-
wchar_t wpold[MAX_PATH], wpnew[MAX_PATH];
2559+
wchar_t wpold[MAX_LONG_PATH], wpnew[MAX_LONG_PATH];
25252560
int wpnew_len;
25262561

2527-
if (xutftowcs_path(wpold, pold) < 0)
2562+
if (xutftowcs_long_path(wpold, pold) < 0)
25282563
return -1;
2529-
wpnew_len = xutftowcs_path(wpnew, pnew);
2564+
wpnew_len = xutftowcs_long_path(wpnew, pnew);
25302565
if (wpnew_len < 0)
25312566
return -1;
25322567

@@ -2565,9 +2600,9 @@ int mingw_rename(const char *pold, const char *pnew)
25652600
* flex array so that the structure has to be allocated on
25662601
* the heap. As we declare this structure ourselves though
25672602
* we can avoid the allocation and define FileName to have
2568-
* MAX_PATH bytes.
2603+
* MAX_LONG_PATH bytes.
25692604
*/
2570-
WCHAR FileName[MAX_PATH];
2605+
WCHAR FileName[MAX_LONG_PATH];
25712606
} rename_info = { 0 };
25722607
HANDLE old_handle = INVALID_HANDLE_VALUE;
25732608
BOOL success;
@@ -2926,9 +2961,9 @@ int mingw_raise(int sig)
29262961

29272962
int link(const char *oldpath, const char *newpath)
29282963
{
2929-
wchar_t woldpath[MAX_PATH], wnewpath[MAX_PATH];
2930-
if (xutftowcs_path(woldpath, oldpath) < 0 ||
2931-
xutftowcs_path(wnewpath, newpath) < 0)
2964+
wchar_t woldpath[MAX_LONG_PATH], wnewpath[MAX_LONG_PATH];
2965+
if (xutftowcs_long_path(woldpath, oldpath) < 0 ||
2966+
xutftowcs_long_path(wnewpath, newpath) < 0)
29322967
return -1;
29332968

29342969
if (!CreateHardLinkW(wnewpath, woldpath, NULL)) {
@@ -2996,8 +3031,8 @@ int mingw_is_mount_point(struct strbuf *path)
29963031
{
29973032
WIN32_FIND_DATAW findbuf = { 0 };
29983033
HANDLE handle;
2999-
wchar_t wfilename[MAX_PATH];
3000-
int wlen = xutftowcs_path(wfilename, path->buf);
3034+
wchar_t wfilename[MAX_LONG_PATH];
3035+
int wlen = xutftowcs_long_path(wfilename, path->buf);
30013036
if (wlen < 0)
30023037
die(_("could not get long path for '%s'"), path->buf);
30033038

@@ -3149,9 +3184,9 @@ static size_t append_system_bin_dirs(char *path, size_t size)
31493184

31503185
static int is_system32_path(const char *path)
31513186
{
3152-
WCHAR system32[MAX_PATH], wpath[MAX_PATH];
3187+
WCHAR system32[MAX_LONG_PATH], wpath[MAX_LONG_PATH];
31533188

3154-
if (xutftowcs_path(wpath, path) < 0 ||
3189+
if (xutftowcs_long_path(wpath, path) < 0 ||
31553190
!GetSystemDirectoryW(system32, ARRAY_SIZE(system32)) ||
31563191
_wcsicmp(system32, wpath))
31573192
return 0;
@@ -3584,6 +3619,68 @@ int is_valid_win32_path(const char *path, int allow_literal_nul)
35843619
}
35853620
}
35863621

3622+
int handle_long_path(wchar_t *path, int len, int max_path, int expand)
3623+
{
3624+
int result;
3625+
wchar_t buf[MAX_LONG_PATH];
3626+
3627+
/*
3628+
* we don't need special handling if path is relative to the current
3629+
* directory, and current directory + path don't exceed the desired
3630+
* max_path limit. This should cover > 99 % of cases with minimal
3631+
* performance impact (git almost always uses relative paths).
3632+
*/
3633+
if ((len < 2 || (!is_dir_sep(path[0]) && path[1] != ':')) &&
3634+
(current_directory_len + len < max_path))
3635+
return len;
3636+
3637+
/*
3638+
* handle everything else:
3639+
* - absolute paths: "C:\dir\file"
3640+
* - absolute UNC paths: "\\server\share\dir\file"
3641+
* - absolute paths on current drive: "\dir\file"
3642+
* - relative paths on other drive: "X:file"
3643+
* - prefixed paths: "\\?\...", "\\.\..."
3644+
*/
3645+
3646+
/* convert to absolute path using GetFullPathNameW */
3647+
result = GetFullPathNameW(path, MAX_LONG_PATH, buf, NULL);
3648+
if (!result) {
3649+
errno = err_win_to_posix(GetLastError());
3650+
return -1;
3651+
}
3652+
3653+
/*
3654+
* return absolute path if it fits within max_path (even if
3655+
* "cwd + path" doesn't due to '..' components)
3656+
*/
3657+
if (result < max_path) {
3658+
wcscpy(path, buf);
3659+
return result;
3660+
}
3661+
3662+
/* error out if we shouldn't expand the path or buf is too small */
3663+
if (!expand || result >= MAX_LONG_PATH - 6) {
3664+
errno = ENAMETOOLONG;
3665+
return -1;
3666+
}
3667+
3668+
/* prefix full path with "\\?\" or "\\?\UNC\" */
3669+
if (buf[0] == '\\') {
3670+
/* ...unless already prefixed */
3671+
if (buf[1] == '\\' && (buf[2] == '?' || buf[2] == '.'))
3672+
return len;
3673+
3674+
wcscpy(path, L"\\\\?\\UNC\\");
3675+
wcscpy(path + 8, buf + 2);
3676+
return result + 6;
3677+
} else {
3678+
wcscpy(path, L"\\\\?\\");
3679+
wcscpy(path + 4, buf);
3680+
return result + 4;
3681+
}
3682+
}
3683+
35873684
#if !defined(_MSC_VER)
35883685
/*
35893686
* Disable MSVCRT command line wildcard expansion (__getmainargs called from
@@ -3746,6 +3843,9 @@ int wmain(int argc, const wchar_t **wargv)
37463843
/* initialize Unicode console */
37473844
winansi_init();
37483845

3846+
/* init length of current directory for handle_long_path */
3847+
current_directory_len = GetCurrentDirectoryW(0, NULL);
3848+
37493849
/* invoke the real main() using our utf8 version of argv. */
37503850
exit_status = main(argc, argv);
37513851

0 commit comments

Comments
 (0)