Skip to content

Commit 8d207a4

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 11acaee commit 8d207a4

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
@@ -598,6 +598,13 @@ core.fscache::
598598
Git for Windows uses this to bulk-read and cache lstat data of entire
599599
directories (instead of doing lstat file by file).
600600

601+
core.longpaths::
602+
Enable long path (> 260) support for builtin commands in Git for
603+
Windows. This is disabled by default, as long paths are not supported
604+
by Windows Explorer, cmd.exe and the Git for Windows tool chain
605+
(msys, bash, tcl, perl...). Only enable this if you know what you're
606+
doing and are prepared to live with a few quirks.
607+
601608
core.unsetenvvars::
602609
Windows-only: comma-separated list of environment variables'
603610
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
@@ -233,6 +233,7 @@ static int core_restrict_inherited_handles = -1;
233233
static enum hide_dotfiles_type hide_dotfiles = HIDE_DOTFILES_DOTGITONLY;
234234
static char *unset_environment_variables;
235235
int core_fscache;
236+
int core_long_paths;
236237

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

253+
if (!strcmp(var, "core.longpaths")) {
254+
core_long_paths = git_config_bool(var, value);
255+
return 0;
256+
}
257+
252258
if (!strcmp(var, "core.unsetenvvars")) {
253259
free(unset_environment_variables);
254260
unset_environment_variables = xstrdup(value);
@@ -295,8 +301,8 @@ static wchar_t *normalize_ntpath(wchar_t *wbuf)
295301
int mingw_unlink(const char *pathname)
296302
{
297303
int ret, tries = 0;
298-
wchar_t wpathname[MAX_PATH];
299-
if (xutftowcs_path(wpathname, pathname) < 0)
304+
wchar_t wpathname[MAX_LONG_PATH];
305+
if (xutftowcs_long_path(wpathname, pathname) < 0)
300306
return -1;
301307

302308
if (DeleteFileW(wpathname))
@@ -328,7 +334,7 @@ static int is_dir_empty(const wchar_t *wpath)
328334
{
329335
WIN32_FIND_DATAW findbuf;
330336
HANDLE handle;
331-
wchar_t wbuf[MAX_PATH + 2];
337+
wchar_t wbuf[MAX_LONG_PATH + 2];
332338
wcscpy(wbuf, wpath);
333339
wcscat(wbuf, L"\\*");
334340
handle = FindFirstFileW(wbuf, &findbuf);
@@ -349,7 +355,7 @@ static int is_dir_empty(const wchar_t *wpath)
349355
int mingw_rmdir(const char *pathname)
350356
{
351357
int ret, tries = 0;
352-
wchar_t wpathname[MAX_PATH];
358+
wchar_t wpathname[MAX_LONG_PATH];
353359
struct stat st;
354360

355361
/*
@@ -371,7 +377,7 @@ int mingw_rmdir(const char *pathname)
371377
return -1;
372378
}
373379

374-
if (xutftowcs_path(wpathname, pathname) < 0)
380+
if (xutftowcs_long_path(wpathname, pathname) < 0)
375381
return -1;
376382

377383
while ((ret = _wrmdir(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {
@@ -450,15 +456,18 @@ static int set_hidden_flag(const wchar_t *path, int set)
450456
int mingw_mkdir(const char *path, int mode)
451457
{
452458
int ret;
453-
wchar_t wpath[MAX_PATH];
459+
wchar_t wpath[MAX_LONG_PATH];
454460

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

460-
if (xutftowcs_path(wpath, path) < 0)
466+
/* CreateDirectoryW path limit is 248 (MAX_PATH - 8.3 file name) */
467+
if (xutftowcs_path_ex(wpath, path, MAX_LONG_PATH, -1, 248,
468+
core_long_paths) < 0)
461469
return -1;
470+
462471
ret = _wmkdir(wpath);
463472
if (!ret && needs_hiding(path))
464473
return set_hidden_flag(wpath, 1);
@@ -544,7 +553,7 @@ int mingw_open (const char *filename, int oflags, ...)
544553
va_list args;
545554
unsigned mode;
546555
int fd, create = (oflags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL);
547-
wchar_t wfilename[MAX_PATH];
556+
wchar_t wfilename[MAX_LONG_PATH];
548557
open_fn_t open_fn;
549558

550559
va_start(args, oflags);
@@ -563,7 +572,7 @@ int mingw_open (const char *filename, int oflags, ...)
563572

564573
if (filename && !strcmp(filename, "/dev/null"))
565574
wcscpy(wfilename, L"nul");
566-
else if (xutftowcs_path(wfilename, filename) < 0)
575+
else if (xutftowcs_long_path(wfilename, filename) < 0)
567576
return -1;
568577

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

634643
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -650,14 +659,14 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
650659
{
651660
int hide = needs_hiding(filename);
652661
FILE *file;
653-
wchar_t wfilename[MAX_PATH], wotype[4];
662+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
654663
if (filename && !strcmp(filename, "/dev/null"))
655664
wcscpy(wfilename, L"nul");
656665
else if (!is_valid_win32_path(filename, 1)) {
657666
int create = otype && strchr(otype, 'w');
658667
errno = create ? EINVAL : ENOENT;
659668
return NULL;
660-
} else if (xutftowcs_path(wfilename, filename) < 0)
669+
} else if (xutftowcs_long_path(wfilename, filename) < 0)
661670
return NULL;
662671

663672
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -714,27 +723,33 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
714723

715724
int mingw_access(const char *filename, int mode)
716725
{
717-
wchar_t wfilename[MAX_PATH];
726+
wchar_t wfilename[MAX_LONG_PATH];
718727
if (!strcmp("nul", filename) || !strcmp("/dev/null", filename))
719728
return 0;
720-
if (xutftowcs_path(wfilename, filename) < 0)
729+
if (xutftowcs_long_path(wfilename, filename) < 0)
721730
return -1;
722731
/* X_OK is not supported by the MSVCRT version */
723732
return _waccess(wfilename, mode & ~X_OK);
724733
}
725734

735+
/* cached length of current directory for handle_long_path */
736+
static int current_directory_len = 0;
737+
726738
int mingw_chdir(const char *dirname)
727739
{
728-
wchar_t wdirname[MAX_PATH];
729-
if (xutftowcs_path(wdirname, dirname) < 0)
740+
int result;
741+
wchar_t wdirname[MAX_LONG_PATH];
742+
if (xutftowcs_long_path(wdirname, dirname) < 0)
730743
return -1;
731-
return _wchdir(wdirname);
744+
result = _wchdir(wdirname);
745+
current_directory_len = GetCurrentDirectoryW(0, NULL);
746+
return result;
732747
}
733748

734749
int mingw_chmod(const char *filename, int mode)
735750
{
736-
wchar_t wfilename[MAX_PATH];
737-
if (xutftowcs_path(wfilename, filename) < 0)
751+
wchar_t wfilename[MAX_LONG_PATH];
752+
if (xutftowcs_long_path(wfilename, filename) < 0)
738753
return -1;
739754
return _wchmod(wfilename, mode);
740755
}
@@ -782,8 +797,8 @@ static int has_valid_directory_prefix(wchar_t *wfilename)
782797
static int do_lstat(int follow, const char *file_name, struct stat *buf)
783798
{
784799
WIN32_FILE_ATTRIBUTE_DATA fdata;
785-
wchar_t wfilename[MAX_PATH];
786-
if (xutftowcs_path(wfilename, file_name) < 0)
800+
wchar_t wfilename[MAX_LONG_PATH];
801+
if (xutftowcs_long_path(wfilename, file_name) < 0)
787802
return -1;
788803

789804
if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
@@ -954,8 +969,8 @@ int mingw_utime (const char *file_name, const struct utimbuf *times)
954969
FILETIME mft, aft;
955970
int fh, rc;
956971
DWORD attrs;
957-
wchar_t wfilename[MAX_PATH];
958-
if (xutftowcs_path(wfilename, file_name) < 0)
972+
wchar_t wfilename[MAX_LONG_PATH];
973+
if (xutftowcs_long_path(wfilename, file_name) < 0)
959974
return -1;
960975

961976
/* must have write permission */
@@ -1025,6 +1040,7 @@ char *mingw_mktemp(char *template)
10251040
wchar_t wtemplate[MAX_PATH];
10261041
int offset = 0;
10271042

1043+
/* we need to return the path, thus no long paths here! */
10281044
if (xutftowcs_path(wtemplate, template) < 0)
10291045
return NULL;
10301046

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

16631679
if (*argv && !strcmp(cmd, *argv))
16641680
wcmd[0] = L'\0';
1681+
/*
1682+
* Paths to executables and to the current directory do not support
1683+
* long paths, therefore we cannot use xutftowcs_long_path() here.
1684+
*/
16651685
else if (xutftowcs_path(wcmd, cmd) < 0)
16661686
return -1;
16671687
if (dir && xutftowcs_path(wdir, dir) < 0)
@@ -2313,8 +2333,9 @@ int mingw_rename(const char *pold, const char *pnew)
23132333
{
23142334
DWORD attrs, gle;
23152335
int tries = 0;
2316-
wchar_t wpold[MAX_PATH], wpnew[MAX_PATH];
2317-
if (xutftowcs_path(wpold, pold) < 0 || xutftowcs_path(wpnew, pnew) < 0)
2336+
wchar_t wpold[MAX_LONG_PATH], wpnew[MAX_LONG_PATH];
2337+
if (xutftowcs_long_path(wpold, pold) < 0 ||
2338+
xutftowcs_long_path(wpnew, pnew) < 0)
23182339
return -1;
23192340

23202341
/*
@@ -2628,9 +2649,9 @@ int mingw_raise(int sig)
26282649

26292650
int link(const char *oldpath, const char *newpath)
26302651
{
2631-
wchar_t woldpath[MAX_PATH], wnewpath[MAX_PATH];
2632-
if (xutftowcs_path(woldpath, oldpath) < 0 ||
2633-
xutftowcs_path(wnewpath, newpath) < 0)
2652+
wchar_t woldpath[MAX_LONG_PATH], wnewpath[MAX_LONG_PATH];
2653+
if (xutftowcs_long_path(woldpath, oldpath) < 0 ||
2654+
xutftowcs_long_path(wnewpath, newpath) < 0)
26342655
return -1;
26352656

26362657
if (!CreateHardLinkW(wnewpath, woldpath, NULL)) {
@@ -2698,8 +2719,8 @@ int mingw_is_mount_point(struct strbuf *path)
26982719
{
26992720
WIN32_FIND_DATAW findbuf = { 0 };
27002721
HANDLE handle;
2701-
wchar_t wfilename[MAX_PATH];
2702-
int wlen = xutftowcs_path(wfilename, path->buf);
2722+
wchar_t wfilename[MAX_LONG_PATH];
2723+
int wlen = xutftowcs_long_path(wfilename, path->buf);
27032724
if (wlen < 0)
27042725
die(_("could not get long path for '%s'"), path->buf);
27052726

@@ -2844,9 +2865,9 @@ static size_t append_system_bin_dirs(char *path, size_t size)
28442865

28452866
static int is_system32_path(const char *path)
28462867
{
2847-
WCHAR system32[MAX_PATH], wpath[MAX_PATH];
2868+
WCHAR system32[MAX_LONG_PATH], wpath[MAX_LONG_PATH];
28482869

2849-
if (xutftowcs_path(wpath, path) < 0 ||
2870+
if (xutftowcs_long_path(wpath, path) < 0 ||
28502871
!GetSystemDirectoryW(system32, ARRAY_SIZE(system32)) ||
28512872
_wcsicmp(system32, wpath))
28522873
return 0;
@@ -3063,6 +3084,68 @@ int is_valid_win32_path(const char *path, int allow_literal_nul)
30633084
}
30643085
}
30653086

3087+
int handle_long_path(wchar_t *path, int len, int max_path, int expand)
3088+
{
3089+
int result;
3090+
wchar_t buf[MAX_LONG_PATH];
3091+
3092+
/*
3093+
* we don't need special handling if path is relative to the current
3094+
* directory, and current directory + path don't exceed the desired
3095+
* max_path limit. This should cover > 99 % of cases with minimal
3096+
* performance impact (git almost always uses relative paths).
3097+
*/
3098+
if ((len < 2 || (!is_dir_sep(path[0]) && path[1] != ':')) &&
3099+
(current_directory_len + len < max_path))
3100+
return len;
3101+
3102+
/*
3103+
* handle everything else:
3104+
* - absolute paths: "C:\dir\file"
3105+
* - absolute UNC paths: "\\server\share\dir\file"
3106+
* - absolute paths on current drive: "\dir\file"
3107+
* - relative paths on other drive: "X:file"
3108+
* - prefixed paths: "\\?\...", "\\.\..."
3109+
*/
3110+
3111+
/* convert to absolute path using GetFullPathNameW */
3112+
result = GetFullPathNameW(path, MAX_LONG_PATH, buf, NULL);
3113+
if (!result) {
3114+
errno = err_win_to_posix(GetLastError());
3115+
return -1;
3116+
}
3117+
3118+
/*
3119+
* return absolute path if it fits within max_path (even if
3120+
* "cwd + path" doesn't due to '..' components)
3121+
*/
3122+
if (result < max_path) {
3123+
wcscpy(path, buf);
3124+
return result;
3125+
}
3126+
3127+
/* error out if we shouldn't expand the path or buf is too small */
3128+
if (!expand || result >= MAX_LONG_PATH - 6) {
3129+
errno = ENAMETOOLONG;
3130+
return -1;
3131+
}
3132+
3133+
/* prefix full path with "\\?\" or "\\?\UNC\" */
3134+
if (buf[0] == '\\') {
3135+
/* ...unless already prefixed */
3136+
if (buf[1] == '\\' && (buf[2] == '?' || buf[2] == '.'))
3137+
return len;
3138+
3139+
wcscpy(path, L"\\\\?\\UNC\\");
3140+
wcscpy(path + 8, buf + 2);
3141+
return result + 6;
3142+
} else {
3143+
wcscpy(path, L"\\\\?\\");
3144+
wcscpy(path + 4, buf);
3145+
return result + 4;
3146+
}
3147+
}
3148+
30663149
#if !defined(_MSC_VER)
30673150
/*
30683151
* Disable MSVCRT command line wildcard expansion (__getmainargs called from
@@ -3224,6 +3307,9 @@ int wmain(int argc, const wchar_t **wargv)
32243307
/* initialize Unicode console */
32253308
winansi_init();
32263309

3310+
/* init length of current directory for handle_long_path */
3311+
current_directory_len = GetCurrentDirectoryW(0, NULL);
3312+
32273313
/* invoke the real main() using our utf8 version of argv. */
32283314
exit_status = main(argc, argv);
32293315

0 commit comments

Comments
 (0)