Skip to content

Commit 091418e

Browse files
kbleesGit for Windows Build Agent
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 0efe96c commit 091418e

File tree

7 files changed

+325
-59
lines changed

7 files changed

+325
-59
lines changed

Documentation/config/core.txt

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

562+
core.longpaths::
563+
Enable long path (> 260) support for builtin commands in Git for
564+
Windows. This is disabled by default, as long paths are not supported
565+
by Windows Explorer, cmd.exe and the Git for Windows tool chain
566+
(msys, bash, tcl, perl...). Only enable this if you know what you're
567+
doing and are prepared to live with a few quirks.
568+
562569
core.unsetenvvars::
563570
Windows-only: comma-separated list of environment variables'
564571
names that need to be unset before spawning any other process.

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
/* read-only files cannot be removed */
@@ -323,7 +329,7 @@ static int is_dir_empty(const wchar_t *wpath)
323329
{
324330
WIN32_FIND_DATAW findbuf;
325331
HANDLE handle;
326-
wchar_t wbuf[MAX_PATH + 2];
332+
wchar_t wbuf[MAX_LONG_PATH + 2];
327333
wcscpy(wbuf, wpath);
328334
wcscat(wbuf, L"\\*");
329335
handle = FindFirstFileW(wbuf, &findbuf);
@@ -344,8 +350,8 @@ static int is_dir_empty(const wchar_t *wpath)
344350
int mingw_rmdir(const char *pathname)
345351
{
346352
int ret, tries = 0;
347-
wchar_t wpathname[MAX_PATH];
348-
if (xutftowcs_path(wpathname, pathname) < 0)
353+
wchar_t wpathname[MAX_LONG_PATH];
354+
if (xutftowcs_long_path(wpathname, pathname) < 0)
349355
return -1;
350356

351357
while ((ret = _wrmdir(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {
@@ -422,15 +428,18 @@ static int set_hidden_flag(const wchar_t *path, int set)
422428
int mingw_mkdir(const char *path, int mode)
423429
{
424430
int ret;
425-
wchar_t wpath[MAX_PATH];
431+
wchar_t wpath[MAX_LONG_PATH];
426432

427433
if (!is_valid_win32_path(path, 0)) {
428434
errno = EINVAL;
429435
return -1;
430436
}
431437

432-
if (xutftowcs_path(wpath, path) < 0)
438+
/* CreateDirectoryW path limit is 248 (MAX_PATH - 8.3 file name) */
439+
if (xutftowcs_path_ex(wpath, path, MAX_LONG_PATH, -1, 248,
440+
core_long_paths) < 0)
433441
return -1;
442+
434443
ret = _wmkdir(wpath);
435444
if (!ret && needs_hiding(path))
436445
return set_hidden_flag(wpath, 1);
@@ -514,7 +523,7 @@ int mingw_open (const char *filename, int oflags, ...)
514523
va_list args;
515524
unsigned mode;
516525
int fd, create = (oflags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL);
517-
wchar_t wfilename[MAX_PATH];
526+
wchar_t wfilename[MAX_LONG_PATH];
518527
open_fn_t open_fn;
519528

520529
va_start(args, oflags);
@@ -533,7 +542,7 @@ int mingw_open (const char *filename, int oflags, ...)
533542

534543
if (filename && !strcmp(filename, "/dev/null"))
535544
wcscpy(wfilename, L"nul");
536-
else if (xutftowcs_path(wfilename, filename) < 0)
545+
else if (xutftowcs_long_path(wfilename, filename) < 0)
537546
return -1;
538547

539548
fd = open_fn(wfilename, oflags, mode);
@@ -591,14 +600,14 @@ FILE *mingw_fopen (const char *filename, const char *otype)
591600
{
592601
int hide = needs_hiding(filename);
593602
FILE *file;
594-
wchar_t wfilename[MAX_PATH], wotype[4];
603+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
595604
if (filename && !strcmp(filename, "/dev/null"))
596605
wcscpy(wfilename, L"nul");
597606
else if (!is_valid_win32_path(filename, 1)) {
598607
int create = otype && strchr(otype, 'w');
599608
errno = create ? EINVAL : ENOENT;
600609
return NULL;
601-
} else if (xutftowcs_path(wfilename, filename) < 0)
610+
} else if (xutftowcs_long_path(wfilename, filename) < 0)
602611
return NULL;
603612

604613
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -620,14 +629,14 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
620629
{
621630
int hide = needs_hiding(filename);
622631
FILE *file;
623-
wchar_t wfilename[MAX_PATH], wotype[4];
632+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
624633
if (filename && !strcmp(filename, "/dev/null"))
625634
wcscpy(wfilename, L"nul");
626635
else if (!is_valid_win32_path(filename, 1)) {
627636
int create = otype && strchr(otype, 'w');
628637
errno = create ? EINVAL : ENOENT;
629638
return NULL;
630-
} else if (xutftowcs_path(wfilename, filename) < 0)
639+
} else if (xutftowcs_long_path(wfilename, filename) < 0)
631640
return NULL;
632641

633642
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -684,25 +693,31 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
684693

685694
int mingw_access(const char *filename, int mode)
686695
{
687-
wchar_t wfilename[MAX_PATH];
688-
if (xutftowcs_path(wfilename, filename) < 0)
696+
wchar_t wfilename[MAX_LONG_PATH];
697+
if (xutftowcs_long_path(wfilename, filename) < 0)
689698
return -1;
690699
/* X_OK is not supported by the MSVCRT version */
691700
return _waccess(wfilename, mode & ~X_OK);
692701
}
693702

703+
/* cached length of current directory for handle_long_path */
704+
static int current_directory_len = 0;
705+
694706
int mingw_chdir(const char *dirname)
695707
{
696-
wchar_t wdirname[MAX_PATH];
697-
if (xutftowcs_path(wdirname, dirname) < 0)
708+
int result;
709+
wchar_t wdirname[MAX_LONG_PATH];
710+
if (xutftowcs_long_path(wdirname, dirname) < 0)
698711
return -1;
699-
return _wchdir(wdirname);
712+
result = _wchdir(wdirname);
713+
current_directory_len = GetCurrentDirectoryW(0, NULL);
714+
return result;
700715
}
701716

702717
int mingw_chmod(const char *filename, int mode)
703718
{
704-
wchar_t wfilename[MAX_PATH];
705-
if (xutftowcs_path(wfilename, filename) < 0)
719+
wchar_t wfilename[MAX_LONG_PATH];
720+
if (xutftowcs_long_path(wfilename, filename) < 0)
706721
return -1;
707722
return _wchmod(wfilename, mode);
708723
}
@@ -750,8 +765,8 @@ static int has_valid_directory_prefix(wchar_t *wfilename)
750765
static int do_lstat(int follow, const char *file_name, struct stat *buf)
751766
{
752767
WIN32_FILE_ATTRIBUTE_DATA fdata;
753-
wchar_t wfilename[MAX_PATH];
754-
if (xutftowcs_path(wfilename, file_name) < 0)
768+
wchar_t wfilename[MAX_LONG_PATH];
769+
if (xutftowcs_long_path(wfilename, file_name) < 0)
755770
return -1;
756771

757772
if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
@@ -922,8 +937,8 @@ int mingw_utime (const char *file_name, const struct utimbuf *times)
922937
FILETIME mft, aft;
923938
int fh, rc;
924939
DWORD attrs;
925-
wchar_t wfilename[MAX_PATH];
926-
if (xutftowcs_path(wfilename, file_name) < 0)
940+
wchar_t wfilename[MAX_LONG_PATH];
941+
if (xutftowcs_long_path(wfilename, file_name) < 0)
927942
return -1;
928943

929944
/* must have write permission */
@@ -984,6 +999,7 @@ char *mingw_mktemp(char *template)
984999
wchar_t wtemplate[MAX_PATH];
9851000
int offset = 0;
9861001

1002+
/* we need to return the path, thus no long paths here! */
9871003
if (xutftowcs_path(wtemplate, template) < 0)
9881004
return NULL;
9891005

@@ -1547,6 +1563,10 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen
15471563

15481564
if (*argv && !strcmp(cmd, *argv))
15491565
wcmd[0] = L'\0';
1566+
/*
1567+
* Paths to executables and to the current directory do not support
1568+
* long paths, therefore we cannot use xutftowcs_long_path() here.
1569+
*/
15501570
else if (xutftowcs_path(wcmd, cmd) < 0)
15511571
return -1;
15521572
if (dir && xutftowcs_path(wdir, dir) < 0)
@@ -2198,8 +2218,9 @@ int mingw_rename(const char *pold, const char *pnew)
21982218
{
21992219
DWORD attrs, gle;
22002220
int tries = 0;
2201-
wchar_t wpold[MAX_PATH], wpnew[MAX_PATH];
2202-
if (xutftowcs_path(wpold, pold) < 0 || xutftowcs_path(wpnew, pnew) < 0)
2221+
wchar_t wpold[MAX_LONG_PATH], wpnew[MAX_LONG_PATH];
2222+
if (xutftowcs_long_path(wpold, pold) < 0 ||
2223+
xutftowcs_long_path(wpnew, pnew) < 0)
22032224
return -1;
22042225

22052226
/*
@@ -2513,9 +2534,9 @@ int mingw_raise(int sig)
25132534

25142535
int link(const char *oldpath, const char *newpath)
25152536
{
2516-
wchar_t woldpath[MAX_PATH], wnewpath[MAX_PATH];
2517-
if (xutftowcs_path(woldpath, oldpath) < 0 ||
2518-
xutftowcs_path(wnewpath, newpath) < 0)
2537+
wchar_t woldpath[MAX_LONG_PATH], wnewpath[MAX_LONG_PATH];
2538+
if (xutftowcs_long_path(woldpath, oldpath) < 0 ||
2539+
xutftowcs_long_path(wnewpath, newpath) < 0)
25192540
return -1;
25202541

25212542
if (!CreateHardLinkW(wnewpath, woldpath, NULL)) {
@@ -2583,8 +2604,8 @@ int mingw_is_mount_point(struct strbuf *path)
25832604
{
25842605
WIN32_FIND_DATAW findbuf = { 0 };
25852606
HANDLE handle;
2586-
wchar_t wfilename[MAX_PATH];
2587-
int wlen = xutftowcs_path(wfilename, path->buf);
2607+
wchar_t wfilename[MAX_LONG_PATH];
2608+
int wlen = xutftowcs_long_path(wfilename, path->buf);
25882609
if (wlen < 0)
25892610
die(_("could not get long path for '%s'"), path->buf);
25902611

@@ -2861,6 +2882,68 @@ int is_valid_win32_path(const char *path, int allow_literal_nul)
28612882
}
28622883
}
28632884

2885+
int handle_long_path(wchar_t *path, int len, int max_path, int expand)
2886+
{
2887+
int result;
2888+
wchar_t buf[MAX_LONG_PATH];
2889+
2890+
/*
2891+
* we don't need special handling if path is relative to the current
2892+
* directory, and current directory + path don't exceed the desired
2893+
* max_path limit. This should cover > 99 % of cases with minimal
2894+
* performance impact (git almost always uses relative paths).
2895+
*/
2896+
if ((len < 2 || (!is_dir_sep(path[0]) && path[1] != ':')) &&
2897+
(current_directory_len + len < max_path))
2898+
return len;
2899+
2900+
/*
2901+
* handle everything else:
2902+
* - absolute paths: "C:\dir\file"
2903+
* - absolute UNC paths: "\\server\share\dir\file"
2904+
* - absolute paths on current drive: "\dir\file"
2905+
* - relative paths on other drive: "X:file"
2906+
* - prefixed paths: "\\?\...", "\\.\..."
2907+
*/
2908+
2909+
/* convert to absolute path using GetFullPathNameW */
2910+
result = GetFullPathNameW(path, MAX_LONG_PATH, buf, NULL);
2911+
if (!result) {
2912+
errno = err_win_to_posix(GetLastError());
2913+
return -1;
2914+
}
2915+
2916+
/*
2917+
* return absolute path if it fits within max_path (even if
2918+
* "cwd + path" doesn't due to '..' components)
2919+
*/
2920+
if (result < max_path) {
2921+
wcscpy(path, buf);
2922+
return result;
2923+
}
2924+
2925+
/* error out if we shouldn't expand the path or buf is too small */
2926+
if (!expand || result >= MAX_LONG_PATH - 6) {
2927+
errno = ENAMETOOLONG;
2928+
return -1;
2929+
}
2930+
2931+
/* prefix full path with "\\?\" or "\\?\UNC\" */
2932+
if (buf[0] == '\\') {
2933+
/* ...unless already prefixed */
2934+
if (buf[1] == '\\' && (buf[2] == '?' || buf[2] == '.'))
2935+
return len;
2936+
2937+
wcscpy(path, L"\\\\?\\UNC\\");
2938+
wcscpy(path + 8, buf + 2);
2939+
return result + 6;
2940+
} else {
2941+
wcscpy(path, L"\\\\?\\");
2942+
wcscpy(path + 4, buf);
2943+
return result + 4;
2944+
}
2945+
}
2946+
28642947
#if !defined(_MSC_VER)
28652948
/*
28662949
* Disable MSVCRT command line wildcard expansion (__getmainargs called from
@@ -3022,6 +3105,9 @@ int wmain(int argc, const wchar_t **wargv)
30223105
/* initialize Unicode console */
30233106
winansi_init();
30243107

3108+
/* init length of current directory for handle_long_path */
3109+
current_directory_len = GetCurrentDirectoryW(0, NULL);
3110+
30253111
/* invoke the real main() using our utf8 version of argv. */
30263112
exit_status = main(argc, argv);
30273113

0 commit comments

Comments
 (0)