Skip to content

Commit 7686f5b

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 cbe19d6 commit 7686f5b

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
@@ -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

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, void *cb)
252273
{
@@ -309,8 +330,8 @@ static wchar_t *normalize_ntpath(wchar_t *wbuf)
309330
int mingw_unlink(const char *pathname)
310331
{
311332
int ret, tries = 0;
312-
wchar_t wpathname[MAX_PATH];
313-
if (xutftowcs_path(wpathname, pathname) < 0)
333+
wchar_t wpathname[MAX_LONG_PATH];
334+
if (xutftowcs_long_path(wpathname, pathname) < 0)
314335
return -1;
315336

316337
if (DeleteFileW(wpathname))
@@ -342,7 +363,7 @@ static int is_dir_empty(const wchar_t *wpath)
342363
{
343364
WIN32_FIND_DATAW findbuf;
344365
HANDLE handle;
345-
wchar_t wbuf[MAX_PATH + 2];
366+
wchar_t wbuf[MAX_LONG_PATH + 2];
346367
wcscpy(wbuf, wpath);
347368
wcscat(wbuf, L"\\*");
348369
handle = FindFirstFileW(wbuf, &findbuf);
@@ -363,7 +384,7 @@ static int is_dir_empty(const wchar_t *wpath)
363384
int mingw_rmdir(const char *pathname)
364385
{
365386
int ret, tries = 0;
366-
wchar_t wpathname[MAX_PATH];
387+
wchar_t wpathname[MAX_LONG_PATH];
367388
struct stat st;
368389

369390
/*
@@ -385,7 +406,7 @@ int mingw_rmdir(const char *pathname)
385406
return -1;
386407
}
387408

388-
if (xutftowcs_path(wpathname, pathname) < 0)
409+
if (xutftowcs_long_path(wpathname, pathname) < 0)
389410
return -1;
390411

391412
while ((ret = _wrmdir(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {
@@ -464,15 +485,18 @@ static int set_hidden_flag(const wchar_t *path, int set)
464485
int mingw_mkdir(const char *path, int mode)
465486
{
466487
int ret;
467-
wchar_t wpath[MAX_PATH];
488+
wchar_t wpath[MAX_LONG_PATH];
468489

469490
if (!is_valid_win32_path(path, 0)) {
470491
errno = EINVAL;
471492
return -1;
472493
}
473494

474-
if (xutftowcs_path(wpath, path) < 0)
495+
/* CreateDirectoryW path limit is 248 (MAX_PATH - 8.3 file name) */
496+
if (xutftowcs_path_ex(wpath, path, MAX_LONG_PATH, -1, 248,
497+
are_long_paths_enabled()) < 0)
475498
return -1;
499+
476500
ret = _wmkdir(wpath);
477501
if (!ret && needs_hiding(path))
478502
return set_hidden_flag(wpath, 1);
@@ -559,7 +583,7 @@ int mingw_open (const char *filename, int oflags, ...)
559583
va_list args;
560584
unsigned mode;
561585
int fd, create = (oflags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL);
562-
wchar_t wfilename[MAX_PATH];
586+
wchar_t wfilename[MAX_LONG_PATH];
563587
open_fn_t open_fn;
564588

565589
va_start(args, oflags);
@@ -587,7 +611,7 @@ int mingw_open (const char *filename, int oflags, ...)
587611

588612
if (filename && !strcmp(filename, "/dev/null"))
589613
wcscpy(wfilename, L"nul");
590-
else if (xutftowcs_path(wfilename, filename) < 0)
614+
else if (xutftowcs_long_path(wfilename, filename) < 0)
591615
return -1;
592616

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

658682
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -674,14 +698,14 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
674698
{
675699
int hide = needs_hiding(filename);
676700
FILE *file;
677-
wchar_t wfilename[MAX_PATH], wotype[4];
701+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
678702
if (filename && !strcmp(filename, "/dev/null"))
679703
wcscpy(wfilename, L"nul");
680704
else if (!is_valid_win32_path(filename, 1)) {
681705
int create = otype && strchr(otype, 'w');
682706
errno = create ? EINVAL : ENOENT;
683707
return NULL;
684-
} else if (xutftowcs_path(wfilename, filename) < 0)
708+
} else if (xutftowcs_long_path(wfilename, filename) < 0)
685709
return NULL;
686710

687711
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -730,7 +754,7 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
730754
if (GetFileType(h) == FILE_TYPE_PIPE)
731755
errno = EPIPE;
732756
else {
733-
wchar_t path[MAX_PATH];
757+
wchar_t path[MAX_LONG_PATH];
734758
DWORD ret = GetFinalPathNameByHandleW(h, path,
735759
ARRAY_SIZE(path), 0);
736760
UINT drive_type = ret > 0 && ret < ARRAY_SIZE(path) ?
@@ -756,27 +780,33 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
756780

757781
int mingw_access(const char *filename, int mode)
758782
{
759-
wchar_t wfilename[MAX_PATH];
783+
wchar_t wfilename[MAX_LONG_PATH];
760784
if (!strcmp("nul", filename) || !strcmp("/dev/null", filename))
761785
return 0;
762-
if (xutftowcs_path(wfilename, filename) < 0)
786+
if (xutftowcs_long_path(wfilename, filename) < 0)
763787
return -1;
764788
/* X_OK is not supported by the MSVCRT version */
765789
return _waccess(wfilename, mode & ~X_OK);
766790
}
767791

792+
/* cached length of current directory for handle_long_path */
793+
static int current_directory_len = 0;
794+
768795
int mingw_chdir(const char *dirname)
769796
{
770-
wchar_t wdirname[MAX_PATH];
771-
if (xutftowcs_path(wdirname, dirname) < 0)
797+
int result;
798+
wchar_t wdirname[MAX_LONG_PATH];
799+
if (xutftowcs_long_path(wdirname, dirname) < 0)
772800
return -1;
773-
return _wchdir(wdirname);
801+
result = _wchdir(wdirname);
802+
current_directory_len = GetCurrentDirectoryW(0, NULL);
803+
return result;
774804
}
775805

776806
int mingw_chmod(const char *filename, int mode)
777807
{
778-
wchar_t wfilename[MAX_PATH];
779-
if (xutftowcs_path(wfilename, filename) < 0)
808+
wchar_t wfilename[MAX_LONG_PATH];
809+
if (xutftowcs_long_path(wfilename, filename) < 0)
780810
return -1;
781811
return _wchmod(wfilename, mode);
782812
}
@@ -824,8 +854,8 @@ static int has_valid_directory_prefix(wchar_t *wfilename)
824854
static int do_lstat(int follow, const char *file_name, struct stat *buf)
825855
{
826856
WIN32_FILE_ATTRIBUTE_DATA fdata;
827-
wchar_t wfilename[MAX_PATH];
828-
if (xutftowcs_path(wfilename, file_name) < 0)
857+
wchar_t wfilename[MAX_LONG_PATH];
858+
if (xutftowcs_long_path(wfilename, file_name) < 0)
829859
return -1;
830860

831861
if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
@@ -996,10 +1026,10 @@ int mingw_utime (const char *file_name, const struct utimbuf *times)
9961026
FILETIME mft, aft;
9971027
int rc;
9981028
DWORD attrs;
999-
wchar_t wfilename[MAX_PATH];
1029+
wchar_t wfilename[MAX_LONG_PATH];
10001030
HANDLE osfilehandle;
10011031

1002-
if (xutftowcs_path(wfilename, file_name) < 0)
1032+
if (xutftowcs_long_path(wfilename, file_name) < 0)
10031033
return -1;
10041034

10051035
/* must have write permission */
@@ -1082,6 +1112,7 @@ char *mingw_mktemp(char *template)
10821112
wchar_t wtemplate[MAX_PATH];
10831113
int offset = 0;
10841114

1115+
/* we need to return the path, thus no long paths here! */
10851116
if (xutftowcs_path(wtemplate, template) < 0)
10861117
return NULL;
10871118

@@ -1733,6 +1764,10 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen
17331764

17341765
if (*argv && !strcmp(cmd, *argv))
17351766
wcmd[0] = L'\0';
1767+
/*
1768+
* Paths to executables and to the current directory do not support
1769+
* long paths, therefore we cannot use xutftowcs_long_path() here.
1770+
*/
17361771
else if (xutftowcs_path(wcmd, cmd) < 0)
17371772
return -1;
17381773
if (dir && xutftowcs_path(wdir, dir) < 0)
@@ -2381,8 +2416,9 @@ int mingw_rename(const char *pold, const char *pnew)
23812416
{
23822417
DWORD attrs, gle;
23832418
int tries = 0;
2384-
wchar_t wpold[MAX_PATH], wpnew[MAX_PATH];
2385-
if (xutftowcs_path(wpold, pold) < 0 || xutftowcs_path(wpnew, pnew) < 0)
2419+
wchar_t wpold[MAX_LONG_PATH], wpnew[MAX_LONG_PATH];
2420+
if (xutftowcs_long_path(wpold, pold) < 0 ||
2421+
xutftowcs_long_path(wpnew, pnew) < 0)
23862422
return -1;
23872423

23882424
/*
@@ -2696,9 +2732,9 @@ int mingw_raise(int sig)
26962732

26972733
int link(const char *oldpath, const char *newpath)
26982734
{
2699-
wchar_t woldpath[MAX_PATH], wnewpath[MAX_PATH];
2700-
if (xutftowcs_path(woldpath, oldpath) < 0 ||
2701-
xutftowcs_path(wnewpath, newpath) < 0)
2735+
wchar_t woldpath[MAX_LONG_PATH], wnewpath[MAX_LONG_PATH];
2736+
if (xutftowcs_long_path(woldpath, oldpath) < 0 ||
2737+
xutftowcs_long_path(wnewpath, newpath) < 0)
27022738
return -1;
27032739

27042740
if (!CreateHardLinkW(wnewpath, woldpath, NULL)) {
@@ -2766,8 +2802,8 @@ int mingw_is_mount_point(struct strbuf *path)
27662802
{
27672803
WIN32_FIND_DATAW findbuf = { 0 };
27682804
HANDLE handle;
2769-
wchar_t wfilename[MAX_PATH];
2770-
int wlen = xutftowcs_path(wfilename, path->buf);
2805+
wchar_t wfilename[MAX_LONG_PATH];
2806+
int wlen = xutftowcs_long_path(wfilename, path->buf);
27712807
if (wlen < 0)
27722808
die(_("could not get long path for '%s'"), path->buf);
27732809

@@ -2912,9 +2948,9 @@ static size_t append_system_bin_dirs(char *path, size_t size)
29122948

29132949
static int is_system32_path(const char *path)
29142950
{
2915-
WCHAR system32[MAX_PATH], wpath[MAX_PATH];
2951+
WCHAR system32[MAX_LONG_PATH], wpath[MAX_LONG_PATH];
29162952

2917-
if (xutftowcs_path(wpath, path) < 0 ||
2953+
if (xutftowcs_long_path(wpath, path) < 0 ||
29182954
!GetSystemDirectoryW(system32, ARRAY_SIZE(system32)) ||
29192955
_wcsicmp(system32, wpath))
29202956
return 0;
@@ -3282,6 +3318,68 @@ int is_valid_win32_path(const char *path, int allow_literal_nul)
32823318
}
32833319
}
32843320

3321+
int handle_long_path(wchar_t *path, int len, int max_path, int expand)
3322+
{
3323+
int result;
3324+
wchar_t buf[MAX_LONG_PATH];
3325+
3326+
/*
3327+
* we don't need special handling if path is relative to the current
3328+
* directory, and current directory + path don't exceed the desired
3329+
* max_path limit. This should cover > 99 % of cases with minimal
3330+
* performance impact (git almost always uses relative paths).
3331+
*/
3332+
if ((len < 2 || (!is_dir_sep(path[0]) && path[1] != ':')) &&
3333+
(current_directory_len + len < max_path))
3334+
return len;
3335+
3336+
/*
3337+
* handle everything else:
3338+
* - absolute paths: "C:\dir\file"
3339+
* - absolute UNC paths: "\\server\share\dir\file"
3340+
* - absolute paths on current drive: "\dir\file"
3341+
* - relative paths on other drive: "X:file"
3342+
* - prefixed paths: "\\?\...", "\\.\..."
3343+
*/
3344+
3345+
/* convert to absolute path using GetFullPathNameW */
3346+
result = GetFullPathNameW(path, MAX_LONG_PATH, buf, NULL);
3347+
if (!result) {
3348+
errno = err_win_to_posix(GetLastError());
3349+
return -1;
3350+
}
3351+
3352+
/*
3353+
* return absolute path if it fits within max_path (even if
3354+
* "cwd + path" doesn't due to '..' components)
3355+
*/
3356+
if (result < max_path) {
3357+
wcscpy(path, buf);
3358+
return result;
3359+
}
3360+
3361+
/* error out if we shouldn't expand the path or buf is too small */
3362+
if (!expand || result >= MAX_LONG_PATH - 6) {
3363+
errno = ENAMETOOLONG;
3364+
return -1;
3365+
}
3366+
3367+
/* prefix full path with "\\?\" or "\\?\UNC\" */
3368+
if (buf[0] == '\\') {
3369+
/* ...unless already prefixed */
3370+
if (buf[1] == '\\' && (buf[2] == '?' || buf[2] == '.'))
3371+
return len;
3372+
3373+
wcscpy(path, L"\\\\?\\UNC\\");
3374+
wcscpy(path + 8, buf + 2);
3375+
return result + 6;
3376+
} else {
3377+
wcscpy(path, L"\\\\?\\");
3378+
wcscpy(path + 4, buf);
3379+
return result + 4;
3380+
}
3381+
}
3382+
32853383
#if !defined(_MSC_VER)
32863384
/*
32873385
* Disable MSVCRT command line wildcard expansion (__getmainargs called from
@@ -3443,6 +3541,9 @@ int wmain(int argc, const wchar_t **wargv)
34433541
/* initialize Unicode console */
34443542
winansi_init();
34453543

3544+
/* init length of current directory for handle_long_path */
3545+
current_directory_len = GetCurrentDirectoryW(0, NULL);
3546+
34463547
/* invoke the real main() using our utf8 version of argv. */
34473548
exit_status = main(argc, argv);
34483549

0 commit comments

Comments
 (0)