diff --git a/CSUtilities.Tests/IO/StreamIOTests.cs b/CSUtilities.Tests/IO/StreamIOTests.cs index 27b98fd..b23c817 100644 --- a/CSUtilities.Tests/IO/StreamIOTests.cs +++ b/CSUtilities.Tests/IO/StreamIOTests.cs @@ -9,6 +9,42 @@ namespace CSUtilities.Tests.IO; public class StreamIOTests : IDisposable { + private sealed class PartialReadMemoryStream : MemoryStream + { + public PartialReadMemoryStream(byte[] data) : base(data) { } + + public override int Read(byte[] buffer, int offset, int count) + { + return base.Read(buffer, offset, Math.Min(count, 2)); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) + { + return base.ReadAsync(buffer, offset, Math.Min(count, 2), cancellationToken); + } + } + + private sealed class NonSeekableStream : Stream + { + private readonly Stream _inner; + + public NonSeekableStream(byte[] data) + { + this._inner = new MemoryStream(data); + } + + public override bool CanRead => true; + 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() { } + public override int Read(byte[] buffer, int offset, int count) => this._inner.Read(buffer, offset, Math.Min(count, 2)); + 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 NotSupportedException(); + } + private readonly MemoryStream _memoryStream; private readonly StreamIO _streamIO; @@ -41,6 +77,35 @@ public void WriteAndReadBytes_WorksCorrectly() Assert.Equal(data, _streamIO.ReadBytes(data.Length)); } + [Fact] + public void ConstructorBuffersANonSeekablePartialReadStream() + { + byte[] data = { 1, 2, 3, 4, 5, 6 }; + using NonSeekableStream source = new NonSeekableStream(data); + using StreamIO stream = new StreamIO(source); + + Assert.True(stream.Stream.CanSeek); + Assert.Equal(data, stream.ReadBytes(data.Length)); + } + + [Fact] + public void ReadBytesCompletesPartialReads() + { + byte[] data = { 1, 2, 3, 4, 5, 6 }; + using StreamIO stream = new StreamIO(new PartialReadMemoryStream(data)); + + Assert.Equal(data, stream.ReadBytes(data.Length)); + } + + [Fact] + public async Task ReadBytesAsyncCompletesPartialReads() + { + byte[] data = { 1, 2, 3, 4, 5, 6 }; + using StreamIO stream = new StreamIO(new PartialReadMemoryStream(data)); + + Assert.Equal(data, await stream.ReadBytesAsync(data.Length)); + } + [Fact] public void WriteAndReadString_WorksCorrectly() { @@ -206,4 +271,4 @@ public void ReadBytes_ThrowsOnEndOfStream() { Assert.Throws(() => _streamIO.ReadBytes(1)); } -} \ No newline at end of file +} diff --git a/CSUtilities/IO/StreamIO.cs b/CSUtilities/IO/StreamIO.cs index fbf56b2..6e06a19 100644 --- a/CSUtilities/IO/StreamIO.cs +++ b/CSUtilities/IO/StreamIO.cs @@ -59,17 +59,36 @@ public StreamIO(string filename, FileMode mode, FileAccess access) /// public StreamIO(Stream stream, bool createCopy, bool resetPosition) { - long position = stream.Position; + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } - //Check if supports seeking + //Readers require seeking. A non-seekable input therefore has to be buffered from its + //current position; querying Position or Length first would itself throw. if (!stream.CanSeek || createCopy) { - stream.Position = 0; - //Create a copy of the stream to allow seeking - byte[] buffer = new byte[stream.Length]; - stream.Read(buffer, 0, buffer.Length); - _stream = (Stream)new MemoryStream(buffer); - stream.Position = position; + MemoryStream copy = new MemoryStream(); + if (stream.CanSeek) + { + long position = stream.Position; + try + { + stream.Position = 0; + stream.CopyTo(copy); + } + finally + { + stream.Position = position; + } + } + else + { + stream.CopyTo(copy); + } + + copy.Position = 0; + _stream = copy; } else { @@ -116,14 +135,15 @@ public byte[] GetBytes(int offset, int length) //Set the position to the begining this.Position = offset; - byte[] buffer = this.ReadBytes(length); - //if (this.m_stream.Read(buffer, offset, length) < length) - // throw new EndOfStreamException(); - - //Reset the position - this.Position = save; - - return buffer; + try + { + return this.ReadBytes(length); + } + finally + { + //A failed read must not leave this utility at the requested offset. + this.Position = save; + } } public async Task GetBytesAsync(int offset, int length) @@ -136,14 +156,14 @@ public async Task GetBytesAsync(int offset, int length) //Set the position to the begining this.Position = offset; - byte[] buffer = await this.ReadBytesAsync(length); - //if (this.m_stream.Read(buffer, offset, length) < length) - // throw new EndOfStreamException(); - - //Reset the position - this.Position = save; - - return buffer; + try + { + return await this.ReadBytesAsync(length); + } + finally + { + this.Position = save; + } } /// @@ -163,9 +183,15 @@ public byte LookByte() /// public byte[] LookBytes(int count) { - byte[] bs = this.ReadBytes(count); - this.Position -= count; - return bs; + long position = this.Position; + try + { + return this.ReadBytes(count); + } + finally + { + this.Position = position; + } } /// @@ -214,8 +240,17 @@ public virtual byte[] ReadBytes(int length) byte[] buffer = new byte[length]; - if (this._stream.Read(buffer, 0, length) < length) - throw new EndOfStreamException(); + int offset = 0; + while (offset < length) + { + int read = this._stream.Read(buffer, offset, length - offset); + if (read == 0) + { + throw new EndOfStreamException(); + } + + offset += read; + } return buffer; } @@ -227,8 +262,17 @@ public virtual async Task ReadBytesAsync(int length, CancellationToken c byte[] buffer = new byte[length]; - if (await this._stream.ReadAsync(buffer, 0, length, cancellationToken) < length) - throw new EndOfStreamException(); + int offset = 0; + while (offset < length) + { + int read = await this._stream.ReadAsync(buffer, offset, length - offset, cancellationToken); + if (read == 0) + { + throw new EndOfStreamException(); + } + + offset += read; + } return buffer; } @@ -520,4 +564,4 @@ public void Dispose() { _stream.Dispose(); } -} \ No newline at end of file +}