[release/6.0-rc1] Add internal junction support to link APIs (#58285)
authorgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sat, 28 Aug 2021 02:04:04 +0000 (20:04 -0600)
committerGitHub <noreply@github.com>
Sat, 28 Aug 2021 02:04:04 +0000 (20:04 -0600)
* Add mount point support to link APIs.

* Add junction and virtual drive tests.

* Move PrintName comment outside of if else of reparseTag check.

* Add Windows platform specific attribute to junction and virtual drive test classes.

* Revert FILE_NAME_OPENED to FILE_NAME_NORMALIZED

* Revert addition of FILE_NAME_OPENED const.

* Remove unnecessary enumeration junction test.

* Rename GetNewCwdPath to ChangeCurrentDirectory

* Make Junction_ResolveLinkTarget a theory and test both resolveFinalTarget

* Shorter name for targetPath string. Typo in comment. Fix Debug.Assert.

* Clarify test comment. Change PlatformDetection for OperatingSystem check.

* Cleaner unit tests for virtual drive, add indirection test

* Skip virtual drive tests in Windows Nano (subst not available). Small test rename.

* Simplify Junctions tests, add indirection test

* Address test suggestions.

* Revert MountHelper.CreateSymbolicLink changes. Unrelated, and will be refactored/removed in the future. Detect if SUBST is available in Windows machine, to bring back Nano.

* Add dwReserved0 check for mount points in GetFinalLinkTarget.

* Use Yoda we don't.

* Fix CI issues

Co-authored-by: carlossanlop <carlossanlop@users.noreply.github.com>
Co-authored-by: David Cantu <dacantu@microsoft.com>
src/libraries/Common/src/Interop/Windows/Kernel32/Interop.REPARSE_DATA_BUFFER.cs
src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs
src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystem.cs
src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs
src/libraries/System.IO.FileSystem/tests/Junctions.Windows.cs [new file with mode: 0644]
src/libraries/System.IO.FileSystem/tests/PortedCommon/ReparsePointUtilities.cs
src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj
src/libraries/System.IO.FileSystem/tests/VirtualDriveSymbolicLinks.Windows.cs [new file with mode: 0644]
src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs

index 3bcb916..123ac92 100644 (file)
@@ -14,24 +14,29 @@ internal static partial class Interop
         internal const uint SYMLINK_FLAG_RELATIVE = 1;
 
         // https://msdn.microsoft.com/library/windows/hardware/ff552012.aspx
-        // We don't need all the struct fields; omitting the rest.
         [StructLayout(LayoutKind.Sequential)]
-        internal unsafe struct REPARSE_DATA_BUFFER
+        internal unsafe struct SymbolicLinkReparseBuffer
         {
             internal uint ReparseTag;
             internal ushort ReparseDataLength;
             internal ushort Reserved;
-            internal SymbolicLinkReparseBuffer ReparseBufferSymbolicLink;
+            internal ushort SubstituteNameOffset;
+            internal ushort SubstituteNameLength;
+            internal ushort PrintNameOffset;
+            internal ushort PrintNameLength;
+            internal uint Flags;
+        }
 
-            [StructLayout(LayoutKind.Sequential)]
-            internal struct SymbolicLinkReparseBuffer
-            {
-                internal ushort SubstituteNameOffset;
-                internal ushort SubstituteNameLength;
-                internal ushort PrintNameOffset;
-                internal ushort PrintNameLength;
-                internal uint Flags;
-            }
+        [StructLayout(LayoutKind.Sequential)]
+        internal struct MountPointReparseBuffer
+        {
+            public uint ReparseTag;
+            public ushort ReparseDataLength;
+            public ushort Reserved;
+            public ushort SubstituteNameOffset;
+            public ushort SubstituteNameLength;
+            public ushort PrintNameOffset;
+            public ushort PrintNameLength;
         }
     }
 }
index efef067..fbd5df9 100644 (file)
@@ -267,6 +267,28 @@ namespace System
         public static bool IsIcuGlobalization => ICUVersion > new Version(0,0,0,0);
         public static bool IsNlsGlobalization => IsNotInvariantGlobalization && !IsIcuGlobalization;
 
