redos_detector/
to_friendly.rs1use crate::{AnalysisLimit, Report, Score, TrailEntrySide};
8
9const TRUNCATE_LENGTH: usize = 100;
10
11pub const DEFAULT_RESULTS_LIMIT: u64 = 15;
13
14#[derive(Clone, Debug)]
16pub struct ToFriendlyConfig {
17 pub results_limit: Option<u64>,
19 pub always_include_trails: bool,
21}
22
23impl Default for ToFriendlyConfig {
24 fn default() -> Self {
25 ToFriendlyConfig {
26 results_limit: Some(DEFAULT_RESULTS_LIMIT),
27 always_include_trails: false,
28 }
29 }
30}
31
32fn utf16_len(s: &str) -> usize {
33 s.encode_utf16().count()
34}
35
36fn pad_start(s: &str, width: usize) -> String {
37 let len = utf16_len(s);
38 if len >= width {
39 s.to_string()
40 } else {
41 format!("{}{}", " ".repeat(width - len), s)
42 }
43}
44
45fn pad_end(s: &str, width: usize) -> String {
46 let len = utf16_len(s);
47 if len >= width {
48 s.to_string()
49 } else {
50 format!("{}{}", s, " ".repeat(width - len))
51 }
52}
53
54fn get_breadcrumbs(side: &TrailEntrySide) -> String {
55 let mut parts: Vec<String> = side
56 .backreference_stack
57 .iter()
58 .map(|b| b.node.start.offset.to_string())
59 .collect();
60 parts.reverse();
61 parts.push(side.node.start.offset.to_string());
62 parts.join("\u{2192}")
63}
64
65fn score_string(score: Score) -> String {
66 match score {
67 Score::Infinite => "There could be infinite backtracks.".to_string(),
68 Score::Finite(value) => format!("Score: {}", value),
69 }
70}
71
72pub fn to_friendly(result: &Report, config: &ToFriendlyConfig) -> String {
76 let results_limit = config.results_limit;
77 let score_str = score_string(result.score);
78
79 if result.is_safe() && !config.always_include_trails {
80 return format!("Regex is safe. {}", score_str);
81 }
82
83 let mut output_lines: Vec<String> = Vec::new();
84
85 if result.pattern_downgraded {
86 output_lines.push(format!("Pattern was downgraded to `{}`.", result.pattern));
87 }
88
89 if result.trails.is_empty() {
90 let mut parts: Vec<String> = Vec::new();
91 parts.push(if result.is_safe() {
92 format!("Regex is safe. {}", score_str)
93 } else {
94 format!("Regex may not be safe. {}", score_str)
95 });
96 if result.error == Some(AnalysisLimit::TimedOut) {
97 parts.push("Timed out.".to_string());
98 }
99 if result.error == Some(AnalysisLimit::HitMaxSteps) {
100 parts.push("Reached steps limit.".to_string());
101 }
102 if !result.is_safe() {
103 parts.push("The pattern may have too many variations.".to_string());
104 }
105 output_lines.push(parts.join(" "));
106 } else {
107 let limit = results_limit.map_or(result.trails.len(), |n| n as usize);
108 let result_blocks: Vec<String> =
109 result.trails.iter().take(limit).map(render_block).collect();
110
111 output_lines.push(format!(
112 "Regex is {}safe. {}",
113 if !result.is_safe() { "not " } else { "" },
114 score_str
115 ));
116
117 if results_limit != Some(0) {
118 output_lines.push(String::new());
119 let plural = if result.trails.len() > 1 { "s" } else { "" };
120 let singular = if result.trails.len() == 1 { "s" } else { "" };
121 output_lines.push(format!(
122 "The following trail{} show{} how the same input can be matched multiple ways.",
123 plural, singular
124 ));
125 output_lines.extend(result_blocks);
126 output_lines.push(String::new());
127 if let Some(error) = result.error {
128 output_lines.push(error_message(error).to_string());
129 }
130 if results_limit.is_some_and(|n| result.trails.len() as u64 > n) {
131 output_lines
132 .push("There are more results than this but hit results limit.".to_string());
133 }
134 }
135 }
136
137 output_lines.join("\n")
138}
139
140fn render_block(trail: &crate::Trail) -> String {
141 let row_contents: Vec<[String; 4]> = trail
142 .entries
143 .iter()
144 .take(TRUNCATE_LENGTH)
145 .map(|entry| {
146 [
147 get_breadcrumbs(&entry.a),
148 format!("`{}`", entry.a.node.source),
149 get_breadcrumbs(&entry.b),
150 format!("`{}`", entry.b.node.source),
151 ]
152 })
153 .collect();
154
155 let max_col1 = row_contents
156 .iter()
157 .map(|c| utf16_len(&c[0]))
158 .max()
159 .unwrap_or(0);
160 let max_col2 = row_contents
161 .iter()
162 .map(|c| utf16_len(&c[1]))
163 .max()
164 .unwrap_or(0);
165 let max_col3 = row_contents
166 .iter()
167 .map(|c| utf16_len(&c[2]))
168 .max()
169 .unwrap_or(0);
170
171 let mut rows: Vec<String> = row_contents
172 .iter()
173 .map(|c| {
174 format!(
175 "{}: {} | {}: {}",
176 pad_start(&c[0], max_col1),
177 pad_end(&c[1], max_col2),
178 pad_start(&c[2], max_col3),
179 c[3]
180 )
181 })
182 .collect();
183
184 if trail.entries.len() > TRUNCATE_LENGTH {
185 rows.push("\u{2026}".to_string());
186 }
187
188 let max_row_length = rows.iter().map(|r| utf16_len(r)).max().unwrap_or(0);
189 rows.push("=".repeat(max_row_length));
190
191 rows.join("\n")
192}
193
194fn error_message(error: AnalysisLimit) -> &'static str {
195 match error {
196 AnalysisLimit::HitMaxScore => {
197 "Hit the max score so there may be more results than shown here."
198 }
199 AnalysisLimit::HitMaxSteps => {
200 "Hit maximum number of steps so there may be more results than shown here."
201 }
202 AnalysisLimit::TimedOut => "Timed out so there may be more results than shown here.",
203 }
204}