Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FIXED] Fix to properly deal with block scopes in lexer. #5406

Merged
merged 3 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
70 changes: 68 additions & 2 deletions conf/lex.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2013-2018 The NATS Authors
// Copyright 2013-2024 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
75 changes: 75 additions & 0 deletions conf/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
derekcollison marked this conversation as resolved.
Show resolved Hide resolved
})
}
}
20 changes: 19 additions & 1 deletion server/server_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2012-2020 The NATS Authors
// Copyright 2012-2024 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
Expand Down Expand Up @@ -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()
}