Implemented ZLib streams

Refactored tag classes into own namespace
This commit is contained in:
ForeverZer0 2021-08-24 02:55:03 -04:00
parent 22f0404561
commit 3672fad3cb
28 changed files with 834 additions and 76 deletions

View File

@ -15,7 +15,7 @@ namespace SharpNBT.Tests
compound.Add(new ByteTag("Child Byte", 255));
compound.Add(new StringTag("Child String", "Hello World!"));
using var stream = NbtFile.OpenWrite("./Data/write-test-uncompressed.nbt", CompressionLevel.NoCompression);
using var stream = NbtFile.OpenWrite("./Data/write-test-uncompressed.nbt", CompressionType.None);
stream.WriteTag(compound);
}
@ -26,7 +26,7 @@ namespace SharpNBT.Tests
compound.Add(new ByteTag("Child Byte", 255));
compound.Add(new StringTag("Child String", "Hello World!"));
using var stream = NbtFile.OpenWrite("./Data/write-test-compressed.nbt", CompressionLevel.Optimal);
using var stream = NbtFile.OpenWrite("./Data/write-test-compressed.nbt", CompressionType.GZip, CompressionLevel.Optimal);
stream.WriteTag(compound);
}

58
SharpNBT.Tests/ZLib.cs Normal file
View File

@ -0,0 +1,58 @@
using System.IO;
using System.IO.Compression;
using System.Text;
using SharpNBT.ZLib;
using Xunit;
using Xunit.Abstractions;
namespace SharpNBT.Tests
{
public class ZLib
{
private readonly ITestOutputHelper console;
public ZLib(ITestOutputHelper output)
{
console = output;
}
[Fact]
public void Compress()
{
var text = File.ReadAllText("./Data/bigtest.json");
using (var output = File.OpenWrite("./Data/bigtest.zlib"))
{
using (var zlib = new ZLibStream(output, CompressionLevel.Fastest, true))
{
var bytes = Encoding.UTF8.GetBytes(text);
zlib.Write(bytes, 0, bytes.Length);
}
}
using (var input = File.OpenRead("./Data/bigtest.zlib"))
{
using (var zlib = new ZLibStream(input, CompressionMode.Decompress))
{
var sb = new StringBuilder();
var buffer = new byte[1024];
while (true)
{
var read = zlib.Read(buffer, 0, 1024);
sb.Append(Encoding.UTF8.GetString(buffer, 0, read));
if (read < 1024)
break;
}
console.WriteLine(sb.ToString());
}
}
}
}
}

View File

@ -0,0 +1,32 @@
using JetBrains.Annotations;
namespace SharpNBT
{
/// <summary>
/// Describes compression formats supported by the NBT specification.
/// </summary>
[PublicAPI]
public enum CompressionType : byte
{
/// <summary>
/// No compression.
/// </summary>
None,
/// <summary>
/// GZip compression
/// </summary>
GZip,
/// <summary>
/// ZLib compression
/// </summary>
ZLib,
/// <summary>
/// Automatically detect compression using magic numbers.
/// </summary>
AutoDetect = 0xFF
}
}

View File

@ -26,7 +26,7 @@ namespace SharpNBT
/// <inheritdoc cref="BigEndianBytes(short)"/>
[CLSCompliant(false)]
internal static byte[] BigEndianBytes(this ushort n) => BitConverter.GetBytes(BitConverter.IsLittleEndian ? SwapEndian(n) : n);
public static byte[] BigEndianBytes(this ushort n) => BitConverter.GetBytes(BitConverter.IsLittleEndian ? SwapEndian(n) : n);
/// <inheritdoc cref="BigEndianBytes(short)"/>
internal static byte[] BigEndianBytes(this float n)

View File

