Merge "aconfig: create aconfig_storage_write_api crate" into main am: 672af9523b
Original change: https://android-review.googlesource.com/c/platform/build/+/2995596
Change-Id: I35d1960355142aa4004fce0b0650f44e83e2c36e
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/tools/aconfig/Cargo.toml b/tools/aconfig/Cargo.toml
index 7112fd4..6bd0d06 100644
--- a/tools/aconfig/Cargo.toml
+++ b/tools/aconfig/Cargo.toml
@@ -5,6 +5,7 @@
"aconfig_protos",
"aconfig_storage_file",
"aconfig_storage_read_api",
+ "aconfig_storage_write_api",
"aflags",
"printflags"
]
diff --git a/tools/aconfig/TEST_MAPPING b/tools/aconfig/TEST_MAPPING
index 26d6172..4acbe60 100644
--- a/tools/aconfig/TEST_MAPPING
+++ b/tools/aconfig/TEST_MAPPING
@@ -70,6 +70,10 @@
],
"postsubmit": [
{
+ // aconfig_storage_write_api unit tests
+ "name": "aconfig_storage_write_api.test"
+ },
+ {
// aconfig_storage_read_api unit tests
"name": "aconfig_storage_read_api.test"
},
diff --git a/tools/aconfig/aconfig_storage_file/src/lib.rs b/tools/aconfig/aconfig_storage_file/src/lib.rs
index 202f6a4..c40caba 100644
--- a/tools/aconfig/aconfig_storage_file/src/lib.rs
+++ b/tools/aconfig/aconfig_storage_file/src/lib.rs
@@ -163,6 +163,12 @@
#[error("fail to map storage file")]
MapFileFail(#[source] anyhow::Error),
+ #[error("fail to get mapped file")]
+ ObtainMappedFileFail(#[source] anyhow::Error),
+
+ #[error("fail to flush mapped storage file")]
+ MapFlushFail(#[source] anyhow::Error),
+
#[error("number of items in hash table exceed limit")]
HashTableSizeLimit(#[source] anyhow::Error),
diff --git a/tools/aconfig/aconfig_storage_write_api/Android.bp b/tools/aconfig/aconfig_storage_write_api/Android.bp
new file mode 100644
index 0000000..1382aba
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_write_api/Android.bp
@@ -0,0 +1,37 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_defaults {
+ name: "aconfig_storage_write_api.defaults",
+ edition: "2021",
+ lints: "none",
+ srcs: ["src/lib.rs"],
+ rustlibs: [
+ "libanyhow",
+ "libtempfile",
+ "libmemmap2",
+ "libcxx",
+ "libthiserror",
+ "libaconfig_storage_file",
+ ],
+}
+
+rust_library {
+ name: "libaconfig_storage_write_api",
+ crate_name: "aconfig_storage_write_api",
+ host_supported: true,
+ defaults: ["aconfig_storage_write_api.defaults"],
+}
+
+rust_test_host {
+ name: "aconfig_storage_write_api.test",
+ test_suites: ["general-tests"],
+ defaults: ["aconfig_storage_write_api.defaults"],
+ data: [
+ "tests/flag.val",
+ ],
+ rustlibs: [
+ "libaconfig_storage_read_api",
+ ],
+}
diff --git a/tools/aconfig/aconfig_storage_write_api/Cargo.toml b/tools/aconfig/aconfig_storage_write_api/Cargo.toml
new file mode 100644
index 0000000..494c19c
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_write_api/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "aconfig_storage_write_api"
+version = "0.1.0"
+edition = "2021"
+
+[features]
+default = ["cargo"]
+cargo = []
+
+[dependencies]
+anyhow = "1.0.69"
+memmap2 = "0.8.0"
+tempfile = "3.9.0"
+thiserror = "1.0.56"
+protobuf = "3.2.0"
+once_cell = "1.19.0"
+aconfig_storage_file = { path = "../aconfig_storage_file" }
+aconfig_storage_read_api = { path = "../aconfig_storage_read_api" }
+
+[build-dependencies]
+cxx-build = "1.0"
diff --git a/tools/aconfig/aconfig_storage_write_api/src/flag_value_update.rs b/tools/aconfig/aconfig_storage_write_api/src/flag_value_update.rs
new file mode 100644
index 0000000..1e9612a
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_write_api/src/flag_value_update.rs
@@ -0,0 +1,113 @@
+/*
+ * 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.
+ */
+
+//! flag value update module defines the flag value file write to mapped bytes
+
+use aconfig_storage_file::{AconfigStorageError, FlagValueHeader, FILE_VERSION};
+use anyhow::anyhow;
+
+/// Set flag value
+pub fn update_boolean_flag_value(
+ buf: &mut [u8],
+ flag_offset: u32,
+ flag_value: bool,
+) -> Result<(), AconfigStorageError> {
+ let interpreted_header = FlagValueHeader::from_bytes(buf)?;
+ if interpreted_header.version > FILE_VERSION {
+ return Err(AconfigStorageError::HigherStorageFileVersion(anyhow!(
+ "Cannot write to storage file with a higher version of {} with lib version {}",
+ interpreted_header.version,
+ FILE_VERSION
+ )));
+ }
+
+ let head = (interpreted_header.boolean_value_offset + flag_offset) as usize;
+
+ // TODO: right now, there is only boolean flags, with more flag value types added
+ // later, the end of boolean flag value section should be updated (b/322826265).
+ if head >= interpreted_header.file_size as usize {
+ return Err(AconfigStorageError::InvalidStorageFileOffset(anyhow!(
+ "Flag value offset goes beyond the end of the file."
+ )));
+ }
+
+ buf[head] = u8::from(flag_value).to_le_bytes()[0];
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use aconfig_storage_file::FlagValueList;
+
+ pub fn create_test_flag_value_list() -> FlagValueList {
+ let header = FlagValueHeader {
+ version: FILE_VERSION,
+ container: String::from("system"),
+ file_size: 34,
+ num_flags: 8,
+ boolean_value_offset: 26,
+ };
+ let booleans: Vec<bool> = vec![false; 8];
+ FlagValueList { header, booleans }
+ }
+
+ #[test]
+ // this test point locks down flag value update
+ fn test_boolean_flag_value_update() {
+ let flag_value_list = create_test_flag_value_list();
+ let value_offset = flag_value_list.header.boolean_value_offset;
+ let mut content = flag_value_list.as_bytes();
+ let true_byte = u8::from(true).to_le_bytes()[0];
+ let false_byte = u8::from(false).to_le_bytes()[0];
+
+ for i in 0..flag_value_list.header.num_flags {
+ let offset = (value_offset + i) as usize;
+ update_boolean_flag_value(&mut content, i, true).unwrap();
+ assert_eq!(content[offset], true_byte);
+ update_boolean_flag_value(&mut content, i, false).unwrap();
+ assert_eq!(content[offset], false_byte);
+ }
+ }
+
+ #[test]
+ // this test point locks down update beyond the end of boolean section
+ fn test_boolean_out_of_range() {
+ let mut flag_value_list = create_test_flag_value_list().as_bytes();
+ let error = update_boolean_flag_value(&mut flag_value_list[..], 8, true).unwrap_err();
+ assert_eq!(
+ format!("{:?}", error),
+ "InvalidStorageFileOffset(Flag value offset goes beyond the end of the file.)"
+ );
+ }
+
+ #[test]
+ // this test point locks down query error when file has a higher version
+ fn test_higher_version_storage_file() {
+ let mut value_list = create_test_flag_value_list();
+ value_list.header.version = FILE_VERSION + 1;
+ let mut flag_value = value_list.as_bytes();
+ let error = update_boolean_flag_value(&mut flag_value[..], 4, true).unwrap_err();
+ assert_eq!(
+ format!("{:?}", error),
+ format!(
+ "HigherStorageFileVersion(Cannot write to storage file with a higher version of {} with lib version {})",
+ FILE_VERSION + 1,
+ FILE_VERSION
+ )
+ );
+ }
+}
diff --git a/tools/aconfig/aconfig_storage_write_api/src/lib.rs b/tools/aconfig/aconfig_storage_write_api/src/lib.rs
new file mode 100644
index 0000000..17a6538
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_write_api/src/lib.rs
@@ -0,0 +1,120 @@
+/*
+ * 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.
+ */
+
+//! `aconfig_storage_write_api` is a crate that defines write apis to update flag value
+//! in storage file. It provides one api to interface with storage files.
+
+pub mod flag_value_update;
+pub mod mapped_file;
+
+#[cfg(test)]
+mod test_utils;
+
+use aconfig_storage_file::AconfigStorageError;
+
+use anyhow::anyhow;
+use memmap2::MmapMut;
+
+/// Storage file location pb file
+pub const STORAGE_LOCATION_FILE: &str = "/metadata/aconfig/persistent_storage_file_records.pb";
+
+/// Get mmaped flag value file given the container name
+///
+/// \input container: the flag package container
+/// \return a result of mapped file
+///
+///
+/// # Safety
+///
+/// The memory mapped file may have undefined behavior if there are writes to this
+/// file not thru this memory mapped file or there are concurrent writes to this
+/// memory mapped file. Ensure all writes to the underlying file are thru this memory
+/// mapped file and there are no concurrent writes.
+pub unsafe fn get_mapped_flag_value_file(container: &str) -> Result<MmapMut, AconfigStorageError> {
+ unsafe { crate::mapped_file::get_mapped_file(STORAGE_LOCATION_FILE, container) }
+}
+
+/// Set boolean flag value thru mapped file and flush the change to file
+///
+/// \input mapped_file: the mapped flag value file
+/// \input offset: flag value offset
+/// \input value: updated flag value
+/// \return a result of ()
+///
+pub fn set_boolean_flag_value(
+ file: &mut MmapMut,
+ offset: u32,
+ value: bool,
+) -> Result<(), AconfigStorageError> {
+ crate::flag_value_update::update_boolean_flag_value(file, offset, value)?;
+ file.flush().map_err(|errmsg| {
+ AconfigStorageError::MapFlushFail(anyhow!("fail to flush storage file: {}", errmsg))
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::test_utils::copy_to_temp_file;
+ use aconfig_storage_file::protos::storage_record_pb::write_proto_to_temp_file;
+ use aconfig_storage_read_api::flag_value_query::find_boolean_flag_value;
+ use std::fs::File;
+ use std::io::Read;
+
+ fn get_boolean_flag_value_at_offset(file: &str, offset: u32) -> bool {
+ let mut f = File::open(&file).unwrap();
+ let mut bytes = Vec::new();
+ f.read_to_end(&mut bytes).unwrap();
+ find_boolean_flag_value(&bytes, offset).unwrap()
+ }
+
+ #[test]
+ fn test_set_boolean_flag_value() {
+ let flag_value_file = copy_to_temp_file("./tests/flag.val", false).unwrap();
+ let flag_value_path = flag_value_file.path().display().to_string();
+ let text_proto = format!(
+ r#"
+files {{
+ version: 0
+ container: "system"
+ package_map: "some_package.map"
+ flag_map: "some_flag.map"
+ flag_val: "{}"
+ timestamp: 12345
+}}
+"#,
+ flag_value_path
+ );
+ let record_pb_file = write_proto_to_temp_file(&text_proto).unwrap();
+ let record_pb_path = record_pb_file.path().display().to_string();
+
+ // SAFETY:
+ // The safety here is guaranteed as only this single threaded test process will
+ // write to this file
+ unsafe {
+ let mut file = crate::mapped_file::get_mapped_file(&record_pb_path, "system").unwrap();
+ for i in 0..8 {
+ set_boolean_flag_value(&mut file, i, true).unwrap();
+ let value = get_boolean_flag_value_at_offset(&flag_value_path, i);
+ assert_eq!(value, true);
+
+ set_boolean_flag_value(&mut file, i, false).unwrap();
+ let value = get_boolean_flag_value_at_offset(&flag_value_path, i);
+ assert_eq!(value, false);
+ }
+ }
+ }
+}
diff --git a/tools/aconfig/aconfig_storage_write_api/src/mapped_file.rs b/tools/aconfig/aconfig_storage_write_api/src/mapped_file.rs
new file mode 100644
index 0000000..4c98be4
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_write_api/src/mapped_file.rs
@@ -0,0 +1,189 @@
+/*
+ * 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.
+ */
+
+use std::fs::{self, File, OpenOptions};
+use std::io::{BufReader, Read};
+
+use anyhow::anyhow;
+use memmap2::MmapMut;
+
+use aconfig_storage_file::protos::{storage_record_pb::try_from_binary_proto, ProtoStorageFiles};
+use aconfig_storage_file::AconfigStorageError::{
+ self, FileReadFail, MapFileFail, ProtobufParseFail, StorageFileNotFound,
+};
+
+/// Find where persistent storage value file is for a particular container
+fn find_persist_flag_value_file(
+ location_pb_file: &str,
+ container: &str,
+) -> Result<String, AconfigStorageError> {
+ let file = File::open(location_pb_file).map_err(|errmsg| {
+ FileReadFail(anyhow!("Failed to open file {}: {}", location_pb_file, errmsg))
+ })?;
+ let mut reader = BufReader::new(file);
+ let mut bytes = Vec::new();
+ reader.read_to_end(&mut bytes).map_err(|errmsg| {
+ FileReadFail(anyhow!("Failed to read file {}: {}", location_pb_file, errmsg))
+ })?;
+ let storage_locations: ProtoStorageFiles = try_from_binary_proto(&bytes).map_err(|errmsg| {
+ ProtobufParseFail(anyhow!(
+ "Failed to parse storage location pb file {}: {}",
+ location_pb_file,
+ errmsg
+ ))
+ })?;
+ for location_info in storage_locations.files.iter() {
+ if location_info.container() == container {
+ return Ok(location_info.flag_val().to_string());
+ }
+ }
+ Err(StorageFileNotFound(anyhow!("Persistent flag value file does not exist for {}", container)))
+}
+
+/// Get a mapped storage file given the container and file type
+///
+/// # Safety
+///
+/// The memory mapped file may have undefined behavior if there are writes to this
+/// file not thru this memory mapped file or there are concurrent writes to this
+/// memory mapped file. Ensure all writes to the underlying file are thru this memory
+/// mapped file and there are no concurrent writes.
+pub unsafe fn get_mapped_file(
+ location_pb_file: &str,
+ container: &str,
+) -> Result<MmapMut, AconfigStorageError> {
+ let file_path = find_persist_flag_value_file(location_pb_file, container)?;
+
+ // make sure file has read write permission
+ let perms = fs::metadata(&file_path).unwrap().permissions();
+ if perms.readonly() {
+ return Err(MapFileFail(anyhow!("fail to map non read write storage file {}", file_path)));
+ }
+
+ let file =
+ OpenOptions::new().read(true).write(true).open(&file_path).map_err(|errmsg| {
+ FileReadFail(anyhow!("Failed to open file {}: {}", file_path, errmsg))
+ })?;
+
+ unsafe {
+ MmapMut::map_mut(&file).map_err(|errmsg| {
+ MapFileFail(anyhow!("fail to map storage file {}: {}", file_path, errmsg))
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::test_utils::copy_to_temp_file;
+ use aconfig_storage_file::protos::storage_record_pb::write_proto_to_temp_file;
+
+ #[test]
+ fn test_find_persist_flag_value_file_location() {
+ 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 file = write_proto_to_temp_file(&text_proto).unwrap();
+ let file_full_path = file.path().display().to_string();
+ let flag_value_file = find_persist_flag_value_file(&file_full_path, "system").unwrap();
+ assert_eq!(flag_value_file, "/metadata/aconfig/system.val");
+ let flag_value_file = find_persist_flag_value_file(&file_full_path, "product").unwrap();
+ assert_eq!(flag_value_file, "/metadata/aconfig/product.val");
+ let err = find_persist_flag_value_file(&file_full_path, "vendor").unwrap_err();
+ assert_eq!(
+ format!("{:?}", err),
+ "StorageFileNotFound(Persistent flag value file does not exist for vendor)"
+ );
+ }
+
+ #[test]
+ fn test_mapped_file_contents() {
+ let mut rw_file = copy_to_temp_file("./tests/flag.val", false).unwrap();
+ let text_proto = format!(
+ r#"
+files {{
+ version: 0
+ container: "system"
+ package_map: "some_package.map"
+ flag_map: "some_flag.map"
+ flag_val: "{}"
+ timestamp: 12345
+}}
+"#,
+ rw_file.path().display().to_string()
+ );
+ let storage_record_file = write_proto_to_temp_file(&text_proto).unwrap();
+ let storage_record_file_path = storage_record_file.path().display().to_string();
+
+ let mut content = Vec::new();
+ rw_file.read_to_end(&mut content).unwrap();
+
+ // SAFETY:
+ // The safety here is guaranteed here as no writes happens to this temp file
+ unsafe {
+ let mmaped_file = get_mapped_file(&storage_record_file_path, "system").unwrap();
+ assert_eq!(mmaped_file[..], content[..]);
+ }
+ }
+
+ #[test]
+ fn test_mapped_read_only_file() {
+ let ro_file = copy_to_temp_file("./tests/flag.val", true).unwrap();
+ let text_proto = format!(
+ r#"
+files {{
+ version: 0
+ container: "system"
+ package_map: "some_package.map"
+ flag_map: "some_flag.map"
+ flag_val: "{}"
+ timestamp: 12345
+}}
+"#,
+ ro_file.path().display().to_string()
+ );
+ let storage_record_file = write_proto_to_temp_file(&text_proto).unwrap();
+ let storage_record_file_path = storage_record_file.path().display().to_string();
+
+ // SAFETY:
+ // The safety here is guaranteed here as no writes happens to this temp file
+ unsafe {
+ let error = get_mapped_file(&storage_record_file_path, "system").unwrap_err();
+ assert_eq!(
+ format!("{:?}", error),
+ format!(
+ "MapFileFail(fail to map non read write storage file {})",
+ ro_file.path().display().to_string()
+ )
+ );
+ }
+ }
+}
diff --git a/tools/aconfig/aconfig_storage_write_api/src/test_utils.rs b/tools/aconfig/aconfig_storage_write_api/src/test_utils.rs
new file mode 100644
index 0000000..06e2e22
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_write_api/src/test_utils.rs
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+use anyhow::Result;
+use std::fs;
+use tempfile::NamedTempFile;
+
+/// Create temp file copy
+pub(crate) fn copy_to_temp_file(source_file: &str, read_only: bool) -> Result<NamedTempFile> {
+ let file = NamedTempFile::new()?;
+ fs::copy(source_file, file.path())?;
+ if read_only {
+ let file_name = file.path().display().to_string();
+ let mut perms = fs::metadata(file_name).unwrap().permissions();
+ perms.set_readonly(true);
+ fs::set_permissions(file.path(), perms.clone()).unwrap();
+ }
+ Ok(file)
+}
diff --git a/tools/aconfig/aconfig_storage_write_api/tests/flag.val b/tools/aconfig/aconfig_storage_write_api/tests/flag.val
new file mode 100644
index 0000000..f39f8d3
--- /dev/null
+++ b/tools/aconfig/aconfig_storage_write_api/tests/flag.val
Binary files differ