Skip to content

Commit a243d5c

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 <[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 9f411f8 commit a243d5c

File tree

10 files changed

+325
-57
lines changed

10 files changed

+325
-57
lines changed

Documentation/config.txt

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

731+
core.longpaths::
732+
Enable long path (> 260) support for builtin commands in Git for
733+
Windows. This is disabled by default, as long paths are not supported
734+
by Windows Explorer, cmd.exe and the Git for Windows tool chain
735+
(msys, bash, tcl, perl...). Only enable this if you know what you're
736+
doing and are prepared to live with a few quirks.
737+
731738
core.createObject::
732739
You can set this to 'link', in which case a hardlink followed by
733740
a delete of the source are used to make sure that object creation

cache.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,8 @@ extern enum hide_dotfiles_type hide_dotfiles;
707707

708708
extern int core_fscache;
709709

710+
extern int core_long_paths;
711+
710712
enum branch_track {
711713
BRANCH_TRACK_UNSPECIFIED = -1,
712714
BRANCH_TRACK_NEVER = 0,

compat/mingw.c

Lines changed: 107 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,8 @@ static int ask_yes_no_if_possible(const char *format, ...)
206206
int mingw_unlink(const char *pathname)
207207
{
208208
int ret, tries = 0;
209-
wchar_t wpathname[MAX_PATH];
210-
if (xutftowcs_path(wpathname, pathname) < 0)
209+
wchar_t wpathname[MAX_LONG_PATH];
210+
if (xutftowcs_long_path(wpathname, pathname) < 0)
211211
return -1;
212212

213213
/* read-only files cannot be removed */
@@ -236,7 +236,7 @@ static int is_dir_empty(const wchar_t *wpath)
236236
{
237237
WIN32_FIND_DATAW findbuf;
238238
HANDLE handle;
239-
wchar_t wbuf[MAX_PATH + 2];
239+
wchar_t wbuf[MAX_LONG_PATH + 2];
240240
wcscpy(wbuf, wpath);
241241
wcscat(wbuf, L"\\*");
242242
handle = FindFirstFileW(wbuf, &findbuf);
@@ -257,8 +257,8 @@ static int is_dir_empty(const wchar_t *wpath)
257257
int mingw_rmdir(const char *pathname)
258258
{
259259
int ret, tries = 0;
260-
wchar_t wpathname[MAX_PATH];
261-
if (xutftowcs_path(wpathname, pathname) < 0)
260+
wchar_t wpathname[MAX_LONG_PATH];
261+
if (xutftowcs_long_path(wpathname, pathname) < 0)
262262
return -1;
263263

264264
while ((ret = _wrmdir(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {
@@ -298,9 +298,9 @@ static int make_hidden(const wchar_t *path)
298298

299299
void mingw_mark_as_git_dir(const char *dir)
300300
{
301-
wchar_t wdir[MAX_PATH];
301+
wchar_t wdir[MAX_LONG_PATH];
302302
if (hide_dotfiles != HIDE_DOTFILES_FALSE && !is_bare_repository())
303-
if (xutftowcs_path(wdir, dir) < 0 || make_hidden(wdir))
303+
if (xutftowcs_long_path(wdir, dir) < 0 || make_hidden(wdir))
304304
warning("Failed to make '%s' hidden", dir);
305305
git_config_set("core.hideDotFiles",
306306
hide_dotfiles == HIDE_DOTFILES_FALSE ? "false" :
@@ -311,9 +311,12 @@ void mingw_mark_as_git_dir(const char *dir)
311311
int mingw_mkdir(const char *path, int mode)
312312
{
313313
int ret;
314-
wchar_t wpath[MAX_PATH];
315-
if (xutftowcs_path(wpath, path) < 0)
314+
wchar_t wpath[MAX_LONG_PATH];
315+
/* CreateDirectoryW path limit is 248 (MAX_PATH - 8.3 file name) */
316+
if (xutftowcs_path_ex(wpath, path, MAX_LONG_PATH, -1, 248,
317+
core_long_paths) < 0)
316318
return -1;
319+
317320
ret = _wmkdir(wpath);
318321
if (!ret && hide_dotfiles == HIDE_DOTFILES_TRUE) {
319322
/*
@@ -333,7 +336,7 @@ int mingw_open (const char *filename, int oflags, ...)
333336
va_list args;
334337
unsigned mode;
335338
int fd;
336-
wchar_t wfilename[MAX_PATH];
339+
wchar_t wfilename[MAX_LONG_PATH];
337340

338341
va_start(args, oflags);
339342
mode = va_arg(args, int);
@@ -342,7 +345,7 @@ int mingw_open (const char *filename, int oflags, ...)
342345
if (filename && !strcmp(filename, "/dev/null"))
343346
filename = "nul";
344347

345-
if (xutftowcs_path(wfilename, filename) < 0)
348+
if (xutftowcs_long_path(wfilename, filename) < 0)
346349
return -1;
347350
fd = _wopen(wfilename, oflags, mode);
348351

@@ -395,13 +398,13 @@ FILE *mingw_fopen (const char *filename, const char *otype)
395398
{
396399
int hide = 0;
397400
FILE *file;
398-
wchar_t wfilename[MAX_PATH], wotype[4];
401+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
399402
if (hide_dotfiles == HIDE_DOTFILES_TRUE &&
400403
basename((char*)filename)[0] == '.')
401404
hide = access(filename, F_OK);
402405
if (filename && !strcmp(filename, "/dev/null"))
403406
filename = "nul";
404-
if (xutftowcs_path(wfilename, filename) < 0 ||
407+
if (xutftowcs_long_path(wfilename, filename) < 0 ||
405408
xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
406409
return NULL;
407410
file = _wfopen(wfilename, wotype);
@@ -414,13 +417,13 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
414417
{
415418
int hide = 0;
416419
FILE *file;
417-
wchar_t wfilename[MAX_PATH], wotype[4];
420+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
418421
if (hide_dotfiles == HIDE_DOTFILES_TRUE &&
419422
basename((char*)filename)[0] == '.')
420423
hide = access(filename, F_OK);
421424
if (filename && !strcmp(filename, "/dev/null"))
422425
filename = "nul";
423-
if (xutftowcs_path(wfilename, filename) < 0 ||
426+
if (xutftowcs_long_path(wfilename, filename) < 0 ||
424427
xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
425428
return NULL;
426429
file = _wfreopen(wfilename, wotype, stream);
@@ -470,25 +473,32 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
470473

471474
int mingw_access(const char *filename, int mode)
472475
{
473-
wchar_t wfilename[MAX_PATH];
474-
if (xutftowcs_path(wfilename, filename) < 0)
476+
wchar_t wfilename[MAX_LONG_PATH];
477+
if (xutftowcs_long_path(wfilename, filename) < 0)
475478
return -1;
476479
/* X_OK is not supported by the MSVCRT version */
477480
return _waccess(wfilename, mode & ~X_OK);
478481
}
479482

483+
/* cached length of current directory for handle_long_path */
484+
static int current_directory_len = 0;
485+
480486
int mingw_chdir(const char *dirname)
481487
{
488+
int result;
482489
wchar_t wdirname[MAX_PATH];
490+
/* SetCurrentDirectoryW doesn't support long paths */
483491
if (xutftowcs_path(wdirname, dirname) < 0)
484492
return -1;
485-
return _wchdir(wdirname);
493+
result = _wchdir(wdirname);
494+
current_directory_len = GetCurrentDirectoryW(0, NULL);
495+
return result;
486496
}
487497

488498
int mingw_chmod(const char *filename, int mode)
489499
{
490-
wchar_t wfilename[MAX_PATH];
491-
if (xutftowcs_path(wfilename, filename) < 0)
500+
wchar_t wfilename[MAX_LONG_PATH];
501+
if (xutftowcs_long_path(wfilename, filename) < 0)
492502
return -1;
493503
return _wchmod(wfilename, mode);
494504
}
@@ -536,8 +546,8 @@ static int has_valid_directory_prefix(wchar_t *wfilename)
536546
static int do_lstat(int follow, const char *file_name, struct stat *buf)
537547
{
538548
WIN32_FILE_ATTRIBUTE_DATA fdata;
539-
wchar_t wfilename[MAX_PATH];
540-
if (xutftowcs_path(wfilename, file_name) < 0)
549+
wchar_t wfilename[MAX_LONG_PATH];
550+
if (xutftowcs_long_path(wfilename, file_name) < 0)
541551
return -1;
542552

543553
if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
@@ -686,8 +696,8 @@ int mingw_utime (const char *file_name, const struct utimbuf *times)
686696
FILETIME mft, aft;
687697
int fh, rc;
688698
DWORD attrs;
689-
wchar_t wfilename[MAX_PATH];
690-
if (xutftowcs_path(wfilename, file_name) < 0)
699+
wchar_t wfilename[MAX_LONG_PATH];
700+
if (xutftowcs_long_path(wfilename, file_name) < 0)
691701
return -1;
692702

693703
/* must have write permission */
@@ -735,6 +745,7 @@ unsigned int sleep (unsigned int seconds)
735745
char *mingw_mktemp(char *template)
736746
{
737747
wchar_t wtemplate[MAX_PATH];
748+
/* we need to return the path, thus no long paths here! */
738749
if (xutftowcs_path(wtemplate, template) < 0)
739750
return NULL;
740751
if (!_wmktemp(wtemplate))
@@ -1088,6 +1099,7 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen
10881099
si.hStdOutput = winansi_get_osfhandle(fhout);
10891100
si.hStdError = winansi_get_osfhandle(fherr);
10901101

1102+
/* executables and the current directory don't support long paths */
10911103
if (xutftowcs_path(wcmd, cmd) < 0)
10921104
return -1;
10931105
if (dir && xutftowcs_path(wdir, dir) < 0)
@@ -1663,8 +1675,9 @@ int mingw_rename(const char *pold, const char *pnew)
16631675
{
16641676
DWORD attrs, gle;
16651677
int tries = 0;
1666-
wchar_t wpold[MAX_PATH], wpnew[MAX_PATH];
1667-
if (xutftowcs_path(wpold, pold) < 0 || xutftowcs_path(wpnew, pnew) < 0)
1678+
wchar_t wpold[MAX_LONG_PATH], wpnew[MAX_LONG_PATH];
1679+
if (xutftowcs_long_path(wpold, pold) < 0 ||
1680+
xutftowcs_long_path(wpnew, pnew) < 0)
16681681
return -1;
16691682

16701683
/*
@@ -1946,9 +1959,9 @@ int link(const char *oldpath, const char *newpath)
19461959
{
19471960
typedef BOOL (WINAPI *T)(LPCWSTR, LPCWSTR, LPSECURITY_ATTRIBUTES);
19481961
static T create_hard_link = NULL;
1949-
wchar_t woldpath[MAX_PATH], wnewpath[MAX_PATH];
1950-
if (xutftowcs_path(woldpath, oldpath) < 0 ||
1951-
xutftowcs_path(wnewpath, newpath) < 0)
1962+
wchar_t woldpath[MAX_LONG_PATH], wnewpath[MAX_LONG_PATH];
1963+
if (xutftowcs_long_path(woldpath, oldpath) < 0 ||
1964+
xutftowcs_long_path(wnewpath, newpath) < 0)
19521965
return -1;
19531966

19541967
if (!create_hard_link) {
@@ -2163,6 +2176,68 @@ static void setup_windows_environment()
21632176
setenv("TERM", "cygwin", 1);
21642177
}
21652178

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

22552330
/* initialize Unicode console */
22562331
winansi_init();
2332+
2333+
/* init length of current directory for handle_long_path */
2334+
current_directory_len = GetCurrentDirectoryW(0, NULL);
22572335
}
22582336

22592337
int uname(struct utsname *buf)

0 commit comments

Comments
 (0)