Skip to main content

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 fidl_fuchsia_component_runtime::RouteRequest;
7use fidl_fuchsia_io as fio;
8use fuchsia_async as fasync;
9use futures::channel::mpsc::{UnboundedSender, unbounded};
10use namespace::{Entry as NamespaceEntry, EntryError, Namespace, NamespaceError, Tree};
11use router_error::Explain;
12use runtime_capabilities::{Capability, Dictionary, RemotableCapability, WeakInstanceToken};
13use thiserror::Error;
14use vfs::directory::entry::serve_directory;
15use vfs::execution_scope::ExecutionScope;
16
17/// A builder object for assembling a program's incoming namespace.
18pub struct NamespaceBuilder {
19    /// Mapping from namespace path to capabilities that can be turned into `Directory`.
20    entries: Tree<Capability>,
21
22    /// Path-not-found errors are sent here.
23    not_found: UnboundedSender<String>,
24
25    /// Scope in which the namespace vfs executes.
26    ///
27    /// This can be used to terminate the vfs.
28    namespace_scope: ExecutionScope,
29
30    /// The token for the component whose namespace this is. This is used for route attribution.
31    token: WeakInstanceToken,
32}
33
34#[derive(Error, Debug, Clone)]
35pub enum BuildNamespaceError {
36    #[error(transparent)]
37    NamespaceError(#[from] NamespaceError),
38
39    #[error(
40        "while installing capabilities within the namespace entry `{path}`, \
41        failed to convert the namespace entry to Directory: {err}"
42    )]
43    Conversion {
44        path: NamespacePath,
45        #[source]
46        err: runtime_capabilities::ConversionError,
47    },
48
49    #[error("unable to serve `{path}` after converting to directory: {err}")]
50    Serve {
51        path: NamespacePath,
52        #[source]
53        err: fidl::Status,
54    },
55}
56
57impl NamespaceBuilder {
58    pub fn new(
59        namespace_scope: ExecutionScope,
60        not_found: UnboundedSender<String>,
61        token: WeakInstanceToken,
62    ) -> Self {
63        return NamespaceBuilder { entries: Default::default(), not_found, namespace_scope, token };
64    }
65
66    /// Add a capability `cap` at `path`. As a result, the framework will create a
67    /// namespace entry at the parent directory of `path`.
68    pub fn add_object(
69        self: &mut Self,
70        cap: Capability,
71        path: &Path,
72    ) -> Result<(), BuildNamespaceError> {
73        let dirname = path.parent();
74
75        // Get the entry, or if it doesn't exist, make an empty dictionary.
76        let any = match self.entries.get_mut(&dirname) {
77            Some(dir) => dir,
78            None => {
79                let dict = self.make_dict_with_not_found_logging(dirname.to_string());
80                self.entries.add(&dirname, Capability::Dictionary(dict))?
81            }
82        };
83
84        // Cast the namespace entry as a Dict. This may fail if the user added a duplicate
85        // namespace entry that is not a Dict (see `add_entry`).
86        let dict = match any {
87            Capability::Dictionary(d) => d,
88            _ => Err(NamespaceError::Duplicate(path.clone().into()))?,
89        };
90
91        // Insert the capability into the Dict.
92        if dict.insert(path.basename().into(), cap).is_some() {
93            return Err(NamespaceError::Duplicate(path.clone().into()).into());
94        }
95        Ok(())
96    }
97
98    /// Add a capability `cap` at `path`. As a result, the framework will create a
99    /// namespace entry at `path` directly. The capability will be exercised when the user
100    /// opens the `path`.
101    pub fn add_entry(
102        self: &mut Self,
103        cap: Capability,
104        path: &NamespacePath,
105    ) -> Result<(), BuildNamespaceError> {
106        match &cap {
107            Capability::Dictionary(_)
108            | Capability::DirConnector(_)
109            | Capability::DirConnectorRouter(_)
110            | Capability::DictionaryRouter(_)
111            | Capability::Handle(_) => {}
112            _ => return Err(NamespaceError::EntryError(EntryError::UnsupportedType).into()),
113        }
114        self.entries.add(path, cap)?;
115        Ok(())
116    }
117
118    pub fn serve(self: Self) -> Result<Namespace, BuildNamespaceError> {
119        let mut entries = vec![];
120        for (path, cap) in self.entries.flatten() {
121            let client_end: ClientEnd<fio::DirectoryMarker> = match cap {
122                Capability::Handle(handle) => ClientEnd::new(fidl::handle::Channel::from(
123                    handle.take().expect("namespace handle already taken"),
124                )),
125                Capability::DirConnector(c) => {
126                    let (client, server) =
127                        fidl::endpoints::create_endpoints::<fio::DirectoryMarker>();
128                    // We don't wait to sit around for the results of this task, so we drop the
129                    // fuchsia_async::JoinHandle which causes the task to be detached.
130                    let _ = self.namespace_scope.spawn(async move {
131                        let res =
132                            fasync::OnSignals::new(&server, fidl::Signals::OBJECT_READABLE).await;
133                        if res.is_err() {
134                            return;
135                        }
136                        // We set the rights to `None` because no rights are given to us by the
137                        // client for this operation (opening a directory in their namespace). The
138                        // `DirConnector` should apply the "default" rights for this connection, as
139                        // determined by capability routing.
140                        let _ = c.send(server, RelativePath::dot(), None);
141                    });
142                    client
143                }
144                Capability::DirConnectorRouter(c) => {
145                    let (client, server) =
146                        fidl::endpoints::create_endpoints::<fio::DirectoryMarker>();
147                    // We don't wait to sit around for the results of this task, so we drop the
148                    // fuchsia_async::JoinHandle which causes the task to be detached.
149                    let token = self.token.clone();
150                    let _ = self.namespace_scope.spawn(async move {
151                        let res =
152                            fasync::OnSignals::new(&server, fidl::Signals::OBJECT_READABLE).await;
153                        if res.is_err() {
154                            return;
155                        }
156                        match c.route(RouteRequest::default(), token.clone()).await {
157                            Ok(Some(dir_connector)) => {
158                                // See the comment in the `DirConnector` branch for why rights are
159                                // `None`.
160                                let _ = dir_connector.send(server, RelativePath::dot(), None);
161                            }
162                            Ok(None) => {
163                                let _ = server.close_with_epitaph(fidl::Status::NOT_FOUND);
164                            }
165                            Err(e) => {
166                                // Error logging will be performed by the ErrorReporter router set
167                                // up by sandbox construction, so we don't need to log about
168                                // routing errors here.
169                                let _ = server.close_with_epitaph(e.as_zx_status());
170                            }
171                        }
172                    });
173                    client
174                }
175                Capability::Dictionary(dict) => {
176                    let entry = dict
177                        .try_into_directory_entry(self.namespace_scope.clone(), self.token.clone())
178                        .map_err(|err| BuildNamespaceError::Conversion {
179                            path: path.clone(),
180                            err,
181                        })?;
182                    if entry.entry_info().type_() != fio::DirentType::Directory {
183                        return Err(BuildNamespaceError::Conversion {
184                            path: path.clone(),
185                            err: runtime_capabilities::ConversionError::NotSupported,
186                        });
187                    }
188                    serve_directory(
189                        entry,
190                        &self.namespace_scope,
191                        fio::Flags::PROTOCOL_DIRECTORY
192                            | fio::PERM_READABLE
193                            | fio::Flags::PERM_INHERIT_WRITE
194                            | fio::Flags::PERM_INHERIT_EXECUTE,
195                    )
196                    .map_err(|err| BuildNamespaceError::Serve { path: path.clone(), err })?
197                }
198                Capability::DictionaryRouter(router) => {
199                    let (client, server) =
200                        fidl::endpoints::create_endpoints::<fio::DirectoryMarker>();
201                    // We don't wait to sit around for the results of this task, so we drop the
202                    // fuchsia_async::JoinHandle which causes the task to be detached.
203                    let path = path.clone();
204                    let scope = self.namespace_scope.clone();
205                    let token = self.token.clone();
206                    let _ = self.namespace_scope.spawn(async move {
207                        let res =
208                            fasync::OnSignals::new(&server, fidl::Signals::OBJECT_READABLE).await;
209                        if res.is_err() {
210                            return;
211                        }
212                        match router.route(RouteRequest::default(), token.clone()).await {
213                            Ok(Some(dictionary)) => {
214                                let entry = match dictionary.try_into_directory_entry(scope.clone(), token.clone()) {
215                                    Ok(entry) => entry,
216                                    Err(e) => {
217                                        log::error!("failed to convert namespace dictionary at path {path} into dir entry: {e:?}");
218                                        return;
219                                    }
220                                };
221                                let client_end = serve_directory(
222                                    entry,
223                                    &scope,
224                                    fio::Flags::PROTOCOL_DIRECTORY
225                                        | fio::PERM_READABLE
226                                        | fio::Flags::PERM_INHERIT_WRITE
227                                        | fio::Flags::PERM_INHERIT_EXECUTE,
228                                ).expect("failed to serve dictionary as directory");
229                                let proxy = client_end.into_proxy();
230                                fuchsia_fs::directory::clone_onto(&proxy, server).expect("failed to clone directory we are hosting");
231                            }
232                            Ok(None) => {
233                                let _ = server.close_with_epitaph(fidl::Status::NOT_FOUND);
234                            }
235                            Err(e) => {
236                                // Error logging will be performed by the ErrorReporter router set
237                                // up by sandbox construction, so we don't need to log about
238                                // routing errors here.
239                                let _ = server.close_with_epitaph(e.as_zx_status());
240                            }
241                        }
242                    });
243                    client
244                }
245                _ => return Err(NamespaceError::EntryError(EntryError::UnsupportedType).into()),
246            };
247            entries.push(NamespaceEntry { path, directory: client_end.into() })
248        }
249        let ns = entries.try_into()?;
250        Ok(ns)
251    }
252
253    fn make_dict_with_not_found_logging(&self, root_path: String) -> Dictionary {
254        let not_found = self.not_found.clone();
255        let new_dict = Dictionary::new_with_not_found(move |key| {
256            let requested_path = format!("{}/{}", root_path, key);
257            // Ignore the result of sending. The receiver is free to break away to ignore all the
258            // not-found errors.
259            let _ = not_found.unbounded_send(requested_path);
260        });
261        new_dict
262    }
263}
264
265/// Returns a disconnected sender which should ignore all the path-not-found errors.
266pub fn ignore_not_found() -> UnboundedSender<String> {
267    let (sender, _receiver) = unbounded();
268    sender
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274    use anyhow::Result;
275    use assert_matches::assert_matches;
276    use fidl::Peered;
277    use fidl::endpoints::{self, Proxy};
278    use fuchsia_fs::directory::DirEntry;
279    use futures::channel::mpsc;
280    use futures::{StreamExt, TryStreamExt};
281    use runtime_capabilities::{Connector, DirConnector, Receiver};
282    use std::sync::Arc;
283    use test_case::test_case;
284    use vfs::directory::entry::{DirectoryEntry, EntryInfo, GetEntryInfo, OpenRequest};
285    use vfs::remote::RemoteLike;
286    use vfs::{ObjectRequestRef, path, pseudo_directory};
287
288    use fidl_fuchsia_io as fio;
289    use 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 dir_connector_cap() -> Capability {
302        let fs = pseudo_directory! {};
303        DirConnector::from_directory_entry(fs, fio::PERM_READABLE).into()
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(dir_connector_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(dir_connector_cap(), &ns_path("/svc/a")).expect("");
366        // Adding again will fail.
367        assert_matches!(
368            namespace.add_entry(dir_connector_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(dir_connector_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(dir_connector_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(dir_connector_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 = receiver.receive().await.unwrap();
456        client_end.signal_peer(zx::Signals::empty(), zx::Signals::USER_0).unwrap();
457        server_end.wait_one(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) = runtime_capabilities::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 = DirConnector::from_directory_entry(fs, rights);
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 = DirConnector::from_directory_entry(fs, fio::PERM_READABLE);
669
670        let scope = ExecutionScope::new();
671        let mut namespace =
672            NamespaceBuilder::new(scope, ignore_not_found(), WeakInstanceToken::new_invalid());
673        namespace.add_entry(dir.into(), &ns_path("/dir")).unwrap();
674        let mut ns = namespace.serve().unwrap();
675        let dir_proxy = ns.remove(&"/dir".parse().unwrap()).unwrap();
676        let dir_proxy = dir_proxy.into_proxy();
677
678        // Try to open as executable. Should fail (ACCESS_DENIED)
679        let (node, server_end) = endpoints::create_endpoints::<fio::NodeMarker>();
680        dir_proxy
681            .open(
682                "foo",
683                fio::Flags::PROTOCOL_DIRECTORY | fio::PERM_READABLE | fio::PERM_EXECUTABLE,
684                &fio::Options::default(),
685                server_end.into_channel(),
686            )
687            .unwrap();
688        let node = node.into_proxy();
689        let mut node = node.take_event_stream();
690        assert_matches!(
691            node.try_next().await,
692            Err(fidl::Error::ClientChannelClosed { status: zx::Status::ACCESS_DENIED, .. })
693        );
694
695        // Try to open as read-only. Should succeed.
696        let (_, server_end) = endpoints::create_endpoints::<fio::NodeMarker>();
697        dir_proxy
698            .open(
699                "foo",
700                fio::Flags::PROTOCOL_DIRECTORY | fio::PERM_READABLE,
701                &fio::Options::default(),
702                server_end.into_channel(),
703            )
704            .unwrap();
705
706        // The MockDir should receive the Open request.
707        open_rx.next().await.unwrap();
708    }
709}