Skip to content

Commit

Permalink
v14.0.0 - Refactor StreamSpan into a superclass that can work without…
Browse files Browse the repository at this point in the history
… specifying a length
  • Loading branch information
monoman committed Nov 21, 2023
1 parent ba414e4 commit a7d3fbd
Show file tree
Hide file tree
Showing 24 changed files with 442 additions and 256 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Version>14.0.0</Version>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
Expand All @@ -9,9 +10,9 @@
<Target Name="TagSources" />
<Target Name="NugetOrg" />
<ItemGroup>
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\InterlockLedger.Commons\InterlockLedger.Commons.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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<ArgumentException>(() => 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<ArgumentOutOfRangeException>(() => 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<ArgumentException>(() => new StreamSpan(null!, 10));
Assert.AreEqual("Required (Parameter 's')", e?.Message);
}
}
Original file line number Diff line number Diff line change
@@ -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<ArgumentException>(() => 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<ArgumentOutOfRangeException>(() => 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<ArgumentException>(() => new WrappedReadonlyStream(null!, 0, 10, closeWrappedStreamOnDispose: false));
Assert.AreEqual("Required (Parameter 's')", e?.Message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,15 @@ public static ReadOnlySequence<byte> ToSequence(this IEnumerable<ReadOnlyMemory<
_ => 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<byte>
private sealed class LinkedSegment : ReadOnlySequenceSegment<byte>
{
public int Length => Memory.Length;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ public static ReadOnlySequence<byte> Add(this ReadOnlySequence<byte> sequence, b
public static ReadOnlySequenceStream AsStream(this ReadOnlySequence<byte> memory) => new(memory);

public static T AsStreamDo<T>(this ReadOnlySequence<byte> memory, Func<Stream, T> func) {
if (func is null)
throw new ArgumentNullException(nameof(func));
ArgumentNullException.ThrowIfNull(func);
using Stream s = memory.AsStream();
return func(s);
}
Expand All @@ -73,10 +72,12 @@ public static ReadOnlySequence<byte> Realloc(this ReadOnlySequence<byte> body) {
return new ReadOnlySequence<byte>(newBuffer);
}

#pragma warning disable CA1055 // URI-like return values should not be strings
public static string ToUrlSafeBase64(this ReadOnlySequence<byte> 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<ReadOnlyMemory<byte>> Append(this ReadOnlySequence<byte> sequence, ReadOnlyMemory<byte> memory) {
var current = sequence.Start;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ namespace System.Collections.Generic;

public static class ListExtensions
{
#pragma warning disable CA1002 // Do not expose generic lists
public static List<T>? SafeAdd<T>(this List<T>? list, params T[] itens) {
list?.AddRange(itens);
return list;
}
#pragma warning restore CA1002 // Do not expose generic lists
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,18 @@ public static async Task<string> 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<FileInfo> onDispose) : FileStream(fileInfo.Required().FullName, FileMode.CreateNew, FileAccess.Write)
{
public FbbaInputStream(FileInfo fileInfo, Action<FileInfo> 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<FileInfo> _onDispose;
private readonly FileInfo _fileInfo = fileInfo.Required();
private readonly Action<FileInfo> _onDispose = onDispose.Required();
}
}
23 changes: 8 additions & 15 deletions InterlockLedger.Commons/Extensions/System.IO/StreamExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte>.Shared.Rent(bufferSize);
Expand All @@ -66,16 +64,15 @@ 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<byte[]> 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();
}

public static byte[] ReadBytes(this Stream s, int length) {
if (s is null || length <= 0)
return Array.Empty<byte>();
return [];
byte[] bytes = new byte[length];
int offset = 0;
int retries = 3;
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -123,17 +118,15 @@ 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();
return s;
}

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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public static class IEnumerableOfReadOnlyMemoryExtensions
{
public static T[] ToArray<T>(this IEnumerable<ReadOnlyMemory<T>> buffers) {
if (buffers.None())
return Array.Empty<T>();
return [];
int length = buffers.Sum(rom => rom.Length);
var result = new T[length];
int offset = 0;
Expand Down
Loading

0 comments on commit a7d3fbd

Please sign in to comment.