diff --git a/src/Microsoft.DotNet.SignTool.Tests/SignToolTests.cs b/src/Microsoft.DotNet.SignTool.Tests/SignToolTests.cs
index bb25bde1135..d6ae31e00b5 100644
--- a/src/Microsoft.DotNet.SignTool.Tests/SignToolTests.cs
+++ b/src/Microsoft.DotNet.SignTool.Tests/SignToolTests.cs
@@ -17,6 +17,7 @@
using Xunit.Abstractions;
using Microsoft.DotNet.Build.Tasks.Installers;
using Microsoft.DotNet.StrongName;
+using System.Formats.Tar;
namespace Microsoft.DotNet.SignTool.Tests
{
@@ -584,6 +585,30 @@ private void ValidateProducedTarGZipContent(
}
}
+ private void ValidateProducedTarGZipHardlinks(
+ string tarGZipPath,
+ (string path, string linkName)[] expectedHardlinks)
+ {
+ using FileStream stream = File.OpenRead(tarGZipPath);
+ using GZipStream decompressor = new(stream, CompressionMode.Decompress);
+ using TarReader reader = new(decompressor);
+
+ var hardlinkEntries = new List<(string Name, string LinkName)>();
+ while (reader.GetNextEntry() is TarEntry entry)
+ {
+ if (entry.EntryType == TarEntryType.HardLink)
+ {
+ hardlinkEntries.Add((entry.Name, entry.LinkName));
+ }
+ }
+
+ foreach ((string path, string linkName) in expectedHardlinks)
+ {
+ hardlinkEntries.Should().Contain((Name: path, LinkName: linkName),
+ $"hardlink '{path}' -> '{linkName}' should be preserved in the repacked tarball");
+ }
+ }
+
[Fact]
public void EmptySigningList()
{
@@ -1756,8 +1781,8 @@ public void SignTarGZipFile()
///
/// Validates that tar.gz archives containing symbolic links are handled correctly.
- /// On Windows, ReadTarGZipEntriesWithExternalTar throws when symlinks are detected,
- /// so this test is skipped on Windows.
+ /// This test is Unix-only because creating symlinks on Windows requires elevation
+ /// and there are no signing scenarios that use symbolic links on Windows.
///
[UnixOnlyFactAttribute]
public void SignTarGZipFileWithSymlinks()
@@ -1794,8 +1819,7 @@ public void SignTarGZipFileWithSymlinks()
});
}
- // TODO: Remove WindowsOnlyFact once https://github.com/dotnet/arcade/issues/16484 is resolved.
- [WindowsOnlyFact]
+ [Fact]
public void SignTarGZipFileWithHardlinks()
{
// List of files to be considered for signing
@@ -1813,19 +1837,16 @@ public void SignTarGZipFileWithHardlinks()
// Overriding information
var fileSignInfo = new Dictionary();
- // All three files (original + 2 hardlinks) should be detected for signing
+ // Only the regular file (hardlink1.dll) is detected for signing.
+ // Hardlink entries are skipped (like symlinks) and preserved during repack.
ValidateFileSignInfos(itemsToSign, strongNameSignInfo, fileSignInfo, s_fileExtensionSignInfo, new[]
{
"File 'hardlink1.dll' TargetFramework='.NETStandard,Version=v2.0' Certificate='ArcadeCertTest' StrongName='ArcadeStrongTest'",
- "File 'hardlink2.dll' TargetFramework='.NETStandard,Version=v2.0' Certificate='ArcadeCertTest' StrongName='ArcadeStrongTest'",
- "File 'original.dll' TargetFramework='.NETStandard,Version=v2.0' Certificate='ArcadeCertTest' StrongName='ArcadeStrongTest'",
"File 'testHardlinks.tgz'",
},
expectedWarnings: new[]
{
$@"SIGN004: Signing 3rd party library '{Path.Combine(_tmpDir, "ContainerSigning", "0", "hardlink1.dll")}' with Microsoft certificate 'ArcadeCertTest'. The library is considered 3rd party library due to its copyright: ''.",
- $@"SIGN004: Signing 3rd party library '{Path.Combine(_tmpDir, "ContainerSigning", "1", "hardlink2.dll")}' with Microsoft certificate 'ArcadeCertTest'. The library is considered 3rd party library due to its copyright: ''.",
- $@"SIGN004: Signing 3rd party library '{Path.Combine(_tmpDir, "ContainerSigning", "2", "original.dll")}' with Microsoft certificate 'ArcadeCertTest'. The library is considered 3rd party library due to its copyright: ''.",
});
ValidateGeneratedProject(itemsToSign, strongNameSignInfo, fileSignInfo, s_fileExtensionSignInfo, new[]
@@ -1835,16 +1856,15 @@ public void SignTarGZipFileWithHardlinks()
ArcadeCertTest
ArcadeStrongTest
-
- ArcadeCertTest
- ArcadeStrongTest
-
-
- ArcadeCertTest
- ArcadeStrongTest
-
"
});
+
+ // Verify that hardlinks are preserved in the repacked tarball
+ ValidateProducedTarGZipHardlinks(Path.Combine(_tmpDir, "testHardlinks.tgz"), new[]
+ {
+ ("hardlink2.dll", "hardlink1.dll"),
+ ("original.dll", "hardlink1.dll"),
+ });
}
[Fact]
diff --git a/src/Microsoft.DotNet.SignTool/src/ZipData.cs b/src/Microsoft.DotNet.SignTool/src/ZipData.cs
index ae9676caf85..d55413eb9a5 100644
--- a/src/Microsoft.DotNet.SignTool/src/ZipData.cs
+++ b/src/Microsoft.DotNet.SignTool/src/ZipData.cs
@@ -55,15 +55,9 @@ public static IEnumerable ReadEntries(string archivePath, string t
{
if (FileSignInfo.IsTarGZip(archivePath))
{
- // TODO: Remove workaround for https://github.com/dotnet/arcade/issues/16484
- // Hardlinks are used on Windows but System.Formats.Tar doesn't fully support them yet.
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- return ReadTarGZipEntriesWithExternalTar(archivePath, tempDir, log, ignoreContent);
- }
-
return ReadTarGZipEntries(archivePath)
.Where(static entry => entry.EntryType != TarEntryType.SymbolicLink &&
+ entry.EntryType != TarEntryType.HardLink &&
entry.EntryType != TarEntryType.Directory)
.Select(static entry => new ZipDataEntry(entry.Name, entry.DataStream, entry.Length)
{
@@ -394,14 +388,6 @@ private void RepackPkgOrAppBundles(TaskLoggingHelper log, string tempDir, string
private void RepackTarGZip(TaskLoggingHelper log, string tempDir)
{
- // TODO: Remove workaround for https://github.com/dotnet/arcade/issues/16484
- // Hardlinks are used on Windows but System.Formats.Tar doesn't fully support them yet.
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- RepackTarGZipWithExternalTar(log, tempDir);
- return;
- }
-
using MemoryStream streamToCompress = new();
using (TarWriter writer = new(streamToCompress, leaveOpen: true))
{
@@ -437,103 +423,6 @@ private void RepackTarGZip(TaskLoggingHelper log, string tempDir)
}
}
- ///
- /// Read tar.gz entries using external tar.exe to properly handle hardlinks.
- /// Windows tarballs use hardlinks for deduplication, which System.Formats.Tar doesn't yet support.
- /// When tar.exe extracts hardlinks, they become regular files with the same content.
- ///
- private static IEnumerable ReadTarGZipEntriesWithExternalTar(string archivePath, string tempDir, TaskLoggingHelper log, bool ignoreContent)
- {
- string extractDir = Path.Combine(tempDir, Guid.NewGuid().ToString());
- Directory.CreateDirectory(extractDir);
-
- try
- {
- // Extract the tarball - tar.exe will resolve hardlinks to regular files
- if (!RunExternalProcess(log, "tar", $"-xzf \"{archivePath}\" -C \"{extractDir}\"", out _))
- {
- throw new Exception($"Failed to extract tar archive: {archivePath}");
- }
-
- foreach (var path in Directory.EnumerateFiles(extractDir, "*", SearchOption.AllDirectories))
- {
- // Symbolic links require elevated permissions to create on Windows. They should not be used therefore they are not supported.
- if (new FileInfo(path).LinkTarget != null)
- {
- throw new InvalidOperationException(
- $"Symbolic link detected in tar archive '{archivePath}': '{path}'. " +
- $"Tarballs containing symbolic links are not supported for signing on Windows.");
- }
-
- string relativePath = path.Substring(extractDir.Length + 1).Replace(Path.DirectorySeparatorChar, '/');
- using var stream = ignoreContent ? null : (Stream)File.Open(path, FileMode.Open);
- yield return new ZipDataEntry(relativePath, stream);
- }
- }
- finally
- {
- if (Directory.Exists(extractDir))
- {
- Directory.Delete(extractDir, recursive: true);
- }
- }
- }
-
- ///
- /// Repack tar.gz using external tar.exe to preserve hardlinks.
- /// Windows tarballs use hardlinks for deduplication, which System.Formats.Tar doesn't yet support.
- ///
- private void RepackTarGZipWithExternalTar(TaskLoggingHelper log, string tempDir)
- {
- string extractDir = Path.Combine(tempDir, Guid.NewGuid().ToString());
- Directory.CreateDirectory(extractDir);
-
- try
- {
- // Extract the tarball - tar.exe will recreate hardlinks
- if (!RunExternalProcess(log, "tar", $"-xzf \"{FileSignInfo.FullPath}\" -C \"{extractDir}\"", out _))
- {
- log.LogError($"Failed to extract tar archive: {FileSignInfo.FullPath}");
- return;
- }
-
- // Replace signed files in the extracted directory
- foreach (var path in Directory.EnumerateFiles(extractDir, "*", SearchOption.AllDirectories))
- {
- // Symbolic links are not supported by the external tar.exe signing path.
- if (new FileInfo(path).LinkTarget != null)
- {
- throw new InvalidOperationException(
- $"Symbolic link detected in tar archive '{FileSignInfo.FullPath}': '{path}'. " +
- $"Tarballs containing symbolic links are not supported for signing on Windows.");
- }
-
- string relativePath = path.Substring(extractDir.Length + 1).Replace(Path.DirectorySeparatorChar, '/');
- ZipPart? signedPart = FindNestedPart(relativePath);
-
- if (signedPart.HasValue)
- {
- log.LogMessage(MessageImportance.Low, $"Copying signed file from {signedPart.Value.FileSignInfo.FullPath} to {FileSignInfo.FullPath} -> {relativePath}");
- File.Copy(signedPart.Value.FileSignInfo.FullPath, path, overwrite: true);
- }
- }
-
- // Repack the tarball - tar.exe will detect and preserve hardlinks
- if (!RunExternalProcess(log, "tar", $"-czf \"{FileSignInfo.FullPath}\" -C \"{extractDir}\" .", out _))
- {
- log.LogError($"Failed to create tar archive: {FileSignInfo.FullPath}");
- return;
- }
- }
- finally
- {
- if (Directory.Exists(extractDir))
- {
- Directory.Delete(extractDir, recursive: true);
- }
- }
- }
-
private static IEnumerable ReadTarGZipEntries(string path)
{
using FileStream streamToDecompress = File.OpenRead(path);