Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial read-only support for LZip #191

Merged
merged 1 commit into from
Oct 14, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/SharpCompress/Common/CompressionType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public enum CompressionType
LZMA,
BCJ,
BCJ2,
LZip,
Unknown
}
}
153 changes: 153 additions & 0 deletions src/SharpCompress/Compressors/LZMA/LZipStream.cs
Original file line number Diff line number Diff line change
@@ -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

/// <summary>
/// Stream supporting the LZIP format, as documented at http://www.nongnu.org/lzip/manual/lzip_manual.html
/// </summary>
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

/// <summary>
/// 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.
/// </summary>
/// <param name="stream">The stream to read from. Must not be null.</param>
/// <returns><c>true</c> if the given stream is an LZip file, <c>false</c> otherwise.</returns>
public static bool IsLZipFile(Stream stream) => ValidateAndReadSize(stream) != 0;

/// <summary>
/// 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.
/// </summary>
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));
}

/// <summary>
/// Creates a byte array to communicate the parameters and dictionary size to LzmaStream.
/// </summary>
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)
};
}
}
13 changes: 13 additions & 0 deletions src/SharpCompress/Readers/ReaderFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using SharpCompress.Readers.Rar;
using SharpCompress.Readers.Tar;
using SharpCompress.Readers.Zip;
using SharpCompress.Compressors.LZMA;

namespace SharpCompress.Readers
{
Expand Down Expand Up @@ -64,6 +65,18 @@ public static IReader Open(Stream stream, ReaderOptions options = null)
}
}

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))
{
Expand Down
18 changes: 18 additions & 0 deletions src/SharpCompress/Readers/Tar/TarReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using SharpCompress.Compressors.BZip2;
using SharpCompress.Compressors.Deflate;
using SharpCompress.IO;
using SharpCompress.Compressors.LZMA;

namespace SharpCompress.Readers.Tar
{
Expand Down Expand Up @@ -38,6 +39,10 @@ internal override Stream RequestInitialStream()
{
return new GZipStream(stream, CompressionMode.Decompress);
}
case CompressionType.LZip:
{
return new LZipStream(stream, CompressionMode.Decompress);
}
case CompressionType.None:
{
return stream;
Expand Down Expand Up @@ -87,6 +92,19 @@ public static TarReader Open(Stream stream, ReaderOptions options = null)
}
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);
}
Expand Down
6 changes: 6 additions & 0 deletions test/SharpCompress.Test/Tar/TarReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ public void Tar_GZip_Reader()
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()
{
Expand Down
Binary file added test/TestArchives/Archives/Tar.tar.lz
Binary file not shown.