feat: port session 07 — Protocol Parser, Auth extras (TPM/certidp/certstore), Internal utilities & data structures

Session 07 scope (5 features, 17 tests, ~1165 Go LOC):
- Protocol/ParserTypes.cs: ParserState enum (79 states), PublishArgument, ParseContext
- Protocol/IProtocolHandler.cs: handler interface decoupling parser from client
- Protocol/ProtocolParser.cs: Parse(), ProtoSnippet(), OverMaxControlLineLimit(),
  ProcessPub/HeaderPub/RoutedMsgArgs/RoutedHeaderMsgArgs, ClonePubArg(), GetHeader()
- tests/Protocol/ProtocolParserTests.cs: 17 tests via TestProtocolHandler stub

Auth extras from session 06 (committed separately):
- Auth/TpmKeyProvider.cs, Auth/CertificateIdentityProvider/, Auth/CertificateStore/

Internal utilities & data structures (session 06 overflow):
- Internal/AccessTimeService.cs, ElasticPointer.cs, SystemMemory.cs, ProcessStatsProvider.cs
- Internal/DataStructures/GenericSublist.cs, HashWheel.cs
- Internal/DataStructures/SubjectTree.cs, SubjectTreeNode.cs, SubjectTreeParts.cs

All 461 tests pass (460 unit + 1 integration). DB updated for features 2588-2592 and tests 2598-2614.
This commit is contained in:
Joseph Doherty
2026-02-26 13:16:56 -05:00
parent 0a54d342ba
commit 88b1391ef0
56 changed files with 9006 additions and 6 deletions

View File

