From 9edb089db864c8a2ac61bf3d19aea531a020e442 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 27 Aug 2021 20:04:04 -0600 Subject: [PATCH] [release/6.0-rc1] Add internal junction support to link APIs (#58285) * 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 Co-authored-by: David Cantu --- .../Kernel32/Interop.REPARSE_DATA_BUFFER.cs | 29 ++- .../TestUtilities/System/PlatformDetection.cs | 22 ++ .../SymbolicLinks/BaseSymbolicLinks.FileSystem.cs | 5 +- .../tests/Base/SymbolicLinks/BaseSymbolicLinks.cs | 17 +- .../tests/Junctions.Windows.cs | 71 ++++++ .../tests/PortedCommon/ReparsePointUtilities.cs | 125 +++++++++-- .../tests/System.IO.FileSystem.Tests.csproj | 7 +- .../tests/VirtualDriveSymbolicLinks.Windows.cs | 247 +++++++++++++++++++++ .../src/System/IO/FileSystem.Windows.cs | 55 +++-- 9 files changed, 522 insertions(+), 56 deletions(-) create mode 100644 src/libraries/System.IO.FileSystem/tests/Junctions.Windows.cs create mode 100644 src/libraries/System.IO.FileSystem/tests/VirtualDriveSymbolicLinks.Windows.cs diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.REPARSE_DATA_BUFFER.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.REPARSE_DATA_BUFFER.cs index 3bcb916..123ac92 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.REPARSE_DATA_BUFFER.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.REPARSE_DATA_BUFFER.cs @@ -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; } } } diff --git a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs index efef067..fbd5df9 100644 --- a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs +++ b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs @@ -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; diff --git a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystem.cs b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystem.cs index 0682f8c..9f61e73 100644 --- a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystem.cs +++ b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystem.cs @@ -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(); diff --git a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs index f120cdf..6e5637d 100644 --- a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs +++ b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs @@ -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 ActualTestDirectory => new Lazy(() => GetTestDirectoryActualCasing()); + + /// + /// 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. + /// + /// The path of the new cwd. + 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 index 0000000..f4f2150 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/Junctions.Windows.cs @@ -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); + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/PortedCommon/ReparsePointUtilities.cs b/src/libraries/System.IO.FileSystem/tests/PortedCommon/ReparsePointUtilities.cs index 5423bda..8c7f360 100644 --- a/src/libraries/System.IO.FileSystem/tests/PortedCommon/ReparsePointUtilities.cs +++ b/src/libraries/System.IO.FileSystem/tests/PortedCommon/ReparsePointUtilities.cs @@ -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); - /// Creates a symbolic link using command line tools - /// The existing file - /// + /// Creates a symbolic link using command line tools. 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; + } + + /// On Windows, creates a junction using command line tools. + 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)); + } + + /// + /// On Windows, mounts a folder to an assigned virtual drive letter using the subst command. + /// subst is not available in Windows Nano. + /// + 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 existingDrives = DriveInfo.GetDrives().Select(x => x.Name[0]).ToList(); + + // A,B are reserved, C is usually reserved + IEnumerable range = Enumerable.Range('D', 'Z' - 'D'); + IEnumerable castRange = range.Select(x => Convert.ToChar(x)); + IEnumerable 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) + /// + /// On Windows, unassigns the specified virtual drive letter from its mounted folder. + /// + 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 { diff --git a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj index 736d3b1..bb7cac2 100644 --- a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj +++ b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj @@ -1,4 +1,4 @@ - + true true @@ -80,9 +80,11 @@ + + @@ -211,8 +213,7 @@ - + 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 index 0000000..d884250 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/VirtualDriveSymbolicLinks.Windows.cs @@ -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; + } + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs index b92de0a..d2bde47 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs @@ -499,32 +499,48 @@ namespace System.IO } Span 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 targetPath = MemoryMarshal.Cast(bufferSpan.Slice(printNameOffset, printNameLength)); + Debug.Assert((rbSymlink.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) == 0 || !PathInternal.IsExtended(targetPath)); - Span targetPath = MemoryMarshal.Cast(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 targetPath = MemoryMarshal.Cast(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; } -- 2.7.4