[inspect][selectors] Support both quotes and escapes for whitespace

Change-Id: I5155eaed65ad78ce5d2bf165c84b002626bc340c
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/1157872
Reviewed-by: Miguel Flores <miguelfrde@google.com>
Commit-Queue: Auto-Submit <auto-submit@fuchsia-infra.iam.gserviceaccount.com>
Fuchsia-Auto-Submit: Clayton McCray <claytonmccray@google.com>
diff --git a/src/lib/diagnostics/selectors/BUILD.gn b/src/lib/diagnostics/selectors/BUILD.gn
index cef6212..71c1938 100644
--- a/src/lib/diagnostics/selectors/BUILD.gn
+++ b/src/lib/diagnostics/selectors/BUILD.gn
@@ -14,6 +14,7 @@
     "//sdk/fidl/fuchsia.diagnostics:fuchsia.diagnostics_rust",
     "//src/sys/lib/moniker",
     "//third_party/rust_crates:anyhow",
+    "//third_party/rust_crates:bitflags",
     "//third_party/rust_crates:nom",
     "//third_party/rust_crates:thiserror",
   ]
diff --git a/src/lib/diagnostics/selectors/src/parser.rs b/src/lib/diagnostics/selectors/src/parser.rs
index 412d9fb..0f470c8 100644
--- a/src/lib/diagnostics/selectors/src/parser.rs
+++ b/src/lib/diagnostics/selectors/src/parser.rs
@@ -5,6 +5,7 @@
 use crate::error::ParseError;
 use crate::types::*;
 use crate::validate::{ValidateComponentSelectorExt, ValidateExt, ValidateTreeSelectorExt};
+use bitflags::bitflags;
 use nom::branch::alt;
 use nom::bytes::complete::{escaped, is_not, tag, take_while};
 use nom::character::complete::{alphanumeric1, multispace0, none_of, one_of};
@@ -16,11 +17,12 @@
 
 const ALL_TREE_NAMES_SELECTED_SYMBOL: &str = "...";
 
-#[derive(Default)]
-pub enum RequireEscapedColons {
-    #[default]
-    Yes,
-    No,
+bitflags! {
+    pub struct RequireEscaped: u8 {
+        const NONE = 0;
+        const COLONS = 1;
+        const WHITESPACE = 2;
+    }
 }
 
 /// Recognizes 0 or more spaces or tabs.
@@ -101,60 +103,73 @@
     result
 }
 
