Skip to content

Commit c8b6c1d

Browse files
kbleesdscho
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 2994178 commit c8b6c1d

File tree

8 files changed

+347
-65
lines changed

8 files changed

+347
-65
lines changed

Documentation/config/core.adoc

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

694+
core.longpaths::
695+
Enable long path (> 260) support for builtin commands in Git for
696+
Windows. This is disabled by default, as long paths are not supported
697+
by Windows Explorer, cmd.exe and the Git for Windows tool chain
698+
(msys, bash, tcl, perl...). Only enable this if you know what you're
699+
doing and are prepared to live with a few quirks.
700+
694701
core.unsetenvvars::
695702
Windows-only: comma-separated list of environment variables'
696703
names that need to be unset before spawning any other process.

compat/mingw.c

Lines changed: 135 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,27 @@ static enum hide_dotfiles_type hide_dotfiles = HIDE_DOTFILES_DOTGITONLY;
248248
static char *unset_environment_variables;
249249
int core_fscache;
250250

251+
int are_long_paths_enabled(void)
252+
{
253+
/* default to `false` during initialization */
254+
static const int fallback = 0;
255+
256+
static int enabled = -1;
257+
258+
if (enabled < 0) {
259+
/* avoid infinite recursion */
260+
if (!the_repository)
261+
return fallback;
262+
263+
if (the_repository->config &&
264+
the_repository->config->hash_initialized &&
265+
git_config_get_bool("core.longpaths", &enabled) < 0)
266+
enabled = 0;
267+
}
268+
269+
return enabled < 0 ? fallback : enabled;
270+
}
271+
251272
int mingw_core_config(const char *var, const char *value,
252273
const struct config_context *ctx UNUSED,
253274
void *cb UNUSED)
@@ -304,8 +325,8 @@ static wchar_t *normalize_ntpath(wchar_t *wbuf)
304325
int mingw_unlink(const char *pathname)
305326
{
306327
int ret, tries = 0;
307-
wchar_t wpathname[MAX_PATH];
308-
if (xutftowcs_path(wpathname, pathname) < 0)
328+
wchar_t wpathname[MAX_LONG_PATH];
329+
if (xutftowcs_long_path(wpathname, pathname) < 0)
309330
return -1;
310331

311332
if (DeleteFileW(wpathname))
@@ -337,7 +358,7 @@ static int is_dir_empty(const wchar_t *wpath)
337358
{
338359
WIN32_FIND_DATAW findbuf;
339360
HANDLE handle;
340-
wchar_t wbuf[MAX_PATH + 2];
361+
wchar_t wbuf[MAX_LONG_PATH + 2];
341362
wcscpy(wbuf, wpath);
342363
wcscat(wbuf, L"\\*");
343364
handle = FindFirstFileW(wbuf, &findbuf);
@@ -358,7 +379,7 @@ static int is_dir_empty(const wchar_t *wpath)
358379
int mingw_rmdir(const char *pathname)
359380
{
360381
int ret, tries = 0;
361-
wchar_t wpathname[MAX_PATH];
382+
wchar_t wpathname[MAX_LONG_PATH];
362383
struct stat st;
363384

364385
/*
@@ -380,7 +401,7 @@ int mingw_rmdir(const char *pathname)
380401
return -1;
381402
}
382403

383-
if (xutftowcs_path(wpathname, pathname) < 0)
404+
if (xutftowcs_long_path(wpathname, pathname) < 0)
384405
return -1;
385406

386407
while ((ret = _wrmdir(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {
@@ -459,15 +480,18 @@ static int set_hidden_flag(const wchar_t *path, int set)
459480
int mingw_mkdir(const char *path, int mode UNUSED)
460481
{
461482
int ret;
462-
wchar_t wpath[MAX_PATH];
483+
wchar_t wpath[MAX_LONG_PATH];
463484

464485
if (!is_valid_win32_path(path, 0)) {
465486
errno = EINVAL;
466487
return -1;
467488
}
468489

469-
if (xutftowcs_path(wpath, path) < 0)
490+
/* CreateDirectoryW path limit is 248 (MAX_PATH - 8.3 file name) */
491+
if (xutftowcs_path_ex(wpath, path, MAX_LONG_PATH, -1, 248,
492+
are_long_paths_enabled()) < 0)
470493
return -1;
494+
471495
ret = _wmkdir(wpath);
472496
if (!ret && needs_hiding(path))
473497
return set_hidden_flag(wpath, 1);
@@ -629,7 +653,7 @@ int mingw_open (const char *filename, int oflags, ...)
629653
va_list args;
630654
unsigned mode;
631655
int fd, create = (oflags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL);
632-
wchar_t wfilename[MAX_PATH];
656+
wchar_t wfilename[MAX_LONG_PATH];
633657
open_fn_t open_fn;
634658

635659
va_start(args, oflags);
@@ -659,7 +683,7 @@ int mingw_open (const char *filename, int oflags, ...)
659683

660684
if (filename && !strcmp(filename, "/dev/null"))
661685
wcscpy(wfilename, L"nul");
662-
else if (xutftowcs_path(wfilename, filename) < 0)
686+
else if (xutftowcs_long_path(wfilename, filename) < 0)
663687
return -1;
664688

665689
fd = open_fn(wfilename, oflags, mode);
@@ -717,14 +741,14 @@ FILE *mingw_fopen (const char *filename, const char *otype)
717741
{
718742
int hide = needs_hiding(filename);
719743
FILE *file;
720-
wchar_t wfilename[MAX_PATH], wotype[4];
744+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
721745
if (filename && !strcmp(filename, "/dev/null"))
722746
wcscpy(wfilename, L"nul");
723747
else if (!is_valid_win32_path(filename, 1)) {
724748
int create = otype && strchr(otype, 'w');
725749
errno = create ? EINVAL : ENOENT;
726750
return NULL;
727-
} else if (xutftowcs_path(wfilename, filename) < 0)
751+
} else if (xutftowcs_long_path(wfilename, filename) < 0)
728752
return NULL;
729753

730754
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -746,14 +770,14 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
746770
{
747771
int hide = needs_hiding(filename);
748772
FILE *file;
749-
wchar_t wfilename[MAX_PATH], wotype[4];
773+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
750774
if (filename && !strcmp(filename, "/dev/null"))
751775
wcscpy(wfilename, L"nul");
752776
else if (!is_valid_win32_path(filename, 1)) {
753777
int create = otype && strchr(otype, 'w');
754778
errno = create ? EINVAL : ENOENT;
755779
return NULL;
756-
} else if (xutftowcs_path(wfilename, filename) < 0)
780+
} else if (xutftowcs_long_path(wfilename, filename) < 0)
757781
return NULL;
758782

759783
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -803,7 +827,7 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
803827
HANDLE h = (HANDLE) _get_osfhandle(fd);
804828
if (GetFileType(h) != FILE_TYPE_PIPE) {
805829
if (orig == EINVAL) {
806-
wchar_t path[MAX_PATH];
830+
wchar_t path[MAX_LONG_PATH];
807831
DWORD ret = GetFinalPathNameByHandleW(h, path,
808832
ARRAY_SIZE(path), 0);
809833
UINT drive_type = ret > 0 && ret < ARRAY_SIZE(path) ?
@@ -840,27 +864,33 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
840864

841865
int mingw_access(const char *filename, int mode)
842866
{
843-
wchar_t wfilename[MAX_PATH];
867+
wchar_t wfilename[MAX_LONG_PATH];
844868
if (!strcmp("nul", filename) || !strcmp("/dev/null", filename))
845869
return 0;
846-
if (xutftowcs_path(wfilename, filename) < 0)
870+
if (xutftowcs_long_path(wfilename, filename) < 0)
847871
return -1;
848872
/* X_OK is not supported by the MSVCRT version */
849873
return _waccess(wfilename, mode & ~X_OK);
850874
}
851875

876+
/* cached length of current directory for handle_long_path */
877+
static int current_directory_len = 0;
878+
852879
int mingw_chdir(const char *dirname)
853880
{
854-
wchar_t wdirname[MAX_PATH];
855-
if (xutftowcs_path(wdirname, dirname) < 0)
881+
int result;
882+
wchar_t wdirname[MAX_LONG_PATH];
883+
if (xutftowcs_long_path(wdirname, dirname) < 0)
856884
return -1;
857-
return _wchdir(wdirname);
885+
result = _wchdir(wdirname);
886+
current_directory_len = GetCurrentDirectoryW(0, NULL);
887+
return result;
858888
}
859889

860890
int mingw_chmod(const char *filename, int mode)
861891
{
862-
wchar_t wfilename[MAX_PATH];
863-
if (xutftowcs_path(wfilename, filename) < 0)
892+
wchar_t wfilename[MAX_LONG_PATH];
893+
if (xutftowcs_long_path(wfilename, filename) < 0)
864894
return -1;
865895
return _wchmod(wfilename, mode);
866896
}
@@ -908,8 +938,8 @@ static int has_valid_directory_prefix(wchar_t *wfilename)
908938
static int do_lstat(int follow, const char *file_name, struct stat *buf)
909939
{
910940
WIN32_FILE_ATTRIBUTE_DATA fdata;
911-
wchar_t wfilename[MAX_PATH];
912-
if (xutftowcs_path(wfilename, file_name) < 0)
941+
wchar_t wfilename[MAX_LONG_PATH];
942+
if (xutftowcs_long_path(wfilename, file_name) < 0)
913943
return -1;
914944

915945
if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
@@ -1080,10 +1110,10 @@ int mingw_utime (const char *file_name, const struct utimbuf *times)
10801110
FILETIME mft, aft;
10811111
int rc;
10821112
DWORD attrs;
1083-
wchar_t wfilename[MAX_PATH];
1113+
wchar_t wfilename[MAX_LONG_PATH];
10841114
HANDLE osfilehandle;
10851115

1086-
if (xutftowcs_path(wfilename, file_name) < 0)
1116+
if (xutftowcs_long_path(wfilename, file_name) < 0)
10871117
return -1;
10881118

10891119
/* must have write permission */
@@ -1166,6 +1196,7 @@ char *mingw_mktemp(char *template)
11661196
wchar_t wtemplate[MAX_PATH];
11671197
int offset = 0;
11681198

1199+
/* we need to return the path, thus no long paths here! */
11691200
if (xutftowcs_path(wtemplate, template) < 0)
11701201
return NULL;
11711202

@@ -1807,6 +1838,10 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen
18071838

18081839
if (*argv && !strcmp(cmd, *argv))
18091840
wcmd[0] = L'\0';
1841+
/*
1842+
* Paths to executables and to the current directory do not support
1843+
* long paths, therefore we cannot use xutftowcs_long_path() here.
1844+
*/
18101845
else if (xutftowcs_path(wcmd, cmd) < 0)
18111846
return -1;
18121847
if (dir && xutftowcs_path(wdir, dir) < 0)
@@ -2496,12 +2531,12 @@ int mingw_rename(const char *pold, const char *pnew)
24962531
static int supports_file_rename_info_ex = 1;
24972532
DWORD attrs, gle;
24982533
int tries = 0;
2499-
wchar_t wpold[MAX_PATH], wpnew[MAX_PATH];
2534+
wchar_t wpold[MAX_LONG_PATH], wpnew[MAX_LONG_PATH];
25002535
int wpnew_len;
25012536

2502-
if (xutftowcs_path(wpold, pold) < 0)
2537+
if (xutftowcs_long_path(wpold, pold) < 0)
25032538
return -1;
2504-
wpnew_len = xutftowcs_path(wpnew, pnew);
2539+
wpnew_len = xutftowcs_long_path(wpnew, pnew);
25052540
if (wpnew_len < 0)
25062541
return -1;
25072542

@@ -2895,9 +2930,9 @@ int mingw_raise(int sig)
28952930

28962931
int link(const char *oldpath, const char *newpath)
28972932
{
2898-
wchar_t woldpath[MAX_PATH], wnewpath[MAX_PATH];
2899-
if (xutftowcs_path(woldpath, oldpath) < 0 ||
2900-
xutftowcs_path(wnewpath, newpath) < 0)
2933+
wchar_t woldpath[MAX_LONG_PATH], wnewpath[MAX_LONG_PATH];
2934+
if (xutftowcs_long_path(woldpath, oldpath) < 0 ||
2935+
xutftowcs_long_path(wnewpath, newpath) < 0)
29012936
return -1;
29022937

29032938
if (!CreateHardLinkW(wnewpath, woldpath, NULL)) {
@@ -2965,8 +3000,8 @@ int mingw_is_mount_point(struct strbuf *path)
29653000
{
29663001
WIN32_FIND_DATAW findbuf = { 0 };
29673002
HANDLE handle;
2968-
wchar_t wfilename[MAX_PATH];
2969-
int wlen = xutftowcs_path(wfilename, path->buf);
3003+
wchar_t wfilename[MAX_LONG_PATH];
3004+
int wlen = xutftowcs_long_path(wfilename, path->buf);
29703005
if (wlen < 0)
29713006
die(_("could not get long path for '%s'"), path->buf);
29723007

@@ -3114,9 +3149,9 @@ static size_t append_system_bin_dirs(char *path, size_t size)
31143149

31153150
static int is_system32_path(const char *path)
31163151
{
3117-
WCHAR system32[MAX_PATH], wpath[MAX_PATH];
3152+
WCHAR system32[MAX_LONG_PATH], wpath[MAX_LONG_PATH];
31183153

3119-
if (xutftowcs_path(wpath, path) < 0 ||
3154+
if (xutftowcs_long_path(wpath, path) < 0 ||
31203155
!GetSystemDirectoryW(system32, ARRAY_SIZE(system32)) ||
31213156
_wcsicmp(system32, wpath))
31223157
return 0;
@@ -3528,6 +3563,68 @@ int is_valid_win32_path(const char *path, int allow_literal_nul)
35283563
}
35293564
}
35303565

3566+
int handle_long_path(wchar_t *path, int len, int max_path, int expand)
3567+
{
3568+
int result;
3569+
wchar_t buf[MAX_LONG_PATH];
3570+
3571+
/*
3572+
* we don't need special handling if path is relative to the current
3573+
* directory, and current directory + path don't exceed the desired
3574+
* max_path limit. This should cover > 99 % of cases with minimal
3575+
* performance impact (git almost always uses relative paths).
3576+
*/
3577+
if ((len < 2 || (!is_dir_sep(path[0]) && path[1] != ':')) &&
3578+
(current_directory_len + len < max_path))
3579+
return len;
3580+
3581+
/*
3582+
* handle everything else:
3583+
* - absolute paths: "C:\dir\file"
3584+
* - absolute UNC paths: "\\server\share\dir\file"
3585+
* - absolute paths on current drive: "\dir\file"
3586+
* - relative paths on other drive: "X:file"
3587+
* - prefixed paths: "\\?\...", "\\.\..."
3588+
*/
3589+
3590+
/* convert to absolute path using GetFullPathNameW */
3591+
result = GetFullPathNameW(path, MAX_LONG_PATH, buf, NULL);
3592+
if (!result) {
3593+
errno = err_win_to_posix(GetLastError());
3594+
return -1;
3595+
}
3596+
3597+
/*
3598+
* return absolute path if it fits within max_path (even if
3599+
* "cwd + path" doesn't due to '..' components)
3600+
*/
3601+
if (result < max_path) {
3602+
wcscpy(path, buf);
3603+
return result;
3604+
}
3605+
3606+
/* error out if we shouldn't expand the path or buf is too small */
3607+
if (!expand || result >= MAX_LONG_PATH - 6) {
3608+
errno = ENAMETOOLONG;
3609+
return -1;
3610+
}
3611+
3612+
/* prefix full path with "\\?\" or "\\?\UNC\" */
3613+
if (buf[0] == '\\') {
3614+
/* ...unless already prefixed */
3615+
if (buf[1] == '\\' && (buf[2] == '?' || buf[2] == '.'))
3616+
return len;
3617+
3618+
wcscpy(path, L"\\\\?\\UNC\\");
3619+
wcscpy(path + 8, buf + 2);
3620+
return result + 6;
3621+
} else {
3622+
wcscpy(path, L"\\\\?\\");
3623+
wcscpy(path + 4, buf);
3624+
return result + 4;
3625+
}
3626+
}
3627+
35313628
#if !defined(_MSC_VER)
35323629
/*
35333630
* Disable MSVCRT command line wildcard expansion (__getmainargs called from
@@ -3690,6 +3787,9 @@ int wmain(int argc, const wchar_t **wargv)
36903787
/* initialize Unicode console */
36913788
winansi_init();
36923789

3790+
/* init length of current directory for handle_long_path */
3791+
current_directory_len = GetCurrentDirectoryW(0, NULL);
3792+
36933793
/* invoke the real main() using our utf8 version of argv. */
36943794
exit_status = main(argc, argv);
36953795

0 commit comments

Comments
 (0)