Skip to content

Commit d4b0b0e

Browse files
authored
Escape quotes in DOT (#14657)
1 parent c6ab1db commit d4b0b0e

File tree

2 files changed

+119
-1
lines changed

2 files changed

+119
-1
lines changed

lib/mix/lib/mix/utils.ex

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,33 @@ defmodule Mix.Utils do
481481
[" ", parent, quoted(name), edge_info, ?\n]
482482
end
483483

484-
defp quoted(data), do: [?", to_string(data), ?"]
484+
defp quoted(data) do
485+
string = to_string(data)
486+
escape_dot_string(string, <<?">>)
487+
end
488+
489+
# Escape a string for DOT format according to GraphViz specification https://graphviz.org/doc/info/lang.html
490+
# - Only quotes need escaping
491+
# - The ending quote should not be escaped (which requires an even of trailing backslashes)
492+
defp escape_dot_string(<<?\\, ?\\, rest::binary>>, acc) do
493+
escape_dot_string(rest, <<acc::binary, ?\\, ?\\>>)
494+
end
495+
496+
defp escape_dot_string(<<?", rest::binary>>, acc) do
497+
escape_dot_string(rest, <<acc::binary, ?\\, ?">>)
498+
end
499+
500+
defp escape_dot_string(<<?\\>>, acc) do
501+
<<acc::binary, ?\\, ?\\, ?">>
502+
end
503+
504+
defp escape_dot_string(<<char, rest::binary>>, acc) do
505+
escape_dot_string(rest, <<acc::binary, char>>)
506+
end
507+
508+
defp escape_dot_string(<<>>, acc) do
509+
<<acc::binary, ?">>
510+
end
485511

486512
@doc false
487513
@deprecated "Use Macro.underscore/1 instead"

lib/mix/test/mix/utils_test.exs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,98 @@ defmodule Mix.UtilsTest do
228228
end
229229
end
230230

231+
describe "write_dot_graph!/4" do
232+
test "preserves newlines and other control characters" do
233+
in_tmp("dot_newlines", fn ->
234+
callback = fn node -> {{node, nil}, []} end
235+
236+
Mix.Utils.write_dot_graph!("graph.dot", "graph", ["foo \nbar\r\nbaz"], callback, [])
237+
238+
assert File.read!("graph.dot") == """
239+
digraph "graph" {
240+
"foo
241+
bar\r
242+
baz"
243+
}
244+
"""
245+
end)
246+
end
247+
248+
test "quote and backslash combinations" do
249+
in_tmp("dot_complex", fn ->
250+
callback = fn node -> {{node, nil}, []} end
251+
252+
test_cases = [
253+
# "fo"o" -> "fo\"o"
254+
{"fo\"o", "fo\\\"o"},
255+
# "fo\"o" -> "fo\\\"o"
256+
{"fo\\\"o", "fo\\\\\"o"},
257+
# "fo\o" -> "fo\o"
258+
{"fo\\o", "fo\\o"},
259+
# "fo\\o" -> "fo\\o"
260+
{"fo\\\\o", "fo\\\\o"},
261+
# "fo\\\o" -> "fo\\\o"
262+
{"fo\\\\\\o", "fo\\\\\\o"}
263+
]
264+
265+
Enum.each(test_cases, fn {input, expected} ->
266+
Mix.Utils.write_dot_graph!("graph.dot", "graph", [input], callback, [])
267+
content = File.read!("graph.dot")
268+
assert content == "digraph \"graph\" {\n \"#{expected}\"\n}\n"
269+
end)
270+
end)
271+
end
272+
273+
test "escapes backslash at end of string" do
274+
in_tmp("dot_end_backslash", fn ->
275+
callback = fn node -> {{node, nil}, []} end
276+
277+
test_cases = [
278+
# "fo\" -> "fo\\" (add backslash)
279+
{"fo\\", "fo\\\\"},
280+
# "fo\\" -> "fo\\" (already valid)
281+
{"fo\\\\", "fo\\\\"},
282+
# "fo\\\" -> "fo\\\\" (add backslash)
283+
{"fo\\\\\\", "fo\\\\\\\\"}
284+
]
285+
286+
Enum.each(test_cases, fn {input, expected} ->
287+
Mix.Utils.write_dot_graph!("graph.dot", "graph", [input], callback, [])
288+
content = File.read!("graph.dot")
289+
assert content == "digraph \"graph\" {\n \"#{expected}\"\n}\n"
290+
end)
291+
end)
292+
end
293+
294+
test "handles empty strings" do
295+
in_tmp("dot_empty", fn ->
296+
callback = fn node -> {{node, nil}, []} end
297+
298+
Mix.Utils.write_dot_graph!("graph.dot", "graph", [""], callback, [])
299+
300+
assert File.read!("graph.dot") == """
301+
digraph "graph" {
302+
""
303+
}
304+
"""
305+
end)
306+
end
307+
308+
test "handles edge labels with escaping" do
309+
in_tmp("dot_edge_labels", fn ->
310+
callback = fn node -> {{node, "edge \"label\""}, []} end
311+
312+
Mix.Utils.write_dot_graph!("graph.dot", "graph", ["node"], callback, [])
313+
314+
assert File.read!("graph.dot") == """
315+
digraph "graph" {
316+
"node" [label="edge \\"label\\""]
317+
}
318+
"""
319+
end)
320+
end
321+
end
322+
231323
defp assert_ebin_symlinked_or_copied(result) do
232324
case result do
233325
{:ok, paths} ->

0 commit comments

Comments
 (0)