mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
14 changed files with 1155 additions and 214 deletions
@ -0,0 +1,581 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
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 |
|||
{ |
|||
/// <summary>
|
|||
/// The default length in bytes of each buffer chunk.
|
|||
/// </summary>
|
|||
public const int DefaultBufferLength = 128 * 1024; |
|||
|
|||
// The memory allocator.
|
|||
private readonly MemoryAllocator allocator; |
|||
|
|||
// Data
|
|||
private MemoryChunk memoryChunk; |
|||
|
|||
// The length of each buffer chunk
|
|||
private readonly int chunkLength; |
|||
|
|||
// 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>
|
|||
public ChunkedMemoryStream(MemoryAllocator allocator) |
|||
: this(DefaultBufferLength, allocator) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ChunkedMemoryStream"/> class.
|
|||
/// </summary>
|
|||
/// <param name="bufferLength">The length, in bytes of each buffer chunk.</param>
|
|||
/// <param name="allocator">The memory allocator.</param>
|
|||
public ChunkedMemoryStream(int bufferLength, MemoryAllocator allocator) |
|||
{ |
|||
Guard.MustBeGreaterThan(bufferLength, 0, nameof(bufferLength)); |
|||
Guard.NotNull(allocator, nameof(allocator)); |
|||
|
|||
this.chunkLength = bufferLength; |
|||
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) |
|||
{ |
|||
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; |
|||
ThrowArgumentOutOfRange(nameof(value)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <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; |
|||
} |
|||
|
|||
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) |
|||
{ |
|||
this.ReleaseMemoryChunks(this.memoryChunk); |
|||
} |
|||
|
|||
this.memoryChunk = null; |
|||
this.writeChunk = null; |
|||
this.readChunk = null; |
|||
} |
|||
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().Slice(offset, count)); |
|||
} |
|||
|
|||
#if SUPPORTS_SPAN_STREAM
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public override int Read(Span<byte> buffer) => this.ReadImpl(buffer); |
|||
#endif
|
|||
|
|||
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; |
|||
} |
|||
|
|||
Span<byte> chunkBuffer = this.readChunk.Buffer.GetSpan(); |
|||
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.GetSpan(); |
|||
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.Slice(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; |
|||
} |
|||
|
|||
byte[] chunkBuffer = this.readChunk.Buffer.Array; |
|||
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.Array; |
|||
} |
|||
|
|||
return chunkBuffer[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().Slice(offset, count)); |
|||
} |
|||
|
|||
#if SUPPORTS_SPAN_STREAM
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public override void Write(ReadOnlySpan<byte> buffer) => this.WriteImpl(buffer); |
|||
#endif
|
|||
|
|||
private void WriteImpl(ReadOnlySpan<byte> buffer) |
|||
{ |
|||
this.EnsureNotDisposed(); |
|||
|
|||
if (this.memoryChunk is null) |
|||
{ |
|||
this.memoryChunk = this.AllocateMemoryChunk(); |
|||
this.writeChunk = this.memoryChunk; |
|||
this.writeOffset = 0; |
|||
} |
|||
|
|||
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.Slice(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; |
|||
} |
|||
|
|||
byte[] chunkBuffer = this.writeChunk.Buffer.Array; |
|||
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.Array; |
|||
} |
|||
|
|||
chunkBuffer[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; |
|||
} |
|||
|
|||
byte[] chunkBuffer = this.readChunk.Buffer.Array; |
|||
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.Array; |
|||
chunkSize = this.readChunk.Length; |
|||
if (this.readChunk.Next is null) |
|||
{ |
|||
chunkSize = this.writeOffset; |
|||
} |
|||
} |
|||
|
|||
int writeCount = chunkSize - this.readOffset; |
|||
stream.Write(chunkBuffer, 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.AggressiveInlining)] |
|||
private MemoryChunk AllocateMemoryChunk() |
|||
{ |
|||
IManagedByteBuffer buffer = this.allocator.AllocateManagedByteBuffer(this.chunkLength); |
|||
return new MemoryChunk |
|||
{ |
|||
Buffer = buffer, |
|||
Next = null, |
|||
Length = buffer.Length() |
|||
}; |
|||
} |
|||
|
|||
private void ReleaseMemoryChunks(MemoryChunk chunk) |
|||
{ |
|||
while (chunk != null) |
|||
{ |
|||
chunk.Dispose(); |
|||
chunk = chunk.Next; |
|||
} |
|||
} |
|||
|
|||
private sealed class MemoryChunk : IDisposable |
|||
{ |
|||
private bool isDisposed; |
|||
|
|||
public IManagedByteBuffer Buffer { get; set; } |
|||
|
|||
public MemoryChunk Next { get; set; } |
|||
|
|||
public int Length { get; set; } |
|||
|
|||
private void Dispose(bool disposing) |
|||
{ |
|||
if (!this.isDisposed) |
|||
{ |
|||
if (disposing) |
|||
{ |
|||
this.Buffer.Dispose(); |
|||
} |
|||
|
|||
this.Buffer = null; |
|||
this.isDisposed = true; |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
this.Dispose(disposing: true); |
|||
GC.SuppressFinalize(this); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,69 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Buffers; |
|||
using System.IO; |
|||
using SixLabors.ImageSharp.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.IO |
|||
{ |
|||
/// <summary>
|
|||
/// A memory stream constructed from a pooled buffer of known length.
|
|||
/// </summary>
|
|||
internal sealed class FixedCapacityPooledMemoryStream : MemoryStream |
|||
{ |
|||
private readonly IManagedByteBuffer buffer; |
|||
private bool isDisposed; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="FixedCapacityPooledMemoryStream"/> class.
|
|||
/// </summary>
|
|||
/// <param name="length">The length of the stream buffer to rent.</param>
|
|||
/// <param name="allocator">The allocator to rent the buffer from.</param>
|
|||
public FixedCapacityPooledMemoryStream(long length, MemoryAllocator allocator) |
|||
: this(RentBuffer(length, allocator)) => this.Length = length; |
|||
|
|||
private FixedCapacityPooledMemoryStream(IManagedByteBuffer buffer) |
|||
: base(buffer.Array) => this.buffer = buffer; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Length { get; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool TryGetBuffer(out ArraySegment<byte> buffer) |
|||
{ |
|||
if (this.isDisposed) |
|||
{ |
|||
throw new ObjectDisposedException(this.GetType().Name); |
|||
} |
|||
|
|||
buffer = new ArraySegment<byte>(this.buffer.Array, 0, this.buffer.Length()); |
|||
return true; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void Dispose(bool disposing) |
|||
{ |
|||
if (!this.isDisposed) |
|||
{ |
|||
this.isDisposed = true; |
|||
|
|||
if (disposing) |
|||
{ |
|||
this.buffer.Dispose(); |
|||
} |
|||
|
|||
base.Dispose(disposing); |
|||
} |
|||
} |
|||
|
|||
// In the extrememly unlikely event someone ever gives us a stream
|
|||
// with length longer than int.MaxValue then we'll use something else.
|
|||
private static IManagedByteBuffer RentBuffer(long length, MemoryAllocator allocator) |
|||
{ |
|||
Guard.MustBeBetweenOrEqualTo(length, 0, int.MaxValue, nameof(length)); |
|||
return allocator.AllocateManagedByteBuffer((int)length); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,379 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using SixLabors.ImageSharp.IO; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; |
|||
using Xunit; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.IO |
|||
{ |
|||
/// <summary>
|
|||
/// Tests for the <see cref="ChunkedMemoryStream"/> class.
|
|||
/// </summary>
|
|||
public class ChunkedMemoryStreamTests |
|||
{ |
|||
private readonly MemoryAllocator allocator; |
|||
|
|||
public ChunkedMemoryStreamTests() |
|||
{ |
|||
this.allocator = Configuration.Default.MemoryAllocator; |
|||
} |
|||
|
|||
[Fact] |
|||
public void MemoryStream_Ctor_InvalidCapacities() |
|||
{ |
|||
Assert.Throws<ArgumentOutOfRangeException>(() => new ChunkedMemoryStream(int.MinValue, this.allocator)); |
|||
Assert.Throws<ArgumentOutOfRangeException>(() => new ChunkedMemoryStream(0, this.allocator)); |
|||
} |
|||
|
|||
[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(ChunkedMemoryStream.DefaultBufferLength)] |
|||
[InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 1.5))] |
|||
[InlineData(ChunkedMemoryStream.DefaultBufferLength * 4)] |
|||
[InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 5.5))] |
|||
[InlineData(ChunkedMemoryStream.DefaultBufferLength * 8)] |
|||
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; |
|||
var expected = ms.ToArray(); |
|||
|
|||
for (int i = 0; i < expected.Length; i++) |
|||
{ |
|||
Assert.Equal(expected[i], cms.ReadByte()); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(ChunkedMemoryStream.DefaultBufferLength)] |
|||
[InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 1.5))] |
|||
[InlineData(ChunkedMemoryStream.DefaultBufferLength * 4)] |
|||
[InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 5.5))] |
|||
[InlineData(ChunkedMemoryStream.DefaultBufferLength * 8)] |
|||
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; |
|||
var expected = ms.ToArray(); |
|||
var 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(ChunkedMemoryStream.DefaultBufferLength)] |
|||
[InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 1.5))] |
|||
[InlineData(ChunkedMemoryStream.DefaultBufferLength * 4)] |
|||
[InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 5.5))] |
|||
[InlineData(ChunkedMemoryStream.DefaultBufferLength * 8)] |
|||
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; |
|||
var 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 = "bufferSize"; |
|||
using (memoryStream = new ChunkedMemoryStream(this.allocator)) |
|||
{ |
|||
const string Destination = "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) |
|||
{ |
|||
var buffer = new byte[length]; |
|||
var random = new Random(); |
|||
random.NextBytes(buffer); |
|||
|
|||
return new MemoryStream(buffer); |
|||
} |
|||
} |
|||
} |
|||
@ -1,42 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.Tests.Memory; |
|||
using Xunit; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.IO |
|||
{ |
|||
public class FixedCapacityPooledMemoryStreamTests |
|||
{ |
|||
private readonly TestMemoryAllocator memoryAllocator = new TestMemoryAllocator(); |
|||
|
|||
[Theory] |
|||
[InlineData(1)] |
|||
[InlineData(512)] |
|||
public void RentsManagedBuffer(int length) |
|||
{ |
|||
MemoryStream ms = this.memoryAllocator.AllocateFixedCapacityMemoryStream(length); |
|||
Assert.Equal(length, this.memoryAllocator.AllocationLog.Single().Length); |
|||
ms.Dispose(); |
|||
Assert.Equal(1, this.memoryAllocator.ReturnLog.Count); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(42)] |
|||
[InlineData(2999)] |
|||
public void UsesRentedBuffer(int length) |
|||
{ |
|||
using MemoryStream ms = this.memoryAllocator.AllocateFixedCapacityMemoryStream(length); |
|||
ms.TryGetBuffer(out ArraySegment<byte> buffer); |
|||
byte[] array = buffer.Array; |
|||
Assert.Equal(array.GetHashCode(), this.memoryAllocator.AllocationLog.Single().HashCodeOfBuffer); |
|||
|
|||
ms.Write(new byte[] { 123 }); |
|||
Assert.Equal(123, array[0]); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests |
|||
{ |
|||
internal class NonSeekableStream : Stream |
|||
{ |
|||
private readonly Stream dataStream; |
|||
|
|||
public NonSeekableStream(Stream dataStream) |
|||
=> this.dataStream = dataStream; |
|||
|
|||
public override bool CanRead => this.dataStream.CanRead; |
|||
|
|||
public override bool CanSeek => false; |
|||
|
|||
public override bool CanWrite => false; |
|||
|
|||
public override long Length => throw new NotSupportedException(); |
|||
|
|||
public override long Position |
|||
{ |
|||
get { throw new NotSupportedException(); } |
|||
set { throw new NotSupportedException(); } |
|||
} |
|||
|
|||
public override void Flush() => this.dataStream.Flush(); |
|||
|
|||
public override int Read(byte[] buffer, int offset, int count) => this.dataStream.Read(buffer, offset, count); |
|||
|
|||
public override long Seek(long offset, SeekOrigin origin) |
|||
=> throw new NotSupportedException(); |
|||
|
|||
public override void SetLength(long value) |
|||
=> throw new NotSupportedException(); |
|||
|
|||
public override void Write(byte[] buffer, int offset, int count) |
|||
=> throw new NotImplementedException(); |
|||
} |
|||
} |
|||
@ -1,53 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests |
|||
{ |
|||
internal class NoneSeekableStream : Stream |
|||
{ |
|||
private Stream dataStream; |
|||
|
|||
public NoneSeekableStream(Stream dataStream) |
|||
{ |
|||
this.dataStream = dataStream; |
|||
} |
|||
|
|||
public override bool CanRead => this.dataStream.CanRead; |
|||
|
|||
public override bool CanSeek => false; |
|||
|
|||
public override bool CanWrite => false; |
|||
|
|||
public override long Length => this.dataStream.Length; |
|||
|
|||
public override long Position { get => this.dataStream.Position; set => throw new NotImplementedException(); } |
|||
|
|||
public override void Flush() |
|||
{ |
|||
this.dataStream.Flush(); |
|||
} |
|||
|
|||
public override int Read(byte[] buffer, int offset, int count) |
|||
{ |
|||
return this.dataStream.Read(buffer, offset, count); |
|||
} |
|||
|
|||
public override long Seek(long offset, SeekOrigin origin) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
public override void SetLength(long value) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
public override void Write(byte[] buffer, int offset, int count) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue