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 1a729094a68328..2e55db15c4f8f2 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 @@ -565,9 +565,11 @@ private void ReadUstarAttributes(ReadOnlySpan buffer) // Name, if the full path did not fit in the Name byte array. if (!string.IsNullOrEmpty(_prefix)) { - // Prefix never has a leading separator, so we add it. - // It should always be a forward slash for compatibility - _name = $"{_prefix}/{_name}"; + // Prefix should not have a trailing separator, but to avoid producing a + // synthesized double slash for archives that do include one, only add + // the separator when it is not already present. + // It should always be a forward slash for compatibility. + _name = _prefix.EndsWith('/') ? $"{_prefix}{_name}" : $"{_prefix}/{_name}"; } } diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.GetNextEntry.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.GetNextEntry.Tests.cs index a534e0267bbac0..08ae17200abe23 100644 --- a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.GetNextEntry.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.GetNextEntry.Tests.cs @@ -627,6 +627,44 @@ public void Read_PaxEntryWithOnlyLinkpath_PreservesUstarPrefix() Assert.Null(reader2.GetNextEntry()); } + [Fact] + public void Read_UstarEntry_PrefixWithTrailingSlash_DoesNotDuplicateSeparator() + { + // Some archives may write the ustar prefix field with a trailing slash already + // included. TarReader must not synthesize a double slash when combining prefix and name. + string prefix = "./sdk/"; + string nameField = "file.txt"; + string expectedName = "./sdk/file.txt"; + + using MemoryStream archiveStream = new MemoryStream(); + + byte[] entryHeader = new byte[512]; + Encoding.UTF8.GetBytes(nameField).CopyTo(entryHeader.AsSpan(0)); + Encoding.UTF8.GetBytes("0000644\0").CopyTo(entryHeader.AsSpan(100, 8)); + Encoding.UTF8.GetBytes("0000000\0").CopyTo(entryHeader.AsSpan(108, 8)); + Encoding.UTF8.GetBytes("0000000\0").CopyTo(entryHeader.AsSpan(116, 8)); + Encoding.UTF8.GetBytes("00000000000\0").CopyTo(entryHeader.AsSpan(124, 12)); + Encoding.UTF8.GetBytes("00000000000\0").CopyTo(entryHeader.AsSpan(136, 12)); + entryHeader[156] = (byte)'0'; // RegularFile + Encoding.UTF8.GetBytes("ustar\0").CopyTo(entryHeader.AsSpan(257, 6)); + Encoding.UTF8.GetBytes("00").CopyTo(entryHeader.AsSpan(263, 2)); + Encoding.UTF8.GetBytes(prefix).CopyTo(entryHeader.AsSpan(345)); + + WriteHeaderChecksum(entryHeader); + archiveStream.Write(entryHeader); + + // End-of-archive markers. + archiveStream.Write(new byte[1024]); + archiveStream.Seek(0, SeekOrigin.Begin); + + using TarReader reader = new TarReader(archiveStream); + TarEntry entry = reader.GetNextEntry(); + Assert.NotNull(entry); + Assert.Equal(expectedName, entry.Name); + Assert.DoesNotContain("//", entry.Name); + Assert.Null(reader.GetNextEntry()); + } + [Theory] [InlineData("PaxExtendedAttributes", MaxMetadataBlockSize - 100)] [InlineData("GnuLongPath", MaxMetadataBlockSize)]