Skip to content

Commit

Permalink
Add internal junction support to link APIs (#57996)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: David Cantu <[email protected]>
  • Loading branch information
3 people authored Aug 27, 2021
1 parent 1630a67 commit f7848bc
Show file tree
Hide file tree
Showing 9 changed files with 522 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,29 @@ internal static partial class Kernel32
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,28 @@ private static bool GetStaticNonPublicBooleanPropertyValue(string typeName, stri
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -460,11 +460,10 @@ private void ResolveLinkTarget_ReturnFinalTarget(string link1Path, string link1T
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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -36,5 +32,18 @@ protected DirectoryInfo CreateSelfReferencingSymbolicLink()
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;
}
}
}
71 changes: 71 additions & 0 deletions src/libraries/System.IO.FileSystem/tests/Junctions.Windows.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
#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)]
Expand All @@ -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();
Expand All @@ -48,20 +46,78 @@ public static bool CreateSymbolicLink(string linkPath, string targetPath, bool i
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)
Expand Down Expand Up @@ -93,8 +149,47 @@ public static void Unmount(string mountPoint)
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
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
Expand Down Expand Up @@ -80,9 +80,11 @@
<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" />
Expand Down Expand Up @@ -211,8 +213,7 @@
<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" />
Expand Down
Loading

0 comments on commit f7848bc

Please sign in to comment.