internal/graph: Support comments with double quotes (#688)

The first comment in a pprof file is used as the subgraph ID for the
legend, but was not escaped. If the comment contained double quotes,
it could cause graphviz to fail to parse the output, or to render
incorrect graphs. To reproduce, run the following commands:

$ pprof -add_comment "unterminated \"double quote" -proto -output=bug.pprof in.pprof
Generating report in labels-no-specials-unterminated-double-quote.pprof
$ pprof -comments bug.pprof
unterminated "double quote
$ pprof -web bug.pprof
Error: <stdin>: syntax error in line 3 near '\'
pprof: failed to execute dot. Is Graphviz installed? Error: exit status 1

Add a test for this case. Without the change to dotgraph.go, the test
produced the following dot output:

digraph "testtitle" {
node [style=filled fillcolor="#f8f8f8"]
subgraph cluster_L { "comment line 1
comment line 2 "unterminated double quote" [shape=box fontsize=16 label="comment line 1\lcomment line 2 \"unterminated double quote\lsecond comment \"double quote\"\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"]
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)"]
}

This failed to parse with dot:

$ dot ./internal/graph/testdata/compose9.dot
Error: ./internal/graph/testdata/compose9.dot: syntax error in line 4 near '\'

After adding the escaping, the test case now parses with dot, and the
new test case works as expected.
diff --git a/internal/graph/dotgraph.go b/internal/graph/dotgraph.go
index 8008675..9ff4c95 100644
--- a/internal/graph/dotgraph.go
+++ b/internal/graph/dotgraph.go
@@ -126,7 +126,7 @@
 		return
 	}
 	title := labels[0]
-	fmt.Fprintf(b, `subgraph cluster_L { "%s" [shape=box fontsize=16`, title)
+	fmt.Fprintf(b, `subgraph cluster_L { "%s" [shape=box fontsize=16`, escapeForDot(title))
 	fmt.Fprintf(b, ` label="%s\l"`, strings.Join(escapeAllForDot(labels), `\l`))
 	if b.config.LegendURL != "" {
 		fmt.Fprintf(b, ` URL="%s" target="_blank"`, b.config.LegendURL)
@@ -485,7 +485,7 @@
 
 // escapeForDot escapes double quotes and backslashes, and replaces Graphviz's
 // "center" character (\n) with a left-justified character.
-// See https://graphviz.org/doc/info/attrs.html#k:escString for more info.
+// 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`)
 }
diff --git a/internal/graph/dotgraph_test.go b/internal/graph/dotgraph_test.go
index 08d80eb..0a94495 100644
--- a/internal/graph/dotgraph_test.go
+++ b/internal/graph/dotgraph_test.go
@@ -150,6 +150,19 @@
 	compareGraphs(t, buf.Bytes(), "compose7.dot")
 }
 
+func TestComposeWithCommentsWithNewlines(t *testing.T) {
+	g := baseGraph()
+	a, c := baseAttrsAndConfig()
+	// comments that could be added with the -add_comment command line tool
+	// the first label is used as the dot "node name"; the others are escaped as labels
+	c.Labels = []string{"comment line 1\ncomment line 2 \"unterminated double quote", `second comment "double quote"`}
+
+	var buf bytes.Buffer
+	ComposeDot(&buf, g, a, c)
+
+	compareGraphs(t, buf.Bytes(), "compose9.dot")
+}
+
 func baseGraph() *Graph {
 	src := &Node{
 		Info:        NodeInfo{Name: "src"},
diff --git a/internal/graph/testdata/compose9.dot b/internal/graph/testdata/compose9.dot
new file mode 100644
index 0000000..2e163ce
--- /dev/null
+++ b/internal/graph/testdata/compose9.dot
@@ -0,0 +1,7 @@
+digraph "testtitle" {
+node [style=filled fillcolor="#f8f8f8"]
+subgraph cluster_L { "comment line 1\lcomment line 2 \"unterminated double quote" [shape=box fontsize=16 label="comment line 1\lcomment line 2 \"unterminated double quote\lsecond comment \"double quote\"\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"]
+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)"]
+}