Skip to content

Commit 8004a40

Browse files
committed
Win32: 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] Thanks-to: Martin W. Kirst <maki@bitkings.de> Thanks-to: Doug Kelly <dougk.ff7@gmail.com> Signed-off-by: Karsten Blees <blees@dcon.de> Original-test-by: Andrey Rogozhnikov <rogozhnikov.andrey@gmail.com> Signed-off-by: Stepan Kasal <kasal@ucw.cz> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
1 parent 66b5c61 commit 8004a40

File tree

7 files changed

+319
-55
lines changed

7 files changed

+319
-55
lines changed

Documentation/config.txt

+7
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,13 @@ core.fscache::
915915
Git for Windows uses this to bulk-read and cache lstat data of entire
916916
directories (instead of doing lstat file by file).
917917

918+
core.longpaths::
919+
Enable long path (> 260) support for builtin commands in Git for
920+
Windows. This is disabled by default, as long paths are not supported
921+
by Windows Explorer, cmd.exe and the Git for Windows tool chain
922+
(msys, bash, tcl, perl...). Only enable this if you know what you're
923+
doing and are prepared to live with a few quirks.
924+
918925
core.unsetenvvars::
919926
EXPERIMENTAL, Windows-only: comma-separated list of environment
920927
variables' names that need to be unset before spawning any other

compat/mingw.c

