From ad2f81ef6ddda642018a271133a68ff7b58ce8fc Mon Sep 17 00:00:00 2001 From: Julien Pivotto <291750+roidelapluie@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:19:17 +0100 Subject: [PATCH] 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> --- model/textparse/openmetricsparse.go | 4 +++- model/textparse/openmetricsparse_test.go | 24 ++++++++++++++++++++++-- model/textparse/promparse.go | 4 +++- model/textparse/promparse_test.go | 20 ++++++++++++++++++++ 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/model/textparse/openmetricsparse.go b/model/textparse/openmetricsparse.go index 3a36402ef4..2f6671eb62 100644 --- a/model/textparse/openmetricsparse.go +++ b/model/textparse/openmetricsparse.go @@ -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) } diff --git a/model/textparse/openmetricsparse_test.go b/model/textparse/openmetricsparse_test.go index 8f6393cd53..d365d8d959 100644 --- a/model/textparse/openmetricsparse_test.go +++ b/model/textparse/openmetricsparse_test.go @@ -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 diff --git a/model/textparse/promparse.go b/model/textparse/promparse.go index ada1b29013..c10a8002a7 100644 --- a/model/textparse/promparse.go +++ b/model/textparse/promparse.go @@ -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]) } diff --git a/model/textparse/promparse_test.go b/model/textparse/promparse_test.go index a398067efe..c6e0364131 100644 --- a/model/textparse/promparse_test.go +++ b/model/textparse/promparse_test.go @@ -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