diff --git a/conf/lex.go b/conf/lex.go index 013b883866..f9920a4411 100644 --- a/conf/lex.go +++ b/conf/lex.go @@ -263,7 +263,8 @@ func lexTop(lx *lexer) stateFn { switch r { case topOptStart: - return lexSkip(lx, lexTop) + lx.push(lexTop) + return lexSkip(lx, lexBlockStart) case commentHashStart: lx.push(lexTop) return lexCommentStart @@ -318,6 +319,71 @@ func lexTopValueEnd(lx *lexer) stateFn { "comment or EOF, but got '%v' instead.", r) } +func lexBlockStart(lx *lexer) stateFn { + r := lx.next() + if unicode.IsSpace(r) { + return lexSkip(lx, lexBlockStart) + } + + switch r { + case topOptStart: + lx.push(lexBlockEnd) + return lexSkip(lx, lexBlockStart) + case commentHashStart: + lx.push(lexBlockEnd) + return lexCommentStart + case commentSlashStart: + rn := lx.next() + if rn == commentSlashStart { + lx.push(lexBlockEnd) + return lexCommentStart + } + lx.backup() + fallthrough + case eof: + if lx.pos > lx.start { + return lx.errorf("Unexpected EOF.") + } + lx.emit(itemEOF) + return nil + } + + // At this point, the only valid item can be a key, so we back up + // and let the key lexer do the rest. + lx.backup() + lx.push(lexBlockEnd) + return lexKeyStart +} + +// lexBlockEnd is entered whenever a block-level value has been consumed. +// It must see only whitespace, and will turn back to lexTop upon a "}". +func lexBlockEnd(lx *lexer) stateFn { + r := lx.next() + switch { + case r == commentHashStart: + // a comment will read to a new line for us. + lx.push(lexBlockEnd) + return lexCommentStart + case r == commentSlashStart: + rn := lx.next() + if rn == commentSlashStart { + lx.push(lexBlockEnd) + return lexCommentStart + } + lx.backup() + fallthrough + case isNL(r) || isWhitespace(r): + return lexBlockEnd + case r == optValTerm || r == topOptValTerm: + lx.ignore() + return lexBlockStart + case r == topOptTerm: + lx.ignore() + return lx.pop() + } + return lx.errorf("Expected a block-level value to end with a '}', but got '%v' instead.", r) +} + // lexKeyStart consumes a key name up until the first non-whitespace character. // lexKeyStart will ignore whitespace. It will also eat enclosing quotes. func lexKeyStart(lx *lexer) stateFn { diff --git a/conf/parse_test.go b/conf/parse_test.go index 8cf1ea98f3..98dffc0ab2 100644 --- a/conf/parse_test.go +++ b/conf/parse_test.go @@ -740,3 +740,78 @@ func TestJSONParseCompat(t *testing.T) { }) } } + +func TestBlocks(t *testing.T) { + for _, test := range []struct { + name string + input string + expected map[string]any + err string + linepos string + }{ + { + "inline block", + `{ listen: 0.0.0.0:4222 }`, + map[string]any{ + "listen": "0.0.0.0:4222", + }, + "", "", + }, + { + "newline block", + `{ + listen: 0.0.0.0:4222 + }`, + map[string]any{ + "listen": "0.0.0.0:4222", + }, + "", "", + }, + { + "newline block with trailing comment", + ` + { + listen: 0.0.0.0:4222 + } + # wibble + `, + map[string]any{ + "listen": "0.0.0.0:4222", + }, + "", "", + }, + { + "nested newline blocks with trailing comment", + ` + { + { + listen: 0.0.0.0:4222 // random comment + } + # wibble1 + } + # wibble2 + `, + map[string]any{ + "listen": "0.0.0.0:4222", + }, + "", "", + }, + } { + t.Run(test.name, func(t *testing.T) { + f, err := os.CreateTemp(t.TempDir(), "nats.conf-") + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(f.Name(), []byte(test.input), 066); err != nil { + t.Error(err) + } + if m, err := ParseFile(f.Name()); err == nil { + if !reflect.DeepEqual(m, test.expected) { + t.Fatalf("Not Equal:\nReceived: '%+v'\nExpected: '%+v'\n", m, test.expected) + } + } else if !strings.Contains(err.Error(), test.err) || !strings.Contains(err.Error(), test.linepos) { + t.Errorf("expected invalid conf error, got: %v", err) + } + }) + } +} diff --git a/server/server_test.go b/server/server_test.go index b75c48badd..3dae1aa55e 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -2121,3 +2121,21 @@ func TestServerAuthBlockAndSysAccounts(t *testing.T) { _, err = nats.Connect(s.ClientURL()) require_Error(t, err, nats.ErrAuthorization, errors.New("nats: Authorization Violation")) } + +// https://github.com/nats-io/nats-server/issues/5396 +func TestServerConfigLastLineComments(t *testing.T) { + conf := createConfFile(t, []byte(` + { + "listen": "0.0.0.0:4222" + } + # wibble + `)) + + s, _ := RunServerWithConfig(conf) + defer s.Shutdown() + + // This should work of course. + nc, err := nats.Connect(s.ClientURL()) + require_NoError(t, err) + defer nc.Close() +}