diff --git a/src/System.Net.Http.Formatting/Internal/ByteRangeStream.cs b/src/System.Net.Http.Formatting/Internal/ByteRangeStream.cs index b519144bc..33325d4f4 100644 --- a/src/System.Net.Http.Formatting/Internal/ByteRangeStream.cs +++ b/src/System.Net.Http.Formatting/Internal/ByteRangeStream.cs @@ -3,12 +3,14 @@ using System.IO; using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; using System.Web.Http; namespace System.Net.Http.Internal { /// - /// Stream which only exposes a read-only only range view of an + /// Stream which only exposes a read-only only range view of an /// inner stream. /// internal class ByteRangeStream : DelegatingStream @@ -16,7 +18,7 @@ internal class ByteRangeStream : DelegatingStream // The offset stream position at which the range starts. private readonly long _lowerbounds; - // The total number of bytes within the range. + // The total number of bytes within the range. private readonly long _totalCount; // The current number of bytes read into the range @@ -92,6 +94,23 @@ public override bool CanWrite get { return false; } } + public override long Position + { + get + { + return _currentCount; + } + set + { + if (value < 0) + { + throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, 0L); + } + + _currentCount = value; + } + } + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) { return base.BeginRead(buffer, offset, PrepareStreamForRangeRead(count), callback, state); @@ -102,6 +121,11 @@ public override int Read(byte[] buffer, int offset, int count) return base.Read(buffer, offset, PrepareStreamForRangeRead(count)); } + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return base.ReadAsync(buffer, offset, PrepareStreamForRangeRead(count), cancellationToken); + } + public override int ReadByte() { int effectiveCount = PrepareStreamForRangeRead(1); @@ -109,9 +133,35 @@ public override int ReadByte() { return -1; } + return base.ReadByte(); } + public override long Seek(long offset, SeekOrigin origin) + { + switch (origin) + { + case SeekOrigin.Begin: + _currentCount = offset; + break; + case SeekOrigin.Current: + _currentCount = _currentCount + offset; + break; + case SeekOrigin.End: + _currentCount = _totalCount + offset; + break; + default: + throw Error.InvalidEnumArgument("origin", (int)origin, typeof(SeekOrigin)); + } + + if (_currentCount < 0L) + { + throw new IOException(Properties.Resources.ByteRangeStreamInvalidOffset); + } + + return _currentCount; + } + public override void SetLength(long value) { throw Error.NotSupported(Properties.Resources.ByteRangeStreamReadOnly); @@ -132,33 +182,49 @@ public override void EndWrite(IAsyncResult asyncResult) throw Error.NotSupported(Properties.Resources.ByteRangeStreamReadOnly); } + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw Error.NotSupported(Properties.Resources.ByteRangeStreamReadOnly); + } + public override void WriteByte(byte value) { throw Error.NotSupported(Properties.Resources.ByteRangeStreamReadOnly); } /// - /// Gets the + /// Gets the correct count for the next read operation. /// /// The count requested to be read by the caller. /// The remaining bytes to read within the range defined for this stream. private int PrepareStreamForRangeRead(int count) { - long effectiveCount = Math.Min(count, _totalCount - _currentCount); - if (effectiveCount > 0) + // A negative count causes base.Raad* methods to throw an ArgumentOutOfRangeException. + if (count <= 0) { - // Check if we should update the stream position - long position = InnerStream.Position; - if (_lowerbounds + _currentCount != position) - { - InnerStream.Position = _lowerbounds + _currentCount; - } + return count; + } - // Update current number of bytes read - _currentCount += effectiveCount; + // Reading past the end simply does nothing. + if (_currentCount >= _totalCount) + { + return 0; } - // Effective count can never be bigger than int + long effectiveCount = Math.Min(count, _totalCount - _currentCount); + + // Check if we should update the inner stream's position. + var newPosition = _lowerbounds + _currentCount; + var position = InnerStream.Position; + if (newPosition != position) + { + InnerStream.Position = newPosition; + } + + // Update current number of bytes read. + _currentCount += effectiveCount; + + // Effective count can never be bigger than int. return (int)effectiveCount; } } diff --git a/src/System.Net.Http.Formatting/Internal/DelegatingStream.cs b/src/System.Net.Http.Formatting/Internal/DelegatingStream.cs index 9547b6704..ee0c51bde 100644 --- a/src/System.Net.Http.Formatting/Internal/DelegatingStream.cs +++ b/src/System.Net.Http.Formatting/Internal/DelegatingStream.cs @@ -9,12 +9,12 @@ namespace System.Net.Http.Internal { /// - /// Stream that delegates to inner stream. + /// Stream that delegates to inner stream. /// This is taken from System.Net.Http /// internal abstract class DelegatingStream : Stream { - private Stream _innerStream; + private readonly Stream _innerStream; protected DelegatingStream(Stream innerStream) { @@ -119,11 +119,6 @@ public override void Flush() _innerStream.Flush(); } - public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) - { - return _innerStream.CopyToAsync(destination, bufferSize, cancellationToken); - } - public override Task FlushAsync(CancellationToken cancellationToken) { return _innerStream.FlushAsync(cancellationToken); diff --git a/src/System.Net.Http.Formatting/Properties/Resources.Designer.cs b/src/System.Net.Http.Formatting/Properties/Resources.Designer.cs index dab0cdd71..8797f4ffa 100644 --- a/src/System.Net.Http.Formatting/Properties/Resources.Designer.cs +++ b/src/System.Net.Http.Formatting/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace System.Net.Http.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -137,6 +137,15 @@ internal static string ByteRangeStreamInvalidFrom { } } + /// + /// Looks up a localized string similar to An attempt was made to move the position before the beginning of the stream.. + /// + internal static string ByteRangeStreamInvalidOffset { + get { + return ResourceManager.GetString("ByteRangeStreamInvalidOffset", resourceCulture); + } + } + /// /// Looks up a localized string similar to None of the requested ranges ({0}) overlap with the current extent of the selected resource.. /// diff --git a/src/System.Net.Http.Formatting/Properties/Resources.resx b/src/System.Net.Http.Formatting/Properties/Resources.resx index 77b63a96a..d20156c5a 100644 --- a/src/System.Net.Http.Formatting/Properties/Resources.resx +++ b/src/System.Net.Http.Formatting/Properties/Resources.resx @@ -339,4 +339,7 @@ The '{0}' method in '{1}' returned null. It must return a RemoteStreamResult instance containing a writable stream and a valid URL. + + An attempt was made to move the position before the beginning of the stream. + \ No newline at end of file diff --git a/test/System.Net.Http.Formatting.Test/Internal/ByteRangeStreamTest.cs b/test/System.Net.Http.Formatting.Test/Internal/ByteRangeStreamTest.cs index bc53fd98d..eb193b51a 100644 --- a/test/System.Net.Http.Formatting.Test/Internal/ByteRangeStreamTest.cs +++ b/test/System.Net.Http.Formatting.Test/Internal/ByteRangeStreamTest.cs @@ -3,6 +3,8 @@ using System.IO; using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; using Microsoft.TestCommon; using Moq; @@ -10,10 +12,63 @@ namespace System.Net.Http.Internal { public class ByteRangeStreamTest { + // from, to, expectedText + public static TheoryDataSet CopyBoundsData + { + get + { + return new TheoryDataSet + { + { null, 23, "This is the whole text." }, + { 0, null, "This is the whole text." }, + { 0, 22, "This is the whole text." }, + { 0, 3, "This" }, + { 12, 16, "whole" }, + { null, 5, "text." }, + { 18, null, "text." }, + { 18, 22, "text." }, + }; + } + } + + // from, to, innerLength, effectiveLength + public static TheoryDataSet ReadBoundsData + { + get + { + return new TheoryDataSet + { + { 0, 9, 20, 10 }, + { 8, 8, 10, 1 }, + { 0, 19, 20, 20 }, + { 0, 29, 40, 30 }, + { 0, 29, 20, 20 }, + { 19, 29, 20, 1 }, + }; + } + } + + // from, to, innerLength, effectiveLength for reads limited by byte[] size. + public static TheoryDataSet ReadBoundsDataWithLimit + { + get + { + return new TheoryDataSet + { + { 0, 9, 20, 10 }, + { 8, 8, 10, 1 }, + { 0, 19, 20, 20 }, + { 0, 29, 40, 25 }, + { 0, 29, 20, 20 }, + { 19, 29, 20, 1 }, + }; + } + } + [Fact] public void Ctor_ThrowsOnNullInnerStream() { - RangeItemHeaderValue range = new RangeItemHeaderValue(0, 10); + var range = new RangeItemHeaderValue(0, 10); Assert.ThrowsArgumentNull(() => new ByteRangeStream(innerStream: null, range: range), "innerStream"); } @@ -27,9 +82,9 @@ public void Ctor_ThrowsOnNullRange() public void Ctor_ThrowsIfCantSeekInnerStream() { // Arrange - Mock mockInnerStream = new Mock(); + var mockInnerStream = new Mock(); mockInnerStream.Setup(s => s.CanSeek).Returns(false); - RangeItemHeaderValue range = new RangeItemHeaderValue(0, 10); + var range = new RangeItemHeaderValue(0, 10); // Act/Assert Assert.ThrowsArgument(() => new ByteRangeStream(mockInnerStream.Object, range), "innerStream"); @@ -39,10 +94,10 @@ public void Ctor_ThrowsIfCantSeekInnerStream() public void Ctor_ThrowsIfLowerRangeExceedsInnerStream() { // Arrange - Mock mockInnerStream = new Mock(); + var mockInnerStream = new Mock(); mockInnerStream.Setup(s => s.CanSeek).Returns(true); mockInnerStream.Setup(s => s.Length).Returns(5); - RangeItemHeaderValue range = new RangeItemHeaderValue(10, 20); + var range = new RangeItemHeaderValue(10, 20); // Act/Assert Assert.ThrowsArgumentOutOfRange(() => new ByteRangeStream(mockInnerStream.Object, range), "range", @@ -53,17 +108,18 @@ public void Ctor_ThrowsIfLowerRangeExceedsInnerStream() public void Ctor_SetsContentRange() { // Arrange - ContentRangeHeaderValue expectedContentRange = new ContentRangeHeaderValue(5, 9, 20); - Mock mockInnerStream = new Mock(); + var expectedContentRange = new ContentRangeHeaderValue(5, 9, 20); + var mockInnerStream = new Mock(); mockInnerStream.Setup(s => s.CanSeek).Returns(true); mockInnerStream.Setup(s => s.Length).Returns(20); - RangeItemHeaderValue range = new RangeItemHeaderValue(5, 9); + var range = new RangeItemHeaderValue(5, 9); // Act - ByteRangeStream rangeStream = new ByteRangeStream(mockInnerStream.Object, range); - - // Assert - Assert.Equal(expectedContentRange, rangeStream.ContentRange); + using (var rangeStream = new ByteRangeStream(mockInnerStream.Object, range)) + { + // Assert + Assert.Equal(expectedContentRange, rangeStream.ContentRange); + } } [Theory] @@ -72,87 +128,462 @@ public void Ctor_SetsContentRange() public void Ctor_ThrowsIfInnerStreamLengthIsLessThanOne(int innerLength) { // Arrange - Mock mockInnerStream = new Mock(); + var mockInnerStream = new Mock(); mockInnerStream.Setup(s => s.CanSeek).Returns(true); mockInnerStream.Setup(s => s.Length).Returns(innerLength); - RangeItemHeaderValue range = new RangeItemHeaderValue(null, 0); + var range = new RangeItemHeaderValue(null, 0); // Act/Assert - Assert.ThrowsArgumentOutOfRange(() => new ByteRangeStream(mockInnerStream.Object, range), "innerStream", - "The stream over which 'ByteRangeStream' provides a range view must have a length greater than or equal to 1.", - false, innerLength); + Assert.ThrowsArgumentOutOfRange( + () => new ByteRangeStream(mockInnerStream.Object, range), + "innerStream", + "The stream over which 'ByteRangeStream' provides a range view must have a length greater than or " + + "equal to 1.", + false, + innerLength); } [Theory] - [InlineData(0, 9, 20, 10)] - [InlineData(8, 8, 10, 1)] - [InlineData(0, 19, 20, 20)] + [PropertyData("ReadBoundsData")] public void Ctor_SetsLength(int from, int to, int innerLength, int expectedLength) { // Arrange - Mock mockInnerStream = new Mock(); + var mockInnerStream = new Mock(); mockInnerStream.Setup(s => s.CanSeek).Returns(true); mockInnerStream.Setup(s => s.Length).Returns(innerLength); - RangeItemHeaderValue range = new RangeItemHeaderValue(from, to); + var range = new RangeItemHeaderValue(from, to); // Act - ByteRangeStream rangeStream = new ByteRangeStream(mockInnerStream.Object, range); + using (var rangeStream = new ByteRangeStream(mockInnerStream.Object, range)) + { + // Assert + Assert.Equal(expectedLength, rangeStream.Length); + } + } - // Assert - Assert.Equal(expectedLength, rangeStream.Length); + [Theory] + [PropertyData("CopyBoundsData")] + public async Task CopyTo_ReadsSpecifiedRange(long? from, long? to, string expectedText) + { + // Arrange + var originalText = "This is the whole text."; + var range = new RangeItemHeaderValue(from, to); + + using (var innerStream = new MemoryStream()) + using (var writer = new StreamWriter(innerStream)) + using (var targetStream = new MemoryStream()) + using (var reader = new StreamReader(targetStream)) + { + await writer.WriteAsync(originalText); + await writer.FlushAsync(); + + // Act + using (var rangeStream = new ByteRangeStream(innerStream, range)) + { + rangeStream.CopyTo(targetStream); + } + + // Assert + targetStream.Position = 0L; + var text = await reader.ReadToEndAsync(); + Assert.Equal(expectedText, text); + } } [Theory] - [InlineData(0, 9, 20, 10)] - [InlineData(8, 8, 10, 1)] - [InlineData(0, 19, 20, 20)] - [InlineData(0, 29, 40, 25)] - [InlineData(0, 29, 20, 20)] - [InlineData(19, 29, 20, 1)] + [PropertyData("CopyBoundsData")] + public async Task CopyToAsync_ReadsSpecifiedRange(long? from, long? to, string expectedText) + { + // Arrange + var originalText = "This is the whole text."; + var range = new RangeItemHeaderValue(from, to); + + using (var innerStream = new MemoryStream()) + using (var writer = new StreamWriter(innerStream)) + using (var targetStream = new MemoryStream()) + using (var reader = new StreamReader(targetStream)) + { + await writer.WriteAsync(originalText); + await writer.FlushAsync(); + + // Act + using (var rangeStream = new ByteRangeStream(innerStream, range)) + { + await rangeStream.CopyToAsync(targetStream); + } + + // Assert + targetStream.Position = 0L; + var text = await reader.ReadToEndAsync(); + Assert.Equal(expectedText, text); + } + } + + [Fact] + public void Position_ThrowsOnNegativeValue() + { + // Arrange + var mockInnerStream = new Mock(); + mockInnerStream.Setup(s => s.CanSeek).Returns(true); + mockInnerStream.Setup(s => s.Length).Returns(10L); + var range = new RangeItemHeaderValue(0, 25L); + + using (var rangeStream = new ByteRangeStream(mockInnerStream.Object, range)) + { + // Act & Assert + Assert.Throws(() => rangeStream.Position = -1L); + } + } + + [Theory] + [InlineData(null)] + [InlineData(0L)] + [InlineData(7L)] + [InlineData(9L)] + public void Position_ReturnsZeroInitially(long? from) + { + // Arrange + var mockInnerStream = new Mock(); + mockInnerStream.Setup(s => s.CanSeek).Returns(true); + mockInnerStream.Setup(s => s.Length).Returns(10L); + var range = new RangeItemHeaderValue(from, 25L); + + using (var rangeStream = new ByteRangeStream(mockInnerStream.Object, range)) + { + // Act + var position = rangeStream.Position; + + // Assert + Assert.Equal(0L, position); + } + } + + [Fact] + public void Position_CanBeSetAfterLength() + { + // Arrange + var expectedPosition = 300L; + var mockInnerStream = new Mock(); + mockInnerStream.Setup(s => s.CanSeek).Returns(true); + mockInnerStream.Setup(s => s.Length).Returns(10L); + var range = new RangeItemHeaderValue(0L, 10L); + + using (var rangeStream = new ByteRangeStream(mockInnerStream.Object, range)) + { + // Act + rangeStream.Position = expectedPosition; + + // Assert + Assert.Equal(expectedPosition, rangeStream.Position); + } + } + + [Fact] + public async Task Position_PositionsNextRead() + { + // Arrange + var originalText = "890123456789"; + var range = new RangeItemHeaderValue(2L, null); + + using (var innerStream = new MemoryStream()) + using (var writer = new StreamWriter(innerStream)) + { + await writer.WriteAsync(originalText); + await writer.FlushAsync(); + + using (var rangeStream = new ByteRangeStream(innerStream, range)) + { + // Act + rangeStream.Position = 5L; + + // Assert + var read = rangeStream.ReadByte(); + Assert.Equal('5', (char)read); + } + } + } + + [Theory] + [PropertyData("ReadBoundsDataWithLimit")] + public void BeginRead_ReadsEffectiveLengthBytes(int from, int to, int innerLength, int effectiveLength) + { + // Arrange + var mockInnerStream = new Mock(); + mockInnerStream.Setup(s => s.CanSeek).Returns(true); + mockInnerStream.Setup(s => s.Length).Returns(innerLength); + var range = new RangeItemHeaderValue(from, to); + var data = new byte[25]; + var offset = 5; + var callback = new AsyncCallback(_ => { }); + var userState = new object(); + + using (var rangeStream = new ByteRangeStream(mockInnerStream.Object, range)) + { + // Act + var result = rangeStream.BeginRead(data, offset, data.Length, callback, userState); + rangeStream.EndRead(result); + + // Assert + mockInnerStream.Verify( + s => s.BeginRead(data, offset, effectiveLength, callback, userState), + Times.Once()); + Assert.Equal(effectiveLength, rangeStream.Position); + } + } + + [Fact] + public async Task BeginRead_CanReadAfterLength() + { + // Arrange + var originalText = "This is the whole text."; + var range = new RangeItemHeaderValue(0L, null); + var data = new byte[25]; + var callback = new AsyncCallback(_ => { }); + var userState = new object(); + + using (var innerStream = new MemoryStream()) + using (var writer = new StreamWriter(innerStream)) + { + await writer.WriteAsync(originalText); + await writer.FlushAsync(); + + using (var rangeStream = new ByteRangeStream(innerStream, range)) + { + rangeStream.Position = 50L; + + // Act + var result = rangeStream.BeginRead(data, 0, data.Length, callback, userState); + var read = rangeStream.EndRead(result); + + // Assert + Assert.Equal(0, read); + } + } + } + + [Theory] + [PropertyData("ReadBoundsDataWithLimit")] public void Read_ReadsEffectiveLengthBytes(int from, int to, int innerLength, int effectiveLength) { // Arrange - Mock mockInnerStream = new Mock(); + var mockInnerStream = new Mock(); mockInnerStream.Setup(s => s.CanSeek).Returns(true); mockInnerStream.Setup(s => s.Length).Returns(innerLength); - RangeItemHeaderValue range = new RangeItemHeaderValue(from, to); - byte[] data = new byte[25]; - int offset = 5; + var range = new RangeItemHeaderValue(from, to); + var data = new byte[25]; + var offset = 5; - // Act - ByteRangeStream rangeStream = new ByteRangeStream(mockInnerStream.Object, range); - rangeStream.Read(data, offset, data.Length); + using (var rangeStream = new ByteRangeStream(mockInnerStream.Object, range)) + { + // Act + rangeStream.Read(data, offset, data.Length); + + // Assert + mockInnerStream.Verify(s => s.Read(data, offset, effectiveLength), Times.Once()); + Assert.Equal(effectiveLength, rangeStream.Position); + } + } + + [Fact] + public async Task Read_CanReadAfterLength() + { + // Arrange + var originalText = "This is the whole text."; + var range = new RangeItemHeaderValue(0L, null); + var data = new byte[25]; + + using (var innerStream = new MemoryStream()) + using (var writer = new StreamWriter(innerStream)) + { + await writer.WriteAsync(originalText); + await writer.FlushAsync(); + + using (var rangeStream = new ByteRangeStream(innerStream, range)) + { + rangeStream.Position = 50L; + + // Act + var read = rangeStream.Read(data, 0, data.Length); + + // Assert + Assert.Equal(0, read); + } + } + } + + [Theory] + [PropertyData("ReadBoundsDataWithLimit")] + public async Task ReadAsync_ReadsEffectiveLengthBytes(int from, int to, int innerLength, int effectiveLength) + { + // Arrange + var mockInnerStream = new Mock(); + mockInnerStream.Setup(s => s.CanSeek).Returns(true); + mockInnerStream.Setup(s => s.Length).Returns(innerLength); + var range = new RangeItemHeaderValue(from, to); + var data = new byte[25]; + var offset = 5; + + using (var rangeStream = new ByteRangeStream(mockInnerStream.Object, range)) + { + // Act + await rangeStream.ReadAsync(data, offset, data.Length); + + // Assert + mockInnerStream.Verify( + s => s.ReadAsync(data, offset, effectiveLength, CancellationToken.None), + Times.Once()); + Assert.Equal(effectiveLength, rangeStream.Position); + } + } + + [Fact] + public async Task ReadAsync_CanReadAfterLength() + { + // Arrange + var originalText = "This is the whole text."; + var range = new RangeItemHeaderValue(0L, null); + var data = new byte[25]; + + using (var innerStream = new MemoryStream()) + using (var writer = new StreamWriter(innerStream)) + { + await writer.WriteAsync(originalText); + await writer.FlushAsync(); - // Assert - mockInnerStream.Verify(s => s.Read(data, offset, effectiveLength), Times.Once()); + using (var rangeStream = new ByteRangeStream(innerStream, range)) + { + rangeStream.Position = 50L; + + // Act + var read = await rangeStream.ReadAsync(data, 0, data.Length); + + // Assert + Assert.Equal(0, read); + } + } } [Theory] - [InlineData(0, 9, 20, 10)] - [InlineData(8, 8, 10, 1)] - [InlineData(0, 19, 20, 20)] - [InlineData(0, 29, 40, 30)] - [InlineData(0, 29, 20, 20)] - [InlineData(19, 29, 20, 1)] + [PropertyData("ReadBoundsData")] public void ReadByte_ReadsEffectiveLengthTimes(int from, int to, int innerLength, int effectiveLength) { // Arrange - Mock mockInnerStream = new Mock(); + var mockInnerStream = new Mock(); mockInnerStream.Setup(s => s.CanSeek).Returns(true); mockInnerStream.Setup(s => s.Length).Returns(innerLength); - RangeItemHeaderValue range = new RangeItemHeaderValue(from, to); + var range = new RangeItemHeaderValue(from, to); + var counter = 0; - // Act - ByteRangeStream rangeStream = new ByteRangeStream(mockInnerStream.Object, range); - int counter = 0; - while (rangeStream.ReadByte() != -1) + using (var rangeStream = new ByteRangeStream(mockInnerStream.Object, range)) + { + // Act + while (rangeStream.ReadByte() != -1) + { + counter++; + } + + // Assert + mockInnerStream.Verify(s => s.ReadByte(), Times.Exactly(effectiveLength)); + Assert.Equal(effectiveLength, counter); + Assert.Equal(effectiveLength, rangeStream.Position); + } + } + + [Fact] + public async Task ReadByte_CanReadAfterLength() + { + // Arrange + var originalText = "This is the whole text."; + var range = new RangeItemHeaderValue(0L, null); + + using (var innerStream = new MemoryStream()) + using (var writer = new StreamWriter(innerStream)) + { + await writer.WriteAsync(originalText); + await writer.FlushAsync(); + + using (var rangeStream = new ByteRangeStream(innerStream, range)) + { + rangeStream.Position = 50L; + + // Act + var read = rangeStream.ReadByte(); + + // Assert + Assert.Equal(-1, read); + } + } + } + + [Theory] + [InlineData(-1, SeekOrigin.Begin)] + [InlineData(-1, SeekOrigin.Current)] + [InlineData(-11, SeekOrigin.End)] + public void Seek_ThrowsIfBeforeOrigin(int offset, SeekOrigin origin) + { + // Arrange + var mockInnerStream = new Mock(); + mockInnerStream.Setup(s => s.CanSeek).Returns(true); + mockInnerStream.Setup(s => s.Length).Returns(10L); + var range = new RangeItemHeaderValue(0, 25L); + + using (var rangeStream = new ByteRangeStream(mockInnerStream.Object, range)) + { + // Act & Assert + Assert.Throws(() => rangeStream.Seek(offset, origin)); + } + } + + [Theory] + [InlineData(25, SeekOrigin.Begin)] + [InlineData(25, SeekOrigin.Current)] + [InlineData(15, SeekOrigin.End)] + public void Seek_CanMoveAfterLength(int offset, SeekOrigin origin) + { + // Arrange + var expectedPosition = 25L; + var mockInnerStream = new Mock(); + mockInnerStream.Setup(s => s.CanSeek).Returns(true); + mockInnerStream.Setup(s => s.Length).Returns(10L); + var range = new RangeItemHeaderValue(0L, 10L); + + using (var rangeStream = new ByteRangeStream(mockInnerStream.Object, range)) { - counter++; + // Act + var newPosition = rangeStream.Seek(offset, origin); + + // Assert + Assert.Equal(expectedPosition, newPosition); + Assert.Equal(expectedPosition, rangeStream.Position); } + } - // Assert - Assert.Equal(effectiveLength, counter); - mockInnerStream.Verify(s => s.ReadByte(), Times.Exactly(effectiveLength)); + [Theory] + [InlineData(5, SeekOrigin.Begin)] + [InlineData(5, SeekOrigin.Current)] + [InlineData(-5, SeekOrigin.End)] + public async Task Seek_PositionsNextRead(int offset, SeekOrigin origin) + { + // Arrange + var originalText = "890123456789"; + var range = new RangeItemHeaderValue(2L, null); + + using (var innerStream = new MemoryStream()) + using (var writer = new StreamWriter(innerStream)) + { + await writer.WriteAsync(originalText); + await writer.FlushAsync(); + + using (var rangeStream = new ByteRangeStream(innerStream, range)) + { + // Act + rangeStream.Seek(offset, origin); + + // Assert + var read = rangeStream.ReadByte(); + Assert.Equal('5', (char)read); + } + } } } }