Skip to content

Commit e4d5f41

Browse files
paulirwinCopilot
andauthored
Fix Stream.ReadExactly CA2022 warning; optimize for stackalloc/arraypool (#1254)
* Fix Stream.ReadExactly CA2022 warning; optimize for stackalloc/arraypool * PR feedback * Fix compilation issue with test on .NET Framework * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 217c1e4 commit e4d5f41

3 files changed

Lines changed: 404 additions & 69 deletions

File tree

Directory.Build.targets

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,17 @@
3838
<PublishDir Condition="'$(AlternatePublishRootDirectory)' != ''">$(AlternatePublishRootDirectory)/$(TargetFramework)/$(MSBuildProjectName)/</PublishDir>
3939
</PropertyGroup>
4040

41-
<!-- Features in .NET 9.x+ only -->
42-
<PropertyGroup Condition=" $(TargetFramework.StartsWith('net9.')) Or $(TargetFramework.StartsWith('net10.')) ">
41+
<!-- Features in .NET 8.x+ only -->
42+
<PropertyGroup Condition=" $(TargetFramework.StartsWith('net8.')) Or $(TargetFramework.StartsWith('net9.')) Or $(TargetFramework.StartsWith('net10.')) ">
4343

44-
<DefineConstants>$(DefineConstants);FEATURE_STREAM_READEXACTLY</DefineConstants>
44+
<DefineConstants>$(DefineConstants);FEATURE_ASPNETCORE_TESTHOST</DefineConstants>
4545

4646
</PropertyGroup>
4747

48-
<!-- Features in .NET 8.x+ only -->
49-
<PropertyGroup Condition=" $(TargetFramework.StartsWith('net8.')) Or $(TargetFramework.StartsWith('net9.')) Or $(TargetFramework.StartsWith('net10.')) ">
48+
<!-- Features in .NET 7.x+ only -->
49+
<PropertyGroup Condition=" $(TargetFramework.StartsWith('net7.')) Or $(TargetFramework.StartsWith('net8.')) Or $(TargetFramework.StartsWith('net9.')) Or $(TargetFramework.StartsWith('net10.')) ">
5050

51-
<DefineConstants>$(DefineConstants);FEATURE_ASPNETCORE_TESTHOST</DefineConstants>
51+
<DefineConstants>$(DefineConstants);FEATURE_STREAM_READEXACTLY</DefineConstants>
5252

5353
</PropertyGroup>
5454

src/Lucene.Net.Tests/Support/IO/TestStreamExtensions.cs

Lines changed: 224 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,168 @@ public void TestRead_Span()
7878
Assert.IsTrue(Encoding.UTF8.GetString(buffer).Equals(fileString));
7979
}
8080

