mirror of https://github.com/SixLabors/ImageSharp
6 changed files with 786 additions and 57 deletions
@ -0,0 +1,544 @@ |
|||||
|
// 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 = 4096; |
||||
|
|
||||
|
// 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) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException(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; |
||||
|
throw new ArgumentOutOfRangeException(nameof(value)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
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/>
|
||||
|
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)); |
||||
|
if (buffer.Length - offset < count) |
||||
|
{ |
||||
|
throw new ArgumentException($"{offset} subtracted from the buffer length is less than {count}"); |
||||
|
} |
||||
|
|
||||
|
this.EnsureNotDisposed(); |
||||
|
|
||||
|
if (this.readChunk is null) |
||||
|
{ |
||||
|
if (this.memoryChunk is null) |
||||
|
{ |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
this.readChunk = this.memoryChunk; |
||||
|
this.readOffset = 0; |
||||
|
} |
||||
|
|
||||
|
byte[] chunkBuffer = this.writeChunk.Buffer.Array; |
||||
|
int chunkSize = this.readChunk.Length; |
||||
|
if (this.readChunk.Next is null) |
||||
|
{ |
||||
|
chunkSize = this.writeOffset; |
||||
|
} |
||||
|
|
||||
|
int bytesRead = 0; |
||||
|
|
||||
|
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.writeChunk.Buffer.Array; |
||||
|
chunkSize = this.readChunk.Length; |
||||
|
if (this.readChunk.Next is null) |
||||
|
{ |
||||
|
chunkSize = this.writeOffset; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
int readCount = Math.Min(count, chunkSize - this.readOffset); |
||||
|
Buffer.BlockCopy(chunkBuffer, this.readOffset, buffer, offset, readCount); |
||||
|
offset += readCount; |
||||
|
count -= readCount; |
||||
|
this.readOffset += readCount; |
||||
|
bytesRead += readCount; |
||||
|
} |
||||
|
|
||||
|
return bytesRead; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
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.writeChunk.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.writeChunk.Buffer.Array; |
||||
|
} |
||||
|
|
||||
|
return chunkBuffer[this.readOffset++]; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override void Write(byte[] buffer, int offset, int count) |
||||
|
{ |
||||
|
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; |
||||
|
|
||||
|
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.Array; |
||||
|
chunkSize = this.writeChunk.Length; |
||||
|
} |
||||
|
|
||||
|
int copyCount = Math.Min(count, chunkSize - this.writeOffset); |
||||
|
Buffer.BlockCopy(buffer, offset, chunkBuffer, this.writeOffset, copyCount); |
||||
|
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.AggressiveInlining)] |
||||
|
private MemoryChunk AllocateMemoryChunk() |
||||
|
{ |
||||
|
IManagedByteBuffer buffer = this.allocator.AllocateManagedByteBuffer(this.chunkLength); |
||||
|
return new MemoryChunk |
||||
|
{ |
||||
|
Buffer = buffer, |
||||
|
Next = null, |
||||
|
Length = this.chunkLength |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,183 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
|
using SixLabors.ImageSharp.IO; |
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
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 ChunkedPooledMemoryStream_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>(null, () => ms2.Read(new byte[] { 1 }, 2, 0)); |
||||
|
Assert.Throws<ArgumentException>(null, () => ms2.Read(new byte[] { 1 }, 0, 2)); |
||||
|
|
||||
|
ms2.Dispose(); |
||||
|
|
||||
|
Assert.Throws<ObjectDisposedException>(() => ms2.Read(new byte[] { 1 }, 0, 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 FileStream, check the filestream
|
||||
|
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_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<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>() }; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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