Files
natsdotnet/src/NATS.Server/Configuration/NatsConfLexer.cs

1504 lines
36 KiB
C#

// Port of Go conf/lex.go — state-machine tokenizer for NATS config files.
// Reference: golang/nats-server/conf/lex.go
namespace NATS.Server.Configuration;
public sealed class NatsConfLexer
{
private const char Eof = '\0';
private const char MapStartChar = '{';
private const char MapEndChar = '}';
private const char KeySepEqual = '=';
private const char KeySepColon = ':';
private const char ArrayStartChar = '[';
private const char ArrayEndChar = ']';
private const char ArrayValTerm = ',';
private const char MapValTerm = ',';
private const char CommentHashStart = '#';
private const char CommentSlashStart = '/';
private const char DqStringStart = '"';
private const char DqStringEnd = '"';
private const char SqStringStart = '\'';
private const char SqStringEnd = '\'';
private const char OptValTerm = ';';
private const char TopOptStart = '{';
private const char TopOptTerm = '}';
private const char TopOptValTerm = ',';
private const char BlockStartChar = '(';
private const char BlockEndChar = ')';
private delegate LexState? LexState(NatsConfLexer lx);
private readonly string _input;
private int _start;
private int _pos;
private int _width;
private int _line;
private readonly List<Token> _items;
private readonly Stack<LexState> _stack;
private readonly List<string> _stringParts;
private LexState? _stringStateFn;
// Start position of the current line (after newline character).
private int _lstart;
// Start position of the line from the current item.
private int _ilstart;
private NatsConfLexer(string input)
{
_input = input;
_start = 0;
_pos = 0;
_width = 0;
_line = 1;
_items = [];
_stack = new Stack<LexState>();
_stringParts = [];
_lstart = 0;
_ilstart = 0;
}
public static IReadOnlyList<Token> Tokenize(string input)
{
ArgumentNullException.ThrowIfNull(input);
var lx = new NatsConfLexer(input);
LexState? state = LexTop;
while (state is not null)
{
state = state(lx);
}
return lx._items;
}
private void Push(LexState state) => _stack.Push(state);
private LexState? Pop()
{
if (_stack.Count == 0)
{
return Errorf("BUG in lexer: no states to pop.");
}
return _stack.Pop();
}
private void Emit(TokenType type)
{
string val;
if (_stringParts.Count > 0)
{
val = string.Concat(_stringParts) + _input[_start.._pos];
_stringParts.Clear();
}
else
{
val = _input[_start.._pos];
}
var pos = _pos - _ilstart - val.Length;
_items.Add(new Token(type, val, _line, pos));
_start = _pos;
_ilstart = _lstart;
}
private void EmitString()
{
string finalString;
if (_stringParts.Count > 0)
{
finalString = string.Concat(_stringParts) + _input[_start.._pos];
_stringParts.Clear();
}
else
{
finalString = _input[_start.._pos];
}
var pos = _pos - _ilstart - finalString.Length;
_items.Add(new Token(TokenType.String, finalString, _line, pos));
_start = _pos;
_ilstart = _lstart;
}
private void AddCurrentStringPart(int offset)
{
_stringParts.Add(_input[_start..(_pos - offset)]);
_start = _pos;
}
private LexState? AddStringPart(string s)
{
_stringParts.Add(s);
_start = _pos;
return _stringStateFn;
}
private bool HasEscapedParts() => _stringParts.Count > 0;
private char Next()
{
if (_pos >= _input.Length)
{
_width = 0;
return Eof;
}
if (_input[_pos] == '\n')
{
_line++;
_lstart = _pos;
}
var c = _input[_pos];
_width = 1;
_pos += _width;
return c;
}
private void Ignore()
{
_start = _pos;
_ilstart = _lstart;
}
private void Backup()
{
_pos -= _width;
if (_pos < _input.Length && _input[_pos] == '\n')
{
_line--;
}
}
private char Peek()
{
var r = Next();
Backup();
return r;
}
private LexState? Errorf(string message)
{
var pos = _pos - _lstart;
_items.Add(new Token(TokenType.Error, message, _line, pos));
return null;
}
// --- Helper methods ---
private static bool IsWhitespace(char c) => c is '\t' or ' ';
private static bool IsNL(char c) => c is '\n' or '\r';
private static bool IsKeySeparator(char c) => c is KeySepEqual or KeySepColon;
private static bool IsNumberSuffix(char c) =>
c is 'k' or 'K' or 'm' or 'M' or 'g' or 'G' or 't' or 'T' or 'p' or 'P' or 'e' or 'E';
// --- State functions ---
private static LexState? LexTop(NatsConfLexer lx)
{
var r = lx.Next();
if (char.IsWhiteSpace(r))
{
return LexSkip(lx, LexTop);
}
switch (r)
{
case TopOptStart:
lx.Push(LexTop);
return LexSkip(lx, LexBlockStart);
case CommentHashStart:
lx.Push(LexTop);
return LexCommentStart;
case CommentSlashStart:
{
var rn = lx.Next();
if (rn == CommentSlashStart)
{
lx.Push(LexTop);
return LexCommentStart;
}
lx.Backup();
goto case Eof;
}
case Eof:
if (lx._pos > lx._start)
{
return lx.Errorf("Unexpected EOF.");
}
lx.Emit(TokenType.Eof);
return null;
}
// Back up and let the key lexer handle it.
lx.Backup();
lx.Push(LexTopValueEnd);
return LexKeyStart;
}
private static LexState? LexTopValueEnd(NatsConfLexer lx)
{
var r = lx.Next();
switch (r)
{
case CommentHashStart:
lx.Push(LexTop);
return LexCommentStart;
case CommentSlashStart:
{
var rn = lx.Next();
if (rn == CommentSlashStart)
{
lx.Push(LexTop);
return LexCommentStart;
}
lx.Backup();
if (IsWhitespace(r))
{
return LexTopValueEnd;
}
break;
}
default:
if (IsWhitespace(r))
{
return LexTopValueEnd;
}
break;
}
if (IsNL(r) || r == Eof || r == OptValTerm || r == TopOptValTerm || r == TopOptTerm)
{
lx.Ignore();
return LexTop;
}
return lx.Errorf($"Expected a top-level value to end with a new line, comment or EOF, but got '{EscapeSpecial(r)}' instead.");
}
private static LexState? LexBlockStart(NatsConfLexer lx)
{
var r = lx.Next();
if (char.IsWhiteSpace(r))
{
return LexSkip(lx, LexBlockStart);
}
switch (r)
{
case TopOptStart:
lx.Push(LexBlockEnd);
return LexSkip(lx, LexBlockStart);
case TopOptTerm:
lx.Ignore();
return lx.Pop();
case CommentHashStart:
lx.Push(LexBlockStart);
return LexCommentStart;
case CommentSlashStart:
{
var rn = lx.Next();
if (rn == CommentSlashStart)
{
lx.Push(LexBlockStart);
return LexCommentStart;
}
lx.Backup();
goto case Eof;
}
case Eof:
if (lx._pos > lx._start)
{
return lx.Errorf("Unexpected EOF.");
}
lx.Emit(TokenType.Eof);
return null;
}
lx.Backup();
lx.Push(LexBlockValueEnd);
return LexKeyStart;
}
private static LexState? LexBlockValueEnd(NatsConfLexer lx)
{
var r = lx.Next();
switch (r)
{
case CommentHashStart:
lx.Push(LexBlockValueEnd);
return LexCommentStart;
case CommentSlashStart:
{
var rn = lx.Next();
if (rn == CommentSlashStart)
{
lx.Push(LexBlockValueEnd);
return LexCommentStart;
}
lx.Backup();
if (IsWhitespace(r))
{
return LexBlockValueEnd;
}
break;
}
default:
if (IsWhitespace(r))
{
return LexBlockValueEnd;
}
break;
}
if (IsNL(r) || r == OptValTerm || r == TopOptValTerm)
{
lx.Ignore();
return LexBlockStart;
}
if (r == TopOptTerm)
{
lx.Backup();
return LexBlockEnd;
}
return lx.Errorf($"Expected a block-level value to end with a new line, comment or EOF, but got '{EscapeSpecial(r)}' instead.");
}
private static LexState? LexBlockEnd(NatsConfLexer lx)
{
var r = lx.Next();
switch (r)
{
case CommentHashStart:
lx.Push(LexBlockStart);
return LexCommentStart;
case CommentSlashStart:
{
var rn = lx.Next();
if (rn == CommentSlashStart)
{
lx.Push(LexBlockStart);
return LexCommentStart;
}
lx.Backup();
if (IsNL(r) || IsWhitespace(r))
{
return LexBlockEnd;
}
break;
}
default:
if (IsNL(r) || IsWhitespace(r))
{
return LexBlockEnd;
}
break;
}
if (r == OptValTerm || r == TopOptValTerm)
{
lx.Ignore();
return LexBlockStart;
}
if (r == TopOptTerm)
{
lx.Ignore();
return lx.Pop();
}
return lx.Errorf($"Expected a block-level to end with a '}}', but got '{EscapeSpecial(r)}' instead.");
}
private static LexState? LexKeyStart(NatsConfLexer lx)
{
var r = lx.Peek();
if (IsKeySeparator(r))
{
return lx.Errorf($"Unexpected key separator '{r}'");
}
if (char.IsWhiteSpace(r))
{
lx.Next();
return LexSkip(lx, LexKeyStart);
}
if (r == DqStringStart)
{
lx.Next();
return LexSkip(lx, LexDubQuotedKey);
}
if (r == SqStringStart)
{
lx.Next();
return LexSkip(lx, LexQuotedKey);
}
lx.Ignore();
lx.Next();
return LexKey;
}
private static LexState? LexDubQuotedKey(NatsConfLexer lx)
{
var r = lx.Peek();
if (r == DqStringEnd)
{
lx.Emit(TokenType.Key);
lx.Next();
return LexSkip(lx, LexKeyEnd);
}
if (r == Eof)
{
if (lx._pos > lx._start)
{
return lx.Errorf("Unexpected EOF.");
}
lx.Emit(TokenType.Eof);
return null;
}
lx.Next();
return LexDubQuotedKey;
}
private static LexState? LexQuotedKey(NatsConfLexer lx)
{
var r = lx.Peek();
if (r == SqStringEnd)
{
lx.Emit(TokenType.Key);
lx.Next();
return LexSkip(lx, LexKeyEnd);
}
if (r == Eof)
{
if (lx._pos > lx._start)
{
return lx.Errorf("Unexpected EOF.");
}
lx.Emit(TokenType.Eof);
return null;
}
lx.Next();
return LexQuotedKey;
}
private LexState? KeyCheckKeyword(LexState fallThrough, LexState? push)
{
var key = _input[_start.._pos].ToLowerInvariant();
if (key == "include")
{
Ignore();
if (push is not null)
{
Push(push);
}
return LexIncludeStart;
}
Emit(TokenType.Key);
return fallThrough;
}
private static LexState? LexIncludeStart(NatsConfLexer lx)
{
var r = lx.Next();
if (IsWhitespace(r))
{
return LexSkip(lx, LexIncludeStart);
}
lx.Backup();
return LexInclude;
}
private static LexState? LexIncludeQuotedString(NatsConfLexer lx)
{
var r = lx.Next();
if (r == SqStringEnd)
{
lx.Backup();
lx.Emit(TokenType.Include);
lx.Next();
lx.Ignore();
return lx.Pop();
}
if (r == Eof)
{
return lx.Errorf("Unexpected EOF in quoted include");
}
return LexIncludeQuotedString;
}
private static LexState? LexIncludeDubQuotedString(NatsConfLexer lx)
{
var r = lx.Next();
if (r == DqStringEnd)
{
lx.Backup();
lx.Emit(TokenType.Include);
lx.Next();
lx.Ignore();
return lx.Pop();
}
if (r == Eof)
{
return lx.Errorf("Unexpected EOF in double quoted include");
}
return LexIncludeDubQuotedString;
}
private static LexState? LexIncludeString(NatsConfLexer lx)
{
var r = lx.Next();
if (IsNL(r) || r == Eof || r == OptValTerm || r == MapEndChar || IsWhitespace(r))
{
lx.Backup();
lx.Emit(TokenType.Include);
return lx.Pop();
}
if (r == SqStringEnd)
{
lx.Backup();
lx.Emit(TokenType.Include);
lx.Next();
lx.Ignore();
return lx.Pop();
}
return LexIncludeString;
}
private static LexState? LexInclude(NatsConfLexer lx)
{
var r = lx.Next();
switch (r)
{
case SqStringStart:
lx.Ignore();
return LexIncludeQuotedString;
case DqStringStart:
lx.Ignore();
return LexIncludeDubQuotedString;
case ArrayStartChar:
return lx.Errorf("Expected include value but found start of an array");
case MapStartChar:
return lx.Errorf("Expected include value but found start of a map");
case BlockStartChar:
return lx.Errorf("Expected include value but found start of a block");
case '\\':
return lx.Errorf("Expected include value but found escape sequence");
}
if (char.IsDigit(r) || r == '-')
{
return lx.Errorf("Expected include value but found start of a number");
}
if (IsNL(r))
{
return lx.Errorf("Expected include value but found new line");
}
lx.Backup();
return LexIncludeString;
}
private static LexState? LexKey(NatsConfLexer lx)
{
var r = lx.Peek();
if (char.IsWhiteSpace(r) && !IsNL(r))
{
// Spaces signal we could be looking at a keyword, e.g. include.
return lx.KeyCheckKeyword(LexKeyEnd, null);
}
if (IsKeySeparator(r) || r == Eof)
{
lx.Emit(TokenType.Key);
return LexKeyEnd;
}
if (IsNL(r))
{
// Newline after key with no separator — check for keyword.
return lx.KeyCheckKeyword(LexKeyEnd, null);
}
lx.Next();
return LexKey;
}
private static LexState? LexKeyEnd(NatsConfLexer lx)
{
var r = lx.Next();
if (char.IsWhiteSpace(r))
{
return LexSkip(lx, LexKeyEnd);
}
if (IsKeySeparator(r))
{
return LexSkip(lx, LexValue);
}
if (r == Eof)
{
lx.Emit(TokenType.Eof);
return null;
}
// We start the value here.
lx.Backup();
return LexValue;
}
private static LexState? LexValue(NatsConfLexer lx)
{
var r = lx.Next();
if (IsWhitespace(r))
{
return LexSkip(lx, LexValue);
}
switch (r)
{
case ArrayStartChar:
lx.Ignore();
lx.Emit(TokenType.ArrayStart);
return LexArrayValue;
case MapStartChar:
lx.Ignore();
lx.Emit(TokenType.MapStart);
return LexMapKeyStart;
case SqStringStart:
lx.Ignore();
return LexQuotedString;
case DqStringStart:
lx.Ignore();
lx._stringStateFn = LexDubQuotedString;
return LexDubQuotedString;
case '-':
return LexNegNumberStart;
case BlockStartChar:
lx.Ignore();
return LexBlock;
case '.':
return lx.Errorf("Floats must start with a digit");
}
if (char.IsDigit(r))
{
lx.Backup();
return LexNumberOrDateOrStringOrIPStart;
}
if (IsNL(r))
{
return lx.Errorf("Expected value but found new line");
}
lx.Backup();
lx._stringStateFn = LexString;
return LexString;
}
private static LexState? LexArrayValue(NatsConfLexer lx)
{
var r = lx.Next();
if (char.IsWhiteSpace(r))
{
return LexSkip(lx, LexArrayValue);
}
switch (r)
{
case CommentHashStart:
lx.Push(LexArrayValue);
return LexCommentStart;
case CommentSlashStart:
{
var rn = lx.Next();
if (rn == CommentSlashStart)
{
lx.Push(LexArrayValue);
return LexCommentStart;
}
lx.Backup();
// fallthrough to ArrayValTerm check
if (r == ArrayValTerm)
{
return lx.Errorf($"Unexpected array value terminator '{ArrayValTerm}'.");
}
break;
}
case ArrayValTerm:
return lx.Errorf($"Unexpected array value terminator '{ArrayValTerm}'.");
case ArrayEndChar:
return LexArrayEnd;
}
lx.Backup();
lx.Push(LexArrayValueEnd);
return LexValue;
}
private static LexState? LexArrayValueEnd(NatsConfLexer lx)
{
var r = lx.Next();
if (IsWhitespace(r))
{
return LexSkip(lx, LexArrayValueEnd);
}
switch (r)
{
case CommentHashStart:
lx.Push(LexArrayValueEnd);
return LexCommentStart;
case CommentSlashStart:
{
var rn = lx.Next();
if (rn == CommentSlashStart)
{
lx.Push(LexArrayValueEnd);
return LexCommentStart;
}
lx.Backup();
// fallthrough
if (r == ArrayValTerm || IsNL(r))
{
return LexSkip(lx, LexArrayValue);
}
break;
}
case ArrayEndChar:
return LexArrayEnd;
}
if (r == ArrayValTerm || IsNL(r))
{
return LexSkip(lx, LexArrayValue);
}
return lx.Errorf($"Expected an array value terminator ',' or an array terminator ']', but got '{EscapeSpecial(r)}' instead.");
}
private static LexState? LexArrayEnd(NatsConfLexer lx)
{
lx.Ignore();
lx.Emit(TokenType.ArrayEnd);
return lx.Pop();
}
private static LexState? LexMapKeyStart(NatsConfLexer lx)
{
var r = lx.Peek();
if (IsKeySeparator(r))
{
return lx.Errorf($"Unexpected key separator '{r}'.");
}
if (r == ArrayEndChar)
{
return lx.Errorf($"Unexpected array end '{r}' processing map.");
}
if (char.IsWhiteSpace(r))
{
lx.Next();
return LexSkip(lx, LexMapKeyStart);
}
if (r == MapEndChar)
{
lx.Next();
return LexSkip(lx, LexMapEnd);
}
if (r == CommentHashStart)
{
lx.Next();
lx.Push(LexMapKeyStart);
return LexCommentStart;
}
if (r == CommentSlashStart)
{
lx.Next();
var rn = lx.Next();
if (rn == CommentSlashStart)
{
lx.Push(LexMapKeyStart);
return LexCommentStart;
}
lx.Backup();
}
if (r == SqStringStart)
{
lx.Next();
return LexSkip(lx, LexMapQuotedKey);
}
if (r == DqStringStart)
{
lx.Next();
return LexSkip(lx, LexMapDubQuotedKey);
}
if (r == Eof)
{
return lx.Errorf("Unexpected EOF processing map.");
}
lx.Ignore();
lx.Next();
return LexMapKey;
}
private static LexState? LexMapQuotedKey(NatsConfLexer lx)
{
var r = lx.Peek();
if (r == Eof)
{
return lx.Errorf("Unexpected EOF processing quoted map key.");
}
if (r == SqStringEnd)
{
lx.Emit(TokenType.Key);
lx.Next();
return LexSkip(lx, LexMapKeyEnd);
}
lx.Next();
return LexMapQuotedKey;
}
private static LexState? LexMapDubQuotedKey(NatsConfLexer lx)
{
var r = lx.Peek();
if (r == Eof)
{
return lx.Errorf("Unexpected EOF processing double quoted map key.");
}
if (r == DqStringEnd)
{
lx.Emit(TokenType.Key);
lx.Next();
return LexSkip(lx, LexMapKeyEnd);
}
lx.Next();
return LexMapDubQuotedKey;
}
private static LexState? LexMapKey(NatsConfLexer lx)
{
var r = lx.Peek();
if (r == Eof)
{
return lx.Errorf("Unexpected EOF processing map key.");
}
if (char.IsWhiteSpace(r) && !IsNL(r))
{
return lx.KeyCheckKeyword(LexMapKeyEnd, LexMapValueEnd);
}
if (IsNL(r))
{
return lx.KeyCheckKeyword(LexMapKeyEnd, LexMapValueEnd);
}
if (IsKeySeparator(r))
{
lx.Emit(TokenType.Key);
return LexMapKeyEnd;
}
lx.Next();
return LexMapKey;
}
private static LexState? LexMapKeyEnd(NatsConfLexer lx)
{
var r = lx.Next();
if (char.IsWhiteSpace(r))
{
return LexSkip(lx, LexMapKeyEnd);
}
if (IsKeySeparator(r))
{
return LexSkip(lx, LexMapValue);
}
// We start the value here.
lx.Backup();
return LexMapValue;
}
private static LexState? LexMapValue(NatsConfLexer lx)
{
var r = lx.Next();
if (char.IsWhiteSpace(r))
{
return LexSkip(lx, LexMapValue);
}
if (r == MapValTerm)
{
return lx.Errorf($"Unexpected map value terminator '{MapValTerm}'.");
}
if (r == MapEndChar)
{
return LexSkip(lx, LexMapEnd);
}
lx.Backup();
lx.Push(LexMapValueEnd);
return LexValue;
}
private static LexState? LexMapValueEnd(NatsConfLexer lx)
{
var r = lx.Next();
if (IsWhitespace(r))
{
return LexSkip(lx, LexMapValueEnd);
}
switch (r)
{
case CommentHashStart:
lx.Push(LexMapValueEnd);
return LexCommentStart;
case CommentSlashStart:
{
var rn = lx.Next();
if (rn == CommentSlashStart)
{
lx.Push(LexMapValueEnd);
return LexCommentStart;
}
lx.Backup();
// fallthrough
if (r == OptValTerm || r == MapValTerm || IsNL(r))
{
return LexSkip(lx, LexMapKeyStart);
}
break;
}
}
if (r == OptValTerm || r == MapValTerm || IsNL(r))
{
return LexSkip(lx, LexMapKeyStart);
}
if (r == MapEndChar)
{
return LexSkip(lx, LexMapEnd);
}
return lx.Errorf($"Expected a map value terminator ',' or a map terminator '}}', but got '{EscapeSpecial(r)}' instead.");
}
private static LexState? LexMapEnd(NatsConfLexer lx)
{
lx.Ignore();
lx.Emit(TokenType.MapEnd);
return lx.Pop();
}
private bool IsBool()
{
var str = _input[_start.._pos].ToLowerInvariant();
return str is "true" or "false" or "on" or "off" or "yes" or "no";
}
private bool IsVariable()
{
if (_start >= _input.Length)
{
return false;
}
if (_input[_start] == '$')
{
_start += 1;
return true;
}
return false;
}
private static LexState? LexQuotedString(NatsConfLexer lx)
{
var r = lx.Next();
if (r == SqStringEnd)
{
lx.Backup();
lx.Emit(TokenType.String);
lx.Next();
lx.Ignore();
return lx.Pop();
}
if (r == Eof)
{
if (lx._pos > lx._start)
{
return lx.Errorf("Unexpected EOF.");
}
lx.Emit(TokenType.Eof);
return null;
}
return LexQuotedString;
}
private static LexState? LexDubQuotedString(NatsConfLexer lx)
{
var r = lx.Next();
if (r == '\\')
{
lx.AddCurrentStringPart(1);
return LexStringEscape;
}
if (r == DqStringEnd)
{
lx.Backup();
lx.EmitString();
lx.Next();
lx.Ignore();
return lx.Pop();
}
if (r == Eof)
{
if (lx._pos > lx._start)
{
return lx.Errorf("Unexpected EOF.");
}
lx.Emit(TokenType.Eof);
return null;
}
return LexDubQuotedString;
}
private static LexState? LexString(NatsConfLexer lx)
{
var r = lx.Next();
if (r == '\\')
{
lx.AddCurrentStringPart(1);
return LexStringEscape;
}
// Termination of non-quoted strings.
if (IsNL(r) || r == Eof || r == OptValTerm ||
r == ArrayValTerm || r == ArrayEndChar || r == MapEndChar ||
IsWhitespace(r))
{
lx.Backup();
if (lx.HasEscapedParts())
{
lx.EmitString();
}
else if (lx.IsBool())
{
lx.Emit(TokenType.Bool);
}
else if (lx.IsVariable())
{
lx.Emit(TokenType.Variable);
}
else
{
lx.EmitString();
}
return lx.Pop();
}
if (r == SqStringEnd)
{
lx.Backup();
lx.EmitString();
lx.Next();
lx.Ignore();
return lx.Pop();
}
return LexString;
}
private static LexState? LexBlock(NatsConfLexer lx)
{
var r = lx.Next();
if (r == BlockEndChar)
{
lx.Backup();
lx.Backup();
// Looking for a ')' character on a line by itself.
// If the previous character isn't a newline, keep processing.
if (lx.Next() != '\n')
{
lx.Next();
return LexBlock;
}
lx.Next();
// Make sure the next character is a newline or EOF.
var next = lx.Next();
if (next is '\n' or Eof)
{
lx.Backup();
lx.Backup();
lx.Emit(TokenType.String);
lx.Next();
lx.Ignore();
return lx.Pop();
}
lx.Backup();
return LexBlock;
}
if (r == Eof)
{
return lx.Errorf("Unexpected EOF processing block.");
}
return LexBlock;
}
private static LexState? LexStringEscape(NatsConfLexer lx)
{
var r = lx.Next();
return r switch
{
'x' => LexStringBinary(lx),
't' => lx.AddStringPart("\t"),
'n' => lx.AddStringPart("\n"),
'r' => lx.AddStringPart("\r"),
'"' => lx.AddStringPart("\""),
'\\' => lx.AddStringPart("\\"),
_ => lx.Errorf($"Invalid escape character '{EscapeSpecial(r)}'. Only the following escape characters are allowed: \\xXX, \\t, \\n, \\r, \\\", \\\\."),
};
}
private static LexState? LexStringBinary(NatsConfLexer lx)
{
var r1 = lx.Next();
if (IsNL(r1) || r1 == Eof)
{
return lx.Errorf("Expected two hexadecimal digits after '\\x', but hit end of line");
}
var r2 = lx.Next();
if (IsNL(r2) || r2 == Eof)
{
return lx.Errorf("Expected two hexadecimal digits after '\\x', but hit end of line");
}
var hexStr = lx._input[(lx._pos - 2)..lx._pos];
try
{
var bytes = Convert.FromHexString(hexStr);
return lx.AddStringPart(System.Text.Encoding.Latin1.GetString(bytes));
}
catch (FormatException)
{
return lx.Errorf($"Expected two hexadecimal digits after '\\x', but got '{hexStr}'");
}
}
private static LexState? LexNumberOrDateOrStringOrIPStart(NatsConfLexer lx)
{
var r = lx.Next();
if (!char.IsDigit(r))
{
if (r == '.')
{
return lx.Errorf("Floats must start with a digit, not '.'.");
}
return lx.Errorf($"Expected a digit but got '{EscapeSpecial(r)}'.");
}
return LexNumberOrDateOrStringOrIP;
}
private static LexState? LexNumberOrDateOrStringOrIP(NatsConfLexer lx)
{
var r = lx.Next();
if (r == '-')
{
if (lx._pos - lx._start != 5)
{
return lx.Errorf("All ISO8601 dates must be in full Zulu form.");
}
return LexDateAfterYear;
}
if (char.IsDigit(r))
{
return LexNumberOrDateOrStringOrIP;
}
if (r == '.')
{
return LexFloatStart;
}
if (IsNumberSuffix(r))
{
return LexConvenientNumber;
}
// Check if this is a terminator or a string character.
if (!(IsNL(r) || r == Eof || r == MapEndChar || r == OptValTerm || r == MapValTerm || IsWhitespace(r) || r == ArrayValTerm || r == ArrayEndChar))
{
// Treat it as a string value.
lx._stringStateFn = LexString;
return LexString;
}
lx.Backup();
lx.Emit(TokenType.Integer);
return lx.Pop();
}
private static LexState? LexConvenientNumber(NatsConfLexer lx)
{
var r = lx.Next();
if (r is 'b' or 'B' or 'i' or 'I')
{
return LexConvenientNumber;
}
lx.Backup();
if (IsNL(r) || r == Eof || r == MapEndChar || r == OptValTerm || r == MapValTerm ||
IsWhitespace(r) || char.IsDigit(r) || r == ArrayValTerm || r == ArrayEndChar)
{
lx.Emit(TokenType.Integer);
return lx.Pop();
}
// This is not a number, treat as string.
lx._stringStateFn = LexString;
return LexString;
}
private static LexState? LexDateAfterYear(NatsConfLexer lx)
{
// Expected: MM-DDTHH:MM:SSZ
char[] formats =
[
'0', '0', '-', '0', '0',
'T',
'0', '0', ':', '0', '0', ':', '0', '0',
'Z',
];
foreach (var f in formats)
{
var r = lx.Next();
if (f == '0')
{
if (!char.IsDigit(r))
{
return lx.Errorf($"Expected digit in ISO8601 datetime, but found '{EscapeSpecial(r)}' instead.");
}
}
else if (f != r)
{
return lx.Errorf($"Expected '{f}' in ISO8601 datetime, but found '{EscapeSpecial(r)}' instead.");
}
}
lx.Emit(TokenType.DateTime);
return lx.Pop();
}
private static LexState? LexNegNumberStart(NatsConfLexer lx)
{
var r = lx.Next();
if (!char.IsDigit(r))
{
if (r == '.')
{
return lx.Errorf("Floats must start with a digit, not '.'.");
}
return lx.Errorf($"Expected a digit but got '{EscapeSpecial(r)}'.");
}
return LexNegNumber;
}
private static LexState? LexNegNumber(NatsConfLexer lx)
{
var r = lx.Next();
if (char.IsDigit(r))
{
return LexNegNumber;
}
if (r == '.')
{
return LexFloatStart;
}
if (IsNumberSuffix(r))
{
return LexConvenientNumber;
}
lx.Backup();
lx.Emit(TokenType.Integer);
return lx.Pop();
}
private static LexState? LexFloatStart(NatsConfLexer lx)
{
var r = lx.Next();
if (!char.IsDigit(r))
{
return lx.Errorf($"Floats must have a digit after the '.', but got '{EscapeSpecial(r)}' instead.");
}
return LexFloat;
}
private static LexState? LexFloat(NatsConfLexer lx)
{
var r = lx.Next();
if (char.IsDigit(r))
{
return LexFloat;
}
// Not a digit; if it's another '.', this might be an IP address.
if (r == '.')
{
return LexIPAddr;
}
lx.Backup();
lx.Emit(TokenType.Float);
return lx.Pop();
}
private static LexState? LexIPAddr(NatsConfLexer lx)
{
var r = lx.Next();
if (char.IsDigit(r) || r is '.' or ':' or '-')
{
return LexIPAddr;
}
lx.Backup();
lx.Emit(TokenType.String);
return lx.Pop();
}
private static LexState? LexCommentStart(NatsConfLexer lx)
{
lx.Ignore();
lx.Emit(TokenType.Comment);
return LexComment;
}
private static LexState? LexComment(NatsConfLexer lx)
{
var r = lx.Peek();
if (IsNL(r) || r == Eof)
{
// Consume the comment text but don't emit it as a user-visible token.
// Just ignore it and pop back.
lx.Ignore();
return lx.Pop();
}
lx.Next();
return LexComment;
}
private static LexState LexSkip(NatsConfLexer lx, LexState nextState)
{
lx.Ignore();
return nextState;
}
private static string EscapeSpecial(char c) => c switch
{
'\n' => "\\n",
'\r' => "\\r",
'\t' => "\\t",
Eof => "EOF",
_ => c.ToString(),
};
}