81+
#if !FEATURE_STREAM_READEXACTLY
82+
[Test]
83+
public void TestReadExactly_ZeroLength()
84+
{
85+
using var ms = new MemoryStream();
86+
Span<byte> buffer = Array.Empty<byte>();
87+
ms.ReadExactly(buffer); // should succeed
88+
}
89+
90+
[Test]
91+
public void TestReadExactly_Success_FromStart()
92+
{
93+
var bytes = new byte[] { 1, 2, 3, 4 };
94+
using var ms = new MemoryStream(bytes);
95+
96+
Span<byte> buffer = stackalloc byte[2];
97+
ms.ReadExactly(buffer);
98+
99+
Assert.AreEqual((byte)1, buffer[0]);
100+
Assert.AreEqual((byte)2, buffer[1]);
101+
}
102+
103+
[Test]
104+
public void TestReadExactly_Success_FromMiddle()
105+
{
106+
var bytes = new byte[] { 1, 2, 3, 4 };
107+
using var ms = new MemoryStream(bytes);
108+
ms.Seek(2, SeekOrigin.Begin);
109+
110+
Span<byte> buffer = stackalloc byte[2];
111+
ms.ReadExactly(buffer);
112+
113+
Assert.AreEqual((byte)3, buffer[0]);
114+
Assert.AreEqual((byte)4, buffer[1]);
115+
}
116+
117+
[Test]
118+
public void TestReadExactly_Success_IntoMiddle()
119+
{
120+
var bytes = new byte[] { 1, 2, 3, 4 };
121+
using var ms = new MemoryStream(bytes);
122+
123+
Span<byte> buffer = stackalloc byte[4];
124+
ms.ReadExactly(buffer.Slice(2));
125+
126+
Assert.AreEqual((byte)1, buffer[2]);
127+
Assert.AreEqual((byte)2, buffer[3]);
128+
}
129+
130+
[Test]
131+
public void TestReadExactly_EndOfStream()
132+
{
133+
var bytes = new byte[] { 1, 2, 3, 4 };
134+
135+
Assert.Throws<EndOfStreamException>(() =>
136+
{
137+
using var ms = new MemoryStream(bytes);
138+
139+
Span<byte> buffer = stackalloc byte[5];
140+
ms.ReadExactly(buffer);
141+
});
142+
}
143+
144+
[Test]
145+
public void TestReadExactly_PartialReads()
146+
{
147+
var bytes = new byte[] { 1, 2, 3, 4 };
148+
var partialStream = new MaxBytesPerReadStream(bytes, maxBytesPerRead: 1);
149+
150+
Span<byte> buffer = stackalloc byte[4];
151+
partialStream.ReadExactly(buffer);
152+
153+
Assert.AreEqual((byte)1, buffer[0]);
154+
Assert.AreEqual((byte)2, buffer[1]);
155+
Assert.AreEqual((byte)3, buffer[2]);
156+
Assert.AreEqual((byte)4, buffer[3]);
157+
}
158+
159+
[Test]
160+
public async Task TestReadExactlyAsync_ZeroLength()
161+
{
162+
using var ms = new MemoryStream();
163+
var buffer = Array.Empty<byte>();
164+
await ms.ReadExactlyAsync(buffer, 0, 0); // should succeed
165+
}
166+
167+
[Test]
168+
public async Task TestReadExactlyAsync_Success_FromStart()
169+
{
170+
var bytes = new byte[] { 1, 2, 3, 4 };
171+
using var ms = new MemoryStream(bytes);
172+
173+
var buffer = new byte[2];
174+
await ms.ReadExactlyAsync(buffer, 0, 2);
175+
176+
Assert.AreEqual((byte)1, buffer[0]);
177+
Assert.AreEqual((byte)2, buffer[1]);
178+
}
179+
180+
[Test]
181+
public async Task TestReadExactlyAsync_Success_FromMiddle()
182+
{
183+
var bytes = new byte[] { 1, 2, 3, 4 };
184+
using var ms = new MemoryStream(bytes);
185+
ms.Seek(2, SeekOrigin.Begin);
186+
187+
var buffer = new byte[2];
188+
await ms.ReadExactlyAsync(buffer, 0, 2);
189+
190+
Assert.AreEqual((byte)3, buffer[0]);
191+
Assert.AreEqual((byte)4, buffer[1]);
192+
}
193+
194+
[Test]
195+
public async Task TestReadExactlyAsync_Success_IntoMiddle()
196+
{
197+
var bytes = new byte[] { 1, 2, 3, 4 };
198+
using var ms = new MemoryStream(bytes);
199+
200+
var buffer = new byte[4];
201+
await ms.ReadExactlyAsync(buffer, 2, 2);
202+
203+
Assert.AreEqual((byte)1, buffer[2]);
204+
Assert.AreEqual((byte)2, buffer[3]);
205+
}
206+
207+
[Test]
208+
public async Task TestReadExactlyAsync_EndOfStream()
209+
{
210+
var bytes = new byte[] { 1, 2, 3, 4 };
211+
212+
try
213+
{
214+
using var ms = new MemoryStream(bytes);
215+
216+
var buffer = new byte[5];
217+
await ms.ReadExactlyAsync(buffer, 0, 5);
218+
219+
Assert.Fail("Should have thrown an exception");
220+
}
221+
catch (EndOfStreamException)
222+
{
223+
Assert.Pass("Expected EndOfStreamException thrown");
224+
}
225+
}
226+
227+
[Test]
228+
public async Task TestReadExactlyAsync_PartialReads()
229+
{
230+
var bytes = new byte[] { 1, 2, 3, 4 };
231+
var partialStream = new MaxBytesPerReadStream(bytes, maxBytesPerRead: 1);
232+
233+
var buffer = new byte[4];
234+
await partialStream.ReadExactlyAsync(buffer, 0, 4);
235+
236+
Assert.AreEqual((byte)1, buffer[0]);
237+
Assert.AreEqual((byte)2, buffer[1]);
238+
Assert.AreEqual((byte)3, buffer[2]);
239+
Assert.AreEqual((byte)4, buffer[3]);
240+
}
241+
#endif
242+
81243
[Test]
82244
public void TestWrite_Span()
83245
{
@@ -237,7 +399,7 @@ public async Task TestReadUTFAsync()
237399
/// Helper method to calculate expected UTF stream length for validation
238400
/// Matches DataOutput.writeUTF() spec (Java)
239401
/// </summary>
240-
private long CalculateExpectedUTFStreamLength(string value)
402+
private static long CalculateExpectedUTFStreamLength(string value)
241403
{
242404
long utfCount = 0;
243405
foreach (char ch in value)
@@ -260,10 +422,71 @@ private void ResetStreamForReading() // LUCENENET - was "OpenDataInputStream" in
260422
stream.Position = 0;
261423
}
262424

425+
// Partial-read async tests: verify that async read methods handle
426+
// streams that return fewer bytes than requested per call.
427+
428+
[Test]
429+
public async Task TestReadInt32BigEndianAsync_PartialReads()
430+
{
431+
await stream.WriteInt32BigEndianAsync(768347202);
432+
var partialStream = new MaxBytesPerReadStream(((MemoryStream)stream).ToArray(), maxBytesPerRead: 1);
433+
434+
int result = await partialStream.ReadInt32BigEndianAsync();
435+
Assert.AreEqual(768347202, result, "Incorrect int read with partial reads (async)");
436+
}
437+
438+
[Test]
439+
public async Task TestReadInt64BigEndianAsync_PartialReads()
440+
{
441+
await stream.WriteInt64BigEndianAsync(9875645283333L);
442+
var partialStream = new MaxBytesPerReadStream(((MemoryStream)stream).ToArray(), maxBytesPerRead: 1);
443+
444+
long result = await partialStream.ReadInt64BigEndianAsync();
445+
Assert.AreEqual(9875645283333L, result, "Incorrect long read with partial reads (async)");
446+
}
447+
448+
[Test]
449+
public async Task TestReadUTFAsync_PartialReads()
450+
{
451+
await stream.WriteUTFAsync(unihw);
452+
var partialStream = new MaxBytesPerReadStream(((MemoryStream)stream).ToArray(), maxBytesPerRead: 1);
453+
454+
string result = await partialStream.ReadUTFAsync();
455+
Assert.AreEqual(unihw, result, "Incorrect string read with partial reads (async)");
456+
}
457+
263458
public override void SetUp()
264459
{
265460
base.SetUp();
266461
stream = new MemoryStream();
267462
}
268463
}
464+
465+
/// <summary>
466+
/// A stream wrapper that returns at most <c>maxBytesPerRead</c> bytes per
467+
/// <see cref="ReadAsync(byte[], int, int, System.Threading.CancellationToken)"/> call,
468+
/// simulating partial reads (e.g. network streams).
469+
/// </summary>
470+
internal sealed class MaxBytesPerReadStream : MemoryStream
471+
{
472+
private readonly int maxBytesPerRead;
473+
474+
public MaxBytesPerReadStream(byte[] data, int maxBytesPerRead)
475+
: base(data, writable: false)
476+
{
477+
this.maxBytesPerRead = maxBytesPerRead;
478+
}
479+
480+
public override int Read(byte[] buffer, int offset, int count)
481+
{
482+
int clamped = Math.Min(count, maxBytesPerRead);
483+
return base.Read(buffer, offset, clamped);
484+
}
485+
486+
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken)
487+
{
488+
int clamped = Math.Min(count, maxBytesPerRead);
489+
return base.ReadAsync(buffer, offset, clamped, cancellationToken);
490+
}
491+
}
269492
}

0 commit comments

Comments
 (0)