@ -1,10 +1,14 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Threading.Tasks;
using JetBrains.Annotations;
using SharpNBT.ZLib;
namespace SharpNBT
{
// TODO: Implement the allowance of ListTag via overloads when writing in Bedrock compatible format
/// <summary>
/// Provides static convenience methods for working with NBT-formatted files, including both reading and writing.
/// </summary>
@ -15,23 +19,23 @@ namespace SharpNBT
/// Reads a file at the given <paramref name="path"/> and deserializes the top-level <see cref="CompoundTag"/> contained in the file.
/// </summary>
/// <param name="path">The path to the file to be read.</param>
/// <param name="compression">Indicates the compression algorithm used to compress the file.</param>
/// <returns>The deserialized <see cref="CompoundTag"/> instance.</returns>
public static CompoundTag Read([NotNull] string path)
public static CompoundTag Read([NotNull] string path, CompressionType compression = CompressionType.AutoDetect)
{
using var stream = File.OpenRead(path);
using var reader = new TagReader(stream, IsCompressed(path), false);
using var reader = new TagReader(GetReadStream(path, compression));
return reader.ReadTag<CompoundTag>();
}
/// <summary>
/// Asynchronously reads a file at the given <paramref name="path"/> and deserializes the top-level <see cref="CompoundTag"/> contained in the file.
/// </summary>
/// <param name="path">The path to the file to be read.</param>
/// <param name="compression">Indicates the compression algorithm used to compress the file.</param>
/// <returns>The deserialized <see cref="CompoundTag"/> instance.</returns>
public static async Task<CompoundTag> ReadAsync([NotNull] string path)
public static async Task<CompoundTag> ReadAsync([NotNull] string path, CompressionType compression = CompressionType.AutoDetect)
{
await using var stream = File.OpenRead(path);
await using var reader = new TagReader(stream, IsCompressed(path), false);
await using var reader = new TagReader(GetReadStream(path, compression));
return await reader.ReadTagAsync<CompoundTag>();
}
@ -39,60 +43,88 @@ namespace SharpNBT
/// Writes the given <paramref name="tag"/> to a file at the specified <paramref name="path"/>.
/// </summary>
/// <param name="path">The path to the file to be written to.</param>
/// <param name="type">A flag indicating the type of compression to use.</param>
/// <param name="tag">The top-level <see cref="CompoundTag"/> instance to be serialized.</param>
/// <param name="compression">Indicates a compression strategy to be used, if any.</param>
public static void Write([NotNull] string path, [NotNull] CompoundTag tag, CompressionLevel compression = CompressionLevel.NoCompression)
/// <param name="level">Indicates a compression strategy to be used, if any.</param>
public static void Write([NotNull] string path, [NotNull] CompoundTag tag, CompressionType type = CompressionType.GZip, CompressionLevel level = CompressionLevel.Fastest)
{
using var stream = File.OpenWrite(path);
using var writer = new TagWriter(stream, compression);
using var writer = new TagWriter(GetWriteStream(stream, type, level));
writer.WriteTag(tag);
}
public static async Task WriteAsync([NotNull] string path, [NotNull] CompoundTag tag, CompressionLevel compression = CompressionLevel.NoCompression)
public static async Task WriteAsync([NotNull] string path, [NotNull] CompoundTag tag, CompressionType type = CompressionType.GZip, CompressionLevel level = CompressionLevel.Fastest)
{
await using var stream = File.OpenWrite(path);
await using var writer = new TagWriter(stream, compression);
await using var writer = new TagWriter(GetWriteStream(stream, type, level));
await writer.WriteTagAsync(tag);
}
/// <summary>
/// Detects the presence of a GZip compressed file at the given <paramref name="path"/> by searching for the "magic number" in the header.
/// </summary>
/// <param name="path">The path of the file to query.</param>
/// <returns><see langword="true"/> if GZip compression was detected, otherwise <see langword="false"/>.</returns>
public static bool IsCompressed([NotNull] string path)
{
using var str = File.OpenRead(path);
return str.ReadByte() == 0x1F && str.ReadByte() == 0x8B;
}
/// <summary>
/// Opens an existing NBT file for reading, and returns a <see cref="TagReader"/> instance for it.
/// </summary>
/// <param name="path">The path of the file to query write.</param>
/// <param name="compression">Indicates the compression algorithm used to compress the file.</param>
/// <returns>A <see cref="TagReader"/> instance for the file stream.</returns>
/// <remarks>File compression will be automatically detected and used handled when necessary.</remarks>
public static TagReader OpenRead([NotNull] string path)
public static TagReader OpenRead([NotNull] string path, CompressionType compression = CompressionType.AutoDetect)
{
var compressed = IsCompressed(path);
var stream = File.OpenRead(path);
return new TagReader(stream, compressed, false);
return new TagReader(GetReadStream(path, compression));
}
/// <summary>
/// Opens an existing NBT file or creates a new one if one if it does not exist, and returns a <see cref="TagWriter"/> instance for it.
/// </summary>
/// <param name="path">The path of the file to query write.</param>
/// <param name="compression">A flag indicating the compression strategy that will be used, if any.</param>
/// <param name="type">A flag indicating the type of compression to use.</param>
/// <param name="level">A flag indicating the compression strategy that will be used, if any.</param>
/// <returns>A <see cref="TagWriter"/> instance for the file stream.</returns>
public static TagWriter OpenWrite([NotNull] string path, CompressionLevel compression = CompressionLevel.NoCompression)
public static TagWriter OpenWrite([NotNull] string path, CompressionType type = CompressionType.GZip, CompressionLevel level = CompressionLevel.Fastest)
{
var stream = File.OpenWrite(path);
if (compression == CompressionLevel.NoCompression)
return new TagWriter(stream, compression);
var gzip = new GZipStream(stream, compression, false);
return new TagWriter(gzip, compression);
var stream = GetWriteStream(File.OpenWrite(path), type, level);
return new TagWriter(stream);
}
private static Stream GetWriteStream(Stream stream, CompressionType type, CompressionLevel level)
{
switch (type)
{
case CompressionType.None: return stream;
case CompressionType.GZip: return new GZipStream(stream, level, false);
case CompressionType.ZLib: return new ZLibStream(stream, level);
case CompressionType.AutoDetect:
throw new ArgumentOutOfRangeException(nameof(type), "Auto-detect is not a valid compression type for writing files.");
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
private static Stream GetReadStream(string path, CompressionType compression)
{
var stream = File.OpenRead(path);
if (compression == CompressionType.AutoDetect)
{
var firstByte = (byte) stream.ReadByte();
stream.Seek(0, SeekOrigin.Begin);
compression = firstByte switch
{
0x78 => CompressionType.ZLib,
0x1F => CompressionType.GZip,
0x08 => CompressionType.None, // ListTag (valid in Bedrock)
0x0A => CompressionType.None, // CompoundTag
_ => throw new FormatException("Unable to detect compression type.")
};
}
return compression switch
{
CompressionType.None => stream,
CompressionType.GZip => new GZipStream(stream, CompressionMode.Decompress, false),
CompressionType.ZLib => new ZLibStream(stream, CompressionMode.Decompress),
_ => throw new ArgumentOutOfRangeException(nameof(compression), compression, null)
};
}
}
}

34
SharpNBT/Specification.cs Normal file
View File

@ -0,0 +1,34 @@
using JetBrains.Annotations;
namespace SharpNBT
{
/// <summary>
/// Describes the specification to use for reading/writing.
/// </summary>
/// <remarks>
/// There are some major changes between the original Java version, and the Bedrock editions of Minecraft that makes them incompatible with one another.
/// Furthermore, the Bedrock editions use a different specification depending on whether it is writing to disk or sending over a network.
/// </remarks>
/// <seealso href="https://wiki.vg/NBT#Bedrock_edition"/>
[PublicAPI]
public enum Specification
{
/// <summary>
/// The original NBT specification which encodes all numbers in big-endian format.
/// </summary>
/// <remarks>This is the specification used by Java editions of Minecraft.</remarks>
Standard,
/// <summary>
/// Similar to <see cref="Standard"/>, but numbers are in little-endian format.
/// </summary>
/// <remarks>This specification is used by Bedrock editions of Minecraft when reading/writing to <b>disk</b>.</remarks>
LittleEndian,
/// <summary>
/// Similar to <see cref="LittleEndian"/> format, but uses variable-length integers in place of many fixed-length integers.
/// </summary>
/// <remarks>This specification is used by Bedrock editions of Minecraft when reading/writing to a <b>network stream</b>.</remarks>
VarInt
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
@ -41,25 +40,12 @@ namespace SharpNBT
/// <param name="leaveOpen">
/// <paramref langword="true"/> to leave the <paramref name="stream"/> object open after disposing the <see cref="TagReader"/>
/// object; otherwise, <see langword="false"/>.</param>
public TagReader([NotNull] Stream stream, bool leaveOpen) : this(stream, stream is GZipStream, leaveOpen)
{
}
/// <summary>
/// Creates a new instance of the <see cref="TagReader"/> class from the given <paramref name="stream"/>.
/// </summary>
/// <param name="stream">A <see cref="Stream"/> instance that the reader will be reading from.</param>
/// <param name="compressed">Flag indicating if the underlying <paramref name="stream"/> is compressed.</param>
/// <param name="leaveOpen">
/// <paramref langword="true"/> to leave the <paramref name="stream"/> object open after disposing the <see cref="TagReader"/>
/// object; otherwise, <see langword="false"/>.</param>
public TagReader([NotNull] Stream stream, bool compressed, bool leaveOpen)
public TagReader([NotNull] Stream stream, bool leaveOpen = false)
{
BaseStream = stream ?? throw new ArgumentNullException(nameof(stream));
if (!stream.CanRead)
throw new IOException("Stream is not opened for reading.");
this.leaveOpen = leaveOpen;
if (compressed && !(stream is GZipStream))
BaseStream = new GZipStream(stream, CompressionMode.Decompress, leaveOpen);
else
BaseStream = stream;
}
/// <summary>

View File

@ -1,6 +1,5 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -31,28 +30,14 @@ namespace SharpNBT
/// <param name="leaveOpen">
/// <paramref langword="true"/> to leave the <paramref name="stream"/> object open after disposing the <see cref="TagWriter"/>
/// object; otherwise, <see langword="false"/>.</param>
public TagWriter([NotNull] Stream stream, bool leaveOpen = false) : this(stream, CompressionLevel.NoCompression, leaveOpen)
{
}
/// <summary>
/// Creates a new instance of the <see cref="TagWriter"/> class from the given <paramref name="stream"/>.
/// </summary>
/// <param name="stream">A <see cref="Stream"/> instance that the writer will be writing to.</param>
/// <param name="compression">Indicates a compression strategy to be used, if any.</param>
/// <param name="leaveOpen">
/// <paramref langword="true"/> to leave the <paramref name="stream"/> object open after disposing the <see cref="TagWriter"/>
/// object; otherwise, <see langword="false"/>.</param>
public TagWriter([NotNull] Stream stream, CompressionLevel compression, bool leaveOpen = false)
public TagWriter([NotNull] Stream stream, bool leaveOpen = false)
{
BaseStream = stream ?? throw new ArgumentNullException(nameof(stream));
if (!stream.CanWrite)
throw new IOException("Stream is not opened for writing.");
this.leaveOpen = leaveOpen;
if (compression != CompressionLevel.NoCompression && !(stream is GZipStream))
BaseStream = new GZipStream(stream, compression, leaveOpen);
else
BaseStream = stream ?? throw new ArgumentNullException(nameof(stream), "Stream cannot be null");
}
/// <summary>
/// Writes a <see cref="ByteTag"/> to the stream.
/// </summary>

113
SharpNBT/ZLib/Adler32.cs Normal file
View File

@ -0,0 +1,113 @@
using System;
using System.Runtime.CompilerServices;
namespace SharpNBT.ZLib
{
/// <summary>
/// An Adler-32 checksum implementation for ZLib streams.
/// </summary>
/// <seealso href=""/>
public sealed class Adler32
{
private uint a = 1;
private uint b;
private const int BASE = 65521;
private const int MAX = 5550;
private int pending;
/// <summary>
/// Update the checksum value with the specified <paramref name="data"/>.
/// </summary>
/// <param name="data">A single value to calculate into the checksum.</param>
public void Update(byte data)
{
if (pending >= MAX)
UpdateModulus();
a += data;
b += a;
pending++;
}
/// <summary>
/// Update the checksum value with the specified <paramref name="data"/>.
/// </summary>
/// <param name="data">A buffer containing the values to calculate into the checksum.</param>
public void Update(byte[] data) => Update(new ReadOnlySpan<byte>(data, 0, data.Length));
/// <summary>
/// Update the checksum value with the specified <paramref name="data"/>.
/// </summary>
/// <param name="data">A buffer containing the values to calculate into the checksum.</param>
public void Update(ReadOnlySpan<byte> data)
{
unchecked
{
var nextCompute = MAX - pending;
for (var i = 0; i < data.Length; i++)
{
if (i == nextCompute)
{
UpdateModulus();
nextCompute = i + MAX;
}
a += data[i];
b += a;
pending++;
}
}
}
/// <summary>
/// Update the checksum value with the specified <paramref name="data"/>.
/// </summary>
/// <param name="data">A buffer containing the values to calculate into the checksum.</param>
/// <param name="offset">An offset into the <paramref name="data"/> to begin adding from.</param>
/// <param name="length">The number of bytes in <paramref name="data"/> to calculate.</param>
public void Update(byte[] data, int offset, int length) => Update(new ReadOnlySpan<byte>(data, offset, length));
/// <summary>
/// Reset the checksum back to the initial state.
/// </summary>
public void Reset()
{
a = 1;
b = 0;
pending = 0;
}
/// <summary>
/// Gets the current calculated checksum value as a signed 32-bit integer.
/// </summary>
public int Value
{
get
{
if (pending > 0)
UpdateModulus();
return unchecked((int)((b << 16) | a));
}
}
/// <summary>
/// Gets the current calculated checksum value as an unsigned 32-bit integer.
/// </summary>
[CLSCompliant(false)]
public uint UnsignedValue
{
get
{
if (pending > 0)
UpdateModulus();
return (b << 16) | a;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UpdateModulus()
{
a %= BASE;
b %= BASE;
pending = 0;
}
}
}

115
SharpNBT/ZLib/ZLibHeader.cs Normal file
View File

@ -0,0 +1,115 @@
using System;
using System.IO.Compression;
using JetBrains.Annotations;
namespace SharpNBT.ZLib
{
/// <summary>
/// Provides methods for the creation of a ZLib header as outlined by RFC-1950.
/// </summary>
/// <see href="https://datatracker.ietf.org/doc/html/rfc1950"/>
public sealed class ZLibHeader
{
private byte compressionMethod;
private byte compressionInfo;
private byte fCheck;
private byte fLevel;
private byte fDict;
/// <summary>
/// Gets a flag indicating if this <see cref="ZLibHeader"/> represents a valid and supported ZLib format.
/// </summary>
public bool IsSupported { get; private set; }
/// <summary>
/// Creates a new instance of the <see cref="ZLibHeader"/> class using the specified compression strategy.
/// </summary>
/// <param name="compressionLevel">The desired level of compression.</param>
public ZLibHeader(CompressionLevel compressionLevel = CompressionLevel.Fastest)
{
const byte FASTER = 0;
const byte DEFAULT = 2;
const byte OPTIMAL = 3;
compressionMethod = 8; // Deflate algorithm
compressionInfo = 7; // Window size
fDict = 0; // false
fLevel = compressionLevel switch
{
CompressionLevel.NoCompression => FASTER,
CompressionLevel.Fastest => DEFAULT,
CompressionLevel.Optimal => OPTIMAL,
_ => throw new ArgumentOutOfRangeException(nameof(compressionLevel))
};
}
private void RefreshFCheck()
{
var flg = (byte) (Convert.ToByte(fLevel) << 1);
flg |= Convert.ToByte(fDict);
fCheck = Convert.ToByte(31 - Convert.ToByte((CMF * 256 + flg) % 31));
if (fCheck > 31)
throw new ArgumentOutOfRangeException(nameof(fCheck), "Value cannot be greater than 31.");
}
/// <summary>
/// Gets the computed "compression method and flags" (CMF) value of the header.
/// </summary>
// ReSharper disable once InconsistentNaming
private byte CMF => (byte)((compressionInfo << 4) | compressionMethod);
/// <summary>
/// Gets the computed "flags" (FLG) value of the header.
/// </summary>
// ReSharper disable once InconsistentNaming
private byte FLG => (byte)((fLevel << 6) | (fDict << 5) | fCheck);
/// <summary>
/// Computes and returns the CMF and FLG magic numbers associated with a ZLib header.
/// </summary>
/// <returns>A two element byte array containing the CMF and FLG values.</returns>
[NotNull]
public byte[] Encode()
{
var result = new byte[2];
RefreshFCheck();
result[0] = CMF;
result[1] = FLG;
return result;
}
/// <summary>
/// Calculates and returns a new <see cref="ZLibHeader"/> instance from the specified CMF and FLG magic bytes read from a ZLib header.
/// </summary>
/// <param name="cmf">The first byte of a ZLib header.</param>
/// <param name="flg">The second byte of a ZLib header.</param>
/// <returns>The decoded <see cref="ZLibHeader"/> instance.</returns>
[NotNull]
public static ZLibHeader Decode(int cmf, int flg)
{
var result = new ZLibHeader();
cmf = cmf & 0x0FF;
flg = flg & 0x0FF;
result.compressionInfo = Convert.ToByte((cmf & 0xF0) >> 4);
if (result.compressionInfo > 15)
throw new ArgumentOutOfRangeException(nameof(result.compressionInfo), "Value cannot be greater than 15");
result.compressionMethod = Convert.ToByte(cmf & 0x0F);
if (result.compressionInfo > 15)
throw new ArgumentOutOfRangeException(nameof(result.compressionMethod), "Value cannot be greater than 15");
result.fCheck = Convert.ToByte(flg & 0x1F);
result.fDict = Convert.ToByte((flg & 0x20) >> 5);
result.fLevel = Convert.ToByte((flg & 0xC0) >> 6);
result.IsSupported = (result.compressionMethod == 8) && (result.compressionInfo == 7) && (((cmf * 256 + flg) % 31 == 0)) && (result.fDict == 0);
return result;
}
}
}

403
SharpNBT/ZLib/ZLibStream.cs Normal file
View File

@ -0,0 +1,403 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
namespace SharpNBT.ZLib
{
/// <summary>
/// ZLib stream implementation for reading/writing.
/// <para/>
/// Generally speaking, the ZLib format is merely a DEFLATE stream, prefixed with a header, and performs a cyclic redundancy check to ensure data integrity
/// by storing an Adler-32 checksum after the compressed payload.
/// </summary>
[PublicAPI]
public class ZLibStream : Stream
{
protected readonly DeflateStream DeflateStream;
protected readonly Stream BaseStream;
private readonly CompressionMode compressionMode;
private readonly bool leaveOpen;
private readonly Adler32 adler32 = new Adler32();
private bool isClosed;
private byte[] checksum;
/// <summary>
/// Initializes a new instance of the <see cref="IsSupported"/> class using the specified compression <paramref name="level"/> and <paramref name="stream"/>.
/// </summary>
/// <param name="stream">A <see cref="Stream"/> instance to be compressed.</param>
/// <param name="level">The level of compression to use.</param>
public ZLibStream(Stream stream, CompressionLevel level) : this(stream, level, false)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="IsSupported"/> class using the specified compression <paramref name="mode"/> and <paramref name="stream"/>.
/// </summary>
/// <param name="stream">A <see cref="Stream"/> instance to be compressed or uncompressed.</param>
/// <param name="mode">The type of compression to use.</param>
public ZLibStream(Stream stream, CompressionMode mode) : this(stream, mode, false)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ZLibStream"/> class using the specified compression <paramref name="level"/> and <paramref name="stream"/>,
/// and optionally leaves the stream open.
/// </summary>
/// <param name="stream">A <see cref="Stream"/> instance to be compressed.</param>
/// <param name="level">The level of compression to use.</param>
/// <param name="leaveOpen">Indicates if the <paramref name="stream"/> should be left open after this <see cref="ZLibStream"/> is closed.</param>
public ZLibStream(Stream stream, CompressionLevel level, bool leaveOpen)
{
compressionMode = CompressionMode.Compress;
this.leaveOpen = leaveOpen;
BaseStream = stream;
DeflateStream = CreateStream(level);
}
/// <summary>
/// Initializes a new instance of the <see cref="ZLibStream"/> class using the specified compression <paramref name="mode"/> and <paramref name="stream"/>,
/// and optionally leaves the stream open.
/// </summary>
/// <param name="stream">A <see cref="Stream"/> instance to be compressed or uncompressed.</param>
/// <param name="mode">The type of compression to use.</param>
/// <param name="leaveOpen">Indicates if the <paramref name="stream"/> should be left open after this <see cref="ZLibStream"/> is closed.</param>
public ZLibStream(Stream stream, CompressionMode mode, bool leaveOpen)
{
compressionMode = mode;
this.leaveOpen = leaveOpen;
BaseStream = stream;
DeflateStream = CreateStream(CompressionLevel.Fastest);
}
/// <summary>Gets a value indicating whether the current stream supports reading.</summary>
/// <returns>
/// <see langword="true" /> if the stream supports reading; otherwise, <see langword="false" />.</returns>
public override bool CanRead => compressionMode == CompressionMode.Decompress && !isClosed;
/// <summary>Gets a value indicating whether the current stream supports writing.</summary>
/// <returns>
/// <see langword="true" /> if the stream supports writing; otherwise, <see langword="false" />.</returns>
public override bool CanWrite => compressionMode == CompressionMode.Compress && !isClosed;
/// <summary>Gets a value indicating whether the current stream supports seeking.</summary>
/// <returns>
/// <see langword="true" /> if the stream supports seeking; otherwise, <see langword="false" />.</returns>
public override bool CanSeek => false;
/// <summary>Gets the length in bytes of the stream.</summary>
/// <returns>A long value representing the length of the stream in bytes.</returns>
/// <exception cref="NotSupportedException">This property is not supported and will always throw an exception.</exception>
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed.</exception>
public override long Length => throw new NotSupportedException("Stream does not support this function.");
/// <summary>Gets or sets the position within the current stream.</summary>
/// <returns>The current position within the stream.</returns>
/// <exception cref="T:System.IO.IOException">An I/O error occurs.</exception>
/// <exception cref="NotSupportedException">This property is not supported and will always throw an exception.</exception>
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed.</exception>
public override long Position
{
get => throw new NotSupportedException("Stream does not support getting/setting position.");
set => throw new NotSupportedException("Stream does not support getting/setting position.");
}
/// <summary>Reads a byte from the stream and advances the position within the stream by one byte, or returns -1 if at the end of the stream.</summary>
/// <returns>The unsigned byte cast to an <see langword="Int32" />, or -1 if at the end of the stream.</returns>
/// <exception cref="T:System.NotSupportedException">The stream does not support reading.</exception>
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed.</exception>
public override int ReadByte()
{
var n = DeflateStream.ReadByte();
if (n == -1) // EOF
ReadCrc();
else
adler32.Update(Convert.ToByte(n));
return n;
}
/// <summary>Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read.</summary>
/// <param name="buffer">A region of memory. When this method returns, the contents of this region are replaced by the bytes read from the current source.</param>
/// <returns>The total number of bytes read into the buffer. This can be less than the number of bytes allocated in the buffer if that many bytes are not currently available, or zero (0) if the end of the stream has been reached.</returns>
public override int Read(Span<byte> buffer)
{
var read = DeflateStream.Read(buffer);
if (read < 1 && buffer.Length > 0)
ReadCrc();
else
adler32.Update(buffer[..read]);
return read;
}
/// <summary>Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read.</summary>
/// <param name="buffer">An array of bytes. When this method returns, the buffer contains the specified byte array with the values between <paramref name="offset" /> and (<paramref name="offset" /> + <paramref name="count" /> - 1) replaced by the bytes read from the current source.</param>
/// <param name="offset">The zero-based byte offset in <paramref name="buffer" /> at which to begin storing the data read from the current stream.</param>
/// <param name="count">The maximum number of bytes to be read from the current stream.</param>
/// <returns>The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached.</returns>
/// <exception cref="T:System.ArgumentException">The sum of <paramref name="offset" /> and <paramref name="count" /> is larger than the buffer length.</exception>
/// <exception cref="T:System.ArgumentNullException">
/// <paramref name="buffer" /> is <see langword="null" />.</exception>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// <paramref name="offset" /> or <paramref name="count" /> is negative.</exception>
/// <exception cref="T:System.IO.IOException">An I/O error occurs.</exception>
/// <exception cref="T:System.NotSupportedException">The stream does not support reading.</exception>
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed.</exception>
public override int Read(byte[] buffer, int offset, int count) => Read(new Span<byte>(buffer, offset, count));
/// <summary>Asynchronously reads a sequence of bytes from the current stream, advances the position within the stream by the number of bytes read, and monitors cancellation requests.</summary>
/// <param name="buffer">The region of memory to write the data into.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="P:System.Threading.CancellationToken.None" />.</param>
/// <returns>A task that represents the asynchronous read operation. The value of its <see cref="P:System.Threading.Tasks.ValueTask`1.Result" /> property contains the total number of bytes read into the buffer. The result value can be less than the number of bytes allocated in the buffer if that many bytes are not currently available, or it can be 0 (zero) if the end of the stream has been reached.</returns>
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
var read = await DeflateStream.ReadAsync(buffer, cancellationToken);
adler32.Update(buffer.Slice(0, read).Span);
return read;
}
/// <summary>Asynchronously reads a sequence of bytes from the current stream, advances the position within the stream by the number of bytes read, and monitors cancellation requests.</summary>
/// <param name="buffer">The buffer to write the data into.</param>
/// <param name="offset">The byte offset in <paramref name="buffer" /> at which to begin writing data from the stream.</param>
/// <param name="count">The maximum number of bytes to read.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="P:System.Threading.CancellationToken.None" />.</param>
/// <returns>A task that represents the asynchronous read operation. The value of the task parameter contains the total number of bytes read into the buffer. The result value can be less than the number of bytes requested if the number of bytes currently available is less than the requested number, or it can be 0 (zero) if the end of the stream has been reached.</returns>
/// <exception cref="T:System.ArgumentNullException">
/// <paramref name="buffer" /> is <see langword="null" />.</exception>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// <paramref name="offset" /> or <paramref name="count" /> is negative.</exception>
/// <exception cref="T:System.ArgumentException">The sum of <paramref name="offset" /> and <paramref name="count" /> is larger than the buffer length.</exception>
/// <exception cref="T:System.NotSupportedException">The stream does not support reading.</exception>
/// <exception cref="T:System.ObjectDisposedException">The stream has been disposed.</exception>
/// <exception cref="T:System.InvalidOperationException">The stream is currently in use by a previous read operation.</exception>
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
var read = await DeflateStream.ReadAsync(buffer, offset, count, cancellationToken);
adler32.Update(new ReadOnlySpan<byte>(buffer, offset, read));
return read;
}
/// <summary>Writes a byte to the current position in the stream and advances the position within the stream by one byte.</summary>
/// <param name="value">The byte to write to the stream.</param>
/// <exception cref="T:System.IO.IOException">An I/O error occurs.</exception>
/// <exception cref="T:System.NotSupportedException">The stream does not support writing, or the stream is already closed.</exception>
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed.</exception>
public override void WriteByte(byte value)
{
DeflateStream.WriteByte(value);
adler32.Update(value);
}
/// <summary>Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written.</summary>
/// <param name="buffer">An array of bytes. This method copies <paramref name="count" /> bytes from <paramref name="buffer" /> to the current stream.</param>
/// <param name="offset">The zero-based byte offset in <paramref name="buffer" /> at which to begin copying bytes to the current stream.</param>
/// <param name="count">The number of bytes to be written to the current stream.</param>
/// <exception cref="T:System.ArgumentException">The sum of <paramref name="offset" /> and <paramref name="count" /> is greater than the buffer length.</exception>
/// <exception cref="T:System.ArgumentNullException">
/// <paramref name="buffer" /> is <see langword="null" />.</exception>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// <paramref name="offset" /> or <paramref name="count" /> is negative.</exception>
/// <exception cref="T:System.IO.IOException">An I/O error occurred, such as the specified file cannot be found.</exception>
/// <exception cref="T:System.NotSupportedException">The stream does not support writing.</exception>
/// <exception cref="T:System.ObjectDisposedException">
/// <see cref="M:System.IO.Stream.Write(System.Byte[],System.Int32,System.Int32)" /> was called after the stream was closed.</exception>
public override void Write(byte[] buffer, int offset, int count)
{
DeflateStream.Write(buffer, offset, count);
adler32.Update(buffer, offset, count);
}
/// <summary>Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written.</summary>
/// <param name="buffer">A region of memory. This method copies the contents of this region to the current stream.</param>
public override void Write(ReadOnlySpan<byte> buffer)
{
DeflateStream.Write(buffer);
adler32.Update(buffer);
}
/// <summary>Asynchronously writes a sequence of bytes to the current stream, advances the current position within this stream by the number of bytes written, and monitors cancellation requests.</summary>
/// <param name="buffer">The buffer to write data from.</param>
/// <param name="offset">The zero-based byte offset in <paramref name="buffer" /> from which to begin copying bytes to the stream.</param>
/// <param name="count">The maximum number of bytes to write.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="P:System.Threading.CancellationToken.None" />.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
/// <exception cref="T:System.ArgumentNullException">
/// <paramref name="buffer" /> is <see langword="null" />.</exception>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// <paramref name="offset" /> or <paramref name="count" /> is negative.</exception>
/// <exception cref="T:System.ArgumentException">The sum of <paramref name="offset" /> and <paramref name="count" /> is larger than the buffer length.</exception>
/// <exception cref="T:System.NotSupportedException">The stream does not support writing.</exception>
/// <exception cref="T:System.ObjectDisposedException">The stream has been disposed.</exception>
/// <exception cref="T:System.InvalidOperationException">The stream is currently in use by a previous write operation.</exception>
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
await DeflateStream.WriteAsync(buffer, offset, count, cancellationToken);
adler32.Update(new ReadOnlySpan<byte>(buffer, offset, count));
}
/// <summary>Asynchronously writes a sequence of bytes to the current stream, advances the current position within this stream by the number of bytes written, and monitors cancellation requests.</summary>
/// <param name="buffer">The region of memory to write data from.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="P:System.Threading.CancellationToken.None" />.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
{
await DeflateStream.WriteAsync(buffer, cancellationToken);
adler32.Update(buffer.Span);
}
/// <summary>
/// Closes the current stream and releases any resources (such as sockets and file handles) associated with the current stream.
/// Instead of calling this method, ensure that the stream is properly disposed.
/// </summary>
public override void Close()
{
if (isClosed)
return;
isClosed = true;
if (compressionMode == CompressionMode.Compress)
{
Flush();
DeflateStream.Close();
checksum = BitConverter.GetBytes(adler32.Value);
if (BitConverter.IsLittleEndian)
Array.Reverse(checksum);
BaseStream.Write(checksum, 0, checksum.Length);
}
else
{
DeflateStream.Close();
if (checksum == null)
ReadCrc();
}
if (!leaveOpen)
BaseStream.Close();
}
/// <summary>Clears all buffers for this stream and causes any buffered data to be written to the underlying device.</summary>
/// <exception cref="T:System.IO.IOException">An I/O error occurs.</exception>
public override void Flush()
{
DeflateStream?.Flush();
BaseStream?.Flush();
}
/// <summary>Sets the position within the current stream.</summary>
/// <param name="offset">A byte offset relative to the <paramref name="origin" /> parameter.</param>
/// <param name="origin">A value of type <see cref="T:System.IO.SeekOrigin" /> indicating the reference point used to obtain the new position.</param>
/// <returns>The new position within the current stream.</returns>
/// <exception cref="T:System.IO.IOException">An I/O error occurs.</exception>
/// <exception cref="T:System.NotSupportedException">The stream does not support seeking, such as if the stream is constructed from a pipe or console output.</exception>
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed.</exception>
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
/// <summary>Sets the length of the current stream.</summary>
/// <param name="value">The desired length of the current stream in bytes.</param>
/// <exception cref="T:System.IO.IOException">An I/O error occurs.</exception>
/// <exception cref="T:System.NotSupportedException">The stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output.</exception>
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed.</exception>
public override void SetLength(long value) => throw new NotSupportedException();
/// <summary>
/// Checks if the given <paramref name="stream"/> is in ZLib format.
/// </summary>
/// <param name="stream">A <see cref="Stream"/> instance to query.</param>
/// <returns><see langword="true"/> is <paramref name="stream"/> is in a supported ZLib format, otherwise <see langword="false"/> if not or an error occured.</returns>
public static bool IsSupported(Stream stream)
{
int cmf;
int flag;
if (!stream.CanRead)
return false;
if (stream.Position != 0)
{
var pos = stream.Position;
stream.Seek(0, SeekOrigin.Begin);
cmf = stream.ReadByte();
flag = stream.ReadByte();
stream.Seek(pos, SeekOrigin.Begin);
}
else
{
cmf = stream.ReadByte();
flag = stream.ReadByte();
}
try
{
var header = ZLibHeader.Decode(cmf, flag);
return header.IsSupported;
}
catch
{
return false;
}
}
/// <summary>
/// Reads the last 4 bytes of the stream where the CRC is stored.
/// </summary>
/// <exception cref="EndOfStreamException">Thrown when the stream is cannot seek to the checksum location to read.</exception>
/// <exception cref="InvalidDataException">Thrown when the checksum comparison does not match.</exception>
private void ReadCrc()
{
checksum = new byte[4];
BaseStream.Seek(-4, SeekOrigin.End);
if (BaseStream.Read(checksum, 0, 4) < 4)
throw new EndOfStreamException();
if (BitConverter.IsLittleEndian)
Array.Reverse(checksum);
var crcAdler = adler32.Value;
var crcStream = BitConverter.ToInt32(checksum, 0);
if (crcStream != crcAdler)
throw new InvalidDataException("CRC validation failed.");
}
/// <summary>
/// Initializes the underlying <see cref="System.IO.Compression.DeflateStream"/> instance.
/// </summary>
private DeflateStream CreateStream(CompressionLevel compressionLevel)
{
switch (compressionMode)
{
case CompressionMode.Compress:
{
WriteHeader(compressionLevel);
return new DeflateStream(BaseStream, compressionLevel, true);
}
case CompressionMode.Decompress:
{
if (!IsSupported(BaseStream))
throw new InvalidDataException();
return new DeflateStream(BaseStream, CompressionMode.Decompress, true);
}
default:
throw new ArgumentOutOfRangeException(nameof(compressionMode));
}
}
/// <summary>
/// Writes the ZLib header to the stream.
/// </summary>
/// <param name="compressionLevel">The compression level being used.</param>
protected void WriteHeader(CompressionLevel compressionLevel)
{
var header = new ZLibHeader(compressionLevel);
var magicNumber = header.Encode();
BaseStream.WriteByte(magicNumber[0]);
BaseStream.WriteByte(magicNumber[1]);
}
}
}