diff --git a/tools/aconfig/TEST_MAPPING b/tools/aconfig/TEST_MAPPING index 2f1694b1f8..0ea8feab7c 100644 --- a/tools/aconfig/TEST_MAPPING +++ b/tools/aconfig/TEST_MAPPING @@ -81,6 +81,10 @@ // aconfig_storage write api rust integration tests "name": "aconfig_storage_write_api.test.rust" }, + { + // aconfig_storage write api cpp integration tests + "name": "aconfig_storage_write_api.test.cpp" + }, { // aconfig_storage read api rust integration tests "name": "aconfig_storage_read_api.test.rust" diff --git a/tools/aconfig/aconfig_storage_write_api/Android.bp b/tools/aconfig/aconfig_storage_write_api/Android.bp index 1382aba92e..0f15b9c762 100644 --- a/tools/aconfig/aconfig_storage_write_api/Android.bp +++ b/tools/aconfig/aconfig_storage_write_api/Android.bp @@ -35,3 +35,47 @@ rust_test_host { "libaconfig_storage_read_api", ], } + +// cxx source codegen from rust api +genrule { + name: "libcxx_aconfig_storage_write_api_bridge_code", + tools: ["cxxbridge"], + cmd: "$(location cxxbridge) $(in) > $(out)", + srcs: ["src/lib.rs"], + out: ["aconfig_storage/lib.rs.cc"], +} + +// cxx header codegen from rust api +genrule { + name: "libcxx_aconfig_storage_write_api_bridge_header", + tools: ["cxxbridge"], + cmd: "$(location cxxbridge) $(in) --header > $(out)", + srcs: ["src/lib.rs"], + out: ["aconfig_storage/lib.rs.h"], +} + +// a static cc lib based on generated code +rust_ffi_static { + name: "libaconfig_storage_write_api_cxx_bridge", + crate_name: "aconfig_storage_write_api_cxx_bridge", + host_supported: true, + defaults: ["aconfig_storage_write_api.defaults"], +} + +// flag write api cc interface +cc_library_static { + name: "libaconfig_storage_write_api_cc", + srcs: ["aconfig_storage_write_api.cpp"], + generated_headers: [ + "cxx-bridge-header", + "libcxx_aconfig_storage_write_api_bridge_header" + ], + generated_sources: ["libcxx_aconfig_storage_write_api_bridge_code"], + whole_static_libs: ["libaconfig_storage_write_api_cxx_bridge"], + export_include_dirs: ["include"], + static_libs: [ + "libaconfig_storage_protos_cc", + "libprotobuf-cpp-lite", + "libbase", + ], +} diff --git a/tools/aconfig/aconfig_storage_write_api/Cargo.toml b/tools/aconfig/aconfig_storage_write_api/Cargo.toml index 494c19c145..eaa55f2531 100644 --- a/tools/aconfig/aconfig_storage_write_api/Cargo.toml +++ b/tools/aconfig/aconfig_storage_write_api/Cargo.toml @@ -9,11 +9,11 @@ cargo = [] [dependencies] anyhow = "1.0.69" +cxx = "1.0" 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" } diff --git a/tools/aconfig/aconfig_storage_write_api/aconfig_storage_write_api.cpp b/tools/aconfig/aconfig_storage_write_api/aconfig_storage_write_api.cpp new file mode 100644 index 0000000000..391b3050ca --- /dev/null +++ b/tools/aconfig/aconfig_storage_write_api/aconfig_storage_write_api.cpp @@ -0,0 +1,124 @@ + +#include +#include +#include + +#include +#include +#include + +#include "rust/cxx.h" +#include "aconfig_storage/lib.rs.h" +#include "aconfig_storage/aconfig_storage_write_api.hpp" + +using storage_records_pb = android::aconfig_storage_metadata::storage_files; +using storage_record_pb = android::aconfig_storage_metadata::storage_file_info; +using namespace android::base; + +namespace aconfig_storage { + +/// Storage location pb file +static constexpr char kPersistStorageRecordsPb[] = + "/metadata/aconfig/persistent_storage_file_records.pb"; + +/// Read aconfig storage records pb file +static Result read_storage_records_pb(std::string const& pb_file) { + auto records = storage_records_pb(); + auto content = std::string(); + if (!ReadFileToString(pb_file, &content)) { + return ErrnoError() << "ReadFileToString failed"; + } + + if (!records.ParseFromString(content)) { + return ErrnoError() << "Unable to parse persistent storage records protobuf"; + } + return records; +} + +/// Get storage file path +static Result find_storage_file( + std::string const& pb_file, + std::string const& container) { + auto records_pb = read_storage_records_pb(pb_file); + if (!records_pb.ok()) { + return Error() << "Unable to read storage records from " << pb_file + << " : " << records_pb.error(); + } + + for (auto& entry : records_pb->files()) { + if (entry.container() == container) { + return entry.flag_val(); + } + } + + return Error() << "Unable to find storage files for container " << container;; +} + +/// Map a storage file +static Result map_storage_file(std::string const& file) { + struct stat file_stat; + if (stat(file.c_str(), &file_stat) < 0) { + return Error() << "fstat failed"; + } + + if ((file_stat.st_mode & (S_IWUSR | S_IWGRP | S_IWOTH)) == 0) { + return Error() << "cannot map nonwriteable file"; + } + + size_t file_size = file_stat.st_size; + + const int fd = open(file.c_str(), O_RDWR | O_NOFOLLOW | O_CLOEXEC); + if (fd == -1) { + return Error() << "failed to open " << file; + }; + + void* const map_result = + mmap(nullptr, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (map_result == MAP_FAILED) { + return Error() << "mmap failed"; + } + + auto mapped_file = MappedFlagValueFile(); + mapped_file.file_ptr = map_result; + mapped_file.file_size = file_size; + + return mapped_file; +} + +namespace private_internal_api { + +/// Get mapped file implementation. +Result get_mapped_flag_value_file_impl( + std::string const& pb_file, + std::string const& container) { + auto file_result = find_storage_file(pb_file, container); + if (!file_result.ok()) { + return Error() << file_result.error(); + } + return map_storage_file(*file_result); +} + +} // namespace private internal api + +/// Get mapped writeable flag value file +Result get_mapped_flag_value_file( + std::string const& container) { + return private_internal_api::get_mapped_flag_value_file_impl( + kPersistStorageRecordsPb, container); +} + +/// Set boolean flag value +Result set_boolean_flag_value( + const MappedFlagValueFile& file, + uint32_t offset, + bool value) { + auto content = rust::Slice( + static_cast(file.file_ptr), file.file_size); + auto update_cxx = update_boolean_flag_value_cxx(content, offset, value); + if (!update_cxx.update_success) { + return Error() << std::string(update_cxx.error_message.c_str()); + } + return {}; +} + +} // namespace aconfig_storage diff --git a/tools/aconfig/aconfig_storage_write_api/build.rs b/tools/aconfig/aconfig_storage_write_api/build.rs new file mode 100644 index 0000000000..7b1aa53b5f --- /dev/null +++ b/tools/aconfig/aconfig_storage_write_api/build.rs @@ -0,0 +1,4 @@ +fn main() { + let _ = cxx_build::bridge("src/lib.rs"); + println!("cargo:rerun-if-changed=src/lib.rs"); +} diff --git a/tools/aconfig/aconfig_storage_write_api/include/aconfig_storage/aconfig_storage_write_api.hpp b/tools/aconfig/aconfig_storage_write_api/include/aconfig_storage/aconfig_storage_write_api.hpp new file mode 100644 index 0000000000..9e6332ac27 --- /dev/null +++ b/tools/aconfig/aconfig_storage_write_api/include/aconfig_storage/aconfig_storage_write_api.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +#include + +using namespace android::base; + +namespace aconfig_storage { + +/// Mapped flag value file +struct MappedFlagValueFile{ + void* file_ptr; + size_t file_size; +}; + +/// DO NOT USE APIS IN THE FOLLOWING NAMESPACE DIRECTLY +namespace private_internal_api { + +Result get_mapped_flag_value_file_impl( + std::string const& pb_file, + std::string const& container); + +} // namespace private_internal_api + +/// Get mapped writeable flag value file +Result get_mapped_flag_value_file( + std::string const& container); + +/// Set boolean flag value +Result set_boolean_flag_value( + const MappedFlagValueFile& file, + uint32_t offset, + bool value); + +} // namespace aconfig_storage diff --git a/tools/aconfig/aconfig_storage_write_api/src/lib.rs b/tools/aconfig/aconfig_storage_write_api/src/lib.rs index 17a6538f71..5562d6a126 100644 --- a/tools/aconfig/aconfig_storage_write_api/src/lib.rs +++ b/tools/aconfig/aconfig_storage_write_api/src/lib.rs @@ -65,6 +65,45 @@ pub fn set_boolean_flag_value( }) } +// *************************************** // +// CC INTERLOP +// *************************************** // + +// Exported rust data structure and methods, c++ code will be generated +#[cxx::bridge] +mod ffi { + // Flag value update return for cc interlop + pub struct BooleanFlagValueUpdateCXX { + pub update_success: bool, + pub error_message: String, + } + + // Rust export to c++ + extern "Rust" { + pub fn update_boolean_flag_value_cxx( + file: &mut [u8], + offset: u32, + value: bool, + ) -> BooleanFlagValueUpdateCXX; + } +} + +pub(crate) fn update_boolean_flag_value_cxx( + file: &mut [u8], + offset: u32, + value: bool, +) -> ffi::BooleanFlagValueUpdateCXX { + match crate::flag_value_update::update_boolean_flag_value(file, offset, value) { + Ok(()) => { + ffi::BooleanFlagValueUpdateCXX { update_success: true, error_message: String::from("") } + } + Err(errmsg) => ffi::BooleanFlagValueUpdateCXX { + update_success: false, + error_message: format!("{:?}", errmsg), + }, + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/tools/aconfig/aconfig_storage_write_api/tests/Android.bp b/tools/aconfig/aconfig_storage_write_api/tests/Android.bp index bb8c6dfa5e..d2a52fe09e 100644 --- a/tools/aconfig/aconfig_storage_write_api/tests/Android.bp +++ b/tools/aconfig/aconfig_storage_write_api/tests/Android.bp @@ -17,3 +17,26 @@ rust_test { ], test_suites: ["general-tests"], } + +cc_test { + name: "aconfig_storage_write_api.test.cpp", + srcs: [ + "storage_write_api_test.cpp", + ], + static_libs: [ + "libgmock", + "libaconfig_storage_protos_cc", + "libprotobuf-cpp-lite", + "libaconfig_storage_read_api_cc", + "libaconfig_storage_write_api_cc", + "libbase", + "liblog", + ], + data: [ + "flag.val", + ], + test_suites: [ + "device-tests", + "general-tests", + ], +} diff --git a/tools/aconfig/aconfig_storage_write_api/tests/storage_write_api_test.cpp b/tools/aconfig/aconfig_storage_write_api/tests/storage_write_api_test.cpp new file mode 100644 index 0000000000..3035f9e155 --- /dev/null +++ b/tools/aconfig/aconfig_storage_write_api/tests/storage_write_api_test.cpp @@ -0,0 +1,134 @@ +/* + * 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. + */ + +#include +#include +#include + +#include +#include "aconfig_storage/aconfig_storage_read_api.hpp" +#include "aconfig_storage/aconfig_storage_write_api.hpp" +#include +#include +#include +#include + +using android::aconfig_storage_metadata::storage_files; +using namespace android::base; + +namespace api = aconfig_storage; +namespace private_api = aconfig_storage::private_internal_api; + +class AconfigStorageTest : public ::testing::Test { + protected: + Result copy_to_rw_temp_file(std::string const& source_file) { + auto temp_file = std::string(std::tmpnam(nullptr)); + auto content = std::string(); + if (!ReadFileToString(source_file, &content)) { + return Error() << "failed to read file: " << source_file; + } + if (!WriteStringToFile(content, temp_file)) { + return Error() << "failed to copy file: " << source_file; + } + if (chmod(temp_file.c_str(), + S_IRUSR | S_IRGRP | S_IROTH | S_IWUSR | S_IWGRP | S_IWOTH) == -1) { + return Error() << "failed to chmod"; + } + return temp_file; + } + + Result write_storage_location_pb_file(std::string const& flag_val) { + auto temp_file = std::tmpnam(nullptr); + auto proto = storage_files(); + auto* info = proto.add_files(); + info->set_version(0); + info->set_container("system"); + info->set_package_map("some_package.map"); + info->set_flag_map("some_flag.map"); + info->set_flag_val(flag_val); + info->set_timestamp(12345); + + auto content = std::string(); + proto.SerializeToString(&content); + if (!WriteStringToFile(content, temp_file)) { + return Error() << "failed to write storage records pb file"; + } + return temp_file; + } + + void SetUp() override { + auto const test_dir = android::base::GetExecutableDirectory(); + flag_val = *copy_to_rw_temp_file(test_dir + "/flag.val"); + storage_record_pb = *write_storage_location_pb_file(flag_val); + } + + void TearDown() override { + std::remove(flag_val.c_str()); + std::remove(storage_record_pb.c_str()); + } + + std::string flag_val; + std::string storage_record_pb; +}; + +/// Negative test to lock down the error when mapping none exist storage files +TEST_F(AconfigStorageTest, test_none_exist_storage_file_mapping) { + auto mapped_file_result = private_api::get_mapped_flag_value_file_impl( + storage_record_pb, "vendor"); + ASSERT_FALSE(mapped_file_result.ok()); + ASSERT_EQ(mapped_file_result.error().message(), + "Unable to find storage files for container vendor"); +} + +/// Negative test to lock down the error when mapping a non writeable storage file +TEST_F(AconfigStorageTest, test_non_writable_storage_file_mapping) { + ASSERT_TRUE(chmod(flag_val.c_str(), S_IRUSR | S_IRGRP | S_IROTH) != -1); + auto mapped_file_result = private_api::get_mapped_flag_value_file_impl( + storage_record_pb, "system"); + ASSERT_FALSE(mapped_file_result.ok()); + ASSERT_EQ(mapped_file_result.error().message(), "cannot map nonwriteable file"); +} + +/// Test to lock down storage flag value update api +TEST_F(AconfigStorageTest, test_boolean_flag_value_update) { + auto mapped_file_result = private_api::get_mapped_flag_value_file_impl( + storage_record_pb, "system"); + ASSERT_TRUE(mapped_file_result.ok()); + auto mapped_file = *mapped_file_result; + + for (int offset = 0; offset < 8; ++offset) { + auto update_result = api::set_boolean_flag_value(mapped_file, offset, true); + ASSERT_TRUE(update_result.ok()); + auto ro_mapped_file = api::MappedStorageFile(); + ro_mapped_file.file_ptr = mapped_file.file_ptr; + ro_mapped_file.file_size = mapped_file.file_size; + auto value_query = api::get_boolean_flag_value(ro_mapped_file, offset); + ASSERT_TRUE(value_query.query_success); + ASSERT_TRUE(value_query.flag_value); + } +} + +/// Negative test to lock down the error when querying flag value out of range +TEST_F(AconfigStorageTest, test_invalid_boolean_flag_value_update) { + auto mapped_file_result = private_api::get_mapped_flag_value_file_impl( + storage_record_pb, "system"); + ASSERT_TRUE(mapped_file_result.ok()); + auto mapped_file = *mapped_file_result; + auto update_result = api::set_boolean_flag_value(mapped_file, 8, true); + ASSERT_FALSE(update_result.ok()); + ASSERT_EQ(update_result.error().message(), + std::string("InvalidStorageFileOffset(Flag value offset goes beyond the end of the file.)")); +}