Skip to main content

archivist_lib/inspect/
container.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::diagnostics::{GlobalConnectionStats, TRACE_CATEGORY};
6use crate::identity::ComponentIdentity;
7use crate::inspect::collector::{self as collector, InspectData};
8use crate::pipeline::ComponentAllowlist;
9use diagnostics_data::{self as schema, InspectHandleName};
10use diagnostics_hierarchy::DiagnosticsHierarchy;
11use fidl::endpoints::Proxy;
12use fidl_fuchsia_diagnostics as fdiagnostics;
13use fidl_fuchsia_inspect as finspect;
14use fidl_fuchsia_io as fio;
15use flyweights::FlyStr;
16use fuchsia_async as fasync;
17use fuchsia_async::{DurationExt, TimeoutExt};
18use fuchsia_inspect::UintProperty;
19use fuchsia_inspect::reader::snapshot::{Snapshot, SnapshotTree};
20use fuchsia_trace as ftrace;
21use futures::channel::oneshot;
22use futures::{FutureExt, Stream};
23use inspect_fidl_load as deprecated_inspect;
24use selectors::SelectorExt;
25use std::collections::HashMap;
26use std::future::Future;
27use std::sync::{Arc, Weak};
28use std::time::Duration;
29use zx::{self as zx, AsHandleRef};
30
31const DIRECTORY_READ_TIMED_OUT: &str = "Reading Inspect handles from a directory timed out";
32
33#[derive(Debug)]
34pub enum InspectHandle {
35    Tree {
36        proxy: finspect::TreeProxy,
37        name: Option<FlyStr>,
38    },
39    Directory {
40        proxy: fio::DirectoryProxy,
41    },
42    Escrow {
43        vmo: Arc<zx::Vmo>,
44        name: Option<FlyStr>,
45        token: finspect::EscrowToken,
46        related_koid: zx::Koid,
47    },
48}
49
50impl InspectHandle {
51    fn is_closed(&self) -> bool {
52        match self {
53            Self::Directory { proxy } => proxy.as_channel().is_closed(),
54            Self::Tree { proxy, .. } => proxy.as_channel().is_closed(),
55            Self::Escrow { .. } => false,
56        }
57    }
58
59    async fn on_closed(&self) -> Result<zx::Signals, zx::Status> {
60        match self {
61            Self::Tree { proxy, .. } => proxy.on_closed().await,
62            Self::Directory { proxy, .. } => proxy.on_closed().await,
63            Self::Escrow { token, .. } => {
64                fasync::OnSignals::new(&token.token, fidl::Signals::OBJECT_PEER_CLOSED).await
65            }
66        }
67    }
68
69    pub fn koid(&self) -> zx::Koid {
70        match self {
71            Self::Directory { proxy } => {
72                proxy.as_channel().as_handle_ref().koid().expect("DirectoryProxy has koid")
73            }
74            Self::Tree { proxy, .. } => {
75                proxy.as_channel().as_handle_ref().koid().expect("TreeProxy has koid")
76            }
77            // We return the related koid to index based on it so that the retrieval is more
78            // efficient.
79            Self::Escrow { related_koid, .. } => *related_koid,
80        }
81    }
82
83    pub fn tree<T: Into<FlyStr>>(proxy: finspect::TreeProxy, name: Option<T>) -> Self {
84        InspectHandle::Tree { proxy, name: name.map(|n| n.into()) }
85    }
86
87    pub fn escrow<T: Into<FlyStr>>(
88        vmo: zx::Vmo,
89        token: finspect::EscrowToken,
90        name: Option<T>,
91    ) -> Self {
92        let related_koid = token.token.basic_info().unwrap().related_koid;
93        InspectHandle::Escrow {
94            vmo: Arc::new(vmo),
95            name: name.map(|n| n.into()),
96            token,
97            related_koid,
98        }
99    }
100
101    pub fn directory(proxy: fio::DirectoryProxy) -> Self {
102        InspectHandle::Directory { proxy }
103    }
104}
105
106struct StoredInspectHandle {
107    handle: Arc<InspectHandle>,
108    // Whenever we delete the stored handle from the map, we can leverage dropping this oneshot to
109    // trigger completion of the Task listening for related handle peer closed.
110    _control: oneshot::Sender<()>,
111}
112
113#[derive(Default)]
114pub struct InspectArtifactsContainer {
115    /// One or more proxies that this container is configured for.
116    inspect_handles: HashMap<zx::Koid, StoredInspectHandle>,
117}
118
119impl InspectArtifactsContainer {
120    /// Remove a handle via its `koid` from the set of proxies managed by `self`.
121    pub fn remove_handle(&mut self, koid: zx::Koid) -> (Option<Arc<InspectHandle>>, usize) {
122        let stored = self.inspect_handles.remove(&koid);
123        (stored.map(|stored| stored.handle), self.inspect_handles.len())
124    }
125
126    /// Push a new handle into the container.
127    ///
128    /// Returns `None` if the handle is a DirectoryProxy and there is already one tracked,
129    /// as only single handles are supported in the DirectoryProxy case.
130    pub fn push_handle<T: FnOnce(zx::Koid)>(
131        &mut self,
132        handle: InspectHandle,
133        on_closed: T,
134    ) -> Option<impl Future<Output = ()> + use<T>> {
135        if !self.inspect_handles.is_empty() && matches!(handle, InspectHandle::Directory { .. }) {
136            return None;
137        }
138        let (control_snd, control_rcv) = oneshot::channel();
139        let handle = Arc::new(handle);
140        let stored = StoredInspectHandle { _control: control_snd, handle: Arc::clone(&handle) };
141        let koid = handle.koid();
142        self.inspect_handles.insert(koid, stored);
143        Some(async move {
144            if !handle.is_closed() {
145                let closed_fut = handle.on_closed();
146                futures::pin_mut!(closed_fut);
147                let _ = futures::future::select(closed_fut, control_rcv).await;
148            }
149            on_closed(koid);
150        })
151    }
152
153    /// Generate an `UnpopulatedInspectDataContainer` from the proxies managed by `self`.
154    ///
155    /// Returns `None` if there are no valid proxies.
156    pub fn create_unpopulated(
157        &self,
158        identity: &Arc<ComponentIdentity>,
159        allowlist: ComponentAllowlist,
160        dynamic_selectors: &Option<impl AsRef<[fdiagnostics::Selector]>>,
161    ) -> Option<UnpopulatedInspectDataContainer> {
162        if self.inspect_handles.is_empty() {
163            return None;
164        }
165
166        if allowlist.all_filtered_out() {
167            return None;
168        }
169
170        // Verify that dynamic selectors contain an entry that applies to this moniker
171        let mut dynamic_selectors = dynamic_selectors.as_ref().map(|all_components| {
172            all_components
173                .as_ref()
174                .iter()
175                .filter(|s| matches!(identity.moniker.matches_selector(s), Ok(true)))
176                .peekable()
177        });
178
179        if let Some(None) = dynamic_selectors.as_mut().map(|s| s.peek()) {
180            return None;
181        }
182
183        Some(UnpopulatedInspectDataContainer {
184            identity: Arc::clone(identity),
185            component_allowlist: allowlist,
186            inspect_handles: self
187                .inspect_handles
188                .values()
189                .filter(|s| {
190                    // dynamic_selectors.clone() should just be a clone of the iterator, so as to
191                    // always start at the beginning
192                    Self::name_filters_satisfied(s.handle.as_ref(), dynamic_selectors.clone())
193                })
194                .map(|s| Arc::downgrade(&s.handle))
195                .collect::<Vec<_>>(),
196        })
197    }
198
199    fn name_filters_satisfied<'a>(
200        handle: &InspectHandle,
201        mut dynamic_selectors: Option<impl Iterator<Item = &'a fdiagnostics::Selector>>,
202    ) -> bool {
203        let Some(ref mut selectors) = dynamic_selectors else {
204            return true;
205        };
206        match handle {
207            InspectHandle::Tree { name, .. } | InspectHandle::Escrow { name, .. } => {
208                selectors.any(|s| {
209                    let name = match name.as_ref() {
210                        Some(fly) => fly.as_str(),
211                        None => finspect::DEFAULT_TREE_NAME,
212                    };
213                    selectors::match_tree_name_against_selector(name, s)
214                })
215            }
216            InspectHandle::Directory { .. } => selectors.any(|s| {
217                matches!(s.tree_names.as_ref(), None | Some(fdiagnostics::TreeNames::All(_)))
218            }),
219        }
220    }
221}
222
223#[cfg(test)]
224impl InspectArtifactsContainer {
225    pub(crate) fn handles(&self) -> impl ExactSizeIterator<Item = &InspectHandle> {
226        self.inspect_handles.values().map(|stored| stored.handle.as_ref())
227    }
228}
229
230#[derive(Debug)]
231pub enum ReadSnapshot {
232    Single(Snapshot),
233    Tree(SnapshotTree),
234    Finished(DiagnosticsHierarchy),
235}
236
237/// Packet containing a snapshot and all the metadata needed to
238/// populate a diagnostics schema for that snapshot.
239#[derive(Debug)]
240pub struct SnapshotData {
241    /// Optional name of the file or InspectSink proxy that created this snapshot.
242    pub name: Option<InspectHandleName>,
243    /// Timestamp at which this snapshot resolved or failed.
244    pub timestamp: zx::BootInstant,
245    /// Errors encountered when processing this snapshot.
246    pub errors: Vec<schema::InspectError>,
247    /// Optional snapshot of the inspect hierarchy, in case reading fails
248    /// and we have errors to share with client.
249    pub snapshot: Option<ReadSnapshot>,
250    /// Whether or not the data was escrowed.
251    pub escrowed: bool,
252}
253
254impl SnapshotData {
255    async fn new(
256        name: Option<InspectHandleName>,
257        data: InspectData,
258        lazy_child_timeout: Duration,
259        identity: &ComponentIdentity,
260        parent_trace_id: ftrace::Id,
261        timeout_counter: &UintProperty,
262    ) -> SnapshotData {
263        let trace_id = ftrace::Id::new();
264        let _trace_guard = ftrace::async_enter!(
265            trace_id,
266            TRACE_CATEGORY,
267            c"SnapshotData::new",
268            // An async duration cannot have multiple concurrent child async durations
269            // so we include the nonce as metadata to manually determine relationship.
270            "parent_trace_id" => u64::from(parent_trace_id),
271            "trace_id" => u64::from(trace_id),
272            "moniker" => identity.to_string().as_ref(),
273            "filename" => name
274                    .as_ref()
275                    .and_then(InspectHandleName::as_filename)
276                    .unwrap_or(""),
277            "name" => name
278                    .as_ref()
279                    .and_then(InspectHandleName::as_name)
280                    .unwrap_or("")
281        );
282        match data {
283            InspectData::Tree(tree) => {
284                match SnapshotTree::try_from_with_timeout(
285                    &tree,
286                    lazy_child_timeout,
287                    timeout_counter,
288                )
289                .await
290                {
291                    Ok(snapshot_tree) => {
292                        SnapshotData::successful(ReadSnapshot::Tree(snapshot_tree), name, false)
293                    }
294                    Err(e) => SnapshotData::failed(
295                        schema::InspectError { message: format!("{e:?}") },
296                        name,
297                        false,
298                    ),
299                }
300            }
301            InspectData::DeprecatedFidl(inspect_proxy) => {
302                match deprecated_inspect::load_hierarchy(inspect_proxy).await {
303                    Ok(hierarchy) => {
304                        SnapshotData::successful(ReadSnapshot::Finished(hierarchy), name, false)
305                    }
306                    Err(e) => SnapshotData::failed(
307                        schema::InspectError { message: format!("{e:?}") },
308                        name,
309                        false,
310                    ),
311                }
312            }
313            InspectData::Vmo { data: vmo, escrowed } => match Snapshot::try_from(vmo.as_ref()) {
314                Ok(snapshot) => {
315                    SnapshotData::successful(ReadSnapshot::Single(snapshot), name, escrowed)
316                }
317                Err(e) => SnapshotData::failed(
318                    schema::InspectError { message: format!("{e:?}") },
319                    name,
320                    escrowed,
321                ),
322            },
323            InspectData::File(contents) => match Snapshot::try_from(contents) {
324                Ok(snapshot) => {
325                    SnapshotData::successful(ReadSnapshot::Single(snapshot), name, false)
326                }
327                Err(e) => SnapshotData::failed(
328                    schema::InspectError { message: format!("{e:?}") },
329                    name,
330                    false,
331                ),
332            },
333        }
334    }
335
336    // Constructs packet that timestamps and packages inspect snapshot for exfiltration.
337    fn successful(
338        snapshot: ReadSnapshot,
339        name: Option<InspectHandleName>,
340        escrowed: bool,
341    ) -> SnapshotData {
342        SnapshotData {
343            name,
344            timestamp: zx::BootInstant::get(),
345            errors: Vec::new(),
346            snapshot: Some(snapshot),
347            escrowed,
348        }
349    }
350
351    // Constructs packet that timestamps and packages inspect snapshot failure for exfiltration.
352    fn failed(
353        error: schema::InspectError,
354        name: Option<InspectHandleName>,
355        escrowed: bool,
356    ) -> SnapshotData {
357        SnapshotData {
358            name,
359            timestamp: zx::BootInstant::get(),
360            errors: vec![error],
361            snapshot: None,
362            escrowed,
363        }
364    }
365}
366
367/// PopulatedInspectDataContainer is the container that
368/// holds the actual Inspect data for a given component,
369/// along with all information needed to transform that data
370/// to be returned to the client.
371pub struct PopulatedInspectDataContainer {
372    pub identity: Arc<ComponentIdentity>,
373    /// Vector of all the snapshots of inspect hierarchies under
374    /// the diagnostics directory of the component identified by
375    /// moniker, along with the metadata needed to populate
376    /// this snapshot's diagnostics schema.
377    pub snapshot: SnapshotData,
378    pub component_allowlist: ComponentAllowlist,
379}
380
381enum Status {
382    Begin,
383    Pending(collector::InspectHandleDeque),
384    DirectoryFailed,
385}
386
387struct State {
388    status: Status,
389    unpopulated: Arc<UnpopulatedInspectDataContainer>,
390    batch_timeout: zx::MonotonicDuration,
391    elapsed_time: zx::MonotonicDuration,
392    global_stats: Arc<GlobalConnectionStats>,
393    trace_guard: Arc<Option<ftrace::AsyncScope>>,
394    trace_id: ftrace::Id,
395}
396
397impl State {
398    fn into_pending(
399        self,
400        pending: collector::InspectHandleDeque,
401        start_time: zx::MonotonicInstant,
402    ) -> Self {
403        Self {
404            unpopulated: self.unpopulated,
405            status: Status::Pending(pending),
406            batch_timeout: self.batch_timeout,
407            global_stats: self.global_stats,
408            elapsed_time: self.elapsed_time + (zx::MonotonicInstant::get() - start_time),
409            trace_guard: self.trace_guard,
410            trace_id: self.trace_id,
411        }
412    }
413
414    fn into_directory_failed(self, start_time: zx::MonotonicInstant) -> Self {
415        Self {
416            unpopulated: self.unpopulated,
417            status: Status::DirectoryFailed,
418            batch_timeout: self.batch_timeout,
419            global_stats: self.global_stats,
420            elapsed_time: self.elapsed_time + (zx::MonotonicInstant::get() - start_time),
421            trace_guard: self.trace_guard,
422            trace_id: self.trace_id,
423        }
424    }
425
426    fn add_elapsed_time(&mut self, start_time: zx::MonotonicInstant) {
427        self.elapsed_time += zx::MonotonicInstant::get() - start_time
428    }
429
430    async fn iterate(
431        mut self,
432        start_time: zx::MonotonicInstant,
433    ) -> Option<(PopulatedInspectDataContainer, State)> {
434        let timeout = Duration::from_nanos(self.batch_timeout.into_nanos() as u64);
435        loop {
436            match &mut self.status {
437                Status::Begin => {
438                    match collector::populate_data_map(&self.unpopulated.inspect_handles)
439                        .map(Some)
440                        // This timeout is protecting against unresponsive directories only.
441                        // It does not handle timeouts for the reading/snapshotting process.
442                        .on_timeout(timeout.after_now(), || None)
443                        .await
444                    {
445                        Some(data_map) => self = self.into_pending(data_map, start_time),
446                        None => self = self.into_directory_failed(start_time),
447                    }
448                }
449                Status::Pending(pending) => match pending.pop_front() {
450                    None => {
451                        self.global_stats.record_component_duration(
452                            self.unpopulated.identity.moniker.as_ref(),
453                            self.elapsed_time + (zx::MonotonicInstant::get() - start_time),
454                        );
455                        return None;
456                    }
457                    Some((name, data)) => {
458                        let snapshot = SnapshotData::new(
459                            name,
460                            data,
461                            timeout,
462                            &self.unpopulated.identity,
463                            self.trace_id,
464                            self.global_stats.timeout_counter(),
465                        )
466                        .await;
467                        let result = PopulatedInspectDataContainer {
468                            identity: Arc::clone(&self.unpopulated.identity),
469                            snapshot,
470                            component_allowlist: self.unpopulated.component_allowlist.clone(),
471                        };
472                        self.add_elapsed_time(start_time);
473                        return Some((result, self));
474                    }
475                },
476                Status::DirectoryFailed => {
477                    self.global_stats.add_timeout();
478                    let result = PopulatedInspectDataContainer {
479                        identity: Arc::clone(&self.unpopulated.identity),
480                        component_allowlist: self.unpopulated.component_allowlist.clone(),
481                        snapshot: SnapshotData::failed(
482                            schema::InspectError { message: DIRECTORY_READ_TIMED_OUT.to_string() },
483                            None,
484                            false,
485                        ),
486                    };
487
488                    return Some((result, self.into_pending(Default::default(), start_time)));
489                }
490            }
491        }
492    }
493}
494
495/// UnpopulatedInspectDataContainer is the container that holds
496/// all information needed to retrieve Inspect data
497/// for a given component, when requested.
498#[derive(Debug)]
499pub struct UnpopulatedInspectDataContainer {
500    pub identity: Arc<ComponentIdentity>,
501    /// Proxies configured for container. It is an invariant that if any value is an
502    /// InspectHandle::Directory, then there is exactly one value.
503    pub inspect_handles: Vec<Weak<InspectHandle>>,
504    /// Optional hierarchy matcher. If unset, the reader is running
505    /// in all-access mode, meaning no matching or filtering is required.
506    pub component_allowlist: ComponentAllowlist,
507}
508
509impl UnpopulatedInspectDataContainer {
510    /// Populates this data container with a timeout. On the timeout firing returns a
511    /// container suitable to return to clients, but with timeout error information recorded.
512    pub fn populate(
513        self,
514        timeout: i64,
515        global_stats: Arc<GlobalConnectionStats>,
516        parent_trace_id: ftrace::Id,
517    ) -> impl Stream<Item = PopulatedInspectDataContainer> {
518        let trace_id = ftrace::Id::new();
519        let trace_guard = ftrace::async_enter!(
520            trace_id,
521            TRACE_CATEGORY,
522            c"ReaderServer::stream.populate",
523            // An async duration cannot have multiple concurrent child async durations
524            // so we include the nonce as metadata to manually determine relationship.
525            "parent_trace_id" => u64::from(parent_trace_id),
526            "trace_id" => u64::from(trace_id),
527            "moniker" => self.identity.to_string().as_ref()
528        );
529        let this = Arc::new(self);
530        let state = State {
531            status: Status::Begin,
532            unpopulated: this,
533            batch_timeout: zx::MonotonicDuration::from_seconds(timeout),
534            global_stats,
535            elapsed_time: zx::MonotonicDuration::ZERO,
536            trace_guard: Arc::new(trace_guard),
537            trace_id,
538        };
539
540        futures::stream::unfold(state, |state| {
541            let start_time = zx::MonotonicInstant::get();
542
543            state.iterate(start_time)
544        })
545    }
546}
547
548#[cfg(test)]
549mod test {
550    use super::*;
551    use fuchsia_inspect::Node;
552    use std::pin::pin;
553
554    use futures::StreamExt;
555    use std::sync::LazyLock;
556
557    static EMPTY_IDENTITY: LazyLock<Arc<ComponentIdentity>> =
558        LazyLock::new(|| Arc::new(ComponentIdentity::unknown()));
559
560    static O_K_IDENTITY: LazyLock<Arc<ComponentIdentity>> =
561        LazyLock::new(|| Arc::new(ComponentIdentity::from(vec!["o", "k"])));
562
563    #[fuchsia::test]
564    async fn population_times_out() {
565        // Simulate a directory that hangs indefinitely in any request so that we consistently
566        // trigger the 0 timeout.
567        let (directory, mut stream) =
568            fidl::endpoints::create_proxy_and_stream::<fio::DirectoryMarker>();
569        fasync::Task::spawn(async move {
570            while stream.next().await.is_some() {
571                fasync::Timer::new(fasync::MonotonicInstant::after(
572                    zx::MonotonicDuration::from_seconds(100000),
573                ))
574                .await;
575            }
576        })
577        .detach();
578
579        let handle = Arc::new(InspectHandle::directory(directory));
580        let container = UnpopulatedInspectDataContainer {
581            identity: EMPTY_IDENTITY.clone(),
582            inspect_handles: vec![Arc::downgrade(&handle)],
583            component_allowlist: ComponentAllowlist::new_disabled(),
584        };
585        let mut stream = pin!(container.populate(
586            0,
587            Arc::new(GlobalConnectionStats::new(Node::default())),
588            ftrace::Id::new(),
589        ));
590        let res = stream.next().await.unwrap();
591        assert_eq!(res.snapshot.name, None);
592        assert_eq!(
593            res.snapshot.errors,
594            vec![schema::InspectError { message: DIRECTORY_READ_TIMED_OUT.to_string() }]
595        );
596    }
597
598    #[fuchsia::test]
599    async fn no_inspect_files_do_not_give_an_error_response() {
600        let directory = Arc::new(InspectHandle::directory(
601            fuchsia_fs::directory::open_in_namespace("/tmp", fio::PERM_READABLE).unwrap(),
602        ));
603        let container = UnpopulatedInspectDataContainer {
604            identity: EMPTY_IDENTITY.clone(),
605            inspect_handles: vec![Arc::downgrade(&directory)],
606            component_allowlist: ComponentAllowlist::new_disabled(),
607        };
608        let mut stream = pin!(container.populate(
609            1000000,
610            Arc::new(GlobalConnectionStats::new(Node::default())),
611            ftrace::Id::new(),
612        ));
613        assert!(stream.next().await.is_none());
614    }
615
616    #[fuchsia::test]
617    fn only_one_directory_proxy_is_populated() {
618        let _executor = fuchsia_async::LocalExecutor::default();
619        let (directory, _) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
620        let mut container = InspectArtifactsContainer::default();
621        let _rx = container.push_handle(InspectHandle::directory(directory), |_| {});
622        let (directory2, _) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
623        assert!(container.push_handle(InspectHandle::directory(directory2), |_| {}).is_none());
624    }
625
626    fn inspect_artifacts_container_with_one_dir() -> InspectArtifactsContainer {
627        let (directory, _) = fidl::endpoints::create_proxy_and_stream::<fio::DirectoryMarker>();
628        let handle = InspectHandle::directory(directory);
629        let mut container = InspectArtifactsContainer::default();
630        container.push_handle(handle, |_| {});
631        container
632    }
633
634    #[fuchsia::test]
635    fn prefilter_on_names_for_directories() {
636        let _executor = fuchsia_async::LocalExecutor::default();
637        let all_selector = fdiagnostics::Selector {
638            tree_names: Some(fdiagnostics::TreeNames::All(fdiagnostics::All {})),
639            component_selector: Some(fdiagnostics::ComponentSelector {
640                moniker_segments: Some(vec![
641                    fdiagnostics::StringSelector::ExactMatch("o".to_string()),
642                    fdiagnostics::StringSelector::ExactMatch("k".to_string()),
643                ]),
644                ..Default::default()
645            }),
646            tree_selector: Some(fdiagnostics::TreeSelector::SubtreeSelector(
647                fdiagnostics::SubtreeSelector {
648                    node_path: vec![fdiagnostics::StringSelector::ExactMatch("root".to_string())],
649                },
650            )),
651            ..Default::default()
652        };
653        let selectors = Some(vec![all_selector]);
654
655        let iac = inspect_artifacts_container_with_one_dir();
656        let container = iac
657            .create_unpopulated(&O_K_IDENTITY, ComponentAllowlist::new_disabled(), &selectors)
658            .unwrap();
659
660        assert_eq!(1, container.inspect_handles.len());
661
662        let selector_with_wrong_tree_name = fdiagnostics::Selector {
663            tree_names: Some(fdiagnostics::TreeNames::Some(vec!["a".to_string()])),
664            component_selector: Some(fdiagnostics::ComponentSelector {
665                moniker_segments: Some(vec![
666                    fdiagnostics::StringSelector::ExactMatch("o".to_string()),
667                    fdiagnostics::StringSelector::ExactMatch("k".to_string()),
668                ]),
669                ..Default::default()
670            }),
671            tree_selector: Some(fdiagnostics::TreeSelector::SubtreeSelector(
672                fdiagnostics::SubtreeSelector {
673                    node_path: vec![fdiagnostics::StringSelector::ExactMatch("root".to_string())],
674                },
675            )),
676            ..Default::default()
677        };
678        let selectors = Some(vec![selector_with_wrong_tree_name]);
679
680        let iac = inspect_artifacts_container_with_one_dir();
681        let container = iac
682            .create_unpopulated(&O_K_IDENTITY, ComponentAllowlist::new_disabled(), &selectors)
683            .unwrap();
684
685        assert_eq!(0, container.inspect_handles.len());
686
687        let selector_with_no_tree_names = fdiagnostics::Selector {
688            tree_names: None,
689            component_selector: Some(fdiagnostics::ComponentSelector {
690                moniker_segments: Some(vec![
691                    fdiagnostics::StringSelector::ExactMatch("o".to_string()),
692                    fdiagnostics::StringSelector::ExactMatch("k".to_string()),
693                ]),
694                ..Default::default()
695            }),
696            tree_selector: Some(fdiagnostics::TreeSelector::SubtreeSelector(
697                fdiagnostics::SubtreeSelector {
698                    node_path: vec![fdiagnostics::StringSelector::ExactMatch("root".to_string())],
699                },
700            )),
701            ..Default::default()
702        };
703
704        let selectors = Some(vec![selector_with_no_tree_names]);
705
706        let iac = inspect_artifacts_container_with_one_dir();
707        let container = iac
708            .create_unpopulated(&O_K_IDENTITY, ComponentAllowlist::new_disabled(), &selectors)
709            .unwrap();
710        assert_eq!(1, container.inspect_handles.len());
711    }
712
713    fn inspect_artifacts_container_with_n_trees<'a>(
714        tree_names: impl IntoIterator<Item = &'a str>,
715    ) -> InspectArtifactsContainer {
716        let mut container = InspectArtifactsContainer::default();
717        for name in tree_names {
718            let (proxy, _) = fidl::endpoints::create_proxy::<finspect::TreeMarker>();
719            let handle = InspectHandle::tree(proxy, Some(name));
720            container.push_handle(handle, |_| {});
721        }
722        container
723    }
724
725    #[fuchsia::test]
726    fn name_filter_with_no_matches() {
727        let _executor = fuchsia_async::LocalExecutor::default();
728
729        let b_only = fdiagnostics::Selector {
730            tree_names: Some(fdiagnostics::TreeNames::Some(vec!["b".to_string()])),
731            component_selector: Some(fdiagnostics::ComponentSelector {
732                moniker_segments: Some(vec![
733                    fdiagnostics::StringSelector::ExactMatch("o".to_string()),
734                    fdiagnostics::StringSelector::ExactMatch("k".to_string()),
735                ]),
736                ..Default::default()
737            }),
738            tree_selector: Some(fdiagnostics::TreeSelector::SubtreeSelector(
739                fdiagnostics::SubtreeSelector {
740                    node_path: vec![fdiagnostics::StringSelector::ExactMatch("root".to_string())],
741                },
742            )),
743            ..Default::default()
744        };
745        let selectors = Some(vec![b_only]);
746
747        let iac = inspect_artifacts_container_with_n_trees(["a"]);
748        let container = iac
749            .create_unpopulated(&O_K_IDENTITY, ComponentAllowlist::new_disabled(), &selectors)
750            .unwrap();
751        assert_eq!(0, container.inspect_handles.len());
752    }
753
754    #[fuchsia::test]
755    fn all_name_filter_matches_everything() {
756        let _executor = fuchsia_async::LocalExecutor::default();
757
758        let iac = inspect_artifacts_container_with_n_trees(["a", "b"]);
759        let container = iac
760            .create_unpopulated(
761                &O_K_IDENTITY,
762                ComponentAllowlist::new_disabled(),
763                &None::<&[fdiagnostics::Selector]>,
764            )
765            .unwrap();
766        assert_eq!(2, container.inspect_handles.len());
767
768        let all_selector = fdiagnostics::Selector {
769            tree_names: Some(fdiagnostics::TreeNames::All(fdiagnostics::All {})),
770            component_selector: Some(fdiagnostics::ComponentSelector {
771                moniker_segments: Some(vec![
772                    fdiagnostics::StringSelector::ExactMatch("o".to_string()),
773                    fdiagnostics::StringSelector::ExactMatch("k".to_string()),
774                ]),
775                ..Default::default()
776            }),
777            tree_selector: Some(fdiagnostics::TreeSelector::SubtreeSelector(
778                fdiagnostics::SubtreeSelector {
779                    node_path: vec![fdiagnostics::StringSelector::ExactMatch("root".to_string())],
780                },
781            )),
782            ..Default::default()
783        };
784        let selectors = Some(vec![all_selector]);
785
786        let iac = inspect_artifacts_container_with_n_trees(["a", "b"]);
787        let container = iac
788            .create_unpopulated(&O_K_IDENTITY, ComponentAllowlist::new_disabled(), &selectors)
789            .unwrap();
790        assert_eq!(2, container.inspect_handles.len());
791    }
792
793    #[fuchsia::test]
794    fn none_name_filter_matches_root_only() {
795        let _executor = fuchsia_async::LocalExecutor::default();
796
797        let lists_each_name = fdiagnostics::Selector {
798            tree_names: None,
799            component_selector: Some(fdiagnostics::ComponentSelector {
800                moniker_segments: Some(vec![
801                    fdiagnostics::StringSelector::ExactMatch("o".to_string()),
802                    fdiagnostics::StringSelector::ExactMatch("k".to_string()),
803                ]),
804                ..Default::default()
805            }),
806            tree_selector: Some(fdiagnostics::TreeSelector::SubtreeSelector(
807                fdiagnostics::SubtreeSelector {
808                    node_path: vec![fdiagnostics::StringSelector::ExactMatch("root".to_string())],
809                },
810            )),
811            ..Default::default()
812        };
813        let selectors = Some(vec![lists_each_name]);
814
815        let iac = inspect_artifacts_container_with_n_trees(["a", "b", "root"]);
816        let container = iac
817            .create_unpopulated(&O_K_IDENTITY, ComponentAllowlist::new_disabled(), &selectors)
818            .unwrap();
819        assert_eq!(1, container.inspect_handles.len());
820    }
821
822    #[fuchsia::test]
823    fn select_subset_of_names() {
824        let _executor = fuchsia_async::LocalExecutor::default();
825
826        let a_only = fdiagnostics::Selector {
827            tree_names: Some(fdiagnostics::TreeNames::Some(vec!["a".to_string()])),
828            component_selector: Some(fdiagnostics::ComponentSelector {
829                moniker_segments: Some(vec![
830                    fdiagnostics::StringSelector::ExactMatch("o".to_string()),
831                    fdiagnostics::StringSelector::ExactMatch("k".to_string()),
832                ]),
833                ..Default::default()
834            }),
835            tree_selector: Some(fdiagnostics::TreeSelector::SubtreeSelector(
836                fdiagnostics::SubtreeSelector {
837                    node_path: vec![fdiagnostics::StringSelector::ExactMatch("root".to_string())],
838                },
839            )),
840            ..Default::default()
841        };
842        let selectors = Some(vec![a_only]);
843
844        let iac = inspect_artifacts_container_with_n_trees(["a", "b"]);
845        let container = iac
846            .create_unpopulated(&O_K_IDENTITY, ComponentAllowlist::new_disabled(), &selectors)
847            .unwrap();
848        assert_eq!(1, container.inspect_handles.len());
849
850        match container.inspect_handles[0].upgrade().unwrap().as_ref() {
851            InspectHandle::Tree { name: Some(n), .. } => assert_eq!(n.as_str(), "a"),
852            bad_handle => {
853                panic!("filtering failed, got {bad_handle:?}");
854            }
855        };
856    }
857
858    #[fuchsia::test]
859    fn names_are_case_sensitive() {
860        let _executor = fuchsia_async::LocalExecutor::default();
861
862        let a_but_wrong_case = fdiagnostics::Selector {
863            tree_names: Some(fdiagnostics::TreeNames::Some(vec!["A".to_string()])),
864            component_selector: Some(fdiagnostics::ComponentSelector {
865                moniker_segments: Some(vec![
866                    fdiagnostics::StringSelector::ExactMatch("o".to_string()),
867                    fdiagnostics::StringSelector::ExactMatch("k".to_string()),
868                ]),
869                ..Default::default()
870            }),
871            tree_selector: Some(fdiagnostics::TreeSelector::SubtreeSelector(
872                fdiagnostics::SubtreeSelector {
873                    node_path: vec![fdiagnostics::StringSelector::ExactMatch("root".to_string())],
874                },
875            )),
876            ..Default::default()
877        };
878        let selectors = Some(vec![a_but_wrong_case]);
879
880        let iac = inspect_artifacts_container_with_n_trees(["a", "b"]);
881        let container = iac
882            .create_unpopulated(&O_K_IDENTITY, ComponentAllowlist::new_disabled(), &selectors)
883            .unwrap();
884        assert_eq!(0, container.inspect_handles.len());
885    }
886}