aconfig: add create storage command

Add a new aconfig command called create-storage which takes a number
of aconfig cache files that belong to a specific container and produces
storage files.

Add a new module called storage (src/storage/mod.rs) as the entry point
of storage files generation. FlagPackage struct is defined as an
intermediate data structure that will be used to drive all storage files creation.

Add a unit test to lock down FlagPackage creation behaviors.

Bug: b/312243587
Test: atest aconfig.test

Change-Id: Ia7e9f68237ea903f295ac7891c923f6a39f3422d
diff --git a/tools/aconfig/src/commands.rs b/tools/aconfig/src/commands.rs
index be32bde..2a3fe27 100644
--- a/tools/aconfig/src/commands.rs
+++ b/tools/aconfig/src/commands.rs
@@ -23,6 +23,8 @@
 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::protos::{
     ParsedFlagExt, ProtoFlagMetadata, ProtoFlagPermission, ProtoFlagState, ProtoParsedFlag,
     ProtoParsedFlags, ProtoTracepoint,
@@ -217,6 +219,17 @@
     generate_rust_code(package, parsed_flags.parsed_flag.iter(), codegen_mode)
 }
 
+pub fn create_storage(caches: Vec<Input>, container: &str) -> Result<Vec<OutputFile>> {
+    let parsed_flags_vec: Vec<ProtoParsedFlags> = caches
+        .into_iter()
+        .map(|mut input| input.try_parse_flags())
+        .collect::<Result<Vec<_>>>()?
+        .into_iter()
+        .filter(|pfs| find_unique_container(pfs) == Some(container))
+        .collect();
+    generate_storage_files(container, parsed_flags_vec.iter())
+}
+
 pub fn create_device_config_defaults(mut input: Input) -> Result<Vec<u8>> {
     let parsed_flags = input.try_parse_flags()?;
     let mut output = Vec::new();
@@ -339,6 +352,16 @@
     Some(package)
 }
 
+fn find_unique_container(parsed_flags: &ProtoParsedFlags) -> Option<&str> {
+    let Some(container) = parsed_flags.parsed_flag.first().map(|pf| pf.container()) else {
+        return None;
+    };
+    if parsed_flags.parsed_flag.iter().any(|pf| pf.container() != container) {
+        return None;
+    }
+    Some(container)
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/tools/aconfig/src/main.rs b/tools/aconfig/src/main.rs
index 6872809..63a50c8 100644
--- a/tools/aconfig/src/main.rs
+++ b/tools/aconfig/src/main.rs
@@ -27,6 +27,7 @@
 mod codegen;
 mod commands;
 mod protos;
+mod storage;
 
 #[cfg(test)]
 mod test;
@@ -108,6 +109,17 @@
                 .arg(Arg::new("dedup").long("dedup").num_args(0).action(ArgAction::SetTrue))
                 .arg(Arg::new("out").long("out").default_value("-")),
         )
+        .subcommand(
+            Command::new("create-storage")
+                .arg(
+                    Arg::new("container")
+                        .long("container")
+                        .required(true)
+                        .help("The target container for the generated storage file."),
+                )
+                .arg(Arg::new("cache").long("cache").required(true))
+                .arg(Arg::new("out").long("out").required(true)),
+        )
 }
 
 fn get_required_arg<'a, T>(matches: &'a ArgMatches, arg_name: &str) -> Result<&'a T>
@@ -242,6 +254,16 @@
             let path = get_required_arg::<String>(sub_matches, "out")?;
             write_output_to_file_or_stdout(path, &output)?;
         }
+        Some(("create-storage", sub_matches)) => {
+            let cache = open_zero_or_more_files(sub_matches, "cache")?;
+            let container = get_required_arg::<String>(sub_matches, "container")?;
+            let dir = PathBuf::from(get_required_arg::<String>(sub_matches, "out")?);
+            let generated_files = commands::create_storage(cache, container)
+                .context("failed to create storage files")?;
+            generated_files
+                .iter()
+                .try_for_each(|file| write_output_file_realtive_to_dir(&dir, file))?;
+        }
         _ => unreachable!(),
     }
     Ok(())
