Skip to content

Commit 48de501

Browse files
committed
internal/graph: Escape labels to support double quotes
Fixes syntax errors when trying to render pprof profiles that have double quotes in tags. These can be created with Go's pprof labels feature, for example with: Fixes syntax errors when trying to render pprof profiles that have double quotes in tags. These can be created with Go's pprof labels feature, for example with: pprof.Labels("key", "label \"double quote\"\nline two") Trying to display a graph generated with this lable will fail: Error: <stdin>: syntax error in line 5 near 'quote' The double quote (") was never escaped in the label strings. Add a new escaping function that replaces newlines with centered lines (\n) because the existing one replaces newline with left-justified lines (\l).
1 parent 5bba342 commit 48de501

File tree

5 files changed

+66
-6
lines changed

5 files changed

+66
-6
lines changed

internal/graph/dotgraph.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ func (b *builder) addNodelets(node *Node, nodeID int) bool {
247247
continue
248248
}
249249
weight := b.config.FormatValue(w)
250-
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)
250+
nodelets += fmt.Sprintf(`N%d_%d [label = "%s" id="N%d_%d" fontsize=8 shape=box3d tooltip="%s"]`+"\n", nodeID, i, escapeForDotCentered(t.Name), nodeID, i, weight)
251251
nodelets += fmt.Sprintf(`N%d -> N%d_%d [label=" %s" weight=100 tooltip="%s" labeltooltip="%s"]`+"\n", nodeID, nodeID, i, weight, weight, weight)
252252
if nts := lnts[t.Name]; nts != nil {
253253
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,
274274
}
275275
if w != 0 {
276276
weight := b.config.FormatValue(w)
277-
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)
277+
nodelets += fmt.Sprintf(`N%s_%d [label = "%s" id="N%s_%d" fontsize=8 shape=box3d tooltip="%s"]`+"\n", source, j, escapeForDotCentered(t.Name), source, j, weight)
278278
nodelets += fmt.Sprintf(`%s -> N%s_%d [label=" %s" weight=100 tooltip="%s" labeltooltip="%s"%s]`+"\n", source, source, j, weight, weight, weight, attr)
279279
}
280280
}
@@ -483,9 +483,16 @@ func escapeAllForDot(in []string) []string {
483483
return out
484484
}
485485

486-
// escapeForDot escapes double quotes and backslashes, and replaces Graphviz's
487-
// "center" character (\n) with a left-justified character.
486+
// escapeForDot escapes double quotes and backslashes, and replaces newlines
487+
// with a left-justified escape (\l).
488488
// See https://graphviz.org/docs/attr-types/escString/ for more info.
489489
func escapeForDot(str string) string {
490490
return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(str, `\`, `\\`), `"`, `\"`), "\n", `\l`)
491491
}
492+
493+
// escapeForDotCentered escapes double quotes and backslashes, and replaces
494+
// newlines with Graphviz's center escape (\n).
495+
// See https://graphviz.org/docs/attr-types/escString/ for more info.
496+
func escapeForDotCentered(str string) string {
497+
return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(str, `\`, `\\`), `"`, `\"`), "\n", `\n`)
498+
}

internal/graph/dotgraph_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,29 @@ func TestComposeWithNamesThatNeedEscaping(t *testing.T) {
150150
compareGraphs(t, buf.Bytes(), "compose7.dot")
151151
}
152152

153+
func TestComposeWithTagsThatNeedEscaping(t *testing.T) {
154+
g := baseGraph()
155+
a, c := baseAttrsAndConfig()
156+
g.Nodes[0].LabelTags["a"] = &Tag{
157+
Name: `label"quote"` + "\nline2",
158+
Cum: 10,
159+
Flat: 10,
160+
}
161+
g.Nodes[0].NumericTags[""] = TagMap{
162+
"b": &Tag{
163+
Name: `numeric"quote"` + "\nline2",
164+
Cum: 20,
165+
Flat: 20,
166+
Unit: "ms",
167+
},
168+
}
169+
170+
var buf bytes.Buffer
171+
ComposeDot(&buf, g, a, c)
172+
173+
compareGraphs(t, buf.Bytes(), "compose8.dot")
174+
}
175+
153176
func TestComposeWithCommentsWithNewlines(t *testing.T) {
154177
g := baseGraph()
155178
a, c := baseAttrsAndConfig()

internal/graph/graph.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,8 @@ func (g *Graph) TrimTree(kept NodePtrSet) {
519519
g.RemoveRedundantEdges()
520520
}
521521

522+
// joinLabels returns the labels as a string. Newlines in the labels are
523+
// replaced with "\n". Separate labels are joined with newlines.
522524
func joinLabels(s *profile.Sample) string {
523525
if len(s.Label) == 0 {
524526
return ""
@@ -527,11 +529,13 @@ func joinLabels(s *profile.Sample) string {
527529
var labels []string
528530
for key, vals := range s.Label {
529531
for _, v := range vals {
530-
labels = append(labels, key+":"+v)
532+
joined := key + ":" + v
533+
escaped := strings.ReplaceAll(joined, "\n", `\n`)
534+
labels = append(labels, escaped)
531535
}
532536
}
533537
sort.Strings(labels)
534-
return strings.Join(labels, `\n`)
538+
return strings.Join(labels, "\n")
535539
}
536540

537541
// isNegative returns true if the node is considered as "negative" for the

internal/graph/graph_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,3 +531,18 @@ func TestShortenFunctionName(t *testing.T) {
531531
}
532532
}
533533
}
534+
535+
func TestJoinLabels(t *testing.T) {
536+
input := &profile.Sample{
537+
Label: map[string][]string{
538+
"key1": {"v1", "v2"},
539+
// value with an embedded newline: is escaped to \n
540+
"key2": {"value line1\nline2"},
541+
},
542+
}
543+
const expected = "key1:v1\nkey1:v2\nkey2:value line1\\nline2"
544+
output := joinLabels(input)
545+
if output != expected {
546+
t.Errorf("output=%#v != expected=%#v", output, expected)
547+
}
548+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
digraph "testtitle" {
2+
node [style=filled fillcolor="#f8f8f8"]
3+
subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\llabel3: \"foo\"\l" tooltip="testtitle"] }
4+
N1 [label="src\n10 (10.00%)\nof 25 (25.00%)" id="node1" fontsize=22 shape=box tooltip="src (25)" color="#b23c00" fillcolor="#edddd5"]
5+
N1_0 [label = "label\"quote\"\nline2" id="N1_0" fontsize=8 shape=box3d tooltip="10"]
6+
N1 -> N1_0 [label=" 10" weight=100 tooltip="10" labeltooltip="10"]
7+
NN1_0 [label = "numeric\"quote\"\nline2" id="NN1_0" fontsize=8 shape=box3d tooltip="20"]
8+
N1 -> NN1_0 [label=" 20" weight=100 tooltip="20" labeltooltip="20"]
9+
N2 [label="dest\n15 (15.00%)\nof 25 (25.00%)" id="node2" fontsize=24 shape=box tooltip="dest (25)" color="#b23c00" fillcolor="#edddd5"]
10+
N1 -> N2 [label=" 10" weight=11 color="#b28559" tooltip="src -> dest (10)" labeltooltip="src -> dest (10)" minlen=2]
11+
}

0 commit comments

Comments
 (0)