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.
1256 lines
54 KiB
C#
1256 lines
54 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.
|
|
//
|
|
// Adapted from server/parser.go in the NATS server Go source.
|
|
|
|
using System.Text;
|
|
using ZB.MOM.NatsNet.Server.Internal;
|
|
|
|
namespace ZB.MOM.NatsNet.Server.Protocol;
|
|
|
|
/// <summary>
|
|
/// NATS wire protocol parser — byte-by-byte state machine.
|
|
/// Mirrors Go <c>client.parse</c>, <c>protoSnippet</c>, <c>overMaxControlLineLimit</c>,
|
|
/// <c>clonePubArg</c>, and <c>parseState.getHeader</c> from parser.go.
|
|
/// </summary>
|
|
public static class ProtocolParser
|
|
{
|
|
// =====================================================================
|
|
// Parse — main state machine
|
|
// =====================================================================
|
|
|
|
/// <summary>
|
|
/// Parses raw bytes through the NATS protocol state machine.
|
|
/// Mirrors Go <c>client.parse(buf []byte) error</c>.
|
|
/// </summary>
|
|
public static Exception? Parse(ParseContext c, IProtocolHandler handler, byte[] buf)
|
|
{
|
|
if (handler.IsMqtt)
|
|
return null; // MQTT clients handled separately
|
|
|
|
// Snapshot connection state
|
|
var authSet = handler.IsAwaitingAuth;
|
|
var mcl = c.MaxControlLine;
|
|
var trace = handler.Trace;
|
|
var kind = c.Kind;
|
|
var lmsg = false;
|
|
|
|
for (var i = 0; i < buf.Length; i++)
|
|
{
|
|
var b = buf[i];
|
|
|
|
switch (c.State)
|
|
{
|
|
case ParserState.OpStart:
|
|
c.Op = b;
|
|
if (b != 'C' && b != 'c')
|
|
{
|
|
if (authSet)
|
|
{
|
|
if (!handler.TryRegisterNoAuthUser())
|
|
goto authErr;
|
|
authSet = false;
|
|
}
|
|
if (kind == ClientKind.Gateway && handler.IsGatewayInboundNotConnected)
|
|
goto authErr;
|
|
}
|
|
|
|
switch (b)
|
|
{
|
|
case (byte)'P': case (byte)'p': c.State = ParserState.OpP; break;
|
|
case (byte)'H': case (byte)'h': c.State = ParserState.OpH; break;
|
|
case (byte)'S': case (byte)'s': c.State = ParserState.OpS; break;
|
|
case (byte)'U': case (byte)'u': c.State = ParserState.OpU; break;
|
|
case (byte)'R': case (byte)'r':
|
|
if (kind == ClientKind.Client) goto parseErr;
|
|
c.State = ParserState.OpR; break;
|
|
case (byte)'L': case (byte)'l':
|
|
if (kind != ClientKind.Leaf && kind != ClientKind.Router) goto parseErr;
|
|
c.State = ParserState.OpL; break;
|
|
case (byte)'A': case (byte)'a':
|
|
if (kind == ClientKind.Client) goto parseErr;
|
|
c.State = ParserState.OpA; break;
|
|
case (byte)'C': case (byte)'c': c.State = ParserState.OpC; break;
|
|
case (byte)'I': case (byte)'i': c.State = ParserState.OpI; break;
|
|
case (byte)'+': c.State = ParserState.OpPlus; break;
|
|
case (byte)'-': c.State = ParserState.OpMinus; break;
|
|
default: goto parseErr;
|
|
}
|
|
break;
|
|
|
|
// ---- HPUB / HMSG ----
|
|
case ParserState.OpH:
|
|
switch (b) {
|
|
case (byte)'P': case (byte)'p': c.State = ParserState.OpHp; break;
|
|
case (byte)'M': case (byte)'m': c.State = ParserState.OpHm; break;
|
|
default: goto parseErr;
|
|
}
|
|
break;
|
|
case ParserState.OpHp:
|
|
if (b == 'U' || b == 'u') c.State = ParserState.OpHpu; else goto parseErr; break;
|
|
case ParserState.OpHpu:
|
|
if (b == 'B' || b == 'b') c.State = ParserState.OpHpub; else goto parseErr; break;
|
|
case ParserState.OpHpub:
|
|
if (b == ' ' || b == '\t') c.State = ParserState.OpHpubSpc; else goto parseErr; break;
|
|
case ParserState.OpHpubSpc:
|
|
if (b == ' ' || b == '\t') break; // continue
|
|
c.Pa.HeaderSize = 0;
|
|
c.State = ParserState.HpubArg;
|
|
c.ArgStart = i;
|
|
break;
|
|
case ParserState.HpubArg:
|
|
switch (b)
|
|
{
|
|
case (byte)'\r': c.Drop = 1; break;
|
|
case (byte)'\n':
|
|
{
|
|
var arg = CollectArg(c, buf, i);
|
|
var err = OverMaxControlLineLimit(c, handler, arg, mcl);
|
|
if (err != null) return err;
|
|
if (trace) handler.TraceInOp("HPUB", arg);
|
|
byte[]? remaining = i < buf.Length - 1 ? buf[(i + 1)..] : null;
|
|
err = ProcessHeaderPub(c, arg, remaining);
|
|
if (err != null) return err;
|
|
c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.MsgPayload;
|
|
if (c.MsgBuf == null) i = c.ArgStart + c.Pa.Size - ServerConstants.LenCrLf;
|
|
break;
|
|
}
|
|
default:
|
|
if (c.ArgBuf != null) AppendToArgBuf(c, b);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case ParserState.OpHm:
|
|
if (b == 'S' || b == 's') c.State = ParserState.OpHms; else goto parseErr; break;
|
|
case ParserState.OpHms:
|
|
if (b == 'G' || b == 'g') c.State = ParserState.OpHmsg; else goto parseErr; break;
|
|
case ParserState.OpHmsg:
|
|
if (b == ' ' || b == '\t') c.State = ParserState.OpHmsgSpc; else goto parseErr; break;
|
|
case ParserState.OpHmsgSpc:
|
|
if (b == ' ' || b == '\t') break;
|
|
c.Pa.HeaderSize = 0;
|
|
c.State = ParserState.HmsgArg;
|
|
c.ArgStart = i;
|
|
break;
|
|
case ParserState.HmsgArg:
|
|
switch (b)
|
|
{
|
|
case (byte)'\r': c.Drop = 1; break;
|
|
case (byte)'\n':
|
|
{
|
|
var arg = CollectArg(c, buf, i);
|
|
var err = OverMaxControlLineLimit(c, handler, arg, mcl);
|
|
if (err != null) return err;
|
|
if (kind == ClientKind.Router || kind == ClientKind.Gateway)
|
|
{
|
|
if (trace) handler.TraceInOp("HMSG", arg);
|
|
err = ProcessRoutedHeaderMsgArgs(c, arg);
|
|
}
|
|
else if (kind == ClientKind.Leaf)
|
|
{
|
|
if (trace) handler.TraceInOp("HMSG", arg);
|
|
err = ProcessLeafHeaderMsgArgs(c, arg);
|
|
}
|
|
if (err != null) return err;
|
|
c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.MsgPayload;
|
|
i = c.ArgStart + c.Pa.Size - ServerConstants.LenCrLf;
|
|
break;
|
|
}
|
|
default:
|
|
if (c.ArgBuf != null) AppendToArgBuf(c, b);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
// ---- PUB ----
|
|
case ParserState.OpP:
|
|
switch (b) {
|
|
case (byte)'U': case (byte)'u': c.State = ParserState.OpPu; break;
|
|
case (byte)'I': case (byte)'i': c.State = ParserState.OpPi; break;
|
|
case (byte)'O': case (byte)'o': c.State = ParserState.OpPo; break;
|
|
default: goto parseErr;
|
|
}
|
|
break;
|
|
case ParserState.OpPu:
|
|
if (b == 'B' || b == 'b') c.State = ParserState.OpPub; else goto parseErr; break;
|
|
case ParserState.OpPub:
|
|
if (b == ' ' || b == '\t') c.State = ParserState.OpPubSpc; else goto parseErr; break;
|
|
case ParserState.OpPubSpc:
|
|
if (b == ' ' || b == '\t') break;
|
|
c.Pa.HeaderSize = -1;
|
|
c.State = ParserState.PubArg;
|
|
c.ArgStart = i;
|
|
break;
|
|
case ParserState.PubArg:
|
|
switch (b)
|
|
{
|
|
case (byte)'\r': c.Drop = 1; break;
|
|
case (byte)'\n':
|
|
{
|
|
var arg = CollectArg(c, buf, i);
|
|
var err = OverMaxControlLineLimit(c, handler, arg, mcl);
|
|
if (err != null) return err;
|
|
if (trace) handler.TraceInOp("PUB", arg);
|
|
err = ProcessPub(c, arg);
|
|
if (err != null) return err;
|
|
c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.MsgPayload;
|
|
if (c.MsgBuf == null) i = c.ArgStart + c.Pa.Size - ServerConstants.LenCrLf;
|
|
break;
|
|
}
|
|
default:
|
|
if (c.ArgBuf != null) AppendToArgBuf(c, b);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
// ---- MSG_PAYLOAD / MSG_END ----
|
|
case ParserState.MsgPayload:
|
|
if (c.MsgBuf != null)
|
|
{
|
|
var toCopy = c.Pa.Size - c.MsgBuf.Length;
|
|
var avail = buf.Length - i;
|
|
if (avail < toCopy) toCopy = avail;
|
|
if (toCopy > 0)
|
|
{
|
|
var start = c.MsgBuf.Length;
|
|
var tmp = new byte[start + toCopy];
|
|
Array.Copy(c.MsgBuf, tmp, start);
|
|
Array.Copy(buf, i, tmp, start, toCopy);
|
|
c.MsgBuf = tmp;
|
|
i = i + toCopy - 1;
|
|
}
|
|
else
|
|
{
|
|
var tmp = c.MsgBuf;
|
|
Array.Resize(ref tmp, tmp.Length + 1);
|
|
tmp[^1] = b;
|
|
c.MsgBuf = tmp;
|
|
}
|
|
if (c.MsgBuf.Length >= c.Pa.Size) c.State = ParserState.MsgEndR;
|
|
}
|
|
else if (i - c.ArgStart + 1 >= c.Pa.Size)
|
|
{
|
|
c.State = ParserState.MsgEndR;
|
|
}
|
|
break;
|
|
|
|
case ParserState.MsgEndR:
|
|
if (b != '\r') goto parseErr;
|
|
if (c.MsgBuf != null) AppendToMsgBuf(c, b);
|
|
c.State = ParserState.MsgEndN;
|
|
break;
|
|
|
|
case ParserState.MsgEndN:
|
|
if (b != '\n') goto parseErr;
|
|
if (c.MsgBuf != null)
|
|
AppendToMsgBuf(c, b);
|
|
else
|
|
c.MsgBuf = buf[c.ArgStart..(i + 1)];
|
|
|
|
// Check for mappings
|
|
if ((kind == ClientKind.Client || kind == ClientKind.Leaf) && handler.HasMappings)
|
|
{
|
|
if (handler.SelectMappedSubject())
|
|
{
|
|
if (trace)
|
|
handler.TraceInOp("MAPPING",
|
|
Encoding.ASCII.GetBytes($"{AsString(c.Pa.Mapped)} -> {AsString(c.Pa.Subject)}"));
|
|
}
|
|
}
|
|
if (trace) handler.TraceMsg(c.MsgBuf);
|
|
handler.ProcessInboundMsg(c.MsgBuf);
|
|
|
|
c.ArgBuf = null; c.MsgBuf = null;
|
|
c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.OpStart;
|
|
// Drop all pub args
|
|
c.Pa.Reset();
|
|
lmsg = false;
|
|
break;
|
|
|
|
// ---- A+/A- (account sub/unsub) ----
|
|
case ParserState.OpA:
|
|
switch (b) {
|
|
case (byte)'+': c.State = ParserState.OpAsub; break;
|
|
case (byte)'-': case (byte)'u': c.State = ParserState.OpAusub; break;
|
|
default: goto parseErr;
|
|
}
|
|
break;
|
|
case ParserState.OpAsub:
|
|
if (b == ' ' || b == '\t') c.State = ParserState.OpAsubSpc; else goto parseErr; break;
|
|
case ParserState.OpAsubSpc:
|
|
if (b == ' ' || b == '\t') break;
|
|
c.State = ParserState.AsubArg;
|
|
c.ArgStart = i;
|
|
break;
|
|
case ParserState.AsubArg:
|
|
switch (b)
|
|
{
|
|
case (byte)'\r': c.Drop = 1; break;
|
|
case (byte)'\n':
|
|
{
|
|
var arg = CollectArg(c, buf, i);
|
|
var err = OverMaxControlLineLimit(c, handler, arg, mcl);
|
|
if (err != null) return err;
|
|
if (trace) handler.TraceInOp("A+", arg);
|
|
err = handler.ProcessAccountSub(arg);
|
|
if (err != null) return err;
|
|
c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.OpStart;
|
|
break;
|
|
}
|
|
default:
|
|
if (c.ArgBuf != null) AppendToArgBuf(c, b);
|
|
break;
|
|
}
|
|
break;
|
|
case ParserState.OpAusub:
|
|
if (b == ' ' || b == '\t') c.State = ParserState.OpAusubSpc; else goto parseErr; break;
|
|
case ParserState.OpAusubSpc:
|
|
if (b == ' ' || b == '\t') break;
|
|
c.State = ParserState.AusubArg;
|
|
c.ArgStart = i;
|
|
break;
|
|
case ParserState.AusubArg:
|
|
switch (b)
|
|
{
|
|
case (byte)'\r': c.Drop = 1; break;
|
|
case (byte)'\n':
|
|
{
|
|
var arg = CollectArg(c, buf, i);
|
|
var err = OverMaxControlLineLimit(c, handler, arg, mcl);
|
|
if (err != null) return err;
|
|
if (trace) handler.TraceInOp("A-", arg);
|
|
handler.ProcessAccountUnsub(arg);
|
|
c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.OpStart;
|
|
break;
|
|
}
|
|
default:
|
|
if (c.ArgBuf != null) AppendToArgBuf(c, b);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
// ---- SUB ----
|
|
case ParserState.OpS:
|
|
if (b == 'U' || b == 'u') c.State = ParserState.OpSu; else goto parseErr; break;
|
|
case ParserState.OpSu:
|
|
if (b == 'B' || b == 'b') c.State = ParserState.OpSub; else goto parseErr; break;
|
|
case ParserState.OpSub:
|
|
if (b == ' ' || b == '\t') c.State = ParserState.OpSubSpc; else goto parseErr; break;
|
|
case ParserState.OpSubSpc:
|
|
if (b == ' ' || b == '\t') break;
|
|
c.State = ParserState.SubArg;
|
|
c.ArgStart = i;
|
|
break;
|
|
case ParserState.SubArg:
|
|
switch (b)
|
|
{
|
|
case (byte)'\r': c.Drop = 1; break;
|
|
case (byte)'\n':
|
|
{
|
|
var arg = CollectArg(c, buf, i);
|
|
var err = OverMaxControlLineLimit(c, handler, arg, mcl);
|
|
if (err != null) return err;
|
|
var subTokens = SplitArgs(arg);
|
|
if (subTokens.Count is < 2 or > 3)
|
|
return new InvalidOperationException(
|
|
$"processClientSub Parse Error '{Encoding.ASCII.GetString(arg)}'");
|
|
switch (kind)
|
|
{
|
|
case ClientKind.Client:
|
|
if (trace) handler.TraceInOp("SUB", arg);
|
|
err = handler.ProcessClientSub(arg);
|
|
break;
|
|
case ClientKind.Router:
|
|
if (c.Op is (byte)'R' or (byte)'r')
|
|
{
|
|
if (trace) handler.TraceInOp("RS+", arg);
|
|
err = handler.ProcessRemoteSub(arg, false);
|
|
}
|
|
else
|
|
{
|
|
if (trace) handler.TraceInOp("LS+", arg);
|
|
err = handler.ProcessRemoteSub(arg, true);
|
|
}
|
|
break;
|
|
case ClientKind.Gateway:
|
|
if (trace) handler.TraceInOp("RS+", arg);
|
|
err = handler.ProcessGatewayRSub(arg);
|
|
break;
|
|
case ClientKind.Leaf:
|
|
if (trace) handler.TraceInOp("LS+", arg);
|
|
err = handler.ProcessLeafSub(arg);
|
|
break;
|
|
}
|
|
if (err != null) return err;
|
|
c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.OpStart;
|
|
break;
|
|
}
|
|
default:
|
|
if (c.ArgBuf != null) AppendToArgBuf(c, b);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
// ---- L (LS+/LS- or LMSG) ----
|
|
case ParserState.OpL:
|
|
switch (b) {
|
|
case (byte)'S': case (byte)'s': c.State = ParserState.OpLs; break;
|
|
case (byte)'M': case (byte)'m': c.State = ParserState.OpM; break;
|
|
default: goto parseErr;
|
|
}
|
|
break;
|
|
case ParserState.OpLs:
|
|
switch (b) {
|
|
case (byte)'+': c.State = ParserState.OpSub; break;
|
|
case (byte)'-': c.State = ParserState.OpUnsub; break;
|
|
default: goto parseErr;
|
|
}
|
|
break;
|
|
|
|
// ---- R (RS+/RS- or RMSG) ----
|
|
case ParserState.OpR:
|
|
switch (b) {
|
|
case (byte)'S': case (byte)'s': c.State = ParserState.OpRs; break;
|
|
case (byte)'M': case (byte)'m': c.State = ParserState.OpM; break;
|
|
default: goto parseErr;
|
|
}
|
|
break;
|
|
case ParserState.OpRs:
|
|
switch (b) {
|
|
case (byte)'+': c.State = ParserState.OpSub; break;
|
|
case (byte)'-': c.State = ParserState.OpUnsub; break;
|
|
default: goto parseErr;
|
|
}
|
|
break;
|
|
|
|
// ---- UNSUB ----
|
|
case ParserState.OpU:
|
|
if (b == 'N' || b == 'n') c.State = ParserState.OpUn; else goto parseErr; break;
|
|
case ParserState.OpUn:
|
|
if (b == 'S' || b == 's') c.State = ParserState.OpUns; else goto parseErr; break;
|
|
case ParserState.OpUns:
|
|
if (b == 'U' || b == 'u') c.State = ParserState.OpUnsu; else goto parseErr; break;
|
|
case ParserState.OpUnsu:
|
|
if (b == 'B' || b == 'b') c.State = ParserState.OpUnsub; else goto parseErr; break;
|
|
case ParserState.OpUnsub:
|
|
if (b == ' ' || b == '\t') c.State = ParserState.OpUnsubSpc; else goto parseErr; break;
|
|
case ParserState.OpUnsubSpc:
|
|
if (b == ' ' || b == '\t') break;
|
|
c.State = ParserState.UnsubArg;
|
|
c.ArgStart = i;
|
|
break;
|
|
case ParserState.UnsubArg:
|
|
switch (b)
|
|
{
|
|
case (byte)'\r': c.Drop = 1; break;
|
|
case (byte)'\n':
|
|
{
|
|
var arg = CollectArg(c, buf, i);
|
|
var err = OverMaxControlLineLimit(c, handler, arg, mcl);
|
|
if (err != null) return err;
|
|
var unsubTokens = SplitArgs(arg);
|
|
if (unsubTokens.Count is 0 or > 2)
|
|
return new InvalidOperationException(
|
|
$"processClientUnsub Parse Error '{Encoding.ASCII.GetString(arg)}'");
|
|
switch (kind)
|
|
{
|
|
case ClientKind.Client:
|
|
if (trace) handler.TraceInOp("UNSUB", arg);
|
|
err = handler.ProcessClientUnsub(arg);
|
|
break;
|
|
case ClientKind.Router:
|
|
{
|
|
if (trace)
|
|
{
|
|
var op = c.Op is (byte)'R' or (byte)'r' ? "RS-" : "LS-";
|
|
handler.TraceInOp(op, arg);
|
|
}
|
|
var leafUnsub = c.Op is (byte)'L' or (byte)'l';
|
|
err = handler.ProcessRemoteUnsub(arg, leafUnsub);
|
|
break;
|
|
}
|
|
case ClientKind.Gateway:
|
|
if (trace) handler.TraceInOp("RS-", arg);
|
|
err = handler.ProcessGatewayRUnsub(arg);
|
|
break;
|
|
case ClientKind.Leaf:
|
|
if (trace) handler.TraceInOp("LS-", arg);
|
|
err = handler.ProcessLeafUnsub(arg);
|
|
break;
|
|
}
|
|
if (err != null) return err;
|
|
c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.OpStart;
|
|
break;
|
|
}
|
|
default:
|
|
if (c.ArgBuf != null) AppendToArgBuf(c, b);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
// ---- PING ----
|
|
case ParserState.OpPi:
|
|
if (b == 'N' || b == 'n') c.State = ParserState.OpPin; else goto parseErr; break;
|
|
case ParserState.OpPin:
|
|
if (b == 'G' || b == 'g') c.State = ParserState.OpPing; else goto parseErr; break;
|
|
case ParserState.OpPing:
|
|
if (b == '\n')
|
|
{
|
|
if (trace) handler.TraceInOp("PING", null);
|
|
handler.ProcessPing();
|
|
c.Drop = 0; c.State = ParserState.OpStart;
|
|
}
|
|
break;
|
|
|
|
// ---- PONG ----
|
|
case ParserState.OpPo:
|
|
if (b == 'N' || b == 'n') c.State = ParserState.OpPon; else goto parseErr; break;
|
|
case ParserState.OpPon:
|
|
if (b == 'G' || b == 'g') c.State = ParserState.OpPong; else goto parseErr; break;
|
|
case ParserState.OpPong:
|
|
if (b == '\n')
|
|
{
|
|
if (trace) handler.TraceInOp("PONG", null);
|
|
handler.ProcessPong();
|
|
c.Drop = 0; c.State = ParserState.OpStart;
|
|
}
|
|
break;
|
|
|
|
// ---- CONNECT ----
|
|
case ParserState.OpC:
|
|
if (b == 'O' || b == 'o') c.State = ParserState.OpCo; else goto parseErr; break;
|
|
case ParserState.OpCo:
|
|
if (b == 'N' || b == 'n') c.State = ParserState.OpCon; else goto parseErr; break;
|
|
case ParserState.OpCon:
|
|
if (b == 'N' || b == 'n') c.State = ParserState.OpConn; else goto parseErr; break;
|
|
case ParserState.OpConn:
|
|
if (b == 'E' || b == 'e') c.State = ParserState.OpConne; else goto parseErr; break;
|
|
case ParserState.OpConne:
|
|
if (b == 'C' || b == 'c') c.State = ParserState.OpConnec; else goto parseErr; break;
|
|
case ParserState.OpConnec:
|
|
if (b == 'T' || b == 't') c.State = ParserState.OpConnect; else goto parseErr; break;
|
|
case ParserState.OpConnect:
|
|
if (b == ' ' || b == '\t') break;
|
|
c.State = ParserState.ConnectArg;
|
|
c.ArgStart = i;
|
|
break;
|
|
case ParserState.ConnectArg:
|
|
switch (b)
|
|
{
|
|
case (byte)'\r': c.Drop = 1; break;
|
|
case (byte)'\n':
|
|
{
|
|
var arg = CollectArg(c, buf, i);
|
|
var err = OverMaxControlLineLimit(c, handler, arg, mcl);
|
|
if (err != null) return err;
|
|
if (SplitArgs(arg).Count == 0) goto parseErr;
|
|
if (trace) handler.TraceInOp("CONNECT", arg);
|
|
err = handler.ProcessConnect(arg);
|
|
if (err != null) return err;
|
|
c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.OpStart;
|
|
authSet = handler.IsAwaitingAuth;
|
|
break;
|
|
}
|
|
default:
|
|
if (c.ArgBuf != null) AppendToArgBuf(c, b);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
// ---- MSG (route/leaf/gateway) ----
|
|
case ParserState.OpM:
|
|
if (b == 'S' || b == 's') c.State = ParserState.OpMs; else goto parseErr; break;
|
|
case ParserState.OpMs:
|
|
if (b == 'G' || b == 'g') c.State = ParserState.OpMsg; else goto parseErr; break;
|
|
case ParserState.OpMsg:
|
|
if (b == ' ' || b == '\t') c.State = ParserState.OpMsgSpc; else goto parseErr; break;
|
|
case ParserState.OpMsgSpc:
|
|
if (b == ' ' || b == '\t') break;
|
|
c.Pa.HeaderSize = -1;
|
|
c.State = ParserState.MsgArg;
|
|
c.ArgStart = i;
|
|
break;
|
|
case ParserState.MsgArg:
|
|
switch (b)
|
|
{
|
|
case (byte)'\r': c.Drop = 1; break;
|
|
case (byte)'\n':
|
|
{
|
|
var arg = CollectArg(c, buf, i);
|
|
var err = OverMaxControlLineLimit(c, handler, arg, mcl);
|
|
if (err != null) return err;
|
|
if (kind == ClientKind.Router || kind == ClientKind.Gateway)
|
|
{
|
|
if (c.Op is (byte)'R' or (byte)'r')
|
|
{
|
|
if (trace) handler.TraceInOp("RMSG", arg);
|
|
err = ProcessRoutedMsgArgs(c, arg);
|
|
}
|
|
else
|
|
{
|
|
if (trace) handler.TraceInOp("LMSG", arg);
|
|
lmsg = true;
|
|
err = ProcessRoutedOriginClusterMsgArgs(c, arg);
|
|
}
|
|
}
|
|
else if (kind == ClientKind.Leaf)
|
|
{
|
|
if (trace) handler.TraceInOp("LMSG", arg);
|
|
err = ProcessLeafMsgArgs(c, arg);
|
|
}
|
|
if (err != null) return err;
|
|
c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.MsgPayload;
|
|
i = c.ArgStart + c.Pa.Size - ServerConstants.LenCrLf;
|
|
break;
|
|
}
|
|
default:
|
|
if (c.ArgBuf != null) AppendToArgBuf(c, b);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
// ---- INFO ----
|
|
case ParserState.OpI:
|
|
if (b == 'N' || b == 'n') c.State = ParserState.OpIn; else goto parseErr; break;
|
|
case ParserState.OpIn:
|
|
if (b == 'F' || b == 'f') c.State = ParserState.OpInf; else goto parseErr; break;
|
|
case ParserState.OpInf:
|
|
if (b == 'O' || b == 'o') c.State = ParserState.OpInfo; else goto parseErr; break;
|
|
case ParserState.OpInfo:
|
|
if (b == ' ' || b == '\t') break;
|
|
c.State = ParserState.InfoArg;
|
|
c.ArgStart = i;
|
|
break;
|
|
case ParserState.InfoArg:
|
|
switch (b)
|
|
{
|
|
case (byte)'\r': c.Drop = 1; break;
|
|
case (byte)'\n':
|
|
{
|
|
var arg = CollectArg(c, buf, i);
|
|
var err = OverMaxControlLineLimit(c, handler, arg, mcl);
|
|
if (err != null) return err;
|
|
if (SplitArgs(arg).Count == 0) goto parseErr;
|
|
err = handler.ProcessInfo(arg);
|
|
if (err != null) return err;
|
|
c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.OpStart;
|
|
break;
|
|
}
|
|
default:
|
|
if (c.ArgBuf != null) AppendToArgBuf(c, b);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
// ---- +OK ----
|
|
case ParserState.OpPlus:
|
|
if (b == 'O' || b == 'o') c.State = ParserState.OpPlusO; else goto parseErr; break;
|
|
case ParserState.OpPlusO:
|
|
if (b == 'K' || b == 'k') c.State = ParserState.OpPlusOk; else goto parseErr; break;
|
|
case ParserState.OpPlusOk:
|
|
if (b == '\n') { c.Drop = 0; c.State = ParserState.OpStart; }
|
|
break;
|
|
|
|
// ---- -ERR ----
|
|
case ParserState.OpMinus:
|
|
if (b == 'E' || b == 'e') c.State = ParserState.OpMinusE; else goto parseErr; break;
|
|
case ParserState.OpMinusE:
|
|
if (b == 'R' || b == 'r') c.State = ParserState.OpMinusEr; else goto parseErr; break;
|
|
case ParserState.OpMinusEr:
|
|
if (b == 'R' || b == 'r') c.State = ParserState.OpMinusErr; else goto parseErr; break;
|
|
case ParserState.OpMinusErr:
|
|
if (b == ' ' || b == '\t') c.State = ParserState.OpMinusErrSpc; else goto parseErr; break;
|
|
case ParserState.OpMinusErrSpc:
|
|
if (b == ' ' || b == '\t') break;
|
|
c.State = ParserState.MinusErrArg;
|
|
c.ArgStart = i;
|
|
break;
|
|
case ParserState.MinusErrArg:
|
|
switch (b)
|
|
{
|
|
case (byte)'\r': c.Drop = 1; break;
|
|
case (byte)'\n':
|
|
{
|
|
var arg = CollectArg(c, buf, i);
|
|
var err = OverMaxControlLineLimit(c, handler, arg, mcl);
|
|
if (err != null) return err;
|
|
handler.ProcessErr(Encoding.ASCII.GetString(arg));
|
|
c.Drop = 0; c.ArgStart = i + 1; c.State = ParserState.OpStart;
|
|
break;
|
|
}
|
|
default:
|
|
if (c.ArgBuf != null) AppendToArgBuf(c, b);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
goto parseErr;
|
|
}
|
|
}
|
|
|
|
// --- Split buffer handling ---
|
|
|
|
// Check for split buffer scenarios for any ARG state.
|
|
if (c.State is ParserState.SubArg or ParserState.UnsubArg
|
|
or ParserState.PubArg or ParserState.HpubArg
|
|
or ParserState.AsubArg or ParserState.AusubArg
|
|
or ParserState.MsgArg or ParserState.HmsgArg
|
|
or ParserState.MinusErrArg or ParserState.ConnectArg or ParserState.InfoArg)
|
|
{
|
|
if (c.ArgBuf == null)
|
|
{
|
|
var end = buf.Length - c.Drop;
|
|
c.ArgBuf = new byte[end - c.ArgStart];
|
|
Array.Copy(buf, c.ArgStart, c.ArgBuf, 0, c.ArgBuf.Length);
|
|
}
|
|
var err = OverMaxControlLineLimit(c, handler, c.ArgBuf, mcl);
|
|
if (err != null) return err;
|
|
}
|
|
|
|
// Check for split msg
|
|
if (c.State is ParserState.MsgPayload or ParserState.MsgEndR or ParserState.MsgEndN
|
|
&& c.MsgBuf == null)
|
|
{
|
|
if (c.ArgBuf == null)
|
|
{
|
|
var cloneErr = ClonePubArg(c, handler, lmsg);
|
|
if (cloneErr != null) goto parseErr;
|
|
}
|
|
|
|
var lrem = buf.Length - c.ArgStart;
|
|
if (lrem > c.Pa.Size + ServerConstants.LenCrLf)
|
|
goto parseErr;
|
|
c.MsgBuf = new byte[lrem];
|
|
Array.Copy(buf, c.ArgStart, c.MsgBuf, 0, lrem);
|
|
}
|
|
|
|
return null;
|
|
|
|
authErr:
|
|
handler.AuthViolation();
|
|
return ServerErrors.ErrAuthentication;
|
|
|
|
parseErr:
|
|
handler.SendErr("Unknown Protocol Operation");
|
|
var snip = ProtoSnippet(buf.Length > 0 ? Math.Min(buf.Length - 1, Math.Max(0, 0)) : 0,
|
|
ServerConstants.ProtoSnippetSize, buf);
|
|
return new InvalidOperationException(
|
|
$"{handler.KindString()} parser ERROR, state={(int)c.State}: proto='{snip}...'");
|
|
}
|
|
|
|
// =====================================================================
|
|
// ProtoSnippet
|
|
// =====================================================================
|
|
|
|
/// <summary>
|
|
/// Returns a quoted snippet of the protocol buffer for error messages.
|
|
/// Mirrors Go <c>protoSnippet(start, max int, buf []byte) string</c>.
|
|
/// </summary>
|
|
public static string ProtoSnippet(int start, int max, byte[] buf)
|
|
{
|
|
var stop = start + max;
|
|
var bufSize = buf.Length;
|
|
if (start >= bufSize)
|
|
return "\"\"";
|
|
if (stop > bufSize)
|
|
stop = bufSize - 1;
|
|
return $"\"{Encoding.ASCII.GetString(buf, start, stop - start)}\"";
|
|
}
|
|
|
|
// =====================================================================
|
|
// OverMaxControlLineLimit
|
|
// =====================================================================
|
|
|
|
/// <summary>
|
|
/// Checks if the argument exceeds the max control line limit.
|
|
/// Only enforced for CLIENT connections.
|
|
/// Mirrors Go <c>client.overMaxControlLineLimit</c>.
|
|
/// </summary>
|
|
public static Exception? OverMaxControlLineLimit(ParseContext c, IProtocolHandler handler, byte[] arg, int mcl)
|
|
{
|
|
if (c.Kind != ClientKind.Client)
|
|
return null;
|
|
if (arg.Length > mcl)
|
|
{
|
|
var snip = ProtoSnippet(0, ServerConstants.MaxControlLineSnippetSize, arg);
|
|
var err = ErrorContextHelper.NewErrorCtx(ServerErrors.ErrMaxControlLine,
|
|
"State {0}, max_control_line {1}, Buffer len {2} (snip: {3}...)",
|
|
(int)c.State, mcl, c.ArgBuf?.Length ?? arg.Length, snip);
|
|
handler.SendErr(err.Message);
|
|
handler.CloseConnection(0); // MaxControlLineExceeded
|
|
return err;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// =====================================================================
|
|
// ProcessPub — parse PUB arguments
|
|
// =====================================================================
|
|
|
|
/// <summary>
|
|
/// Parses PUB protocol arguments: "subject [reply] size".
|
|
/// Mirrors Go <c>client.processPub</c> from client.go.
|
|
/// </summary>
|
|
public static Exception? ProcessPub(ParseContext c, byte[] arg)
|
|
{
|
|
var tokens = SplitArgs(arg);
|
|
byte[]? subject, reply, szb;
|
|
|
|
switch (tokens.Count)
|
|
{
|
|
case 2:
|
|
subject = tokens[0];
|
|
reply = null;
|
|
szb = tokens[1];
|
|
break;
|
|
case 3:
|
|
subject = tokens[0];
|
|
reply = tokens[1];
|
|
szb = tokens[2];
|
|
break;
|
|
default:
|
|
return new InvalidOperationException(
|
|
$"processPub error: {ProtoSnippet(0, ServerConstants.ProtoSnippetSize, arg)}");
|
|
}
|
|
|
|
if (subject.Length == 0)
|
|
return new InvalidOperationException(
|
|
$"processPub error: empty subject {ProtoSnippet(0, ServerConstants.ProtoSnippetSize, arg)}");
|
|
|
|
if (!TryParseSize(szb, out var size))
|
|
return new InvalidOperationException(
|
|
$"processPub error: bad size {ProtoSnippet(0, ServerConstants.ProtoSnippetSize, arg)}");
|
|
|
|
if (c.MaxPayload >= 0 && size > c.MaxPayload)
|
|
return ServerErrors.ErrMaxPayload;
|
|
|
|
c.Pa.Subject = subject;
|
|
c.Pa.Reply = reply;
|
|
c.Pa.SizeBytes = szb;
|
|
c.Pa.Size = size;
|
|
c.Pa.Arg = arg;
|
|
return null;
|
|
}
|
|
|
|
// =====================================================================
|
|
// ProcessHeaderPub — parse HPUB arguments
|
|
// =====================================================================
|
|
|
|
/// <summary>
|
|
/// Parses HPUB protocol arguments: "subject [reply] hdr_size total_size".
|
|
/// Mirrors Go <c>client.processHeaderPub</c> from client.go.
|
|
/// </summary>
|
|
public static Exception? ProcessHeaderPub(ParseContext c, byte[] arg, byte[]? remaining)
|
|
{
|
|
if (!c.HasHeaders)
|
|
return ServerErrors.ErrMsgHeadersNotSupported;
|
|
|
|
var tokens = SplitArgs(arg);
|
|
byte[]? subject, reply, hdb, szb;
|
|
|
|
switch (tokens.Count)
|
|
{
|
|
case 3:
|
|
subject = tokens[0];
|
|
reply = null;
|
|
hdb = tokens[1];
|
|
szb = tokens[2];
|
|
break;
|
|
case 4:
|
|
subject = tokens[0];
|
|
reply = tokens[1];
|
|
hdb = tokens[2];
|
|
szb = tokens[3];
|
|
break;
|
|
default:
|
|
return new InvalidOperationException(
|
|
$"processHeaderPub error: {ProtoSnippet(0, ServerConstants.ProtoSnippetSize, arg)}");
|
|
}
|
|
|
|
if (subject.Length == 0)
|
|
return new InvalidOperationException("processHeaderPub error: empty subject");
|
|
|
|
if (!TryParseSize(hdb, out var hdr))
|
|
return new InvalidOperationException("processHeaderPub error: bad header size");
|
|
if (!TryParseSize(szb, out var size))
|
|
return new InvalidOperationException("processHeaderPub error: bad size");
|
|
|
|
if (hdr > size)
|
|
return ServerErrors.ErrBadMsgHeader;
|
|
|
|
if (c.MaxPayload >= 0 && size > c.MaxPayload)
|
|
return ServerErrors.ErrMaxPayload;
|
|
|
|
c.Pa.Subject = subject;
|
|
c.Pa.Reply = reply;
|
|
c.Pa.HeaderBytes = hdb;
|
|
c.Pa.SizeBytes = szb;
|
|
c.Pa.HeaderSize = hdr;
|
|
c.Pa.Size = size;
|
|
c.Pa.Arg = arg;
|
|
return null;
|
|
}
|
|
|
|
// =====================================================================
|
|
// ProcessRoutedMsgArgs — parse RMSG arguments
|
|
// =====================================================================
|
|
|
|
/// <summary>
|
|
/// Parses RMSG protocol arguments: "account subject [+ reply] [| queue...] [reply] size".
|
|
/// Mirrors Go <c>client.processRoutedMsgArgs</c> from client.go.
|
|
/// </summary>
|
|
public static Exception? ProcessRoutedMsgArgs(ParseContext c, byte[] arg)
|
|
{
|
|
var tokens = SplitArgs(arg);
|
|
if (tokens.Count < 3)
|
|
return new InvalidOperationException("processRoutedMsgArgs error: not enough args");
|
|
|
|
c.Pa.Account = tokens[0];
|
|
c.Pa.Subject = tokens[1];
|
|
|
|
if (tokens.Count >= 5 && tokens[2].Length == 1 && tokens[2][0] == '+')
|
|
{
|
|
// + reply queues... size
|
|
c.Pa.Reply = tokens[3];
|
|
c.Pa.Queues = [];
|
|
for (var j = 4; j < tokens.Count - 1; j++)
|
|
c.Pa.Queues.Add(tokens[j]);
|
|
var szb = tokens[^1];
|
|
if (!TryParseSize(szb, out var size))
|
|
return new InvalidOperationException("processRoutedMsgArgs error: bad size");
|
|
c.Pa.SizeBytes = szb;
|
|
c.Pa.Size = size;
|
|
}
|
|
else if (tokens.Count >= 4 && tokens[2].Length == 1 && tokens[2][0] == '|')
|
|
{
|
|
// | queues... size (no reply)
|
|
c.Pa.Reply = [];;
|
|
c.Pa.Queues = [];
|
|
for (var j = 3; j < tokens.Count - 1; j++)
|
|
c.Pa.Queues.Add(tokens[j]);
|
|
var szb = tokens[^1];
|
|
if (!TryParseSize(szb, out var size))
|
|
return new InvalidOperationException("processRoutedMsgArgs error: bad size");
|
|
c.Pa.SizeBytes = szb;
|
|
c.Pa.Size = size;
|
|
}
|
|
else if (tokens.Count == 4)
|
|
{
|
|
// account subject reply size
|
|
c.Pa.Reply = tokens[2];
|
|
var szb = tokens[3];
|
|
if (!TryParseSize(szb, out var size))
|
|
return new InvalidOperationException("processRoutedMsgArgs error: bad size");
|
|
c.Pa.SizeBytes = szb;
|
|
c.Pa.Size = size;
|
|
}
|
|
else if (tokens.Count == 3)
|
|
{
|
|
// account subject size
|
|
c.Pa.Reply = null;
|
|
var szb = tokens[2];
|
|
if (!TryParseSize(szb, out var size))
|
|
return new InvalidOperationException("processRoutedMsgArgs error: bad size");
|
|
c.Pa.SizeBytes = szb;
|
|
c.Pa.Size = size;
|
|
}
|
|
else
|
|
{
|
|
return new InvalidOperationException("processRoutedMsgArgs error: bad args");
|
|
}
|
|
|
|
c.Pa.Arg = arg;
|
|
return null;
|
|
}
|
|
|
|
// =====================================================================
|
|
// ProcessRoutedHeaderMsgArgs — parse route HMSG arguments
|
|
// =====================================================================
|
|
|
|
/// <summary>
|
|
/// Parses route HMSG protocol arguments: "account subject [+ reply queues...] [| queues...] [reply] hdr_size total_size".
|
|
/// Mirrors Go <c>client.processRoutedHeaderMsgArgs</c> from client.go.
|
|
/// </summary>
|
|
public static Exception? ProcessRoutedHeaderMsgArgs(ParseContext c, byte[] arg)
|
|
{
|
|
var tokens = SplitArgs(arg);
|
|
if (tokens.Count < 4)
|
|
return new InvalidOperationException("processRoutedHeaderMsgArgs error: not enough args");
|
|
|
|
c.Pa.Account = tokens[0];
|
|
c.Pa.Subject = tokens[1];
|
|
|
|
if (tokens.Count >= 6 && tokens[2].Length == 1 && tokens[2][0] == '+')
|
|
{
|
|
// + reply queues... hdr_size total_size
|
|
c.Pa.Reply = tokens[3];
|
|
c.Pa.Queues = [];
|
|
for (var j = 4; j < tokens.Count - 2; j++)
|
|
c.Pa.Queues.Add(tokens[j]);
|
|
if (!TryParseSize(tokens[^2], out var hdr))
|
|
return new InvalidOperationException("processRoutedHeaderMsgArgs error: bad hdr size");
|
|
if (!TryParseSize(tokens[^1], out var size))
|
|
return new InvalidOperationException("processRoutedHeaderMsgArgs error: bad size");
|
|
if (hdr > size) return ServerErrors.ErrBadMsgHeader;
|
|
c.Pa.HeaderSize = hdr;
|
|
c.Pa.SizeBytes = tokens[^1];
|
|
c.Pa.Size = size;
|
|
}
|
|
else if (tokens.Count >= 5 && tokens[2].Length == 1 && tokens[2][0] == '|')
|
|
{
|
|
// | queues... hdr_size total_size (no reply)
|
|
c.Pa.Reply = [];
|
|
c.Pa.Queues = [];
|
|
for (var j = 3; j < tokens.Count - 2; j++)
|
|
c.Pa.Queues.Add(tokens[j]);
|
|
if (!TryParseSize(tokens[^2], out var hdr))
|
|
return new InvalidOperationException("processRoutedHeaderMsgArgs error: bad hdr size");
|
|
if (!TryParseSize(tokens[^1], out var size))
|
|
return new InvalidOperationException("processRoutedHeaderMsgArgs error: bad size");
|
|
if (hdr > size) return ServerErrors.ErrBadMsgHeader;
|
|
c.Pa.HeaderSize = hdr;
|
|
c.Pa.SizeBytes = tokens[^1];
|
|
c.Pa.Size = size;
|
|
}
|
|
else if (tokens.Count == 5)
|
|
{
|
|
// account subject reply hdr_size total_size
|
|
c.Pa.Reply = tokens[2];
|
|
if (!TryParseSize(tokens[3], out var hdr))
|
|
return new InvalidOperationException("processRoutedHeaderMsgArgs error: bad hdr size");
|
|
if (!TryParseSize(tokens[4], out var size))
|
|
return new InvalidOperationException("processRoutedHeaderMsgArgs error: bad size");
|
|
if (hdr > size) return ServerErrors.ErrBadMsgHeader;
|
|
c.Pa.HeaderSize = hdr;
|
|
c.Pa.SizeBytes = tokens[4];
|
|
c.Pa.Size = size;
|
|
}
|
|
else if (tokens.Count == 4)
|
|
{
|
|
// account subject hdr_size total_size
|
|
c.Pa.Reply = null;
|
|
if (!TryParseSize(tokens[2], out var hdr))
|
|
return new InvalidOperationException("processRoutedHeaderMsgArgs error: bad hdr size");
|
|
if (!TryParseSize(tokens[3], out var size))
|
|
return new InvalidOperationException("processRoutedHeaderMsgArgs error: bad size");
|
|
if (hdr > size) return ServerErrors.ErrBadMsgHeader;
|
|
c.Pa.HeaderSize = hdr;
|
|
c.Pa.SizeBytes = tokens[3];
|
|
c.Pa.Size = size;
|
|
}
|
|
else
|
|
{
|
|
return new InvalidOperationException("processRoutedHeaderMsgArgs error: bad args");
|
|
}
|
|
|
|
c.Pa.Arg = arg;
|
|
return null;
|
|
}
|
|
|
|
// =====================================================================
|
|
// ProcessLeafMsgArgs / ProcessLeafHeaderMsgArgs — stubs
|
|
// =====================================================================
|
|
|
|
/// <summary>
|
|
/// Parses leaf MSG arguments. Same format as routed MSG args.
|
|
/// Stub — will be fully implemented with leaf node support.
|
|
/// </summary>
|
|
public static Exception? ProcessLeafMsgArgs(ParseContext c, byte[] arg) =>
|
|
ProcessRoutedMsgArgs(c, arg);
|
|
|
|
/// <summary>
|
|
/// Parses leaf HMSG arguments. Same format as routed header MSG args.
|
|
/// Stub — will be fully implemented with leaf node support.
|
|
/// </summary>
|
|
public static Exception? ProcessLeafHeaderMsgArgs(ParseContext c, byte[] arg) =>
|
|
ProcessRoutedHeaderMsgArgs(c, arg);
|
|
|
|
/// <summary>
|
|
/// Parses LMSG arguments (origin cluster routed messages).
|
|
/// Stub — will be fully implemented with cluster routing support.
|
|
/// </summary>
|
|
public static Exception? ProcessRoutedOriginClusterMsgArgs(ParseContext c, byte[] arg) =>
|
|
ProcessRoutedMsgArgs(c, arg);
|
|
|
|
// =====================================================================
|
|
// ClonePubArg
|
|
// =====================================================================
|
|
|
|
/// <summary>
|
|
/// Clones the pub arg by re-processing the original arg buffer.
|
|
/// Used in split buffer scenarios.
|
|
/// Mirrors Go <c>client.clonePubArg</c>.
|
|
/// </summary>
|
|
public static Exception? ClonePubArg(ParseContext c, IProtocolHandler handler, bool lmsg)
|
|
{
|
|
if (c.Pa.Arg == null)
|
|
return null;
|
|
|
|
c.ArgBuf = (byte[])c.Pa.Arg.Clone();
|
|
var kind = c.Kind;
|
|
|
|
if (kind == ClientKind.Router || kind == ClientKind.Gateway)
|
|
{
|
|
if (lmsg)
|
|
return ProcessRoutedOriginClusterMsgArgs(c, c.ArgBuf);
|
|
return c.Pa.HeaderSize < 0
|
|
? ProcessRoutedMsgArgs(c, c.ArgBuf)
|
|
: ProcessRoutedHeaderMsgArgs(c, c.ArgBuf);
|
|
}
|
|
|
|
if (kind == ClientKind.Leaf)
|
|
{
|
|
return c.Pa.HeaderSize < 0
|
|
? ProcessLeafMsgArgs(c, c.ArgBuf)
|
|
: ProcessLeafHeaderMsgArgs(c, c.ArgBuf);
|
|
}
|
|
|
|
return c.Pa.HeaderSize < 0
|
|
? ProcessPub(c, c.ArgBuf)
|
|
: ProcessHeaderPub(c, c.ArgBuf, null);
|
|
}
|
|
|
|
// =====================================================================
|
|
// GetHeader
|
|
// =====================================================================
|
|
|
|
/// <summary>
|
|
/// Parses message headers from the message buffer.
|
|
/// Mirrors Go <c>parseState.getHeader</c>.
|
|
/// </summary>
|
|
public static Dictionary<string, List<string>>? GetHeader(ParseContext c)
|
|
{
|
|
if (c.Pa.HeaderSize <= 0 || c.MsgBuf == null)
|
|
return null;
|
|
|
|
var headerBytes = c.MsgBuf.AsSpan(0, c.Pa.HeaderSize);
|
|
var result = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
|
var text = Encoding.ASCII.GetString(headerBytes);
|
|
var lines = text.Split('\n');
|
|
|
|
// Skip first line (contains version, e.g. "NATS/1.0\r")
|
|
for (var i = 1; i < lines.Length; i++)
|
|
{
|
|
var line = lines[i].TrimEnd('\r');
|
|
if (string.IsNullOrEmpty(line))
|
|
break;
|
|
var colonIdx = line.IndexOf(':');
|
|
if (colonIdx <= 0) continue;
|
|
var key = line[..colonIdx].Trim();
|
|
var value = line[(colonIdx + 1)..].Trim();
|
|
if (!result.TryGetValue(key, out var values))
|
|
{
|
|
values = [];
|
|
result[key] = values;
|
|
}
|
|
values.Add(value);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// =====================================================================
|
|
// Internal helpers
|
|
// =====================================================================
|
|
|
|
/// <summary>Collects the argument bytes, using argBuf if available (split buffer case).</summary>
|
|
private static byte[] CollectArg(ParseContext c, byte[] buf, int i)
|
|
{
|
|
if (c.ArgBuf != null)
|
|
{
|
|
var arg = c.ArgBuf;
|
|
c.ArgBuf = null;
|
|
return arg;
|
|
}
|
|
|
|
var end = i - c.Drop;
|
|
var len = end - c.ArgStart;
|
|
if (len <= 0) return [];
|
|
var result = new byte[len];
|
|
Array.Copy(buf, c.ArgStart, result, 0, len);
|
|
return result;
|
|
}
|
|
|
|
/// <summary>Appends a byte to the arg buffer.</summary>
|
|
private static void AppendToArgBuf(ParseContext c, byte b)
|
|
{
|
|
var old = c.ArgBuf!;
|
|
var newBuf = new byte[old.Length + 1];
|
|
Array.Copy(old, newBuf, old.Length);
|
|
newBuf[^1] = b;
|
|
c.ArgBuf = newBuf;
|
|
}
|
|
|
|
/// <summary>Appends a byte to the msg buffer.</summary>
|
|
private static void AppendToMsgBuf(ParseContext c, byte b)
|
|
{
|
|
var old = c.MsgBuf!;
|
|
var newBuf = new byte[old.Length + 1];
|
|
Array.Copy(old, newBuf, old.Length);
|
|
newBuf[^1] = b;
|
|
c.MsgBuf = newBuf;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Splits argument bytes into tokens separated by spaces/tabs.
|
|
/// Mirrors the "unrolled splitArgs" pattern in Go client.go.
|
|
/// </summary>
|
|
internal static List<byte[]> SplitArgs(byte[] arg)
|
|
{
|
|
var tokens = new List<byte[]>(6);
|
|
var i = 0;
|
|
var len = arg.Length;
|
|
|
|
while (i < len)
|
|
{
|
|
// Skip whitespace (including \r and \n, matching Go's splitArg)
|
|
while (i < len && (arg[i] == ' ' || arg[i] == '\t' || arg[i] == '\r' || arg[i] == '\n'))
|
|
i++;
|
|
if (i >= len) break;
|
|
|
|
// Collect token
|
|
var start = i;
|
|
while (i < len && arg[i] != ' ' && arg[i] != '\t' && arg[i] != '\r' && arg[i] != '\n')
|
|
i++;
|
|
tokens.Add(arg[start..i]);
|
|
}
|
|
|
|
return tokens;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a size value from ASCII digit bytes. Returns false on overflow or non-digit chars.
|
|
/// </summary>
|
|
internal static bool TryParseSize(byte[] bytes, out int size)
|
|
{
|
|
size = 0;
|
|
if (bytes.Length == 0) return false;
|
|
|
|
foreach (var b in bytes)
|
|
{
|
|
if (b < '0' || b > '9') return false;
|
|
var prev = size;
|
|
size = size * 10 + (b - '0');
|
|
if (size < prev) return false; // overflow
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>Helper to convert nullable byte[] to string.</summary>
|
|
private static string AsString(byte[]? bytes) =>
|
|
bytes == null ? "" : Encoding.ASCII.GetString(bytes);
|
|
}
|