+        public static bool IsSubstAvailable
+        {
+            get
+            {
+                try
+                {
+                    if (IsWindows)
+                    {
+                        string systemRoot = Environment.GetEnvironmentVariable("SystemRoot");
+                        if (string.IsNullOrWhiteSpace(systemRoot))
+                        {
+                            return false;
+                        }
+                        string system32 = Path.Combine(systemRoot, "System32");
+                        return File.Exists(Path.Combine(system32, "subst.exe"));
+                    }
+                }
+                catch { }
+                return false;
+            }
+        }
+
         private static Version GetICUVersion()
         {
             int version = 0;
index 0682f8c..9f61e73 100644 (file)
@@ -460,11 +460,10 @@ namespace System.IO.Tests
             Assert.Equal(filePath, finalTarget.FullName);
         }
 
+        // Must call inside a remote executor
         protected void CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(bool createOpposite)
         {
-            string tempCwd = GetRandomDirPath();
-            Directory.CreateDirectory(tempCwd);
-            Directory.SetCurrentDirectory(tempCwd);
+            string tempCwd = ChangeCurrentDirectory();
 
             // Create a dummy file or directory in cwd.
             string fileOrDirectoryInCwd = GetRandomFileName();
index f120cdf..6e5637d 100644 (file)
@@ -1,10 +1,6 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System.Buffers;
-using System.Diagnostics;
-using System.Runtime.InteropServices;
-using Microsoft.Win32.SafeHandles;
 using Xunit;
 
 namespace System.IO.Tests
@@ -36,5 +32,18 @@ namespace System.IO.Tests
         protected string GetRandomDirPath()  => Path.Join(ActualTestDirectory.Value, GetRandomDirName());
 
         private Lazy<string> ActualTestDirectory => new Lazy<string>(() => GetTestDirectoryActualCasing());
+
+        /// <summary>
+        /// Changes the current working directory path to a new temporary directory.
+        /// Important: Make sure to call this inside a remote executor to avoid changing the cwd for all tests in same process.
+        /// </summary>
+        /// <returns>The path of the new cwd.</returns>
+        protected string ChangeCurrentDirectory()
+        {
+            string tempCwd = GetRandomDirPath();
+            Directory.CreateDirectory(tempCwd);
+            Directory.SetCurrentDirectory(tempCwd);
+            return tempCwd;
+        }
     }
 }
