Skip to content

Commit c9cc20a

Browse files
kbleesdscho
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 749611c commit c9cc20a

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
@@ -252,6 +252,27 @@ static enum hide_dotfiles_type hide_dotfiles = HIDE_DOTFILES_DOTGITONLY;
252252
static char *unset_environment_variables;
253253
int core_fscache;
254254

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

315336
if (DeleteFileW(wpathname))
@@ -344,7 +365,7 @@ static int is_dir_empty(const wchar_t *wpath)
344365
{
345366
WIN32_FIND_DATAW findbuf;
346367
HANDLE handle;
347-
wchar_t wbuf[MAX_PATH + 2];
368+
wchar_t wbuf[MAX_LONG_PATH + 2];
348369
wcscpy(wbuf, wpath);
349370
wcscat(wbuf, L"\\*");
350371
handle = FindFirstFileW(wbuf, &findbuf);
@@ -365,7 +386,7 @@ static int is_dir_empty(const wchar_t *wpath)
365386
int mingw_rmdir(const char *pathname)
366387
{
367388
int ret, tries = 0;
368-
wchar_t wpathname[MAX_PATH];
389+
wchar_t wpathname[MAX_LONG_PATH];
369390
struct stat st;
370391

371392
/*
@@ -387,7 +408,7 @@ int mingw_rmdir(const char *pathname)
387408
return -1;
388409
}
389410

390-
if (xutftowcs_path(wpathname, pathname) < 0)
411+
if (xutftowcs_long_path(wpathname, pathname) < 0)
391412
return -1;
392413

393414
while ((ret = _wrmdir(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {
@@ -466,15 +487,18 @@ static int set_hidden_flag(const wchar_t *path, int set)
466487
int mingw_mkdir(const char *path, int mode UNUSED)
467488
{
468489
int ret;
469-
wchar_t wpath[MAX_PATH];
490+
wchar_t wpath[MAX_LONG_PATH];
470491

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

476-
if (xutftowcs_path(wpath, path) < 0)
497+
/* CreateDirectoryW path limit is 248 (MAX_PATH - 8.3 file name) */
498+
if (xutftowcs_path_ex(wpath, path, MAX_LONG_PATH, -1, 248,
499+
are_long_paths_enabled()) < 0)
477500
return -1;
501+
478502
ret = _wmkdir(wpath);
479503
if (!ret && needs_hiding(path))
480504
return set_hidden_flag(wpath, 1);
@@ -636,7 +660,7 @@ int mingw_open (const char *filename, int oflags, ...)
636660
va_list args;
637661
unsigned mode;
638662
int fd, create = (oflags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL);
639-
wchar_t wfilename[MAX_PATH];
663+
wchar_t wfilename[MAX_LONG_PATH];
640664
open_fn_t open_fn;
641665

642666
DECLARE_PROC_ADDR(ntdll.dll, NTSTATUS, NTAPI, RtlGetLastNtStatus, void);
@@ -668,7 +692,7 @@ int mingw_open (const char *filename, int oflags, ...)
668692

669693
if (filename && !strcmp(filename, "/dev/null"))
670694
wcscpy(wfilename, L"nul");
671-
else if (xutftowcs_path(wfilename, filename) < 0)
695+
else if (xutftowcs_long_path(wfilename, filename) < 0)
672696
return -1;
673697

674698
fd = open_fn(wfilename, oflags, mode);
@@ -741,14 +765,14 @@ FILE *mingw_fopen (const char *filename, const char *otype)
741765
{
742766
int hide = needs_hiding(filename);
743767
FILE *file;
744-
wchar_t wfilename[MAX_PATH], wotype[4];
768+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
745769
if (filename && !strcmp(filename, "/dev/null"))
746770
wcscpy(wfilename, L"nul");
747771
else if (!is_valid_win32_path(filename, 1)) {
748772
int create = otype && strchr(otype, 'w');
749773
errno = create ? EINVAL : ENOENT;
750774
return NULL;
751-
} else if (xutftowcs_path(wfilename, filename) < 0)
775+
} else if (xutftowcs_long_path(wfilename, filename) < 0)
752776
return NULL;
753777

754778
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -770,14 +794,14 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
770794
{
771795
int hide = needs_hiding(filename);
772796
FILE *file;
773-
wchar_t wfilename[MAX_PATH], wotype[4];
797+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
774798
if (filename && !strcmp(filename, "/dev/null"))
775799
wcscpy(wfilename, L"nul");
776800
else if (!is_valid_win32_path(filename, 1)) {
777801
int create = otype && strchr(otype, 'w');
778802
errno = create ? EINVAL : ENOENT;
779803
return NULL;
780-
} else if (xutftowcs_path(wfilename, filename) < 0)
804+
} else if (xutftowcs_long_path(wfilename, filename) < 0)
781805
return NULL;
782806

783807
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -827,7 +851,7 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
827851
HANDLE h = (HANDLE) _get_osfhandle(fd);
828852
if (GetFileType(h) != FILE_TYPE_PIPE) {
829853
if (orig == EINVAL) {
830-
wchar_t path[MAX_PATH];
854+
wchar_t path[MAX_LONG_PATH];
831855
DWORD ret = GetFinalPathNameByHandleW(h, path,
832856
ARRAY_SIZE(path), 0);
833857
UINT drive_type = ret > 0 && ret < ARRAY_SIZE(path) ?
@@ -864,27 +888,33 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
864888

865889
int mingw_access(const char *filename, int mode)
866890
{
867-
wchar_t wfilename[MAX_PATH];
891+
wchar_t wfilename[MAX_LONG_PATH];
868892
if (!strcmp("nul", filename) || !strcmp("/dev/null", filename))
869893
return 0;
870-
if (xutftowcs_path(wfilename, filename) < 0)
894+
if (xutftowcs_long_path(wfilename, filename) < 0)
871895
return -1;
872896
/* X_OK is not supported by the MSVCRT version */
873897
return _waccess(wfilename, mode & ~X_OK);
874898
}
875899

900+
/* cached length of current directory for handle_long_path */
901+
static int current_directory_len = 0;
902+
876903
int mingw_chdir(const char *dirname)
877904
{
878-
wchar_t wdirname[MAX_PATH];
879-
if (xutftowcs_path(wdirname, dirname) < 0)
905+
int result;
906+
wchar_t wdirname[MAX_LONG_PATH];
907+
if (xutftowcs_long_path(wdirname, dirname) < 0)
880908
return -1;
881-
return _wchdir(wdirname);
909+
result = _wchdir(wdirname);
910+
current_directory_len = GetCurrentDirectoryW(0, NULL);
911+
return result;
882912
}
883913

884914
int mingw_chmod(const char *filename, int mode)
885915
{
886-
wchar_t wfilename[MAX_PATH];
887-
if (xutftowcs_path(wfilename, filename) < 0)
916+
wchar_t wfilename[MAX_LONG_PATH];
917+
if (xutftowcs_long_path(wfilename, filename) < 0)
888918
return -1;
889919
return _wchmod(wfilename, mode);
890920
}
@@ -932,8 +962,8 @@ static int has_valid_directory_prefix(wchar_t *wfilename)
932962
static int do_lstat(int follow, const char *file_name, struct stat *buf)
933963
{
934964
WIN32_FILE_ATTRIBUTE_DATA fdata;
935-
wchar_t wfilename[MAX_PATH];
936-
if (xutftowcs_path(wfilename, file_name) < 0)
965+
wchar_t wfilename[MAX_LONG_PATH];
966+
if (xutftowcs_long_path(wfilename, file_name) < 0)
937967
return -1;
938968

939969
if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
@@ -1104,10 +1134,10 @@ int mingw_utime (const char *file_name, const struct utimbuf *times)
11041134
FILETIME mft, aft;
11051135
int rc;
11061136
DWORD attrs;
1107-
wchar_t wfilename[MAX_PATH];
1137+
wchar_t wfilename[MAX_LONG_PATH];
11081138
HANDLE osfilehandle;
11091139

1110-
if (xutftowcs_path(wfilename, file_name) < 0)
1140+
if (xutftowcs_long_path(wfilename, file_name) < 0)
11111141
return -1;
11121142

11131143
/* must have write permission */
@@ -1190,6 +1220,7 @@ char *mingw_mktemp(char *template)
11901220
wchar_t wtemplate[MAX_PATH];
11911221
int offset = 0;
11921222

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

@@ -1831,6 +1862,10 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen
18311862

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

2526-
if (xutftowcs_path(wpold, pold) < 0)
2561+
if (xutftowcs_long_path(wpold, pold) < 0)
25272562
return -1;
2528-
wpnew_len = xutftowcs_path(wpnew, pnew);
2563+
wpnew_len = xutftowcs_long_path(wpnew, pnew);
25292564
if (wpnew_len < 0)
25302565
return -1;
25312566

@@ -2564,9 +2599,9 @@ int mingw_rename(const char *pold, const char *pnew)
25642599
* flex array so that the structure has to be allocated on
25652600
* the heap. As we declare this structure ourselves though
25662601
* we can avoid the allocation and define FileName to have
2567-
* MAX_PATH bytes.
2602+
* MAX_LONG_PATH bytes.
25682603
*/
2569-
WCHAR FileName[MAX_PATH];
2604+
WCHAR FileName[MAX_LONG_PATH];
25702605
} rename_info = { 0 };
25712606
HANDLE old_handle = INVALID_HANDLE_VALUE;
25722607
BOOL success;
@@ -2919,9 +2954,9 @@ int mingw_raise(int sig)
29192954

29202955
int link(const char *oldpath, const char *newpath)
29212956
{
2922-
wchar_t woldpath[MAX_PATH], wnewpath[MAX_PATH];
2923-
if (xutftowcs_path(woldpath, oldpath) < 0 ||
2924-
xutftowcs_path(wnewpath, newpath) < 0)
2957+
wchar_t woldpath[MAX_LONG_PATH], wnewpath[MAX_LONG_PATH];
2958+
if (xutftowcs_long_path(woldpath, oldpath) < 0 ||
2959+
xutftowcs_long_path(wnewpath, newpath) < 0)
29252960
return -1;
29262961

29272962
if (!CreateHardLinkW(wnewpath, woldpath, NULL)) {
@@ -2989,8 +3024,8 @@ int mingw_is_mount_point(struct strbuf *path)
29893024
{
29903025
WIN32_FIND_DATAW findbuf = { 0 };
29913026
HANDLE handle;
2992-
wchar_t wfilename[MAX_PATH];
2993-
int wlen = xutftowcs_path(wfilename, path->buf);
3027+
wchar_t wfilename[MAX_LONG_PATH];
3028+
int wlen = xutftowcs_long_path(wfilename, path->buf);
29943029
if (wlen < 0)
29953030
die(_("could not get long path for '%s'"), path->buf);
29963031

@@ -3142,9 +3177,9 @@ static size_t append_system_bin_dirs(char *path, size_t size)
31423177

31433178
static int is_system32_path(const char *path)
31443179
{
3145-
WCHAR system32[MAX_PATH], wpath[MAX_PATH];
3180+
WCHAR system32[MAX_LONG_PATH], wpath[MAX_LONG_PATH];
31463181

3147-
if (xutftowcs_path(wpath, path) < 0 ||
3182+
if (xutftowcs_long_path(wpath, path) < 0 ||
31483183
!GetSystemDirectoryW(system32, ARRAY_SIZE(system32)) ||
31493184
_wcsicmp(system32, wpath))
31503185
return 0;
@@ -3577,6 +3612,68 @@ int is_valid_win32_path(const char *path, int allow_literal_nul)
35773612
}
35783613
}
35793614

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

3839+
/* init length of current directory for handle_long_path */
3840+
current_directory_len = GetCurrentDirectoryW(0, NULL);
3841+
37423842
/* invoke the real main() using our utf8 version of argv. */
37433843
exit_status = main(argc, argv);
37443844

0 commit comments

Comments
 (0)