diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f56d86612..c36208dc3e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,42 @@
Release Notes
====
+# 12-29-2024
+This release is aimed to improve AOT compatibility. All the examples in the repo are now AOT compatible.
+DotNext 5.17.0
+* Fixed AOT compatibility in `TaskType` class
+* Added [ISpanFormattable](https://learn.microsoft.com/en-us/dotnet/api/system.ispanformattable) and [IParsable<T>](https://learn.microsoft.com/en-us/dotnet/api/system.iparsable-1) interfaces to `HttpEndPoint`
+* Introduced `TryEncodeAsUtf8` extension method for `SpanWriter`
+* Added more factory methods to `DotNext.Buffers.Memory` class to create [ReadOnlySequence<T>](https://learn.microsoft.com/en-us/dotnet/api/system.buffers.readonlysequence-1)
+* `Intrinsics.KeepAlive` is introduced for value types
+* Added `Synchronization.Wait()` synchronous methods for blocking wait of [value tasks](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask) without wait handles
+
+DotNext.Metaprogramming 5.17.0
+* Updated dependencies
+
+DotNext.Unsafe 5.17.0
+* Improved AOT support
+* Fixed finalizer for unmanaged memory manager that allows to release the allocated unmanaged memory automatically by GC to avoid memory leak
+* Updated dependencies
+
+DotNext.Threading 5.17.0
+* Improved AOT support
+
+DotNext.IO 5.17.0
+* Reduced memory consumption for applications that use `FileReader` and `FileWriter` classes. These classes are now implemented by using lazy buffer pattern. It means that the different instances can reuse the same buffer taken from the pool
+* Fixed [255](https://github.com/dotnet/dotNext/issues/255)
+* `PoolingBufferedStream` is introduced to replace classic [BufferedStream](https://learn.microsoft.com/en-us/dotnet/api/system.io.bufferedstream). This class supports memory pooling and implements lazy buffer pattern
+
+DotNext.Net.Cluster 5.17.0
+* Improved AOT support
+
+DotNext.AspNetCore.Cluster 5.17.0
+* Improved AOT support
+* Fixed [254](https://github.com/dotnet/dotNext/issues/254)
+
+DotNext.MaintenanceServices 0.5.0
+* Improved AOT support
+
# 12-07-2024
DotNext 5.16.1
* Added [LEB128](https://en.wikipedia.org/wiki/LEB128) encoder and decoder as a public API. See `DotNext.Buffers.Binary.Leb128` type for more information
diff --git a/README.md b/README.md
index 9cdaa12210..9cf9fed9b1 100644
--- a/README.md
+++ b/README.md
@@ -44,34 +44,42 @@ All these things are implemented in 100% managed code on top of existing .NET AP
* [NuGet Packages](https://www.nuget.org/profiles/rvsakno)
# What's new
-Release Date: 12-07-2024
-
-DotNext 5.16.1
-* Added [LEB128](https://en.wikipedia.org/wiki/LEB128) encoder and decoder as a public API. See `DotNext.Buffers.Binary.Leb128` type for more information
-* Added `SlideToEnd` method to `SpanWriter` type
-* Added `IsBitSet` and `SetBit` generic methods to `Number` type
-* Added `DetachOrCopyBuffer` to `BufferWriterSlim` type
-
-DotNext.Metaprogramming 5.16.1
+Release Date: 12-29-2024
+
+This release is aimed to improve AOT compatibility. All the examples in the repo are now AOT compatible.
+DotNext 5.17.0
+* Fixed AOT compatibility in `TaskType` class
+* Added [ISpanFormattable](https://learn.microsoft.com/en-us/dotnet/api/system.ispanformattable) and [IParsable<T>](https://learn.microsoft.com/en-us/dotnet/api/system.iparsable-1) interfaces to `HttpEndPoint`
+* Introduced `TryEncodeAsUtf8` extension method for `SpanWriter`
+* Added more factory methods to `DotNext.Buffers.Memory` class to create [ReadOnlySequence<T>](https://learn.microsoft.com/en-us/dotnet/api/system.buffers.readonlysequence-1)
+* `Intrinsics.KeepAlive` is introduced for value types
+* Added `Synchronization.Wait()` synchronous methods for blocking wait of [value tasks](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask) without wait handles
+
+DotNext.Metaprogramming 5.17.0
* Updated dependencies
-DotNext.Unsafe 5.16.1
+DotNext.Unsafe 5.17.0
+* Improved AOT support
+* Fixed finalizer for unmanaged memory manager that allows to release the allocated unmanaged memory automatically by GC to avoid memory leak
* Updated dependencies
-DotNext.Threading 5.16.1
-* Async locks with synchronous acquisition methods now throw [LockRecursionException](https://learn.microsoft.com/en-us/dotnet/api/system.threading.lockrecursionexception) if the current thread tries to acquire the lock synchronously and recursively.
-* Added support of cancellation token to synchronous acquisition methods of `AsyncExclusiveLock` and `AsyncReaderWriterLock` classes
-* Introduced `LinkTo` method overload that supports multiple cancellation tokens
+DotNext.Threading 5.17.0
+* Improved AOT support
-DotNext.IO 5.16.1
-* Introduced `RandomAccessStream` class that represents [Stream](https://learn.microsoft.com/en-us/dotnet/api/system.io.stream) wrapper over the underlying data storage that supports random access pattern
-* Added extension method for `SpanWriter` that provides length-prefixed string encoding
+DotNext.IO 5.17.0
+* Reduced memory consumption for applications that use `FileReader` and `FileWriter` classes. These classes are now implemented by using lazy buffer pattern. It means that the different instances can reuse the same buffer taken from the pool
+* Fixed [255](https://github.com/dotnet/dotNext/issues/255)
+* `PoolingBufferedStream` is introduced to replace classic [BufferedStream](https://learn.microsoft.com/en-us/dotnet/api/system.io.bufferedstream). This class supports memory pooling and implements lazy buffer pattern
-DotNext.Net.Cluster 5.16.1
-* Updated dependencies
+DotNext.Net.Cluster 5.17.0
+* Improved AOT support
-DotNext.AspNetCore.Cluster 5.16.1
-* Updated dependencies
+DotNext.AspNetCore.Cluster 5.17.0
+* Improved AOT support
+* Fixed [254](https://github.com/dotnet/dotNext/issues/254)
+
+DotNext.MaintenanceServices 0.5.0
+* Improved AOT support
Changelog for previous versions located [here](./CHANGELOG.md).
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 695220793b..ab3f7d3764 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -2,6 +2,8 @@ variables:
Solution: src/DotNext.sln
TestFolder: src/DotNext.Tests
TestProject: $(TestFolder)/DotNext.Tests.csproj
+ AotTestFolder: src/DotNext.AotTests
+ AotTestProject: $(AotTestFolder)/DotNext.AotTests.csproj
InternetAccess: false
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
isMain: $[eq(variables['Build.SourceBranch'], 'refs/heads/master')]
@@ -47,6 +49,19 @@ stages:
feedsToUse: 'config'
nugetConfigPath: 'NuGet.config'
arguments: --configuration Debug
+ - task: DotNetCoreCLI@2
+ displayName: Publish AOT Tests
+ inputs:
+ command: publish
+ publishWebProjects: false
+ zipAfterPublish: false
+ projects: $(AotTestProject)
+ arguments: --configuration Release --output $(AotTestFolder)/bin/
+ - task: CmdLine@2
+ displayName: Run AOT Tests
+ inputs:
+ workingDirectory: $(AotTestFolder)/bin/DotNext.AotTests
+ script: ./DotNext.AotTests
- task: DotNetCoreCLI@2
displayName: Test Debug
inputs:
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 8a6086688a..a9b5278073 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -28,6 +28,12 @@
+
+
+
+
+
+
diff --git a/src/DotNext.AotTests/Collections/Generic/ListTests.cs b/src/DotNext.AotTests/Collections/Generic/ListTests.cs
new file mode 100644
index 0000000000..2b1eb50da2
--- /dev/null
+++ b/src/DotNext.AotTests/Collections/Generic/ListTests.cs
@@ -0,0 +1,24 @@
+namespace DotNext.Collections.Generic;
+
+[TestClass]
+public class ListTests
+{
+ [TestMethod]
+ public void Indexer()
+ {
+ IList array = [5L, 6L, 30L];
+ Assert.AreEqual(30L, List.Indexer.Getter(array, 2));
+ List.Indexer.Setter(array, 1, 10L);
+ Assert.AreEqual(10L, array.IndexerGetter().Invoke(1));
+ array.IndexerSetter().Invoke(0, 6L);
+ Assert.AreEqual(6L, array.IndexerGetter().Invoke(0));
+ }
+
+ [TestMethod]
+ public void ReadOnlyIndexer()
+ {
+ IReadOnlyList array = [5L, 6L, 20L];
+ Assert.AreEqual(20L, List.Indexer.ReadOnly(array, 2));
+ Assert.AreEqual(6L, array.IndexerGetter().Invoke(1));
+ }
+}
\ No newline at end of file
diff --git a/src/DotNext.AotTests/DotNext.AotTests.csproj b/src/DotNext.AotTests/DotNext.AotTests.csproj
new file mode 100644
index 0000000000..7c3cc758f8
--- /dev/null
+++ b/src/DotNext.AotTests/DotNext.AotTests.csproj
@@ -0,0 +1,35 @@
+
+
+ Exe
+ net8.0
+ DotNext
+ enable
+ enable
+ 5.17.0
+ false
+ .NET Foundation and Contributors
+ .NEXT Family of Libraries
+ AOT compatibility tests for .NEXT Familiy of Libraries
+ Copyright © .NET Foundation and Contributors
+ https://github.com/dotnet/DotNext/blob/master/LICENSE
+ https://github.com/dotnet/DotNext
+ https://github.com/dotnet/DotNext.git
+ git
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DotNext.AotTests/Reflection/TaskTypeTests.cs b/src/DotNext.AotTests/Reflection/TaskTypeTests.cs
new file mode 100644
index 0000000000..ed1bfab8d0
--- /dev/null
+++ b/src/DotNext.AotTests/Reflection/TaskTypeTests.cs
@@ -0,0 +1,23 @@
+namespace DotNext.Reflection;
+
+[TestClass]
+public class TaskTypeTests
+{
+ [TestMethod]
+ public void IsCompletedSuccessfullyPropertyGetter()
+ {
+ Assert.IsTrue(TaskType.IsCompletedSuccessfullyGetter(Task.CompletedTask));
+ }
+
+ [TestMethod]
+ public void GetResultSynchronously()
+ {
+ Assert.AreEqual(42, TaskType.GetResultGetter().Invoke(Task.FromResult(42)));
+ }
+
+ [TestMethod]
+ public void IsCompletedPropertyGetter()
+ {
+ Assert.IsTrue(Task.CompletedTask.GetIsCompletedGetter().Invoke());
+ }
+}
\ No newline at end of file
diff --git a/src/DotNext.Benchmarks/DotNext.Benchmarks.csproj b/src/DotNext.Benchmarks/DotNext.Benchmarks.csproj
index 3b35466501..88bbe4db00 100644
--- a/src/DotNext.Benchmarks/DotNext.Benchmarks.csproj
+++ b/src/DotNext.Benchmarks/DotNext.Benchmarks.csproj
@@ -8,7 +8,7 @@
DotNext
DotNext.Program
false
- 5.16.1
+ 5.17.0
.NET Foundation and Contributors
.NEXT Family of Libraries
Various benchmarks demonstrating performance aspects of .NEXT extensions
diff --git a/src/DotNext.IO/Buffers/IBufferedChannel.cs b/src/DotNext.IO/Buffers/IBufferedChannel.cs
new file mode 100644
index 0000000000..fc8ea7fe74
--- /dev/null
+++ b/src/DotNext.IO/Buffers/IBufferedChannel.cs
@@ -0,0 +1,19 @@
+using System.Buffers;
+
+namespace DotNext.Buffers;
+
+///
+/// Represents buffered reader or writer.
+///
+public interface IBufferedChannel : IResettable, IDisposable
+{
+ ///
+ /// Gets buffer allocator.
+ ///
+ MemoryAllocator? Allocator { get; init; }
+
+ ///
+ /// Gets the maximum size of the internal buffer.
+ ///
+ int MaxBufferSize { get; init; }
+}
\ No newline at end of file
diff --git a/src/DotNext.IO/Buffers/IBufferedReader.cs b/src/DotNext.IO/Buffers/IBufferedReader.cs
new file mode 100644
index 0000000000..5d4849f04f
--- /dev/null
+++ b/src/DotNext.IO/Buffers/IBufferedReader.cs
@@ -0,0 +1,55 @@
+namespace DotNext.Buffers;
+
+///
+/// Represents buffered reader.
+///
+public interface IBufferedReader : IBufferedChannel
+{
+ ///
+ /// Gets unconsumed part of the buffer.
+ ///
+ ReadOnlyMemory Buffer { get; }
+
+ ///
+ /// Advances read position.
+ ///
+ /// The number of consumed bytes.
+ /// The reader has been disposed.
+ /// is larger than the length of .
+ void Consume(int count);
+
+ ///
+ /// Fetches the data from the underlying storage to the internal buffer.
+ ///
+ /// The token that can be used to cancel the operation.
+ ///
+ /// if the data has been copied from the underlying storage to the internal buffer;
+ /// if no more data to read.
+ ///
+ /// The reader has been disposed.
+ /// Internal buffer has no free space.
+ /// The operation has been canceled.
+ ValueTask ReadAsync(CancellationToken token = default);
+
+ ///
+ /// Reads the block of the memory.
+ ///
+ /// The output buffer.
+ /// The token that can be used to cancel the operation.
+ /// The number of bytes copied to .
+ /// The reader has been disposed.
+ /// The operation has been canceled.
+ async ValueTask ReadAsync(Memory destination, CancellationToken token = default)
+ {
+ var result = 0;
+ for (int bytesRead; result < destination.Length; result += bytesRead, destination = destination.Slice(bytesRead))
+ {
+ Buffer.Span.CopyTo(destination.Span, out bytesRead);
+ Consume(bytesRead);
+ if (!await ReadAsync(token).ConfigureAwait(false))
+ break;
+ }
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/src/DotNext.IO/Buffers/IBufferedWriter.cs b/src/DotNext.IO/Buffers/IBufferedWriter.cs
new file mode 100644
index 0000000000..e04d2fa475
--- /dev/null
+++ b/src/DotNext.IO/Buffers/IBufferedWriter.cs
@@ -0,0 +1,73 @@
+using System.Buffers;
+
+namespace DotNext.Buffers;
+
+///
+/// Represents buffered writer.
+///
+public interface IBufferedWriter : IBufferedChannel, IBufferWriter
+{
+ ///
+ /// Marks the specified number of bytes in the buffer as produced.
+ ///
+ /// The number of produced bytes.
+ /// is larger than the length of .
+ /// The writer has been disposed.
+ void Produce(int count);
+
+ ///
+ /// The remaining part of the internal buffer available for write.
+ ///
+ ///
+ /// The size of returned buffer may be less than or equal to .
+ ///
+ Memory Buffer { get; }
+
+ ///
+ /// Flushes buffered data to the underlying storage.
+ ///
+ /// The token that can be used to cancel the operation.
+ /// The task representing asynchronous result.
+ /// The writer has been disposed.
+ /// The operation has been canceled.
+ ValueTask WriteAsync(CancellationToken token = default);
+
+ ///
+ /// Writes the data to the underlying storage through the buffer.
+ ///
+ /// The input data to write.
+ /// The token that can be used to cancel the operation.
+ /// The task representing asynchronous result.
+ /// The object has been disposed.
+ /// The operation has been canceled.
+ async ValueTask WriteAsync(ReadOnlyMemory input, CancellationToken token = default)
+ {
+ for (int bytesWritten; !input.IsEmpty; input = input.Slice(bytesWritten))
+ {
+ input.Span.CopyTo(Buffer.Span, out bytesWritten);
+ Produce(bytesWritten);
+ await WriteAsync(token).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ void IBufferWriter.Advance(int count) => Produce(count);
+
+ ///
+ Memory IBufferWriter.GetMemory(int sizeHint)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(sizeHint);
+
+ var result = Buffer;
+ return sizeHint <= result.Length ? result : throw new InsufficientMemoryException();
+ }
+
+ ///
+ Span IBufferWriter.GetSpan(int sizeHint)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(sizeHint);
+
+ var result = Buffer.Span;
+ return sizeHint <= result.Length ? result : throw new InsufficientMemoryException();
+ }
+}
\ No newline at end of file
diff --git a/src/DotNext.IO/DotNext.IO.csproj b/src/DotNext.IO/DotNext.IO.csproj
index abca1d4222..4bda5105be 100644
--- a/src/DotNext.IO/DotNext.IO.csproj
+++ b/src/DotNext.IO/DotNext.IO.csproj
@@ -5,13 +5,13 @@
latest
enable
true
- true
+ true
nullablePublicOnly
DotNext
.NET Foundation and Contributors
.NEXT Family of Libraries
- 5.16.1
+ 5.17.0
DotNext.IO
MIT
diff --git a/src/DotNext.IO/ExceptionMessages.cs b/src/DotNext.IO/ExceptionMessages.cs
index 6a0aebb7e6..5a0cf11805 100644
--- a/src/DotNext.IO/ExceptionMessages.cs
+++ b/src/DotNext.IO/ExceptionMessages.cs
@@ -22,7 +22,9 @@ internal static string DirectoryNotFound(string path)
internal static string WriterInReadMode => (string)Resources.Get();
- internal static string NoConsumerProvided => (string)Resources.Get();
-
internal static string FileHandleClosed => (string)Resources.Get();
+
+ internal static string ReadBufferNotEmpty => (string)Resources.Get();
+
+ internal static string WriteBufferNotEmpty => (string)Resources.Get();
}
\ No newline at end of file
diff --git a/src/DotNext.IO/ExceptionMessages.restext b/src/DotNext.IO/ExceptionMessages.restext
index 4dbd90f40f..c941177ef3 100644
--- a/src/DotNext.IO/ExceptionMessages.restext
+++ b/src/DotNext.IO/ExceptionMessages.restext
@@ -3,5 +3,6 @@ StreamNotWritable=Stream is not writable
StreamNotReadable=Stream is not readable
DirectoryNotFound=Directory {0} doesn't exist
WriterInReadMode=The writer is in read-only mode. Dispose active memory manager obtained from writer
-NoConsumerProvided=No actual consumer is provided
-FileHandleClosed=The file handle is closed
\ No newline at end of file
+FileHandleClosed=The file handle is closed
+ReadBufferNotEmpty=The internal buffer has unconsumed data to read
+WriteBufferNotEmpty=The internal buffer has data to flush
\ No newline at end of file
diff --git a/src/DotNext.IO/IO/FileReader.Binary.cs b/src/DotNext.IO/IO/FileReader.Binary.cs
index 562fae07b9..f920ced45e 100644
--- a/src/DotNext.IO/IO/FileReader.Binary.cs
+++ b/src/DotNext.IO/IO/FileReader.Binary.cs
@@ -1,4 +1,5 @@
using System.Buffers;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Numerics;
using System.Runtime.CompilerServices;
@@ -229,24 +230,32 @@ public async ValueTask ParseAsync(LengthFormat lengthFormat, IFormatProvid
where T : notnull, IUtf8SpanParsable
{
var length = await ReadLengthAsync(lengthFormat, token).ConfigureAwait(false);
+ T result;
if (length <= 0)
- return T.Parse([], provider);
-
- // fast path without allocation of temp buffer
- if (TryConsume(length, out var block))
{
+ result = T.Parse([], provider);
+ }
+ else if (TryRead(length, out var block))
+ {
+ // fast path without allocation of temp buffer
block = TrimLength(block, this.length);
this.length -= block.Length;
- return block.Length == length
+ result = block.Length == length
? T.Parse(block.Span, provider)
: throw new EndOfStreamException();
+
+ ResetIfNeeded();
+ }
+ else
+ {
+ // slow path with temp buffer
+ using var buffer = Allocator.AllocateExactly(length);
+ await ReadAsync(new(buffer.Memory), token).ConfigureAwait(false);
+ result = T.Parse(buffer.Span, provider);
}
- // slow path with temp buffer
- using var buffer = allocator.AllocateExactly(length);
- await ReadAsync(new(buffer.Memory), token).ConfigureAwait(false);
- return T.Parse(buffer.Span, provider);
+ return result;
}
///
@@ -266,22 +275,29 @@ public async ValueTask ParseAsync(LengthFormat lengthFormat, NumberStyles
{
var length = await ReadLengthAsync(lengthFormat, token).ConfigureAwait(false);
+ T result;
if (length <= 0)
- return T.Parse(ReadOnlySpan.Empty, style, provider);
-
- // fast path without allocation of temp buffer
- if (TryConsume(length, out var block))
{
+ result = T.Parse(ReadOnlySpan.Empty, style, provider);
+ }
+ else if (TryRead(length, out var block))
+ {
+ // fast path without allocation of temp buffer
block = TrimLength(block, this.length);
this.length -= block.Length;
- return block.Length == length
+ result = block.Length == length
? T.Parse(block.Span, style, provider)
: throw new EndOfStreamException();
+ ResetIfNeeded();
+ }
+ else
+ {
+ using var buffer = Allocator.AllocateExactly(length);
+ await ReadAsync(new(buffer.Memory), token).ConfigureAwait(false);
+ result = T.Parse(buffer.Span, style, provider);
}
- using var buffer = allocator.AllocateExactly(length);
- await ReadAsync(new(buffer.Memory), token).ConfigureAwait(false);
- return T.Parse(buffer.Span, style, provider);
+ return result;
}
///
diff --git a/src/DotNext.IO/IO/FileReader.Segment.cs b/src/DotNext.IO/IO/FileReader.Segment.cs
index eeb433953e..a2ecdb8b5c 100644
--- a/src/DotNext.IO/IO/FileReader.Segment.cs
+++ b/src/DotNext.IO/IO/FileReader.Segment.cs
@@ -24,6 +24,8 @@ public SegmentLength()
{
}
+ internal int Truncate(int value) => IsInfinite ? value : (int)Math.Min(value, this.value);
+
internal bool IsInfinite => value < 0L;
public bool Equals(long other) => !IsInfinite && value == other;
@@ -98,8 +100,8 @@ public long? ReaderSegmentLength
}
private static ReadOnlyMemory TrimLength(ReadOnlyMemory buffer, SegmentLength length)
- => length.IsInfinite ? buffer : buffer.TrimLength(int.CreateSaturating((long)length));
+ => buffer.TrimLength(length.Truncate(buffer.Length));
private static Memory TrimLength(Memory buffer, SegmentLength length)
- => length.IsInfinite ? buffer : buffer.TrimLength(int.CreateSaturating((long)length));
+ => buffer.TrimLength(length.Truncate(buffer.Length));
}
\ No newline at end of file
diff --git a/src/DotNext.IO/IO/FileReader.Utils.cs b/src/DotNext.IO/IO/FileReader.Utils.cs
index b0dee8a659..6063fc1dbe 100644
--- a/src/DotNext.IO/IO/FileReader.Utils.cs
+++ b/src/DotNext.IO/IO/FileReader.Utils.cs
@@ -9,10 +9,17 @@ namespace DotNext.IO;
public partial class FileReader : IDynamicInterfaceCastable
{
- private readonly Action readCallback, readDirectCallback;
+ private Action? readCallback, readDirectCallback, readAndCopyCallback;
private ManualResetValueTaskSourceCore source;
private ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter;
private int extraCount;
+ private Memory destinationBuffer;
+
+ private Action ReadCallback => readCallback ??= OnRead;
+
+ private Action ReadDirectCallback => readDirectCallback ??= OnReadDirect;
+
+ private Action ReadAndCopyCallback => readAndCopyCallback ??= OnReadAndCopy;
private int GetAsyncResult(short token)
{
@@ -37,6 +44,7 @@ private void OnRead()
count = awaiter.GetResult();
bufferEnd += count;
+ ResetIfNeeded();
}
catch (Exception e)
{
@@ -72,6 +80,33 @@ private void OnReadDirect()
source.SetResult(count);
}
+ private void OnReadAndCopy()
+ {
+ var awaiter = this.awaiter;
+ this.awaiter = default;
+
+ var extraCount = this.extraCount;
+ this.extraCount = 0;
+
+ var destinationBuffer = this.destinationBuffer;
+ this.destinationBuffer = default;
+
+ int result;
+ try
+ {
+ result = awaiter.GetResult();
+ bufferEnd += result;
+ result = ReadFromBuffer(destinationBuffer.Span) + extraCount;
+ }
+ catch (Exception e)
+ {
+ source.SetException(e);
+ return;
+ }
+
+ source.SetResult(result);
+ }
+
private ValueTask SubmitAsInt32(ValueTask task, Action callback)
{
awaiter = task.ConfigureAwait(false).GetAwaiter();
diff --git a/src/DotNext.IO/IO/FileReader.cs b/src/DotNext.IO/IO/FileReader.cs
index af31be02f4..d7b1c3266b 100644
--- a/src/DotNext.IO/IO/FileReader.cs
+++ b/src/DotNext.IO/IO/FileReader.cs
@@ -1,4 +1,5 @@
using System.Diagnostics;
+using System.Runtime.CompilerServices;
using SafeFileHandle = Microsoft.Win32.SafeHandles.SafeFileHandle;
namespace DotNext.IO;
@@ -12,13 +13,16 @@ namespace DotNext.IO;
/// This class is not thread-safe. However, it's possible to share the same file
/// handle across multiple readers and use dedicated reader in each thread.
///
-public partial class FileReader : Disposable, IResettable
+public partial class FileReader : Disposable, IBufferedReader
{
+ private const int MinBufferSize = 16;
+ private const int DefaultBufferSize = 4096;
+
///
/// Represents the file handle.
///
protected readonly SafeFileHandle handle;
- private readonly MemoryAllocator? allocator;
+ private readonly int maxBufferSize;
private MemoryOwner buffer;
private int bufferStart, bufferEnd;
private long fileOffset;
@@ -27,45 +31,35 @@ public partial class FileReader : Disposable, IResettable
/// Initializes a new buffered file reader.
///
/// The file handle.
- /// The initial offset within the file.
- /// The buffer size.
- /// The buffer allocator.
- ///
- /// is less than zero;
- /// or is less than 16 bytes.
- ///
/// is .
- ///
- /// is less than zero;
- /// or too small.
- ///
- public FileReader(SafeFileHandle handle, long fileOffset = 0L, int bufferSize = 4096, MemoryAllocator? allocator = null)
+ public FileReader(SafeFileHandle handle)
{
ArgumentNullException.ThrowIfNull(handle);
ArgumentOutOfRangeException.ThrowIfNegative(fileOffset);
- ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(bufferSize, 16);
- buffer = allocator.AllocateAtLeast(bufferSize);
+ maxBufferSize = DefaultBufferSize;
this.handle = handle;
- this.fileOffset = fileOffset;
- this.allocator = allocator;
-
- readCallback = OnRead;
- readDirectCallback = OnReadDirect;
}
///
/// Initializes a new buffered file reader.
///
/// Readable file stream.
- /// The buffer size.
- /// The buffer allocator.
/// is not readable.
- public FileReader(FileStream source, int bufferSize = 4096, MemoryAllocator? allocator = null)
- : this(source.SafeFileHandle, source.Position, bufferSize, allocator)
+ public FileReader(FileStream source)
+ : this(source.SafeFileHandle)
{
if (source.CanRead is false)
throw new ArgumentException(ExceptionMessages.StreamNotReadable, nameof(source));
+
+ FilePosition = source.Position;
+ }
+
+ ///
+ public MemoryAllocator? Allocator
+ {
+ get;
+ init;
}
///
@@ -81,7 +75,7 @@ public long FilePosition
ArgumentOutOfRangeException.ThrowIfNegative(value);
if (HasBufferedData)
- throw new InvalidOperationException();
+ throw new InvalidOperationException(ExceptionMessages.ReadBufferNotEmpty);
fileOffset = value;
}
@@ -99,50 +93,52 @@ public long FilePosition
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private int BufferLength => bufferEnd - bufferStart;
- ///
- /// Gets unconsumed part of the buffer.
- ///
- public ReadOnlyMemory Buffer => buffer.Memory.Slice(bufferStart, BufferLength);
+ ///
+ public ReadOnlyMemory Buffer => buffer.Memory[bufferStart..bufferEnd];
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
- private ReadOnlySpan BufferSpan => buffer.Span.Slice(bufferStart, BufferLength);
+ private ReadOnlySpan BufferSpan => buffer.Span[bufferStart..bufferEnd];
+
+ private ref readonly MemoryOwner EnsureBufferAllocated()
+ {
+ ref var result = ref buffer;
+ if (result.IsEmpty)
+ result = Allocator.AllocateExactly(maxBufferSize);
+
+ Debug.Assert(!result.IsEmpty);
+ return ref result;
+ }
///
/// Gets a value indicating that the read buffer is not empty.
///
public bool HasBufferedData => bufferStart < bufferEnd;
- ///
- /// Gets the maximum possible amount of data that can be placed to the buffer.
- ///
- public int MaxBufferSize => buffer.Length;
-
- ///
- /// Advances read position.
- ///
- /// The number of consumed bytes.
- /// is larger than the length of .
- public void Consume(int bytes)
+ ///
+ public int MaxBufferSize
{
- var newPosition = bytes + bufferStart;
- ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)newPosition, (uint)bufferEnd, nameof(bytes));
+ get => maxBufferSize;
+ init => maxBufferSize = value >= MinBufferSize ? value : throw new ArgumentOutOfRangeException(nameof(value));
+ }
- if (newPosition == bufferEnd)
- {
- Reset();
- }
- else
- {
- bufferStart = newPosition;
- }
+ ///
+ public void Consume(int count)
+ {
+ ObjectDisposedException.ThrowIf(IsDisposed, this);
+
+ var newPosition = count + bufferStart;
+ ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)newPosition, (uint)bufferEnd, nameof(count));
- fileOffset += bytes;
+ Consume(count, newPosition);
}
- private void ConsumeUnsafe(int bytes)
- {
- var newPosition = bytes + bufferStart;
+ private void ConsumeUnsafe(int count) => Consume(count, count + bufferStart);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void Consume(int count, int newPosition)
+ {
+ Debug.Assert(newPosition == count + bufferStart);
+
if (newPosition == bufferEnd)
{
Reset();
@@ -152,76 +148,85 @@ private void ConsumeUnsafe(int bytes)
bufferStart = newPosition;
}
- fileOffset += bytes;
+ fileOffset += count;
}
-
- ///
- /// Attempts to consume buffered data.
- ///
- /// The number of bytes to consume.
- /// The slice of internal buffer containing consumed bytes.
- /// if the specified number of bytes is consumed successfully; otherwise, .
- public bool TryConsume(int bytes, out ReadOnlyMemory buffer)
+
+ private bool TryRead(int count, out ReadOnlyMemory buffer)
{
- var newPosition = bytes + bufferStart;
+ var newPosition = count + bufferStart;
if ((uint)newPosition > (uint)bufferEnd)
{
buffer = default;
return false;
}
- buffer = this.buffer.Memory.Slice(bufferStart, bytes);
+ buffer = this.buffer.Memory.Slice(bufferStart, count);
if (newPosition == bufferEnd)
{
- Reset();
+ bufferStart = bufferEnd = 0;
}
else
{
bufferStart = newPosition;
}
- fileOffset += bytes;
+ fileOffset += count;
return true;
}
+ private void ResetIfNeeded()
+ {
+ if (bufferStart == bufferEnd)
+ Reset();
+ }
+
///
/// Clears the read buffer.
///
- public void Reset() => bufferStart = bufferEnd = 0;
+ public void Reset()
+ {
+ bufferStart = bufferEnd = 0;
+ buffer.Dispose();
+ }
- ///
- /// Reads the data from the file to the underlying buffer.
- ///
- /// The token that can be used to cancel the operation.
- ///
- /// if the data has been copied from the file to the internal buffer;
- /// if no more data to read.
- ///
- /// The reader has been disposed.
- /// Internal buffer has no free space.
- /// The operation has been canceled.
+ ///
public ValueTask ReadAsync(CancellationToken token = default)
{
- if (IsDisposed)
- return new(GetDisposedTask());
+ return IsDisposed
+ ? new(GetDisposedTask())
+ : SubmitAsBoolean(ReadCoreAsync(token), ReadCallback);
+ }
- var buffer = this.buffer.Memory;
+ private ValueTask ReadCoreAsync(CancellationToken token)
+ {
+ return PrepareReadBuffer(out var readBuffer)
+ ? RandomAccess.ReadAsync(handle, readBuffer.Slice(bufferEnd), fileOffset + bufferEnd, token)
+ : ValueTask.FromException(new InternalBufferOverflowException());
+ }
+ private bool PrepareReadBuffer(out Memory readBuffer)
+ {
switch (bufferStart)
{
- case 0 when bufferEnd == buffer.Length:
- return ValueTask.FromException(new InternalBufferOverflowException());
+ case 0 when bufferEnd == maxBufferSize:
+ readBuffer = default;
+ return false;
case > 0:
+ readBuffer = buffer.Memory;
+
// compact buffer
- buffer.Slice(bufferStart, BufferLength).CopyTo(buffer);
+ readBuffer[bufferStart..bufferEnd].CopyTo(readBuffer);
bufferEnd -= bufferStart;
bufferStart = 0;
break;
+ default:
+ readBuffer = EnsureBufferAllocated().Memory;
+ break;
}
- return SubmitAsBoolean(RandomAccess.ReadAsync(handle, buffer.Slice(bufferEnd), fileOffset + bufferEnd, token), readCallback);
+ return true;
}
-
+
///
/// Reads the data from the file to the underlying buffer.
///
@@ -235,61 +240,58 @@ public bool Read()
{
ObjectDisposedException.ThrowIf(IsDisposed, this);
- var buffer = this.buffer.Span;
+ return ReadCore();
+ }
- switch (bufferStart)
- {
- case 0 when bufferEnd == buffer.Length:
- throw new InternalBufferOverflowException();
- case > 0:
- // compact buffer
- buffer.Slice(bufferStart, BufferLength).CopyTo(buffer);
- bufferEnd -= bufferStart;
- bufferStart = 0;
- break;
- }
+ private bool ReadCore()
+ {
+ if (!PrepareReadBuffer(out var readBuffer))
+ throw new InternalBufferOverflowException();
- var count = RandomAccess.Read(handle, buffer.Slice(bufferEnd), fileOffset + bufferEnd);
+ var count = RandomAccess.Read(handle, readBuffer.Span.Slice(bufferEnd), fileOffset + bufferEnd);
bufferEnd += count;
+
+ ResetIfNeeded();
return count > 0;
}
- ///
- /// Reads the block of the memory.
- ///
- /// The output buffer.
- /// The token that can be used to cancel the operation.
- /// The number of bytes copied to .
- /// The reader has been disposed.
- /// The operation has been canceled.
+ ///
public ValueTask ReadAsync(Memory destination, CancellationToken token = default)
{
+ ValueTask task;
if (IsDisposed)
{
- return new(GetDisposedTask());
+ task = new(GetDisposedTask());
}
-
- if (destination.IsEmpty)
+ else if (destination.IsEmpty)
{
- return ValueTask.FromResult(0);
+ task = new(result: 0);
}
-
- if (!HasBufferedData)
+ else
{
- return ReadDirectAsync(destination, token);
- }
+ extraCount = ReadFromBuffer(destination.Span);
+ destination = destination.Slice(extraCount);
- BufferSpan.CopyTo(destination.Span, out extraCount);
- ConsumeUnsafe(extraCount);
- destination = destination.Slice(extraCount);
+ if (destination.Length > maxBufferSize)
+ {
+ task = ReadDirectAsync(destination, token);
+ }
+ else if (destination.IsEmpty)
+ {
+ task = new(extraCount);
+ }
+ else
+ {
+ destinationBuffer = destination;
+ task = SubmitAsInt32(ReadCoreAsync(token), ReadAndCopyCallback);
+ }
+ }
- return destination.IsEmpty
- ? ValueTask.FromResult(extraCount)
- : ReadDirectAsync(destination, token);
+ return task;
}
private ValueTask ReadDirectAsync(Memory output, CancellationToken token)
- => SubmitAsInt32(RandomAccess.ReadAsync(handle, output, fileOffset, token), readDirectCallback);
+ => SubmitAsInt32(RandomAccess.ReadAsync(handle, output, fileOffset, token), ReadDirectCallback);
///
/// Reads the block of the memory.
@@ -306,46 +308,50 @@ public int Read(Span destination)
{
count = 0;
}
- else if (!HasBufferedData)
- {
- count = RandomAccess.Read(handle, destination, fileOffset);
- fileOffset += count;
- }
else
{
- BufferSpan.CopyTo(destination, out count);
- ConsumeUnsafe(count);
+ count = ReadFromBuffer(destination);
destination = destination.Slice(count);
-
- if (!destination.IsEmpty)
+ if (destination.Length > maxBufferSize)
{
var directBytes = RandomAccess.Read(handle, destination, fileOffset);
fileOffset += directBytes;
count += directBytes;
}
+ else if (!destination.IsEmpty && ReadCore())
+ {
+ count += ReadFromBuffer(destination);
+ }
}
return count;
}
+ private int ReadFromBuffer(Span destination)
+ {
+ BufferSpan.CopyTo(destination, out var bytesCopied);
+ ConsumeUnsafe(bytesCopied);
+ return bytesCopied;
+ }
+
///
/// Skips the specified number of bytes and advances file read cursor.
///
- /// The number of bytes to skip.
- /// is less than zero.
- public void Skip(long bytes)
+ /// The number of bytes to skip.
+ /// is less than zero.
+ public void Skip(long count)
{
ObjectDisposedException.ThrowIf(IsDisposed, this);
- ArgumentOutOfRangeException.ThrowIfNegative(bytes);
+ ArgumentOutOfRangeException.ThrowIfNegative(count);
- if (bytes < BufferLength)
+ if (count < BufferLength)
{
- ConsumeUnsafe((int)bytes);
+ ConsumeUnsafe((int)count);
}
else
{
Reset();
- fileOffset += bytes;
+ fileOffset += count;
}
}
@@ -355,6 +361,7 @@ protected override void Dispose(bool disposing)
if (disposing)
{
buffer.Dispose();
+ readCallback = readDirectCallback = readAndCopyCallback = null;
}
fileOffset = 0L;
diff --git a/src/DotNext.IO/IO/FileWriter.Binary.cs b/src/DotNext.IO/IO/FileWriter.Binary.cs
index 49311bbebd..9bbbae8806 100644
--- a/src/DotNext.IO/IO/FileWriter.Binary.cs
+++ b/src/DotNext.IO/IO/FileWriter.Binary.cs
@@ -32,7 +32,7 @@ private ValueTask WriteAsync(T arg, SpanAction writer, int length, C
task = ValueTask.FromException(e);
}
}
- else if (MaxBufferSize >= length)
+ else if (maxBufferSize >= length)
{
task = WriteBufferedAsync(arg, writer, length, token);
}
@@ -56,7 +56,7 @@ private async ValueTask WriteBufferedAsync(T arg, SpanAction writer,
private async ValueTask WriteDirectAsync(T arg, SpanAction writer, int length, CancellationToken token)
{
- using var buffer = allocator.AllocateExactly(length);
+ using var buffer = Allocator.AllocateExactly(length);
writer(buffer.Span, arg);
await WriteDirectAsync(buffer.Memory, token).ConfigureAwait(false);
}
@@ -162,11 +162,12 @@ public async ValueTask WriteAsync(ReadOnlyMemory input, LengthFormat lengt
public async ValueTask EncodeAsync(ReadOnlyMemory chars, EncodingContext context, LengthFormat? lengthFormat, CancellationToken token = default)
{
long result;
+
if (lengthFormat.HasValue)
{
if (FreeCapacity < Leb128.MaxSizeInBytes)
await FlushAsync(token).ConfigureAwait(false);
-
+
result = WriteLength(context.Encoding.GetByteCount(chars.Span), lengthFormat.GetValueOrDefault());
}
else
@@ -286,12 +287,13 @@ private async ValueTask FormatSlowAsync(T value, LengthFormat? lengthFor
where T : notnull, IUtf8SpanFormattable
{
await FlushAsync(token).ConfigureAwait(false);
+
if (!TryFormat(value, lengthFormat, format, provider, out var bytesWritten))
{
const int maxBufferSize = int.MaxValue / 2;
- for (var bufferSize = MaxBufferSize + Leb128.MaxSizeInBytes; ; bufferSize = bufferSize <= maxBufferSize ? bufferSize << 1 : throw new InsufficientMemoryException())
+ for (var bufferSize = maxBufferSize + Leb128.MaxSizeInBytes; ; bufferSize = bufferSize <= maxBufferSize ? bufferSize << 1 : throw new InsufficientMemoryException())
{
- using var buffer = allocator.AllocateAtLeast(bufferSize);
+ using var buffer = Allocator.AllocateAtLeast(bufferSize);
if (value.TryFormat(buffer.Span, out bytesWritten, format, provider))
{
if (lengthFormat.HasValue)
@@ -331,12 +333,14 @@ public async ValueTask CopyFromAsync(Stream input, CancellationToken token = def
{
await WriteAsync(token).ConfigureAwait(false);
- var buffer = this.buffer.Memory;
+ var buffer = EnsureBufferAllocated().Memory;
for (int bytesWritten; (bytesWritten = await input.ReadAsync(buffer, token).ConfigureAwait(false)) > 0; fileOffset += bytesWritten)
{
await RandomAccess.WriteAsync(handle, buffer.Slice(0, bytesWritten), fileOffset, token).ConfigureAwait(false);
}
+
+ Reset();
}
///
@@ -355,7 +359,7 @@ public async ValueTask CopyFromAsync(Stream source, long count, CancellationToke
await WriteAsync(token).ConfigureAwait(false);
- var buffer = this.buffer.Memory;
+ var buffer = EnsureBufferAllocated().Memory;
for (int bytesWritten; count > 0L; fileOffset += bytesWritten, count -= bytesWritten)
{
@@ -365,6 +369,8 @@ public async ValueTask CopyFromAsync(Stream source, long count, CancellationToke
await RandomAccess.WriteAsync(handle, buffer.Slice(0, bytesWritten), fileOffset, token).ConfigureAwait(false);
}
+
+ Reset();
}
///
diff --git a/src/DotNext.IO/IO/FileWriter.BufferWriter.cs b/src/DotNext.IO/IO/FileWriter.BufferWriter.cs
deleted file mode 100644
index b2206e5344..0000000000
--- a/src/DotNext.IO/IO/FileWriter.BufferWriter.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System.Buffers;
-
-namespace DotNext.IO;
-
-public partial class FileWriter : IBufferWriter
-{
- ///
- void IBufferWriter.Advance(int count) => Produce(count);
-
- ///
- Memory IBufferWriter.GetMemory(int sizeHint)
- {
- ArgumentOutOfRangeException.ThrowIfNegative(sizeHint);
-
- var result = Buffer;
- return sizeHint <= result.Length ? result : throw new InsufficientMemoryException();
- }
-
- ///
- Span IBufferWriter.GetSpan(int sizeHint)
- {
- ArgumentOutOfRangeException.ThrowIfNegative(sizeHint);
-
- var result = BufferSpan;
- return sizeHint <= result.Length ? result : throw new InsufficientMemoryException();
- }
-}
\ No newline at end of file
diff --git a/src/DotNext.IO/IO/FileWriter.Utils.cs b/src/DotNext.IO/IO/FileWriter.Utils.cs
index 47eece1ce5..e015035e7d 100644
--- a/src/DotNext.IO/IO/FileWriter.Utils.cs
+++ b/src/DotNext.IO/IO/FileWriter.Utils.cs
@@ -1,4 +1,5 @@
using System.Collections;
+using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -10,11 +11,15 @@ namespace DotNext.IO;
public partial class FileWriter : IDynamicInterfaceCastable
{
- private readonly Action writeCallback, writeAndCopyCallback;
+ private Action? writeCallback, writeAndCopyCallback;
private ReadOnlyMemory secondBuffer;
private ManualResetValueTaskSourceCore source;
private ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter awaiter;
+ private Action WriteCallback => writeCallback ??= OnWrite;
+
+ private Action WriteAndCopyCallback => writeAndCopyCallback ??= OnWriteAndCopy;
+
private ReadOnlyMemory GetBuffer(int index) => index switch
{
0 => WrittenBuffer,
@@ -66,16 +71,15 @@ private void OnWrite()
try
{
awaiter.GetResult();
-
fileOffset += secondBuffer.Length + bufferOffset;
- bufferOffset = 0;
+ Reset();
}
catch (Exception e)
{
source.SetException(e);
return;
}
-
+
source.SetResult(0);
}
@@ -87,6 +91,7 @@ private void OnWriteAndCopy()
var secondBuffer = this.secondBuffer;
this.secondBuffer = default;
+ Debug.Assert(buffer.Length > 0);
try
{
awaiter.GetResult();
diff --git a/src/DotNext.IO/IO/FileWriter.cs b/src/DotNext.IO/IO/FileWriter.cs
index ef1ee3c54b..11dbd68b12 100644
--- a/src/DotNext.IO/IO/FileWriter.cs
+++ b/src/DotNext.IO/IO/FileWriter.cs
@@ -12,13 +12,16 @@ namespace DotNext.IO;
/// This class is not thread-safe. However, it's possible to share the same file
/// handle across multiple writers and use dedicated writer in each thread.
///
-public partial class FileWriter : Disposable, IFlushable
+public partial class FileWriter : Disposable, IFlushable, IBufferedWriter
{
+ private const int MinBufferSize = 16;
+ private const int DefaultBufferSize = 4096;
+
///
/// Represents the file handle.
///
protected readonly SafeFileHandle handle;
- private readonly MemoryAllocator? allocator;
+ private readonly int maxBufferSize;
private MemoryOwner buffer;
private int bufferOffset;
private long fileOffset;
@@ -27,44 +30,34 @@ public partial class FileWriter : Disposable, IFlushable
/// Creates a new writer backed by the file.
///
/// The file handle.
- /// The initial offset within the file.
- /// The buffer size.
- /// The buffer allocator.
- ///
- /// is less than zero;
- /// or is less than 16 bytes.
- ///
/// is .
- ///
- /// is less than zero;
- /// or to small.
- ///
- public FileWriter(SafeFileHandle handle, long fileOffset = 0L, int bufferSize = 4096, MemoryAllocator? allocator = null)
+ public FileWriter(SafeFileHandle handle)
{
ArgumentNullException.ThrowIfNull(handle);
- ArgumentOutOfRangeException.ThrowIfNegative(fileOffset);
- ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(bufferSize, 16);
- buffer = allocator.AllocateAtLeast(bufferSize);
+ maxBufferSize = DefaultBufferSize;
this.handle = handle;
- this.fileOffset = fileOffset;
- this.allocator = allocator;
- writeCallback = OnWrite;
- writeAndCopyCallback = OnWriteAndCopy;
}
///
/// Creates a new writer backed by the file.
///
/// Writable file stream.
- /// The buffer size.
- /// The buffer allocator.
/// is not writable.
- public FileWriter(FileStream destination, int bufferSize = 4096, MemoryAllocator? allocator = null)
- : this(destination.SafeFileHandle, destination.Position, bufferSize, allocator)
+ public FileWriter(FileStream destination)
+ : this(destination.SafeFileHandle)
{
if (!destination.CanWrite)
throw new ArgumentException(ExceptionMessages.StreamNotWritable, nameof(destination));
+
+ FilePosition = destination.Position;
+ }
+
+ ///
+ public MemoryAllocator? Allocator
+ {
+ get;
+ init;
}
///
@@ -72,7 +65,23 @@ public FileWriter(FileStream destination, int bufferSize = 4096, MemoryAllocator
///
public ReadOnlyMemory WrittenBuffer => buffer.Memory.Slice(0, bufferOffset);
- private int FreeCapacity => buffer.Length - bufferOffset;
+ private int FreeCapacity => maxBufferSize - bufferOffset;
+
+ private ref readonly MemoryOwner EnsureBufferAllocated()
+ {
+ ref var result = ref buffer;
+ if (result.IsEmpty)
+ result = Allocator.AllocateExactly(maxBufferSize);
+
+ Debug.Assert(!result.IsEmpty);
+ return ref result;
+ }
+
+ [Conditional("DEBUG")]
+ private void AssertState()
+ {
+ Debug.Assert(bufferOffset <= buffer.Length, $"Offset = {bufferOffset}, Buffer Size = {buffer.Length}");
+ }
///
/// The remaining part of the internal buffer available for write.
@@ -80,43 +89,78 @@ public FileWriter(FileStream destination, int bufferSize = 4096, MemoryAllocator
///
/// The size of returned buffer may be less than or equal to .
///
- public Memory Buffer => buffer.Memory.Slice(bufferOffset);
+ public Memory Buffer => EnsureBufferAllocated().Memory.Slice(bufferOffset);
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
- private Span BufferSpan => buffer.Span.Slice(bufferOffset);
+ private Span BufferSpan => EnsureBufferAllocated().Span.Slice(bufferOffset);
- ///
- /// Gets the maximum available buffer size.
- ///
- public int MaxBufferSize => buffer.Length;
+ ///
+ public int MaxBufferSize
+ {
+ get => maxBufferSize;
+ init => maxBufferSize = value >= MinBufferSize ? value : throw new ArgumentOutOfRangeException(nameof(value));
+ }
+
+ ///
+ public void Produce(int count)
+ {
+ ObjectDisposedException.ThrowIf(IsDisposed, this);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)count, (uint)FreeCapacity, nameof(count));
+
+ if (count > 0 && buffer.IsEmpty)
+ buffer = Allocator.AllocateExactly(maxBufferSize);
+
+ bufferOffset += count;
+ }
///
- /// Marks the specified number of bytes in the buffer as produced.
+ /// Tries to write the data to the internal buffer.
///
- /// The number of produced bytes.
- /// is larger than the length of .
- public void Produce(int bytes)
+ /// The input data to be copied.
+ /// if the internal buffer has enough space to place the data from ;
+ /// otherwise, .
+ ///
+ public bool TryWrite(ReadOnlySpan input)
{
- ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)bytes, (uint)FreeCapacity, nameof(bytes));
+ ObjectDisposedException.ThrowIf(IsDisposed, this);
- bufferOffset += bytes;
+ bool result;
+ if (result = input.Length <= FreeCapacity)
+ {
+ input.CopyTo(BufferSpan);
+ bufferOffset += input.Length;
+ }
+
+ return result;
}
///
/// Drops all buffered data.
///
- public void ClearBuffer() => bufferOffset = 0;
+ public void Reset()
+ {
+ bufferOffset = 0;
+ buffer.Dispose();
+ }
///
/// Gets a value indicating that this writer has buffered data.
///
- public bool HasBufferedData => bufferOffset > 0;
+ public bool HasBufferedData
+ {
+ get
+ {
+ AssertState();
+
+ return bufferOffset > 0;
+ }
+ }
///
/// Gets or sets the cursor position within the file.
///
/// The value is less than zero.
- /// There is buffered data present. Call or before changing the position.
+ /// There is buffered data present. Call or before changing the position.
public long FilePosition
{
get => fileOffset;
@@ -125,7 +169,7 @@ public long FilePosition
ArgumentOutOfRangeException.ThrowIfNegative(value);
if (HasBufferedData)
- throw new InvalidOperationException();
+ throw new InvalidOperationException(ExceptionMessages.WriteBufferNotEmpty);
fileOffset = value;
}
@@ -141,22 +185,16 @@ public long FilePosition
public long WritePosition => fileOffset + bufferOffset;
private ValueTask FlushAsync(CancellationToken token)
- => Submit(RandomAccess.WriteAsync(handle, WrittenBuffer, fileOffset, token), writeCallback);
+ => Submit(RandomAccess.WriteAsync(handle, WrittenBuffer, fileOffset, token), WriteCallback);
private void Flush()
{
RandomAccess.Write(handle, WrittenBuffer.Span, fileOffset);
fileOffset += bufferOffset;
- bufferOffset = 0;
+ Reset();
}
- ///
- /// Flushes buffered data to the file.
- ///
- /// The token that can be used to cancel the operation.
- /// The task representing asynchronous result.
- /// The writer has been disposed.
- /// The operation has been canceled.
+ ///
public ValueTask WriteAsync(CancellationToken token = default)
{
if (IsDisposed)
@@ -165,6 +203,7 @@ public ValueTask WriteAsync(CancellationToken token = default)
if (token.IsCancellationRequested)
return ValueTask.FromCanceled(token);
+ AssertState();
return HasBufferedData ? FlushAsync(token) : ValueTask.CompletedTask;
}
@@ -176,6 +215,7 @@ public void FlushToDisk()
{
ObjectDisposedException.ThrowIf(IsDisposed, this);
+ AssertState();
RandomAccess.FlushToDisk(handle);
}
@@ -199,57 +239,60 @@ public void Write()
private void WriteSlow(ReadOnlySpan input)
{
- if (input.Length >= buffer.Length)
+ if (input.Length >= maxBufferSize)
{
RandomAccess.Write(handle, WrittenBuffer.Span, fileOffset);
fileOffset += bufferOffset;
RandomAccess.Write(handle, input, fileOffset);
fileOffset += input.Length;
- bufferOffset = 0;
+ Reset();
}
else
{
RandomAccess.Write(handle, WrittenBuffer.Span, fileOffset);
fileOffset += bufferOffset;
- input.CopyTo(buffer.Span);
+ input.CopyTo(EnsureBufferAllocated().Span);
bufferOffset += input.Length;
}
}
- ///
- /// Writes the data to the file through the buffer.
- ///
- /// The input data to write.
- /// The token that can be used to cancel the operation.
- /// The task representing asynchronous result.
- /// The object has been disposed.
- /// The operation has been canceled.
+ ///
public ValueTask WriteAsync(ReadOnlyMemory input, CancellationToken token = default)
{
+ ValueTask task;
+
if (IsDisposed)
- return new(DisposedTask);
-
- if (input.IsEmpty)
- goto completed_synchronously;
-
- var freeCapacity = FreeCapacity;
- switch (input.Length.CompareTo(freeCapacity))
{
- case < 0:
- input.CopyTo(Buffer);
- bufferOffset += input.Length;
- break;
- case 0:
- return WriteDirectAsync(input, token);
- case > 0 when input.Length < MaxBufferSize:
- return WriteAndCopyAsync(input, token);
- default:
- goto case 0;
+ task = new(DisposedTask);
+ }
+ else if (input.IsEmpty)
+ {
+ task = new();
+ }
+ else
+ {
+ AssertState();
+ var freeCapacity = FreeCapacity;
+ switch (input.Length.CompareTo(freeCapacity))
+ {
+ case < 0:
+ input.CopyTo(Buffer);
+ bufferOffset += input.Length;
+ task = new();
+ break;
+ case 0:
+ task = WriteDirectAsync(input, token);
+ break;
+ case > 0 when input.Length < maxBufferSize:
+ task = WriteAndCopyAsync(input, token);
+ break;
+ default:
+ goto case 0;
+ }
}
- completed_synchronously:
- return ValueTask.CompletedTask;
+ return task;
}
private ValueTask WriteDirectAsync(ReadOnlyMemory input, CancellationToken token)
@@ -266,7 +309,7 @@ private ValueTask WriteDirectAsync(ReadOnlyMemory input, CancellationToken
task = RandomAccess.WriteAsync(handle, input, fileOffset, token);
}
- return Submit(task, writeCallback);
+ return Submit(task, WriteCallback);
}
private ValueTask WriteAndCopyAsync(ReadOnlyMemory input, CancellationToken token)
@@ -274,7 +317,7 @@ private ValueTask WriteAndCopyAsync(ReadOnlyMemory input, CancellationToke
Debug.Assert(HasBufferedData);
secondBuffer = input;
- return Submit(RandomAccess.WriteAsync(handle, WrittenBuffer, fileOffset, token), writeAndCopyCallback);
+ return Submit(RandomAccess.WriteAsync(handle, WrittenBuffer, fileOffset, token), WriteAndCopyCallback);
}
///
@@ -286,6 +329,7 @@ public void Write(ReadOnlySpan input)
{
ObjectDisposedException.ThrowIf(IsDisposed, this);
+ AssertState();
if (input.Length <= FreeCapacity)
{
input.CopyTo(BufferSpan);
@@ -302,6 +346,7 @@ protected override void Dispose(bool disposing)
{
if (disposing)
{
+ writeCallback = writeAndCopyCallback = null;
buffer.Dispose();
}
diff --git a/src/DotNext.IO/IO/Pipelines/PipeExtensions.Readers.cs b/src/DotNext.IO/IO/Pipelines/PipeExtensions.Readers.cs
index 271c8b2ee2..d46da49e61 100644
--- a/src/DotNext.IO/IO/Pipelines/PipeExtensions.Readers.cs
+++ b/src/DotNext.IO/IO/Pipelines/PipeExtensions.Readers.cs
@@ -530,12 +530,11 @@ public static ValueTask ReadAsync(this PipeReader reader, Memory outp
public static async IAsyncEnumerable> ReadAllAsync(this PipeReader reader, [EnumeratorCancellation] CancellationToken token = default)
{
ReadResult result;
- ReadOnlySequence buffer;
do
{
result = await reader.ReadAsync(token).ConfigureAwait(false);
result.ThrowIfCancellationRequested(reader, token);
- buffer = result.Buffer;
+ var buffer = result.Buffer;
var consumed = buffer.Start;
try
@@ -550,6 +549,58 @@ public static async IAsyncEnumerable> ReadAllAsync(this Pip
}
while (!result.IsCompleted);
}
+
+ ///
+ /// Reads exactly the specified amount of bytes as a sequence of chunks.
+ ///
+ /// The pipe reader.
+ /// The numbers of bytes to read.
+ /// The token that can be used to cancel the operation.
+ /// A collection of chunks.
+ /// Reader doesn't have enough data to skip.
+ /// The operation has been canceled.
+ /// is
+ public static IAsyncEnumerable> ReadExactlyAsync(this PipeReader reader, long length, CancellationToken token = default)
+ {
+ return length switch
+ {
+ < 0L => AsyncEnumerable.Throw>(new ArgumentOutOfRangeException(nameof(length))),
+ 0L => AsyncEnumerable.Empty>(),
+ _ => DoReadExactlyAsync(reader, length, token),
+ };
+
+ static async IAsyncEnumerable> DoReadExactlyAsync(PipeReader reader, long length, [EnumeratorCancellation] CancellationToken token)
+ {
+ ReadResult result;
+ do
+ {
+ result = await reader.ReadAsync(token).ConfigureAwait(false);
+ result.ThrowIfCancellationRequested(reader, token);
+ var buffer = result.Buffer;
+ var consumed = buffer.Start;
+
+ try
+ {
+ for (ReadOnlyMemory block;
+ length > 0L && buffer.TryGet(ref consumed, out block, advance: false) && !block.IsEmpty;
+ consumed = buffer.GetPosition(block.Length, consumed),
+ length -= block.Length)
+ {
+ block = block.TrimLength(int.CreateSaturating(length));
+ yield return block;
+ }
+ }
+ finally
+ {
+ reader.AdvanceTo(consumed, buffer.End);
+ }
+ }
+ while (!result.IsCompleted);
+
+ if (length > 0L)
+ throw new EndOfStreamException();
+ }
+ }
///
/// Decodes null-terminated UTF-8 encoded string.
diff --git a/src/DotNext.IO/IO/PoolingBufferedStream.cs b/src/DotNext.IO/IO/PoolingBufferedStream.cs
new file mode 100644
index 0000000000..4013f348a1
--- /dev/null
+++ b/src/DotNext.IO/IO/PoolingBufferedStream.cs
@@ -0,0 +1,835 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
+namespace DotNext.IO;
+
+using Buffers;
+
+///
+/// Represents alternative implementation of that supports
+/// memory pooling.
+///
+///
+/// The stream implements lazy buffer pattern. It means that the stream releases the buffer when there is no buffered data.
+///
+/// The underlying stream to be buffered.
+/// to leave open after the object is disposed; otherwise, .
+public sealed class PoolingBufferedStream(Stream stream, bool leaveOpen = false) : Stream, IBufferedWriter, IFlushable, IBufferedReader
+{
+ private const int MinBufferSize = 16;
+ private const int DefaultBufferSize = 4096;
+
+ private readonly int maxBufferSize = DefaultBufferSize;
+ private int readPosition, writePosition, readLength;
+ private MemoryOwner buffer;
+ private Stream? stream = stream ?? throw new ArgumentNullException(nameof(stream));
+
+ ///
+ /// Gets or sets buffer allocator.
+ ///
+ public MemoryAllocator? Allocator
+ {
+ get;
+ init;
+ }
+
+ ///
+ /// Gets the base stream.
+ ///
+ public Stream BaseStream
+ {
+ get
+ {
+ ThrowIfDisposed();
+ return stream;
+ }
+ }
+
+ ///
+ /// Gets the maximum size of the internal buffer, in bytes.
+ ///
+ public int MaxBufferSize
+ {
+ get => maxBufferSize;
+ init => maxBufferSize = value >= MinBufferSize ? value : throw new ArgumentOutOfRangeException(nameof(value));
+ }
+
+ ///
+ public override bool CanRead => stream?.CanRead ?? false;
+
+ ///
+ public override bool CanWrite => stream?.CanWrite ?? false;
+
+ ///
+ public override bool CanSeek => stream?.CanSeek ?? false;
+
+ ///
+ public override bool CanTimeout => stream?.CanTimeout ?? false;
+
+ ///
+ public override int ReadTimeout
+ {
+ get => stream?.ReadTimeout ?? throw new InvalidOperationException();
+ set
+ {
+ if (stream is null)
+ throw new InvalidOperationException();
+
+ stream.ReadTimeout = value;
+ }
+ }
+
+ ///
+ public override int WriteTimeout
+ {
+ get => stream?.WriteTimeout ?? throw new InvalidOperationException();
+ set
+ {
+ if (stream is null)
+ throw new InvalidOperationException();
+
+ stream.WriteTimeout = value;
+ }
+ }
+
+ ///
+ public override long Length
+ {
+ get
+ {
+ ThrowIfDisposed();
+
+ WriteCore();
+ return stream.Length;
+ }
+ }
+
+ ///
+ public override void SetLength(long value)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(value);
+ ThrowIfDisposed();
+
+ WriteCore();
+ stream.SetLength(value);
+ }
+
+ ///
+ public override long Position
+ {
+ get
+ {
+ ThrowIfDisposed();
+
+ return stream.Position + (readPosition - readLength + writePosition);
+ }
+
+ set
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(value);
+
+ Seek(value, SeekOrigin.Begin);
+ }
+ }
+
+ [MemberNotNull(nameof(stream))]
+ private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(stream is null, this);
+
+ ///
+ /// Resets the internal buffer.
+ ///
+ public void Reset()
+ {
+ readPosition = writePosition = readLength = 0;
+ buffer.Dispose();
+ }
+
+ private void EnsureReadBufferIsEmpty()
+ {
+ if (readPosition != readLength)
+ throw new InvalidOperationException(ExceptionMessages.ReadBufferNotEmpty);
+ }
+
+ ///
+ Memory IBufferedWriter.Buffer
+ {
+ get
+ {
+ ThrowIfDisposed();
+ EnsureReadBufferIsEmpty();
+
+ return EnsureBufferAllocated().Memory.Slice(writePosition);
+ }
+ }
+
+ ///
+ void IBufferedWriter.Produce(int count)
+ {
+ ThrowIfDisposed();
+ EnsureReadBufferIsEmpty();
+
+ var freeCapacity = maxBufferSize - writePosition;
+ ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)count, (uint)freeCapacity, nameof(count));
+
+ if (count > 0 && buffer.IsEmpty)
+ buffer = Allocator.AllocateExactly(maxBufferSize);
+
+ writePosition += count;
+ }
+
+ ///
+ /// Gets a value indicating that the stream has buffered data in write buffer.
+ ///
+ public bool HasBufferedDataToWrite => writePosition > 0;
+
+ private ReadOnlyMemory WrittenMemory => buffer.Memory.Slice(0, writePosition);
+
+ ///
+ /// Writes the buffered data to the underlying stream.
+ ///
+ public void Write()
+ {
+ AssertState();
+ ThrowIfDisposed();
+
+ if (!stream.CanWrite)
+ throw new NotSupportedException();
+
+ WriteCore();
+ }
+
+ ///
+ /// Writes the buffered data to the underlying stream.
+ ///
+ /// The token that can be used to cancel the operation.
+ /// The task representing asynchronous execution of the operation.
+ /// The operation has been canceled.
+ /// The stream is disposed.
+ public ValueTask WriteAsync(CancellationToken token = default)
+ {
+ ValueTask task;
+ if (stream is null)
+ {
+ task = new(DisposedTask);
+ }
+ else if (stream.CanWrite)
+ {
+ task = WriteCoreAsync(token);
+ }
+ else
+ {
+ task = ValueTask.FromException(new NotSupportedException());
+ }
+
+ return task;
+ }
+
+ private void WriteCore()
+ {
+ Debug.Assert(stream is not null);
+
+ if (WrittenMemory.Span is { IsEmpty: false } writeBuf)
+ {
+ stream.Write(writeBuf);
+ writePosition = 0;
+ }
+ }
+
+ private ValueTask WriteCoreAsync(CancellationToken token)
+ => WrittenMemory is { IsEmpty: false } writeBuf
+ ? WriteAndResetAsync(writeBuf, token)
+ : ValueTask.CompletedTask;
+
+ private ValueTask WriteAndResetAsync(ReadOnlyMemory data, CancellationToken token)
+ {
+ Debug.Assert(stream is not null);
+
+ writePosition = 0;
+ return stream.WriteAsync(data, token);
+ }
+
+ private void ClearReadBufferBeforeWrite()
+ {
+ var relativePos = readPosition - readLength;
+ if (relativePos is not 0)
+ {
+ Debug.Assert(stream is not null);
+ stream.Seek(relativePos, SeekOrigin.Current);
+ }
+
+ readLength = readPosition = 0;
+ }
+
+ private ref readonly MemoryOwner EnsureBufferAllocated()
+ {
+ ref var result = ref buffer;
+ if (result.IsEmpty)
+ result = Allocator.AllocateExactly(maxBufferSize);
+
+ Debug.Assert(!result.IsEmpty);
+ return ref result;
+ }
+
+ private void ResetIfNeeded()
+ {
+ if (writePosition is 0 && readLength == readPosition)
+ Reset();
+ }
+
+ ///
+ public override void Write(ReadOnlySpan data)
+ {
+ AssertState();
+ ThrowIfDisposed();
+
+ if (!stream.CanWrite)
+ throw new NotSupportedException();
+
+ if (!data.IsEmpty)
+ WriteCore(data);
+ }
+
+ private void WriteCore(ReadOnlySpan data)
+ {
+ Debug.Assert(stream is not null);
+
+ if (writePosition is 0)
+ ClearReadBufferBeforeWrite();
+
+ var freeBuf = EnsureBufferAllocated().Span.Slice(writePosition);
+
+ // drain buffered data if needed
+ if (freeBuf.Length < data.Length)
+ WriteCore();
+
+ // if internal buffer has not enough space then just write through
+ if (data.Length > freeBuf.Length)
+ {
+ stream.Write(data);
+ ResetIfNeeded();
+ }
+ else
+ {
+ data.CopyTo(freeBuf);
+ writePosition += data.Length;
+ }
+ }
+
+ ///
+ public override void Write(byte[] data, int offset, int count)
+ {
+ ValidateBufferArguments(data, offset, count);
+
+ Write(new ReadOnlySpan(data, offset, count));
+ }
+
+ ///
+ public override void WriteByte(byte value)
+ => Write(new ReadOnlySpan(in value));
+
+ ///
+ public override ValueTask WriteAsync(ReadOnlyMemory data, CancellationToken token = default)
+ {
+ AssertState();
+
+ ValueTask task;
+ if (stream is null)
+ {
+ task = new(DisposedTask);
+ }
+ else if (!stream.CanWrite)
+ {
+ task = ValueTask.FromException(new NotSupportedException());
+ }
+ else if (data.IsEmpty)
+ {
+ task = new();
+ }
+ else
+ {
+ task = WriteCoreAsync(data, token);
+ }
+
+ return task;
+ }
+
+ private async ValueTask WriteCoreAsync(ReadOnlyMemory data, CancellationToken token)
+ {
+ Debug.Assert(stream is not null);
+
+ if (writePosition is 0)
+ ClearReadBufferBeforeWrite();
+
+ var freeBuf = EnsureBufferAllocated().Memory.Slice(writePosition);
+
+ // drain buffered data if needed
+ if (freeBuf.Length < data.Length)
+ {
+ await WriteCoreAsync(token).ConfigureAwait(false);
+ freeBuf = buffer.Memory.Slice(writePosition);
+ }
+
+ // if internal buffer has not enough space then just write through
+ if (data.Length > freeBuf.Length)
+ {
+ await stream.WriteAsync(data, token).ConfigureAwait(false);
+ ResetIfNeeded();
+ }
+ else
+ {
+ data.CopyTo(freeBuf);
+ writePosition += data.Length;
+ }
+ }
+
+ ///
+ public override Task WriteAsync(byte[] data, int offset, int count, CancellationToken token)
+ => WriteAsync(new ReadOnlyMemory(data, offset, count), token).AsTask();
+
+ ///
+ public override IAsyncResult BeginWrite(byte[] data, int offset, int count, AsyncCallback? callback, object? state)
+ => TaskToAsyncResult.Begin(WriteAsync(data, offset, count), callback, state);
+
+ ///
+ public override void EndWrite(IAsyncResult asyncResult) => TaskToAsyncResult.End(asyncResult);
+
+ private ReadOnlyMemory MemoryToRead => buffer.Memory[readPosition..readLength];
+
+ private int ReadFromBuffer(Span destination)
+ {
+ int count;
+ if (MemoryToRead.Span is { IsEmpty: false } readBuf)
+ {
+ readBuf.CopyTo(destination, out count);
+ readPosition += count;
+ ResetIfNeeded();
+ }
+ else
+ {
+ count = 0;
+ }
+
+ return count;
+ }
+
+ ///
+ /// Gets a value indicating that the stream has data in read buffer.
+ ///
+ public bool HasBufferedDataToRead => readPosition != readLength;
+
+ ///
+ public override int Read(Span data)
+ {
+ AssertState();
+ ThrowIfDisposed();
+
+ if (!stream.CanRead)
+ throw new InvalidOperationException();
+
+ return data.IsEmpty ? 0 : ReadCore(data);
+ }
+
+ private int ReadCore(Span data)
+ {
+ Debug.Assert(stream is not null);
+
+ var bytesRead = ReadFromBuffer(data);
+ data = data.Slice(bytesRead);
+ WriteCore();
+
+ if (data.IsEmpty)
+ {
+ // nothing to do
+ }
+ else if (data.Length > MaxBufferSize)
+ {
+ bytesRead += stream.Read(data);
+ }
+ else
+ {
+ readLength = stream.Read(EnsureBufferAllocated().Span);
+ bytesRead += ReadFromBuffer(data);
+ }
+
+ return bytesRead;
+ }
+
+ ///
+ public override int Read(byte[] data, int offset, int count)
+ {
+ ValidateBufferArguments(data, offset, count);
+
+ return Read(data.AsSpan(offset, count));
+ }
+
+ ///
+ public override ValueTask ReadAsync(Memory data, CancellationToken token = default)
+ {
+ AssertState();
+ ValueTask task;
+
+ if (stream is null)
+ {
+ task = GetDisposedTask();
+ }
+ else if (!stream.CanRead)
+ {
+ task = ValueTask.FromException(new NotSupportedException());
+ }
+ else if (buffer.IsEmpty)
+ {
+ task = new(result: 0);
+ }
+ else
+ {
+ task = ReadCoreAsync(data, token);
+ }
+
+ return task;
+ }
+
+ ///
+ public override Task ReadAsync(byte[] data, int offset, int count, CancellationToken token)
+ => ReadAsync(data.AsMemory(offset, count), token).AsTask();
+
+ private async ValueTask ReadCoreAsync(Memory data, CancellationToken token)
+ {
+ Debug.Assert(stream is not null);
+
+ var bytesRead = ReadFromBuffer(data.Span);
+ data = data.Slice(bytesRead);
+
+ await WriteCoreAsync(token).ConfigureAwait(false);
+ if (data.IsEmpty)
+ {
+ // nothing to do
+ }
+ else if (data.Length > MaxBufferSize)
+ {
+ bytesRead += await stream.ReadAsync(data, token).ConfigureAwait(false);
+ }
+ else
+ {
+ readLength = await stream.ReadAsync(EnsureBufferAllocated().Memory, token).ConfigureAwait(false);
+ bytesRead += ReadFromBuffer(data.Span);
+ }
+
+ return bytesRead;
+ }
+
+ ///
+ /// Fetches the internal buffer from the underlying stream.
+ ///
+ /// The token that can be used to cancel the operation.
+ ///
+ /// if the data has been copied from the file to the internal buffer;
+ /// if no more data to read.
+ ///
+ /// The operation has been canceled.
+ /// The stream is disposed.
+ /// The internal buffer is full.
+ public async ValueTask ReadAsync(CancellationToken token = default)
+ {
+ AssertState();
+ ThrowIfDisposed();
+
+ if (!stream.CanRead)
+ throw new NotSupportedException();
+
+ await WriteCoreAsync(token).ConfigureAwait(false);
+
+ var count = PrepareReadBuffer(out var readBuf)
+ ? await stream.ReadAsync(readBuf, token).ConfigureAwait(false)
+ : throw new InternalBufferOverflowException();
+ readLength += count;
+ ResetIfNeeded();
+
+ return count > 0;
+ }
+
+ ///
+ /// Populates the internal buffer from the underlying stream.
+ ///
+ /// if
+ /// The stream is disposed.
+ /// The internal buffer is full.
+ public bool Read()
+ {
+ AssertState();
+ ThrowIfDisposed();
+
+ if (!stream.CanRead)
+ throw new NotSupportedException();
+
+ WriteCore();
+
+ var count = PrepareReadBuffer(out var readBuf)
+ ? stream.Read(readBuf.Span)
+ : throw new InternalBufferOverflowException();
+ readLength += count;
+ ResetIfNeeded();
+
+ return count > 0;
+ }
+
+ private bool PrepareReadBuffer(out Memory readBuffer)
+ {
+ Debug.Assert(writePosition is 0);
+
+ switch (readPosition)
+ {
+ case 0 when readLength == maxBufferSize:
+ readBuffer = default;
+ return false;
+ case > 0:
+ readBuffer = buffer.Memory;
+
+ // compact buffer
+ readBuffer[readPosition..readLength].CopyTo(readBuffer);
+ readLength -= readPosition;
+ readPosition = 0;
+ break;
+ default:
+ readBuffer = EnsureBufferAllocated().Memory;
+ break;
+ }
+
+ return true;
+ }
+
+ ///
+ public override IAsyncResult BeginRead(byte[] data, int offset, int count, AsyncCallback? callback, object? state)
+ => TaskToAsyncResult.Begin(ReadAsync(data, offset, count), callback, state);
+
+ ///
+ public override int EndRead(IAsyncResult asyncResult) => TaskToAsyncResult.End(asyncResult);
+
+ ///
+ public override Task FlushAsync(CancellationToken token)
+ {
+ Task task;
+ if (writePosition > 0)
+ {
+ task = WriteAndFlushAsync(token);
+ }
+ else if (stream is null)
+ {
+ task = DisposedTask;
+ }
+ else
+ {
+ Reset();
+ task = stream.FlushAsync(token);
+ }
+
+ return task;
+ }
+
+ private Task DisposedTask => Task.FromException(new ObjectDisposedException(GetType().Name));
+
+ private ValueTask GetDisposedTask() => ValueTask.FromException(new ObjectDisposedException(GetType().Name));
+
+ private async Task WriteAndFlushAsync(CancellationToken token)
+ {
+ Debug.Assert(writePosition > 0);
+ Debug.Assert(buffer.Length > 0);
+
+ ThrowIfDisposed();
+
+ await stream.WriteAsync(WrittenMemory, token).ConfigureAwait(false);
+ await stream.FlushAsync(token).ConfigureAwait(false);
+
+ Reset();
+ }
+
+ ///
+ public override void Flush()
+ {
+ AssertState();
+ ThrowIfDisposed();
+
+ WriteCore();
+ stream.Flush();
+ Reset();
+ }
+
+ ///
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ AssertState();
+ ThrowIfDisposed();
+
+ long result;
+ if (WrittenMemory.Span is { IsEmpty: false } writeBuf)
+ {
+ stream.Write(writeBuf);
+ result = stream.Seek(offset, origin);
+ ResetIfNeeded();
+ }
+ else
+ {
+ result = SeekNoWriteBuffer(offset, origin);
+ }
+
+ return result;
+ }
+
+ private long SeekNoWriteBuffer(long offset, SeekOrigin origin)
+ {
+ Debug.Assert(stream is not null);
+
+ var readBytes = readLength - readPosition;
+ if (origin is SeekOrigin.Current && readBytes > 0)
+ offset -= readBytes;
+
+ var oldPos = stream.Position - readBytes;
+ var newPos = stream.Seek(offset, origin);
+
+ var readPos = newPos - oldPos + readPosition;
+ if (readPos >= 0L && readPos < readLength)
+ {
+ readPosition = (int)readPos;
+ stream.Seek(readLength - readPosition, SeekOrigin.Current);
+ }
+ else
+ {
+ Reset();
+ }
+
+ return newPos;
+ }
+
+ ///
+ public override int ReadByte()
+ {
+ Unsafe.SkipInit(out byte value);
+ return Read(new Span(ref value)) > 0 ? value : -1;
+ }
+
+ private void EnsureWriteBufferIsEmpty()
+ {
+ if (writePosition is not 0)
+ throw new InvalidOperationException(ExceptionMessages.WriteBufferNotEmpty);
+ }
+
+ ///
+ ReadOnlyMemory IBufferedReader.Buffer
+ {
+ get
+ {
+ AssertState();
+ ThrowIfDisposed();
+ EnsureWriteBufferIsEmpty();
+
+ return buffer.Memory[readPosition..readLength];
+ }
+ }
+
+ ///
+ void IBufferedReader.Consume(int count)
+ {
+ AssertState();
+ ThrowIfDisposed();
+ EnsureWriteBufferIsEmpty();
+
+ var newPosition = count + readPosition;
+ ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)newPosition, (uint)readLength, nameof(count));
+
+ if (newPosition == readLength)
+ {
+ Reset();
+ }
+ else
+ {
+ readPosition = newPosition;
+ }
+ }
+
+ [Conditional("DEBUG")]
+ private void AssertState()
+ {
+ // if reader or writer state differs from the default one, the buffer must be allocated
+ Debug.Assert((readPosition == readLength && writePosition is 0) || buffer.Length > 0);
+ }
+
+ ///
+ public override void CopyTo(Stream destination, int bufferSize)
+ {
+ AssertState();
+ ValidateCopyToArguments(destination, bufferSize);
+ ThrowIfDisposed();
+
+ if (MemoryToRead.Span is { IsEmpty: false } readBuf)
+ {
+ destination.Write(readBuf);
+ readLength = readPosition = 0;
+ }
+ else if (WrittenMemory.Span is { IsEmpty: false } writeBuf)
+ {
+ stream.Write(writeBuf);
+ writePosition = 0;
+ }
+
+ stream.CopyTo(destination, bufferSize);
+ ResetIfNeeded();
+ }
+
+ ///
+ public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken token)
+ {
+ AssertState();
+ ValidateCopyToArguments(destination, bufferSize);
+ ThrowIfDisposed();
+
+ if (MemoryToRead is { IsEmpty: false } readBuf)
+ {
+ await destination.WriteAsync(readBuf, token).ConfigureAwait(false);
+ readLength = readPosition = 0;
+ }
+ else if (WrittenMemory is { IsEmpty: false } writeBuf)
+ {
+ await stream.WriteAsync(writeBuf, token).ConfigureAwait(false);
+ writePosition = 0;
+ }
+
+ await stream.CopyToAsync(destination, bufferSize, token).ConfigureAwait(false);
+ ResetIfNeeded();
+ }
+
+ ///
+ public override ValueTask DisposeAsync()
+ {
+ ValueTask task;
+ if (stream is null)
+ {
+ task = new();
+ }
+ else
+ {
+ Reset();
+ task = leaveOpen ? new() : stream.DisposeAsync();
+ stream = null;
+ }
+
+ return task;
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ if (!leaveOpen && stream is not null)
+ stream.Dispose();
+
+ stream = null;
+ }
+
+ Reset();
+ base.Dispose(disposing);
+ }
+
+ ///
+ ~PoolingBufferedStream() => Dispose(false);
+}
\ No newline at end of file
diff --git a/src/DotNext.IO/IO/RandomAccessStream.cs b/src/DotNext.IO/IO/RandomAccessStream.cs
index eb6b256f63..169849c6b7 100644
--- a/src/DotNext.IO/IO/RandomAccessStream.cs
+++ b/src/DotNext.IO/IO/RandomAccessStream.cs
@@ -141,7 +141,10 @@ public override long Seek(long offset, SeekOrigin origin)
? newPosition
: throw new IOException();
}
-
+
+ ///
+ public override bool CanSeek => true;
+
///
protected override void Dispose(bool disposing)
{
diff --git a/src/DotNext.IO/IO/SequenceReader.cs b/src/DotNext.IO/IO/SequenceReader.cs
index 8904eb8e76..8cd7d54914 100644
--- a/src/DotNext.IO/IO/SequenceReader.cs
+++ b/src/DotNext.IO/IO/SequenceReader.cs
@@ -252,7 +252,7 @@ public void Skip(long length)
/// The memory allocator used to place the decoded block of bytes.
/// The decoded block of bytes.
/// Unexpected end of sequence.
- public MemoryOwner ReadBlock(LengthFormat lengthFormat, MemoryAllocator? allocator = null)
+ public MemoryOwner ReadBlock(LengthFormat lengthFormat, MemoryAllocator? allocator)
{
var length = ReadLength(lengthFormat);
MemoryOwner result;
@@ -269,6 +269,15 @@ public MemoryOwner ReadBlock(LengthFormat lengthFormat, MemoryAllocator
+ /// Reads length-prefixed block of bytes.
+ ///
+ /// The format of the block length encoded in the underlying stream.
+ /// The decoded block of bytes.
+ /// Unexpected end of sequence.
+ public ReadOnlySequence ReadBlock(LengthFormat lengthFormat)
+ => Read(ReadLength(lengthFormat));
+
private int Read7BitEncodedInt32()
{
var parser = new SevenBitEncodedIntReader();
diff --git a/src/DotNext.IO/IO/SparseStream.cs b/src/DotNext.IO/IO/SparseStream.cs
index eaa21bcbc2..bee687bdb0 100644
--- a/src/DotNext.IO/IO/SparseStream.cs
+++ b/src/DotNext.IO/IO/SparseStream.cs
@@ -1,39 +1,41 @@
+using System.Diagnostics;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
namespace DotNext.IO;
+using Buffers;
+
///
/// Represents multiple streams as a single stream.
///
///
/// The stream is available for read-only operations.
///
-internal sealed class SparseStream : Stream, IFlushable
+internal abstract class SparseStream(bool leaveOpen) : Stream, IFlushable
{
- private readonly IEnumerator enumerator;
- private bool streamAvailable;
-
- ///
- /// Initializes a new sparse stream.
- ///
- /// A collection of readable streams.
- public SparseStream(IEnumerable streams)
+ private int runningIndex;
+
+ protected abstract ReadOnlySpan Streams { get; }
+
+ private Stream? Current
{
- enumerator = streams.GetEnumerator();
- streamAvailable = enumerator.MoveNext();
- }
+ get
+ {
+ var streams = Streams;
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void MoveToNextStream() => streamAvailable = enumerator.MoveNext();
+ return (uint)runningIndex < (uint)streams.Length ? streams[runningIndex] : null;
+ }
+ }
///
- public override int ReadByte()
+ public sealed override int ReadByte()
{
var result = -1;
- for (; streamAvailable; MoveToNextStream())
+ for (; Current is { } current; runningIndex++)
{
- result = enumerator.Current.ReadByte();
+ result = current.ReadByte();
if (result >= 0)
break;
@@ -43,12 +45,12 @@ public override int ReadByte()
}
///
- public override int Read(Span buffer)
+ public sealed override int Read(Span buffer)
{
int count;
- for (count = 0; streamAvailable; MoveToNextStream())
+ for (count = 0; Current is { } current; runningIndex++)
{
- count = enumerator.Current.Read(buffer);
+ count = current.Read(buffer);
if (count > 0)
break;
@@ -58,7 +60,7 @@ public override int Read(Span buffer)
}
///
- public override int Read(byte[] buffer, int offset, int count)
+ public sealed override int Read(byte[] buffer, int offset, int count)
{
ValidateBufferArguments(buffer, offset, count);
@@ -67,12 +69,12 @@ public override int Read(byte[] buffer, int offset, int count)
///
[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))]
- public override async ValueTask ReadAsync(Memory buffer, CancellationToken token = default)
+ public sealed override async ValueTask ReadAsync(Memory buffer, CancellationToken token = default)
{
int count;
- for (count = 0; streamAvailable; MoveToNextStream())
+ for (count = 0; Current is { } current; runningIndex++)
{
- count = await enumerator.Current.ReadAsync(buffer, token).ConfigureAwait(false);
+ count = await current.ReadAsync(buffer, token).ConfigureAwait(false);
if (count > 0)
break;
@@ -82,95 +84,168 @@ public override async ValueTask ReadAsync(Memory buffer, Cancellation
}
///
- public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken token)
+ public sealed override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken token)
=> ReadAsync(buffer.AsMemory(offset, count), token).AsTask();
+ public sealed override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
+ => TaskToAsyncResult.Begin(ReadAsync(buffer, offset, count), callback, state);
+
+ public sealed override int EndRead(IAsyncResult asyncResult)
+ => TaskToAsyncResult.End(asyncResult);
+
///
- public override void CopyTo(Stream destination, int bufferSize)
+ public sealed override void CopyTo(Stream destination, int bufferSize)
{
ValidateCopyToArguments(destination, bufferSize);
- for (; streamAvailable; MoveToNextStream())
- enumerator.Current.CopyTo(destination, bufferSize);
+ for (; Current is { } current; runningIndex++)
+ current.CopyTo(destination, bufferSize);
}
///
- public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken token)
+ public sealed override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken token)
{
ValidateCopyToArguments(destination, bufferSize);
- for (; streamAvailable; MoveToNextStream())
- await enumerator.Current.CopyToAsync(destination, bufferSize, token).ConfigureAwait(false);
+ for (; Current is { } current; runningIndex++)
+ await current.CopyToAsync(destination, bufferSize, token).ConfigureAwait(false);
}
///
- public override bool CanRead => true;
+ public sealed override bool CanRead => true;
///
- public override bool CanWrite => false;
+ public sealed override bool CanWrite => false;
///
- public override bool CanSeek => false;
+ public sealed override bool CanSeek => false;
///
- public override long Position
+ public sealed override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
///
- public override void Flush()
- {
- if (streamAvailable)
- enumerator.Current.Flush();
- }
+ public sealed override void Flush() => Current?.Flush();
///
- public override Task FlushAsync(CancellationToken token)
- => streamAvailable ? enumerator.Current.FlushAsync(token) : Task.CompletedTask;
+ public sealed override Task FlushAsync(CancellationToken token)
+ => Current?.FlushAsync(token) ?? Task.CompletedTask;
///
- public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
+ public sealed override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
///
- public override long Length => throw new NotSupportedException();
+ public sealed override long Length
+ {
+ get
+ {
+ var length = 0L;
+
+ foreach (var stream in Streams)
+ {
+ length += stream.Length;
+ }
+
+ return length;
+ }
+ }
///
- public override void SetLength(long value) => throw new NotSupportedException();
+ public sealed override void SetLength(long value) => throw new NotSupportedException();
///
- public override void WriteByte(byte value) => throw new NotSupportedException();
+ public sealed override void WriteByte(byte value) => throw new NotSupportedException();
///
- public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
+ public sealed override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
///
- public override void Write(ReadOnlySpan buffer) => throw new NotSupportedException();
+ public sealed override void Write(ReadOnlySpan buffer) => throw new NotSupportedException();
///
- public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken token) => Task.FromException(new NotSupportedException());
+ public sealed override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken token) => Task.FromException(new NotSupportedException());
///
- public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default)
+ public sealed override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default)
=> ValueTask.FromException(new NotSupportedException());
///
- public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
+ public sealed override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
=> throw new NotSupportedException();
///
- public override void EndWrite(IAsyncResult asyncResult) => throw new InvalidOperationException();
+ public sealed override void EndWrite(IAsyncResult asyncResult) => throw new InvalidOperationException();
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && !leaveOpen)
+ {
+ Disposable.Dispose(Streams);
+ }
+ }
+
+ public override async ValueTask DisposeAsync()
+ {
+ if (!leaveOpen)
+ {
+ for (var i = 0; i < Streams.Length; i++)
+ {
+ await Streams[i].DisposeAsync().ConfigureAwait(false);
+ }
+ }
+
+ GC.SuppressFinalize(this);
+ }
+}
+
+internal sealed class SparseStream(T streams, bool leaveOpen) : SparseStream(leaveOpen)
+ where T : struct, ITuple
+{
+ protected override ReadOnlySpan Streams
+ => MemoryMarshal.CreateReadOnlySpan(in Unsafe.As(ref Unsafe.AsRef(in streams)), streams.Length);
+}
+
+internal sealed class UnboundedSparseStream : SparseStream
+{
+ private MemoryOwner streams;
+
+ internal UnboundedSparseStream(Stream stream, ReadOnlySpan streams, bool leaveOpen)
+ : base(leaveOpen)
+ {
+ Debug.Assert(streams.Length < int.MaxValue);
+
+ this.streams = Memory.AllocateExactly(streams.Length + 1);
+ var output = this.streams.Span;
+ output[0] = stream;
+ streams.CopyTo(output.Slice(1));
+ }
+
+ protected override ReadOnlySpan Streams => streams.Span;
- ///
protected override void Dispose(bool disposing)
{
- if (disposing)
+ try
{
- enumerator.Dispose();
+ base.Dispose(disposing);
}
+ finally
+ {
+ streams.Dispose();
+ }
+ }
- streamAvailable = false;
- base.Dispose(disposing);
+ public override async ValueTask DisposeAsync()
+ {
+ try
+ {
+ await base.DisposeAsync().ConfigureAwait(false);
+ }
+ finally
+ {
+ streams.Dispose();
+ }
}
}
\ No newline at end of file
diff --git a/src/DotNext.IO/IO/StreamExtensions.Readers.cs b/src/DotNext.IO/IO/StreamExtensions.Readers.cs
index 23e9333f79..e540b16189 100644
--- a/src/DotNext.IO/IO/StreamExtensions.Readers.cs
+++ b/src/DotNext.IO/IO/StreamExtensions.Readers.cs
@@ -461,6 +461,7 @@ public static async ValueTask CopyToAsync(this Stream source, IBufferWriter or is negative.
/// doesn't support reading.
/// The operation has been canceled.
+ /// The underlying source doesn't contain necessary amount of bytes.
public static async ValueTask CopyToAsync(this Stream source, IBufferWriter destination, long count, int bufferSize = 0, CancellationToken token = default)
{
ArgumentNullException.ThrowIfNull(destination);
@@ -482,21 +483,55 @@ public static async ValueTask CopyToAsync(this Stream source, IBufferWriter
/// The returned memory block should not be used between iterations.
///
- /// Readable stream.
+ /// Readable stream.
/// The buffer size.
/// The allocator of the buffer.
/// The token that can be used to cancel the enumeration.
/// A collection of memory blocks that can be obtained sequentially to read a whole stream.
/// is less than 1.
- public static async IAsyncEnumerable> ReadAllAsync(this Stream stream, int bufferSize, MemoryAllocator? allocator = null, [EnumeratorCancellation] CancellationToken token = default)
+ /// The operation has been canceled.
+ /// doesn't support reading.
+ public static async IAsyncEnumerable> ReadAllAsync(this Stream source, int bufferSize, MemoryAllocator? allocator = null, [EnumeratorCancellation] CancellationToken token = default)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize);
+
+ using var buffer = allocator.AllocateAtLeast(bufferSize);
+
+ for (int bytesRead; (bytesRead = await source.ReadAsync(buffer.Memory, token).ConfigureAwait(false)) > 0;)
+ yield return buffer.Memory.Slice(0, bytesRead);
+ }
+
+ ///
+ /// Reads exactly the specified amount of bytes as a sequence of chunks.
+ ///
+ ///
+ /// The returned memory block should not be used between iterations.
+ ///
+ /// The stream to read from.
+ /// The numbers of bytes to read.
+ /// The maximum size of chunks to be returned.
+ /// The buffer allocator.
+ /// The token that can be used to cancel the enumeration.
+ /// A collection of memory blocks that can be obtained sequentially to read the requested amount of bytes.
+ /// The underlying source doesn't contain necessary amount of bytes.
+ /// is less than 1; or is less than 0.
+ /// The operation has been canceled.
+ public static async IAsyncEnumerable> ReadExactlyAsync(this Stream stream, long length, int bufferSize,
+ MemoryAllocator? allocator = null, [EnumeratorCancellation] CancellationToken token = default)
{
+ ArgumentOutOfRangeException.ThrowIfNegative(length);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize);
- using var bufferOwner = allocator.AllocateAtLeast(bufferSize);
- var buffer = bufferOwner.Memory;
+ using var buffer = allocator.AllocateAtLeast(bufferSize);
- for (int count; (count = await stream.ReadAsync(buffer, token).ConfigureAwait(false)) > 0;)
- yield return buffer.Slice(0, count);
+ for (int bytesRead; length > 0L; length -= bytesRead)
+ {
+ bytesRead = await stream.ReadAsync(buffer.Memory.TrimLength(int.CreateSaturating(length)), token).ConfigureAwait(false);
+ if (bytesRead <= 0)
+ throw new EndOfStreamException();
+
+ yield return buffer.Memory.Slice(0, bytesRead);
+ }
}
///
diff --git a/src/DotNext.IO/IO/StreamExtensions.cs b/src/DotNext.IO/IO/StreamExtensions.cs
index edb7b62137..47f929e432 100644
--- a/src/DotNext.IO/IO/StreamExtensions.cs
+++ b/src/DotNext.IO/IO/StreamExtensions.cs
@@ -3,6 +3,9 @@
namespace DotNext.IO;
+using Buffers;
+using static Runtime.Intrinsics;
+
///
/// Represents high-level read/write methods for the stream.
///
@@ -18,14 +21,70 @@ internal static void ThrowIfEmpty(in Memory buffer, [CallerArgumentExpress
throw new ArgumentException(ExceptionMessages.BufferTooSmall, expression);
}
+ private static Stream Combine(Stream stream, ReadOnlySpan others, bool leaveOpen)
+ => others switch
+ {
+ [] => stream,
+ [var s] => new SparseStream<(Stream, Stream)>((stream, s), leaveOpen),
+ [var s1, var s2] => new SparseStream<(Stream, Stream, Stream)>((stream, s1, s2), leaveOpen),
+ [var s1, var s2, var s3] => new SparseStream<(Stream, Stream, Stream, Stream)>((stream, s1, s2, s3), leaveOpen),
+ [var s1, var s2, var s3, var s4] => new SparseStream<(Stream, Stream, Stream, Stream, Stream)>((stream, s1, s2, s3, s4), leaveOpen),
+ [var s1, var s2, var s3, var s4, var s5] => new SparseStream<(Stream, Stream, Stream, Stream, Stream, Stream)>((stream, s1, s2, s3, s4,
+ s5), leaveOpen),
+ [var s1, var s2, var s3, var s4, var s5, var s6] => new SparseStream<(Stream, Stream, Stream, Stream, Stream, Stream, Stream)>((stream, s1, s2, s3, s4,
+ s5, s6), leaveOpen),
+ { Length: int.MaxValue } => throw new InsufficientMemoryException(),
+ _ => new UnboundedSparseStream(stream, others, leaveOpen),
+ };
+
///
/// Combines multiple readable streams.
///
/// The stream to combine.
/// A collection of streams.
/// An object that represents multiple streams as one logical stream.
- public static Stream Combine(this Stream stream, ReadOnlySpan others)
- => others is { Length: > 0 } ? new SparseStream([stream, .. others]) : stream;
+ public static Stream Combine(this Stream stream, ReadOnlySpan others) // TODO: Use params in future
+ => Combine(stream, others, leaveOpen: true);
+
+ ///
+ /// Combines multiple readable streams.
+ ///
+ /// A collection of streams.
+ /// to keep the wrapped streams alive when combined stream disposed; otherwise, .
+ /// An object that represents multiple streams as one logical stream.
+ /// is empty.
+ public static Stream Combine(this ReadOnlySpan streams, bool leaveOpen = true)
+ => streams is [var first, .. var rest]
+ ? Combine(first, rest, leaveOpen)
+ : throw new ArgumentException(ExceptionMessages.BufferTooSmall, nameof(streams));
+
+ ///
+ /// Combines multiple readable streams.
+ ///
+ /// A collection of streams.
+ /// to keep the wrapped streams alive when combined stream disposed; otherwise, .
+ /// An object that represents multiple streams as one logical stream.
+ /// is empty.
+ public static Stream Combine(this IEnumerable streams, bool leaveOpen = true)
+ {
+ // Use buffer to allocate streams on the stack
+ var buffer = new StreamBuffer();
+ var writer = new BufferWriterSlim(buffer);
+
+ Stream result;
+ try
+ {
+ writer.AddAll(streams);
+ result = Combine(writer.WrittenSpan, leaveOpen);
+ }
+ finally
+ {
+ writer.Dispose();
+ KeepAlive(in buffer);
+ }
+
+ return result;
+ }
///
/// Creates a stream for the specified file handle.
@@ -46,4 +105,10 @@ public static Stream AsUnbufferedStream(this SafeFileHandle handle, FileAccess a
? new UnbufferedFileStream(handle, access)
: throw new ArgumentException(ExceptionMessages.FileHandleClosed, nameof(handle));
}
+
+ [InlineArray(32)]
+ private struct StreamBuffer
+ {
+ private Stream element0;
+ }
}
\ No newline at end of file
diff --git a/src/DotNext.IO/IO/TextStreamExtensions.cs b/src/DotNext.IO/IO/TextStreamExtensions.cs
index 2e24be6a35..4690dd343d 100644
--- a/src/DotNext.IO/IO/TextStreamExtensions.cs
+++ b/src/DotNext.IO/IO/TextStreamExtensions.cs
@@ -199,16 +199,15 @@ public static void WriteLine(this TextWriter writer, MemoryAllocator? allo
/// The buffer size.
/// The allocator of the buffer.
/// The token that can be used to cancel the enumeration.
- /// A collection of memort blocks that can be obtained sequentially to read a whole stream.
+ /// A collection of memory blocks that can be obtained sequentially to read a whole stream.
/// is less than 1.
public static async IAsyncEnumerable> ReadAllAsync(this TextReader reader, int bufferSize, MemoryAllocator? allocator = null, [EnumeratorCancellation] CancellationToken token = default)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize);
- using var bufferOwner = allocator.AllocateAtLeast(bufferSize);
- var buffer = bufferOwner.Memory;
+ using var buffer = allocator.AllocateAtLeast(bufferSize);
- for (int count; (count = await reader.ReadAsync(buffer, token).ConfigureAwait(false)) > 0;)
- yield return buffer.Slice(0, count);
+ for (int count; (count = await reader.ReadAsync(buffer.Memory, token).ConfigureAwait(false)) > 0;)
+ yield return buffer.Memory.Slice(0, count);
}
}
\ No newline at end of file
diff --git a/src/DotNext.MaintenanceServices/DotNext.MaintenanceServices.csproj b/src/DotNext.MaintenanceServices/DotNext.MaintenanceServices.csproj
index 00e3fced30..c7fa4198ae 100644
--- a/src/DotNext.MaintenanceServices/DotNext.MaintenanceServices.csproj
+++ b/src/DotNext.MaintenanceServices/DotNext.MaintenanceServices.csproj
@@ -5,13 +5,13 @@
latest
enable
true
- true
+ true
nullablePublicOnly
DotNext
.NET Foundation and Contributors
.NEXT Family of Libraries
- 0.4.0
+ 0.5.0
DotNext.MaintenanceServices
MIT
diff --git a/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj b/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj
index 144ae42529..0dcdba38ad 100644
--- a/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj
+++ b/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj
@@ -6,9 +6,9 @@
latest
enable
true
- false
+ false
nullablePublicOnly
- 5.16.1
+ 5.17.0
.NET Foundation
.NEXT Family of Libraries
diff --git a/src/DotNext.Tests/Buffers/Binary/Leb128Tests.cs b/src/DotNext.Tests/Buffers/Binary/Leb128Tests.cs
index 627edd60ed..7e7e812e6b 100644
--- a/src/DotNext.Tests/Buffers/Binary/Leb128Tests.cs
+++ b/src/DotNext.Tests/Buffers/Binary/Leb128Tests.cs
@@ -83,4 +83,12 @@ public static void DifferenceBetweenSignedAndUnsignedEncoding()
True(Leb128.TryGetBytes(0x7F, buffer, out bytesWritten));
Equal(2, bytesWritten);
}
+
+ [Fact]
+ public static void MaxSizeInBytes()
+ {
+ Equal(sizeof(uint) + 1, Leb128.MaxSizeInBytes);
+ Equal(sizeof(ulong) + 2, Leb128.MaxSizeInBytes);
+ Equal(16 + 3, Leb128.MaxSizeInBytes);
+ }
}
\ No newline at end of file
diff --git a/src/DotNext.Tests/Buffers/BufferWriterSlimTests.cs b/src/DotNext.Tests/Buffers/BufferWriterSlimTests.cs
index ce8975f6ba..11e71be962 100644
--- a/src/DotNext.Tests/Buffers/BufferWriterSlimTests.cs
+++ b/src/DotNext.Tests/Buffers/BufferWriterSlimTests.cs
@@ -1,3 +1,4 @@
+using System.Collections.Immutable;
using System.Numerics;
using System.Text;
using DotNext.Buffers.Binary;
@@ -19,7 +20,7 @@ public static void GrowableBuffer()
Equal(2, builder.Capacity);
Equal(2, builder.FreeCapacity);
- builder.Write(stackalloc int[] { 10, 20 });
+ builder.Write([10, 20]);
Equal(2, builder.WrittenCount);
Equal(2, builder.Capacity);
Equal(0, builder.FreeCapacity);
@@ -27,7 +28,7 @@ public static void GrowableBuffer()
Equal(10, builder[0]);
Equal(20, builder[1]);
- builder.Write(stackalloc int[] { 30, 40 });
+ builder.Write([30, 40]);
Equal(4, builder.WrittenCount);
True(builder.Capacity >= 2);
Equal(30, builder[2]);
@@ -39,7 +40,7 @@ public static void GrowableBuffer()
builder.Clear(true);
Equal(0, builder.WrittenCount);
- builder.Write(stackalloc int[] { 50, 60, 70, 80 });
+ builder.Write([50, 60, 70, 80]);
Equal(4, builder.WrittenCount);
True(builder.Capacity >= 2);
Equal(50, builder[0]);
@@ -47,9 +48,9 @@ public static void GrowableBuffer()
Equal(70, builder[2]);
Equal(80, builder[3]);
- builder.Clear(false);
+ builder.Clear();
Equal(0, builder.WrittenCount);
- builder.Write(stackalloc int[] { 10, 20, 30, 40 });
+ builder.Write([10, 20, 30, 40]);
Equal(4, builder.WrittenCount);
True(builder.Capacity >= 2);
Equal(10, builder[0]);
@@ -404,7 +405,7 @@ public static void WriteLengthPrefixedBytes(LengthFormat format)
using var buffer = writer.DetachOrCopyBuffer();
var reader = IAsyncBinaryReader.Create(buffer.Memory);
- using var actual = reader.ReadBlock(format);
+ using var actual = reader.ReadBlock(format, allocator: null);
Equal(expected, actual.Span);
}
@@ -455,4 +456,66 @@ public static void EncodeDecodeString(string encodingName, LengthFormat? format)
Equal(expected, actual.Span);
}
}
+
+ [Fact]
+ public static void AddList()
+ {
+ var writer = new BufferWriterSlim();
+ try
+ {
+ writer.AddAll(new List { 1, 2 });
+ Equal([1, 2], writer.WrittenSpan);
+ }
+ finally
+ {
+ writer.Dispose();
+ }
+ }
+
+ [Fact]
+ public static void AddArray()
+ {
+ var writer = new BufferWriterSlim();
+ try
+ {
+ writer.AddAll([1, 2]);
+ Equal([1, 2], writer.WrittenSpan);
+ }
+ finally
+ {
+ writer.Dispose();
+ }
+ }
+
+ [Fact]
+ public static void AddString()
+ {
+ var writer = new BufferWriterSlim();
+ try
+ {
+ const string expected = "ab";
+ writer.AddAll(expected);
+
+ Equal(expected, writer.WrittenSpan);
+ }
+ finally
+ {
+ writer.Dispose();
+ }
+ }
+
+ [Fact]
+ public static void AddCountableCollection()
+ {
+ var writer = new BufferWriterSlim();
+ try
+ {
+ writer.AddAll(ImmutableList.Create(1, 2));
+ Equal([1, 2], writer.WrittenSpan);
+ }
+ finally
+ {
+ writer.Dispose();
+ }
+ }
}
\ No newline at end of file
diff --git a/src/DotNext.Tests/Buffers/ChunkSequenceTests.cs b/src/DotNext.Tests/Buffers/ChunkSequenceTests.cs
index 0df4a8037a..dce6ca3771 100644
--- a/src/DotNext.Tests/Buffers/ChunkSequenceTests.cs
+++ b/src/DotNext.Tests/Buffers/ChunkSequenceTests.cs
@@ -46,7 +46,19 @@ static IEnumerable> ToEnumerable(ReadOnlyMemory block
}
[Fact]
- public static void StringConcatenation()
+ public static void Concatenation3()
+ {
+ Equal([], Memory.ToReadOnlySequence([]).ToArray());
+
+ ReadOnlyMemory block1 = new byte[] { 1, 2 };
+ Equal(block1.Span, Memory.ToReadOnlySequence([block1]).ToArray());
+
+ ReadOnlyMemory block2 = new byte[] { 3, 4 };
+ Equal([1, 2, 3, 4], Memory.ToReadOnlySequence([block1, block2]).ToArray());
+ }
+
+ [Fact]
+ public static void StringConcatenation1()
{
string block1 = string.Empty, block2 = null;
Equal(string.Empty, new[] { block1, block2 }.ToReadOnlySequence().ToString());
@@ -68,6 +80,18 @@ static IEnumerable ToEnumerable(string block1, string block2)
yield return block2;
}
}
+
+ [Fact]
+ public static void StringConcatenation2()
+ {
+ Equal([], Memory.ToReadOnlySequence(ReadOnlySpan.Empty).ToArray());
+
+ const string block1 = "Hello";
+ Equal(block1, Memory.ToReadOnlySequence([block1]).ToString());
+
+ const string block2 = ", world!";
+ Equal(block1 + block2, Memory.ToReadOnlySequence([block1, block2]).ToString());
+ }
[Fact]
public static void CopyFromSequence()
diff --git a/src/DotNext.Tests/Buffers/MemoryOwnerTests.cs b/src/DotNext.Tests/Buffers/MemoryOwnerTests.cs
index 7b35edf465..b1670a487b 100644
--- a/src/DotNext.Tests/Buffers/MemoryOwnerTests.cs
+++ b/src/DotNext.Tests/Buffers/MemoryOwnerTests.cs
@@ -127,4 +127,20 @@ public static void ResizeBuffer()
True(buffer.TryResize(10));
}
+
+ [Fact]
+ public static void FromFactoryWithSize()
+ {
+ const int size = 512;
+ using var buffer = new MemoryOwner(MemoryPool.Shared.Rent, size);
+ True(buffer.Length >= size);
+ }
+
+ [Fact]
+ public static void FromFactoryWithoutSize()
+ {
+ const int size = 512;
+ using var buffer = new MemoryOwner(static () => MemoryPool.Shared.Rent(size));
+ True(buffer.Length >= size);
+ }
}
\ No newline at end of file
diff --git a/src/DotNext.Tests/Buffers/SpanReaderWriterTests.cs b/src/DotNext.Tests/Buffers/SpanReaderWriterTests.cs
index d353a09186..e3661097b5 100644
--- a/src/DotNext.Tests/Buffers/SpanReaderWriterTests.cs
+++ b/src/DotNext.Tests/Buffers/SpanReaderWriterTests.cs
@@ -449,8 +449,7 @@ public static void WriteLengthPrefixedBytes(LengthFormat format)
True(writer.Write(expected, format) > 0);
var reader = IAsyncBinaryReader.Create(buffer.AsMemory(0, writer.WrittenCount));
- using var actual = reader.ReadBlock(format);
- Equal(expected, actual.Span);
+ Equal(expected, reader.ReadBlock(format).FirstSpan);
}
private static void EncodeDecodeLeb128(ReadOnlySpan values)
diff --git a/src/DotNext.Tests/Buffers/UnmanagedMemoryPoolTests.cs b/src/DotNext.Tests/Buffers/UnmanagedMemoryPoolTests.cs
index 6dff5a604d..eb8bb8c684 100644
--- a/src/DotNext.Tests/Buffers/UnmanagedMemoryPoolTests.cs
+++ b/src/DotNext.Tests/Buffers/UnmanagedMemoryPoolTests.cs
@@ -2,6 +2,8 @@
namespace DotNext.Buffers;
+using static Runtime.Intrinsics;
+
public sealed class UnmanagedMemoryPoolTests : Test
{
[Fact]
@@ -234,5 +236,7 @@ public static unsafe void MarshalAsMemory()
var memory = UnmanagedMemory.AsMemory(ptr, 3);
False(memory.IsEmpty);
Equal([10, 20, 30], memory.Span);
+
+ KeepAlive(in memory);
}
}
\ No newline at end of file
diff --git a/src/DotNext.Tests/Collections/Generic/ListTests.cs b/src/DotNext.Tests/Collections/Generic/ListTests.cs
index 8b75914fc3..e12e2f0243 100644
--- a/src/DotNext.Tests/Collections/Generic/ListTests.cs
+++ b/src/DotNext.Tests/Collections/Generic/ListTests.cs
@@ -34,7 +34,7 @@ public static void OrderedInsertion()
[Fact]
public static void ReadOnlyView()
{
- var view = new ReadOnlyListView(new[] { "1", "2", "3" }, new Converter(int.Parse));
+ var view = new ReadOnlyListView(["1", "2", "3"], new Converter(int.Parse));
Equal(3, view.Count);
Equal(1, view[0]);
Equal(2, view[1]);
@@ -46,7 +46,7 @@ public static void ReadOnlyView()
[Fact]
public static void ReadOnlyIndexer()
{
- IReadOnlyList array = new[] { 5L, 6L, 20L };
+ IReadOnlyList array = [5L, 6L, 20L];
Equal(20L, List.Indexer.ReadOnly(array, 2));
Equal(6L, array.IndexerGetter().Invoke(1));
}
@@ -54,7 +54,7 @@ public static void ReadOnlyIndexer()
[Fact]
public static void Indexer()
{
- IList array = new[] { 5L, 6L, 30L };
+ IList array = [5L, 6L, 30L];
Equal(30L, List.Indexer.Getter(array, 2));
List.Indexer.Setter(array, 1, 10L);
Equal(10L, array.IndexerGetter().Invoke(1));
@@ -99,8 +99,8 @@ private static void SliceTest(IList list)
public static void SliceList()
{
SliceTest(new List { 10L, 20L, 30L, 40L });
- SliceTest(new[] { 10L, 20L, 30L, 40L });
- SliceTest(new ArraySegment(new[] { 10L, 20L, 30L, 40L }, 0, 4));
+ SliceTest([10L, 20L, 30L, 40L]);
+ SliceTest(new ArraySegment([10L, 20L, 30L, 40L], 0, 4));
}
[Fact]
@@ -117,7 +117,7 @@ public static void InsertRemove()
[Fact]
public static void ArraySlice()
{
- var segment = List.Slice(new int[] { 10, 20, 30 }, 0..2);
+ var segment = List.Slice([10, 20, 30], 0..2);
True(segment.TryGetSpan(out var span));
Equal(2, span.Length);
Equal(10, span[0]);
diff --git a/src/DotNext.Tests/DotNext.Tests.csproj b/src/DotNext.Tests/DotNext.Tests.csproj
index 8bee2de2b3..23e778e5a4 100644
--- a/src/DotNext.Tests/DotNext.Tests.csproj
+++ b/src/DotNext.Tests/DotNext.Tests.csproj
@@ -6,7 +6,7 @@
latest
true
false
- 5.16.1
+ 5.17.0
false
.NET Foundation and Contributors
.NEXT Family of Libraries
@@ -52,6 +52,7 @@
+
diff --git a/src/DotNext.Tests/GlobalUsings.cs b/src/DotNext.Tests/GlobalUsings.cs
deleted file mode 100644
index 8c927eb747..0000000000
--- a/src/DotNext.Tests/GlobalUsings.cs
+++ /dev/null
@@ -1 +0,0 @@
-global using Xunit;
\ No newline at end of file
diff --git a/src/DotNext.Tests/IO/AsyncBinaryReaderWriterTests.cs b/src/DotNext.Tests/IO/AsyncBinaryReaderWriterTests.cs
index f30b0e5e27..3777f99f30 100644
--- a/src/DotNext.Tests/IO/AsyncBinaryReaderWriterTests.cs
+++ b/src/DotNext.Tests/IO/AsyncBinaryReaderWriterTests.cs
@@ -143,9 +143,9 @@ private sealed class FileSource : Disposable, IAsyncBinaryReaderWriterSource, IF
public FileSource(int bufferSize)
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read, FileOptions.Asynchronous);
- writer = new(handle, bufferSize: bufferSize);
- reader = new(handle, bufferSize: bufferSize);
+ handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read, FileOptions.Asynchronous | FileOptions.DeleteOnClose);
+ writer = new(handle) { MaxBufferSize = bufferSize };
+ reader = new(handle) { MaxBufferSize = bufferSize };
}
public IAsyncBinaryWriter CreateWriter() => writer;
@@ -156,8 +156,10 @@ public FileSource(int bufferSize)
void IFlushable.Flush()
{
- using (var task = FlushAsync(CancellationToken.None))
- task.Wait(DefaultTimeout);
+ using var cts = new CancellationTokenSource();
+ using var task = FlushAsync(cts.Token);
+ cts.CancelAfter(DefaultTimeout);
+ task.Wait();
}
protected override void Dispose(bool disposing)
diff --git a/src/DotNext.Tests/IO/DictionarySerializer.cs b/src/DotNext.Tests/IO/DictionarySerializer.cs
index 843ef46d7f..a7ad7d136d 100644
--- a/src/DotNext.Tests/IO/DictionarySerializer.cs
+++ b/src/DotNext.Tests/IO/DictionarySerializer.cs
@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;
using DotNext.Buffers;
+using DotNext.Text;
namespace DotNext.IO;
@@ -12,11 +13,13 @@ internal static async Task SerializeAsync(IReadOnlyDictionary di
// write count
await output.WriteLittleEndianAsync(dictionary.Count, buffer);
+ var context = new EncodingContext(Encoding.UTF8, reuseEncoder: true);
+
// write pairs
foreach (var (key, value) in dictionary)
{
- await output.EncodeAsync(key.AsMemory(), Encoding.UTF8, LengthFormat.LittleEndian, buffer);
- await output.EncodeAsync(value.AsMemory(), Encoding.UTF8, LengthFormat.LittleEndian, buffer);
+ await output.EncodeAsync(key.AsMemory(), context, LengthFormat.LittleEndian, buffer);
+ await output.EncodeAsync(value.AsMemory(), context, LengthFormat.LittleEndian, buffer);
}
}
@@ -24,11 +27,12 @@ internal static async Task> DeserializeAsync
{
var count = await input.ReadLittleEndianAsync(buffer);
var result = new Dictionary(count);
+ var context = new DecodingContext(Encoding.UTF8, reuseDecoder: true);
while (--count >= 0)
{
- using var key = await input.DecodeAsync(Encoding.UTF8, LengthFormat.LittleEndian, buffer);
- using var value = await input.DecodeAsync(Encoding.UTF8, LengthFormat.LittleEndian, buffer);
+ using var key = await input.DecodeAsync(context, LengthFormat.LittleEndian, buffer);
+ using var value = await input.DecodeAsync(context, LengthFormat.LittleEndian, buffer);
result.Add(key.ToString(), value.ToString());
}
diff --git a/src/DotNext.Tests/IO/FileReaderTests.cs b/src/DotNext.Tests/IO/FileReaderTests.cs
index 582fa06d6c..15b71f5664 100644
--- a/src/DotNext.Tests/IO/FileReaderTests.cs
+++ b/src/DotNext.Tests/IO/FileReaderTests.cs
@@ -6,7 +6,8 @@ public sealed class FileReaderTests : Test
public static async Task SimpleReadAsync()
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous);
+ using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None,
+ FileOptions.Asynchronous | FileOptions.DeleteOnClose);
using var reader = new FileReader(handle);
False(reader.HasBufferedData);
True(reader.Buffer.IsEmpty);
@@ -26,26 +27,27 @@ public static async Task SimpleReadAsync()
True(reader.As().TryGetRemainingBytesCount(out remainingCount));
Equal(expected.Length, remainingCount);
- Equal(expected, reader.Buffer.ToArray());
+ Equal(expected, reader.Buffer);
}
[Fact]
public static async Task ReadBufferTwiceAsync()
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous);
- using var reader = new FileReader(handle, bufferSize: 32);
+ using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None,
+ FileOptions.Asynchronous | FileOptions.DeleteOnClose);
+ using var reader = new FileReader(handle) { MaxBufferSize = 32 };
var expected = RandomBytes(reader.MaxBufferSize * 2);
await RandomAccess.WriteAsync(handle, expected, 0L);
True(await reader.ReadAsync());
- Equal(expected.AsMemory(0, reader.Buffer.Length).ToArray(), reader.Buffer.ToArray());
+ Equal(expected.AsMemory(0, reader.Buffer.Length), reader.Buffer);
reader.Consume(16);
True(await reader.ReadAsync());
- Equal(expected.AsMemory(16, reader.Buffer.Length).ToArray(), reader.Buffer.ToArray());
+ Equal(expected.AsMemory(16, reader.Buffer.Length), reader.Buffer);
reader.Consume(16);
True(await reader.ReadAsync());
@@ -60,20 +62,19 @@ public static async Task ReadBufferTwiceAsync()
public static async Task ReadLargeDataAsync()
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous);
- using var reader = new FileReader(handle, bufferSize: 32);
+ using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None,
+ FileOptions.Asynchronous | FileOptions.DeleteOnClose);
+ using var reader = new FileReader(handle) { MaxBufferSize = 32 };
var expected = RandomBytes(reader.MaxBufferSize * 2);
await RandomAccess.WriteAsync(handle, expected, 0L);
True(await reader.ReadAsync());
- Equal(expected.AsMemory(0, reader.Buffer.Length).ToArray(), reader.Buffer.ToArray());
+ Equal(expected.AsMemory(0, reader.Buffer.Length), reader.Buffer);
var actual = new byte[expected.Length];
Equal(actual.Length, await reader.ReadAsync(actual));
-
Equal(expected, actual);
-
False(await reader.ReadAsync());
}
@@ -81,7 +82,8 @@ public static async Task ReadLargeDataAsync()
public static void SimpleRead()
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous);
+ using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None,
+ FileOptions.Asynchronous | FileOptions.DeleteOnClose);
using var reader = new FileReader(handle);
False(reader.HasBufferedData);
True(reader.Buffer.IsEmpty);
@@ -93,26 +95,27 @@ public static void SimpleRead()
True(reader.HasBufferedData);
False(reader.Buffer.IsEmpty);
- Equal(expected, reader.Buffer.ToArray());
+ Equal(expected, reader.Buffer);
}
[Fact]
public static void ReadBufferTwice()
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous);
- using var reader = new FileReader(handle, bufferSize: 32);
+ using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None,
+ FileOptions.Asynchronous | FileOptions.DeleteOnClose);
+ using var reader = new FileReader(handle) { MaxBufferSize = 32 };
var expected = RandomBytes(reader.MaxBufferSize * 2);
RandomAccess.Write(handle, expected, 0L);
True(reader.Read());
- Equal(expected.AsMemory(0, reader.Buffer.Length).ToArray(), reader.Buffer.ToArray());
+ Equal(expected.AsMemory(0, reader.Buffer.Length), reader.Buffer);
reader.Consume(16);
True(reader.Read());
- Equal(expected.AsMemory(16, reader.Buffer.Length).ToArray(), reader.Buffer.ToArray());
+ Equal(expected.AsMemory(16, reader.Buffer.Length), reader.Buffer);
reader.Consume(16);
True(reader.Read());
@@ -127,14 +130,15 @@ public static void ReadBufferTwice()
public static void ReadLargeData()
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous);
- using var reader = new FileReader(handle, bufferSize: 32);
+ using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None,
+ FileOptions.Asynchronous | FileOptions.DeleteOnClose);
+ using var reader = new FileReader(handle) { MaxBufferSize = 32 };
var expected = RandomBytes(reader.MaxBufferSize * 2);
RandomAccess.Write(handle, expected, 0L);
True(reader.Read());
- Equal(expected.AsMemory(0, reader.Buffer.Length).ToArray(), reader.Buffer.ToArray());
+ Equal(expected.AsMemory(0, reader.Buffer.Length), reader.Buffer);
var actual = new byte[expected.Length];
Equal(actual.Length, reader.Read(actual));
@@ -148,8 +152,9 @@ public static void ReadLargeData()
public static async Task ReadSequentially()
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- await using var fs = new FileStream(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.Asynchronous);
- using var reader = new FileReader(fs, bufferSize: 32);
+ await using var fs = new FileStream(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, 4096,
+ FileOptions.Asynchronous | FileOptions.DeleteOnClose);
+ using var reader = new FileReader(fs) { MaxBufferSize = 32 };
var bytes = RandomBytes(1024);
await fs.WriteAsync(bytes);
diff --git a/src/DotNext.Tests/IO/FileWriterTests.cs b/src/DotNext.Tests/IO/FileWriterTests.cs
index 3d00484e7b..4cced67e31 100644
--- a/src/DotNext.Tests/IO/FileWriterTests.cs
+++ b/src/DotNext.Tests/IO/FileWriterTests.cs
@@ -11,8 +11,9 @@ public sealed class FileWriterTests : Test
public static async Task WriteWithoutOverflowAsync()
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous);
- using var writer = new FileWriter(handle, bufferSize: 64);
+ using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None,
+ FileOptions.Asynchronous | FileOptions.DeleteOnClose);
+ using var writer = new FileWriter(handle) { MaxBufferSize = 64 };
False(writer.HasBufferedData);
Equal(0L, writer.FilePosition);
@@ -34,8 +35,9 @@ public static async Task WriteWithoutOverflowAsync()
public static async Task WriteWithOverflowAsync()
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous);
- using var writer = new FileWriter(handle, bufferSize: 64);
+ using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None,
+ FileOptions.Asynchronous | FileOptions.DeleteOnClose);
+ using var writer = new FileWriter(handle) { MaxBufferSize = 64 };
var expected = RandomBytes(writer.Buffer.Length + 10);
await writer.WriteAsync(expected);
@@ -52,8 +54,9 @@ public static async Task WriteWithOverflowAsync()
public static async Task WritDirectAsync()
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous);
- using var writer = new FileWriter(handle, bufferSize: 64);
+ using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None,
+ FileOptions.Asynchronous | FileOptions.DeleteOnClose);
+ using var writer = new FileWriter(handle) { MaxBufferSize = 64 };
var expected = RandomBytes(writer.Buffer.Length << 2);
await writer.WriteAsync(expected.AsMemory(0, 63));
@@ -71,8 +74,8 @@ public static async Task WritDirectAsync()
public static void WriteWithoutOverflow()
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.None);
- using var writer = new FileWriter(handle, bufferSize: 64);
+ using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.DeleteOnClose);
+ using var writer = new FileWriter(handle) { MaxBufferSize = 64 };
False(writer.HasBufferedData);
Equal(0L, writer.FilePosition);
@@ -94,8 +97,8 @@ public static void WriteWithoutOverflow()
public static void WriteWithOverflow()
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.None);
- using var writer = new FileWriter(handle, bufferSize: 64);
+ using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.DeleteOnClose);
+ using var writer = new FileWriter(handle) { MaxBufferSize = 64 };
var expected = RandomBytes(writer.Buffer.Length + 10);
writer.Write(expected);
@@ -112,8 +115,8 @@ public static void WriteWithOverflow()
public static void WriteUsingBufferWriter()
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- using var fs = new FileStream(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.None);
- using var writer = new FileWriter(fs, bufferSize: 64);
+ using var fs = new FileStream(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.DeleteOnClose);
+ using var writer = new FileWriter(fs) { MaxBufferSize = 64 };
False(writer.HasBufferedData);
Equal(0L, writer.FilePosition);
@@ -135,8 +138,8 @@ public static void WriteUsingBufferWriter()
public static void WriteUsingBufferWriter2()
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.None);
- using var writer = new FileWriter(handle, bufferSize: 64);
+ using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.DeleteOnClose);
+ using var writer = new FileWriter(handle) { MaxBufferSize = 64 };
False(writer.HasBufferedData);
Equal(0L, writer.FilePosition);
@@ -159,8 +162,9 @@ public static void WriteUsingBufferWriter2()
public static async Task FlushWithOffsetAsync()
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous);
- using var writer = new FileWriter(handle, fileOffset: 100L, bufferSize: 64);
+ using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None,
+ FileOptions.Asynchronous | FileOptions.DeleteOnClose);
+ using var writer = new FileWriter(handle) { FilePosition = 100L, MaxBufferSize = 64 };
writer.Buffer.Span[0] = 1;
writer.Buffer.Span[1] = 2;
writer.Produce(2);
@@ -176,8 +180,9 @@ public static async Task FlushWithOffsetAsync()
public static async Task WriteDirect()
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous);
- using var writer = new FileWriter(handle, fileOffset: 0L, bufferSize: 64);
+ using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None,
+ FileOptions.Asynchronous | FileOptions.DeleteOnClose);
+ using var writer = new FileWriter(handle) { MaxBufferSize = 64 };
await writer.WriteAsync(new Blittable { Value = default });
False(writer.HasBufferedData);
Equal(writer.FilePosition, Unsafe.SizeOf());
@@ -187,14 +192,27 @@ public static async Task WriteDirect()
public static async Task BufferOverflow()
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous);
- using var writer = new FileWriter(handle, fileOffset: 0L, bufferSize: 64);
+ using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None,
+ FileOptions.Asynchronous | FileOptions.DeleteOnClose);
+ using var writer = new FileWriter(handle) { MaxBufferSize = 64 };
await writer.WriteAsync(new byte[2]);
await writer.WriteAsync(new Blittable { Value = default });
False(writer.HasBufferedData);
Equal(writer.FilePosition, Unsafe.SizeOf() + 2);
}
+ [Fact]
+ public static void TryWrite()
+ {
+ var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ using var handle = File.OpenHandle(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, FileOptions.DeleteOnClose);
+ using var writer = new FileWriter(handle) { MaxBufferSize = 64 };
+
+ True(writer.TryWrite(new byte[2]));
+ True(writer.HasBufferedData);
+ Equal("\0\0"u8, writer.WrittenBuffer.Span);
+ }
+
[InlineArray(512)]
private struct Buffer512
{
diff --git a/src/DotNext.Tests/IO/Pipelines/PipeExtensionsTests.cs b/src/DotNext.Tests/IO/Pipelines/PipeExtensionsTests.cs
index efdea66410..872c4f9206 100644
--- a/src/DotNext.Tests/IO/Pipelines/PipeExtensionsTests.cs
+++ b/src/DotNext.Tests/IO/Pipelines/PipeExtensionsTests.cs
@@ -302,6 +302,39 @@ public static async Task ReadPortionAsync()
}
}
+ [Fact]
+ public static async Task ReadBlockExactlyAsync()
+ {
+ var bytes = RandomBytes(128);
+
+ var reader = PipeReader.Create(new(bytes));
+
+ using var destination = new MemoryStream();
+ await foreach (var chunk in reader.ReadExactlyAsync(64L))
+ {
+ await destination.WriteAsync(chunk);
+ }
+
+ Equal(new ReadOnlySpan(bytes, 0, 64), destination.ToArray());
+ }
+
+ [Fact]
+ public static void ReadEmptyBlockAsync()
+ {
+ var reader = PipeReader.Create(ReadOnlySequence.Empty);
+
+ Empty(reader.ReadExactlyAsync(0L));
+ }
+
+ [Fact]
+ public static async Task ReadInvalidSizedBlockAsync()
+ {
+ var reader = PipeReader.Create(ReadOnlySequence.Empty);
+
+ await using var enumerator = reader.ReadExactlyAsync(-1L).GetAsyncEnumerator();
+ await ThrowsAsync(enumerator.MoveNextAsync().AsTask);
+ }
+
[Fact]
public static async Task DecodeNullTerminatedStringAsync()
{
diff --git a/src/DotNext.Tests/IO/PoolingBufferedStreamTests.cs b/src/DotNext.Tests/IO/PoolingBufferedStreamTests.cs
new file mode 100644
index 0000000000..6747a9b96c
--- /dev/null
+++ b/src/DotNext.Tests/IO/PoolingBufferedStreamTests.cs
@@ -0,0 +1,344 @@
+using DotNext.Buffers;
+
+namespace DotNext.IO;
+
+public sealed class PoolingBufferedStreamTests : Test
+{
+ [Fact]
+ public static void SimpleRead()
+ {
+ const int bufferSize = 4096;
+ var expected = RandomBytes(bufferSize);
+ using var stream = new MemoryStream(expected.Length);
+ stream.Write(expected);
+ stream.Seek(0L, SeekOrigin.Begin);
+
+ using var bufferedStream = new PoolingBufferedStream(stream, leaveOpen: true) { MaxBufferSize = bufferSize };
+ Same(stream, bufferedStream.BaseStream);
+ False(bufferedStream.HasBufferedDataToRead);
+ False(bufferedStream.HasBufferedDataToWrite);
+ Equal(stream.Position, bufferedStream.Position);
+
+ Equal(expected[0], bufferedStream.ReadByte());
+ True(bufferedStream.HasBufferedDataToRead);
+ False(bufferedStream.HasBufferedDataToWrite);
+ Equal(bufferSize, stream.Position);
+ Equal(1L, bufferedStream.Position);
+
+ var actual = new byte[bufferSize - 1];
+ Equal(actual.Length, bufferedStream.Read(actual));
+ Equal(stream.Position, bufferedStream.Position);
+ Equal(expected.AsSpan(1), actual.AsSpan());
+
+ Equal(0, bufferedStream.Read(actual));
+ Equal(-1, bufferedStream.ReadByte());
+ False(bufferedStream.HasBufferedDataToRead);
+ False(bufferedStream.HasBufferedDataToWrite);
+ }
+
+ [Fact]
+ public static async Task SimpleReadAsync()
+ {
+ const int bufferSize = 4096;
+ var expected = RandomBytes(bufferSize);
+ await using var stream = new MemoryStream(expected.Length);
+ stream.Write(expected);
+ stream.Seek(0L, SeekOrigin.Begin);
+
+ await using var bufferedStream = new PoolingBufferedStream(stream, leaveOpen: true) { MaxBufferSize = bufferSize };
+ False(bufferedStream.HasBufferedDataToRead);
+ False(bufferedStream.HasBufferedDataToWrite);
+ Equal(stream.Position, bufferedStream.Position);
+
+ Equal(expected[0], bufferedStream.ReadByte());
+ True(bufferedStream.HasBufferedDataToRead);
+ False(bufferedStream.HasBufferedDataToWrite);
+ Equal(bufferSize, stream.Position);
+ Equal(1L, bufferedStream.Position);
+
+ var actual = new byte[bufferSize - 1];
+ Equal(actual.Length, await bufferedStream.ReadAsync(actual));
+ Equal(stream.Position, bufferedStream.Position);
+ Equal(expected.AsSpan(1), actual.AsSpan());
+
+ Equal(0, await bufferedStream.ReadAsync(actual));
+ Equal(-1, bufferedStream.ReadByte());
+ False(bufferedStream.HasBufferedDataToRead);
+ False(bufferedStream.HasBufferedDataToWrite);
+ }
+
+ [Fact]
+ public static void BufferizeAndAdvancePosition()
+ {
+ const int bufferSize = 4096;
+ var expected = RandomBytes(bufferSize);
+ using var stream = new MemoryStream(expected.Length);
+ stream.Write(expected);
+ stream.Seek(0L, SeekOrigin.Begin);
+
+ using var bufferedStream = new PoolingBufferedStream(stream, leaveOpen: true) { MaxBufferSize = bufferSize };
+ True(bufferedStream.Read());
+
+ bufferedStream.Position = bufferSize / 2;
+ True(bufferedStream.HasBufferedDataToRead);
+
+ var actual = new byte[bufferSize / 2];
+ Equal(actual.Length, bufferedStream.Read(actual, 0, actual.Length));
+ Equal(stream.Position, bufferedStream.Position);
+
+ Equal(expected.AsSpan(bufferSize / 2), actual.AsSpan());
+ }
+
+ [Fact]
+ public static async Task BufferizeAndAdvancePositionAsync()
+ {
+ const int bufferSize = 4096;
+ var expected = RandomBytes(bufferSize);
+ using var stream = new MemoryStream(expected.Length);
+ stream.Write(expected);
+ stream.Seek(0L, SeekOrigin.Begin);
+
+ await using var bufferedStream = new PoolingBufferedStream(stream, leaveOpen: true) { MaxBufferSize = bufferSize };
+ True(await bufferedStream.ReadAsync());
+
+ bufferedStream.Position = bufferSize / 2;
+ Equal(bufferSize, stream.Position);
+ True(bufferedStream.HasBufferedDataToRead);
+
+ var actual = new byte[bufferSize / 2];
+ Equal(actual.Length, await bufferedStream.ReadAsync(actual, 0, actual.Length));
+ Equal(stream.Position, bufferedStream.Position);
+
+ Equal(expected.AsSpan(bufferSize / 2), actual.AsSpan());
+ }
+
+ [Fact]
+ public static void DrainBuffer()
+ {
+ const int bufferSize = 4096;
+ using var bufferedStream = new PoolingBufferedStream(new MemoryStream(bufferSize)) { MaxBufferSize = bufferSize };
+
+ var expected = RandomBytes(bufferSize);
+ bufferedStream.Write(expected);
+ True(bufferedStream.HasBufferedDataToWrite);
+ False(bufferedStream.HasBufferedDataToRead);
+
+ Equal(0L, bufferedStream.BaseStream.Position);
+ Equal(bufferSize, bufferedStream.Length);
+
+ bufferedStream.Write();
+ Equal(bufferSize, bufferedStream.BaseStream.Position);
+ Equal(bufferedStream.BaseStream.Length, bufferedStream.Length);
+ }
+
+ [Fact]
+ public static async Task DrainBufferAsync()
+ {
+ const int bufferSize = 4096;
+ await using var bufferedStream = new PoolingBufferedStream(new MemoryStream(bufferSize)) { MaxBufferSize = bufferSize };
+
+ var expected = RandomBytes(bufferSize);
+ await bufferedStream.WriteAsync(expected);
+ True(bufferedStream.HasBufferedDataToWrite);
+ False(bufferedStream.HasBufferedDataToRead);
+
+ Equal(0L, bufferedStream.BaseStream.Position);
+ Equal(bufferSize, bufferedStream.Length);
+
+ await bufferedStream.WriteAsync();
+ Equal(bufferSize, bufferedStream.BaseStream.Position);
+ Equal(bufferedStream.BaseStream.Length, bufferedStream.Length);
+ }
+
+ [Fact]
+ public static void CheckProperties()
+ {
+ using var stream = new MemoryStream();
+ using var bufferedStream = new PoolingBufferedStream(stream, leaveOpen: true);
+
+ Equal(stream.CanRead, bufferedStream.CanRead);
+ Equal(stream.CanWrite, bufferedStream.CanWrite);
+ Equal(stream.CanSeek, bufferedStream.CanSeek);
+ Equal(stream.CanTimeout, bufferedStream.CanTimeout);
+
+ Throws(() => stream.ReadTimeout);
+ Throws(() => stream.ReadTimeout = 10);
+
+ Throws(() => stream.WriteTimeout);
+ Throws(() => stream.WriteTimeout = 10);
+ }
+
+ [Fact]
+ public static async Task BufferedWriter()
+ {
+ const int bufferSize = 4096;
+ await using var bufferedStream = new PoolingBufferedStream(new MemoryStream(bufferSize)) { MaxBufferSize = bufferSize };
+
+ var expected = RandomBytes(bufferSize);
+ IBufferedWriter writer = bufferedStream;
+ expected.CopyTo(writer.Buffer);
+ writer.Produce(expected.Length);
+ True(bufferedStream.HasBufferedDataToWrite);
+ False(bufferedStream.HasBufferedDataToRead);
+ await writer.WriteAsync();
+
+ False(bufferedStream.HasBufferedDataToWrite);
+ False(bufferedStream.HasBufferedDataToRead);
+ }
+
+ [Fact]
+ public static async Task BufferedReader()
+ {
+ const int bufferSize = 4096;
+ await using var bufferedStream = new PoolingBufferedStream(new MemoryStream(bufferSize)) { MaxBufferSize = bufferSize };
+
+ var expected = RandomBytes(bufferSize);
+ await bufferedStream.WriteAsync(expected);
+ await bufferedStream.WriteAsync();
+ bufferedStream.Position = 0L;
+
+ False(bufferedStream.HasBufferedDataToRead);
+ IBufferedReader reader = bufferedStream;
+ await reader.ReadAsync();
+ True(bufferedStream.HasBufferedDataToRead);
+ Equal(expected, reader.Buffer);
+ }
+
+ [Fact]
+ public static void InvalidReaderWriterStates()
+ {
+ const int bufferSize = 4096;
+ using var bufferedStream = new PoolingBufferedStream(new MemoryStream(bufferSize)) { MaxBufferSize = bufferSize };
+ bufferedStream.WriteByte(42);
+ bufferedStream.WriteByte(43);
+ Throws(() => bufferedStream.As().Buffer);
+ Throws(() => bufferedStream.As().Consume(1));
+
+ bufferedStream.Flush();
+ bufferedStream.Position = 0;
+ bufferedStream.Read();
+ bufferedStream.As().Consume(1);
+ Equal([43], bufferedStream.As().Buffer.Span);
+
+ Throws(() => bufferedStream.As().Buffer);
+ Throws(() => bufferedStream.As().Produce(1));
+ }
+
+ [Fact]
+ public static void StreamLength()
+ {
+ const int bufferSize = 4096;
+ using var bufferedStream = new PoolingBufferedStream(new MemoryStream(bufferSize)) { MaxBufferSize = bufferSize };
+ Equal(0L, bufferedStream.Length);
+
+ bufferedStream.WriteByte(42);
+ bufferedStream.WriteByte(43);
+ Equal(2L, bufferedStream.Length);
+ }
+
+ [Fact]
+ public static void CopyStream()
+ {
+ const int bufferSize = 4096;
+ using var bufferedStream = new PoolingBufferedStream(new MemoryStream(bufferSize)) { MaxBufferSize = bufferSize };
+
+ var expected = RandomBytes(bufferSize);
+ bufferedStream.Write(expected);
+ bufferedStream.Flush();
+ bufferedStream.Position = 0L;
+
+ using var destination = new MemoryStream(bufferSize);
+ bufferedStream.CopyTo(destination);
+
+ Equal(expected, destination.GetBuffer());
+ }
+
+ [Fact]
+ public static void CopyStream2()
+ {
+ const int bufferSize = 4096;
+ using var bufferedStream = new PoolingBufferedStream(new MemoryStream(bufferSize)) { MaxBufferSize = bufferSize };
+
+ var expected = RandomBytes(bufferSize);
+ bufferedStream.Write(expected);
+ bufferedStream.Flush();
+ bufferedStream.Position = 0L;
+ True(bufferedStream.Read());
+
+ using var destination = new MemoryStream(bufferSize);
+ bufferedStream.CopyTo(destination);
+
+ Equal(expected, destination.GetBuffer());
+ }
+
+ [Fact]
+ public static async Task CopyStreamAsync()
+ {
+ const int bufferSize = 4096;
+ await using var bufferedStream = new PoolingBufferedStream(new MemoryStream(bufferSize)) { MaxBufferSize = bufferSize };
+
+ var expected = RandomBytes(bufferSize);
+ await bufferedStream.WriteAsync(expected);
+ await bufferedStream.FlushAsync();
+ bufferedStream.Position = 0L;
+
+ await using var destination = new MemoryStream(bufferSize);
+ await bufferedStream.CopyToAsync(destination);
+
+ Equal(expected, destination.GetBuffer());
+ }
+
+ [Fact]
+ public static async Task CopyStream2Async()
+ {
+ const int bufferSize = 4096;
+ await using var bufferedStream = new PoolingBufferedStream(new MemoryStream(bufferSize)) { MaxBufferSize = bufferSize };
+
+ var expected = RandomBytes(bufferSize);
+ await bufferedStream.WriteAsync(expected);
+ await bufferedStream.FlushAsync();
+ bufferedStream.Position = 0L;
+ True(await bufferedStream.ReadAsync());
+
+ await using var destination = new MemoryStream(bufferSize);
+ await bufferedStream.CopyToAsync(destination);
+
+ Equal(expected, destination.GetBuffer());
+ }
+
+ [Fact]
+ public static void SetLength()
+ {
+ const int bufferSize = 4096;
+ using var stream = new MemoryStream(bufferSize);
+ using var bufferedStream = new PoolingBufferedStream(stream, leaveOpen: false)
+ {
+ MaxBufferSize = bufferSize,
+ Allocator = Memory.GetArrayAllocator(),
+ };
+
+ var expected = RandomBytes(bufferSize);
+ bufferedStream.Write(expected);
+
+ bufferedStream.SetLength(bufferSize);
+ Equal(expected, stream.GetBuffer());
+ }
+
+ [Fact]
+ public static void ResetBuffer()
+ {
+ const int bufferSize = 4096;
+ using var bufferedStream = new PoolingBufferedStream(new MemoryStream(bufferSize), leaveOpen: false)
+ {
+ MaxBufferSize = bufferSize,
+ Allocator = Memory.GetArrayAllocator(),
+ };
+
+ var expected = RandomBytes(bufferSize);
+ bufferedStream.Write(expected);
+ True(bufferedStream.HasBufferedDataToWrite);
+
+ bufferedStream.Reset();
+ False(bufferedStream.HasBufferedDataToWrite);
+ }
+}
\ No newline at end of file
diff --git a/src/DotNext.Tests/IO/StreamExtensionsTests.cs b/src/DotNext.Tests/IO/StreamExtensionsTests.cs
index 6b362f6526..245fd5e4a9 100644
--- a/src/DotNext.Tests/IO/StreamExtensionsTests.cs
+++ b/src/DotNext.Tests/IO/StreamExtensionsTests.cs
@@ -1,14 +1,10 @@
using System.Buffers;
-using System.Globalization;
using System.Text;
-using static System.Globalization.CultureInfo;
-using DateTimeStyles = System.Globalization.DateTimeStyles;
namespace DotNext.IO;
-using DotNext.Buffers.Binary;
-using Net.Cluster;
-using Text;
+using Buffers.Binary;
+using Collections.Generic;
public sealed class StreamExtensionsTests : Test
{
@@ -156,7 +152,7 @@ public static async Task CombineStreamsAsync()
{
await using var ms1 = new MemoryStream([1, 2, 3]);
await using var ms2 = new MemoryStream([4, 5, 6]);
- await using var combined = ms1.Combine([ms2]);
+ await using var combined = StreamExtensions.Combine([ms1, ms2]);
var buffer = new byte[6];
await combined.ReadExactlyAsync(buffer);
@@ -169,7 +165,7 @@ public static void CopyCombinedStreams()
{
using var ms1 = new MemoryStream([1, 2, 3]);
using var ms2 = new MemoryStream([4, 5, 6]);
- using var combined = ms1.Combine([ms2]);
+ using var combined = List.Singleton(ms1).Append(ms2).Combine();
using var result = new MemoryStream();
combined.CopyTo(result, 128);
@@ -204,6 +200,37 @@ public static void ReadBytesFromCombinedStream()
Equal(-1, combined.ReadByte());
}
+ [Theory]
+ [InlineData(2)]
+ [InlineData(3)]
+ [InlineData(4)]
+ [InlineData(5)]
+ [InlineData(6)]
+ [InlineData(7)]
+ [InlineData(8)]
+ [InlineData(9)]
+ [InlineData(10)]
+ public static void CombineManyStreams(byte streamCount)
+ {
+ using var stream = GetStreams(streamCount).Combine(leaveOpen: false);
+ Equal(streamCount, stream.Length);
+ var actual = new byte[streamCount];
+ stream.ReadExactly(actual);
+ var expected = Set.Range, DisclosedEndpoint>(0, streamCount);
+ Equal(expected, actual);
+
+ static IEnumerable GetStreams(byte streamCount)
+ {
+ for (var i = 0; i < streamCount; i++)
+ {
+ var ms = new MemoryStream();
+ ms.WriteByte((byte)i);
+ ms.Seek(0L, SeekOrigin.Begin);
+ yield return ms;
+ }
+ }
+ }
+
[Fact]
public static async Task UnsupportedMethodsOfSparseStream()
{
@@ -351,4 +378,19 @@ public static void DecodeNullTerminatedEmptyString()
ms.ReadUtf8(stackalloc byte[8], writer);
Equal(string.Empty, writer.WrittenSpan.ToString());
}
+
+ [Fact]
+ public static async Task ReadBlockAsSequenceAsync()
+ {
+ var bytes = RandomBytes(1024);
+ using var source = new MemoryStream(bytes, false);
+ using var destination = new MemoryStream(1024);
+
+ await foreach (var chunk in source.ReadExactlyAsync(512L, 64))
+ {
+ await destination.WriteAsync(chunk);
+ }
+
+ Equal(new ReadOnlySpan(bytes, 0, 512), destination.ToArray());
+ }
}
\ No newline at end of file
diff --git a/src/DotNext.Tests/Net/EndPointFormatterTests.cs b/src/DotNext.Tests/Net/EndPointFormatterTests.cs
index 8b319f08e7..351bfcc459 100644
--- a/src/DotNext.Tests/Net/EndPointFormatterTests.cs
+++ b/src/DotNext.Tests/Net/EndPointFormatterTests.cs
@@ -12,14 +12,17 @@ public sealed class EndPointFormatterTests : Test
{
public static TheoryData> GetTestEndPoints()
{
+ var ip = IPAddress.Parse("192.168.0.1");
var rows = new TheoryData>();
rows.Add(new DnsEndPoint("host", 3262), EqualityComparer.Default);
- rows.Add(new IPEndPoint(IPAddress.Parse("192.168.0.1"), 3263), EqualityComparer.Default);
+ rows.Add(new IPEndPoint(ip, 3263), EqualityComparer.Default);
rows.Add(new IPEndPoint(IPAddress.Parse("2001:0db8:0000:0000:0000:8a2e:0370:7334"), 3264), EqualityComparer.Default);
rows.Add(new HttpEndPoint("192.168.0.1", 3262, true, AddressFamily.InterNetwork), EqualityComparer.Default);
rows.Add(new HttpEndPoint("192.168.0.1", 3262, false, AddressFamily.InterNetwork), EqualityComparer.Default);
rows.Add(new HttpEndPoint("2001:0db8:0000:0000:0000:8a2e:0370:7334", 3262, true, AddressFamily.InterNetworkV6),
EqualityComparer.Default);
+ rows.Add(new HttpEndPoint(ip, 8080, false), EqualityComparer.Default);
+ rows.Add(new HttpEndPoint(new IPEndPoint(ip, 8080), false), EqualityComparer.Default);
rows.Add(new HttpEndPoint("host", 3262, true), EqualityComparer.Default);
rows.Add(new HttpEndPoint("host", 3262, false), EqualityComparer.Default);
rows.Add(new UriEndPoint(new Uri("http://host:3262/")), EndPointFormatter.UriEndPointComparer);
diff --git a/src/DotNext.Tests/Net/Http/HttpEndPointTests.cs b/src/DotNext.Tests/Net/Http/HttpEndPointTests.cs
index 789b9c1746..ce142f8d01 100644
--- a/src/DotNext.Tests/Net/Http/HttpEndPointTests.cs
+++ b/src/DotNext.Tests/Net/Http/HttpEndPointTests.cs
@@ -1,4 +1,6 @@
using System.Net.Sockets;
+using System.Text;
+using Microsoft.AspNetCore.Components.Forms;
namespace DotNext.Net.Http;
@@ -64,4 +66,46 @@ public static void ToUri()
Equal(Uri.UriSchemeHttps, uri.Scheme, ignoreCase: true);
Equal(3262, ep.Port);
}
+
+ [Fact]
+ public static void Format()
+ {
+ const string expected = "http://localhost:3262/";
+ Span buffer = stackalloc char[64];
+ ISpanFormattable formattable = new HttpEndPoint(new Uri(expected));
+ True(formattable.TryFormat(buffer, out var charsWritten, ReadOnlySpan.Empty, provider: null));
+
+ Equal(expected, buffer.Slice(0, charsWritten));
+ Equal(expected, formattable.ToString(format: null, formatProvider: null));
+ }
+
+ [Fact]
+ public static void FormatAsUtf8()
+ {
+ const string expected = "http://localhost:3262/";
+ var expectedBytes = Encoding.UTF8.GetBytes(expected);
+
+ Span buffer = stackalloc byte[64];
+ IUtf8SpanFormattable formattable = new HttpEndPoint(new Uri(expected));
+ True(formattable.TryFormat(buffer, out var charsWritten, ReadOnlySpan.Empty, provider: null));
+
+ Equal(expectedBytes, buffer.Slice(0, charsWritten));
+ }
+
+ [Fact]
+ public static void Parse()
+ {
+ const string expected = "http://localhost:3262/";
+ Equal(expected, Parse(expected).ToString());
+ True(TryParse(expected, out var ep));
+ Equal(expected, ep.ToString());
+
+ static T Parse(string input)
+ where T : IParsable
+ => T.Parse(input, provider: null);
+
+ static bool TryParse(string input, out T result)
+ where T : IParsable
+ => T.TryParse(input, provider: null, out result);
+ }
}
\ No newline at end of file
diff --git a/src/DotNext.Tests/Reflection/TaskTypeTests.cs b/src/DotNext.Tests/Reflection/TaskTypeTests.cs
index 4d9c62e8a1..657665f272 100644
--- a/src/DotNext.Tests/Reflection/TaskTypeTests.cs
+++ b/src/DotNext.Tests/Reflection/TaskTypeTests.cs
@@ -39,6 +39,6 @@ public static void GetResultSynchronously()
[Fact]
public static void IsCompletedPropertyGetter()
{
- True(TaskType.GetIsCompletedGetter(Task.CompletedTask).Invoke());
+ True(Task.CompletedTask.GetIsCompletedGetter().Invoke());
}
}
\ No newline at end of file
diff --git a/src/DotNext.Tests/SpanTests.cs b/src/DotNext.Tests/SpanTests.cs
index 1a993477a6..b90a5e5a3d 100644
--- a/src/DotNext.Tests/SpanTests.cs
+++ b/src/DotNext.Tests/SpanTests.cs
@@ -562,6 +562,9 @@ public static void CheckMask()
public static void CheckLargeMask()
{
var value = RandomBytes(1024);
+ if (value[0] is byte.MaxValue)
+ value[0] = byte.MaxValue - 1;
+
var mask = value.AsSpan().ToArray();
mask.AsSpan(0, 512).Clear();
diff --git a/src/DotNext.Tests/Threading/Tasks/SynchronizationTests.cs b/src/DotNext.Tests/Threading/Tasks/SynchronizationTests.cs
index d756a21a6f..d60009f798 100644
--- a/src/DotNext.Tests/Threading/Tasks/SynchronizationTests.cs
+++ b/src/DotNext.Tests/Threading/Tasks/SynchronizationTests.cs
@@ -179,4 +179,39 @@ public static async Task WhenAllWithResult5()
Equal(40, result4.Value);
Equal(50, result5.Value);
}
+
+ [Fact]
+ public static void SynchronousWait()
+ {
+ var cts = new TaskCompletionSource();
+ var task = new ValueTask(cts.Task);
+ ThreadPool.UnsafeQueueUserWorkItem(static cts =>
+ {
+ Thread.Sleep(100);
+ cts.SetResult();
+ }, cts, preferLocal: false);
+ task.Wait();
+
+ // ensure that the current thread in not interrupted
+ Thread.Sleep(0);
+ }
+
+ [Fact]
+ public static void SynchronousWaitWithResult()
+ {
+ var cts = new TaskCompletionSource();
+ var task = new ValueTask(cts.Task);
+
+ const int expected = 42;
+ ThreadPool.UnsafeQueueUserWorkItem(static cts =>
+ {
+ Thread.Sleep(100);
+ cts.SetResult(expected);
+ }, cts, preferLocal: false);
+
+ Equal(expected, task.Wait());
+
+ // ensure that the current thread in not interrupted
+ Thread.Sleep(0);
+ }
}
\ No newline at end of file
diff --git a/src/DotNext.Threading/DotNext.Threading.csproj b/src/DotNext.Threading/DotNext.Threading.csproj
index 33f32226f5..e6407ffa25 100644
--- a/src/DotNext.Threading/DotNext.Threading.csproj
+++ b/src/DotNext.Threading/DotNext.Threading.csproj
@@ -5,9 +5,9 @@
latest
enable
true
- true
+ true
nullablePublicOnly
- 5.16.1
+ 5.17.0
.NET Foundation and Contributors
.NEXT Family of Libraries
diff --git a/src/DotNext.Threading/Threading/AsyncReaderWriterLock.cs b/src/DotNext.Threading/Threading/AsyncReaderWriterLock.cs
index 583f2d5e6b..c6d9ed16f1 100644
--- a/src/DotNext.Threading/Threading/AsyncReaderWriterLock.cs
+++ b/src/DotNext.Threading/Threading/AsyncReaderWriterLock.cs
@@ -36,6 +36,7 @@ private enum LockType : byte
// describes internal state of reader/writer lock
[StructLayout(LayoutKind.Auto)]
+ [SuppressMessage("Usage", "CA1001", Justification = "The disposable field is disposed in the Dispose() method")]
internal struct State : IDisposable
{
private ulong version; // version of write lock
diff --git a/src/DotNext.Unsafe/Buffers/UnmanagedMemory.cs b/src/DotNext.Unsafe/Buffers/UnmanagedMemory.cs
index 0eb708d296..f0aa19f6c1 100644
--- a/src/DotNext.Unsafe/Buffers/UnmanagedMemory.cs
+++ b/src/DotNext.Unsafe/Buffers/UnmanagedMemory.cs
@@ -1,5 +1,6 @@
using System.Buffers;
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -79,7 +80,7 @@ public static unsafe Memory AsMemory(T* pointer, int length)
if (length > 0)
{
- MemoryManager manager = new UnmanagedMemory((nint)pointer, length);
+ var manager = new UnmanagedMemory((nint)pointer, length);
// GC perf: manager doesn't own the memory represented by the pointer, no need to call Dispose from finalizer
GC.SuppressFinalize(manager);
@@ -146,6 +147,8 @@ internal void Reallocate(int length)
public sealed override Span GetSpan()
=> address is not null ? new(address, Length) : [];
+ public sealed override Memory Memory => address is not null ? CreateMemory(Length) : Memory.Empty;
+
public sealed override MemoryHandle Pin(int elementIndex = 0)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)elementIndex, (uint)Length, nameof(elementIndex));
@@ -167,6 +170,9 @@ protected override void Dispose(bool disposing)
address = null;
Length = 0;
}
+
+ [SuppressMessage("Reliability", "CA2015", Justification = "The caller must hold the reference to the memory object.")]
+ ~UnmanagedMemory() => Dispose(disposing: false);
}
internal class UnmanagedMemoryOwner : UnmanagedMemory, IUnmanagedMemory
diff --git a/src/DotNext.Unsafe/DotNext.Unsafe.csproj b/src/DotNext.Unsafe/DotNext.Unsafe.csproj
index 5d086a8ea8..05e006b259 100644
--- a/src/DotNext.Unsafe/DotNext.Unsafe.csproj
+++ b/src/DotNext.Unsafe/DotNext.Unsafe.csproj
@@ -6,8 +6,8 @@
latest
enable
true
- true
- 5.16.1
+ true
+ 5.17.0
nullablePublicOnly
.NET Foundation and Contributors
diff --git a/src/DotNext.sln b/src/DotNext.sln
index dce0ee1c5a..f8f215fad7 100644
--- a/src/DotNext.sln
+++ b/src/DotNext.sln
@@ -42,6 +42,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLineAMI", "examples\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RandomAccessCacheBenchmark", "examples\RandomAccessCacheBenchmark\RandomAccessCacheBenchmark.csproj", "{73185946-8EFD-4153-8AC4-05AFD8BAC2E4}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNext.AotTests", "DotNext.AotTests\DotNext.AotTests.csproj", "{155A8FEF-5FDD-42D3-AC31-335542FC588C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Bench|Any CPU = Bench|Any CPU
@@ -133,6 +135,12 @@ Global
{73185946-8EFD-4153-8AC4-05AFD8BAC2E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{73185946-8EFD-4153-8AC4-05AFD8BAC2E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{73185946-8EFD-4153-8AC4-05AFD8BAC2E4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {155A8FEF-5FDD-42D3-AC31-335542FC588C}.Bench|Any CPU.ActiveCfg = Debug|Any CPU
+ {155A8FEF-5FDD-42D3-AC31-335542FC588C}.Bench|Any CPU.Build.0 = Debug|Any CPU
+ {155A8FEF-5FDD-42D3-AC31-335542FC588C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {155A8FEF-5FDD-42D3-AC31-335542FC588C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {155A8FEF-5FDD-42D3-AC31-335542FC588C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {155A8FEF-5FDD-42D3-AC31-335542FC588C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -146,6 +154,7 @@ Global
{1A2FAA85-95B5-4377-B430-622DC5000C89} = {F8DCA620-40E7-411E-8970-85DC808B6BAF}
{39583DDE-E579-44AD-B7AF-5BB77D979E55} = {F8DCA620-40E7-411E-8970-85DC808B6BAF}
{73185946-8EFD-4153-8AC4-05AFD8BAC2E4} = {F8DCA620-40E7-411E-8970-85DC808B6BAF}
+ {155A8FEF-5FDD-42D3-AC31-335542FC588C} = {4956E982-79AA-462C-B592-E904D24EFFAE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1CE24157-AFE0-4CDF-B063-2876343F0A66}
diff --git a/src/DotNext/Buffers/BufferWriterSlim.cs b/src/DotNext/Buffers/BufferWriterSlim.cs
index 30c8950f24..8017ed4d97 100644
--- a/src/DotNext/Buffers/BufferWriterSlim.cs
+++ b/src/DotNext/Buffers/BufferWriterSlim.cs
@@ -353,6 +353,58 @@ public void Clear(bool reuseBuffer = false)
position = 0;
}
+ ///
+ /// Writes a collection of elements.
+ ///
+ /// A collection of elements.
+ /// is .
+ public void AddAll(IEnumerable collection)
+ {
+ ArgumentNullException.ThrowIfNull(collection);
+
+ ReadOnlySpan input;
+ switch (collection)
+ {
+ case List list:
+ input = CollectionsMarshal.AsSpan(list);
+ break;
+ case T[] array:
+ input = array;
+ break;
+ case string str:
+ input = Unsafe.BitCast, ReadOnlyMemory>(str.AsMemory()).Span;
+ break;
+ case ArraySegment segment:
+ input = segment;
+ break;
+ default:
+ WriteSlow(collection);
+ return;
+ }
+
+ Write(input);
+ }
+
+ private void WriteSlow(IEnumerable collection)
+ {
+ using var enumerator = collection.GetEnumerator();
+ if (collection.TryGetNonEnumeratedCount(out var count))
+ {
+ var buffer = InternalGetSpan(count);
+ for (count = 0; count < buffer.Length && enumerator.MoveNext(); count++)
+ {
+ buffer[count] = enumerator.Current;
+ }
+
+ position += count;
+ }
+
+ while (enumerator.MoveNext())
+ {
+ Add(enumerator.Current);
+ }
+ }
+
///
/// Releases internal buffer used by this builder.
///
diff --git a/src/DotNext/Buffers/ByteBuffer.cs b/src/DotNext/Buffers/ByteBuffer.cs
index 672755fcfc..256fcb9a6d 100644
--- a/src/DotNext/Buffers/ByteBuffer.cs
+++ b/src/DotNext/Buffers/ByteBuffer.cs
@@ -1,6 +1,7 @@
using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
+using System.Text.Unicode;
namespace DotNext.Buffers;
@@ -324,6 +325,21 @@ public static bool TryFormat(this ref SpanWriter writer, T value, ReadO
return result;
}
+ ///
+ /// Writes the specified sequence of characters as UTF-8 encoded octets.
+ ///
+ /// The buffer writer.
+ /// The input characters to be encoded.
+ /// if is encoded successfully; otherwise, .
+ public static bool TryEncodeAsUtf8(this ref SpanWriter writer, ReadOnlySpan input)
+ {
+ bool result;
+ if (result = Utf8.FromUtf16(input, writer.RemainingSpan, out _, out var bytesWritten, replaceInvalidSequences: false) is OperationStatus.Done)
+ writer.Advance(bytesWritten);
+
+ return result;
+ }
+
///
/// Writes 32-bit integer in a compressed format.
///
diff --git a/src/DotNext/Buffers/Chunk.cs b/src/DotNext/Buffers/Chunk.cs
index dfc9af07ab..43387156fe 100644
--- a/src/DotNext/Buffers/Chunk.cs
+++ b/src/DotNext/Buffers/Chunk.cs
@@ -26,6 +26,10 @@ internal static void AddChunk(in ReadOnlyMemory segment, [AllowNull] ref Chun
: last.Next(segment);
}
- internal static ReadOnlySequence CreateSequence(Chunk head, Chunk tail)
- => new(head, 0, tail, tail.Memory.Length);
+ internal static ReadOnlySequence CreateSequence(Chunk? head, Chunk? tail)
+ => head is null || tail is null
+ ? new()
+ : ReferenceEquals(head, tail)
+ ? new(head.Memory)
+ : new(head, 0, tail, tail.Memory.Length);
}
\ No newline at end of file
diff --git a/src/DotNext/Buffers/Memory.ReadOnlySequence.cs b/src/DotNext/Buffers/Memory.ReadOnlySequence.cs
index 1d6ba732cd..068bd79c0d 100644
--- a/src/DotNext/Buffers/Memory.ReadOnlySequence.cs
+++ b/src/DotNext/Buffers/Memory.ReadOnlySequence.cs
@@ -1,4 +1,5 @@
using System.Buffers;
+using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
@@ -13,17 +14,19 @@ public static partial class Memory
/// The sequence of memory blocks.
/// The type of elements in the memory blocks.
/// The constructed instance containing memory blocks.
- public static ReadOnlySequence ToReadOnlySequence(this IEnumerable> chunks)
+ public static ReadOnlySequence ToReadOnlySequence(this IEnumerable>? chunks)
{
Chunk? head = null, tail = null;
switch (chunks)
{
+ case null:
+ break;
case ReadOnlyMemory[] array:
- FromSpan(array.AsSpan(), ref head, ref tail);
+ CreateChunks(array.AsSpan(), ref head, ref tail);
break;
case List> list:
- FromSpan(CollectionsMarshal.AsSpan(list), ref head, ref tail);
+ CreateChunks(CollectionsMarshal.AsSpan(list), ref head, ref tail);
break;
case LinkedList> list:
FromLinkedList(list, ref head, ref tail);
@@ -33,12 +36,6 @@ public static ReadOnlySequence ToReadOnlySequence(this IEnumerable.Empty;
-
- if (ReferenceEquals(head, tail))
- return new(head.Memory);
-
return Chunk.CreateSequence(head, tail);
static void ToReadOnlySequenceSlow(IEnumerable> chunks, ref Chunk? head, ref Chunk? tail)
@@ -50,15 +47,6 @@ static void ToReadOnlySequenceSlow(IEnumerable> chunks, ref Ch
}
}
- static void FromSpan(ReadOnlySpan> chunks, ref Chunk? head, ref Chunk? tail)
- {
- foreach (ref readonly var segment in chunks)
- {
- if (!segment.IsEmpty)
- Chunk.AddChunk(segment, ref head, ref tail);
- }
- }
-
static void FromLinkedList(LinkedList> chunks, ref Chunk? head, ref Chunk? tail)
{
for (var current = chunks.First; current is not null; current = current.Next)
@@ -70,36 +58,63 @@ static void FromLinkedList(LinkedList> chunks, ref Chunk? h
}
}
+ ///
+ /// Converts the sequence of memory blocks to data type.
+ ///
+ /// The sequence of memory blocks.
+ /// The type of elements in the memory blocks.
+ /// The constructed instance containing memory blocks.
+ public static ReadOnlySequence ToReadOnlySequence(ReadOnlySpan> chunks) // TODO: use params
+ {
+ switch (chunks)
+ {
+ case []:
+ return ReadOnlySequence.Empty;
+ case [var chunk]:
+ return new(chunk);
+ default:
+ Chunk? head = null, tail = null;
+ CreateChunks(chunks, ref head, ref tail);
+ return Chunk.CreateSequence(head, tail);
+ }
+ }
+
+ private static void CreateChunks(ReadOnlySpan> chunks, ref Chunk? head, ref Chunk? tail)
+ {
+ foreach (ref readonly var segment in chunks)
+ {
+ if (!segment.IsEmpty)
+ Chunk.AddChunk(segment, ref head, ref tail);
+ }
+ }
+
///
/// Constructs a sequence of characters from a collection of strings.
///
/// A collection of strings.
/// A sequence of characters representing concatenated strings.
- public static ReadOnlySequence ToReadOnlySequence(this IEnumerable strings)
+ public static ReadOnlySequence ToReadOnlySequence(this IEnumerable? strings)
{
Chunk? head = null, tail = null;
switch (strings)
{
+ case null:
+ break;
case List list:
- ToReadOnlySequence(CollectionsMarshal.AsSpan(list), ref head, ref tail);
+ CreateChunks(CollectionsMarshal.AsSpan(list), ref head, ref tail);
break;
case string?[] array:
- ToReadOnlySequence(array.AsSpan(), ref head, ref tail);
+ CreateChunks(array.AsSpan(), ref head, ref tail);
break;
default:
ToReadOnlySequenceSlow(strings, ref head, ref tail);
break;
}
- if (head is null || tail is null)
- return ReadOnlySequence.Empty;
-
- return ReferenceEquals(head, tail)
- ? new(head.Memory)
- : Chunk.CreateSequence(head, tail);
+ return Chunk.CreateSequence(head, tail);
- static void ToReadOnlySequence(ReadOnlySpan strings, ref Chunk? head, ref Chunk? tail)
+ static void ToReadOnlySequenceSlow(IEnumerable strings, ref Chunk? head, ref Chunk? tail)
{
foreach (var str in strings)
{
@@ -107,14 +122,34 @@ static void ToReadOnlySequence(ReadOnlySpan strings, ref Chunk? h
Chunk.AddChunk(str.AsMemory(), ref head, ref tail);
}
}
+ }
- static void ToReadOnlySequenceSlow(IEnumerable strings, ref Chunk? head, ref Chunk? tail)
+ ///
+ /// Constructs a sequence of characters from a collection of strings.
+ ///
+ /// A collection of strings.
+ /// A sequence of characters representing concatenated strings.
+ public static ReadOnlySequence ToReadOnlySequence(ReadOnlySpan strings) // TODO: Use params
+ {
+ switch (strings)
{
- foreach (var str in strings)
- {
- if (str is { Length: > 0 })
- Chunk.AddChunk(str.AsMemory(), ref head, ref tail);
- }
+ case []:
+ return ReadOnlySequence.Empty;
+ case [var str]:
+ return new(str.AsMemory());
+ default:
+ Chunk? head = null, tail = null;
+ CreateChunks(strings, ref head, ref tail);
+ return Chunk.CreateSequence(head, tail);
+ }
+ }
+
+ private static void CreateChunks(ReadOnlySpan strings, ref Chunk? head, ref Chunk? tail)
+ {
+ foreach (var str in strings)
+ {
+ if (str is { Length: > 0 })
+ Chunk.AddChunk(str.AsMemory(), ref head, ref tail);
}
}
diff --git a/src/DotNext/Buffers/SparseBufferWriter.Reader.cs b/src/DotNext/Buffers/SparseBufferWriter.Reader.cs
index c32e89f737..1108f9b349 100644
--- a/src/DotNext/Buffers/SparseBufferWriter.Reader.cs
+++ b/src/DotNext/Buffers/SparseBufferWriter.Reader.cs
@@ -145,12 +145,6 @@ public ReadOnlySequence Read(ref SequencePosition position, long count)
if (count > 0L)
throw new InvalidOperationException(ExceptionMessages.EndOfBuffer(count));
- if (head is null || tail is null)
- return ReadOnlySequence.Empty;
-
- if (ReferenceEquals(head, tail))
- return new(head.Memory);
-
return Chunk.CreateSequence(head, tail);
}
diff --git a/src/DotNext/Disposable.cs b/src/DotNext/Disposable.cs
index 2e0176a0f6..f85c15c8c8 100644
--- a/src/DotNext/Disposable.cs
+++ b/src/DotNext/Disposable.cs
@@ -156,7 +156,8 @@ public static async ValueTask DisposeAsync(IEnumerable object
/// Disposes many objects in safe manner.
///
/// An array of objects to dispose.
- public static void Dispose(ReadOnlySpan objects)
+ public static void Dispose(ReadOnlySpan objects)
+ where T : IDisposable?
{
foreach (var obj in objects)
obj?.Dispose();
diff --git a/src/DotNext/DotNext.csproj b/src/DotNext/DotNext.csproj
index 43af4e637e..1bd85e3d14 100644
--- a/src/DotNext/DotNext.csproj
+++ b/src/DotNext/DotNext.csproj
@@ -5,13 +5,13 @@
latest
enable
true
- true
+ true
nullablePublicOnly
DotNext
.NET Foundation and Contributors
.NEXT Family of Libraries
- 5.16.1
+ 5.17.0
DotNext
MIT
diff --git a/src/DotNext/Dynamic/TaskResultBinder.cs b/src/DotNext/Dynamic/TaskResultBinder.cs
index 23b08fc006..4cfdace94f 100644
--- a/src/DotNext/Dynamic/TaskResultBinder.cs
+++ b/src/DotNext/Dynamic/TaskResultBinder.cs
@@ -7,8 +7,8 @@
namespace DotNext.Dynamic;
-[RequiresUnreferencedCode("Runtime binding may be incompatible with IL trimming")]
[RequiresDynamicCode("DLR is required to resolve underlying task type at runtime")]
+[RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")]
internal sealed class TaskResultBinder : CallSiteBinder
{
private static Expression BindProperty(PropertyInfo resultProperty, Expression target, out Expression restrictions)
diff --git a/src/DotNext/EqualityComparerBuilder.cs b/src/DotNext/EqualityComparerBuilder.cs
index c4d9693cd8..7173c830aa 100644
--- a/src/DotNext/EqualityComparerBuilder.cs
+++ b/src/DotNext/EqualityComparerBuilder.cs
@@ -64,6 +64,7 @@ internal ConstructedEqualityComparer(Func equality, Func h
}
[RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")]
+ [RequiresDynamicCode("Dynamic code generation may be incompatible with AOT")]
private static PropertyInfo? GetDefaultEqualityComparer(Type target)
=> typeof(EqualityComparer<>)
.MakeGenericType(target)
@@ -71,6 +72,7 @@ internal ConstructedEqualityComparer(Func equality, Func h
private static FieldInfo? HashSaltField => typeof(RandomExtensions).GetField(nameof(RandomExtensions.BitwiseHashSalt), BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly);
+ [RequiresDynamicCode("Runtime binding requires dynamic code compilation")]
[RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")]
private static MethodCallExpression EqualsMethodForValueType(MemberExpression first, MemberExpression second)
{
@@ -97,6 +99,7 @@ private static MethodCallExpression EqualsMethodForValueType(MemberExpression fi
return Expression.Call(Expression.Property(null, defaultProperty), method, first, second);
}
+ [RequiresDynamicCode("Runtime binding requires dynamic code compilation")]
[RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")]
private static Expression HashCodeMethodForValueType(Expression expr, ConstantExpression salted)
{
@@ -130,6 +133,7 @@ private static Expression HashCodeMethodForValueType(Expression expr, ConstantEx
return expr;
}
+ [RequiresDynamicCode("Runtime binding requires dynamic code compilation")]
[RequiresUnreferencedCode("Dynamic code generation may be incompatible with IL trimming")]
private static MethodInfo EqualsMethodForArrayElementType(Type itemType)
{
@@ -151,6 +155,7 @@ private static MethodInfo EqualsMethodForArrayElementType(Type itemType)
return new Func?, IEnumerable