/* * 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); } }