Files

233 lines
9.3 KiB
C#

using System;
using System.Text;
namespace io.github.hatayama.uLoopMCP
{
/// <summary>
/// Parses Content-Length framed messages for JSON-RPC 2.0 communication.
/// Handles HTTP-style headers with format: "Content-Length: <n>\r\n\r\n<json_content>"
/// </summary>
public class FrameParser
{
private const string CONTENT_LENGTH_HEADER = "Content-Length:";
private const string HEADER_SEPARATOR = "\r\n\r\n";
private const string LINE_SEPARATOR = "\r\n";
/// <summary>
/// Attempts to parse a Content-Length framed message from the buffer.
/// </summary>
/// <param name="buffer">The buffer containing the data</param>
/// <param name="length">The length of valid data in the buffer</param>
/// <param name="contentLength">The parsed content length, or -1 if parsing failed</param>
/// <param name="headerLength">The total length of the header including separators, or -1 if parsing failed</param>
/// <returns>True if the header was successfully parsed, false otherwise</returns>
public bool TryParseFrame(byte[] buffer, int length, out int contentLength, out int headerLength)
{
contentLength = -1;
headerLength = -1;
if (buffer == null || length <= 0)
{
return false;
}
// Find the header separator without converting entire buffer to string
byte[] separatorBytes = Encoding.UTF8.GetBytes(HEADER_SEPARATOR);
int separatorIndex = FindByteSequence(buffer, length, separatorBytes);
if (separatorIndex == -1)
{
// Header not complete yet
return false;
}
// Extract header section only - convert only the header part to string
string headerSection = Encoding.UTF8.GetString(buffer, 0, separatorIndex);
int tempHeaderLength = separatorIndex + separatorBytes.Length;
// Parse Content-Length from header
bool parseResult = TryParseContentLength(headerSection, out contentLength);
// Only set headerLength if parsing was successful
if (parseResult)
{
headerLength = tempHeaderLength;
return true;
}
else
{
// Reset values on failure
contentLength = -1;
headerLength = -1;
return false;
}
}
/// <summary>
/// Checks if a complete frame is available in the buffer.
/// </summary>
/// <param name="buffer">The buffer containing the data</param>
/// <param name="length">The length of valid data in the buffer</param>
/// <param name="contentLength">The expected content length</param>
/// <param name="headerLength">The header length including separators</param>
/// <returns>True if the complete frame is available, false otherwise</returns>
public bool IsCompleteFrame(byte[] buffer, int length, int contentLength, int headerLength)
{
if (buffer == null || length <= 0 || contentLength < 0 || headerLength < 0)
{
return false;
}
int expectedTotalLength = headerLength + contentLength;
return length >= expectedTotalLength;
}
/// <summary>
/// Extracts the JSON content from a complete frame.
/// </summary>
/// <param name="buffer">The buffer containing the complete frame</param>
/// <param name="contentLength">The content length</param>
/// <param name="headerLength">The header length including separators</param>
/// <returns>The JSON content as a string, or null if extraction failed</returns>
public string ExtractJsonContent(byte[] buffer, int contentLength, int headerLength)
{
if (buffer == null || contentLength <= 0 || headerLength < 0)
{
return null;
}
// Extract JSON content after the header - let encoding exceptions bubble up
return Encoding.UTF8.GetString(buffer, headerLength, contentLength);
}
/// <summary>
/// Validates that the Content-Length value is within acceptable limits.
/// </summary>
/// <param name="contentLength">The content length to validate</param>
/// <returns>True if the content length is valid, false otherwise</returns>
public bool IsValidContentLength(int contentLength)
{
return contentLength >= 0 && contentLength <= BufferConfig.MAX_MESSAGE_SIZE;
}
/// <summary>
/// Parses the Content-Length value from the header section.
/// </summary>
/// <param name="headerSection">The header section string</param>
/// <param name="contentLength">The parsed content length</param>
/// <returns>True if parsing was successful, false otherwise</returns>
private bool TryParseContentLength(string headerSection, out int contentLength)
{
contentLength = -1;
if (string.IsNullOrEmpty(headerSection))
{
return false;
}
// Split header into lines
string[] lines = headerSection.Split(new[] { LINE_SEPARATOR }, StringSplitOptions.RemoveEmptyEntries);
foreach (string line in lines)
{
string trimmedLine = line.Trim();
// Check if this line contains Content-Length header
if (trimmedLine.StartsWith(CONTENT_LENGTH_HEADER, StringComparison.OrdinalIgnoreCase))
{
// Extract the value after the colon
int colonIndex = trimmedLine.IndexOf(':');
if (colonIndex == -1 || colonIndex >= trimmedLine.Length - 1)
{
continue;
}
string valueString = trimmedLine.Substring(colonIndex + 1).Trim();
// Try to parse the integer value
if (int.TryParse(valueString, out int parsedValue))
{
if (IsValidContentLength(parsedValue))
{
contentLength = parsedValue;
return true;
}
else
{
// Return false but don't set headerLength to -1 in calling method
contentLength = -1;
return false;
}
}
else
{
// Return false but don't set headerLength to -1 in calling method
contentLength = -1;
return false;
}
}
}
return false;
}
/// <summary>
/// Finds the first occurrence of a byte sequence in a buffer.
/// </summary>
/// <param name="buffer">The buffer to search in</param>
/// <param name="bufferLength">The length of valid data in the buffer</param>
/// <param name="sequence">The byte sequence to find</param>
/// <returns>The index of the first occurrence, or -1 if not found</returns>
private static int FindByteSequence(byte[] buffer, int bufferLength, byte[] sequence)
{
if (buffer == null || sequence == null || sequence.Length == 0 || bufferLength < sequence.Length)
return -1;
for (int i = 0; i <= bufferLength - sequence.Length; i++)
{
bool found = true;
for (int j = 0; j < sequence.Length; j++)
{
if (buffer[i + j] != sequence[j])
{
found = false;
break;
}
}
if (found)
return i;
}
return -1;
}
}
/// <summary>
/// Configuration constants for buffer management and frame parsing.
/// </summary>
public static class BufferConfig
{
/// <summary>
/// Initial buffer size for new connections.
/// </summary>
public const int INITIAL_BUFFER_SIZE = 4096;
/// <summary>
/// Maximum buffer size to prevent memory exhaustion.
/// </summary>
public const int MAX_BUFFER_SIZE = 1024 * 1024; // 1MB
/// <summary>
/// Maximum allowed message size.
/// </summary>
public const int MAX_MESSAGE_SIZE = 1024 * 1024; // 1MB
/// <summary>
/// Buffer growth factor when resizing is needed.
/// </summary>
public const int BUFFER_GROWTH_FACTOR = 2;
/// <summary>
/// Minimum buffer size to maintain.
/// </summary>
public const int MIN_BUFFER_SIZE = 1024;
}
}