Skip to content

Commit c4ee517

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 308e87c commit c4ee517

File tree

7 files changed

+346
-64
lines changed

7 files changed

+346
-64
lines changed

Documentation/config/core.txt

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

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

compat/mingw.c

Lines changed: 135 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,27 @@ static enum hide_dotfiles_type hide_dotfiles = HIDE_DOTFILES_DOTGITONLY;
247247
static char *unset_environment_variables;
248248
int core_fscache;
249249

250+
int are_long_paths_enabled(void)
251+
{
252+
/* default to `false` during initialization */
253+
static const int fallback = 0;
254+
255+
static int enabled = -1;
256+
257+
if (enabled < 0) {
258+
/* avoid infinite recursion */
259+
if (!the_repository)
260+
return fallback;
261+
262+
if (the_repository->config &&
263+
the_repository->config->hash_initialized &&
264+
git_config_get_bool("core.longpaths", &enabled) < 0)
265+
enabled = 0;
266+
}
267+
268+
return enabled < 0 ? fallback : enabled;
269+
}
270+
250271
int mingw_core_config(const char *var, const char *value,
251272
const struct config_context *ctx UNUSED,
252273
void *cb UNUSED)
@@ -312,8 +333,8 @@ static wchar_t *normalize_ntpath(wchar_t *wbuf)
312333
int mingw_unlink(const char *pathname)
313334
{
314335
int ret, tries = 0;
315-
wchar_t wpathname[MAX_PATH];
316-
if (xutftowcs_path(wpathname, pathname) < 0)
336+
wchar_t wpathname[MAX_LONG_PATH];
337+
if (xutftowcs_long_path(wpathname, pathname) < 0)
317338
return -1;
318339

319340
if (DeleteFileW(wpathname))
@@ -345,7 +366,7 @@ static int is_dir_empty(const wchar_t *wpath)
345366
{
346367
WIN32_FIND_DATAW findbuf;
347368
HANDLE handle;
348-
wchar_t wbuf[MAX_PATH + 2];
369+
wchar_t wbuf[MAX_LONG_PATH + 2];
349370
wcscpy(wbuf, wpath);
350371
wcscat(wbuf, L"\\*");
351372
handle = FindFirstFileW(wbuf, &findbuf);
@@ -366,7 +387,7 @@ static int is_dir_empty(const wchar_t *wpath)
366387
int mingw_rmdir(const char *pathname)
367388
{
368389
int ret, tries = 0;
369-
wchar_t wpathname[MAX_PATH];
390+
wchar_t wpathname[MAX_LONG_PATH];
370391
struct stat st;
371392

372393
/*
@@ -388,7 +409,7 @@ int mingw_rmdir(const char *pathname)
388409
return -1;
389410
}
390411

391-
if (xutftowcs_path(wpathname, pathname) < 0)
412+
if (xutftowcs_long_path(wpathname, pathname) < 0)
392413
return -1;
393414

394415
while ((ret = _wrmdir(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {
@@ -467,15 +488,18 @@ static int set_hidden_flag(const wchar_t *path, int set)
467488
int mingw_mkdir(const char *path, int mode UNUSED)
468489
{
469490
int ret;
470-
wchar_t wpath[MAX_PATH];
491+
wchar_t wpath[MAX_LONG_PATH];
471492

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

477-
if (xutftowcs_path(wpath, path) < 0)
498+
/* CreateDirectoryW path limit is 248 (MAX_PATH - 8.3 file name) */
499+
if (xutftowcs_path_ex(wpath, path, MAX_LONG_PATH, -1, 248,
500+
are_long_paths_enabled()) < 0)
478501
return -1;
502+
479503
ret = _wmkdir(wpath);
480504
if (!ret && needs_hiding(path))
481505
return set_hidden_flag(wpath, 1);
@@ -562,7 +586,7 @@ int mingw_open (const char *filename, int oflags, ...)
562586
va_list args;
563587
unsigned mode;
564588
int fd, create = (oflags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL);
565-
wchar_t wfilename[MAX_PATH];
589+
wchar_t wfilename[MAX_LONG_PATH];
566590
open_fn_t open_fn;
567591

568592
va_start(args, oflags);
@@ -590,7 +614,7 @@ int mingw_open (const char *filename, int oflags, ...)
590614

591615
if (filename && !strcmp(filename, "/dev/null"))
592616
wcscpy(wfilename, L"nul");
593-
else if (xutftowcs_path(wfilename, filename) < 0)
617+
else if (xutftowcs_long_path(wfilename, filename) < 0)
594618
return -1;
595619

596620
fd = open_fn(wfilename, oflags, mode);
@@ -648,14 +672,14 @@ FILE *mingw_fopen (const char *filename, const char *otype)
648672
{
649673
int hide = needs_hiding(filename);
650674
FILE *file;
651-
wchar_t wfilename[MAX_PATH], wotype[4];
675+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
652676
if (filename && !strcmp(filename, "/dev/null"))
653677
wcscpy(wfilename, L"nul");
654678
else if (!is_valid_win32_path(filename, 1)) {
655679
int create = otype && strchr(otype, 'w');
656680
errno = create ? EINVAL : ENOENT;
657681
return NULL;
658-
} else if (xutftowcs_path(wfilename, filename) < 0)
682+
} else if (xutftowcs_long_path(wfilename, filename) < 0)
659683
return NULL;
660684

661685
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -677,14 +701,14 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
677701
{
678702
int hide = needs_hiding(filename);
679703
FILE *file;
680-
wchar_t wfilename[MAX_PATH], wotype[4];
704+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
681705
if (filename && !strcmp(filename, "/dev/null"))
682706
wcscpy(wfilename, L"nul");
683707
else if (!is_valid_win32_path(filename, 1)) {
684708
int create = otype && strchr(otype, 'w');
685709
errno = create ? EINVAL : ENOENT;
686710
return NULL;
687-
} else if (xutftowcs_path(wfilename, filename) < 0)
711+
} else if (xutftowcs_long_path(wfilename, filename) < 0)
688712
return NULL;
689713

690714
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -734,7 +758,7 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
734758
HANDLE h = (HANDLE) _get_osfhandle(fd);
735759
if (GetFileType(h) != FILE_TYPE_PIPE) {
736760
if (orig == EINVAL) {
737-
wchar_t path[MAX_PATH];
761+
wchar_t path[MAX_LONG_PATH];
738762
DWORD ret = GetFinalPathNameByHandleW(h, path,
739763
ARRAY_SIZE(path), 0);
740764
UINT drive_type = ret > 0 && ret < ARRAY_SIZE(path) ?
@@ -771,27 +795,33 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
771795

772796
int mingw_access(const char *filename, int mode)
773797
{
774-
wchar_t wfilename[MAX_PATH];
798+
wchar_t wfilename[MAX_LONG_PATH];
775799
if (!strcmp("nul", filename) || !strcmp("/dev/null", filename))
776800
return 0;
777-
if (xutftowcs_path(wfilename, filename) < 0)
801+
if (xutftowcs_long_path(wfilename, filename) < 0)
778802
return -1;
779803
/* X_OK is not supported by the MSVCRT version */
780804
return _waccess(wfilename, mode & ~X_OK);
781805
}
782806

807+
/* cached length of current directory for handle_long_path */
808+
static int current_directory_len = 0;
809+
783810
int mingw_chdir(const char *dirname)
784811
{
785-
wchar_t wdirname[MAX_PATH];
786-
if (xutftowcs_path(wdirname, dirname) < 0)
812+
int result;
813+
wchar_t wdirname[MAX_LONG_PATH];
814+
if (xutftowcs_long_path(wdirname, dirname) < 0)
787815
return -1;
788-
return _wchdir(wdirname);
816+
result = _wchdir(wdirname);
817+
current_directory_len = GetCurrentDirectoryW(0, NULL);
818+
return result;
789819
}
790820

791821
int mingw_chmod(const char *filename, int mode)
792822
{
793-
wchar_t wfilename[MAX_PATH];
794-
if (xutftowcs_path(wfilename, filename) < 0)
823+
wchar_t wfilename[MAX_LONG_PATH];
824+
if (xutftowcs_long_path(wfilename, filename) < 0)
795825
return -1;
796826
return _wchmod(wfilename, mode);
797827
}
@@ -839,8 +869,8 @@ static int has_valid_directory_prefix(wchar_t *wfilename)
839869
static int do_lstat(int follow, const char *file_name, struct stat *buf)
840870
{
841871
WIN32_FILE_ATTRIBUTE_DATA fdata;
842-
wchar_t wfilename[MAX_PATH];
843-
if (xutftowcs_path(wfilename, file_name) < 0)
872+
wchar_t wfilename[MAX_LONG_PATH];
873+
if (xutftowcs_long_path(wfilename, file_name) < 0)
844874
return -1;
845875

846876
if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
@@ -1011,10 +1041,10 @@ int mingw_utime (const char *file_name, const struct utimbuf *times)
10111041
FILETIME mft, aft;
10121042
int rc;
10131043
DWORD attrs;
1014-
wchar_t wfilename[MAX_PATH];
1044+
wchar_t wfilename[MAX_LONG_PATH];
10151045
HANDLE osfilehandle;
10161046

1017-
if (xutftowcs_path(wfilename, file_name) < 0)
1047+
if (xutftowcs_long_path(wfilename, file_name) < 0)
10181048
return -1;
10191049

10201050
/* must have write permission */
@@ -1097,6 +1127,7 @@ char *mingw_mktemp(char *template)
10971127
wchar_t wtemplate[MAX_PATH];
10981128
int offset = 0;
10991129

1130+
/* we need to return the path, thus no long paths here! */
11001131
if (xutftowcs_path(wtemplate, template) < 0)
11011132
return NULL;
11021133

@@ -1748,6 +1779,10 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen
17481779

17491780
if (*argv && !strcmp(cmd, *argv))
17501781
wcmd[0] = L'\0';
1782+
/*
1783+
* Paths to executables and to the current directory do not support
1784+
* long paths, therefore we cannot use xutftowcs_long_path() here.
1785+
*/
17511786
else if (xutftowcs_path(wcmd, cmd) < 0)
17521787
return -1;
17531788
if (dir && xutftowcs_path(wdir, dir) < 0)
@@ -2396,8 +2431,9 @@ int mingw_rename(const char *pold, const char *pnew)
23962431
{
23972432
DWORD attrs, gle;
23982433
int tries = 0;
2399-
wchar_t wpold[MAX_PATH], wpnew[MAX_PATH];
2400-
if (xutftowcs_path(wpold, pold) < 0 || xutftowcs_path(wpnew, pnew) < 0)
2434+
wchar_t wpold[MAX_LONG_PATH], wpnew[MAX_LONG_PATH];
2435+
if (xutftowcs_long_path(wpold, pold) < 0 ||
2436+
xutftowcs_long_path(wpnew, pnew) < 0)
24012437
return -1;
24022438

24032439
/*
@@ -2715,9 +2751,9 @@ int mingw_raise(int sig)
27152751

27162752
int link(const char *oldpath, const char *newpath)
27172753
{
2718-
wchar_t woldpath[MAX_PATH], wnewpath[MAX_PATH];
2719-
if (xutftowcs_path(woldpath, oldpath) < 0 ||
2720-
xutftowcs_path(wnewpath, newpath) < 0)
2754+
wchar_t woldpath[MAX_LONG_PATH], wnewpath[MAX_LONG_PATH];
2755+
if (xutftowcs_long_path(woldpath, oldpath) < 0 ||
2756+
xutftowcs_long_path(wnewpath, newpath) < 0)
27212757
return -1;
27222758

27232759
if (!CreateHardLinkW(wnewpath, woldpath, NULL)) {
@@ -2785,8 +2821,8 @@ int mingw_is_mount_point(struct strbuf *path)
27852821
{
27862822
WIN32_FIND_DATAW findbuf = { 0 };
27872823
HANDLE handle;
2788-
wchar_t wfilename[MAX_PATH];
2789-
int wlen = xutftowcs_path(wfilename, path->buf);
2824+
wchar_t wfilename[MAX_LONG_PATH];
2825+
int wlen = xutftowcs_long_path(wfilename, path->buf);
27902826
if (wlen < 0)
27912827
die(_("could not get long path for '%s'"), path->buf);
27922828

@@ -2931,9 +2967,9 @@ static size_t append_system_bin_dirs(char *path, size_t size)
29312967

29322968
static int is_system32_path(const char *path)
29332969
{
2934-
WCHAR system32[MAX_PATH], wpath[MAX_PATH];
2970+
WCHAR system32[MAX_LONG_PATH], wpath[MAX_LONG_PATH];
29352971

2936-
if (xutftowcs_path(wpath, path) < 0 ||
2972+
if (xutftowcs_long_path(wpath, path) < 0 ||
29372973
!GetSystemDirectoryW(system32, ARRAY_SIZE(system32)) ||
29382974
_wcsicmp(system32, wpath))
29392975
return 0;
@@ -3345,6 +3381,68 @@ int is_valid_win32_path(const char *path, int allow_literal_nul)
33453381
}
33463382
}
33473383

3384+
int handle_long_path(wchar_t *path, int len, int max_path, int expand)
3385+
{
3386+
int result;
3387+
wchar_t buf[MAX_LONG_PATH];
3388+
3389+
/*
3390+
* we don't need special handling if path is relative to the current
3391+
* directory, and current directory + path don't exceed the desired
3392+
* max_path limit. This should cover > 99 % of cases with minimal
3393+
* performance impact (git almost always uses relative paths).
3394+
*/
3395+
if ((len < 2 || (!is_dir_sep(path[0]) && path[1] != ':')) &&
3396+
(current_directory_len + len < max_path))
3397+
return len;
3398+
3399+
/*
3400+
* handle everything else:
3401+
* - absolute paths: "C:\dir\file"
3402+
* - absolute UNC paths: "\\server\share\dir\file"
3403+
* - absolute paths on current drive: "\dir\file"
3404+
* - relative paths on other drive: "X:file"
3405+
* - prefixed paths: "\\?\...", "\\.\..."
3406+
*/
3407+
3408+
/* convert to absolute path using GetFullPathNameW */
3409+
result = GetFullPathNameW(path, MAX_LONG_PATH, buf, NULL);
3410+
if (!result) {
3411+
errno = err_win_to_posix(GetLastError());
3412+
return -1;
3413+
}
3414+
3415+
/*
3416+
* return absolute path if it fits within max_path (even if
3417+
* "cwd + path" doesn't due to '..' components)
3418+
*/
3419+
if (result < max_path) {
3420+
wcscpy(path, buf);
3421+
return result;
3422+
}
3423+
3424+
/* error out if we shouldn't expand the path or buf is too small */
3425+
if (!expand || result >= MAX_LONG_PATH - 6) {
3426+
errno = ENAMETOOLONG;
3427+
return -1;
3428+
}
3429+
3430+
/* prefix full path with "\\?\" or "\\?\UNC\" */
3431+
if (buf[0] == '\\') {
3432+
/* ...unless already prefixed */
3433+
if (buf[1] == '\\' && (buf[2] == '?' || buf[2] == '.'))
3434+
return len;
3435+
3436+
wcscpy(path, L"\\\\?\\UNC\\");
3437+
wcscpy(path + 8, buf + 2);
3438+
return result + 6;
3439+
} else {
3440+
wcscpy(path, L"\\\\?\\");
3441+
wcscpy(path + 4, buf);
3442+
return result + 4;
3443+
}
3444+
}
3445+
33483446
#if !defined(_MSC_VER)
33493447
/*
33503448
* Disable MSVCRT command line wildcard expansion (__getmainargs called from
@@ -3506,6 +3604,9 @@ int wmain(int argc, const wchar_t **wargv)
35063604
/* initialize Unicode console */
35073605
winansi_init();
35083606

3607+
/* init length of current directory for handle_long_path */
3608+
current_directory_len = GetCurrentDirectoryW(0, NULL);
3609+
35093610
/* invoke the real main() using our utf8 version of argv. */
35103611
exit_status = main(argc, argv);
35113612

0 commit comments

Comments
 (0)