Skip to content

Commit 4d875d1

Browse files
committed
Patch 5.18.1 for DotNext.IO
1 parent 04b91db commit 4d875d1

File tree

6 files changed

+157
-45
lines changed

6 files changed

+157
-45
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ Release Notes
1515
* Synchronous `TryAcquire` implemented by `AsyncExclusiveLock` and `AsyncReaderWriterLock` are now implemented in portable way. Previously, WASM target was not supported. Additionally, the method supports lock stealing
1616
* * Improved synchronous support for `RandomAccessCache` class
1717

18-
<a href="https://www.nuget.org/packages/dotnext.io/5.18.0">DotNext.IO 5.18.0</a>
18+
<a href="https://www.nuget.org/packages/dotnext.io/5.18.1">DotNext.IO 5.18.1</a>
1919
* Fixed issue of `PoolingBufferedStream` class when the stream has buffered bytes in the write buffer and `Position` is set to backward
20+
* Fixed [256](https://github.com/dotnet/dotNext/issues/256)
2021

2122
<a href="https://www.nuget.org/packages/dotnext.net.cluster/5.18.0">DotNext.Net.Cluster 5.18.0</a>
2223
* Updated dependencies

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,9 @@ Release Date: 01-20-2025
5959
* Synchronous `TryAcquire` implemented by `AsyncExclusiveLock` and `AsyncReaderWriterLock` are now implemented in portable way. Previously, WASM target was not supported. Additionally, the method supports lock stealing
6060
* Improved synchronous support for `RandomAccessCache` class
6161

62-
<a href="https://www.nuget.org/packages/dotnext.io/5.18.0">DotNext.IO 5.18.0</a>
62+
<a href="https://www.nuget.org/packages/dotnext.io/5.18.1">DotNext.IO 5.18.1</a>
6363
* Fixed issue of `PoolingBufferedStream` class when the stream has buffered bytes in the write buffer and `Position` is set to backward
64+
* Fixed [256](https://github.com/dotnet/dotNext/issues/256)
6465

6566
<a href="https://www.nuget.org/packages/dotnext.net.cluster/5.18.0">DotNext.Net.Cluster 5.18.0</a>
6667
* Updated dependencies

src/DotNext.IO/DotNext.IO.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<Authors>.NET Foundation and Contributors</Authors>
1212
<Company />
1313
<Product>.NEXT Family of Libraries</Product>
14-
<VersionPrefix>5.18.0</VersionPrefix>
14+
<VersionPrefix>5.18.1</VersionPrefix>
1515
<VersionSuffix></VersionSuffix>
1616
<AssemblyName>DotNext.IO</AssemblyName>
1717
<PackageLicenseExpression>MIT</PackageLicenseExpression>

src/DotNext.IO/IO/FileWriter.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -239,19 +239,17 @@ public void Write()
239239

240240
private void WriteSlow(ReadOnlySpan<byte> input)
241241
{
242+
RandomAccess.Write(handle, WrittenBuffer.Span, fileOffset);
243+
fileOffset += bufferOffset;
244+
242245
if (input.Length >= maxBufferSize)
243246
{
244-
RandomAccess.Write(handle, WrittenBuffer.Span, fileOffset);
245-
fileOffset += bufferOffset;
246-
247247
RandomAccess.Write(handle, input, fileOffset);
248248
fileOffset += input.Length;
249249
Reset();
250250
}
251251
else
252252
{
253-
RandomAccess.Write(handle, WrittenBuffer.Span, fileOffset);
254-
fileOffset += bufferOffset;
255253
input.CopyTo(EnsureBufferAllocated().Span);
256254
bufferOffset += input.Length;
257255
}

src/DotNext.IO/IO/PoolingBufferedStream.cs

Lines changed: 78 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ public override int WriteTimeout
9494
}
9595

9696
/// <inheritdoc/>
97+
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
9798
public override long Length
9899
{
99100
get
@@ -156,6 +157,7 @@ private void EnsureReadBufferIsEmpty()
156157
}
157158

158159
/// <inheritdoc/>
160+
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
159161
Memory<byte> IBufferedWriter.Buffer
160162
{
161163
get
@@ -302,23 +304,29 @@ private void WriteCore(ReadOnlySpan<byte> data)
302304

303305
if (writePosition is 0)
304306
ClearReadBufferBeforeWrite();
305-
307+
306308
var freeBuf = EnsureBufferAllocated().Span.Slice(writePosition);
307309

308-
// drain buffered data if needed
309-
if (freeBuf.Length < data.Length)
310-
WriteCore();
311-
312-
// if internal buffer has not enough space then just write through
313-
if (data.Length > freeBuf.Length)
310+
if (data.Length <= freeBuf.Length)
314311
{
315-
stream.Write(data);
316-
Reset();
312+
data.CopyTo(freeBuf);
313+
writePosition += data.Length;
317314
}
318-
else
315+
else if (data.Length < maxBufferSize)
319316
{
317+
data.CopyTo(freeBuf, out var bytesWritten);
318+
stream.Write(freeBuf = buffer.Span);
319+
data = data.Slice(bytesWritten);
320320
data.CopyTo(freeBuf);
321-
writePosition += data.Length;
321+
writePosition = data.Length;
322+
323+
Debug.Assert(writePosition > 0);
324+
}
325+
else
326+
{
327+
WriteCore();
328+
stream.Write(data);
329+
Reset();
322330
}
323331
}
324332

@@ -360,35 +368,64 @@ public override ValueTask WriteAsync(ReadOnlyMemory<byte> data, CancellationToke
360368
return task;
361369
}
362370

363-
private async ValueTask WriteCoreAsync(ReadOnlyMemory<byte> data, CancellationToken token)
371+
private ValueTask WriteCoreAsync(ReadOnlyMemory<byte> data, CancellationToken token)
364372
{
365373
Debug.Assert(stream is not null);
366374

367375
if (writePosition is 0)
368376
ClearReadBufferBeforeWrite();
369377

370-
var freeBuf = EnsureBufferAllocated().Memory.Slice(writePosition);
371-
372-
// drain buffered data if needed
373-
if (freeBuf.Length < data.Length)
378+
var freeCapacity = maxBufferSize - writePosition;
379+
380+
ValueTask task;
381+
if (data.Length <= freeCapacity)
374382
{
375-
await WriteCoreAsync(out _, token).ConfigureAwait(false);
376-
freeBuf = buffer.Memory.Slice(writePosition);
383+
data.CopyTo(EnsureBufferAllocated().Memory.Slice(writePosition));
384+
writePosition += data.Length;
385+
task = ValueTask.CompletedTask;
377386
}
378-
379-
// if internal buffer has not enough space then just write through
380-
if (data.Length > freeBuf.Length)
387+
else if (data.Length < maxBufferSize)
381388
{
382-
await stream.WriteAsync(data, token).ConfigureAwait(false);
383-
Reset();
389+
task = CopyAndWriteAsync(data, token);
390+
}
391+
else if (writePosition is 0)
392+
{
393+
task = stream.WriteAsync(data, token);
384394
}
385395
else
386396
{
387-
data.CopyTo(freeBuf);
388-
writePosition += data.Length;
397+
task = WriteWithBufferAsync(data, token);
389398
}
399+
400+
return task;
390401
}
391-
402+
403+
private async ValueTask CopyAndWriteAsync(ReadOnlyMemory<byte> data, CancellationToken token)
404+
{
405+
Debug.Assert(stream is not null);
406+
Debug.Assert(data.Length < maxBufferSize);
407+
408+
var writeBuffer = buffer.Memory;
409+
data.Span.CopyTo(writeBuffer.Span.Slice(writePosition), out var bytesWritten);
410+
await stream.WriteAsync(writeBuffer, token).ConfigureAwait(false);
411+
data = data.Slice(bytesWritten);
412+
data.CopyTo(writeBuffer);
413+
writePosition = data.Length;
414+
415+
Debug.Assert(writePosition > 0);
416+
}
417+
418+
private async ValueTask WriteWithBufferAsync(ReadOnlyMemory<byte> data, CancellationToken token)
419+
{
420+
Debug.Assert(stream is not null);
421+
Debug.Assert(data.Length >= maxBufferSize);
422+
Debug.Assert(writePosition > 0);
423+
424+
await stream.WriteAsync(WrittenMemory, token).ConfigureAwait(false);
425+
await stream.WriteAsync(data, token).ConfigureAwait(false);
426+
Reset();
427+
}
428+
392429
/// <inheritdoc/>
393430
public override Task WriteAsync(byte[] data, int offset, int count, CancellationToken token)
394431
=> WriteAsync(new ReadOnlyMemory<byte>(data, offset, count), token).AsTask();
@@ -452,20 +489,24 @@ private int ReadCore(Span<byte> data)
452489

453490
if (data.IsEmpty)
454491
{
455-
// nothing to do
492+
if (readPosition == readLength)
493+
Reset();
456494
}
457-
else if (data.Length > MaxBufferSize)
495+
else if (data.Length >= maxBufferSize)
458496
{
497+
Debug.Assert(readPosition == readLength);
498+
459499
bytesRead += stream.Read(data);
500+
Reset();
460501
}
461502
else
462503
{
504+
Debug.Assert(readPosition == readLength);
505+
506+
readPosition = 0;
463507
readLength = stream.Read(EnsureBufferAllocated().Span);
464508
bytesRead += ReadFromBuffer(data);
465509
}
466-
467-
if (readPosition == readLength)
468-
Reset();
469510

470511
return bytesRead;
471512
}
@@ -526,22 +567,21 @@ private async ValueTask<int> ReadCoreAsync(Memory<byte> data, CancellationToken
526567

527568
if (data.IsEmpty)
528569
{
529-
// nothing to do
570+
if (readPosition == readLength)
571+
Reset();
530572
}
531-
else if (data.Length > MaxBufferSize)
573+
else if (data.Length >= maxBufferSize)
532574
{
533-
Debug.Assert(readPosition == readLength);
534575
bytesRead += await stream.ReadAsync(data, token).ConfigureAwait(false);
576+
Reset();
535577
}
536578
else
537579
{
538580
Debug.Assert(readPosition == readLength);
581+
readPosition = 0;
539582
readLength = await stream.ReadAsync(EnsureBufferAllocated().Memory, token).ConfigureAwait(false);
540583
bytesRead += ReadFromBuffer(data.Span);
541584
}
542-
543-
if (readPosition == readLength)
544-
Reset();
545585

546586
return bytesRead;
547587
}
@@ -740,6 +780,7 @@ private void EnsureWriteBufferIsEmpty()
740780
}
741781

