Skip to content

Commit b534531

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 26668da commit b534531

File tree

7 files changed

+347
-63
lines changed

7 files changed

+347
-63
lines changed

Documentation/config/core.txt

+7
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,13 @@ core.fscache::
676676
Git for Windows uses this to bulk-read and cache lstat data of entire
677677
directories (instead of doing lstat file by file).
678678

679+
core.longpaths::
680+
Enable long path (> 260) support for builtin commands in Git for
681+
Windows. This is disabled by default, as long paths are not supported
682+
by Windows Explorer, cmd.exe and the Git for Windows tool chain
683+
(msys, bash, tcl, perl...). Only enable this if you know what you're
684+
doing and are prepared to live with a few quirks.
685+
679686
core.unsetenvvars::
680687
Windows-only: comma-separated list of environment variables'
681688
names that need to be unset before spawning any other process.

compat/mingw.c

+134-33
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,27 @@ static enum hide_dotfiles_type hide_dotfiles = HIDE_DOTFILES_DOTGITONLY;
239239
static char *unset_environment_variables;
240240
int core_fscache;
241241

242+
int are_long_paths_enabled(void)
243+
{
244+
/* default to `false` during initialization */
245+
static const int fallback = 0;
246+
247+
static int enabled = -1;
248+
249+
if (enabled < 0) {
250+
/* avoid infinite recursion */
251+
if (!the_repository)
252+
return fallback;
253+
254+
if (the_repository->config &&
255+
the_repository->config->hash_initialized &&
256+
git_config_get_bool("core.longpaths", &enabled) < 0)
257+
enabled = 0;
258+
}
259+
260+
return enabled < 0 ? fallback : enabled;
261+
}
262+
242263
int mingw_core_config(const char *var, const char *value, void *cb)
243264
{
244265
if (!strcmp(var, "core.hidedotfiles")) {
@@ -300,8 +321,8 @@ static wchar_t *normalize_ntpath(wchar_t *wbuf)
300321
int mingw_unlink(const char *pathname)
301322
{
302323
int ret, tries = 0;
303-
wchar_t wpathname[MAX_PATH];
304-
if (xutftowcs_path(wpathname, pathname) < 0)
324+
wchar_t wpathname[MAX_LONG_PATH];
325+
if (xutftowcs_long_path(wpathname, pathname) < 0)
305326
return -1;
306327

307328
if (DeleteFileW(wpathname))
@@ -333,7 +354,7 @@ static int is_dir_empty(const wchar_t *wpath)
333354
{
334355
WIN32_FIND_DATAW findbuf;
335356
HANDLE handle;
336-
wchar_t wbuf[MAX_PATH + 2];
357+
wchar_t wbuf[MAX_LONG_PATH + 2];
337358
wcscpy(wbuf, wpath);
338359
wcscat(wbuf, L"\\*");
339360
handle = FindFirstFileW(wbuf, &findbuf);
@@ -354,7 +375,7 @@ static int is_dir_empty(const wchar_t *wpath)
354375
int mingw_rmdir(const char *pathname)
355376
{
356377
int ret, tries = 0;
357-
wchar_t wpathname[MAX_PATH];
378+
wchar_t wpathname[MAX_LONG_PATH];
358379
struct stat st;
359380

360381
/*
@@ -376,7 +397,7 @@ int mingw_rmdir(const char *pathname)
376397
return -1;
377398
}
378399

379-
if (xutftowcs_path(wpathname, pathname) < 0)
400+
if (xutftowcs_long_path(wpathname, pathname) < 0)
380401
return -1;
381402

382403
while ((ret = _wrmdir(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {
@@ -455,15 +476,18 @@ static int set_hidden_flag(const wchar_t *path, int set)
455476
int mingw_mkdir(const char *path, int mode)
456477
{
457478
int ret;
458-
wchar_t wpath[MAX_PATH];
479+
wchar_t wpath[MAX_LONG_PATH];
459480

460481
if (!is_valid_win32_path(path, 0)) {
461482
errno = EINVAL;
462483
return -1;
463484
}
464485

465-
if (xutftowcs_path(wpath, path) < 0)
486+
/* CreateDirectoryW path limit is 248 (MAX_PATH - 8.3 file name) */
487+
if (xutftowcs_path_ex(wpath, path, MAX_LONG_PATH, -1, 248,
488+
are_long_paths_enabled()) < 0)
466489
return -1;
490+
467491
ret = _wmkdir(wpath);
468492
if (!ret && needs_hiding(path))
469493
return set_hidden_flag(wpath, 1);
@@ -550,7 +574,7 @@ int mingw_open (const char *filename, int oflags, ...)
550574
va_list args;
551575
unsigned mode;
552576
int fd, create = (oflags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL);
553-
wchar_t wfilename[MAX_PATH];
577+
wchar_t wfilename[MAX_LONG_PATH];
554578
open_fn_t open_fn;
555579

556580
va_start(args, oflags);
@@ -578,7 +602,7 @@ int mingw_open (const char *filename, int oflags, ...)
578602

579603
if (filename && !strcmp(filename, "/dev/null"))
580604
wcscpy(wfilename, L"nul");
581-
else if (xutftowcs_path(wfilename, filename) < 0)
605+
else if (xutftowcs_long_path(wfilename, filename) < 0)
582606
return -1;
583607

584608
fd = open_fn(wfilename, oflags, mode);
@@ -636,14 +660,14 @@ FILE *mingw_fopen (const char *filename, const char *otype)
636660
{
637661
int hide = needs_hiding(filename);
638662
FILE *file;
639-
wchar_t wfilename[MAX_PATH], wotype[4];
663+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
640664
if (filename && !strcmp(filename, "/dev/null"))
641665
wcscpy(wfilename, L"nul");
642666
else if (!is_valid_win32_path(filename, 1)) {
643667
int create = otype && strchr(otype, 'w');
644668
errno = create ? EINVAL : ENOENT;
645669
return NULL;
646-
} else if (xutftowcs_path(wfilename, filename) < 0)
670+
} else if (xutftowcs_long_path(wfilename, filename) < 0)
647671
return NULL;
648672

649673
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -665,14 +689,14 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
665689
{
666690
int hide = needs_hiding(filename);
667691
FILE *file;
668-
wchar_t wfilename[MAX_PATH], wotype[4];
692+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
669693
if (filename && !strcmp(filename, "/dev/null"))
670694
wcscpy(wfilename, L"nul");
671695
else if (!is_valid_win32_path(filename, 1)) {
672696
int create = otype && strchr(otype, 'w');
673697
errno = create ? EINVAL : ENOENT;
674698
return NULL;
675-
} else if (xutftowcs_path(wfilename, filename) < 0)
699+
} else if (xutftowcs_long_path(wfilename, filename) < 0)
676700
return NULL;
677701

678702
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -747,27 +771,33 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
747771

748772
int mingw_access(const char *filename, int mode)
749773
{
750-
wchar_t wfilename[MAX_PATH];
774+
wchar_t wfilename[MAX_LONG_PATH];
751775
if (!strcmp("nul", filename) || !strcmp("/dev/null", filename))
752776
return 0;
753-
if (xutftowcs_path(wfilename, filename) < 0)
777+
if (xutftowcs_long_path(wfilename, filename) < 0)
754778
return -1;
755779
/* X_OK is not supported by the MSVCRT version */
756780
return _waccess(wfilename, mode & ~X_OK);
757781
}
758782

783+
/* cached length of current directory for handle_long_path */
784+
static int current_directory_len = 0;
785+
759786
int mingw_chdir(const char *dirname)
760787
{
761-
wchar_t wdirname[MAX_PATH];
762-
if (xutftowcs_path(wdirname, dirname) < 0)
788+
int result;
789+
wchar_t wdirname[MAX_LONG_PATH];
790+
if (xutftowcs_long_path(wdirname, dirname) < 0)
763791
return -1;
764-
return _wchdir(wdirname);
792+
result = _wchdir(wdirname);
793+
current_directory_len = GetCurrentDirectoryW(0, NULL);
794+
return result;
765795
}
766796

767797
int mingw_chmod(const char *filename, int mode)
768798
{
769-
wchar_t wfilename[MAX_PATH];
770-
if (xutftowcs_path(wfilename, filename) < 0)
799+
wchar_t wfilename[MAX_LONG_PATH];
800+
if (xutftowcs_long_path(wfilename, filename) < 0)
771801
return -1;
772802
return _wchmod(wfilename, mode);
773803
}
@@ -815,8 +845,8 @@ static int has_valid_directory_prefix(wchar_t *wfilename)
815845
static int do_lstat(int follow, const char *file_name, struct stat *buf)
816846
{
817847
WIN32_FILE_ATTRIBUTE_DATA fdata;
818-
wchar_t wfilename[MAX_PATH];
819-
if (xutftowcs_path(wfilename, file_name) < 0)
848+
wchar_t wfilename[MAX_LONG_PATH];
849+
if (xutftowcs_long_path(wfilename, file_name) < 0)
820850
return -1;
821851

822852
if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
@@ -987,10 +1017,10 @@ int mingw_utime (const char *file_name, const struct utimbuf *times)
9871017
FILETIME mft, aft;
9881018
int rc;
9891019
DWORD attrs;
990-
wchar_t wfilename[MAX_PATH];
1020+
wchar_t wfilename[MAX_LONG_PATH];
9911021
HANDLE osfilehandle;
9921022

993-
if (xutftowcs_path(wfilename, file_name) < 0)
1023+
if (xutftowcs_long_path(wfilename, file_name) < 0)
9941024
return -1;
9951025

9961026
/* must have write permission */
@@ -1073,6 +1103,7 @@ char *mingw_mktemp(char *template)
10731103
wchar_t wtemplate[MAX_PATH];
10741104
int offset = 0;
10751105

1106+
/* we need to return the path, thus no long paths here! */
10761107
if (xutftowcs_path(wtemplate, template) < 0)
10771108
return NULL;
10781109

@@ -1707,6 +1738,10 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen
17071738

17081739
if (*argv && !strcmp(cmd, *argv))
17091740
wcmd[0] = L'\0';
1741+
/*
1742+
* Paths to executables and to the current directory do not support
1743+
* long paths, therefore we cannot use xutftowcs_long_path() here.
1744+
*/
17101745
else if (xutftowcs_path(wcmd, cmd) < 0)
17111746
return -1;
17121747
if (dir && xutftowcs_path(wdir, dir) < 0)
@@ -2358,8 +2393,9 @@ int mingw_rename(const char *pold, const char *pnew)
23582393
{
23592394
DWORD attrs, gle;
23602395
int tries = 0;
2361-
wchar_t wpold[MAX_PATH], wpnew[MAX_PATH];
2362-
if (xutftowcs_path(wpold, pold) < 0 || xutftowcs_path(wpnew, pnew) < 0)
2396+
wchar_t wpold[MAX_LONG_PATH], wpnew[MAX_LONG_PATH];
2397+
if (xutftowcs_long_path(wpold, pold) < 0 ||
2398+
xutftowcs_long_path(wpnew, pnew) < 0)
23632399
return -1;
23642400

23652401
/*
@@ -2673,9 +2709,9 @@ int mingw_raise(int sig)
26732709

26742710
int link(const char *oldpath, const char *newpath)
26752711
{
2676-
wchar_t woldpath[MAX_PATH], wnewpath[MAX_PATH];
2677-
if (xutftowcs_path(woldpath, oldpath) < 0 ||
2678-
xutftowcs_path(wnewpath, newpath) < 0)
2712+
wchar_t woldpath[MAX_LONG_PATH], wnewpath[MAX_LONG_PATH];
2713+
if (xutftowcs_long_path(woldpath, oldpath) < 0 ||
2714+
xutftowcs_long_path(wnewpath, newpath) < 0)
26792715
return -1;
26802716

26812717
if (!CreateHardLinkW(wnewpath, woldpath, NULL)) {
@@ -2743,8 +2779,8 @@ int mingw_is_mount_point(struct strbuf *path)
27432779
{
27442780
WIN32_FIND_DATAW findbuf = { 0 };
27452781
HANDLE handle;
2746-
wchar_t wfilename[MAX_PATH];
2747-
int wlen = xutftowcs_path(wfilename, path->buf);
2782+
wchar_t wfilename[MAX_LONG_PATH];
2783+
int wlen = xutftowcs_long_path(wfilename, path->buf);
27482784
if (wlen < 0)
27492785
die(_("could not get long path for '%s'"), path->buf);
27502786

@@ -2889,9 +2925,9 @@ static size_t append_system_bin_dirs(char *path, size_t size)
28892925

28902926
static int is_system32_path(const char *path)
28912927
{
2892-
WCHAR system32[MAX_PATH], wpath[MAX_PATH];
2928+
WCHAR system32[MAX_LONG_PATH], wpath[MAX_LONG_PATH];
28932929

2894-
if (xutftowcs_path(wpath, path) < 0 ||
2930+
if (xutftowcs_long_path(wpath, path) < 0 ||
28952931
!GetSystemDirectoryW(system32, ARRAY_SIZE(system32)) ||
28962932
_wcsicmp(system32, wpath))
28972933
return 0;
@@ -3259,6 +3295,68 @@ int is_valid_win32_path(const char *path, int allow_literal_nul)
32593295
}
32603296
}
32613297

3298+
int handle_long_path(wchar_t *path, int len, int max_path, int expand)
3299+
{
3300+
int result;
3301+
wchar_t buf[MAX_LONG_PATH];
3302+
3303+
/*
3304+
* we don't need special handling if path is relative to the current
3305+
* directory, and current directory + path don't exceed the desired
3306+
* max_path limit. This should cover > 99 % of cases with minimal
3307+
* performance impact (git almost always uses relative paths).
3308+
*/
3309+
if ((len < 2 || (!is_dir_sep(path[0]) && path[1] != ':')) &&
3310+
(current_directory_len + len < max_path))
3311+
return len;
3312+
3313+
/*
3314+
* handle everything else:
3315+
* - absolute paths: "C:\dir\file"
3316+
* - absolute UNC paths: "\\server\share\dir\file"
3317+
* - absolute paths on current drive: "\dir\file"
3318+
* - relative paths on other drive: "X:file"
3319+
* - prefixed paths: "\\?\...", "\\.\..."
3320+
*/
3321+
3322+
/* convert to absolute path using GetFullPathNameW */
3323+
result = GetFullPathNameW(path, MAX_LONG_PATH, buf, NULL);
3324+
if (!result) {
3325+
errno = err_win_to_posix(GetLastError());
3326+
return -1;
3327+
}
3328+
3329+
/*
3330+
* return absolute path if it fits within max_path (even if
3331+
* "cwd + path" doesn't due to '..' components)
3332+
*/
3333+
if (result < max_path) {
3334+
wcscpy(path, buf);
3335+
return result;
3336+
}
3337+
3338+
/* error out if we shouldn't expand the path or buf is too small */
3339+
if (!expand || result >= MAX_LONG_PATH - 6) {
3340+
errno = ENAMETOOLONG;
3341+
return -1;
3342+
}
3343+
3344+
/* prefix full path with "\\?\" or "\\?\UNC\" */
3345+
if (buf[0] == '\\') {
3346+
/* ...unless already prefixed */
3347+
if (buf[1] == '\\' && (buf[2] == '?' || buf[2] == '.'))
3348+
return len;
3349+
3350+
wcscpy(path, L"\\\\?\\UNC\\");
3351+
wcscpy(path + 8, buf + 2);
3352+
return result + 6;
3353+
} else {
3354+
wcscpy(path, L"\\\\?\\");
3355+
wcscpy(path + 4, buf);
3356+
return result + 4;
3357+
}
3358+
}
3359+
32623360
#if !defined(_MSC_VER)
32633361
/*
32643362
* Disable MSVCRT command line wildcard expansion (__getmainargs called from
@@ -3420,6 +3518,9 @@ int wmain(int argc, const wchar_t **wargv)
34203518
/* initialize Unicode console */
34213519
winansi_init();
34223520

3521+
/* init length of current directory for handle_long_path */
3522+
current_directory_len = GetCurrentDirectoryW(0, NULL);
3523+
34233524
/* invoke the real main() using our utf8 version of argv. */
34243525
exit_status = main(argc, argv);
34253526

0 commit comments

Comments
 (0)