From 8ce258f797e5aad8448c79f9ac4bb086ed9c4f9d Mon Sep 17 00:00:00 2001 From: reginaldalfret Date: Fri, 23 Oct 2026 19:17:34 +0530 Subject: [PATCH] internal/graph: escape tag names in dot nodelets Sample tag names may contain quotes, backslashes, or newlines (for example, structured database keys or raw string tags). When generating DOT nodelets, the unescaped tag names broke Graphviz syntax. Escape tag names with escapeForDot in addNodelets and numericNodelets, and add a regression test. Fixes #769 Signed-off-by: reginaldalfret --- internal/graph/dotgraph.go | 4 ++-- internal/graph/dotgraph_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/internal/graph/dotgraph.go b/internal/graph/dotgraph.go index d704fc4e13..9776640fe9 100644 --- a/internal/graph/dotgraph.go +++ b/internal/graph/dotgraph.go @@ -247,7 +247,7 @@ func (b *builder) addNodelets(node *Node, nodeID int) bool { continue } weight := b.config.FormatValue(w) - nodelets += fmt.Sprintf(`N%d_%d [label = "%s" id="N%d_%d" fontsize=8 shape=box3d tooltip="%s"]`+"\n", nodeID, i, t.Name, nodeID, i, weight) + nodelets += fmt.Sprintf(`N%d_%d [label = "%s" id="N%d_%d" fontsize=8 shape=box3d tooltip="%s"]`+"\n", nodeID, i, escapeForDot(t.Name), nodeID, i, weight) nodelets += fmt.Sprintf(`N%d -> N%d_%d [label=" %s" weight=100 tooltip="%s" labeltooltip="%s"]`+"\n", nodeID, nodeID, i, weight, weight, weight) if nts := lnts[t.Name]; nts != nil { nodelets += b.numericNodelets(nts, maxNodelets, flatTags, fmt.Sprintf(`N%d_%d`, nodeID, i)) @@ -274,7 +274,7 @@ func (b *builder) numericNodelets(nts []*Tag, maxNumNodelets int, flatTags bool, } if w != 0 { weight := b.config.FormatValue(w) - nodelets += fmt.Sprintf(`N%s_%d [label = "%s" id="N%s_%d" fontsize=8 shape=box3d tooltip="%s"]`+"\n", source, j, t.Name, source, j, weight) + nodelets += fmt.Sprintf(`N%s_%d [label = "%s" id="N%s_%d" fontsize=8 shape=box3d tooltip="%s"]`+"\n", source, j, escapeForDot(t.Name), source, j, weight) nodelets += fmt.Sprintf(`%s -> N%s_%d [label=" %s" weight=100 tooltip="%s" labeltooltip="%s"%s]`+"\n", source, source, j, weight, weight, weight, attr) } } diff --git a/internal/graph/dotgraph_test.go b/internal/graph/dotgraph_test.go index b375bb6225..55ec50c00b 100644 --- a/internal/graph/dotgraph_test.go +++ b/internal/graph/dotgraph_test.go @@ -150,6 +150,39 @@ func TestComposeWithNamesThatNeedEscaping(t *testing.T) { compareGraphs(t, buf.Bytes(), "compose7.dot") } +func TestComposeWithTagNamesThatNeedEscaping(t *testing.T) { + g := baseGraph() + a, c := baseAttrsAndConfig() + + // Add tags with quotes and backslashes to Node 1. + g.Nodes[0].LabelTags[`tag"with"quotes`] = &Tag{ + Name: `tag"with"quotes`, + Cum: 10, + Flat: 10, + } + g.Nodes[0].NumericTags[""] = TagMap{ + `num"tag"`: &Tag{ + Name: `num"tag"`, + Cum: 20, + Flat: 20, + Unit: "ms", + }, + } + + var buf bytes.Buffer + ComposeDot(&buf, g, a, c) + + dot := buf.String() + wantLabel := `label = "tag\"with\"quotes"` + if !strings.Contains(dot, wantLabel) { + t.Errorf("expected dot output to contain %q, but got:\n%s", wantLabel, dot) + } + wantNumLabel := `label = "num\"tag\""` + if !strings.Contains(dot, wantNumLabel) { + t.Errorf("expected dot output to contain %q, but got:\n%s", wantNumLabel, dot) + } +} + func TestComposeWithCommentsWithNewlines(t *testing.T) { g := baseGraph() a, c := baseAttrsAndConfig()