Files
SabreTools/SabreTools.Library/External/Compress/SevenZip/Compress/ZSTD/ZstandardStream.cs
Matt Nadareski 916d2a3b51 Internal Fixes, etc. (#20)
* Start removing mixed usages

* Check for directories before opening

* Fix writing

* Kinda fix rebuild

* One more try

* Better internal handling

* Slighty fix a couple more things

* Update RVWorld Compress code to db7d750bba

* Fix build

Co-authored-by: Matt Nadareski <mnadareski@mparticle.com>
2020-04-03 13:19:21 -07:00

398 lines
15 KiB
C#

using System;
using System.IO;
using System.IO.Compression;
using System.Runtime.InteropServices;
using Interop = Zstandard.Net.ZstandardInterop;
namespace Zstandard.Net
{
/// <summary>
/// Provides methods and properties for compressing and decompressing streams by using the Zstandard algorithm.
/// </summary>
public class ZstandardStream : Stream
{
private Stream stream;
private CompressionMode mode;
private Boolean leaveOpen;
private Boolean isClosed = false;
private Boolean isDisposed = false;
private Boolean isInitialized = false;
private IntPtr zstream;
private uint zstreamInputSize;
private uint zstreamOutputSize;
private byte[] data;
private bool dataDepleted = false;
private bool dataSkipRead = false;
private int dataPosition = 0;
private int dataSize = 0;
private long position = 0;
private Interop.Buffer outputBuffer = new Interop.Buffer();
private Interop.Buffer inputBuffer = new Interop.Buffer();
/// <summary>
/// Initializes a new instance of the <see cref="ZstandardStream"/> class by using the specified stream and compression mode, and optionally leaves the stream open.
/// </summary>
/// <param name="stream">The stream to compress.</param>
/// <param name="mode">One of the enumeration values that indicates whether to compress or decompress the stream.</param>
/// <param name="leaveOpen">true to leave the stream open after disposing the <see cref="ZstandardStream"/> object; otherwise, false.</param>
public ZstandardStream(Stream stream, CompressionMode mode, bool leaveOpen = false)
{
this.stream = stream ?? throw new ArgumentNullException(nameof(stream));
this.mode = mode;
this.leaveOpen = leaveOpen;
position = 0;
if (mode == CompressionMode.Compress)
{
this.zstreamInputSize = Interop.ZSTD_CStreamInSize().ToUInt32();
this.zstreamOutputSize = Interop.ZSTD_CStreamOutSize().ToUInt32();
this.zstream = Interop.ZSTD_createCStream();
this.data = new byte[(int)this.zstreamOutputSize];
}
if (mode == CompressionMode.Decompress)
{
this.zstreamInputSize = Interop.ZSTD_DStreamInSize().ToUInt32();
this.zstreamOutputSize = Interop.ZSTD_DStreamOutSize().ToUInt32();
this.zstream = Interop.ZSTD_createDStream();
this.data = new byte[(int)this.zstreamInputSize];
}
}
/// <summary>
/// Initializes a new instance of the <see cref="ZstandardStream"/> class by using the specified stream and compression level, and optionally leaves the stream open.
/// </summary>
/// <param name="stream">The stream to compress.</param>
/// <param name="compressionLevel">The compression level.</param>
/// <param name="leaveOpen">true to leave the stream open after disposing the <see cref="ZstandardStream"/> object; otherwise, false.</param>
public ZstandardStream(Stream stream, int compressionLevel, bool leaveOpen = false) : this(stream, CompressionMode.Compress, leaveOpen)
{
this.CompressionLevel = compressionLevel;
}
//-----------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------
/// <summary>
/// The version of the native Zstd library.
/// </summary>
public static Version Version
{
get
{
var version = (int)Interop.ZSTD_versionNumber();
return new Version((version / 10000) % 100, (version / 100) % 100, version % 100);
}
}
/// <summary>
/// The maximum compression level supported by the native Zstd library.
/// </summary>
public static int MaxCompressionLevel
{
get
{
return Interop.ZSTD_maxCLevel();
}
}
//-----------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------
/// <summary>
/// Gets or sets the compression level to use, the default is 6.
/// </summary>
/// <remarks>
/// To get the maximum compression level see <see cref="MaxCompressionLevel"/>.
/// </remarks>
public int CompressionLevel { get; set; } = 6;
/// <summary>
/// Gets or sets the compression dictionary tp use, the default is null.
/// </summary>
/// <value>
/// The compression dictionary.
/// </value>
public ZstandardDictionary CompressionDictionary { get; set; } = null;
/// <summary>
/// Gets whether the current stream supports reading.
/// </summary>
public override bool CanRead => this.stream.CanRead && this.mode == CompressionMode.Decompress;
/// <summary>
/// Gets whether the current stream supports writing.
/// </summary>
public override bool CanWrite => this.stream.CanWrite && this.mode == CompressionMode.Compress;
/// <summary>
/// Gets whether the current stream supports seeking.
/// </summary>
public override bool CanSeek => false;
/// <summary>
/// Gets the length in bytes of the stream.
/// </summary>
public override long Length => throw new NotSupportedException();
/// <summary>
/// Gets or sets the position within the current stream.
/// </summary>
public override long Position
{
get => position;
set => throw new NotSupportedException();
}
//-----------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (this.isDisposed == false)
{
if (!this.isClosed) ReleaseResources(flushStream: false);
this.isDisposed = true;
this.data = null;
}
}
public override void Close()
{
if (this.isClosed) return;
try
{
ReleaseResources(flushStream: true);
}
finally
{
this.isClosed = true;
base.Close();
}
}
private void ReleaseResources(bool flushStream)
{
if (this.mode == CompressionMode.Compress)
{
try
{
if (flushStream)
{
this.ProcessStream((zcs, buffer) => Interop.ThrowIfError(Interop.ZSTD_flushStream(zcs, buffer)));
this.ProcessStream((zcs, buffer) => Interop.ThrowIfError(Interop.ZSTD_endStream(zcs, buffer)));
this.stream.Flush();
}
}
finally
{
Interop.ZSTD_freeCStream(this.zstream);
if (!this.leaveOpen) this.stream.Close();
}
}
else if (this.mode == CompressionMode.Decompress)
{
Interop.ZSTD_freeDStream(this.zstream);
if (!this.leaveOpen) this.stream.Close();
}
}
public override void Flush()
{
if (this.mode == CompressionMode.Compress)
{
this.ProcessStream((zcs, buffer) => Interop.ThrowIfError(Interop.ZSTD_flushStream(zcs, buffer)));
this.stream.Flush();
}
}
public override int Read(byte[] buffer, int offset, int count)
{
if (this.CanRead == false) throw new NotSupportedException();
// prevent the buffers from being moved around by the garbage collector
var alloc1 = GCHandle.Alloc(buffer, GCHandleType.Pinned);
var alloc2 = GCHandle.Alloc(this.data, GCHandleType.Pinned);
try
{
var length = 0;
if (this.isInitialized == false)
{
this.isInitialized = true;
var result = this.CompressionDictionary == null
? Interop.ZSTD_initDStream(this.zstream)
: Interop.ZSTD_initDStream_usingDDict(this.zstream, this.CompressionDictionary.GetDecompressionDictionary());
}
while (count > 0)
{
var inputSize = this.dataSize - this.dataPosition;
// read data from input stream
if (inputSize <= 0 && !this.dataDepleted && !this.dataSkipRead)
{
this.dataSize = this.stream.Read(this.data, 0, (int)this.zstreamInputSize);
this.dataDepleted = this.dataSize <= 0;
this.dataPosition = 0;
inputSize = this.dataDepleted ? 0 : this.dataSize;
// skip stream.Read until the internal buffer is depleted
// avoids a Read timeout for applications that know the exact number of bytes in the stream
this.dataSkipRead = true;
}
// configure the inputBuffer
this.inputBuffer.Data = inputSize <= 0 ? IntPtr.Zero : Marshal.UnsafeAddrOfPinnedArrayElement(this.data, this.dataPosition);
this.inputBuffer.Size = inputSize <= 0 ? UIntPtr.Zero : new UIntPtr((uint)inputSize);
this.inputBuffer.Position = UIntPtr.Zero;
// configure the outputBuffer
this.outputBuffer.Data = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, offset);
this.outputBuffer.Size = new UIntPtr((uint)count);
this.outputBuffer.Position = UIntPtr.Zero;
// decompress inputBuffer to outputBuffer
Interop.ThrowIfError(Interop.ZSTD_decompressStream(this.zstream, this.outputBuffer, this.inputBuffer));
// calculate progress in outputBuffer
var outputBufferPosition = (int)this.outputBuffer.Position.ToUInt32();
if (outputBufferPosition == 0)
{
// the internal buffer is depleted, we're either done
if (this.dataDepleted) break;
// or we need more bytes
this.dataSkipRead = false;
}
length += outputBufferPosition;
offset += outputBufferPosition;
count -= outputBufferPosition;
// calculate progress in inputBuffer
var inputBufferPosition = (int)inputBuffer.Position.ToUInt32();
this.dataPosition += inputBufferPosition;
}
position += length;
return length;
}
finally
{
alloc1.Free();
alloc2.Free();
}
}
public override void Write(byte[] buffer, int offset, int count)
{
if (this.CanWrite == false) throw new NotSupportedException();
// prevent the buffers from being moved around by the garbage collector
var alloc1 = GCHandle.Alloc(buffer, GCHandleType.Pinned);
var alloc2 = GCHandle.Alloc(this.data, GCHandleType.Pinned);
try
{
if (this.isInitialized == false)
{
this.isInitialized = true;
var result = this.CompressionDictionary == null
? Interop.ZSTD_initCStream(this.zstream, this.CompressionLevel)
: Interop.ZSTD_initCStream_usingCDict(this.zstream, this.CompressionDictionary.GetCompressionDictionary(this.CompressionLevel));
Interop.ThrowIfError(result);
}
while (count > 0)
{
var inputSize = Math.Min((uint)count, this.zstreamInputSize);
// configure the outputBuffer
this.outputBuffer.Data = Marshal.UnsafeAddrOfPinnedArrayElement(this.data, 0);
this.outputBuffer.Size = new UIntPtr(this.zstreamOutputSize);
this.outputBuffer.Position = UIntPtr.Zero;
// configure the inputBuffer
this.inputBuffer.Data = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, offset);
this.inputBuffer.Size = new UIntPtr((uint)inputSize);
this.inputBuffer.Position = UIntPtr.Zero;
// compress inputBuffer to outputBuffer
Interop.ThrowIfError(Interop.ZSTD_compressStream(this.zstream, this.outputBuffer, this.inputBuffer));
// write data to output stream
var outputBufferPosition = (int)this.outputBuffer.Position.ToUInt32();
this.stream.Write(this.data, 0, outputBufferPosition);
// calculate progress in inputBuffer
var inputBufferPosition = (int)this.inputBuffer.Position.ToUInt32();
offset += inputBufferPosition;
count -= inputBufferPosition;
}
}
finally
{
alloc1.Free();
alloc2.Free();
}
}
public override long Seek(long offset, SeekOrigin origin)
{
if (origin != SeekOrigin.Current)
throw new NotImplementedException();
byte[] tmpBuff = new byte[1024];
long sizeToGo = offset;
while (sizeToGo > 0)
{
int sizenow = sizeToGo > 1024 ? 1024 : (int)sizeToGo;
Read(tmpBuff, 0, sizenow);
sizeToGo -= sizenow;
}
position += offset;
return offset;
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
//-----------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------
private void ProcessStream(Action<IntPtr, Interop.Buffer> outputAction)
{
var alloc = GCHandle.Alloc(this.data, GCHandleType.Pinned);
try
{
this.outputBuffer.Data = Marshal.UnsafeAddrOfPinnedArrayElement(this.data, 0);
this.outputBuffer.Size = new UIntPtr(this.zstreamOutputSize);
this.outputBuffer.Position = UIntPtr.Zero;
outputAction(this.zstream, this.outputBuffer);
var outputBufferPosition = (int)this.outputBuffer.Position.ToUInt32();
this.stream.Write(this.data, 0, outputBufferPosition);
}
finally
{
alloc.Free();
}
}
}
}