Skip to content

Commit 9edb089

Browse files
github-actions[bot]carlossanlopjozkee
authored
[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 <[email protected]> Co-authored-by: David Cantu <[email protected]>
1 parent 7bfbc96 commit 9edb089

File tree

9 files changed

+522
-56
lines changed

9 files changed

+522
-56
lines changed

src/libraries/Common/src/Interop/Windows/Kernel32/Interop.REPARSE_DATA_BUFFER.cs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,29 @@ internal static partial class Kernel32
1414
internal const uint SYMLINK_FLAG_RELATIVE = 1;
1515

1616
// https://msdn.microsoft.com/library/windows/hardware/ff552012.aspx
17-
// We don't need all the struct fields; omitting the rest.
1817
[StructLayout(LayoutKind.Sequential)]
19-
internal unsafe struct REPARSE_DATA_BUFFER
18+
internal unsafe struct SymbolicLinkReparseBuffer
2019
{
2120
internal uint ReparseTag;
2221
internal ushort ReparseDataLength;
2322
internal ushort Reserved;
24-
internal SymbolicLinkReparseBuffer ReparseBufferSymbolicLink;
23+
internal ushort SubstituteNameOffset;
24+
internal ushort SubstituteNameLength;
25+
internal ushort PrintNameOffset;
26+
internal ushort PrintNameLength;
27+
internal uint Flags;
28+
}
2529

26-
[StructLayout(LayoutKind.Sequential)]
27-
internal struct SymbolicLinkReparseBuffer
28-
{
29-
internal ushort SubstituteNameOffset;
30-
internal ushort SubstituteNameLength;
31-
internal ushort PrintNameOffset;
32-
internal ushort PrintNameLength;
33-
internal uint Flags;
34-
}
30+
[StructLayout(LayoutKind.Sequential)]
31+
internal struct MountPointReparseBuffer
32+
{
33+
public uint ReparseTag;
34+
public ushort ReparseDataLength;
35+
public ushort Reserved;
36+
public ushort SubstituteNameOffset;
37+
public ushort SubstituteNameLength;
38+
public ushort PrintNameOffset;
39+
public ushort PrintNameLength;
3540
}
3641
}
3742
}

