Skip to content
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
119 changes: 119 additions & 0 deletions src/ImageSharp/Compression/Zlib/ChunkedReadStream.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using SixLabors.ImageSharp.IO;

namespace SixLabors.ImageSharp.Compression.Zlib;

/// <summary>
/// A read-only stream over a sequence of length-delimited segments. Bytes are
/// pulled from the inner stream up to the current segment's remaining length;
/// when the segment is exhausted the supplied delegate is invoked to advance
/// to the next segment and return its length. The inner stream is not owned
/// and is not disposed.
/// </summary>
internal sealed class ChunkedReadStream : Stream
{
private static readonly Func<int> GetDataNoOp = () => 0;

private readonly BufferedReadStream innerStream;
private readonly Func<int> getData;
private int currentDataRemaining;

public ChunkedReadStream(BufferedReadStream innerStream)
: this(innerStream, GetDataNoOp)
{
}

public ChunkedReadStream(BufferedReadStream innerStream, Func<int> getData)
{
this.innerStream = innerStream;
this.getData = getData;
}

/// <inheritdoc/>
public override bool CanRead => this.innerStream.CanRead;

/// <inheritdoc/>
public override bool CanSeek => false;

/// <inheritdoc/>
public override bool CanWrite => throw new NotSupportedException();

/// <inheritdoc/>
public override long Length => throw new NotSupportedException();

/// <inheritdoc/>
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }

/// <summary>
/// Sets the number of bytes available to read from the current segment.
/// Must be called before reading each segment.
/// </summary>
public void SetCurrentSegmentLength(int bytes) => this.currentDataRemaining = bytes;

/// <inheritdoc/>
public override void Flush() => throw new NotSupportedException();

/// <inheritdoc/>
public override int ReadByte()
{
if (this.currentDataRemaining is 0)
{
this.currentDataRemaining = this.getData();
if (this.currentDataRemaining is 0)
{
return -1;
}
}

int value = this.innerStream.ReadByte();
if (value is not -1)
{
this.currentDataRemaining--;
}

return value;
}

/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
{
// Decrement currentDataRemaining only by bytes actually returned by
// innerStream.Read; a short read otherwise underflows the segment
// counter and triggers getData() before the segment is truly drained.
int totalBytesRead = 0;
while (totalBytesRead < count)
{
if (this.currentDataRemaining is 0)
{
this.currentDataRemaining = this.getData();
if (this.currentDataRemaining is 0)
{
break;
}
}

int bytesToRead = Math.Min(count - totalBytesRead, this.currentDataRemaining);
int bytesRead = this.innerStream.Read(buffer, offset + totalBytesRead, bytesToRead);
if (bytesRead is 0)
{
break;
}

this.currentDataRemaining -= bytesRead;
totalBytesRead += bytesRead;
}

return totalBytesRead;
}

/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();

/// <inheritdoc/>
public override void SetLength(long value) => throw new NotSupportedException();

/// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}
125 changes: 125 additions & 0 deletions src/ImageSharp/Compression/Zlib/ZlibInflateReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Diagnostics.CodeAnalysis;
using System.IO.Compression;
using SixLabors.ImageSharp.IO;

namespace SixLabors.ImageSharp.Compression.Zlib;

/// <summary>
/// Reads chunked input, parses the zlib CMF/FLG header, and exposes a
/// <see cref="DeflateStream"/> over the remaining DEFLATE payload. The
/// Adler-32 trailer is not validated.
/// </summary>
internal sealed class ZlibInflateReader : IDisposable
{
/// <summary>
/// Used to read the Adler-32 and Crc-32 checksums.
/// We don't actually use this for anything so it doesn't
/// have to be threadsafe.
/// </summary>
private static readonly byte[] ChecksumBuffer = new byte[4];

private readonly ChunkedReadStream segmentStream;

public ZlibInflateReader(BufferedReadStream innerStream)
=> this.segmentStream = new ChunkedReadStream(innerStream);

public ZlibInflateReader(BufferedReadStream innerStream, Func<int> getData)
=> this.segmentStream = new ChunkedReadStream(innerStream, getData);

/// <summary>
/// Gets the compressed stream over the deframed inner stream.
/// </summary>
public DeflateStream? CompressedStream { get; private set; }

/// <summary>
/// Sets the length of the next segment of compressed input and, on first
/// call, parses the zlib header.
/// </summary>
/// <param name="bytes">The remaining data length for the current segment.</param>
/// <param name="isCriticalChunk">Whether to throw on a malformed zlib header.</param>
/// <returns>The <see cref="bool"/>.</returns>
[MemberNotNullWhen(true, nameof(CompressedStream))]
public bool AllocateNewBytes(int bytes, bool isCriticalChunk)
{
this.segmentStream.SetCurrentSegmentLength(bytes);
if (this.CompressedStream is null)
{
return this.InitializeInflateStream(isCriticalChunk);
}

return true;
}

public void Dispose()
{
this.CompressedStream?.Dispose();
this.segmentStream?.Dispose();
}

[MemberNotNullWhen(true, nameof(CompressedStream))]
private bool InitializeInflateStream(bool isCriticalChunk)
{
// Read the zlib header : http://tools.ietf.org/html/rfc1950
// CMF(Compression Method and flags)
// This byte is divided into a 4 - bit compression method and a
// 4-bit information field depending on the compression method.
// bits 0 to 3 CM Compression method
// bits 4 to 7 CINFO Compression info
//
// 0 1
// +---+---+
// |CMF|FLG|
// +---+---+
int cmf = this.segmentStream.ReadByte();
int flag = this.segmentStream.ReadByte();
if (cmf == -1 || flag == -1)
{
return false;
}

if ((cmf & 0x0F) == 8)
{
// CINFO is the base-2 logarithm of the LZ77 window size, minus eight.
int cinfo = (cmf & 0xF0) >> 4;

if (cinfo > 7)
{
if (isCriticalChunk)
{
// Values of CINFO above 7 are not allowed in RFC1950.
// CINFO is not defined in this specification for CM not equal to 8.
throw new ImageFormatException($"Invalid window size for ZLIB header: cinfo={cinfo}");
}

return false;
}
}
else if (isCriticalChunk)
{
throw new ImageFormatException($"Bad method for ZLIB header: cmf={cmf}");
}
else
{
return false;
}

// The preset dictionary.
bool fdict = (flag & 32) != 0;
if (fdict)
{
// We don't need this for inflate so simply skip by the next four bytes.
// https://tools.ietf.org/html/rfc1950#page-6
if (this.segmentStream.Read(ChecksumBuffer, 0, 4) != 4)
{
return false;
}
}

this.CompressedStream = new DeflateStream(this.segmentStream, CompressionMode.Decompress, leaveOpen: true);

return true;
}
}
Loading
Loading