diff --git a/src/libraries/Common/tests/System/IO/ReparsePointUtilities.cs b/src/libraries/Common/tests/System/IO/ReparsePointUtilities.cs index 5e9b33146e48d3..bfd24ac0c13dae 100644 --- a/src/libraries/Common/tests/System/IO/ReparsePointUtilities.cs +++ b/src/libraries/Common/tests/System/IO/ReparsePointUtilities.cs @@ -32,6 +32,25 @@ public static partial class MountHelper // Helper for ConditionalClass attributes internal static bool IsSubstAvailable => PlatformDetection.IsSubstAvailable; + /// + /// Verifies that hard link creation is supported on the file system. + /// + internal static bool CanCreateHardLinks => s_canCreateHardLinks.Value; + + private static readonly Lazy s_canCreateHardLinks = new Lazy(() => + { + bool success = true; + + // Verify file hard link creation + string path = Path.GetTempFileName(); + string linkPath = path + ".link"; + success = CreateHardLink(linkPath: linkPath, targetPath: path); + try { File.Delete(path); } catch { } + try { File.Delete(linkPath); } catch { } + + return success; + }); + /// /// In some cases (such as when running without elevated privileges), /// the symbolic link may fail to create. Only run this test if it creates @@ -65,6 +84,46 @@ public static partial class MountHelper return success; }); + /// Creates a hard link using command line tools. + public static bool CreateHardLink(string linkPath, string targetPath) + { + // It's easy to get the parameters backwards. + Assert.EndsWith(".link", linkPath); + if (linkPath != targetPath) // testing loop + Assert.False(targetPath.EndsWith(".link"), $"{targetPath} should not end with .link"); + +#if NETFRAMEWORK + bool isWindows = true; +#else + if (!IsProcessStartSupported()) + { + return false; + } + + bool isWindows = OperatingSystem.IsWindows(); +#endif + + using Process hardLinkProcess = new Process(); + if (isWindows) + { + hardLinkProcess.StartInfo.FileName = "cmd"; + hardLinkProcess.StartInfo.Arguments = string.Format("/c mklink /H \"{0}\" \"{1}\"", linkPath, targetPath); + } + else + { + hardLinkProcess.StartInfo.FileName = "/bin/ln"; + hardLinkProcess.StartInfo.Arguments = string.Format("\"{0}\" \"{1}\"", targetPath, linkPath); + } + hardLinkProcess.StartInfo.UseShellExecute = false; + hardLinkProcess.StartInfo.RedirectStandardOutput = true; + + hardLinkProcess.Start(); + + hardLinkProcess.WaitForExit(); + + return hardLinkProcess.ExitCode == 0; + } + /// Creates a symbolic link using command line tools. public static bool CreateSymbolicLink(string linkPath, string targetPath, bool isDirectory) { @@ -76,13 +135,14 @@ public static bool CreateSymbolicLink(string linkPath, string targetPath, bool i #if NETFRAMEWORK bool isWindows = true; #else - if (OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsMacCatalyst() || OperatingSystem.IsBrowser() || OperatingSystem.IsWasi()) // OSes that don't support Process.Start() + if (!IsProcessStartSupported()) { return false; } bool isWindows = OperatingSystem.IsWindows(); #endif + using Process symLinkProcess = new Process(); if (isWindows) { @@ -101,7 +161,16 @@ public static bool CreateSymbolicLink(string linkPath, string targetPath, bool i symLinkProcess.WaitForExit(); - return (symLinkProcess.ExitCode == 0); + return symLinkProcess.ExitCode == 0; + } + + private static bool IsProcessStartSupported() + { +#if NETFRAMEWORK + return true; +#else + return !(OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsMacCatalyst() || OperatingSystem.IsBrowser() || OperatingSystem.IsWasi()); // OSes that don't support Process.Start() +#endif } /// On Windows, creates a junction using command line tools. diff --git a/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj b/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj index 465e91734c08aa..23c69846b94583 100644 --- a/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj +++ b/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj @@ -45,13 +45,8 @@ - - - - - diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Unix.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Unix.cs index 95158c000fd7fa..d7c1777c96fe7c 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Unix.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Unix.cs @@ -37,7 +37,7 @@ private void ExtractAsHardLink(string targetFilePath, string hardLinkFilePath) Debug.Assert(EntryType is TarEntryType.HardLink); Debug.Assert(!string.IsNullOrEmpty(targetFilePath)); Debug.Assert(!string.IsNullOrEmpty(hardLinkFilePath)); - Interop.CheckIo(Interop.Sys.Link(targetFilePath, hardLinkFilePath), hardLinkFilePath); + File.CreateHardLink(hardLinkFilePath, targetFilePath); } } } diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Windows.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Windows.cs index 17d48681719988..c0767922842fff 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Windows.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.Windows.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.IO; using Microsoft.Win32.SafeHandles; namespace System.Formats.Tar @@ -37,7 +38,7 @@ private void ExtractAsHardLink(string targetFilePath, string hardLinkFilePath) Debug.Assert(EntryType is TarEntryType.HardLink); Debug.Assert(!string.IsNullOrEmpty(targetFilePath)); Debug.Assert(!string.IsNullOrEmpty(hardLinkFilePath)); - Interop.Kernel32.CreateHardLink(hardLinkFilePath, targetFilePath); + File.CreateHardLink(hardLinkFilePath, targetFilePath); } #pragma warning restore IDE0060 } diff --git a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj index 144749edded032..2468dbe20d768f 100644 --- a/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj +++ b/src/libraries/System.Formats.Tar/tests/System.Formats.Tar.Tests.csproj @@ -77,13 +77,7 @@ - - - - - - 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 3383f70a0b4e64..d3b69efe615e4f 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 @@ -1816,6 +1816,9 @@ Common\Interop\Windows\Kernel32\Interop.CreateFile_IntPtr.cs + + Common\Interop\Windows\Kernel32\Interop.CreateHardLink.cs + Common\Interop\Windows\Kernel32\Interop.CreateSymbolicLink.cs 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 e7c4e948d6c3ce..9ae27137e96079 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs @@ -1410,6 +1410,29 @@ public static Task AppendAllLinesAsync(string path, IEnumerable contents public static Task AppendAllLinesAsync(string path, IEnumerable contents, Encoding encoding, CancellationToken cancellationToken = default) => WriteAllLinesAsync(path, contents, encoding, append: true, cancellationToken); + /// + /// Creates a hard link located in that refers to the same file content as . + /// + /// The path where the hard link should be created. + /// The path of the hard link target. + /// A instance that wraps the newly created file. + /// or is . + /// or is empty. + /// -or- + /// or contains a null character. + /// The file specified by does not exist. + /// A file or directory already exists in the location of . + /// -or- + /// An I/O error occurred. + public static FileSystemInfo CreateHardLink(string path, string pathToTarget) + { + string fullPath = Path.GetFullPath(path); + FileSystem.VerifyValidPath(pathToTarget, nameof(pathToTarget)); + + FileSystem.CreateHardLink(path, pathToTarget); + return new FileInfo(originalPath: path, fullPath: fullPath, isNormalized: true); + } + /// /// Creates a file symbolic link identified by that points to . /// diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileInfo.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileInfo.cs index f752f64e8e0d58..199e702875af10 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileInfo.cs @@ -204,5 +204,23 @@ private StreamWriter CreateStreamWriter(bool append) Invalidate(); return streamWriter; } + + /// + /// Creates a hard link located in that refers to the same file content as . + /// + /// The path of the hard link target. + /// is . + /// is empty. + /// -or- + /// contains invalid path characters. + /// The file specified by does not exist. + /// A file or directory already exists in the location of . + /// -or- + /// An I/O error occurred. + public void CreateAsHardLink(string pathToTarget) + { + FileSystem.VerifyValidPath(pathToTarget, nameof(pathToTarget)); + FileSystem.CreateHardLink(OriginalPath, pathToTarget); + } } } 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 69ed0a6016adcf..db94041abb9851 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 @@ -667,6 +667,11 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i } #pragma warning restore IDE0060 + internal static void CreateHardLink(string path, string pathToTarget) + { + Interop.CheckIo(Interop.Sys.Link(pathToTarget, path), path); + } + internal static FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget, bool isDirectory) { ValueStringBuilder sb = new(Interop.DefaultPathBufferSize); 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 e3879ddf6bf651..cc29702789a71f 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 @@ -518,6 +518,11 @@ internal static void CreateSymbolicLink(string path, string pathToTarget, bool i Interop.Kernel32.CreateSymbolicLink(path, pathToTarget, isDirectory); } + internal static void CreateHardLink(string path, string pathToTarget) + { + Interop.Kernel32.CreateHardLink(path, pathToTarget); + } + internal static FileSystemInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget, bool isDirectory) { string? targetPath = returnFinalTarget ? diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 116718056d7a9a..a71cc2643d3365 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -10292,6 +10292,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 CreateHardLink(string path, string pathToTarget) { 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")] @@ -10423,6 +10424,7 @@ public FileInfo(string fileName) { } public System.IO.FileInfo CopyTo(string destFileName) { throw null; } public System.IO.FileInfo CopyTo(string destFileName, bool overwrite) { throw null; } public System.IO.FileStream Create() { throw null; } + public void CreateAsHardLink(string pathToTarget) { } public System.IO.StreamWriter CreateText() { throw null; } [System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")] public void Decrypt() { } diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Base/HardLinks/BaseHardLinks.FileSystem.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Base/HardLinks/BaseHardLinks.FileSystem.cs new file mode 100644 index 00000000000000..ab1cae77d2a89b --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Base/HardLinks/BaseHardLinks.FileSystem.cs @@ -0,0 +1,119 @@ +// 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 Xunit; + +namespace System.IO.Tests +{ + // Contains test methods that can be used for FileInfo or File. + [ConditionalClass(typeof(MountHelper), nameof(MountHelper.CanCreateHardLinks))] + public abstract class BaseHardLinks_FileSystem : FileSystemTest + { + public BaseHardLinks_FileSystem() + { + Assert.True(MountHelper.CanCreateHardLinks); + } + + /// Creates a new file depending on the implementing class. + protected abstract void CreateFile(string path); + + protected abstract void AssertLinkExists(FileSystemInfo linkInfo); + + /// Calls the actual public API for creating a hard link. + protected abstract FileSystemInfo CreateHardLink(string path, string pathToTarget); + + [Fact] + public void CreateHardLink_NullPathToTarget() + { + Assert.Throws(() => CreateHardLink(GetRandomFilePath(), pathToTarget: null)); + } + + [Theory] + [InlineData("")] + [InlineData("\0")] + public void CreateHardLink_InvalidPathToTarget(string pathToTarget) + { + Assert.Throws(() => CreateHardLink(GetRandomFilePath(), pathToTarget)); + } + + [Fact] + public void CreateHardLink_RelativePath() + { + string relativePathToTarget = GetRandomFileName(); + string relativePath = GetRandomFileName(); + + CreateFile(relativePathToTarget); + CreateHardLink(relativePath, relativePathToTarget); + } + + [Fact] + public void CreateHardLink_TargetDoesNotExist_Throws() + { + string linkPath = GetRandomFilePath(); + string nonExistentTarget = GetRandomFilePath(); + Assert.Throws(() => CreateHardLink(linkPath, nonExistentTarget)); + } + + [Fact] + public void CreateHardLink_LinkPathAlreadyExists_Throws() + { + string targetPath = GetRandomFilePath(); + string linkPath = GetRandomFilePath(); + + CreateFile(targetPath); + CreateFile(linkPath); + + Assert.Throws(() => CreateHardLink(linkPath, targetPath)); + } + + [Fact] + public void CreateHardLink_TargetExists_Succeeds() + { + string targetPath = GetRandomFilePath(); + string linkPath = GetRandomFilePath(); + + CreateFile(targetPath); + + FileSystemInfo linkInfo = CreateHardLink(linkPath, targetPath); + AssertLinkExists(linkInfo); + + // Both files should have the same content + Assert.Equal(File.ReadAllText(targetPath), File.ReadAllText(linkPath)); + } + + [Fact] + public void CreateHardLink_ModifyViaOneLink_VisibleViaOther() + { + string targetPath = GetRandomFilePath(); + string linkPath = GetRandomFilePath(); + + File.WriteAllText(targetPath, "original"); + CreateHardLink(linkPath, targetPath); + + // Modify via link + File.WriteAllText(linkPath, "changed"); + + // Read via target + Assert.Equal("changed", File.ReadAllText(targetPath)); + + } + + [Fact] + public void CreateHardLink_DeleteOneLink_FileStillAccessible() + { + string targetPath = GetRandomFilePath(); + string linkPath = GetRandomFilePath(); + + File.WriteAllText(targetPath, "data"); + CreateHardLink(linkPath, targetPath); + + // Delete the original file + File.Delete(targetPath); + + // The link should still exist and have the data + Assert.Equal("data", File.ReadAllText(linkPath)); + } + } +} diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Base/SymbolicLinks/BaseSymbolicLinks.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Base/SymbolicLinks/BaseSymbolicLinks.cs index adaec2b9094f00..48222d38e03053 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Base/SymbolicLinks/BaseSymbolicLinks.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Base/SymbolicLinks/BaseSymbolicLinks.cs @@ -8,6 +8,7 @@ namespace System.IO.Tests { // Contains helper methods that are shared by all symbolic link test classes. + [ConditionalClass(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] public abstract partial class BaseSymbolicLinks : FileSystemTest { public BaseSymbolicLinks() diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Enumeration/SymbolicLinksTests.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Enumeration/SymbolicLinksTests.cs index 48603c927d1841..3740d1f080a9e5 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Enumeration/SymbolicLinksTests.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Enumeration/SymbolicLinksTests.cs @@ -8,7 +8,6 @@ namespace System.IO.Tests.Enumeration { - [ConditionalClass(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] public class Enumeration_SymbolicLinksTests : BaseSymbolicLinks { [Fact] diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/HardLinks.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/HardLinks.cs new file mode 100644 index 00000000000000..e67c994d401801 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/HardLinks.cs @@ -0,0 +1,21 @@ +// 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 Xunit; + +namespace System.IO.Tests +{ + public class File_HardLinks : BaseHardLinks_FileSystem + { + protected override void CreateFile(string path) => + File.Create(path).Dispose(); + + protected override void AssertLinkExists(FileSystemInfo linkInfo) => + Assert.True(linkInfo.Exists); + + protected override FileSystemInfo CreateHardLink(string path, string pathToTarget) => + File.CreateHardLink(path, pathToTarget); + } +} diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileInfo/HardLinks.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileInfo/HardLinks.cs new file mode 100644 index 00000000000000..591c38d8acc457 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileInfo/HardLinks.cs @@ -0,0 +1,27 @@ +// 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 Xunit; + +namespace System.IO.Tests +{ + public class FileInfo_HardLinks : BaseHardLinks_FileSystem + { + protected override void CreateFile(string path) => + File.Create(path).Dispose(); + + protected override void AssertLinkExists(FileSystemInfo linkInfo) => + Assert.True(linkInfo.Exists); + + protected override FileSystemInfo CreateHardLink(string path, string pathToTarget) + { + FileInfo link = new FileInfo(path); + link.CreateAsHardLink(pathToTarget); + return link; + } + + } +} + diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Junctions.Windows.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Junctions.Windows.cs index da5299946f44b2..c280008148977a 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Junctions.Windows.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Junctions.Windows.cs @@ -6,7 +6,6 @@ namespace System.IO.Tests { [PlatformSpecific(TestPlatforms.Windows)] - [ConditionalClass(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] public class Junctions : BaseSymbolicLinks { private DirectoryInfo CreateJunction(string junctionPath, string targetPath) diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj index 78bd2c7bc6379c..82f488a4676546 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj @@ -24,6 +24,7 @@ + @@ -34,6 +35,7 @@ + @@ -180,6 +182,7 @@ +