Skip to content
This repository was archived by the owner on Nov 9, 2017. It is now read-only.

Commit 1ccf877

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: #122 (comment) 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 91fb22d commit 1ccf877

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
@@ -637,6 +637,13 @@ core.fscache::
637637
Git for Windows uses this to bulk-read and cache lstat data of entire
638638
directories (instead of doing lstat file by file).
639639

640+
core.longpaths::
641+
Enable long path (> 260) support for builtin commands in Git for
642+
Windows. This is disabled by default, as long paths are not supported
643+
by Windows Explorer, cmd.exe and the Git for Windows tool chain
644+
(msys, bash, tcl, perl...). Only enable this if you know what you're
645+
doing and are prepared to live with a few quirks.
646+
640647
core.createObject::
641648
You can set this to 'link', in which case a hardlink followed by
642649
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
@@ -650,6 +650,8 @@ extern enum hide_dotfiles_type hide_dotfiles;
650650

651651
extern int core_fscache;
652652

653+
extern int core_long_paths;
654+
653655
enum branch_track {
654656
BRANCH_TRACK_UNSPECIFIED = -1,
655657
BRANCH_TRACK_NEVER = 0,

compat/mingw.c

Lines changed: 107 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,8 @@ static int ask_yes_no_if_possible(const char *format, ...)
204204
int mingw_unlink(const char *pathname)
205205
{
206206
int ret, tries = 0;
207-
wchar_t wpathname[MAX_PATH];
208-
if (xutftowcs_path(wpathname, pathname) < 0)
207+
wchar_t wpathname[MAX_LONG_PATH];
208+
if (xutftowcs_long_path(wpathname, pathname) < 0)
209209
return -1;
210210

211211
/* read-only files cannot be removed */
@@ -234,7 +234,7 @@ static int is_dir_empty(const wchar_t *wpath)
234234
{
235235
WIN32_FIND_DATAW findbuf;
236236
HANDLE handle;
237-
wchar_t wbuf[MAX_PATH + 2];
237+
wchar_t wbuf[MAX_LONG_PATH + 2];
238238
wcscpy(wbuf, wpath);
239239
wcscat(wbuf, L"\\*");
240240
handle = FindFirstFileW(wbuf, &findbuf);
@@ -255,8 +255,8 @@ static int is_dir_empty(const wchar_t *wpath)
255255
int mingw_rmdir(const char *pathname)
256256
{
257257
int ret, tries = 0;
258-
wchar_t wpathname[MAX_PATH];
259-
if (xutftowcs_path(wpathname, pathname) < 0)
258+
wchar_t wpathname[MAX_LONG_PATH];
259+
if (xutftowcs_long_path(wpathname, pathname) < 0)
260260
return -1;
261261

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

297297
void mingw_mark_as_git_dir(const char *dir)
298298
{
299-
wchar_t wdir[MAX_PATH];
299+
wchar_t wdir[MAX_LONG_PATH];
300300
if (hide_dotfiles != HIDE_DOTFILES_FALSE && !is_bare_repository())
301-
if (xutftowcs_path(wdir, dir) < 0 || make_hidden(wdir))
301+
if (xutftowcs_long_path(wdir, dir) < 0 || make_hidden(wdir))
302302
warning("Failed to make '%s' hidden", dir);
303303
git_config_set("core.hideDotFiles",
304304
hide_dotfiles == HIDE_DOTFILES_FALSE ? "false" :
@@ -309,9 +309,12 @@ void mingw_mark_as_git_dir(const char *dir)
309309
int mingw_mkdir(const char *path, int mode)
310310
{
311311
int ret;
312-
wchar_t wpath[MAX_PATH];
313-
if (xutftowcs_path(wpath, path) < 0)
312+
wchar_t wpath[MAX_LONG_PATH];
313+
/* CreateDirectoryW path limit is 248 (MAX_PATH - 8.3 file name) */
314+
if (xutftowcs_path_ex(wpath, path, MAX_LONG_PATH, -1, 248,
315+
core_long_paths) < 0)
314316
return -1;
317+
315318
ret = _wmkdir(wpath);
316319
if (!ret && hide_dotfiles == HIDE_DOTFILES_TRUE) {
317320
/*
@@ -331,7 +334,7 @@ int mingw_open (const char *filename, int oflags, ...)
331334
va_list args;
332335
unsigned mode;
333336
int fd;
334-
wchar_t wfilename[MAX_PATH];
337+
wchar_t wfilename[MAX_LONG_PATH];
335338

336339
va_start(args, oflags);
337340
mode = va_arg(args, int);
@@ -340,7 +343,7 @@ int mingw_open (const char *filename, int oflags, ...)
340343
if (filename && !strcmp(filename, "/dev/null"))
341344
filename = "nul";
342345

343-
if (xutftowcs_path(wfilename, filename) < 0)
346+
if (xutftowcs_long_path(wfilename, filename) < 0)
344347
return -1;
345348
fd = _wopen(wfilename, oflags, mode);
346349

@@ -393,13 +396,13 @@ FILE *mingw_fopen (const char *filename, const char *otype)
393396
{
394397
int hide = 0;
395398
FILE *file;
396-
wchar_t wfilename[MAX_PATH], wotype[4];
399+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
397400
if (hide_dotfiles == HIDE_DOTFILES_TRUE &&
398401
basename((char*)filename)[0] == '.')
399402
hide = access(filename, F_OK);
400403
if (filename && !strcmp(filename, "/dev/null"))
401404
filename = "nul";
402-
if (xutftowcs_path(wfilename, filename) < 0 ||
405+
if (xutftowcs_long_path(wfilename, filename) < 0 ||
403406
xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
404407
return NULL;
405408
file = _wfopen(wfilename, wotype);
@@ -412,13 +415,13 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
412415
{
413416
int hide = 0;
414417
FILE *file;
415-
wchar_t wfilename[MAX_PATH], wotype[4];
418+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
416419
if (hide_dotfiles == HIDE_DOTFILES_TRUE &&
417420
basename((char*)filename)[0] == '.')
418421
hide = access(filename, F_OK);
419422
if (filename && !strcmp(filename, "/dev/null"))
420423
filename = "nul";
421-
if (xutftowcs_path(wfilename, filename) < 0 ||
424+
if (xutftowcs_long_path(wfilename, filename) < 0 ||
422425
xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
423426
return NULL;
424427
file = _wfreopen(wfilename, wotype, stream);
@@ -451,25 +454,32 @@ int mingw_fflush(FILE *stream)
451454

452455
int mingw_access(const char *filename, int mode)
453456
{
454-
wchar_t wfilename[MAX_PATH];
455-
if (xutftowcs_path(wfilename, filename) < 0)
457+
wchar_t wfilename[MAX_LONG_PATH];
458+
if (xutftowcs_long_path(wfilename, filename) < 0)
456459
return -1;
457460
/* X_OK is not supported by the MSVCRT version */
458461
return _waccess(wfilename, mode & ~X_OK);
459462
}
460463

464+
/* cached length of current directory for handle_long_path */
465+
static int current_directory_len = 0;
466+
461467
int mingw_chdir(const char *dirname)
462468
{
469+
int result;
463470
wchar_t wdirname[MAX_PATH];
471+
/* SetCurrentDirectoryW doesn't support long paths */
464472
if (xutftowcs_path(wdirname, dirname) < 0)
465473
return -1;
466-
return _wchdir(wdirname);
474+
result = _wchdir(wdirname);
475+
current_directory_len = GetCurrentDirectoryW(0, NULL);
476+
return result;
467477
}
468478

469479
int mingw_chmod(const char *filename, int mode)
470480
{
471-
wchar_t wfilename[MAX_PATH];
472-
if (xutftowcs_path(wfilename, filename) < 0)
481+
wchar_t wfilename[MAX_LONG_PATH];
482+
if (xutftowcs_long_path(wfilename, filename) < 0)
473483
return -1;
474484
return _wchmod(wfilename, mode);
475485
}
@@ -484,8 +494,8 @@ int mingw_chmod(const char *filename, int mode)
484494
static int do_lstat(int follow, const char *file_name, struct stat *buf)
485495
{
486496
WIN32_FILE_ATTRIBUTE_DATA fdata;
487-
wchar_t wfilename[MAX_PATH];
488-
if (xutftowcs_path(wfilename, file_name) < 0)
497+
wchar_t wfilename[MAX_LONG_PATH];
498+
if (xutftowcs_long_path(wfilename, file_name) < 0)
489499
return -1;
490500

491501
if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
@@ -628,8 +638,8 @@ int mingw_utime (const char *file_name, const struct utimbuf *times)
628638
FILETIME mft, aft;
629639
int fh, rc;
630640
DWORD attrs;
631-
wchar_t wfilename[MAX_PATH];
632-
if (xutftowcs_path(wfilename, file_name) < 0)
641+
wchar_t wfilename[MAX_LONG_PATH];
642+
if (xutftowcs_long_path(wfilename, file_name) < 0)
633643
return -1;
634644

635645
/* must have write permission */
@@ -677,6 +687,7 @@ unsigned int sleep (unsigned int seconds)
677687
char *mingw_mktemp(char *template)
678688
{
679689
wchar_t wtemplate[MAX_PATH];
690+
/* we need to return the path, thus no long paths here! */
680691
if (xutftowcs_path(wtemplate, template) < 0)
681692
return NULL;
682693
if (!_wmktemp(wtemplate))
@@ -1030,6 +1041,7 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen
10301041
si.hStdOutput = winansi_get_osfhandle(fhout);
10311042
si.hStdError = winansi_get_osfhandle(fherr);
10321043

1044+
/* executables and the current directory don't support long paths */
10331045
if (xutftowcs_path(wcmd, cmd) < 0)
10341046
return -1;
10351047
if (dir && xutftowcs_path(wdir, dir) < 0)
@@ -1605,8 +1617,9 @@ int mingw_rename(const char *pold, const char *pnew)
16051617
{
16061618
DWORD attrs, gle;
16071619
int tries = 0;
1608-
wchar_t wpold[MAX_PATH], wpnew[MAX_PATH];
1609-
if (xutftowcs_path(wpold, pold) < 0 || xutftowcs_path(wpnew, pnew) < 0)
1620+
wchar_t wpold[MAX_LONG_PATH], wpnew[MAX_LONG_PATH];
1621+
if (xutftowcs_long_path(wpold, pold) < 0 ||
1622+
xutftowcs_long_path(wpnew, pnew) < 0)
16101623
return -1;
16111624

16121625
/*
@@ -1882,9 +1895,9 @@ int link(const char *oldpath, const char *newpath)
18821895
{
18831896
typedef BOOL (WINAPI *T)(LPCWSTR, LPCWSTR, LPSECURITY_ATTRIBUTES);
18841897
static T create_hard_link = NULL;
1885-
wchar_t woldpath[MAX_PATH], wnewpath[MAX_PATH];
1886-
if (xutftowcs_path(woldpath, oldpath) < 0 ||
1887-
xutftowcs_path(wnewpath, newpath) < 0)
1898+
wchar_t woldpath[MAX_LONG_PATH], wnewpath[MAX_LONG_PATH];
1899+
if (xutftowcs_long_path(woldpath, oldpath) < 0 ||
1900+
xutftowcs_long_path(wnewpath, newpath) < 0)
18881901
return -1;
18891902

18901903
if (!create_hard_link) {
@@ -2065,6 +2078,68 @@ int xwcstoutf(char *utf, const wchar_t *wcs, size_t utflen)
20652078
return -1;
20662079
}
20672080

2081+
int handle_long_path(wchar_t *path, int len, int max_path, int expand)
2082+
{
2083+
int result;
2084+
wchar_t buf[MAX_LONG_PATH];
2085+
2086+
/*
2087+
* we don't need special handling if path is relative to the current
2088+
* directory, and current directory + path don't exceed the desired
2089+
* max_path limit. This should cover > 99 % of cases with minimal
2090+
* performance impact (git almost always uses relative paths).
2091+
*/
2092+
if ((len < 2 || (!is_dir_sep(path[0]) && path[1] != ':')) &&
2093+
(current_directory_len + len < max_path))
2094+
return len;
2095+
2096+
/*
2097+
* handle everything else:
2098+
* - absolute paths: "C:\dir\file"
2099+
* - absolute UNC paths: "\\server\share\dir\file"
2100+
* - absolute paths on current drive: "\dir\file"
2101+
* - relative paths on other drive: "X:file"
2102+
* - prefixed paths: "\\?\...", "\\.\..."
2103+
*/
2104+
2105+
/* convert to absolute path using GetFullPathNameW */
2106+
result = GetFullPathNameW(path, MAX_LONG_PATH, buf, NULL);
2107+
if (!result) {
2108+
errno = err_win_to_posix(GetLastError());
2109+
return -1;
2110+
}
2111+
2112+
/*
2113+
* return absolute path if it fits within max_path (even if
2114+
* "cwd + path" doesn't due to '..' components)
2115+
*/
2116+
if (result < max_path) {
2117+
wcscpy(path, buf);
2118+
return result;
2119+
}
2120+
2121+
/* error out if we shouldn't expand the path or buf is too small */
2122+
if (!expand || result >= MAX_LONG_PATH - 6) {
2123+
errno = ENAMETOOLONG;
2124+
return -1;
2125+
}
2126+
2127+
/* prefix full path with "\\?\" or "\\?\UNC\" */
2128+
if (buf[0] == '\\') {
2129+
/* ...unless already prefixed */
2130+
if (buf[1] == '\\' && (buf[2] == '?' || buf[2] == '.'))
2131+
return len;
2132+
2133+
wcscpy(path, L"\\\\?\\UNC\\");
2134+
wcscpy(path + 8, buf + 2);
2135+
return result + 6;
2136+
} else {
2137+
wcscpy(path, L"\\\\?\\");
2138+
wcscpy(path + 4, buf);
2139+
return result + 4;
2140+
}
2141+
}
2142+
20682143
/*
20692144
* Disable MSVCRT command line wildcard expansion (__getmainargs called from
20702145
* mingw startup code, see init.c in mingw runtime).
@@ -2168,4 +2243,7 @@ void mingw_startup()
21682243

21692244
/* initialize Unicode console */
21702245
winansi_init();
2246+
2247+
/* init length of current directory for handle_long_path */
2248+
current_directory_len = GetCurrentDirectoryW(0, NULL);
21712249
}

0 commit comments

Comments
 (0)