Skip to content

Commit 839fd64

Browse files
authored
Merge pull request #520 from CommunityToolkit/dev/fix-stream-read-of-t
Fix StreamExtensions.Read<T> when using buffered streams
2 parents 6fff80d + 7eef9f2 commit 839fd64

File tree

2 files changed

+104
-8
lines changed

2 files changed

+104
-8
lines changed

src/CommunityToolkit.HighPerformance/Extensions/StreamExtensions.cs

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -207,27 +207,45 @@ public static unsafe T Read<T>(this Stream stream)
207207
{
208208
#if NETSTANDARD2_1_OR_GREATER
209209
T result = default;
210-
int length = sizeof(T);
210+
int bytesOffset = 0;
211211

212-
unsafe
212+
// As per Stream.Read's documentation:
213+
// "The total number of bytes read into the buffer. This can be less than the number of bytes allocated in the
214+
// buffer if that many bytes are not currently available, or zero (0) if the end of the stream has been reached."
215+
// Because of this, we have to loop until all requires bytes have been read, and only throw if the return is 0.
216+
do
213217
{
214-
if (stream.Read(new Span<byte>(&result, length)) != length)
218+
int bytesRead = stream.Read(new Span<byte>((byte*)&result + bytesOffset, sizeof(T) - bytesOffset));
219+
220+
// A return value of 0 indicates that the end of the stream has been reached
221+
if (bytesRead == 0)
215222
{
216223
ThrowInvalidOperationExceptionForEndOfStream();
217224
}
225+
226+
bytesOffset += bytesRead;
218227
}
228+
while (bytesOffset < sizeof(T));
219229

220230
return result;
221231
#else
222-
int length = sizeof(T);
223-
byte[] buffer = ArrayPool<byte>.Shared.Rent(length);
232+
int bytesOffset = 0;
233+
byte[] buffer = ArrayPool<byte>.Shared.Rent(sizeof(T));
224234

225235
try
226236
{
227-
if (stream.Read(buffer, 0, length) != length)
237+
do
228238
{
229-
ThrowInvalidOperationExceptionForEndOfStream();
239+
int bytesRead = stream.Read(buffer, bytesOffset, sizeof(T) - bytesOffset);
240+
241+
if (bytesRead == 0)
242+
{
243+
ThrowInvalidOperationExceptionForEndOfStream();
244+
}
245+
246+
bytesOffset += bytesRead;
230247
}
248+
while (bytesOffset < sizeof(T));
231249

232250
return Unsafe.ReadUnaligned<T>(ref buffer[0]);
233251
}

tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_StreamExtensions.cs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
using System;
66
using System.IO;
7-
using CommunityToolkit.HighPerformance;
87
using Microsoft.VisualStudio.TestTools.UnitTesting;
98

109
namespace CommunityToolkit.HighPerformance.UnitTests.Extensions;
@@ -37,4 +36,83 @@ public void Test_StreamExtensions_ReadWrite()
3736

3837
_ = Assert.ThrowsException<InvalidOperationException>(() => stream.Read<long>());
3938
}
39+
40+
// See https://github.com/CommunityToolkit/dotnet/issues/513
41+
[TestMethod]
42+
public void Test_StreamExtensions_ReadWrite_WithBufferedStream()
43+
{
44+
Stream stream = new BufferedStream();
45+
46+
stream.Write(true);
47+
stream.Write(42);
48+
stream.Write(3.14f);
49+
stream.Write(unchecked(uint.MaxValue * 324823489204ul));
50+
51+
stream.Position = 0;
52+
53+
Assert.AreEqual(true, stream.Read<bool>());
54+
Assert.AreEqual(42, stream.Read<int>());
55+
Assert.AreEqual(3.14f, stream.Read<float>());
56+
Assert.AreEqual(unchecked(uint.MaxValue * 324823489204ul), stream.Read<ulong>());
57+
58+
_ = Assert.ThrowsException<InvalidOperationException>(() => stream.Read<long>());
59+
}
60+
61+
private sealed class BufferedStream : MemoryStream
62+
{
63+
private ReadOnlyMemory<byte> bufferedBytes;
64+
65+
public override int Read(byte[] buffer, int offset, int count)
66+
{
67+
if (this.bufferedBytes.IsEmpty)
68+
{
69+
this.bufferedBytes = ReadMoreBytes();
70+
}
71+
72+
int bytesToCopy = Math.Min(this.bufferedBytes.Length, count);
73+
74+
this.bufferedBytes.Span.Slice(0, bytesToCopy).CopyTo(buffer.AsSpan(offset, count));
75+
this.bufferedBytes = this.bufferedBytes.Slice(bytesToCopy);
76+
77+
return bytesToCopy;
78+
}
79+
80+
#if NET6_0_OR_GREATER
81+
public override int Read(Span<byte> buffer)
82+
{
83+
if (this.bufferedBytes.IsEmpty)
84+
{
85+
this.bufferedBytes = ReadMoreBytes();
86+
}
87+
88+
int bytesToCopy = Math.Min(this.bufferedBytes.Length, buffer.Length);
89+
90+
this.bufferedBytes.Span.Slice(0, bytesToCopy).CopyTo(buffer);
91+
this.bufferedBytes = this.bufferedBytes.Slice(bytesToCopy);
92+
93+
return bytesToCopy;
94+
}
95+
#endif
96+
97+
private byte[] ReadMoreBytes()
98+
{
99+
byte[] array = new byte[3];
100+
int bytesOffset = 0;
101+
102+
do
103+
{
104+
int bytesRead = base.Read(array, bytesOffset, 3 - bytesOffset);
105+
106+
bytesOffset += bytesRead;
107+
108+
if (bytesRead == 0)
109+
{
110+
return array.AsSpan(0, bytesOffset).ToArray();
111+
}
112+
}
113+
while (bytesOffset < 3);
114+
115+
return array;
116+
}
117+
}
40118
}

0 commit comments

Comments
 (0)