src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,28 @@ private static bool GetStaticNonPublicBooleanPropertyValue(string typeName, stri
267267
public static bool IsIcuGlobalization => ICUVersion > new Version(0,0,0,0);
268268
public static bool IsNlsGlobalization => IsNotInvariantGlobalization && !IsIcuGlobalization;
269269

270+
public static bool IsSubstAvailable
271+
{
272+
get
273+
{
274+
try
275+
{
276+
if (IsWindows)
277+
{
278+
string systemRoot = Environment.GetEnvironmentVariable("SystemRoot");
279+
if (string.IsNullOrWhiteSpace(systemRoot))
280+
{
281+
return false;
282+
}
283+
string system32 = Path.Combine(systemRoot, "System32");
284+
return File.Exists(Path.Combine(system32, "subst.exe"));
285+
}
286+
}
287+
catch { }
288+
return false;
289+
}
290+
}
291+
270292
private static Version GetICUVersion()
271293
{
272294
int version = 0;

src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystem.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -460,11 +460,10 @@ private void ResolveLinkTarget_ReturnFinalTarget(string link1Path, string link1T
460460
Assert.Equal(filePath, finalTarget.FullName);
461461
}
462462

463+
// Must call inside a remote executor
463464
protected void CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(bool createOpposite)
464465
{
465-
string tempCwd = GetRandomDirPath();
466-
Directory.CreateDirectory(tempCwd);
467-
Directory.SetCurrentDirectory(tempCwd);
466+
string tempCwd = ChangeCurrentDirectory();
468467

469468
// Create a dummy file or directory in cwd.
470469
string fileOrDirectoryInCwd = GetRandomFileName();

src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Buffers;
5-
using System.Diagnostics;
6-
using System.Runtime.InteropServices;
7-
using Microsoft.Win32.SafeHandles;
84
using Xunit;
95

106
namespace System.IO.Tests
@@ -36,5 +32,18 @@ protected DirectoryInfo CreateSelfReferencingSymbolicLink()
3632
protected string GetRandomDirPath() => Path.Join(ActualTestDirectory.Value, GetRandomDirName());
3733

3834
private Lazy<string> ActualTestDirectory => new Lazy<string>(() => GetTestDirectoryActualCasing());
35+
36+
/// <summary>
37+
/// Changes the current working directory path to a new temporary directory.
38+
/// Important: Make sure to call this inside a remote executor to avoid changing the cwd for all tests in same process.
39+
/// </summary>
40+
/// <returns>The path of the new cwd.</returns>
41+
protected string ChangeCurrentDirectory()
42+
{
43+
string tempCwd = GetRandomDirPath();
44+
Directory.CreateDirectory(tempCwd);
45+
Directory.SetCurrentDirectory(tempCwd);
46+
return tempCwd;
47+
}
3948
}
4049
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using Xunit;
7+
8+
namespace System.IO.Tests
9+
{
10+
[PlatformSpecific(TestPlatforms.Windows)]
11+
public class Junctions : BaseSymbolicLinks
12+
{
13+
protected DirectoryInfo CreateJunction(string junctionPath, string targetPath)
14+
{
15+
Assert.True(MountHelper.CreateJunction(junctionPath, targetPath));
16+
DirectoryInfo junctionInfo = new(junctionPath);
17+
return junctionInfo;
18+
}
19+
20+
[Theory]
21+
[InlineData(false)]
22+
[InlineData(true)]
23+
public void Junction_ResolveLinkTarget(bool returnFinalTarget)
24+
{
25+
string junctionPath = GetRandomLinkPath();
26+
string targetPath = GetRandomDirPath();
27+
28+
Directory.CreateDirectory(targetPath);
29+
DirectoryInfo junctionInfo = CreateJunction(junctionPath, targetPath);
30+
31+
FileSystemInfo? targetFromDirectoryInfo = junctionInfo.ResolveLinkTarget(returnFinalTarget);
32+
FileSystemInfo? targetFromDirectory = Directory.ResolveLinkTarget(junctionPath, returnFinalTarget);
33+
34+
Assert.True(targetFromDirectoryInfo is DirectoryInfo);
35+
Assert.True(targetFromDirectory is DirectoryInfo);
36+
37+
Assert.Equal(targetPath, junctionInfo.LinkTarget);
38+
39+
Assert.Equal(targetPath, targetFromDirectoryInfo.FullName);
40+
Assert.Equal(targetPath, targetFromDirectory.FullName);
41+
}
42+
43+
[Theory]
44+
[InlineData(false)]
45+
[InlineData(true)]
46+
public void Junction_ResolveLinkTarget_WithIndirection(bool returnFinalTarget)
47+
{
48+
string firstJunctionPath = GetRandomLinkPath();
49+
string middleJunctionPath = GetRandomLinkPath();
50+
string targetPath = GetRandomDirPath();
51+
52+
Directory.CreateDirectory(targetPath);
53+
CreateJunction(middleJunctionPath, targetPath);
54+
DirectoryInfo firstJunctionInfo = CreateJunction(firstJunctionPath, middleJunctionPath);
55+
56+
string expectedTargetPath = returnFinalTarget ? targetPath : middleJunctionPath;
57+
58+
FileSystemInfo? targetFromDirectoryInfo = firstJunctionInfo.ResolveLinkTarget(returnFinalTarget);
59+
FileSystemInfo? targetFromDirectory = Directory.ResolveLinkTarget(firstJunctionPath, returnFinalTarget);
60+
61+
Assert.True(targetFromDirectoryInfo is DirectoryInfo);
62+
Assert.True(targetFromDirectory is DirectoryInfo);
63+
64+
// Always the immediate target
65+
Assert.Equal(middleJunctionPath, firstJunctionInfo.LinkTarget);
66+
67+
Assert.Equal(expectedTargetPath, targetFromDirectoryInfo.FullName);
68+
Assert.Equal(expectedTargetPath, targetFromDirectory.FullName);
69+
}
70+
}
71+
}

src/libraries/System.IO.FileSystem/tests/PortedCommon/ReparsePointUtilities.cs

Lines changed: 110 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010
#define DEBUG
1111

1212
using System;
13-
using System.IO;
14-
using System.Text;
15-
using System.Diagnostics;
1613
using System.Collections.Generic;
14+
using System.Diagnostics;
15+
using System.IO;
16+
using System.Linq;
1717
using System.Runtime.InteropServices;
18-
using System.ComponentModel;
19-
using System.Threading;
18+
using System.Text;
2019
using System.Threading.Tasks;
20+
2121
public static class MountHelper
2222
{
2323
[DllImport("kernel32.dll", EntryPoint = "GetVolumeNameForVolumeMountPointW", CharSet = CharSet.Unicode, BestFitMapping = false, SetLastError = true)]
@@ -28,9 +28,7 @@ public static class MountHelper
2828
[DllImport("kernel32.dll", EntryPoint = "DeleteVolumeMountPointW", CharSet = CharSet.Unicode, BestFitMapping = false, SetLastError = true)]
2929
private static extern bool DeleteVolumeMountPoint(string mountPoint);
3030

31-
/// <summary>Creates a symbolic link using command line tools</summary>
32-
/// <param name="linkPath">The existing file</param>
33-
/// <param name="targetPath"></param>
31+
/// <summary>Creates a symbolic link using command line tools.</summary>
3432
public static bool CreateSymbolicLink(string linkPath, string targetPath, bool isDirectory)
3533
{
3634
Process symLinkProcess = new Process();
@@ -48,20 +46,78 @@ public static bool CreateSymbolicLink(string linkPath, string targetPath, bool i
4846
symLinkProcess.StartInfo.RedirectStandardOutput = true;
4947
symLinkProcess.Start();
5048

51-
if (symLinkProcess != null)
49+
symLinkProcess.WaitForExit();
50+
return symLinkProcess.ExitCode == 0;
51+
}
52+
53+
/// <summary>On Windows, creates a junction using command line tools.</summary>
54+
public static bool CreateJunction(string junctionPath, string targetPath)
55+
{
56+
if (!OperatingSystem.IsWindows())
5257
{
53-
symLinkProcess.WaitForExit();
54-
return (0 == symLinkProcess.ExitCode);
58+
throw new PlatformNotSupportedException();
5559
}
56-
else
60+
61+
return RunProcess(CreateProcessStartInfo("cmd", "/c", "mklink", "/J", junctionPath, targetPath));
62+
}
63+
64+
///<summary>
65+
/// On Windows, mounts a folder to an assigned virtual drive letter using the subst command.
66+
/// subst is not available in Windows Nano.
67+
/// </summary>
68+
public static char CreateVirtualDrive(string targetDir)
69+
{
70+
if (!OperatingSystem.IsWindows())
71+
{
72+
throw new PlatformNotSupportedException();
73+
}
74+
75+
char driveLetter = GetNextAvailableDriveLetter();
76+
bool success = RunProcess(CreateProcessStartInfo("cmd", "/c", SubstPath, $"{driveLetter}:", targetDir));
77+
if (!success || !DriveInfo.GetDrives().Any(x => x.Name[0] == driveLetter))
78+
{
79+
throw new InvalidOperationException($"Could not create virtual drive {driveLetter}: with subst");
80+
}
81+
return driveLetter;
82+
83+
// Finds the next unused drive letter and returns it.
84+
char GetNextAvailableDriveLetter()
5785
{
58-
return false;
86+
List<char> existingDrives = DriveInfo.GetDrives().Select(x => x.Name[0]).ToList();
87+
88+
// A,B are reserved, C is usually reserved
89+
IEnumerable<int> range = Enumerable.Range('D', 'Z' - 'D');
90+
IEnumerable<char> castRange = range.Select(x => Convert.ToChar(x));
91+
IEnumerable<char> allDrivesLetters = castRange.Except(existingDrives);
92+
93+
if (!allDrivesLetters.Any())
94+
{
95+
throw new ArgumentOutOfRangeException("No drive letters available");
96+
}
97+
98+
return allDrivesLetters.First();
5999
}
60100
}
61101

62-
public static void Mount(string volumeName, string mountPoint)
102+
/// <summary>
103+
/// On Windows, unassigns the specified virtual drive letter from its mounted folder.
104+
/// </summary>
105+
public static void DeleteVirtualDrive(char driveLetter)
63106
{
107+
if (!OperatingSystem.IsWindows())
108+
{
109+
throw new PlatformNotSupportedException();
110+
}
64111

112+
bool success = RunProcess(CreateProcessStartInfo("cmd", "/c", SubstPath, "/d", $"{driveLetter}:"));
113+
if (!success || DriveInfo.GetDrives().Any(x => x.Name[0] == driveLetter))
114+
{
115+
throw new InvalidOperationException($"Could not delete virtual drive {driveLetter}: with subst");
116+
}
117+
}
118+
119+
public static void Mount(string volumeName, string mountPoint)
120+
{
65121
if (volumeName[volumeName.Length - 1] != Path.DirectorySeparatorChar)
66122
volumeName += Path.DirectorySeparatorChar;
67123
if (mountPoint[mountPoint.Length - 1] != Path.DirectorySeparatorChar)
@@ -93,8 +149,47 @@ public static void Unmount(string mountPoint)
93149
throw new Exception(string.Format("Win32 error: {0}", Marshal.GetLastPInvokeError()));
94150
}
95151

152+
private static ProcessStartInfo CreateProcessStartInfo(string fileName, params string[] arguments)
153+
{
154+
var info = new ProcessStartInfo
155+
{
156+
FileName = fileName,
157+
UseShellExecute = false,
158+
RedirectStandardOutput = true
159+
};
160+
161+
foreach (var argument in arguments)
162+
{
163+
info.ArgumentList.Add(argument);
164+
}
165+
166+
return info;
167+
}
168+
169+
private static bool RunProcess(ProcessStartInfo startInfo)
170+
{
171+
var process = Process.Start(startInfo);
172+
process.WaitForExit();
173+
return process.ExitCode == 0;
174+
}
175+
176+
private static string SubstPath
177+
{
178+
get
179+
{
180+
if (!OperatingSystem.IsWindows())
181+
{
182+
throw new PlatformNotSupportedException();
183+
}
184+
185+
string systemRoot = Environment.GetEnvironmentVariable("SystemRoot") ?? @"C:\Windows";
186+
string system32 = Path.Join(systemRoot, "System32");
187+
return Path.Join(system32, "subst.exe");
188+
}
189+
}
190+
96191
/// For standalone debugging help. Change Main0 to Main
97-
public static void Main0(string[] args)
192+
public static void Main0(string[] args)
98193
{
99194
try
100195
{

src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
44
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
@@ -80,9 +80,11 @@
8080
<Compile Include="FileSystemTest.Windows.cs" />
8181
<Compile Include="FileStream\ctor_options_as.Windows.cs" />
8282
<Compile Include="FileStream\FileStreamConformanceTests.Windows.cs" />
83+
<Compile Include="Junctions.Windows.cs" />
8384
<Compile Include="RandomAccess\Mixed.Windows.cs" />
8485
<Compile Include="RandomAccess\NoBuffering.Windows.cs" />
8586
<Compile Include="RandomAccess\SectorAlignedMemory.Windows.cs" />
87+
<Compile Include="VirtualDriveSymbolicLinks.Windows.cs" />
8688
<Compile Include="$(CommonPath)Interop\Windows\Interop.BOOL.cs" Link="Common\Interop\Windows\Interop.BOOL.cs" />
8789
<Compile Include="$(CommonPath)Interop\Windows\Interop.Libraries.cs" Link="Common\Interop\Windows\Interop.Libraries.cs" />
8890
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.CreateFile.cs" Link="Common\Interop\Windows\Interop.CreateFile.cs" />
@@ -211,8 +213,7 @@
211213
<Compile Include="$(CommonTestPath)System\IO\TempFile.cs" Link="Common\System\IO\TempFile.cs" />
212214
<Compile Include="$(CommonTestPath)System\IO\PathFeatures.cs" Link="Common\System\IO\PathFeatures.cs" />
213215
<Content Include="DirectoryInfo\test-dir\dummy.txt" Link="test-dir\dummy.txt" />
214-
<Compile Include="$(CommonPath)System\IO\PathInternal.CaseSensitivity.cs"
215-
Link="Common\System\IO\PathInternal.CaseSensitivity.cs" />
216+
<Compile Include="$(CommonPath)System\IO\PathInternal.CaseSensitivity.cs" Link="Common\System\IO\PathInternal.CaseSensitivity.cs" />
216217
</ItemGroup>
217218
<ItemGroup>
218219
<ProjectReference Include="$(CommonTestPath)StreamConformanceTests\StreamConformanceTests.csproj" />

0 commit comments

Comments
 (0)