using System.Buffers; namespace Iceshrimp.Backend.Core.Extensions; public static class StreamExtensions { public static async Task CopyToAsync( this Stream source, Stream destination, long? maxLength, CancellationToken cancellationToken ) { var buffer = ArrayPool.Shared.Rent(81920); try { int bytesRead; var totalBytesRead = 0L; while ((maxLength == null || totalBytesRead <= maxLength) && (bytesRead = await DoReadAsync()) != 0) { totalBytesRead += bytesRead; await destination.WriteAsync(new ReadOnlyMemory(buffer, 0, bytesRead), cancellationToken); } } finally { ArrayPool.Shared.Return(buffer); } return; ValueTask DoReadAsync() => source.ReadAsync(new Memory(buffer), cancellationToken); } /// /// We can't trust the Content-Length header, and it might be null. /// This makes sure that we only ever read up to maxLength into memory. /// /// The response content stream /// The maximum length to buffer (null = unlimited) /// The content length, if known /// A CancellationToken, if applicable /// Either a buffered MemoryStream, or Stream.Null public static async Task GetSafeStreamOrNullAsync( this Stream stream, long? maxLength, long? contentLength, CancellationToken token = default ) { if (maxLength is 0) return Stream.Null; if (contentLength > maxLength) return Stream.Null; MemoryStream buf = new(); if (contentLength < maxLength) maxLength = contentLength.Value; await stream.CopyToAsync(buf, maxLength, token); if (maxLength == null || buf.Length <= maxLength) { buf.Seek(0, SeekOrigin.Begin); return buf; } await buf.DisposeAsync(); return Stream.Null; } }