Skip to content

Commit a2b0abe

Browse files
dschoGit for Windows Build Agent
authored andcommitted
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 b69d460 commit a2b0abe

File tree

10 files changed

+323
-55
lines changed

10 files changed

+323
-55
lines changed

Documentation/config.txt

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

769+
core.longpaths::
770+
Enable long path (> 260) support for builtin commands in Git for
771+
Windows. This is disabled by default, as long paths are not supported
772+
by Windows Explorer, cmd.exe and the Git for Windows tool chain
773+
(msys, bash, tcl, perl...). Only enable this if you know what you're
774+
doing and are prepared to live with a few quirks.
775+
769776
core.createObject::
770777
You can set this to 'link', in which case a hardlink followed by
771778
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
@@ -731,6 +731,8 @@ extern enum hide_dotfiles_type hide_dotfiles;
731731

732732
extern int core_fscache;
733733

734+
extern int core_long_paths;
735+
734736
enum branch_track {
735737
BRANCH_TRACK_UNSPECIFIED = -1,
736738
BRANCH_TRACK_NEVER = 0,

compat/mingw.c

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

212212
/* read-only files cannot be removed */
@@ -235,7 +235,7 @@ static int is_dir_empty(const wchar_t *wpath)
235235
{
236236
WIN32_FIND_DATAW findbuf;
237237
HANDLE handle;
238-
wchar_t wbuf[MAX_PATH + 2];
238+
wchar_t wbuf[MAX_LONG_PATH + 2];
239239
wcscpy(wbuf, wpath);
240240
wcscat(wbuf, L"\\*");
241241
handle = FindFirstFileW(wbuf, &findbuf);
@@ -256,8 +256,8 @@ static int is_dir_empty(const wchar_t *wpath)
256256
int mingw_rmdir(const char *pathname)
257257
{
258258
int ret, tries = 0;
259-
wchar_t wpathname[MAX_PATH];
260-
if (xutftowcs_path(wpathname, pathname) < 0)
259+
wchar_t wpathname[MAX_LONG_PATH];
260+
if (xutftowcs_long_path(wpathname, pathname) < 0)
261261
return -1;
262262

263263
while ((ret = _wrmdir(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {
@@ -332,9 +332,12 @@ static int set_hidden_flag(const wchar_t *path, int set)
332332
int mingw_mkdir(const char *path, int mode)
333333
{
334334
int ret;
335-
wchar_t wpath[MAX_PATH];
336-
if (xutftowcs_path(wpath, path) < 0)
335+
wchar_t wpath[MAX_LONG_PATH];
336+
/* CreateDirectoryW path limit is 248 (MAX_PATH - 8.3 file name) */
337+
if (xutftowcs_path_ex(wpath, path, MAX_LONG_PATH, -1, 248,
338+
core_long_paths) < 0)
337339
return -1;
340+
338341
ret = _wmkdir(wpath);
339342
if (!ret && needs_hiding(path))
340343
return set_hidden_flag(wpath, 1);
@@ -346,7 +349,7 @@ int mingw_open (const char *filename, int oflags, ...)
346349
va_list args;
347350
unsigned mode;
348351
int fd;
349-
wchar_t wfilename[MAX_PATH];
352+
wchar_t wfilename[MAX_LONG_PATH];
350353

351354
va_start(args, oflags);
352355
mode = va_arg(args, int);
@@ -355,7 +358,7 @@ int mingw_open (const char *filename, int oflags, ...)
355358
if (filename && !strcmp(filename, "/dev/null"))
356359
filename = "nul";
357360

358-
if (xutftowcs_path(wfilename, filename) < 0)
361+
if (xutftowcs_long_path(wfilename, filename) < 0)
359362
return -1;
360363
fd = _wopen(wfilename, oflags, mode);
361364

@@ -412,10 +415,10 @@ FILE *mingw_fopen (const char *filename, const char *otype)
412415
{
413416
int hide = needs_hiding(filename);
414417
FILE *file;
415-
wchar_t wfilename[MAX_PATH], wotype[4];
418+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
416419
if (filename && !strcmp(filename, "/dev/null"))
417420
filename = "nul";
418-
if (xutftowcs_path(wfilename, filename) < 0 ||
421+
if (xutftowcs_long_path(wfilename, filename) < 0 ||
419422
xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
420423
return NULL;
421424
if (hide && !access(filename, F_OK) && set_hidden_flag(wfilename, 0)) {
@@ -432,10 +435,10 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
432435
{
433436
int hide = needs_hiding(filename);
434437
FILE *file;
435-
wchar_t wfilename[MAX_PATH], wotype[4];
438+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
436439
if (filename && !strcmp(filename, "/dev/null"))
437440
filename = "nul";
438-
if (xutftowcs_path(wfilename, filename) < 0 ||
441+
if (xutftowcs_long_path(wfilename, filename) < 0 ||
439442
xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
440443
return NULL;
441444
if (hide && !access(filename, F_OK) && set_hidden_flag(wfilename, 0)) {
@@ -489,25 +492,32 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
489492

490493
int mingw_access(const char *filename, int mode)
491494
{
492-
wchar_t wfilename[MAX_PATH];
493-
if (xutftowcs_path(wfilename, filename) < 0)
495+
wchar_t wfilename[MAX_LONG_PATH];
496+
if (xutftowcs_long_path(wfilename, filename) < 0)
494497
return -1;
495498
/* X_OK is not supported by the MSVCRT version */
496499
return _waccess(wfilename, mode & ~X_OK);
497500
}
498501

502+
/* cached length of current directory for handle_long_path */
503+
static int current_directory_len = 0;
504+
499505
int mingw_chdir(const char *dirname)
500506
{
507+
int result;
501508
wchar_t wdirname[MAX_PATH];
509+
/* SetCurrentDirectoryW doesn't support long paths */
502510
if (xutftowcs_path(wdirname, dirname) < 0)
503511
return -1;
504-
return _wchdir(wdirname);
512+
result = _wchdir(wdirname);
513+
current_directory_len = GetCurrentDirectoryW(0, NULL);
514+
return result;
505515
}
506516

507517
int mingw_chmod(const char *filename, int mode)
508518
{
509-
wchar_t wfilename[MAX_PATH];
510-
if (xutftowcs_path(wfilename, filename) < 0)
519+
wchar_t wfilename[MAX_LONG_PATH];
520+
if (xutftowcs_long_path(wfilename, filename) < 0)
511521
return -1;
512522
return _wchmod(wfilename, mode);
513523
}
@@ -555,8 +565,8 @@ static int has_valid_directory_prefix(wchar_t *wfilename)
555565
static int do_lstat(int follow, const char *file_name, struct stat *buf)
556566
{
557567
WIN32_FILE_ATTRIBUTE_DATA fdata;
558-
wchar_t wfilename[MAX_PATH];
559-
if (xutftowcs_path(wfilename, file_name) < 0)
568+
wchar_t wfilename[MAX_LONG_PATH];
569+
if (xutftowcs_long_path(wfilename, file_name) < 0)
560570
return -1;
561571

562572
if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
@@ -705,8 +715,8 @@ int mingw_utime (const char *file_name, const struct utimbuf *times)
705715
FILETIME mft, aft;
706716
int fh, rc;
707717
DWORD attrs;
708-
wchar_t wfilename[MAX_PATH];
709-
if (xutftowcs_path(wfilename, file_name) < 0)
718+
wchar_t wfilename[MAX_LONG_PATH];
719+
if (xutftowcs_long_path(wfilename, file_name) < 0)
710720
return -1;
711721

712722
/* must have write permission */
@@ -754,6 +764,7 @@ unsigned int sleep (unsigned int seconds)
754764
char *mingw_mktemp(char *template)
755765
{
756766
wchar_t wtemplate[MAX_PATH];
767+
/* we need to return the path, thus no long paths here! */
757768
if (xutftowcs_path(wtemplate, template) < 0)
758769
return NULL;
759770
if (!_wmktemp(wtemplate))
@@ -1104,6 +1115,7 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen
11041115
si.hStdOutput = winansi_get_osfhandle(fhout);
11051116
si.hStdError = winansi_get_osfhandle(fherr);
11061117

1118+
/* executables and the current directory don't support long paths */
11071119
if (xutftowcs_path(wcmd, cmd) < 0)
11081120
return -1;
11091121
if (dir && xutftowcs_path(wdir, dir) < 0)
@@ -1679,8 +1691,9 @@ int mingw_rename(const char *pold, const char *pnew)
16791691
{
16801692
DWORD attrs, gle;
16811693
int tries = 0;
1682-
wchar_t wpold[MAX_PATH], wpnew[MAX_PATH];
1683-
if (xutftowcs_path(wpold, pold) < 0 || xutftowcs_path(wpnew, pnew) < 0)
1694+
wchar_t wpold[MAX_LONG_PATH], wpnew[MAX_LONG_PATH];
1695+
if (xutftowcs_long_path(wpold, pold) < 0 ||
1696+
xutftowcs_long_path(wpnew, pnew) < 0)
16841697
return -1;
16851698

16861699
/*
@@ -1920,9 +1933,9 @@ int link(const char *oldpath, const char *newpath)
19201933
{
19211934
typedef BOOL (WINAPI *T)(LPCWSTR, LPCWSTR, LPSECURITY_ATTRIBUTES);
19221935
static T create_hard_link = NULL;
1923-
wchar_t woldpath[MAX_PATH], wnewpath[MAX_PATH];
1924-
if (xutftowcs_path(woldpath, oldpath) < 0 ||
1925-
xutftowcs_path(wnewpath, newpath) < 0)
1936+
wchar_t woldpath[MAX_LONG_PATH], wnewpath[MAX_LONG_PATH];
1937+
if (xutftowcs_long_path(woldpath, oldpath) < 0 ||
1938+
xutftowcs_long_path(wnewpath, newpath) < 0)
19261939
return -1;
19271940

19281941
if (!create_hard_link) {
@@ -2135,6 +2148,68 @@ static void setup_windows_environment(void)
21352148
setenv("TERM", "cygwin", 1);
21362149
}
21372150

2151+
int handle_long_path(wchar_t *path, int len, int max_path, int expand)
2152+
{
2153+
int result;
2154+
wchar_t buf[MAX_LONG_PATH];
2155+
2156+
/*
2157+
* we don't need special handling if path is relative to the current
2158+
* directory, and current directory + path don't exceed the desired
2159+
* max_path limit. This should cover > 99 % of cases with minimal
2160+
* performance impact (git almost always uses relative paths).
2161+
*/
2162+
if ((len < 2 || (!is_dir_sep(path[0]) && path[1] != ':')) &&
2163+
(current_directory_len + len < max_path))
2164+
return len;
2165+
2166+
/*
2167+
* handle everything else:
2168+
* - absolute paths: "C:\dir\file"
2169+
* - absolute UNC paths: "\\server\share\dir\file"
2170+
* - absolute paths on current drive: "\dir\file"
2171+
* - relative paths on other drive: "X:file"
2172+
* - prefixed paths: "\\?\...", "\\.\..."
2173+
*/
2174+
2175+
/* convert to absolute path using GetFullPathNameW */
2176+
result = GetFullPathNameW(path, MAX_LONG_PATH, buf, NULL);
2177+
if (!result) {
2178+
errno = err_win_to_posix(GetLastError());
2179+
return -1;
2180+
}
2181+
2182+
/*
2183+
* return absolute path if it fits within max_path (even if
2184+
* "cwd + path" doesn't due to '..' components)
2185+
*/
2186+
if (result < max_path) {
2187+
wcscpy(path, buf);
2188+
return result;
2189+
}
2190+
2191+
/* error out if we shouldn't expand the path or buf is too small */
2192+
if (!expand || result >= MAX_LONG_PATH - 6) {
2193+
errno = ENAMETOOLONG;
2194+
return -1;
2195+
}
2196+
2197+
/* prefix full path with "\\?\" or "\\?\UNC\" */
2198+
if (buf[0] == '\\') {
2199+
/* ...unless already prefixed */
2200+
if (buf[1] == '\\' && (buf[2] == '?' || buf[2] == '.'))
2201+
return len;
2202+
2203+
wcscpy(path, L"\\\\?\\UNC\\");
2204+
wcscpy(path + 8, buf + 2);
2205+
return result + 6;
2206+
} else {
2207+
wcscpy(path, L"\\\\?\\");
2208+
wcscpy(path + 4, buf);
2209+
return result + 4;
2210+
}
2211+
}
2212+
21382213
/*
21392214
* Disable MSVCRT command line wildcard expansion (__getmainargs called from
21402215
* mingw startup code, see init.c in mingw runtime).
@@ -2226,6 +2301,9 @@ void mingw_startup(void)
22262301

22272302
/* initialize Unicode console */
22282303
winansi_init();
2304+
2305+
/* init length of current directory for handle_long_path */
2306+
current_directory_len = GetCurrentDirectoryW(0, NULL);
22292307
}
22302308

22312309
int uname(struct utsname *buf)

compat/mingw.h

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,42 @@ int mingw_offset_1st_component(const char *path);
437437
#include <inttypes.h>
438438
#endif
439439

440+
/**
441+
* Max length of long paths (exceeding MAX_PATH). The actual maximum supported
442+
* by NTFS is 32,767 (* sizeof(wchar_t)), but we choose an arbitrary smaller
443+
* value to limit required stack memory.
444+
*/
445+
#define MAX_LONG_PATH 4096
446+
447+
/**
448+
* Handles paths that would exceed the MAX_PATH limit of Windows Unicode APIs.
449+
*
450+
* With expand == false, the function checks for over-long paths and fails
451+
* with ENAMETOOLONG. The path parameter is not modified, except if cwd + path
452+
* exceeds max_path, but the resulting absolute path doesn't (e.g. due to
453+
* eliminating '..' components). The path parameter must point to a buffer
454+
* of max_path wide characters.
455+
*
456+
* With expand == true, an over-long path is automatically converted in place
457+
* to an absolute path prefixed with '\\?\', and the new length is returned.
458+
* The path parameter must point to a buffer of MAX_LONG_PATH wide characters.
459+
*
460+
* Parameters:
461+
* path: path to check and / or convert
462+
* len: size of path on input (number of wide chars without \0)
463+
* max_path: max short path length to check (usually MAX_PATH = 260, but just
464+
* 248 for CreateDirectoryW)
465+
* expand: false to only check the length, true to expand the path to a
466+
* '\\?\'-prefixed absolute path
467+
*
468+
* Return:
469+
* length of the resulting path, or -1 on failure
470+
*
471+
* Errors:
472+
* ENAMETOOLONG if path is too long
473+
*/
474+
int handle_long_path(wchar_t *path, int len, int max_path, int expand);
475+
440476
/**
441477
* Converts UTF-8 encoded string to UTF-16LE.
442478
*
@@ -495,18 +531,49 @@ static inline int xutftowcs(wchar_t *wcs, const char *utf, size_t wcslen)
495531
}
496532

497533
/**
498-
* Simplified file system specific variant of xutftowcsn, assumes output
499-
* buffer size is MAX_PATH wide chars and input string is \0-terminated,
500-
* fails with ENAMETOOLONG if input string is too long.
534+
* Simplified file system specific wrapper of xutftowcsn and handle_long_path.
535+
* Converts ERANGE to ENAMETOOLONG. If expand is true, wcs must be at least
536+
* MAX_LONG_PATH wide chars (see handle_long_path).
501537
*/
502-
static inline int xutftowcs_path(wchar_t *wcs, const char *utf)
538+
static inline int xutftowcs_path_ex(wchar_t *wcs, const char *utf,
539+
size_t wcslen, int utflen, int max_path, int expand)
503540
{
504-
int result = xutftowcsn(wcs, utf, MAX_PATH, -1);
541+
int result = xutftowcsn(wcs, utf, wcslen, utflen);
505542
if (result < 0 && errno == ERANGE)
506543
errno = ENAMETOOLONG;
544+
if (result >= 0)
545+
result = handle_long_path(wcs, result, max_path, expand);
507546
return result;
508547
}
509548

549+
/**
550+
* Simplified file system specific variant of xutftowcsn, assumes output
551+
* buffer size is MAX_PATH wide chars and input string is \0-terminated,
552+
* fails with ENAMETOOLONG if input string is too long. Typically used for
553+
* Windows APIs that don't support long paths, e.g. SetCurrentDirectory,
554+
* LoadLibrary, CreateProcess...
555+
*/
556+
static inline int xutftowcs_path(wchar_t *wcs, const char *utf)
557+
{
558+
return xutftowcs_path_ex(wcs, utf, MAX_PATH, -1, MAX_PATH, 0);
559+
}
560+
561+
/* need to re-declare that here as mingw.h is included before cache.h */
562+
extern int core_long_paths;
563+
564+
/**
565+
* Simplified file system specific variant of xutftowcsn for Windows APIs
566+
* that support long paths via '\\?\'-prefix, assumes output buffer size is
567+
* MAX_LONG_PATH wide chars, fails with ENAMETOOLONG if input string is too
568+
* long. The 'core.longpaths' git-config option controls whether the path
569+
* is only checked or expanded to a long path.
570+
*/
571+
static inline int xutftowcs_long_path(wchar_t *wcs, const char *utf)
572+
{
573+
return xutftowcs_path_ex(wcs, utf, MAX_LONG_PATH, -1, MAX_PATH,
574+
core_long_paths);
575+
}
576+
510577
/**
511578
* Converts UTF-16LE encoded string to UTF-8.
512579
*

0 commit comments

Comments
 (0)