mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-15 09:35:37 -05:00
* Move unused forked code to external directory * Fix SLN build errors * Add back resource provider core since it's referenced by main resource provider project * Update PackageProjects step of pipeline
259 lines
8.7 KiB
C#
259 lines
8.7 KiB
C#
//
|
|
// Copyright (c) Microsoft. All rights reserved.
|
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
//
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.SqlTools.Hosting.Utility;
|
|
using Microsoft.SqlTools.Hosting.v2;
|
|
|
|
namespace Microsoft.SqlTools.Hosting.Protocol
|
|
{
|
|
public class MessageReader
|
|
{
|
|
#region Private Fields
|
|
|
|
public const int DefaultBufferSize = 8192;
|
|
private const double BufferResizeTrigger = 0.25;
|
|
|
|
private const int CR = 0x0D;
|
|
private const int LF = 0x0A;
|
|
private static readonly string[] NewLineDelimiters = { Environment.NewLine };
|
|
|
|
private readonly Stream inputStream;
|
|
|
|
private bool needsMoreData = true;
|
|
private int readOffset;
|
|
private int bufferEndOffset;
|
|
|
|
private byte[] messageBuffer;
|
|
|
|
private int expectedContentLength;
|
|
private Dictionary<string, string> messageHeaders;
|
|
|
|
internal enum ReadState
|
|
{
|
|
Headers,
|
|
Content
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Constructors
|
|
|
|
public MessageReader(Stream inputStream, Encoding messageEncoding = null)
|
|
{
|
|
Validate.IsNotNull("streamReader", inputStream);
|
|
|
|
this.inputStream = inputStream;
|
|
MessageEncoding = messageEncoding ?? Encoding.UTF8;
|
|
|
|
messageBuffer = new byte[DefaultBufferSize];
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Testable Properties
|
|
|
|
internal byte[] MessageBuffer
|
|
{
|
|
get => messageBuffer;
|
|
set => messageBuffer = value;
|
|
}
|
|
|
|
internal ReadState CurrentState { get; private set; }
|
|
|
|
internal Encoding MessageEncoding { get; private set; }
|
|
|
|
#endregion
|
|
|
|
#region Public Methods
|
|
|
|
public virtual async Task<Message> ReadMessage()
|
|
{
|
|
string messageContent = null;
|
|
|
|
// Do we need to read more data or can we process the existing buffer?
|
|
while (!needsMoreData || await ReadNextChunk())
|
|
{
|
|
// Clear the flag since we should have what we need now
|
|
needsMoreData = false;
|
|
|
|
// Do we need to look for message headers?
|
|
if (CurrentState == ReadState.Headers && !TryReadMessageHeaders())
|
|
{
|
|
// If we don't have enough data to read headers yet, keep reading
|
|
needsMoreData = true;
|
|
continue;
|
|
}
|
|
|
|
// Do we need to look for message content?
|
|
if (CurrentState == ReadState.Content && !TryReadMessageContent(out messageContent))
|
|
{
|
|
// If we don't have enough data yet to construct the content, keep reading
|
|
needsMoreData = true;
|
|
continue;
|
|
}
|
|
|
|
// We've read a message now, break out of the loop
|
|
break;
|
|
}
|
|
|
|
// Now that we have a message, reset the buffer's state
|
|
ShiftBufferBytesAndShrink(readOffset);
|
|
|
|
// Return the parsed message
|
|
return Message.Deserialize(messageContent);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Private Methods
|
|
|
|
private async Task<bool> ReadNextChunk()
|
|
{
|
|
// Do we need to resize the buffer? See if less than 1/4 of the space is left.
|
|
if ((double)(messageBuffer.Length - bufferEndOffset) / messageBuffer.Length < BufferResizeTrigger)
|
|
{
|
|
// Double the size of the buffer
|
|
Array.Resize(ref messageBuffer, messageBuffer.Length * 2);
|
|
}
|
|
|
|
// Read the next chunk into the message buffer
|
|
int readLength =
|
|
await inputStream.ReadAsync(messageBuffer, bufferEndOffset, messageBuffer.Length - bufferEndOffset);
|
|
|
|
bufferEndOffset += readLength;
|
|
|
|
if (readLength == 0)
|
|
{
|
|
// If ReadAsync returns 0 then it means that the stream was
|
|
// closed unexpectedly (usually due to the client application
|
|
// ending suddenly). For now, just terminate the language
|
|
// server immediately.
|
|
throw new EndOfStreamException(SR.HostingUnexpectedEndOfStream);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool TryReadMessageHeaders()
|
|
{
|
|
int scanOffset = readOffset;
|
|
|
|
// Scan for the final double-newline that marks the end of the header lines
|
|
while (scanOffset + 3 < bufferEndOffset &&
|
|
(messageBuffer[scanOffset] != CR ||
|
|
messageBuffer[scanOffset + 1] != LF ||
|
|
messageBuffer[scanOffset + 2] != CR ||
|
|
messageBuffer[scanOffset + 3] != LF))
|
|
{
|
|
scanOffset++;
|
|
}
|
|
|
|
// Make sure we haven't reached the end of the buffer without finding a separator (e.g CRLFCRLF)
|
|
if (scanOffset + 3 >= bufferEndOffset)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Convert the header block into a array of lines
|
|
var headers = Encoding.ASCII.GetString(messageBuffer, readOffset, scanOffset)
|
|
.Split(NewLineDelimiters, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
try
|
|
{
|
|
// Read each header and store it in the dictionary
|
|
messageHeaders = new Dictionary<string, string>();
|
|
foreach (var header in headers)
|
|
{
|
|
int currentLength = header.IndexOf(':');
|
|
if (currentLength == -1)
|
|
{
|
|
throw new ArgumentException(SR.HostingHeaderMissingColon);
|
|
}
|
|
|
|
var key = header.Substring(0, currentLength);
|
|
var value = header.Substring(currentLength + 1).Trim();
|
|
messageHeaders[key] = value;
|
|
}
|
|
|
|
// Parse out the content length as an int
|
|
string contentLengthString;
|
|
if (!messageHeaders.TryGetValue("Content-Length", out contentLengthString))
|
|
{
|
|
throw new MessageParseException("", SR.HostingHeaderMissingContentLengthHeader);
|
|
}
|
|
|
|
// Parse the content length to an integer
|
|
if (!int.TryParse(contentLengthString, out expectedContentLength))
|
|
{
|
|
throw new MessageParseException("", SR.HostingHeaderMissingContentLengthValue);
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// The content length was invalid or missing. Trash the buffer we've read
|
|
ShiftBufferBytesAndShrink(scanOffset + 4);
|
|
throw;
|
|
}
|
|
|
|
// Skip past the headers plus the newline characters
|
|
readOffset += scanOffset + 4;
|
|
|
|
// Done reading headers, now read content
|
|
CurrentState = ReadState.Content;
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool TryReadMessageContent(out string messageContent)
|
|
{
|
|
messageContent = null;
|
|
|
|
// Do we have enough bytes to reach the expected length?
|
|
if (bufferEndOffset - readOffset < expectedContentLength)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Convert the message contents to a string using the specified encoding
|
|
messageContent = MessageEncoding.GetString(messageBuffer, readOffset, expectedContentLength);
|
|
|
|
readOffset += expectedContentLength;
|
|
|
|
// Done reading content, now look for headers for the next message
|
|
CurrentState = ReadState.Headers;
|
|
|
|
return true;
|
|
}
|
|
|
|
private void ShiftBufferBytesAndShrink(int bytesToRemove)
|
|
{
|
|
// Create a new buffer that is shrunken by the number of bytes to remove
|
|
// Note: by using Max, we can guarantee a buffer of at least default buffer size
|
|
byte[] newBuffer = new byte[Math.Max(messageBuffer.Length - bytesToRemove, DefaultBufferSize)];
|
|
|
|
// If we need to do shifting, do the shifting
|
|
if (bytesToRemove <= messageBuffer.Length)
|
|
{
|
|
// Copy the existing buffer starting at the offset to remove
|
|
Buffer.BlockCopy(messageBuffer, bytesToRemove, newBuffer, 0, bufferEndOffset - bytesToRemove);
|
|
}
|
|
|
|
// Make the new buffer the message buffer
|
|
messageBuffer = newBuffer;
|
|
|
|
// Reset the read offset and the end offset
|
|
readOffset = 0;
|
|
bufferEndOffset -= bytesToRemove;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|