diff --git a/tools/aconfig/src/storage/mod.rs b/tools/aconfig/src/storage/mod.rs
new file mode 100644
index 0000000..90e05f5
--- /dev/null
+++ b/tools/aconfig/src/storage/mod.rs
@@ -0,0 +1,166 @@
+/*
+ * 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 anyhow::Result;
+use std::collections::{HashMap, HashSet};
+
+use crate::commands::OutputFile;
+use crate::protos::{ProtoParsedFlag, ProtoParsedFlags};
+
+pub struct FlagPackage<'a> {
+    pub package_name: &'a str,
+    pub package_id: u32,
+    pub flag_names: HashSet<&'a str>,
+    pub boolean_flags: Vec<&'a ProtoParsedFlag>,
+    pub boolean_offset: u32,
+}
+
+impl<'a> FlagPackage<'a> {
+    fn new(package_name: &'a str, package_id: u32) -> Self {
+        FlagPackage {
+            package_name,
+            package_id,
+            flag_names: HashSet::new(),
+            boolean_flags: vec![],
+            boolean_offset: 0,
+        }
+    }
+
+    fn insert(&mut self, pf: &'a ProtoParsedFlag) {
+        if self.flag_names.insert(pf.name()) {
+            self.boolean_flags.push(pf);
+        }
+    }
+}
+
+pub fn group_flags_by_package<'a, I>(parsed_flags_vec_iter: I) -> Vec<FlagPackage<'a>>
+where
+    I: Iterator<Item = &'a ProtoParsedFlags>,
+{
+    // group flags by package
+    let mut packages: Vec<FlagPackage<'a>> = Vec::new();
+    let mut package_index: HashMap<&'a str, usize> = HashMap::new();
+    for parsed_flags in parsed_flags_vec_iter {
+        for parsed_flag in parsed_flags.parsed_flag.iter() {
+            let index = *(package_index.entry(parsed_flag.package()).or_insert(packages.len()));
+            if index == packages.len() {
+                packages.push(FlagPackage::new(parsed_flag.package(), index as u32));
+            }
+            packages[index].insert(parsed_flag);
+        }
+    }
+
+    // calculate package flag value start offset, in flag value file, each boolean
+    // is stored as two bytes, the first byte will be the flag value. the second
+    // byte is flag info byte, which is a bitmask to indicate the status of a flag
+    let mut boolean_offset = 0;
+    for p in packages.iter_mut() {
+        p.boolean_offset = boolean_offset;
+        boolean_offset += 2 * p.boolean_flags.len() as u32;
+    }
+
+    packages
+}
+
+pub fn generate_storage_files<'a, I>(
+    _containser: &str,
+    parsed_flags_vec_iter: I,
+) -> Result<Vec<OutputFile>>
+where
+    I: Iterator<Item = &'a ProtoParsedFlags>,
+{
+    let _packages = group_flags_by_package(parsed_flags_vec_iter);
+    Ok(vec![])
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::Input;
+
+    pub fn parse_all_test_flags() -> Vec<ProtoParsedFlags> {
+        let aconfig_files = [
+            (
+                "com.android.aconfig.storage.test_1",
+                "storage_test_1_part_1.aconfig",
+                include_bytes!("../../tests/storage_test_1_part_1.aconfig").as_slice(),
+            ),
+            (
+                "com.android.aconfig.storage.test_1",
+                "storage_test_1_part_2.aconfig",
+                include_bytes!("../../tests/storage_test_1_part_2.aconfig").as_slice(),
+            ),
+            (
+                "com.android.aconfig.storage.test_2",
+                "storage_test_2.aconfig",
+                include_bytes!("../../tests/storage_test_2.aconfig").as_slice(),
+            ),
+        ];
+
+        aconfig_files
+            .into_iter()
+            .map(|(pkg, file, content)| {
+                let bytes = crate::commands::parse_flags(
+                    pkg,
+                    Some("system"),
+                    vec![Input {
+                        source: format!("tests/{}", file).to_string(),
+                        reader: Box::new(content),
+                    }],
+                    vec![],
+                    crate::commands::DEFAULT_FLAG_PERMISSION,
+                )
+                .unwrap();
+                crate::protos::parsed_flags::try_from_binary_proto(&bytes).unwrap()
+            })
+            .collect()
+    }
+
+    #[test]
+    fn test_flag_package() {
+        let caches = parse_all_test_flags();
+        let packages = group_flags_by_package(caches.iter());
+
+        for pkg in packages.iter() {
+            let pkg_name = pkg.package_name;
+            assert_eq!(pkg.flag_names.len(), pkg.boolean_flags.len());
+            for pf in pkg.boolean_flags.iter() {
+                assert!(pkg.flag_names.contains(pf.name()));
+                assert_eq!(pf.package(), pkg_name);
+            }
+        }
+
+        assert_eq!(packages.len(), 2);
+
+        assert_eq!(packages[0].package_name, "com.android.aconfig.storage.test_1");
+        assert_eq!(packages[0].package_id, 0);
+        assert_eq!(packages[0].flag_names.len(), 5);
+        assert!(packages[0].flag_names.contains("enabled_rw"));
+        assert!(packages[0].flag_names.contains("disabled_rw"));
+        assert!(packages[0].flag_names.contains("enabled_ro"));
+        assert!(packages[0].flag_names.contains("disabled_ro"));
+        assert!(packages[0].flag_names.contains("enabled_fixed_ro"));
+        assert_eq!(packages[0].boolean_offset, 0);
+
+        assert_eq!(packages[1].package_name, "com.android.aconfig.storage.test_2");
+        assert_eq!(packages[1].package_id, 1);
+        assert_eq!(packages[1].flag_names.len(), 3);
+        assert!(packages[1].flag_names.contains("enabled_ro"));
+        assert!(packages[1].flag_names.contains("disabled_ro"));
+        assert!(packages[1].flag_names.contains("enabled_fixed_ro"));
+        assert_eq!(packages[1].boolean_offset, 10);
+    }
+}
diff --git a/tools/aconfig/tests/storage_test_1_part_1.aconfig b/tools/aconfig/tests/storage_test_1_part_1.aconfig
new file mode 100644
index 0000000..70462cd
--- /dev/null
+++ b/tools/aconfig/tests/storage_test_1_part_1.aconfig
@@ -0,0 +1,17 @@
+package: "com.android.aconfig.storage.test_1"
+container: "system"
+
+flag {
+    name: "enabled_rw"
+    namespace: "aconfig_test"
+    description: "This flag is ENABLED + READ_WRITE"
+    bug: ""
+}
+
+flag {
+    name: "disabled_rw"
+    namespace: "aconfig_test"
+    description: "This flag is DISABLED + READ_WRITE"
+    bug: "456"
+    is_exported: true
+}
diff --git a/tools/aconfig/tests/storage_test_1_part_2.aconfig b/tools/aconfig/tests/storage_test_1_part_2.aconfig
new file mode 100644
index 0000000..5eb0c0c
--- /dev/null
+++ b/tools/aconfig/tests/storage_test_1_part_2.aconfig
@@ -0,0 +1,24 @@
+package: "com.android.aconfig.storage.test_1"
+container: "system"
+
+flag {
+    name: "enabled_ro"
+    namespace: "aconfig_test"
+    description: "This flag is ENABLED + READ_ONLY"
+    bug: "abc"
+}
+
+flag {
+    name: "disabled_ro"
+    namespace: "aconfig_test"
+    description: "This flag is DISABLED + READ_ONLY"
+    bug: "123"
+}
+
+flag {
+    name: "enabled_fixed_ro"
+    namespace: "aconfig_test"
+    description: "This flag is fixed READ_ONLY + ENABLED"
+    bug: ""
+    is_fixed_read_only: true
+}
diff --git a/tools/aconfig/tests/storage_test_2.aconfig b/tools/aconfig/tests/storage_test_2.aconfig
new file mode 100644
index 0000000..bb14fd1
--- /dev/null
+++ b/tools/aconfig/tests/storage_test_2.aconfig
@@ -0,0 +1,24 @@
+package: "com.android.aconfig.storage.test_2"
+container: "system"
+
+flag {
+    name: "enabled_ro"
+    namespace: "aconfig_test"
+    description: "This flag is ENABLED + READ_ONLY"
+    bug: "abc"
+}
+
+flag {
+    name: "disabled_ro"
+    namespace: "aconfig_test"
+    description: "This flag is DISABLED + READ_ONLY"
+    bug: "123"
+}
+
+flag {
+    name: "enabled_fixed_ro"
+    namespace: "aconfig_test"
+    description: "This flag is fixed READ_ONLY + ENABLED"
+    bug: ""
+    is_fixed_read_only: true
+}