+111-27
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ enum hide_dotfiles_type {
213213
static enum hide_dotfiles_type hide_dotfiles = HIDE_DOTFILES_DOTGITONLY;
214214
static char *unset_environment_variables;
215215
int core_fscache;
216+
int core_long_paths;
216217

217218
int mingw_core_config(const char *var, const char *value, void *cb)
218219
{
@@ -229,6 +230,11 @@ int mingw_core_config(const char *var, const char *value, void *cb)
229230
return 0;
230231
}
231232

233+
if (!strcmp(var, "core.longpaths")) {
234+
core_long_paths = git_config_bool(var, value);
235+
return 0;
236+
}
237+
232238
if (!strcmp(var, "core.unsetenvvars")) {
233239
free(unset_environment_variables);
234240
unset_environment_variables = xstrdup(value);
@@ -266,8 +272,8 @@ static wchar_t *normalize_ntpath(wchar_t *wbuf)
266272
int mingw_unlink(const char *pathname)
267273
{
268274
int ret, tries = 0;
269-
wchar_t wpathname[MAX_PATH];
270-
if (xutftowcs_path(wpathname, pathname) < 0)
275+
wchar_t wpathname[MAX_LONG_PATH];
276+
if (xutftowcs_long_path(wpathname, pathname) < 0)
271277
return -1;
272278

273279
/* read-only files cannot be removed */
@@ -296,7 +302,7 @@ static int is_dir_empty(const wchar_t *wpath)
296302
{
297303
WIN32_FIND_DATAW findbuf;
298304
HANDLE handle;
299-
wchar_t wbuf[MAX_PATH + 2];
305+
wchar_t wbuf[MAX_LONG_PATH + 2];
300306
wcscpy(wbuf, wpath);
301307
wcscat(wbuf, L"\\*");
302308
handle = FindFirstFileW(wbuf, &findbuf);
@@ -317,8 +323,8 @@ static int is_dir_empty(const wchar_t *wpath)
317323
int mingw_rmdir(const char *pathname)
318324
{
319325
int ret, tries = 0;
320-
wchar_t wpathname[MAX_PATH];
321-
if (xutftowcs_path(wpathname, pathname) < 0)
326+
wchar_t wpathname[MAX_LONG_PATH];
327+
if (xutftowcs_long_path(wpathname, pathname) < 0)
322328
return -1;
323329

324330
while ((ret = _wrmdir(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {
@@ -393,9 +399,12 @@ static int set_hidden_flag(const wchar_t *path, int set)
393399
int mingw_mkdir(const char *path, int mode)
394400
{
395401
int ret;
396-
wchar_t wpath[MAX_PATH];
397-
if (xutftowcs_path(wpath, path) < 0)
402+
wchar_t wpath[MAX_LONG_PATH];
403+
/* CreateDirectoryW path limit is 248 (MAX_PATH - 8.3 file name) */
404+
if (xutftowcs_path_ex(wpath, path, MAX_LONG_PATH, -1, 248,
405+
core_long_paths) < 0)
398406
return -1;
407+
399408
ret = _wmkdir(wpath);
400409
if (!ret && needs_hiding(path))
401410
return set_hidden_flag(wpath, 1);
@@ -438,7 +447,7 @@ int mingw_open (const char *filename, int oflags, ...)
438447
va_list args;
439448
unsigned mode;
440449
int fd;
441-
wchar_t wfilename[MAX_PATH];
450+
wchar_t wfilename[MAX_LONG_PATH];
442451
open_fn_t open_fn;
443452

444453
va_start(args, oflags);
@@ -453,7 +462,7 @@ int mingw_open (const char *filename, int oflags, ...)
453462
else
454463
open_fn = _wopen;
455464

456-
if (xutftowcs_path(wfilename, filename) < 0)
465+
if (xutftowcs_long_path(wfilename, filename) < 0)
457466
return -1;
458467
fd = open_fn(wfilename, oflags, mode);
459468

@@ -510,10 +519,10 @@ FILE *mingw_fopen (const char *filename, const char *otype)
510519
{
511520
int hide = needs_hiding(filename);
512521
FILE *file;
513-
wchar_t wfilename[MAX_PATH], wotype[4];
522+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
514523
if (filename && !strcmp(filename, "/dev/null"))
515524
filename = "nul";
516-
if (xutftowcs_path(wfilename, filename) < 0 ||
525+
if (xutftowcs_long_path(wfilename, filename) < 0 ||
517526
xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
518527
return NULL;
519528
if (hide && !access(filename, F_OK) && set_hidden_flag(wfilename, 0)) {
@@ -532,10 +541,10 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
532541
{
533542
int hide = needs_hiding(filename);
534543
FILE *file;
535-
wchar_t wfilename[MAX_PATH], wotype[4];
544+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
536545
if (filename && !strcmp(filename, "/dev/null"))
537546
filename = "nul";
538-
if (xutftowcs_path(wfilename, filename) < 0 ||
547+
if (xutftowcs_long_path(wfilename, filename) < 0 ||
539548
xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
540549
return NULL;
541550
if (hide && !access(filename, F_OK) && set_hidden_flag(wfilename, 0)) {
@@ -589,25 +598,32 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
589598

590599
int mingw_access(const char *filename, int mode)
591600
{
592-
wchar_t wfilename[MAX_PATH];
593-
if (xutftowcs_path(wfilename, filename) < 0)
601+
wchar_t wfilename[MAX_LONG_PATH];
602+
if (xutftowcs_long_path(wfilename, filename) < 0)
594603
return -1;
595604
/* X_OK is not supported by the MSVCRT version */
596605
return _waccess(wfilename, mode & ~X_OK);
597606
}
598607

608+
/* cached length of current directory for handle_long_path */
609+
static int current_directory_len = 0;
610+
599611
int mingw_chdir(const char *dirname)
600612
{
613+
int result;
601614
wchar_t wdirname[MAX_PATH];
615+
/* SetCurrentDirectoryW doesn't support long paths */
602616
if (xutftowcs_path(wdirname, dirname) < 0)
603617
return -1;
604-
return _wchdir(wdirname);
618+
result = _wchdir(wdirname);
619+
current_directory_len = GetCurrentDirectoryW(0, NULL);
620+
return result;
605621
}
606622

607623
int mingw_chmod(const char *filename, int mode)
608624
{
609-
wchar_t wfilename[MAX_PATH];
610-
if (xutftowcs_path(wfilename, filename) < 0)
625+
wchar_t wfilename[MAX_LONG_PATH];
626+
if (xutftowcs_long_path(wfilename, filename) < 0)
611627
return -1;
612628
return _wchmod(wfilename, mode);
613629
}
@@ -655,8 +671,8 @@ static int has_valid_directory_prefix(wchar_t *wfilename)
655671
static int do_lstat(int follow, const char *file_name, struct stat *buf)
656672
{
657673
WIN32_FILE_ATTRIBUTE_DATA fdata;
658-
wchar_t wfilename[MAX_PATH];
659-
if (xutftowcs_path(wfilename, file_name) < 0)
674+
wchar_t wfilename[MAX_LONG_PATH];
675+
if (xutftowcs_long_path(wfilename, file_name) < 0)
660676
return -1;
661677

662678
if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
@@ -827,8 +843,8 @@ int mingw_utime (const char *file_name, const struct utimbuf *times)
827843
FILETIME mft, aft;
828844
int fh, rc;
829845
DWORD attrs;
830-
wchar_t wfilename[MAX_PATH];
831-
if (xutftowcs_path(wfilename, file_name) < 0)
846+
wchar_t wfilename[MAX_LONG_PATH];
847+
if (xutftowcs_long_path(wfilename, file_name) < 0)
832848
return -1;
833849

834850
/* must have write permission */
@@ -889,6 +905,7 @@ char *mingw_mktemp(char *template)
889905
wchar_t wtemplate[MAX_PATH];
890906
int offset = 0;
891907

908+
/* we need to return the path, thus no long paths here! */
892909
if (xutftowcs_path(wtemplate, template) < 0)
893910
return NULL;
894911

@@ -1329,6 +1346,7 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen
13291346
si.hStdOutput = winansi_get_osfhandle(fhout);
13301347
si.hStdError = winansi_get_osfhandle(fherr);
13311348

1349+
/* executables and the current directory don't support long paths */
13321350
if (xutftowcs_path(wcmd, cmd) < 0)
13331351
return -1;
13341352
if (dir && xutftowcs_path(wdir, dir) < 0)
@@ -1896,8 +1914,9 @@ int mingw_rename(const char *pold, const char *pnew)
18961914
{
18971915
DWORD attrs, gle;
18981916
int tries = 0;
1899-
wchar_t wpold[MAX_PATH], wpnew[MAX_PATH];
1900-
if (xutftowcs_path(wpold, pold) < 0 || xutftowcs_path(wpnew, pnew) < 0)
1917+
wchar_t wpold[MAX_LONG_PATH], wpnew[MAX_LONG_PATH];
1918+
if (xutftowcs_long_path(wpold, pold) < 0 ||
1919+
xutftowcs_long_path(wpnew, pnew) < 0)
19011920
return -1;
19021921

19031922
/*
@@ -2161,9 +2180,9 @@ int mingw_raise(int sig)
21612180

21622181
int link(const char *oldpath, const char *newpath)
21632182
{
2164-
wchar_t woldpath[MAX_PATH], wnewpath[MAX_PATH];
2165-
if (xutftowcs_path(woldpath, oldpath) < 0 ||
2166-
xutftowcs_path(wnewpath, newpath) < 0)
2183+
wchar_t woldpath[MAX_LONG_PATH], wnewpath[MAX_LONG_PATH];
2184+
if (xutftowcs_long_path(woldpath, oldpath) < 0 ||
2185+
xutftowcs_long_path(wnewpath, newpath) < 0)
21672186
return -1;
21682187

21692188
if (!CreateHardLinkW(wnewpath, woldpath, NULL)) {
@@ -2366,6 +2385,68 @@ static void setup_windows_environment(void)
23662385
setenv("TERM", "cygwin", 1);
23672386
}
23682387

2388+
int handle_long_path(wchar_t *path, int len, int max_path, int expand)
2389+
{
2390+
int result;
2391+
wchar_t buf[MAX_LONG_PATH];
2392+
2393+
/*
2394+
* we don't need special handling if path is relative to the current
2395+
* directory, and current directory + path don't exceed the desired
2396+
* max_path limit. This should cover > 99 % of cases with minimal
2397+
* performance impact (git almost always uses relative paths).
2398+
*/
2399+
if ((len < 2 || (!is_dir_sep(path[0]) && path[1] != ':')) &&
2400+
(current_directory_len + len < max_path))
2401+
return len;
2402+
2403+
/*
2404+
* handle everything else:
2405+
* - absolute paths: "C:\dir\file"
2406+
* - absolute UNC paths: "\\server\share\dir\file"
2407+
* - absolute paths on current drive: "\dir\file"
2408+
* - relative paths on other drive: "X:file"
2409+
* - prefixed paths: "\\?\...", "\\.\..."
2410+
*/
2411+
2412+
/* convert to absolute path using GetFullPathNameW */
2413+
result = GetFullPathNameW(path, MAX_LONG_PATH, buf, NULL);
2414+
if (!result) {
2415+
errno = err_win_to_posix(GetLastError());
2416+
return -1;
2417+
}
2418+
2419+
/*
2420+
* return absolute path if it fits within max_path (even if
2421+
* "cwd + path" doesn't due to '..' components)
2422+
*/
2423+
if (result < max_path) {
2424+
wcscpy(path, buf);
2425+
return result;
2426+
}
2427+
2428+
/* error out if we shouldn't expand the path or buf is too small */
2429+
if (!expand || result >= MAX_LONG_PATH - 6) {
2430+
errno = ENAMETOOLONG;
2431+
return -1;
2432+
}
2433+
2434+
/* prefix full path with "\\?\" or "\\?\UNC\" */
2435+
if (buf[0] == '\\') {
2436+
/* ...unless already prefixed */
2437+
if (buf[1] == '\\' && (buf[2] == '?' || buf[2] == '.'))
2438+
return len;
2439+
2440+
wcscpy(path, L"\\\\?\\UNC\\");
2441+
wcscpy(path + 8, buf + 2);
2442+
return result + 6;
2443+
} else {
2444+
wcscpy(path, L"\\\\?\\");
2445+
wcscpy(path + 4, buf);
2446+
return result + 4;
2447+
}
2448+
}
2449+
23692450
/*
23702451
* Disable MSVCRT command line wildcard expansion (__getmainargs called from
23712452
* mingw startup code, see init.c in mingw runtime).
@@ -2530,6 +2611,9 @@ int wmain(int argc, const wchar_t **wargv)
25302611
/* initialize Unicode console */
25312612
winansi_init();
25322613

2614+
/* init length of current directory for handle_long_path */
2615+
current_directory_len = GetCurrentDirectoryW(0, NULL);
2616+
25332617
/* invoke the real main() using our utf8 version of argv. */
25342618
exit_status = main(argc, argv);
25352619

0 commit comments

Comments
 (0)