Compare commits

...

4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
7b746c49cf Fix code review issues: LzwVolume multi-volume flag and extension case
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-02-07 10:30:56 +00:00
copilot-swe-agent[bot]
1da178a4be Run code formatter on LzwReader implementation
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-02-07 10:30:00 +00:00
copilot-swe-agent[bot]
2b74807f5e Implement LzwReader support for .Z archives
- Add Lzw to ArchiveType enum
- Create Common/Lzw classes (LzwEntry, LzwVolume, LzwFilePart)
- Create Readers/Lzw/LzwReader with factory methods
- Create LzwFactory for integration with ReaderFactory
- Add comprehensive tests in Lzw test directory
- Update ReaderFactory error message to include Lzw format

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-02-07 10:29:18 +00:00
copilot-swe-agent[bot]
38d295b089 Initial plan 2026-02-07 10:23:41 +00:00
14 changed files with 371 additions and 1 deletions

View File

@@ -10,4 +10,5 @@ public enum ArchiveType
Arc,
Arj,
Ace,
Lzw,
}

View File

@@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
namespace SharpCompress.Common.Lzw;
public partial class LzwEntry
{
internal static async IAsyncEnumerable<LzwEntry> GetEntriesAsync(
Stream stream,
OptionsBase options,
[EnumeratorCancellation] CancellationToken cancellationToken = default
)
{
yield return new LzwEntry(
await LzwFilePart.CreateAsync(stream, options.ArchiveEncoding, cancellationToken)
);
}
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace SharpCompress.Common.Lzw;
public partial class LzwEntry : Entry
{
private readonly LzwFilePart? _filePart;
internal LzwEntry(LzwFilePart? filePart) => _filePart = filePart;
public override CompressionType CompressionType => CompressionType.Lzw;
public override long Crc => 0;
public override string? Key => _filePart?.FilePartName;
public override string? LinkTarget => null;
public override long CompressedSize => 0;
public override long Size => 0;
public override DateTime? LastModifiedTime => null;
public override DateTime? CreatedTime => null;
public override DateTime? LastAccessedTime => null;
public override DateTime? ArchivedTime => null;
public override bool IsEncrypted => false;
public override bool IsDirectory => false;
public override bool IsSplitAfter => false;
internal override IEnumerable<FilePart> Parts => _filePart.Empty();
internal static IEnumerable<LzwEntry> GetEntries(Stream stream, OptionsBase options)
{
yield return new LzwEntry(LzwFilePart.Create(stream, options.ArchiveEncoding));
}
// Async methods moved to LzwEntry.Async.cs
}

View File

@@ -0,0 +1,30 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.Common.Lzw;
internal sealed partial class LzwFilePart
{
internal static async ValueTask<LzwFilePart> CreateAsync(
Stream stream,
IArchiveEncoding archiveEncoding,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
var part = new LzwFilePart(stream, archiveEncoding);
if (stream.CanSeek)
{
part.EntryStartPosition = stream.Position;
}
else
{
// For non-seekable streams, we can't track position.
// Set to 0 since the stream will be read sequentially from its current position.
part.EntryStartPosition = 0;
}
return part;
}
}

View File

@@ -0,0 +1,38 @@
using System.IO;
using SharpCompress.Compressors.Lzw;
namespace SharpCompress.Common.Lzw;
internal sealed partial class LzwFilePart : FilePart
{
private readonly Stream _stream;
internal static LzwFilePart Create(Stream stream, IArchiveEncoding archiveEncoding)
{
var part = new LzwFilePart(stream, archiveEncoding);
if (stream.CanSeek)
{
part.EntryStartPosition = stream.Position;
}
else
{
// For non-seekable streams, we can't track position.
// Set to 0 since the stream will be read sequentially from its current position.
part.EntryStartPosition = 0;
}
return part;
}
private LzwFilePart(Stream stream, IArchiveEncoding archiveEncoding)
: base(archiveEncoding) => _stream = stream;
internal long EntryStartPosition { get; private set; }
internal override string? FilePartName => null;
internal override Stream GetCompressedStream() =>
new LzwStream(_stream) { IsStreamOwner = false };
internal override Stream GetRawStream() => _stream;
}

View File

@@ -0,0 +1,17 @@
using System.IO;
using SharpCompress.Readers;
namespace SharpCompress.Common.Lzw;
public class LzwVolume : Volume
{
public LzwVolume(Stream stream, ReaderOptions? options, int index)
: base(stream, options, index) { }
public LzwVolume(FileInfo fileInfo, ReaderOptions options)
: base(fileInfo.OpenRead(), options) => options.LeaveStreamOpen = false;
public override bool IsFirstVolume => true;
public override bool IsMultiVolume => false;
}

View File

@@ -18,6 +18,7 @@ public abstract class Factory : IFactory
RegisterFactory(new RarFactory());
RegisterFactory(new TarFactory()); //put tar before most
RegisterFactory(new GZipFactory());
RegisterFactory(new LzwFactory());
RegisterFactory(new ArcFactory());
RegisterFactory(new ArjFactory());
RegisterFactory(new AceFactory());

View File

@@ -0,0 +1,67 @@
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Compressors.Lzw;
using SharpCompress.IO;
using SharpCompress.Readers;
using SharpCompress.Readers.Lzw;
namespace SharpCompress.Factories;
/// <summary>
/// Represents the foundation factory of LZW archive.
/// </summary>
public class LzwFactory : Factory, IReaderFactory
{
#region IFactory
/// <inheritdoc/>
public override string Name => "Lzw";
/// <inheritdoc/>
public override ArchiveType? KnownArchiveType => ArchiveType.Lzw;
/// <inheritdoc/>
public override IEnumerable<string> GetSupportedExtensions()
{
yield return "z";
}
/// <inheritdoc/>
public override bool IsArchive(Stream stream, string? password = null) =>
LzwStream.IsLzwStream(stream);
/// <inheritdoc/>
public override ValueTask<bool> IsArchiveAsync(
Stream stream,
string? password = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new(IsArchive(stream, password));
}
#endregion
#region IReaderFactory
/// <inheritdoc/>
public IReader OpenReader(Stream stream, ReaderOptions? options) =>
LzwReader.OpenReader(stream, options);
/// <inheritdoc/>
public ValueTask<IAsyncReader> OpenAsyncReader(
Stream stream,
ReaderOptions? options,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new((IAsyncReader)LzwReader.OpenReader(stream, options));
}
#endregion
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.IO;
using SharpCompress.Common;
using SharpCompress.Common.Lzw;
namespace SharpCompress.Readers.Lzw;
public partial class LzwReader
{
/// <summary>
/// Returns entries asynchronously for streams that only support async reads.
/// </summary>
protected override IAsyncEnumerable<LzwEntry> GetEntriesAsync(Stream stream) =>
LzwEntry.GetEntriesAsync(stream, Options);
}

View File

@@ -0,0 +1,59 @@
using System.IO;
using System.Threading;
namespace SharpCompress.Readers.Lzw;
public partial class LzwReader
#if NET8_0_OR_GREATER
: IReaderOpenable
#endif
{
public static IAsyncReader OpenAsyncReader(
string path,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
path.NotNullOrEmpty(nameof(path));
return (IAsyncReader)OpenReader(new FileInfo(path), readerOptions);
}
public static IAsyncReader OpenAsyncReader(
Stream stream,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IAsyncReader)OpenReader(stream, readerOptions);
}
public static IAsyncReader OpenAsyncReader(
FileInfo fileInfo,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IAsyncReader)OpenReader(fileInfo, readerOptions);
}
public static IReader OpenReader(string filePath, ReaderOptions? readerOptions = null)
{
filePath.NotNullOrEmpty(nameof(filePath));
return OpenReader(new FileInfo(filePath), readerOptions);
}
public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull(nameof(fileInfo));
return OpenReader(fileInfo.OpenRead(), readerOptions);
}
public static IReader OpenReader(Stream stream, ReaderOptions? options = null)
{
stream.NotNull(nameof(stream));
return new LzwReader(stream, options ?? new ReaderOptions());
}
}

