env_filter/
parser.rs

1use log::LevelFilter;
2use std::error::Error;
3use std::fmt::{Display, Formatter};
4
5use crate::Directive;
6use crate::FilterOp;
7
8#[derive(Default, Debug)]
9pub(crate) struct ParseResult {
10    pub(crate) directives: Vec<Directive>,
11    pub(crate) filter: Option<FilterOp>,
12    pub(crate) errors: Vec<String>,
13}
14
15impl ParseResult {
16    fn add_directive(&mut self, directive: Directive) {
17        self.directives.push(directive);
18    }
19
20    fn set_filter(&mut self, filter: FilterOp) {
21        self.filter = Some(filter);
22    }
23
24    fn add_error(&mut self, message: String) {
25        self.errors.push(message);
26    }
27
28    pub(crate) fn ok(self) -> Result<(Vec<Directive>, Option<FilterOp>), ParseError> {
29        let Self {
30            directives,
31            filter,
32            errors,
33        } = self;
34        if let Some(error) = errors.into_iter().next() {
35            Err(ParseError { details: error })
36        } else {
37            Ok((directives, filter))
38        }
39    }
40}
41
42/// Error during logger directive parsing process.
43#[derive(Debug, Clone, PartialEq, Eq, Hash)]
44pub struct ParseError {
45    details: String,
46}
47
48impl Display for ParseError {
49    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
50        write!(f, "error parsing logger filter: {}", self.details)
51    }
52}
53
54impl Error for ParseError {}
55
56/// Parse a logging specification string (e.g: `crate1,crate2::mod3,crate3::x=error/foo`)
57/// and return a vector with log directives.
58pub(crate) fn parse_spec(spec: &str) -> ParseResult {
59    let mut result = ParseResult::default();
60
61    let mut parts = spec.split('/');
62    let mods = parts.next();
63    let filter = parts.next();
64    if parts.next().is_some() {
65        result.add_error(format!("invalid logging spec '{spec}' (too many '/'s)"));
66        return result;
67    }
68    if let Some(m) = mods {
69        for s in m.split(',').map(|ss| ss.trim()) {
70            if s.is_empty() {
71                continue;
72            }
73            let mut parts = s.split('=');
74            let (log_level, name) =
75                match (parts.next(), parts.next().map(|s| s.trim()), parts.next()) {
76                    (Some(part0), None, None) => {
77                        // if the single argument is a log-level string or number,
78                        // treat that as a global fallback
79                        match part0.parse() {
80                            Ok(num) => (num, None),
81                            Err(_) => (LevelFilter::max(), Some(part0)),
82                        }
83                    }
84                    (Some(part0), Some(""), None) => (LevelFilter::max(), Some(part0)),
85                    (Some(part0), Some(part1), None) => {
86                        if let Ok(num) = part1.parse() {
87                            (num, Some(part0))
88                        } else {
89                            result.add_error(format!("invalid logging spec '{part1}'"));
90                            continue;
91                        }
92                    }
93                    _ => {
94                        result.add_error(format!("invalid logging spec '{s}'"));
95                        continue;
96                    }
97                };
98
99            result.add_directive(Directive {
100                name: name.map(|s| s.to_owned()),
101                level: log_level,
102            });
103        }
104    }
105
106    if let Some(filter) = filter {
107        match FilterOp::new(filter) {
108            Ok(filter_op) => result.set_filter(filter_op),
109            Err(err) => result.add_error(format!("invalid regex filter - {err}")),
110        }
111    }
112
113    result
114}
115
116#[cfg(test)]
117mod tests {
118    use crate::ParseError;
119    use log::LevelFilter;
120    use snapbox::{assert_data_eq, str, Data, IntoData};
121
122    use super::{parse_spec, ParseResult};
123
124    impl IntoData for ParseError {
125        fn into_data(self) -> Data {
126            self.to_string().into_data()
127        }
128    }
129
130    #[test]
131    fn parse_spec_valid() {
132        let ParseResult {
133            directives: dirs,
134            filter,
135            errors,
136        } = parse_spec("crate1::mod1=error,crate1::mod2,crate2=debug");
137
138        assert_eq!(dirs.len(), 3);
139        assert_eq!(dirs[0].name, Some("crate1::mod1".to_owned()));
140        assert_eq!(dirs[0].level, LevelFilter::Error);
141
142        assert_eq!(dirs[1].name, Some("crate1::mod2".to_owned()));
143        assert_eq!(dirs[1].level, LevelFilter::max());
144
145        assert_eq!(dirs[2].name, Some("crate2".to_owned()));
146        assert_eq!(dirs[2].level, LevelFilter::Debug);
147        assert!(filter.is_none());
148
149        assert!(errors.is_empty());
150    }
151
152    #[test]
153    fn parse_spec_invalid_crate() {
154        // test parse_spec with multiple = in specification
155        let ParseResult {
156            directives: dirs,
157            filter,
158            errors,
159        } = parse_spec("crate1::mod1=warn=info,crate2=debug");
160
161        assert_eq!(dirs.len(), 1);
162        assert_eq!(dirs[0].name, Some("crate2".to_owned()));
163        assert_eq!(dirs[0].level, LevelFilter::Debug);
164        assert!(filter.is_none());
165
166        assert_eq!(errors.len(), 1);
167        assert_data_eq!(
168            &errors[0],
169            str!["invalid logging spec 'crate1::mod1=warn=info'"]
170        );
171    }
172
173    #[test]
174    fn parse_spec_invalid_level() {
175        // test parse_spec with 'noNumber' as log level
176        let ParseResult {
177            directives: dirs,
178            filter,
179            errors,
180        } = parse_spec("crate1::mod1=noNumber,crate2=debug");
181
182        assert_eq!(dirs.len(), 1);
183        assert_eq!(dirs[0].name, Some("crate2".to_owned()));
184        assert_eq!(dirs[0].level, LevelFilter::Debug);
185        assert!(filter.is_none());
186
187        assert_eq!(errors.len(), 1);
188        assert_data_eq!(&errors[0], str!["invalid logging spec 'noNumber'"]);
189    }
190
191    #[test]
192    fn parse_spec_string_level() {
193        // test parse_spec with 'warn' as log level
194        let ParseResult {
195            directives: dirs,
196            filter,
197            errors,
198        } = parse_spec("crate1::mod1=wrong,crate2=warn");
199
200        assert_eq!(dirs.len(), 1);
201        assert_eq!(dirs[0].name, Some("crate2".to_owned()));
202        assert_eq!(dirs[0].level, LevelFilter::Warn);
203        assert!(filter.is_none());
204
205        assert_eq!(errors.len(), 1);
206        assert_data_eq!(&errors[0], str!["invalid logging spec 'wrong'"]);
207    }
208
209    #[test]
210    fn parse_spec_empty_level() {
211        // test parse_spec with '' as log level
212        let ParseResult {
213            directives: dirs,
214            filter,
215            errors,
216        } = parse_spec("crate1::mod1=wrong,crate2=");
217
218        assert_eq!(dirs.len(), 1);
219        assert_eq!(dirs[0].name, Some("crate2".to_owned()));
220        assert_eq!(dirs[0].level, LevelFilter::max());
221        assert!(filter.is_none());
222
223        assert_eq!(errors.len(), 1);
224        assert_data_eq!(&errors[0], str!["invalid logging spec 'wrong'"]);
225    }
226
227    #[test]
228    fn parse_spec_empty_level_isolated() {
229        // test parse_spec with "" as log level (and the entire spec str)
230        let ParseResult {
231            directives: dirs,
232            filter,
233            errors,
234        } = parse_spec(""); // should be ignored
235        assert_eq!(dirs.len(), 0);
236        assert!(filter.is_none());
237        assert!(errors.is_empty());
238    }
239
240    #[test]
241    fn parse_spec_blank_level_isolated() {
242        // test parse_spec with a white-space-only string specified as the log
243        // level (and the entire spec str)
244        let ParseResult {
245            directives: dirs,
246            filter,
247            errors,
248        } = parse_spec("     "); // should be ignored
249        assert_eq!(dirs.len(), 0);
250        assert!(filter.is_none());
251        assert!(errors.is_empty());
252    }
253
254    #[test]
255    fn parse_spec_blank_level_isolated_comma_only() {
256        // The spec should contain zero or more comma-separated string slices,
257        // so a comma-only string should be interpreted as two empty strings
258        // (which should both be treated as invalid, so ignored).
259        let ParseResult {
260            directives: dirs,
261            filter,
262            errors,
263        } = parse_spec(","); // should be ignored
264        assert_eq!(dirs.len(), 0);
265        assert!(filter.is_none());
266        assert!(errors.is_empty());
267    }
268
269    #[test]
270    fn parse_spec_blank_level_isolated_comma_blank() {
271        // The spec should contain zero or more comma-separated string slices,
272        // so this bogus spec should be interpreted as containing one empty
273        // string and one blank string. Both should both be treated as
274        // invalid, so ignored.
275        let ParseResult {
276            directives: dirs,
277            filter,
278            errors,
279        } = parse_spec(",     "); // should be ignored
280        assert_eq!(dirs.len(), 0);
281        assert!(filter.is_none());
282        assert!(errors.is_empty());
283    }
284
285    #[test]
286    fn parse_spec_blank_level_isolated_blank_comma() {
287        // The spec should contain zero or more comma-separated string slices,
288        // so this bogus spec should be interpreted as containing one blank
289        // string and one empty string. Both should both be treated as
290        // invalid, so ignored.
291        let ParseResult {
292            directives: dirs,
293            filter,
294            errors,
295        } = parse_spec("     ,"); // should be ignored
296        assert_eq!(dirs.len(), 0);
297        assert!(filter.is_none());
298        assert!(errors.is_empty());
299    }
300
301    #[test]
302    fn parse_spec_global() {
303        // test parse_spec with no crate
304        let ParseResult {
305            directives: dirs,
306            filter,
307            errors,
308        } = parse_spec("warn,crate2=debug");
309        assert_eq!(dirs.len(), 2);
310        assert_eq!(dirs[0].name, None);
311        assert_eq!(dirs[0].level, LevelFilter::Warn);
312        assert_eq!(dirs[1].name, Some("crate2".to_owned()));
313        assert_eq!(dirs[1].level, LevelFilter::Debug);
314        assert!(filter.is_none());
315        assert!(errors.is_empty());
316    }
317
318    #[test]
319    fn parse_spec_global_bare_warn_lc() {
320        // test parse_spec with no crate, in isolation, all lowercase
321        let ParseResult {
322            directives: dirs,
323            filter,
324            errors,
325        } = parse_spec("warn");
326        assert_eq!(dirs.len(), 1);
327        assert_eq!(dirs[0].name, None);
328        assert_eq!(dirs[0].level, LevelFilter::Warn);
329        assert!(filter.is_none());
330        assert!(errors.is_empty());
331    }
332
333    #[test]
334    fn parse_spec_global_bare_warn_uc() {
335        // test parse_spec with no crate, in isolation, all uppercase
336        let ParseResult {
337            directives: dirs,
338            filter,
339            errors,
340        } = parse_spec("WARN");
341        assert_eq!(dirs.len(), 1);
342        assert_eq!(dirs[0].name, None);
343        assert_eq!(dirs[0].level, LevelFilter::Warn);
344        assert!(filter.is_none());
345        assert!(errors.is_empty());
346    }
347
348    #[test]
349    fn parse_spec_global_bare_warn_mixed() {
350        // test parse_spec with no crate, in isolation, mixed case
351        let ParseResult {
352            directives: dirs,
353            filter,
354            errors,
355        } = parse_spec("wArN");
356        assert_eq!(dirs.len(), 1);
357        assert_eq!(dirs[0].name, None);
358        assert_eq!(dirs[0].level, LevelFilter::Warn);
359        assert!(filter.is_none());
360        assert!(errors.is_empty());
361    }
362
363    #[test]
364    fn parse_spec_valid_filter() {
365        let ParseResult {
366            directives: dirs,
367            filter,
368            errors,
369        } = parse_spec("crate1::mod1=error,crate1::mod2,crate2=debug/abc");
370        assert_eq!(dirs.len(), 3);
371        assert_eq!(dirs[0].name, Some("crate1::mod1".to_owned()));
372        assert_eq!(dirs[0].level, LevelFilter::Error);
373
374        assert_eq!(dirs[1].name, Some("crate1::mod2".to_owned()));
375        assert_eq!(dirs[1].level, LevelFilter::max());
376
377        assert_eq!(dirs[2].name, Some("crate2".to_owned()));
378        assert_eq!(dirs[2].level, LevelFilter::Debug);
379        assert!(filter.is_some() && filter.unwrap().to_string() == "abc");
380        assert!(errors.is_empty());
381    }
382
383    #[test]
384    fn parse_spec_invalid_crate_filter() {
385        let ParseResult {
386            directives: dirs,
387            filter,
388            errors,
389        } = parse_spec("crate1::mod1=error=warn,crate2=debug/a.c");
390
391        assert_eq!(dirs.len(), 1);
392        assert_eq!(dirs[0].name, Some("crate2".to_owned()));
393        assert_eq!(dirs[0].level, LevelFilter::Debug);
394        assert!(filter.is_some() && filter.unwrap().to_string() == "a.c");
395
396        assert_eq!(errors.len(), 1);
397        assert_data_eq!(
398            &errors[0],
399            str!["invalid logging spec 'crate1::mod1=error=warn'"]
400        );
401    }
402
403    #[test]
404    fn parse_spec_empty_with_filter() {
405        let ParseResult {
406            directives: dirs,
407            filter,
408            errors,
409        } = parse_spec("crate1/a*c");
410        assert_eq!(dirs.len(), 1);
411        assert_eq!(dirs[0].name, Some("crate1".to_owned()));
412        assert_eq!(dirs[0].level, LevelFilter::max());
413        assert!(filter.is_some() && filter.unwrap().to_string() == "a*c");
414        assert!(errors.is_empty());
415    }
416
417    #[test]
418    fn parse_spec_with_multiple_filters() {
419        let ParseResult {
420            directives: dirs,
421            filter,
422            errors,
423        } = parse_spec("debug/abc/a.c");
424        assert!(dirs.is_empty());
425        assert!(filter.is_none());
426
427        assert_eq!(errors.len(), 1);
428        assert_data_eq!(
429            &errors[0],
430            str!["invalid logging spec 'debug/abc/a.c' (too many '/'s)"]
431        );
432    }
433
434    #[test]
435    fn parse_spec_multiple_invalid_crates() {
436        // test parse_spec with multiple = in specification
437        let ParseResult {
438            directives: dirs,
439            filter,
440            errors,
441        } = parse_spec("crate1::mod1=warn=info,crate2=debug,crate3=error=error");
442
443        assert_eq!(dirs.len(), 1);
444        assert_eq!(dirs[0].name, Some("crate2".to_owned()));
445        assert_eq!(dirs[0].level, LevelFilter::Debug);
446        assert!(filter.is_none());
447
448        assert_eq!(errors.len(), 2);
449        assert_data_eq!(
450            &errors[0],
451            str!["invalid logging spec 'crate1::mod1=warn=info'"]
452        );
453        assert_data_eq!(
454            &errors[1],
455            str!["invalid logging spec 'crate3=error=error'"]
456        );
457    }
458
459    #[test]
460    fn parse_spec_multiple_invalid_levels() {
461        // test parse_spec with 'noNumber' as log level
462        let ParseResult {
463            directives: dirs,
464            filter,
465            errors,
466        } = parse_spec("crate1::mod1=noNumber,crate2=debug,crate3=invalid");
467
468        assert_eq!(dirs.len(), 1);
469        assert_eq!(dirs[0].name, Some("crate2".to_owned()));
470        assert_eq!(dirs[0].level, LevelFilter::Debug);
471        assert!(filter.is_none());
472
473        assert_eq!(errors.len(), 2);
474        assert_data_eq!(&errors[0], str!["invalid logging spec 'noNumber'"]);
475        assert_data_eq!(&errors[1], str!["invalid logging spec 'invalid'"]);
476    }
477
478    #[test]
479    fn parse_spec_invalid_crate_and_level() {
480        // test parse_spec with 'noNumber' as log level
481        let ParseResult {
482            directives: dirs,
483            filter,
484            errors,
485        } = parse_spec("crate1::mod1=debug=info,crate2=debug,crate3=invalid");
486
487        assert_eq!(dirs.len(), 1);
488        assert_eq!(dirs[0].name, Some("crate2".to_owned()));
489        assert_eq!(dirs[0].level, LevelFilter::Debug);
490        assert!(filter.is_none());
491
492        assert_eq!(errors.len(), 2);
493        assert_data_eq!(
494            &errors[0],
495            str!["invalid logging spec 'crate1::mod1=debug=info'"]
496        );
497        assert_data_eq!(&errors[1], str!["invalid logging spec 'invalid'"]);
498    }
499
500    #[test]
501    fn parse_error_message_single_error() {
502        let error = parse_spec("crate1::mod1=debug=info,crate2=debug")
503            .ok()
504            .unwrap_err();
505        assert_data_eq!(
506            error,
507            str!["error parsing logger filter: invalid logging spec 'crate1::mod1=debug=info'"]
508        );
509    }
510
511    #[test]
512    fn parse_error_message_multiple_errors() {
513        let error = parse_spec("crate1::mod1=debug=info,crate2=debug,crate3=invalid")
514            .ok()
515            .unwrap_err();
516        assert_data_eq!(
517            error,
518            str!["error parsing logger filter: invalid logging spec 'crate1::mod1=debug=info'"]
519        );
520    }
521}