1use crate::error::Error;
6use anyhow::anyhow;
7use diagnostics_hierarchy::HierarchyMatcher;
8use fidl_fuchsia_diagnostics::{Selector, TreeNames};
9use fidl_fuchsia_inspect::DEFAULT_TREE_NAME;
10use moniker::ExtendedMoniker;
11use selectors::SelectorExt;
12use std::collections::HashMap;
13use std::sync::Arc;
14
15#[derive(Debug, Clone)]
16enum ComponentAllowlistState {
17 FilteringEnabled {
19 names_allowlist: Arc<HashMap<String, HierarchyMatcher>>,
20 all_allowlist: Option<Arc<HierarchyMatcher>>,
21 },
22
23 AllFilteredOut,
26
27 FilteringDisabled,
29}
30
31pub enum PrivacyExplicitOption<T> {
32 Found(T),
34
35 NotFound,
37
38 FilteringDisabled,
40}
41
42#[derive(Debug, Clone)]
43pub struct ComponentAllowlist(ComponentAllowlistState);
44
45impl ComponentAllowlist {
46 pub fn all_filtered_out(&self) -> bool {
47 matches!(self.0, ComponentAllowlistState::AllFilteredOut)
48 }
49
50 pub fn matcher(&self, name: &str) -> PrivacyExplicitOption<&HierarchyMatcher> {
51 match &self.0 {
52 ComponentAllowlistState::FilteringEnabled { names_allowlist, all_allowlist } => {
53 if let Some(matcher) = names_allowlist.get(name) {
54 PrivacyExplicitOption::Found(matcher)
55 } else if let Some(matcher) = all_allowlist {
56 PrivacyExplicitOption::Found(matcher.as_ref())
57 } else {
58 PrivacyExplicitOption::NotFound
59 }
60 }
61 ComponentAllowlistState::AllFilteredOut => PrivacyExplicitOption::NotFound,
62 ComponentAllowlistState::FilteringDisabled => PrivacyExplicitOption::FilteringDisabled,
63 }
64 }
65
66 fn new<'a>(
67 selectors_for_this_component: impl Iterator<Item = Result<&'a Selector, anyhow::Error>>,
68 ) -> Result<Self, Error> {
69 let buckets = bucketize_selectors_by_name(selectors_for_this_component)?;
70 Ok(Self(ComponentAllowlistState::FilteringEnabled {
71 names_allowlist: Arc::new(HashMap::from_iter(
72 buckets
73 .names
74 .into_iter()
75 .map(|(k, v)| Ok((k, HierarchyMatcher::try_from(v)?)))
76 .collect::<Result<Vec<_>, Error>>()?,
77 )),
78 all_allowlist: buckets.all.map(HierarchyMatcher::try_from).transpose()?.map(Arc::new),
79 }))
80 }
81}
82
83#[cfg(test)]
84impl ComponentAllowlist {
85 pub fn filtering_enabled(&self) -> bool {
86 match self.0 {
87 ComponentAllowlistState::FilteringEnabled { .. }
88 | ComponentAllowlistState::AllFilteredOut => true,
89 ComponentAllowlistState::FilteringDisabled => false,
90 }
91 }
92
93 pub fn new_disabled() -> Self {
94 ComponentAllowlist(ComponentAllowlistState::FilteringDisabled)
95 }
96}
97
98struct BucketedSelectors<'a> {
99 all: Option<Vec<&'a Selector>>,
100 names: HashMap<String, Vec<&'a Selector>>,
101}
102
103fn bucketize_selectors_by_name<'a>(
104 selectors: impl Iterator<Item = Result<&'a Selector, anyhow::Error>>,
105) -> Result<BucketedSelectors<'a>, Error> {
106 let mut names_to_selectors: HashMap<_, Vec<_>> = HashMap::new();
107 let mut selectors_against_all = vec![];
108 for s in selectors {
112 let s = s.map_err(Error::Selectors)?;
113 match s.tree_names {
114 Some(TreeNames::Some(ref tree_names)) => {
115 for name in tree_names {
116 if let Some(mapped_selectors) = names_to_selectors.get_mut(name) {
117 mapped_selectors.push(s);
118 } else {
119 names_to_selectors.insert(name.to_string(), vec![s]);
120 }
121 }
122 }
123 None => {
124 if let Some(mapped_selectors) = names_to_selectors.get_mut(DEFAULT_TREE_NAME) {
125 mapped_selectors.push(s);
126 } else {
127 names_to_selectors.insert(DEFAULT_TREE_NAME.to_string(), vec![s]);
128 }
129 }
130 Some(TreeNames::All(_)) => {
131 selectors_against_all.push(s);
132 }
133 Some(TreeNames::__SourceBreaking { unknown_ordinal }) => {
134 return Err(Error::Selectors(anyhow!(
135 "unknown TreeNames variant {unknown_ordinal} in {s:?}"
136 )));
137 }
138 }
139 }
140
141 if selectors_against_all.is_empty() {
142 return Ok(BucketedSelectors { all: None, names: names_to_selectors });
143 }
144
145 for names in names_to_selectors.values_mut() {
146 names.extend(selectors_against_all.iter());
147 }
148
149 Ok(BucketedSelectors { all: Some(selectors_against_all), names: names_to_selectors })
150}
151
152#[derive(Clone)]
153enum StaticHierarchyAllowlistState {
154 FilteringEnabled {
156 component_allowlists: HashMap<ExtendedMoniker, ComponentAllowlist>,
157 all_selectors: Vec<Selector>,
158 },
159
160 FilteringDisabled,
162}
163
164#[derive(Clone)]
165pub struct StaticHierarchyAllowlist(StaticHierarchyAllowlistState);
166
167impl StaticHierarchyAllowlist {
168 pub fn get(&self, moniker: &ExtendedMoniker) -> ComponentAllowlist {
173 match &self.0 {
174 StaticHierarchyAllowlistState::FilteringEnabled { component_allowlists, .. } => {
175 component_allowlists
176 .get(moniker)
177 .cloned()
178 .unwrap_or(ComponentAllowlist(ComponentAllowlistState::AllFilteredOut))
179 }
180 StaticHierarchyAllowlistState::FilteringDisabled => {
181 ComponentAllowlist(ComponentAllowlistState::FilteringDisabled)
182 }
183 }
184 }
185
186 pub fn new(all_selectors: Option<Vec<Selector>>) -> Self {
187 if let Some(all_selectors) = all_selectors {
188 return Self(StaticHierarchyAllowlistState::FilteringEnabled {
189 component_allowlists: HashMap::new(),
191 all_selectors,
192 });
193 }
194
195 Self(StaticHierarchyAllowlistState::FilteringDisabled)
196 }
197
198 pub fn remove_component(&mut self, moniker: &ExtendedMoniker) {
199 match &mut self.0 {
200 StaticHierarchyAllowlistState::FilteringEnabled { component_allowlists, .. } => {
201 component_allowlists.remove(moniker);
202 }
203 StaticHierarchyAllowlistState::FilteringDisabled => {}
204 }
205 }
206
207 pub fn add_component(&mut self, moniker: ExtendedMoniker) -> Result<(), Error> {
209 match &mut self.0 {
210 StaticHierarchyAllowlistState::FilteringEnabled {
211 all_selectors,
212 component_allowlists,
213 } => {
214 let mut matched_selectors =
215 moniker.match_against_selectors(all_selectors.iter()).peekable();
216 if matched_selectors.peek().is_none() {
217 drop(matched_selectors);
218 component_allowlists.insert(
219 moniker,
220 ComponentAllowlist(ComponentAllowlistState::AllFilteredOut),
221 );
222 } else {
223 let allowlist = ComponentAllowlist::new(matched_selectors)?;
224 component_allowlists.insert(moniker, allowlist);
225 }
226 }
227 StaticHierarchyAllowlistState::FilteringDisabled => {}
228 }
229
230 Ok(())
231 }
232}
233
234#[cfg(test)]
235impl StaticHierarchyAllowlist {
236 pub fn filtering_enabled(&self) -> bool {
237 match self.0 {
238 StaticHierarchyAllowlistState::FilteringEnabled { .. } => true,
239 StaticHierarchyAllowlistState::FilteringDisabled => false,
240 }
241 }
242
243 pub fn component_was_added(&self, moniker: &ExtendedMoniker) -> bool {
244 match &self.0 {
245 StaticHierarchyAllowlistState::FilteringEnabled { component_allowlists, .. } => {
246 component_allowlists.get(moniker).is_some()
247 }
248 StaticHierarchyAllowlistState::FilteringDisabled => false,
249 }
250 }
251
252 pub fn new_disabled() -> Self {
253 Self(StaticHierarchyAllowlistState::FilteringDisabled)
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use selectors::{parse_selector, VerboseError};
261
262 fn make_selectors(s: &[Selector]) -> impl Iterator<Item = Result<&Selector, anyhow::Error>> {
263 s.iter().map(Ok)
264 }
265
266 fn matcher_found(m: PrivacyExplicitOption<&HierarchyMatcher>) -> bool {
267 matches!(m, PrivacyExplicitOption::Found(_))
268 }
269
270 fn matcher_not_found(m: PrivacyExplicitOption<&HierarchyMatcher>) -> bool {
271 matches!(m, PrivacyExplicitOption::NotFound)
272 }
273
274 #[fuchsia::test]
275 fn component_allowlist() {
276 let selectors = &[
277 parse_selector::<VerboseError>(r#"*:[name=foo]root:hello"#).unwrap(),
278 parse_selector::<VerboseError>(r#"*:[name=bar]root:hello"#).unwrap(),
279 parse_selector::<VerboseError>(r#"*:[...]root:good"#).unwrap(),
280 ];
281
282 let selectors = make_selectors(selectors);
283
284 let list = ComponentAllowlist::new(selectors).unwrap();
285 assert!(list.filtering_enabled());
286
287 assert!(matcher_found(list.matcher("foo")));
288 assert!(matcher_found(list.matcher("bar")));
289 assert!(matcher_found(list.matcher("should match all")));
290 }
291
292 #[fuchsia::test]
293 fn test_bucketize_selectors() {
294 let orig_selectors = &[
295 parse_selector::<VerboseError>(r#"*:[name=foo]root:hello"#).unwrap(),
296 parse_selector::<VerboseError>(r#"*:[name=bar]root:hello"#).unwrap(),
297 parse_selector::<VerboseError>(r#"*:[...]root:something"#).unwrap(),
298 parse_selector::<VerboseError>(r#"*:[name=foo]root:goodbye"#).unwrap(),
299 ];
300
301 let selectors = make_selectors(orig_selectors);
302
303 let buckets = bucketize_selectors_by_name(selectors).unwrap();
304
305 let named_expected = HashMap::from([
306 ("foo".into(), vec![&orig_selectors[0], &orig_selectors[3], &orig_selectors[2]]),
307 ("bar".into(), vec![&orig_selectors[1], &orig_selectors[2]]),
308 ]);
309
310 let all_expected = Some(vec![&orig_selectors[2]]);
311
312 assert_eq!(buckets.names, named_expected);
313 assert_eq!(buckets.all, all_expected);
314 }
315
316 #[fuchsia::test]
317 fn static_hierarchy_allowlist() {
318 let selectors = vec![
319 parse_selector::<VerboseError>(r#"component1:[name=foo]root:foo_one"#).unwrap(),
320 parse_selector::<VerboseError>(r#"component1:[...]root:all_one"#).unwrap(),
321 parse_selector::<VerboseError>(r#"*:[name=foo, name=bar]root"#).unwrap(),
322 parse_selector::<VerboseError>(r#"component2:[name=bar]root:bar_two"#).unwrap(),
323 parse_selector::<VerboseError>(r#"component2:[name=qux]root:bar_two"#).unwrap(),
324 ];
325
326 let mut allowlist = StaticHierarchyAllowlist::new(Some(selectors));
327
328 assert!(allowlist.filtering_enabled());
329
330 let component_moniker = ExtendedMoniker::try_from("component1").unwrap();
331 allowlist.add_component(component_moniker.clone()).unwrap();
332
333 let list = allowlist.get(&component_moniker);
334
335 assert!(list.filtering_enabled());
336
337 assert!(matcher_found(list.matcher("foo")));
338 assert!(matcher_found(list.matcher("bar")));
339 assert!(matcher_found(list.matcher("should match all")));
340
341 let component_moniker = ExtendedMoniker::try_from("component2").unwrap();
342 allowlist.add_component(component_moniker.clone()).unwrap();
343
344 let list = allowlist.get(&component_moniker);
345 assert!(matcher_found(list.matcher("foo")));
346 assert!(matcher_found(list.matcher("bar")));
347 assert!(matcher_found(list.matcher("qux")));
348 assert!(matcher_not_found(list.matcher("no 'all' for 2")));
349 }
350}