@@ -0,0 +1,798 @@
// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System.Text;
using Shouldly;
using ZB.MOM.NatsNet.Server.Internal;
using ZB.MOM.NatsNet.Server.Protocol;
namespace ZB.MOM.NatsNet.Server.Tests.Protocol;
/// <summary>
/// Tests for the NATS protocol parser.
/// Mirrors Go parser_test.go — 17 test functions.
/// </summary>
public class ProtocolParserTests
{
// =====================================================================
// Test helpers — mirrors Go dummyClient/dummyRouteClient
// =====================================================================
private static ParseContext DummyClient() => new()
{
Kind = ClientKind.Client,
MaxControlLine = ServerConstants.MaxControlLineSize,
MaxPayload = -1,
HasHeaders = false,
};
private static ParseContext DummyRouteClient() => new()
{
Kind = ClientKind.Router,
MaxControlLine = ServerConstants.MaxControlLineSize,
MaxPayload = -1,
};
private static TestProtocolHandler DummyHandler() => new();
// =====================================================================
// TestParsePing — Go test ID 2598
// =====================================================================
[Fact]
public void ParsePing_ByteByByte()
{
var c = DummyClient();
var h = DummyHandler();
c.State.ShouldBe(ParserState.OpStart);
var ping = "PING\r\n"u8.ToArray();
ProtocolParser.Parse(c, h, ping[..1]).ShouldBeNull();
c.State.ShouldBe(ParserState.OpP);
ProtocolParser.Parse(c, h, ping[1..2]).ShouldBeNull();
c.State.ShouldBe(ParserState.OpPi);
ProtocolParser.Parse(c, h, ping[2..3]).ShouldBeNull();
c.State.ShouldBe(ParserState.OpPin);
ProtocolParser.Parse(c, h, ping[3..4]).ShouldBeNull();
c.State.ShouldBe(ParserState.OpPing);
ProtocolParser.Parse(c, h, ping[4..5]).ShouldBeNull();
c.State.ShouldBe(ParserState.OpPing);
ProtocolParser.Parse(c, h, ping[5..6]).ShouldBeNull();
c.State.ShouldBe(ParserState.OpStart);
h.PingCount.ShouldBe(1);
// Full message
ProtocolParser.Parse(c, h, ping).ShouldBeNull();
c.State.ShouldBe(ParserState.OpStart);
h.PingCount.ShouldBe(2);
// Should tolerate spaces
var pingSpaces = "PING \r"u8.ToArray();
ProtocolParser.Parse(c, h, pingSpaces).ShouldBeNull();
c.State.ShouldBe(ParserState.OpPing);
c.State = ParserState.OpStart;
var pingSpaces2 = "PING \r \n"u8.ToArray();
ProtocolParser.Parse(c, h, pingSpaces2).ShouldBeNull();
c.State.ShouldBe(ParserState.OpStart);
}
// =====================================================================
// TestParsePong — Go test ID 2599
// =====================================================================
[Fact]
public void ParsePong_ByteByByte()
{
var c = DummyClient();
var h = DummyHandler();
c.State.ShouldBe(ParserState.OpStart);
var pong = "PONG\r\n"u8.ToArray();
ProtocolParser.Parse(c, h, pong[..1]).ShouldBeNull();
c.State.ShouldBe(ParserState.OpP);
ProtocolParser.Parse(c, h, pong[1..2]).ShouldBeNull();
c.State.ShouldBe(ParserState.OpPo);
ProtocolParser.Parse(c, h, pong[2..3]).ShouldBeNull();
c.State.ShouldBe(ParserState.OpPon);
ProtocolParser.Parse(c, h, pong[3..4]).ShouldBeNull();
c.State.ShouldBe(ParserState.OpPong);
ProtocolParser.Parse(c, h, pong[4..5]).ShouldBeNull();
c.State.ShouldBe(ParserState.OpPong);
ProtocolParser.Parse(c, h, pong[5..6]).ShouldBeNull();
c.State.ShouldBe(ParserState.OpStart);
h.PongCount.ShouldBe(1);
// Full message
ProtocolParser.Parse(c, h, pong).ShouldBeNull();
c.State.ShouldBe(ParserState.OpStart);
h.PongCount.ShouldBe(2);
// Should tolerate spaces
var pongSpaces = "PONG \r"u8.ToArray();
ProtocolParser.Parse(c, h, pongSpaces).ShouldBeNull();
c.State.ShouldBe(ParserState.OpPong);
c.State = ParserState.OpStart;
var pongSpaces2 = "PONG \r \n"u8.ToArray();
ProtocolParser.Parse(c, h, pongSpaces2).ShouldBeNull();
c.State.ShouldBe(ParserState.OpStart);
}
// =====================================================================
// TestParseConnect — Go test ID 2600
// =====================================================================
[Fact]
public void ParseConnect_ParsesCorrectly()
{
var c = DummyClient();
var h = DummyHandler();
var connect = Encoding.ASCII.GetBytes(
"CONNECT {\"verbose\":false,\"pedantic\":true,\"tls_required\":false}\r\n");
ProtocolParser.Parse(c, h, connect).ShouldBeNull();
c.State.ShouldBe(ParserState.OpStart);
h.ConnectArgs.ShouldNotBeNull();
// Check saved state: arg start should be 8 (after "CONNECT ")
c.ArgStart.ShouldBe(connect.Length); // After full parse, ArgStart is past the end
}
// =====================================================================
// TestParseSub — Go test ID 2601
// =====================================================================
[Fact]
public void ParseSub_SetsState()
{
var c = DummyClient();
var h = DummyHandler();
var sub = "SUB foo 1\r"u8.ToArray();
ProtocolParser.Parse(c, h, sub).ShouldBeNull();
c.State.ShouldBe(ParserState.SubArg);
// The arg buffer should have been set up for split buffer
c.ArgBuf.ShouldNotBeNull();
Encoding.ASCII.GetString(c.ArgBuf!).ShouldBe("foo 1");
}
// =====================================================================
// TestParsePub — Go test ID 2602
// =====================================================================
[Fact]
public void ParsePub_ParsesSubjectReplySize()
{
var c = DummyClient();
var h = DummyHandler();
// Simple PUB
var pub = "PUB foo 5\r\nhello\r"u8.ToArray();
ProtocolParser.Parse(c, h, pub).ShouldBeNull();
c.State.ShouldBe(ParserState.MsgEndN);
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo");
c.Pa.Reply.ShouldBeNull();
c.Pa.Size.ShouldBe(5);
// Clear snapshots
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
// PUB with reply
pub = "PUB foo.bar INBOX.22 11\r\nhello world\r"u8.ToArray();
ProtocolParser.Parse(c, h, pub).ShouldBeNull();
c.State.ShouldBe(ParserState.MsgEndN);
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo.bar");
Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("INBOX.22");
c.Pa.Size.ShouldBe(11);
// Clear snapshots
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
// Data larger than expected size
pub = "PUB foo.bar 11\r\nhello world hello world\r"u8.ToArray();
ProtocolParser.Parse(c, h, pub).ShouldNotBeNull();
c.MsgBuf.ShouldBeNull();
}
// =====================================================================
// TestParsePubSizeOverflow — Go test ID 2603
// =====================================================================
[Fact]
public void ParsePubSizeOverflow_ReturnsError()
{
var c = DummyClient();
var h = DummyHandler();
var pub = Encoding.ASCII.GetBytes(
"PUB foo 3333333333333333333333333333333333333333333333333333333333333333\r\n");
ProtocolParser.Parse(c, h, pub).ShouldNotBeNull();
}
// =====================================================================
// TestParsePubArg — Go test ID 2604
// =====================================================================
[Theory]
[MemberData(nameof(PubArgTestCases))]
public void ProcessPub_ParsesArgsCorrectly(string arg, string subject, string reply, int size, string szb)
{
var c = DummyClient();
var err = ProtocolParser.ProcessPub(c, Encoding.ASCII.GetBytes(arg));
err.ShouldBeNull();
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe(subject);
if (string.IsNullOrEmpty(reply))
c.Pa.Reply.ShouldBeNull();
else
Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe(reply);
Encoding.ASCII.GetString(c.Pa.SizeBytes!).ShouldBe(szb);
c.Pa.Size.ShouldBe(size);
}
public static TheoryData<string, string, string, int, string> PubArgTestCases => new()
{
{ "a 2", "a", "", 2, "2" },
{ "a 222", "a", "", 222, "222" },
{ "foo 22", "foo", "", 22, "22" },
{ " foo 22", "foo", "", 22, "22" },
{ "foo 22 ", "foo", "", 22, "22" },
{ "foo 22", "foo", "", 22, "22" },
{ " foo 22 ", "foo", "", 22, "22" },
{ " foo 22 ", "foo", "", 22, "22" },
{ "foo bar 22", "foo", "bar", 22, "22" },
{ " foo bar 22", "foo", "bar", 22, "22" },
{ "foo bar 22 ", "foo", "bar", 22, "22" },
{ "foo bar 22", "foo", "bar", 22, "22" },
{ " foo bar 22 ", "foo", "bar", 22, "22" },
{ " foo bar 22 ", "foo", "bar", 22, "22" },
{ " foo bar 2222 ", "foo", "bar", 2222, "2222" },
{ " foo 2222 ", "foo", "", 2222, "2222" },
{ "a\t2", "a", "", 2, "2" },
{ "a\t222", "a", "", 222, "222" },
{ "foo\t22", "foo", "", 22, "22" },
{ "\tfoo\t22", "foo", "", 22, "22" },
{ "foo\t22\t", "foo", "", 22, "22" },
{ "foo\t\t\t22", "foo", "", 22, "22" },
{ "\tfoo\t22\t", "foo", "", 22, "22" },
{ "\tfoo\t\t\t22\t", "foo", "", 22, "22" },
{ "foo\tbar\t22", "foo", "bar", 22, "22" },
{ "\tfoo\tbar\t22", "foo", "bar", 22, "22" },
{ "foo\tbar\t22\t", "foo", "bar", 22, "22" },
{ "foo\t\tbar\t\t22", "foo", "bar", 22, "22" },
{ "\tfoo\tbar\t22\t", "foo", "bar", 22, "22" },
{ "\t \tfoo\t \t \tbar\t \t22\t \t", "foo", "bar", 22, "22" },
{ "\t\tfoo\t\t\tbar\t\t2222\t\t", "foo", "bar", 2222, "2222" },
{ "\t \tfoo\t \t \t\t\t2222\t \t", "foo", "", 2222, "2222" },
};
// =====================================================================
// TestParsePubBadSize — Go test ID 2605
// =====================================================================
[Fact]
public void ProcessPub_BadSize_ReturnsError()
{
var c = DummyClient();
c.MaxPayload = 32768;
var err = ProtocolParser.ProcessPub(c, "foo 2222222222222222"u8.ToArray());
err.ShouldNotBeNull();
}
// =====================================================================
// TestParseHeaderPub — Go test ID 2606
// =====================================================================
[Fact]
public void ParseHeaderPub_ParsesSubjectReplyHdrSize()
{
var c = DummyClient();
c.HasHeaders = true;
var h = DummyHandler();
// Simple HPUB
var hpub = "HPUB foo 12 17\r\nname:derek\r\nHELLO\r"u8.ToArray();
ProtocolParser.Parse(c, h, hpub).ShouldBeNull();
c.State.ShouldBe(ParserState.MsgEndN);
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo");
c.Pa.Reply.ShouldBeNull();
c.Pa.HeaderSize.ShouldBe(12);
Encoding.ASCII.GetString(c.Pa.HeaderBytes!).ShouldBe("12");
c.Pa.Size.ShouldBe(17);
// Clear snapshots
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
// HPUB with reply
hpub = "HPUB foo INBOX.22 12 17\r\nname:derek\r\nHELLO\r"u8.ToArray();
ProtocolParser.Parse(c, h, hpub).ShouldBeNull();
c.State.ShouldBe(ParserState.MsgEndN);
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo");
Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("INBOX.22");
c.Pa.HeaderSize.ShouldBe(12);
Encoding.ASCII.GetString(c.Pa.HeaderBytes!).ShouldBe("12");
c.Pa.Size.ShouldBe(17);
// Clear snapshots
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
// HPUB with hdr=0
hpub = "HPUB foo INBOX.22 0 5\r\nHELLO\r"u8.ToArray();
ProtocolParser.Parse(c, h, hpub).ShouldBeNull();
c.State.ShouldBe(ParserState.MsgEndN);
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo");
Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("INBOX.22");
c.Pa.HeaderSize.ShouldBe(0);
Encoding.ASCII.GetString(c.Pa.HeaderBytes!).ShouldBe("0");
c.Pa.Size.ShouldBe(5);
}
// =====================================================================
// TestParseHeaderPubArg — Go test ID 2607
// =====================================================================
[Theory]
[MemberData(nameof(HeaderPubArgTestCases))]
public void ProcessHeaderPub_ParsesArgsCorrectly(
string arg, string subject, string reply, int hdr, int size, string szb)
{
var c = DummyClient();
c.HasHeaders = true;
var err = ProtocolParser.ProcessHeaderPub(c, Encoding.ASCII.GetBytes(arg), null);
err.ShouldBeNull();
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe(subject);
if (string.IsNullOrEmpty(reply))
c.Pa.Reply.ShouldBeNull();
else
Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe(reply);
Encoding.ASCII.GetString(c.Pa.SizeBytes!).ShouldBe(szb);
c.Pa.HeaderSize.ShouldBe(hdr);
c.Pa.Size.ShouldBe(size);
}
public static TheoryData<string, string, string, int, int, string> HeaderPubArgTestCases => new()
{
{ "a 2 4", "a", "", 2, 4, "4" },
{ "a 22 222", "a", "", 22, 222, "222" },
{ "foo 3 22", "foo", "", 3, 22, "22" },
{ " foo 1 22", "foo", "", 1, 22, "22" },
{ "foo 0 22 ", "foo", "", 0, 22, "22" },
{ "foo 0 22", "foo", "", 0, 22, "22" },
{ " foo 1 22 ", "foo", "", 1, 22, "22" },
{ " foo 3 22 ", "foo", "", 3, 22, "22" },
{ "foo bar 1 22", "foo", "bar", 1, 22, "22" },
{ " foo bar 11 22", "foo", "bar", 11, 22, "22" },
{ "foo bar 11 22 ", "foo", "bar", 11, 22, "22" },
{ "foo bar 11 22", "foo", "bar", 11, 22, "22" },
{ " foo bar 11 22 ", "foo", "bar", 11, 22, "22" },
{ " foo bar 11 22 ", "foo", "bar", 11, 22, "22" },
{ " foo bar 22 2222 ", "foo", "bar", 22, 2222, "2222" },
{ " foo 1 2222 ", "foo", "", 1, 2222, "2222" },
{ "a\t2\t22", "a", "", 2, 22, "22" },
{ "a\t2\t\t222", "a", "", 2, 222, "222" },
{ "foo\t2 22", "foo", "", 2, 22, "22" },
{ "\tfoo\t11\t 22", "foo", "", 11, 22, "22" },
{ "foo\t11\t22\t", "foo", "", 11, 22, "22" },
{ "foo\t\t\t11 22", "foo", "", 11, 22, "22" },
{ "\tfoo\t11\t \t 22\t", "foo", "", 11, 22, "22" },
{ "\tfoo\t\t\t11 22\t", "foo", "", 11, 22, "22" },
{ "foo\tbar\t2 22", "foo", "bar", 2, 22, "22" },
{ "\tfoo\tbar\t11\t22", "foo", "bar", 11, 22, "22" },
{ "foo\tbar\t11\t\t22\t ", "foo", "bar", 11, 22, "22" },
{ "foo\t\tbar\t\t11\t\t\t22", "foo", "bar", 11, 22, "22" },
{ "\tfoo\tbar\t11\t22\t", "foo", "bar", 11, 22, "22" },
{ "\t \tfoo\t \t \tbar\t \t11\t 22\t \t", "foo", "bar", 11, 22, "22" },
{ "\t\tfoo\t\t\tbar\t\t22\t\t\t2222\t\t", "foo", "bar", 22, 2222, "2222" },
{ "\t \tfoo\t \t \t\t\t11\t\t 2222\t \t", "foo", "", 11, 2222, "2222" },
};
// =====================================================================
// TestParseRoutedHeaderMsg — Go test ID 2608
// =====================================================================
[Fact]
public void ParseRoutedHeaderMsg_ParsesCorrectly()
{
var c = DummyRouteClient();
var h = DummyHandler();
// hdr > size should error
var pub = "HMSG $foo foo 10 8\r\nXXXhello\r"u8.ToArray();
ProtocolParser.Parse(c, h, pub).ShouldNotBeNull();
// Clear snapshots
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
// Simple HMSG
pub = "HMSG $foo foo 3 8\r\nXXXhello\r"u8.ToArray();
ProtocolParser.Parse(c, h, pub).ShouldBeNull();
c.State.ShouldBe(ParserState.MsgEndN);
Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$foo");
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo");
c.Pa.Reply.ShouldBeNull();
c.Pa.HeaderSize.ShouldBe(3);
c.Pa.Size.ShouldBe(8);
// Clear snapshots
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
// HMSG with reply
pub = "HMSG $G foo.bar INBOX.22 3 14\r\nOK:hello world\r"u8.ToArray();
ProtocolParser.Parse(c, h, pub).ShouldBeNull();
c.State.ShouldBe(ParserState.MsgEndN);
Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G");
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo.bar");
Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("INBOX.22");
c.Pa.HeaderSize.ShouldBe(3);
c.Pa.Size.ShouldBe(14);
// Clear snapshots
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
// HMSG with + reply and queue
pub = "HMSG $G foo.bar + reply baz 3 14\r\nOK:hello world\r"u8.ToArray();
ProtocolParser.Parse(c, h, pub).ShouldBeNull();
c.State.ShouldBe(ParserState.MsgEndN);
Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G");
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo.bar");
Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("reply");
c.Pa.Queues.ShouldNotBeNull();
c.Pa.Queues!.Count.ShouldBe(1);
Encoding.ASCII.GetString(c.Pa.Queues[0]).ShouldBe("baz");
c.Pa.HeaderSize.ShouldBe(3);
c.Pa.Size.ShouldBe(14);
// Clear snapshots
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
// HMSG with | queue (no reply)
pub = "HMSG $G foo.bar | baz 3 14\r\nOK:hello world\r"u8.ToArray();
ProtocolParser.Parse(c, h, pub).ShouldBeNull();
c.State.ShouldBe(ParserState.MsgEndN);
Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G");
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo.bar");
Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("");
c.Pa.Queues.ShouldNotBeNull();
c.Pa.Queues!.Count.ShouldBe(1);
Encoding.ASCII.GetString(c.Pa.Queues[0]).ShouldBe("baz");
c.Pa.HeaderSize.ShouldBe(3);
c.Pa.Size.ShouldBe(14);
}
// =====================================================================
// TestParseRouteMsg — Go test ID 2609
// =====================================================================
[Fact]
public void ParseRouteMsg_ParsesCorrectly()
{
var c = DummyRouteClient();
var h = DummyHandler();
// MSG from route should error (must use RMSG)
var pub = "MSG $foo foo 5\r\nhello\r"u8.ToArray();
ProtocolParser.Parse(c, h, pub).ShouldNotBeNull();
// Reset
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
// RMSG simple
pub = "RMSG $foo foo 5\r\nhello\r"u8.ToArray();
ProtocolParser.Parse(c, h, pub).ShouldBeNull();
c.State.ShouldBe(ParserState.MsgEndN);
Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$foo");
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo");
c.Pa.Reply.ShouldBeNull();
c.Pa.Size.ShouldBe(5);
// Clear
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
// RMSG with reply
pub = "RMSG $G foo.bar INBOX.22 11\r\nhello world\r"u8.ToArray();
ProtocolParser.Parse(c, h, pub).ShouldBeNull();
c.State.ShouldBe(ParserState.MsgEndN);
Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G");
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo.bar");
Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("INBOX.22");
c.Pa.Size.ShouldBe(11);
// Clear
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
// RMSG with + reply and queue
pub = "RMSG $G foo.bar + reply baz 11\r\nhello world\r"u8.ToArray();
ProtocolParser.Parse(c, h, pub).ShouldBeNull();
c.State.ShouldBe(ParserState.MsgEndN);
Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G");
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo.bar");
Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("reply");
c.Pa.Queues.ShouldNotBeNull();
c.Pa.Queues!.Count.ShouldBe(1);
Encoding.ASCII.GetString(c.Pa.Queues[0]).ShouldBe("baz");
// Clear
c.ArgBuf = null; c.MsgBuf = null; c.State = ParserState.OpStart;
// RMSG with | queue (no reply)
pub = "RMSG $G foo.bar | baz 11\r\nhello world\r"u8.ToArray();
ProtocolParser.Parse(c, h, pub).ShouldBeNull();
c.State.ShouldBe(ParserState.MsgEndN);
Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G");
Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo.bar");
Encoding.ASCII.GetString(c.Pa.Reply!).ShouldBe("");
c.Pa.Queues.ShouldNotBeNull();
c.Pa.Queues!.Count.ShouldBe(1);
Encoding.ASCII.GetString(c.Pa.Queues[0]).ShouldBe("baz");
}
// =====================================================================
// TestParseMsgSpace — Go test ID 2610
// =====================================================================
[Fact]
public void ParseMsgSpace_ErrorsCorrectly()
{
// MSG <SPC> from route should error
var c = DummyRouteClient();
var h = DummyHandler();
ProtocolParser.Parse(c, h, "MSG \r\n"u8.ToArray()).ShouldNotBeNull();
// M from client should error
c = DummyClient();
ProtocolParser.Parse(c, h, "M"u8.ToArray()).ShouldNotBeNull();
}
// =====================================================================
// TestShouldFail — Go test ID 2611
// =====================================================================
[Theory]
[MemberData(nameof(ShouldFailClientProtos))]
public void ShouldFail_ClientProtos(string proto)
{
var c = DummyClient();
var h = DummyHandler();
ProtocolParser.Parse(c, h, Encoding.ASCII.GetBytes(proto)).ShouldNotBeNull();
}
public static TheoryData<string> ShouldFailClientProtos => new()
{
"xxx",
"Px", "PIx", "PINx", " PING",
"POx", "PONx",
"+x", "+Ox",
"-x", "-Ex", "-ERx", "-ERRx",
"Cx", "COx", "CONx", "CONNx", "CONNEx", "CONNECx", "CONNECT \r\n",
"PUx", "PUB foo\r\n", "PUB \r\n", "PUB foo bar \r\n",
"PUB foo 2\r\nok \r\n", "PUB foo 2\r\nok\r \n",
"Sx", "SUx", "SUB\r\n", "SUB \r\n", "SUB foo\r\n",
"SUB foo bar baz 22\r\n",
"Ux", "UNx", "UNSx", "UNSUx", "UNSUBx", "UNSUBUNSUB 1\r\n", "UNSUB_2\r\n",
"UNSUB_UNSUB_UNSUB 2\r\n", "UNSUB_\t2\r\n", "UNSUB\r\n", "UNSUB \r\n",
"UNSUB \t \r\n",
"Ix", "INx", "INFx", "INFO \r\n",
};
[Theory]
[MemberData(nameof(ShouldFailRouterProtos))]
public void ShouldFail_RouterProtos(string proto)
{
var c = DummyClient();
c.Kind = ClientKind.Router;
var h = DummyHandler();
ProtocolParser.Parse(c, h, Encoding.ASCII.GetBytes(proto)).ShouldNotBeNull();
}
public static TheoryData<string> ShouldFailRouterProtos => new()
{
"Mx", "MSx", "MSGx", "MSG \r\n",
};
// =====================================================================
// TestProtoSnippet — Go test ID 2612
// =====================================================================
[Fact]
public void ProtoSnippet_MatchesGoOutput()
{
var sample = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"u8.ToArray();
var tests = new (int Start, string Expected)[]
{
(0, "\"abcdefghijklmnopqrstuvwxyzABCDEF\""),
(1, "\"bcdefghijklmnopqrstuvwxyzABCDEFG\""),
(2, "\"cdefghijklmnopqrstuvwxyzABCDEFGH\""),
(3, "\"defghijklmnopqrstuvwxyzABCDEFGHI\""),
(4, "\"efghijklmnopqrstuvwxyzABCDEFGHIJ\""),
(5, "\"fghijklmnopqrstuvwxyzABCDEFGHIJK\""),
(6, "\"ghijklmnopqrstuvwxyzABCDEFGHIJKL\""),
(7, "\"hijklmnopqrstuvwxyzABCDEFGHIJKLM\""),
(8, "\"ijklmnopqrstuvwxyzABCDEFGHIJKLMN\""),
(9, "\"jklmnopqrstuvwxyzABCDEFGHIJKLMNO\""),
(10, "\"klmnopqrstuvwxyzABCDEFGHIJKLMNOP\""),
(11, "\"lmnopqrstuvwxyzABCDEFGHIJKLMNOPQ\""),
(12, "\"mnopqrstuvwxyzABCDEFGHIJKLMNOPQR\""),
(13, "\"nopqrstuvwxyzABCDEFGHIJKLMNOPQRS\""),
(14, "\"opqrstuvwxyzABCDEFGHIJKLMNOPQRST\""),
(15, "\"pqrstuvwxyzABCDEFGHIJKLMNOPQRSTU\""),
(16, "\"qrstuvwxyzABCDEFGHIJKLMNOPQRSTUV\""),
(17, "\"rstuvwxyzABCDEFGHIJKLMNOPQRSTUVW\""),
(18, "\"stuvwxyzABCDEFGHIJKLMNOPQRSTUVWX\""),
(19, "\"tuvwxyzABCDEFGHIJKLMNOPQRSTUVWXY\""),
(20, "\"uvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\""),
(21, "\"vwxyzABCDEFGHIJKLMNOPQRSTUVWXY\""),
(22, "\"wxyzABCDEFGHIJKLMNOPQRSTUVWXY\""),
(23, "\"xyzABCDEFGHIJKLMNOPQRSTUVWXY\""),
(24, "\"yzABCDEFGHIJKLMNOPQRSTUVWXY\""),
(25, "\"zABCDEFGHIJKLMNOPQRSTUVWXY\""),
(26, "\"ABCDEFGHIJKLMNOPQRSTUVWXY\""),
(27, "\"BCDEFGHIJKLMNOPQRSTUVWXY\""),
(28, "\"CDEFGHIJKLMNOPQRSTUVWXY\""),
(29, "\"DEFGHIJKLMNOPQRSTUVWXY\""),
(30, "\"EFGHIJKLMNOPQRSTUVWXY\""),
(31, "\"FGHIJKLMNOPQRSTUVWXY\""),
(32, "\"GHIJKLMNOPQRSTUVWXY\""),
(33, "\"HIJKLMNOPQRSTUVWXY\""),
(34, "\"IJKLMNOPQRSTUVWXY\""),
(35, "\"JKLMNOPQRSTUVWXY\""),
(36, "\"KLMNOPQRSTUVWXY\""),
(37, "\"LMNOPQRSTUVWXY\""),
(38, "\"MNOPQRSTUVWXY\""),
(39, "\"NOPQRSTUVWXY\""),
(40, "\"OPQRSTUVWXY\""),
(41, "\"PQRSTUVWXY\""),
(42, "\"QRSTUVWXY\""),
(43, "\"RSTUVWXY\""),
(44, "\"STUVWXY\""),
(45, "\"TUVWXY\""),
(46, "\"UVWXY\""),
(47, "\"VWXY\""),
(48, "\"WXY\""),
(49, "\"XY\""),
(50, "\"Y\""),
(51, "\"\""),
(52, "\"\""),
(53, "\"\""),
(54, "\"\""),
};
foreach (var (start, expected) in tests)
{
var got = ProtocolParser.ProtoSnippet(start, ServerConstants.ProtoSnippetSize, sample);
got.ShouldBe(expected, $"start={start}");
}
}
// =====================================================================
// TestParseOK — Go test ID 2613 (mapped from Go TestParseOK)
// =====================================================================
[Fact]
public void ParseOK_ByteByByte()
{
var c = DummyClient();
var h = DummyHandler();
c.State.ShouldBe(ParserState.OpStart);
var ok = "+OK\r\n"u8.ToArray();
ProtocolParser.Parse(c, h, ok[..1]).ShouldBeNull();
c.State.ShouldBe(ParserState.OpPlus);
ProtocolParser.Parse(c, h, ok[1..2]).ShouldBeNull();
c.State.ShouldBe(ParserState.OpPlusO);
ProtocolParser.Parse(c, h, ok[2..3]).ShouldBeNull();
c.State.ShouldBe(ParserState.OpPlusOk);
ProtocolParser.Parse(c, h, ok[3..4]).ShouldBeNull();
c.State.ShouldBe(ParserState.OpPlusOk);
ProtocolParser.Parse(c, h, ok[4..5]).ShouldBeNull();
c.State.ShouldBe(ParserState.OpStart);
}
// =====================================================================
// TestMaxControlLine — Go test ID 2614
// =====================================================================
[Theory]
[InlineData(ClientKind.Client, true)]
[InlineData(ClientKind.Leaf, false)]
[InlineData(ClientKind.Router, false)]
[InlineData(ClientKind.Gateway, false)]
public void MaxControlLine_EnforcedForClientOnly(ClientKind kind, bool shouldFail)
{
var pub = "PUB foo.bar.baz 2\r\nok\r\n"u8.ToArray();
var c = new ParseContext
{
Kind = kind,
MaxControlLine = 8, // Very small limit
MaxPayload = -1,
};
var h = DummyHandler();
// For non-client kinds, we need to set up the OP appropriately
// Routes use RMSG not PUB, but PUB is fine for testing mcl enforcement
// since the state machine handles it the same way.
var err = ProtocolParser.Parse(c, h, pub);
if (shouldFail)
{
err.ShouldNotBeNull();
ErrorContextHelper.ErrorIs(err, ServerErrors.ErrMaxControlLine).ShouldBeTrue();
}
else
{
// Non-client kinds don't enforce max control line
err.ShouldBeNull();
}
}
// =====================================================================
// TestProtocolHandler — stub handler for tests
// =====================================================================
private sealed class TestProtocolHandler : IProtocolHandler
{
public bool IsMqtt => false;
public bool Trace => false;
public bool HasMappings => false;
public bool IsAwaitingAuth => false;
public bool TryRegisterNoAuthUser() => true; // Allow all
public bool IsGatewayInboundNotConnected => false;
public int PingCount { get; private set; }
public int PongCount { get; private set; }
public byte[]? ConnectArgs { get; private set; }
public Exception? ProcessConnect(byte[] arg) { ConnectArgs = arg; return null; }
public Exception? ProcessInfo(byte[] arg) => null;
public void ProcessPing() => PingCount++;
public void ProcessPong() => PongCount++;
public void ProcessErr(string arg) { }
public Exception? ProcessClientSub(byte[] arg) => null;
public Exception? ProcessClientUnsub(byte[] arg) => null;
public Exception? ProcessRemoteSub(byte[] arg, bool isLeaf) => null;
public Exception? ProcessRemoteUnsub(byte[] arg, bool isLeafUnsub) => null;
public Exception? ProcessGatewayRSub(byte[] arg) => null;
public Exception? ProcessGatewayRUnsub(byte[] arg) => null;
public Exception? ProcessLeafSub(byte[] arg) => null;
public Exception? ProcessLeafUnsub(byte[] arg) => null;
public Exception? ProcessAccountSub(byte[] arg) => null;
public void ProcessAccountUnsub(byte[] arg) { }
public void ProcessInboundMsg(byte[] msg) { }
public bool SelectMappedSubject() => false;
public void TraceInOp(string name, byte[]? arg) { }
public void TraceMsg(byte[] msg) { }
public void SendErr(string msg) { }
public void AuthViolation() { }
public void CloseConnection(int reason) { }
public string KindString() => "CLIENT";
}
}