mirror of https://github.com/SixLabors/ImageSharp
9 changed files with 998 additions and 107 deletions
@ -0,0 +1,301 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Buffers; |
|||
using System.IO; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace SixLabors.ImageSharp.IO |
|||
{ |
|||
/// <summary>
|
|||
/// A readonly stream that add a secondary level buffer in addition to native stream
|
|||
/// buffered reading to reduce the overhead of small incremental reads.
|
|||
/// </summary>
|
|||
internal sealed unsafe class BufferedReadStream : Stream |
|||
{ |
|||
/// <summary>
|
|||
/// The length, in bytes, of the underlying buffer.
|
|||
/// </summary>
|
|||
public const int BufferLength = 8192; |
|||
|
|||
private const int MaxBufferIndex = BufferLength - 1; |
|||
|
|||
private readonly Stream stream; |
|||
|
|||
private readonly int streamLength; |
|||
|
|||
private readonly byte[] readBuffer; |
|||
|
|||
private MemoryHandle readBufferHandle; |
|||
|
|||
private readonly byte* pinnedReadBuffer; |
|||
|
|||
// Index within our buffer, not reader position.
|
|||
private int readBufferIndex; |
|||
|
|||
// Matches what the stream position would be without buffering
|
|||
private int readerPosition; |
|||
|
|||
private bool isDisposed; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BufferedReadStream"/> class.
|
|||
/// </summary>
|
|||
/// <param name="stream">The input stream.</param>
|
|||
public BufferedReadStream(Stream stream) |
|||
{ |
|||
Guard.IsTrue(stream.CanRead, nameof(stream), "Stream must be readable."); |
|||
Guard.IsTrue(stream.CanSeek, nameof(stream), "Stream must be seekable."); |
|||
|
|||
// Ensure all underlying buffers have been flushed before we attempt to read the stream.
|
|||
// User streams may have opted to throw from Flush if CanWrite is false
|
|||
// (although the abstract Stream does not do so).
|
|||
if (stream.CanWrite) |
|||
{ |
|||
stream.Flush(); |
|||
} |
|||
|
|||
this.stream = stream; |
|||
this.Position = (int)stream.Position; |
|||
this.streamLength = (int)stream.Length; |
|||
|
|||
this.readBuffer = ArrayPool<byte>.Shared.Rent(BufferLength); |
|||
this.readBufferHandle = new Memory<byte>(this.readBuffer).Pin(); |
|||
this.pinnedReadBuffer = (byte*)this.readBufferHandle.Pointer; |
|||
|
|||
// This triggers a full read on first attempt.
|
|||
this.readBufferIndex = BufferLength; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the length, in bytes, of the stream.
|
|||
/// </summary>
|
|||
public override long Length => this.streamLength; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the current position within the stream.
|
|||
/// </summary>
|
|||
public override long Position |
|||
{ |
|||
get => this.readerPosition; |
|||
|
|||
set |
|||
{ |
|||
// Only reset readBufferIndex if we are out of bounds of our working buffer
|
|||
// otherwise we should simply move the value by the diff.
|
|||
int v = (int)value; |
|||
if (this.IsInReadBuffer(v, out int index)) |
|||
{ |
|||
this.readBufferIndex = index; |
|||
this.readerPosition = v; |
|||
} |
|||
else |
|||
{ |
|||
this.readerPosition = v; |
|||
this.stream.Seek(value, SeekOrigin.Begin); |
|||
this.readBufferIndex = BufferLength; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanRead { get; } = true; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanSeek { get; } = true; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanWrite { get; } = false; |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public override int ReadByte() |
|||
{ |
|||
if (this.readerPosition >= this.streamLength) |
|||
{ |
|||
return -1; |
|||
} |
|||
|
|||
// Our buffer has been read.
|
|||
// We need to refill and start again.
|
|||
if (this.readBufferIndex > MaxBufferIndex) |
|||
{ |
|||
this.FillReadBuffer(); |
|||
} |
|||
|
|||
this.readerPosition++; |
|||
return this.pinnedReadBuffer[this.readBufferIndex++]; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public override int Read(byte[] buffer, int offset, int count) |
|||
{ |
|||
// Too big for our buffer. Read directly from the stream.
|
|||
if (count > BufferLength) |
|||
{ |
|||
return this.ReadToBufferDirectSlow(buffer, offset, count); |
|||
} |
|||
|
|||
// Too big for remaining buffer but less than entire buffer length
|
|||
// Copy to buffer then read from there.
|
|||
if (count + this.readBufferIndex > BufferLength) |
|||
{ |
|||
return this.ReadToBufferViaCopySlow(buffer, offset, count); |
|||
} |
|||
|
|||
return this.ReadToBufferViaCopyFast(buffer, offset, count); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void Flush() |
|||
{ |
|||
// Reset the stream position to match reader position.
|
|||
if (this.readerPosition != this.stream.Position) |
|||
{ |
|||
this.stream.Seek(this.readerPosition, SeekOrigin.Begin); |
|||
this.readerPosition = (int)this.stream.Position; |
|||
} |
|||
|
|||
// Reset to trigger full read on next attempt.
|
|||
this.readBufferIndex = BufferLength; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public override long Seek(long offset, SeekOrigin origin) |
|||
{ |
|||
if (origin == SeekOrigin.Begin) |
|||
{ |
|||
this.Position = offset; |
|||
} |
|||
else |
|||
{ |
|||
this.Position += offset; |
|||
} |
|||
|
|||
return this.readerPosition; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
/// <exception cref="NotSupportedException">This operation is not supported in <see cref="BufferedReadStream"/>.</exception>
|
|||
public override void SetLength(long value) |
|||
=> throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
/// <exception cref="NotSupportedException">This operation is not supported in <see cref="BufferedReadStream"/>.</exception>
|
|||
public override void Write(byte[] buffer, int offset, int count) |
|||
=> throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void Dispose(bool disposing) |
|||
{ |
|||
if (!this.isDisposed) |
|||
{ |
|||
this.isDisposed = true; |
|||
this.readBufferHandle.Dispose(); |
|||
ArrayPool<byte>.Shared.Return(this.readBuffer); |
|||
this.Flush(); |
|||
|
|||
base.Dispose(true); |
|||
} |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private int GetPositionDifference(int p) => p - this.readerPosition; |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private bool IsInReadBuffer(int p, out int index) |
|||
{ |
|||
index = this.GetPositionDifference(p) + this.readBufferIndex; |
|||
return index > -1 && index < BufferLength; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.NoInlining)] |
|||
private void FillReadBuffer() |
|||
{ |
|||
if (this.readerPosition != this.stream.Position) |
|||
{ |
|||
this.stream.Seek(this.readerPosition, SeekOrigin.Begin); |
|||
} |
|||
|
|||
this.stream.Read(this.readBuffer, 0, BufferLength); |
|||
this.readBufferIndex = 0; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private int ReadToBufferViaCopyFast(byte[] buffer, int offset, int count) |
|||
{ |
|||
int n = this.GetCopyCount(count); |
|||
this.CopyBytes(buffer, offset, n); |
|||
|
|||
this.readerPosition += n; |
|||
this.readBufferIndex += n; |
|||
|
|||
return n; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.NoInlining)] |
|||
private int ReadToBufferViaCopySlow(byte[] buffer, int offset, int count) |
|||
{ |
|||
// Refill our buffer then copy.
|
|||
this.FillReadBuffer(); |
|||
|
|||
return this.ReadToBufferViaCopyFast(buffer, offset, count); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.NoInlining)] |
|||
private int ReadToBufferDirectSlow(byte[] buffer, int offset, int count) |
|||
{ |
|||
// Read to target but don't copy to our read buffer.
|
|||
if (this.readerPosition != this.stream.Position) |
|||
{ |
|||
this.stream.Seek(this.readerPosition, SeekOrigin.Begin); |
|||
} |
|||
|
|||
int n = this.stream.Read(buffer, offset, count); |
|||
this.Position += n; |
|||
|
|||
return n; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private int GetCopyCount(int count) |
|||
{ |
|||
int n = this.streamLength - this.readerPosition; |
|||
if (n > count) |
|||
{ |
|||
n = count; |
|||
} |
|||
|
|||
if (n < 0) |
|||
{ |
|||
n = 0; |
|||
} |
|||
|
|||
return n; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private void CopyBytes(byte[] buffer, int offset, int count) |
|||
{ |
|||
// Same as MemoryStream.
|
|||
if (count < 9) |
|||
{ |
|||
int byteCount = count; |
|||
int read = this.readBufferIndex; |
|||
byte* pinned = this.pinnedReadBuffer; |
|||
|
|||
while (--byteCount > -1) |
|||
{ |
|||
buffer[offset + byteCount] = pinned[read + byteCount]; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
Buffer.BlockCopy(this.readBuffer, this.readBufferIndex, buffer, offset, count); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,293 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Buffers; |
|||
using System.IO; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace SixLabors.ImageSharp.IO |
|||
{ |
|||
/// <summary>
|
|||
/// A readonly stream that add a secondary level buffer in addition to native stream
|
|||
/// buffered reading to reduce the overhead of small incremental reads.
|
|||
/// </summary>
|
|||
internal sealed unsafe class BufferedReadStream2 : IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// The length, in bytes, of the underlying buffer.
|
|||
/// </summary>
|
|||
public const int BufferLength = 8192; |
|||
|
|||
private const int MaxBufferIndex = BufferLength - 1; |
|||
|
|||
private readonly Stream stream; |
|||
|
|||
private readonly byte[] readBuffer; |
|||
|
|||
private MemoryHandle readBufferHandle; |
|||
|
|||
private readonly byte* pinnedReadBuffer; |
|||
|
|||
private int readBufferIndex; |
|||
|
|||
private readonly int length; |
|||
|
|||
private int position; |
|||
|
|||
private bool isDisposed; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BufferedReadStream2"/> class.
|
|||
/// </summary>
|
|||
/// <param name="stream">The input stream.</param>
|
|||
public BufferedReadStream2(Stream stream) |
|||
{ |
|||
Guard.IsTrue(stream.CanRead, nameof(stream), "Stream must be readable."); |
|||
Guard.IsTrue(stream.CanSeek, nameof(stream), "Stream must be seekable."); |
|||
|
|||
// Ensure all underlying buffers have been flushed before we attempt to read the stream.
|
|||
// User streams may have opted to throw from Flush if CanWrite is false
|
|||
// (although the abstract Stream does not do so).
|
|||
if (stream.CanWrite) |
|||
{ |
|||
stream.Flush(); |
|||
} |
|||
|
|||
this.stream = stream; |
|||
this.Position = (int)stream.Position; |
|||
this.length = (int)stream.Length; |
|||
|
|||
this.readBuffer = ArrayPool<byte>.Shared.Rent(BufferLength); |
|||
this.readBufferHandle = new Memory<byte>(this.readBuffer).Pin(); |
|||
this.pinnedReadBuffer = (byte*)this.readBufferHandle.Pointer; |
|||
|
|||
// This triggers a full read on first attempt.
|
|||
this.readBufferIndex = BufferLength; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the length, in bytes, of the stream.
|
|||
/// </summary>
|
|||
public long Length => this.length; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the current position within the stream.
|
|||
/// </summary>
|
|||
public long Position |
|||
{ |
|||
get => this.position; |
|||
|
|||
set |
|||
{ |
|||
// Only reset readIndex if we are out of bounds of our working buffer
|
|||
// otherwise we should simply move the value by the diff.
|
|||
int v = (int)value; |
|||
if (this.IsInReadBuffer(v, out int index)) |
|||
{ |
|||
this.readBufferIndex = index; |
|||
this.position = v; |
|||
} |
|||
else |
|||
{ |
|||
this.position = v; |
|||
this.stream.Seek(value, SeekOrigin.Begin); |
|||
this.readBufferIndex = BufferLength; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public bool CanRead { get; } = true; |
|||
|
|||
public bool CanSeek { get; } = true; |
|||
|
|||
public bool CanWrite { get; } = false; |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public int ReadByte() |
|||
{ |
|||
if (this.position >= this.length) |
|||
{ |
|||
return -1; |
|||
} |
|||
|
|||
if (this.readBufferIndex > MaxBufferIndex) |
|||
{ |
|||
this.FillReadBuffer(); |
|||
} |
|||
|
|||
this.position++; |
|||
return this.pinnedReadBuffer[this.readBufferIndex++]; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public int Read(byte[] buffer, int offset, int count) |
|||
{ |
|||
if (count > BufferLength) |
|||
{ |
|||
return this.ReadToBufferDirectSlow(buffer, offset, count); |
|||
} |
|||
|
|||
if (count + this.readBufferIndex > BufferLength) |
|||
{ |
|||
return this.ReadToBufferViaCopySlow(buffer, offset, count); |
|||
} |
|||
|
|||
// return this.ReadToBufferViaCopyFast(buffer, offset, count);
|
|||
int n = this.GetCopyCount(count); |
|||
this.CopyBytes(buffer, offset, n); |
|||
|
|||
this.position += n; |
|||
this.readBufferIndex += n; |
|||
|
|||
return n; |
|||
} |
|||
|
|||
public void Flush() |
|||
{ |
|||
// Reset the stream position.
|
|||
if (this.position != this.stream.Position) |
|||
{ |
|||
this.stream.Seek(this.position, SeekOrigin.Begin); |
|||
this.position = (int)this.stream.Position; |
|||
} |
|||
|
|||
this.readBufferIndex = BufferLength; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public long Seek(long offset, SeekOrigin origin) |
|||
{ |
|||
if (origin == SeekOrigin.Begin) |
|||
{ |
|||
this.Position = offset; |
|||
} |
|||
else |
|||
{ |
|||
this.Position += offset; |
|||
} |
|||
|
|||
return this.position; |
|||
} |
|||
|
|||
public void SetLength(long value) |
|||
=> throw new NotSupportedException(); |
|||
|
|||
public void Write(byte[] buffer, int offset, int count) |
|||
=> throw new NotSupportedException(); |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (!this.isDisposed) |
|||
{ |
|||
this.isDisposed = true; |
|||
this.readBufferHandle.Dispose(); |
|||
ArrayPool<byte>.Shared.Return(this.readBuffer); |
|||
this.Flush(); |
|||
} |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private int GetPositionDifference(int p) => p - this.position; |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private bool IsInReadBuffer(int p, out int index) |
|||
{ |
|||
index = this.GetPositionDifference(p) + this.readBufferIndex; |
|||
return index > -1 && index < BufferLength; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.NoInlining)] |
|||
private void FillReadBuffer() |
|||
{ |
|||
if (this.position != this.stream.Position) |
|||
{ |
|||
this.stream.Seek(this.position, SeekOrigin.Begin); |
|||
} |
|||
|
|||
this.stream.Read(this.readBuffer, 0, BufferLength); |
|||
this.readBufferIndex = 0; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private int ReadToBufferViaCopyFast(byte[] buffer, int offset, int count) |
|||
{ |
|||
int n = this.GetCopyCount(count); |
|||
this.CopyBytes(buffer, offset, n); |
|||
|
|||
this.position += n; |
|||
this.readBufferIndex += n; |
|||
|
|||
return n; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.NoInlining)] |
|||
private int ReadToBufferViaCopySlow(byte[] buffer, int offset, int count) |
|||
{ |
|||
// Refill our buffer then copy.
|
|||
this.FillReadBuffer(); |
|||
|
|||
// return this.ReadToBufferViaCopyFast(buffer, offset, count);
|
|||
int n = this.GetCopyCount(count); |
|||
this.CopyBytes(buffer, offset, n); |
|||
|
|||
this.position += n; |
|||
this.readBufferIndex += n; |
|||
|
|||
return n; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.NoInlining)] |
|||
private int ReadToBufferDirectSlow(byte[] buffer, int offset, int count) |
|||
{ |
|||
// Read to target but don't copy to our read buffer.
|
|||
if (this.position != this.stream.Position) |
|||
{ |
|||
this.stream.Seek(this.position, SeekOrigin.Begin); |
|||
} |
|||
|
|||
int n = this.stream.Read(buffer, offset, count); |
|||
this.Position += n; |
|||
|
|||
return n; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private int GetCopyCount(int count) |
|||
{ |
|||
int n = this.length - this.position; |
|||
if (n > count) |
|||
{ |
|||
n = count; |
|||
} |
|||
|
|||
if (n < 0) |
|||
{ |
|||
n = 0; |
|||
} |
|||
|
|||
return n; |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private void CopyBytes(byte[] buffer, int offset, int count) |
|||
{ |
|||
// Same as MemoryStream.
|
|||
if (count < 9) |
|||
{ |
|||
int byteCount = count; |
|||
int read = this.readBufferIndex; |
|||
byte* pinned = this.pinnedReadBuffer; |
|||
|
|||
while (--byteCount > -1) |
|||
{ |
|||
buffer[offset + byteCount] = pinned[read + byteCount]; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
Buffer.BlockCopy(this.readBuffer, this.readBufferIndex, buffer, offset, count); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,211 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
using SixLabors.ImageSharp.IO; |
|||
using Xunit; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.IO |
|||
{ |
|||
public class BufferedReadStreamTests |
|||
{ |
|||
[Fact] |
|||
public void BufferedStreamCanReadSingleByteFromOrigin() |
|||
{ |
|||
using (MemoryStream stream = this.CreateTestStream()) |
|||
{ |
|||
byte[] expected = stream.ToArray(); |
|||
using (var reader = new BufferedReadStream(stream)) |
|||
{ |
|||
Assert.Equal(expected[0], reader.ReadByte()); |
|||
|
|||
// We've read a whole chunk but increment by 1 in our reader.
|
|||
Assert.Equal(BufferedReadStream.BufferLength, stream.Position); |
|||
Assert.Equal(1, reader.Position); |
|||
} |
|||
|
|||
// Position of the stream should be reset on disposal.
|
|||
Assert.Equal(1, stream.Position); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void BufferedStreamCanReadSingleByteFromOffset() |
|||
{ |
|||
using (MemoryStream stream = this.CreateTestStream()) |
|||
{ |
|||
byte[] expected = stream.ToArray(); |
|||
const int offset = 5; |
|||
using (var reader = new BufferedReadStream(stream)) |
|||
{ |
|||
reader.Position = offset; |
|||
|
|||
Assert.Equal(expected[offset], reader.ReadByte()); |
|||
|
|||
// We've read a whole chunk but increment by 1 in our reader.
|
|||
Assert.Equal(BufferedReadStream.BufferLength + offset, stream.Position); |
|||
Assert.Equal(offset + 1, reader.Position); |
|||
} |
|||
|
|||
Assert.Equal(offset + 1, stream.Position); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void BufferedStreamCanReadSubsequentSingleByteCorrectly() |
|||
{ |
|||
using (MemoryStream stream = this.CreateTestStream()) |
|||
{ |
|||
byte[] expected = stream.ToArray(); |
|||
int i; |
|||
using (var reader = new BufferedReadStream(stream)) |
|||
{ |
|||
for (i = 0; i < expected.Length; i++) |
|||
{ |
|||
Assert.Equal(expected[i], reader.ReadByte()); |
|||
Assert.Equal(i + 1, reader.Position); |
|||
|
|||
if (i < BufferedReadStream.BufferLength) |
|||
{ |
|||
Assert.Equal(stream.Position, BufferedReadStream.BufferLength); |
|||
} |
|||
else if (i >= BufferedReadStream.BufferLength && i < BufferedReadStream.BufferLength * 2) |
|||
{ |
|||
// We should have advanced to the second chunk now.
|
|||
Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 2); |
|||
} |
|||
else |
|||
{ |
|||
// We should have advanced to the third chunk now.
|
|||
Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 3); |
|||
} |
|||
} |
|||
} |
|||
|
|||
Assert.Equal(i, stream.Position); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void BufferedStreamCanReadMultipleBytesFromOrigin() |
|||
{ |
|||
using (MemoryStream stream = this.CreateTestStream()) |
|||
{ |
|||
var buffer = new byte[2]; |
|||
byte[] expected = stream.ToArray(); |
|||
using (var reader = new BufferedReadStream(stream)) |
|||
{ |
|||
Assert.Equal(2, reader.Read(buffer, 0, 2)); |
|||
Assert.Equal(expected[0], buffer[0]); |
|||
Assert.Equal(expected[1], buffer[1]); |
|||
|
|||
// We've read a whole chunk but increment by the buffer length in our reader.
|
|||
Assert.Equal(stream.Position, BufferedReadStream.BufferLength); |
|||
Assert.Equal(buffer.Length, reader.Position); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void BufferedStreamCanReadSubsequentMultipleByteCorrectly() |
|||
{ |
|||
using (MemoryStream stream = this.CreateTestStream()) |
|||
{ |
|||
var buffer = new byte[2]; |
|||
byte[] expected = stream.ToArray(); |
|||
using (var reader = new BufferedReadStream(stream)) |
|||
{ |
|||
for (int i = 0, o = 0; i < expected.Length / 2; i++, o += 2) |
|||
{ |
|||
Assert.Equal(2, reader.Read(buffer, 0, 2)); |
|||
Assert.Equal(expected[o], buffer[0]); |
|||
Assert.Equal(expected[o + 1], buffer[1]); |
|||
Assert.Equal(o + 2, reader.Position); |
|||
|
|||
int offset = i * 2; |
|||
if (offset < BufferedReadStream.BufferLength) |
|||
{ |
|||
Assert.Equal(stream.Position, BufferedReadStream.BufferLength); |
|||
} |
|||
else if (offset >= BufferedReadStream.BufferLength && offset < BufferedReadStream.BufferLength * 2) |
|||
{ |
|||
// We should have advanced to the second chunk now.
|
|||
Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 2); |
|||
} |
|||
else |
|||
{ |
|||
// We should have advanced to the third chunk now.
|
|||
Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 3); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void BufferedStreamCanSkip() |
|||
{ |
|||
using (MemoryStream stream = this.CreateTestStream()) |
|||
{ |
|||
byte[] expected = stream.ToArray(); |
|||
using (var reader = new BufferedReadStream(stream)) |
|||
{ |
|||
int skip = 50; |
|||
int plusOne = 1; |
|||
int skip2 = BufferedReadStream.BufferLength; |
|||
|
|||
// Skip
|
|||
reader.Skip(skip); |
|||
Assert.Equal(skip, reader.Position); |
|||
Assert.Equal(stream.Position, reader.Position); |
|||
|
|||
// Read
|
|||
Assert.Equal(expected[skip], reader.ReadByte()); |
|||
|
|||
// Skip Again
|
|||
reader.Skip(skip2); |
|||
|
|||
// First Skip + First Read + Second Skip
|
|||
int position = skip + plusOne + skip2; |
|||
|
|||
Assert.Equal(position, reader.Position); |
|||
Assert.Equal(stream.Position, reader.Position); |
|||
Assert.Equal(expected[position], reader.ReadByte()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void BufferedStreamReadsSmallStream() |
|||
{ |
|||
// Create a stream smaller than the default buffer length
|
|||
using (MemoryStream stream = this.CreateTestStream(BufferedReadStream.BufferLength / 4)) |
|||
{ |
|||
byte[] expected = stream.ToArray(); |
|||
const int offset = 5; |
|||
using (var reader = new BufferedReadStream(stream)) |
|||
{ |
|||
reader.Position = offset; |
|||
|
|||
Assert.Equal(expected[offset], reader.ReadByte()); |
|||
|
|||
// We've read a whole length of the stream but increment by 1 in our reader.
|
|||
Assert.Equal(BufferedReadStream.BufferLength / 4, stream.Position); |
|||
Assert.Equal(offset + 1, reader.Position); |
|||
} |
|||
|
|||
Assert.Equal(offset + 1, stream.Position); |
|||
} |
|||
} |
|||
|
|||
private MemoryStream CreateTestStream(int length = BufferedReadStream.BufferLength * 3) |
|||
{ |
|||
var buffer = new byte[length]; |
|||
var random = new Random(); |
|||
random.NextBytes(buffer); |
|||
|
|||
return new MemoryStream(buffer); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue