* 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>
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;
}
}
}
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;
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();
// 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
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;
+ }
}
}
--- /dev/null
+// 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);
+ }
+ }
+}
#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)]
[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();
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)
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
{
-<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" />
--- /dev/null
+// 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;
+ }
+ }
+ }
+}
}
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
{
// 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;
}