blob: cf6ab28e8b87e6ea82cb407e88255b168e66ce72 [file] [log] [blame]
/*
* 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 crate::load_protos;
use crate::{Flag, FlagSource, FlagValue, ValuePickedFrom};
use anyhow::{anyhow, bail, Result};
use regex::Regex;
use std::collections::HashMap;
use std::process::Command;
use std::str;
pub struct DeviceConfigSource {}
fn parse_device_config(raw: &str) -> Result<HashMap<String, FlagValue>> {
let mut flags = HashMap::new();
let regex = Regex::new(r"(?m)^([[[:alnum:]]_]+/[[[:alnum:]]_\.]+)=(true|false)$")?;
for capture in regex.captures_iter(raw) {
let key =
capture.get(1).ok_or(anyhow!("invalid device_config output"))?.as_str().to_string();
let value = FlagValue::try_from(
capture.get(2).ok_or(anyhow!("invalid device_config output"))?.as_str(),
)?;
flags.insert(key, value);
}
Ok(flags)
}
fn read_device_config_output(command: &[&str]) -> Result<String> {
let output = Command::new("/system/bin/device_config").args(command).output()?;
if !output.status.success() {
let reason = match output.status.code() {
Some(code) => {
let output = str::from_utf8(&output.stdout)?;
if !output.is_empty() {
format!("exit code {code}, output was {output}")
} else {
format!("exit code {code}")
}
}
None => "terminated by signal".to_string(),
};
bail!("failed to access flag storage: {}", reason);
}
Ok(str::from_utf8(&output.stdout)?.to_string())
}
fn read_device_config_flags() -> Result<HashMap<String, FlagValue>> {
let list_output = read_device_config_output(&["list"])?;
parse_device_config(&list_output)
}
/// Parse the list of newline-separated staged flags.
///
/// The output is a newline-sepaarated list of entries which follow this format:
/// `namespace*flagname=value`
///
/// The resulting map maps from `namespace/flagname` to `value`, if a staged flag exists for
/// `namespace/flagname`.
fn parse_staged_flags(raw: &str) -> Result<HashMap<String, FlagValue>> {
let mut flags = HashMap::new();
for line in raw.split('\n') {
match (line.find('*'), line.find('=')) {
(Some(star_index), Some(equal_index)) => {
let namespace = &line[..star_index];
let flag = &line[star_index + 1..equal_index];
if let Ok(value) = FlagValue::try_from(&line[equal_index + 1..]) {
flags.insert(namespace.to_owned() + "/" + flag, value);
}
}
_ => continue,
};
}
Ok(flags)
}
fn read_staged_flags() -> Result<HashMap<String, FlagValue>> {
let staged_flags_output = read_device_config_output(&["list", "staged"])?;
parse_staged_flags(&staged_flags_output)
}
fn reconcile(
pb_flags: &[Flag],
dc_flags: HashMap<String, FlagValue>,
staged_flags: HashMap<String, FlagValue>,
) -> Vec<Flag> {
pb_flags
.iter()
.map(|f| {
let server_override = dc_flags.get(&format!("{}/{}", f.namespace, f.qualified_name()));
let (value_picked_from, selected_value) = match server_override {
Some(value) if *value != f.value => (ValuePickedFrom::Server, *value),
_ => (ValuePickedFrom::Default, f.value),
};
Flag { value_picked_from, value: selected_value, ..f.clone() }
})
.map(|f| {
let staged_value = staged_flags
.get(&format!("{}/{}", f.namespace, f.qualified_name()))
.map(|value| if *value != f.value { Some(*value) } else { None })
.unwrap_or(None);
Flag { staged_value, ..f }
})
.collect()
}
impl FlagSource for DeviceConfigSource {
fn list_flags() -> Result<Vec<Flag>> {
let pb_flags = load_protos::load()?;
let dc_flags = read_device_config_flags()?;
let staged_flags = read_staged_flags()?;
let flags = reconcile(&pb_flags, dc_flags, staged_flags);
Ok(flags)
}
fn override_flag(namespace: &str, qualified_name: &str, value: &str) -> Result<()> {
read_device_config_output(&["put", namespace, qualified_name, value]).map(|_| ())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_device_config() {
let input = r#"
namespace_one/com.foo.bar.flag_one=true
namespace_one/com.foo.bar.flag_two=false
random_noise;
namespace_two/android.flag_one=true
namespace_two/android.flag_two=nonsense
"#;
let expected = HashMap::from([
("namespace_one/com.foo.bar.flag_one".to_string(), FlagValue::Enabled),
("namespace_one/com.foo.bar.flag_two".to_string(), FlagValue::Disabled),
("namespace_two/android.flag_one".to_string(), FlagValue::Enabled),
]);
let actual = parse_device_config(input).unwrap();
assert_eq!(expected, actual);
}
}