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 FilterTypes() { foreach (NotifyFilters filter in Enum.GetValues(typeof(NotifyFilters))) diff --git a/src/libraries/System.IO.FileSystem/tests/FileSystemTest.cs b/src/libraries/System.IO.FileSystem/tests/FileSystemTest.cs index d82346f6f4f10a..9300660f1418a2 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileSystemTest.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileSystemTest.cs @@ -55,32 +55,6 @@ public static TheoryData TrailingSeparators } } - /// - /// 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(() => - { - // Verify file symlink creation - string path = Path.GetTempFileName(); - string linkPath = path + ".link"; - bool success = MountHelper.CreateSymbolicLink(linkPath, path, isDirectory: false); - try { File.Delete(path); } catch { } - try { File.Delete(linkPath); } catch { } - - // Verify directory symlink creation - path = Path.GetTempFileName(); - linkPath = path + ".link"; - success = success && MountHelper.CreateSymbolicLink(linkPath, path, isDirectory: true); - try { Directory.Delete(path); } catch { } - try { Directory.Delete(linkPath); } catch { } - - return success; - }); - public static string GetNamedPipeServerStreamName() { if (PlatformDetection.IsInAppContainer) From 480d3dff340b6e611369d13b5c66aec595d6e011 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 18 Jun 2021 15:26:33 -0700 Subject: [PATCH 09/50] Fix some Microsoft.IO.Redist CI failures. --- .../src/Microsoft.IO.Redist.csproj | 12 +++++++++++- .../Microsoft.IO.Redist/src/Resources/Strings.resx | 3 +++ .../src/System/IO/Directory.cs | 2 +- .../System.Private.CoreLib/src/System/IO/File.cs | 2 +- .../src/System/IO/FileSystem.cs | 3 +++ 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj b/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj index db9472c1432f36..f22780b1a5e6e7 100644 --- a/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj +++ b/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj @@ -1,4 +1,4 @@ - + net472 $(DefineConstants);MS_IO_REDIST @@ -34,6 +34,8 @@ Link="Microsoft\IO\File.cs" /> + + + + + The file is too long. This operation is currently limited to supporting files less than 2 gigabytes in size. + + The link's file system entry type is inconsistent with that of its target. + Could not find a part of the path. 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 6dafaada1a1a63..78dd5d17b51e02 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs @@ -351,7 +351,7 @@ public static FileSystemInfo CreateSymbolicLink(string path, string pathToTarget /// -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) + public static 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 72116173a046d6..038d8fd990edf8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs @@ -1041,7 +1041,7 @@ public static FileSystemInfo CreateSymbolicLink(string path, string pathToTarget /// -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) + public static 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.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs index 2c4710636e1c86..1e30d42fe61477 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs @@ -1,7 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; + #if MS_IO_REDIST + namespace Microsoft.IO #else namespace System.IO From a6cd9da99b124dc79f2316a837dd199ae69df3bf Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 18 Jun 2021 16:08:54 -0700 Subject: [PATCH 10/50] Fix CI build failures from Net5Compat.Tests --- .../System/IO/FileCleanupTestBase.cs | 18 ++++++++---- .../BaseSymbolicLinks.FileSystem.cs | 28 ++++++++++++++++++- ...stem.IO.FileSystem.Net5Compat.Tests.csproj | 3 ++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs b/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs index b53bafd345879b..cb5d642abd5c09 100644 --- a/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs +++ b/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs @@ -158,22 +158,27 @@ private string GenerateTestFileName(int? index, string memberName, int lineNumbe protected static bool CreateSymLink(string targetPath, string linkPath, bool isDirectory) { +#if NETFRAMEWORK + Process symLinkProcess = new Process(); + symLinkProcess.StartInfo = GetWindowsStartInfo(targetPath, linkPath, isDirectory); +#else 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)); + symLinkProcess.StartInfo = GetWindowsStartInfo(targetPath, linkPath, isDirectory); } else { symLinkProcess.StartInfo.FileName = "/bin/ln"; symLinkProcess.StartInfo.Arguments = string.Format("-s \"{0}\" \"{1}\"", Path.GetFullPath(targetPath), Path.GetFullPath(linkPath)); } + +#endif symLinkProcess.StartInfo.RedirectStandardOutput = true; symLinkProcess.Start(); @@ -182,9 +187,12 @@ protected static bool CreateSymLink(string targetPath, string linkPath, bool isD symLinkProcess.WaitForExit(); return (0 == symLinkProcess.ExitCode); } - else + + return false; + + static ProcessStartInfo GetWindowsStartInfo(string targetPath, string linkPath, bool isDirectory) { - return false; + return new ProcessStartInfo("cmd", string.Format("/c mklink{0} \"{1}\" \"{2}\"", isDirectory ? " /D" : "", Path.GetFullPath(linkPath), Path.GetFullPath(targetPath))); } } 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 3960b98db8b8b4..c0a0f5d9b0cb54 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 @@ -1,7 +1,7 @@ // 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 Microsoft.DotNet.RemoteExecutor; using Xunit; namespace System.IO.Tests @@ -256,6 +256,23 @@ public void DetectSymbolicLinkCycle() Assert.Throws(() => ResolveLinkTarget(link2Path, expectedLinkTarget: link1Path, returnFinalTarget: true)); } + [Fact] + public void DetectLinkReferenceToSelf() + { + // link + // ^ \ + // \___/ + + string linkPath = GetRandomFilePath(); + FileSystemInfo linkInfo = CreateSymbolicLink(linkPath, linkPath); + + // Can get target without following symlinks + FileSystemInfo linkTarget = ResolveLinkTarget(linkPath, expectedLinkTarget: linkPath); + + // Cannot get target when following symlinks + Assert.Throws(() => ResolveLinkTarget(linkPath, expectedLinkTarget: linkPath, returnFinalTarget: true)); + } + [Fact] public void CreateSymbolicLink_WrongTargetType() { @@ -344,5 +361,14 @@ private void ResolveLinkTarget_ReturnFinalTarget(string link1Path, string expect Assert.False(finalTarget.Attributes.HasFlag(FileAttributes.ReparsePoint)); AssertPathEquals(filePath, finalTarget.FullName); } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void ExecWait_OutputCaptured() + { + RemoteExecutor.Invoke(() => + { + + }).Dispose(); + } } } diff --git a/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj index e9ab0a7365e303..e828b4892a3315 100644 --- a/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj +++ b/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj @@ -27,6 +27,9 @@ + + + From 6952eb7d9c3c9552a4d47eab8924c41e3438587f Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 21 Jun 2021 13:11:26 -0700 Subject: [PATCH 11/50] Clean CanCreateSymbolicLinks for readability. Remove incomplete code. --- .../System/IO/FileCleanupTestBase.cs | 18 ++++++------------ .../BaseSymbolicLinks.FileSystem.cs | 9 --------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs b/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs index cb5d642abd5c09..6da7e85825cb93 100644 --- a/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs +++ b/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs @@ -159,26 +159,25 @@ private string GenerateTestFileName(int? index, string memberName, int lineNumbe protected static bool CreateSymLink(string targetPath, string linkPath, bool isDirectory) { #if NETFRAMEWORK - Process symLinkProcess = new Process(); - symLinkProcess.StartInfo = GetWindowsStartInfo(targetPath, linkPath, isDirectory); + bool isWindows = true; #else if (OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsMacCatalyst()) // OSes that don't support Process.Start() { return false; } + bool isWindows = OperatingSystem.IsWindows(); +#endif Process symLinkProcess = new Process(); - - if (OperatingSystem.IsWindows()) + if (isWindows) { - symLinkProcess.StartInfo = GetWindowsStartInfo(targetPath, linkPath, isDirectory); + 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)); } - -#endif symLinkProcess.StartInfo.RedirectStandardOutput = true; symLinkProcess.Start(); @@ -189,11 +188,6 @@ protected static bool CreateSymLink(string targetPath, string linkPath, bool isD } return false; - - static ProcessStartInfo GetWindowsStartInfo(string targetPath, string linkPath, bool isDirectory) - { - return new ProcessStartInfo("cmd", string.Format("/c mklink{0} \"{1}\" \"{2}\"", isDirectory ? " /D" : "", Path.GetFullPath(linkPath), Path.GetFullPath(targetPath))); - } } } 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 c0a0f5d9b0cb54..5d094f389162c0 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 @@ -361,14 +361,5 @@ private void ResolveLinkTarget_ReturnFinalTarget(string link1Path, string expect Assert.False(finalTarget.Attributes.HasFlag(FileAttributes.ReparsePoint)); AssertPathEquals(filePath, finalTarget.FullName); } - - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public void ExecWait_OutputCaptured() - { - RemoteExecutor.Invoke(() => - { - - }).Dispose(); - } } } From 797ace513deffb45d0d8dcd12bb3dfc2dcf26e08 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Fri, 18 Jun 2021 16:38:28 -0700 Subject: [PATCH 12/50] Support long paths in GetFinalPathNameByHandle --- .../src/System/IO/FileSystem.Windows.cs | 80 +++++++++++++++---- 1 file changed, 65 insertions(+), 15 deletions(-) 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 e3bc53a222ac34..74c65a002f7687 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 @@ -466,10 +466,9 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i } } - byte[]? buffer = null; + byte[]? buffer = ArrayPool.Shared.Rent(Interop.Kernel32.MAXIMUM_REPARSE_DATA_BUFFER_SIZE); ; 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, @@ -536,32 +535,83 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i return null; } + // We try to open the final file, not the Reparse Point. using SafeFileHandle handle = OpenSafeFileHandle(linkPath, Interop.Kernel32.FileOperations.OPEN_EXISTING | Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS); if (handle.IsInvalid) { - Win32Marshal.GetExceptionForLastWin32Error(linkPath); + // If the handle fails with "not found", is becuse the link was broken. + // We need to fallback to manually traverse the link targets and return the target of the last resolved link. + int error = Marshal.GetLastWin32Error(); + if (error == Interop.Errors.ERROR_FILE_NOT_FOUND || + error == Interop.Errors.ERROR_PATH_NOT_FOUND) + { + return GetFinalLinkTargetSlow(linkPath); + } + + throw Win32Marshal.GetExceptionForWin32Error(error, 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); + const int InitialBufferSize = 4096; + char[]? buffer = ArrayPool.Shared.Rent(InitialBufferSize); + try + { + uint result = GetFinalPathNameByHandle(handle, buffer); + + // 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. + if (result > InitialBufferSize) + { + ArrayPool.Shared.Return(buffer); + buffer = null; + buffer = ArrayPool.Shared.Rent((int)result); + + result = GetFinalPathNameByHandle(handle, buffer); + } - // 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 (result == 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)result); + } + finally + { + if (buffer != null) + { + ArrayPool.Shared.Return(buffer); + } + } - // If the function fails for any other reason, the return value is zero. - if (res == 0) + uint GetFinalPathNameByHandle(SafeFileHandle handle, char[] buffer) { - throw Win32Marshal.GetExceptionForLastWin32Error(linkPath); + fixed (char* bufPtr = buffer) + { + return Interop.Kernel32.GetFinalPathNameByHandle(handle, bufPtr, (uint)buffer.Length, Interop.Kernel32.FILE_NAME_NORMALIZED); + } } - // 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); + string? GetFinalLinkTargetSlow(string linkPath) + { + // Since all these paths will be passed to CreateFile, which takes a string anyway, it is pointless to use span. + // I am not sure if it's possible to change CreateFile's param to ROS and avoid all these allocations. + string? current = GetImmediateLinkTarget(linkPath, isDirectory, throwOnNotFound: false, normalize: true); + string? prev = null; + + while (current != null) + { + prev = current; + current = GetImmediateLinkTarget(current, isDirectory, throwOnNotFound: false, normalize: true); + } + + return prev; + } } private static unsafe SafeFileHandle OpenSafeFileHandle(string path, int flags) From 8639e3b3a2e444ac178713e20e0ceb20c8e26d84 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Fri, 18 Jun 2021 18:10:55 -0700 Subject: [PATCH 13/50] Use MS.IO.Redist friendly APIs for marshalling --- .../System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 74c65a002f7687..e62426f64f75f7 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 @@ -492,7 +492,8 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i } ReadOnlySpan bufferSpan = new(buffer); - ref readonly Interop.Kernel32.REPARSE_DATA_BUFFER rdb = ref MemoryMarshal.AsRef(bufferSpan); + success = MemoryMarshal.TryRead(bufferSpan, out Interop.Kernel32.REPARSE_DATA_BUFFER rdb); + Debug.Assert(success); // Only symbolic links are supported at the moment. if ((rdb.ReparseTag & Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_SYMLINK) == 0) From 6bb1aa9d8e30f823773c46a0f1040b5be2a7ceaf Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 21 Jun 2021 13:17:00 -0700 Subject: [PATCH 14/50] Add IsBrowser check to CanCreateSymbolicLinks --- .../Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs b/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs index 6da7e85825cb93..98d6f3498624cf 100644 --- a/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs +++ b/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs @@ -161,7 +161,7 @@ protected static bool CreateSymLink(string targetPath, string linkPath, bool isD #if NETFRAMEWORK bool isWindows = true; #else - if (OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsMacCatalyst()) // OSes that don't support Process.Start() + if (OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsMacCatalyst() || OperatingSystem.IsBrowser()) // OSes that don't support Process.Start() { return false; } From 88bcadbcdd287c09493fe373efc10f5116f3e4a4 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Mon, 21 Jun 2021 20:56:06 -0700 Subject: [PATCH 15/50] Add more tests and fix some issues in windows --- .../Kernel32/Interop.CreateSymbolicLink.cs | 12 +- .../BaseSymbolicLinks.FileSystem.cs | 229 ++++++++++++------ .../BaseSymbolicLinks.FileSystemInfo.cs | 53 ++-- .../tests/Directory/SymbolicLinks.cs | 19 +- .../tests/DirectoryInfo/SymbolicLinks.cs | 17 +- .../tests/File/SymbolicLinks.cs | 17 +- .../tests/FileInfo/SymbolicLinks.cs | 15 +- .../src/System/IO/FileSystem.Windows.cs | 29 ++- 8 files changed, 247 insertions(+), 144 deletions(-) diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs index 64c789b026014d..4c441d42f2f692 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs @@ -9,11 +9,6 @@ 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. /// @@ -41,8 +36,11 @@ internal static bool CreateSymbolicLink(string symlinkFileName, string targetFil 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); + int flags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; + if (isDirectory) + { + flags |= SYMBOLIC_LINK_FLAG_DIRECTORY; + } return CreateSymbolicLinkPrivate(symlinkFileName, targetFileName, 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 5d094f389162c0..b061bd64e53176 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 @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.DotNet.RemoteExecutor; +using System.Collections.Generic; using Xunit; namespace System.IO.Tests @@ -9,31 +9,29 @@ namespace System.IO.Tests // Contains test methods that can be used for FileInfo, DirectoryInfo, File or Directory. public abstract class BaseSymbolicLinks_FileSystem : BaseSymbolicLinks { + private const string ExtendedPrefix = @"\\?\"; /// 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); + protected abstract void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo linkInfo); + protected abstract void AssertLinkExists(FileSystemInfo linkInfo); /// 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); + protected abstract FileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget = false); /// - /// Appends \??\ to the absolute path + /// Verifies that FileSystemInfo.LinkTarget matches the specified expected path. + /// If the current platform is Windows and is absolute, this method asserts that starts with \\?\. /// - protected void AssertPathEquals(string expected, string actual) + protected static void AssertLinkTargetEquals(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)) +#if WINDOWS + if (Path.IsPathFullyQualified(actual)) { - actual = actual.Substring(4); + actual.StartsWith(ExtendedPrefix); + expected = Path.Join(ExtendedPrefix, expected); } // Windows syscalls remove the redundant segments in the link target path. @@ -42,11 +40,29 @@ protected void AssertPathEquals(string expected, string actual) if (rootLength > 0) { expected = PathInternal.RemoveRelativeSegments(expected, rootLength); - } + } #endif Assert.Equal(expected, actual); } + + /// + /// Asserts that the FullPath of the FileSystemInfo returned by ResolveLinkTarget() matches with the expected path of the file created. + /// Trims the Windows device prefix, in case there's any, before comparing. + /// + private static void AssertFullNameEquals(string expected, string actual) + { +#if WINDOWS + if (PathInternal.IsExtended(actual)) + { + Assert.StartsWith(ExtendedPrefix, actual); + actual = actual.Substring(4); + } +#endif + + Assert.Equal(expected, actual); + } + [Fact] public void CreateSymbolicLink_NullPathToTarget() { @@ -61,6 +77,23 @@ public void CreateSymbolicLink_InvalidPathToTarget(string pathToTarget) Assert.Throws(() => CreateSymbolicLink(GetRandomFilePath(), pathToTarget)); } + [Fact] + public void CreateSymbolicLink_PathToTargetRelativeToLinkPath() + { + string tempFileName = GetRandomFileName(); + + // Create file in our current working directory. + using var tempFile = new TempFile(tempFileName); + + // Create link in our temporary folder with the target using the same name as the file in the current working directory. + // No issues may occour. + FileSystemInfo linkInfo = CreateSymbolicLink(GetRandomLinkPath(), tempFileName); + + FileSystemInfo targetInfo = linkInfo.ResolveLinkTarget(); + Assert.False(targetInfo.Exists); + Assert.Equal(Path.GetDirectoryName(linkInfo.FullName), Path.GetDirectoryName(targetInfo.FullName)); + } + [Fact] public void CreateSymbolicLink_RelativeTargetPath_TargetExists() { @@ -147,6 +180,43 @@ public void CreateSymbolicLink_AbsoluteTargetPath_NonExistentTarget() targetPath: null); // do not create target } + protected void ResolveLinkTarget_Throws_NotExists_Internal() where T : Exception + { + string path = GetRandomFilePath(); + Assert.Throws(() => ResolveLinkTarget(path)); + Assert.Throws(() => ResolveLinkTarget(path, returnFinalTarget: true)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ResolveLinkTarget_ReturnsNull_NotALink(bool returnFinalTarget) + { + string path = GetTestFilePath(); + CreateFileOrDirectory(path); + Assert.Null(ResolveLinkTarget(path, returnFinalTarget)); + } + + [Theory] + [MemberData(nameof(ResolveLinkTarget_PathToTarget_Data))] + public void ResolveLinkTarget_Succeeds(string pathToTarget, bool returnFinalTarget) + { + string linkPath = GetRandomLinkPath(); + FileSystemInfo linkInfo = CreateSymbolicLink(linkPath, pathToTarget); + AssertLinkExists(linkInfo); + AssertIsCorrectTypeAndDirectoryAttribute(linkInfo); + AssertLinkTargetEquals(pathToTarget, linkInfo.LinkTarget); + + FileSystemInfo targetInfo = ResolveLinkTarget(linkPath, returnFinalTarget); + Assert.NotNull(targetInfo); + Assert.False(targetInfo.Exists); + + string expectedTargetFullName = Path.IsPathFullyQualified(pathToTarget) ? + pathToTarget : Path.GetFullPath(Path.Join(Path.GetDirectoryName(linkPath), pathToTarget)); + + AssertFullNameEquals(expectedTargetFullName, targetInfo.FullName); + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -155,7 +225,7 @@ public void ResolveLinkTarget_FileSystemEntryExistsButIsNotALink(bool returnFina string path = GetRandomFilePath(); CreateFileOrDirectory(path); // entry exists as a normal file, not as a link - FileSystemInfo target = ResolveLinkTarget(path, expectedLinkTarget: null, returnFinalTarget); + FileSystemInfo target = ResolveLinkTarget(path, returnFinalTarget); Assert.Null(target); } @@ -168,9 +238,9 @@ public void ResolveLinkTarget_ReturnFinalTarget_Absolute() ResolveLinkTarget_ReturnFinalTarget( link1Path: link1Path, - expectedLink1Target: link2Path, + link1Target: link2Path, link2Path: link2Path, - expectedLink2Target: filePath, + link2Target: filePath, filePath: filePath); } @@ -189,9 +259,9 @@ public void ResolveLinkTarget_ReturnFinalTarget_Absolute_WithRedundantSegments() ResolveLinkTarget_ReturnFinalTarget( link1Path: link1Path, - expectedLink1Target: Path.Join(dirPath, "..", dirName, link2FileName), + link1Target: Path.Join(dirPath, "..", dirName, link2FileName), link2Path: link2Path, - expectedLink2Target: Path.Join(dirPath, "..", dirName, fileName), + link2Target: Path.Join(dirPath, "..", dirName, fileName), filePath: filePath); } @@ -199,7 +269,7 @@ public void ResolveLinkTarget_ReturnFinalTarget_Absolute_WithRedundantSegments() public void ResolveLinkTarget_ReturnFinalTarget_Relative() { string link1Path = GetRandomLinkPath(); - string link2Path = GetRandomLinkPath(); + string link2Path = GetRandomLinkPath(); string filePath = GetRandomFilePath(); string link2FileName = Path.GetFileName(link2Path); @@ -207,9 +277,9 @@ public void ResolveLinkTarget_ReturnFinalTarget_Relative() ResolveLinkTarget_ReturnFinalTarget( link1Path: link1Path, - expectedLink1Target: link2FileName, + link1Target: link2FileName, link2Path: link2Path, - expectedLink2Target: fileName, + link2Target: fileName, filePath: filePath); } @@ -217,7 +287,7 @@ public void ResolveLinkTarget_ReturnFinalTarget_Relative() public void ResolveLinkTarget_ReturnFinalTarget_Relative_WithRedundantSegments() { string link1Path = GetRandomLinkPath(); - string link2Path = GetRandomLinkPath(); + string link2Path = GetRandomLinkPath(); string filePath = GetRandomFilePath(); string dirPath = Path.GetDirectoryName(filePath); @@ -228,9 +298,9 @@ public void ResolveLinkTarget_ReturnFinalTarget_Relative_WithRedundantSegments() ResolveLinkTarget_ReturnFinalTarget( link1Path: link1Path, - expectedLink1Target: Path.Join("..", dirName, link2FileName), + link1Target: Path.Join("..", dirName, link2FileName), link2Path: link2Path, - expectedLink2Target: Path.Join("..", dirName, fileName), + link2Target: Path.Join("..", dirName, fileName), filePath: filePath); } @@ -248,12 +318,12 @@ public void DetectSymbolicLinkCycle() FileSystemInfo link2Info = CreateSymbolicLink(link2Path, link1Path); // Can get targets without following symlinks - FileSystemInfo link1Target = ResolveLinkTarget(link1Path, expectedLinkTarget: link2Path); - FileSystemInfo link2Target = ResolveLinkTarget(link2Path, expectedLinkTarget: link1Path); + FileSystemInfo link1Target = ResolveLinkTarget(link1Path); + FileSystemInfo link2Target = ResolveLinkTarget(link2Path); // Cannot get target when following symlinks - Assert.Throws(() => ResolveLinkTarget(link1Path, expectedLinkTarget: link2Path, returnFinalTarget: true)); - Assert.Throws(() => ResolveLinkTarget(link2Path, expectedLinkTarget: link1Path, returnFinalTarget: true)); + Assert.Throws(() => ResolveLinkTarget(link1Path, returnFinalTarget: true)); + Assert.Throws(() => ResolveLinkTarget(link2Path, returnFinalTarget: true)); } [Fact] @@ -267,10 +337,10 @@ public void DetectLinkReferenceToSelf() FileSystemInfo linkInfo = CreateSymbolicLink(linkPath, linkPath); // Can get target without following symlinks - FileSystemInfo linkTarget = ResolveLinkTarget(linkPath, expectedLinkTarget: linkPath); + FileSystemInfo linkTarget = ResolveLinkTarget(linkPath); // Cannot get target when following symlinks - Assert.Throws(() => ResolveLinkTarget(linkPath, expectedLinkTarget: linkPath, returnFinalTarget: true)); + Assert.Throws(() => ResolveLinkTarget(linkPath, returnFinalTarget: true)); } [Fact] @@ -284,15 +354,6 @@ public void CreateSymbolicLink_WrongTargetType() 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) @@ -306,60 +367,88 @@ private void VerifySymbolicLinkAndResolvedTarget(string linkPath, string expecte if (targetPath == null) { // Behavior different between files and directories when target does not exist - AssertExistsWhenNoTarget(link); + AssertLinkExists(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); + FileSystemInfo target = ResolveLinkTarget(linkPath); AssertIsCorrectTypeAndDirectoryAttribute(target); Assert.True(Path.IsPathFullyQualified(target.FullName)); } - private void ResolveLinkTarget_ReturnFinalTarget(string link1Path, string expectedLink1Target, string link2Path, string expectedLink2Target, string filePath) + /// + /// Creates and Resolves a chain of links. + /// link1 -> link2 -> file + /// + private void ResolveLinkTarget_ReturnFinalTarget(string link1Path, string link1Target, string link2Path, string link2Target, string filePath) { - // link1Path -> expectedLink1Target (created in link2Path) -> expectedLink2Target (created in filePath) + Assert.True(Path.IsPathFullyQualified(link1Path)); + Assert.True(Path.IsPathFullyQualified(link2Path)); + Assert.True(Path.IsPathFullyQualified(filePath)); CreateFileOrDirectory(filePath); - // link2 to target - FileSystemInfo link2 = CreateSymbolicLink(link2Path, expectedLink2Target); - Assert.True(link2.Exists); - Assert.True(link2.Attributes.HasFlag(FileAttributes.ReparsePoint)); - AssertIsCorrectTypeAndDirectoryAttribute(link2); - AssertPathEquals(expectedLink2Target, link2.LinkTarget); + // link2 to file + FileSystemInfo link2Info = CreateSymbolicLink(link2Path, link2Target); + Assert.True(link2Info.Exists); + Assert.True(link2Info.Attributes.HasFlag(FileAttributes.ReparsePoint)); + AssertIsCorrectTypeAndDirectoryAttribute(link2Info); + AssertLinkTargetEquals(link2Target, link2Info.LinkTarget); // link1 to link2 - FileSystemInfo link1 = CreateSymbolicLink(link1Path, expectedLink1Target); - Assert.True(link1.Exists); - Assert.True(link1.Attributes.HasFlag(FileAttributes.ReparsePoint)); - AssertIsCorrectTypeAndDirectoryAttribute(link1); - AssertPathEquals(expectedLink1Target, link1.LinkTarget ); + FileSystemInfo link1Info = CreateSymbolicLink(link1Path, link1Target); + Assert.True(link1Info.Exists); + Assert.True(link1Info.Attributes.HasFlag(FileAttributes.ReparsePoint)); + AssertIsCorrectTypeAndDirectoryAttribute(link1Info); + AssertLinkTargetEquals(link1Target, link1Info.LinkTarget); // link1: do not follow symlinks - FileSystemInfo link1Target = ResolveLinkTarget(link1Path, expectedLink1Target); - Assert.True(link1Target.Exists); - AssertIsCorrectTypeAndDirectoryAttribute(link1Target); - Assert.True(link1Target.Attributes.HasFlag(FileAttributes.ReparsePoint)); - AssertPathEquals(link2Path, link1Target.FullName); - AssertPathEquals(expectedLink2Target, link1Target.LinkTarget); + FileSystemInfo link1TargetInfo = ResolveLinkTarget(link1Path); + Assert.True(link1TargetInfo.Exists); + AssertIsCorrectTypeAndDirectoryAttribute(link1TargetInfo); + Assert.True(link1TargetInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)); + AssertFullNameEquals(link2Path, link1TargetInfo.FullName); + AssertLinkTargetEquals(link2Target, link1TargetInfo.LinkTarget); // link2: do not follow symlinks - FileSystemInfo link2Target = ResolveLinkTarget(link2Path, expectedLink2Target); - Assert.True(link2Target.Exists); - AssertIsCorrectTypeAndDirectoryAttribute(link2Target); - Assert.False(link2Target.Attributes.HasFlag(FileAttributes.ReparsePoint)); - AssertPathEquals(filePath, link2Target.FullName); - Assert.Null(link2Target.LinkTarget); + FileSystemInfo link2TargetInfo = ResolveLinkTarget(link2Path); + Assert.True(link2TargetInfo.Exists); + AssertIsCorrectTypeAndDirectoryAttribute(link2TargetInfo); + Assert.False(link2TargetInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)); + AssertFullNameEquals(filePath, link2TargetInfo.FullName); + Assert.Null(link2TargetInfo.LinkTarget); // link1: follow symlinks - FileSystemInfo finalTarget = ResolveLinkTarget(link1Path, expectedLinkTarget: expectedLink1Target, returnFinalTarget: true); + FileSystemInfo finalTarget = ResolveLinkTarget(link1Path, returnFinalTarget: true); Assert.True(finalTarget.Exists); AssertIsCorrectTypeAndDirectoryAttribute(finalTarget); Assert.False(finalTarget.Attributes.HasFlag(FileAttributes.ReparsePoint)); - AssertPathEquals(filePath, finalTarget.FullName); + AssertFullNameEquals(filePath, finalTarget.FullName); } + + public static IEnumerable ResolveLinkTarget_PathToTarget_Data + { + get + { + foreach(string path in PathToTargetData) + { + yield return new object[] { path, false }; + yield return new object[] { path, true }; + } + } + } + + internal static string[] PathToTargetData => new[] + { + // Non-rooted relative + "foo", ".\\foo", "..\\foo", + // Rooted relative + "\\foo", + // Rooted absolute + Path.Combine(Path.GetTempPath(), "foo") + }; } } 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 1a21adb874199e..129967d542665b 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 @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.Diagnostics; using Xunit; @@ -19,30 +20,46 @@ protected override FileSystemInfo CreateSymbolicLink(string path, string pathToT return link; } - protected override FileSystemInfo ResolveLinkTarget(string linkPath, string? expectedLinkTarget, bool returnFinalTarget = false) + protected override FileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) + => GetFileSystemInfo(linkPath).ResolveLinkTarget(returnFinalTarget); + + [Fact] + public void LinkTarget_ReturnsNull_NotExists() { - FileSystemInfo link = GetFileSystemInfo(linkPath); + FileSystemInfo info = GetFileSystemInfo(GetRandomLinkPath()); + Assert.Null(info.LinkTarget); + } - if (expectedLinkTarget == null) - { - // LinkTarget is null when linkPath does not exist or is not a link - Assert.Null(link.LinkTarget); - } - else - { - AssertPathEquals(expectedLinkTarget, link.LinkTarget); - } + [Fact] + public void LinkTarget_ReturnsNull_NotALink() + { + string path = GetTestFilePath(); + CreateFileOrDirectory(path); + FileSystemInfo info = GetFileSystemInfo(path); - FileSystemInfo? target = link.ResolveLinkTarget(returnFinalTarget); + Assert.True(info.Exists); + Assert.Null(info.LinkTarget); + } + + [Theory] + [MemberData(nameof(LinkTarget_PathToTarget_Data))] + public void LinkTarget_Succeeds(string pathToTarget) + { + FileSystemInfo linkInfo = CreateSymbolicLink(GetRandomLinkPath(), pathToTarget); - // 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) + AssertLinkExists(linkInfo); + AssertLinkTargetEquals(pathToTarget, linkInfo.LinkTarget); + } + + public static IEnumerable LinkTarget_PathToTarget_Data + { + get { - Assert.Null(link.LinkTarget); + foreach (string path in PathToTargetData) + { + yield return new object[] { path }; + } } - - return target; } } } diff --git a/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs index 2df64f9912143f..2ab62bbac111c4 100644 --- a/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs +++ b/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs @@ -23,22 +23,19 @@ protected override void CreateFileOrDirectory(string path, bool createOpposite = protected override FileSystemInfo CreateSymbolicLink(string path, string pathToTarget) => Directory.CreateSymbolicLink(path, pathToTarget); - protected override FileSystemInfo ResolveLinkTarget(string linkPath, string? expectedLinkTarget, bool returnFinalTarget = false) => + protected override FileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) => Directory.ResolveLinkTarget(linkPath, returnFinalTarget); - protected override void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo fsi) + protected override void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo linkInfo) { - if (fsi.Exists) + if (linkInfo.Exists) { - Assert.True(fsi.Attributes.HasFlag(FileAttributes.Directory)); + Assert.True(linkInfo.Attributes.HasFlag(FileAttributes.Directory)); } - Assert.True(fsi is DirectoryInfo); + Assert.True(linkInfo is DirectoryInfo); } - protected override void AssertLinkExists(FileSystemInfo link) => - Assert.False(link.Exists); // For directory symlinks, we return the exists info from the target - - protected override void AssertExistsWhenNoTarget(FileSystemInfo link) + protected override void AssertLinkExists(FileSystemInfo link) { if (PlatformDetection.IsWindows) { @@ -83,7 +80,7 @@ public void EnumerateFileSystemEntries_LinksWithCycles_ShouldNotThrow() } [Fact] - public void ResolveLinkTarget_LinkDoesNotExist() => - ResolveLinkTarget_LinkDoesNotExist_Internal(); + public void ResolveLinkTarget_Throws_NotExists() => + ResolveLinkTarget_Throws_NotExists_Internal(); } } diff --git a/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs index bab96280ff22c1..558dc562b07924 100644 --- a/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs +++ b/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs @@ -23,19 +23,16 @@ protected override void CreateFileOrDirectory(string path, bool createOpposite = } } - protected override void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo fsi) + protected override void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo linkInfo) { - if (fsi.Exists) + if (linkInfo.Exists) { - Assert.True(fsi.Attributes.HasFlag(FileAttributes.Directory)); + Assert.True(linkInfo.Attributes.HasFlag(FileAttributes.Directory)); } - Assert.True(fsi is DirectoryInfo); + Assert.True(linkInfo is DirectoryInfo); } - protected override void AssertLinkExists(FileSystemInfo link) => - Assert.False(link.Exists); // For directory symlinks, we return the exists info from the target - - protected override void AssertExistsWhenNoTarget(FileSystemInfo link) + protected override void AssertLinkExists(FileSystemInfo link) { if (PlatformDetection.IsWindows) { @@ -119,7 +116,7 @@ public void EnumerateFileSystemInfos_LinksWithCycles_ThrowsTooManyLevelsOfSymbol } [Fact] - public void ResolveLinkTarget_LinkDoesNotExist() => - ResolveLinkTarget_LinkDoesNotExist_Internal(); + public void ResolveLinkTarget_Throws_NotExists() => + ResolveLinkTarget_Throws_NotExists_Internal(); } } diff --git a/src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs index a1b275a90564a8..a7a3de98ba1e0b 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs @@ -23,26 +23,23 @@ protected override void CreateFileOrDirectory(string path, bool createOpposite = protected override FileSystemInfo CreateSymbolicLink(string path, string pathToTarget) => File.CreateSymbolicLink(path, pathToTarget); - protected override FileSystemInfo ResolveLinkTarget(string linkPath, string? expectedLinkTarget, bool returnFinalTarget = false) => + protected override FileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) => File.ResolveLinkTarget(linkPath, returnFinalTarget); - protected override void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo fsi) + protected override void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo linkInfo) { - if (fsi.Exists) + if (linkInfo.Exists) { - Assert.False(fsi.Attributes.HasFlag(FileAttributes.Directory)); + Assert.False(linkInfo.Attributes.HasFlag(FileAttributes.Directory)); } - Assert.True(fsi is FileInfo); + Assert.True(linkInfo 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(); + public void ResolveLinkTarget_Throws_NotExists() => + ResolveLinkTarget_Throws_NotExists_Internal(); } } diff --git a/src/libraries/System.IO.FileSystem/tests/FileInfo/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/FileInfo/SymbolicLinks.cs index 0218e51a30987e..8d418f787b4440 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileInfo/SymbolicLinks.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileInfo/SymbolicLinks.cs @@ -22,23 +22,20 @@ protected override void CreateFileOrDirectory(string path, bool createOpposite = } } - protected override void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo fsi) + protected override void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo linkInfo) { - if (fsi.Exists) + if (linkInfo.Exists) { - Assert.False(fsi.Attributes.HasFlag(FileAttributes.Directory)); + Assert.False(linkInfo.Attributes.HasFlag(FileAttributes.Directory)); } - Assert.True(fsi is FileInfo); + Assert.True(linkInfo 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(); + public void ResolveLinkTarget_Throws_NotExists() => + ResolveLinkTarget_Throws_NotExists_Internal(); } } 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 e62426f64f75f7..90ccd36541be05 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 @@ -409,16 +409,18 @@ public static string[] GetLogicalDrives() internal static void CreateSymbolicLink(string path, string pathToTarget, bool isDirectory) { + string pathToTargetFullPath = PathInternal.IsPartiallyQualified(pathToTarget) ? + Path.Join(Path.GetDirectoryName(path), pathToTarget) : pathToTarget; + Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA data = default; - FillAttributeInfo(pathToTarget, ref data, returnErrorOnNotFound: false); + FillAttributeInfo(pathToTargetFullPath, 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) + if (!Interop.Kernel32.CreateSymbolicLink(path, pathToTarget, isDirectory)) { throw Win32Marshal.GetExceptionForLastWin32Error(path); } @@ -466,7 +468,7 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i } } - byte[]? buffer = ArrayPool.Shared.Rent(Interop.Kernel32.MAXIMUM_REPARSE_DATA_BUFFER_SIZE); ; + byte[]? buffer = ArrayPool.Shared.Rent(Interop.Kernel32.MAXIMUM_REPARSE_DATA_BUFFER_SIZE); try { bool success = Interop.Kernel32.DeviceIoControl( @@ -491,7 +493,7 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i throw Win32Marshal.GetExceptionForWin32Error(error, linkPath); } - ReadOnlySpan bufferSpan = new(buffer); + Span bufferSpan = new(buffer); success = MemoryMarshal.TryRead(bufferSpan, out Interop.Kernel32.REPARSE_DATA_BUFFER rdb); Debug.Assert(success); @@ -504,11 +506,20 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i int substituteNameOffset = sizeof(Interop.Kernel32.REPARSE_DATA_BUFFER) + rdb.ReparseBufferSymbolicLink.SubstituteNameOffset; int substituteNameLength = rdb.ReparseBufferSymbolicLink.SubstituteNameLength; - ReadOnlySpan targetPath = MemoryMarshal.Cast(bufferSpan.Slice(substituteNameOffset, substituteNameLength)); + Span 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) + if ((rdb.ReparseBufferSymbolicLink.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) == 0) + { + // Target path is absolute. + // DeviceIoControl returns a DOS Device path (\??\) which many APIs don't tolerate. + // We should instead return a Win32 path (\\?\). + Debug.Assert(Path.IsPathFullyQualified(targetPath)); + Debug.Assert(targetPath[1] == '?'); + targetPath[1] = '\\'; + } + else if (normalize) { + // Target path is relative and is for ResolveLinkTarget(), we need to append the link directory. return Path.Join(Path.GetDirectoryName(linkPath.AsSpan()), targetPath); } @@ -536,7 +547,7 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i return null; } - // We try to open the final file, not the Reparse Point. + // We try to open the final file since they asked for the final target. using SafeFileHandle handle = OpenSafeFileHandle(linkPath, Interop.Kernel32.FileOperations.OPEN_EXISTING | Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS); From 870111861f7d8df7ddb7abcdaf983ead6e20e7f3 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 21 Jun 2021 22:33:24 -0700 Subject: [PATCH 16/50] Add path to IO_InconsistentLinkType exception msg --- src/libraries/System.Private.CoreLib/src/Resources/Strings.resx | 2 +- .../System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs | 2 +- .../System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 48b367ffc327f0..17326edd8fdfa6 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's file system entry type is inconsistent with that of its target. + The link's file system entry type is inconsistent with that of its target: {0} The file '{0}' already exists. 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 4f27dd8cd68e88..458759fd26fe64 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 @@ -560,7 +560,7 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i 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); + throw new IOException(SR.Format(SR.IO_InconsistentLinkType, path)); } } 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 90ccd36541be05..6ab1996a80814c 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 @@ -417,7 +417,7 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i if (data.dwFileAttributes != -1 && isDirectory != ((data.dwFileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_DIRECTORY) != 0)) { - throw new IOException(SR.IO_InconsistentLinkType); + throw new IOException(SR.Format(SR.IO_InconsistentLinkType, path)); } if (!Interop.Kernel32.CreateSymbolicLink(path, pathToTarget, isDirectory)) From d10e00c2231fde3fc2b68c92807022862b58b77d Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Mon, 21 Jun 2021 22:34:09 -0700 Subject: [PATCH 17/50] Fix failure in test creating inconsistent file/dir type. --- .../Base/SymbolicLinks/BaseSymbolicLinks.FileSystem.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 b061bd64e53176..84c536b8962448 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 @@ -82,14 +82,14 @@ public void CreateSymbolicLink_PathToTargetRelativeToLinkPath() { string tempFileName = GetRandomFileName(); - // Create file in our current working directory. - using var tempFile = new TempFile(tempFileName); + // Create file or directory inside the current working directory. + CreateFileOrDirectory(tempFileName); // Create link in our temporary folder with the target using the same name as the file in the current working directory. - // No issues may occour. + // No issues may occur. FileSystemInfo linkInfo = CreateSymbolicLink(GetRandomLinkPath(), tempFileName); - FileSystemInfo targetInfo = linkInfo.ResolveLinkTarget(); + Assert.False(targetInfo.Exists); Assert.Equal(Path.GetDirectoryName(linkInfo.FullName), Path.GetDirectoryName(targetInfo.FullName)); } From fa5ff40a1e5450b0d6d236ed9cd2df1b818a791e Mon Sep 17 00:00:00 2001 From: David Cantu Date: Mon, 21 Jun 2021 23:36:38 -0700 Subject: [PATCH 18/50] Fix net48 error --- .../System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6ab1996a80814c..18dc6dbf741695 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 @@ -409,7 +409,7 @@ public static string[] GetLogicalDrives() internal static void CreateSymbolicLink(string path, string pathToTarget, bool isDirectory) { - string pathToTargetFullPath = PathInternal.IsPartiallyQualified(pathToTarget) ? + string pathToTargetFullPath = PathInternal.IsPartiallyQualified(pathToTarget.AsSpan()) ? Path.Join(Path.GetDirectoryName(path), pathToTarget) : pathToTarget; Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA data = default; From 6788db7fe570f60685a0b895dc0bc3cbdc1b70d3 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 22 Jun 2021 14:38:27 -0700 Subject: [PATCH 19/50] Address CreateSymbolicLink initial check on Unix to verify pathToTarget is always relative to linkPath. --- .../System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 458759fd26fe64..34582c1cac5ac2 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 @@ -551,8 +551,11 @@ 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) { + string pathToTargetFullPath = PathInternal.IsPartiallyQualified(pathToTarget.AsSpan()) ? + Path.Join(Path.GetDirectoryName(path), pathToTarget) : 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) + if (Interop.Sys.LStat(pathToTargetFullPath, 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 From 45392b4f4b22472edbd053a4878966157dafb9bc Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 22 Jun 2021 19:44:31 -0700 Subject: [PATCH 20/50] Use RemoteExecutor for a test related to change the current working dir --- .../BaseSymbolicLinks.FileSystem.cs | 44 +++++++++++-------- .../tests/Directory/SymbolicLinks.cs | 8 ++++ .../tests/DirectoryInfo/SymbolicLinks.cs | 8 ++++ .../tests/File/SymbolicLinks.cs | 8 ++++ .../tests/FileInfo/SymbolicLinks.cs | 8 ++++ 5 files changed, 58 insertions(+), 18 deletions(-) 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 84c536b8962448..fad51e96441fc5 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 @@ -77,23 +77,6 @@ public void CreateSymbolicLink_InvalidPathToTarget(string pathToTarget) Assert.Throws(() => CreateSymbolicLink(GetRandomFilePath(), pathToTarget)); } - [Fact] - public void CreateSymbolicLink_PathToTargetRelativeToLinkPath() - { - string tempFileName = GetRandomFileName(); - - // Create file or directory inside the current working directory. - CreateFileOrDirectory(tempFileName); - - // Create link in our temporary folder with the target using the same name as the file in the current working directory. - // No issues may occur. - FileSystemInfo linkInfo = CreateSymbolicLink(GetRandomLinkPath(), tempFileName); - FileSystemInfo targetInfo = linkInfo.ResolveLinkTarget(); - - Assert.False(targetInfo.Exists); - Assert.Equal(Path.GetDirectoryName(linkInfo.FullName), Path.GetDirectoryName(targetInfo.FullName)); - } - [Fact] public void CreateSymbolicLink_RelativeTargetPath_TargetExists() { @@ -429,6 +412,29 @@ private void ResolveLinkTarget_ReturnFinalTarget(string link1Path, string link1T AssertFullNameEquals(filePath, finalTarget.FullName); } + protected void CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(bool createOpposite) + { + string tempCwd = GetRandomDirPath(); + Directory.CreateDirectory(tempCwd); + Directory.SetCurrentDirectory(tempCwd); + + // Create a dummy file or directory in cwd. + string fileOrDirectoryInCwd = GetRandomFileName(); + CreateFileOrDirectory(fileOrDirectoryInCwd, createOpposite); + + string oneLevelUpPath = Path.Combine(tempCwd, "one-level-up"); + Directory.CreateDirectory(oneLevelUpPath); + string linkPath = Path.Combine(oneLevelUpPath, GetRandomLinkName()); + + // Create a link with a similar Target Path to the one of our dummy file or directory. + FileSystemInfo linkInfo = CreateSymbolicLink(linkPath, fileOrDirectoryInCwd); + FileSystemInfo targetInfo = linkInfo.ResolveLinkTarget(); + + // Verify that Target is resolved and is relative to Link's directory and not to the cwd. + Assert.False(targetInfo.Exists); + Assert.Equal(Path.GetDirectoryName(linkInfo.FullName), Path.GetDirectoryName(targetInfo.FullName)); + } + public static IEnumerable ResolveLinkTarget_PathToTarget_Data { get @@ -448,7 +454,9 @@ public static IEnumerable ResolveLinkTarget_PathToTarget_Data // Rooted relative "\\foo", // Rooted absolute - Path.Combine(Path.GetTempPath(), "foo") + Path.Combine(Path.GetTempPath(), "foo"), + //Path.Combine(@"\\?\", Path.GetTempPath(), "foo"), + //@"\\server\share\path", @"\\.\pipe\foo", }; } } diff --git a/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs index 2ab62bbac111c4..f096baec1617b9 100644 --- a/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs +++ b/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; +using Microsoft.DotNet.RemoteExecutor; using Xunit; namespace System.IO.Tests @@ -82,5 +83,12 @@ public void EnumerateFileSystemEntries_LinksWithCycles_ShouldNotThrow() [Fact] public void ResolveLinkTarget_Throws_NotExists() => ResolveLinkTarget_Throws_NotExists_Internal(); + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void CreateSymbolicLink_PathToTarget_RelativeToLinkPath() + { + RemoteExecutor.Invoke(() => CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(false)).Dispose(); + RemoteExecutor.Invoke(() => CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(true)).Dispose(); + } } } diff --git a/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs index 558dc562b07924..381a4879bac2a6 100644 --- a/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs +++ b/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; +using Microsoft.DotNet.RemoteExecutor; using Xunit; namespace System.IO.Tests @@ -118,5 +119,12 @@ public void EnumerateFileSystemInfos_LinksWithCycles_ThrowsTooManyLevelsOfSymbol [Fact] public void ResolveLinkTarget_Throws_NotExists() => ResolveLinkTarget_Throws_NotExists_Internal(); + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void CreateSymbolicLink_PathToTarget_RelativeToLinkPath() + { + RemoteExecutor.Invoke(() => CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(false)).Dispose(); + RemoteExecutor.Invoke(() => CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(true)).Dispose(); + } } } diff --git a/src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs index a7a3de98ba1e0b..1f39fdcf11e7d1 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using Microsoft.DotNet.RemoteExecutor; using Xunit; namespace System.IO.Tests @@ -41,5 +42,12 @@ protected override void AssertLinkExists(FileSystemInfo link) => [Fact] public void ResolveLinkTarget_Throws_NotExists() => ResolveLinkTarget_Throws_NotExists_Internal(); + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void CreateSymbolicLink_PathToTarget_RelativeToLinkPath() + { + RemoteExecutor.Invoke(() => CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(false)).Dispose(); + RemoteExecutor.Invoke(() => CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(true)).Dispose(); + } } } diff --git a/src/libraries/System.IO.FileSystem/tests/FileInfo/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/FileInfo/SymbolicLinks.cs index 8d418f787b4440..a19f700992ee24 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileInfo/SymbolicLinks.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileInfo/SymbolicLinks.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.DotNet.RemoteExecutor; using Xunit; namespace System.IO.Tests @@ -37,5 +38,12 @@ protected override void AssertLinkExists(FileSystemInfo link) => [Fact] public void ResolveLinkTarget_Throws_NotExists() => ResolveLinkTarget_Throws_NotExists_Internal(); + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void CreateSymbolicLink_PathToTarget_RelativeToLinkPath() + { + RemoteExecutor.Invoke(() => CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(false)).Dispose(); + RemoteExecutor.Invoke(() => CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(true)).Dispose(); + } } } From 38b6e8845956e2c5004add3cb5fd59febd676023 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 22 Jun 2021 21:36:39 -0700 Subject: [PATCH 21/50] Add missing argument to exception message in MS.IO.Redist --- src/libraries/Microsoft.IO.Redist/src/Resources/Strings.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.IO.Redist/src/Resources/Strings.resx b/src/libraries/Microsoft.IO.Redist/src/Resources/Strings.resx index 3c267f7f6b9f99..66a414fe02e6d6 100644 --- a/src/libraries/Microsoft.IO.Redist/src/Resources/Strings.resx +++ b/src/libraries/Microsoft.IO.Redist/src/Resources/Strings.resx @@ -72,7 +72,7 @@ The file is too long. This operation is currently limited to supporting files less than 2 gigabytes in size. - The link's file system entry type is inconsistent with that of its target. + The link's file system entry type is inconsistent with that of its target: {0} Could not find a part of the path. From a70cdef67438fe0f7811a834407b6060e0912234 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Tue, 22 Jun 2021 21:38:15 -0700 Subject: [PATCH 22/50] nit: Clean comment on CreateSymbolicLink p/invoke. --- .../src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs index 4c441d42f2f692..a07005ba5c55b3 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs @@ -15,7 +15,7 @@ internal static partial class Kernel32 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. + /// Allows creation of symbolic links from a process that is not elevated. Requires Windows 10 Insiders build 14972 or later. /// Developer Mode must first be enabled on the machine before this option will function. /// internal const int SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE = 0x2; From cbcf7af23f79ab8f11d696760da220e15be88765 Mon Sep 17 00:00:00 2001 From: carlossanlop Date: Fri, 25 Jun 2021 18:28:11 -0700 Subject: [PATCH 23/50] Fix CI bug in Windows Nano where %TEMP% points to "C:\Windows\TEMP" but the actual folder name is "C:\Windows\Temp", which prevents us from using Assert.Equal for path comparison. --- .../BaseSymbolicLinks.Browser.cs | 19 ++++++ .../SymbolicLinks/BaseSymbolicLinks.Unix.cs | 19 ++++++ .../BaseSymbolicLinks.Windows.cs | 66 +++++++++++++++++++ .../Base/SymbolicLinks/BaseSymbolicLinks.cs | 12 ++-- ...stem.IO.FileSystem.Net5Compat.Tests.csproj | 3 + .../tests/System.IO.FileSystem.Tests.csproj | 6 ++ 6 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Browser.cs create mode 100644 src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Unix.cs create mode 100644 src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Windows.cs diff --git a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Browser.cs b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Browser.cs new file mode 100644 index 00000000000000..8acc040a8cb695 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Browser.cs @@ -0,0 +1,19 @@ +// 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.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.IO.Tests +{ + public abstract partial class BaseSymbolicLinks : FileSystemTest + { + private string GetTestDirectoryActualCasing() => TestDirectory; + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Unix.cs b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Unix.cs new file mode 100644 index 00000000000000..8acc040a8cb695 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Unix.cs @@ -0,0 +1,19 @@ +// 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.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.IO.Tests +{ + public abstract partial class BaseSymbolicLinks : FileSystemTest + { + private string GetTestDirectoryActualCasing() => TestDirectory; + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Windows.cs b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Windows.cs new file mode 100644 index 00000000000000..fd1352b37eaa90 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Windows.cs @@ -0,0 +1,66 @@ +// 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.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.IO.Tests +{ + public abstract partial class BaseSymbolicLinks : FileSystemTest + { + private const int OPEN_EXISTING = 3; + private const int FILE_FLAG_BACKUP_SEMANTICS = 0x02000000; + + // Some Windows versions like Windows Nano Server have the %TEMP% environment variable set to "C:\TEMP" but the + // actual folder name is "C:\Temp", which prevents asserting path values using Assert.Equal due to case sensitiveness. + // So instead of using TestDirectory directly, we retrieve the real path with proper casing of the initial folder path. + private unsafe string GetTestDirectoryActualCasing() + { + try + { + using SafeFileHandle handle = Interop.Kernel32.CreateFile( + TestDirectory, + dwDesiredAccess: 0, + dwShareMode: FileShare.ReadWrite | FileShare.Delete, + dwCreationDisposition: FileMode.Open, + dwFlagsAndAttributes: + OPEN_EXISTING | + FILE_FLAG_BACKUP_SEMANTICS // Necessary to obtain a handle to a directory + ); + + if (!handle.IsInvalid) + { + const int InitialBufferSize = 4096; + char[]? buffer = ArrayPool.Shared.Rent(InitialBufferSize); + uint result = GetFinalPathNameByHandle(handle, buffer); + + // Remove extended prefix + int skip = (result > 3 && buffer[0] == '\\' && buffer[3] == '\\' && (buffer[2] == '.' || buffer[2] == '?')) ? 4 : 0; + + return new string( + buffer, + skip, + (int)result - skip); + } + } + catch { } + + return TestDirectory; + } + + private unsafe uint GetFinalPathNameByHandle(SafeFileHandle handle, char[] buffer) + { + fixed (char* bufPtr = buffer) + { + return Interop.Kernel32.GetFinalPathNameByHandle(handle, bufPtr, (uint)buffer.Length, Interop.Kernel32.FILE_NAME_NORMALIZED); + } + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs index a65fe5f2e5c5a2..bb7673bbff72b3 100644 --- a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs +++ b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs @@ -1,14 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using System.Diagnostics; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; using Xunit; namespace System.IO.Tests { [ConditionalClass(typeof(BaseSymbolicLinks), nameof(CanCreateSymbolicLinks))] // Contains helper methods that are shared by all symbolic link test classes. - public abstract class BaseSymbolicLinks : FileSystemTest + public abstract partial class BaseSymbolicLinks : FileSystemTest { protected DirectoryInfo CreateDirectoryContainingSelfReferencingSymbolicLink() { @@ -22,9 +25,10 @@ protected DirectoryInfo CreateDirectoryContainingSelfReferencingSymbolicLink() 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()); + protected string GetRandomFilePath() => Path.Join(ActualTestDirectory.Value, GetRandomFileName()); + protected string GetRandomLinkPath() => Path.Join(ActualTestDirectory.Value, GetRandomLinkName()); + protected string GetRandomDirPath() => Path.Join(ActualTestDirectory.Value, GetRandomDirName()); + private Lazy ActualTestDirectory => new Lazy(() => GetTestDirectoryActualCasing()); } } diff --git a/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj index e828b4892a3315..dc418e5dfdb7cc 100644 --- a/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj +++ b/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj @@ -23,9 +23,12 @@ + + + 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 579ccac9b6066b..d655c97a527cb3 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 @@ -68,12 +68,14 @@ + + @@ -81,9 +83,12 @@ + + + @@ -91,6 +96,7 @@ + From c43664a3b6a279025fdbda037ce16dd2b805ef47 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Fri, 25 Jun 2021 18:01:22 -0700 Subject: [PATCH 24/50] Use ALLOW_UNPRIVILEGED_CREATE only on windows versions >= 10.0.14972 --- .../Windows/Kernel32/Interop.CreateSymbolicLink.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs index a07005ba5c55b3..5ec0ce6592b082 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs @@ -36,7 +36,13 @@ internal static bool CreateSymbolicLink(string symlinkFileName, string targetFil symlinkFileName = PathInternal.EnsureExtendedPrefixIfNeeded(symlinkFileName); targetFileName = PathInternal.EnsureExtendedPrefixIfNeeded(targetFileName); - int flags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; + int flags = 0; + + if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 14972)) + { + flags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; + } + if (isDirectory) { flags |= SYMBOLIC_LINK_FLAG_DIRECTORY; From 94602649da0921bc0f50fb54d37b3f21cfd0b917 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Mon, 28 Jun 2021 11:08:22 -0700 Subject: [PATCH 25/50] Do not use IsWindowsVersionAtLeast since isn't avail in ns2.0 --- .../src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs index 5ec0ce6592b082..221c799af01572 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs @@ -38,7 +38,8 @@ internal static bool CreateSymbolicLink(string symlinkFileName, string targetFil int flags = 0; - if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 14972)) + if (Environment.OSVersion.Version.Major == 10 && Environment.OSVersion.Version.Build >= 14972 || + Environment.OSVersion.Version.Major >= 11) { flags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; } From bbaa35ac2d7c2c1da7ec101335c0ad99504a0ce9 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Mon, 28 Jun 2021 15:34:37 -0700 Subject: [PATCH 26/50] Use PrintName (Dos) instead of SubstituteName (NT) --- .../BaseSymbolicLinks.FileSystem.cs | 33 +++---------------- .../BaseSymbolicLinks.FileSystemInfo.cs | 2 +- .../src/System/IO/FileSystem.Windows.cs | 30 +++++++---------- 3 files changed, 17 insertions(+), 48 deletions(-) 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 fad51e96441fc5..0e42ec2f0d8aad 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,31 +21,6 @@ public abstract class BaseSymbolicLinks_FileSystem : BaseSymbolicLinks /// Calls the actual public API for resolving the symbolic link target. protected abstract FileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget = false); - /// - /// Verifies that FileSystemInfo.LinkTarget matches the specified expected path. - /// If the current platform is Windows and is absolute, this method asserts that starts with \\?\. - /// - protected static void AssertLinkTargetEquals(string expected, string actual) - { -#if WINDOWS - if (Path.IsPathFullyQualified(actual)) - { - actual.StartsWith(ExtendedPrefix); - expected = Path.Join(ExtendedPrefix, expected); - } - - // 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); - } - - /// /// Asserts that the FullPath of the FileSystemInfo returned by ResolveLinkTarget() matches with the expected path of the file created. /// Trims the Windows device prefix, in case there's any, before comparing. @@ -188,7 +163,7 @@ public void ResolveLinkTarget_Succeeds(string pathToTarget, bool returnFinalTarg FileSystemInfo linkInfo = CreateSymbolicLink(linkPath, pathToTarget); AssertLinkExists(linkInfo); AssertIsCorrectTypeAndDirectoryAttribute(linkInfo); - AssertLinkTargetEquals(pathToTarget, linkInfo.LinkTarget); + Assert.Equal(pathToTarget, linkInfo.LinkTarget); FileSystemInfo targetInfo = ResolveLinkTarget(linkPath, returnFinalTarget); Assert.NotNull(targetInfo); @@ -379,14 +354,14 @@ private void ResolveLinkTarget_ReturnFinalTarget(string link1Path, string link1T Assert.True(link2Info.Exists); Assert.True(link2Info.Attributes.HasFlag(FileAttributes.ReparsePoint)); AssertIsCorrectTypeAndDirectoryAttribute(link2Info); - AssertLinkTargetEquals(link2Target, link2Info.LinkTarget); + Assert.Equal(link2Target, link2Info.LinkTarget); // link1 to link2 FileSystemInfo link1Info = CreateSymbolicLink(link1Path, link1Target); Assert.True(link1Info.Exists); Assert.True(link1Info.Attributes.HasFlag(FileAttributes.ReparsePoint)); AssertIsCorrectTypeAndDirectoryAttribute(link1Info); - AssertLinkTargetEquals(link1Target, link1Info.LinkTarget); + Assert.Equal(link1Target, link1Info.LinkTarget); // link1: do not follow symlinks FileSystemInfo link1TargetInfo = ResolveLinkTarget(link1Path); @@ -394,7 +369,7 @@ private void ResolveLinkTarget_ReturnFinalTarget(string link1Path, string link1T AssertIsCorrectTypeAndDirectoryAttribute(link1TargetInfo); Assert.True(link1TargetInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)); AssertFullNameEquals(link2Path, link1TargetInfo.FullName); - AssertLinkTargetEquals(link2Target, link1TargetInfo.LinkTarget); + Assert.Equal(link2Target, link1TargetInfo.LinkTarget); // link2: do not follow symlinks FileSystemInfo link2TargetInfo = ResolveLinkTarget(link2Path); 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 129967d542665b..6422b01554cc40 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 @@ -48,7 +48,7 @@ public void LinkTarget_Succeeds(string pathToTarget) FileSystemInfo linkInfo = CreateSymbolicLink(GetRandomLinkPath(), pathToTarget); AssertLinkExists(linkInfo); - AssertLinkTargetEquals(pathToTarget, linkInfo.LinkTarget); + Assert.Equal(pathToTarget, linkInfo.LinkTarget); } public static IEnumerable LinkTarget_PathToTarget_Data 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 18dc6dbf741695..1936bf5b047c9e 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 @@ -468,7 +468,7 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i } } - byte[]? buffer = ArrayPool.Shared.Rent(Interop.Kernel32.MAXIMUM_REPARSE_DATA_BUFFER_SIZE); + byte[] buffer = ArrayPool.Shared.Rent(Interop.Kernel32.MAXIMUM_REPARSE_DATA_BUFFER_SIZE); try { bool success = Interop.Kernel32.DeviceIoControl( @@ -503,21 +503,18 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i return null; } - int substituteNameOffset = sizeof(Interop.Kernel32.REPARSE_DATA_BUFFER) + rdb.ReparseBufferSymbolicLink.SubstituteNameOffset; - int substituteNameLength = rdb.ReparseBufferSymbolicLink.SubstituteNameLength; + // We use PrintName instead of SubstitutneName given that we don't want to return a NT path. + // Unlike SubstituteName and GetFinalPathNameByHandle(), PrintName doesn't start with a prefix. + // Another nuance is that SubstituteName does not contain redundant path segments while PrintName does. + int printNameNameOffset = sizeof(Interop.Kernel32.REPARSE_DATA_BUFFER) + rdb.ReparseBufferSymbolicLink.PrintNameOffset; + int printNameNameLength = rdb.ReparseBufferSymbolicLink.PrintNameLength; - Span targetPath = MemoryMarshal.Cast(bufferSpan.Slice(substituteNameOffset, substituteNameLength)); + Span targetPath = MemoryMarshal.Cast(bufferSpan.Slice(printNameNameOffset, printNameNameLength)); + Debug.Assert(Path.IsPathFullyQualified(targetPath)); + Debug.Assert((rdb.ReparseBufferSymbolicLink.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) == 0 || + targetPath.Length >= 4 && !targetPath.StartsWith(PathInternal.ExtendedPathPrefix) && !targetPath.StartsWith(@"\??\")); - if ((rdb.ReparseBufferSymbolicLink.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) == 0) - { - // Target path is absolute. - // DeviceIoControl returns a DOS Device path (\??\) which many APIs don't tolerate. - // We should instead return a Win32 path (\\?\). - Debug.Assert(Path.IsPathFullyQualified(targetPath)); - Debug.Assert(targetPath[1] == '?'); - targetPath[1] = '\\'; - } - else if (normalize) + if (normalize && (rdb.ReparseBufferSymbolicLink.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) != 0) { // Target path is relative and is for ResolveLinkTarget(), we need to append the link directory. return Path.Join(Path.GetDirectoryName(linkPath.AsSpan()), targetPath); @@ -527,10 +524,7 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i } finally { - if (buffer != null) - { - ArrayPool.Shared.Return(buffer); - } + ArrayPool.Shared.Return(buffer); } } From 7e2eaa934da42ec449225b28416f9b4844a82a11 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Mon, 28 Jun 2021 18:42:46 -0700 Subject: [PATCH 27/50] Fix issues related to server share paths --- .../IO/FileSystem.Attributes.Windows.cs | 36 +++++++++----- .../BaseSymbolicLinks.FileSystem.cs | 18 +++++-- .../src/System/IO/FileSystem.Windows.cs | 48 +++++++++---------- 3 files changed, 62 insertions(+), 40 deletions(-) diff --git a/src/libraries/Common/src/System/IO/FileSystem.Attributes.Windows.cs b/src/libraries/Common/src/System/IO/FileSystem.Attributes.Windows.cs index 60e7a0aa466f1d..ad087304b4e5ac 100644 --- a/src/libraries/Common/src/System/IO/FileSystem.Attributes.Windows.cs +++ b/src/libraries/Common/src/System/IO/FileSystem.Attributes.Windows.cs @@ -64,19 +64,7 @@ internal static int FillAttributeInfo(string? path, ref Interop.Kernel32.WIN32_F { errorCode = Marshal.GetLastWin32Error(); - if (errorCode != Interop.Errors.ERROR_FILE_NOT_FOUND - && errorCode != Interop.Errors.ERROR_PATH_NOT_FOUND - && errorCode != Interop.Errors.ERROR_NOT_READY - && errorCode != Interop.Errors.ERROR_INVALID_NAME - && errorCode != Interop.Errors.ERROR_BAD_PATHNAME - && errorCode != Interop.Errors.ERROR_BAD_NETPATH - && errorCode != Interop.Errors.ERROR_BAD_NET_NAME - && errorCode != Interop.Errors.ERROR_INVALID_PARAMETER - && errorCode != Interop.Errors.ERROR_NETWORK_UNREACHABLE - && errorCode != Interop.Errors.ERROR_NETWORK_ACCESS_DENIED - && errorCode != Interop.Errors.ERROR_INVALID_HANDLE // eg from \\.\CON - && errorCode != Interop.Errors.ERROR_FILENAME_EXCED_RANGE // Path is too long - ) + if (!IsPathUnreachableError(errorCode)) { // Assert so we can track down other cases (if any) to add to our test suite Debug.Assert(errorCode == Interop.Errors.ERROR_ACCESS_DENIED || errorCode == Interop.Errors.ERROR_SHARING_VIOLATION || errorCode == Interop.Errors.ERROR_SEM_TIMEOUT, @@ -127,5 +115,27 @@ internal static int FillAttributeInfo(string? path, ref Interop.Kernel32.WIN32_F return errorCode; } + + internal static bool IsPathUnreachableError(int errorCode) + { + switch (errorCode) + { + case Interop.Errors.ERROR_FILE_NOT_FOUND: + case Interop.Errors.ERROR_PATH_NOT_FOUND: + case Interop.Errors.ERROR_NOT_READY: + case Interop.Errors.ERROR_INVALID_NAME: + case Interop.Errors.ERROR_BAD_PATHNAME: + case Interop.Errors.ERROR_BAD_NETPATH: + case Interop.Errors.ERROR_BAD_NET_NAME: + case Interop.Errors.ERROR_INVALID_PARAMETER: + case Interop.Errors.ERROR_NETWORK_UNREACHABLE: + case Interop.Errors.ERROR_NETWORK_ACCESS_DENIED: + case Interop.Errors.ERROR_INVALID_HANDLE: // eg from \\.\CON + case Interop.Errors.ERROR_FILENAME_EXCED_RANGE: // Path is too long + return true; + default: + return false; + } + } } } 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 0e42ec2f0d8aad..d5b88aa0db5598 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 @@ -424,14 +424,26 @@ public static IEnumerable ResolveLinkTarget_PathToTarget_Data internal static string[] PathToTargetData => new[] { - // Non-rooted relative + //Non-rooted relative "foo", ".\\foo", "..\\foo", // Rooted relative "\\foo", // Rooted absolute Path.Combine(Path.GetTempPath(), "foo"), - //Path.Combine(@"\\?\", Path.GetTempPath(), "foo"), - //@"\\server\share\path", @"\\.\pipe\foo", + Path.Combine(@"\\?\", Path.GetTempPath(), "foo"), + @"\\SERVER\share\path", @"\\.\pipe\foo", }; + + [Fact] + public void quicktest() + { + //Debugger.Launch(); + var info = new DirectoryInfo(@"C:\\linktests\\0628.f"); + Assert.True(info.Exists); + Assert.Equal(@"\\LOCALHOST\\Users\david\share", info.LinkTarget); + + var targetInfo = info.ResolveLinkTarget(); + Assert.True(targetInfo.Exists); + } } } 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 1936bf5b047c9e..592d85bb28fcfc 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 @@ -413,8 +413,9 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i Path.Join(Path.GetDirectoryName(path), pathToTarget) : pathToTarget; Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA data = default; - FillAttributeInfo(pathToTargetFullPath, ref data, returnErrorOnNotFound: false); - if (data.dwFileAttributes != -1 && + int errorCode = FillAttributeInfo(pathToTargetFullPath, ref data, returnErrorOnNotFound: true); + if (errorCode == Interop.Errors.ERROR_SUCCESS && + data.dwFileAttributes != -1 && isDirectory != ((data.dwFileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_DIRECTORY) != 0)) { throw new IOException(SR.Format(SR.IO_InconsistentLinkType, path)); @@ -430,20 +431,20 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i { string? targetPath = returnFinalTarget ? GetFinalLinkTarget(linkPath, isDirectory) : - GetImmediateLinkTarget(linkPath, isDirectory, throwOnNotFound: true, normalize: true); + GetImmediateLinkTarget(linkPath, isDirectory, throwOnUnreachable: true, normalize: true); return targetPath == null ? null : isDirectory ? new DirectoryInfo(targetPath) : new FileInfo(targetPath); } internal static string? GetLinkTarget(string linkPath, bool isDirectory) - => GetImmediateLinkTarget(linkPath, isDirectory, throwOnNotFound: false, normalize: false); + => GetImmediateLinkTarget(linkPath, isDirectory, throwOnUnreachable: 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) + internal static unsafe string? GetImmediateLinkTarget(string linkPath, bool isDirectory, bool throwOnUnreachable, bool normalize) { using SafeFileHandle handle = OpenSafeFileHandle(linkPath, Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS | @@ -452,20 +453,19 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i if (handle.IsInvalid) { int error = Marshal.GetLastWin32Error(); - switch (error) + + if (!throwOnUnreachable && IsPathUnreachableError(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); + return null; } + + // File not found doesn't make much sense coming from a directory. + if (isDirectory && error == Interop.Errors.ERROR_FILE_NOT_FOUND) + { + error = Interop.Errors.ERROR_PATH_NOT_FOUND; + } + + throw Win32Marshal.GetExceptionForWin32Error(error, linkPath); } byte[] buffer = ArrayPool.Shared.Rent(Interop.Kernel32.MAXIMUM_REPARSE_DATA_BUFFER_SIZE); @@ -503,9 +503,10 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i return null; } - // We use PrintName instead of SubstitutneName given that we don't want to return a NT path. + // We use PrintName instead of SubstitutneName given that we don't want to return a NT path when the link wasn't created with such NT path. // Unlike SubstituteName and GetFinalPathNameByHandle(), PrintName doesn't start with a prefix. // Another nuance is that SubstituteName does not contain redundant path segments while PrintName does. + // PrintName can ONLY return a NT path if the link was created explicitly targeting a file/folder in such way. e.g: mklink /D linkName \??\C:\path\to\target. int printNameNameOffset = sizeof(Interop.Kernel32.REPARSE_DATA_BUFFER) + rdb.ReparseBufferSymbolicLink.PrintNameOffset; int printNameNameLength = rdb.ReparseBufferSymbolicLink.PrintNameLength; @@ -548,11 +549,10 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i if (handle.IsInvalid) { - // If the handle fails with "not found", is becuse the link was broken. - // We need to fallback to manually traverse the link targets and return the target of the last resolved link. + // If the handle fails because it is unreachable, is becuse the link was broken. + // We need to fallback to manually traverse the links and return the target of the last resolved link. int error = Marshal.GetLastWin32Error(); - if (error == Interop.Errors.ERROR_FILE_NOT_FOUND || - error == Interop.Errors.ERROR_PATH_NOT_FOUND) + if (IsPathUnreachableError(error)) { return GetFinalLinkTargetSlow(linkPath); } @@ -607,13 +607,13 @@ uint GetFinalPathNameByHandle(SafeFileHandle handle, char[] buffer) { // Since all these paths will be passed to CreateFile, which takes a string anyway, it is pointless to use span. // I am not sure if it's possible to change CreateFile's param to ROS and avoid all these allocations. - string? current = GetImmediateLinkTarget(linkPath, isDirectory, throwOnNotFound: false, normalize: true); + string? current = GetImmediateLinkTarget(linkPath, isDirectory, throwOnUnreachable: false, normalize: true); string? prev = null; while (current != null) { prev = current; - current = GetImmediateLinkTarget(current, isDirectory, throwOnNotFound: false, normalize: true); + current = GetImmediateLinkTarget(current, isDirectory, throwOnUnreachable: false, normalize: true); } return prev; From 0d4250b767c6933f68f17ecc25479b35e58e5df3 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Mon, 28 Jun 2021 20:05:13 -0700 Subject: [PATCH 28/50] Fix CI issues --- .../System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 592d85bb28fcfc..e4c31941ad97f3 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 @@ -513,7 +513,7 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i Span targetPath = MemoryMarshal.Cast(bufferSpan.Slice(printNameNameOffset, printNameNameLength)); Debug.Assert(Path.IsPathFullyQualified(targetPath)); Debug.Assert((rdb.ReparseBufferSymbolicLink.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) == 0 || - targetPath.Length >= 4 && !targetPath.StartsWith(PathInternal.ExtendedPathPrefix) && !targetPath.StartsWith(@"\??\")); + targetPath.Length >= 4 && !targetPath.StartsWith(PathInternal.ExtendedPathPrefix.AsSpan()) && !targetPath.StartsWith(@"\??\".AsSpan())); if (normalize && (rdb.ReparseBufferSymbolicLink.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) != 0) { From 1a05a563b8621f9adf05a9a0f1a925abf6bb4b2c Mon Sep 17 00:00:00 2001 From: David Cantu Date: Mon, 28 Jun 2021 22:33:52 -0700 Subject: [PATCH 29/50] Address suggestions --- .../Kernel32/Interop.DeviceIoControl.cs | 2 +- .../Interop.GetFinalPathNameByHandle.cs | 4 ++-- .../System/IO/FileCleanupTestBase.cs | 9 ++----- .../BaseSymbolicLinks.FileSystem.cs | 24 +++++-------------- .../tests/System.IO.FileSystem.Tests.csproj | 3 +-- .../src/System/IO/FileSystem.Unix.cs | 3 +-- .../src/System/IO/FileSystem.Windows.cs | 13 ++++------ .../src/System/IO/FileSystem.cs | 4 +--- .../src/System/IO/PathInternal.cs | 8 +++++++ 9 files changed, 26 insertions(+), 44 deletions(-) diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.DeviceIoControl.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.DeviceIoControl.cs index 2ebf537adfa35b..be8def215178fb 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.DeviceIoControl.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.DeviceIoControl.cs @@ -13,7 +13,7 @@ internal static partial class Kernel32 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( + internal static extern bool DeviceIoControl( SafeHandle hDevice, uint dwIoControlCode, IntPtr lpInBuffer, diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetFinalPathNameByHandle.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetFinalPathNameByHandle.cs index 17f0ed2e8ca3b4..756b1bbd72db12 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetFinalPathNameByHandle.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetFinalPathNameByHandle.cs @@ -10,11 +10,11 @@ internal static partial class Interop { internal static partial class Kernel32 { - public const uint FILE_NAME_NORMALIZED = 0x0; + internal 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( + internal static unsafe extern uint GetFinalPathNameByHandle( SafeFileHandle hFile, char* lpszFilePath, uint cchFilePath, diff --git a/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs b/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs index 98d6f3498624cf..a45aab12165a51 100644 --- a/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs +++ b/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs @@ -181,13 +181,8 @@ protected static bool CreateSymLink(string targetPath, string linkPath, bool isD symLinkProcess.StartInfo.RedirectStandardOutput = true; symLinkProcess.Start(); - if (symLinkProcess != null) - { - symLinkProcess.WaitForExit(); - return (0 == symLinkProcess.ExitCode); - } - - return false; + symLinkProcess.WaitForExit(); + return (0 == symLinkProcess.ExitCode); } } 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 d5b88aa0db5598..ac4bee876230f9 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 @@ -14,10 +14,14 @@ public abstract class BaseSymbolicLinks_FileSystem : BaseSymbolicLinks /// 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 linkInfo); + protected abstract void AssertLinkExists(FileSystemInfo linkInfo); + /// 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, bool returnFinalTarget = false); @@ -265,9 +269,7 @@ public void ResolveLinkTarget_ReturnFinalTarget_Relative_WithRedundantSegments() [Fact] public void DetectSymbolicLinkCycle() { - // link1 -> link2 - // ^ / - // \______/ + // link1 -> link2 -> link1 (cycle) string link2Path = GetRandomFilePath(); string link1Path = GetRandomFilePath(); @@ -287,9 +289,7 @@ public void DetectSymbolicLinkCycle() [Fact] public void DetectLinkReferenceToSelf() { - // link - // ^ \ - // \___/ + // link -> link (reference to itself) string linkPath = GetRandomFilePath(); FileSystemInfo linkInfo = CreateSymbolicLink(linkPath, linkPath); @@ -433,17 +433,5 @@ public static IEnumerable ResolveLinkTarget_PathToTarget_Data Path.Combine(@"\\?\", Path.GetTempPath(), "foo"), @"\\SERVER\share\path", @"\\.\pipe\foo", }; - - [Fact] - public void quicktest() - { - //Debugger.Launch(); - var info = new DirectoryInfo(@"C:\\linktests\\0628.f"); - Assert.True(info.Exists); - Assert.Equal(@"\\LOCALHOST\\Users\david\share", info.LinkTarget); - - var targetInfo = info.ResolveLinkTarget(); - Assert.True(targetInfo.Exists); - } } } 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 d655c97a527cb3..a31750434fb4eb 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 @@ -1,11 +1,10 @@ - + true true $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-Unix;$(NetCoreAppCurrent)-Browser --working-dir=/test-dir - ../../System.Private.CoreLib/src/Resources/Strings.resx 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 34582c1cac5ac2..a723406f5fe098 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 @@ -551,8 +551,7 @@ 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) { - string pathToTargetFullPath = PathInternal.IsPartiallyQualified(pathToTarget.AsSpan()) ? - Path.Join(Path.GetDirectoryName(path), pathToTarget) : pathToTarget; + string pathToTargetFullPath = PathInternal.GetLinkTargetFullPath(path, pathToTarget); // Fail if the target exists but is not consistent with the expected filesystem entry type if (Interop.Sys.LStat(pathToTargetFullPath, out Interop.Sys.FileStatus targetInfo) == 0) 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 e4c31941ad97f3..e2ccfb4ef2a153 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 @@ -409,8 +409,7 @@ public static string[] GetLogicalDrives() internal static void CreateSymbolicLink(string path, string pathToTarget, bool isDirectory) { - string pathToTargetFullPath = PathInternal.IsPartiallyQualified(pathToTarget.AsSpan()) ? - Path.Join(Path.GetDirectoryName(path), pathToTarget) : pathToTarget; + string pathToTargetFullPath = PathInternal.GetLinkTargetFullPath(path, pathToTarget); Interop.Kernel32.WIN32_FILE_ATTRIBUTE_DATA data = default; int errorCode = FillAttributeInfo(pathToTargetFullPath, ref data, returnErrorOnNotFound: true); @@ -427,7 +426,7 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i } } - internal static unsafe FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget, bool isDirectory) + internal static FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget, bool isDirectory) { string? targetPath = returnFinalTarget ? GetFinalLinkTarget(linkPath, isDirectory) : @@ -561,7 +560,7 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i } const int InitialBufferSize = 4096; - char[]? buffer = ArrayPool.Shared.Rent(InitialBufferSize); + char[] buffer = ArrayPool.Shared.Rent(InitialBufferSize); try { uint result = GetFinalPathNameByHandle(handle, buffer); @@ -571,7 +570,6 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i if (result > InitialBufferSize) { ArrayPool.Shared.Return(buffer); - buffer = null; buffer = ArrayPool.Shared.Rent((int)result); result = GetFinalPathNameByHandle(handle, buffer); @@ -589,10 +587,7 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i } finally { - if (buffer != null) - { - ArrayPool.Shared.Return(buffer); - } + ArrayPool.Shared.Return(buffer); } uint GetFinalPathNameByHandle(SafeFileHandle handle, char[] buffer) 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 1e30d42fe61477..a1d506e9218c86 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.cs @@ -1,9 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - -using System; - #if MS_IO_REDIST +using System; namespace Microsoft.IO #else diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.cs b/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.cs index fcbeece65a73fa..ff9d79caee2532 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.cs @@ -245,5 +245,13 @@ internal static ReadOnlySpan TrimEndingDirectorySeparator(ReadOnlySpan internal static bool EndsInDirectorySeparator(ReadOnlySpan path) => path.Length > 0 && IsDirectorySeparator(path[path.Length - 1]); + + internal static string GetLinkTargetFullPath(string path, string pathToTarget) + => IsPartiallyQualified(pathToTarget.AsSpan()) ? +#if MS_IO_REDIST + Path.Combine(Path.GetDirectoryName(path), pathToTarget) : pathToTarget; +#else + Path.Join(Path.GetDirectoryName(path.AsSpan()), pathToTarget.AsSpan()) : pathToTarget; +#endif } } From d656aed9ff124e05da68aa41bf55771f024e4f8b Mon Sep 17 00:00:00 2001 From: David Cantu Date: Mon, 28 Jun 2021 22:39:18 -0700 Subject: [PATCH 30/50] Address suggestions --- .../System.Private.CoreLib/src/System/IO/FileSystem.Windows.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e2ccfb4ef2a153..d5dd7296b8ab0f 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 @@ -567,7 +567,7 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i // 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. - if (result > InitialBufferSize) + if (result > buffer.Length) { ArrayPool.Shared.Return(buffer); buffer = ArrayPool.Shared.Rent((int)result); From adcca169084166af8bf45b0c8001d064deb2c3f6 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 29 Jun 2021 13:14:35 -0700 Subject: [PATCH 31/50] Address suggestions about using PathInternal.IsExtended --- .../tests/Base/SymbolicLinks/BaseSymbolicLinks.Windows.cs | 2 +- .../src/System/IO/FileSystem.Windows.cs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Windows.cs b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Windows.cs index fd1352b37eaa90..4572d48544fcf2 100644 --- a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Windows.cs +++ b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Windows.cs @@ -42,7 +42,7 @@ private unsafe string GetTestDirectoryActualCasing() uint result = GetFinalPathNameByHandle(handle, buffer); // Remove extended prefix - int skip = (result > 3 && buffer[0] == '\\' && buffer[3] == '\\' && (buffer[2] == '.' || buffer[2] == '?')) ? 4 : 0; + int skip = PathInternal.IsExtended(buffer) ? 4 : 0; return new string( buffer, 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 d5dd7296b8ab0f..2256d05bfb02e0 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 @@ -510,9 +510,7 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i int printNameNameLength = rdb.ReparseBufferSymbolicLink.PrintNameLength; Span targetPath = MemoryMarshal.Cast(bufferSpan.Slice(printNameNameOffset, printNameNameLength)); - Debug.Assert(Path.IsPathFullyQualified(targetPath)); - Debug.Assert((rdb.ReparseBufferSymbolicLink.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) == 0 || - targetPath.Length >= 4 && !targetPath.StartsWith(PathInternal.ExtendedPathPrefix.AsSpan()) && !targetPath.StartsWith(@"\??\".AsSpan())); + Debug.Assert((rdb.ReparseBufferSymbolicLink.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) == 0 || !PathInternal.IsExtended(targetPath)); if (normalize && (rdb.ReparseBufferSymbolicLink.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) != 0) { From 35165b08dd65a7628018fcc5775eef5f0817f69e Mon Sep 17 00:00:00 2001 From: David Cantu Date: Wed, 30 Jun 2021 23:17:44 -0700 Subject: [PATCH 32/50] Add more scenarios for "file system entry type is inconsistent with that of its target" --- .../BaseSymbolicLinks.FileSystem.cs | 52 +++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) 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 ac4bee876230f9..820351fb527d53 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 @@ -9,7 +9,6 @@ namespace System.IO.Tests // Contains test methods that can be used for FileInfo, DirectoryInfo, File or Directory. public abstract class BaseSymbolicLinks_FileSystem : BaseSymbolicLinks { - private const string ExtendedPrefix = @"\\?\"; /// 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. @@ -22,6 +21,19 @@ public abstract class BaseSymbolicLinks_FileSystem : BaseSymbolicLinks /// Calls the actual public API for creating a symbolic link. protected abstract FileSystemInfo CreateSymbolicLink(string path, string pathToTarget); + private void CreateSymbolicLink_Opposite(string path, string pathToTarget) + { + Type t = GetType(); + if (t == typeof(Directory_SymbolicLinks) || t == typeof(DirectoryInfo_SymbolicLinks)) + { + File.CreateSymbolicLink(path, pathToTarget); + } + else + { + Directory.CreateSymbolicLink(path, pathToTarget); + } + } + /// Calls the actual public API for resolving the symbolic link target. protected abstract FileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget = false); @@ -34,7 +46,7 @@ private static void AssertFullNameEquals(string expected, string actual) #if WINDOWS if (PathInternal.IsExtended(actual)) { - Assert.StartsWith(ExtendedPrefix, actual); + Assert.StartsWith(PathInternal.ExtendedPathPrefix, actual); actual = actual.Substring(4); } #endif @@ -302,7 +314,7 @@ public void DetectLinkReferenceToSelf() } [Fact] - public void CreateSymbolicLink_WrongTargetType() + public void CreateSymbolicLink_WrongTargetType_Throws() { // dirLink -> file // fileLink -> dir @@ -312,6 +324,38 @@ public void CreateSymbolicLink_WrongTargetType() Assert.Throws(() => CreateSymbolicLink(GetRandomFilePath(), targetPath)); } + [Fact] + public void CreateSymbolicLink_WrongTargetType_Indirect_Throws() + { + // link-2 (dir) -> link-1 (file) -> file + // link-2 (file) -> link-1 (dir) -> dir + string targetPath = GetRandomFilePath(); + string firstLinkPath = GetRandomFilePath(); + string secondLinkPath = GetRandomFilePath(); + + CreateFileOrDirectory(targetPath, createOpposite: true); + CreateSymbolicLink_Opposite(firstLinkPath, targetPath); + + Assert.Throws(() => CreateSymbolicLink(secondLinkPath, firstLinkPath)); + } + + [Fact] + public void CreateSymbolicLink_CorrectTargetType_Indirect_Succeeds() + { + // link-2 (file) -> link-1 (file) -> file + // link-2 (dir) -> link-1 (dir) -> dir + string targetPath = GetRandomFilePath(); + string firstLinkPath = GetRandomFilePath(); + string secondLinkPath = GetRandomFilePath(); + + CreateFileOrDirectory(targetPath, createOpposite: false); + CreateSymbolicLink(firstLinkPath, targetPath); + + FileSystemInfo secondLinkInfo = CreateSymbolicLink(secondLinkPath, firstLinkPath); + Assert.Equal(firstLinkPath, secondLinkInfo.LinkTarget); + AssertFullNameEquals(targetPath, secondLinkInfo.ResolveLinkTarget(true).FullName); + } + private void VerifySymbolicLinkAndResolvedTarget(string linkPath, string expectedLinkTarget, string targetPath = null) { // linkPath -> expectedLinkTarget (created in targetPath if not null) @@ -431,7 +475,7 @@ public static IEnumerable ResolveLinkTarget_PathToTarget_Data // Rooted absolute Path.Combine(Path.GetTempPath(), "foo"), Path.Combine(@"\\?\", Path.GetTempPath(), "foo"), - @"\\SERVER\share\path", @"\\.\pipe\foo", + @"\\SERVER\share\path" }; } } From 9d2694ba8b441c7914605f90cb998d902653199a Mon Sep 17 00:00:00 2001 From: David Cantu Date: Wed, 30 Jun 2021 23:18:03 -0700 Subject: [PATCH 33/50] Remove duplicated validation checks --- .../src/System/IO/Directory.cs | 5 +++-- .../System.Private.CoreLib/src/System/IO/File.cs | 5 +++-- .../src/System/IO/FileSystem.Windows.cs | 12 ++++++------ 3 files changed, 12 insertions(+), 10 deletions(-) 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 78dd5d17b51e02..13a405c72e826b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs @@ -333,10 +333,11 @@ public static string[] GetLogicalDrives() /// An I/O error occurred. public static FileSystemInfo CreateSymbolicLink(string path, string pathToTarget) { - FileSystem.VerifyValidPath(path, nameof(path)); + string fullPath = Path.GetFullPath(path); FileSystem.VerifyValidPath(pathToTarget, nameof(pathToTarget)); + FileSystem.CreateSymbolicLink(path, pathToTarget, isDirectory: true); - return new DirectoryInfo(path); + return new DirectoryInfo(originalPath: path, fullPath: fullPath, isNormalized: 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 038d8fd990edf8..e67b658e709631 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs @@ -1023,10 +1023,11 @@ 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)); + string fullPath = Path.GetFullPath(path); FileSystem.VerifyValidPath(pathToTarget, nameof(pathToTarget)); + FileSystem.CreateSymbolicLink(path, pathToTarget, isDirectory: false); - return new FileInfo(path); + return new FileInfo(originalPath: path, fullPath: fullPath, isNormalized: true); } /// 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 2256d05bfb02e0..82b26b9f5e1680 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 @@ -430,20 +430,20 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i { string? targetPath = returnFinalTarget ? GetFinalLinkTarget(linkPath, isDirectory) : - GetImmediateLinkTarget(linkPath, isDirectory, throwOnUnreachable: true, normalize: true); + GetImmediateLinkTarget(linkPath, isDirectory, throwOnUnreachable: true, returnFullPath: true); return targetPath == null ? null : isDirectory ? new DirectoryInfo(targetPath) : new FileInfo(targetPath); } internal static string? GetLinkTarget(string linkPath, bool isDirectory) - => GetImmediateLinkTarget(linkPath, isDirectory, throwOnUnreachable: false, normalize: false); + => GetImmediateLinkTarget(linkPath, isDirectory, throwOnUnreachable: false, returnFullPath: 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 throwOnUnreachable, bool normalize) + internal static unsafe string? GetImmediateLinkTarget(string linkPath, bool isDirectory, bool throwOnUnreachable, bool returnFullPath) { using SafeFileHandle handle = OpenSafeFileHandle(linkPath, Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS | @@ -512,7 +512,7 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i Span targetPath = MemoryMarshal.Cast(bufferSpan.Slice(printNameNameOffset, printNameNameLength)); Debug.Assert((rdb.ReparseBufferSymbolicLink.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) == 0 || !PathInternal.IsExtended(targetPath)); - if (normalize && (rdb.ReparseBufferSymbolicLink.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) != 0) + if (returnFullPath && (rdb.ReparseBufferSymbolicLink.Flags & Interop.Kernel32.SYMLINK_FLAG_RELATIVE) != 0) { // Target path is relative and is for ResolveLinkTarget(), we need to append the link directory. return Path.Join(Path.GetDirectoryName(linkPath.AsSpan()), targetPath); @@ -600,13 +600,13 @@ uint GetFinalPathNameByHandle(SafeFileHandle handle, char[] buffer) { // Since all these paths will be passed to CreateFile, which takes a string anyway, it is pointless to use span. // I am not sure if it's possible to change CreateFile's param to ROS and avoid all these allocations. - string? current = GetImmediateLinkTarget(linkPath, isDirectory, throwOnUnreachable: false, normalize: true); + string? current = GetImmediateLinkTarget(linkPath, isDirectory, throwOnUnreachable: false, returnFullPath: true); string? prev = null; while (current != null) { prev = current; - current = GetImmediateLinkTarget(current, isDirectory, throwOnUnreachable: false, normalize: true); + current = GetImmediateLinkTarget(current, isDirectory, throwOnUnreachable: false, returnFullPath: true); } return prev; From c79df26c5daaa6c5f9a538089fc0ad61ebdd3a40 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Thu, 1 Jul 2021 00:08:16 -0700 Subject: [PATCH 34/50] Address suggestions for Unix --- .../Unix/System.Native/Interop.ReadLink.cs | 7 +++-- .../src/System/IO/FileSystem.Unix.cs | 26 ++----------------- 2 files changed, 5 insertions(+), 28 deletions(-) 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 0ecefe429e4291..94f37d4ccc3f85 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 @@ -30,12 +30,11 @@ internal static partial class Sys /// Returns the link to the target path on success; and null otherwise. internal static string? ReadLink(ReadOnlySpan path) { - int outputBufferSize = DefaultPathBufferSize; + int outputBufferSize = 1024; // 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]); + using var converter = new ValueUtf8Converter(stackalloc byte[1024]); while (true) { @@ -64,7 +63,7 @@ ref MemoryMarshal.GetReference(converter.ConvertAndTerminateString(path)), } // Output buffer was too small, loop around again and try with a larger buffer. - outputBufferSize *= 2; + outputBufferSize = buffer.Length * 2; } } } 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 a723406f5fe098..231ef33a646ab7 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 @@ -536,31 +536,16 @@ public static string[] GetLogicalDrives() return DriveInfoInternal.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(ReadOnlySpan linkPath, bool isDirectory) => 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) { string pathToTargetFullPath = PathInternal.GetLinkTargetFullPath(path, pathToTarget); // Fail if the target exists but is not consistent with the expected filesystem entry type - if (Interop.Sys.LStat(pathToTargetFullPath, out Interop.Sys.FileStatus targetInfo) == 0) + if (Interop.Sys.Stat(pathToTargetFullPath, 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)) + if (isDirectory != ((targetInfo.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR)) { throw new IOException(SR.Format(SR.IO_InconsistentLinkType, path)); } @@ -569,13 +554,6 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i Interop.CheckIo(Interop.Sys.SymLink(pathToTarget, path), path, isDirectory); } - /// Gets the target of the specified link path. - /// 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 - /// 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, 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 From fc4241d2f0f1379cdef9574158207df50d4df88f Mon Sep 17 00:00:00 2001 From: David Cantu Date: Thu, 1 Jul 2021 00:44:37 -0700 Subject: [PATCH 35/50] ifdef the list of paths used for theories --- .../SymbolicLinks/BaseSymbolicLinks.FileSystem.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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 820351fb527d53..a3451941ba990a 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 @@ -468,6 +468,7 @@ public static IEnumerable ResolveLinkTarget_PathToTarget_Data internal static string[] PathToTargetData => new[] { +#if WINDOWS //Non-rooted relative "foo", ".\\foo", "..\\foo", // Rooted relative @@ -475,7 +476,16 @@ public static IEnumerable ResolveLinkTarget_PathToTarget_Data // Rooted absolute Path.Combine(Path.GetTempPath(), "foo"), Path.Combine(@"\\?\", Path.GetTempPath(), "foo"), - @"\\SERVER\share\path" + @"\\SERVER\share\path", @"\\.\pipe\foo" +#else + //Non-rooted relative + "foo", "./foo", "../foo", + // Rooted relative + "/foo", + // Rooted absolute + Path.Combine(Path.GetTempPath(), "foo"), + @"//SERVER/share/path", @"//./pipe/foo" +#endif }; } } From 4881f2b598d5d13ef18667c56a7f2639fb43f456 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Thu, 1 Jul 2021 17:19:15 -0700 Subject: [PATCH 36/50] Add tests to verify the limit of followed links Address suggestions --- .../BaseSymbolicLinks.Browser.cs | 19 ---- .../BaseSymbolicLinks.FileSystem.cs | 105 ++++++++++++++---- .../tests/Directory/SymbolicLinks.cs | 2 + .../tests/DirectoryInfo/SymbolicLinks.cs | 2 + .../tests/File/SymbolicLinks.cs | 2 + .../tests/FileInfo/SymbolicLinks.cs | 2 + .../tests/System.IO.FileSystem.Tests.csproj | 4 +- 7 files changed, 92 insertions(+), 44 deletions(-) delete mode 100644 src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Browser.cs diff --git a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Browser.cs b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Browser.cs deleted file mode 100644 index 8acc040a8cb695..00000000000000 --- a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.Browser.cs +++ /dev/null @@ -1,19 +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 System; -using System.Buffers; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Win32.SafeHandles; -using Xunit; - -namespace System.IO.Tests -{ - public abstract partial class BaseSymbolicLinks : FileSystemTest - { - private string GetTestDirectoryActualCasing() => 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 index a3451941ba990a..c67096afa01096 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 @@ -9,6 +9,8 @@ namespace System.IO.Tests // Contains test methods that can be used for FileInfo, DirectoryInfo, File or Directory. public abstract class BaseSymbolicLinks_FileSystem : BaseSymbolicLinks { + protected abstract bool IsDirectoryTest { get; } + /// 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. @@ -23,8 +25,7 @@ public abstract class BaseSymbolicLinks_FileSystem : BaseSymbolicLinks private void CreateSymbolicLink_Opposite(string path, string pathToTarget) { - Type t = GetType(); - if (t == typeof(Directory_SymbolicLinks) || t == typeof(DirectoryInfo_SymbolicLinks)) + if (IsDirectoryTest) { File.CreateSymbolicLink(path, pathToTarget); } @@ -278,6 +279,52 @@ public void ResolveLinkTarget_ReturnFinalTarget_Relative_WithRedundantSegments() filePath: filePath); } + [Fact] + public void ResolveLinkTarget_ReturnFinalTarget_MaxFollowedLinks() + { + if (OperatingSystem.IsWindows()) + { + // As per the docs, this should be 63 but I don't see that in practice. + // https://docs.microsoft.com/windows/win32/fileio/reparse-points + Verify(limit: 62, relative: true); + Verify(limit: 31, relative: false); + } + else + { + Verify(limit: 40, relative: true); + Verify(limit: 40, relative: false); + } + + void Verify(int limit, bool relative) + { + string? root = relative ? Directory.CreateDirectory(GetRandomFilePath()).FullName : null; + string finalTarget = GetFullPath(root); + + CreateFileOrDirectory(finalTarget); + string previousPath = finalTarget; + + for (int i = 0; i < limit; i++) + { + string currentLinkPath = GetFullPath(root); + CreateSymbolicLink(currentLinkPath, GetLinkTargetPath(previousPath, relative)); + + previousPath = currentLinkPath; + } + + // This is the edge of the limit + FileSystemInfo linkInfo = ResolveLinkTarget(previousPath, returnFinalTarget: true); + AssertFullNameEquals(finalTarget, linkInfo.FullName); + + // One after the limit + linkInfo = CreateSymbolicLink(GetFullPath(root), GetLinkTargetPath(previousPath, relative)); + Assert.Throws(() => ResolveLinkTarget(linkInfo.FullName, returnFinalTarget: true)); + } + + string GetFullPath(string? root) => root != null ? Path.Join(root, GetRandomFileName()) : GetRandomFilePath(); + + string GetLinkTargetPath(string fullPath, bool relative) => relative ? Path.GetFileName(fullPath) : fullPath; + } + [Fact] public void DetectSymbolicLinkCycle() { @@ -458,7 +505,7 @@ public static IEnumerable ResolveLinkTarget_PathToTarget_Data { get { - foreach(string path in PathToTargetData) + foreach (string path in PathToTargetData) { yield return new object[] { path, false }; yield return new object[] { path, true }; @@ -466,26 +513,38 @@ public static IEnumerable ResolveLinkTarget_PathToTarget_Data } } - internal static string[] PathToTargetData => new[] + internal static IEnumerable PathToTargetData { -#if WINDOWS - //Non-rooted relative - "foo", ".\\foo", "..\\foo", - // Rooted relative - "\\foo", - // Rooted absolute - Path.Combine(Path.GetTempPath(), "foo"), - Path.Combine(@"\\?\", Path.GetTempPath(), "foo"), - @"\\SERVER\share\path", @"\\.\pipe\foo" -#else - //Non-rooted relative - "foo", "./foo", "../foo", - // Rooted relative - "/foo", - // Rooted absolute - Path.Combine(Path.GetTempPath(), "foo"), - @"//SERVER/share/path", @"//./pipe/foo" -#endif - }; + get + { + if (OperatingSystem.IsWindows()) + { + //Non-rooted relative + yield return "foo"; + yield return @".\foo"; + yield return @"..\foo"; + // Rooted relative + yield return @"\foo"; + // Rooted absolute + yield return Path.Combine(Path.GetTempPath(), "foo"); + // Extended DOS + yield return Path.Combine(@"\\?\", Path.GetTempPath(), "foo"); + // UNC + yield return @"\\SERVER\share\path"; + yield return @"\\.\pipe\foo"; + } + else + { + //Non-rooted relative + yield return "foo"; + yield return "./foo"; + yield return "../foo"; + // Rooted relative + yield return "/foo"; + // Rooted absolute + Path.Combine(Path.GetTempPath(), "foo"); + } + } + } } } diff --git a/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs index f096baec1617b9..8ebe0fa9544d0d 100644 --- a/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs +++ b/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs @@ -9,6 +9,8 @@ namespace System.IO.Tests { public class Directory_SymbolicLinks : BaseSymbolicLinks_FileSystem { + protected override bool IsDirectoryTest => true; + protected override void CreateFileOrDirectory(string path, bool createOpposite = false) { if (!createOpposite) diff --git a/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs index 381a4879bac2a6..e7ee5d0c6727c5 100644 --- a/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs +++ b/src/libraries/System.IO.FileSystem/tests/DirectoryInfo/SymbolicLinks.cs @@ -9,6 +9,8 @@ namespace System.IO.Tests { public class DirectoryInfo_SymbolicLinks : BaseSymbolicLinks_FileSystemInfo { + protected override bool IsDirectoryTest => true; + protected override FileSystemInfo GetFileSystemInfo(string path) => new DirectoryInfo(path); diff --git a/src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs index 1f39fdcf11e7d1..17d1766d6b795e 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs @@ -9,6 +9,8 @@ namespace System.IO.Tests { public class File_SymbolicLinks : BaseSymbolicLinks_FileSystem { + protected override bool IsDirectoryTest => false; + protected override void CreateFileOrDirectory(string path, bool createOpposite = false) { if (!createOpposite) diff --git a/src/libraries/System.IO.FileSystem/tests/FileInfo/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/FileInfo/SymbolicLinks.cs index a19f700992ee24..fa334dd1ca376d 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileInfo/SymbolicLinks.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileInfo/SymbolicLinks.cs @@ -8,6 +8,8 @@ namespace System.IO.Tests { public class FileInfo_SymbolicLinks : BaseSymbolicLinks_FileSystemInfo { + protected override bool IsDirectoryTest => false; + protected override FileSystemInfo GetFileSystemInfo(string path) => new FileInfo(path); 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 a31750434fb4eb..bade5f1242cfb9 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 @@ -1,4 +1,4 @@ - + true true @@ -95,7 +95,7 @@ - + From b377cb024f2770cd18f7af58597e5b52591c38a7 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Thu, 1 Jul 2021 19:08:48 -0700 Subject: [PATCH 37/50] Fix bug related to follow links limit in Unix --- .../src/System/IO/FileSystem.Unix.cs | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) 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 231ef33a646ab7..2041146e92008c 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 @@ -557,37 +557,40 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i internal static FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget, bool isDirectory) { // throws if the current link file does not exist - Interop.CheckIo(Interop.Sys.LStat(linkPath, out _), linkPath, isDirectory); + Interop.CheckIo(Interop.Sys.LStat(linkPath, out Interop.Sys.FileStatus info), linkPath, isDirectory); + + if ((info.Mode & Interop.Sys.FileTypes.S_IFMT) != Interop.Sys.FileTypes.S_IFLNK) + { + return null; + } ValueStringBuilder sb = new(stackalloc char[Interop.DefaultPathBufferSize]); sb.Append(linkPath); - int maxVisits = returnFinalTarget ? MaxFollowedLinks : 1; - int visitCount = 0; - while (visitCount < maxVisits) + if (returnFinalTarget) { - if (!TryGetLinkTarget(ref sb)) + int visitCount = 0; + while (true) { - if (visitCount == 0) + if (!TryGetLinkTarget(ref sb)) { - // 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; + // We finally found the final target: either + // this file does not exist (broken links are acceptable) + // or this file is not a link + break; } - // 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.IO_TooManySymbolicLinkLevels, linkPath)); + } } - visitCount++; } - - if (visitCount >= MaxFollowedLinks) + else { - // We went over the limit and couldn't reach the final target - throw new IOException(SR.Format(SR.IO_TooManySymbolicLinkLevels, linkPath)); + TryGetLinkTarget(ref sb); } Debug.Assert(sb.Length > 0); From 496487f5248cd6148f258054eba1b41b882d2c1a Mon Sep 17 00:00:00 2001 From: David Cantu Date: Thu, 1 Jul 2021 19:16:15 -0700 Subject: [PATCH 38/50] Trim extended prefix when the passed-in path is not extended --- .../src/System/IO/FileSystem.Windows.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 82b26b9f5e1680..947177c7839809 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 @@ -546,7 +546,7 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i if (handle.IsInvalid) { - // If the handle fails because it is unreachable, is becuse the link was broken. + // If the handle fails because it is unreachable, is because the link was broken. // We need to fallback to manually traverse the links and return the target of the last resolved link. int error = Marshal.GetLastWin32Error(); if (IsPathUnreachableError(error)) @@ -579,9 +579,12 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i 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)result); + Debug.Assert(PathInternal.IsExtended(new string(buffer, 0, (int)result))); + // GetFinalPathNameByHandle always returns with extended DOS prefix even if the link target was created without one. + // While this does not interfere with correct behavior, it might be unexpected. + // Hence we trim it if the passed-in path to the link wasn't extended. + int start = PathInternal.IsExtended(linkPath) ? 0 : 4; + return new string(buffer, start, (int)result - start); } finally { From beaf70153e2d25fa4850e66910f08392d57edd45 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Thu, 1 Jul 2021 20:24:08 -0700 Subject: [PATCH 39/50] Remove helper AssertFullNameEquals since is no longer needed and fix CI errors --- .../BaseSymbolicLinks.FileSystem.cs | 29 ++++--------------- .../src/System/IO/FileSystem.Windows.cs | 4 +-- 2 files changed, 8 insertions(+), 25 deletions(-) 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 c67096afa01096..65039d43745e9d 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 @@ -38,23 +38,6 @@ private void CreateSymbolicLink_Opposite(string path, string pathToTarget) /// Calls the actual public API for resolving the symbolic link target. protected abstract FileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget = false); - /// - /// Asserts that the FullPath of the FileSystemInfo returned by ResolveLinkTarget() matches with the expected path of the file created. - /// Trims the Windows device prefix, in case there's any, before comparing. - /// - private static void AssertFullNameEquals(string expected, string actual) - { -#if WINDOWS - if (PathInternal.IsExtended(actual)) - { - Assert.StartsWith(PathInternal.ExtendedPathPrefix, actual); - actual = actual.Substring(4); - } -#endif - - Assert.Equal(expected, actual); - } - [Fact] public void CreateSymbolicLink_NullPathToTarget() { @@ -189,7 +172,7 @@ public void ResolveLinkTarget_Succeeds(string pathToTarget, bool returnFinalTarg string expectedTargetFullName = Path.IsPathFullyQualified(pathToTarget) ? pathToTarget : Path.GetFullPath(Path.Join(Path.GetDirectoryName(linkPath), pathToTarget)); - AssertFullNameEquals(expectedTargetFullName, targetInfo.FullName); + Assert.Equal(expectedTargetFullName, targetInfo.FullName); } [Theory] @@ -313,7 +296,7 @@ void Verify(int limit, bool relative) // This is the edge of the limit FileSystemInfo linkInfo = ResolveLinkTarget(previousPath, returnFinalTarget: true); - AssertFullNameEquals(finalTarget, linkInfo.FullName); + Assert.Equal(finalTarget, linkInfo.FullName); // One after the limit linkInfo = CreateSymbolicLink(GetFullPath(root), GetLinkTargetPath(previousPath, relative)); @@ -400,7 +383,7 @@ public void CreateSymbolicLink_CorrectTargetType_Indirect_Succeeds() FileSystemInfo secondLinkInfo = CreateSymbolicLink(secondLinkPath, firstLinkPath); Assert.Equal(firstLinkPath, secondLinkInfo.LinkTarget); - AssertFullNameEquals(targetPath, secondLinkInfo.ResolveLinkTarget(true).FullName); + Assert.Equal(targetPath, secondLinkInfo.ResolveLinkTarget(true).FullName); } private void VerifySymbolicLinkAndResolvedTarget(string linkPath, string expectedLinkTarget, string targetPath = null) @@ -459,7 +442,7 @@ private void ResolveLinkTarget_ReturnFinalTarget(string link1Path, string link1T Assert.True(link1TargetInfo.Exists); AssertIsCorrectTypeAndDirectoryAttribute(link1TargetInfo); Assert.True(link1TargetInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)); - AssertFullNameEquals(link2Path, link1TargetInfo.FullName); + Assert.Equal(link2Path, link1TargetInfo.FullName); Assert.Equal(link2Target, link1TargetInfo.LinkTarget); // link2: do not follow symlinks @@ -467,7 +450,7 @@ private void ResolveLinkTarget_ReturnFinalTarget(string link1Path, string link1T Assert.True(link2TargetInfo.Exists); AssertIsCorrectTypeAndDirectoryAttribute(link2TargetInfo); Assert.False(link2TargetInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)); - AssertFullNameEquals(filePath, link2TargetInfo.FullName); + Assert.Equal(filePath, link2TargetInfo.FullName); Assert.Null(link2TargetInfo.LinkTarget); // link1: follow symlinks @@ -475,7 +458,7 @@ private void ResolveLinkTarget_ReturnFinalTarget(string link1Path, string link1T Assert.True(finalTarget.Exists); AssertIsCorrectTypeAndDirectoryAttribute(finalTarget); Assert.False(finalTarget.Attributes.HasFlag(FileAttributes.ReparsePoint)); - AssertFullNameEquals(filePath, finalTarget.FullName); + Assert.Equal(filePath, finalTarget.FullName); } protected void CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(bool createOpposite) 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 947177c7839809..ca9ec5455f1a5f 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 @@ -579,11 +579,11 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i throw Win32Marshal.GetExceptionForLastWin32Error(linkPath); } - Debug.Assert(PathInternal.IsExtended(new string(buffer, 0, (int)result))); + Debug.Assert(PathInternal.IsExtended(new string(buffer, 0, (int)result).AsSpan())); // GetFinalPathNameByHandle always returns with extended DOS prefix even if the link target was created without one. // While this does not interfere with correct behavior, it might be unexpected. // Hence we trim it if the passed-in path to the link wasn't extended. - int start = PathInternal.IsExtended(linkPath) ? 0 : 4; + int start = PathInternal.IsExtended(linkPath.AsSpan()) ? 0 : 4; return new string(buffer, start, (int)result - start); } finally From a5986fb5e1e71df24edaa8d8bfe926e7dc704e1a Mon Sep 17 00:00:00 2001 From: David Cantu Date: Fri, 2 Jul 2021 01:43:34 -0700 Subject: [PATCH 40/50] Add a check for versions < win10 build 14972 to fix silent error --- .../Kernel32/Interop.CreateSymbolicLink.cs | 25 +++++++++++++++---- .../src/System/IO/FileSystem.Windows.cs | 5 +--- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs index 221c799af01572..9ecd41c46bd6b3 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateSymbolicLink.cs @@ -30,16 +30,19 @@ internal static partial class Kernel32 /// 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) + internal static void CreateSymbolicLink(string symlinkFileName, string targetFileName, bool isDirectory) { + string originalPath = symlinkFileName; symlinkFileName = PathInternal.EnsureExtendedPrefixIfNeeded(symlinkFileName); targetFileName = PathInternal.EnsureExtendedPrefixIfNeeded(targetFileName); int flags = 0; - if (Environment.OSVersion.Version.Major == 10 && Environment.OSVersion.Version.Build >= 14972 || - Environment.OSVersion.Version.Major >= 11) + bool isAtLeastWin10Build14972 = + Environment.OSVersion.Version.Major == 10 && Environment.OSVersion.Version.Build >= 14972 || + Environment.OSVersion.Version.Major >= 11; + + if (isAtLeastWin10Build14972) { flags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; } @@ -49,7 +52,19 @@ internal static bool CreateSymbolicLink(string symlinkFileName, string targetFil flags |= SYMBOLIC_LINK_FLAG_DIRECTORY; } - return CreateSymbolicLinkPrivate(symlinkFileName, targetFileName, flags); + bool success = CreateSymbolicLinkPrivate(symlinkFileName, targetFileName, flags); + + int error; + if (!success) + { + throw Win32Marshal.GetExceptionForLastWin32Error(originalPath); + } + // In older versions we need to check GetLastWin32Error regardless of the return value of CreateSymbolicLink, + // e.g: if the user doesn't have enough privileges to create a symlink the method returns success which we can consider as a silent failure. + else if (!isAtLeastWin10Build14972 && (error = Marshal.GetLastWin32Error()) != 0) + { + throw Win32Marshal.GetExceptionForWin32Error(error, originalPath); + } } } } 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 ca9ec5455f1a5f..5f88f53a7c539f 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 @@ -420,10 +420,7 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i throw new IOException(SR.Format(SR.IO_InconsistentLinkType, path)); } - if (!Interop.Kernel32.CreateSymbolicLink(path, pathToTarget, isDirectory)) - { - throw Win32Marshal.GetExceptionForLastWin32Error(path); - } + Interop.Kernel32.CreateSymbolicLink(path, pathToTarget, isDirectory); } internal static FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget, bool isDirectory) From b7ec269003b4fdbdf46417ba009d08774b48fbea Mon Sep 17 00:00:00 2001 From: David Cantu Date: Fri, 2 Jul 2021 01:49:28 -0700 Subject: [PATCH 41/50] Add Windows limit for ResolveLinkTarget to remarks --- src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs | 2 +- src/libraries/System.Private.CoreLib/src/System/IO/File.cs | 2 +- .../System.Private.CoreLib/src/System/IO/FileSystemInfo.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 13a405c72e826b..f6bc4f42b064d2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs @@ -351,7 +351,7 @@ public static FileSystemInfo CreateSymbolicLink(string path, string pathToTarget /// 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. + /// When is , the maximum number of symbolic links that are followed are 40 on Unix and 63 on Windows. public static 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 e67b658e709631..8480e46a094d3e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs @@ -1041,7 +1041,7 @@ public static FileSystemInfo CreateSymbolicLink(string path, string pathToTarget /// 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. + /// When is , the maximum number of symbolic links that are followed are 40 on Unix and 63 on Windows. public static FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) { FileSystem.VerifyValidPath(linkPath, nameof(linkPath)); 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 6f3df4cf72de67..b58955a92160a7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs @@ -151,7 +151,7 @@ public void CreateAsSymbolicLink(string pathToTarget) /// 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. + /// When is , the maximum number of symbolic links that are followed are 40 on Unix and 63 on Windows. public FileSystemInfo? ResolveLinkTarget(bool returnFinalTarget = false) => FileSystem.ResolveLinkTarget(FullPath, returnFinalTarget, this is DirectoryInfo); From 7f37a86bc5842c834394fe2ce59b45ca88a60bc0 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Fri, 2 Jul 2021 01:51:03 -0700 Subject: [PATCH 42/50] Remove pipe test --- .../tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystem.cs | 1 - 1 file changed, 1 deletion(-) 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 65039d43745e9d..3fbfb0a96ef5f3 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 @@ -514,7 +514,6 @@ internal static IEnumerable PathToTargetData yield return Path.Combine(@"\\?\", Path.GetTempPath(), "foo"); // UNC yield return @"\\SERVER\share\path"; - yield return @"\\.\pipe\foo"; } else { From 3e36f72df85605aa37e1e8c8b07729f56d05eb77 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 6 Jul 2021 16:25:08 -0700 Subject: [PATCH 43/50] Remove lstat from ResolveLinkTarget --- .../src/System/IO/FileSystem.Unix.cs | 76 +++++++++---------- 1 file changed, 36 insertions(+), 40 deletions(-) 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 2041146e92008c..dabdf76835ab42 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 @@ -556,63 +556,61 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i internal static FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget, bool isDirectory) { - // throws if the current link file does not exist - Interop.CheckIo(Interop.Sys.LStat(linkPath, out Interop.Sys.FileStatus info), linkPath, isDirectory); + ValueStringBuilder sb = new(Interop.DefaultPathBufferSize); + sb.Append(linkPath); - if ((info.Mode & Interop.Sys.FileTypes.S_IFMT) != Interop.Sys.FileTypes.S_IFLNK) + string? linkTarget = GetLinkTarget(linkPath, isDirectory: false /* Irrelevant in Unix */); + if (linkTarget == null) { - return null; - } + sb.Dispose(); + Interop.Error error = Interop.Sys.GetLastError(); + // Not a link, return null + if (error == Interop.Error.EINVAL) + { + return null; + } - ValueStringBuilder sb = new(stackalloc char[Interop.DefaultPathBufferSize]); - sb.Append(linkPath); + throw Interop.GetExceptionForIoErrno(new Interop.ErrorInfo(error), linkPath, isDirectory); + } - if (returnFinalTarget) + if (!returnFinalTarget) { - int visitCount = 0; - while (true) - { - if (!TryGetLinkTarget(ref sb)) - { - // We finally found the final target: either - // this file does not exist (broken links are acceptable) - // or this file is not a link - break; - } + GetLinkTargetFullPath(ref sb, linkTarget); + } + else + { + string? current = linkTarget; + int visitCount = 1; - visitCount++; + while (current != null) + { if (visitCount > MaxFollowedLinks) { + sb.Dispose(); // We went over the limit and couldn't reach the final target throw new IOException(SR.Format(SR.IO_TooManySymbolicLinkLevels, linkPath)); } + + GetLinkTargetFullPath(ref sb, current); + current = GetLinkTarget(sb.AsSpan(), isDirectory: false); + visitCount++; } } - else - { - TryGetLinkTarget(ref sb); - } Debug.Assert(sb.Length > 0); + linkTarget = sb.ToString(); // ToString disposes return isDirectory ? - new DirectoryInfo(sb.ToString()) : - new FileInfo(sb.ToString()); // ToString disposes + new DirectoryInfo(linkTarget) : + new FileInfo(linkTarget); - static bool TryGetLinkTarget(ref ValueStringBuilder sb) + // In case of link target being relative: + // Preserve the full path of the directory of the previous path + // so the final target is returned with a valid full path + static void GetLinkTargetFullPath(ref ValueStringBuilder sb, ReadOnlySpan linkTarget) { - string? linkTarget = GetLinkTarget(sb.AsSpan(), isDirectory: false /* Irrelevant in Unix */); - if (string.IsNullOrEmpty(linkTarget)) + if (PathInternal.IsPartiallyQualified(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); } @@ -620,9 +618,7 @@ static bool TryGetLinkTarget(ref ValueStringBuilder sb) { sb.Length = 0; } - sb.Append(linkTarget.AsSpan()); - - return true; + sb.Append(linkTarget); } } } From 6f5b99a53ec8ff29c85f59662614b17ff40d998f Mon Sep 17 00:00:00 2001 From: David Cantu Date: Wed, 7 Jul 2021 20:38:30 -0700 Subject: [PATCH 44/50] Avoid testing the reparse point/soft link limit very precisely --- .../BaseSymbolicLinks.FileSystem.cs | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) 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 3fbfb0a96ef5f3..5f3c300ba3e12f 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 @@ -262,50 +262,50 @@ public void ResolveLinkTarget_ReturnFinalTarget_Relative_WithRedundantSegments() filePath: filePath); } - [Fact] - public void ResolveLinkTarget_ReturnFinalTarget_MaxFollowedLinks() + [Theory] + [InlineData(1, false)] + [InlineData(10, false)] + [InlineData(30, false)] // Close to Windows limit when reparsing absolute paths, for relative paths the limit is supposedly 63. + [InlineData(1, true)] + [InlineData(10, true)] + [InlineData(30, true)] + [InlineData(40, true)] // The limit specified in Unix, this is the same for absolute and relative paths. + public void ResolveLinkTarget_ReturnFinalTarget_ChainOfLinks_Succeeds(int length, bool relative) { - if (OperatingSystem.IsWindows()) - { - // As per the docs, this should be 63 but I don't see that in practice. - // https://docs.microsoft.com/windows/win32/fileio/reparse-points - Verify(limit: 62, relative: true); - Verify(limit: 31, relative: false); - } - else - { - Verify(limit: 40, relative: true); - Verify(limit: 40, relative: false); - } - - void Verify(int limit, bool relative) - { - string? root = relative ? Directory.CreateDirectory(GetRandomFilePath()).FullName : null; - string finalTarget = GetFullPath(root); + string target = GetTestFilePath(); + CreateFileOrDirectory(target); - CreateFileOrDirectory(finalTarget); - string previousPath = finalTarget; + string tail = CreateChainOfLinks(target, length, relative); + FileSystemInfo targetInfo = ResolveLinkTarget(tail, returnFinalTarget: true); + Assert.Equal(target, targetInfo.FullName); + } - for (int i = 0; i < limit; i++) - { - string currentLinkPath = GetFullPath(root); - CreateSymbolicLink(currentLinkPath, GetLinkTargetPath(previousPath, relative)); + [Theory] + // 100 is way beyond the limit, we just want to make sure that a nice exception is thrown when its exceeded. + // We also don't want to test for a very precise limit given that it is very inconsistent across Windows versions. + [InlineData(100, false)] + [InlineData(100, true)] + public void ResolveLinkTarget_ReturnFinalTarget_ChainOfLinks_ExceedsLimit_Throws(int length, bool relative) + { + string target = GetTestFilePath(); + CreateFileOrDirectory(target); - previousPath = currentLinkPath; - } + string tail = CreateChainOfLinks(target, length, relative); + Assert.Throws(() => ResolveLinkTarget(tail, returnFinalTarget: true)); + } - // This is the edge of the limit - FileSystemInfo linkInfo = ResolveLinkTarget(previousPath, returnFinalTarget: true); - Assert.Equal(finalTarget, linkInfo.FullName); + private string CreateChainOfLinks(string target, int length, bool relative) + { + string previousPath = target; - // One after the limit - linkInfo = CreateSymbolicLink(GetFullPath(root), GetLinkTargetPath(previousPath, relative)); - Assert.Throws(() => ResolveLinkTarget(linkInfo.FullName, returnFinalTarget: true)); + for (int i = 0; i < length; i++) + { + string currentLinkPath = GetTestFilePath(); + CreateSymbolicLink(currentLinkPath, relative ? Path.GetFileName(previousPath) : previousPath); + previousPath = currentLinkPath; } - string GetFullPath(string? root) => root != null ? Path.Join(root, GetRandomFileName()) : GetRandomFilePath(); - - string GetLinkTargetPath(string fullPath, bool relative) => relative ? Path.GetFileName(fullPath) : fullPath; + return previousPath; } [Fact] From 2d09f7eb36d8affbd8b7d45fad10ab7da96afc0f Mon Sep 17 00:00:00 2001 From: David Cantu Date: Wed, 7 Jul 2021 21:33:04 -0700 Subject: [PATCH 45/50] Fix LinkTarget invalidation logic --- .../BaseSymbolicLinks.FileSystemInfo.cs | 34 +++++++++++++++++++ .../src/System/IO/FileSystemInfo.Unix.cs | 6 +++- .../src/System/IO/FileSystemInfo.Windows.cs | 1 + .../src/System/IO/FileSystemInfo.cs | 18 ++++++++-- 4 files changed, 56 insertions(+), 3 deletions(-) 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 6422b01554cc40..2f6d26c5e3446d 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 @@ -23,6 +23,18 @@ protected override FileSystemInfo CreateSymbolicLink(string path, string pathToT protected override FileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) => GetFileSystemInfo(linkPath).ResolveLinkTarget(returnFinalTarget); + private void Delete(string path) + { + if (IsDirectoryTest) + { + Directory.Delete(path); + } + else + { + File.Delete(path); + } + } + [Fact] public void LinkTarget_ReturnsNull_NotExists() { @@ -51,6 +63,28 @@ public void LinkTarget_Succeeds(string pathToTarget) Assert.Equal(pathToTarget, linkInfo.LinkTarget); } + [Fact] + public void LinkTarget_RefreshesCorrectly() + { + string path = GetRandomLinkPath(); + string pathToTarget = GetTestFileName(); + FileSystemInfo linkInfo = CreateSymbolicLink(path, pathToTarget); + Assert.Equal(pathToTarget, linkInfo.LinkTarget); + + Delete(path); + Assert.Equal(pathToTarget, linkInfo.LinkTarget); + + linkInfo.Refresh(); + Assert.Null(linkInfo.LinkTarget); + + string newPathToTarget = GetTestFileName(); + FileSystemInfo newLinkInfo = CreateSymbolicLink(path, newPathToTarget); + + linkInfo.Refresh(); + Assert.Equal(newPathToTarget, linkInfo.LinkTarget); + Assert.Equal(newLinkInfo.LinkTarget, linkInfo.LinkTarget); + } + public static IEnumerable LinkTarget_PathToTarget_Data { get 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 1d9f93da102910..d59c90f23104ff 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 @@ -63,7 +63,11 @@ internal DateTimeOffset LastWriteTimeCore internal long LengthCore => _fileStatus.GetLength(FullPath); - public void Refresh() => _fileStatus.RefreshCaches(FullPath); + public void Refresh() + { + _linkTargetIsValid = false; + _fileStatus.RefreshCaches(FullPath); + } internal static void ThrowNotFound(string path) { 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 ddd7caab688f88..ee9552d9e2ee1c 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 @@ -151,6 +151,7 @@ private void EnsureDataInitialized() public void Refresh() { + _linkTargetIsValid = false; // This should not throw, instead we store the result so that we can throw it // when someone actually accesses a property _dataInitialized = FileSystem.FillAttributeInfo(FullPath, ref _data, returnErrorOnNotFound: false); 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 b58955a92160a7..18e1f4fdd4cd51 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs @@ -20,6 +20,7 @@ public abstract partial class FileSystemInfo : MarshalByRefObject, ISerializable internal string _name = null!; // Fields initiated in derived classes private string? _linkTarget; + private bool _linkTargetIsValid; protected FileSystemInfo(SerializationInfo info, StreamingContext context) { @@ -28,7 +29,7 @@ protected FileSystemInfo(SerializationInfo info, StreamingContext context) internal void Invalidate() { - _linkTarget = null; + _linkTargetIsValid = false; InvalidateCore(); } @@ -119,7 +120,20 @@ 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, this is DirectoryInfo); + public string? LinkTarget + { + get + { + if (_linkTargetIsValid) + { + return _linkTarget; + } + + _linkTarget = FileSystem.GetLinkTarget(FullPath, this is DirectoryInfo); + _linkTargetIsValid = true; + return _linkTarget; + } + } /// /// Creates a symbolic link located in that points to the specified . From 4b5e133ef52e11039641976c38e21d63621d8135 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Wed, 7 Jul 2021 22:01:40 -0700 Subject: [PATCH 46/50] Make returnFinalTarget no longer optional --- .../BaseSymbolicLinks.FileSystem.cs | 18 +++++++++--------- .../BaseSymbolicLinks.FileSystemInfo.cs | 2 +- .../tests/Directory/SymbolicLinks.cs | 2 +- .../tests/File/SymbolicLinks.cs | 2 +- .../src/System/IO/FileSystemInfo.Windows.cs | 9 +++++++-- .../src/System/IO/FileSystemInfo.cs | 2 +- .../System.Runtime/ref/System.Runtime.cs | 2 +- 7 files changed, 21 insertions(+), 16 deletions(-) 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 5f3c300ba3e12f..db040c2a44c73e 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 @@ -36,7 +36,7 @@ private void CreateSymbolicLink_Opposite(string path, string pathToTarget) } /// Calls the actual public API for resolving the symbolic link target. - protected abstract FileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget = false); + protected abstract FileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget); [Fact] public void CreateSymbolicLink_NullPathToTarget() @@ -141,7 +141,7 @@ public void CreateSymbolicLink_AbsoluteTargetPath_NonExistentTarget() protected void ResolveLinkTarget_Throws_NotExists_Internal() where T : Exception { string path = GetRandomFilePath(); - Assert.Throws(() => ResolveLinkTarget(path)); + Assert.Throws(() => ResolveLinkTarget(path, returnFinalTarget: false)); Assert.Throws(() => ResolveLinkTarget(path, returnFinalTarget: true)); } @@ -320,8 +320,8 @@ public void DetectSymbolicLinkCycle() FileSystemInfo link2Info = CreateSymbolicLink(link2Path, link1Path); // Can get targets without following symlinks - FileSystemInfo link1Target = ResolveLinkTarget(link1Path); - FileSystemInfo link2Target = ResolveLinkTarget(link2Path); + FileSystemInfo link1Target = ResolveLinkTarget(link1Path, returnFinalTarget: false); + FileSystemInfo link2Target = ResolveLinkTarget(link2Path, returnFinalTarget: false); // Cannot get target when following symlinks Assert.Throws(() => ResolveLinkTarget(link1Path, returnFinalTarget: true)); @@ -337,7 +337,7 @@ public void DetectLinkReferenceToSelf() FileSystemInfo linkInfo = CreateSymbolicLink(linkPath, linkPath); // Can get target without following symlinks - FileSystemInfo linkTarget = ResolveLinkTarget(linkPath); + FileSystemInfo linkTarget = ResolveLinkTarget(linkPath, returnFinalTarget: false); // Cannot get target when following symlinks Assert.Throws(() => ResolveLinkTarget(linkPath, returnFinalTarget: true)); @@ -406,7 +406,7 @@ private void VerifySymbolicLinkAndResolvedTarget(string linkPath, string expecte 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); + FileSystemInfo target = ResolveLinkTarget(linkPath, returnFinalTarget: false); AssertIsCorrectTypeAndDirectoryAttribute(target); Assert.True(Path.IsPathFullyQualified(target.FullName)); } @@ -438,7 +438,7 @@ private void ResolveLinkTarget_ReturnFinalTarget(string link1Path, string link1T Assert.Equal(link1Target, link1Info.LinkTarget); // link1: do not follow symlinks - FileSystemInfo link1TargetInfo = ResolveLinkTarget(link1Path); + FileSystemInfo link1TargetInfo = ResolveLinkTarget(link1Path, returnFinalTarget: false); Assert.True(link1TargetInfo.Exists); AssertIsCorrectTypeAndDirectoryAttribute(link1TargetInfo); Assert.True(link1TargetInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)); @@ -446,7 +446,7 @@ private void ResolveLinkTarget_ReturnFinalTarget(string link1Path, string link1T Assert.Equal(link2Target, link1TargetInfo.LinkTarget); // link2: do not follow symlinks - FileSystemInfo link2TargetInfo = ResolveLinkTarget(link2Path); + FileSystemInfo link2TargetInfo = ResolveLinkTarget(link2Path, returnFinalTarget: false); Assert.True(link2TargetInfo.Exists); AssertIsCorrectTypeAndDirectoryAttribute(link2TargetInfo); Assert.False(link2TargetInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)); @@ -477,7 +477,7 @@ protected void CreateSymbolicLink_PathToTarget_RelativeToLinkPath_Internal(bool // Create a link with a similar Target Path to the one of our dummy file or directory. FileSystemInfo linkInfo = CreateSymbolicLink(linkPath, fileOrDirectoryInCwd); - FileSystemInfo targetInfo = linkInfo.ResolveLinkTarget(); + FileSystemInfo targetInfo = linkInfo.ResolveLinkTarget(returnFinalTarget: false); // Verify that Target is resolved and is relative to Link's directory and not to the cwd. Assert.False(targetInfo.Exists); 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 2f6d26c5e3446d..77d085c944f2a0 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 @@ -20,7 +20,7 @@ protected override FileSystemInfo CreateSymbolicLink(string path, string pathToT return link; } - protected override FileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) + protected override FileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget) => GetFileSystemInfo(linkPath).ResolveLinkTarget(returnFinalTarget); private void Delete(string path) diff --git a/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs index 8ebe0fa9544d0d..d3d04ca2e54888 100644 --- a/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs +++ b/src/libraries/System.IO.FileSystem/tests/Directory/SymbolicLinks.cs @@ -26,7 +26,7 @@ protected override void CreateFileOrDirectory(string path, bool createOpposite = protected override FileSystemInfo CreateSymbolicLink(string path, string pathToTarget) => Directory.CreateSymbolicLink(path, pathToTarget); - protected override FileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) => + protected override FileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget) => Directory.ResolveLinkTarget(linkPath, returnFinalTarget); protected override void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo linkInfo) diff --git a/src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs index 17d1766d6b795e..4c41fa7bb16576 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/SymbolicLinks.cs @@ -26,7 +26,7 @@ protected override void CreateFileOrDirectory(string path, bool createOpposite = protected override FileSystemInfo CreateSymbolicLink(string path, string pathToTarget) => File.CreateSymbolicLink(path, pathToTarget); - protected override FileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) => + protected override FileSystemInfo ResolveLinkTarget(string linkPath, bool returnFinalTarget) => File.ResolveLinkTarget(linkPath, returnFinalTarget); protected override void AssertIsCorrectTypeAndDirectoryAttribute(FileSystemInfo linkInfo) 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 ee9552d9e2ee1c..bb1440851733cf 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 @@ -74,7 +74,7 @@ internal bool ExistsCore get { if (_dataInitialized == -1) - Refresh(); + RefreshCore(); if (_dataInitialized != 0) { // Refresh was unable to initialize the data. @@ -142,7 +142,7 @@ private void EnsureDataInitialized() if (_dataInitialized == -1) { _data = default; - Refresh(); + RefreshCore(); } if (_dataInitialized != 0) // Refresh was unable to initialize the data @@ -152,6 +152,11 @@ private void EnsureDataInitialized() public void Refresh() { _linkTargetIsValid = false; + RefreshCore(); + } + + private void RefreshCore() + { // This should not throw, instead we store the result so that we can throw it // when someone actually accesses a property _dataInitialized = FileSystem.FillAttributeInfo(FullPath, ref _data, returnErrorOnNotFound: false); 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 18e1f4fdd4cd51..3beed9e25635e2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileSystemInfo.cs @@ -166,7 +166,7 @@ public void CreateAsSymbolicLink(string pathToTarget) /// -or- /// Too many levels of symbolic links. /// When is , the maximum number of symbolic links that are followed are 40 on Unix and 63 on Windows. - public FileSystemInfo? ResolveLinkTarget(bool returnFinalTarget = false) => + public FileSystemInfo? ResolveLinkTarget(bool returnFinalTarget) => FileSystem.ResolveLinkTarget(FullPath, returnFinalTarget, this is DirectoryInfo); /// diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 166acefa454f22..0f5102de7b8dd2 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -7482,7 +7482,7 @@ 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 System.IO.FileSystemInfo? ResolveLinkTarget(bool returnFinalTarget) { throw null; } public override string ToString() { throw null; } } public sealed partial class DirectoryInfo : System.IO.FileSystemInfo From 48e1af1ec2f4934c0992fa95964dbfe402ee77db Mon Sep 17 00:00:00 2001 From: David Cantu Date: Thu, 8 Jul 2021 09:18:51 -0700 Subject: [PATCH 47/50] Try to fix failing tests found in CI --- .../Base/SymbolicLinks/BaseSymbolicLinks.FileSystem.cs | 4 +--- .../Base/SymbolicLinks/BaseSymbolicLinks.FileSystemInfo.cs | 6 ++++-- 2 files changed, 5 insertions(+), 5 deletions(-) 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 db040c2a44c73e..6d31a41077f077 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 @@ -265,11 +265,9 @@ public void ResolveLinkTarget_ReturnFinalTarget_Relative_WithRedundantSegments() [Theory] [InlineData(1, false)] [InlineData(10, false)] - [InlineData(30, false)] // Close to Windows limit when reparsing absolute paths, for relative paths the limit is supposedly 63. [InlineData(1, true)] [InlineData(10, true)] - [InlineData(30, true)] - [InlineData(40, true)] // The limit specified in Unix, this is the same for absolute and relative paths. + [InlineData(40, true)] public void ResolveLinkTarget_ReturnFinalTarget_ChainOfLinks_Succeeds(int length, bool relative) { string target = GetTestFilePath(); 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 77d085c944f2a0..294ae1894492ae 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 @@ -67,7 +67,8 @@ public void LinkTarget_Succeeds(string pathToTarget) public void LinkTarget_RefreshesCorrectly() { string path = GetRandomLinkPath(); - string pathToTarget = GetTestFileName(); + string pathToTarget = GetTestFilePath(); + CreateFileOrDirectory(pathToTarget); FileSystemInfo linkInfo = CreateSymbolicLink(path, pathToTarget); Assert.Equal(pathToTarget, linkInfo.LinkTarget); @@ -77,7 +78,8 @@ public void LinkTarget_RefreshesCorrectly() linkInfo.Refresh(); Assert.Null(linkInfo.LinkTarget); - string newPathToTarget = GetTestFileName(); + string newPathToTarget = GetTestFilePath(); + CreateFileOrDirectory(newPathToTarget); FileSystemInfo newLinkInfo = CreateSymbolicLink(path, newPathToTarget); linkInfo.Refresh(); From 0495f5972e71739e9f34665cb547b1a3721a5fe7 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Thu, 8 Jul 2021 16:37:06 -0700 Subject: [PATCH 48/50] Try smaller chains of links --- .../tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystem.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 6d31a41077f077..603804d199a130 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 @@ -265,9 +265,10 @@ public void ResolveLinkTarget_ReturnFinalTarget_Relative_WithRedundantSegments() [Theory] [InlineData(1, false)] [InlineData(10, false)] + [InlineData(20, false)] [InlineData(1, true)] [InlineData(10, true)] - [InlineData(40, true)] + [InlineData(20, true)] public void ResolveLinkTarget_ReturnFinalTarget_ChainOfLinks_Succeeds(int length, bool relative) { string target = GetTestFilePath(); @@ -279,7 +280,7 @@ public void ResolveLinkTarget_ReturnFinalTarget_ChainOfLinks_Succeeds(int length } [Theory] - // 100 is way beyond the limit, we just want to make sure that a nice exception is thrown when its exceeded. + // 100 is way beyond the limit (63 in Windows and 40 in Unix), we just want to make sure that a nice exception is thrown when its exceeded. // We also don't want to test for a very precise limit given that it is very inconsistent across Windows versions. [InlineData(100, false)] [InlineData(100, true)] From 40cb7371a4526107512a06c6c36b3b33c21c7e68 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Thu, 8 Jul 2021 22:50:33 -0700 Subject: [PATCH 49/50] Avoid using GetTestFilePath in order to correctly compare against the real temp path Bring back previous InlineData since it wasn't the cause of the CI issue --- .../SymbolicLinks/BaseSymbolicLinks.FileSystem.cs | 12 ++++++------ .../BaseSymbolicLinks.FileSystemInfo.cs | 6 +++--- .../tests/Base/SymbolicLinks/BaseSymbolicLinks.cs | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) 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 603804d199a130..b6ec719d2adc62 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 @@ -150,7 +150,7 @@ protected void ResolveLinkTarget_Throws_NotExists_Internal() where T : Except [InlineData(true)] public void ResolveLinkTarget_ReturnsNull_NotALink(bool returnFinalTarget) { - string path = GetTestFilePath(); + string path = GetRandomFilePath(); CreateFileOrDirectory(path); Assert.Null(ResolveLinkTarget(path, returnFinalTarget)); } @@ -265,13 +265,13 @@ public void ResolveLinkTarget_ReturnFinalTarget_Relative_WithRedundantSegments() [Theory] [InlineData(1, false)] [InlineData(10, false)] - [InlineData(20, false)] + [InlineData(30, false)] [InlineData(1, true)] [InlineData(10, true)] - [InlineData(20, true)] + [InlineData(30, true)] public void ResolveLinkTarget_ReturnFinalTarget_ChainOfLinks_Succeeds(int length, bool relative) { - string target = GetTestFilePath(); + string target = GetRandomFilePath(); CreateFileOrDirectory(target); string tail = CreateChainOfLinks(target, length, relative); @@ -286,7 +286,7 @@ public void ResolveLinkTarget_ReturnFinalTarget_ChainOfLinks_Succeeds(int length [InlineData(100, true)] public void ResolveLinkTarget_ReturnFinalTarget_ChainOfLinks_ExceedsLimit_Throws(int length, bool relative) { - string target = GetTestFilePath(); + string target = GetRandomFilePath(); CreateFileOrDirectory(target); string tail = CreateChainOfLinks(target, length, relative); @@ -299,7 +299,7 @@ private string CreateChainOfLinks(string target, int length, bool relative) for (int i = 0; i < length; i++) { - string currentLinkPath = GetTestFilePath(); + string currentLinkPath = GetRandomLinkPath(); CreateSymbolicLink(currentLinkPath, relative ? Path.GetFileName(previousPath) : previousPath); previousPath = currentLinkPath; } 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 294ae1894492ae..e82864248eb434 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 @@ -45,7 +45,7 @@ public void LinkTarget_ReturnsNull_NotExists() [Fact] public void LinkTarget_ReturnsNull_NotALink() { - string path = GetTestFilePath(); + string path = GetRandomFilePath(); CreateFileOrDirectory(path); FileSystemInfo info = GetFileSystemInfo(path); @@ -67,7 +67,7 @@ public void LinkTarget_Succeeds(string pathToTarget) public void LinkTarget_RefreshesCorrectly() { string path = GetRandomLinkPath(); - string pathToTarget = GetTestFilePath(); + string pathToTarget = GetRandomFilePath(); CreateFileOrDirectory(pathToTarget); FileSystemInfo linkInfo = CreateSymbolicLink(path, pathToTarget); Assert.Equal(pathToTarget, linkInfo.LinkTarget); @@ -78,7 +78,7 @@ public void LinkTarget_RefreshesCorrectly() linkInfo.Refresh(); Assert.Null(linkInfo.LinkTarget); - string newPathToTarget = GetTestFilePath(); + string newPathToTarget = GetRandomFilePath(); CreateFileOrDirectory(newPathToTarget); FileSystemInfo newLinkInfo = CreateSymbolicLink(path, newPathToTarget); diff --git a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs index bb7673bbff72b3..d8869ff1581217 100644 --- a/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs +++ b/src/libraries/System.IO.FileSystem/tests/Base/SymbolicLinks/BaseSymbolicLinks.cs @@ -15,8 +15,8 @@ public abstract partial class BaseSymbolicLinks : FileSystemTest { protected DirectoryInfo CreateDirectoryContainingSelfReferencingSymbolicLink() { - DirectoryInfo testDirectory = Directory.CreateDirectory(GetTestFilePath()); - string pathToLink = Path.Join(testDirectory.FullName, GetTestFileName()); + DirectoryInfo testDirectory = Directory.CreateDirectory(GetRandomDirPath()); + string pathToLink = Path.Join(testDirectory.FullName, GetRandomDirName()); Assert.True(MountHelper.CreateSymbolicLink(pathToLink, pathToLink, isDirectory: true)); // Create a symlink cycle return testDirectory; } From 995ead537bc9d0f82786522691a879fbdef37278 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Fri, 9 Jul 2021 00:02:08 -0700 Subject: [PATCH 50/50] Use chain length of 20 instead of 30, which fails in Win 1809 --- .../tests/Base/SymbolicLinks/BaseSymbolicLinks.FileSystem.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 b6ec719d2adc62..6f9a1ace40045f 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 @@ -265,10 +265,10 @@ public void ResolveLinkTarget_ReturnFinalTarget_Relative_WithRedundantSegments() [Theory] [InlineData(1, false)] [InlineData(10, false)] - [InlineData(30, false)] + [InlineData(20, false)] [InlineData(1, true)] [InlineData(10, true)] - [InlineData(30, true)] + [InlineData(20, true)] public void ResolveLinkTarget_ReturnFinalTarget_ChainOfLinks_Succeeds(int length, bool relative) { string target = GetRandomFilePath();