1use colored::*;
2
3#[derive(Debug, Clone, Copy)]
4pub enum ColorScheme {
5 Ripe,
6 BgpTools,
7 Mtf,
8 None,
9}
10
11pub struct OutputColorizer;
12
13impl OutputColorizer {
14 pub fn detect_scheme(output: &str) -> ColorScheme {
16 if Self::is_bgp_tools_format(output) {
17 ColorScheme::BgpTools
18 } else {
19 ColorScheme::Ripe
20 }
21 }
22
23 pub fn colorize(output: &str, scheme: ColorScheme) -> String {
25 match scheme {
26 ColorScheme::Ripe => Self::colorize_ripe(output),
27 ColorScheme::BgpTools => Self::colorize_bgptools(output),
28 ColorScheme::Mtf => Self::colorize_mtf(output),
29 ColorScheme::None => output.to_string(),
30 }
31 }
32
33 fn is_bgp_tools_format(output: &str) -> bool {
35 let lines: Vec<&str> = output.lines().collect();
36 if lines.len() >= 2 {
37 let first_line = lines[0].trim();
38 return first_line.contains("AS") &&
39 first_line.contains("|") &&
40 (first_line.contains("BGP") ||
41 first_line.contains("CC") ||
42 first_line.contains("Registry"));
43 }
44 false
45 }
46
47 fn colorize_ripe(output: &str) -> String {
49 let mut colored_lines = Vec::new();
50 let mut in_comment_block = false;
51
52 for line in output.lines() {
53 if line.starts_with('%') || line.starts_with('#') || line.starts_with("remarks:") {
55 colored_lines.push(line.bright_black().to_string());
56 in_comment_block = true;
57 continue;
58 }
59
60 if in_comment_block && line.trim().is_empty() {
62 colored_lines.push(line.to_string());
63 continue;
64 }
65
66 if in_comment_block && !line.trim().is_empty() {
67 in_comment_block = false;
68 }
69
70 if line.trim().is_empty() {
72 colored_lines.push(line.to_string());
73 continue;
74 }
75
76 if line.contains(':') {
78 if let Some(colored_line) = Self::colorize_field_value_pair(line) {
79 colored_lines.push(colored_line);
80 continue;
81 }
82 }
83
84 colored_lines.push(Self::colorize_special_lines(line));
86 }
87
88 colored_lines.join("\n")
89 }
90
91 fn colorize_field_value_pair(line: &str) -> Option<String> {
93 let parts: Vec<&str> = line.splitn(2, ':').collect();
94 if parts.len() != 2 {
95 return None;
96 }
97
98 let field = parts[0].trim();
99 let value = parts[1].trim();
100
101 let colored_field = Self::colorize_field_name(field);
102 let colored_value = Self::colorize_field_value(field, value);
103
104 Some(format!("{}: {}", colored_field, colored_value))
105 }
106
107 fn colorize_field_name(field: &str) -> String {
109 match field.to_lowercase().as_str() {
110 "aut-num" | "as-block" | "inet6num" | "inetnum" | "route" | "route6" | "netname" =>
112 field.bright_cyan().to_string(),
113
114 "domain" | "domain name" =>
116 field.bright_cyan().bold().to_string(),
117
118 "nserver" | "name server" | "nameserver" | "name servers" =>
120 field.yellow().bold().to_string(),
121
122 "domain status" | "status" =>
124 field.bright_yellow().to_string(),
125
126 "registrar" | "sponsoring registrar" | "registrar iana id" | "reseller" =>
128 field.bright_blue().to_string(),
129
130 "registry domain id" | "registrar whois server" | "registrar url" =>
132 field.blue().to_string(),
133
134 "creation date" | "created" | "created on" | "registration date" |
136 "updated date" | "last modified" | "last update" | "changed" |
137 "expiration date" | "expiry date" | "registry expiry date" |
138 "registrar registration expiration date" =>
139 field.bright_magenta().to_string(),
140
141 "privacy" | "whois privacy" | "domain privacy" =>
143 field.bright_red().to_string(),
144
145 "as-name" | "org-name" | "role" | "person" | "registrant name" |
147 "admin name" | "tech name" =>
148 field.bright_green().to_string(),
149
150 "org" | "organisation" | "org-type" | "registrant organization" | "registrant" =>
152 field.yellow().to_string(),
153
154 "admin-c" | "tech-c" | "abuse-c" | "nic-hdl" | "abuse-mailbox" |
156 "registrant contact" | "admin contact" | "technical contact" | "billing contact" =>
157 field.green().to_string(),
158
159 "mnt-by" | "mnt-ref" | "mnt-domains" | "mnt-lower" | "mnt-routes" =>
161 field.bright_blue().to_string(),
162
163 "import" | "export" | "mp-import" | "mp-export" | "default" | "peer" =>
165 field.magenta().to_string(),
166
167 "country" | "address" | "source" | "registrant country" |
169 "admin country" | "tech country" =>
170 field.bright_white().to_string(),
171
172 "e-mail" | "email" | "phone" | "registrant email" | "admin email" | "tech email" =>
174 field.blue().to_string(),
175
176 "dnssec" | "ds record" =>
178 field.magenta().bold().to_string(),
179
180 "sponsoring-org" =>
182 field.bright_yellow().to_string(),
183
184 _ => field.white().to_string(),
186 }
187 }
188
189 fn colorize_field_value(field: &str, value: &str) -> String {
191 let field_lower = field.to_lowercase();
192
193 if field_lower == "domain" || field_lower == "domain name" {
195 return value.bright_white().bold().to_string();
196 }
197
198 if field_lower == "aut-num" {
200 return value.bright_red().bold().to_string();
201 }
202
203 if field_lower == "status" || field_lower == "domain status" {
205 return Self::colorize_status_value(value);
206 }
207
208 if field_lower == "source" {
210 return value.bright_blue().to_string();
211 }
212
213 if field_lower == "country" || field_lower.contains("country") {
215 return value.yellow().to_string();
216 }
217
218 if field_lower.contains("name server") || field_lower.contains("nserver") ||
220 field_lower == "nameserver" {
221 return value.bright_green().to_string();
222 }
223
224 if field_lower.contains("registrar") {
226 return value.bright_blue().bold().to_string();
227 }
228
229 if field_lower.contains("dnssec") {
231 return if value.to_lowercase().contains("signed") || value.to_lowercase().contains("yes") {
232 value.bright_green().to_string()
233 } else {
234 value.bright_red().to_string()
235 };
236 }
237
238 if field_lower.contains("date") || field_lower.contains("created") ||
240 field_lower.contains("changed") || field_lower.contains("expir") ||
241 field_lower.contains("update") {
242 return value.bright_magenta().to_string();
243 }
244
245 if value.contains('@') {
247 return value.bright_yellow().to_string();
248 }
249
250 if field_lower.contains("phone") {
252 return value.bright_white().to_string();
253 }
254
255 if value.starts_with("AS") && value.len() > 2 && value[2..].chars().all(|c| c.is_digit(10)) {
257 return value.bright_red().to_string();
258 }
259
260 if (field == "import" || field == "export") && value.contains("AS") {
262 return Self::colorize_routing_policy(value);
263 }
264
265 if Self::looks_like_ip_or_cidr(value) {
267 return value.bright_cyan().to_string();
268 }
269
270 if field.starts_with("mnt-") {
272 return if value.contains("-") {
273 value.bright_blue().to_string()
274 } else {
275 value.white().to_string()
276 };
277 }
278
279 if field_lower == "as-name" || field_lower == "org-name" || field_lower == "netname" {
281 return value.bright_white().bold().to_string();
282 }
283
284 if field_lower == "role" || field_lower == "person" ||
286 field_lower.contains("registrant name") {
287 return value.bright_green().bold().to_string();
288 }
289
290 if field.ends_with("-c") {
292 return value.green().to_string();
293 }
294
295 value.white().to_string()
297 }
298
299 fn colorize_status_value(value: &str) -> String {
301 match value.to_uppercase().as_str() {
302 "ASSIGNED" | "ALLOCATED" => value.bright_green().to_string(),
303 "AVAILABLE" => value.bright_cyan().to_string(),
304 "RESERVED" => value.yellow().to_string(),
305 "CLIENT DELETE PROHIBITED" | "CLIENT TRANSFER PROHIBITED" |
306 "CLIENT UPDATE PROHIBITED" => value.bright_yellow().to_string(),
307 "INACTIVE" | "PENDING DELETE" => value.bright_red().to_string(),
308 "OK" | "ACTIVE" | "CLIENT OK" => value.bright_green().to_string(),
309 _ => value.bright_yellow().to_string(),
310 }
311 }
312
313 fn colorize_routing_policy(value: &str) -> String {
315 let mut colored_parts = Vec::new();
316 let parts: Vec<&str> = value.split_whitespace().collect();
317
318 for part in parts {
319 if part.starts_with("AS") && part.len() > 2 && part[2..].chars().all(|c| c.is_digit(10)) {
320 colored_parts.push(part.bright_red().to_string());
321 } else if matches!(part, "from" | "to" | "accept" | "announce") {
322 colored_parts.push(part.bright_cyan().to_string());
323 } else {
324 colored_parts.push(part.white().to_string());
325 }
326 }
327
328 colored_parts.join(" ")
329 }
330
331 fn looks_like_ip_or_cidr(value: &str) -> bool {
333 value.chars().all(|c| c.is_digit(10) || c == '.' || c == ':' || c == '/')
334 }
335
336 fn colorize_special_lines(line: &str) -> String {
338 let line_lower = line.to_lowercase();
339
340 if line_lower.contains("error") || line_lower.contains("not found") ||
341 line_lower.contains("no match") {
342 line.bright_red().to_string()
343 } else if line_lower.contains("available") {
344 line.bright_green().to_string()
345 } else {
346 line.white().to_string()
347 }
348 }
349
350 fn colorize_bgptools(output: &str) -> String {
352 let lines: Vec<&str> = output.lines().collect();
353 let mut colored_lines = Vec::new();
354 let mut headers: Vec<&str> = Vec::new();
355
356 for (i, line) in lines.iter().enumerate() {
357 if line.trim().is_empty() {
358 colored_lines.push("".to_string());
359 continue;
360 }
361
362 if i == 0 || (i == 1 && lines[0].trim().is_empty()) {
364 headers = line.split('|').map(|s| s.trim()).collect();
365 let colored_headers: Vec<String> = headers.iter()
366 .map(|&header| header.bright_cyan().bold().to_string())
367 .collect();
368 colored_lines.push(colored_headers.join(" | "));
369 continue;
370 }
371
372 let fields: Vec<&str> = line.split('|').map(|s| s.trim()).collect();
374 let mut colored_fields = Vec::new();
375
376 for (j, field) in fields.iter().enumerate() {
377 let header = if j < headers.len() { headers[j] } else { "" };
378
379 let colored_field = match header {
380 "AS" => field.bright_red().to_string(),
381 "IP" | "BGP Prefix" => field.bright_cyan().to_string(),
382 "CC" => field.bright_yellow().to_string(),
383 "Registry" => field.bright_blue().to_string(),
384 "Allocated" => field.bright_magenta().to_string(),
385 "AS Name" => field.bright_white().bold().to_string(),
386 _ => field.white().to_string(),
387 };
388
389 colored_fields.push(colored_field);
390 }
391
392 colored_lines.push(colored_fields.join(" | "));
393 }
394
395 colored_lines.join("\n")
396 }
397
398 fn colorize_mtf(output: &str) -> String {
400 let mut colored_lines = Vec::new();
401 let mut line_count = 0;
402
403 for line in output.lines() {
404 if line.trim().is_empty() {
405 colored_lines.push(line.to_string());
406 continue;
407 }
408
409 let colored_line = match line_count % 5 {
411 0 => line.truecolor(91, 207, 250).to_string(), 1 => line.truecolor(245, 171, 185).to_string(), 2 => line.truecolor(255, 255, 255).to_string(), 3 => line.truecolor(245, 171, 185).to_string(), 4 => line.truecolor(91, 207, 250).to_string(), _ => unreachable!(),
417 };
418
419 colored_lines.push(colored_line);
420 line_count += 1;
421 }
422
423 colored_lines.join("\n")
424 }
425}