mirror of https://github.com/SixLabors/ImageSharp
9 changed files with 36 additions and 983 deletions
@ -1,585 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Buffers; |
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.ImageSharp.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.IO; |
|||
|
|||
/// <summary>
|
|||
/// Provides an in-memory stream composed of non-contiguous chunks that doesn't need to be resized.
|
|||
/// Chunks are allocated by the <see cref="MemoryAllocator"/> assigned via the constructor
|
|||
/// and is designed to take advantage of buffer pooling when available.
|
|||
/// </summary>
|
|||
internal sealed class ChunkedMemoryStream : Stream |
|||
{ |
|||
// The memory allocator.
|
|||
private readonly MemoryAllocator allocator; |
|||
|
|||
// Data
|
|||
private MemoryChunk? memoryChunk; |
|||
|
|||
// The total number of allocated chunks
|
|||
private int chunkCount; |
|||
|
|||
// The length of the largest contiguous buffer that can be handled by the allocator.
|
|||
private readonly int allocatorCapacity; |
|||
|
|||
// Has the stream been disposed.
|
|||
private bool isDisposed; |
|||
|
|||
// Current chunk to write to
|
|||
private MemoryChunk? writeChunk; |
|||
|
|||
// Offset into chunk to write to
|
|||
private int writeOffset; |
|||
|
|||
// Current chunk to read from
|
|||
private MemoryChunk? readChunk; |
|||
|
|||
// Offset into chunk to read from
|
|||
private int readOffset; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ChunkedMemoryStream"/> class.
|
|||
/// </summary>
|
|||
/// <param name="allocator">The memory allocator.</param>
|
|||
public ChunkedMemoryStream(MemoryAllocator allocator) |
|||
{ |
|||
this.allocatorCapacity = allocator.GetBufferCapacityInBytes(); |
|||
this.allocator = allocator; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanRead => !this.isDisposed; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanSeek => !this.isDisposed; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanWrite => !this.isDisposed; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Length |
|||
{ |
|||
get |
|||
{ |
|||
this.EnsureNotDisposed(); |
|||
|
|||
int length = 0; |
|||
MemoryChunk? chunk = this.memoryChunk; |
|||
while (chunk != null) |
|||
{ |
|||
MemoryChunk? next = chunk.Next; |
|||
if (next != null) |
|||
{ |
|||
length += chunk.Length; |
|||
} |
|||
else |
|||
{ |
|||
length += this.writeOffset; |
|||
} |
|||
|
|||
chunk = next; |
|||
} |
|||
|
|||
return length; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Position |
|||
{ |
|||
get |
|||
{ |
|||
this.EnsureNotDisposed(); |
|||
|
|||
if (this.readChunk is null) |
|||
{ |
|||
return 0; |
|||
} |
|||
|
|||
int pos = 0; |
|||
MemoryChunk? chunk = this.memoryChunk; |
|||
while (chunk != this.readChunk && chunk is not null) |
|||
{ |
|||
pos += chunk.Length; |
|||
chunk = chunk.Next; |
|||
} |
|||
|
|||
pos += this.readOffset; |
|||
|
|||
return pos; |
|||
} |
|||
|
|||
set |
|||
{ |
|||
this.EnsureNotDisposed(); |
|||
|
|||
if (value < 0) |
|||
{ |
|||
ThrowArgumentOutOfRange(nameof(value)); |
|||
} |
|||
|
|||
// Back up current position in case new position is out of range
|
|||
MemoryChunk? backupReadChunk = this.readChunk; |
|||
int backupReadOffset = this.readOffset; |
|||
|
|||
this.readChunk = null; |
|||
this.readOffset = 0; |
|||
|
|||
int leftUntilAtPos = (int)value; |
|||
MemoryChunk? chunk = this.memoryChunk; |
|||
while (chunk != null) |
|||
{ |
|||
if ((leftUntilAtPos < chunk.Length) |
|||
|| ((leftUntilAtPos == chunk.Length) |
|||
&& (chunk.Next is null))) |
|||
{ |
|||
// The desired position is in this chunk
|
|||
this.readChunk = chunk; |
|||
this.readOffset = leftUntilAtPos; |
|||
break; |
|||
} |
|||
|
|||
leftUntilAtPos -= chunk.Length; |
|||
chunk = chunk.Next; |
|||
} |
|||
|
|||
if (this.readChunk is null) |
|||
{ |
|||
// Position is out of range
|
|||
this.readChunk = backupReadChunk; |
|||
this.readOffset = backupReadOffset; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public override long Seek(long offset, SeekOrigin origin) |
|||
{ |
|||
this.EnsureNotDisposed(); |
|||
|
|||
switch (origin) |
|||
{ |
|||
case SeekOrigin.Begin: |
|||
this.Position = offset; |
|||
break; |
|||
|
|||
case SeekOrigin.Current: |
|||
this.Position += offset; |
|||
break; |
|||
|
|||
case SeekOrigin.End: |
|||
this.Position = this.Length + offset; |
|||
break; |
|||
default: |
|||
ThrowInvalidSeek(); |
|||
break; |
|||
} |
|||
|
|||
return this.Position; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void SetLength(long value) |
|||
=> throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void Dispose(bool disposing) |
|||
{ |
|||
if (this.isDisposed) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
this.isDisposed = true; |
|||
if (disposing) |
|||
{ |
|||
ReleaseMemoryChunks(this.memoryChunk); |
|||
} |
|||
|
|||
this.memoryChunk = null; |
|||
this.writeChunk = null; |
|||
this.readChunk = null; |
|||
this.chunkCount = 0; |
|||
} |
|||
finally |
|||
{ |
|||
base.Dispose(disposing); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void Flush() |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public override int Read(byte[] buffer, int offset, int count) |
|||
{ |
|||
Guard.NotNull(buffer, nameof(buffer)); |
|||
Guard.MustBeGreaterThanOrEqualTo(offset, 0, nameof(offset)); |
|||
Guard.MustBeGreaterThanOrEqualTo(count, 0, nameof(count)); |
|||
|
|||
const string bufferMessage = "Offset subtracted from the buffer length is less than count."; |
|||
Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage); |
|||
|
|||
return this.ReadImpl(buffer.AsSpan(offset, count)); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public override int Read(Span<byte> buffer) => this.ReadImpl(buffer); |
|||
|
|||
private int ReadImpl(Span<byte> buffer) |
|||
{ |
|||
this.EnsureNotDisposed(); |
|||
|
|||
if (this.readChunk is null) |
|||
{ |
|||
if (this.memoryChunk is null) |
|||
{ |
|||
return 0; |
|||
} |
|||
|
|||
this.readChunk = this.memoryChunk; |
|||
this.readOffset = 0; |
|||
} |
|||
|
|||
IMemoryOwner<byte> chunkBuffer = this.readChunk.Buffer; |
|||
int chunkSize = this.readChunk.Length; |
|||
if (this.readChunk.Next is null) |
|||
{ |
|||
chunkSize = this.writeOffset; |
|||
} |
|||
|
|||
int bytesRead = 0; |
|||
int offset = 0; |
|||
int count = buffer.Length; |
|||
while (count > 0) |
|||
{ |
|||
if (this.readOffset == chunkSize) |
|||
{ |
|||
// Exit if no more chunks are currently available
|
|||
if (this.readChunk.Next is null) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
this.readChunk = this.readChunk.Next; |
|||
this.readOffset = 0; |
|||
chunkBuffer = this.readChunk.Buffer; |
|||
chunkSize = this.readChunk.Length; |
|||
if (this.readChunk.Next is null) |
|||
{ |
|||
chunkSize = this.writeOffset; |
|||
} |
|||
} |
|||
|
|||
int readCount = Math.Min(count, chunkSize - this.readOffset); |
|||
chunkBuffer.Slice(this.readOffset, readCount).CopyTo(buffer[offset..]); |
|||
offset += readCount; |
|||
count -= readCount; |
|||
this.readOffset += readCount; |
|||
bytesRead += readCount; |
|||
} |
|||
|
|||
return bytesRead; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public override int ReadByte() |
|||
{ |
|||
this.EnsureNotDisposed(); |
|||
|
|||
if (this.readChunk is null) |
|||
{ |
|||
if (this.memoryChunk is null) |
|||
{ |
|||
return 0; |
|||
} |
|||
|
|||
this.readChunk = this.memoryChunk; |
|||
this.readOffset = 0; |
|||
} |
|||
|
|||
IMemoryOwner<byte> chunkBuffer = this.readChunk.Buffer; |
|||
int chunkSize = this.readChunk.Length; |
|||
if (this.readChunk.Next is null) |
|||
{ |
|||
chunkSize = this.writeOffset; |
|||
} |
|||
|
|||
if (this.readOffset == chunkSize) |
|||
{ |
|||
// Exit if no more chunks are currently available
|
|||
if (this.readChunk.Next is null) |
|||
{ |
|||
return -1; |
|||
} |
|||
|
|||
this.readChunk = this.readChunk.Next; |
|||
this.readOffset = 0; |
|||
chunkBuffer = this.readChunk.Buffer; |
|||
} |
|||
|
|||
return chunkBuffer.GetSpan()[this.readOffset++]; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public override void Write(byte[] buffer, int offset, int count) |
|||
{ |
|||
Guard.NotNull(buffer, nameof(buffer)); |
|||
Guard.MustBeGreaterThanOrEqualTo(offset, 0, nameof(offset)); |
|||
Guard.MustBeGreaterThanOrEqualTo(count, 0, nameof(count)); |
|||
|
|||
const string bufferMessage = "Offset subtracted from the buffer length is less than count."; |
|||
Guard.IsFalse(buffer.Length - offset < count, nameof(buffer), bufferMessage); |
|||
|
|||
this.WriteImpl(buffer.AsSpan(offset, count)); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public override void Write(ReadOnlySpan<byte> buffer) => this.WriteImpl(buffer); |
|||
|
|||
private void WriteImpl(ReadOnlySpan<byte> buffer) |
|||
{ |
|||
this.EnsureNotDisposed(); |
|||
|
|||
if (this.memoryChunk is null) |
|||
{ |
|||
this.memoryChunk = this.AllocateMemoryChunk(); |
|||
this.writeChunk = this.memoryChunk; |
|||
this.writeOffset = 0; |
|||
} |
|||
|
|||
Guard.NotNull(this.writeChunk); |
|||
|
|||
Span<byte> chunkBuffer = this.writeChunk.Buffer.GetSpan(); |
|||
int chunkSize = this.writeChunk.Length; |
|||
int count = buffer.Length; |
|||
int offset = 0; |
|||
while (count > 0) |
|||
{ |
|||
if (this.writeOffset == chunkSize) |
|||
{ |
|||
// Allocate a new chunk if the current one is full
|
|||
this.writeChunk.Next = this.AllocateMemoryChunk(); |
|||
this.writeChunk = this.writeChunk.Next; |
|||
this.writeOffset = 0; |
|||
chunkBuffer = this.writeChunk.Buffer.GetSpan(); |
|||
chunkSize = this.writeChunk.Length; |
|||
} |
|||
|
|||
int copyCount = Math.Min(count, chunkSize - this.writeOffset); |
|||
buffer.Slice(offset, copyCount).CopyTo(chunkBuffer[this.writeOffset..]); |
|||
|
|||
offset += copyCount; |
|||
count -= copyCount; |
|||
this.writeOffset += copyCount; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void WriteByte(byte value) |
|||
{ |
|||
this.EnsureNotDisposed(); |
|||
|
|||
if (this.memoryChunk is null) |
|||
{ |
|||
this.memoryChunk = this.AllocateMemoryChunk(); |
|||
this.writeChunk = this.memoryChunk; |
|||
this.writeOffset = 0; |
|||
} |
|||
|
|||
Guard.NotNull(this.writeChunk); |
|||
|
|||
IMemoryOwner<byte> chunkBuffer = this.writeChunk.Buffer; |
|||
int chunkSize = this.writeChunk.Length; |
|||
|
|||
if (this.writeOffset == chunkSize) |
|||
{ |
|||
// Allocate a new chunk if the current one is full
|
|||
this.writeChunk.Next = this.AllocateMemoryChunk(); |
|||
this.writeChunk = this.writeChunk.Next; |
|||
this.writeOffset = 0; |
|||
chunkBuffer = this.writeChunk.Buffer; |
|||
} |
|||
|
|||
chunkBuffer.GetSpan()[this.writeOffset++] = value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Copy entire buffer into an array.
|
|||
/// </summary>
|
|||
/// <returns>The <see cref="T:byte[]"/>.</returns>
|
|||
public byte[] ToArray() |
|||
{ |
|||
int length = (int)this.Length; // This will throw if stream is closed
|
|||
byte[] copy = new byte[this.Length]; |
|||
|
|||
MemoryChunk? backupReadChunk = this.readChunk; |
|||
int backupReadOffset = this.readOffset; |
|||
|
|||
this.readChunk = this.memoryChunk; |
|||
this.readOffset = 0; |
|||
this.Read(copy, 0, length); |
|||
|
|||
this.readChunk = backupReadChunk; |
|||
this.readOffset = backupReadOffset; |
|||
|
|||
return copy; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Write remainder of this stream to another stream.
|
|||
/// </summary>
|
|||
/// <param name="stream">The stream to write to.</param>
|
|||
public void WriteTo(Stream stream) |
|||
{ |
|||
this.EnsureNotDisposed(); |
|||
|
|||
Guard.NotNull(stream, nameof(stream)); |
|||
|
|||
if (this.readChunk is null) |
|||
{ |
|||
if (this.memoryChunk is null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
this.readChunk = this.memoryChunk; |
|||
this.readOffset = 0; |
|||
} |
|||
|
|||
IMemoryOwner<byte> chunkBuffer = this.readChunk.Buffer; |
|||
int chunkSize = this.readChunk.Length; |
|||
if (this.readChunk.Next is null) |
|||
{ |
|||
chunkSize = this.writeOffset; |
|||
} |
|||
|
|||
// Following code mirrors Read() logic (readChunk/readOffset should
|
|||
// point just past last byte of last chunk when done)
|
|||
// loop until end of chunks is found
|
|||
while (true) |
|||
{ |
|||
if (this.readOffset == chunkSize) |
|||
{ |
|||
// Exit if no more chunks are currently available
|
|||
if (this.readChunk.Next is null) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
this.readChunk = this.readChunk.Next; |
|||
this.readOffset = 0; |
|||
chunkBuffer = this.readChunk.Buffer; |
|||
chunkSize = this.readChunk.Length; |
|||
if (this.readChunk.Next is null) |
|||
{ |
|||
chunkSize = this.writeOffset; |
|||
} |
|||
} |
|||
|
|||
int writeCount = chunkSize - this.readOffset; |
|||
stream.Write(chunkBuffer.GetSpan(), this.readOffset, writeCount); |
|||
this.readOffset = chunkSize; |
|||
} |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private void EnsureNotDisposed() |
|||
{ |
|||
if (this.isDisposed) |
|||
{ |
|||
ThrowDisposed(); |
|||
} |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.NoInlining)] |
|||
private static void ThrowDisposed() => throw new ObjectDisposedException(null, "The stream is closed."); |
|||
|
|||
[MethodImpl(MethodImplOptions.NoInlining)] |
|||
private static void ThrowArgumentOutOfRange(string value) => throw new ArgumentOutOfRangeException(value); |
|||
|
|||
[MethodImpl(MethodImplOptions.NoInlining)] |
|||
private static void ThrowInvalidSeek() => throw new ArgumentException("Invalid seek origin."); |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private MemoryChunk AllocateMemoryChunk() |
|||
{ |
|||
// Tweak our buffer sizes to take the minimum of the provided buffer sizes
|
|||
// or the allocator buffer capacity which provides us with the largest
|
|||
// available contiguous buffer size.
|
|||
IMemoryOwner<byte> buffer = this.allocator.Allocate<byte>(Math.Min(this.allocatorCapacity, GetChunkSize(this.chunkCount++))); |
|||
|
|||
return new MemoryChunk(buffer) |
|||
{ |
|||
Next = null, |
|||
Length = buffer.Length() |
|||
}; |
|||
} |
|||
|
|||
private static void ReleaseMemoryChunks(MemoryChunk? chunk) |
|||
{ |
|||
while (chunk != null) |
|||
{ |
|||
chunk.Dispose(); |
|||
chunk = chunk.Next; |
|||
} |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private static int GetChunkSize(int i) |
|||
{ |
|||
// Increment chunks sizes with moderate speed, but without using too many buffers from the same ArrayPool bucket of the default MemoryAllocator.
|
|||
// https://github.com/SixLabors/ImageSharp/pull/2006#issuecomment-1066244720
|
|||
#pragma warning disable IDE1006 // Naming Styles
|
|||
const int _128K = 1 << 17; |
|||
const int _4M = 1 << 22; |
|||
return i < 16 ? _128K * (1 << (int)((uint)i / 4)) : _4M; |
|||
#pragma warning restore IDE1006 // Naming Styles
|
|||
} |
|||
|
|||
private sealed class MemoryChunk : IDisposable |
|||
{ |
|||
private bool isDisposed; |
|||
|
|||
public MemoryChunk(IMemoryOwner<byte> buffer) => this.Buffer = buffer; |
|||
|
|||
public IMemoryOwner<byte> Buffer { get; } |
|||
|
|||
public MemoryChunk? Next { get; set; } |
|||
|
|||
public int Length { get; init; } |
|||
|
|||
private void Dispose(bool disposing) |
|||
{ |
|||
if (!this.isDisposed) |
|||
{ |
|||
if (disposing) |
|||
{ |
|||
this.Buffer.Dispose(); |
|||
} |
|||
|
|||
this.isDisposed = true; |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
this.Dispose(disposing: true); |
|||
GC.SuppressFinalize(this); |
|||
} |
|||
} |
|||
} |
|||
@ -1,373 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.IO; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.IO; |
|||
|
|||
/// <summary>
|
|||
/// Tests for the <see cref="ChunkedMemoryStream"/> class.
|
|||
/// </summary>
|
|||
public class ChunkedMemoryStreamTests |
|||
{ |
|||
/// <summary>
|
|||
/// The default length in bytes of each buffer chunk when allocating large buffers.
|
|||
/// </summary>
|
|||
private const int DefaultLargeChunkSize = 1024 * 1024 * 4; // 4 Mb
|
|||
|
|||
/// <summary>
|
|||
/// The default length in bytes of each buffer chunk when allocating small buffers.
|
|||
/// </summary>
|
|||
private const int DefaultSmallChunkSize = DefaultLargeChunkSize / 32; // 128 Kb
|
|||
|
|||
private readonly MemoryAllocator allocator; |
|||
|
|||
public ChunkedMemoryStreamTests() => this.allocator = Configuration.Default.MemoryAllocator; |
|||
|
|||
[Fact] |
|||
public void MemoryStream_GetPositionTest_Negative() |
|||
{ |
|||
using var ms = new ChunkedMemoryStream(this.allocator); |
|||
long iCurrentPos = ms.Position; |
|||
for (int i = -1; i > -6; i--) |
|||
{ |
|||
Assert.Throws<ArgumentOutOfRangeException>(() => ms.Position = i); |
|||
Assert.Equal(ms.Position, iCurrentPos); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void MemoryStream_ReadTest_Negative() |
|||
{ |
|||
var ms2 = new ChunkedMemoryStream(this.allocator); |
|||
|
|||
Assert.Throws<ArgumentNullException>(() => ms2.Read(null, 0, 0)); |
|||
Assert.Throws<ArgumentOutOfRangeException>(() => ms2.Read(new byte[] { 1 }, -1, 0)); |
|||
Assert.Throws<ArgumentOutOfRangeException>(() => ms2.Read(new byte[] { 1 }, 0, -1)); |
|||
Assert.Throws<ArgumentException>(() => ms2.Read(new byte[] { 1 }, 2, 0)); |
|||
Assert.Throws<ArgumentException>(() => ms2.Read(new byte[] { 1 }, 0, 2)); |
|||
|
|||
ms2.Dispose(); |
|||
|
|||
Assert.Throws<ObjectDisposedException>(() => ms2.Read(new byte[] { 1 }, 0, 1)); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(DefaultSmallChunkSize)] |
|||
[InlineData((int)(DefaultSmallChunkSize * 1.5))] |
|||
[InlineData(DefaultSmallChunkSize * 4)] |
|||
[InlineData((int)(DefaultSmallChunkSize * 5.5))] |
|||
[InlineData(DefaultSmallChunkSize * 16)] |
|||
public void MemoryStream_ReadByteTest(int length) |
|||
{ |
|||
using MemoryStream ms = this.CreateTestStream(length); |
|||
using var cms = new ChunkedMemoryStream(this.allocator); |
|||
|
|||
ms.CopyTo(cms); |
|||
cms.Position = 0; |
|||
byte[] expected = ms.ToArray(); |
|||
|
|||
for (int i = 0; i < expected.Length; i++) |
|||
{ |
|||
Assert.Equal(expected[i], cms.ReadByte()); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(DefaultSmallChunkSize)] |
|||
[InlineData((int)(DefaultSmallChunkSize * 1.5))] |
|||
[InlineData(DefaultSmallChunkSize * 4)] |
|||
[InlineData((int)(DefaultSmallChunkSize * 5.5))] |
|||
[InlineData(DefaultSmallChunkSize * 16)] |
|||
public void MemoryStream_ReadByteBufferTest(int length) |
|||
{ |
|||
using MemoryStream ms = this.CreateTestStream(length); |
|||
using var cms = new ChunkedMemoryStream(this.allocator); |
|||
|
|||
ms.CopyTo(cms); |
|||
cms.Position = 0; |
|||
byte[] expected = ms.ToArray(); |
|||
byte[] buffer = new byte[2]; |
|||
for (int i = 0; i < expected.Length; i += 2) |
|||
{ |
|||
cms.Read(buffer); |
|||
Assert.Equal(expected[i], buffer[0]); |
|||
Assert.Equal(expected[i + 1], buffer[1]); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(DefaultSmallChunkSize)] |
|||
[InlineData((int)(DefaultSmallChunkSize * 1.5))] |
|||
[InlineData(DefaultSmallChunkSize * 4)] |
|||
[InlineData((int)(DefaultSmallChunkSize * 5.5))] |
|||
[InlineData(DefaultSmallChunkSize * 16)] |
|||
public void MemoryStream_ReadByteBufferSpanTest(int length) |
|||
{ |
|||
using MemoryStream ms = this.CreateTestStream(length); |
|||
using var cms = new ChunkedMemoryStream(this.allocator); |
|||
|
|||
ms.CopyTo(cms); |
|||
cms.Position = 0; |
|||
byte[] expected = ms.ToArray(); |
|||
Span<byte> buffer = new byte[2]; |
|||
for (int i = 0; i < expected.Length; i += 2) |
|||
{ |
|||
cms.Read(buffer); |
|||
Assert.Equal(expected[i], buffer[0]); |
|||
Assert.Equal(expected[i + 1], buffer[1]); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void MemoryStream_WriteToTests() |
|||
{ |
|||
using (var ms2 = new ChunkedMemoryStream(this.allocator)) |
|||
{ |
|||
byte[] bytArrRet; |
|||
byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; |
|||
|
|||
// [] Write to memoryStream, check the memoryStream
|
|||
ms2.Write(bytArr, 0, bytArr.Length); |
|||
|
|||
using var readonlyStream = new ChunkedMemoryStream(this.allocator); |
|||
ms2.WriteTo(readonlyStream); |
|||
readonlyStream.Flush(); |
|||
readonlyStream.Position = 0; |
|||
bytArrRet = new byte[(int)readonlyStream.Length]; |
|||
readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); |
|||
for (int i = 0; i < bytArr.Length; i++) |
|||
{ |
|||
Assert.Equal(bytArr[i], bytArrRet[i]); |
|||
} |
|||
} |
|||
|
|||
// [] Write to memoryStream, check the memoryStream
|
|||
using (var ms2 = new ChunkedMemoryStream(this.allocator)) |
|||
using (var ms3 = new ChunkedMemoryStream(this.allocator)) |
|||
{ |
|||
byte[] bytArrRet; |
|||
byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; |
|||
|
|||
ms2.Write(bytArr, 0, bytArr.Length); |
|||
ms2.WriteTo(ms3); |
|||
ms3.Position = 0; |
|||
bytArrRet = new byte[(int)ms3.Length]; |
|||
ms3.Read(bytArrRet, 0, (int)ms3.Length); |
|||
for (int i = 0; i < bytArr.Length; i++) |
|||
{ |
|||
Assert.Equal(bytArr[i], bytArrRet[i]); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void MemoryStream_WriteToSpanTests() |
|||
{ |
|||
using (var ms2 = new ChunkedMemoryStream(this.allocator)) |
|||
{ |
|||
Span<byte> bytArrRet; |
|||
Span<byte> bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; |
|||
|
|||
// [] Write to memoryStream, check the memoryStream
|
|||
ms2.Write(bytArr, 0, bytArr.Length); |
|||
|
|||
using var readonlyStream = new ChunkedMemoryStream(this.allocator); |
|||
ms2.WriteTo(readonlyStream); |
|||
readonlyStream.Flush(); |
|||
readonlyStream.Position = 0; |
|||
bytArrRet = new byte[(int)readonlyStream.Length]; |
|||
readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); |
|||
for (int i = 0; i < bytArr.Length; i++) |
|||
{ |
|||
Assert.Equal(bytArr[i], bytArrRet[i]); |
|||
} |
|||
} |
|||
|
|||
// [] Write to memoryStream, check the memoryStream
|
|||
using (var ms2 = new ChunkedMemoryStream(this.allocator)) |
|||
using (var ms3 = new ChunkedMemoryStream(this.allocator)) |
|||
{ |
|||
Span<byte> bytArrRet; |
|||
Span<byte> bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; |
|||
|
|||
ms2.Write(bytArr, 0, bytArr.Length); |
|||
ms2.WriteTo(ms3); |
|||
ms3.Position = 0; |
|||
bytArrRet = new byte[(int)ms3.Length]; |
|||
ms3.Read(bytArrRet, 0, (int)ms3.Length); |
|||
for (int i = 0; i < bytArr.Length; i++) |
|||
{ |
|||
Assert.Equal(bytArr[i], bytArrRet[i]); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void MemoryStream_WriteByteTests() |
|||
{ |
|||
using (var ms2 = new ChunkedMemoryStream(this.allocator)) |
|||
{ |
|||
byte[] bytArrRet; |
|||
byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 }; |
|||
|
|||
for (int i = 0; i < bytArr.Length; i++) |
|||
{ |
|||
ms2.WriteByte(bytArr[i]); |
|||
} |
|||
|
|||
using var readonlyStream = new ChunkedMemoryStream(this.allocator); |
|||
ms2.WriteTo(readonlyStream); |
|||
readonlyStream.Flush(); |
|||
readonlyStream.Position = 0; |
|||
bytArrRet = new byte[(int)readonlyStream.Length]; |
|||
readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); |
|||
for (int i = 0; i < bytArr.Length; i++) |
|||
{ |
|||
Assert.Equal(bytArr[i], bytArrRet[i]); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void MemoryStream_WriteToTests_Negative() |
|||
{ |
|||
using var ms2 = new ChunkedMemoryStream(this.allocator); |
|||
Assert.Throws<ArgumentNullException>(() => ms2.WriteTo(null)); |
|||
|
|||
ms2.Write(new byte[] { 1 }, 0, 1); |
|||
var readonlyStream = new MemoryStream(new byte[1028], false); |
|||
Assert.Throws<NotSupportedException>(() => ms2.WriteTo(readonlyStream)); |
|||
|
|||
readonlyStream.Dispose(); |
|||
|
|||
// [] Pass in a closed stream
|
|||
Assert.Throws<ObjectDisposedException>(() => ms2.WriteTo(readonlyStream)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void MemoryStream_CopyTo_Invalid() |
|||
{ |
|||
ChunkedMemoryStream memoryStream; |
|||
const string bufferSize = nameof(bufferSize); |
|||
using (memoryStream = new ChunkedMemoryStream(this.allocator)) |
|||
{ |
|||
const string destination = nameof(destination); |
|||
Assert.Throws<ArgumentNullException>(destination, () => memoryStream.CopyTo(destination: null)); |
|||
|
|||
// Validate the destination parameter first.
|
|||
Assert.Throws<ArgumentNullException>(destination, () => memoryStream.CopyTo(destination: null, bufferSize: 0)); |
|||
Assert.Throws<ArgumentNullException>(destination, () => memoryStream.CopyTo(destination: null, bufferSize: -1)); |
|||
|
|||
// Then bufferSize.
|
|||
Assert.Throws<ArgumentOutOfRangeException>(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: 0)); // 0-length buffer doesn't make sense.
|
|||
Assert.Throws<ArgumentOutOfRangeException>(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: -1)); |
|||
} |
|||
|
|||
// After the Stream is disposed, we should fail on all CopyTos.
|
|||
Assert.Throws<ArgumentOutOfRangeException>(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: 0)); // Not before bufferSize is validated.
|
|||
Assert.Throws<ArgumentOutOfRangeException>(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: -1)); |
|||
|
|||
ChunkedMemoryStream disposedStream = memoryStream; |
|||
|
|||
// We should throw first for the source being disposed...
|
|||
Assert.Throws<ObjectDisposedException>(() => memoryStream.CopyTo(disposedStream, 1)); |
|||
|
|||
// Then for the destination being disposed.
|
|||
memoryStream = new ChunkedMemoryStream(this.allocator); |
|||
Assert.Throws<ObjectDisposedException>(() => memoryStream.CopyTo(disposedStream, 1)); |
|||
memoryStream.Dispose(); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(CopyToData))] |
|||
public void CopyTo(Stream source, byte[] expected) |
|||
{ |
|||
using var destination = new ChunkedMemoryStream(this.allocator); |
|||
source.CopyTo(destination); |
|||
Assert.InRange(source.Position, source.Length, int.MaxValue); // Copying the data should have read to the end of the stream or stayed past the end.
|
|||
Assert.Equal(expected, destination.ToArray()); |
|||
} |
|||
|
|||
public static IEnumerable<string> GetAllTestImages() |
|||
{ |
|||
IEnumerable<string> allImageFiles = Directory.EnumerateFiles(TestEnvironment.InputImagesDirectoryFullPath, "*.*", SearchOption.AllDirectories) |
|||
.Where(s => !s.EndsWith("txt", StringComparison.OrdinalIgnoreCase)); |
|||
|
|||
var result = new List<string>(); |
|||
foreach (string path in allImageFiles) |
|||
{ |
|||
result.Add(path.Substring(TestEnvironment.InputImagesDirectoryFullPath.Length)); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public static IEnumerable<string> AllTestImages = GetAllTestImages(); |
|||
|
|||
[Theory] |
|||
[WithFileCollection(nameof(AllTestImages), PixelTypes.Rgba32)] |
|||
public void DecoderIntegrationTest<TPixel>(TestImageProvider<TPixel> provider) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
if (!TestEnvironment.Is64BitProcess) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
Image<TPixel> expected; |
|||
try |
|||
{ |
|||
expected = provider.GetImage(); |
|||
} |
|||
catch |
|||
{ |
|||
// The image is invalid
|
|||
return; |
|||
} |
|||
|
|||
string fullPath = Path.Combine( |
|||
TestEnvironment.InputImagesDirectoryFullPath, |
|||
((TestImageProvider<TPixel>.FileProvider)provider).FilePath); |
|||
|
|||
using FileStream fs = File.OpenRead(fullPath); |
|||
using var nonSeekableStream = new NonSeekableStream(fs); |
|||
|
|||
var actual = Image.Load<TPixel>(nonSeekableStream); |
|||
|
|||
ImageComparer.Exact.VerifySimilarity(expected, actual); |
|||
} |
|||
|
|||
public static IEnumerable<object[]> CopyToData() |
|||
{ |
|||
// Stream is positioned @ beginning of data
|
|||
byte[] data1 = new byte[] { 1, 2, 3 }; |
|||
var stream1 = new MemoryStream(data1); |
|||
|
|||
yield return new object[] { stream1, data1 }; |
|||
|
|||
// Stream is positioned in the middle of data
|
|||
byte[] data2 = new byte[] { 0xff, 0xf3, 0xf0 }; |
|||
var stream2 = new MemoryStream(data2) { Position = 1 }; |
|||
|
|||
yield return new object[] { stream2, new byte[] { 0xf3, 0xf0 } }; |
|||
|
|||
// Stream is positioned after end of data
|
|||
byte[] data3 = data2; |
|||
var stream3 = new MemoryStream(data3) { Position = data3.Length + 1 }; |
|||
|
|||
yield return new object[] { stream3, Array.Empty<byte>() }; |
|||
} |
|||
|
|||
private MemoryStream CreateTestStream(int length) |
|||
{ |
|||
byte[] buffer = new byte[length]; |
|||
var random = new Random(); |
|||
random.NextBytes(buffer); |
|||
|
|||
return new MemoryStream(buffer); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue