This is a live mirror of the Perl 5 development currently hosted at https://github.com/perl/perl5
win32 symlink: treats paths that look like directories as directories
authorTony Cook <tony@develop-help.com>
Wed, 11 Nov 2020 00:42:23 +0000 (11:42 +1100)
committerTony Cook <tony@develop-help.com>
Tue, 1 Dec 2020 04:29:34 +0000 (15:29 +1100)
t/win32/symlink.t
win32/win32.c

index e9088cd..96ed7a1 100644 (file)
@@ -35,6 +35,34 @@ 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)");
 
+# test our various name based directory tests
+{
+    use Win32API::File qw(GetFileAttributes FILE_ATTRIBUTE_DIRECTORY
+                          INVALID_FILE_ATTRIBUTES);
+    # we can't use lstat() here, since the directory && symlink state
+    # can't be preserved in it's result, and normal stat would
+    # follow the link (which is broken for most of these)
+    # GetFileAttributes() doesn't follow the link and can present the
+    # directory && symlink state
+    my @tests =
+        (
+         "x:",
+         "x:\\",
+         "x:/",
+         "unknown\\",
+         "unknown/",
+         ".",
+         "..",
+        );
+    for my $path (@tests) {
+        ok(symlink($path, $tmpfile2), "symlink $path");
+        my $attr = GetFileAttributes($tmpfile2);
+        ok($attr != INVALID_FILE_ATTRIBUTES && ($attr & FILE_ATTRIBUTE_DIRECTORY) != 0,
+           "symlink $path: treated as a directory");
+        unlink($tmpfile2);
+    }
+}
+
 # to check the unlink code for symlinks isn't mis-handling non-symlink
 # directories
 ok(!unlink($tmpfile1), "we can't unlink the original directory");
index 7ea15e4..2922248 100644 (file)
@@ -3476,12 +3476,9 @@ 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);
     pCreateSymbolicLinkA_t pCreateSymbolicLinkA =
         (pCreateSymbolicLinkA_t)GetProcAddress(GetModuleHandle("kernel32.dll"), "CreateSymbolicLinkA");
-    DWORD dest_attr;
     DWORD create_flags = 0;
 
     /* this flag can be used only on Windows 10 1703 or newer */
@@ -3504,9 +3501,29 @@ win32_symlink(const char *oldfile, const char *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 it looks like a directory name:
+        - ends in slash
+        - is just . or ..
+        - ends in /. or /.. (with either slash)
+        - is a simple drive letter
+       assume it's a directory.
+
+       Otherwise if the oldfile is relative we need to make a relative path
+       based on the newfile to check if the target is a directory.
     */
+    if ((oldfile_len >= 1 && isSLASH(oldfile[oldfile_len-1])) ||
+        strEQ(oldfile, "..") ||
+        strEQ(oldfile, ".") ||
+        (isSLASH(oldfile[oldfile_len-2]) && oldfile[oldfile_len-1] == '.') ||
+        strEQ(oldfile+oldfile_len-3, "\\..") ||
+        strEQ(oldfile+oldfile_len-3, "/..") ||
+        (oldfile_len == 2 && oldfile[1] == ':')) {
+        create_flags |= SYMBOLIC_LINK_FLAG_DIRECTORY;
+    }
+    else { /* indent in a second commit */
+    DWORD dest_attr;
+    const char *dest_path = oldfile;
+    char szTargetName[MAX_PATH+1];
     if (oldfile_len >= 3 && oldfile[1] == ':' && oldfile[2] != '\\' && oldfile[2] != '/') {
         /* relative to current directory on a drive */
         /* dest_path = oldfile; already done */
@@ -3540,6 +3557,7 @@ win32_symlink(const char *oldfile, const char *newfile)
     if (dest_attr != (DWORD)-1 && (dest_attr & FILE_ATTRIBUTE_DIRECTORY)) {
         create_flags |= SYMBOLIC_LINK_FLAG_DIRECTORY;
     }
+    }
 
     if (!pCreateSymbolicLinkA(newfile, oldfile, create_flags)) {
         translate_to_errno();