919 lines
36 KiB
C#
919 lines
36 KiB
C#
// 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;
|
|
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");
|
|
}
|
|
|
|
[Fact]
|
|
public void ClientConnection_InboundDispatchAndPingIntervalHelpers_ShouldBehave()
|
|
{
|
|
var c = new ClientConnection(ClientKind.Client);
|
|
var before = DateTime.UtcNow;
|
|
c.ProcessInboundMsg(Encoding.ASCII.GetBytes("hello"));
|
|
c.LastIn.ShouldBeGreaterThan(before - TimeSpan.FromMilliseconds(1));
|
|
|
|
ClientConnection.AdjustPingInterval(ClientKind.Router, TimeSpan.FromHours(1))
|
|
.ShouldBeLessThan(TimeSpan.FromHours(1));
|
|
ClientConnection.AdjustPingInterval(ClientKind.Gateway, TimeSpan.FromHours(1))
|
|
.ShouldBeLessThan(TimeSpan.FromHours(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();
|
|
}
|
|
}
|
|
|
|
[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
|
|
// =====================================================================
|
|
|
|
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 List<byte[]> InboundMessages { get; } = [];
|
|
|
|
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) => InboundMessages.Add(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";
|
|
}
|
|
}
|