diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs index 2afcfb8a9f23df..1de798f25566f2 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs @@ -374,12 +374,9 @@ private int WritePosixName(Span buffer) if (_name.Length > FieldLengths.Name) { - int prefixBytesLength = Math.Min(_name.Length - FieldLengths.Name, FieldLengths.Name); - Span remaining = prefixBytesLength <= 256 ? - stackalloc byte[prefixBytesLength] : - new byte[prefixBytesLength]; - - int encoded = Encoding.ASCII.GetBytes(_name.AsSpan(FieldLengths.Name), remaining); + int prefixBytesLength = Math.Min(_name.Length - FieldLengths.Name, FieldLengths.Prefix); + Span remaining = stackalloc byte[prefixBytesLength]; + int encoded = Encoding.ASCII.GetBytes(_name.AsSpan(FieldLengths.Name, prefixBytesLength), remaining); Debug.Assert(encoded == remaining.Length); checksum += WriteLeftAlignedBytesAndGetChecksum(remaining, buffer.Slice(FieldLocations.Prefix, FieldLengths.Prefix)); diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectory.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectory.File.Tests.cs index 308a9b68ba4def..807af05d3969a7 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectory.File.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectory.File.Tests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using Microsoft.DotNet.XUnitExtensions; using Xunit; namespace System.Formats.Tar.Tests @@ -253,5 +254,55 @@ public void SkipRecursionIntoBaseDirectorySymlink() Assert.Null(reader.GetNextEntry()); } + + [ConditionalTheory(nameof(CanExtractWithTarTool))] + [MemberData(nameof(GetPaxAndGnuTestCaseNames))] + public void ResultingArchive_CanBeExtractedByOtherTools(string testCaseName) + { + if (testCaseName == "specialfiles" && !PlatformDetection.IsSuperUser) + { + throw new SkipTestException("specialfiles is only supported on Unix and it needs sudo permissions for extraction."); + } + + string[] symlinkTestCases = { "file_symlink", "foldersymlink_folder_subfolder_file", "file_longsymlink" }; + if (symlinkTestCases.Contains(testCaseName) && PlatformDetection.IsWindowsNanoServer) // Tar version of Windows Nano Server does not support extracting symlinks. + { + throw new SkipTestException("Symlinks are not supported on this platform."); + } + + using TempDirectory root = new TempDirectory(); + + string archivePath = GetTarFilePath(CompressionMethod.Uncompressed, TestTarFormat.pax.ToString(), testCaseName); + string tarExtractedPath = Path.Join(root.Path, "tarExtracted"); + Directory.CreateDirectory(tarExtractedPath); + + // extract with /usr/bin/tar + ExtractWithTarTool(archivePath, tarExtractedPath); + + // create archive with TarFile + string dotnetTarArchive = Path.Join(root.Path, "dotnetTarArchive.tar"); + TarFile.CreateFromDirectory(tarExtractedPath, dotnetTarArchive, includeBaseDirectory: false); + + // extract TarFile's archive with /usr/bin/tar + string dotnetTarExtractedPath = Path.Join(root.Path, "dotnetTarExtracted"); + Directory.CreateDirectory(dotnetTarExtractedPath); + ExtractWithTarTool(dotnetTarArchive, dotnetTarExtractedPath); + + // compare results + Dictionary expectedEntries = GetFileSystemInfosRecursive(tarExtractedPath).ToDictionary(fsi => fsi.FullName.Substring(tarExtractedPath.Length)); + + foreach (FileSystemInfo actualEntry in GetFileSystemInfosRecursive(dotnetTarExtractedPath)) + { + string keyPath = actualEntry.FullName.Substring(dotnetTarExtractedPath.Length); + Assert.True(expectedEntries.TryGetValue(keyPath, out FileSystemInfo expectedEntry), $"Entry '{keyPath}' not expected."); + Assert.Equal(expectedEntry.Attributes, actualEntry.Attributes); + Assert.Equal(expectedEntry.LinkTarget, actualEntry.LinkTarget); + + expectedEntries.Remove(keyPath); + } + + // All expected entries should've been found and removed. + Assert.Equal(0, expectedEntries.Count); + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs b/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs index 23dc653c83a165..fe29235d40372c 100644 --- a/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs +++ b/src/libraries/System.Formats.Tar/tests/TarTestsBase.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Diagnostics; using System.IO; +using System.IO.Enumeration; using System.Linq; using System.Runtime.CompilerServices; using Microsoft.DotNet.RemoteExecutor; @@ -611,5 +613,58 @@ public static IEnumerable GetPaxAndGnuTestCaseNames() yield return new object[] { name }; } } + + protected static FileSystemEnumerable GetFileSystemInfosRecursive(string path) + { + var enumerationOptions = new EnumerationOptions { RecurseSubdirectories = true }; + return new FileSystemEnumerable(path, (ref FileSystemEntry e) => e.ToFileSystemInfo(), enumerationOptions); + } + + public static bool CanExtractWithTarTool => s_canExtractWithTarTool.Value; + + private static readonly Lazy s_canExtractWithTarTool = new Lazy(() => + { + if (PlatformDetection.IsAndroid || PlatformDetection.IsLinuxBionic) // Android reports tar: chown 7913:3579 'file.txt': Operation not permitted. + { + return false; + } + + if (PlatformDetection.IsWindows) + { + Version osVersion = Environment.OSVersion.Version; + bool isAtLeastWin10Build17063 = osVersion.Major >= 11 || osVersion.Major == 10 && osVersion.Build >= 17063; + if (!isAtLeastWin10Build17063) + { + return false; + } + } + + using Process tarToolProcess = new Process(); + tarToolProcess.StartInfo.FileName = "tar"; // location varies: find it on the PATH. + tarToolProcess.StartInfo.Arguments = "--help"; + + tarToolProcess.StartInfo.RedirectStandardOutput = true; + tarToolProcess.Start(); + + string output = tarToolProcess.StandardOutput.ReadToEnd(); + tarToolProcess.WaitForExit(); + + return tarToolProcess.ExitCode == 0; + }); + + public static void ExtractWithTarTool(string source, string destination) + { + using Process tarToolProcess = new Process(); + tarToolProcess.StartInfo.FileName = "tar"; // location varies: find it on the PATH. + tarToolProcess.StartInfo.Arguments = $"-xf {source} -C {destination}"; + + tarToolProcess.StartInfo.RedirectStandardOutput = true; + tarToolProcess.Start(); + + string output = tarToolProcess.StandardOutput.ReadToEnd(); + tarToolProcess.WaitForExit(); + + Assert.True(tarToolProcess.ExitCode == 0, "Tar process output: " + output); + } } }