Skip to content

Commit 438e0e7

Browse files
kbleesvdye
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 <maki@bitkings.de> Thanks-to: Doug Kelly <dougk.ff7@gmail.com> Original-test-by: Andrey Rogozhnikov <rogozhnikov.andrey@gmail.com> Signed-off-by: Karsten Blees <blees@dcon.de> Signed-off-by: Stepan Kasal <kasal@ucw.cz> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
1 parent e5a31e4 commit 438e0e7

File tree

7 files changed

+329
-63
lines changed

7 files changed

+329
-63
lines changed

Documentation/config/core.txt

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

664+
core.longpaths::
665+
Enable long path (> 260) support for builtin commands in Git for
666+
Windows. This is disabled by default, as long paths are not supported
667+
by Windows Explorer, cmd.exe and the Git for Windows tool chain
668+
(msys, bash, tcl, perl...). Only enable this if you know what you're
669+
doing and are prepared to live with a few quirks.
670+
664671
core.unsetenvvars::
665672
Windows-only: comma-separated list of environment variables'
666673
names that need to be unset before spawning any other process.

compat/mingw.c

Lines changed: 119 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ static int core_restrict_inherited_handles = -1;
234234
static enum hide_dotfiles_type hide_dotfiles = HIDE_DOTFILES_DOTGITONLY;
235235
static char *unset_environment_variables;
236236
int core_fscache;
237+
int core_long_paths;
237238

238239
int mingw_core_config(const char *var, const char *value, void *cb)
239240
{
@@ -250,6 +251,11 @@ int mingw_core_config(const char *var, const char *value, void *cb)
250251
return 0;
251252
}
252253

