From a7d3fbd9b06f63aa8e9785ace475a87682a56491 Mon Sep 17 00:00:00 2001 From: Rafael Teixeira Date: Tue, 21 Nov 2023 19:39:02 -0300 Subject: [PATCH] v14.0.0 - Refactor StreamSpan into a superclass that can work without specifying a length --- ...InterlockLedger.Commons.NUnit.Tests.csproj | 11 +- .../System.IO/Types/NonSeekMemoryStream.cs | 38 ++++ .../System.IO/Types/StreamSpanTests.cs | 39 ++-- .../Types/WrappedReadonlyStreamTests.cs | 94 +++++++++ .../ReadOnlyMemoryExtensions.cs | 4 +- .../ReadOnlySequenceExtensions.cs | 5 +- .../ListExtensions.cs | 2 + .../System.IO/FileInfoExtensions.cs | 13 +- .../Extensions/System.IO/StreamExtensions.cs | 23 +-- .../IEnumerableOfReadOnlyMemoryExtensions.cs | 2 +- .../System/ArrayOfByteExtensions.cs | 19 +- .../Extensions/System/ArrayOfTExtensions.cs | 2 +- .../Extensions/System/DateOnlyConverter.cs | 9 +- .../Extensions/System/ObjectExtensions.cs | 2 + .../Extensions/System/StringExtensions.cs | 2 +- .../InterlockLedger.Commons.csproj | 6 +- .../Statics/EnumerableHelpers.cs | 4 +- .../System.Buffers/ReadOnlySequenceStream.cs | 3 +- .../SingleEnumerable.cs | 12 +- .../System.IO/StreamIsReadonlyException.cs | 38 ++++ .../Types/System.IO/StreamSpan.cs | 172 +--------------- .../Types/System.IO/WrappedReadonlyStream.cs | 192 ++++++++++++++++++ .../Types/System.Threading.Tasks/AsyncLock.cs | 2 +- .../Types/System/Result.cs | 4 +- 24 files changed, 442 insertions(+), 256 deletions(-) create mode 100644 InterlockLedger.Commons.NUnit.Tests/System.IO/Types/NonSeekMemoryStream.cs create mode 100644 InterlockLedger.Commons.NUnit.Tests/System.IO/Types/WrappedReadonlyStreamTests.cs create mode 100644 InterlockLedger.Commons/Types/System.IO/StreamIsReadonlyException.cs create mode 100644 InterlockLedger.Commons/Types/System.IO/WrappedReadonlyStream.cs diff --git a/InterlockLedger.Commons.NUnit.Tests/InterlockLedger.Commons.NUnit.Tests.csproj b/InterlockLedger.Commons.NUnit.Tests/InterlockLedger.Commons.NUnit.Tests.csproj index 30c96f8..156e2ab 100644 --- a/InterlockLedger.Commons.NUnit.Tests/InterlockLedger.Commons.NUnit.Tests.csproj +++ b/InterlockLedger.Commons.NUnit.Tests/InterlockLedger.Commons.NUnit.Tests.csproj @@ -1,6 +1,7 @@ - + - net7.0 + net8.0 + 14.0.0 preview false enable @@ -9,9 +10,9 @@ - - - + + + diff --git a/InterlockLedger.Commons.NUnit.Tests/System.IO/Types/NonSeekMemoryStream.cs b/InterlockLedger.Commons.NUnit.Tests/System.IO/Types/NonSeekMemoryStream.cs new file mode 100644 index 0000000..8b59b30 --- /dev/null +++ b/InterlockLedger.Commons.NUnit.Tests/System.IO/Types/NonSeekMemoryStream.cs @@ -0,0 +1,38 @@ +// ****************************************************************************************************************************** +// +// Copyright (c) 2018-2023 InterlockLedger Network +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES, LOSS OF USE, DATA, OR PROFITS, OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// ****************************************************************************************************************************** + +namespace System.IO; + +internal class NonSeekMemoryStream(byte[] buffer) : MemoryStream(buffer, writable: true) +{ + public override bool CanSeek => false; +} diff --git a/InterlockLedger.Commons.NUnit.Tests/System.IO/Types/StreamSpanTests.cs b/InterlockLedger.Commons.NUnit.Tests/System.IO/Types/StreamSpanTests.cs index 5faed39..3fb1975 100644 --- a/InterlockLedger.Commons.NUnit.Tests/System.IO/Types/StreamSpanTests.cs +++ b/InterlockLedger.Commons.NUnit.Tests/System.IO/Types/StreamSpanTests.cs @@ -34,29 +34,16 @@ namespace System.IO; public class StreamSpanTests { - [Test] - public void TestSkippingToEndOfSpanOnDisposeWithoutSeek() { - using var baseStream = new NonSeekMemoryStream(new byte[100]); - TestSkippingOn(baseStream); - } - [Test] public void TestSkippingToEndOfSpanOnDisposeWithSeek() { using var baseStream = new MemoryStream(new byte[100]); - TestSkippingOn(baseStream); - } - - private static void TestSkippingOn(Stream baseStream) { _ = baseStream.Seek(10, SeekOrigin.Begin); Assert.AreEqual(10L, baseStream.Position); baseStream.WriteByte(30); _ = baseStream.Seek(10, SeekOrigin.Begin); Assert.AreEqual(10L, baseStream.Position); using (var sp = new StreamSpan(baseStream, (ulong)baseStream.ReadByte())) { - if (sp.CanSeek) - Assert.AreEqual("[30] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ", sp.DEBUG_SomeBytes); - else - Assert.AreEqual(StreamSpan.NonSeekable, sp.DEBUG_SomeBytes); + Assert.AreEqual("StreamSpan [30] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ", sp.GetDebuggerDisplay()); Assert.AreEqual(30L, sp.Length); Assert.AreEqual(0L, sp.Position); Assert.AreEqual(11L, baseStream.Position); @@ -105,10 +92,7 @@ private static void TestSkippingOn(Stream baseStream) { Assert.AreEqual(41L, baseStream.Position); using (var sp2 = new StreamSpan(baseStream, (ulong)(baseStream.Length - baseStream.Position))) { - if (sp2.CanSeek) - Assert.AreEqual("[59] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ", sp2.DEBUG_SomeBytes); - else - Assert.AreEqual(StreamSpan.NonSeekable, sp2.DEBUG_SomeBytes); + Assert.AreEqual("StreamSpan [59] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ", sp2.GetDebuggerDisplay()); Assert.AreEqual(0L, sp2.Position); Assert.AreEqual(41L, baseStream.Position); Assert.AreEqual(baseStream.Position, sp2.OriginalPosition); @@ -121,8 +105,21 @@ private static void TestSkippingOn(Stream baseStream) { Assert.AreEqual(baseStream.Length, baseStream.Position); } - private class NonSeekMemoryStream(byte[] buffer) : MemoryStream(buffer, writable: true) - { - public override bool CanSeek => false; + [Test] + public void RejectNonSeekableOriginalStream() { + var e = Assert.Throws(() => new StreamSpan(new NonSeekMemoryStream([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), 10)); + Assert.AreEqual("original stream needs to be seekable", e?.Message); + } + + [Test] + public void RejectNegativeOriginOnOriginalStream() { + var e = Assert.Throws(() => new StreamSpan(new MemoryStream([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), -1, 10)); + StringAssert.Contains("offset ('-1') must be a non-negative value. (Parameter 'offset')", e?.Message); + } + + [Test] + public void RejectNullOriginalStream() { + var e = Assert.Throws(() => new StreamSpan(null!, 10)); + Assert.AreEqual("Required (Parameter 's')", e?.Message); } } diff --git a/InterlockLedger.Commons.NUnit.Tests/System.IO/Types/WrappedReadonlyStreamTests.cs b/InterlockLedger.Commons.NUnit.Tests/System.IO/Types/WrappedReadonlyStreamTests.cs new file mode 100644 index 0000000..ffb08d9 --- /dev/null +++ b/InterlockLedger.Commons.NUnit.Tests/System.IO/Types/WrappedReadonlyStreamTests.cs @@ -0,0 +1,94 @@ +// ****************************************************************************************************************************** +// +// Copyright (c) 2018-2023 InterlockLedger Network +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES, LOSS OF USE, DATA, OR PROFITS, OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// ****************************************************************************************************************************** + +namespace System.IO; + +internal class DisposableMemoryStream(byte[] bytes) : MemoryStream(bytes) +{ + public bool Disposed { get; private set; } + protected override void Dispose(bool disposing) { + base.Dispose(disposing); + Disposed = true; + } +} +public class WrappedReadonlyStreamTests +{ + [Test] + public void RejectNonSeekableOriginalStream() { + var e = Assert.Throws(() => new WrappedReadonlyStream(new NonSeekMemoryStream([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), 1, 10, closeWrappedStreamOnDispose: false)); + Assert.AreEqual("original stream needs to be seekable", e?.Message); + } + + [Test] + public void RejectNegativeOriginOnOriginalStream() { + var e = Assert.Throws(() => new WrappedReadonlyStream(new MemoryStream([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), -1, 10, closeWrappedStreamOnDispose: false)); + StringAssert.Contains("offset ('-1') must be a non-negative value. (Parameter 'offset')", e?.Message); + } + [Test] + public void AssertDisposalOfOriginalStreamOnCondition() { + var ms = new DisposableMemoryStream([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + using (var wrs = new WrappedReadonlyStream(ms, 1, 10, closeWrappedStreamOnDispose: false)) { + Assert.AreEqual(10L, wrs.Length); + CollectionAssert.AreEqual(new byte[] { 2, 3, 4 }, wrs.ReadBytes(3)); + } + Assert.AreEqual(4L, ms.Position); + Assert.False(ms.Disposed, "Original stream was disposed"); + using (var wrs = new WrappedReadonlyStream(ms, 0, -1, closeWrappedStreamOnDispose: false)) { + Assert.AreEqual(12L, wrs.Length); + CollectionAssert.AreEqual(new byte[] { 1, 2, 3, 4 }, wrs.ReadBytes(4)); + } + Assert.AreEqual(4L, ms.Position); + Assert.False(ms.Disposed, "Original stream was disposed"); + using (var wrs = new WrappedReadonlyStream(ms)) { + Assert.AreEqual(12L, wrs.Length); + CollectionAssert.AreEqual(new byte[] { 1, 2, 3, 4, 5 }, wrs.ReadBytes(5)); + } + Assert.AreEqual(5L, ms.Position); + Assert.False(ms.Disposed, "Original stream was disposed"); + using (var wrs = new WrappedReadonlyStream(ms, 1, 10, closeWrappedStreamOnDispose: true)) { + Assert.AreEqual(10L, wrs.Length); + wrs.Position = 5; + byte[] buffer = new byte[6]; + int count = wrs.Read(buffer, 0, buffer.Length); + Assert.AreEqual(5, count); + CollectionAssert.AreEqual(new byte[] { 7, 8, 9, 10, 11, 0 }, buffer); + Assert.AreEqual(11L, ms.Position); + } + Assert.True(ms.Disposed, "Original stream was not disposed"); + } + + [Test] + public void RejectNullOriginalStream() { + var e = Assert.Throws(() => new WrappedReadonlyStream(null!, 0, 10, closeWrappedStreamOnDispose: false)); + Assert.AreEqual("Required (Parameter 's')", e?.Message); + } +} diff --git a/InterlockLedger.Commons/Extensions/System.Buffers/ReadOnlyMemoryExtensions.cs b/InterlockLedger.Commons/Extensions/System.Buffers/ReadOnlyMemoryExtensions.cs index bf0853b..e4c883b 100644 --- a/InterlockLedger.Commons/Extensions/System.Buffers/ReadOnlyMemoryExtensions.cs +++ b/InterlockLedger.Commons/Extensions/System.Buffers/ReadOnlyMemoryExtensions.cs @@ -48,13 +48,15 @@ public static ReadOnlySequence ToSequence(this IEnumerable LinkedSegment.Link(segments!) }; +#pragma warning disable CA1055 // URI-like return values should not be strings public static string ToUrlSafeBase64(this byte[] bytes) => Convert.ToBase64String(bytes ?? throw new ArgumentNullException(nameof(bytes))) .Trim('=') .Replace('+', '-') .Replace('/', '_'); +#pragma warning restore CA1055 // URI-like return values should not be strings - private class LinkedSegment : ReadOnlySequenceSegment + private sealed class LinkedSegment : ReadOnlySequenceSegment { public int Length => Memory.Length; diff --git a/InterlockLedger.Commons/Extensions/System.Buffers/ReadOnlySequenceExtensions.cs b/InterlockLedger.Commons/Extensions/System.Buffers/ReadOnlySequenceExtensions.cs index 7b219df..6b2e583 100644 --- a/InterlockLedger.Commons/Extensions/System.Buffers/ReadOnlySequenceExtensions.cs +++ b/InterlockLedger.Commons/Extensions/System.Buffers/ReadOnlySequenceExtensions.cs @@ -49,8 +49,7 @@ public static ReadOnlySequence Add(this ReadOnlySequence sequence, b public static ReadOnlySequenceStream AsStream(this ReadOnlySequence memory) => new(memory); public static T AsStreamDo(this ReadOnlySequence memory, Func func) { - if (func is null) - throw new ArgumentNullException(nameof(func)); + ArgumentNullException.ThrowIfNull(func); using Stream s = memory.AsStream(); return func(s); } @@ -73,10 +72,12 @@ public static ReadOnlySequence Realloc(this ReadOnlySequence body) { return new ReadOnlySequence(newBuffer); } +#pragma warning disable CA1055 // URI-like return values should not be strings public static string ToUrlSafeBase64(this ReadOnlySequence readOnlyBytes) => readOnlyBytes.Length > 256 ? ReadOnlyMemoryExtensions.ToUrlSafeBase64(readOnlyBytes.Slice(0, 256).ToArray()) + "..." : ReadOnlyMemoryExtensions.ToUrlSafeBase64(readOnlyBytes.ToArray()); +#pragma warning restore CA1055 // URI-like return values should not be strings private static IEnumerable> Append(this ReadOnlySequence sequence, ReadOnlyMemory memory) { var current = sequence.Start; diff --git a/InterlockLedger.Commons/Extensions/System.Collections.Generic/ListExtensions.cs b/InterlockLedger.Commons/Extensions/System.Collections.Generic/ListExtensions.cs index 7cb6b5d..4bf75bb 100644 --- a/InterlockLedger.Commons/Extensions/System.Collections.Generic/ListExtensions.cs +++ b/InterlockLedger.Commons/Extensions/System.Collections.Generic/ListExtensions.cs @@ -34,8 +34,10 @@ namespace System.Collections.Generic; public static class ListExtensions { +#pragma warning disable CA1002 // Do not expose generic lists public static List? SafeAdd(this List? list, params T[] itens) { list?.AddRange(itens); return list; } +#pragma warning restore CA1002 // Do not expose generic lists } diff --git a/InterlockLedger.Commons/Extensions/System.IO/FileInfoExtensions.cs b/InterlockLedger.Commons/Extensions/System.IO/FileInfoExtensions.cs index f8427e4..1d44670 100644 --- a/InterlockLedger.Commons/Extensions/System.IO/FileInfoExtensions.cs +++ b/InterlockLedger.Commons/Extensions/System.IO/FileInfoExtensions.cs @@ -48,23 +48,18 @@ public static async Task ReadToEndAsync(this FileInfo file, string missi using var reader = file.OpenText(); return await reader.ReadToEndAsync().ConfigureAwait(false); } - return string.Format(missingFileMessageMask.Required(), file.Name); + return string.Format(CultureInfo.InvariantCulture, missingFileMessageMask.Required(), file.Name); } - private class FbbaInputStream : FileStream + private sealed class FbbaInputStream(FileInfo fileInfo, Action onDispose) : FileStream(fileInfo.Required().FullName, FileMode.CreateNew, FileAccess.Write) { - public FbbaInputStream(FileInfo fileInfo, Action onDispose) : base(fileInfo.Required().FullName, FileMode.CreateNew, FileAccess.Write) { - _fileInfo = fileInfo.Required(); - _onDispose = onDispose.Required(); - } - protected override void Dispose(bool disposing) { base.Dispose(disposing); if (disposing) _onDispose(_fileInfo); } - private readonly FileInfo _fileInfo; - private readonly Action _onDispose; + private readonly FileInfo _fileInfo = fileInfo.Required(); + private readonly Action _onDispose = onDispose.Required(); } } diff --git a/InterlockLedger.Commons/Extensions/System.IO/StreamExtensions.cs b/InterlockLedger.Commons/Extensions/System.IO/StreamExtensions.cs index ff8ad6f..e725e70 100644 --- a/InterlockLedger.Commons/Extensions/System.IO/StreamExtensions.cs +++ b/InterlockLedger.Commons/Extensions/System.IO/StreamExtensions.cs @@ -37,10 +37,8 @@ namespace System.IO; public static partial class StreamExtensions { public static async Task CopyToAsync(this Stream source, Stream destination, long fileSizeLimit, int bufferSize, CancellationToken cancellationToken) { - if (source is null) - throw new ArgumentNullException(nameof(source)); - if (destination is null) - throw new ArgumentNullException(nameof(destination)); + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(destination); if (source.CanSeek) CheckSizeLimit(source.Length); byte[] buffer = ArrayPool.Shared.Rent(bufferSize); @@ -66,8 +64,7 @@ void CheckSizeLimit(long totalBytes) { public static bool HasBytes(this Stream s) => s is not null && s.CanSeek && s.Position < s.Length; public static async Task ReadAllBytesAsync(this Stream s) { - if (s == null) - throw new ArgumentNullException(nameof(s)); + ArgumentNullException.ThrowIfNull(s); using var buffer = new MemoryStream(); await s.CopyToAsync(buffer).ConfigureAwait(false); return buffer.ToArray(); @@ -75,7 +72,7 @@ public static async Task ReadAllBytesAsync(this Stream s) { public static byte[] ReadBytes(this Stream s, int length) { if (s is null || length <= 0) - return Array.Empty(); + return []; byte[] bytes = new byte[length]; int offset = 0; int retries = 3; @@ -97,8 +94,7 @@ public static byte[] ReadBytes(this Stream s, int length) { } public static byte[] ReadExactly(this Stream s, int length) { - if (s is null) - throw new ArgumentNullException(nameof(s)); + ArgumentNullException.ThrowIfNull(s); int offset = 0; byte[] buffer = new byte[length]; while (offset < length) { @@ -109,8 +105,7 @@ public static byte[] ReadExactly(this Stream s, int length) { } public static byte ReadSingleByte(this Stream s) { - if (s is null) - throw new ArgumentNullException(nameof(s)); + ArgumentNullException.ThrowIfNull(s); byte[] bytes = new byte[1]; int retries = 3; while (retries-- > 0) { @@ -123,8 +118,7 @@ public static byte ReadSingleByte(this Stream s) { } public static Stream WriteBytes(this Stream s, byte[] bytes) { - if (s is null) - throw new ArgumentNullException(nameof(s)); + ArgumentNullException.ThrowIfNull(s); if (bytes?.Length > 0) s.Write(bytes, 0, bytes.Length); s.Flush(); @@ -132,8 +126,7 @@ public static Stream WriteBytes(this Stream s, byte[] bytes) { } public static Stream WriteSingleByte(this Stream s, byte value) { - if (s is null) - throw new ArgumentNullException(nameof(s)); + ArgumentNullException.ThrowIfNull(s); s.WriteByte(value); return s; } diff --git a/InterlockLedger.Commons/Extensions/System.Linq/IEnumerableOfReadOnlyMemoryExtensions.cs b/InterlockLedger.Commons/Extensions/System.Linq/IEnumerableOfReadOnlyMemoryExtensions.cs index 9cfbcee..be4b215 100644 --- a/InterlockLedger.Commons/Extensions/System.Linq/IEnumerableOfReadOnlyMemoryExtensions.cs +++ b/InterlockLedger.Commons/Extensions/System.Linq/IEnumerableOfReadOnlyMemoryExtensions.cs @@ -36,7 +36,7 @@ public static class IEnumerableOfReadOnlyMemoryExtensions { public static T[] ToArray(this IEnumerable> buffers) { if (buffers.None()) - return Array.Empty(); + return []; int length = buffers.Sum(rom => rom.Length); var result = new T[length]; int offset = 0; diff --git a/InterlockLedger.Commons/Extensions/System/ArrayOfByteExtensions.cs b/InterlockLedger.Commons/Extensions/System/ArrayOfByteExtensions.cs index 17f3152..b7d48fd 100644 --- a/InterlockLedger.Commons/Extensions/System/ArrayOfByteExtensions.cs +++ b/InterlockLedger.Commons/Extensions/System/ArrayOfByteExtensions.cs @@ -30,6 +30,7 @@ // // ****************************************************************************************************************************** +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; namespace System; @@ -70,9 +71,7 @@ public static ulong AsULong(this byte[] bytes, int offset = 0) { public static string AsUTF8String(this byte[] bytes) => Encoding.UTF8.GetString(bytes); public static string Chunked(this byte[] bytes, int length) { - if (bytes == null) { - throw new ArgumentNullException(nameof(bytes)); - } + ArgumentNullException.ThrowIfNull(bytes); length = (int)(Math.Floor((decimal)(Math.Abs(length) / 4)) + 1) * 4; string value = Convert.ToBase64String(bytes).Replace('+', '-').Replace('/', '_'); @@ -104,8 +103,7 @@ public static int CompareTo(this byte[] bytes1, byte[] bytes2) { } public static void DumpToFile(this byte[] bytes, string filename) { - if (bytes is null) - throw new ArgumentNullException(nameof(bytes)); + ArgumentNullException.ThrowIfNull(bytes); if (string.IsNullOrEmpty(filename)) throw new ArgumentException("message", nameof(filename)); using var file = File.CreateText(filename); @@ -133,7 +131,7 @@ public static byte[] FromSafeBase64(this string base64) { } } - return Array.Empty(); + return []; } public static bool HasSameBytesAs(this byte[] bytes1, params byte[] bytes2) { @@ -157,11 +155,7 @@ public static byte[] PartOf(this byte[] bytes, int length, int offset = 0) { Array.Copy(bytes, offset, part, 0, length); return part; } - public static byte[] RandomBytes(this int size) { - byte[] buffer = new byte[size]; - _rnd.NextBytes(buffer); - return buffer; - } + public static byte[] RandomBytes(this int size) => RandomNumberGenerator.GetBytes(size); public static int SafeGetHashCode(this byte[] bytes) => bytes?.ToSafeBase64().GetHashCode(StringComparison.InvariantCulture) ?? 0; @@ -184,7 +178,4 @@ private static string PadBase64(string base64) { base64 += "="; return base64; } - - private static readonly Random _rnd = new(); - } diff --git a/InterlockLedger.Commons/Extensions/System/ArrayOfTExtensions.cs b/InterlockLedger.Commons/Extensions/System/ArrayOfTExtensions.cs index 1c19c03..06d8d3e 100644 --- a/InterlockLedger.Commons/Extensions/System/ArrayOfTExtensions.cs +++ b/InterlockLedger.Commons/Extensions/System/ArrayOfTExtensions.cs @@ -38,7 +38,7 @@ public static string JoinedBy(this T[]? values, string joiner) => [return: NotNull] public static T[] OrEmpty(this T[]? values) => - values ?? Array.Empty(); + values ?? []; [return: NotNull] public static T[] MinLength([NotNull] this T[] array, int length, [CallerArgumentExpression(nameof(array))] string? parameterName = null) => diff --git a/InterlockLedger.Commons/Extensions/System/DateOnlyConverter.cs b/InterlockLedger.Commons/Extensions/System/DateOnlyConverter.cs index 55a5b85..7802674 100644 --- a/InterlockLedger.Commons/Extensions/System/DateOnlyConverter.cs +++ b/InterlockLedger.Commons/Extensions/System/DateOnlyConverter.cs @@ -34,12 +34,9 @@ namespace System; -public class DateOnlyConverter : JsonConverter +public class DateOnlyConverter(string? format = null) : JsonConverter { - private readonly string _format; - - public DateOnlyConverter(string? format = null) => - _format = format ?? "yyyy'-'MM'-'dd"; + private readonly string _format = format ?? "yyyy'-'MM'-'dd"; public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(DateOnly); @@ -54,5 +51,5 @@ public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, Jso throw new InvalidDataException("Not a valid date"); } public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) => - writer.WriteStringValue(value.ToString(_format, CultureInfo.InvariantCulture)); + writer.Required().WriteStringValue(value.ToString(_format, CultureInfo.InvariantCulture)); } diff --git a/InterlockLedger.Commons/Extensions/System/ObjectExtensions.cs b/InterlockLedger.Commons/Extensions/System/ObjectExtensions.cs index b7d033b..eecc838 100644 --- a/InterlockLedger.Commons/Extensions/System/ObjectExtensions.cs +++ b/InterlockLedger.Commons/Extensions/System/ObjectExtensions.cs @@ -43,8 +43,10 @@ public static class ObjectExtensions public static IEnumerable AsSingle(this T s) => new SingleEnumerable(s); +#pragma warning disable CA1002 // Do not expose generic lists public static List AsSingleList(this T s) => s.AsSingle().ToList(); +#pragma warning restore CA1002 // Do not expose generic lists public static async Task IfNotNullAsync(this T? value, Func> transformAsync) where T : class diff --git a/InterlockLedger.Commons/Extensions/System/StringExtensions.cs b/InterlockLedger.Commons/Extensions/System/StringExtensions.cs index 667baaf..3ba3888 100644 --- a/InterlockLedger.Commons/Extensions/System/StringExtensions.cs +++ b/InterlockLedger.Commons/Extensions/System/StringExtensions.cs @@ -82,7 +82,7 @@ public static bool IsParenthesized(this string? s) { return false; s = s.Trim(); int count = 0; - if (!s.StartsWith("(", StringComparison.Ordinal)) return false; + if (!s.StartsWith('(')) return false; for (int i = 0; i < s.Length; i++) { char c = s[i]; if (c == '(') { diff --git a/InterlockLedger.Commons/InterlockLedger.Commons.csproj b/InterlockLedger.Commons/InterlockLedger.Commons.csproj index f01ff33..5e627aa 100644 --- a/InterlockLedger.Commons/InterlockLedger.Commons.csproj +++ b/InterlockLedger.Commons/InterlockLedger.Commons.csproj @@ -1,6 +1,6 @@ - net7.0 + net8.0 preview true InterlockLedger Network Team @@ -11,9 +11,9 @@ InterlockLedger git https://github.com/interlockledger/interlockledger-commons.git - 13.0.0 + 14.0.0 InterlockLedger.Commons - Bring more bits from client projects + Refactor StreamSpan into a superclass that can work without knowing the length il2.png true true diff --git a/InterlockLedger.Commons/Statics/EnumerableHelpers.cs b/InterlockLedger.Commons/Statics/EnumerableHelpers.cs index 6e7be79..c4d2a7a 100644 --- a/InterlockLedger.Commons/Statics/EnumerableHelpers.cs +++ b/InterlockLedger.Commons/Statics/EnumerableHelpers.cs @@ -39,7 +39,7 @@ public static class Helpers public static void Try(Action action, Action? errorHandler = null) { try { - action(); + action.Required()(); } catch (Exception ex) { if (errorHandler is not null) errorHandler(ex); @@ -48,7 +48,7 @@ public static void Try(Action action, Action? errorHandler = null) { public static Result Try(Func action, Action? errorHandler = null) where T : class { try { - return action(); + return action.Required()(); } catch (Exception ex) { if (errorHandler is not null) errorHandler(ex); diff --git a/InterlockLedger.Commons/Types/System.Buffers/ReadOnlySequenceStream.cs b/InterlockLedger.Commons/Types/System.Buffers/ReadOnlySequenceStream.cs index 2b323bc..833a693 100644 --- a/InterlockLedger.Commons/Types/System.Buffers/ReadOnlySequenceStream.cs +++ b/InterlockLedger.Commons/Types/System.Buffers/ReadOnlySequenceStream.cs @@ -67,8 +67,7 @@ public override void Flush() { } public override int Read(byte[] buffer, int offset, int count) { - if (buffer is null) - throw new ArgumentNullException(nameof(buffer)); + ArgumentNullException.ThrowIfNull(buffer); if (offset < 0 || offset >= buffer.Length) throw new ArgumentOutOfRangeException(nameof(offset)); if (count < 0 || offset + count > buffer.Length) diff --git a/InterlockLedger.Commons/Types/System.Collections.Generic/SingleEnumerable.cs b/InterlockLedger.Commons/Types/System.Collections.Generic/SingleEnumerable.cs index b1df39d..e288260 100644 --- a/InterlockLedger.Commons/Types/System.Collections.Generic/SingleEnumerable.cs +++ b/InterlockLedger.Commons/Types/System.Collections.Generic/SingleEnumerable.cs @@ -32,17 +32,13 @@ namespace System.Collections.Generic; -public sealed class SingleEnumerable : IEnumerable +public sealed class SingleEnumerable(T singleElement) : IEnumerable { - public SingleEnumerable(T singleElement) => _singleElement = singleElement; + public IEnumerator GetEnumerator() => new Enumerator(singleElement); - public IEnumerator GetEnumerator() => new Enumerator(_singleElement); + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(singleElement); - IEnumerator IEnumerable.GetEnumerator() => new Enumerator(_singleElement); - - private readonly T _singleElement; - - private class Enumerator : IEnumerator + private sealed class Enumerator : IEnumerator { public Enumerator(T singleElement) { _value = singleElement; diff --git a/InterlockLedger.Commons/Types/System.IO/StreamIsReadonlyException.cs b/InterlockLedger.Commons/Types/System.IO/StreamIsReadonlyException.cs new file mode 100644 index 0000000..97d6122 --- /dev/null +++ b/InterlockLedger.Commons/Types/System.IO/StreamIsReadonlyException.cs @@ -0,0 +1,38 @@ +// ****************************************************************************************************************************** +// +// Copyright (c) 2018-2023 InterlockLedger Network +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES, LOSS OF USE, DATA, OR PROFITS, OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// ****************************************************************************************************************************** + +namespace System.IO; + +public class StreamIsReadonlyException : Exception +{ + public StreamIsReadonlyException() : base("This stream is readonly") { } +} diff --git a/InterlockLedger.Commons/Types/System.IO/StreamSpan.cs b/InterlockLedger.Commons/Types/System.IO/StreamSpan.cs index 2e39a68..9bd48ca 100644 --- a/InterlockLedger.Commons/Types/System.IO/StreamSpan.cs +++ b/InterlockLedger.Commons/Types/System.IO/StreamSpan.cs @@ -32,176 +32,24 @@ namespace System.IO; -public class StreamSpan : Stream +public sealed class StreamSpan : WrappedReadonlyStream { - public StreamSpan(Stream s, ulong length) : this(s, -1, length) { + public StreamSpan(Stream s, ulong length) : this(s, s.Required().Position, length) { } - public const string NonSeekable = "Non-seekable"; + public StreamSpan(Stream s, long offset, ulong length, bool closeWrappedStreamOnDispose = false) : base(s, offset, ValidateLength(length), closeWrappedStreamOnDispose) + => _positionAfterSpan = Length + _begin; - public StreamSpan(Stream s, long offset, ulong length, bool closeWrappedStreamOnDispose = false) { - _s = s.Required(); - _closeWrappedStreamOnDispose = closeWrappedStreamOnDispose; - if (!s.CanRead) - throw new ArgumentException("original stream needs to be readable"); - _length = (long)length; - if (_length < 0) - throw new ArgumentException("length is too large and wrapped around!!!"); - if (offset >= 0 && offset != s.Position) - s.Position = s.CanSeek - ? offset - : throw new ArgumentException("offset doesn't match current position on non-seeking stream"); - _begin = s.Position; - _positionAfterSpan = _length + _begin; - DEBUG_SomeBytes = "Empty"; - if (_length > 0) { - if (!s.CanSeek) - DEBUG_SomeBytes = NonSeekable; - else { - try { - Position = 0; - if (_length > 100) { - byte[] buffer = new byte[50]; - string head = DumpBytes(buffer); - Position = Length - 50; - string tail = DumpBytes(buffer); - DEBUG_SomeBytes = $"[{_length}] {head}... {tail}"; - - } else { - byte[] buffer = new byte[_length]; - DEBUG_SomeBytes = $"[{_length}] {DumpBytes(buffer)}"; - } - } catch (Exception e) { - DEBUG_SomeBytes = e.Message; - } finally { - Position = 0; - } - } - } - } - - private string DumpBytes(byte[] buffer) { - _ = Read(buffer, 0, buffer.Length); - var sb = new StringBuilder(); - foreach (byte b in buffer) - _ = sb.Append(b).Append(' '); - return sb.ToString(); - } - - public override bool CanRead => true; - - public override bool CanSeek => _s.CanSeek; - - public override bool CanWrite => false; - - public override long Length => _length; - - public long OriginalPosition => _s is StreamSpan ss ? ss.OriginalPosition : _s.Position; - public override long Position { - get => _s.Position - _begin; - set { - if (!CanSeek) - throw new NotSupportedException("Can't position non-seekable stream"); - if (value < 0) - throw new ArgumentOutOfRangeException(nameof(value), "Can't position before start"); - if (value > _length) - throw new ArgumentOutOfRangeException(nameof(value), "Can't position after end"); - _s.Position = value + _begin; - } - } - - public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => throw new NotSupportedException(_isReadonly); - - public override void EndWrite(IAsyncResult asyncResult) => throw new NotSupportedException(_isReadonly); - - public override void Flush() { - } - - public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; - - public override int Read(byte[] buffer, int offset, int count) { - if (buffer.Length == 0 || count == 0) - return 0; - if (offset < 0) - throw new ArgumentOutOfRangeException(nameof(offset), "Value is less than zero"); - if (offset >= buffer.Length) - throw new ArgumentOutOfRangeException(nameof(offset), "Value is more than the size of the buffer"); - if (Position + count > _length) { - long newCount = _length - Position; - if (newCount <= 0) - return 0; - count = newCount > int.MaxValue ? int.MaxValue : (int)newCount; - } - return _s.Read(buffer, offset, count); - } - - public override long Seek(long offset, SeekOrigin origin) { - return CanSeek - ? _s.Seek(AdjustOffset(offset, origin), SeekOrigin.Begin) - _begin - : throw new NotSupportedException("Can't position non-seekable stream"); - - long AdjustOffset(long offset, SeekOrigin origin) => - origin switch { - SeekOrigin.Begin => ValidateWithinBounds(offset), - SeekOrigin.Current => ValidateWithinBounds(offset + Position), - SeekOrigin.End => ValidateWithinBounds(_length + offset), - _ => throw new ArgumentException($"Unknown origin {origin}") - }; - long ValidateWithinBounds(long offset) => - offset < 0 - ? throw new ArgumentOutOfRangeException(nameof(offset), "Can't position before start") - : offset > _length - ? throw new ArgumentOutOfRangeException(nameof(offset), "Can't position after end") - : offset + _begin; + private static long ValidateLength(ulong length) { + long signedLength = (long)length; + return signedLength < 0 ? throw new ArgumentException("length is too large and wrapped around!!!") : signedLength; } - public override void SetLength(long value) => throw new NotSupportedException("This StreamSpan can't have its length changed"); - - public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(_isReadonly); - - public override void Write(ReadOnlySpan buffer) => throw new NotSupportedException(_isReadonly); - - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw new NotSupportedException(_isReadonly); - - public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => throw new NotSupportedException(_isReadonly); - - public override void WriteByte(byte value) => throw new NotSupportedException(_isReadonly); - - protected override void Dispose(bool disposing) { - base.Dispose(disposing); - if (_closeWrappedStreamOnDispose) { - _s.Dispose(); - } else { - int unreadBytes = (int)(_positionAfterSpan - _s.Position); - if (unreadBytes > 0) { - if (CanSeek) { - _s.Position = _positionAfterSpan; - } else { - AdvanceByReading(_s, unreadBytes); - } - } - } - - static void AdvanceByReading(Stream s, int unreadBytes) { - int bufferSize = Math.Min(unreadBytes, 16 * 1024); - byte[] buffer = new byte[unreadBytes]; - while (unreadBytes > bufferSize) { - int read = s.Read(buffer, 0, bufferSize); - if (read == 0) - return; - unreadBytes -= read; - } - - _ = s.Read(buffer, 0, unreadBytes); + protected sealed override void DisposingButNotClosingWrappedStream() { + if ((_positionAfterSpan - _s.Position) > 0) { + _s.Position = _positionAfterSpan; } } - public readonly string DEBUG_SomeBytes; - - private const string _isReadonly = "This StreamSpan is readonly"; - private readonly long _begin; - private readonly bool _closeWrappedStreamOnDispose; - private readonly long _length; private readonly long _positionAfterSpan; - private readonly Stream _s; } diff --git a/InterlockLedger.Commons/Types/System.IO/WrappedReadonlyStream.cs b/InterlockLedger.Commons/Types/System.IO/WrappedReadonlyStream.cs new file mode 100644 index 0000000..17fa230 --- /dev/null +++ b/InterlockLedger.Commons/Types/System.IO/WrappedReadonlyStream.cs @@ -0,0 +1,192 @@ +// ****************************************************************************************************************************** +// +// Copyright (c) 2018-2023 InterlockLedger Network +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES, LOSS OF USE, DATA, OR PROFITS, OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// ****************************************************************************************************************************** + +using System.Diagnostics; + +namespace System.IO; + +[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] +public class WrappedReadonlyStream : Stream +{ + protected readonly long _begin; + protected readonly Stream _s; + + /// + /// Wrap around a stream as a readonly stream with origin at offset and length + /// + /// Original stream to be wrapped + /// Origin (Position 0) of this instance in original stream terms + /// Length to limit reading for, if -1 no limit is imposed defauting to original stream Length + /// When this is true wrapped stream will be disposed (closed) when this stream is disposed + /// Stream s must be provided and readable and if non-seekable position must be at offset + public WrappedReadonlyStream(Stream s, long offset, long length, bool closeWrappedStreamOnDispose) { + _s = s.Required(); + _closeWrappedStreamOnDispose = closeWrappedStreamOnDispose; + if (!s.CanRead) + throw new ArgumentException("original stream needs to be readable"); + if (!s.CanSeek) + throw new ArgumentException("original stream needs to be seekable"); + ArgumentOutOfRangeException.ThrowIfNegative(offset); + if (offset != s.Position) + s.Position = offset; + _begin = s.Position; + _length = length >= 0 ? length : s.Length - _begin; + DebugHelperInitialize(); + } + + public WrappedReadonlyStream(Stream s) : this(s, 0, -1, closeWrappedStreamOnDispose: false) { + } + + protected sealed override void Dispose(bool disposing) { + base.Dispose(disposing); + if (_closeWrappedStreamOnDispose) + _s.Dispose(); + else + DisposingButNotClosingWrappedStream(); + } + + protected virtual void DisposingButNotClosingWrappedStream() { } + + public override bool CanRead => true; + + public override bool CanSeek => true; + + public override bool CanWrite => false; + + public override long Length => _length; + + public long OriginalPosition => _s is WrappedReadonlyStream ss ? ss.OriginalPosition : _s.Position; + public override long Position { + get => _s.Position - _begin; + set { + if (value < 0) + throw new ArgumentOutOfRangeException(nameof(value), "Can't position before start"); + if (value > Length) + throw new ArgumentOutOfRangeException(nameof(value), "Can't position after end"); + _s.Position = value + _begin; + } + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => throw new StreamIsReadonlyException(); + + public override void EndWrite(IAsyncResult asyncResult) => throw new StreamIsReadonlyException(); + + public override void Flush() { + } + + public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public override int Read(byte[] buffer, int offset, int count) { + if (buffer.Required().Length == 0 || count <= 0) + return 0; + if (offset < 0) + throw new ArgumentOutOfRangeException(nameof(offset), "Value is less than zero"); + if (offset >= buffer.Length) + throw new ArgumentOutOfRangeException(nameof(offset), "Value is more than the size of the buffer"); + if (offset + count > buffer.Length) + count = buffer.Length - offset; + if (Position + count > Length) { + long newCount = Length - Position; + if (newCount <= 0) + return 0; + count = newCount > int.MaxValue ? int.MaxValue : (int)newCount; + } + return _s.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) { + return _s.Seek(AdjustOffset(offset, origin), SeekOrigin.Begin) - _begin; + + long AdjustOffset(long offset, SeekOrigin origin) => + origin switch { + SeekOrigin.Begin => ValidateWithinBounds(offset), + SeekOrigin.Current => ValidateWithinBounds(offset + Position), + SeekOrigin.End => ValidateWithinBounds(Length + offset), + _ => throw new ArgumentException($"Unknown origin {origin}") + }; + long ValidateWithinBounds(long offset) => + offset < 0 + ? throw new ArgumentOutOfRangeException(nameof(offset), "Can't position before start") + : offset > Length + ? throw new ArgumentOutOfRangeException(nameof(offset), "Can't position after end") + : offset + _begin; + } + + public override void SetLength(long value) => throw new NotSupportedException("This StreamSpan can't have its length changed"); + + public override void Write(byte[] buffer, int offset, int count) => throw new StreamIsReadonlyException(); + + public override void Write(ReadOnlySpan buffer) => throw new StreamIsReadonlyException(); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw new StreamIsReadonlyException(); + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => throw new StreamIsReadonlyException(); + + public override void WriteByte(byte value) => throw new StreamIsReadonlyException(); + + [Conditional("DEBUG")] + private void DebugHelperInitialize() { + if (Length == 0) + _someBytesForDebug = "Empty"; + else + try { + Position = 0; + if (Length > 100) { + byte[] buffer = new byte[50]; + string head = DumpBytes(buffer); + Position = Length - 50; + string tail = DumpBytes(buffer); + _someBytesForDebug = $"[{Length}] {head}... {tail}"; + } else { + byte[] buffer = new byte[Length]; + _someBytesForDebug = $"[{Length}] {DumpBytes(buffer)}"; + } + } catch (Exception e) { + _someBytesForDebug = e.Message; + } finally { + Position = 0; + } + + string DumpBytes(byte[] buffer) { + _ = Read(buffer, 0, buffer.Length); + var sb = new StringBuilder(); + foreach (byte b in buffer) + _ = sb.Append(b).Append(' '); + return sb.ToString(); + } + } + + private readonly bool _closeWrappedStreamOnDispose; + private readonly long _length; + private string _someBytesForDebug = "?"; + public string GetDebuggerDisplay() => $"{GetType().Name} {_someBytesForDebug}"; +} diff --git a/InterlockLedger.Commons/Types/System.Threading.Tasks/AsyncLock.cs b/InterlockLedger.Commons/Types/System.Threading.Tasks/AsyncLock.cs index d95acb0..82126cc 100644 --- a/InterlockLedger.Commons/Types/System.Threading.Tasks/AsyncLock.cs +++ b/InterlockLedger.Commons/Types/System.Threading.Tasks/AsyncLock.cs @@ -62,7 +62,7 @@ public Task LockAsync() { private readonly AsyncLock _asyncLocktoRelease; } - private class AsyncSemaphore + private sealed class AsyncSemaphore { public AsyncSemaphore() => _currentCount = 1; diff --git a/InterlockLedger.Commons/Types/System/Result.cs b/InterlockLedger.Commons/Types/System/Result.cs index aa845b7..c97dd07 100644 --- a/InterlockLedger.Commons/Types/System/Result.cs +++ b/InterlockLedger.Commons/Types/System/Result.cs @@ -44,7 +44,7 @@ public class Result : IResult public static Result Ok { get; } = new(IResult._noError); public static implicit operator Result(Exception error) => new Error(error); - public static implicit operator bool(Result result) => result.Success; + public static implicit operator bool(Result result) => result.Required().Success; public bool Success => _errorType == IResult._noError; protected Result(int errorType) => _errorType = errorType; @@ -77,7 +77,7 @@ public Error(string errorMessage, int errorType = IError.DefaultErrorType) : bas public class Result : Result { - public static implicit operator T?(Result r) => r.Value; + public static implicit operator T?(Result r) => r.Required().Value; public static implicit operator Result(T? value) => new(value);