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