425 lines
16 KiB
C#
425 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
|
|
namespace io.github.hatayama.uLoopMCP
|
|
{
|
|
/// <summary>
|
|
/// Reassembles fragmented TCP messages using Content-Length framing.
|
|
/// Handles partial frames and maintains state across multiple data chunks.
|
|
/// </summary>
|
|
public class MessageReassembler : IDisposable
|
|
{
|
|
private readonly DynamicBufferManager _bufferManager;
|
|
private byte[] _assemblyBuffer;
|
|
private int _currentDataLength = 0;
|
|
private bool _disposed = false;
|
|
|
|
// Frame parsing state
|
|
private int _expectedContentLength = -1;
|
|
private int _headerLength = -1;
|
|
private bool _headerParsed = false;
|
|
|
|
// Statistics
|
|
private int _totalMessagesReassembled = 0;
|
|
private int _totalDataChunksProcessed = 0;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the MessageReassembler.
|
|
/// </summary>
|
|
/// <param name="bufferManager">The buffer manager to use for memory management</param>
|
|
public MessageReassembler(DynamicBufferManager bufferManager = null)
|
|
{
|
|
this._bufferManager = bufferManager ?? new DynamicBufferManager();
|
|
this._assemblyBuffer = this._bufferManager.GetBuffer(BufferConfig.INITIAL_BUFFER_SIZE);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets whether there is incomplete data waiting for more chunks.
|
|
/// </summary>
|
|
public bool HasIncompleteData => _currentDataLength > 0;
|
|
|
|
/// <summary>
|
|
/// Gets the current amount of data in the assembly buffer.
|
|
/// </summary>
|
|
public int CurrentDataLength => _currentDataLength;
|
|
|
|
/// <summary>
|
|
/// Gets whether the header has been parsed and we're waiting for content.
|
|
/// </summary>
|
|
public bool IsWaitingForContent => _headerParsed && _expectedContentLength > 0;
|
|
|
|
/// <summary>
|
|
/// Adds new data to the reassembly buffer.
|
|
/// </summary>
|
|
/// <param name="data">The data chunk to add</param>
|
|
/// <param name="length">The length of valid data in the chunk</param>
|
|
public void AddData(byte[] data, int length)
|
|
{
|
|
if (_disposed)
|
|
{
|
|
throw new ObjectDisposedException(nameof(MessageReassembler));
|
|
}
|
|
|
|
if (data == null || length <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (length > data.Length)
|
|
{
|
|
throw new ArgumentException("Length cannot exceed data array size", nameof(length));
|
|
}
|
|
|
|
_totalDataChunksProcessed++;
|
|
|
|
// Ensure we have enough space in the assembly buffer
|
|
int requiredSize = _currentDataLength + length;
|
|
if (requiredSize > _assemblyBuffer.Length)
|
|
{
|
|
_bufferManager.ResizeBuffer(ref _assemblyBuffer, _currentDataLength, requiredSize);
|
|
}
|
|
|
|
// Copy new data to assembly buffer
|
|
Array.Copy(data, 0, _assemblyBuffer, _currentDataLength, length);
|
|
_currentDataLength += length;
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to extract complete messages from the assembly buffer.
|
|
/// </summary>
|
|
/// <returns>Array of complete JSON messages, or empty array if none available</returns>
|
|
public string[] ExtractCompleteMessages()
|
|
{
|
|
if (_disposed)
|
|
{
|
|
throw new ObjectDisposedException(nameof(MessageReassembler));
|
|
}
|
|
|
|
var completeMessages = new List<string>();
|
|
|
|
while (_currentDataLength > 0)
|
|
{
|
|
string extractedMessage = TryExtractSingleMessage();
|
|
if (extractedMessage == null)
|
|
{
|
|
// No complete message available
|
|
break;
|
|
}
|
|
|
|
completeMessages.Add(extractedMessage);
|
|
_totalMessagesReassembled++;
|
|
}
|
|
|
|
return completeMessages.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to extract a single complete message from the buffer.
|
|
/// </summary>
|
|
/// <returns>The extracted JSON message, or null if not complete</returns>
|
|
private string TryExtractSingleMessage()
|
|
{
|
|
// If we haven't parsed the header yet, try to parse it
|
|
if (!_headerParsed)
|
|
{
|
|
if (!TryParseHeader())
|
|
{
|
|
// Header not complete yet
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Early detection of abnormal states - throw exceptions immediately
|
|
if (_expectedContentLength < 0)
|
|
{
|
|
throw new InvalidOperationException($"Invalid Content-Length value: {_expectedContentLength}. Message framing is corrupted.");
|
|
}
|
|
|
|
if (_headerLength < 0)
|
|
{
|
|
throw new InvalidOperationException($"Invalid header length: {_headerLength}. Message framing is corrupted.");
|
|
}
|
|
|
|
if (_expectedContentLength > BufferConfig.MAX_MESSAGE_SIZE)
|
|
{
|
|
throw new InvalidOperationException($"Content-Length {_expectedContentLength} exceeds maximum message size {BufferConfig.MAX_MESSAGE_SIZE}.");
|
|
}
|
|
|
|
// Check if we have the complete message
|
|
int expectedTotalLength = _headerLength + _expectedContentLength;
|
|
if (_currentDataLength < expectedTotalLength)
|
|
{
|
|
// Message not complete yet
|
|
return null;
|
|
}
|
|
|
|
// Validate buffer bounds before extraction
|
|
if (_headerLength + _expectedContentLength > _currentDataLength)
|
|
{
|
|
throw new InvalidOperationException($"Buffer underflow: trying to read {_expectedContentLength} bytes at offset {_headerLength}, but only {_currentDataLength} bytes available.");
|
|
}
|
|
|
|
// Extract the JSON content with proper error handling
|
|
string jsonContent;
|
|
try
|
|
{
|
|
jsonContent = Encoding.UTF8.GetString(_assemblyBuffer, _headerLength, _expectedContentLength);
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
throw new InvalidOperationException($"Invalid UTF-8 sequence detected in message content at offset {_headerLength}, length {_expectedContentLength}: {ex.Message}", ex);
|
|
}
|
|
|
|
// Log before resetting state
|
|
|
|
// Remove the processed message from the buffer
|
|
RemoveProcessedData(expectedTotalLength);
|
|
|
|
// Reset parsing state for next message
|
|
ResetParsingState();
|
|
|
|
return jsonContent;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to parse the Content-Length header from the current buffer.
|
|
/// </summary>
|
|
/// <returns>True if header was successfully parsed, false otherwise</returns>
|
|
private bool TryParseHeader()
|
|
{
|
|
var frameParser = new FrameParser();
|
|
|
|
bool parseResult = frameParser.TryParseFrame(_assemblyBuffer, _currentDataLength,
|
|
out _expectedContentLength, out _headerLength);
|
|
|
|
if (parseResult)
|
|
{
|
|
// Early validation of parsed values
|
|
if (_expectedContentLength < 0)
|
|
{
|
|
throw new InvalidOperationException($"FrameParser returned invalid Content-Length: {_expectedContentLength}");
|
|
}
|
|
|
|
if (_headerLength < 0)
|
|
{
|
|
throw new InvalidOperationException($"FrameParser returned invalid header length: {_headerLength}");
|
|
}
|
|
|
|
_headerParsed = true;
|
|
}
|
|
|
|
return parseResult;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes processed data from the beginning of the assembly buffer.
|
|
/// </summary>
|
|
/// <param name="bytesToRemove">Number of bytes to remove from the beginning</param>
|
|
private void RemoveProcessedData(int bytesToRemove)
|
|
{
|
|
if (bytesToRemove <= 0 || bytesToRemove > _currentDataLength)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Move remaining data to the beginning of the buffer
|
|
int remainingDataLength = _currentDataLength - bytesToRemove;
|
|
if (remainingDataLength > 0)
|
|
{
|
|
Array.Copy(_assemblyBuffer, bytesToRemove, _assemblyBuffer, 0, remainingDataLength);
|
|
}
|
|
|
|
_currentDataLength = remainingDataLength;
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets the parsing state for the next message.
|
|
/// </summary>
|
|
private void ResetParsingState()
|
|
{
|
|
_headerParsed = false;
|
|
_expectedContentLength = -1;
|
|
_headerLength = -1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all data from the assembly buffer.
|
|
/// Useful for error recovery or connection reset.
|
|
/// </summary>
|
|
public void Clear()
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_currentDataLength = 0;
|
|
ResetParsingState();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets statistics about the reassembler's performance.
|
|
/// </summary>
|
|
/// <returns>Statistics about message reassembly</returns>
|
|
public MessageReassemblerStats GetStats()
|
|
{
|
|
return new MessageReassemblerStats
|
|
{
|
|
TotalMessagesReassembled = _totalMessagesReassembled,
|
|
TotalDataChunksProcessed = _totalDataChunksProcessed,
|
|
CurrentBufferSize = _assemblyBuffer?.Length ?? 0,
|
|
CurrentDataLength = _currentDataLength,
|
|
HasIncompleteData = HasIncompleteData,
|
|
IsWaitingForContent = IsWaitingForContent,
|
|
ExpectedContentLength = _expectedContentLength,
|
|
HeaderLength = _headerLength
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates the current state of the reassembler.
|
|
/// </summary>
|
|
/// <returns>True if state is valid, false if cleanup is needed</returns>
|
|
public bool ValidateState()
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Check for reasonable buffer size - throw exception instead of silently clearing
|
|
if (_currentDataLength > BufferConfig.MAX_MESSAGE_SIZE)
|
|
{
|
|
throw new InvalidOperationException($"Buffer size {_currentDataLength} exceeds maximum message size {BufferConfig.MAX_MESSAGE_SIZE}. Data corruption detected.");
|
|
}
|
|
|
|
// Check for stale incomplete data - throw exception for corrupted state
|
|
if (_headerParsed && _expectedContentLength > 0)
|
|
{
|
|
int expectedTotalLength = _headerLength + _expectedContentLength;
|
|
if (expectedTotalLength > BufferConfig.MAX_MESSAGE_SIZE)
|
|
{
|
|
throw new InvalidOperationException($"Expected message size {expectedTotalLength} exceeds maximum {BufferConfig.MAX_MESSAGE_SIZE}. Message framing is corrupted.");
|
|
}
|
|
}
|
|
|
|
// Validate consistency of parsing state
|
|
if (_headerParsed && (_expectedContentLength < 0 || _headerLength < 0))
|
|
{
|
|
throw new InvalidOperationException($"Inconsistent parsing state: headerParsed={_headerParsed}, expectedContentLength={_expectedContentLength}, headerLength={_headerLength}");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a preview of the current buffer content for debugging.
|
|
/// </summary>
|
|
/// <param name="maxLength">Maximum length of preview</param>
|
|
/// <returns>String representation of buffer content</returns>
|
|
public string GetBufferPreview(int maxLength = 100)
|
|
{
|
|
if (_disposed || _currentDataLength == 0)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
// Safe UTF-8 decoding with proper error handling
|
|
int previewLength = Math.Min(_currentDataLength, maxLength);
|
|
string preview;
|
|
try
|
|
{
|
|
preview = Encoding.UTF8.GetString(_assemblyBuffer, 0, previewLength);
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
throw new InvalidOperationException($"Invalid UTF-8 sequence detected in buffer preview (length {previewLength}): {ex.Message}", ex);
|
|
}
|
|
|
|
if (_currentDataLength > maxLength)
|
|
{
|
|
preview += "...";
|
|
}
|
|
|
|
return preview;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Releases all resources used by the MessageReassembler.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
if (!_disposed)
|
|
{
|
|
if (_assemblyBuffer != null)
|
|
{
|
|
_bufferManager.ReturnBuffer(_assemblyBuffer);
|
|
_assemblyBuffer = null;
|
|
}
|
|
|
|
_bufferManager?.Dispose();
|
|
_disposed = true;
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Statistics about message reassembler performance and state.
|
|
/// </summary>
|
|
public class MessageReassemblerStats
|
|
{
|
|
/// <summary>
|
|
/// Total number of complete messages reassembled.
|
|
/// </summary>
|
|
public int TotalMessagesReassembled { get; set; }
|
|
|
|
/// <summary>
|
|
/// Total number of data chunks processed.
|
|
/// </summary>
|
|
public int TotalDataChunksProcessed { get; set; }
|
|
|
|
/// <summary>
|
|
/// Current size of the assembly buffer.
|
|
/// </summary>
|
|
public int CurrentBufferSize { get; set; }
|
|
|
|
/// <summary>
|
|
/// Current amount of data in the buffer.
|
|
/// </summary>
|
|
public int CurrentDataLength { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether there is incomplete data in the buffer.
|
|
/// </summary>
|
|
public bool HasIncompleteData { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether the reassembler is waiting for content after parsing header.
|
|
/// </summary>
|
|
public bool IsWaitingForContent { get; set; }
|
|
|
|
/// <summary>
|
|
/// Expected content length from parsed header.
|
|
/// </summary>
|
|
public int ExpectedContentLength { get; set; }
|
|
|
|
/// <summary>
|
|
/// Length of the parsed header.
|
|
/// </summary>
|
|
public int HeaderLength { get; set; }
|
|
|
|
/// <summary>
|
|
/// Returns a formatted string representation of the statistics.
|
|
/// </summary>
|
|
public override string ToString()
|
|
{
|
|
return $"MessageReassembler Stats: Messages={TotalMessagesReassembled}, " +
|
|
$"Chunks={TotalDataChunksProcessed}, BufferSize={CurrentDataLength}/{CurrentBufferSize}, " +
|
|
$"Incomplete={HasIncompleteData}, WaitingForContent={IsWaitingForContent}";
|
|
}
|
|
}
|
|
} |