// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
#if !NETSTANDARD2_1_OR_GREATER
using System.Buffers;
using System.Threading;
using System.Threading.Tasks;
#endif
namespace CommunityToolkit.HighPerformance;
///
/// Helpers for working with the type.
///
public static class StreamExtensions
{
#if !NETSTANDARD2_1_OR_GREATER
///
/// Asynchronously reads a sequence of bytes from a given instance.
///
/// The source to read data from.
/// The destination to write data to.
/// The optional for the operation.
/// A representing the operation being performed.
public static ValueTask ReadAsync(this Stream stream, Memory buffer, CancellationToken cancellationToken = default)
{
if (cancellationToken.IsCancellationRequested)
{
return new ValueTask(Task.FromCanceled(cancellationToken));
}
// If the memory wraps an array, extract it and use it directly
if (MemoryMarshal.TryGetArray(buffer, out ArraySegment segment))
{
return new ValueTask(stream.ReadAsync(segment.Array!, segment.Offset, segment.Count, cancellationToken));
}
// Local function used as the fallback path. This happens when the input memory
// doesn't wrap an array instance we can use. We use a local function as we need
// the body to be asynchronous, in order to execute the finally block after the
// write operation has been completed. By separating the logic, we can keep the
// main method as a synchronous, value-task returning function. This fallback
// path should hopefully be pretty rare, as memory instances are typically just
// created around arrays, often being rented from a memory pool in particular.
static async Task ReadAsyncFallback(Stream stream, Memory buffer, CancellationToken cancellationToken)
{
byte[] rent = ArrayPool.Shared.Rent(buffer.Length);
try
{
int bytesRead = await stream.ReadAsync(rent, 0, buffer.Length, cancellationToken);
if (bytesRead > 0)
{
rent.AsSpan(0, bytesRead).CopyTo(buffer.Span);
}
return bytesRead;
}
finally
{
ArrayPool.Shared.Return(rent);
}
}
return new ValueTask(ReadAsyncFallback(stream, buffer, cancellationToken));
}
///
/// Asynchronously writes a sequence of bytes to a given instance.
///
/// The destination to write data to.
/// The source to read data from.
/// The optional for the operation.
/// A representing the operation being performed.
public static ValueTask WriteAsync(this Stream stream, ReadOnlyMemory buffer, CancellationToken cancellationToken = default)
{
if (cancellationToken.IsCancellationRequested)
{
return new ValueTask(Task.FromCanceled(cancellationToken));
}
if (MemoryMarshal.TryGetArray(buffer, out ArraySegment segment))
{
return new ValueTask(stream.WriteAsync(segment.Array!, segment.Offset, segment.Count, cancellationToken));
}
// Local function, same idea as above
static async Task WriteAsyncFallback(Stream stream, ReadOnlyMemory buffer, CancellationToken cancellationToken)
{
byte[] rent = ArrayPool.Shared.Rent(buffer.Length);
try
{
buffer.Span.CopyTo(rent);
await stream.WriteAsync(rent, 0, buffer.Length, cancellationToken);
}
finally
{
ArrayPool.Shared.Return(rent);
}
}
return new ValueTask(WriteAsyncFallback(stream, buffer, cancellationToken));
}
///
/// Reads a sequence of bytes from a given instance.
///
/// The source to read data from.
/// The target to write data to.
/// The number of bytes that have been read.
public static int Read(this Stream stream, Span buffer)
{
byte[] rent = ArrayPool.Shared.Rent(buffer.Length);
try
{
int bytesRead = stream.Read(rent, 0, buffer.Length);
if (bytesRead > 0)
{
rent.AsSpan(0, bytesRead).CopyTo(buffer);
}
return bytesRead;
}
finally
{
ArrayPool.Shared.Return(rent);
}
}
///
/// Writes a sequence of bytes to a given instance.
///
/// The destination to write data to.
/// The source to read data from.
public static void Write(this Stream stream, ReadOnlySpan buffer)
{
byte[] rent = ArrayPool.Shared.Rent(buffer.Length);
try
{
buffer.CopyTo(rent);
stream.Write(rent, 0, buffer.Length);
}
finally
{
ArrayPool.Shared.Return(rent);
}
}
#endif
///
/// Reads a value of a specified type from a source instance.
///
/// The type of value to read.
/// The source instance to read from.
/// The value read from .
/// Thrown if reaches the end.
#if NETSTANDARD2_1_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
public static T Read(this Stream stream)
where T : unmanaged
{
#if NETSTANDARD2_1_OR_GREATER
T result = default;
int length = Unsafe.SizeOf();
unsafe
{
if (stream.Read(new Span(&result, length)) != length)
{
ThrowInvalidOperationExceptionForEndOfStream();
}
}
return result;
#else
int length = Unsafe.SizeOf();
byte[] buffer = ArrayPool.Shared.Rent(length);
try
{
if (stream.Read(buffer, 0, length) != length)
{
ThrowInvalidOperationExceptionForEndOfStream();
}
return Unsafe.ReadUnaligned(ref buffer[0]);
}
finally
{
ArrayPool.Shared.Return(buffer);
}
#endif
}
///
/// Writes a value of a specified type into a target instance.
///
/// The type of value to write.
/// The target instance to write to.
/// The input value to write to .
#if NETSTANDARD2_1_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
public static void Write(this Stream stream, in T value)
where T : unmanaged
{
#if NETSTANDARD2_1_OR_GREATER
ref T r0 = ref Unsafe.AsRef(value);
ref byte r1 = ref Unsafe.As(ref r0);
int length = Unsafe.SizeOf();
ReadOnlySpan span = MemoryMarshal.CreateReadOnlySpan(ref r1, length);
stream.Write(span);
#else
int length = Unsafe.SizeOf();
byte[] buffer = ArrayPool.Shared.Rent(length);
try
{
Unsafe.WriteUnaligned(ref buffer[0], value);
stream.Write(buffer, 0, length);
}
finally
{
ArrayPool.Shared.Return(buffer);
}
#endif
}
///
/// Throws an when fails.
///
private static void ThrowInvalidOperationExceptionForEndOfStream()
{
throw new InvalidOperationException("The stream didn't contain enough data to read the requested item.");
}
}