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 a8ce8d9..424aa65 100644
Binary files a/porting.db and b/porting.db differ