diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs index 0d5ec998497f9..fd07150b3508a 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs @@ -723,13 +723,7 @@ private static bool TryGetNextExtendedAttribute( { return false; } - line = line.Slice(spacePos + 1).TrimStart((byte)' '); - - // If there are any more spaces, it's malformed. - if (line.IndexOf((byte)' ') >= 0) - { - return false; - } + line = line.Slice(spacePos + 1); // Find the equal separator. int equalPos = line.IndexOf((byte)'='); diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.Stream.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.Stream.Tests.cs index b62196bb5c5d6..ab90f0043bc7d 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.Stream.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.Stream.Tests.cs @@ -177,5 +177,32 @@ public void Extract_UnseekableStream_BlockAlignmentPadding_DoesNotAffectNextEntr Assert.Equal(2, Directory.GetFileSystemEntries(destination.Path, "*", SearchOption.AllDirectories).Count()); } + + [Fact] + public void PaxNameCollision_DedupInExtendedAttributes() + { + using TempDirectory root = new(); + + string sharedRootFolders = Path.Join(root.Path, "folder with spaces", new string('a', 100)); + string path1 = Path.Join(sharedRootFolders, "entry 1 with spaces.txt"); + string path2 = Path.Join(sharedRootFolders, "entry 2 with spaces.txt"); + + using MemoryStream stream = new(); + using (TarWriter writer = new(stream, TarEntryFormat.Pax, leaveOpen: true)) + { + // Paths don't fit in the standard 'name' field, but they differ in the filename, + // which is fully stored as an extended attribute + PaxTarEntry entry1 = new(TarEntryType.RegularFile, path1); + writer.WriteEntry(entry1); + PaxTarEntry entry2 = new(TarEntryType.RegularFile, path2); + writer.WriteEntry(entry2); + } + stream.Position = 0; + + TarFile.ExtractToDirectory(stream, root.Path, overwriteFiles: true); + + Assert.True(File.Exists(path1)); + Assert.True(Path.Exists(path2)); + } } } 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 cde32d3f97916..d7502d940e94e 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 @@ -242,5 +242,32 @@ public async Task Extract_UnseekableStream_BlockAlignmentPadding_DoesNotAffectNe Assert.Equal(2, Directory.GetFileSystemEntries(destination.Path, "*", SearchOption.AllDirectories).Count()); } + + [Fact] + public async Task PaxNameCollision_DedupInExtendedAttributesAsync() + { + using TempDirectory root = new(); + + string sharedRootFolders = Path.Join(root.Path, "folder with spaces", new string('a', 100)); + string path1 = Path.Join(sharedRootFolders, "entry 1 with spaces.txt"); + string path2 = Path.Join(sharedRootFolders, "entry 2 with spaces.txt"); + + await using MemoryStream stream = new(); + await using (TarWriter writer = new(stream, TarEntryFormat.Pax, leaveOpen: true)) + { + // Paths don't fit in the standard 'name' field, but they differ in the filename, + // which is fully stored as an extended attribute + PaxTarEntry entry1 = new(TarEntryType.RegularFile, path1); + await writer.WriteEntryAsync(entry1); + PaxTarEntry entry2 = new(TarEntryType.RegularFile, path2); + await writer.WriteEntryAsync(entry2); + } + stream.Position = 0; + + await TarFile.ExtractToDirectoryAsync(stream, root.Path, overwriteFiles: true); + + Assert.True(File.Exists(path1)); + Assert.True(Path.Exists(path2)); + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.GlobalExtendedAttributes.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.GlobalExtendedAttributes.Tests.cs index 2661c542ae4c0..755f3377703fc 100644 --- a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.GlobalExtendedAttributes.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.GlobalExtendedAttributes.Tests.cs @@ -82,5 +82,29 @@ public void ExtractGlobalExtendedAttributesEntry_Throws() Assert.Throws(() => entry.ExtractToFile(Path.Join(root.Path, "file"), overwrite: true)); } } + + [Theory] + [InlineData("key", "value")] + [InlineData("key ", "value ")] + [InlineData(" key", " value")] + [InlineData(" key ", " value ")] + [InlineData(" key spaced ", " value spaced ")] + [InlineData("many sla/s\\hes", "/////////////\\\\\\///////////")] + public void GlobalExtendedAttribute_Roundtrips(string key, string value) + { + var stream = new MemoryStream(); + using (var writer = new TarWriter(stream, leaveOpen: true)) + { + writer.WriteEntry(new PaxGlobalExtendedAttributesTarEntry(new Dictionary() { { key, value } })); + } + + stream.Position = 0; + using (var reader = new TarReader(stream)) + { + PaxGlobalExtendedAttributesTarEntry entry = Assert.IsType(reader.GetNextEntry()); + Assert.Equal(1, entry.GlobalExtendedAttributes.Count); + Assert.Equal(KeyValuePair.Create(key, value), entry.GlobalExtendedAttributes.First()); + } + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Tests.cs index 17c67423c390b..0e1b325ed1197 100644 --- a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.File.Tests.cs @@ -325,6 +325,30 @@ public void PaxSizeLargerThanMaxAllowedByStream() Assert.Throws(() => reader.GetNextEntry()); } + [Theory] + [InlineData("key", "value")] + [InlineData("key ", "value ")] + [InlineData(" key", " value")] + [InlineData(" key ", " value ")] + [InlineData(" key spaced ", " value spaced ")] + [InlineData("many sla/s\\hes", "/////////////\\\\\\///////////")] + public void PaxExtendedAttribute_Roundtrips(string key, string value) + { + var stream = new MemoryStream(); + using (var writer = new TarWriter(stream, leaveOpen: true)) + { + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, "entryName", new Dictionary() { { key, value } })); + } + + stream.Position = 0; + using (var reader = new TarReader(stream)) + { + PaxTarEntry entry = Assert.IsType(reader.GetNextEntry()); + Assert.Equal(5, entry.ExtendedAttributes.Count); + Assert.Contains(KeyValuePair.Create(key, value), entry.ExtendedAttributes); + } + } + private static void VerifyDataStreamOfTarUncompressedInternal(string testFolderName, string testCaseName, bool copyData) { using MemoryStream archiveStream = GetTarMemoryStream(CompressionMethod.Uncompressed, testFolderName, testCaseName);