diff --git a/src/SmtpServer.Benchmarks/Program.cs b/src/SmtpServer.Benchmarks/Program.cs index e8c71cb9..10523351 100644 --- a/src/SmtpServer.Benchmarks/Program.cs +++ b/src/SmtpServer.Benchmarks/Program.cs @@ -8,17 +8,13 @@ public class Program { public static void Main(string[] args) { - //var summary = BenchmarkRunner.Run( - // ManualConfig - // .Create(DefaultConfig.Instance) - // .With(ConfigOptions.DisableOptimizationsValidator)); - - //var summary = BenchmarkRunner.Run(); - - var summary = BenchmarkRunner.Run( - ManualConfig - .Create(DefaultConfig.Instance) - .With(ConfigOptions.DisableOptimizationsValidator)); + BenchmarkSwitcher + .FromAssembly(typeof(Program).Assembly) + .Run( + args, + ManualConfig + .Create(DefaultConfig.Instance) + .WithOptions(ConfigOptions.DisableOptimizationsValidator)); } } } diff --git a/src/SmtpServer.Benchmarks/SmtpParserBenchmarks.cs b/src/SmtpServer.Benchmarks/SmtpParserBenchmarks.cs new file mode 100644 index 00000000..b94fe492 --- /dev/null +++ b/src/SmtpServer.Benchmarks/SmtpParserBenchmarks.cs @@ -0,0 +1,150 @@ +using System.Buffers; +using System.Text; +using BenchmarkDotNet.Attributes; +using SmtpServer.Protocol; +using SmtpServer.Text; + +namespace SmtpServer.Benchmarks +{ + [MemoryDiagnoser] + [ShortRunJob] + public class SmtpParserBenchmarks + { + static readonly SmtpResponse UnrecognizedCommand = new SmtpResponse(SmtpReplyCode.CommandNotImplemented, "Unrecognized command"); + + readonly SmtpParser _parser = new SmtpParser(new SmtpCommandFactory()); + byte[] _buffer; + + [Params( + "EHLO example.com", + "MAIL FROM: SIZE=12345 SMTPUTF8", + "RCPT TO:", + "AUTH PLAIN Y2Fpbi5vc3VsbGl2YW5AZ21haWwuY29t", + "PROXY TCP4 192.168.1.1 192.168.1.2 1234 16789", + "ABCDE FGHIJ KLMNO")] + public string Input { get; set; } + + [GlobalSetup] + public void Setup() + { + _buffer = Encoding.UTF8.GetBytes(Input); + } + + [Benchmark(Baseline = true)] + public bool LegacySequentialCandidateLoop() + { + var sequence = new ReadOnlySequence(_buffer); + + return LegacyTryMake(ref sequence, out _, out _); + } + + [Benchmark] + public bool SinglePassVerbDispatch() + { + var sequence = new ReadOnlySequence(_buffer); + + return _parser.TryMake(ref sequence, out _, out _); + } + + bool LegacyTryMake(ref ReadOnlySequence buffer, out SmtpCommand command, out SmtpResponse errorResponse) + { + return TryMakeEhlo(buffer, out command, out errorResponse) + || TryMakeHelo(buffer, out command, out errorResponse) + || TryMakeMail(buffer, out command, out errorResponse) + || TryMakeRcpt(buffer, out command, out errorResponse) + || TryMakeData(buffer, out command, out errorResponse) + || TryMakeQuit(buffer, out command, out errorResponse) + || TryMakeRset(buffer, out command, out errorResponse) + || TryMakeNoop(buffer, out command, out errorResponse) + || TryMakeStartTls(buffer, out command, out errorResponse) + || TryMakeAuth(buffer, out command, out errorResponse) + || TryMakeProxy(buffer, out command, out errorResponse) + || MakeUnrecognized(out command, out errorResponse); + } + + bool TryMakeEhlo(ReadOnlySequence buffer, out SmtpCommand command, out SmtpResponse errorResponse) + { + var reader = new TokenReader(buffer); + + return _parser.TryMakeEhlo(ref reader, out command, out errorResponse); + } + + bool TryMakeHelo(ReadOnlySequence buffer, out SmtpCommand command, out SmtpResponse errorResponse) + { + var reader = new TokenReader(buffer); + + return _parser.TryMakeHelo(ref reader, out command, out errorResponse); + } + + bool TryMakeMail(ReadOnlySequence buffer, out SmtpCommand command, out SmtpResponse errorResponse) + { + var reader = new TokenReader(buffer); + + return _parser.TryMakeMail(ref reader, out command, out errorResponse); + } + + bool TryMakeRcpt(ReadOnlySequence buffer, out SmtpCommand command, out SmtpResponse errorResponse) + { + var reader = new TokenReader(buffer); + + return _parser.TryMakeRcpt(ref reader, out command, out errorResponse); + } + + bool TryMakeData(ReadOnlySequence buffer, out SmtpCommand command, out SmtpResponse errorResponse) + { + var reader = new TokenReader(buffer); + + return _parser.TryMakeData(ref reader, out command, out errorResponse); + } + + bool TryMakeQuit(ReadOnlySequence buffer, out SmtpCommand command, out SmtpResponse errorResponse) + { + var reader = new TokenReader(buffer); + + return _parser.TryMakeQuit(ref reader, out command, out errorResponse); + } + + bool TryMakeRset(ReadOnlySequence buffer, out SmtpCommand command, out SmtpResponse errorResponse) + { + var reader = new TokenReader(buffer); + + return _parser.TryMakeRset(ref reader, out command, out errorResponse); + } + + bool TryMakeNoop(ReadOnlySequence buffer, out SmtpCommand command, out SmtpResponse errorResponse) + { + var reader = new TokenReader(buffer); + + return _parser.TryMakeNoop(ref reader, out command, out errorResponse); + } + + bool TryMakeStartTls(ReadOnlySequence buffer, out SmtpCommand command, out SmtpResponse errorResponse) + { + var reader = new TokenReader(buffer); + + return _parser.TryMakeStartTls(ref reader, out command, out errorResponse); + } + + bool TryMakeAuth(ReadOnlySequence buffer, out SmtpCommand command, out SmtpResponse errorResponse) + { + var reader = new TokenReader(buffer); + + return _parser.TryMakeAuth(ref reader, out command, out errorResponse); + } + + bool TryMakeProxy(ReadOnlySequence buffer, out SmtpCommand command, out SmtpResponse errorResponse) + { + var reader = new TokenReader(buffer); + + return _parser.TryMakeProxy(ref reader, out command, out errorResponse); + } + + static bool MakeUnrecognized(out SmtpCommand command, out SmtpResponse errorResponse) + { + command = null; + errorResponse = UnrecognizedCommand; + + return false; + } + } +} diff --git a/src/SmtpServer.Tests/SmtpParserTests.cs b/src/SmtpServer.Tests/SmtpParserTests.cs index 555093cd..5ca7c838 100644 --- a/src/SmtpServer.Tests/SmtpParserTests.cs +++ b/src/SmtpServer.Tests/SmtpParserTests.cs @@ -37,6 +37,33 @@ public void CanMakeUnrecognized() Assert.Equal(SmtpReplyCode.CommandNotImplemented, errorResponse.ReplyCode); } + [Theory] + [InlineData("ehlo example.com", typeof(EhloCommand))] + [InlineData("HELO example.com", typeof(HeloCommand))] + [InlineData("MAIL FROM:", typeof(MailCommand))] + [InlineData("RCPT TO:", typeof(RcptCommand))] + [InlineData("DATA", typeof(DataCommand))] + [InlineData("QUIT", typeof(QuitCommand))] + [InlineData("RSET", typeof(RsetCommand))] + [InlineData("NOOP", typeof(NoopCommand))] + [InlineData("STARTTLS", typeof(StartTlsCommand))] + [InlineData("AUTH PLAIN Y2Fpbi5vc3VsbGl2YW5AZ21haWwuY29t", typeof(AuthCommand))] + [InlineData("PROXY UNKNOWN", typeof(ProxyCommand))] + public void CanMakeKnownCommandUsingTopLevelDispatch(string input, Type commandType) + { + // arrange + var buffer = Encoding.UTF8.GetBytes(input); + var sequence = new ReadOnlySequence(buffer, 0, buffer.Length); + + // act + var result = Parser.TryMake(ref sequence, out var command, out var errorResponse); + + // assert + Assert.True(result); + Assert.Equal(commandType, command.GetType()); + Assert.Null(errorResponse); + } + [Fact] public void CanMakeQuit() { diff --git a/src/SmtpServer/Protocol/SmtpParser.cs b/src/SmtpServer/Protocol/SmtpParser.cs index 3b57b32c..9d08a431 100644 --- a/src/SmtpServer/Protocol/SmtpParser.cs +++ b/src/SmtpServer/Protocol/SmtpParser.cs @@ -39,25 +39,106 @@ public SmtpParser(ISmtpCommandFactory smtpCommandFactory) /// Returns true if a command could be made, false if not. public bool TryMake(ref ReadOnlySequence buffer, out SmtpCommand command, out SmtpResponse errorResponse) { - return Make(buffer, TryMakeEhlo, out command, out errorResponse) - || Make(buffer, TryMakeHelo, out command, out errorResponse) - || Make(buffer, TryMakeMail, out command, out errorResponse) - || Make(buffer, TryMakeRcpt, out command, out errorResponse) - || Make(buffer, TryMakeData, out command, out errorResponse) - || Make(buffer, TryMakeQuit, out command, out errorResponse) - || Make(buffer, TryMakeRset, out command, out errorResponse) - || Make(buffer, TryMakeNoop, out command, out errorResponse) - || Make(buffer, TryMakeStartTls, out command, out errorResponse) - || Make(buffer, TryMakeAuth, out command, out errorResponse) - || Make(buffer, TryMakeProxy, out command, out errorResponse) - || Make(buffer, MakeUnrecognized, out command, out errorResponse); + var reader = new TokenReader(buffer); + var verb = reader.Peek(); - static bool Make(ReadOnlySequence buffer, TryMakeDelegate tryMakeDelegate, out SmtpCommand command, out SmtpResponse errorResponse) + if (verb.Kind != TokenKind.Text) { - var reader = new TokenReader(buffer); + return MakeUnrecognized(ref reader, out command, out errorResponse); + } + + if (IsVerb(verb.Text, "EHLO")) + { + return Make(ref reader, TryMakeEhlo, out command, out errorResponse); + } + + if (IsVerb(verb.Text, "HELO")) + { + return Make(ref reader, TryMakeHelo, out command, out errorResponse); + } + + if (IsVerb(verb.Text, "MAIL")) + { + return Make(ref reader, TryMakeMail, out command, out errorResponse); + } + + if (IsVerb(verb.Text, "RCPT")) + { + return Make(ref reader, TryMakeRcpt, out command, out errorResponse); + } + + if (IsVerb(verb.Text, "DATA")) + { + return Make(ref reader, TryMakeData, out command, out errorResponse); + } + + if (IsVerb(verb.Text, "QUIT")) + { + return Make(ref reader, TryMakeQuit, out command, out errorResponse); + } + + if (IsVerb(verb.Text, "RSET")) + { + return Make(ref reader, TryMakeRset, out command, out errorResponse); + } + + if (IsVerb(verb.Text, "NOOP")) + { + return Make(ref reader, TryMakeNoop, out command, out errorResponse); + } + + if (IsVerb(verb.Text, "STARTTLS")) + { + return Make(ref reader, TryMakeStartTls, out command, out errorResponse); + } + + if (IsVerb(verb.Text, "AUTH")) + { + return Make(ref reader, TryMakeAuth, out command, out errorResponse); + } - return tryMakeDelegate(ref reader, out command, out errorResponse); + if (IsVerb(verb.Text, "PROXY")) + { + return Make(ref reader, TryMakeProxy, out command, out errorResponse); } + + return MakeUnrecognized(ref reader, out command, out errorResponse); + + static bool Make(ref TokenReader reader, TryMakeDelegate tryMakeDelegate, out SmtpCommand command, out SmtpResponse errorResponse) + { + if (tryMakeDelegate(ref reader, out command, out errorResponse)) + { + return true; + } + + command = null; + errorResponse = UnrecognizedCommand; + return false; + } + } + + static bool IsVerb(ReadOnlySpan verb, string expected) + { + if (verb.Length != expected.Length) + { + return false; + } + + for (var i = 0; i < verb.Length; i++) + { + var ch = verb[i]; + if (ch >= 'a' && ch <= 'z') + { + ch = (byte)(ch - 32); + } + + if (ch != expected[i]) + { + return false; + } + } + + return true; } static bool MakeUnrecognized(ref TokenReader reader, out SmtpCommand command, out SmtpResponse errorResponse)