Skip to content

Commit

Permalink
mingw: lstat: compute correct size for symlinks
Browse files Browse the repository at this point in the history
This commit fixes mingw_lstat by computing the proper size for symlinks
according to POSIX. POSIX specifies that upon successful return from
lstat: "the value of the st_size member shall be set to the length of
the pathname contained in the symbolic link not including any
terminating null byte".

Prior to this commit the mingw_lstat function returned a fixed size of
4096. This caused problems in git repositories that were accessed by
git for Cygwin or git for WSL. For example, doing `git reset --hard`
using git for Windows would update the size of symlinks in the index
to be 4096; at a later time git for Cygwin or git for WSL would find
that symlinks have changed size during `git status`. Vice versa doing
`git reset --hard` in git for Cygwin or git for WSL would update the
size of symlinks in the index with the correct value, only for git for
Windows to find incorrectly at a later time that the size had changed.

Signed-off-by: Bill Zissimopoulos <billziss@navimatics.com>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
  • Loading branch information
billziss-gh authored and dscho committed Sep 24, 2024
1 parent 0137e5b commit 7cbbc7e
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 21 deletions.
65 changes: 44 additions & 21 deletions compat/mingw.c
Original file line number Diff line number Diff line change
Expand Up @@ -893,10 +893,14 @@ static int has_valid_directory_prefix(wchar_t *wfilename)
return 1;
}

static int readlink_1(const WCHAR *wpath, BOOL fail_on_unknown_tag,
char *tmpbuf, int *plen, DWORD *ptag);

int mingw_lstat(const char *file_name, struct stat *buf)
{
WIN32_FILE_ATTRIBUTE_DATA fdata;
WIN32_FIND_DATAW findbuf = { 0 };
DWORD reparse_tag = 0;
int link_len = 0;
wchar_t wfilename[MAX_LONG_PATH];
int wlen = xutftowcs_long_path(wfilename, file_name);
if (wlen < 0)
Expand All @@ -911,28 +915,29 @@ int mingw_lstat(const char *file_name, struct stat *buf)
}

if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
/* for reparse points, use FindFirstFile to get the reparse tag */
/* for reparse points, get the link tag and length */
if (fdata.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
HANDLE handle = FindFirstFileW(wfilename, &findbuf);
if (handle == INVALID_HANDLE_VALUE)
goto error;
FindClose(handle);
char tmpbuf[MAX_LONG_PATH];

if (readlink_1(wfilename, FALSE, tmpbuf, &link_len,
&reparse_tag) < 0)
return -1;
}
buf->st_ino = 0;
buf->st_gid = 0;
buf->st_uid = 0;
buf->st_nlink = 1;
buf->st_mode = file_attr_to_st_mode(fdata.dwFileAttributes,
findbuf.dwReserved0);
buf->st_size = S_ISLNK(buf->st_mode) ? MAX_LONG_PATH :
reparse_tag);
buf->st_size = S_ISLNK(buf->st_mode) ? link_len :
fdata.nFileSizeLow | (((off_t) fdata.nFileSizeHigh) << 32);
buf->st_dev = buf->st_rdev = 0; /* not used by Git */
filetime_to_timespec(&(fdata.ftLastAccessTime), &(buf->st_atim));
filetime_to_timespec(&(fdata.ftLastWriteTime), &(buf->st_mtim));
filetime_to_timespec(&(fdata.ftCreationTime), &(buf->st_ctim));
return 0;
}
error:

switch (GetLastError()) {
case ERROR_ACCESS_DENIED:
case ERROR_SHARING_VIOLATION:
Expand Down Expand Up @@ -2789,17 +2794,13 @@ typedef struct _REPARSE_DATA_BUFFER {
} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
#endif

int readlink(const char *path, char *buf, size_t bufsiz)
static int readlink_1(const WCHAR *wpath, BOOL fail_on_unknown_tag,
char *tmpbuf, int *plen, DWORD *ptag)
{
HANDLE handle;
WCHAR wpath[MAX_LONG_PATH], *wbuf;
WCHAR *wbuf;
REPARSE_DATA_BUFFER *b = alloca(MAXIMUM_REPARSE_DATA_BUFFER_SIZE);
DWORD dummy;
char tmpbuf[MAX_LONG_PATH];
int len;

if (xutftowcs_long_path(wpath, path) < 0)
return -1;

/* read reparse point data */
handle = CreateFileW(wpath, 0,
Expand All @@ -2819,7 +2820,7 @@ int readlink(const char *path, char *buf, size_t bufsiz)
CloseHandle(handle);

/* get target path for symlinks or mount points (aka 'junctions') */
switch (b->ReparseTag) {
switch ((*ptag = b->ReparseTag)) {
case IO_REPARSE_TAG_SYMLINK:
wbuf = (WCHAR*) (((char*) b->SymbolicLinkReparseBuffer.PathBuffer)
+ b->SymbolicLinkReparseBuffer.SubstituteNameOffset);
Expand All @@ -2833,19 +2834,41 @@ int readlink(const char *path, char *buf, size_t bufsiz)
+ b->MountPointReparseBuffer.SubstituteNameLength) = 0;
break;
default:
errno = EINVAL;
return -1;
if (fail_on_unknown_tag) {
errno = EINVAL;
return -1;
} else {
*plen = MAX_LONG_PATH;
return 0;
}
}

if ((*plen =
xwcstoutf(tmpbuf, normalize_ntpath(wbuf), MAX_LONG_PATH)) < 0)
return -1;
return 0;
}

int readlink(const char *path, char *buf, size_t bufsiz)
{
WCHAR wpath[MAX_LONG_PATH];
char tmpbuf[MAX_LONG_PATH];
int len;
DWORD tag;

if (xutftowcs_long_path(wpath, path) < 0)
return -1;

if (readlink_1(wpath, TRUE, tmpbuf, &len, &tag) < 0)
return -1;

/*
* Adapt to strange readlink() API: Copy up to bufsiz *bytes*, potentially
* cutting off a UTF-8 sequence. Insufficient bufsize is *not* a failure
* condition. There is no conversion function that produces invalid UTF-8,
* so convert to a (hopefully large enough) temporary buffer, then memcpy
* the requested number of bytes (including '\0' for robustness).
*/
if ((len = xwcstoutf(tmpbuf, normalize_ntpath(wbuf), MAX_LONG_PATH)) < 0)
return -1;
memcpy(buf, tmpbuf, min(bufsiz, len + 1));
return min(bufsiz, len);
}
Expand Down
12 changes: 12 additions & 0 deletions compat/win32/fscache.c
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,18 @@ int fscache_lstat(const char *filename, struct stat *st)
return -1;
}

/*
* Special case symbolic links: FindFirstFile()/FindNextFile() did not
* provide us with the length of the target path.
*/
if (fse->u.s.st_size == MAX_LONG_PATH && S_ISLNK(fse->st_mode)) {
char buf[MAX_LONG_PATH];
int len = readlink(filename, buf, sizeof(buf) - 1);

if (len > 0)
fse->u.s.st_size = len;
}

/* copy stat data */
st->st_ino = 0;
st->st_gid = 0;
Expand Down

0 comments on commit 7cbbc7e

Please sign in to comment.