diff --git a/internal/graph/dotgraph.go b/internal/graph/dotgraph.go index 09debfb00..363444c20 100644 --- a/internal/graph/dotgraph.go +++ b/internal/graph/dotgraph.go @@ -127,7 +127,7 @@ func (b *builder) addLegend() { } title := labels[0] fmt.Fprintf(b, `subgraph cluster_L { "%s" [shape=box fontsize=16`, title) - fmt.Fprintf(b, ` label="%s\l"`, strings.Join(labels, `\l`)) + fmt.Fprintf(b, ` label="%s\l"`, strings.Join(escapeForDot(labels), `\l`)) if b.config.LegendURL != "" { fmt.Fprintf(b, ` URL="%s" target="_blank"`, b.config.LegendURL) } @@ -472,3 +472,13 @@ func min64(a, b int64) int64 { } return b } + +// 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. +func escapeForDot(s []string) []string { + for i := range s { + s[i] = strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(s[i], `\`, `\\`), `"`, `\"`), "\n", `\l`) + } + return s +} diff --git a/internal/graph/dotgraph_test.go b/internal/graph/dotgraph_test.go index b8368b8fa..4232efaa5 100644 --- a/internal/graph/dotgraph_test.go +++ b/internal/graph/dotgraph_test.go @@ -178,7 +178,7 @@ func baseAttrsAndConfig() (*DotAttributes, *DotConfig) { } c := &DotConfig{ Title: "testtitle", - Labels: []string{"label1", "label2"}, + Labels: []string{"label1", "label2", `label3: "foo"`}, Total: 100, FormatValue: func(v int64) string { return strconv.FormatInt(v, 10) @@ -326,6 +326,46 @@ func TestTagCollapse(t *testing.T) { } } +func TestEscapeForDot(t *testing.T) { + for _, tc := range []struct { + desc string + input []string + want []string + }{ + { + desc: "with multiple doubles quotes", + input: []string{`label: "foo" and "bar"`}, + want: []string{`label: \"foo\" and \"bar\"`}, + }, + { + desc: "with graphviz center line character", + input: []string{"label: foo \n bar"}, + want: []string{`label: foo \l bar`}, + }, + { + desc: "with two backslashes", + input: []string{`label: \\`}, + want: []string{`label: \\\\`}, + }, + { + desc: "with two double quotes together", + input: []string{`label: ""`}, + want: []string{`label: \"\"`}, + }, + { + desc: "with multiple labels", + input: []string{`label1: "foo"`, `label2: "bar"`}, + want: []string{`label1: \"foo\"`, `label2: \"bar\"`}, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + if got := escapeForDot(tc.input); !reflect.DeepEqual(got, tc.want) { + t.Errorf("escapeForDot(%s) = %s, want %s", tc.input, got, tc.want) + } + }) + } +} + func tagString(t []*Tag) string { var ret []string for _, s := range t { diff --git a/internal/graph/testdata/compose1.dot b/internal/graph/testdata/compose1.dot index da349a40a..a0842ee0e 100644 --- a/internal/graph/testdata/compose1.dot +++ b/internal/graph/testdata/compose1.dot @@ -1,6 +1,6 @@ digraph "testtitle" { node [style=filled fillcolor="#f8f8f8"] -subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l" tooltip="testtitle"] } +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"] 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)"] diff --git a/internal/graph/testdata/compose2.dot b/internal/graph/testdata/compose2.dot index 0c1a6ebaf..44c2aecd5 100644 --- a/internal/graph/testdata/compose2.dot +++ b/internal/graph/testdata/compose2.dot @@ -1,6 +1,6 @@ digraph "testtitle" { node [style=filled fillcolor="#f8f8f8"] -subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l" tooltip="testtitle"] } +subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\llabel3: \"foo\"\l" tooltip="testtitle"] } N1 [label="SRC10 (10.00%)\nof 25 (25.00%)" id="node1" fontsize=24 shape=folder tooltip="src (25)" color="#b23c00" fillcolor="#edddd5" style="bold,filled" peripheries=2 URL="www.google.com" target="_blank"] N2 [label="dest\n0 of 25 (25.00%)" id="node2" fontsize=8 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)"] diff --git a/internal/graph/testdata/compose3.dot b/internal/graph/testdata/compose3.dot index 1b878b79d..f22ad9fe4 100644 --- a/internal/graph/testdata/compose3.dot +++ b/internal/graph/testdata/compose3.dot @@ -1,6 +1,6 @@ digraph "testtitle" { node [style=filled fillcolor="#f8f8f8"] -subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l" tooltip="testtitle"] } +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 = "tag1" id="N1_0" fontsize=8 shape=box3d tooltip="10"] N1 -> N1_0 [label=" 10" weight=100 tooltip="10" labeltooltip="10"] diff --git a/internal/graph/testdata/compose4.dot b/internal/graph/testdata/compose4.dot index 302da8ce9..ed770d101 100644 --- a/internal/graph/testdata/compose4.dot +++ b/internal/graph/testdata/compose4.dot @@ -1,4 +1,4 @@ digraph "testtitle" { node [style=filled fillcolor="#f8f8f8"] -subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l" tooltip="testtitle"] } +subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\llabel3: \"foo\"\l" tooltip="testtitle"] } } diff --git a/internal/graph/testdata/compose5.dot b/internal/graph/testdata/compose5.dot index 8876e337e..3f2285c31 100644 --- a/internal/graph/testdata/compose5.dot +++ b/internal/graph/testdata/compose5.dot @@ -1,6 +1,6 @@ digraph "testtitle" { node [style=filled fillcolor="#f8f8f8"] -subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l" tooltip="testtitle"] } +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 = "tag1" id="N1_0" fontsize=8 shape=box3d tooltip="10"] N1 -> N1_0 [label=" 10" weight=100 tooltip="10" labeltooltip="10"] diff --git a/internal/graph/testdata/compose6.dot b/internal/graph/testdata/compose6.dot index cf884394c..1dfc3feeb 100644 --- a/internal/graph/testdata/compose6.dot +++ b/internal/graph/testdata/compose6.dot @@ -1,6 +1,6 @@ digraph "testtitle" { node [style=filled fillcolor="#f8f8f8"] -subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\l" URL="http://example.com" target="_blank" tooltip="testtitle"] } +subgraph cluster_L { "label1" [shape=box fontsize=16 label="label1\llabel2\llabel3: \"foo\"\l" URL="http://example.com" target="_blank" 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)"] diff --git a/internal/report/report.go b/internal/report/report.go index a34520891..bc5685d61 100644 --- a/internal/report/report.go +++ b/internal/report/report.go @@ -1207,7 +1207,7 @@ func reportLabels(rpt *Report, g *graph.Graph, origCount, droppedNodes, droppedE // Help new users understand the graph. // A new line is intentionally added here to better show this message. if fullHeaders { - label = append(label, "\\lSee https://git.io/JfYMW for how to read the graph") + label = append(label, "\nSee https://git.io/JfYMW for how to read the graph") } return label