blob: 202f6a465d118875cd235474d83dc0425a5125fd [file]
/*
* 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 package_table;
pub mod protos;
#[cfg(test)]
mod test_utils;
use anyhow::anyhow;
use std::collections::hash_map::DefaultHasher;
use std::fs::File;
use std::hash::{Hash, Hasher};
use std::io::Read;
pub use crate::flag_table::{FlagTable, FlagTableHeader, FlagTableNode};
pub use crate::flag_value::{FlagValueHeader, FlagValueList};
pub use crate::package_table::{PackageTable, PackageTableHeader, PackageTableNode};
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 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 type, valid types are package_map|flag_map|flag_val]"
)),
}
}
}
/// 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 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 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),
#[error("failed to create file")]
FileCreationFail(#[source] anyhow::Error),
}
/// Read in storage file as bytes
pub fn read_file_to_bytes(file_path: &str) -> Result<Vec<u8>, AconfigStorageError> {
let mut file = File::open(file_path).map_err(|errmsg| {
AconfigStorageError::FileReadFail(anyhow!("Failed to open file {}: {}", file_path, errmsg))
})?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).map_err(|errmsg| {
AconfigStorageError::FileReadFail(anyhow!(
"Failed to read 4 bytes from file {}: {}",
file_path,
errmsg
))
})?;
Ok(buffer)
}
/// List flag values from storage files
pub fn list_flags(
package_map: &str,
flag_map: &str,
flag_val: &str,
) -> Result<Vec<(String, bool)>, AconfigStorageError> {
let package_table = PackageTable::from_bytes(&read_file_to_bytes(package_map)?)?;
let flag_table = FlagTable::from_bytes(&read_file_to_bytes(flag_map)?)?;
let flag_value_list = FlagValueList::from_bytes(&read_file_to_bytes(flag_val)?)?;
let mut package_info = vec![("", 0); package_table.header.num_packages as usize];
for node in package_table.nodes.iter() {
package_info[node.package_id as usize] = (&node.package_name, node.boolean_offset);
}
let mut flags = Vec::new();
for node in flag_table.nodes.iter() {
let (package_name, package_offset) = package_info[node.package_id as usize];
let full_flag_name = String::from(package_name) + "/" + &node.flag_name;
let flag_offset = package_offset + node.flag_id as u32;
let flag_value = flag_value_list.booleans[flag_offset as usize];
flags.push((full_flag_name, flag_value));
}
flags.sort_by(|v1, v2| v1.0.cmp(&v2.0));
Ok(flags)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::{
create_test_flag_table, create_test_flag_value_list, create_test_package_table,
write_bytes_to_temp_file,
};
#[test]
// this test point locks down the flag list api
fn test_list_flag() {
let package_table =
write_bytes_to_temp_file(&create_test_package_table().as_bytes()).unwrap();
let flag_table = write_bytes_to_temp_file(&create_test_flag_table().as_bytes()).unwrap();
let flag_value_list =
write_bytes_to_temp_file(&create_test_flag_value_list().as_bytes()).unwrap();
let package_table_path = package_table.path().display().to_string();
let flag_table_path = flag_table.path().display().to_string();
let flag_value_list_path = flag_value_list.path().display().to_string();
let flags =
list_flags(&package_table_path, &flag_table_path, &flag_value_list_path).unwrap();
let expected = [
(String::from("com.android.aconfig.storage.test_1/disabled_rw"), false),
(String::from("com.android.aconfig.storage.test_1/enabled_ro"), true),
(String::from("com.android.aconfig.storage.test_1/enabled_rw"), false),
(String::from("com.android.aconfig.storage.test_2/disabled_ro"), false),
(String::from("com.android.aconfig.storage.test_2/enabled_fixed_ro"), true),
(String::from("com.android.aconfig.storage.test_2/enabled_ro"), true),
(String::from("com.android.aconfig.storage.test_4/enabled_fixed_ro"), false),
(String::from("com.android.aconfig.storage.test_4/enabled_ro"), true),
];
assert_eq!(flags, expected);
}
}