| // Copyright 2021 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use crate::types::*; |
| use nom::{ |
| self, |
| branch::alt, |
| bytes::complete::{escaped, is_not, tag, take_while, take_while1}, |
| character::complete::{alphanumeric1, char, digit1, hex_digit1, multispace0, none_of, one_of}, |
| combinator::{ |
| all_consuming, complete, cond, map, map_opt, map_res, opt, peek, recognize, verify, |
| }, |
| error::{ErrorKind, ParseError}, |
| multi::{many0, separated_nonempty_list}, |
| sequence::{delimited, pair, preceded, tuple}, |
| IResult, |
| }; |
| |
| macro_rules! comparison { |
| ($tag:literal, $variant:ident) => { |
| map(tag($tag), move |_| ComparisonOperator::$variant) |
| }; |
| } |
| |
| /// Parses comparison operators. |
| fn comparison(input: &str) -> IResult<&str, ComparisonOperator> { |
| alt(( |
| comparison!("=", Equal), |
| comparison!(">=", GreaterEq), |
| comparison!(">", Greater), |
| comparison!("<=", LessEq), |
| comparison!("<", Less), |
| comparison!("!=", NotEq), |
| ))(input) |
| } |
| |
| /// Recognizes 1 or more spaces or tabs. |
| fn whitespace1<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, &'a str, E> { |
| take_while1(move |c| c == ' ' || c == '\t')(input) |
| } |
| |
| /// Recognizes 0 or more spaces or tabs. |
| fn whitespace0<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, &'a str, E> { |
| take_while(move |c| c == ' ' || c == '\t')(input) |
| } |
| |
| /// Parses the `has any` operator in the two flavors we support: all characters uppercase or all |
| /// of them lowercase. |
| fn has_any(input: &str) -> IResult<&str, (&str, &str)> { |
| let (rest, (has, _, any)) = alt(( |
| tuple((tag("has"), whitespace1, tag("any"))), |
| tuple((tag("HAS"), whitespace1, tag("ANY"))), |
| ))(input)?; |
| Ok((rest, (has, any))) |
| } |
| |
| /// Parses the `has all` operator in the two flavors we support: all characters uppercase or all |
| /// of them lowercase. |
| fn has_all(input: &str) -> IResult<&str, (&str, &str)> { |
| let (rest, (has, _, all)) = alt(( |
| tuple((tag("has"), whitespace1, tag("all"))), |
| tuple((tag("HAS"), whitespace1, tag("ALL"))), |
| ))(input)?; |
| Ok((rest, (has, all))) |
| } |
| |
| /// Parses any inclusion operator (`has any`, `has all`, `in`) in the two flavors we support: all |
| /// characters uppercase or all of them lowercase. |
| fn inclusion(input: &str) -> IResult<&str, InclusionOperator> { |
| alt(( |
| map(has_any, move |_| InclusionOperator::HasAny), |
| map(has_all, move |_| InclusionOperator::HasAll), |
| map(alt((tag("in"), tag("IN"))), move |_| InclusionOperator::In), |
| ))(input) |
| } |
| |
| /// Parses any operator (inclusion and comparison). |
| fn operator(input: &str) -> IResult<&str, Operator> { |
| alt(( |
| map(inclusion, move |o| Operator::Inclusion(o)), |
| map(comparison, move |o| Operator::Comparison(o)), |
| ))(input) |
| } |
| |
| // This macro generates: |
| // - A general parser function `$parser` for all the given `tags`. |
| // - Parser functions `$tag_parser` that parses inputs that match the given accepted `$tag`s into |
| // `Severity`. |
| // - A function `severity_sym` that parses an input into a `Severity`. |
| macro_rules! reserved { |
| ( |
| parser: $parser:ident, |
| ty: $type:ident, |
| tags: [ |
| $({ |
| parser: $tag_parser:ident, |
| variant: $variant_name:ident, |
| accepts: [$($tag:ident),+] |
| }),+ |
| ] |
| ) => { |
| $( |
| fn $tag_parser(input: &str) -> IResult<&str, $type> { |
| map(alt(($(tag(stringify!($tag))),+)), move |_| $type::$variant_name)(input) |
| } |
| )+ |
| |
| fn $parser(input: &str) -> IResult<&str, $type> { |
| alt(($( $tag_parser ),+))(input) |
| } |
| }; |
| } |
| |
| reserved!( |
| parser: severity_sym, |
| ty: Severity, |
| tags: [ |
| { |
| parser: trace, |
| variant: Trace, |
| accepts: [ trace, Trace, TRACE ] |
| }, |
| { |
| parser: debug, |
| variant: Debug, |
| accepts: [ debug, Debug, DEBUG ] |
| }, |
| { |
| parser: info, |
| variant: Info, |
| accepts: [ info, Info, INFO ] |
| }, |
| { |
| parser: warn, |
| variant: Warn, |
| accepts: [ warn, Warn, WARN ] |
| }, |
| { |
| parser: error, |
| variant: Error, |
| accepts: [ error, Error, ERROR ] |
| }, |
| { |
| parser: fatal, |
| variant: Fatal, |
| accepts: [ fatal, Fatal, FATAL ] |
| } |
| ] |
| ); |
| |
| reserved!( |
| parser: identifier, |
| ty: Identifier, |
| tags: [ |
| { |
| parser: filename, |
| variant: Filename, |
| accepts: [ filename, Filename, FILENAME ] |
| }, |
| { |
| parser: lifecycle_event_type, |
| variant: LifecycleEventType, |
| accepts: [ lifecycle_event_type, LifecycleEventType, lifecycleEventType, LIFECYCLE_EVENT_TYPE ] |
| }, |
| { |
| parser: line_number, |
| variant: LineNumber, |
| accepts: [ line_number, LineNumber, lineNumber, LINE_NUMBER ] |
| }, |
| { |
| parser: pid, |
| variant: Pid, |
| accepts: [ pid, Pid, PID ] |
| }, |
| { |
| parser: severity, |
| variant: Severity, |
| accepts: [ severity, Severity, SEVERITY ] |
| }, |
| { |
| parser: tags, |
| variant: Tags, |
| accepts: [ tags, Tags, TAGS ] |
| }, |
| { |
| parser: tid, |
| variant: Tid, |
| accepts: [ tid, Tid, TID ] |
| }, |
| { |
| parser: timestamp, |
| variant: Timestamp, |
| accepts: [ timestamp, Timestamp, TIMESTAMP ] |
| } |
| ] |
| ); |
| |
| // This macro is purely a utility fo validating that all the values in the given `$one_or_many` are |
| // of a given type. |
| macro_rules! match_one_or_many_value { |
| ($one_or_many:ident, $variant:pat) => { |
| match $one_or_many { |
| OneOrMany::One($variant) => true, |
| OneOrMany::Many(values) => values.iter().all(|value| matches!(value, $variant)), |
| _ => false, |
| } |
| }; |
| } |
| |
| impl Identifier { |
| /// Validates that all the values are of a type that can be used in an operation with this |
| /// identifier. |
| fn can_be_used_with_value_type(&self, value: &OneOrMany<Value<'_>>) -> bool { |
| match (self, value) { |
| (Identifier::Filename | Identifier::LifecycleEventType | Identifier::Tags, value) => { |
| match_one_or_many_value!(value, Value::StringLiteral(_)) |
| } |
| // TODO(fxbug.dev/86960): similar to severities, we can probably have reserved values |
| // for lifecycle event types. |
| // TODO(fxbug.dev/86961): support time diferences (1h30m, 30s, etc) instead of only |
| // timestamp comparison. |
| ( |
| Identifier::Pid | Identifier::Tid | Identifier::LineNumber | Identifier::Timestamp, |
| value, |
| ) => { |
| match_one_or_many_value!(value, Value::Number(_)) |
| } |
| // TODO(fxbug.dev/86962): it should also be possible to compare severities with a fixed |
| // set of numbers. |
| (Identifier::Severity, value) => { |
| match_one_or_many_value!(value, Value::Severity(_)) |
| } |
| } |
| } |
| |
| /// Validates that this identifier can be used in an operation defined by the given `operator`. |
| fn can_be_used_with_operator(&self, operator: &Operator) -> bool { |
| match (self, &operator) { |
| ( |
| Identifier::Filename |
| | Identifier::LifecycleEventType |
| | Identifier::Pid |
| | Identifier::Tid |
| | Identifier::LineNumber |
| | Identifier::Severity, |
| Operator::Comparison(ComparisonOperator::Equal) |
| | Operator::Comparison(ComparisonOperator::NotEq) |
| | Operator::Inclusion(InclusionOperator::In), |
| ) => true, |
| (Identifier::Severity | Identifier::Timestamp, Operator::Comparison(_)) => true, |
| ( |
| Identifier::Tags, |
| Operator::Inclusion(InclusionOperator::HasAny | InclusionOperator::HasAll), |
| ) => true, |
| _ => false, |
| } |
| } |
| } |
| |
| /// Parses an input containing any number and type of whitespace at the front. |
| fn spaced<'a, E, F, O>(parser: F) -> impl Fn(&'a str) -> IResult<&'a str, O, E> |
| where |
| F: Fn(&'a str) -> IResult<&'a str, O, E>, |
| E: ParseError<&'a str>, |
| { |
| preceded(whitespace0, parser) |
| } |
| |
| /// Parses the input as a string literal wrapped in double quotes. Returns the value |
| /// inside the quotes. |
| fn string_literal(input: &str) -> IResult<&str, &str> { |
| // TODO(fxbug.dev/86963): this doesn't handle escape sequences. |
| // Consider accepting escaped `"`: `\"` too. |
| let (rest, value) = recognize(delimited(char('"'), many0(is_not("\"")), char('"')))(input)?; |
| Ok((rest, &value[1..value.len() - 1])) |
| } |
| |
| /// Parses a 64 bit unsigned integer. |
| fn integer(input: &str) -> IResult<&str, u64> { |
| map_res(digit1, |s: &str| s.parse::<u64>())(input) |
| } |
| |
| /// Parses a hexadecimal number as 64 bit integer. |
| fn hex_integer(input: &str) -> IResult<&str, u64> { |
| map_res(preceded(tag("0x"), hex_digit1), |s: &str| u64::from_str_radix(&s, 16))(input) |
| } |
| |
| /// Parses a unsigned decimal and hexadecimal numbers of 64 bits. |
| /// For example: 123, 0x7b will be accepted as |
| // TODO(fxbug.dev/86964): consider also accepting signed numbers. |
| fn number(input: &str) -> IResult<&str, u64> { |
| alt((hex_integer, integer))(input) |
| } |
| |
| // This macro parses a list of expressions accepted by the given `$parser` comma separated with any |
| // number of spaces in between. |
| macro_rules! comma_separated_value { |
| ($parser:ident, $value:expr) => { |
| map(separated_nonempty_list(spaced(char(',')), spaced($parser)), move |ns| { |
| ns.into_iter().map(|n| $value(n)).collect() |
| }) |
| }; |
| } |
| |
| /// Parses a list of values. Each list can only contain values of the same type: |
| /// - String literal |
| /// - Number |
| /// - Severity |
| fn list_of_values(input: &str) -> IResult<&str, Vec<Value<'_>>> { |
| delimited( |
| spaced(char('[')), |
| alt(( |
| comma_separated_value!(number, Value::Number), |
| comma_separated_value!(string_literal, Value::StringLiteral), |
| comma_separated_value!(severity_sym, Value::Severity), |
| )), |
| spaced(char(']')), |
| )(input) |
| } |
| |
| /// Parses a single filter expression in a metadata selector. |
| fn filter_expression(input: &str) -> IResult<&str, FilterExpression<'_>> { |
| let (rest, identifier) = spaced(identifier)(input)?; |
| let (rest, op) = |
| verify(spaced(operator), |op| identifier.can_be_used_with_operator(&op))(rest)?; |
| let (rest, op) = map_opt( |
| verify( |
| spaced(alt(( |
| map(number, move |n| OneOrMany::One(Value::Number(n))), |
| map(severity_sym, move |s| OneOrMany::One(Value::Severity(s))), |
| map(string_literal, move |s| OneOrMany::One(Value::StringLiteral(s))), |
| map(list_of_values, OneOrMany::Many), |
| ))), |
| |value| identifier.can_be_used_with_value_type(value), |
| ), |
| move |one_or_many| Operation::maybe_new(op, one_or_many), |
| )(rest)?; |
| Ok((rest, FilterExpression { identifier, op })) |
| } |
| |
| /// Parses a metadata selector. |
| fn metadata_selector(input: &str) -> IResult<&str, MetadataSelector<'_>> { |
| let (rest, _) = spaced(alt((tag("WHERE"), tag("where"))))(input)?; |
| let (rest, filters) = spaced(separated_nonempty_list(char(','), filter_expression))(rest)?; |
| Ok((rest, MetadataSelector::new(filters))) |
| } |
| |
| /// Parses a tree selector, which is a node selector and an optional property selector. |
| fn tree_selector(input: &str) -> IResult<&str, TreeSelector<'_>> { |
| let esc = escaped(none_of(":/\\ \t\n"), '\\', one_of("* \t/:\\")); |
| let (rest, node_segments) = |
| verify(separated_nonempty_list(tag("/"), &esc), |segments: &Vec<&str>| { |
| !segments.iter().any(|s| s.contains("**")) |
| })(input)?; |
| let (rest, property_segment) = if peek::<&str, _, (&str, ErrorKind), _>(tag(":"))(rest).is_ok() |
| { |
| let (rest, _) = tag(":")(rest)?; |
| let (rest, property) = |
| verify(esc, |value: &str| !value.is_empty() && !value.contains("**"))(rest)?; |
| (rest, Some(property)) |
| } else { |
| (rest, None) |
| }; |
| Ok(( |
| rest, |
| TreeSelector { |
| node: node_segments.into_iter().map(|value| value.into()).collect(), |
| property: property_segment.map(|value| value.into()), |
| }, |
| )) |
| } |
| |
| /// Parses a component selector. |
| fn component_selector(input: &str) -> IResult<&str, ComponentSelector<'_>> { |
| let accepted_characters = escaped( |
| alt((alphanumeric1, tag("*"), tag("."), tag("-"), tag("_"), tag(">"), tag("<"))), |
| '\\', |
| tag(":"), |
| ); |
| let (rest, segments) = verify( |
| separated_nonempty_list(tag("/"), recognize(accepted_characters)), |
| |segments: &Vec<&str>| { |
| // TODO: it's probably possible to write this more cleanly as a combinator. |
| segments.iter().enumerate().all(|(i, segment)| { |
| if segment.contains("**") { |
| if i == segments.len() - 1 { |
| // The last segment can be the recursive glob, but nothing else. |
| return *segment == "**"; |
| } |
| // Other segments aren't allowed to contain recursive globs. |
| return false; |
| } |
| true |
| }) |
| }, |
| )(input)?; |
| Ok((rest, ComponentSelector { segments: segments.into_iter().map(|s| s.into()).collect() })) |
| } |
| |
| /// A comment allowed in selector files. |
| fn comment(input: &str) -> IResult<&str, &str> { |
| let (rest, comment) = spaced(preceded(tag("//"), is_not("\n\r")))(input)?; |
| if rest.len() > 0 { |
| let (rest, _) = one_of("\n\r")(rest)?; // consume the newline character |
| return Ok((rest, comment)); |
| } |
| Ok((rest, comment)) |
| } |
| |
| /// Parses a core selector (component + tree + property). It accepts both raw selectors or |
| /// selectors wrapped in double quotes. Selectors wrapped in quotes accept spaces in the tree and |
| /// property names and require internal quotes to be escaped. |
| fn core_selector(input: &str) -> IResult<&str, (ComponentSelector<'_>, TreeSelector<'_>)> { |
| let (rest, (component, _, tree)) = tuple((component_selector, tag(":"), tree_selector))(input)?; |
| Ok((rest, (component, tree))) |
| } |
| |
| /// Recognizes selectors, with comments allowed or disallowed. |
| fn do_parse_selector<'a>( |
| allow_inline_comment: bool, |
| ) -> impl Fn(&'a str) -> IResult<&'a str, Selector<'a>, (&'a str, ErrorKind)> { |
| map( |
| tuple(( |
| spaced(core_selector), |
| opt(metadata_selector), |
| cond(allow_inline_comment, opt(comment)), |
| multispace0, |
| )), |
| move |((component, tree), metadata, _, _)| Selector { component, tree, metadata }, |
| ) |
| } |
| |
| /// Parses the input into a `Selector`. |
| pub fn selector(input: &str) -> Result<Selector<'_>, (&str, ErrorKind)> { |
| let result = complete(all_consuming(do_parse_selector(/*allow_inline_comment=*/ false)))(input); |
| match result { |
| Ok((_, s)) => Ok(s), |
| Err(nom::Err::Error(e) | nom::Err::Failure(e)) => Err(e), |
| _ => unreachable!("through the complete combinator we get rid of Incomplete"), |
| } |
| } |
| |
| /// Parses the input into a `ComponentSelector` ignoring any whitespace around the component |
| /// selector. |
| pub fn consuming_component_selector( |
| input: &str, |
| ) -> Result<ComponentSelector<'_>, (&str, ErrorKind)> { |
| let result = |
| nom::combinator::all_consuming(pair(spaced(component_selector), multispace0))(input); |
| match result { |
| Ok((_, (selector, _))) => Ok(selector), |
| Err(nom::Err::Error(e) | nom::Err::Failure(e)) => Err(e), |
| _ => unreachable!("through the complete combinator we get rid of Incomplete"), |
| } |
| } |
| |
| /// Parses the given input line into a Selector or None. |
| pub fn selector_or_comment(input: &str) -> Result<Option<Selector<'_>>, (&str, ErrorKind)> { |
| let result = complete(all_consuming(alt(( |
| map(comment, |_| None), |
| map(do_parse_selector(/*allow_inline_comment=*/ true), |s| Some(s)), |
| ))))(input); |
| match result { |
| Ok((_, maybe_selector)) => Ok(maybe_selector), |
| Err(nom::Err::Error(e) | nom::Err::Failure(e)) => Err(e), |
| _ => unreachable!("through the complete combinator we get rid of Incomplete"), |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use nom::combinator::all_consuming; |
| use rand::distributions::Distribution; |
| |
| #[fuchsia::test] |
| fn canonical_component_selector_test() { |
| let test_vector = vec![ |
| ( |
| "a/b/c", |
| vec![ |
| Segment::ExactMatch("a".into()), |
| Segment::ExactMatch("b".into()), |
| Segment::ExactMatch("c".into()), |
| ], |
| ), |
| ( |
| "a/*/c", |
| vec![ |
| Segment::ExactMatch("a".into()), |
| Segment::Pattern("*"), |
| Segment::ExactMatch("c".into()), |
| ], |
| ), |
| ( |
| "a/b*/c", |
| vec![ |
| Segment::ExactMatch("a".into()), |
| Segment::Pattern("b*"), |
| Segment::ExactMatch("c".into()), |
| ], |
| ), |
| ( |
| "a/b/**", |
| vec![ |
| Segment::ExactMatch("a".into()), |
| Segment::ExactMatch("b".into()), |
| Segment::Pattern("**"), |
| ], |
| ), |
| ( |
| "core/session\\:id/foo", |
| vec![ |
| Segment::ExactMatch("core".into()), |
| Segment::ExactMatch("session:id".into()), |
| Segment::ExactMatch("foo".into()), |
| ], |
| ), |
| ("c", vec![Segment::ExactMatch("c".into())]), |
| ("<component_manager>", vec![Segment::ExactMatch("<component_manager>".into())]), |
| ( |
| r#"a/*/b/**"#, |
| vec![ |
| Segment::ExactMatch("a".into()), |
| Segment::Pattern("*"), |
| Segment::ExactMatch("b".into()), |
| Segment::Pattern("**"), |
| ], |
| ), |
| ]; |
| |
| for (test_string, expected_segments) in test_vector { |
| let (_, component_selector) = component_selector(&test_string).unwrap(); |
| |
| assert_eq!( |
| expected_segments, component_selector.segments, |
| "For '{}', got: {:?}", |
| test_string, component_selector, |
| ); |
| } |
| } |
| |
| #[fuchsia::test] |
| fn missing_path_component_selector_test() { |
| let component_selector_string = "c"; |
| let (_, component_selector) = component_selector(component_selector_string).unwrap(); |
| let mut path_vec = component_selector.segments; |
| assert_eq!(path_vec.pop(), Some(Segment::ExactMatch("c".into()))); |
| assert!(path_vec.is_empty()); |
| } |
| |
| #[fuchsia::test] |
| fn errorful_component_selector_test() { |
| let test_vector: Vec<&str> = vec![ |
| "", |
| "a\\", |
| r#"a/b***/c"#, |
| r#"a/***/c"#, |
| r#"a/**/c"#, |
| // NOTE: This used to be accepted but not anymore. Spaces shouldn't be a valid component |
| // selector character since it's not a valid moniker character. |
| " ", |
| // NOTE: The previous parser was accepting quotes in component selectors. However, by |
| // definition, a component moniker (both in v1 and v2) doesn't allow a `*` in its name. |
| r#"a/b\*/c"#, |
| r#"a/\*/c"#, |
| // Invalid characters |
| "a$c/d", |
| ]; |
| for test_string in test_vector { |
| let component_selector_result = all_consuming(component_selector)(test_string); |
| assert!(component_selector_result.is_err(), "expected '{}' to fail", test_string); |
| } |
| } |
| |
| #[fuchsia::test] |
| fn canonical_tree_selector_test() { |
| let test_vector = vec![ |
| ( |
| "a/b:c", |
| vec![Segment::ExactMatch("a".into()), Segment::ExactMatch("b".into())], |
| Some(Segment::ExactMatch("c".into())), |
| ), |
| ( |
| "a/*:c", |
| vec![Segment::ExactMatch("a".into()), Segment::Pattern("*")], |
| Some(Segment::ExactMatch("c".into())), |
| ), |
| ( |
| "a/b:*", |
| vec![Segment::ExactMatch("a".into()), Segment::ExactMatch("b".into())], |
| Some(Segment::Pattern("*")), |
| ), |
| ("a/b", vec![Segment::ExactMatch("a".into()), Segment::ExactMatch("b".into())], None), |
| ( |
| r#"a/b\:\*c"#, |
| vec![Segment::ExactMatch("a".into()), Segment::ExactMatch("b:*c".into())], |
| None, |
| ), |
| ]; |
| |
| for (string, expected_path, expected_property) in test_vector { |
| let (_, tree_selector) = tree_selector(string).unwrap(); |
| assert_eq!( |
| tree_selector, |
| TreeSelector { node: expected_path, property: expected_property } |
| ); |
| } |
| } |
| |
| #[fuchsia::test] |
| fn errorful_tree_selector_test() { |
| let test_vector = vec![ |
| // Not allowed due to empty property selector. |
| "a/b:", |
| // Not allowed due to glob property selector. |
| "a/b:**", |
| // String literals can't have globs. |
| r#"a/b**:c"#, |
| // Property selector string literals cant have globs. |
| r#"a/b:c**"#, |
| "a/b:**", |
| // Node path cant have globs. |
| "a/**:c", |
| // Node path can't be empty |
| ":c", |
| // Spaces aren't accepted when parsing with allow_spaces=false. |
| "a b:c", |
| "a*b:\tc", |
| ]; |
| for string in test_vector { |
| assert!(all_consuming(tree_selector)(string).is_err(), "{} should fail", string); |
| } |
| } |
| |
| #[fuchsia::test] |
| fn tree_selector_with_spaces() { |
| let with_spaces = vec![ |
| ( |
| r#"a\ b:c"#, |
| vec![Segment::ExactMatch("a b".into())], |
| Some(Segment::ExactMatch("c".into())), |
| ), |
| ( |
| r#"ab/\ d:c\ "#, |
| vec![Segment::ExactMatch("ab".into()), Segment::ExactMatch(" d".into())], |
| Some(Segment::ExactMatch("c ".into())), |
| ), |
| ("a\\\t*b:c", vec![Segment::Pattern("a\\\t*b")], Some(Segment::ExactMatch("c".into()))), |
| ( |
| r#"a\ "x":c"#, |
| vec![Segment::ExactMatch(r#"a "x""#.into())], |
| Some(Segment::ExactMatch("c".into())), |
| ), |
| ]; |
| for (string, node, property) in with_spaces { |
| assert_eq!( |
| all_consuming(tree_selector)(string).unwrap().1, |
| TreeSelector { node, property } |
| ); |
| } |
| |
| // Un-escaped quotes aren't accepted when parsing with spaces. |
| assert!(all_consuming(tree_selector)(r#"a/b:"xc"/d"#).is_err()); |
| } |
| |
| #[fuchsia::test] |
| fn parse_full_selector() { |
| assert_eq!( |
| selector( |
| "core/**:some-node/he*re:prop where filename = \"baz\", severity in [info, error]" |
| ) |
| .unwrap(), |
| Selector { |
| component: ComponentSelector { |
| segments: vec![Segment::ExactMatch("core".into()), Segment::Pattern("**"),], |
| }, |
| tree: TreeSelector { |
| node: vec![Segment::ExactMatch("some-node".into()), Segment::Pattern("he*re"),], |
| property: Some(Segment::ExactMatch("prop".into())), |
| }, |
| metadata: Some(MetadataSelector::new(vec![ |
| FilterExpression { |
| identifier: Identifier::Filename, |
| op: Operation::Comparison( |
| ComparisonOperator::Equal, |
| Value::StringLiteral("baz") |
| ), |
| }, |
| FilterExpression { |
| identifier: Identifier::Severity, |
| op: Operation::Inclusion( |
| InclusionOperator::In, |
| vec![Value::Severity(Severity::Info), Value::Severity(Severity::Error)] |
| ), |
| }, |
| ])), |
| } |
| ); |
| |
| // Parses selectors without metadata. Also ignores whitespace. |
| assert_eq!( |
| selector(" foo:bar ").unwrap(), |
| Selector { |
| component: ComponentSelector { segments: vec![Segment::ExactMatch("foo".into())] }, |
| tree: TreeSelector { |
| node: vec![Segment::ExactMatch("bar".into())], |
| property: None |
| }, |
| metadata: None, |
| } |
| ); |
| |
| // At least one filter is required when `where` is provided. |
| assert!(selector("foo:bar where").is_err()); |
| } |
| |
| #[fuchsia::test] |
| fn assert_no_trailing_backward_slash() { |
| assert!(selector(r#"foo:bar:baz\"#).is_err()); |
| } |
| |
| #[fuchsia::test] |
| fn parse_full_selector_with_spaces() { |
| assert_eq!( |
| selector(r#"core/foo:some\ node/*:prop where pid = 123"#).unwrap(), |
| Selector { |
| component: ComponentSelector { |
| segments: vec![ |
| Segment::ExactMatch("core".into()), |
| Segment::ExactMatch("foo".into()), |
| ], |
| }, |
| tree: TreeSelector { |
| node: vec![Segment::ExactMatch("some node".into()), Segment::Pattern("*"),], |
| property: Some(Segment::ExactMatch("prop".into())), |
| }, |
| metadata: Some(MetadataSelector::new(vec![FilterExpression { |
| identifier: Identifier::Pid, |
| op: Operation::Comparison(ComparisonOperator::Equal, Value::Number(123)), |
| },])), |
| } |
| ); |
| } |
| |
| macro_rules! test_sym_parser { |
| ( |
| parser: $parser:ident, |
| accepts: [$([$($accepted:literal),+] => $expected:expr),+] |
| ) => {{ |
| // Verify we can parse all accepted values. |
| $($(assert_eq!(Ok(("", $expected)), $parser($accepted));)+)+ |
| // Verify we reject all rejected values. |
| assert!($parser("").is_err()); |
| |
| // Verify we fail to parse any of the accepted values with random case swaps. |
| let accepted_values = vec![$($($accepted),+),+]; |
| // Generates random numbers in [0,2) |
| let uniform = rand::distributions::Uniform::from(0..2); |
| let mut rng = rand::thread_rng(); |
| for value in &accepted_values { |
| loop { |
| let mut rejected_value = String::new(); |
| rejected_value.reserve(value.len()); |
| |
| for byte in value.as_bytes() { |
| let c = *byte as char; |
| if uniform.sample(&mut rng) == 1 { |
| if c.is_lowercase() { |
| rejected_value.push(c.to_uppercase().next().unwrap()); |
| } |
| if c.is_uppercase() { |
| rejected_value.push(c.to_lowercase().next().unwrap()); |
| } |
| } else { |
| rejected_value.push(c); |
| } |
| } |
| if accepted_values.contains(&&*rejected_value) { |
| continue; |
| } |
| assert!( |
| $parser(&rejected_value).is_err(), |
| "{} should be rejected by {}", rejected_value, stringify!($operator)); |
| break; |
| } |
| } |
| }}; |
| } |
| |
| #[fuchsia::test] |
| fn parse_identifier() { |
| test_sym_parser! { |
| parser: identifier, |
| accepts: [ |
| [ "filename", "Filename", "FILENAME" ] => Identifier::Filename, |
| [ |
| "lifecycle_event_type", "LifecycleEventType", "LIFECYCLE_EVENT_TYPE", |
| "lifecycleEventType" |
| ] => Identifier::LifecycleEventType, |
| [ |
| "line_number", "LineNumber", "LINE_NUMBER", "lineNumber" |
| ] => Identifier::LineNumber, |
| [ "pid", "Pid", "PID" ] => Identifier::Pid, |
| [ "severity", "Severity", "SEVERITY" ] => Identifier::Severity, |
| [ "tags", "Tags", "TAGS" ] => Identifier::Tags, |
| [ "tid", "Tid", "TID" ] => Identifier::Tid, |
| [ "timestamp", "Timestamp", "TIMESTAMP" ] => Identifier::Timestamp |
| ] |
| } |
| } |
| |
| #[fuchsia::test] |
| fn parse_operator() { |
| test_sym_parser! { |
| parser: operator, |
| accepts: [ |
| [ "=" ] => Operator::Comparison(ComparisonOperator::Equal), |
| [ ">" ] => Operator::Comparison(ComparisonOperator::Greater), |
| [ ">=" ] => Operator::Comparison(ComparisonOperator::GreaterEq), |
| [ "<" ] => Operator::Comparison(ComparisonOperator::Less), |
| [ "<=" ] => Operator::Comparison(ComparisonOperator::LessEq), |
| [ "!=" ] => Operator::Comparison(ComparisonOperator::NotEq), |
| [ "in", "IN" ] => Operator::Inclusion(InclusionOperator::In), |
| [ "has any", "HAS ANY" ] => Operator::Inclusion(InclusionOperator::HasAny), |
| [ "has all", "HAS ALL" ] => Operator::Inclusion(InclusionOperator::HasAll) |
| ] |
| } |
| } |
| |
| #[fuchsia::test] |
| fn parse_severity() { |
| test_sym_parser! { |
| parser: severity_sym, |
| accepts: [ |
| [ "trace", "TRACE", "Trace" ] => Severity::Trace, |
| [ "debug", "DEBUG", "Debug" ] => Severity::Debug, |
| [ "info", "INFO", "Info" ] => Severity::Info, |
| [ "warn", "WARN", "Warn" ] => Severity::Warn, |
| [ "error", "ERROR", "Error" ] => Severity::Error |
| ] |
| } |
| } |
| |
| #[fuchsia::test] |
| fn parse_string_literal() { |
| // Only strings within quotes are accepted |
| assert_eq!(Ok(("", "foo")), string_literal("\"foo\"")); |
| assert_eq!(Ok(("", "_2")), string_literal("\"_2\"")); |
| assert_eq!(Ok(("", "$3.x")), string_literal("\"$3.x\"")); |
| |
| // The empty string is a valid string |
| assert_eq!(Ok(("", "")), string_literal("\"\"")); |
| |
| // Inputs with missing quotes aren't accepted. |
| assert!(string_literal("\"foo").is_err()); |
| assert!(string_literal("foo\"").is_err()); |
| assert!(string_literal("foo").is_err()); |
| } |
| |
| #[fuchsia::test] |
| fn parse_number() { |
| // Unsigned 64 bit integers are accepted. |
| assert_eq!(Ok(("", 0)), number("0")); |
| assert_eq!(Ok(("", 1234567890)), number("1234567890")); |
| assert_eq!(Ok(("", std::u64::MAX)), number(&format!("{}", std::u64::MAX))); |
| |
| // Unsigned hexadecimal 64 bit integers are accepted. |
| assert_eq!(Ok(("", 0)), number("0x0")); |
| assert_eq!(Ok(("", 1311768467463790320)), number("0x123456789abcdef0")); |
| assert_eq!(Ok(("", std::u64::MAX)), number("0xffffffffffffffff")); |
| |
| // Not hexadecimal chars are rejected |
| assert_eq!(Ok(("g", 171)), number("0xabg")); |
| |
| // Negative numbers aren't accepted, for now. |
| assert!(number("-1").is_err()); |
| assert!(number("-0xdf").is_err()); |
| |
| // Numbers that don't fit in 64 bits are rejected. |
| assert!(number("18446744073709551616").is_err()); //2^64 |
| assert!(all_consuming(number)("0xffffffffffffffffff").is_err()); |
| } |
| |
| #[fuchsia::test] |
| fn parse_list_of_values() { |
| // Accepts values of the same type. |
| let expected = vec![0, 25, 149].into_iter().map(|n| Value::Number(n)).collect(); |
| assert_eq!(Ok(("", expected)), list_of_values("[0, 25, 0x95]")); |
| assert_eq!(Ok(("", vec![Value::Number(3)])), list_of_values("[3]")); |
| |
| let expected = |
| vec![Severity::Info, Severity::Warn].into_iter().map(|s| Value::Severity(s)).collect(); |
| assert_eq!(Ok(("", expected)), list_of_values("[INFO,warn]")); |
| |
| let expected = vec!["foo", "bar"].into_iter().map(|s| Value::StringLiteral(s)).collect(); |
| assert_eq!(Ok(("", expected)), list_of_values(r#"[ "foo", "bar" ]"#)); |
| |
| // Rejects values of mixed types |
| assert!(list_of_values("[INFO, 2]").is_err()); |
| assert!(list_of_values(r#"[1, "foo"]"#).is_err()); |
| assert!(list_of_values(r#"["bar", WARN]"#).is_err()); |
| assert!(list_of_values(r#"["bar", 2, error]"#).is_err()); |
| |
| // The empty list is rejected. |
| assert!(list_of_values("[]").is_err()); |
| } |
| |
| #[fuchsia::test] |
| fn parse_filter_expression() { |
| let expected = FilterExpression { |
| identifier: Identifier::Pid, |
| op: Operation::Comparison(ComparisonOperator::Equal, Value::Number(123)), |
| }; |
| assert_eq!(Ok(("", expected)), filter_expression("pid = 123")); |
| |
| let expected = FilterExpression { |
| identifier: Identifier::Severity, |
| op: Operation::Comparison( |
| ComparisonOperator::GreaterEq, |
| Value::Severity(Severity::Info), |
| ), |
| }; |
| assert_eq!(Ok(("", expected)), filter_expression("severity>=info")); |
| |
| // All three operands are required |
| assert!(filter_expression("tid >").is_err()); |
| assert!(filter_expression("!= 3").is_err()); |
| |
| // The inclusion operator HAS can be used with lists and single values. |
| let expected = FilterExpression { |
| identifier: Identifier::Tags, |
| op: Operation::Inclusion( |
| InclusionOperator::HasAny, |
| vec![Value::StringLiteral("foo"), Value::StringLiteral("bar")], |
| ), |
| }; |
| assert_eq!(Ok(("", expected)), filter_expression("tags HAS ANY [\"foo\", \"bar\"]")); |
| |
| let expected = FilterExpression { |
| identifier: Identifier::Tags, |
| op: Operation::Inclusion(InclusionOperator::HasAny, vec![Value::StringLiteral("foo")]), |
| }; |
| assert_eq!(Ok(("", expected)), filter_expression("tags has any \"foo\"")); |
| |
| // The inclusion operator IN can only be used with lists. |
| let expected = FilterExpression { |
| identifier: Identifier::LifecycleEventType, |
| op: Operation::Inclusion( |
| InclusionOperator::In, |
| vec![Value::StringLiteral("started"), Value::StringLiteral("stopped")], |
| ), |
| }; |
| assert_eq!( |
| Ok(("", expected)), |
| filter_expression("lifecycle_event_type in [\"started\", \"stopped\"]") |
| ); |
| assert!(filter_expression("pid in 123").is_err()); |
| } |
| |
| #[fuchsia::test] |
| fn filename_operations() { |
| let expected = FilterExpression { |
| identifier: Identifier::Filename, |
| op: Operation::Inclusion(InclusionOperator::In, vec![Value::StringLiteral("foo.rs")]), |
| }; |
| assert_eq!(Ok(("", expected)), filter_expression("filename in [\"foo.rs\"]")); |
| |
| let expected = FilterExpression { |
| identifier: Identifier::Filename, |
| op: Operation::Comparison(ComparisonOperator::Equal, Value::StringLiteral("foo.rs")), |
| }; |
| assert_eq!(Ok(("", expected)), filter_expression("filename = \"foo.rs\"")); |
| |
| let expected = FilterExpression { |
| identifier: Identifier::Filename, |
| op: Operation::Comparison(ComparisonOperator::NotEq, Value::StringLiteral("foo.rs")), |
| }; |
| assert_eq!(Ok(("", expected)), filter_expression("filename != \"foo.rs\"")); |
| |
| assert!(filter_expression("filename > \"foo.rs\"").is_err()); |
| assert!(filter_expression("filename < \"foo.rs\"").is_err()); |
| assert!(filter_expression("filename >= \"foo.rs\"").is_err()); |
| assert!(filter_expression("filename <= \"foo.rs\"").is_err()); |
| assert!(filter_expression("filename has any [\"foo.rs\"]").is_err()); |
| assert!(filter_expression("filename has all [\"foo.rs\"]").is_err()); |
| } |
| |
| #[fuchsia::test] |
| fn lifecycle_event_type_operations() { |
| let expected = FilterExpression { |
| identifier: Identifier::LifecycleEventType, |
| op: Operation::Inclusion(InclusionOperator::In, vec![Value::StringLiteral("stopped")]), |
| }; |
| assert_eq!(Ok(("", expected)), filter_expression("lifecycle_event_type in [\"stopped\"]")); |
| |
| let expected = FilterExpression { |
| identifier: Identifier::LifecycleEventType, |
| op: Operation::Comparison(ComparisonOperator::Equal, Value::StringLiteral("stopped")), |
| }; |
| assert_eq!(Ok(("", expected)), filter_expression("lifecycle_event_type = \"stopped\"")); |
| |
| let expected = FilterExpression { |
| identifier: Identifier::LifecycleEventType, |
| op: Operation::Comparison(ComparisonOperator::NotEq, Value::StringLiteral("stopped")), |
| }; |
| assert_eq!(Ok(("", expected)), filter_expression("lifecycle_event_type != \"stopped\"")); |
| |
| assert!(filter_expression("lifecycle_event_type > \"stopped\"").is_err()); |
| assert!(filter_expression("lifecycle_event_type < \"started\"").is_err()); |
| assert!(filter_expression("lifecycle_event_type >= \"diagnostics_ready\"").is_err()); |
| assert!(filter_expression("lifecycle_event_type <= \"log_sink_connected\"").is_err()); |
| assert!( |
| filter_expression("lifecycle_event_type has all [\"started\", \"stopped\"]").is_err() |
| ); |
| assert!( |
| filter_expression("lifecycle_event_type has any [\"started\", \"stopped\"]").is_err() |
| ); |
| } |
| |
| #[fuchsia::test] |
| fn line_number_pid_tid_operations() { |
| for (identifier, identifier_str) in vec![ |
| (Identifier::Pid, "pid"), |
| (Identifier::Tid, "tid"), |
| (Identifier::LineNumber, "line_number"), |
| ] { |
| let expected = FilterExpression { |
| identifier: identifier.clone(), |
| op: Operation::Inclusion( |
| InclusionOperator::In, |
| vec![Value::Number(100), Value::Number(200)], |
| ), |
| }; |
| assert_eq!( |
| Ok(("", expected)), |
| filter_expression(&format!("{} in [100, 200]", identifier_str)) |
| ); |
| |
| let expected = FilterExpression { |
| identifier: identifier.clone(), |
| op: Operation::Comparison(ComparisonOperator::Equal, Value::Number(123)), |
| }; |
| assert_eq!(Ok(("", expected)), filter_expression(&format!("{} = 123", identifier_str))); |
| |
| let expected = FilterExpression { |
| identifier: identifier.clone(), |
| op: Operation::Comparison(ComparisonOperator::NotEq, Value::Number(123)), |
| }; |
| assert_eq!( |
| Ok(("", expected)), |
| filter_expression(&format!("{} != 123", identifier_str)) |
| ); |
| |
| assert!(filter_expression(&format!("{} > 1", identifier_str)).is_err()); |
| assert!(filter_expression(&format!("{} < 2", identifier_str)).is_err()); |
| assert!(filter_expression(&format!("{} >= 3", identifier_str)).is_err()); |
| assert!(filter_expression(&format!("{} <= 4", identifier_str)).is_err()); |
| assert!(filter_expression(&format!("{} has any [5, 6]", identifier_str)).is_err()); |
| assert!(filter_expression(&format!("{} has all [5, 6]", identifier_str)).is_err()); |
| } |
| } |
| |
| #[fuchsia::test] |
| fn tags_operations() { |
| for (operator, operator_str) in |
| vec![(InclusionOperator::HasAny, "has any"), (InclusionOperator::HasAll, "has all")] |
| { |
| let expected = FilterExpression { |
| identifier: Identifier::Tags, |
| op: Operation::Inclusion( |
| operator, |
| vec![Value::StringLiteral("a"), Value::StringLiteral("b")], |
| ), |
| }; |
| assert_eq!( |
| Ok(("", expected)), |
| filter_expression(&format!("tags {} [\"a\", \"b\"]", operator_str)) |
| ); |
| } |
| |
| assert!(filter_expression("tags > \"a\"").is_err()); |
| assert!(filter_expression("tags < \"b\"").is_err()); |
| assert!(filter_expression("tags >= \"c\"").is_err()); |
| assert!(filter_expression("tags <= \"d\"").is_err()); |
| assert!(filter_expression("tags = \"e\"").is_err()); |
| assert!(filter_expression("tags != \"f\"").is_err()); |
| assert!(filter_expression("tags in [\"g\", \"h\"]").is_err()); |
| } |
| |
| #[fuchsia::test] |
| fn timestamp_operations() { |
| for (operator, operator_str) in vec![ |
| (ComparisonOperator::Equal, "="), |
| (ComparisonOperator::GreaterEq, ">="), |
| (ComparisonOperator::Greater, ">"), |
| (ComparisonOperator::LessEq, "<="), |
| (ComparisonOperator::Less, "<"), |
| (ComparisonOperator::NotEq, "!="), |
| ] { |
| let expected = FilterExpression { |
| identifier: Identifier::Timestamp, |
| op: Operation::Comparison(operator, Value::Number(123)), |
| }; |
| assert_eq!( |
| Ok(("", expected)), |
| filter_expression(&format!("timestamp {} 123", operator_str)) |
| ); |
| } |
| assert!(filter_expression("timestamp in [1, 2]").is_err()); |
| assert!(filter_expression("timestamp has any [3, 4]").is_err()); |
| assert!(filter_expression("timestamp has all [5, 6]").is_err()); |
| } |
| |
| #[fuchsia::test] |
| fn severity_operations() { |
| for (operator, operator_str) in vec![ |
| (ComparisonOperator::Equal, "="), |
| (ComparisonOperator::GreaterEq, ">="), |
| (ComparisonOperator::Greater, ">"), |
| (ComparisonOperator::LessEq, "<="), |
| (ComparisonOperator::Less, "<"), |
| (ComparisonOperator::NotEq, "!="), |
| ] { |
| let expected = FilterExpression { |
| identifier: Identifier::Severity, |
| op: Operation::Comparison(operator, Value::Severity(Severity::Info)), |
| }; |
| assert_eq!( |
| Ok(("", expected)), |
| filter_expression(&format!("severity {} info", operator_str)) |
| ); |
| } |
| |
| let expected = FilterExpression { |
| identifier: Identifier::Severity, |
| op: Operation::Inclusion( |
| InclusionOperator::In, |
| vec![Value::Severity(Severity::Info), Value::Severity(Severity::Error)], |
| ), |
| }; |
| assert_eq!(Ok(("", expected)), filter_expression("severity in [info, error]")); |
| |
| assert!(filter_expression("severity has any [info, error]").is_err()); |
| assert!(filter_expression("severity has all [warn]").is_err()); |
| } |
| |
| #[fuchsia::test] |
| fn allowed_severity_types() { |
| let expected = FilterExpression { |
| identifier: Identifier::Severity, |
| op: Operation::Comparison(ComparisonOperator::Equal, Value::Severity(Severity::Info)), |
| }; |
| assert_eq!(Ok(("", expected)), filter_expression("severity = info")); |
| assert!(filter_expression("severity = 2").is_err()); |
| assert!(filter_expression("severity = \"info\"").is_err()); |
| } |
| |
| #[fuchsia::test] |
| fn allowed_numeric_identifiers() { |
| for (identifier, name) in vec![ |
| (Identifier::Pid, "pid"), |
| (Identifier::Tid, "tid"), |
| (Identifier::LineNumber, "line_number"), |
| (Identifier::Timestamp, "timestamp"), |
| ] { |
| let expected = FilterExpression { |
| identifier, |
| op: Operation::Comparison(ComparisonOperator::Equal, Value::Number(42)), |
| }; |
| assert_eq!(Ok(("", expected)), filter_expression(&format!("{} = 42", name))); |
| assert!(filter_expression(&format!("{} = info", name)).is_err()); |
| assert!(filter_expression(&format!("{} = \"42\"", name)).is_err()); |
| } |
| } |
| |
| #[fuchsia::test] |
| fn allowed_string_identifiers() { |
| for (identifier, name) in vec![ |
| (Identifier::Filename, "filename"), |
| (Identifier::LifecycleEventType, "lifecycle_event_type"), |
| ] { |
| let expected = FilterExpression { |
| identifier, |
| op: Operation::Comparison(ComparisonOperator::Equal, Value::StringLiteral("foo")), |
| }; |
| assert_eq!(Ok(("", expected)), filter_expression(&format!("{} = \"foo\"", name))); |
| assert!(filter_expression(&format!("{} = info", name)).is_err()); |
| assert!(filter_expression(&format!("{} = 42", name)).is_err()); |
| } |
| |
| let expected = FilterExpression { |
| identifier: Identifier::Tags, |
| op: Operation::Inclusion( |
| InclusionOperator::HasAny, |
| vec![Value::StringLiteral("a"), Value::StringLiteral("b")], |
| ), |
| }; |
| assert_eq!(Ok(("", expected)), filter_expression("tags has any [\"a\", \"b\"]")); |
| assert!(filter_expression("tags has any [info, error]").is_err()); |
| assert!(filter_expression("tags has any [2, 3]").is_err()); |
| } |
| |
| #[fuchsia::test] |
| fn parse_metadata_selector() { |
| let expected = MetadataSelector::new(vec![ |
| FilterExpression { |
| identifier: Identifier::LineNumber, |
| op: Operation::Comparison(ComparisonOperator::Equal, Value::Number(10)), |
| }, |
| FilterExpression { |
| identifier: Identifier::Filename, |
| op: Operation::Inclusion( |
| InclusionOperator::In, |
| vec![Value::StringLiteral("foo.rs")], |
| ), |
| }, |
| FilterExpression { |
| identifier: Identifier::Severity, |
| op: Operation::Comparison( |
| ComparisonOperator::LessEq, |
| Value::Severity(Severity::Error), |
| ), |
| }, |
| FilterExpression { |
| identifier: Identifier::Tags, |
| op: Operation::Inclusion( |
| InclusionOperator::HasAll, |
| vec![Value::StringLiteral("foo"), Value::StringLiteral("bar")], |
| ), |
| }, |
| ]); |
| assert_eq!( |
| Ok(("", expected)), |
| metadata_selector( |
| "where line_number = 10, filename in [\"foo.rs\"],\ |
| severity <= ERROR, tags HAS ALL [\"foo\", \"bar\"]" |
| ) |
| ); |
| |
| // Requires >= 1 filters. |
| let expected = MetadataSelector::new(vec![FilterExpression { |
| identifier: Identifier::Timestamp, |
| op: Operation::Comparison(ComparisonOperator::Greater, Value::Number(123)), |
| }]); |
| assert_eq!(Ok(("", expected)), metadata_selector("WHERE timestamp > 123")); |
| assert!(metadata_selector("where").is_err()); |
| } |
| } |