From 3253ea3dafff4769a71efcf10c1650a419baddbd Mon Sep 17 00:00:00 2001 From: aianlinb Date: Thu, 25 Jul 2024 18:27:50 +0800 Subject: [PATCH] Add IBufferWriter.AsStream(). Add MemoryStream.AsIBufferWriter() Add byte[].AsStream(). Add List.AsMemory() and .AsSpan(). Add PickFirstEnumerator. Move SystemExtensions.System.FIle to SystemExtensions.System.IO.File. Update README.md --- README.md | 10 ++- System.Private.CoreLib/MemoryStream.cs | 22 ++++++ SystemExtensions.Test/ArrayExtensionsTests.cs | 17 +++++ .../CollectionExtensionsTests.cs | 45 ++++++++++- SystemExtensions.Test/FileTests.cs | 2 +- .../StreamExtensionsTests.cs | 75 ++++++++++++++++++ .../Collections/ArrayExtensions.cs | 24 ++++++ .../Collections/CollectionExtensions.cs | 31 +++++++- .../Collections/PickFirstEnumerator.cs | 76 +++++++++++++++++++ SystemExtensions/LongIndex.cs | 12 ++- SystemExtensions/LongRange.cs | 8 ++ SystemExtensions/Spans/SpanExtensions.cs | 3 +- .../Streams/BufferWriterStream.cs | 63 +++++++++++++++ .../Streams/BufferWriterWrapper.cs | 47 ++++++++++++ SystemExtensions/Streams/ReadOnlySubStream.cs | 2 +- SystemExtensions/Streams/StreamExtensions.cs | 15 +++- SystemExtensions/Streams/SubStream.cs | 2 +- .../Streams/UnseekableSubStream.cs | 2 +- SystemExtensions/System/{ => IO}/File.cs | 2 +- SystemExtensions/SystemExtensions.csproj | 4 +- SystemExtensions/Utils.cs | 44 +---------- 21 files changed, 446 insertions(+), 60 deletions(-) create mode 100644 System.Private.CoreLib/MemoryStream.cs create mode 100644 SystemExtensions/Collections/PickFirstEnumerator.cs create mode 100644 SystemExtensions/Streams/BufferWriterStream.cs create mode 100644 SystemExtensions/Streams/BufferWriterWrapper.cs rename SystemExtensions/System/{ => IO}/File.cs (99%) diff --git a/README.md b/README.md index 3dfebbf..b0104b9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,10 @@ [![NuGet](https://img.shields.io/nuget/v/aianlinb.SystemExtensions)](https://www.nuget.org/packages/aianlinb.SystemExtensions) -[![Test](https://github.com/aianlinb/SystemExtensions/actions/workflows/test.yml/badge.svg)](https://github.com/aianlinb/SystemExtensions/actions/workflows/test.yml) \ No newline at end of file +[![Test](https://github.com/aianlinb/SystemExtensions/actions/workflows/test.yml/badge.svg)](https://github.com/aianlinb/SystemExtensions/actions/workflows/test.yml) + +Useful and high-performance helper classes and extension methods. + +## Highlights +- SpanExtensions +- StreamExtensions +- ValueList +- SubStream \ No newline at end of file diff --git a/System.Private.CoreLib/MemoryStream.cs b/System.Private.CoreLib/MemoryStream.cs new file mode 100644 index 0000000..49cb968 --- /dev/null +++ b/System.Private.CoreLib/MemoryStream.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; + +namespace System.IO; +/// +/// Forwarded to System.Private.CoreLib.dll at runtime +/// +public class MemoryStream { + public byte[] _buffer; + public readonly int _origin; + public int _position; + public int _length; + public int _capacity; + public bool _expandable; + public bool _writable; + public bool _exposable; + public bool _isOpen; + public Task? _lastReadTask; + public const int MemStreamMaxLength = int.MaxValue; + + public extern void EnsureNotClosed(); + public extern bool EnsureCapacity(int value); +} \ No newline at end of file diff --git a/SystemExtensions.Test/ArrayExtensionsTests.cs b/SystemExtensions.Test/ArrayExtensionsTests.cs index 2f3a874..321f071 100644 --- a/SystemExtensions.Test/ArrayExtensionsTests.cs +++ b/SystemExtensions.Test/ArrayExtensionsTests.cs @@ -75,4 +75,21 @@ public void AsList_Test(int count) { Assert.AreEqual(0, result.Count); Assert.AreEqual(array.Length, result.Capacity); } + + [TestMethod] + public void ByteArrayAsStream_Test() { + // Arrange + byte[] buffer = [1, 2, 3]; + + // Act + using var ms = buffer.AsStream(0, -1, false, true, true); + + // Assert + Assert.AreEqual(buffer.Length, ms.Length); + Assert.AreEqual(buffer.Length, ms.Capacity); + Assert.IsFalse(ms.CanWrite); + Assert.AreSame(buffer, ms.GetBuffer()); + ms.Capacity = 5; + Assert.AreNotSame(buffer, ms.GetBuffer()); + } } \ No newline at end of file diff --git a/SystemExtensions.Test/CollectionExtensionsTests.cs b/SystemExtensions.Test/CollectionExtensionsTests.cs index 2bc5acb..4c77a38 100644 --- a/SystemExtensions.Test/CollectionExtensionsTests.cs +++ b/SystemExtensions.Test/CollectionExtensionsTests.cs @@ -1,13 +1,50 @@ -namespace SystemExtensions.Tests; +using System.Runtime.InteropServices; + +namespace SystemExtensions.Tests; + [TestClass] public class CollectionExtensionsTests { + private static readonly int[] enumerable = [1, 2, 3, 4, 5]; [TestMethod] public void IndexOf_Test() { - // Arrange - var enumerable = new[] { 1, 2, 3, 4, 5 }; - // Act + Assert for (var i = 0; i < enumerable.Length; ++i) Assert.AreEqual(i, Collections.CollectionExtensions.IndexOf(enumerable, enumerable[i])); } + + [TestMethod] + public void PickFirstEnumerator_Test() { + // Arrange + using var pfe = new PickFirstEnumerator(enumerable); + + // Act + Assert + Assert.AreEqual(enumerable[0], pfe.First); + using var et = enumerable.AsEnumerable().GetEnumerator(); + while (pfe.MoveNext()) { + Assert.IsTrue(et.MoveNext()); + Assert.AreEqual(et.Current, pfe.Current); + } + pfe.Reset(); + Assert.AreEqual(enumerable[0], pfe.First); + Assert.IsTrue(enumerable.SequenceEqual(pfe)); + } + + [TestMethod] + public unsafe void ListAsSpanAsMemory_Test() { + // Arrange + var list = new List(enumerable); + + // Act + var span = list.AsSpan(); + var memory = list.AsMemory(); + + // Assert + Assert.AreEqual(list.Count, span.Length); + Assert.AreEqual(list.Count, memory.Length); + fixed (int* expected = CollectionsMarshal.AsSpan(list), actual1 = span) { + var actual2 = (int*)memory.Pin().Pointer; + Assert.IsTrue(expected == actual1); + Assert.IsTrue(expected == actual2); + } + } } \ No newline at end of file diff --git a/SystemExtensions.Test/FileTests.cs b/SystemExtensions.Test/FileTests.cs index 0360843..5184c38 100644 --- a/SystemExtensions.Test/FileTests.cs +++ b/SystemExtensions.Test/FileTests.cs @@ -1,5 +1,5 @@ namespace SystemExtensions.Tests; -using File = SystemExtensions.System.File; +using File = SystemExtensions.System.IO.File; using System = global::System; [TestClass] diff --git a/SystemExtensions.Test/StreamExtensionsTests.cs b/SystemExtensions.Test/StreamExtensionsTests.cs index b4d26a7..cb41f11 100644 --- a/SystemExtensions.Test/StreamExtensionsTests.cs +++ b/SystemExtensions.Test/StreamExtensionsTests.cs @@ -1,3 +1,8 @@ +using System.Buffers; +using System.Data; + +using SystemExtensions.Streams; + namespace SystemExtensions.Tests; [TestClass] public class StreamExtensionsTests { @@ -131,4 +136,74 @@ public void ReadToEndTest() { new ReadOnlySpan(expected).Slice(pos).SequenceEqual(result1); new ReadOnlySpan(expected).Slice(pos).SequenceEqual(result2); } + + [TestMethod] + [DataRow(false)] + [DataRow(true)] + public void BufferWriterWrapper_Test(bool getMemory) { + // Arrange + using var ms = new MemoryStream(4); + var bww = ms.AsIBufferWriter(); + ReadOnlySpan data = [1, 2, 3]; + + // Act + Assert + data.CopyTo(getMemory ? bww.GetMemory().Span : bww.GetSpan()); + bww.Advance(data.Length); + Assert.AreEqual(4, ms.Capacity); // not changed + Assert.AreEqual(data.Length, ms.Length); + + data.CopyTo(getMemory ? bww.GetMemory(5).Span : bww.GetSpan(5)); + Assert.IsTrue(data.Length + 5 <= ms.Capacity); // expanded + bww.Advance(data.Length); + Assert.AreEqual(data.Length + data.Length, ms.Length); + + var result = ms.ToArray().AsReadOnlySpan(); + Assert.IsTrue(data.SequenceEqual(result[..data.Length])); + Assert.IsTrue(data.SequenceEqual(result.Slice(data.Length))); + + ms.SetLength(2); + ms.Capacity = 2; + Assert.AreNotEqual(0, (getMemory ? bww.GetMemory().Span : bww.GetSpan()).Length); + Assert.IsTrue(3 <= ms.Capacity); // expanded + } + + [TestMethod] + public async Task BufferWriterStream_Test() { + // Arrange + var abw = new ArrayBufferWriter(); + var bws = abw.AsStream(); + byte[] data = [1, 2, 3]; + + // Act + Assert + Assert.IsTrue(bws.CanWrite); + Assert.IsFalse(bws.CanRead); + Assert.IsFalse(bws.CanSeek); + Assert.ThrowsException(() => bws.Position = 1); + Assert.ThrowsException(() => bws.Read([], default, default)); + Assert.ThrowsException(() => bws.Read(default)); + await Assert.ThrowsExceptionAsync(() => bws.ReadAsync(default).AsTask()); + await Assert.ThrowsExceptionAsync(() => bws.ReadAsync([], default, default, default)); + Assert.ThrowsException(() => bws.BeginRead([], default, default, default, default)); + Assert.ThrowsException(() => bws.Seek(default, default)); + Assert.ThrowsException(() => bws.SetLength(default)); + + bws.Write(data, 0, 2); + Assert.IsTrue(abw.WrittenSpan.SequenceEqual(new(data, 0, 2))); + abw.ResetWrittenCount(); +#pragma warning disable CA1835 + await bws.WriteAsync(data, 0, 2); +#pragma warning restore CA1835 + Assert.IsTrue(abw.WrittenSpan.SequenceEqual(new(data, 0, 2))); + abw.ResetWrittenCount(); + + bws.Write(data); + Assert.IsTrue(abw.WrittenSpan.SequenceEqual(data)); + abw.ResetWrittenCount(); + await bws.WriteAsync(data); + Assert.IsTrue(abw.WrittenSpan.SequenceEqual(data)); + + bws.WriteByte(7); + Assert.AreEqual(data.Length + 1, abw.WrittenCount); + Assert.AreEqual(7, abw.WrittenSpan[data.Length]); + } } \ No newline at end of file diff --git a/SystemExtensions/Collections/ArrayExtensions.cs b/SystemExtensions/Collections/ArrayExtensions.cs index 6efdf94..61e4903 100644 --- a/SystemExtensions/Collections/ArrayExtensions.cs +++ b/SystemExtensions/Collections/ArrayExtensions.cs @@ -56,4 +56,28 @@ public static List AsList(this T[] array, int count) { result._size = count; return Unsafe.As>(result); } + + /// + /// Creates a that use as its underlying array. + /// And with an additional parameter than the original constructor: . + /// + /// The underlying buffer for creating the . + /// + /// The index of the at which the stream begins. + /// Must be 0 if is due to the internal implementation. + /// + /// The initial length of the stream in bytes + /// The setting of the property, which determines whether the stream supports writing. + /// Whether allows the underlying array of the stream to be returned by or . + /// Whether the stream can be expanded. That is, whether the of this stream can be changed. + /// The created + public static MemoryStream AsStream(this byte[] buffer, int index = 0, int count = -1, bool writable = true, bool publiclyVisible = false, bool expandable = false) { + if (count == -1) + count = unchecked(buffer.Length - index); + if (expandable && index != 0) + ThrowHelper.Throw("The expandable is true while index != 0", nameof(expandable)); + var ms = new MemoryStream(buffer, index, count, writable, publiclyVisible); + Unsafe.As(ms)._expandable = expandable; + return ms; + } } \ No newline at end of file diff --git a/SystemExtensions/Collections/CollectionExtensions.cs b/SystemExtensions/Collections/CollectionExtensions.cs index 4f9a8a0..b2449dd 100644 --- a/SystemExtensions/Collections/CollectionExtensions.cs +++ b/SystemExtensions/Collections/CollectionExtensions.cs @@ -1,8 +1,12 @@ -using System.Collections; +extern alias corelib; + +using System.Collections; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace SystemExtensions.Collections; + public static class CollectionExtensions { public static int IndexOf(this IEnumerable source, T value) { switch (source) { @@ -41,12 +45,33 @@ public static int IndexOf(this IEnumerable source, Predicate match, out return -1; } + /// + /// Get a view over a 's data. + /// + /// + /// While the returned is in use, items should not be added/removed to/from the , + /// and the should not be changed. + /// Same as + /// + public static Span AsSpan(this List list) => CollectionsMarshal.AsSpan(list); + /// + /// Get a view over a 's data. + /// + /// + /// While the returned is in use, items should not be added/removed to/from the , + /// and the should not be changed. + /// + public static Memory AsMemory(this List list) { + var l = Unsafe.As>(list); + return new(l._items, 0, l._size); + } + /// /// Returns a wrapper with method that returns as is. /// /// - /// Note that the returned cannot be enumerate repeatedly. - /// And it will start from current state of the passed. + /// Note that the returned cannot be enumerated repeatedly (unless calling of ). + /// And it will start from current state of the . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static EnumeratorWrapper AsEnumerable(this IEnumerator source) => new(source); diff --git a/SystemExtensions/Collections/PickFirstEnumerator.cs b/SystemExtensions/Collections/PickFirstEnumerator.cs new file mode 100644 index 0000000..1740fc0 --- /dev/null +++ b/SystemExtensions/Collections/PickFirstEnumerator.cs @@ -0,0 +1,76 @@ +using System.Collections; + +namespace SystemExtensions.Collections; + +/// +/// Wrap a to pick its first value and save to , +/// but still can iterate all values including the first one without calling . +/// +/// +/// +/// This is useful when you want to get the first value before a foreach loop, +/// but don't want to re-enumerate the collection. +/// +/// +/// Note that the returns the itself, +/// so this can't be used again before calling . +/// +/// +public struct PickFirstEnumerator : IEnumerator, IEnumerable { + public PickFirstEnumerator(IEnumerable enumerable) : this(enumerable.GetEnumerator()) { } + public PickFirstEnumerator(IEnumerator enumerator) { + if (enumerator.MoveNext()) + First = enumerator.Current; + else + State = 1; + BaseEnumerator = enumerator; + } + + /// + /// The base that was passed to the constructor. + /// + public readonly IEnumerator BaseEnumerator { get; } + /// + /// The first value of , or if it's empty. + /// + public readonly T? First { get; } + private byte State; // 0: Initial, 1: Initial (Empty), 2: First moved, 3: Other + + public readonly T Current => State switch { + 0 or 1 => default!, + 2 => First!, + _ => BaseEnumerator.Current + }; + readonly object IEnumerator.Current => Current!; + + public bool MoveNext() { + switch (State) { + case 0: + State = 2; + return true; + case 1: + State = 3; + return false; + case 2: + State = 3; + break; + } + return BaseEnumerator.MoveNext(); + } + + public void Reset() { + BaseEnumerator.Reset(); + this = new(BaseEnumerator); + } + + public readonly void Dispose() => BaseEnumerator.Dispose(); + + /// + /// Returns as is. + /// + /// + /// See the remarks of . + /// + public readonly IEnumerator GetEnumerator() => this; + readonly IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} \ No newline at end of file diff --git a/SystemExtensions/LongIndex.cs b/SystemExtensions/LongIndex.cs index 11a3a8a..9697591 100644 --- a/SystemExtensions/LongIndex.cs +++ b/SystemExtensions/LongIndex.cs @@ -23,6 +23,7 @@ public LongIndex(long value, bool fromEnd = false) { else _value = value; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private LongIndex(long value) { _value = value; } @@ -66,7 +67,7 @@ public static LongIndex FromEnd(long value) { [MethodImpl(MethodImplOptions.AggressiveInlining)] public long GetOffset(long length) { var offset = _value; - if (IsFromEnd) unchecked { + if (offset < 0L /*IsFromEnd*/) unchecked { offset += length + 1L; // offset = length - (~value) // offset = length + (~(~value) + 1) @@ -75,8 +76,11 @@ public long GetOffset(long length) { return offset; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override bool Equals([NotNullWhen(true)] object? obj) => obj is LongIndex li && _value == li._value; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(LongIndex other) => _value == other._value; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(Index other) => _value == Unsafe.As(ref other); /// Returns the hash code for this instance. @@ -90,22 +94,28 @@ public override string ToString() { } /// Converts integer number to a . + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator LongIndex(long value) => FromStart(value); /// Converts integer number to a . + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator LongIndex(int value) => (long)value; /// Converts to a . + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator LongIndex(Index value) => new(Unsafe.As(ref value)); /// /// Converts to an if the value is within the range of . /// /// The value is outside the range of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] #pragma warning disable CS9193 public static explicit operator Index(LongIndex value) => Unsafe.As(ref Unsafe.AsRef(checked((int)value._value))); #pragma warning restore CS9193 + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator ==(LongIndex left, LongIndex right) { return left.Equals(right); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator !=(LongIndex left, LongIndex right) { return !(left == right); } diff --git a/SystemExtensions/LongRange.cs b/SystemExtensions/LongRange.cs index f665d9a..c3d3e00 100644 --- a/SystemExtensions/LongRange.cs +++ b/SystemExtensions/LongRange.cs @@ -10,15 +10,19 @@ namespace SystemExtensions; /// /// Represent the inclusive start of the . /// Represent the exclusive end of the . +[method: MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly struct LongRange(LongIndex start, LongIndex end) : IEquatable, IEquatable { /// Represent the inclusive start of the . public LongIndex Start { get; } = start; /// Represent the exclusive end of the . public LongIndex End { get; } = end; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override bool Equals([NotNullWhen(true)] object? obj) => obj is LongRange r && Start.Equals(r.Start) && End.Equals(r.End); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(LongRange other) => Start.Equals(other.Start) && End.Equals(other.End); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(Range other) => Start.Equals(other.Start) && End.Equals(other.End); /// Returns the hash code for this instance. @@ -62,16 +66,20 @@ public override bool Equals([NotNullWhen(true)] object? obj) => } /// Converts to a . + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator LongRange(Range value) => new(value.Start, value.End); /// /// Converts to a if the values of and are both within the range of . /// /// The value is outside the range of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static explicit operator Range(LongRange value) => new((Index)value.Start, (Index)value.End); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator ==(LongRange left, LongRange right) { return left.Equals(right); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator !=(LongRange left, LongRange right) { return !(left == right); } diff --git a/SystemExtensions/Spans/SpanExtensions.cs b/SystemExtensions/Spans/SpanExtensions.cs index 4bff8f2..3a9af25 100644 --- a/SystemExtensions/Spans/SpanExtensions.cs +++ b/SystemExtensions/Spans/SpanExtensions.cs @@ -6,7 +6,6 @@ using System.Runtime.InteropServices; namespace SystemExtensions.Spans; -using System = global::System; /// /// Some of the methods in this class have the same signature as the methods in without the limitation.
/// Use those in for T that implements instead, for better performance. @@ -15,7 +14,9 @@ public static class SpanExtensions { public static ReadOnlySpan AsReadOnly(this Span span) => span; public static ReadOnlyMemory AsReadOnly(this Memory memory) => memory; public static ReadOnlySpan AsReadOnlySpan(this T[] array) => array; + public static ReadOnlySpan AsReadOnlySpan(this T[] array, int start, int length) => new(array, start, length); public static ReadOnlyMemory AsReadOnlyMemory(this T[] array) => array; + public static ReadOnlyMemory AsReadOnlyMemory(this T[] array, int start, int length) => new(array, start, length); /// /// Returns a of that represents the memory of the by reference parameter . diff --git a/SystemExtensions/Streams/BufferWriterStream.cs b/SystemExtensions/Streams/BufferWriterStream.cs new file mode 100644 index 0000000..92c2cfd --- /dev/null +++ b/SystemExtensions/Streams/BufferWriterStream.cs @@ -0,0 +1,63 @@ +using System.Buffers; + +namespace SystemExtensions.Streams; + +/// +/// A write-only that wrap the . +/// +/// +/// of this class is no action. +/// +public class BufferWriterStream(IBufferWriter writer) : Stream, IBufferWriter { + public void Advance(int count) => writer.Advance(count); + public Memory GetMemory(int sizeHint = 0) => writer.GetMemory(sizeHint); + public Span GetSpan(int sizeHint = 0) => writer.GetSpan(sizeHint); + + public override void Flush() { } + public override Task FlushAsync(CancellationToken cancellationToken) { + if (cancellationToken.IsCancellationRequested) + return Task.FromCanceled(cancellationToken); + return Task.CompletedTask; + } + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => Write(new(buffer, offset, count)); + public override void Write(ReadOnlySpan buffer) { + buffer.CopyTo(writer.GetSpan(buffer.Length)); + writer.Advance(buffer.Length); + } + public override void WriteByte(byte value) { + writer.GetSpan(sizeof(byte))[0] = value; + writer.Advance(sizeof(byte)); + } + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { + if (cancellationToken.IsCancellationRequested) + return Task.FromCanceled(cancellationToken); + try { + Write(buffer, offset, count); + } catch (Exception ex) { + return Task.FromException(ex); + } + return Task.CompletedTask; + } + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { + if (cancellationToken.IsCancellationRequested) + return ValueTask.FromCanceled(cancellationToken); + try { + Write(buffer.Span); + } catch (Exception ex) { + return ValueTask.FromException(ex); + } + return ValueTask.CompletedTask; + } + + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => writer is ArrayBufferWriter abw ? abw.WrittenCount : throw new NotSupportedException(); + public override long Position { + get => Length; + set => throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/SystemExtensions/Streams/BufferWriterWrapper.cs b/SystemExtensions/Streams/BufferWriterWrapper.cs new file mode 100644 index 0000000..5e85247 --- /dev/null +++ b/SystemExtensions/Streams/BufferWriterWrapper.cs @@ -0,0 +1,47 @@ +extern alias corelib; + +using System.Buffers; +using System.Runtime.CompilerServices; + +namespace SystemExtensions.Streams; + +/// +/// Wrap a to implement of +/// which can be written starting from the current . +/// +/// +/// Do not seek/read/write the stream between calls to / and , +/// otherwise the results will not be as expected. +/// +public readonly struct BufferWriterWrapper(MemoryStream baseStream) : IBufferWriter { + private readonly corelib::System.IO.MemoryStream baseStream = Unsafe.As(baseStream); + /// + /// The underlying passed to the constructor. + /// + public MemoryStream BaseStream => Unsafe.As(baseStream); + + public readonly void Advance(int count) { + baseStream.EnsureNotClosed(); + ArgumentOutOfRangeException.ThrowIfNegative(count); + var pos = unchecked(baseStream._position + count); + if (pos < 0) + ThrowHelper.ThrowArgumentOutOfRange(count); + baseStream._position = pos; + if (pos > baseStream._length) + baseStream._length = pos; + } + + public readonly Memory GetMemory(int sizeHint = 0) { + baseStream.EnsureNotClosed(); + if (sizeHint >= 0) + baseStream.EnsureCapacity(unchecked(baseStream._position + (sizeHint == 0 ? 1 : sizeHint))); + return baseStream._buffer.AsMemory(baseStream._position); + } + + public readonly Span GetSpan(int sizeHint = 0) { + baseStream.EnsureNotClosed(); + if (sizeHint >= 0) + baseStream.EnsureCapacity(unchecked(baseStream._position + (sizeHint == 0 ? 1 : sizeHint))); + return baseStream._buffer.AsSpan(baseStream._position); + } +} \ No newline at end of file diff --git a/SystemExtensions/Streams/ReadOnlySubStream.cs b/SystemExtensions/Streams/ReadOnlySubStream.cs index dd3223f..c8722de 100644 --- a/SystemExtensions/Streams/ReadOnlySubStream.cs +++ b/SystemExtensions/Streams/ReadOnlySubStream.cs @@ -37,7 +37,7 @@ public ReadOnlySubStream(Stream baseStream, LongRange range) : base(baseStream, public override void WriteByte(byte value) => throw new NotSupportedException(ReadOnlyMessage); /// [DoesNotReturn] - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) => throw new NotSupportedException(ReadOnlyMessage); + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw new NotSupportedException(ReadOnlyMessage); /// [DoesNotReturn] public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => throw new NotSupportedException(ReadOnlyMessage); diff --git a/SystemExtensions/Streams/StreamExtensions.cs b/SystemExtensions/Streams/StreamExtensions.cs index 68dfbe5..6fccf16 100644 --- a/SystemExtensions/Streams/StreamExtensions.cs +++ b/SystemExtensions/Streams/StreamExtensions.cs @@ -185,10 +185,21 @@ public static void Write(this IBufferWriter writer, List list, int o /// See /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static SubStream Slice(this Stream stream, long offset, long length) => new(stream, offset, length); + public static SubStream Slice(this Stream stream, long offset, long length = -1) => new(stream, offset, length); /// /// See /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static SubStream Slice(this Stream stream, Range range) => new(stream, range); + public static SubStream Slice(this Stream stream, LongRange range) => new(stream, range); + /// + /// See + /// + /// of + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BufferWriterWrapper AsIBufferWriter(this MemoryStream stream) => new(stream); + /// + /// See + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BufferWriterStream AsStream(this IBufferWriter writer) => new(writer); } \ No newline at end of file diff --git a/SystemExtensions/Streams/SubStream.cs b/SystemExtensions/Streams/SubStream.cs index ec944a6..b074bb7 100644 --- a/SystemExtensions/Streams/SubStream.cs +++ b/SystemExtensions/Streams/SubStream.cs @@ -107,7 +107,7 @@ public virtual void Rescope(LongRange range) { public override int Read(byte[] buffer, int offset, int count) => _baseStream!.Read(buffer, offset, CheckLengthBeforeRead(count)); public override int Read(scoped Span buffer) => _baseStream!.Read(buffer[..CheckLengthBeforeRead(buffer.Length)]); public override int ReadByte() => (ulong)BaseStream.Position >= unchecked((ulong)EndOffset) ? -1 : _baseStream.ReadByte(); - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) => _baseStream!.ReadAsync(buffer, offset, CheckLengthBeforeRead(count), cancellationToken); + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _baseStream!.ReadAsync(buffer, offset, CheckLengthBeforeRead(count), cancellationToken); public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) => _baseStream!.ReadAsync(buffer[..CheckLengthBeforeRead(buffer.Length)], cancellationToken); public override long Position { diff --git a/SystemExtensions/Streams/UnseekableSubStream.cs b/SystemExtensions/Streams/UnseekableSubStream.cs index 93d934d..902c9b0 100644 --- a/SystemExtensions/Streams/UnseekableSubStream.cs +++ b/SystemExtensions/Streams/UnseekableSubStream.cs @@ -87,7 +87,7 @@ public override void WriteByte(byte value) { baseStream.WriteByte(value); ++position; } - public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) { + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { CheckLengthBeforeWrite(count); #pragma warning disable CA1835 await baseStream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); diff --git a/SystemExtensions/System/File.cs b/SystemExtensions/System/IO/File.cs similarity index 99% rename from SystemExtensions/System/File.cs rename to SystemExtensions/System/IO/File.cs index 21db774..0dff819 100644 --- a/SystemExtensions/System/File.cs +++ b/SystemExtensions/System/IO/File.cs @@ -4,7 +4,7 @@ using SystemExtensions.Spans; -namespace SystemExtensions.System; +namespace SystemExtensions.System.IO; using System = global::System; using RandomAccess = corelib::System.IO.RandomAccess; diff --git a/SystemExtensions/SystemExtensions.csproj b/SystemExtensions/SystemExtensions.csproj index cbcddc3..59ae652 100644 --- a/SystemExtensions/SystemExtensions.csproj +++ b/SystemExtensions/SystemExtensions.csproj @@ -10,10 +10,10 @@ - 1.7.1 + 1.7.2 aianlinb Copyright © 2024 aianlinb - Library with useful high-performance methods for Span/Stream/List etc... + Library with useful high-performance methods for Span/Stream/Enumerable etc... $(Authors).$(AssemblyName) https://github.com/$(Authors)/$(AssemblyName) $(AssemblyName);Span;Stream;Memory;Extension;Performance;IO;System diff --git a/SystemExtensions/Utils.cs b/SystemExtensions/Utils.cs index 596c411..80545bc 100644 --- a/SystemExtensions/Utils.cs +++ b/SystemExtensions/Utils.cs @@ -82,13 +82,7 @@ public static unsafe void ReverseBytes(ref T value) where T : unmanaged { /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static long GetOffset(this scoped in Index index, long length) { - Debug.Assert(length >= 0); - long value = Unsafe.As(ref Unsafe.AsRef(in index)); // local copy - if (value < 0) // index.IsFromEnd - unchecked { - value += length + 1; // length - ~value == value + length + 1 - } - return value; + return ((LongIndex)index).GetOffset(length); } /// @@ -96,23 +90,7 @@ public static long GetOffset(this scoped in Index index, long length) { /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static (long Offset, long Length) GetOffsetAndLength(this scoped in Range range, long length) { - Debug.Assert(length >= 0); - // Since Range is actually two Index (which is actually an int) - long start = Unsafe.As(ref Unsafe.AsRef(in range)); // local copy and cast to long - long end = Unsafe.Add(ref Unsafe.As(ref Unsafe.AsRef(in range)), 1); - - unchecked { - // See Index.GetOffset - // We don't cache length + 1 here because it's rare that both Start and End are IsFromEnd - if (start < 0) - start += length + 1; // length - ~start == start + length + 1 - if (end < 0) - end += length + 1; - - if ((ulong)end > (ulong)length || (ulong)start > (ulong)end) - ThrowHelper.ThrowArgumentOutOfRange(length); - } - return (start, end - start); + return ((LongRange)range).GetOffsetAndLength(length); } /// @@ -144,23 +122,7 @@ public static (int Offset, int End) GetOffsetAndEnd(this Range range, int length /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static (long Offset, long End) GetOffsetAndEnd(this scoped in Range range, long length) { - Debug.Assert(length >= 0); - // Since Range is actually two Index (which is actually an int) - long start = Unsafe.As(ref Unsafe.AsRef(in range)); // local copy and cast to long - long end = Unsafe.Add(ref Unsafe.As(ref Unsafe.AsRef(in range)), 1); - - unchecked { - // See Index.GetOffset - // We don't cache length + 1 here because it's rare that both Start and End are IsFromEnd - if (start < 0) - start += length + 1; // length - ~start == start + length + 1 - if (end < 0) - end += length + 1; - - if ((ulong)end > (ulong)length || (ulong)start > (ulong)end) - ThrowHelper.ThrowArgumentOutOfRange(length); - } - return (start, end); + return ((LongRange)range).GetOffsetAndEnd(length); } ///