Skip to content

Commit

Permalink
mingw: support long paths
Browse files Browse the repository at this point in the history
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 <maki@bitkings.de>
Thanks-to: Doug Kelly <dougk.ff7@gmail.com>
Original-test-by: Andrey Rogozhnikov <rogozhnikov.andrey@gmail.com>
Signed-off-by: Karsten Blees <blees@dcon.de>
Signed-off-by: Stepan Kasal <kasal@ucw.cz>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
  • Loading branch information
kblees authored and dscho committed Sep 18, 2024
1 parent ed4cdb7 commit 90a0bd5
Show file tree
Hide file tree
Showing 7 changed files with 346 additions and 64 deletions.
7 changes: 7 additions & 0 deletions Documentation/config/core.txt
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,13 @@ core.fscache::
Git for Windows uses this to bulk-read and cache lstat data of entire
directories (instead of doing lstat file by file).

core.longpaths::
Enable long path (> 260) support for builtin commands in Git for
Windows. This is disabled by default, as long paths are not supported
by Windows Explorer, cmd.exe and the Git for Windows tool chain
(msys, bash, tcl, perl...). Only enable this if you know what you're
doing and are prepared to live with a few quirks.

core.unsetenvvars::
Windows-only: comma-separated list of environment variables'
names that need to be unset before spawning any other process.
Expand Down
169 changes: 135 additions & 34 deletions compat/mingw.c
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,27 @@ static enum hide_dotfiles_type hide_dotfiles = HIDE_DOTFILES_DOTGITONLY;
static char *unset_environment_variables;
int core_fscache;

int are_long_paths_enabled(void)
{
/* default to `false` during initialization */
static const int fallback = 0;

static int enabled = -1;

if (enabled < 0) {
/* avoid infinite recursion */
if (!the_repository)
return fallback;

if (the_repository->config &&
the_repository->config->hash_initialized &&
git_config_get_bool("core.longpaths", &enabled) < 0)
enabled = 0;
}

return enabled < 0 ? fallback : enabled;
}

