diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs index 61368173b0d31..e7d6761c592c8 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs @@ -349,11 +349,17 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b throw new InvalidDataException(SR.TarEntryHardLinkOrSymlinkLinkNameEmpty); } - linkTargetPath = GetSanitizedFullPath(destinationDirectoryPath, LinkName); + linkTargetPath = GetSanitizedFullPath(destinationDirectoryPath, + Path.IsPathFullyQualified(LinkName) ? LinkName : Path.Join(Path.GetDirectoryName(fileDestinationPath), LinkName)); + if (linkTargetPath == null) { throw new IOException(SR.Format(SR.TarExtractingResultsLinkOutside, LinkName, destinationDirectoryPath)); } + + // after TarExtractingResultsLinkOutside validation, preserve the original + // symlink target path (to match behavior of other utilities). + linkTargetPath = LinkName; } return (fileDestinationPath, linkTargetPath); 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 ca1b4d99e508f..1d30aa09d0f97 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 @@ -2,7 +2,7 @@ $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-Unix true - $(LibrariesProjectRoot)/Common/tests/Resources/Strings.resx + $(MSBuildProjectDirectory)\..\src\Resources\Strings.resx true true @@ -17,6 +17,7 @@ + diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectory.File.Roundtrip.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectory.File.Roundtrip.cs new file mode 100644 index 0000000000000..460dec7578008 --- /dev/null +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectory.File.Roundtrip.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using Xunit; + +namespace System.Formats.Tar.Tests +{ + public class TarFile_CreateFromDirectory_Roundtrip_Tests : TarTestsBase + { + [ConditionalTheory(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + [InlineData("./file.txt", "subDirectory")] + [InlineData("../file.txt", "subDirectory")] + [InlineData("../file.txt", "subDirectory1/subDirectory1.1")] + [InlineData("./file.txt", "subDirectory1/subDirectory1.1")] + [InlineData("./file.txt", null)] + public void SymlinkRelativeTargets_InsideTheArchive_RoundtripsSuccessfully(string symlinkTargetPath, string subDirectory) + { + using TempDirectory root = new TempDirectory(); + + string destinationArchive = Path.Join(root.Path, "destination.tar"); + + string sourceDirectoryName = Path.Join(root.Path, "baseDirectory"); + Directory.CreateDirectory(sourceDirectoryName); + + string destinationDirectoryName = Path.Join(root.Path, "destinationDirectory"); + Directory.CreateDirectory(destinationDirectoryName); + + string sourceSubDirectory = Path.Join(sourceDirectoryName, subDirectory); + if(subDirectory != null) Directory.CreateDirectory(sourceSubDirectory); + + File.Create(Path.Join(sourceDirectoryName, subDirectory, symlinkTargetPath)).Dispose(); + File.CreateSymbolicLink(Path.Join(sourceSubDirectory, "linkToFile"), symlinkTargetPath); + + TarFile.CreateFromDirectory(sourceDirectoryName, destinationArchive, includeBaseDirectory: false); + + using FileStream archiveStream = File.OpenRead(destinationArchive); + TarFile.ExtractToDirectory(archiveStream, destinationDirectoryName, overwriteFiles: true); + + string destinationSubDirectory = Path.Join(destinationDirectoryName, subDirectory); + string symlinkPath = Path.Join(destinationSubDirectory, "linkToFile"); + Assert.True(File.Exists(symlinkPath)); + + FileInfo? fileInfo = new(symlinkPath); + Assert.Equal(symlinkTargetPath, fileInfo.LinkTarget); + + FileSystemInfo? symlinkTarget = File.ResolveLinkTarget(symlinkPath, returnFinalTarget: true); + Assert.True(File.Exists(symlinkTarget.FullName)); + } + + [ConditionalTheory(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + [InlineData("../file.txt", null)] + [InlineData("../../file.txt", "subDirectory")] + public void SymlinkRelativeTargets_OutsideTheArchive_Fails(string symlinkTargetPath, string subDirectory) + { + using TempDirectory root = new TempDirectory(); + + string destinationArchive = Path.Join(root.Path, "destination.tar"); + + string sourceDirectoryName = Path.Join(root.Path, "baseDirectory"); + Directory.CreateDirectory(sourceDirectoryName); + + string destinationDirectoryName = Path.Join(root.Path, "destinationDirectory"); + Directory.CreateDirectory(destinationDirectoryName); + + string sourceSubDirectory = Path.Join(sourceDirectoryName, subDirectory); + if(subDirectory != null) Directory.CreateDirectory(sourceSubDirectory); + + File.CreateSymbolicLink(Path.Join(sourceSubDirectory, "linkToFile"), symlinkTargetPath); + + TarFile.CreateFromDirectory(sourceDirectoryName, destinationArchive, includeBaseDirectory: false); + + using FileStream archiveStream = File.OpenRead(destinationArchive); + Exception exception = Assert.Throws(() => TarFile.ExtractToDirectory(archiveStream, destinationDirectoryName, overwriteFiles: true)); + + Assert.Equal(SR.Format(SR.TarExtractingResultsLinkOutside, symlinkTargetPath, destinationDirectoryName), exception.Message); + } + } +} diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.Stream.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.Stream.Tests.cs index ec32ab1ab8136..cde32d3f97916 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.Stream.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectoryAsync.Stream.Tests.cs @@ -110,6 +110,29 @@ public async Task ExtractEntry_DockerImageTarWithFileTypeInDirectoriesInMode_Suc } } + [ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + public async Task ExtractEntry_PodmanImageTarWithRelativeSymlinksPointingInExtractDirectory_SuccessfullyExtracts_Async() + { + using (TempDirectory root = new TempDirectory()) + { + await using MemoryStream archiveStream = GetTarMemoryStream(CompressionMethod.Uncompressed, "misc", "podman-hello-world"); + await TarFile.ExtractToDirectoryAsync(archiveStream, root.Path, overwriteFiles: true); + + Assert.True(File.Exists(Path.Join(root.Path, "manifest.json"))); + Assert.True(File.Exists(Path.Join(root.Path, "repositories"))); + Assert.True(File.Exists(Path.Join(root.Path, "efb53921da3394806160641b72a2cbd34ca1a9a8345ac670a85a04ad3d0e3507.tar"))); + + string symlinkPath = Path.Join(root.Path, "e7fc2b397c1ab5af9938f18cc9a80d526cccd1910e4678390157d8cc6c94410d/layer.tar"); + Assert.True(File.Exists(symlinkPath)); + + FileInfo? fileInfo = new(symlinkPath); + Assert.Equal("../efb53921da3394806160641b72a2cbd34ca1a9a8345ac670a85a04ad3d0e3507.tar", fileInfo.LinkTarget); + + FileSystemInfo? symlinkTarget = File.ResolveLinkTarget(symlinkPath, returnFinalTarget: true); + Assert.True(File.Exists(symlinkTarget.FullName)); + } + } + [Theory] [InlineData(TarEntryType.SymbolicLink)] [InlineData(TarEntryType.HardLink)]