aconfig: add aconfig_storage_metadata proto

Introduce a new proto to capture storage file location for each
container. This proto file will appeara as
/metadata/aconfig/storage_file_location.pb. Storage service daemon is
responsible for writing entires to it when a new storage file set is
available. The flag read lib will use this file to find the
corresponding storage file and mmap them.

Bug: b/321077378
Test: atest aconfig_storage_file.test
Change-Id: I226e76be895805dce52a075050dcd5b42d337be8
diff --git a/tools/aconfig/aconfig_storage_file/Android.bp b/tools/aconfig/aconfig_storage_file/Android.bp
index 13072c9..a2650d8 100644
--- a/tools/aconfig/aconfig_storage_file/Android.bp
+++ b/tools/aconfig/aconfig_storage_file/Android.bp
@@ -9,6 +9,9 @@
     srcs: ["src/lib.rs"],
     rustlibs: [
         "libanyhow",
+        "libaconfig_storage_protos",
+        "libonce_cell",
+        "libprotobuf",
     ],
 }
 
@@ -24,3 +27,11 @@
     test_suites: ["general-tests"],
     defaults: ["aconfig_storage_file.defaults"],
 }
+
+rust_protobuf {
+    name: "libaconfig_storage_protos",
+    protos: ["protos/aconfig_storage_metadata.proto"],
+    crate_name: "aconfig_storage_protos",
+    source_stem: "aconfig_storage_protos",
+    host_supported: true,
+}
diff --git a/tools/aconfig/aconfig_storage_file/Cargo.toml b/tools/aconfig/aconfig_storage_file/Cargo.toml
index 03c7309..e65b1bf 100644
--- a/tools/aconfig/aconfig_storage_file/Cargo.toml
+++ b/tools/aconfig/aconfig_storage_file/Cargo.toml
@@ -9,3 +9,7 @@
 
 [dependencies]
 anyhow = "1.0.69"
+protobuf = "3.2.0"
+
+[build-dependencies]
+protobuf-codegen = "3.2.0"
diff --git a/tools/aconfig/aconfig_storage_file/build.rs b/tools/aconfig/aconfig_storage_file/build.rs
new file mode 100644
index 0000000..1feeb60
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_file/build.rs
@@ -0,0 +1,17 @@
+use protobuf_codegen::Codegen;
+
+fn main() {
+    let proto_files = vec!["protos/aconfig_storage_metadata.proto"];
+
+    // tell cargo to only re-run the build script if any of the proto files has changed
+    for path in &proto_files {
+        println!("cargo:rerun-if-changed={}", path);
+    }
+
+    Codegen::new()
+        .pure()
+        .include("protos")
+        .inputs(proto_files)
+        .cargo_out_dir("aconfig_storage_protos")
+        .run_from_script();
+}
diff --git a/tools/aconfig/aconfig_storage_file/protos/aconfig_storage_metadata.proto b/tools/aconfig/aconfig_storage_file/protos/aconfig_storage_metadata.proto
new file mode 100644
index 0000000..c6728bd
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_file/protos/aconfig_storage_metadata.proto
@@ -0,0 +1,34 @@
+// Copyright (C) 2024 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
+
+// This is the schema definition for aconfig files. Modifications need to be
+// either backwards compatible, or include updates to all aconfig files in the
+// Android tree.
+
+syntax = "proto2";
+
+package android.aconfig_storage_metadata;
+
+message storage_file_info {
+  optional uint32 version = 1;
+  optional string container = 2;
+  optional string package_map = 3;
+  optional string flag_map = 4;
+  optional string flag_val = 5;
+  optional int64 timestamp = 6;
+}
+
+message storage_files {
+  repeated storage_file_info files = 1;
+};
diff --git a/tools/aconfig/aconfig_storage_file/src/lib.rs b/tools/aconfig/aconfig_storage_file/src/lib.rs
index 47fcc90..f5aecff 100644
--- a/tools/aconfig/aconfig_storage_file/src/lib.rs
+++ b/tools/aconfig/aconfig_storage_file/src/lib.rs
@@ -21,6 +21,10 @@
 pub mod flag_value;
 pub mod package_table;
 
+mod protos;
+#[cfg(test)]
+mod test_utils;
+
 use anyhow::{anyhow, Result};
 use std::collections::hash_map::DefaultHasher;
 use std::hash::{Hash, Hasher};
