starnix_core/task/
container_namespace.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4use fidl::endpoints::create_endpoints;
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7use {fidl_fuchsia_component_runner as frunner, fidl_fuchsia_unknown as funknown};
8
9#[derive(Debug)]
10pub struct ContainerNamespace {
11    /// Internal collection of path to namespace entry.
12    namespace_entries: HashMap<PathBuf, funknown::CloneableSynchronousProxy>,
13}
14
15impl ContainerNamespace {
16    pub fn new() -> Self {
17        Self { namespace_entries: Default::default() }
18    }
19
20    /// Returns a bool indicating whether this ContainerNamespace contains a
21    /// channel entry corresponding to the given path.
22    pub fn has_channel_entry(&self, channel_path: impl AsRef<Path>) -> bool {
23        return self.namespace_entries.contains_key(channel_path.as_ref());
24    }
25
26    /// Attempts to find a channel at the given namespace path.
27    pub fn get_namespace_channel(
28        &self,
29        channel_path: impl AsRef<Path>,
30    ) -> Result<zx::Channel, anyhow::Error> {
31        let path = channel_path.as_ref();
32        if !path.is_absolute() {
33            anyhow::bail!(
34                "Invalid parameter provided to get_namespace_channel: {}",
35                path.display()
36            );
37        }
38
39        match self.namespace_entries.get(path) {
40            Some(cloneable_proxy) => {
41                let (cloned_client, cloned_server) =
42                    create_endpoints::<funknown::CloneableMarker>();
43                let clone_result = cloneable_proxy.clone(cloned_server);
44                if clone_result.is_err() {
45                    anyhow::bail!("Unable to clone the proxy channel for {}!", path.display())
46                }
47                Ok(cloned_client.into_channel())
48            }
49            None => anyhow::bail!("Could not find an entry for {}", path.display()),
50        }
51    }
52
53    /// Iterate backwards through the path ancestors, until we find a channel
54    /// which matches. This is needed since namespaces can be nested
55    /// (e.g. /foo/bar and /foo), so we should find the closest match to our
56    /// input query (e.g. /foo/bar/some should match /foo/bar). Returns the
57    /// namespace proxy which was found, and the remaining subdirectory paths.
58    /// For instance, if the input parameter is `/foo/bar/test` and we have a
59    /// namespace corresponding to `/foo/bar`, then this function will return
60    /// a proxy to `/foo/bar` and the remaining subdir `/test`.
61    pub fn find_closest_channel(
62        &self,
63        search_path: impl AsRef<Path>,
64    ) -> Result<(zx::Channel, String), anyhow::Error> {
65        let search_path = search_path.as_ref();
66        if !search_path.is_absolute() {
67            anyhow::bail!(
68                "Invalid parameter provided to find_closest_channel: {}",
69                search_path.display()
70            );
71        }
72
73        let mut root_channel = None;
74        let mut remaining_subdir = String::new();
75
76        let mut ns_path_ancestors = search_path.ancestors();
77        while let Some(path) = ns_path_ancestors.next() {
78            // If there is not an entry, we'll continue looking and prepend the
79            // last path segment as a remaining subdir.
80            if !self.has_channel_entry(path) {
81                let last_segment = path
82                    .components()
83                    .next_back()
84                    .and_then(|component| component.as_os_str().to_str())
85                    .unwrap_or("");
86                remaining_subdir.insert_str(0, &format!("{last_segment}/"));
87                continue;
88            }
89
90            // If we found an entry, we can save and halt the search.
91            root_channel = Some(self.get_namespace_channel(path)?);
92            break;
93        }
94
95        if let Some(channel) = root_channel {
96            // Trim the trailing `/` from the remaining subdirs.
97            remaining_subdir.pop();
98            Ok((channel, remaining_subdir))
99        } else {
100            anyhow::bail!("Unable to find a namespace corresponding to {}", search_path.display());
101        }
102    }
103
104    /// Attempts to clone this ContainerNamespace, returning an error if the
105    /// proxy cloning process failed.
106    pub fn try_clone(&self) -> Result<ContainerNamespace, anyhow::Error> {
107        let mut cloned_entries = HashMap::new();
108        for (path, _) in &self.namespace_entries {
109            match self.get_namespace_channel(path) {
110                Ok(cloned_channel) => {
111                    cloned_entries.insert(
112                        path.clone(),
113                        funknown::CloneableSynchronousProxy::new(cloned_channel),
114                    );
115                }
116                Err(err) => {
117                    anyhow::bail!(
118                        "The ContainerNamespace clone operation for {} has failed: {}",
119                        path.display(),
120                        err,
121                    )
122                }
123            }
124        }
125        Ok(ContainerNamespace { namespace_entries: cloned_entries })
126    }
127}
128
129impl From<Vec<frunner::ComponentNamespaceEntry>> for ContainerNamespace {
130    fn from(namespace: Vec<frunner::ComponentNamespaceEntry>) -> Self {
131        let mut namespace_entries = HashMap::new();
132        for mut entry in namespace {
133            if let (Some(entry_name), Some(entry_dir)) =
134                (entry.path.clone(), entry.directory.take())
135            {
136                let entry_channel = entry_dir.into_channel();
137                namespace_entries.insert(
138                    PathBuf::from(entry_name),
139                    funknown::CloneableSynchronousProxy::new(entry_channel),
140                );
141            }
142        }
143        ContainerNamespace { namespace_entries }
144    }
145}
146
147#[cfg(test)]
148mod test {
149    use super::*;
150    use fidl::endpoints::{ClientEnd, Proxy};
151    use fidl_fuchsia_io as fio;
152    use fuchsia_fs::directory;
153
154    #[::fuchsia::test]
155    fn correctly_reports_entries() {
156        // Initialize with only the /pkg channel.
157        let _stub_exec = fuchsia_async::TestExecutor::new();
158        let mut ns = Vec::<frunner::ComponentNamespaceEntry>::new();
159        let pkg_channel: zx::Channel =
160            directory::open_in_namespace("/pkg", fio::PERM_READABLE | fio::PERM_EXECUTABLE)
161                .expect("failed to open /pkg")
162                .into_channel()
163                .expect("into_channel")
164                .into();
165        let data_handle = ClientEnd::new(pkg_channel);
166        ns.push(frunner::ComponentNamespaceEntry {
167            path: Some("/pkg".to_string()),
168            directory: Some(data_handle),
169            ..Default::default()
170        });
171
172        // Assert that /pkg is reported, and /data is not.
173        let cn_under_test = ContainerNamespace::from(ns);
174        assert!(cn_under_test.has_channel_entry("/pkg"));
175        assert_eq!(cn_under_test.has_channel_entry("/data"), false);
176    }
177
178    #[::fuchsia::test]
179    fn correctly_provides_and_retains_channel_entries() {
180        // Initialize with only the /pkg channel.
181        let _stub_exec = fuchsia_async::TestExecutor::new();
182        let mut ns = Vec::<frunner::ComponentNamespaceEntry>::new();
183        let pkg_channel: zx::Channel =
184            directory::open_in_namespace("/pkg", fio::PERM_READABLE | fio::PERM_EXECUTABLE)
185                .expect("failed to open /pkg")
186                .into_channel()
187                .expect("into_channel")
188                .into();
189        let data_handle = ClientEnd::new(pkg_channel);
190        ns.push(frunner::ComponentNamespaceEntry {
191            path: Some("/pkg".to_string()),
192            directory: Some(data_handle),
193            ..Default::default()
194        });
195
196        // Assert that we can get a channel for /pkg, that the channel is valid,
197        // and that the ContainerNamespace still retains its own /pkg reference.
198        let cn_under_test = ContainerNamespace::from(ns);
199        let returned_channel = cn_under_test
200            .get_namespace_channel("/pkg")
201            .expect("get_namespace_channel should return a valid /pkg channel.");
202        assert!(returned_channel.write(b"hello", &mut vec![]).is_ok());
203        assert!(cn_under_test.has_channel_entry("/pkg"));
204    }
205
206    #[::fuchsia::test]
207    fn returns_err_on_invalid_request() {
208        let _stub_exec = fuchsia_async::TestExecutor::new();
209
210        // Initialize with no channels, and validate request fails.
211        let cn_under_test = ContainerNamespace::new();
212        assert!(cn_under_test.get_namespace_channel("/pkg").is_err());
213    }
214
215    #[::fuchsia::test]
216    fn correctly_returns_closest_channel_partial_match() {
217        // Initialize with only the /pkg channel.
218        let _stub_exec = fuchsia_async::TestExecutor::new();
219        let mut ns = Vec::<frunner::ComponentNamespaceEntry>::new();
220        let pkg_channel: zx::Channel =
221            directory::open_in_namespace("/pkg", fio::PERM_READABLE | fio::PERM_EXECUTABLE)
222                .expect("failed to open /pkg")
223                .into_channel()
224                .expect("into_channel")
225                .into();
226        let data_handle = ClientEnd::new(pkg_channel);
227        ns.push(frunner::ComponentNamespaceEntry {
228            path: Some("/pkg".to_string()),
229            directory: Some(data_handle),
230            ..Default::default()
231        });
232
233        // Assert that a request for an namespace extension channel
234        // (e.g. /pkg/foo/test) returns a channel for the root (e.g. /pkg).
235        let cn_under_test = ContainerNamespace::from(ns);
236        let (returned_channel, subdir) = cn_under_test
237            .find_closest_channel("/pkg/foo/bar")
238            .expect("get_namespace_channel should return a valid /pkg channel.");
239
240        // Assert that the channel is valid, and the ContainerNamespace retains
241        // a reference to the root channel as well.
242        assert!(returned_channel.write(b"hello", &mut vec![]).is_ok());
243        assert!(cn_under_test.has_channel_entry("/pkg"));
244
245        // Assert that the remaining subdir returned is correct.
246        assert_eq!(subdir, "foo/bar");
247    }
248
249    #[::fuchsia::test]
250    fn correctly_returns_closest_channel_exact_match() {
251        // Initialize with only the /pkg channel.
252        let _stub_exec = fuchsia_async::TestExecutor::new();
253        let mut ns = Vec::<frunner::ComponentNamespaceEntry>::new();
254        let pkg_channel: zx::Channel =
255            directory::open_in_namespace("/pkg", fio::PERM_READABLE | fio::PERM_EXECUTABLE)
256                .expect("failed to open /pkg")
257                .into_channel()
258                .expect("into_channel")
259                .into();
260        let data_handle = ClientEnd::new(pkg_channel);
261        ns.push(frunner::ComponentNamespaceEntry {
262            path: Some("/pkg".to_string()),
263            directory: Some(data_handle),
264            ..Default::default()
265        });
266
267        // Assert that a request for an exact namespace (e.g. /pkg)
268        // returns a channel for the namespace (e.g. /pkg).
269        let cn_under_test = ContainerNamespace::from(ns);
270        let (returned_channel, subdir) = cn_under_test
271            .find_closest_channel("/pkg")
272            .expect("get_namespace_channel should return a valid /pkg channel.");
273
274        // Assert that the channel is valid, and the ContainerNamespace retains
275        // a reference to the root channel as well.
276        assert!(returned_channel.write(b"hello", &mut vec![]).is_ok());
277        assert!(cn_under_test.has_channel_entry("/pkg"));
278
279        // Assert that the remaining subdir returned is correct.
280        assert_eq!(subdir, "");
281    }
282
283    #[::fuchsia::test]
284    fn correctly_clones() {
285        // Initialize with only the /pkg channel.
286        let _stub_exec = fuchsia_async::TestExecutor::new();
287        let mut ns = Vec::<frunner::ComponentNamespaceEntry>::new();
288        let pkg_channel: zx::Channel =
289            directory::open_in_namespace("/pkg", fio::PERM_READABLE | fio::PERM_EXECUTABLE)
290                .expect("failed to open /pkg")
291                .into_channel()
292                .expect("into_channel")
293                .into();
294        let data_handle = ClientEnd::new(pkg_channel);
295        ns.push(frunner::ComponentNamespaceEntry {
296            path: Some("/pkg".to_string()),
297            directory: Some(data_handle),
298            ..Default::default()
299        });
300
301        let cn_under_test = ContainerNamespace::from(ns);
302        let clone_under_test = cn_under_test.try_clone().expect("Clone should succeed.");
303
304        // Assert both original and clone contain /pkg channel references,
305        // and that those channels are both valid.
306        let original_channel = cn_under_test
307            .get_namespace_channel("/pkg")
308            .expect("get_namespace_channel should return a valid /pkg channel.");
309        let cloned_channel = clone_under_test
310            .get_namespace_channel("/pkg")
311            .expect("get_namespace_channel should return a valid /pkg channel.");
312        assert!(original_channel.write(b"hello", &mut vec![]).is_ok());
313        assert!(cloned_channel.write(b"hello", &mut vec![]).is_ok());
314
315        // Assert both original and clone retain their own references.
316        assert!(cn_under_test.has_channel_entry("/pkg"));
317        assert!(clone_under_test.has_channel_entry("/pkg"));
318    }
319}