diff --git a/src/libraries/System.IO.FileSystem/tests/Junctions.Windows.cs b/src/libraries/System.IO.FileSystem/tests/Junctions.Windows.cs
new file mode 100644 (file)
index 0000000..f4f2150
--- /dev/null
@@ -0,0 +1,71 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+
+namespace System.IO.Tests
+{
+    [PlatformSpecific(TestPlatforms.Windows)]
+    public class Junctions : BaseSymbolicLinks
+    {
+        protected DirectoryInfo CreateJunction(string junctionPath, string targetPath)
+        {
+            Assert.True(MountHelper.CreateJunction(junctionPath, targetPath));
+            DirectoryInfo junctionInfo = new(junctionPath);
+            return junctionInfo;
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public void Junction_ResolveLinkTarget(bool returnFinalTarget)
+        {
+            string junctionPath = GetRandomLinkPath();
+            string targetPath = GetRandomDirPath();
+
+            Directory.CreateDirectory(targetPath);
+            DirectoryInfo junctionInfo = CreateJunction(junctionPath, targetPath);
+
+            FileSystemInfo? targetFromDirectoryInfo = junctionInfo.ResolveLinkTarget(returnFinalTarget);
+            FileSystemInfo? targetFromDirectory = Directory.ResolveLinkTarget(junctionPath, returnFinalTarget);
+
+            Assert.True(targetFromDirectoryInfo is DirectoryInfo);
+            Assert.True(targetFromDirectory is DirectoryInfo);
+
+            Assert.Equal(targetPath, junctionInfo.LinkTarget);
+
+            Assert.Equal(targetPath, targetFromDirectoryInfo.FullName);
+            Assert.Equal(targetPath, targetFromDirectory.FullName);
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public void Junction_ResolveLinkTarget_WithIndirection(bool returnFinalTarget)
+        {
+            string firstJunctionPath = GetRandomLinkPath();
+            string middleJunctionPath = GetRandomLinkPath();
+            string targetPath = GetRandomDirPath();
+
+            Directory.CreateDirectory(targetPath);
+            CreateJunction(middleJunctionPath, targetPath);
+            DirectoryInfo firstJunctionInfo = CreateJunction(firstJunctionPath, middleJunctionPath);
+
+            string expectedTargetPath = returnFinalTarget ? targetPath : middleJunctionPath;
+
+            FileSystemInfo? targetFromDirectoryInfo = firstJunctionInfo.ResolveLinkTarget(returnFinalTarget);
+            FileSystemInfo? targetFromDirectory = Directory.ResolveLinkTarget(firstJunctionPath, returnFinalTarget);
+
+            Assert.True(targetFromDirectoryInfo is DirectoryInfo);
+            Assert.True(targetFromDirectory is DirectoryInfo);
+
+            // Always the immediate target
+            Assert.Equal(middleJunctionPath, firstJunctionInfo.LinkTarget);
+
+            Assert.Equal(expectedTargetPath, targetFromDirectoryInfo.FullName);
+            Assert.Equal(expectedTargetPath, targetFromDirectory.FullName);
+        }
+    }
+}
index 5423bda..8c7f360 100644 (file)
@@ -10,14 +10,14 @@ This is meant to contain useful utilities for IO related work in ReparsePoints
 #define DEBUG
 
 using System;
-using System.IO;
-using System.Text;
-using System.Diagnostics;
 using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
 using System.Runtime.InteropServices;
-using System.ComponentModel;
-using System.Threading;
+using System.Text;
 using System.Threading.Tasks;
+
 public static class MountHelper
 {
     [DllImport("kernel32.dll", EntryPoint = "GetVolumeNameForVolumeMountPointW", CharSet = CharSet.Unicode, BestFitMapping = false, SetLastError = true)]
@@ -28,9 +28,7 @@ public static class MountHelper
     [DllImport("kernel32.dll", EntryPoint = "DeleteVolumeMountPointW", CharSet = CharSet.Unicode, BestFitMapping = false, SetLastError = true)]
     private static extern bool DeleteVolumeMountPoint(string mountPoint);
 
-    /// <summary>Creates a symbolic link using command line tools</summary>
-    /// <param name="linkPath">The existing file</param>
-    /// <param name="targetPath"></param>
+    /// <summary>Creates a symbolic link using command line tools.</summary>
     public static bool CreateSymbolicLink(string linkPath, string targetPath, bool isDirectory)
     {
         Process symLinkProcess = new Process();
@@ -48,20 +46,78 @@ public static class MountHelper
         symLinkProcess.StartInfo.RedirectStandardOutput = true;
         symLinkProcess.Start();
 
-        if (symLinkProcess != null)
+        symLinkProcess.WaitForExit();
+        return symLinkProcess.ExitCode == 0;
+    }
+
+    /// <summary>On Windows, creates a junction using command line tools.</summary>
+    public static bool CreateJunction(string junctionPath, string targetPath)
+    {
+        if (!OperatingSystem.IsWindows())
         {
-            symLinkProcess.WaitForExit();
-            return (0 == symLinkProcess.ExitCode);
+            throw new PlatformNotSupportedException();
         }
-        else
+
+        return RunProcess(CreateProcessStartInfo("cmd", "/c", "mklink", "/J", junctionPath, targetPath));
+    }
+
+    ///<summary>
+    /// On Windows, mounts a folder to an assigned virtual drive letter using the subst command.
+    /// subst is not available in Windows Nano.
+    /// </summary>
+    public static char CreateVirtualDrive(string targetDir)
+    {
+        if (!OperatingSystem.IsWindows())
+        {
+            throw new PlatformNotSupportedException();
+        }
+
+        char driveLetter = GetNextAvailableDriveLetter();
+        bool success = RunProcess(CreateProcessStartInfo("cmd", "/c", SubstPath, $"{driveLetter}:", targetDir));
+        if (!success || !DriveInfo.GetDrives().Any(x => x.Name[0] == driveLetter))
+        {
+            throw new InvalidOperationException($"Could not create virtual drive {driveLetter}: with subst");
+        }
+        return driveLetter;
+
+        // Finds the next unused drive letter and returns it.
+        char GetNextAvailableDriveLetter()
         {
-            return false;
+            List<char> existingDrives = DriveInfo.GetDrives().Select(x => x.Name[0]).ToList();
+
+            // A,B are reserved, C is usually reserved
+            IEnumerable<int> range = Enumerable.Range('D', 'Z' - 'D');
+            IEnumerable<char> castRange = range.Select(x => Convert.ToChar(x));
+            IEnumerable<char> allDrivesLetters = castRange.Except(existingDrives);
+
+            if (!allDrivesLetters.Any())
+            {
+                throw new ArgumentOutOfRangeException("No drive letters available");
+            }
+
+            return allDrivesLetters.First();
         }
     }
 
-    public static void Mount(string volumeName, string mountPoint)
+    /// <summary>
+    /// On Windows, unassigns the specified virtual drive letter from its mounted folder.
+    /// </summary>
+    public static void DeleteVirtualDrive(char driveLetter)
     {
+        if (!OperatingSystem.IsWindows())
+        {
+            throw new PlatformNotSupportedException();
+        }
 
+        bool success = RunProcess(CreateProcessStartInfo("cmd", "/c", SubstPath, "/d", $"{driveLetter}:"));
+        if (!success || DriveInfo.GetDrives().Any(x => x.Name[0] == driveLetter))
+        {
+            throw new InvalidOperationException($"Could not delete virtual drive {driveLetter}: with subst");
+        }
+    }
+
+    public static void Mount(string volumeName, string mountPoint)
+    {
         if (volumeName[volumeName.Length - 1] != Path.DirectorySeparatorChar)
             volumeName += Path.DirectorySeparatorChar;
         if (mountPoint[mountPoint.Length - 1] != Path.DirectorySeparatorChar)
@@ -93,8 +149,47 @@ public static class MountHelper
             throw new Exception(string.Format("Win32 error: {0}", Marshal.GetLastPInvokeError()));
     }
 
+    private static ProcessStartInfo CreateProcessStartInfo(string fileName, params string[] arguments)
+    {
+        var info = new ProcessStartInfo
+        {
+            FileName = fileName,
+            UseShellExecute = false,
+            RedirectStandardOutput = true
+        };
+
+        foreach (var argument in arguments)
+        {
+            info.ArgumentList.Add(argument);
+        }
+
+        return info;
+    }
+
+    private static bool RunProcess(ProcessStartInfo startInfo)
+    {
+        var process = Process.Start(startInfo);
+        process.WaitForExit();
+        return process.ExitCode == 0;
+    }
+
+    private static string SubstPath
+    {
+        get
+        {
+            if (!OperatingSystem.IsWindows())
+            {
+                throw new PlatformNotSupportedException();
+            }
+
+            string systemRoot = Environment.GetEnvironmentVariable("SystemRoot") ?? @"C:\Windows";
+            string system32 = Path.Join(systemRoot, "System32");
+            return Path.Join(system32, "subst.exe");
+        }
+    }
+
     /// For standalone debugging help. Change Main0 to Main
-     public static void Main0(string[] args)
+    public static void Main0(string[] args)
     {
          try
         {
index 736d3b1..bb7cac2 100644 (file)
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
     <IncludeRemoteExecutor>true</IncludeRemoteExecutor>
     <Compile Include="FileSystemTest.Windows.cs" />
     <Compile Include="FileStream\ctor_options_as.Windows.cs" />
     <Compile Include="FileStream\FileStreamConformanceTests.Windows.cs" />
+    <Compile Include="Junctions.Windows.cs" />
     <Compile Include="RandomAccess\Mixed.Windows.cs" />
     <Compile Include="RandomAccess\NoBuffering.Windows.cs" />
     <Compile Include="RandomAccess\SectorAlignedMemory.Windows.cs" />
+    <Compile Include="VirtualDriveSymbolicLinks.Windows.cs" />
     <Compile Include="$(CommonPath)Interop\Windows\Interop.BOOL.cs" Link="Common\Interop\Windows\Interop.BOOL.cs" />
     <Compile Include="$(CommonPath)Interop\Windows\Interop.Libraries.cs" Link="Common\Interop\Windows\Interop.Libraries.cs" />
     <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.CreateFile.cs" Link="Common\Interop\Windows\Interop.CreateFile.cs" />
     <Compile Include="$(CommonTestPath)System\IO\TempFile.cs" Link="Common\System\IO\TempFile.cs" />
     <Compile Include="$(CommonTestPath)System\IO\PathFeatures.cs" Link="Common\System\IO\PathFeatures.cs" />
     <Content Include="DirectoryInfo\test-dir\dummy.txt" Link="test-dir\dummy.txt" />
-    <Compile Include="$(CommonPath)System\IO\PathInternal.CaseSensitivity.cs"
-             Link="Common\System\IO\PathInternal.CaseSensitivity.cs" />
+    <Compile Include="$(CommonPath)System\IO\PathInternal.CaseSensitivity.cs" Link="Common\System\IO\PathInternal.CaseSensitivity.cs" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="$(CommonTestPath)StreamConformanceTests\StreamConformanceTests.csproj" />
diff --git a/src/libraries/System.IO.FileSystem/tests/VirtualDriveSymbolicLinks.Windows.cs b/src/libraries/System.IO.FileSystem/tests/VirtualDriveSymbolicLinks.Windows.cs
new file mode 100644 (file)
index 0000000..d884250
--- /dev/null
@@ -0,0 +1,247 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace System.IO.Tests
+{
+    // Need to reuse the same virtual drive for all the test methods.
+    // Creating and disposing one virtual drive per class achieves this.
+    [PlatformSpecific(TestPlatforms.Windows)]
+    [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsSubstAvailable))]
+    public class VirtualDrive_SymbolicLinks : BaseSymbolicLinks
+    {
+        protected override void Dispose(bool disposing)
+        {
+            try
+            {
+                if (VirtualDriveLetter != default)
+                {
+                    MountHelper.DeleteVirtualDrive(VirtualDriveLetter);
+                    Directory.Delete(VirtualDriveTargetDir, recursive: true);
+                }
+            }
+            catch { } // avoid exceptions on dispose
+            base.Dispose(disposing);
+        }
+
+        [Theory]
+        // false, false, false, false          // Target is not in virtual drive
+        // false, false, true, false           // Target is not in virtual drive
+        [InlineData(false, true, false, true)] // Immediate target expected, target is in virtual drive
+        [InlineData(false, true, true, false)] // Final target expected, target is in virtual drive
+        // true, false, false, false           // Target is not in virtual drive
+        // true, false, true, false            // Target is not in virtual drive
+        [InlineData(true, true, false, true)]  // Immediate target expected, target is in virtual drive
+        [InlineData(true, true, true, false)]  // Final target expected, target is in virtual drive
+        public void VirtualDrive_SymbolicLinks_LinkAndTarget(
+            bool isLinkInVirtualDrive,
+            bool isTargetInVirtualDrive,
+            bool returnFinalTarget,
+            bool isExpectedTargetPathVirtual)
+        {
+            string linkExpectedFolderPath = GetVirtualOrRealPath(isLinkInVirtualDrive);
+            // File link
+            string fileLinkName = GetRandomLinkName();
+            string fileLinkPath = Path.Join(linkExpectedFolderPath, fileLinkName);
+            // Directory link
+            string dirLinkName = GetRandomLinkName();
+            string dirLinkPath = Path.Join(linkExpectedFolderPath, dirLinkName);
+
+            string targetExpectedFolderPath = GetVirtualOrRealPath(isTargetInVirtualDrive);
+            // File target
+            string fileTargetFileName = GetRandomFileName();
+            string fileTargetPath = Path.Join(targetExpectedFolderPath, fileTargetFileName);
+            // Directory target
+            string dirTargetFileName = GetRandomDirName();
+            string dirTargetPath = Path.Join(targetExpectedFolderPath, dirTargetFileName);
+
+            // Create targets
+            File.Create(fileTargetPath).Dispose();
+            Directory.CreateDirectory(dirTargetPath);
+
+            // Create links
+            FileInfo fileLinkInfo = new FileInfo(fileLinkPath);
+            fileLinkInfo.CreateAsSymbolicLink(fileTargetPath);
+            DirectoryInfo dirLinkInfo = new DirectoryInfo(dirLinkPath);
+            dirLinkInfo.CreateAsSymbolicLink(dirTargetPath);
+
+            // The expected results depend on the target location and the value of returnFinalTarget
+
+            // LinkTarget always retrieves the immediate target, so the expected value
+            // is always the path that was provided by the user for the target
+
+            // Verify the LinkTarget values of the link infos
+            Assert.Equal(fileTargetPath, fileLinkInfo.LinkTarget);
+            Assert.Equal(dirTargetPath, dirLinkInfo.LinkTarget);
+
+            // When the target is in a virtual drive, and returnFinalTarget is true,
+            // the expected target path is the real path, not the virtual path
+            string expectedTargetPath = GetVirtualOrRealPath(isExpectedTargetPathVirtual);
+
+            string expectedTargetFileInfoFullName = Path.Join(expectedTargetPath, fileTargetFileName);
+            string expectedTargetDirectoryInfoFullName = Path.Join(expectedTargetPath, dirTargetFileName);
+
+            // Verify target infos from link info instances
+            FileSystemInfo? targetFileInfoFromFileInfoLink = fileLinkInfo.ResolveLinkTarget(returnFinalTarget);
+            FileSystemInfo? targetDirInfoFromDirInfoLink = dirLinkInfo.ResolveLinkTarget(returnFinalTarget);
+
+            Assert.True(targetFileInfoFromFileInfoLink is FileInfo);
+            Assert.True(targetDirInfoFromDirInfoLink is DirectoryInfo);
+
+            Assert.Equal(expectedTargetFileInfoFullName, targetFileInfoFromFileInfoLink.FullName);
+            Assert.Equal(expectedTargetDirectoryInfoFullName, targetDirInfoFromDirInfoLink.FullName);
+
+            // Verify targets infos via static methods
+            FileSystemInfo? targetFileInfoFromFile = File.ResolveLinkTarget(fileLinkPath, returnFinalTarget);
+            FileSystemInfo? targetFileInfoFromDirectory = Directory.ResolveLinkTarget(dirLinkPath, returnFinalTarget);
+
+            Assert.True(targetFileInfoFromFile is FileInfo);
+            Assert.True(targetFileInfoFromDirectory is DirectoryInfo);
+
+            Assert.Equal(expectedTargetFileInfoFullName, targetFileInfoFromFile.FullName);
+            Assert.Equal(expectedTargetDirectoryInfoFullName, targetFileInfoFromDirectory.FullName);
+        }
+
+
+        [Theory]
+        // false, false, false, false, false           // Target is not in virtual drive
+        // false, false, false, true, false            // Target is not in virtual drive
+        [InlineData(false, false, true, false, false)] // Immediate target expected, middle link is NOT in virtual drive
+        [InlineData(false, false, true, true, false)]  // Final target expected, target is in virtual drive
+        // false, true, false, false, false            // Target is not in virtual drive
+        // false, true, false, true, false             // Target is not in virtual drive
+        [InlineData(false, true, true, false, true)]   // Immediate target expected, target is in virtual drive
+        [InlineData(false, true, true, true, false)]   // Final target expected, target is in virtual drive
+        // true, false, false, false, false            // Target is not in virtual drive
+        // true, false, false, true, false             // Target is not in virtual drive
+        [InlineData(true, false, true, false, false)]  // Immediate target expected, middle link is NOT in virtual drive
+        [InlineData(true, false, true, true, false)]   // Final target expected, target is in virtual drive
+        // true, true, false, false, false             // Target is not in virtual drive
+        // true, true, false, true, false              // Target is not in virtual drive
+        [InlineData(true, true, true, false, true)]    // Immediate target expected, target is in virtual drive
+        [InlineData(true, true, true, true, false)]    // Final target expected, target is in virtual drive
+        public void VirtualDrive_SymbolicLinks_WithIndirection(
+            bool isFirstLinkInVirtualDrive,
+            bool isMiddleLinkInVirtualDrive,
+            bool isTargetInVirtualDrive,
+            bool returnFinalTarget,
+            bool isExpectedTargetPathVirtual)
+        {
+            string firstLinkExpectedFolderPath = GetVirtualOrRealPath(isFirstLinkInVirtualDrive);
+            // File link
+            string fileLinkPath = Path.Join(firstLinkExpectedFolderPath, GetRandomLinkName());
+            // Directory link
+            string dirLinkPath = Path.Join(firstLinkExpectedFolderPath, GetRandomLinkName());
+
+            string middleLinkExpectedFolderPath = GetVirtualOrRealPath(isMiddleLinkInVirtualDrive);
+            // File middle link
+            string fileMiddleLinkFileName = GetRandomLinkName();
+            string fileMiddleLinkPath = Path.Join(middleLinkExpectedFolderPath, fileMiddleLinkFileName);
+            // Directory middle link
+            string dirMiddleLinkFileName = GetRandomLinkName();
+            string dirMiddleLinkPath = Path.Join(middleLinkExpectedFolderPath, dirMiddleLinkFileName);
+
+            string targetExpectedFolderPath = GetVirtualOrRealPath(isTargetInVirtualDrive);
+            // File final target
+            string fileFinalTargetFileName = GetRandomFileName();
+            string fileFinalTargetPath = Path.Join(targetExpectedFolderPath, fileFinalTargetFileName);
+            // Directory final target
+            string dirFinalTargetFileName = GetRandomDirName();
+            string dirFinalTargetPath = Path.Join(targetExpectedFolderPath, dirFinalTargetFileName);
+
+            // Create targets
+            File.Create(fileFinalTargetPath).Dispose();
+            Directory.CreateDirectory(dirFinalTargetPath);
+
+            // Create initial links
+            FileInfo fileLinkInfo = new FileInfo(fileLinkPath);
+            fileLinkInfo.CreateAsSymbolicLink(fileMiddleLinkPath);
+
+            DirectoryInfo dirLinkInfo = new DirectoryInfo(dirLinkPath);
+            dirLinkInfo.CreateAsSymbolicLink(dirMiddleLinkPath);
+
+            // Create middle links
+            FileInfo fileMiddleLinkInfo = new FileInfo(fileMiddleLinkPath);
+            fileMiddleLinkInfo.CreateAsSymbolicLink(fileFinalTargetPath);
+
+            DirectoryInfo dirMiddleLinkInfo = new DirectoryInfo(dirMiddleLinkPath);
+            dirMiddleLinkInfo.CreateAsSymbolicLink(dirFinalTargetPath);
+
+            // The expected results depend on the target location and the value of returnFinalTarget
+
+            // LinkTarget always retrieves the immediate target, so the expected value
+            // is always the path that was provided by the user for the middle link
+
+            // Verify the LinkTarget values of the link infos
+            Assert.Equal(fileMiddleLinkPath, fileLinkInfo.LinkTarget);
+            Assert.Equal(dirMiddleLinkPath, dirLinkInfo.LinkTarget);
+
+            // When the target is in a virtual drive,
+            // the expected target path is the real path, not the virtual path
+            // When returnFinalTarget is true, the expected target path is the
+            // resolved path from the final target in the chain of links
+            string expectedTargetPath = GetVirtualOrRealPath(isExpectedTargetPathVirtual);
+
+            string expectedTargetFileInfoFullName = Path.Join(expectedTargetPath,
+                returnFinalTarget ? fileFinalTargetFileName : fileMiddleLinkFileName);
+
+            string expectedTargetDirectoryInfoFullName = Path.Join(expectedTargetPath,
+                returnFinalTarget ? dirFinalTargetFileName : dirMiddleLinkFileName);
+
+            // Verify target infos from link info instances
+            FileSystemInfo? targetFileInfoFromFileInfoLink = fileLinkInfo.ResolveLinkTarget(returnFinalTarget);
+            FileSystemInfo? targetDirInfoFromDirInfoLink = dirLinkInfo.ResolveLinkTarget(returnFinalTarget);
+
+            Assert.True(targetFileInfoFromFileInfoLink is FileInfo);
+            Assert.True(targetDirInfoFromDirInfoLink is DirectoryInfo);
+
+            Assert.Equal(expectedTargetFileInfoFullName, targetFileInfoFromFileInfoLink.FullName);
+            Assert.Equal(expectedTargetDirectoryInfoFullName, targetDirInfoFromDirInfoLink.FullName);
+
+            // Verify targets infos via static methods
+            FileSystemInfo? targetFileInfoFromFile = File.ResolveLinkTarget(fileLinkPath, returnFinalTarget);
+            FileSystemInfo? targetFileInfoFromDirectory = Directory.ResolveLinkTarget(dirLinkPath, returnFinalTarget);
+
+            Assert.True(targetFileInfoFromFile is FileInfo);
+            Assert.True(targetFileInfoFromDirectory is DirectoryInfo);
+
+            Assert.Equal(expectedTargetFileInfoFullName, targetFileInfoFromFile.FullName);
+            Assert.Equal(expectedTargetDirectoryInfoFullName, targetFileInfoFromDirectory.FullName);
+        }
+
+        private string GetVirtualOrRealPath(bool condition) => condition ? $"{VirtualDriveLetter}:" : VirtualDriveTargetDir;
+
+        // Temporary Windows directory that can be mounted to a drive letter using the subst command
+        private string? _virtualDriveTargetDir = null;
+        private string VirtualDriveTargetDir
+        {
+            get
+            {
+                if (_virtualDriveTargetDir == null)
+                {
+                    // Create a folder inside the temp directory so that it can be mounted to a drive letter with subst
+                    _virtualDriveTargetDir = Path.Join(Path.GetTempPath(), GetRandomDirName());
+                    Directory.CreateDirectory(_virtualDriveTargetDir);
+                }
+
+                return _virtualDriveTargetDir;
+            }
+        }
+
+        // Windows drive letter that points to a mounted directory using the subst command
+        private char _virtualDriveLetter = default;
+        private char VirtualDriveLetter
+        {
+            get
+            {
+                if (_virtualDriveLetter == default)
+                {
+                    // Mount the folder to a drive letter
+                    _virtualDriveLetter = MountHelper.CreateVirtualDrive(VirtualDriveTargetDir);
+                }
+                return _virtualDriveLetter;
+            }
+        }
+    }
+}
index b92de0a..d2bde47 100644 (file)
@@ -499,32 +499,48 @@ namespace System.IO
                 }
 
                 Span<byte> bufferSpan = new(buffer);
-                success = MemoryMarshal.TryRead(bufferSpan, out Interop.Kernel32.REPARSE_DATA_BUFFER rdb);
+                success = MemoryMarshal.TryRead(bufferSpan, out Interop.Kernel32.SymbolicLinkReparseBuffer rbSymlink);
                 Debug.Assert(success);
 
-                // Only symbolic links are supported at the moment.
-                if ((rdb.ReparseTag & Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_SYMLINK) == 0)
-                {
-                    return null;
-                }
-
-                // We use PrintName instead of SubstitutneName given that we don't want to return a NT path when the link wasn't created with such NT path.
+                // We use PrintName(Offset|Length) instead of SubstituteName(Offset|Length) given that we don't want to return
+                // an NT path when the link wasn't created with such NT path.
                 // Unlike SubstituteName and GetFinalPathNameByHandle(), PrintName doesn't start with a prefix.
                 // Another nuance is that SubstituteName does not contain redundant path segments while PrintName does.
-                // PrintName can ONLY return a NT path if the link was created explicitly targeting a file/folder in such way. e.g: mklink /D linkName \??\C:\path\to\target.
-                int printNameNameOffset = sizeof(Interop.Kernel32.REPARSE_DATA_BUFFER) + rdb.ReparseBufferSymbolicLink.PrintNameOffset;
-                int printNameNameLength = rdb.ReparseBufferSymbolicLink.PrintNameLength;
+                // PrintName can ONLY return a NT path if the link was created explicitly targeting a file/folder in such way.
+                //   e.g: mklink /D linkName \??\C:\path\to\target.
+
+                if (rbSymlink.ReparseTag == Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_SYMLINK)
+                {
+                    int printNameOffset = sizeof(Interop.Kernel32.SymbolicLinkReparseBuffer) + rbSymlink.PrintNameOffset;
+                    int printNameLength = rbSymlink.PrintNameLength;
+
+                    Span<char> targetPath = MemoryMarshal.Cast<byte, char>(bufferSpan.Slice(printNameOffset, printNameLength));
+                    Debug.Assert((rbSymlink.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) == 0 || !PathInternal.IsExtended(targetPath));
 
-                Span<char> targetPath = MemoryMarshal.Cast<byte, char>(bufferSpan.Slice(printNameNameOffset, printNameNameLength));
-                Debug.Assert((rdb.ReparseBufferSymbolicLink.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) == 0 || !PathInternal.IsExtended(targetPath));
+                    if (returnFullPath && (rbSymlink.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) != 0)
+                    {
+                        // Target path is relative and is for ResolveLinkTarget(), we need to append the link directory.
+                        return Path.Join(Path.GetDirectoryName(linkPath.AsSpan()), targetPath);
+                    }
 
-                if (returnFullPath && (rdb.ReparseBufferSymbolicLink.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) != 0)
+                    return targetPath.ToString();
+                }
+                else if (rbSymlink.ReparseTag == Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_MOUNT_POINT)
                 {
-                    // Target path is relative and is for ResolveLinkTarget(), we need to append the link directory.
-                    return Path.Join(Path.GetDirectoryName(linkPath.AsSpan()), targetPath);
+                    success = MemoryMarshal.TryRead(bufferSpan, out Interop.Kernel32.MountPointReparseBuffer rbMountPoint);
+                    Debug.Assert(success);
+
+                    int printNameOffset = sizeof(Interop.Kernel32.MountPointReparseBuffer) + rbMountPoint.PrintNameOffset;
+                    int printNameLength = rbMountPoint.PrintNameLength;
+
+                    Span<char> targetPath = MemoryMarshal.Cast<byte, char>(bufferSpan.Slice(printNameOffset, printNameLength));
+
+                    // Unlike symlinks, mount point paths cannot be relative
+                    Debug.Assert(!PathInternal.IsPartiallyQualified(targetPath));
+                    return targetPath.ToString();
                 }
 
-                return targetPath.ToString();
+                return null;
             }
             finally
             {
@@ -539,8 +555,9 @@ namespace System.IO
 
             // The file or directory is not a reparse point.
             if ((data.dwFileAttributes & (uint)FileAttributes.ReparsePoint) == 0 ||
-                // Only symbolic links are supported at the moment.
-                (data.dwReserved0 & Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_SYMLINK) == 0)
+                // Only symbolic links and mount points are supported at the moment.
+                ((data.dwReserved0 & Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_SYMLINK) == 0 &&
+                 (data.dwReserved0 & Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_MOUNT_POINT) == 0))
             {
                 return null;
             }