Win32: implement symlink() and readlink()

The API used requires Windows Vista or later.

The API itself requires either elevated privileges or a sufficiently
recent version of Windows 10 running in "Developer Mode", so some
tests require updates.
This commit is contained in:
Tony Cook 2020-10-07 16:31:22 +11:00
parent 92b3a3ebc0
commit 680b2c5ee3
14 changed files with 294 additions and 56 deletions

View File

@ -6165,6 +6165,7 @@ 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/symlink.t Test Win32 symlink
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

@ -654,6 +654,10 @@ typedef int (*LPLIOUnlink)(struct IPerlLIO*, const char*);
typedef int (*LPLIOUtime)(struct IPerlLIO*, const char*, struct utimbuf*);
typedef int (*LPLIOWrite)(struct IPerlLIO*, int, const void*,
unsigned int);
typedef int (*LPLIOSymLink)(struct IPerlLIO*, const char*,
const char *);
typedef int (*LPLIOReadLink)(struct IPerlLIO*, const char*,
char *, size_t);
struct IPerlLIO
{
@ -683,6 +687,8 @@ struct IPerlLIO
LPLIOUnlink pUnlink;
LPLIOUtime pUtime;
LPLIOWrite pWrite;
LPLIOSymLink pSymLink;
LPLIOReadLink pReadLink;
};
struct IPerlLIOInfo
@ -715,6 +721,10 @@ struct IPerlLIOInfo
(*PL_LIO->pIsatty)(PL_LIO, (fd))
#define PerlLIO_link(oldname, newname) \
(*PL_LIO->pLink)(PL_LIO, (oldname), (newname))
#define PerlLIO_symlink(oldname, newname) \
(*PL_LIO->pSymLink)(PL_LIO, (oldname), (newname))
#define PerlLIO_readlink(path, buf, bufsiz) \
(*PL_LIO->pReadLink)(PL_LIO, (path), (buf), (bufsiz))
#define PerlLIO_lseek(fd, offset, mode) \
(*PL_LIO->pLseek)(PL_LIO, (fd), (offset), (mode))
#define PerlLIO_lstat(name, buf) \
@ -764,6 +774,8 @@ struct IPerlLIOInfo
#define PerlLIO_ioctl(fd, u, buf) ioctl((fd), (u), (buf))
#define PerlLIO_isatty(fd) isatty((fd))
#define PerlLIO_link(oldname, newname) link((oldname), (newname))
#define PerlLIO_symlink(oldname, newname) symlink((oldname), (newname))
#define PerlLIO_readlink(path, buf, bufsiz) readlink((path), (buf), (bufsiz))
#define PerlLIO_lseek(fd, offset, mode) lseek((fd), (offset), (mode))
#define PerlLIO_stat(name, buf) Stat((name), (buf))
#ifdef HAS_LSTAT

View File

@ -1571,6 +1571,12 @@ filehandle may be closed, or pointer may be in a different position.
The value returned by L<C<tell>|perlfunc/tell FILEHANDLE> may be affected
after the call, and the filehandle may be flushed.
=item chdir
(Win32)
The current directory reported by the system may include any symbolic
links specified to chdir().
=item chmod
(Win32)
@ -2100,9 +2106,17 @@ true value speeds up C<stat> by not performing this operation.
=item symlink
(Win32, S<RISC OS>)
(S<RISC OS>)
Not implemented.
(Win32)
Requires either elevated permissions or developer mode and a
sufficiently recent version of Windows 10. Since Windows needs to
know whether the target is a directory or not when creating the link
the target Perl will only create the link as a directory link when the
target exists and is a directory.
(VMS)
Implemented on 64 bit VMS 8.3. VMS requires the symbolic link to be in Unix
syntax if it is intended to resolve to a valid path.

View File

