Skip to content

Commit 05cc044

Browse files
dschoGit for Windows Build Agent
authored and
Git for Windows Build Agent
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 <[email protected]> Thanks-to: Doug Kelly <[email protected]> Signed-off-by: Karsten Blees <[email protected]> Original-test-by: Andrey Rogozhnikov <[email protected]> Signed-off-by: Stepan Kasal <[email protected]> Signed-off-by: Johannes Schindelin <[email protected]>
1 parent 1571477 commit 05cc044

File tree

7 files changed

+319
-55
lines changed

7 files changed

+319
-55
lines changed

Documentation/config.txt

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

884+
core.longpaths::
885+
Enable long path (> 260) support for builtin commands in Git for
886+
Windows. This is disabled by default, as long paths are not supported
887+
by Windows Explorer, cmd.exe and the Git for Windows tool chain
888+
(msys, bash, tcl, perl...). Only enable this if you know what you're
889+
doing and are prepared to live with a few quirks.
890+
884891
core.unsetenvvars::
885892
EXPERIMENTAL, Windows-only: comma-separated list of environment
886893
variables' names that need to be unset before spawning any other

compat/mingw.c

Lines changed: 111 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ enum hide_dotfiles_type {
212212
static enum hide_dotfiles_type hide_dotfiles = HIDE_DOTFILES_DOTGITONLY;
213213
static char *unset_environment_variables;
214214
int core_fscache;
215+
int core_long_paths;
215216

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

232+
if (!strcmp(var, "core.longpaths")) {
233+
core_long_paths = git_config_bool(var, value);
234+
return 0;
235+
}
236+
231237
if (!strcmp(var, "core.unsetenvvars")) {
232238
free(unset_environment_variables);
233239
unset_environment_variables = xstrdup(value);
@@ -240,8 +246,8 @@ int mingw_core_config(const char *var, const char *value, void *cb)
240246
int mingw_unlink(const char *pathname)
241247
{
242248
int ret, tries = 0;
243-
wchar_t wpathname[MAX_PATH];
244-
if (xutftowcs_path(wpathname, pathname) < 0)
249+
wchar_t wpathname[MAX_LONG_PATH];
250+
if (xutftowcs_long_path(wpathname, pathname) < 0)
245251
return -1;
246252

247253
/* read-only files cannot be removed */
@@ -270,7 +276,7 @@ static int is_dir_empty(const wchar_t *wpath)
270276
{
271277
WIN32_FIND_DATAW findbuf;
272278
HANDLE handle;
273-
wchar_t wbuf[MAX_PATH + 2];
279+
wchar_t wbuf[MAX_LONG_PATH + 2];
274280
wcscpy(wbuf, wpath);
275281
wcscat(wbuf, L"\\*");
276282
handle = FindFirstFileW(wbuf, &findbuf);
@@ -291,8 +297,8 @@ static int is_dir_empty(const wchar_t *wpath)
291297
int mingw_rmdir(const char *pathname)
292298
{
293299
int ret, tries = 0;
294-
wchar_t wpathname[MAX_PATH];
295-
if (xutftowcs_path(wpathname, pathname) < 0)
300+
wchar_t wpathname[MAX_LONG_PATH];
301+
if (xutftowcs_long_path(wpathname, pathname) < 0)
296302
return -1;
297303

298304
while ((ret = _wrmdir(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {
@@ -367,9 +373,12 @@ static int set_hidden_flag(const wchar_t *path, int set)
367373
int mingw_mkdir(const char *path, int mode)
368374
{
369375
int ret;
370-
wchar_t wpath[MAX_PATH];
371-
if (xutftowcs_path(wpath, path) < 0)
376+
wchar_t wpath[MAX_LONG_PATH];
377+
/* CreateDirectoryW path limit is 248 (MAX_PATH - 8.3 file name) */
378+
if (xutftowcs_path_ex(wpath, path, MAX_LONG_PATH, -1, 248,
379+
core_long_paths) < 0)
372380
return -1;
381+
373382
ret = _wmkdir(wpath);
374383
if (!ret && needs_hiding(path))
375384
return set_hidden_flag(wpath, 1);
@@ -381,7 +390,7 @@ int mingw_open (const char *filename, int oflags, ...)
381390
va_list args;
382391
unsigned mode;
383392
int fd;
384-
wchar_t wfilename[MAX_PATH];
393+
wchar_t wfilename[MAX_LONG_PATH];
385394

386395
va_start(args, oflags);
387396
mode = va_arg(args, int);
@@ -390,7 +399,7 @@ int mingw_open (const char *filename, int oflags, ...)
390399
if (filename && !strcmp(filename, "/dev/null"))
391400
filename = "nul";
392401

393-
if (xutftowcs_path(wfilename, filename) < 0)
402+
if (xutftowcs_long_path(wfilename, filename) < 0)
394403
return -1;
395404
fd = _wopen(wfilename, oflags, mode);
396405

@@ -447,10 +456,10 @@ FILE *mingw_fopen (const char *filename, const char *otype)
447456
{
448457
int hide = needs_hiding(filename);
449458
FILE *file;
450-
wchar_t wfilename[MAX_PATH], wotype[4];
459+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
451460
if (filename && !strcmp(filename, "/dev/null"))
452461
filename = "nul";
453-
if (xutftowcs_path(wfilename, filename) < 0 ||
462+
if (xutftowcs_long_path(wfilename, filename) < 0 ||
454463
xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
455464
return NULL;
456465
if (hide && !access(filename, F_OK) && set_hidden_flag(wfilename, 0)) {
@@ -469,10 +478,10 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
469478
{
470479
int hide = needs_hiding(filename);
471480
FILE *file;
472-
wchar_t wfilename[MAX_PATH], wotype[4];
481+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
473482
if (filename && !strcmp(filename, "/dev/null"))
474483
filename = "nul";
475-
if (xutftowcs_path(wfilename, filename) < 0 ||
484+
if (xutftowcs_long_path(wfilename, filename) < 0 ||
476485
xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
477486
return NULL;
478487
if (hide && !access(filename, F_OK) && set_hidden_flag(wfilename, 0)) {
@@ -526,25 +535,32 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
526535

527536
int mingw_access(const char *filename, int mode)
528537
{
529-
wchar_t wfilename[MAX_PATH];
530-
if (xutftowcs_path(wfilename, filename) < 0)
538+
wchar_t wfilename[MAX_LONG_PATH];
539+
if (xutftowcs_long_path(wfilename, filename) < 0)
531540
return -1;
532541
/* X_OK is not supported by the MSVCRT version */
533542
return _waccess(wfilename, mode & ~X_OK);
534543
}
535544

545+
/* cached length of current directory for handle_long_path */
546+
static int current_directory_len = 0;
547+
536548
int mingw_chdir(const char *dirname)
537549
{
550+
int result;
538551
wchar_t wdirname[MAX_PATH];
552+
/* SetCurrentDirectoryW doesn't support long paths */
539553
if (xutftowcs_path(wdirname, dirname) < 0)
540554
return -1;
541-
return _wchdir(wdirname);
555+
result = _wchdir(wdirname);
556+
current_directory_len = GetCurrentDirectoryW(0, NULL);
557+
return result;
542558
}
543559

544560
int mingw_chmod(const char *filename, int mode)
545561
{
546-
wchar_t wfilename[MAX_PATH];
547-
if (xutftowcs_path(wfilename, filename) < 0)
562+
wchar_t wfilename[MAX_LONG_PATH];
563+
if (xutftowcs_long_path(wfilename, filename) < 0)
548564
return -1;
549565
return _wchmod(wfilename, mode);
550566
}
@@ -592,8 +608,8 @@ static int has_valid_directory_prefix(wchar_t *wfilename)
592608
static int do_lstat(int follow, const char *file_name, struct stat *buf)
593609
{
594610
WIN32_FILE_ATTRIBUTE_DATA fdata;
595-
wchar_t wfilename[MAX_PATH];
596-
if (xutftowcs_path(wfilename, file_name) < 0)
611+
wchar_t wfilename[MAX_LONG_PATH];
612+
if (xutftowcs_long_path(wfilename, file_name) < 0)
597613
return -1;
598614

599615
if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
@@ -742,8 +758,8 @@ int mingw_utime (const char *file_name, const struct utimbuf *times)
742758
FILETIME mft, aft;
743759
int fh, rc;
744760
DWORD attrs;
745-
wchar_t wfilename[MAX_PATH];
746-
if (xutftowcs_path(wfilename, file_name) < 0)
761+
wchar_t wfilename[MAX_LONG_PATH];
762+
if (xutftowcs_long_path(wfilename, file_name) < 0)
747763
return -1;
748764

749765
/* must have write permission */
@@ -791,6 +807,7 @@ unsigned int sleep (unsigned int seconds)
791807
char *mingw_mktemp(char *template)
792808
{
793809
wchar_t wtemplate[MAX_PATH];
810+
/* we need to return the path, thus no long paths here! */
794811
if (xutftowcs_path(wtemplate, template) < 0)
795812
return NULL;
796813
if (!_wmktemp(wtemplate))
@@ -1127,6 +1144,7 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen
11271144
si.hStdOutput = winansi_get_osfhandle(fhout);
11281145
si.hStdError = winansi_get_osfhandle(fherr);
11291146

1147+
/* executables and the current directory don't support long paths */
11301148
if (xutftowcs_path(wcmd, cmd) < 0)
11311149
return -1;
11321150
if (dir && xutftowcs_path(wdir, dir) < 0)
@@ -1695,8 +1713,9 @@ int mingw_rename(const char *pold, const char *pnew)
16951713
{
16961714
DWORD attrs, gle;
16971715
int tries = 0;
1698-
wchar_t wpold[MAX_PATH], wpnew[MAX_PATH];
1699-
if (xutftowcs_path(wpold, pold) < 0 || xutftowcs_path(wpnew, pnew) < 0)
1716+
wchar_t wpold[MAX_LONG_PATH], wpnew[MAX_LONG_PATH];
1717+
if (xutftowcs_long_path(wpold, pold) < 0 ||
1718+
xutftowcs_long_path(wpnew, pnew) < 0)
17001719
return -1;
17011720

17021721
/*
@@ -1936,9 +1955,9 @@ int link(const char *oldpath, const char *newpath)
19361955
{
19371956
typedef BOOL (WINAPI *T)(LPCWSTR, LPCWSTR, LPSECURITY_ATTRIBUTES);
19381957
static T create_hard_link = NULL;
1939-
wchar_t woldpath[MAX_PATH], wnewpath[MAX_PATH];
1940-
if (xutftowcs_path(woldpath, oldpath) < 0 ||
1941-
xutftowcs_path(wnewpath, newpath) < 0)
1958+
wchar_t woldpath[MAX_LONG_PATH], wnewpath[MAX_LONG_PATH];
1959+
if (xutftowcs_long_path(woldpath, oldpath) < 0 ||
1960+
xutftowcs_long_path(wnewpath, newpath) < 0)
19421961
return -1;
19431962

19441963
if (!create_hard_link) {
@@ -2151,6 +2170,68 @@ static void setup_windows_environment(void)
21512170
setenv("TERM", "cygwin", 1);
21522171
}
21532172

2173+
int handle_long_path(wchar_t *path, int len, int max_path, int expand)
2174+
{
2175+
int result;
2176+
wchar_t buf[MAX_LONG_PATH];
2177+
2178+
/*
2179+
* we don't need special handling if path is relative to the current
2180+
* directory, and current directory + path don't exceed the desired
2181+
* max_path limit. This should cover > 99 % of cases with minimal
2182+
* performance impact (git almost always uses relative paths).
2183+
*/
2184+
if ((len < 2 || (!is_dir_sep(path[0]) && path[1] != ':')) &&
2185+
(current_directory_len + len < max_path))
2186+
return len;
2187+
2188+
/*
2189+
* handle everything else:
2190+
* - absolute paths: "C:\dir\file"
2191+
* - absolute UNC paths: "\\server\share\dir\file"
2192+
* - absolute paths on current drive: "\dir\file"
2193+
* - relative paths on other drive: "X:file"
2194+
* - prefixed paths: "\\?\...", "\\.\..."
2195+
*/
2196+
2197+
/* convert to absolute path using GetFullPathNameW */
2198+
result = GetFullPathNameW(path, MAX_LONG_PATH, buf, NULL);
2199+
if (!result) {
2200+
errno = err_win_to_posix(GetLastError());
2201+
return -1;
2202+
}
2203+
2204+
/*
2205+
* return absolute path if it fits within max_path (even if
2206+
* "cwd + path" doesn't due to '..' components)
2207+
*/
2208+
if (result < max_path) {
2209+
wcscpy(path, buf);
2210+
return result;
2211+
}
2212+
2213+
/* error out if we shouldn't expand the path or buf is too small */
2214+
if (!expand || result >= MAX_LONG_PATH - 6) {
2215+
errno = ENAMETOOLONG;
2216+
return -1;
2217+
}
2218+
2219+
/* prefix full path with "\\?\" or "\\?\UNC\" */
2220+
if (buf[0] == '\\') {
2221+
/* ...unless already prefixed */
2222+
if (buf[1] == '\\' && (buf[2] == '?' || buf[2] == '.'))
2223+
return len;
2224+
2225+
wcscpy(path, L"\\\\?\\UNC\\");
2226+
wcscpy(path + 8, buf + 2);
2227+
return result + 6;
2228+
} else {
2229+
wcscpy(path, L"\\\\?\\");
2230+
wcscpy(path + 4, buf);
2231+
return result + 4;
2232+
}
2233+
}
2234+
21542235
/*
21552236
* Disable MSVCRT command line wildcard expansion (__getmainargs called from
21562237
* mingw startup code, see init.c in mingw runtime).
@@ -2244,6 +2325,9 @@ void mingw_startup(void)
22442325

22452326
/* initialize Unicode console */
22462327
winansi_init();
2328+
2329+
/* init length of current directory for handle_long_path */
2330+
current_directory_len = GetCurrentDirectoryW(0, NULL);
22472331
}
22482332

22492333
int uname(struct utsname *buf)

0 commit comments

Comments
 (0)