use fidl_fuchsia_diagnostics::Selector;
use fuchsia_inspect as inspect;
use selectors::{contains_recursive_glob, parse_selector_file, FastError};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
static DISABLE_FILTER_FILE_NAME: &str = "DISABLE_FILTERING.txt";
pub struct PipelineConfig {
inspect_configs: BTreeMap<PathBuf, usize>,
inspect_selectors: Option<Vec<Selector>>,
errors: Vec<String>,
pub disable_filtering: bool,
}
#[derive(PartialEq)]
pub enum EmptyBehavior {
Disable,
DoNotFilter,
}
impl PipelineConfig {
pub fn from_directory(dir: impl AsRef<Path>, empty_behavior: EmptyBehavior) -> Self {
let suffix = std::ffi::OsStr::new("cfg");
let disable_filter_file_name = std::ffi::OsStr::new(DISABLE_FILTER_FILE_NAME);
let mut inspect_configs = BTreeMap::new();
let mut inspect_selectors = Some(vec![]);
let mut errors = vec![];
let mut disable_filtering = false;
let readdir = dir.as_ref().read_dir();
match readdir {
Err(_) => {
errors.push(format!("Failed to read directory {}", dir.as_ref().to_string_lossy()));
}
Ok(mut readdir) => {
while let Some(Ok(entry)) = readdir.next() {
let path = entry.path();
if path.extension() == Some(suffix) {
match parse_selector_file::<FastError>(&path) {
Ok(selectors) => {
let mut validated_selectors = vec![];
for selector in selectors.into_iter() {
match validate_static_selector(&selector) {
Ok(()) => validated_selectors.push(selector),
Err(e) => {
errors.push(format!("Invalid static selector: {e}"))
}
}
}
inspect_configs.insert(path, validated_selectors.len());
inspect_selectors.as_mut().unwrap().extend(validated_selectors);
}
Err(e) => {
errors.push(format!(
"Failed to parse {}: {}",
path.to_string_lossy(),
e
));
}
}
} else if path.file_name() == Some(disable_filter_file_name) {
disable_filtering = true;
}
}
}
}
if inspect_configs.is_empty() && empty_behavior == EmptyBehavior::DoNotFilter {
inspect_selectors = None;
disable_filtering = true;
}
Self { inspect_configs, inspect_selectors, errors, disable_filtering }
}
pub fn take_inspect_selectors(&mut self) -> Option<Vec<Selector>> {
self.inspect_selectors.take()
}
pub fn record_to_inspect(&self, node: &inspect::Node) {
node.record_bool("filtering_enabled", !self.disable_filtering);
let files = node.create_child("config_files");
let mut selector_sum = 0;
for (name, count) in self.inspect_configs.iter() {
let c = files.create_child(name.file_stem().unwrap_or_default().to_string_lossy());
c.record_uint("selector_count", *count as u64);
files.record(c);
selector_sum += count;
}
node.record(files);
node.record_uint("selector_count", selector_sum as u64);
if !self.errors.is_empty() {
let errors = node.create_child("errors");
for (i, error) in self.errors.iter().enumerate() {
let error_node = errors.create_child(format!("{i}"));
error_node.record_string("message", error);
errors.record(error_node);
}
node.record(errors);
}
}
}
fn validate_static_selector(static_selector: &Selector) -> Result<(), String> {
match static_selector.component_selector.as_ref() {
Some(selector) if contains_recursive_glob(selector) => {
Err("Recursive glob not allowed in static selector configs".to_string())
}
Some(_) => Ok(()),
None => Err("A selector does not contain a component selector".to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use diagnostics_assertions::{assert_data_tree, AnyProperty};
use std::fs;
#[fuchsia::test]
fn parse_missing_pipeline() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(config_path).unwrap();
let config = PipelineConfig::from_directory("config/missing", EmptyBehavior::Disable);
let inspector = inspect::Inspector::default();
config.record_to_inspect(inspector.root());
assert_data_tree!(inspector, root: {
filtering_enabled: true,
selector_count: 0u64,
errors: {
"0": {
message: "Failed to read directory config/missing"
}
},
config_files: {}
});
assert!(!config.disable_filtering);
}
#[fuchsia::test]
fn parse_partially_valid_pipeline() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
fs::write(config_path.join("ok.cfg"), "my_component:root:status").unwrap();
fs::write(config_path.join("ignored.txt"), "This file is ignored").unwrap();
fs::write(config_path.join("bad.cfg"), "This file fails to parse").unwrap();
let mut config = PipelineConfig::from_directory(&config_path, EmptyBehavior::Disable);
let inspector = inspect::Inspector::default();
config.record_to_inspect(inspector.root());
assert_data_tree!(inspector, root: {
filtering_enabled: true,
selector_count: 1u64,
errors: {
"0": {
message: AnyProperty
}
},
config_files: {
ok: {
selector_count: 1u64
},
}
});
assert!(!config.disable_filtering);
assert_eq!(1, config.take_inspect_selectors().unwrap_or_default().len());
}
#[fuchsia::test]
fn parse_valid_pipeline() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
fs::write(config_path.join("ok.cfg"), "my_component:root:status").unwrap();
fs::write(config_path.join("ignored.txt"), "This file is ignored").unwrap();
fs::write(config_path.join("also_ok.cfg"), "my_component:root:a\nmy_component:root/b:c\n")
.unwrap();
let mut config = PipelineConfig::from_directory(&config_path, EmptyBehavior::Disable);
let inspector = inspect::Inspector::default();
config.record_to_inspect(inspector.root());
assert_data_tree!(inspector, root: {
filtering_enabled: true,
selector_count: 3u64,
config_files: {
ok: {
selector_count: 1u64,
},
also_ok: {
selector_count: 2u64,
}
}
});
assert!(!config.disable_filtering);
assert_eq!(3, config.take_inspect_selectors().unwrap_or_default().len());
}
#[fuchsia::test]
fn parse_allow_empty_pipeline() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
let mut config = PipelineConfig::from_directory(&config_path, EmptyBehavior::DoNotFilter);
let inspector = inspect::Inspector::default();
config.record_to_inspect(inspector.root());
assert_data_tree!(inspector, root: {
filtering_enabled: false,
selector_count: 0u64,
config_files: {
}
});
assert!(config.disable_filtering);
assert_eq!(None, config.take_inspect_selectors());
}
#[fuchsia::test]
fn parse_disabled_valid_pipeline() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
fs::write(config_path.join("DISABLE_FILTERING.txt"), "This file disables filtering.")
.unwrap();
fs::write(config_path.join("ok.cfg"), "my_component:root:status").unwrap();
fs::write(config_path.join("ignored.txt"), "This file is ignored").unwrap();
fs::write(config_path.join("also_ok.cfg"), "my_component:root:a\nmy_component:root/b:c\n")
.unwrap();
let mut config = PipelineConfig::from_directory(&config_path, EmptyBehavior::Disable);
let inspector = inspect::Inspector::default();
config.record_to_inspect(inspector.root());
assert_data_tree!(inspector, root: {
filtering_enabled: false,
selector_count: 3u64,
config_files: {
ok: {
selector_count: 1u64,
},
also_ok: {
selector_count: 2u64,
}
}
});
assert!(config.disable_filtering);
assert_eq!(3, config.take_inspect_selectors().unwrap_or_default().len());
}
#[fuchsia::test]
fn parse_pipeline_disallow_recursive_glob() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
fs::write(config_path.join("glob.cfg"), "core/a/**:root:status").unwrap();
fs::write(config_path.join("ok.cfg"), "core/b:root:status").unwrap();
let mut config = PipelineConfig::from_directory(&config_path, EmptyBehavior::Disable);
let inspector = inspect::Inspector::default();
config.record_to_inspect(inspector.root());
assert_data_tree!(inspector, root: {
filtering_enabled: true,
selector_count: 1u64,
errors: {
"0": {
message: AnyProperty
}
},
config_files: {
ok: {
selector_count: 1u64,
},
glob: {
selector_count: 0u64,
}
}
});
assert!(!config.disable_filtering);
assert_eq!(1, config.take_inspect_selectors().unwrap_or_default().len());
}
}