File: Rpc\NativeRpcFraming.cs
Web Access
Project: src\src\Aspire.Hosting.Native.Server\Aspire.Hosting.Native.Server.csproj (Aspire.Hosting.Native.Server)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Buffers.Text;
using Aspire.Hosting.Native.Server;
 
namespace Aspire.Hosting.Native.Rpc;
 
/// <summary>Reads bounded UTF-8 JSON-RPC frames without allocating an untrusted declared payload length.</summary>
internal sealed class NativeRpcFraming(Stream stream, int maximumRequestBytes)
{
    public NativeRpcFraming(Stream stream) : this(stream, NativeServerOptions.DefaultMaximumRequestBytes)
    {
    }
    private readonly byte[] _buffer = new byte[8192];
    private readonly byte[] _header = new byte[NativeRpcFrameProtocol.MaximumHeaderBytes];
    private int _position;
    private int _length;
 
    public async Task<byte[]?> ReadAsync(CancellationToken cancellationToken)
    {
        var header = _header;
        var count = 0;
        while (true)
        {
            if (_position == _length)
            {
                _length = await stream.ReadAsync(_buffer, cancellationToken).ConfigureAwait(false);
                _position = 0;
                if (_length == 0)
                {
                    if (count == 0)
                    {
                        return null;
                    }
                    throw new EndOfStreamException("The JSON-RPC frame header is incomplete.");
                }
            }
            var available = _buffer.AsSpan(_position, _length - _position);
            var newline = available.IndexOf((byte)'\n');
            var copied = newline < 0 ? available.Length : newline + 1;
            if (copied > header.Length - count)
            {
                throw new InvalidDataException("The JSON-RPC frame header exceeds the limit.");
            }
            var chunk = available[..copied];
            if (chunk.IndexOfAnyInRange((byte)128, byte.MaxValue) >= 0)
            {
                throw new InvalidDataException("The JSON-RPC frame header must be ASCII.");
            }
            chunk.CopyTo(header.AsSpan(count));
            count += copied;
            _position += copied;
            if (count >= 4 && header.AsSpan(count - 4, 4).SequenceEqual("\r\n\r\n"u8))
            {
                break;
            }
        }
 
        var contentLength = NativeRpcFrameProtocol.ReadContentLength(header.AsSpan(0, count - 4), maximumRequestBytes);
        var body = new byte[contentLength];
        for (var index = 0; index < body.Length;)
        {
            if (_position < _length)
            {
                var copied = Math.Min(body.Length - index, _length - _position);
                _buffer.AsMemory(_position, copied).CopyTo(body.AsMemory(index));
                _position += copied;
                index += copied;
            }
            else
            {
                await stream.ReadExactlyAsync(body.AsMemory(index), cancellationToken).ConfigureAwait(false);
                break;
            }
        }
 
        return body;
    }
 
    public static async Task WriteAsync(Stream stream, byte[] payload, CancellationToken cancellationToken)
    {
        var header = ArrayPool<byte>.Shared.Rent(32);
        try
        {
            // Sixteen prefix bytes, at most ten Int32 digits and four delimiter
            // bytes fit in this lease. Keep it until both writes and flush complete.
            "Content-Length: "u8.CopyTo(header);
            Utf8Formatter.TryFormat(payload.Length, header.AsSpan(16), out var digits);
            "\r\n\r\n"u8.CopyTo(header.AsSpan(16 + digits));
            await stream.WriteAsync(header.AsMemory(0, 20 + digits), cancellationToken).ConfigureAwait(false);
            await stream.WriteAsync(payload, cancellationToken).ConfigureAwait(false);
            await stream.FlushAsync(cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(header);
        }
    }
}