1#![deny(missing_docs)]
2
3use petgraph::graph::{DiGraph, NodeIndex};
4use petgraph::visit::{EdgeRef, IntoNodeReferences};
5
6use crate::utils::GBF_DARK_GRAY;
7
8pub trait RenderableNode {
10 fn render_node(&self, padding: usize) -> String;
12}
13
14pub trait NodeResolver {
16 type NodeData: RenderableNode;
18
19 fn resolve(&self, node_index: NodeIndex) -> Option<&Self::NodeData>;
21
22 fn resolve_edge_color(&self, source: NodeIndex, target: NodeIndex) -> String;
24
25 fn resolve_border_color(&self, _: NodeIndex) -> Option<String> {
27 None
28 }
29}
30
31pub trait DotRenderableGraph: NodeResolver {
33 fn render_dot(&self, config: CfgDotConfig) -> String;
35}
36
37#[derive(Debug)]
39pub struct CfgDotConfig {
40 pub rankdir: String,
42 pub edge_color: String,
44 pub node_shape: String,
46 pub fontname: String,
48 pub fontsize: String,
50 pub fillcolor: String,
52}
53
54impl Default for CfgDotConfig {
55 fn default() -> Self {
56 Self {
57 rankdir: "TB".to_string(),
58 edge_color: "#ffffff".to_string(),
59 node_shape: "box".to_string(),
60 fontname: "Courier".to_string(),
61 fontsize: "12".to_string(),
62 fillcolor: GBF_DARK_GRAY.to_string(),
63 }
64 }
65}
66
67pub struct CfgDotBuilder {
69 config: CfgDotConfig,
70}
71
72impl CfgDotBuilder {
73 pub fn new() -> Self {
75 Self {
76 config: CfgDotConfig::default(),
77 }
78 }
79
80 pub fn rankdir(mut self, rankdir: &str) -> Self {
82 self.config.rankdir = rankdir.to_string();
83 self
84 }
85
86 pub fn edge_color(mut self, edge_color: &str) -> Self {
88 self.config.edge_color = edge_color.to_string();
89 self
90 }
91
92 pub fn node_shape(mut self, node_shape: &str) -> Self {
94 self.config.node_shape = node_shape.to_string();
95 self
96 }
97
98 pub fn fontname(mut self, fontname: &str) -> Self {
100 self.config.fontname = fontname.to_string();
101 self
102 }
103
104 pub fn fontsize(mut self, fontsize: &str) -> Self {
106 self.config.fontsize = fontsize.to_string();
107 self
108 }
109
110 pub fn fillcolor(mut self, fillcolor: &str) -> Self {
112 self.config.fillcolor = fillcolor.to_string();
113 self
114 }
115
116 pub fn build(self) -> CfgDot {
118 CfgDot {
119 config: self.config,
120 }
121 }
122}
123
124pub struct CfgDot {
126 pub config: CfgDotConfig,
128}
129
130impl CfgDot {
131 pub fn render<R, N, E>(&self, graph: &DiGraph<N, E>, resolver: &R) -> String
158 where
159 R: NodeResolver,
160 {
161 let mut dot = String::new();
163
164 dot.push_str("digraph CFG {\n");
166 dot.push_str(&format!(
167 " graph [rankdir={}, bgcolor=\"transparent\", splines=\"ortho\"];\n",
168 self.config.rankdir
169 ));
170 dot.push_str(&format!(
171 " edge [color=\"{}\"]; \n",
172 self.config.edge_color
173 ));
174 dot.push_str(&format!(
175 " node [shape=\"{}\", fontname=\"{}\", fontsize=\"{}\"]; \n",
176 self.config.node_shape, self.config.fontname, self.config.fontsize
177 ));
178
179 for (node_index, _node_data) in graph.node_references() {
181 if let Some(data) = resolver.resolve(node_index) {
183 let border_color = resolver.resolve_border_color(node_index);
184 let border_color_str = border_color.clone().unwrap_or("none".to_string());
186 let border_width = if border_color.is_some() {
187 ", penwidth=2"
188 } else {
189 ""
190 };
191 dot.push_str(&format!(
192 " N{} [style=filled, fillcolor=\"{}\", color=\"{}\"{}, label=<\n{}\n >];\n",
193 node_index.index(),
194 self.config.fillcolor,
195 border_color_str,
196 border_width,
197 data.render_node(8)
198 ));
199 }
200 }
201
202 for edge in graph.edge_references() {
204 let source = edge.source();
205 let target = edge.target();
206
207 if resolver.resolve(source).is_some() && resolver.resolve(target).is_some() {
209 let edge_color = resolver.resolve_edge_color(source, target);
210
211 dot.push_str(&format!(
213 " N{} -> N{} [color=\"{}\"]; \n",
214 source.index(),
215 target.index(),
216 edge_color
217 ));
218 }
219 }
220
221 dot.push_str("}\n");
225
226 dot
227 }
228}
229
230impl Default for CfgDotBuilder {
232 fn default() -> Self {
233 Self::new()
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use petgraph::graph::{DiGraph, NodeIndex};
241 use std::collections::HashMap;
242
243 struct MockNode {
245 label: String,
246 }
247
248 impl RenderableNode for MockNode {
249 fn render_node(&self, padding: usize) -> String {
250 format!("{}{}", " ".repeat(padding), self.label)
251 }
252 }
253
254 struct MockResolver {
256 nodes: HashMap<NodeIndex, MockNode>,
257 }
258
259 impl NodeResolver for MockResolver {
260 type NodeData = MockNode;
261
262 fn resolve(&self, node_index: NodeIndex) -> Option<&Self::NodeData> {
263 self.nodes.get(&node_index)
264 }
265
266 fn resolve_edge_color(&self, _source: NodeIndex, _target: NodeIndex) -> String {
267 "#ff0000".to_string()
268 }
269 }
270
271 #[test]
272 fn test_cfgdot_default_render() {
273 let mut graph = DiGraph::new();
275 let a = graph.add_node(());
276 let b = graph.add_node(());
277 graph.add_edge(a, b, ());
278
279 let resolver = MockResolver {
281 nodes: vec![
282 (
283 a,
284 MockNode {
285 label: "Node A".to_string(),
286 },
287 ),
288 (
289 b,
290 MockNode {
291 label: "Node B".to_string(),
292 },
293 ),
294 ]
295 .into_iter()
296 .collect(),
297 };
298
299 let cfg_dot = CfgDotBuilder::new().build();
301 let dot_output = cfg_dot.render(&graph, &resolver);
302
303 assert!(dot_output.contains("digraph CFG {"));
305 assert!(dot_output.contains("graph [rankdir=TB"));
306 assert!(dot_output.contains(&format!(
307 "N0 [style=filled, fillcolor=\"{}\"",
308 GBF_DARK_GRAY
309 )));
310 assert!(dot_output.contains("Node A"));
311 assert!(dot_output.contains("Node B"));
312 assert!(dot_output.contains("N0 -> N1"));
313 }
314
315 #[test]
316 fn test_cfgdot_custom_config() {
317 let mut graph = DiGraph::new();
319 let a = graph.add_node(());
320 let b = graph.add_node(());
321 graph.add_edge(a, b, ());
322
323 let resolver = MockResolver {
325 nodes: vec![
326 (
327 a,
328 MockNode {
329 label: "Node A".to_string(),
330 },
331 ),
332 (
333 b,
334 MockNode {
335 label: "Node B".to_string(),
336 },
337 ),
338 ]
339 .into_iter()
340 .collect(),
341 };
342
343 let cfg_dot = CfgDotBuilder::new()
345 .rankdir("LR")
346 .edge_color("#ff0000")
347 .node_shape("record")
348 .fontname("Arial")
349 .fontsize("14")
350 .fillcolor("#ff0000")
351 .build();
352 let dot_output = cfg_dot.render(&graph, &resolver);
353
354 assert!(dot_output.contains("digraph CFG {"));
356 assert!(dot_output.contains("graph [rankdir=LR"));
357 assert!(dot_output.contains("edge [color=\"#ff0000\"]"));
358 assert!(
359 dot_output.contains("node [shape=\"record\", fontname=\"Arial\", fontsize=\"14\"]")
360 );
361 assert!(dot_output.contains("N0 [style=filled, fillcolor=\"#ff0000\""));
362 assert!(dot_output.contains("Node A"));
363 assert!(dot_output.contains("Node B"));
364 assert!(dot_output.contains("N0 -> N1"));
365 }
366
367 #[test]
368 fn test_cfgdot_default_config() {
369 let mut graph = DiGraph::new();
371 let a = graph.add_node(());
372 let b = graph.add_node(());
373 graph.add_edge(a, b, ());
374
375 let resolver = MockResolver {
377 nodes: vec![
378 (
379 a,
380 MockNode {
381 label: "Node A".to_string(),
382 },
383 ),
384 (
385 b,
386 MockNode {
387 label: "Node B".to_string(),
388 },
389 ),
390 ]
391 .into_iter()
392 .collect(),
393 };
394
395 let cfg_dot = CfgDotBuilder::default().build();
397 let dot_output = cfg_dot.render(&graph, &resolver);
398
399 assert!(dot_output.contains("digraph CFG {"));
401 assert!(dot_output.contains("graph [rankdir=TB"));
402 assert!(dot_output.contains(&format!(
403 "N0 [style=filled, fillcolor=\"{}\"",
404 GBF_DARK_GRAY
405 )));
406 assert!(dot_output.contains("Node A"));
407 assert!(dot_output.contains("Node B"));
408 assert!(dot_output.contains("N0 -> N1"));
409 }
410
411 #[test]
412 fn test_cfgdot_no_nodes() {
413 let graph: DiGraph<(), ()> = DiGraph::new();
415
416 let resolver = MockResolver {
418 nodes: HashMap::new(),
419 };
420
421 let cfg_dot = CfgDotBuilder::new().build();
423 let dot_output = cfg_dot.render(&graph, &resolver);
424
425 assert!(dot_output.contains("digraph CFG {"));
427 assert!(dot_output.contains("}")); assert!(!dot_output.contains("N0")); }
430
431 #[test]
432 fn test_cfgdot_multiple_edges() {
433 let mut graph = DiGraph::new();
435 let a = graph.add_node(());
436 let b = graph.add_node(());
437 let c = graph.add_node(());
438 graph.add_edge(a, b, ());
439 graph.add_edge(b, c, ());
440 graph.add_edge(a, c, ());
441
442 let resolver = MockResolver {
444 nodes: vec![
445 (
446 a,
447 MockNode {
448 label: "Node A".to_string(),
449 },
450 ),
451 (
452 b,
453 MockNode {
454 label: "Node B".to_string(),
455 },
456 ),
457 (
458 c,
459 MockNode {
460 label: "Node C".to_string(),
461 },
462 ),
463 ]
464 .into_iter()
465 .collect(),
466 };
467
468 let cfg_dot = CfgDotBuilder::new().build();
470 let dot_output = cfg_dot.render(&graph, &resolver);
471
472 assert!(dot_output.contains("N0 -> N1"));
474 assert!(dot_output.contains("N1 -> N2"));
475 assert!(dot_output.contains("N0 -> N2"));
476 }
477
478 #[test]
480 fn test_cfgdot_missing_node() {
481 let mut graph = DiGraph::new();
483 let a = graph.add_node(());
484 let b = graph.add_node(());
485 graph.add_edge(a, b, ());
486
487 let resolver = MockResolver {
489 nodes: vec![(
490 a,
491 MockNode {
492 label: "Node A".to_string(),
493 },
494 )]
495 .into_iter()
496 .collect(),
497 };
498
499 let cfg_dot = CfgDotBuilder::new().build();
501 let dot_output = cfg_dot.render(&graph, &resolver);
502
503 assert!(dot_output.contains(&format!(
505 "N0 [style=filled, fillcolor=\"{}\"",
506 GBF_DARK_GRAY
507 )));
508 assert!(!dot_output.contains("N1")); }
510}