254+
if (!strcmp(var, "core.longpaths")) {
255+
core_long_paths = git_config_bool(var, value);
256+
return 0;
257+
}
258+
253259
if (!strcmp(var, "core.unsetenvvars")) {
254260
free(unset_environment_variables);
255261
unset_environment_variables = xstrdup(value);
@@ -296,8 +302,8 @@ static wchar_t *normalize_ntpath(wchar_t *wbuf)
296302
int mingw_unlink(const char *pathname)
297303
{
298304
int ret, tries = 0;
299-
wchar_t wpathname[MAX_PATH];
300-
if (xutftowcs_path(wpathname, pathname) < 0)
305+
wchar_t wpathname[MAX_LONG_PATH];
306+
if (xutftowcs_long_path(wpathname, pathname) < 0)
301307
return -1;
302308

303309
if (DeleteFileW(wpathname))
@@ -329,7 +335,7 @@ static int is_dir_empty(const wchar_t *wpath)
329335
{
330336
WIN32_FIND_DATAW findbuf;
331337
HANDLE handle;
332-
wchar_t wbuf[MAX_PATH + 2];
338+
wchar_t wbuf[MAX_LONG_PATH + 2];
333339
wcscpy(wbuf, wpath);
334340
wcscat(wbuf, L"\\*");
335341
handle = FindFirstFileW(wbuf, &findbuf);
@@ -350,7 +356,7 @@ static int is_dir_empty(const wchar_t *wpath)
350356
int mingw_rmdir(const char *pathname)
351357
{
352358
int ret, tries = 0;
353-
wchar_t wpathname[MAX_PATH];
359+
wchar_t wpathname[MAX_LONG_PATH];
354360
struct stat st;
355361

356362
/*
@@ -372,7 +378,7 @@ int mingw_rmdir(const char *pathname)
372378
return -1;
373379
}
374380

375-
if (xutftowcs_path(wpathname, pathname) < 0)
381+
if (xutftowcs_long_path(wpathname, pathname) < 0)
376382
return -1;
377383

378384
while ((ret = _wrmdir(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {
@@ -451,15 +457,18 @@ static int set_hidden_flag(const wchar_t *path, int set)
451457
int mingw_mkdir(const char *path, int mode)
452458
{
453459
int ret;
454-
wchar_t wpath[MAX_PATH];
460+
wchar_t wpath[MAX_LONG_PATH];
455461

456462
if (!is_valid_win32_path(path, 0)) {
457463
errno = EINVAL;
458464
return -1;
459465
}
460466

461-
if (xutftowcs_path(wpath, path) < 0)
467+
/* CreateDirectoryW path limit is 248 (MAX_PATH - 8.3 file name) */
468+
if (xutftowcs_path_ex(wpath, path, MAX_LONG_PATH, -1, 248,
469+
core_long_paths) < 0)
462470
return -1;
471+
463472
ret = _wmkdir(wpath);
464473
if (!ret && needs_hiding(path))
465474
return set_hidden_flag(wpath, 1);
@@ -545,7 +554,7 @@ int mingw_open (const char *filename, int oflags, ...)
545554
va_list args;
546555
unsigned mode;
547556
int fd, create = (oflags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL);
548-
wchar_t wfilename[MAX_PATH];
557+
wchar_t wfilename[MAX_LONG_PATH];
549558
open_fn_t open_fn;
550559

551560
va_start(args, oflags);
@@ -564,7 +573,7 @@ int mingw_open (const char *filename, int oflags, ...)
564573

565574
if (filename && !strcmp(filename, "/dev/null"))
566575
wcscpy(wfilename, L"nul");
567-
else if (xutftowcs_path(wfilename, filename) < 0)
576+
else if (xutftowcs_long_path(wfilename, filename) < 0)
568577
return -1;
569578

570579
fd = open_fn(wfilename, oflags, mode);
@@ -622,14 +631,14 @@ FILE *mingw_fopen (const char *filename, const char *otype)
622631
{
623632
int hide = needs_hiding(filename);
624633
FILE *file;
625-
wchar_t wfilename[MAX_PATH], wotype[4];
634+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
626635
if (filename && !strcmp(filename, "/dev/null"))
627636
wcscpy(wfilename, L"nul");
628637
else if (!is_valid_win32_path(filename, 1)) {
629638
int create = otype && strchr(otype, 'w');
630639
errno = create ? EINVAL : ENOENT;
631640
return NULL;
632-
} else if (xutftowcs_path(wfilename, filename) < 0)
641+
} else if (xutftowcs_long_path(wfilename, filename) < 0)
633642
return NULL;
634643

635644
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -651,14 +660,14 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
651660
{
652661
int hide = needs_hiding(filename);
653662
FILE *file;
654-
wchar_t wfilename[MAX_PATH], wotype[4];
663+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
655664
if (filename && !strcmp(filename, "/dev/null"))
656665
wcscpy(wfilename, L"nul");
657666
else if (!is_valid_win32_path(filename, 1)) {
658667
int create = otype && strchr(otype, 'w');
659668
errno = create ? EINVAL : ENOENT;
660669
return NULL;
661-
} else if (xutftowcs_path(wfilename, filename) < 0)
670+
} else if (xutftowcs_long_path(wfilename, filename) < 0)
662671
return NULL;
663672

664673
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -715,27 +724,33 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
715724

716725
int mingw_access(const char *filename, int mode)
717726
{
718-
wchar_t wfilename[MAX_PATH];
727+
wchar_t wfilename[MAX_LONG_PATH];
719728
if (!strcmp("nul", filename) || !strcmp("/dev/null", filename))
720729
return 0;
721-
if (xutftowcs_path(wfilename, filename) < 0)
730+
if (xutftowcs_long_path(wfilename, filename) < 0)
722731
return -1;
723732
/* X_OK is not supported by the MSVCRT version */
724733
return _waccess(wfilename, mode & ~X_OK);
725734
}
726735

736+
/* cached length of current directory for handle_long_path */
737+
static int current_directory_len = 0;
738+
727739
int mingw_chdir(const char *dirname)
728740
{
729-
wchar_t wdirname[MAX_PATH];
730-
if (xutftowcs_path(wdirname, dirname) < 0)
741+
int result;
742+
wchar_t wdirname[MAX_LONG_PATH];
743+
if (xutftowcs_long_path(wdirname, dirname) < 0)
731744
return -1;
732-
return _wchdir(wdirname);
745+
result = _wchdir(wdirname);
746+
current_directory_len = GetCurrentDirectoryW(0, NULL);
747+
return result;
733748
}
734749

735750
int mingw_chmod(const char *filename, int mode)
736751
{
737-
wchar_t wfilename[MAX_PATH];
738-
if (xutftowcs_path(wfilename, filename) < 0)
752+
wchar_t wfilename[MAX_LONG_PATH];
753+
if (xutftowcs_long_path(wfilename, filename) < 0)
739754
return -1;
740755
return _wchmod(wfilename, mode);
741756
}
@@ -783,8 +798,8 @@ static int has_valid_directory_prefix(wchar_t *wfilename)
783798
static int do_lstat(int follow, const char *file_name, struct stat *buf)
784799
{
785800
WIN32_FILE_ATTRIBUTE_DATA fdata;
786-
wchar_t wfilename[MAX_PATH];
787-
if (xutftowcs_path(wfilename, file_name) < 0)
801+
wchar_t wfilename[MAX_LONG_PATH];
802+
if (xutftowcs_long_path(wfilename, file_name) < 0)
788803
return -1;
789804

790805
if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
@@ -955,10 +970,10 @@ int mingw_utime (const char *file_name, const struct utimbuf *times)
955970
FILETIME mft, aft;
956971
int rc;
957972
DWORD attrs;
958-
wchar_t wfilename[MAX_PATH];
973+
wchar_t wfilename[MAX_LONG_PATH];
959974
HANDLE osfilehandle;
960975

961-
if (xutftowcs_path(wfilename, file_name) < 0)
976+
if (xutftowcs_long_path(wfilename, file_name) < 0)
962977
return -1;
963978

964979
/* must have write permission */
@@ -1041,6 +1056,7 @@ char *mingw_mktemp(char *template)
10411056
wchar_t wtemplate[MAX_PATH];
10421057
int offset = 0;
10431058

1059+
/* we need to return the path, thus no long paths here! */
10441060
if (xutftowcs_path(wtemplate, template) < 0)
10451061
return NULL;
10461062

@@ -1678,6 +1694,10 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen
16781694

16791695
if (*argv && !strcmp(cmd, *argv))
16801696
wcmd[0] = L'\0';
1697+
/*
1698+
* Paths to executables and to the current directory do not support
1699+
* long paths, therefore we cannot use xutftowcs_long_path() here.
1700+
*/
16811701
else if (xutftowcs_path(wcmd, cmd) < 0)
16821702
return -1;
16831703
if (dir && xutftowcs_path(wdir, dir) < 0)
@@ -2329,8 +2349,9 @@ int mingw_rename(const char *pold, const char *pnew)
23292349
{
23302350
DWORD attrs, gle;
23312351
int tries = 0;
2332-
wchar_t wpold[MAX_PATH], wpnew[MAX_PATH];
2333-
if (xutftowcs_path(wpold, pold) < 0 || xutftowcs_path(wpnew, pnew) < 0)
2352+
wchar_t wpold[MAX_LONG_PATH], wpnew[MAX_LONG_PATH];
2353+
if (xutftowcs_long_path(wpold, pold) < 0 ||
2354+
xutftowcs_long_path(wpnew, pnew) < 0)
23342355
return -1;
23352356

23362357
/*
@@ -2644,9 +2665,9 @@ int mingw_raise(int sig)
26442665

26452666
int link(const char *oldpath, const char *newpath)
26462667
{
2647-
wchar_t woldpath[MAX_PATH], wnewpath[MAX_PATH];
2648-
if (xutftowcs_path(woldpath, oldpath) < 0 ||
2649-
xutftowcs_path(wnewpath, newpath) < 0)
2668+
wchar_t woldpath[MAX_LONG_PATH], wnewpath[MAX_LONG_PATH];
2669+
if (xutftowcs_long_path(woldpath, oldpath) < 0 ||
2670+
xutftowcs_long_path(wnewpath, newpath) < 0)
26502671
return -1;
26512672

26522673
if (!CreateHardLinkW(wnewpath, woldpath, NULL)) {
@@ -2714,8 +2735,8 @@ int mingw_is_mount_point(struct strbuf *path)
27142735
{
27152736
WIN32_FIND_DATAW findbuf = { 0 };
27162737
HANDLE handle;
2717-
wchar_t wfilename[MAX_PATH];
2718-
int wlen = xutftowcs_path(wfilename, path->buf);
2738+
wchar_t wfilename[MAX_LONG_PATH];
2739+
int wlen = xutftowcs_long_path(wfilename, path->buf);
27192740
if (wlen < 0)
27202741
die(_("could not get long path for '%s'"), path->buf);
27212742

@@ -2860,9 +2881,9 @@ static size_t append_system_bin_dirs(char *path, size_t size)
28602881

28612882
static int is_system32_path(const char *path)
28622883
{
2863-
WCHAR system32[MAX_PATH], wpath[MAX_PATH];
2884+
WCHAR system32[MAX_LONG_PATH], wpath[MAX_LONG_PATH];
28642885

2865-
if (xutftowcs_path(wpath, path) < 0 ||
2886+
if (xutftowcs_long_path(wpath, path) < 0 ||
28662887
!GetSystemDirectoryW(system32, ARRAY_SIZE(system32)) ||
28672888
_wcsicmp(system32, wpath))
28682889
return 0;
@@ -3165,6 +3186,68 @@ int is_valid_win32_path(const char *path, int allow_literal_nul)
31653186
}
31663187
}
31673188

3189+
int handle_long_path(wchar_t *path, int len, int max_path, int expand)
3190+
{
3191+
int result;
3192+
wchar_t buf[MAX_LONG_PATH];
3193+
3194+
/*
3195+
* we don't need special handling if path is relative to the current
3196+
* directory, and current directory + path don't exceed the desired
3197+
* max_path limit. This should cover > 99 % of cases with minimal
3198+
* performance impact (git almost always uses relative paths).
3199+
*/
3200+
if ((len < 2 || (!is_dir_sep(path[0]) && path[1] != ':')) &&
3201+
(current_directory_len + len < max_path))
3202+
return len;
3203+
3204+
/*
3205+
* handle everything else:
3206+
* - absolute paths: "C:\dir\file"
3207+
* - absolute UNC paths: "\\server\share\dir\file"
3208+
* - absolute paths on current drive: "\dir\file"
3209+
* - relative paths on other drive: "X:file"
3210+
* - prefixed paths: "\\?\...", "\\.\..."
3211+
*/
3212+
3213+
/* convert to absolute path using GetFullPathNameW */
3214+
result = GetFullPathNameW(path, MAX_LONG_PATH, buf, NULL);
3215+
if (!result) {
3216+
errno = err_win_to_posix(GetLastError());
3217+
return -1;
3218+
}
3219+
3220+
/*
3221+
* return absolute path if it fits within max_path (even if
3222+
* "cwd + path" doesn't due to '..' components)
3223+
*/
3224+
if (result < max_path) {
3225+
wcscpy(path, buf);
3226+
return result;
3227+
}
3228+
3229+
/* error out if we shouldn't expand the path or buf is too small */
3230+
if (!expand || result >= MAX_LONG_PATH - 6) {
3231+
errno = ENAMETOOLONG;
3232+
return -1;
3233+
}
3234+
3235+
/* prefix full path with "\\?\" or "\\?\UNC\" */
3236+
if (buf[0] == '\\') {
3237+
/* ...unless already prefixed */
3238+
if (buf[1] == '\\' && (buf[2] == '?' || buf[2] == '.'))
3239+
return len;
3240+
3241+
wcscpy(path, L"\\\\?\\UNC\\");
3242+
wcscpy(path + 8, buf + 2);
3243+
return result + 6;
3244+
} else {
3245+
wcscpy(path, L"\\\\?\\");
3246+
wcscpy(path + 4, buf);
3247+
return result + 4;
3248+
}
3249+
}
3250+
31683251
#if !defined(_MSC_VER)
31693252
/*
31703253
* Disable MSVCRT command line wildcard expansion (__getmainargs called from
@@ -3326,6 +3409,9 @@ int wmain(int argc, const wchar_t **wargv)
33263409
/* initialize Unicode console */
33273410
winansi_init();
33283411

3412+
/* init length of current directory for handle_long_path */
3413+
current_directory_len = GetCurrentDirectoryW(0, NULL);
3414+
33293415
/* invoke the real main() using our utf8 version of argv. */
33303416
exit_status = main(argc, argv);
33313417

0 commit comments

Comments
 (0)