Skip to main content

midenc_log/filter/
parser.rs

1use std::{
2    error::Error,
3    fmt::{Display, Formatter},
4};
5
6use log::LevelFilter;
7
8use super::directive::DirectiveKind;
9use crate::filter::{Directive, FilterOp};
10
11#[derive(Default, Debug)]
12pub(crate) struct ParseResult {
13    pub(crate) directives: Vec<Directive>,
14    pub(crate) filter: Option<FilterOp>,
15    pub(crate) errors: Vec<String>,
16}
17
18impl ParseResult {
19    fn add_directive(&mut self, directive: Directive) {
20        self.directives.push(directive);
21    }
22
23    fn set_filter(&mut self, filter: FilterOp) {
24        self.filter = Some(filter);
25    }
26
27    fn add_error(&mut self, message: String) {
28        self.errors.push(message);
29    }
30
31    pub(crate) fn ok(self) -> Result<(Vec<Directive>, Option<FilterOp>), ParseError> {
32        let Self {
33            directives,
34            filter,
35            errors,
36        } = self;
37        if let Some(error) = errors.into_iter().next() {
38            Err(ParseError { details: error })
39        } else {
40            Ok((directives, filter))
41        }
42    }
43}
44
45/// Error during logger directive parsing process.
46#[derive(Debug, Clone, PartialEq, Eq, Hash)]
47pub struct ParseError {
48    details: String,
49}
50
51impl Display for ParseError {
52    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
53        write!(f, "error parsing logger filter: {}", self.details)
54    }
55}
56
57impl Error for ParseError {}
58
59/// Parse a logging specification string (e.g: `crate1,crate2::mod3,crate3::x=error/foo`)
60/// and return a vector with log directives.
61pub(crate) fn parse_spec(s: &str) -> ParseResult {
62    let mut result = ParseResult::default();
63
64    let (spec, filter) = s.rsplit_once('/').unwrap_or((s, ""));
65    if spec.contains('/') {
66        result.add_error(format!("invalid logging spec '{s}': too many '/'"));
67        return result;
68    }
69    let filter = if filter.is_empty() {
70        None
71    } else {
72        Some(filter)
73    };
74    let directives = spec.split(',').map(|s| s.trim());
75    for directive in directives {
76        if directive.is_empty() {
77            continue;
78        }
79
80        let (matcher_spec, level, negated) = match directive.rsplit_once('=') {
81            Some((ms, "")) => {
82                let ms = ms.trim();
83                if ms.contains('=') {
84                    result.add_error(format!(
85                        "invalid logging spec '{directive}': '=' is not allowed in paths"
86                    ));
87                    continue;
88                }
89                if let Some(ms) = ms.strip_prefix('-') {
90                    (Some(ms), LevelFilter::max(), true)
91                } else {
92                    (Some(ms), LevelFilter::max(), false)
93                }
94            }
95            Some((ms, level)) => {
96                let ms = ms.trim();
97                if ms.contains('=') {
98                    result.add_error(format!(
99                        "invalid logging spec '{directive}': '=' is not allowed in paths"
100                    ));
101                    continue;
102                }
103                let level = level
104                    .trim()
105                    .parse::<LevelFilter>()
106                    .map_err(|err| format!("invalid logging spec '{directive}': {err}"));
107                match level {
108                    Ok(level) => {
109                        if let Some(ms) = ms.strip_prefix('-') {
110                            (Some(ms), level, true)
111                        } else {
112                            (Some(ms), level, false)
113                        }
114                    }
115                    Err(err) => {
116                        result.add_error(err);
117                        continue;
118                    }
119                }
120            }
121            None => {
122                let (level, negated) = if let Some(level) = directive.strip_prefix('-') {
123                    (level.trim(), true)
124                } else {
125                    (directive, false)
126                };
127                match level.parse::<LevelFilter>() {
128                    Ok(level) => (None, level, negated),
129                    Err(_) => (Some(directive), LevelFilter::max(), negated),
130                }
131            }
132        };
133
134        if let Some(matcher_spec) = matcher_spec {
135            match matcher_spec.split_once(':') {
136                Some((component, "*" | "")) => {
137                    result.add_directive(Directive {
138                        kind: DirectiveKind::Component {
139                            component: component.to_owned(),
140                        },
141                        level,
142                        negated,
143                    });
144                }
145                // If `topic` starts with a ':', we've attempted to parse a module path as a filter
146                // spec, e.g. `core::option`
147                Some((_, topic)) if topic.starts_with(':') => {
148                    result.add_directive(Directive {
149                        kind: DirectiveKind::Module {
150                            module: matcher_spec.to_owned(),
151                        },
152                        level,
153                        negated,
154                    });
155                    continue;
156                }
157                Some((component, topic)) => {
158                    let topic = match FilterOp::new(topic) {
159                        Ok(topic) => topic,
160                        Err(err) => {
161                            result.add_error(format!("invalid logging spec '{directive}': {err}"));
162                            continue;
163                        }
164                    };
165                    result.add_directive(Directive {
166                        kind: DirectiveKind::Topic {
167                            component: component.to_owned(),
168                            topic,
169                        },
170                        level,
171                        negated,
172                    });
173                }
174                None => {
175                    result.add_directive(Directive {
176                        kind: DirectiveKind::Component {
177                            component: matcher_spec.to_owned(),
178                        },
179                        level,
180                        negated,
181                    });
182                }
183            }
184        } else {
185            result.add_directive(Directive {
186                kind: DirectiveKind::Any,
187                level,
188                negated,
189            });
190        }
191    }
192
193    if let Some(filter) = filter {
194        match FilterOp::new(filter) {
195            Ok(filter_op) => result.set_filter(filter_op),
196            Err(err) => result.add_error(format!("invalid regex filter - {err}")),
197        }
198    }
199
200    result
201}
202
203#[cfg(test)]
204mod tests {
205    use log::LevelFilter;
206    use snapbox::{Data, IntoData, assert_data_eq, str};
207
208    use super::{ParseResult, parse_spec};
209    use crate::filter::{ParseError, directive::DirectiveKind, op::FilterOp};
210
211    impl IntoData for ParseError {
212        fn into_data(self) -> Data {
213            self.to_string().into_data()
214        }
215    }
216
217    #[test]
218    fn parse_spec_valid() {
219        let ParseResult {
220            directives: dirs,
221            filter,
222            errors,
223        } = parse_spec(
224            "crate1::mod1=error,crate1::mod2,crate2=debug,component:topic=trace,component2=trace",
225        );
226
227        assert_eq!(dirs.len(), 5);
228        assert_eq!(
229            dirs[0].kind,
230            DirectiveKind::Module {
231                module: "crate1::mod1".to_owned()
232            }
233        );
234        assert_eq!(dirs[0].level, LevelFilter::Error);
235
236        assert_eq!(
237            dirs[1].kind,
238            DirectiveKind::Module {
239                module: "crate1::mod2".to_owned()
240            }
241        );
242        assert_eq!(dirs[1].level, LevelFilter::max());
243
244        assert_eq!(
245            dirs[2].kind,
246            DirectiveKind::Component {
247                component: "crate2".to_owned()
248            }
249        );
250        assert_eq!(dirs[2].level, LevelFilter::Debug);
251        assert!(filter.is_none());
252
253        assert_eq!(
254            dirs[3].kind,
255            DirectiveKind::Topic {
256                component: "component".to_owned(),
257                topic: FilterOp::new("topic").unwrap()
258            }
259        );
260        assert_eq!(dirs[3].level, LevelFilter::Trace);
261        assert!(filter.is_none());
262
263        assert_eq!(
264            dirs[4].kind,
265            DirectiveKind::Component {
266                component: "component2".to_owned()
267            }
268        );
269        assert_eq!(dirs[4].level, LevelFilter::Trace);
270        assert!(filter.is_none());
271
272        assert!(errors.is_empty());
273    }
274
275    #[test]
276    fn parse_spec_invalid_crate() {
277        // test parse_spec with multiple = in specification
278        let ParseResult {
279            directives: dirs,
280            filter,
281            errors,
282        } = parse_spec("crate1::mod1=warn=info,crate2=debug");
283
284        assert_eq!(dirs.len(), 1);
285        assert_eq!(
286            dirs[0].kind,
287            DirectiveKind::Component {
288                component: "crate2".to_owned()
289            }
290        );
291        assert_eq!(dirs[0].level, LevelFilter::Debug);
292        assert!(filter.is_none());
293
294        assert_eq!(errors.len(), 1);
295        assert_data_eq!(
296            &errors[0],
297            str!["invalid logging spec 'crate1::mod1=warn=info': '=' is not allowed in paths"]
298        );
299    }
300
301    #[test]
302    fn parse_spec_invalid_level() {
303        // test parse_spec with 'noNumber' as log level
304        let ParseResult {
305            directives: dirs,
306            filter,
307            errors,
308        } = parse_spec("crate1::mod1=noNumber,crate2=debug");
309
310        assert_eq!(dirs.len(), 1);
311        assert_eq!(
312            dirs[0].kind,
313            DirectiveKind::Component {
314                component: "crate2".to_owned()
315            }
316        );
317        assert_eq!(dirs[0].level, LevelFilter::Debug);
318        assert!(filter.is_none());
319
320        assert_eq!(errors.len(), 1);
321        assert_data_eq!(
322            &errors[0],
323            str![
324                "invalid logging spec 'crate1::mod1=noNumber': attempted to convert a string that \
325                 doesn't match an existing log level"
326            ]
327        );
328    }
329
330    #[test]
331    fn parse_spec_string_level() {
332        // test parse_spec with 'warn' as log level
333        let ParseResult {
334            directives: dirs,
335            filter,
336            errors,
337        } = parse_spec("crate1::mod1=wrong,crate2=warn");
338
339        assert_eq!(dirs.len(), 1);
340        assert_eq!(
341            dirs[0].kind,
342            DirectiveKind::Component {
343                component: "crate2".to_owned()
344            }
345        );
346        assert_eq!(dirs[0].level, LevelFilter::Warn);
347        assert!(filter.is_none());
348
349        assert_eq!(errors.len(), 1);
350        assert_data_eq!(
351            &errors[0],
352            str![
353                "invalid logging spec 'crate1::mod1=wrong': attempted to convert a string that \
354                 doesn't match an existing log level"
355            ]
356        );
357    }
358
359    #[test]
360    fn parse_spec_empty_level() {
361        // test parse_spec with '' as log level
362        let ParseResult {
363            directives: dirs,
364            filter,
365            errors,
366        } = parse_spec("crate1::mod1=wrong,crate2=");
367
368        assert_eq!(dirs.len(), 1);
369        assert_eq!(
370            dirs[0].kind,
371            DirectiveKind::Component {
372                component: "crate2".to_owned()
373            }
374        );
375        assert_eq!(dirs[0].level, LevelFilter::max());
376        assert!(filter.is_none());
377
378        assert_eq!(errors.len(), 1);
379        assert_data_eq!(
380            &errors[0],
381            str![
382                "invalid logging spec 'crate1::mod1=wrong': attempted to convert a string that \
383                 doesn't match an existing log level"
384            ]
385        );
386    }
387
388    #[test]
389    fn parse_spec_empty_level_isolated() {
390        // test parse_spec with "" as log level (and the entire spec str)
391        let ParseResult {
392            directives: dirs,
393            filter,
394            errors,
395        } = parse_spec(""); // should be ignored
396        assert_eq!(dirs.len(), 0);
397        assert!(filter.is_none());
398        assert!(errors.is_empty());
399    }
400
401    #[test]
402    fn parse_spec_blank_level_isolated() {
403        // test parse_spec with a white-space-only string specified as the log
404        // level (and the entire spec str)
405        let ParseResult {
406            directives: dirs,
407            filter,
408            errors,
409        } = parse_spec("     "); // should be ignored
410        assert_eq!(dirs.len(), 0);
411        assert!(filter.is_none());
412        assert!(errors.is_empty());
413    }
414
415    #[test]
416    fn parse_spec_blank_level_isolated_comma_only() {
417        // The spec should contain zero or more comma-separated string slices,
418        // so a comma-only string should be interpreted as two empty strings
419        // (which should both be treated as invalid, so ignored).
420        let ParseResult {
421            directives: dirs,
422            filter,
423            errors,
424        } = parse_spec(","); // should be ignored
425        assert_eq!(dirs.len(), 0);
426        assert!(filter.is_none());
427        assert!(errors.is_empty());
428    }
429
430    #[test]
431    fn parse_spec_blank_level_isolated_comma_blank() {
432        // The spec should contain zero or more comma-separated string slices,
433        // so this bogus spec should be interpreted as containing one empty
434        // string and one blank string. Both should both be treated as
435        // invalid, so ignored.
436        let ParseResult {
437            directives: dirs,
438            filter,
439            errors,
440        } = parse_spec(",     "); // should be ignored
441        assert_eq!(dirs.len(), 0);
442        assert!(filter.is_none());
443        assert!(errors.is_empty());
444    }
445
446    #[test]
447    fn parse_spec_blank_level_isolated_blank_comma() {
448        // The spec should contain zero or more comma-separated string slices,
449        // so this bogus spec should be interpreted as containing one blank
450        // string and one empty string. Both should both be treated as
451        // invalid, so ignored.
452        let ParseResult {
453            directives: dirs,
454            filter,
455            errors,
456        } = parse_spec("     ,"); // should be ignored
457        assert_eq!(dirs.len(), 0);
458        assert!(filter.is_none());
459        assert!(errors.is_empty());
460    }
461
462    #[test]
463    fn parse_spec_global() {
464        // test parse_spec with no crate
465        let ParseResult {
466            directives: dirs,
467            filter,
468            errors,
469        } = parse_spec("warn,crate2=debug");
470        assert_eq!(dirs.len(), 2);
471        assert_eq!(dirs[0].kind, DirectiveKind::Any);
472        assert_eq!(dirs[0].level, LevelFilter::Warn);
473        assert_eq!(
474            dirs[1].kind,
475            DirectiveKind::Component {
476                component: "crate2".to_owned()
477            }
478        );
479        assert_eq!(dirs[1].level, LevelFilter::Debug);
480        assert!(filter.is_none());
481        assert!(errors.is_empty());
482    }
483
484    #[test]
485    fn parse_spec_global_bare_warn_lc() {
486        // test parse_spec with no crate, in isolation, all lowercase
487        let ParseResult {
488            directives: dirs,
489            filter,
490            errors,
491        } = parse_spec("warn");
492        assert_eq!(dirs.len(), 1);
493        assert_eq!(dirs[0].kind, DirectiveKind::Any);
494        assert_eq!(dirs[0].level, LevelFilter::Warn);
495        assert!(filter.is_none());
496        assert!(errors.is_empty());
497    }
498
499    #[test]
500    fn parse_spec_global_bare_warn_uc() {
501        // test parse_spec with no crate, in isolation, all uppercase
502        let ParseResult {
503            directives: dirs,
504            filter,
505            errors,
506        } = parse_spec("WARN");
507        assert_eq!(dirs.len(), 1);
508        assert_eq!(dirs[0].kind, DirectiveKind::Any);
509        assert_eq!(dirs[0].level, LevelFilter::Warn);
510        assert!(filter.is_none());
511        assert!(errors.is_empty());
512    }
513
514    #[test]
515    fn parse_spec_global_bare_warn_mixed() {
516        // test parse_spec with no crate, in isolation, mixed case
517        let ParseResult {
518            directives: dirs,
519            filter,
520            errors,
521        } = parse_spec("wArN");
522        assert_eq!(dirs.len(), 1);
523        assert_eq!(dirs[0].kind, DirectiveKind::Any);
524        assert_eq!(dirs[0].level, LevelFilter::Warn);
525        assert!(filter.is_none());
526        assert!(errors.is_empty());
527    }
528
529    #[test]
530    fn parse_spec_valid_filter() {
531        let ParseResult {
532            directives: dirs,
533            filter,
534            errors,
535        } = parse_spec("crate1::mod1=error,crate1::mod2,crate2=debug/abc");
536        assert_eq!(dirs.len(), 3);
537        assert_eq!(
538            dirs[0].kind,
539            DirectiveKind::Module {
540                module: "crate1::mod1".to_owned()
541            }
542        );
543        assert_eq!(dirs[0].level, LevelFilter::Error);
544
545        assert_eq!(
546            dirs[1].kind,
547            DirectiveKind::Module {
548                module: "crate1::mod2".to_owned()
549            }
550        );
551        assert_eq!(dirs[1].level, LevelFilter::max());
552
553        assert_eq!(
554            dirs[2].kind,
555            DirectiveKind::Component {
556                component: "crate2".to_owned()
557            }
558        );
559        assert_eq!(dirs[2].level, LevelFilter::Debug);
560        assert!(filter.is_some() && filter.unwrap().to_string() == "abc");
561        assert!(errors.is_empty());
562    }
563
564    #[test]
565    fn parse_spec_invalid_crate_filter() {
566        let ParseResult {
567            directives: dirs,
568            filter,
569            errors,
570        } = parse_spec("crate1::mod1=error=warn,crate2=debug/a.c");
571
572        assert_eq!(dirs.len(), 1);
573        assert_eq!(
574            dirs[0].kind,
575            DirectiveKind::Component {
576                component: "crate2".to_owned()
577            }
578        );
579        assert_eq!(dirs[0].level, LevelFilter::Debug);
580        assert!(filter.is_some() && filter.unwrap().to_string() == "a.c");
581
582        assert_eq!(errors.len(), 1);
583        assert_data_eq!(
584            &errors[0],
585            str!["invalid logging spec 'crate1::mod1=error=warn': '=' is not allowed in paths"]
586        );
587    }
588
589    #[test]
590    fn parse_spec_empty_with_filter() {
591        let ParseResult {
592            directives: dirs,
593            filter,
594            errors,
595        } = parse_spec("crate1/a*c");
596        assert_eq!(dirs.len(), 1);
597        assert_eq!(
598            dirs[0].kind,
599            DirectiveKind::Component {
600                component: "crate1".to_owned()
601            }
602        );
603        assert_eq!(dirs[0].level, LevelFilter::max());
604        assert!(filter.is_some() && filter.unwrap().to_string() == "a*c");
605        assert!(errors.is_empty());
606    }
607
608    #[test]
609    fn parse_spec_with_multiple_filters() {
610        let ParseResult {
611            directives: dirs,
612            filter,
613            errors,
614        } = parse_spec("debug/abc/a.c");
615        assert!(dirs.is_empty());
616        assert!(filter.is_none());
617
618        assert_eq!(errors.len(), 1);
619        assert_data_eq!(&errors[0], str!["invalid logging spec 'debug/abc/a.c': too many '/'"]);
620    }
621
622    #[test]
623    fn parse_spec_multiple_invalid_crates() {
624        // test parse_spec with multiple = in specification
625        let ParseResult {
626            directives: dirs,
627            filter,
628            errors,
629        } = parse_spec("crate1::mod1=warn=info,crate2=debug,crate3=error=error");
630
631        assert_eq!(dirs.len(), 1);
632        assert_eq!(
633            dirs[0].kind,
634            DirectiveKind::Component {
635                component: "crate2".to_owned()
636            }
637        );
638        assert_eq!(dirs[0].level, LevelFilter::Debug);
639        assert!(filter.is_none());
640
641        assert_eq!(errors.len(), 2);
642        assert_data_eq!(
643            &errors[0],
644            str!["invalid logging spec 'crate1::mod1=warn=info': '=' is not allowed in paths"]
645        );
646        assert_data_eq!(
647            &errors[1],
648            str!["invalid logging spec 'crate3=error=error': '=' is not allowed in paths"]
649        );
650    }
651
652    #[test]
653    fn parse_spec_multiple_invalid_levels() {
654        // test parse_spec with 'noNumber' as log level
655        let ParseResult {
656            directives: dirs,
657            filter,
658            errors,
659        } = parse_spec("crate1::mod1=noNumber,crate2=debug,crate3=invalid");
660
661        assert_eq!(dirs.len(), 1);
662        assert_eq!(
663            dirs[0].kind,
664            DirectiveKind::Component {
665                component: "crate2".to_owned()
666            }
667        );
668        assert_eq!(dirs[0].level, LevelFilter::Debug);
669        assert!(filter.is_none());
670
671        assert_eq!(errors.len(), 2);
672        assert_data_eq!(
673            &errors[0],
674            str![
675                "invalid logging spec 'crate1::mod1=noNumber': attempted to convert a string that \
676                 doesn't match an existing log level"
677            ]
678        );
679        assert_data_eq!(
680            &errors[1],
681            str![
682                "invalid logging spec 'crate3=invalid': attempted to convert a string that \
683                 doesn't match an existing log level"
684            ]
685        );
686    }
687
688    #[test]
689    fn parse_spec_invalid_crate_and_level() {
690        // test parse_spec with 'noNumber' as log level
691        let ParseResult {
692            directives: dirs,
693            filter,
694            errors,
695        } = parse_spec("crate1::mod1=debug=info,crate2=debug,crate3=invalid");
696
697        assert_eq!(dirs.len(), 1);
698        assert_eq!(
699            dirs[0].kind,
700            DirectiveKind::Component {
701                component: "crate2".to_owned()
702            }
703        );
704        assert_eq!(dirs[0].level, LevelFilter::Debug);
705        assert!(filter.is_none());
706
707        assert_eq!(errors.len(), 2);
708        assert_data_eq!(
709            &errors[0],
710            str!["invalid logging spec 'crate1::mod1=debug=info': '=' is not allowed in paths"]
711        );
712        assert_data_eq!(
713            &errors[1],
714            str![
715                "invalid logging spec 'crate3=invalid': attempted to convert a string that \
716                 doesn't match an existing log level"
717            ]
718        );
719    }
720
721    #[test]
722    fn parse_error_message_single_error() {
723        let error = parse_spec("crate1::mod1=debug=info,crate2=debug").ok().unwrap_err();
724        assert_data_eq!(
725            error,
726            str![
727                "error parsing logger filter: invalid logging spec 'crate1::mod1=debug=info': '=' \
728                 is not allowed in paths"
729            ]
730        );
731    }
732
733    #[test]
734    fn parse_error_message_multiple_errors() {
735        let error = parse_spec("crate1::mod1=debug=info,crate2=debug,crate3=invalid")
736            .ok()
737            .unwrap_err();
738        assert_data_eq!(
739            error,
740            str![
741                "error parsing logger filter: invalid logging spec 'crate1::mod1=debug=info': '=' \
742                 is not allowed in paths"
743            ]
744        );
745    }
746}