View File

@@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.IO;
using SharpCompress.Common;
using SharpCompress.Common.Lzw;
namespace SharpCompress.Readers.Lzw;
public partial class LzwReader : AbstractReader<LzwEntry, LzwVolume>
{
private LzwReader(Stream stream, ReaderOptions options)
: base(options, ArchiveType.Lzw) => Volume = new LzwVolume(stream, options, 0);
public override LzwVolume Volume { get; }
protected override IEnumerable<LzwEntry> GetEntries(Stream stream) =>
LzwEntry.GetEntries(stream, Options);
// GetEntriesAsync moved to LzwReader.Async.cs
}

View File

@@ -77,7 +77,7 @@ public static partial class ReaderFactory
}
throw new InvalidFormatException(
"Cannot determine compressed stream type. Supported Reader Formats: Ace, Arc, Arj, Zip, GZip, BZip2, Tar, Rar, LZip, XZ, ZStandard"
"Cannot determine compressed stream type. Supported Reader Formats: Ace, Arc, Arj, Zip, GZip, BZip2, Tar, Rar, LZip, Lzw, XZ, ZStandard"
);
}
}

View File

@@ -0,0 +1,15 @@
using SharpCompress.Common;
using Xunit;
namespace SharpCompress.Test.Lzw;
public class LzwReaderAsyncTests : ReaderTests
{
public LzwReaderAsyncTests() => UseExtensionInsteadOfNameToVerify = true;
[Fact]
public async System.Threading.Tasks.Task Lzw_Reader_Async()
{
await ReadAsync("Tar.tar.Z", CompressionType.Lzw);
}
}

View File

@@ -0,0 +1,41 @@
using System.IO;
using SharpCompress.Common;
using SharpCompress.IO;
using SharpCompress.Readers;
using SharpCompress.Readers.Lzw;
using Xunit;
namespace SharpCompress.Test.Lzw;
public class LzwReaderTests : ReaderTests
{
public LzwReaderTests() => UseExtensionInsteadOfNameToVerify = true;
[Fact]
public void Lzw_Reader_Generic() => Read("Tar.tar.Z", CompressionType.Lzw);
[Fact]
public void Lzw_Reader_Generic2()
{
//read only as Lzw item
using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.Z"));
using var reader = LzwReader.OpenReader(SharpCompressStream.CreateNonDisposing(stream));
while (reader.MoveToNextEntry())
{
// LZW doesn't have CRC or Size in header like GZip, so we just check the entry exists
Assert.NotNull(reader.Entry);
}
}
[Fact]
public void Lzw_Reader_Factory_Detects_Format()
{
using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.Z"));
using var reader = ReaderFactory.OpenReader(
stream,
new ReaderOptions { LeaveStreamOpen = false }
);
Assert.True(reader.MoveToNextEntry());
Assert.NotNull(reader.Entry);
}
}