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