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