diff --git a/tools/aconfig/aconfig_storage_file/src/protos.rs b/tools/aconfig/aconfig_storage_file/src/protos.rs
new file mode 100644
index 0000000..37df3e1
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_file/src/protos.rs
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+// When building with the Android tool-chain
+//
+//   - an external crate `aconfig_storage_metadata_protos` will be generated
+//   - the feature "cargo" will be disabled
+//
+// When building with cargo
+//
+//   - a local sub-module will be generated in OUT_DIR and included in this file
+//   - the feature "cargo" will be enabled
+//
+// This module hides these differences from the rest of the codebase.
+
+// ---- When building with the Android tool-chain ----
+#[cfg(not(feature = "cargo"))]
+mod auto_generated {
+    pub use aconfig_storage_protos::aconfig_storage_metadata as ProtoStorage;
+    pub use ProtoStorage::Storage_file_info as ProtoStorageFileInfo;
+    pub use ProtoStorage::Storage_files as ProtoStorageFiles;
+}
+
+// ---- When building with cargo ----
+#[cfg(feature = "cargo")]
+mod auto_generated {
+    // include! statements should be avoided (because they import file contents verbatim), but
+    // because this is only used during local development, and only if using cargo instead of the
+    // Android tool-chain, we allow it
+    include!(concat!(env!("OUT_DIR"), "/aconfig_storage_protos/mod.rs"));
+    pub use aconfig_storage_metadata::Storage_file_info as ProtoStorageFileInfo;
+    pub use aconfig_storage_metadata::Storage_files as ProtoStorageFiles;
+}
+
+// ---- Common for both the Android tool-chain and cargo ----
+pub use auto_generated::*;
+
+use anyhow::Result;
+
+pub mod storage_files {
+    use super::*;
+    use anyhow::ensure;
+
+    pub fn try_from_binary_proto(bytes: &[u8]) -> Result<ProtoStorageFiles> {
+        let message: ProtoStorageFiles = protobuf::Message::parse_from_bytes(bytes)?;
+        verify_fields(&message)?;
+        Ok(message)
+    }
+
+    pub fn verify_fields(storage_files: &ProtoStorageFiles) -> Result<()> {
+        for storage_file_info in storage_files.files.iter() {
+            ensure!(
+                !storage_file_info.package_map().is_empty(),
+                "invalid storage file record: missing package map file for container {}",
+                storage_file_info.container()
+            );
+            ensure!(
+                !storage_file_info.flag_map().is_empty(),
+                "invalid storage file record: missing flag map file for container {}",
+                storage_file_info.container()
+            );
+            ensure!(
+                !storage_file_info.flag_val().is_empty(),
+                "invalid storage file record: missing flag val file for container {}",
+                storage_file_info.container()
+            );
+        }
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::test_utils::get_binary_storage_proto_bytes;
+
+    #[test]
+    fn test_parse_storage_files() {
+        let text_proto = r#"
+files {
+    version: 0
+    container: "system"
+    package_map: "/system/etc/package.map"
+    flag_map: "/system/etc/flag.map"
+    flag_val: "/metadata/aconfig/system.val"
+    timestamp: 12345
+}
+files {
+    version: 1
+    container: "product"
+    package_map: "/product/etc/package.map"
+    flag_map: "/product/etc/flag.map"
+    flag_val: "/metadata/aconfig/product.val"
+    timestamp: 54321
+}
+"#;
+        let binary_proto_bytes = get_binary_storage_proto_bytes(text_proto).unwrap();
+        let storage_files = storage_files::try_from_binary_proto(&binary_proto_bytes).unwrap();
+        assert_eq!(storage_files.files.len(), 2);
+        let system_file = &storage_files.files[0];
+        assert_eq!(system_file.version(), 0);
+        assert_eq!(system_file.container(), "system");
+        assert_eq!(system_file.package_map(), "/system/etc/package.map");
+        assert_eq!(system_file.flag_map(), "/system/etc/flag.map");
+        assert_eq!(system_file.flag_val(), "/metadata/aconfig/system.val");
+        assert_eq!(system_file.timestamp(), 12345);
+        let product_file = &storage_files.files[1];
+        assert_eq!(product_file.version(), 1);
+        assert_eq!(product_file.container(), "product");
+        assert_eq!(product_file.package_map(), "/product/etc/package.map");
+        assert_eq!(product_file.flag_map(), "/product/etc/flag.map");
+        assert_eq!(product_file.flag_val(), "/metadata/aconfig/product.val");
+        assert_eq!(product_file.timestamp(), 54321);
+    }
+
+    #[test]
+    fn test_parse_invalid_storage_files() {
+        let text_proto = r#"
+files {
+    version: 0
+    container: "system"
+    package_map: ""
+    flag_map: "/system/etc/flag.map"
+    flag_val: "/metadata/aconfig/system.val"
+    timestamp: 12345
+}
+"#;
+        let binary_proto_bytes = get_binary_storage_proto_bytes(text_proto).unwrap();
+        let err = storage_files::try_from_binary_proto(&binary_proto_bytes).unwrap_err();
+        assert_eq!(
+            format!("{:?}", err),
+            "invalid storage file record: missing package map file for container system"
+        );
+
+        let text_proto = r#"
+files {
+    version: 0
+    container: "system"
+    package_map: "/system/etc/package.map"
+    flag_map: ""
+    flag_val: "/metadata/aconfig/system.val"
+    timestamp: 12345
+}
+"#;
+        let binary_proto_bytes = get_binary_storage_proto_bytes(text_proto).unwrap();
+        let err = storage_files::try_from_binary_proto(&binary_proto_bytes).unwrap_err();
+        assert_eq!(
+            format!("{:?}", err),
+            "invalid storage file record: missing flag map file for container system"
+        );
+
+        let text_proto = r#"
+files {
+    version: 0
+    container: "system"
+    package_map: "/system/etc/package.map"
+    flag_map: "/system/etc/flag.map"
+    flag_val: ""
+    timestamp: 12345
+}
+"#;
+        let binary_proto_bytes = get_binary_storage_proto_bytes(text_proto).unwrap();
+        let err = storage_files::try_from_binary_proto(&binary_proto_bytes).unwrap_err();
+        assert_eq!(
+            format!("{:?}", err),
+            "invalid storage file record: missing flag val file for container system"
+        );
+    }
+}
diff --git a/tools/aconfig/aconfig_storage_file/src/test_utils.rs b/tools/aconfig/aconfig_storage_file/src/test_utils.rs
new file mode 100644
index 0000000..e1fb6c7
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_file/src/test_utils.rs
@@ -0,0 +1,26 @@
+/*
+ * 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 protobuf::Message;
+use crate::protos::ProtoStorageFiles;
+
+pub fn get_binary_storage_proto_bytes(text_proto: &str) -> Result<Vec<u8>> {
+    let storage_files: ProtoStorageFiles = protobuf::text_format::parse_from_str(text_proto)?;
+    let mut binary_proto = Vec::new();
+    storage_files.write_to_vec(&mut binary_proto)?;
+    Ok(binary_proto)
+}