Skip to content

Commit 64da6f2

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 2d725f2 commit 64da6f2

File tree

9 files changed

+333
-67
lines changed

9 files changed

+333
-67
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/fsmonitor/fsm-listen-win32.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,9 @@ static struct one_watch *create_watch(struct fsmonitor_daemon_state *state,
113113
DWORD share_mode =
114114
FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE;
115115
HANDLE hDir;
116-
wchar_t wpath[MAX_PATH];
116+
wchar_t wpath[MAX_LONG_PATH];
117117

118-
if (xutftowcs_path(wpath, path) < 0) {
118+
if (xutftowcs_long_path(wpath, path) < 0) {
119119
error(_("could not convert to wide characters: '%s'"), path);
120120
return NULL;
121121
}

compat/fsmonitor/fsm-settings-win32.c

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,16 @@ static enum fsmonitor_reason is_virtual(struct repository *r)
7676
*/
7777
static enum fsmonitor_reason is_remote(struct repository *r)
7878
{
79-
wchar_t wpath[MAX_PATH];
80-
wchar_t wfullpath[MAX_PATH];
79+
wchar_t wpath[MAX_LONG_PATH];
80+
wchar_t wfullpath[MAX_LONG_PATH];
8181
size_t wlen;
8282
UINT driveType;
8383

8484
/*
8585
* Do everything in wide chars because the drive letter might be
8686
* a multi-byte sequence. See win32_has_dos_drive_prefix().
8787
*/
88-
if (xutftowcs_path(wpath, r->worktree) < 0)
88+
if (xutftowcs_long_path(wpath, r->worktree) < 0)
8989
return FSMONITOR_REASON_ZERO;
9090

9191
/*
@@ -103,7 +103,7 @@ static enum fsmonitor_reason is_remote(struct repository *r)
103103
* slashes to backslashes. This is essential to get GetDriveTypeW()
104104
* correctly handle some UNC "\\server\share\..." paths.
105105
*/
106-
if (!GetFullPathNameW(wpath, MAX_PATH, wfullpath, NULL))
106+
if (!GetFullPathNameW(wpath, MAX_LONG_PATH, wfullpath, NULL))
107107
return FSMONITOR_REASON_ZERO;
108108

109109
driveType = GetDriveTypeW(wfullpath);

compat/mingw.c

Lines changed: 117 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ static int core_restrict_inherited_handles = -1;
231231
static enum hide_dotfiles_type hide_dotfiles = HIDE_DOTFILES_DOTGITONLY;
232232
static char *unset_environment_variables;
233233
int core_fscache;
234+
int core_long_paths;
234235

235236
int mingw_core_config(const char *var, const char *value, void *cb)
236237
{
@@ -247,6 +248,11 @@ int mingw_core_config(const char *var, const char *value, void *cb)
247248
return 0;
248249
}
249250

251+
if (!strcmp(var, "core.longpaths")) {
252+
core_long_paths = git_config_bool(var, value);
253+
return 0;
254+
}
255+
250256
if (!strcmp(var, "core.unsetenvvars")) {
251257
free(unset_environment_variables);
252258
unset_environment_variables = xstrdup(value);
@@ -293,8 +299,8 @@ static wchar_t *normalize_ntpath(wchar_t *wbuf)
293299
int mingw_unlink(const char *pathname)
294300
{
295301
int ret, tries = 0;
296-
wchar_t wpathname[MAX_PATH];
297-
if (xutftowcs_path(wpathname, pathname) < 0)
302+
wchar_t wpathname[MAX_LONG_PATH];
303+
if (xutftowcs_long_path(wpathname, pathname) < 0)
298304
return -1;
299305

300306
if (DeleteFileW(wpathname))
@@ -326,7 +332,7 @@ static int is_dir_empty(const wchar_t *wpath)
326332
{
327333
WIN32_FIND_DATAW findbuf;
328334
HANDLE handle;
329-
wchar_t wbuf[MAX_PATH + 2];
335+
wchar_t wbuf[MAX_LONG_PATH + 2];
330336
wcscpy(wbuf, wpath);
331337
wcscat(wbuf, L"\\*");
332338
handle = FindFirstFileW(wbuf, &findbuf);
@@ -347,7 +353,7 @@ static int is_dir_empty(const wchar_t *wpath)
347353
int mingw_rmdir(const char *pathname)
348354
{
349355
int ret, tries = 0;
350-
wchar_t wpathname[MAX_PATH];
356+
wchar_t wpathname[MAX_LONG_PATH];
351357
struct stat st;
352358

353359
/*
@@ -369,7 +375,7 @@ int mingw_rmdir(const char *pathname)
369375
return -1;
370376
}
371377

372-
if (xutftowcs_path(wpathname, pathname) < 0)
378+
if (xutftowcs_long_path(wpathname, pathname) < 0)
373379
return -1;
374380

375381
while ((ret = _wrmdir(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {
@@ -448,15 +454,18 @@ static int set_hidden_flag(const wchar_t *path, int set)
448454
int mingw_mkdir(const char *path, int mode)
449455
{
450456
int ret;
451-
wchar_t wpath[MAX_PATH];
457+
wchar_t wpath[MAX_LONG_PATH];
452458

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

458-
if (xutftowcs_path(wpath, path) < 0)
464+
/* CreateDirectoryW path limit is 248 (MAX_PATH - 8.3 file name) */
465+
if (xutftowcs_path_ex(wpath, path, MAX_LONG_PATH, -1, 248,
466+
core_long_paths) < 0)
459467
return -1;
468+
460469
ret = _wmkdir(wpath);
461470
if (!ret && needs_hiding(path))
462471
return set_hidden_flag(wpath, 1);
@@ -542,7 +551,7 @@ int mingw_open (const char *filename, int oflags, ...)
542551
va_list args;
543552
unsigned mode;
544553
int fd, create = (oflags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL);
545-
wchar_t wfilename[MAX_PATH];
554+
wchar_t wfilename[MAX_LONG_PATH];
546555
open_fn_t open_fn;
547556

548557
va_start(args, oflags);
@@ -561,7 +570,7 @@ int mingw_open (const char *filename, int oflags, ...)
561570

562571
if (filename && !strcmp(filename, "/dev/null"))
563572
wcscpy(wfilename, L"nul");
564-
else if (xutftowcs_path(wfilename, filename) < 0)
573+
else if (xutftowcs_long_path(wfilename, filename) < 0)
565574
return -1;
566575

567576
fd = open_fn(wfilename, oflags, mode);
@@ -619,14 +628,14 @@ FILE *mingw_fopen (const char *filename, const char *otype)
619628
{
620629
int hide = needs_hiding(filename);
621630
FILE *file;
622-
wchar_t wfilename[MAX_PATH], wotype[4];
631+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
623632
if (filename && !strcmp(filename, "/dev/null"))
624633
wcscpy(wfilename, L"nul");
625634
else if (!is_valid_win32_path(filename, 1)) {
626635
int create = otype && strchr(otype, 'w');
627636
errno = create ? EINVAL : ENOENT;
628637
return NULL;
629-
} else if (xutftowcs_path(wfilename, filename) < 0)
638+
} else if (xutftowcs_long_path(wfilename, filename) < 0)
630639
return NULL;
631640

632641
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -648,14 +657,14 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
648657
{
649658
int hide = needs_hiding(filename);
650659
FILE *file;
651-
wchar_t wfilename[MAX_PATH], wotype[4];
660+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
652661
if (filename && !strcmp(filename, "/dev/null"))
653662
wcscpy(wfilename, L"nul");
654663
else if (!is_valid_win32_path(filename, 1)) {
655664
int create = otype && strchr(otype, 'w');
656665
errno = create ? EINVAL : ENOENT;
657666
return NULL;
658-
} else if (xutftowcs_path(wfilename, filename) < 0)
667+
} else if (xutftowcs_long_path(wfilename, filename) < 0)
659668
return NULL;
660669

661670
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -712,27 +721,33 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
712721

713722
int mingw_access(const char *filename, int mode)
714723
{
715-
wchar_t wfilename[MAX_PATH];
724+
wchar_t wfilename[MAX_LONG_PATH];
716725
if (!strcmp("nul", filename) || !strcmp("/dev/null", filename))
717726
return 0;
718-
if (xutftowcs_path(wfilename, filename) < 0)
727+
if (xutftowcs_long_path(wfilename, filename) < 0)
719728
return -1;
720729
/* X_OK is not supported by the MSVCRT version */
721730
return _waccess(wfilename, mode & ~X_OK);
722731
}
723732

733+
/* cached length of current directory for handle_long_path */
734+
static int current_directory_len = 0;
735+
724736
int mingw_chdir(const char *dirname)
725737
{
726-
wchar_t wdirname[MAX_PATH];
727-
if (xutftowcs_path(wdirname, dirname) < 0)
738+
int result;
739+
wchar_t wdirname[MAX_LONG_PATH];
740+
if (xutftowcs_long_path(wdirname, dirname) < 0)
728741
return -1;
729-
return _wchdir(wdirname);
742+
result = _wchdir(wdirname);
743+
current_directory_len = GetCurrentDirectoryW(0, NULL);
744+
return result;
730745
}
731746

732747
int mingw_chmod(const char *filename, int mode)
733748
{
734-
wchar_t wfilename[MAX_PATH];
735-
if (xutftowcs_path(wfilename, filename) < 0)
749+
wchar_t wfilename[MAX_LONG_PATH];
750+
if (xutftowcs_long_path(wfilename, filename) < 0)
736751
return -1;
737752
return _wchmod(wfilename, mode);
738753
}
@@ -780,8 +795,8 @@ static int has_valid_directory_prefix(wchar_t *wfilename)
780795
static int do_lstat(int follow, const char *file_name, struct stat *buf)
781796
{
782797
WIN32_FILE_ATTRIBUTE_DATA fdata;
783-
wchar_t wfilename[MAX_PATH];
784-
if (xutftowcs_path(wfilename, file_name) < 0)
798+
wchar_t wfilename[MAX_LONG_PATH];
799+
if (xutftowcs_long_path(wfilename, file_name) < 0)
785800
return -1;
786801

787802
if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
@@ -952,8 +967,8 @@ int mingw_utime (const char *file_name, const struct utimbuf *times)
952967
FILETIME mft, aft;
953968
int fh, rc;
954969
DWORD attrs;
955-
wchar_t wfilename[MAX_PATH];
956-
if (xutftowcs_path(wfilename, file_name) < 0)
970+
wchar_t wfilename[MAX_LONG_PATH];
971+
if (xutftowcs_long_path(wfilename, file_name) < 0)
957972
return -1;
958973

959974
/* must have write permission */
@@ -1023,6 +1038,7 @@ char *mingw_mktemp(char *template)
10231038
wchar_t wtemplate[MAX_PATH];
10241039
int offset = 0;
10251040

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

@@ -1654,6 +1670,10 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen
16541670

16551671
if (*argv && !strcmp(cmd, *argv))
16561672
wcmd[0] = L'\0';
1673+
/*
1674+
* Paths to executables and to the current directory do not support
1675+
* long paths, therefore we cannot use xutftowcs_long_path() here.
1676+
*/
16571677
else if (xutftowcs_path(wcmd, cmd) < 0)
16581678
return -1;
16591679
if (dir && xutftowcs_path(wdir, dir) < 0)
@@ -2305,8 +2325,9 @@ int mingw_rename(const char *pold, const char *pnew)
23052325
{
23062326
DWORD attrs, gle;
23072327
int tries = 0;
2308-
wchar_t wpold[MAX_PATH], wpnew[MAX_PATH];
2309-
if (xutftowcs_path(wpold, pold) < 0 || xutftowcs_path(wpnew, pnew) < 0)
2328+
wchar_t wpold[MAX_LONG_PATH], wpnew[MAX_LONG_PATH];
2329+
if (xutftowcs_long_path(wpold, pold) < 0 ||
2330+
xutftowcs_long_path(wpnew, pnew) < 0)
23102331
return -1;
23112332

23122333
/*
@@ -2620,9 +2641,9 @@ int mingw_raise(int sig)
26202641

26212642
int link(const char *oldpath, const char *newpath)
26222643
{
2623-
wchar_t woldpath[MAX_PATH], wnewpath[MAX_PATH];
2624-
if (xutftowcs_path(woldpath, oldpath) < 0 ||
2625-
xutftowcs_path(wnewpath, newpath) < 0)
2644+
wchar_t woldpath[MAX_LONG_PATH], wnewpath[MAX_LONG_PATH];
2645+
if (xutftowcs_long_path(woldpath, oldpath) < 0 ||
2646+
xutftowcs_long_path(wnewpath, newpath) < 0)
26262647
return -1;
26272648

26282649
if (!CreateHardLinkW(wnewpath, woldpath, NULL)) {
@@ -2690,8 +2711,8 @@ int mingw_is_mount_point(struct strbuf *path)
26902711
{
26912712
WIN32_FIND_DATAW findbuf = { 0 };
26922713
HANDLE handle;
2693-
wchar_t wfilename[MAX_PATH];
2694-
int wlen = xutftowcs_path(wfilename, path->buf);
2714+
wchar_t wfilename[MAX_LONG_PATH];
2715+
int wlen = xutftowcs_long_path(wfilename, path->buf);
26952716
if (wlen < 0)
26962717
die(_("could not get long path for '%s'"), path->buf);
26972718

@@ -3055,6 +3076,68 @@ int is_valid_win32_path(const char *path, int allow_literal_nul)
30553076
}
30563077
}
30573078

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

3302+
/* init length of current directory for handle_long_path */
3303+
current_directory_len = GetCurrentDirectoryW(0, NULL);
3304+
32193305
/* invoke the real main() using our utf8 version of argv. */
32203306
exit_status = main(argc, argv);
32213307

0 commit comments

Comments
 (0)