Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +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.Collections.Generic;
using System.IO.Enumeration;
using System.Linq;
using Xunit;
Expand Down Expand Up @@ -158,7 +157,7 @@ public void ResolveLinkTarget_ReturnsNull_NotALink(bool returnFinalTarget)
}

[Theory]
[MemberData(nameof(ResolveLinkTarget_PathToTarget_Data))]
[MemberData(nameof(SymbolicLink_ResolveLinkTarget_PathToTarget_Data))]
public void ResolveLinkTarget_Succeeds(string pathToTarget, bool returnFinalTarget)
{
string linkPath = GetRandomLinkPath();
Expand Down Expand Up @@ -344,32 +343,6 @@ public void DetectLinkReferenceToSelf()
Assert.Throws<IOException>(() => ResolveLinkTarget(linkPath, returnFinalTarget: true));
}

[Fact]
public void CreateSymbolicLink_WrongTargetType_Throws()
{
// dirLink -> file
// fileLink -> dir

string targetPath = GetRandomFilePath();
CreateFileOrDirectory(targetPath, createOpposite: true); // The underlying file system entry needs to be different
Assert.Throws<IOException>(() => CreateSymbolicLink(GetRandomFilePath(), targetPath));
}

[Fact]
public void CreateSymbolicLink_WrongTargetType_Indirect_Throws()
{
// link-2 (dir) -> link-1 (file) -> file
// link-2 (file) -> link-1 (dir) -> dir
string targetPath = GetRandomFilePath();
string firstLinkPath = GetRandomFilePath();
string secondLinkPath = GetRandomFilePath();

CreateFileOrDirectory(targetPath, createOpposite: true);
CreateSymbolicLink_Opposite(firstLinkPath, targetPath);

Assert.Throws<IOException>(() => CreateSymbolicLink(secondLinkPath, firstLinkPath));
}

