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