int mingw_core_config(const char *var, const char *value,
const struct config_context *ctx, void *cb)
{
Expand Down Expand Up @@ -311,8 +332,8 @@ static wchar_t *normalize_ntpath(wchar_t *wbuf)
int mingw_unlink(const char *pathname)
{
int ret, tries = 0;
wchar_t wpathname[MAX_PATH];
if (xutftowcs_path(wpathname, pathname) < 0)
wchar_t wpathname[MAX_LONG_PATH];
if (xutftowcs_long_path(wpathname, pathname) < 0)
return -1;

if (DeleteFileW(wpathname))
Expand Down Expand Up @@ -344,7 +365,7 @@ static int is_dir_empty(const wchar_t *wpath)
{
WIN32_FIND_DATAW findbuf;
HANDLE handle;
wchar_t wbuf[MAX_PATH + 2];
wchar_t wbuf[MAX_LONG_PATH + 2];
wcscpy(wbuf, wpath);
wcscat(wbuf, L"\\*");
handle = FindFirstFileW(wbuf, &findbuf);
Expand All @@ -365,7 +386,7 @@ static int is_dir_empty(const wchar_t *wpath)
int mingw_rmdir(const char *pathname)
{
int ret, tries = 0;
wchar_t wpathname[MAX_PATH];
wchar_t wpathname[MAX_LONG_PATH];
struct stat st;

/*
Expand All @@ -387,7 +408,7 @@ int mingw_rmdir(const char *pathname)
return -1;
}

if (xutftowcs_path(wpathname, pathname) < 0)
if (xutftowcs_long_path(wpathname, pathname) < 0)
return -1;

while ((ret = _wrmdir(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {
Expand Down Expand Up @@ -466,15 +487,18 @@ static int set_hidden_flag(const wchar_t *path, int set)
int mingw_mkdir(const char *path, int mode)
{
int ret;
wchar_t wpath[MAX_PATH];
wchar_t wpath[MAX_LONG_PATH];

if (!is_valid_win32_path(path, 0)) {
errno = EINVAL;
return -1;
}

if (xutftowcs_path(wpath, path) < 0)
/* CreateDirectoryW path limit is 248 (MAX_PATH - 8.3 file name) */
if (xutftowcs_path_ex(wpath, path, MAX_LONG_PATH, -1, 248,
are_long_paths_enabled()) < 0)
return -1;

ret = _wmkdir(wpath);
if (!ret && needs_hiding(path))
return set_hidden_flag(wpath, 1);
Expand Down Expand Up @@ -561,7 +585,7 @@ int mingw_open (const char *filename, int oflags, ...)
va_list args;
unsigned mode;
int fd, create = (oflags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL);
wchar_t wfilename[MAX_PATH];
wchar_t wfilename[MAX_LONG_PATH];
open_fn_t open_fn;

va_start(args, oflags);
Expand Down Expand Up @@ -589,7 +613,7 @@ int mingw_open (const char *filename, int oflags, ...)

if (filename && !strcmp(filename, "/dev/null"))
wcscpy(wfilename, L"nul");
else if (xutftowcs_path(wfilename, filename) < 0)
else if (xutftowcs_long_path(wfilename, filename) < 0)
return -1;

fd = open_fn(wfilename, oflags, mode);
Expand Down Expand Up @@ -647,14 +671,14 @@ FILE *mingw_fopen (const char *filename, const char *otype)
{
int hide = needs_hiding(filename);
FILE *file;
wchar_t wfilename[MAX_PATH], wotype[4];
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
if (filename && !strcmp(filename, "/dev/null"))
wcscpy(wfilename, L"nul");
else if (!is_valid_win32_path(filename, 1)) {
int create = otype && strchr(otype, 'w');
errno = create ? EINVAL : ENOENT;
return NULL;
} else if (xutftowcs_path(wfilename, filename) < 0)
} else if (xutftowcs_long_path(wfilename, filename) < 0)
return NULL;

if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
Expand All @@ -676,14 +700,14 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
{
int hide = needs_hiding(filename);
FILE *file;
wchar_t wfilename[MAX_PATH], wotype[4];
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
if (filename && !strcmp(filename, "/dev/null"))
wcscpy(wfilename, L"nul");
else if (!is_valid_win32_path(filename, 1)) {
int create = otype && strchr(otype, 'w');
errno = create ? EINVAL : ENOENT;
return NULL;
} else if (xutftowcs_path(wfilename, filename) < 0)
} else if (xutftowcs_long_path(wfilename, filename) < 0)
return NULL;

if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
Expand Down Expand Up @@ -733,7 +757,7 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)
HANDLE h = (HANDLE) _get_osfhandle(fd);
if (GetFileType(h) != FILE_TYPE_PIPE) {
if (orig == EINVAL) {
wchar_t path[MAX_PATH];
wchar_t path[MAX_LONG_PATH];
DWORD ret = GetFinalPathNameByHandleW(h, path,
ARRAY_SIZE(path), 0);
UINT drive_type = ret > 0 && ret < ARRAY_SIZE(path) ?
Expand Down Expand Up @@ -770,27 +794,33 @@ ssize_t mingw_write(int fd, const void *buf, size_t len)

int mingw_access(const char *filename, int mode)
{
wchar_t wfilename[MAX_PATH];
wchar_t wfilename[MAX_LONG_PATH];
if (!strcmp("nul", filename) || !strcmp("/dev/null", filename))
return 0;
if (xutftowcs_path(wfilename, filename) < 0)
if (xutftowcs_long_path(wfilename, filename) < 0)
return -1;
/* X_OK is not supported by the MSVCRT version */
return _waccess(wfilename, mode & ~X_OK);
}

/* cached length of current directory for handle_long_path */
static int current_directory_len = 0;

int mingw_chdir(const char *dirname)
{
wchar_t wdirname[MAX_PATH];
if (xutftowcs_path(wdirname, dirname) < 0)
int result;
wchar_t wdirname[MAX_LONG_PATH];
if (xutftowcs_long_path(wdirname, dirname) < 0)
return -1;
return _wchdir(wdirname);
result = _wchdir(wdirname);
current_directory_len = GetCurrentDirectoryW(0, NULL);
return result;
}

int mingw_chmod(const char *filename, int mode)
{
wchar_t wfilename[MAX_PATH];
if (xutftowcs_path(wfilename, filename) < 0)
wchar_t wfilename[MAX_LONG_PATH];
if (xutftowcs_long_path(wfilename, filename) < 0)
return -1;
return _wchmod(wfilename, mode);
}
Expand Down Expand Up @@ -838,8 +868,8 @@ static int has_valid_directory_prefix(wchar_t *wfilename)
static int do_lstat(int follow, const char *file_name, struct stat *buf)
{
WIN32_FILE_ATTRIBUTE_DATA fdata;
wchar_t wfilename[MAX_PATH];
if (xutftowcs_path(wfilename, file_name) < 0)
wchar_t wfilename[MAX_LONG_PATH];
if (xutftowcs_long_path(wfilename, file_name) < 0)
return -1;

if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
Expand Down Expand Up @@ -1010,10 +1040,10 @@ int mingw_utime (const char *file_name, const struct utimbuf *times)
FILETIME mft, aft;
int rc;
DWORD attrs;
wchar_t wfilename[MAX_PATH];
wchar_t wfilename[MAX_LONG_PATH];
HANDLE osfilehandle;

if (xutftowcs_path(wfilename, file_name) < 0)
if (xutftowcs_long_path(wfilename, file_name) < 0)
return -1;

/* must have write permission */
Expand Down Expand Up @@ -1096,6 +1126,7 @@ char *mingw_mktemp(char *template)
wchar_t wtemplate[MAX_PATH];
int offset = 0;

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

Expand Down Expand Up @@ -1747,6 +1778,10 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen

if (*argv && !strcmp(cmd, *argv))
wcmd[0] = L'\0';
/*
* Paths to executables and to the current directory do not support
* long paths, therefore we cannot use xutftowcs_long_path() here.
*/
else if (xutftowcs_path(wcmd, cmd) < 0)
return -1;
if (dir && xutftowcs_path(wdir, dir) < 0)
Expand Down Expand Up @@ -2395,8 +2430,9 @@ int mingw_rename(const char *pold, const char *pnew)
{
DWORD attrs, gle;
int tries = 0;
wchar_t wpold[MAX_PATH], wpnew[MAX_PATH];
if (xutftowcs_path(wpold, pold) < 0 || xutftowcs_path(wpnew, pnew) < 0)
wchar_t wpold[MAX_LONG_PATH], wpnew[MAX_LONG_PATH];
if (xutftowcs_long_path(wpold, pold) < 0 ||
xutftowcs_long_path(wpnew, pnew) < 0)
return -1;

/*
Expand Down Expand Up @@ -2714,9 +2750,9 @@ int mingw_raise(int sig)

int link(const char *oldpath, const char *newpath)
{
wchar_t woldpath[MAX_PATH], wnewpath[MAX_PATH];
if (xutftowcs_path(woldpath, oldpath) < 0 ||
xutftowcs_path(wnewpath, newpath) < 0)
wchar_t woldpath[MAX_LONG_PATH], wnewpath[MAX_LONG_PATH];
if (xutftowcs_long_path(woldpath, oldpath) < 0 ||
xutftowcs_long_path(wnewpath, newpath) < 0)
return -1;

if (!CreateHardLinkW(wnewpath, woldpath, NULL)) {
Expand Down Expand Up @@ -2784,8 +2820,8 @@ int mingw_is_mount_point(struct strbuf *path)
{
WIN32_FIND_DATAW findbuf = { 0 };
HANDLE handle;
wchar_t wfilename[MAX_PATH];
int wlen = xutftowcs_path(wfilename, path->buf);
wchar_t wfilename[MAX_LONG_PATH];
int wlen = xutftowcs_long_path(wfilename, path->buf);
if (wlen < 0)
die(_("could not get long path for '%s'"), path->buf);

Expand Down Expand Up @@ -2930,9 +2966,9 @@ static size_t append_system_bin_dirs(char *path, size_t size)

static int is_system32_path(const char *path)
{
WCHAR system32[MAX_PATH], wpath[MAX_PATH];
WCHAR system32[MAX_LONG_PATH], wpath[MAX_LONG_PATH];

if (xutftowcs_path(wpath, path) < 0 ||
if (xutftowcs_long_path(wpath, path) < 0 ||
!GetSystemDirectoryW(system32, ARRAY_SIZE(system32)) ||
_wcsicmp(system32, wpath))
return 0;
Expand Down Expand Up @@ -3344,6 +3380,68 @@ int is_valid_win32_path(const char *path, int allow_literal_nul)
}
}

int handle_long_path(wchar_t *path, int len, int max_path, int expand)
{
int result;
wchar_t buf[MAX_LONG_PATH];

/*
* we don't need special handling if path is relative to the current
* directory, and current directory + path don't exceed the desired
* max_path limit. This should cover > 99 % of cases with minimal
* performance impact (git almost always uses relative paths).
*/
if ((len < 2 || (!is_dir_sep(path[0]) && path[1] != ':')) &&
(current_directory_len + len < max_path))
return len;

/*
* handle everything else:
* - absolute paths: "C:\dir\file"
* - absolute UNC paths: "\\server\share\dir\file"
* - absolute paths on current drive: "\dir\file"
* - relative paths on other drive: "X:file"
* - prefixed paths: "\\?\...", "\\.\..."
*/

/* convert to absolute path using GetFullPathNameW */
result = GetFullPathNameW(path, MAX_LONG_PATH, buf, NULL);
if (!result) {
errno = err_win_to_posix(GetLastError());
return -1;
}

/*
* return absolute path if it fits within max_path (even if
* "cwd + path" doesn't due to '..' components)
*/
if (result < max_path) {
wcscpy(path, buf);
return result;
}

/* error out if we shouldn't expand the path or buf is too small */
if (!expand || result >= MAX_LONG_PATH - 6) {
errno = ENAMETOOLONG;
return -1;
}

/* prefix full path with "\\?\" or "\\?\UNC\" */
if (buf[0] == '\\') {
/* ...unless already prefixed */
if (buf[1] == '\\' && (buf[2] == '?' || buf[2] == '.'))
return len;

wcscpy(path, L"\\\\?\\UNC\\");
wcscpy(path + 8, buf + 2);
return result + 6;
} else {
wcscpy(path, L"\\\\?\\");
wcscpy(path + 4, buf);
return result + 4;
}
}

#if !defined(_MSC_VER)
/*
* Disable MSVCRT command line wildcard expansion (__getmainargs called from
Expand Down Expand Up @@ -3505,6 +3603,9 @@ int wmain(int argc, const wchar_t **wargv)
/* initialize Unicode console */
winansi_init();

/* init length of current directory for handle_long_path */
current_directory_len = GetCurrentDirectoryW(0, NULL);

/* invoke the real main() using our utf8 version of argv. */
exit_status = main(argc, argv);

Expand Down
Loading

0 comments on commit 90a0bd5

Please sign in to comment.