[Fact]
public void CreateSymbolicLink_CorrectTargetType_Indirect_Succeeds()
{
Expand Down Expand Up @@ -429,22 +402,22 @@ private void ResolveLinkTarget_ReturnFinalTarget(string link1Path, string link1T
Assert.True(link2Info.Exists);
Assert.True(link2Info.Attributes.HasFlag(FileAttributes.ReparsePoint));
AssertIsCorrectTypeAndDirectoryAttribute(link2Info);
Assert.Equal(link2Target, link2Info.LinkTarget);
AssertPathEquals_RelativeSegments(link2Target, link2Info.LinkTarget);

// link1 to link2
FileSystemInfo link1Info = CreateSymbolicLink(link1Path, link1Target);
Assert.True(link1Info.Exists);
Assert.True(link1Info.Attributes.HasFlag(FileAttributes.ReparsePoint));
AssertIsCorrectTypeAndDirectoryAttribute(link1Info);
Assert.Equal(link1Target, link1Info.LinkTarget);
AssertPathEquals_RelativeSegments(link1Target, link1Info.LinkTarget);

// link1: do not follow symlinks
FileSystemInfo link1TargetInfo = ResolveLinkTarget(link1Path, returnFinalTarget: false);
Assert.True(link1TargetInfo.Exists);
AssertIsCorrectTypeAndDirectoryAttribute(link1TargetInfo);
Assert.True(link1TargetInfo.Attributes.HasFlag(FileAttributes.ReparsePoint));
Assert.Equal(link2Path, link1TargetInfo.FullName);
Assert.Equal(link2Target, link1TargetInfo.LinkTarget);
AssertPathEquals_RelativeSegments(link2Target, link1TargetInfo.LinkTarget);

// link2: do not follow symlinks
FileSystemInfo link2TargetInfo = ResolveLinkTarget(link2Path, returnFinalTarget: false);
Expand All @@ -460,6 +433,19 @@ private void ResolveLinkTarget_ReturnFinalTarget(string link1Path, string link1T
AssertIsCorrectTypeAndDirectoryAttribute(finalTarget);
Assert.False(finalTarget.Attributes.HasFlag(FileAttributes.ReparsePoint));
Assert.Equal(filePath, finalTarget.FullName);

void AssertPathEquals_RelativeSegments(string expected, string actual)
{
#if WINDOWS
// DeviceIoControl canonicalizes the target path i.e: removes redundant segments.
int rootLength = PathInternal.GetRootLength(expected);
if (rootLength > 0)
{
expected = PathInternal.RemoveRelativeSegments(expected, rootLength);
}
#endif
Assert.Equal(expected, actual);
}
}

// Must call inside a remote executor
Expand Down Expand Up @@ -511,50 +497,5 @@ protected void CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(bool
(entry.Attributes & FileAttributes.ReparsePoint) != 0
}.FirstOrDefault();
}

public static IEnumerable<object[]> ResolveLinkTarget_PathToTarget_Data
{
get
{
foreach (string path in PathToTargetData)
{
yield return new object[] { path, false };
yield return new object[] { path, true };
}
}
}

internal static IEnumerable<string> PathToTargetData
{
get
{
if (OperatingSystem.IsWindows())
{
//Non-rooted relative
yield return "foo";
yield return @".\foo";
yield return @"..\foo";
// Rooted relative
yield return @"\foo";
// Rooted absolute
yield return Path.Combine(Path.GetTempPath(), "foo");
// Extended DOS
yield return Path.Combine(@"\\?\", Path.GetTempPath(), "foo");
// UNC
yield return @"\\LOCALHOST\share\path";
}
else
{
//Non-rooted relative
yield return "foo";
yield return "./foo";
yield return "../foo";
// Rooted relative
yield return "/foo";
// Rooted absolute
Path.Combine(Path.GetTempPath(), "foo");
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +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.Collections.Generic;
using System.Diagnostics;
using Xunit;

namespace System.IO.Tests
Expand Down Expand Up @@ -54,7 +52,7 @@ public void LinkTarget_ReturnsNull_NotALink()
}

[Theory]
[MemberData(nameof(LinkTarget_PathToTarget_Data))]
[MemberData(nameof(SymbolicLink_LinkTarget_PathToTarget_Data))]
public void LinkTarget_Succeeds(string pathToTarget)
{
FileSystemInfo linkInfo = CreateSymbolicLink(GetRandomLinkPath(), pathToTarget);
Expand Down Expand Up @@ -86,16 +84,5 @@ public void LinkTarget_RefreshesCorrectly()
Assert.Equal(newPathToTarget, linkInfo.LinkTarget);
Assert.Equal(newLinkInfo.LinkTarget, linkInfo.LinkTarget);
}

public static IEnumerable<object[]> LinkTarget_PathToTarget_Data
{
get
{
foreach (string path in PathToTargetData)
{
yield return new object[] { path };
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// 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
Expand Down Expand Up @@ -45,5 +47,95 @@ protected string ChangeCurrentDirectory()
Directory.SetCurrentDirectory(tempCwd);
return tempCwd;
}

public static IEnumerable<object[]> SymbolicLink_LinkTarget_PathToTarget_Data
{
get
{
foreach (string path in PathToTargetData.Union(PathToTargetUncData))
{
yield return new object[] { path };
}
}
}

public static IEnumerable<object[]> SymbolicLink_ResolveLinkTarget_PathToTarget_Data
{
get
{
foreach (string path in PathToTargetData.Union(PathToTargetUncData))
{
yield return new object[] { path, false };
yield return new object[] { path, true };
}
}
}

// Junctions doesn't support remote shares.
public static IEnumerable<object[]> Junction_LinkTarget_PathToTarget_Data
{
get
{
foreach (string path in PathToTargetData)
{
yield return new object[] { path };
}
}
}

public static IEnumerable<object[]> Junction_ResolveLinkTarget_PathToTarget_Data
{
get
{
foreach (string path in PathToTargetData)
{
yield return new object[] { path, false };
yield return new object[] { path, true };
}
}
}

internal static IEnumerable<string> PathToTargetData
{
get
{
if (OperatingSystem.IsWindows())
{
//Non-rooted relative
yield return "foo";
yield return @".\foo";
yield return @"..\foo";
// Rooted relative
yield return @"\foo";
// Rooted absolute
yield return Path.Combine(Path.GetTempPath(), "foo");
// Extended DOS
yield return Path.Combine(@"\\?\", Path.GetTempPath(), "foo");
}
else
{
//Non-rooted relative
yield return "foo";
yield return "./foo";
yield return "../foo";
// Rooted relative
yield return "/foo";
// Rooted absolute
Path.Combine(Path.GetTempPath(), "foo");
}
}
}

internal static IEnumerable<string> PathToTargetUncData
{
get
{
if (OperatingSystem.IsWindows())
{
// UNC/Remote Share
yield return @"\\LOCALHOST\share\path";
}
}
}
}
}
42 changes: 39 additions & 3 deletions src/libraries/System.IO.FileSystem/tests/Junctions.Windows.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
// 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)
private DirectoryInfo CreateJunction(string junctionPath, string targetPath)
{
Assert.True(MountHelper.CreateJunction(junctionPath, targetPath));
DirectoryInfo junctionInfo = new(junctionPath);
Expand Down Expand Up @@ -67,5 +65,43 @@ public void Junction_ResolveLinkTarget_WithIndirection(bool returnFinalTarget)
Assert.Equal(expectedTargetPath, targetFromDirectoryInfo.FullName);
Assert.Equal(expectedTargetPath, targetFromDirectory.FullName);
}

[Theory]
[MemberData(nameof(Junction_ResolveLinkTarget_PathToTarget_Data))]
public void Junction_ResolveLinkTarget_Succeeds(string pathToTarget, bool returnFinalTarget)
{
string linkPath = GetRandomLinkPath();
FileSystemInfo linkInfo = CreateJunction(linkPath, pathToTarget);

// Junctions are always created with absolute targets, even if a relative path is passed.
string expectedTarget = Path.GetFullPath(pathToTarget);

Assert.True(linkInfo.Exists);
Assert.IsType<DirectoryInfo>(linkInfo);
Assert.True(linkInfo.Attributes.HasFlag(FileAttributes.Directory));
Assert.Equal(expectedTarget, linkInfo.LinkTarget);

FileSystemInfo? targetFromDirectoryInfo = linkInfo.ResolveLinkTarget(returnFinalTarget);
FileSystemInfo? targetFromDirectory = Directory.ResolveLinkTarget(linkPath, returnFinalTarget);

Assert.NotNull(targetFromDirectoryInfo);
Assert.NotNull(targetFromDirectory);

Assert.False(targetFromDirectoryInfo.Exists);
Assert.False(targetFromDirectory.Exists);


Assert.Equal(expectedTarget, targetFromDirectoryInfo.FullName);
Assert.Equal(expectedTarget, targetFromDirectory.FullName);
}

[Theory]
[MemberData(nameof(Junction_LinkTarget_PathToTarget_Data))]
public void Junction_LinkTarget_Succeeds(string pathToTarget)
{
FileSystemInfo linkInfo = CreateJunction(GetRandomLinkPath(), pathToTarget);
Assert.True(linkInfo.Exists);
Assert.Equal(Path.GetFullPath(pathToTarget), linkInfo.LinkTarget);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2659,9 +2659,6 @@
<data name="IO_BindHandleFailed" xml:space="preserve">
<value>BindHandle for ThreadPool failed on this handle.</value>
</data>
<data name="IO_InconsistentLinkType" xml:space="preserve">
<value>The link's file system entry type is inconsistent with that of its target: {0}</value>
</data>
<data name="IO_FileExists_Name" xml:space="preserve">
<value>The file '{0}' already exists.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -571,16 +571,6 @@ public static string[] GetLogicalDrives()
internal static void CreateSymbolicLink(string path, string pathToTarget, bool isDirectory)
{
string pathToTargetFullPath = PathInternal.GetLinkTargetFullPath(path, pathToTarget);

// Fail if the target exists but is not consistent with the expected filesystem entry type
if (Interop.Sys.Stat(pathToTargetFullPath, out Interop.Sys.FileStatus targetInfo) == 0)
{
if (isDirectory != ((targetInfo.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR))
{
throw new IOException(SR.Format(SR.IO_InconsistentLinkType, path));
}
}

Interop.CheckIo(Interop.Sys.SymLink(pathToTarget, path), path, isDirectory);
}

Expand Down
Loading