Skip to content

Commit e6fdb12

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 6856809 commit e6fdb12

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
@@ -235,6 +235,7 @@ static int core_restrict_inherited_handles = -1;
235235
static enum hide_dotfiles_type hide_dotfiles = HIDE_DOTFILES_DOTGITONLY;
236236
static char *unset_environment_variables;
237237
int core_fscache;
238+
int core_long_paths;
238239

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

255+
if (!strcmp(var, "core.longpaths")) {
256+
core_long_paths = git_config_bool(var, value);
257+
return 0;
258+
}
259+
254260
if (!strcmp(var, "core.unsetenvvars")) {
255261
free(unset_environment_variables);
256262
unset_environment_variables = xstrdup(value);
@@ -297,8 +303,8 @@ static wchar_t *normalize_ntpath(wchar_t *wbuf)
297303
int mingw_unlink(const char *pathname)
298304
{
299305
int ret, tries = 0;
300-
wchar_t wpathname[MAX_PATH];
301-
if (xutftowcs_path(wpathname, pathname) < 0)
306+
wchar_t wpathname[MAX_LONG_PATH];
307+
if (xutftowcs_long_path(wpathname, pathname) < 0)
302308
return -1;
303309

304310
if (DeleteFileW(wpathname))
@@ -330,7 +336,7 @@ static int is_dir_empty(const wchar_t *wpath)
330336
{
331337
WIN32_FIND_DATAW findbuf;
332338
HANDLE handle;
333-
wchar_t wbuf[MAX_PATH + 2];
339+
wchar_t wbuf[MAX_LONG_PATH + 2];
334340
wcscpy(wbuf, wpath);
335341
wcscat(wbuf, L"\\*");
336342
handle = FindFirstFileW(wbuf, &findbuf);
@@ -351,7 +357,7 @@ static int is_dir_empty(const wchar_t *wpath)
351357
int mingw_rmdir(const char *pathname)
352358
{
353359
int ret, tries = 0;
354-
wchar_t wpathname[MAX_PATH];
360+
wchar_t wpathname[MAX_LONG_PATH];
355361
struct stat st;
356362

357363
/*
@@ -373,7 +379,7 @@ int mingw_rmdir(const char *pathname)
373379
return -1;
374380
}
375381

376-
if (xutftowcs_path(wpathname, pathname) < 0)
382+
if (xutftowcs_long_path(wpathname, pathname) < 0)
377383
return -1;
378384

379385
while ((ret = _wrmdir(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {
@@ -452,15 +458,18 @@ static int set_hidden_flag(const wchar_t *path, int set)
452458
int mingw_mkdir(const char *path, int mode)
453459
{
454460
int ret;
455-
wchar_t wpath[MAX_PATH];
461+
wchar_t wpath[MAX_LONG_PATH];
456462

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

462-
if (xutftowcs_path(wpath, path) < 0)
468+
/* CreateDirectoryW path limit is 248 (MAX_PATH - 8.3 file name) */
469+
if (xutftowcs_path_ex(wpath, path, MAX_LONG_PATH, -1, 248,
470+
core_long_paths) < 0)
463471
return -1;
472+
464473
ret = _wmkdir(wpath);
465474
if (!ret && needs_hiding(path))
466475
return set_hidden_flag(wpath, 1);
@@ -547,7 +556,7 @@ int mingw_open (const char *filename, int oflags, ...)
547556
va_list args;
548557
unsigned mode;
549558
int fd, create = (oflags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL);
550-
wchar_t wfilename[MAX_PATH];
559+
wchar_t wfilename[MAX_LONG_PATH];
551560
open_fn_t open_fn;
552561

553562
va_start(args, oflags);
@@ -575,7 +584,7 @@ int mingw_open (const char *filename, int oflags, ...)
575584

576585
if (filename && !strcmp(filename, "/dev/null"))
577586
wcscpy(wfilename, L"nul");
578-
else if (xutftowcs_path(wfilename, filename) < 0)
587+
else if (xutftowcs_long_path(wfilename, filename) < 0)
579588
return -1;
580589

581590
fd = open_fn(wfilename, oflags, mode);
@@ -633,14 +642,14 @@ FILE *mingw_fopen (const char *filename, const char *otype)
633642
{
634643
int hide = needs_hiding(filename);
635644
FILE *file;
636-
wchar_t wfilename[MAX_PATH], wotype[4];
645+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
637646
if (filename && !strcmp(filename, "/dev/null"))
638647
wcscpy(wfilename, L"nul");
639648
else if (!is_valid_win32_path(filename, 1)) {
640649
int create = otype && strchr(otype, 'w');
641650
errno = create ? EINVAL : ENOENT;
642651
return NULL;
643-
} else if (xutftowcs_path(wfilename, filename) < 0)
652+
} else if (xutftowcs_long_path(wfilename, filename) < 0)
644653
return NULL;
645654

646655
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -662,14 +671,14 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
662671
{
663672
int hide = needs_hiding(filename);
664673
FILE *file;
665-
wchar_t wfilename[MAX_PATH], wotype[4];
674+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
666675
if (filename && !strcmp(filename, "/dev/null"))
667676
wcscpy(wfilename, L"nul");
668677
else if (!is_valid_win32_path(filename, 1)) {
669678
int create = otype && strchr(otype, 'w');
670679
errno = create ? EINVAL : ENOENT;
671680
return NULL;
672-
} else if (xutftowcs_path(wfilename, filename) < 0)
681+
} else if (xutftowcs_long_path(wfilename, filename) < 0)
673682
return NULL;
674683

675684
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -744,27 +753,33 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
744753

745754
int mingw_access(const char *filename, int mode)
746755
{
747-
wchar_t wfilename[MAX_PATH];
756+
wchar_t wfilename[MAX_LONG_PATH];
748757
if (!strcmp("nul", filename) || !strcmp("/dev/null", filename))
749758
return 0;
750-
if (xutftowcs_path(wfilename, filename) < 0)
759+
if (xutftowcs_long_path(wfilename, filename) < 0)
751760
return -1;
752761
/* X_OK is not supported by the MSVCRT version */
753762
return _waccess(wfilename, mode & ~X_OK);
754763
}
755764

765+
/* cached length of current directory for handle_long_path */
766+
static int current_directory_len = 0;
767+
756768
int mingw_chdir(const char *dirname)
757769
{
758-
wchar_t wdirname[MAX_PATH];
759-
if (xutftowcs_path(wdirname, dirname) < 0)
770+
int result;
771+
wchar_t wdirname[MAX_LONG_PATH];
772+
if (xutftowcs_long_path(wdirname, dirname) < 0)
760773
return -1;
761-
return _wchdir(wdirname);
774+
result = _wchdir(wdirname);
775+
current_directory_len = GetCurrentDirectoryW(0, NULL);
776+
return result;
762777
}
763778

764779
int mingw_chmod(const char *filename, int mode)
765780
{
766-
wchar_t wfilename[MAX_PATH];
767-
if (xutftowcs_path(wfilename, filename) < 0)
781+
wchar_t wfilename[MAX_LONG_PATH];
782+
if (xutftowcs_long_path(wfilename, filename) < 0)
768783
return -1;
769784
return _wchmod(wfilename, mode);
770785
}
@@ -812,8 +827,8 @@ static int has_valid_directory_prefix(wchar_t *wfilename)
812827
static int do_lstat(int follow, const char *file_name, struct stat *buf)
813828
{
814829
WIN32_FILE_ATTRIBUTE_DATA fdata;
815-
wchar_t wfilename[MAX_PATH];
816-
if (xutftowcs_path(wfilename, file_name) < 0)
830+
wchar_t wfilename[MAX_LONG_PATH];
831+
if (xutftowcs_long_path(wfilename, file_name) < 0)
817832
return -1;
818833

819834
if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
@@ -984,10 +999,10 @@ int mingw_utime (const char *file_name, const struct utimbuf *times)
984999
FILETIME mft, aft;
9851000
int rc;
9861001
DWORD attrs;
987-
wchar_t wfilename[MAX_PATH];
1002+
wchar_t wfilename[MAX_LONG_PATH];
9881003
HANDLE osfilehandle;
9891004

990-
if (xutftowcs_path(wfilename, file_name) < 0)
1005+
if (xutftowcs_long_path(wfilename, file_name) < 0)
9911006
return -1;
9921007

9931008
/* must have write permission */
@@ -1070,6 +1085,7 @@ char *mingw_mktemp(char *template)
10701085
wchar_t wtemplate[MAX_PATH];
10711086
int offset = 0;
10721087

1088+
/* we need to return the path, thus no long paths here! */
10731089
if (xutftowcs_path(wtemplate, template) < 0)
10741090
return NULL;
10751091

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

17081724
if (*argv && !strcmp(cmd, *argv))
17091725
wcmd[0] = L'\0';
1726+
/*
1727+
* Paths to executables and to the current directory do not support
1728+
* long paths, therefore we cannot use xutftowcs_long_path() here.
1729+
*/
17101730
else if (xutftowcs_path(wcmd, cmd) < 0)
17111731
return -1;
17121732
if (dir && xutftowcs_path(wdir, dir) < 0)
@@ -2358,8 +2378,9 @@ int mingw_rename(const char *pold, const char *pnew)
23582378
{
23592379
DWORD attrs, gle;
23602380
int tries = 0;
2361-
wchar_t wpold[MAX_PATH], wpnew[MAX_PATH];
2362-
if (xutftowcs_path(wpold, pold) < 0 || xutftowcs_path(wpnew, pnew) < 0)
2381+
wchar_t wpold[MAX_LONG_PATH], wpnew[MAX_LONG_PATH];
2382+
if (xutftowcs_long_path(wpold, pold) < 0 ||
2383+
xutftowcs_long_path(wpnew, pnew) < 0)
23632384
return -1;
23642385

23652386
/*
@@ -2673,9 +2694,9 @@ int mingw_raise(int sig)
26732694

26742695
int link(const char *oldpath, const char *newpath)
26752696
{
2676-
wchar_t woldpath[MAX_PATH], wnewpath[MAX_PATH];
2677-
if (xutftowcs_path(woldpath, oldpath) < 0 ||
2678-
xutftowcs_path(wnewpath, newpath) < 0)
2697+
wchar_t woldpath[MAX_LONG_PATH], wnewpath[MAX_LONG_PATH];
2698+
if (xutftowcs_long_path(woldpath, oldpath) < 0 ||
2699+
xutftowcs_long_path(wnewpath, newpath) < 0)
26792700
return -1;
26802701

26812702
if (!CreateHardLinkW(wnewpath, woldpath, NULL)) {
@@ -2743,8 +2764,8 @@ int mingw_is_mount_point(struct strbuf *path)
27432764
{
27442765
WIN32_FIND_DATAW findbuf = { 0 };
27452766
HANDLE handle;
2746-
wchar_t wfilename[MAX_PATH];
2747-
int wlen = xutftowcs_path(wfilename, path->buf);
2767+
wchar_t wfilename[MAX_LONG_PATH];
2768+
int wlen = xutftowcs_long_path(wfilename, path->buf);
27482769
if (wlen < 0)
27492770
die(_("could not get long path for '%s'"), path->buf);
27502771

@@ -2889,9 +2910,9 @@ static size_t append_system_bin_dirs(char *path, size_t size)
28892910

28902911
static int is_system32_path(const char *path)
28912912
{
2892-
WCHAR system32[MAX_PATH], wpath[MAX_PATH];
2913+
WCHAR system32[MAX_LONG_PATH], wpath[MAX_LONG_PATH];
28932914

2894-
if (xutftowcs_path(wpath, path) < 0 ||
2915+
if (xutftowcs_long_path(wpath, path) < 0 ||
28952916
!GetSystemDirectoryW(system32, ARRAY_SIZE(system32)) ||
28962917
_wcsicmp(system32, wpath))
28972918
return 0;
@@ -3256,6 +3277,68 @@ int is_valid_win32_path(const char *path, int allow_literal_nul)
32563277
}
32573278
}
32583279

3280+
int handle_long_path(wchar_t *path, int len, int max_path, int expand)
3281+
{
3282+
int result;
3283+
wchar_t buf[MAX_LONG_PATH];
3284+
3285+
/*
3286+
* we don't need special handling if path is relative to the current
3287+
* directory, and current directory + path don't exceed the desired
3288+
* max_path limit. This should cover > 99 % of cases with minimal
3289+
* performance impact (git almost always uses relative paths).
3290+
*/
3291+
if ((len < 2 || (!is_dir_sep(path[0]) && path[1] != ':')) &&
3292+
(current_directory_len + len < max_path))
3293+
return len;
3294+
3295+
/*
3296+
* handle everything else:
3297+
* - absolute paths: "C:\dir\file"
3298+
* - absolute UNC paths: "\\server\share\dir\file"
3299+
* - absolute paths on current drive: "\dir\file"
3300+
* - relative paths on other drive: "X:file"
3301+
* - prefixed paths: "\\?\...", "\\.\..."
3302+
*/
3303+
3304+
/* convert to absolute path using GetFullPathNameW */
3305+
result = GetFullPathNameW(path, MAX_LONG_PATH, buf, NULL);
3306+
if (!result) {
3307+
errno = err_win_to_posix(GetLastError());
3308+
return -1;
3309+
}
3310+
3311+
/*
3312+
* return absolute path if it fits within max_path (even if
3313+
* "cwd + path" doesn't due to '..' components)
3314+
*/
3315+
if (result < max_path) {
3316+
wcscpy(path, buf);
3317+
return result;
3318+
}
3319+
3320+
/* error out if we shouldn't expand the path or buf is too small */
3321+
if (!expand || result >= MAX_LONG_PATH - 6) {
3322+
errno = ENAMETOOLONG;
3323+
return -1;
3324+
}
3325+
3326+
/* prefix full path with "\\?\" or "\\?\UNC\" */
3327+
if (buf[0] == '\\') {
3328+
/* ...unless already prefixed */
3329+
if (buf[1] == '\\' && (buf[2] == '?' || buf[2] == '.'))
3330+
return len;
3331+
3332+
wcscpy(path, L"\\\\?\\UNC\\");
3333+
wcscpy(path + 8, buf + 2);
3334+
return result + 6;
3335+
} else {
3336+
wcscpy(path, L"\\\\?\\");
3337+
wcscpy(path + 4, buf);
3338+
return result + 4;
3339+
}
3340+
}
3341+
32593342
#if !defined(_MSC_VER)
32603343
/*
32613344
* Disable MSVCRT command line wildcard expansion (__getmainargs called from
@@ -3417,6 +3500,9 @@ int wmain(int argc, const wchar_t **wargv)
34173500
/* initialize Unicode console */
34183501
winansi_init();
34193502

3503+
/* init length of current directory for handle_long_path */
3504+
current_directory_len = GetCurrentDirectoryW(0, NULL);
3505+
34203506
/* invoke the real main() using our utf8 version of argv. */
34213507
exit_status = main(argc, argv);
34223508

0 commit comments

Comments
 (0)