1use fidl_fuchsia_diagnostics::Selector;
6use fuchsia_inspect as inspect;
7use selectors::{contains_recursive_glob, parse_selector_file, FastError};
8use std::collections::BTreeMap;
9use std::path::{Path, PathBuf};
10
11static DISABLE_FILTER_FILE_NAME: &str = "DISABLE_FILTERING.txt";
12
13pub struct PipelineConfig {
15 inspect_configs: BTreeMap<PathBuf, usize>,
18
19 inspect_selectors: Option<Vec<Selector>>,
21
22 errors: Vec<String>,
24
25 pub disable_filtering: bool,
29}
30
31#[derive(PartialEq)]
33pub enum EmptyBehavior {
34 Disable,
36 DoNotFilter,
38}
39
40impl PipelineConfig {
41 pub fn from_directory(dir: impl AsRef<Path>, empty_behavior: EmptyBehavior) -> Self {
46 let suffix = std::ffi::OsStr::new("cfg");
47 let disable_filter_file_name = std::ffi::OsStr::new(DISABLE_FILTER_FILE_NAME);
48 let mut inspect_configs = BTreeMap::new();
49 let mut inspect_selectors = Some(vec![]);
50 let mut errors = vec![];
51 let mut disable_filtering = false;
52
53 let readdir = dir.as_ref().read_dir();
54
55 match readdir {
56 Err(_) => {
57 errors.push(format!("Failed to read directory {}", dir.as_ref().to_string_lossy()));
58 }
59 Ok(mut readdir) => {
60 while let Some(Ok(entry)) = readdir.next() {
61 let path = entry.path();
62 if path.extension() == Some(suffix) {
63 match parse_selector_file::<FastError>(&path) {
64 Ok(selectors) => {
65 let mut validated_selectors = vec![];
66 for selector in selectors.into_iter() {
67 match validate_static_selector(&selector) {
68 Ok(()) => validated_selectors.push(selector),
69 Err(e) => {
70 errors.push(format!("Invalid static selector: {e}"))
71 }
72 }
73 }
74 inspect_configs.insert(path, validated_selectors.len());
75 inspect_selectors.as_mut().unwrap().extend(validated_selectors);
76 }
77 Err(e) => {
78 errors.push(format!(
79 "Failed to parse {}: {}",
80 path.to_string_lossy(),
81 e
82 ));
83 }
84 }
85 } else if path.file_name() == Some(disable_filter_file_name) {
86 disable_filtering = true;
87 }
88 }
89 }
90 }
91
92 if inspect_configs.is_empty() && empty_behavior == EmptyBehavior::DoNotFilter {
93 inspect_selectors = None;
94 disable_filtering = true;
95 }
96
97 Self { inspect_configs, inspect_selectors, errors, disable_filtering }
98 }
99
100 pub fn take_inspect_selectors(&mut self) -> Option<Vec<Selector>> {
102 self.inspect_selectors.take()
103 }
104
105 pub fn record_to_inspect(&self, node: &inspect::Node) {
107 node.record_bool("filtering_enabled", !self.disable_filtering);
108 let files = node.create_child("config_files");
109 let mut selector_sum = 0;
110 for (name, count) in self.inspect_configs.iter() {
111 let c = files.create_child(name.file_stem().unwrap_or_default().to_string_lossy());
112 c.record_uint("selector_count", *count as u64);
113 files.record(c);
114 selector_sum += count;
115 }
116 node.record(files);
117 node.record_uint("selector_count", selector_sum as u64);
118
119 if !self.errors.is_empty() {
120 let errors = node.create_child("errors");
121 for (i, error) in self.errors.iter().enumerate() {
122 let error_node = errors.create_child(format!("{i}"));
123 error_node.record_string("message", error);
124 errors.record(error_node);
125 }
126 node.record(errors);
127 }
128 }
129}
130
131fn validate_static_selector(static_selector: &Selector) -> Result<(), String> {
135 match static_selector.component_selector.as_ref() {
136 Some(selector) if contains_recursive_glob(selector) => {
137 Err("Recursive glob not allowed in static selector configs".to_string())
138 }
139 Some(_) => Ok(()),
140 None => Err("A selector does not contain a component selector".to_string()),
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use diagnostics_assertions::{assert_data_tree, AnyProperty};
148 use std::fs;
149
150 #[fuchsia::test]
151 fn parse_missing_pipeline() {
152 let dir = tempfile::tempdir().unwrap();
153 let config_path = dir.path().join("config");
154 fs::create_dir(config_path).unwrap();
155
156 let config = PipelineConfig::from_directory("config/missing", EmptyBehavior::Disable);
157
158 let inspector = inspect::Inspector::default();
159 config.record_to_inspect(inspector.root());
160 assert_data_tree!(inspector, root: {
161 filtering_enabled: true,
162 selector_count: 0u64,
163 errors: {
164 "0": {
165 message: "Failed to read directory config/missing"
166 }
167 },
168 config_files: {}
169 });
170 assert!(!config.disable_filtering);
171 }
172
173 #[fuchsia::test]
174 fn parse_partially_valid_pipeline() {
175 let dir = tempfile::tempdir().unwrap();
176 let config_path = dir.path().join("config");
177 fs::create_dir(&config_path).unwrap();
178 fs::write(config_path.join("ok.cfg"), "my_component:root:status").unwrap();
179 fs::write(config_path.join("ignored.txt"), "This file is ignored").unwrap();
180 fs::write(config_path.join("bad.cfg"), "This file fails to parse").unwrap();
181
182 let mut config = PipelineConfig::from_directory(&config_path, EmptyBehavior::Disable);
183
184 let inspector = inspect::Inspector::default();
185 config.record_to_inspect(inspector.root());
186 assert_data_tree!(inspector, root: {
187 filtering_enabled: true,
188 selector_count: 1u64,
189 errors: {
190 "0": {
191 message: AnyProperty
192 }
193 },
194 config_files: {
195 ok: {
196 selector_count: 1u64
197 },
198 }
199 });
200
201 assert!(!config.disable_filtering);
202 assert_eq!(1, config.take_inspect_selectors().unwrap_or_default().len());
203 }
204
205 #[fuchsia::test]
206 fn parse_valid_pipeline() {
207 let dir = tempfile::tempdir().unwrap();
208 let config_path = dir.path().join("config");
209 fs::create_dir(&config_path).unwrap();
210 fs::write(config_path.join("ok.cfg"), "my_component:root:status").unwrap();
211 fs::write(config_path.join("ignored.txt"), "This file is ignored").unwrap();
212 fs::write(config_path.join("also_ok.cfg"), "my_component:root:a\nmy_component:root/b:c\n")
213 .unwrap();
214
215 let mut config = PipelineConfig::from_directory(&config_path, EmptyBehavior::Disable);
216
217 let inspector = inspect::Inspector::default();
218 config.record_to_inspect(inspector.root());
219 assert_data_tree!(inspector, root: {
220 filtering_enabled: true,
221 selector_count: 3u64,
222 config_files: {
223 ok: {
224 selector_count: 1u64,
225 },
226 also_ok: {
227 selector_count: 2u64,
228 }
229 }
230 });
231
232 assert!(!config.disable_filtering);
233 assert_eq!(3, config.take_inspect_selectors().unwrap_or_default().len());
234 }
235
236 #[fuchsia::test]
237 fn parse_allow_empty_pipeline() {
238 let dir = tempfile::tempdir().unwrap();
240 let config_path = dir.path().join("config");
241 fs::create_dir(&config_path).unwrap();
242
243 let mut config = PipelineConfig::from_directory(&config_path, EmptyBehavior::DoNotFilter);
244
245 let inspector = inspect::Inspector::default();
246 config.record_to_inspect(inspector.root());
247 assert_data_tree!(inspector, root: {
248 filtering_enabled: false,
249 selector_count: 0u64,
250 config_files: {
251 }
252 });
253
254 assert!(config.disable_filtering);
255 assert_eq!(None, config.take_inspect_selectors());
256 }
257
258 #[fuchsia::test]
259 fn parse_disabled_valid_pipeline() {
260 let dir = tempfile::tempdir().unwrap();
261 let config_path = dir.path().join("config");
262 fs::create_dir(&config_path).unwrap();
263 fs::write(config_path.join("DISABLE_FILTERING.txt"), "This file disables filtering.")
264 .unwrap();
265 fs::write(config_path.join("ok.cfg"), "my_component:root:status").unwrap();
266 fs::write(config_path.join("ignored.txt"), "This file is ignored").unwrap();
267 fs::write(config_path.join("also_ok.cfg"), "my_component:root:a\nmy_component:root/b:c\n")
268 .unwrap();
269
270 let mut config = PipelineConfig::from_directory(&config_path, EmptyBehavior::Disable);
271
272 let inspector = inspect::Inspector::default();
273 config.record_to_inspect(inspector.root());
274 assert_data_tree!(inspector, root: {
275 filtering_enabled: false,
276 selector_count: 3u64,
277 config_files: {
278 ok: {
279 selector_count: 1u64,
280 },
281 also_ok: {
282 selector_count: 2u64,
283 }
284 }
285 });
286
287 assert!(config.disable_filtering);
288 assert_eq!(3, config.take_inspect_selectors().unwrap_or_default().len());
289 }
290
291 #[fuchsia::test]
292 fn parse_pipeline_disallow_recursive_glob() {
293 let dir = tempfile::tempdir().unwrap();
294 let config_path = dir.path().join("config");
295 fs::create_dir(&config_path).unwrap();
296 fs::write(config_path.join("glob.cfg"), "core/a/**:root:status").unwrap();
297 fs::write(config_path.join("ok.cfg"), "core/b:root:status").unwrap();
298
299 let mut config = PipelineConfig::from_directory(&config_path, EmptyBehavior::Disable);
300
301 let inspector = inspect::Inspector::default();
302 config.record_to_inspect(inspector.root());
303 assert_data_tree!(inspector, root: {
304 filtering_enabled: true,
305 selector_count: 1u64,
306 errors: {
307 "0": {
308 message: AnyProperty
309 }
310 },
311 config_files: {
312 ok: {
313 selector_count: 1u64,
314 },
315 glob: {
316 selector_count: 0u64,
317 }
318 }
319 });
320
321 assert!(!config.disable_filtering);
322 assert_eq!(1, config.take_inspect_selectors().unwrap_or_default().len());
323 }
324}