-/// Parses a tree selector, which is a node selector and an optional property selector.
-pub fn tree_selector<'a, E>(input: &'a str) -> IResult<&'a str, TreeSelector<'a>, E>
+/// Returns the parser for a tree selector, which is a node selector and an optional property selector.
+fn tree_selector<'a, E>(
+    // this can't be determined from the input to the resulting parser, because this function is
+    // used on both whole selector strings and strings that are only tree selectors
+    required_escapes: RequireEscaped,
+) -> impl FnMut(&'a str) -> IResult<&'a str, TreeSelector<'a>, E>
 where
     E: NomParseError<&'a str>,
 {
-    let mut esc = escaped(none_of(":/\\ \t\n"), '\\', one_of("* \t/:\\"));
+    move |input| {
+        let mut esc = if required_escapes.intersects(RequireEscaped::WHITESPACE) {
+            escaped(none_of(":/\\ \t\n"), '\\', one_of("* \t/:\\"))
+        } else {
+            escaped(none_of(":/\\\t\n"), '\\', one_of("* \t/:\\"))
+        };
 
-    let (rest, unparsed_name_list) = extract_conjoined_names::<E>(input).unwrap_or((input, None));
+        let (rest, unparsed_name_list) =
+            extract_conjoined_names::<E>(input).unwrap_or((input, None));
 
-    let tree_names = if unparsed_name_list == Some(ALL_TREE_NAMES_SELECTED_SYMBOL) {
-        Some(TreeNames::All)
-    } else {
-        // because of strict requirements around using quotation marks and the context of
-        // list brackets, not that much stuff needs to be escaped. Note that `"` is both
-        // an allowed normal character and an escaped character because at the time this
-        // parser is applied, wrapper quotes have not been stripped
-        let mut name_escapes = escaped(none_of(r#"*\"#), '\\', one_of(r#""*"#));
-        unparsed_name_list
-            .map(|names: &str| {
-                parse_quote_sensitive_separator(names, ',')
-                    .into_iter()
-                    .map(|name| {
-                        let (_, (_, value)) =
-                            spaced(separated_pair(tag("name"), tag("="), &mut name_escapes))(name)?;
-                        Ok(extract_from_quotes(value))
-                    })
-                    .collect::<Result<Vec<_>, _>>()
-            })
-            .transpose()?
-            .map(|value| value.into())
-    };
+        let tree_names = if unparsed_name_list == Some(ALL_TREE_NAMES_SELECTED_SYMBOL) {
+            Some(TreeNames::All)
+        } else {
+            // because of strict requirements around using quotation marks and the context of
+            // list brackets, not that much stuff needs to be escaped. Note that `"` is both
+            // an allowed normal character and an escaped character because at the time this
+            // parser is applied, wrapper quotes have not been stripped
+            let mut name_escapes = escaped(none_of(r#"*\"#), '\\', one_of(r#""*"#));
+            unparsed_name_list
+                .map(|names: &str| {
+                    parse_quote_sensitive_separator(names, ',')
+                        .into_iter()
+                        .map(|name| {
+                            let (_, (_, value)) =
+                                spaced(separated_pair(tag("name"), tag("="), &mut name_escapes))(
+                                    name,
+                                )?;
+                            Ok(extract_from_quotes(value))
+                        })
+                        .collect::<Result<Vec<_>, _>>()
+                })
+                .transpose()?
+                .map(|value| value.into())
+        };
 
-    let (rest, node_segments) = separated_list1(tag("/"), &mut esc)(rest)?;
-    let (rest, property_segment) = if peek::<&str, _, E, _>(tag(":"))(rest).is_ok() {
-        let (rest, _) = tag(":")(rest)?;
-        let (rest, property) = verify(esc, |value: &str| !value.is_empty())(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()),
-            tree_names,
-        },
-    ))
+        let (rest, node_segments) = separated_list1(tag("/"), &mut esc)(rest)?;
+        let (rest, property_segment) = if peek::<&str, _, E, _>(tag(":"))(rest).is_ok() {
+            let (rest, _) = tag(":")(rest)?;
+            let (rest, property) = verify(esc, |value: &str| !value.is_empty())(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()),
+                tree_names,
+            },
+        ))
+    }
 }
 
 /// Returns the parser for a component selector. The parser accepts unescaped depending on the
 /// the argument `escape_colons`.
 fn component_selector<'a, E>(
-    require_escape_colons: RequireEscapedColons,
+    required_escapes: RequireEscaped,
 ) -> impl FnMut(&'a str) -> IResult<&'a str, ComponentSelector<'a>, E>
 where
     E: NomParseError<&'a str>,
@@ -177,7 +192,7 @@
     }
 
     move |input| {
-        if matches!(require_escape_colons, RequireEscapedColons::Yes) {
+        if required_escapes.intersects(RequireEscaped::COLONS) {
             inner_component_selector(
                 recognize(escaped(
                     alt((
@@ -234,8 +249,13 @@
 where
     E: NomParseError<&'a str>,
 {
-    let (rest, (component, _, tree)) =
-        tuple((component_selector(RequireEscapedColons::Yes), tag(":"), tree_selector))(input)?;
+    let required_tree_escape =
+        if input.starts_with('"') { RequireEscaped::empty() } else { RequireEscaped::WHITESPACE };
+    let (rest, (component, _, tree)) = tuple((
+        component_selector(RequireEscaped::COLONS),
+        tag(":"),
+        tree_selector(required_tree_escape),
+    ))(extract_from_quotes(input))?;
     Ok((rest, (component, tree)))
 }
 
@@ -308,15 +328,17 @@
     }
 }
 
-/// Parses the input into a `ComponentSelector` ignoring any whitespace around the component
+/// Parses the input into a `TreeSelector` ignoring any whitespace around the component
 /// selector.
-pub fn consuming_tree_selector<'a, E>(input: &'a str) -> Result<TreeSelector<'a>, ParseError>
+pub fn standalone_tree_selector<'a, E>(input: &'a str) -> Result<TreeSelector<'a>, ParseError>
 where
     E: ParsingError<'a>,
 {
+    let required_tree_escape =
+        if input.starts_with('"') { RequireEscaped::empty() } else { RequireEscaped::WHITESPACE };
     let result = nom::combinator::all_consuming::<_, _, <E as ParsingError<'_>>::Internal, _>(
-        pair(spaced(tree_selector), multispace0),
-    )(input);
+        pair(spaced(tree_selector(required_tree_escape)), multispace0),
+    )(extract_from_quotes(input));
     match result {
         Ok((_, (tree_selector, _))) => {
             tree_selector.validate()?;
@@ -331,13 +353,13 @@
 /// selector.
 pub fn consuming_component_selector<'a, E>(
     input: &'a str,
-    require_escape_colons: RequireEscapedColons,
+    required_escapes: RequireEscaped,
 ) -> Result<ComponentSelector<'a>, ParseError>
 where
     E: ParsingError<'a>,
 {
     let result = nom::combinator::all_consuming::<_, _, <E as ParsingError<'_>>::Internal, _>(
-        pair(spaced(component_selector(require_escape_colons)), multispace0),
+        pair(spaced(component_selector(required_escapes)), multispace0),
     )(input);
     match result {
         Ok((_, (component_selector, _))) => {
@@ -438,7 +460,7 @@
 
         for (test_string, expected_segments) in test_vector {
             let (_, selector) = component_selector::<nom::error::VerboseError<&str>>(
-                RequireEscapedColons::Yes,
+                RequireEscaped::COLONS,
             )(test_string)
             .unwrap();
             assert_eq!(
@@ -450,7 +472,7 @@
             // Component selectors can start with `/`
             let test_moniker_string = format!("/{test_string}");
             let (_, selector) = component_selector::<nom::error::VerboseError<&str>>(
-                RequireEscapedColons::Yes,
+                RequireEscaped::COLONS,
             )(&test_moniker_string)
             .unwrap();
             assert_eq!(
@@ -462,7 +484,7 @@
             // Component selectors can start with `./`
             let test_moniker_string = format!("./{test_string}");
             let (_, selector) = component_selector::<nom::error::VerboseError<&str>>(
-                RequireEscapedColons::Yes,
+                RequireEscaped::COLONS,
             )(&test_moniker_string)
             .unwrap();
             assert_eq!(
@@ -474,7 +496,7 @@
             // We can also accept component selectors without escaping
             let test_moniker_string = test_string.replace("\\:", ":");
             let (_, selector) = component_selector::<nom::error::VerboseError<&str>>(
-                RequireEscapedColons::No,
+                RequireEscaped::empty(),
             )(&test_moniker_string)
             .unwrap();
             assert_eq!(
@@ -489,7 +511,7 @@
     fn missing_path_component_selector_test() {
         let component_selector_string = "c";
         let (_, component_selector) = component_selector::<nom::error::VerboseError<&str>>(
-            RequireEscapedColons::Yes,
+            RequireEscaped::COLONS,
         )(component_selector_string)
         .unwrap();
         let mut path_vec = component_selector.segments;
@@ -516,10 +538,8 @@
             "a$c/d",
         ];
         for test_string in test_vector {
-            let component_selector_result = consuming_component_selector::<VerboseError>(
-                test_string,
-                RequireEscapedColons::Yes,
-            );
+            let component_selector_result =
+                consuming_component_selector::<VerboseError>(test_string, RequireEscaped::COLONS);
             assert!(component_selector_result.is_err(), "expected '{}' to fail", test_string);
         }
     }
@@ -588,6 +608,48 @@
                 Some(vec!["a", "a/*:a"].into()),
             ),
             (
+                r#""a 1/b:d""#,
+                vec![Segment::ExactMatch("a 1".into()), Segment::ExactMatch("b".into())],
+                Some(Segment::ExactMatch("d".into())),
+                None,
+            ),
+            (
+                r#""a 1/b 2:d""#,
+                vec![Segment::ExactMatch("a 1".into()), Segment::ExactMatch("b 2".into())],
+                Some(Segment::ExactMatch("d".into())),
+                None,
+            ),
+            (
+                r#""a 1/b 2:d 3""#,
+                vec![Segment::ExactMatch("a 1".into()), Segment::ExactMatch("b 2".into())],
+                Some(Segment::ExactMatch("d 3".into())),
+                None,
+            ),
+            (
+                r#"a\ 1/b:d"#,
+                vec![Segment::ExactMatch("a 1".into()), Segment::ExactMatch("b".into())],
+                Some(Segment::ExactMatch("d".into())),
+                None,
+            ),
+            (
+                r#"a\ 1/b\ 2:d"#,
+                vec![Segment::ExactMatch("a 1".into()), Segment::ExactMatch("b 2".into())],
+                Some(Segment::ExactMatch("d".into())),
+                None,
+            ),
+            (
+                r#"a\ 1/b\ 2:d\ 3"#,
+                vec![Segment::ExactMatch("a 1".into()), Segment::ExactMatch("b 2".into())],
+                Some(Segment::ExactMatch("d 3".into())),
+                None,
+            ),
+            (
+                r#""a\ 1/b\ 2:d\ 3""#,
+                vec![Segment::ExactMatch("a 1".into()), Segment::ExactMatch("b 2".into())],
+                Some(Segment::ExactMatch("d 3".into())),
+                None,
+            ),
+            (
                 "a/b:c",
                 vec![Segment::ExactMatch("a".into()), Segment::ExactMatch("b".into())],
                 Some(Segment::ExactMatch("c".into())),
@@ -620,7 +682,7 @@
         ];
 
         for (string, expected_path, expected_property, expected_tree_name) in test_vector {
-            let (_, tree_selector) = tree_selector::<nom::error::VerboseError<&str>>(string)
+            let tree_selector = standalone_tree_selector::<VerboseError>(string)
                 .unwrap_or_else(|e| panic!("input: |{string}| error: {e}"));
             assert_eq!(
                 tree_selector,
@@ -691,19 +753,18 @@
         ];
         for (string, node, property) in with_spaces {
             assert_eq!(
-                all_consuming::<_, _, nom::error::VerboseError<&str>, _>(tree_selector)(string)
-                    .unwrap_or_else(|e| panic!("all_consuming |{string}| failed: {e}"))
-                    .1,
+                all_consuming::<_, _, nom::error::VerboseError<&str>, _>(tree_selector(
+                    RequireEscaped::WHITESPACE
+                ))(string)
+                .unwrap_or_else(|e| panic!("all_consuming |{string}| failed: {e}"))
+                .1,
                 TreeSelector { node, property, tree_names: None },
                 "input: |{string}|",
             );
         }
 
         // Un-escaped quotes aren't accepted when parsing with spaces.
-        assert!(all_consuming::<_, _, nom::error::VerboseError<&str>, _>(tree_selector)(
-            r#"a/b:"xc"/d"#
-        )
-        .is_err());
+        assert!(standalone_tree_selector::<VerboseError>(r#"a/b:"xc"/d"#).is_err());
     }
 
     #[fuchsia::test]
@@ -834,24 +895,27 @@
 
     #[fuchsia::test]
     fn parse_full_selector_with_spaces() {
+        let expected_regardless_of_escape_or_quote = Selector {
+            component: ComponentSelector {
+                segments: vec![
+                    Segment::ExactMatch("core".into()),
+                    Segment::ExactMatch("foo".into()),
+                ],
+            },
+            tree: TreeSelector {
+                node: vec![Segment::ExactMatch("some node".into()), Segment::Pattern("*".into())],
+                property: Some(Segment::ExactMatch("prop".into())),
+                tree_names: None,
+            },
+        };
         assert_eq!(
             selector::<VerboseError>(r#"core/foo:some\ node/*:prop"#).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("*".into()),
-                    ],
-                    property: Some(Segment::ExactMatch("prop".into())),
-                    tree_names: None,
-                },
-            }
+            expected_regardless_of_escape_or_quote,
+        );
+
+        assert_eq!(
+            selector::<VerboseError>(r#""core/foo:some node/*:prop""#).unwrap(),
+            expected_regardless_of_escape_or_quote,
         );
     }
 
diff --git a/src/lib/diagnostics/selectors/src/selectors.rs b/src/lib/diagnostics/selectors/src/selectors.rs
index 19d6975..62b6182 100644
--- a/src/lib/diagnostics/selectors/src/selectors.rs
+++ b/src/lib/diagnostics/selectors/src/selectors.rs
@@ -3,7 +3,7 @@
 // found in the LICENSE file.
 
 use crate::error::*;
-use crate::parser::{self, ParsingError, RequireEscapedColons, VerboseError};
+use crate::parser::{self, ParsingError, RequireEscaped, VerboseError};
 use crate::validate::*;
 use anyhow::format_err;
 use fidl_fuchsia_diagnostics::{
@@ -72,7 +72,7 @@
 where
     E: ParsingError<'a>,
 {
-    let result = parser::consuming_tree_selector::<E>(unparsed_tree_selector)?;
+    let result = parser::standalone_tree_selector::<E>(unparsed_tree_selector)?;
     Ok(result.into())
 }
 
@@ -85,7 +85,7 @@
 {
     let result = parser::consuming_component_selector::<E>(
         unparsed_component_selector,
-        RequireEscapedColons::Yes,
+        RequireEscaped::COLONS,
     )?;
     Ok(result.into())
 }
@@ -98,7 +98,7 @@
 {
     let result = parser::consuming_component_selector::<E>(
         unparsed_component_selector,
-        RequireEscapedColons::No,
+        RequireEscaped::empty(),
     )?;
     Ok(result.into())
 }