Compare commits

...

3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
e55af11800 Add tests for compressed TAR detection and fix stream disposal
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-11 14:07:01 +00:00
copilot-swe-agent[bot]
c8ca687dc2 Add XZ/Lzw support to TarReader and detection for compressed TAR in TarArchive
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-11 14:01:18 +00:00
copilot-swe-agent[bot]
ea8fcb9370 Initial plan 2025-11-11 13:38:50 +00:00
3 changed files with 215 additions and 2 deletions

View File

@@ -4,9 +4,17 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives.GZip;
using SharpCompress.Common;
using SharpCompress.Common.Tar;
using SharpCompress.Common.Tar.Headers;
using SharpCompress.Compressors;
using SharpCompress.Compressors.BZip2;
using SharpCompress.Compressors.Deflate;
using SharpCompress.Compressors.LZMA;
using SharpCompress.Compressors.Lzw;
using SharpCompress.Compressors.Xz;
using SharpCompress.Compressors.ZStandard;
using SharpCompress.IO;
using SharpCompress.Readers;
using SharpCompress.Readers.Tar;
@@ -36,6 +44,76 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
public static TarArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull(nameof(fileInfo));
// Check if the file extension suggests it's a compressed TAR file
var extension = fileInfo.Extension.ToLowerInvariant();
if (
extension is ".gz" or ".bz2" or ".xz" or ".zst" or ".lz" or ".z"
|| fileInfo.Name.ToLowerInvariant().Contains(".tar.")
)
{
// Open the file to check for compression
using var testStream = fileInfo.OpenRead();
using var rewindableStream = SharpCompressStream.Create(testStream, leaveOpen: false);
var streamStack = (IStreamStack)rewindableStream;
var startPos = streamStack.GetPosition();
string? detectedCompression = null;
if (GZipArchive.IsGZipFile(rewindableStream))
{
detectedCompression = "GZip";
}
else
{
streamStack.StackSeek(startPos);
if (BZip2Stream.IsBZip2(rewindableStream))
{
detectedCompression = "BZip2";
}
else
{
streamStack.StackSeek(startPos);
if (ZStandardStream.IsZStandard(rewindableStream))
{
detectedCompression = "ZStandard";
}
else
{
streamStack.StackSeek(startPos);
if (LZipStream.IsLZipFile(rewindableStream))
{
detectedCompression = "LZip";
}
else
{
streamStack.StackSeek(startPos);
if (XZStream.IsXZStream(rewindableStream))
{
detectedCompression = "XZ";
}
else
{
streamStack.StackSeek(startPos);
if (LzwStream.IsLzwStream(rewindableStream))
{
detectedCompression = "LZW";
}
}
}
}
}
}
if (detectedCompression is not null)
{
throw new InvalidFormatException(
$"Compressed TAR archives ({detectedCompression}) are not supported by TarArchive.Open(). "
+ "Please use TarReader.Open() instead for forward-only reading of compressed TAR files."
);
}
}
return new TarArchive(
new SourceStream(
fileInfo,
@@ -98,9 +176,75 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
throw new ArgumentException("Stream must be seekable", nameof(stream));
}
return new TarArchive(
new SourceStream(stream, i => null, readerOptions ?? new ReaderOptions())
readerOptions ??= new ReaderOptions();
// Wrap in SharpCompressStream to enable rewindable reading for compression detection
var rewindableStream = SharpCompressStream.Create(
stream,
leaveOpen: readerOptions.LeaveStreamOpen
);
var streamStack = (IStreamStack)rewindableStream;
var startPos = streamStack.GetPosition();
// Detect if the TAR file is compressed
string? detectedCompression = null;
if (GZipArchive.IsGZipFile(rewindableStream))
{
detectedCompression = "GZip";
}
else
{
streamStack.StackSeek(startPos);
if (BZip2Stream.IsBZip2(rewindableStream))
{
detectedCompression = "BZip2";
}
else
{
streamStack.StackSeek(startPos);
if (ZStandardStream.IsZStandard(rewindableStream))
{
detectedCompression = "ZStandard";
}
else
{
streamStack.StackSeek(startPos);
if (LZipStream.IsLZipFile(rewindableStream))
{
detectedCompression = "LZip";
}
else
{
streamStack.StackSeek(startPos);
if (XZStream.IsXZStream(rewindableStream))
{
detectedCompression = "XZ";
}
else
{
streamStack.StackSeek(startPos);
if (LzwStream.IsLzwStream(rewindableStream))
{
detectedCompression = "LZW";
}
}
}
}
}
}
if (detectedCompression is not null)
{
throw new InvalidFormatException(
$"Compressed TAR archives ({detectedCompression}) are not supported by TarArchive.Open(Stream). "
+ "Please use TarReader.Open(Stream) instead for forward-only reading of compressed TAR files."
);
}
// No compression detected, treat as plain tar file
streamStack.StackSeek(startPos);
return new TarArchive(new SourceStream(rewindableStream, i => null, readerOptions));
}
public static bool IsTarFile(string filePath) => IsTarFile(new FileInfo(filePath));

View File

@@ -111,6 +111,32 @@ public class TarReader : AbstractReader<TarEntry, TarVolume>
throw new InvalidFormatException("Not a tar file.");
}
((IStreamStack)rewindableStream).StackSeek(pos);
if (XZStream.IsXZStream(rewindableStream))
{
((IStreamStack)rewindableStream).StackSeek(pos);
var testStream = new XZStream(rewindableStream);
if (TarArchive.IsTarFile(testStream))
{
((IStreamStack)rewindableStream).StackSeek(pos);
return new TarReader(rewindableStream, options, CompressionType.Xz);
}
throw new InvalidFormatException("Not a tar file.");
}
((IStreamStack)rewindableStream).StackSeek(pos);
if (LzwStream.IsLzwStream(rewindableStream))
{
((IStreamStack)rewindableStream).StackSeek(pos);
var testStream = new LzwStream(rewindableStream);
if (TarArchive.IsTarFile(testStream))
{
((IStreamStack)rewindableStream).StackSeek(pos);
return new TarReader(rewindableStream, options, CompressionType.Lzw);
}
throw new InvalidFormatException("Not a tar file.");
}
((IStreamStack)rewindableStream).StackSeek(pos);
return new TarReader(rewindableStream, options, CompressionType.None);
}

View File

@@ -295,4 +295,47 @@ public class TarArchiveTests : ArchiveTests
Assert.False(isTar);
}
[Fact]
public void TarArchive_Open_Compressed_XZ_Throws()
{
var exception = Assert.Throws<InvalidFormatException>(() =>
TarArchive.Open(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.xz"))
);
Assert.Contains("XZ", exception.Message);
Assert.Contains("TarReader.Open", exception.Message);
}
[Fact]
public void TarArchive_Open_Compressed_GZip_Throws()
{
var exception = Assert.Throws<InvalidFormatException>(() =>
TarArchive.Open(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"))
);
Assert.Contains("GZip", exception.Message);
Assert.Contains("TarReader.Open", exception.Message);
}
[Fact]
public void TarArchive_Open_Compressed_BZip2_Throws()
{
var exception = Assert.Throws<InvalidFormatException>(() =>
TarArchive.Open(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.bz2"))
);
Assert.Contains("BZip2", exception.Message);
Assert.Contains("TarReader.Open", exception.Message);
}
[Fact]
public void TarArchive_Open_Compressed_Stream_XZ_Throws()
{
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.xz"));
var exception = Assert.Throws<InvalidFormatException>(() => TarArchive.Open(stream));
Assert.Contains("XZ", exception.Message);
Assert.Contains("TarReader.Open", exception.Message);
}
}