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