diff --git a/tools/aconfig/aconfig_storage_file/Android.bp b/tools/aconfig/aconfig_storage_file/Android.bp index 13072c9ccf..a2650d8d40 100644 --- a/tools/aconfig/aconfig_storage_file/Android.bp +++ b/tools/aconfig/aconfig_storage_file/Android.bp @@ -9,6 +9,9 @@ rust_defaults { srcs: ["src/lib.rs"], rustlibs: [ "libanyhow", + "libaconfig_storage_protos", + "libonce_cell", + "libprotobuf", ], } @@ -24,3 +27,11 @@ rust_test_host { 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 03c7309c0d..e65b1bfe45 100644 --- a/tools/aconfig/aconfig_storage_file/Cargo.toml +++ b/tools/aconfig/aconfig_storage_file/Cargo.toml @@ -9,3 +9,7 @@ cargo = [] [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 0000000000..1feeb60677 --- /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 0000000000..c6728bdfee --- /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 47fcc9075c..f5aecffa5c 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_table; 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 0000000000..37df3e1eb5 --- /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 { + 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 0000000000..e1fb6c76e8 --- /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> { + 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) +}