From b63c7b7bde76dd5abd245a083fe83a8bd521a57b Mon Sep 17 00:00:00 2001
From: carlossanlop <1175054+carlossanlop@users.noreply.github.com>
Date: Tue, 1 Jun 2021 16:49:37 -0700
Subject: [PATCH 01/50] ref declarations and src empty definitions
---
.../src/System/IO/Directory.cs | 22 +++++++++++++++++++
.../src/System/IO/File.cs | 22 +++++++++++++++++++
.../src/System/IO/FileSystemInfo.cs | 19 ++++++++++++++++
.../System.Runtime/ref/System.Runtime.cs | 7 ++++++
4 files changed, 70 insertions(+)
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs
index 3fbf585db5de12..4fb023a21c3e58 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs
@@ -48,6 +48,17 @@ public static DirectoryInfo CreateDirectory(string path)
return new DirectoryInfo(path, fullPath, isNormalized: true);
}
+ ///
+ /// Creates a directory symbolic link identified by that points to .
+ ///
+ /// The location of the directory symbolic link.
+ /// The target of the directory symbolic link.
+ /// A instance that wraps the newly created directory symbolic link.
+ public static FileSystemInfo CreateSymbolicLink(string path, string pathToTarget)
+ {
+ return new DirectoryInfo("");
+ }
+
// Tests if the given path refers to an existing DirectoryInfo on disk.
public static bool Exists([NotNullWhen(true)] string? path)
{
@@ -315,5 +326,16 @@ public static string[] GetLogicalDrives()
{
return FileSystem.GetLogicalDrives();
}
+
+ ///
+ /// Gets the target of the specified directory symbolic link.
+ ///
+ /// The path of the directory symbolic link.
+ /// to follow links to the final target; to return the immediate next link.
+ /// A instance if the symbolic link exists, independently if the target exists or not. if the symbolic link does not exist.
+ public static System.IO.FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false)
+ {
+ return null;
+ }
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs
index 626dc761e807b7..021c907e693b8b 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs
@@ -99,6 +99,17 @@ public static FileStream Create(string path, int bufferSize)
public static FileStream Create(string path, int bufferSize, FileOptions options)
=> new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None, bufferSize, options);
+ ///
+ /// Creates a file symbolic link identified by that points to .
+ ///
+ /// The location of the file symbolic link.
+ /// The target of the file symbolic link.
+ /// A instance that wraps the newly created file symbolic link.
+ public static FileSystemInfo CreateSymbolicLink(string path, string pathToTarget)
+ {
+ return new FileInfo("");
+ }
+
// Deletes a file. The file specified by the designated path is deleted.
// If the file does not exist, Delete succeeds without throwing
// an exception.
@@ -1007,5 +1018,16 @@ private static async Task InternalWriteAllTextAsync(StreamWriter sw, string cont
? Task.FromCanceled(cancellationToken)
: InternalWriteAllLinesAsync(AsyncStreamWriter(path, encoding, append: true), contents, cancellationToken);
}
+
+ ///
+ /// Gets the target of the specified file symbolic link.
+ ///
+ /// The path of the file symbolic link.
+ /// to follow links to the final target; to return the immediate next link.
+ /// A instance if the symbolic link exists, independently if the target exists or not. if the symbolic link does not exist.
+ public static System.IO.FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false)
+ {
+ return null;
+ }
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs
index 98bf36b6fd0d6c..e389832b090926 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs
@@ -107,6 +107,25 @@ public DateTime LastWriteTimeUtc
set => LastWriteTimeCore = File.GetUtcDateTimeOffset(value);
}
+ ///
+ /// If this instance represents a link, returns the link target's path.
+ /// If the link does not exist, returns .
+ ///
+ public string? LinkTarget { get { return null; } }
+
+ ///
+ /// Creates a symbolic link located in that points to the specified .
+ ///
+ /// The path of the symbolic link target.
+ public void CreateAsSymbolicLink(string pathToTarget) { }
+
+ ///
+ /// Gets the target of the specified symbolic link.
+ ///
+ /// to follow links to the final target; to return the immediate next link.
+ /// A instance if the symbolic link exists, independently if the target exists or not. if the symbolic link does not exist.
+ public System.IO.FileSystemInfo? ResolveLinkTarget(bool returnFinalTarget = false) { return null; }
+
///
/// Returns the original path. Use FullName or Name properties for the full path or file/directory name.
///
diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs
index 47e45170b40c9f..166acefa454f22 100644
--- a/src/libraries/System.Runtime/ref/System.Runtime.cs
+++ b/src/libraries/System.Runtime/ref/System.Runtime.cs
@@ -7412,6 +7412,7 @@ public override void WriteByte(byte value) { }
public static partial class Directory
{
public static System.IO.DirectoryInfo CreateDirectory(string path) { throw null; }
+ public static System.IO.FileSystemInfo CreateSymbolicLink(string path, string pathToTarget) { throw null; }
public static void Delete(string path) { }
public static void Delete(string path, bool recursive) { }
public static System.Collections.Generic.IEnumerable EnumerateDirectories(string path) { throw null; }
@@ -7450,6 +7451,7 @@ public static void Delete(string path, bool recursive) { }
public static string[] GetLogicalDrives() { throw null; }
public static System.IO.DirectoryInfo? GetParent(string path) { throw null; }
public static void Move(string sourceDirName, string destDirName) { }
+ public static System.IO.FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) { throw null; }
public static void SetCreationTime(string path, System.DateTime creationTime) { }
public static void SetCreationTimeUtc(string path, System.DateTime creationTimeUtc) { }
public static void SetCurrentDirectory(string path) { }
@@ -7474,10 +7476,13 @@ protected FileSystemInfo(System.Runtime.Serialization.SerializationInfo info, Sy
public System.DateTime LastAccessTimeUtc { get { throw null; } set { } }
public System.DateTime LastWriteTime { get { throw null; } set { } }
public System.DateTime LastWriteTimeUtc { get { throw null; } set { } }
+ public string? LinkTarget { get { throw null; } }
public abstract string Name { get; }
+ public void CreateAsSymbolicLink(string pathToTarget) { }
public abstract void Delete();
public virtual void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { }
public void Refresh() { }
+ public System.IO.FileSystemInfo? ResolveLinkTarget(bool returnFinalTarget = false) { throw null; }
public override string ToString() { throw null; }
}
public sealed partial class DirectoryInfo : System.IO.FileSystemInfo
@@ -7562,6 +7567,7 @@ public static void Copy(string sourceFileName, string destFileName, bool overwri
public static System.IO.FileStream Create(string path) { throw null; }
public static System.IO.FileStream Create(string path, int bufferSize) { throw null; }
public static System.IO.FileStream Create(string path, int bufferSize, System.IO.FileOptions options) { throw null; }
+ public static System.IO.FileSystemInfo CreateSymbolicLink(string path, string pathToTarget) { throw null; }
public static System.IO.StreamWriter CreateText(string path) { throw null; }
[System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
public static void Decrypt(string path) { }
@@ -7600,6 +7606,7 @@ public static void Move(string sourceFileName, string destFileName, bool overwri
public static System.Collections.Generic.IEnumerable ReadLines(string path, System.Text.Encoding encoding) { throw null; }
public static void Replace(string sourceFileName, string destinationFileName, string? destinationBackupFileName) { }
public static void Replace(string sourceFileName, string destinationFileName, string? destinationBackupFileName, bool ignoreMetadataErrors) { }
+ public static System.IO.FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) { throw null; }
public static void SetAttributes(string path, System.IO.FileAttributes fileAttributes) { }
public static void SetCreationTime(string path, System.DateTime creationTime) { }
public static void SetCreationTimeUtc(string path, System.DateTime creationTimeUtc) { }
From b2233043ba2e74b4ac3f4cc131a73c5bd320e0c2 Mon Sep 17 00:00:00 2001
From: carlossanlop <1175054+carlossanlop@users.noreply.github.com>
Date: Tue, 1 Jun 2021 16:49:53 -0700
Subject: [PATCH 02/50] Implementation for Unix symlinks APIs and
cross-platform unit tests.
---
.../Unix/System.Native/Interop.SymLink.cs | 14 +
.../Native/Unix/System.Native/entrypoints.c | 1 +
.../Native/Unix/System.Native/pal_io.c | 7 +
.../Native/Unix/System.Native/pal_io.h | 9 +-
.../tests/Base/BaseSymbolicLinks.cs | 18 -
.../BaseSymbolicLinks.FileSystem.cs | 323 ++++++++++++++++++
.../BaseSymbolicLinks.FileSystemInfo.cs | 48 +++
.../Base/SymbolicLinks/BaseSymbolicLinks.cs | 30 ++
.../tests/Directory/SymbolicLinks.cs | 50 ++-
.../tests/DirectoryInfo/SymbolicLinks.cs | 47 ++-
.../tests/File/SymbolicLinks.cs | 48 +++
.../tests/FileInfo/SymbolicLinks.cs | 44 +++
.../tests/System.IO.FileSystem.Tests.csproj | 6 +-
.../src/Resources/Strings.resx | 6 +
.../System.Private.CoreLib.Shared.projitems | 4 +
.../src/System/IO/Directory.cs | 42 ++-
.../src/System/IO/File.cs | 42 ++-
.../src/System/IO/FileSystem.Unix.cs | 104 ++++++
.../src/System/IO/FileSystem.Windows.cs | 14 +
.../src/System/IO/FileSystem.cs | 28 ++
.../src/System/IO/FileSystemInfo.Unix.cs | 2 +-
.../src/System/IO/FileSystemInfo.Windows.cs | 5 +-
.../src/System/IO/FileSystemInfo.cs | 35 +-
23 files changed, 852 insertions(+), 75 deletions(-)
create mode 100644 src/libraries/Common/src/Interop/Unix/System.Native/Interop.SymLink.cs
delete mode 100644 src/libraries/System.IO.FileSystem/tests/Base/BaseSymbolicLinks.cs
create mode 100644 src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystem.cs
create mode 100644 src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystemInfo.cs
create mode 100644 src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs
create mode 100644 src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs
create mode 100644 src/libraries/System.IO.FileSystem/tests/FileInfo/SymbolicLinks.cs
create mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs
diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SymLink.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SymLink.cs
new file mode 100644
index 00000000000000..922ecd5bc66255
--- /dev/null
+++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.SymLink.cs
@@ -0,0 +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;
+using System.Runtime.InteropServices;
+
+internal static partial class Interop
+{
+ internal static partial class Sys
+ {
+ [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_SymLink", SetLastError = true)]
+ internal static extern int SymLink(string target, string linkPath);
+ }
+}
diff --git a/src/libraries/Native/Unix/System.Native/entrypoints.c b/src/libraries/Native/Unix/System.Native/entrypoints.c
index cf6485ecf8430a..4de1acc17b0570 100644
--- a/src/libraries/Native/Unix/System.Native/entrypoints.c
+++ b/src/libraries/Native/Unix/System.Native/entrypoints.c
@@ -81,6 +81,7 @@ static const Entry s_sysNative[] =
DllImportEntry(SystemNative_Access)
DllImportEntry(SystemNative_LSeek)
DllImportEntry(SystemNative_Link)
+ DllImportEntry(SystemNative_SymLink)
DllImportEntry(SystemNative_MksTemps)
DllImportEntry(SystemNative_MMap)
DllImportEntry(SystemNative_MUnmap)
diff --git a/src/libraries/Native/Unix/System.Native/pal_io.c b/src/libraries/Native/Unix/System.Native/pal_io.c
index d7eb6c4ab23aca..9e02aa7c2e65d4 100644
--- a/src/libraries/Native/Unix/System.Native/pal_io.c
+++ b/src/libraries/Native/Unix/System.Native/pal_io.c
@@ -715,6 +715,13 @@ int32_t SystemNative_Link(const char* source, const char* linkTarget)
return result;
}
+int32_t SystemNative_SymLink(const char* target, const char* linkPath)
+{
+ int32_t result;
+ while ((result = symlink(target, linkPath)) < 0 && errno == EINTR);
+ return result;
+}
+
intptr_t SystemNative_MksTemps(char* pathTemplate, int32_t suffixLength)
{
intptr_t result;
diff --git a/src/libraries/Native/Unix/System.Native/pal_io.h b/src/libraries/Native/Unix/System.Native/pal_io.h
index 1dc70387ad5eac..6461881a91eb1c 100644
--- a/src/libraries/Native/Unix/System.Native/pal_io.h
+++ b/src/libraries/Native/Unix/System.Native/pal_io.h
@@ -527,12 +527,19 @@ PALEXPORT int32_t SystemNative_Access(const char* path, int32_t mode);
PALEXPORT int64_t SystemNative_LSeek(intptr_t fd, int64_t offset, int32_t whence);
/**
- * Creates a hard-link at link pointing to source.
+ * Creates a hard-link at linkTarget pointing to source.
*
* Returns 0 on success; otherwise, returns -1 and errno is set.
*/
PALEXPORT int32_t SystemNative_Link(const char* source, const char* linkTarget);
+/**
+ * Creates a symbolic link at linkPath pointing to target.
+ *
+ * Returns 0 on success; otherwise, returns -1 and errno is set.
+ */
+PALEXPORT int32_t SystemNative_SymLink(const char* target, const char* linkPath);
+
/**
* Creates a file name that adheres to the specified template, creates the file on disk with
* 0600 permissions, and returns an open r/w File Descriptor on the file.
diff --git a/src/libraries/System.IO.FileSystem/tests/Base/BaseSymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/Base/BaseSymbolicLinks.cs
deleted file mode 100644
index caa5ae9c3e11d5..00000000000000
--- a/src/libraries/System.IO.FileSystem/tests/Base/BaseSymbolicLinks.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-// 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
-{
- public abstract class BaseSymbolicLinks : FileSystemTest
- {
- protected DirectoryInfo CreateDirectoryContainingSelfReferencingSymbolicLink()
- {
- DirectoryInfo testDirectory = Directory.CreateDirectory(GetTestFilePath());
- string pathToLink = Path.Join(testDirectory.FullName, GetTestFileName());
- Assert.True(MountHelper.CreateSymbolicLink(pathToLink, pathToLink, isDirectory: true)); // Create a symlink cycle
- return testDirectory;
- }
- }
-}
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
new file mode 100644
index 00000000000000..d0be3a3fab7410
--- /dev/null
+++ b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystem.cs
@@ -0,0 +1,323 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using Xunit;
+
+namespace System.IO.Tests
+{
+ // Contains test methods that can be used for FileInfo, DirectoryInfo, File or Directory.
+ public abstract class BaseSymbolicLinks_FileSystem : BaseSymbolicLinks
+ {
+ /// Creates a new file or directory depending on the implementing class.
+ /// If createOpposite is true, creates a directory if the implementing class is for File or FileInfo, or
+ /// creates a file if the implementing class is for Directory or DirectoryInfo.
+ protected abstract void CreateFileOrDirectory(string path, bool createOpposite = false);
+ protected abstract void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo fsi);
+ protected abstract void AssertLinkExists(FileSystemInfo link);
+ protected abstract void AssertExistsWhenNoTarget(FileSystemInfo link);
+ /// Calls the actual public API for creating a symbolic link.
+ protected abstract FileSystemInfo CreateSymbolicLink(string path, string pathToTarget);
+ /// Calls the actual public API for resolving the symbolic link target.
+ protected abstract FileSystemInfo ResolveLinkTarget(string linkPath, string? expectedLinkTarget, bool returnFinalTarget = false);
+
+ [Fact]
+ public void CreateSymbolicLink_NullPathToTarget()
+ {
+ Assert.Throws(() => CreateSymbolicLink(GetRandomFilePath(), pathToTarget: null));
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData("\0")]
+ public void CreateSymbolicLink_InvalidPathToTarget(string pathToTarget)
+ {
+ Assert.Throws(() => CreateSymbolicLink(GetRandomFilePath(), pathToTarget));
+ }
+
+ [Fact]
+ public void CreateSymbolicLink_RelativeTargetPath_TargetExists()
+ {
+ // /path/to/link -> /path/to/existingtarget
+
+ string linkPath = GetRandomLinkPath();
+ string existentTarget = GetRandomFileName();
+ string targetPath = Path.Join(Path.GetDirectoryName(linkPath), existentTarget);
+ VerifySymbolicLinkAndResolvedTarget(
+ linkPath: linkPath,
+ expectedLinkTarget: existentTarget,
+ targetPath: targetPath);
+ }
+
+ [Fact]
+ public void CreateSymbolicLink_RelativeTargetPath_TargetExists_WithRedundantSegments()
+ {
+ // /path/to/link -> /path/to/../to/existingtarget
+
+ string linkPath = GetRandomLinkPath();
+ string fileName = GetRandomFileName();
+ string dirPath = Path.GetDirectoryName(linkPath);
+ string dirName = Path.GetFileName(dirPath);
+ string targetPath = Path.Join(dirPath, fileName);
+ string existentTarget = Path.Join("..", dirName, fileName);
+ VerifySymbolicLinkAndResolvedTarget(
+ linkPath: linkPath,
+ expectedLinkTarget: existentTarget,
+ targetPath: targetPath);
+ }
+
+ [Fact]
+ public void CreateSymbolicLink_AbsoluteTargetPath_TargetExists()
+ {
+ // /path/to/link -> /path/to/existingtarget
+
+ string linkPath = GetRandomLinkPath();
+ string targetPath = GetRandomFilePath();
+ VerifySymbolicLinkAndResolvedTarget(
+ linkPath: linkPath,
+ expectedLinkTarget: targetPath,
+ targetPath: targetPath);
+ }
+
+ [Fact]
+ public void CreateSymbolicLink_AbsoluteTargetPath_TargetExists_WithRedundantSegments()
+ {
+ // /path/to/link -> /path/to/../to/existingtarget
+
+ string linkPath = GetRandomLinkPath();
+ string fileName = GetRandomFileName();
+ string dirPath = Path.GetDirectoryName(linkPath);
+ string dirName = Path.GetFileName(dirPath);
+ string targetPath = Path.Join(dirPath, fileName);
+ string existentTarget = Path.Join(dirPath, "..", dirName, fileName);
+ VerifySymbolicLinkAndResolvedTarget(
+ linkPath: linkPath,
+ expectedLinkTarget: existentTarget,
+ targetPath: targetPath);
+ }
+
+ [Fact]
+ public void CreateSymbolicLink_RelativeTargetPath_NonExistentTarget()
+ {
+ // /path/to/link -> /path/to/nonexistenttarget
+
+ string linkPath = GetRandomLinkPath();
+ string nonExistentTarget = GetRandomFileName();
+ VerifySymbolicLinkAndResolvedTarget(
+ linkPath: linkPath,
+ expectedLinkTarget: nonExistentTarget,
+ targetPath: null); // do not create target
+ }
+
+ [Fact]
+ public void CreateSymbolicLink_AbsoluteTargetPath_NonExistentTarget()
+ {
+ // /path/to/link -> /path/to/nonexistenttarget
+
+ string linkPath = GetRandomLinkPath();
+ string nonExistentTarget = GetRandomFilePath();
+ VerifySymbolicLinkAndResolvedTarget(
+ linkPath: linkPath,
+ expectedLinkTarget: nonExistentTarget,
+ targetPath: null); // do not create target
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void ResolveLinkTarget_FileSystemEntryExistsButIsNotALink(bool returnFinalTarget)
+ {
+ string path = GetRandomFilePath();
+ CreateFileOrDirectory(path); // entry exists as a normal file, not as a link
+
+ FileSystemInfo target = ResolveLinkTarget(path, expectedLinkTarget: null, returnFinalTarget);
+ Assert.Null(target);
+ }
+
+ [Fact]
+ public void ResolveLinkTarget_ReturnFinalTarget_Absolute()
+ {
+ string link1Path = GetRandomLinkPath();
+ string link2Path = GetRandomLinkPath();
+ string filePath = GetRandomFilePath();
+
+ ResolveLinkTarget_ReturnFinalTarget(
+ link1Path: link1Path,
+ expectedLink1Target: link2Path,
+ link2Path: link2Path,
+ expectedLink2Target: filePath,
+ filePath: filePath);
+ }
+
+ [Fact]
+ public void ResolveLinkTarget_ReturnFinalTarget_Absolute_WithRedundantSegments()
+ {
+ string link1Path = GetRandomLinkPath();
+ string link2Path = GetRandomLinkPath();
+ string filePath = GetRandomFilePath();
+
+ string dirPath = Path.GetDirectoryName(filePath);
+ string dirName = Path.GetFileName(dirPath);
+
+ string link2FileName = Path.GetFileName(link2Path);
+ string fileName = Path.GetFileName(filePath);
+
+ ResolveLinkTarget_ReturnFinalTarget(
+ link1Path: link1Path,
+ expectedLink1Target: Path.Join(dirPath, "..", dirName, link2FileName),
+ link2Path: link2Path,
+ expectedLink2Target: Path.Join(dirPath, "..", dirName, fileName),
+ filePath: filePath);
+ }
+
+ [Fact]
+ public void ResolveLinkTarget_ReturnFinalTarget_Relative()
+ {
+ string link1Path = GetRandomLinkPath();
+ string link2Path = GetRandomLinkPath();
+ string filePath = GetRandomFilePath();
+
+ string link2FileName = Path.GetFileName(link2Path);
+ string fileName = Path.GetFileName(filePath);
+
+ ResolveLinkTarget_ReturnFinalTarget(
+ link1Path: link1Path,
+ expectedLink1Target: link2FileName,
+ link2Path: link2Path,
+ expectedLink2Target: fileName,
+ filePath: filePath);
+ }
+
+ [Fact]
+ public void ResolveLinkTarget_ReturnFinalTarget_Relative_WithRedundantSegments()
+ {
+ string link1Path = GetRandomLinkPath();
+ string link2Path = GetRandomLinkPath();
+ string filePath = GetRandomFilePath();
+
+ string dirPath = Path.GetDirectoryName(filePath);
+ string dirName = Path.GetFileName(dirPath);
+
+ string link2FileName = Path.GetFileName(link2Path);
+ string fileName = Path.GetFileName(filePath);
+
+ ResolveLinkTarget_ReturnFinalTarget(
+ link1Path: link1Path,
+ expectedLink1Target: Path.Join("..", dirName, link2FileName),
+ link2Path: link2Path,
+ expectedLink2Target: Path.Join("..", dirName, fileName),
+ filePath: filePath);
+ }
+
+ [Fact]
+ public void DetectSymbolicLinkCycle()
+ {
+ // link1 -> link2
+ // ^ /
+ // \______/
+
+ string link2Path = GetRandomFilePath();
+ string link1Path = GetRandomFilePath();
+
+ FileSystemInfo link1Info = CreateSymbolicLink(link1Path, link2Path);
+ FileSystemInfo link2Info = CreateSymbolicLink(link2Path, link1Path);
+
+ // Can get targets without following symlinks
+ FileSystemInfo link1Target = ResolveLinkTarget(link1Path, expectedLinkTarget: link2Path);
+ FileSystemInfo link2Target = ResolveLinkTarget(link2Path, expectedLinkTarget: link1Path);
+
+ // Cannot get target when following symlinks
+ Assert.Throws(() => ResolveLinkTarget(link1Path, expectedLinkTarget: link2Path, returnFinalTarget: true));
+ Assert.Throws(() => ResolveLinkTarget(link2Path, expectedLinkTarget: link1Path, returnFinalTarget: true));
+ }
+
+ [Fact]
+ public void CreateSymbolicLink_WrongTargetType()
+ {
+ // dirLink -> file
+ // fileLink -> dir
+
+ string targetPath = GetRandomFilePath();
+ CreateFileOrDirectory(targetPath, createOpposite: true); // The underlying file system entry needs to be different
+ Assert.Throws(() => CreateSymbolicLink(GetRandomFilePath(), targetPath));
+ }
+
+ protected void ResolveLinkTarget_LinkDoesNotExist_Internal() where T : Exception
+ {
+ // ? -> ?
+
+ string path = GetRandomFilePath();
+ Assert.Throws(() => ResolveLinkTarget(path, expectedLinkTarget: null));
+ Assert.Throws(() => ResolveLinkTarget(path, expectedLinkTarget: null, returnFinalTarget: true));
+ }
+
+ private void VerifySymbolicLinkAndResolvedTarget(string linkPath, string expectedLinkTarget, string targetPath = null)
+ {
+ // linkPath -> expectedLinkTarget (created in targetPath if not null)
+
+ if (targetPath != null)
+ {
+ CreateFileOrDirectory(targetPath);
+ }
+
+ FileSystemInfo link = CreateSymbolicLink(linkPath, expectedLinkTarget);
+ if (targetPath == null)
+ {
+ // Behavior different between files and directories when target does not exist
+ AssertExistsWhenNoTarget(link);
+ }
+ else
+ {
+ Assert.True(link.Exists); // The target file or directory was created above, so we report Exists of the target for both
+ }
+
+ FileSystemInfo target = ResolveLinkTarget(linkPath, expectedLinkTarget);
+ AssertIsCorrectTypeAndDirectoryAttribute(target);
+ Assert.True(Path.IsPathFullyQualified(target.FullName));
+ }
+
+ private void ResolveLinkTarget_ReturnFinalTarget(string link1Path, string expectedLink1Target, string link2Path, string expectedLink2Target, string filePath)
+ {
+ // link1Path -> expectedLink1Target (created in link2Path) -> expectedLink2Target (created in filePath)
+
+ CreateFileOrDirectory(filePath);
+
+ // link2 to target
+ FileSystemInfo link2 = CreateSymbolicLink(link2Path, expectedLink2Target);
+ Assert.True(link2.Exists);
+ Assert.True(link2.Attributes.HasFlag(FileAttributes.ReparsePoint));
+ AssertIsCorrectTypeAndDirectoryAttribute(link2);
+ Assert.Equal(link2.LinkTarget, expectedLink2Target);
+
+ // link1 to link2
+ FileSystemInfo link1 = CreateSymbolicLink(link1Path, expectedLink1Target);
+ Assert.True(link1.Exists);
+ Assert.True(link1.Attributes.HasFlag(FileAttributes.ReparsePoint));
+ AssertIsCorrectTypeAndDirectoryAttribute(link1);
+ Assert.Equal(link1.LinkTarget, expectedLink1Target);
+
+ // link1: do not follow symlinks
+ FileSystemInfo link1Target = ResolveLinkTarget(link1Path, expectedLink1Target);
+ Assert.True(link1Target.Exists);
+ AssertIsCorrectTypeAndDirectoryAttribute(link1Target);
+ Assert.True(link1Target.Attributes.HasFlag(FileAttributes.ReparsePoint));
+ Assert.Equal(link1Target.FullName, link2Path);
+ Assert.Equal(link1Target.LinkTarget, expectedLink2Target);
+
+ // link2: do not follow symlinks
+ FileSystemInfo link2Target = ResolveLinkTarget(link2Path, expectedLink2Target);
+ Assert.True(link2Target.Exists);
+ AssertIsCorrectTypeAndDirectoryAttribute(link2Target);
+ Assert.False(link2Target.Attributes.HasFlag(FileAttributes.ReparsePoint));
+ Assert.Equal(link2Target.FullName, filePath);
+ Assert.Null(link2Target.LinkTarget);
+
+ // link1: follow symlinks
+ FileSystemInfo finalTarget = ResolveLinkTarget(link1Path, expectedLinkTarget: expectedLink1Target, returnFinalTarget: true);
+ Assert.True(finalTarget.Exists);
+ AssertIsCorrectTypeAndDirectoryAttribute(finalTarget);
+ Assert.False(finalTarget.Attributes.HasFlag(FileAttributes.ReparsePoint));
+ Assert.Equal(finalTarget.FullName, filePath);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystemInfo.cs b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystemInfo.cs
new file mode 100644
index 00000000000000..9a56e0c04ef203
--- /dev/null
+++ b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystemInfo.cs
@@ -0,0 +1,48 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using Xunit;
+
+namespace System.IO.Tests
+{
+ // Contains test methods that can be used for FileInfo and DirectoryInfo.
+ public abstract class BaseSymbolicLinks_FileSystemInfo : BaseSymbolicLinks_FileSystem
+ {
+ // Creates and returns FileSystemInfo instance by calling either the DirectoryInfo or FileInfo constructor and passing the path.
+ protected abstract FileSystemInfo GetFileSystemInfo(string path);
+
+ protected override FileSystemInfo CreateSymbolicLink(string path, string pathToTarget)
+ {
+ FileSystemInfo link = GetFileSystemInfo(path);
+ link.CreateAsSymbolicLink(pathToTarget);
+ return link;
+ }
+
+ protected override FileSystemInfo ResolveLinkTarget(string linkPath, string? expectedLinkTarget, bool returnFinalTarget = false)
+ {
+ FileSystemInfo link = GetFileSystemInfo(linkPath);
+
+ if (expectedLinkTarget == null)
+ {
+ // LinkTarget is null when linkPath does not exist or is not a link
+ Assert.Null(link.LinkTarget);
+ }
+ else
+ {
+ Assert.Equal(link.LinkTarget, expectedLinkTarget);
+ }
+
+ FileSystemInfo? target = link.ResolveLinkTarget(returnFinalTarget);
+
+ // When the resolved target is the immediate next, and it does not exist,
+ // verify that the link's LinkTarget returns null
+ if (!returnFinalTarget && target == null)
+ {
+ Assert.Null(link.LinkTarget);
+ }
+
+ return target;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs
new file mode 100644
index 00000000000000..a65fe5f2e5c5a2
--- /dev/null
+++ b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs
@@ -0,0 +1,30 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using Xunit;
+
+namespace System.IO.Tests
+{
+ [ConditionalClass(typeof(BaseSymbolicLinks), nameof(CanCreateSymbolicLinks))]
+ // Contains helper methods that are shared by all symbolic link test classes.
+ public abstract class BaseSymbolicLinks : FileSystemTest
+ {
+ protected DirectoryInfo CreateDirectoryContainingSelfReferencingSymbolicLink()
+ {
+ DirectoryInfo testDirectory = Directory.CreateDirectory(GetTestFilePath());
+ string pathToLink = Path.Join(testDirectory.FullName, GetTestFileName());
+ Assert.True(MountHelper.CreateSymbolicLink(pathToLink, pathToLink, isDirectory: true)); // Create a symlink cycle
+ return testDirectory;
+ }
+
+ protected string GetRandomFileName() => GetTestFileName() + ".txt";
+ protected string GetRandomLinkName() => GetTestFileName() + ".link";
+ protected string GetRandomDirName() => GetTestFileName() + "_dir";
+
+ protected string GetRandomFilePath() => Path.Join(TestDirectory, GetRandomFileName());
+ protected string GetRandomLinkPath() => Path.Join(TestDirectory, GetRandomLinkName());
+ protected string GetRandomDirPath() => Path.Join(TestDirectory, GetRandomDirName());
+
+ }
+}
diff --git a/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs
index 970c784c7f8198..49606897a1b0d8 100644
--- a/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs
+++ b/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs
@@ -1,15 +1,51 @@
// 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
{
- public class Directory_SymbolicLinks : BaseSymbolicLinks
+ public class Directory_SymbolicLinks : BaseSymbolicLinks_FileSystem
{
- [ConditionalFact(nameof(CanCreateSymbolicLinks))]
+ protected override void CreateFileOrDirectory(string path, bool createOpposite = false)
+ {
+ if (!createOpposite)
+ {
+ Directory.CreateDirectory(path);
+ }
+ else
+ {
+ File.Create(path).Dispose();
+ }
+ }
+
+ protected override FileSystemInfo CreateSymbolicLink(string path, string pathToTarget) =>
+ Directory.CreateSymbolicLink(path, pathToTarget);
+
+ protected override FileSystemInfo ResolveLinkTarget(string linkPath, string? expectedLinkTarget, bool returnFinalTarget = false) =>
+ Directory.ResolveLinkTarget(linkPath, returnFinalTarget);
+
+ protected override void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo fsi)
+ {
+ if (fsi.Exists)
+ {
+ Assert.True(fsi.Attributes.HasFlag(FileAttributes.Directory));
+ }
+ Assert.True(fsi is DirectoryInfo);
+ }
+
+ protected override void AssertLinkExists(FileSystemInfo link) =>
+ Assert.False(link.Exists); // For directory symlinks, we return the exists info from the target
+
+ // When the directory target does not exist FileStatus.GetExists returns false because:
+ // - We check _exists (which whould be true because the link itself exists).
+ // - We check InitiallyDirectory, which is the initial expected object type (which would be true).
+ // - We check _directory (false because the target directory does not exist)
+ protected override void AssertExistsWhenNoTarget(FileSystemInfo link) =>
+ Assert.False(link.Exists);
+
+ [Fact]
public void EnumerateDirectories_LinksWithCycles_ShouldNotThrow()
{
DirectoryInfo testDirectory = CreateDirectoryContainingSelfReferencingSymbolicLink();
@@ -19,7 +55,7 @@ public void EnumerateDirectories_LinksWithCycles_ShouldNotThrow()
Assert.Equal(expected, Directory.EnumerateDirectories(testDirectory.FullName).Count());
}
- [ConditionalFact(nameof(CanCreateSymbolicLinks))]
+ [Fact]
public void EnumerateFiles_LinksWithCycles_ShouldNotThrow()
{
DirectoryInfo testDirectory = CreateDirectoryContainingSelfReferencingSymbolicLink();
@@ -29,11 +65,15 @@ public void EnumerateFiles_LinksWithCycles_ShouldNotThrow()
Assert.Equal(expected, Directory.EnumerateFiles(testDirectory.FullName).Count());
}
- [ConditionalFact(nameof(CanCreateSymbolicLinks))]
+ [Fact]
public void EnumerateFileSystemEntries_LinksWithCycles_ShouldNotThrow()
{
DirectoryInfo testDirectory = CreateDirectoryContainingSelfReferencingSymbolicLink();
Assert.Single(Directory.EnumerateFileSystemEntries(testDirectory.FullName));
}
+
+ [Fact]
+ public void ResolveLinkTarget_LinkDoesNotExist() =>
+ ResolveLinkTarget_LinkDoesNotExist_Internal();
}
}
diff --git a/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs
index 5dedc0b7b45285..ec99f4d224c403 100644
--- a/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs
+++ b/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs
@@ -1,15 +1,48 @@
// 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
{
- public class DirectoryInfo_SymbolicLinks : BaseSymbolicLinks
+ public class DirectoryInfo_SymbolicLinks : BaseSymbolicLinks_FileSystemInfo
{
- [ConditionalTheory(nameof(CanCreateSymbolicLinks))]
+ protected override FileSystemInfo GetFileSystemInfo(string path) =>
+ new DirectoryInfo(path);
+
+ protected override void CreateFileOrDirectory(string path, bool createOpposite = false)
+ {
+ if (!createOpposite)
+ {
+ Directory.CreateDirectory(path);
+ }
+ else
+ {
+ File.Create(path).Dispose();
+ }
+ }
+
+ protected override void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo fsi)
+ {
+ if (fsi.Exists)
+ {
+ Assert.True(fsi.Attributes.HasFlag(FileAttributes.Directory));
+ }
+ Assert.True(fsi is DirectoryInfo);
+ }
+
+ protected override void AssertLinkExists(FileSystemInfo link) =>
+ Assert.False(link.Exists); // For directory symlinks, we return the exists info from the target
+
+ // When the directory target does not exist FileStatus.GetExists returns false because:
+ // - We check _exists (which whould be true because the link itself exists).
+ // - We check InitiallyDirectory, which is the initial expected object type (which would be true).
+ // - We check _directory (false because the target directory does not exist)
+ protected override void AssertExistsWhenNoTarget(FileSystemInfo link) =>
+ Assert.False(link.Exists);
+
+ [Theory]
[InlineData(false)]
[InlineData(true)]
public void EnumerateDirectories_LinksWithCycles_ThrowsTooManyLevelsOfSymbolicLinks(bool recurse)
@@ -31,7 +64,7 @@ public void EnumerateDirectories_LinksWithCycles_ThrowsTooManyLevelsOfSymbolicLi
}
}
- [ConditionalTheory(nameof(CanCreateSymbolicLinks))]
+ [Theory]
[InlineData(false)]
[InlineData(true)]
public void EnumerateFiles_LinksWithCycles_ThrowsTooManyLevelsOfSymbolicLinks(bool recurse)
@@ -53,7 +86,7 @@ public void EnumerateFiles_LinksWithCycles_ThrowsTooManyLevelsOfSymbolicLinks(bo
}
}
- [ConditionalTheory(nameof(CanCreateSymbolicLinks))]
+ [Theory]
[InlineData(false)]
[InlineData(true)]
public void EnumerateFileSystemInfos_LinksWithCycles_ThrowsTooManyLevelsOfSymbolicLinks(bool recurse)
@@ -74,5 +107,9 @@ public void EnumerateFileSystemInfos_LinksWithCycles_ThrowsTooManyLevelsOfSymbol
Assert.Throws(() => testDirectory.GetFileSystemInfos("*", options).Count());
}
}
+
+ [Fact]
+ public void ResolveLinkTarget_LinkDoesNotExist() =>
+ ResolveLinkTarget_LinkDoesNotExist_Internal();
}
}
diff --git a/src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs
new file mode 100644
index 00000000000000..a1b275a90564a8
--- /dev/null
+++ b/src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs
@@ -0,0 +1,48 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using Xunit;
+
+namespace System.IO.Tests
+{
+ public class File_SymbolicLinks : BaseSymbolicLinks_FileSystem
+ {
+ protected override void CreateFileOrDirectory(string path, bool createOpposite = false)
+ {
+ if (!createOpposite)
+ {
+ File.Create(path).Dispose();
+ }
+ else
+ {
+ Directory.CreateDirectory(path);
+ }
+ }
+
+ protected override FileSystemInfo CreateSymbolicLink(string path, string pathToTarget) =>
+ File.CreateSymbolicLink(path, pathToTarget);
+
+ protected override FileSystemInfo ResolveLinkTarget(string linkPath, string? expectedLinkTarget, bool returnFinalTarget = false) =>
+ File.ResolveLinkTarget(linkPath, returnFinalTarget);
+
+ protected override void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo fsi)
+ {
+ if (fsi.Exists)
+ {
+ Assert.False(fsi.Attributes.HasFlag(FileAttributes.Directory));
+ }
+ Assert.True(fsi is FileInfo);
+ }
+
+ protected override void AssertLinkExists(FileSystemInfo link) =>
+ Assert.True(link.Exists); // For file symlinks, we return the exists info from the actual link, not the target
+
+ protected override void AssertExistsWhenNoTarget(FileSystemInfo link) =>
+ Assert.True(link.Exists);
+
+ [Fact]
+ public void ResolveLinkTarget_LinkDoesNotExist() =>
+ ResolveLinkTarget_LinkDoesNotExist_Internal();
+ }
+}
diff --git a/src/libraries/System.IO.FileSystem/tests/FileInfo/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/FileInfo/SymbolicLinks.cs
new file mode 100644
index 00000000000000..0218e51a30987e
--- /dev/null
+++ b/src/libraries/System.IO.FileSystem/tests/FileInfo/SymbolicLinks.cs
@@ -0,0 +1,44 @@
+// 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
+{
+ public class FileInfo_SymbolicLinks : BaseSymbolicLinks_FileSystemInfo
+ {
+ protected override FileSystemInfo GetFileSystemInfo(string path) =>
+ new FileInfo(path);
+
+ protected override void CreateFileOrDirectory(string path, bool createOpposite = false)
+ {
+ if (!createOpposite)
+ {
+ File.Create(path).Dispose();
+ }
+ else
+ {
+ Directory.CreateDirectory(path);
+ }
+ }
+
+ protected override void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo fsi)
+ {
+ if (fsi.Exists)
+ {
+ Assert.False(fsi.Attributes.HasFlag(FileAttributes.Directory));
+ }
+ Assert.True(fsi is FileInfo);
+ }
+
+ protected override void AssertLinkExists(FileSystemInfo link) =>
+ Assert.True(link.Exists); // For file symlinks, we return the exists info from the actual link, not the target
+
+ protected override void AssertExistsWhenNoTarget(FileSystemInfo link) =>
+ Assert.True(link.Exists);
+
+ [Fact]
+ public void ResolveLinkTarget_LinkDoesNotExist() =>
+ ResolveLinkTarget_LinkDoesNotExist_Internal();
+ }
+}
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 bf845ce77b57f2..2c0d97af8758b6 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
@@ -14,13 +14,16 @@
-
+
+
+
+
@@ -136,6 +139,7 @@
+
diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx
index 1669ffb9bbb593..6648c44fc7ebc4 100644
--- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx
+++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx
@@ -2335,6 +2335,9 @@
Unmanaged memory stream position was beyond the capacity of the stream.
+
+ Too many levels of symbolic links in '{0}'.
+
Insufficient available memory to meet the expected demands of an operation at this time. Please try again later.
@@ -2659,6 +2662,9 @@
BindHandle for ThreadPool failed on this handle.
+
+ The link file system type is inconsistent with the link target system type.
+
The file '{0}' already exists.
diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
index de5bc44f1ba793..4b7da59ca6c000 100644
--- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
+++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
@@ -420,6 +420,7 @@
+
@@ -2028,6 +2029,9 @@
Common\Interop\Unix\System.Native\Interop.Stat.Span.cs
+
+ Common\Interop\Unix\System.Native\Interop.SymLink.cs
+
Common\Interop\Unix\System.Native\Interop.SysConf.cs
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs
index 4fb023a21c3e58..58e145d540ecd7 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs
@@ -48,17 +48,6 @@ public static DirectoryInfo CreateDirectory(string path)
return new DirectoryInfo(path, fullPath, isNormalized: true);
}
- ///
- /// Creates a directory symbolic link identified by that points to .
- ///
- /// The location of the directory symbolic link.
- /// The target of the directory symbolic link.
- /// A instance that wraps the newly created directory symbolic link.
- public static FileSystemInfo CreateSymbolicLink(string path, string pathToTarget)
- {
- return new DirectoryInfo("");
- }
-
// Tests if the given path refers to an existing DirectoryInfo on disk.
public static bool Exists([NotNullWhen(true)] string? path)
{
@@ -328,14 +317,33 @@ public static string[] GetLogicalDrives()
}
///
- /// Gets the target of the specified directory symbolic link.
+ /// Creates a directory symbolic link identified by that points to .
///
- /// The path of the directory symbolic link.
- /// to follow links to the final target; to return the immediate next link.
- /// A instance if the symbolic link exists, independently if the target exists or not. if the symbolic link does not exist.
- public static System.IO.FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false)
+ /// The absolute path where the symbolic link should be created.
+ /// The target directory of the symbolic link.
+ /// A instance that wraps the newly created directory symbolic link.
+ /// or is .
+ /// or is empty.
+ /// -or-
+ /// is not an absolute path.
+ /// -or-
+ /// or contains invalid path characters.
+ /// A file or directory already exists in the location of .
+ /// -or-
+ /// An I/O error occurred.
+ public static FileSystemInfo CreateSymbolicLink(string path, string pathToTarget)
{
- return null;
+ FileSystem.CreateSymbolicLink(path, pathToTarget, isDirectory: true);
+ return new DirectoryInfo(path);
}
+
+ ///
+ /// Gets the target of the specified directory link.
+ ///
+ /// The path of the directory link.
+ /// to follow links to the final target; to return the immediate next link.
+ /// A instance if exists, independently if the target exists or not. if does not exist.
+ public static System.IO.FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) =>
+ FileSystem.ResolveLinkTarget(linkPath, returnFinalTarget, isDirectory: true);
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs
index 021c907e693b8b..0e81dfba1087ba 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs
@@ -99,17 +99,6 @@ public static FileStream Create(string path, int bufferSize)
public static FileStream Create(string path, int bufferSize, FileOptions options)
=> new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None, bufferSize, options);
- ///
- /// Creates a file symbolic link identified by that points to .
- ///
- /// The location of the file symbolic link.
- /// The target of the file symbolic link.
- /// A instance that wraps the newly created file symbolic link.
- public static FileSystemInfo CreateSymbolicLink(string path, string pathToTarget)
- {
- return new FileInfo("");
- }
-
// Deletes a file. The file specified by the designated path is deleted.
// If the file does not exist, Delete succeeds without throwing
// an exception.
@@ -1020,14 +1009,33 @@ private static async Task InternalWriteAllTextAsync(StreamWriter sw, string cont
}
///
- /// Gets the target of the specified file symbolic link.
+ /// Creates a file symbolic link identified by that points to .
///
- /// The path of the file symbolic link.
- /// to follow links to the final target; to return the immediate next link.
- /// A instance if the symbolic link exists, independently if the target exists or not. if the symbolic link does not exist.
- public static System.IO.FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false)
+ /// The path where the symbolic link should be created.
+ /// The path of the target to which the symbolic link points.
+ /// A instance that wraps the newly created file symbolic link.
+ /// or is .
+ /// or is empty.
+ /// -or-
+ /// is not an absolute path.
+ /// -or-
+ /// or contains invalid path characters.
+ /// A file or directory already exists in the location of .
+ /// -or-
+ /// An I/O error occurred.
+ public static FileSystemInfo CreateSymbolicLink(string path, string pathToTarget)
{
- return null;
+ FileSystem.CreateSymbolicLink(path, pathToTarget, isDirectory: false);
+ return new FileInfo(path);
}
+
+ ///
+ /// Gets the target of the specified file link.
+ ///
+ /// The path of the file link.
+ /// to follow links to the final target; to return the immediate next link.
+ /// A instance if exists, independently if the target exists or not. if does not exist.
+ public static System.IO.FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) =>
+ FileSystem.ResolveLinkTarget(linkPath, returnFinalTarget, isDirectory: false);
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs
index 224de5abcd5560..1f3afe1aff30f8 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs
@@ -11,6 +11,10 @@ internal static partial class FileSystem
{
internal const int DefaultBufferSize = 4096;
+ // On Linux, the maximum number of symbolic links that are followed while resolving a pathname is 40.
+ // See: https://man7.org/linux/man-pages/man7/path_resolution.7.html
+ private const int MaxFollowedLinks = 40;
+
public static void CopyFile(string sourceFullPath, string destFullPath, bool overwrite)
{
// If the destination path points to a directory, we throw to match Windows behaviour
@@ -530,5 +534,105 @@ public static string[] GetLogicalDrives()
{
return DriveInfoInternal.GetLogicalDrives();
}
+
+ /// Gets the path of the target of the specified link.
+ /// A path to a link file.
+ /// If linkPath represents a link file and it exists, returns the link's target path.
+ /// If linkPath is not a link or the target does not exist, returns null.
+ internal static string? GetLinkTarget(string linkPath) => Interop.Sys.ReadLink(linkPath);
+
+ ///
+ /// Creates a file symbolic link identified by path that points to pathToTarget..
+ ///
+ /// The path where the symbolic link should be created.
+ /// The path of the target to which the symbolic link points.
+ /// True if the pathToTarget represents a directory or a symlink to a directory.
+ internal static void CreateSymbolicLink(string path, string pathToTarget, bool isDirectory)
+ {
+ VerifyValidPath(path, nameof(path));
+ VerifyValidPath(pathToTarget, nameof(pathToTarget));
+
+ // Fail if the target exists but is not consistent with the expected filesystem entry type
+ if (Interop.Sys.LStat(pathToTarget, out Interop.Sys.FileStatus targetInfo) == 0)
+ {
+ // Skip this check if the target is a link:
+ // - It could be part of a chain of links, or
+ // - The link could be broken (which could be intended by the user)
+ if ((targetInfo.Mode & Interop.Sys.FileTypes.S_IFMT) != Interop.Sys.FileTypes.S_IFLNK &&
+ isDirectory != ((targetInfo.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR))
+ {
+ throw new IOException(SR.IO_InconsistentLinkType);
+ }
+ }
+
+ Interop.CheckIo(Interop.Sys.SymLink(pathToTarget, path), path, isDirectory);
+ }
+
+ /// Gets the target of the specified link path.
+ /// A path to a link file.
+ /// true to return the final target file or directory in a chain of links; false to return the immediate next target.
+ /// True if the linkPath points to a directory or a symlink to a directory.
+ /// If the specified linkPath represents a link file and it exists, returns a FileInfo if isDirectory
+ /// is false, or a DirectoryInfo if isDirectory is true, independently if the target file/directory exists or not.
+ /// If the specified linkPath is not a link file or it does not exist, returns null.
+ internal static FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget, bool isDirectory)
+ {
+ VerifyValidPath(linkPath, nameof(linkPath));
+
+ // throws if the current link file does not exist
+ Interop.CheckIo(Interop.Sys.LStat(linkPath, out _), linkPath, isDirectory);
+
+ string? targetPath = GetLinkTarget(linkPath);
+ if (targetPath == null)
+ {
+ // linkPath exists but is not a link
+ return null;
+ }
+
+ //Ensure all paths are fully qualified, by adding a prefix that is relative to the previous path
+ string? prefix;
+ if (PathInternal.IsPartiallyQualified(targetPath))
+ {
+ prefix = Path.GetDirectoryName(linkPath);
+ targetPath = Path.Join(prefix, targetPath);
+ }
+
+ int maxVisits = returnFinalTarget ? MaxFollowedLinks : 1;
+ int visitCount = 1;
+ while (visitCount < maxVisits)
+ {
+ string? nextPath = GetLinkTarget(targetPath);
+
+ if (nextPath == null)
+ {
+ // targetPath does not exist or is not a link
+ break;
+ }
+
+ if (PathInternal.IsPartiallyQualified(nextPath))
+ {
+ prefix = Path.GetDirectoryName(targetPath);
+ targetPath = Path.Join(prefix, nextPath);
+ }
+ else
+ {
+ targetPath = nextPath;
+ }
+
+ visitCount++;
+ }
+
+ if (visitCount >= MaxFollowedLinks)
+ {
+ // We went over the limit and couldn't reach the final target
+ throw new IOException(SR.Format(SR.IndexOutOfRange_SymbolicLinkLevels, linkPath));
+ }
+
+ Debug.Assert(targetPath != null);
+
+ return isDirectory ?
+ new DirectoryInfo(targetPath) :
+ new FileInfo(targetPath);
+ }
}
}
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 5b5f9a86bc7446..fd6198e25f5ff9 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
@@ -407,5 +407,19 @@ public static void SetLastWriteTime(string fullPath, DateTimeOffset time, bool a
public static string[] GetLogicalDrives()
=> DriveInfoInternal.GetLogicalDrives();
+
+ internal static string? GetLinkTarget(string linkPath)
+ {
+ return null;
+ }
+
+ internal static void CreateSymbolicLink(string path, string pathToTarget, bool isDirectory)
+ {
+ }
+
+ internal static FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget, bool isDirectory)
+ {
+ return null;
+ }
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs
new file mode 100644
index 00000000000000..97a61bbf2b8aa4
--- /dev/null
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs
@@ -0,0 +1,28 @@
+// 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;
+
+namespace System.IO
+{
+ /// Provides a platform independent implementation of FileSystem.
+ internal static partial class FileSystem
+ {
+ private static void VerifyValidPath(string path, string argName)
+ {
+ if (path == null)
+ {
+ throw new ArgumentNullException(argName);
+ }
+ else if (path.Length == 0)
+ {
+ throw new ArgumentException(SR.Arg_PathEmpty, argName);
+ }
+ else if (path.Contains('\0'))
+ {
+ throw new ArgumentException(SR.Argument_InvalidPathChars, argName);
+ }
+ }
+ }
+}
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Unix.cs
index 9bcae393120a8b..1d9f93da102910 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Unix.cs
@@ -27,7 +27,7 @@ internal static FileSystemInfo Create(string fullPath, string fileName, ref File
return info;
}
- internal void Invalidate() => _fileStatus.InvalidateCaches();
+ internal void InvalidateCore() => _fileStatus.InvalidateCaches();
internal unsafe void Init(ref FileStatus fileStatus)
{
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Windows.cs
index 47d064e8fe1735..ddd7caab688f88 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.Windows.cs
@@ -42,10 +42,7 @@ internal static unsafe FileSystemInfo Create(string fullPath, ref FileSystemEntr
return info;
}
- internal void Invalidate()
- {
- _dataInitialized = -1;
- }
+ internal void InvalidateCore() => _dataInitialized = -1;
internal unsafe void Init(Interop.NtDll.FILE_FULL_DIR_INFORMATION* info)
{
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs
index e389832b090926..3ab8ae39f974e0 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs
@@ -19,11 +19,19 @@ public abstract partial class FileSystemInfo : MarshalByRefObject, ISerializable
internal string _name = null!; // Fields initiated in derived classes
+ private string? _linkTarget;
+
protected FileSystemInfo(SerializationInfo info, StreamingContext context)
{
throw new PlatformNotSupportedException();
}
+ internal void Invalidate()
+ {
+ _linkTarget = null;
+ InvalidateCore();
+ }
+
public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
{
throw new PlatformNotSupportedException();
@@ -109,22 +117,37 @@ public DateTime LastWriteTimeUtc
///
/// If this instance represents a link, returns the link target's path.
- /// If the link does not exist, returns .
+ /// If a link does not exist in , or this instance does not represent a link, returns .
///
- public string? LinkTarget { get { return null; } }
+ public string? LinkTarget => _linkTarget ??= FileSystem.GetLinkTarget(FullPath);
///
/// Creates a symbolic link located in that points to the specified .
///
/// The path of the symbolic link target.
- public void CreateAsSymbolicLink(string pathToTarget) { }
+ /// is .
+ /// is empty.
+ /// -or-
+ /// This instance was not created passing an absolute path.
+ /// -or-
+ /// contains invalid path characters.
+ /// A file or directory already exists in the location of .
+ /// -or-
+ /// An I/O error occurred.
+ public void CreateAsSymbolicLink(string pathToTarget)
+ {
+ FileSystem.CreateSymbolicLink(OriginalPath, pathToTarget, this is DirectoryInfo);
+ Invalidate();
+ }
///
- /// Gets the target of the specified symbolic link.
+ /// Gets the target of the specified link.
///
/// to follow links to the final target; to return the immediate next link.
- /// A instance if the symbolic link exists, independently if the target exists or not. if the symbolic link does not exist.
- public System.IO.FileSystemInfo? ResolveLinkTarget(bool returnFinalTarget = false) { return null; }
+ /// A instance if the link exists, independently if the target exists or not; if a link does not exist
+ /// in , or this instance does not represent a link.
+ public FileSystemInfo? ResolveLinkTarget(bool returnFinalTarget = false) =>
+ FileSystem.ResolveLinkTarget(FullPath, returnFinalTarget, this is DirectoryInfo);
///
/// Returns the original path. Use FullName or Name properties for the full path or file/directory name.
From 2e822fdf21a9fb78d5555b7b354049df2baa17f6 Mon Sep 17 00:00:00 2001
From: David Cantu
Date: Fri, 11 Jun 2021 16:52:21 -0700
Subject: [PATCH 03/50] Add windows implementation of [sym]link APIs
---
.../src/Interop/Windows/Interop.Errors.cs | 1 +
.../Kernel32/Interop.CreateSymbolicLink.cs | 50 +++++
.../Kernel32/Interop.DeviceIoControl.cs | 26 +++
.../Kernel32/Interop.FileOperations.cs | 2 +
.../Interop.GetFinalPathNameByHandle.cs | 23 +++
.../Kernel32/Interop.REPARSE_DATA_BUFFER.cs | 37 ++++
.../BaseSymbolicLinks.FileSystem.cs | 41 +++-
.../BaseSymbolicLinks.FileSystemInfo.cs | 4 +-
.../tests/Directory/SymbolicLinks.cs | 22 +-
.../tests/DirectoryInfo/SymbolicLinks.cs | 22 +-
.../tests/System.IO.FileSystem.Tests.csproj | 4 +
.../System.Private.CoreLib.Shared.projitems | 12 ++
.../src/System/IO/Directory.cs | 9 +-
.../src/System/IO/File.cs | 9 +-
.../src/System/IO/FileSystem.Unix.cs | 7 +-
.../src/System/IO/FileSystem.Windows.cs | 188 ++++++++++++++++--
.../src/System/IO/FileSystem.cs | 12 +-
.../src/System/IO/FileSystemInfo.cs | 3 +-
18 files changed, 417 insertions(+), 55 deletions(-)
create mode 100644 src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs
create mode 100644 src/libraries/Common/src/Interop/Windows/Kernel32/Interop.DeviceIoControl.cs
create mode 100644 src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetFinalPathNameByHandle.cs
create mode 100644 src/libraries/Common/src/Interop/Windows/Kernel32/Interop.REPARSE_DATA_BUFFER.cs
diff --git a/src/libraries/Common/src/Interop/Windows/Interop.Errors.cs b/src/libraries/Common/src/Interop/Windows/Interop.Errors.cs
index 338706ea8491bc..d5f6d1637507fa 100644
--- a/src/libraries/Common/src/Interop/Windows/Interop.Errors.cs
+++ b/src/libraries/Common/src/Interop/Windows/Interop.Errors.cs
@@ -91,5 +91,6 @@ internal static partial class Errors
internal const int ERROR_EVENTLOG_FILE_CHANGED = 0x5DF;
internal const int ERROR_TRUSTED_RELATIONSHIP_FAILURE = 0x6FD;
internal const int ERROR_RESOURCE_LANG_NOT_FOUND = 0x717;
+ internal const int ERROR_NOT_A_REPARSE_POINT = 0x1126;
}
}
diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs
new file mode 100644
index 00000000000000..64c789b026014d
--- /dev/null
+++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs
@@ -0,0 +1,50 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+
+internal static partial class Interop
+{
+ internal static partial class Kernel32
+ {
+ ///
+ /// The link target is a file.
+ ///
+ internal const int SYMBOLIC_LINK_FLAG_FILE = 0x0;
+
+ ///
+ /// The link target is a directory.
+ ///
+ internal const int SYMBOLIC_LINK_FLAG_DIRECTORY = 0x1;
+
+ ///
+ /// Allows creation of symbolic links when the process is not elevated. Starting with Windows 10 Insiders build 14972.
+ /// Developer Mode must first be enabled on the machine before this option will function.
+ ///
+ internal const int SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE = 0x2;
+
+ [DllImport(Libraries.Kernel32, EntryPoint = "CreateSymbolicLinkW", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false, ExactSpelling = true)]
+ private static extern bool CreateSymbolicLinkPrivate(string lpSymlinkFileName, string lpTargetFileName, int dwFlags);
+
+ ///
+ /// Creates a symbolic link.
+ ///
+ /// The symbolic link to be created.
+ /// The name of the target for the symbolic link to be created.
+ /// If it has a device name associated with it, the link is treated as an absolute link; otherwise, the link is treated as a relative link.
+ /// if the link target is a directory; otherwise.
+ /// if the operation succeeds; otherwise.
+ internal static bool CreateSymbolicLink(string symlinkFileName, string targetFileName, bool isDirectory)
+ {
+ symlinkFileName = PathInternal.EnsureExtendedPrefixIfNeeded(symlinkFileName);
+ targetFileName = PathInternal.EnsureExtendedPrefixIfNeeded(targetFileName);
+
+ int flags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE |
+ (isDirectory ? SYMBOLIC_LINK_FLAG_DIRECTORY : SYMBOLIC_LINK_FLAG_FILE);
+
+ return CreateSymbolicLinkPrivate(symlinkFileName, targetFileName, flags);
+ }
+ }
+}
diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.DeviceIoControl.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.DeviceIoControl.cs
new file mode 100644
index 00000000000000..2ebf537adfa35b
--- /dev/null
+++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.DeviceIoControl.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+
+internal static partial class Interop
+{
+ internal static partial class Kernel32
+ {
+ // https://docs.microsoft.com/windows/win32/api/winioctl/ni-winioctl-fsctl_get_reparse_point
+ internal const int FSCTL_GET_REPARSE_POINT = 0x000900a8;
+
+ [DllImport(Libraries.Kernel32, EntryPoint = "DeviceIoControl", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true)]
+ public static extern bool DeviceIoControl(
+ SafeHandle hDevice,
+ uint dwIoControlCode,
+ IntPtr lpInBuffer,
+ uint nInBufferSize,
+ byte[] lpOutBuffer,
+ uint nOutBufferSize,
+ out uint lpBytesReturned,
+ IntPtr lpOverlapped);
+ }
+}
diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FileOperations.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FileOperations.cs
index f2a3872299d2c1..cc4896c1c52e48 100644
--- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FileOperations.cs
+++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FileOperations.cs
@@ -9,6 +9,7 @@ internal static partial class IOReparseOptions
{
internal const uint IO_REPARSE_TAG_FILE_PLACEHOLDER = 0x80000015;
internal const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003;
+ internal const uint IO_REPARSE_TAG_SYMLINK = 0xA000000C;
}
internal static partial class FileOperations
@@ -18,6 +19,7 @@ internal static partial class FileOperations
internal const int FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
internal const int FILE_FLAG_FIRST_PIPE_INSTANCE = 0x00080000;
+ internal const int FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000;
internal const int FILE_FLAG_OVERLAPPED = 0x40000000;
internal const int FILE_LIST_DIRECTORY = 0x0001;
diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetFinalPathNameByHandle.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetFinalPathNameByHandle.cs
new file mode 100644
index 00000000000000..17f0ed2e8ca3b4
--- /dev/null
+++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetFinalPathNameByHandle.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using Microsoft.Win32.SafeHandles;
+
+internal static partial class Interop
+{
+ internal static partial class Kernel32
+ {
+ public const uint FILE_NAME_NORMALIZED = 0x0;
+
+ // https://docs.microsoft.com/windows/desktop/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew (kernel32)
+ [DllImport(Libraries.Kernel32, EntryPoint = "GetFinalPathNameByHandleW", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)]
+ public static unsafe extern uint GetFinalPathNameByHandle(
+ SafeFileHandle hFile,
+ char* lpszFilePath,
+ uint cchFilePath,
+ uint dwFlags);
+ }
+}
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
new file mode 100644
index 00000000000000..a91498c283caa2
--- /dev/null
+++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.REPARSE_DATA_BUFFER.cs
@@ -0,0 +1,37 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Runtime.InteropServices;
+
+internal static partial class Interop
+{
+ internal static partial class Kernel32
+ {
+ // https://docs.microsoft.com/windows-hardware/drivers/ifs/fsctl-get-reparse-point
+ public const int MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16 * 1024;
+
+ public 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)]
+ public unsafe struct REPARSE_DATA_BUFFER
+ {
+ public uint ReparseTag;
+ public ushort ReparseDataLength;
+ public ushort Reserved;
+ public SymbolicLinkReparseBuffer ReparseBufferSymbolicLink;
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SymbolicLinkReparseBuffer
+ {
+ public ushort SubstituteNameOffset;
+ public ushort SubstituteNameLength;
+ public ushort PrintNameOffset;
+ public ushort PrintNameLength;
+ public uint Flags;
+ }
+ }
+ }
+}
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 d0be3a3fab7410..3960b98db8b8b4 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
@@ -21,6 +21,32 @@ public abstract class BaseSymbolicLinks_FileSystem : BaseSymbolicLinks
/// Calls the actual public API for resolving the symbolic link target.
protected abstract FileSystemInfo ResolveLinkTarget(string linkPath, string? expectedLinkTarget, bool returnFinalTarget = false);
+ ///
+ /// Appends \??\ to the absolute path
+ ///
+ protected void AssertPathEquals(string expected, string actual)
+ {
+#if WINDOWS // Windows implementation has a couple nuances compared to Unix.
+ // If the link target path is absolute, the following occurs:
+ // ResolveLinkTarget(), which uses DeviceIoControl, will return the path with the DOS Device prefix (\??\).
+ // ResolveLinkTarget(returnFinalTarget: true), which uses GetFinalPathNameByHandleW, will return with the extended prefix (\\?\).
+ // For testing, we will ignore this prefix.
+ if (PathInternal.IsExtended(actual))
+ {
+ actual = actual.Substring(4);
+ }
+
+ // Windows syscalls remove the redundant segments in the link target path.
+ // We will remove them from the expected path when testing Windows but keep them when testing Unix, which doesn't remove them.
+ int rootLength = PathInternal.GetRootLength(expected);
+ if (rootLength > 0)
+ {
+ expected = PathInternal.RemoveRelativeSegments(expected, rootLength);
+ }
+#endif
+ Assert.Equal(expected, actual);
+ }
+
[Fact]
public void CreateSymbolicLink_NullPathToTarget()
{
@@ -70,7 +96,6 @@ public void CreateSymbolicLink_RelativeTargetPath_TargetExists_WithRedundantSegm
public void CreateSymbolicLink_AbsoluteTargetPath_TargetExists()
{
// /path/to/link -> /path/to/existingtarget
-
string linkPath = GetRandomLinkPath();
string targetPath = GetRandomFilePath();
VerifySymbolicLinkAndResolvedTarget(
@@ -287,29 +312,29 @@ private void ResolveLinkTarget_ReturnFinalTarget(string link1Path, string expect
Assert.True(link2.Exists);
Assert.True(link2.Attributes.HasFlag(FileAttributes.ReparsePoint));
AssertIsCorrectTypeAndDirectoryAttribute(link2);
- Assert.Equal(link2.LinkTarget, expectedLink2Target);
+ AssertPathEquals(expectedLink2Target, link2.LinkTarget);
// link1 to link2
FileSystemInfo link1 = CreateSymbolicLink(link1Path, expectedLink1Target);
Assert.True(link1.Exists);
Assert.True(link1.Attributes.HasFlag(FileAttributes.ReparsePoint));
AssertIsCorrectTypeAndDirectoryAttribute(link1);
- Assert.Equal(link1.LinkTarget, expectedLink1Target);
+ AssertPathEquals(expectedLink1Target, link1.LinkTarget );
// link1: do not follow symlinks
FileSystemInfo link1Target = ResolveLinkTarget(link1Path, expectedLink1Target);
Assert.True(link1Target.Exists);
AssertIsCorrectTypeAndDirectoryAttribute(link1Target);
Assert.True(link1Target.Attributes.HasFlag(FileAttributes.ReparsePoint));
- Assert.Equal(link1Target.FullName, link2Path);
- Assert.Equal(link1Target.LinkTarget, expectedLink2Target);
+ AssertPathEquals(link2Path, link1Target.FullName);
+ AssertPathEquals(expectedLink2Target, link1Target.LinkTarget);
// link2: do not follow symlinks
FileSystemInfo link2Target = ResolveLinkTarget(link2Path, expectedLink2Target);
Assert.True(link2Target.Exists);
AssertIsCorrectTypeAndDirectoryAttribute(link2Target);
Assert.False(link2Target.Attributes.HasFlag(FileAttributes.ReparsePoint));
- Assert.Equal(link2Target.FullName, filePath);
+ AssertPathEquals(filePath, link2Target.FullName);
Assert.Null(link2Target.LinkTarget);
// link1: follow symlinks
@@ -317,7 +342,7 @@ private void ResolveLinkTarget_ReturnFinalTarget(string link1Path, string expect
Assert.True(finalTarget.Exists);
AssertIsCorrectTypeAndDirectoryAttribute(finalTarget);
Assert.False(finalTarget.Attributes.HasFlag(FileAttributes.ReparsePoint));
- Assert.Equal(finalTarget.FullName, filePath);
+ AssertPathEquals(filePath, finalTarget.FullName);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystemInfo.cs b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystemInfo.cs
index 9a56e0c04ef203..1a21adb874199e 100644
--- a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystemInfo.cs
+++ b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystemInfo.cs
@@ -30,7 +30,7 @@ protected override FileSystemInfo ResolveLinkTarget(string linkPath, string? exp
}
else
{
- Assert.Equal(link.LinkTarget, expectedLinkTarget);
+ AssertPathEquals(expectedLinkTarget, link.LinkTarget);
}
FileSystemInfo? target = link.ResolveLinkTarget(returnFinalTarget);
@@ -45,4 +45,4 @@ protected override FileSystemInfo ResolveLinkTarget(string linkPath, string? exp
return target;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs
index 49606897a1b0d8..2df64f9912143f 100644
--- a/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs
+++ b/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs
@@ -38,12 +38,22 @@ protected override void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo
protected override void AssertLinkExists(FileSystemInfo link) =>
Assert.False(link.Exists); // For directory symlinks, we return the exists info from the target
- // When the directory target does not exist FileStatus.GetExists returns false because:
- // - We check _exists (which whould be true because the link itself exists).
- // - We check InitiallyDirectory, which is the initial expected object type (which would be true).
- // - We check _directory (false because the target directory does not exist)
- protected override void AssertExistsWhenNoTarget(FileSystemInfo link) =>
- Assert.False(link.Exists);
+ protected override void AssertExistsWhenNoTarget(FileSystemInfo link)
+ {
+ if (PlatformDetection.IsWindows)
+ {
+ Assert.True(link.Exists);
+ }
+ else
+ {
+ // Unix implementation detail:
+ // When the directory target does not exist FileStatus.GetExists returns false because:
+ // - We check _exists (which whould be true because the link itself exists).
+ // - We check InitiallyDirectory, which is the initial expected object type (which would be true).
+ // - We check _directory (false because the target directory does not exist)
+ Assert.False(link.Exists);
+ }
+ }
[Fact]
public void EnumerateDirectories_LinksWithCycles_ShouldNotThrow()
diff --git a/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs
index ec99f4d224c403..bab96280ff22c1 100644
--- a/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs
+++ b/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs
@@ -35,12 +35,22 @@ protected override void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo
protected override void AssertLinkExists(FileSystemInfo link) =>
Assert.False(link.Exists); // For directory symlinks, we return the exists info from the target
- // When the directory target does not exist FileStatus.GetExists returns false because:
- // - We check _exists (which whould be true because the link itself exists).
- // - We check InitiallyDirectory, which is the initial expected object type (which would be true).
- // - We check _directory (false because the target directory does not exist)
- protected override void AssertExistsWhenNoTarget(FileSystemInfo link) =>
- Assert.False(link.Exists);
+ protected override void AssertExistsWhenNoTarget(FileSystemInfo link)
+ {
+ if (PlatformDetection.IsWindows)
+ {
+ Assert.True(link.Exists);
+ }
+ else
+ {
+ // Unix implementation detail:
+ // When the directory target does not exist FileStatus.GetExists returns false because:
+ // - We check _exists (which whould be true because the link itself exists).
+ // - We check InitiallyDirectory, which is the initial expected object type (which would be true).
+ // - We check _directory (false because the target directory does not exist)
+ Assert.False(link.Exists);
+ }
+ }
[Theory]
[InlineData(false)]
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 2c0d97af8758b6..579ccac9b6066b 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
@@ -5,6 +5,7 @@
$(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-Unix;$(NetCoreAppCurrent)-Browser--working-dir=/test-dir
+ ../../System.Private.CoreLib/src/Resources/Strings.resx
@@ -84,6 +85,9 @@
+
+
+
diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
index 4b7da59ca6c000..8bcfa3f38d6e0a 100644
--- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
+++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
@@ -1425,9 +1425,15 @@
Common\Interop\Windows\Kernel32\Interop.CreateFile_IntPtr.cs
+
+ Common\Interop\Windows\Kernel32\Interop.CreateSymbolicLink.cs
+
Common\Interop\Windows\Kernel32\Interop.CriticalSection.cs
+
+ Common\Interop\Windows\Kernel32\Interop.DeviceIoControl.cs
+
Common\Interop\Windows\Kernel32\Interop.ExpandEnvironmentStrings.cs
@@ -1506,6 +1512,9 @@
Common\Interop\Windows\Kernel32\Interop.GetFileType_SafeHandle.cs
+
+ Common\Interop\Windows\Kernel32\Interop.GetFinalPathNameByHandle.cs
+
Common\Interop\Windows\Kernel32\Interop.GetFullPathNameW.cs
@@ -1614,6 +1623,9 @@
Common\Interop\Windows\Kernel32\Interop.RemoveDirectory.cs
+
+ Common\Interop\Windows\Kernel32\Interop.REPARSE_DATA_BUFFER.cs
+
Common\Interop\Windows\Kernel32\Interop.ReplaceFile.cs
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs
index 58e145d540ecd7..fc0022befa88fb 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs
@@ -333,6 +333,8 @@ public static string[] GetLogicalDrives()
/// An I/O error occurred.
public static FileSystemInfo CreateSymbolicLink(string path, string pathToTarget)
{
+ FileSystem.VerifyValidPath(path, nameof(path));
+ FileSystem.VerifyValidPath(pathToTarget, nameof(pathToTarget));
FileSystem.CreateSymbolicLink(path, pathToTarget, isDirectory: true);
return new DirectoryInfo(path);
}
@@ -343,7 +345,10 @@ public static FileSystemInfo CreateSymbolicLink(string path, string pathToTarget
/// The path of the directory link.
/// to follow links to the final target; to return the immediate next link.
/// A instance if exists, independently if the target exists or not. if does not exist.
- public static System.IO.FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) =>
- FileSystem.ResolveLinkTarget(linkPath, returnFinalTarget, isDirectory: true);
+ public static System.IO.FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false)
+ {
+ FileSystem.VerifyValidPath(linkPath, nameof(linkPath));
+ return FileSystem.ResolveLinkTarget(linkPath, returnFinalTarget, isDirectory: true);
+ }
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs
index 0e81dfba1087ba..9bf27e7252a777 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs
@@ -1025,6 +1025,8 @@ private static async Task InternalWriteAllTextAsync(StreamWriter sw, string cont
/// An I/O error occurred.
public static FileSystemInfo CreateSymbolicLink(string path, string pathToTarget)
{
+ FileSystem.VerifyValidPath(path, nameof(path));
+ FileSystem.VerifyValidPath(pathToTarget, nameof(pathToTarget));
FileSystem.CreateSymbolicLink(path, pathToTarget, isDirectory: false);
return new FileInfo(path);
}
@@ -1035,7 +1037,10 @@ public static FileSystemInfo CreateSymbolicLink(string path, string pathToTarget
/// The path of the file link.
/// to follow links to the final target; to return the immediate next link.
/// A instance if exists, independently if the target exists or not. if does not exist.
- public static System.IO.FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) =>
- FileSystem.ResolveLinkTarget(linkPath, returnFinalTarget, isDirectory: false);
+ public static System.IO.FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false)
+ {
+ FileSystem.VerifyValidPath(linkPath, nameof(linkPath));
+ return FileSystem.ResolveLinkTarget(linkPath, returnFinalTarget, isDirectory: false);
+ }
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs
index 1f3afe1aff30f8..5cb5d898103c60 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs
@@ -539,7 +539,7 @@ public static string[] GetLogicalDrives()
/// A path to a link file.
/// If linkPath represents a link file and it exists, returns the link's target path.
/// If linkPath is not a link or the target does not exist, returns null.
- internal static string? GetLinkTarget(string linkPath) => Interop.Sys.ReadLink(linkPath);
+ internal static string? GetLinkTarget(string linkPath, bool isDirectory) => Interop.Sys.ReadLink(linkPath);
///
/// Creates a file symbolic link identified by path that points to pathToTarget..
@@ -549,9 +549,6 @@ public static string[] GetLogicalDrives()
/// True if the pathToTarget represents a directory or a symlink to a directory.
internal static void CreateSymbolicLink(string path, string pathToTarget, bool isDirectory)
{
- VerifyValidPath(path, nameof(path));
- VerifyValidPath(pathToTarget, nameof(pathToTarget));
-
// Fail if the target exists but is not consistent with the expected filesystem entry type
if (Interop.Sys.LStat(pathToTarget, out Interop.Sys.FileStatus targetInfo) == 0)
{
@@ -577,8 +574,6 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i
/// If the specified linkPath is not a link file or it does not exist, returns null.
internal static FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget, bool isDirectory)
{
- VerifyValidPath(linkPath, nameof(linkPath));
-
// throws if the current link file does not exist
Interop.CheckIo(Interop.Sys.LStat(linkPath, out _), linkPath, isDirectory);
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 fd6198e25f5ff9..e3bc53a222ac34 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
@@ -8,6 +8,7 @@
using System.Runtime.InteropServices;
using System.IO;
using System.Text;
+using System.Buffers;
#if MS_IO_REDIST
namespace Microsoft.IO
@@ -185,7 +186,7 @@ public static void RemoveDirectory(string fullPath, bool recursive)
}
Interop.Kernel32.WIN32_FIND_DATA findData = default;
- GetFindData(fullPath, ref findData);
+ GetFindData(fullPath, isDirectory: true, ref findData);
if (IsNameSurrogateReparsePoint(ref findData))
{
// Don't recurse
@@ -199,18 +200,16 @@ public static void RemoveDirectory(string fullPath, bool recursive)
RemoveDirectoryRecursive(fullPath, ref findData, topLevel: true);
}
- private static void GetFindData(string fullPath, ref Interop.Kernel32.WIN32_FIND_DATA findData)
+ private static void GetFindData(string fullPath, bool isDirectory, ref Interop.Kernel32.WIN32_FIND_DATA findData)
{
- using (SafeFindHandle handle = Interop.Kernel32.FindFirstFile(Path.TrimEndingDirectorySeparator(fullPath), ref findData))
+ using SafeFindHandle handle = Interop.Kernel32.FindFirstFile(Path.TrimEndingDirectorySeparator(fullPath), ref findData);
+ if (handle.IsInvalid)
{
- if (handle.IsInvalid)
- {
- int errorCode = Marshal.GetLastWin32Error();
- // File not found doesn't make much sense coming from a directory delete.
- if (errorCode == Interop.Errors.ERROR_FILE_NOT_FOUND)
- errorCode = Interop.Errors.ERROR_PATH_NOT_FOUND;
- throw Win32Marshal.GetExceptionForWin32Error(errorCode, fullPath);
- }
+ int errorCode = Marshal.GetLastWin32Error();
+ // File not found doesn't make much sense coming from a directory.
+ if (isDirectory && errorCode == Interop.Errors.ERROR_FILE_NOT_FOUND)
+ errorCode = Interop.Errors.ERROR_PATH_NOT_FOUND;
+ throw Win32Marshal.GetExceptionForWin32Error(errorCode, fullPath);
}
}
@@ -408,18 +407,175 @@ public static void SetLastWriteTime(string fullPath, DateTimeOffset time, bool a
public static string[] GetLogicalDrives()
=> DriveInfoInternal.GetLogicalDrives();
- internal static string? GetLinkTarget(string linkPath)
+ internal static void CreateSymbolicLink(string path, string pathToTarget, bool isDirectory)
{
- return null;
+ Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA data = default;
+ FillAttributeInfo(pathToTarget, ref data, returnErrorOnNotFound: false);
+ if (data.dwFileAttributes != -1 &&
+ isDirectory != ((data.dwFileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_DIRECTORY) != 0))
+ {
+ throw new IOException(SR.IO_InconsistentLinkType);
+ }
+
+ bool result = Interop.Kernel32.CreateSymbolicLink(path, pathToTarget, isDirectory);
+ if (!result)
+ {
+ throw Win32Marshal.GetExceptionForLastWin32Error(path);
+ }
}
- internal static void CreateSymbolicLink(string path, string pathToTarget, bool isDirectory)
+ internal static unsafe FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget, bool isDirectory)
{
+ string? targetPath = returnFinalTarget ?
+ GetFinalLinkTarget(linkPath, isDirectory) :
+ GetImmediateLinkTarget(linkPath, isDirectory, throwOnNotFound: true, normalize: true);
+
+ return targetPath == null ? null :
+ isDirectory ? new DirectoryInfo(targetPath) : new FileInfo(targetPath);
}
- internal static FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget, bool isDirectory)
+ internal static string? GetLinkTarget(string linkPath, bool isDirectory)
+ => GetImmediateLinkTarget(linkPath, isDirectory, throwOnNotFound: false, normalize: false);
+
+ ///
+ /// Gets reparse point information associated to .
+ ///
+ /// The immediate link target, absolute or relative or null if the file is not a supported link.
+ internal static unsafe string? GetImmediateLinkTarget(string linkPath, bool isDirectory, bool throwOnNotFound, bool normalize)
{
- return null;
+ using SafeFileHandle handle = OpenSafeFileHandle(linkPath,
+ Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS |
+ Interop.Kernel32.FileOperations.FILE_FLAG_OPEN_REPARSE_POINT);
+
+ if (handle.IsInvalid)
+ {
+ int error = Marshal.GetLastWin32Error();
+ switch (error)
+ {
+ case Interop.Errors.ERROR_FILE_NOT_FOUND:
+ case Interop.Errors.ERROR_PATH_NOT_FOUND:
+ if (throwOnNotFound)
+ {
+ throw Win32Marshal.GetExceptionForWin32Error(
+ // File not found doesn't make much sense coming from a directory.
+ isDirectory ? Interop.Errors.ERROR_PATH_NOT_FOUND : error, linkPath);
+ }
+ return null;
+ default:
+ throw Win32Marshal.GetExceptionForWin32Error(error, linkPath);
+ }
+ }
+
+ byte[]? buffer = null;
+ try
+ {
+ buffer = ArrayPool.Shared.Rent(Interop.Kernel32.MAXIMUM_REPARSE_DATA_BUFFER_SIZE);
+ bool success = Interop.Kernel32.DeviceIoControl(
+ handle,
+ dwIoControlCode: Interop.Kernel32.FSCTL_GET_REPARSE_POINT,
+ lpInBuffer: IntPtr.Zero,
+ nInBufferSize: 0,
+ lpOutBuffer: buffer,
+ nOutBufferSize: Interop.Kernel32.MAXIMUM_REPARSE_DATA_BUFFER_SIZE,
+ out _,
+ IntPtr.Zero);
+
+ if (!success)
+ {
+ int error = Marshal.GetLastWin32Error();
+ // The file or directory is not a reparse point.
+ if (error == Interop.Errors.ERROR_NOT_A_REPARSE_POINT)
+ {
+ return null;
+ }
+
+ throw Win32Marshal.GetExceptionForWin32Error(error, linkPath);
+ }
+
+ ReadOnlySpan bufferSpan = new(buffer);
+ ref readonly Interop.Kernel32.REPARSE_DATA_BUFFER rdb = ref MemoryMarshal.AsRef(bufferSpan);
+
+ // Only symbolic links are supported at the moment.
+ if ((rdb.ReparseTag & Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_SYMLINK) == 0)
+ {
+ return null;
+ }
+
+ int substituteNameOffset = sizeof(Interop.Kernel32.REPARSE_DATA_BUFFER) + rdb.ReparseBufferSymbolicLink.SubstituteNameOffset;
+ int substituteNameLength = rdb.ReparseBufferSymbolicLink.SubstituteNameLength;
+
+ ReadOnlySpan targetPath = MemoryMarshal.Cast(bufferSpan.Slice(substituteNameOffset, substituteNameLength));
+
+ // Target path is relative, we need to append the link directory.
+ if (normalize && (rdb.ReparseBufferSymbolicLink.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) != 0)
+ {
+ return Path.Join(Path.GetDirectoryName(linkPath.AsSpan()), targetPath);
+ }
+
+ return targetPath.ToString();
+ }
+ finally
+ {
+ if (buffer != null)
+ {
+ ArrayPool.Shared.Return(buffer);
+ }
+ }
+ }
+
+ private static unsafe string? GetFinalLinkTarget(string linkPath, bool isDirectory)
+ {
+ Interop.Kernel32.WIN32_FIND_DATA data = default;
+ GetFindData(linkPath, isDirectory, ref data);
+
+ // 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)
+ {
+ return null;
+ }
+
+ using SafeFileHandle handle = OpenSafeFileHandle(linkPath,
+ Interop.Kernel32.FileOperations.OPEN_EXISTING |
+ Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS);
+
+ if (handle.IsInvalid)
+ {
+ Win32Marshal.GetExceptionForLastWin32Error(linkPath);
+ }
+
+ char* buffer = stackalloc char[Interop.Kernel32.MAX_PATH];
+ uint res = Interop.Kernel32.GetFinalPathNameByHandle(handle, buffer, Interop.Kernel32.MAX_PATH, Interop.Kernel32.FILE_NAME_NORMALIZED);
+
+ // If the function fails because lpszFilePath is too small to hold the string plus the terminating null character,
+ // the return value is the required buffer size, in TCHARs. This value includes the size of the terminating null character.
+ // .NET dev note: This should never happen.
+ Debug.Assert(res <= Interop.Kernel32.MAX_PATH);
+
+ // If the function fails for any other reason, the return value is zero.
+ if (res == 0)
+ {
+ throw Win32Marshal.GetExceptionForLastWin32Error(linkPath);
+ }
+
+ // If the function succeeds, the return value is the length of the string received by lpszFilePath, in TCHARs.
+ // This value does not include the size of the terminating null character.
+ return new string(buffer, 0, (int)res);
+ }
+
+ private static unsafe SafeFileHandle OpenSafeFileHandle(string path, int flags)
+ {
+ SafeFileHandle handle = Interop.Kernel32.CreateFile(
+ path,
+ dwDesiredAccess: 0,
+ FileShare.ReadWrite | FileShare.Delete,
+ lpSecurityAttributes: (Interop.Kernel32.SECURITY_ATTRIBUTES*)IntPtr.Zero,
+ FileMode.Open,
+ dwFlagsAndAttributes: flags,
+ hTemplateFile: IntPtr.Zero);
+
+ return handle;
}
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs
index 97a61bbf2b8aa4..2c4710636e1c86 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs
@@ -1,15 +1,15 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// 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;
-
+#if MS_IO_REDIST
+namespace Microsoft.IO
+#else
namespace System.IO
+#endif
{
- /// Provides a platform independent implementation of FileSystem.
internal static partial class FileSystem
{
- private static void VerifyValidPath(string path, string argName)
+ internal static void VerifyValidPath(string path, string argName)
{
if (path == null)
{
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs
index 3ab8ae39f974e0..447c54f1ffa9a7 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs
@@ -119,7 +119,7 @@ public DateTime LastWriteTimeUtc
/// If this instance represents a link, returns the link target's path.
/// If a link does not exist in , or this instance does not represent a link, returns .
///
- public string? LinkTarget => _linkTarget ??= FileSystem.GetLinkTarget(FullPath);
+ public string? LinkTarget => _linkTarget ??= FileSystem.GetLinkTarget(FullPath, this is DirectoryInfo);
///
/// Creates a symbolic link located in that points to the specified .
@@ -136,6 +136,7 @@ public DateTime LastWriteTimeUtc
/// An I/O error occurred.
public void CreateAsSymbolicLink(string pathToTarget)
{
+ FileSystem.VerifyValidPath(pathToTarget, nameof(pathToTarget));
FileSystem.CreateSymbolicLink(OriginalPath, pathToTarget, this is DirectoryInfo);
Invalidate();
}
From 32e196c1132d73c956643c65061c522bd774f0ca Mon Sep 17 00:00:00 2001
From: Carlos Sanchez <1175054+carlossanlop@users.noreply.github.com>
Date: Tue, 15 Jun 2021 16:22:55 -0700
Subject: [PATCH 04/50] Use ValueStringBuilder in ResolveLinkTarget
---
.../Unix/Interop.DefaultPathBufferSize.cs | 9 ++
.../Unix/System.Native/Interop.GetCwd.cs | 8 +-
.../Unix/System.Native/Interop.ReadLink.cs | 31 ++++---
.../Unix/System.Native/Interop.Stat.Span.cs | 8 +-
.../src/Resources/Strings.resx | 6 +-
.../System.Private.CoreLib.Shared.projitems | 3 +
.../src/System/IO/FileSystem.Unix.cs | 87 +++++++++++--------
.../src/System/IO/Path.cs | 2 +-
8 files changed, 91 insertions(+), 63 deletions(-)
create mode 100644 src/libraries/Common/src/Interop/Unix/Interop.DefaultPathBufferSize.cs
diff --git a/src/libraries/Common/src/Interop/Unix/Interop.DefaultPathBufferSize.cs b/src/libraries/Common/src/Interop/Unix/Interop.DefaultPathBufferSize.cs
new file mode 100644
index 00000000000000..d9807b427bf26d
--- /dev/null
+++ b/src/libraries/Common/src/Interop/Unix/Interop.DefaultPathBufferSize.cs
@@ -0,0 +1,9 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+internal static partial class Interop
+{
+ // Unix max paths are typically 1K or 4K UTF-8 bytes, 256 should handle the majority of paths
+ // without putting too much pressure on the stack.
+ internal const int DefaultPathBufferSize = 256;
+}
diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetCwd.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetCwd.cs
index 1faef8cc0be8dd..78da5a667310f1 100644
--- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetCwd.cs
+++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetCwd.cs
@@ -14,18 +14,16 @@ internal static partial class Sys
internal static unsafe string GetCwd()
{
- const int StackLimit = 256;
-
// First try to get the path into a buffer on the stack
- byte* stackBuf = stackalloc byte[StackLimit];
- string? result = GetCwdHelper(stackBuf, StackLimit);
+ byte* stackBuf = stackalloc byte[DefaultPathBufferSize];
+ string? result = GetCwdHelper(stackBuf, DefaultPathBufferSize);
if (result != null)
{
return result;
}
// If that was too small, try increasing large buffer sizes
- int bufferSize = StackLimit;
+ int bufferSize = DefaultPathBufferSize;
while (true)
{
checked { bufferSize *= 2; }
diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ReadLink.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ReadLink.cs
index 8f0f6a15fed95d..0ecefe429e4291 100644
--- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ReadLink.cs
+++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ReadLink.cs
@@ -4,6 +4,7 @@
using System.Runtime.InteropServices;
using System.Buffers;
using System.Text;
+using System;
internal static partial class Interop
{
@@ -20,24 +21,32 @@ internal static partial class Sys
/// Returns the number of bytes placed into the buffer on success; bufferSize if the buffer is too small; and -1 on error.
///
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_ReadLink", SetLastError = true)]
- private static extern int ReadLink(string path, byte[] buffer, int bufferSize);
+ private static extern int ReadLink(ref byte path, byte[] buffer, int bufferSize);
///
/// Takes a path to a symbolic link and returns the link target path.
///
- /// The path to the symlink
- ///
- /// Returns the link to the target path on success; and null otherwise.
- ///
- public static string? ReadLink(string path)
+ /// The path to the symlink.
+ /// Returns the link to the target path on success; and null otherwise.
+ internal static string? ReadLink(ReadOnlySpan path)
{
- int bufferSize = 256;
+ int outputBufferSize = DefaultPathBufferSize;
+
+ // Use an initial buffer size that prevents disposing and renting
+ // a second time when calling ConvertAndTerminateString.
+ int pathBufferSize = Encoding.UTF8.GetMaxByteCount(path.Length) + 1;
+ using var converter = new ValueUtf8Converter(stackalloc byte[pathBufferSize]);
+
while (true)
{
- byte[] buffer = ArrayPool.Shared.Rent(bufferSize);
+ byte[] buffer = ArrayPool.Shared.Rent(outputBufferSize);
try
{
- int resultLength = Interop.Sys.ReadLink(path, buffer, buffer.Length);
+ int resultLength = Interop.Sys.ReadLink(
+ ref MemoryMarshal.GetReference(converter.ConvertAndTerminateString(path)),
+ buffer,
+ buffer.Length);
+
if (resultLength < 0)
{
// error
@@ -54,8 +63,8 @@ internal static partial class Sys
ArrayPool.Shared.Return(buffer);
}
- // buffer was too small, loop around again and try with a larger buffer.
- bufferSize *= 2;
+ // Output buffer was too small, loop around again and try with a larger buffer.
+ outputBufferSize *= 2;
}
}
}
diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Stat.Span.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Stat.Span.cs
index 3c638cb60aa524..85028fd0fd088d 100644
--- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Stat.Span.cs
+++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Stat.Span.cs
@@ -9,16 +9,12 @@ internal static partial class Interop
{
internal static partial class Sys
{
- // Unix max paths are typically 1K or 4K UTF-8 bytes, 256 should handle the majority of paths
- // without putting too much pressure on the stack.
- private const int StackBufferSize = 256;
-
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_Stat", SetLastError = true)]
internal static extern int Stat(ref byte path, out FileStatus output);
internal static int Stat(ReadOnlySpan path, out FileStatus output)
{
- var converter = new ValueUtf8Converter(stackalloc byte[StackBufferSize]);
+ var converter = new ValueUtf8Converter(stackalloc byte[DefaultPathBufferSize]);
int result = Stat(ref MemoryMarshal.GetReference(converter.ConvertAndTerminateString(path)), out output);
converter.Dispose();
return result;
@@ -29,7 +25,7 @@ internal static int Stat(ReadOnlySpan path, out FileStatus output)
internal static int LStat(ReadOnlySpan path, out FileStatus output)
{
- var converter = new ValueUtf8Converter(stackalloc byte[StackBufferSize]);
+ var converter = new ValueUtf8Converter(stackalloc byte[DefaultPathBufferSize]);
int result = LStat(ref MemoryMarshal.GetReference(converter.ConvertAndTerminateString(path)), out output);
converter.Dispose();
return result;
diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx
index 6648c44fc7ebc4..82893fbe72c143 100644
--- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx
+++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx
@@ -2335,9 +2335,6 @@
Unmanaged memory stream position was beyond the capacity of the stream.
-
- Too many levels of symbolic links in '{0}'.
-
Insufficient available memory to meet the expected demands of an operation at this time. Please try again later.
@@ -2713,6 +2710,9 @@
The path '{0}' is too long, or a component of the specified path is too long.
+
+ Too many levels of symbolic links in '{0}'.
+
[Unknown]
diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
index 8bcfa3f38d6e0a..bb5fcf8455cc3a 100644
--- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
+++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
@@ -1900,6 +1900,9 @@
Common\Interop\Unix\Interop.Libraries.cs
+
+ Common\Interop\Unix\Interop.DefaultPathBufferSize.cs
+
Common\Interop\Unix\System.Native\Interop.Access.cs
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs
index 5cb5d898103c60..313545557f87b6 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Diagnostics;
+using System.Text;
namespace System.IO
{
@@ -537,12 +538,13 @@ public static string[] GetLogicalDrives()
/// Gets the path of the target of the specified link.
/// A path to a link file.
+ /// Whether the link represents a directory or not. Irrelevant in Unix since readlink does not care about the underlying type.
/// If linkPath represents a link file and it exists, returns the link's target path.
/// If linkPath is not a link or the target does not exist, returns null.
- internal static string? GetLinkTarget(string linkPath, bool isDirectory) => Interop.Sys.ReadLink(linkPath);
+ internal static string? GetLinkTarget(ReadOnlySpan linkPath, bool isDirectory) => Interop.Sys.ReadLink(linkPath);
///
- /// Creates a file symbolic link identified by path that points to pathToTarget..
+ /// Creates a file symbolic link identified by path that points to pathToTarget.
///
/// The path where the symbolic link should be created.
/// The path of the target to which the symbolic link points.
@@ -566,7 +568,7 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i
}
/// Gets the target of the specified link path.
- /// A path to a link file.
+ /// A path (absolute or relative) to a link file.
/// true to return the final target file or directory in a chain of links; false to return the immediate next target.
/// True if the linkPath points to a directory or a symlink to a directory.
/// If the specified linkPath represents a link file and it exists, returns a FileInfo if isDirectory
@@ -577,57 +579,68 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i
// throws if the current link file does not exist
Interop.CheckIo(Interop.Sys.LStat(linkPath, out _), linkPath, isDirectory);
- string? targetPath = GetLinkTarget(linkPath);
- if (targetPath == null)
- {
- // linkPath exists but is not a link
- return null;
- }
-
- //Ensure all paths are fully qualified, by adding a prefix that is relative to the previous path
- string? prefix;
- if (PathInternal.IsPartiallyQualified(targetPath))
- {
- prefix = Path.GetDirectoryName(linkPath);
- targetPath = Path.Join(prefix, targetPath);
- }
+ ValueStringBuilder sb = new(stackalloc char[Interop.DefaultPathBufferSize]);
+ sb.Append(linkPath);
int maxVisits = returnFinalTarget ? MaxFollowedLinks : 1;
- int visitCount = 1;
+ int visitCount = 0;
while (visitCount < maxVisits)
{
- string? nextPath = GetLinkTarget(targetPath);
-
- if (nextPath == null)
+ if (!TryGetLinkTarget(ref sb))
{
- // targetPath does not exist or is not a link
- break;
- }
+ if (visitCount == 0)
+ {
+ // Special case: Reaching here means linkPath is not a link,
+ // but we know it exists because we did an lstat at the top
+ sb.Dispose();
+ return null;
+ }
- if (PathInternal.IsPartiallyQualified(nextPath))
- {
- prefix = Path.GetDirectoryName(targetPath);
- targetPath = Path.Join(prefix, nextPath);
- }
- else
- {
- targetPath = nextPath;
+ // We finally found the final target: either
+ // this file does not exist (broken links are acceptable)
+ // or this file is not a link
+ break;
}
-
visitCount++;
}
if (visitCount >= MaxFollowedLinks)
{
// We went over the limit and couldn't reach the final target
- throw new IOException(SR.Format(SR.IndexOutOfRange_SymbolicLinkLevels, linkPath));
+ throw new IOException(SR.Format(SR.IO_TooManySymbolicLinkLevels, linkPath));
}
- Debug.Assert(targetPath != null);
+ Debug.Assert(sb.Length > 0);
return isDirectory ?
- new DirectoryInfo(targetPath) :
- new FileInfo(targetPath);
+ new DirectoryInfo(sb.ToString()) :
+ new FileInfo(sb.ToString()); // ToString disposes
+
+ static bool TryGetLinkTarget(ref ValueStringBuilder sb)
+ {
+ string? linkTarget = GetLinkTarget(sb.AsSpan(), isDirectory: false /* Irrelevant in Unix */);
+ if (string.IsNullOrEmpty(linkTarget))
+ {
+ // Either linkPath does not exist
+ // or linkPath is not a link
+ return false;
+ }
+
+ if (PathInternal.IsPartiallyQualified(linkTarget.AsSpan()))
+ {
+ // Preserve the full path of the directory of the previous file
+ // so the final target is returned with a valid full path
+ sb.Length = Path.GetDirectoryNameOffset(sb.AsSpan());
+ sb.Append(PathInternal.DirectorySeparatorChar);
+ }
+ else
+ {
+ sb.Length = 0;
+ }
+ sb.Append(linkTarget.AsSpan());
+
+ return true;
+ }
}
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs
index 104704de7b7154..cdc96b5f45228c 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs
@@ -125,7 +125,7 @@ public static ReadOnlySpan GetDirectoryName(ReadOnlySpan path)
return end >= 0 ? path.Slice(0, end) : ReadOnlySpan.Empty;
}
- private static int GetDirectoryNameOffset(ReadOnlySpan path)
+ internal static int GetDirectoryNameOffset(ReadOnlySpan path)
{
int rootLength = PathInternal.GetRootLength(path);
int end = path.Length;
From 8ed27725d69493b8e17bde82c90d80e5bc2ca5c2 Mon Sep 17 00:00:00 2001
From: carlossanlop
Date: Thu, 17 Jun 2021 15:04:11 -0700
Subject: [PATCH 05/50] Build failure in FileSystemWatcher csproj due to
missing new interop dependency.
---
.../src/System.IO.FileSystem.Watcher.csproj | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/libraries/System.IO.FileSystem.Watcher/src/System.IO.FileSystem.Watcher.csproj b/src/libraries/System.IO.FileSystem.Watcher/src/System.IO.FileSystem.Watcher.csproj
index e0f8628d0b0b1a..b6b0579e63026f 100644
--- a/src/libraries/System.IO.FileSystem.Watcher/src/System.IO.FileSystem.Watcher.csproj
+++ b/src/libraries/System.IO.FileSystem.Watcher/src/System.IO.FileSystem.Watcher.csproj
@@ -79,6 +79,8 @@
+
Date: Wed, 16 Jun 2021 19:51:38 -0700
Subject: [PATCH 06/50] Fix build failure. Address documentation suggestions.
---
.../Kernel32/Interop.REPARSE_DATA_BUFFER.cs | 26 +++++++++----------
.../src/System.Diagnostics.Process.csproj | 4 +++
.../src/System.Net.Ping.csproj | 4 +++
.../src/Resources/Strings.resx | 2 +-
.../src/System/IO/Directory.cs | 8 +++++-
.../src/System/IO/File.cs | 12 ++++++---
.../src/System/IO/FileSystem.Unix.cs | 2 +-
.../src/System/IO/FileSystemInfo.cs | 9 +++++--
...urity.Cryptography.X509Certificates.csproj | 4 +++
9 files changed, 49 insertions(+), 22 deletions(-)
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 a91498c283caa2..3bcb9162d57bfc 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
@@ -9,28 +9,28 @@ internal static partial class Interop
internal static partial class Kernel32
{
// https://docs.microsoft.com/windows-hardware/drivers/ifs/fsctl-get-reparse-point
- public const int MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16 * 1024;
+ internal const int MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16 * 1024;
- public const uint SYMLINK_FLAG_RELATIVE = 1;
+ 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)]
- public unsafe struct REPARSE_DATA_BUFFER
+ internal unsafe struct REPARSE_DATA_BUFFER
{
- public uint ReparseTag;
- public ushort ReparseDataLength;
- public ushort Reserved;
- public SymbolicLinkReparseBuffer ReparseBufferSymbolicLink;
+ internal uint ReparseTag;
+ internal ushort ReparseDataLength;
+ internal ushort Reserved;
+ internal SymbolicLinkReparseBuffer ReparseBufferSymbolicLink;
[StructLayout(LayoutKind.Sequential)]
- public struct SymbolicLinkReparseBuffer
+ internal struct SymbolicLinkReparseBuffer
{
- public ushort SubstituteNameOffset;
- public ushort SubstituteNameLength;
- public ushort PrintNameOffset;
- public ushort PrintNameLength;
- public uint Flags;
+ internal ushort SubstituteNameOffset;
+ internal ushort SubstituteNameLength;
+ internal ushort PrintNameOffset;
+ internal ushort PrintNameLength;
+ internal uint Flags;
}
}
}
diff --git a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj
index cdd65c22c44315..552f68fb857116 100644
--- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj
+++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj
@@ -233,6 +233,8 @@
Link="Common\Interop\Unix\Interop.Errors.cs" />
+
+
+
+
diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx
index 82893fbe72c143..48b367ffc327f0 100644
--- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx
+++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx
@@ -2660,7 +2660,7 @@
BindHandle for ThreadPool failed on this handle.
- The link file system type is inconsistent with the link target system type.
+ The link's file system entry type is inconsistent with that of its target.The file '{0}' already exists.
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs
index fc0022befa88fb..6dafaada1a1a63 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs
@@ -344,7 +344,13 @@ public static FileSystemInfo CreateSymbolicLink(string path, string pathToTarget
///
/// The path of the directory link.
/// to follow links to the final target; to return the immediate next link.
- /// A instance if exists, independently if the target exists or not. if does not exist.
+ /// A instance if exists, independently if the target exists or not. if is not a link.
+ /// The directory on does not exist.
+ /// -or-
+ /// The link's file system entry type is inconsistent with that of its target.
+ /// -or-
+ /// Too many levels of symbolic links.
+ /// When is , the maximum number of symbolic links that are followed are 40 on Unix.
public static System.IO.FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false)
{
FileSystem.VerifyValidPath(linkPath, nameof(linkPath));
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs
index 9bf27e7252a777..72116173a046d6 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs
@@ -1017,9 +1017,7 @@ private static async Task InternalWriteAllTextAsync(StreamWriter sw, string cont
/// or is .
/// or is empty.
/// -or-
- /// is not an absolute path.
- /// -or-
- /// or contains invalid path characters.
+ /// or contains a null character.
/// A file or directory already exists in the location of .
/// -or-
/// An I/O error occurred.
@@ -1036,7 +1034,13 @@ public static FileSystemInfo CreateSymbolicLink(string path, string pathToTarget
///
/// The path of the file link.
/// to follow links to the final target; to return the immediate next link.
- /// A instance if exists, independently if the target exists or not. if does not exist.
+ /// A instance if exists, independently if the target exists or not. if is not a link.
+ /// The file on does not exist.
+ /// -or-
+ /// The link's file system entry type is inconsistent with that of its target.
+ /// -or-
+ /// Too many levels of symbolic links.
+ /// When is , the maximum number of symbolic links that are followed are 40 on Unix.
public static System.IO.FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false)
{
FileSystem.VerifyValidPath(linkPath, nameof(linkPath));
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs
index 313545557f87b6..4f27dd8cd68e88 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs
@@ -573,7 +573,7 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i
/// True if the linkPath points to a directory or a symlink to a directory.
/// If the specified linkPath represents a link file and it exists, returns a FileInfo if isDirectory
/// is false, or a DirectoryInfo if isDirectory is true, independently if the target file/directory exists or not.
- /// If the specified linkPath is not a link file or it does not exist, returns null.
+ /// If the specified linkPath is not a link, returns null. Throws if the file or directory in linkPath does not exist.
internal static FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget, bool isDirectory)
{
// throws if the current link file does not exist
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs
index 447c54f1ffa9a7..6f3df4cf72de67 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs
@@ -145,8 +145,13 @@ public void CreateAsSymbolicLink(string pathToTarget)
/// Gets the target of the specified link.
///
/// to follow links to the final target; to return the immediate next link.
- /// A instance if the link exists, independently if the target exists or not; if a link does not exist
- /// in , or this instance does not represent a link.
+ /// A instance if the link exists, independently if the target exists or not; if this file or directory is not a link.
+ /// The file or directory does not exist.
+ /// -or-
+ /// The link's file system entry type is inconsistent with that of its target.
+ /// -or-
+ /// Too many levels of symbolic links.
+ /// When is , the maximum number of symbolic links that are followed are 40 on Unix.
public FileSystemInfo? ResolveLinkTarget(bool returnFinalTarget = false) =>
FileSystem.ResolveLinkTarget(FullPath, returnFinalTarget, this is DirectoryInfo);
diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/System.Security.Cryptography.X509Certificates.csproj b/src/libraries/System.Security.Cryptography.X509Certificates/src/System.Security.Cryptography.X509Certificates.csproj
index 2c0921f9fcd93c..505d487f1d486b 100644
--- a/src/libraries/System.Security.Cryptography.X509Certificates/src/System.Security.Cryptography.X509Certificates.csproj
+++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/System.Security.Cryptography.X509Certificates.csproj
@@ -324,10 +324,14 @@
Link="Common\Interop\Unix\Interop.Libraries.cs" />
+
+
Date: Thu, 17 Jun 2021 17:23:02 -0700
Subject: [PATCH 07/50] Add missing csproj references to
System.Net.Ping.Functional.Tests (project that is using ReadLink).
---
.../FunctionalTests/System.Net.Ping.Functional.Tests.csproj | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/libraries/System.Net.Ping/tests/FunctionalTests/System.Net.Ping.Functional.Tests.csproj b/src/libraries/System.Net.Ping/tests/FunctionalTests/System.Net.Ping.Functional.Tests.csproj
index 86a7dacccaefdf..90acc7605d8ab8 100644
--- a/src/libraries/System.Net.Ping/tests/FunctionalTests/System.Net.Ping.Functional.Tests.csproj
+++ b/src/libraries/System.Net.Ping/tests/FunctionalTests/System.Net.Ping.Functional.Tests.csproj
@@ -19,6 +19,8 @@
Link="SocketCommon\Configuration.cs" />
+
+
From 86cbd19584fde30e3554cc2253b78f6d36d782fe Mon Sep 17 00:00:00 2001
From: carlossanlop
Date: Fri, 18 Jun 2021 13:09:42 -0700
Subject: [PATCH 08/50] Move CanCreateSymbolicLinks from FileSystemWatcher one
class above, so FileSystem can consume that method. Delete the duplicate
CanCreateSymbolicLinks method that currently fails in the CI.
---
.../System/IO/FileCleanupTestBase.cs | 61 ++++++++++++++++++
.../tests/Utility/FileSystemWatcherTest.cs | 62 -------------------
.../tests/FileSystemTest.cs | 26 --------
3 files changed, 61 insertions(+), 88 deletions(-)
diff --git a/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs b/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs
index 02ffa607c94aa7..b53bafd345879b 100644
--- a/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs
+++ b/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs
@@ -127,5 +127,66 @@ private string GenerateTestFileName(int? index, string memberName, int lineNumbe
lineNumber,
index.GetValueOrDefault(),
Guid.NewGuid().ToString("N").Substring(0, 8)); // randomness to avoid collisions between derived test classes using same base method concurrently
+
+ ///
+ /// In some cases (such as when running without elevated privileges),
+ /// the symbolic link may fail to create. Only run this test if it creates
+ /// links successfully.
+ ///
+ protected static bool CanCreateSymbolicLinks => s_canCreateSymbolicLinks.Value;
+
+ private static readonly Lazy s_canCreateSymbolicLinks = new Lazy(() =>
+ {
+ bool success = true;
+
+ // Verify file symlink creation
+ string path = Path.GetTempFileName();
+ string linkPath = path + ".link";
+ success = CreateSymLink(path, linkPath, isDirectory: false);
+ try { File.Delete(path); } catch { }
+ try { File.Delete(linkPath); } catch { }
+
+ // Verify directory symlink creation
+ path = Path.GetTempFileName();
+ linkPath = path + ".link";
+ success = success && CreateSymLink(path, linkPath, isDirectory: true);
+ try { Directory.Delete(path); } catch { }
+ try { Directory.Delete(linkPath); } catch { }
+
+ return success;
+ });
+
+ protected static bool CreateSymLink(string targetPath, string linkPath, bool isDirectory)
+ {
+ if (OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsMacCatalyst()) // OSes that don't support Process.Start()
+ {
+ return false;
+ }
+
+ Process symLinkProcess = new Process();
+ if (OperatingSystem.IsWindows())
+ {
+ symLinkProcess.StartInfo.FileName = "cmd";
+ symLinkProcess.StartInfo.Arguments = string.Format("/c mklink{0} \"{1}\" \"{2}\"", isDirectory ? " /D" : "", Path.GetFullPath(linkPath), Path.GetFullPath(targetPath));
+ }
+ else
+ {
+ symLinkProcess.StartInfo.FileName = "/bin/ln";
+ symLinkProcess.StartInfo.Arguments = string.Format("-s \"{0}\" \"{1}\"", Path.GetFullPath(targetPath), Path.GetFullPath(linkPath));
+ }
+ symLinkProcess.StartInfo.RedirectStandardOutput = true;
+ symLinkProcess.Start();
+
+ if (symLinkProcess != null)
+ {
+ symLinkProcess.WaitForExit();
+ return (0 == symLinkProcess.ExitCode);
+ }
+ else
+ {
+ return false;
+ }
+ }
+
}
}
diff --git a/src/libraries/System.IO.FileSystem.Watcher/tests/Utility/FileSystemWatcherTest.cs b/src/libraries/System.IO.FileSystem.Watcher/tests/Utility/FileSystemWatcherTest.cs
index bdfaf509d68244..53c4d71f7a1a78 100644
--- a/src/libraries/System.IO.FileSystem.Watcher/tests/Utility/FileSystemWatcherTest.cs
+++ b/src/libraries/System.IO.FileSystem.Watcher/tests/Utility/FileSystemWatcherTest.cs
@@ -430,68 +430,6 @@ public static bool TryErrorEvent(FileSystemWatcher watcher, Action action, Actio
return result;
}
- ///
- /// In some cases (such as when running without elevated privileges),
- /// the symbolic link may fail to create. Only run this test if it creates
- /// links successfully.
- ///
- protected static bool CanCreateSymbolicLinks
- {
- get
- {
- bool success = true;
-
- // Verify file symlink creation
- string path = Path.GetTempFileName();
- string linkPath = path + ".link";
- success = CreateSymLink(path, linkPath, isDirectory: false);
- try { File.Delete(path); } catch { }
- try { File.Delete(linkPath); } catch { }
-
- // Verify directory symlink creation
- path = Path.GetTempFileName();
- linkPath = path + ".link";
- success = success && CreateSymLink(path, linkPath, isDirectory: true);
- try { Directory.Delete(path); } catch { }
- try { Directory.Delete(linkPath); } catch { }
-
- return success;
- }
- }
-
- public static bool CreateSymLink(string targetPath, string linkPath, bool isDirectory)
- {
- if (OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsMacCatalyst()) // OSes that don't support Process.Start()
- {
- return false;
- }
-
- Process symLinkProcess = new Process();
- if (OperatingSystem.IsWindows())
- {
- symLinkProcess.StartInfo.FileName = "cmd";
- symLinkProcess.StartInfo.Arguments = string.Format("/c mklink{0} \"{1}\" \"{2}\"", isDirectory ? " /D" : "", Path.GetFullPath(linkPath), Path.GetFullPath(targetPath));
- }
- else
- {
- symLinkProcess.StartInfo.FileName = "/bin/ln";
- symLinkProcess.StartInfo.Arguments = string.Format("-s \"{0}\" \"{1}\"", Path.GetFullPath(targetPath), Path.GetFullPath(linkPath));
- }
- symLinkProcess.StartInfo.RedirectStandardOutput = true;
- symLinkProcess.Start();
-
- if (symLinkProcess != null)
- {
- symLinkProcess.WaitForExit();
- return (0 == symLinkProcess.ExitCode);
- }
- else
- {
- return false;
- }
- }
-
-
public static IEnumerable