aspnetcore/src/Microsoft.AspNetCore.Socket.../ServerSentEventsMessagePars...

194 lines
7.0 KiB
C#

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Sequences;
using System.IO.Pipelines;
using System.Runtime.CompilerServices;
using System.Text;
namespace Microsoft.AspNetCore.Sockets.Internal.Formatters
{
public class ServerSentEventsMessageParser
{
private const byte ByteCR = (byte)'\r';
private const byte ByteLF = (byte)'\n';
private const byte ByteColon = (byte)':';
private static byte[] _dataPrefix = Encoding.UTF8.GetBytes("data: ");
private static byte[] _sseLineEnding = Encoding.UTF8.GetBytes("\r\n");
private static byte[] _newLine = Encoding.UTF8.GetBytes(Environment.NewLine);
private InternalParseState _internalParserState = InternalParseState.ReadMessagePayload;
private List<byte[]> _data = new List<byte[]>();
public ParseResult ParseMessage(ReadOnlyBuffer<byte> buffer, out SequencePosition consumed, out SequencePosition examined, out byte[] message)
{
consumed = buffer.Start;
examined = buffer.End;
message = null;
var start = consumed;
var end = examined;
while (buffer.Length > 0)
{
if (!(buffer.PositionOf(ByteLF) is SequencePosition lineEnd))
{
// For the case of data: Foo\r\n\r\<Anytine except \n>
if (_internalParserState == InternalParseState.ReadEndOfMessage)
{
if (ConvertBufferToSpan(buffer.Slice(start, buffer.End)).Length > 1)
{
throw new FormatException("Expected a \\r\\n frame ending");
}
}
// Partial message. We need to read more.
return ParseResult.Incomplete;
}
lineEnd = buffer.GetPosition(lineEnd, 1);
var line = ConvertBufferToSpan(buffer.Slice(start, lineEnd));
buffer = buffer.Slice(line.Length);
if (line.Length <= 1)
{
throw new FormatException("There was an error in the frame format");
}
// Skip comments
if (line[0] == ByteColon)
{
start = lineEnd;
consumed = lineEnd;
continue;
}
if (IsMessageEnd(line))
{
_internalParserState = InternalParseState.ReadEndOfMessage;
}
// To ensure that the \n was preceded by a \r
// since messages can't contain \n.
// data: foo\n\bar should be encoded as
// data: foo\r\n
// data: bar\r\n
else if (line[line.Length - _sseLineEnding.Length] != ByteCR)
{
throw new FormatException("Unexpected '\n' in message. A '\n' character can only be used as part of the newline sequence '\r\n'");
}
else
{
EnsureStartsWithDataPrefix(line);
}
var payload = Array.Empty<byte>();
switch (_internalParserState)
{
case InternalParseState.ReadMessagePayload:
EnsureStartsWithDataPrefix(line);
// Slice away the 'data: '
var payloadLength = line.Length - (_dataPrefix.Length + _sseLineEnding.Length);
var newData = line.Slice(_dataPrefix.Length, payloadLength).ToArray();
_data.Add(newData);
start = lineEnd;
consumed = lineEnd;
break;
case InternalParseState.ReadEndOfMessage:
if (_data.Count == 1)
{
payload = _data[0];
}
else if (_data.Count > 1)
{
// Find the final size of the payload
var payloadSize = 0;
foreach (var dataLine in _data)
{
payloadSize += dataLine.Length;
}
payloadSize += _newLine.Length * _data.Count;
// Allocate space in the payload buffer for the data and the new lines.
// Subtract newLine length because we don't want a trailing newline.
payload = new byte[payloadSize - _newLine.Length];
var offset = 0;
foreach (var dataLine in _data)
{
dataLine.CopyTo(payload, offset);
offset += dataLine.Length;
if (offset < payload.Length)
{
_newLine.CopyTo(payload, offset);
offset += _newLine.Length;
}
}
}
message = payload;
consumed = lineEnd;
examined = consumed;
return ParseResult.Completed;
}
if (buffer.Length > 0 && buffer.First.Span[0] == ByteCR)
{
_internalParserState = InternalParseState.ReadEndOfMessage;
}
}
return ParseResult.Incomplete;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ReadOnlySpan<byte> ConvertBufferToSpan(ReadOnlyBuffer<byte> buffer)
{
if (buffer.IsSingleSegment)
{
return buffer.First.Span;
}
return buffer.ToArray();
}
public void Reset()
{
_internalParserState = InternalParseState.ReadMessagePayload;
_data.Clear();
}
private void EnsureStartsWithDataPrefix(ReadOnlySpan<byte> line)
{
if (!line.StartsWith(_dataPrefix))
{
throw new FormatException("Expected the message prefix 'data: '");
}
}
private bool IsMessageEnd(ReadOnlySpan<byte> line)
{
return line.Length == _sseLineEnding.Length && line.SequenceEqual(_sseLineEnding);
}
public enum ParseResult
{
Completed,
Incomplete,
}
private enum InternalParseState
{
ReadMessagePayload,
ReadEndOfMessage,
Error
}
}
}