1use crate::cli::Args;
2use anyhow::{Context, Result};
3use regex::Regex;
4use std::collections::HashMap;
5use std::path::PathBuf;
6use termcolor::Color;
7
8const REGEX_SIZE_LIMIT: usize = 10 * 1024 * 1024; #[derive(Debug, Clone)]
12pub struct Config {
13 pub files: Vec<PathBuf>,
14 pub patterns: Vec<String>,
15 pub regex_patterns: Vec<Regex>,
16 pub exclude_patterns: Vec<String>,
17 pub exclude_patterns_lowercase: Vec<String>, pub exclude_regex_patterns: Vec<Regex>,
19 pub case_insensitive: bool,
20 pub color_mappings: HashMap<String, Color>,
21 pub notify_enabled: bool,
22 pub notify_patterns: Vec<String>,
23 pub notify_throttle: u32,
24 pub dry_run: bool,
25 pub quiet: bool,
26 pub no_color: bool,
27 pub prefix_files: bool,
28 pub poll_interval: u64,
29 pub buffer_size: usize,
30}
31
32impl Config {
33 pub fn from_args(args: &Args) -> Result<Self> {
34 let patterns = args.patterns();
35 let notify_patterns = args.notify_patterns();
36 let exclude_patterns = args.exclude_patterns();
37
38 let regex_patterns = if args.regex {
40 Self::compile_regex_patterns(&patterns, args.case_insensitive)?
41 } else {
42 vec![]
43 };
44
45 let exclude_regex_patterns = if args.regex && !exclude_patterns.is_empty() {
47 Self::compile_regex_patterns(&exclude_patterns, args.case_insensitive)?
48 } else {
49 vec![]
50 };
51
52 let exclude_patterns_lowercase = if args.case_insensitive {
54 exclude_patterns.iter().map(|p| p.to_lowercase()).collect()
55 } else {
56 vec![]
57 };
58
59 let color_mappings = Self::parse_color_mappings(&args.color_mappings())?;
61
62 Ok(Config {
63 files: args.files().to_vec(),
64 patterns,
65 regex_patterns,
66 exclude_patterns,
67 exclude_patterns_lowercase,
68 exclude_regex_patterns,
69 case_insensitive: args.case_insensitive,
70 color_mappings,
71 notify_enabled: args.notify,
72 notify_patterns,
73 notify_throttle: args.notify_throttle,
74 dry_run: args.dry_run,
75 quiet: args.quiet,
76 no_color: args.no_color,
77 prefix_files: args.should_prefix_files(),
78 poll_interval: args.poll_interval,
79 buffer_size: args.buffer_size,
80 })
81 }
82
83 fn compile_regex_patterns(patterns: &[String], case_insensitive: bool) -> Result<Vec<Regex>> {
84 let mut compiled = Vec::new();
85
86 for pattern in patterns {
87 let mut regex_builder = regex::RegexBuilder::new(pattern);
88 regex_builder.case_insensitive(case_insensitive);
89 regex_builder.size_limit(REGEX_SIZE_LIMIT);
91 regex_builder.dfa_size_limit(REGEX_SIZE_LIMIT);
93
94 let regex = regex_builder
95 .build()
96 .with_context(|| format!("Invalid or too complex regex pattern: {}", pattern))?;
97
98 compiled.push(regex);
99 }
100
101 Ok(compiled)
102 }
103
104 fn parse_color_mappings(mappings: &[(String, String)]) -> Result<HashMap<String, Color>> {
105 let mut color_map = HashMap::new();
106
107 for (pattern, color_name) in mappings {
108 let color = Self::parse_color(color_name)?;
109 color_map.insert(pattern.clone(), color);
110 }
111
112 Self::add_default_color_mappings(&mut color_map);
114
115 Ok(color_map)
116 }
117
118 fn parse_color(color_name: &str) -> Result<Color> {
119 match color_name.to_lowercase().as_str() {
120 "black" => Ok(Color::Black),
121 "red" => Ok(Color::Red),
122 "green" => Ok(Color::Green),
123 "yellow" => Ok(Color::Yellow),
124 "blue" => Ok(Color::Blue),
125 "magenta" => Ok(Color::Magenta),
126 "cyan" => Ok(Color::Cyan),
127 "white" => Ok(Color::White),
128 _ => Err(anyhow::anyhow!("Unknown color: {}", color_name)),
129 }
130 }
131
132 fn add_default_color_mappings(color_map: &mut HashMap<String, Color>) {
133 let defaults = [
134 ("ERROR", Color::Red),
135 ("WARN", Color::Yellow),
136 ("WARNING", Color::Yellow),
137 ("INFO", Color::Green),
138 ("DEBUG", Color::Cyan),
139 ("TRACE", Color::Magenta),
140 ("FATAL", Color::Red),
141 ("CRITICAL", Color::Red),
142 ];
143
144 for (pattern, color) in defaults {
145 color_map.entry(pattern.to_string()).or_insert(color);
146 }
147 }
148
149 pub fn should_notify_for_pattern(&self, pattern: &str) -> bool {
151 self.notify_enabled && self.notify_patterns.contains(&pattern.to_string())
152 }
153
154 pub fn get_color_for_pattern(&self, pattern: &str) -> Option<Color> {
156 self.color_mappings.get(pattern).copied()
157 }
158
159 pub fn should_exclude(&self, line: &str) -> bool {
161 if self.exclude_patterns.is_empty() {
162 return false;
163 }
164
165 if !self.exclude_regex_patterns.is_empty() {
167 for regex in &self.exclude_regex_patterns {
168 if regex.is_match(line) {
169 return true;
170 }
171 }
172 } else if self.case_insensitive {
173 let search_line = line.to_lowercase();
175 for pattern in &self.exclude_patterns_lowercase {
176 if search_line.contains(pattern) {
177 return true;
178 }
179 }
180 } else {
181 for pattern in &self.exclude_patterns {
183 if line.contains(pattern.as_str()) {
184 return true;
185 }
186 }
187 }
188
189 false
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn test_all_color_parsing() {
199 assert_eq!(Config::parse_color("yellow").unwrap(), Color::Yellow);
201 assert_eq!(Config::parse_color("magenta").unwrap(), Color::Magenta);
202 assert_eq!(Config::parse_color("cyan").unwrap(), Color::Cyan);
203 assert_eq!(Config::parse_color("white").unwrap(), Color::White);
204 }
205
206 #[test]
207 fn test_parse_color_unknown_coverage_line_100() {
208 let result = Config::parse_color("unknown_color");
210 assert!(result.is_err());
211 assert!(result
212 .unwrap_err()
213 .to_string()
214 .contains("Unknown color: unknown_color"));
215 }
216
217 #[test]
218 fn test_get_color_for_pattern() {
219 let args = Args {
220 files: vec![PathBuf::from("test.log")],
221 completions: None,
222 patterns: "ERROR".to_string(),
223 regex: false,
224 case_insensitive: false,
225 color_map: None,
226 notify: false,
227 notify_patterns: None,
228 quiet: false,
229 dry_run: false,
230 exclude: None,
231 prefix_file: Some(false),
232 poll_interval: 1000,
233 buffer_size: 8192,
234 no_color: false,
235 notify_throttle: 0,
236 };
237
238 let config = Config::from_args(&args).unwrap();
239
240 assert_eq!(config.get_color_for_pattern("ERROR"), Some(Color::Red));
242 assert_eq!(config.get_color_for_pattern("WARN"), Some(Color::Yellow));
243 assert_eq!(config.get_color_for_pattern("INFO"), Some(Color::Green));
244 assert_eq!(config.get_color_for_pattern("DEBUG"), Some(Color::Cyan));
245 assert_eq!(config.get_color_for_pattern("UNKNOWN"), None);
246 }
247
248 #[test]
249 fn test_should_exclude_literal() {
250 let args = Args {
251 files: vec![PathBuf::from("test.log")],
252 completions: None,
253 patterns: "ERROR".to_string(),
254 regex: false,
255 case_insensitive: false,
256 color_map: None,
257 notify: false,
258 notify_patterns: None,
259 quiet: false,
260 dry_run: false,
261 exclude: Some("DEBUG,TRACE".to_string()),
262 prefix_file: Some(false),
263 poll_interval: 1000,
264 buffer_size: 8192,
265 no_color: false,
266 notify_throttle: 0,
267 };
268
269 let config = Config::from_args(&args).unwrap();
270
271 assert!(config.should_exclude("DEBUG: Some debug message"));
272 assert!(config.should_exclude("TRACE: Some trace message"));
273 assert!(!config.should_exclude("ERROR: Some error message"));
274 assert!(!config.should_exclude("INFO: Some info message"));
275 }
276
277 #[test]
278 fn test_should_exclude_case_insensitive() {
279 let args = Args {
280 files: vec![PathBuf::from("test.log")],
281 completions: None,
282 patterns: "ERROR".to_string(),
283 regex: false,
284 case_insensitive: true,
285 color_map: None,
286 notify: false,
287 notify_patterns: None,
288 quiet: false,
289 dry_run: false,
290 exclude: Some("debug".to_string()),
291 prefix_file: Some(false),
292 poll_interval: 1000,
293 buffer_size: 8192,
294 no_color: false,
295 notify_throttle: 0,
296 };
297
298 let config = Config::from_args(&args).unwrap();
299
300 assert!(config.should_exclude("DEBUG: Some debug message"));
301 assert!(config.should_exclude("debug: Some debug message"));
302 assert!(!config.should_exclude("ERROR: Some error message"));
303 }
304
305 #[test]
306 fn test_should_exclude_regex() {
307 let args = Args {
308 files: vec![PathBuf::from("test.log")],
309 completions: None,
310 patterns: "ERROR".to_string(),
311 regex: true,
312 case_insensitive: false,
313 color_map: None,
314 notify: false,
315 notify_patterns: None,
316 quiet: false,
317 dry_run: false,
318 exclude: Some(r"DEBUG|TRACE".to_string()),
319 prefix_file: Some(false),
320 poll_interval: 1000,
321 buffer_size: 8192,
322 no_color: false,
323 notify_throttle: 0,
324 };
325
326 let config = Config::from_args(&args).unwrap();
327
328 assert!(config.should_exclude("DEBUG: Some debug message"));
329 assert!(config.should_exclude("TRACE: Some trace message"));
330 assert!(!config.should_exclude("ERROR: Some error message"));
331 }
332
333 #[test]
334 fn test_should_exclude_empty() {
335 let args = Args {
336 files: vec![PathBuf::from("test.log")],
337 completions: None,
338 patterns: "ERROR".to_string(),
339 regex: false,
340 case_insensitive: false,
341 color_map: None,
342 notify: false,
343 notify_patterns: None,
344 quiet: false,
345 dry_run: false,
346 exclude: None,
347 prefix_file: Some(false),
348 poll_interval: 1000,
349 buffer_size: 8192,
350 no_color: false,
351 notify_throttle: 0,
352 };
353
354 let config = Config::from_args(&args).unwrap();
355
356 assert!(!config.should_exclude("DEBUG: Some debug message"));
357 assert!(!config.should_exclude("ERROR: Some error message"));
358 }
359}