aconfig: dump --filter: implement predicates

The aconfig dump command can now limit which flags to print by passing
in one or more --filter=<query> commands.

If multiple --filter arguments are provided, flags that match any filter
will be included in the output.

The <query> syntax is <what>:<value>, where <what> is the name of a
ProtoParsedFlag field. Multiple queries can be AND-ed together by
joining them with a plus ('+') character.

Example queries:

  - --filter='is_exported:true' # only show exported flags
  - --filter='permission:READ_ONLY+state:ENABLED' # only show flags that are read-only and enabled
  - --filter='permission:READ_ONLY' --filter='state:ENABLED' # only show flags that are read-only or enabled (or both)

Current limitations that may be addressed in future CLs:

  - No support to invert a query, e.g. "flags *not* in the following
    namespace"

  - No support to quote strings; this makes description matching
    difficult

  - No support to glob strings, only exact matches are considered

  - No support to filter by description, trace or metadata fields

Bug: 315487153
Test: atest aconfig.test
Test: printflags --format="{fully_qualified_name}={state}" --filter=permission:READ_ONLY # manually verify output
Change-Id: Ie1e40fa444cec429e336048439da955f30e22979
diff --git a/tools/aconfig/src/commands.rs b/tools/aconfig/src/commands.rs
index d8213e0..87905fd 100644
--- a/tools/aconfig/src/commands.rs
+++ b/tools/aconfig/src/commands.rs
@@ -292,7 +292,10 @@
     let filters: Vec<Box<DumpPredicate>> = if filters.is_empty() {
         vec![Box::new(|_| true)]
     } else {
-        filters.iter().map(|f| crate::dump::create_filter_predicate(f)).collect::<Result<Vec<_>>>()?
+        filters
+            .iter()
+            .map(|f| crate::dump::create_filter_predicate(f))
+            .collect::<Result<Vec<_>>>()?
     };
     crate::dump::dump_parsed_flags(
         parsed_flags.parsed_flag.into_iter().filter(|flag| filters.iter().any(|p| p(flag))),
diff --git a/tools/aconfig/src/dump.rs b/tools/aconfig/src/dump.rs
index 2380468..f2b3687 100644
--- a/tools/aconfig/src/dump.rs
+++ b/tools/aconfig/src/dump.rs
@@ -14,9 +14,11 @@
  * limitations under the License.
  */
 
-use crate::protos::{ParsedFlagExt, ProtoFlagMetadata, ProtoFlagState, ProtoTracepoint};
+use crate::protos::{
+    ParsedFlagExt, ProtoFlagMetadata, ProtoFlagPermission, ProtoFlagState, ProtoTracepoint,
+};
 use crate::protos::{ProtoParsedFlag, ProtoParsedFlags};
-use anyhow::Result;
+use anyhow::{anyhow, bail, Context, Result};
 use protobuf::Message;
 
 #[derive(Clone, Debug, PartialEq, Eq)]
@@ -125,9 +127,78 @@
 
 pub type DumpPredicate = dyn Fn(&ProtoParsedFlag) -> bool;
 
-#[allow(unused)]
 pub fn create_filter_predicate(filter: &str) -> Result<Box<DumpPredicate>> {
-    todo!();
+    let predicates = filter
+        .split('+')
+        .map(|sub_filter| create_filter_predicate_single(sub_filter))
+        .collect::<Result<Vec<_>>>()?;
+    Ok(Box::new(move |flag| predicates.iter().all(|p| p(flag))))
+}
+
+fn create_filter_predicate_single(filter: &str) -> Result<Box<DumpPredicate>> {
+    fn enum_from_str<T>(expected: &[T], s: &str) -> Result<T>
+    where
+        T: std::fmt::Debug + Copy,
+    {
+        for candidate in expected.iter() {
+            if s == format!("{:?}", candidate) {
+                return Ok(*candidate);
+            }
+        }
+        let expected =
+            expected.iter().map(|state| format!("{:?}", state)).collect::<Vec<_>>().join(", ");
+        bail!("\"{s}\": not a valid flag state, expected one of {expected}");
+    }
+
+    let error_msg = format!("\"{filter}\": filter syntax error");
+    let (what, arg) = filter.split_once(':').ok_or_else(|| anyhow!(error_msg.clone()))?;
+    match what {
+        "package" => {
+            let expected = arg.to_owned();
+            Ok(Box::new(move |flag: &ProtoParsedFlag| flag.package() == expected))
+        }
+        "name" => {
+            let expected = arg.to_owned();
+            Ok(Box::new(move |flag: &ProtoParsedFlag| flag.name() == expected))
+        }
+        "namespace" => {
+            let expected = arg.to_owned();
+            Ok(Box::new(move |flag: &ProtoParsedFlag| flag.namespace() == expected))
+        }
+        // description: not supported yet
+        "bug" => {
+            let expected = arg.to_owned();
+            Ok(Box::new(move |flag: &ProtoParsedFlag| flag.bug.join(", ") == expected))
+        }
+        "state" => {
+            let expected = enum_from_str(&[ProtoFlagState::ENABLED, ProtoFlagState::DISABLED], arg)
+                .context(error_msg)?;
+            Ok(Box::new(move |flag: &ProtoParsedFlag| flag.state() == expected))
+        }
+        "permission" => {
+            let expected = enum_from_str(
+                &[ProtoFlagPermission::READ_ONLY, ProtoFlagPermission::READ_WRITE],
+                arg,
+            )
+            .context(error_msg)?;
+            Ok(Box::new(move |flag: &ProtoParsedFlag| flag.permission() == expected))
+        }
+        // trace: not supported yet
+        "is_fixed_read_only" => {
+            let expected: bool = arg.parse().context(error_msg)?;
+            Ok(Box::new(move |flag: &ProtoParsedFlag| flag.is_fixed_read_only() == expected))
+        }
+        "is_exported" => {
+            let expected: bool = arg.parse().context(error_msg)?;
+            Ok(Box::new(move |flag: &ProtoParsedFlag| flag.is_exported() == expected))
+        }
+        "container" => {
+            let expected = arg.to_owned();
+            Ok(Box::new(move |flag: &ProtoParsedFlag| flag.container() == expected))
+        }
+        // metadata: not supported yet
+        _ => Err(anyhow!(error_msg)),
+    }
 }
 
 #[cfg(test)]
@@ -244,4 +315,100 @@
         assert_custom_format!("name={name}|state={state}", "name=enabled_ro|state=ENABLED\n");
         assert_custom_format!("{state}{state}{state}", "ENABLEDENABLEDENABLED\n");
     }
