serve_processargs/
namespace.rs

1// Use of this source code is governed by a BSD-style license that can be
2// found in the LICENSE file.
3
4use cm_types::{NamespacePath, Path, RelativePath};
5use fidl::endpoints::ClientEnd;
6use futures::channel::mpsc::{UnboundedSender, unbounded};
7use namespace::{Entry as NamespaceEntry, EntryError, Namespace, NamespaceError, Tree};
8use router_error::Explain;
9use sandbox::{Capability, Dict, RemotableCapability, RouterResponse};
10use thiserror::Error;
11use vfs::directory::entry::serve_directory;
12use vfs::execution_scope::ExecutionScope;
13use {fidl_fuchsia_io as fio, fuchsia_async as fasync};
14
15/// A builder object for assembling a program's incoming namespace.
16pub struct NamespaceBuilder {
17    /// Mapping from namespace path to capabilities that can be turned into `Directory`.
18    entries: Tree<Capability>,
19
20    /// Path-not-found errors are sent here.
21    not_found: UnboundedSender<String>,
22
23    /// Scope in which the namespace vfs executes.
24    ///
25    /// This can be used to terminate the vfs.
26    namespace_scope: ExecutionScope,
27}
28
29#[derive(Error, Debug, Clone)]
30pub enum BuildNamespaceError {
31    #[error(transparent)]
32    NamespaceError(#[from] NamespaceError),
33
34    #[error(
35        "while installing capabilities within the namespace entry `{path}`, \
36        failed to convert the namespace entry to Directory: {err}"
37    )]
38    Conversion {
39        path: NamespacePath,
40        #[source]
41        err: sandbox::ConversionError,
42    },
43
44    #[error("unable to serve `{path}` after converting to directory: {err}")]
45    Serve {
46        path: NamespacePath,
47        #[source]
48        err: fidl::Status,
49    },
50}
51
52impl NamespaceBuilder {
53    pub fn new(namespace_scope: ExecutionScope, not_found: UnboundedSender<String>) -> Self {
54        return NamespaceBuilder { entries: Default::default(), not_found, namespace_scope };
55    }
56
57    /// Add a capability `cap` at `path`. As a result, the framework will create a
58    /// namespace entry at the parent directory of `path`.
59    pub fn add_object(
60        self: &mut Self,
61        cap: Capability,
62        path: &Path,
63    ) -> Result<(), BuildNamespaceError> {
64        let dirname = path.parent();
65
66        // Get the entry, or if it doesn't exist, make an empty dictionary.
67        let any = match self.entries.get_mut(&dirname) {
68            Some(dir) => dir,
69            None => {
70                let dict = self.make_dict_with_not_found_logging(dirname.to_string());
71                self.entries.add(&dirname, Capability::Dictionary(dict))?
72            }
73        };
74
75        // Cast the namespace entry as a Dict. This may fail if the user added a duplicate
76        // namespace entry that is not a Dict (see `add_entry`).
77        let dict = match any {
78            Capability::Dictionary(d) => d,
79            _ => Err(NamespaceError::Duplicate(path.clone().into()))?,
80        };
81
82        // Insert the capability into the Dict.
83        dict.insert(path.basename().into(), cap)
84            .map_err(|_| NamespaceError::Duplicate(path.clone().into()).into())
85    }
86
87    /// Add a capability `cap` at `path`. As a result, the framework will create a
88    /// namespace entry at `path` directly. The capability will be exercised when the user
89    /// opens the `path`.
90    pub fn add_entry(
91        self: &mut Self,
92        cap: Capability,
93        path: &NamespacePath,
94    ) -> Result<(), BuildNamespaceError> {
95        match &cap {
96            Capability::Directory(_)
97            | Capability::Dictionary(_)
98            | Capability::DirEntry(_)
99            | Capability::DirConnector(_)
100            | Capability::DirConnectorRouter(_)
101            | Capability::DictionaryRouter(_) => {}
102            _ => return Err(NamespaceError::EntryError(EntryError::UnsupportedType).into()),
103        }
104        self.entries.add(path, cap)?;
105        Ok(())
106    }
107
108    pub fn serve(self: Self) -> Result<Namespace, BuildNamespaceError> {
109        let mut entries = vec![];
110        for (path, cap) in self.entries.flatten() {
111            let client_end: ClientEnd<fio::DirectoryMarker> = match cap {
112                Capability::Directory(d) => d.into(),
113                Capability::DirConnector(c) => {
114                    let (client, server) =
115                        fidl::endpoints::create_endpoints::<fio::DirectoryMarker>();
116                    // We don't wait to sit around for the results of this task, so we drop the
117                    // fuchsia_async::JoinHandle which causes the task to be detached.
118                    let _ = self.namespace_scope.spawn(async move {
119                        let res =
120                            fasync::OnSignals::new(&server, fidl::Signals::OBJECT_READABLE).await;
121                        if res.is_err() {
122                            return;
123                        }
124                        // We set the rights to `None` because no rights are given to us by the
125                        // client for this operation (opening a directory in their namespace). The
126                        // `DirConnector` should apply the "default" rights for this connection, as
127                        // determined by capability routing.
128                        let _ = c.send(server, RelativePath::dot(), None);
129                    });
130                    client
131                }
132                Capability::DirConnectorRouter(c) => {
133                    let (client, server) =
134                        fidl::endpoints::create_endpoints::<fio::DirectoryMarker>();
135                    // We don't wait to sit around for the results of this task, so we drop the
136                    // fuchsia_async::JoinHandle which causes the task to be detached.
137                    let _ = self.namespace_scope.spawn(async move {
138                        let res =
139                            fasync::OnSignals::new(&server, fidl::Signals::OBJECT_READABLE).await;
140                        if res.is_err() {
141                            return;
142                        }
143                        match c.route(None, false).await {
144                            Ok(RouterResponse::Capability(dir_connector)) => {
145                                // See the comment in the `DirConnector` branch for why rights are
146                                // `None`.
147                                let _ = dir_connector.send(server, RelativePath::dot(), None);
148                            }
149                            Ok(RouterResponse::Unavailable) => {
150                                let _ = server.close_with_epitaph(fidl::Status::NOT_FOUND);
151                            }
152                            Ok(RouterResponse::Debug(_)) => {
153                                panic!("debug response wasn't requested");
154                            }
155                            Err(e) => {
156                                // Error logging will be performed by the ErrorReporter router set
157                                // up by sandbox construction, so we don't need to log about
158                                // routing errors here.
159                                let _ = server.close_with_epitaph(e.as_zx_status());
160                            }
161                        }
162                    });
163                    client
164                }
165                Capability::Dictionary(dict) => {
166                    let entry =
167                        dict.try_into_directory_entry(self.namespace_scope.clone()).map_err(
168                            |err| BuildNamespaceError::Conversion { path: path.clone(), err },
169                        )?;
170                    if entry.entry_info().type_() != fio::DirentType::Directory {
171                        return Err(BuildNamespaceError::Conversion {
172                            path: path.clone(),
173                            err: sandbox::ConversionError::NotSupported,
174                        });
175                    }
176                    serve_directory(
177                        entry,
178                        &self.namespace_scope,
179                        fio::Flags::PROTOCOL_DIRECTORY
180                            | fio::PERM_READABLE
181                            | fio::Flags::PERM_INHERIT_WRITE
182                            | fio::Flags::PERM_INHERIT_EXECUTE,
183                    )
184                    .map_err(|err| BuildNamespaceError::Serve { path: path.clone(), err })?
185                }
186                Capability::DictionaryRouter(router) => {
187                    let (client, server) =
188                        fidl::endpoints::create_endpoints::<fio::DirectoryMarker>();
189                    // We don't wait to sit around for the results of this task, so we drop the
190                    // fuchsia_async::JoinHandle which causes the task to be detached.
191                    let path = path.clone();
192                    let scope = self.namespace_scope.clone();
193                    let _ = self.namespace_scope.spawn(async move {
194                        let res =
195                            fasync::OnSignals::new(&server, fidl::Signals::OBJECT_READABLE).await;
196                        if res.is_err() {
197                            return;
198                        }
199                        match router.route(None, false).await {
200                            Ok(RouterResponse::Capability(dictionary)) => {
201                                let entry = match dictionary.try_into_directory_entry(scope.clone()) {
202                                    Ok(entry) => entry,
203                                    Err(e) => {
204                                        log::error!("failed to convert namespace dictionary at path {path} into dir entry: {e:?}");
205                                        return;
206                                    }
207                                };
208                                let client_end = serve_directory(
209                                    entry,
210                                    &scope,
211                                    fio::Flags::PROTOCOL_DIRECTORY
212                                        | fio::PERM_READABLE
213                                        | fio::Flags::PERM_INHERIT_WRITE
214                                        | fio::Flags::PERM_INHERIT_EXECUTE,
215                                ).expect("failed to serve dictionary as directory");
216                                let proxy = client_end.into_proxy();
217                                fuchsia_fs::directory::clone_onto(&proxy, server).expect("failed to clone directory we are hosting");
218                            }
219                            Ok(RouterResponse::Unavailable) => {
220                                let _ = server.close_with_epitaph(fidl::Status::NOT_FOUND);
221                            }
222                            Ok(RouterResponse::Debug(_)) => {
223                                panic!("debug response wasn't requested");
224                            }
225                            Err(e) => {
226                                // Error logging will be performed by the ErrorReporter router set
227                                // up by sandbox construction, so we don't need to log about
228                                // routing errors here.
229                                let _ = server.close_with_epitaph(e.as_zx_status());
230                            }
231                        }
232                    });
233                    client
234                }
235                _ => return Err(NamespaceError::EntryError(EntryError::UnsupportedType).into()),
236            };
237            entries.push(NamespaceEntry { path, directory: client_end.into() })
238        }
239        let ns = entries.try_into()?;
240        Ok(ns)
241    }
242
243    fn make_dict_with_not_found_logging(&self, root_path: String) -> Dict {
244        let not_found = self.not_found.clone();
245        let new_dict = Dict::new_with_not_found(move |key| {
246            let requested_path = format!("{}/{}", root_path, key);
247            // Ignore the result of sending. The receiver is free to break away to ignore all the
248            // not-found errors.
249            let _ = not_found.unbounded_send(requested_path);
250        });
251        new_dict
252    }
253}
254
255/// Returns a disconnected sender which should ignore all the path-not-found errors.
256pub fn ignore_not_found() -> UnboundedSender<String> {
257    let (sender, _receiver) = unbounded();
258    sender
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use anyhow::Result;
265    use assert_matches::assert_matches;
266    use fidl::Peered;
267    use fidl::endpoints::{self, Proxy};
268    use fuchsia_fs::directory::DirEntry;
269    use futures::channel::mpsc;
270    use futures::{StreamExt, TryStreamExt};
271    use sandbox::{Connector, Directory, Receiver};
272    use std::sync::Arc;
273    use test_case::test_case;
274    use vfs::directory::entry::{DirectoryEntry, EntryInfo, GetEntryInfo, OpenRequest};
275    use vfs::remote::RemoteLike;
276    use vfs::{ObjectRequestRef, path, pseudo_directory};
277    use zx::AsHandleRef;
278    use {fidl_fuchsia_io as fio, fuchsia_async as fasync};
279
280    fn multishot() -> (Connector, Receiver) {
281        let (receiver, sender) = Connector::new();
282        (sender, receiver)
283    }
284
285    fn connector_cap() -> Capability {
286        let (sender, _receiver) = multishot();
287        Capability::Connector(sender)
288    }
289
290    fn directory_cap() -> Capability {
291        let (client, _server) = endpoints::create_endpoints();
292        Capability::Directory(Directory::new(client))
293    }
294
295    fn ns_path(str: &str) -> NamespacePath {
296        str.parse().unwrap()
297    }
298
299    fn path(str: &str) -> Path {
300        str.parse().unwrap()
301    }
302
303    fn parents_valid(paths: Vec<&str>) -> Result<(), BuildNamespaceError> {
304        let scope = ExecutionScope::new();
305        let mut shadow = NamespaceBuilder::new(scope, ignore_not_found());
306        for p in paths {
307            shadow.add_object(connector_cap(), &path(p))?;
308        }
309        Ok(())
310    }
311
312    #[fuchsia::test]
313    async fn test_shadow() {
314        assert_matches!(parents_valid(vec!["/svc/foo/bar/Something", "/svc/Something"]), Err(_));
315        assert_matches!(parents_valid(vec!["/svc/Something", "/svc/foo/bar/Something"]), Err(_));
316        assert_matches!(parents_valid(vec!["/svc/Something", "/foo"]), Err(_));
317
318        assert_matches!(parents_valid(vec!["/foo/bar/a", "/foo/bar/b", "/foo/bar/c"]), Ok(()));
319        assert_matches!(parents_valid(vec!["/a", "/b", "/c"]), Ok(()));
320
321        let scope = ExecutionScope::new();
322        let mut shadow = NamespaceBuilder::new(scope, ignore_not_found());
323        shadow.add_object(connector_cap(), &path("/svc/foo")).unwrap();
324        assert_matches!(shadow.add_object(connector_cap(), &path("/svc/foo/bar")), Err(_));
325
326        let scope = ExecutionScope::new();
327        let mut not_shadow = NamespaceBuilder::new(scope, ignore_not_found());
328        not_shadow.add_object(connector_cap(), &path("/svc/foo")).unwrap();
329        assert_matches!(not_shadow.add_entry(directory_cap(), &ns_path("/svc2")), Ok(_));
330    }
331
332    #[fuchsia::test]
333    async fn test_duplicate_object() {
334        let scope = ExecutionScope::new();
335        let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
336        namespace.add_object(connector_cap(), &path("/svc/a")).expect("");
337        // Adding again will fail.
338        assert_matches!(
339            namespace.add_object(connector_cap(), &path("/svc/a")),
340            Err(BuildNamespaceError::NamespaceError(NamespaceError::Duplicate(path)))
341            if path.to_string() == "/svc/a"
342        );
343    }
344
345    #[fuchsia::test]
346    async fn test_duplicate_entry() {
347        let scope = ExecutionScope::new();
348        let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
349        namespace.add_entry(directory_cap(), &ns_path("/svc/a")).expect("");
350        // Adding again will fail.
351        assert_matches!(
352            namespace.add_entry(directory_cap(), &ns_path("/svc/a")),
353            Err(BuildNamespaceError::NamespaceError(NamespaceError::Duplicate(path)))
354            if path.to_string() == "/svc/a"
355        );
356    }
357
358    #[fuchsia::test]
359    async fn test_duplicate_object_and_entry() {
360        let scope = ExecutionScope::new();
361        let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
362        namespace.add_object(connector_cap(), &path("/svc/a")).expect("");
363        assert_matches!(
364            namespace.add_entry(directory_cap(), &ns_path("/svc/a")),
365            Err(BuildNamespaceError::NamespaceError(NamespaceError::Shadow(path)))
366            if path.to_string() == "/svc/a"
367        );
368    }
369
370    /// If we added a namespaced object at "/foo/bar", thus creating a namespace entry at "/foo",
371    /// we cannot add another entry directly at "/foo" again.
372    #[fuchsia::test]
373    async fn test_duplicate_entry_at_object_parent() {
374        let scope = ExecutionScope::new();
375        let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
376        namespace.add_object(connector_cap(), &path("/foo/bar")).expect("");
377        assert_matches!(
378            namespace.add_entry(directory_cap(), &ns_path("/foo")),
379            Err(BuildNamespaceError::NamespaceError(NamespaceError::Duplicate(path)))
380            if path.to_string() == "/foo"
381        );
382    }
383
384    /// If we directly added an entry at "/foo", it's not possible to add a namespaced object at
385    /// "/foo/bar", as that would've required overwriting "/foo" with a namespace entry served by
386    /// the framework.
387    #[fuchsia::test]
388    async fn test_duplicate_object_parent_at_entry() {
389        let scope = ExecutionScope::new();
390        let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
391        namespace.add_entry(directory_cap(), &ns_path("/foo")).expect("");
392        assert_matches!(
393            namespace.add_object(connector_cap(), &path("/foo/bar")),
394            Err(BuildNamespaceError::NamespaceError(NamespaceError::Duplicate(path)))
395            if path.to_string() == "/foo/bar"
396        );
397    }
398
399    #[fuchsia::test]
400    async fn test_empty() {
401        let scope = ExecutionScope::new();
402        let namespace = NamespaceBuilder::new(scope, ignore_not_found());
403        let ns = namespace.serve().unwrap();
404        assert_eq!(ns.flatten().len(), 0);
405    }
406
407    #[fuchsia::test]
408    async fn test_one_connector_end_to_end() {
409        let (sender, receiver) = multishot();
410
411        let scope = ExecutionScope::new();
412        let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
413        namespace.add_object(sender.into(), &path("/svc/a")).unwrap();
414        let ns = namespace.serve().unwrap();
415
416        let mut ns = ns.flatten();
417        assert_eq!(ns.len(), 1);
418        assert_eq!(ns[0].path.to_string(), "/svc");
419
420        // Check that there is exactly one protocol inside the svc directory.
421        let dir = ns.pop().unwrap().directory.into_proxy();
422        let entries = fuchsia_fs::directory::readdir(&dir).await.unwrap();
423        assert_eq!(
424            entries,
425            vec![DirEntry { name: "a".to_string(), kind: fio::DirentType::Service }]
426        );
427
428        // Connect to the protocol using namespace functionality.
429        let (client_end, server_end) = zx::Channel::create();
430        fdio::service_connect_at(&dir.into_channel().unwrap().into_zx_channel(), "a", server_end)
431            .unwrap();
432
433        // Make sure the server_end is received, and test connectivity.
434        let server_end: zx::Channel = receiver.receive().await.unwrap().channel.into();
435        client_end.signal_peer(zx::Signals::empty(), zx::Signals::USER_0).unwrap();
436        server_end.wait_handle(zx::Signals::USER_0, zx::MonotonicInstant::INFINITE_PAST).unwrap();
437    }
438
439    #[fuchsia::test]
440    async fn test_two_connectors_in_same_namespace_entry() {
441        let scope = ExecutionScope::new();
442        let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
443        namespace.add_object(connector_cap(), &path("/svc/a")).unwrap();
444        namespace.add_object(connector_cap(), &path("/svc/b")).unwrap();
445        let ns = namespace.serve().unwrap();
446
447        let mut ns = ns.flatten();
448        assert_eq!(ns.len(), 1);
449        assert_eq!(ns[0].path.to_string(), "/svc");
450
451        // Check that there are exactly two protocols inside the svc directory.
452        let dir = ns.pop().unwrap().directory.into_proxy();
453        let mut entries = fuchsia_fs::directory::readdir(&dir).await.unwrap();
454        let mut expectation = vec![
455            DirEntry { name: "a".to_string(), kind: fio::DirentType::Service },
456            DirEntry { name: "b".to_string(), kind: fio::DirentType::Service },
457        ];
458        entries.sort();
459        expectation.sort();
460        assert_eq!(entries, expectation);
461
462        drop(dir);
463    }
464
465    #[fuchsia::test]
466    async fn test_two_connectors_in_different_namespace_entries() {
467        let scope = ExecutionScope::new();
468        let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
469        namespace.add_object(connector_cap(), &path("/svc1/a")).unwrap();
470        namespace.add_object(connector_cap(), &path("/svc2/b")).unwrap();
471        let ns = namespace.serve().unwrap();
472
473        let ns = ns.flatten();
474        assert_eq!(ns.len(), 2);
475        let (mut svc1, ns): (Vec<_>, Vec<_>) =
476            ns.into_iter().partition(|e| e.path.to_string() == "/svc1");
477        let (mut svc2, _ns): (Vec<_>, Vec<_>) =
478            ns.into_iter().partition(|e| e.path.to_string() == "/svc2");
479
480        // Check that there are one protocol inside each directory.
481        {
482            let dir = svc1.pop().unwrap().directory.into_proxy();
483            assert_eq!(
484                fuchsia_fs::directory::readdir(&dir).await.unwrap(),
485                vec![DirEntry { name: "a".to_string(), kind: fio::DirentType::Service },]
486            );
487        }
488        {
489            let dir = svc2.pop().unwrap().directory.into_proxy();
490            assert_eq!(
491                fuchsia_fs::directory::readdir(&dir).await.unwrap(),
492                vec![DirEntry { name: "b".to_string(), kind: fio::DirentType::Service },]
493            );
494        }
495
496        drop(svc1);
497        drop(svc2);
498    }
499
500    #[fuchsia::test]
501    async fn test_not_found() {
502        let (not_found_sender, mut not_found_receiver) = unbounded();
503        let scope = ExecutionScope::new();
504        let mut namespace = NamespaceBuilder::new(scope, not_found_sender);
505        namespace.add_object(connector_cap(), &path("/svc/a")).unwrap();
506        let ns = namespace.serve().unwrap();
507
508        let mut ns = ns.flatten();
509        assert_eq!(ns.len(), 1);
510        assert_eq!(ns[0].path.to_string(), "/svc");
511
512        let dir = ns.pop().unwrap().directory.into_proxy();
513        let (client_end, server_end) = zx::Channel::create();
514        let _ = fdio::service_connect_at(
515            &dir.into_channel().unwrap().into_zx_channel(),
516            "non_existent",
517            server_end,
518        );
519
520        // Server endpoint is closed because the path does not exist.
521        fasync::Channel::from_channel(client_end).on_closed().await.unwrap();
522
523        // We should get a notification about this path.
524        assert_eq!(not_found_receiver.next().await, Some("/svc/non_existent".to_string()));
525
526        drop(ns);
527    }
528
529    #[fuchsia::test]
530    async fn test_not_directory() {
531        let (not_found_sender, _) = unbounded();
532        let scope = ExecutionScope::new();
533        let mut namespace = NamespaceBuilder::new(scope, not_found_sender);
534        let (_, sender) = sandbox::Connector::new();
535        assert_matches!(
536            namespace.add_entry(sender.into(), &ns_path("/a")),
537            Err(BuildNamespaceError::NamespaceError(NamespaceError::EntryError(
538                EntryError::UnsupportedType
539            )))
540        );
541    }
542
543    #[test_case(fio::PERM_READABLE)]
544    #[test_case(fio::PERM_READABLE | fio::PERM_EXECUTABLE)]
545    #[test_case(fio::PERM_READABLE | fio::PERM_WRITABLE)]
546    #[test_case(fio::PERM_READABLE | fio::Flags::PERM_INHERIT_WRITE | fio::Flags::PERM_INHERIT_EXECUTE)]
547    #[fuchsia::test]
548    async fn test_directory_rights(rights: fio::Flags) {
549        let (open_tx, mut open_rx) = mpsc::channel::<()>(1);
550
551        struct MockDir {
552            tx: mpsc::Sender<()>,
553            rights: fio::Flags,
554        }
555        impl DirectoryEntry for MockDir {
556            fn open_entry(self: Arc<Self>, request: OpenRequest<'_>) -> Result<(), zx::Status> {
557                request.open_remote(self)
558            }
559        }
560        impl GetEntryInfo for MockDir {
561            fn entry_info(&self) -> EntryInfo {
562                EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
563            }
564        }
565        impl RemoteLike for MockDir {
566            fn open(
567                self: Arc<Self>,
568                _scope: ExecutionScope,
569                relative_path: path::Path,
570                flags: fio::Flags,
571                _object_request: ObjectRequestRef<'_>,
572            ) -> Result<(), zx::Status> {
573                assert_eq!(relative_path.into_string(), "");
574                assert_eq!(flags, fio::Flags::PROTOCOL_DIRECTORY | self.rights);
575                self.tx.clone().try_send(()).unwrap();
576                Ok(())
577            }
578        }
579
580        let mock = Arc::new(MockDir { tx: open_tx, rights });
581
582        let fs = pseudo_directory! {
583            "foo" => mock,
584        };
585        let dir = Directory::from(vfs::directory::serve(fs, rights).into_client_end().unwrap());
586
587        let scope = ExecutionScope::new();
588        let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
589        namespace.add_entry(dir.into(), &ns_path("/dir")).unwrap();
590        let mut ns = namespace.serve().unwrap();
591        let dir_proxy = ns.remove(&"/dir".parse().unwrap()).unwrap();
592        let dir_proxy = dir_proxy.into_proxy();
593        let (_, server_end) = endpoints::create_endpoints::<fio::NodeMarker>();
594        dir_proxy
595            .open(
596                "foo",
597                fio::Flags::PROTOCOL_DIRECTORY | rights,
598                &fio::Options::default(),
599                server_end.into_channel(),
600            )
601            .unwrap();
602
603        // The MockDir should receive the Open request.
604        open_rx.next().await.unwrap();
605    }
606
607    #[fuchsia::test]
608    async fn test_directory_non_executable() {
609        let (open_tx, mut open_rx) = mpsc::channel::<()>(1);
610
611        struct MockDir(mpsc::Sender<()>);
612        impl DirectoryEntry for MockDir {
613            fn open_entry(self: Arc<Self>, request: OpenRequest<'_>) -> Result<(), zx::Status> {
614                request.open_remote(self)
615            }
616        }
617        impl GetEntryInfo for MockDir {
618            fn entry_info(&self) -> EntryInfo {
619                EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
620            }
621        }
622        impl RemoteLike for MockDir {
623            fn open(
624                self: Arc<Self>,
625                _scope: ExecutionScope,
626                relative_path: path::Path,
627                flags: fio::Flags,
628                _object_request: ObjectRequestRef<'_>,
629            ) -> Result<(), zx::Status> {
630                assert_eq!(relative_path.into_string(), "");
631                assert_eq!(flags, fio::Flags::PROTOCOL_DIRECTORY | fio::PERM_READABLE);
632                self.0.clone().try_send(()).unwrap();
633                Ok(())
634            }
635        }
636
637        let mock = Arc::new(MockDir(open_tx));
638
639        let fs = pseudo_directory! {
640            "foo" => mock,
641        };
642        let dir = Directory::from(
643            vfs::directory::serve(fs, fio::PERM_READABLE).into_client_end().unwrap(),
644        );
645
646        let scope = ExecutionScope::new();
647        let mut namespace = NamespaceBuilder::new(scope, ignore_not_found());
648        namespace.add_entry(dir.into(), &ns_path("/dir")).unwrap();
649        let mut ns = namespace.serve().unwrap();
650        let dir_proxy = ns.remove(&"/dir".parse().unwrap()).unwrap();
651        let dir_proxy = dir_proxy.into_proxy();
652
653        // Try to open as executable. Should fail (ACCESS_DENIED)
654        let (node, server_end) = endpoints::create_endpoints::<fio::NodeMarker>();
655        dir_proxy
656            .open(
657                "foo",
658                fio::Flags::PROTOCOL_DIRECTORY | fio::PERM_READABLE | fio::PERM_EXECUTABLE,
659                &fio::Options::default(),
660                server_end.into_channel(),
661            )
662            .unwrap();
663        let node = node.into_proxy();
664        let mut node = node.take_event_stream();
665        assert_matches!(
666            node.try_next().await,
667            Err(fidl::Error::ClientChannelClosed { status: zx::Status::ACCESS_DENIED, .. })
668        );
669
670        // Try to open as read-only. Should succeed.
671        let (_, server_end) = endpoints::create_endpoints::<fio::NodeMarker>();
672        dir_proxy
673            .open(
674                "foo",
675                fio::Flags::PROTOCOL_DIRECTORY | fio::PERM_READABLE,
676                &fio::Options::default(),
677                server_end.into_channel(),
678            )
679            .unwrap();
680
681        // The MockDir should receive the Open request.
682        open_rx.next().await.unwrap();
683    }
684}