@ -3774,13 +3774,13 @@ PP(pp_link)
# if defined(HAS_LINK) && defined(HAS_SYMLINK)
/* Both present - need to choose which. */
(op_type == OP_LINK) ?
PerlLIO_link(tmps, tmps2) : symlink(tmps, tmps2);
PerlLIO_link(tmps, tmps2) : PerlLIO_symlink(tmps, tmps2);
# elif defined(HAS_LINK)
/* Only have link, so calls to pp_symlink will have DIE()d above. */
PerlLIO_link(tmps, tmps2);
# elif defined(HAS_SYMLINK)
/* Only have symlink, so calls to pp_link will have DIE()d above. */
symlink(tmps, tmps2);
PerlLIO_symlink(tmps, tmps2);
# endif
}
@ -3811,7 +3811,7 @@ PP(pp_readlink)
tmps = POPpconstx;
/* NOTE: if the length returned by readlink() is sizeof(buf) - 1,
* it is impossible to know whether the result was truncated. */
len = readlink(tmps, buf, sizeof(buf) - 1);
len = PerlLIO_readlink(tmps, buf, sizeof(buf) - 1);
if (len < 0)
RETPUSHUNDEF;
buf[len] = '\0';

View File

@ -27,6 +27,8 @@ if ($^O eq 'MSWin32') {
${^WIN32_SLOPPY_STAT} = 0;
}
my $Errno_loaded = eval { require Errno };
plan tests => 110;
my $Perl = which_perl();
@ -241,7 +243,10 @@ ok(! -f '.', '!-f cwd' );
SKIP: {
unlink($tmpfile_link);
my $symlink_rslt = eval { symlink $tmpfile, $tmpfile_link };
my $error = 0 + $!;
skip "symlink not implemented", 3 if $@ =~ /unimplemented/;
skip "symlink not available or we can't check", 3
if $^O eq "MSWin32" && (!$Errno_loaded || $error == &Errno::ENOSYS || $error == &Errno::EPERM);
is( $@, '', 'symlink() implemented' );
ok( $symlink_rslt, 'symlink() ok' );
@ -634,7 +639,6 @@ SKIP:
{
skip "There is a file named '2', which invalidates this test", 2 if -e '2';
my $Errno_loaded = eval { require Errno };
my @statarg = ($statfile, $statfile);
no warnings 'syntax';
ok !stat(@statarg),

77
t/win32/symlink.t Normal file
View File

@ -0,0 +1,77 @@
#!./perl
BEGIN {
chdir 't' if -d 't';
@INC = '../lib';
require "./test.pl";
}
use Errno;
Win32::FsType() eq 'NTFS'
or skip_all("need NTFS");
plan skip_all => "no symlink available in this Windows"
if !symlink('', '') && $! == &Errno::ENOSYS;
my $tmpfile1 = tempfile();
my $tmpfile2 = tempfile();
my $ok = symlink($tmpfile1, $tmpfile2);
plan skip_all => "no access to symlink as this user"
if !$ok && $! == &Errno::EPERM;
ok($ok, "create a dangling symbolic link");
ok(-l $tmpfile2, "-l sees it as a symlink");
ok(unlink($tmpfile2), "and remove it");
ok(mkdir($tmpfile1), "make a directory");
ok(!-l $tmpfile1, "doesn't look like a symlink");
ok(symlink($tmpfile1, $tmpfile2), "and symlink to it");
ok(-l $tmpfile2, "which does look like a symlink");
ok(!-d _, "-d on the lstat result is false");
ok(-d $tmpfile2, "normal -d sees it as a directory");
is(readlink($tmpfile2), $tmpfile1, "readlink works");
check_stat($tmpfile1, $tmpfile2, "check directory and link stat are the same");
ok(unlink($tmpfile2), "and we can unlink the symlink (rather than only rmdir)");
# to check the unlink code for symlinks isn't mis-handling non-symlink
# directories
ok(!unlink($tmpfile1), "we can't unlink the original directory");
ok(rmdir($tmpfile1), "we can rmdir it");
ok(open(my $fh, ">", $tmpfile1), "make a file");
close $fh if $fh;
ok(symlink($tmpfile1, $tmpfile2), "link to it");
ok(-l $tmpfile2, "-l sees a link");
ok(!-f _, "-f on the lstat result is false");
ok(-f $tmpfile2, "normal -d sees it as a file");
is(readlink($tmpfile2), $tmpfile1, "readlink works");
check_stat($tmpfile1, $tmpfile2, "check file and link stat are the same");
ok(unlink($tmpfile2), "unlink the symlink");
ok(unlink($tmpfile1), "and the file");
# test we don't treat directory junctions like symlinks
ok(mkdir($tmpfile1), "make a directory");
# mklink is available from Vista onwards
# this may only work in an admin shell
# MKLINK [[/D] | [/H] | [/J]] Link Target
if (system("mklink /j $tmpfile2 $tmpfile1") == 0) {
ok(!-l $tmpfile2, "junction doesn't look like a symlink");
ok(!unlink($tmpfile2), "no unlink magic for junctions");
rmdir($tmpfile2);
}
rmdir($tmpfile1);
done_testing();
sub check_stat {
my ($file1, $file2, $name) = @_;
my @stat1 = stat($file1);
my @stat2 = stat($file2);
is("@stat1", "@stat2", $name);
}

View File

@ -960,7 +960,7 @@ regen_config_h:
-$(MINIPERL) -I..\lib config_h.PL
rename config.h $(CFGH_TMPL)
$(CONFIGPM) : $(MINIPERL) ..\config.sh config_h.PL
$(CONFIGPM) : $(MINIPERL) ..\config.sh config_h.PL ..\git_version.h
$(MINIPERL) -I..\lib ..\configpm --chdir=..
$(XCOPY) ..\*.h $(COREDIR)\*.*
$(XCOPY) *.h $(COREDIR)\*.*
@ -1100,7 +1100,7 @@ $(WIN32_OBJ) : $(CORE_H)
$(CORE_OBJ) : $(CORE_H)
$(DLL_OBJ) : $(CORE_H)
perldll.def : $(MINIPERL) $(CONFIGPM) ..\embed.fnc ..\makedef.pl create_perllibst_h.pl
perldll.def : $(MINIPERL) $(CONFIGPM) ..\embed.fnc ..\makedef.pl create_perllibst_h.pl ..\git_version.h
$(MINIPERL) -I..\lib create_perllibst_h.pl
$(MINIPERL) -I..\lib -w ..\makedef.pl PLATFORM=win32 $(OPTIMIZE) $(DEFINES) $(BUILDOPT) \
CCTYPE=$(CCTYPE) TARG_DIR=..\ > perldll.def

View File

@ -446,7 +446,7 @@ d_random_r='undef'
d_readdir64_r='undef'
d_readdir='define'
d_readdir_r='undef'
d_readlink='undef'
d_readlink='define'
d_readv='undef'
d_recvmsg='undef'
d_regcomp='undef'
@ -571,7 +571,7 @@ d_strtoull='undef'
d_strtouq='undef'
d_strxfrm='define'
d_suidsafe='undef'
d_symlink='undef'
d_symlink='define'
d_syscall='undef'
d_syscallproto='undef'
d_sysconf='undef'

View File

@ -446,7 +446,7 @@ d_random_r='undef'
d_readdir64_r='undef'
d_readdir='define'
d_readdir_r='undef'
d_readlink='undef'
d_readlink='define'
d_readv='undef'
d_recvmsg='undef'
d_regcomp='undef'
@ -571,7 +571,7 @@ d_strtoull='undef'
d_strtouq='undef'
d_strxfrm='define'
d_suidsafe='undef'
d_symlink='undef'
d_symlink='define'
d_syscall='undef'
d_syscallproto='undef'
d_sysconf='undef'

View File

@ -9,7 +9,7 @@
/* Package name : perl5
* Source directory :
* Configuration time: Wed Oct 7 16:27:47 2020
* Configuration time: Wed Oct 7 16:35:37 2020
* Configured by : tony
* Target system :
*/
@ -342,7 +342,7 @@
* This symbol, if defined, indicates that the readlink routine is
* available to read the value of a symbolic link.
*/
/*#define HAS_READLINK / **/
#define HAS_READLINK /**/
/* HAS_REGCOMP:
* This symbol, if defined, indicates that the regcomp() routine is
@ -500,7 +500,7 @@
* This symbol, if defined, indicates that the symlink routine is available
* to create symbolic links.
*/
/*#define HAS_SYMLINK / **/
#define HAS_SYMLINK /**/
/* HAS_SYSCALL:
* This symbol, if defined, indicates that the syscall routine is

View File

@ -9,7 +9,7 @@
/* Package name : perl5
* Source directory :
* Configuration time: Wed Oct 7 16:25:12 2020
* Configuration time: Wed Oct 7 16:33:14 2020
* Configured by : tony
* Target system :
*/
@ -342,7 +342,7 @@
* This symbol, if defined, indicates that the readlink routine is
* available to read the value of a symbolic link.
*/
/*#define HAS_READLINK / **/
#define HAS_READLINK /**/
/* HAS_REGCOMP:
* This symbol, if defined, indicates that the regcomp() routine is
@ -500,7 +500,7 @@
* This symbol, if defined, indicates that the symlink routine is available
* to create symbolic links.
*/
/*#define HAS_SYMLINK / **/
#define HAS_SYMLINK /**/
/* HAS_SYSCALL:
* This symbol, if defined, indicates that the syscall routine is

View File

@ -986,6 +986,18 @@ PerlLIOLink(struct IPerlLIO* piPerl, const char*oldname, const char *newname)
return win32_link(oldname, newname);
}
int
PerlLIOSymLink(struct IPerlLIO* piPerl, const char*oldname, const char *newname)
{
return win32_symlink(oldname, newname);
}
int
PerlLIOReadLink(struct IPerlLIO* piPerl, const char *path, char *buf, size_t bufsiz)
{
return win32_readlink(path, buf, bufsiz);
}
Off_t
PerlLIOLseek(struct IPerlLIO* piPerl, int handle, Off_t offset, int origin)
{
@ -1098,6 +1110,8 @@ const struct IPerlLIO perlLIO =
PerlLIOUnlink,
PerlLIOUtime,
PerlLIOWrite,
PerlLIOSymLink,
PerlLIOReadLink
};

View File

@ -1697,6 +1697,81 @@ is_symlink(HANDLE h) {
return TRUE;
}
static BOOL
is_symlink_name(const char *name) {
HANDLE f = CreateFileA(name, GENERIC_READ, 0, NULL, OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT|FILE_FLAG_BACKUP_SEMANTICS, 0);
BOOL result;
if (f == INVALID_HANDLE_VALUE) {
return FALSE;
}
result = is_symlink(f);
CloseHandle(f);
return result;
}
DllExport int
win32_readlink(const char *pathname, char *buf, size_t bufsiz) {
MY_REPARSE_DATA_BUFFER linkdata;
const MY_SYMLINK_REPARSE_BUFFER * const sd =
&linkdata.Data.SymbolicLinkReparseBuffer;
HANDLE hlink;
DWORD fileattr = GetFileAttributes(pathname);
DWORD linkdata_returned;
int bytes_out;
BOOL used_default;
if (fileattr == INVALID_FILE_ATTRIBUTES) {
translate_to_errno();
return -1;
}
if (!(fileattr & FILE_ATTRIBUTE_REPARSE_POINT)) {
/* not a symbolic link */
errno = EINVAL;
return -1;
}
hlink =
CreateFileA(pathname, GENERIC_READ, 0, NULL, OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT|FILE_FLAG_BACKUP_SEMANTICS, 0);
if (hlink == INVALID_HANDLE_VALUE) {
translate_to_errno();
return -1;
}
if (!DeviceIoControl(hlink, FSCTL_GET_REPARSE_POINT, NULL, 0, &linkdata, sizeof(linkdata), &linkdata_returned, NULL)) {
translate_to_errno();
CloseHandle(hlink);
return -1;
}
CloseHandle(hlink);
if (linkdata_returned < offsetof(MY_REPARSE_DATA_BUFFER, Data.SymbolicLinkReparseBuffer.PathBuffer)
|| linkdata.ReparseTag != IO_REPARSE_TAG_SYMLINK) {
errno = EINVAL;
return -1;
}
bytes_out = WideCharToMultiByte(CP_ACP, WC_NO_BEST_FIT_CHARS,
sd->PathBuffer+sd->SubstituteNameOffset/2,
sd->SubstituteNameLength/2,
buf, bufsiz, NULL, &used_default);
if (bytes_out == 0 || used_default) {
/* failed conversion from unicode to ANSI or otherwise failed */
errno = EINVAL;
return -1;
}
if ((size_t)bytes_out > bufsiz) {
errno = EINVAL;
return -1;
}
return bytes_out;
}
DllExport int
win32_lstat(const char *path, Stat_t *sbuf)
{
@ -2129,8 +2204,14 @@ win32_unlink(const char *filename)
if (ret == -1)
(void)SetFileAttributesA(filename, attrs);
}
else
else if ((attrs & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY))
== (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY)
&& is_symlink_name(filename)) {
ret = rmdir(filename);
}
else {
ret = unlink(filename);
}
return ret;
}
@ -3341,46 +3422,76 @@ win32_link(const char *oldname, const char *newname)
{
return 0;
}
/* 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:
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. on a FAT volume */
errno = EINVAL;
break;
}
translate_to_errno();
return -1;
}
#ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE
# define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x2
#endif
DllExport int
win32_symlink(const char *oldfile, const char *newfile)
{
dTHX;
const char *dest_path = oldfile;
char szTargetName[MAX_PATH+1];
size_t oldfile_len = strlen(oldfile);
DWORD dest_attr;
DWORD create_flags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE;
/* oldfile might be relative and we don't want to change that,
so don't map that.
*/
newfile = PerlDir_mapA(newfile);
/* are we linking to a directory?
CreateSymlinkA() needs to know if the target is a directory,
if the oldfile is relative we need to make a relative path
based on the newfile
*/
if (oldfile_len >= 3 && oldfile[1] == ':' && oldfile[2] != '\\' && oldfile[2] != '/') {
/* relative to current directory on a drive */
/* dest_path = oldfile; already done */
}
else if (oldfile[0] != '\\' && oldfile[0] != '/') {
size_t newfile_len = strlen(newfile);
char *last_slash = strrchr(newfile, '/');
char *last_bslash = strrchr(newfile, '\\');
char *end_dir = last_slash && last_bslash
? ( last_slash > last_bslash ? last_slash : last_bslash)
: last_slash ? last_slash : last_bslash ? last_bslash : NULL;
if (end_dir) {
if ((end_dir - newfile + 1) + oldfile_len > MAX_PATH) {
/* too long */
errno = EINVAL;
return -1;
}
memcpy(szTargetName, newfile, end_dir - newfile + 1);
strcpy(szTargetName + (end_dir - newfile + 1), oldfile);
dest_path = szTargetName;
}
else {
/* newpath is just a filename */
/* dest_path = oldfile; */
}
}
dest_attr = GetFileAttributes(dest_path);
if (dest_attr != (DWORD)-1 && (dest_attr & FILE_ATTRIBUTE_DIRECTORY)) {
create_flags |= SYMBOLIC_LINK_FLAG_DIRECTORY;
}
if (!CreateSymbolicLinkA(newfile, oldfile, create_flags)) {
translate_to_errno();
return -1;
}
return 0;
}
DllExport int
win32_rename(const char *oname, const char *newname)
{

View File

@ -137,6 +137,8 @@ DllExport char* win32_longpath(char *path);
DllExport char* win32_ansipath(const WCHAR *path);
DllExport int win32_ioctl(int i, unsigned int u, char *data);
DllExport int win32_link(const char *oldname, const char *newname);
DllExport int win32_symlink(const char *oldname, const char *newname);
DllExport int win32_readlink(const char *path, char *buf, size_t bufsiz);
DllExport int win32_unlink(const char *f);
DllExport int win32_utime(const char *f, struct utimbuf *t);
DllExport int win32_gettimeofday(struct timeval *tp, void *not_used);
@ -286,7 +288,8 @@ END_EXTERN_C
#define putchar win32_putchar
#define access(p,m) win32_access(p,m)
#define chmod(p,m) win32_chmod(p,m)
#define symlink(targ,realp) win32_symlink(targ,realp)
#define readlink(p,buf,bufsiz) win32_readlink(p,buf,bufsiz)
#if !defined(MYMALLOC) || !defined(PERL_CORE)
#undef malloc
@ -309,6 +312,8 @@ END_EXTERN_C
#define times win32_times
#define ioctl win32_ioctl
#define link win32_link
#define symlink win32_symlink
#define readlink win32_readlink
#define unlink win32_unlink
#define utime win32_utime
#define gettimeofday win32_gettimeofday