diff --git a/src/SharpCompress/Common/CompressionType.cs b/src/SharpCompress/Common/CompressionType.cs
index b43d4b49..2f44ed11 100644
--- a/src/SharpCompress/Common/CompressionType.cs
+++ b/src/SharpCompress/Common/CompressionType.cs
@@ -11,6 +11,7 @@
LZMA,
BCJ,
BCJ2,
+ LZip,
Unknown
}
}
\ No newline at end of file
diff --git a/src/SharpCompress/Compressors/LZMA/LZipStream.cs b/src/SharpCompress/Compressors/LZMA/LZipStream.cs
new file mode 100644
index 00000000..19b89764
--- /dev/null
+++ b/src/SharpCompress/Compressors/LZMA/LZipStream.cs
@@ -0,0 +1,153 @@
+using System;
+using System.IO;
+
+namespace SharpCompress.Compressors.LZMA
+{
+ // TODO:
+ // - Write as well as read
+ // - Multi-volume support
+ // - Use of the data size / member size values at the end of the stream
+
+ ///
+ /// Stream supporting the LZIP format, as documented at http://www.nongnu.org/lzip/manual/lzip_manual.html
+ ///
+ public class LZipStream : Stream
+ {
+ private readonly Stream stream;
+ private bool disposed;
+ private readonly bool leaveOpen;
+
+ public LZipStream(Stream stream, CompressionMode mode)
+ : this(stream, mode, false)
+ {
+ }
+
+ public LZipStream(Stream stream, CompressionMode mode, bool leaveOpen)
+ {
+ if (mode != CompressionMode.Decompress)
+ {
+ throw new NotImplementedException("Only LZip decompression is currently supported");
+ }
+ Mode = mode;
+ this.leaveOpen = leaveOpen;
+ int dictionarySize = ValidateAndReadSize(stream);
+ if (dictionarySize == 0)
+ {
+ throw new IOException("Not an LZip stream");
+ }
+ byte[] properties = GetProperties(dictionarySize);
+ this.stream = new LzmaStream(properties, stream);
+ }
+
+ #region Stream methods
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposed)
+ {
+ return;
+ }
+ disposed = true;
+ if (disposing && !leaveOpen)
+ {
+ stream.Dispose();
+ }
+ }
+
+ public CompressionMode Mode { get; }
+
+ public override bool CanRead => stream.CanRead;
+
+ public override bool CanSeek => false;
+
+ public override bool CanWrite => false;
+
+ public override void Flush()
+ {
+ stream.Flush();
+ }
+
+ // TODO: Both Length and Position are sometimes feasible, but would require
+ // reading the output length when we initialize.
+ public override long Length { get { throw new NotImplementedException(); } }
+
+ public override long Position { get { throw new NotImplementedException(); } set { throw new NotImplementedException(); } }
+
+ public override int Read(byte[] buffer, int offset, int count) => stream.Read(buffer, offset, count);
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ throw new NotImplementedException();
+ }
+ #endregion
+
+ ///
+ /// Determines if the given stream is positioned at the start of a v1 LZip
+ /// file, as indicated by the ASCII characters "LZIP" and a version byte
+ /// of 1, followed by at least one byte.
+ ///
+ /// The stream to read from. Must not be null.
+ /// true if the given stream is an LZip file, false otherwise.
+ public static bool IsLZipFile(Stream stream) => ValidateAndReadSize(stream) != 0;
+
+ ///
+ /// Reads the 6-byte header of the stream, and returns 0 if either the header
+ /// couldn't be read or it isn't a validate LZIP header, or the dictionary
+ /// size if it *is* a valid LZIP file.
+ ///
+ private static int ValidateAndReadSize(Stream stream)
+ {
+ if (stream == null)
+ {
+ throw new ArgumentNullException(nameof(stream));
+ }
+ // Read the header
+ byte[] header = new byte[6];
+ int n = stream.Read(header, 0, header.Length);
+
+ // TODO: Handle reading only part of the header?
+
+ if (n != 6)
+ {
+ return 0;
+ }
+
+ if (header[0] != 'L' || header[1] != 'Z' || header[2] != 'I' || header[3] != 'P' || header[4] != 1 /* version 1 */)
+ {
+ return 0;
+ }
+ int basePower = header[5] & 0x1F;
+ int subtractionNumerator = (header[5] & 0xE0) >> 5;
+ return (1 << basePower) - subtractionNumerator * (1 << (basePower - 4));
+ }
+
+ ///
+ /// Creates a byte array to communicate the parameters and dictionary size to LzmaStream.
+ ///
+ private static byte[] GetProperties(int dictionarySize) =>
+ new byte[]
+ {
+ // Parameters as per http://www.nongnu.org/lzip/manual/lzip_manual.html#Stream-format
+ // but encoded as a single byte in the format LzmaStream expects.
+ // literal_context_bits = 3
+ // literal_pos_state_bits = 0
+ // pos_state_bits = 2
+ 93,
+ // Dictionary size as 4-byte little-endian value
+ (byte)(dictionarySize & 0xff),
+ (byte)((dictionarySize >> 8) & 0xff),
+ (byte)((dictionarySize >> 16) & 0xff),
+ (byte)((dictionarySize >> 24) & 0xff)
+ };
+ }
+}
diff --git a/src/SharpCompress/Readers/ReaderFactory.cs b/src/SharpCompress/Readers/ReaderFactory.cs
index a1a931f4..0cd9b9fe 100644
--- a/src/SharpCompress/Readers/ReaderFactory.cs
+++ b/src/SharpCompress/Readers/ReaderFactory.cs
@@ -13,6 +13,7 @@ using SharpCompress.Readers.GZip;
using SharpCompress.Readers.Rar;
using SharpCompress.Readers.Tar;
using SharpCompress.Readers.Zip;
+using SharpCompress.Compressors.LZMA;
namespace SharpCompress.Readers
{
@@ -64,6 +65,18 @@ namespace SharpCompress.Readers
}
}
+ rewindableStream.Rewind(false);
+ if (LZipStream.IsLZipFile(rewindableStream))
+ {
+ rewindableStream.Rewind(false);
+ LZipStream testStream = new LZipStream(rewindableStream, CompressionMode.Decompress, true);
+ if (TarArchive.IsTarFile(testStream))
+ {
+ rewindableStream.Rewind(true);
+ return new TarReader(rewindableStream, options, CompressionType.LZip);
+ }
+ }
+
rewindableStream.Rewind(false);
if (RarArchive.IsRarFile(rewindableStream, options))
{
diff --git a/src/SharpCompress/Readers/Tar/TarReader.cs b/src/SharpCompress/Readers/Tar/TarReader.cs
index bc8a5642..43543b86 100644
--- a/src/SharpCompress/Readers/Tar/TarReader.cs
+++ b/src/SharpCompress/Readers/Tar/TarReader.cs
@@ -9,6 +9,7 @@ using SharpCompress.Compressors;
using SharpCompress.Compressors.BZip2;
using SharpCompress.Compressors.Deflate;
using SharpCompress.IO;
+using SharpCompress.Compressors.LZMA;
namespace SharpCompress.Readers.Tar
{
@@ -38,6 +39,10 @@ namespace SharpCompress.Readers.Tar
{
return new GZipStream(stream, CompressionMode.Decompress);
}
+ case CompressionType.LZip:
+ {
+ return new LZipStream(stream, CompressionMode.Decompress);
+ }
case CompressionType.None:
{
return stream;
@@ -87,6 +92,19 @@ namespace SharpCompress.Readers.Tar
}
throw new InvalidFormatException("Not a tar file.");
}
+
+ rewindableStream.Rewind(false);
+ if (LZipStream.IsLZipFile(rewindableStream))
+ {
+ rewindableStream.Rewind(false);
+ LZipStream testStream = new LZipStream(rewindableStream, CompressionMode.Decompress, false);
+ if (TarArchive.IsTarFile(testStream))
+ {
+ rewindableStream.Rewind(true);
+ return new TarReader(rewindableStream, options, CompressionType.LZip);
+ }
+ throw new InvalidFormatException("Not a tar file.");
+ }
rewindableStream.Rewind(true);
return new TarReader(rewindableStream, options, CompressionType.None);
}
diff --git a/test/SharpCompress.Test/Tar/TarReaderTests.cs b/test/SharpCompress.Test/Tar/TarReaderTests.cs
index d251ad3c..2251b0d8 100644
--- a/test/SharpCompress.Test/Tar/TarReaderTests.cs
+++ b/test/SharpCompress.Test/Tar/TarReaderTests.cs
@@ -33,6 +33,12 @@ namespace SharpCompress.Test
Read("Tar.tar.gz", CompressionType.GZip);
}
+ [Fact]
+ public void Tar_LZip_Reader()
+ {
+ Read("Tar.tar.lz", CompressionType.LZip);
+ }
+
[Fact]
public void Tar_BZip2_Entry_Stream()
{
diff --git a/test/TestArchives/Archives/Tar.tar.lz b/test/TestArchives/Archives/Tar.tar.lz
new file mode 100644
index 00000000..650772db
Binary files /dev/null and b/test/TestArchives/Archives/Tar.tar.lz differ