blob: f20600d6664e0c0f729fcb1c842fbfbbd83e2345 [file] [log] [blame]
/*
* 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.
*/
//! `aconfig_storage_file` is a crate that defines aconfig storage file format, it
//! also includes apis to read flags from storage files. It provides three apis to
//! interface with storage files:
//!
//! 1, function to get package flag value start offset
//! pub fn get_package_offset(container: &str, package: &str) -> `Result<Option<PackageOffset>>>`
//!
//! 2, function to get flag offset within a specific package
//! pub fn get_flag_offset(container: &str, package_id: u32, flag: &str) -> `Result<Option<u16>>>`
//!
//! 3, function to get the actual flag value given the global offset (combined package and
//! flag offset).
//! pub fn get_boolean_flag_value(container: &str, offset: u32) -> `Result<bool>`
//!
//! Note these are low level apis that are expected to be only used in auto generated flag
//! apis. DO NOT DIRECTLY USE THESE APIS IN YOUR SOURCE CODE. For auto generated flag apis
//! please refer to the g3doc go/android-flags
pub mod flag_table;
pub mod flag_value;
pub mod mapped_file;
pub mod package_table;
pub mod protos;
#[cfg(test)]
mod test_utils;
use anyhow::anyhow;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
pub use crate::flag_table::{FlagOffset, FlagTable, FlagTableHeader, FlagTableNode};
pub use crate::flag_value::{FlagValueHeader, FlagValueList};
pub use crate::package_table::{PackageOffset, PackageTable, PackageTableHeader, PackageTableNode};
pub use crate::protos::ProtoStorageFiles;
use crate::AconfigStorageError::{BytesParseFail, HashTableSizeLimit};
/// Storage file version
pub const FILE_VERSION: u32 = 1;
/// Good hash table prime number
pub(crate) const HASH_PRIMES: [u32; 29] = [
7, 17, 29, 53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593, 49157, 98317, 196613, 393241,
786433, 1572869, 3145739, 6291469, 12582917, 25165843, 50331653, 100663319, 201326611,
402653189, 805306457, 1610612741,
];
/// Storage file location pb file
pub const STORAGE_LOCATION_FILE: &str = "/metadata/aconfig/available_storage_file_records.pb";
/// Storage file type enum
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum StorageFileSelection {
PackageMap,
FlagMap,
FlagVal,
}
impl TryFrom<&str> for StorageFileSelection {
type Error = anyhow::Error;
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
match value {
"package_map" => Ok(Self::PackageMap),
"flag_map" => Ok(Self::FlagMap),
"flag_val" => Ok(Self::FlagVal),
_ => Err(anyhow!("Invalid storage file to create")),
}
}
}
/// Get the right hash table size given number of entries in the table. Use a
/// load factor of 0.5 for performance.
pub fn get_table_size(entries: u32) -> Result<u32, AconfigStorageError> {
HASH_PRIMES
.iter()
.find(|&&num| num >= 2 * entries)
.copied()
.ok_or(HashTableSizeLimit(anyhow!("Number of items in a hash table exceeds limit")))
}
/// Get the corresponding bucket index given the key and number of buckets
pub(crate) fn get_bucket_index<T: Hash>(val: &T, num_buckets: u32) -> u32 {
let mut s = DefaultHasher::new();
val.hash(&mut s);
(s.finish() % num_buckets as u64) as u32
}
/// Read and parse bytes as u8
pub(crate) fn read_u8_from_bytes(buf: &[u8], head: &mut usize) -> Result<u8, AconfigStorageError> {
let val =
u8::from_le_bytes(buf[*head..*head + 1].try_into().map_err(|errmsg| {
BytesParseFail(anyhow!("fail to parse u8 from bytes: {}", errmsg))
})?);
*head += 1;
Ok(val)
}
/// Read and parse bytes as u16
pub(crate) fn read_u16_from_bytes(
buf: &[u8],
head: &mut usize,
) -> Result<u16, AconfigStorageError> {
let val =
u16::from_le_bytes(buf[*head..*head + 2].try_into().map_err(|errmsg| {
BytesParseFail(anyhow!("fail to parse u16 from bytes: {}", errmsg))
})?);
*head += 2;
Ok(val)
}
/// Read and parse bytes as u32
pub(crate) fn read_u32_from_bytes(
buf: &[u8],
head: &mut usize,
) -> Result<u32, AconfigStorageError> {
let val =
u32::from_le_bytes(buf[*head..*head + 4].try_into().map_err(|errmsg| {
BytesParseFail(anyhow!("fail to parse u32 from bytes: {}", errmsg))
})?);
*head += 4;
Ok(val)
}
/// Read and parse bytes as string
pub(crate) fn read_str_from_bytes(
buf: &[u8],
head: &mut usize,
) -> Result<String, AconfigStorageError> {
let num_bytes = read_u32_from_bytes(buf, head)? as usize;
let val = String::from_utf8(buf[*head..*head + num_bytes].to_vec())
.map_err(|errmsg| BytesParseFail(anyhow!("fail to parse string from bytes: {}", errmsg)))?;
*head += num_bytes;
Ok(val)
}
/// Storage query api error
#[non_exhaustive]
#[derive(thiserror::Error, Debug)]
pub enum AconfigStorageError {
#[error("failed to read the file")]
FileReadFail(#[source] anyhow::Error),
#[error("fail to parse protobuf")]
ProtobufParseFail(#[source] anyhow::Error),
#[error("storage files not found for this container")]
StorageFileNotFound(#[source] anyhow::Error),
#[error("fail to map storage file")]
MapFileFail(#[source] anyhow::Error),
#[error("number of items in hash table exceed limit")]
HashTableSizeLimit(#[source] anyhow::Error),
#[error("failed to parse bytes into data")]
BytesParseFail(#[source] anyhow::Error),
#[error("cannot parse storage files with a higher version")]
HigherStorageFileVersion(#[source] anyhow::Error),
#[error("invalid storage file byte offset")]
InvalidStorageFileOffset(#[source] anyhow::Error),
}
/// Get package start offset implementation
pub fn get_package_offset_impl(
pb_file: &str,
container: &str,
package: &str,
) -> Result<Option<PackageOffset>, AconfigStorageError> {
let mapped_file =
crate::mapped_file::get_mapped_file(pb_file, container, StorageFileSelection::PackageMap)?;
crate::package_table::find_package_offset(&mapped_file, package)
}
/// Get flag offset implementation
pub fn get_flag_offset_impl(
pb_file: &str,
container: &str,
package_id: u32,
flag: &str,
) -> Result<Option<FlagOffset>, AconfigStorageError> {
let mapped_file =
crate::mapped_file::get_mapped_file(pb_file, container, StorageFileSelection::FlagMap)?;
crate::flag_table::find_flag_offset(&mapped_file, package_id, flag)
}
/// Get boolean flag value implementation
pub fn get_boolean_flag_value_impl(
pb_file: &str,
container: &str,
offset: u32,
) -> Result<bool, AconfigStorageError> {
let mapped_file =
crate::mapped_file::get_mapped_file(pb_file, container, StorageFileSelection::FlagVal)?;
crate::flag_value::find_boolean_flag_value(&mapped_file, offset)
}
/// Get package start offset for flags given the container and package name.
///
/// This function would map the corresponding package map file if has not been mapped yet,
/// and then look for the target package in this mapped file.
///
/// If a package is found, it returns Ok(Some(PackageOffset))
/// If a package is not found, it returns Ok(None)
/// If errors out such as no such package map file is found, it returns an Err(errmsg)
pub fn get_package_offset(
container: &str,
package: &str,
) -> Result<Option<PackageOffset>, AconfigStorageError> {
get_package_offset_impl(STORAGE_LOCATION_FILE, container, package)
}
/// Get flag offset within a package given the container name, package id and flag name.
///
/// This function would map the corresponding flag map file if has not been mapped yet,
/// and then look for the target flag in this mapped file.
///
/// If a flag is found, it returns Ok(Some(u16))
/// If a flag is not found, it returns Ok(None)
/// If errors out such as no such flag map file is found, it returns an Err(errmsg)
pub fn get_flag_offset(
container: &str,
package_id: u32,
flag: &str,
) -> Result<Option<FlagOffset>, AconfigStorageError> {
get_flag_offset_impl(STORAGE_LOCATION_FILE, container, package_id, flag)
}
/// Get the boolean flag value given the container name and flag global offset
///
/// This function would map the corresponding flag value file if has not been mapped yet,
/// and then look for the target flag value at the specified offset.
///
/// If flag value file is successfully mapped and the provide offset is valid, it returns
/// the boolean flag value, otherwise it returns the error message.
pub fn get_boolean_flag_value(container: &str, offset: u32) -> Result<bool, AconfigStorageError> {
get_boolean_flag_value_impl(STORAGE_LOCATION_FILE, container, offset)
}
#[cxx::bridge]
mod ffi {
// Package table query return for cc interlop
pub struct PackageOffsetQueryCXX {
pub query_success: bool,
pub error_message: String,
pub package_exists: bool,
pub package_id: u32,
pub boolean_offset: u32,
}
// Flag table query return for cc interlop
pub struct FlagOffsetQueryCXX {
pub query_success: bool,
pub error_message: String,
pub flag_exists: bool,
pub flag_offset: u16,
}
// Flag value query return for cc interlop
pub struct BooleanFlagValueQueryCXX {
pub query_success: bool,
pub error_message: String,
pub flag_value: bool,
}
// Rust export to c++
extern "Rust" {
pub fn get_package_offset_cxx_impl(
pb_file: &str,
container: &str,
package: &str,
) -> PackageOffsetQueryCXX;
pub fn get_flag_offset_cxx_impl(
pb_file: &str,
container: &str,
package_id: u32,
flag: &str,
) -> FlagOffsetQueryCXX;
pub fn get_boolean_flag_value_cxx_impl(
pb_file: &str,
container: &str,
offset: u32,
) -> BooleanFlagValueQueryCXX;
pub fn get_package_offset_cxx(container: &str, package: &str) -> PackageOffsetQueryCXX;
pub fn get_flag_offset_cxx(
container: &str,
package_id: u32,
flag: &str,
) -> FlagOffsetQueryCXX;
pub fn get_boolean_flag_value_cxx(container: &str, offset: u32)
-> BooleanFlagValueQueryCXX;
}
}
/// Get package start offset impl cc interlop
pub fn get_package_offset_cxx_impl(
pb_file: &str,
container: &str,
package: &str,
) -> ffi::PackageOffsetQueryCXX {
ffi::PackageOffsetQueryCXX::new(get_package_offset_impl(pb_file, container, package))
}
/// Get flag start offset impl cc interlop
pub fn get_flag_offset_cxx_impl(
pb_file: &str,
container: &str,
package_id: u32,
flag: &str,
) -> ffi::FlagOffsetQueryCXX {
ffi::FlagOffsetQueryCXX::new(get_flag_offset_impl(pb_file, container, package_id, flag))
}
/// Get boolean flag value impl cc interlop
pub fn get_boolean_flag_value_cxx_impl(
pb_file: &str,
container: &str,
offset: u32,
) -> ffi::BooleanFlagValueQueryCXX {
ffi::BooleanFlagValueQueryCXX::new(get_boolean_flag_value_impl(pb_file, container, offset))
}
/// Get package start offset cc interlop
pub fn get_package_offset_cxx(container: &str, package: &str) -> ffi::PackageOffsetQueryCXX {
ffi::PackageOffsetQueryCXX::new(get_package_offset(container, package))
}
/// Get flag start offset cc interlop
pub fn get_flag_offset_cxx(
container: &str,
package_id: u32,
flag: &str,
) -> ffi::FlagOffsetQueryCXX {
ffi::FlagOffsetQueryCXX::new(get_flag_offset(container, package_id, flag))
}
/// Get boolean flag value cc interlop
pub fn get_boolean_flag_value_cxx(container: &str, offset: u32) -> ffi::BooleanFlagValueQueryCXX {
ffi::BooleanFlagValueQueryCXX::new(get_boolean_flag_value(container, offset))
}
impl ffi::PackageOffsetQueryCXX {
pub(crate) fn new(offset_result: Result<Option<PackageOffset>, AconfigStorageError>) -> Self {
match offset_result {
Ok(offset_opt) => match offset_opt {
Some(offset) => Self {
query_success: true,
error_message: String::from(""),
package_exists: true,
package_id: offset.package_id,
boolean_offset: offset.boolean_offset,
},
None => Self {
query_success: true,
error_message: String::from(""),
package_exists: false,
package_id: 0,
boolean_offset: 0,
},
},
Err(errmsg) => Self {
query_success: false,
error_message: format!("{:?}", errmsg),
package_exists: false,
package_id: 0,
boolean_offset: 0,
},
}
}
}
impl ffi::FlagOffsetQueryCXX {
pub(crate) fn new(offset_result: Result<Option<FlagOffset>, AconfigStorageError>) -> Self {
match offset_result {
Ok(offset_opt) => match offset_opt {
Some(offset) => Self {
query_success: true,
error_message: String::from(""),
flag_exists: true,
flag_offset: offset,
},
None => Self {
query_success: true,
error_message: String::from(""),
flag_exists: false,
flag_offset: 0,
},
},
Err(errmsg) => Self {
query_success: false,
error_message: format!("{:?}", errmsg),
flag_exists: false,
flag_offset: 0,
},
}
}
}
impl ffi::BooleanFlagValueQueryCXX {
pub(crate) fn new(value_result: Result<bool, AconfigStorageError>) -> Self {
match value_result {
Ok(value) => {
Self { query_success: true, error_message: String::from(""), flag_value: value }
}
Err(errmsg) => Self {
query_success: false,
error_message: format!("{:?}", errmsg),
flag_value: false,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::{write_storage_text_to_temp_file, TestStorageFileSet};
fn create_test_storage_files(read_only: bool) -> TestStorageFileSet {
TestStorageFileSet::new(
"./tests/package.map",
"./tests/flag.map",
"./tests/flag.val",
read_only,
)
.unwrap()
}
#[test]
// this test point locks down flag package offset query
fn test_package_offset_query() {
let ro_files = create_test_storage_files(true);
let text_proto = format!(
r#"
files {{
version: 0
container: "system"
package_map: "{}"
flag_map: "{}"
flag_val: "{}"
timestamp: 12345
}}
"#,
ro_files.package_map.name, ro_files.flag_map.name, ro_files.flag_val.name
);
let file = write_storage_text_to_temp_file(&text_proto).unwrap();
let file_full_path = file.path().display().to_string();
let package_offset = get_package_offset_impl(
&file_full_path,
"system",
"com.android.aconfig.storage.test_1",
)
.unwrap()
.unwrap();
let expected_package_offset = PackageOffset { package_id: 0, boolean_offset: 0 };
assert_eq!(package_offset, expected_package_offset);
let package_offset = get_package_offset_impl(
&file_full_path,
"system",
"com.android.aconfig.storage.test_2",
)
.unwrap()
.unwrap();
let expected_package_offset = PackageOffset { package_id: 1, boolean_offset: 3 };
assert_eq!(package_offset, expected_package_offset);
let package_offset = get_package_offset_impl(
&file_full_path,
"system",
"com.android.aconfig.storage.test_4",
)
.unwrap()
.unwrap();
let expected_package_offset = PackageOffset { package_id: 2, boolean_offset: 6 };
assert_eq!(package_offset, expected_package_offset);
}
#[test]
// this test point locks down flag offset query
fn test_flag_offset_query() {
let ro_files = create_test_storage_files(true);
let text_proto = format!(
r#"
files {{
version: 0
container: "system"
package_map: "{}"
flag_map: "{}"
flag_val: "{}"
timestamp: 12345
}}
"#,
ro_files.package_map.name, ro_files.flag_map.name, ro_files.flag_val.name
);
let file = write_storage_text_to_temp_file(&text_proto).unwrap();
let file_full_path = file.path().display().to_string();
let baseline = vec![
(0, "enabled_ro", 1u16),
(0, "enabled_rw", 2u16),
(1, "disabled_ro", 0u16),
(2, "enabled_ro", 1u16),
(1, "enabled_fixed_ro", 1u16),
(1, "enabled_ro", 2u16),
(2, "enabled_fixed_ro", 0u16),
(0, "disabled_rw", 0u16),
];
for (package_id, flag_name, expected_offset) in baseline.into_iter() {
let flag_offset =
get_flag_offset_impl(&file_full_path, "system", package_id, flag_name)
.unwrap()
.unwrap();
assert_eq!(flag_offset, expected_offset);
}
}
#[test]
// this test point locks down flag offset query
fn test_flag_value_query() {
let ro_files = create_test_storage_files(true);
let text_proto = format!(
r#"
files {{
version: 0
container: "system"
package_map: "{}"
flag_map: "{}"
flag_val: "{}"
timestamp: 12345
}}
"#,
ro_files.package_map.name, ro_files.flag_map.name, ro_files.flag_val.name
);
let file = write_storage_text_to_temp_file(&text_proto).unwrap();
let file_full_path = file.path().display().to_string();
let baseline: Vec<bool> = vec![false; 8];
for (offset, expected_value) in baseline.into_iter().enumerate() {
let flag_value =
get_boolean_flag_value_impl(&file_full_path, "system", offset as u32).unwrap();
assert_eq!(flag_value, expected_value);
}
}
}