742782
/// <inheritdoc/>
783+
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
743784
ReadOnlyMemory<byte> IBufferedReader.Buffer
744785
{
745786
get

src/DotNext.Tests/IO/PoolingBufferedStreamTests.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,4 +389,75 @@ public static void OverwriteStream()
389389
buffered.Write("text"u8);
390390
Equal(4L, buffered.Position);
391391
}
392+
393+
[Fact]
394+
public static async Task RegressionIssue256Async()
395+
{
396+
const int dataSize = 128 + 3105 + 66 + 3111 + 66 + 3105 + 66 + 2513 + 128;
397+
ReadOnlyMemory<byte> expected = RandomBytes(dataSize);
398+
await using var ms = new MemoryStream();
399+
400+
await using (var buffered = new PoolingBufferedStream(ms, leaveOpen: true) { MaxBufferSize = 8192 })
401+
{
402+
await buffered.WriteAsync(expected);
403+
await buffered.FlushAsync();
404+
}
405+
406+
ms.Position = 0;
407+
await using (var reader = new PoolingBufferedStream(ms, leaveOpen: true) { MaxBufferSize = 4096 })
408+
{
409+
Memory<byte> buffer = new byte[dataSize];
410+
await reader.ReadExactlyAsync(buffer.Slice(0, 3175));
411+
reader.Position = 3303;
412+
await reader.ReadExactlyAsync(buffer.Slice(0, 3107));
413+
Equal(expected.Slice(3303, 3107), buffer.Slice(0, 3107));
414+
}
415+
}
416+
417+
[Fact]
418+
public static void RegressionIssue256()
419+
{
420+
const int dataSize = 128 + 3105 + 66 + 3111 + 66 + 3105 + 66 + 2513 + 128;
421+
ReadOnlySpan<byte> expected = RandomBytes(dataSize);
422+
using var ms = new MemoryStream();
423+
424+
using (var buffered = new PoolingBufferedStream(ms, leaveOpen: true) { MaxBufferSize = 8192 })
425+
{
426+
buffered.Write(expected);
427+
buffered.Flush();
428+
}
429+
430+
ms.Position = 0;
431+
using (var reader = new PoolingBufferedStream(ms, leaveOpen: true) { MaxBufferSize = 4096 })
432+
{
433+
Span<byte> buffer = new byte[dataSize];
434+
reader.ReadExactly(buffer.Slice(0, 3175));
435+
reader.Position = 3303;
436+
reader.ReadExactly(buffer.Slice(0, 3107));
437+
Equal(expected.Slice(3303, 3107), buffer.Slice(0, 3107));
438+
}
439+
}
440+
441+
[Fact]
442+
public static void RepeatableReads()
443+
{
444+
var bytes = RandomBytes(128);
445+
using var reader = new PoolingBufferedStream(new MemoryStream(bytes)) { MaxBufferSize = 256 };
446+
True(reader.Read());
447+
False(reader.Read());
448+
True(reader.HasBufferedDataToRead);
449+
450+
Equal(bytes, reader.As<IBufferedReader>().Buffer);
451+
}
452+
453+
[Fact]
454+
public static void ReadEmpty()
455+
{
456+
var bytes = RandomBytes(128);
457+
using var reader = new PoolingBufferedStream(new MemoryStream(bytes)) { MaxBufferSize = 256 };
458+
True(reader.Read());
459+
reader.ReadExactly(new byte[bytes.Length]);
460+
False(reader.Read());
461+
False(reader.HasBufferedDataToRead);
462+
}
392463
}

0 commit comments

Comments
 (0)