whois_cli/
colorize.rs

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    /// Detect the appropriate color scheme for the output
15    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    /// Apply colorization based on the scheme
24    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    /// Detect if the output is in BGP Tools format
34    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    /// Colorize RIPE format output (field: value pairs)
48    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            // Handle comment lines
54            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            // Comment block state management
61            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            // Handle empty lines
71            if line.trim().is_empty() {
72                colored_lines.push(line.to_string());
73                continue;
74            }
75            
76            // Handle field: value pairs
77            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            // Handle special cases
85            colored_lines.push(Self::colorize_special_lines(line));
86        }
87        
88        colored_lines.join("\n")
89    }
90
91    /// Colorize a field: value pair
92    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    /// Colorize field names based on their type
108    fn colorize_field_name(field: &str) -> String {
109        match field.to_lowercase().as_str() {
110            // Network and AS fields
111            "aut-num" | "as-block" | "inet6num" | "inetnum" | "route" | "route6" | "netname" =>
112                field.bright_cyan().to_string(),
113            
114            // Domain fields
115            "domain" | "domain name" =>
116                field.bright_cyan().bold().to_string(),
117            
118            // DNS fields
119            "nserver" | "name server" | "nameserver" | "name servers" =>
120                field.yellow().bold().to_string(),
121            
122            // Status fields
123            "domain status" | "status" =>
124                field.bright_yellow().to_string(),
125            
126            // Registrar fields
127            "registrar" | "sponsoring registrar" | "registrar iana id" | "reseller" =>
128                field.bright_blue().to_string(),
129            
130            // Registry fields
131            "registry domain id" | "registrar whois server" | "registrar url" =>
132                field.blue().to_string(),
133            
134            // Date fields
135            "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 fields
142            "privacy" | "whois privacy" | "domain privacy" =>
143                field.bright_red().to_string(),
144            
145            // Name fields
146            "as-name" | "org-name" | "role" | "person" | "registrant name" | 
147            "admin name" | "tech name" =>
148                field.bright_green().to_string(),
149            
150            // Organization fields
151            "org" | "organisation" | "org-type" | "registrant organization" | "registrant" =>
152                field.yellow().to_string(),
153            
154            // Contact fields
155            "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            // Maintainer fields
160            "mnt-by" | "mnt-ref" | "mnt-domains" | "mnt-lower" | "mnt-routes" =>
161                field.bright_blue().to_string(),
162            
163            // Routing fields
164            "import" | "export" | "mp-import" | "mp-export" | "default" | "peer" =>
165                field.magenta().to_string(),
166            
167            // Location fields
168            "country" | "address" | "source" | "registrant country" | 
169            "admin country" | "tech country" =>
170                field.bright_white().to_string(),
171            
172            // Communication fields
173            "e-mail" | "email" | "phone" | "registrant email" | "admin email" | "tech email" =>
174                field.blue().to_string(),
175            
176            // DNSSEC fields
177            "dnssec" | "ds record" =>
178                field.magenta().bold().to_string(),
179            
180            // Special org field
181            "sponsoring-org" =>
182                field.bright_yellow().to_string(),
183            
184            // Default
185            _ => field.white().to_string(),
186        }
187    }
188
189    /// Colorize field values based on content and context
190    fn colorize_field_value(field: &str, value: &str) -> String {
191        let field_lower = field.to_lowercase();
192        
193        // Domain names
194        if field_lower == "domain" || field_lower == "domain name" {
195            return value.bright_white().bold().to_string();
196        }
197        
198        // AS Numbers
199        if field_lower == "aut-num" {
200            return value.bright_red().bold().to_string();
201        }
202        
203        // Status values
204        if field_lower == "status" || field_lower == "domain status" {
205            return Self::colorize_status_value(value);
206        }
207        
208        // Source registry
209        if field_lower == "source" {
210            return value.bright_blue().to_string();
211        }
212        
213        // Country codes
214        if field_lower == "country" || field_lower.contains("country") {
215            return value.yellow().to_string();
216        }
217        
218        // Name servers
219        if field_lower.contains("name server") || field_lower.contains("nserver") || 
220           field_lower == "nameserver" {
221            return value.bright_green().to_string();
222        }
223        
224        // Registrar information
225        if field_lower.contains("registrar") {
226            return value.bright_blue().bold().to_string();
227        }
228        
229        // DNSSEC status
230        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        // Dates
239        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        // Email addresses
246        if value.contains('@') {
247            return value.bright_yellow().to_string();
248        }
249        
250        // Phone numbers
251        if field_lower.contains("phone") {
252            return value.bright_white().to_string();
253        }
254        
255        // AS numbers in values
256        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        // Import/Export specialized coloring
261        if (field == "import" || field == "export") && value.contains("AS") {
262            return Self::colorize_routing_policy(value);
263        }
264        
265        // IP addresses and CIDR blocks
266        if Self::looks_like_ip_or_cidr(value) {
267            return value.bright_cyan().to_string();
268        }
269        
270        // Maintainer values
271        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        // Names
280        if field_lower == "as-name" || field_lower == "org-name" || field_lower == "netname" {
281            return value.bright_white().bold().to_string();
282        }
283        
284        // Person/role names
285        if field_lower == "role" || field_lower == "person" || 
286           field_lower.contains("registrant name") {
287            return value.bright_green().bold().to_string();
288        }
289        
290        // Handles
291        if field.ends_with("-c") {
292            return value.green().to_string();
293        }
294        
295        // Default
296        value.white().to_string()
297    }
298
299    /// Colorize status values
300    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    /// Colorize routing policy lines (import/export)
314    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    /// Check if a string looks like an IP address or CIDR block
332    fn looks_like_ip_or_cidr(value: &str) -> bool {
333        value.chars().all(|c| c.is_digit(10) || c == '.' || c == ':' || c == '/')
334    }
335
336    /// Colorize special lines (errors, availability, etc.)
337    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    /// Colorize BGP Tools format output (table format)
351    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            // Process header row
363            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            // Process data rows
373            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    /// MTF flag coloring (trans flag pattern)
399    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            // Trans flag pattern: blue, pink, white, pink, blue
410            let colored_line = match line_count % 5 {
411                0 => line.truecolor(91, 207, 250).to_string(),   // Blue #5BCFFA
412                1 => line.truecolor(245, 171, 185).to_string(),  // Pink #F5ABB9
413                2 => line.truecolor(255, 255, 255).to_string(),  // White #FFFFFF
414                3 => line.truecolor(245, 171, 185).to_string(),  // Pink #F5ABB9
415                4 => line.truecolor(91, 207, 250).to_string(),   // Blue #5BCFFA
416                _ => unreachable!(),
417            };
418            
419            colored_lines.push(colored_line);
420            line_count += 1;
421        }
422        
423        colored_lines.join("\n")
424    }
425}