Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 517ac480 authored by Mårten Kongstad's avatar Mårten Kongstad
Browse files

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
parent 49e4d6e7
Loading
Loading
Loading
Loading
+4 −1
Original line number Diff line number Diff line
@@ -292,7 +292,10 @@ pub fn dump_parsed_flags(
    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))),
+171 −4
Original line number Diff line number Diff line
@@ -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 @@ fn dump_custom_format(flag: &ProtoParsedFlag, format: &str, output: &mut Vec<u8>

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 @@ mod tests {
        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",
            ]
        );
    }
}