From 3ace39d606c7d8a7c5ebbd7c13aed15f6e185058 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 28 Feb 2026 07:06:09 -0500 Subject: [PATCH] feat(batch2): verify parser remainder features --- .../ZB.MOM.NatsNet.Server/ClientConnection.cs | 52 +++++++++ .../Protocol/ProtocolParserTests.cs | 108 +++++++++++++++++- porting.db | Bin 6352896 -> 6356992 bytes 3 files changed, 159 insertions(+), 1 deletion(-) diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs index 00a9682..f35f978 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs @@ -1158,6 +1158,58 @@ public sealed partial class ClientConnection TraceInOp("PRE", pre); } + // ========================================================================= + // Parser compatibility wrappers (features 2588, 2590, 2591) + // ========================================================================= + + /// + /// Parses protocol bytes using the shared parser state for this connection. + /// Mirrors Go client.parse. + /// + internal Exception? Parse(byte[] buf, IProtocolHandler handler) + { + ArgumentNullException.ThrowIfNull(buf); + ArgumentNullException.ThrowIfNull(handler); + + ParseCtx.Kind = Kind; + ParseCtx.HasHeaders = Headers; + if (_mcl > 0) + ParseCtx.MaxControlLine = _mcl; + if (_mpay != 0) + ParseCtx.MaxPayload = _mpay; + + return ProtocolParser.Parse(ParseCtx, handler, buf); + } + + /// + /// Checks max control line enforcement for the current connection kind. + /// Mirrors Go client.overMaxControlLineLimit. + /// + internal Exception? OverMaxControlLineLimit(byte[] arg, int mcl, IProtocolHandler handler) + { + ArgumentNullException.ThrowIfNull(arg); + ArgumentNullException.ThrowIfNull(handler); + + ParseCtx.Kind = Kind; + return ProtocolParser.OverMaxControlLineLimit(ParseCtx, handler, arg, mcl); + } + + /// + /// Re-processes stored pub args for split-buffer message payload handling. + /// Mirrors Go client.clonePubArg. + /// + internal Exception? ClonePubArg(IProtocolHandler handler, bool lmsg) + { + ArgumentNullException.ThrowIfNull(handler); + + ParseCtx.Kind = Kind; + ParseCtx.HasHeaders = Headers; + if (_mpay != 0) + ParseCtx.MaxPayload = _mpay; + + return ProtocolParser.ClonePubArg(ParseCtx, handler, lmsg); + } + /// /// Generates the INFO JSON bytes sent to the client on connect. /// Stub — full implementation in session 09. diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Protocol/ProtocolParserTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Protocol/ProtocolParserTests.cs index ebc9e82..70fb822 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Protocol/ProtocolParserTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Protocol/ProtocolParserTests.cs @@ -13,6 +13,7 @@ using System.Text; using Shouldly; +using ZB.MOM.NatsNet.Server; using ZB.MOM.NatsNet.Server.Internal; using ZB.MOM.NatsNet.Server.Protocol; @@ -754,6 +755,110 @@ public class ProtocolParserTests } } + [Fact] + public void ParsePub_SplitPayload_ClonesPubArgAndCompletesMessage() + { + var c = DummyClient(); + var h = DummyHandler(); + + var first = "PUB foo 5\r\nhe"u8.ToArray(); + ProtocolParser.Parse(c, h, first).ShouldBeNull(); + c.State.ShouldBe(ParserState.MsgPayload); + c.ArgBuf.ShouldNotBeNull(); + Encoding.ASCII.GetString(c.ArgBuf!).ShouldBe("foo 5"); + c.MsgBuf.ShouldNotBeNull(); + Encoding.ASCII.GetString(c.MsgBuf!).ShouldBe("he"); + h.InboundMessages.Count.ShouldBe(0); + + var second = "llo\r\n"u8.ToArray(); + ProtocolParser.Parse(c, h, second).ShouldBeNull(); + c.State.ShouldBe(ParserState.OpStart); + h.InboundMessages.Count.ShouldBe(1); + Encoding.ASCII.GetString(h.InboundMessages[0]).ShouldBe("hello\r\n"); + } + + [Fact] + public void ClonePubArg_ClientBranch_ReprocessesPubArgs() + { + var c = DummyClient(); + var h = DummyHandler(); + c.Pa.Arg = "foo 5"u8.ToArray(); + c.Pa.HeaderSize = -1; + + var err = ProtocolParser.ClonePubArg(c, h, lmsg: false); + + err.ShouldBeNull(); + c.ArgBuf.ShouldNotBeNull(); + Encoding.ASCII.GetString(c.ArgBuf!).ShouldBe("foo 5"); + Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo"); + c.Pa.Size.ShouldBe(5); + } + + [Fact] + public void ClonePubArg_RouterLmsgBranch_ReprocessesRoutedOriginArgs() + { + var c = DummyRouteClient(); + var h = DummyHandler(); + c.Pa.Arg = "$G foo 5"u8.ToArray(); + c.Pa.HeaderSize = -1; + + var err = ProtocolParser.ClonePubArg(c, h, lmsg: true); + + err.ShouldBeNull(); + c.ArgBuf.ShouldNotBeNull(); + Encoding.ASCII.GetString(c.ArgBuf!).ShouldBe("$G foo 5"); + Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G"); + Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo"); + c.Pa.Size.ShouldBe(5); + } + + [Fact] + public void ClonePubArg_LeafHeaderBranch_ReprocessesLeafHeaderArgs() + { + var c = new ParseContext { Kind = ClientKind.Leaf, MaxPayload = -1 }; + var h = DummyHandler(); + c.Pa.Arg = "$G foo 2 5"u8.ToArray(); + c.Pa.HeaderSize = 2; + + var err = ProtocolParser.ClonePubArg(c, h, lmsg: false); + + err.ShouldBeNull(); + c.ArgBuf.ShouldNotBeNull(); + Encoding.ASCII.GetString(c.ArgBuf!).ShouldBe("$G foo 2 5"); + Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G"); + Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo"); + c.Pa.HeaderSize.ShouldBe(2); + c.Pa.Size.ShouldBe(5); + } + + [Fact] + public void ClientConnection_ExposesParserCompatibilityMethods() + { + var flags = System.Reflection.BindingFlags.Instance + | System.Reflection.BindingFlags.NonPublic + | System.Reflection.BindingFlags.Public; + var methods = typeof(ClientConnection).GetMethods(flags); + + methods.Any(method => + method.Name == "Parse" && + method.GetParameters().Length == 2 && + method.GetParameters()[0].ParameterType == typeof(byte[]) && + method.GetParameters()[1].ParameterType == typeof(IProtocolHandler)).ShouldBeTrue(); + + methods.Any(method => + method.Name == "OverMaxControlLineLimit" && + method.GetParameters().Length == 3 && + method.GetParameters()[0].ParameterType == typeof(byte[]) && + method.GetParameters()[1].ParameterType == typeof(int) && + method.GetParameters()[2].ParameterType == typeof(IProtocolHandler)).ShouldBeTrue(); + + methods.Any(method => + method.Name == "ClonePubArg" && + method.GetParameters().Length == 2 && + method.GetParameters()[0].ParameterType == typeof(IProtocolHandler) && + method.GetParameters()[1].ParameterType == typeof(bool)).ShouldBeTrue(); + } + // ===================================================================== // TestProtocolHandler — stub handler for tests // ===================================================================== @@ -770,6 +875,7 @@ public class ProtocolParserTests public int PingCount { get; private set; } public int PongCount { get; private set; } public byte[]? ConnectArgs { get; private set; } + public List InboundMessages { get; } = []; public Exception? ProcessConnect(byte[] arg) { ConnectArgs = arg; return null; } public Exception? ProcessInfo(byte[] arg) => null; @@ -786,7 +892,7 @@ public class ProtocolParserTests public Exception? ProcessLeafUnsub(byte[] arg) => null; public Exception? ProcessAccountSub(byte[] arg) => null; public void ProcessAccountUnsub(byte[] arg) { } - public void ProcessInboundMsg(byte[] msg) { } + public void ProcessInboundMsg(byte[] msg) => InboundMessages.Add(msg); public bool SelectMappedSubject() => false; public void TraceInOp(string name, byte[]? arg) { } public void TraceMsg(byte[] msg) { } diff --git a/porting.db b/porting.db index a8ce8d99ec89b4405d6d96bf0c89c0570c4d5e03..424aa6511a5b2ea2cdf59f0233dadc8baab27c5c 100644 GIT binary patch delta 1703 zcmciBOKclO7zgm(wKs`3YsZe0v`uaAI*r>r>U!6Y*fAls2`!~Ok~ZN{QkSglbul=$ zwcdaVhuBTQ2Xi12(%cY5a4b?K(4LSua3NArflGu?Rn(j+h=hbx2_(edDUJh|UaB5` z(#+0C^Z#bQsoq{QRjXG_9~@+-DXPQ1E*__+uP-G6^cQPa1IE<=J@O^(GB&p8pIDdG zUfY{wDJg zWAl;g;r&VDx0_7N+Cfq5kBvZm{1yWyWBiu+_oU4ZOa1l1M>~ZIqVH`&tWgpX|g?Uf8E0g&7bq8s^xQe+_C3B zGZ#s0FCDB~oOW<}1czW4d~g_!zzB@O7>vVF z@WU|(fCMrGAp~KFzyw4g29pqn$6yK`hiNzt2}r^dFauA*2{;K)!6|qePQx?sEIbF# z!z|3f88{2`umCT>i|`V>4Cmk#ScLP2#hLnxHw{qNNT--ON?#+g?CceFwW3#oQgFgA1^vPM?HG(lqV+{@3NFB_#-exWv+sJX zcoHn1pnA^QKDBOIc|Kuy=TgV`1GQ6)aCb9mjdj-|?W_V(it->SUXl;5%0WqPJ!It8 zS1SHoq4buRc}>l{Q7+|+x|mjTB~2Au{S_C>idIoIu~oa6TPYMSwTne*N-?`rftvka z33yRq#0s@yS@-7h+|M-|IM++0cjST9SnSb93$~)wBHisP!|(n1yq{cnSMFb(n0(Z! k#u{?F<+#?pbHYRFx#$CWt~S1nh1;SFxm!27h$xD#6oS;Osx2rk1VQ}Tg^M5jzjG>;8NO7x;YQObf;ZE~!vz^*asq@;3#~;)Ck9=4xk9!8))mVdrMZ_BNkGv*VwKx1Pd#RZ$D%EJBja?Mq zO=W;lEu?&=iPt<%4-BP;%C}^Ps2fTbqkGH2I0a0lfA#33f~n-_>O;AMDqn+Ms+@Dl z^vYC%^fc{qRl^YltY8B>sNeu6tp44giEzZTxFrALnncMI4~eL*XBbxtG~?B8Q-X0n zSzGiec869b_4_osQub6;pAKGV0Uxx29|Eur*24y9gLVi)2XsOggus9>n6MEd&<#Bh zg