diff --git a/NEventSocket.Tests/Sockets/MessageParsingTests.cs b/NEventSocket.Tests/Sockets/MessageParsingTests.cs index 13cd8ed..3abcb0b 100644 --- a/NEventSocket.Tests/Sockets/MessageParsingTests.cs +++ b/NEventSocket.Tests/Sockets/MessageParsingTests.cs @@ -1,24 +1,41 @@ using System; +using System.Text; using System.Collections.Generic; using System.Reactive.Linq; using NEventSocket.FreeSwitch; +using NEventSocket.Logging; using NEventSocket.Sockets; using NEventSocket.Tests.Properties; using NEventSocket.Tests.TestSupport; using NEventSocket.Util; using Xunit; +using Microsoft.Extensions.Logging; + namespace NEventSocket.Tests.Sockets { public class MessageParsingTests { - [Theory, MemberData(nameof(ExampleMessages))] + public MessageParsingTests() + { + PreventThreadPoolStarvation.Init(); + Logger.Configure(LoggerFactory.Create(builder => + { + builder + .AddFilter("Microsoft", LogLevel.Warning) + .AddFilter("System", LogLevel.Warning) + .AddFilter("LoggingConsoleApp.Program", LogLevel.Debug) + .AddConsole(); + })); + } + +[Theory, MemberData(nameof(ExampleMessages))] public void it_should_parse_the_expected_messages_from_a_stream(int expectedMessageCount, string exampleInput) { int parsedMessageCount = 0; - - exampleInput.ToObservable() - .AggregateUntil(() => new Parser(), (builder, ch) => builder.Append(ch), builder => builder.Completed) + byte[] exampleByteInput = Encoding.UTF8.GetBytes(exampleInput); + exampleByteInput.ToObservable() + .AggregateUntil(() => new Parser(), (builder, b) => builder.Append(b), builder => builder.Completed) .Select(parser => parser.ExtractMessage()) .Subscribe(_ => parsedMessageCount++); @@ -31,14 +48,17 @@ public void it_should_parse_the_expected_messages_from_a_stream(int expectedMess [InlineData(TestMessages.ConnectEvent)] [InlineData(TestMessages.DisconnectEvent)] [InlineData(TestMessages.PlaybackComplete)] + [InlineData(TestMessages.DetectedSpeech)] + [InlineData(TestMessages.DetectedSpeechEnglish)] public void can_parse_test_messages(string input) { var parser = new Parser(); var rawInput = input.Replace("\r\n", "\n") + "\n\n"; + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); - foreach (char c in rawInput) + foreach (byte b in byteInput) { - parser.Append(c); + parser.Append(b); } Assert.True(parser.Completed); @@ -51,13 +71,17 @@ public void can_parse_test_messages(string input) [Theory] [InlineData(TestMessages.BackgroundJob)] [InlineData(TestMessages.CallState)] + [InlineData(TestMessages.DetectedSpeech)] + [InlineData(TestMessages.DetectedSpeechEnglish)] public void it_should_extract_the_body_from_a_message(string input) { var parser = new Parser(); var rawInput = input.Replace("\r\n", "\n") + "\n\n"; - foreach (char c in rawInput) + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); + + foreach (byte b in byteInput) { - parser.Append(c); + parser.Append(b); } Assert.True(parser.Completed); @@ -65,7 +89,7 @@ public void it_should_extract_the_body_from_a_message(string input) BasicMessage payload = parser.ExtractMessage(); Assert.Equal(ContentTypes.EventPlain, payload.ContentType); Assert.NotNull(payload.BodyText); - Assert.Equal(payload.ContentLength, payload.BodyText.Length); + Assert.Equal(payload.ContentLength, payload.BodyBytes.Length); Console.WriteLine(payload.ToString()); } @@ -73,13 +97,17 @@ public void it_should_extract_the_body_from_a_message(string input) [Theory] [InlineData(TestMessages.BackgroundJob, EventName.BackgroundJob)] [InlineData(TestMessages.CallState, EventName.ChannelCallstate)] + [InlineData(TestMessages.DetectedSpeech, EventName.DetectedSpeech)] + [InlineData(TestMessages.DetectedSpeechEnglish, EventName.DetectedSpeech)] public void it_should_parse_event_messages(string input, EventName eventName) { var parser = new Parser(); var rawInput = input.Replace("\r\n", "\n") + "\n\n"; - foreach (char c in rawInput) + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); + + foreach (byte b in byteInput) { - parser.Append(c); + parser.Append(b); } Assert.True(parser.Completed); @@ -91,16 +119,44 @@ public void it_should_parse_event_messages(string input, EventName eventName) Console.WriteLine(eventMessage.ToString()); } + [Theory] + [InlineData(TestMessages.DetectedSpeech, EventName.DetectedSpeech)] + [InlineData(TestMessages.DetectedSpeechEnglish, EventName.DetectedSpeech)] + public void it_should_parse_event_messages_and_extract_body_payload(string input, EventName eventName) + { + var parser = new Parser(); + var rawInput = input.Replace("\r\n", "\n") + "\n\n"; + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); + + foreach (byte b in byteInput) + { + parser.Append(b); + } + + Assert.True(parser.Completed); + + var eventMessage = new EventMessage(parser.ExtractMessage()); + Assert.NotNull(eventMessage); + Assert.Equal(eventName, eventMessage.EventName); + + var contentLength = int.Parse(eventMessage.Headers[HeaderNames.ContentLength]); + + Assert.Equal(contentLength, Encoding.UTF8.GetByteCount(eventMessage.BodyText)); + + Console.WriteLine(eventMessage.ToString()); + } + [Fact] public void it_should_parse_BackgroundJobResult_OK() { var input = TestMessages.BackgroundJob; var parser = new Parser(); var rawInput = input.Replace("\r\n", "\n") + "\n\n"; + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); - foreach (char c in rawInput) + foreach (byte b in byteInput) { - parser.Append(c); + parser.Append(b); } Assert.True(parser.Completed); @@ -118,10 +174,11 @@ public void it_should_parse_BackgroundJobResult_ERR() var input = TestMessages.BackgroundJobError; var parser = new Parser(); var rawInput = input.Replace("\r\n", "\n") + "\n\n"; + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); - foreach (char c in rawInput) + foreach (byte b in byteInput) { - parser.Append(c); + parser.Append(b); } Assert.True(parser.Completed); @@ -139,10 +196,11 @@ public void it_should_parse_Command_Reply_OK() { var parser = new Parser(); var rawInput = "Content-Type: command/reply\nReply-Text: +OK\n\n"; + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); - foreach (char c in rawInput) + foreach (byte b in byteInput) { - parser.Append(c); + parser.Append(b); } Assert.True(parser.Completed); @@ -159,10 +217,11 @@ public void it_should_parse_Command_Reply_ERR() { var parser = new Parser(); var rawInput = "Content-Type: command/reply\nReply-Text: -ERR Error\n\n"; + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); - foreach (char c in rawInput) + foreach (byte b in byteInput) { - parser.Append(c); + parser.Append(b); } Assert.True(parser.Completed); @@ -180,10 +239,11 @@ public void it_should_parse_Api_Response_OK() { var parser = new Parser(); var rawInput = "Content-Type: api/response\nContent-Length: 3\n\n+OK"; + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); - foreach (char c in rawInput) + foreach (byte b in byteInput) { - parser.Append(c); + parser.Append(b); } Assert.True(parser.Completed); @@ -200,10 +260,11 @@ public void it_should_parse_Api_Response_ERR() { var parser = new Parser(); var rawInput = "Content-Type: api/response\nContent-Length: 10\n\n-ERR Error"; + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); - foreach (char c in rawInput) + foreach (byte b in byteInput) { - parser.Append(c); + parser.Append(b); } Assert.True(parser.Completed); @@ -221,10 +282,11 @@ public void it_should_treat_Api_Response_ERR_no_reply_as_Success() { var parser = new Parser(); var rawInput = "Content-Type: api/response\nContent-Length: 13\n\n-ERR no reply"; + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); - foreach (char c in rawInput) + foreach (byte b in byteInput) { - parser.Append(c); + parser.Append(b); } Assert.True(parser.Completed); @@ -242,10 +304,11 @@ public void it_should_trim_new_lines_from__the_end_of_ApiResponse_Body_text() { var parser = new Parser(); var rawInput = "Content-Type: api/response\nContent-Length: 14\n\n-ERR no reply\n"; + byte[] byteInput = Encoding.UTF8.GetBytes(rawInput); - foreach (char c in rawInput) + foreach (byte b in byteInput) { - parser.Append(c); + parser.Append(b); } Assert.True(parser.Completed); @@ -269,9 +332,10 @@ public void Can_parse_example_sessions_to_completion(string input) } bool gotDisconnectNotice = false; + byte[] byteInput = Encoding.UTF8.GetBytes(input); - input.ToObservable() - .AggregateUntil(() => new Parser(), (builder, ch) => builder.Append(ch), builder => builder.Completed) + byteInput.ToObservable() + .AggregateUntil(() => new Parser(), (builder, b) => builder.Append(b), builder => builder.Completed) .Select(parser => parser.ExtractMessage()) .Subscribe( m => @@ -297,8 +361,9 @@ public void Can_parse_disconnect_notice() Disconnected, goodbye. See you at ClueCon! http://www.cluecon.com/ "; - msg.ToObservable() - .AggregateUntil(() => new Parser(), (builder, ch) => builder.Append(ch), builder => builder.Completed) + byte[] byteMsg = Encoding.UTF8.GetBytes(msg); + byteMsg.ToObservable() + .AggregateUntil(() => new Parser(), (builder, b) => builder.Append(b), builder => builder.Completed) .Select(parser => parser.ExtractMessage()) .Subscribe( Console.WriteLine); diff --git a/NEventSocket.Tests/TestSupport/TestMessages.cs b/NEventSocket.Tests/TestSupport/TestMessages.cs index 4d7981a..1c07f18 100644 --- a/NEventSocket.Tests/TestSupport/TestMessages.cs +++ b/NEventSocket.Tests/TestSupport/TestMessages.cs @@ -278,6 +278,155 @@ public class TestMessages Disconnected, goodbye. See you at ClueCon! http://www.cluecon.com/"; + public const string DetectedSpeech = @"Content-Length: 2471 +Content-Type: text/event-plain + +Event-Name: DETECTED_SPEECH +Core-UUID: ec530c4e-484d-473e-9bd0-073863f752eb +FreeSWITCH-Hostname: ser +FreeSWITCH-Switchname: ser +FreeSWITCH-IPv4: 192.168.1.104 +FreeSWITCH-IPv6: %3A%3A1 +Event-Date-Local: 2021-06-28%2023%3A20%3A38 +Event-Date-GMT: Mon,%2028%20Jun%202021%2021%3A20%3A38%20GMT +Event-Date-Timestamp: 1624915238513391 +Event-Calling-File: switch_ivr_async.c +Event-Calling-Function: speech_thread +Event-Calling-Line-Number: 4947 +Event-Sequence: 251969 +Speech-Type: detected-speech +ASR-Completion-Cause: 0 +Channel-State: CS_EXECUTE +Channel-Call-State: ACTIVE +Channel-State-Number: 4 +Channel-Name: sofia/internal/%2B491734064561%40172.16.50.128 +Unique-ID: f5228692-9715-4b6f-b54b-9e35be1a7335 +Call-Direction: inbound +Presence-Call-Direction: inbound +Channel-HIT-Dialplan: true +Channel-Presence-ID: %2B491734064561%40172.16.50.128 +Channel-Call-UUID: f5228692-9715-4b6f-b54b-9e35be1a7335 +Answer-State: answered +Channel-Read-Codec-Name: G722 +Channel-Read-Codec-Rate: 16000 +Channel-Read-Codec-Bit-Rate: 64000 +Channel-Write-Codec-Name: G722 +Channel-Write-Codec-Rate: 16000 +Channel-Write-Codec-Bit-Rate: 64000 +Caller-Direction: inbound +Caller-Logical-Direction: inbound +Caller-Username: %2B491734000000 +Caller-Dialplan: XML +Caller-Caller-ID-Name: %2B491734000000 +Caller-Caller-ID-Number: %2B491734000000 +Caller-Orig-Caller-ID-Name: %2B491734000000 +Caller-Orig-Caller-ID-Number: %2B491734000000 +Caller-Network-Addr: 10.1.10.100 +Caller-ANI: %2B491734000000 +Caller-Destination-Number: 493000000000000 +Caller-Unique-ID: f5228692-9715-4b6f-b54b-9e35be1a7335 +Caller-Source: mod_sofia +Caller-Context: public +Caller-Channel-Name: sofia/internal/%2B491734064561%40172.16.50.128 +Caller-Profile-Index: 1 +Caller-Profile-Created-Time: 1624915212053386 +Caller-Channel-Created-Time: 1624915212053386 +Caller-Channel-Answered-Time: 1624915222293356 +Caller-Channel-Progress-Time: 0 +Caller-Channel-Progress-Media-Time: 1624915216013390 +Caller-Channel-Hangup-Time: 0 +Caller-Channel-Transfer-Time: 0 +Caller-Channel-Resurrect-Time: 0 +Caller-Channel-Bridged-Time: 0 +Caller-Channel-Last-Hold: 0 +Caller-Channel-Hold-Accum: 0 +Caller-Screen-Bit: true +Caller-Privacy-Hide-Name: false +Caller-Privacy-Hide-Number: false +Content-Length: 261 + + + + + Verlängerung Störung Bestätigung + Verlängerung Störung Bestätigung + +"; + + public const string DetectedSpeechEnglish = @"Content-Length: 2465 +Content-Type: text/event-plain + +Event-Name: DETECTED_SPEECH +Core-UUID: ec530c4e-484d-473e-9bd0-073863f752eb +FreeSWITCH-Hostname: ser +FreeSWITCH-Switchname: ser +FreeSWITCH-IPv4: 192.168.1.104 +FreeSWITCH-IPv6: %3A%3A1 +Event-Date-Local: 2021-06-28%2023%3A20%3A38 +Event-Date-GMT: Mon,%2028%20Jun%202021%2021%3A20%3A38%20GMT +Event-Date-Timestamp: 1624915238513391 +Event-Calling-File: switch_ivr_async.c +Event-Calling-Function: speech_thread +Event-Calling-Line-Number: 4947 +Event-Sequence: 251969 +Speech-Type: detected-speech +ASR-Completion-Cause: 0 +Channel-State: CS_EXECUTE +Channel-Call-State: ACTIVE +Channel-State-Number: 4 +Channel-Name: sofia/internal/%2B491734064561%40172.16.50.128 +Unique-ID: f5228692-9715-4b6f-b54b-9e35be1a7335 +Call-Direction: inbound +Presence-Call-Direction: inbound +Channel-HIT-Dialplan: true +Channel-Presence-ID: %2B491734064561%40172.16.50.128 +Channel-Call-UUID: f5228692-9715-4b6f-b54b-9e35be1a7335 +Answer-State: answered +Channel-Read-Codec-Name: G722 +Channel-Read-Codec-Rate: 16000 +Channel-Read-Codec-Bit-Rate: 64000 +Channel-Write-Codec-Name: G722 +Channel-Write-Codec-Rate: 16000 +Channel-Write-Codec-Bit-Rate: 64000 +Caller-Direction: inbound +Caller-Logical-Direction: inbound +Caller-Username: %2B491734000000 +Caller-Dialplan: XML +Caller-Caller-ID-Name: %2B491734000000 +Caller-Caller-ID-Number: %2B491734000000 +Caller-Orig-Caller-ID-Name: %2B491734000000 +Caller-Orig-Caller-ID-Number: %2B491734000000 +Caller-Network-Addr: 10.1.10.100 +Caller-ANI: %2B491734000000 +Caller-Destination-Number: 493000000000000 +Caller-Unique-ID: f5228692-9715-4b6f-b54b-9e35be1a7335 +Caller-Source: mod_sofia +Caller-Context: public +Caller-Channel-Name: sofia/internal/%2B491734064561%40172.16.50.128 +Caller-Profile-Index: 1 +Caller-Profile-Created-Time: 1624915212053386 +Caller-Channel-Created-Time: 1624915212053386 +Caller-Channel-Answered-Time: 1624915222293356 +Caller-Channel-Progress-Time: 0 +Caller-Channel-Progress-Media-Time: 1624915216013390 +Caller-Channel-Hangup-Time: 0 +Caller-Channel-Transfer-Time: 0 +Caller-Channel-Resurrect-Time: 0 +Caller-Channel-Bridged-Time: 0 +Caller-Channel-Last-Hold: 0 +Caller-Channel-Hold-Accum: 0 +Caller-Screen-Bit: true +Caller-Privacy-Hide-Name: false +Caller-Privacy-Hide-Number: false +Content-Length: 255 + + + + + Extensions Problems Confirmation + Extensions Problems Confirmation + +"; public const string PlaybackComplete = @"Content-Length: 7209 Content-Type: text/event-plain diff --git a/NEventSocket/FreeSwitch/BasicMessage.cs b/NEventSocket/FreeSwitch/BasicMessage.cs index b985200..afa0e53 100644 --- a/NEventSocket/FreeSwitch/BasicMessage.cs +++ b/NEventSocket/FreeSwitch/BasicMessage.cs @@ -9,7 +9,7 @@ namespace NEventSocket.FreeSwitch using System; using System.Collections.Generic; using System.Linq; - + using System.Text; using NEventSocket.Util; using NEventSocket.Util.ObjectPooling; @@ -25,10 +25,11 @@ internal BasicMessage(IDictionary headers) Headers = new Dictionary(headers, StringComparer.OrdinalIgnoreCase); } - internal BasicMessage(IDictionary headers, string body) + internal BasicMessage(IDictionary headers, byte[] bodyBytes) { Headers = new Dictionary(headers, StringComparer.OrdinalIgnoreCase); - BodyText = body; + BodyText = Encoding.UTF8.GetString(bodyBytes); + BodyBytes = bodyBytes; } /// @@ -48,6 +49,8 @@ protected BasicMessage() /// public string BodyText { get; protected set; } + public byte[] BodyBytes { get; protected set; } + /// /// Gets the Content Type header. /// diff --git a/NEventSocket/FreeSwitch/EventMessage.cs b/NEventSocket/FreeSwitch/EventMessage.cs index 8ce0d0c..86d1b53 100644 --- a/NEventSocket/FreeSwitch/EventMessage.cs +++ b/NEventSocket/FreeSwitch/EventMessage.cs @@ -7,9 +7,10 @@ namespace NEventSocket.FreeSwitch { using System; + using System.Collections.Generic; using System.Diagnostics; using System.Linq; - + using System.Text; using Microsoft.Extensions.Logging; using NEventSocket.Logging; @@ -22,6 +23,7 @@ namespace NEventSocket.FreeSwitch [Serializable] public class EventMessage : BasicMessage { + public static readonly byte[] delimiterBytes = Encoding.UTF8.GetBytes("\n\n"); private static readonly ILogger log = Logger.Get(); internal EventMessage(BasicMessage basicMessage) @@ -60,8 +62,8 @@ internal EventMessage(BasicMessage basicMessage) try { - var delimiterIndex = basicMessage.BodyText.IndexOf("\n\n", StringComparison.Ordinal); - if (delimiterIndex == -1 || delimiterIndex == basicMessage.BodyText.Length - 2) + var delimiterIndex = PatternAt(basicMessage.BodyBytes, delimiterBytes); + if (delimiterIndex == -1 || delimiterIndex == basicMessage.BodyBytes.Length - 2) { // body text consists of key-value-pair event headers, no body Headers = basicMessage.BodyText.ParseKeyValuePairs(": "); @@ -71,13 +73,16 @@ internal EventMessage(BasicMessage basicMessage) { // ...but some Event Messages also carry a body payload, eg. a BACKGROUND_JOB event // which is a message carried inside an EventMessage carried inside a BasicMessage.. - Headers = basicMessage.BodyText.Substring(0, delimiterIndex).ParseKeyValuePairs(": "); + string headersSection = Encoding.UTF8.GetString(basicMessage.BodyBytes, 0, delimiterIndex); + Headers = headersSection.ParseKeyValuePairs(": "); Debug.Assert(Headers.ContainsKey(HeaderNames.ContentLength)); var contentLength = int.Parse(Headers[HeaderNames.ContentLength]); - Debug.Assert(delimiterIndex + 2 + contentLength <= basicMessage.BodyText.Length, "Message cut off mid-transmission"); - var body = basicMessage.BodyText.Substring(delimiterIndex + 2, contentLength); + Debug.Assert(Headers.ContainsKey(HeaderNames.EventName)); + + Debug.Assert(delimiterIndex + 2 + contentLength <= basicMessage.BodyBytes.Length, "Message cut off mid-transmission"); + var body = Encoding.UTF8.GetString(basicMessage.BodyBytes, delimiterIndex + 2, contentLength); //remove any \n\n if any var index = body.IndexOf("\n\n", StringComparison.Ordinal); @@ -99,6 +104,18 @@ protected EventMessage() { } + private static int PatternAt(byte[] source, byte[] pattern) + { + for (int i = 0; i < source.Length; i++) + { + if (source.Skip(i).Take(pattern.Length).SequenceEqual(pattern)) + { + return i; + } + } + return -1; + } + /// /// Gets the of this instance. /// diff --git a/NEventSocket/NEventSocket.csproj b/NEventSocket/NEventSocket.csproj index b850a7f..9e81c14 100644 --- a/NEventSocket/NEventSocket.csproj +++ b/NEventSocket/NEventSocket.csproj @@ -7,7 +7,7 @@ iamkinetic NEventSocket.DotNetCore .Net Core port of NEventSocket - 2.0.2 + 2.0.3 https://github.com/iamkinetic/NEventSocket FreeSwitch NEventSocket DotNetCore - .Net Standard 2.0 support. diff --git a/NEventSocket/Sockets/EventSocket.cs b/NEventSocket/Sockets/EventSocket.cs index 29c664a..47e5f6c 100644 --- a/NEventSocket/Sockets/EventSocket.cs +++ b/NEventSocket/Sockets/EventSocket.cs @@ -50,8 +50,8 @@ protected EventSocket(TcpClient tcpClient, TimeSpan? responseTimeOut = null) : b ResponseTimeOut = responseTimeOut ?? TimeSpan.FromSeconds(5); messages = - Receiver.SelectMany(x => Encoding.UTF8.GetString(x)) - .AggregateUntil(() => new Parser(), (builder, ch) => builder.Append(ch), builder => builder.Completed) + Receiver.SelectMany(x => x) + .AggregateUntil(() => new Parser(), (builder, b) => builder.Append(b), builder => builder.Completed) .Select(builder => builder.ExtractMessage()) .Do( x => Log.LogTrace("Messages Received [{0}].".Fmt(x.ContentType)), diff --git a/NEventSocket/Sockets/Parser.cs b/NEventSocket/Sockets/Parser.cs index df85376..9584351 100644 --- a/NEventSocket/Sockets/Parser.cs +++ b/NEventSocket/Sockets/Parser.cs @@ -8,27 +8,28 @@ namespace NEventSocket.Sockets using System; using System.Collections.Generic; using System.Diagnostics; + using System.IO; using System.Text; - using NEventSocket.FreeSwitch; using NEventSocket.Util; - using NEventSocket.Util.ObjectPooling; /// /// A parser for converting a stream of strings or chars into a stream of s from FreeSwitch. /// public class Parser : IDisposable { - private StringBuilder buffer = StringBuilderPool.Allocate(); - - private char previous; + private byte previous; private int? contentLength; private IDictionary headers; + private MemoryStream headerBytes = new MemoryStream(); + private MemoryStream bodyBytes; + private readonly InterlockedBoolean disposed = new InterlockedBoolean(); + private const byte headerEndDelimiter = (byte)'\n'; ~Parser() { Dispose(false); @@ -45,26 +46,25 @@ public class Parser : IDisposable public bool HasBody => contentLength.HasValue && contentLength > 0; /// - /// Appends the given to the message. + /// Appends the given to the message. /// - /// The next of the message. + /// The next of the message. /// The same instance of the . - public Parser Append(char next) + public Parser Append(byte next) { if (Completed) { return new Parser().Append(next); } - buffer.Append(next); - if (!HasBody) { + headerBytes.WriteByte(next); // we're parsing the headers - if (previous == '\n' && next == '\n') + if (previous == headerEndDelimiter && next == headerEndDelimiter) { // \n\n denotes the end of the Headers - var headerString = buffer.ToString(); + var headerString = Encoding.UTF8.GetString(headerBytes.ToArray()); headers = headerString.ParseKeyValuePairs(": "); @@ -79,10 +79,7 @@ public Parser Append(char next) else { // start parsing the body content - buffer.Clear(); - - // allocate the buffer up front given that we now know the expected size - buffer.EnsureCapacity(contentLength.Value); + bodyBytes = new MemoryStream(contentLength.Value); } } else @@ -98,8 +95,11 @@ public Parser Append(char next) } else { + Debug.Assert(bodyBytes != null); + bodyBytes.WriteByte(next); // if we've read the Content-Length amount of bytes then we're done - Completed = buffer.Length == contentLength.GetValueOrDefault() || contentLength == 0; + Debug.Assert(contentLength > 0); + Completed = bodyBytes.Length == contentLength.GetValueOrDefault(); } return this; @@ -108,11 +108,11 @@ public Parser Append(char next) /// /// Appends the provided string to the internal buffer. /// - public Parser Append(string next) + public Parser Append(byte[] next) { var parser = this; - foreach (var c in next) + foreach (var b in next) { parser = parser.Append(next); } @@ -141,17 +141,17 @@ public BasicMessage ExtractMessage() if (HasBody) { - errorMessage += "expected a body with length {0}, got {1} instead.".Fmt(contentLength, buffer.Length); + errorMessage += "expected a body with length {0}, got {1} instead.".Fmt(contentLength, bodyBytes.Length); } throw new InvalidOperationException(errorMessage); } - var result = HasBody ? new BasicMessage(headers, buffer.ToString()) : new BasicMessage(headers); + var result = HasBody ? new BasicMessage(headers, bodyBytes.ToArray()) : new BasicMessage(headers); if (HasBody) { - Debug.Assert(result.BodyText.Length == result.ContentLength); + Debug.Assert(bodyBytes.Length == result.ContentLength); } Dispose(); @@ -168,11 +168,7 @@ protected virtual void Dispose(bool disposing) { if (disposed != null && !disposed.EnsureCalledOnce()) { - if (buffer != null) - { - StringBuilderPool.Free(buffer); - buffer = null; - } + bodyBytes = null; } } }