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

Commit c910aa0e authored by Mårten Kongstad's avatar Mårten Kongstad Committed by Automerger Merge Worker
Browse files

Merge changes from topic "aconfig-dump-format-and-filter-args" into main am: 9c80abc3

parents d095d94f 9c80abc3
Loading
Loading
Loading
Loading
+24 −87
Original line number Diff line number Diff line
@@ -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, DumpPredicate};
use crate::protos::{
    ParsedFlagExt, ProtoFlagMetadata, ProtoFlagPermission, ProtoFlagState, ProtoParsedFlag,
    ProtoParsedFlags, ProtoTracepoint,
};
use crate::storage::generate_storage_files;

pub struct Input {
    pub source: String,
@@ -279,73 +279,28 @@ pub fn create_device_config_sysprops(mut input: Input) -> Result<Vec<u8>> {
    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<Input>,
    format: DumpFormat,
    filters: &[&str],
    dedup: bool,
) -> Result<Vec<u8>> {
    let individually_parsed_flags: Result<Vec<ProtoParsedFlags>> =
        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)
    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<_>>>()?
    };
    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> {
@@ -622,43 +577,25 @@ 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::<ProtoParsedFlags>(
            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]
    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());
    }
+414 −0
Original line number Diff line number Diff line
/*
 * 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, ProtoFlagPermission, ProtoFlagState, ProtoTracepoint,
};
use crate::protos::{ProtoParsedFlag, ProtoParsedFlags};
use anyhow::{anyhow, bail, Context, 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<Self, Self::Error> {
        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<I>(parsed_flags_iter: I, format: DumpFormat) -> Result<Vec<u8>>
where
    I: Iterator<Item = ProtoParsedFlag>,
{
    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<u8>) {
    fn format_trace(trace: &[ProtoTracepoint]) -> String {
        trace
            .iter()
            .map(|tracepoint| {
                format!(
                    "{}: {:?} + {:?}",
                    tracepoint.source(),
                    tracepoint.permission(),
                    tracepoint.state()
                )
            })
            .collect::<Vec<_>>()
            .join(", ")
    }

    fn format_trace_paths(trace: &[ProtoTracepoint]) -> String {
        trace.iter().map(|tracepoint| tracepoint.source()).collect::<Vec<_>>().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());
}

pub type DumpPredicate = dyn Fn(&ProtoParsedFlag) -> bool;

pub fn create_filter_predicate(filter: &str) -> Result<Box<DumpPredicate>> {
    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)]
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::<ProtoParsedFlags>(
            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");
    }

    #[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",
            ]
        );
    }
}
+17 −3
Original line number Diff line number Diff line
@@ -26,13 +26,21 @@ 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};

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")
@@ -103,9 +111,10 @@ fn cli() -> Command {
                .arg(
                    Arg::new("format")
                        .long("format")
                        .value_parser(EnumValueParser::<commands::DumpFormat>::new())
                        .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("-")),
        )
@@ -249,8 +258,13 @@ fn main() -> Result<()> {
            let input = open_zero_or_more_files(sub_matches, "cache")?;
            let format = get_required_arg::<DumpFormat>(sub_matches, "format")
                .context("failed to dump previously parsed flags")?;
            let filters = sub_matches
                .get_many::<String>("filter")
                .unwrap_or_default()
                .map(String::as_ref)
                .collect::<Vec<_>>();
            let dedup = get_required_arg::<bool>(sub_matches, "dedup")?;
            let output = commands::dump_parsed_flags(input, *format, *dedup)?;
            let output = commands::dump_parsed_flags(input, format.clone(), &filters, *dedup)?;
            let path = get_required_arg::<String>(sub_matches, "out")?;
            write_output_to_file_or_stdout(path, &output)?;
        }