From 48de501c086643153a42e7e94a73eb70ab9d065b Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Tue, 15 Mar 2022 10:29:32 -0400 Subject: [PATCH] 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: : 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). --- internal/graph/dotgraph.go | 15 +++++++++++---- internal/graph/dotgraph_test.go | 23 +++++++++++++++++++++++ internal/graph/graph.go | 8 ++++++-- internal/graph/graph_test.go | 15 +++++++++++++++ internal/graph/testdata/compose8.dot | 11 +++++++++++ 5 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 internal/graph/testdata/compose8.dot diff --git a/internal/graph/dotgraph.go b/internal/graph/dotgraph.go index 9ff4c95ad..e15b9b5b0 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, escapeForDotCentered(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, escapeForDotCentered(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) } } @@ -483,9 +483,16 @@ func escapeAllForDot(in []string) []string { return out } -// escapeForDot escapes double quotes and backslashes, and replaces Graphviz's -// "center" character (\n) with a left-justified character. +// escapeForDot escapes double quotes and backslashes, and replaces newlines +// with a left-justified escape (\l). // See https://graphviz.org/docs/attr-types/escString/ for more info. func escapeForDot(str string) string { return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(str, `\`, `\\`), `"`, `\"`), "\n", `\l`) } + +// escapeForDotCentered escapes double quotes and backslashes, and replaces +// newlines with Graphviz's center escape (\n). +// See https://graphviz.org/docs/attr-types/escString/ for more info. +func escapeForDotCentered(str string) string { + return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(str, `\`, `\\`), `"`, `\"`), "\n", `\n`) +} diff --git a/internal/graph/dotgraph_test.go b/internal/graph/dotgraph_test.go index 0a9449523..d91ba777c 100644 --- a/internal/graph/dotgraph_test.go +++ b/internal/graph/dotgraph_test.go @@ -150,6 +150,29 @@ func TestComposeWithNamesThatNeedEscaping(t *testing.T) { compareGraphs(t, buf.Bytes(), "compose7.dot") } +func TestComposeWithTagsThatNeedEscaping(t *testing.T) { + g := baseGraph() + a, c := baseAttrsAndConfig() + g.Nodes[0].LabelTags["a"] = &Tag{ + Name: `label"quote"` + "\nline2", + Cum: 10, + Flat: 10, + } + g.Nodes[0].NumericTags[""] = TagMap{ + "b": &Tag{ + Name: `numeric"quote"` + "\nline2", + Cum: 20, + Flat: 20, + Unit: "ms", + }, + } + + var buf bytes.Buffer + ComposeDot(&buf, g, a, c) + + compareGraphs(t, buf.Bytes(), "compose8.dot") +} + func TestComposeWithCommentsWithNewlines(t *testing.T) { g := baseGraph() a, c := baseAttrsAndConfig() diff --git a/internal/graph/graph.go b/internal/graph/graph.go index 74b904c40..a2bb0e288 100644 --- a/internal/graph/graph.go +++ b/internal/graph/graph.go @@ -519,6 +519,8 @@ func (g *Graph) TrimTree(kept NodePtrSet) { g.RemoveRedundantEdges() } +// joinLabels returns the labels as a string. Newlines in the labels are +// replaced with "\n". Separate labels are joined with newlines. func joinLabels(s *profile.Sample) string { if len(s.Label) == 0 { return "" @@ -527,11 +529,13 @@ func joinLabels(s *profile.Sample) string { var labels []string for key, vals := range s.Label { for _, v := range vals { - labels = append(labels, key+":"+v) + joined := key + ":" + v + escaped := strings.ReplaceAll(joined, "\n", `\n`) + labels = append(labels, escaped) } } sort.Strings(labels) - return strings.Join(labels, `\n`) + return strings.Join(labels, "\n") } // isNegative returns true if the node is considered as "negative" for the diff --git a/internal/graph/graph_test.go b/internal/graph/graph_test.go index bdcb984ee..571d7eccd 100644 --- a/internal/graph/graph_test.go +++ b/internal/graph/graph_test.go @@ -531,3 +531,18 @@ func TestShortenFunctionName(t *testing.T) { } } } + +func TestJoinLabels(t *testing.T) { + input := &profile.Sample{ + Label: map[string][]string{ + "key1": {"v1", "v2"}, + // value with an embedded newline: is escaped to \n + "key2": {"value line1\nline2"}, + }, + } + const expected = "key1:v1\nkey1:v2\nkey2:value line1\\nline2" + output := joinLabels(input) + if output != expected { + t.Errorf("output=%#v != expected=%#v", output, expected) + } +} diff --git a/internal/graph/testdata/compose8.dot b/internal/graph/testdata/compose8.dot new file mode 100644 index 000000000..fefb64bbd --- /dev/null +++ b/internal/graph/testdata/compose8.dot @@ -0,0 +1,11 @@ +digraph "testtitle" { +node [style=filled fillcolor="#f8f8f8"] +subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\llabel3: \"foo\"\l" tooltip="testtitle"] } +N1 [label="src\n10 (10.00%)\nof 25 (25.00%)" id="node1" fontsize=22 shape=box tooltip="src (25)" color="#b23c00" fillcolor="#edddd5"] +N1_0 [label = "label\"quote\"\nline2" id="N1_0" fontsize=8 shape=box3d tooltip="10"] +N1 -> N1_0 [label=" 10" weight=100 tooltip="10" labeltooltip="10"] +NN1_0 [label = "numeric\"quote\"\nline2" id="NN1_0" fontsize=8 shape=box3d tooltip="20"] +N1 -> NN1_0 [label=" 20" weight=100 tooltip="20" labeltooltip="20"] +N2 [label="dest\n15 (15.00%)\nof 25 (25.00%)" id="node2" fontsize=24 shape=box tooltip="dest (25)" color="#b23c00" fillcolor="#edddd5"] +N1 -> N2 [label=" 10" weight=11 color="#b28559" tooltip="src -> dest (10)" labeltooltip="src -> dest (10)" minlen=2] +}