// 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;
///
/// NATS wire protocol parser — byte-by-byte state machine.
/// Mirrors Go client.parse, protoSnippet, overMaxControlLineLimit,
/// clonePubArg, and parseState.getHeader from parser.go.
///
public static class ProtocolParser
{
// =====================================================================
// Parse — main state machine
// =====================================================================
///
/// Parses raw bytes through the NATS protocol state machine.
/// Mirrors Go client.parse(buf []byte) error.
///
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
// =====================================================================
///
/// Returns a quoted snippet of the protocol buffer for error messages.
/// Mirrors Go protoSnippet(start, max int, buf []byte) string.
///
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
// =====================================================================
///
/// Checks if the argument exceeds the max control line limit.
/// Only enforced for CLIENT connections.
/// Mirrors Go client.overMaxControlLineLimit.
///
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
// =====================================================================
///
/// Parses PUB protocol arguments: "subject [reply] size".
/// Mirrors Go client.processPub from client.go.
///
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
// =====================================================================
///
/// Parses HPUB protocol arguments: "subject [reply] hdr_size total_size".
/// Mirrors Go client.processHeaderPub from client.go.
///
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
// =====================================================================
///
/// Parses RMSG protocol arguments: "account subject [+ reply] [| queue...] [reply] size".
/// Mirrors Go client.processRoutedMsgArgs from client.go.
///
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
// =====================================================================
///
/// Parses route HMSG protocol arguments: "account subject [+ reply queues...] [| queues...] [reply] hdr_size total_size".
/// Mirrors Go client.processRoutedHeaderMsgArgs from client.go.
///
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
// =====================================================================
///
/// Parses leaf MSG arguments. Same format as routed MSG args.
/// Stub — will be fully implemented with leaf node support.
///
public static Exception? ProcessLeafMsgArgs(ParseContext c, byte[] arg) =>
ProcessRoutedMsgArgs(c, arg);
///
/// Parses leaf HMSG arguments. Same format as routed header MSG args.
/// Stub — will be fully implemented with leaf node support.
///
public static Exception? ProcessLeafHeaderMsgArgs(ParseContext c, byte[] arg) =>
ProcessRoutedHeaderMsgArgs(c, arg);
///
/// Parses LMSG arguments (origin cluster routed messages).
/// Stub — will be fully implemented with cluster routing support.
///
public static Exception? ProcessRoutedOriginClusterMsgArgs(ParseContext c, byte[] arg) =>
ProcessRoutedMsgArgs(c, arg);
// =====================================================================
// ClonePubArg
// =====================================================================
///
/// Clones the pub arg by re-processing the original arg buffer.
/// Used in split buffer scenarios.
/// Mirrors Go client.clonePubArg.
///
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
// =====================================================================
///
/// Parses message headers from the message buffer.
/// Mirrors Go parseState.getHeader.
///
public static Dictionary>? 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>(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
// =====================================================================
/// Collects the argument bytes, using argBuf if available (split buffer case).
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;
}
/// Appends a byte to the arg buffer.
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;
}
/// Appends a byte to the msg buffer.
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;
}
///
/// Splits argument bytes into tokens separated by spaces/tabs.
/// Mirrors the "unrolled splitArgs" pattern in Go client.go.
///
internal static List SplitArgs(byte[] arg)
{
var tokens = new List(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;
}
///
/// Parses a size value from ASCII digit bytes. Returns false on overflow or non-digit chars.
///
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;
}
/// Helper to convert nullable byte[] to string.
private static string AsString(byte[]? bytes) =>
bytes == null ? "" : Encoding.ASCII.GetString(bytes);
}