using System; using System.Text; namespace io.github.hatayama.uLoopMCP { /// /// Parses Content-Length framed messages for JSON-RPC 2.0 communication. /// Handles HTTP-style headers with format: "Content-Length: \r\n\r\n" /// 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"; /// /// Attempts to parse a Content-Length framed message from the buffer. /// /// The buffer containing the data /// The length of valid data in the buffer /// The parsed content length, or -1 if parsing failed /// The total length of the header including separators, or -1 if parsing failed /// True if the header was successfully parsed, false otherwise 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; } } /// /// Checks if a complete frame is available in the buffer. /// /// The buffer containing the data /// The length of valid data in the buffer /// The expected content length /// The header length including separators /// True if the complete frame is available, false otherwise 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; } /// /// Extracts the JSON content from a complete frame. /// /// The buffer containing the complete frame /// The content length /// The header length including separators /// The JSON content as a string, or null if extraction failed 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); } /// /// Validates that the Content-Length value is within acceptable limits. /// /// The content length to validate /// True if the content length is valid, false otherwise public bool IsValidContentLength(int contentLength) { return contentLength >= 0 && contentLength <= BufferConfig.MAX_MESSAGE_SIZE; } /// /// Parses the Content-Length value from the header section. /// /// The header section string /// The parsed content length /// True if parsing was successful, false otherwise 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; } /// /// Finds the first occurrence of a byte sequence in a buffer. /// /// The buffer to search in /// The length of valid data in the buffer /// The byte sequence to find /// The index of the first occurrence, or -1 if not found 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; } } /// /// Configuration constants for buffer management and frame parsing. /// public static class BufferConfig { /// /// Initial buffer size for new connections. /// public const int INITIAL_BUFFER_SIZE = 4096; /// /// Maximum buffer size to prevent memory exhaustion. /// public const int MAX_BUFFER_SIZE = 1024 * 1024; // 1MB /// /// Maximum allowed message size. /// public const int MAX_MESSAGE_SIZE = 1024 * 1024; // 1MB /// /// Buffer growth factor when resizing is needed. /// public const int BUFFER_GROWTH_FACTOR = 2; /// /// Minimum buffer size to maintain. /// public const int MIN_BUFFER_SIZE = 1024; } }