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 @@
+