Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion mql/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,16 @@ func (p *Parser) parseValueFromPos(startPos int) string {
}
if endIdx < len(p.input) {
val := p.input[startIdx+1 : endIdx]
// Advance scanner past the quoted string
// Advance scanner past the quoted string. When the closing quote is
// the final character, the scanner is already at EOF and next() no
// longer advances its offset, so guard on forward progress to avoid
// looping forever (issue #221).
for p.s.Pos().Offset-1 < endIdx+1 {
prev := p.s.Pos().Offset
p.next()
if p.s.Pos().Offset == prev {
break // scanner reached EOF, no further progress possible
}
}
return val
}
Expand Down
59 changes: 59 additions & 0 deletions mql/parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package mql

import (
"testing"
"time"
)

// TestParseQuotedValueAsFinalToken guards against a regression where a quoted
// value appearing as the final token of the input caused Parser.Parse to spin
// forever in parseValueFromPos: at EOF the scanner stops advancing its offset,
// so the scanner-advance loop's condition never becomes false (issue #221).
//
// Each case is parsed in a goroutine so the test fails via timeout instead of
// hanging the whole suite if the loop ever regresses.
func TestParseQuotedValueAsFinalToken(t *testing.T) {
tests := []struct {
name string
input string
key string
value string
}{
{"single-char quoted final value", `k:"v"`, "k", "v"},
{"word quoted final value", `name:"alice"`, "name", "alice"},
{"quoted final value with space", `k:"hello world"`, "k", "hello world"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
type parseResult struct {
expr Expr
err error
}
done := make(chan parseResult, 1)
go func() {
expr, err := NewParser(tt.input).Parse()
done <- parseResult{expr, err}
}()

select {
case r := <-done:
if r.err != nil {
t.Fatalf("Parse(%q) returned error: %v", tt.input, r.err)
}
term, ok := r.expr.(*TermExpr)
if !ok {
t.Fatalf("Parse(%q) = %T, want *TermExpr", tt.input, r.expr)
}
if term.Key != tt.key {
t.Errorf("Parse(%q) key = %q, want %q", tt.input, term.Key, tt.key)
}
if term.Value != tt.value {
t.Errorf("Parse(%q) value = %v, want %q", tt.input, term.Value, tt.value)
}
case <-time.After(2 * time.Second):
t.Fatalf("Parse(%q) did not terminate within 2s (infinite loop)", tt.input)
}
})
}
}