textparse: fix parseLVals to only treat quoted strings as metric names

When a label-name position is followed by comma or brace-close, only
treat it as a metric name shorthand if the token was a double-quoted
string (tQString). Bare identifiers must be followed by an equal sign.

Add tests for bare identifier inputs that previously could panic.

Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com>
This commit is contained in:
Julien Pivotto 2026-03-04 15:19:17 +01:00
parent 5a02b92c0e
commit ad2f81ef6d
4 changed files with 48 additions and 4 deletions

View File

@ -634,11 +634,13 @@ func (p *OpenMetricsParser) parseLVals(offsets []int, isExemplar bool) ([]int, e
for {
curTStart := p.l.start
curTI := p.l.i
var isQString bool
switch t {
case tBraceClose:
return offsets, nil
case tLName:
case tQString:
isQString = true
default:
return nil, p.parseError("expected label name", t)
}
@ -647,7 +649,7 @@ func (p *OpenMetricsParser) parseLVals(offsets []int, isExemplar bool) ([]int, e
// A quoted string followed by a comma or brace is a metric name. Set the
// offsets and continue processing. If this is an exemplar, this format
// is not allowed.
if t == tComma || t == tBraceClose {
if isQString && (t == tComma || t == tBraceClose) {
if isExemplar {
return nil, p.parseError("expected label name", t)
}

View File

@ -941,6 +941,10 @@ func TestOpenMetricsParseErrors(t *testing.T) {
input: "empty_label_name{=\"\"} 0\n# EOF\n",
err: "expected label name, got \"=\\\"\" (\"EQUAL\") while parsing: \"empty_label_name{=\\\"\"",
},
{
input: "{A}0\n# EOF\n",
err: "expected equal, got \"}0\" (\"BCLOSE\") while parsing: \"{A}0\"",
},
{
input: "foo 1_2\n\n# EOF\n",
err: "unsupported character in float while parsing: \"foo 1_2\"",
@ -971,11 +975,11 @@ func TestOpenMetricsParseErrors(t *testing.T) {
},
{
input: `custom_metric_total 1 # {bb}`,
err: "expected label name, got \"}\" (\"BCLOSE\") while parsing: \"custom_metric_total 1 # {bb}\"",
err: "expected equal, got \"}\" (\"BCLOSE\") while parsing: \"custom_metric_total 1 # {bb}\"",
},
{
input: `custom_metric_total 1 # {bb, a="dd"}`,
err: "expected label name, got \", \" (\"COMMA\") while parsing: \"custom_metric_total 1 # {bb, \"",
err: "expected equal, got \", \" (\"COMMA\") while parsing: \"custom_metric_total 1 # {bb, \"",
},
{
input: `custom_metric_total 1 # {aa="bb",,cc="dd"} 1`,
@ -1037,6 +1041,22 @@ func TestOpenMetricsParseErrors(t *testing.T) {
}
}
func TestOpenMetricsParseBareIdentifierInBraces(t *testing.T) {
require.NotPanics(t, func() {
p := NewOpenMetricsParser([]byte("{A} 0\n# EOF\n"), labels.NewSymbolTable(), WithOMParserSTSeriesSkipped())
for {
et, err := p.Next()
if err != nil {
break
}
if et == EntrySeries {
var lset labels.Labels
p.Labels(&lset)
}
}
})
}
func TestOMNullByteHandling(t *testing.T) {
cases := []struct {
input string

View File

@ -408,11 +408,13 @@ func (p *PromParser) parseLVals() error {
for {
curTStart := p.l.start
curTI := p.l.i
var isQString bool
switch t {
case tBraceClose:
return nil
case tLName:
case tQString:
isQString = true
default:
return p.parseError("expected label name", t)
}
@ -420,7 +422,7 @@ func (p *PromParser) parseLVals() error {
t = p.nextToken()
// A quoted string followed by a comma or brace is a metric name. Set the
// offsets and continue processing.
if t == tComma || t == tBraceClose {
if isQString && (t == tComma || t == tBraceClose) {
if p.offsets[0] != -1 || p.offsets[1] != -1 {
return fmt.Errorf("metric name already set while parsing: %q", p.l.b[p.start:p.l.i])
}

View File

@ -510,6 +510,10 @@ func TestPromParseErrors(t *testing.T) {
input: "empty_label_name{=\"\"} 0",
err: "expected label name, got \"=\\\"\" (\"EQUAL\") while parsing: \"empty_label_name{=\\\"\"",
},
{
input: "{A}0",
err: "expected equal, got \"}0\" (\"BCLOSE\") while parsing: \"{A}0\"",
},
{
input: "foo 1_2\n",
err: "unsupported character in float while parsing: \"foo 1_2\"",
@ -550,6 +554,22 @@ func TestPromParseErrors(t *testing.T) {
}
}
func TestPromParseBareIdentifierInBraces(t *testing.T) {
require.NotPanics(t, func() {
p := NewPromParser([]byte("{A} 0\n"), labels.NewSymbolTable(), false)
for {
et, err := p.Next()
if err != nil {
break
}
if et == EntrySeries {
var lset labels.Labels
p.Labels(&lset)
}
}
})
}
func TestPromNullByteHandling(t *testing.T) {
cases := []struct {
input string