+
+    #[test]
+    fn test_create_filter_predicate() {
+        macro_rules! assert_create_filter_predicate {
+            ($filter:expr, $expected:expr) => {
+                let parsed_flags = parse_test_flags();
+                let predicate = create_filter_predicate($filter).unwrap();
+                let mut filtered_flags: Vec<String> = parsed_flags
+                    .parsed_flag
+                    .into_iter()
+                    .filter(predicate)
+                    .map(|flag| flag.fully_qualified_name())
+                    .collect();
+                filtered_flags.sort();
+                assert_eq!(&filtered_flags, $expected);
+            };
+        }
+
+        assert_create_filter_predicate!(
+            "package:com.android.aconfig.test",
+            &[
+                "com.android.aconfig.test.disabled_ro",
+                "com.android.aconfig.test.disabled_rw",
+                "com.android.aconfig.test.disabled_rw_exported",
+                "com.android.aconfig.test.disabled_rw_in_other_namespace",
+                "com.android.aconfig.test.enabled_fixed_ro",
+                "com.android.aconfig.test.enabled_ro",
+                "com.android.aconfig.test.enabled_ro_exported",
+                "com.android.aconfig.test.enabled_rw",
+            ]
+        );
+        assert_create_filter_predicate!(
+            "name:disabled_rw",
+            &["com.android.aconfig.test.disabled_rw"]
+        );
+        assert_create_filter_predicate!(
+            "namespace:other_namespace",
+            &["com.android.aconfig.test.disabled_rw_in_other_namespace"]
+        );
+        // description: not supported yet
+        assert_create_filter_predicate!("bug:123", &["com.android.aconfig.test.disabled_ro",]);
+        assert_create_filter_predicate!(
+            "state:ENABLED",
+            &[
+                "com.android.aconfig.test.enabled_fixed_ro",
+                "com.android.aconfig.test.enabled_ro",
+                "com.android.aconfig.test.enabled_ro_exported",
+                "com.android.aconfig.test.enabled_rw",
+            ]
+        );
+        assert_create_filter_predicate!(
+            "permission:READ_ONLY",
+            &[
+                "com.android.aconfig.test.disabled_ro",
+                "com.android.aconfig.test.enabled_fixed_ro",
+                "com.android.aconfig.test.enabled_ro",
+                "com.android.aconfig.test.enabled_ro_exported",
+            ]
+        );
+        // trace: not supported yet
+        assert_create_filter_predicate!(
+            "is_fixed_read_only:true",
+            &["com.android.aconfig.test.enabled_fixed_ro"]
+        );
+        assert_create_filter_predicate!(
+            "is_exported:true",
+            &[
+                "com.android.aconfig.test.disabled_rw_exported",
+                "com.android.aconfig.test.enabled_ro_exported",
+            ]
+        );
+        assert_create_filter_predicate!(
+            "container:system",
+            &[
+                "com.android.aconfig.test.disabled_ro",
+                "com.android.aconfig.test.disabled_rw",
+                "com.android.aconfig.test.disabled_rw_exported",
+                "com.android.aconfig.test.disabled_rw_in_other_namespace",
+                "com.android.aconfig.test.enabled_fixed_ro",
+                "com.android.aconfig.test.enabled_ro",
+                "com.android.aconfig.test.enabled_ro_exported",
+                "com.android.aconfig.test.enabled_rw",
+            ]
+        );
+        // metadata: not supported yet
+
+        // multiple sub filters
+        assert_create_filter_predicate!(
+            "permission:READ_ONLY+state:ENABLED",
+            &[
+                "com.android.aconfig.test.enabled_fixed_ro",
+                "com.android.aconfig.test.enabled_ro",
+                "com.android.aconfig.test.enabled_ro_exported",
+            ]
+        );
+    }
 }