From ee58f98cc0333be170c1c70f7b22eb61fcfd88cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Kongstad?= Date: Fri, 15 Dec 2023 08:31:51 +0100 Subject: [PATCH 1/3] aconfig: support custom `dump format` specs Teach `dump --format=` to format the output according to a user-defined format string. The format string now accepts these arguments: - "protobuf": output all data as binary protobuf (as before) - "textproto": output all data as text protobuf (as before) - any other string: format according to the format spec, see below Custom format spec: placeholders, enclosed in { and } and named after the fields of ProtoParsedFlag, will be replaced by the actual values. All other text is output verbatim. As an example: - "{name}={state}" -> "enabled_ro=ENABLED" Some fields support an alternative formatting via {:}. As an example: - "{name}={state:bool}" -> "enabled_ro=true" Note that the text replacement does not support escaping { and }. This means there is no way to print the string "{name}" without expanding it to the actual flag's name. If needed this feature can be introduced in a later CL. For backwards compatibility, the following format strings have special meaning and will produce an output identically to what it was before this change: - "text" - "verbose" - "bool" A follow-up CL will add a new `dump --filter=` argument to limit which parsed flags are included in the output. Test: atest Bug: b/315487153 Change-Id: If7c14b5fb3e7b41ea962425078bd04b4996318f4 --- tools/aconfig/src/commands.rs | 95 ++------------ tools/aconfig/src/dump.rs | 240 ++++++++++++++++++++++++++++++++++ tools/aconfig/src/main.rs | 9 +- 3 files changed, 255 insertions(+), 89 deletions(-) create mode 100644 tools/aconfig/src/dump.rs diff --git a/tools/aconfig/src/commands.rs b/tools/aconfig/src/commands.rs index 50049d5b01..7ae1219820 100644 --- a/tools/aconfig/src/commands.rs +++ b/tools/aconfig/src/commands.rs @@ -23,12 +23,12 @@ use std::path::PathBuf; use crate::codegen::cpp::generate_cpp_code; use crate::codegen::java::generate_java_code; use crate::codegen::rust::generate_rust_code; -use crate::storage::generate_storage_files; - +use crate::dump::DumpFormat; use crate::protos::{ ParsedFlagExt, ProtoFlagMetadata, ProtoFlagPermission, ProtoFlagState, ProtoParsedFlag, ProtoParsedFlags, ProtoTracepoint, }; +use crate::storage::generate_storage_files; pub struct Input { pub source: String, @@ -279,15 +279,6 @@ pub fn create_device_config_sysprops(mut input: Input) -> Result> { Ok(output) } -#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)] -pub enum DumpFormat { - Text, - Verbose, - Protobuf, - Textproto, - Bool, -} - pub fn dump_parsed_flags( mut input: Vec, format: DumpFormat, @@ -297,55 +288,7 @@ pub fn dump_parsed_flags( input.iter_mut().map(|i| i.try_parse_flags()).collect(); let parsed_flags: ProtoParsedFlags = crate::protos::parsed_flags::merge(individually_parsed_flags?, dedup)?; - - let mut output = Vec::new(); - match format { - DumpFormat::Text => { - for parsed_flag in parsed_flags.parsed_flag.into_iter() { - let line = format!( - "{} [{}]: {:?} + {:?}\n", - parsed_flag.fully_qualified_name(), - parsed_flag.container(), - parsed_flag.permission(), - parsed_flag.state() - ); - output.extend_from_slice(line.as_bytes()); - } - } - DumpFormat::Verbose => { - for parsed_flag in parsed_flags.parsed_flag.into_iter() { - let sources: Vec<_> = - parsed_flag.trace.iter().map(|tracepoint| tracepoint.source()).collect(); - let line = format!( - "{} [{}]: {:?} + {:?} ({})\n", - parsed_flag.fully_qualified_name(), - parsed_flag.container(), - parsed_flag.permission(), - parsed_flag.state(), - sources.join(", ") - ); - output.extend_from_slice(line.as_bytes()); - } - } - DumpFormat::Protobuf => { - parsed_flags.write_to_vec(&mut output)?; - } - DumpFormat::Textproto => { - let s = protobuf::text_format::print_to_string_pretty(&parsed_flags); - output.extend_from_slice(s.as_bytes()); - } - DumpFormat::Bool => { - for parsed_flag in parsed_flags.parsed_flag.into_iter() { - let line = format!( - "{}={:?}\n", - parsed_flag.fully_qualified_name(), - parsed_flag.state() == ProtoFlagState::ENABLED - ); - output.extend_from_slice(line.as_bytes()); - } - } - } - Ok(output) + crate::dump::dump_parsed_flags(parsed_flags.parsed_flag.into_iter(), format) } fn find_unique_package(parsed_flags: &[ProtoParsedFlag]) -> Option<&str> { @@ -622,36 +565,16 @@ mod tests { } #[test] - fn test_dump_text_format() { + fn test_dump() { let input = parse_test_flags_as_input(); - let bytes = dump_parsed_flags(vec![input], DumpFormat::Text, false).unwrap(); - let text = std::str::from_utf8(&bytes).unwrap(); - assert!( - text.contains("com.android.aconfig.test.disabled_ro [system]: READ_ONLY + DISABLED") - ); - } - - #[test] - fn test_dump_protobuf_format() { - let expected = protobuf::text_format::parse_from_str::( - crate::test::TEST_FLAGS_TEXTPROTO, + let bytes = dump_parsed_flags( + vec![input], + DumpFormat::Custom("{fully_qualified_name}".to_string()), + false, ) - .unwrap() - .write_to_bytes() .unwrap(); - - let input = parse_test_flags_as_input(); - let actual = dump_parsed_flags(vec![input], DumpFormat::Protobuf, false).unwrap(); - - assert_eq!(expected, actual); - } - - #[test] - fn test_dump_textproto_format() { - let input = parse_test_flags_as_input(); - let bytes = dump_parsed_flags(vec![input], DumpFormat::Textproto, false).unwrap(); let text = std::str::from_utf8(&bytes).unwrap(); - assert_eq!(crate::test::TEST_FLAGS_TEXTPROTO.trim(), text.trim()); + assert!(text.contains("com.android.aconfig.test.disabled_ro")); } #[test] diff --git a/tools/aconfig/src/dump.rs b/tools/aconfig/src/dump.rs new file mode 100644 index 0000000000..410c39260f --- /dev/null +++ b/tools/aconfig/src/dump.rs @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use crate::protos::{ParsedFlagExt, ProtoFlagMetadata, ProtoFlagState, ProtoTracepoint}; +use crate::protos::{ProtoParsedFlag, ProtoParsedFlags}; +use anyhow::Result; +use protobuf::Message; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum DumpFormat { + Protobuf, + Textproto, + Custom(String), +} + +impl TryFrom<&str> for DumpFormat { + type Error = anyhow::Error; + + fn try_from(value: &str) -> std::result::Result { + match value { + // protobuf formats + "protobuf" => Ok(Self::Protobuf), + "textproto" => Ok(Self::Textproto), + + // old formats now implemented as aliases to custom format + "text" => Ok(Self::Custom( + "{fully_qualified_name} [{container}]: {permission} + {state}".to_owned(), + )), + "verbose" => Ok(Self::Custom( + "{fully_qualified_name} [{container}]: {permission} + {state} ({trace:paths})" + .to_owned(), + )), + "bool" => Ok(Self::Custom("{fully_qualified_name}={state:bool}".to_owned())), + + // custom format + _ => Ok(Self::Custom(value.to_owned())), + } + } +} + +pub fn dump_parsed_flags(parsed_flags_iter: I, format: DumpFormat) -> Result> +where + I: Iterator, +{ + let mut output = Vec::new(); + match format { + DumpFormat::Protobuf => { + let parsed_flags = + ProtoParsedFlags { parsed_flag: parsed_flags_iter.collect(), ..Default::default() }; + parsed_flags.write_to_vec(&mut output)?; + } + DumpFormat::Textproto => { + let parsed_flags = + ProtoParsedFlags { parsed_flag: parsed_flags_iter.collect(), ..Default::default() }; + let s = protobuf::text_format::print_to_string_pretty(&parsed_flags); + output.extend_from_slice(s.as_bytes()); + } + DumpFormat::Custom(format) => { + for flag in parsed_flags_iter { + dump_custom_format(&flag, &format, &mut output); + } + } + } + Ok(output) +} + +fn dump_custom_format(flag: &ProtoParsedFlag, format: &str, output: &mut Vec) { + fn format_trace(trace: &[ProtoTracepoint]) -> String { + trace + .iter() + .map(|tracepoint| { + format!( + "{}: {:?} + {:?}", + tracepoint.source(), + tracepoint.permission(), + tracepoint.state() + ) + }) + .collect::>() + .join(", ") + } + + fn format_trace_paths(trace: &[ProtoTracepoint]) -> String { + trace.iter().map(|tracepoint| tracepoint.source()).collect::>().join(", ") + } + + fn format_metadata(metadata: &ProtoFlagMetadata) -> String { + format!("{:?}", metadata.purpose()) + } + + let mut str = format + // ProtoParsedFlag fields + .replace("{package}", flag.package()) + .replace("{name}", flag.name()) + .replace("{namespace}", flag.namespace()) + .replace("{description}", flag.description()) + .replace("{bug}", &flag.bug.join(", ")) + .replace("{state}", &format!("{:?}", flag.state())) + .replace("{state:bool}", &format!("{}", flag.state() == ProtoFlagState::ENABLED)) + .replace("{permission}", &format!("{:?}", flag.permission())) + .replace("{trace}", &format_trace(&flag.trace)) + .replace("{trace:paths}", &format_trace_paths(&flag.trace)) + .replace("{is_fixed_read_only}", &format!("{}", flag.is_fixed_read_only())) + .replace("{is_exported}", &format!("{}", flag.is_exported())) + .replace("{container}", flag.container()) + .replace("{metadata}", &format_metadata(&flag.metadata)) + // ParsedFlagExt functions + .replace("{fully_qualified_name}", &flag.fully_qualified_name()); + str.push('\n'); + output.extend_from_slice(str.as_bytes()); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::protos::ProtoParsedFlags; + use crate::test::parse_test_flags; + use protobuf::Message; + + fn parse_enabled_ro_flag() -> ProtoParsedFlag { + parse_test_flags().parsed_flag.into_iter().find(|pf| pf.name() == "enabled_ro").unwrap() + } + + #[test] + fn test_dumpformat_from_str() { + // supported format types + assert_eq!(DumpFormat::try_from("protobuf").unwrap(), DumpFormat::Protobuf); + assert_eq!(DumpFormat::try_from("textproto").unwrap(), DumpFormat::Textproto); + assert_eq!( + DumpFormat::try_from("foobar").unwrap(), + DumpFormat::Custom("foobar".to_owned()) + ); + } + + #[test] + fn test_dump_parsed_flags_protobuf_format() { + let expected = protobuf::text_format::parse_from_str::( + crate::test::TEST_FLAGS_TEXTPROTO, + ) + .unwrap() + .write_to_bytes() + .unwrap(); + let parsed_flags = parse_test_flags(); + let actual = + dump_parsed_flags(parsed_flags.parsed_flag.into_iter(), DumpFormat::Protobuf).unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn test_dump_parsed_flags_textproto_format() { + let parsed_flags = parse_test_flags(); + let bytes = + dump_parsed_flags(parsed_flags.parsed_flag.into_iter(), DumpFormat::Textproto).unwrap(); + let text = std::str::from_utf8(&bytes).unwrap(); + assert_eq!(crate::test::TEST_FLAGS_TEXTPROTO.trim(), text.trim()); + } + + #[test] + fn test_dump_parsed_flags_custom_format() { + macro_rules! assert_dump_parsed_flags_custom_format_contains { + ($format:expr, $expected:expr) => { + let parsed_flags = parse_test_flags(); + let bytes = dump_parsed_flags( + parsed_flags.parsed_flag.into_iter(), + $format.try_into().unwrap(), + ) + .unwrap(); + let text = std::str::from_utf8(&bytes).unwrap(); + assert!(text.contains($expected)); + }; + } + + // custom format + assert_dump_parsed_flags_custom_format_contains!( + "{fully_qualified_name}={permission} + {state}", + "com.android.aconfig.test.enabled_ro=READ_ONLY + ENABLED" + ); + + // aliases + assert_dump_parsed_flags_custom_format_contains!( + "text", + "com.android.aconfig.test.enabled_ro [system]: READ_ONLY + ENABLED" + ); + assert_dump_parsed_flags_custom_format_contains!( + "verbose", + "com.android.aconfig.test.enabled_ro [system]: READ_ONLY + ENABLED (tests/test.aconfig, tests/first.values, tests/second.values)" + ); + assert_dump_parsed_flags_custom_format_contains!( + "bool", + "com.android.aconfig.test.enabled_ro=true" + ); + } + + #[test] + fn test_dump_custom_format() { + macro_rules! assert_custom_format { + ($format:expr, $expected:expr) => { + let flag = parse_enabled_ro_flag(); + let mut bytes = vec![]; + dump_custom_format(&flag, $format, &mut bytes); + let text = std::str::from_utf8(&bytes).unwrap(); + assert_eq!(text, $expected); + }; + } + + assert_custom_format!("{package}", "com.android.aconfig.test\n"); + assert_custom_format!("{name}", "enabled_ro\n"); + assert_custom_format!("{namespace}", "aconfig_test\n"); + assert_custom_format!("{description}", "This flag is ENABLED + READ_ONLY\n"); + assert_custom_format!("{bug}", "abc\n"); + assert_custom_format!("{state}", "ENABLED\n"); + assert_custom_format!("{state:bool}", "true\n"); + assert_custom_format!("{permission}", "READ_ONLY\n"); + assert_custom_format!("{trace}", "tests/test.aconfig: READ_WRITE + DISABLED, tests/first.values: READ_WRITE + DISABLED, tests/second.values: READ_ONLY + ENABLED\n"); + assert_custom_format!( + "{trace:paths}", + "tests/test.aconfig, tests/first.values, tests/second.values\n" + ); + assert_custom_format!("{is_fixed_read_only}", "false\n"); + assert_custom_format!("{is_exported}", "false\n"); + assert_custom_format!("{container}", "system\n"); + assert_custom_format!("{metadata}", "PURPOSE_BUGFIX\n"); + + assert_custom_format!("name={name}|state={state}", "name=enabled_ro|state=ENABLED\n"); + assert_custom_format!("{state}{state}{state}", "ENABLEDENABLEDENABLED\n"); + } +} diff --git a/tools/aconfig/src/main.rs b/tools/aconfig/src/main.rs index 63a50c86d2..08e8b97df0 100644 --- a/tools/aconfig/src/main.rs +++ b/tools/aconfig/src/main.rs @@ -26,13 +26,16 @@ use std::path::{Path, PathBuf}; mod codegen; mod commands; +mod dump; mod protos; mod storage; +use dump::DumpFormat; + #[cfg(test)] mod test; -use commands::{CodegenMode, DumpFormat, Input, OutputFile}; +use commands::{CodegenMode, Input, OutputFile}; fn cli() -> Command { Command::new("aconfig") @@ -103,7 +106,7 @@ fn cli() -> Command { .arg( Arg::new("format") .long("format") - .value_parser(EnumValueParser::::new()) + .value_parser(|s: &str| DumpFormat::try_from(s)) .default_value("text"), ) .arg(Arg::new("dedup").long("dedup").num_args(0).action(ArgAction::SetTrue)) @@ -250,7 +253,7 @@ fn main() -> Result<()> { let format = get_required_arg::(sub_matches, "format") .context("failed to dump previously parsed flags")?; let dedup = get_required_arg::(sub_matches, "dedup")?; - let output = commands::dump_parsed_flags(input, *format, *dedup)?; + let output = commands::dump_parsed_flags(input, format.clone(), *dedup)?; let path = get_required_arg::(sub_matches, "out")?; write_output_to_file_or_stdout(path, &output)?; } From 49e4d6e73dc00a383cfbe1a0048b26f7575211ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Kongstad?= Date: Fri, 15 Dec 2023 10:21:57 +0100 Subject: [PATCH 2/3] aconfig: dump --filter: hook up command line args to dump.rs This is the first step towards teaching dump to (optionally) filter which flags to print. A follow-up CL will implement dump::create_filter_predicate. Bug: 315487153 Test: atest aconfig.test Change-Id: Ibe0d4ce6563d3b5718fedd3ebfd45fbf5d935b92 --- tools/aconfig/src/commands.rs | 17 ++++++++++++++--- tools/aconfig/src/dump.rs | 7 +++++++ tools/aconfig/src/main.rs | 13 ++++++++++++- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/tools/aconfig/src/commands.rs b/tools/aconfig/src/commands.rs index 7ae1219820..d8213e0018 100644 --- a/tools/aconfig/src/commands.rs +++ b/tools/aconfig/src/commands.rs @@ -23,7 +23,7 @@ use std::path::PathBuf; use crate::codegen::cpp::generate_cpp_code; use crate::codegen::java::generate_java_code; use crate::codegen::rust::generate_rust_code; -use crate::dump::DumpFormat; +use crate::dump::{DumpFormat, DumpPredicate}; use crate::protos::{ ParsedFlagExt, ProtoFlagMetadata, ProtoFlagPermission, ProtoFlagState, ProtoParsedFlag, ProtoParsedFlags, ProtoTracepoint, @@ -282,13 +282,22 @@ pub fn create_device_config_sysprops(mut input: Input) -> Result> { pub fn dump_parsed_flags( mut input: Vec, format: DumpFormat, + filters: &[&str], dedup: bool, ) -> Result> { let individually_parsed_flags: Result> = input.iter_mut().map(|i| i.try_parse_flags()).collect(); let parsed_flags: ProtoParsedFlags = crate::protos::parsed_flags::merge(individually_parsed_flags?, dedup)?; - crate::dump::dump_parsed_flags(parsed_flags.parsed_flag.into_iter(), format) + let filters: Vec> = if filters.is_empty() { + vec![Box::new(|_| true)] + } else { + filters.iter().map(|f| crate::dump::create_filter_predicate(f)).collect::>>()? + }; + crate::dump::dump_parsed_flags( + parsed_flags.parsed_flag.into_iter().filter(|flag| filters.iter().any(|p| p(flag))), + format, + ) } fn find_unique_package(parsed_flags: &[ProtoParsedFlag]) -> Option<&str> { @@ -570,6 +579,7 @@ mod tests { let bytes = dump_parsed_flags( vec![input], DumpFormat::Custom("{fully_qualified_name}".to_string()), + &[], false, ) .unwrap(); @@ -581,7 +591,8 @@ mod tests { fn test_dump_textproto_format_dedup() { let input = parse_test_flags_as_input(); let input2 = parse_test_flags_as_input(); - let bytes = dump_parsed_flags(vec![input, input2], DumpFormat::Textproto, true).unwrap(); + let bytes = + dump_parsed_flags(vec![input, input2], DumpFormat::Textproto, &[], true).unwrap(); let text = std::str::from_utf8(&bytes).unwrap(); assert_eq!(crate::test::TEST_FLAGS_TEXTPROTO.trim(), text.trim()); } diff --git a/tools/aconfig/src/dump.rs b/tools/aconfig/src/dump.rs index 410c39260f..238046813a 100644 --- a/tools/aconfig/src/dump.rs +++ b/tools/aconfig/src/dump.rs @@ -123,6 +123,13 @@ fn dump_custom_format(flag: &ProtoParsedFlag, format: &str, output: &mut Vec output.extend_from_slice(str.as_bytes()); } +pub type DumpPredicate = dyn Fn(&ProtoParsedFlag) -> bool; + +#[allow(unused)] +pub fn create_filter_predicate(filter: &str) -> Result> { + todo!(); +} + #[cfg(test)] mod tests { use super::*; diff --git a/tools/aconfig/src/main.rs b/tools/aconfig/src/main.rs index 08e8b97df0..fcc5ea5085 100644 --- a/tools/aconfig/src/main.rs +++ b/tools/aconfig/src/main.rs @@ -37,6 +37,11 @@ mod test; use commands::{CodegenMode, Input, OutputFile}; +const HELP_DUMP_FILTER: &str = r#" +Limit which flags to output. If multiple --filter arguments are provided, the output will be +limited to flags that match any of the filters. +"#; + fn cli() -> Command { Command::new("aconfig") .subcommand_required(true) @@ -109,6 +114,7 @@ fn cli() -> Command { .value_parser(|s: &str| DumpFormat::try_from(s)) .default_value("text"), ) + .arg(Arg::new("filter").long("filter").action(ArgAction::Append).help(HELP_DUMP_FILTER.trim())) .arg(Arg::new("dedup").long("dedup").num_args(0).action(ArgAction::SetTrue)) .arg(Arg::new("out").long("out").default_value("-")), ) @@ -252,8 +258,13 @@ fn main() -> Result<()> { let input = open_zero_or_more_files(sub_matches, "cache")?; let format = get_required_arg::(sub_matches, "format") .context("failed to dump previously parsed flags")?; + let filters = sub_matches + .get_many::("filter") + .unwrap_or_default() + .map(String::as_ref) + .collect::>(); let dedup = get_required_arg::(sub_matches, "dedup")?; - let output = commands::dump_parsed_flags(input, format.clone(), *dedup)?; + let output = commands::dump_parsed_flags(input, format.clone(), &filters, *dedup)?; let path = get_required_arg::(sub_matches, "out")?; write_output_to_file_or_stdout(path, &output)?; } From 517ac4801b6476c26e87ed8943d01f57ae4e75f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Kongstad?= Date: Fri, 15 Dec 2023 12:29:52 +0100 Subject: [PATCH 3/3] aconfig: dump --filter: implement predicates The aconfig dump command can now limit which flags to print by passing in one or more --filter= commands. If multiple --filter arguments are provided, flags that match any filter will be included in the output. The syntax is :, where 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 --- tools/aconfig/src/commands.rs | 5 +- tools/aconfig/src/dump.rs | 175 +++++++++++++++++++++++++++++++++- 2 files changed, 175 insertions(+), 5 deletions(-) diff --git a/tools/aconfig/src/commands.rs b/tools/aconfig/src/commands.rs index d8213e0018..87905fda1b 100644 --- a/tools/aconfig/src/commands.rs +++ b/tools/aconfig/src/commands.rs @@ -292,7 +292,10 @@ pub fn dump_parsed_flags( let filters: Vec> = if filters.is_empty() { vec![Box::new(|_| true)] } else { - filters.iter().map(|f| crate::dump::create_filter_predicate(f)).collect::>>()? + filters + .iter() + .map(|f| crate::dump::create_filter_predicate(f)) + .collect::>>()? }; 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 238046813a..f2b368774c 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 @@ fn dump_custom_format(flag: &ProtoParsedFlag, format: &str, output: &mut Vec pub type DumpPredicate = dyn Fn(&ProtoParsedFlag) -> bool; -#[allow(unused)] pub fn create_filter_predicate(filter: &str) -> Result> { - todo!(); + let predicates = filter + .split('+') + .map(|sub_filter| create_filter_predicate_single(sub_filter)) + .collect::>>()?; + Ok(Box::new(move |flag| predicates.iter().all(|p| p(flag)))) +} + +fn create_filter_predicate_single(filter: &str) -> Result> { + fn enum_from_str(expected: &[T], s: &str) -> Result + 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::>().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 = 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", + ] + ); + } }