Skip to content

Commit 1bd9bd5

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 f02bd51 commit 1bd9bd5

File tree

8 files changed

+347
-65
lines changed

8 files changed

+347
-65
lines changed

Documentation/config/core.txt

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
@@ -249,6 +249,27 @@ static enum hide_dotfiles_type hide_dotfiles = HIDE_DOTFILES_DOTGITONLY;
249249
static char *unset_environment_variables;
250250
int core_fscache;
251251

252+
int are_long_paths_enabled(void)
253+
{
254+
/* default to `false` during initialization */
255+
static const int fallback = 0;
256+
257+
static int enabled = -1;
258+
259+
if (enabled < 0) {
260+
/* avoid infinite recursion */
261+
if (!the_repository)
262+
return fallback;
263+
264+
if (the_repository->config &&
265+
the_repository->config->hash_initialized &&
266+
git_config_get_bool("core.longpaths", &enabled) < 0)
267+
enabled = 0;
268+
}
269+
270+
return enabled < 0 ? fallback : enabled;
271+
}
272+
252273
int mingw_core_config(const char *var, const char *value,
253274
const struct config_context *ctx UNUSED,
254275
void *cb UNUSED)
@@ -314,8 +335,8 @@ static wchar_t *normalize_ntpath(wchar_t *wbuf)
314335
int mingw_unlink(const char *pathname)
315336
{
316337
int ret, tries = 0;
317-
wchar_t wpathname[MAX_PATH];
318-
if (xutftowcs_path(wpathname, pathname) < 0)
338+
wchar_t wpathname[MAX_LONG_PATH];
339+
if (xutftowcs_long_path(wpathname, pathname) < 0)
319340
return -1;
320341

321342
if (DeleteFileW(wpathname))
@@ -347,7 +368,7 @@ static int is_dir_empty(const wchar_t *wpath)
347368
{
348369
WIN32_FIND_DATAW findbuf;
349370
HANDLE handle;
350-
wchar_t wbuf[MAX_PATH + 2];
371+
wchar_t wbuf[MAX_LONG_PATH + 2];
351372
wcscpy(wbuf, wpath);
352373
wcscat(wbuf, L"\\*");
353374
handle = FindFirstFileW(wbuf, &findbuf);
@@ -368,7 +389,7 @@ static int is_dir_empty(const wchar_t *wpath)
368389
int mingw_rmdir(const char *pathname)
369390
{
370391
int ret, tries = 0;
371-
wchar_t wpathname[MAX_PATH];
392+
wchar_t wpathname[MAX_LONG_PATH];
372393
struct stat st;
373394

374395
/*
@@ -390,7 +411,7 @@ int mingw_rmdir(const char *pathname)
390411
return -1;
391412
}
392413

393-
if (xutftowcs_path(wpathname, pathname) < 0)
414+
if (xutftowcs_long_path(wpathname, pathname) < 0)
394415
return -1;
395416

396417
while ((ret = _wrmdir(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {
@@ -469,15 +490,18 @@ static int set_hidden_flag(const wchar_t *path, int set)
469490
int mingw_mkdir(const char *path, int mode UNUSED)
470491
{
471492
int ret;
472-
wchar_t wpath[MAX_PATH];
493+
wchar_t wpath[MAX_LONG_PATH];
473494

474495
if (!is_valid_win32_path(path, 0)) {
475496
errno = EINVAL;
476497
return -1;
477498
}
478499

479-
if (xutftowcs_path(wpath, path) < 0)
500+
/* CreateDirectoryW path limit is 248 (MAX_PATH - 8.3 file name) */
501+
if (xutftowcs_path_ex(wpath, path, MAX_LONG_PATH, -1, 248,
502+
are_long_paths_enabled()) < 0)
480503
return -1;
504+
481505
ret = _wmkdir(wpath);
482506
if (!ret && needs_hiding(path))
483507
return set_hidden_flag(wpath, 1);
@@ -639,7 +663,7 @@ int mingw_open (const char *filename, int oflags, ...)
639663
va_list args;
640664
unsigned mode;
641665
int fd, create = (oflags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL);
642-
wchar_t wfilename[MAX_PATH];
666+
wchar_t wfilename[MAX_LONG_PATH];
643667
open_fn_t open_fn;
644668

645669
va_start(args, oflags);
@@ -669,7 +693,7 @@ int mingw_open (const char *filename, int oflags, ...)
669693

670694
if (filename && !strcmp(filename, "/dev/null"))
671695
wcscpy(wfilename, L"nul");
672-
else if (xutftowcs_path(wfilename, filename) < 0)
696+
else if (xutftowcs_long_path(wfilename, filename) < 0)
673697
return -1;
674698

675699
fd = open_fn(wfilename, oflags, mode);
@@ -727,14 +751,14 @@ FILE *mingw_fopen (const char *filename, const char *otype)
727751
{
728752
int hide = needs_hiding(filename);
729753
FILE *file;
730-
wchar_t wfilename[MAX_PATH], wotype[4];
754+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
731755
if (filename && !strcmp(filename, "/dev/null"))
732756
wcscpy(wfilename, L"nul");
733757
else if (!is_valid_win32_path(filename, 1)) {
734758
int create = otype && strchr(otype, 'w');
735759
errno = create ? EINVAL : ENOENT;
736760
return NULL;
737-
} else if (xutftowcs_path(wfilename, filename) < 0)
761+
} else if (xutftowcs_long_path(wfilename, filename) < 0)
738762
return NULL;
739763

740764
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -756,14 +780,14 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
756780
{
757781
int hide = needs_hiding(filename);
758782
FILE *file;
759-
wchar_t wfilename[MAX_PATH], wotype[4];
783+
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
760784
if (filename && !strcmp(filename, "/dev/null"))
761785
wcscpy(wfilename, L"nul");
762786
else if (!is_valid_win32_path(filename, 1)) {
763787
int create = otype && strchr(otype, 'w');
764788
errno = create ? EINVAL : ENOENT;
765789
return NULL;
766-
} else if (xutftowcs_path(wfilename, filename) < 0)
790+
} else if (xutftowcs_long_path(wfilename, filename) < 0)
767791
return NULL;
768792

769793
if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
@@ -813,7 +837,7 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
813837
HANDLE h = (HANDLE) _get_osfhandle(fd);
814838
if (GetFileType(h) != FILE_TYPE_PIPE) {
815839
if (orig == EINVAL) {
816-
wchar_t path[MAX_PATH];
840+
wchar_t path[MAX_LONG_PATH];
817841
DWORD ret = GetFinalPathNameByHandleW(h, path,
818842
ARRAY_SIZE(path), 0);
819843
UINT drive_type = ret > 0 && ret < ARRAY_SIZE(path) ?
@@ -850,27 +874,33 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
850874

851875
int mingw_access(const char *filename, int mode)
852876
{
853-
wchar_t wfilename[MAX_PATH];
877+
wchar_t wfilename[MAX_LONG_PATH];
854878
if (!strcmp("nul", filename) || !strcmp("/dev/null", filename))
855879
return 0;
856-
if (xutftowcs_path(wfilename, filename) < 0)
880+
if (xutftowcs_long_path(wfilename, filename) < 0)
857881
return -1;
858882
/* X_OK is not supported by the MSVCRT version */
859883
return _waccess(wfilename, mode & ~X_OK);
860884
}
861885

886+
/* cached length of current directory for handle_long_path */
887+
static int current_directory_len = 0;
888+
862889
int mingw_chdir(const char *dirname)
863890
{
864-
wchar_t wdirname[MAX_PATH];
865-
if (xutftowcs_path(wdirname, dirname) < 0)
891+
int result;
892+
wchar_t wdirname[MAX_LONG_PATH];
893+
if (xutftowcs_long_path(wdirname, dirname) < 0)
866894
return -1;
867-
return _wchdir(wdirname);
895+
result = _wchdir(wdirname);
896+
current_directory_len = GetCurrentDirectoryW(0, NULL);
897+
return result;
868898
}
869899

870900
int mingw_chmod(const char *filename, int mode)
871901
{
872-
wchar_t wfilename[MAX_PATH];
873-
if (xutftowcs_path(wfilename, filename) < 0)
902+
wchar_t wfilename[MAX_LONG_PATH];
903+
if (xutftowcs_long_path(wfilename, filename) < 0)
874904
return -1;
875905
return _wchmod(wfilename, mode);
876906
}
@@ -918,8 +948,8 @@ static int has_valid_directory_prefix(wchar_t *wfilename)
918948
static int do_lstat(int follow, const char *file_name, struct stat *buf)
919949
{
920950
WIN32_FILE_ATTRIBUTE_DATA fdata;
921-
wchar_t wfilename[MAX_PATH];
922-
if (xutftowcs_path(wfilename, file_name) < 0)
951+
wchar_t wfilename[MAX_LONG_PATH];
952+
if (xutftowcs_long_path(wfilename, file_name) < 0)
923953
return -1;
924954

925955
if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
@@ -1090,10 +1120,10 @@ int mingw_utime (const char *file_name, const struct utimbuf *times)
10901120
FILETIME mft, aft;
10911121
int rc;
10921122
DWORD attrs;
1093-
wchar_t wfilename[MAX_PATH];
1123+
wchar_t wfilename[MAX_LONG_PATH];
10941124
HANDLE osfilehandle;
10951125

1096-
if (xutftowcs_path(wfilename, file_name) < 0)
1126+
if (xutftowcs_long_path(wfilename, file_name) < 0)
10971127
return -1;
10981128

10991129
/* must have write permission */
@@ -1176,6 +1206,7 @@ char *mingw_mktemp(char *template)
11761206
wchar_t wtemplate[MAX_PATH];
11771207
int offset = 0;
11781208

1209+
/* we need to return the path, thus no long paths here! */
11791210
if (xutftowcs_path(wtemplate, template) < 0)
11801211
return NULL;
11811212

@@ -1828,6 +1859,10 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen
18281859

18291860
if (*argv && !strcmp(cmd, *argv))
18301861
wcmd[0] = L'\0';
1862+
/*
1863+
* Paths to executables and to the current directory do not support
1864+
* long paths, therefore we cannot use xutftowcs_long_path() here.
1865+
*/
18311866
else if (xutftowcs_path(wcmd, cmd) < 0)
18321867
return -1;
18331868
if (dir && xutftowcs_path(wdir, dir) < 0)
@@ -2556,12 +2591,12 @@ int mingw_rename(const char *pold, const char *pnew)
25562591
static int supports_file_rename_info_ex = 1;
25572592
DWORD attrs, gle;
25582593
int tries = 0;
2559-
wchar_t wpold[MAX_PATH], wpnew[MAX_PATH];
2594+
wchar_t wpold[MAX_LONG_PATH], wpnew[MAX_LONG_PATH];
25602595
int wpnew_len;
25612596

2562-
if (xutftowcs_path(wpold, pold) < 0)
2597+
if (xutftowcs_long_path(wpold, pold) < 0)
25632598
return -1;
2564-
wpnew_len = xutftowcs_path(wpnew, pnew);
2599+
wpnew_len = xutftowcs_long_path(wpnew, pnew);
25652600
if (wpnew_len < 0)
25662601
return -1;
25672602

@@ -2953,9 +2988,9 @@ int mingw_raise(int sig)
29532988

29542989
int link(const char *oldpath, const char *newpath)
29552990
{
2956-
wchar_t woldpath[MAX_PATH], wnewpath[MAX_PATH];
2957-
if (xutftowcs_path(woldpath, oldpath) < 0 ||
2958-
xutftowcs_path(wnewpath, newpath) < 0)
2991+
wchar_t woldpath[MAX_LONG_PATH], wnewpath[MAX_LONG_PATH];
2992+
if (xutftowcs_long_path(woldpath, oldpath) < 0 ||
2993+
xutftowcs_long_path(wnewpath, newpath) < 0)
29592994
return -1;
29602995

29612996
if (!CreateHardLinkW(wnewpath, woldpath, NULL)) {
@@ -3023,8 +3058,8 @@ int mingw_is_mount_point(struct strbuf *path)
30233058
{
30243059
WIN32_FIND_DATAW findbuf = { 0 };
30253060
HANDLE handle;
3026-
wchar_t wfilename[MAX_PATH];
3027-
int wlen = xutftowcs_path(wfilename, path->buf);
3061+
wchar_t wfilename[MAX_LONG_PATH];
3062+
int wlen = xutftowcs_long_path(wfilename, path->buf);
30283063
if (wlen < 0)
30293064
die(_("could not get long path for '%s'"), path->buf);
30303065

@@ -3169,9 +3204,9 @@ static size_t append_system_bin_dirs(char *path, size_t size)
31693204

31703205
static int is_system32_path(const char *path)
31713206
{
3172-
WCHAR system32[MAX_PATH], wpath[MAX_PATH];
3207+
WCHAR system32[MAX_LONG_PATH], wpath[MAX_LONG_PATH];
31733208

3174-
if (xutftowcs_path(wpath, path) < 0 ||
3209+
if (xutftowcs_long_path(wpath, path) < 0 ||
31753210
!GetSystemDirectoryW(system32, ARRAY_SIZE(system32)) ||
31763211
_wcsicmp(system32, wpath))
31773212
return 0;
@@ -3583,6 +3618,68 @@ int is_valid_win32_path(const char *path, int allow_literal_nul)
35833618
}
35843619
}
35853620

3621+
int handle_long_path(wchar_t *path, int len, int max_path, int expand)
3622+
{
3623+
int result;
3624+
wchar_t buf[MAX_LONG_PATH];
3625+
3626+
/*
3627+
* we don't need special handling if path is relative to the current
3628+
* directory, and current directory + path don't exceed the desired
3629+
* max_path limit. This should cover > 99 % of cases with minimal
3630+
* performance impact (git almost always uses relative paths).
3631+
*/
3632+
if ((len < 2 || (!is_dir_sep(path[0]) && path[1] != ':')) &&
3633+
(current_directory_len + len < max_path))
3634+
return len;
3635+
3636+
/*
3637+
* handle everything else:
3638+
* - absolute paths: "C:\dir\file"
3639+
* - absolute UNC paths: "\\server\share\dir\file"
3640+
* - absolute paths on current drive: "\dir\file"
3641+
* - relative paths on other drive: "X:file"
3642+
* - prefixed paths: "\\?\...", "\\.\..."
3643+
*/
3644+
3645+
/* convert to absolute path using GetFullPathNameW */
3646+
result = GetFullPathNameW(path, MAX_LONG_PATH, buf, NULL);
3647+
if (!result) {
3648+
errno = err_win_to_posix(GetLastError());
3649+
return -1;
3650+
}
3651+
3652+
/*
3653+
* return absolute path if it fits within max_path (even if
3654+
* "cwd + path" doesn't due to '..' components)
3655+
*/
3656+
if (result < max_path) {
3657+
wcscpy(path, buf);
3658+
return result;
3659+
}
3660+
3661+
/* error out if we shouldn't expand the path or buf is too small */
3662+
if (!expand || result >= MAX_LONG_PATH - 6) {
3663+
errno = ENAMETOOLONG;
3664+
return -1;
3665+
}
3666+
3667+
/* prefix full path with "\\?\" or "\\?\UNC\" */
3668+
if (buf[0] == '\\') {
3669+
/* ...unless already prefixed */
3670+
if (buf[1] == '\\' && (buf[2] == '?' || buf[2] == '.'))
3671+
return len;
3672+
3673+
wcscpy(path, L"\\\\?\\UNC\\");
3674+
wcscpy(path + 8, buf + 2);
3675+
return result + 6;
3676+
} else {
3677+
wcscpy(path, L"\\\\?\\");
3678+
wcscpy(path + 4, buf);
3679+
return result + 4;
3680+
}
3681+
}
3682+
35863683
#if !defined(_MSC_VER)
35873684
/*
35883685
* Disable MSVCRT command line wildcard expansion (__getmainargs called from
@@ -3745,6 +3842,9 @@ int wmain(int argc, const wchar_t **wargv)
37453842
/* initialize Unicode console */
37463843
winansi_init();
37473844

3845+
/* init length of current directory for handle_long_path */
3846+
current_directory_len = GetCurrentDirectoryW(0, NULL);
3847+
37483848
/* invoke the real main() using our utf8 version of argv. */
37493849
exit_status = main(argc, argv);
37503850

0 commit comments

Comments
 (0)