Win32: add lstat(), fetch st_dev and st_ino and fetch st_nlink for fstat

We need lstat() for various modules to work well with symlinks,
and the same modules often want to check for matches on the device
and inode number.

The values we're using for st_ino match those that the Python and Rust
libraries use, and Go uses the same volume and file index values for
testing if two stat objects refer to the same file.

They aren't entirely unique, given ReFS uses 128-bit file ids, but
the API used to check for this (GetFileInformationByHandleEx() for
FileIdInfo) is only available on server operating systems, so I can't
directly test it anyway.
This commit is contained in:
Tony Cook 2020-10-06 17:07:00 +11:00
parent c1ec4bdd80
commit 92b3a3ebc0
12 changed files with 1067 additions and 426 deletions

View File

@ -6164,6 +6164,7 @@ t/win32/fs.t Test Win32 link for compatibility
t/win32/popen.t Test for stdout races in backticks, etc
t/win32/runenv.t Test if Win* perl honors its env variables
t/win32/signal.t Test Win32 signal emulation
t/win32/stat.t Test Win32 stat emulation
t/win32/system.t See if system works in Win*
t/win32/system_tests Test runner for system.t
taint.c Tainting code

View File

@ -70,7 +70,11 @@
* to include <sys/stat.h> and <sys/types.h> to get any typedef'ed
* information.
*/
#define Stat_t struct _stati64
#if defined(WIN32)
# define Stat_t struct w32_stat
#else
# define Stat_t struct _stati64
#endif
/* USE_STAT_RDEV:
* This symbol is defined if this system has a stat structure declaring

View File

@ -502,14 +502,19 @@ like $@, qr/^The stat preceding lstat\(\) wasn't an lstat at /,
}
SKIP: {
skip "No lstat", 2 unless $Config{d_lstat};
skip "No lstat", 2 unless $Config{d_lstat} && $Config{d_symlink};
# bug id 20020124.004 (#8334)
# If we have d_lstat, we should have symlink()
my $linkname = 'stat-' . rand =~ y/.//dr;
my $target = $Perl;
$target =~ s/;\d+\z// if $Is_VMS; # symlinks don't like version numbers
symlink $target, $linkname or die "# Can't symlink $0: $!";
unless (symlink $target, $linkname) {
if ($^O eq "MSWin32") {
# likely we don't have permission
skip "symlink failed: $!", 2;
}
die "# Can't symlink $0: $!";
}
lstat $linkname;
-T _;
eval { lstat _ };

111
t/win32/stat.t Normal file
View File

@ -0,0 +1,111 @@
#!./perl
BEGIN {
chdir 't' if -d 't';
@INC = '../lib';
require "./test.pl";
}
use strict;
Win32::FsType() eq 'NTFS'
or skip_all("need NTFS");
my $tmpfile1 = tempfile();
# test some of the win32 specific stat code, since we
# don't depend on the CRT for some of it
ok(link($0, $tmpfile1), "make a link to test nlink");
my @st = stat $0;
open my $fh, "<", $0 or die;
my @fst = stat $fh;
close $fh;
# the ucrt stat() is inconsistent here, using an A=0 drive letter for stat()
# and the fd for fstat(), I assume that's something backward compatible.
#
# I don't see anything we could reasonable populate it with either.
$st[6] = $fst[6] = 0;
is("@st", "@fst", "check named stat vs handle stat");
ok($st[0], "we set dev by default now");
ok($st[1], "and ino");
# unlikely, but someone else might have linked to win32/stat.t
cmp_ok($st[3], '>', 1, "should be more than one link");
my $nlink = $st[3];
# check we get nlinks etc for a directory
@st = stat("win32");
ok($st[0], "got dev for a directory");
ok($st[1], "got ino for a directory");
ok($st[3], "got nlink for a directory");
${^WIN32_SLOPPY_STAT} = 1;
@st = stat $0;
open my $fh, "<", $0 or die;
@fst = stat $fh;
close $fh;
$st[6] = $fst[6] = 0;
is("@st", "@fst", "sloppy check named stat vs handle stat");
is($st[0], 0, "sloppy no dev");
is($st[1], 0, "sloppy no ino");
# don't check nlink, Microsoft might fix it one day
${^WIN32_SLOPPY_STAT} = 0;
# symbolic links
unlink($tmpfile1); # no more hard link
# mklink is available from Vista onwards
# this may only work in an admin shell
# MKLINK [[/D] | [/H] | [/J]] Link Target
if (system("mklink $tmpfile1 win32\\stat.t") == 0) {
ok(-l $tmpfile1, "lstat sees a symlink");
# check stat on file vs symlink
@st = stat $0;
my @lst = stat $tmpfile1;
$st[6] = $lst[6] = 0;
is("@st", "@lst", "check stat on file vs link");
# our hard link no longer exists, check that is reflected in nlink
is($st[3], $nlink-1, "check nlink updated");
unlink($tmpfile1);
}
# similarly for a directory
if (system("mklink /d $tmpfile1 win32") == 0) {
ok(-l $tmpfile1, "lstat sees a symlink on the directory symlink");
# check stat on directory vs symlink
@st = stat "win32";
my @lst = stat $tmpfile1;
$st[6] = $lst[6] = 0;
is("@st", "@lst", "check stat on dir vs link");
# for now at least, we need to rmdir symlinks to directories
rmdir( $tmpfile1 );
}
# check a junction doesn't look like a symlink
if (system("mklink /j $tmpfile1 win32") == 0) {
ok(!-l $tmpfile1, "lstat doesn't see a symlink on the directory junction");
rmdir( $tmpfile1 );
}
done_testing();

View File

@ -358,7 +358,7 @@ d_lrintl='define'
d_lround='define'
d_lroundl='define'
d_lseekproto='define'
d_lstat='undef'
d_lstat='define'
d_madvise='undef'
d_malloc_good_size='undef'
d_malloc_size='undef'

View File

@ -358,7 +358,7 @@ d_lrintl='undef'
d_lround='undef'
d_lroundl='undef'
d_lseekproto='define'
d_lstat='undef'
d_lstat='define'
d_madvise='undef'
d_malloc_good_size='undef'
d_malloc_size='undef'

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -995,7 +995,7 @@ PerlLIOLseek(struct IPerlLIO* piPerl, int handle, Off_t offset, int origin)
int
PerlLIOLstat(struct IPerlLIO* piPerl, const char *path, Stat_t *buffer)
{
return win32_stat(path, buffer);
return win32_lstat(path, buffer);
}
char*

View File

@ -39,6 +39,7 @@
#include <tlhelp32.h>
#include <io.h>
#include <signal.h>
#include <winioctl.h>
/* #include "config.h" */
@ -1462,7 +1463,10 @@ win32_stat(const char *path, Stat_t *sbuf)
dTHX;
int res;
int nlink = 1;
unsigned __int64 ino = 0;
DWORD vol = 0;
BOOL expect_dir = FALSE;
struct _stati64 st;
if (l > 1) {
switch(path[l - 1]) {
@ -1508,11 +1512,16 @@ win32_stat(const char *path, Stat_t *sbuf)
/* We must open & close the file once; otherwise file attribute changes */
/* might not yet have propagated to "other" hard links of the same file. */
/* This also gives us an opportunity to determine the number of links. */
HANDLE handle = CreateFileA(path, 0, 0, NULL, OPEN_EXISTING, 0, NULL);
HANDLE handle = CreateFileA(path, 0, 0, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (handle != INVALID_HANDLE_VALUE) {
BY_HANDLE_FILE_INFORMATION bhi;
if (GetFileInformationByHandle(handle, &bhi))
if (GetFileInformationByHandle(handle, &bhi)) {
nlink = bhi.nNumberOfLinks;
ino = bhi.nFileIndexHigh;
ino <<= 32;
ino |= bhi.nFileIndexLow;
vol = bhi.dwVolumeSerialNumber;
}
CloseHandle(handle);
}
else {
@ -1527,7 +1536,17 @@ win32_stat(const char *path, Stat_t *sbuf)
/* path will be mapped correctly above */
res = _stati64(path, sbuf);
sbuf->st_dev = vol;
sbuf->st_ino = ino;
sbuf->st_mode = st.st_mode;
sbuf->st_nlink = nlink;
sbuf->st_uid = st.st_uid;
sbuf->st_gid = st.st_gid;
sbuf->st_rdev = st.st_rdev;
sbuf->st_size = st.st_size;
sbuf->st_atime = st.st_atime;
sbuf->st_mtime = st.st_mtime;
sbuf->st_ctime = st.st_ctime;
if (res < 0) {
/* CRT is buggy on sharenames, so make sure it really isn't.
@ -1575,6 +1594,147 @@ win32_stat(const char *path, Stat_t *sbuf)
return res;
}
static void
translate_to_errno(void)
{
/* This isn't perfect, eg. Win32 returns ERROR_ACCESS_DENIED for
both permissions errors and if the source is a directory, while
POSIX wants EACCES and EPERM respectively.
Determined by experimentation on Windows 7 x64 SP1, since MS
don't document what error codes are returned.
*/
switch (GetLastError()) {
case ERROR_BAD_NET_NAME:
case ERROR_BAD_NETPATH:
case ERROR_BAD_PATHNAME:
case ERROR_FILE_NOT_FOUND:
case ERROR_FILENAME_EXCED_RANGE:
case ERROR_INVALID_DRIVE:
case ERROR_PATH_NOT_FOUND:
errno = ENOENT;
break;
case ERROR_ALREADY_EXISTS:
errno = EEXIST;
break;
case ERROR_ACCESS_DENIED:
case ERROR_PRIVILEGE_NOT_HELD:
errno = EACCES;
break;
case ERROR_NOT_SAME_DEVICE:
errno = EXDEV;
break;
case ERROR_DISK_FULL:
errno = ENOSPC;
break;
case ERROR_NOT_ENOUGH_QUOTA:
errno = EDQUOT;
break;
default:
/* ERROR_INVALID_FUNCTION - eg. symlink on a FAT volume */
errno = EINVAL;
break;
}
}
/* Adapted from:
https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_reparse_data_buffer
Renamed to avoid conflicts, apparently some SDKs define this
structure.
Hoisted the symlink data into a new type to allow us to make a pointer
to it, and to avoid C++ scoping issues.
*/
typedef struct {
USHORT SubstituteNameOffset;
USHORT SubstituteNameLength;
USHORT PrintNameOffset;
USHORT PrintNameLength;
ULONG Flags;
WCHAR PathBuffer[MAX_PATH*3];
} MY_SYMLINK_REPARSE_BUFFER, *PMY_SYMLINK_REPARSE_BUFFER;
typedef struct {
ULONG ReparseTag;
USHORT ReparseDataLength;
USHORT Reserved;
union {
MY_SYMLINK_REPARSE_BUFFER SymbolicLinkReparseBuffer;
struct {
USHORT SubstituteNameOffset;
USHORT SubstituteNameLength;
USHORT PrintNameOffset;
USHORT PrintNameLength;
WCHAR PathBuffer[1];
} MountPointReparseBuffer;
struct {
UCHAR DataBuffer[1];
} GenericReparseBuffer;
} Data;
} MY_REPARSE_DATA_BUFFER, *PMY_REPARSE_DATA_BUFFER;
static BOOL
is_symlink(HANDLE h) {
MY_REPARSE_DATA_BUFFER linkdata;
const MY_SYMLINK_REPARSE_BUFFER * const sd =
&linkdata.Data.SymbolicLinkReparseBuffer;
DWORD linkdata_returned;
if (!DeviceIoControl(h, FSCTL_GET_REPARSE_POINT, NULL, 0, &linkdata, sizeof(linkdata), &linkdata_returned, NULL)) {
return FALSE;
}
if (linkdata_returned < offsetof(MY_REPARSE_DATA_BUFFER, Data.SymbolicLinkReparseBuffer.PathBuffer)
|| linkdata.ReparseTag != IO_REPARSE_TAG_SYMLINK) {
/* some other type of reparse point */
return FALSE;
}
return TRUE;
}
DllExport int
win32_lstat(const char *path, Stat_t *sbuf)
{
HANDLE f;
int fd;
int result;
DWORD attr = GetFileAttributes(path); /* doesn't follow symlinks */
if (attr == INVALID_FILE_ATTRIBUTES) {
translate_to_errno();
return -1;
}
if (!(attr & FILE_ATTRIBUTE_REPARSE_POINT)) {
return win32_stat(path, sbuf);
}
f = CreateFileA(path, GENERIC_READ, 0, NULL, OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT|FILE_FLAG_BACKUP_SEMANTICS, 0);
if (f == INVALID_HANDLE_VALUE) {
translate_to_errno();
return -1;
}
if (!is_symlink(f)) {
CloseHandle(f);
return win32_stat(path, sbuf);
}
fd = win32_open_osfhandle((intptr_t)f, 0);
result = win32_fstat(fd, sbuf);
if (result != -1){
sbuf->st_mode = (sbuf->st_mode & ~_S_IFMT) | _S_IFLNK;
}
close(fd);
return result;
}
#define isSLASH(c) ((c) == '/' || (c) == '\\')
#define SKIP_SLASHES(s) \
STMT_START { \
@ -1668,7 +1828,6 @@ win32_longpath(char *path)
}
else {
/* failed a step, just return without side effects */
/*PerlIO_printf(Perl_debug_log, "Failed to find %s\n", path);*/
errno = EINVAL;
return NULL;
}
@ -2955,7 +3114,39 @@ win32_abort(void)
DllExport int
win32_fstat(int fd, Stat_t *sbufptr)
{
return _fstati64(fd, sbufptr);
int result;
struct _stati64 st;
dTHX;
result = _fstati64(fd, &st);
if (result == 0) {
sbufptr->st_mode = st.st_mode;
sbufptr->st_uid = st.st_uid;
sbufptr->st_gid = st.st_gid;
sbufptr->st_rdev = st.st_rdev;
sbufptr->st_size = st.st_size;
sbufptr->st_atime = st.st_atime;
sbufptr->st_mtime = st.st_mtime;
sbufptr->st_ctime = st.st_ctime;
if (w32_sloppystat) {
sbufptr->st_nlink = st.st_nlink;
sbufptr->st_dev = st.st_dev;
sbufptr->st_ino = st.st_ino;
}
else {
HANDLE handle = (HANDLE)win32_get_osfhandle(fd);
BY_HANDLE_FILE_INFORMATION bhi;
if (GetFileInformationByHandle(handle, &bhi)) {
sbufptr->st_nlink = bhi.nNumberOfLinks;
sbufptr->st_ino = bhi.nFileIndexHigh;
sbufptr->st_ino <<= 32;
sbufptr->st_ino |= bhi.nFileIndexLow;
sbufptr->st_dev = bhi.dwVolumeSerialNumber;
}
}
}
return result;
}
DllExport int

View File

@ -731,5 +731,37 @@ DllExport void *win32_signal_context(void);
# define O_ACCMODE (O_RDWR | O_WRONLY | O_RDONLY)
#endif
/* ucrt at least seems to allocate a whole bit per type,
just mask off one bit from the mask for our symlink
file type.
*/
#define _S_IFLNK ((unsigned)(_S_IFMT ^ (_S_IFMT & -_S_IFMT)))
#undef S_ISLNK
#define S_ISLNK(mode) (((mode) & _S_IFMT) == _S_IFLNK)
/*
The default CRT struct stat uses unsigned short for st_dev and st_ino
which obviously isn't enough, so we define our own structure.
*/
typedef DWORD Dev_t;
typedef unsigned __int64 Ino_t;
struct w32_stat {
Dev_t st_dev;
Ino_t st_ino;
unsigned short st_mode;
DWORD st_nlink;
short st_uid;
short st_gid;
Dev_t st_rdev;
Off_t st_size;
time_t st_atime;
time_t st_mtime;
time_t st_ctime;
};
#endif /* _INC_WIN32_PERL5 */

View File

@ -69,6 +69,7 @@ DllExport FILE* win32_tmpfile(void);
DllExport void win32_abort(void);
DllExport int win32_fstat(int fd,Stat_t *sbufptr);
DllExport int win32_stat(const char *name,Stat_t *sbufptr);
DllExport int win32_lstat(const char *name,Stat_t *sbufptr);
DllExport int win32_pipe( int *phandles, unsigned int psize, int textmode );
DllExport PerlIO* win32_popen( const char *command, const char *mode );
DllExport PerlIO* win32_popenlist(const char *mode, IV narg, SV **args);
@ -241,6 +242,7 @@ END_EXTERN_C
# undef stat
#endif
#define stat(pth,bufptr) win32_stat(pth,bufptr)
#define lstat(pth,bufptr) win32_lstat(pth,bufptr)
#define longpath(pth) win32_longpath(pth)
#define ansipath(pth) win32_ansipath(pth)
#define rename(old,new) win32_rename(old,new)